forgecad 0.9.16 → 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 (147) hide show
  1. package/dist/assets/{AdminPage-CXvls4-J.js → AdminPage-DwYHz72L.js} +1 -1
  2. package/dist/assets/{BenchmarkPage-B27zk8xL.js → BenchmarkPage-a9_f-1US.js} +1 -1
  3. package/dist/assets/{BlogPage-CMAVvgQL.js → BlogPage-DodHpvmf.js} +1 -1
  4. package/dist/assets/{DocsPage-knf4I4h7.js → DocsPage-B5LePEuj.js} +8 -858
  5. package/dist/assets/EditorApp-QXsAISLR.js +16307 -0
  6. package/dist/assets/{EmbedViewer-D7ZGlFjx.js → EmbedViewer-DdEHGUMU.js} +2 -2
  7. package/dist/assets/{LandingPageProofDriven-CnevhTE8.js → LandingPageProofDriven-yhhOodbf.js} +1 -1
  8. package/dist/assets/{LegalPage-BPTUmqeg.js → LegalPage-5RbKRGYK.js} +1 -1
  9. package/dist/assets/{PricingPage-B0D4goG_.js → PricingPage-E3Rma7aV.js} +1 -1
  10. package/dist/assets/{SettingsPage-CFF-UgjI.js → SettingsPage-BJZcM97j.js} +1 -1
  11. package/dist/assets/{app-T0pDcSX4.js → app-DSYrDg0V.js} +733 -205
  12. package/dist/assets/cli/{render-C5pcIISc.js → render-ZMHR9HkV.js} +19 -46
  13. package/dist/assets/{constructionHistoryWorker-Ba2Hm58b.js → constructionHistoryWorker-AwMMWSxg.js} +1103 -349
  14. package/dist/assets/{evalWorker-vkx310U2.js → evalWorker-DbNs7Dkp.js} +3798 -1622
  15. package/dist/assets/{inspectWorker-BuTJDVX6.js → inspectWorker-CZsCFtQT.js} +1163 -409
  16. package/dist/assets/{jointPose-B_Cgedn9.js → jointPose-DO6mnXn_.js} +1 -1
  17. package/dist/assets/{manifold-BWgsjmAM.js → manifold-BGlQBBH9.js} +1 -1
  18. package/dist/assets/{manifold-rZexZI0G.js → manifold-BU-tJwQh.js} +1 -1
  19. package/dist/assets/{manifold-D6IFSkhH.js → manifold-fy2MV7K1.js} +2 -2
  20. package/dist/assets/{reportWorker-0AGij1Ru.js → reportWorker-DO6hcQbh.js} +7155 -2437
  21. package/dist/assets/{scalar-sampling-budget-J5cuzxT1.js → scalar-sampling-budget-o90NSNmF.js} +3940 -1742
  22. package/dist/assets/{scanProxyWorker-Vl4Wxa1y.js → scanProxyWorker-2GtDLk-R.js} +1 -1
  23. package/dist/assets/{javascript-1kQXfVaz.js → typescript-DBQ6RN5l.js} +874 -22
  24. package/dist/cli/render.html +1 -1
  25. package/dist/docs/index.html +3 -3
  26. package/dist/docs-raw/AI/usage.md +1 -1
  27. package/dist/docs-raw/CLI.md +63 -241
  28. package/dist/docs-raw/README.md +6 -0
  29. package/dist/docs-raw/component-model.md +17 -150
  30. package/dist/docs-raw/generated/assembly.md +139 -598
  31. package/dist/docs-raw/generated/concepts.md +245 -3501
  32. package/dist/docs-raw/generated/core.md +277 -1251
  33. package/dist/docs-raw/generated/curves.md +387 -1608
  34. package/dist/docs-raw/generated/legacy.md +162 -0
  35. package/dist/docs-raw/generated/lib.md +227 -85
  36. package/dist/docs-raw/generated/output.md +38 -73
  37. package/dist/docs-raw/generated/runtime-names.md +23 -23
  38. package/dist/docs-raw/generated/sdf.md +68 -284
  39. package/dist/docs-raw/generated/sheet-metal.md +68 -335
  40. package/dist/docs-raw/generated/sketch.md +240 -1161
  41. package/dist/docs-raw/generated/viewport.md +75 -316
  42. package/dist/docs-raw/generated/wood.md +21 -49
  43. package/dist/docs-raw/guides/coordinate-system.md +4 -42
  44. package/dist/docs-raw/guides/inspection-bundles.md +44 -442
  45. package/dist/docs-raw/guides/joint-design.md +18 -79
  46. package/dist/docs-raw/guides/positioning.md +21 -143
  47. package/dist/docs-raw/guides/scene-presentation.md +89 -0
  48. package/dist/docs-raw/skills/forgecad-3d-reconstruction.md +25 -111
  49. package/dist/docs-raw/skills/forgecad-blockout-model.md +20 -117
  50. package/dist/docs-raw/skills/forgecad-component-model.md +23 -107
  51. package/dist/docs-raw/skills/forgecad-high-level-spec.md +47 -155
  52. package/dist/docs-raw/skills/forgecad-image-replicator.md +26 -143
  53. package/dist/docs-raw/skills/forgecad-lld.md +19 -113
  54. package/dist/docs-raw/skills/forgecad-make-a-model.md +112 -532
  55. package/dist/docs-raw/skills/forgecad-model-grader.md +38 -108
  56. package/dist/docs-raw/skills/forgecad-prepare-prompt.md +24 -211
  57. package/dist/docs-raw/skills/forgecad-project.md +13 -131
  58. package/dist/docs-raw/skills/forgecad-reconstruction-benchmark.md +42 -134
  59. package/dist/docs-raw/skills/forgecad-render-inspect.md +27 -174
  60. package/dist/docs-raw/skills/forgecad-visual-spec.md +32 -112
  61. package/dist/docs-raw/skills/forgecad.md +19 -18
  62. package/dist/docs-raw/skills/index.md +2 -0
  63. package/dist/docs-raw/welcome.md +2 -2
  64. package/dist/index.html +1 -1
  65. package/dist/llms.txt +1 -2
  66. package/dist/sitemap.xml +13 -13
  67. package/dist-cli/{check-compiler-SYQ2PWOB.js → check-compiler-JTVBITCR.js} +1 -1
  68. package/dist-cli/{check-query-propagation-HIAGV62W.js → check-query-propagation-3FFLSMVN.js} +1 -1
  69. package/dist-cli/{chunk-SPZE3DUY.js → chunk-OAN5T4XD.js} +4412 -2212
  70. package/dist-cli/forgecad.js +507 -179
  71. package/dist-skill/CONTEXT.md +2172 -8377
  72. package/dist-skill/SKILL.md +15 -15
  73. package/dist-skill/docs/API/core/concepts.md +27 -157
  74. package/dist-skill/docs/CLI.md +63 -241
  75. package/dist-skill/docs/generated/assembly.md +138 -549
  76. package/dist-skill/docs/generated/core.md +277 -1251
  77. package/dist-skill/docs/generated/curves.md +387 -1609
  78. package/dist-skill/docs/generated/lib.md +227 -85
  79. package/dist-skill/docs/generated/output.md +38 -73
  80. package/dist-skill/docs/generated/runtime-names.md +16 -21
  81. package/dist-skill/docs/generated/sdf.md +68 -284
  82. package/dist-skill/docs/generated/sheet-metal.md +68 -335
  83. package/dist-skill/docs/generated/sketch.md +240 -1160
  84. package/dist-skill/docs/generated/viewport.md +75 -223
  85. package/dist-skill/docs/generated/wood.md +21 -49
  86. package/dist-skill/docs/guides/coordinate-system.md +4 -42
  87. package/dist-skill/docs/guides/inspection-bundles.md +44 -442
  88. package/dist-skill/docs/guides/joint-design.md +18 -79
  89. package/dist-skill/docs/guides/positioning.md +21 -143
  90. package/dist-skill/docs/guides/scene-presentation.md +89 -0
  91. package/dist-skill/docs/guides/surface-members.md +26 -0
  92. package/dist-skill/library/forgecad-3d-reconstruction/SKILL.md +23 -111
  93. package/dist-skill/library/forgecad-blockout-model/SKILL.md +18 -117
  94. package/dist-skill/library/forgecad-component-model/SKILL.md +21 -107
  95. package/dist-skill/library/forgecad-high-level-spec/SKILL.md +45 -155
  96. package/dist-skill/library/forgecad-image-replicator/SKILL.md +24 -143
  97. package/dist-skill/library/forgecad-lld/SKILL.md +17 -113
  98. package/dist-skill/library/forgecad-make-a-model/SKILL.md +110 -532
  99. package/dist-skill/library/forgecad-model-grader/SKILL.md +36 -108
  100. package/dist-skill/library/forgecad-prepare-prompt/SKILL.md +35 -224
  101. package/dist-skill/library/forgecad-prepare-prompt/references/default-profiles.md +43 -271
  102. package/dist-skill/library/forgecad-prepare-prompt/references/master-prompt.md +30 -99
  103. package/dist-skill/library/forgecad-project/SKILL.md +13 -133
  104. package/dist-skill/library/forgecad-reconstruction-benchmark/SKILL.md +29 -123
  105. package/dist-skill/library/forgecad-render-inspect/SKILL.md +25 -174
  106. package/dist-skill/library/forgecad-visual-spec/SKILL.md +30 -111
  107. package/dist-skill/website/skills/forgecad-3d-reconstruction.md +58 -0
  108. package/dist-skill/website/skills/forgecad-blockout-model.md +49 -0
  109. package/dist-skill/website/skills/forgecad-component-model.md +53 -0
  110. package/dist-skill/website/skills/forgecad-high-level-spec.md +101 -0
  111. package/dist-skill/website/skills/forgecad-image-replicator.md +63 -0
  112. package/dist-skill/website/skills/forgecad-lld.md +41 -0
  113. package/dist-skill/website/skills/forgecad-make-a-model.md +186 -0
  114. package/dist-skill/website/skills/forgecad-model-grader.md +82 -0
  115. package/dist-skill/website/skills/forgecad-prepare-prompt.md +63 -0
  116. package/dist-skill/website/skills/forgecad-project.md +26 -0
  117. package/dist-skill/website/skills/forgecad-reconstruction-benchmark.md +60 -0
  118. package/dist-skill/website/skills/forgecad-render-inspect.md +80 -0
  119. package/dist-skill/website/skills/forgecad-visual-spec.md +71 -0
  120. package/dist-skill/website/skills/forgecad.md +122 -0
  121. package/dist-skill/website/skills/index.md +26 -0
  122. package/examples/api/comparison-imported-sphere-candidate.forge.js +1 -1
  123. package/examples/api/conformal-product-ribbon.forge.js +1 -1
  124. package/examples/api/exact-sheet-shell-assembly.forge.js +1 -1
  125. package/examples/api/extrude-options.forge.js +4 -2
  126. package/examples/api/field-loft-drive-tip.forge.js +40 -0
  127. package/examples/api/guided-loft-olive-oil-bottle.forge.js +1 -1
  128. package/examples/api/highlight-debug.forge.js +10 -10
  129. package/examples/api/mesh-import-slats.forge.js +1 -1
  130. package/examples/api/real-product-curves.forge.js +1 -1
  131. package/examples/api/sculpt-box-circle-booleans.forge.js +1 -1
  132. package/examples/api/sdf-shapes.forge.js +2 -5
  133. package/examples/api/sketch-rounding-strategies.forge.js +6 -6
  134. package/examples/api/surface-member-bottle-cage.forge.js +3 -3
  135. package/examples/api/surface-member-conformal-product-ribbon.forge.js +3 -3
  136. package/examples/api/surface-member-razor-inlay.forge.js +1 -1
  137. package/examples/api/variable-sweep-test.forge.js +3 -3
  138. package/examples/mechanical/airplane-propeller.forge.js +74 -39
  139. package/examples/nurbs-surface.forge.js +1 -1
  140. package/examples/products/iphone.forge.js +1 -1
  141. package/package.json +1 -1
  142. package/dist/assets/EditorApp-BHMQlJ-D.js +0 -14686
  143. package/dist/docs-raw/guides/geometry-conventions.md +0 -52
  144. package/dist/docs-raw/guides/modeling-recipes.md +0 -78
  145. package/dist-skill/docs/guides/geometry-conventions.md +0 -52
  146. package/dist-skill/docs/guides/modeling-recipes.md +0 -78
  147. package/dist-skill/library/forgecad-visual-spec/references/prompt-template.md +0 -79
