forgecad 0.9.16 → 0.10.1

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 (162) hide show
  1. package/dist/assets/{AdminPage-CXvls4-J.js → AdminPage-DcCnj0qo.js} +1 -1
  2. package/dist/assets/{BenchmarkPage-B27zk8xL.js → BenchmarkPage-BVEpJSVk.js} +1 -1
  3. package/dist/assets/{BlogPage-CMAVvgQL.js → BlogPage-DHaGP50_.js} +1 -1
  4. package/dist/assets/{DocsPage-knf4I4h7.js → DocsPage-CDoxHkz8.js} +40 -859
  5. package/dist/assets/EditorApp-BJ0Dloyh.js +16446 -0
  6. package/dist/assets/{EmbedViewer-D7ZGlFjx.js → EmbedViewer-CRKZbY0y.js} +2 -2
  7. package/dist/assets/{LandingPageProofDriven-CnevhTE8.js → LandingPageProofDriven-BxHkYRE7.js} +1 -1
  8. package/dist/assets/{LegalPage-BPTUmqeg.js → LegalPage-B-u6FrVv.js} +1 -1
  9. package/dist/assets/{PricingPage-B0D4goG_.js → PricingPage-CzpZ6-Ce.js} +1 -1
  10. package/dist/assets/{SettingsPage-CFF-UgjI.js → SettingsPage-CIZSSAd0.js} +1 -1
  11. package/dist/assets/{app-CE3sYcV7.css → app-CjsbDlb7.css} +143 -0
  12. package/dist/assets/{app-T0pDcSX4.js → app-DaTMg3nH.js} +1310 -290
  13. package/dist/assets/cli/{render-C5pcIISc.js → render-DPf4AYJK.js} +55 -60
  14. package/dist/assets/{constructionHistoryWorker-Ba2Hm58b.js → constructionHistoryWorker-AwMMWSxg.js} +1103 -349
  15. package/dist/assets/{evalWorker-vkx310U2.js → evalWorker-CjZZWRWW.js} +5209 -2643
  16. package/dist/assets/{inspectWorker-BuTJDVX6.js → inspectWorker-CZsCFtQT.js} +1163 -409
  17. package/dist/assets/{jointPose-B_Cgedn9.js → jointPose-DzQOViQH.js} +1 -1
  18. package/dist/assets/{manifold-BWgsjmAM.js → manifold-BYlzU521.js} +1 -1
  19. package/dist/assets/{manifold-D6IFSkhH.js → manifold-DgXo0T5P.js} +2 -2
  20. package/dist/assets/{manifold-rZexZI0G.js → manifold-K1SkarlQ.js} +1 -1
  21. package/dist/assets/{reportWorker-0AGij1Ru.js → reportWorker-B9nWwSrB.js} +8501 -3393
  22. package/dist/assets/{scalar-sampling-budget-J5cuzxT1.js → scalar-sampling-budget-prBw_s8t.js} +6067 -3479
  23. package/dist/assets/{scanProxyWorker-Vl4Wxa1y.js → scanProxyWorker-2GtDLk-R.js} +1 -1
  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 +1 -1
  28. package/dist/docs-raw/CLI.md +77 -240
  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 +188 -582
  32. package/dist/docs-raw/generated/concepts.md +259 -3501
  33. package/dist/docs-raw/generated/core.md +283 -1250
  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 +227 -85
  37. package/dist/docs-raw/generated/output.md +35 -99
  38. package/dist/docs-raw/generated/runtime-names.md +23 -23
  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/guides/simready-quickstart.md +171 -0
  50. package/dist/docs-raw/simulation-workflow.md +273 -0
  51. package/dist/docs-raw/skills/forgecad-3d-reconstruction.md +25 -111
  52. package/dist/docs-raw/skills/forgecad-blockout-model.md +20 -117
  53. package/dist/docs-raw/skills/forgecad-component-model.md +23 -107
  54. package/dist/docs-raw/skills/forgecad-high-level-spec.md +47 -155
  55. package/dist/docs-raw/skills/forgecad-image-replicator.md +26 -143
  56. package/dist/docs-raw/skills/forgecad-lld.md +19 -113
  57. package/dist/docs-raw/skills/forgecad-make-a-model.md +112 -532
  58. package/dist/docs-raw/skills/forgecad-model-grader.md +38 -108
  59. package/dist/docs-raw/skills/forgecad-prepare-prompt.md +24 -211
  60. package/dist/docs-raw/skills/forgecad-project.md +13 -131
  61. package/dist/docs-raw/skills/forgecad-reconstruction-benchmark.md +42 -134
  62. package/dist/docs-raw/skills/forgecad-render-inspect.md +27 -174
  63. package/dist/docs-raw/skills/forgecad-visual-spec.md +32 -112
  64. package/dist/docs-raw/skills/forgecad.md +19 -18
  65. package/dist/docs-raw/skills/index.md +2 -0
  66. package/dist/docs-raw/welcome.md +2 -2
  67. package/dist/index.html +2 -2
  68. package/dist/llms.txt +1 -2
  69. package/dist/sitemap.xml +25 -13
  70. package/dist-cli/{check-compiler-SYQ2PWOB.js → check-compiler-II7NLPAB.js} +1 -1
  71. package/dist-cli/{check-query-propagation-HIAGV62W.js → check-query-propagation-7462TR3R.js} +1 -1
  72. package/dist-cli/{chunk-SPZE3DUY.js → chunk-UWTJCGXF.js} +5848 -2915
  73. package/dist-cli/forgecad.js +3496 -703
  74. package/dist-skill/CONTEXT.md +1797 -7963
  75. package/dist-skill/SKILL.md +15 -15
  76. package/dist-skill/docs/API/core/concepts.md +27 -157
  77. package/dist-skill/docs/CLI.md +77 -240
  78. package/dist-skill/docs/generated/assembly.md +182 -532
  79. package/dist-skill/docs/generated/core.md +283 -1250
  80. package/dist-skill/docs/generated/curves.md +387 -1609
  81. package/dist-skill/docs/generated/lib.md +227 -85
  82. package/dist-skill/docs/generated/output.md +35 -99
  83. package/dist-skill/docs/generated/runtime-names.md +16 -21
  84. package/dist-skill/docs/generated/sdf.md +68 -284
  85. package/dist-skill/docs/generated/sheet-metal.md +68 -335
  86. package/dist-skill/docs/generated/sketch.md +240 -1160
  87. package/dist-skill/docs/generated/viewport.md +75 -223
  88. package/dist-skill/docs/generated/wood.md +21 -49
  89. package/dist-skill/docs/guides/coordinate-system.md +4 -42
  90. package/dist-skill/docs/guides/inspection-bundles.md +44 -442
  91. package/dist-skill/docs/guides/joint-design.md +18 -79
  92. package/dist-skill/docs/guides/positioning.md +21 -143
  93. package/dist-skill/docs/guides/scene-presentation.md +89 -0
  94. package/dist-skill/docs/guides/surface-members.md +26 -0
  95. package/dist-skill/library/forgecad-3d-reconstruction/SKILL.md +23 -111
  96. package/dist-skill/library/forgecad-blockout-model/SKILL.md +18 -117
  97. package/dist-skill/library/forgecad-component-model/SKILL.md +21 -107
  98. package/dist-skill/library/forgecad-high-level-spec/SKILL.md +45 -155
  99. package/dist-skill/library/forgecad-image-replicator/SKILL.md +24 -143
  100. package/dist-skill/library/forgecad-lld/SKILL.md +17 -113
  101. package/dist-skill/library/forgecad-make-a-model/SKILL.md +110 -532
  102. package/dist-skill/library/forgecad-model-grader/SKILL.md +36 -108
  103. package/dist-skill/library/forgecad-prepare-prompt/SKILL.md +35 -224
  104. package/dist-skill/library/forgecad-prepare-prompt/references/default-profiles.md +43 -271
  105. package/dist-skill/library/forgecad-prepare-prompt/references/master-prompt.md +30 -99
  106. package/dist-skill/library/forgecad-project/SKILL.md +13 -133
  107. package/dist-skill/library/forgecad-reconstruction-benchmark/SKILL.md +29 -123
  108. package/dist-skill/library/forgecad-render-inspect/SKILL.md +25 -174
  109. package/dist-skill/library/forgecad-visual-spec/SKILL.md +30 -111
  110. package/dist-skill/website/skills/forgecad-3d-reconstruction.md +58 -0
  111. package/dist-skill/website/skills/forgecad-blockout-model.md +49 -0
  112. package/dist-skill/website/skills/forgecad-component-model.md +53 -0
  113. package/dist-skill/website/skills/forgecad-high-level-spec.md +101 -0
  114. package/dist-skill/website/skills/forgecad-image-replicator.md +63 -0
  115. package/dist-skill/website/skills/forgecad-lld.md +41 -0
  116. package/dist-skill/website/skills/forgecad-make-a-model.md +186 -0
  117. package/dist-skill/website/skills/forgecad-model-grader.md +82 -0
  118. package/dist-skill/website/skills/forgecad-prepare-prompt.md +63 -0
  119. package/dist-skill/website/skills/forgecad-project.md +26 -0
  120. package/dist-skill/website/skills/forgecad-reconstruction-benchmark.md +60 -0
  121. package/dist-skill/website/skills/forgecad-render-inspect.md +80 -0
  122. package/dist-skill/website/skills/forgecad-visual-spec.md +71 -0
  123. package/dist-skill/website/skills/forgecad.md +122 -0
  124. package/dist-skill/website/skills/index.md +26 -0
  125. package/examples/api/comparison-imported-sphere-candidate.forge.js +1 -1
  126. package/examples/api/conformal-product-ribbon.forge.js +1 -1
  127. package/examples/api/exact-sheet-shell-assembly.forge.js +1 -1
  128. package/examples/api/extrude-options.forge.js +4 -2
  129. package/examples/api/field-loft-drive-tip.forge.js +40 -0
  130. package/examples/api/guided-loft-olive-oil-bottle.forge.js +1 -1
  131. package/examples/api/highlight-debug.forge.js +10 -10
  132. package/examples/api/mesh-import-slats.forge.js +1 -1
  133. package/examples/api/real-product-curves.forge.js +1 -1
  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 +3 -3
  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/examples/robotics/README.md +46 -0
  145. package/examples/robotics/scout-cam-rover-simready/README.md +119 -0
  146. package/examples/robotics/scout-cam-rover-simready/lib/dims.js +140 -0
  147. package/examples/robotics/scout-cam-rover-simready/main.forge.js +343 -0
  148. package/examples/robotics/scout-cam-rover-simready/parts/body.forge.js +304 -0
  149. package/examples/robotics/scout-cam-rover-simready/parts/chassis.forge.js +320 -0
  150. package/examples/robotics/scout-cam-rover-simready/parts/hardware.forge.js +21 -0
  151. package/examples/robotics/scout-cam-rover-simready/parts/turret.forge.js +70 -0
  152. package/examples/robotics/scout-cam-rover-simready/parts/wheel.forge.js +116 -0
  153. package/examples/robotics/simready-asset-crate.forge.js +79 -0
  154. package/examples/robotics/simready-diff-drive-rover.forge.js +141 -0
  155. package/examples/robotics/simready-parallel-gripper.forge.js +102 -0
  156. package/package.json +1 -1
  157. package/dist/assets/EditorApp-BHMQlJ-D.js +0 -14686
  158. package/dist/docs-raw/guides/geometry-conventions.md +0 -52
  159. package/dist/docs-raw/guides/modeling-recipes.md +0 -78
  160. package/dist-skill/docs/guides/geometry-conventions.md +0 -52
  161. package/dist-skill/docs/guides/modeling-recipes.md +0 -78
  162. package/dist-skill/library/forgecad-visual-spec/references/prompt-template.md +0 -79
