forgecad 0.9.15 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/dist/assets/{AdminPage-CDyGUinA.js → AdminPage-DwYHz72L.js} +1 -1
  2. package/dist/assets/{BenchmarkPage-DfPMY_-d.js → BenchmarkPage-a9_f-1US.js} +1 -1
  3. package/dist/assets/{BlogPage-kF0fkdJT.js → BlogPage-DodHpvmf.js} +1 -1
  4. package/dist/assets/{DocsPage-B954L3YN.js → DocsPage-B5LePEuj.js} +8 -858
  5. package/dist/assets/{EditorApp-CuDLxKqL.css → EditorApp-BpjZgzk0.css} +148 -0
  6. package/dist/assets/EditorApp-QXsAISLR.js +16307 -0
  7. package/dist/assets/{EmbedViewer-C77B-TrF.js → EmbedViewer-DdEHGUMU.js} +2 -2
  8. package/dist/assets/{LandingPageProofDriven-Cr6fXMDj.js → LandingPageProofDriven-yhhOodbf.js} +2 -2
  9. package/dist/assets/{LegalPage-Dzklqmmg.js → LegalPage-5RbKRGYK.js} +1 -1
  10. package/dist/assets/{PricingPage-zWXkvlwl.js → PricingPage-E3Rma7aV.js} +1 -1
  11. package/dist/assets/{SettingsPage-Bz0of4KQ.js → SettingsPage-BJZcM97j.js} +1 -1
  12. package/dist/assets/{app-D3kDkggg.js → app-DSYrDg0V.js} +1846 -352
  13. package/dist/assets/cli/{render-DSY3mMQa.js → render-ZMHR9HkV.js} +161 -70
  14. package/dist/assets/{constructionHistoryWorker-gpDo-uH2.js → constructionHistoryWorker-AwMMWSxg.js} +1104 -349
  15. package/dist/assets/{evalWorker-CU0Ke6DP.js → evalWorker-DbNs7Dkp.js} +5155 -3772
  16. package/dist/assets/{inspectWorker-COyp8XXA.js → inspectWorker-CZsCFtQT.js} +1415 -439
  17. package/dist/assets/{targets-B9sGB5nB.js → jointPose-DO6mnXn_.js} +71 -3
  18. package/dist/assets/{manifold-DNkrUWpA.js → manifold-BGlQBBH9.js} +1 -1
  19. package/dist/assets/{manifold-BRI5prcH.js → manifold-BU-tJwQh.js} +1 -1
  20. package/dist/assets/{manifold-C-3h2M7p.js → manifold-fy2MV7K1.js} +2 -2
  21. package/dist/assets/{reportWorker-CdBz5bNg.js → reportWorker-DO6hcQbh.js} +8474 -4549
  22. package/dist/assets/{scalar-sampling-budget-wJF98aY9.js → scalar-sampling-budget-o90NSNmF.js} +5347 -3906
  23. package/dist/assets/{scanProxyWorker-B-9VbLIs.js → scanProxyWorker-2GtDLk-R.js} +19 -6
  24. package/dist/assets/{javascript-1kQXfVaz.js → typescript-DBQ6RN5l.js} +874 -22
  25. package/dist/cli/render.html +1 -1
  26. package/dist/docs/index.html +3 -3
  27. package/dist/docs-raw/AI/usage.md +3 -1
  28. package/dist/docs-raw/CLI.md +65 -239
  29. package/dist/docs-raw/README.md +6 -0
  30. package/dist/docs-raw/component-model.md +17 -150
  31. package/dist/docs-raw/generated/assembly.md +159 -520
  32. package/dist/docs-raw/generated/concepts.md +245 -3491
  33. package/dist/docs-raw/generated/core.md +277 -1251
  34. package/dist/docs-raw/generated/curves.md +387 -1608
  35. package/dist/docs-raw/generated/legacy.md +162 -0
  36. package/dist/docs-raw/generated/lib.md +238 -112
  37. package/dist/docs-raw/generated/output.md +51 -76
  38. package/dist/docs-raw/generated/runtime-names.md +30 -22
  39. package/dist/docs-raw/generated/sdf.md +68 -284
  40. package/dist/docs-raw/generated/sheet-metal.md +68 -335
  41. package/dist/docs-raw/generated/sketch.md +240 -1161
  42. package/dist/docs-raw/generated/viewport.md +75 -316
  43. package/dist/docs-raw/generated/wood.md +21 -49
  44. package/dist/docs-raw/guides/coordinate-system.md +4 -42
  45. package/dist/docs-raw/guides/inspection-bundles.md +44 -442
  46. package/dist/docs-raw/guides/joint-design.md +18 -79
  47. package/dist/docs-raw/guides/positioning.md +21 -143
  48. package/dist/docs-raw/guides/scene-presentation.md +89 -0
  49. package/dist/docs-raw/skills/forgecad-3d-reconstruction.md +25 -111
  50. package/dist/docs-raw/skills/forgecad-blockout-model.md +20 -117
  51. package/dist/docs-raw/skills/forgecad-component-model.md +23 -107
  52. package/dist/docs-raw/skills/forgecad-high-level-spec.md +47 -155
  53. package/dist/docs-raw/skills/forgecad-image-replicator.md +26 -143
  54. package/dist/docs-raw/skills/forgecad-lld.md +19 -113
  55. package/dist/docs-raw/skills/forgecad-make-a-model.md +113 -532
  56. package/dist/docs-raw/skills/forgecad-model-grader.md +38 -108
  57. package/dist/docs-raw/skills/forgecad-prepare-prompt.md +24 -211
  58. package/dist/docs-raw/skills/forgecad-project.md +13 -129
  59. package/dist/docs-raw/skills/forgecad-reconstruction-benchmark.md +42 -134
  60. package/dist/docs-raw/skills/forgecad-render-inspect.md +27 -174
  61. package/dist/docs-raw/skills/forgecad-visual-spec.md +32 -112
  62. package/dist/docs-raw/skills/forgecad.md +19 -18
  63. package/dist/docs-raw/skills/index.md +2 -0
  64. package/dist/docs-raw/welcome.md +4 -2
  65. package/dist/index.html +1 -1
  66. package/dist/llms.txt +1 -2
  67. package/dist/sitemap.xml +13 -13
  68. package/dist-cli/{check-compiler-SDX5QIXI.js → check-compiler-JTVBITCR.js} +1 -1
  69. package/dist-cli/{check-query-propagation-EAYEFT77.js → check-query-propagation-3FFLSMVN.js} +1 -1
  70. package/dist-cli/{chunk-N4O47JLF.js → chunk-OAN5T4XD.js} +5722 -4287
  71. package/dist-cli/forgecad.js +2195 -656
  72. package/dist-skill/CONTEXT.md +1778 -7912
  73. package/dist-skill/SKILL.md +15 -15
  74. package/dist-skill/docs/API/core/concepts.md +27 -157
  75. package/dist-skill/docs/CLI.md +65 -239
  76. package/dist-skill/docs/generated/assembly.md +160 -493
  77. package/dist-skill/docs/generated/core.md +277 -1251
  78. package/dist-skill/docs/generated/curves.md +387 -1609
  79. package/dist-skill/docs/generated/lib.md +238 -112
  80. package/dist-skill/docs/generated/output.md +51 -76
  81. package/dist-skill/docs/generated/runtime-names.md +16 -22
  82. package/dist-skill/docs/generated/sdf.md +68 -284
  83. package/dist-skill/docs/generated/sheet-metal.md +68 -335
  84. package/dist-skill/docs/generated/sketch.md +240 -1160
  85. package/dist-skill/docs/generated/viewport.md +75 -223
  86. package/dist-skill/docs/generated/wood.md +21 -49
  87. package/dist-skill/docs/guides/coordinate-system.md +4 -42
  88. package/dist-skill/docs/guides/inspection-bundles.md +44 -442
  89. package/dist-skill/docs/guides/joint-design.md +18 -79
  90. package/dist-skill/docs/guides/positioning.md +21 -143
  91. package/dist-skill/docs/guides/scene-presentation.md +89 -0
  92. package/dist-skill/docs/guides/surface-members.md +26 -0
  93. package/dist-skill/library/forgecad-3d-reconstruction/SKILL.md +23 -111
  94. package/dist-skill/library/forgecad-blockout-model/SKILL.md +18 -117
  95. package/dist-skill/library/forgecad-component-model/SKILL.md +21 -107
  96. package/dist-skill/library/forgecad-high-level-spec/SKILL.md +45 -155
  97. package/dist-skill/library/forgecad-image-replicator/SKILL.md +24 -143
  98. package/dist-skill/library/forgecad-lld/SKILL.md +17 -113
  99. package/dist-skill/library/forgecad-make-a-model/SKILL.md +111 -532
  100. package/dist-skill/library/forgecad-model-grader/SKILL.md +36 -108
  101. package/dist-skill/library/forgecad-prepare-prompt/SKILL.md +35 -224
  102. package/dist-skill/library/forgecad-prepare-prompt/references/default-profiles.md +43 -271
  103. package/dist-skill/library/forgecad-prepare-prompt/references/master-prompt.md +30 -99
  104. package/dist-skill/library/forgecad-project/SKILL.md +13 -131
  105. package/dist-skill/library/forgecad-reconstruction-benchmark/SKILL.md +29 -123
  106. package/dist-skill/library/forgecad-render-inspect/SKILL.md +25 -174
  107. package/dist-skill/library/forgecad-visual-spec/SKILL.md +30 -111
  108. package/dist-skill/website/skills/forgecad-3d-reconstruction.md +58 -0
  109. package/dist-skill/website/skills/forgecad-blockout-model.md +49 -0
  110. package/dist-skill/website/skills/forgecad-component-model.md +53 -0
  111. package/dist-skill/website/skills/forgecad-high-level-spec.md +101 -0
  112. package/dist-skill/website/skills/forgecad-image-replicator.md +63 -0
  113. package/dist-skill/website/skills/forgecad-lld.md +41 -0
  114. package/dist-skill/website/skills/forgecad-make-a-model.md +186 -0
  115. package/dist-skill/website/skills/forgecad-model-grader.md +82 -0
  116. package/dist-skill/website/skills/forgecad-prepare-prompt.md +63 -0
  117. package/dist-skill/website/skills/forgecad-project.md +26 -0
  118. package/dist-skill/website/skills/forgecad-reconstruction-benchmark.md +60 -0
  119. package/dist-skill/website/skills/forgecad-render-inspect.md +80 -0
  120. package/dist-skill/website/skills/forgecad-visual-spec.md +71 -0
  121. package/dist-skill/website/skills/forgecad.md +122 -0
  122. package/dist-skill/website/skills/index.md +26 -0
  123. package/examples/api/comparison-imported-sphere-candidate.forge.js +1 -1
  124. package/examples/api/conformal-product-ribbon.forge.js +1 -1
  125. package/examples/api/exact-sheet-shell-assembly.forge.js +1 -1
  126. package/examples/api/extrude-options.forge.js +4 -2
  127. package/examples/api/field-loft-drive-tip.forge.js +40 -0
  128. package/examples/api/guided-loft-olive-oil-bottle.forge.js +1 -1
  129. package/examples/api/helix-basics.forge.js +2 -2
  130. package/examples/api/highlight-debug.forge.js +10 -10
  131. package/examples/api/mesh-import-slats.forge.js +1 -1
  132. package/examples/api/real-product-curves.forge.js +1 -1
  133. package/examples/api/route3d-elbow.forge.js +3 -0
  134. package/examples/api/sculpt-box-circle-booleans.forge.js +1 -1
  135. package/examples/api/sdf-shapes.forge.js +2 -5
  136. package/examples/api/sketch-rounding-strategies.forge.js +6 -6
  137. package/examples/api/surface-member-bottle-cage.forge.js +3 -3
  138. package/examples/api/surface-member-conformal-product-ribbon.forge.js +3 -3
  139. package/examples/api/surface-member-razor-inlay.forge.js +1 -1
  140. package/examples/api/variable-sweep-test.forge.js +4 -2
  141. package/examples/mechanical/airplane-propeller.forge.js +74 -39
  142. package/examples/nurbs-surface.forge.js +1 -1
  143. package/examples/products/iphone.forge.js +1 -1
  144. package/package.json +4 -1
  145. package/dist/assets/EditorApp-Beb-IZ0y.js +0 -14014
  146. package/dist/docs-raw/guides/geometry-conventions.md +0 -52
  147. package/dist/docs-raw/guides/modeling-recipes.md +0 -78
  148. package/dist-skill/docs/guides/geometry-conventions.md +0 -52
  149. package/dist-skill/docs/guides/modeling-recipes.md +0 -78
  150. package/dist-skill/library/forgecad-visual-spec/references/prompt-template.md +0 -79
  151. package/examples/api/bolted-service-cover.forge.js +0 -17
  152. package/examples/api/cable-gland-anchor.forge.js +0 -14
  153. package/examples/api/captured-cartridge-guide.forge.js +0 -14
  154. package/examples/api/captured-linear-slide.forge.js +0 -13
  155. package/examples/api/clevis-pin-joint.forge.js +0 -13
  156. package/examples/api/datum-enclosure.forge.js +0 -16
  157. package/examples/api/hose-barb-port.forge.js +0 -14
  158. package/examples/api/knuckled-hinge-assembly.forge.js +0 -15
  159. package/examples/api/living-hinge-cover.forge.js +0 -14
  160. package/examples/api/pcb-terminal-block.forge.js +0 -22
  161. package/examples/api/pinned-lever-pivot-stack.forge.js +0 -14
  162. package/examples/api/retained-shaft-knob-stack.forge.js +0 -15
  163. package/examples/api/routed-tube-clip.forge.js +0 -15
  164. package/examples/api/seated-bearing-stack.forge.js +0 -30
  165. package/examples/api/snap-latch-cover.forge.js +0 -14
  166. package/examples/api/thumb-screw-clamp.forge.js +0 -15
@@ -7603,7 +7603,7 @@ function requireNonZeroFiniteScale3(value, label) {
7603
7603
  }
7604
7604
  return scale2;
7605
7605
  }
7606
- const EPS$4 = 1e-10;
7606
+ const EPS$3 = 1e-10;
7607
7607
  function subVec3(a2, b) {
7608
7608
  return [a2[0] - b[0], a2[1] - b[1], a2[2] - b[2]];
7609
7609
  }
@@ -7629,7 +7629,7 @@ function projectRadial(v, axis) {
7629
7629
  function signedAngleAroundAxis(from, to, axis) {
7630
7630
  const fromLen = lengthVec3$1(from);
7631
7631
  const toLen = lengthVec3$1(to);
7632
- if (fromLen < EPS$4 || toLen < EPS$4) return 0;
7632
+ if (fromLen < EPS$3 || toLen < EPS$3) return 0;
7633
7633
  const fn = scaleVec3(from, 1 / fromLen);
7634
7634
  const tn = scaleVec3(to, 1 / toLen);
7635
7635
  const sin2 = dotVec3$4(axis, crossVec3$2(fn, tn));
@@ -7650,19 +7650,19 @@ function solveRotateAroundAngle(axis, pivot, movingPoint, targetPoint, options =
7650
7650
  const targetDecomp = projectRadial(target, unitAxis);
7651
7651
  const movingRadialLen = lengthVec3$1(movingDecomp.radial);
7652
7652
  const targetRadialLen = lengthVec3$1(targetDecomp.radial);
7653
- if (movingRadialLen < EPS$4) {
7654
- if (mode === "line" && targetRadialLen >= EPS$4) {
7653
+ if (movingRadialLen < EPS$3) {
7654
+ if (mode === "line" && targetRadialLen >= EPS$3) {
7655
7655
  throw new Error("rotateAroundTo(...): moving point lies on the rotation axis, so line alignment is impossible");
7656
7656
  }
7657
7657
  return 0;
7658
7658
  }
7659
7659
  if (mode === "plane") {
7660
- if (targetRadialLen < EPS$4) {
7660
+ if (targetRadialLen < EPS$3) {
7661
7661
  throw new Error("rotateAroundTo(...): target point lies on the rotation axis, so the target plane is undefined");
7662
7662
  }
7663
7663
  return signedAngleAroundAxis(movingDecomp.radial, targetDecomp.radial, unitAxis);
7664
7664
  }
7665
- if (targetRadialLen < EPS$4) {
7665
+ if (targetRadialLen < EPS$3) {
7666
7666
  throw new Error("rotateAroundTo(...): target line lies on the rotation axis, but the moving point does not");
7667
7667
  }
7668
7668
  const axialTol = 1e-8 * Math.max(1, Math.abs(movingDecomp.axial), Math.abs(targetDecomp.axial));
@@ -7699,7 +7699,7 @@ function multiplyMat4(a2, b) {
7699
7699
  }
7700
7700
  function normalizeVec3$3(v) {
7701
7701
  const len = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
7702
- if (len < EPS$4) throw new Error("Axis must be non-zero");
7702
+ if (len < EPS$3) throw new Error("Axis must be non-zero");
7703
7703
  return [v[0] / len, v[1] / len, v[2] / len];
7704
7704
  }
7705
7705
  function transformPoint(m2, p2, w2) {
@@ -7729,7 +7729,7 @@ function invertMat4(m2) {
7729
7729
  const b10 = a21 * a33 - a23 * a31;
7730
7730
  const b11 = a22 * a33 - a23 * a32;
7731
7731
  const det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
7732
- if (Math.abs(det) < EPS$4) throw new Error("Transform matrix is not invertible");
7732
+ if (Math.abs(det) < EPS$3) throw new Error("Transform matrix is not invertible");
7733
7733
  const invDet = 1 / det;
7734
7734
  out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * invDet;
7735
7735
  out[1] = (a02 * b10 - a01 * b11 - a03 * b09) * invDet;
@@ -7762,6 +7762,32 @@ class Transform {
7762
7762
  static from(input) {
7763
7763
  return input instanceof Transform ? input : new Transform(requireFiniteMat4(input, "Transform.from() matrix"));
7764
7764
  }
7765
+ /**
7766
+ * Compose transforms in chain order: `Transform.compose(a, b, c)` applies
7767
+ * `a`, then `b`, then `c` — the same left-to-right order as
7768
+ * `Transform.from(a).mul(b).mul(c)`.
7769
+ *
7770
+ * Prefer this over manual `.mul()` chains when composing 3+ transforms
7771
+ * (e.g. kinematics: `local -> childBase -> jointMotion -> jointFrame ->
7772
+ * parentWorld`); the variadic form makes the application order explicit and
7773
+ * prevents order mistakes.
7774
+ *
7775
+ * **Example**
7776
+ *
7777
+ * ```ts
7778
+ * const world = Transform.compose(childBase, jointMotion, jointFrame, parentWorld);
7779
+ * ```
7780
+ *
7781
+ * @param steps Transforms (or raw 4x4 matrices) applied left to right.
7782
+ * @returns The composed transform. With no arguments, the identity.
7783
+ */
7784
+ static compose(...steps) {
7785
+ let acc = Transform.identity();
7786
+ for (const step of steps) {
7787
+ acc = acc.mul(step);
7788
+ }
7789
+ return acc;
7790
+ }
7765
7791
  /** Create a translation transform. */
7766
7792
  static translation(x2, y2, z2) {
7767
7793
  return new Transform([
@@ -7845,6 +7871,7 @@ class Transform {
7845
7871
  return this.rotateAxis([0, 0, 1], angleDeg, pivot);
7846
7872
  }
7847
7873
  /** Scale after the current transform. */
7874
+ // biome-ignore lint/suspicious/useAdjacentOverloadSignatures: Static Transform.scale() and chainable instance scale() intentionally share the CAD API name.
7848
7875
  scale(v) {
7849
7876
  return this.mul(Transform.scale(v));
7850
7877
  }
@@ -9020,6 +9047,8 @@ function cloneSdfNode(node) {
9020
9047
  return { kind: "sdf:circularArray", child: cloneSdfNode(node.child), count: node.count, offset: node.offset };
9021
9048
  case "sdf:shell":
9022
9049
  return { kind: "sdf:shell", child: cloneSdfNode(node.child), thickness: node.thickness };
9050
+ case "sdf:offset":
9051
+ return { kind: "sdf:offset", child: cloneSdfNode(node.child), distance: node.distance };
9023
9052
  case "sdf:displace":
9024
9053
  return {
9025
9054
  kind: "sdf:displace",
@@ -9104,7 +9133,7 @@ function cloneSdfNode(node) {
9104
9133
  }
9105
9134
  }
9106
9135
  const SHEET_METAL_EDGES = ["top", "right", "bottom", "left"];
9107
- const EPS$3 = 1e-9;
9136
+ const EPS$2 = 1e-9;
9108
9137
  function isFinitePositive$1(value) {
9109
9138
  return Number.isFinite(value) && value > 0;
9110
9139
  }
@@ -9145,7 +9174,7 @@ function edgeDisplayName(edge) {
9145
9174
  return `sheetMetal().flange("${edge}", ...)`;
9146
9175
  }
9147
9176
  function normalizeAngle(angleDeg) {
9148
- return Math.abs(angleDeg) <= EPS$3 ? 0 : angleDeg;
9177
+ return Math.abs(angleDeg) <= EPS$2 ? 0 : angleDeg;
9149
9178
  }
9150
9179
  function validateSheetMetalModel(model) {
9151
9180
  if (!isFinitePositive$1(model.panel.width) || !isFinitePositive$1(model.panel.height)) {
@@ -9157,7 +9186,7 @@ function validateSheetMetalModel(model) {
9157
9186
  if (!isFiniteNonNegative(model.bendRadius)) {
9158
9187
  return "sheetMetal() requires a finite non-negative bendRadius.";
9159
9188
  }
9160
- if (model.bendRadius <= EPS$3) {
9189
+ if (model.bendRadius <= EPS$2) {
9161
9190
  return "sheetMetal() v1 requires a positive bendRadius so the bend region stays explicit instead of collapsing into a sharp fold.";
9162
9191
  }
9163
9192
  if (model.bendAllowance.kind !== "k-factor") {
@@ -9219,7 +9248,7 @@ function deriveSheetMetalModel(model) {
9219
9248
  const trimEnd = flanges.has(adjacent.end) ? model.cornerRelief.size : 0;
9220
9249
  const fullLength = edge === "top" || edge === "bottom" ? model.panel.width : model.panel.height;
9221
9250
  const span = fullLength - trimStart - trimEnd;
9222
- if (!(span > EPS$3)) {
9251
+ if (!(span > EPS$2)) {
9223
9252
  throw new Error(
9224
9253
  `${edgeDisplayName(edge)} loses all usable span after applying the defended rectangular corner relief size ${model.cornerRelief.size}.`
9225
9254
  );
@@ -9265,7 +9294,7 @@ function transformPlacement(origin, u2, v, normal) {
9265
9294
  };
9266
9295
  }
9267
9296
  function translatePlan(plan, x2, y2, z2) {
9268
- if (Math.abs(x2) <= EPS$3 && Math.abs(y2) <= EPS$3 && Math.abs(z2) <= EPS$3) return cloneShapeCompilePlan(plan);
9297
+ if (Math.abs(x2) <= EPS$2 && Math.abs(y2) <= EPS$2 && Math.abs(z2) <= EPS$2) return cloneShapeCompilePlan(plan);
9269
9298
  return appendShapeCompileTransform(cloneShapeCompilePlan(plan), {
9270
9299
  kind: "translate",
9271
9300
  x: x2,
@@ -10437,6 +10466,8 @@ function cloneShapeCompilePlan(plan) {
10437
10466
  heights: plan.heights.map((height) => height),
10438
10467
  edgeLength: plan.edgeLength,
10439
10468
  boundsPadding: plan.boundsPadding,
10469
+ ...plan.forceField ? { forceField: true } : {},
10470
+ ...plan.meshing ? { meshing: cloneSdfCompileMeshingSettings(plan.meshing) } : {},
10440
10471
  edgeLabels: plan.edgeLabels ? { ...plan.edgeLabels } : void 0,
10441
10472
  capLabels: plan.capLabels ? { ...plan.capLabels } : void 0
10442
10473
  };
@@ -10704,7 +10735,6 @@ function cloneShapeCompilePlan(plan) {
10704
10735
  default:
10705
10736
  assertExhaustive(plan);
10706
10737
  }
10707
- if (plan._occtCache) result._occtCache = plan._occtCache;
10708
10738
  return result;
10709
10739
  }
10710
10740
  function appendProfileCompileTransform(plan, step) {
@@ -10717,22 +10747,31 @@ function appendShapeCompileTransform(plan, step) {
10717
10747
  if (plan.kind === "transform") {
10718
10748
  return {
10719
10749
  kind: "transform",
10720
- base: cloneShapeCompilePlan(plan.base),
10721
- steps: [...plan.steps.map(cloneShapeTransform), cloneShapeTransform(step)]
10750
+ base: plan.base,
10751
+ steps: [...plan.steps, cloneShapeTransform(step)]
10722
10752
  };
10723
10753
  }
10724
10754
  return {
10725
10755
  kind: "transform",
10726
- base: cloneShapeCompilePlan(plan),
10756
+ base: plan,
10727
10757
  steps: [cloneShapeTransform(step)]
10728
10758
  };
10729
10759
  }
10730
10760
  function appendShapeCompileTransforms(plan, steps) {
10731
- let out = cloneShapeCompilePlan(plan);
10732
- for (const step of steps) {
10733
- out = appendShapeCompileTransform(out, step);
10761
+ if (!plan) return null;
10762
+ if (steps.length === 0) return plan;
10763
+ if (plan.kind === "transform") {
10764
+ return {
10765
+ kind: "transform",
10766
+ base: plan.base,
10767
+ steps: [...plan.steps, ...steps.map(cloneShapeTransform)]
10768
+ };
10734
10769
  }
10735
- return out;
10770
+ return {
10771
+ kind: "transform",
10772
+ base: plan,
10773
+ steps: steps.map(cloneShapeTransform)
10774
+ };
10736
10775
  }
10737
10776
  function wrapShapeCompilePlanWithQueryOwner(plan, owner) {
10738
10777
  if (!plan) return null;
@@ -10806,6 +10845,8 @@ function buildLoftShapeCompilePlan(profiles, heights, options) {
10806
10845
  heights: heights.map((height) => canonicalNumber(height)),
10807
10846
  edgeLength: canonicalNumber(options.edgeLength),
10808
10847
  boundsPadding: canonicalNumber(options.boundsPadding),
10848
+ ...options.forceField ? { forceField: true } : {},
10849
+ ...options.meshing ? { meshing: cloneSdfCompileMeshingSettings(options.meshing) } : {},
10809
10850
  edgeLabels: options.edgeLabels ? { ...options.edgeLabels } : void 0
10810
10851
  };
10811
10852
  }
@@ -10907,14 +10948,14 @@ function sub$2(a2, b) {
10907
10948
  function dot$3(a2, b) {
10908
10949
  return a2[0] * b[0] + a2[1] * b[1] + a2[2] * b[2];
10909
10950
  }
10910
- function cross$4(a2, b) {
10951
+ function cross$3(a2, b) {
10911
10952
  return [a2[1] * b[2] - a2[2] * b[1], a2[2] * b[0] - a2[0] * b[2], a2[0] * b[1] - a2[1] * b[0]];
10912
10953
  }
10913
10954
  function rotateAroundAxis(v, axis, angleRad) {
10914
10955
  const c2 = Math.cos(angleRad);
10915
10956
  const s = Math.sin(angleRad);
10916
10957
  const term1 = scale$2(v, c2);
10917
- const term2 = scale$2(cross$4(axis, v), s);
10958
+ const term2 = scale$2(cross$3(axis, v), s);
10918
10959
  const term3 = scale$2(axis, dot$3(axis, v) * (1 - c2));
10919
10960
  return add$2(add$2(term1, term2), term3);
10920
10961
  }
@@ -11190,13 +11231,13 @@ function sweepPathToPolylineAdaptive(path, baseSamples = 48) {
11190
11231
  pts.push(evalPathAt(path, 1));
11191
11232
  return pts;
11192
11233
  }
11193
- const EPS$2 = 1e-8;
11234
+ const EPS$1 = 1e-8;
11194
11235
  function midpoint$1(start, end) {
11195
11236
  return [(start[0] + end[0]) * 0.5, (start[1] + end[1]) * 0.5, (start[2] + end[2]) * 0.5];
11196
11237
  }
11197
11238
  function normalize$3(v) {
11198
11239
  const len = Math.hypot(v[0], v[1], v[2]);
11199
- if (len <= EPS$2) throw new Error("Edge feature selection requires a non-zero direction vector");
11240
+ if (len <= EPS$1) throw new Error("Edge feature selection requires a non-zero direction vector");
11200
11241
  return [v[0] / len, v[1] / len, v[2] / len];
11201
11242
  }
11202
11243
  function subtract(a2, b) {
@@ -11275,7 +11316,7 @@ function rigidTransformForEdgeStep(step) {
11275
11316
  case "mirror": {
11276
11317
  const [nx0, ny0, nz0] = [step.normalX, step.normalY, step.normalZ];
11277
11318
  const len = Math.hypot(nx0, ny0, nz0);
11278
- if (len <= EPS$2) return Transform.identity();
11319
+ if (len <= EPS$1) return Transform.identity();
11279
11320
  const nx = nx0 / len;
11280
11321
  const ny = ny0 / len;
11281
11322
  const nz = nz0 / len;
@@ -11572,7 +11613,7 @@ function isRectangleProfile(points) {
11572
11613
  return [next[0] - point[0], next[1] - point[1]];
11573
11614
  });
11574
11615
  const lengths = vectors.map(([x2, y2]) => Math.hypot(x2, y2));
11575
- if (lengths.some((length4) => length4 <= EPS$2)) return false;
11616
+ if (lengths.some((length4) => length4 <= EPS$1)) return false;
11576
11617
  const dot01 = vectors[0][0] * vectors[1][0] + vectors[0][1] * vectors[1][1];
11577
11618
  const dot12 = vectors[1][0] * vectors[2][0] + vectors[1][1] * vectors[2][1];
11578
11619
  const dot23 = vectors[2][0] * vectors[3][0] + vectors[2][1] * vectors[3][1];
@@ -13511,7 +13552,9 @@ function lowerLoftShellToConcretePlan(plan, thickness, openFaces) {
13511
13552
  profiles: innerProfiles,
13512
13553
  heights: innerHeights,
13513
13554
  edgeLength: plan.edgeLength,
13514
- boundsPadding: plan.boundsPadding
13555
+ boundsPadding: plan.boundsPadding,
13556
+ ...plan.forceField ? { forceField: true } : {},
13557
+ ...plan.meshing ? { meshing: { ...plan.meshing } } : {}
13515
13558
  };
13516
13559
  return { ok: true, plan: buildBooleanShapeCompilePlan("difference", [plan, inner]) };
13517
13560
  }
@@ -13649,6 +13692,197 @@ function lowerShellShapeCompilePlanToConcretePlan(plan) {
13649
13692
  }
13650
13693
  return lowerBaseShellPlanToConcretePlan(plan.base, plan.thickness, normalizeShellOpenFaces(plan.openFaces));
13651
13694
  }
13695
+ function cyrb53(str, seed) {
13696
+ let h1 = 3735928559 ^ seed;
13697
+ let h2 = 1103547991 ^ seed;
13698
+ for (let i = 0; i < str.length; i++) {
13699
+ const ch = str.charCodeAt(i);
13700
+ h1 = Math.imul(h1 ^ ch, 2654435761);
13701
+ h2 = Math.imul(h2 ^ ch, 1597334677);
13702
+ }
13703
+ h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507);
13704
+ h1 ^= Math.imul(h2 ^ h2 >>> 13, 3266489909);
13705
+ h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507);
13706
+ h2 ^= Math.imul(h1 ^ h1 >>> 13, 3266489909);
13707
+ return 4294967296 * (2097151 & h2) + (h1 >>> 0);
13708
+ }
13709
+ function hash128(str) {
13710
+ return cyrb53(str, 2654435769).toString(36) + cyrb53(str, 2246822507).toString(36);
13711
+ }
13712
+ function isPlainValue(value) {
13713
+ return value === null || typeof value === "number" || typeof value === "boolean" || typeof value === "string";
13714
+ }
13715
+ function createStructuralHasher(normalize2) {
13716
+ const memo = /* @__PURE__ */ new WeakMap();
13717
+ const skipKey = normalize2 == null ? void 0 : normalize2.skipKey;
13718
+ const unwrap = normalize2 == null ? void 0 : normalize2.unwrap;
13719
+ const entriesSource = normalize2 == null ? void 0 : normalize2.entriesSource;
13720
+ return function hashValue(root) {
13721
+ if (isPlainValue(root)) return JSON.stringify(root);
13722
+ if (root === void 0 || typeof root === "function" || typeof root === "symbol") return "null";
13723
+ const inProgress = /* @__PURE__ */ new Set();
13724
+ const stack = [];
13725
+ const attach = (frame, key, token) => {
13726
+ if (frame.isArray) frame.parts.push(token);
13727
+ else frame.parts.push(`${JSON.stringify(key)}:${token}`);
13728
+ };
13729
+ const open = (objIn, key) => {
13730
+ const aliases = [];
13731
+ let current = objIn;
13732
+ while (current !== null && typeof current === "object" && !Array.isArray(current) && unwrap) {
13733
+ const cached2 = memo.get(current);
13734
+ if (cached2 !== void 0) {
13735
+ for (const alias of aliases) memo.set(alias, cached2);
13736
+ return cached2;
13737
+ }
13738
+ const replaced = unwrap(current);
13739
+ if (replaced === void 0 || replaced === current) break;
13740
+ aliases.push(current);
13741
+ if (aliases.includes(replaced)) {
13742
+ throw new Error("planHash: cycle detected through unwrap chain");
13743
+ }
13744
+ current = replaced;
13745
+ }
13746
+ if (isPlainValue(current)) {
13747
+ const token = JSON.stringify(current);
13748
+ for (const alias of aliases) memo.set(alias, token);
13749
+ return token;
13750
+ }
13751
+ if (current === void 0 || typeof current === "function" || typeof current === "symbol") {
13752
+ for (const alias of aliases) memo.set(alias, "null");
13753
+ return "null";
13754
+ }
13755
+ const obj = current;
13756
+ const cached = memo.get(obj);
13757
+ if (cached !== void 0) {
13758
+ for (const alias of aliases) memo.set(alias, cached);
13759
+ return cached;
13760
+ }
13761
+ if (inProgress.has(obj)) {
13762
+ throw new Error(`planHash: cycle detected in plan data (kind=${String(obj.kind)})`);
13763
+ }
13764
+ inProgress.add(obj);
13765
+ aliases.push(obj);
13766
+ let entriesNode = obj;
13767
+ if (!Array.isArray(obj) && entriesSource) {
13768
+ const substitute = entriesSource(obj);
13769
+ if (substitute) entriesNode = substitute;
13770
+ }
13771
+ let pending;
13772
+ if (Array.isArray(entriesNode)) {
13773
+ pending = entriesNode.map((value) => ({ value })).reverse();
13774
+ } else {
13775
+ const entries = Object.entries(entriesNode).sort(([left], [right]) => left.localeCompare(right));
13776
+ pending = [];
13777
+ for (let i = entries.length - 1; i >= 0; i--) {
13778
+ const [entryKey, value] = entries[i];
13779
+ if (skipKey == null ? void 0 : skipKey(entryKey)) continue;
13780
+ pending.push({ value, key: entryKey });
13781
+ }
13782
+ }
13783
+ stack.push({ pending, parts: [], isArray: Array.isArray(entriesNode), aliases, key });
13784
+ return null;
13785
+ };
13786
+ const rootToken = open(root, void 0);
13787
+ if (rootToken !== null) return rootToken;
13788
+ let result = "null";
13789
+ while (stack.length > 0) {
13790
+ const frame = stack[stack.length - 1];
13791
+ const next = frame.pending.pop();
13792
+ if (next) {
13793
+ if (isPlainValue(next.value)) {
13794
+ attach(frame, next.key, JSON.stringify(next.value));
13795
+ continue;
13796
+ }
13797
+ if (next.value === void 0 || typeof next.value === "function" || typeof next.value === "symbol") {
13798
+ if (frame.isArray) frame.parts.push("null");
13799
+ continue;
13800
+ }
13801
+ const token2 = open(next.value, next.key);
13802
+ if (token2 !== null) attach(frame, next.key, token2);
13803
+ continue;
13804
+ }
13805
+ const canonical = frame.isArray ? `[${frame.parts.join(",")}]` : `{${frame.parts.join(",")}}`;
13806
+ const token = "#" + hash128(canonical);
13807
+ for (const alias of frame.aliases) {
13808
+ memo.set(alias, token);
13809
+ inProgress.delete(alias);
13810
+ }
13811
+ stack.pop();
13812
+ if (stack.length === 0) result = token;
13813
+ else attach(stack[stack.length - 1], frame.key, token);
13814
+ }
13815
+ return result;
13816
+ };
13817
+ }
13818
+ const PLAN_BOOKKEEPING_KEY = (key) => key.startsWith("_") || key === "owner" || key === "queryPropagation";
13819
+ const encodedLengthMemo = /* @__PURE__ */ new WeakMap();
13820
+ function estimatePlanEncodedLength(value) {
13821
+ if (value === void 0 || typeof value === "function" || typeof value === "symbol") return 4;
13822
+ if (value === null || typeof value === "boolean") return 5;
13823
+ if (typeof value === "number") return 8;
13824
+ if (typeof value === "string") return value.length + 2;
13825
+ const root = value;
13826
+ const known = encodedLengthMemo.get(root);
13827
+ if (known !== void 0) return known;
13828
+ const childValues = (node) => {
13829
+ if (Array.isArray(node)) return node;
13830
+ const values = [];
13831
+ for (const [key, item] of Object.entries(node)) {
13832
+ if (PLAN_BOOKKEEPING_KEY(key)) continue;
13833
+ values.push(item);
13834
+ }
13835
+ return values;
13836
+ };
13837
+ const order = [];
13838
+ const seen = /* @__PURE__ */ new Set();
13839
+ const discover = [root];
13840
+ while (discover.length > 0) {
13841
+ const node = discover.pop();
13842
+ if (seen.has(node) || encodedLengthMemo.has(node)) continue;
13843
+ seen.add(node);
13844
+ order.push(node);
13845
+ for (const child of childValues(node)) {
13846
+ if (child !== null && typeof child === "object") discover.push(child);
13847
+ }
13848
+ }
13849
+ for (let i = order.length - 1; i >= 0; i--) {
13850
+ const node = order[i];
13851
+ let total = 2;
13852
+ if (Array.isArray(node)) {
13853
+ for (const item of node) {
13854
+ total += 1 + (item !== null && typeof item === "object" ? encodedLengthMemo.get(item) : estimatePlanEncodedLength(item));
13855
+ }
13856
+ } else {
13857
+ for (const [key, item] of Object.entries(node)) {
13858
+ if (PLAN_BOOKKEEPING_KEY(key)) continue;
13859
+ total += key.length + 4 + (item !== null && typeof item === "object" ? encodedLengthMemo.get(item) : estimatePlanEncodedLength(item));
13860
+ }
13861
+ }
13862
+ encodedLengthMemo.set(node, total);
13863
+ }
13864
+ return encodedLengthMemo.get(root);
13865
+ }
13866
+ function deepFreezePlanData(value) {
13867
+ if (value === null || typeof value !== "object") return value;
13868
+ const stack = [value];
13869
+ while (stack.length > 0) {
13870
+ const node = stack.pop();
13871
+ if (Object.isFrozen(node)) continue;
13872
+ Object.freeze(node);
13873
+ if (Array.isArray(node)) {
13874
+ for (const item of node) {
13875
+ if (item !== null && typeof item === "object") stack.push(item);
13876
+ }
13877
+ continue;
13878
+ }
13879
+ for (const [key, item] of Object.entries(node)) {
13880
+ if (PLAN_BOOKKEEPING_KEY(key)) continue;
13881
+ if (item !== null && typeof item === "object") stack.push(item);
13882
+ }
13883
+ }
13884
+ return value;
13885
+ }
13652
13886
  const SHAPE_GEOMETRY_CACHE_KEY_VERSION = "shape-geometry-v1";
13653
13887
  const SHAPE_BUILD_CACHE_POLICY = "live-structural-lru-v1";
13654
13888
  const STRUCTURAL_CACHE_BUDGET_MB = 64;
@@ -13694,60 +13928,26 @@ function recordEvent(event) {
13694
13928
  runEvents.push(next);
13695
13929
  if (runEvents.length > MAX_RECORDED_EVENTS) runEvents = runEvents.slice(-MAX_RECORDED_EVENTS);
13696
13930
  }
13697
- function stableGeometryEncode(value, arrayMember) {
13698
- if (value === void 0 || typeof value === "function" || typeof value === "symbol") {
13699
- return arrayMember ? "null" : void 0;
13700
- }
13701
- if (value === null || typeof value === "number" || typeof value === "boolean" || typeof value === "string") {
13702
- return JSON.stringify(value);
13703
- }
13704
- if (Array.isArray(value)) {
13705
- return `[${value.map((item) => stableGeometryEncode(item, true) ?? "null").join(",")}]`;
13706
- }
13707
- const record = value;
13708
- if (record.kind === "queryOwner" && record.base) {
13709
- return stableGeometryEncode(record.base, arrayMember);
13710
- }
13711
- const entries = Object.entries(record).sort(([left], [right]) => left.localeCompare(right));
13712
- const encodedEntries = [];
13713
- for (const [key, item] of entries) {
13714
- if (key.startsWith("_") || key === "owner" || key === "queryPropagation") continue;
13715
- const encoded = stableGeometryEncode(item, false);
13716
- if (encoded !== void 0) encodedEntries.push(`${JSON.stringify(key)}:${encoded}`);
13717
- }
13718
- return `{${encodedEntries.join(",")}}`;
13719
- }
13720
- function stableCacheOpportunityEncode(value, arrayMember) {
13721
- if (value === void 0 || typeof value === "function" || typeof value === "symbol") {
13722
- return arrayMember ? "null" : void 0;
13723
- }
13724
- if (value === null || typeof value === "number" || typeof value === "boolean" || typeof value === "string") {
13725
- return JSON.stringify(value);
13726
- }
13727
- if (Array.isArray(value)) {
13728
- return `[${value.map((item) => stableCacheOpportunityEncode(item, true) ?? "null").join(",")}]`;
13729
- }
13730
- const record = value;
13731
- if (record.kind === "queryOwner" && record.base) {
13732
- return stableCacheOpportunityEncode(record.base, arrayMember);
13733
- }
13734
- let encodedRecord = record;
13735
- if (record.kind === "transform" && record.base) {
13736
- const retainedSteps = Array.isArray(record.steps) ? record.steps.filter((step) => step.kind === "scale") : [];
13737
- if (retainedSteps.length === 0) return stableCacheOpportunityEncode(record.base, arrayMember);
13738
- encodedRecord = { kind: "transform", base: record.base, steps: retainedSteps };
13739
- }
13740
- const entries = Object.entries(encodedRecord).sort(([left], [right]) => left.localeCompare(right));
13741
- const encodedEntries = [];
13742
- for (const [key, item] of entries) {
13743
- if (key.startsWith("_") || key === "owner" || key === "queryPropagation") continue;
13744
- const encoded = stableCacheOpportunityEncode(item, false);
13745
- if (encoded !== void 0) encodedEntries.push(`${JSON.stringify(key)}:${encoded}`);
13931
+ const skipBookkeepingKey = (key) => key.startsWith("_") || key === "owner" || key === "queryPropagation";
13932
+ const geometryPlanHasher = createStructuralHasher({
13933
+ skipKey: skipBookkeepingKey,
13934
+ unwrap: (record) => record.kind === "queryOwner" && record.base ? record.base : void 0
13935
+ });
13936
+ const scaleOnlySteps = (record) => Array.isArray(record.steps) ? record.steps.filter((step) => step.kind === "scale") : [];
13937
+ createStructuralHasher({
13938
+ skipKey: skipBookkeepingKey,
13939
+ unwrap: (record) => {
13940
+ if (record.kind === "queryOwner" && record.base) return record.base;
13941
+ if (record.kind === "transform" && record.base && scaleOnlySteps(record).length === 0) return record.base;
13942
+ return void 0;
13943
+ },
13944
+ entriesSource: (record) => {
13945
+ if (record.kind !== "transform" || !record.base) return void 0;
13946
+ return { kind: "transform", base: record.base, steps: scaleOnlySteps(record) };
13746
13947
  }
13747
- return `{${encodedEntries.join(",")}}`;
13748
- }
13948
+ });
13749
13949
  function shapeGeometryCacheKey(plan) {
13750
- return `${SHAPE_GEOMETRY_CACHE_KEY_VERSION}:${stableGeometryEncode(plan, false) ?? "null"}`;
13950
+ return `${SHAPE_GEOMETRY_CACHE_KEY_VERSION}:${geometryPlanHasher(plan)}`;
13751
13951
  }
13752
13952
  function planComplexityScore(value) {
13753
13953
  if (!value || typeof value !== "object") return 0;
@@ -13806,8 +14006,7 @@ function planComplexityScore(value) {
13806
14006
  }
13807
14007
  }
13808
14008
  function estimateCacheRetainedMb(plan) {
13809
- var _a3;
13810
- const encodedLength = ((_a3 = stableCacheOpportunityEncode(plan, false)) == null ? void 0 : _a3.length) ?? 0;
14009
+ const encodedLength = estimatePlanEncodedLength(plan);
13811
14010
  const serializedComplexityMb = encodedLength / 24e3;
13812
14011
  return round2(0.08 + planComplexityScore(plan) * 0.09 + serializedComplexityMb);
13813
14012
  }
@@ -13825,26 +14024,62 @@ function splitCacheablePlacement(plan) {
13825
14024
  }
13826
14025
  return { basePlan: plan, placementSteps: [] };
13827
14026
  }
14027
+ const uncacheableReasonMemo = /* @__PURE__ */ new WeakMap();
13828
14028
  function findUncacheableReason(value) {
13829
14029
  if (value === void 0 || value === null) return null;
13830
14030
  if (typeof value === "function" || typeof value === "symbol") return "plan contains runtime-only values";
13831
14031
  if (typeof value !== "object") return null;
13832
- if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) return "plan contains binary file data";
13833
- if (Array.isArray(value)) {
13834
- for (const item of value) {
13835
- const reason = findUncacheableReason(item);
13836
- if (reason) return reason;
14032
+ const root = value;
14033
+ const known = uncacheableReasonMemo.get(root);
14034
+ if (known !== void 0) return known;
14035
+ const order = [];
14036
+ const seen = /* @__PURE__ */ new Set();
14037
+ const discover = [root];
14038
+ let reason = null;
14039
+ while (discover.length > 0 && reason === null) {
14040
+ const node = discover.pop();
14041
+ if (seen.has(node)) continue;
14042
+ const cached = uncacheableReasonMemo.get(node);
14043
+ if (cached !== void 0) {
14044
+ if (cached !== null) reason = cached;
14045
+ continue;
14046
+ }
14047
+ seen.add(node);
14048
+ if (ArrayBuffer.isView(node) || node instanceof ArrayBuffer) {
14049
+ reason = "plan contains binary file data";
14050
+ break;
14051
+ }
14052
+ order.push(node);
14053
+ if (Array.isArray(node)) {
14054
+ for (const item of node) {
14055
+ if (typeof item === "function" || typeof item === "symbol") {
14056
+ reason = "plan contains runtime-only values";
14057
+ break;
14058
+ }
14059
+ if (item !== null && typeof item === "object") discover.push(item);
14060
+ }
14061
+ continue;
14062
+ }
14063
+ const record = node;
14064
+ if (record.kind === "importedMesh" || record.kind === "importedStep") {
14065
+ reason = "plan depends on imported file contents";
14066
+ break;
14067
+ }
14068
+ for (const [key, item] of Object.entries(record)) {
14069
+ if (skipBookkeepingKey(key)) continue;
14070
+ if (typeof item === "function" || typeof item === "symbol") {
14071
+ reason = "plan contains runtime-only values";
14072
+ break;
14073
+ }
14074
+ if (item !== null && typeof item === "object") discover.push(item);
13837
14075
  }
13838
- return null;
13839
14076
  }
13840
- const record = value;
13841
- if (record.kind === "importedMesh" || record.kind === "importedStep") return "plan depends on imported file contents";
13842
- for (const [key, item] of Object.entries(record)) {
13843
- if (key.startsWith("_")) continue;
13844
- const reason = findUncacheableReason(item);
13845
- if (reason) return reason;
14077
+ if (reason === null) {
14078
+ for (const node of order) uncacheableReasonMemo.set(node, null);
14079
+ } else {
14080
+ uncacheableReasonMemo.set(root, reason);
13846
14081
  }
13847
- return null;
14082
+ return reason;
13848
14083
  }
13849
14084
  function applyPlacementStep(backend, step) {
13850
14085
  switch (step.kind) {
@@ -14521,8 +14756,9 @@ function analyzeNodeUV(node, toLocal) {
14521
14756
  if (result.majorRadius !== void 0) result.majorRadius *= node.factor;
14522
14757
  return result;
14523
14758
  }
14524
- // ── Shell — UV comes from the inner shape ──
14759
+ // ── Shell / offset — UV comes from the inner shape ──
14525
14760
  case "sdf:shell":
14761
+ case "sdf:offset":
14526
14762
  return analyzeNodeUV(node.child, toLocal);
14527
14763
  // ── CSG — take UV from the first (primary) child ──
14528
14764
  case "sdf:union":
@@ -15058,6 +15294,11 @@ function compileSdfNode3(node) {
15058
15294
  const t = node.thickness * 0.5;
15059
15295
  return (x2, y2, z2) => abs(fn(x2, y2, z2)) - t;
15060
15296
  }
15297
+ case "sdf:offset": {
15298
+ const fn = compileSdfNode3(node.child);
15299
+ const d2 = node.distance;
15300
+ return (x2, y2, z2) => fn(x2, y2, z2) - d2;
15301
+ }
15061
15302
  case "sdf:displace": {
15062
15303
  const fn = compileSdfNode3(node.child);
15063
15304
  const constEntries = Object.entries(node.constants ?? {});
@@ -15551,6 +15792,10 @@ function emitSdfProgramNode(b, node, x2, y2, z2) {
15551
15792
  const child = emitSdfProgramNode(b, node.child, x2, y2, z2);
15552
15793
  return b.sub(b.abs(child), b.constant(node.thickness * 0.5));
15553
15794
  }
15795
+ case "sdf:offset": {
15796
+ const child = emitSdfProgramNode(b, node.child, x2, y2, z2);
15797
+ return b.sub(child, b.constant(node.distance));
15798
+ }
15554
15799
  case "sdf:onion": {
15555
15800
  let d2 = emitSdfProgramNode(b, node.child, x2, y2, z2);
15556
15801
  for (let i = 0; i < node.layers; i++) d2 = b.sub(b.abs(d2), b.constant(node.thickness));
@@ -15699,6 +15944,7 @@ function getUnsupportedSdfProgramReason(node) {
15699
15944
  case "sdf:bend":
15700
15945
  case "sdf:repeat":
15701
15946
  case "sdf:shell":
15947
+ case "sdf:offset":
15702
15948
  case "sdf:onion":
15703
15949
  return getUnsupportedSdfProgramReason(node.child);
15704
15950
  default:
@@ -16140,7 +16386,19 @@ function simplifyMesh(triVerts, vertProperties, targetRatio, maxError) {
16140
16386
  if (!_simplifier) {
16141
16387
  throw new Error("meshoptimizer not initialized — call initMeshoptimizer() first");
16142
16388
  }
16143
- const targetIndexCount = Math.max(3, Math.floor(triVerts.length * targetRatio));
16389
+ if (triVerts.length === 0 || triVerts.length % 3 !== 0) {
16390
+ throw new Error("Mesh simplification requires triangle indices in groups of 3");
16391
+ }
16392
+ if (!Number.isFinite(targetRatio) || targetRatio <= 0) {
16393
+ throw new Error("Mesh simplification targetRatio must be a positive finite number");
16394
+ }
16395
+ if (!Number.isFinite(maxError) || maxError < 0) {
16396
+ throw new Error("Mesh simplification maxError must be a non-negative finite number");
16397
+ }
16398
+ const inputTriangleCount = triVerts.length / 3;
16399
+ const targetTriangleCount = Math.max(1, Math.min(inputTriangleCount, Math.floor(inputTriangleCount * targetRatio)));
16400
+ if (targetTriangleCount >= inputTriangleCount) return triVerts;
16401
+ const targetIndexCount = targetTriangleCount * 3;
16144
16402
  const [simplified] = _simplifier.simplify(
16145
16403
  triVerts,
16146
16404
  vertProperties,
@@ -17117,117 +17375,10 @@ function buildSweepLevelSetInput(profilePolygons, pathInput, options) {
17117
17375
  edgeLength: options.edgeLength
17118
17376
  };
17119
17377
  }
17120
- const EPS$1 = 1e-9;
17121
- function resamplePolygon(poly, targetCount) {
17122
- if (poly.length < 2) return poly;
17123
- if (targetCount <= 0) return [];
17124
- const dists = [0];
17125
- for (let i = 0; i < poly.length; i++) {
17126
- const p1 = poly[i];
17127
- const p2 = poly[(i + 1) % poly.length];
17128
- const dx = p2[0] - p1[0];
17129
- const dy = p2[1] - p1[1];
17130
- const d2 = Math.sqrt(dx * dx + dy * dy);
17131
- dists.push(dists[dists.length - 1] + d2);
17132
- }
17133
- const totalDist = dists[dists.length - 1];
17134
- if (totalDist < 1e-12) {
17135
- return Array.from({ length: targetCount }, () => [poly[0][0], poly[0][1]]);
17136
- }
17137
- const out = [];
17138
- for (let i = 0; i < targetCount; i++) {
17139
- const targetDist = i / targetCount * totalDist;
17140
- let low = 0;
17141
- let high = dists.length - 1;
17142
- while (low < high) {
17143
- const mid = low + high >> 1;
17144
- if (dists[mid] <= targetDist) {
17145
- low = mid + 1;
17146
- } else {
17147
- high = mid;
17148
- }
17149
- }
17150
- const seg = low - 1;
17151
- const t = (targetDist - dists[seg]) / (dists[seg + 1] - dists[seg]);
17152
- const p1 = poly[seg % poly.length];
17153
- const p2 = poly[(seg + 1) % poly.length];
17154
- out.push([p1[0] + (p2[0] - p1[0]) * t, p1[1] + (p2[1] - p1[1]) * t]);
17155
- }
17156
- return out;
17157
- }
17158
- function resamplePolygonByAngle(poly, targetCount, center = polygonCentroid(poly)) {
17159
- if (poly.length < 3 || targetCount <= 0) return null;
17160
- if (!isConvexPolygon(poly)) return null;
17161
- const out = [];
17162
- for (let index2 = 0; index2 < targetCount; index2 += 1) {
17163
- const angle = index2 / targetCount * Math.PI * 2;
17164
- const point = rayPolygonIntersection(center, [Math.cos(angle), Math.sin(angle)], poly);
17165
- if (!point) return null;
17166
- out.push(point);
17167
- }
17168
- return out;
17169
- }
17170
- function rayPolygonIntersection(origin, direction, poly) {
17171
- let bestT = Infinity;
17172
- let best = null;
17173
- for (let index2 = 0; index2 < poly.length; index2 += 1) {
17174
- const a2 = poly[index2];
17175
- const b = poly[(index2 + 1) % poly.length];
17176
- const edge = [b[0] - a2[0], b[1] - a2[1]];
17177
- const denom = cross$3(direction, edge);
17178
- if (Math.abs(denom) < EPS$1) continue;
17179
- const delta = [a2[0] - origin[0], a2[1] - origin[1]];
17180
- const rayT = cross$3(delta, edge) / denom;
17181
- const edgeT = cross$3(delta, direction) / denom;
17182
- if (rayT >= -EPS$1 && edgeT >= -EPS$1 && edgeT <= 1 + EPS$1 && rayT < bestT) {
17183
- bestT = rayT;
17184
- best = [origin[0] + direction[0] * rayT, origin[1] + direction[1] * rayT];
17185
- }
17186
- }
17187
- return best;
17188
- }
17189
- function polygonCentroid(poly) {
17190
- let area2 = 0;
17191
- let cx = 0;
17192
- let cy = 0;
17193
- for (let index2 = 0; index2 < poly.length; index2 += 1) {
17194
- const a2 = poly[index2];
17195
- const b = poly[(index2 + 1) % poly.length];
17196
- const crossValue = cross$3(a2, b);
17197
- area2 += crossValue;
17198
- cx += (a2[0] + b[0]) * crossValue;
17199
- cy += (a2[1] + b[1]) * crossValue;
17200
- }
17201
- if (Math.abs(area2) < EPS$1) return averagePoint(poly);
17202
- return [cx / (3 * area2), cy / (3 * area2)];
17203
- }
17204
- function averagePoint(poly) {
17205
- let x2 = 0;
17206
- let y2 = 0;
17207
- for (const point of poly) {
17208
- x2 += point[0];
17209
- y2 += point[1];
17210
- }
17211
- return [x2 / poly.length, y2 / poly.length];
17212
- }
17213
- function isConvexPolygon(poly) {
17214
- let sign = 0;
17215
- for (let index2 = 0; index2 < poly.length; index2 += 1) {
17216
- const a2 = poly[index2];
17217
- const b = poly[(index2 + 1) % poly.length];
17218
- const c2 = poly[(index2 + 2) % poly.length];
17219
- const turn = cross$3([b[0] - a2[0], b[1] - a2[1]], [c2[0] - b[0], c2[1] - b[1]]);
17220
- if (Math.abs(turn) < EPS$1) continue;
17221
- const currentSign = Math.sign(turn);
17222
- if (sign !== 0 && currentSign !== sign) return false;
17223
- sign = currentSign;
17224
- }
17225
- return sign !== 0;
17226
- }
17227
- function cross$3(a2, b) {
17228
- return a2[0] * b[1] - a2[1] * b[0];
17229
- }
17230
- function loftStitched(profiles, heights, wasm) {
17378
+ const CORNER_TURN_DEG = 30;
17379
+ const SPAN_ANGLE_PER_RING_DEG = 3;
17380
+ const MAX_SPAN_SUBDIVISION = 24;
17381
+ function loftStitched(profiles, heights, wasm, options = {}) {
17231
17382
  if (profiles.length < 2) return null;
17232
17383
  const classified = profiles.map((loops) => classifyLoops(loops));
17233
17384
  const outerCount = classified[0].outers.length;
@@ -17242,7 +17393,7 @@ function loftStitched(profiles, heights, wasm) {
17242
17393
  const holeGroups = holeCount > 0 ? matchLoopsAcrossProfiles(classified.map((c2) => c2.holes)) : [];
17243
17394
  const outerSolids = [];
17244
17395
  for (const group of outerGroups) {
17245
- const solid = stitchSingleLoopLoft(group, heights, wasm);
17396
+ const solid = stitchSingleLoopLoft(group, heights, wasm, options);
17246
17397
  if (!solid) {
17247
17398
  for (const s of outerSolids) s.delete();
17248
17399
  return null;
@@ -17259,7 +17410,7 @@ function loftStitched(profiles, heights, wasm) {
17259
17410
  if (holeGroups.length > 0) {
17260
17411
  const holeSolids = [];
17261
17412
  for (const group of holeGroups) {
17262
- const solid = stitchSingleLoopLoft(group, heights, wasm);
17413
+ const solid = stitchSingleLoopLoft(group, heights, wasm, options);
17263
17414
  if (!solid) {
17264
17415
  result.delete();
17265
17416
  for (const s of holeSolids) s.delete();
@@ -17345,68 +17496,527 @@ function signedArea$3(loop) {
17345
17496
  }
17346
17497
  return area * 0.5;
17347
17498
  }
17348
- function stitchSingleLoopLoft(loops, heights, wasm) {
17499
+ function detectCorners(loop) {
17500
+ const corners = [];
17501
+ const n = loop.length;
17502
+ const threshold = CORNER_TURN_DEG * Math.PI / 180;
17503
+ for (let i = 0; i < n; i++) {
17504
+ const prev = loop[(i - 1 + n) % n];
17505
+ const curr = loop[i];
17506
+ const next = loop[(i + 1) % n];
17507
+ const ax = curr[0] - prev[0];
17508
+ const ay = curr[1] - prev[1];
17509
+ const bx = next[0] - curr[0];
17510
+ const by = next[1] - curr[1];
17511
+ const la = Math.hypot(ax, ay);
17512
+ const lb = Math.hypot(bx, by);
17513
+ if (la < 1e-12 || lb < 1e-12) continue;
17514
+ const dot2 = (ax * bx + ay * by) / (la * lb);
17515
+ const turn = Math.acos(Math.min(1, Math.max(-1, dot2)));
17516
+ if (turn > threshold) corners.push(i);
17517
+ }
17518
+ return corners;
17519
+ }
17520
+ function cumulativeArcLength(loop) {
17521
+ const dists = [0];
17522
+ for (let i = 0; i < loop.length; i++) {
17523
+ const p1 = loop[i];
17524
+ const p2 = loop[(i + 1) % loop.length];
17525
+ dists.push(dists[i] + Math.hypot(p2[0] - p1[0], p2[1] - p1[1]));
17526
+ }
17527
+ return { dists, total: dists[loop.length] };
17528
+ }
17529
+ function pointAtArcLength(loop, dists, total, s) {
17530
+ if (total < 1e-12) return [loop[0][0], loop[0][1]];
17531
+ let target = s % total;
17532
+ if (target < 0) target += total;
17533
+ let low = 0;
17534
+ let high = dists.length - 1;
17535
+ while (low < high) {
17536
+ const mid = low + high >> 1;
17537
+ if (dists[mid] <= target) low = mid + 1;
17538
+ else high = mid;
17539
+ }
17540
+ const seg = low - 1;
17541
+ const segLen = dists[seg + 1] - dists[seg];
17542
+ const t = segLen < 1e-12 ? 0 : (target - dists[seg]) / segLen;
17543
+ const p1 = loop[seg % loop.length];
17544
+ const p2 = loop[(seg + 1) % loop.length];
17545
+ return [p1[0] + (p2[0] - p1[0]) * t, p1[1] + (p2[1] - p1[1]) * t];
17546
+ }
17547
+ function refineParams(params, rangeEnd, maxGap) {
17548
+ if (!Number.isFinite(maxGap)) return params;
17549
+ const out = [];
17550
+ for (let i = 0; i < params.length; i++) {
17551
+ const a2 = params[i];
17552
+ const b = i + 1 < params.length ? params[i + 1] : rangeEnd;
17553
+ out.push(a2);
17554
+ const gap = b - a2;
17555
+ if (gap > maxGap) {
17556
+ const pieces = Math.min(64, Math.ceil(gap / maxGap));
17557
+ for (let k2 = 1; k2 < pieces; k2++) out.push(a2 + gap * k2 / pieces);
17558
+ }
17559
+ }
17560
+ return out;
17561
+ }
17562
+ function turnAngle(prev, curr, next) {
17563
+ const ax = curr[0] - prev[0];
17564
+ const ay = curr[1] - prev[1];
17565
+ const bx = next[0] - curr[0];
17566
+ const by = next[1] - curr[1];
17567
+ const la = Math.hypot(ax, ay);
17568
+ const lb = Math.hypot(bx, by);
17569
+ if (la < 1e-12 || lb < 1e-12) return 0;
17570
+ const dot2 = (ax * bx + ay * by) / (la * lb);
17571
+ return Math.acos(Math.min(1, Math.max(-1, dot2)));
17572
+ }
17573
+ function maxTurnPerColumn(rings) {
17574
+ const N = rings[0].length;
17575
+ const out = new Float64Array(N);
17576
+ for (const ring of rings) {
17577
+ for (let j = 0; j < N; j++) {
17578
+ const turn = turnAngle(ring[(j - 1 + N) % N], ring[j], ring[(j + 1) % N]);
17579
+ if (turn > out[j]) out[j] = turn;
17580
+ }
17581
+ }
17582
+ return out;
17583
+ }
17584
+ const CURVE_TURN_EPS = 0.5 * Math.PI / 180;
17585
+ function maxQuadDeviation(rings, heights, colA, colB) {
17586
+ let worst = 0;
17587
+ for (let i = 0; i < rings.length - 1; i++) {
17588
+ const a2 = [rings[i][colA][0], rings[i][colA][1], heights[i]];
17589
+ const b = [rings[i][colB][0], rings[i][colB][1], heights[i]];
17590
+ const c2 = [rings[i + 1][colB][0], rings[i + 1][colB][1], heights[i + 1]];
17591
+ const d2 = [rings[i + 1][colA][0], rings[i + 1][colA][1], heights[i + 1]];
17592
+ const n = cross3$5(sub3$1(b, a2), sub3$1(d2, a2));
17593
+ const len = Math.hypot(n[0], n[1], n[2]);
17594
+ if (len < 1e-12) continue;
17595
+ const deviation = Math.abs((n[0] * (c2[0] - a2[0]) + n[1] * (c2[1] - a2[1]) + n[2] * (c2[2] - a2[2])) / len);
17596
+ if (deviation > worst) worst = deviation;
17597
+ }
17598
+ return worst;
17599
+ }
17600
+ function buildCompatibleRings(loops, heights, edgeLength2) {
17601
+ const cornerSets = loops.map(detectCorners);
17602
+ const cornerCount = cornerSets[0].length;
17603
+ const cornersMatch = cornerCount > 0 && cornerSets.every((c2) => c2.length === cornerCount);
17604
+ if (cornersMatch) {
17605
+ const anchored = buildCornerAnchoredRings(loops, heights, cornerSets, edgeLength2);
17606
+ if (anchored) return anchored;
17607
+ }
17608
+ return buildSeamAlignedRings(loops, heights, edgeLength2);
17609
+ }
17610
+ function buildCornerAnchoredRings(loops, heights, cornerSets, edgeLength2) {
17611
+ const cornerCount = cornerSets[0].length;
17612
+ const arcs = loops.map((loop) => cumulativeArcLength(loop));
17613
+ const alignedCorners = [cornerSets[0]];
17614
+ for (let i = 1; i < loops.length; i++) {
17615
+ const prev = alignedCorners[i - 1];
17616
+ const prevLoop = loops[i - 1];
17617
+ const curr = cornerSets[i];
17618
+ const loop = loops[i];
17619
+ let best = curr;
17620
+ let bestCost = Infinity;
17621
+ for (let r = 0; r < cornerCount; r++) {
17622
+ const rotated = curr.map((_2, k2) => curr[(k2 + r) % cornerCount]);
17623
+ let cost = 0;
17624
+ for (let k2 = 0; k2 < cornerCount; k2++) {
17625
+ const a2 = prevLoop[prev[k2]];
17626
+ const b = loop[rotated[k2]];
17627
+ cost += (a2[0] - b[0]) ** 2 + (a2[1] - b[1]) ** 2;
17628
+ }
17629
+ if (cost < bestCost) {
17630
+ bestCost = cost;
17631
+ best = rotated;
17632
+ }
17633
+ }
17634
+ alignedCorners.push(best);
17635
+ }
17636
+ const segParams = [];
17637
+ const segLengths = [];
17638
+ for (let i = 0; i < loops.length; i++) {
17639
+ const loop = loops[i];
17640
+ const { dists, total } = arcs[i];
17641
+ if (total < 1e-9) return null;
17642
+ const corners = alignedCorners[i];
17643
+ const stationSegs = [];
17644
+ const stationLens = [];
17645
+ for (let s = 0; s < cornerCount; s++) {
17646
+ const from = corners[s];
17647
+ const to = corners[(s + 1) % cornerCount];
17648
+ const start = dists[from];
17649
+ let end = dists[to];
17650
+ if (to <= from) end += total;
17651
+ const len = end - start;
17652
+ if (len < 1e-9) return null;
17653
+ const interior = [];
17654
+ const n = loop.length;
17655
+ for (let v = (from + 1) % n; v !== to; v = (v + 1) % n) {
17656
+ let d2 = dists[v];
17657
+ if (d2 < start) d2 += total;
17658
+ const p2 = (d2 - start) / len;
17659
+ if (p2 > 1e-9 && p2 < 1 - 1e-9) interior.push(p2);
17660
+ }
17661
+ stationSegs.push(interior);
17662
+ stationLens.push(len);
17663
+ }
17664
+ segParams.push(stationSegs);
17665
+ segLengths.push(stationLens);
17666
+ }
17667
+ let masterParams = [];
17668
+ for (let s = 0; s < cornerCount; s++) {
17669
+ let bestStation = 0;
17670
+ for (let i = 1; i < loops.length; i++) {
17671
+ if (segParams[i][s].length > segParams[bestStation][s].length) bestStation = i;
17672
+ }
17673
+ masterParams.push([0, ...segParams[bestStation][s]]);
17674
+ }
17675
+ const sampleRings = (paramsBySegment) => {
17676
+ const rings2 = [];
17677
+ for (let i = 0; i < loops.length; i++) {
17678
+ const loop = loops[i];
17679
+ const { dists, total } = arcs[i];
17680
+ const corners = alignedCorners[i];
17681
+ const ring = [];
17682
+ for (let s = 0; s < cornerCount; s++) {
17683
+ const from = corners[s];
17684
+ const to = corners[(s + 1) % cornerCount];
17685
+ const start = dists[from];
17686
+ let end = dists[to];
17687
+ if (to <= from) end += total;
17688
+ const len = end - start;
17689
+ for (const p2 of paramsBySegment[s]) {
17690
+ if (p2 === 0) {
17691
+ ring.push([loop[from][0], loop[from][1]]);
17692
+ } else {
17693
+ ring.push(pointAtArcLength(loop, dists, total, start + p2 * len));
17694
+ }
17695
+ }
17696
+ }
17697
+ rings2.push(ring);
17698
+ }
17699
+ return rings2;
17700
+ };
17701
+ let rings = sampleRings(masterParams);
17702
+ if (edgeLength2 && edgeLength2 > 0) {
17703
+ const ringLength = rings[0].length;
17704
+ const turns = maxTurnPerColumn(rings);
17705
+ const segStart = [];
17706
+ {
17707
+ let col = 0;
17708
+ for (let s = 0; s < cornerCount; s++) {
17709
+ segStart.push(col);
17710
+ col += masterParams[s].length;
17711
+ }
17712
+ }
17713
+ let changed = false;
17714
+ const refined = [];
17715
+ for (let s = 0; s < cornerCount; s++) {
17716
+ const params = masterParams[s];
17717
+ let maxLen = 0;
17718
+ for (let i = 0; i < loops.length; i++) maxLen = Math.max(maxLen, segLengths[i][s]);
17719
+ const out = [];
17720
+ for (let k2 = 0; k2 < params.length; k2++) {
17721
+ const a2 = params[k2];
17722
+ const b = k2 + 1 < params.length ? params[k2 + 1] : 1;
17723
+ out.push(a2);
17724
+ const gapLen = (b - a2) * maxLen;
17725
+ if (gapLen <= edgeLength2) continue;
17726
+ const colA = segStart[s] + k2;
17727
+ const colB = k2 + 1 < params.length ? colA + 1 : segStart[(s + 1) % cornerCount] % ringLength;
17728
+ const curved = k2 > 0 && turns[colA] > CURVE_TURN_EPS || k2 + 1 < params.length && turns[colB] > CURVE_TURN_EPS;
17729
+ const twisted = !curved && maxQuadDeviation(rings, heights, colA, colB) > edgeLength2 * 0.05;
17730
+ if (!curved && !twisted) continue;
17731
+ const pieces = Math.min(64, Math.ceil(gapLen / edgeLength2));
17732
+ for (let p2 = 1; p2 < pieces; p2++) out.push(a2 + (b - a2) * p2 / pieces);
17733
+ changed = true;
17734
+ }
17735
+ refined.push(out);
17736
+ }
17737
+ if (changed) {
17738
+ masterParams = refined;
17739
+ rings = sampleRings(masterParams);
17740
+ }
17741
+ }
17742
+ const cornerColumns = [];
17743
+ {
17744
+ let col = 0;
17745
+ for (let s = 0; s < cornerCount; s++) {
17746
+ cornerColumns.push(col);
17747
+ col += masterParams[s].length;
17748
+ }
17749
+ }
17750
+ return { rings, cornerColumns };
17751
+ }
17752
+ function buildSeamAlignedRings(loops, _heights, edgeLength2) {
17753
+ const arcs = loops.map((loop) => cumulativeArcLength(loop));
17754
+ let bestStation = 0;
17755
+ for (let i = 1; i < loops.length; i++) {
17756
+ if (loops[i].length > loops[bestStation].length) bestStation = i;
17757
+ }
17758
+ const { dists: masterDists, total: masterTotal } = arcs[bestStation];
17759
+ if (masterTotal < 1e-9) return null;
17760
+ let params = loops[bestStation].map((_2, v) => masterDists[v] / masterTotal);
17761
+ if (params.length < 24) {
17762
+ params = refineParams(params, 1, 1 / 24);
17763
+ }
17764
+ const sampleRings = (paramList) => loops.map((loop, i) => {
17765
+ const { dists, total } = arcs[i];
17766
+ if (total < 1e-9) {
17767
+ return paramList.map(() => [loop[0][0], loop[0][1]]);
17768
+ }
17769
+ return paramList.map((p2) => pointAtArcLength(loop, dists, total, p2 * total));
17770
+ });
17771
+ let rings = sampleRings(params);
17772
+ if (edgeLength2 && edgeLength2 > 0) {
17773
+ let maxPerimeter = 0;
17774
+ for (const { total } of arcs) maxPerimeter = Math.max(maxPerimeter, total);
17775
+ const turns = maxTurnPerColumn(rings);
17776
+ const N0 = params.length;
17777
+ const out = [];
17778
+ let changed = false;
17779
+ for (let k2 = 0; k2 < N0; k2++) {
17780
+ const a2 = params[k2];
17781
+ const b = k2 + 1 < N0 ? params[k2 + 1] : 1;
17782
+ out.push(a2);
17783
+ const gapLen = (b - a2) * maxPerimeter;
17784
+ if (gapLen <= edgeLength2) continue;
17785
+ if (turns[k2] <= CURVE_TURN_EPS && turns[(k2 + 1) % N0] <= CURVE_TURN_EPS) continue;
17786
+ const pieces = Math.min(64, Math.ceil(gapLen / edgeLength2));
17787
+ for (let p2 = 1; p2 < pieces; p2++) out.push(a2 + (b - a2) * p2 / pieces);
17788
+ changed = true;
17789
+ }
17790
+ if (changed) {
17791
+ params = out;
17792
+ rings = sampleRings(params);
17793
+ }
17794
+ }
17795
+ const N = params.length;
17796
+ for (let i = 1; i < rings.length; i++) {
17797
+ const prev = rings[i - 1];
17798
+ const curr = rings[i];
17799
+ let bestShift = 0;
17800
+ let bestCost = Infinity;
17801
+ for (let shift = 0; shift < N; shift++) {
17802
+ let cost = 0;
17803
+ for (let j = 0; j < N; j++) {
17804
+ const a2 = prev[j];
17805
+ const b = curr[(j + shift) % N];
17806
+ cost += (a2[0] - b[0]) ** 2 + (a2[1] - b[1]) ** 2;
17807
+ if (cost >= bestCost) break;
17808
+ }
17809
+ if (cost < bestCost) {
17810
+ bestCost = cost;
17811
+ bestShift = shift;
17812
+ }
17813
+ }
17814
+ if (bestShift !== 0) {
17815
+ rings[i] = curr.map((_2, j) => curr[(j + bestShift) % N]);
17816
+ }
17817
+ }
17818
+ return { rings, cornerColumns: [] };
17819
+ }
17820
+ function buildSpanRows(rings, heights) {
17821
+ const R = rings.length;
17822
+ const N = rings[0].length;
17823
+ const stations = rings.map((ring, i) => ring.map(([x2, y2]) => [x2, y2, heights[i]]));
17824
+ const t = [0];
17825
+ for (let i = 0; i < R - 1; i++) {
17826
+ let sum2 = 0;
17827
+ for (let j = 0; j < N; j++) {
17828
+ sum2 += dist3(stations[i][j], stations[i + 1][j]);
17829
+ }
17830
+ const avg = Math.max(sum2 / N, 1e-9);
17831
+ t.push(t[i] + Math.sqrt(avg));
17832
+ }
17833
+ const tangents = [];
17834
+ for (let i = 0; i < R; i++) {
17835
+ const row = [];
17836
+ for (let j = 0; j < N; j++) {
17837
+ row.push(stationTangent(stations, t, i, j));
17838
+ }
17839
+ tangents.push(row);
17840
+ }
17841
+ const rows = [{ points: stations[0], tangents: tangents[0], isStation: true }];
17842
+ for (let i = 0; i < R - 1; i++) {
17843
+ const h = t[i + 1] - t[i];
17844
+ let maxAngle = 0;
17845
+ const stride = Math.max(1, Math.floor(N / 16));
17846
+ for (let j = 0; j < N; j += stride) {
17847
+ maxAngle = Math.max(maxAngle, angleBetween(tangents[i][j], tangents[i + 1][j]));
17848
+ }
17849
+ const k2 = Math.min(MAX_SPAN_SUBDIVISION, Math.max(1, Math.ceil(maxAngle / SPAN_ANGLE_PER_RING_DEG)));
17850
+ for (let s = 1; s < k2; s++) {
17851
+ const u2 = s / k2;
17852
+ const points = [];
17853
+ const rowTangents = [];
17854
+ for (let j = 0; j < N; j++) {
17855
+ const { point, tangent } = hermite(stations[i][j], tangents[i][j], stations[i + 1][j], tangents[i + 1][j], h, u2);
17856
+ points.push(point);
17857
+ rowTangents.push(tangent);
17858
+ }
17859
+ rows.push({ points, tangents: rowTangents, isStation: false });
17860
+ }
17861
+ rows.push({ points: stations[i + 1], tangents: tangents[i + 1], isStation: true });
17862
+ }
17863
+ return rows;
17864
+ }
17865
+ function stationTangent(stations, t, i, j) {
17866
+ const R = stations.length;
17867
+ if (i === 0) {
17868
+ return scale3$1(sub3$1(stations[1][j], stations[0][j]), 1 / (t[1] - t[0]));
17869
+ }
17870
+ if (i === R - 1) {
17871
+ return scale3$1(sub3$1(stations[R - 1][j], stations[R - 2][j]), 1 / (t[R - 1] - t[R - 2]));
17872
+ }
17873
+ const hPrev = t[i] - t[i - 1];
17874
+ const hNext = t[i + 1] - t[i];
17875
+ const dPrev = scale3$1(sub3$1(stations[i][j], stations[i - 1][j]), 1 / hPrev);
17876
+ const dNext = scale3$1(sub3$1(stations[i + 1][j], stations[i][j]), 1 / hNext);
17877
+ return scale3$1(add3$1(scale3$1(dPrev, hNext), scale3$1(dNext, hPrev)), 1 / (hPrev + hNext));
17878
+ }
17879
+ function hermite(p0, m0, p1, m1, h, u2) {
17880
+ const u22 = u2 * u2;
17881
+ const u3 = u22 * u2;
17882
+ const h00 = 2 * u3 - 3 * u22 + 1;
17883
+ const h10 = u3 - 2 * u22 + u2;
17884
+ const h01 = -2 * u3 + 3 * u22;
17885
+ const h11 = u3 - u22;
17886
+ const d00 = 6 * u22 - 6 * u2;
17887
+ const d10 = 3 * u22 - 4 * u2 + 1;
17888
+ const d01 = -6 * u22 + 6 * u2;
17889
+ const d11 = 3 * u22 - 2 * u2;
17890
+ const point = [
17891
+ h00 * p0[0] + h10 * h * m0[0] + h01 * p1[0] + h11 * h * m1[0],
17892
+ h00 * p0[1] + h10 * h * m0[1] + h01 * p1[1] + h11 * h * m1[1],
17893
+ h00 * p0[2] + h10 * h * m0[2] + h01 * p1[2] + h11 * h * m1[2]
17894
+ ];
17895
+ const tangent = [
17896
+ d00 * p0[0] / h + d10 * m0[0] + d01 * p1[0] / h + d11 * m1[0],
17897
+ d00 * p0[1] / h + d10 * m0[1] + d01 * p1[1] / h + d11 * m1[1],
17898
+ d00 * p0[2] / h + d10 * m0[2] + d01 * p1[2] / h + d11 * m1[2]
17899
+ ];
17900
+ return { point, tangent };
17901
+ }
17902
+ function stitchSingleLoopLoft(loops, heights, wasm, options) {
17349
17903
  const normalizedLoops = loops.map((loop) => {
17350
17904
  const area = signedArea$3(loop);
17351
17905
  return area < 0 ? [...loop].reverse() : loop;
17352
17906
  });
17353
- let maxPoints = 0;
17354
- for (const loop of normalizedLoops) {
17355
- maxPoints = Math.max(maxPoints, loop.length);
17356
- }
17357
- const N = Math.max(maxPoints, 24);
17358
- const angularSamples = normalizedLoops.map((loop) => resamplePolygonByAngle(loop, N));
17359
- const useAngularSamples = angularSamples.every((samples) => samples != null);
17360
- const resampled = normalizedLoops.map((loop, i) => {
17361
- const pts2d = useAngularSamples ? angularSamples[i] : resamplePolygon(loop, N);
17362
- const z2 = heights[i];
17363
- return pts2d.map(([x2, y2]) => [x2, y2, z2]);
17364
- });
17365
- const vertices = [];
17366
- const triangles = [];
17367
- for (const layer of resampled) {
17368
- for (const [x2, y2, z2] of layer) {
17369
- vertices.push(x2, y2, z2);
17907
+ const compatible = buildCompatibleRings(normalizedLoops, heights, options.edgeLength);
17908
+ if (!compatible) return null;
17909
+ const { rings, cornerColumns } = compatible;
17910
+ const N = rings[0].length;
17911
+ if (N < 3) return null;
17912
+ const rows = buildSpanRows(rings, heights);
17913
+ const R = rows.length;
17914
+ const cornerSet = new Set(cornerColumns);
17915
+ const vertProps = [];
17916
+ let vertCount = 0;
17917
+ const fwdIdx = [];
17918
+ const bwdIdx = [];
17919
+ const pushVert = (p2, n) => {
17920
+ vertProps.push(p2[0], p2[1], p2[2], n[0], n[1], n[2]);
17921
+ return vertCount++;
17922
+ };
17923
+ for (let r = 0; r < R; r++) {
17924
+ const { points, tangents } = rows[r];
17925
+ const fwd = new Array(N);
17926
+ const bwd = new Array(N);
17927
+ for (let j = 0; j < N; j++) {
17928
+ const prev = points[(j - 1 + N) % N];
17929
+ const curr = points[j];
17930
+ const next = points[(j + 1) % N];
17931
+ if (cornerSet.has(j)) {
17932
+ const nFwd = surfaceNormal(sub3$1(next, curr), tangents[j]);
17933
+ const nBwd = surfaceNormal(sub3$1(curr, prev), tangents[j]);
17934
+ fwd[j] = pushVert(curr, nFwd);
17935
+ bwd[j] = pushVert(curr, nBwd);
17936
+ } else {
17937
+ const idx = pushVert(curr, surfaceNormal(sub3$1(next, prev), tangents[j]));
17938
+ fwd[j] = idx;
17939
+ bwd[j] = idx;
17940
+ }
17370
17941
  }
17942
+ fwdIdx.push(fwd);
17943
+ bwdIdx.push(bwd);
17371
17944
  }
17372
- for (let i = 0; i < resampled.length - 1; i++) {
17373
- const baseIdx = i * N;
17374
- const nextIdx = (i + 1) * N;
17945
+ const triangles = [];
17946
+ for (let r = 0; r < R - 1; r++) {
17375
17947
  for (let j = 0; j < N; j++) {
17376
17948
  const j1 = (j + 1) % N;
17377
- const v0 = baseIdx + j;
17378
- const v1 = nextIdx + j;
17379
- const v2 = nextIdx + j1;
17380
- const v3 = baseIdx + j1;
17949
+ const v0 = fwdIdx[r][j];
17950
+ const v3 = bwdIdx[r][j1];
17951
+ const v2 = bwdIdx[r + 1][j1];
17952
+ const v1 = fwdIdx[r + 1][j];
17381
17953
  triangles.push(v0, v3, v2);
17382
17954
  triangles.push(v0, v2, v1);
17383
17955
  }
17384
17956
  }
17385
- const bottomResampled2D = resampled[0].map(([x2, y2]) => [x2, y2]);
17386
- const bottomTrisResampled = wasm.triangulate([bottomResampled2D]);
17387
- for (const tri of bottomTrisResampled) {
17957
+ const bottomRing = rows[0].points;
17958
+ const topRing = rows[R - 1].points;
17959
+ const bottom2D = bottomRing.map(([x2, y2]) => [x2, y2]);
17960
+ const bottomTris = wasm.triangulate([bottom2D]);
17961
+ const bottomBase = vertCount;
17962
+ for (const p2 of bottomRing) pushVert(p2, [0, 0, -1]);
17963
+ for (const tri of bottomTris) {
17388
17964
  const [v0, v1, v2] = Array.isArray(tri) ? tri : [tri[0], tri[1], tri[2]];
17389
- triangles.push(v0, v2, v1);
17965
+ triangles.push(bottomBase + v0, bottomBase + v2, bottomBase + v1);
17390
17966
  }
17391
- const topResampled2D = resampled[resampled.length - 1].map(([x2, y2]) => [x2, y2]);
17392
- const topTrisResampled = wasm.triangulate([topResampled2D]);
17393
- const topStartIdx = (resampled.length - 1) * N;
17394
- for (const tri of topTrisResampled) {
17967
+ const top2D = topRing.map(([x2, y2]) => [x2, y2]);
17968
+ const topTris = wasm.triangulate([top2D]);
17969
+ const topBase = vertCount;
17970
+ for (const p2 of topRing) pushVert(p2, [0, 0, 1]);
17971
+ for (const tri of topTris) {
17395
17972
  const [v0, v1, v2] = Array.isArray(tri) ? tri : [tri[0], tri[1], tri[2]];
17396
- triangles.push(topStartIdx + v0, topStartIdx + v1, topStartIdx + v2);
17973
+ triangles.push(topBase + v0, topBase + v1, topBase + v2);
17397
17974
  }
17398
17975
  const mesh = new wasm.Mesh({
17399
- numProp: 3,
17400
- vertProperties: new Float32Array(vertices),
17976
+ numProp: 6,
17977
+ vertProperties: new Float32Array(vertProps),
17401
17978
  triVerts: new Uint32Array(triangles)
17402
17979
  });
17403
17980
  try {
17981
+ mesh.merge();
17404
17982
  const manifold = new wasm.Manifold(mesh);
17405
17983
  return manifold;
17406
17984
  } catch (_e2) {
17407
17985
  return null;
17408
17986
  }
17409
17987
  }
17988
+ function surfaceNormal(chord, span) {
17989
+ const n = cross3$5(chord, span);
17990
+ const len = Math.hypot(n[0], n[1], n[2]);
17991
+ if (len < 1e-12) {
17992
+ const radial = Math.hypot(chord[0], chord[1]);
17993
+ if (radial > 1e-12) return [chord[1] / radial, -chord[0] / radial, 0];
17994
+ return [0, 0, 1];
17995
+ }
17996
+ return [n[0] / len, n[1] / len, n[2] / len];
17997
+ }
17998
+ function sub3$1(a2, b) {
17999
+ return [a2[0] - b[0], a2[1] - b[1], a2[2] - b[2]];
18000
+ }
18001
+ function add3$1(a2, b) {
18002
+ return [a2[0] + b[0], a2[1] + b[1], a2[2] + b[2]];
18003
+ }
18004
+ function scale3$1(a2, s) {
18005
+ return [a2[0] * s, a2[1] * s, a2[2] * s];
18006
+ }
18007
+ function cross3$5(a2, b) {
18008
+ return [a2[1] * b[2] - a2[2] * b[1], a2[2] * b[0] - a2[0] * b[2], a2[0] * b[1] - a2[1] * b[0]];
18009
+ }
18010
+ function dist3(a2, b) {
18011
+ return Math.hypot(a2[0] - b[0], a2[1] - b[1], a2[2] - b[2]);
18012
+ }
18013
+ function angleBetween(a2, b) {
18014
+ const la = Math.hypot(a2[0], a2[1], a2[2]);
18015
+ const lb = Math.hypot(b[0], b[1], b[2]);
18016
+ if (la < 1e-12 || lb < 1e-12) return 0;
18017
+ const dot2 = (a2[0] * b[0] + a2[1] * b[1] + a2[2] * b[2]) / (la * lb);
18018
+ return Math.acos(Math.min(1, Math.max(-1, dot2))) * 180 / Math.PI;
18019
+ }
17410
18020
  let _wasm$1 = null;
17411
18021
  async function initManifoldWasm() {
17412
18022
  if (_wasm$1) return _wasm$1;
@@ -17512,7 +18122,9 @@ const _ManifoldShapeBackend = class _ManifoldShapeBackend {
17512
18122
  return this.getLiveManifold("numTri()").numTri();
17513
18123
  }
17514
18124
  getMesh() {
17515
- return this.getLiveManifold("getMesh()").getMesh();
18125
+ const manifold = this.getLiveManifold("getMesh()");
18126
+ const mesh = manifold.numProp() >= 3 ? manifold.getMesh(0) : manifold.getMesh();
18127
+ return mesh;
17516
18128
  }
17517
18129
  slice(offset) {
17518
18130
  return this.getLiveManifold("slice()").slice(offset);
@@ -17561,6 +18173,43 @@ function requireManifoldShapeBackend(backend, apiName = "requireManifoldShapeBac
17561
18173
  }
17562
18174
  throw new Error(`${apiName} currently requires a Manifold-backed runtime shape.`);
17563
18175
  }
18176
+ function resamplePolygon(poly, targetCount) {
18177
+ if (poly.length < 2) return poly;
18178
+ if (targetCount <= 0) return [];
18179
+ const dists = [0];
18180
+ for (let i = 0; i < poly.length; i++) {
18181
+ const p1 = poly[i];
18182
+ const p2 = poly[(i + 1) % poly.length];
18183
+ const dx = p2[0] - p1[0];
18184
+ const dy = p2[1] - p1[1];
18185
+ const d2 = Math.sqrt(dx * dx + dy * dy);
18186
+ dists.push(dists[dists.length - 1] + d2);
18187
+ }
18188
+ const totalDist = dists[dists.length - 1];
18189
+ if (totalDist < 1e-12) {
18190
+ return Array.from({ length: targetCount }, () => [poly[0][0], poly[0][1]]);
18191
+ }
18192
+ const out = [];
18193
+ for (let i = 0; i < targetCount; i++) {
18194
+ const targetDist = i / targetCount * totalDist;
18195
+ let low = 0;
18196
+ let high = dists.length - 1;
18197
+ while (low < high) {
18198
+ const mid = low + high >> 1;
18199
+ if (dists[mid] <= targetDist) {
18200
+ low = mid + 1;
18201
+ } else {
18202
+ high = mid;
18203
+ }
18204
+ }
18205
+ const seg = low - 1;
18206
+ const t = (targetDist - dists[seg]) / (dists[seg + 1] - dists[seg]);
18207
+ const p1 = poly[seg % poly.length];
18208
+ const p2 = poly[(seg + 1) % poly.length];
18209
+ out.push([p1[0] + (p2[0] - p1[0]) * t, p1[1] + (p2[1] - p1[1]) * t]);
18210
+ }
18211
+ return out;
18212
+ }
17564
18213
  function sweepStitched(profilePolygons, pathPoints, up, wasm) {
17565
18214
  if (pathPoints.length < 2) return null;
17566
18215
  if (profilePolygons.length === 0) return null;
@@ -18258,7 +18907,7 @@ function lowerOffsetLoftCompilePlan(plan, thickness, wasm) {
18258
18907
  throw new Error("offsetSolid() collapsed the compatible-loft height span.");
18259
18908
  }
18260
18909
  const offsetPolygons = plan.profiles.map((profile) => offsetProfilePolygonsForManifold(profile, thickness, wasm));
18261
- const stitched = loftStitched(offsetPolygons, heights, wasm);
18910
+ const stitched = loftStitched(offsetPolygons, heights, wasm, { edgeLength: plan.edgeLength });
18262
18911
  if (!stitched) {
18263
18912
  throw new Error(`Offset solid requires the OCCT backend. ${OCCT_BACKEND_REQUIRED_HINT}`);
18264
18913
  }
@@ -18393,12 +19042,12 @@ function lowerShapeLoftCompilePlan(plan, wasm) {
18393
19042
  disposeWasmObject(crossSection);
18394
19043
  }
18395
19044
  });
18396
- if (inputPolygons.length >= 2) {
18397
- const stitched = loftStitched(inputPolygons, plan.heights, wasm);
19045
+ if (!plan.forceField && inputPolygons.length >= 2) {
19046
+ const stitched = loftStitched(inputPolygons, plan.heights, wasm, { edgeLength: plan.edgeLength });
18398
19047
  if (stitched) return stitched;
18399
19048
  }
18400
19049
  const input = buildLoftLevelSetInput(inputPolygons, plan.heights, { edgeLength: plan.edgeLength, boundsPadding: plan.boundsPadding });
18401
- return lowerSdfToManifold(levelSetFieldToStandardSdf3(input.sdf), input.bounds, input.edgeLength, wasm);
19050
+ return lowerSdfToManifold(levelSetFieldToStandardSdf3(input.sdf), input.bounds, input.edgeLength, wasm, plan.meshing);
18402
19051
  }
18403
19052
  function lowerShapeSweepCompilePlan(plan, wasm) {
18404
19053
  const crossSection = lowerProfileCompilePlanToCrossSection(plan.profile, wasm);
@@ -18663,7 +19312,7 @@ function lowerFromSlicesToManifold(plan, wasm) {
18663
19312
  }
18664
19313
  });
18665
19314
  const heights = sorted.map((s) => s.offset);
18666
- const stitched = polygons.length >= 2 ? loftStitched(polygons, heights, wasm) : null;
19315
+ const stitched = polygons.length >= 2 ? loftStitched(polygons, heights, wasm, { edgeLength: plan.edgeLength }) : null;
18667
19316
  if (stitched) {
18668
19317
  solid = stitched;
18669
19318
  } else {
@@ -19104,17 +19753,21 @@ function lowerSdfToManifold(evalFn, bounds, edgeLength2, wasm, meshing, evaluato
19104
19753
  if (diagnostics) diagnostics.projectionMs = nowMs() - projectionStart;
19105
19754
  const simplificationStart = nowMs();
19106
19755
  if (((meshing == null ? void 0 : meshing.simplify) ?? "safe") !== "off" && snMesh.numTris > 100) {
19107
- triVerts = simplifySdfMesh(triVerts, snMesh.vertProperties, edgeLength2, wasm, meshing == null ? void 0 : meshing.maxTriangles);
19756
+ triVerts = simplifySdfMesh(triVerts, snMesh.vertProperties, vertProps6, edgeLength2, meshing == null ? void 0 : meshing.maxTriangles);
19108
19757
  }
19109
19758
  if (diagnostics) {
19110
19759
  diagnostics.simplificationMs = nowMs() - simplificationStart;
19111
19760
  diagnostics.trianglesAfterSimplification = triVerts.length / 3;
19112
19761
  }
19113
19762
  if ((meshing == null ? void 0 : meshing.maxTriangles) !== void 0 && triVerts.length / 3 > meshing.maxTriangles) {
19763
+ const verb = (meshing.simplify ?? "safe") === "off" ? "produced" : "could only simplify to";
19114
19764
  throw new Error(
19115
- `SDF meshing produced ${triVerts.length / 3} triangles, above maxTriangles=${meshing.maxTriangles}. Increase maxTriangles, use a larger edgeLength, or choose quality: "draft".`
19765
+ `SDF meshing ${verb} ${triVerts.length / 3} safe triangles, above maxTriangles=${meshing.maxTriangles}. Increase maxTriangles or use a larger edgeLength.`
19116
19766
  );
19117
19767
  }
19768
+ if (!isClosedConsistentlyWoundTriangleMesh(triVerts, vertProps6.length / 6)) {
19769
+ throw new Error("SDF meshing produced an open, non-manifold, or inconsistently wound triangle mesh.");
19770
+ }
19118
19771
  const wasmMesh = new wasm.Mesh({
19119
19772
  numProp: 6,
19120
19773
  vertProperties: vertProps6,
@@ -19132,28 +19785,102 @@ function lowerSdfToManifold(evalFn, bounds, edgeLength2, wasm, meshing, evaluato
19132
19785
  disposeWasmObject(wasmMesh);
19133
19786
  }
19134
19787
  }
19135
- function simplifySdfMesh(triVerts, vertProperties, edgeLength2, wasm, maxTriangles) {
19788
+ function simplifySdfMesh(triVerts, vertProperties, finalVertProperties, edgeLength2, maxTriangles) {
19136
19789
  const maxError = edgeLength2 * 0.15;
19137
19790
  const inputTriangles = triVerts.length / 3;
19138
- const requestedRatio = maxTriangles && maxTriangles < inputTriangles ? maxTriangles / inputTriangles : 0.5;
19139
- const ratios = Array.from(/* @__PURE__ */ new Set([Math.max(0.05, Math.min(0.5, requestedRatio)), 0.75]));
19791
+ const ratios = buildSimplificationRatios(inputTriangles, maxTriangles);
19792
+ const vertexCount = finalVertProperties.length / 6;
19793
+ let bestValid = null;
19140
19794
  for (const ratio of ratios) {
19141
- let simplified = simplifyMesh(triVerts, vertProperties, ratio, maxError);
19142
- simplified = filterDegenerateTriangles(simplified);
19143
- let mesh = null;
19144
- let manifold = null;
19795
+ let simplified;
19145
19796
  try {
19146
- mesh = new wasm.Mesh({ numProp: 3, vertProperties, triVerts: simplified });
19147
- manifold = new wasm.Manifold(mesh);
19148
- return simplified;
19797
+ simplified = simplifyMesh(triVerts, vertProperties, ratio, maxError);
19149
19798
  } catch {
19150
- } finally {
19151
- disposeWasmObject(manifold);
19152
- disposeWasmObject(mesh);
19799
+ continue;
19800
+ }
19801
+ simplified = filterDegenerateTriangles(simplified);
19802
+ if (isClosedConsistentlyWoundTriangleMesh(simplified, vertexCount)) {
19803
+ if (maxTriangles === void 0 || simplified.length / 3 <= maxTriangles) {
19804
+ return simplified;
19805
+ }
19806
+ if (!bestValid || simplified.length < bestValid.length) {
19807
+ bestValid = simplified;
19808
+ }
19153
19809
  }
19154
19810
  }
19811
+ if (bestValid) {
19812
+ return bestValid;
19813
+ }
19155
19814
  return triVerts;
19156
19815
  }
19816
+ function buildSimplificationRatios(inputTriangles, maxTriangles) {
19817
+ if (maxTriangles === void 0 || maxTriangles >= inputTriangles) {
19818
+ return [0.5, 0.75];
19819
+ }
19820
+ const requestedRatio = Math.max(1 / inputTriangles, Math.min(0.5, maxTriangles / inputTriangles));
19821
+ const candidates = [
19822
+ requestedRatio,
19823
+ 0.05,
19824
+ 0.04,
19825
+ 0.03,
19826
+ 0.02,
19827
+ 0.015,
19828
+ 0.01,
19829
+ 5e-3,
19830
+ requestedRatio * 0.85,
19831
+ requestedRatio * 0.7,
19832
+ requestedRatio * 0.55,
19833
+ requestedRatio * 0.4,
19834
+ requestedRatio * 0.25,
19835
+ requestedRatio * 0.1,
19836
+ 0.5,
19837
+ 0.75
19838
+ ];
19839
+ const seen = /* @__PURE__ */ new Set();
19840
+ const ratios = [];
19841
+ for (const ratio of candidates) {
19842
+ const rounded = Number(Math.max(1 / inputTriangles, Math.min(0.75, ratio)).toFixed(6));
19843
+ if (seen.has(rounded)) continue;
19844
+ seen.add(rounded);
19845
+ ratios.push(rounded);
19846
+ }
19847
+ return ratios;
19848
+ }
19849
+ function isClosedConsistentlyWoundTriangleMesh(triVerts, vertexCount) {
19850
+ if (triVerts.length === 0 || triVerts.length % 3 !== 0) return false;
19851
+ const edgeUse = /* @__PURE__ */ new Map();
19852
+ for (let i = 0; i < triVerts.length; i += 3) {
19853
+ const a2 = triVerts[i];
19854
+ const b = triVerts[i + 1];
19855
+ const c2 = triVerts[i + 2];
19856
+ if (!isValidMeshIndex(a2, vertexCount) || !isValidMeshIndex(b, vertexCount) || !isValidMeshIndex(c2, vertexCount)) return false;
19857
+ if (a2 === b || b === c2 || a2 === c2) return false;
19858
+ for (const [from, to] of [
19859
+ [a2, b],
19860
+ [b, c2],
19861
+ [c2, a2]
19862
+ ]) {
19863
+ const lo = Math.min(from, to);
19864
+ const hi = Math.max(from, to);
19865
+ const key = `${lo}/${hi}`;
19866
+ const winding = from === lo ? 1 : -1;
19867
+ const entry = edgeUse.get(key);
19868
+ if (entry) {
19869
+ entry.count += 1;
19870
+ entry.winding += winding;
19871
+ } else {
19872
+ edgeUse.set(key, { count: 1, winding });
19873
+ }
19874
+ }
19875
+ }
19876
+ for (const entry of edgeUse.values()) {
19877
+ if (entry.count !== 2 || entry.winding !== 0) return false;
19878
+ }
19879
+ return true;
19880
+ }
19881
+ function isValidMeshIndex(value, vertexCount) {
19882
+ return Number.isInteger(value) && value >= 0 && value < vertexCount;
19883
+ }
19157
19884
  function filterDegenerateTriangles(triVerts) {
19158
19885
  let writeIdx = 0;
19159
19886
  for (let i = 0; i < triVerts.length; i += 3) {
@@ -20367,11 +21094,12 @@ function profileMayContainInteriorLoopsForOCCT(plan) {
20367
21094
  return false;
20368
21095
  }
20369
21096
  }
21097
+ const occtLoweredCache = /* @__PURE__ */ new WeakMap();
20370
21098
  function lowerShapeCompilePlanToOCCT(plan, oc) {
20371
- const cached = plan._occtCache;
21099
+ const cached = occtLoweredCache.get(plan);
20372
21100
  if (cached) return cached;
20373
21101
  const shape = _lowerShapeCompilePlanToOCCTInner(plan, oc);
20374
- plan._occtCache = shape;
21102
+ occtLoweredCache.set(plan, shape);
20375
21103
  return shape;
20376
21104
  }
20377
21105
  function _lowerShapeCompilePlanToOCCTInner(plan, oc) {
@@ -36471,29 +37199,11 @@ function lowerExactSlicedShapeCompilePlanToTruckProfileBackend(plan, offset) {
36471
37199
  return profilePlan ? lowerProfileCompilePlanToTruckProfileBackend(profilePlan) : null;
36472
37200
  }
36473
37201
  const SHAPE_COMPILE_PLAN_CACHE_KEY_VERSION = "shape-plan-v1";
36474
- function stableJsonEncode(value, arrayMember) {
36475
- if (value === void 0 || typeof value === "function" || typeof value === "symbol") {
36476
- return arrayMember ? "null" : void 0;
36477
- }
36478
- if (value === null || typeof value === "number" || typeof value === "boolean" || typeof value === "string") {
36479
- return JSON.stringify(value);
36480
- }
36481
- if (Array.isArray(value)) {
36482
- return `[${value.map((item) => stableJsonEncode(item, true) ?? "null").join(",")}]`;
36483
- }
36484
- const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
36485
- const encodedEntries = [];
36486
- for (const [key, item] of entries) {
36487
- const encoded = stableJsonEncode(item, false);
36488
- if (encoded !== void 0) encodedEntries.push(`${JSON.stringify(key)}:${encoded}`);
36489
- }
36490
- return `{${encodedEntries.join(",")}}`;
36491
- }
36492
- function stableJsonStringify(value) {
36493
- return stableJsonEncode(value, false) ?? "null";
36494
- }
37202
+ const exactPlanHasher = createStructuralHasher({
37203
+ skipKey: (key) => key.startsWith("_") || key === "owner" || key === "queryPropagation"
37204
+ });
36495
37205
  function shapeCompilePlanCacheKey(plan) {
36496
- return `${SHAPE_COMPILE_PLAN_CACHE_KEY_VERSION}:${stableJsonStringify(plan)}`;
37206
+ return `${SHAPE_COMPILE_PLAN_CACHE_KEY_VERSION}:${exactPlanHasher(plan)}`;
36497
37207
  }
36498
37208
  function formatFaceQuery(query) {
36499
37209
  const parts = [];
@@ -39346,7 +40056,7 @@ class ShapeGroup {
39346
40056
  };
39347
40057
  return this.attachTo(parent, face, opp[face], uvMap[face](u2, v, p2));
39348
40058
  }
39349
- /** Rotate the group around an arbitrary axis through the origin. */
40059
+ /** Rotate the group around an arbitrary axis through the origin. Unlike `scale()`/`mirror()` (bounding-box center) and `Sketch.rotate()`, this pivots at the world origin — pass `options.pivot` to rotate in place. */
39350
40060
  rotate(axis, angleDeg, options) {
39351
40061
  requireRotateAxis(axis, "ShapeGroup.rotate()");
39352
40062
  requireFiniteAngle(angleDeg, "ShapeGroup.rotate()");
@@ -39591,6 +40301,11 @@ class ShapeGroup {
39591
40301
  * Position this group by matching connectors to a target.
39592
40302
  * Connector names support dotted paths into named children: "ChildName.connectorName".
39593
40303
  *
40304
+ * Alignment: with a single connector pair, the group translates and rotates so the connector
40305
+ * origins coincide and the axes oppose (plug-in model); `up` pins the roll. With multiple pairs,
40306
+ * the connector origins define the rigid transform — still author meaningful `axis`/`up` values
40307
+ * so the same connectors remain useful for `connect()`, audits, and future matching.
40308
+ *
39594
40309
  * Overloads:
39595
40310
  * - Single pair: `matchTo(target, selfConn, targetConn, options?)`
39596
40311
  * - Dictionary (same target): `matchTo(target, { selfConn: targetConn, ... }, options?)`
@@ -41255,10 +41970,10 @@ function rigidTransformStepsFromMatrix(m2) {
41255
41970
  return [{ kind: "workplanePlacement", matrix: Array.from(m2) }];
41256
41971
  }
41257
41972
  async function initKernel() {
41258
- const [manifoldModule] = await Promise.all([initManifoldWasm(), initMeshoptimizer(), initTruckGeometryWasm()]);
41973
+ const [manifoldModule] = await Promise.all([initManifoldWasm(), initMeshoptimizer()]);
41259
41974
  return manifoldModule;
41260
41975
  }
41261
- const DEFAULT_ACTIVE_BACKEND = "truck";
41976
+ const DEFAULT_ACTIVE_BACKEND = "manifold";
41262
41977
  let _activeBackend = DEFAULT_ACTIVE_BACKEND;
41263
41978
  let _runtimeWarn = (msg) => console.warn(msg);
41264
41979
  async function activateBackend(backend) {
@@ -41285,6 +42000,7 @@ const _shapePlacementRefs = /* @__PURE__ */ new WeakMap();
41285
42000
  const _shapeExplodeHint = /* @__PURE__ */ new WeakMap();
41286
42001
  const _shapeRuntimeBackends = /* @__PURE__ */ new WeakMap();
41287
42002
  const _shapeTopology = /* @__PURE__ */ new WeakMap();
42003
+ const _shapeLineageTokens = /* @__PURE__ */ new WeakMap();
41288
42004
  const _shapeFaceLabels = /* @__PURE__ */ new WeakMap();
41289
42005
  const _shapeReferenceNames = /* @__PURE__ */ new WeakMap();
41290
42006
  const _shapeReferenceAliases = /* @__PURE__ */ new WeakMap();
@@ -41345,6 +42061,10 @@ function copyShapeReferenceMetadata(source, target) {
41345
42061
  const aliases = cloneReferenceAliases(_shapeReferenceAliases.get(source));
41346
42062
  if (aliases && aliases.size > 0) _shapeReferenceAliases.set(target, aliases);
41347
42063
  }
42064
+ function copyShapeLineage(source, target) {
42065
+ const token = _shapeLineageTokens.get(source);
42066
+ if (token) _shapeLineageTokens.set(target, token);
42067
+ }
41348
42068
  function assertNonEmptyReferenceName(name, apiName) {
41349
42069
  const trimmed = name.trim();
41350
42070
  if (!trimmed) throw new Error(`${apiName} requires a non-empty reference name.`);
@@ -41413,50 +42133,25 @@ function setShapeRuntimeBackendInternal(shape, payload) {
41413
42133
  return shape;
41414
42134
  }
41415
42135
  function setShapeCompilePlanInternal(shape, plan) {
41416
- _shapeCompilePlans.set(shape, cloneShapeCompilePlan(plan));
42136
+ _shapeCompilePlans.set(shape, deepFreezePlanData(plan));
41417
42137
  recordShapeSourceSpanInternal(shape, plan);
41418
42138
  return shape;
41419
42139
  }
41420
- function cloneShapeSourceSpanRecords(records) {
41421
- return (records ?? []).map((record) => ({
41422
- planCacheKey: record.planCacheKey,
41423
- sourceSpan: { ...record.sourceSpan }
41424
- }));
41425
- }
41426
42140
  function upsertShapeSourceSpanRecord(shape, record) {
41427
- const records = cloneShapeSourceSpanRecords(_shapeSourceSpans.get(shape));
41428
- if (records.some((existing) => existing.planCacheKey === record.planCacheKey)) return;
41429
- records.push({
41430
- planCacheKey: record.planCacheKey,
41431
- sourceSpan: { ...record.sourceSpan }
41432
- });
41433
- _shapeSourceSpans.set(shape, records);
41434
- }
41435
- function stableTraceSourcePlanEncode(value, arrayMember) {
41436
- if (value === void 0 || typeof value === "function" || typeof value === "symbol") {
41437
- return arrayMember ? "null" : void 0;
41438
- }
41439
- if (value === null || typeof value === "number" || typeof value === "boolean" || typeof value === "string") {
41440
- return JSON.stringify(value);
42141
+ let records = _shapeSourceSpans.get(shape);
42142
+ if (!records) {
42143
+ records = /* @__PURE__ */ new Map();
42144
+ _shapeSourceSpans.set(shape, records);
41441
42145
  }
41442
- if (Array.isArray(value)) {
41443
- return `[${value.map((item) => stableTraceSourcePlanEncode(item, true) ?? "null").join(",")}]`;
41444
- }
41445
- const record = value;
41446
- if (record.kind === "queryOwner" && record.base) {
41447
- return stableTraceSourcePlanEncode(record.base, arrayMember);
41448
- }
41449
- const entries = Object.entries(record).sort(([left], [right]) => left.localeCompare(right));
41450
- const encodedEntries = [];
41451
- for (const [key, item] of entries) {
41452
- if (key.startsWith("_") || key === "owner" || key === "queryPropagation") continue;
41453
- const encoded = stableTraceSourcePlanEncode(item, false);
41454
- if (encoded !== void 0) encodedEntries.push(`${JSON.stringify(key)}:${encoded}`);
41455
- }
41456
- return `{${encodedEntries.join(",")}}`;
42146
+ if (records.has(record.planCacheKey)) return;
42147
+ records.set(record.planCacheKey, Object.freeze({ ...record.sourceSpan }));
41457
42148
  }
42149
+ const traceSourcePlanHasher = createStructuralHasher({
42150
+ skipKey: (key) => key.startsWith("_") || key === "owner" || key === "queryPropagation",
42151
+ unwrap: (record) => record.kind === "queryOwner" && record.base ? record.base : void 0
42152
+ });
41458
42153
  function normalizedTraceSourcePlanCacheKey(plan) {
41459
- return `shape-plan-v1:${stableTraceSourcePlanEncode(plan, false) ?? "null"}`;
42154
+ return `shape-plan-v1:${traceSourcePlanHasher(plan)}`;
41460
42155
  }
41461
42156
  function recordShapeSourceSpanInternal(shape, plan) {
41462
42157
  if (!hasActiveRuntimeSourceResolver()) return;
@@ -41476,13 +42171,20 @@ function recordShapeSourceSpanInternal(shape, plan) {
41476
42171
  }
41477
42172
  }
41478
42173
  function copyShapeSourceSpans(source, target) {
41479
- const records = cloneShapeSourceSpanRecords(_shapeSourceSpans.get(source));
41480
- if (records.length > 0) _shapeSourceSpans.set(target, records);
42174
+ const records = _shapeSourceSpans.get(source);
42175
+ if (records && records.size > 0) _shapeSourceSpans.set(target, new Map(records));
41481
42176
  }
41482
42177
  function mergeShapeSourceSpans(sources, target) {
42178
+ let records = _shapeSourceSpans.get(target);
41483
42179
  for (const source of sources) {
41484
- for (const record of _shapeSourceSpans.get(source) ?? []) {
41485
- upsertShapeSourceSpanRecord(target, record);
42180
+ const sourceRecords = _shapeSourceSpans.get(source);
42181
+ if (!sourceRecords) continue;
42182
+ if (!records) {
42183
+ records = /* @__PURE__ */ new Map();
42184
+ _shapeSourceSpans.set(target, records);
42185
+ }
42186
+ for (const [key, span] of sourceRecords) {
42187
+ if (!records.has(key)) records.set(key, span);
41486
42188
  }
41487
42189
  }
41488
42190
  }
@@ -41517,7 +42219,7 @@ function getShapeRuntimeBackendInternal(shape) {
41517
42219
  function getShapeCompilePlanInternal(shape) {
41518
42220
  const stored = _shapeCompilePlans.get(shape);
41519
42221
  if (!stored) throw new Error("Shape has no compile plan — every Shape must have an explicit plan set via setShapeCompilePlanInternal()");
41520
- return cloneShapeCompilePlan(stored);
42222
+ return stored;
41521
42223
  }
41522
42224
  function getShapePlacementRefsInternal(shape) {
41523
42225
  return clonePlacementReferences(_shapePlacementRefs.get(shape) ?? createPlacementReferences());
@@ -42209,6 +42911,30 @@ function checkLabelCollisions(operation2, plans) {
42209
42911
  seen.push(...labels);
42210
42912
  }
42211
42913
  }
42914
+ function formatDiagnosticNumber(value) {
42915
+ if (!Number.isFinite(value)) return String(value);
42916
+ const rounded = Math.abs(value) < 5e-4 ? 0 : value;
42917
+ return Number(rounded.toFixed(3)).toString();
42918
+ }
42919
+ function formatDiagnosticVec(values) {
42920
+ return `[${values.slice(0, 3).map(formatDiagnosticNumber).join(",")}]`;
42921
+ }
42922
+ function formatShapeBoundsForDiagnostic(shape) {
42923
+ try {
42924
+ const bounds = shape.boundingBox();
42925
+ return `bounds=${formatDiagnosticVec(bounds.min)}..${formatDiagnosticVec(bounds.max)}`;
42926
+ } catch (error) {
42927
+ const message = error instanceof Error ? error.message : String(error);
42928
+ return `bounds=unavailable(${message})`;
42929
+ }
42930
+ }
42931
+ function formatShapeOperandForDiagnostic(role, shape) {
42932
+ const name = _shapeReferenceNames.get(shape);
42933
+ return `${role}=${name ? `${name} ` : ""}${formatShapeBoundsForDiagnostic(shape)}`;
42934
+ }
42935
+ function formatSourceSpanForDiagnostic(sourceSpan) {
42936
+ return sourceSpan ? ` source=${sourceSpan.fileName}:${sourceSpan.line}:${sourceSpan.column}` : "";
42937
+ }
42212
42938
  function withCopiedDimensions(source, out) {
42213
42939
  setShapeDimensionsInternal(out, cloneDimensions(getShapeDimensionsInternal(source), true));
42214
42940
  setShapeGeometryInfoInternal(out, getShapeGeometryInfoInternal(source));
@@ -42223,6 +42949,7 @@ function withCopiedDimensions(source, out) {
42223
42949
  const sourceLabels = cloneFaceLabelMap(_shapeFaceLabels.get(source));
42224
42950
  if (sourceLabels) _shapeFaceLabels.set(out, sourceLabels);
42225
42951
  copyShapeReferenceMetadata(source, out);
42952
+ copyShapeLineage(source, out);
42226
42953
  copyShapeSourceSpans(source, out);
42227
42954
  return setShapeCompilePlanInternal(out, getShapeCompilePlanInternal(source));
42228
42955
  }
@@ -42252,6 +42979,7 @@ function withTransformedDimensions(source, out, m2) {
42252
42979
  const sourceLabelsT = cloneFaceLabelMap(_shapeFaceLabels.get(source));
42253
42980
  if (sourceLabelsT) _shapeFaceLabels.set(out, sourceLabelsT);
42254
42981
  copyShapeReferenceMetadata(source, out);
42982
+ copyShapeLineage(source, out);
42255
42983
  copyShapeSourceSpans(source, out);
42256
42984
  return setShapeCompilePlanInternal(out, getShapeCompilePlanInternal(source));
42257
42985
  }
@@ -42679,6 +43407,7 @@ class Shape {
42679
43407
  this.colorHex = color;
42680
43408
  setShapeRuntimeBackendInternal(this, payload);
42681
43409
  setShapeGeometryInfoInternal(this, createGeometryInfo(geometryInfo));
43410
+ _shapeLineageTokens.set(this, {});
42682
43411
  }
42683
43412
  /** @internal Use .color() instead. */
42684
43413
  setColor(value) {
@@ -42797,6 +43526,12 @@ class Shape {
42797
43526
  * with `union()` / `difference()` to avoid collisions. Collision detection throws a
42798
43527
  * clear error with a fix suggestion.
42799
43528
  *
43529
+ * Boolean survival: `union()` and `intersection()` carry labels from every operand;
43530
+ * `difference()` carries only the base (first) operand's labels — cutter labels are
43531
+ * dropped. A surviving label addresses whatever portion of its face survives the
43532
+ * boolean; cutters may split or erase it, and a lineage shared by multiple union
43533
+ * operands resolves as a face set rather than a single face.
43534
+ *
42800
43535
  * For compile-covered shapes (extrude, loft, etc.) the lookup resolves via the shape's
42801
43536
  * compile plan. As a fallback, planar-faced mesh shapes (e.g. results of boolean ops)
42802
43537
  * are resolved via coplanar triangle clustering.
@@ -43213,7 +43948,7 @@ class Shape {
43213
43948
  const tbb = s.boundingBox();
43214
43949
  return this.moveTo(tbb.min[0] + localX, tbb.min[1] + localY, tbb.min[2] + localZ);
43215
43950
  }
43216
- /** Rotate around an arbitrary axis through the origin. */
43951
+ /** Rotate around an arbitrary axis through the origin. Unlike `Sketch.rotate()` (bounding-box center), this pivots at the world origin — pass `options.pivot` to rotate in place. */
43217
43952
  rotate(axis, angleDeg, options) {
43218
43953
  validateRotateAxis(axis, "Shape.rotate()");
43219
43954
  validateRotateAngle(angleDeg, "Shape.rotate()");
@@ -43400,7 +44135,7 @@ class Shape {
43400
44135
  * Warn if a boolean operation had no geometric effect.
43401
44136
  * Compares volumes before and after; if they match within tolerance, the operation was a no-op.
43402
44137
  */
43403
- static _checkBooleanNoOp(op, base, result) {
44138
+ static _checkBooleanNoOp(op, base, result, tools = []) {
43404
44139
  try {
43405
44140
  if (op === "intersection") {
43406
44141
  if (result.isEmpty()) {
@@ -43413,8 +44148,15 @@ class Shape {
43413
44148
  const volAfter = result.volume();
43414
44149
  const tol = Math.max(volBefore * 1e-4, 1e-3);
43415
44150
  if (Math.abs(volBefore - volAfter) < tol) {
44151
+ const sourceSpan = captureRuntimeSourceSpan();
44152
+ const operandContext = [
44153
+ formatShapeOperandForDiagnostic("base", base),
44154
+ ...tools.map((tool, index2) => formatShapeOperandForDiagnostic(`tool[${index2 + 1}]`, tool))
44155
+ ].join(" ");
43416
44156
  _runtimeWarn(
43417
- `subtract() had no effect — the tool may not overlap the base shape. Base vol=${volBefore.toFixed(1)}mm³, result vol=${volAfter.toFixed(1)}mm³`
44157
+ `subtract() had no effect — the tool may not overlap the base shape. Base vol=${volBefore.toFixed(1)}mm³, result vol=${volAfter.toFixed(1)}mm³.${formatSourceSpanForDiagnostic(sourceSpan)} ${operandContext}`,
44158
+ "boolean.difference.noEffect",
44159
+ sourceSpan ? { sourceSpan } : void 0
43418
44160
  );
43419
44161
  }
43420
44162
  }
@@ -43474,7 +44216,7 @@ class Shape {
43474
44216
  ),
43475
44217
  nextPlan
43476
44218
  );
43477
- Shape._checkBooleanNoOp("difference", this, resultShape);
44219
+ Shape._checkBooleanNoOp("difference", this, resultShape, shapes.slice(1));
43478
44220
  return resultShape;
43479
44221
  }
43480
44222
  /** Keep only the overlap with other shapes. Method form of intersection(). */
@@ -43877,6 +44619,11 @@ class Shape {
43877
44619
  /**
43878
44620
  * Position this shape by matching connectors to a target.
43879
44621
  *
44622
+ * Alignment: with a single connector pair, the shape translates and rotates so the connector
44623
+ * origins coincide and the axes oppose (plug-in model); `up` pins the roll. With multiple pairs,
44624
+ * the connector origins define the rigid transform — still author meaningful `axis`/`up` values
44625
+ * so the same connectors remain useful for `connect()`, audits, and future matching.
44626
+ *
43880
44627
  * Overloads:
43881
44628
  * - Single pair: `matchTo(target, selfConn, targetConn, options?)`
43882
44629
  * - Dictionary (same target): `matchTo(target, { selfConn: targetConn, ... }, options?)`
@@ -44254,15 +45001,23 @@ function computeGeometryArrays(mesh, options = {}) {
44254
45001
  normals[o + 7] = vertNormals[i2 * 3 + 1];
44255
45002
  normals[o + 8] = vertNormals[i2 * 3 + 2];
44256
45003
  } else if (numProp >= 6) {
44257
- normals[o] = vertProperties[i0 * numProp + 3];
44258
- normals[o + 1] = vertProperties[i0 * numProp + 4];
44259
- normals[o + 2] = vertProperties[i0 * numProp + 5];
44260
- normals[o + 3] = vertProperties[i1 * numProp + 3];
44261
- normals[o + 4] = vertProperties[i1 * numProp + 4];
44262
- normals[o + 5] = vertProperties[i1 * numProp + 5];
44263
- normals[o + 6] = vertProperties[i2 * numProp + 3];
44264
- normals[o + 7] = vertProperties[i2 * numProp + 4];
44265
- normals[o + 8] = vertProperties[i2 * numProp + 5];
45004
+ const corners = [i0, i1, i2];
45005
+ for (let v = 0; v < 3; v++) {
45006
+ const base = corners[v] * numProp;
45007
+ const nx = vertProperties[base + 3];
45008
+ const ny = vertProperties[base + 4];
45009
+ const nz = vertProperties[base + 5];
45010
+ const oc = o + v * 3;
45011
+ if (nx * nx + ny * ny + nz * nz > 1e-12) {
45012
+ normals[oc] = nx;
45013
+ normals[oc + 1] = ny;
45014
+ normals[oc + 2] = nz;
45015
+ } else {
45016
+ normals[oc] = fnx;
45017
+ normals[oc + 1] = fny;
45018
+ normals[oc + 2] = fnz;
45019
+ }
45020
+ }
44266
45021
  } else {
44267
45022
  normals[o] = fnx;
44268
45023
  normals[o + 1] = fny;