From 1e596fb4ad7c543743e6de56739ebffdbb8dc01c Mon Sep 17 00:00:00 2001 From: "Mr.Gump" <47901429+MrGumpIT@users.noreply.github.com> Date: Thu, 21 Mar 2024 16:11:27 +0100 Subject: [PATCH] Shape detection (#632) * pub * Revert "pub" This reverts commit 32588e62e5e310cc90a39dbcd3aa675f20a91ebc. * shapeDetection * Shape detection * Shape detection * Shape detection * shape Detection * shapeDetection * shapeDetection * shapeDetection * shape Detection * shape detection * shapeDetection * shape detection --- api/lib/src/models/property.dart | 3 + api/lib/src/models/property.freezed.dart | 212 +++++++++++++++++++ api/lib/src/models/property.g.dart | 12 ++ app/lib/handlers/handler.dart | 4 +- app/lib/handlers/pen.dart | 251 +++++++++++++++++++---- app/lib/l10n/app_en.arb | 1 + app/lib/renderers/elements/shape.dart | 28 +++ app/lib/visualizer/property.dart | 2 + app/pubspec.lock | 10 +- app/pubspec.yaml | 2 +- 10 files changed, 479 insertions(+), 46 deletions(-) diff --git a/api/lib/src/models/property.dart b/api/lib/src/models/property.dart index 2512b5971d83..611f56fb9e4d 100644 --- a/api/lib/src/models/property.dart +++ b/api/lib/src/models/property.dart @@ -50,6 +50,9 @@ sealed class PathShape with _$PathShape { @Default(0) double bottomLeftCornerRadius, @Default(0) double bottomRightCornerRadius}) = RectangleShape; const factory PathShape.line() = LineShape; + const factory PathShape.triangle({ + @Default(BasicColors.transparent) int fillColor, + }) = TriangleShape; factory PathShape.fromJson(Map json) => _$PathShapeFromJson(json); diff --git a/api/lib/src/models/property.freezed.dart b/api/lib/src/models/property.freezed.dart index a4ec08fb17a9..ee1244b15959 100644 --- a/api/lib/src/models/property.freezed.dart +++ b/api/lib/src/models/property.freezed.dart @@ -566,6 +566,8 @@ PathShape _$PathShapeFromJson(Map json) { return RectangleShape.fromJson(json); case 'line': return LineShape.fromJson(json); + case 'triangle': + return TriangleShape.fromJson(json); default: throw CheckedFromJsonException( @@ -586,6 +588,7 @@ mixin _$PathShape { double bottomRightCornerRadius) rectangle, required TResult Function() line, + required TResult Function(int fillColor) triangle, }) => throw _privateConstructorUsedError; @optionalTypeArgs @@ -599,6 +602,7 @@ mixin _$PathShape { double bottomRightCornerRadius)? rectangle, TResult? Function()? line, + TResult? Function(int fillColor)? triangle, }) => throw _privateConstructorUsedError; @optionalTypeArgs @@ -612,6 +616,7 @@ mixin _$PathShape { double bottomRightCornerRadius)? rectangle, TResult Function()? line, + TResult Function(int fillColor)? triangle, required TResult orElse(), }) => throw _privateConstructorUsedError; @@ -620,6 +625,7 @@ mixin _$PathShape { required TResult Function(CircleShape value) circle, required TResult Function(RectangleShape value) rectangle, required TResult Function(LineShape value) line, + required TResult Function(TriangleShape value) triangle, }) => throw _privateConstructorUsedError; @optionalTypeArgs @@ -627,6 +633,7 @@ mixin _$PathShape { TResult? Function(CircleShape value)? circle, TResult? Function(RectangleShape value)? rectangle, TResult? Function(LineShape value)? line, + TResult? Function(TriangleShape value)? triangle, }) => throw _privateConstructorUsedError; @optionalTypeArgs @@ -634,6 +641,7 @@ mixin _$PathShape { TResult Function(CircleShape value)? circle, TResult Function(RectangleShape value)? rectangle, TResult Function(LineShape value)? line, + TResult Function(TriangleShape value)? triangle, required TResult orElse(), }) => throw _privateConstructorUsedError; @@ -742,6 +750,7 @@ class _$CircleShapeImpl extends CircleShape { double bottomRightCornerRadius) rectangle, required TResult Function() line, + required TResult Function(int fillColor) triangle, }) { return circle(fillColor); } @@ -758,6 +767,7 @@ class _$CircleShapeImpl extends CircleShape { double bottomRightCornerRadius)? rectangle, TResult? Function()? line, + TResult? Function(int fillColor)? triangle, }) { return circle?.call(fillColor); } @@ -774,6 +784,7 @@ class _$CircleShapeImpl extends CircleShape { double bottomRightCornerRadius)? rectangle, TResult Function()? line, + TResult Function(int fillColor)? triangle, required TResult orElse(), }) { if (circle != null) { @@ -788,6 +799,7 @@ class _$CircleShapeImpl extends CircleShape { required TResult Function(CircleShape value) circle, required TResult Function(RectangleShape value) rectangle, required TResult Function(LineShape value) line, + required TResult Function(TriangleShape value) triangle, }) { return circle(this); } @@ -798,6 +810,7 @@ class _$CircleShapeImpl extends CircleShape { TResult? Function(CircleShape value)? circle, TResult? Function(RectangleShape value)? rectangle, TResult? Function(LineShape value)? line, + TResult? Function(TriangleShape value)? triangle, }) { return circle?.call(this); } @@ -808,6 +821,7 @@ class _$CircleShapeImpl extends CircleShape { TResult Function(CircleShape value)? circle, TResult Function(RectangleShape value)? rectangle, TResult Function(LineShape value)? line, + TResult Function(TriangleShape value)? triangle, required TResult orElse(), }) { if (circle != null) { @@ -975,6 +989,7 @@ class _$RectangleShapeImpl extends RectangleShape { double bottomRightCornerRadius) rectangle, required TResult Function() line, + required TResult Function(int fillColor) triangle, }) { return rectangle(fillColor, topLeftCornerRadius, topRightCornerRadius, bottomLeftCornerRadius, bottomRightCornerRadius); @@ -992,6 +1007,7 @@ class _$RectangleShapeImpl extends RectangleShape { double bottomRightCornerRadius)? rectangle, TResult? Function()? line, + TResult? Function(int fillColor)? triangle, }) { return rectangle?.call(fillColor, topLeftCornerRadius, topRightCornerRadius, bottomLeftCornerRadius, bottomRightCornerRadius); @@ -1009,6 +1025,7 @@ class _$RectangleShapeImpl extends RectangleShape { double bottomRightCornerRadius)? rectangle, TResult Function()? line, + TResult Function(int fillColor)? triangle, required TResult orElse(), }) { if (rectangle != null) { @@ -1024,6 +1041,7 @@ class _$RectangleShapeImpl extends RectangleShape { required TResult Function(CircleShape value) circle, required TResult Function(RectangleShape value) rectangle, required TResult Function(LineShape value) line, + required TResult Function(TriangleShape value) triangle, }) { return rectangle(this); } @@ -1034,6 +1052,7 @@ class _$RectangleShapeImpl extends RectangleShape { TResult? Function(CircleShape value)? circle, TResult? Function(RectangleShape value)? rectangle, TResult? Function(LineShape value)? line, + TResult? Function(TriangleShape value)? triangle, }) { return rectangle?.call(this); } @@ -1044,6 +1063,7 @@ class _$RectangleShapeImpl extends RectangleShape { TResult Function(CircleShape value)? circle, TResult Function(RectangleShape value)? rectangle, TResult Function(LineShape value)? line, + TResult Function(TriangleShape value)? triangle, required TResult orElse(), }) { if (rectangle != null) { @@ -1138,6 +1158,7 @@ class _$LineShapeImpl extends LineShape { double bottomRightCornerRadius) rectangle, required TResult Function() line, + required TResult Function(int fillColor) triangle, }) { return line(); } @@ -1154,6 +1175,7 @@ class _$LineShapeImpl extends LineShape { double bottomRightCornerRadius)? rectangle, TResult? Function()? line, + TResult? Function(int fillColor)? triangle, }) { return line?.call(); } @@ -1170,6 +1192,7 @@ class _$LineShapeImpl extends LineShape { double bottomRightCornerRadius)? rectangle, TResult Function()? line, + TResult Function(int fillColor)? triangle, required TResult orElse(), }) { if (line != null) { @@ -1184,6 +1207,7 @@ class _$LineShapeImpl extends LineShape { required TResult Function(CircleShape value) circle, required TResult Function(RectangleShape value) rectangle, required TResult Function(LineShape value) line, + required TResult Function(TriangleShape value) triangle, }) { return line(this); } @@ -1194,6 +1218,7 @@ class _$LineShapeImpl extends LineShape { TResult? Function(CircleShape value)? circle, TResult? Function(RectangleShape value)? rectangle, TResult? Function(LineShape value)? line, + TResult? Function(TriangleShape value)? triangle, }) { return line?.call(this); } @@ -1204,6 +1229,7 @@ class _$LineShapeImpl extends LineShape { TResult Function(CircleShape value)? circle, TResult Function(RectangleShape value)? rectangle, TResult Function(LineShape value)? line, + TResult Function(TriangleShape value)? triangle, required TResult orElse(), }) { if (line != null) { @@ -1227,3 +1253,189 @@ abstract class LineShape extends PathShape { factory LineShape.fromJson(Map json) = _$LineShapeImpl.fromJson; } + +/// @nodoc +abstract class _$$TriangleShapeImplCopyWith<$Res> { + factory _$$TriangleShapeImplCopyWith( + _$TriangleShapeImpl value, $Res Function(_$TriangleShapeImpl) then) = + __$$TriangleShapeImplCopyWithImpl<$Res>; + @useResult + $Res call({int fillColor}); +} + +/// @nodoc +class __$$TriangleShapeImplCopyWithImpl<$Res> + extends _$PathShapeCopyWithImpl<$Res, _$TriangleShapeImpl> + implements _$$TriangleShapeImplCopyWith<$Res> { + __$$TriangleShapeImplCopyWithImpl( + _$TriangleShapeImpl _value, $Res Function(_$TriangleShapeImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? fillColor = null, + }) { + return _then(_$TriangleShapeImpl( + fillColor: null == fillColor + ? _value.fillColor + : fillColor // ignore: cast_nullable_to_non_nullable + as int, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$TriangleShapeImpl extends TriangleShape { + const _$TriangleShapeImpl( + {this.fillColor = BasicColors.transparent, final String? $type}) + : $type = $type ?? 'triangle', + super._(); + + factory _$TriangleShapeImpl.fromJson(Map json) => + _$$TriangleShapeImplFromJson(json); + + @override + @JsonKey() + final int fillColor; + + @JsonKey(name: 'type') + final String $type; + + @override + String toString() { + return 'PathShape.triangle(fillColor: $fillColor)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$TriangleShapeImpl && + (identical(other.fillColor, fillColor) || + other.fillColor == fillColor)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, fillColor); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$TriangleShapeImplCopyWith<_$TriangleShapeImpl> get copyWith => + __$$TriangleShapeImplCopyWithImpl<_$TriangleShapeImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(int fillColor) circle, + required TResult Function( + int fillColor, + double topLeftCornerRadius, + double topRightCornerRadius, + double bottomLeftCornerRadius, + double bottomRightCornerRadius) + rectangle, + required TResult Function() line, + required TResult Function(int fillColor) triangle, + }) { + return triangle(fillColor); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(int fillColor)? circle, + TResult? Function( + int fillColor, + double topLeftCornerRadius, + double topRightCornerRadius, + double bottomLeftCornerRadius, + double bottomRightCornerRadius)? + rectangle, + TResult? Function()? line, + TResult? Function(int fillColor)? triangle, + }) { + return triangle?.call(fillColor); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(int fillColor)? circle, + TResult Function( + int fillColor, + double topLeftCornerRadius, + double topRightCornerRadius, + double bottomLeftCornerRadius, + double bottomRightCornerRadius)? + rectangle, + TResult Function()? line, + TResult Function(int fillColor)? triangle, + required TResult orElse(), + }) { + if (triangle != null) { + return triangle(fillColor); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(CircleShape value) circle, + required TResult Function(RectangleShape value) rectangle, + required TResult Function(LineShape value) line, + required TResult Function(TriangleShape value) triangle, + }) { + return triangle(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(CircleShape value)? circle, + TResult? Function(RectangleShape value)? rectangle, + TResult? Function(LineShape value)? line, + TResult? Function(TriangleShape value)? triangle, + }) { + return triangle?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(CircleShape value)? circle, + TResult Function(RectangleShape value)? rectangle, + TResult Function(LineShape value)? line, + TResult Function(TriangleShape value)? triangle, + required TResult orElse(), + }) { + if (triangle != null) { + return triangle(this); + } + return orElse(); + } + + @override + Map toJson() { + return _$$TriangleShapeImplToJson( + this, + ); + } +} + +abstract class TriangleShape extends PathShape { + const factory TriangleShape({final int fillColor}) = _$TriangleShapeImpl; + const TriangleShape._() : super._(); + + factory TriangleShape.fromJson(Map json) = + _$TriangleShapeImpl.fromJson; + + int get fillColor; + @JsonKey(ignore: true) + _$$TriangleShapeImplCopyWith<_$TriangleShapeImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/api/lib/src/models/property.g.dart b/api/lib/src/models/property.g.dart index 2f130ccebf40..ba1101e6c9ec 100644 --- a/api/lib/src/models/property.g.dart +++ b/api/lib/src/models/property.g.dart @@ -88,3 +88,15 @@ Map _$$LineShapeImplToJson(_$LineShapeImpl instance) => { 'type': instance.$type, }; + +_$TriangleShapeImpl _$$TriangleShapeImplFromJson(Map json) => + _$TriangleShapeImpl( + fillColor: json['fillColor'] as int? ?? BasicColors.transparent, + $type: json['type'] as String?, + ); + +Map _$$TriangleShapeImplToJson(_$TriangleShapeImpl instance) => + { + 'fillColor': instance.fillColor, + 'type': instance.$type, + }; diff --git a/app/lib/handlers/handler.dart b/app/lib/handlers/handler.dart index 924356609e2a..31408c2bca4d 100644 --- a/app/lib/handlers/handler.dart +++ b/app/lib/handlers/handler.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:math'; +import 'dart:ui'; import 'package:animations/animations.dart'; import 'package:butterfly/api/open.dart'; @@ -30,6 +31,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:image/image.dart' as img; import 'package:lw_sysapi/lw_sysapi.dart'; import 'package:material_leap/material_leap.dart'; + import 'package:phosphor_flutter/phosphor_flutter.dart'; import 'package:share_plus/share_plus.dart'; @@ -51,7 +53,7 @@ import '../views/toolbar/components.dart'; import '../views/toolbar/label.dart'; import '../views/toolbar/presentation/toolbar.dart'; import '../widgets/context_menu.dart'; - +import 'package:one_dollar_unistroke_recognizer/one_dollar_unistroke_recognizer.dart'; part 'area.dart'; part 'asset.dart'; part 'eraser.dart'; diff --git a/app/lib/handlers/pen.dart b/app/lib/handlers/pen.dart index a5515c903f02..173ba111626c 100644 --- a/app/lib/handlers/pen.dart +++ b/app/lib/handlers/pen.dart @@ -10,9 +10,16 @@ class PenHandler extends Handler with ColoredHandler { final Map lastPosition = {}; // Dictionary to plot the total distance traveled by each pointer Map totalDistance = {}; - // Timer to track the time interval for updating the line. - Timer? _timer; + // List for shapeDetection + final points = []; + // For control if the pointer has not moved + bool isDrawing = false; + // Timer to initiate shape detection after a period of inactivity + Timer? _positionCheckTimer; + // Variable to store the current position of the pointer + Offset? lastPosit; Offset? localPos; + PenHandler(super.data); // Create foregrounds for rendering the PenRendere @@ -39,10 +46,15 @@ class PenHandler extends Handler with ColoredHandler { // Handle the pointer release event. @override void onPointerUp(PointerUpEvent event, EventContext context) { + isDrawing = false; + // Cancel the timer when the pointer is lifted, preventing shape detection + _positionCheckTimer?.cancel(); + _positionCheckTimer = null; addPoint(context.buildContext, event.pointer, event.localPosition, _getPressure(event), event.kind, refresh: false); submitElements(context.getDocumentBloc(), [event.pointer]); + points.clear(); } // Flag to check if elements are being submitted. @@ -99,36 +111,10 @@ class PenHandler extends Handler with ColoredHandler { if (refresh) bloc.refresh(); } - // This function updates the current line with the pointer's start and end position. - void _tickShapeDetection( - int pointer, EventContext context, Offset localPosition) { - if (totalDistance[pointer] != null && totalDistance[pointer]! < 1000) { - // Check if the last known position of the pointer has not changed since the timer started. - if (lastPosition[pointer] == localPosition) { - // If the position has not changed, get the PenElement associated with the pointer. - final element = elements[pointer]; - if (element != null) { - // index point - int midIndex = (element.points.length / 2).floor(); - - // Update the line with the start,middle,and position of the pointer. - if (data.shapeDetectionEnabled) { - elements[pointer] = element.copyWith(points: [ - element.points.first, - element.points[midIndex], - element.points.last - ]); - } - _timer?.cancel(); - _timer = null; - } - } - } - } - // This function is called when the pointer is pressed down. @override void onPointerDown(PointerDownEvent event, EventContext context) { + isDrawing = true; final currentIndex = context.getCurrentIndex(); // Save initial position startPosition[event.pointer] = event.localPosition; @@ -152,23 +138,202 @@ class PenHandler extends Handler with ColoredHandler { @override void onPointerMove(PointerMoveEvent event, EventContext context) { - // Calculates the distance the pointer travels - double distance = ((lastPosition[event.pointer] ?? event.localPosition) - - event.localPosition) - .distance; - // Updates the total distance traveled by the pointer - totalDistance[event.pointer] = - (totalDistance[event.pointer] ?? 0) + distance; + if (!isDrawing) return; + // Check if the pointer has not moved + if (lastPosit == event.localPosition) return; + // Update the current position with the new position of the pointer + lastPosit = event.localPosition; + // Cancel any existing timer + _positionCheckTimer?.cancel(); + // Start a new timer that will call the shape detection after a delay by data.shapeDetectionTime + _positionCheckTimer = Timer( + Duration(milliseconds: (data.shapeDetectionTime * 1000).round()), () { + _tickShapeDetection(event.pointer, context, event.localPosition); + }); // Call the addPoint function to add a point to the current brush stroke. addPoint(context.buildContext, event.pointer, event.localPosition, _getPressure(event), event.kind); - // Update the last position with the current position - lastPosition[event.pointer] = event.localPosition; - // Start a timer that fires after 500 milliseconds - _timer?.cancel(); - _timer = Timer( - Duration(milliseconds: (data.shapeDetectionTime * 1000).round()), - () => _tickShapeDetection(event.pointer, context, event.localPosition)); + points.add(event.localPosition); + } + + void showMessage(EventContext context, String recognizedshape) { + // show SnackBar with recognized shape + ScaffoldMessenger.of(context.buildContext).showSnackBar( + SnackBar( + width: MediaQuery.of(context.buildContext).size.width * 0.1, + behavior: SnackBarBehavior.floating, + content: Text( + textAlign: TextAlign.center, + recognizedshape, + ), + duration: const Duration(milliseconds: 300), + ), + ); + } + + // Detects shapes and draws them + void _tickShapeDetection( + int pointer, EventContext context, Offset localPosition) { + final transform = context.getCameraTransform(); + // Create recognizeUnistroke + final recognized = recognizeUnistroke(points); + final element = elements[pointer]; + + if (recognized == null || points.length > 600 || element == null) { + return; + } + switch (recognized.name) { + case DefaultUnistrokeNames.line: + double startX = points.first.dx; + double startY = points.first.dy; + double endX = points.last.dx; + double endY = points.last.dy; + + Point firstPosition = Point(startX, startY); + Point secondPosition = Point(endX, endY); + + // Convert coordinates from the document coordinate system to the view coordinate system + Offset firstPositionInView = + transform.localToGlobal(firstPosition.toOffset()); + Offset secondPositionInView = + transform.localToGlobal(secondPosition.toOffset()); + + // Create new shape element + PadElement shapeElement = PadElement.shape( + firstPosition: firstPositionInView.toPoint(), + secondPosition: secondPositionInView.toPoint(), + property: ShapeProperty( + shape: const LineShape(), + color: data.property.color, + strokeWidth: data.property.strokeWidth), + ); + + // Show dialog + showMessage(context, AppLocalizations.of(context.buildContext).line); + + // Add element on document + context.getDocumentBloc().add(ElementsCreated([shapeElement])); + + elements.clear(); + context.refresh(); + + case DefaultUnistrokeNames.circle: + // Calculate the center of the circle as the average of the points + double centerX = + points.map((p) => p.dx).reduce((a, b) => a + b) / points.length; + double centerY = + points.map((p) => p.dy).reduce((a, b) => a + b) / points.length; + Offset center = Offset(centerX, centerY); + + // Calculate the radius as the average of the distances of the points from the center + double radius = + points.map((p) => (p - center).distance).reduce((a, b) => a + b) / + points.length; + + Point firstPosition = + Point(center.dx - radius, center.dy - radius); + Point secondPosition = + Point(center.dx + radius, center.dy + radius); + + // Convert coordinates from the document coordinate system to the view coordinate system + Offset firstPositionInView = + transform.localToGlobal(firstPosition.toOffset()); + Offset secondPositionInView = + transform.localToGlobal(secondPosition.toOffset()); + + // Create new ShapeElement + PadElement shapeElement = PadElement.shape( + firstPosition: firstPositionInView.toPoint(), + secondPosition: secondPositionInView.toPoint(), + property: ShapeProperty( + shape: const CircleShape(), + color: data.property.color, + strokeWidth: data.property.strokeWidth), + ); + + // Show dialog + showMessage(context, AppLocalizations.of(context.buildContext).circle); + + // Add element on document + context.getDocumentBloc().add(ElementsCreated([shapeElement])); + + elements.clear(); + context.refresh(); + + case DefaultUnistrokeNames.rectangle: + double minX = points.map((p) => p.dx).reduce(min); + double maxX = points.map((p) => p.dx).reduce(max); + double minY = points.map((p) => p.dy).reduce(min); + double maxY = points.map((p) => p.dy).reduce(max); + + Point firstPosition = Point(minX, minY); + Point secondPosition = Point(maxX, maxY); + + // Convert coordinates from the document coordinate system to the view coordinate system + Offset firstPositionInView = + transform.localToGlobal(firstPosition.toOffset()); + Offset secondPositionInView = + transform.localToGlobal(secondPosition.toOffset()); + + // Create new ShapeElement + PadElement shapeElement = PadElement.shape( + firstPosition: firstPositionInView.toPoint(), + secondPosition: secondPositionInView.toPoint(), + property: ShapeProperty( + shape: const RectangleShape(), + color: data.property.color, + strokeWidth: data.property.strokeWidth), + ); + + // Show dialog + showMessage( + context, AppLocalizations.of(context.buildContext).rectangle); + + // Add element on document + context.getDocumentBloc().add(ElementsCreated([shapeElement])); + + elements.clear(); + context.refresh(); + + case DefaultUnistrokeNames.triangle: + double minX = points.map((p) => p.dx).reduce(min); + double maxX = points.map((p) => p.dx).reduce(max); + double minY = points.map((p) => p.dy).reduce(min); + double maxY = points.map((p) => p.dy).reduce(max); + + Point firstPosition = Point(minX, minY); + Point secondPosition = Point(maxX, maxY); + + // Convert coordinates from the document coordinate system to the view coordinate system + Offset firstPositionInView = + transform.localToGlobal(firstPosition.toOffset()); + Offset secondPositionInView = + transform.localToGlobal(secondPosition.toOffset()); + + // Create new ShapeElement + PadElement shapeElement = PadElement.shape( + firstPosition: firstPositionInView.toPoint(), + secondPosition: secondPositionInView.toPoint(), + property: ShapeProperty( + shape: const TriangleShape(), + color: data.property.color, + strokeWidth: data.property.strokeWidth), + ); + + // Show dialog + showMessage( + context, AppLocalizations.of(context.buildContext).triangle); + + // Add element on document + context.getDocumentBloc().add(ElementsCreated([shapeElement])); + + elements.clear(); + context.refresh(); + + default: // Manage custom shapes here + } + // Reset the points list for the next shape detection + points.clear(); } @override diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index f72e5fb61672..fc160a77fe27 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -310,6 +310,7 @@ "shape": "Shape", "circle": "Circle", "rectangle": "Rectangle", + "triangle": "Triangle", "line": "Line", "cornerRadius": "Corner radius", "topLeft": "Top left", diff --git a/app/lib/renderers/elements/shape.dart b/app/lib/renderers/elements/shape.dart index 4ba1fc6c6e09..89d9fedd01e3 100644 --- a/app/lib/renderers/elements/shape.dart +++ b/app/lib/renderers/elements/shape.dart @@ -64,6 +64,25 @@ class ShapeRenderer extends Renderer { } else if (shape is LineShape) { canvas.drawLine(element.firstPosition.toOffset(), element.secondPosition.toOffset(), paint); + } else if (shape is TriangleShape) { + final center = Offset( + (element.firstPosition.x + element.secondPosition.x) / 2, + (element.firstPosition.y + element.secondPosition.y) / 2); + final height = element.secondPosition.y - element.firstPosition.y; + final path = Path(); + path.moveTo(center.dx, center.dy - height / 2); // Upper point + path.lineTo(center.dx - height * sqrt(3) / 4, + center.dy + height / 2); // Bottom Left Dot + path.lineTo(center.dx + height * sqrt(3) / 4, + center.dy + height / 2); // Bottom Right Dot + path.close(); + canvas.drawPath( + path, + _buildPaint( + color: Color(shape.fillColor), style: PaintingStyle.fill)); + if (strokeWidth > 0) { + canvas.drawPath(path, paint); + } } } @@ -213,6 +232,7 @@ class ShapeHitCalculator extends HitCalculator { return shape.map( circle: (e) => containsRect(), rectangle: (e) => containsRect(), + triangle: (e) => containsRect(), line: (e) { final firstX = min(element.firstPosition.x, element.secondPosition.x); final firstY = min(element.firstPosition.y, element.secondPosition.y); @@ -259,6 +279,14 @@ class ShapeHitCalculator extends HitCalculator { isPointInPolygon(polygon, bottomRight) || isPointInPolygon(polygon, center); }, + triangle: (value) { + final firstPosition = + element.firstPosition.toOffset().rotate(center, rotation); + final secondPosition = + element.secondPosition.toOffset().rotate(center, rotation); + return isPointInPolygon(polygon, firstPosition) || + isPointInPolygon(polygon, secondPosition); + }, ); } } diff --git a/app/lib/visualizer/property.dart b/app/lib/visualizer/property.dart index 777aab5e3c09..e4eac33fae62 100644 --- a/app/lib/visualizer/property.dart +++ b/app/lib/visualizer/property.dart @@ -9,6 +9,7 @@ extension PathShapeVisualizer on PathShape { circle: (_) => PhosphorIcons.circle, rectangle: (_) => PhosphorIcons.square, line: (_) => PhosphorIcons.lineSegment, + triangle: (_) => PhosphorIcons.triangle, ); String getLocalizedName(BuildContext context) { @@ -17,6 +18,7 @@ extension PathShapeVisualizer on PathShape { circle: (_) => loc.circle, rectangle: (_) => loc.rectangle, line: (_) => loc.line, + triangle: (_) => loc.triangle, ); } } diff --git a/app/pubspec.lock b/app/pubspec.lock index 0e9b668950fe..6cb0354def59 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -612,7 +612,7 @@ packages: source: hosted version: "2.4.7" freezed_annotation: - dependency: "direct main" + dependency: transitive description: name: freezed_annotation sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d @@ -876,6 +876,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.0" + one_dollar_unistroke_recognizer: + dependency: "direct main" + description: + name: one_dollar_unistroke_recognizer + sha256: c35bb3c054e792836c9b36d0dd680ec02a447e6bc29a77b9f32c98ffcc11c716 + url: "https://pub.dev" + source: hosted + version: "1.1.2" package_config: dependency: transitive description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 3d2de510a87f..075f58d2a332 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -92,7 +92,7 @@ dependencies: markdown: ^7.2.2 device_info_plus: ^10.0.1 image: ^4.1.7 - freezed_annotation: ^2.4.1 + one_dollar_unistroke_recognizer: ^1.0.0 dev_dependencies: flutter_native_splash: ^2.4.0 #flutter_launcher_icons: ^0.11.0