@@ -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([
@@ -13341,6 +13369,8 @@ function cloneShapeCompilePlan(plan) {
13341
13369
  heights: plan.heights.map((height) => height),
13342
13370
  edgeLength: plan.edgeLength,
13343
13371
  boundsPadding: plan.boundsPadding,
13372
+ ...plan.forceField ? { forceField: true } : {},
13373
+ ...plan.meshing ? { meshing: cloneSdfCompileMeshingSettings(plan.meshing) } : {},
13344
13374
  edgeLabels: plan.edgeLabels ? { ...plan.edgeLabels } : void 0,
13345
13375
  capLabels: plan.capLabels ? { ...plan.capLabels } : void 0
13346
13376
  };
@@ -13608,7 +13638,6 @@ function cloneShapeCompilePlan(plan) {
13608
13638
  default:
13609
13639
  assertExhaustive(plan);
13610
13640
  }
13611
- if (plan._occtCache) result._occtCache = plan._occtCache;
13612
13641
  return result;
13613
13642
  }
13614
13643
  function appendProfileCompileTransform(plan, step) {
@@ -13621,22 +13650,31 @@ function appendShapeCompileTransform(plan, step) {
13621
13650
  if (plan.kind === "transform") {
13622
13651
  return {
13623
13652
  kind: "transform",
13624
- base: cloneShapeCompilePlan(plan.base),
13625
- steps: [...plan.steps.map(cloneShapeTransform), cloneShapeTransform(step)]
13653
+ base: plan.base,
13654
+ steps: [...plan.steps, cloneShapeTransform(step)]
13626
13655
  };
13627
13656
  }
13628
13657
  return {
13629
13658
  kind: "transform",
13630
- base: cloneShapeCompilePlan(plan),
13659
+ base: plan,
13631
13660
  steps: [cloneShapeTransform(step)]
13632
13661
  };
13633
13662
  }
13634
13663
  function appendShapeCompileTransforms(plan, steps) {
13635
- let out = cloneShapeCompilePlan(plan);
13636
- for (const step of steps) {
13637
- 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
+ };
13638
13672
  }
13639
- return out;
13673
+ return {
13674
+ kind: "transform",
13675
+ base: plan,
13676
+ steps: steps.map(cloneShapeTransform)
13677
+ };
13640
13678
  }
13641
13679
  function wrapShapeCompilePlanWithQueryOwner(plan, owner) {
13642
13680
  if (!plan) return null;
@@ -13710,6 +13748,8 @@ function buildLoftShapeCompilePlan(profiles, heights, options) {
13710
13748
  heights: heights.map((height) => canonicalNumber(height)),
13711
13749
  edgeLength: canonicalNumber(options.edgeLength),
13712
13750
  boundsPadding: canonicalNumber(options.boundsPadding),
13751
+ ...options.forceField ? { forceField: true } : {},
13752
+ ...options.meshing ? { meshing: cloneSdfCompileMeshingSettings(options.meshing) } : {},
13713
13753
  edgeLabels: options.edgeLabels ? { ...options.edgeLabels } : void 0
13714
13754
  };
13715
13755
  }
@@ -13811,14 +13851,14 @@ function sub$2(a2, b) {
13811
13851
  function dot$3(a2, b) {
13812
13852
  return a2[0] * b[0] + a2[1] * b[1] + a2[2] * b[2];
13813
13853
  }
13814
- function cross$4(a2, b) {
13854
+ function cross$3(a2, b) {
13815
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]];
13816
13856
  }
13817
13857
  function rotateAroundAxis(v, axis, angleRad) {
13818
13858
  const c2 = Math.cos(angleRad);
13819
13859
  const s = Math.sin(angleRad);
13820
13860
  const term1 = scale$2(v, c2);
13821
- const term2 = scale$2(cross$4(axis, v), s);
13861
+ const term2 = scale$2(cross$3(axis, v), s);
13822
13862
  const term3 = scale$2(axis, dot$3(axis, v) * (1 - c2));
13823
13863
  return add$2(add$2(term1, term2), term3);
13824
13864
  }
@@ -14094,13 +14134,13 @@ function sweepPathToPolylineAdaptive(path, baseSamples = 48) {
14094
14134
  pts.push(evalPathAt(path, 1));
14095
14135
  return pts;
14096
14136
  }
14097
- const EPS$2 = 1e-8;
14137
+ const EPS$1 = 1e-8;
14098
14138
  function midpoint$1(start, end) {
14099
14139
  return [(start[0] + end[0]) * 0.5, (start[1] + end[1]) * 0.5, (start[2] + end[2]) * 0.5];
14100
14140
  }
14101
14141
  function normalize$3(v) {
14102
14142
  const len = Math.hypot(v[0], v[1], v[2]);
14103
- 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");
14104
14144
  return [v[0] / len, v[1] / len, v[2] / len];
14105
14145
  }
14106
14146
  function subtract(a2, b) {
@@ -14179,7 +14219,7 @@ function rigidTransformForEdgeStep(step) {
14179
14219
  case "mirror": {
14180
14220
  const [nx0, ny0, nz0] = [step.normalX, step.normalY, step.normalZ];
14181
14221
  const len = Math.hypot(nx0, ny0, nz0);
14182
- if (len <= EPS$2) return Transform.identity();
14222
+ if (len <= EPS$1) return Transform.identity();
14183
14223
  const nx = nx0 / len;
14184
14224
  const ny = ny0 / len;
14185
14225
  const nz = nz0 / len;
@@ -14476,7 +14516,7 @@ function isRectangleProfile(points) {
14476
14516
  return [next[0] - point[0], next[1] - point[1]];
14477
14517
  });
14478
14518
  const lengths = vectors.map(([x2, y2]) => Math.hypot(x2, y2));
14479
- if (lengths.some((length4) => length4 <= EPS$2)) return false;
14519
+ if (lengths.some((length4) => length4 <= EPS$1)) return false;
14480
14520
  const dot01 = vectors[0][0] * vectors[1][0] + vectors[0][1] * vectors[1][1];
14481
14521
  const dot12 = vectors[1][0] * vectors[2][0] + vectors[1][1] * vectors[2][1];
14482
14522
  const dot23 = vectors[2][0] * vectors[3][0] + vectors[2][1] * vectors[3][1];
@@ -16415,7 +16455,9 @@ function lowerLoftShellToConcretePlan(plan, thickness, openFaces) {
16415
16455
  profiles: innerProfiles,
16416
16456
  heights: innerHeights,
16417
16457
  edgeLength: plan.edgeLength,
16418
- boundsPadding: plan.boundsPadding
16458
+ boundsPadding: plan.boundsPadding,
16459
+ ...plan.forceField ? { forceField: true } : {},
16460
+ ...plan.meshing ? { meshing: { ...plan.meshing } } : {}
16419
16461
  };
16420
16462
  return { ok: true, plan: buildBooleanShapeCompilePlan("difference", [plan, inner]) };
16421
16463
  }
@@ -16553,6 +16595,197 @@ function lowerShellShapeCompilePlanToConcretePlan(plan) {
16553
16595
  }
16554
16596
  return lowerBaseShellPlanToConcretePlan(plan.base, plan.thickness, normalizeShellOpenFaces(plan.openFaces));
16555
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
+ }
16556
16789
  const SHAPE_BACKEND_MARKER = Symbol.for("forgecad.shapeBackend");
16557
16790
  function isShapeBackend(value) {
16558
16791
  return Boolean(value && typeof value === "object" && value[SHAPE_BACKEND_MARKER] === true);
@@ -16606,60 +16839,26 @@ function recordEvent(event) {
16606
16839
  runEvents.push(next);
16607
16840
  if (runEvents.length > MAX_RECORDED_EVENTS) runEvents = runEvents.slice(-MAX_RECORDED_EVENTS);
16608
16841
  }
16609
- function stableGeometryEncode(value, arrayMember) {
16610
- if (value === void 0 || typeof value === "function" || typeof value === "symbol") {
16611
- return arrayMember ? "null" : void 0;
16612
- }
16613
- if (value === null || typeof value === "number" || typeof value === "boolean" || typeof value === "string") {
16614
- return JSON.stringify(value);
16615
- }
16616
- if (Array.isArray(value)) {
16617
- return `[${value.map((item) => stableGeometryEncode(item, true) ?? "null").join(",")}]`;
16618
- }
16619
- const record = value;
16620
- if (record.kind === "queryOwner" && record.base) {
16621
- return stableGeometryEncode(record.base, arrayMember);
16622
- }
16623
- const entries = Object.entries(record).sort(([left], [right]) => left.localeCompare(right));
16624
- const encodedEntries = [];
16625
- for (const [key, item] of entries) {
16626
- if (key.startsWith("_") || key === "owner" || key === "queryPropagation") continue;
16627
- const encoded = stableGeometryEncode(item, false);
16628
- if (encoded !== void 0) encodedEntries.push(`${JSON.stringify(key)}:${encoded}`);
16629
- }
16630
- return `{${encodedEntries.join(",")}}`;
16631
- }
16632
- function stableCacheOpportunityEncode(value, arrayMember) {
16633
- if (value === void 0 || typeof value === "function" || typeof value === "symbol") {
16634
- return arrayMember ? "null" : void 0;
16635
- }
16636
- if (value === null || typeof value === "number" || typeof value === "boolean" || typeof value === "string") {
16637
- return JSON.stringify(value);
16638
- }
16639
- if (Array.isArray(value)) {
16640
- return `[${value.map((item) => stableCacheOpportunityEncode(item, true) ?? "null").join(",")}]`;
16641
- }
16642
- const record = value;
16643
- if (record.kind === "queryOwner" && record.base) {
16644
- return stableCacheOpportunityEncode(record.base, arrayMember);
16645
- }
16646
- let encodedRecord = record;
16647
- if (record.kind === "transform" && record.base) {
16648
- const retainedSteps = Array.isArray(record.steps) ? record.steps.filter((step) => step.kind === "scale") : [];
16649
- if (retainedSteps.length === 0) return stableCacheOpportunityEncode(record.base, arrayMember);
16650
- encodedRecord = { kind: "transform", base: record.base, steps: retainedSteps };
16651
- }
16652
- const entries = Object.entries(encodedRecord).sort(([left], [right]) => left.localeCompare(right));
16653
- const encodedEntries = [];
16654
- for (const [key, item] of entries) {
16655
- if (key.startsWith("_") || key === "owner" || key === "queryPropagation") continue;
16656
- const encoded = stableCacheOpportunityEncode(item, false);
16657
- 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) };
16658
16858
  }
16659
- return `{${encodedEntries.join(",")}}`;
16660
- }
16859
+ });
16661
16860
  function shapeGeometryCacheKey(plan) {
16662
- return `${SHAPE_GEOMETRY_CACHE_KEY_VERSION}:${stableGeometryEncode(plan, false) ?? "null"}`;
16861
+ return `${SHAPE_GEOMETRY_CACHE_KEY_VERSION}:${geometryPlanHasher(plan)}`;
16663
16862
  }
