@@ -151,6 +151,17 @@ class CubicToCommand extends PathCommand {
151151 const CubicToCommand (this .x1, this .y1, this .x2, this .y2, this .x3, this .y3)
152152 : super ._(PathCommandType .cubic);
153153
154+ /// Creates a cubic command from the current point to [end] using [control1]
155+ /// and [control2] as control points.
156+ CubicToCommand .fromPoints (Point control1, Point control2, Point end)
157+ : this (control1.x, control1.y, control2.x, control2.y, end.x, end.y);
158+
159+ factory CubicToCommand ._fromIterablePoints (Iterable <Point > points) {
160+ final List <Point > list = points.toList ();
161+ assert (list.length == 3 );
162+ return CubicToCommand .fromPoints (list[0 ], list[1 ], list[2 ]);
163+ }
164+
154165 /// The absolute offset of the first control point for this path from the x
155166 /// axis.
156167 final double x1;
@@ -159,6 +170,9 @@ class CubicToCommand extends PathCommand {
159170 /// axis.
160171 final double y1;
161172
173+ /// A [Point] representation of [x1] ,[y1] , the first control point.
174+ Point get controlPoint1 => Point (x1, y1);
175+
162176 /// The absolute offset of the second control point for this path from the x
163177 /// axis.
164178 final double x2;
@@ -167,6 +181,9 @@ class CubicToCommand extends PathCommand {
167181 /// axis.
168182 final double y2;
169183
184+ /// A [Point] representation of [x2] ,[y2] , the second control point.
185+ Point get controlPoint2 => Point (x2, y2);
186+
170187 /// The absolute offset of the destination point for this path from the x
171188 /// axis.
172189 final double x3;
@@ -175,6 +192,83 @@ class CubicToCommand extends PathCommand {
175192 /// axis.
176193 final double y3;
177194
195+ /// A [Point] representation of [x3] ,[y3] , the end point of the curve.
196+ Point get endPoint => Point (x3, y3);
197+
198+ /// Subdivides the cubic curve described by [start] , [control1] , [control2] ,
199+ /// [end] .
200+ ///
201+ /// The returned list describes two cubics, where elements `0, 1, 2, 3` are
202+ /// the start, cp1, cp2, and end points of the first cubic and `3, 4, 5, 6`
203+ /// are the start, cp1, cp2, and end points of the second cubic.
204+ static List <Point > subdivide (
205+ Point start,
206+ Point control1,
207+ Point control2,
208+ Point end,
209+ double t,
210+ ) {
211+ final Point ab = Point .lerp (start, control1, t);
212+ final Point bc = Point .lerp (control1, control2, t);
213+ final Point cd = Point .lerp (control2, end, t);
214+ final Point abc = Point .lerp (ab, bc, t);
215+ final Point bcd = Point .lerp (bc, cd, t);
216+ final Point abcd = Point .lerp (abc, bcd, t);
217+ return < Point > [
218+ start,
219+ ab,
220+ abc,
221+ abcd,
222+ bcd,
223+ cd,
224+ end,
225+ ];
226+ }
227+
228+ /// Precisions > 1 increase precision at cost of extra computation time.
229+ double computeLength (Point start) {
230+ // Mike Reed just made this up! The nerve of him.
231+ // One difference from Skia is just setting a default tolerance of 3. This
232+ // is good enough for a particular test SVG that has this curve:
233+ // M65 33c0 17.673-14.326 32-32 32S1 50.673 1 33C1 15.327 15.326 1 33 1s32 14.327 32 32z
234+ // Lower values end up getting the end points wrong when dashing a path.
235+ const double tolerance = 1 / 2 * 3 ;
236+
237+ double _compute (
238+ Point p1,
239+ Point cp1,
240+ Point cp2,
241+ Point p2,
242+ double distance,
243+ ) {
244+ // If it's "too curvy," cut it in half
245+ if (Point .distance (cp1, Point .lerp (p1, p2, 1 / 3 )) > tolerance ||
246+ Point .distance (cp2, Point .lerp (p1, p2, 2 / 3 )) > tolerance) {
247+ final List <Point > points = subdivide (p1, cp1, cp2, p2, .5 );
248+ distance = _compute (
249+ points[0 ],
250+ points[1 ],
251+ points[2 ],
252+ points[3 ],
253+ distance,
254+ );
255+ distance = _compute (
256+ points[3 ],
257+ points[4 ],
258+ points[5 ],
259+ points[6 ],
260+ distance,
261+ );
262+ } else {
263+ // It's collinear enough to just treat as a line.
264+ distance += Point .distance (p1, p2);
265+ }
266+ return distance;
267+ }
268+
269+ return _compute (start, Point (x1, y1), Point (x2, y2), Point (x3, y3), 0 );
270+ }
271+
178272 @override
179273 CubicToCommand transformed (AffineMatrix matrix) {
180274 final Point xy1 = matrix.transformPoint (Point (x1, y1));
@@ -244,16 +338,9 @@ class PathBuilder implements PathProxy {
244338
245339 final List <PathCommand > _commands = < PathCommand > [];
246340
247- Point _currentSubPathPoint = Point .zero;
248-
249- /// The last destination point used by this builder.
250- Point get currentPoint => _currentPoint;
251- Point _currentPoint = Point .zero;
252-
253341 @override
254342 PathBuilder close () {
255343 _commands.add (const CloseCommand ());
256- _currentPoint = _currentSubPathPoint;
257344 return this ;
258345 }
259346
@@ -267,21 +354,18 @@ class PathBuilder implements PathProxy {
267354 double y3,
268355 ) {
269356 _commands.add (CubicToCommand (x1, y1, x2, y2, x3, y3));
270- _currentPoint = Point (x3, y3);
271357 return this ;
272358 }
273359
274360 @override
275361 PathBuilder lineTo (double x, double y) {
276362 _commands.add (LineToCommand (x, y));
277- _currentPoint = Point (x, y);
278363 return this ;
279364 }
280365
281366 @override
282367 PathBuilder moveTo (double x, double y) {
283368 _commands.add (MoveToCommand (x, y));
284- _currentPoint = _currentSubPathPoint = Point (x, y);
285369 return this ;
286370 }
287371
@@ -323,10 +407,10 @@ class PathBuilder implements PathProxy {
323407
324408 /// Adds a rectangle to the new path.
325409 PathBuilder addRect (Rect rect) {
326- moveTo (rect.right, rect.top);
410+ moveTo (rect.left, rect.top);
411+ lineTo (rect.right, rect.top);
327412 lineTo (rect.right, rect.bottom);
328413 lineTo (rect.left, rect.bottom);
329- lineTo (rect.left, rect.top);
330414 close ();
331415 return this ;
332416 }
@@ -462,6 +546,20 @@ class Path {
462546 other.fillType == fillType;
463547 }
464548
549+ /// Creates a dashed version of this path.
550+ ///
551+ /// The interval list is read in a circular fashion, such that the first
552+ /// interval is used to dash and the second to move. If the list is an odd
553+ /// number of elements, it is effectively the same as if it were repeated
554+ /// twice.
555+ ///
556+ /// Callers are responsible for not passing interval lists consisting entirely
557+ /// of `0` .
558+ Path dashed (List <double > intervals) {
559+ final _PathDasher dasher = _PathDasher (intervals);
560+ return dasher.dash (this );
561+ }
562+
465563 /// Compute the bounding box for the given path segment.
466564 Rect bounds () {
467565 if (_commands.isEmpty) {
@@ -549,3 +647,127 @@ Path parseSvgPathData(String svg, [PathFillType type = PathFillType.nonZero]) {
549647 }
550648 return pathBuilder.toPath ();
551649}
650+
651+ class _CircularIntervalList {
652+ _CircularIntervalList (this ._vals)
653+ : assert (_vals.isNotEmpty),
654+ assert (! _vals.every ((double val) => val == 0 ));
655+
656+ final List <double > _vals;
657+ int _idx = 0 ;
658+
659+ double get next {
660+ if (_idx >= _vals.length) {
661+ _idx = 0 ;
662+ }
663+ return _vals[_idx++ ];
664+ }
665+ }
666+
667+ class _PathDasher {
668+ _PathDasher (List <double > intervals)
669+ : assert (! intervals.every ((double interval) => interval == 0 )),
670+ _intervals = _CircularIntervalList (intervals);
671+
672+ final _CircularIntervalList _intervals;
673+
674+ late double length;
675+ Point currentPoint = Point .zero;
676+ Point currentSubpathPoint = Point .zero;
677+ late bool draw;
678+
679+ final List <PathCommand > _dashedCommands = < PathCommand > [];
680+
681+ void _dashLineTo (Point target) {
682+ double distance = Point .distance (currentPoint, target);
683+
684+ if (distance <= 0 || length <= 0 ) {
685+ return ;
686+ }
687+
688+ while (distance >= length) {
689+ final double t = length / distance;
690+ currentPoint = Point .lerp (currentPoint, target, t);
691+ length = _intervals.next;
692+
693+ if (draw) {
694+ _dashedCommands.add (LineToCommand (currentPoint.x, currentPoint.y));
695+ } else {
696+ _dashedCommands.add (MoveToCommand (currentPoint.x, currentPoint.y));
697+ }
698+
699+ distance = Point .distance (currentPoint, target);
700+ draw = ! draw;
701+ }
702+ if (distance > 0 ) {
703+ length -= distance;
704+ if (draw) {
705+ _dashedCommands.add (LineToCommand (target.x, target.y));
706+ }
707+ }
708+ currentPoint = target;
709+ }
710+
711+ void _dashCubicTo (CubicToCommand cubic) {
712+ double distance = cubic.computeLength (currentPoint);
713+ while (distance >= length) {
714+ final double t = length / distance;
715+ final List <Point > dividedPoints = CubicToCommand .subdivide (
716+ currentPoint,
717+ cubic.controlPoint1,
718+ cubic.controlPoint2,
719+ cubic.endPoint,
720+ t,
721+ );
722+ currentPoint = dividedPoints[3 ];
723+ if (draw) {
724+ _dashedCommands.add (CubicToCommand ._fromIterablePoints (
725+ dividedPoints.skip (1 ).take (3 ),
726+ ));
727+ } else {
728+ _dashedCommands.add (MoveToCommand (
729+ currentPoint.x,
730+ currentPoint.y,
731+ ));
732+ }
733+ cubic = CubicToCommand ._fromIterablePoints (
734+ dividedPoints.skip (4 ).take (3 ),
735+ );
736+ length = _intervals.next;
737+ distance = cubic.computeLength (currentPoint);
738+ draw = ! draw;
739+ }
740+ length -= distance;
741+ currentPoint = cubic.endPoint;
742+ if (draw) {
743+ _dashedCommands.add (cubic);
744+ }
745+ }
746+
747+ Path dash (Path path) {
748+ length = _intervals.next;
749+ draw = true ;
750+ for (final PathCommand command in path._commands) {
751+ switch (command.type) {
752+ case PathCommandType .move:
753+ final MoveToCommand move = command as MoveToCommand ;
754+ currentPoint = Point (move.x, move.y);
755+ currentSubpathPoint = currentPoint;
756+ _dashedCommands.add (command);
757+ break ;
758+ case PathCommandType .line:
759+ final LineToCommand line = command as LineToCommand ;
760+ _dashLineTo (Point (line.x, line.y));
761+ break ;
762+ case PathCommandType .cubic:
763+ _dashCubicTo (command as CubicToCommand );
764+ break ;
765+ case PathCommandType .close:
766+ _dashLineTo (currentSubpathPoint);
767+ currentPoint = currentSubpathPoint;
768+ break ;
769+ }
770+ }
771+ return Path (commands: _dashedCommands, fillType: path.fillType);
772+ }
773+ }
0 commit comments