From 6ff0deb17f7f52bc35c8c115d96aefd804a77ad2 Mon Sep 17 00:00:00 2001 From: Thomas Burkhart Date: Mon, 15 Dec 2025 20:10:22 -0500 Subject: [PATCH] Add viewTransform support for external pan/zoom integration This feature allows Scribble to work seamlessly with external pan/zoom controllers (like InteractiveViewer) by transforming coordinates internally rather than relying on widget-level transforms. This solves hit-testing issues that occur when using Transform widgets for zoom. ## Changes ### ScribbleState - Added `viewTransform: Matrix4?` field to both Drawing and Erasing states - Field is excluded from JSON serialization (runtime-only value) ### ScribbleNotifier - Added `setViewTransform(Matrix4? transform)` method - `_getPointFromEvent()` now transforms coordinates using inverse viewTransform - `_erasePoint()` also transforms coordinates for consistent erasing - When viewTransform is set, lines are stored with visual width (not divided by scaleFactor) to maintain constant thickness regardless of zoom ### Painters (ScribblePainter, ScribbleEditingPainter) - Apply viewTransform to canvas before rendering - Use `1/scaleFactor` for line width when viewTransform is set, so after canvas transform, lines appear at their intended visual width - Lines maintain constant visual thickness at any zoom level ### Dependencies - Added vector_math to dependencies for Matrix4/Vector3 operations ## Usage ```dart // In your pan/zoom controller callback: _transformController.addListener(() { final transform = _transformController.value; _scribbleNotifier.setViewTransform(transform); _scribbleNotifier.setScaleFactor(transform.getMaxScaleOnAxis()); }); ``` ## How it works 1. Scribble stays viewport-sized (always receives all touches) 2. Pointer coordinates are transformed using inverse matrix before storing 3. Canvas applies viewTransform when painting 4. Lines maintain constant visual thickness regardless of zoom level --- lib/src/view/notifier/scribble_notifier.dart | 86 ++++++- .../painting/scribble_editing_painter.dart | 23 +- lib/src/view/painting/scribble_painter.dart | 27 +- lib/src/view/scribble.dart | 62 +++-- lib/src/view/state/scribble.state.dart | 36 +++ .../view/state/scribble.state.freezed.dart | 230 +++++++++++++++--- lib/src/view/state/scribble.state.g.dart | 1 + pubspec.yaml | 1 + 8 files changed, 399 insertions(+), 67 deletions(-) diff --git a/lib/src/view/notifier/scribble_notifier.dart b/lib/src/view/notifier/scribble_notifier.dart index 748a197..34e0bc4 100644 --- a/lib/src/view/notifier/scribble_notifier.dart +++ b/lib/src/view/notifier/scribble_notifier.dart @@ -8,6 +8,7 @@ import 'package:scribble/scribble.dart'; import 'package:scribble/src/view/painting/point_to_offset_x.dart'; import 'package:scribble/src/view/simplification/sketch_simplifier.dart'; import 'package:value_notifier_tools/value_notifier_tools.dart'; +import 'package:vector_math/vector_math_64.dart' show Vector3; /// {@template scribble_notifier_base} /// The base class for a notifier that controls the state of a [Scribble] @@ -225,6 +226,20 @@ class ScribbleNotifier extends ScribbleNotifierBase ); } + /// Sets the view transform matrix for pan/zoom functionality. + /// + /// When set, pointer coordinates are transformed using the inverse of this + /// matrix before being stored, and the sketch is rendered using this + /// transform. This allows Scribble to work seamlessly with external pan/zoom + /// controllers like InteractiveViewer without hit-testing issues. + /// + /// Set to `null` to disable view transformation. + void setViewTransform(Matrix4? transform) { + temporaryValue = value.copyWith( + viewTransform: transform, + ); + } + /// Sets the color of the pen to the given color. void setColor(Color color) { temporaryValue = value.map( @@ -293,6 +308,17 @@ class ScribbleNotifier extends ScribbleNotifierBase @override void onPointerDown(PointerDownEvent event) { if (!value.supportedPointerKinds.contains(event.kind)) return; + + // For singleTouchOnly mode, ignore additional touch pointers but allow + // mouse and stylus to work regardless + if (value.allowedPointersMode == ScribblePointerMode.singleTouchOnly && + value.activePointerIds.isNotEmpty && + event.kind == PointerDeviceKind.touch) { + // Don't capture this pointer - let it pass through to parent widgets + // (e.g., InteractiveViewer for pan/zoom) + return; + } + var s = value; // Are there already pointers on the screen? @@ -308,12 +334,18 @@ class ScribbleNotifier extends ScribbleNotifierBase erasing: (s) => s, ); } else if (value is Drawing) { + // When viewTransform is used, store the intended visual width so lines + // maintain constant thickness regardless of zoom level. + // Otherwise use canvas-coordinate width for backwards compatibility. + final lineWidth = value.viewTransform != null + ? value.selectedWidth + : value.selectedWidth / value.scaleFactor; s = (value as Drawing).copyWith( pointerPosition: _getPointFromEvent(event), activeLine: SketchLine( points: [_getPointFromEvent(event)], color: (value as Drawing).selectedColor, - width: value.selectedWidth / value.scaleFactor, + width: lineWidth, ), ); } @@ -326,6 +358,13 @@ class ScribbleNotifier extends ScribbleNotifierBase @override void onPointerUpdate(PointerMoveEvent event) { if (!value.supportedPointerKinds.contains(event.kind)) return; + + // For singleTouchOnly, ignore updates for pointers we didn't track + if (value.allowedPointersMode == ScribblePointerMode.singleTouchOnly && + !value.activePointerIds.contains(event.pointer)) { + return; + } + if (!value.active) { temporaryValue = value.copyWith( pointerPosition: null, @@ -357,6 +396,13 @@ class ScribbleNotifier extends ScribbleNotifierBase @override void onPointerUp(PointerUpEvent event) { if (!value.supportedPointerKinds.contains(event.kind)) return; + + // For singleTouchOnly, ignore events for pointers we didn't track + if (value.allowedPointersMode == ScribblePointerMode.singleTouchOnly && + !value.activePointerIds.contains(event.pointer)) { + return; + } + final pos = event.kind == PointerDeviceKind.mouse ? value.pointerPosition : null; if (value is Drawing) { @@ -391,6 +437,13 @@ class ScribbleNotifier extends ScribbleNotifierBase @override void onPointerCancel(PointerCancelEvent event) { if (!value.supportedPointerKinds.contains(event.kind)) return; + + // For singleTouchOnly, ignore events for pointers we didn't track + if (value.allowedPointersMode == ScribblePointerMode.singleTouchOnly && + !value.activePointerIds.contains(event.pointer)) { + return; + } + if (value is Drawing) { value = _finishLineForState(_addPoint(event, value)).copyWith( pointerPosition: null, @@ -448,11 +501,21 @@ class ScribbleNotifier extends ScribbleNotifierBase } ScribbleState? _erasePoint(PointerEvent event) { + // Get transformed position for erasing + var position = event.localPosition; + final viewTransform = value.viewTransform; + if (viewTransform != null) { + final inverse = Matrix4.inverted(viewTransform); + final transformed = + inverse.transform3(Vector3(position.dx, position.dy, 0)); + position = Offset(transformed.x, transformed.y); + } + final filteredLines = value.sketch.lines .where( (l) => l.points.every( (p) => - (event.localPosition - p.asOffset).distance > + (position - p.asOffset).distance > l.width + value.selectedWidth, ), ) @@ -470,14 +533,29 @@ class ScribbleNotifier extends ScribbleNotifierBase } /// Converts a pointer event to the [Point] on the canvas. + /// + /// When a viewTransform is set, the coordinates are transformed using the + /// inverse of that matrix to convert screen coordinates to canvas + /// coordinates. Point _getPointFromEvent(PointerEvent event) { + var position = event.localPosition; + + // Apply inverse view transform to get canvas coordinates + final viewTransform = value.viewTransform; + if (viewTransform != null) { + final inverse = Matrix4.inverted(viewTransform); + final transformed = + inverse.transform3(Vector3(position.dx, position.dy, 0)); + position = Offset(transformed.x, transformed.y); + } + final p = event.pressureMin == event.pressureMax ? 0.5 : (event.pressure - event.pressureMin) / (event.pressureMax - event.pressureMin); return Point( - event.localPosition.dx, - event.localPosition.dy, + position.dx, + position.dy, pressure: pressureCurve.transform(p), ); } diff --git a/lib/src/view/painting/scribble_editing_painter.dart b/lib/src/view/painting/scribble_editing_painter.dart index d53748a..a6ee03f 100644 --- a/lib/src/view/painting/scribble_editing_painter.dart +++ b/lib/src/view/painting/scribble_editing_painter.dart @@ -35,6 +35,20 @@ class ScribbleEditingPainter extends CustomPainter with SketchLinePathMixin { @override void paint(Canvas canvas, Size size) { + // Apply view transform if set + final viewTransform = state.viewTransform; + if (viewTransform != null) { + canvas.save(); + canvas.transform(viewTransform.storage); + } + + // When viewTransform is applied, lines are stored with their intended + // visual width. We pass 1/scaleFactor so that after canvas transform + // (which scales by scaleFactor), lines appear at their stored visual width. + // This ensures constant line thickness regardless of zoom level. + final effectiveScaleFactor = + viewTransform != null ? 1.0 / state.scaleFactor : state.scaleFactor; + final paint = Paint()..style = PaintingStyle.fill; final activeLine = state.map( @@ -44,7 +58,7 @@ class ScribbleEditingPainter extends CustomPainter with SketchLinePathMixin { if (activeLine != null) { final path = getPathForLine( activeLine, - scaleFactor: state.scaleFactor, + scaleFactor: effectiveScaleFactor, ); if (path != null) { paint.color = Color(activeLine.color); @@ -66,10 +80,15 @@ class ScribbleEditingPainter extends CustomPainter with SketchLinePathMixin { ..strokeWidth = 1; canvas.drawCircle( state.pointerPosition!.asOffset, - state.selectedWidth / state.scaleFactor, + state.selectedWidth / effectiveScaleFactor, paint, ); } + + // Restore canvas if we applied a transform + if (viewTransform != null) { + canvas.restore(); + } } @override diff --git a/lib/src/view/painting/scribble_painter.dart b/lib/src/view/painting/scribble_painter.dart index 177068f..4b0bff9 100644 --- a/lib/src/view/painting/scribble_painter.dart +++ b/lib/src/view/painting/scribble_painter.dart @@ -9,6 +9,7 @@ class ScribblePainter extends CustomPainter with SketchLinePathMixin { required this.sketch, required this.scaleFactor, required this.simulatePressure, + this.viewTransform, }); /// The [Sketch] to draw. @@ -17,17 +18,33 @@ class ScribblePainter extends CustomPainter with SketchLinePathMixin { /// {@macro view.state.scribble_state.scale_factor} final double scaleFactor; + /// {@macro view.state.scribble_state.view_transform} + final Matrix4? viewTransform; + @override final bool simulatePressure; @override void paint(Canvas canvas, Size size) { + // Apply view transform if set + if (viewTransform != null) { + canvas.save(); + canvas.transform(viewTransform!.storage); + } + + // When viewTransform is applied, lines are stored with their intended + // visual width. We pass 1/scaleFactor so that after canvas transform + // (which scales by scaleFactor), lines appear at their stored visual width. + // This ensures constant line thickness regardless of zoom level. + final effectiveScaleFactor = + viewTransform != null ? 1.0 / scaleFactor : scaleFactor; + final paint = Paint()..style = PaintingStyle.fill; for (var i = 0; i < sketch.lines.length; ++i) { final path = getPathForLine( sketch.lines[i], - scaleFactor: scaleFactor, + scaleFactor: effectiveScaleFactor, ); if (path == null) { continue; @@ -35,12 +52,18 @@ class ScribblePainter extends CustomPainter with SketchLinePathMixin { paint.color = Color(sketch.lines[i].color); canvas.drawPath(path, paint); } + + // Restore canvas if we applied a transform + if (viewTransform != null) { + canvas.restore(); + } } @override bool shouldRepaint(ScribblePainter oldDelegate) { return oldDelegate.sketch != sketch || oldDelegate.simulatePressure != simulatePressure || - oldDelegate.scaleFactor != scaleFactor; + oldDelegate.scaleFactor != scaleFactor || + oldDelegate.viewTransform != viewTransform; } } diff --git a/lib/src/view/scribble.dart b/lib/src/view/scribble.dart index 75d523e..e40e1b0 100644 --- a/lib/src/view/scribble.dart +++ b/lib/src/view/scribble.dart @@ -68,32 +68,52 @@ class Scribble extends StatelessWidget { sketch: state.sketch, scaleFactor: state.scaleFactor, simulatePressure: simulatePressure, + viewTransform: state.viewTransform, ), ), ), ), ); - return !state.active - ? child - : GestureCatcher( - pointerKindsToCatch: state.supportedPointerKinds, - child: MouseRegion( - cursor: drawCurrentTool && - state.supportedPointerKinds - .contains(PointerDeviceKind.mouse) - ? SystemMouseCursors.none - : MouseCursor.defer, - onExit: notifier.onPointerExit, - child: Listener( - onPointerDown: notifier.onPointerDown, - onPointerMove: notifier.onPointerUpdate, - onPointerUp: notifier.onPointerUp, - onPointerHover: notifier.onPointerHover, - onPointerCancel: notifier.onPointerCancel, - child: child, - ), - ), - ); + // For singleTouchOnly mode, don't use GestureCatcher so multi-touch + // events can bubble up to parent widgets (e.g., InteractiveViewer) + final isSingleTouchOnly = + state.allowedPointersMode == ScribblePointerMode.singleTouchOnly; + + final listenerWidget = MouseRegion( + cursor: drawCurrentTool && + state.supportedPointerKinds.contains(PointerDeviceKind.mouse) + ? SystemMouseCursors.none + : MouseCursor.defer, + onExit: notifier.onPointerExit, + child: Listener( + onPointerDown: notifier.onPointerDown, + onPointerMove: notifier.onPointerUpdate, + onPointerUp: notifier.onPointerUp, + onPointerHover: notifier.onPointerHover, + onPointerCancel: notifier.onPointerCancel, + // Use translucent behavior for singleTouchOnly to allow events to + // pass through to parent widgets + behavior: isSingleTouchOnly + ? HitTestBehavior.translucent + : HitTestBehavior.opaque, + child: child, + ), + ); + + if (!state.active) { + return child; + } + + // For singleTouchOnly, skip GestureCatcher to allow multi-touch events + // to reach parent widgets like InteractiveViewer + if (isSingleTouchOnly) { + return listenerWidget; + } + + return GestureCatcher( + pointerKindsToCatch: state.supportedPointerKinds, + child: listenerWidget, + ); }, ); } diff --git a/lib/src/view/state/scribble.state.dart b/lib/src/view/state/scribble.state.dart index c820f71..719cc48 100644 --- a/lib/src/view/state/scribble.state.dart +++ b/lib/src/view/state/scribble.state.dart @@ -1,5 +1,6 @@ import 'dart:ui'; +import 'package:flutter/widgets.dart' show Matrix4; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:scribble/src/domain/model/sketch/sketch.dart'; @@ -25,6 +26,15 @@ enum ScribblePointerMode { /// Allow drawing with both mouse and pen. mouseAndPen, + + /// Allow drawing with a single touch only. + /// + /// When a second touch is detected, the scribble widget will stop capturing + /// events, allowing them to pass through to parent widgets like + /// `InteractiveViewer` for pan/zoom gestures. + /// + /// Mouse and stylus inputs still work for drawing regardless of touch count. + singleTouchOnly, } /// Represents the state of the scribble widget. @@ -70,6 +80,22 @@ sealed class ScribbleState with _$ScribbleState { /// will mean no simplification. /// {@endtemplate} @Default(0) double simplificationTolerance, + + /// {@template view.state.scribble_state.view_transform} + /// The transformation matrix for the view (pan/zoom). + /// + /// When set, coordinates are transformed using the inverse of this matrix + /// before being stored, and the sketch is rendered using this transform. + /// This allows Scribble to work with external pan/zoom controllers like + /// InteractiveViewer without hit-testing issues. + /// + /// Note: This field is not serialized to JSON since Matrix4 has no + /// JSON converter. It defaults to null and is a runtime-only value. + /// {@endtemplate} + // ignore: invalid_annotation_target + @JsonKey(includeFromJson: false, includeToJson: false) + @Default(null) + Matrix4? viewTransform, }) = Drawing; /// The state of the scribble widget when the user is currently erasing. @@ -102,6 +128,12 @@ sealed class ScribbleState with _$ScribbleState { /// Lines will be simplified when they are finished. A value of 0 (default) /// will mean no simplification. @Default(0) double simplificationTolerance, + + /// {@macro view.state.scribble_state.view_transform} + // ignore: invalid_annotation_target + @JsonKey(includeFromJson: false, includeToJson: false) + @Default(null) + Matrix4? viewTransform, }) = Erasing; /// Constructs a [ScribbleState] from a JSON object. @@ -141,6 +173,10 @@ sealed class ScribbleState with _$ScribbleState { PointerDeviceKind.stylus, PointerDeviceKind.invertedStylus, }; + case ScribblePointerMode.singleTouchOnly: + // Accept all device types - multi-touch filtering is done in the + // notifier and widget + return Set.from(PointerDeviceKind.values); } } } diff --git a/lib/src/view/state/scribble.state.freezed.dart b/lib/src/view/state/scribble.state.freezed.dart index ea1774c..5ad2502 100644 --- a/lib/src/view/state/scribble.state.freezed.dart +++ b/lib/src/view/state/scribble.state.freezed.dart @@ -62,6 +62,21 @@ mixin _$ScribbleState { /// will mean no simplification. /// {@endtemplate} double get simplificationTolerance => throw _privateConstructorUsedError; + + /// {@template view.state.scribble_state.view_transform} + /// The transformation matrix for the view (pan/zoom). + /// + /// When set, coordinates are transformed using the inverse of this matrix + /// before being stored, and the sketch is rendered using this transform. + /// This allows Scribble to work with external pan/zoom controllers like + /// InteractiveViewer without hit-testing issues. + /// + /// Note: This field is not serialized to JSON since Matrix4 has no + /// JSON converter. It defaults to null and is a runtime-only value. + /// {@endtemplate} +// ignore: invalid_annotation_target + @JsonKey(includeFromJson: false, includeToJson: false) + Matrix4? get viewTransform => throw _privateConstructorUsedError; @optionalTypeArgs TResult when({ required TResult Function( @@ -73,7 +88,9 @@ mixin _$ScribbleState { int selectedColor, double selectedWidth, double scaleFactor, - double simplificationTolerance) + double simplificationTolerance, + @JsonKey(includeFromJson: false, includeToJson: false) + Matrix4? viewTransform) drawing, required TResult Function( Sketch sketch, @@ -82,7 +99,9 @@ mixin _$ScribbleState { Point? pointerPosition, double selectedWidth, double scaleFactor, - double simplificationTolerance) + double simplificationTolerance, + @JsonKey(includeFromJson: false, includeToJson: false) + Matrix4? viewTransform) erasing, }) => throw _privateConstructorUsedError; @@ -97,7 +116,9 @@ mixin _$ScribbleState { int selectedColor, double selectedWidth, double scaleFactor, - double simplificationTolerance)? + double simplificationTolerance, + @JsonKey(includeFromJson: false, includeToJson: false) + Matrix4? viewTransform)? drawing, TResult? Function( Sketch sketch, @@ -106,7 +127,9 @@ mixin _$ScribbleState { Point? pointerPosition, double selectedWidth, double scaleFactor, - double simplificationTolerance)? + double simplificationTolerance, + @JsonKey(includeFromJson: false, includeToJson: false) + Matrix4? viewTransform)? erasing, }) => throw _privateConstructorUsedError; @@ -121,7 +144,9 @@ mixin _$ScribbleState { int selectedColor, double selectedWidth, double scaleFactor, - double simplificationTolerance)? + double simplificationTolerance, + @JsonKey(includeFromJson: false, includeToJson: false) + Matrix4? viewTransform)? drawing, TResult Function( Sketch sketch, @@ -130,7 +155,9 @@ mixin _$ScribbleState { Point? pointerPosition, double selectedWidth, double scaleFactor, - double simplificationTolerance)? + double simplificationTolerance, + @JsonKey(includeFromJson: false, includeToJson: false) + Matrix4? viewTransform)? erasing, required TResult orElse(), }) => @@ -178,7 +205,9 @@ abstract class $ScribbleStateCopyWith<$Res> { Point? pointerPosition, double selectedWidth, double scaleFactor, - double simplificationTolerance}); + double simplificationTolerance, + @JsonKey(includeFromJson: false, includeToJson: false) + Matrix4? viewTransform}); $SketchCopyWith<$Res> get sketch; $PointCopyWith<$Res>? get pointerPosition; @@ -206,6 +235,7 @@ class _$ScribbleStateCopyWithImpl<$Res, $Val extends ScribbleState> Object? selectedWidth = null, Object? scaleFactor = null, Object? simplificationTolerance = null, + Object? viewTransform = freezed, }) { return _then(_value.copyWith( sketch: null == sketch @@ -236,6 +266,10 @@ class _$ScribbleStateCopyWithImpl<$Res, $Val extends ScribbleState> ? _value.simplificationTolerance : simplificationTolerance // ignore: cast_nullable_to_non_nullable as double, + viewTransform: freezed == viewTransform + ? _value.viewTransform + : viewTransform // ignore: cast_nullable_to_non_nullable + as Matrix4?, ) as $Val); } @@ -281,7 +315,9 @@ abstract class _$$DrawingImplCopyWith<$Res> int selectedColor, double selectedWidth, double scaleFactor, - double simplificationTolerance}); + double simplificationTolerance, + @JsonKey(includeFromJson: false, includeToJson: false) + Matrix4? viewTransform}); @override $SketchCopyWith<$Res> get sketch; @@ -312,6 +348,7 @@ class __$$DrawingImplCopyWithImpl<$Res> Object? selectedWidth = null, Object? scaleFactor = null, Object? simplificationTolerance = null, + Object? viewTransform = freezed, }) { return _then(_$DrawingImpl( sketch: null == sketch @@ -350,6 +387,10 @@ class __$$DrawingImplCopyWithImpl<$Res> ? _value.simplificationTolerance : simplificationTolerance // ignore: cast_nullable_to_non_nullable as double, + viewTransform: freezed == viewTransform + ? _value.viewTransform + : viewTransform // ignore: cast_nullable_to_non_nullable + as Matrix4?, )); } @@ -381,6 +422,8 @@ class _$DrawingImpl extends Drawing { this.selectedWidth = 5, this.scaleFactor = 1, this.simplificationTolerance = 0, + @JsonKey(includeFromJson: false, includeToJson: false) + this.viewTransform = null, final String? $type}) : _activePointerIds = activePointerIds, $type = $type ?? 'drawing', @@ -452,12 +495,28 @@ class _$DrawingImpl extends Drawing { @JsonKey() final double simplificationTolerance; + /// {@template view.state.scribble_state.view_transform} + /// The transformation matrix for the view (pan/zoom). + /// + /// When set, coordinates are transformed using the inverse of this matrix + /// before being stored, and the sketch is rendered using this transform. + /// This allows Scribble to work with external pan/zoom controllers like + /// InteractiveViewer without hit-testing issues. + /// + /// Note: This field is not serialized to JSON since Matrix4 has no + /// JSON converter. It defaults to null and is a runtime-only value. + /// {@endtemplate} +// ignore: invalid_annotation_target + @override + @JsonKey(includeFromJson: false, includeToJson: false) + final Matrix4? viewTransform; + @JsonKey(name: 'runtimeType') final String $type; @override String toString() { - return 'ScribbleState.drawing(sketch: $sketch, activeLine: $activeLine, allowedPointersMode: $allowedPointersMode, activePointerIds: $activePointerIds, pointerPosition: $pointerPosition, selectedColor: $selectedColor, selectedWidth: $selectedWidth, scaleFactor: $scaleFactor, simplificationTolerance: $simplificationTolerance)'; + return 'ScribbleState.drawing(sketch: $sketch, activeLine: $activeLine, allowedPointersMode: $allowedPointersMode, activePointerIds: $activePointerIds, pointerPosition: $pointerPosition, selectedColor: $selectedColor, selectedWidth: $selectedWidth, scaleFactor: $scaleFactor, simplificationTolerance: $simplificationTolerance, viewTransform: $viewTransform)'; } @override @@ -482,7 +541,9 @@ class _$DrawingImpl extends Drawing { other.scaleFactor == scaleFactor) && (identical( other.simplificationTolerance, simplificationTolerance) || - other.simplificationTolerance == simplificationTolerance)); + other.simplificationTolerance == simplificationTolerance) && + (identical(other.viewTransform, viewTransform) || + other.viewTransform == viewTransform)); } @JsonKey(includeFromJson: false, includeToJson: false) @@ -497,7 +558,8 @@ class _$DrawingImpl extends Drawing { selectedColor, selectedWidth, scaleFactor, - simplificationTolerance); + simplificationTolerance, + viewTransform); /// Create a copy of ScribbleState /// with the given fields replaced by the non-null parameter values. @@ -519,7 +581,9 @@ class _$DrawingImpl extends Drawing { int selectedColor, double selectedWidth, double scaleFactor, - double simplificationTolerance) + double simplificationTolerance, + @JsonKey(includeFromJson: false, includeToJson: false) + Matrix4? viewTransform) drawing, required TResult Function( Sketch sketch, @@ -528,7 +592,9 @@ class _$DrawingImpl extends Drawing { Point? pointerPosition, double selectedWidth, double scaleFactor, - double simplificationTolerance) + double simplificationTolerance, + @JsonKey(includeFromJson: false, includeToJson: false) + Matrix4? viewTransform) erasing, }) { return drawing( @@ -540,7 +606,8 @@ class _$DrawingImpl extends Drawing { selectedColor, selectedWidth, scaleFactor, - simplificationTolerance); + simplificationTolerance, + viewTransform); } @override @@ -555,7 +622,9 @@ class _$DrawingImpl extends Drawing { int selectedColor, double selectedWidth, double scaleFactor, - double simplificationTolerance)? + double simplificationTolerance, + @JsonKey(includeFromJson: false, includeToJson: false) + Matrix4? viewTransform)? drawing, TResult? Function( Sketch sketch, @@ -564,7 +633,9 @@ class _$DrawingImpl extends Drawing { Point? pointerPosition, double selectedWidth, double scaleFactor, - double simplificationTolerance)? + double simplificationTolerance, + @JsonKey(includeFromJson: false, includeToJson: false) + Matrix4? viewTransform)? erasing, }) { return drawing?.call( @@ -576,7 +647,8 @@ class _$DrawingImpl extends Drawing { selectedColor, selectedWidth, scaleFactor, - simplificationTolerance); + simplificationTolerance, + viewTransform); } @override @@ -591,7 +663,9 @@ class _$DrawingImpl extends Drawing { int selectedColor, double selectedWidth, double scaleFactor, - double simplificationTolerance)? + double simplificationTolerance, + @JsonKey(includeFromJson: false, includeToJson: false) + Matrix4? viewTransform)? drawing, TResult Function( Sketch sketch, @@ -600,7 +674,9 @@ class _$DrawingImpl extends Drawing { Point? pointerPosition, double selectedWidth, double scaleFactor, - double simplificationTolerance)? + double simplificationTolerance, + @JsonKey(includeFromJson: false, includeToJson: false) + Matrix4? viewTransform)? erasing, required TResult orElse(), }) { @@ -614,7 +690,8 @@ class _$DrawingImpl extends Drawing { selectedColor, selectedWidth, scaleFactor, - simplificationTolerance); + simplificationTolerance, + viewTransform); } return orElse(); } @@ -668,7 +745,9 @@ abstract class Drawing extends ScribbleState { final int selectedColor, final double selectedWidth, final double scaleFactor, - final double simplificationTolerance}) = _$DrawingImpl; + final double simplificationTolerance, + @JsonKey(includeFromJson: false, includeToJson: false) + final Matrix4? viewTransform}) = _$DrawingImpl; const Drawing._() : super._(); factory Drawing.fromJson(Map json) = _$DrawingImpl.fromJson; @@ -719,6 +798,22 @@ abstract class Drawing extends ScribbleState { @override double get simplificationTolerance; + /// {@template view.state.scribble_state.view_transform} + /// The transformation matrix for the view (pan/zoom). + /// + /// When set, coordinates are transformed using the inverse of this matrix + /// before being stored, and the sketch is rendered using this transform. + /// This allows Scribble to work with external pan/zoom controllers like + /// InteractiveViewer without hit-testing issues. + /// + /// Note: This field is not serialized to JSON since Matrix4 has no + /// JSON converter. It defaults to null and is a runtime-only value. + /// {@endtemplate} +// ignore: invalid_annotation_target + @override + @JsonKey(includeFromJson: false, includeToJson: false) + Matrix4? get viewTransform; + /// Create a copy of ScribbleState /// with the given fields replaced by the non-null parameter values. @override @@ -742,7 +837,9 @@ abstract class _$$ErasingImplCopyWith<$Res> Point? pointerPosition, double selectedWidth, double scaleFactor, - double simplificationTolerance}); + double simplificationTolerance, + @JsonKey(includeFromJson: false, includeToJson: false) + Matrix4? viewTransform}); @override $SketchCopyWith<$Res> get sketch; @@ -770,6 +867,7 @@ class __$$ErasingImplCopyWithImpl<$Res> Object? selectedWidth = null, Object? scaleFactor = null, Object? simplificationTolerance = null, + Object? viewTransform = freezed, }) { return _then(_$ErasingImpl( sketch: null == sketch @@ -800,6 +898,10 @@ class __$$ErasingImplCopyWithImpl<$Res> ? _value.simplificationTolerance : simplificationTolerance // ignore: cast_nullable_to_non_nullable as double, + viewTransform: freezed == viewTransform + ? _value.viewTransform + : viewTransform // ignore: cast_nullable_to_non_nullable + as Matrix4?, )); } } @@ -815,6 +917,8 @@ class _$ErasingImpl extends Erasing { this.selectedWidth = 5, this.scaleFactor = 1, this.simplificationTolerance = 0, + @JsonKey(includeFromJson: false, includeToJson: false) + this.viewTransform = null, final String? $type}) : _activePointerIds = activePointerIds, $type = $type ?? 'erasing', @@ -873,12 +977,18 @@ class _$ErasingImpl extends Erasing { @JsonKey() final double simplificationTolerance; + /// {@macro view.state.scribble_state.view_transform} +// ignore: invalid_annotation_target + @override + @JsonKey(includeFromJson: false, includeToJson: false) + final Matrix4? viewTransform; + @JsonKey(name: 'runtimeType') final String $type; @override String toString() { - return 'ScribbleState.erasing(sketch: $sketch, allowedPointersMode: $allowedPointersMode, activePointerIds: $activePointerIds, pointerPosition: $pointerPosition, selectedWidth: $selectedWidth, scaleFactor: $scaleFactor, simplificationTolerance: $simplificationTolerance)'; + return 'ScribbleState.erasing(sketch: $sketch, allowedPointersMode: $allowedPointersMode, activePointerIds: $activePointerIds, pointerPosition: $pointerPosition, selectedWidth: $selectedWidth, scaleFactor: $scaleFactor, simplificationTolerance: $simplificationTolerance, viewTransform: $viewTransform)'; } @override @@ -899,7 +1009,9 @@ class _$ErasingImpl extends Erasing { other.scaleFactor == scaleFactor) && (identical( other.simplificationTolerance, simplificationTolerance) || - other.simplificationTolerance == simplificationTolerance)); + other.simplificationTolerance == simplificationTolerance) && + (identical(other.viewTransform, viewTransform) || + other.viewTransform == viewTransform)); } @JsonKey(includeFromJson: false, includeToJson: false) @@ -912,7 +1024,8 @@ class _$ErasingImpl extends Erasing { pointerPosition, selectedWidth, scaleFactor, - simplificationTolerance); + simplificationTolerance, + viewTransform); /// Create a copy of ScribbleState /// with the given fields replaced by the non-null parameter values. @@ -934,7 +1047,9 @@ class _$ErasingImpl extends Erasing { int selectedColor, double selectedWidth, double scaleFactor, - double simplificationTolerance) + double simplificationTolerance, + @JsonKey(includeFromJson: false, includeToJson: false) + Matrix4? viewTransform) drawing, required TResult Function( Sketch sketch, @@ -943,11 +1058,20 @@ class _$ErasingImpl extends Erasing { Point? pointerPosition, double selectedWidth, double scaleFactor, - double simplificationTolerance) + double simplificationTolerance, + @JsonKey(includeFromJson: false, includeToJson: false) + Matrix4? viewTransform) erasing, }) { - return erasing(sketch, allowedPointersMode, activePointerIds, - pointerPosition, selectedWidth, scaleFactor, simplificationTolerance); + return erasing( + sketch, + allowedPointersMode, + activePointerIds, + pointerPosition, + selectedWidth, + scaleFactor, + simplificationTolerance, + viewTransform); } @override @@ -962,7 +1086,9 @@ class _$ErasingImpl extends Erasing { int selectedColor, double selectedWidth, double scaleFactor, - double simplificationTolerance)? + double simplificationTolerance, + @JsonKey(includeFromJson: false, includeToJson: false) + Matrix4? viewTransform)? drawing, TResult? Function( Sketch sketch, @@ -971,11 +1097,20 @@ class _$ErasingImpl extends Erasing { Point? pointerPosition, double selectedWidth, double scaleFactor, - double simplificationTolerance)? + double simplificationTolerance, + @JsonKey(includeFromJson: false, includeToJson: false) + Matrix4? viewTransform)? erasing, }) { - return erasing?.call(sketch, allowedPointersMode, activePointerIds, - pointerPosition, selectedWidth, scaleFactor, simplificationTolerance); + return erasing?.call( + sketch, + allowedPointersMode, + activePointerIds, + pointerPosition, + selectedWidth, + scaleFactor, + simplificationTolerance, + viewTransform); } @override @@ -990,7 +1125,9 @@ class _$ErasingImpl extends Erasing { int selectedColor, double selectedWidth, double scaleFactor, - double simplificationTolerance)? + double simplificationTolerance, + @JsonKey(includeFromJson: false, includeToJson: false) + Matrix4? viewTransform)? drawing, TResult Function( Sketch sketch, @@ -999,13 +1136,22 @@ class _$ErasingImpl extends Erasing { Point? pointerPosition, double selectedWidth, double scaleFactor, - double simplificationTolerance)? + double simplificationTolerance, + @JsonKey(includeFromJson: false, includeToJson: false) + Matrix4? viewTransform)? erasing, required TResult orElse(), }) { if (erasing != null) { - return erasing(sketch, allowedPointersMode, activePointerIds, - pointerPosition, selectedWidth, scaleFactor, simplificationTolerance); + return erasing( + sketch, + allowedPointersMode, + activePointerIds, + pointerPosition, + selectedWidth, + scaleFactor, + simplificationTolerance, + viewTransform); } return orElse(); } @@ -1057,7 +1203,9 @@ abstract class Erasing extends ScribbleState { final Point? pointerPosition, final double selectedWidth, final double scaleFactor, - final double simplificationTolerance}) = _$ErasingImpl; + final double simplificationTolerance, + @JsonKey(includeFromJson: false, includeToJson: false) + final Matrix4? viewTransform}) = _$ErasingImpl; const Erasing._() : super._(); factory Erasing.fromJson(Map json) = _$ErasingImpl.fromJson; @@ -1098,6 +1246,12 @@ abstract class Erasing extends ScribbleState { @override double get simplificationTolerance; + /// {@macro view.state.scribble_state.view_transform} +// ignore: invalid_annotation_target + @override + @JsonKey(includeFromJson: false, includeToJson: false) + Matrix4? get viewTransform; + /// Create a copy of ScribbleState /// with the given fields replaced by the non-null parameter values. @override diff --git a/lib/src/view/state/scribble.state.g.dart b/lib/src/view/state/scribble.state.g.dart index a76c097..2933433 100644 --- a/lib/src/view/state/scribble.state.g.dart +++ b/lib/src/view/state/scribble.state.g.dart @@ -50,6 +50,7 @@ const _$ScribblePointerModeEnumMap = { ScribblePointerMode.mouseOnly: 'mouseOnly', ScribblePointerMode.penOnly: 'penOnly', ScribblePointerMode.mouseAndPen: 'mouseAndPen', + ScribblePointerMode.singleTouchOnly: 'singleTouchOnly', }; _$ErasingImpl _$$ErasingImplFromJson(Map json) => diff --git a/pubspec.yaml b/pubspec.yaml index 08ef6a8..d235cc0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,6 +16,7 @@ dependencies: perfect_freehand: ^2.3.2 simpli: ^0.1.1 value_notifier_tools: ^0.1.2 + vector_math: ^2.1.0 dev_dependencies: build_runner: ^2.4.9