@metamesh/modeling 0.0.0-bootstrap.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/README.md +313 -6
  3. package/THIRD_PARTY_NOTICES.md +153 -0
  4. package/dist/api/engine.d.ts +54 -0
  5. package/dist/api/geometry.d.ts +2912 -0
  6. package/dist/api/index.d.ts +1648 -0
  7. package/dist/api/text.d.ts +77 -0
  8. package/dist/engine/construction-operations.js +189 -0
  9. package/dist/engine/execution.js +227 -0
  10. package/dist/engine/export-3mf.js +100 -0
  11. package/dist/engine/extrusion-finishing.js +82 -0
  12. package/dist/engine/finalization.js +214 -0
  13. package/dist/engine/index.js +72 -0
  14. package/dist/engine/inspection-operations.js +52 -0
  15. package/dist/engine/kernel/LICENSE +201 -0
  16. package/dist/engine/kernel/manifold.js +2 -0
  17. package/dist/engine/kernel/manifold.wasm +0 -0
  18. package/dist/engine/loft-operations.js +156 -0
  19. package/dist/engine/mechanical-operations.js +51 -0
  20. package/dist/engine/modeling-context.js +38 -0
  21. package/dist/engine/native/adapter.js +20 -0
  22. package/dist/engine/native/context.js +209 -0
  23. package/dist/engine/native/copied-views.js +46 -0
  24. package/dist/engine/native/index.js +13 -0
  25. package/dist/engine/native/operations.js +115 -0
  26. package/dist/engine/native/ownership.js +98 -0
  27. package/dist/engine/native/regions.js +138 -0
  28. package/dist/engine/native/scoped-primitives.js +127 -0
  29. package/dist/engine/native/scoped-threads.js +34 -0
  30. package/dist/engine/native/threads.js +52 -0
  31. package/dist/engine/native/transforms.js +111 -0
  32. package/dist/engine/operation-environment.js +20 -0
  33. package/dist/engine/pattern-operations.js +36 -0
  34. package/dist/engine/pattern-placements.js +176 -0
  35. package/dist/engine/primitive-anchors.js +70 -0
  36. package/dist/engine/primitive-correspondence.js +74 -0
  37. package/dist/engine/primitive-operations.js +151 -0
  38. package/dist/engine/primitive-profile-types.js +1 -0
  39. package/dist/engine/primitive-profiles.js +250 -0
  40. package/dist/engine/profile-mesh.js +279 -0
  41. package/dist/engine/section-operations.js +93 -0
  42. package/dist/engine/shape-anchors.js +88 -0
  43. package/dist/engine/transform-operations.js +225 -0
  44. package/dist/engine/types.js +1 -0
  45. package/dist/geometry/anchors.js +226 -0
  46. package/dist/geometry/curves/evaluation.js +162 -0
  47. package/dist/geometry/curves/sampling.js +265 -0
  48. package/dist/geometry/curves/types.js +1 -0
  49. package/dist/geometry/cylinder.js +117 -0
  50. package/dist/geometry/edge-finishing.js +160 -0
  51. package/dist/geometry/errors.js +76 -0
  52. package/dist/geometry/extrusion.js +150 -0
  53. package/dist/geometry/heightfield.js +107 -0
  54. package/dist/geometry/index.js +42 -0
  55. package/dist/geometry/loft-correspondence.js +204 -0
  56. package/dist/geometry/loft-geometry.js +184 -0
  57. package/dist/geometry/loft-options.js +56 -0
  58. package/dist/geometry/loft-simple-mesh.js +92 -0
  59. package/dist/geometry/loft-triangulation.js +64 -0
  60. package/dist/geometry/loft.js +80 -0
  61. package/dist/geometry/math/vector3.js +41 -0
  62. package/dist/geometry/mechanical/gear-motion.js +94 -0
  63. package/dist/geometry/mechanical/gears.js +484 -0
  64. package/dist/geometry/mechanical/index.js +13 -0
  65. package/dist/geometry/mechanical/standards.js +67 -0
  66. package/dist/geometry/mechanical/thread-helices.js +44 -0
  67. package/dist/geometry/mechanical/thread-mesh-plan.js +358 -0
  68. package/dist/geometry/mechanical/thread-mesh.js +142 -0
  69. package/dist/geometry/mechanical/thread-profiles.js +368 -0
  70. package/dist/geometry/mechanical/thread-solids.js +230 -0
  71. package/dist/geometry/mechanical/thread-surface.js +433 -0
  72. package/dist/geometry/mechanical/threads.js +138 -0
  73. package/dist/geometry/mesh-planar.js +408 -0
  74. package/dist/geometry/mesh-quality.js +396 -0
  75. package/dist/geometry/mesh-signature.js +137 -0
  76. package/dist/geometry/mesh.js +300 -0
  77. package/dist/geometry/model-limits.js +50 -0
  78. package/dist/geometry/numeric.js +11 -0
  79. package/dist/geometry/paths/frames.js +118 -0
  80. package/dist/geometry/paths/helix.js +49 -0
  81. package/dist/geometry/paths/path.js +30 -0
  82. package/dist/geometry/paths/planar.js +135 -0
  83. package/dist/geometry/paths/resampling.js +124 -0
  84. package/dist/geometry/polygon-clipping.js +52 -0
  85. package/dist/geometry/primitive-options.js +1 -0
  86. package/dist/geometry/primitive-resolution.js +17 -0
  87. package/dist/geometry/primitive-resolvers.js +235 -0
  88. package/dist/geometry/profiles/matched.js +184 -0
  89. package/dist/geometry/profiles/open-profile.js +137 -0
  90. package/dist/geometry/profiles/profiles.js +70 -0
  91. package/dist/geometry/profiles/rectangle.js +235 -0
  92. package/dist/geometry/profiles/shapes.js +34 -0
  93. package/dist/geometry/profiles/span-sampling.js +28 -0
  94. package/dist/geometry/profiles/types.js +1 -0
  95. package/dist/geometry/profiles/validation.js +20 -0
  96. package/dist/geometry/regions.js +371 -0
  97. package/dist/geometry/resolution.js +236 -0
  98. package/dist/geometry/revolve/revolve-mesh.js +123 -0
  99. package/dist/geometry/sampling-limits.js +29 -0
  100. package/dist/geometry/stroke-path.js +181 -0
  101. package/dist/geometry/svg/errors.js +23 -0
  102. package/dist/geometry/svg/fill.js +172 -0
  103. package/dist/geometry/svg/index.js +1 -0
  104. package/dist/geometry/svg/matrix.js +130 -0
  105. package/dist/geometry/svg/parser.js +536 -0
  106. package/dist/geometry/svg/path.js +349 -0
  107. package/dist/geometry/svg/types.js +1 -0
  108. package/dist/geometry/svg-import.js +662 -0
  109. package/dist/geometry/sweeps/options.js +28 -0
  110. package/dist/geometry/sweeps/orientation.js +106 -0
  111. package/dist/geometry/sweeps/stations.js +161 -0
  112. package/dist/geometry/sweeps/sweep-closed.js +96 -0
  113. package/dist/geometry/sweeps/sweep-mesh.js +20 -0
  114. package/dist/geometry/sweeps/sweep-open.js +76 -0
  115. package/dist/geometry/sweeps/validation.js +116 -0
  116. package/dist/geometry/text-contours.js +32 -0
  117. package/dist/geometry/text-layout.js +1 -0
  118. package/dist/geometry/tolerance.js +53 -0
  119. package/dist/geometry/transform-geometry.js +41 -0
  120. package/dist/geometry/transform3.js +340 -0
  121. package/dist/geometry/types.js +1 -0
  122. package/dist/index.js +1 -0
  123. package/dist/model/execution-binding.js +30 -0
  124. package/dist/model/operations.js +152 -0
  125. package/dist/model/options.js +1 -0
  126. package/dist/model/types.js +1 -0
  127. package/dist/text/index.js +48 -0
  128. package/dist/text/vector-text-layout.js +279 -0
  129. package/package.json +89 -8
