@metamesh/modeling 0.0.0-stage → 0.7.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.
- package/CHANGELOG.md +111 -0
- package/LICENSE +200 -0
- package/README.md +376 -2
- package/THIRD_PARTY_NOTICES.md +153 -0
- package/dist/api/engine.d.ts +71 -0
- package/dist/api/geometry.d.ts +3136 -0
- package/dist/api/index.d.ts +2087 -0
- package/dist/api/text.d.ts +93 -0
- package/dist/engine/construction-operations.js +189 -0
- package/dist/engine/execution.js +227 -0
- package/dist/engine/export-3mf.js +100 -0
- package/dist/engine/extrusion-finishing.js +82 -0
- package/dist/engine/finalization.js +214 -0
- package/dist/engine/index.js +83 -0
- package/dist/engine/inspection-operations.js +52 -0
- package/dist/engine/kernel/LICENSE +201 -0
- package/dist/engine/kernel/manifold.js +2 -0
- package/dist/engine/kernel/manifold.wasm +0 -0
- package/dist/engine/loft-operations.js +156 -0
- package/dist/engine/mechanical-operations.js +51 -0
- package/dist/engine/modeling-context.js +38 -0
- package/dist/engine/native/adapter.js +20 -0
- package/dist/engine/native/context.js +209 -0
- package/dist/engine/native/copied-views.js +46 -0
- package/dist/engine/native/index.js +13 -0
- package/dist/engine/native/operations.js +115 -0
- package/dist/engine/native/ownership.js +98 -0
- package/dist/engine/native/regions.js +138 -0
- package/dist/engine/native/scoped-primitives.js +127 -0
- package/dist/engine/native/scoped-threads.js +34 -0
- package/dist/engine/native/threads.js +52 -0
- package/dist/engine/native/transforms.js +111 -0
- package/dist/engine/operation-environment.js +20 -0
- package/dist/engine/pattern-operations.js +36 -0
- package/dist/engine/pattern-placements.js +176 -0
- package/dist/engine/primitive-anchors.js +70 -0
- package/dist/engine/primitive-correspondence.js +74 -0
- package/dist/engine/primitive-operations.js +151 -0
- package/dist/engine/primitive-profile-types.js +1 -0
- package/dist/engine/primitive-profiles.js +250 -0
- package/dist/engine/profile-mesh.js +279 -0
- package/dist/engine/section-operations.js +93 -0
- package/dist/engine/shape-anchors.js +88 -0
- package/dist/engine/transform-operations.js +225 -0
- package/dist/engine/types.js +1 -0
- package/dist/geometry/anchors.js +226 -0
- package/dist/geometry/curves/evaluation.js +162 -0
- package/dist/geometry/curves/sampling.js +266 -0
- package/dist/geometry/curves/types.js +1 -0
- package/dist/geometry/cylinder.js +117 -0
- package/dist/geometry/edge-finishing.js +160 -0
- package/dist/geometry/errors.js +76 -0
- package/dist/geometry/extrusion.js +150 -0
- package/dist/geometry/heightfield.js +115 -0
- package/dist/geometry/index.js +42 -0
- package/dist/geometry/loft-correspondence.js +204 -0
- package/dist/geometry/loft-geometry.js +184 -0
- package/dist/geometry/loft-options.js +56 -0
- package/dist/geometry/loft-simple-mesh.js +92 -0
- package/dist/geometry/loft-triangulation.js +64 -0
- package/dist/geometry/loft.js +95 -0
- package/dist/geometry/math/vector3.js +71 -0
- package/dist/geometry/mechanical/gear-motion.js +94 -0
- package/dist/geometry/mechanical/gears.js +484 -0
- package/dist/geometry/mechanical/index.js +13 -0
- package/dist/geometry/mechanical/standards.js +67 -0
- package/dist/geometry/mechanical/thread-helices.js +44 -0
- package/dist/geometry/mechanical/thread-mesh-plan.js +358 -0
- package/dist/geometry/mechanical/thread-mesh.js +142 -0
- package/dist/geometry/mechanical/thread-profiles.js +373 -0
- package/dist/geometry/mechanical/thread-solids.js +230 -0
- package/dist/geometry/mechanical/thread-surface.js +433 -0
- package/dist/geometry/mechanical/threads.js +138 -0
- package/dist/geometry/mesh-edge-table.js +126 -0
- package/dist/geometry/mesh-planar.js +408 -0
- package/dist/geometry/mesh-quality.js +396 -0
- package/dist/geometry/mesh-signature.js +137 -0
- package/dist/geometry/mesh.js +283 -0
- package/dist/geometry/model-limits.js +50 -0
- package/dist/geometry/numeric.js +11 -0
- package/dist/geometry/paths/frames.js +118 -0
- package/dist/geometry/paths/helix.js +49 -0
- package/dist/geometry/paths/path.js +32 -0
- package/dist/geometry/paths/planar.js +135 -0
- package/dist/geometry/paths/resampling.js +125 -0
- package/dist/geometry/polygon-clipping.js +52 -0
- package/dist/geometry/primitive-options.js +1 -0
- package/dist/geometry/primitive-resolution.js +17 -0
- package/dist/geometry/primitive-resolvers.js +235 -0
- package/dist/geometry/profiles/matched.js +192 -0
- package/dist/geometry/profiles/open-profile.js +137 -0
- package/dist/geometry/profiles/profiles.js +91 -0
- package/dist/geometry/profiles/rectangle.js +249 -0
- package/dist/geometry/profiles/shapes.js +41 -0
- package/dist/geometry/profiles/span-sampling.js +28 -0
- package/dist/geometry/profiles/types.js +1 -0
- package/dist/geometry/profiles/validation.js +20 -0
- package/dist/geometry/regions.js +371 -0
- package/dist/geometry/resolution.js +236 -0
- package/dist/geometry/revolve/revolve-mesh.js +129 -0
- package/dist/geometry/sampling-limits.js +29 -0
- package/dist/geometry/stroke-path.js +181 -0
- package/dist/geometry/svg/errors.js +23 -0
- package/dist/geometry/svg/fill.js +172 -0
- package/dist/geometry/svg/index.js +1 -0
- package/dist/geometry/svg/matrix.js +130 -0
- package/dist/geometry/svg/parser.js +536 -0
- package/dist/geometry/svg/path.js +349 -0
- package/dist/geometry/svg/types.js +1 -0
- package/dist/geometry/svg-import.js +662 -0
- package/dist/geometry/sweeps/options.js +28 -0
- package/dist/geometry/sweeps/orientation.js +106 -0
- package/dist/geometry/sweeps/stations.js +161 -0
- package/dist/geometry/sweeps/sweep-closed.js +96 -0
- package/dist/geometry/sweeps/sweep-mesh.js +30 -0
- package/dist/geometry/sweeps/sweep-open.js +76 -0
- package/dist/geometry/sweeps/validation.js +116 -0
- package/dist/geometry/text-contours.js +32 -0
- package/dist/geometry/text-layout.js +1 -0
- package/dist/geometry/tolerance.js +53 -0
- package/dist/geometry/transform-geometry.js +41 -0
- package/dist/geometry/transform3.js +340 -0
- package/dist/geometry/types.js +1 -0
- package/dist/index.js +1 -0
- package/dist/model/execution-binding.js +30 -0
- package/dist/model/operations.js +154 -0
- package/dist/model/options.js +1 -0
- package/dist/model/types.js +1 -0
- package/dist/text/index.js +58 -0
- package/dist/text/vector-text-layout.js +279 -0
- package/package.json +100 -3
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
import { SaxesParser } from "saxes";
|
|
2
|
+
import { complexity, emptyGeometry, invalid, rethrowAsInvalid, unsupported } from "./errors.js";
|
|
3
|
+
import { multiplyMatrices, parseNumberSequence, parseTransform } from "./matrix.js";
|
|
4
|
+
import { flattenSvgPath, SvgVertexBudget } from "./path.js";
|
|
5
|
+
const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
|
|
6
|
+
const XMLNS_NAMESPACE = "http://www.w3.org/2000/xmlns/";
|
|
7
|
+
const MM_PER_PX = 25.4 / 96;
|
|
8
|
+
const MAX_SVG_BYTES = 1024 * 1024;
|
|
9
|
+
const MAX_XML_DEPTH = 64;
|
|
10
|
+
const MAX_RENDERED_SHAPES = 10_000;
|
|
11
|
+
const SUPPORTED_ELEMENTS = new Set(["svg", "g", "path", "rect", "circle", "ellipse", "polygon"]);
|
|
12
|
+
const COMMON_ATTRIBUTES = new Set([
|
|
13
|
+
"class",
|
|
14
|
+
"display",
|
|
15
|
+
"fill",
|
|
16
|
+
"fill-opacity",
|
|
17
|
+
"fill-rule",
|
|
18
|
+
"id",
|
|
19
|
+
"opacity",
|
|
20
|
+
"stroke",
|
|
21
|
+
"stroke-opacity",
|
|
22
|
+
"stroke-width",
|
|
23
|
+
"style",
|
|
24
|
+
"transform",
|
|
25
|
+
"visibility",
|
|
26
|
+
"xmlns",
|
|
27
|
+
]);
|
|
28
|
+
const ELEMENT_ATTRIBUTES = {
|
|
29
|
+
circle: new Set(["cx", "cy", "r"]),
|
|
30
|
+
ellipse: new Set(["cx", "cy", "rx", "ry"]),
|
|
31
|
+
g: new Set(),
|
|
32
|
+
path: new Set(["d"]),
|
|
33
|
+
polygon: new Set(["points"]),
|
|
34
|
+
rect: new Set(["height", "rx", "ry", "width", "x", "y"]),
|
|
35
|
+
svg: new Set(["height", "preserveAspectRatio", "version", "viewBox", "width", "x", "y"]),
|
|
36
|
+
};
|
|
37
|
+
const STYLE_PROPERTIES = new Set([
|
|
38
|
+
"display",
|
|
39
|
+
"fill",
|
|
40
|
+
"fill-opacity",
|
|
41
|
+
"fill-rule",
|
|
42
|
+
"opacity",
|
|
43
|
+
"stroke",
|
|
44
|
+
"stroke-opacity",
|
|
45
|
+
"stroke-width",
|
|
46
|
+
"visibility",
|
|
47
|
+
]);
|
|
48
|
+
function utf8ByteLength(value) {
|
|
49
|
+
let bytes = 0;
|
|
50
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
51
|
+
const code = value.charCodeAt(index);
|
|
52
|
+
if (code <= 0x7f)
|
|
53
|
+
bytes += 1;
|
|
54
|
+
else if (code <= 0x7ff)
|
|
55
|
+
bytes += 2;
|
|
56
|
+
else if (code >= 0xd800 && code <= 0xdbff) {
|
|
57
|
+
const next = value.charCodeAt(index + 1);
|
|
58
|
+
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
59
|
+
bytes += 4;
|
|
60
|
+
index += 1;
|
|
61
|
+
}
|
|
62
|
+
else
|
|
63
|
+
bytes += 3;
|
|
64
|
+
}
|
|
65
|
+
else
|
|
66
|
+
bytes += 3;
|
|
67
|
+
}
|
|
68
|
+
return bytes;
|
|
69
|
+
}
|
|
70
|
+
function validateInputSize(value, operation) {
|
|
71
|
+
const bytes = utf8ByteLength(value);
|
|
72
|
+
if (bytes > MAX_SVG_BYTES) {
|
|
73
|
+
complexity(operation, `SVG input exceeds ${MAX_SVG_BYTES} UTF-8 bytes.`, {
|
|
74
|
+
actualBytes: bytes,
|
|
75
|
+
maxBytes: MAX_SVG_BYTES,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function parseOptions(options, operation) {
|
|
80
|
+
const origin = options.origin ?? "center";
|
|
81
|
+
if (origin !== "center" && origin !== "document") {
|
|
82
|
+
invalid(operation, 'origin must be either "center" or "document".');
|
|
83
|
+
}
|
|
84
|
+
return { origin, resolution: options.resolution ?? {} };
|
|
85
|
+
}
|
|
86
|
+
function parseLength(value, operation) {
|
|
87
|
+
const match = /^\s*([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?)\s*([A-Za-z%]*)\s*$/.exec(value);
|
|
88
|
+
if (match === null)
|
|
89
|
+
invalid(operation, `Invalid SVG length "${value}".`);
|
|
90
|
+
const scalar = Number(match[1]);
|
|
91
|
+
if (!Number.isFinite(scalar))
|
|
92
|
+
invalid(operation, "SVG lengths must be finite.");
|
|
93
|
+
switch ((match[2] ?? "").toLowerCase()) {
|
|
94
|
+
case "":
|
|
95
|
+
case "px":
|
|
96
|
+
return scalar;
|
|
97
|
+
case "mm":
|
|
98
|
+
return scalar / MM_PER_PX;
|
|
99
|
+
case "cm":
|
|
100
|
+
return (scalar * 10) / MM_PER_PX;
|
|
101
|
+
case "q":
|
|
102
|
+
return (scalar * 0.25) / MM_PER_PX;
|
|
103
|
+
case "in":
|
|
104
|
+
return scalar * 96;
|
|
105
|
+
case "pt":
|
|
106
|
+
return scalar * (96 / 72);
|
|
107
|
+
case "pc":
|
|
108
|
+
return scalar * 16;
|
|
109
|
+
case "%":
|
|
110
|
+
return unsupported(operation, "Percentage SVG lengths are not supported.");
|
|
111
|
+
default:
|
|
112
|
+
return unsupported(operation, `Unsupported SVG length unit "${match[2]}".`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function parseRootPhysicalLength(value, fallbackPx, operation) {
|
|
116
|
+
return parseLength(value ?? String(fallbackPx), operation) * MM_PER_PX;
|
|
117
|
+
}
|
|
118
|
+
function parseViewBox(value, operation) {
|
|
119
|
+
if (value === undefined)
|
|
120
|
+
return null;
|
|
121
|
+
const numbers = parseNumberSequence(value, operation);
|
|
122
|
+
if (numbers.length !== 4)
|
|
123
|
+
invalid(operation, "viewBox must contain four numbers.");
|
|
124
|
+
const [minX = 0, minY = 0, width = 0, height = 0] = numbers;
|
|
125
|
+
if (width <= 0 || height <= 0)
|
|
126
|
+
invalid(operation, "viewBox width and height must be positive.");
|
|
127
|
+
return { height, minX, minY, width };
|
|
128
|
+
}
|
|
129
|
+
function viewBoxMatrix(viewBox, viewportWidth, viewportHeight, preserveAspectRatio, operation) {
|
|
130
|
+
const setting = preserveAspectRatio?.trim() || "xMidYMid meet";
|
|
131
|
+
if (setting === "none") {
|
|
132
|
+
const sx = viewportWidth / viewBox.width;
|
|
133
|
+
const sy = viewportHeight / viewBox.height;
|
|
134
|
+
return [sx, 0, 0, sy, -viewBox.minX * sx, -viewBox.minY * sy];
|
|
135
|
+
}
|
|
136
|
+
const match = /^(x(?:Min|Mid|Max)Y(?:Min|Mid|Max))(?:\s+(meet|slice))?$/.exec(setting);
|
|
137
|
+
if (match === null)
|
|
138
|
+
invalid(operation, `Invalid preserveAspectRatio value "${setting}".`);
|
|
139
|
+
const scaleX = viewportWidth / viewBox.width;
|
|
140
|
+
const scaleY = viewportHeight / viewBox.height;
|
|
141
|
+
if (match[2] === "slice") {
|
|
142
|
+
unsupported(operation, 'preserveAspectRatio="slice" requires viewport clipping, which is outside the fill-only SVG subset.');
|
|
143
|
+
}
|
|
144
|
+
const scale = Math.min(scaleX, scaleY);
|
|
145
|
+
const spareX = viewportWidth - viewBox.width * scale;
|
|
146
|
+
const spareY = viewportHeight - viewBox.height * scale;
|
|
147
|
+
const align = match[1] ?? "xMidYMid";
|
|
148
|
+
const offsetX = align.startsWith("xMin") ? 0 : align.startsWith("xMid") ? spareX / 2 : spareX;
|
|
149
|
+
const offsetY = align.endsWith("YMin") ? 0 : align.endsWith("YMid") ? spareY / 2 : spareY;
|
|
150
|
+
return [scale, 0, 0, scale, offsetX - viewBox.minX * scale, offsetY - viewBox.minY * scale];
|
|
151
|
+
}
|
|
152
|
+
function rootViewport(attributes, operation) {
|
|
153
|
+
const width = parseRootPhysicalLength(attributes.width, 300, operation);
|
|
154
|
+
const height = parseRootPhysicalLength(attributes.height, 150, operation);
|
|
155
|
+
if (width <= 0 || height <= 0)
|
|
156
|
+
invalid(operation, "SVG viewport dimensions must be positive.");
|
|
157
|
+
const viewBox = parseViewBox(attributes.viewBox, operation);
|
|
158
|
+
const downMatrix = viewBox === null
|
|
159
|
+
? [MM_PER_PX, 0, 0, MM_PER_PX, 0, 0]
|
|
160
|
+
: viewBoxMatrix(viewBox, width, height, attributes.preserveAspectRatio, operation);
|
|
161
|
+
const yUp = [1, 0, 0, -1, 0, height];
|
|
162
|
+
return {
|
|
163
|
+
matrix: multiplyMatrices(yUp, downMatrix),
|
|
164
|
+
viewport: { height, width },
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function nestedSvgMatrix(attributes, operation) {
|
|
168
|
+
const x = parseLength(attributes.x ?? "0", operation);
|
|
169
|
+
const y = parseLength(attributes.y ?? "0", operation);
|
|
170
|
+
const viewBox = parseViewBox(attributes.viewBox, operation);
|
|
171
|
+
if (viewBox === null)
|
|
172
|
+
return [1, 0, 0, 1, x, y];
|
|
173
|
+
const width = parseLength(attributes.width ?? String(viewBox.width), operation);
|
|
174
|
+
const height = parseLength(attributes.height ?? String(viewBox.height), operation);
|
|
175
|
+
if (width <= 0 || height <= 0)
|
|
176
|
+
invalid(operation, "Nested SVG dimensions must be positive.");
|
|
177
|
+
return multiplyMatrices([1, 0, 0, 1, x, y], viewBoxMatrix(viewBox, width, height, attributes.preserveAspectRatio, operation));
|
|
178
|
+
}
|
|
179
|
+
function attributeMap(tag, operation) {
|
|
180
|
+
const attributes = {};
|
|
181
|
+
const allowed = ELEMENT_ATTRIBUTES[tag.local] ?? new Set();
|
|
182
|
+
for (const attribute of Object.values(tag.attributes)) {
|
|
183
|
+
if (attribute.uri !== "" && attribute.uri !== XMLNS_NAMESPACE) {
|
|
184
|
+
unsupported(operation, `Namespaced attribute "${attribute.name}" is not supported.`);
|
|
185
|
+
}
|
|
186
|
+
const name = attribute.local === "" ? attribute.name : attribute.local;
|
|
187
|
+
if (/^on/i.test(name))
|
|
188
|
+
unsupported(operation, "SVG event handler attributes are not supported.");
|
|
189
|
+
if (name === "class")
|
|
190
|
+
unsupported(operation, "Class-based SVG styling is not supported.");
|
|
191
|
+
if (!COMMON_ATTRIBUTES.has(name) &&
|
|
192
|
+
!allowed.has(name) &&
|
|
193
|
+
!name.startsWith("data-") &&
|
|
194
|
+
!name.startsWith("aria-") &&
|
|
195
|
+
name !== "role") {
|
|
196
|
+
unsupported(operation, `Attribute "${name}" is not supported on <${tag.local}>.`);
|
|
197
|
+
}
|
|
198
|
+
attributes[name] = attribute.value;
|
|
199
|
+
}
|
|
200
|
+
return attributes;
|
|
201
|
+
}
|
|
202
|
+
function parseInlineStyle(value, operation) {
|
|
203
|
+
if (value === undefined || value.trim() === "")
|
|
204
|
+
return {};
|
|
205
|
+
const properties = {};
|
|
206
|
+
for (const declaration of value.split(";")) {
|
|
207
|
+
if (declaration.trim() === "")
|
|
208
|
+
continue;
|
|
209
|
+
const separator = declaration.indexOf(":");
|
|
210
|
+
if (separator <= 0)
|
|
211
|
+
invalid(operation, "Malformed inline SVG style declaration.");
|
|
212
|
+
const property = declaration.slice(0, separator).trim().toLowerCase();
|
|
213
|
+
const propertyValue = declaration.slice(separator + 1).trim();
|
|
214
|
+
if (!STYLE_PROPERTIES.has(property) ||
|
|
215
|
+
propertyValue === "" ||
|
|
216
|
+
/!\s*important/i.test(propertyValue)) {
|
|
217
|
+
unsupported(operation, `Inline SVG style property "${property}" is not supported.`);
|
|
218
|
+
}
|
|
219
|
+
properties[property] = propertyValue;
|
|
220
|
+
}
|
|
221
|
+
return properties;
|
|
222
|
+
}
|
|
223
|
+
function parseOpacity(value, operation) {
|
|
224
|
+
const percentage = value.trim().endsWith("%");
|
|
225
|
+
const scalar = Number(percentage ? value.trim().slice(0, -1) : value);
|
|
226
|
+
if (!Number.isFinite(scalar))
|
|
227
|
+
invalid(operation, `Invalid SVG opacity "${value}".`);
|
|
228
|
+
const normalized = percentage ? scalar / 100 : scalar;
|
|
229
|
+
return Math.min(1, Math.max(0, normalized));
|
|
230
|
+
}
|
|
231
|
+
function property(attributes, styles, name) {
|
|
232
|
+
return styles[name] ?? attributes[name];
|
|
233
|
+
}
|
|
234
|
+
function resolvePaint(parent, attributes, operation) {
|
|
235
|
+
const styles = parseInlineStyle(attributes.style, operation);
|
|
236
|
+
const inherited = (name, fallback) => {
|
|
237
|
+
const value = property(attributes, styles, name);
|
|
238
|
+
return value === undefined || value.trim() === "inherit" ? fallback : value.trim();
|
|
239
|
+
};
|
|
240
|
+
const displayValue = property(attributes, styles, "display")?.trim() ?? "inline";
|
|
241
|
+
if (displayValue !== "none" && displayValue !== "inline" && displayValue !== "block") {
|
|
242
|
+
unsupported(operation, `display="${displayValue}" is not supported.`);
|
|
243
|
+
}
|
|
244
|
+
const visibilityValue = inherited("visibility", parent.visibility);
|
|
245
|
+
if (visibilityValue !== "visible" &&
|
|
246
|
+
visibilityValue !== "hidden" &&
|
|
247
|
+
visibilityValue !== "collapse") {
|
|
248
|
+
unsupported(operation, `visibility="${visibilityValue}" is not supported.`);
|
|
249
|
+
}
|
|
250
|
+
const fillRuleValue = inherited("fill-rule", parent.fillRule);
|
|
251
|
+
if (fillRuleValue !== "nonzero" && fillRuleValue !== "evenodd") {
|
|
252
|
+
invalid(operation, `Invalid fill-rule "${fillRuleValue}".`);
|
|
253
|
+
}
|
|
254
|
+
const fill = inherited("fill", parent.fill);
|
|
255
|
+
const stroke = inherited("stroke", parent.stroke);
|
|
256
|
+
if (/url\s*\(|^(?:context-fill|context-stroke|var\s*\()/i.test(fill)) {
|
|
257
|
+
unsupported(operation, "Referenced or context-dependent SVG fill paint is not supported.");
|
|
258
|
+
}
|
|
259
|
+
if (/url\s*\(|^(?:context-fill|context-stroke|var\s*\()/i.test(stroke)) {
|
|
260
|
+
unsupported(operation, "Referenced or context-dependent SVG stroke paint is not supported.");
|
|
261
|
+
}
|
|
262
|
+
const ownOpacity = property(attributes, styles, "opacity");
|
|
263
|
+
const fillOpacity = inherited("fill-opacity", String(parent.fillOpacity));
|
|
264
|
+
const strokeOpacity = inherited("stroke-opacity", String(parent.strokeOpacity));
|
|
265
|
+
const strokeWidth = inherited("stroke-width", String(parent.strokeWidth));
|
|
266
|
+
return {
|
|
267
|
+
display: parent.display && displayValue !== "none",
|
|
268
|
+
fill,
|
|
269
|
+
fillOpacity: parseOpacity(fillOpacity, operation),
|
|
270
|
+
fillRule: fillRuleValue,
|
|
271
|
+
opacity: parent.opacity * (ownOpacity === undefined ? 1 : parseOpacity(ownOpacity, operation)),
|
|
272
|
+
stroke,
|
|
273
|
+
strokeOpacity: parseOpacity(strokeOpacity, operation),
|
|
274
|
+
strokeWidth: parseLength(strokeWidth, operation),
|
|
275
|
+
visibility: visibilityValue === "visible" ? "visible" : "hidden",
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
function rendered(paint) {
|
|
279
|
+
return paint.display && paint.visibility === "visible" && paint.opacity > 0;
|
|
280
|
+
}
|
|
281
|
+
function fillVisible(paint) {
|
|
282
|
+
const fill = paint.fill.trim().toLowerCase();
|
|
283
|
+
return rendered(paint) && paint.fillOpacity > 0 && fill !== "none" && fill !== "transparent";
|
|
284
|
+
}
|
|
285
|
+
function assertNoVisibleStroke(paint, operation) {
|
|
286
|
+
if (rendered(paint) &&
|
|
287
|
+
paint.strokeOpacity > 0 &&
|
|
288
|
+
paint.strokeWidth > 0 &&
|
|
289
|
+
!["none", "transparent"].includes(paint.stroke.trim().toLowerCase())) {
|
|
290
|
+
unsupported(operation, "Visible SVG strokes are not supported; expand the stroke to a fill first.");
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function requireAttribute(attributes, name, operation) {
|
|
294
|
+
const value = attributes[name];
|
|
295
|
+
if (value === undefined || value.trim() === "") {
|
|
296
|
+
invalid(operation, `<${operation}> requires a non-empty ${name} attribute.`);
|
|
297
|
+
}
|
|
298
|
+
return value;
|
|
299
|
+
}
|
|
300
|
+
function pathForShape(name, attributes, operation) {
|
|
301
|
+
switch (name) {
|
|
302
|
+
case "path":
|
|
303
|
+
return requireAttribute(attributes, "d", operation);
|
|
304
|
+
case "polygon": {
|
|
305
|
+
const points = parseNumberSequence(requireAttribute(attributes, "points", operation), operation);
|
|
306
|
+
if (points.length % 2 !== 0)
|
|
307
|
+
invalid(operation, "Polygon points must contain x/y pairs.");
|
|
308
|
+
if (points.length < 6)
|
|
309
|
+
return null;
|
|
310
|
+
const commands = [`M ${points[0]} ${points[1]}`];
|
|
311
|
+
for (let index = 2; index < points.length; index += 2) {
|
|
312
|
+
commands.push(`L ${points[index]} ${points[index + 1]}`);
|
|
313
|
+
}
|
|
314
|
+
commands.push("Z");
|
|
315
|
+
return commands.join(" ");
|
|
316
|
+
}
|
|
317
|
+
case "circle": {
|
|
318
|
+
const cx = parseLength(attributes.cx ?? "0", operation);
|
|
319
|
+
const cy = parseLength(attributes.cy ?? "0", operation);
|
|
320
|
+
const radius = parseLength(requireAttribute(attributes, "r", operation), operation);
|
|
321
|
+
if (radius < 0)
|
|
322
|
+
invalid(operation, "Circle radius cannot be negative.");
|
|
323
|
+
if (radius === 0)
|
|
324
|
+
return null;
|
|
325
|
+
return `M ${cx - radius} ${cy} A ${radius} ${radius} 0 1 0 ${cx + radius} ${cy} A ${radius} ${radius} 0 1 0 ${cx - radius} ${cy} Z`;
|
|
326
|
+
}
|
|
327
|
+
case "ellipse": {
|
|
328
|
+
const cx = parseLength(attributes.cx ?? "0", operation);
|
|
329
|
+
const cy = parseLength(attributes.cy ?? "0", operation);
|
|
330
|
+
const rx = parseLength(requireAttribute(attributes, "rx", operation), operation);
|
|
331
|
+
const ry = parseLength(requireAttribute(attributes, "ry", operation), operation);
|
|
332
|
+
if (rx < 0 || ry < 0)
|
|
333
|
+
invalid(operation, "Ellipse radii cannot be negative.");
|
|
334
|
+
if (rx === 0 || ry === 0)
|
|
335
|
+
return null;
|
|
336
|
+
return `M ${cx - rx} ${cy} A ${rx} ${ry} 0 1 0 ${cx + rx} ${cy} A ${rx} ${ry} 0 1 0 ${cx - rx} ${cy} Z`;
|
|
337
|
+
}
|
|
338
|
+
case "rect": {
|
|
339
|
+
const x = parseLength(attributes.x ?? "0", operation);
|
|
340
|
+
const y = parseLength(attributes.y ?? "0", operation);
|
|
341
|
+
const width = parseLength(requireAttribute(attributes, "width", operation), operation);
|
|
342
|
+
const height = parseLength(requireAttribute(attributes, "height", operation), operation);
|
|
343
|
+
if (width < 0 || height < 0)
|
|
344
|
+
invalid(operation, "Rectangle dimensions cannot be negative.");
|
|
345
|
+
if (width === 0 || height === 0)
|
|
346
|
+
return null;
|
|
347
|
+
const rawRx = attributes.rx === undefined ? undefined : parseLength(attributes.rx, operation);
|
|
348
|
+
const rawRy = attributes.ry === undefined ? undefined : parseLength(attributes.ry, operation);
|
|
349
|
+
if ((rawRx ?? 0) < 0 || (rawRy ?? 0) < 0)
|
|
350
|
+
invalid(operation, "Rectangle radii cannot be negative.");
|
|
351
|
+
const rx = Math.min(rawRx ?? rawRy ?? 0, width / 2);
|
|
352
|
+
const ry = Math.min(rawRy ?? rawRx ?? 0, height / 2);
|
|
353
|
+
if (rx === 0 || ry === 0) {
|
|
354
|
+
return `M ${x} ${y} H ${x + width} V ${y + height} H ${x} Z`;
|
|
355
|
+
}
|
|
356
|
+
return [
|
|
357
|
+
`M ${x + rx} ${y}`,
|
|
358
|
+
`H ${x + width - rx}`,
|
|
359
|
+
`A ${rx} ${ry} 0 0 1 ${x + width} ${y + ry}`,
|
|
360
|
+
`V ${y + height - ry}`,
|
|
361
|
+
`A ${rx} ${ry} 0 0 1 ${x + width - rx} ${y + height}`,
|
|
362
|
+
`H ${x + rx}`,
|
|
363
|
+
`A ${rx} ${ry} 0 0 1 ${x} ${y + height - ry}`,
|
|
364
|
+
`V ${y + ry}`,
|
|
365
|
+
`A ${rx} ${ry} 0 0 1 ${x + rx} ${y}`,
|
|
366
|
+
"Z",
|
|
367
|
+
].join(" ");
|
|
368
|
+
}
|
|
369
|
+
default:
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function geometryBounds(groups, operation) {
|
|
374
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
375
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
376
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
377
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
378
|
+
for (const group of groups) {
|
|
379
|
+
for (const contour of group.contours) {
|
|
380
|
+
for (const point of contour) {
|
|
381
|
+
minX = Math.min(minX, point[0]);
|
|
382
|
+
minY = Math.min(minY, point[1]);
|
|
383
|
+
maxX = Math.max(maxX, point[0]);
|
|
384
|
+
maxY = Math.max(maxY, point[1]);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (!Number.isFinite(minX))
|
|
389
|
+
emptyGeometry(operation, "SVG contains no filled geometry.");
|
|
390
|
+
return { max: [maxX, maxY], min: [minX, minY] };
|
|
391
|
+
}
|
|
392
|
+
function freezeGeometry(groups, viewport, origin, operation) {
|
|
393
|
+
const frozenGroups = groups.map((group) => Object.freeze({
|
|
394
|
+
contours: Object.freeze(group.contours.map((contour) => Object.freeze(contour.map((point) => Object.freeze([point[0], point[1]]))))),
|
|
395
|
+
fillRule: group.fillRule,
|
|
396
|
+
}));
|
|
397
|
+
const bounds = geometryBounds(frozenGroups, operation);
|
|
398
|
+
const translation = [0, 0];
|
|
399
|
+
return Object.freeze({
|
|
400
|
+
bounds: Object.freeze({
|
|
401
|
+
max: Object.freeze(bounds.max),
|
|
402
|
+
min: Object.freeze(bounds.min),
|
|
403
|
+
}),
|
|
404
|
+
groups: Object.freeze(frozenGroups),
|
|
405
|
+
placement: Object.freeze({ origin, translation: Object.freeze(translation) }),
|
|
406
|
+
viewport: viewport === null ? null : Object.freeze({ ...viewport }),
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Parses one SVG path-data string into deterministic millimetre, Y-up fill contours.
|
|
411
|
+
* Centering is deferred until the adapter has resolved and unioned all fills.
|
|
412
|
+
*/
|
|
413
|
+
export function parseSvgPath(pathData, options = {}) {
|
|
414
|
+
const operation = "svgPathSection";
|
|
415
|
+
validateInputSize(pathData, operation);
|
|
416
|
+
const { origin, resolution } = parseOptions(options, operation);
|
|
417
|
+
const fillRule = options.fillRule ?? "nonzero";
|
|
418
|
+
if (fillRule !== "nonzero" && fillRule !== "evenodd") {
|
|
419
|
+
invalid(operation, 'fillRule must be either "nonzero" or "evenodd".');
|
|
420
|
+
}
|
|
421
|
+
const scale = options.scale ?? 1;
|
|
422
|
+
if (!Number.isFinite(scale) || scale <= 0)
|
|
423
|
+
invalid(operation, "scale must be finite and positive.");
|
|
424
|
+
const budget = new SvgVertexBudget(options.limits);
|
|
425
|
+
const contours = flattenSvgPath(pathData, [scale, 0, 0, -scale, 0, 0], resolution, budget, operation);
|
|
426
|
+
if (contours.length === 0)
|
|
427
|
+
emptyGeometry(operation, "SVG path contains no filled geometry.");
|
|
428
|
+
return freezeGeometry([{ contours, fillRule }], null, origin, operation);
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Parses the supported, fill-only SVG document subset without DOM or browser dependencies.
|
|
432
|
+
* Centering is deferred until the adapter has resolved and unioned all fills.
|
|
433
|
+
*/
|
|
434
|
+
export function parseSvgDocument(svg, options = {}) {
|
|
435
|
+
const operation = "svgSection";
|
|
436
|
+
validateInputSize(svg, operation);
|
|
437
|
+
if (/&(?:#\d+|#x[\da-f]+|[A-Za-z_:][\w:.-]*);/i.test(svg)) {
|
|
438
|
+
unsupported(operation, "SVG entity references are not supported.");
|
|
439
|
+
}
|
|
440
|
+
const { origin, resolution } = parseOptions(options, operation);
|
|
441
|
+
const groups = [];
|
|
442
|
+
const frames = [];
|
|
443
|
+
const budget = new SvgVertexBudget(options.limits);
|
|
444
|
+
let viewport = null;
|
|
445
|
+
let renderedShapes = 0;
|
|
446
|
+
let sawRoot = false;
|
|
447
|
+
const parser = new SaxesParser({ xmlns: true });
|
|
448
|
+
parser.on("doctype", () => unsupported(operation, "SVG doctypes are not supported."));
|
|
449
|
+
parser.on("processinginstruction", () => unsupported(operation, "SVG processing instructions are not supported."));
|
|
450
|
+
parser.on("cdata", () => unsupported(operation, "SVG CDATA sections are not supported."));
|
|
451
|
+
parser.on("text", (text) => {
|
|
452
|
+
if (/\S/.test(text))
|
|
453
|
+
unsupported(operation, "Text content is not supported in fill-only SVG.");
|
|
454
|
+
});
|
|
455
|
+
parser.on("opentag", (tag) => {
|
|
456
|
+
if (frames.length >= MAX_XML_DEPTH) {
|
|
457
|
+
complexity(operation, `SVG nesting exceeds ${MAX_XML_DEPTH} levels.`, {
|
|
458
|
+
maxDepth: MAX_XML_DEPTH,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
if (tag.uri !== "" && tag.uri !== SVG_NAMESPACE) {
|
|
462
|
+
unsupported(operation, `Element namespace "${tag.uri}" is not supported.`);
|
|
463
|
+
}
|
|
464
|
+
const name = tag.local;
|
|
465
|
+
if (!SUPPORTED_ELEMENTS.has(name)) {
|
|
466
|
+
unsupported(operation, `SVG element <${name}> is not supported.`);
|
|
467
|
+
}
|
|
468
|
+
if (!sawRoot && name !== "svg")
|
|
469
|
+
invalid(operation, "The document root must be an <svg> element.");
|
|
470
|
+
const attributes = attributeMap(tag, operation);
|
|
471
|
+
const parent = frames.at(-1);
|
|
472
|
+
const parentPaint = parent?.paint ?? {
|
|
473
|
+
display: true,
|
|
474
|
+
fill: "black",
|
|
475
|
+
fillOpacity: 1,
|
|
476
|
+
fillRule: "nonzero",
|
|
477
|
+
opacity: 1,
|
|
478
|
+
stroke: "none",
|
|
479
|
+
strokeOpacity: 1,
|
|
480
|
+
strokeWidth: 1,
|
|
481
|
+
visibility: "visible",
|
|
482
|
+
};
|
|
483
|
+
const paint = resolvePaint(parentPaint, attributes, operation);
|
|
484
|
+
const explicitTransform = parseTransform(attributes.transform, operation);
|
|
485
|
+
let matrix;
|
|
486
|
+
if (!sawRoot) {
|
|
487
|
+
const root = rootViewport(attributes, operation);
|
|
488
|
+
viewport = root.viewport;
|
|
489
|
+
matrix = multiplyMatrices(root.matrix, explicitTransform);
|
|
490
|
+
sawRoot = true;
|
|
491
|
+
}
|
|
492
|
+
else if (name === "svg") {
|
|
493
|
+
if (parent === undefined)
|
|
494
|
+
invalid(operation, "Nested SVG has no parent coordinate system.");
|
|
495
|
+
matrix = multiplyMatrices(parent.matrix, multiplyMatrices(explicitTransform, nestedSvgMatrix(attributes, operation)));
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
if (parent === undefined)
|
|
499
|
+
invalid(operation, "SVG element has no root coordinate system.");
|
|
500
|
+
matrix = multiplyMatrices(parent.matrix, explicitTransform);
|
|
501
|
+
}
|
|
502
|
+
const frame = { matrix, name, paint };
|
|
503
|
+
frames.push(frame);
|
|
504
|
+
if (name === "svg" || name === "g")
|
|
505
|
+
return;
|
|
506
|
+
assertNoVisibleStroke(paint, operation);
|
|
507
|
+
if (!fillVisible(paint))
|
|
508
|
+
return;
|
|
509
|
+
renderedShapes += 1;
|
|
510
|
+
if (renderedShapes > MAX_RENDERED_SHAPES) {
|
|
511
|
+
complexity(operation, `SVG exceeds ${MAX_RENDERED_SHAPES} rendered shapes.`, {
|
|
512
|
+
maxShapes: MAX_RENDERED_SHAPES,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
const pathData = pathForShape(name, attributes, operation);
|
|
516
|
+
if (pathData === null)
|
|
517
|
+
return;
|
|
518
|
+
const contours = flattenSvgPath(pathData, matrix, resolution, budget, operation);
|
|
519
|
+
if (contours.length > 0)
|
|
520
|
+
groups.push({ contours, fillRule: paint.fillRule });
|
|
521
|
+
});
|
|
522
|
+
parser.on("closetag", () => {
|
|
523
|
+
frames.pop();
|
|
524
|
+
});
|
|
525
|
+
try {
|
|
526
|
+
parser.write(svg).close();
|
|
527
|
+
}
|
|
528
|
+
catch (error) {
|
|
529
|
+
rethrowAsInvalid(operation, error);
|
|
530
|
+
}
|
|
531
|
+
if (!sawRoot)
|
|
532
|
+
invalid(operation, "SVG document is empty.");
|
|
533
|
+
if (frames.length !== 0)
|
|
534
|
+
invalid(operation, "SVG document has unclosed elements.");
|
|
535
|
+
return freezeGeometry(groups, viewport, origin, operation);
|
|
536
|
+
}
|