@metamesh/modeling 0.0.0-stage → 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 (130) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/LICENSE +200 -0
  3. package/README.md +314 -2
  4. package/THIRD_PARTY_NOTICES.md +153 -0
  5. package/dist/api/engine.d.ts +54 -0
  6. package/dist/api/geometry.d.ts +2912 -0
  7. package/dist/api/index.d.ts +1648 -0
  8. package/dist/api/text.d.ts +77 -0
  9. package/dist/engine/construction-operations.js +189 -0
  10. package/dist/engine/execution.js +227 -0
  11. package/dist/engine/export-3mf.js +100 -0
  12. package/dist/engine/extrusion-finishing.js +82 -0
  13. package/dist/engine/finalization.js +214 -0
  14. package/dist/engine/index.js +72 -0
  15. package/dist/engine/inspection-operations.js +52 -0
  16. package/dist/engine/kernel/LICENSE +201 -0
  17. package/dist/engine/kernel/manifold.js +2 -0
  18. package/dist/engine/kernel/manifold.wasm +0 -0
  19. package/dist/engine/loft-operations.js +156 -0
  20. package/dist/engine/mechanical-operations.js +51 -0
  21. package/dist/engine/modeling-context.js +38 -0
  22. package/dist/engine/native/adapter.js +20 -0
  23. package/dist/engine/native/context.js +209 -0
  24. package/dist/engine/native/copied-views.js +46 -0
  25. package/dist/engine/native/index.js +13 -0
  26. package/dist/engine/native/operations.js +115 -0
  27. package/dist/engine/native/ownership.js +98 -0
  28. package/dist/engine/native/regions.js +138 -0
  29. package/dist/engine/native/scoped-primitives.js +127 -0
  30. package/dist/engine/native/scoped-threads.js +34 -0
  31. package/dist/engine/native/threads.js +52 -0
  32. package/dist/engine/native/transforms.js +111 -0
  33. package/dist/engine/operation-environment.js +20 -0
  34. package/dist/engine/pattern-operations.js +36 -0
  35. package/dist/engine/pattern-placements.js +176 -0
  36. package/dist/engine/primitive-anchors.js +70 -0
  37. package/dist/engine/primitive-correspondence.js +74 -0
  38. package/dist/engine/primitive-operations.js +151 -0
  39. package/dist/engine/primitive-profile-types.js +1 -0
  40. package/dist/engine/primitive-profiles.js +250 -0
  41. package/dist/engine/profile-mesh.js +279 -0
  42. package/dist/engine/section-operations.js +93 -0
  43. package/dist/engine/shape-anchors.js +88 -0
  44. package/dist/engine/transform-operations.js +225 -0
  45. package/dist/engine/types.js +1 -0
  46. package/dist/geometry/anchors.js +226 -0
  47. package/dist/geometry/curves/evaluation.js +162 -0
  48. package/dist/geometry/curves/sampling.js +265 -0
  49. package/dist/geometry/curves/types.js +1 -0
  50. package/dist/geometry/cylinder.js +117 -0
  51. package/dist/geometry/edge-finishing.js +160 -0
  52. package/dist/geometry/errors.js +76 -0
  53. package/dist/geometry/extrusion.js +150 -0
  54. package/dist/geometry/heightfield.js +107 -0
  55. package/dist/geometry/index.js +42 -0
  56. package/dist/geometry/loft-correspondence.js +204 -0
  57. package/dist/geometry/loft-geometry.js +184 -0
  58. package/dist/geometry/loft-options.js +56 -0
  59. package/dist/geometry/loft-simple-mesh.js +92 -0
  60. package/dist/geometry/loft-triangulation.js +64 -0
  61. package/dist/geometry/loft.js +80 -0
  62. package/dist/geometry/math/vector3.js +41 -0
  63. package/dist/geometry/mechanical/gear-motion.js +94 -0
  64. package/dist/geometry/mechanical/gears.js +484 -0
  65. package/dist/geometry/mechanical/index.js +13 -0
  66. package/dist/geometry/mechanical/standards.js +67 -0
  67. package/dist/geometry/mechanical/thread-helices.js +44 -0
  68. package/dist/geometry/mechanical/thread-mesh-plan.js +358 -0
  69. package/dist/geometry/mechanical/thread-mesh.js +142 -0
  70. package/dist/geometry/mechanical/thread-profiles.js +368 -0
  71. package/dist/geometry/mechanical/thread-solids.js +230 -0
  72. package/dist/geometry/mechanical/thread-surface.js +433 -0
  73. package/dist/geometry/mechanical/threads.js +138 -0
  74. package/dist/geometry/mesh-planar.js +408 -0
  75. package/dist/geometry/mesh-quality.js +396 -0
  76. package/dist/geometry/mesh-signature.js +137 -0
  77. package/dist/geometry/mesh.js +300 -0
  78. package/dist/geometry/model-limits.js +50 -0
  79. package/dist/geometry/numeric.js +11 -0
  80. package/dist/geometry/paths/frames.js +118 -0
  81. package/dist/geometry/paths/helix.js +49 -0
  82. package/dist/geometry/paths/path.js +30 -0
  83. package/dist/geometry/paths/planar.js +135 -0
  84. package/dist/geometry/paths/resampling.js +124 -0
  85. package/dist/geometry/polygon-clipping.js +52 -0
  86. package/dist/geometry/primitive-options.js +1 -0
  87. package/dist/geometry/primitive-resolution.js +17 -0
  88. package/dist/geometry/primitive-resolvers.js +235 -0
  89. package/dist/geometry/profiles/matched.js +184 -0
  90. package/dist/geometry/profiles/open-profile.js +137 -0
  91. package/dist/geometry/profiles/profiles.js +70 -0
  92. package/dist/geometry/profiles/rectangle.js +235 -0
  93. package/dist/geometry/profiles/shapes.js +34 -0
  94. package/dist/geometry/profiles/span-sampling.js +28 -0
  95. package/dist/geometry/profiles/types.js +1 -0
  96. package/dist/geometry/profiles/validation.js +20 -0
  97. package/dist/geometry/regions.js +371 -0
  98. package/dist/geometry/resolution.js +236 -0
  99. package/dist/geometry/revolve/revolve-mesh.js +123 -0
  100. package/dist/geometry/sampling-limits.js +29 -0
  101. package/dist/geometry/stroke-path.js +181 -0
  102. package/dist/geometry/svg/errors.js +23 -0
  103. package/dist/geometry/svg/fill.js +172 -0
  104. package/dist/geometry/svg/index.js +1 -0
  105. package/dist/geometry/svg/matrix.js +130 -0
  106. package/dist/geometry/svg/parser.js +536 -0
  107. package/dist/geometry/svg/path.js +349 -0
  108. package/dist/geometry/svg/types.js +1 -0
  109. package/dist/geometry/svg-import.js +662 -0
  110. package/dist/geometry/sweeps/options.js +28 -0
  111. package/dist/geometry/sweeps/orientation.js +106 -0
  112. package/dist/geometry/sweeps/stations.js +161 -0
  113. package/dist/geometry/sweeps/sweep-closed.js +96 -0
  114. package/dist/geometry/sweeps/sweep-mesh.js +20 -0
  115. package/dist/geometry/sweeps/sweep-open.js +76 -0
  116. package/dist/geometry/sweeps/validation.js +116 -0
  117. package/dist/geometry/text-contours.js +32 -0
  118. package/dist/geometry/text-layout.js +1 -0
  119. package/dist/geometry/tolerance.js +53 -0
  120. package/dist/geometry/transform-geometry.js +41 -0
  121. package/dist/geometry/transform3.js +340 -0
  122. package/dist/geometry/types.js +1 -0
  123. package/dist/index.js +1 -0
  124. package/dist/model/execution-binding.js +30 -0
  125. package/dist/model/operations.js +152 -0
  126. package/dist/model/options.js +1 -0
  127. package/dist/model/types.js +1 -0
  128. package/dist/text/index.js +48 -0
  129. package/dist/text/vector-text-layout.js +279 -0
  130. package/package.json +100 -3
