Line data Source code
1 : import 'dart:math';
2 :
3 : import 'package:mrx_icon_font_gen/parser/path/model/arguments/coordinate_pair.dart';
4 : import 'package:mrx_icon_font_gen/parser/path/model/arguments/coordinate_pair_sequence.dart';
5 : import 'package:mrx_icon_font_gen/parser/path/model/arguments/curve_to_coordinate_sequence.dart';
6 : import 'package:mrx_icon_font_gen/parser/path/model/arguments/elliptical_arc_argument.dart';
7 : import 'package:mrx_icon_font_gen/parser/path/model/arguments/elliptical_arc_argument_sequence.dart';
8 : import 'package:mrx_icon_font_gen/parser/path/model/command.dart';
9 : import 'package:mrx_icon_font_gen/parser/path/model/commands/curve_to_command.dart';
10 : import 'package:mrx_icon_font_gen/parser/path/model/commands/line_to_command.dart';
11 : import 'package:vector_math/vector_math_64.dart';
12 :
13 : class EllipticalArcCommand extends Command {
14 2 : @override
15 : String get absoluteCommandName => 'A';
16 :
17 2 : @override
18 : String get relativeCommandName => 'a';
19 :
20 2 : EllipticalArcCommand({
21 : String? command,
22 : bool? isRelative,
23 : required EllipticalArcArgumentSequence commandArguments,
24 2 : }) : super(
25 : command: command,
26 : isRelative: isRelative,
27 : commandArguments: commandArguments,
28 : );
29 :
30 1 : @override
31 : List<Command> applyTransformation(
32 : Matrix3 transform,
33 : CoordinatePair startPoint,
34 : ) {
35 : final EllipticalArcArgumentSequence argumentSequence =
36 1 : commandArguments as EllipticalArcArgumentSequence;
37 1 : final List<Command> bezierCurves = [];
38 : CoordinatePair lastPoint = startPoint;
39 2 : for (final arguments in argumentSequence.ellipticalArcArguments) {
40 : final List<Command> newBezierCurves =
41 1 : _convertToBezierCurves(arguments, lastPoint);
42 2 : for (final Command curve in newBezierCurves) {
43 2 : bezierCurves.addAll(curve.applyTransformation(transform, lastPoint));
44 1 : lastPoint = curve.getLastPoint(lastPoint);
45 : }
46 : }
47 : return bezierCurves;
48 : }
49 :
50 : // This function is a port of SVGAndroidRenderer::arcTo method from
51 : // BigBadaboom's androidsvg library (Apache 2 license).
52 1 : List<Command> _convertToBezierCurves(
53 : EllipticalArcArgument arguments,
54 : CoordinatePair startPoint,
55 : ) {
56 4 : final double x = arguments.x + (isAbsolute ? 0 : startPoint.x);
57 4 : final double y = arguments.y + (isAbsolute ? 0 : startPoint.y);
58 :
59 4 : if ((startPoint.x == x && startPoint.y == y) ||
60 2 : arguments.rx == 0.0 ||
61 2 : arguments.ry == 0.0) {
62 1 : return [
63 1 : LineToCommand(
64 1 : isRelative: isRelative,
65 1 : commandArguments: CoordinatePairSequence(
66 1 : coordinatePairs: [
67 1 : CoordinatePair(
68 : x: x,
69 : y: y,
70 : ),
71 : ],
72 : ),
73 : ),
74 : ];
75 : }
76 :
77 : // Sign of the radii is ignored (behaviour specified by the spec)
78 2 : double rx = arguments.rx.abs();
79 2 : double ry = arguments.ry.abs();
80 :
81 1 : final double twoPi = pi * 2.0;
82 :
83 : // Convert angle from degrees to radians
84 : final double angleRad =
85 4 : arguments.xAxisRotation.remainder(360.0) * twoPi / 360.0;
86 1 : final double cosAngle = cos(angleRad);
87 1 : final double sinAngle = sin(angleRad);
88 :
89 : // We simplify the calculations by transforming the arc so that the origin is at the
90 : // midpoint calculated above followed by a rotation to line up the coordinate axes
91 : // with the axes of the ellipse.
92 :
93 : // Compute the midpoint of the line between the current and the end point
94 3 : final double dx2 = (startPoint.x - x) / 2.0;
95 3 : final double dy2 = (startPoint.y - y) / 2.0;
96 :
97 : // Step 1 : Compute (x1', y1')
98 : // x1,y1 is the midpoint vector rotated to take the arc's angle out of consideration
99 3 : final double x1 = (cosAngle * dx2 + sinAngle * dy2);
100 4 : final double y1 = (-sinAngle * dx2 + cosAngle * dy2);
101 :
102 1 : double rxSq = rx * rx;
103 1 : double rySq = ry * ry;
104 1 : final double x1Sq = x1 * x1;
105 1 : final double y1Sq = y1 * y1;
106 :
107 : // Check that radii are large enough.
108 : // If they are not, the spec says to scale them up so they are.
109 : // This is to compensate for potential rounding errors/differences between SVG implementations.
110 3 : final double radiiCheck = x1Sq / rxSq + y1Sq / rySq;
111 1 : if (radiiCheck > 0.99999) {
112 2 : final double radiiScale = sqrt(radiiCheck) * 1.00001;
113 1 : rx = radiiScale * rx;
114 1 : ry = radiiScale * ry;
115 1 : rxSq = rx * rx;
116 1 : rySq = ry * ry;
117 : }
118 :
119 : // Step 2 : Compute (cx1, cy1) - the transformed centre point
120 3 : double sign = (arguments.largeArcFlag == arguments.sweepFlag) ? -1 : 1;
121 1 : final double sq = max(
122 : 0,
123 6 : ((rxSq * rySq) - (rxSq * y1Sq) - (rySq * x1Sq)) /
124 3 : ((rxSq * y1Sq) + (rySq * x1Sq)),
125 : );
126 2 : final double coefficient = (sign * sqrt(sq));
127 3 : final double cx1 = coefficient * ((rx * y1) / ry);
128 4 : final double cy1 = coefficient * -((ry * x1) / rx);
129 :
130 : // Step 3 : Compute (cx, cy) from (cx1, cy1)
131 3 : final double sx2 = (startPoint.x + x) / 2.0;
132 3 : final double sy2 = (startPoint.y + y) / 2.0;
133 4 : final double cx = sx2 + (cosAngle * cx1 - sinAngle * cy1);
134 4 : final double cy = sy2 + (sinAngle * cx1 + cosAngle * cy1);
135 :
136 : // Step 4 : Compute the angleStart (angle1) and the angleExtent (dangle)
137 2 : final double ux = (x1 - cx1) / rx;
138 2 : final double uy = (y1 - cy1) / ry;
139 3 : final double vx = (-x1 - cx1) / rx;
140 3 : final double vy = (-y1 - cy1) / ry;
141 :
142 : // Angle between two vectors is +/- arcCos( u.v / len(u) * len(v))
143 : // Where '.' is the dot product. And +/- is calculated from the sign of the cross product (u x v)
144 :
145 : // Compute the start angle
146 : // The angle between (ux,uy) and the 0deg angle (1,0)
147 4 : double n = sqrt((ux * ux) + (uy * uy)); // len(u) * len(1,0) == len(u)
148 : double p = ux; // u.v == (ux,uy).(1,0) == (1 * ux) + (0 * uy) == ux
149 2 : sign = (uy < 0) ? -1.0 : 1.0; // u x v == (1 * uy - ux * 0) == uy
150 : // No need for checkedArcCos() here. (p >= n) should always be true.
151 3 : double angleStart = sign * acos(p / n);
152 :
153 : // Compute the angle extent
154 8 : n = sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy));
155 3 : p = ux * vx + uy * vy;
156 5 : sign = (ux * vy - uy * vx < 0) ? -1.0 : 1.0;
157 3 : double angleExtent = sign * checkedArcCos(p / n);
158 :
159 3 : if (arguments.sweepFlag == 0 && angleExtent > 0) {
160 1 : angleExtent -= twoPi;
161 3 : } else if (arguments.sweepFlag == 1 && angleExtent < 0) {
162 1 : angleExtent += twoPi;
163 : }
164 1 : angleExtent = angleExtent.remainder(twoPi);
165 1 : angleStart = angleStart.remainder(twoPi);
166 :
167 : // Many elliptical arc implementations including the Java2D and Android ones, only
168 : // support arcs that are axis aligned. Therefore we need to substitute the arc
169 : // with bezier curves. The following method call will generate the beziers for
170 : // a unit circle that covers the arc angles we want.
171 1 : List<CoordinatePair> bezierPoints = arcToBeziers(angleStart, angleExtent);
172 :
173 : // Calculate a transformation matrix that will move and scale these bezier points to the correct location.
174 : // translate
175 3 : Matrix3 m = Matrix3.identity()..setColumn(2, Vector3(cx, cy, 1.0));
176 : // rotate
177 1 : m.multiply(
178 1 : Matrix3.identity()
179 3 : ..setRow(0, Vector3(cosAngle, -sinAngle, 0.0))
180 2 : ..setRow(1, Vector3(sinAngle, cosAngle, 0.0)),
181 : );
182 : // scale
183 1 : m.multiply(
184 1 : Matrix3.identity()
185 2 : ..setRow(0, Vector3(rx, 0.0, 0.0))
186 2 : ..setRow(1, Vector3(0.0, ry, 0.0)),
187 : );
188 :
189 3 : for (int i = 0; i < bezierPoints.length; i++) {
190 : final Vector3 transformedPoint =
191 6 : m.transform(Vector3(bezierPoints[i].x, bezierPoints[i].y, 1.0));
192 2 : bezierPoints[i] = CoordinatePair(
193 1 : x: transformedPoint.x,
194 1 : y: transformedPoint.y,
195 : );
196 : }
197 :
198 : // The last point in the bezier set should match exactly the last coordinates pair in the arc (ie: x,y). But
199 : // considering all the mathematical manipulation we have been doing, it is bound to be off by a tiny
200 : // fraction. Experiments show that it can be up to around 0.00002. So why don't we just set it to
201 : // exactly what it ought to be.
202 4 : bezierPoints[bezierPoints.length - 1] = CoordinatePair(
203 : x: x,
204 : y: y,
205 : );
206 :
207 1 : List<Command> bezierCurves = [];
208 : // Final step is to add the bezier curves to the path
209 3 : for (int i = 0; i < bezierPoints.length; i += 3) {
210 1 : bezierCurves.add(
211 1 : CurveToCommand(
212 : isRelative: false,
213 1 : commandArguments: CurveToCoordinateSequence(
214 1 : coordinatePairTriplets: [
215 1 : CoordinatePairTriplet(
216 1 : coordinatePairs: [
217 1 : bezierPoints[i],
218 2 : bezierPoints[i + 1],
219 2 : bezierPoints[i + 2],
220 : ],
221 : )
222 : ],
223 : ),
224 : ),
225 : );
226 : }
227 : return bezierCurves;
228 : }
229 :
230 1 : double checkedArcCos(double val) {
231 2 : return (val < -1.0)
232 : ? pi
233 1 : : (val > 1.0)
234 : ? 0
235 1 : : acos(val);
236 : }
237 :
238 1 : List<CoordinatePair> arcToBeziers(double angleStart, double angleExtent) {
239 : int numSegments =
240 4 : (angleExtent.abs() * 2.0 / pi).ceil(); // (angleExtent / 90deg)
241 :
242 1 : double angleIncrement = angleExtent / numSegments;
243 :
244 : // The length of each control point vector is given by the following formula.
245 1 : double controlLength = 4.0 /
246 1 : 3.0 *
247 3 : sin(angleIncrement / 2.0) /
248 3 : (1.0 + cos(angleIncrement / 2.0));
249 :
250 1 : List<CoordinatePair> coordinatePairs = [];
251 :
252 2 : for (int i = 0; i < numSegments; i++) {
253 2 : double angle = angleStart + i * angleIncrement;
254 : // Calculate the control vector at this angle
255 1 : double dx = cos(angle);
256 1 : double dy = sin(angle);
257 : // First control point
258 1 : coordinatePairs.add(
259 1 : CoordinatePair(
260 2 : x: dx - controlLength * dy,
261 2 : y: dy + controlLength * dx,
262 : ),
263 : );
264 : // Second control point
265 1 : angle += angleIncrement;
266 1 : dx = cos(angle);
267 1 : dy = sin(angle);
268 1 : coordinatePairs.add(
269 1 : CoordinatePair(
270 2 : x: dx + controlLength * dy,
271 2 : y: dy - controlLength * dx,
272 : ),
273 : );
274 : // Endpoint of bezier
275 1 : coordinatePairs.add(
276 1 : CoordinatePair(
277 : x: dx,
278 : y: dy,
279 : ),
280 : );
281 : }
282 : return coordinatePairs;
283 : }
284 :
285 1 : @override
286 : CoordinatePair getLastPoint(CoordinatePair startPoint) {
287 1 : if (isAbsolute) {
288 : final EllipticalArcArgument lastPoint =
289 1 : (commandArguments as EllipticalArcArgumentSequence)
290 1 : .ellipticalArcArguments
291 1 : .last;
292 1 : return CoordinatePair(
293 1 : x: lastPoint.x,
294 1 : y: lastPoint.y,
295 : );
296 : }
297 1 : double x = startPoint.x;
298 1 : double y = startPoint.y;
299 : for (final EllipticalArcArgument eaa
300 1 : in (commandArguments as EllipticalArcArgumentSequence)
301 2 : .ellipticalArcArguments) {
302 2 : x += eaa.x;
303 2 : y += eaa.y;
304 : }
305 1 : return CoordinatePair(x: x, y: y);
306 : }
307 :
308 2 : @override
309 : bool operator ==(Object other) {
310 2 : if (other is! EllipticalArcCommand) {
311 : return false;
312 : }
313 6 : return command == other.command &&
314 4 : commandArguments as EllipticalArcArgumentSequence ==
315 2 : other.commandArguments as EllipticalArcArgumentSequence;
316 : }
317 :
318 1 : @override
319 3 : int get hashCode => Object.hash(command, commandArguments);
320 : }
|