@metamesh/modeling 0.0.0-bootstrap.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/README.md +313 -6
  3. package/THIRD_PARTY_NOTICES.md +153 -0
  4. package/dist/api/engine.d.ts +54 -0
  5. package/dist/api/geometry.d.ts +2912 -0
  6. package/dist/api/index.d.ts +1648 -0
  7. package/dist/api/text.d.ts +77 -0
  8. package/dist/engine/construction-operations.js +189 -0
  9. package/dist/engine/execution.js +227 -0
  10. package/dist/engine/export-3mf.js +100 -0
  11. package/dist/engine/extrusion-finishing.js +82 -0
  12. package/dist/engine/finalization.js +214 -0
  13. package/dist/engine/index.js +72 -0
  14. package/dist/engine/inspection-operations.js +52 -0
  15. package/dist/engine/kernel/LICENSE +201 -0
  16. package/dist/engine/kernel/manifold.js +2 -0
  17. package/dist/engine/kernel/manifold.wasm +0 -0
  18. package/dist/engine/loft-operations.js +156 -0
  19. package/dist/engine/mechanical-operations.js +51 -0
  20. package/dist/engine/modeling-context.js +38 -0
  21. package/dist/engine/native/adapter.js +20 -0
  22. package/dist/engine/native/context.js +209 -0
  23. package/dist/engine/native/copied-views.js +46 -0
  24. package/dist/engine/native/index.js +13 -0
  25. package/dist/engine/native/operations.js +115 -0
  26. package/dist/engine/native/ownership.js +98 -0
  27. package/dist/engine/native/regions.js +138 -0
  28. package/dist/engine/native/scoped-primitives.js +127 -0
  29. package/dist/engine/native/scoped-threads.js +34 -0
  30. package/dist/engine/native/threads.js +52 -0
  31. package/dist/engine/native/transforms.js +111 -0
  32. package/dist/engine/operation-environment.js +20 -0
  33. package/dist/engine/pattern-operations.js +36 -0
  34. package/dist/engine/pattern-placements.js +176 -0
  35. package/dist/engine/primitive-anchors.js +70 -0
  36. package/dist/engine/primitive-correspondence.js +74 -0
  37. package/dist/engine/primitive-operations.js +151 -0
  38. package/dist/engine/primitive-profile-types.js +1 -0
  39. package/dist/engine/primitive-profiles.js +250 -0
  40. package/dist/engine/profile-mesh.js +279 -0
  41. package/dist/engine/section-operations.js +93 -0
  42. package/dist/engine/shape-anchors.js +88 -0
  43. package/dist/engine/transform-operations.js +225 -0
  44. package/dist/engine/types.js +1 -0
  45. package/dist/geometry/anchors.js +226 -0
  46. package/dist/geometry/curves/evaluation.js +162 -0
  47. package/dist/geometry/curves/sampling.js +265 -0
  48. package/dist/geometry/curves/types.js +1 -0
  49. package/dist/geometry/cylinder.js +117 -0
  50. package/dist/geometry/edge-finishing.js +160 -0
  51. package/dist/geometry/errors.js +76 -0
  52. package/dist/geometry/extrusion.js +150 -0
  53. package/dist/geometry/heightfield.js +107 -0
  54. package/dist/geometry/index.js +42 -0
  55. package/dist/geometry/loft-correspondence.js +204 -0
  56. package/dist/geometry/loft-geometry.js +184 -0
  57. package/dist/geometry/loft-options.js +56 -0
  58. package/dist/geometry/loft-simple-mesh.js +92 -0
  59. package/dist/geometry/loft-triangulation.js +64 -0
  60. package/dist/geometry/loft.js +80 -0
  61. package/dist/geometry/math/vector3.js +41 -0
  62. package/dist/geometry/mechanical/gear-motion.js +94 -0
  63. package/dist/geometry/mechanical/gears.js +484 -0
  64. package/dist/geometry/mechanical/index.js +13 -0
  65. package/dist/geometry/mechanical/standards.js +67 -0
  66. package/dist/geometry/mechanical/thread-helices.js +44 -0
  67. package/dist/geometry/mechanical/thread-mesh-plan.js +358 -0
  68. package/dist/geometry/mechanical/thread-mesh.js +142 -0
  69. package/dist/geometry/mechanical/thread-profiles.js +368 -0
  70. package/dist/geometry/mechanical/thread-solids.js +230 -0
  71. package/dist/geometry/mechanical/thread-surface.js +433 -0
  72. package/dist/geometry/mechanical/threads.js +138 -0
  73. package/dist/geometry/mesh-planar.js +408 -0
  74. package/dist/geometry/mesh-quality.js +396 -0
  75. package/dist/geometry/mesh-signature.js +137 -0
  76. package/dist/geometry/mesh.js +300 -0
  77. package/dist/geometry/model-limits.js +50 -0
  78. package/dist/geometry/numeric.js +11 -0
  79. package/dist/geometry/paths/frames.js +118 -0
  80. package/dist/geometry/paths/helix.js +49 -0
  81. package/dist/geometry/paths/path.js +30 -0
  82. package/dist/geometry/paths/planar.js +135 -0
  83. package/dist/geometry/paths/resampling.js +124 -0
  84. package/dist/geometry/polygon-clipping.js +52 -0
  85. package/dist/geometry/primitive-options.js +1 -0
  86. package/dist/geometry/primitive-resolution.js +17 -0
  87. package/dist/geometry/primitive-resolvers.js +235 -0
  88. package/dist/geometry/profiles/matched.js +184 -0
  89. package/dist/geometry/profiles/open-profile.js +137 -0
  90. package/dist/geometry/profiles/profiles.js +70 -0
  91. package/dist/geometry/profiles/rectangle.js +235 -0
  92. package/dist/geometry/profiles/shapes.js +34 -0
  93. package/dist/geometry/profiles/span-sampling.js +28 -0
  94. package/dist/geometry/profiles/types.js +1 -0
  95. package/dist/geometry/profiles/validation.js +20 -0
  96. package/dist/geometry/regions.js +371 -0
  97. package/dist/geometry/resolution.js +236 -0
  98. package/dist/geometry/revolve/revolve-mesh.js +123 -0
  99. package/dist/geometry/sampling-limits.js +29 -0
  100. package/dist/geometry/stroke-path.js +181 -0
  101. package/dist/geometry/svg/errors.js +23 -0
  102. package/dist/geometry/svg/fill.js +172 -0
  103. package/dist/geometry/svg/index.js +1 -0
  104. package/dist/geometry/svg/matrix.js +130 -0
  105. package/dist/geometry/svg/parser.js +536 -0
  106. package/dist/geometry/svg/path.js +349 -0
  107. package/dist/geometry/svg/types.js +1 -0
  108. package/dist/geometry/svg-import.js +662 -0
  109. package/dist/geometry/sweeps/options.js +28 -0
  110. package/dist/geometry/sweeps/orientation.js +106 -0
  111. package/dist/geometry/sweeps/stations.js +161 -0
  112. package/dist/geometry/sweeps/sweep-closed.js +96 -0
  113. package/dist/geometry/sweeps/sweep-mesh.js +20 -0
  114. package/dist/geometry/sweeps/sweep-open.js +76 -0
  115. package/dist/geometry/sweeps/validation.js +116 -0
  116. package/dist/geometry/text-contours.js +32 -0
  117. package/dist/geometry/text-layout.js +1 -0
  118. package/dist/geometry/tolerance.js +53 -0
  119. package/dist/geometry/transform-geometry.js +41 -0
  120. package/dist/geometry/transform3.js +340 -0
  121. package/dist/geometry/types.js +1 -0
  122. package/dist/index.js +1 -0
  123. package/dist/model/execution-binding.js +30 -0
  124. package/dist/model/operations.js +152 -0
  125. package/dist/model/options.js +1 -0
  126. package/dist/model/types.js +1 -0
  127. package/dist/text/index.js +48 -0
  128. package/dist/text/vector-text-layout.js +279 -0
  129. package/package.json +89 -8