@@ -0,0 +1,106 @@
1
+ import { GeometryError } from "../errors.js";
2
+ import { cross3, normalize3, projectOntoPlane3, subtract3 } from "../math/vector3.js";
3
+ import { computeClosedParallelTransportFrames, computeParallelTransportFrames, } from "../paths/frames.js";
4
+ import { validateSweepNormal } from "./validation.js";
5
+ const fail = (message, code = "INVALID_ARGUMENT") => {
6
+ throw new GeometryError(code, message, { operation: "sweep" });
7
+ };
8
+ /** @internal Validates the canonical sweep controls without evaluating authored guides. */
9
+ export function validateSweepOptions(options) {
10
+ if (typeof options !== "object" || options === null || Array.isArray(options))
11
+ fail("Sweep options must be an object.");
12
+ const allowed = [
13
+ "closed",
14
+ "limits",
15
+ "orientation",
16
+ "resolution",
17
+ "twistDegrees",
18
+ "twistBy",
19
+ "scale",
20
+ "seamProfileOffset",
21
+ ];
22
+ if (Object.keys(options).some((key) => !allowed.includes(key)))
23
+ fail("Unsupported sweep option.");
24
+ if (options.closed !== undefined && typeof options.closed !== "boolean")
25
+ fail("Sweep closed must be boolean.");
26
+ if (options.twistBy !== undefined &&
27
+ options.twistBy !== "length" &&
28
+ options.twistBy !== "stations")
29
+ fail("Sweep twistBy must be length or stations.");
30
+ if (!Number.isFinite(options.twistDegrees ?? 0))
31
+ throw new GeometryError("NON_FINITE_VALUE", "Sweep twistDegrees must be finite.", {
32
+ operation: "sweep",
33
+ });
34
+ const policy = options.orientation;
35
+ if (policy === undefined)
36
+ return;
37
+ if (typeof policy !== "object" || policy === null || Array.isArray(policy))
38
+ fail("Sweep orientation must be a policy object.");
39
+ let keys;
40
+ if (policy.kind === "transport") {
41
+ keys = ["kind", "initialNormal"];
42
+ if (policy.initialNormal !== undefined)
43
+ validateSweepNormal(policy.initialNormal, "sweep");
44
+ }
45
+ else if (policy.kind === "fixed-normal") {
46
+ keys = ["kind", "normal"];
47
+ validateSweepNormal(policy.normal, "sweep");
48
+ }
49
+ else if (policy.kind === "guide") {
50
+ keys = ["kind", "normal"];
51
+ if (typeof policy.normal !== "function")
52
+ fail("Guide orientation needs a normal function.");
53
+ }
54
+ else if (policy.kind === "frenet")
55
+ keys = ["kind"];
56
+ else
57
+ return fail("Unsupported sweep orientation policy.");
58
+ if (Object.keys(policy).some((key) => !keys.includes(key)))
59
+ fail("Unsupported field for the selected sweep orientation.");
60
+ }
61
+ function directedNormal(value, tangent, label) {
62
+ validateSweepNormal(value, "sweep");
63
+ const maximum = Math.max(...value.map(Math.abs));
64
+ const unit = normalize3([value[0] / maximum, value[1] / maximum, value[2] / maximum]);
65
+ const projected = projectOntoPlane3(unit, tangent);
66
+ if (Math.hypot(...projected) <= 1e-10)
67
+ fail(`${label} normal must not be parallel to the path tangent.`, "DEGENERATE_GEOMETRY");
68
+ return normalize3(projected);
69
+ }
70
+ /** @internal One frame policy for simple and filled sweeps, evaluated once per original normalized station. */
71
+ export function computeSweepFrames(path, options) {
72
+ validateSweepOptions(options);
73
+ const closed = options.closed ?? false;
74
+ const policy = options.orientation ?? { kind: "transport" };
75
+ const tangentAt = (index) => {
76
+ const previous = closed ? (index - 1 + path.length) % path.length : Math.max(0, index - 1);
77
+ const next = closed ? (index + 1) % path.length : Math.min(path.length - 1, index + 1);
78
+ return normalize3(subtract3(path[next], path[previous]));
79
+ };
80
+ if (policy.kind === "transport") {
81
+ const preferred = policy.initialNormal === undefined
82
+ ? undefined
83
+ : directedNormal(policy.initialNormal, tangentAt(0), "Initial transport");
84
+ return closed
85
+ ? computeClosedParallelTransportFrames(path, preferred, options.limits)
86
+ : computeParallelTransportFrames(path, preferred, undefined, options.limits);
87
+ }
88
+ const tangents = path.map((_, index) => tangentAt(index));
89
+ return path.map((point, index) => {
90
+ const tangent = tangents[index];
91
+ let normal;
92
+ if (policy.kind === "fixed-normal")
93
+ normal = directedNormal(policy.normal, tangent, "Fixed");
94
+ else if (policy.kind === "guide")
95
+ normal = directedNormal(policy.normal(point, index), tangent, "Guide");
96
+ else {
97
+ const previous = closed ? (index - 1 + path.length) % path.length : Math.max(0, index - 1);
98
+ const next = closed ? (index + 1) % path.length : Math.min(path.length - 1, index + 1);
99
+ const curvature = projectOntoPlane3(subtract3(tangents[next], tangents[previous]), tangent);
100
+ if (Math.hypot(...curvature) <= 1e-10)
101
+ fail("Frenet orientation needs nonzero sampled curvature at every station.", "DEGENERATE_GEOMETRY");
102
+ normal = normalize3(curvature);
103
+ }
104
+ return { tangent, normal, binormal: normalize3(cross3(tangent, normal)) };
105
+ });
106
+ }
@@ -0,0 +1,161 @@
1
+ import { GeometryError } from "../errors.js";
2
+ import { cross3, dot3, lerp3, normalize3, rotateAroundAxis3 } from "../math/vector3.js";
3
+ import { resolveGeometryResolution } from "../resolution.js";
4
+ import { assertSamplingCount, resolveGeometrySamplingLimits } from "../sampling-limits.js";
5
+ /** @internal Original station progression, with a virtual final seam endpoint for closed paths. */
6
+ export function sweepProgress(path, options) {
7
+ if (options.twistBy !== undefined &&
8
+ options.twistBy !== "length" &&
9
+ options.twistBy !== "stations")
10
+ throw new GeometryError("INVALID_ARGUMENT", "Sweep twistBy must be length or stations.", {
11
+ operation: "sweep",
12
+ });
13
+ const closed = options.closed ?? false, gaps = closed ? path.length : path.length - 1;
14
+ if (options.twistBy === "stations")
15
+ return Array.from({ length: gaps + 1 }, (_, index) => index / gaps);
16
+ const cumulative = [0];
17
+ for (let index = 0; index < gaps; index++) {
18
+ const next = path[(index + 1) % path.length], previous = path[index];
19
+ cumulative.push(cumulative[index] +
20
+ Math.hypot(next[0] - previous[0], next[1] - previous[1], next[2] - previous[2]));
21
+ }
22
+ const total = cumulative.at(-1);
23
+ if (!Number.isFinite(total) || total <= 0)
24
+ throw new GeometryError("DEGENERATE_GEOMETRY", "Sweep progression needs finite positive path length.", { operation: "sweep" });
25
+ return cumulative.map((length) => length / total);
26
+ }
27
+ /** Shortest rotation between two right-handed transported frames, including a stable half turn. */
28
+ function frameRotation(from, to) {
29
+ const cosine = Math.max(-1, Math.min(1, (dot3(from.normal, to.normal) +
30
+ dot3(from.binormal, to.binormal) +
31
+ dot3(from.tangent, to.tangent) -
32
+ 1) /
33
+ 2));
34
+ const axes = [
35
+ cross3(from.normal, to.normal),
36
+ cross3(from.binormal, to.binormal),
37
+ cross3(from.tangent, to.tangent),
38
+ ];
39
+ const axis = [
40
+ axes.reduce((sum, v) => sum + v[0], 0),
41
+ axes.reduce((sum, v) => sum + v[1], 0),
42
+ axes.reduce((sum, v) => sum + v[2], 0),
43
+ ];
44
+ const angle = Math.atan2(Math.hypot(...axis) / 2, cosine);
45
+ if (angle <= 1e-12)
46
+ return { axis: [1, 0, 0], angle: 0 };
47
+ if (cosine >= 0 || Math.hypot(...axis) > 1e-8)
48
+ return { axis: normalize3(axis), angle };
49
+ const matrix = Array.from({ length: 3 }, (_, row) => Array.from({ length: 3 }, (_, column) => to.normal[row] * from.normal[column] +
50
+ to.binormal[row] * from.binormal[column] +
51
+ to.tangent[row] * from.tangent[column]));
52
+ let principal = 0;
53
+ for (let i = 1; i < 3; i++)
54
+ if (matrix[i][i] > matrix[principal][principal])
55
+ principal = i;
56
+ const component = Math.sqrt(Math.max(0, (matrix[principal][principal] + 1) / 2));
57
+ const halfTurn = [0, 0, 0];
58
+ halfTurn[principal] = component;
59
+ for (let i = 0; i < 3; i++)
60
+ if (i !== principal)
61
+ halfTurn[i] = (matrix[i][principal] + matrix[principal][i]) / (4 * component);
62
+ return { axis: normalize3(halfTurn), angle };
63
+ }
64
+ /** Plans refinement of the whole triangulated surface, including its mixed edge/twist error. */
65
+ export function planSweepStations(contours, path, scales, options, frames) {
66
+ const closed = options.closed ?? false;
67
+ const segmentCount = closed ? path.length : path.length - 1;
68
+ const twist = ((options.twistDegrees ?? 0) / 180) * Math.PI;
69
+ if (!Number.isFinite(twist))
70
+ throw new GeometryError("NON_FINITE_VALUE", "Sweep twistDegrees must be finite.", {
71
+ operation: "sweep",
72
+ });
73
+ const intervals = [];
74
+ const progress = sweepProgress(path, options);
75
+ if (twist === 0)
76
+ return { intervals: Array(segmentCount).fill(1), stationCount: path.length, progress };
77
+ const resolution = resolveGeometryResolution(options.resolution, options.limits);
78
+ const limits = resolveGeometrySamplingLimits(options.limits);
79
+ let radius = 0, edgeLength = 0;
80
+ for (const contour of contours)
81
+ for (let i = 0; i < contour.length; i++) {
82
+ const point = contour[i], next = contour[(i + 1) % contour.length];
83
+ if (!Array.isArray(point) ||
84
+ point.length !== 2 ||
85
+ !point.every((coordinate) => typeof coordinate === "number" && Number.isFinite(coordinate)))
86
+ throw new GeometryError("INVALID_ARGUMENT", "Sweep profiles require finite XY points.", {
87
+ operation: "sweep",
88
+ });
89
+ radius = Math.max(radius, Math.hypot(...point));
90
+ edgeLength = Math.max(edgeLength, Math.hypot(next[0] - point[0], next[1] - point[1]));
91
+ }
92
+ let stationCount = closed ? 0 : 1;
93
+ for (let i = 0; i < segmentCount; i++) {
94
+ const next = (i + 1) % path.length;
95
+ const twistStep = Math.abs(twist * (progress[i + 1] - progress[i]));
96
+ const frameAngle = frames === undefined ? 0 : frameRotation(frames[i], frames[next]).angle;
97
+ const angle = frameAngle + twistStep;
98
+ const maximumScale = Math.max(...scales[i], ...scales[next]);
99
+ const scaleChange = Math.max(Math.abs(scales[next][0] - scales[i][0]), Math.abs(scales[next][1] - scales[i][1]));
100
+ // Interpolation on each triangular side patch has error <= A/n² + B/n. The B
101
+ // term bounds the mixed profile-edge/station derivative; bounding only vertex
102
+ // trajectories misses the systematic inward bias of a twisted quad diagonal.
103
+ const curvature = radius * (maximumScale * (angle * angle + frameAngle * twistStep) + 2 * scaleChange * angle);
104
+ const a = curvature / 8, b = (edgeLength * (maximumScale * angle + scaleChange)) / 4;
105
+ const requiredAngular = resolution.circularSegments === undefined
106
+ ? Math.max(1, Math.ceil((b + Math.hypot(b, 2 * Math.sqrt(a * resolution.maxChordError))) /
107
+ (2 * resolution.maxChordError)), Math.ceil((resolution.minimumSegments * angle) / (2 * Math.PI)))
108
+ : Math.max(1, Math.ceil((resolution.circularSegments * angle) / (2 * Math.PI)));
109
+ const allowedAngular = Math.max(1, scaleChange === 0 ? 0 : resolution.maximumSegments, Math.ceil((resolution.maximumSegments * angle) / (2 * Math.PI)));
110
+ if (requiredAngular > allowedAngular)
111
+ throw new GeometryError("COMPLEXITY_LIMIT", `Sweep surface refinement requires ${requiredAngular} intervals; the selected circular ceiling allows ${allowedAngular}.`, {
112
+ operation: "sweep",
113
+ details: { requiredCount: requiredAngular, allowedCount: allowedAngular },
114
+ });
115
+ const travel = Math.hypot(...[0, 1, 2].map((axis) => path[next][axis] - path[i][axis])) +
116
+ radius * (maximumScale * angle + scaleChange);
117
+ const lengthIntervals = resolution.maxSegmentLength === undefined
118
+ ? 1
119
+ : Math.ceil(travel / resolution.maxSegmentLength);
120
+ const count = Math.max(requiredAngular, lengthIntervals);
121
+ stationCount += count;
122
+ assertSamplingCount(stationCount, "sweep", limits.maxPathPoints);
123
+ intervals.push(count);
124
+ }
125
+ return { intervals, stationCount, progress };
126
+ }
127
+ /** Inserts twist samples without calling authored guides again or changing original station meaning. */
128
+ export function refineSweepStations(path, scales, frames, options, plan) {
129
+ const twist = ((options.twistDegrees ?? 0) / 180) * Math.PI;
130
+ const segmentCount = plan.intervals.length;
131
+ const points = [], sampledScales = [], sampledFrames = [], twists = [];
132
+ for (let i = 0; i < segmentCount; i++) {
133
+ const next = (i + 1) % path.length;
134
+ const rotation = frameRotation(frames[i], frames[next]);
135
+ for (let j = 0; j < plan.intervals[i]; j++) {
136
+ const amount = j / plan.intervals[i];
137
+ points.push(amount === 0 ? path[i] : lerp3(path[i], path[next], amount));
138
+ sampledScales.push(amount === 0
139
+ ? scales[i]
140
+ : [
141
+ scales[i][0] + (scales[next][0] - scales[i][0]) * amount,
142
+ scales[i][1] + (scales[next][1] - scales[i][1]) * amount,
143
+ ]);
144
+ sampledFrames.push(amount === 0
145
+ ? frames[i]
146
+ : {
147
+ normal: rotateAroundAxis3(frames[i].normal, rotation.axis, rotation.angle * amount),
148
+ binormal: rotateAroundAxis3(frames[i].binormal, rotation.axis, rotation.angle * amount),
149
+ tangent: rotateAroundAxis3(frames[i].tangent, rotation.axis, rotation.angle * amount),
150
+ });
151
+ twists.push(twist * (plan.progress[i] + (plan.progress[i + 1] - plan.progress[i]) * amount));
152
+ }
153
+ }
154
+ if (options.closed !== true) {
155
+ points.push(path.at(-1));
156
+ sampledScales.push(scales.at(-1));
157
+ sampledFrames.push(frames.at(-1));
158
+ twists.push(twist);
159
+ }
160
+ return { path: points, scales: sampledScales, frames: sampledFrames, twists };
161
+ }
@@ -0,0 +1,96 @@
1
+ import { GeometryError } from "../errors.js";
2
+ import { add3, rotateAroundAxis3, scale3 } from "../math/vector3.js";
3
+ import { validateTriangleMesh } from "../mesh.js";
4
+ import { resolveGeometryTolerance } from "../tolerance.js";
5
+ import { resolveSweepScales } from "./options.js";
6
+ import { computeSweepFrames, validateSweepOptions } from "./orientation.js";
7
+ import { planSweepStations, refineSweepStations } from "./stations.js";
8
+ import { assertSweepAllocation, normalizeSweepPath, normalizeSweepProfile, writeSweepVertex, } from "./validation.js";
9
+ /** @internal Checks host allocation policy before frames or manufacturing buffers. */
10
+ export const buildClosedSweepMesh = (profile, path, options = {}, allocationGuard) => {
11
+ const operation = "sweep";
12
+ validateSweepOptions(options);
13
+ profile = normalizeSweepProfile(profile, operation, options.limits);
14
+ path = normalizeSweepPath(path, true, operation, options.limits);
15
+ assertSweepAllocation(path.length, profile.length, true, operation, allocationGuard, options.limits);
16
+ let scales = resolveSweepScales(options.scale, path.length);
17
+ const seamProfileOffset = options.seamProfileOffset ?? 0;
18
+ if (!Number.isSafeInteger(seamProfileOffset)) {
19
+ throw new GeometryError("INVALID_ARGUMENT", "seamProfileOffset must be a safe integer.", {
20
+ operation,
21
+ });
22
+ }
23
+ const twistRadians = ((options.twistDegrees ?? 0) / 180) * Math.PI;
24
+ if (!Number.isFinite(twistRadians)) {
25
+ throw new GeometryError("NON_FINITE_VALUE", "Sweep twistDegrees must be finite.", {
26
+ operation,
27
+ });
28
+ }
29
+ const ringVertexCount = profile.length;
30
+ const normalizedSeamProfileOffset = ((seamProfileOffset % ringVertexCount) + ringVertexCount) % ringVertexCount;
31
+ const profileScale = profile.reduce((extent, [x, y]) => Math.max(extent, Math.abs(x), Math.abs(y)), 1);
32
+ const seamTolerance = resolveGeometryTolerance(profileScale).linear;
33
+ const cosine = Math.cos(twistRadians);
34
+ const sine = Math.sin(twistRadians);
35
+ for (let index = 0; index < profile.length; index++) {
36
+ const [profileX, profileY] = profile[index];
37
+ const [scaleX, scaleY] = scales[0];
38
+ const x = profileX * scaleX;
39
+ const y = profileY * scaleY;
40
+ const target = profile[(index + normalizedSeamProfileOffset) % profile.length];
41
+ if (Math.hypot(x * cosine - y * sine - target[0] * scaleX, x * sine + y * cosine - target[1] * scaleY) > seamTolerance) {
42
+ throw new GeometryError("INVALID_TOPOLOGY", "Sweep twist must end at the seam's corresponding profile orientation.", { operation });
43
+ }
44
+ }
45
+ if (twistRadians !== 0) {
46
+ const plan = planSweepStations([profile], path, scales, options);
47
+ assertSweepAllocation(plan.stationCount, profile.length, true, operation, allocationGuard, options.limits);
48
+ }
49
+ let transportedFrames = computeSweepFrames(path, options);
50
+ let twists;
51
+ if (twistRadians !== 0) {
52
+ const plan = planSweepStations([profile], path, scales, options, transportedFrames);
53
+ assertSweepAllocation(plan.stationCount, profile.length, true, operation, allocationGuard, options.limits);
54
+ const refined = refineSweepStations(path, scales, transportedFrames, options, plan);
55
+ path = refined.path;
56
+ scales = refined.scales;
57
+ transportedFrames = refined.frames;
58
+ twists = refined.twists;
59
+ }
60
+ const positions = new Float32Array(path.length * ringVertexCount * 3);
61
+ for (let ring = 0; ring < path.length; ring += 1) {
62
+ const point = path[ring];
63
+ const tangent = transportedFrames[ring].tangent;
64
+ const twist = twists?.[ring] ?? twistRadians * (ring / path.length);
65
+ const baseNormal = transportedFrames[ring].normal;
66
+ const normal = rotateAroundAxis3(baseNormal, tangent, twist);
67
+ const binormal = rotateAroundAxis3(transportedFrames[ring].binormal, tangent, twist);
68
+ const [scaleX, scaleY] = scales[ring];
69
+ for (let profileIndex = 0; profileIndex < profile.length; profileIndex += 1) {
70
+ const [x, y] = profile[profileIndex];
71
+ const vertex = add3(point, add3(scale3(normal, x * scaleX), scale3(binormal, y * scaleY)));
72
+ writeSweepVertex(positions, (ring * ringVertexCount + profileIndex) * 3, vertex, operation);
73
+ }
74
+ }
75
+ const indices = new Uint32Array(path.length * ringVertexCount * 2 * 3);
76
+ let cursor = 0;
77
+ for (let ring = 0; ring < path.length; ring += 1) {
78
+ const nextRing = (ring + 1) % path.length;
79
+ const nextProfileOffset = nextRing === 0 ? normalizedSeamProfileOffset : 0;
80
+ for (let profileIndex = 0; profileIndex < ringVertexCount; profileIndex += 1) {
81
+ const following = (profileIndex + 1) % ringVertexCount;
82
+ const a = ring * ringVertexCount + profileIndex;
83
+ const b = ring * ringVertexCount + following;
84
+ const c = nextRing * ringVertexCount + ((following + nextProfileOffset) % ringVertexCount);
85
+ const d = nextRing * ringVertexCount + ((profileIndex + nextProfileOffset) % ringVertexCount);
86
+ indices[cursor] = a;
87
+ indices[cursor + 1] = b;
88
+ indices[cursor + 2] = c;
89
+ indices[cursor + 3] = a;
90
+ indices[cursor + 4] = c;
91
+ indices[cursor + 5] = d;
92
+ cursor += 6;
93
+ }
94
+ }
95
+ return validateTriangleMesh({ indices, positions });
96
+ };
@@ -0,0 +1,20 @@
1
+ import { GeometryError } from "../errors.js";
2
+ import { validateSweepOptions } from "./orientation.js";
3
+ import { buildClosedSweepMesh } from "./sweep-closed.js";
4
+ import { buildOpenSweepMesh } from "./sweep-open.js";
5
+ /** Sweeps one explicitly ordered point loop, with caps or a geometrically matching closed seam. */
6
+ export function sweepMesh(profile, path, options = {}) {
7
+ return buildSweepMesh(profile, path, options);
8
+ }
9
+ /** @internal Applies host ceilings before frames, caps, or manufacturing buffers. */
10
+ export function buildSweepMesh(profile, path, options = {}, allocationGuard) {
11
+ validateSweepOptions(options);
12
+ if (options.closed === true) {
13
+ return buildClosedSweepMesh(profile, path, options, allocationGuard);
14
+ }
15
+ if ((options.seamProfileOffset ?? 0) !== 0)
16
+ throw new GeometryError("INVALID_ARGUMENT", "A seamProfileOffset requires a closed path.", {
17
+ operation: "sweep",
18
+ });
19
+ return buildOpenSweepMesh(profile, path, options, allocationGuard);
20
+ }
@@ -0,0 +1,76 @@
1
+ import { triangulateLoop2 } from "../loft.js";
2
+ import { add3, rotateAroundAxis3, scale3 } from "../math/vector3.js";
3
+ import { validateTriangleMesh } from "../mesh.js";
4
+ import { resolveSweepScales } from "./options.js";
5
+ import { computeSweepFrames, validateSweepOptions } from "./orientation.js";
6
+ import { planSweepStations, refineSweepStations } from "./stations.js";
7
+ import { assertSweepAllocation, normalizeSweepPath, normalizeSweepProfile, writeSweepVertex, } from "./validation.js";
8
+ /** @internal Checks host allocation policy before frames, caps or manufacturing buffers. */
9
+ export const buildOpenSweepMesh = (profile, path, options = {}, allocationGuard) => {
10
+ const operation = "sweep";
11
+ validateSweepOptions(options);
12
+ profile = normalizeSweepProfile(profile, operation, options.limits);
13
+ path = normalizeSweepPath(path, false, operation, options.limits);
14
+ assertSweepAllocation(path.length, profile.length, false, operation, allocationGuard, options.limits);
15
+ let scales = resolveSweepScales(options.scale, path.length);
16
+ const twistRadians = ((options.twistDegrees ?? 0) / 180) * Math.PI;
17
+ if (twistRadians !== 0) {
18
+ const plan = planSweepStations([profile], path, scales, options);
19
+ assertSweepAllocation(plan.stationCount, profile.length, false, operation, allocationGuard, options.limits);
20
+ }
21
+ let frames = computeSweepFrames(path, options);
22
+ let twists;
23
+ if (twistRadians !== 0) {
24
+ const plan = planSweepStations([profile], path, scales, options, frames);
25
+ assertSweepAllocation(plan.stationCount, profile.length, false, operation, allocationGuard, options.limits);
26
+ const refined = refineSweepStations(path, scales, frames, options, plan);
27
+ path = refined.path;
28
+ scales = refined.scales;
29
+ frames = refined.frames;
30
+ twists = refined.twists;
31
+ }
32
+ const caps = triangulateLoop2(profile);
33
+ const ringVertexCount = profile.length;
34
+ const sideVertexCount = path.length * ringVertexCount;
35
+ const positions = new Float32Array(sideVertexCount * 3);
36
+ for (let ring = 0; ring < path.length; ring += 1) {
37
+ const point = path[ring];
38
+ const frame = frames[ring];
39
+ const twist = twists?.[ring] ?? (twistRadians * ring) / (path.length - 1);
40
+ const normal = rotateAroundAxis3(frame.normal, frame.tangent, twist);
41
+ const binormal = rotateAroundAxis3(frame.binormal, frame.tangent, twist);
42
+ const [scaleX, scaleY] = scales[ring];
43
+ for (let profileIndex = 0; profileIndex < profile.length; profileIndex += 1) {
44
+ const [x, y] = profile[profileIndex];
45
+ const vertex = add3(point, add3(scale3(normal, x * scaleX), scale3(binormal, y * scaleY)));
46
+ writeSweepVertex(positions, (ring * ringVertexCount + profileIndex) * 3, vertex, operation);
47
+ }
48
+ }
49
+ const triangleCount = (path.length - 1) * ringVertexCount * 2 + (caps.length / 3) * 2;
50
+ const indices = new Uint32Array(triangleCount * 3);
51
+ let cursor = 0;
52
+ const triangle = (a, b, c) => {
53
+ indices[cursor] = a;
54
+ indices[cursor + 1] = b;
55
+ indices[cursor + 2] = c;
56
+ cursor += 3;
57
+ };
58
+ for (let ring = 0; ring < path.length - 1; ring += 1) {
59
+ const current = ring * ringVertexCount;
60
+ const next = current + ringVertexCount;
61
+ for (let profileIndex = 0; profileIndex < ringVertexCount; profileIndex += 1) {
62
+ const following = (profileIndex + 1) % ringVertexCount;
63
+ triangle(current + profileIndex, current + following, next + following);
64
+ triangle(current + profileIndex, next + following, next + profileIndex);
65
+ }
66
+ }
67
+ const finalRing = (path.length - 1) * ringVertexCount;
68
+ for (let offset = 0; offset < caps.length; offset += 3) {
69
+ const a = caps[offset];
70
+ const b = caps[offset + 1];
71
+ const c = caps[offset + 2];
72
+ triangle(a, c, b);
73
+ triangle(finalRing + a, finalRing + b, finalRing + c);
74
+ }
75
+ return validateTriangleMesh({ positions, indices });
76
+ };
@@ -0,0 +1,116 @@
1
+ import { GeometryError } from "../errors.js";
2
+ import { assertMeshAllocationWithinLimits, resolveModelingLimits, } from "../model-limits.js";
3
+ import { writeMeshVertex } from "../numeric.js";
4
+ import { normalizeLoop2 } from "../regions.js";
5
+ import { assertSamplingCount, resolveGeometrySamplingLimits, } from "../sampling-limits.js";
6
+ import { resolveGeometryTolerance } from "../tolerance.js";
7
+ /** @internal Normalizes simple profiles before cap triangulation or side construction. */
8
+ export function normalizeSweepProfile(profile, operation, limits = {}) {
9
+ const hasArrayShape = Array.isArray(profile);
10
+ if (!hasArrayShape) {
11
+ throw new GeometryError("INVALID_ARGUMENT", "Sweep profiles must be arrays of XY points.", {
12
+ operation,
13
+ });
14
+ }
15
+ if (profile.length < 3) {
16
+ throw new GeometryError("DEGENERATE_GEOMETRY", "A sweep profile needs at least three points.", {
17
+ operation,
18
+ });
19
+ }
20
+ assertSamplingCount(profile.length, operation, resolveGeometrySamplingLimits(limits).maxSweepProfilePoints);
21
+ for (const point of profile) {
22
+ const hasPointShape = Array.isArray(point);
23
+ if (!hasPointShape || point.length !== 2) {
24
+ throw new GeometryError("INVALID_ARGUMENT", "Sweep profile points must be exact XY tuples.", {
25
+ operation,
26
+ });
27
+ }
28
+ }
29
+ return normalizeLoop2(profile, true);
30
+ }
31
+ /** @internal Cleans redundant path samples and validates finite, non-degenerate centered tangents. */
32
+ export function normalizeSweepPath(path, closed, operation, limits = {}) {
33
+ const hasArrayShape = Array.isArray(path);
34
+ if (!hasArrayShape) {
35
+ throw new GeometryError("INVALID_ARGUMENT", "Sweep paths must be arrays of XYZ points.", {
36
+ operation,
37
+ });
38
+ }
39
+ assertSamplingCount(path.length, operation, resolveGeometrySamplingLimits(limits).maxPathPoints);
40
+ const min = [Infinity, Infinity, Infinity];
41
+ const max = [-Infinity, -Infinity, -Infinity];
42
+ for (const point of path) {
43
+ const hasPointShape = Array.isArray(point);
44
+ if (!hasPointShape || point.length !== 3) {
45
+ throw new GeometryError("INVALID_ARGUMENT", "Sweep path points must be exact XYZ tuples.", {
46
+ operation,
47
+ });
48
+ }
49
+ if (![point[0], point[1], point[2]].every((coordinate) => Number.isFinite(coordinate) && Number.isFinite(Math.fround(coordinate)))) {
50
+ throw new GeometryError("NON_FINITE_VALUE", "Sweep path coordinates must be finite XYZ points.", {
51
+ operation,
52
+ });
53
+ }
54
+ for (let axis = 0; axis < 3; axis += 1) {
55
+ min[axis] = Math.min(min[axis], point[axis]);
56
+ max[axis] = Math.max(max[axis], point[axis]);
57
+ }
58
+ }
59
+ const extent = path.length === 0 ? 0 : Math.max(max[0] - min[0], max[1] - min[1], max[2] - min[2]);
60
+ const tolerance = resolveGeometryTolerance(extent).linear;
61
+ const distance = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
62
+ const cleaned = [];
63
+ for (const point of path) {
64
+ if (cleaned.length === 0 || distance(point, cleaned.at(-1)) > tolerance) {
65
+ cleaned.push(Object.freeze([point[0], point[1], point[2]]));
66
+ }
67
+ }
68
+ if (cleaned.length > 1 && distance(cleaned[0], cleaned.at(-1)) <= tolerance) {
69
+ if (closed)
70
+ cleaned.pop();
71
+ else
72
+ throw new GeometryError("INVALID_TOPOLOGY", "An open sweep path cannot have a closed seam.", {
73
+ operation,
74
+ });
75
+ }
76
+ const minimum = closed ? 3 : 2;
77
+ if (cleaned.length < minimum) {
78
+ throw new GeometryError("DEGENERATE_GEOMETRY", `A ${closed ? "closed" : "open"} sweep path needs at least ${closed ? "three" : "two"} points.`, { operation });
79
+ }
80
+ for (let index = closed ? 0 : 1; index < (closed ? cleaned.length : cleaned.length - 1); index++) {
81
+ const previous = cleaned[(index - 1 + cleaned.length) % cleaned.length];
82
+ const next = cleaned[(index + 1) % cleaned.length];
83
+ if (distance(previous, next) <= tolerance) {
84
+ throw new GeometryError("DEGENERATE_GEOMETRY", "Sweep paths cannot reverse through a zero-length centered tangent.", { operation });
85
+ }
86
+ }
87
+ return Object.freeze(cleaned);
88
+ }
89
+ /** @internal Checks output allocation before creating frames or buffers. */
90
+ export function assertSweepAllocation(pathCount, profileCount, closed, operation, allocationGuard, limits = {}) {
91
+ const vertexCount = pathCount * profileCount;
92
+ const triangleCount = (closed ? pathCount : pathCount - 1) * profileCount * 2 + (closed ? 0 : (profileCount - 2) * 2);
93
+ assertMeshAllocationWithinLimits(vertexCount, triangleCount, (vertexCount + triangleCount) * 12, operation, resolveModelingLimits(limits));
94
+ allocationGuard?.(vertexCount, triangleCount, (vertexCount + triangleCount) * 12, operation);
95
+ }
96
+ /** @internal Rejects coordinates that cannot be represented in manufacturing mesh buffers. */
97
+ export function writeSweepVertex(positions, offset, point, operation) {
98
+ writeMeshVertex(positions, offset, point, operation);
99
+ }
100
+ /** @internal Checks user-provided orientation controls before vector normalization. */
101
+ export function validateSweepNormal(normal, operation) {
102
+ const hasArrayShape = Array.isArray(normal);
103
+ if (!hasArrayShape || normal.length !== 3) {
104
+ throw new GeometryError("INVALID_ARGUMENT", "Sweep normals must be exact XYZ tuples.", {
105
+ operation,
106
+ });
107
+ }
108
+ if (![normal[0], normal[1], normal[2]].every(Number.isFinite)) {
109
+ throw new GeometryError("NON_FINITE_VALUE", "Sweep normal coordinates must be finite XYZ values.", { operation });
110
+ }
111
+ if (Math.max(...normal.map(Math.abs)) === 0) {
112
+ throw new GeometryError("DEGENERATE_GEOMETRY", "Sweep normals must have non-zero length.", {
113
+ operation,
114
+ });
115
+ }
116
+ }
@@ -0,0 +1,32 @@
1
+ import { GeometryError } from "./errors.js";
2
+ import { flattenSvgPath, SvgVertexBudget } from "./svg/path.js";
3
+ /**
4
+ * Samples a prepared text layout without shaping text again or loading a font.
5
+ *
6
+ * @param layout - Immutable millimetre paths and measurements retained from text preparation.
7
+ * @param resolution - Explicit curve sampling policy; omitted uses the pure default policy.
8
+ * @param limits - Explicit profile and circular allocation ceilings.
9
+ * @returns Frozen contours with original glyph winding and no repeated closing vertex.
10
+ * @throws GeometryError for empty outlines, invalid paths or policies, over 1000 paths or selected sampling ceilings.
11
+ */
12
+ export function sampleTextContours(layout, resolution = {}, limits = {}) {
13
+ const operation = "sampleTextContours";
14
+ if (!Array.isArray(layout.paths) || layout.paths.length === 0) {
15
+ throw new GeometryError("DEGENERATE_GEOMETRY", "Prepared text has no visible outlines.", {
16
+ operation,
17
+ });
18
+ }
19
+ if (layout.paths.length > 1000) {
20
+ throw new GeometryError("COMPLEXITY_LIMIT", "Prepared text exceeds 1000 input paths.", {
21
+ operation,
22
+ });
23
+ }
24
+ const budget = new SvgVertexBudget(limits);
25
+ const contours = layout.paths.flatMap((path) => flattenSvgPath(path, [1, 0, 0, 1, 0, 0], resolution, budget, operation));
26
+ if (contours.length === 0) {
27
+ throw new GeometryError("DEGENERATE_GEOMETRY", "Prepared text has no visible outlines.", {
28
+ operation,
29
+ });
30
+ }
31
+ return Object.freeze(contours.map((contour) => Object.freeze(contour.map((point) => Object.freeze(point)))));
32
+ }
@@ -0,0 +1 @@
1
+ export {};