16664
16863
  function planComplexityScore(value) {
16665
16864
  if (!value || typeof value !== "object") return 0;
@@ -16718,8 +16917,7 @@ function planComplexityScore(value) {
16718
16917
  }
16719
16918
  }
16720
16919
  function estimateCacheRetainedMb(plan) {
16721
- var _a3;
16722
- const encodedLength = ((_a3 = stableCacheOpportunityEncode(plan, false)) == null ? void 0 : _a3.length) ?? 0;
16920
+ const encodedLength = estimatePlanEncodedLength(plan);
16723
16921
  const serializedComplexityMb = encodedLength / 24e3;
16724
16922
  return round2(0.08 + planComplexityScore(plan) * 0.09 + serializedComplexityMb);
16725
16923
  }
@@ -16737,26 +16935,62 @@ function splitCacheablePlacement(plan) {
16737
16935
  }
16738
16936
  return { basePlan: plan, placementSteps: [] };
16739
16937
  }
16938
+ const uncacheableReasonMemo = /* @__PURE__ */ new WeakMap();
16740
16939
  function findUncacheableReason(value) {
16741
16940
  if (value === void 0 || value === null) return null;
16742
16941
  if (typeof value === "function" || typeof value === "symbol") return "plan contains runtime-only values";
16743
16942
  if (typeof value !== "object") return null;
16744
- if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) return "plan contains binary file data";
16745
- if (Array.isArray(value)) {
16746
- for (const item of value) {
16747
- const reason = findUncacheableReason(item);
16748
- 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);
16749
16986
  }
16750
- return null;
16751
16987
  }
16752
- const record = value;
16753
- if (record.kind === "importedMesh" || record.kind === "importedStep") return "plan depends on imported file contents";
16754
- for (const [key, item] of Object.entries(record)) {
16755
- if (key.startsWith("_")) continue;
16756
- const reason = findUncacheableReason(item);
16757
- 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);
16758
16992
  }
16759
- return null;
16993
+ return reason;
16760
16994
  }
