// lib/editor/canvas/input_diagnostics.dart // // Live zoom/pan diagnostics for the pen canvas. PenInteractiveViewer records one // entry per scale-update frame; the editor's diagnostic overlay displays the // accumulated summary + a rolling trace so a SINGLE device session reveals the // nature of any "跳变" (is it a raw-scale spike, a focal/position jump, or a // pointer-count oscillation?). All numbers reset via [reset]. import 'package:flutter/foundation.dart'; import '../../diagnostics/frame_sampler.dart'; import '../input/diagnostic_logger.dart'; class InputDiagnostics extends ChangeNotifier { InputDiagnostics._(); static final InputDiagnostics instance = InputDiagnostics._(); int frames = 0; int scaleDropped = 0; // frames HARD-rejected (legacy; prefer soft-clamp) int softClamped = 0; // frames whose step was soft-clamped (still applied) int focalDropped = 0; // frames rejected as a focal/position glitch int rebaselines = 0; // pointer-count re-baselines int pointerCountMax = 0; double rawScaleMin = double.infinity, rawScaleMax = 0; double scaleMin = double.infinity, scaleMax = 0; double maxFocalJumpPx = 0; // largest single-frame local focal delta double maxAppliedScaleJump = 1; // largest single-frame applied scale ratio final List _trace = []; List get trace => List.unmodifiable(_trace); void recordRebaseline() { rebaselines++; DiagnosticLogger.instance.log('ZOOM rebaseline'); notifyListeners(); } void recordScaleFrame({ required double rawScale, required int pointerCount, required double currentScale, required double appliedChange, // 1.0 when the frame was dropped required double focalJumpPx, required bool scaleDrop, required bool focalDrop, bool softClamped = false, double? liveScale, }) { frames++; if (scaleDrop) scaleDropped++; if (softClamped) this.softClamped++; if (focalDrop) focalDropped++; if (rawScale < rawScaleMin) rawScaleMin = rawScale; if (rawScale > rawScaleMax) rawScaleMax = rawScale; final double resulting = currentScale; if (resulting < scaleMin) scaleMin = resulting; if (resulting > scaleMax) scaleMax = resulting; if (pointerCount > pointerCountMax) pointerCountMax = pointerCount; if (focalJumpPx > maxFocalJumpPx) maxFocalJumpPx = focalJumpPx; final double jump = appliedChange >= 1 ? appliedChange : 1 / appliedChange; if (jump > maxAppliedScaleJump) maxAppliedScaleJump = jump; final liveBit = liveScale == null ? '' : ' live=${liveScale.toStringAsFixed(3)}'; final String line = 'p$pointerCount raw=${rawScale.toStringAsFixed(3)} ' 'ch=${appliedChange.toStringAsFixed(3)} ' 'cur=${currentScale.toStringAsFixed(3)}$liveBit ' 'fj=${focalJumpPx.toStringAsFixed(0)}' '${softClamped ? " SCLAMP" : ""}' '${scaleDrop ? " SDROP" : ""}' '${focalDrop ? " FDROP" : ""}'; _trace.add(line); if (_trace.length > 24) _trace.removeAt(0); FrameSampler.instance.recordZoom( rawScale: rawScale, scaleDrop: scaleDrop || softClamped, focalDrop: focalDrop, focalJumpPx: focalJumpPx, ); DiagnosticLogger.instance.log('ZOOM $line'); notifyListeners(); } void reset() { frames = scaleDropped = softClamped = focalDropped = rebaselines = pointerCountMax = 0; rawScaleMin = scaleMin = double.infinity; rawScaleMax = scaleMax = 0; maxFocalJumpPx = 0; maxAppliedScaleJump = 1; _trace.clear(); notifyListeners(); } String summary() { final rawLo = rawScaleMin.isFinite ? rawScaleMin.toStringAsFixed(2) : '-'; final rawHi = rawScaleMax > 0 ? rawScaleMax.toStringAsFixed(2) : '-'; final scLo = scaleMin.isFinite ? scaleMin.toStringAsFixed(2) : '-'; final scHi = scaleMax > 0 ? scaleMax.toStringAsFixed(2) : '-'; return 'zoom f=$frames sDrop=$scaleDropped sClamp=$softClamped ' 'fDrop=$focalDropped rebase=$rebaselines pMax=$pointerCountMax\n' ' raw=$rawLo..$rawHi scale=$scLo..$scHi\n' ' maxFocalJump=${maxFocalJumpPx.toStringAsFixed(0)}px ' 'maxScaleJump=${maxAppliedScaleJump.toStringAsFixed(2)}'; } }