diff --git a/CHANGELOG.md b/CHANGELOG.md index 10d23f9..ef3fcbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,3 +63,11 @@ ## [1.1.2] - May 12, 2023 * Fixed complexity bug with node relations detection. + +## [1.2.0] - Jun 25, 2024 + +* Added new `EdgeStyle` param with options for edges customization, like border-radius and like style. +* Changed `PaintBuilder` to `EdgeStyleBuilder`, paint selection is now a part of `EdgeStyleBuilder`. +* Changed `PathBuilder` to accept `EdgeStyle style` param as an argument. +* Edges gestures hit-box rework. + diff --git a/README.md b/README.md index f5a641c..9cb02f0 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ class _MyHomePageState extends State { * Ability to add overlays. * Ability to add edge text or `Widget` labels. * Ability to provide custom paint builder to graph edges. -* Ability to customize arrows. +* Ability to customize arrows and lines. ## License diff --git a/example/lib/custom_edges.dart b/example/lib/custom_edges.dart index 35f4163..31d7cec 100644 --- a/example/lib/custom_edges.dart +++ b/example/lib/custom_edges.dart @@ -57,7 +57,6 @@ class CustomEdgesPageState extends State { cellPadding: const EdgeInsets.all(14), contactEdgesDistance: 5.0, orientation: MatrixOrientation.Vertical, - pathBuilder: customEdgePathBuilder, centered: false, onEdgeTapDown: (details, edge) { print("${edge.from.id}->${edge.to.id}"); @@ -77,18 +76,31 @@ class CustomEdgesPageState extends State { ), ); }, - paintBuilder: (edge) { + styleBuilder: (edge) { var p = Paint() ..color = Colors.blueGrey ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.round ..strokeJoin = StrokeJoin.round ..strokeWidth = 2; + LineStyle lineStyle = LineStyle.solid; + switch (edge.from.id) { + case "U": + lineStyle = LineStyle.dotted; + break; + case "C": + lineStyle = LineStyle.dashed; + break; + case "M": + lineStyle = LineStyle.dashDotted; + break; + } if ((selected[edge.from.id] ?? false) && (selected[edge.to.id] ?? false)) { p.color = Colors.red; } - return p; + return EdgeStyle( + lineStyle: lineStyle, borderRadius: 40, linePaint: p); }, onNodeTapDown: (_, node, __) { _onItemSelected(node.id); @@ -99,13 +111,3 @@ class CustomEdgesPageState extends State { ); } } - -Path customEdgePathBuilder(NodeInput from, NodeInput to, - List> points, EdgeArrowType arrowType) { - var path = Path(); - path.moveTo(points[0][0], points[0][1]); - points.sublist(1).forEach((p) { - path.lineTo(p[0], p[1]); - }); - return path; -} diff --git a/example/lib/digimon.dart b/example/lib/digimon.dart index 1a9b0ea..7717266 100644 --- a/example/lib/digimon.dart +++ b/example/lib/digimon.dart @@ -229,11 +229,11 @@ class DigimonPageState extends State children: [ Text( _currentNodeInfo!.data.title, - style: Theme.of(context).textTheme.headline6, + style: Theme.of(context).textTheme.titleSmall, ), Text( "Level: ${_currentNodeInfo!.data.level}", - style: Theme.of(context).textTheme.subtitle2, + style: Theme.of(context).textTheme.titleMedium, ), ], ), @@ -278,16 +278,17 @@ class DigimonPageState extends State child: DirectGraph( list: imagePreset, defaultCellSize: const Size(100.0, 100.0), - cellPadding: const EdgeInsets.symmetric(horizontal: 25, vertical: 5), + cellPadding: + const EdgeInsets.symmetric(horizontal: 25, vertical: 5), contactEdgesDistance: 5.0, orientation: MatrixOrientation.Horizontal, clipBehavior: Clip.none, centered: true, minScale: .1, maxScale: 3, - overlayBuilder: - (BuildContext context, List nodes, List edges) => - _buildOverlay(context, nodes, edges), + overlayBuilder: (BuildContext context, List nodes, + List edges) => + _buildOverlay(context, nodes, edges), onCanvasTap: _onCanvasTap, onNodeTapUp: _onNodeTap, nodeBuilder: (BuildContext context, NodeInput node) => FittedBox( diff --git a/example/lib/flowchart.dart b/example/lib/flowchart.dart index c057de3..27fd5f9 100644 --- a/example/lib/flowchart.dart +++ b/example/lib/flowchart.dart @@ -75,18 +75,18 @@ class FlowchartPageState extends State { constraints: BoxConstraints(minWidth: screenSize.width), child: Center( child: DirectGraph( - list: list, - defaultCellSize: const Size(250.0, 100.0), - cellPadding: - const EdgeInsets.symmetric(vertical: 30, horizontal: 5), - contactEdgesDistance: 0, - orientation: MatrixOrientation.Vertical, - nodeBuilder: (BuildContext context, NodeInput node) => Padding( - padding: const EdgeInsets.all(5), child: _buildNode(node)), - centered: true, - minScale: .1, - maxScale: 1, - ), + list: list, + defaultCellSize: const Size(250.0, 100.0), + cellPadding: + const EdgeInsets.symmetric(vertical: 30, horizontal: 5), + contactEdgesDistance: 0, + orientation: MatrixOrientation.Vertical, + nodeBuilder: (BuildContext context, NodeInput node) => Padding( + padding: const EdgeInsets.all(5), child: _buildNode(node)), + centered: true, + minScale: .1, + maxScale: 1, + ), ), ), ), @@ -113,7 +113,7 @@ class Start extends StatelessWidget { child: Center( child: Text( data.text, - style: Theme.of(context).textTheme.subtitle2, + style: Theme.of(context).textTheme.titleMedium, ), ), ); @@ -138,7 +138,7 @@ class Document extends StatelessWidget { child: Center( child: Text( data.text, - style: Theme.of(context).textTheme.subtitle2, + style: Theme.of(context).textTheme.titleMedium, ), ), ); @@ -163,7 +163,7 @@ class Process extends StatelessWidget { child: Center( child: Text( data.text, - style: Theme.of(context).textTheme.subtitle2, + style: Theme.of(context).textTheme.titleMedium, ), ), ); @@ -198,7 +198,7 @@ class Decision extends StatelessWidget { child: Center( child: Text( data.text, - style: Theme.of(context).textTheme.subtitle2, + style: Theme.of(context).textTheme.titleMedium, ), ), ) @@ -225,7 +225,7 @@ class End extends StatelessWidget { child: Center( child: Text( data.text, - style: Theme.of(context).textTheme.subtitle2, + style: Theme.of(context).textTheme.titleMedium, ), ), ); diff --git a/example/lib/labels.dart b/example/lib/labels.dart index 0234d5c..524b6f5 100644 --- a/example/lib/labels.dart +++ b/example/lib/labels.dart @@ -64,18 +64,20 @@ class LabelsPageState extends State { child: Text( style: Theme.of(context) .textTheme - .bodyText2! + .bodyMedium! .copyWith( backgroundColor: Theme.of(context) - .backgroundColor), + .colorScheme + .surface), "${edge.from.id}=>${edge.to.id}")) : Text( style: Theme.of(context) .textTheme - .bodyText2! + .bodyMedium! .copyWith( - backgroundColor: - Theme.of(context).backgroundColor), + backgroundColor: Theme.of(context) + .colorScheme + .surface), "${edge.from.id}=>${edge.to.id}"), )), centered: _isCentered, diff --git a/example/lib/main.dart b/example/lib/main.dart index 5b31df4..dd872a6 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -74,8 +74,9 @@ class _MyAppState extends State { return MaterialApp( title: 'Flutter Graphite', theme: ThemeData( - primarySwatch: Colors.teal, - backgroundColor: Colors.white, + colorScheme: ColorScheme.fromSwatch( + primarySwatch: Colors.teal, backgroundColor: Colors.white) + .copyWith(surface: Colors.white), ), home: FlowchartPage(bottomBar: _buildBottomBar), routes: { diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj index c84862c..d9ae3f4 100644 --- a/example/macos/Runner.xcodeproj/project.pbxproj +++ b/example/macos/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ @@ -182,7 +182,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 33CC10EC2044A3C60003C045 = { @@ -235,6 +235,7 @@ /* Begin PBXShellScriptBuildPhase section */ 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -344,7 +345,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -423,7 +424,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -470,7 +471,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index fb7259e..5b055a3 100644 --- a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ =3.0.0-417 <4.0.0" + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/image/custom.gif b/image/custom.gif index 552090c..e5733b9 100644 Binary files a/image/custom.gif and b/image/custom.gif differ diff --git a/lib/graphite.dart b/lib/graphite.dart index f33ca76..2f9d8a0 100644 --- a/lib/graphite.dart +++ b/lib/graphite.dart @@ -1,7 +1,5 @@ library graphite; -import 'dart:math' as math; - import 'package:flutter/widgets.dart'; import 'package:graphite/core/graph.dart'; import 'package:graphite/core/matrix.dart'; @@ -38,14 +36,6 @@ class DirectGraph extends StatefulWidget { /// views and [MatrixOrientation.Vertical] for column views. final MatrixOrientation orientation; - /// is the length (in pixels) of each of the 2 lines making the arrow. - /// Ignored if using custom [pathBuilder] is set. - final double tipLength; - - /// [tipAngle] is the angle (in radians) between each of the 2 lines making the arrow and the curve at this point. - /// Ignored if using custom [pathBuilder] is set. - final double tipAngle; - /// Value for internal [InteractiveViewer.maxScale]. final double maxScale; @@ -111,14 +101,15 @@ class DirectGraph extends StatefulWidget { /// [GestureDetector.onSecondaryTapUp] for node widget. final GestureNodeTapUpCallback? onNodeSecondaryTapUp; - /// [Path] builder function to draw custom shaped edges. + /// [EdgeStyle] builder function to provide custom style for node edges. /// Called on each canvas render cycle, provides info about each - /// edge as [Edge]. If set, arrows on tip of each path wont be added. - final EdgePaintBuilder? paintBuilder; + /// edge as [Edge]. + final EdgeStyleBuilder? styleBuilder; - /// [Paint] builder function to provide custom style for node edges. + /// [Path] builder function to draw custom shaped edges. + /// Allows user to define fully custom edge path shape. /// Called on each canvas render cycle, provides info about each - /// edge as [Edge]. + /// edge as [Edge]. If set, arrows on tip of each path wont be added. final EdgePathBuilder? pathBuilder; /// [GestureDetector.onTapDown] event on any point inside canvas. @@ -176,12 +167,6 @@ class DirectGraph extends StatefulWidget { /// Use [cellPadding] to set padding between nodes. Note, that increasing [cellPadding] will /// increase edges length, which might be useful if you need more space to add overlays or labels. /// - /// [tipLength] is the length (in pixels) of each of the 2 lines making the arrow. - /// Ignored if using custom [pathBuilder] is set. - /// - /// [tipAngle] is the angle (in radians) between each of the 2 lines making the arrow and the curve at this point. - /// Ignored if using custom [pathBuilder] is set. Defaults to [math.pi] * 0.1. - /// /// If [clipBehavior] clipBehavior of internal InteractiveViewer and Stack. /// Defaults to [Clip.hardEdge]. /// @@ -245,12 +230,10 @@ class DirectGraph extends StatefulWidget { this.nodeBuilder, this.contactEdgesDistance = 5.0, this.orientation = MatrixOrientation.Horizontal, - this.tipAngle = math.pi * 0.1, - this.tipLength = 10.0, this.maxScale = 2.5, this.minScale = 0.25, this.pathBuilder, - this.paintBuilder, + this.styleBuilder, this.overlayBuilder, this.edgeLabels, this.onCanvasTap, @@ -301,8 +284,6 @@ class _DirectGraphState extends State { contactEdgesDistance: widget.contactEdgesDistance, orientation: widget.orientation, builder: widget.nodeBuilder, - tipLength: widget.tipLength, - tipAngle: widget.tipAngle, onCanvasTap: widget.onCanvasTap, onNodeTapDown: widget.onNodeTapDown, onNodeTapUp: widget.onNodeTapUp, @@ -318,7 +299,7 @@ class _DirectGraphState extends State { onNodePanDown: widget.onNodePanDown, onNodeSecondaryTapDown: widget.onNodeSecondaryTapDown, onNodeSecondaryTapUp: widget.onNodeSecondaryTapUp, - paintBuilder: widget.paintBuilder, + styleBuilder: widget.styleBuilder, onEdgeTapDown: widget.onEdgeTapDown, onEdgeTapUp: widget.onEdgeTapUp, onEdgeLongPressStart: widget.onEdgeLongPressStart, diff --git a/lib/graphite_canvas.dart b/lib/graphite_canvas.dart index 10eed8f..207d0b8 100644 --- a/lib/graphite_canvas.dart +++ b/lib/graphite_canvas.dart @@ -16,8 +16,6 @@ class GraphiteCanvas extends StatefulWidget { final double contactEdgesDistance; final Matrix matrix; final MatrixOrientation orientation; - final double tipLength; - final double tipAngle; final double maxScale; final double minScale; final NodeCellBuilder? builder; @@ -48,7 +46,7 @@ class GraphiteCanvas extends StatefulWidget { final GestureNodeTapUpCallback? onNodeSecondaryTapUp; // Edge - final EdgePaintBuilder? paintBuilder; + final EdgeStyleBuilder? styleBuilder; final EdgePathBuilder? pathBuilder; final GestureEdgeTapDownCallback? onEdgeTapDown; @@ -77,8 +75,6 @@ class GraphiteCanvas extends StatefulWidget { required this.cellPadding, required this.contactEdgesDistance, required this.orientation, - required this.tipLength, - required this.tipAngle, required this.maxScale, required this.minScale, required this.clipBehavior, @@ -96,7 +92,7 @@ class GraphiteCanvas extends StatefulWidget { this.onEdgeSecondaryTapDown, this.onEdgeSecondaryTapUp, this.onCanvasTap, - this.paintBuilder, + this.styleBuilder, this.pathBuilder, this.onNodeTapDown, this.onNodeTapUp, @@ -499,48 +495,45 @@ class _GraphiteCanvasState extends State { Widget _buildCanvas(BuildContext context, List edges, Size size) { return Container( - width: size.width, - height: size.height, - child: CanvasTouchDetector( - gesturesToOverride: [ - GestureType.onTapDown, - GestureType.onTapUp, - GestureType.onLongPressStart, - GestureType.onLongPressEnd, - GestureType.onLongPressMoveUpdate, - GestureType.onForcePressStart, - GestureType.onForcePressEnd, - GestureType.onForcePressPeak, - GestureType.onForcePressUpdate, - GestureType.onSecondaryTapDown, - GestureType.onSecondaryTapUp, - ], - builder: (BuildContext ctx) { - return CustomPaint( - size: Size.infinite, - painter: LinesPainter( - ctx, - edges, - tipLength: widget.tipLength, - tipAngle: widget.tipAngle, - onCanvasTap: widget.onCanvasTap, - paintBuilder: widget.paintBuilder, - onEdgeTapDown: widget.onEdgeTapDown, - onEdgeTapUp: widget.onEdgeTapUp, - onEdgeLongPressStart: widget.onEdgeLongPressStart, - onEdgeLongPressEnd: widget.onEdgeLongPressEnd, - onEdgeLongPressMoveUpdate: widget.onEdgeLongPressMoveUpdate, - onEdgeForcePressStart: widget.onEdgeForcePressStart, - onEdgeForcePressEnd: widget.onEdgeForcePressEnd, - onEdgeForcePressPeak: widget.onEdgeForcePressPeak, - onEdgeForcePressUpdate: widget.onEdgeForcePressUpdate, - onEdgeSecondaryTapDown: widget.onEdgeSecondaryTapDown, - onEdgeSecondaryTapUp: widget.onEdgeSecondaryTapUp, - pathBuilder: widget.pathBuilder, - ), - ); - }) - ); + width: size.width, + height: size.height, + child: CanvasTouchDetector( + gesturesToOverride: [ + GestureType.onTapDown, + GestureType.onTapUp, + GestureType.onLongPressStart, + GestureType.onLongPressEnd, + GestureType.onLongPressMoveUpdate, + GestureType.onForcePressStart, + GestureType.onForcePressEnd, + GestureType.onForcePressPeak, + GestureType.onForcePressUpdate, + GestureType.onSecondaryTapDown, + GestureType.onSecondaryTapUp, + ], + builder: (BuildContext ctx) { + return CustomPaint( + size: Size.infinite, + painter: LinesPainter( + ctx, + edges, + onCanvasTap: widget.onCanvasTap, + styleBuilder: widget.styleBuilder, + onEdgeTapDown: widget.onEdgeTapDown, + onEdgeTapUp: widget.onEdgeTapUp, + onEdgeLongPressStart: widget.onEdgeLongPressStart, + onEdgeLongPressEnd: widget.onEdgeLongPressEnd, + onEdgeLongPressMoveUpdate: widget.onEdgeLongPressMoveUpdate, + onEdgeForcePressStart: widget.onEdgeForcePressStart, + onEdgeForcePressEnd: widget.onEdgeForcePressEnd, + onEdgeForcePressPeak: widget.onEdgeForcePressPeak, + onEdgeForcePressUpdate: widget.onEdgeForcePressUpdate, + onEdgeSecondaryTapDown: widget.onEdgeSecondaryTapDown, + onEdgeSecondaryTapUp: widget.onEdgeSecondaryTapUp, + pathBuilder: widget.pathBuilder, + ), + ); + })); } Widget _buildStack(BuildContext context, Size size) { diff --git a/lib/graphite_edges_painter.dart b/lib/graphite_edges_painter.dart index a40e5fd..086c8fa 100644 --- a/lib/graphite_edges_painter.dart +++ b/lib/graphite_edges_painter.dart @@ -1,25 +1,20 @@ +import 'dart:math'; + import 'package:arrow_path/arrow_path.dart'; import 'package:flutter/material.dart'; import 'package:graphite/core/typings.dart'; import 'package:graphite/graphite_typings.dart'; import 'package:touchable/touchable.dart'; -Paint _defaultPaintBuilder(Edge edge) { - return Paint() - ..color = Color(0xFF000000) - ..style = PaintingStyle.stroke - ..strokeCap = StrokeCap.round - ..strokeJoin = StrokeJoin.round - ..strokeWidth = 2; +EdgeStyle _defaultStyleBuilder(Edge edge) { + return EdgeStyle(); } class LinesPainter extends CustomPainter { final List edges; final BuildContext context; - final EdgePaintBuilder? paintBuilder; + final EdgeStyleBuilder? styleBuilder; final EdgePathBuilder? pathBuilder; - final double tipLength; - final double tipAngle; final GestureBackgroundTapCallback? onCanvasTap; final GestureEdgeTapDownCallback? onEdgeTapDown; @@ -41,26 +36,9 @@ class LinesPainter extends CustomPainter { final GestureEdgeTapUpCallback? onEdgeSecondaryTapUp; - Path _defaultEdgePathBuilder(NodeInput from, NodeInput to, - List> points, EdgeArrowType arrowType) { - var path = Path(); - path.moveTo(points[0][0], points[0][1]); - points.sublist(1).forEach((p) => path.lineTo(p[0], p[1])); - if (arrowType == EdgeArrowType.none) { - return path; - } - return ArrowPath.make( - path: path, - isDoubleSided: arrowType == EdgeArrowType.both, - tipLength: tipLength, - tipAngle: tipAngle); - } - const LinesPainter( this.context, this.edges, { - required this.tipLength, - required this.tipAngle, this.onCanvasTap, this.onEdgeTapDown, this.edgePaintStyleForTouch, @@ -74,10 +52,24 @@ class LinesPainter extends CustomPainter { this.onEdgeForcePressUpdate, this.onEdgeSecondaryTapDown, this.onEdgeSecondaryTapUp, - this.paintBuilder, + this.styleBuilder, this.pathBuilder, }); + bool areNoListenersDefined() { + return onEdgeTapDown == null && + onEdgeTapUp == null && + onEdgeLongPressStart == null && + onEdgeLongPressEnd == null && + onEdgeLongPressMoveUpdate == null && + onEdgeForcePressStart == null && + onEdgeForcePressEnd == null && + onEdgeForcePressPeak == null && + onEdgeForcePressUpdate == null && + onEdgeSecondaryTapDown == null && + onEdgeSecondaryTapUp == null; + } + @override void paint(Canvas c, Size size) { var canvas = TouchyCanvas(context, c); @@ -90,27 +82,35 @@ class LinesPainter extends CustomPainter { onTapDown: onCanvasTap != null ? onCanvasTap : null); List _state = edges; _state.forEach((e) { - var points = e.points.reversed.toList(); - var path = pathBuilder == null + final points = e.points.reversed.toList(); + final style = + styleBuilder == null ? _defaultStyleBuilder(e) : styleBuilder!(e); + final path = pathBuilder == null ? _defaultEdgePathBuilder( - e.from.toInput(), e.to.toInput(), points, e.arrowType) - : pathBuilder!(e.from.toInput(), e.to.toInput(), points, e.arrowType); - final paint = - paintBuilder == null ? _defaultPaintBuilder(e) : paintBuilder!(e); + e.from.toInput(), e.to.toInput(), points, style) + : pathBuilder!(e.from.toInput(), e.to.toInput(), points, style); + path.close(); + final paint = style.linePaint; c.drawPath( path, paint, ); - // add transparent wider lines on top to track gestures - for (int i = 1; i < points.length; i++) { - var p = Paint() - ..color = Colors.transparent - ..style = PaintingStyle.stroke - ..strokeWidth = paint.strokeWidth * 3; - canvas.drawLine( - Offset(points[i - 1][0], points[i - 1][1]), - Offset(points[i][0], points[i][1]), - p, + + if (areNoListenersDefined()) return; + + // add wider transparent lines with made off hit dots on top to track gestures + final gestureHitRadius = max(style.linePaint.strokeWidth, 10.0); + var p = style.linePaint + ..color = Colors.transparent + ..style = PaintingStyle.fill + ..strokeWidth = gestureHitRadius; + + var pp = pathBuilder != null + ? pathBuilder!(e.from.toInput(), e.to.toInput(), points, style) + : _getPathFromPoints(e.from.toInput(), e.to.toInput(), points, style); + pp = _createHitPath(pp, gestureHitRadius); + + canvas.drawPath(pp, p, paintStyleForTouch: PaintingStyle.fill, onTapDown: onEdgeTapDown != null ? (details) => onEdgeTapDown!(details, e) @@ -144,9 +144,7 @@ class LinesPainter extends CustomPainter { : null, onSecondaryTapUp: onEdgeSecondaryTapUp != null ? (details) => onEdgeSecondaryTapUp!(details, e) - : null, - ); - } + : null); }); } @@ -155,3 +153,192 @@ class LinesPainter extends CustomPainter { return true; } } + +Path _getPathFromPoints( + NodeInput from, NodeInput to, List> points, EdgeStyle style) { + var path = Path(); + + List offsetPoints = points.map((p) => Offset(p[0], p[1])).toList(); + List smoothPoints = _smoothCorners(offsetPoints, style.borderRadius); + + path.moveTo(smoothPoints.first.dx, smoothPoints.first.dy); + for (int i = 1; i < smoothPoints.length; i++) { + if (smoothPoints[i] is ArcPoint) { + ArcPoint arcPoint = smoothPoints[i] as ArcPoint; + path.arcToPoint( + arcPoint.end, + radius: Radius.circular(arcPoint.radius), + clockwise: arcPoint.clockwise, + ); + } else { + path.lineTo(smoothPoints[i].dx, smoothPoints[i].dy); + } + } + return path; +} + +Path _defaultEdgePathBuilder( + NodeInput from, NodeInput to, List> points, EdgeStyle style) { + Path path = _getPathFromPoints(from, to, points, style); + Path styledPath = _applyLineStyle(path, style.lineStyle, style.dashLength, + style.gapLength, style.dotLength); + + if (style.arrowType == EdgeArrowType.none) { + return styledPath; + } + return ArrowPath.make( + path: styledPath, + isDoubleSided: style.arrowType == EdgeArrowType.both, + tipLength: style.tipLength, + tipAngle: style.tipAngle); +} + +List _smoothCorners(List points, double radius) { + if (points.length < 3 || radius <= 0) return points; + + List smoothPoints = []; + smoothPoints.add(points.first); + + for (int i = 1; i < points.length - 1; i++) { + Offset prev = points[i - 1]; + Offset curr = points[i]; + Offset next = points[i + 1]; + + Offset toPrev = prev - curr; + Offset toNext = next - curr; + + double angle = _angleBetween(toPrev, toNext); + + double actualRadius = + min(radius, min(toPrev.distance / 2, toNext.distance / 2)); + + Offset toPrevNorm = toPrev / toPrev.distance; + Offset toNextNorm = toNext / toNext.distance; + + Offset cornerStart = curr + toPrevNorm * actualRadius; + Offset cornerEnd = curr + toNextNorm * actualRadius; + + smoothPoints.add(cornerStart); + smoothPoints.add(ArcPoint(cornerEnd, actualRadius, angle < 0)); + } + + smoothPoints.add(points.last); + return smoothPoints; +} + +double _angleBetween(Offset v1, Offset v2) { + return atan2(v1.dx * v2.dy - v1.dy * v2.dx, v1.dx * v2.dx + v1.dy * v2.dy); +} + +class ArcPoint extends Offset { + final Offset end; + final double radius; + final bool clockwise; + + ArcPoint(this.end, this.radius, this.clockwise) : super(end.dx, end.dy); +} + +Path _applyLineStyle(Path originalPath, LineStyle style, double dashLength, + double gapLength, double dotLength) { + if (style == LineStyle.solid) { + return originalPath; + } + var path = Path(); + var metrics = originalPath.computeMetrics(); + + for (var metric in metrics) { + var length = metric.length; + var distance = 0.0; + + while (distance < length) { + var tangent = metric.getTangentForOffset(distance)!; + var start = tangent.position; + var remainingLength = length - distance; + + switch (style) { + case LineStyle.solid: + return originalPath; + case LineStyle.dashed: + var dashEnd = metric + .getTangentForOffset( + distance + dashLength.clamp(0, remainingLength))! + .position; + path.moveTo(start.dx, start.dy); + path.lineTo(dashEnd.dx, dashEnd.dy); + distance += dashLength + gapLength; + break; + case LineStyle.dotted: + // draw last line at the and of the line to make arrows look ok + if (distance + dotLength + gapLength >= length) { + var dashEnd = metric + .getTangentForOffset( + distance + dashLength.clamp(0, remainingLength))! + .position; + path.moveTo(start.dx, start.dy); + path.lineTo(dashEnd.dx, dashEnd.dy); + distance += dashLength + gapLength; + break; + } + path.addOval(Rect.fromCircle(center: start, radius: dotLength / 2)); + distance += dotLength + gapLength; + break; + case LineStyle.dashDotted: + var dashEnd = metric + .getTangentForOffset( + distance + dashLength.clamp(0, remainingLength))! + .position; + path.moveTo(start.dx, start.dy); + path.lineTo(dashEnd.dx, dashEnd.dy); + distance += dashLength + gapLength; + + if (distance < length) { + var dotPosition = metric.getTangentForOffset(distance)!.position; + path.addOval( + Rect.fromCircle(center: dotPosition, radius: dotLength / 2)); + distance += dotLength + gapLength; + } + break; + } + } + } + + return path; +} + +Path _createHitPath(Path inputPath, double hitWidth) { + final hitPath = Path(); + final metric = inputPath.computeMetrics().first; + final halfWidth = hitWidth / 2; + + var distance = 0.0; + final increment = 5.0; + + while (distance < metric.length) { + final tangent = metric.getTangentForOffset(distance)!; + final normal = Offset(-tangent.vector.dy, tangent.vector.dx); + + final outerPoint = tangent.position + normal * halfWidth; + + if (distance == 0) { + hitPath.moveTo(outerPoint.dx, outerPoint.dy); + } else { + hitPath.lineTo(outerPoint.dx, outerPoint.dy); + } + + distance += increment; + } + + distance = metric.length; + while (distance > 0) { + final tangent = metric.getTangentForOffset(distance)!; + final normal = Offset(-tangent.vector.dy, tangent.vector.dx); + + final innerPoint = tangent.position - normal * halfWidth; + + hitPath.lineTo(innerPoint.dx, innerPoint.dy); + + distance -= increment; + } + hitPath.close(); + return hitPath; +} diff --git a/lib/graphite_root.dart b/lib/graphite_root.dart index e310af9..7502efb 100644 --- a/lib/graphite_root.dart +++ b/lib/graphite_root.dart @@ -10,8 +10,6 @@ class GraphiteRoot extends StatefulWidget { final EdgeInsets cellPadding; final double contactEdgesDistance; final MatrixOrientation orientation; - final double tipLength; - final double tipAngle; final double maxScale; final double minScale; final Clip clipBehavior; @@ -48,7 +46,7 @@ class GraphiteRoot extends StatefulWidget { final GestureNodeTapUpCallback? onNodeSecondaryTapUp; // Edge - final EdgePaintBuilder? paintBuilder; + final EdgeStyleBuilder? styleBuilder; final EdgePathBuilder? pathBuilder; final GestureBackgroundTapCallback? onCanvasTap; @@ -74,8 +72,6 @@ class GraphiteRoot extends StatefulWidget { required this.mtx, required this.defaultCellSize, required this.cellPadding, - required this.tipLength, - required this.tipAngle, required this.maxScale, required this.minScale, required this.orientation, @@ -95,7 +91,7 @@ class GraphiteRoot extends StatefulWidget { this.onEdgeForcePressUpdate, this.onEdgeSecondaryTapDown, this.onEdgeSecondaryTapUp, - this.paintBuilder, + this.styleBuilder, this.onNodeTapDown, this.onNodeTapUp, this.onNodeLongPressStart, @@ -129,7 +125,7 @@ class _GraphiteRootState extends State { cellPadding: widget.cellPadding, contactEdgesDistance: widget.contactEdgesDistance, orientation: widget.orientation, - paintBuilder: widget.paintBuilder, + styleBuilder: widget.styleBuilder, onCanvasTap: widget.onCanvasTap, onEdgeTapDown: widget.onEdgeTapDown, onEdgeTapUp: widget.onEdgeTapUp, @@ -142,8 +138,6 @@ class _GraphiteRootState extends State { onEdgeForcePressUpdate: widget.onEdgeForcePressUpdate, onEdgeSecondaryTapDown: widget.onEdgeSecondaryTapDown, onEdgeSecondaryTapUp: widget.onEdgeSecondaryTapUp, - tipAngle: widget.tipAngle, - tipLength: widget.tipLength, maxScale: widget.maxScale, minScale: widget.minScale, pathBuilder: widget.pathBuilder, diff --git a/lib/graphite_typings.dart b/lib/graphite_typings.dart index 9ea762c..1f52f79 100644 --- a/lib/graphite_typings.dart +++ b/lib/graphite_typings.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/widgets.dart'; import 'package:graphite/core/typings.dart'; @@ -74,9 +76,8 @@ typedef GestureNodePanUpdateCallback = void Function( typedef GestureNodePanDownCallback = void Function( DragDownDetails details, MatrixNode node, Rect rect); -typedef GestureBackgroundTapCallback = void Function( - TapDownDetails details); -typedef EdgePaintBuilder = Paint Function(Edge edge); +typedef GestureBackgroundTapCallback = void Function(TapDownDetails details); +typedef EdgeStyleBuilder = EdgeStyle Function(Edge edge); typedef GestureEdgeTapDownCallback = void Function( TapDownDetails details, Edge edge); typedef GestureEdgeTapUpCallback = void Function( @@ -104,4 +105,61 @@ typedef GestureEdgeDragDownCallback = void Function( DragDownDetails details, Edge edge); typedef EdgePathBuilder = Path Function(NodeInput income, NodeInput node, - List> points, EdgeArrowType arrowType); + List> points, EdgeStyle style); + +enum LineStyle { solid, dashed, dotted, dashDotted } + +class EdgeStyle { + /// [LineStyle] of edges to draw. + final LineStyle lineStyle; + + /// [Paint] of edges to use in drawing. + final Paint linePaint; + + /// border radius of edges angles. + final double borderRadius; + + /// length of dash in [LineStyle.dashed] + /// and [LineStyle.dashDotted] styles. Ignored if + /// style is [LineStyle.solid] or [LineStyle.dotted]. + final double dashLength; + + /// length of a gap in [LineStyle.dotted], [LineStyle.dashed] + /// and [LineStyle.dashDotted] styles. Ignored if + /// style is [LineStyle.solid]. + final double gapLength; + + /// diameter of dot in [LineStyle.dotted] + /// and [LineStyle.dashDotted] styles. Ignored if + /// style is [LineStyle.solid] or [LineStyle.dashed]. + final double dotLength; + + /// the type of arrows on the edge. + final EdgeArrowType arrowType; + + /// is the length (in pixels) of each of the 2 lines making the arrow. + /// Ignored if using custom [pathBuilder] is set. + final double tipLength; + + /// [tipAngle] is the angle (in radians) between each of the 2 lines making the arrow and the curve at this point. + /// Ignored if using custom [pathBuilder] is set. + final double tipAngle; + + EdgeStyle({ + Paint? linePaint, + this.lineStyle = LineStyle.solid, + this.borderRadius = 0, + this.dashLength = 10, + this.gapLength = 5, + this.dotLength = 2, + this.arrowType = EdgeArrowType.one, + this.tipAngle = pi * 0.1, + this.tipLength = 10.0, + }) : this.linePaint = linePaint ?? + (Paint() + ..color = Color(0xFF000000) + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round + ..strokeJoin = StrokeJoin.round + ..strokeWidth = 2); +} diff --git a/pubspec.lock b/pubspec.lock index 94e9c79..d014829 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -45,10 +45,10 @@ packages: dependency: transitive description: name: collection - sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.1" + version: "1.18.0" fake_async: dependency: transitive description: @@ -67,46 +67,62 @@ packages: description: flutter source: sdk version: "0.0.0" - js: + leak_tracker: dependency: transitive description: - name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + name: leak_tracker + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" url: "https://pub.dev" source: hosted - version: "0.6.7" + version: "10.0.4" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + url: "https://pub.dev" + source: hosted + version: "3.0.3" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" matcher: dependency: transitive description: name: matcher - sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.15" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.12.0" path: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" sky_engine: dependency: transitive description: flutter @@ -116,26 +132,26 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" stack_trace: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" string_scanner: dependency: transitive description: @@ -156,10 +172,10 @@ packages: dependency: transitive description: name: test_api - sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.7.0" touchable: dependency: "direct main" description: @@ -176,5 +192,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + url: "https://pub.dev" + source: hosted + version: "14.2.1" sdks: - dart: ">=3.0.0-0 <4.0.0" + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/pubspec.yaml b/pubspec.yaml index 4eed044..729cf30 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: graphite description: Flutter widget to easily draw direct graphs, trees, flowcharts. Includes gesture API to create graphs interactions. -version: 1.1.2 +version: 1.2.0 homepage: https://github.com/lempiy/flutter_graphite environment: