forgecad 0.10.2 → 0.10.4

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 (132) hide show
  1. package/README.md +7 -6
  2. package/dist/assets/{AdminPage-CHY6ZN-p.js → AdminPage-B3L3W1Uo.js} +1 -1
  3. package/dist/assets/{BenchmarkPage-BcRT5iGN.js → BenchmarkPage-DXKVXMrJ.js} +2 -2
  4. package/dist/assets/{BlogPage-BssBbnb-.js → BlogPage-B7BWxOCg.js} +1 -1
  5. package/dist/assets/{DocsPage-DsvdiRNK.js → DocsPage-BPGGwht1.js} +28 -48
  6. package/dist/assets/{EditorApp-Bfd3jbtC.js → EditorApp-BWUGCdD5.js} +183 -21
  7. package/dist/assets/{EditorApp-BpjZgzk0.css → EditorApp-C5f24ZN9.css} +8 -0
  8. package/dist/assets/{EmbedViewer-D5t8WamV.js → EmbedViewer-DygByZS2.js} +2 -2
  9. package/dist/assets/{LandingPageProofDriven-DbN7o-Be.js → LandingPageProofDriven-BoVE7JGY.js} +54 -36
  10. package/dist/assets/{LegalPage-DNGrrY0p.js → LegalPage-Din8wv8d.js} +2 -2
  11. package/dist/assets/{PricingPage-Nczr3pRz.js → PricingPage-C2PMzmDc.js} +2 -2
  12. package/dist/assets/{SettingsPage-DZlyu4d4.js → SettingsPage-BlJDCRe8.js} +1 -1
  13. package/dist/assets/{app-C9ct2hRD.js → app-BsRYSfxY.js} +2264 -6259
  14. package/dist/assets/{backendInit-ymjonyQp.js → backendInit-6C0DLgH0.js} +8290 -2136
  15. package/dist/assets/cli/{render-B_0lQwKU.js → render-XXol_ET7.js} +822 -105
  16. package/dist/assets/{constructionHistoryWorker-CZ42Dksy.js → constructionHistoryWorker-cTHWRJEi.js} +699 -284
  17. package/dist/assets/{evalWorker-C2pm8LHP.js → evalWorker-BssDYW9u.js} +2559 -1330
  18. package/dist/assets/{forgecad_geometry-BlMtqluF.js → forgecad_geometry-CZ_IfuvA.js} +1 -9
  19. package/dist/assets/{forgecad_geometry_bg-BllP_WiL.wasm → forgecad_geometry_bg-C3rQHfwg.wasm} +0 -0
  20. package/dist/assets/{inspectWorker-D5T5VbfK.js → inspectWorker-ymhBV4Ll.js} +6254 -671
  21. package/dist/assets/{jointPose-4r8ed8_5.js → jointPose-B0blBj9A.js} +1 -1
  22. package/dist/assets/{landing-proof-driven-ORyigZ6p.css → landing-proof-driven-Cpf-MIbI.css} +73 -13
  23. package/dist/assets/{manifold-5PP1eGLN.js → manifold-B_7QXpGB.js} +1 -1
  24. package/dist/assets/{manifold-DjBkyIc8.js → manifold-CNShmpEJ.js} +1 -1
  25. package/dist/assets/{manifold-C4r6B-XY.js → manifold-CYlIm-M6.js} +2 -2
  26. package/dist/assets/{reportWorker-CwenM7wB.js → reportWorker-Cb5eyM7D.js} +2485 -1275
  27. package/dist/cli/render.html +1 -1
  28. package/dist/docs/index.html +2 -2
  29. package/dist/docs-raw/AI/usage.md +17 -17
  30. package/dist/docs-raw/CLI.md +9 -7
  31. package/dist/docs-raw/README.md +1 -1
  32. package/dist/docs-raw/component-model.md +2 -2
  33. package/dist/docs-raw/generated/assembly.md +1 -1
  34. package/dist/docs-raw/generated/concepts.md +10 -4
  35. package/dist/docs-raw/generated/core.md +96 -1
  36. package/dist/docs-raw/generated/curves.md +8 -1
  37. package/dist/docs-raw/generated/output.md +0 -64
  38. package/dist/docs-raw/generated/runtime-names.md +6 -6
  39. package/dist/docs-raw/generated/viewport.md +3 -12
  40. package/dist/docs-raw/guides/inspection-bundles.md +1 -1
  41. package/dist/docs-raw/simulation-workflow.md +58 -0
  42. package/{dist-skill/website/skills/forgecad-make-a-model.md → dist/docs-raw/skills/forgecad-build-model.md} +18 -8
  43. package/dist/docs-raw/skills/forgecad-design-spec.md +145 -0
  44. package/dist/docs-raw/skills/{forgecad-model-grader.md → forgecad-grade-model.md} +8 -6
  45. package/{dist-skill/website/skills/forgecad-visual-spec.md → dist/docs-raw/skills/forgecad-image-prompt.md} +7 -7
  46. package/dist/docs-raw/skills/{forgecad-render-inspect.md → forgecad-inspect-model.md} +6 -6
  47. package/{dist-skill/website/skills/forgecad-project.md → dist/docs-raw/skills/forgecad-project-sync.md} +5 -5
  48. package/{dist-skill/website/skills/forgecad-3d-reconstruction.md → dist/docs-raw/skills/forgecad-reconstruct-cad-file.md} +7 -7
  49. package/dist/docs-raw/skills/{forgecad-image-replicator.md → forgecad-reconstruct-from-images.md} +12 -12
  50. package/dist/docs-raw/skills/forgecad-verify-mujoco.md +78 -0
  51. package/dist/docs-raw/skills/forgecad.md +24 -24
  52. package/dist/docs-raw/skills/index.md +9 -13
  53. package/dist/index.html +9 -9
  54. package/dist/llms.txt +7 -7
  55. package/dist/sitemap.xml +16 -16
  56. package/dist-cli/{check-compiler-SP7FAL7R.js → check-compiler-4RPB6SB5.js} +1 -1
  57. package/dist-cli/{check-query-propagation-BRLSHP22.js → check-query-propagation-KN3DFQTX.js} +1 -1
  58. package/dist-cli/{chunk-RQQ42YCP.js → chunk-UHBRMYA6.js} +30770 -29253
  59. package/dist-cli/forgecad.js +3277 -237
  60. package/dist-cli/{forgecad_geometry-7TVSNVUB.js → forgecad_geometry-2IMYCUWW.js} +0 -8
  61. package/dist-cli/forgecad_geometry_bg.wasm +0 -0
  62. package/dist-skill/CONTEXT.md +111 -73
  63. package/dist-skill/SKILL.md +1 -1
  64. package/dist-skill/docs/CLI.md +9 -7
  65. package/dist-skill/docs/generated/assembly.md +1 -1
  66. package/dist-skill/docs/generated/core.md +96 -1
  67. package/dist-skill/docs/generated/curves.md +8 -1
  68. package/dist-skill/docs/generated/output.md +0 -64
  69. package/dist-skill/docs/generated/runtime-names.md +6 -6
  70. package/dist-skill/docs/generated/viewport.md +3 -12
  71. package/dist-skill/docs/guides/inspection-bundles.md +1 -1
  72. package/dist-skill/library/README.md +9 -13
  73. package/dist-skill/library/{forgecad-make-a-model → forgecad-build-model}/SKILL.md +16 -6
  74. package/dist-skill/library/forgecad-design-spec/SKILL.md +132 -0
  75. package/dist-skill/library/{forgecad-prepare-prompt → forgecad-design-spec}/references/master-prompt.md +1 -1
  76. package/dist-skill/library/{forgecad-model-grader → forgecad-grade-model}/SKILL.md +6 -4
  77. package/dist-skill/library/forgecad-grade-model/agents/openai.yaml +4 -0
  78. package/dist-skill/library/{forgecad-visual-spec → forgecad-image-prompt}/SKILL.md +5 -5
  79. package/dist-skill/library/forgecad-image-prompt/agents/openai.yaml +4 -0
  80. package/dist-skill/library/{forgecad-render-inspect → forgecad-inspect-model}/SKILL.md +4 -4
  81. package/dist-skill/library/{forgecad-project → forgecad-project-sync}/SKILL.md +3 -3
  82. package/dist-skill/library/{forgecad-3d-reconstruction → forgecad-reconstruct-cad-file}/SKILL.md +5 -5
  83. package/dist-skill/library/forgecad-reconstruct-cad-file/agents/openai.yaml +4 -0
  84. package/dist-skill/library/{forgecad-image-replicator → forgecad-reconstruct-from-images}/SKILL.md +10 -10
  85. package/dist-skill/library/forgecad-reconstruct-from-images/agents/openai.yaml +4 -0
  86. package/dist-skill/library/forgecad-verify-mujoco/SKILL.md +66 -0
  87. package/dist-skill/library/forgecad-verify-mujoco/scripts/mujoco_verify.py +385 -0
  88. package/{dist/docs-raw/skills/forgecad-make-a-model.md → dist-skill/website/skills/forgecad-build-model.md} +18 -8
  89. package/dist-skill/website/skills/forgecad-design-spec.md +145 -0
  90. package/dist-skill/website/skills/{forgecad-model-grader.md → forgecad-grade-model.md} +8 -6
  91. package/{dist/docs-raw/skills/forgecad-visual-spec.md → dist-skill/website/skills/forgecad-image-prompt.md} +7 -7
  92. package/dist-skill/website/skills/{forgecad-render-inspect.md → forgecad-inspect-model.md} +6 -6
  93. package/{dist/docs-raw/skills/forgecad-project.md → dist-skill/website/skills/forgecad-project-sync.md} +5 -5
  94. package/{dist/docs-raw/skills/forgecad-3d-reconstruction.md → dist-skill/website/skills/forgecad-reconstruct-cad-file.md} +7 -7
  95. package/dist-skill/website/skills/{forgecad-image-replicator.md → forgecad-reconstruct-from-images.md} +12 -12
  96. package/dist-skill/website/skills/forgecad-verify-mujoco.md +78 -0
  97. package/dist-skill/website/skills/forgecad.md +24 -24
  98. package/dist-skill/website/skills/index.md +9 -13
  99. package/examples/analysis/clearance-fit.forge.js +31 -0
  100. package/examples/analysis/lever-arm-actuator.forge.js +43 -0
  101. package/examples/analysis/tipping-tripod.forge.js +35 -0
  102. package/examples/api/texture-projection.forge.js +75 -0
  103. package/examples/assets/uv-grid.png +0 -0
  104. package/examples/products/sportscar.forge.js +77 -0
  105. package/package.json +1 -3
  106. package/dist/docs-raw/skills/forgecad-blockout-model.md +0 -49
  107. package/dist/docs-raw/skills/forgecad-component-model.md +0 -53
  108. package/dist/docs-raw/skills/forgecad-high-level-spec.md +0 -101
  109. package/dist/docs-raw/skills/forgecad-lld.md +0 -41
  110. package/dist/docs-raw/skills/forgecad-prepare-prompt.md +0 -63
  111. package/dist/docs-raw/skills/forgecad-reconstruction-benchmark.md +0 -60
  112. package/dist-skill/library/forgecad-3d-reconstruction/agents/openai.yaml +0 -4
  113. package/dist-skill/library/forgecad-blockout-model/SKILL.md +0 -42
  114. package/dist-skill/library/forgecad-component-model/SKILL.md +0 -46
  115. package/dist-skill/library/forgecad-high-level-spec/SKILL.md +0 -94
  116. package/dist-skill/library/forgecad-image-replicator/agents/openai.yaml +0 -4
  117. package/dist-skill/library/forgecad-lld/SKILL.md +0 -34
  118. package/dist-skill/library/forgecad-model-grader/agents/openai.yaml +0 -4
  119. package/dist-skill/library/forgecad-prepare-prompt/SKILL.md +0 -50
  120. package/dist-skill/library/forgecad-reconstruction-benchmark/SKILL.md +0 -48
  121. package/dist-skill/library/forgecad-reconstruction-benchmark/agents/openai.yaml +0 -4
  122. package/dist-skill/library/forgecad-visual-spec/agents/openai.yaml +0 -4
  123. package/dist-skill/website/skills/forgecad-blockout-model.md +0 -49
  124. package/dist-skill/website/skills/forgecad-component-model.md +0 -53
  125. package/dist-skill/website/skills/forgecad-high-level-spec.md +0 -101
  126. package/dist-skill/website/skills/forgecad-lld.md +0 -41
  127. package/dist-skill/website/skills/forgecad-prepare-prompt.md +0 -63
  128. package/dist-skill/website/skills/forgecad-reconstruction-benchmark.md +0 -60
  129. /package/dist/assets/{landing-proof-driven-DiGqdtWa.js → landing-proof-driven-BxZZh5r5.js} +0 -0
  130. /package/dist-skill/library/{forgecad-prepare-prompt → forgecad-design-spec}/references/default-profiles.md +0 -0
  131. /package/dist-skill/library/{forgecad-render-inspect → forgecad-inspect-model}/summarize_manifest.py +0 -0
  132. /package/dist-skill/library/{forgecad-image-replicator → forgecad-reconstruct-from-images}/scripts/compare_images.py +0 -0
@@ -722,12 +722,12 @@ class Vector2 {
722
722
  * @param {number} angle - The angle to rotate, in radians.
723
723
  * @return {Vector2} A reference to this vector.
724
724
  */
725
- rotateAround(center, angle) {
725
+ rotateAround(center2, angle) {
726
726
  const c2 = Math.cos(angle), s = Math.sin(angle);
727
- const x2 = this.x - center.x;
728
- const y2 = this.y - center.y;
729
- this.x = x2 * c2 - y2 * s + center.x;
730
- this.y = x2 * s + y2 * c2 + center.y;
727
+ const x2 = this.x - center2.x;
728
+ const y2 = this.y - center2.y;
729
+ this.x = x2 * c2 - y2 * s + center2.x;
730
+ this.y = x2 * s + y2 * c2 + center2.y;
731
731
  return this;
732
732
  }
733
733
  /**
@@ -3256,10 +3256,10 @@ class Box3 {
3256
3256
  * @param {Vector3} size - The x, y and z dimensions of the box.
3257
3257
  * @return {Box3} A reference to this bounding box.
3258
3258
  */
3259
- setFromCenterAndSize(center, size) {
3259
+ setFromCenterAndSize(center2, size) {
3260
3260
  const halfSize = _vector$b.copy(size).multiplyScalar(0.5);
3261
- this.min.copy(center).sub(halfSize);
3262
- this.max.copy(center).add(halfSize);
3261
+ this.min.copy(center2).sub(halfSize);
3262
+ this.max.copy(center2).add(halfSize);
3263
3263
  return this;
3264
3264
  }
3265
3265
  /**
@@ -3709,9 +3709,9 @@ class Sphere {
3709
3709
  * @param {Vector3} [center=(0,0,0)] - The center of the sphere
3710
3710
  * @param {number} [radius=-1] - The radius of the sphere.
3711
3711
  */
3712
- constructor(center = new Vector3(), radius = -1) {
3712
+ constructor(center2 = new Vector3(), radius = -1) {
3713
3713
  this.isSphere = true;
3714
- this.center = center;
3714
+ this.center = center2;
3715
3715
  this.radius = radius;
3716
3716
  }
3717
3717
  /**
@@ -3721,8 +3721,8 @@ class Sphere {
3721
3721
  * @param {number} radius - The radius.
3722
3722
  * @return {Sphere} A reference to this sphere.
3723
3723
  */
3724
- set(center, radius) {
3725
- this.center.copy(center);
3724
+ set(center2, radius) {
3725
+ this.center.copy(center2);
3726
3726
  this.radius = radius;
3727
3727
  return this;
3728
3728
  }
@@ -3737,15 +3737,15 @@ class Sphere {
3737
3737
  * @return {Sphere} A reference to this sphere.
3738
3738
  */
3739
3739
  setFromPoints(points, optionalCenter) {
3740
- const center = this.center;
3740
+ const center2 = this.center;
3741
3741
  if (optionalCenter !== void 0) {
3742
- center.copy(optionalCenter);
3742
+ center2.copy(optionalCenter);
3743
3743
  } else {
3744
- _box$3.setFromPoints(points).getCenter(center);
3744
+ _box$3.setFromPoints(points).getCenter(center2);
3745
3745
  }
3746
3746
  let maxRadiusSq = 0;
3747
3747
  for (let i = 0, il = points.length; i < il; i++) {
3748
- maxRadiusSq = Math.max(maxRadiusSq, center.distanceToSquared(points[i]));
3748
+ maxRadiusSq = Math.max(maxRadiusSq, center2.distanceToSquared(points[i]));
3749
3749
  }
3750
3750
  this.radius = Math.sqrt(maxRadiusSq);
3751
3751
  return this;
@@ -7133,7 +7133,7 @@ class BufferGeometry extends EventDispatcher {
7133
7133
  return;
7134
7134
  }
7135
7135
  if (position) {
7136
- const center = this.boundingSphere.center;
7136
+ const center2 = this.boundingSphere.center;
7137
7137
  _box$2.setFromBufferAttribute(position);
7138
7138
  if (morphAttributesPosition) {
7139
7139
  for (let i = 0, il = morphAttributesPosition.length; i < il; i++) {
@@ -7150,11 +7150,11 @@ class BufferGeometry extends EventDispatcher {
7150
7150
  }
7151
7151
  }
7152
7152
  }
7153
- _box$2.getCenter(center);
7153
+ _box$2.getCenter(center2);
7154
7154
  let maxRadiusSq = 0;
7155
7155
  for (let i = 0, il = position.count; i < il; i++) {
7156
7156
  _vector$8.fromBufferAttribute(position, i);
7157
- maxRadiusSq = Math.max(maxRadiusSq, center.distanceToSquared(_vector$8));
7157
+ maxRadiusSq = Math.max(maxRadiusSq, center2.distanceToSquared(_vector$8));
7158
7158
  }
7159
7159
  if (morphAttributesPosition) {
7160
7160
  for (let i = 0, il = morphAttributesPosition.length; i < il; i++) {
@@ -7166,7 +7166,7 @@ class BufferGeometry extends EventDispatcher {
7166
7166
  _offset.fromBufferAttribute(position, j);
7167
7167
  _vector$8.add(_offset);
7168
7168
  }
7169
- maxRadiusSq = Math.max(maxRadiusSq, center.distanceToSquared(_vector$8));
7169
+ maxRadiusSq = Math.max(maxRadiusSq, center2.distanceToSquared(_vector$8));
7170
7170
  }
7171
7171
  }
7172
7172
  }
@@ -8043,6 +8043,12 @@ if (typeof window !== "undefined") {
8043
8043
  }
8044
8044
  }
8045
8045
  const SHAPE_BACKEND_MARKER = Symbol.for("forgecad.shapeBackend");
8046
+ const POSITION_OFFSET = 0;
8047
+ const NORMAL_OFFSET = 3;
8048
+ const UV_OFFSET = 6;
8049
+ const NUM_PROP_POSITION_ONLY = 3;
8050
+ const NUM_PROP_WITH_NORMAL = 6;
8051
+ const NUM_PROP_WITH_UV = 8;
8046
8052
  function isShapeBackend(value) {
8047
8053
  return Boolean(value && typeof value === "object" && value[SHAPE_BACKEND_MARKER] === true);
8048
8054
  }
@@ -9187,11 +9193,11 @@ function applyFilletSelectionToManifold(base, selection, radius, segments, wasm)
9187
9193
  const height = edgeLength(selection);
9188
9194
  if (!(height > 1e-6)) return base;
9189
9195
  const pad = Math.max(MIN_EDGE_PAD, radius);
9190
- const span = height + pad * 2;
9196
+ const span2 = height + pad * 2;
9191
9197
  const frame = edgeFrameMatrix(selection, -pad);
9192
9198
  const cs = buildFilletCrossSection(selection, radius, segments, wasm) ?? legacyFilletCrossSection(selection, radius, segments, wasm);
9193
- const corner = cs.wedge.extrude(span, 0, 0, void 0, false).transform(frame);
9194
- const cyl = cs.cylinder.extrude(span, 0, 0, void 0, false).transform(frame);
9199
+ const corner = cs.wedge.extrude(span2, 0, 0, void 0, false).transform(frame);
9200
+ const cyl = cs.cylinder.extrude(span2, 0, 0, void 0, false).transform(frame);
9195
9201
  const crescent = wasm.Manifold.difference([corner, cyl]);
9196
9202
  return wasm.Manifold.difference([base, crescent]);
9197
9203
  }
@@ -9199,11 +9205,11 @@ function applyConcaveFilletSelectionToManifold(base, selection, radius, segments
9199
9205
  const height = edgeLength(selection);
9200
9206
  if (!(height > 1e-6)) return base;
9201
9207
  const pad = Math.max(MIN_EDGE_PAD, radius);
9202
- const span = height + pad * 2;
9208
+ const span2 = height + pad * 2;
9203
9209
  const frame = edgeFrameMatrix(selection, -pad);
9204
9210
  const cs = buildFilletCrossSection(selection, radius, segments, wasm) ?? legacyFilletCrossSection(selection, radius, segments, wasm);
9205
- const corner = cs.wedge.extrude(span, 0, 0, void 0, false).transform(frame);
9206
- const cyl = cs.cylinder.extrude(span, 0, 0, void 0, false).transform(frame);
9211
+ const corner = cs.wedge.extrude(span2, 0, 0, void 0, false).transform(frame);
9212
+ const cyl = cs.cylinder.extrude(span2, 0, 0, void 0, false).transform(frame);
9207
9213
  const crescent = wasm.Manifold.difference([corner, cyl]);
9208
9214
  return wasm.Manifold.union([base, crescent]);
9209
9215
  }
@@ -9211,20 +9217,20 @@ function applyChamferSelectionToManifold(base, selection, size, wasm) {
9211
9217
  const height = edgeLength(selection);
9212
9218
  if (!(height > 1e-6)) return base;
9213
9219
  const pad = Math.max(MIN_EDGE_PAD, size);
9214
- const span = height + pad * 2;
9220
+ const span2 = height + pad * 2;
9215
9221
  const frame = edgeFrameMatrix(selection, -pad);
9216
9222
  const triangle = buildChamferCrossSection(selection, size, wasm) ?? legacyChamferCrossSection(selection, size, wasm);
9217
- const chamfer = triangle.extrude(span, 0, 0, void 0, false).transform(frame);
9223
+ const chamfer = triangle.extrude(span2, 0, 0, void 0, false).transform(frame);
9218
9224
  return wasm.Manifold.difference([base, chamfer]);
9219
9225
  }
9220
9226
  function applyConcaveChamferSelectionToManifold(base, selection, size, wasm) {
9221
9227
  const height = edgeLength(selection);
9222
9228
  if (!(height > 1e-6)) return base;
9223
9229
  const pad = Math.max(MIN_EDGE_PAD, size);
9224
- const span = height + pad * 2;
9230
+ const span2 = height + pad * 2;
9225
9231
  const frame = edgeFrameMatrix(selection, -pad);
9226
9232
  const triangle = buildChamferCrossSection(selection, size, wasm) ?? legacyChamferCrossSection(selection, size, wasm);
9227
- const chamfer = triangle.extrude(span, 0, 0, void 0, false).transform(frame);
9233
+ const chamfer = triangle.extrude(span2, 0, 0, void 0, false).transform(frame);
9228
9234
  return wasm.Manifold.union([base, chamfer]);
9229
9235
  }
9230
9236
  function cloneEdgeFinishQuadrant(quadrant) {
@@ -9378,6 +9384,7 @@ function cloneTopologyRewritePropagation(propagation) {
9378
9384
  }
9379
9385
  function faceQueryRefsEqual(a2, b) {
9380
9386
  if (a2 == null || b == null) return a2 == null && b == null;
9387
+ if (a2 === b) return true;
9381
9388
  if (a2.kind !== b.kind) return false;
9382
9389
  switch (a2.kind) {
9383
9390
  case "canonical-face":
@@ -9394,6 +9401,7 @@ function faceQueryRefsEqual(a2, b) {
9394
9401
  }
9395
9402
  function edgeQueryRefsEqual(a2, b) {
9396
9403
  if (a2 == null || b == null) return a2 == null && b == null;
9404
+ if (a2 === b) return true;
9397
9405
  if (a2.kind !== b.kind) return false;
9398
9406
  switch (a2.kind) {
9399
9407
  case "tracked-edge":
@@ -9760,8 +9768,8 @@ function deriveSheetMetalModel(model) {
9760
9768
  const trimStart = flanges.has(adjacent.start) ? model.cornerRelief.size : 0;
9761
9769
  const trimEnd = flanges.has(adjacent.end) ? model.cornerRelief.size : 0;
9762
9770
  const fullLength = edge === "top" || edge === "bottom" ? model.panel.width : model.panel.height;
9763
- const span = fullLength - trimStart - trimEnd;
9764
- if (!(span > EPS$5)) {
9771
+ const span2 = fullLength - trimStart - trimEnd;
9772
+ if (!(span2 > EPS$5)) {
9765
9773
  throw new Error(
9766
9774
  `${edgeDisplayName(edge)} loses all usable span after applying the defended rectangular corner relief size ${model.cornerRelief.size}.`
9767
9775
  );
@@ -9773,7 +9781,7 @@ function deriveSheetMetalModel(model) {
9773
9781
  bendAllowance,
9774
9782
  trimStart,
9775
9783
  trimEnd,
9776
- span,
9784
+ span: span2,
9777
9785
  centerAlongEdge: (trimStart - trimEnd) / 2
9778
9786
  });
9779
9787
  }
@@ -9952,10 +9960,10 @@ function lowerSheetMetalBasePlan(model, output) {
9952
9960
  const pieces = lowerSheetMetalBasePiecePlans(model, output).map((piece) => piece.plan);
9953
9961
  return pieces.length === 1 ? pieces[0] : buildBooleanShapeCompilePlan("union", pieces);
9954
9962
  }
9955
- function descriptor(name, center, normal2, planar, uAxis, vAxis, semantic = "face", memberNames = [name], coplanar = planar) {
9963
+ function descriptor(name, center2, normal2, planar, uAxis, vAxis, semantic = "face", memberNames = [name], coplanar = planar) {
9956
9964
  return {
9957
9965
  name,
9958
- center: cloneVec3$3(center),
9966
+ center: cloneVec3$3(center2),
9959
9967
  normal: cloneVec3$3(normal2),
9960
9968
  planar,
9961
9969
  uAxis: cloneFaceAxis(uAxis),
@@ -11408,14 +11416,14 @@ function findSpan(n, degree, u2, knots) {
11408
11416
  }
11409
11417
  return mid;
11410
11418
  }
11411
- function basisFuns(span, u2, degree, knots) {
11419
+ function basisFuns(span2, u2, degree, knots) {
11412
11420
  const N = new Array(degree + 1);
11413
11421
  const left = new Array(degree + 1);
11414
11422
  const right = new Array(degree + 1);
11415
11423
  N[0] = 1;
11416
11424
  for (let j = 1; j <= degree; j++) {
11417
- left[j] = u2 - knots[span + 1 - j];
11418
- right[j] = knots[span + j] - u2;
11425
+ left[j] = u2 - knots[span2 + 1 - j];
11426
+ right[j] = knots[span2 + j] - u2;
11419
11427
  let saved = 0;
11420
11428
  for (let r = 0; r < j; r++) {
11421
11429
  const denom = right[r + 1] + left[j - r];
@@ -11427,13 +11435,72 @@ function basisFuns(span, u2, degree, knots) {
11427
11435
  }
11428
11436
  return N;
11429
11437
  }
11438
+ function basisFunsDeriv(span2, u2, degree, knots, nDeriv) {
11439
+ const ndu = Array.from({ length: degree + 1 }, () => new Array(degree + 1).fill(0));
11440
+ const a2 = Array.from({ length: 2 }, () => new Array(degree + 1).fill(0));
11441
+ const left = new Array(degree + 1);
11442
+ const right = new Array(degree + 1);
11443
+ ndu[0][0] = 1;
11444
+ for (let j = 1; j <= degree; j++) {
11445
+ left[j] = u2 - knots[span2 + 1 - j];
11446
+ right[j] = knots[span2 + j] - u2;
11447
+ let saved = 0;
11448
+ for (let r2 = 0; r2 < j; r2++) {
11449
+ ndu[j][r2] = right[r2 + 1] + left[j - r2];
11450
+ const temp = ndu[j][r2] === 0 ? 0 : ndu[r2][j - 1] / ndu[j][r2];
11451
+ ndu[r2][j] = saved + right[r2 + 1] * temp;
11452
+ saved = left[j - r2] * temp;
11453
+ }
11454
+ ndu[j][j] = saved;
11455
+ }
11456
+ const ders = Array.from({ length: nDeriv + 1 }, () => new Array(degree + 1).fill(0));
11457
+ for (let j = 0; j <= degree; j++) {
11458
+ ders[0][j] = ndu[j][degree];
11459
+ }
11460
+ for (let r2 = 0; r2 <= degree; r2++) {
11461
+ let s1 = 0;
11462
+ let s2 = 1;
11463
+ a2[0][0] = 1;
11464
+ for (let k2 = 1; k2 <= nDeriv; k2++) {
11465
+ let d2 = 0;
11466
+ const rk = r2 - k2;
11467
+ const pk = degree - k2;
11468
+ if (r2 >= k2) {
11469
+ a2[s2][0] = ndu[pk + 1][rk] === 0 ? 0 : a2[s1][0] / ndu[pk + 1][rk];
11470
+ d2 = a2[s2][0] * ndu[rk][pk];
11471
+ }
11472
+ const j1 = rk >= -1 ? 1 : -rk;
11473
+ const j2 = r2 - 1 <= pk ? k2 - 1 : degree - r2;
11474
+ for (let j = j1; j <= j2; j++) {
11475
+ a2[s2][j] = ndu[pk + 1][rk + j] === 0 ? 0 : (a2[s1][j] - a2[s1][j - 1]) / ndu[pk + 1][rk + j];
11476
+ d2 += a2[s2][j] * ndu[rk + j][pk];
11477
+ }
11478
+ if (r2 <= pk) {
11479
+ a2[s2][k2] = ndu[pk + 1][r2] === 0 ? 0 : -a2[s1][k2 - 1] / ndu[pk + 1][r2];
11480
+ d2 += a2[s2][k2] * ndu[r2][pk];
11481
+ }
11482
+ ders[k2][r2] = d2;
11483
+ const tmp = s1;
11484
+ s1 = s2;
11485
+ s2 = tmp;
11486
+ }
11487
+ }
11488
+ let r = degree;
11489
+ for (let k2 = 1; k2 <= nDeriv; k2++) {
11490
+ for (let j = 0; j <= degree; j++) {
11491
+ ders[k2][j] *= r;
11492
+ }
11493
+ r *= degree - k2;
11494
+ }
11495
+ return ders;
11496
+ }
11430
11497
  function deBoor3D(controlPoints, weights, knots, degree, u2) {
11431
11498
  const n = controlPoints.length;
11432
- const span = findSpan(n, degree, u2, knots);
11433
- const N = basisFuns(span, u2, degree, knots);
11499
+ const span2 = findSpan(n, degree, u2, knots);
11500
+ const N = basisFuns(span2, u2, degree, knots);
11434
11501
  let wx = 0, wy = 0, wz = 0, wSum = 0;
11435
11502
  for (let j = 0; j <= degree; j++) {
11436
- const idx = span - degree + j;
11503
+ const idx = span2 - degree + j;
11437
11504
  const w2 = weights[idx] * N[j];
11438
11505
  wx += w2 * controlPoints[idx][0];
11439
11506
  wy += w2 * controlPoints[idx][1];
@@ -11445,11 +11512,11 @@ function deBoor3D(controlPoints, weights, knots, degree, u2) {
11445
11512
  }
11446
11513
  function deBoor2D(controlPoints, weights, knots, degree, u2) {
11447
11514
  const n = controlPoints.length;
11448
- const span = findSpan(n, degree, u2, knots);
11449
- const N = basisFuns(span, u2, degree, knots);
11515
+ const span2 = findSpan(n, degree, u2, knots);
11516
+ const N = basisFuns(span2, u2, degree, knots);
11450
11517
  let wx = 0, wy = 0, wSum = 0;
11451
11518
  for (let j = 0; j <= degree; j++) {
11452
- const idx = span - degree + j;
11519
+ const idx = span2 - degree + j;
11453
11520
  const w2 = weights[idx] * N[j];
11454
11521
  wx += w2 * controlPoints[idx][0];
11455
11522
  wy += w2 * controlPoints[idx][1];
@@ -15413,6 +15480,23 @@ function analyzeNodeUV(node, toLocal) {
15413
15480
  function clampUnit(v) {
15414
15481
  return v < -1 ? -1 : v > 1 ? 1 : v;
15415
15482
  }
15483
+ function sphereUVLocal(lx, ly, lz, radius) {
15484
+ const u2 = atan2(ly, lx) * radius;
15485
+ const len = sqrt$3(lx * lx + ly * ly + lz * lz);
15486
+ const v = acos(clampUnit(lz / (len || 1))) * radius;
15487
+ return [u2, v];
15488
+ }
15489
+ function cylinderUVLocal(lx, ly, lz, radius) {
15490
+ const u2 = atan2(ly, lx) * radius;
15491
+ const v = lz;
15492
+ return [u2, v];
15493
+ }
15494
+ function torusUVLocal(lx, ly, lz, majorRadius, minorRadius) {
15495
+ const u2 = atan2(ly, lx) * majorRadius;
15496
+ const xyDist = sqrt$3(lx * lx + ly * ly) - majorRadius;
15497
+ const v = atan2(lz, xyDist) * minorRadius;
15498
+ return [u2, v];
15499
+ }
15416
15500
  function compileUVFunction(analysis) {
15417
15501
  if (analysis.mode === "triplanar") return null;
15418
15502
  const toLocal = analysis.toLocal;
@@ -15421,19 +15505,14 @@ function compileUVFunction(analysis) {
15421
15505
  const R = analysis.radius;
15422
15506
  return (p2) => {
15423
15507
  const lp = toLocal(p2);
15424
- const u2 = atan2(lp[1], lp[0]) * R;
15425
- const len = sqrt$3(lp[0] * lp[0] + lp[1] * lp[1] + lp[2] * lp[2]);
15426
- const v = acos(clampUnit(lp[2] / (len || 1))) * R;
15427
- return [u2, v];
15508
+ return sphereUVLocal(lp[0], lp[1], lp[2], R);
15428
15509
  };
15429
15510
  }
15430
15511
  case "cylinder": {
15431
15512
  const r = analysis.radius;
15432
15513
  return (p2) => {
15433
15514
  const lp = toLocal(p2);
15434
- const u2 = atan2(lp[1], lp[0]) * r;
15435
- const v = lp[2];
15436
- return [u2, v];
15515
+ return cylinderUVLocal(lp[0], lp[1], lp[2], r);
15437
15516
  };
15438
15517
  }
15439
15518
  case "torus": {
@@ -15441,10 +15520,7 @@ function compileUVFunction(analysis) {
15441
15520
  const r = analysis.radius;
15442
15521
  return (p2) => {
15443
15522
  const lp = toLocal(p2);
15444
- const u2 = atan2(lp[1], lp[0]) * R;
15445
- const xyDist = sqrt$3(lp[0] * lp[0] + lp[1] * lp[1]) - R;
15446
- const v = atan2(lp[2], xyDist) * r;
15447
- return [u2, v];
15523
+ return torusUVLocal(lp[0], lp[1], lp[2], R, r);
15448
15524
  };
15449
15525
  }
15450
15526
  }
@@ -15755,8 +15831,8 @@ function sdProfileRevolve$1(profileFn, degrees, x2, y2, z2) {
15755
15831
  const profileDistance = profileFn(radial, z2);
15756
15832
  if (abs(degrees) >= 360) return profileDistance;
15757
15833
  const sweep = abs(degrees) * DEG$1;
15758
- const center = degrees * DEG$1 * 0.5;
15759
- let angle = Math.atan2(y2, x2) - center;
15834
+ const center2 = degrees * DEG$1 * 0.5;
15835
+ let angle = Math.atan2(y2, x2) - center2;
15760
15836
  while (angle <= -PI$1) angle += PI$1 * 2;
15761
15837
  while (angle > PI$1) angle -= PI$1 * 2;
15762
15838
  const half = sweep * 0.5;
@@ -15774,9 +15850,9 @@ function smax$1(a2, b, k2) {
15774
15850
  function repeatCoord$1(v, spacing, count) {
15775
15851
  if (spacing <= 0) return v;
15776
15852
  if (count > 0) {
15777
- const center = (count - 1) * 0.5;
15778
- const index2 = clamp$3(Math.round(v / spacing + center), 0, count - 1);
15779
- return v - (index2 - center) * spacing;
15853
+ const center2 = (count - 1) * 0.5;
15854
+ const index2 = clamp$3(Math.round(v / spacing + center2), 0, count - 1);
15855
+ return v - (index2 - center2) * spacing;
15780
15856
  }
15781
15857
  return v - spacing * Math.round(v / spacing);
15782
15858
  }
@@ -16702,8 +16778,8 @@ function sdProfileRevolve(b, node, x2, y2, z2) {
16702
16778
  const profileDistance = profilePolygonsSdf(b, node.polygons, radial, z2);
16703
16779
  if (Math.abs(node.degrees) >= 360) return profileDistance;
16704
16780
  const sweep = Math.abs(node.degrees) * DEG;
16705
- const center = node.degrees * DEG * 0.5;
16706
- const angle = b.sub(b.atan2(y2, x2), b.constant(center));
16781
+ const center2 = node.degrees * DEG * 0.5;
16782
+ const angle = b.sub(b.atan2(y2, x2), b.constant(center2));
16707
16783
  const wrapped = b.sub(angle, b.mul(b.constant(TAU), b.round(b.div(angle, b.constant(TAU)))));
16708
16784
  const absAngle = b.abs(wrapped);
16709
16785
  const half = sweep * 0.5;
@@ -16770,9 +16846,9 @@ function select01(b, selector, ifOne, ifZero) {
16770
16846
  function repeatCoord(b, v, spacing, count) {
16771
16847
  if (spacing <= 0) return v;
16772
16848
  if (count > 0) {
16773
- const center = (count - 1) * 0.5;
16774
- const index2 = clampSlot(b, b.round(b.add(b.div(v, b.constant(spacing)), b.constant(center))), 0, count - 1);
16775
- return b.sub(v, b.mul(b.sub(index2, b.constant(center)), b.constant(spacing)));
16849
+ const center2 = (count - 1) * 0.5;
16850
+ const index2 = clampSlot(b, b.round(b.add(b.div(v, b.constant(spacing)), b.constant(center2))), 0, count - 1);
16851
+ return b.sub(v, b.mul(b.sub(index2, b.constant(center2)), b.constant(spacing)));
16776
16852
  }
16777
16853
  return b.sub(v, b.mul(b.constant(spacing), b.round(b.div(v, b.constant(spacing)))));
16778
16854
  }
@@ -16863,7 +16939,7 @@ async function initTruckGeometryWasm() {
16863
16939
  if (_initPromise$1) return _initPromise$1;
16864
16940
  _initPromise$1 = (async () => {
16865
16941
  try {
16866
- const geometryModule = await import("./forgecad_geometry-BlMtqluF.js");
16942
+ const geometryModule = await import("./forgecad_geometry-CZ_IfuvA.js");
16867
16943
  const isNode = isNodeRuntime();
16868
16944
  if (isNode) {
16869
16945
  const { readFileSync, existsSync } = await import("./__vite-browser-external-Dhvy_jtL.js");
@@ -17328,23 +17404,62 @@ class NurbsSurface {
17328
17404
  return [wx / wSum, wy / wSum, wz / wSum];
17329
17405
  }
17330
17406
  /**
17331
- * Evaluate the surface normal at (u, v) via cross product of partial derivatives.
17407
+ * Evaluate the surface unit normal at (u, v) from analytic first derivatives.
17408
+ *
17409
+ * Uses Algorithm A2.3 basis-function derivatives with the rational quotient
17410
+ * rule, so the normal is exact (no finite-difference epsilon, no error near
17411
+ * the boundary). Constant chain-rule factors from the parameter remap scale Su
17412
+ * and Sv positively and cancel under normalization, so they are omitted.
17332
17413
  */
17333
17414
  normalAt(u2, v) {
17334
- const eps = 1e-5;
17335
- const u0 = Math.max(0, u2 - eps), u1 = Math.min(1, u2 + eps);
17336
- const v0 = Math.max(0, v - eps), v1 = Math.min(1, v + eps);
17337
- const pu = this.pointAt(u1, v), pmu = this.pointAt(u0, v);
17338
- const pv = this.pointAt(u2, v1), pmv = this.pointAt(u2, v0);
17339
- const du = [pu[0] - pmu[0], pu[1] - pmu[1], pu[2] - pmu[2]];
17340
- const dv = [pv[0] - pmv[0], pv[1] - pmv[1], pv[2] - pmv[2]];
17341
- const nx = du[1] * dv[2] - du[2] * dv[1];
17342
- const ny = du[2] * dv[0] - du[0] * dv[2];
17343
- const nz = du[0] * dv[1] - du[1] * dv[0];
17415
+ const { Su, Sv } = this.derivativesAt(u2, v);
17416
+ const nx = Su[1] * Sv[2] - Su[2] * Sv[1];
17417
+ const ny = Su[2] * Sv[0] - Su[0] * Sv[2];
17418
+ const nz = Su[0] * Sv[1] - Su[1] * Sv[0];
17344
17419
  const len = Math.sqrt(nx * nx + ny * ny + nz * nz);
17345
17420
  if (len < 1e-12) return [0, 0, 1];
17346
17421
  return [nx / len, ny / len, nz / len];
17347
17422
  }
17423
+ /** Analytic first partial derivatives S_u, S_v (rational quotient rule). */
17424
+ derivativesAt(u2, v) {
17425
+ const uu = this.remapU(Math.max(0, Math.min(1, u2)));
17426
+ const vv = this.remapV(Math.max(0, Math.min(1, v)));
17427
+ const spanU = findSpan(this.nU, this.degreeU, uu, this.knotsU);
17428
+ const spanV = findSpan(this.nV, this.degreeV, vv, this.knotsV);
17429
+ const dU = basisFunsDeriv(spanU, uu, this.degreeU, this.knotsU, 1);
17430
+ const dV = basisFunsDeriv(spanV, vv, this.degreeV, this.knotsV, 1);
17431
+ const A = [0, 0, 0];
17432
+ const Au = [0, 0, 0];
17433
+ const Av = [0, 0, 0];
17434
+ let w2 = 0;
17435
+ let wu = 0;
17436
+ let wv = 0;
17437
+ for (let i = 0; i <= this.degreeU; i++) {
17438
+ const rowIdx = spanU - this.degreeU + i;
17439
+ for (let j = 0; j <= this.degreeV; j++) {
17440
+ const colIdx = spanV - this.degreeV + j;
17441
+ const weight = this.weightsGrid[rowIdx][colIdx];
17442
+ const pt = this.controlGrid[rowIdx][colIdx];
17443
+ const n00 = dU[0][i] * dV[0][j] * weight;
17444
+ const n10 = dU[1][i] * dV[0][j] * weight;
17445
+ const n01 = dU[0][i] * dV[1][j] * weight;
17446
+ w2 += n00;
17447
+ wu += n10;
17448
+ wv += n01;
17449
+ for (let c2 = 0; c2 < 3; c2++) {
17450
+ A[c2] += n00 * pt[c2];
17451
+ Au[c2] += n10 * pt[c2];
17452
+ Av[c2] += n01 * pt[c2];
17453
+ }
17454
+ }
17455
+ }
17456
+ if (w2 === 0) return { S: [0, 0, 0], Su: [0, 0, 0], Sv: [0, 0, 0] };
17457
+ const invW = 1 / w2;
17458
+ const S = [A[0] * invW, A[1] * invW, A[2] * invW];
17459
+ const Su = [(Au[0] - wu * S[0]) * invW, (Au[1] - wu * S[1]) * invW, (Au[2] - wu * S[2]) * invW];
17460
+ const Sv = [(Av[0] - wv * S[0]) * invW, (Av[1] - wv * S[1]) * invW, (Av[2] - wv * S[2]) * invW];
17461
+ return { S, Su, Sv };
17462
+ }
17348
17463
  /**
17349
17464
  * Tessellate the surface into a triangle mesh.
17350
17465
  * Returns positions, normals, and triangle indices.
@@ -17789,49 +17904,55 @@ function signedArea2D(loop) {
17789
17904
  }
17790
17905
  return area2 * 0.5;
17791
17906
  }
17792
- function pointInLoop$2(point2, loop) {
17907
+ function buildSdfLoopEdges(pts) {
17908
+ const n = pts.length;
17909
+ const ax = new Float64Array(n);
17910
+ const ay = new Float64Array(n);
17911
+ const ex = new Float64Array(n);
17912
+ const ey = new Float64Array(n);
17913
+ const invLen2 = new Float64Array(n);
17914
+ for (let i = 0; i < n; i += 1) {
17915
+ const a2 = pts[i];
17916
+ const b = pts[(i + 1) % n];
17917
+ const vx = b[0] - a2[0];
17918
+ const vy = b[1] - a2[1];
17919
+ ax[i] = a2[0];
17920
+ ay[i] = a2[1];
17921
+ ex[i] = vx;
17922
+ ey[i] = vy;
17923
+ const len2 = vx * vx + vy * vy;
17924
+ invLen2[i] = len2 < 1e-12 ? 0 : 1 / len2;
17925
+ }
17926
+ return { n, area: signedArea2D(pts), ax, ay, ex, ey, invLen2 };
17927
+ }
17928
+ function loopSignedDistanceScalar(px, py, e) {
17929
+ let minDist2 = Infinity;
17793
17930
  let inside = false;
17794
- const [px, py] = point2;
17795
- for (let index2 = 0, prev = loop.length - 1; index2 < loop.length; prev = index2, index2 += 1) {
17796
- const [xi, yi] = loop[index2];
17797
- const [xj, yj] = loop[prev];
17798
- const intersects2 = yi > py !== yj > py && px < (xj - xi) * (py - yi) / (yj - yi + 1e-20) + xi;
17799
- if (intersects2) inside = !inside;
17800
- }
17801
- return inside;
17802
- }
17803
- function pointSegDist2D(point2, a2, b) {
17804
- const abx = b[0] - a2[0];
17805
- const aby = b[1] - a2[1];
17806
- const apx = point2[0] - a2[0];
17807
- const apy = point2[1] - a2[1];
17808
- const den = abx * abx + aby * aby;
17809
- const t = den < 1e-12 ? 0 : clamp$1((apx * abx + apy * aby) / den, 0, 1);
17810
- const qx = a2[0] + abx * t;
17811
- const qy = a2[1] + aby * t;
17812
- const dx = point2[0] - qx;
17813
- const dy = point2[1] - qy;
17814
- return Math.sqrt(dx * dx + dy * dy);
17815
- }
17816
- function loopSignedDistance(point2, loop) {
17817
- let minDist = Infinity;
17818
- for (let index2 = 0; index2 < loop.length; index2 += 1) {
17819
- const a2 = loop[index2];
17820
- const b = loop[(index2 + 1) % loop.length];
17821
- minDist = Math.min(minDist, pointSegDist2D(point2, a2, b));
17822
- }
17823
- return pointInLoop$2(point2, loop) ? minDist : -minDist;
17931
+ for (let i = 0; i < e.n; i += 1) {
17932
+ const ax = e.ax[i];
17933
+ const ay = e.ay[i];
17934
+ const vx = e.ex[i];
17935
+ const vy = e.ey[i];
17936
+ const t = clamp$1(((px - ax) * vx + (py - ay) * vy) * e.invLen2[i], 0, 1);
17937
+ const ddx = px - (ax + vx * t);
17938
+ const ddy = py - (ay + vy * t);
17939
+ const d22 = ddx * ddx + ddy * ddy;
17940
+ if (d22 < minDist2) minDist2 = d22;
17941
+ const by = ay + vy;
17942
+ if (ay > py !== by > py && px < vx * (py - ay) / (vy + 1e-20) + ax) inside = !inside;
17943
+ }
17944
+ const d2 = Math.sqrt(minDist2);
17945
+ return inside ? d2 : -d2;
17824
17946
  }
17825
17947
  function compilePolygonsSdf(polygons) {
17826
- const loops = polygons.filter((loop) => Array.isArray(loop) && loop.length >= 3).map((loop) => ({ pts: loop.map(([x2, y2]) => [x2, y2]), area: signedArea2D(loop) }));
17948
+ const loops = polygons.filter((loop) => Array.isArray(loop) && loop.length >= 3).map((loop) => buildSdfLoopEdges(loop.map(([x2, y2]) => [x2, y2])));
17827
17949
  if (loops.length === 0) {
17828
17950
  return () => -1;
17829
17951
  }
17830
17952
  return (x2, y2) => {
17831
- const point2 = [x2, y2];
17832
17953
  let field2 = -Infinity;
17833
17954
  for (const loop of loops) {
17834
- const loopField = loopSignedDistance(point2, loop.pts);
17955
+ const loopField = loopSignedDistanceScalar(x2, y2, loop);
17835
17956
  field2 = loop.area >= 0 ? Math.max(field2, loopField) : Math.min(field2, -loopField);
17836
17957
  }
17837
17958
  return field2;
@@ -17989,24 +18110,40 @@ function interpolateSweepFrame(segment, alpha, origin) {
17989
18110
  function findNearestSweepPoint(point2, segments) {
17990
18111
  let bestIndex = 0;
17991
18112
  let bestAlpha = 0;
17992
- let bestPoint = segments[0].a;
18113
+ let bestAlong = 0;
17993
18114
  let bestDist2 = Infinity;
18115
+ const px = point2[0];
18116
+ const py = point2[1];
18117
+ const pz = point2[2];
17994
18118
  for (let index2 = 0; index2 < segments.length; index2 += 1) {
17995
18119
  const segment = segments[index2];
17996
- const offset = vec3Sub(point2, segment.a);
17997
- const along = clamp$1(vec3Dot(offset, segment.t), 0, segment.len);
17998
- const alpha = segment.len > 1e-9 ? along / segment.len : 0;
17999
- const pointOnPath = vec3Add(segment.a, vec3Scale(segment.t, along));
18000
- const delta = vec3Sub(point2, pointOnPath);
18001
- const dist2 = vec3Dot(delta, delta);
18120
+ const ax = segment.a[0];
18121
+ const ay = segment.a[1];
18122
+ const az = segment.a[2];
18123
+ const tx = segment.t[0];
18124
+ const ty = segment.t[1];
18125
+ const tz = segment.t[2];
18126
+ const along = clamp$1((px - ax) * tx + (py - ay) * ty + (pz - az) * tz, 0, segment.len);
18127
+ const qx = ax + tx * along;
18128
+ const qy = ay + ty * along;
18129
+ const qz = az + tz * along;
18130
+ const dx = px - qx;
18131
+ const dy = py - qy;
18132
+ const dz = pz - qz;
18133
+ const dist2 = dx * dx + dy * dy + dz * dz;
18002
18134
  if (dist2 < bestDist2) {
18003
18135
  bestIndex = index2;
18004
- bestAlpha = alpha;
18005
- bestPoint = pointOnPath;
18136
+ bestAlong = along;
18137
+ bestAlpha = segment.len > 1e-9 ? along / segment.len : 0;
18006
18138
  bestDist2 = dist2;
18007
18139
  }
18008
18140
  }
18009
18141
  const bestSegment = segments[bestIndex];
18142
+ const bestPoint = [
18143
+ bestSegment.a[0] + bestSegment.t[0] * bestAlong,
18144
+ bestSegment.a[1] + bestSegment.t[1] * bestAlong,
18145
+ bestSegment.a[2] + bestSegment.t[2] * bestAlong
18146
+ ];
18010
18147
  return {
18011
18148
  segmentIndex: bestIndex,
18012
18149
  segment: bestSegment,
@@ -18995,8 +19132,8 @@ function stitchSingleLoopLoft(loops, heights, wasm, options) {
18995
19132
  return null;
18996
19133
  }
18997
19134
  }
18998
- function surfaceNormal(chord, span) {
18999
- const n = cross3$6(chord, span);
19135
+ function surfaceNormal(chord, span2) {
19136
+ const n = cross3$6(chord, span2);
19000
19137
  const len = Math.hypot(n[0], n[1], n[2]);
19001
19138
  if (len < 1e-12) {
19002
19139
  const radial = Math.hypot(chord[0], chord[1]);
@@ -19656,6 +19793,30 @@ function fromSlicesSingleSliceHalfExtentForManifold(plan) {
19656
19793
  }
19657
19794
  return Math.max(1, radius + maxOffsetMagnitude + plan.boundsPadding + plan.edgeLength * 3);
19658
19795
  }
19796
+ const BOOLEAN_OPERAND_NORMAL_SHARP_ANGLE_DEG = 60;
19797
+ const BOOLEAN_OPERAND_UV_EXTRA_PROP_MIN = 4;
19798
+ function rejectParametricUvBooleanOperands(shapes) {
19799
+ for (const shape of shapes) {
19800
+ if (shape.numProp() >= BOOLEAN_OPERAND_UV_EXTRA_PROP_MIN) {
19801
+ throw new Error(
19802
+ "Cannot boolean a shape that carries parametric UV (mesh numProp 8, vertProperties[6..7]). Parametric UV is baked into the mesh and is destroyed by cuts — Manifold interpolates the uv channels from the other operand onto every new cut/seam vertex. Apply booleans BEFORE texturing the result, or use a projected texture (Shape.wrapTexture with Wrap.*), which is computed from final position and survives booleans."
19803
+ );
19804
+ }
19805
+ }
19806
+ }
19807
+ function promoteBooleanOperandNormals(shapes) {
19808
+ let maxExtra = 0;
19809
+ for (const shape of shapes) maxExtra = Math.max(maxExtra, shape.numProp());
19810
+ if (maxExtra < 3) return { operands: shapes, created: [] };
19811
+ const created = [];
19812
+ const operands = shapes.map((shape) => {
19813
+ if (shape.numProp() >= 3) return shape;
19814
+ const promoted = shape.calculateNormals(0, BOOLEAN_OPERAND_NORMAL_SHARP_ANGLE_DEG);
19815
+ created.push(promoted);
19816
+ return promoted;
19817
+ });
19818
+ return { operands, created };
19819
+ }
19659
19820
  function lowerShapeBooleanCompilePlan(plan, wasm) {
19660
19821
  const shapes = plan.shapes.map((shape) => lowerShapeCompilePlanToManifold(shape, wasm));
19661
19822
  if (shapes.length === 0) {
@@ -19664,16 +19825,24 @@ function lowerShapeBooleanCompilePlan(plan, wasm) {
19664
19825
  if (shapes.length === 1) {
19665
19826
  return shapes[0];
19666
19827
  }
19828
+ try {
19829
+ rejectParametricUvBooleanOperands(shapes);
19830
+ } catch (error) {
19831
+ disposeWasmObjects(shapes);
19832
+ throw error;
19833
+ }
19834
+ const { operands, created } = promoteBooleanOperandNormals(shapes);
19667
19835
  try {
19668
19836
  switch (plan.op) {
19669
19837
  case "union":
19670
- return wasm.Manifold.union(shapes);
19838
+ return wasm.Manifold.union(operands);
19671
19839
  case "difference":
19672
- return wasm.Manifold.difference(shapes);
19840
+ return wasm.Manifold.difference(operands);
19673
19841
  case "intersection":
19674
- return wasm.Manifold.intersection(shapes);
19842
+ return wasm.Manifold.intersection(operands);
19675
19843
  }
19676
19844
  } finally {
19845
+ disposeWasmObjects(created);
19677
19846
  disposeWasmObjects(shapes);
19678
19847
  }
19679
19848
  }
@@ -20419,9 +20588,9 @@ function lowerVerticalVariableSweepStitchedForManifold(plan, sectionPolygons, wa
20419
20588
  return null;
20420
20589
  }
20421
20590
  const frameY = cross3$5(tangent, frameX);
20422
- const span = end[2] - start[2];
20591
+ const span2 = end[2] - start[2];
20423
20592
  const sections = sectionPolygons.map((section) => ({
20424
- height: start[2] + span * section.t,
20593
+ height: start[2] + span2 * section.t,
20425
20594
  polygons: section.polygons.map(
20426
20595
  (loop) => loop.map(([u2, v]) => [start[0] + u2 * frameX[0] + v * frameY[0], start[1] + u2 * frameX[1] + v * frameY[1]])
20427
20596
  )
@@ -21023,12 +21192,16 @@ function lowerNurbsSurfaceToManifold(plan, wasm) {
21023
21192
  const { positions, normals, indices } = surface.tessellate(res, res);
21024
21193
  const thickness = plan.thickness;
21025
21194
  const numVerts = positions.length;
21026
- const allPositions = [];
21027
- for (const [x2, y2, z2] of positions) allPositions.push(x2, y2, z2);
21195
+ const vertProps = [];
21028
21196
  for (let i = 0; i < numVerts; i++) {
21029
21197
  const [x2, y2, z2] = positions[i];
21030
21198
  const [nx, ny, nz] = normals[i];
21031
- allPositions.push(x2 - nx * thickness, y2 - ny * thickness, z2 - nz * thickness);
21199
+ vertProps.push(x2, y2, z2, nx, ny, nz);
21200
+ }
21201
+ for (let i = 0; i < numVerts; i++) {
21202
+ const [x2, y2, z2] = positions[i];
21203
+ const [nx, ny, nz] = normals[i];
21204
+ vertProps.push(x2 - nx * thickness, y2 - ny * thickness, z2 - nz * thickness, -nx, -ny, -nz);
21032
21205
  }
21033
21206
  const allIndices = [];
21034
21207
  for (const idx of indices) allIndices.push(idx);
@@ -21053,11 +21226,12 @@ function lowerNurbsSurfaceToManifold(plan, wasm) {
21053
21226
  allIndices.push(c2, c2 + numVerts, d2, d2, c2 + numVerts, d2 + numVerts);
21054
21227
  }
21055
21228
  const mesh = new wasm.Mesh({
21056
- numProp: 3,
21057
- vertProperties: new Float32Array(allPositions),
21229
+ numProp: 6,
21230
+ vertProperties: new Float32Array(vertProps),
21058
21231
  triVerts: new Uint32Array(allIndices)
21059
21232
  });
21060
21233
  try {
21234
+ mesh.merge();
21061
21235
  return new wasm.Manifold(mesh);
21062
21236
  } finally {
21063
21237
  disposeWasmObject(mesh);
@@ -22963,7 +23137,7 @@ function lowerAxisymmetricEllipsoidForOCCT(oc, radius, heightRadius) {
22963
23137
  }
22964
23138
  function lowerOrthogonalEllipseFromSlicesPlan(oc, plan) {
22965
23139
  if (plan.groups.length !== 2) return null;
22966
- const center = [0, 0, 0];
23140
+ const center2 = [0, 0, 0];
22967
23141
  const radii = [null, null, null];
22968
23142
  for (const group of plan.groups) {
22969
23143
  if (group.slices.length !== 1) return null;
@@ -22975,7 +23149,7 @@ function lowerOrthogonalEllipseFromSlicesPlan(oc, plan) {
22975
23149
  const uAxis = axisAlignedDirectionForOCCT(frame.u);
22976
23150
  const vAxis = axisAlignedDirectionForOCCT(frame.v);
22977
23151
  if (!normalAxis || !uAxis || !vAxis) return null;
22978
- center[normalAxis.axis] = normalAxis.sign * slice.offset;
23152
+ center2[normalAxis.axis] = normalAxis.sign * slice.offset;
22979
23153
  if (!setEllipsoidRadiusForOCCT(radii, uAxis.axis, profileRadii[0])) return null;
22980
23154
  if (!setEllipsoidRadiusForOCCT(radii, vAxis.axis, profileRadii[1])) return null;
22981
23155
  }
@@ -22986,9 +23160,9 @@ function lowerOrthogonalEllipseFromSlicesPlan(oc, plan) {
22986
23160
  const axisymmetricTolerance = Math.max(1e-7, Math.max(Math.abs(rx), Math.abs(ry)) * 1e-7);
22987
23161
  if (Math.abs(rx - ry) > axisymmetricTolerance) return null;
22988
23162
  let shape = lowerAxisymmetricEllipsoidForOCCT(oc, (rx + ry) / 2, rz);
22989
- if (Math.hypot(center[0], center[1], center[2]) > 1e-10) {
23163
+ if (Math.hypot(center2[0], center2[1], center2[2]) > 1e-10) {
22990
23164
  const trsf = new oc.gp_Trsf_1();
22991
- trsf.SetTranslation_1(new oc.gp_Vec_4(center[0], center[1], center[2]));
23165
+ trsf.SetTranslation_1(new oc.gp_Vec_4(center2[0], center2[1], center2[2]));
22992
23166
  const moved = new oc.BRepBuilderAPI_Transform_2(shape, trsf, true);
22993
23167
  shape = moved.Shape();
22994
23168
  }
@@ -23910,8 +24084,8 @@ function resamplePolyline$1(points, count) {
23910
24084
  for (let index2 = 0; index2 < count; index2 += 1) {
23911
24085
  const target = index2 / (count - 1) * total;
23912
24086
  while (segment < cumulative.length - 2 && cumulative[segment + 1] < target) segment += 1;
23913
- const span = cumulative[segment + 1] - cumulative[segment];
23914
- const t = span > 1e-12 ? (target - cumulative[segment]) / span : 0;
24087
+ const span2 = cumulative[segment + 1] - cumulative[segment];
24088
+ const t = span2 > 1e-12 ? (target - cumulative[segment]) / span2 : 0;
23915
24089
  const a2 = points[segment];
23916
24090
  const b = points[segment + 1];
23917
24091
  out.push([a2[0] + (b[0] - a2[0]) * t, a2[1] + (b[1] - a2[1]) * t, a2[2] + (b[2] - a2[2]) * t]);
@@ -24042,19 +24216,23 @@ function surfaceGridForFillPlan$1(plan) {
24042
24216
  function surfaceGridForAnalyticPlan$1(plan) {
24043
24217
  const count = Math.max(2, plan.resolution + 1);
24044
24218
  const grid = [];
24219
+ const uvGrid = [];
24045
24220
  const { uMin, uMax, vMin, vMax } = plan.surface;
24046
24221
  for (let uIndex = 0; uIndex < count; uIndex += 1) {
24047
24222
  const uNorm = uIndex / (count - 1);
24048
24223
  const u2 = uMin + (uMax - uMin) * uNorm;
24049
24224
  const row = [];
24225
+ const uvRow = [];
24050
24226
  for (let vIndex = 0; vIndex < count; vIndex += 1) {
24051
24227
  const vNorm = vIndex / (count - 1);
24052
24228
  const v = vMin + (vMax - vMin) * vNorm;
24053
24229
  row.push(analyticSurfacePoint$1(plan.surface, u2, v));
24230
+ uvRow.push([uNorm, vNorm]);
24054
24231
  }
24055
24232
  grid.push(row);
24233
+ uvGrid.push(uvRow);
24056
24234
  }
24057
- return { grid, trim: sampleTrim(plan.trim) };
24235
+ return { grid, trim: sampleTrim(plan.trim), uvGrid };
24058
24236
  }
24059
24237
  function computeGridNormals(positions, indices) {
24060
24238
  const normals = Array.from({ length: positions.length }, () => [0, 0, 0]);
@@ -24080,6 +24258,7 @@ function meshFromSurfaceGrid(grid, trim = null, context = "SDF surface sheet", u
24080
24258
  const cols = ((_a3 = grid[0]) == null ? void 0 : _a3.length) ?? 0;
24081
24259
  if (rows < 2 || cols < 2) throw new Error(`${context} requires at least a 2x2 sampled grid.`);
24082
24260
  const positions = grid.flat();
24261
+ const hasParametricUv = uvGrid !== void 0;
24083
24262
  const uvs = [];
24084
24263
  if (uvGrid) {
24085
24264
  if (uvGrid.length !== rows || uvGrid.some((row) => row.length !== cols)) {
@@ -24091,6 +24270,16 @@ function meshFromSurfaceGrid(grid, trim = null, context = "SDF surface sheet", u
24091
24270
  for (let col = 0; col < cols; col += 1) uvs.push([row / (rows - 1), col / (cols - 1)]);
24092
24271
  }
24093
24272
  }
24273
+ const normalizeUv = (() => {
24274
+ if (!uvGrid) return (uv) => uv;
24275
+ const { uDomain, vDomain } = uvDomainFromGrid(uvGrid);
24276
+ const uSpan = uDomain[1] - uDomain[0];
24277
+ const vSpan = vDomain[1] - vDomain[0];
24278
+ return (uv) => [
24279
+ uSpan > 0 ? (uv[0] - uDomain[0]) / uSpan : 0,
24280
+ vSpan > 0 ? (uv[1] - vDomain[0]) / vSpan : 0
24281
+ ];
24282
+ })();
24094
24283
  const sourceIndices = [];
24095
24284
  const includeTriangle = (a2, b, c2) => {
24096
24285
  const centroid2 = [(uvs[a2][0] + uvs[b][0] + uvs[c2][0]) / 3, (uvs[a2][1] + uvs[b][1] + uvs[c2][1]) / 3];
@@ -24109,29 +24298,37 @@ function meshFromSurfaceGrid(grid, trim = null, context = "SDF surface sheet", u
24109
24298
  if (sourceIndices.length === 0) throw new Error(`${context} trim loops removed the whole sampled surface.`);
24110
24299
  const used = /* @__PURE__ */ new Map();
24111
24300
  const compactPositions = [];
24301
+ const compactUvs = [];
24112
24302
  const compactIndices = sourceIndices.map((sourceIndex) => {
24113
24303
  const existing = used.get(sourceIndex);
24114
24304
  if (existing !== void 0) return existing;
24115
24305
  const next = compactPositions.length;
24116
24306
  used.set(sourceIndex, next);
24117
24307
  compactPositions.push(positions[sourceIndex]);
24308
+ if (hasParametricUv) compactUvs.push(normalizeUv(uvs[sourceIndex]));
24118
24309
  return next;
24119
24310
  });
24120
24311
  const compactNormals = computeGridNormals(compactPositions, compactIndices);
24121
- const vertProperties = new Float32Array(compactPositions.length * 6);
24312
+ const numProp = hasParametricUv ? NUM_PROP_WITH_UV : NUM_PROP_WITH_NORMAL;
24313
+ const vertProperties = new Float32Array(compactPositions.length * numProp);
24122
24314
  for (let index2 = 0; index2 < compactPositions.length; index2 += 1) {
24123
24315
  const [x2, y2, z2] = compactPositions[index2];
24124
24316
  const [nx, ny, nz] = compactNormals[index2];
24125
- const offset = index2 * 6;
24126
- vertProperties[offset] = x2;
24127
- vertProperties[offset + 1] = y2;
24128
- vertProperties[offset + 2] = z2;
24129
- vertProperties[offset + 3] = nx;
24130
- vertProperties[offset + 4] = ny;
24131
- vertProperties[offset + 5] = nz;
24317
+ const offset = index2 * numProp;
24318
+ vertProperties[offset + POSITION_OFFSET] = x2;
24319
+ vertProperties[offset + POSITION_OFFSET + 1] = y2;
24320
+ vertProperties[offset + POSITION_OFFSET + 2] = z2;
24321
+ vertProperties[offset + NORMAL_OFFSET] = nx;
24322
+ vertProperties[offset + NORMAL_OFFSET + 1] = ny;
24323
+ vertProperties[offset + NORMAL_OFFSET + 2] = nz;
24324
+ if (hasParametricUv) {
24325
+ const [u2, v] = compactUvs[index2];
24326
+ vertProperties[offset + UV_OFFSET] = u2;
24327
+ vertProperties[offset + UV_OFFSET + 1] = v;
24328
+ }
24132
24329
  }
24133
24330
  return {
24134
- numProp: 6,
24331
+ numProp,
24135
24332
  numVert: compactPositions.length,
24136
24333
  numTri: compactIndices.length / 3,
24137
24334
  vertProperties,
@@ -24216,16 +24413,20 @@ function surfaceGridForNurbsPlan(plan) {
24216
24413
  });
24217
24414
  const resolution = Math.max(2, Math.round(plan.resolution));
24218
24415
  const grid = [];
24416
+ const uvGrid = [];
24219
24417
  for (let i = 0; i <= resolution; i += 1) {
24220
24418
  const u2 = i / resolution;
24221
24419
  const row = [];
24420
+ const uvRow = [];
24222
24421
  for (let j = 0; j <= resolution; j += 1) {
24223
24422
  const v = j / resolution;
24224
24423
  row.push(surface.pointAt(u2, v));
24424
+ uvRow.push([u2, v]);
24225
24425
  }
24226
24426
  grid.push(row);
24427
+ uvGrid.push(uvRow);
24227
24428
  }
24228
- return { grid, rowAxis: "u", trim: sampleTrim(plan.trim), uDomain: [0, 1], vDomain: [0, 1] };
24429
+ return { grid, rowAxis: "u", trim: sampleTrim(plan.trim), uDomain: [0, 1], vDomain: [0, 1], uvGrid };
24229
24430
  }
24230
24431
  function surfaceGridForSheetPlan(plan) {
24231
24432
  if (plan.kind === "queryOwner") return surfaceGridForSheetPlan(plan.base);
@@ -24240,8 +24441,8 @@ function surfaceGridForSheetPlan(plan) {
24240
24441
  }
24241
24442
  if (plan.kind === "nurbsSurface") return surfaceGridForNurbsPlan(plan);
24242
24443
  if (plan.kind === "analyticSurface") {
24243
- const { grid, trim } = surfaceGridForAnalyticPlan$1(plan);
24244
- return { grid, rowAxis: "u", trim, uDomain: [0, 1], vDomain: [0, 1] };
24444
+ const { grid, trim, uvGrid } = surfaceGridForAnalyticPlan$1(plan);
24445
+ return { grid, rowAxis: "u", trim, uDomain: [0, 1], vDomain: [0, 1], uvGrid };
24245
24446
  }
24246
24447
  if (plan.kind === "surfaceRuled") return { grid: surfaceGridForRuledPlan$1(plan), rowAxis: "v" };
24247
24448
  if (plan.kind === "surfaceFill") return { grid: surfaceGridForFillPlan$1(plan), rowAxis: "v" };
@@ -24515,8 +24716,8 @@ function meshFromOpenSurfaceSheetPlan(plan) {
24515
24716
  }
24516
24717
  if (plan.kind === "nurbsSurface") return meshFromOpenNurbsSurface(plan);
24517
24718
  if (plan.kind === "analyticSurface") {
24518
- const { grid, trim } = surfaceGridForAnalyticPlan$1(plan);
24519
- return meshFromSurfaceGrid(grid, trim, "SDF analytic surface");
24719
+ const { grid, trim, uvGrid } = surfaceGridForAnalyticPlan$1(plan);
24720
+ return meshFromSurfaceGrid(grid, trim, "SDF analytic surface", uvGrid);
24520
24721
  }
24521
24722
  if (plan.kind === "surfaceRuled") return meshFromSurfaceGrid(surfaceGridForRuledPlan$1(plan), null, "SDF ruled surface");
24522
24723
  if (plan.kind === "surfaceFill") return meshFromSurfaceFillPlan(plan);
@@ -31998,8 +32199,8 @@ function revolveProfilePolygonsToSdfField(polygons, degrees = 360) {
31998
32199
  const profileDistance = profile.eval(radial, z2);
31999
32200
  if (Math.abs(degrees) >= 360) return profileDistance;
32000
32201
  const sweep = Math.abs(degrees) * (Math.PI / 180);
32001
- const center = degrees * Math.PI / 360;
32002
- let angle = Math.atan2(y2, x2) - center;
32202
+ const center2 = degrees * Math.PI / 360;
32203
+ let angle = Math.atan2(y2, x2) - center2;
32003
32204
  while (angle <= -Math.PI) angle += Math.PI * 2;
32004
32205
  while (angle > Math.PI) angle -= Math.PI * 2;
32005
32206
  const half = sweep / 2;
@@ -33305,15 +33506,15 @@ function deBoorPoint(degree, knots, controlPoints, parameter) {
33305
33506
  const domainEnd = knots[controlPoints.length];
33306
33507
  const t = Math.max(domainStart, Math.min(domainEnd, parameter));
33307
33508
  if (t >= domainEnd - 1e-12) return controlPoints[controlPoints.length - 1];
33308
- let span = degree;
33509
+ let span2 = degree;
33309
33510
  const lastSpan = controlPoints.length - 1;
33310
- while (span < lastSpan && t >= knots[span + 1]) span += 1;
33511
+ while (span2 < lastSpan && t >= knots[span2 + 1]) span2 += 1;
33311
33512
  const points = [];
33312
- for (let index2 = 0; index2 <= degree; index2 += 1) points.push([...controlPoints[span - degree + index2]]);
33513
+ for (let index2 = 0; index2 <= degree; index2 += 1) points.push([...controlPoints[span2 - degree + index2]]);
33313
33514
  for (let order = 1; order <= degree; order += 1) {
33314
33515
  for (let index2 = degree; index2 >= order; index2 -= 1) {
33315
- const knotLeft = knots[span - degree + index2];
33316
- const knotRight = knots[span + index2 + 1 - order];
33516
+ const knotLeft = knots[span2 - degree + index2];
33517
+ const knotRight = knots[span2 + index2 + 1 - order];
33317
33518
  const alpha = Math.abs(knotRight - knotLeft) <= 1e-12 ? 0 : (t - knotLeft) / (knotRight - knotLeft);
33318
33519
  points[index2] = [
33319
33520
  points[index2 - 1][0] * (1 - alpha) + points[index2][0] * alpha,
@@ -33329,16 +33530,16 @@ function deBoorHomogeneousPoint(degree, knots, controlPoints, parameter) {
33329
33530
  const domainEnd = knots[controlPoints.length];
33330
33531
  const t = Math.max(domainStart, Math.min(domainEnd, parameter));
33331
33532
  if (t >= domainEnd - 1e-12) return controlPoints[controlPoints.length - 1];
33332
- let span = degree;
33533
+ let span2 = degree;
33333
33534
  const lastSpan = controlPoints.length - 1;
33334
- while (span < lastSpan && t >= knots[span + 1]) span += 1;
33535
+ while (span2 < lastSpan && t >= knots[span2 + 1]) span2 += 1;
33335
33536
  const points = [];
33336
33537
  for (let index2 = 0; index2 <= degree; index2 += 1)
33337
- points.push([...controlPoints[span - degree + index2]]);
33538
+ points.push([...controlPoints[span2 - degree + index2]]);
33338
33539
  for (let order = 1; order <= degree; order += 1) {
33339
33540
  for (let index2 = degree; index2 >= order; index2 -= 1) {
33340
- const knotLeft = knots[span - degree + index2];
33341
- const knotRight = knots[span + index2 + 1 - order];
33541
+ const knotLeft = knots[span2 - degree + index2];
33542
+ const knotRight = knots[span2 + index2 + 1 - order];
33342
33543
  const alpha = Math.abs(knotRight - knotLeft) <= 1e-12 ? 0 : (t - knotLeft) / (knotRight - knotLeft);
33343
33544
  points[index2] = [
33344
33545
  points[index2 - 1][0] * (1 - alpha) + points[index2][0] * alpha,
@@ -33400,9 +33601,9 @@ function closestBSplineParameter(curve, point2) {
33400
33601
  function wrappedBSplinePoint(curve, parameter) {
33401
33602
  const [domainStart, domainEnd] = bsplineDomain(curve);
33402
33603
  if (!curve.closed) return bsplinePoint(curve, parameter);
33403
- const span = domainEnd - domainStart;
33404
- let wrapped = (parameter - domainStart) % span + domainStart;
33405
- if (wrapped < domainStart) wrapped += span;
33604
+ const span2 = domainEnd - domainStart;
33605
+ let wrapped = (parameter - domainStart) % span2 + domainStart;
33606
+ if (wrapped < domainStart) wrapped += span2;
33406
33607
  return bsplinePoint(curve, wrapped);
33407
33608
  }
33408
33609
  function bsplineSurfacePoint(surface, u2, v) {
@@ -33718,8 +33919,8 @@ function resampleClosedLoop(loop, count) {
33718
33919
  for (let index2 = 0; index2 < count; index2 += 1) {
33719
33920
  const target = index2 / count * total;
33720
33921
  while (segment < points.length - 1 && cumulative[segment + 1] < target) segment += 1;
33721
- const span = cumulative[segment + 1] - cumulative[segment];
33722
- const t = span <= 1e-12 ? 0 : (target - cumulative[segment]) / span;
33922
+ const span2 = cumulative[segment + 1] - cumulative[segment];
33923
+ const t = span2 <= 1e-12 ? 0 : (target - cumulative[segment]) / span2;
33723
33924
  const a2 = points[segment];
33724
33925
  const b = points[(segment + 1) % points.length];
33725
33926
  out.push([a2[0] + (b[0] - a2[0]) * t, a2[1] + (b[1] - a2[1]) * t, a2[2] + (b[2] - a2[2]) * t]);
@@ -33747,14 +33948,14 @@ function orientRadialTriangles(triangles, frame, sameSense) {
33747
33948
  const flip = sameSense ? !radialOutward : radialOutward;
33748
33949
  return flip ? triangles.map(reverseTriangle) : triangles;
33749
33950
  }
33750
- function orientCenteredTriangles(triangles, center, sameSense) {
33951
+ function orientCenteredTriangles(triangles, center2, sameSense) {
33751
33952
  return triangles.map((triangle) => {
33752
33953
  const sample = [
33753
33954
  (triangle[0][0] + triangle[1][0] + triangle[2][0]) / 3,
33754
33955
  (triangle[0][1] + triangle[1][1] + triangle[2][1]) / 3,
33755
33956
  (triangle[0][2] + triangle[1][2] + triangle[2][2]) / 3
33756
33957
  ];
33757
- const outward = [sample[0] - center[0], sample[1] - center[1], sample[2] - center[2]];
33958
+ const outward = [sample[0] - center2[0], sample[1] - center2[1], sample[2] - center2[2]];
33758
33959
  const normal2 = cross3$4(
33759
33960
  [triangle[1][0] - triangle[0][0], triangle[1][1] - triangle[0][1], triangle[1][2] - triangle[0][2]],
33760
33961
  [triangle[2][0] - triangle[0][0], triangle[2][1] - triangle[0][1], triangle[2][2] - triangle[0][2]]
@@ -33993,8 +34194,8 @@ function orientRevolvedProfileTriangles(triangles, frame, axis, profileCenter, p
33993
34194
  (triangle[0][2] + triangle[1][2] + triangle[2][2]) / 3
33994
34195
  ];
33995
34196
  const angle = revolutionAngle(frame, sample, filePath, "SURFACE_OF_REVOLUTION");
33996
- const center = rotateAroundAxis(profileCenter, axis, normalizeAngleDelta(angle - profileAngle));
33997
- const outward = sub3$1(sample, center);
34197
+ const center2 = rotateAroundAxis(profileCenter, axis, normalizeAngleDelta(angle - profileAngle));
34198
+ const outward = sub3$1(sample, center2);
33998
34199
  if (Math.hypot(outward[0], outward[1], outward[2]) <= 1e-12)
33999
34200
  failStepImport(filePath, "SURFACE_OF_REVOLUTION sample hit degenerate profile centerline.");
34000
34201
  const normal2 = cross3$4(sub3$1(triangle[1], triangle[0]), sub3$1(triangle[2], triangle[0]));
@@ -34399,18 +34600,18 @@ function triangulateTrimmedBSplineSurface(surface, loops, sameSense, filePath, c
34399
34600
  const { outer: uvLoop, holes } = trimmedBSplineSurfaceUvLoops(surface, loops, filePath, context);
34400
34601
  if (holes.length > 0 || !isConvexUvLoop(uvLoop))
34401
34602
  return triangulateBSplineSurfaceUvPolygon(surface, uvLoop, holes, sameSense, filePath, context);
34402
- const center = uvCentroid(uvLoop);
34603
+ const center2 = uvCentroid(uvLoop);
34403
34604
  const ringCount = Math.max(3, Math.ceil(Math.max(surface.controlPoints.length, surface.controlPoints[0].length) * 1.5));
34404
34605
  const rings = [];
34405
34606
  for (let ringIndex = 1; ringIndex <= ringCount; ringIndex += 1) {
34406
34607
  const t = ringIndex / ringCount;
34407
- rings.push(uvLoop.map((point2) => [center[0] + (point2[0] - center[0]) * t, center[1] + (point2[1] - center[1]) * t]));
34608
+ rings.push(uvLoop.map((point2) => [center2[0] + (point2[0] - center2[0]) * t, center2[1] + (point2[1] - center2[1]) * t]));
34408
34609
  }
34409
34610
  const mapPoint = (point2) => bsplineSurfacePoint(surface, point2[0], point2[1]);
34410
34611
  const triangles = [];
34411
34612
  const firstRing = rings[0];
34412
34613
  for (let index2 = 0; index2 < firstRing.length; index2 += 1) {
34413
- triangles.push([mapPoint(center), mapPoint(firstRing[index2]), mapPoint(firstRing[(index2 + 1) % firstRing.length])]);
34614
+ triangles.push([mapPoint(center2), mapPoint(firstRing[index2]), mapPoint(firstRing[(index2 + 1) % firstRing.length])]);
34414
34615
  }
34415
34616
  for (let ringIndex = 1; ringIndex < rings.length; ringIndex += 1) {
34416
34617
  const inner = rings[ringIndex - 1];
@@ -34427,9 +34628,9 @@ function rotateArray(items, start) {
34427
34628
  return [...items.slice(start), ...items.slice(0, start)];
34428
34629
  }
34429
34630
  function periodicParameterDistance(a2, b, start, end) {
34430
- const span = end - start;
34631
+ const span2 = end - start;
34431
34632
  const raw = Math.abs(a2 - b);
34432
- return Math.min(raw, Math.max(0, span - raw));
34633
+ return Math.min(raw, Math.max(0, span2 - raw));
34433
34634
  }
34434
34635
  function closedUBSplineBoundaryRing(surface, loop, count, filePath, context) {
34435
34636
  const points = resampleClosedLoop(loop, count);
@@ -34694,11 +34895,11 @@ function sphericalLoopInfo(frame, radius, loop, filePath, context) {
34694
34895
  if (Math.abs(distance - radius) > tolerance)
34695
34896
  failStepImport(filePath, `${context} bounded SPHERICAL_SURFACE boundary point is not on the sphere.`);
34696
34897
  }
34697
- const center = centroid(points);
34898
+ const center2 = centroid(points);
34698
34899
  const normal2 = normalize3$2(newellNormal(points), `${context} bounded SPHERICAL_SURFACE boundary normal`);
34699
- const maxPlaneError = points.reduce((max2, point2) => Math.max(max2, Math.abs(dot3$3(sub3$1(point2, center), normal2))), 0);
34900
+ const maxPlaneError = points.reduce((max2, point2) => Math.max(max2, Math.abs(dot3$3(sub3$1(point2, center2), normal2))), 0);
34700
34901
  if (maxPlaneError > tolerance) failStepImport(filePath, `${context} bounded SPHERICAL_SURFACE boundary loop is not planar.`);
34701
- return { points, center, normal: normal2 };
34902
+ return { points, center: center2, normal: normal2 };
34702
34903
  }
34703
34904
  function triangulateBoundedSphericalBand(frame, radius, loops, sameSense, filePath, context) {
34704
34905
  if (loops.length !== 2) failStepImport(filePath, `${context} bounded SPHERICAL_SURFACE band supports exactly two loops right now.`);
@@ -34719,15 +34920,15 @@ function triangulateBoundedSphericalBand(frame, radius, loops, sameSense, filePa
34719
34920
  const xProjection = dot3$3(rawX, axis);
34720
34921
  const xAxis = normalize3$2([rawX[0] - axis[0] * xProjection, rawX[1] - axis[1] * xProjection, rawX[2] - axis[2] * xProjection], context);
34721
34922
  const yAxis = cross3$4(axis, xAxis);
34722
- const loopAngle = (point2, center) => {
34723
- const local = sub3$1(point2, center);
34923
+ const loopAngle = (point2, center2) => {
34924
+ const local = sub3$1(point2, center2);
34724
34925
  return Math.atan2(dot3$3(local, yAxis), dot3$3(local, xAxis));
34725
34926
  };
34726
- const rotateLoop = (loop, center, targetAngle) => {
34927
+ const rotateLoop = (loop, center2, targetAngle) => {
34727
34928
  let bestIndex = 0;
34728
34929
  let bestDistance = Number.POSITIVE_INFINITY;
34729
34930
  for (let index2 = 0; index2 < loop.length; index2 += 1) {
34730
- const distance = angleDistance(loopAngle(loop[index2], center), targetAngle);
34931
+ const distance = angleDistance(loopAngle(loop[index2], center2), targetAngle);
34731
34932
  if (distance < bestDistance) {
34732
34933
  bestDistance = distance;
34733
34934
  bestIndex = index2;
@@ -34771,14 +34972,14 @@ function triangulateBoundedSphericalBand(frame, radius, loops, sameSense, filePa
34771
34972
  return orientCenteredTriangles(triangles, frame.origin, sameSense);
34772
34973
  }
34773
34974
  function orientBridgeTriangles(triangles, aCenter, bCenter, sameSense) {
34774
- const center = [(aCenter[0] + bCenter[0]) / 2, (aCenter[1] + bCenter[1]) / 2, (aCenter[2] + bCenter[2]) / 2];
34975
+ const center2 = [(aCenter[0] + bCenter[0]) / 2, (aCenter[1] + bCenter[1]) / 2, (aCenter[2] + bCenter[2]) / 2];
34775
34976
  const sample = triangles[0];
34776
34977
  const mid = [
34777
34978
  (sample[0][0] + sample[1][0] + sample[2][0]) / 3,
34778
34979
  (sample[0][1] + sample[1][1] + sample[2][1]) / 3,
34779
34980
  (sample[0][2] + sample[1][2] + sample[2][2]) / 3
34780
34981
  ];
34781
- const outward = [mid[0] - center[0], mid[1] - center[1], mid[2] - center[2]];
34982
+ const outward = [mid[0] - center2[0], mid[1] - center2[1], mid[2] - center2[2]];
34782
34983
  const normal2 = cross3$4(
34783
34984
  [sample[1][0] - sample[0][0], sample[1][1] - sample[0][1], sample[1][2] - sample[0][2]],
34784
34985
  [sample[2][0] - sample[0][0], sample[2][1] - sample[0][1], sample[2][2] - sample[0][2]]
@@ -36286,18 +36487,18 @@ function circleFootprintFromProfile(plan) {
36286
36487
  if (plan.kind !== "circle") return null;
36287
36488
  const radius = Math.abs(plan.radius);
36288
36489
  if (!finitePositive(radius)) return null;
36289
- const center = transformProfilePointThrough([0, 0], plan.transforms);
36490
+ const center2 = transformProfilePointThrough([0, 0], plan.transforms);
36290
36491
  const xPoint = transformProfilePointThrough([1, 0], plan.transforms);
36291
36492
  const yPoint = transformProfilePointThrough([0, 1], plan.transforms);
36292
- const xAxis = [xPoint[0] - center[0], xPoint[1] - center[1]];
36293
- const yAxis = [yPoint[0] - center[0], yPoint[1] - center[1]];
36493
+ const xAxis = [xPoint[0] - center2[0], xPoint[1] - center2[1]];
36494
+ const yAxis = [yPoint[0] - center2[0], yPoint[1] - center2[1]];
36294
36495
  const xScale = Math.hypot(xAxis[0], xAxis[1]);
36295
36496
  const yScale = Math.hypot(yAxis[0], yAxis[1]);
36296
36497
  const dot2 = xAxis[0] * yAxis[0] + xAxis[1] * yAxis[1];
36297
36498
  if (!finitePositive(xScale) || !finitePositive(yScale)) return null;
36298
36499
  if (Math.abs(xScale - yScale) > EPS || Math.abs(dot2) > EPS * xScale * yScale) return null;
36299
36500
  return {
36300
- center,
36501
+ center: center2,
36301
36502
  radius: radius * xScale,
36302
36503
  segments: plan.segments
36303
36504
  };
@@ -36856,19 +37057,19 @@ function explicitGeometrySurface(geometry, face) {
36856
37057
  }
36857
37058
  }
36858
37059
  if ("AnalyticSphere" in geometry) {
36859
- const center = geometry.AnalyticSphere.center;
37060
+ const center2 = geometry.AnalyticSphere.center;
36860
37061
  const radius = geometry.AnalyticSphere.radius;
36861
- if (isVec3(center) && isFiniteNumber(radius) && radius > 0) {
36862
- return { kind: "sphere", center, radius };
37062
+ if (isVec3(center2) && isFiniteNumber(radius) && radius > 0) {
37063
+ return { kind: "sphere", center: center2, radius };
36863
37064
  }
36864
37065
  }
36865
37066
  if ("AnalyticTorus" in geometry) {
36866
- const center = geometry.AnalyticTorus.center;
37067
+ const center2 = geometry.AnalyticTorus.center;
36867
37068
  const axis = geometry.AnalyticTorus.axis;
36868
37069
  const majorRadius = geometry.AnalyticTorus.major_radius;
36869
37070
  const minorRadius = geometry.AnalyticTorus.minor_radius;
36870
- if (isVec3(center) && isVec3(axis) && isFiniteNumber(majorRadius) && isFiniteNumber(minorRadius) && majorRadius > 0 && minorRadius > 0) {
36871
- return { kind: "torus", center, axis: normalizeVec3(axis), majorRadius, minorRadius };
37071
+ if (isVec3(center2) && isVec3(axis) && isFiniteNumber(majorRadius) && isFiniteNumber(minorRadius) && majorRadius > 0 && minorRadius > 0) {
37072
+ return { kind: "torus", center: center2, axis: normalizeVec3(axis), majorRadius, minorRadius };
36872
37073
  }
36873
37074
  }
36874
37075
  if ("NurbsPatch" in geometry) {
@@ -36962,13 +37163,13 @@ function explicitGeometrySurface(geometry, face) {
36962
37163
  function explicitEdgeCurve(geometry, faceName) {
36963
37164
  var _a3, _b3, _c2, _d2, _e2;
36964
37165
  if (!geometry) return void 0;
36965
- const center = (_a3 = geometry.CircularArc) == null ? void 0 : _a3.center;
37166
+ const center2 = (_a3 = geometry.CircularArc) == null ? void 0 : _a3.center;
36966
37167
  const axis = (_b3 = geometry.CircularArc) == null ? void 0 : _b3.axis;
36967
37168
  const radius = (_c2 = geometry.CircularArc) == null ? void 0 : _c2.radius;
36968
- if (center !== void 0 && axis !== void 0 && radius !== void 0 && isVec3(center) && isVec3(axis) && isFiniteNumber(radius) && radius > 0) {
37169
+ if (center2 !== void 0 && axis !== void 0 && radius !== void 0 && isVec3(center2) && isVec3(axis) && isFiniteNumber(radius) && radius > 0) {
36969
37170
  return {
36970
37171
  kind: "circle",
36971
- center,
37172
+ center: center2,
36972
37173
  axis: normalizeVec3(axis),
36973
37174
  radius,
36974
37175
  faceName
@@ -37485,6 +37686,7 @@ const _TruckShapeBackend = class _TruckShapeBackend {
37485
37686
  const payload = JSON.parse(getTruckGeometryWasm().geometry_mesh(this.getLiveHandle("getMesh()")));
37486
37687
  const numTri = payload.triangles.length / 3;
37487
37688
  const numVert = payload.positions.length / 3;
37689
+ const cornerNormals = payload.normals && payload.normals.length === numTri * 9 ? new Float32Array(payload.normals) : void 0;
37488
37690
  this.resource.mesh = {
37489
37691
  numProp: 3,
37490
37692
  numTri,
@@ -37498,7 +37700,8 @@ const _TruckShapeBackend = class _TruckShapeBackend {
37498
37700
  runTransform: new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0]),
37499
37701
  faceID: new Int32Array(payload.face_ids),
37500
37702
  faceIdNames: payload.face_id_names ?? [],
37501
- halfedgeTangent: new Float32Array(0)
37703
+ halfedgeTangent: new Float32Array(0),
37704
+ cornerNormals
37502
37705
  };
37503
37706
  return this.resource.mesh;
37504
37707
  }
@@ -38719,6 +38922,16 @@ function boundsInteriorOverlap(a2, b) {
38719
38922
  const tolerance = 1e-8;
38720
38923
  return Math.min(a2.max[0], b.max[0]) - Math.max(a2.min[0], b.min[0]) > tolerance && Math.min(a2.max[1], b.max[1]) - Math.max(a2.min[1], b.min[1]) > tolerance && Math.min(a2.max[2], b.max[2]) - Math.max(a2.min[2], b.min[2]) > tolerance;
38721
38924
  }
38925
+ function boundsFaceOrInteriorOverlap(a2, b) {
38926
+ const tolerance = 1e-8;
38927
+ const overlaps = [
38928
+ Math.min(a2.max[0], b.max[0]) - Math.max(a2.min[0], b.min[0]),
38929
+ Math.min(a2.max[1], b.max[1]) - Math.max(a2.min[1], b.min[1]),
38930
+ Math.min(a2.max[2], b.max[2]) - Math.max(a2.min[2], b.min[2])
38931
+ ];
38932
+ if (overlaps.some((overlap) => overlap < -tolerance)) return false;
38933
+ return overlaps.filter((overlap) => overlap <= tolerance).length <= 1;
38934
+ }
38722
38935
  function boundsFromPoints(points) {
38723
38936
  const first = points[0];
38724
38937
  if (!first) return null;
@@ -38801,16 +39014,16 @@ function shapePlanBounds(plan) {
38801
39014
  return null;
38802
39015
  }
38803
39016
  }
38804
- function hasPairwiseInteriorBoundsOverlap(shapes) {
39017
+ function hasPairwiseFaceOrInteriorBoundsOverlap(shapes) {
38805
39018
  const bounds = shapes.map((shape) => shape.boundingBox());
38806
39019
  for (let i = 0; i < bounds.length; i++) {
38807
39020
  for (let j = i + 1; j < bounds.length; j++) {
38808
- if (boundsInteriorOverlap(bounds[i], bounds[j])) return true;
39021
+ if (boundsFaceOrInteriorOverlap(bounds[i], bounds[j])) return true;
38809
39022
  }
38810
39023
  }
38811
39024
  return false;
38812
39025
  }
38813
- function interiorOverlapComponents(shapes) {
39026
+ function faceOrInteriorOverlapComponents(shapes) {
38814
39027
  const bounds = shapes.map((shape) => shape.boundingBox());
38815
39028
  const visited = /* @__PURE__ */ new Set();
38816
39029
  const components = [];
@@ -38824,7 +39037,7 @@ function interiorOverlapComponents(shapes) {
38824
39037
  component.push(current);
38825
39038
  for (let next = 0; next < shapes.length; next++) {
38826
39039
  if (visited.has(next)) continue;
38827
- if (boundsInteriorOverlap(bounds[current], bounds[next])) {
39040
+ if (boundsFaceOrInteriorOverlap(bounds[current], bounds[next])) {
38828
39041
  visited.add(next);
38829
39042
  queue.push(next);
38830
39043
  }
@@ -38869,10 +39082,10 @@ function lowerGenericBooleanPlan(plan) {
38869
39082
  let returned = null;
38870
39083
  try {
38871
39084
  if (plan.op === "union") {
38872
- if (!hasPairwiseInteriorBoundsOverlap(shapes)) {
39085
+ if (!hasPairwiseFaceOrInteriorBoundsOverlap(shapes)) {
38873
39086
  return null;
38874
39087
  }
38875
- const components = interiorOverlapComponents(shapes);
39088
+ const components = faceOrInteriorOverlapComponents(shapes);
38876
39089
  if (components.length > 1) {
38877
39090
  returned = lowerClusteredUnion(shapes, components);
38878
39091
  return returned;
@@ -39402,9 +39615,13 @@ function shapeHasClosedNativeTopology(shape) {
39402
39615
  function normalizeTruckShapeForBooleanInput(shape) {
39403
39616
  if (shapeHasClosedNativeTopology(shape)) return shape;
39404
39617
  if (!meshHasRawBoundaryEdges(shape.getMesh())) return shape;
39405
- const normalized = normalizeFacetedTruckShape(shape);
39406
- disposeShapeBackend(shape);
39407
- return normalized;
39618
+ try {
39619
+ const normalized = normalizeFacetedTruckShape(shape);
39620
+ disposeShapeBackend(shape);
39621
+ return normalized;
39622
+ } catch {
39623
+ return shape;
39624
+ }
39408
39625
  }
39409
39626
  function lowerSdfPlan(plan) {
39410
39627
  if (getUnsupportedSdfProgramReason(plan.tree) === void 0) {
@@ -39899,8 +40116,8 @@ function resamplePolyline(points, count) {
39899
40116
  for (let idx = 0; idx < count; idx++) {
39900
40117
  const target = idx / (count - 1) * total;
39901
40118
  while (segment < cumulative.length - 2 && cumulative[segment + 1] < target) segment++;
39902
- const span = cumulative[segment + 1] - cumulative[segment];
39903
- const t = span > 1e-12 ? (target - cumulative[segment]) / span : 0;
40119
+ const span2 = cumulative[segment + 1] - cumulative[segment];
40120
+ const t = span2 > 1e-12 ? (target - cumulative[segment]) / span2 : 0;
39904
40121
  const a2 = points[segment];
39905
40122
  const b = points[segment + 1];
39906
40123
  out.push([a2[0] + (b[0] - a2[0]) * t, a2[1] + (b[1] - a2[1]) * t, a2[2] + (b[2] - a2[2]) * t]);
@@ -40658,12 +40875,12 @@ function transformedSphereProfile(basePlan, steps, sliceOffset) {
40658
40875
  transform = transform.mul(transformForShapeTransform(step));
40659
40876
  scale2 *= stepScale;
40660
40877
  }
40661
- const center = transform.point([0, 0, 0]);
40878
+ const center2 = transform.point([0, 0, 0]);
40662
40879
  const radius = Math.abs(source.radius * scale2);
40663
- const sectionRadius = sliceOffset == null ? radius : Math.sqrt(Math.max(0, radius * radius - (sliceOffset - center[2]) * (sliceOffset - center[2])));
40880
+ const sectionRadius = sliceOffset == null ? radius : Math.sqrt(Math.max(0, radius * radius - (sliceOffset - center2[2]) * (sliceOffset - center2[2])));
40664
40881
  if (sectionRadius <= EXACT_PROFILE_EPS) return emptyProfilePlan();
40665
40882
  const profile = circleProfilePlan(sectionRadius, source.segments);
40666
- if (!isNearlyZero(center[0]) || !isNearlyZero(center[1])) profile.transforms.push({ kind: "translate", x: center[0], y: center[1] });
40883
+ if (!isNearlyZero(center2[0]) || !isNearlyZero(center2[1])) profile.transforms.push({ kind: "translate", x: center2[0], y: center2[1] });
40667
40884
  return profile;
40668
40885
  }
40669
40886
  function profileTransformsForShapeTransform(step) {
@@ -40766,20 +40983,20 @@ function sliceOffsetBeforeShapeTransforms(steps, offset) {
40766
40983
  }
40767
40984
  return sourceOffset;
40768
40985
  }
40769
- function transformZSpan(span, step) {
40986
+ function transformZSpan(span2, step) {
40770
40987
  switch (step.kind) {
40771
40988
  case "translate":
40772
- return [span[0] + step.z, span[1] + step.z];
40989
+ return [span2[0] + step.z, span2[1] + step.z];
40773
40990
  case "scale":
40774
40991
  if (!Number.isFinite(step.z) || isNearlyZero(step.z)) return null;
40775
- return [Math.min(span[0] * step.z, span[1] * step.z), Math.max(span[0] * step.z, span[1] * step.z)];
40992
+ return [Math.min(span2[0] * step.z, span2[1] * step.z), Math.max(span2[0] * step.z, span2[1] * step.z)];
40776
40993
  case "rotateAround": {
40777
40994
  const axisLength = Math.hypot(step.axisX, step.axisY, step.axisZ);
40778
40995
  if (axisLength <= EXACT_PROFILE_EPS) return null;
40779
40996
  const axisX = step.axisX / axisLength;
40780
40997
  const axisY = step.axisY / axisLength;
40781
40998
  const axisZ = step.axisZ / axisLength;
40782
- return !isNearlyZero(axisX) || !isNearlyZero(axisY) || Math.abs(Math.abs(axisZ) - 1) > EXACT_PROFILE_EPS ? null : span;
40999
+ return !isNearlyZero(axisX) || !isNearlyZero(axisY) || Math.abs(Math.abs(axisZ) - 1) > EXACT_PROFILE_EPS ? null : span2;
40783
41000
  }
40784
41001
  case "mirror": {
40785
41002
  const normalLength = Math.hypot(step.normalX, step.normalY, step.normalZ);
@@ -40787,14 +41004,14 @@ function transformZSpan(span, step) {
40787
41004
  const normalX = step.normalX / normalLength;
40788
41005
  const normalY = step.normalY / normalLength;
40789
41006
  const normalZ = step.normalZ / normalLength;
40790
- if (isNearlyZero(normalX) && isNearlyZero(normalY)) return [-span[1], -span[0]];
40791
- return isNearlyZero(normalZ) ? span : null;
41007
+ if (isNearlyZero(normalX) && isNearlyZero(normalY)) return [-span2[1], -span2[0]];
41008
+ return isNearlyZero(normalZ) ? span2 : null;
40792
41009
  }
40793
41010
  case "workplanePlacement": {
40794
41011
  const replay = workplanePlacementReplay(step.matrix);
40795
41012
  if (!replay) return null;
40796
- const z0 = replay.zScale * span[0] + replay.zTranslate;
40797
- const z1 = replay.zScale * span[1] + replay.zTranslate;
41013
+ const z0 = replay.zScale * span2[0] + replay.zTranslate;
41014
+ const z1 = replay.zScale * span2[1] + replay.zTranslate;
40798
41015
  return [Math.min(z0, z1), Math.max(z0, z1)];
40799
41016
  }
40800
41017
  default:
@@ -40848,14 +41065,14 @@ function transformProfilePointThroughRadial(point2, transforms) {
40848
41065
  return out;
40849
41066
  }
40850
41067
  function profileRadialTransformBasis(transforms) {
40851
- const center = transformProfilePointThroughRadial([0, 0], transforms);
41068
+ const center2 = transformProfilePointThroughRadial([0, 0], transforms);
40852
41069
  const xBasis = transformProfilePointThroughRadial([1, 0], transforms);
40853
41070
  const yBasis = transformProfilePointThroughRadial([0, 1], transforms);
40854
- if (!center || !xBasis || !yBasis) return null;
41071
+ if (!center2 || !xBasis || !yBasis) return null;
40855
41072
  return {
40856
- center,
40857
- xAxis: [xBasis[0] - center[0], xBasis[1] - center[1]],
40858
- yAxis: [yBasis[0] - center[0], yBasis[1] - center[1]]
41073
+ center: center2,
41074
+ xAxis: [xBasis[0] - center2[0], xBasis[1] - center2[1]],
41075
+ yAxis: [yBasis[0] - center2[0], yBasis[1] - center2[1]]
40859
41076
  };
40860
41077
  }
40861
41078
  function circleRadialProjectionInterval(plan) {
@@ -41003,13 +41220,13 @@ function radialRoundedRectBoundaryMaxDistanceFromPoint(footprint, point2) {
41003
41220
  [-1, -1]
41004
41221
  ];
41005
41222
  for (const [sx, sy] of cornerSigns) {
41006
- const center = [sx * coreHalfWidth, sy * coreHalfHeight];
41007
- includeLocal([center[0] + sx * footprint.radius, center[1]]);
41008
- includeLocal([center[0], center[1] + sy * footprint.radius]);
41009
- const away = [center[0] - localPoint[0], center[1] - localPoint[1]];
41223
+ const center2 = [sx * coreHalfWidth, sy * coreHalfHeight];
41224
+ includeLocal([center2[0] + sx * footprint.radius, center2[1]]);
41225
+ includeLocal([center2[0], center2[1] + sy * footprint.radius]);
41226
+ const away = [center2[0] - localPoint[0], center2[1] - localPoint[1]];
41010
41227
  const awayLength = Math.hypot(away[0], away[1]);
41011
41228
  if (awayLength > EXACT_PROFILE_EPS && away[0] * sx >= -EXACT_PROFILE_EPS && away[1] * sy >= -EXACT_PROFILE_EPS) {
41012
- includeLocal([center[0] + away[0] / awayLength * footprint.radius, center[1] + away[1] / awayLength * footprint.radius]);
41229
+ includeLocal([center2[0] + away[0] / awayLength * footprint.radius, center2[1] + away[1] / awayLength * footprint.radius]);
41013
41230
  }
41014
41231
  }
41015
41232
  return maxDistance;
@@ -41107,10 +41324,10 @@ function lineIntervalsForRect(base, tangent, halfWidth, halfHeight) {
41107
41324
  if (!applyAxis(base[0], tangent[0], halfWidth) || !applyAxis(base[1], tangent[1], halfHeight)) return [];
41108
41325
  return [[min2, max2]];
41109
41326
  }
41110
- function lineIntervalsForCircle(base, tangent, center, radius) {
41327
+ function lineIntervalsForCircle(base, tangent, center2, radius) {
41111
41328
  if (radius < -EXACT_PROFILE_EPS) return null;
41112
- const dx = base[0] - center[0];
41113
- const dy = base[1] - center[1];
41329
+ const dx = base[0] - center2[0];
41330
+ const dy = base[1] - center2[1];
41114
41331
  const projection = dx * tangent[0] + dy * tangent[1];
41115
41332
  const distanceSq = dx * dx + dy * dy - projection * projection;
41116
41333
  const radiusSq = radius * radius;
@@ -41153,7 +41370,7 @@ function roundedRectRadialSliceIntervals(plan, y2) {
41153
41370
  [-coreHalfWidth, coreHalfHeight],
41154
41371
  [-coreHalfWidth, -coreHalfHeight]
41155
41372
  ];
41156
- for (const center of centers) include(lineIntervalsForCircle(base, tangent, center, radius));
41373
+ for (const center2 of centers) include(lineIntervalsForCircle(base, tangent, center2, radius));
41157
41374
  }
41158
41375
  const xFunctional = [basis.xAxis[0], basis.yAxis[0]];
41159
41376
  const xAtBase = basis.center[0] + xFunctional[0] * base[0] + xFunctional[1] * base[1];
@@ -41288,7 +41505,7 @@ function circleProfilePlan(radius, segments) {
41288
41505
  }
41289
41506
  function annulusProfilePlan(outerRadius, innerRadius, segments) {
41290
41507
  const outer = Math.abs(outerRadius);
41291
- const inner = Math.abs(innerRadius);
41508
+ const inner = Math.max(0, innerRadius);
41292
41509
  if (outer <= EXACT_PROFILE_EPS) return emptyProfilePlan();
41293
41510
  if (inner <= EXACT_PROFILE_EPS) return circleProfilePlan(outer, segments);
41294
41511
  return {
@@ -42461,9 +42678,9 @@ function expandSimpleFullRevolutionProfileFootprint(plan, distance) {
42461
42678
  }
42462
42679
  }
42463
42680
  function profilePlanForSimpleFullRevolutionFootprint(footprint, segments) {
42464
- const translate = (profile, center) => {
42465
- if (!isNearlyZero(center[0]) || !isNearlyZero(center[1])) {
42466
- profile.transforms.push({ kind: "translate", x: center[0], y: center[1] });
42681
+ const translate = (profile, center2) => {
42682
+ if (!isNearlyZero(center2[0]) || !isNearlyZero(center2[1])) {
42683
+ profile.transforms.push({ kind: "translate", x: center2[0], y: center2[1] });
42467
42684
  }
42468
42685
  return profile;
42469
42686
  };
@@ -42481,7 +42698,7 @@ function profilePlanForSimpleFullRevolutionFootprint(footprint, segments) {
42481
42698
  footprint.footprint.center
42482
42699
  ) : null;
42483
42700
  case "roundedRect": {
42484
- const { halfWidth, halfHeight, radius, xAxis, yAxis, center } = footprint.footprint;
42701
+ const { halfWidth, halfHeight, radius, xAxis, yAxis, center: center2 } = footprint.footprint;
42485
42702
  if (halfWidth <= EXACT_PROFILE_EPS || halfHeight <= EXACT_PROFILE_EPS || radius < -EXACT_PROFILE_EPS) return null;
42486
42703
  const profile = roundedRectProfilePlan({
42487
42704
  width: halfWidth * 2,
@@ -42496,7 +42713,7 @@ function profilePlanForSimpleFullRevolutionFootprint(footprint, segments) {
42496
42713
  m11: yAxis[1]
42497
42714
  })
42498
42715
  );
42499
- return translate(profile, center);
42716
+ return translate(profile, center2);
42500
42717
  }
42501
42718
  default:
42502
42719
  return assertExhaustive(footprint);
@@ -43083,16 +43300,16 @@ function exactVerticalProjectionProfilePlan(plan) {
43083
43300
  const base = exactVerticalProjectionProfilePlan(plan.base);
43084
43301
  if (!base) return null;
43085
43302
  let profile = base.profile;
43086
- let span = [base.zMin, base.zMax];
43303
+ let span2 = [base.zMin, base.zMax];
43087
43304
  for (const step of plan.steps) {
43088
43305
  const projectedTransforms = profileTransformsForShapeTransform(step);
43089
43306
  if (!projectedTransforms) return null;
43090
- const nextSpan = transformZSpan(span, step);
43307
+ const nextSpan = transformZSpan(span2, step);
43091
43308
  if (!nextSpan) return null;
43092
43309
  profile = appendProfileTransforms(profile, projectedTransforms);
43093
- span = nextSpan;
43310
+ span2 = nextSpan;
43094
43311
  }
43095
- return { profile, zMin: span[0], zMax: span[1] };
43312
+ return { profile, zMin: span2[0], zMax: span2[1] };
43096
43313
  }
43097
43314
  default:
43098
43315
  return null;
@@ -43112,7 +43329,7 @@ function projectVerticalDifferenceProfilePlan(subject, clips) {
43112
43329
  const clipped = clips.map((clip) => ({ clip, span: clippedZSpan(subject, clip) })).filter((entry) => entry.span != null);
43113
43330
  if (clipped.length === 0) return cloneProfileCompilePlan(subject.profile);
43114
43331
  const zBreaks = [subject.zMin, subject.zMax];
43115
- for (const { span } of clipped) zBreaks.push(span[0], span[1]);
43332
+ for (const { span: span2 } of clipped) zBreaks.push(span2[0], span2[1]);
43116
43333
  zBreaks.sort((a2, b) => a2 - b);
43117
43334
  const uniqueBreaks = [];
43118
43335
  for (const z2 of zBreaks) {
@@ -43125,7 +43342,7 @@ function projectVerticalDifferenceProfilePlan(subject, clips) {
43125
43342
  const zMax = uniqueBreaks[idx + 1];
43126
43343
  if (zMax - zMin <= EXACT_PROFILE_EPS) continue;
43127
43344
  const zMid = (zMin + zMax) / 2;
43128
- const activeClips = clipped.filter(({ span }) => offsetInRange(zMid, span[0], span[1]));
43345
+ const activeClips = clipped.filter(({ span: span2 }) => offsetInRange(zMid, span2[0], span2[1]));
43129
43346
  if (activeClips.length === 0) return cloneProfileCompilePlan(subject.profile);
43130
43347
  slabProfiles.push({
43131
43348
  kind: "boolean",
@@ -44098,7 +44315,7 @@ function clusterMeshFaces(shape) {
44098
44315
  return clusters;
44099
44316
  }
44100
44317
  function clusterToFaceRef(cluster, name = "") {
44101
- const center = [
44318
+ const center2 = [
44102
44319
  cluster.centroidSum[0] / cluster.count,
44103
44320
  cluster.centroidSum[1] / cluster.count,
44104
44321
  cluster.centroidSum[2] / cluster.count
@@ -44107,7 +44324,7 @@ function clusterToFaceRef(cluster, name = "") {
44107
44324
  return {
44108
44325
  name,
44109
44326
  normal: cluster.normal,
44110
- center,
44327
+ center: center2,
44111
44328
  planar: true,
44112
44329
  uAxis: u2,
44113
44330
  vAxis: v
@@ -44279,7 +44496,7 @@ function representativeFaceForMembers(name, faces, query, semantic) {
44279
44496
  const cloned = faces.map((face) => cloneFaceRefValue(face));
44280
44497
  const first = cloned[0];
44281
44498
  const coplanar = facesAreCoplanar(cloned);
44282
- const center = cloned.reduce(
44499
+ const center2 = cloned.reduce(
44283
44500
  (acc, face) => [acc[0] + face.center[0], acc[1] + face.center[1], acc[2] + face.center[2]],
44284
44501
  [0, 0, 0]
44285
44502
  );
@@ -44287,7 +44504,7 @@ function representativeFaceForMembers(name, faces, query, semantic) {
44287
44504
  const representative = {
44288
44505
  ...first,
44289
44506
  name,
44290
- center: [center[0] / count, center[1] / count, center[2] / count],
44507
+ center: [center2[0] / count, center2[1] / count, center2[2] / count],
44291
44508
  query: cloneFaceQueryRef(query),
44292
44509
  planar: coplanar ? first.planar : false,
44293
44510
  uAxis: coplanar ? first.uAxis : void 0,
@@ -45407,10 +45624,10 @@ function resolveShapeFaceTableInternal(plan, owner) {
45407
45624
  if (!planeCapQuery) return table;
45408
45625
  const baseTable = resolveShapeFaceTable(plan.base);
45409
45626
  const centers = Array.from(baseTable.faces.values()).map((face) => face.center);
45410
- const averageCenter = centers.length > 0 ? centers.reduce((acc, center2) => [acc[0] + center2[0], acc[1] + center2[1], acc[2] + center2[2]], [0, 0, 0]).map((value) => value / centers.length) : [0, 0, plan.originOffset];
45627
+ const averageCenter = centers.length > 0 ? centers.reduce((acc, center22) => [acc[0] + center22[0], acc[1] + center22[1], acc[2] + center22[2]], [0, 0, 0]).map((value) => value / centers.length) : [0, 0, plan.originOffset];
45411
45628
  const normal2 = normalizeAxis([plan.normalX, plan.normalY, plan.normalZ]);
45412
45629
  const planeDistance = averageCenter[0] * normal2[0] + averageCenter[1] * normal2[1] + averageCenter[2] * normal2[2] - plan.originOffset;
45413
- const center = [
45630
+ const center2 = [
45414
45631
  averageCenter[0] - normal2[0] * planeDistance,
45415
45632
  averageCenter[1] - normal2[1] * planeDistance,
45416
45633
  averageCenter[2] - normal2[2] * planeDistance
@@ -45419,7 +45636,7 @@ function resolveShapeFaceTableInternal(plan, owner) {
45419
45636
  registerFace(table, {
45420
45637
  name: "plane-cap",
45421
45638
  normal: normal2,
45422
- center,
45639
+ center: center2,
45423
45640
  planar: true,
45424
45641
  uAxis: basis.u,
45425
45642
  vAxis: basis.v,
@@ -48319,6 +48536,99 @@ function computeSeatOverTranslation(targetFaceVertices, targetFaceNormal, selfMe
48319
48536
  sampleCount: targetFaceVertices.length
48320
48537
  };
48321
48538
  }
48539
+ const UV_ANCHORS = ["top", "bottom", "front", "back", "left", "right"];
48540
+ new Set(UV_ANCHORS);
48541
+ const FACE_FRAME = {
48542
+ front: { u: 0, v: 2 },
48543
+ // -y face: u→x, v→z
48544
+ back: { u: 0, v: 2 },
48545
+ // +y face
48546
+ left: { u: 1, v: 2 },
48547
+ // -x face: u→y, v→z
48548
+ right: { u: 1, v: 2 },
48549
+ // +x face
48550
+ top: { u: 0, v: 1 },
48551
+ // +z face: u→x, v→y
48552
+ bottom: { u: 0, v: 1 }
48553
+ // -z face
48554
+ };
48555
+ const AXIS_INDEX = { x: 0, y: 1, z: 2 };
48556
+ function span(bbox, axis) {
48557
+ return bbox.max[axis] - bbox.min[axis];
48558
+ }
48559
+ function center(bbox) {
48560
+ return [
48561
+ (bbox.min[0] + bbox.max[0]) / 2,
48562
+ (bbox.min[1] + bbox.max[1]) / 2,
48563
+ (bbox.min[2] + bbox.max[2]) / 2
48564
+ ];
48565
+ }
48566
+ function autoFitProjection(spec, bbox) {
48567
+ switch (spec.kind) {
48568
+ case "flat": {
48569
+ const frame = FACE_FRAME[spec.onto];
48570
+ const out = { ...spec };
48571
+ if (out.width === void 0) {
48572
+ const w2 = span(bbox, frame.u);
48573
+ if (w2 > 0) {
48574
+ out.width = w2;
48575
+ if (out.offsetU === void 0) out.offsetU = bbox.min[frame.u];
48576
+ }
48577
+ }
48578
+ if (out.height === void 0) {
48579
+ const h = span(bbox, frame.v);
48580
+ if (h > 0) {
48581
+ out.height = h;
48582
+ if (out.offsetV === void 0) out.offsetV = bbox.min[frame.v];
48583
+ }
48584
+ }
48585
+ return out;
48586
+ }
48587
+ case "box": {
48588
+ const out = { ...spec };
48589
+ if (out.size === void 0) {
48590
+ const sx = span(bbox, 0);
48591
+ const sy = span(bbox, 1);
48592
+ const sz = span(bbox, 2);
48593
+ if (sx > 0 && sy > 0 && sz > 0) out.size = [sx, sy, sz];
48594
+ }
48595
+ if (out.origin === void 0) out.origin = center(bbox);
48596
+ return out;
48597
+ }
48598
+ case "cylinder": {
48599
+ const axis = AXIS_INDEX[spec.axis];
48600
+ const out = { ...spec };
48601
+ if (out.height === void 0) {
48602
+ const h = span(bbox, axis);
48603
+ if (h > 0) {
48604
+ out.height = h;
48605
+ if (out.offsetV === void 0) out.offsetV = bbox.min[axis];
48606
+ }
48607
+ }
48608
+ if (out.origin === void 0) out.origin = center(bbox);
48609
+ return out;
48610
+ }
48611
+ case "sphere": {
48612
+ const out = { ...spec };
48613
+ if (out.origin === void 0) out.origin = center(bbox);
48614
+ if (out.radius === void 0) {
48615
+ const c2 = center(bbox);
48616
+ const radius = Math.max(
48617
+ bbox.max[0] - c2[0],
48618
+ bbox.max[1] - c2[1],
48619
+ bbox.max[2] - c2[2]
48620
+ );
48621
+ if (radius > 0) out.radius = radius;
48622
+ }
48623
+ return out;
48624
+ }
48625
+ default:
48626
+ throw new Error(`autoFitProjection: unknown projection kind "${spec.kind}"`);
48627
+ }
48628
+ }
48629
+ function isImageHandle(value) {
48630
+ return typeof value === "object" && value !== null && value.__forgeImage === true;
48631
+ }
48322
48632
  const _shapeDimensions = /* @__PURE__ */ new WeakMap();
48323
48633
  let _shapeDimensionCounter = 0;
48324
48634
  function nextShapeDimensionId() {
@@ -48691,8 +49001,8 @@ function mergeShapeSourceSpans(sources, target) {
48691
49001
  records = /* @__PURE__ */ new Map();
48692
49002
  _shapeSourceSpans.set(target, records);
48693
49003
  }
48694
- for (const [key2, span] of sourceRecords) {
48695
- if (!records.has(key2)) records.set(key2, span);
49004
+ for (const [key2, span2] of sourceRecords) {
49005
+ if (!records.has(key2)) records.set(key2, span2);
48696
49006
  }
48697
49007
  }
48698
49008
  }
@@ -49098,7 +49408,7 @@ function annotateTruckPrimitiveAnalyticFaces(plan, topology) {
49098
49408
  const radiusTop = (source.radiusTop ?? source.radius) * distanceScale;
49099
49409
  const origin = transform.point(signedHeight >= 0 ? [0, 0, 0] : [0, 0, signedHeight]);
49100
49410
  const sidePointRadius = (Math.abs(radiusBottom) + Math.abs(radiusTop)) / 2;
49101
- const center = transform.point([sidePointRadius / distanceScale, 0, signedHeight / 2]);
49411
+ const center2 = transform.point([sidePointRadius / distanceScale, 0, signedHeight / 2]);
49102
49412
  const normal2 = transformedUnitAxis(transform, [1, 0, 0]) ?? [1, 0, 0];
49103
49413
  const surface = sameScalar(radiusBottom, radiusTop) ? { kind: "cylinder", origin, axis, radius: radiusBottom, height } : { kind: "cone", origin, axis, radiusBottom, radiusTop, height };
49104
49414
  for (const [name, face] of faces) {
@@ -49109,7 +49419,7 @@ function annotateTruckPrimitiveAnalyticFaces(plan, topology) {
49109
49419
  upsertAnalyticFace(faces, {
49110
49420
  name: "side",
49111
49421
  normal: normal2,
49112
- center,
49422
+ center: center2,
49113
49423
  planar: false,
49114
49424
  surface
49115
49425
  });
@@ -49119,8 +49429,8 @@ function annotateTruckPrimitiveAnalyticFaces(plan, topology) {
49119
49429
  if (distanceScale === null) return;
49120
49430
  const normal2 = transformedUnitAxis(transform, [1, 0, 0]) ?? [1, 0, 0];
49121
49431
  const radius = source.radius * distanceScale;
49122
- const center = transform.point([0, 0, 0]);
49123
- const surface = { kind: "sphere", center, radius };
49432
+ const center2 = transform.point([0, 0, 0]);
49433
+ const surface = { kind: "sphere", center: center2, radius };
49124
49434
  for (const [name, face] of faces) {
49125
49435
  if (name === "surface" || /^north-\d+$/.test(name) || /^band-\d+-\d+$/.test(name) || /^south-\d+$/.test(name)) {
49126
49436
  faces.set(name, { ...face, surface: cloneFaceSurface(surface) ?? surface });
@@ -49140,10 +49450,10 @@ function annotateTruckPrimitiveAnalyticFaces(plan, topology) {
49140
49450
  const axis = transformedUnitAxis(transform, [0, 0, 1]);
49141
49451
  const normal2 = transformedUnitAxis(transform, [1, 0, 0]) ?? [1, 0, 0];
49142
49452
  if (!axis) return;
49143
- const center = transform.point([0, 0, 0]);
49453
+ const center2 = transform.point([0, 0, 0]);
49144
49454
  const surface = {
49145
49455
  kind: "torus",
49146
- center,
49456
+ center: center2,
49147
49457
  axis,
49148
49458
  majorRadius: source.majorRadius * distanceScale,
49149
49459
  minorRadius: source.minorRadius * distanceScale
@@ -49318,7 +49628,7 @@ function addCompileAliasesToTopology(plan, topology) {
49318
49628
  ["top-rim", [radiusTop, 0, source.height], [0, radiusTop, source.height], [0, 0, source.height], radiusTop, [0, 0, 1]],
49319
49629
  ["bottom-rim", [source.radius, 0, 0], [0, source.radius, 0], [0, 0, 0], source.radius, [0, 0, -1]]
49320
49630
  ];
49321
- for (const [name, start, end, center, radius, axis] of semanticEdges2) {
49631
+ for (const [name, start, end, center2, radius, axis] of semanticEdges2) {
49322
49632
  if (topology.edges.has(name)) continue;
49323
49633
  const faceName = name === "top-rim" ? "top" : "bottom";
49324
49634
  topology.edges.set(name, {
@@ -49333,7 +49643,7 @@ function addCompileAliasesToTopology(plan, topology) {
49333
49643
  },
49334
49644
  curve: {
49335
49645
  kind: "circle",
49336
- center,
49646
+ center: center2,
49337
49647
  axis,
49338
49648
  radius,
49339
49649
  faceName
@@ -49527,6 +49837,7 @@ function withBaseDimensions(base, out) {
49527
49837
  if (baseTopo) _shapeTopology.set(out, cloneTopology(baseTopo));
49528
49838
  const baseLabels = cloneFaceLabelMap(_shapeFaceLabels.get(base));
49529
49839
  if (baseLabels) _shapeFaceLabels.set(out, baseLabels);
49840
+ if (base.materialProps) out.materialProps = { ...base.materialProps };
49530
49841
  copyShapeReferenceMetadata(base, out);
49531
49842
  copyShapeSourceSpans(base, out);
49532
49843
  return setShapeCompilePlanInternal(out, getShapeCompilePlanInternal(base));
@@ -49615,6 +49926,30 @@ function createOwnedTopologyRewritePlan(plan, operation2, buildPropagation) {
49615
49926
  const owner = createShapeQueryOwner(operation2);
49616
49927
  return wrapShapeCompilePlanWithQueryOwner(attachTopologyRewritePropagation(plan, buildPropagation(owner)), owner);
49617
49928
  }
49929
+ const KNOWN_PROJECTION_KINDS = /* @__PURE__ */ new Set(["flat", "cylinder", "sphere", "box"]);
49930
+ function validateProjectionFinite(spec) {
49931
+ switch (spec.kind) {
49932
+ case "flat":
49933
+ if (spec.width !== void 0) requireFiniteNumber(spec.width, "wrapTexture() flat width");
49934
+ if (spec.height !== void 0) requireFiniteNumber(spec.height, "wrapTexture() flat height");
49935
+ if (spec.offsetU !== void 0) requireFiniteNumber(spec.offsetU, "wrapTexture() flat offsetU");
49936
+ if (spec.offsetV !== void 0) requireFiniteNumber(spec.offsetV, "wrapTexture() flat offsetV");
49937
+ break;
49938
+ case "box":
49939
+ if (spec.size !== void 0) requireFiniteVec3$1(spec.size, "wrapTexture() box size");
49940
+ if (spec.origin !== void 0) requireFiniteVec3$1(spec.origin, "wrapTexture() box origin");
49941
+ break;
49942
+ case "cylinder":
49943
+ if (spec.height !== void 0) requireFiniteNumber(spec.height, "wrapTexture() cylinder height");
49944
+ if (spec.offsetV !== void 0) requireFiniteNumber(spec.offsetV, "wrapTexture() cylinder offsetV");
49945
+ if (spec.origin !== void 0) requireFiniteVec3$1(spec.origin, "wrapTexture() cylinder origin");
49946
+ break;
49947
+ case "sphere":
49948
+ if (spec.origin !== void 0) requireFiniteVec3$1(spec.origin, "wrapTexture() sphere origin");
49949
+ if (spec.radius !== void 0) requireFiniteNumber(spec.radius, "wrapTexture() sphere radius");
49950
+ break;
49951
+ }
49952
+ }
49618
49953
  function parseReferencePath(path) {
49619
49954
  const trimmed = path.trim();
49620
49955
  if (!trimmed) throw new Error("Shape.ref() requires a non-empty path.");
@@ -49960,7 +50295,7 @@ class Shape {
49960
50295
  *
49961
50296
  * Use `.color()` to set the base diffuse color; `.material()` controls how that color behaves
49962
50297
  * under light (metalness, roughness, clearcoat) and can add emissive glow independent of
49963
- * lighting. Emissive glow pairs naturally with the `postProcessing.bloom` effect in `scene()`.
50298
+ * lighting.
49964
50299
  *
49965
50300
  * **Example**
49966
50301
  *
@@ -49992,6 +50327,61 @@ class Shape {
49992
50327
  out.materialProps = { ...this.materialProps ?? {}, ...props };
49993
50328
  return out;
49994
50329
  }
50330
+ /**
50331
+ * Wrap an imported bitmap image around this shape using a projection.
50332
+ *
50333
+ * **Details**
50334
+ *
50335
+ * The `image` comes from `Import.image('path.png')`; the `projection` is one of the `Wrap.*`
50336
+ * helpers — `Wrap.flat({ onto: 'top' })` lays it flat on a face, `Wrap.aroundCylinder({ axis: 'z' })`
50337
+ * wraps it like a can label, `Wrap.onSphere()` maps it like a globe, and `Wrap.box()` cube-maps
50338
+ * it onto the six sides.
50339
+ *
50340
+ * By default the image **auto-fits** the shape — one copy across the relevant extent, so no
50341
+ * `width`/`height`/`size` is needed (pass them only to override). The (u,v) is derived from each
50342
+ * vertex's final world position, so the image stays glued to the surface through transforms and
50343
+ * boolean cuts with no UV layout to maintain — apply `wrapTexture` *after* positioning the shape.
50344
+ * Returns a new Shape; the original is unchanged.
50345
+ *
50346
+ * **Example**
50347
+ *
50348
+ * ```js
50349
+ * const logo = Import.image('./logo.png');
50350
+ * box(80, 80, 10).wrapTexture(logo, Wrap.flat({ onto: 'top' })); // auto-fits the face
50351
+ *
50352
+ * const label = Import.image('./label.jpg');
50353
+ * cylinder(60, 20).wrapTexture(label, Wrap.aroundCylinder({ axis: 'z' })); // wraps the side
50354
+ * ```
50355
+ *
50356
+ * @param image - An imported bitmap from `Import.image(...)`
50357
+ * @param projection - A projection spec from `Wrap.flat` / `Wrap.aroundCylinder` / `Wrap.onSphere` / `Wrap.box`
50358
+ * @returns A new Shape with the projected texture recorded in its material properties
50359
+ * @category Materials
50360
+ */
50361
+ wrapTexture(image, projection) {
50362
+ if (!isImageHandle(image)) {
50363
+ throw new Error("wrapTexture() expects an image from Import.image(...) as its first argument");
50364
+ }
50365
+ if (!projection || typeof projection !== "object" || !KNOWN_PROJECTION_KINDS.has(projection.kind)) {
50366
+ throw new Error(
50367
+ `wrapTexture() expects a projection from Wrap.flat / Wrap.aroundCylinder / Wrap.onSphere / Wrap.box, got ${JSON.stringify(projection == null ? void 0 : projection.kind)}`
50368
+ );
50369
+ }
50370
+ const bbox = this.boundingBox();
50371
+ const filled = autoFitProjection(projection, bbox);
50372
+ validateProjectionFinite(filled);
50373
+ const out = this.clone();
50374
+ out.materialProps = {
50375
+ ...this.materialProps ?? {},
50376
+ texture: {
50377
+ image: image.dataUri,
50378
+ projection: filled,
50379
+ imageWidth: image.width,
50380
+ imageHeight: image.height
50381
+ }
50382
+ };
50383
+ return out;
50384
+ }
49995
50385
  /** Return a new Shape wrapper for explicit duplication in scripts. */
49996
50386
  clone() {
49997
50387
  const out = withCopiedDimensions(this, new Shape(getShapeRuntimeBackendInternal(this).clone(), this.colorHex));
@@ -50519,12 +50909,12 @@ class Shape {
50519
50909
  scale(v) {
50520
50910
  const scale2 = requireNonZeroFiniteScale3(v, "Shape.scale() scale");
50521
50911
  const bb = this.boundingBox();
50522
- const center = [
50912
+ const center2 = [
50523
50913
  (bb.min[0] + bb.max[0]) / 2,
50524
50914
  (bb.min[1] + bb.max[1]) / 2,
50525
50915
  (bb.min[2] + bb.max[2]) / 2
50526
50916
  ];
50527
- return this.scaleAround(center, scale2);
50917
+ return this.scaleAround(center2, scale2);
50528
50918
  }
50529
50919
  /** Scale the shape uniformly or per-axis from an explicit pivot point. */
50530
50920
  scaleAround(pivot, v) {
@@ -50558,12 +50948,12 @@ class Shape {
50558
50948
  /** Mirror across a plane through the shape's bounding box center, defined by its normal vector. */
50559
50949
  mirror(normal2) {
50560
50950
  const bb = this.boundingBox();
50561
- const center = [
50951
+ const center2 = [
50562
50952
  (bb.min[0] + bb.max[0]) / 2,
50563
50953
  (bb.min[1] + bb.max[1]) / 2,
50564
50954
  (bb.min[2] + bb.max[2]) / 2
50565
50955
  ];
50566
- return this.mirrorThrough(center, normal2);
50956
+ return this.mirrorThrough(center2, normal2);
50567
50957
  }
50568
50958
  /** Mirror across a plane through an explicit point, defined by its normal vector. */
50569
50959
  mirrorThrough(point2, normal2) {
@@ -51323,7 +51713,8 @@ class FrozenShapeBackend {
51323
51713
  edgePositions: this._data.geometryEdgePositions,
51324
51714
  triangleFaceIds: this._data.geometryTriangleFaceIds,
51325
51715
  faceIdNames: this._data.geometryFaceIdNames,
51326
- hasSmoothNormals: this._data.hasSmoothNormals ?? false
51716
+ hasSmoothNormals: this._data.hasSmoothNormals ?? false,
51717
+ uvs: this._data.geometryUvs
51327
51718
  };
51328
51719
  }
51329
51720
  getMeshReconstructionData() {
@@ -51493,9 +51884,18 @@ class FrozenShape extends Shape {
51493
51884
  const EDGE_THRESHOLD_DOT = Math.cos(Math.PI / 180);
51494
51885
  const SMOOTH_THRESHOLD_DOT = Math.cos(30 * Math.PI / 180);
51495
51886
  function computeGeometryArrays(mesh, options = {}) {
51496
- const { numProp, numTri: triCount, triVerts, vertProperties, vertNormals } = mesh;
51887
+ const { numProp, numTri: triCount, triVerts, vertProperties, vertNormals, cornerNormals } = mesh;
51888
+ if (numProp !== NUM_PROP_POSITION_ONLY && numProp !== NUM_PROP_WITH_NORMAL && numProp !== NUM_PROP_WITH_UV) {
51889
+ throw new Error(
51890
+ `computeGeometryArrays: illegal vertProperties numProp ${numProp}. Only ${NUM_PROP_POSITION_ONLY} (position), ${NUM_PROP_WITH_NORMAL} (position+normal), and ${NUM_PROP_WITH_UV} (position+normal+uv) are valid; ${numProp} (e.g. a truncated uv channel) is not.`
51891
+ );
51892
+ }
51893
+ const hasStoredNormals = numProp === NUM_PROP_WITH_NORMAL || numProp === NUM_PROP_WITH_UV;
51894
+ const hasStoredUvs = numProp === NUM_PROP_WITH_UV;
51895
+ const useCornerNormals = !!cornerNormals && cornerNormals.length === triCount * 9;
51497
51896
  const positions = new Float32Array(triCount * 9);
51498
51897
  const normals = new Float32Array(triCount * 9);
51898
+ const uvs = hasStoredUvs ? new Float32Array(triCount * 6) : void 0;
51499
51899
  const faceNx = new Float32Array(triCount);
51500
51900
  const faceNy = new Float32Array(triCount);
51501
51901
  const faceNz = new Float32Array(triCount);
@@ -51525,7 +51925,18 @@ function computeGeometryArrays(mesh, options = {}) {
51525
51925
  positions[o + 6] = cx;
51526
51926
  positions[o + 7] = cy;
51527
51927
  positions[o + 8] = cz;
51528
- if (vertNormals) {
51928
+ if (uvs) {
51929
+ const u2 = t * 6;
51930
+ uvs[u2] = vertProperties[i0 * numProp + UV_OFFSET];
51931
+ uvs[u2 + 1] = vertProperties[i0 * numProp + UV_OFFSET + 1];
51932
+ uvs[u2 + 2] = vertProperties[i1 * numProp + UV_OFFSET];
51933
+ uvs[u2 + 3] = vertProperties[i1 * numProp + UV_OFFSET + 1];
51934
+ uvs[u2 + 4] = vertProperties[i2 * numProp + UV_OFFSET];
51935
+ uvs[u2 + 5] = vertProperties[i2 * numProp + UV_OFFSET + 1];
51936
+ }
51937
+ if (useCornerNormals) {
51938
+ for (let k2 = 0; k2 < 9; k2++) normals[o + k2] = cornerNormals[o + k2];
51939
+ } else if (vertNormals) {
51529
51940
  normals[o] = vertNormals[i0 * 3];
51530
51941
  normals[o + 1] = vertNormals[i0 * 3 + 1];
51531
51942
  normals[o + 2] = vertNormals[i0 * 3 + 2];
@@ -51535,13 +51946,13 @@ function computeGeometryArrays(mesh, options = {}) {
51535
51946
  normals[o + 6] = vertNormals[i2 * 3];
51536
51947
  normals[o + 7] = vertNormals[i2 * 3 + 1];
51537
51948
  normals[o + 8] = vertNormals[i2 * 3 + 2];
51538
- } else if (numProp >= 6) {
51949
+ } else if (hasStoredNormals) {
51539
51950
  const corners = [i0, i1, i2];
51540
51951
  for (let v = 0; v < 3; v++) {
51541
51952
  const base = corners[v] * numProp;
51542
- const nx = vertProperties[base + 3];
51543
- const ny = vertProperties[base + 4];
51544
- const nz = vertProperties[base + 5];
51953
+ const nx = vertProperties[base + NORMAL_OFFSET];
51954
+ const ny = vertProperties[base + NORMAL_OFFSET + 1];
51955
+ const nz = vertProperties[base + NORMAL_OFFSET + 2];
51545
51956
  const oc = o + v * 3;
51546
51957
  if (nx * nx + ny * ny + nz * nz > 1e-12) {
51547
51958
  normals[oc] = nx;
@@ -51568,7 +51979,7 @@ function computeGeometryArrays(mesh, options = {}) {
51568
51979
  faceNy[t] = fny;
51569
51980
  faceNz[t] = fnz;
51570
51981
  }
51571
- if (!vertNormals && numProp < 6 && triCount > 0) {
51982
+ if (!useCornerNormals && !vertNormals && !hasStoredNormals && triCount > 0) {
51572
51983
  computeAutoSmoothNormals(
51573
51984
  triVerts,
51574
51985
  vertProperties,
@@ -51587,7 +51998,8 @@ function computeGeometryArrays(mesh, options = {}) {
51587
51998
  positions,
51588
51999
  normals,
51589
52000
  edgePositions,
51590
- hasSmoothNormals: triCount > 0
52001
+ hasSmoothNormals: triCount > 0,
52002
+ uvs
51591
52003
  };
51592
52004
  }
51593
52005
  function computeAutoSmoothNormals(triVerts, vertProperties, numProp, triCount, faceNx, faceNy, faceNz, normals, mergeFromVert, mergeToVert) {
@@ -51722,10 +52134,11 @@ function attachKernelFaceMetadata(geometry, triangleFaceIds, faceIdNames) {
51722
52134
  }
51723
52135
  function shapeToGeometry(shape) {
51724
52136
  if (shape instanceof FrozenShape) {
51725
- const { positions, normals, edgePositions, triangleFaceIds, faceIdNames, hasSmoothNormals } = shape.getPrecomputedGeometry();
52137
+ const { positions, normals, edgePositions, triangleFaceIds, faceIdNames, hasSmoothNormals, uvs } = shape.getPrecomputedGeometry();
51726
52138
  const solid = new BufferGeometry();
51727
52139
  solid.setAttribute("position", new BufferAttribute(positions, 3));
51728
52140
  solid.setAttribute("normal", new BufferAttribute(normals, 3));
52141
+ if (uvs) solid.setAttribute("uv", new BufferAttribute(uvs, 2));
51729
52142
  attachKernelFaceMetadata(solid, triangleFaceIds, faceIdNames);
51730
52143
  const edges = new BufferGeometry();
51731
52144
  edges.setAttribute("position", new BufferAttribute(edgePositions, 3));
@@ -51748,18 +52161,20 @@ function shapeToGeometryFallback(shape) {
51748
52161
  } catch {
51749
52162
  mesh = shape.getMesh();
51750
52163
  }
51751
- const { positions, normals, edgePositions, hasSmoothNormals } = computeGeometryArrays({
52164
+ const { positions, normals, edgePositions, hasSmoothNormals, uvs } = computeGeometryArrays({
51752
52165
  numProp: mesh.numProp,
51753
52166
  numTri: mesh.numTri,
51754
52167
  triVerts: mesh.triVerts,
51755
52168
  vertProperties: mesh.vertProperties,
51756
52169
  mergeFromVert: mesh.mergeFromVert,
51757
52170
  mergeToVert: mesh.mergeToVert,
51758
- vertNormals
52171
+ vertNormals,
52172
+ cornerNormals: mesh.cornerNormals && mesh.cornerNormals.length === mesh.numTri * 9 ? mesh.cornerNormals : void 0
51759
52173
  });
51760
52174
  const solid = new BufferGeometry();
51761
52175
  solid.setAttribute("position", new BufferAttribute(positions, 3));
51762
52176
  solid.setAttribute("normal", new BufferAttribute(normals, 3));
52177
+ if (uvs) solid.setAttribute("uv", new BufferAttribute(uvs, 2));
51763
52178
  attachKernelFaceMetadata(solid, mesh.faceID, mesh.faceIdNames);
51764
52179
  const edges = new BufferGeometry();
51765
52180
  edges.setAttribute("position", new BufferAttribute(edgePositions, 3));