Skip to content

packages/core/src/lib/geometry/path-geometry.ts

Read as Markdown

This is the source snapshot used to build these API details. View this revision on GitHub.

Back to reference

1 import { arcPoint, arcSweep, TAU, type ArcSegment } from './path-arc.js';
2 /**
3  * A canonical, independently editable path segment.
4  *
5  * @see {@link PathGeometry}
6  */
7 export type PathSegment =
8   | {
9       /** Selects `"move"`, `"line"` for type. See {@link PathSegment}. */
10       type: 'move' | 'line';
11       /**
12        * Horizontal coordinate or displacement in the containing coordinate system. See
13        * {@link PathSegment}.
14        */
15       x: number;
16       /**
17        * Vertical coordinate or displacement in the containing coordinate system. See
18        * {@link PathSegment}.
19        */
20       y: number;
21     }
22   | {
23       /** The literal "quadratic" identifying this variant. See {@link PathSegment}. */
24       type: 'quadratic';
25       /** Horizontal coordinate of the quadratic control point. See {@link PathSegment}. */
26       cpx: number;
27       /** Vertical coordinate of the quadratic control point. See {@link PathSegment}. */
28       cpy: number;
29       /**
30        * Horizontal coordinate or displacement in the containing coordinate system. See
31        * {@link PathSegment}.
32        */
33       x: number;
34       /**
35        * Vertical coordinate or displacement in the containing coordinate system. See
36        * {@link PathSegment}.
37        */
38       y: number;
39     }
40   | {
41       /** The literal "cubic" identifying this variant. See {@link PathSegment}. */
42       type: 'cubic';
43       /** Horizontal coordinate of the first cubic control point. See {@link PathSegment}. */
44       cp1x: number;
45       /** Vertical coordinate of the first cubic control point. See {@link PathSegment}. */
46       cp1y: number;
47       /** Horizontal coordinate of the second cubic control point. See {@link PathSegment}. */
48       cp2x: number;
49       /** Vertical coordinate of the second cubic control point. See {@link PathSegment}. */
50       cp2y: number;
51       /**
52        * Horizontal coordinate or displacement in the containing coordinate system. See
53        * {@link PathSegment}.
54        */
55       x: number;
56       /**
57        * Vertical coordinate or displacement in the containing coordinate system. See
58        * {@link PathSegment}.
59        */
60       y: number;
61     }
62   | ArcSegment
63   | {
64       /** The literal "close" identifying this variant. See {@link PathSegment}. */
65       type: 'close';
66     };
67 
68 const names = ['move', 'line', 'quadratic', 'cubic', 'close', 'arc'] as const;
69 const widths = [2, 2, 4, 6, 0, 8] as const;
70 interface Storage {
71   types: Uint8Array;
72   data: Float64Array;
73   count: number;
74   length: number;
75   revision: number;
76   x: number; y: number; startX: number; startY: number;
77 }
78 const storage = new WeakMap<PathGeometry, Storage>();
79 
80 function state(path: PathGeometry): Storage {
81   const value = storage.get(path);
82   if (!value) throw new TypeError('Expected a PathGeometry.');
83   return value;
84 }
85 
86 /** @internal Borrowed buffers must never escape this synchronous traversal. */
87 export function visitPathGeometry(
88   path: PathGeometry,
89   visit: (type: number, data: Float64Array, offset: number) => void,
90 ): void {
91   const value = state(path);
92   let offset = 0;
93   for (let i = 0; i < value.count; i++) {
94     const type = value.types[i];
95     visit(type, value.data, offset);
96     offset += widths[type];
97   }
98 }
99 
100 function ensureRevision(value: Storage): void {
101   if (value.revision >= Number.MAX_SAFE_INTEGER) {
102     throw new RangeError('PathGeometry revision exhausted.');
103   }
104 }
105 
106 function reserve(value: Storage, count: number, length: number): void {
107   if (count > value.types.length) {
108     const next = new Uint8Array(Math.max(count, value.types.length * 2, 8));
109     next.set(value.types.subarray(0, value.count));
110     value.types = next;
111   }
112   if (length > value.data.length) {
113     const next = new Float64Array(Math.max(length, value.data.length * 2, 32));
114     next.set(value.data.subarray(0, value.length));
115     value.data = next;
116   }
117 }
118 
119 function append(value: Storage, type: number, data: readonly number[]): void {
120   reserve(value, value.count + 1, value.length + data.length);
121   value.types[value.count++] = type;
122   value.data.set(data, value.length);
123   value.length += data.length;
124   updatePoint(value, type, data, 0);
125 }
126 
127 function operands(segment: PathSegment): number[] {
128   switch (segment.type) {
129     case 'move': case 'line': return [segment.x, segment.y];
130     case 'quadratic': return [segment.cpx, segment.cpy, segment.x, segment.y];
131     case 'cubic': return [segment.cp1x, segment.cp1y, segment.cp2x, segment.cp2y, segment.x, segment.y];
132     case 'close': return [];
133     case 'arc': return [segment.cx, segment.cy, segment.ux, segment.uy, segment.vx, segment.vy, segment.startAngle, segment.sweep];
134     default: throw new TypeError('Unknown path segment type.');
135   }
136 }
137 
138 /**
139  * Mutable packed geometry. Mutations do not schedule Pibbl rendering.
140  *
141  * @see {@link PathSegment}
142  */
143 export class PathGeometry implements Iterable<PathSegment> {
144   /**
145    * Creates empty geometry or an independent copy of the supplied path. See {@link PathGeometry}.
146    * @param source - Optional geometry whose segments are copied; omission creates an empty path.
147    * See {@link PathGeometry} .
148    */
149   constructor(source?: PathGeometry) {
150     const original = source === undefined ? undefined : state(source);
151     storage.set(this, {
152       types: original ? original.types.slice(0, original.count) : new Uint8Array(0),
153       data: original ? original.data.slice(0, original.length) : new Float64Array(0),
154       count: original?.count ?? 0,
155       length: original?.length ?? 0,
156       revision: 0,
157       x: original?.x ?? 0, y: original?.y ?? 0,
158       startX: original?.startX ?? 0, startY: original?.startY ?? 0,
159     });
160   }
161 
162   /**
163    * Monotonic mutation revision; changing geometry does not itself request a Pibbl frame. See
164    * {@link PathGeometry}.
165    * @returns The mutation revision used to invalidate cached representations.
166    */
167   get revision(): number { return state(this).revision; }
168   /**
169    * Number of stored path segments, including move and close commands. See {@link PathGeometry}.
170    * @returns The number of stored path segments.
171    */
172   get segmentCount(): number { return state(this).count; }
173 
174   /**
175    * Returns an independent copy with its own mutation revision. See {@link PathGeometry}.
176    * @returns An independent copy of this path's segments. See {@link PathGeometry}.
177    */
178   clone(): PathGeometry { return new PathGeometry(this); }
179 
180   /** Removes all segments and resets the current point. See {@link PathGeometry}. */
181   clear(): void {
182     const value = state(this);
183     if (!value.count) return;
184     ensureRevision(value);
185     value.count = value.length = 0;
186     value.x = value.y = value.startX = value.startY = 0;
187     value.revision++;
188   }
189 
190   /**
191    * Begins a subpath at the supplied coordinates. See {@link PathGeometry}.
192    * @param x - Horizontal coordinate of the new subpath's starting point.
193    * @param y - Vertical coordinate of the new subpath's starting point.
194    */
195   moveTo(x: number, y: number): void { this.#build(0, [x, y]); }
196   /**
197    * Appends a straight segment to the supplied coordinates. See {@link PathGeometry}.
198    * @param x - Horizontal coordinate of the line endpoint.
199    * @param y - Vertical coordinate of the line endpoint.
200    */
201   lineTo(x: number, y: number): void { this.#build(1, [x, y]); }
202   /**
203    * Appends a quadratic curve using one control point and an endpoint. See {@link PathGeometry}.
204    * @param cpx - Horizontal coordinate of the quadratic control point.
205    * @param cpy - Vertical coordinate of the quadratic control point.
206    * @param x - Horizontal coordinate of the curve endpoint.
207    * @param y - Vertical coordinate of the curve endpoint.
208    */
209   quadraticCurveTo(cpx: number, cpy: number, x: number, y: number): void {
210     this.#build(2, [cpx, cpy, x, y]);
211   }
212   /**
213    * Appends a cubic curve using two control points and an endpoint. See {@link PathGeometry}.
214    * @param cp1x - Horizontal coordinate of the first control point.
215    * @param cp1y - Vertical coordinate of the first control point.
216    * @param cp2x - Horizontal coordinate of the second control point.
217    * @param cp2y - Vertical coordinate of the second control point.
218    * @param x - Horizontal coordinate of the curve endpoint.
219    * @param y - Vertical coordinate of the curve endpoint.
220    */
221   bezierCurveTo(cp1x: number, cp1y: number, cp2x: number, cp2y: number, x: number, y: number): void {
222     this.#build(3, [cp1x, cp1y, cp2x, cp2y, x, y]);
223   }
224   /**
225    * Closes the current subpath; an already closed or empty path is unchanged. See
226    * {@link PathGeometry}.
227    */
228   closePath(): void {
229     const value = state(this);
230     if (!value.count || value.types[value.count - 1] === 4) return;
231     this.#build(4, []);
232   }
233 
234   /**
235    * Appends a circular arc using Canvas-compatible angles in radians. See {@link PathGeometry}.
236    * @param x - Horizontal coordinate of the circle center.
237    * @param y - Vertical coordinate of the circle center.
238    * @param radius - Circle radius in path units.
239    * @param startAngle - Starting angle in radians.
240    * @param endAngle - Ending angle in radians.
241    * @param counterclockwise - Whether the arc runs counterclockwise; defaults to false.
242    */
243   arc(x: number, y: number, radius: number, startAngle: number, endAngle: number, counterclockwise = false): void {
244     this.ellipse(x, y, radius, radius, 0, startAngle, endAngle, counterclockwise);
245   }
246 
247   /**
248    * Appends an elliptical arc; rotation and arc angles are in radians. See {@link PathGeometry}.
249    * @param x - Horizontal coordinate of the ellipse center.
250    * @param y - Vertical coordinate of the ellipse center.
251    * @param radiusX - Horizontal radius in path units.
252    * @param radiusY - Vertical radius in path units.
253    * @param rotation - Ellipse rotation in radians.
254    * @param startAngle - Starting angle in radians.
255    * @param endAngle - Ending angle in radians.
256    * @param counterclockwise - Whether the arc runs counterclockwise; defaults to false.
257    */
258   ellipse(x: number, y: number, radiusX: number, radiusY: number, rotation: number, startAngle: number, endAngle: number, counterclockwise = false): void {
259     if (![x, y, radiusX, radiusY, rotation, startAngle, endAngle].every(Number.isFinite)) return;
260     if (radiusX < 0 || radiusY < 0) throw new DOMException('Arc radii must not be negative.', 'IndexSizeError');
261     const cos = Math.cos(rotation), sin = Math.sin(rotation);
262     const arc: ArcSegment = {
263       type: 'arc', cx: x, cy: y, ux: radiusX * cos, uy: radiusX * sin,
264       vx: -radiusY * sin, vy: radiusY * cos, startAngle: startAngle % TAU,
265       sweep: arcSweep(startAngle, endAngle, counterclockwise),
266     };
267     this.#appendArc(arc);
268   }
269 
270   #appendArc(arc: ArcSegment): void {
271     const [x, y] = arcPoint(arc, arc.startAngle);
272     const end = arcPoint(arc, arc.startAngle + arc.sweep);
273     if (![x, y, ...end].every(Number.isFinite)) throw new RangeError('Arc coordinates overflowed.');
274     const value = state(this);
275     ensureRevision(value);
276     if (!value.count) append(value, 0, [x, y]);
277     else if (value.x !== x || value.y !== y) append(value, 1, [x, y]);
278     append(value, 5, operands(arc));
279     value.revision++;
280   }
281 
282   /**
283    * Appends a circular arc tangent to two lines using Canvas-compatible geometry. See
284    * {@link PathGeometry}.
285    * @param x1 - Horizontal coordinate of the first tangent intersection.
286    * @param y1 - Vertical coordinate of the first tangent intersection.
287    * @param x2 - Horizontal coordinate of the second tangent endpoint.
288    * @param y2 - Vertical coordinate of the second tangent endpoint.
289    * @param radius - Arc radius in path units.
290    */
291   arcTo(x1: number, y1: number, x2: number, y2: number, radius: number): void {
292     if (![x1, y1, x2, y2, radius].every(Number.isFinite)) return;
293     if (radius < 0) throw new DOMException('Arc radius must not be negative.', 'IndexSizeError');
294     const value = state(this);
295     if (!value.count) { this.moveTo(x1, y1); return; }
296     const ax = value.x - x1, ay = value.y - y1, bx = x2 - x1, by = y2 - y1;
297     const al = Math.hypot(ax, ay), bl = Math.hypot(bx, by);
298     if (al === 0 || bl === 0 || radius === 0) { this.lineTo(x1, y1); return; }
299     const ux = ax / al, uy = ay / al, vx = bx / bl, vy = by / bl;
300     const cross = ux * vy - uy * vx;
301     if (cross === 0) { this.lineTo(x1, y1); return; }
302     const dot = Math.max(-1, Math.min(1, ux * vx + uy * vy));
303     const distance = radius * (1 + dot) / Math.abs(cross);
304     const startX = x1 + ux * distance, startY = y1 + uy * distance;
305     const sign = cross < 0 ? -1 : 1;
306     const cx = startX - uy * radius * sign, cy = startY + ux * radius * sign;
307     const endX = x1 + vx * distance, endY = y1 + vy * distance;
308     const start = Math.atan2(startY - cy, startX - cx);
309     const end = Math.atan2(endY - cy, endX - cx);
310     if (![cx, cy, endX, endY].every(Number.isFinite)) throw new RangeError('Arc coordinates overflowed.');
311     this.#appendArc({ type: 'arc', cx, cy, ux: radius, uy: 0, vx: 0, vy: radius,
312       startAngle: start, sweep: arcSweep(start, end, cross > 0) });
313   }
314 
315   /**
316    * Appends a closed rectangle subpath. See {@link PathGeometry}.
317    * @param x - Horizontal coordinate of the rectangle origin.
318    * @param y - Vertical coordinate of the rectangle origin.
319    * @param width - Signed horizontal extent in path units.
320    * @param height - Signed vertical extent in path units.
321    */
322   rect(x: number, y: number, width: number, height: number): void {
323     if (![x, y, width, height].every(Number.isFinite)) return;
324     if (![x + width, y + height].every(Number.isFinite)) throw new RangeError('Rectangle coordinates overflowed.');
325     const value = state(this);
326     ensureRevision(value);
327     append(value, 0, [x, y]);
328     append(value, 1, [x + width, y]);
329     append(value, 1, [x + width, y + height]);
330     append(value, 1, [x, y + height]);
331     append(value, 4, []);
332     append(value, 0, [x, y]);
333     value.revision++;
334   }
335 
336   /**
337    * Appends a closed rectangle with Canvas-compatible corner radii. See {@link PathGeometry}.
338    * @param x - Horizontal coordinate of the rectangle origin.
339    * @param y - Vertical coordinate of the rectangle origin.
340    * @param width - Signed horizontal extent in path units.
341    * @param height - Signed vertical extent in path units.
342    * @param radii - Circular or elliptical corner radii, using Canvas roundRect ordering; defaults
343    * to zero.
344    */
345   roundRect(x: number, y: number, width: number, height: number,
346     radii: number | DOMPointInit | (number | DOMPointInit)[] = 0): void {
347     if (![x, y, width, height].every(Number.isFinite)) return;
348     const list = Array.isArray(radii) ? radii : [radii];
349     if (!list.length || list.length > 4) throw new RangeError('Expected one to four corner radii.');
350     const values: [number, number][] = [];
351     for (const radius of list) {
352       const rx = typeof radius === 'number' ? radius : radius.x ?? 0;
353       const ry = typeof radius === 'number' ? radius : radius.y ?? 0;
354       if (![rx, ry].every(Number.isFinite)) return;
355       if (rx < 0 || ry < 0) throw new RangeError('Corner radii must not be negative.');
356       values.push([rx, ry]);
357     }
358     const corners = [values[0], values[1] ?? values[0], values[2] ?? values[0], values[3] ?? values[1] ?? values[0]];
359     const w = Math.abs(width), h = Math.abs(height);
360     const ratio = (length: number, sum: number) => sum === 0 ? 1 : length / sum;
361     const scale = Math.min(1, ratio(w, corners[0][0] + corners[1][0]), ratio(w, corners[2][0] + corners[3][0]),
362       ratio(h, corners[0][1] + corners[3][1]), ratio(h, corners[1][1] + corners[2][1]));
363     const [tl, tr, br, bl] = corners.map(([rx, ry]) => [rx * scale, ry * scale]);
364     const path = new PathGeometry();
365     path.moveTo(tl[0], 0);
366     path.lineTo(w - tr[0], 0);
367     path.ellipse(w - tr[0], tr[1], tr[0], tr[1], 0, -Math.PI / 2, 0);
368     path.lineTo(w, h - br[1]);
369     path.ellipse(w - br[0], h - br[1], br[0], br[1], 0, 0, Math.PI / 2);
370     path.lineTo(bl[0], h);
371     path.ellipse(bl[0], h - bl[1], bl[0], bl[1], 0, Math.PI / 2, Math.PI);
372     path.lineTo(0, tl[1]);
373     path.ellipse(tl[0], tl[1], tl[0], tl[1], 0, Math.PI, Math.PI * 1.5);
374     path.closePath();
375     // Chromium resumes at the normalized rectangle origin after negative dimensions.
376     path.moveTo(width < 0 ? w : 0, height < 0 ? h : 0);
377     path.transform({ a: width < 0 ? -1 : 1, d: height < 0 ? -1 : 1, e: x, f: y });
378     this.#copyPath(path);
379   }
380 
381   /**
382    * Appends a copy of another path, optionally transformed by a 2D matrix. See
383    * {@link PathGeometry}.
384    * @param other - Geometry whose segments are appended. See {@link PathGeometry}.
385    * @param matrix - Optional affine transform applied to the appended segments; defaults to
386    * identity.
387    */
388   addPath(other: PathGeometry, matrix: DOMMatrix2DInit = {}): void {
389     if (!state(other).count) return;
390     const normalized = DOMMatrix.fromMatrix(matrix);
391     if (!normalized.is2D) throw new TypeError('PathGeometry requires a 2D affine matrix.');
392     if (![normalized.a, normalized.b, normalized.c, normalized.d, normalized.e, normalized.f].every(Number.isFinite)) return;
393     const copy = other.clone();
394     copy.transform(normalized);
395     const value = state(copy);
396     append(value, 0, [value.x, value.y]);
397     this.#copyPath(copy);
398   }
399 
400   #copyPath(other: PathGeometry): void {
401     const source = state(other), value = state(this);
402     ensureRevision(value);
403     reserve(value, value.count + source.count, value.length + source.length);
404     value.types.set(source.types.subarray(0, source.count), value.count);
405     value.data.set(source.data.subarray(0, source.length), value.length);
406     value.count += source.count; value.length += source.length;
407     value.x = source.x; value.y = source.y;
408     value.startX = source.startX; value.startY = source.startY;
409     value.revision++;
410   }
411 
412   /**
413    * Replaces a segment range with validated independent segment values. See {@link PathGeometry}.
414    * @param start - Index at which to begin replacing segments.
415    * @param deleteCount - Number of segments to remove.
416    * @param segments - Replacement segments; defaults to no inserted segments. See
417    * {@link PathSegment} .
418    */
419   spliceSegments(start: number, deleteCount: number, segments: readonly PathSegment[] = []): void {
420     const value = state(this);
421     if (!Number.isInteger(start) || start < 0 || start > value.count ||
422       !Number.isInteger(deleteCount) || deleteCount < 0 || deleteCount > value.count - start) {
423       throw new RangeError('Invalid path splice range.');
424     }
425     const additions = segments.map(segment => ({ type: names.indexOf(segment.type), data: validatedOperands(segment) }));
426     const count = value.count - deleteCount + additions.length;
427     const firstType = start === 0 ? (additions[0]?.type ?? value.types[deleteCount]) : value.types[0];
428     if (count > 0 && firstType !== 0) throw new TypeError('A path must start with a move segment.');
429     if (!deleteCount && !additions.length) return;
430     ensureRevision(value);
431     let offset = 0, removed = 0;
432     for (let i = 0; i < start; i++) offset += widths[value.types[i]];
433     for (let i = start; i < start + deleteCount; i++) removed += widths[value.types[i]];
434     const added = additions.reduce((sum, segment) => sum + segment.data.length, 0);
435     const length = value.length - removed + added;
436     reserve(value, count, length);
437     value.types.copyWithin(start + additions.length, start + deleteCount, value.count);
438     value.data.copyWithin(offset + added, offset + removed, value.length);
439     for (let i = 0; i < additions.length; i++) {
440       value.types[start + i] = additions[i].type;
441       value.data.set(additions[i].data, offset);
442       offset += additions[i].data.length;
443     }
444     value.count = count; value.length = length;
445     rebuildPoint(value);
446     value.revision++;
447   }
448 
449   #build(type: number, data: number[]): void {
450     if (!data.every(Number.isFinite)) return;
451     const value = state(this);
452     ensureRevision(value);
453     if (!value.count && type !== 0) {
454       // Canvas starts a missing subpath at the first control point (or endpoint).
455       append(value, 0, data.slice(0, 2));
456       if (type === 1) {
457         value.revision++;
458         return;
459       }
460     }
461     append(value, type, data);
462     value.revision++;
463   }
464 
465   #offset(index: number): number {
466     const value = state(this);
467     if (!Number.isInteger(index) || index < 0 || index >= value.count) {
468       throw new RangeError('Path segment index is out of range.');
469     }
470     let offset = 0;
471     for (let i = 0; i < index; i++) offset += widths[value.types[i]];
472     return offset;
473   }
474 
475   /**
476    * Returns an independent segment value at the specified index. See {@link PathSegment}.
477    * @param index - Zero-based segment index.
478    * @returns The segment at the requested index. See {@link PathSegment}.
479    */
480   getSegment(index: number): PathSegment {
481     return readSegment(state(this), index, this.#offset(index));
482   }
483 
484   /**
485    * Replaces one stored segment with a validated independent value. See {@link PathGeometry}.
486    * @param index - Zero-based index of the segment to replace.
487    * @param segment - Replacement segment data. See {@link PathSegment}.
488    */
489   setSegment(index: number, segment: PathSegment): void {
490     const offset = this.#offset(index);
491     const type = names.indexOf(segment.type);
492     const data = validatedOperands(segment);
493     if (!data.every(Number.isFinite)) throw new RangeError('Path coordinates must be finite.');
494     if (index === 0 && type !== 0) throw new TypeError('A path must start with a move segment.');
495     const value = state(this);
496     ensureRevision(value);
497     const oldWidth = widths[value.types[index]];
498     const length = value.length + data.length - oldWidth;
499     reserve(value, value.count, length);
500     value.data.copyWithin(offset + data.length, offset + oldWidth, value.length);
501     value.data.set(data, offset);
502     value.types[index] = type;
503     value.length = length;
504     rebuildPoint(value);
505     value.revision++;
506   }
507 
508   /**
509    * Applies a finite 2D affine matrix. Old native conversions remain unchanged.
510    * @param matrix - Affine matrix applied to all segments in this path.
511    */
512   transform(matrix: DOMMatrix2DInit): void {
513     const input = matrix as DOMMatrixInit;
514     const normalized = DOMMatrix.fromMatrix(input);
515     if (!normalized.is2D) throw new TypeError('PathGeometry requires a 2D affine matrix.');
516     const { a, b, c, d, e, f } = normalized;
517     if (![a, b, c, d, e, f].every(Number.isFinite)) {
518       throw new RangeError('Path transform must be finite.');
519     }
520     const value = state(this);
521     if (!value.count) return;
522     ensureRevision(value);
523     // Two passes keep overflow errors atomic without allocating another buffer.
524     for (const write of [false, true]) {
525       visitPathGeometry(this, (type, data, offset) => {
526         const length = type === 5 ? 6 : widths[type];
527         for (let i = 0; i < length; i += 2) {
528           const x = data[offset + i], y = data[offset + i + 1];
529           const vector = type === 5 && i > 0;
530           const nextX = a * x + c * y + (vector ? 0 : e);
531           const nextY = b * x + d * y + (vector ? 0 : f);
532           if (!Number.isFinite(nextX) || !Number.isFinite(nextY)) {
533             throw new RangeError('Path transform coordinates overflowed.');
534           }
535           if (write) { data[offset + i] = nextX; data[offset + i + 1] = nextY; }
536         }
537       });
538     }
539     rebuildPoint(value);
540     value.revision++;
541   }
542 
543   /**
544    * Iterates over independent segment values in source order. See {@link PathSegment}.
545    * @returns An iterator over the path's segments in drawing order. See {@link PathSegment}.
546    */
547   *[Symbol.iterator](): IterableIterator<PathSegment> {
548     const value = state(this);
549     const revision = value.revision;
550     let offset = 0;
551     for (let index = 0; index < value.count; index++) {
552       if (value.revision !== revision) throw new Error('PathGeometry changed during iteration.');
553       const width = widths[value.types[index]];
554       yield readSegment(value, index, offset);
555       if (value.revision !== revision) throw new Error('PathGeometry changed during iteration.');
556       offset += width;
557     }
558   }
559 }
560 
561 function readSegment(value: Storage, index: number, offset: number): PathSegment {
562   const d = value.data;
563   switch (value.types[index]) {
564     case 0: return { type: 'move', x: d[offset], y: d[offset + 1] };
565     case 1: return { type: 'line', x: d[offset], y: d[offset + 1] };
566     case 2: return { type: 'quadratic', cpx: d[offset], cpy: d[offset + 1], x: d[offset + 2], y: d[offset + 3] };
567     case 3: return { type: 'cubic', cp1x: d[offset], cp1y: d[offset + 1], cp2x: d[offset + 2], cp2y: d[offset + 3], x: d[offset + 4], y: d[offset + 5] };
568     case 5: return { type: 'arc', cx: d[offset], cy: d[offset + 1], ux: d[offset + 2], uy: d[offset + 3], vx: d[offset + 4], vy: d[offset + 5], startAngle: d[offset + 6], sweep: d[offset + 7] };
569     default: return { type: 'close' };
570   }
571 }
572 
573 function validatedOperands(segment: PathSegment): number[] {
574   const data = operands(segment);
575   if (!data.every(Number.isFinite)) throw new RangeError('Path coordinates must be finite.');
576   if (segment.type === 'arc' && (Math.abs(segment.sweep) > TAU || Math.abs(segment.startAngle) > TAU)) {
577     throw new RangeError('Arc angles must be normalized to one revolution.');
578   }
579   return data;
580 }
581 
582 function updatePoint(value: Storage, type: number, data: ArrayLike<number>, offset: number): void {
583   if (type === 4) { value.x = value.startX; value.y = value.startY; return; }
584   if (type === 5) {
585     const angle = data[offset + 6] + data[offset + 7];
586     value.x = data[offset] + data[offset + 2] * Math.cos(angle) + data[offset + 4] * Math.sin(angle);
587     value.y = data[offset + 1] + data[offset + 3] * Math.cos(angle) + data[offset + 5] * Math.sin(angle);
588   } else {
589     value.x = data[offset + widths[type] - 2]; value.y = data[offset + widths[type] - 1];
590   }
591   if (type === 0) { value.startX = value.x; value.startY = value.y; }
592 }
593 function rebuildPoint(value: Storage): void {
594   value.x = value.y = value.startX = value.startY = 0;
595   let offset = 0;
596   for (let i = 0; i < value.count; i++) {
597     updatePoint(value, value.types[i], value.data, offset);
598     offset += widths[value.types[i]];
599   }
600 }
601 

Documentation built with @pibbl/core 0.0.2, revision 272a94a. ALPHA — NOT FOR PRODUCTION USE.