packages/core/src/lib/geometry/path-geometry.ts
This is the source snapshot used to build these API details. View this revision on GitHub.
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 version
Section titled “Documentation version”Documentation built with @pibbl/core 0.0.2, revision 2dccb19. ALPHA — NOT FOR PRODUCTION USE.