@@ -0,0 +1,127 @@
1
+ import { resolveCylinderPrimitive } from "../../geometry/cylinder.js";
2
+ import { assertMeshAllocationWithinLimits, resolveModelingLimits, } from "../../geometry/model-limits.js";
3
+ import { boxFaceAxes, resolveBoxPrimitive, resolveSpherePrimitive, resolveTorusPrimitive, resolveTubePrimitive, } from "../../geometry/primitive-resolvers.js";
4
+ import { ownScopedManifoldValue, scopedManifoldModule, } from "./context.js";
5
+ import { transform } from "./transforms.js";
6
+ const meshBudget = (vertices, triangles, operation, limits) => assertMeshAllocationWithinLimits(vertices, triangles, 12 * (vertices + triangles), operation, resolveModelingLimits(limits));
7
+ const sphereCounts = (segments) => {
8
+ const frequency = Math.ceil(segments / 4);
9
+ return { vertices: 4 * frequency * frequency + 2, triangles: 8 * frequency * frequency };
10
+ };
11
+ /** Creates an axis-aligned scope-owned box. */
12
+ export function box(context, options) {
13
+ const { center, dimensions, treatment } = resolveBoxPrimitive(options);
14
+ if (treatment?.kind === "selected") {
15
+ const module = scopedManifoldModule(context);
16
+ let result = ownScopedManifoldValue(context, module.Manifold.cube(dimensions, center));
17
+ for (const { edge, profile } of treatment.cuts) {
18
+ const section = ownScopedManifoldValue(context, new module.CrossSection([[[0, 0], ...profile.map(([x, y]) => [x, y])]]));
19
+ const [first, second] = edge.split("-");
20
+ const [a, signA] = boxFaceAxes[first], [b, signB] = boxFaceAxes[second];
21
+ const axis = 3 - a - b;
22
+ const origin = dimensions.map((size) => (center ? -size / 2 : 0));
23
+ origin[a] = (center ? signA / 2 : signA > 0 ? 1 : 0) * dimensions[a];
24
+ origin[b] = (center ? signB / 2 : signB > 0 ? 1 : 0) * dimensions[b];
25
+ const matrix = [0, 0, 0, origin[0], 0, 0, 0, origin[1], 0, 0, 0, origin[2], 0, 0, 0, 1];
26
+ matrix[b * 4] = -signB;
27
+ matrix[a * 4 + 1] = -signA;
28
+ matrix[axis * 4 + 2] = 1;
29
+ const cutter = ownScopedManifoldValue(context, section.extrude(dimensions[axis]));
30
+ result = ownScopedManifoldValue(context, result.subtract(transform(context, cutter, matrix)));
31
+ }
32
+ meshBudget(result.numVert(), result.numTri(), "box", options.limits);
33
+ return result;
34
+ }
35
+ if (treatment === undefined)
36
+ meshBudget(8, 12, "box", options.limits);
37
+ else if (treatment.kind === "chamfer")
38
+ meshBudget(24, 44, "box", options.limits);
39
+ else {
40
+ const vertices = 8 * sphereCounts(treatment.segments).vertices;
41
+ meshBudget(vertices, 2 * vertices - 4, "box", options.limits);
42
+ }
43
+ const module = scopedManifoldModule(context);
44
+ if (treatment === undefined) {
45
+ return ownScopedManifoldValue(context, module.Manifold.cube(dimensions, center));
46
+ }
47
+ const { inner } = treatment;
48
+ let treated;
49
+ if (treatment.kind === "round") {
50
+ const core = ownScopedManifoldValue(context, module.Manifold.cube(inner, true));
51
+ const ball = ownScopedManifoldValue(context, module.Manifold.sphere(treatment.radius, treatment.segments));
52
+ treated = ownScopedManifoldValue(context, core.minkowskiSum(ball));
53
+ }
54
+ else {
55
+ const cores = [
56
+ ownScopedManifoldValue(context, module.Manifold.cube([dimensions[0], inner[1], inner[2]], true)),
57
+ ownScopedManifoldValue(context, module.Manifold.cube([inner[0], dimensions[1], inner[2]], true)),
58
+ ownScopedManifoldValue(context, module.Manifold.cube([inner[0], inner[1], dimensions[2]], true)),
59
+ ];
60
+ treated = ownScopedManifoldValue(context, module.Manifold.hull(cores));
61
+ }
62
+ return center
63
+ ? treated
64
+ : ownScopedManifoldValue(context, treated.translate([dimensions[0] / 2, dimensions[1] / 2, dimensions[2] / 2]));
65
+ }
66
+ /**
67
+ * Creates a scope-owned cylinder, cone, or frustum along +Z.
68
+ *
69
+ * A scalar radius is uniform; a tuple is `[bottom, top]` in +Z order. End
70
+ * treatments use cap inset and sloped-side setback coordinates; roundovers retain actual radii.
71
+ */
72
+ export function cylinder(context, options) {
73
+ const resolved = resolveCylinderPrimitive(options);
74
+ let vertices;
75
+ if (resolved.profile === undefined)
76
+ vertices =
77
+ resolved.bottomRadius === 0 || resolved.topRadius === 0
78
+ ? resolved.segments + 1
79
+ : 2 * resolved.segments;
80
+ else
81
+ vertices = resolved.profile.reduce((count, [radius]) => count + (radius > 0 ? resolved.segments : 1), 0);
82
+ meshBudget(vertices, Math.max(0, 2 * vertices - 4), "cylinder", options.limits);
83
+ const module = scopedManifoldModule(context);
84
+ if (resolved.profile === undefined) {
85
+ return ownScopedManifoldValue(context, module.Manifold.cylinder(resolved.height, resolved.bottomRadius, resolved.topRadius, resolved.segments, resolved.center));
86
+ }
87
+ const contour = resolved.profile.map(([radius, z]) => [radius, z]);
88
+ const section = ownScopedManifoldValue(context, new module.CrossSection([contour]));
89
+ return ownScopedManifoldValue(context, section.revolve(resolved.segments));
90
+ }
91
+ /** Creates a scope-owned hollow tube along +Z. */
92
+ export function tube(context, options) {
93
+ resolveTubePrimitive(options);
94
+ const segments = resolveCylinderPrimitive({ ...options, radius: options.outerRadius }).segments;
95
+ meshBudget(4 * segments, 8 * segments, "tube", options.limits);
96
+ const outer = cylinder(context, {
97
+ ...options,
98
+ radius: options.outerRadius,
99
+ });
100
+ const inner = cylinder(context, {
101
+ ...options,
102
+ center: true,
103
+ height: options.height + 0.02,
104
+ radius: options.innerRadius,
105
+ });
106
+ const positionedInner = options.center === false
107
+ ? ownScopedManifoldValue(context, inner.translate([0, 0, options.height / 2]))
108
+ : inner;
109
+ return ownScopedManifoldValue(context, outer.subtract(positionedInner));
110
+ }
111
+ /** Creates a scope-owned sphere centered at the origin. */
112
+ export function sphere(context, options) {
113
+ const { radius, segments } = resolveSpherePrimitive(options);
114
+ const counts = sphereCounts(segments);
115
+ meshBudget(counts.vertices, counts.triangles, "sphere", options.limits);
116
+ return ownScopedManifoldValue(context, scopedManifoldModule(context).Manifold.sphere(radius, segments));
117
+ }
118
+ /** Creates a scope-owned torus centered around +Z. */
119
+ export function torus(context, options) {
120
+ const { profileSegments, sweepSegments } = resolveTorusPrimitive(options);
121
+ const vertices = profileSegments * sweepSegments;
122
+ meshBudget(vertices, 2 * vertices, "torus", options.limits);
123
+ const module = scopedManifoldModule(context);
124
+ const circle = ownScopedManifoldValue(context, module.CrossSection.circle(options.minorRadius, profileSegments));
125
+ const translated = ownScopedManifoldValue(context, circle.translate([options.majorRadius, 0]));
126
+ return ownScopedManifoldValue(context, translated.revolve(sweepSegments));
127
+ }
@@ -0,0 +1,34 @@
1
+ import { scopedManifoldModule, scopedManifoldScope, trackScopedManifoldValue, } from "./context.js";
2
+ import { internalThreadCutterSolid, preflightInternalThreadCutterSolid, preflightThreadedNutSolid, preflightThreadedRodSolid, threadedNutSolid, threadedRodSolid, threadRidgeSolid, } from "./threads.js";
3
+ /** Converts a runtime-neutral helical ridge mesh into a context-owned Manifold. */
4
+ export function threadRidge(context, options) {
5
+ const solid = threadRidgeSolid(scopedManifoldModule(context), options, scopedManifoldScope(context));
6
+ return trackScopedManifoldValue(context, solid);
7
+ }
8
+ /** Preflights the exact compensated direct mesh used by {@link threadedRod}. */
9
+ export function preflightThreadedRod(options) {
10
+ return preflightThreadedRodSolid(options);
11
+ }
12
+ /** Preflights the exact extended mesh used by {@link internalThreadCutter}. */
13
+ export function preflightInternalThreadCutter(options) {
14
+ return preflightInternalThreadCutterSolid(options);
15
+ }
16
+ /** Creates a compensated context-owned external threaded rod along +Z. */
17
+ export function threadedRod(context, options) {
18
+ const solid = threadedRodSolid(scopedManifoldModule(context), options, scopedManifoldScope(context));
19
+ return trackScopedManifoldValue(context, solid);
20
+ }
21
+ /** Creates a compensated context-owned internal-thread subtraction tool. */
22
+ export function internalThreadCutter(context, options) {
23
+ const solid = internalThreadCutterSolid(scopedManifoldModule(context), options, scopedManifoldScope(context));
24
+ return trackScopedManifoldValue(context, solid);
25
+ }
26
+ /** Preflights a nut's exact body, cutter, and chamfer geometry without WASM allocation. */
27
+ export function preflightThreadedNut(options) {
28
+ return preflightThreadedNutSolid(options);
29
+ }
30
+ /** Creates a context-owned nut body and subtracts its compensated internal thread. */
31
+ export function threadedNut(context, options) {
32
+ const solid = threadedNutSolid(scopedManifoldModule(context), options, scopedManifoldScope(context));
33
+ return trackScopedManifoldValue(context, solid);
34
+ }
@@ -0,0 +1,52 @@
1
+ import { buildHelicalThreadRidgeMesh, buildHelicalThreadSolidMesh, } from "../../geometry/mechanical/index.js";
2
+ import { resolveCompensatedThreadSolidMesh, resolveThreadedNutSolid, } from "../../geometry/mechanical/thread-solids.js";
3
+ import { triangleMeshToManifold } from "./adapter.js";
4
+ export { preflightThreadedRodSolid, preflightInternalThreadCutterSolid, preflightThreadedNutSolid, } from "../../geometry/mechanical/thread-solids.js";
5
+ /** Converts the runtime-neutral ridge mesh into a scope-owned Manifold solid. */
6
+ export function threadRidgeSolid(module, options, scope) {
7
+ return triangleMeshToManifold(module, buildHelicalThreadRidgeMesh(options), scope);
8
+ }
9
+ const compensatedThreadSolid = (module, options, role, scope) => {
10
+ const resolved = resolveCompensatedThreadSolidMesh(options, role);
11
+ return compensatedThreadSolidFromResolved(module, resolved, scope);
12
+ };
13
+ const compensatedThreadSolidFromResolved = (module, resolved, scope) => {
14
+ const { axialExtension, meshOptions } = resolved;
15
+ const combined = triangleMeshToManifold(module, buildHelicalThreadSolidMesh(meshOptions), scope);
16
+ return axialExtension === 0 ? combined : scope.own(combined.translate([0, 0, -axialExtension]));
17
+ };
18
+ /** Creates a compensated, scope-owned external threaded rod along +Z. */
19
+ export function threadedRodSolid(module, options, scope) {
20
+ return compensatedThreadSolid(module, options, "male", scope);
21
+ }
22
+ /**
23
+ * Creates a compensated, faceting-corrected internal-thread subtraction tool.
24
+ * The tool extends one pitch beyond both requested faces for a complete
25
+ * through-thread cavity.
26
+ */
27
+ export function internalThreadCutterSolid(module, options, scope) {
28
+ return compensatedThreadSolid(module, options, "female", scope);
29
+ }
30
+ /** Creates a scope-owned nut body and subtracts its compensated internal thread. */
31
+ export function threadedNutSolid(module, options, scope) {
32
+ const resolved = resolveThreadedNutSolid(options, "threadedNutSolid");
33
+ const { bevelBottom, bevelTop, outerRadius } = resolved;
34
+ const { bodySegments: segments, chamferSize: chamfer } = resolved.preflight;
35
+ const bodyParts = [];
36
+ if (bevelBottom) {
37
+ bodyParts.push(scope.own(module.Manifold.cylinder(chamfer, outerRadius - chamfer, outerRadius, segments, false)));
38
+ }
39
+ const middleStart = bevelBottom ? chamfer : 0;
40
+ const middleEnd = options.thickness - (bevelTop ? chamfer : 0);
41
+ if (middleEnd > middleStart) {
42
+ const middle = scope.own(module.Manifold.cylinder(middleEnd - middleStart, outerRadius, outerRadius, segments, false));
43
+ bodyParts.push(middleStart === 0 ? middle : scope.own(middle.translate([0, 0, middleStart])));
44
+ }
45
+ if (bevelTop) {
46
+ const top = scope.own(module.Manifold.cylinder(chamfer, outerRadius, outerRadius - chamfer, segments, false));
47
+ bodyParts.push(scope.own(top.translate([0, 0, options.thickness - chamfer])));
48
+ }
49
+ const body = bodyParts.length === 1 ? bodyParts[0] : scope.own(module.Manifold.union(bodyParts));
50
+ const cutter = compensatedThreadSolidFromResolved(module, resolved.cutter, scope);
51
+ return scope.own(body.subtract(cutter));
52
+ }
@@ -0,0 +1,111 @@
1
+ import { attachTransform } from "../../geometry/anchors.js";
2
+ import { GeometryError } from "../../geometry/errors.js";
3
+ import { assertScopedManifoldValue, ownScopedManifoldValue, } from "./context.js";
4
+ const finiteVector = (value, name, operation) => {
5
+ if (!value.every(Number.isFinite)) {
6
+ throw new GeometryError("NON_FINITE_VALUE", `${name} coordinates must be finite.`, {
7
+ operation,
8
+ });
9
+ }
10
+ };
11
+ /** Translates a context-owned solid in world coordinates. */
12
+ export function translate(context, solid, offset) {
13
+ assertScopedManifoldValue(context, solid);
14
+ finiteVector(offset, "offset", "translate");
15
+ if (offset.every((value) => value === 0))
16
+ return solid;
17
+ return ownScopedManifoldValue(context, solid.translate(offset));
18
+ }
19
+ /**
20
+ * Rotates a context-owned solid about global X, then Y, then Z.
21
+ *
22
+ * `xyzDegrees` is an XYZ Euler-angle tuple in degrees. Scoped Manifold modeling
23
+ * uses degrees consistently; callers should convert radians at their boundary.
24
+ */
25
+ export function rotate(context, solid, xyzDegrees) {
26
+ assertScopedManifoldValue(context, solid);
27
+ finiteVector(xyzDegrees, "xyzDegrees", "rotate");
28
+ if (xyzDegrees.every((value) => value === 0))
29
+ return solid;
30
+ return ownScopedManifoldValue(context, solid.rotate(xyzDegrees));
31
+ }
32
+ /** Scales a context-owned solid about the world origin. */
33
+ export function scale(context, solid, factors) {
34
+ assertScopedManifoldValue(context, solid);
35
+ const values = typeof factors === "number" ? [factors] : factors;
36
+ if (!values.every(Number.isFinite) || values.some((value) => value === 0)) {
37
+ throw new GeometryError("SINGULAR_TRANSFORM", "Scale factors must be finite and non-zero.", {
38
+ operation: "scale",
39
+ });
40
+ }
41
+ if (values.every((value) => value === 1))
42
+ return solid;
43
+ return ownScopedManifoldValue(context, solid.scale(factors));
44
+ }
45
+ /** Reflects a context-owned solid across an origin plane. */
46
+ export function mirror(context, solid, normal) {
47
+ assertScopedManifoldValue(context, solid);
48
+ finiteVector(normal, "normal", "mirror");
49
+ if (normal.every((value) => value === 0)) {
50
+ throw new GeometryError("INVALID_ARGUMENT", "normal must be non-zero.", {
51
+ operation: "mirror",
52
+ });
53
+ }
54
+ return ownScopedManifoldValue(context, solid.mirror(normal));
55
+ }
56
+ /** Applies a row-major affine 4x4 transform to a context-owned solid. */
57
+ export function transform(context, solid, matrix) {
58
+ assertScopedManifoldValue(context, solid);
59
+ if (!matrix.every(Number.isFinite)) {
60
+ throw new GeometryError("NON_FINITE_VALUE", "Transform entries must be finite.", {
61
+ operation: "transform",
62
+ });
63
+ }
64
+ if (matrix[12] !== 0 || matrix[13] !== 0 || matrix[14] !== 0 || matrix[15] !== 1) {
65
+ throw new GeometryError("INVALID_ARGUMENT", "Transform must be affine with a [0, 0, 0, 1] final row.", { operation: "transform" });
66
+ }
67
+ if (matrix.every((value, index) => value === (index === 0 || index === 5 || index === 10 || index === 15 ? 1 : 0))) {
68
+ return solid;
69
+ }
70
+ const determinant = matrix[0] * (matrix[5] * matrix[10] - matrix[6] * matrix[9]) -
71
+ matrix[1] * (matrix[4] * matrix[10] - matrix[6] * matrix[8]) +
72
+ matrix[2] * (matrix[4] * matrix[9] - matrix[5] * matrix[8]);
73
+ if (!Number.isFinite(determinant) || determinant === 0) {
74
+ throw new GeometryError("SINGULAR_TRANSFORM", "Transform linear component must be non-singular.", { operation: "transform" });
75
+ }
76
+ const columnMajor = [
77
+ matrix[0],
78
+ matrix[4],
79
+ matrix[8],
80
+ matrix[12],
81
+ matrix[1],
82
+ matrix[5],
83
+ matrix[9],
84
+ matrix[13],
85
+ matrix[2],
86
+ matrix[6],
87
+ matrix[10],
88
+ matrix[14],
89
+ matrix[3],
90
+ matrix[7],
91
+ matrix[11],
92
+ matrix[15],
93
+ ];
94
+ return ownScopedManifoldValue(context, solid.transform(columnMajor));
95
+ }
96
+ /** Moves a local source anchor onto a world-space target anchor. */
97
+ export function attach(context, solid, source, target, options = {}) {
98
+ if (options.spinDegrees !== undefined && !Number.isFinite(options.spinDegrees)) {
99
+ throw new GeometryError("NON_FINITE_VALUE", "spinDegrees must be finite.", {
100
+ operation: "attach",
101
+ });
102
+ }
103
+ return transform(context, solid, attachTransform(source, target, {
104
+ ...(options.alignment === undefined ? {} : { alignment: options.alignment }),
105
+ ...(options.gap === undefined ? {} : { gap: options.gap }),
106
+ ...(options.spinDegrees === undefined
107
+ ? {}
108
+ : { spinRadians: (options.spinDegrees * Math.PI) / 180 }),
109
+ ...(options.tolerance === undefined ? {} : { tolerance: options.tolerance }),
110
+ }));
111
+ }
@@ -0,0 +1,20 @@
1
+ import { resolveModelingLimits } from "../geometry/model-limits.js";
2
+ import { resolveCircularPrimitiveSegments } from "../geometry/primitive-resolution.js";
3
+ /** Applies explicit resolution inheritance once for this bound context. */
4
+ export function createOperationEnvironment(owner, resolution) {
5
+ const withResolutionDefaults = (value) => {
6
+ const limits = resolveModelingLimits(value.limits, owner.limits);
7
+ return {
8
+ ...value,
9
+ resolution: {
10
+ ...resolution,
11
+ maximumSegments: Math.min(resolution.maximumSegments ?? limits.maxCircularSegments, limits.maxCircularSegments),
12
+ minimumSegments: Math.min(resolution.minimumSegments ?? 16, limits.maxCircularSegments),
13
+ ...value.resolution,
14
+ },
15
+ limits,
16
+ };
17
+ };
18
+ const resolveCircularSegments = (radius, value) => resolveCircularPrimitiveSegments(radius, withResolutionDefaults(value), "model");
19
+ return { ...owner, resolution, withResolutionDefaults, resolveCircularSegments };
20
+ }
@@ -0,0 +1,36 @@
1
+ import { IDENTITY_TRANSFORM3 } from "../geometry/transform3.js";
2
+ import { gridPatternPlan, linearPatternPlan, pathPatternPlan, radialPatternPlan, } from "./pattern-placements.js";
3
+ /** Private pattern owner; all copies use the shared transform/metadata/lifetime path. */
4
+ export function createPatternOperations(environment, transforms) {
5
+ const apply = (source, makePlan, operation) => {
6
+ environment.assertActive();
7
+ const plan = makePlan();
8
+ let vertices = 0, triangles = 0;
9
+ const budget = (solid, multiplier) => {
10
+ const native = environment.nativeSolid(solid);
11
+ vertices += native.numVert() * multiplier;
12
+ triangles += native.numTri() * multiplier;
13
+ environment.assertMeshAllocation(vertices, triangles, (vertices + triangles) * 12, operation);
14
+ };
15
+ if (typeof source !== "function")
16
+ budget(source, plan.count);
17
+ const copies = [];
18
+ for (let index = 0; index < plan.count; index++) {
19
+ const solid = typeof source === "function" ? source(index) : source;
20
+ if (typeof source === "function")
21
+ budget(solid, 1);
22
+ const matrix = plan.transformAt(index);
23
+ copies.push(matrix.every((value, i) => value === IDENTITY_TRANSFORM3[i])
24
+ ? solid
25
+ : transforms.operations.transform(solid, matrix));
26
+ }
27
+ return copies;
28
+ };
29
+ const operations = {
30
+ linearPattern: (source, options) => apply(source, () => linearPatternPlan(options), "linearPattern"),
31
+ radialPattern: (source, options) => apply(source, () => radialPatternPlan(options), "radialPattern"),
32
+ gridPattern: (source, options) => apply(source, () => gridPatternPlan(options), "gridPattern"),
33
+ pathPattern: (source, path, options) => apply(source, () => pathPatternPlan(path, options, environment.limits), "pathPattern"),
34
+ };
35
+ return operations;
36
+ }
@@ -0,0 +1,176 @@
1
+ import { GeometryError } from "../geometry/errors.js";
2
+ import { cross3, dot3, scale3 } from "../geometry/math/vector3.js";
3
+ import { distributeAlongPath } from "../geometry/paths/planar.js";
4
+ import { normalizeSweepPath } from "../geometry/sweeps/validation.js";
5
+ import { composeTransform3, frameTransform3, IDENTITY_TRANSFORM3, rotationTransform3, translationTransform3, } from "../geometry/transform3.js";
6
+ const fail = (operation, message) => {
7
+ throw new GeometryError("INVALID_ARGUMENT", message, { operation });
8
+ };
9
+ export function patternCount(count, operation) {
10
+ if (!Number.isSafeInteger(count) || count < 1)
11
+ fail(operation, "Pattern count must be a positive safe integer.");
12
+ if (count > 100_000)
13
+ throw new GeometryError("COMPLEXITY_LIMIT", "Patterns allow at most 100000 copies.", {
14
+ operation,
15
+ details: { requiredCount: count, allowedCount: 100_000 },
16
+ });
17
+ return count;
18
+ }
19
+ const finite = (value, operation) => {
20
+ if (!Number.isFinite(value))
21
+ throw new GeometryError("NON_FINITE_VALUE", "Pattern dimensions must be finite.", {
22
+ operation,
23
+ });
24
+ return value;
25
+ };
26
+ const vector = (value, operation) => {
27
+ if (!Array.isArray(value) || value.length !== 3 || !value.every(Number.isFinite))
28
+ fail(operation, "Pattern vectors require three finite coordinates.");
29
+ return value;
30
+ };
31
+ const direction = (value, operation) => {
32
+ vector(value, operation);
33
+ const maximum = Math.max(...value.map(Math.abs));
34
+ if (maximum === 0)
35
+ fail(operation, "Pattern direction must be nonzero.");
36
+ const scaled = value.map((coordinate) => coordinate / maximum);
37
+ return scale3(scaled, 1 / Math.hypot(...scaled));
38
+ };
39
+ const optionsObject = (value, operation) => {
40
+ if (typeof value !== "object" || value === null || Array.isArray(value))
41
+ fail(operation, "Pattern options must be an object.");
42
+ };
43
+ /** Private lazy placements: count/extent validation precedes copy or callback allocation. */
44
+ export function linearPatternPlan(options) {
45
+ const operation = "linearPattern";
46
+ optionsObject(options, operation);
47
+ const supplied = [options.count, options.spacing, options.length].filter((v) => v !== undefined);
48
+ if (supplied.length !== 2)
49
+ fail(operation, "Specify exactly two of count, spacing and length.");
50
+ supplied.forEach((v) => finite(v, operation));
51
+ let count, spacing;
52
+ if (options.count !== undefined) {
53
+ count = patternCount(options.count, operation);
54
+ if (options.spacing !== undefined) {
55
+ spacing = options.spacing;
56
+ if (spacing === 0)
57
+ fail(operation, "Pattern spacing must be nonzero.");
58
+ }
59
+ else {
60
+ const length = options.length;
61
+ if (count === 1 && length !== 0)
62
+ fail(operation, "A one-copy pattern requires zero total length.");
63
+ if (count > 1 && length === 0)
64
+ fail(operation, "Pattern total length must be nonzero.");
65
+ spacing = count === 1 ? 0 : length / (count - 1);
66
+ }
67
+ }
68
+ else {
69
+ spacing = options.spacing;
70
+ if (spacing === 0)
71
+ fail(operation, "Pattern spacing must be nonzero.");
72
+ const intervals = options.length / spacing;
73
+ if (!Number.isFinite(intervals) ||
74
+ intervals < 0 ||
75
+ Math.abs(intervals - Math.round(intervals)) >
76
+ Number.EPSILON * 8 * Math.max(1, Math.abs(intervals)))
77
+ fail(operation, "Length must be a nonnegative whole multiple of the signed spacing.");
78
+ count = patternCount(Math.round(intervals) + 1, operation);
79
+ }
80
+ const axis = direction(options.direction ?? [1, 0, 0], operation);
81
+ finite(spacing * (count - 1), operation);
82
+ return { count, transformAt: (index) => translationTransform3(scale3(axis, spacing * index)) };
83
+ }
84
+ export function radialPatternPlan(options) {
85
+ const operation = "radialPattern";
86
+ optionsObject(options, operation);
87
+ const count = patternCount(options.count, operation);
88
+ const axis = direction(options.axis, operation);
89
+ const angle = finite((options.angleDegrees / 180) * Math.PI, operation);
90
+ if (angle === 0)
91
+ fail(operation, "Pattern angleDegrees must be nonzero.");
92
+ finite(angle * (count - 1), operation);
93
+ const center = vector(options.center ?? [0, 0, 0], operation);
94
+ const before = translationTransform3(scale3(center, -1));
95
+ const after = translationTransform3(center);
96
+ return {
97
+ count,
98
+ transformAt: (index) => index === 0
99
+ ? IDENTITY_TRANSFORM3
100
+ : composeTransform3(after, composeTransform3(rotationTransform3(axis, { radians: angle * index }), before)),
101
+ };
102
+ }
103
+ export function gridPatternPlan(options) {
104
+ const operation = "gridPattern";
105
+ optionsObject(options, operation);
106
+ const dimensions = options.count?.length;
107
+ if (!Array.isArray(options.count) ||
108
+ !Array.isArray(options.spacing) ||
109
+ (dimensions !== 2 && dimensions !== 3) ||
110
+ options.spacing?.length !== dimensions)
111
+ fail(operation, "Grid count and spacing require matching XY or XYZ tuples.");
112
+ if (options.center !== undefined && typeof options.center !== "boolean")
113
+ fail(operation, "Grid center must be a boolean.");
114
+ const counts = [options.count[0], options.count[1], options.count[2] ?? 1];
115
+ counts.forEach((count) => patternCount(count, operation));
116
+ const count = patternCount(counts[0] * counts[1] * counts[2], operation);
117
+ const spacing = [options.spacing[0], options.spacing[1], options.spacing[2] ?? 0];
118
+ const extent = spacing.map((distance, axis) => {
119
+ finite(distance, operation);
120
+ if (counts[axis] > 1 && distance === 0)
121
+ fail(operation, "Repeated grid axes require nonzero spacing.");
122
+ return finite(distance * (counts[axis] - 1), operation);
123
+ });
124
+ const frame = options.frame === undefined ? IDENTITY_TRANSFORM3 : frameTransform3(options.frame);
125
+ return {
126
+ count,
127
+ transformAt: (index) => {
128
+ const coordinates = [
129
+ index % counts[0],
130
+ Math.floor(index / counts[0]) % counts[1],
131
+ Math.floor(index / (counts[0] * counts[1])),
132
+ ];
133
+ const offset = coordinates.map((value, axis) => value * spacing[axis] - (options.center === true ? extent[axis] / 2 : 0));
134
+ return composeTransform3(frame, translationTransform3(offset));
135
+ },
136
+ };
137
+ }
138
+ export function pathPatternPlan(input, options, limits) {
139
+ const operation = "pathPattern";
140
+ optionsObject(options, operation);
141
+ const count = patternCount(options.count, operation);
142
+ if (count > limits.maxPathPoints)
143
+ throw new GeometryError("COMPLEXITY_LIMIT", "Path placements exceed the selected path point ceiling.", {
144
+ operation,
145
+ details: { requiredCount: count, allowedCount: limits.maxPathPoints },
146
+ });
147
+ if (options.closed !== undefined && typeof options.closed !== "boolean")
148
+ fail(operation, "Path closed must be a boolean.");
149
+ if (options.orientation !== undefined &&
150
+ options.orientation !== "fixed" &&
151
+ options.orientation !== "tangent")
152
+ fail(operation, "Path orientation must be fixed or tangent.");
153
+ const path = normalizeSweepPath(input, options.closed ?? false, operation, limits);
154
+ // Defer the output placement array until the operation has checked the source copy budget.
155
+ let placements;
156
+ return {
157
+ count,
158
+ transformAt: (index) => {
159
+ placements ??= distributeAlongPath(path, count, options.closed ?? false, limits);
160
+ const placement = placements[index];
161
+ const translation = translationTransform3(placement.position);
162
+ if (options.orientation !== "tangent")
163
+ return translation;
164
+ const tangent = placement.tangent;
165
+ const cosine = Math.max(-1, Math.min(1, dot3([0, 0, 1], tangent)));
166
+ const axis = cross3([0, 0, 1], tangent);
167
+ const sine = Math.hypot(...axis);
168
+ const rotation = sine > 1e-12
169
+ ? rotationTransform3(scale3(axis, 1 / sine), { radians: Math.atan2(sine, cosine) })
170
+ : cosine < 0
171
+ ? rotationTransform3([1, 0, 0], { radians: Math.PI })
172
+ : IDENTITY_TRANSFORM3;
173
+ return composeTransform3(translation, rotation);
174
+ },
175
+ };
176
+ }
@@ -0,0 +1,70 @@
1
+ import { boxAnchor3, createAnchor3 } from "../geometry/anchors.js";
2
+ import { resolveCylinderRadii } from "../geometry/cylinder.js";
3
+ const faces = ["left", "right", "back", "front", "bottom", "top"];
4
+ /** Nominal primitive datums; construction validates dimensions before these factories run. */
5
+ export function boxAnchors(options) {
6
+ const size = typeof options.size === "number" ? [options.size, options.size, options.size] : options.size;
7
+ const center = options.center === false ? [size[0] / 2, size[1] / 2, size[2] / 2] : [0, 0, 0];
8
+ return {
9
+ center: createAnchor3(center, [0, 0, 1]),
10
+ ...Object.fromEntries(faces.map((face) => [
11
+ face,
12
+ boxAnchor3({
13
+ size,
14
+ face,
15
+ ...(options.center === undefined ? {} : { center: options.center }),
16
+ }),
17
+ ])),
18
+ };
19
+ }
20
+ /** Cap planes and nominal radial midsection, with the analytic tapered-side outward normal. */
21
+ function axialAnchors(radii, height, centered) {
22
+ const bottom = centered ? -height / 2 : 0;
23
+ const middle = bottom + height / 2;
24
+ const radius = (radii[0] + radii[1]) / 2;
25
+ const slope = (radii[1] - radii[0]) / height;
26
+ return {
27
+ center: createAnchor3([0, 0, middle], [0, 0, 1]),
28
+ ...(radii[0] === 0 ? {} : { bottom: createAnchor3([0, 0, bottom], [0, 0, -1]) }),
29
+ ...(radii[1] === 0 ? {} : { top: createAnchor3([0, 0, bottom + height], [0, 0, 1]) }),
30
+ right: createAnchor3([radius, 0, middle], [1, 0, -slope], [0, 0, 1]),
31
+ left: createAnchor3([-radius, 0, middle], [-1, 0, -slope], [0, 0, 1]),
32
+ front: createAnchor3([0, radius, middle], [0, 1, -slope], [0, 0, 1]),
33
+ back: createAnchor3([0, -radius, middle], [0, -1, -slope], [0, 0, 1]),
34
+ };
35
+ }
36
+ /** Cylinder and frustum cap anchors omit an end whose nominal radius is zero. */
37
+ export function cylinderAnchors(options) {
38
+ return axialAnchors(resolveCylinderRadii(options), options.height, options.center ?? true);
39
+ }
40
+ /** Tube cap centers lie on the nominal cap planes, including the central opening. */
41
+ export function tubeAnchors(options) {
42
+ return axialAnchors([options.outerRadius, options.outerRadius], options.height, options.center ?? true);
43
+ }
44
+ /** Sphere principal surface points and its origin reference. */
45
+ export function sphereAnchors(options) {
46
+ const radius = options.radius ?? options.diameter / 2;
47
+ return boxAnchors({ size: 2 * radius });
48
+ }
49
+ /** Torus envelope-plane datums around its axis; axial centers may lie over the opening. */
50
+ export function torusAnchors(options) {
51
+ const radial = options.majorRadius + options.minorRadius;
52
+ return boxAnchors({ size: [2 * radial, 2 * radial, 2 * options.minorRadius] });
53
+ }
54
+ /** Filled XY shapes use world-axis names; top/bottom reference the two sides of the XY plane. */
55
+ export function planarAnchors(size, centered = true) {
56
+ const x = centered ? 0 : size[0] / 2, y = centered ? 0 : size[1] / 2;
57
+ return {
58
+ center: createAnchor3([x, y, 0], [0, 0, 1]),
59
+ top: createAnchor3([x, y, 0], [0, 0, 1]),
60
+ bottom: createAnchor3([x, y, 0], [0, 0, -1]),
61
+ right: createAnchor3([x + size[0] / 2, y, 0], [1, 0, 0], [0, 0, 1]),
62
+ left: createAnchor3([x - size[0] / 2, y, 0], [-1, 0, 0], [0, 0, 1]),
63
+ front: createAnchor3([x, y + size[1] / 2, 0], [0, 1, 0], [0, 0, 1]),
64
+ back: createAnchor3([x, y - size[1] / 2, 0], [0, -1, 0], [0, 0, 1]),
65
+ };
66
+ }
67
+ /** A construction-origin reference for primitives without meaningful principal side datums. */
68
+ export const originAnchor = () => ({
69
+ center: createAnchor3([0, 0, 0], [0, 0, 1]),
70
+ });