packages/core/src/lib/geometry/path-conversion.ts
This is the source snapshot used to build these API details. View this revision on GitHub.
1 import { arcPoint, ellipseParameters, singularArcPoints, type ArcSegment } from './path-arc.js';
2 import { PathGeometry, visitPathGeometry } from './path-geometry.js';
3
4 /**
5 * Creates a fresh caller-owned native snapshot; never returns a shared cache.
6 *
7 * @param geometry - Pibbl geometry to convert. See {@link PathGeometry}.
8 * @returns A native Canvas path representing the geometry.
9 *
10 * @see {@link PathGeometry}
11 */
12 export function toPath2D(geometry: PathGeometry): Path2D {
13 const path = new Path2D();
14 visitPathGeometry(geometry, (type, data, i) => {
15 switch (type) {
16 case 0: path.moveTo(data[i], data[i + 1]); break;
17 case 1: path.lineTo(data[i], data[i + 1]); break;
18 case 2: path.quadraticCurveTo(data[i], data[i + 1], data[i + 2], data[i + 3]); break;
19 case 3: path.bezierCurveTo(data[i], data[i + 1], data[i + 2], data[i + 3], data[i + 4], data[i + 5]); break;
20 case 4: path.closePath(); break;
21 case 5: {
22 const arc = readArc(data, i);
23 const ellipse = ellipseParameters(arc);
24 if (ellipse) {
25 path.ellipse(arc.cx, arc.cy, ellipse.radiusX, ellipse.radiusY, ellipse.rotation,
26 ellipse.start, ellipse.start + ellipse.sweep, ellipse.sweep < 0);
27 } else {
28 const start = arcPoint(arc, arc.startAngle);
29 path.lineTo(start[0], start[1]);
30 for (const [x, y] of singularArcPoints(arc)) path.lineTo(x, y);
31 }
32 break;
33 }
34 }
35 });
36 return path;
37 }
38
39 /**
40 * Serializes independent absolute SVG path data without implicit rounding.
41 *
42 * @param geometry - Pibbl geometry to serialize. See {@link PathGeometry}.
43 * @returns SVG path data representing the geometry.
44 *
45 * @see {@link PathGeometry}
46 */
47 export function toSvgPathData(geometry: PathGeometry): string {
48 const parts: string[] = [];
49 const commands = ['M', 'L', 'Q', 'C', 'Z'];
50 const widths = [2, 2, 4, 6, 0];
51 let currentX = 0, currentY = 0, startX = 0, startY = 0;
52 visitPathGeometry(geometry, (type, data, i) => {
53 if (type === 5) {
54 const arc = readArc(data, i);
55 const arcStart = arcPoint(arc, arc.startAngle);
56 if (currentX !== arcStart[0] || currentY !== arcStart[1]) {
57 parts.push('L', String(arcStart[0]), String(arcStart[1]));
58 }
59 [currentX, currentY] = arcPoint(arc, arc.startAngle + arc.sweep);
60 const ellipse = ellipseParameters(arc);
61 if (!ellipse) {
62 for (const [x, y] of singularArcPoints(arc)) parts.push('L', String(x), String(y));
63 } else if (arc.sweep !== 0) {
64 // Splitting into at most half-turns handles full circles and avoids SVG's
65 // ambiguous coincident endpoints. No flattening is involved.
66 const count = Math.ceil(Math.abs(arc.sweep) / Math.PI);
67 for (let step = 1; step <= count; step++) {
68 const [x, y] = arcPoint(arc, arc.startAngle + arc.sweep * step / count);
69 parts.push('A', String(ellipse.radiusX), String(ellipse.radiusY),
70 String(ellipse.rotation * 180 / Math.PI), '0', ellipse.sweep < 0 ? '0' : '1', String(x), String(y));
71 }
72 }
73 return;
74 }
75 parts.push(commands[type]);
76 for (let j = 0; j < widths[type]; j++) parts.push(String(data[i + j]));
77 if (type === 4) { currentX = startX; currentY = startY; }
78 else {
79 currentX = data[i + widths[type] - 2]; currentY = data[i + widths[type] - 1];
80 if (type === 0) { startX = currentX; startY = currentY; }
81 }
82 });
83 return parts.join(' ');
84 }
85
86 function readArc(data: Float64Array, i: number): ArcSegment {
87 return { type: 'arc', cx: data[i], cy: data[i + 1], ux: data[i + 2], uy: data[i + 3],
88 vx: data[i + 4], vy: data[i + 5], startAngle: data[i + 6], sweep: data[i + 7] };
89 }
90
Documentation version
Section titled “Documentation version”Documentation built with @pibbl/core 0.0.2, revision 2dccb19. ALPHA — NOT FOR PRODUCTION USE.