forgecad 0.9.15 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/dist/assets/{AdminPage-CDyGUinA.js → AdminPage-DwYHz72L.js} +1 -1
  2. package/dist/assets/{BenchmarkPage-DfPMY_-d.js → BenchmarkPage-a9_f-1US.js} +1 -1
  3. package/dist/assets/{BlogPage-kF0fkdJT.js → BlogPage-DodHpvmf.js} +1 -1
  4. package/dist/assets/{DocsPage-B954L3YN.js → DocsPage-B5LePEuj.js} +8 -858
  5. package/dist/assets/{EditorApp-CuDLxKqL.css → EditorApp-BpjZgzk0.css} +148 -0
  6. package/dist/assets/EditorApp-QXsAISLR.js +16307 -0
  7. package/dist/assets/{EmbedViewer-C77B-TrF.js → EmbedViewer-DdEHGUMU.js} +2 -2
  8. package/dist/assets/{LandingPageProofDriven-Cr6fXMDj.js → LandingPageProofDriven-yhhOodbf.js} +2 -2
  9. package/dist/assets/{LegalPage-Dzklqmmg.js → LegalPage-5RbKRGYK.js} +1 -1
  10. package/dist/assets/{PricingPage-zWXkvlwl.js → PricingPage-E3Rma7aV.js} +1 -1
  11. package/dist/assets/{SettingsPage-Bz0of4KQ.js → SettingsPage-BJZcM97j.js} +1 -1
  12. package/dist/assets/{app-D3kDkggg.js → app-DSYrDg0V.js} +1846 -352
  13. package/dist/assets/cli/{render-DSY3mMQa.js → render-ZMHR9HkV.js} +161 -70
  14. package/dist/assets/{constructionHistoryWorker-gpDo-uH2.js → constructionHistoryWorker-AwMMWSxg.js} +1104 -349
  15. package/dist/assets/{evalWorker-CU0Ke6DP.js → evalWorker-DbNs7Dkp.js} +5155 -3772
  16. package/dist/assets/{inspectWorker-COyp8XXA.js → inspectWorker-CZsCFtQT.js} +1415 -439
  17. package/dist/assets/{targets-B9sGB5nB.js → jointPose-DO6mnXn_.js} +71 -3
  18. package/dist/assets/{manifold-DNkrUWpA.js → manifold-BGlQBBH9.js} +1 -1
  19. package/dist/assets/{manifold-BRI5prcH.js → manifold-BU-tJwQh.js} +1 -1
  20. package/dist/assets/{manifold-C-3h2M7p.js → manifold-fy2MV7K1.js} +2 -2
  21. package/dist/assets/{reportWorker-CdBz5bNg.js → reportWorker-DO6hcQbh.js} +8474 -4549
  22. package/dist/assets/{scalar-sampling-budget-wJF98aY9.js → scalar-sampling-budget-o90NSNmF.js} +5347 -3906
  23. package/dist/assets/{scanProxyWorker-B-9VbLIs.js → scanProxyWorker-2GtDLk-R.js} +19 -6
  24. package/dist/assets/{javascript-1kQXfVaz.js → typescript-DBQ6RN5l.js} +874 -22
  25. package/dist/cli/render.html +1 -1
  26. package/dist/docs/index.html +3 -3
  27. package/dist/docs-raw/AI/usage.md +3 -1
  28. package/dist/docs-raw/CLI.md +65 -239
  29. package/dist/docs-raw/README.md +6 -0
  30. package/dist/docs-raw/component-model.md +17 -150
  31. package/dist/docs-raw/generated/assembly.md +159 -520
  32. package/dist/docs-raw/generated/concepts.md +245 -3491
  33. package/dist/docs-raw/generated/core.md +277 -1251
  34. package/dist/docs-raw/generated/curves.md +387 -1608
  35. package/dist/docs-raw/generated/legacy.md +162 -0
  36. package/dist/docs-raw/generated/lib.md +238 -112
  37. package/dist/docs-raw/generated/output.md +51 -76
  38. package/dist/docs-raw/generated/runtime-names.md +30 -22
  39. package/dist/docs-raw/generated/sdf.md +68 -284
  40. package/dist/docs-raw/generated/sheet-metal.md +68 -335
  41. package/dist/docs-raw/generated/sketch.md +240 -1161
  42. package/dist/docs-raw/generated/viewport.md +75 -316
  43. package/dist/docs-raw/generated/wood.md +21 -49
  44. package/dist/docs-raw/guides/coordinate-system.md +4 -42
  45. package/dist/docs-raw/guides/inspection-bundles.md +44 -442
  46. package/dist/docs-raw/guides/joint-design.md +18 -79
  47. package/dist/docs-raw/guides/positioning.md +21 -143
  48. package/dist/docs-raw/guides/scene-presentation.md +89 -0
  49. package/dist/docs-raw/skills/forgecad-3d-reconstruction.md +25 -111
  50. package/dist/docs-raw/skills/forgecad-blockout-model.md +20 -117
  51. package/dist/docs-raw/skills/forgecad-component-model.md +23 -107
  52. package/dist/docs-raw/skills/forgecad-high-level-spec.md +47 -155
  53. package/dist/docs-raw/skills/forgecad-image-replicator.md +26 -143
  54. package/dist/docs-raw/skills/forgecad-lld.md +19 -113
  55. package/dist/docs-raw/skills/forgecad-make-a-model.md +113 -532
  56. package/dist/docs-raw/skills/forgecad-model-grader.md +38 -108
  57. package/dist/docs-raw/skills/forgecad-prepare-prompt.md +24 -211
  58. package/dist/docs-raw/skills/forgecad-project.md +13 -129
  59. package/dist/docs-raw/skills/forgecad-reconstruction-benchmark.md +42 -134
  60. package/dist/docs-raw/skills/forgecad-render-inspect.md +27 -174
  61. package/dist/docs-raw/skills/forgecad-visual-spec.md +32 -112
  62. package/dist/docs-raw/skills/forgecad.md +19 -18
  63. package/dist/docs-raw/skills/index.md +2 -0
  64. package/dist/docs-raw/welcome.md +4 -2
  65. package/dist/index.html +1 -1
  66. package/dist/llms.txt +1 -2
  67. package/dist/sitemap.xml +13 -13
  68. package/dist-cli/{check-compiler-SDX5QIXI.js → check-compiler-JTVBITCR.js} +1 -1
  69. package/dist-cli/{check-query-propagation-EAYEFT77.js → check-query-propagation-3FFLSMVN.js} +1 -1
  70. package/dist-cli/{chunk-N4O47JLF.js → chunk-OAN5T4XD.js} +5722 -4287
  71. package/dist-cli/forgecad.js +2195 -656
  72. package/dist-skill/CONTEXT.md +1778 -7912
  73. package/dist-skill/SKILL.md +15 -15
  74. package/dist-skill/docs/API/core/concepts.md +27 -157
  75. package/dist-skill/docs/CLI.md +65 -239
  76. package/dist-skill/docs/generated/assembly.md +160 -493
  77. package/dist-skill/docs/generated/core.md +277 -1251
  78. package/dist-skill/docs/generated/curves.md +387 -1609
  79. package/dist-skill/docs/generated/lib.md +238 -112
  80. package/dist-skill/docs/generated/output.md +51 -76
  81. package/dist-skill/docs/generated/runtime-names.md +16 -22
  82. package/dist-skill/docs/generated/sdf.md +68 -284
  83. package/dist-skill/docs/generated/sheet-metal.md +68 -335
  84. package/dist-skill/docs/generated/sketch.md +240 -1160
  85. package/dist-skill/docs/generated/viewport.md +75 -223
  86. package/dist-skill/docs/generated/wood.md +21 -49
  87. package/dist-skill/docs/guides/coordinate-system.md +4 -42
  88. package/dist-skill/docs/guides/inspection-bundles.md +44 -442
  89. package/dist-skill/docs/guides/joint-design.md +18 -79
  90. package/dist-skill/docs/guides/positioning.md +21 -143
  91. package/dist-skill/docs/guides/scene-presentation.md +89 -0
  92. package/dist-skill/docs/guides/surface-members.md +26 -0
  93. package/dist-skill/library/forgecad-3d-reconstruction/SKILL.md +23 -111
  94. package/dist-skill/library/forgecad-blockout-model/SKILL.md +18 -117
  95. package/dist-skill/library/forgecad-component-model/SKILL.md +21 -107
  96. package/dist-skill/library/forgecad-high-level-spec/SKILL.md +45 -155
  97. package/dist-skill/library/forgecad-image-replicator/SKILL.md +24 -143
  98. package/dist-skill/library/forgecad-lld/SKILL.md +17 -113
  99. package/dist-skill/library/forgecad-make-a-model/SKILL.md +111 -532
  100. package/dist-skill/library/forgecad-model-grader/SKILL.md +36 -108
  101. package/dist-skill/library/forgecad-prepare-prompt/SKILL.md +35 -224
  102. package/dist-skill/library/forgecad-prepare-prompt/references/default-profiles.md +43 -271
  103. package/dist-skill/library/forgecad-prepare-prompt/references/master-prompt.md +30 -99
  104. package/dist-skill/library/forgecad-project/SKILL.md +13 -131
  105. package/dist-skill/library/forgecad-reconstruction-benchmark/SKILL.md +29 -123
  106. package/dist-skill/library/forgecad-render-inspect/SKILL.md +25 -174
  107. package/dist-skill/library/forgecad-visual-spec/SKILL.md +30 -111
  108. package/dist-skill/website/skills/forgecad-3d-reconstruction.md +58 -0
  109. package/dist-skill/website/skills/forgecad-blockout-model.md +49 -0
  110. package/dist-skill/website/skills/forgecad-component-model.md +53 -0
  111. package/dist-skill/website/skills/forgecad-high-level-spec.md +101 -0
  112. package/dist-skill/website/skills/forgecad-image-replicator.md +63 -0
  113. package/dist-skill/website/skills/forgecad-lld.md +41 -0
  114. package/dist-skill/website/skills/forgecad-make-a-model.md +186 -0
  115. package/dist-skill/website/skills/forgecad-model-grader.md +82 -0
  116. package/dist-skill/website/skills/forgecad-prepare-prompt.md +63 -0
  117. package/dist-skill/website/skills/forgecad-project.md +26 -0
  118. package/dist-skill/website/skills/forgecad-reconstruction-benchmark.md +60 -0
  119. package/dist-skill/website/skills/forgecad-render-inspect.md +80 -0
  120. package/dist-skill/website/skills/forgecad-visual-spec.md +71 -0
  121. package/dist-skill/website/skills/forgecad.md +122 -0
  122. package/dist-skill/website/skills/index.md +26 -0
  123. package/examples/api/comparison-imported-sphere-candidate.forge.js +1 -1
  124. package/examples/api/conformal-product-ribbon.forge.js +1 -1
  125. package/examples/api/exact-sheet-shell-assembly.forge.js +1 -1
  126. package/examples/api/extrude-options.forge.js +4 -2
  127. package/examples/api/field-loft-drive-tip.forge.js +40 -0
  128. package/examples/api/guided-loft-olive-oil-bottle.forge.js +1 -1
  129. package/examples/api/helix-basics.forge.js +2 -2
  130. package/examples/api/highlight-debug.forge.js +10 -10
  131. package/examples/api/mesh-import-slats.forge.js +1 -1
  132. package/examples/api/real-product-curves.forge.js +1 -1
  133. package/examples/api/route3d-elbow.forge.js +3 -0
  134. package/examples/api/sculpt-box-circle-booleans.forge.js +1 -1
  135. package/examples/api/sdf-shapes.forge.js +2 -5
  136. package/examples/api/sketch-rounding-strategies.forge.js +6 -6
  137. package/examples/api/surface-member-bottle-cage.forge.js +3 -3
  138. package/examples/api/surface-member-conformal-product-ribbon.forge.js +3 -3
  139. package/examples/api/surface-member-razor-inlay.forge.js +1 -1
  140. package/examples/api/variable-sweep-test.forge.js +4 -2
  141. package/examples/mechanical/airplane-propeller.forge.js +74 -39
  142. package/examples/nurbs-surface.forge.js +1 -1
  143. package/examples/products/iphone.forge.js +1 -1
  144. package/package.json +4 -1
  145. package/dist/assets/EditorApp-Beb-IZ0y.js +0 -14014
  146. package/dist/docs-raw/guides/geometry-conventions.md +0 -52
  147. package/dist/docs-raw/guides/modeling-recipes.md +0 -78
  148. package/dist-skill/docs/guides/geometry-conventions.md +0 -52
  149. package/dist-skill/docs/guides/modeling-recipes.md +0 -78
  150. package/dist-skill/library/forgecad-visual-spec/references/prompt-template.md +0 -79
  151. package/examples/api/bolted-service-cover.forge.js +0 -17
  152. package/examples/api/cable-gland-anchor.forge.js +0 -14
  153. package/examples/api/captured-cartridge-guide.forge.js +0 -14
  154. package/examples/api/captured-linear-slide.forge.js +0 -13
  155. package/examples/api/clevis-pin-joint.forge.js +0 -13
  156. package/examples/api/datum-enclosure.forge.js +0 -16
  157. package/examples/api/hose-barb-port.forge.js +0 -14
  158. package/examples/api/knuckled-hinge-assembly.forge.js +0 -15
  159. package/examples/api/living-hinge-cover.forge.js +0 -14
  160. package/examples/api/pcb-terminal-block.forge.js +0 -22
  161. package/examples/api/pinned-lever-pivot-stack.forge.js +0 -14
  162. package/examples/api/retained-shaft-knob-stack.forge.js +0 -15
  163. package/examples/api/routed-tube-clip.forge.js +0 -15
  164. package/examples/api/seated-bearing-stack.forge.js +0 -30
  165. package/examples/api/snap-latch-cover.forge.js +0 -14
  166. package/examples/api/thumb-screw-clamp.forge.js +0 -15
