@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.
- package/CHANGELOG.md +105 -0
- package/README.md +313 -6
- package/THIRD_PARTY_NOTICES.md +153 -0
- package/dist/api/engine.d.ts +54 -0
- package/dist/api/geometry.d.ts +2912 -0
- package/dist/api/index.d.ts +1648 -0
- package/dist/api/text.d.ts +77 -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 +72 -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 +265 -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 +107 -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 +80 -0
- package/dist/geometry/math/vector3.js +41 -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 +368 -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-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 +300 -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 +30 -0
- package/dist/geometry/paths/planar.js +135 -0
- package/dist/geometry/paths/resampling.js +124 -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 +184 -0
- package/dist/geometry/profiles/open-profile.js +137 -0
- package/dist/geometry/profiles/profiles.js +70 -0
- package/dist/geometry/profiles/rectangle.js +235 -0
- package/dist/geometry/profiles/shapes.js +34 -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 +123 -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 +20 -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 +152 -0
- package/dist/model/options.js +1 -0
- package/dist/model/types.js +1 -0
- package/dist/text/index.js +48 -0
- package/dist/text/vector-text-layout.js +279 -0
- package/package.json +89 -8
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
import { SaxesParser } from "saxes";
|
|
2
|
+
import { GeometryError } from "./errors.js";
|
|
3
|
+
import { regionArea, signedAreaLoop2 } from "./regions.js";
|
|
4
|
+
import { resolveCircularSegments, resolveGeometryResolution, } from "./resolution.js";
|
|
5
|
+
import { resolveGeometrySamplingLimits } from "./sampling-limits.js";
|
|
6
|
+
import { strokePathMultiPolygon } from "./stroke-path.js";
|
|
7
|
+
import { createBoundedSvgClipping } from "./svg/fill.js";
|
|
8
|
+
import { flattenSvgSubpaths, SvgVertexBudget } from "./svg/path.js";
|
|
9
|
+
const MAX_SVG_BYTES = 256 * 1024;
|
|
10
|
+
const MAX_SVG_ELEMENTS = 500;
|
|
11
|
+
const MAX_SVG_NESTING_DEPTH = 64;
|
|
12
|
+
const MAX_SVG_NODES = 2_000;
|
|
13
|
+
const SHORT_EDGE_LENGTH_MM = 0.01;
|
|
14
|
+
const TINY_ISLAND_AREA_MM2 = 0.01;
|
|
15
|
+
const SUPPORTED_TAGS = new Set([
|
|
16
|
+
"svg",
|
|
17
|
+
"g",
|
|
18
|
+
"rect",
|
|
19
|
+
"circle",
|
|
20
|
+
"ellipse",
|
|
21
|
+
"line",
|
|
22
|
+
"polyline",
|
|
23
|
+
"polygon",
|
|
24
|
+
"path",
|
|
25
|
+
"metadata",
|
|
26
|
+
]);
|
|
27
|
+
const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
|
|
28
|
+
const NUMBER_PATTERN = /^[-+]?(?:\d*\.\d+|\d+\.?)(?:e[-+]?\d+)?$/i;
|
|
29
|
+
const SAFE_COLOR_PATTERN = /^(?:none|#[0-9a-f]{3,8}|(?:rgb|hsl)a?\([\d\s.,%+-]+\)|[a-z]+)$/i;
|
|
30
|
+
const PRESENTATION_ATTRIBUTES = [
|
|
31
|
+
"fill",
|
|
32
|
+
"fill-opacity",
|
|
33
|
+
"fill-rule",
|
|
34
|
+
"stroke",
|
|
35
|
+
"stroke-linecap",
|
|
36
|
+
"stroke-linejoin",
|
|
37
|
+
"stroke-opacity",
|
|
38
|
+
"stroke-width",
|
|
39
|
+
"style",
|
|
40
|
+
];
|
|
41
|
+
const COMMON_ATTRIBUTES = new Set(["id", "name", "transform", ...PRESENTATION_ATTRIBUTES]);
|
|
42
|
+
const ATTRIBUTES_BY_TAG = {
|
|
43
|
+
svg: new Set([
|
|
44
|
+
...COMMON_ATTRIBUTES,
|
|
45
|
+
"height",
|
|
46
|
+
"preserveAspectRatio",
|
|
47
|
+
"version",
|
|
48
|
+
"viewBox",
|
|
49
|
+
"width",
|
|
50
|
+
"xmlns",
|
|
51
|
+
]),
|
|
52
|
+
g: COMMON_ATTRIBUTES,
|
|
53
|
+
rect: new Set([...COMMON_ATTRIBUTES, "height", "rx", "ry", "width", "x", "y"]),
|
|
54
|
+
circle: new Set([...COMMON_ATTRIBUTES, "cx", "cy", "r"]),
|
|
55
|
+
ellipse: new Set([...COMMON_ATTRIBUTES, "cx", "cy", "rx", "ry"]),
|
|
56
|
+
line: new Set([...COMMON_ATTRIBUTES, "x1", "x2", "y1", "y2"]),
|
|
57
|
+
polyline: new Set([...COMMON_ATTRIBUTES, "points"]),
|
|
58
|
+
polygon: new Set([...COMMON_ATTRIBUTES, "points"]),
|
|
59
|
+
path: new Set([...COMMON_ATTRIBUTES, "d", "data-metamesh-text-id"]),
|
|
60
|
+
metadata: new Set(["id"]),
|
|
61
|
+
};
|
|
62
|
+
const DEFAULT_PAINT = {
|
|
63
|
+
fill: "black",
|
|
64
|
+
fillOpacity: 1,
|
|
65
|
+
fillRule: "nonzero",
|
|
66
|
+
stroke: "none",
|
|
67
|
+
strokeLinecap: "butt",
|
|
68
|
+
strokeLinejoin: "miter",
|
|
69
|
+
strokeOpacity: 1,
|
|
70
|
+
strokeWidth: 1,
|
|
71
|
+
};
|
|
72
|
+
function fail(message) {
|
|
73
|
+
throw new GeometryError("INVALID_ARGUMENT", message, { operation: "preflightSvgRegion" });
|
|
74
|
+
}
|
|
75
|
+
function complexityFail(message, details) {
|
|
76
|
+
throw new GeometryError("COMPLEXITY_LIMIT", message, {
|
|
77
|
+
operation: "preflightSvgRegion",
|
|
78
|
+
details,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
function finite(value, fallback) {
|
|
82
|
+
if (value === undefined || value.trim() === "") {
|
|
83
|
+
if (fallback !== undefined)
|
|
84
|
+
return fallback;
|
|
85
|
+
return fail("A required SVG number is missing.");
|
|
86
|
+
}
|
|
87
|
+
if (!NUMBER_PATTERN.test(value.trim()))
|
|
88
|
+
return fail(`Invalid SVG number “${value}”.`);
|
|
89
|
+
const parsed = Number(value);
|
|
90
|
+
return Number.isFinite(parsed) ? parsed : fail(`Invalid SVG number “${value}”.`);
|
|
91
|
+
}
|
|
92
|
+
function multiply(left, right) {
|
|
93
|
+
return [
|
|
94
|
+
left[0] * right[0] + left[2] * right[1],
|
|
95
|
+
left[1] * right[0] + left[3] * right[1],
|
|
96
|
+
left[0] * right[2] + left[2] * right[3],
|
|
97
|
+
left[1] * right[2] + left[3] * right[3],
|
|
98
|
+
left[0] * right[4] + left[2] * right[5] + left[4],
|
|
99
|
+
left[1] * right[4] + left[3] * right[5] + left[5],
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
function transformPoint(matrix, point) {
|
|
103
|
+
return [
|
|
104
|
+
matrix[0] * point[0] + matrix[2] * point[1] + matrix[4],
|
|
105
|
+
matrix[1] * point[0] + matrix[3] * point[1] + matrix[5],
|
|
106
|
+
];
|
|
107
|
+
}
|
|
108
|
+
function parseTransform(value) {
|
|
109
|
+
if (value === undefined || value.trim() === "")
|
|
110
|
+
return [1, 0, 0, 1, 0, 0];
|
|
111
|
+
const pattern = /([a-zA-Z]+)\s*\(([^)]*)\)/g;
|
|
112
|
+
let result = [1, 0, 0, 1, 0, 0];
|
|
113
|
+
let matched = "";
|
|
114
|
+
let match;
|
|
115
|
+
while ((match = pattern.exec(value)) !== null) {
|
|
116
|
+
matched += match[0];
|
|
117
|
+
const name = match[1]?.toLowerCase();
|
|
118
|
+
const values = (match[2]?.match(/[-+]?(?:\d*\.\d+|\d+\.?)(?:e[-+]?\d+)?/gi) ?? []).map(Number);
|
|
119
|
+
let next;
|
|
120
|
+
if (name === "matrix" && values.length === 6) {
|
|
121
|
+
next = values;
|
|
122
|
+
}
|
|
123
|
+
else if (name === "translate" && values.length >= 1 && values.length <= 2) {
|
|
124
|
+
next = [1, 0, 0, 1, values[0] ?? 0, values[1] ?? 0];
|
|
125
|
+
}
|
|
126
|
+
else if (name === "scale" && values.length >= 1 && values.length <= 2) {
|
|
127
|
+
next = [values[0] ?? 1, 0, 0, values[1] ?? values[0] ?? 1, 0, 0];
|
|
128
|
+
}
|
|
129
|
+
else if (name === "rotate" && (values.length === 1 || values.length === 3)) {
|
|
130
|
+
const angle = ((values[0] ?? 0) * Math.PI) / 180;
|
|
131
|
+
const x = values[1] ?? 0;
|
|
132
|
+
const y = values[2] ?? 0;
|
|
133
|
+
const rotation = [
|
|
134
|
+
Math.cos(angle),
|
|
135
|
+
Math.sin(angle),
|
|
136
|
+
-Math.sin(angle),
|
|
137
|
+
Math.cos(angle),
|
|
138
|
+
0,
|
|
139
|
+
0,
|
|
140
|
+
];
|
|
141
|
+
next = multiply([1, 0, 0, 1, x, y], multiply(rotation, [1, 0, 0, 1, -x, -y]));
|
|
142
|
+
}
|
|
143
|
+
else if (name === "skewx" && values.length === 1) {
|
|
144
|
+
next = [1, 0, Math.tan(((values[0] ?? 0) * Math.PI) / 180), 1, 0, 0];
|
|
145
|
+
}
|
|
146
|
+
else if (name === "skewy" && values.length === 1) {
|
|
147
|
+
next = [1, Math.tan(((values[0] ?? 0) * Math.PI) / 180), 0, 1, 0, 0];
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
return fail(`Unsupported SVG transform “${name ?? ""}”.`);
|
|
151
|
+
}
|
|
152
|
+
result = multiply(result, next);
|
|
153
|
+
}
|
|
154
|
+
if (matched.replace(/[\s,]/g, "") !== value.replace(/[\s,]/g, "")) {
|
|
155
|
+
return fail("Invalid SVG transform syntax.");
|
|
156
|
+
}
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
function attributes(tag) {
|
|
160
|
+
const output = {};
|
|
161
|
+
for (const attribute of Object.values(tag.attributes)) {
|
|
162
|
+
const { name, value } = attribute;
|
|
163
|
+
if (name.toLowerCase().startsWith("on") || attribute.prefix !== "") {
|
|
164
|
+
return fail("Event handlers and custom namespaces are not supported in SVG geometry.");
|
|
165
|
+
}
|
|
166
|
+
if (/url\s*\(|javascript:/i.test(value))
|
|
167
|
+
return fail("External SVG references are not supported.");
|
|
168
|
+
output[name] = value;
|
|
169
|
+
}
|
|
170
|
+
return output;
|
|
171
|
+
}
|
|
172
|
+
function inlineStyle(value) {
|
|
173
|
+
if (value === undefined || value.trim() === "")
|
|
174
|
+
return {};
|
|
175
|
+
const output = {};
|
|
176
|
+
const allowed = new Set([
|
|
177
|
+
"fill",
|
|
178
|
+
"fill-opacity",
|
|
179
|
+
"fill-rule",
|
|
180
|
+
"stroke",
|
|
181
|
+
"stroke-linecap",
|
|
182
|
+
"stroke-linejoin",
|
|
183
|
+
"stroke-opacity",
|
|
184
|
+
"stroke-width",
|
|
185
|
+
]);
|
|
186
|
+
for (const declaration of value.split(";")) {
|
|
187
|
+
if (declaration.trim() === "")
|
|
188
|
+
continue;
|
|
189
|
+
const separator = declaration.indexOf(":");
|
|
190
|
+
const key = declaration.slice(0, separator).trim().toLowerCase();
|
|
191
|
+
if (separator <= 0 || !allowed.has(key))
|
|
192
|
+
return fail(`Unsupported SVG style “${key}”.`);
|
|
193
|
+
output[key] = declaration.slice(separator + 1).trim();
|
|
194
|
+
}
|
|
195
|
+
return output;
|
|
196
|
+
}
|
|
197
|
+
function validateAttributes(tag, values) {
|
|
198
|
+
const allowed = ATTRIBUTES_BY_TAG[tag];
|
|
199
|
+
if (allowed === undefined)
|
|
200
|
+
return fail(`Unsupported SVG element <${tag}>.`);
|
|
201
|
+
for (const name of Object.keys(values)) {
|
|
202
|
+
if (!allowed.has(name))
|
|
203
|
+
return fail(`Unsupported SVG attribute “${name}” on <${tag}>.`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function parsePaint(values, inherited) {
|
|
207
|
+
const style = inlineStyle(values.style);
|
|
208
|
+
// Inline declarations have CSS precedence over presentation attributes on the same element.
|
|
209
|
+
const value = (name) => style[name] ?? values[name];
|
|
210
|
+
const fillRule = value("fill-rule") ?? inherited.fillRule;
|
|
211
|
+
const strokeLinecap = value("stroke-linecap") ?? inherited.strokeLinecap;
|
|
212
|
+
const strokeLinejoin = value("stroke-linejoin") ?? inherited.strokeLinejoin;
|
|
213
|
+
if (fillRule !== "evenodd" && fillRule !== "nonzero")
|
|
214
|
+
return fail("Unsupported SVG fill rule.");
|
|
215
|
+
if (strokeLinecap !== "butt" && strokeLinecap !== "round" && strokeLinecap !== "square")
|
|
216
|
+
return fail("Unsupported SVG stroke cap.");
|
|
217
|
+
if (strokeLinejoin !== "bevel" && strokeLinejoin !== "miter" && strokeLinejoin !== "round")
|
|
218
|
+
return fail("Unsupported SVG stroke join.");
|
|
219
|
+
const fill = (value("fill") ?? inherited.fill).toLowerCase();
|
|
220
|
+
const stroke = (value("stroke") ?? inherited.stroke).toLowerCase();
|
|
221
|
+
if (!SAFE_COLOR_PATTERN.test(fill) || !SAFE_COLOR_PATTERN.test(stroke)) {
|
|
222
|
+
return fail("Unsupported SVG fill or stroke color.");
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
fill,
|
|
226
|
+
fillOpacity: Math.min(1, Math.max(0, finite(value("fill-opacity"), inherited.fillOpacity))),
|
|
227
|
+
fillRule,
|
|
228
|
+
stroke,
|
|
229
|
+
strokeLinecap,
|
|
230
|
+
strokeLinejoin,
|
|
231
|
+
strokeOpacity: Math.min(1, Math.max(0, finite(value("stroke-opacity"), inherited.strokeOpacity))),
|
|
232
|
+
strokeWidth: Math.max(0, finite(value("stroke-width"), inherited.strokeWidth)),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function lengthMm(value, fallback) {
|
|
236
|
+
if (value === undefined)
|
|
237
|
+
return fallback;
|
|
238
|
+
const match = /^([-+]?(?:\d*\.\d+|\d+\.?)(?:e[-+]?\d+)?)\s*(mm|cm|in|pt|pc|px)?$/i.exec(value.trim());
|
|
239
|
+
if (match === null)
|
|
240
|
+
return fail(`Unsupported SVG length “${value}”.`);
|
|
241
|
+
const factors = {
|
|
242
|
+
mm: 1,
|
|
243
|
+
cm: 10,
|
|
244
|
+
in: 25.4,
|
|
245
|
+
pt: 25.4 / 72,
|
|
246
|
+
pc: 25.4 / 6,
|
|
247
|
+
px: 25.4 / 96,
|
|
248
|
+
};
|
|
249
|
+
const factor = match[2] === undefined ? 1 : factors[match[2].toLowerCase()];
|
|
250
|
+
const number = Number(match[1]);
|
|
251
|
+
return factor !== undefined && Number.isFinite(number) && number > 0
|
|
252
|
+
? number * factor
|
|
253
|
+
: fail("SVG dimensions must be positive.");
|
|
254
|
+
}
|
|
255
|
+
function viewBox(values) {
|
|
256
|
+
const parsed = values.viewBox?.split(/[\s,]+/).map(Number);
|
|
257
|
+
if (parsed === undefined)
|
|
258
|
+
return [0, 0, finite(values.width, 100), finite(values.height, 100)];
|
|
259
|
+
if (parsed.length !== 4 ||
|
|
260
|
+
parsed.some((entry) => !Number.isFinite(entry)) ||
|
|
261
|
+
(parsed[2] ?? 0) <= 0 ||
|
|
262
|
+
(parsed[3] ?? 0) <= 0) {
|
|
263
|
+
return fail("SVG viewBox must contain four finite values and positive dimensions.");
|
|
264
|
+
}
|
|
265
|
+
return parsed;
|
|
266
|
+
}
|
|
267
|
+
function canvasTransform(values, width, height, box, origin) {
|
|
268
|
+
const [x, y, viewWidth, viewHeight] = box;
|
|
269
|
+
const preserveAspectRatio = values.preserveAspectRatio?.trim() ?? "xMidYMid meet";
|
|
270
|
+
if (preserveAspectRatio !== "none" && preserveAspectRatio !== "xMidYMid meet") {
|
|
271
|
+
return fail('Only preserveAspectRatio="none" and "xMidYMid meet" are supported.');
|
|
272
|
+
}
|
|
273
|
+
const scaleX = width / viewWidth;
|
|
274
|
+
const scaleY = height / viewHeight;
|
|
275
|
+
const scale = preserveAspectRatio === "none" ? undefined : Math.min(scaleX, scaleY);
|
|
276
|
+
const appliedX = scale ?? scaleX;
|
|
277
|
+
const appliedY = scale ?? scaleY;
|
|
278
|
+
const offsetX = origin === "center" ? -(x + viewWidth / 2) * appliedX : -x * appliedX;
|
|
279
|
+
const offsetY = origin === "center" ? (y + viewHeight / 2) * appliedY : (y + viewHeight) * appliedY;
|
|
280
|
+
return [appliedX, 0, 0, -appliedY, offsetX, offsetY];
|
|
281
|
+
}
|
|
282
|
+
function isPainted(color, opacity) {
|
|
283
|
+
return color !== "none" && color !== "transparent" && opacity > 0;
|
|
284
|
+
}
|
|
285
|
+
function ellipsePoints(cx, cy, rx, ry, segments, budget) {
|
|
286
|
+
if (rx <= 0 || ry <= 0)
|
|
287
|
+
return fail("SVG ellipse radii must be positive.");
|
|
288
|
+
return Array.from({ length: segments }, (_, index) => {
|
|
289
|
+
budget.add("preflightSvgRegion");
|
|
290
|
+
const angle = (index * Math.PI * 2) / segments;
|
|
291
|
+
return [cx + Math.cos(angle) * rx, cy + Math.sin(angle) * ry];
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
function parsePoints(value) {
|
|
295
|
+
if (value === undefined)
|
|
296
|
+
return fail("SVG points are required.");
|
|
297
|
+
const numbers = value
|
|
298
|
+
.trim()
|
|
299
|
+
.split(/[\s,]+/)
|
|
300
|
+
.filter(Boolean)
|
|
301
|
+
.map(Number);
|
|
302
|
+
if (numbers.length < 4 ||
|
|
303
|
+
numbers.length % 2 !== 0 ||
|
|
304
|
+
numbers.some((entry) => !Number.isFinite(entry))) {
|
|
305
|
+
return fail("SVG points must contain finite X/Y pairs.");
|
|
306
|
+
}
|
|
307
|
+
const points = [];
|
|
308
|
+
for (let index = 0; index < numbers.length; index += 2)
|
|
309
|
+
points.push([numbers[index] ?? 0, numbers[index + 1] ?? 0]);
|
|
310
|
+
return points;
|
|
311
|
+
}
|
|
312
|
+
function inverseMatrix(matrix) {
|
|
313
|
+
const determinant = matrix[0] * matrix[3] - matrix[1] * matrix[2];
|
|
314
|
+
if (!Number.isFinite(determinant) || Math.abs(determinant) <= 1e-15) {
|
|
315
|
+
return fail("SVG geometry transforms must be non-singular.");
|
|
316
|
+
}
|
|
317
|
+
return [
|
|
318
|
+
matrix[3] / determinant,
|
|
319
|
+
-matrix[1] / determinant,
|
|
320
|
+
-matrix[2] / determinant,
|
|
321
|
+
matrix[0] / determinant,
|
|
322
|
+
(matrix[2] * matrix[5] - matrix[3] * matrix[4]) / determinant,
|
|
323
|
+
(matrix[1] * matrix[4] - matrix[0] * matrix[5]) / determinant,
|
|
324
|
+
];
|
|
325
|
+
}
|
|
326
|
+
function maximumLinearScale(matrix) {
|
|
327
|
+
const trace = matrix[0] * matrix[0] + matrix[1] * matrix[1] + matrix[2] * matrix[2] + matrix[3] * matrix[3];
|
|
328
|
+
const determinant = matrix[0] * matrix[3] - matrix[1] * matrix[2];
|
|
329
|
+
return Math.sqrt(Math.max(0, (trace + Math.sqrt(Math.max(0, trace * trace - 4 * determinant * determinant))) / 2));
|
|
330
|
+
}
|
|
331
|
+
function adaptivePathSubpaths(d, transform, resolution, budget) {
|
|
332
|
+
if (d.trim() === "")
|
|
333
|
+
return fail("SVG path data is required.");
|
|
334
|
+
const inverse = inverseMatrix(transform);
|
|
335
|
+
try {
|
|
336
|
+
return flattenSvgSubpaths(d, transform, resolution, budget, "preflightSvgRegion").map(({ closed, points }) => ({
|
|
337
|
+
closed,
|
|
338
|
+
points: points.map((point) => transformPoint(inverse, point)),
|
|
339
|
+
}));
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
if (error instanceof GeometryError)
|
|
343
|
+
throw error;
|
|
344
|
+
return fail(error instanceof Error ? `Invalid SVG path: ${error.message}` : "Invalid SVG path data.");
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function elementSubpaths(tag, values, transform, resolution, budget) {
|
|
348
|
+
switch (tag) {
|
|
349
|
+
case "rect": {
|
|
350
|
+
const x = finite(values.x, 0);
|
|
351
|
+
const y = finite(values.y, 0);
|
|
352
|
+
const width = finite(values.width);
|
|
353
|
+
const height = finite(values.height);
|
|
354
|
+
if (width <= 0 || height <= 0)
|
|
355
|
+
return fail("SVG rectangle dimensions must be positive.");
|
|
356
|
+
const rx = values.rx === undefined ? finite(values.ry, 0) : finite(values.rx, 0);
|
|
357
|
+
const ry = values.ry === undefined ? rx : finite(values.ry, 0);
|
|
358
|
+
if (rx === 0 || ry === 0) {
|
|
359
|
+
return adaptivePathSubpaths(`M${x} ${y}H${x + width}V${y + height}H${x}Z`, transform, resolution, budget);
|
|
360
|
+
}
|
|
361
|
+
const radiusX = Math.min(Math.max(0, rx), width / 2);
|
|
362
|
+
const radiusY = Math.min(Math.max(0, ry), height / 2);
|
|
363
|
+
return adaptivePathSubpaths(`M${x + radiusX} ${y}H${x + width - radiusX}A${radiusX} ${radiusY} 0 0 1 ${x + width} ${y + radiusY}V${y + height - radiusY}A${radiusX} ${radiusY} 0 0 1 ${x + width - radiusX} ${y + height}H${x + radiusX}A${radiusX} ${radiusY} 0 0 1 ${x} ${y + height - radiusY}V${y + radiusY}A${radiusX} ${radiusY} 0 0 1 ${x + radiusX} ${y}Z`, transform, resolution, budget);
|
|
364
|
+
}
|
|
365
|
+
case "circle": {
|
|
366
|
+
const radius = finite(values.r);
|
|
367
|
+
const segments = resolveCircularSegments(radius * maximumLinearScale(transform), resolution, budget.limits);
|
|
368
|
+
return [
|
|
369
|
+
{
|
|
370
|
+
closed: true,
|
|
371
|
+
points: ellipsePoints(finite(values.cx, 0), finite(values.cy, 0), radius, radius, segments, budget),
|
|
372
|
+
},
|
|
373
|
+
];
|
|
374
|
+
}
|
|
375
|
+
case "ellipse": {
|
|
376
|
+
const radiusX = finite(values.rx);
|
|
377
|
+
const radiusY = finite(values.ry);
|
|
378
|
+
const segments = resolveCircularSegments(Math.max(radiusX, radiusY) * maximumLinearScale(transform), resolution, budget.limits);
|
|
379
|
+
return [
|
|
380
|
+
{
|
|
381
|
+
closed: true,
|
|
382
|
+
points: ellipsePoints(finite(values.cx, 0), finite(values.cy, 0), radiusX, radiusY, segments, budget),
|
|
383
|
+
},
|
|
384
|
+
];
|
|
385
|
+
}
|
|
386
|
+
case "line":
|
|
387
|
+
return adaptivePathSubpaths(`M${finite(values.x1, 0)} ${finite(values.y1, 0)}L${finite(values.x2, 0)} ${finite(values.y2, 0)}`, transform, resolution, budget);
|
|
388
|
+
case "polyline": {
|
|
389
|
+
const points = parsePoints(values.points);
|
|
390
|
+
return adaptivePathSubpaths(points.map(([x, y], index) => `${index === 0 ? "M" : "L"}${x} ${y}`).join(""), transform, resolution, budget);
|
|
391
|
+
}
|
|
392
|
+
case "polygon": {
|
|
393
|
+
const points = parsePoints(values.points);
|
|
394
|
+
return adaptivePathSubpaths(`${points.map(([x, y], index) => `${index === 0 ? "M" : "L"}${x} ${y}`).join("")}Z`, transform, resolution, budget);
|
|
395
|
+
}
|
|
396
|
+
case "path":
|
|
397
|
+
return adaptivePathSubpaths(values.d ?? "", transform, resolution, budget);
|
|
398
|
+
default:
|
|
399
|
+
return [];
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
function strokeConstructionVertexCount(subpath, paint, circularSegments) {
|
|
403
|
+
const minimumPoints = subpath.closed ? 3 : 2;
|
|
404
|
+
if (subpath.points.length < minimumPoints)
|
|
405
|
+
return 0;
|
|
406
|
+
const segments = subpath.closed ? subpath.points.length : subpath.points.length - 1;
|
|
407
|
+
const joins = subpath.closed ? subpath.points.length : Math.max(0, subpath.points.length - 2);
|
|
408
|
+
return (segments * 4 +
|
|
409
|
+
joins *
|
|
410
|
+
(paint.strokeLinejoin === "round"
|
|
411
|
+
? circularSegments
|
|
412
|
+
: paint.strokeLinejoin === "miter"
|
|
413
|
+
? 4
|
|
414
|
+
: 3) +
|
|
415
|
+
(!subpath.closed && paint.strokeLinecap === "round" ? 2 * circularSegments : 0));
|
|
416
|
+
}
|
|
417
|
+
function transformMultiPolygon(value, matrix) {
|
|
418
|
+
return value.map((polygon) => polygon.map((path) => path.map((point) => {
|
|
419
|
+
const transformed = transformPoint(matrix, point);
|
|
420
|
+
return [transformed[0], transformed[1]];
|
|
421
|
+
})));
|
|
422
|
+
}
|
|
423
|
+
function polygonArea(polygon) {
|
|
424
|
+
return (Math.abs(signedAreaLoop2(polygon.outer)) -
|
|
425
|
+
polygon.holes.reduce((total, hole) => total + Math.abs(signedAreaLoop2(hole)), 0));
|
|
426
|
+
}
|
|
427
|
+
function loopEdgeLengths(loop) {
|
|
428
|
+
return loop.map((point, index) => {
|
|
429
|
+
const next = loop[(index + 1) % loop.length];
|
|
430
|
+
return Math.hypot(next[0] - point[0], next[1] - point[1]);
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
function measureSvgRegion(region) {
|
|
434
|
+
const holes = region.reduce((total, polygon) => total + polygon.holes.length, 0);
|
|
435
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
436
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
437
|
+
let minEdgeLength = Number.POSITIVE_INFINITY;
|
|
438
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
439
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
440
|
+
let shortEdges = 0;
|
|
441
|
+
let vertices = 0;
|
|
442
|
+
for (const polygon of region) {
|
|
443
|
+
for (const loop of [polygon.outer, ...polygon.holes]) {
|
|
444
|
+
vertices += loop.length;
|
|
445
|
+
for (const point of loop) {
|
|
446
|
+
minX = Math.min(minX, point[0]);
|
|
447
|
+
minY = Math.min(minY, point[1]);
|
|
448
|
+
maxX = Math.max(maxX, point[0]);
|
|
449
|
+
maxY = Math.max(maxY, point[1]);
|
|
450
|
+
}
|
|
451
|
+
for (const length of loopEdgeLengths(loop)) {
|
|
452
|
+
minEdgeLength = Math.min(minEdgeLength, length);
|
|
453
|
+
if (length < SHORT_EDGE_LENGTH_MM)
|
|
454
|
+
shortEdges += 1;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const bounds = region.length === 0
|
|
459
|
+
? null
|
|
460
|
+
: Object.freeze({
|
|
461
|
+
max: Object.freeze([maxX, maxY]),
|
|
462
|
+
min: Object.freeze([minX, minY]),
|
|
463
|
+
});
|
|
464
|
+
return Object.freeze({
|
|
465
|
+
area: regionArea(region),
|
|
466
|
+
bounds,
|
|
467
|
+
components: region.length,
|
|
468
|
+
contours: region.length + holes,
|
|
469
|
+
holes,
|
|
470
|
+
minEdgeLength: Number.isFinite(minEdgeLength) ? minEdgeLength : null,
|
|
471
|
+
shortEdges,
|
|
472
|
+
tinyIslands: region.filter((polygon) => polygonArea(polygon) < TINY_ISLAND_AREA_MM2).length,
|
|
473
|
+
vertices,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Parses the bounded SVG subset into a robust millimetre region and an import-review report.
|
|
478
|
+
*
|
|
479
|
+
* Presentation colors are ignored, but every visible fill and positive-width stroke contributes
|
|
480
|
+
* geometry. Open filled subpaths use SVG's implicit closure while open strokes remain open.
|
|
481
|
+
* Transforms and declared physical units are flattened, viewport placement is explicit, and
|
|
482
|
+
* metadata is ignored. Tiny-island and short-edge counts are conservative physical indicators,
|
|
483
|
+
* not proof of raster provenance or process-specific manufacturability.
|
|
484
|
+
*
|
|
485
|
+
* @param svg - Complete SVG XML source, limited to 256 KiB of UTF-8.
|
|
486
|
+
* @param options - Physical flattening and viewport-origin controls.
|
|
487
|
+
* @returns The exact normalized region used for import plus bounded review metrics and repairs.
|
|
488
|
+
* @throws {@link GeometryError} When XML, supported-feature, geometry, or complexity checks fail.
|
|
489
|
+
*/
|
|
490
|
+
function createSvgRegionPreflight(svg, options = {}) {
|
|
491
|
+
const bytes = new TextEncoder().encode(svg).byteLength;
|
|
492
|
+
if (bytes > MAX_SVG_BYTES)
|
|
493
|
+
return fail("SVG geometry must be 256 KiB or smaller.");
|
|
494
|
+
const origin = options.origin ?? "center";
|
|
495
|
+
if (origin !== "center" && origin !== "document") {
|
|
496
|
+
return fail('SVG origin must be either "center" or "document".');
|
|
497
|
+
}
|
|
498
|
+
const limits = resolveGeometrySamplingLimits(options.limits);
|
|
499
|
+
const resolution = resolveGeometryResolution({
|
|
500
|
+
...options.resolution,
|
|
501
|
+
...(options.circularSegments === undefined
|
|
502
|
+
? {}
|
|
503
|
+
: { circularSegments: options.circularSegments }),
|
|
504
|
+
}, limits);
|
|
505
|
+
const { assertExpandedVertexCount, boundedMultiPolygon, fillGeometry, multiPolygonVertexCount, regionFromMultiPolygon, unionAll, } = createBoundedSvgClipping(limits.maxProfilePoints);
|
|
506
|
+
const stack = [];
|
|
507
|
+
const geometry = [];
|
|
508
|
+
let elementCount = 0;
|
|
509
|
+
let emptyElements = 0;
|
|
510
|
+
let expandedGeometryVertices = 0;
|
|
511
|
+
let implicitFilledClosures = 0;
|
|
512
|
+
let invisibleElements = 0;
|
|
513
|
+
let nodeCount = 0;
|
|
514
|
+
let rootSeen = false;
|
|
515
|
+
let strokeConstructionVertices = 0;
|
|
516
|
+
const budget = new SvgVertexBudget(limits);
|
|
517
|
+
const appendGeometry = (target, value, stage) => {
|
|
518
|
+
const bounded = boundedMultiPolygon(value, stage);
|
|
519
|
+
expandedGeometryVertices += multiPolygonVertexCount(bounded);
|
|
520
|
+
assertExpandedVertexCount(expandedGeometryVertices, "accumulated geometry");
|
|
521
|
+
target.push(bounded);
|
|
522
|
+
};
|
|
523
|
+
// XML syntax and entity decoding belong to saxes; the bounded geometry policy stays here.
|
|
524
|
+
const parser = new SaxesParser({ xmlns: true });
|
|
525
|
+
parser.on("error", () => fail("SVG XML is malformed."));
|
|
526
|
+
parser.on("doctype", () => fail("DOCTYPE and entity declarations are not supported."));
|
|
527
|
+
parser.on("processinginstruction", () => fail("SVG processing instructions are not supported."));
|
|
528
|
+
parser.on("cdata", () => fail("SVG CDATA sections are not supported."));
|
|
529
|
+
parser.on("text", (text) => {
|
|
530
|
+
if (stack.at(-1)?.tag !== "metadata" && text.trim() !== "") {
|
|
531
|
+
return fail("Unexpected SVG text content.");
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
parser.on("opentag", (node) => {
|
|
535
|
+
if (node.prefix !== "" || (node.uri !== "" && node.uri !== SVG_NAMESPACE)) {
|
|
536
|
+
return fail("Custom XML namespaces are not supported.");
|
|
537
|
+
}
|
|
538
|
+
const tag = node.local;
|
|
539
|
+
if (tag === "image") {
|
|
540
|
+
return fail("This SVG contains an <image> element. MetaMesh requires real vector paths; embedded bitmap images are not supported. Re-export the original artwork as paths. Changing a PNG file extension or embedding it in an SVG will not work.");
|
|
541
|
+
}
|
|
542
|
+
if (!SUPPORTED_TAGS.has(tag))
|
|
543
|
+
return fail(`Unsupported SVG element <${tag}>.`);
|
|
544
|
+
nodeCount += 1;
|
|
545
|
+
if (nodeCount > MAX_SVG_NODES) {
|
|
546
|
+
complexityFail(`SVG may contain at most ${MAX_SVG_NODES} XML elements.`, {
|
|
547
|
+
maxNodes: MAX_SVG_NODES,
|
|
548
|
+
nodes: nodeCount,
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
if (!node.isSelfClosing && stack.length >= MAX_SVG_NESTING_DEPTH) {
|
|
552
|
+
complexityFail(`SVG nesting may be at most ${MAX_SVG_NESTING_DEPTH} elements deep.`, {
|
|
553
|
+
depth: stack.length + 1,
|
|
554
|
+
maxDepth: MAX_SVG_NESTING_DEPTH,
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
const values = attributes(node);
|
|
558
|
+
validateAttributes(tag, values);
|
|
559
|
+
const parent = stack.at(-1);
|
|
560
|
+
if (parent !== undefined && parent.tag !== "svg" && parent.tag !== "g") {
|
|
561
|
+
return fail(`Nested content in <${parent.tag}> is not supported.`);
|
|
562
|
+
}
|
|
563
|
+
if (tag === "svg" && parent !== undefined)
|
|
564
|
+
return fail("Nested SVG roots are not supported.");
|
|
565
|
+
let transform = multiply(parent?.transform ?? [1, 0, 0, 1, 0, 0], parseTransform(values.transform));
|
|
566
|
+
const paint = parsePaint(values, parent?.paint ?? DEFAULT_PAINT);
|
|
567
|
+
if (tag === "svg") {
|
|
568
|
+
if (rootSeen)
|
|
569
|
+
return fail("SVG must contain exactly one root element.");
|
|
570
|
+
rootSeen = true;
|
|
571
|
+
const box = viewBox(values);
|
|
572
|
+
const [, , viewWidth, viewHeight] = box;
|
|
573
|
+
const width = lengthMm(values.width, viewWidth);
|
|
574
|
+
const height = lengthMm(values.height, viewHeight);
|
|
575
|
+
transform = multiply(canvasTransform(values, width, height, box, origin), parseTransform(values.transform));
|
|
576
|
+
}
|
|
577
|
+
else if (parent === undefined) {
|
|
578
|
+
return fail("The SVG root element is missing.");
|
|
579
|
+
}
|
|
580
|
+
if (tag !== "metadata" && tag !== "svg" && tag !== "g") {
|
|
581
|
+
elementCount += 1;
|
|
582
|
+
if (elementCount > MAX_SVG_ELEMENTS)
|
|
583
|
+
return fail(`SVG geometry may contain at most ${MAX_SVG_ELEMENTS} elements.`);
|
|
584
|
+
const localSubpaths = elementSubpaths(tag, values, transform, resolution, budget);
|
|
585
|
+
const subpaths = localSubpaths.map((subpath) => ({
|
|
586
|
+
...subpath,
|
|
587
|
+
points: subpath.points.map((point) => transformPoint(transform, point)),
|
|
588
|
+
}));
|
|
589
|
+
const paintedFill = isPainted(paint.fill, paint.fillOpacity);
|
|
590
|
+
const paintedStroke = isPainted(paint.stroke, paint.strokeOpacity) && paint.strokeWidth > 0;
|
|
591
|
+
const elementGeometry = [];
|
|
592
|
+
if (!paintedFill && !paintedStroke)
|
|
593
|
+
invisibleElements += 1;
|
|
594
|
+
if (paintedFill) {
|
|
595
|
+
implicitFilledClosures += subpaths.filter((subpath) => !subpath.closed && subpath.points.length >= 3).length;
|
|
596
|
+
appendGeometry(elementGeometry, fillGeometry(subpaths, paint.fillRule), "element fill output");
|
|
597
|
+
}
|
|
598
|
+
if (paintedStroke) {
|
|
599
|
+
const roundSegments = paint.strokeLinecap === "round" || paint.strokeLinejoin === "round"
|
|
600
|
+
? resolveCircularSegments((paint.strokeWidth / 2) * maximumLinearScale(transform), resolution, limits)
|
|
601
|
+
: 3;
|
|
602
|
+
// Constructing the stroke before applying the affine matrix preserves SVG's
|
|
603
|
+
// exact non-uniform scale and skew behavior without a scalar-width approximation.
|
|
604
|
+
for (const subpath of localSubpaths) {
|
|
605
|
+
strokeConstructionVertices += strokeConstructionVertexCount(subpath, paint, roundSegments);
|
|
606
|
+
assertExpandedVertexCount(strokeConstructionVertices, "stroke construction");
|
|
607
|
+
appendGeometry(elementGeometry, transformMultiPolygon(strokePathMultiPolygon(subpath.points, {
|
|
608
|
+
cap: paint.strokeLinecap,
|
|
609
|
+
closed: subpath.closed,
|
|
610
|
+
join: paint.strokeLinejoin,
|
|
611
|
+
width: paint.strokeWidth,
|
|
612
|
+
limits,
|
|
613
|
+
}, roundSegments), transform), "element stroke output");
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
if ((paintedFill || paintedStroke) && elementGeometry.every((value) => value.length === 0)) {
|
|
617
|
+
emptyElements += 1;
|
|
618
|
+
}
|
|
619
|
+
geometry.push(...elementGeometry);
|
|
620
|
+
}
|
|
621
|
+
stack.push({ tag, transform, paint });
|
|
622
|
+
});
|
|
623
|
+
parser.on("closetag", () => {
|
|
624
|
+
stack.pop();
|
|
625
|
+
});
|
|
626
|
+
parser.write(svg).close();
|
|
627
|
+
if (stack.length !== 0)
|
|
628
|
+
return fail("SVG contains unclosed elements.");
|
|
629
|
+
if (!rootSeen)
|
|
630
|
+
return fail("The SVG root element is missing.");
|
|
631
|
+
const region = regionFromMultiPolygon(unionAll(geometry));
|
|
632
|
+
assertExpandedVertexCount(region.reduce((total, polygon) => total +
|
|
633
|
+
polygon.outer.length +
|
|
634
|
+
polygon.holes.reduce((holeTotal, hole) => holeTotal + hole.length, 0), 0), "normalized region");
|
|
635
|
+
return Object.freeze({
|
|
636
|
+
geometry: measureSvgRegion(region),
|
|
637
|
+
region,
|
|
638
|
+
repairs: Object.freeze({
|
|
639
|
+
emptyElements,
|
|
640
|
+
implicitFilledClosures,
|
|
641
|
+
invisibleElements,
|
|
642
|
+
removedDuplicateVertices: budget.removedDuplicateCount,
|
|
643
|
+
removedSeamVertices: budget.removedSeamCount,
|
|
644
|
+
}),
|
|
645
|
+
source: Object.freeze({
|
|
646
|
+
bytes,
|
|
647
|
+
elements: elementCount,
|
|
648
|
+
emittedVertices: budget.emittedCount,
|
|
649
|
+
parsedVertices: budget.parsedCount,
|
|
650
|
+
}),
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Preflights bounded SVG geometry without browser or Manifold dependencies.
|
|
655
|
+
*
|
|
656
|
+
* @param svg - Complete SVG XML source, limited to 256 KiB of UTF-8.
|
|
657
|
+
* @param options - Physical flattening and viewport-origin controls.
|
|
658
|
+
* @returns The canonical normalized region plus bounded source, topology, and safe-repair facts.
|
|
659
|
+
*/
|
|
660
|
+
export function preflightSvgRegion(svg, options = {}) {
|
|
661
|
+
return createSvgRegionPreflight(svg, options);
|
|
662
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { GeometryError } from "../errors.js";
|
|
2
|
+
/** @internal Validates local scales before any manufacturing buffer allocation. */
|
|
3
|
+
export function resolveSweepScales(scale, stationCount) {
|
|
4
|
+
const fail = () => {
|
|
5
|
+
throw new GeometryError("INVALID_ARGUMENT", "Sweep scales must be positive finite XY pairs, with one pair per station when varying.", { operation: "sweep" });
|
|
6
|
+
};
|
|
7
|
+
const pair = (value) => {
|
|
8
|
+
if (typeof value === "number") {
|
|
9
|
+
if (!(value > 0) || !Number.isFinite(value))
|
|
10
|
+
fail();
|
|
11
|
+
return [value, value];
|
|
12
|
+
}
|
|
13
|
+
if (!Array.isArray(value) ||
|
|
14
|
+
value.length !== 2 ||
|
|
15
|
+
!value.every((n) => typeof n === "number" && Number.isFinite(n) && n > 0))
|
|
16
|
+
fail();
|
|
17
|
+
return value;
|
|
18
|
+
};
|
|
19
|
+
if (scale === undefined ||
|
|
20
|
+
typeof scale === "number" ||
|
|
21
|
+
(Array.isArray(scale) && typeof scale[0] === "number")) {
|
|
22
|
+
const value = pair(scale ?? 1);
|
|
23
|
+
return Array.from({ length: stationCount }, () => value);
|
|
24
|
+
}
|
|
25
|
+
if (!Array.isArray(scale) || scale.length !== stationCount)
|
|
26
|
+
fail();
|
|
27
|
+
return scale.map(pair);
|
|
28
|
+
}
|