@@ -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([
@@ -9021,6 +9047,8 @@ function cloneSdfNode(node) {
9021
9047
  return { kind: "sdf:circularArray", child: cloneSdfNode(node.child), count: node.count, offset: node.offset };
9022
9048
  case "sdf:shell":
9023
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 };
9024
9052
  case "sdf:displace":
9025
9053
  return {
9026
9054
  kind: "sdf:displace",
@@ -9105,7 +9133,7 @@ function cloneSdfNode(node) {
9105
9133
  }
9106
9134
  }
9107
9135
  const SHEET_METAL_EDGES = ["top", "right", "bottom", "left"];
9108
- const EPS$3 = 1e-9;
9136
+ const EPS$2 = 1e-9;
9109
9137
  function isFinitePositive$1(value) {
9110
9138
  return Number.isFinite(value) && value > 0;
9111
9139
  }
@@ -9146,7 +9174,7 @@ function edgeDisplayName(edge) {
9146
9174
  return `sheetMetal().flange("${edge}", ...)`;
9147
9175
  }
9148
9176
  function normalizeAngle(angleDeg) {
9149
- return Math.abs(angleDeg) <= EPS$3 ? 0 : angleDeg;
9177
+ return Math.abs(angleDeg) <= EPS$2 ? 0 : angleDeg;
9150
9178
  }
9151
9179
  function validateSheetMetalModel(model) {
9152
9180
  if (!isFinitePositive$1(model.panel.width) || !isFinitePositive$1(model.panel.height)) {
@@ -9158,7 +9186,7 @@ function validateSheetMetalModel(model) {
9158
9186
  if (!isFiniteNonNegative(model.bendRadius)) {
9159
9187
  return "sheetMetal() requires a finite non-negative bendRadius.";
9160
9188
  }
9161
- if (model.bendRadius <= EPS$3) {
9189
+ if (model.bendRadius <= EPS$2) {
9162
9190
  return "sheetMetal() v1 requires a positive bendRadius so the bend region stays explicit instead of collapsing into a sharp fold.";
9163
9191
  }
9164
9192
  if (model.bendAllowance.kind !== "k-factor") {
@@ -9220,7 +9248,7 @@ function deriveSheetMetalModel(model) {
9220
9248
  const trimEnd = flanges.has(adjacent.end) ? model.cornerRelief.size : 0;
9221
9249
  const fullLength = edge === "top" || edge === "bottom" ? model.panel.width : model.panel.height;
9222
9250
  const span = fullLength - trimStart - trimEnd;
9223
- if (!(span > EPS$3)) {
9251
+ if (!(span > EPS$2)) {
9224
9252
  throw new Error(
9225
9253
  `${edgeDisplayName(edge)} loses all usable span after applying the defended rectangular corner relief size ${model.cornerRelief.size}.`
9226
9254
  );
@@ -9266,7 +9294,7 @@ function transformPlacement(origin, u2, v, normal) {
9266
9294
  };
9267
9295
  }
9268
9296
  function translatePlan(plan, x2, y2, z2) {
9269
- 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);
9270
9298
  return appendShapeCompileTransform(cloneShapeCompilePlan(plan), {
9271
9299
  kind: "translate",
9272
9300
  x: x2,
@@ -10438,6 +10466,8 @@ function cloneShapeCompilePlan(plan) {
10438
10466
  heights: plan.heights.map((height) => height),
10439
10467
  edgeLength: plan.edgeLength,
10440
10468
  boundsPadding: plan.boundsPadding,
10469
+ ...plan.forceField ? { forceField: true } : {},
10470
+ ...plan.meshing ? { meshing: cloneSdfCompileMeshingSettings(plan.meshing) } : {},
10441
10471
  edgeLabels: plan.edgeLabels ? { ...plan.edgeLabels } : void 0,
10442
10472
  capLabels: plan.capLabels ? { ...plan.capLabels } : void 0
10443
10473
  };
@@ -10705,7 +10735,6 @@ function cloneShapeCompilePlan(plan) {
10705
10735
  default:
10706
10736
  assertExhaustive(plan);
10707
10737
  }
10708
- if (plan._occtCache) result._occtCache = plan._occtCache;
10709
10738
  return result;
10710
10739
  }
10711
10740
  function appendProfileCompileTransform(plan, step) {
@@ -10718,22 +10747,31 @@ function appendShapeCompileTransform(plan, step) {
10718
10747
  if (plan.kind === "transform") {
10719
10748
  return {
10720
10749
  kind: "transform",
10721
- base: cloneShapeCompilePlan(plan.base),
10722
- steps: [...plan.steps.map(cloneShapeTransform), cloneShapeTransform(step)]
10750
+ base: plan.base,
10751
+ steps: [...plan.steps, cloneShapeTransform(step)]
10723
10752
  };
10724
10753
  }
10725
10754
  return {
10726
10755
  kind: "transform",
10727
- base: cloneShapeCompilePlan(plan),
10756
+ base: plan,
10728
10757
  steps: [cloneShapeTransform(step)]
10729
10758
  };
10730
10759
  }
10731
10760
  function appendShapeCompileTransforms(plan, steps) {
10732
- let out = cloneShapeCompilePlan(plan);
10733
- for (const step of steps) {
10734
- 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
+ };
10735
10769
  }
10736
- return out;
10770
+ return {
10771
+ kind: "transform",
10772
+ base: plan,
10773
+ steps: steps.map(cloneShapeTransform)
10774
+ };
10737
10775
  }
10738
10776
  function wrapShapeCompilePlanWithQueryOwner(plan, owner) {
10739
10777
  if (!plan) return null;
@@ -10807,6 +10845,8 @@ function buildLoftShapeCompilePlan(profiles, heights, options) {
10807
10845
  heights: heights.map((height) => canonicalNumber(height)),
10808
10846
  edgeLength: canonicalNumber(options.edgeLength),
10809
10847
  boundsPadding: canonicalNumber(options.boundsPadding),
10848
+ ...options.forceField ? { forceField: true } : {},
10849
+ ...options.meshing ? { meshing: cloneSdfCompileMeshingSettings(options.meshing) } : {},
10810
10850
  edgeLabels: options.edgeLabels ? { ...options.edgeLabels } : void 0
10811
10851
  };
10812
10852
  }
@@ -10908,14 +10948,14 @@ function sub$2(a2, b) {
10908
10948
  function dot$3(a2, b) {
10909
10949
  return a2[0] * b[0] + a2[1] * b[1] + a2[2] * b[2];
10910
10950
  }
10911
- function cross$4(a2, b) {
10951
+ function cross$3(a2, b) {
10912
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]];
10913
10953
  }
10914
10954
  function rotateAroundAxis(v, axis, angleRad) {
10915
10955
  const c2 = Math.cos(angleRad);
10916
10956
  const s = Math.sin(angleRad);
10917
10957
  const term1 = scale$2(v, c2);
10918
- const term2 = scale$2(cross$4(axis, v), s);
10958
+ const term2 = scale$2(cross$3(axis, v), s);
10919
10959
  const term3 = scale$2(axis, dot$3(axis, v) * (1 - c2));
10920
10960
  return add$2(add$2(term1, term2), term3);
10921
10961
  }
@@ -11191,13 +11231,13 @@ function sweepPathToPolylineAdaptive(path, baseSamples = 48) {
11191
11231
  pts.push(evalPathAt(path, 1));
11192
11232
  return pts;
11193
11233
  }
11194
- const EPS$2 = 1e-8;
11234
+ const EPS$1 = 1e-8;
11195
11235
  function midpoint$1(start, end) {
11196
11236
  return [(start[0] + end[0]) * 0.5, (start[1] + end[1]) * 0.5, (start[2] + end[2]) * 0.5];
11197
11237
  }
11198
11238
  function normalize$3(v) {
11199
11239
  const len = Math.hypot(v[0], v[1], v[2]);
11200
- 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");
11201
11241
  return [v[0] / len, v[1] / len, v[2] / len];
11202
11242
  }
11203
11243
  function subtract(a2, b) {
@@ -11276,7 +11316,7 @@ function rigidTransformForEdgeStep(step) {
11276
11316
  case "mirror": {
11277
11317
  const [nx0, ny0, nz0] = [step.normalX, step.normalY, step.normalZ];
11278
11318
  const len = Math.hypot(nx0, ny0, nz0);
11279
- if (len <= EPS$2) return Transform.identity();
11319
+ if (len <= EPS$1) return Transform.identity();
11280
11320
  const nx = nx0 / len;
11281
11321
  const ny = ny0 / len;
11282
11322
  const nz = nz0 / len;
@@ -11573,7 +11613,7 @@ function isRectangleProfile(points) {
11573
11613
  return [next[0] - point[0], next[1] - point[1]];
11574
11614
  });
11575
11615
  const lengths = vectors.map(([x2, y2]) => Math.hypot(x2, y2));
11576
- if (lengths.some((length4) => length4 <= EPS$2)) return false;
11616
+ if (lengths.some((length4) => length4 <= EPS$1)) return false;
11577
11617
  const dot01 = vectors[0][0] * vectors[1][0] + vectors[0][1] * vectors[1][1];
11578
11618
  const dot12 = vectors[1][0] * vectors[2][0] + vectors[1][1] * vectors[2][1];
11579
11619
  const dot23 = vectors[2][0] * vectors[3][0] + vectors[2][1] * vectors[3][1];
@@ -13512,7 +13552,9 @@ function lowerLoftShellToConcretePlan(plan, thickness, openFaces) {
13512
13552
  profiles: innerProfiles,
13513
13553
  heights: innerHeights,
13514
13554
  edgeLength: plan.edgeLength,
13515
- boundsPadding: plan.boundsPadding
13555
+ boundsPadding: plan.boundsPadding,
13556
+ ...plan.forceField ? { forceField: true } : {},
13557
+ ...plan.meshing ? { meshing: { ...plan.meshing } } : {}
13516
13558
  };
13517
13559
  return { ok: true, plan: buildBooleanShapeCompilePlan("difference", [plan, inner]) };
13518
13560
  }
@@ -13650,6 +13692,197 @@ function lowerShellShapeCompilePlanToConcretePlan(plan) {
13650
13692
  }
13651
13693
  return lowerBaseShellPlanToConcretePlan(plan.base, plan.thickness, normalizeShellOpenFaces(plan.openFaces));
13652
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
+ }
13653
13886
  const SHAPE_GEOMETRY_CACHE_KEY_VERSION = "shape-geometry-v1";
13654
13887
  const SHAPE_BUILD_CACHE_POLICY = "live-structural-lru-v1";
13655
13888
  const STRUCTURAL_CACHE_BUDGET_MB = 64;
@@ -13695,60 +13928,26 @@ function recordEvent(event) {
13695
13928
  runEvents.push(next);
13696
13929
  if (runEvents.length > MAX_RECORDED_EVENTS) runEvents = runEvents.slice(-MAX_RECORDED_EVENTS);
13697
13930
  }
13698
- function stableGeometryEncode(value, arrayMember) {
13699
- if (value === void 0 || typeof value === "function" || typeof value === "symbol") {
13700
- return arrayMember ? "null" : void 0;
13701
- }
13702
- if (value === null || typeof value === "number" || typeof value === "boolean" || typeof value === "string") {
13703
- return JSON.stringify(value);
13704
- }
13705
- if (Array.isArray(value)) {
13706
- return `[${value.map((item) => stableGeometryEncode(item, true) ?? "null").join(",")}]`;
13707
- }
13708
- const record = value;
13709
- if (record.kind === "queryOwner" && record.base) {
13710
- return stableGeometryEncode(record.base, arrayMember);
13711
- }
13712
- const entries = Object.entries(record).sort(([left], [right]) => left.localeCompare(right));
13713
- const encodedEntries = [];
13714
- for (const [key, item] of entries) {
13715
- if (key.startsWith("_") || key === "owner" || key === "queryPropagation") continue;
13716
- const encoded = stableGeometryEncode(item, false);
13717
- if (encoded !== void 0) encodedEntries.push(`${JSON.stringify(key)}:${encoded}`);
13718
- }
13719
- return `{${encodedEntries.join(",")}}`;
13720
- }
13721
- function stableCacheOpportunityEncode(value, arrayMember) {
13722
- if (value === void 0 || typeof value === "function" || typeof value === "symbol") {
13723
- return arrayMember ? "null" : void 0;
13724
- }
13725
- if (value === null || typeof value === "number" || typeof value === "boolean" || typeof value === "string") {
13726
- return JSON.stringify(value);
13727
- }
13728
- if (Array.isArray(value)) {
13729
- return `[${value.map((item) => stableCacheOpportunityEncode(item, true) ?? "null").join(",")}]`;
13730
- }
13731
- const record = value;
13732
- if (record.kind === "queryOwner" && record.base) {
13733
- return stableCacheOpportunityEncode(record.base, arrayMember);
13734
- }
13735
- let encodedRecord = record;
13736
- if (record.kind === "transform" && record.base) {
13737
- const retainedSteps = Array.isArray(record.steps) ? record.steps.filter((step) => step.kind === "scale") : [];
13738
- if (retainedSteps.length === 0) return stableCacheOpportunityEncode(record.base, arrayMember);
13739
- encodedRecord = { kind: "transform", base: record.base, steps: retainedSteps };
13740
- }
13741
- const entries = Object.entries(encodedRecord).sort(([left], [right]) => left.localeCompare(right));
13742
- const encodedEntries = [];
13743
- for (const [key, item] of entries) {
13744
- if (key.startsWith("_") || key === "owner" || key === "queryPropagation") continue;
13745
- const encoded = stableCacheOpportunityEncode(item, false);
13746
- 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) };
13747
13947
  }
13748
- return `{${encodedEntries.join(",")}}`;
13749
- }
13948
+ });
13750
13949
  function shapeGeometryCacheKey(plan) {
13751
- return `${SHAPE_GEOMETRY_CACHE_KEY_VERSION}:${stableGeometryEncode(plan, false) ?? "null"}`;
13950
+ return `${SHAPE_GEOMETRY_CACHE_KEY_VERSION}:${geometryPlanHasher(plan)}`;
13752
13951
  }
13753
13952
  function planComplexityScore(value) {
13754
13953
  if (!value || typeof value !== "object") return 0;
@@ -13807,8 +14006,7 @@ function planComplexityScore(value) {
13807
14006
  }
13808
14007
  }
13809
14008
  function estimateCacheRetainedMb(plan) {
13810
- var _a3;
13811
- const encodedLength = ((_a3 = stableCacheOpportunityEncode(plan, false)) == null ? void 0 : _a3.length) ?? 0;
14009
+ const encodedLength = estimatePlanEncodedLength(plan);
13812
14010
  const serializedComplexityMb = encodedLength / 24e3;
13813
14011
  return round2(0.08 + planComplexityScore(plan) * 0.09 + serializedComplexityMb);
13814
14012
  }
@@ -13826,26 +14024,62 @@ function splitCacheablePlacement(plan) {
13826
14024
  }
13827
14025
  return { basePlan: plan, placementSteps: [] };
13828
14026
  }
14027
+ const uncacheableReasonMemo = /* @__PURE__ */ new WeakMap();
13829
14028
  function findUncacheableReason(value) {
13830
14029
  if (value === void 0 || value === null) return null;
13831
14030
  if (typeof value === "function" || typeof value === "symbol") return "plan contains runtime-only values";
13832
14031
  if (typeof value !== "object") return null;
13833
- if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) return "plan contains binary file data";
13834
- if (Array.isArray(value)) {
13835
- for (const item of value) {
13836
- const reason = findUncacheableReason(item);
13837
- 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);
13838
14075
  }
13839
- return null;
13840
14076
  }
13841
- const record = value;
13842
- if (record.kind === "importedMesh" || record.kind === "importedStep") return "plan depends on imported file contents";
13843
- for (const [key, item] of Object.entries(record)) {
13844
- if (key.startsWith("_")) continue;
13845
- const reason = findUncacheableReason(item);
13846
- 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);
13847
14081
  }
13848
- return null;
14082
+ return reason;
13849
14083
  }
13850
14084
  function applyPlacementStep(backend, step) {
13851
14085
  switch (step.kind) {
@@ -14522,8 +14756,9 @@ function analyzeNodeUV(node, toLocal) {
14522
14756
  if (result.majorRadius !== void 0) result.majorRadius *= node.factor;
14523
14757
  return result;
14524
14758
  }
14525
- // ── Shell — UV comes from the inner shape ──
14759
+ // ── Shell / offset — UV comes from the inner shape ──
14526
14760
  case "sdf:shell":
14761
+ case "sdf:offset":
14527
14762
  return analyzeNodeUV(node.child, toLocal);
14528
14763
  // ── CSG — take UV from the first (primary) child ──
14529
14764
  case "sdf:union":
@@ -15059,6 +15294,11 @@ function compileSdfNode3(node) {
15059
15294
  const t = node.thickness * 0.5;
15060
15295
  return (x2, y2, z2) => abs(fn(x2, y2, z2)) - t;
15061
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
+ }
15062
15302
  case "sdf:displace": {
15063
15303
  const fn = compileSdfNode3(node.child);
15064
15304
  const constEntries = Object.entries(node.constants ?? {});
@@ -15552,6 +15792,10 @@ function emitSdfProgramNode(b, node, x2, y2, z2) {
15552
15792
  const child = emitSdfProgramNode(b, node.child, x2, y2, z2);
15553
15793
  return b.sub(b.abs(child), b.constant(node.thickness * 0.5));
15554
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
+ }
15555
15799
  case "sdf:onion": {
15556
15800
  let d2 = emitSdfProgramNode(b, node.child, x2, y2, z2);
15557
15801
  for (let i = 0; i < node.layers; i++) d2 = b.sub(b.abs(d2), b.constant(node.thickness));
@@ -15700,6 +15944,7 @@ function getUnsupportedSdfProgramReason(node) {
15700
15944
  case "sdf:bend":
15701
15945
  case "sdf:repeat":
15702
15946
  case "sdf:shell":
15947
+ case "sdf:offset":
15703
15948
  case "sdf:onion":
15704
15949
  return getUnsupportedSdfProgramReason(node.child);
15705
15950
  default:
@@ -16141,7 +16386,19 @@ function simplifyMesh(triVerts, vertProperties, targetRatio, maxError) {
16141
16386
  if (!_simplifier) {
16142
16387
  throw new Error("meshoptimizer not initialized — call initMeshoptimizer() first");
16143
16388
  }
16144
- 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;
16145
16402
  const [simplified] = _simplifier.simplify(
16146
16403
  triVerts,
16147
16404
  vertProperties,
@@ -17118,117 +17375,10 @@ function buildSweepLevelSetInput(profilePolygons, pathInput, options) {
17118
17375
  edgeLength: options.edgeLength
17119
17376
  };
17120
17377
  }
17121
- const EPS$1 = 1e-9;
17122
- function resamplePolygon(poly, targetCount) {
17123
- if (poly.length < 2) return poly;
17124
- if (targetCount <= 0) return [];
17125
- const dists = [0];
17126
- for (let i = 0; i < poly.length; i++) {
17127
- const p1 = poly[i];
17128
- const p2 = poly[(i + 1) % poly.length];
17129
- const dx = p2[0] - p1[0];
17130
- const dy = p2[1] - p1[1];
17131
- const d2 = Math.sqrt(dx * dx + dy * dy);
17132
- dists.push(dists[dists.length - 1] + d2);
17133
- }
17134
- const totalDist = dists[dists.length - 1];
17135
- if (totalDist < 1e-12) {
17136
- return Array.from({ length: targetCount }, () => [poly[0][0], poly[0][1]]);
17137
- }
17138
- const out = [];
17139
- for (let i = 0; i < targetCount; i++) {
17140
- const targetDist = i / targetCount * totalDist;
17141
- let low = 0;
17142
- let high = dists.length - 1;
17143
- while (low < high) {
17144
- const mid = low + high >> 1;
17145
- if (dists[mid] <= targetDist) {
17146
- low = mid + 1;
17147
- } else {
17148
- high = mid;
17149
- }
17150
- }
17151
- const seg = low - 1;
17152
- const t = (targetDist - dists[seg]) / (dists[seg + 1] - dists[seg]);
17153
- const p1 = poly[seg % poly.length];
17154
- const p2 = poly[(seg + 1) % poly.length];
17155
- out.push([p1[0] + (p2[0] - p1[0]) * t, p1[1] + (p2[1] - p1[1]) * t]);
17156
- }
17157
- return out;
17158
- }
17159
- function resamplePolygonByAngle(poly, targetCount, center = polygonCentroid(poly)) {
17160
- if (poly.length < 3 || targetCount <= 0) return null;
17161
- if (!isConvexPolygon(poly)) return null;
17162
- const out = [];
17163
- for (let index2 = 0; index2 < targetCount; index2 += 1) {
17164
- const angle = index2 / targetCount * Math.PI * 2;
17165
- const point = rayPolygonIntersection(center, [Math.cos(angle), Math.sin(angle)], poly);
17166
- if (!point) return null;
17167
- out.push(point);
17168
- }
17169
- return out;
17170
- }
17171
- function rayPolygonIntersection(origin, direction, poly) {
17172
- let bestT = Infinity;
17173
- let best = null;
17174
- for (let index2 = 0; index2 < poly.length; index2 += 1) {
17175
- const a2 = poly[index2];
17176
- const b = poly[(index2 + 1) % poly.length];
17177
- const edge = [b[0] - a2[0], b[1] - a2[1]];
17178
- const denom = cross$3(direction, edge);
17179
- if (Math.abs(denom) < EPS$1) continue;
17180
- const delta = [a2[0] - origin[0], a2[1] - origin[1]];
17181
- const rayT = cross$3(delta, edge) / denom;
17182
- const edgeT = cross$3(delta, direction) / denom;
17183
- if (rayT >= -EPS$1 && edgeT >= -EPS$1 && edgeT <= 1 + EPS$1 && rayT < bestT) {
17184
- bestT = rayT;
17185
- best = [origin[0] + direction[0] * rayT, origin[1] + direction[1] * rayT];
17186
- }
17187
- }
17188
- return best;
17189
- }
17190
- function polygonCentroid(poly) {
17191
- let area2 = 0;
17192
- let cx = 0;
17193
- let cy = 0;
17194
- for (let index2 = 0; index2 < poly.length; index2 += 1) {
17195
- const a2 = poly[index2];
17196
- const b = poly[(index2 + 1) % poly.length];
17197
- const crossValue = cross$3(a2, b);
17198
- area2 += crossValue;
17199
- cx += (a2[0] + b[0]) * crossValue;
17200
- cy += (a2[1] + b[1]) * crossValue;
17201
- }
17202
- if (Math.abs(area2) < EPS$1) return averagePoint(poly);
17203
- return [cx / (3 * area2), cy / (3 * area2)];
17204
- }
17205
- function averagePoint(poly) {
17206
- let x2 = 0;
17207
- let y2 = 0;
17208
- for (const point of poly) {
17209
- x2 += point[0];
17210
- y2 += point[1];
17211
- }
17212
- return [x2 / poly.length, y2 / poly.length];
17213
- }
17214
- function isConvexPolygon(poly) {
17215
- let sign = 0;
17216
- for (let index2 = 0; index2 < poly.length; index2 += 1) {
17217
- const a2 = poly[index2];
17218
- const b = poly[(index2 + 1) % poly.length];
17219
- const c2 = poly[(index2 + 2) % poly.length];
17220
- const turn = cross$3([b[0] - a2[0], b[1] - a2[1]], [c2[0] - b[0], c2[1] - b[1]]);
17221
- if (Math.abs(turn) < EPS$1) continue;
17222
- const currentSign = Math.sign(turn);
17223
- if (sign !== 0 && currentSign !== sign) return false;
17224
- sign = currentSign;
17225
- }
17226
- return sign !== 0;
17227
- }
17228
- function cross$3(a2, b) {
17229
- return a2[0] * b[1] - a2[1] * b[0];
17230
- }
17231
- 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 = {}) {
17232
17382
  if (profiles.length < 2) return null;
17233
17383
  const classified = profiles.map((loops) => classifyLoops(loops));
17234
17384
  const outerCount = classified[0].outers.length;
@@ -17243,7 +17393,7 @@ function loftStitched(profiles, heights, wasm) {
17243
17393
  const holeGroups = holeCount > 0 ? matchLoopsAcrossProfiles(classified.map((c2) => c2.holes)) : [];
17244
17394
  const outerSolids = [];
17245
17395
  for (const group of outerGroups) {
17246
- const solid = stitchSingleLoopLoft(group, heights, wasm);
17396
+ const solid = stitchSingleLoopLoft(group, heights, wasm, options);
17247
17397
  if (!solid) {
17248
17398
  for (const s of outerSolids) s.delete();
17249
17399
  return null;
@@ -17260,7 +17410,7 @@ function loftStitched(profiles, heights, wasm) {
17260
17410
  if (holeGroups.length > 0) {
17261
17411
  const holeSolids = [];
17262
17412
  for (const group of holeGroups) {
17263
- const solid = stitchSingleLoopLoft(group, heights, wasm);
17413
+ const solid = stitchSingleLoopLoft(group, heights, wasm, options);
17264
17414
  if (!solid) {
17265
17415
  result.delete();
17266
17416
  for (const s of holeSolids) s.delete();
@@ -17346,68 +17496,527 @@ function signedArea$3(loop) {
17346
17496
  }
17347
17497
  return area * 0.5;
17348
17498
  }
17349
- 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) {
17350
17903
  const normalizedLoops = loops.map((loop) => {
17351
17904
  const area = signedArea$3(loop);
17352
17905
  return area < 0 ? [...loop].reverse() : loop;
17353
17906
  });
17354
- let maxPoints = 0;
17355
- for (const loop of normalizedLoops) {
17356
- maxPoints = Math.max(maxPoints, loop.length);
17357
- }
17358
- const N = Math.max(maxPoints, 24);
17359
- const angularSamples = normalizedLoops.map((loop) => resamplePolygonByAngle(loop, N));
17360
- const useAngularSamples = angularSamples.every((samples) => samples != null);
17361
- const resampled = normalizedLoops.map((loop, i) => {
17362
- const pts2d = useAngularSamples ? angularSamples[i] : resamplePolygon(loop, N);
17363
- const z2 = heights[i];
17364
- return pts2d.map(([x2, y2]) => [x2, y2, z2]);
17365
- });
17366
- const vertices = [];
17367
- const triangles = [];
17368
- for (const layer of resampled) {
17369
- for (const [x2, y2, z2] of layer) {
17370
- 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
+ }
17371
17941
  }
17942
+ fwdIdx.push(fwd);
17943
+ bwdIdx.push(bwd);
17372
17944
  }
17373
- for (let i = 0; i < resampled.length - 1; i++) {
17374
- const baseIdx = i * N;
17375
- const nextIdx = (i + 1) * N;
17945
+ const triangles = [];
17946
+ for (let r = 0; r < R - 1; r++) {
17376
17947
  for (let j = 0; j < N; j++) {
17377
17948
  const j1 = (j + 1) % N;
17378
- const v0 = baseIdx + j;
17379
- const v1 = nextIdx + j;
17380
- const v2 = nextIdx + j1;
17381
- 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];
17382
17953
  triangles.push(v0, v3, v2);
17383
17954
  triangles.push(v0, v2, v1);
17384
17955
  }
17385
17956
  }
17386
- const bottomResampled2D = resampled[0].map(([x2, y2]) => [x2, y2]);
17387
- const bottomTrisResampled = wasm.triangulate([bottomResampled2D]);
17388
- 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) {
17389
17964
  const [v0, v1, v2] = Array.isArray(tri) ? tri : [tri[0], tri[1], tri[2]];
17390
- triangles.push(v0, v2, v1);
17965
+ triangles.push(bottomBase + v0, bottomBase + v2, bottomBase + v1);
17391
17966
  }
17392
- const topResampled2D = resampled[resampled.length - 1].map(([x2, y2]) => [x2, y2]);
17393
- const topTrisResampled = wasm.triangulate([topResampled2D]);
17394
- const topStartIdx = (resampled.length - 1) * N;
17395
- 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) {
17396
17972
  const [v0, v1, v2] = Array.isArray(tri) ? tri : [tri[0], tri[1], tri[2]];
17397
- triangles.push(topStartIdx + v0, topStartIdx + v1, topStartIdx + v2);
17973
+ triangles.push(topBase + v0, topBase + v1, topBase + v2);
17398
17974
  }
17399
17975
  const mesh = new wasm.Mesh({
17400
- numProp: 3,
17401
- vertProperties: new Float32Array(vertices),
17976
+ numProp: 6,
17977
+ vertProperties: new Float32Array(vertProps),
17402
17978
  triVerts: new Uint32Array(triangles)
17403
17979
  });
17404
17980
  try {
17981
+ mesh.merge();
17405
17982
  const manifold = new wasm.Manifold(mesh);
17406
17983
  return manifold;
17407
17984
  } catch (_e2) {
17408
17985
  return null;
17409
17986
  }
17410
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
+ }
17411
18020
  let _wasm$1 = null;
17412
18021
  async function initManifoldWasm() {
17413
18022
  if (_wasm$1) return _wasm$1;
@@ -17513,7 +18122,9 @@ const _ManifoldShapeBackend = class _ManifoldShapeBackend {
17513
18122
  return this.getLiveManifold("numTri()").numTri();
17514
18123
  }
17515
18124
  getMesh() {
17516
- 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;
17517
18128
  }
17518
18129
  slice(offset) {
17519
18130
  return this.getLiveManifold("slice()").slice(offset);
@@ -17562,6 +18173,43 @@ function requireManifoldShapeBackend(backend, apiName = "requireManifoldShapeBac
17562
18173
  }
17563
18174
  throw new Error(`${apiName} currently requires a Manifold-backed runtime shape.`);
17564
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
+ }
17565
18213
  function sweepStitched(profilePolygons, pathPoints, up, wasm) {
17566
18214
  if (pathPoints.length < 2) return null;
17567
18215
  if (profilePolygons.length === 0) return null;
@@ -18259,7 +18907,7 @@ function lowerOffsetLoftCompilePlan(plan, thickness, wasm) {
18259
18907
  throw new Error("offsetSolid() collapsed the compatible-loft height span.");
18260
18908
  }
18261
18909
  const offsetPolygons = plan.profiles.map((profile) => offsetProfilePolygonsForManifold(profile, thickness, wasm));
18262
- const stitched = loftStitched(offsetPolygons, heights, wasm);
18910
+ const stitched = loftStitched(offsetPolygons, heights, wasm, { edgeLength: plan.edgeLength });
18263
18911
  if (!stitched) {
18264
18912
  throw new Error(`Offset solid requires the OCCT backend. ${OCCT_BACKEND_REQUIRED_HINT}`);
18265
18913
  }
@@ -18394,12 +19042,12 @@ function lowerShapeLoftCompilePlan(plan, wasm) {
18394
19042
  disposeWasmObject(crossSection);
18395
19043
  }
18396
19044
  });
18397
- if (inputPolygons.length >= 2) {
18398
- 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 });
18399
19047
  if (stitched) return stitched;
18400
19048
  }
18401
19049
  const input = buildLoftLevelSetInput(inputPolygons, plan.heights, { edgeLength: plan.edgeLength, boundsPadding: plan.boundsPadding });
18402
- return lowerSdfToManifold(levelSetFieldToStandardSdf3(input.sdf), input.bounds, input.edgeLength, wasm);
19050
+ return lowerSdfToManifold(levelSetFieldToStandardSdf3(input.sdf), input.bounds, input.edgeLength, wasm, plan.meshing);
18403
19051
  }
18404
19052
  function lowerShapeSweepCompilePlan(plan, wasm) {
18405
19053
  const crossSection = lowerProfileCompilePlanToCrossSection(plan.profile, wasm);
@@ -18664,7 +19312,7 @@ function lowerFromSlicesToManifold(plan, wasm) {
18664
19312
  }
18665
19313
  });
18666
19314
  const heights = sorted.map((s) => s.offset);
18667
- const stitched = polygons.length >= 2 ? loftStitched(polygons, heights, wasm) : null;
19315
+ const stitched = polygons.length >= 2 ? loftStitched(polygons, heights, wasm, { edgeLength: plan.edgeLength }) : null;
18668
19316
  if (stitched) {
18669
19317
  solid = stitched;
18670
19318
  } else {
@@ -19105,17 +19753,21 @@ function lowerSdfToManifold(evalFn, bounds, edgeLength2, wasm, meshing, evaluato
19105
19753
  if (diagnostics) diagnostics.projectionMs = nowMs() - projectionStart;
19106
19754
  const simplificationStart = nowMs();
19107
19755
  if (((meshing == null ? void 0 : meshing.simplify) ?? "safe") !== "off" && snMesh.numTris > 100) {
19108
- 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);
19109
19757
  }
19110
19758
  if (diagnostics) {
19111
19759
  diagnostics.simplificationMs = nowMs() - simplificationStart;
19112
19760
  diagnostics.trianglesAfterSimplification = triVerts.length / 3;
19113
19761
  }
19114
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";
19115
19764
  throw new Error(
19116
- `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.`
19117
19766
  );
19118
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
+ }
19119
19771
  const wasmMesh = new wasm.Mesh({
19120
19772
  numProp: 6,
19121
19773
  vertProperties: vertProps6,
@@ -19133,28 +19785,102 @@ function lowerSdfToManifold(evalFn, bounds, edgeLength2, wasm, meshing, evaluato
19133
19785
  disposeWasmObject(wasmMesh);
19134
19786
  }
19135
19787
  }
19136
- function simplifySdfMesh(triVerts, vertProperties, edgeLength2, wasm, maxTriangles) {
19788
+ function simplifySdfMesh(triVerts, vertProperties, finalVertProperties, edgeLength2, maxTriangles) {
19137
19789
  const maxError = edgeLength2 * 0.15;
19138
19790
  const inputTriangles = triVerts.length / 3;
19139
- const requestedRatio = maxTriangles && maxTriangles < inputTriangles ? maxTriangles / inputTriangles : 0.5;
19140
- 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;
19141
19794
  for (const ratio of ratios) {
19142
- let simplified = simplifyMesh(triVerts, vertProperties, ratio, maxError);
19143
- simplified = filterDegenerateTriangles(simplified);
19144
- let mesh = null;
19145
- let manifold = null;
19795
+ let simplified;
19146
19796
  try {
19147
- mesh = new wasm.Mesh({ numProp: 3, vertProperties, triVerts: simplified });
19148
- manifold = new wasm.Manifold(mesh);
19149
- return simplified;
19797
+ simplified = simplifyMesh(triVerts, vertProperties, ratio, maxError);
19150
19798
  } catch {
19151
- } finally {
19152
- disposeWasmObject(manifold);
19153
- 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
+ }
19154
19809
  }
19155
19810
  }
19811
+ if (bestValid) {
19812
+ return bestValid;
19813
+ }
19156
19814
  return triVerts;
19157
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
+ }
19158
19884
  function filterDegenerateTriangles(triVerts) {
19159
19885
  let writeIdx = 0;
19160
19886
  for (let i = 0; i < triVerts.length; i += 3) {
@@ -20368,11 +21094,12 @@ function profileMayContainInteriorLoopsForOCCT(plan) {
20368
21094
  return false;
20369
21095
  }
20370
21096
  }
21097
+ const occtLoweredCache = /* @__PURE__ */ new WeakMap();
20371
21098
  function lowerShapeCompilePlanToOCCT(plan, oc) {
20372
- const cached = plan._occtCache;
21099
+ const cached = occtLoweredCache.get(plan);
20373
21100
  if (cached) return cached;
20374
21101
  const shape = _lowerShapeCompilePlanToOCCTInner(plan, oc);
20375
- plan._occtCache = shape;
21102
+ occtLoweredCache.set(plan, shape);
20376
21103
  return shape;
20377
21104
  }
20378
21105
  function _lowerShapeCompilePlanToOCCTInner(plan, oc) {
@@ -36472,29 +37199,11 @@ function lowerExactSlicedShapeCompilePlanToTruckProfileBackend(plan, offset) {
36472
37199
  return profilePlan ? lowerProfileCompilePlanToTruckProfileBackend(profilePlan) : null;
36473
37200
  }
36474
37201
  const SHAPE_COMPILE_PLAN_CACHE_KEY_VERSION = "shape-plan-v1";
36475
- function stableJsonEncode(value, arrayMember) {
36476
- if (value === void 0 || typeof value === "function" || typeof value === "symbol") {
36477
- return arrayMember ? "null" : void 0;
36478
- }
36479
- if (value === null || typeof value === "number" || typeof value === "boolean" || typeof value === "string") {
36480
- return JSON.stringify(value);
36481
- }
36482
- if (Array.isArray(value)) {
36483
- return `[${value.map((item) => stableJsonEncode(item, true) ?? "null").join(",")}]`;
36484
- }
36485
- const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
36486
- const encodedEntries = [];
36487
- for (const [key, item] of entries) {
36488
- const encoded = stableJsonEncode(item, false);
36489
- if (encoded !== void 0) encodedEntries.push(`${JSON.stringify(key)}:${encoded}`);
36490
- }
36491
- return `{${encodedEntries.join(",")}}`;
36492
- }
36493
- function stableJsonStringify(value) {
36494
- return stableJsonEncode(value, false) ?? "null";
36495
- }
37202
+ const exactPlanHasher = createStructuralHasher({
37203
+ skipKey: (key) => key.startsWith("_") || key === "owner" || key === "queryPropagation"
37204
+ });
36496
37205
  function shapeCompilePlanCacheKey(plan) {
36497
- return `${SHAPE_COMPILE_PLAN_CACHE_KEY_VERSION}:${stableJsonStringify(plan)}`;
37206
+ return `${SHAPE_COMPILE_PLAN_CACHE_KEY_VERSION}:${exactPlanHasher(plan)}`;
36498
37207
  }
36499
37208
  function formatFaceQuery(query) {
36500
37209
  const parts = [];
@@ -39347,7 +40056,7 @@ class ShapeGroup {
39347
40056
  };
39348
40057
  return this.attachTo(parent, face, opp[face], uvMap[face](u2, v, p2));
39349
40058
  }
39350
- /** 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. */
39351
40060
  rotate(axis, angleDeg, options) {
39352
40061
  requireRotateAxis(axis, "ShapeGroup.rotate()");
39353
40062
  requireFiniteAngle(angleDeg, "ShapeGroup.rotate()");
@@ -39592,6 +40301,11 @@ class ShapeGroup {
39592
40301
  * Position this group by matching connectors to a target.
39593
40302
  * Connector names support dotted paths into named children: "ChildName.connectorName".
39594
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
+ *
39595
40309
  * Overloads:
39596
40310
  * - Single pair: `matchTo(target, selfConn, targetConn, options?)`
39597
40311
  * - Dictionary (same target): `matchTo(target, { selfConn: targetConn, ... }, options?)`
@@ -41256,10 +41970,10 @@ function rigidTransformStepsFromMatrix(m2) {
41256
41970
  return [{ kind: "workplanePlacement", matrix: Array.from(m2) }];
41257
41971
  }
41258
41972
  async function initKernel() {
41259
- const [manifoldModule] = await Promise.all([initManifoldWasm(), initMeshoptimizer(), initTruckGeometryWasm()]);
41973
+ const [manifoldModule] = await Promise.all([initManifoldWasm(), initMeshoptimizer()]);
41260
41974
  return manifoldModule;
41261
41975
  }
41262
- const DEFAULT_ACTIVE_BACKEND = "truck";
41976
+ const DEFAULT_ACTIVE_BACKEND = "manifold";
41263
41977
  let _activeBackend = DEFAULT_ACTIVE_BACKEND;
41264
41978
  let _runtimeWarn = (msg) => console.warn(msg);
41265
41979
  async function activateBackend(backend) {
@@ -41286,6 +42000,7 @@ const _shapePlacementRefs = /* @__PURE__ */ new WeakMap();
41286
42000
  const _shapeExplodeHint = /* @__PURE__ */ new WeakMap();
41287
42001
  const _shapeRuntimeBackends = /* @__PURE__ */ new WeakMap();
41288
42002
  const _shapeTopology = /* @__PURE__ */ new WeakMap();
42003
+ const _shapeLineageTokens = /* @__PURE__ */ new WeakMap();
41289
42004
  const _shapeFaceLabels = /* @__PURE__ */ new WeakMap();
41290
42005
  const _shapeReferenceNames = /* @__PURE__ */ new WeakMap();
41291
42006
  const _shapeReferenceAliases = /* @__PURE__ */ new WeakMap();
@@ -41346,6 +42061,10 @@ function copyShapeReferenceMetadata(source, target) {
41346
42061
  const aliases = cloneReferenceAliases(_shapeReferenceAliases.get(source));
41347
42062
  if (aliases && aliases.size > 0) _shapeReferenceAliases.set(target, aliases);
41348
42063
  }
42064
+ function copyShapeLineage(source, target) {
42065
+ const token = _shapeLineageTokens.get(source);
42066
+ if (token) _shapeLineageTokens.set(target, token);
42067
+ }
41349
42068
  function assertNonEmptyReferenceName(name, apiName) {
41350
42069
  const trimmed = name.trim();
41351
42070
  if (!trimmed) throw new Error(`${apiName} requires a non-empty reference name.`);
@@ -41414,50 +42133,25 @@ function setShapeRuntimeBackendInternal(shape, payload) {
41414
42133
  return shape;
41415
42134
  }
41416
42135
  function setShapeCompilePlanInternal(shape, plan) {
41417
- _shapeCompilePlans.set(shape, cloneShapeCompilePlan(plan));
42136
+ _shapeCompilePlans.set(shape, deepFreezePlanData(plan));
41418
42137
  recordShapeSourceSpanInternal(shape, plan);
41419
42138
  return shape;
41420
42139
  }
41421
- function cloneShapeSourceSpanRecords(records) {
41422
- return (records ?? []).map((record) => ({
41423
- planCacheKey: record.planCacheKey,
41424
- sourceSpan: { ...record.sourceSpan }
41425
- }));
41426
- }
41427
42140
  function upsertShapeSourceSpanRecord(shape, record) {
41428
- const records = cloneShapeSourceSpanRecords(_shapeSourceSpans.get(shape));
41429
- if (records.some((existing) => existing.planCacheKey === record.planCacheKey)) return;
41430
- records.push({
41431
- planCacheKey: record.planCacheKey,
41432
- sourceSpan: { ...record.sourceSpan }
41433
- });
41434
- _shapeSourceSpans.set(shape, records);
41435
- }
41436
- function stableTraceSourcePlanEncode(value, arrayMember) {
41437
- if (value === void 0 || typeof value === "function" || typeof value === "symbol") {
41438
- return arrayMember ? "null" : void 0;
41439
- }
41440
- if (value === null || typeof value === "number" || typeof value === "boolean" || typeof value === "string") {
41441
- return JSON.stringify(value);
42141
+ let records = _shapeSourceSpans.get(shape);
42142
+ if (!records) {
42143
+ records = /* @__PURE__ */ new Map();
42144
+ _shapeSourceSpans.set(shape, records);
41442
42145
  }
41443
- if (Array.isArray(value)) {
41444
- return `[${value.map((item) => stableTraceSourcePlanEncode(item, true) ?? "null").join(",")}]`;
41445
- }
41446
- const record = value;
41447
- if (record.kind === "queryOwner" && record.base) {
41448
- return stableTraceSourcePlanEncode(record.base, arrayMember);
41449
- }
41450
- const entries = Object.entries(record).sort(([left], [right]) => left.localeCompare(right));
41451
- const encodedEntries = [];
41452
- for (const [key, item] of entries) {
41453
- if (key.startsWith("_") || key === "owner" || key === "queryPropagation") continue;
41454
- const encoded = stableTraceSourcePlanEncode(item, false);
41455
- if (encoded !== void 0) encodedEntries.push(`${JSON.stringify(key)}:${encoded}`);
41456
- }
41457
- return `{${encodedEntries.join(",")}}`;
42146
+ if (records.has(record.planCacheKey)) return;
42147
+ records.set(record.planCacheKey, Object.freeze({ ...record.sourceSpan }));
41458
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
+ });
41459
42153
  function normalizedTraceSourcePlanCacheKey(plan) {
41460
- return `shape-plan-v1:${stableTraceSourcePlanEncode(plan, false) ?? "null"}`;
42154
+ return `shape-plan-v1:${traceSourcePlanHasher(plan)}`;
41461
42155
  }
41462
42156
  function recordShapeSourceSpanInternal(shape, plan) {
41463
42157
  if (!hasActiveRuntimeSourceResolver()) return;
@@ -41477,13 +42171,20 @@ function recordShapeSourceSpanInternal(shape, plan) {
41477
42171
  }
41478
42172
  }
41479
42173
  function copyShapeSourceSpans(source, target) {
41480
- const records = cloneShapeSourceSpanRecords(_shapeSourceSpans.get(source));
41481
- 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));
41482
42176
  }
41483
42177
  function mergeShapeSourceSpans(sources, target) {
42178
+ let records = _shapeSourceSpans.get(target);
41484
42179
  for (const source of sources) {
41485
- for (const record of _shapeSourceSpans.get(source) ?? []) {
41486
- 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);
41487
42188
  }
41488
42189
  }
41489
42190
  }
@@ -41518,7 +42219,7 @@ function getShapeRuntimeBackendInternal(shape) {
41518
42219
  function getShapeCompilePlanInternal(shape) {
41519
42220
  const stored = _shapeCompilePlans.get(shape);
41520
42221
  if (!stored) throw new Error("Shape has no compile plan — every Shape must have an explicit plan set via setShapeCompilePlanInternal()");
41521
- return cloneShapeCompilePlan(stored);
42222
+ return stored;
41522
42223
  }
41523
42224
  function getShapePlacementRefsInternal(shape) {
41524
42225
  return clonePlacementReferences(_shapePlacementRefs.get(shape) ?? createPlacementReferences());
@@ -42210,6 +42911,30 @@ function checkLabelCollisions(operation2, plans) {
42210
42911
  seen.push(...labels);
42211
42912
  }
42212
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
+ }
42213
42938
  function withCopiedDimensions(source, out) {
42214
42939
  setShapeDimensionsInternal(out, cloneDimensions(getShapeDimensionsInternal(source), true));
42215
42940
  setShapeGeometryInfoInternal(out, getShapeGeometryInfoInternal(source));
@@ -42224,6 +42949,7 @@ function withCopiedDimensions(source, out) {
42224
42949
  const sourceLabels = cloneFaceLabelMap(_shapeFaceLabels.get(source));
42225
42950
  if (sourceLabels) _shapeFaceLabels.set(out, sourceLabels);
42226
42951
  copyShapeReferenceMetadata(source, out);
42952
+ copyShapeLineage(source, out);
42227
42953
  copyShapeSourceSpans(source, out);
42228
42954
  return setShapeCompilePlanInternal(out, getShapeCompilePlanInternal(source));
42229
42955
  }
@@ -42253,6 +42979,7 @@ function withTransformedDimensions(source, out, m2) {
42253
42979
  const sourceLabelsT = cloneFaceLabelMap(_shapeFaceLabels.get(source));
42254
42980
  if (sourceLabelsT) _shapeFaceLabels.set(out, sourceLabelsT);
42255
42981
  copyShapeReferenceMetadata(source, out);
42982
+ copyShapeLineage(source, out);
42256
42983
  copyShapeSourceSpans(source, out);
42257
42984
  return setShapeCompilePlanInternal(out, getShapeCompilePlanInternal(source));
42258
42985
  }
@@ -42680,6 +43407,7 @@ class Shape {
42680
43407
  this.colorHex = color;
42681
43408
  setShapeRuntimeBackendInternal(this, payload);
42682
43409
  setShapeGeometryInfoInternal(this, createGeometryInfo(geometryInfo));
43410
+ _shapeLineageTokens.set(this, {});
42683
43411
  }
42684
43412
  /** @internal Use .color() instead. */
42685
43413
  setColor(value) {
@@ -42798,6 +43526,12 @@ class Shape {
42798
43526
  * with `union()` / `difference()` to avoid collisions. Collision detection throws a
42799
43527
  * clear error with a fix suggestion.
42800
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
+ *
42801
43535
  * For compile-covered shapes (extrude, loft, etc.) the lookup resolves via the shape's
42802
43536
  * compile plan. As a fallback, planar-faced mesh shapes (e.g. results of boolean ops)
42803
43537
  * are resolved via coplanar triangle clustering.
@@ -43214,7 +43948,7 @@ class Shape {
43214
43948
  const tbb = s.boundingBox();
43215
43949
  return this.moveTo(tbb.min[0] + localX, tbb.min[1] + localY, tbb.min[2] + localZ);
43216
43950
  }
43217
- /** 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. */
43218
43952
  rotate(axis, angleDeg, options) {
43219
43953
  validateRotateAxis(axis, "Shape.rotate()");
43220
43954
  validateRotateAngle(angleDeg, "Shape.rotate()");
@@ -43401,7 +44135,7 @@ class Shape {
43401
44135
  * Warn if a boolean operation had no geometric effect.
43402
44136
  * Compares volumes before and after; if they match within tolerance, the operation was a no-op.
43403
44137
  */
43404
- static _checkBooleanNoOp(op, base, result) {
44138
+ static _checkBooleanNoOp(op, base, result, tools = []) {
43405
44139
  try {
43406
44140
  if (op === "intersection") {
43407
44141
  if (result.isEmpty()) {
@@ -43414,8 +44148,15 @@ class Shape {
43414
44148
  const volAfter = result.volume();
43415
44149
  const tol = Math.max(volBefore * 1e-4, 1e-3);
43416
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(" ");
43417
44156
  _runtimeWarn(
43418
- `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
43419
44160
  );
43420
44161
  }
43421
44162
  }
@@ -43475,7 +44216,7 @@ class Shape {
43475
44216
  ),
43476
44217
  nextPlan
43477
44218
  );
43478
- Shape._checkBooleanNoOp("difference", this, resultShape);
44219
+ Shape._checkBooleanNoOp("difference", this, resultShape, shapes.slice(1));
43479
44220
  return resultShape;
43480
44221
  }
43481
44222
  /** Keep only the overlap with other shapes. Method form of intersection(). */
@@ -43878,6 +44619,11 @@ class Shape {
43878
44619
  /**
43879
44620
  * Position this shape by matching connectors to a target.
43880
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
+ *
43881
44627
  * Overloads:
43882
44628
  * - Single pair: `matchTo(target, selfConn, targetConn, options?)`
43883
44629
  * - Dictionary (same target): `matchTo(target, { selfConn: targetConn, ... }, options?)`
@@ -44255,15 +45001,23 @@ function computeGeometryArrays(mesh, options = {}) {
44255
45001
  normals[o + 7] = vertNormals[i2 * 3 + 1];
44256
45002
  normals[o + 8] = vertNormals[i2 * 3 + 2];
44257
45003
  } else if (numProp >= 6) {
44258
- normals[o] = vertProperties[i0 * numProp + 3];
44259
- normals[o + 1] = vertProperties[i0 * numProp + 4];
44260
- normals[o + 2] = vertProperties[i0 * numProp + 5];
44261
- normals[o + 3] = vertProperties[i1 * numProp + 3];
44262
- normals[o + 4] = vertProperties[i1 * numProp + 4];
44263
- normals[o + 5] = vertProperties[i1 * numProp + 5];
44264
- normals[o + 6] = vertProperties[i2 * numProp + 3];
44265
- normals[o + 7] = vertProperties[i2 * numProp + 4];
44266
- 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
+ }
44267
45021
  } else {
44268
45022
  normals[o] = fnx;
44269
45023
  normals[o + 1] = fny;