@@ -11609,6 +11609,8 @@ function cloneSdfNode(node) {
11609
11609
  return { kind: "sdf:circularArray", child: cloneSdfNode(node.child), count: node.count, offset: node.offset };
11610
11610
  case "sdf:shell":
11611
11611
  return { kind: "sdf:shell", child: cloneSdfNode(node.child), thickness: node.thickness };
11612
+ case "sdf:offset":
11613
+ return { kind: "sdf:offset", child: cloneSdfNode(node.child), distance: node.distance };
11612
11614
  case "sdf:displace":
11613
11615
  return {
11614
11616
  kind: "sdf:displace",
@@ -11693,7 +11695,7 @@ function cloneSdfNode(node) {
11693
11695
  }
11694
11696
  }
11695
11697
  const SHEET_METAL_EDGES = ["top", "right", "bottom", "left"];
11696
- const EPS$4 = 1e-9;
11698
+ const EPS$3 = 1e-9;
11697
11699
  function isFinitePositive$1(value) {
11698
11700
  return Number.isFinite(value) && value > 0;
11699
11701
  }
@@ -11734,7 +11736,7 @@ function edgeDisplayName(edge) {
11734
11736
  return `sheetMetal().flange("${edge}", ...)`;
11735
11737
  }
11736
11738
  function normalizeAngle(angleDeg) {
11737
- return Math.abs(angleDeg) <= EPS$4 ? 0 : angleDeg;
11739
+ return Math.abs(angleDeg) <= EPS$3 ? 0 : angleDeg;
11738
11740
  }
11739
11741
  function validateSheetMetalModel(model) {
11740
11742
  if (!isFinitePositive$1(model.panel.width) || !isFinitePositive$1(model.panel.height)) {
@@ -11746,7 +11748,7 @@ function validateSheetMetalModel(model) {
11746
11748
  if (!isFiniteNonNegative(model.bendRadius)) {
11747
11749
  return "sheetMetal() requires a finite non-negative bendRadius.";
11748
11750
  }
11749
- if (model.bendRadius <= EPS$4) {
11751
+ if (model.bendRadius <= EPS$3) {
11750
11752
  return "sheetMetal() v1 requires a positive bendRadius so the bend region stays explicit instead of collapsing into a sharp fold.";
11751
11753
  }
11752
11754
  if (model.bendAllowance.kind !== "k-factor") {
@@ -11808,7 +11810,7 @@ function deriveSheetMetalModel(model) {
11808
11810
  const trimEnd = flanges.has(adjacent.end) ? model.cornerRelief.size : 0;
11809
11811
  const fullLength = edge === "top" || edge === "bottom" ? model.panel.width : model.panel.height;
11810
11812
  const span = fullLength - trimStart - trimEnd;
11811
- if (!(span > EPS$4)) {
11813
+ if (!(span > EPS$3)) {
11812
11814
  throw new Error(
11813
11815
  `${edgeDisplayName(edge)} loses all usable span after applying the defended rectangular corner relief size ${model.cornerRelief.size}.`
11814
11816
  );
@@ -11854,7 +11856,7 @@ function transformPlacement(origin, u2, v, normal) {
11854
11856
  };
11855
11857
  }
11856
11858
  function translatePlan(plan, x2, y2, z2) {
11857
- if (Math.abs(x2) <= EPS$4 && Math.abs(y2) <= EPS$4 && Math.abs(z2) <= EPS$4) return cloneShapeCompilePlan(plan);
11859
+ if (Math.abs(x2) <= EPS$3 && Math.abs(y2) <= EPS$3 && Math.abs(z2) <= EPS$3) return cloneShapeCompilePlan(plan);
11858
11860
  return appendShapeCompileTransform(cloneShapeCompilePlan(plan), {
11859
11861
  kind: "translate",
11860
11862
  x: x2,
@@ -12347,7 +12349,7 @@ function requireNonZeroFiniteScale3(value, label) {
12347
12349
  }
12348
12350
  return scale2;
12349
12351
  }
12350
- const EPS$3 = 1e-10;
12352
+ const EPS$2 = 1e-10;
12351
12353
  function subVec3(a2, b) {
12352
12354
  return [a2[0] - b[0], a2[1] - b[1], a2[2] - b[2]];
12353
12355
  }
@@ -12373,7 +12375,7 @@ function projectRadial(v, axis) {
12373
12375
  function signedAngleAroundAxis(from, to, axis) {
12374
12376
  const fromLen = lengthVec3$1(from);
12375
12377
  const toLen = lengthVec3$1(to);
12376
- if (fromLen < EPS$3 || toLen < EPS$3) return 0;
12378
+ if (fromLen < EPS$2 || toLen < EPS$2) return 0;
12377
12379
  const fn = scaleVec3(from, 1 / fromLen);
12378
12380
  const tn = scaleVec3(to, 1 / toLen);
12379
12381
  const sin2 = dotVec3$4(axis, crossVec3$2(fn, tn));
@@ -12394,19 +12396,19 @@ function solveRotateAroundAngle(axis, pivot, movingPoint, targetPoint, options =
12394
12396
  const targetDecomp = projectRadial(target, unitAxis);
12395
12397
  const movingRadialLen = lengthVec3$1(movingDecomp.radial);
12396
12398
  const targetRadialLen = lengthVec3$1(targetDecomp.radial);
12397
- if (movingRadialLen < EPS$3) {
12398
- if (mode === "line" && targetRadialLen >= EPS$3) {
12399
+ if (movingRadialLen < EPS$2) {
12400
+ if (mode === "line" && targetRadialLen >= EPS$2) {
12399
12401
  throw new Error("rotateAroundTo(...): moving point lies on the rotation axis, so line alignment is impossible");
12400
12402
  }
12401
12403
  return 0;
12402
12404
  }
12403
12405
  if (mode === "plane") {
12404
- if (targetRadialLen < EPS$3) {
12406
+ if (targetRadialLen < EPS$2) {
12405
12407
  throw new Error("rotateAroundTo(...): target point lies on the rotation axis, so the target plane is undefined");
12406
12408
  }
12407
12409
  return signedAngleAroundAxis(movingDecomp.radial, targetDecomp.radial, unitAxis);
12408
12410
  }
12409
- if (targetRadialLen < EPS$3) {
12411
+ if (targetRadialLen < EPS$2) {
12410
12412
  throw new Error("rotateAroundTo(...): target line lies on the rotation axis, but the moving point does not");
12411
12413
  }
12412
12414
  const axialTol = 1e-8 * Math.max(1, Math.abs(movingDecomp.axial), Math.abs(targetDecomp.axial));
@@ -12443,7 +12445,7 @@ function multiplyMat4(a2, b) {
12443
12445
  }
12444
12446
  function normalizeVec3$3(v) {
12445
12447
  const len = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
12446
- if (len < EPS$3) throw new Error("Axis must be non-zero");
12448
+ if (len < EPS$2) throw new Error("Axis must be non-zero");
12447
12449
  return [v[0] / len, v[1] / len, v[2] / len];
12448
12450
  }
12449
12451
  function transformPoint$1(m2, p2, w2) {
@@ -12473,7 +12475,7 @@ function invertMat4(m2) {
12473
12475
  const b10 = a21 * a33 - a23 * a31;
12474
12476
  const b11 = a22 * a33 - a23 * a32;
12475
12477
  const det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
12476
- if (Math.abs(det) < EPS$3) throw new Error("Transform matrix is not invertible");
12478
+ if (Math.abs(det) < EPS$2) throw new Error("Transform matrix is not invertible");
12477
12479
  const invDet = 1 / det;
12478
12480
  out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * invDet;
12479
12481
  out[1] = (a02 * b10 - a01 * b11 - a03 * b09) * invDet;
@@ -12506,6 +12508,32 @@ class Transform {
12506
12508
  static from(input) {
12507
12509
  return input instanceof Transform ? input : new Transform(requireFiniteMat4(input, "Transform.from() matrix"));
12508
12510
  }
12511
+ /**
12512
+ * Compose transforms in chain order: `Transform.compose(a, b, c)` applies
12513
+ * `a`, then `b`, then `c` — the same left-to-right order as
12514
+ * `Transform.from(a).mul(b).mul(c)`.
12515
+ *
12516
+ * Prefer this over manual `.mul()` chains when composing 3+ transforms
12517
+ * (e.g. kinematics: `local -> childBase -> jointMotion -> jointFrame ->
12518
+ * parentWorld`); the variadic form makes the application order explicit and
12519
+ * prevents order mistakes.
12520
+ *
12521
+ * **Example**
12522
+ *
12523
+ * ```ts
12524
+ * const world = Transform.compose(childBase, jointMotion, jointFrame, parentWorld);
12525
+ * ```
12526
+ *
12527
+ * @param steps Transforms (or raw 4x4 matrices) applied left to right.
12528
+ * @returns The composed transform. With no arguments, the identity.
12529
+ */
12530
+ static compose(...steps) {
12531
+ let acc = Transform.identity();
12532
+ for (const step of steps) {
12533
+ acc = acc.mul(step);
12534
+ }
12535
+ return acc;
12536
+ }
12509
12537
  /** Create a translation transform. */
12510
12538
  static translation(x2, y2, z2) {
12511
12539
  return new Transform([
@@ -12589,6 +12617,7 @@ class Transform {
12589
12617
  return this.rotateAxis([0, 0, 1], angleDeg, pivot);
12590
12618
  }
12591
12619
  /** Scale after the current transform. */
12620
+ // biome-ignore lint/suspicious/useAdjacentOverloadSignatures: Static Transform.scale() and chainable instance scale() intentionally share the CAD API name.
12592
12621
  scale(v) {
12593
12622
  return this.mul(Transform.scale(v));
12594
12623
  }
@@ -13340,6 +13369,8 @@ function cloneShapeCompilePlan(plan) {
13340
13369
  heights: plan.heights.map((height) => height),
13341
13370
  edgeLength: plan.edgeLength,
13342
13371
  boundsPadding: plan.boundsPadding,
13372
+ ...plan.forceField ? { forceField: true } : {},
13373
+ ...plan.meshing ? { meshing: cloneSdfCompileMeshingSettings(plan.meshing) } : {},
13343
13374
  edgeLabels: plan.edgeLabels ? { ...plan.edgeLabels } : void 0,
13344
13375
  capLabels: plan.capLabels ? { ...plan.capLabels } : void 0
13345
13376
  };
@@ -13607,7 +13638,6 @@ function cloneShapeCompilePlan(plan) {
13607
13638
  default:
13608
13639
  assertExhaustive(plan);
13609
13640
  }
13610
- if (plan._occtCache) result._occtCache = plan._occtCache;
13611
13641
  return result;
13612
13642
  }
13613
13643
  function appendProfileCompileTransform(plan, step) {
@@ -13620,22 +13650,31 @@ function appendShapeCompileTransform(plan, step) {
13620
13650
  if (plan.kind === "transform") {
13621
13651
  return {
13622
13652
  kind: "transform",
13623
- base: cloneShapeCompilePlan(plan.base),
13624
- steps: [...plan.steps.map(cloneShapeTransform), cloneShapeTransform(step)]
13653
+ base: plan.base,
13654
+ steps: [...plan.steps, cloneShapeTransform(step)]
13625
13655
  };
13626
13656
  }
13627
13657
  return {
13628
13658
  kind: "transform",
13629
- base: cloneShapeCompilePlan(plan),
13659
+ base: plan,
13630
13660
  steps: [cloneShapeTransform(step)]
13631
13661
  };
13632
13662
  }
13633
13663
  function appendShapeCompileTransforms(plan, steps) {
13634
- let out = cloneShapeCompilePlan(plan);
13635
- for (const step of steps) {
13636
- out = appendShapeCompileTransform(out, step);
13664
+ if (!plan) return null;
13665
+ if (steps.length === 0) return plan;
13666
+ if (plan.kind === "transform") {
13667
+ return {
13668
+ kind: "transform",
13669
+ base: plan.base,
13670
+ steps: [...plan.steps, ...steps.map(cloneShapeTransform)]
13671
+ };
13637
13672
  }
13638
- return out;
13673
+ return {
13674
+ kind: "transform",
13675
+ base: plan,
13676
+ steps: steps.map(cloneShapeTransform)
13677
+ };
13639
13678
  }
13640
13679
  function wrapShapeCompilePlanWithQueryOwner(plan, owner) {
13641
13680
  if (!plan) return null;
@@ -13709,6 +13748,8 @@ function buildLoftShapeCompilePlan(profiles, heights, options) {
13709
13748
  heights: heights.map((height) => canonicalNumber(height)),
13710
13749
  edgeLength: canonicalNumber(options.edgeLength),
13711
13750
  boundsPadding: canonicalNumber(options.boundsPadding),
13751
+ ...options.forceField ? { forceField: true } : {},
13752
+ ...options.meshing ? { meshing: cloneSdfCompileMeshingSettings(options.meshing) } : {},
13712
13753
  edgeLabels: options.edgeLabels ? { ...options.edgeLabels } : void 0
13713
13754
  };
13714
13755
  }
@@ -13810,14 +13851,14 @@ function sub$2(a2, b) {
13810
13851
  function dot$3(a2, b) {
13811
13852
  return a2[0] * b[0] + a2[1] * b[1] + a2[2] * b[2];
13812
13853
  }
13813
- function cross$4(a2, b) {
13854
+ function cross$3(a2, b) {
13814
13855
  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]];
13815
13856
  }
13816
13857
  function rotateAroundAxis(v, axis, angleRad) {
13817
13858
  const c2 = Math.cos(angleRad);
13818
13859
  const s = Math.sin(angleRad);
13819
13860
  const term1 = scale$2(v, c2);
13820
- const term2 = scale$2(cross$4(axis, v), s);
13861
+ const term2 = scale$2(cross$3(axis, v), s);
13821
13862
  const term3 = scale$2(axis, dot$3(axis, v) * (1 - c2));
13822
13863
  return add$2(add$2(term1, term2), term3);
13823
13864
  }
@@ -14093,13 +14134,13 @@ function sweepPathToPolylineAdaptive(path, baseSamples = 48) {
14093
14134
  pts.push(evalPathAt(path, 1));
14094
14135
  return pts;
14095
14136
  }
14096
- const EPS$2 = 1e-8;
14137
+ const EPS$1 = 1e-8;
14097
14138
  function midpoint$1(start, end) {
14098
14139
  return [(start[0] + end[0]) * 0.5, (start[1] + end[1]) * 0.5, (start[2] + end[2]) * 0.5];
14099
14140
  }
14100
14141
  function normalize$3(v) {
14101
14142
  const len = Math.hypot(v[0], v[1], v[2]);
14102
- if (len <= EPS$2) throw new Error("Edge feature selection requires a non-zero direction vector");
14143
+ if (len <= EPS$1) throw new Error("Edge feature selection requires a non-zero direction vector");
14103
14144
  return [v[0] / len, v[1] / len, v[2] / len];
14104
14145
  }
14105
14146
  function subtract(a2, b) {
@@ -14178,7 +14219,7 @@ function rigidTransformForEdgeStep(step) {
14178
14219
  case "mirror": {
14179
14220
  const [nx0, ny0, nz0] = [step.normalX, step.normalY, step.normalZ];
14180
14221
  const len = Math.hypot(nx0, ny0, nz0);
14181
- if (len <= EPS$2) return Transform.identity();
14222
+ if (len <= EPS$1) return Transform.identity();
14182
14223
  const nx = nx0 / len;
14183
14224
  const ny = ny0 / len;
14184
14225
  const nz = nz0 / len;
@@ -14475,7 +14516,7 @@ function isRectangleProfile(points) {
14475
14516
  return [next[0] - point[0], next[1] - point[1]];
14476
14517
  });
14477
14518
  const lengths = vectors.map(([x2, y2]) => Math.hypot(x2, y2));
14478
- if (lengths.some((length4) => length4 <= EPS$2)) return false;
14519
+ if (lengths.some((length4) => length4 <= EPS$1)) return false;
14479
14520
  const dot01 = vectors[0][0] * vectors[1][0] + vectors[0][1] * vectors[1][1];
14480
14521
  const dot12 = vectors[1][0] * vectors[2][0] + vectors[1][1] * vectors[2][1];
14481
14522
  const dot23 = vectors[2][0] * vectors[3][0] + vectors[2][1] * vectors[3][1];
@@ -16414,7 +16455,9 @@ function lowerLoftShellToConcretePlan(plan, thickness, openFaces) {
16414
16455
  profiles: innerProfiles,
16415
16456
  heights: innerHeights,
16416
16457
  edgeLength: plan.edgeLength,
16417
- boundsPadding: plan.boundsPadding
16458
+ boundsPadding: plan.boundsPadding,
16459
+ ...plan.forceField ? { forceField: true } : {},
16460
+ ...plan.meshing ? { meshing: { ...plan.meshing } } : {}
16418
16461
  };
16419
16462
  return { ok: true, plan: buildBooleanShapeCompilePlan("difference", [plan, inner]) };
16420
16463
  }
@@ -16552,6 +16595,197 @@ function lowerShellShapeCompilePlanToConcretePlan(plan) {
16552
16595
  }
16553
16596
  return lowerBaseShellPlanToConcretePlan(plan.base, plan.thickness, normalizeShellOpenFaces(plan.openFaces));
16554
16597
  }
16598
+ function cyrb53(str, seed) {
16599
+ let h1 = 3735928559 ^ seed;
16600
+ let h2 = 1103547991 ^ seed;
16601
+ for (let i = 0; i < str.length; i++) {
16602
+ const ch = str.charCodeAt(i);
16603
+ h1 = Math.imul(h1 ^ ch, 2654435761);
16604
+ h2 = Math.imul(h2 ^ ch, 1597334677);
16605
+ }
16606
+ h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507);
16607
+ h1 ^= Math.imul(h2 ^ h2 >>> 13, 3266489909);
16608
+ h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507);
16609
+ h2 ^= Math.imul(h1 ^ h1 >>> 13, 3266489909);
16610
+ return 4294967296 * (2097151 & h2) + (h1 >>> 0);
16611
+ }
16612
+ function hash128(str) {
16613
+ return cyrb53(str, 2654435769).toString(36) + cyrb53(str, 2246822507).toString(36);
16614
+ }
16615
+ function isPlainValue(value) {
16616
+ return value === null || typeof value === "number" || typeof value === "boolean" || typeof value === "string";
16617
+ }
16618
+ function createStructuralHasher(normalize2) {
16619
+ const memo = /* @__PURE__ */ new WeakMap();
16620
+ const skipKey = normalize2 == null ? void 0 : normalize2.skipKey;
16621
+ const unwrap = normalize2 == null ? void 0 : normalize2.unwrap;
16622
+ const entriesSource = normalize2 == null ? void 0 : normalize2.entriesSource;
16623
+ return function hashValue(root) {
16624
+ if (isPlainValue(root)) return JSON.stringify(root);
16625
+ if (root === void 0 || typeof root === "function" || typeof root === "symbol") return "null";
16626
+ const inProgress = /* @__PURE__ */ new Set();
16627
+ const stack = [];
16628
+ const attach = (frame, key, token) => {
16629
+ if (frame.isArray) frame.parts.push(token);
16630
+ else frame.parts.push(`${JSON.stringify(key)}:${token}`);
16631
+ };
16632
+ const open = (objIn, key) => {
16633
+ const aliases = [];
16634
+ let current = objIn;
16635
+ while (current !== null && typeof current === "object" && !Array.isArray(current) && unwrap) {
16636
+ const cached2 = memo.get(current);
16637
+ if (cached2 !== void 0) {
16638
+ for (const alias of aliases) memo.set(alias, cached2);
16639
+ return cached2;
16640
+ }
16641
+ const replaced = unwrap(current);
16642
+ if (replaced === void 0 || replaced === current) break;
16643
+ aliases.push(current);
16644
+ if (aliases.includes(replaced)) {
16645
+ throw new Error("planHash: cycle detected through unwrap chain");
16646
+ }
16647
+ current = replaced;
16648
+ }
16649
+ if (isPlainValue(current)) {
16650
+ const token = JSON.stringify(current);
16651
+ for (const alias of aliases) memo.set(alias, token);
16652
+ return token;
16653
+ }
16654
+ if (current === void 0 || typeof current === "function" || typeof current === "symbol") {
16655
+ for (const alias of aliases) memo.set(alias, "null");
16656
+ return "null";
16657
+ }
16658
+ const obj = current;
16659
+ const cached = memo.get(obj);
16660
+ if (cached !== void 0) {
16661
+ for (const alias of aliases) memo.set(alias, cached);
16662
+ return cached;
16663
+ }
16664
+ if (inProgress.has(obj)) {
16665
+ throw new Error(`planHash: cycle detected in plan data (kind=${String(obj.kind)})`);
16666
+ }
16667
+ inProgress.add(obj);
16668
+ aliases.push(obj);
16669
+ let entriesNode = obj;
16670
+ if (!Array.isArray(obj) && entriesSource) {
16671
+ const substitute = entriesSource(obj);
16672
+ if (substitute) entriesNode = substitute;
16673
+ }
16674
+ let pending;
16675
+ if (Array.isArray(entriesNode)) {
16676
+ pending = entriesNode.map((value) => ({ value })).reverse();
16677
+ } else {
16678
+ const entries = Object.entries(entriesNode).sort(([left], [right]) => left.localeCompare(right));
16679
+ pending = [];
16680
+ for (let i = entries.length - 1; i >= 0; i--) {
16681
+ const [entryKey, value] = entries[i];
16682
+ if (skipKey == null ? void 0 : skipKey(entryKey)) continue;
16683
+ pending.push({ value, key: entryKey });
16684
+ }
16685
+ }
16686
+ stack.push({ pending, parts: [], isArray: Array.isArray(entriesNode), aliases, key });
16687
+ return null;
16688
+ };
16689
+ const rootToken = open(root, void 0);
16690
+ if (rootToken !== null) return rootToken;
16691
+ let result = "null";
16692
+ while (stack.length > 0) {
16693
+ const frame = stack[stack.length - 1];
16694
+ const next = frame.pending.pop();
16695
+ if (next) {
16696
+ if (isPlainValue(next.value)) {
16697
+ attach(frame, next.key, JSON.stringify(next.value));
16698
+ continue;
16699
+ }
16700
+ if (next.value === void 0 || typeof next.value === "function" || typeof next.value === "symbol") {
16701
+ if (frame.isArray) frame.parts.push("null");
16702
+ continue;
16703
+ }
16704
+ const token2 = open(next.value, next.key);
16705
+ if (token2 !== null) attach(frame, next.key, token2);
16706
+ continue;
16707
+ }
16708
+ const canonical = frame.isArray ? `[${frame.parts.join(",")}]` : `{${frame.parts.join(",")}}`;
16709
+ const token = "#" + hash128(canonical);
16710
+ for (const alias of frame.aliases) {
16711
+ memo.set(alias, token);
16712
+ inProgress.delete(alias);
16713
+ }
16714
+ stack.pop();
16715
+ if (stack.length === 0) result = token;
16716
+ else attach(stack[stack.length - 1], frame.key, token);
16717
+ }
16718
+ return result;
16719
+ };
16720
+ }
16721
+ const PLAN_BOOKKEEPING_KEY = (key) => key.startsWith("_") || key === "owner" || key === "queryPropagation";
16722
+ const encodedLengthMemo = /* @__PURE__ */ new WeakMap();
16723
+ function estimatePlanEncodedLength(value) {
16724
+ if (value === void 0 || typeof value === "function" || typeof value === "symbol") return 4;
16725
+ if (value === null || typeof value === "boolean") return 5;
16726
+ if (typeof value === "number") return 8;
16727
+ if (typeof value === "string") return value.length + 2;
16728
+ const root = value;
16729
+ const known = encodedLengthMemo.get(root);
16730
+ if (known !== void 0) return known;
16731
+ const childValues = (node) => {
16732
+ if (Array.isArray(node)) return node;
16733
+ const values = [];
16734
+ for (const [key, item] of Object.entries(node)) {
16735
+ if (PLAN_BOOKKEEPING_KEY(key)) continue;
16736
+ values.push(item);
16737
+ }
16738
+ return values;
16739
+ };
16740
+ const order = [];
16741
+ const seen = /* @__PURE__ */ new Set();
16742
+ const discover = [root];
16743
+ while (discover.length > 0) {
16744
+ const node = discover.pop();
16745
+ if (seen.has(node) || encodedLengthMemo.has(node)) continue;
16746
+ seen.add(node);
16747
+ order.push(node);
16748
+ for (const child of childValues(node)) {
16749
+ if (child !== null && typeof child === "object") discover.push(child);
16750
+ }
16751
+ }
16752
+ for (let i = order.length - 1; i >= 0; i--) {
16753
+ const node = order[i];
16754
+ let total = 2;
16755
+ if (Array.isArray(node)) {
16756
+ for (const item of node) {
16757
+ total += 1 + (item !== null && typeof item === "object" ? encodedLengthMemo.get(item) : estimatePlanEncodedLength(item));
16758
+ }
16759
+ } else {
16760
+ for (const [key, item] of Object.entries(node)) {
16761
+ if (PLAN_BOOKKEEPING_KEY(key)) continue;
16762
+ total += key.length + 4 + (item !== null && typeof item === "object" ? encodedLengthMemo.get(item) : estimatePlanEncodedLength(item));
16763
+ }
16764
+ }
16765
+ encodedLengthMemo.set(node, total);
16766
+ }
16767
+ return encodedLengthMemo.get(root);
16768
+ }
16769
+ function deepFreezePlanData(value) {
16770
+ if (value === null || typeof value !== "object") return value;
16771
+ const stack = [value];
16772
+ while (stack.length > 0) {
16773
+ const node = stack.pop();
16774
+ if (Object.isFrozen(node)) continue;
16775
+ Object.freeze(node);
16776
+ if (Array.isArray(node)) {
16777
+ for (const item of node) {
16778
+ if (item !== null && typeof item === "object") stack.push(item);
16779
+ }
16780
+ continue;
16781
+ }
16782
+ for (const [key, item] of Object.entries(node)) {
16783
+ if (PLAN_BOOKKEEPING_KEY(key)) continue;
16784
+ if (item !== null && typeof item === "object") stack.push(item);
16785
+ }
16786
+ }
16787
+ return value;
16788
+ }
16555
16789
  const SHAPE_BACKEND_MARKER = Symbol.for("forgecad.shapeBackend");
16556
16790
  function isShapeBackend(value) {
16557
16791
  return Boolean(value && typeof value === "object" && value[SHAPE_BACKEND_MARKER] === true);
@@ -16605,60 +16839,26 @@ function recordEvent(event) {
16605
16839
  runEvents.push(next);
16606
16840
  if (runEvents.length > MAX_RECORDED_EVENTS) runEvents = runEvents.slice(-MAX_RECORDED_EVENTS);
16607
16841
  }
16608
- function stableGeometryEncode(value, arrayMember) {
16609
- if (value === void 0 || typeof value === "function" || typeof value === "symbol") {
16610
- return arrayMember ? "null" : void 0;
16611
- }
16612
- if (value === null || typeof value === "number" || typeof value === "boolean" || typeof value === "string") {
16613
- return JSON.stringify(value);
16614
- }
16615
- if (Array.isArray(value)) {
16616
- return `[${value.map((item) => stableGeometryEncode(item, true) ?? "null").join(",")}]`;
16617
- }
16618
- const record = value;
16619
- if (record.kind === "queryOwner" && record.base) {
16620
- return stableGeometryEncode(record.base, arrayMember);
16621
- }
16622
- const entries = Object.entries(record).sort(([left], [right]) => left.localeCompare(right));
16623
- const encodedEntries = [];
16624
- for (const [key, item] of entries) {
16625
- if (key.startsWith("_") || key === "owner" || key === "queryPropagation") continue;
16626
- const encoded = stableGeometryEncode(item, false);
16627
- if (encoded !== void 0) encodedEntries.push(`${JSON.stringify(key)}:${encoded}`);
16628
- }
16629
- return `{${encodedEntries.join(",")}}`;
16630
- }
16631
- function stableCacheOpportunityEncode(value, arrayMember) {
16632
- if (value === void 0 || typeof value === "function" || typeof value === "symbol") {
16633
- return arrayMember ? "null" : void 0;
16634
- }
16635
- if (value === null || typeof value === "number" || typeof value === "boolean" || typeof value === "string") {
16636
- return JSON.stringify(value);
16637
- }
16638
- if (Array.isArray(value)) {
16639
- return `[${value.map((item) => stableCacheOpportunityEncode(item, true) ?? "null").join(",")}]`;
16640
- }
16641
- const record = value;
16642
- if (record.kind === "queryOwner" && record.base) {
16643
- return stableCacheOpportunityEncode(record.base, arrayMember);
16644
- }
16645
- let encodedRecord = record;
16646
- if (record.kind === "transform" && record.base) {
16647
- const retainedSteps = Array.isArray(record.steps) ? record.steps.filter((step) => step.kind === "scale") : [];
16648
- if (retainedSteps.length === 0) return stableCacheOpportunityEncode(record.base, arrayMember);
16649
- encodedRecord = { kind: "transform", base: record.base, steps: retainedSteps };
16650
- }
16651
- const entries = Object.entries(encodedRecord).sort(([left], [right]) => left.localeCompare(right));
16652
- const encodedEntries = [];
16653
- for (const [key, item] of entries) {
16654
- if (key.startsWith("_") || key === "owner" || key === "queryPropagation") continue;
16655
- const encoded = stableCacheOpportunityEncode(item, false);
16656
- if (encoded !== void 0) encodedEntries.push(`${JSON.stringify(key)}:${encoded}`);
16842
+ const skipBookkeepingKey = (key) => key.startsWith("_") || key === "owner" || key === "queryPropagation";
16843
+ const geometryPlanHasher = createStructuralHasher({
16844
+ skipKey: skipBookkeepingKey,
16845
+ unwrap: (record) => record.kind === "queryOwner" && record.base ? record.base : void 0
16846
+ });
16847
+ const scaleOnlySteps = (record) => Array.isArray(record.steps) ? record.steps.filter((step) => step.kind === "scale") : [];
16848
+ createStructuralHasher({
16849
+ skipKey: skipBookkeepingKey,
16850
+ unwrap: (record) => {
16851
+ if (record.kind === "queryOwner" && record.base) return record.base;
16852
+ if (record.kind === "transform" && record.base && scaleOnlySteps(record).length === 0) return record.base;
16853
+ return void 0;
16854
+ },
16855
+ entriesSource: (record) => {
16856
+ if (record.kind !== "transform" || !record.base) return void 0;
16857
+ return { kind: "transform", base: record.base, steps: scaleOnlySteps(record) };
16657
16858
  }
16658
- return `{${encodedEntries.join(",")}}`;
16659
- }
16859
+ });
16660
16860
  function shapeGeometryCacheKey(plan) {
16661
- return `${SHAPE_GEOMETRY_CACHE_KEY_VERSION}:${stableGeometryEncode(plan, false) ?? "null"}`;
16861
+ return `${SHAPE_GEOMETRY_CACHE_KEY_VERSION}:${geometryPlanHasher(plan)}`;
16662
16862
  }
16663
16863
  function planComplexityScore(value) {
16664
16864
  if (!value || typeof value !== "object") return 0;
@@ -16717,8 +16917,7 @@ function planComplexityScore(value) {
16717
16917
  }
16718
16918
  }
16719
16919
  function estimateCacheRetainedMb(plan) {
16720
- var _a3;
16721
- const encodedLength = ((_a3 = stableCacheOpportunityEncode(plan, false)) == null ? void 0 : _a3.length) ?? 0;
16920
+ const encodedLength = estimatePlanEncodedLength(plan);
16722
16921
  const serializedComplexityMb = encodedLength / 24e3;
16723
16922
  return round2(0.08 + planComplexityScore(plan) * 0.09 + serializedComplexityMb);
16724
16923
  }
@@ -16736,26 +16935,62 @@ function splitCacheablePlacement(plan) {
16736
16935
  }
16737
16936
  return { basePlan: plan, placementSteps: [] };
16738
16937
  }
16938
+ const uncacheableReasonMemo = /* @__PURE__ */ new WeakMap();
16739
16939
  function findUncacheableReason(value) {
16740
16940
  if (value === void 0 || value === null) return null;
16741
16941
  if (typeof value === "function" || typeof value === "symbol") return "plan contains runtime-only values";
16742
16942
  if (typeof value !== "object") return null;
16743
- if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) return "plan contains binary file data";
16744
- if (Array.isArray(value)) {
16745
- for (const item of value) {
16746
- const reason = findUncacheableReason(item);
16747
- if (reason) return reason;
16943
+ const root = value;
16944
+ const known = uncacheableReasonMemo.get(root);
16945
+ if (known !== void 0) return known;
16946
+ const order = [];
16947
+ const seen = /* @__PURE__ */ new Set();
16948
+ const discover = [root];
16949
+ let reason = null;
16950
+ while (discover.length > 0 && reason === null) {
16951
+ const node = discover.pop();
16952
+ if (seen.has(node)) continue;
16953
+ const cached = uncacheableReasonMemo.get(node);
16954
+ if (cached !== void 0) {
16955
+ if (cached !== null) reason = cached;
16956
+ continue;
16957
+ }
16958
+ seen.add(node);
16959
+ if (ArrayBuffer.isView(node) || node instanceof ArrayBuffer) {
16960
+ reason = "plan contains binary file data";
16961
+ break;
16962
+ }
16963
+ order.push(node);
16964
+ if (Array.isArray(node)) {
16965
+ for (const item of node) {
16966
+ if (typeof item === "function" || typeof item === "symbol") {
16967
+ reason = "plan contains runtime-only values";
16968
+ break;
16969
+ }
16970
+ if (item !== null && typeof item === "object") discover.push(item);
16971
+ }
16972
+ continue;
16973
+ }
16974
+ const record = node;
16975
+ if (record.kind === "importedMesh" || record.kind === "importedStep") {
16976
+ reason = "plan depends on imported file contents";
16977
+ break;
16978
+ }
16979
+ for (const [key, item] of Object.entries(record)) {
16980
+ if (skipBookkeepingKey(key)) continue;
16981
+ if (typeof item === "function" || typeof item === "symbol") {
16982
+ reason = "plan contains runtime-only values";
16983
+ break;
16984
+ }
16985
+ if (item !== null && typeof item === "object") discover.push(item);
16748
16986
  }
16749
- return null;
16750
16987
  }
16751
- const record = value;
16752
- if (record.kind === "importedMesh" || record.kind === "importedStep") return "plan depends on imported file contents";
16753
- for (const [key, item] of Object.entries(record)) {
16754
- if (key.startsWith("_")) continue;
16755
- const reason = findUncacheableReason(item);
16756
- if (reason) return reason;
16988
+ if (reason === null) {
16989
+ for (const node of order) uncacheableReasonMemo.set(node, null);
16990
+ } else {
16991
+ uncacheableReasonMemo.set(root, reason);
16757
16992
  }
16758
- return null;
16993
+ return reason;
16759
16994
  }
16760
16995
  function applyPlacementStep(backend, step) {
16761
16996
  switch (step.kind) {
@@ -17432,8 +17667,9 @@ function analyzeNodeUV(node, toLocal) {
17432
17667
  if (result.majorRadius !== void 0) result.majorRadius *= node.factor;
17433
17668
  return result;
17434
17669
  }
17435
- // ── Shell — UV comes from the inner shape ──
17670
+ // ── Shell / offset — UV comes from the inner shape ──
17436
17671
  case "sdf:shell":
17672
+ case "sdf:offset":
17437
17673
  return analyzeNodeUV(node.child, toLocal);
17438
17674
  // ── CSG — take UV from the first (primary) child ──
17439
17675
  case "sdf:union":
@@ -17969,6 +18205,11 @@ function compileSdfNode3(node) {
17969
18205
  const t = node.thickness * 0.5;
17970
18206
  return (x2, y2, z2) => abs(fn(x2, y2, z2)) - t;
17971
18207
  }
18208
+ case "sdf:offset": {
18209
+ const fn = compileSdfNode3(node.child);
18210
+ const d2 = node.distance;
18211
+ return (x2, y2, z2) => fn(x2, y2, z2) - d2;
18212
+ }
17972
18213
  case "sdf:displace": {
17973
18214
  const fn = compileSdfNode3(node.child);
17974
18215
  const constEntries = Object.entries(node.constants ?? {});
@@ -18462,6 +18703,10 @@ function emitSdfProgramNode(b, node, x2, y2, z2) {
18462
18703
  const child = emitSdfProgramNode(b, node.child, x2, y2, z2);
18463
18704
  return b.sub(b.abs(child), b.constant(node.thickness * 0.5));
18464
18705
  }
18706
+ case "sdf:offset": {
18707
+ const child = emitSdfProgramNode(b, node.child, x2, y2, z2);
18708
+ return b.sub(child, b.constant(node.distance));
18709
+ }
18465
18710
  case "sdf:onion": {
18466
18711
  let d2 = emitSdfProgramNode(b, node.child, x2, y2, z2);
18467
18712
  for (let i = 0; i < node.layers; i++) d2 = b.sub(b.abs(d2), b.constant(node.thickness));
@@ -18610,6 +18855,7 @@ function getUnsupportedSdfProgramReason(node) {
18610
18855
  case "sdf:bend":
18611
18856
  case "sdf:repeat":
18612
18857
  case "sdf:shell":
18858
+ case "sdf:offset":
18613
18859
  case "sdf:onion":
18614
18860
  return getUnsupportedSdfProgramReason(node.child);
18615
18861
  default:
@@ -18813,7 +19059,19 @@ function simplifyMesh(triVerts, vertProperties, targetRatio, maxError) {
18813
19059
  if (!_simplifier) {
18814
19060
  throw new Error("meshoptimizer not initialized — call initMeshoptimizer() first");
18815
19061
  }
18816
- const targetIndexCount = Math.max(3, Math.floor(triVerts.length * targetRatio));
19062
+ if (triVerts.length === 0 || triVerts.length % 3 !== 0) {
19063
+ throw new Error("Mesh simplification requires triangle indices in groups of 3");
19064
+ }
19065
+ if (!Number.isFinite(targetRatio) || targetRatio <= 0) {
19066
+ throw new Error("Mesh simplification targetRatio must be a positive finite number");
19067
+ }
19068
+ if (!Number.isFinite(maxError) || maxError < 0) {
19069
+ throw new Error("Mesh simplification maxError must be a non-negative finite number");
19070
+ }
19071
+ const inputTriangleCount = triVerts.length / 3;
19072
+ const targetTriangleCount = Math.max(1, Math.min(inputTriangleCount, Math.floor(inputTriangleCount * targetRatio)));
19073
+ if (targetTriangleCount >= inputTriangleCount) return triVerts;
19074
+ const targetIndexCount = targetTriangleCount * 3;
18817
19075
  const [simplified] = _simplifier.simplify(
18818
19076
  triVerts,
18819
19077
  vertProperties,
@@ -20223,11 +20481,12 @@ function profileMayContainInteriorLoopsForOCCT(plan) {
20223
20481
  return false;
20224
20482
  }
20225
20483
  }
20484
+ const occtLoweredCache = /* @__PURE__ */ new WeakMap();
20226
20485
  function lowerShapeCompilePlanToOCCT(plan, oc) {
20227
- const cached = plan._occtCache;
20486
+ const cached = occtLoweredCache.get(plan);
20228
20487
  if (cached) return cached;
20229
20488
  const shape = _lowerShapeCompilePlanToOCCTInner(plan, oc);
20230
- plan._occtCache = shape;
20489
+ occtLoweredCache.set(plan, shape);
20231
20490
  return shape;
20232
20491
  }
20233
20492
  function _lowerShapeCompilePlanToOCCTInner(plan, oc) {
@@ -28765,16 +29024,16 @@ function surfaceNets(sdfFn, bounds, edgeLength2) {
28765
29024
  numTris: faces.length / 3
28766
29025
  };
28767
29026
  }
28768
- const EPS$1 = 1e-9;
29027
+ const EPS = 1e-9;
28769
29028
  function finitePositive$1(value) {
28770
- return Number.isFinite(value) && value > EPS$1;
29029
+ return Number.isFinite(value) && value > EPS;
28771
29030
  }
28772
29031
  function clampNonNegative(value) {
28773
- return Math.abs(value) <= EPS$1 ? 0 : value;
29032
+ return Math.abs(value) <= EPS ? 0 : value;
28774
29033
  }
28775
29034
  function distancePreservingMatrixScale(matrix) {
28776
29035
  if (matrix.length !== 16 || matrix.some((value) => !Number.isFinite(value))) return null;
28777
- if (Math.abs(matrix[3]) > EPS$1 || Math.abs(matrix[7]) > EPS$1 || Math.abs(matrix[11]) > EPS$1 || Math.abs(matrix[15] - 1) > EPS$1) {
29036
+ if (Math.abs(matrix[3]) > EPS || Math.abs(matrix[7]) > EPS || Math.abs(matrix[11]) > EPS || Math.abs(matrix[15] - 1) > EPS) {
28778
29037
  return null;
28779
29038
  }
28780
29039
  const col0 = [matrix[0], matrix[1], matrix[2]];
@@ -28786,8 +29045,8 @@ function distancePreservingMatrixScale(matrix) {
28786
29045
  const sy = length4(col1);
28787
29046
  const sz = length4(col2);
28788
29047
  if (!finitePositive$1(sx) || !finitePositive$1(sy) || !finitePositive$1(sz)) return null;
28789
- if (Math.abs(sx - sy) > EPS$1 || Math.abs(sx - sz) > EPS$1) return null;
28790
- if (Math.abs(dot2(col0, col1)) > EPS$1 || Math.abs(dot2(col0, col2)) > EPS$1 || Math.abs(dot2(col1, col2)) > EPS$1) return null;
29048
+ if (Math.abs(sx - sy) > EPS || Math.abs(sx - sz) > EPS) return null;
29049
+ if (Math.abs(dot2(col0, col1)) > EPS || Math.abs(dot2(col0, col2)) > EPS || Math.abs(dot2(col1, col2)) > EPS) return null;
28791
29050
  return sx;
28792
29051
  }
28793
29052
  function transformStepDistanceScale(step) {
@@ -28803,7 +29062,7 @@ function transformStepDistanceScale(step) {
28803
29062
  const sy = Math.abs(step.y);
28804
29063
  const sz = Math.abs(step.z);
28805
29064
  if (!finitePositive$1(sx) || !finitePositive$1(sy) || !finitePositive$1(sz)) return null;
28806
- return Math.abs(sx - sy) <= EPS$1 && Math.abs(sx - sz) <= EPS$1 ? sx : null;
29065
+ return Math.abs(sx - sy) <= EPS && Math.abs(sx - sz) <= EPS ? sx : null;
28807
29066
  }
28808
29067
  }
28809
29068
  }
@@ -28834,7 +29093,7 @@ function cloneTransformStep(step) {
28834
29093
  }
28835
29094
  }
28836
29095
  function translatedPlan(base, z2) {
28837
- if (Math.abs(z2) <= EPS$1) return base;
29096
+ if (Math.abs(z2) <= EPS) return base;
28838
29097
  return {
28839
29098
  kind: "transform",
28840
29099
  base,
@@ -28851,7 +29110,7 @@ function offsetCylinderDimensions(plan, thickness) {
28851
29110
  const height = zMax - zMin;
28852
29111
  const radiusBottom = plan.radius + thickness * (normalScale - slope);
28853
29112
  const offsetRadiusTop = radiusTop + thickness * (normalScale + slope);
28854
- if (!finitePositive$1(height) || radiusBottom < -EPS$1 || offsetRadiusTop < -EPS$1) return null;
29113
+ if (!finitePositive$1(height) || radiusBottom < -EPS || offsetRadiusTop < -EPS) return null;
28855
29114
  return {
28856
29115
  zMin,
28857
29116
  height,
@@ -28875,7 +29134,7 @@ function transformProfilePoint(point, transform) {
28875
29134
  const nx = transform.normalX;
28876
29135
  const ny = transform.normalY;
28877
29136
  const len = Math.hypot(nx, ny);
28878
- if (len <= EPS$1) return point;
29137
+ if (len <= EPS) return point;
28879
29138
  const ux = nx / len;
28880
29139
  const uy = ny / len;
28881
29140
  const d2 = point[0] * ux + point[1] * uy;
@@ -28889,7 +29148,7 @@ function transformProfilePointThrough(point, transforms) {
28889
29148
  return out;
28890
29149
  }
28891
29150
  function sameScalar$2(a2, b) {
28892
- return Math.abs(a2 - b) <= EPS$1;
29151
+ return Math.abs(a2 - b) <= EPS;
28893
29152
  }
28894
29153
  function sameProfilePoint(a2, b) {
28895
29154
  return sameScalar$2(a2[0], b[0]) && sameScalar$2(a2[1], b[1]);
@@ -28943,7 +29202,7 @@ function rectangleFootprintFromProfile(plan) {
28943
29202
  const [xMin, xMax] = xs;
28944
29203
  const [zMin, zMax] = zs;
28945
29204
  if (xMin == null || xMax == null || zMin == null || zMax == null) return null;
28946
- if (xMin < -EPS$1 || !finitePositive$1(xMax) || !finitePositive$1(zMax - zMin)) return null;
29205
+ if (xMin < -EPS || !finitePositive$1(xMax) || !finitePositive$1(zMax - zMin)) return null;
28947
29206
  const hasCorner = (x2, z2) => points.some(([px, pz]) => sameScalar$2(px, x2) && sameScalar$2(pz, z2));
28948
29207
  if (!hasCorner(xMin, zMin) || !hasCorner(xMax, zMin) || !hasCorner(xMax, zMax) || !hasCorner(xMin, zMax)) return null;
28949
29208
  return {
@@ -28966,7 +29225,7 @@ function circleFootprintFromProfile(plan) {
28966
29225
  const yScale = Math.hypot(yAxis[0], yAxis[1]);
28967
29226
  const dot2 = xAxis[0] * yAxis[0] + xAxis[1] * yAxis[1];
28968
29227
  if (!finitePositive$1(xScale) || !finitePositive$1(yScale)) return null;
28969
- if (Math.abs(xScale - yScale) > EPS$1 || Math.abs(dot2) > EPS$1 * xScale * yScale) return null;
29228
+ if (Math.abs(xScale - yScale) > EPS || Math.abs(dot2) > EPS * xScale * yScale) return null;
28970
29229
  return {
28971
29230
  center,
28972
29231
  radius: radius * xScale,
@@ -28974,11 +29233,11 @@ function circleFootprintFromProfile(plan) {
28974
29233
  };
28975
29234
  }
28976
29235
  function fullCircleRevolveTorusPlan(plan, minorRadiusOffset = 0) {
28977
- if (Math.abs(plan.degrees - 360) > EPS$1) return null;
29236
+ if (Math.abs(plan.degrees - 360) > EPS) return null;
28978
29237
  const circle = circleFootprintFromProfile(plan.profile);
28979
- if (!circle || circle.center[0] <= EPS$1) return null;
29238
+ if (!circle || circle.center[0] <= EPS) return null;
28980
29239
  const minorRadius = circle.radius + minorRadiusOffset;
28981
- if (!finitePositive$1(minorRadius) || minorRadius >= circle.center[0] - EPS$1) return null;
29240
+ if (!finitePositive$1(minorRadius) || minorRadius >= circle.center[0] - EPS) return null;
28982
29241
  return translatedPlan(
28983
29242
  {
28984
29243
  kind: "torus",
@@ -28991,7 +29250,7 @@ function fullCircleRevolveTorusPlan(plan, minorRadiusOffset = 0) {
28991
29250
  }
28992
29251
  function fullAxisRectRevolveCylinderPlan(plan) {
28993
29252
  const rectangle = fullRectRevolveSurfacePlan(plan);
28994
- if (!rectangle || rectangle.innerRadius > EPS$1) return null;
29253
+ if (!rectangle || rectangle.innerRadius > EPS) return null;
28995
29254
  return translatedPlan(
28996
29255
  {
28997
29256
  kind: "cylinder",
@@ -29014,20 +29273,20 @@ function rectRevolveSurfacePlan(plan) {
29014
29273
  };
29015
29274
  }
29016
29275
  function fullRectRevolveSurfacePlan(plan) {
29017
- if (Math.abs(plan.degrees - 360) > EPS$1) return null;
29276
+ if (Math.abs(plan.degrees - 360) > EPS) return null;
29018
29277
  return rectRevolveSurfacePlan(plan);
29019
29278
  }
29020
29279
  function offsetFullRectRevolvePlan(plan, thickness) {
29021
29280
  const cylinderPlan = fullAxisRectRevolveCylinderPlan(plan);
29022
29281
  if (cylinderPlan) return offsetSolidAnalyticPrimitivePlan(cylinderPlan, thickness);
29023
29282
  const rectangle = fullRectRevolveSurfacePlan(plan);
29024
- if (!rectangle || rectangle.innerRadius <= EPS$1) return null;
29283
+ if (!rectangle || rectangle.innerRadius <= EPS) return null;
29025
29284
  const innerRadius = rectangle.innerRadius - thickness;
29026
29285
  const outerRadius = rectangle.outerRadius + thickness;
29027
29286
  const height = rectangle.zMax - rectangle.zMin + 2 * thickness;
29028
- if (innerRadius < -EPS$1 || !finitePositive$1(outerRadius) || !finitePositive$1(height) || outerRadius <= innerRadius + EPS$1) return null;
29287
+ if (innerRadius < -EPS || !finitePositive$1(outerRadius) || !finitePositive$1(height) || outerRadius <= innerRadius + EPS) return null;
29029
29288
  const zCenter = (rectangle.zMin + rectangle.zMax) / 2;
29030
- if (innerRadius <= EPS$1) {
29289
+ if (innerRadius <= EPS) {
29031
29290
  return translatedPlan(
29032
29291
  {
29033
29292
  kind: "cylinder",
@@ -29106,7 +29365,7 @@ function offsetSolidAnalyticPrimitivePlan(base, thickness) {
29106
29365
  }
29107
29366
  case "torus": {
29108
29367
  const minorRadius = base.minorRadius + thickness;
29109
- if (!finitePositive$1(minorRadius) || minorRadius >= base.majorRadius - EPS$1) return null;
29368
+ if (!finitePositive$1(minorRadius) || minorRadius >= base.majorRadius - EPS) return null;
29110
29369
  return {
29111
29370
  kind: "torus",
29112
29371
  majorRadius: base.majorRadius,
@@ -29710,7 +29969,7 @@ function sub$1(a2, b) {
29710
29969
  function scale$1(v, scalar) {
29711
29970
  return [v[0] * scalar, v[1] * scalar, v[2] * scalar];
29712
29971
  }
29713
- function cross$3(a2, b) {
29972
+ function cross$2(a2, b) {
29714
29973
  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]];
29715
29974
  }
29716
29975
  function isFiniteNumber(value) {
@@ -29946,8 +30205,8 @@ function boundsCorners(bounds) {
29946
30205
  }
29947
30206
  function perpendicularAxes(normal) {
29948
30207
  const seed = Math.abs(normal[2]) < 0.9 ? [0, 0, 1] : [0, 1, 0];
29949
- const uAxis = normalizeVec3$1(cross$3(seed, normal));
29950
- const vAxis = normalizeVec3$1(cross$3(normal, uAxis));
30208
+ const uAxis = normalizeVec3$1(cross$2(seed, normal));
30209
+ const vAxis = normalizeVec3$1(cross$2(normal, uAxis));
29951
30210
  return { uAxis, vAxis };
29952
30211
  }
29953
30212
  function createBoundedHalfSpace(bounds, normal, originOffset) {
@@ -30015,7 +30274,7 @@ function faceAxes(face) {
30015
30274
  for (const vertex of face.vertices.slice(1)) {
30016
30275
  const uAxis = normalizeVec3$1([vertex[0] - origin[0], vertex[1] - origin[1], vertex[2] - origin[2]]);
30017
30276
  if (Math.hypot(...uAxis) <= 1e-12) continue;
30018
- const vAxis = normalizeVec3$1(cross$3(face.normal, uAxis));
30277
+ const vAxis = normalizeVec3$1(cross$2(face.normal, uAxis));
30019
30278
  if (Math.hypot(...vAxis) <= 1e-12) continue;
30020
30279
  return {
30021
30280
  uAxis,
@@ -30024,18 +30283,18 @@ function faceAxes(face) {
30024
30283
  }
30025
30284
  return {};
30026
30285
  }
30027
- function edgeKey$1(start, end) {
30286
+ function edgeKey$2(start, end) {
30028
30287
  const encode = (p2) => p2.map((value) => value.toFixed(9)).join(",");
30029
30288
  const a2 = encode(start);
30030
30289
  const b = encode(end);
30031
30290
  return a2 < b ? `${a2}|${b}` : `${b}|${a2}`;
30032
30291
  }
30033
30292
  function faceEdgeIndex(face, start, end) {
30034
- const target = edgeKey$1(start, end);
30293
+ const target = edgeKey$2(start, end);
30035
30294
  for (let i = 0; i < face.vertices.length; i++) {
30036
30295
  const faceStart = face.vertices[i];
30037
30296
  const faceEnd = face.vertices[(i + 1) % face.vertices.length];
30038
- if (faceStart && faceEnd && edgeKey$1(faceStart, faceEnd) === target) return i;
30297
+ if (faceStart && faceEnd && edgeKey$2(faceStart, faceEnd) === target) return i;
30039
30298
  }
30040
30299
  return null;
30041
30300
  }
@@ -30236,7 +30495,7 @@ function topologyPayloadToTopology(payload) {
30236
30495
  const start = explicitVertices[explicitEdge.vertices[0]];
30237
30496
  const end = explicitVertices[explicitEdge.vertices[1]];
30238
30497
  if (!isVec3(start) || !isVec3(end)) continue;
30239
- const key = edgeKey$1(start, end);
30498
+ const key = edgeKey$2(start, end);
30240
30499
  if (seenEdges.has(key)) continue;
30241
30500
  seenEdges.set(key, edges.size);
30242
30501
  const display = explicitEdgeDisplayName(payload, explicitEdge, explicitEdgeIndex, start, end);
@@ -30256,7 +30515,7 @@ function topologyPayloadToTopology(payload) {
30256
30515
  const start = face.vertices[i];
30257
30516
  const end = face.vertices[(i + 1) % face.vertices.length];
30258
30517
  if (!start || !end) continue;
30259
- const key = edgeKey$1(start, end);
30518
+ const key = edgeKey$2(start, end);
30260
30519
  if (seenEdges.has(key)) continue;
30261
30520
  seenEdges.set(key, edges.size);
30262
30521
  edges.set(`${face.id}:edge-${i}`, {
@@ -32576,7 +32835,7 @@ function isStraightMonotonePolyline(points) {
32576
32835
  for (const point of points) {
32577
32836
  const offset = subtract3$1(point, start);
32578
32837
  const projection = dot3$3(offset, axis) / axisLengthSq;
32579
- const lineDistance = vectorLength3$1(cross3$4(offset, axis)) / axisLength;
32838
+ const lineDistance = vectorLength3$1(cross3$5(offset, axis)) / axisLength;
32580
32839
  if (lineDistance > 1e-5 || projection < -1e-6 || projection > 1 + 1e-6 || projection + 1e-6 < previousProjection) {
32581
32840
  return false;
32582
32841
  }
@@ -32652,7 +32911,7 @@ function isDistancePreservingMatrix$1(matrix) {
32652
32911
  const col0 = [matrix[0], matrix[1], matrix[2]];
32653
32912
  const col1 = [matrix[4], matrix[5], matrix[6]];
32654
32913
  const col2 = [matrix[8], matrix[9], matrix[10]];
32655
- const det = dot3$3(col0, cross3$4(col1, col2));
32914
+ const det = dot3$3(col0, cross3$5(col1, col2));
32656
32915
  return Math.abs(vectorLength3$1(col0) - 1) <= eps && Math.abs(vectorLength3$1(col1) - 1) <= eps && Math.abs(vectorLength3$1(col2) - 1) <= eps && Math.abs(dot3$3(col0, col1)) <= eps && Math.abs(dot3$3(col0, col2)) <= eps && Math.abs(dot3$3(col1, col2)) <= eps && Math.abs(Math.abs(det) - 1) <= eps;
32657
32916
  }
32658
32917
  function offsetSolidTransformDistanceScale(step) {
@@ -32897,41 +33156,41 @@ function surfaceGridForFillPlan(plan) {
32897
33156
  }
32898
33157
  return grid;
32899
33158
  }
32900
- function add3(a2, b) {
33159
+ function add3$1(a2, b) {
32901
33160
  return [a2[0] + b[0], a2[1] + b[1], a2[2] + b[2]];
32902
33161
  }
32903
- function scale3(v, s) {
33162
+ function scale3$1(v, s) {
32904
33163
  return [v[0] * s, v[1] * s, v[2] * s];
32905
33164
  }
32906
33165
  function analyticYForFrame(axis, xAxis) {
32907
- return normalizedVector3(cross3$4(axis, xAxis), "analytic surface yAxis");
33166
+ return normalizedVector3(cross3$5(axis, xAxis), "analytic surface yAxis");
32908
33167
  }
32909
33168
  function radialPoint(xAxis, yAxis, u2, radius) {
32910
- return add3(scale3(xAxis, Math.cos(u2) * radius), scale3(yAxis, Math.sin(u2) * radius));
33169
+ return add3$1(scale3$1(xAxis, Math.cos(u2) * radius), scale3$1(yAxis, Math.sin(u2) * radius));
32911
33170
  }
32912
33171
  function analyticSurfacePoint(plan, u2, v) {
32913
33172
  switch (plan.kind) {
32914
33173
  case "plane":
32915
- return add3(add3(plan.origin, scale3(plan.uAxis, u2)), scale3(plan.vAxis, v));
33174
+ return add3$1(add3$1(plan.origin, scale3$1(plan.uAxis, u2)), scale3$1(plan.vAxis, v));
32916
33175
  case "cylinder": {
32917
33176
  const yAxis = analyticYForFrame(plan.axis, plan.xAxis);
32918
- return add3(add3(plan.origin, scale3(plan.axis, v)), radialPoint(plan.xAxis, yAxis, u2, plan.radius));
33177
+ return add3$1(add3$1(plan.origin, scale3$1(plan.axis, v)), radialPoint(plan.xAxis, yAxis, u2, plan.radius));
32919
33178
  }
32920
33179
  case "cone": {
32921
33180
  const yAxis = analyticYForFrame(plan.axis, plan.xAxis);
32922
33181
  const t = (v - plan.vMin) / (plan.vMax - plan.vMin);
32923
33182
  const radius = plan.radiusBottom + (plan.radiusTop - plan.radiusBottom) * t;
32924
- return add3(add3(plan.origin, scale3(plan.axis, v)), radialPoint(plan.xAxis, yAxis, u2, radius));
33183
+ return add3$1(add3$1(plan.origin, scale3$1(plan.axis, v)), radialPoint(plan.xAxis, yAxis, u2, radius));
32925
33184
  }
32926
33185
  case "sphere": {
32927
33186
  const yAxis = analyticYForFrame(plan.axis, plan.xAxis);
32928
33187
  const radial = radialPoint(plan.xAxis, yAxis, u2, plan.radius * Math.cos(v));
32929
- return add3(add3(plan.center, radial), scale3(plan.axis, plan.radius * Math.sin(v)));
33188
+ return add3$1(add3$1(plan.center, radial), scale3$1(plan.axis, plan.radius * Math.sin(v)));
32930
33189
  }
32931
33190
  case "torus": {
32932
33191
  const yAxis = analyticYForFrame(plan.axis, plan.xAxis);
32933
33192
  const radial = radialPoint(plan.xAxis, yAxis, u2, plan.majorRadius + plan.minorRadius * Math.cos(v));
32934
- return add3(add3(plan.center, radial), scale3(plan.axis, plan.minorRadius * Math.sin(v)));
33193
+ return add3$1(add3$1(plan.center, radial), scale3$1(plan.axis, plan.minorRadius * Math.sin(v)));
32935
33194
  }
32936
33195
  default:
32937
33196
  return assertExhaustive(plan);
@@ -33049,7 +33308,7 @@ function triangleNormal(vertices, triangle) {
33049
33308
  const c2 = vertices[triangle[2]];
33050
33309
  const ab = [b[0] - a2[0], b[1] - a2[1], b[2] - a2[2]];
33051
33310
  const ac = [c2[0] - a2[0], c2[1] - a2[1], c2[2] - a2[2]];
33052
- const normal = cross3$4(ab, ac);
33311
+ const normal = cross3$5(ab, ac);
33053
33312
  const len = Math.hypot(normal[0], normal[1], normal[2]);
33054
33313
  if (len <= 1e-12) return null;
33055
33314
  return [normal[0] / len, normal[1] / len, normal[2] / len];
@@ -33195,7 +33454,7 @@ function lowerSurfaceSolidPlan(plan) {
33195
33454
  );
33196
33455
  }
33197
33456
  }
33198
- function cross3$4(a2, b) {
33457
+ function cross3$5(a2, b) {
33199
33458
  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]];
33200
33459
  }
33201
33460
  function fromSlicesPlaneFrame(normalInput) {
@@ -33211,8 +33470,8 @@ function fromSlicesPlaneFrame(normalInput) {
33211
33470
  return { u: [0, 1, 0], v: [0, 0, nx > 0 ? 1 : -1], normal };
33212
33471
  }
33213
33472
  const reference = Math.abs(nx) < 0.9 ? [1, 0, 0] : [0, 1, 0];
33214
- const u2 = normalizedVector3(cross3$4(reference, normal), "Shape.fromSlices profile u axis");
33215
- return { u: u2, v: cross3$4(normal, u2), normal };
33473
+ const u2 = normalizedVector3(cross3$5(reference, normal), "Shape.fromSlices profile u axis");
33474
+ return { u: u2, v: cross3$5(normal, u2), normal };
33216
33475
  }
33217
33476
  function fromSlicesLocalToWorldMatrix(normal) {
33218
33477
  const frame = fromSlicesPlaneFrame(normal);
@@ -36631,29 +36890,11 @@ function lowerExactSlicedShapeCompilePlanToTruckProfileBackend(plan, offset) {
36631
36890
  return profilePlan ? lowerProfileCompilePlanToTruckProfileBackend(profilePlan) : null;
36632
36891
  }
36633
36892
  const SHAPE_COMPILE_PLAN_CACHE_KEY_VERSION = "shape-plan-v1";
36634
- function stableJsonEncode(value, arrayMember) {
36635
- if (value === void 0 || typeof value === "function" || typeof value === "symbol") {
36636
- return arrayMember ? "null" : void 0;
36637
- }
36638
- if (value === null || typeof value === "number" || typeof value === "boolean" || typeof value === "string") {
36639
- return JSON.stringify(value);
36640
- }
36641
- if (Array.isArray(value)) {
36642
- return `[${value.map((item) => stableJsonEncode(item, true) ?? "null").join(",")}]`;
36643
- }
36644
- const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
36645
- const encodedEntries = [];
36646
- for (const [key, item] of entries) {
36647
- const encoded = stableJsonEncode(item, false);
36648
- if (encoded !== void 0) encodedEntries.push(`${JSON.stringify(key)}:${encoded}`);
36649
- }
36650
- return `{${encodedEntries.join(",")}}`;
36651
- }
36652
- function stableJsonStringify(value) {
36653
- return stableJsonEncode(value, false) ?? "null";
36654
- }
36893
+ const exactPlanHasher = createStructuralHasher({
36894
+ skipKey: (key) => key.startsWith("_") || key === "owner" || key === "queryPropagation"
36895
+ });
36655
36896
  function shapeCompilePlanCacheKey(plan) {
36656
- return `${SHAPE_COMPILE_PLAN_CACHE_KEY_VERSION}:${stableJsonStringify(plan)}`;
36897
+ return `${SHAPE_COMPILE_PLAN_CACHE_KEY_VERSION}:${exactPlanHasher(plan)}`;
36657
36898
  }
36658
36899
  function formatFaceQuery(query) {
36659
36900
  const parts = [];
@@ -36983,7 +37224,7 @@ function normalizeFaceSelector(selector) {
36983
37224
  }
36984
37225
  return { compilePlanName: null, query: selector };
36985
37226
  }
36986
- function cross$2(a2, b) {
37227
+ function cross$1(a2, b) {
36987
37228
  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]];
36988
37229
  }
36989
37230
  function dot$1(a2, b) {
@@ -36996,8 +37237,8 @@ function normVec3(v) {
36996
37237
  }
36997
37238
  function tangentFrame(normal) {
36998
37239
  const ref = Math.abs(normal[0]) < 0.9 ? [1, 0, 0] : [0, 1, 0];
36999
- const v = normVec3(cross$2(normal, ref));
37000
- const u2 = normVec3(cross$2(v, normal));
37240
+ const v = normVec3(cross$1(normal, ref));
37241
+ const u2 = normVec3(cross$1(v, normal));
37001
37242
  return { u: u2, v };
37002
37243
  }
37003
37244
  const NORMAL_COS_EPS$1 = 0.9998;
@@ -37016,7 +37257,7 @@ function clusterMeshFaces(shape) {
37016
37257
  const v2 = [vertProperties[i2 * numProp], vertProperties[i2 * numProp + 1], vertProperties[i2 * numProp + 2]];
37017
37258
  const e1 = [v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]];
37018
37259
  const e2 = [v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2]];
37019
- const rawCross = cross$2(e1, e2);
37260
+ const rawCross = cross$1(e1, e2);
37020
37261
  const normal = normVec3(rawCross);
37021
37262
  if (!normal) continue;
37022
37263
  const crossLen = Math.sqrt(rawCross[0] * rawCross[0] + rawCross[1] * rawCross[1] + rawCross[2] * rawCross[2]);
@@ -37426,14 +37667,14 @@ function normalize2d(vec2) {
37426
37667
  if (len < 1e-12) return [1, 0];
37427
37668
  return [vec2[0] / len, vec2[1] / len];
37428
37669
  }
37429
- function cross3$3(a2, b) {
37670
+ function cross3$4(a2, b) {
37430
37671
  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]];
37431
37672
  }
37432
37673
  function orthonormalBasisFromNormal(normal) {
37433
37674
  const n = normalizeAxis(normal);
37434
37675
  const seed = Math.abs(n[2]) < 0.9 ? [0, 0, 1] : [0, 1, 0];
37435
- const u2 = normalizeAxis(cross3$3(seed, n));
37436
- const v = normalizeAxis(cross3$3(n, u2));
37676
+ const u2 = normalizeAxis(cross3$4(seed, n));
37677
+ const v = normalizeAxis(cross3$4(n, u2));
37437
37678
  return { u: u2, v };
37438
37679
  }
37439
37680
  function faceFrom2DEdge(name, start, end, zMid, ownerQuery) {
@@ -38270,7 +38511,7 @@ function resolveShapeFaceTableInternal(plan, owner) {
38270
38511
  forwardStart3[1] - reverseStart3[1],
38271
38512
  forwardStart3[2] - reverseStart3[2]
38272
38513
  ]);
38273
- const normal = normalizeAxis(cross3$3(edgeVec, depthVec));
38514
+ const normal = normalizeAxis(cross3$4(edgeVec, depthVec));
38274
38515
  registerFace(table, {
38275
38516
  name: wall.name,
38276
38517
  normal,
@@ -38756,14 +38997,14 @@ function normalize3$1(v) {
38756
38997
  function dot3$2(a2, b) {
38757
38998
  return a2[0] * b[0] + a2[1] * b[1] + a2[2] * b[2];
38758
38999
  }
38759
- function cross3$2(a2, b) {
39000
+ function cross3$3(a2, b) {
38760
39001
  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]];
38761
39002
  }
38762
39003
  function perpendicularTo(axis) {
38763
39004
  const absX = Math.abs(axis[0]);
38764
39005
  const absZ = Math.abs(axis[2]);
38765
39006
  const seed = absX < absZ ? [1, 0, 0] : [0, 0, 1];
38766
- return normalize3$1(cross3$2(axis, seed));
39007
+ return normalize3$1(cross3$3(axis, seed));
38767
39008
  }
38768
39009
  function normalizePortInput(input) {
38769
39010
  let origin;
@@ -38944,18 +39185,18 @@ function normalize3(v) {
38944
39185
  if (l < 1e-10) throw new Error("Cannot normalize zero-length vector");
38945
39186
  return [v[0] / l, v[1] / l, v[2] / l];
38946
39187
  }
38947
- function cross3$1(a2, b) {
39188
+ function cross3$2(a2, b) {
38948
39189
  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]];
38949
39190
  }
38950
- function sub3(a2, b) {
39191
+ function sub3$1(a2, b) {
38951
39192
  return [a2[0] - b[0], a2[1] - b[1], a2[2] - b[2]];
38952
39193
  }
38953
39194
  function negate3(v) {
38954
39195
  return [-v[0], -v[1], -v[2]];
38955
39196
  }
38956
39197
  function alignmentMatrix(childOrigin, childAxis, childUp, parentOrigin, parentAxis, parentUp) {
38957
- const cRight = normalize3(cross3$1(childAxis, childUp));
38958
- const pRight = normalize3(cross3$1(parentAxis, parentUp));
39198
+ const cRight = normalize3(cross3$2(childAxis, childUp));
39199
+ const pRight = normalize3(cross3$2(parentAxis, parentUp));
38959
39200
  const r00 = pRight[0] * cRight[0] + parentUp[0] * childUp[0] + parentAxis[0] * childAxis[0];
38960
39201
  const r01 = pRight[0] * cRight[1] + parentUp[0] * childUp[1] + parentAxis[0] * childAxis[1];
38961
39202
  const r02 = pRight[0] * cRight[2] + parentUp[0] * childUp[2] + parentAxis[0] * childAxis[2];
@@ -38970,7 +39211,7 @@ function alignmentMatrix(childOrigin, childAxis, childUp, parentOrigin, parentAx
38970
39211
  r10 * childOrigin[0] + r11 * childOrigin[1] + r12 * childOrigin[2],
38971
39212
  r20 * childOrigin[0] + r21 * childOrigin[1] + r22 * childOrigin[2]
38972
39213
  ];
38973
- const t = sub3(parentOrigin, rc);
39214
+ const t = sub3$1(parentOrigin, rc);
38974
39215
  return Transform.from([r00, r10, r20, 0, r01, r11, r21, 0, r02, r12, r22, 0, t[0], t[1], t[2], 1]);
38975
39216
  }
38976
39217
  function computeSinglePairAlignment(childPort, targetPort) {
@@ -39009,8 +39250,8 @@ function computeMultiPairAlignment(pairs, childPorts, targetPorts, tolerance = 0
39009
39250
  [0, 0, 0]
39010
39251
  ];
39011
39252
  for (const p2 of pairs) {
39012
- const s = sub3(p2.childOrigin, srcCentroid);
39013
- const t2 = sub3(p2.targetOrigin, tgtCentroid);
39253
+ const s = sub3$1(p2.childOrigin, srcCentroid);
39254
+ const t2 = sub3$1(p2.targetOrigin, tgtCentroid);
39014
39255
  for (let i = 0; i < 3; i++) {
39015
39256
  for (let j = 0; j < 3; j++) {
39016
39257
  h[i][j] += s[i] * t2[j];
@@ -39023,7 +39264,7 @@ function computeMultiPairAlignment(pairs, childPorts, targetPorts, tolerance = 0
39023
39264
  R[1][0] * srcCentroid[0] + R[1][1] * srcCentroid[1] + R[1][2] * srcCentroid[2],
39024
39265
  R[2][0] * srcCentroid[0] + R[2][1] * srcCentroid[1] + R[2][2] * srcCentroid[2]
39025
39266
  ];
39026
- const t = sub3(tgtCentroid, rSrc);
39267
+ const t = sub3$1(tgtCentroid, rSrc);
39027
39268
  const transform = Transform.from([
39028
39269
  R[0][0],
39029
39270
  R[1][0],
@@ -39045,7 +39286,7 @@ function computeMultiPairAlignment(pairs, childPorts, targetPorts, tolerance = 0
39045
39286
  const residuals = [];
39046
39287
  for (const p2 of pairs) {
39047
39288
  const transformed = transform.point(p2.childOrigin);
39048
- const diff = sub3(transformed, p2.targetOrigin);
39289
+ const diff = sub3$1(transformed, p2.targetOrigin);
39049
39290
  residuals.push(len3(diff));
39050
39291
  }
39051
39292
  const maxResidual = Math.max(...residuals);
@@ -39246,7 +39487,7 @@ function getConnectorDistance(ports, nameA, nameB) {
39246
39487
  const b = ports[nameB];
39247
39488
  if (!a2) throw new Error(`connectorDistance: unknown connector "${nameA}"`);
39248
39489
  if (!b) throw new Error(`connectorDistance: unknown connector "${nameB}"`);
39249
- const d2 = sub3(a2.origin, b.origin);
39490
+ const d2 = sub3$1(a2.origin, b.origin);
39250
39491
  return len3(d2);
39251
39492
  }
39252
39493
  function getConnectorMeasurements(ports, name) {
@@ -39359,7 +39600,9 @@ const _ManifoldShapeBackend = class _ManifoldShapeBackend {
39359
39600
  return this.getLiveManifold("numTri()").numTri();
39360
39601
  }
39361
39602
  getMesh() {
39362
- return this.getLiveManifold("getMesh()").getMesh();
39603
+ const manifold = this.getLiveManifold("getMesh()");
39604
+ const mesh = manifold.numProp() >= 3 ? manifold.getMesh(0) : manifold.getMesh();
39605
+ return mesh;
39363
39606
  }
39364
39607
  slice(offset) {
39365
39608
  return this.getLiveManifold("slice()").slice(offset);
@@ -39660,7 +39903,7 @@ class ShapeGroup {
39660
39903
  };
39661
39904
  return this.attachTo(parent, face, opp[face], uvMap[face](u2, v, p2));
39662
39905
  }
39663
- /** Rotate the group around an arbitrary axis through the origin. */
39906
+ /** 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. */
39664
39907
  rotate(axis, angleDeg, options) {
39665
39908
  requireRotateAxis(axis, "ShapeGroup.rotate()");
39666
39909
  requireFiniteAngle(angleDeg, "ShapeGroup.rotate()");
@@ -39905,6 +40148,11 @@ class ShapeGroup {
39905
40148
  * Position this group by matching connectors to a target.
39906
40149
  * Connector names support dotted paths into named children: "ChildName.connectorName".
39907
40150
  *
40151
+ * Alignment: with a single connector pair, the group translates and rotates so the connector
40152
+ * origins coincide and the axes oppose (plug-in model); `up` pins the roll. With multiple pairs,
40153
+ * the connector origins define the rigid transform — still author meaningful `axis`/`up` values
40154
+ * so the same connectors remain useful for `connect()`, audits, and future matching.
40155
+ *
39908
40156
  * Overloads:
39909
40157
  * - Single pair: `matchTo(target, selfConn, targetConn, options?)`
39910
40158
  * - Dictionary (same target): `matchTo(target, { selfConn: targetConn, ... }, options?)`
@@ -41644,7 +41892,7 @@ async function initKernelManifoldOnly() {
41644
41892
  _activeBackend = "manifold";
41645
41893
  return manifoldModule;
41646
41894
  }
41647
- const DEFAULT_ACTIVE_BACKEND = "truck";
41895
+ const DEFAULT_ACTIVE_BACKEND = "manifold";
41648
41896
  let _activeBackend = DEFAULT_ACTIVE_BACKEND;
41649
41897
  let _runtimeWarn = (msg) => console.warn(msg);
41650
41898
  function unwrapShapeLike(value) {
@@ -41663,6 +41911,7 @@ const _shapePlacementRefs = /* @__PURE__ */ new WeakMap();
41663
41911
  const _shapeExplodeHint = /* @__PURE__ */ new WeakMap();
41664
41912
  const _shapeRuntimeBackends = /* @__PURE__ */ new WeakMap();
41665
41913
  const _shapeTopology = /* @__PURE__ */ new WeakMap();
41914
+ const _shapeLineageTokens = /* @__PURE__ */ new WeakMap();
41666
41915
  const _shapeFaceLabels = /* @__PURE__ */ new WeakMap();
41667
41916
  const _shapeReferenceNames = /* @__PURE__ */ new WeakMap();
41668
41917
  const _shapeReferenceAliases = /* @__PURE__ */ new WeakMap();
@@ -41723,6 +41972,10 @@ function copyShapeReferenceMetadata(source, target) {
41723
41972
  const aliases = cloneReferenceAliases(_shapeReferenceAliases.get(source));
41724
41973
  if (aliases && aliases.size > 0) _shapeReferenceAliases.set(target, aliases);
41725
41974
  }
41975
+ function copyShapeLineage(source, target) {
41976
+ const token = _shapeLineageTokens.get(source);
41977
+ if (token) _shapeLineageTokens.set(target, token);
41978
+ }
41726
41979
  function assertNonEmptyReferenceName(name, apiName) {
41727
41980
  const trimmed = name.trim();
41728
41981
  if (!trimmed) throw new Error(`${apiName} requires a non-empty reference name.`);
@@ -41791,50 +42044,25 @@ function setShapeRuntimeBackendInternal(shape, payload) {
41791
42044
  return shape;
41792
42045
  }
41793
42046
  function setShapeCompilePlanInternal(shape, plan) {
41794
- _shapeCompilePlans.set(shape, cloneShapeCompilePlan(plan));
42047
+ _shapeCompilePlans.set(shape, deepFreezePlanData(plan));
41795
42048
  recordShapeSourceSpanInternal(shape, plan);
41796
42049
  return shape;
41797
42050
  }
41798
- function cloneShapeSourceSpanRecords(records) {
41799
- return (records ?? []).map((record) => ({
41800
- planCacheKey: record.planCacheKey,
41801
- sourceSpan: { ...record.sourceSpan }
41802
- }));
41803
- }
41804
42051
  function upsertShapeSourceSpanRecord(shape, record) {
41805
- const records = cloneShapeSourceSpanRecords(_shapeSourceSpans.get(shape));
41806
- if (records.some((existing) => existing.planCacheKey === record.planCacheKey)) return;
41807
- records.push({
41808
- planCacheKey: record.planCacheKey,
41809
- sourceSpan: { ...record.sourceSpan }
41810
- });
41811
- _shapeSourceSpans.set(shape, records);
41812
- }
41813
- function stableTraceSourcePlanEncode(value, arrayMember) {
41814
- if (value === void 0 || typeof value === "function" || typeof value === "symbol") {
41815
- return arrayMember ? "null" : void 0;
41816
- }
41817
- if (value === null || typeof value === "number" || typeof value === "boolean" || typeof value === "string") {
41818
- return JSON.stringify(value);
42052
+ let records = _shapeSourceSpans.get(shape);
42053
+ if (!records) {
42054
+ records = /* @__PURE__ */ new Map();
42055
+ _shapeSourceSpans.set(shape, records);
41819
42056
  }
41820
- if (Array.isArray(value)) {
41821
- return `[${value.map((item) => stableTraceSourcePlanEncode(item, true) ?? "null").join(",")}]`;
41822
- }
41823
- const record = value;
41824
- if (record.kind === "queryOwner" && record.base) {
41825
- return stableTraceSourcePlanEncode(record.base, arrayMember);
41826
- }
41827
- const entries = Object.entries(record).sort(([left], [right]) => left.localeCompare(right));
41828
- const encodedEntries = [];
41829
- for (const [key, item] of entries) {
41830
- if (key.startsWith("_") || key === "owner" || key === "queryPropagation") continue;
41831
- const encoded = stableTraceSourcePlanEncode(item, false);
41832
- if (encoded !== void 0) encodedEntries.push(`${JSON.stringify(key)}:${encoded}`);
41833
- }
41834
- return `{${encodedEntries.join(",")}}`;
42057
+ if (records.has(record.planCacheKey)) return;
42058
+ records.set(record.planCacheKey, Object.freeze({ ...record.sourceSpan }));
41835
42059
  }
42060
+ const traceSourcePlanHasher = createStructuralHasher({
42061
+ skipKey: (key) => key.startsWith("_") || key === "owner" || key === "queryPropagation",
42062
+ unwrap: (record) => record.kind === "queryOwner" && record.base ? record.base : void 0
42063
+ });
41836
42064
  function normalizedTraceSourcePlanCacheKey(plan) {
41837
- return `shape-plan-v1:${stableTraceSourcePlanEncode(plan, false) ?? "null"}`;
42065
+ return `shape-plan-v1:${traceSourcePlanHasher(plan)}`;
41838
42066
  }
41839
42067
  function recordShapeSourceSpanInternal(shape, plan) {
41840
42068
  if (!hasActiveRuntimeSourceResolver()) return;
@@ -41854,13 +42082,20 @@ function recordShapeSourceSpanInternal(shape, plan) {
41854
42082
  }
41855
42083
  }
41856
42084
  function copyShapeSourceSpans(source, target) {
41857
- const records = cloneShapeSourceSpanRecords(_shapeSourceSpans.get(source));
41858
- if (records.length > 0) _shapeSourceSpans.set(target, records);
42085
+ const records = _shapeSourceSpans.get(source);
42086
+ if (records && records.size > 0) _shapeSourceSpans.set(target, new Map(records));
41859
42087
  }
41860
42088
  function mergeShapeSourceSpans(sources, target) {
42089
+ let records = _shapeSourceSpans.get(target);
41861
42090
  for (const source of sources) {
41862
- for (const record of _shapeSourceSpans.get(source) ?? []) {
41863
- upsertShapeSourceSpanRecord(target, record);
42091
+ const sourceRecords = _shapeSourceSpans.get(source);
42092
+ if (!sourceRecords) continue;
42093
+ if (!records) {
42094
+ records = /* @__PURE__ */ new Map();
42095
+ _shapeSourceSpans.set(target, records);
42096
+ }
42097
+ for (const [key, span] of sourceRecords) {
42098
+ if (!records.has(key)) records.set(key, span);
41864
42099
  }
41865
42100
  }
41866
42101
  }
@@ -41895,7 +42130,7 @@ function getShapeRuntimeBackendInternal(shape) {
41895
42130
  function getShapeCompilePlanInternal(shape) {
41896
42131
  const stored = _shapeCompilePlans.get(shape);
41897
42132
  if (!stored) throw new Error("Shape has no compile plan — every Shape must have an explicit plan set via setShapeCompilePlanInternal()");
41898
- return cloneShapeCompilePlan(stored);
42133
+ return stored;
41899
42134
  }
41900
42135
  function getShapePlacementRefsInternal(shape) {
41901
42136
  return clonePlacementReferences(_shapePlacementRefs.get(shape) ?? createPlacementReferences());
@@ -42587,6 +42822,30 @@ function checkLabelCollisions(operation2, plans) {
42587
42822
  seen.push(...labels);
42588
42823
  }
42589
42824
  }
42825
+ function formatDiagnosticNumber(value) {
42826
+ if (!Number.isFinite(value)) return String(value);
42827
+ const rounded = Math.abs(value) < 5e-4 ? 0 : value;
42828
+ return Number(rounded.toFixed(3)).toString();
42829
+ }
42830
+ function formatDiagnosticVec(values) {
42831
+ return `[${values.slice(0, 3).map(formatDiagnosticNumber).join(",")}]`;
42832
+ }
42833
+ function formatShapeBoundsForDiagnostic(shape) {
42834
+ try {
42835
+ const bounds = shape.boundingBox();
42836
+ return `bounds=${formatDiagnosticVec(bounds.min)}..${formatDiagnosticVec(bounds.max)}`;
42837
+ } catch (error) {
42838
+ const message = error instanceof Error ? error.message : String(error);
42839
+ return `bounds=unavailable(${message})`;
42840
+ }
42841
+ }
42842
+ function formatShapeOperandForDiagnostic(role, shape) {
42843
+ const name = _shapeReferenceNames.get(shape);
42844
+ return `${role}=${name ? `${name} ` : ""}${formatShapeBoundsForDiagnostic(shape)}`;
42845
+ }
42846
+ function formatSourceSpanForDiagnostic(sourceSpan) {
42847
+ return sourceSpan ? ` source=${sourceSpan.fileName}:${sourceSpan.line}:${sourceSpan.column}` : "";
42848
+ }
42590
42849
  function withCopiedDimensions(source, out) {
42591
42850
  setShapeDimensionsInternal(out, cloneDimensions(getShapeDimensionsInternal(source), true));
42592
42851
  setShapeGeometryInfoInternal(out, getShapeGeometryInfoInternal(source));
@@ -42601,6 +42860,7 @@ function withCopiedDimensions(source, out) {
42601
42860
  const sourceLabels = cloneFaceLabelMap(_shapeFaceLabels.get(source));
42602
42861
  if (sourceLabels) _shapeFaceLabels.set(out, sourceLabels);
42603
42862
  copyShapeReferenceMetadata(source, out);
42863
+ copyShapeLineage(source, out);
42604
42864
  copyShapeSourceSpans(source, out);
42605
42865
  return setShapeCompilePlanInternal(out, getShapeCompilePlanInternal(source));
42606
42866
  }
@@ -42630,6 +42890,7 @@ function withTransformedDimensions(source, out, m2) {
42630
42890
  const sourceLabelsT = cloneFaceLabelMap(_shapeFaceLabels.get(source));
42631
42891
  if (sourceLabelsT) _shapeFaceLabels.set(out, sourceLabelsT);
42632
42892
  copyShapeReferenceMetadata(source, out);
42893
+ copyShapeLineage(source, out);
42633
42894
  copyShapeSourceSpans(source, out);
42634
42895
  return setShapeCompilePlanInternal(out, getShapeCompilePlanInternal(source));
42635
42896
  }
@@ -43057,6 +43318,7 @@ class Shape {
43057
43318
  this.colorHex = color;
43058
43319
  setShapeRuntimeBackendInternal(this, payload);
43059
43320
  setShapeGeometryInfoInternal(this, createGeometryInfo(geometryInfo));
43321
+ _shapeLineageTokens.set(this, {});
43060
43322
  }
43061
43323
  /** @internal Use .color() instead. */
43062
43324
  setColor(value) {
@@ -43175,6 +43437,12 @@ class Shape {
43175
43437
  * with `union()` / `difference()` to avoid collisions. Collision detection throws a
43176
43438
  * clear error with a fix suggestion.
43177
43439
  *
43440
+ * Boolean survival: `union()` and `intersection()` carry labels from every operand;
43441
+ * `difference()` carries only the base (first) operand's labels — cutter labels are
43442
+ * dropped. A surviving label addresses whatever portion of its face survives the
43443
+ * boolean; cutters may split or erase it, and a lineage shared by multiple union
43444
+ * operands resolves as a face set rather than a single face.
43445
+ *
43178
43446
  * For compile-covered shapes (extrude, loft, etc.) the lookup resolves via the shape's
43179
43447
  * compile plan. As a fallback, planar-faced mesh shapes (e.g. results of boolean ops)
43180
43448
  * are resolved via coplanar triangle clustering.
@@ -43591,7 +43859,7 @@ class Shape {
43591
43859
  const tbb = s.boundingBox();
43592
43860
  return this.moveTo(tbb.min[0] + localX, tbb.min[1] + localY, tbb.min[2] + localZ);
43593
43861
  }
43594
- /** Rotate around an arbitrary axis through the origin. */
43862
+ /** 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. */
43595
43863
  rotate(axis, angleDeg, options) {
43596
43864
  validateRotateAxis(axis, "Shape.rotate()");
43597
43865
  validateRotateAngle(angleDeg, "Shape.rotate()");
@@ -43778,7 +44046,7 @@ class Shape {
43778
44046
  * Warn if a boolean operation had no geometric effect.
43779
44047
  * Compares volumes before and after; if they match within tolerance, the operation was a no-op.
43780
44048
  */
43781
- static _checkBooleanNoOp(op, base, result) {
44049
+ static _checkBooleanNoOp(op, base, result, tools = []) {
43782
44050
  try {
43783
44051
  if (op === "intersection") {
43784
44052
  if (result.isEmpty()) {
@@ -43791,8 +44059,15 @@ class Shape {
43791
44059
  const volAfter = result.volume();
43792
44060
  const tol = Math.max(volBefore * 1e-4, 1e-3);
43793
44061
  if (Math.abs(volBefore - volAfter) < tol) {
44062
+ const sourceSpan = captureRuntimeSourceSpan();
44063
+ const operandContext = [
44064
+ formatShapeOperandForDiagnostic("base", base),
44065
+ ...tools.map((tool, index2) => formatShapeOperandForDiagnostic(`tool[${index2 + 1}]`, tool))
44066
+ ].join(" ");
43794
44067
  _runtimeWarn(
43795
- `subtract() had no effect — the tool may not overlap the base shape. Base vol=${volBefore.toFixed(1)}mm³, result vol=${volAfter.toFixed(1)}mm³`
44068
+ `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}`,
44069
+ "boolean.difference.noEffect",
44070
+ sourceSpan ? { sourceSpan } : void 0
43796
44071
  );
43797
44072
  }
43798
44073
  }
@@ -43852,7 +44127,7 @@ class Shape {
43852
44127
  ),
43853
44128
  nextPlan
43854
44129
  );
43855
- Shape._checkBooleanNoOp("difference", this, resultShape);
44130
+ Shape._checkBooleanNoOp("difference", this, resultShape, shapes.slice(1));
43856
44131
  return resultShape;
43857
44132
  }
43858
44133
  /** Keep only the overlap with other shapes. Method form of intersection(). */
@@ -44255,6 +44530,11 @@ class Shape {
44255
44530
  /**
44256
44531
  * Position this shape by matching connectors to a target.
44257
44532
  *
44533
+ * Alignment: with a single connector pair, the shape translates and rotates so the connector
44534
+ * origins coincide and the axes oppose (plug-in model); `up` pins the roll. With multiple pairs,
44535
+ * the connector origins define the rigid transform — still author meaningful `axis`/`up` values
44536
+ * so the same connectors remain useful for `connect()`, audits, and future matching.
44537
+ *
44258
44538
  * Overloads:
44259
44539
  * - Single pair: `matchTo(target, selfConn, targetConn, options?)`
44260
44540
  * - Dictionary (same target): `matchTo(target, { selfConn: targetConn, ... }, options?)`
@@ -45324,117 +45604,10 @@ function buildSweepLevelSetInput(profilePolygons, pathInput, options) {
45324
45604
  edgeLength: options.edgeLength
45325
45605
  };
45326
45606
  }
45327
- const EPS = 1e-9;
45328
- function resamplePolygon(poly, targetCount) {
45329
- if (poly.length < 2) return poly;
45330
- if (targetCount <= 0) return [];
45331
- const dists = [0];
45332
- for (let i = 0; i < poly.length; i++) {
45333
- const p1 = poly[i];
45334
- const p2 = poly[(i + 1) % poly.length];
45335
- const dx = p2[0] - p1[0];
45336
- const dy = p2[1] - p1[1];
45337
- const d2 = Math.sqrt(dx * dx + dy * dy);
45338
- dists.push(dists[dists.length - 1] + d2);
45339
- }
45340
- const totalDist = dists[dists.length - 1];
45341
- if (totalDist < 1e-12) {
45342
- return Array.from({ length: targetCount }, () => [poly[0][0], poly[0][1]]);
45343
- }
45344
- const out = [];
45345
- for (let i = 0; i < targetCount; i++) {
45346
- const targetDist = i / targetCount * totalDist;
45347
- let low = 0;
45348
- let high = dists.length - 1;
45349
- while (low < high) {
45350
- const mid = low + high >> 1;
45351
- if (dists[mid] <= targetDist) {
45352
- low = mid + 1;
45353
- } else {
45354
- high = mid;
45355
- }
45356
- }
45357
- const seg = low - 1;
45358
- const t = (targetDist - dists[seg]) / (dists[seg + 1] - dists[seg]);
45359
- const p1 = poly[seg % poly.length];
45360
- const p2 = poly[(seg + 1) % poly.length];
45361
- out.push([p1[0] + (p2[0] - p1[0]) * t, p1[1] + (p2[1] - p1[1]) * t]);
45362
- }
45363
- return out;
45364
- }
45365
- function resamplePolygonByAngle(poly, targetCount, center = polygonCentroid(poly)) {
45366
- if (poly.length < 3 || targetCount <= 0) return null;
45367
- if (!isConvexPolygon(poly)) return null;
45368
- const out = [];
45369
- for (let index2 = 0; index2 < targetCount; index2 += 1) {
45370
- const angle = index2 / targetCount * Math.PI * 2;
45371
- const point = rayPolygonIntersection(center, [Math.cos(angle), Math.sin(angle)], poly);
45372
- if (!point) return null;
45373
- out.push(point);
45374
- }
45375
- return out;
45376
- }
45377
- function rayPolygonIntersection(origin, direction, poly) {
45378
- let bestT = Infinity;
45379
- let best = null;
45380
- for (let index2 = 0; index2 < poly.length; index2 += 1) {
45381
- const a2 = poly[index2];
45382
- const b = poly[(index2 + 1) % poly.length];
45383
- const edge = [b[0] - a2[0], b[1] - a2[1]];
45384
- const denom = cross$1(direction, edge);
45385
- if (Math.abs(denom) < EPS) continue;
45386
- const delta = [a2[0] - origin[0], a2[1] - origin[1]];
45387
- const rayT = cross$1(delta, edge) / denom;
45388
- const edgeT = cross$1(delta, direction) / denom;
45389
- if (rayT >= -EPS && edgeT >= -EPS && edgeT <= 1 + EPS && rayT < bestT) {
45390
- bestT = rayT;
45391
- best = [origin[0] + direction[0] * rayT, origin[1] + direction[1] * rayT];
45392
- }
45393
- }
45394
- return best;
45395
- }
45396
- function polygonCentroid(poly) {
45397
- let area2 = 0;
45398
- let cx = 0;
45399
- let cy = 0;
45400
- for (let index2 = 0; index2 < poly.length; index2 += 1) {
45401
- const a2 = poly[index2];
45402
- const b = poly[(index2 + 1) % poly.length];
45403
- const crossValue = cross$1(a2, b);
45404
- area2 += crossValue;
45405
- cx += (a2[0] + b[0]) * crossValue;
45406
- cy += (a2[1] + b[1]) * crossValue;
45407
- }
45408
- if (Math.abs(area2) < EPS) return averagePoint(poly);
45409
- return [cx / (3 * area2), cy / (3 * area2)];
45410
- }
45411
- function averagePoint(poly) {
45412
- let x2 = 0;
45413
- let y2 = 0;
45414
- for (const point of poly) {
45415
- x2 += point[0];
45416
- y2 += point[1];
45417
- }
45418
- return [x2 / poly.length, y2 / poly.length];
45419
- }
45420
- function isConvexPolygon(poly) {
45421
- let sign = 0;
45422
- for (let index2 = 0; index2 < poly.length; index2 += 1) {
45423
- const a2 = poly[index2];
45424
- const b = poly[(index2 + 1) % poly.length];
45425
- const c2 = poly[(index2 + 2) % poly.length];
45426
- const turn = cross$1([b[0] - a2[0], b[1] - a2[1]], [c2[0] - b[0], c2[1] - b[1]]);
45427
- if (Math.abs(turn) < EPS) continue;
45428
- const currentSign = Math.sign(turn);
45429
- if (sign !== 0 && currentSign !== sign) return false;
45430
- sign = currentSign;
45431
- }
45432
- return sign !== 0;
45433
- }
45434
- function cross$1(a2, b) {
45435
- return a2[0] * b[1] - a2[1] * b[0];
45436
- }
45437
- function loftStitched(profiles, heights, wasm) {
45607
+ const CORNER_TURN_DEG = 30;
45608
+ const SPAN_ANGLE_PER_RING_DEG = 3;
45609
+ const MAX_SPAN_SUBDIVISION = 24;
45610
+ function loftStitched(profiles, heights, wasm, options = {}) {
45438
45611
  if (profiles.length < 2) return null;
45439
45612
  const classified = profiles.map((loops) => classifyLoops(loops));
45440
45613
  const outerCount = classified[0].outers.length;
@@ -45449,7 +45622,7 @@ function loftStitched(profiles, heights, wasm) {
45449
45622
  const holeGroups = holeCount > 0 ? matchLoopsAcrossProfiles(classified.map((c2) => c2.holes)) : [];
45450
45623
  const outerSolids = [];
45451
45624
  for (const group of outerGroups) {
45452
- const solid = stitchSingleLoopLoft(group, heights, wasm);
45625
+ const solid = stitchSingleLoopLoft(group, heights, wasm, options);
45453
45626
  if (!solid) {
45454
45627
  for (const s of outerSolids) s.delete();
45455
45628
  return null;
@@ -45466,7 +45639,7 @@ function loftStitched(profiles, heights, wasm) {
45466
45639
  if (holeGroups.length > 0) {
45467
45640
  const holeSolids = [];
45468
45641
  for (const group of holeGroups) {
45469
- const solid = stitchSingleLoopLoft(group, heights, wasm);
45642
+ const solid = stitchSingleLoopLoft(group, heights, wasm, options);
45470
45643
  if (!solid) {
45471
45644
  result.delete();
45472
45645
  for (const s of holeSolids) s.delete();
@@ -45552,68 +45725,564 @@ function signedArea$1(loop) {
45552
45725
  }
45553
45726
  return area * 0.5;
45554
45727
  }
45555
- function stitchSingleLoopLoft(loops, heights, wasm) {
45728
+ function detectCorners(loop) {
45729
+ const corners = [];
45730
+ const n = loop.length;
45731
+ const threshold = CORNER_TURN_DEG * Math.PI / 180;
45732
+ for (let i = 0; i < n; i++) {
45733
+ const prev = loop[(i - 1 + n) % n];
45734
+ const curr = loop[i];
45735
+ const next = loop[(i + 1) % n];
45736
+ const ax = curr[0] - prev[0];
45737
+ const ay = curr[1] - prev[1];
45738
+ const bx = next[0] - curr[0];
45739
+ const by = next[1] - curr[1];
45740
+ const la = Math.hypot(ax, ay);
45741
+ const lb = Math.hypot(bx, by);
45742
+ if (la < 1e-12 || lb < 1e-12) continue;
45743
+ const dot2 = (ax * bx + ay * by) / (la * lb);
45744
+ const turn = Math.acos(Math.min(1, Math.max(-1, dot2)));
45745
+ if (turn > threshold) corners.push(i);
45746
+ }
45747
+ return corners;
45748
+ }
45749
+ function cumulativeArcLength(loop) {
45750
+ const dists = [0];
45751
+ for (let i = 0; i < loop.length; i++) {
45752
+ const p1 = loop[i];
45753
+ const p2 = loop[(i + 1) % loop.length];
45754
+ dists.push(dists[i] + Math.hypot(p2[0] - p1[0], p2[1] - p1[1]));
45755
+ }
45756
+ return { dists, total: dists[loop.length] };
45757
+ }
45758
+ function pointAtArcLength(loop, dists, total, s) {
45759
+ if (total < 1e-12) return [loop[0][0], loop[0][1]];
45760
+ let target = s % total;
45761
+ if (target < 0) target += total;
45762
+ let low = 0;
45763
+ let high = dists.length - 1;
45764
+ while (low < high) {
45765
+ const mid = low + high >> 1;
45766
+ if (dists[mid] <= target) low = mid + 1;
45767
+ else high = mid;
45768
+ }
45769
+ const seg = low - 1;
45770
+ const segLen = dists[seg + 1] - dists[seg];
45771
+ const t = segLen < 1e-12 ? 0 : (target - dists[seg]) / segLen;
45772
+ const p1 = loop[seg % loop.length];
45773
+ const p2 = loop[(seg + 1) % loop.length];
45774
+ return [p1[0] + (p2[0] - p1[0]) * t, p1[1] + (p2[1] - p1[1]) * t];
45775
+ }
45776
+ function refineParams(params, rangeEnd, maxGap) {
45777
+ if (!Number.isFinite(maxGap)) return params;
45778
+ const out = [];
45779
+ for (let i = 0; i < params.length; i++) {
45780
+ const a2 = params[i];
45781
+ const b = i + 1 < params.length ? params[i + 1] : rangeEnd;
45782
+ out.push(a2);
45783
+ const gap = b - a2;
45784
+ if (gap > maxGap) {
45785
+ const pieces = Math.min(64, Math.ceil(gap / maxGap));
45786
+ for (let k2 = 1; k2 < pieces; k2++) out.push(a2 + gap * k2 / pieces);
45787
+ }
45788
+ }
45789
+ return out;
45790
+ }
45791
+ function turnAngle(prev, curr, next) {
45792
+ const ax = curr[0] - prev[0];
45793
+ const ay = curr[1] - prev[1];
45794
+ const bx = next[0] - curr[0];
45795
+ const by = next[1] - curr[1];
45796
+ const la = Math.hypot(ax, ay);
45797
+ const lb = Math.hypot(bx, by);
45798
+ if (la < 1e-12 || lb < 1e-12) return 0;
45799
+ const dot2 = (ax * bx + ay * by) / (la * lb);
45800
+ return Math.acos(Math.min(1, Math.max(-1, dot2)));
45801
+ }
45802
+ function maxTurnPerColumn(rings) {
45803
+ const N = rings[0].length;
45804
+ const out = new Float64Array(N);
45805
+ for (const ring of rings) {
45806
+ for (let j = 0; j < N; j++) {
45807
+ const turn = turnAngle(ring[(j - 1 + N) % N], ring[j], ring[(j + 1) % N]);
45808
+ if (turn > out[j]) out[j] = turn;
45809
+ }
45810
+ }
45811
+ return out;
45812
+ }
45813
+ const CURVE_TURN_EPS = 0.5 * Math.PI / 180;
45814
+ function maxQuadDeviation(rings, heights, colA, colB) {
45815
+ let worst = 0;
45816
+ for (let i = 0; i < rings.length - 1; i++) {
45817
+ const a2 = [rings[i][colA][0], rings[i][colA][1], heights[i]];
45818
+ const b = [rings[i][colB][0], rings[i][colB][1], heights[i]];
45819
+ const c2 = [rings[i + 1][colB][0], rings[i + 1][colB][1], heights[i + 1]];
45820
+ const d2 = [rings[i + 1][colA][0], rings[i + 1][colA][1], heights[i + 1]];
45821
+ const n = cross3$1(sub3(b, a2), sub3(d2, a2));
45822
+ const len = Math.hypot(n[0], n[1], n[2]);
45823
+ if (len < 1e-12) continue;
45824
+ const deviation = Math.abs((n[0] * (c2[0] - a2[0]) + n[1] * (c2[1] - a2[1]) + n[2] * (c2[2] - a2[2])) / len);
45825
+ if (deviation > worst) worst = deviation;
45826
+ }
45827
+ return worst;
45828
+ }
45829
+ function buildCompatibleRings(loops, heights, edgeLength2) {
45830
+ const cornerSets = loops.map(detectCorners);
45831
+ const cornerCount = cornerSets[0].length;
45832
+ const cornersMatch = cornerCount > 0 && cornerSets.every((c2) => c2.length === cornerCount);
45833
+ if (cornersMatch) {
45834
+ const anchored = buildCornerAnchoredRings(loops, heights, cornerSets, edgeLength2);
45835
+ if (anchored) return anchored;
45836
+ }
45837
+ return buildSeamAlignedRings(loops, heights, edgeLength2);
45838
+ }
45839
+ function buildCornerAnchoredRings(loops, heights, cornerSets, edgeLength2) {
45840
+ const cornerCount = cornerSets[0].length;
45841
+ const arcs = loops.map((loop) => cumulativeArcLength(loop));
45842
+ const alignedCorners = [cornerSets[0]];
45843
+ for (let i = 1; i < loops.length; i++) {
45844
+ const prev = alignedCorners[i - 1];
45845
+ const prevLoop = loops[i - 1];
45846
+ const curr = cornerSets[i];
45847
+ const loop = loops[i];
45848
+ let best = curr;
45849
+ let bestCost = Infinity;
45850
+ for (let r = 0; r < cornerCount; r++) {
45851
+ const rotated = curr.map((_2, k2) => curr[(k2 + r) % cornerCount]);
45852
+ let cost = 0;
45853
+ for (let k2 = 0; k2 < cornerCount; k2++) {
45854
+ const a2 = prevLoop[prev[k2]];
45855
+ const b = loop[rotated[k2]];
45856
+ cost += (a2[0] - b[0]) ** 2 + (a2[1] - b[1]) ** 2;
45857
+ }
45858
+ if (cost < bestCost) {
45859
+ bestCost = cost;
45860
+ best = rotated;
45861
+ }
45862
+ }
45863
+ alignedCorners.push(best);
45864
+ }
45865
+ const segParams = [];
45866
+ const segLengths = [];
45867
+ for (let i = 0; i < loops.length; i++) {
45868
+ const loop = loops[i];
45869
+ const { dists, total } = arcs[i];
45870
+ if (total < 1e-9) return null;
45871
+ const corners = alignedCorners[i];
45872
+ const stationSegs = [];
45873
+ const stationLens = [];
45874
+ for (let s = 0; s < cornerCount; s++) {
45875
+ const from = corners[s];
45876
+ const to = corners[(s + 1) % cornerCount];
45877
+ const start = dists[from];
45878
+ let end = dists[to];
45879
+ if (to <= from) end += total;
45880
+ const len = end - start;
45881
+ if (len < 1e-9) return null;
45882
+ const interior = [];
45883
+ const n = loop.length;
45884
+ for (let v = (from + 1) % n; v !== to; v = (v + 1) % n) {
45885
+ let d2 = dists[v];
45886
+ if (d2 < start) d2 += total;
45887
+ const p2 = (d2 - start) / len;
45888
+ if (p2 > 1e-9 && p2 < 1 - 1e-9) interior.push(p2);
45889
+ }
45890
+ stationSegs.push(interior);
45891
+ stationLens.push(len);
45892
+ }
45893
+ segParams.push(stationSegs);
45894
+ segLengths.push(stationLens);
45895
+ }
45896
+ let masterParams = [];
45897
+ for (let s = 0; s < cornerCount; s++) {
45898
+ let bestStation = 0;
45899
+ for (let i = 1; i < loops.length; i++) {
45900
+ if (segParams[i][s].length > segParams[bestStation][s].length) bestStation = i;
45901
+ }
45902
+ masterParams.push([0, ...segParams[bestStation][s]]);
45903
+ }
45904
+ const sampleRings = (paramsBySegment) => {
45905
+ const rings2 = [];
45906
+ for (let i = 0; i < loops.length; i++) {
45907
+ const loop = loops[i];
45908
+ const { dists, total } = arcs[i];
45909
+ const corners = alignedCorners[i];
45910
+ const ring = [];
45911
+ for (let s = 0; s < cornerCount; s++) {
45912
+ const from = corners[s];
45913
+ const to = corners[(s + 1) % cornerCount];
45914
+ const start = dists[from];
45915
+ let end = dists[to];
45916
+ if (to <= from) end += total;
45917
+ const len = end - start;
45918
+ for (const p2 of paramsBySegment[s]) {
45919
+ if (p2 === 0) {
45920
+ ring.push([loop[from][0], loop[from][1]]);
45921
+ } else {
45922
+ ring.push(pointAtArcLength(loop, dists, total, start + p2 * len));
45923
+ }
45924
+ }
45925
+ }
45926
+ rings2.push(ring);
45927
+ }
45928
+ return rings2;
45929
+ };
45930
+ let rings = sampleRings(masterParams);
45931
+ if (edgeLength2 && edgeLength2 > 0) {
45932
+ const ringLength = rings[0].length;
45933
+ const turns = maxTurnPerColumn(rings);
45934
+ const segStart = [];
45935
+ {
45936
+ let col = 0;
45937
+ for (let s = 0; s < cornerCount; s++) {
45938
+ segStart.push(col);
45939
+ col += masterParams[s].length;
45940
+ }
45941
+ }
45942
+ let changed = false;
45943
+ const refined = [];
45944
+ for (let s = 0; s < cornerCount; s++) {
45945
+ const params = masterParams[s];
45946
+ let maxLen = 0;
45947
+ for (let i = 0; i < loops.length; i++) maxLen = Math.max(maxLen, segLengths[i][s]);
45948
+ const out = [];
45949
+ for (let k2 = 0; k2 < params.length; k2++) {
45950
+ const a2 = params[k2];
45951
+ const b = k2 + 1 < params.length ? params[k2 + 1] : 1;
45952
+ out.push(a2);
45953
+ const gapLen = (b - a2) * maxLen;
45954
+ if (gapLen <= edgeLength2) continue;
45955
+ const colA = segStart[s] + k2;
45956
+ const colB = k2 + 1 < params.length ? colA + 1 : segStart[(s + 1) % cornerCount] % ringLength;
45957
+ const curved = k2 > 0 && turns[colA] > CURVE_TURN_EPS || k2 + 1 < params.length && turns[colB] > CURVE_TURN_EPS;
45958
+ const twisted = !curved && maxQuadDeviation(rings, heights, colA, colB) > edgeLength2 * 0.05;
45959
+ if (!curved && !twisted) continue;
45960
+ const pieces = Math.min(64, Math.ceil(gapLen / edgeLength2));
45961
+ for (let p2 = 1; p2 < pieces; p2++) out.push(a2 + (b - a2) * p2 / pieces);
45962
+ changed = true;
45963
+ }
45964
+ refined.push(out);
45965
+ }
45966
+ if (changed) {
45967
+ masterParams = refined;
45968
+ rings = sampleRings(masterParams);
45969
+ }
45970
+ }
45971
+ const cornerColumns = [];
45972
+ {
45973
+ let col = 0;
45974
+ for (let s = 0; s < cornerCount; s++) {
45975
+ cornerColumns.push(col);
45976
+ col += masterParams[s].length;
45977
+ }
45978
+ }
45979
+ return { rings, cornerColumns };
45980
+ }
45981
+ function buildSeamAlignedRings(loops, _heights, edgeLength2) {
45982
+ const arcs = loops.map((loop) => cumulativeArcLength(loop));
45983
+ let bestStation = 0;
45984
+ for (let i = 1; i < loops.length; i++) {
45985
+ if (loops[i].length > loops[bestStation].length) bestStation = i;
45986
+ }
45987
+ const { dists: masterDists, total: masterTotal } = arcs[bestStation];
45988
+ if (masterTotal < 1e-9) return null;
45989
+ let params = loops[bestStation].map((_2, v) => masterDists[v] / masterTotal);
45990
+ if (params.length < 24) {
45991
+ params = refineParams(params, 1, 1 / 24);
45992
+ }
45993
+ const sampleRings = (paramList) => loops.map((loop, i) => {
45994
+ const { dists, total } = arcs[i];
45995
+ if (total < 1e-9) {
45996
+ return paramList.map(() => [loop[0][0], loop[0][1]]);
45997
+ }
45998
+ return paramList.map((p2) => pointAtArcLength(loop, dists, total, p2 * total));
45999
+ });
46000
+ let rings = sampleRings(params);
46001
+ if (edgeLength2 && edgeLength2 > 0) {
46002
+ let maxPerimeter = 0;
46003
+ for (const { total } of arcs) maxPerimeter = Math.max(maxPerimeter, total);
46004
+ const turns = maxTurnPerColumn(rings);
46005
+ const N0 = params.length;
46006
+ const out = [];
46007
+ let changed = false;
46008
+ for (let k2 = 0; k2 < N0; k2++) {
46009
+ const a2 = params[k2];
46010
+ const b = k2 + 1 < N0 ? params[k2 + 1] : 1;
46011
+ out.push(a2);
46012
+ const gapLen = (b - a2) * maxPerimeter;
46013
+ if (gapLen <= edgeLength2) continue;
46014
+ if (turns[k2] <= CURVE_TURN_EPS && turns[(k2 + 1) % N0] <= CURVE_TURN_EPS) continue;
46015
+ const pieces = Math.min(64, Math.ceil(gapLen / edgeLength2));
46016
+ for (let p2 = 1; p2 < pieces; p2++) out.push(a2 + (b - a2) * p2 / pieces);
46017
+ changed = true;
46018
+ }
46019
+ if (changed) {
46020
+ params = out;
46021
+ rings = sampleRings(params);
46022
+ }
46023
+ }
46024
+ const N = params.length;
46025
+ for (let i = 1; i < rings.length; i++) {
46026
+ const prev = rings[i - 1];
46027
+ const curr = rings[i];
46028
+ let bestShift = 0;
46029
+ let bestCost = Infinity;
46030
+ for (let shift = 0; shift < N; shift++) {
46031
+ let cost = 0;
46032
+ for (let j = 0; j < N; j++) {
46033
+ const a2 = prev[j];
46034
+ const b = curr[(j + shift) % N];
46035
+ cost += (a2[0] - b[0]) ** 2 + (a2[1] - b[1]) ** 2;
46036
+ if (cost >= bestCost) break;
46037
+ }
46038
+ if (cost < bestCost) {
46039
+ bestCost = cost;
46040
+ bestShift = shift;
46041
+ }
46042
+ }
46043
+ if (bestShift !== 0) {
46044
+ rings[i] = curr.map((_2, j) => curr[(j + bestShift) % N]);
46045
+ }
46046
+ }
46047
+ return { rings, cornerColumns: [] };
46048
+ }
46049
+ function buildSpanRows(rings, heights) {
46050
+ const R = rings.length;
46051
+ const N = rings[0].length;
46052
+ const stations = rings.map((ring, i) => ring.map(([x2, y2]) => [x2, y2, heights[i]]));
46053
+ const t = [0];
46054
+ for (let i = 0; i < R - 1; i++) {
46055
+ let sum2 = 0;
46056
+ for (let j = 0; j < N; j++) {
46057
+ sum2 += dist3(stations[i][j], stations[i + 1][j]);
46058
+ }
46059
+ const avg = Math.max(sum2 / N, 1e-9);
46060
+ t.push(t[i] + Math.sqrt(avg));
46061
+ }
46062
+ const tangents = [];
46063
+ for (let i = 0; i < R; i++) {
46064
+ const row = [];
46065
+ for (let j = 0; j < N; j++) {
46066
+ row.push(stationTangent(stations, t, i, j));
46067
+ }
46068
+ tangents.push(row);
46069
+ }
46070
+ const rows = [{ points: stations[0], tangents: tangents[0], isStation: true }];
46071
+ for (let i = 0; i < R - 1; i++) {
46072
+ const h = t[i + 1] - t[i];
46073
+ let maxAngle = 0;
46074
+ const stride = Math.max(1, Math.floor(N / 16));
46075
+ for (let j = 0; j < N; j += stride) {
46076
+ maxAngle = Math.max(maxAngle, angleBetween(tangents[i][j], tangents[i + 1][j]));
46077
+ }
46078
+ const k2 = Math.min(MAX_SPAN_SUBDIVISION, Math.max(1, Math.ceil(maxAngle / SPAN_ANGLE_PER_RING_DEG)));
46079
+ for (let s = 1; s < k2; s++) {
46080
+ const u2 = s / k2;
46081
+ const points = [];
46082
+ const rowTangents = [];
46083
+ for (let j = 0; j < N; j++) {
46084
+ const { point, tangent } = hermite(stations[i][j], tangents[i][j], stations[i + 1][j], tangents[i + 1][j], h, u2);
46085
+ points.push(point);
46086
+ rowTangents.push(tangent);
46087
+ }
46088
+ rows.push({ points, tangents: rowTangents, isStation: false });
46089
+ }
46090
+ rows.push({ points: stations[i + 1], tangents: tangents[i + 1], isStation: true });
46091
+ }
46092
+ return rows;
46093
+ }
46094
+ function stationTangent(stations, t, i, j) {
46095
+ const R = stations.length;
46096
+ if (i === 0) {
46097
+ return scale3(sub3(stations[1][j], stations[0][j]), 1 / (t[1] - t[0]));
46098
+ }
46099
+ if (i === R - 1) {
46100
+ return scale3(sub3(stations[R - 1][j], stations[R - 2][j]), 1 / (t[R - 1] - t[R - 2]));
46101
+ }
46102
+ const hPrev = t[i] - t[i - 1];
46103
+ const hNext = t[i + 1] - t[i];
46104
+ const dPrev = scale3(sub3(stations[i][j], stations[i - 1][j]), 1 / hPrev);
46105
+ const dNext = scale3(sub3(stations[i + 1][j], stations[i][j]), 1 / hNext);
46106
+ return scale3(add3(scale3(dPrev, hNext), scale3(dNext, hPrev)), 1 / (hPrev + hNext));
46107
+ }
46108
+ function hermite(p0, m0, p1, m1, h, u2) {
46109
+ const u22 = u2 * u2;
46110
+ const u3 = u22 * u2;
46111
+ const h00 = 2 * u3 - 3 * u22 + 1;
46112
+ const h10 = u3 - 2 * u22 + u2;
46113
+ const h01 = -2 * u3 + 3 * u22;
46114
+ const h11 = u3 - u22;
46115
+ const d00 = 6 * u22 - 6 * u2;
46116
+ const d10 = 3 * u22 - 4 * u2 + 1;
46117
+ const d01 = -6 * u22 + 6 * u2;
46118
+ const d11 = 3 * u22 - 2 * u2;
46119
+ const point = [
46120
+ h00 * p0[0] + h10 * h * m0[0] + h01 * p1[0] + h11 * h * m1[0],
46121
+ h00 * p0[1] + h10 * h * m0[1] + h01 * p1[1] + h11 * h * m1[1],
46122
+ h00 * p0[2] + h10 * h * m0[2] + h01 * p1[2] + h11 * h * m1[2]
46123
+ ];
46124
+ const tangent = [
46125
+ d00 * p0[0] / h + d10 * m0[0] + d01 * p1[0] / h + d11 * m1[0],
46126
+ d00 * p0[1] / h + d10 * m0[1] + d01 * p1[1] / h + d11 * m1[1],
46127
+ d00 * p0[2] / h + d10 * m0[2] + d01 * p1[2] / h + d11 * m1[2]
46128
+ ];
46129
+ return { point, tangent };
46130
+ }
46131
+ function stitchSingleLoopLoft(loops, heights, wasm, options) {
45556
46132
  const normalizedLoops = loops.map((loop) => {
45557
46133
  const area = signedArea$1(loop);
45558
46134
  return area < 0 ? [...loop].reverse() : loop;
45559
46135
  });
45560
- let maxPoints = 0;
45561
- for (const loop of normalizedLoops) {
45562
- maxPoints = Math.max(maxPoints, loop.length);
45563
- }
45564
- const N = Math.max(maxPoints, 24);
45565
- const angularSamples = normalizedLoops.map((loop) => resamplePolygonByAngle(loop, N));
45566
- const useAngularSamples = angularSamples.every((samples) => samples != null);
45567
- const resampled = normalizedLoops.map((loop, i) => {
45568
- const pts2d = useAngularSamples ? angularSamples[i] : resamplePolygon(loop, N);
45569
- const z2 = heights[i];
45570
- return pts2d.map(([x2, y2]) => [x2, y2, z2]);
45571
- });
45572
- const vertices = [];
45573
- const triangles = [];
45574
- for (const layer of resampled) {
45575
- for (const [x2, y2, z2] of layer) {
45576
- vertices.push(x2, y2, z2);
46136
+ const compatible = buildCompatibleRings(normalizedLoops, heights, options.edgeLength);
46137
+ if (!compatible) return null;
46138
+ const { rings, cornerColumns } = compatible;
46139
+ const N = rings[0].length;
46140
+ if (N < 3) return null;
46141
+ const rows = buildSpanRows(rings, heights);
46142
+ const R = rows.length;
46143
+ const cornerSet = new Set(cornerColumns);
46144
+ const vertProps = [];
46145
+ let vertCount = 0;
46146
+ const fwdIdx = [];
46147
+ const bwdIdx = [];
46148
+ const pushVert = (p2, n) => {
46149
+ vertProps.push(p2[0], p2[1], p2[2], n[0], n[1], n[2]);
46150
+ return vertCount++;
46151
+ };
46152
+ for (let r = 0; r < R; r++) {
46153
+ const { points, tangents } = rows[r];
46154
+ const fwd = new Array(N);
46155
+ const bwd = new Array(N);
46156
+ for (let j = 0; j < N; j++) {
46157
+ const prev = points[(j - 1 + N) % N];
46158
+ const curr = points[j];
46159
+ const next = points[(j + 1) % N];
46160
+ if (cornerSet.has(j)) {
46161
+ const nFwd = surfaceNormal(sub3(next, curr), tangents[j]);
46162
+ const nBwd = surfaceNormal(sub3(curr, prev), tangents[j]);
46163
+ fwd[j] = pushVert(curr, nFwd);
46164
+ bwd[j] = pushVert(curr, nBwd);
46165
+ } else {
46166
+ const idx = pushVert(curr, surfaceNormal(sub3(next, prev), tangents[j]));
46167
+ fwd[j] = idx;
46168
+ bwd[j] = idx;
46169
+ }
45577
46170
  }
46171
+ fwdIdx.push(fwd);
46172
+ bwdIdx.push(bwd);
45578
46173
  }
45579
- for (let i = 0; i < resampled.length - 1; i++) {
45580
- const baseIdx = i * N;
45581
- const nextIdx = (i + 1) * N;
46174
+ const triangles = [];
46175
+ for (let r = 0; r < R - 1; r++) {
45582
46176
  for (let j = 0; j < N; j++) {
45583
46177
  const j1 = (j + 1) % N;
45584
- const v0 = baseIdx + j;
45585
- const v1 = nextIdx + j;
45586
- const v2 = nextIdx + j1;
45587
- const v3 = baseIdx + j1;
46178
+ const v0 = fwdIdx[r][j];
46179
+ const v3 = bwdIdx[r][j1];
46180
+ const v2 = bwdIdx[r + 1][j1];
46181
+ const v1 = fwdIdx[r + 1][j];
45588
46182
  triangles.push(v0, v3, v2);
45589
46183
  triangles.push(v0, v2, v1);
45590
46184
  }
45591
46185
  }
45592
- const bottomResampled2D = resampled[0].map(([x2, y2]) => [x2, y2]);
45593
- const bottomTrisResampled = wasm.triangulate([bottomResampled2D]);
45594
- for (const tri of bottomTrisResampled) {
46186
+ const bottomRing = rows[0].points;
46187
+ const topRing = rows[R - 1].points;
46188
+ const bottom2D = bottomRing.map(([x2, y2]) => [x2, y2]);
46189
+ const bottomTris = wasm.triangulate([bottom2D]);
46190
+ const bottomBase = vertCount;
46191
+ for (const p2 of bottomRing) pushVert(p2, [0, 0, -1]);
46192
+ for (const tri of bottomTris) {
45595
46193
  const [v0, v1, v2] = Array.isArray(tri) ? tri : [tri[0], tri[1], tri[2]];
45596
- triangles.push(v0, v2, v1);
46194
+ triangles.push(bottomBase + v0, bottomBase + v2, bottomBase + v1);
45597
46195
  }
45598
- const topResampled2D = resampled[resampled.length - 1].map(([x2, y2]) => [x2, y2]);
45599
- const topTrisResampled = wasm.triangulate([topResampled2D]);
45600
- const topStartIdx = (resampled.length - 1) * N;
45601
- for (const tri of topTrisResampled) {
46196
+ const top2D = topRing.map(([x2, y2]) => [x2, y2]);
46197
+ const topTris = wasm.triangulate([top2D]);
46198
+ const topBase = vertCount;
46199
+ for (const p2 of topRing) pushVert(p2, [0, 0, 1]);
46200
+ for (const tri of topTris) {
45602
46201
  const [v0, v1, v2] = Array.isArray(tri) ? tri : [tri[0], tri[1], tri[2]];
45603
- triangles.push(topStartIdx + v0, topStartIdx + v1, topStartIdx + v2);
46202
+ triangles.push(topBase + v0, topBase + v1, topBase + v2);
45604
46203
  }
45605
46204
  const mesh = new wasm.Mesh({
45606
- numProp: 3,
45607
- vertProperties: new Float32Array(vertices),
46205
+ numProp: 6,
46206
+ vertProperties: new Float32Array(vertProps),
45608
46207
  triVerts: new Uint32Array(triangles)
45609
46208
  });
45610
46209
  try {
46210
+ mesh.merge();
45611
46211
  const manifold = new wasm.Manifold(mesh);
45612
46212
  return manifold;
45613
46213
  } catch (_e2) {
45614
46214
  return null;
45615
46215
  }
45616
46216
  }
46217
+ function surfaceNormal(chord, span) {
46218
+ const n = cross3$1(chord, span);
46219
+ const len = Math.hypot(n[0], n[1], n[2]);
46220
+ if (len < 1e-12) {
46221
+ const radial = Math.hypot(chord[0], chord[1]);
46222
+ if (radial > 1e-12) return [chord[1] / radial, -chord[0] / radial, 0];
46223
+ return [0, 0, 1];
46224
+ }
46225
+ return [n[0] / len, n[1] / len, n[2] / len];
46226
+ }
46227
+ function sub3(a2, b) {
46228
+ return [a2[0] - b[0], a2[1] - b[1], a2[2] - b[2]];
46229
+ }
46230
+ function add3(a2, b) {
46231
+ return [a2[0] + b[0], a2[1] + b[1], a2[2] + b[2]];
46232
+ }
46233
+ function scale3(a2, s) {
46234
+ return [a2[0] * s, a2[1] * s, a2[2] * s];
46235
+ }
46236
+ function cross3$1(a2, b) {
46237
+ 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]];
46238
+ }
46239
+ function dist3(a2, b) {
46240
+ return Math.hypot(a2[0] - b[0], a2[1] - b[1], a2[2] - b[2]);
46241
+ }
46242
+ function angleBetween(a2, b) {
46243
+ const la = Math.hypot(a2[0], a2[1], a2[2]);
46244
+ const lb = Math.hypot(b[0], b[1], b[2]);
46245
+ if (la < 1e-12 || lb < 1e-12) return 0;
46246
+ const dot2 = (a2[0] * b[0] + a2[1] * b[1] + a2[2] * b[2]) / (la * lb);
46247
+ return Math.acos(Math.min(1, Math.max(-1, dot2))) * 180 / Math.PI;
46248
+ }
46249
+ function resamplePolygon(poly, targetCount) {
46250
+ if (poly.length < 2) return poly;
46251
+ if (targetCount <= 0) return [];
46252
+ const dists = [0];
46253
+ for (let i = 0; i < poly.length; i++) {
46254
+ const p1 = poly[i];
46255
+ const p2 = poly[(i + 1) % poly.length];
46256
+ const dx = p2[0] - p1[0];
46257
+ const dy = p2[1] - p1[1];
46258
+ const d2 = Math.sqrt(dx * dx + dy * dy);
46259
+ dists.push(dists[dists.length - 1] + d2);
46260
+ }
46261
+ const totalDist = dists[dists.length - 1];
46262
+ if (totalDist < 1e-12) {
46263
+ return Array.from({ length: targetCount }, () => [poly[0][0], poly[0][1]]);
46264
+ }
46265
+ const out = [];
46266
+ for (let i = 0; i < targetCount; i++) {
46267
+ const targetDist = i / targetCount * totalDist;
46268
+ let low = 0;
46269
+ let high = dists.length - 1;
46270
+ while (low < high) {
46271
+ const mid = low + high >> 1;
46272
+ if (dists[mid] <= targetDist) {
46273
+ low = mid + 1;
46274
+ } else {
46275
+ high = mid;
46276
+ }
46277
+ }
46278
+ const seg = low - 1;
46279
+ const t = (targetDist - dists[seg]) / (dists[seg + 1] - dists[seg]);
46280
+ const p1 = poly[seg % poly.length];
46281
+ const p2 = poly[(seg + 1) % poly.length];
46282
+ out.push([p1[0] + (p2[0] - p1[0]) * t, p1[1] + (p2[1] - p1[1]) * t]);
46283
+ }
46284
+ return out;
46285
+ }
45617
46286
  function sweepStitched(profilePolygons, pathPoints, up, wasm) {
45618
46287
  if (pathPoints.length < 2) return null;
45619
46288
  if (profilePolygons.length === 0) return null;
@@ -46311,7 +46980,7 @@ function lowerOffsetLoftCompilePlan(plan, thickness, wasm) {
46311
46980
  throw new Error("offsetSolid() collapsed the compatible-loft height span.");
46312
46981
  }
46313
46982
  const offsetPolygons = plan.profiles.map((profile) => offsetProfilePolygonsForManifold(profile, thickness, wasm));
46314
- const stitched = loftStitched(offsetPolygons, heights, wasm);
46983
+ const stitched = loftStitched(offsetPolygons, heights, wasm, { edgeLength: plan.edgeLength });
46315
46984
  if (!stitched) {
46316
46985
  throw new Error(`Offset solid requires the OCCT backend. ${OCCT_BACKEND_REQUIRED_HINT}`);
46317
46986
  }
@@ -46446,12 +47115,12 @@ function lowerShapeLoftCompilePlan(plan, wasm) {
46446
47115
  disposeWasmObject(crossSection);
46447
47116
  }
46448
47117
  });
46449
- if (inputPolygons.length >= 2) {
46450
- const stitched = loftStitched(inputPolygons, plan.heights, wasm);
47118
+ if (!plan.forceField && inputPolygons.length >= 2) {
47119
+ const stitched = loftStitched(inputPolygons, plan.heights, wasm, { edgeLength: plan.edgeLength });
46451
47120
  if (stitched) return stitched;
46452
47121
  }
46453
47122
  const input = buildLoftLevelSetInput(inputPolygons, plan.heights, { edgeLength: plan.edgeLength, boundsPadding: plan.boundsPadding });
46454
- return lowerSdfToManifold(levelSetFieldToStandardSdf3(input.sdf), input.bounds, input.edgeLength, wasm);
47123
+ return lowerSdfToManifold(levelSetFieldToStandardSdf3(input.sdf), input.bounds, input.edgeLength, wasm, plan.meshing);
46455
47124
  }
46456
47125
  function lowerShapeSweepCompilePlan(plan, wasm) {
46457
47126
  const crossSection = lowerProfileCompilePlanToCrossSection(plan.profile, wasm);
@@ -46716,7 +47385,7 @@ function lowerFromSlicesToManifold(plan, wasm) {
46716
47385
  }
46717
47386
  });
46718
47387
  const heights = sorted.map((s) => s.offset);
46719
- const stitched = polygons.length >= 2 ? loftStitched(polygons, heights, wasm) : null;
47388
+ const stitched = polygons.length >= 2 ? loftStitched(polygons, heights, wasm, { edgeLength: plan.edgeLength }) : null;
46720
47389
  if (stitched) {
46721
47390
  solid = stitched;
46722
47391
  } else {
@@ -47157,17 +47826,21 @@ function lowerSdfToManifold(evalFn, bounds, edgeLength2, wasm, meshing, evaluato
47157
47826
  if (diagnostics) diagnostics.projectionMs = nowMs() - projectionStart;
47158
47827
  const simplificationStart = nowMs();
47159
47828
  if (((meshing == null ? void 0 : meshing.simplify) ?? "safe") !== "off" && snMesh.numTris > 100) {
47160
- triVerts = simplifySdfMesh(triVerts, snMesh.vertProperties, edgeLength2, wasm, meshing == null ? void 0 : meshing.maxTriangles);
47829
+ triVerts = simplifySdfMesh(triVerts, snMesh.vertProperties, vertProps6, edgeLength2, meshing == null ? void 0 : meshing.maxTriangles);
47161
47830
  }
47162
47831
  if (diagnostics) {
47163
47832
  diagnostics.simplificationMs = nowMs() - simplificationStart;
47164
47833
  diagnostics.trianglesAfterSimplification = triVerts.length / 3;
47165
47834
  }
47166
47835
  if ((meshing == null ? void 0 : meshing.maxTriangles) !== void 0 && triVerts.length / 3 > meshing.maxTriangles) {
47836
+ const verb = (meshing.simplify ?? "safe") === "off" ? "produced" : "could only simplify to";
47167
47837
  throw new Error(
47168
- `SDF meshing produced ${triVerts.length / 3} triangles, above maxTriangles=${meshing.maxTriangles}. Increase maxTriangles, use a larger edgeLength, or choose quality: "draft".`
47838
+ `SDF meshing ${verb} ${triVerts.length / 3} safe triangles, above maxTriangles=${meshing.maxTriangles}. Increase maxTriangles or use a larger edgeLength.`
47169
47839
  );
47170
47840
  }
47841
+ if (!isClosedConsistentlyWoundTriangleMesh(triVerts, vertProps6.length / 6)) {
47842
+ throw new Error("SDF meshing produced an open, non-manifold, or inconsistently wound triangle mesh.");
47843
+ }
47171
47844
  const wasmMesh = new wasm.Mesh({
47172
47845
  numProp: 6,
47173
47846
  vertProperties: vertProps6,
@@ -47185,28 +47858,102 @@ function lowerSdfToManifold(evalFn, bounds, edgeLength2, wasm, meshing, evaluato
47185
47858
  disposeWasmObject(wasmMesh);
47186
47859
  }
47187
47860
  }
47188
- function simplifySdfMesh(triVerts, vertProperties, edgeLength2, wasm, maxTriangles) {
47861
+ function simplifySdfMesh(triVerts, vertProperties, finalVertProperties, edgeLength2, maxTriangles) {
47189
47862
  const maxError = edgeLength2 * 0.15;
47190
47863
  const inputTriangles = triVerts.length / 3;
47191
- const requestedRatio = maxTriangles && maxTriangles < inputTriangles ? maxTriangles / inputTriangles : 0.5;
47192
- const ratios = Array.from(/* @__PURE__ */ new Set([Math.max(0.05, Math.min(0.5, requestedRatio)), 0.75]));
47864
+ const ratios = buildSimplificationRatios(inputTriangles, maxTriangles);
47865
+ const vertexCount = finalVertProperties.length / 6;
47866
+ let bestValid = null;
47193
47867
  for (const ratio of ratios) {
47194
- let simplified = simplifyMesh(triVerts, vertProperties, ratio, maxError);
47195
- simplified = filterDegenerateTriangles(simplified);
47196
- let mesh = null;
47197
- let manifold = null;
47868
+ let simplified;
47198
47869
  try {
47199
- mesh = new wasm.Mesh({ numProp: 3, vertProperties, triVerts: simplified });
47200
- manifold = new wasm.Manifold(mesh);
47201
- return simplified;
47870
+ simplified = simplifyMesh(triVerts, vertProperties, ratio, maxError);
47202
47871
  } catch {
47203
- } finally {
47204
- disposeWasmObject(manifold);
47205
- disposeWasmObject(mesh);
47872
+ continue;
47873
+ }
47874
+ simplified = filterDegenerateTriangles(simplified);
47875
+ if (isClosedConsistentlyWoundTriangleMesh(simplified, vertexCount)) {
47876
+ if (maxTriangles === void 0 || simplified.length / 3 <= maxTriangles) {
47877
+ return simplified;
47878
+ }
47879
+ if (!bestValid || simplified.length < bestValid.length) {
47880
+ bestValid = simplified;
47881
+ }
47206
47882
  }
47207
47883
  }
47884
+ if (bestValid) {
47885
+ return bestValid;
47886
+ }
47208
47887
  return triVerts;
47209
47888
  }
47889
+ function buildSimplificationRatios(inputTriangles, maxTriangles) {
47890
+ if (maxTriangles === void 0 || maxTriangles >= inputTriangles) {
47891
+ return [0.5, 0.75];
47892
+ }
47893
+ const requestedRatio = Math.max(1 / inputTriangles, Math.min(0.5, maxTriangles / inputTriangles));
47894
+ const candidates = [
47895
+ requestedRatio,
47896
+ 0.05,
47897
+ 0.04,
47898
+ 0.03,
47899
+ 0.02,
47900
+ 0.015,
47901
+ 0.01,
47902
+ 5e-3,
47903
+ requestedRatio * 0.85,
47904
+ requestedRatio * 0.7,
47905
+ requestedRatio * 0.55,
47906
+ requestedRatio * 0.4,
47907
+ requestedRatio * 0.25,
47908
+ requestedRatio * 0.1,
47909
+ 0.5,
47910
+ 0.75
47911
+ ];
47912
+ const seen = /* @__PURE__ */ new Set();
47913
+ const ratios = [];
47914
+ for (const ratio of candidates) {
47915
+ const rounded = Number(Math.max(1 / inputTriangles, Math.min(0.75, ratio)).toFixed(6));
47916
+ if (seen.has(rounded)) continue;
47917
+ seen.add(rounded);
47918
+ ratios.push(rounded);
47919
+ }
47920
+ return ratios;
47921
+ }
47922
+ function isClosedConsistentlyWoundTriangleMesh(triVerts, vertexCount) {
47923
+ if (triVerts.length === 0 || triVerts.length % 3 !== 0) return false;
47924
+ const edgeUse = /* @__PURE__ */ new Map();
47925
+ for (let i = 0; i < triVerts.length; i += 3) {
47926
+ const a2 = triVerts[i];
47927
+ const b = triVerts[i + 1];
47928
+ const c2 = triVerts[i + 2];
47929
+ if (!isValidMeshIndex(a2, vertexCount) || !isValidMeshIndex(b, vertexCount) || !isValidMeshIndex(c2, vertexCount)) return false;
47930
+ if (a2 === b || b === c2 || a2 === c2) return false;
47931
+ for (const [from, to] of [
47932
+ [a2, b],
47933
+ [b, c2],
47934
+ [c2, a2]
47935
+ ]) {
47936
+ const lo = Math.min(from, to);
47937
+ const hi = Math.max(from, to);
47938
+ const key = `${lo}/${hi}`;
47939
+ const winding = from === lo ? 1 : -1;
47940
+ const entry = edgeUse.get(key);
47941
+ if (entry) {
47942
+ entry.count += 1;
47943
+ entry.winding += winding;
47944
+ } else {
47945
+ edgeUse.set(key, { count: 1, winding });
47946
+ }
47947
+ }
47948
+ }
47949
+ for (const entry of edgeUse.values()) {
47950
+ if (entry.count !== 2 || entry.winding !== 0) return false;
47951
+ }
47952
+ return true;
47953
+ }
47954
+ function isValidMeshIndex(value, vertexCount) {
47955
+ return Number.isInteger(value) && value >= 0 && value < vertexCount;
47956
+ }
47210
47957
  function filterDegenerateTriangles(triVerts) {
47211
47958
  let writeIdx = 0;
47212
47959
  for (let i = 0; i < triVerts.length; i += 3) {
@@ -48788,6 +49535,92 @@ function percentile(sorted, q) {
48788
49535
  const index2 = MathUtils.clamp(Math.floor(sorted.length * q), 0, sorted.length - 1);
48789
49536
  return Number(sorted[index2].toFixed(2));
48790
49537
  }
49538
+ class DisjointSet {
49539
+ constructor(size) {
49540
+ __publicField(this, "parent");
49541
+ __publicField(this, "rank");
49542
+ this.parent = Array.from({ length: size }, (_2, index2) => index2);
49543
+ this.rank = Array.from({ length: size }, () => 0);
49544
+ }
49545
+ find(index2) {
49546
+ const parent = this.parent[index2];
49547
+ if (parent === index2) return index2;
49548
+ const root = this.find(parent);
49549
+ this.parent[index2] = root;
49550
+ return root;
49551
+ }
49552
+ union(a2, b) {
49553
+ const rootA = this.find(a2);
49554
+ const rootB = this.find(b);
49555
+ if (rootA === rootB) return;
49556
+ if (this.rank[rootA] < this.rank[rootB]) {
49557
+ this.parent[rootA] = rootB;
49558
+ } else if (this.rank[rootA] > this.rank[rootB]) {
49559
+ this.parent[rootB] = rootA;
49560
+ } else {
49561
+ this.parent[rootB] = rootA;
49562
+ this.rank[rootA] += 1;
49563
+ }
49564
+ }
49565
+ }
49566
+ function connectedCoplanarSurfacePatches(triangles) {
49567
+ const snap = surfacePatchSnap(triangles);
49568
+ const planeKeys = triangles.map((triangle) => planeKey(triangle, snap));
49569
+ const edgeOwners = /* @__PURE__ */ new Map();
49570
+ const sets = new DisjointSet(triangles.length);
49571
+ triangles.forEach((triangle, index2) => {
49572
+ for (const key of triangleEdgeKeys(triangle, snap)) {
49573
+ const owners = edgeOwners.get(key);
49574
+ if (owners) {
49575
+ for (const owner of owners) {
49576
+ if (planeKeys[owner] === planeKeys[index2]) sets.union(owner, index2);
49577
+ }
49578
+ owners.push(index2);
49579
+ } else {
49580
+ edgeOwners.set(key, [index2]);
49581
+ }
49582
+ }
49583
+ });
49584
+ const patchByRoot = /* @__PURE__ */ new Map();
49585
+ triangles.forEach((triangle, index2) => {
49586
+ const root = sets.find(index2);
49587
+ const patch = patchByRoot.get(root) ?? { triangleIndexes: [], area: 0 };
49588
+ patch.triangleIndexes.push(index2);
49589
+ patch.area += triangle.area;
49590
+ patchByRoot.set(root, patch);
49591
+ });
49592
+ return [...patchByRoot.values()];
49593
+ }
49594
+ function surfacePatchSnap(triangles) {
49595
+ const bounds = new Box3();
49596
+ for (const triangle of triangles) {
49597
+ bounds.expandByPoint(triangle.a);
49598
+ bounds.expandByPoint(triangle.b);
49599
+ bounds.expandByPoint(triangle.c);
49600
+ }
49601
+ const size = bounds.getSize(new Vector3());
49602
+ return Math.max(1e-6, size.length() * 1e-8);
49603
+ }
49604
+ function planeKey(triangle, snap) {
49605
+ const normalSnap = 1e-6;
49606
+ const distance = triangle.normal.dot(triangle.a);
49607
+ return [
49608
+ Math.round(triangle.normal.x / normalSnap),
49609
+ Math.round(triangle.normal.y / normalSnap),
49610
+ Math.round(triangle.normal.z / normalSnap),
49611
+ Math.round(distance / snap)
49612
+ ].join(",");
49613
+ }
49614
+ function triangleEdgeKeys(triangle, snap) {
49615
+ const vertices = [vertexKey$2(triangle.a, snap), vertexKey$2(triangle.b, snap), vertexKey$2(triangle.c, snap)];
49616
+ return [edgeKey$1(vertices[0], vertices[1]), edgeKey$1(vertices[1], vertices[2]), edgeKey$1(vertices[2], vertices[0])];
49617
+ }
49618
+ function vertexKey$2(point, snap) {
49619
+ return `${Math.round(point.x / snap)},${Math.round(point.y / snap)},${Math.round(point.z / snap)}`;
49620
+ }
49621
+ function edgeKey$1(a2, b) {
49622
+ return a2 < b ? `${a2}|${b}` : `${b}|${a2}`;
49623
+ }
48791
49624
  const MIN_TRIANGLE_AREA = 1e-12;
48792
49625
  const R2_ALPHA = 0.7548776662466927;
48793
49626
  const R2_BETA = 0.5698402909980532;
@@ -48840,7 +49673,7 @@ function allocateAreaSampleCounts(triangles, maxSamples) {
48840
49673
  return counts;
48841
49674
  }
48842
49675
  function sampleSurfaceTriangles(triangles, maxSamples) {
48843
- const counts = allocateAreaSampleCounts(triangles, maxSamples);
49676
+ const counts = allocateSurfacePatchSampleCounts(triangles, maxSamples);
48844
49677
  const samples = [];
48845
49678
  const position = new Vector3();
48846
49679
  let sampleIndex = 0;
@@ -48865,6 +49698,35 @@ function sampleSurfaceTriangles(triangles, maxSamples) {
48865
49698
  });
48866
49699
  return samples;
48867
49700
  }
49701
+ function allocateSurfacePatchSampleCounts(triangles, maxSamples) {
49702
+ const counts = new Array(triangles.length).fill(0);
49703
+ if (triangles.length === 0) return counts;
49704
+ const patches = connectedCoplanarSurfacePatches(triangles);
49705
+ const patchCounts = allocateAreaSampleCounts(
49706
+ patches.map((patch, index2) => {
49707
+ var _a3, _b3, _c2, _d2;
49708
+ return {
49709
+ index: index2,
49710
+ a: ((_a3 = triangles[patch.triangleIndexes[0]]) == null ? void 0 : _a3.a) ?? new Vector3(),
49711
+ b: ((_b3 = triangles[patch.triangleIndexes[0]]) == null ? void 0 : _b3.b) ?? new Vector3(),
49712
+ c: ((_c2 = triangles[patch.triangleIndexes[0]]) == null ? void 0 : _c2.c) ?? new Vector3(),
49713
+ normal: ((_d2 = triangles[patch.triangleIndexes[0]]) == null ? void 0 : _d2.normal) ?? new Vector3(0, 0, 1),
49714
+ area: patch.area
49715
+ };
49716
+ }),
49717
+ maxSamples
49718
+ );
49719
+ patches.forEach((patch, patchIndex) => {
49720
+ const patchBudget = patchCounts[patchIndex] ?? 0;
49721
+ if (patchBudget <= 0) return;
49722
+ const patchTriangles = patch.triangleIndexes.map((index2) => triangles[index2]);
49723
+ const localCounts = allocateAreaSampleCounts(patchTriangles, patchBudget);
49724
+ patch.triangleIndexes.forEach((triangleIndex, localIndex) => {
49725
+ counts[triangleIndex] += localCounts[localIndex] ?? 0;
49726
+ });
49727
+ });
49728
+ return counts;
49729
+ }
48868
49730
  function totalSurfaceArea(triangles) {
48869
49731
  return triangles.reduce((sum2, triangle) => sum2 + triangle.area, 0);
48870
49732
  }
@@ -49045,16 +49907,23 @@ const DEFAULT_THICKNESS_INSPECTION_OPTIONS = {
49045
49907
  minThickness: 1.2,
49046
49908
  warnThickness: 2,
49047
49909
  maxThickness: 6,
49910
+ colorMinThickness: 0,
49911
+ colorMaxThickness: 6,
49048
49912
  maxSamplesPerObject: 5e3,
49049
49913
  contactTolerance: DEFAULT_PHYSICAL_CONNECTIVITY_OPTIONS.contactTolerance
49050
49914
  };
49051
49915
  const THICKNESS_COLORS = {
49052
49916
  critical: [255, 28, 28],
49053
- warning: [255, 150, 0],
49054
49917
  ok: [60, 220, 90],
49055
49918
  thick: [70, 145, 255],
49056
49919
  unknown: [90, 90, 90]
49057
49920
  };
49921
+ const THICKNESS_GRADIENT_COLORS = [
49922
+ THICKNESS_COLORS.critical,
49923
+ [255, 222, 0],
49924
+ THICKNESS_COLORS.ok,
49925
+ THICKNESS_COLORS.thick
49926
+ ];
49058
49927
  function finitePositive(value, fallback, label) {
49059
49928
  if (value === void 0) return fallback;
49060
49929
  if (!Number.isFinite(value) || value <= 0) {
@@ -49073,6 +49942,16 @@ function resolveThicknessInspectionOptions(raw = {}) {
49073
49942
  const minThickness = finitePositive(raw.minThickness, DEFAULT_THICKNESS_INSPECTION_OPTIONS.minThickness, "minThickness");
49074
49943
  const warnThickness = finitePositive(raw.warnThickness, DEFAULT_THICKNESS_INSPECTION_OPTIONS.warnThickness, "warnThickness");
49075
49944
  const maxThickness = finitePositive(raw.maxThickness, DEFAULT_THICKNESS_INSPECTION_OPTIONS.maxThickness, "maxThickness");
49945
+ const colorMinThickness = finiteNonNegative(
49946
+ raw.colorMinThickness,
49947
+ DEFAULT_THICKNESS_INSPECTION_OPTIONS.colorMinThickness,
49948
+ "colorMinThickness"
49949
+ );
49950
+ const colorMaxThickness = finitePositive(
49951
+ raw.colorMaxThickness,
49952
+ DEFAULT_THICKNESS_INSPECTION_OPTIONS.colorMaxThickness,
49953
+ "colorMaxThickness"
49954
+ );
49076
49955
  const maxSamplesPerObject = finitePositive(
49077
49956
  raw.maxSamplesPerObject,
49078
49957
  DEFAULT_THICKNESS_INSPECTION_OPTIONS.maxSamplesPerObject,
@@ -49089,10 +49968,15 @@ function resolveThicknessInspectionOptions(raw = {}) {
49089
49968
  if (warnThickness > maxThickness) {
49090
49969
  throw new Error("warnThickness must be less than or equal to maxThickness.");
49091
49970
  }
49971
+ if (colorMinThickness >= colorMaxThickness) {
49972
+ throw new Error("colorMinThickness must be less than colorMaxThickness.");
49973
+ }
49092
49974
  return {
49093
49975
  minThickness,
49094
49976
  warnThickness,
49095
49977
  maxThickness,
49978
+ colorMinThickness,
49979
+ colorMaxThickness,
49096
49980
  maxSamplesPerObject: Math.max(1, Math.floor(maxSamplesPerObject)),
49097
49981
  contactTolerance
49098
49982
  };
@@ -49103,6 +49987,16 @@ function lerp$1(a2, b, t) {
49103
49987
  function lerpColor(a2, b, t) {
49104
49988
  return [Math.round(lerp$1(a2[0], b[0], t)), Math.round(lerp$1(a2[1], b[1], t)), Math.round(lerp$1(a2[2], b[2], t))];
49105
49989
  }
49990
+ function gradientColor(stops, t) {
49991
+ if (stops.length === 0) return THICKNESS_COLORS.unknown;
49992
+ if (stops.length === 1) return stops[0] ?? THICKNESS_COLORS.unknown;
49993
+ const clamped = Math.max(0, Math.min(1, t));
49994
+ const scaled = clamped * (stops.length - 1);
49995
+ const leftIndex = Math.min(stops.length - 2, Math.floor(scaled));
49996
+ const left = stops[leftIndex] ?? THICKNESS_COLORS.unknown;
49997
+ const right = stops[leftIndex + 1] ?? left;
49998
+ return lerpColor(left, right, scaled - leftIndex);
49999
+ }
49106
50000
  function thicknessClass(thickness, options) {
49107
50001
  if (thickness == null || !Number.isFinite(thickness) || thickness <= 0) return "unknown";
49108
50002
  if (thickness <= options.minThickness) return "critical";
@@ -49111,18 +50005,9 @@ function thicknessClass(thickness, options) {
49111
50005
  return "thick";
49112
50006
  }
49113
50007
  function thicknessColor(thickness, options) {
49114
- const cls = thicknessClass(thickness, options);
49115
- if (cls === "unknown") return THICKNESS_COLORS.unknown;
49116
- if (cls === "critical") return THICKNESS_COLORS.critical;
49117
- if (cls === "warning") {
49118
- const span = Math.max(1e-9, options.warnThickness - options.minThickness);
49119
- return lerpColor(THICKNESS_COLORS.critical, THICKNESS_COLORS.warning, ((thickness ?? 0) - options.minThickness) / span);
49120
- }
49121
- if (cls === "ok") {
49122
- const span = Math.max(1e-9, options.maxThickness - options.warnThickness);
49123
- return lerpColor(THICKNESS_COLORS.ok, THICKNESS_COLORS.thick, ((thickness ?? 0) - options.warnThickness) / span);
49124
- }
49125
- return THICKNESS_COLORS.thick;
50008
+ if (thickness == null || !Number.isFinite(thickness) || thickness <= 0) return THICKNESS_COLORS.unknown;
50009
+ const span = Math.max(1e-9, options.colorMaxThickness - options.colorMinThickness);
50010
+ return gradientColor(THICKNESS_GRADIENT_COLORS, (thickness - options.colorMinThickness) / span);
49126
50011
  }
49127
50012
  function cloneGeometryForFaceColors(geometry) {
49128
50013
  return geometry.index ? geometry.toNonIndexed() : geometry.clone();
@@ -49508,15 +50393,23 @@ function computeGeometryArrays(mesh, options = {}) {
49508
50393
  normals[o + 7] = vertNormals[i2 * 3 + 1];
49509
50394
  normals[o + 8] = vertNormals[i2 * 3 + 2];
49510
50395
  } else if (numProp >= 6) {
49511
- normals[o] = vertProperties[i0 * numProp + 3];
49512
- normals[o + 1] = vertProperties[i0 * numProp + 4];
49513
- normals[o + 2] = vertProperties[i0 * numProp + 5];
49514
- normals[o + 3] = vertProperties[i1 * numProp + 3];
49515
- normals[o + 4] = vertProperties[i1 * numProp + 4];
49516
- normals[o + 5] = vertProperties[i1 * numProp + 5];
49517
- normals[o + 6] = vertProperties[i2 * numProp + 3];
49518
- normals[o + 7] = vertProperties[i2 * numProp + 4];
49519
- normals[o + 8] = vertProperties[i2 * numProp + 5];
50396
+ const corners = [i0, i1, i2];
50397
+ for (let v = 0; v < 3; v++) {
50398
+ const base = corners[v] * numProp;
50399
+ const nx = vertProperties[base + 3];
50400
+ const ny = vertProperties[base + 4];
50401
+ const nz = vertProperties[base + 5];
50402
+ const oc = o + v * 3;
50403
+ if (nx * nx + ny * ny + nz * nz > 1e-12) {
50404
+ normals[oc] = nx;
50405
+ normals[oc + 1] = ny;
50406
+ normals[oc + 2] = nz;
50407
+ } else {
50408
+ normals[oc] = fnx;
50409
+ normals[oc + 1] = fny;
50410
+ normals[oc + 2] = fnz;
50411
+ }
50412
+ }
49520
50413
  } else {
49521
50414
  normals[o] = fnx;
49522
50415
  normals[o + 1] = fny;
@@ -49986,7 +50879,7 @@ for (let radius = 0; radius <= MAX_SEARCH_RADIUS; radius += 1) {
49986
50879
  function gridKey(x2, y2, z2) {
49987
50880
  return `${x2},${y2},${z2}`;
49988
50881
  }
49989
- function buildGeometryBounds(geometry) {
50882
+ function buildInspectHeatmapFieldBounds(geometry) {
49990
50883
  const position = geometry.getAttribute("position");
49991
50884
  if (!position || position.count === 0) return null;
49992
50885
  const bounds = new Box3().setFromBufferAttribute(position);
@@ -49994,7 +50887,11 @@ function buildGeometryBounds(geometry) {
49994
50887
  const size = bounds.getSize(new Vector3());
49995
50888
  const pad = Math.max(size.x, size.y, size.z, 1) * 1e-5;
49996
50889
  bounds.expandByScalar(pad);
49997
- return bounds;
50890
+ const boundsSize = bounds.getSize(new Vector3());
50891
+ return {
50892
+ boundsMin: [bounds.min.x, bounds.min.y, bounds.min.z],
50893
+ boundsSize: [boundsSize.x, boundsSize.y, boundsSize.z]
50894
+ };
49998
50895
  }
49999
50896
  function buildSampleGrid(pointCloud) {
50000
50897
  const sampleCount = Math.floor(pointCloud.positions.length / 3);
@@ -50078,11 +50975,15 @@ function blendedColorAt(point, pointCloud, sampleGrid) {
50078
50975
  return [r / weightSum, g2 / weightSum, b / weightSum];
50079
50976
  }
50080
50977
  function buildInspectHeatmapFieldData(geometry, pointCloud) {
50081
- const bounds = buildGeometryBounds(geometry);
50978
+ const bounds = buildInspectHeatmapFieldBounds(geometry);
50979
+ return bounds ? buildInspectHeatmapFieldDataFromBounds(bounds, pointCloud) : null;
50980
+ }
50981
+ function buildInspectHeatmapFieldDataFromBounds(bounds, pointCloud) {
50082
50982
  const sampleGrid = buildSampleGrid(pointCloud);
50083
50983
  if (!bounds || !sampleGrid) return null;
50084
50984
  const gridSize = fieldGridSizeForSampleCount(Math.floor(pointCloud.positions.length / 3));
50085
- const boundsSize = bounds.getSize(new Vector3());
50985
+ const boundsMin = new Vector3(...bounds.boundsMin);
50986
+ const boundsSize = new Vector3(...bounds.boundsSize);
50086
50987
  const data = new Uint8Array(gridSize * gridSize * gridSize * 4);
50087
50988
  const point = new Vector3();
50088
50989
  let dataOffset = 0;
@@ -50090,9 +50991,9 @@ function buildInspectHeatmapFieldData(geometry, pointCloud) {
50090
50991
  for (let z2 = 0; z2 < gridSize; z2 += 1) {
50091
50992
  for (let x2 = 0; x2 < gridSize; x2 += 1) {
50092
50993
  point.set(
50093
- bounds.min.x + boundsSize.x * x2 / (gridSize - 1),
50094
- bounds.min.y + boundsSize.y * y2 / (gridSize - 1),
50095
- bounds.min.z + boundsSize.z * z2 / (gridSize - 1)
50994
+ boundsMin.x + boundsSize.x * x2 / (gridSize - 1),
50995
+ boundsMin.y + boundsSize.y * y2 / (gridSize - 1),
50996
+ boundsMin.z + boundsSize.z * z2 / (gridSize - 1)
50096
50997
  );
50097
50998
  const [r, g2, b] = blendedColorAt(point, pointCloud, sampleGrid);
50098
50999
  data[dataOffset] = Math.max(0, Math.min(255, Math.round(r)));
@@ -50105,13 +51006,14 @@ function buildInspectHeatmapFieldData(geometry, pointCloud) {
50105
51006
  }
50106
51007
  return {
50107
51008
  data,
50108
- boundsMin: [bounds.min.x, bounds.min.y, bounds.min.z],
51009
+ boundsMin: bounds.boundsMin,
50109
51010
  boundsSize: [boundsSize.x, boundsSize.y, boundsSize.z],
50110
51011
  gridSize
50111
51012
  };
50112
51013
  }
50113
51014
  const workerScope = self;
50114
51015
  let manifoldReadyPromise = null;
51016
+ let cachedThicknessColorizeAnalysis = null;
50115
51017
  function ensureManifoldReady() {
50116
51018
  if (!manifoldReadyPromise) manifoldReadyPromise = initKernelManifoldOnly();
50117
51019
  return manifoldReadyPromise;
@@ -50157,6 +51059,7 @@ function geometryFromPositions(positions) {
50157
51059
  function pointBuffers(samples) {
50158
51060
  const positions = new Float32Array(samples.length * 3);
50159
51061
  const colors = new Float32Array(samples.length * 3);
51062
+ const values = new Float32Array(samples.length);
50160
51063
  samples.forEach((sample, index2) => {
50161
51064
  const base = index2 * 3;
50162
51065
  positions[base] = sample.position[0] + sample.normal[0] * 0.025;
@@ -50165,8 +51068,9 @@ function pointBuffers(samples) {
50165
51068
  colors[base] = sample.color[0] / 255;
50166
51069
  colors[base + 1] = sample.color[1] / 255;
50167
51070
  colors[base + 2] = sample.color[2] / 255;
51071
+ values[index2] = sample.value ?? Number.NaN;
50168
51072
  });
50169
- return { positions, colors };
51073
+ return { positions, colors, values };
50170
51074
  }
50171
51075
  function rgbFloatsForHex(hex) {
50172
51076
  const color = new Color(hex);
@@ -50191,6 +51095,7 @@ function analyzeScalarChannel(request) {
50191
51095
  const pointObjects = [];
50192
51096
  const heatmapFieldObjects = [];
50193
51097
  const warnings = [];
51098
+ const thicknessCacheObjects = [];
50194
51099
  for (const object of request.objects) {
50195
51100
  if (!object.positions || object.positions.length < 9) continue;
50196
51101
  const geometry = geometryFromPositions(object.positions);
@@ -50198,13 +51103,22 @@ function analyzeScalarChannel(request) {
50198
51103
  const analysis = request.channel === "thickness" ? analyzeThicknessGeometry(geometry, request.thickness) : analyzeRoughnessGeometry(geometry, request.roughness);
50199
51104
  analysis.warnings.forEach((warning) => warnings.push(`${object.name}: ${warning}`));
50200
51105
  const buffers = pointBuffers(analysis.pointSamples);
50201
- const field = buildInspectHeatmapFieldData(analysis.geometry, buffers);
51106
+ const heatmapBounds = buildInspectHeatmapFieldBounds(analysis.geometry);
51107
+ const field = heatmapBounds ? buildInspectHeatmapFieldDataFromBounds(heatmapBounds, buffers) : buildInspectHeatmapFieldData(analysis.geometry, buffers);
50202
51108
  if (field) {
50203
51109
  heatmapFieldObjects.push({
50204
51110
  objectId: object.id,
50205
51111
  ...field
50206
51112
  });
50207
51113
  }
51114
+ if (request.channel === "thickness" && buffers.values) {
51115
+ thicknessCacheObjects.push({
51116
+ objectId: object.id,
51117
+ positions: new Float32Array(buffers.positions),
51118
+ values: new Float32Array(buffers.values),
51119
+ heatmapBounds
51120
+ });
51121
+ }
50208
51122
  pointObjects.push({
50209
51123
  objectId: object.id,
50210
51124
  sampleCount: analysis.pointSamples.length,
@@ -50215,7 +51129,9 @@ function analyzeScalarChannel(request) {
50215
51129
  geometry.dispose();
50216
51130
  }
50217
51131
  }
51132
+ cachedThicknessColorizeAnalysis = request.channel === "thickness" ? { analysisId: request.reqId, objects: thicknessCacheObjects } : null;
50218
51133
  return {
51134
+ analysisId: request.reqId,
50219
51135
  channel: request.channel,
50220
51136
  objectColors: {},
50221
51137
  pointObjects,
@@ -50225,6 +51141,47 @@ function analyzeScalarChannel(request) {
50225
51141
  warnings
50226
51142
  };
50227
51143
  }
51144
+ function thicknessColorsForValues(values, colorMinThickness, colorMaxThickness) {
51145
+ const options = resolveThicknessInspectionOptions({ colorMinThickness, colorMaxThickness });
51146
+ const colors = new Float32Array(values.length * 3);
51147
+ for (let index2 = 0; index2 < values.length; index2 += 1) {
51148
+ const color = thicknessColor(values[index2], options);
51149
+ const offset = index2 * 3;
51150
+ colors[offset] = color[0] / 255;
51151
+ colors[offset + 1] = color[1] / 255;
51152
+ colors[offset + 2] = color[2] / 255;
51153
+ }
51154
+ return colors;
51155
+ }
51156
+ function colorizeThicknessAnalysis(request) {
51157
+ const cached = cachedThicknessColorizeAnalysis;
51158
+ if (!cached || cached.analysisId !== request.analysisId) {
51159
+ throw new Error("Thickness colorize cache is no longer available.");
51160
+ }
51161
+ const pointObjects = [];
51162
+ const heatmapFieldObjects = [];
51163
+ for (const object of cached.objects) {
51164
+ const colors = thicknessColorsForValues(object.values, request.colorMinThickness, request.colorMaxThickness);
51165
+ pointObjects.push({ objectId: object.objectId, colors });
51166
+ if (object.heatmapBounds) {
51167
+ const field = buildInspectHeatmapFieldDataFromBounds(object.heatmapBounds, {
51168
+ positions: object.positions,
51169
+ colors
51170
+ });
51171
+ if (field) {
51172
+ heatmapFieldObjects.push({
51173
+ objectId: object.objectId,
51174
+ ...field
51175
+ });
51176
+ }
51177
+ }
51178
+ }
51179
+ return {
51180
+ analysisId: cached.analysisId,
51181
+ pointObjects,
51182
+ heatmapFieldObjects
51183
+ };
51184
+ }
50228
51185
  function analyzeConnectivityChannel(request) {
50229
51186
  const bodyInput = buildMeshBodyConnectivityInput(
50230
51187
  request.objects.map((object) => ({
@@ -50391,7 +51348,11 @@ function analyzeCollisionChannel(request) {
50391
51348
  }
50392
51349
  function transferFor(result) {
50393
51350
  return [
50394
- ...result.pointObjects.flatMap((object) => [object.positions.buffer, object.colors.buffer]),
51351
+ ...result.pointObjects.flatMap((object) => [
51352
+ object.positions.buffer,
51353
+ object.colors.buffer,
51354
+ ...object.values ? [object.values.buffer] : []
51355
+ ]),
50395
51356
  ...result.meshColorObjects.map((object) => object.colors.buffer),
50396
51357
  ...result.heatmapFieldObjects.map((object) => object.data.buffer),
50397
51358
  ...result.collisionGeometryObjects.flatMap((object) => [object.positions.buffer, object.normals.buffer, object.edgePositions.buffer])
@@ -50399,6 +51360,21 @@ function transferFor(result) {
50399
51360
  }
50400
51361
  async function handleRequest(request) {
50401
51362
  try {
51363
+ if (request.type === "colorize-thickness") {
51364
+ const result2 = colorizeThicknessAnalysis(request.payload);
51365
+ const response2 = {
51366
+ type: "inspect-colorize-success",
51367
+ payload: {
51368
+ reqId: request.payload.reqId,
51369
+ result: result2
51370
+ }
51371
+ };
51372
+ workerScope.postMessage(response2, [
51373
+ ...result2.pointObjects.map((object) => object.colors.buffer),
51374
+ ...result2.heatmapFieldObjects.map((object) => object.data.buffer)
51375
+ ]);
51376
+ return;
51377
+ }
50402
51378
  if (request.payload.channel === "collisions") await ensureManifoldReady();
50403
51379
  const result = request.payload.channel === "thickness" || request.payload.channel === "roughness" ? analyzeScalarChannel(request.payload) : request.payload.channel === "collisions" ? analyzeCollisionChannel(request.payload) : analyzeConnectivityChannel(request.payload);
50404
51380
  const response = {