16761
16995
  function applyPlacementStep(backend, step) {
16762
16996
  switch (step.kind) {
@@ -17433,8 +17667,9 @@ function analyzeNodeUV(node, toLocal) {
17433
17667
  if (result.majorRadius !== void 0) result.majorRadius *= node.factor;
17434
17668
  return result;
17435
17669
  }
17436
- // ── Shell — UV comes from the inner shape ──
17670
+ // ── Shell / offset — UV comes from the inner shape ──
17437
17671
  case "sdf:shell":
17672
+ case "sdf:offset":
17438
17673
  return analyzeNodeUV(node.child, toLocal);
17439
17674
  // ── CSG — take UV from the first (primary) child ──
17440
17675
  case "sdf:union":
@@ -17970,6 +18205,11 @@ function compileSdfNode3(node) {
17970
18205
  const t = node.thickness * 0.5;
17971
18206
  return (x2, y2, z2) => abs(fn(x2, y2, z2)) - t;
17972
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
+ }
17973
18213
  case "sdf:displace": {
17974
18214
  const fn = compileSdfNode3(node.child);
17975
18215
  const constEntries = Object.entries(node.constants ?? {});
@@ -18463,6 +18703,10 @@ function emitSdfProgramNode(b, node, x2, y2, z2) {
18463
18703
  const child = emitSdfProgramNode(b, node.child, x2, y2, z2);
18464
18704
  return b.sub(b.abs(child), b.constant(node.thickness * 0.5));
18465
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
+ }
18466
18710
  case "sdf:onion": {
18467
18711
  let d2 = emitSdfProgramNode(b, node.child, x2, y2, z2);
18468
18712
  for (let i = 0; i < node.layers; i++) d2 = b.sub(b.abs(d2), b.constant(node.thickness));
@@ -18611,6 +18855,7 @@ function getUnsupportedSdfProgramReason(node) {
18611
18855
  case "sdf:bend":
18612
18856
  case "sdf:repeat":
18613
18857
  case "sdf:shell":
18858
+ case "sdf:offset":
18614
18859
  case "sdf:onion":
18615
18860
  return getUnsupportedSdfProgramReason(node.child);
18616
18861
  default:
@@ -18814,7 +19059,19 @@ function simplifyMesh(triVerts, vertProperties, targetRatio, maxError) {
18814
19059
  if (!_simplifier) {
18815
19060
  throw new Error("meshoptimizer not initialized — call initMeshoptimizer() first");
18816
19061
  }
18817
- 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;
18818
19075
  const [simplified] = _simplifier.simplify(
18819
19076
  triVerts,
18820
19077
  vertProperties,
@@ -20224,11 +20481,12 @@ function profileMayContainInteriorLoopsForOCCT(plan) {
20224
20481
  return false;
20225
20482
  }
20226
20483
  }
20484
+ const occtLoweredCache = /* @__PURE__ */ new WeakMap();
20227
20485
  function lowerShapeCompilePlanToOCCT(plan, oc) {
20228
- const cached = plan._occtCache;
20486
+ const cached = occtLoweredCache.get(plan);
20229
20487
  if (cached) return cached;
20230
20488
  const shape = _lowerShapeCompilePlanToOCCTInner(plan, oc);
20231
- plan._occtCache = shape;
20489
+ occtLoweredCache.set(plan, shape);
20232
20490
  return shape;
20233
20491
  }
20234
20492
  function _lowerShapeCompilePlanToOCCTInner(plan, oc) {
@@ -28766,16 +29024,16 @@ function surfaceNets(sdfFn, bounds, edgeLength2) {
28766
29024
  numTris: faces.length / 3
28767
29025
  };
28768
29026
  }
28769
- const EPS$1 = 1e-9;
29027
+ const EPS = 1e-9;
28770
29028
  function finitePositive$1(value) {
28771
- return Number.isFinite(value) && value > EPS$1;
29029
+ return Number.isFinite(value) && value > EPS;
28772
29030
  }
28773
29031
  function clampNonNegative(value) {
28774
- return Math.abs(value) <= EPS$1 ? 0 : value;
29032
+ return Math.abs(value) <= EPS ? 0 : value;
28775
29033
  }
28776
29034
  function distancePreservingMatrixScale(matrix) {
28777
29035
  if (matrix.length !== 16 || matrix.some((value) => !Number.isFinite(value))) return null;
28778
- 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) {
28779
29037
  return null;
28780
29038
  }
28781
29039
  const col0 = [matrix[0], matrix[1], matrix[2]];
@@ -28787,8 +29045,8 @@ function distancePreservingMatrixScale(matrix) {
28787
29045
  const sy = length4(col1);
28788
29046
  const sz = length4(col2);
28789
29047
  if (!finitePositive$1(sx) || !finitePositive$1(sy) || !finitePositive$1(sz)) return null;
28790
- if (Math.abs(sx - sy) > EPS$1 || Math.abs(sx - sz) > EPS$1) return null;
28791
- 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;
28792
29050
  return sx;
28793
29051
  }
28794
29052
  function transformStepDistanceScale(step) {
@@ -28804,7 +29062,7 @@ function transformStepDistanceScale(step) {
28804
29062
  const sy = Math.abs(step.y);
28805
29063
  const sz = Math.abs(step.z);
28806
29064
  if (!finitePositive$1(sx) || !finitePositive$1(sy) || !finitePositive$1(sz)) return null;
28807
- 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;
28808
29066
  }
28809
29067
  }
28810
29068
  }
@@ -28835,7 +29093,7 @@ function cloneTransformStep(step) {
28835
29093
  }
28836
29094
  }
28837
29095
  function translatedPlan(base, z2) {
28838
- if (Math.abs(z2) <= EPS$1) return base;
29096
+ if (Math.abs(z2) <= EPS) return base;
28839
29097
  return {
28840
29098
  kind: "transform",
28841
29099
  base,
@@ -28852,7 +29110,7 @@ function offsetCylinderDimensions(plan, thickness) {
28852
29110
  const height = zMax - zMin;
28853
29111
  const radiusBottom = plan.radius + thickness * (normalScale - slope);
28854
29112
  const offsetRadiusTop = radiusTop + thickness * (normalScale + slope);
28855
- if (!finitePositive$1(height) || radiusBottom < -EPS$1 || offsetRadiusTop < -EPS$1) return null;
29113
+ if (!finitePositive$1(height) || radiusBottom < -EPS || offsetRadiusTop < -EPS) return null;
28856
29114
  return {
28857
29115
  zMin,
28858
29116
  height,
@@ -28876,7 +29134,7 @@ function transformProfilePoint(point, transform) {
28876
29134
  const nx = transform.normalX;
28877
29135
  const ny = transform.normalY;
28878
29136
  const len = Math.hypot(nx, ny);
28879
- if (len <= EPS$1) return point;
29137
+ if (len <= EPS) return point;
28880
29138
  const ux = nx / len;
28881
29139
  const uy = ny / len;
28882
29140
  const d2 = point[0] * ux + point[1] * uy;
@@ -28890,7 +29148,7 @@ function transformProfilePointThrough(point, transforms) {
28890
29148
  return out;
28891
29149
  }
28892
29150
  function sameScalar$2(a2, b) {
28893
- return Math.abs(a2 - b) <= EPS$1;
29151
+ return Math.abs(a2 - b) <= EPS;
28894
29152
  }
28895
29153
  function sameProfilePoint(a2, b) {
28896
29154
  return sameScalar$2(a2[0], b[0]) && sameScalar$2(a2[1], b[1]);
@@ -28944,7 +29202,7 @@ function rectangleFootprintFromProfile(plan) {
28944
29202
  const [xMin, xMax] = xs;
28945
29203
  const [zMin, zMax] = zs;
28946
29204
  if (xMin == null || xMax == null || zMin == null || zMax == null) return null;
28947
- 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;
28948
29206
  const hasCorner = (x2, z2) => points.some(([px, pz]) => sameScalar$2(px, x2) && sameScalar$2(pz, z2));
28949
29207
  if (!hasCorner(xMin, zMin) || !hasCorner(xMax, zMin) || !hasCorner(xMax, zMax) || !hasCorner(xMin, zMax)) return null;
28950
29208
  return {
@@ -28967,7 +29225,7 @@ function circleFootprintFromProfile(plan) {
28967
29225
  const yScale = Math.hypot(yAxis[0], yAxis[1]);
28968
29226
  const dot2 = xAxis[0] * yAxis[0] + xAxis[1] * yAxis[1];
28969
29227
  if (!finitePositive$1(xScale) || !finitePositive$1(yScale)) return null;
28970
- 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;
28971
29229
  return {
28972
29230
  center,
28973
29231
  radius: radius * xScale,
@@ -28975,11 +29233,11 @@ function circleFootprintFromProfile(plan) {
28975
29233
  };
28976
29234
  }
28977
29235
  function fullCircleRevolveTorusPlan(plan, minorRadiusOffset = 0) {
28978
- if (Math.abs(plan.degrees - 360) > EPS$1) return null;
29236
+ if (Math.abs(plan.degrees - 360) > EPS) return null;
28979
29237
  const circle = circleFootprintFromProfile(plan.profile);
28980
- if (!circle || circle.center[0] <= EPS$1) return null;
29238
+ if (!circle || circle.center[0] <= EPS) return null;
28981
29239
  const minorRadius = circle.radius + minorRadiusOffset;
28982
- if (!finitePositive$1(minorRadius) || minorRadius >= circle.center[0] - EPS$1) return null;
29240
+ if (!finitePositive$1(minorRadius) || minorRadius >= circle.center[0] - EPS) return null;
28983
29241
  return translatedPlan(
28984
29242
  {
28985
29243
  kind: "torus",
@@ -28992,7 +29250,7 @@ function fullCircleRevolveTorusPlan(plan, minorRadiusOffset = 0) {
28992
29250
  }
28993
29251
  function fullAxisRectRevolveCylinderPlan(plan) {
28994
29252
  const rectangle = fullRectRevolveSurfacePlan(plan);
28995
- if (!rectangle || rectangle.innerRadius > EPS$1) return null;
29253
+ if (!rectangle || rectangle.innerRadius > EPS) return null;
28996
29254
  return translatedPlan(
28997
29255
  {
28998
29256
  kind: "cylinder",
@@ -29015,20 +29273,20 @@ function rectRevolveSurfacePlan(plan) {
29015
29273
  };
29016
29274
  }
29017
29275
  function fullRectRevolveSurfacePlan(plan) {
29018
- if (Math.abs(plan.degrees - 360) > EPS$1) return null;
29276
+ if (Math.abs(plan.degrees - 360) > EPS) return null;
29019
29277
  return rectRevolveSurfacePlan(plan);
29020
29278
  }
29021
29279
  function offsetFullRectRevolvePlan(plan, thickness) {
29022
29280
  const cylinderPlan = fullAxisRectRevolveCylinderPlan(plan);
29023
29281
  if (cylinderPlan) return offsetSolidAnalyticPrimitivePlan(cylinderPlan, thickness);
29024
29282
  const rectangle = fullRectRevolveSurfacePlan(plan);
29025
- if (!rectangle || rectangle.innerRadius <= EPS$1) return null;
29283
+ if (!rectangle || rectangle.innerRadius <= EPS) return null;
29026
29284
  const innerRadius = rectangle.innerRadius - thickness;
29027
29285
  const outerRadius = rectangle.outerRadius + thickness;
29028
29286
  const height = rectangle.zMax - rectangle.zMin + 2 * thickness;
29029
- 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;
29030
29288
  const zCenter = (rectangle.zMin + rectangle.zMax) / 2;
29031
- if (innerRadius <= EPS$1) {
29289
+ if (innerRadius <= EPS) {
29032
29290
  return translatedPlan(
29033
29291
  {
29034
29292
  kind: "cylinder",
@@ -29107,7 +29365,7 @@ function offsetSolidAnalyticPrimitivePlan(base, thickness) {
29107
29365
  }
29108
29366
  case "torus": {
29109
29367
  const minorRadius = base.minorRadius + thickness;
29110
- if (!finitePositive$1(minorRadius) || minorRadius >= base.majorRadius - EPS$1) return null;
29368
+ if (!finitePositive$1(minorRadius) || minorRadius >= base.majorRadius - EPS) return null;
29111
29369
  return {
29112
29370
  kind: "torus",
29113
29371
  majorRadius: base.majorRadius,
@@ -29711,7 +29969,7 @@ function sub$1(a2, b) {
29711
29969
  function scale$1(v, scalar) {
29712
29970
  return [v[0] * scalar, v[1] * scalar, v[2] * scalar];
29713
29971
  }
29714
- function cross$3(a2, b) {
29972
+ function cross$2(a2, b) {
29715
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]];
29716
29974
  }
29717
29975
  function isFiniteNumber(value) {
@@ -29947,8 +30205,8 @@ function boundsCorners(bounds) {
29947
30205
  }
29948
30206
  function perpendicularAxes(normal) {
29949
30207
  const seed = Math.abs(normal[2]) < 0.9 ? [0, 0, 1] : [0, 1, 0];
29950
- const uAxis = normalizeVec3$1(cross$3(seed, normal));
29951
- 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));
29952
30210
  return { uAxis, vAxis };
29953
30211
  }
29954
30212
  function createBoundedHalfSpace(bounds, normal, originOffset) {
@@ -30016,7 +30274,7 @@ function faceAxes(face) {
30016
30274
  for (const vertex of face.vertices.slice(1)) {
30017
30275
  const uAxis = normalizeVec3$1([vertex[0] - origin[0], vertex[1] - origin[1], vertex[2] - origin[2]]);
30018
30276
  if (Math.hypot(...uAxis) <= 1e-12) continue;
30019
- const vAxis = normalizeVec3$1(cross$3(face.normal, uAxis));
30277
+ const vAxis = normalizeVec3$1(cross$2(face.normal, uAxis));
30020
30278
  if (Math.hypot(...vAxis) <= 1e-12) continue;
30021
30279
  return {
30022
30280
  uAxis,
@@ -32577,7 +32835,7 @@ function isStraightMonotonePolyline(points) {
32577
32835
  for (const point of points) {
32578
32836
  const offset = subtract3$1(point, start);
32579
32837
  const projection = dot3$3(offset, axis) / axisLengthSq;
32580
- const lineDistance = vectorLength3$1(cross3$4(offset, axis)) / axisLength;
32838
+ const lineDistance = vectorLength3$1(cross3$5(offset, axis)) / axisLength;
32581
32839
  if (lineDistance > 1e-5 || projection < -1e-6 || projection > 1 + 1e-6 || projection + 1e-6 < previousProjection) {
32582
32840
  return false;
32583
32841
  }
@@ -32653,7 +32911,7 @@ function isDistancePreservingMatrix$1(matrix) {
32653
32911
  const col0 = [matrix[0], matrix[1], matrix[2]];
32654
32912
  const col1 = [matrix[4], matrix[5], matrix[6]];
32655
32913
  const col2 = [matrix[8], matrix[9], matrix[10]];
32656
- const det = dot3$3(col0, cross3$4(col1, col2));
32914
+ const det = dot3$3(col0, cross3$5(col1, col2));
32657
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;
32658
32916
  }
32659
32917
  function offsetSolidTransformDistanceScale(step) {
@@ -32898,41 +33156,41 @@ function surfaceGridForFillPlan(plan) {
32898
33156
  }
32899
33157
  return grid;
32900
33158
  }
32901
- function add3(a2, b) {
33159
+ function add3$1(a2, b) {
32902
33160
  return [a2[0] + b[0], a2[1] + b[1], a2[2] + b[2]];
32903
33161
  }
32904
- function scale3(v, s) {
33162
+ function scale3$1(v, s) {
32905
33163
  return [v[0] * s, v[1] * s, v[2] * s];
32906
33164
  }
32907
33165
  function analyticYForFrame(axis, xAxis) {
32908
- return normalizedVector3(cross3$4(axis, xAxis), "analytic surface yAxis");
33166
+ return normalizedVector3(cross3$5(axis, xAxis), "analytic surface yAxis");
32909
33167
  }
32910
33168
  function radialPoint(xAxis, yAxis, u2, radius) {
32911
- 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));
32912
33170
  }
32913
33171
  function analyticSurfacePoint(plan, u2, v) {
32914
33172
  switch (plan.kind) {
32915
33173
  case "plane":
32916
- 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));
32917
33175
  case "cylinder": {
32918
33176
  const yAxis = analyticYForFrame(plan.axis, plan.xAxis);
32919
- 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));
32920
33178
  }
32921
33179
  case "cone": {
32922
33180
  const yAxis = analyticYForFrame(plan.axis, plan.xAxis);
32923
33181
  const t = (v - plan.vMin) / (plan.vMax - plan.vMin);
32924
33182
  const radius = plan.radiusBottom + (plan.radiusTop - plan.radiusBottom) * t;
32925
- 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));
32926
33184
  }
32927
33185
  case "sphere": {
32928
33186
  const yAxis = analyticYForFrame(plan.axis, plan.xAxis);
32929
33187
  const radial = radialPoint(plan.xAxis, yAxis, u2, plan.radius * Math.cos(v));
32930
- 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)));
32931
33189
  }
32932
33190
  case "torus": {
32933
33191
  const yAxis = analyticYForFrame(plan.axis, plan.xAxis);
32934
33192
  const radial = radialPoint(plan.xAxis, yAxis, u2, plan.majorRadius + plan.minorRadius * Math.cos(v));
32935
- 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)));
32936
33194
  }
32937
33195
  default:
32938
33196
  return assertExhaustive(plan);
@@ -33050,7 +33308,7 @@ function triangleNormal(vertices, triangle) {
33050
33308
  const c2 = vertices[triangle[2]];
33051
33309
  const ab = [b[0] - a2[0], b[1] - a2[1], b[2] - a2[2]];
33052
33310
  const ac = [c2[0] - a2[0], c2[1] - a2[1], c2[2] - a2[2]];
33053
- const normal = cross3$4(ab, ac);
33311
+ const normal = cross3$5(ab, ac);
33054
33312
  const len = Math.hypot(normal[0], normal[1], normal[2]);
33055
33313
  if (len <= 1e-12) return null;
33056
33314
  return [normal[0] / len, normal[1] / len, normal[2] / len];
@@ -33196,7 +33454,7 @@ function lowerSurfaceSolidPlan(plan) {
33196
33454
  );
33197
33455
  }
33198
33456
  }
33199
- function cross3$4(a2, b) {
33457
+ function cross3$5(a2, b) {
33200
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]];
33201
33459
  }
33202
33460
  function fromSlicesPlaneFrame(normalInput) {
@@ -33212,8 +33470,8 @@ function fromSlicesPlaneFrame(normalInput) {
33212
33470
  return { u: [0, 1, 0], v: [0, 0, nx > 0 ? 1 : -1], normal };
33213
33471
  }
33214
33472
  const reference = Math.abs(nx) < 0.9 ? [1, 0, 0] : [0, 1, 0];
33215
- const u2 = normalizedVector3(cross3$4(reference, normal), "Shape.fromSlices profile u axis");
33216
- 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 };
33217
33475
  }
33218
33476
  function fromSlicesLocalToWorldMatrix(normal) {
33219
33477
  const frame = fromSlicesPlaneFrame(normal);
@@ -36632,29 +36890,11 @@ function lowerExactSlicedShapeCompilePlanToTruckProfileBackend(plan, offset) {
36632
36890
  return profilePlan ? lowerProfileCompilePlanToTruckProfileBackend(profilePlan) : null;
36633
36891
  }
36634
36892
  const SHAPE_COMPILE_PLAN_CACHE_KEY_VERSION = "shape-plan-v1";
36635
- function stableJsonEncode(value, arrayMember) {
36636
- if (value === void 0 || typeof value === "function" || typeof value === "symbol") {
36637
- return arrayMember ? "null" : void 0;
36638
- }
36639
- if (value === null || typeof value === "number" || typeof value === "boolean" || typeof value === "string") {
36640
- return JSON.stringify(value);
36641
- }
36642
- if (Array.isArray(value)) {
36643
- return `[${value.map((item) => stableJsonEncode(item, true) ?? "null").join(",")}]`;
36644
- }
36645
- const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
36646
- const encodedEntries = [];
36647
- for (const [key, item] of entries) {
36648
- const encoded = stableJsonEncode(item, false);
36649
- if (encoded !== void 0) encodedEntries.push(`${JSON.stringify(key)}:${encoded}`);
36650
- }
36651
- return `{${encodedEntries.join(",")}}`;
36652
- }
36653
- function stableJsonStringify(value) {
36654
- return stableJsonEncode(value, false) ?? "null";
36655
- }
36893
+ const exactPlanHasher = createStructuralHasher({
36894
+ skipKey: (key) => key.startsWith("_") || key === "owner" || key === "queryPropagation"
36895
+ });
36656
36896
  function shapeCompilePlanCacheKey(plan) {
36657
- return `${SHAPE_COMPILE_PLAN_CACHE_KEY_VERSION}:${stableJsonStringify(plan)}`;
36897
+ return `${SHAPE_COMPILE_PLAN_CACHE_KEY_VERSION}:${exactPlanHasher(plan)}`;
36658
36898
  }
36659
36899
  function formatFaceQuery(query) {
36660
36900
  const parts = [];
@@ -36984,7 +37224,7 @@ function normalizeFaceSelector(selector) {
36984
37224
  }
36985
37225
  return { compilePlanName: null, query: selector };
36986
37226
  }
36987
- function cross$2(a2, b) {
37227
+ function cross$1(a2, b) {
36988
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]];
36989
37229
  }
36990
37230
  function dot$1(a2, b) {
@@ -36997,8 +37237,8 @@ function normVec3(v) {
36997
37237
  }
36998
37238
  function tangentFrame(normal) {
36999
37239
  const ref = Math.abs(normal[0]) < 0.9 ? [1, 0, 0] : [0, 1, 0];
37000
- const v = normVec3(cross$2(normal, ref));
37001
- const u2 = normVec3(cross$2(v, normal));
37240
+ const v = normVec3(cross$1(normal, ref));
37241
+ const u2 = normVec3(cross$1(v, normal));
37002
37242
  return { u: u2, v };
37003
37243
  }
37004
37244
  const NORMAL_COS_EPS$1 = 0.9998;
@@ -37017,7 +37257,7 @@ function clusterMeshFaces(shape) {
37017
37257
  const v2 = [vertProperties[i2 * numProp], vertProperties[i2 * numProp + 1], vertProperties[i2 * numProp + 2]];
37018
37258
  const e1 = [v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]];
37019
37259
  const e2 = [v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2]];
37020
- const rawCross = cross$2(e1, e2);
37260
+ const rawCross = cross$1(e1, e2);
37021
37261
  const normal = normVec3(rawCross);
37022
37262
  if (!normal) continue;
37023
37263
  const crossLen = Math.sqrt(rawCross[0] * rawCross[0] + rawCross[1] * rawCross[1] + rawCross[2] * rawCross[2]);
@@ -37427,14 +37667,14 @@ function normalize2d(vec2) {
37427
37667
  if (len < 1e-12) return [1, 0];
37428
37668
  return [vec2[0] / len, vec2[1] / len];
37429
37669
  }
37430
- function cross3$3(a2, b) {
37670
+ function cross3$4(a2, b) {
37431
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]];
37432
37672
  }
37433
37673
  function orthonormalBasisFromNormal(normal) {
37434
37674
  const n = normalizeAxis(normal);
37435
37675
  const seed = Math.abs(n[2]) < 0.9 ? [0, 0, 1] : [0, 1, 0];
37436
- const u2 = normalizeAxis(cross3$3(seed, n));
37437
- const v = normalizeAxis(cross3$3(n, u2));
37676
+ const u2 = normalizeAxis(cross3$4(seed, n));
37677
+ const v = normalizeAxis(cross3$4(n, u2));
37438
37678
  return { u: u2, v };
37439
37679
  }
37440
37680
  function faceFrom2DEdge(name, start, end, zMid, ownerQuery) {
@@ -38271,7 +38511,7 @@ function resolveShapeFaceTableInternal(plan, owner) {
38271
38511
  forwardStart3[1] - reverseStart3[1],
38272
38512
  forwardStart3[2] - reverseStart3[2]
38273
38513
  ]);
38274
- const normal = normalizeAxis(cross3$3(edgeVec, depthVec));
38514
+ const normal = normalizeAxis(cross3$4(edgeVec, depthVec));
38275
38515
  registerFace(table, {
38276
38516
  name: wall.name,
38277
38517
  normal,
@@ -38757,14 +38997,14 @@ function normalize3$1(v) {
38757
38997
  function dot3$2(a2, b) {
38758
38998
  return a2[0] * b[0] + a2[1] * b[1] + a2[2] * b[2];
38759
38999
  }
38760
- function cross3$2(a2, b) {
39000
+ function cross3$3(a2, b) {
38761
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]];
38762
39002
  }
38763
39003
  function perpendicularTo(axis) {
38764
39004
  const absX = Math.abs(axis[0]);
38765
39005
  const absZ = Math.abs(axis[2]);
38766
39006
  const seed = absX < absZ ? [1, 0, 0] : [0, 0, 1];
38767
- return normalize3$1(cross3$2(axis, seed));
39007
+ return normalize3$1(cross3$3(axis, seed));
38768
39008
  }
38769
39009
  function normalizePortInput(input) {
38770
39010
  let origin;
@@ -38945,18 +39185,18 @@ function normalize3(v) {
38945
39185
  if (l < 1e-10) throw new Error("Cannot normalize zero-length vector");
38946
39186
  return [v[0] / l, v[1] / l, v[2] / l];
38947
39187
  }
38948
- function cross3$1(a2, b) {
39188
+ function cross3$2(a2, b) {
38949
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]];
38950
39190
  }
38951
- function sub3(a2, b) {
39191
+ function sub3$1(a2, b) {
38952
39192
  return [a2[0] - b[0], a2[1] - b[1], a2[2] - b[2]];
38953
39193
  }
38954
39194
  function negate3(v) {
38955
39195
  return [-v[0], -v[1], -v[2]];
38956
39196
  }
38957
39197
  function alignmentMatrix(childOrigin, childAxis, childUp, parentOrigin, parentAxis, parentUp) {
38958
- const cRight = normalize3(cross3$1(childAxis, childUp));
38959
- const pRight = normalize3(cross3$1(parentAxis, parentUp));
39198
+ const cRight = normalize3(cross3$2(childAxis, childUp));
39199
+ const pRight = normalize3(cross3$2(parentAxis, parentUp));
38960
39200
  const r00 = pRight[0] * cRight[0] + parentUp[0] * childUp[0] + parentAxis[0] * childAxis[0];
38961
39201
  const r01 = pRight[0] * cRight[1] + parentUp[0] * childUp[1] + parentAxis[0] * childAxis[1];
38962
39202
  const r02 = pRight[0] * cRight[2] + parentUp[0] * childUp[2] + parentAxis[0] * childAxis[2];
@@ -38971,7 +39211,7 @@ function alignmentMatrix(childOrigin, childAxis, childUp, parentOrigin, parentAx
38971
39211
  r10 * childOrigin[0] + r11 * childOrigin[1] + r12 * childOrigin[2],
38972
39212
  r20 * childOrigin[0] + r21 * childOrigin[1] + r22 * childOrigin[2]
38973
39213
  ];
38974
- const t = sub3(parentOrigin, rc);
39214
+ const t = sub3$1(parentOrigin, rc);
38975
39215
  return Transform.from([r00, r10, r20, 0, r01, r11, r21, 0, r02, r12, r22, 0, t[0], t[1], t[2], 1]);
38976
39216
  }
38977
39217
  function computeSinglePairAlignment(childPort, targetPort) {
@@ -39010,8 +39250,8 @@ function computeMultiPairAlignment(pairs, childPorts, targetPorts, tolerance = 0
39010
39250
  [0, 0, 0]
39011
39251
  ];
39012
39252
  for (const p2 of pairs) {
39013
- const s = sub3(p2.childOrigin, srcCentroid);
39014
- const t2 = sub3(p2.targetOrigin, tgtCentroid);
39253
+ const s = sub3$1(p2.childOrigin, srcCentroid);
39254
+ const t2 = sub3$1(p2.targetOrigin, tgtCentroid);
39015
39255
  for (let i = 0; i < 3; i++) {
39016
39256
  for (let j = 0; j < 3; j++) {
39017
39257
  h[i][j] += s[i] * t2[j];
@@ -39024,7 +39264,7 @@ function computeMultiPairAlignment(pairs, childPorts, targetPorts, tolerance = 0
39024
39264
  R[1][0] * srcCentroid[0] + R[1][1] * srcCentroid[1] + R[1][2] * srcCentroid[2],
39025
39265
  R[2][0] * srcCentroid[0] + R[2][1] * srcCentroid[1] + R[2][2] * srcCentroid[2]
39026
39266
  ];
39027
- const t = sub3(tgtCentroid, rSrc);
39267
+ const t = sub3$1(tgtCentroid, rSrc);
39028
39268
  const transform = Transform.from([
39029
39269
  R[0][0],
39030
39270
  R[1][0],
@@ -39046,7 +39286,7 @@ function computeMultiPairAlignment(pairs, childPorts, targetPorts, tolerance = 0
39046
39286
  const residuals = [];
39047
39287
  for (const p2 of pairs) {
39048
39288
  const transformed = transform.point(p2.childOrigin);
39049
- const diff = sub3(transformed, p2.targetOrigin);
39289
+ const diff = sub3$1(transformed, p2.targetOrigin);
39050
39290
  residuals.push(len3(diff));
39051
39291
  }
39052
39292
  const maxResidual = Math.max(...residuals);
@@ -39247,7 +39487,7 @@ function getConnectorDistance(ports, nameA, nameB) {
39247
39487
  const b = ports[nameB];
39248
39488
  if (!a2) throw new Error(`connectorDistance: unknown connector "${nameA}"`);
39249
39489
  if (!b) throw new Error(`connectorDistance: unknown connector "${nameB}"`);
39250
- const d2 = sub3(a2.origin, b.origin);
39490
+ const d2 = sub3$1(a2.origin, b.origin);
39251
39491
  return len3(d2);
39252
39492
  }
39253
39493
  function getConnectorMeasurements(ports, name) {
@@ -39360,7 +39600,9 @@ const _ManifoldShapeBackend = class _ManifoldShapeBackend {
39360
39600
  return this.getLiveManifold("numTri()").numTri();
39361
39601
  }
39362
39602
  getMesh() {
39363
- 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;
39364
39606
  }
39365
39607
  slice(offset) {
39366
39608
  return this.getLiveManifold("slice()").slice(offset);
@@ -39661,7 +39903,7 @@ class ShapeGroup {
39661
39903
  };
39662
39904
  return this.attachTo(parent, face, opp[face], uvMap[face](u2, v, p2));
39663
39905
  }
39664
- /** 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. */
39665
39907
  rotate(axis, angleDeg, options) {
39666
39908
  requireRotateAxis(axis, "ShapeGroup.rotate()");
39667
39909
  requireFiniteAngle(angleDeg, "ShapeGroup.rotate()");
@@ -39906,6 +40148,11 @@ class ShapeGroup {
39906
40148
  * Position this group by matching connectors to a target.
39907
40149
  * Connector names support dotted paths into named children: "ChildName.connectorName".
39908
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
+ *
39909
40156
  * Overloads:
39910
40157
  * - Single pair: `matchTo(target, selfConn, targetConn, options?)`
39911
40158
  * - Dictionary (same target): `matchTo(target, { selfConn: targetConn, ... }, options?)`
@@ -41645,7 +41892,7 @@ async function initKernelManifoldOnly() {
41645
41892
  _activeBackend = "manifold";
41646
41893
  return manifoldModule;
41647
41894
  }
41648
- const DEFAULT_ACTIVE_BACKEND = "truck";
41895
+ const DEFAULT_ACTIVE_BACKEND = "manifold";
41649
41896
  let _activeBackend = DEFAULT_ACTIVE_BACKEND;
41650
41897
  let _runtimeWarn = (msg) => console.warn(msg);
41651
41898
  function unwrapShapeLike(value) {
@@ -41664,6 +41911,7 @@ const _shapePlacementRefs = /* @__PURE__ */ new WeakMap();
41664
41911
  const _shapeExplodeHint = /* @__PURE__ */ new WeakMap();
41665
41912
  const _shapeRuntimeBackends = /* @__PURE__ */ new WeakMap();
41666
41913
  const _shapeTopology = /* @__PURE__ */ new WeakMap();
41914
+ const _shapeLineageTokens = /* @__PURE__ */ new WeakMap();
41667
41915
  const _shapeFaceLabels = /* @__PURE__ */ new WeakMap();
41668
41916
  const _shapeReferenceNames = /* @__PURE__ */ new WeakMap();
41669
41917
  const _shapeReferenceAliases = /* @__PURE__ */ new WeakMap();
@@ -41724,6 +41972,10 @@ function copyShapeReferenceMetadata(source, target) {
41724
41972
  const aliases = cloneReferenceAliases(_shapeReferenceAliases.get(source));
41725
41973
  if (aliases && aliases.size > 0) _shapeReferenceAliases.set(target, aliases);
41726
41974
  }
41975
+ function copyShapeLineage(source, target) {
41976
+ const token = _shapeLineageTokens.get(source);
41977
+ if (token) _shapeLineageTokens.set(target, token);
41978
+ }
41727
41979
  function assertNonEmptyReferenceName(name, apiName) {
41728
41980
  const trimmed = name.trim();
41729
41981
  if (!trimmed) throw new Error(`${apiName} requires a non-empty reference name.`);
@@ -41792,50 +42044,25 @@ function setShapeRuntimeBackendInternal(shape, payload) {
41792
42044
  return shape;
41793
42045
  }
41794
42046
  function setShapeCompilePlanInternal(shape, plan) {
41795
- _shapeCompilePlans.set(shape, cloneShapeCompilePlan(plan));
42047
+ _shapeCompilePlans.set(shape, deepFreezePlanData(plan));
41796
42048
  recordShapeSourceSpanInternal(shape, plan);
41797
42049
  return shape;
41798
42050
  }
41799
- function cloneShapeSourceSpanRecords(records) {
41800
- return (records ?? []).map((record) => ({
41801
- planCacheKey: record.planCacheKey,
41802
- sourceSpan: { ...record.sourceSpan }
41803
- }));
41804
- }
41805
42051
  function upsertShapeSourceSpanRecord(shape, record) {
41806
- const records = cloneShapeSourceSpanRecords(_shapeSourceSpans.get(shape));
41807
- if (records.some((existing) => existing.planCacheKey === record.planCacheKey)) return;
41808
- records.push({
41809
- planCacheKey: record.planCacheKey,
41810
- sourceSpan: { ...record.sourceSpan }
41811
- });
41812
- _shapeSourceSpans.set(shape, records);
41813
- }
41814
- function stableTraceSourcePlanEncode(value, arrayMember) {
41815
- if (value === void 0 || typeof value === "function" || typeof value === "symbol") {
41816
- return arrayMember ? "null" : void 0;
41817
- }
41818
- if (value === null || typeof value === "number" || typeof value === "boolean" || typeof value === "string") {
41819
- return JSON.stringify(value);
41820
- }
41821
- if (Array.isArray(value)) {
41822
- return `[${value.map((item) => stableTraceSourcePlanEncode(item, true) ?? "null").join(",")}]`;
41823
- }
41824
- const record = value;
41825
- if (record.kind === "queryOwner" && record.base) {
41826
- return stableTraceSourcePlanEncode(record.base, arrayMember);
41827
- }
41828
- const entries = Object.entries(record).sort(([left], [right]) => left.localeCompare(right));
41829
- const encodedEntries = [];
41830
- for (const [key, item] of entries) {
41831
- if (key.startsWith("_") || key === "owner" || key === "queryPropagation") continue;
41832
- const encoded = stableTraceSourcePlanEncode(item, false);
41833
- if (encoded !== void 0) encodedEntries.push(`${JSON.stringify(key)}:${encoded}`);
42052
+ let records = _shapeSourceSpans.get(shape);
42053
+ if (!records) {
42054
+ records = /* @__PURE__ */ new Map();
42055
+ _shapeSourceSpans.set(shape, records);
41834
42056
  }
41835
- return `{${encodedEntries.join(",")}}`;
42057
+ if (records.has(record.planCacheKey)) return;
42058
+ records.set(record.planCacheKey, Object.freeze({ ...record.sourceSpan }));
41836
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
+ });
41837
42064
  function normalizedTraceSourcePlanCacheKey(plan) {
41838
- return `shape-plan-v1:${stableTraceSourcePlanEncode(plan, false) ?? "null"}`;
42065
+ return `shape-plan-v1:${traceSourcePlanHasher(plan)}`;
41839
42066
  }
41840
42067
  function recordShapeSourceSpanInternal(shape, plan) {
41841
42068
  if (!hasActiveRuntimeSourceResolver()) return;
@@ -41855,13 +42082,20 @@ function recordShapeSourceSpanInternal(shape, plan) {
41855
42082
  }
41856
42083
  }
41857
42084
  function copyShapeSourceSpans(source, target) {
41858
- const records = cloneShapeSourceSpanRecords(_shapeSourceSpans.get(source));
41859
- 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));
41860
42087
  }
41861
42088
  function mergeShapeSourceSpans(sources, target) {
42089
+ let records = _shapeSourceSpans.get(target);
41862
42090
  for (const source of sources) {
41863
- for (const record of _shapeSourceSpans.get(source) ?? []) {
41864
- 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);
41865
42099
  }
41866
42100
  }
41867
42101
  }
@@ -41896,7 +42130,7 @@ function getShapeRuntimeBackendInternal(shape) {
41896
42130
  function getShapeCompilePlanInternal(shape) {
41897
42131
  const stored = _shapeCompilePlans.get(shape);
41898
42132
  if (!stored) throw new Error("Shape has no compile plan — every Shape must have an explicit plan set via setShapeCompilePlanInternal()");
41899
- return cloneShapeCompilePlan(stored);
42133
+ return stored;
41900
42134
  }
41901
42135
  function getShapePlacementRefsInternal(shape) {
41902
42136
  return clonePlacementReferences(_shapePlacementRefs.get(shape) ?? createPlacementReferences());
@@ -42588,6 +42822,30 @@ function checkLabelCollisions(operation2, plans) {
42588
42822
  seen.push(...labels);
42589
42823
  }
42590
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
+ }
42591
42849
  function withCopiedDimensions(source, out) {
42592
42850
  setShapeDimensionsInternal(out, cloneDimensions(getShapeDimensionsInternal(source), true));
42593
42851
  setShapeGeometryInfoInternal(out, getShapeGeometryInfoInternal(source));
@@ -42602,6 +42860,7 @@ function withCopiedDimensions(source, out) {
42602
42860
  const sourceLabels = cloneFaceLabelMap(_shapeFaceLabels.get(source));
42603
42861
  if (sourceLabels) _shapeFaceLabels.set(out, sourceLabels);
42604
42862
  copyShapeReferenceMetadata(source, out);
42863
+ copyShapeLineage(source, out);
42605
42864
  copyShapeSourceSpans(source, out);
42606
42865
  return setShapeCompilePlanInternal(out, getShapeCompilePlanInternal(source));
42607
42866
  }
@@ -42631,6 +42890,7 @@ function withTransformedDimensions(source, out, m2) {
42631
42890
  const sourceLabelsT = cloneFaceLabelMap(_shapeFaceLabels.get(source));
42632
42891
  if (sourceLabelsT) _shapeFaceLabels.set(out, sourceLabelsT);
42633
42892
  copyShapeReferenceMetadata(source, out);
42893
+ copyShapeLineage(source, out);
42634
42894
  copyShapeSourceSpans(source, out);
42635
42895
  return setShapeCompilePlanInternal(out, getShapeCompilePlanInternal(source));
42636
42896
  }
@@ -43058,6 +43318,7 @@ class Shape {
43058
43318
  this.colorHex = color;
43059
43319
  setShapeRuntimeBackendInternal(this, payload);
43060
43320
  setShapeGeometryInfoInternal(this, createGeometryInfo(geometryInfo));
43321
+ _shapeLineageTokens.set(this, {});
43061
43322
  }
43062
43323
  /** @internal Use .color() instead. */
43063
43324
  setColor(value) {
@@ -43176,6 +43437,12 @@ class Shape {
43176
43437
  * with `union()` / `difference()` to avoid collisions. Collision detection throws a
43177
43438
  * clear error with a fix suggestion.
43178
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
+ *
43179
43446
  * For compile-covered shapes (extrude, loft, etc.) the lookup resolves via the shape's
43180
43447
  * compile plan. As a fallback, planar-faced mesh shapes (e.g. results of boolean ops)
43181
43448
  * are resolved via coplanar triangle clustering.
@@ -43592,7 +43859,7 @@ class Shape {
43592
43859
  const tbb = s.boundingBox();
43593
43860
  return this.moveTo(tbb.min[0] + localX, tbb.min[1] + localY, tbb.min[2] + localZ);
43594
43861
  }
43595
- /** 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. */
43596
43863
  rotate(axis, angleDeg, options) {
43597
43864
  validateRotateAxis(axis, "Shape.rotate()");
43598
43865
  validateRotateAngle(angleDeg, "Shape.rotate()");
@@ -43779,7 +44046,7 @@ class Shape {
43779
44046
  * Warn if a boolean operation had no geometric effect.
43780
44047
  * Compares volumes before and after; if they match within tolerance, the operation was a no-op.
43781
44048
  */
43782
- static _checkBooleanNoOp(op, base, result) {
44049
+ static _checkBooleanNoOp(op, base, result, tools = []) {
43783
44050
  try {
43784
44051
  if (op === "intersection") {
43785
44052
  if (result.isEmpty()) {
@@ -43792,8 +44059,15 @@ class Shape {
43792
44059
  const volAfter = result.volume();
43793
44060
  const tol = Math.max(volBefore * 1e-4, 1e-3);
43794
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(" ");
43795
44067
  _runtimeWarn(
43796
- `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
43797
44071
  );
43798
44072
  }
43799
44073
  }
@@ -43853,7 +44127,7 @@ class Shape {
43853
44127
  ),
43854
44128
  nextPlan
43855
44129
  );
43856
- Shape._checkBooleanNoOp("difference", this, resultShape);
44130
+ Shape._checkBooleanNoOp("difference", this, resultShape, shapes.slice(1));
43857
44131
  return resultShape;
43858
44132
  }
43859
44133
  /** Keep only the overlap with other shapes. Method form of intersection(). */
@@ -44256,6 +44530,11 @@ class Shape {
44256
44530
  /**
44257
44531
  * Position this shape by matching connectors to a target.
44258
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
+ *
44259
44538
  * Overloads:
44260
44539
  * - Single pair: `matchTo(target, selfConn, targetConn, options?)`
44261
44540
  * - Dictionary (same target): `matchTo(target, { selfConn: targetConn, ... }, options?)`
@@ -45325,117 +45604,10 @@ function buildSweepLevelSetInput(profilePolygons, pathInput, options) {
45325
45604
  edgeLength: options.edgeLength
45326
45605
  };
45327
45606
  }
45328
- const EPS = 1e-9;
45329
- function resamplePolygon(poly, targetCount) {
45330
- if (poly.length < 2) return poly;
45331
- if (targetCount <= 0) return [];
45332
- const dists = [0];
45333
- for (let i = 0; i < poly.length; i++) {
45334
- const p1 = poly[i];
45335
- const p2 = poly[(i + 1) % poly.length];
45336
- const dx = p2[0] - p1[0];
45337
- const dy = p2[1] - p1[1];
45338
- const d2 = Math.sqrt(dx * dx + dy * dy);
45339
- dists.push(dists[dists.length - 1] + d2);
45340
- }
45341
- const totalDist = dists[dists.length - 1];
45342
- if (totalDist < 1e-12) {
45343
- return Array.from({ length: targetCount }, () => [poly[0][0], poly[0][1]]);
45344
- }
45345
- const out = [];
45346
- for (let i = 0; i < targetCount; i++) {
45347
- const targetDist = i / targetCount * totalDist;
45348
- let low = 0;
45349
- let high = dists.length - 1;
45350
- while (low < high) {
45351
- const mid = low + high >> 1;
45352
- if (dists[mid] <= targetDist) {
45353
- low = mid + 1;
45354
- } else {
45355
- high = mid;
45356
- }
45357
- }
45358
- const seg = low - 1;
45359
- const t = (targetDist - dists[seg]) / (dists[seg + 1] - dists[seg]);
45360
- const p1 = poly[seg % poly.length];
45361
- const p2 = poly[(seg + 1) % poly.length];
45362
- out.push([p1[0] + (p2[0] - p1[0]) * t, p1[1] + (p2[1] - p1[1]) * t]);
45363
- }
45364
- return out;
45365
- }
45366
- function resamplePolygonByAngle(poly, targetCount, center = polygonCentroid(poly)) {
45367
- if (poly.length < 3 || targetCount <= 0) return null;
45368
- if (!isConvexPolygon(poly)) return null;
45369
- const out = [];
45370
- for (let index2 = 0; index2 < targetCount; index2 += 1) {
45371
- const angle = index2 / targetCount * Math.PI * 2;
45372
- const point = rayPolygonIntersection(center, [Math.cos(angle), Math.sin(angle)], poly);
45373
- if (!point) return null;
45374
- out.push(point);
45375
- }
45376
- return out;
45377
- }
45378
- function rayPolygonIntersection(origin, direction, poly) {
45379
- let bestT = Infinity;
45380
- let best = null;
45381
- for (let index2 = 0; index2 < poly.length; index2 += 1) {
45382
- const a2 = poly[index2];
45383
- const b = poly[(index2 + 1) % poly.length];
45384
- const edge = [b[0] - a2[0], b[1] - a2[1]];
45385
- const denom = cross$1(direction, edge);
45386
- if (Math.abs(denom) < EPS) continue;
45387
- const delta = [a2[0] - origin[0], a2[1] - origin[1]];
45388
- const rayT = cross$1(delta, edge) / denom;
45389
- const edgeT = cross$1(delta, direction) / denom;
45390
- if (rayT >= -EPS && edgeT >= -EPS && edgeT <= 1 + EPS && rayT < bestT) {
45391
- bestT = rayT;
45392
- best = [origin[0] + direction[0] * rayT, origin[1] + direction[1] * rayT];
45393
- }
45394
- }
45395
- return best;
45396
- }
45397
- function polygonCentroid(poly) {
45398
- let area2 = 0;
45399
- let cx = 0;
45400
- let cy = 0;
45401
- for (let index2 = 0; index2 < poly.length; index2 += 1) {
45402
- const a2 = poly[index2];
45403
- const b = poly[(index2 + 1) % poly.length];
45404
- const crossValue = cross$1(a2, b);
45405
- area2 += crossValue;
45406
- cx += (a2[0] + b[0]) * crossValue;
45407
- cy += (a2[1] + b[1]) * crossValue;
45408
- }
45409
- if (Math.abs(area2) < EPS) return averagePoint(poly);
45410
- return [cx / (3 * area2), cy / (3 * area2)];
45411
- }
45412
- function averagePoint(poly) {
45413
- let x2 = 0;
45414
- let y2 = 0;
45415
- for (const point of poly) {
45416
- x2 += point[0];
45417
- y2 += point[1];
45418
- }
45419
- return [x2 / poly.length, y2 / poly.length];
45420
- }
45421
- function isConvexPolygon(poly) {
45422
- let sign = 0;
45423
- for (let index2 = 0; index2 < poly.length; index2 += 1) {
45424
- const a2 = poly[index2];
45425
- const b = poly[(index2 + 1) % poly.length];
45426
- const c2 = poly[(index2 + 2) % poly.length];
45427
- const turn = cross$1([b[0] - a2[0], b[1] - a2[1]], [c2[0] - b[0], c2[1] - b[1]]);
45428
- if (Math.abs(turn) < EPS) continue;
45429
- const currentSign = Math.sign(turn);
45430
- if (sign !== 0 && currentSign !== sign) return false;
45431
- sign = currentSign;
45432
- }
45433
- return sign !== 0;
45434
- }
45435
- function cross$1(a2, b) {
45436
- return a2[0] * b[1] - a2[1] * b[0];
45437
- }
45438
- 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 = {}) {
45439
45611
  if (profiles.length < 2) return null;
45440
45612
  const classified = profiles.map((loops) => classifyLoops(loops));
45441
45613
  const outerCount = classified[0].outers.length;
@@ -45450,7 +45622,7 @@ function loftStitched(profiles, heights, wasm) {
45450
45622
  const holeGroups = holeCount > 0 ? matchLoopsAcrossProfiles(classified.map((c2) => c2.holes)) : [];
45451
45623
  const outerSolids = [];
45452
45624
  for (const group of outerGroups) {
45453
- const solid = stitchSingleLoopLoft(group, heights, wasm);
45625
+ const solid = stitchSingleLoopLoft(group, heights, wasm, options);
45454
45626
  if (!solid) {
45455
45627
  for (const s of outerSolids) s.delete();
45456
45628
  return null;
@@ -45467,7 +45639,7 @@ function loftStitched(profiles, heights, wasm) {
45467
45639
  if (holeGroups.length > 0) {
45468
45640
  const holeSolids = [];
45469
45641
  for (const group of holeGroups) {
45470
- const solid = stitchSingleLoopLoft(group, heights, wasm);
45642
+ const solid = stitchSingleLoopLoft(group, heights, wasm, options);
45471
45643
  if (!solid) {
45472
45644
  result.delete();
45473
45645
  for (const s of holeSolids) s.delete();
@@ -45553,68 +45725,564 @@ function signedArea$1(loop) {
45553
45725
  }
45554
45726
  return area * 0.5;
45555
45727
  }
45556
- 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) {
45557
46132
  const normalizedLoops = loops.map((loop) => {
45558
46133
  const area = signedArea$1(loop);
45559
46134
  return area < 0 ? [...loop].reverse() : loop;
45560
46135
  });
45561
- let maxPoints = 0;
45562
- for (const loop of normalizedLoops) {
45563
- maxPoints = Math.max(maxPoints, loop.length);
45564
- }
45565
- const N = Math.max(maxPoints, 24);
45566
- const angularSamples = normalizedLoops.map((loop) => resamplePolygonByAngle(loop, N));
45567
- const useAngularSamples = angularSamples.every((samples) => samples != null);
45568
- const resampled = normalizedLoops.map((loop, i) => {
45569
- const pts2d = useAngularSamples ? angularSamples[i] : resamplePolygon(loop, N);
45570
- const z2 = heights[i];
45571
- return pts2d.map(([x2, y2]) => [x2, y2, z2]);
45572
- });
45573
- const vertices = [];
45574
- const triangles = [];
45575
- for (const layer of resampled) {
45576
- for (const [x2, y2, z2] of layer) {
45577
- 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
+ }
45578
46170
  }
46171
+ fwdIdx.push(fwd);
46172
+ bwdIdx.push(bwd);
45579
46173
  }
45580
- for (let i = 0; i < resampled.length - 1; i++) {
45581
- const baseIdx = i * N;
45582
- const nextIdx = (i + 1) * N;
46174
+ const triangles = [];
46175
+ for (let r = 0; r < R - 1; r++) {
45583
46176
  for (let j = 0; j < N; j++) {
45584
46177
  const j1 = (j + 1) % N;
45585
- const v0 = baseIdx + j;
45586
- const v1 = nextIdx + j;
45587
- const v2 = nextIdx + j1;
45588
- 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];
45589
46182
  triangles.push(v0, v3, v2);
45590
46183
  triangles.push(v0, v2, v1);
45591
46184
  }
45592
46185
  }
45593
- const bottomResampled2D = resampled[0].map(([x2, y2]) => [x2, y2]);
45594
- const bottomTrisResampled = wasm.triangulate([bottomResampled2D]);
45595
- 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) {
45596
46193
  const [v0, v1, v2] = Array.isArray(tri) ? tri : [tri[0], tri[1], tri[2]];
45597
- triangles.push(v0, v2, v1);
46194
+ triangles.push(bottomBase + v0, bottomBase + v2, bottomBase + v1);
45598
46195
  }
45599
- const topResampled2D = resampled[resampled.length - 1].map(([x2, y2]) => [x2, y2]);
45600
- const topTrisResampled = wasm.triangulate([topResampled2D]);
45601
- const topStartIdx = (resampled.length - 1) * N;
45602
- 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) {
45603
46201
  const [v0, v1, v2] = Array.isArray(tri) ? tri : [tri[0], tri[1], tri[2]];
45604
- triangles.push(topStartIdx + v0, topStartIdx + v1, topStartIdx + v2);
46202
+ triangles.push(topBase + v0, topBase + v1, topBase + v2);
45605
46203
  }
45606
46204
  const mesh = new wasm.Mesh({
45607
- numProp: 3,
45608
- vertProperties: new Float32Array(vertices),
46205
+ numProp: 6,
46206
+ vertProperties: new Float32Array(vertProps),
45609
46207
  triVerts: new Uint32Array(triangles)
45610
46208
  });
45611
46209
  try {
46210
+ mesh.merge();
45612
46211
  const manifold = new wasm.Manifold(mesh);
45613
46212
  return manifold;
45614
46213
  } catch (_e2) {
45615
46214
  return null;
45616
46215
  }
45617
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
+ }
45618
46286
  function sweepStitched(profilePolygons, pathPoints, up, wasm) {
45619
46287
  if (pathPoints.length < 2) return null;
45620
46288
  if (profilePolygons.length === 0) return null;
@@ -46312,7 +46980,7 @@ function lowerOffsetLoftCompilePlan(plan, thickness, wasm) {
46312
46980
  throw new Error("offsetSolid() collapsed the compatible-loft height span.");
46313
46981
  }
46314
46982
  const offsetPolygons = plan.profiles.map((profile) => offsetProfilePolygonsForManifold(profile, thickness, wasm));
46315
- const stitched = loftStitched(offsetPolygons, heights, wasm);
46983
+ const stitched = loftStitched(offsetPolygons, heights, wasm, { edgeLength: plan.edgeLength });
46316
46984
  if (!stitched) {
46317
46985
  throw new Error(`Offset solid requires the OCCT backend. ${OCCT_BACKEND_REQUIRED_HINT}`);
46318
46986
  }
@@ -46447,12 +47115,12 @@ function lowerShapeLoftCompilePlan(plan, wasm) {
46447
47115
  disposeWasmObject(crossSection);
46448
47116
  }
46449
47117
  });
46450
- if (inputPolygons.length >= 2) {
46451
- 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 });
46452
47120
  if (stitched) return stitched;
46453
47121
  }
46454
47122
  const input = buildLoftLevelSetInput(inputPolygons, plan.heights, { edgeLength: plan.edgeLength, boundsPadding: plan.boundsPadding });
46455
- return lowerSdfToManifold(levelSetFieldToStandardSdf3(input.sdf), input.bounds, input.edgeLength, wasm);
47123
+ return lowerSdfToManifold(levelSetFieldToStandardSdf3(input.sdf), input.bounds, input.edgeLength, wasm, plan.meshing);
46456
47124
  }
46457
47125
  function lowerShapeSweepCompilePlan(plan, wasm) {
46458
47126
  const crossSection = lowerProfileCompilePlanToCrossSection(plan.profile, wasm);
@@ -46717,7 +47385,7 @@ function lowerFromSlicesToManifold(plan, wasm) {
46717
47385
  }
46718
47386
  });
46719
47387
  const heights = sorted.map((s) => s.offset);
46720
- const stitched = polygons.length >= 2 ? loftStitched(polygons, heights, wasm) : null;
47388
+ const stitched = polygons.length >= 2 ? loftStitched(polygons, heights, wasm, { edgeLength: plan.edgeLength }) : null;
46721
47389
  if (stitched) {
46722
47390
  solid = stitched;
46723
47391
  } else {
@@ -47158,17 +47826,21 @@ function lowerSdfToManifold(evalFn, bounds, edgeLength2, wasm, meshing, evaluato
47158
47826
  if (diagnostics) diagnostics.projectionMs = nowMs() - projectionStart;
47159
47827
  const simplificationStart = nowMs();
47160
47828
  if (((meshing == null ? void 0 : meshing.simplify) ?? "safe") !== "off" && snMesh.numTris > 100) {
47161
- 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);
47162
47830
  }
47163
47831
  if (diagnostics) {
47164
47832
  diagnostics.simplificationMs = nowMs() - simplificationStart;
47165
47833
  diagnostics.trianglesAfterSimplification = triVerts.length / 3;
47166
47834
  }
47167
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";
47168
47837
  throw new Error(
47169
- `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.`
47170
47839
  );
47171
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
+ }
47172
47844
  const wasmMesh = new wasm.Mesh({
47173
47845
  numProp: 6,
47174
47846
  vertProperties: vertProps6,
@@ -47186,28 +47858,102 @@ function lowerSdfToManifold(evalFn, bounds, edgeLength2, wasm, meshing, evaluato
47186
47858
  disposeWasmObject(wasmMesh);
47187
47859
  }
47188
47860
  }
47189
- function simplifySdfMesh(triVerts, vertProperties, edgeLength2, wasm, maxTriangles) {
47861
+ function simplifySdfMesh(triVerts, vertProperties, finalVertProperties, edgeLength2, maxTriangles) {
47190
47862
  const maxError = edgeLength2 * 0.15;
47191
47863
  const inputTriangles = triVerts.length / 3;
47192
- const requestedRatio = maxTriangles && maxTriangles < inputTriangles ? maxTriangles / inputTriangles : 0.5;
47193
- 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;
47194
47867
  for (const ratio of ratios) {
47195
- let simplified = simplifyMesh(triVerts, vertProperties, ratio, maxError);
47196
- simplified = filterDegenerateTriangles(simplified);
47197
- let mesh = null;
47198
- let manifold = null;
47868
+ let simplified;
47199
47869
  try {
47200
- mesh = new wasm.Mesh({ numProp: 3, vertProperties, triVerts: simplified });
47201
- manifold = new wasm.Manifold(mesh);
47202
- return simplified;
47870
+ simplified = simplifyMesh(triVerts, vertProperties, ratio, maxError);
47203
47871
  } catch {
47204
- } finally {
47205
- disposeWasmObject(manifold);
47206
- 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
+ }
47207
47882
  }
47208
47883
  }
47884
+ if (bestValid) {
47885
+ return bestValid;
47886
+ }
47209
47887
  return triVerts;
47210
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
+ }
47211
47957
  function filterDegenerateTriangles(triVerts) {
47212
47958
  let writeIdx = 0;
47213
47959
  for (let i = 0; i < triVerts.length; i += 3) {
@@ -49647,15 +50393,23 @@ function computeGeometryArrays(mesh, options = {}) {
49647
50393
  normals[o + 7] = vertNormals[i2 * 3 + 1];
49648
50394
  normals[o + 8] = vertNormals[i2 * 3 + 2];
49649
50395
  } else if (numProp >= 6) {
49650
- normals[o] = vertProperties[i0 * numProp + 3];
49651
- normals[o + 1] = vertProperties[i0 * numProp + 4];
49652
- normals[o + 2] = vertProperties[i0 * numProp + 5];
49653
- normals[o + 3] = vertProperties[i1 * numProp + 3];
49654
- normals[o + 4] = vertProperties[i1 * numProp + 4];
49655
- normals[o + 5] = vertProperties[i1 * numProp + 5];
49656
- normals[o + 6] = vertProperties[i2 * numProp + 3];
49657
- normals[o + 7] = vertProperties[i2 * numProp + 4];
49658
- 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
+ }
49659
50413
  } else {
49660
50414
  normals[o] = fnx;
49661
50415
  normals[o + 1] = fny;