@@ -0,0 +1,123 @@
1
+ import { GeometryError } from "../errors.js";
2
+ import { triangulateLoop2 } from "../loft.js";
3
+ import { validateTriangleMesh } from "../mesh.js";
4
+ import { assertMeshAllocationWithinLimits, resolveModelingLimits, } from "../model-limits.js";
5
+ import { writeMeshVertex } from "../numeric.js";
6
+ import { resolveCircularPrimitiveSegments } from "../primitive-resolution.js";
7
+ import { normalizeLoop2 } from "../regions.js";
8
+ import { assertSamplingCount } from "../sampling-limits.js";
9
+ const AXIS_EPSILON = 1e-10;
10
+ /**
11
+ * Revolves a non-duplicated closed `[radius, z]` profile around +Z.
12
+ *
13
+ * The profile is ordered counter-clockwise when radius points right and Z
14
+ * points up. Angular and profile seams wrap by index. A profile point on the
15
+ * axis is emitted once and joined as a triangle fan, avoiding coincident seam
16
+ * vertices and zero-area quads. Profile edges that lie entirely on the axis
17
+ * collapse to the solid's interior and are intentionally omitted.
18
+ * Profile coordinates must fit finite Float32 mesh storage.
19
+ *
20
+ * @param profile - Closed CCW `[radius, z]` profile without a duplicated seam point.
21
+ * @param options - Optional physical or exact circumferential sampling controls. Defaults to `{}`.
22
+ * @returns A closed indexed triangle mesh revolved counter-clockwise around +Z.
23
+ */
24
+ export const revolveMesh = (profile, options = {}) => {
25
+ if (typeof options !== "object" || options === null || Array.isArray(options))
26
+ throw new GeometryError("INVALID_ARGUMENT", "Revolution options require an object.", {
27
+ operation: "revolveMesh",
28
+ });
29
+ const degrees = options.revolveDegrees ?? 360;
30
+ if (!Number.isFinite(degrees) || degrees <= 0 || degrees > 360)
31
+ throw new GeometryError("INVALID_ARGUMENT", "revolveDegrees must be in (0, 360].", {
32
+ operation: "revolveMesh",
33
+ });
34
+ if (profile.length < 3) {
35
+ throw new Error("A revolved profile needs at least three points.");
36
+ }
37
+ const limits = resolveModelingLimits(options.limits);
38
+ assertSamplingCount(profile.length, "revolveMesh", limits.maxProfilePoints);
39
+ let maximumRadius = 0;
40
+ for (const [radius, z] of profile) {
41
+ if (!Number.isFinite(radius) || !Number.isFinite(z) || radius < 0) {
42
+ throw new Error("Revolved profile coordinates must be finite and radii non-negative.");
43
+ }
44
+ if (!Number.isFinite(Math.fround(radius)) || !Number.isFinite(Math.fround(z))) {
45
+ throw new GeometryError("NON_FINITE_VALUE", "Revolved profile coordinates exceed finite Float32 mesh storage.", { operation: "revolveMesh" });
46
+ }
47
+ maximumRadius = Math.max(maximumRadius, radius);
48
+ }
49
+ profile = normalizeLoop2(profile, true);
50
+ const circularSegments = resolveCircularPrimitiveSegments(maximumRadius, options, "revolveMesh");
51
+ const closed = degrees === 360;
52
+ const segments = closed
53
+ ? circularSegments
54
+ : Math.max(1, Math.ceil((circularSegments * degrees) / 360));
55
+ const rings = closed ? segments : segments + 1;
56
+ const vertexCount = profile.reduce((count, [radius]) => count + (radius <= AXIS_EPSILON ? 1 : rings), 0);
57
+ const triangleCount = profile.reduce((count, [radius], index) => {
58
+ const currentOnAxis = radius <= AXIS_EPSILON;
59
+ const nextOnAxis = profile[(index + 1) % profile.length][0] <= AXIS_EPSILON;
60
+ return (count +
61
+ (currentOnAxis && nextOnAxis ? 0 : currentOnAxis || nextOnAxis ? segments : segments * 2));
62
+ }, 0) + (closed ? 0 : (profile.length - 2) * 2);
63
+ assertMeshAllocationWithinLimits(vertexCount, triangleCount, (vertexCount + triangleCount) * 12, "revolveMesh", limits);
64
+ const vertexIndices = profile.map(([radius]) => radius <= AXIS_EPSILON ? new Uint32Array(1) : new Uint32Array(rings));
65
+ const positions = new Float32Array(vertexCount * 3);
66
+ let vertex = 0;
67
+ for (let profileIndex = 0; profileIndex < profile.length; profileIndex += 1) {
68
+ const [radius, z] = profile[profileIndex];
69
+ const indices = vertexIndices[profileIndex];
70
+ if (radius <= AXIS_EPSILON) {
71
+ indices[0] = vertex;
72
+ writeMeshVertex(positions, vertex++ * 3, [0, 0, z], "revolveMesh");
73
+ continue;
74
+ }
75
+ for (let segment = 0; segment < rings; segment += 1) {
76
+ const angle = (segment * degrees * Math.PI) / 180 / segments;
77
+ indices[segment] = vertex;
78
+ writeMeshVertex(positions, vertex++ * 3, [radius * Math.cos(angle), radius * Math.sin(angle), z], "revolveMesh");
79
+ }
80
+ }
81
+ const indices = new Uint32Array(triangleCount * 3);
82
+ let cursor = 0;
83
+ const triangle = (a, b, c) => {
84
+ indices[cursor++] = a;
85
+ indices[cursor++] = b;
86
+ indices[cursor++] = c;
87
+ };
88
+ for (let profileIndex = 0; profileIndex < profile.length; profileIndex += 1) {
89
+ const following = (profileIndex + 1) % profile.length;
90
+ const currentRadius = profile[profileIndex][0];
91
+ const followingRadius = profile[following][0];
92
+ const current = vertexIndices[profileIndex];
93
+ const next = vertexIndices[following];
94
+ const currentOnAxis = currentRadius <= AXIS_EPSILON;
95
+ const nextOnAxis = followingRadius <= AXIS_EPSILON;
96
+ if (currentOnAxis && nextOnAxis) {
97
+ continue;
98
+ }
99
+ for (let segment = 0; segment < segments; segment += 1) {
100
+ const nextSegment = closed ? (segment + 1) % segments : segment + 1;
101
+ if (currentOnAxis) {
102
+ triangle(current[0], next[nextSegment], next[segment]);
103
+ }
104
+ else if (nextOnAxis) {
105
+ triangle(current[segment], current[nextSegment], next[0]);
106
+ }
107
+ else {
108
+ triangle(current[segment], current[nextSegment], next[nextSegment]);
109
+ triangle(current[segment], next[nextSegment], next[segment]);
110
+ }
111
+ }
112
+ }
113
+ if (!closed) {
114
+ const cap = triangulateLoop2(profile);
115
+ const at = (profileIndex, station) => vertexIndices[profileIndex][profile[profileIndex][0] <= AXIS_EPSILON ? 0 : station];
116
+ for (let offset = 0; offset < cap.length; offset += 3) {
117
+ const a = cap[offset], b = cap[offset + 1], c = cap[offset + 2];
118
+ triangle(at(a, 0), at(b, 0), at(c, 0));
119
+ triangle(at(a, segments), at(c, segments), at(b, segments));
120
+ }
121
+ }
122
+ return validateTriangleMesh({ positions, indices });
123
+ };
@@ -0,0 +1,29 @@
1
+ import { GeometryError } from "./errors.js";
2
+ /** Conservative defaults; trusted callers can supply larger explicit ceilings. */
3
+ export const defaultGeometrySamplingLimits = Object.freeze({
4
+ maxPathPoints: 100_000,
5
+ maxProfilePoints: 100_000,
6
+ maxSweepProfilePoints: 4_096,
7
+ maxCircularSegments: 512,
8
+ });
9
+ /** Resolves finite positive safe-integer allocation ceilings without ambient state. */
10
+ export function resolveGeometrySamplingLimits(overrides = {}) {
11
+ if (typeof overrides !== "object" || overrides === null || Array.isArray(overrides))
12
+ throw new GeometryError("INVALID_ARGUMENT", "Sampling limits must be an object.", {
13
+ operation: "geometrySamplingLimits",
14
+ });
15
+ const result = { ...defaultGeometrySamplingLimits, ...overrides };
16
+ for (const key of Object.keys(defaultGeometrySamplingLimits)) {
17
+ if (!Number.isSafeInteger(result[key]) || result[key] < (key === "maxCircularSegments" ? 3 : 1))
18
+ throw new GeometryError("INVALID_ARGUMENT", `${key} must be a positive safe integer${key === "maxCircularSegments" ? " of at least three" : ""}.`, { operation: "geometrySamplingLimits" });
19
+ }
20
+ return Object.freeze(result);
21
+ }
22
+ /** @internal Checks predictable allocations before constructing arrays or evaluating callbacks. */
23
+ export function assertSamplingCount(count, operation, maximum, message = `Sampling requires ${count} items; the configured ceiling and array capacity allow ${Math.min(maximum, 0xffff_ffff)}.`) {
24
+ if (!Number.isSafeInteger(count) || count < 0 || count > maximum || count > 0xffff_ffff)
25
+ throw new GeometryError("COMPLEXITY_LIMIT", message, {
26
+ operation,
27
+ details: { requiredCount: count, allowedCount: Math.min(maximum, 0xffff_ffff) },
28
+ });
29
+ }
@@ -0,0 +1,181 @@
1
+ import { GeometryError } from "./errors.js";
2
+ import { unionPolygonGeometry } from "./polygon-clipping.js";
3
+ import { normalizeRegion2 } from "./regions.js";
4
+ import { resolveCircularSegments } from "./resolution.js";
5
+ import { assertSamplingCount, resolveGeometrySamplingLimits, } from "./sampling-limits.js";
6
+ import { resolveGeometryTolerance } from "./tolerance.js";
7
+ const DEFAULT_MITER_LIMIT = 4;
8
+ function fail(message) {
9
+ throw new GeometryError("INVALID_ARGUMENT", message, { operation: "strokePathRegion" });
10
+ }
11
+ function ring(points) {
12
+ const result = points.map(([x, y]) => [x, y]);
13
+ const first = result[0];
14
+ const last = result.at(-1);
15
+ if (first !== undefined && last !== undefined && (first[0] !== last[0] || first[1] !== last[1])) {
16
+ result.push([...first]);
17
+ }
18
+ return result;
19
+ }
20
+ function ellipsePoints(center, radius, segments) {
21
+ return Array.from({ length: segments }, (_, index) => {
22
+ const angle = (index * Math.PI * 2) / segments;
23
+ return [center[0] + Math.cos(angle) * radius, center[1] + Math.sin(angle) * radius];
24
+ });
25
+ }
26
+ function circlePolygon(center, radius, segments) {
27
+ return [ring(ellipsePoints(center, radius, segments))];
28
+ }
29
+ function cornerPolygon(previous, point, next, halfWidth, join) {
30
+ const incomingLength = Math.hypot(point[0] - previous[0], point[1] - previous[1]);
31
+ const outgoingLength = Math.hypot(next[0] - point[0], next[1] - point[1]);
32
+ const incoming = [
33
+ (point[0] - previous[0]) / incomingLength,
34
+ (point[1] - previous[1]) / incomingLength,
35
+ ];
36
+ const outgoing = [
37
+ (next[0] - point[0]) / outgoingLength,
38
+ (next[1] - point[1]) / outgoingLength,
39
+ ];
40
+ const turn = incoming[0] * outgoing[1] - incoming[1] * outgoing[0];
41
+ if (Math.abs(turn) <= Number.EPSILON * 8)
42
+ return undefined;
43
+ const side = turn > 0 ? -1 : 1;
44
+ const before = [-incoming[1] * halfWidth * side, incoming[0] * halfWidth * side];
45
+ const after = [-outgoing[1] * halfWidth * side, outgoing[0] * halfWidth * side];
46
+ const points = [point, [point[0] + before[0], point[1] + before[1]]];
47
+ const denominator = 1 + incoming[0] * outgoing[0] + incoming[1] * outgoing[1];
48
+ // The outer offset-line intersection has radius halfWidth / cos(turnAngle / 2).
49
+ // Apply SVG's miter ratio before division so sharp reversals cannot create an unbounded tip.
50
+ if (join === "miter" && denominator >= 2 / DEFAULT_MITER_LIMIT ** 2) {
51
+ points.push([
52
+ point[0] + (before[0] + after[0]) / denominator,
53
+ point[1] + (before[1] + after[1]) / denominator,
54
+ ]);
55
+ }
56
+ points.push([point[0] + after[0], point[1] + after[1]]);
57
+ return [ring(points)];
58
+ }
59
+ /** Removes consecutive points that cannot define a visible stroke segment at model scale. */
60
+ function deduplicatePath(path, closed, width) {
61
+ const scale = Math.max(width, ...path.flatMap(([x, y]) => [Math.abs(x), Math.abs(y)]));
62
+ const tolerance = resolveGeometryTolerance(scale).linear;
63
+ const points = [];
64
+ for (const point of path) {
65
+ const previous = points.at(-1);
66
+ if (previous === undefined ||
67
+ Math.hypot(point[0] - previous[0], point[1] - previous[1]) > tolerance) {
68
+ points.push(point);
69
+ }
70
+ }
71
+ if (closed && points.length > 1) {
72
+ const first = points[0];
73
+ const last = points.at(-1);
74
+ if (Math.hypot(first[0] - last[0], first[1] - last[1]) <= tolerance)
75
+ points.pop();
76
+ }
77
+ return Object.freeze(points);
78
+ }
79
+ /** @internal Polygon-clipping representation shared with the bounded SVG importer. */
80
+ export function strokePathMultiPolygon(path, options, circularSegments) {
81
+ const closed = options.closed ?? false;
82
+ const cap = options.cap ?? "butt";
83
+ const join = options.join ?? "miter";
84
+ const half = options.width / 2;
85
+ const limits = resolveGeometrySamplingLimits(options.limits);
86
+ assertSamplingCount(path.length, "strokePathRegion", limits.maxPathPoints);
87
+ assertSamplingCount(circularSegments, "strokePathRegion", limits.maxCircularSegments);
88
+ const normalizedPath = deduplicatePath(path, closed, options.width);
89
+ if (normalizedPath.length < (closed ? 3 : 2))
90
+ return [];
91
+ const polygons = [];
92
+ const segmentCount = closed ? normalizedPath.length : normalizedPath.length - 1;
93
+ const joinCount = closed ? normalizedPath.length : Math.max(0, normalizedPath.length - 2);
94
+ const constructionVertices = segmentCount * 4 +
95
+ joinCount * (join === "round" ? circularSegments : join === "miter" ? 4 : 3) +
96
+ (!closed && cap === "round" ? 2 * circularSegments : 0);
97
+ assertSamplingCount(constructionVertices, "strokePathRegion", limits.maxProfilePoints);
98
+ for (let index = 0; index < segmentCount; index += 1) {
99
+ let start = normalizedPath[index];
100
+ let end = normalizedPath[(index + 1) % normalizedPath.length];
101
+ const length = Math.hypot(end[0] - start[0], end[1] - start[1]);
102
+ const tangent = [(end[0] - start[0]) / length, (end[1] - start[1]) / length];
103
+ if (!closed && cap === "square") {
104
+ if (index === 0)
105
+ start = [start[0] - tangent[0] * half, start[1] - tangent[1] * half];
106
+ if (index === segmentCount - 1) {
107
+ end = [end[0] + tangent[0] * half, end[1] + tangent[1] * half];
108
+ }
109
+ }
110
+ const normal = [-tangent[1] * half, tangent[0] * half];
111
+ polygons.push([
112
+ ring([
113
+ [start[0] + normal[0], start[1] + normal[1]],
114
+ [end[0] + normal[0], end[1] + normal[1]],
115
+ [end[0] - normal[0], end[1] - normal[1]],
116
+ [start[0] - normal[0], start[1] - normal[1]],
117
+ ]),
118
+ ]);
119
+ }
120
+ if (join === "round") {
121
+ const joinPoints = closed ? normalizedPath : normalizedPath.slice(1, -1);
122
+ polygons.push(...joinPoints.map((point) => circlePolygon(point, half, circularSegments)));
123
+ }
124
+ else {
125
+ const first = closed ? 0 : 1;
126
+ const end = closed ? normalizedPath.length : normalizedPath.length - 1;
127
+ for (let index = first; index < end; index += 1) {
128
+ const corner = cornerPolygon(normalizedPath[(index - 1 + normalizedPath.length) % normalizedPath.length], normalizedPath[index], normalizedPath[(index + 1) % normalizedPath.length], half, join);
129
+ if (corner !== undefined)
130
+ polygons.push(corner);
131
+ }
132
+ }
133
+ if (!closed && cap === "round") {
134
+ polygons.push(circlePolygon(normalizedPath[0], half, circularSegments), circlePolygon(normalizedPath.at(-1), half, circularSegments));
135
+ }
136
+ const result = unionPolygonGeometry(polygons);
137
+ const outputVertices = result.reduce((count, polygon) => count + polygon.reduce((total, contour) => total + Math.max(0, contour.length - 1), 0), 0);
138
+ assertSamplingCount(outputVertices, "strokePathRegion", limits.maxProfilePoints);
139
+ return result;
140
+ }
141
+ function regionFromMultiPolygon(value) {
142
+ return normalizeRegion2(value.map((polygon) => ({
143
+ outer: polygon[0].slice(0, -1).map(([x, y]) => [x, y]),
144
+ holes: polygon.slice(1).map((hole) => hole.slice(0, -1).map(([x, y]) => [x, y])),
145
+ })));
146
+ }
147
+ /**
148
+ * Converts a sampled millimetre path into the filled region covered by a centered stroke.
149
+ *
150
+ * Closed paths omit a duplicated seam point. Zero-length sampled segments are ignored, while the
151
+ * effective path must still contain visible length. Round caps and joins use deterministic physical
152
+ * resolution. Bevels fill the outer corner triangle; miters extend the outer edges to their
153
+ * intersection, falling back to bevels above SVG's default miter limit of 4.
154
+ */
155
+ export function strokePathRegion(path, options) {
156
+ const closed = options.closed ?? false;
157
+ if (!Number.isFinite(options.width) || options.width <= 0) {
158
+ return fail("Stroke width must be finite and greater than zero.");
159
+ }
160
+ if (path.length < (closed ? 3 : 2)) {
161
+ return fail(`${closed ? "Closed" : "Open"} strokes need more sampled path points.`);
162
+ }
163
+ if (!path.every((point) => Number.isFinite(point[0]) && Number.isFinite(point[1]))) {
164
+ return fail("Stroke path coordinates must be finite.");
165
+ }
166
+ const cap = options.cap ?? "butt";
167
+ const join = options.join ?? "miter";
168
+ if (!["butt", "round", "square"].includes(cap)) {
169
+ return fail("Stroke cap must be butt, round, or square.");
170
+ }
171
+ if (!["bevel", "miter", "round"].includes(join)) {
172
+ return fail("Stroke join must be bevel, miter, or round.");
173
+ }
174
+ const circularSegments = cap === "round" || join === "round"
175
+ ? resolveCircularSegments(options.width / 2, options.resolution, options.limits)
176
+ : 3;
177
+ const result = strokePathMultiPolygon(path, { ...options, cap, closed, join }, circularSegments);
178
+ if (result.length === 0)
179
+ return fail("The sampled stroke path has no visible length.");
180
+ return regionFromMultiPolygon(result);
181
+ }
@@ -0,0 +1,23 @@
1
+ import { GeometryError } from "../errors.js";
2
+ export function invalid(operation, message) {
3
+ throw new GeometryError("INVALID_ARGUMENT", message, { operation });
4
+ }
5
+ export function unsupported(operation, message) {
6
+ throw new GeometryError("UNSUPPORTED_RUNTIME", message, { operation });
7
+ }
8
+ export function emptyGeometry(operation, message) {
9
+ throw new GeometryError("DEGENERATE_GEOMETRY", message, { operation });
10
+ }
11
+ export function complexity(operation, message, details) {
12
+ throw new GeometryError("COMPLEXITY_LIMIT", message, {
13
+ operation,
14
+ ...(details === undefined ? {} : { details }),
15
+ });
16
+ }
17
+ export function rethrowAsInvalid(operation, error) {
18
+ if (error instanceof GeometryError) {
19
+ throw error;
20
+ }
21
+ const detail = error instanceof Error ? error.message : String(error);
22
+ invalid(operation, `Invalid SVG input: ${detail}`);
23
+ }
@@ -0,0 +1,172 @@
1
+ import { polygonClipping, unionPolygonGeometry, xorPolygonGeometry } from "../polygon-clipping.js";
2
+ import { normalizeRegion2, signedAreaLoop2 } from "../regions.js";
3
+ import { assertSamplingCount } from "../sampling-limits.js";
4
+ /** Private clipping stage with the selected execution's immutable allocation ceiling. */
5
+ export function createBoundedSvgClipping(maximumVertices) {
6
+ function ring(points) {
7
+ const result = points.map(([x, y]) => [x, y]);
8
+ const first = result[0];
9
+ const last = result.at(-1);
10
+ if (first !== undefined && last !== undefined && (first[0] !== last[0] || first[1] !== last[1]))
11
+ result.push([...first]);
12
+ return result;
13
+ }
14
+ function ringVertexCount(value) {
15
+ const first = value[0];
16
+ const last = value.at(-1);
17
+ const repeatedSeam = first !== undefined && last !== undefined && first[0] === last[0] && first[1] === last[1];
18
+ return value.length - (repeatedSeam ? 1 : 0);
19
+ }
20
+ function multiPolygonVertexCount(value) {
21
+ return value.reduce((total, polygon) => total + polygon.reduce((polygonTotal, path) => polygonTotal + ringVertexCount(path), 0), 0);
22
+ }
23
+ function assertExpandedVertexCount(vertices, stage) {
24
+ assertSamplingCount(vertices, "preflightSvgRegion", maximumVertices, `SVG ${stage} exceeds ${maximumVertices} expanded vertices (requires ${vertices}).`);
25
+ }
26
+ function boundedMultiPolygon(value, stage) {
27
+ assertExpandedVertexCount(multiPolygonVertexCount(value), stage);
28
+ return value;
29
+ }
30
+ function boundedUnion(values, stage) {
31
+ const nonEmpty = values.filter((value) => value.length > 0);
32
+ assertExpandedVertexCount(nonEmpty.reduce((total, value) => total + multiPolygonVertexCount(value), 0), `${stage} input`);
33
+ return boundedMultiPolygon(unionPolygonGeometry(nonEmpty), stage);
34
+ }
35
+ function boundedXor(values, stage) {
36
+ const nonEmpty = values.filter((value) => value.length > 0);
37
+ assertExpandedVertexCount(nonEmpty.reduce((total, value) => total + multiPolygonVertexCount(value), 0), `${stage} input`);
38
+ return boundedMultiPolygon(xorPolygonGeometry(nonEmpty), stage);
39
+ }
40
+ function unionAll(polygons) {
41
+ return boundedUnion(polygons, "union output");
42
+ }
43
+ function intersectGeometry(left, right) {
44
+ if (left.length === 0 || right.length === 0)
45
+ return [];
46
+ assertExpandedVertexCount(multiPolygonVertexCount(left) + multiPolygonVertexCount(right), "intersection input");
47
+ return boundedMultiPolygon(polygonClipping.intersection(left, right), "intersection output");
48
+ }
49
+ function multiPolygonArea(value) {
50
+ return value.reduce((total, polygon) => {
51
+ const [outer, ...holes] = polygon;
52
+ if (outer === undefined)
53
+ return total;
54
+ return (total +
55
+ Math.abs(signedAreaLoop2(outer)) -
56
+ holes.reduce((holeTotal, hole) => holeTotal + Math.abs(signedAreaLoop2(hole)), 0));
57
+ }, 0);
58
+ }
59
+ function coverageHasMultiplicity(rings, union) {
60
+ const summedArea = rings.reduce((total, points) => total + Math.abs(signedAreaLoop2(points)), 0);
61
+ const tolerance = Math.max(1, summedArea) * 1e-9;
62
+ return Math.abs(summedArea - multiPolygonArea(union)) > tolerance;
63
+ }
64
+ function addCoverageBits(left, right) {
65
+ const output = [];
66
+ let carry = [];
67
+ const inputBits = Math.max(left.length, right.length);
68
+ for (let index = 0; index < inputBits || carry.length > 0; index += 1) {
69
+ const inputs = [left[index] ?? [], right[index] ?? [], carry].filter((geometry) => geometry.length > 0);
70
+ if (inputs.length === 0) {
71
+ output.push([]);
72
+ carry = [];
73
+ }
74
+ else if (inputs.length === 1) {
75
+ output.push(inputs[0]);
76
+ carry = [];
77
+ }
78
+ else if (inputs.length === 2) {
79
+ output.push(boundedXor(inputs, "nonzero coverage XOR"));
80
+ carry = intersectGeometry(inputs[0], inputs[1]);
81
+ }
82
+ else {
83
+ output.push(boundedXor(inputs, "nonzero coverage XOR"));
84
+ carry = boundedUnion([
85
+ intersectGeometry(inputs[0], inputs[1]),
86
+ intersectGeometry(inputs[0], inputs[2]),
87
+ intersectGeometry(inputs[1], inputs[2]),
88
+ ], "nonzero coverage carry");
89
+ }
90
+ }
91
+ return output;
92
+ }
93
+ function coverageBits(polygons) {
94
+ let level = polygons.map((polygon) => [
95
+ boundedUnion([[polygon]], "nonzero coverage seed"),
96
+ ]);
97
+ while (level.length > 1) {
98
+ const next = [];
99
+ for (let index = 0; index < level.length; index += 2) {
100
+ const left = level[index];
101
+ const right = level[index + 1];
102
+ next.push(right === undefined ? left : addCoverageBits(left, right));
103
+ }
104
+ level = next;
105
+ }
106
+ return level[0] ?? [];
107
+ }
108
+ function differingCoverage(positive, negative) {
109
+ const positiveUnion = boundedUnion([positive.slice()], "positive winding union");
110
+ const negativeUnion = boundedUnion([negative.slice()], "negative winding union");
111
+ if (intersectGeometry(positiveUnion, negativeUnion).length === 0) {
112
+ return boundedUnion([positiveUnion, negativeUnion], "disconnected winding union");
113
+ }
114
+ const positiveBits = coverageHasMultiplicity(positive.map((polygon) => polygon[0] ?? []), positiveUnion)
115
+ ? coverageBits(positive)
116
+ : [positiveUnion];
117
+ const negativeBits = coverageHasMultiplicity(negative.map((polygon) => polygon[0] ?? []), negativeUnion)
118
+ ? coverageBits(negative)
119
+ : [negativeUnion];
120
+ const differingBits = [];
121
+ for (let index = 0; index < Math.max(positiveBits.length, negativeBits.length); index += 1) {
122
+ const positiveBit = positiveBits[index] ?? [];
123
+ const negativeBit = negativeBits[index] ?? [];
124
+ if (positiveBit.length === 0)
125
+ differingBits.push(negativeBit);
126
+ else if (negativeBit.length === 0)
127
+ differingBits.push(positiveBit);
128
+ else
129
+ differingBits.push(boundedXor([positiveBit, negativeBit], "winding difference XOR"));
130
+ }
131
+ return boundedUnion(differingBits, "winding difference union");
132
+ }
133
+ function fillGeometry(subpaths, fillRule) {
134
+ const rings = subpaths
135
+ .filter((subpath) => subpath.points.length >= 3)
136
+ .map((subpath) => subpath.points);
137
+ if (rings.length === 0)
138
+ return [];
139
+ const polygons = rings.map((points) => [ring(points)]);
140
+ if (fillRule === "evenodd") {
141
+ return boundedXor(polygons.map((polygon) => [polygon]), "even-odd fill output");
142
+ }
143
+ const positive = rings
144
+ .filter((points) => signedAreaLoop2(points) >= 0)
145
+ .map((points) => [ring(points)]);
146
+ const negative = rings
147
+ .filter((points) => signedAreaLoop2(points) < 0)
148
+ .map((points) => [ring(points)]);
149
+ if (positive.length === 0)
150
+ return boundedUnion([negative], "negative fill output");
151
+ if (negative.length === 0)
152
+ return boundedUnion([positive], "positive fill output");
153
+ // Compare the binary coverage counts instead of choosing one global exterior winding. This
154
+ // preserves disconnected opposite-winding components and nested islands while still cancelling
155
+ // precisely where positive and negative nonzero winding counts match.
156
+ return differingCoverage(positive, negative);
157
+ }
158
+ function regionFromMultiPolygon(value) {
159
+ return normalizeRegion2(value.map((polygon) => ({
160
+ outer: polygon[0].slice(0, -1).map(([x, y]) => [x, y]),
161
+ holes: polygon.slice(1).map((hole) => hole.slice(0, -1).map(([x, y]) => [x, y])),
162
+ })));
163
+ }
164
+ return {
165
+ assertExpandedVertexCount,
166
+ boundedMultiPolygon,
167
+ fillGeometry,
168
+ multiPolygonVertexCount,
169
+ regionFromMultiPolygon,
170
+ unionAll,
171
+ };
172
+ }
@@ -0,0 +1 @@
1
+ export { parseSvgDocument, parseSvgPath } from "./parser.js";
@@ -0,0 +1,130 @@
1
+ import { invalid } from "./errors.js";
2
+ export const IDENTITY_MATRIX = [1, 0, 0, 1, 0, 0];
3
+ export function multiplyMatrices(left, right) {
4
+ return [
5
+ left[0] * right[0] + left[2] * right[1],
6
+ left[1] * right[0] + left[3] * right[1],
7
+ left[0] * right[2] + left[2] * right[3],
8
+ left[1] * right[2] + left[3] * right[3],
9
+ left[0] * right[4] + left[2] * right[5] + left[4],
10
+ left[1] * right[4] + left[3] * right[5] + left[5],
11
+ ];
12
+ }
13
+ export function transformPoint(matrix, point) {
14
+ return [
15
+ matrix[0] * point[0] + matrix[2] * point[1] + matrix[4],
16
+ matrix[1] * point[0] + matrix[3] * point[1] + matrix[5],
17
+ ];
18
+ }
19
+ const NUMBER_PATTERN = /[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?/g;
20
+ /** Parses compact SVG number sequences while rejecting unconsumed syntax. */
21
+ export function parseNumberSequence(value, operation) {
22
+ const numbers = [];
23
+ let end = 0;
24
+ NUMBER_PATTERN.lastIndex = 0;
25
+ for (let match = NUMBER_PATTERN.exec(value); match !== null; match = NUMBER_PATTERN.exec(value)) {
26
+ const separator = value.slice(end, match.index);
27
+ if (!/^[\s,]*$/.test(separator) ||
28
+ (separator.match(/,/g)?.length ?? 0) > 1 ||
29
+ (numbers.length === 0 && separator.includes(","))) {
30
+ invalid(operation, `Invalid numeric syntax near "${separator}".`);
31
+ }
32
+ const parsed = Number(match[0]);
33
+ if (!Number.isFinite(parsed)) {
34
+ invalid(operation, "SVG numeric values must be finite.");
35
+ }
36
+ numbers.push(parsed);
37
+ end = match.index + match[0].length;
38
+ }
39
+ if (!/^\s*$/.test(value.slice(end))) {
40
+ invalid(operation, "Invalid trailing numeric syntax.");
41
+ }
42
+ if (numbers.length === 0 && value.trim() !== "") {
43
+ invalid(operation, `Expected a numeric value, received "${value}".`);
44
+ }
45
+ return numbers;
46
+ }
47
+ function transformFor(name, values, operation) {
48
+ switch (name) {
49
+ case "matrix":
50
+ if (values.length !== 6)
51
+ invalid(operation, "matrix() requires six numbers.");
52
+ return values;
53
+ case "translate":
54
+ if (values.length < 1 || values.length > 2) {
55
+ invalid(operation, "translate() requires one or two numbers.");
56
+ }
57
+ return [1, 0, 0, 1, values[0] ?? 0, values[1] ?? 0];
58
+ case "scale": {
59
+ if (values.length < 1 || values.length > 2) {
60
+ invalid(operation, "scale() requires one or two numbers.");
61
+ }
62
+ const sx = values[0] ?? 1;
63
+ return [sx, 0, 0, values[1] ?? sx, 0, 0];
64
+ }
65
+ case "rotate": {
66
+ if (values.length !== 1 && values.length !== 3) {
67
+ invalid(operation, "rotate() requires an angle, optionally followed by a center.");
68
+ }
69
+ const radians = ((values[0] ?? 0) * Math.PI) / 180;
70
+ const rotation = [
71
+ Math.cos(radians),
72
+ Math.sin(radians),
73
+ -Math.sin(radians),
74
+ Math.cos(radians),
75
+ 0,
76
+ 0,
77
+ ];
78
+ if (values.length === 1)
79
+ return rotation;
80
+ const cx = values[1] ?? 0;
81
+ const cy = values[2] ?? 0;
82
+ return multiplyMatrices(multiplyMatrices([1, 0, 0, 1, cx, cy], rotation), [
83
+ 1,
84
+ 0,
85
+ 0,
86
+ 1,
87
+ -cx,
88
+ -cy,
89
+ ]);
90
+ }
91
+ case "skewX": {
92
+ if (values.length !== 1)
93
+ invalid(operation, "skewX() requires one angle.");
94
+ const tangent = Math.tan(((values[0] ?? 0) * Math.PI) / 180);
95
+ if (!Number.isFinite(tangent))
96
+ invalid(operation, "skewX() produced a non-finite matrix.");
97
+ return [1, 0, tangent, 1, 0, 0];
98
+ }
99
+ case "skewY": {
100
+ if (values.length !== 1)
101
+ invalid(operation, "skewY() requires one angle.");
102
+ const tangent = Math.tan(((values[0] ?? 0) * Math.PI) / 180);
103
+ if (!Number.isFinite(tangent))
104
+ invalid(operation, "skewY() produced a non-finite matrix.");
105
+ return [1, tangent, 0, 1, 0, 0];
106
+ }
107
+ default:
108
+ invalid(operation, `Unsupported SVG transform "${name}".`);
109
+ }
110
+ }
111
+ /** Parses an SVG transform list using standard post-multiplication order. */
112
+ export function parseTransform(value, operation) {
113
+ if (value === undefined || value.trim() === "")
114
+ return IDENTITY_MATRIX;
115
+ const callPattern = /([A-Za-z]+)\s*\(([^()]*)\)/g;
116
+ let result = IDENTITY_MATRIX;
117
+ let end = 0;
118
+ for (let match = callPattern.exec(value); match !== null; match = callPattern.exec(value)) {
119
+ if (!/^[\s,]*$/.test(value.slice(end, match.index))) {
120
+ invalid(operation, "Malformed SVG transform list.");
121
+ }
122
+ const values = parseNumberSequence(match[2] ?? "", operation);
123
+ result = multiplyMatrices(result, transformFor(match[1] ?? "", values, operation));
124
+ end = match.index + match[0].length;
125
+ }
126
+ if (end === 0 || !/^\s*$/.test(value.slice(end))) {
127
+ invalid(operation, "Malformed SVG transform list.");
128
+ }
129
+ return result;
130
+ }