forgecad 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/{AdminPage-DAu1C1ST.js → AdminPage-D4bocK4E.js} +1 -1
  3. package/dist/assets/{DocsPage-Gc_BCdqC.js → DocsPage-D3A_g8V3.js} +85 -45
  4. package/dist/assets/{EditorApp-DG1-oUSV.css → EditorApp-BWYUSpUN.css} +133 -51
  5. package/dist/assets/EditorApp-Cihhqcsq.js +11692 -0
  6. package/dist/assets/{EmbedViewer-CEO8XbV8.js → EmbedViewer-kWjKaC_t.js} +1 -1
  7. package/dist/assets/LandingPageProofDriven-Bg2IUc3l.css +856 -0
  8. package/dist/assets/LandingPageProofDriven-DXkKlyhI.js +601 -0
  9. package/dist/assets/{PricingPage-BSrxu6d7.js → PricingPage-BsU5vzEx.js} +1 -1
  10. package/dist/assets/{SettingsPage-FUCSIRq6.js → SettingsPage-PqvpAKIs.js} +1 -1
  11. package/dist/assets/{evalWorker-KoR0SNKq.js → evalWorker-C-hzNUMy.js} +2218 -286
  12. package/dist/assets/{index-wTEK39at.js → index-Pz321YAt.js} +7416 -1481
  13. package/dist/assets/{index-CyVd1D4D.css → index-ay13WNfa.css} +501 -2
  14. package/dist/assets/{manifold-B1sGWdYk.js → manifold-BcbjWLIo.js} +3 -3
  15. package/dist/assets/{manifold-D7o0N50J.js → manifold-DBckbFgx.js} +1 -1
  16. package/dist/assets/{manifold-G5sBaXzi.js → manifold-O2AAGXyj.js} +1 -1
  17. package/dist/assets/{reportWorker-DYcRHhv9.js → reportWorker-Dxr-5A7w.js} +2003 -259
  18. package/dist/docs/index.html +2 -2
  19. package/dist/docs-raw/CLI.md +488 -0
  20. package/dist/docs-raw/generated/assembly.md +19 -11
  21. package/dist/docs-raw/generated/concepts.md +1023 -360
  22. package/dist/docs-raw/generated/core.md +1165 -264
  23. package/dist/docs-raw/generated/curves.md +168 -1
  24. package/dist/docs-raw/generated/lib.md +10 -5
  25. package/dist/docs-raw/generated/output.md +1 -1
  26. package/dist/docs-raw/generated/sdf.md +208 -0
  27. package/dist/docs-raw/generated/sketch.md +1281 -329
  28. package/dist/docs-raw/generated/viewport.md +29 -2
  29. package/dist/index.html +2 -2
  30. package/dist/landing/proof-ams-adapter.png +0 -0
  31. package/dist/landing/proof-bolt-and-nut.png +0 -0
  32. package/dist/landing/proof-fillet-enclosure.png +0 -0
  33. package/dist/landing/proof-glasses.png +0 -0
  34. package/dist/landing/proof-gyroid.png +0 -0
  35. package/dist/sitemap.xml +6 -6
  36. package/dist-cli/forgecad.js +3148 -555
  37. package/dist-cli/forgecad.js.map +1 -1
  38. package/dist-cli/{solver-FV7TJZGI.js → solver-46FFSK2U.js} +1 -3
  39. package/dist-cli/{solver-FV7TJZGI.js.map → solver-46FFSK2U.js.map} +1 -1
  40. package/dist-skill/CONTEXT.md +3700 -1153
  41. package/dist-skill/SKILL-dev.md +15 -17
  42. package/dist-skill/SKILL.md +14 -9
  43. package/dist-skill/docs/API/core/concepts.md +28 -1
  44. package/dist-skill/docs/CLI.md +488 -0
  45. package/dist-skill/docs/generated/assembly.md +19 -11
  46. package/dist-skill/docs/generated/core.md +1165 -264
  47. package/dist-skill/docs/generated/curves.md +168 -1
  48. package/dist-skill/docs/generated/lib.md +10 -5
  49. package/dist-skill/docs/generated/output.md +1 -1
  50. package/dist-skill/docs/generated/sdf.md +208 -0
  51. package/dist-skill/docs/generated/sketch.md +1281 -329
  52. package/dist-skill/docs/generated/viewport.md +29 -2
  53. package/dist-skill/docs/guides/joint-design.md +78 -0
  54. package/dist-skill/docs-dev/API/core/concepts.md +28 -1
  55. package/dist-skill/docs-dev/CLI.md +488 -0
  56. package/dist-skill/docs-dev/coding.md +1 -1
  57. package/dist-skill/docs-dev/component-model.md +164 -0
  58. package/dist-skill/docs-dev/generated/assembly.md +19 -11
  59. package/dist-skill/docs-dev/generated/core.md +1165 -264
  60. package/dist-skill/docs-dev/generated/curves.md +168 -1
  61. package/dist-skill/docs-dev/generated/lib.md +10 -5
  62. package/dist-skill/docs-dev/generated/output.md +1 -1
  63. package/dist-skill/docs-dev/generated/sdf.md +208 -0
  64. package/dist-skill/docs-dev/generated/sketch.md +1281 -329
  65. package/dist-skill/docs-dev/generated/viewport.md +29 -2
  66. package/dist-skill/docs-dev/guides/joint-design.md +78 -0
  67. package/examples/api/attachTo-basics.forge.js +3 -3
  68. package/examples/api/bill-of-materials.forge.js +9 -9
  69. package/examples/api/bolt-pattern.forge.js +5 -5
  70. package/examples/api/boolean-operations.forge.js +2 -2
  71. package/examples/api/bounding-box-visualizer.forge.js +1 -1
  72. package/examples/api/clone-duplicate.forge.js +1 -1
  73. package/examples/api/connector-assembly.forge.js +4 -2
  74. package/examples/api/connector-basics.forge.js +5 -5
  75. package/examples/api/constrained-sketch-mechanical.forge.js +4 -4
  76. package/examples/api/elbow-test.forge.js +3 -3
  77. package/examples/api/extrude-options.forge.js +4 -4
  78. package/examples/api/fillet-showcase.forge.js +1 -1
  79. package/examples/api/gears-tier1.forge.js +5 -5
  80. package/examples/api/group-test.forge.js +2 -2
  81. package/examples/api/mesh-import-slats.forge.js +3 -3
  82. package/examples/api/patterns.forge.js +3 -3
  83. package/examples/api/pointAlong-orientation.forge.js +2 -2
  84. package/examples/api/profile-2020-b-slot6.forge.js +4 -4
  85. package/examples/api/sketch-rounding-strategies.forge.js +1 -1
  86. package/examples/api/smooth-curve-connections.forge.js +1 -1
  87. package/examples/api/transition-curves.forge.js +3 -3
  88. package/examples/constraints/01-fully-constrained-rect.forge.js +2 -2
  89. package/examples/constraints/02-underconstrained-triangle.forge.js +1 -1
  90. package/examples/constraints/03-redundant-constraints.forge.js +2 -2
  91. package/examples/constraints/05-parallel-with-linedistance.forge.js +2 -2
  92. package/examples/constraints/06-complex-spectrogram.forge.js +1 -1
  93. package/examples/constraints/07-perpendicular-chain.forge.js +4 -4
  94. package/examples/constraints/08-symmetric-bracket.forge.js +4 -4
  95. package/examples/constraints/09-stress-spiral.forge.js +1 -1
  96. package/examples/constraints/10-stress-honeycomb.forge.js +1 -1
  97. package/examples/constraints/11-surface-grid.forge.js +2 -2
  98. package/examples/constraints/12-surface-nested.forge.js +4 -4
  99. package/examples/constraints/13-surface-complex.forge.js +1 -1
  100. package/examples/exact-arc-housing.forge.js +12 -0
  101. package/examples/furniture/adjustable-table.forge.js +13 -13
  102. package/examples/furniture/bathroom.forge.js +15 -15
  103. package/examples/furniture/chair.forge.js +12 -12
  104. package/examples/furniture/picture-frame.forge.js +6 -6
  105. package/examples/furniture/shoe-rack-doors.forge.js +8 -8
  106. package/examples/furniture/shoe-rack.forge.js +7 -7
  107. package/examples/furniture/table-lamp.forge.js +8 -8
  108. package/examples/gcode/lissajous-vase.forge.js +4 -4
  109. package/examples/gcode/math-surface.forge.js +3 -3
  110. package/examples/gcode/parametric-vase.forge.js +4 -4
  111. package/examples/gcode/spiral-tower.forge.js +4 -4
  112. package/examples/generative/crystal-growth.forge.js +7 -7
  113. package/examples/generative/frost-spires.forge.js +6 -6
  114. package/examples/generative/golden-spiral-tower.forge.js +8 -8
  115. package/examples/generative/molten-forge.forge.js +6 -6
  116. package/examples/generative/neon-coral.forge.js +7 -7
  117. package/examples/mechanical/3d-printer.forge.js +9 -9
  118. package/examples/mechanical/5-finger-robot-hand.forge.js +4 -4
  119. package/examples/mechanical/airplane-propeller.forge.js +7 -7
  120. package/examples/mechanical/bolt-and-nut.forge.js +10 -10
  121. package/examples/mechanical/door-with-hinges.forge.js +7 -7
  122. package/examples/mechanical/fillet-enclosure.forge.js +14 -10
  123. package/examples/mechanical/headphone-hanger-v2.forge.js +9 -9
  124. package/examples/mechanical/robot_hand.forge.js +10 -10
  125. package/examples/mechanical/robot_hand_2.forge.js +17 -17
  126. package/examples/nurbs-surface.forge.js +8 -0
  127. package/examples/nurbs-tube.forge.js +7 -0
  128. package/examples/products/bottle.forge.js +7 -7
  129. package/examples/products/chess-set.forge.js +6 -6
  130. package/examples/products/classical-piano.forge.js +9 -9
  131. package/examples/products/clock.forge.js +21 -21
  132. package/examples/products/cup.forge.js +5 -5
  133. package/examples/products/iphone.forge.js +12 -12
  134. package/examples/products/laptop.forge.js +9 -9
  135. package/examples/products/laser-cut-box.forge.js +6 -6
  136. package/examples/products/laser-cut-tray.forge.js +6 -6
  137. package/examples/products/liquid-soap-dispenser.forge.js +5 -5
  138. package/examples/products/origami-fish.forge.js +6 -6
  139. package/examples/products/spiderman-cake.forge.js +2 -2
  140. package/examples/shelf/container.forge.js +5 -5
  141. package/examples/shelf/shelf-unit.forge.js +6 -6
  142. package/examples/toolbox/bolted-joint.forge.js +5 -5
  143. package/package.json +3 -1
  144. package/dist/assets/EditorApp-D9bJvtf7.js +0 -11338
  145. package/dist/assets/LandingPage-CdCuEOdC.js +0 -451
  146. package/dist-cli/chunk-PZ5AY32C.js +0 -10
  147. package/dist-cli/chunk-PZ5AY32C.js.map +0 -1
  148. package/dist-skill/docs/CLI/export.md +0 -91
  149. package/dist-skill/docs/CLI/projects.md +0 -107
  150. package/dist-skill/docs/CLI/studio_publishing.md +0 -52
  151. package/dist-skill/docs/CLI/validation.md +0 -66
  152. package/dist-skill/docs-dev/API/core/sdf-advanced.md +0 -92
  153. package/dist-skill/docs-dev/API/core/sdf-primitives.md +0 -58
  154. package/dist-skill/docs-dev/API/core/sdf-workflow.md +0 -42
  155. package/dist-skill/docs-dev/CLI/export.md +0 -91
  156. package/dist-skill/docs-dev/CLI/projects.md +0 -107
  157. package/dist-skill/docs-dev/CLI/studio_publishing.md +0 -52
  158. package/dist-skill/docs-dev/CLI/validation.md +0 -66
@@ -1688,6 +1688,17 @@ function cloneSweepPathCompilePlan(path2) {
1688
1688
  c1: canonicalVec3(path2.c1),
1689
1689
  chordLength: canonicalNumber(path2.chordLength)
1690
1690
  };
1691
+ case "nurbs":
1692
+ return {
1693
+ kind: "nurbs",
1694
+ controlPoints: path2.controlPoints.map(
1695
+ ([x, y, z]) => [canonicalNumber(x), canonicalNumber(y), canonicalNumber(z)]
1696
+ ),
1697
+ weights: path2.weights.map(canonicalNumber),
1698
+ knots: path2.knots.map(canonicalNumber),
1699
+ degree: path2.degree,
1700
+ closed: path2.closed
1701
+ };
1691
1702
  default:
1692
1703
  assertExhaustive(path2);
1693
1704
  }
@@ -1754,10 +1765,33 @@ function cloneProfileCompilePlan(plan) {
1754
1765
  replayReason: plan.replayReason,
1755
1766
  transforms: plan.transforms.map(cloneProfileTransform)
1756
1767
  };
1768
+ case "pathProfile":
1769
+ return {
1770
+ kind: "pathProfile",
1771
+ points: plan.points.map(([x, y]) => [x, y]),
1772
+ edges: plan.edges.map(cloneProfileEdge),
1773
+ transforms: plan.transforms.map(cloneProfileTransform)
1774
+ };
1757
1775
  default:
1758
1776
  assertExhaustive(plan);
1759
1777
  }
1760
1778
  }
1779
+ function cloneProfileEdge(edge) {
1780
+ switch (edge.kind) {
1781
+ case "line":
1782
+ return { kind: "line", x1: edge.x1, y1: edge.y1, x2: edge.x2, y2: edge.y2 };
1783
+ case "arc":
1784
+ return { kind: "arc", x1: edge.x1, y1: edge.y1, x2: edge.x2, y2: edge.y2, cx: edge.cx, cy: edge.cy, clockwise: edge.clockwise };
1785
+ case "bspline":
1786
+ return {
1787
+ kind: "bspline",
1788
+ controlPoints: edge.controlPoints.map(([x, y]) => [x, y]),
1789
+ weights: [...edge.weights],
1790
+ knots: [...edge.knots],
1791
+ degree: edge.degree
1792
+ };
1793
+ }
1794
+ }
1761
1795
  function cloneShapeCompilePlan(plan) {
1762
1796
  if (!plan) return null;
1763
1797
  let result;
@@ -1998,6 +2032,24 @@ function cloneShapeCompilePlan(plan) {
1998
2032
  boundsPadding: canonicalNumber(plan.boundsPadding)
1999
2033
  };
2000
2034
  break;
2035
+ case "importedStep":
2036
+ result = { kind: "importedStep", filePath: plan.filePath, fileData: plan.fileData };
2037
+ break;
2038
+ case "nurbsSurface":
2039
+ result = {
2040
+ kind: "nurbsSurface",
2041
+ controlGrid: plan.controlGrid.map(
2042
+ (row) => row.map(([x, y, z]) => [canonicalNumber(x), canonicalNumber(y), canonicalNumber(z)])
2043
+ ),
2044
+ weightsGrid: plan.weightsGrid.map((row) => row.map(canonicalNumber)),
2045
+ knotsU: plan.knotsU.map(canonicalNumber),
2046
+ knotsV: plan.knotsV.map(canonicalNumber),
2047
+ degreeU: plan.degreeU,
2048
+ degreeV: plan.degreeV,
2049
+ thickness: canonicalNumber(plan.thickness),
2050
+ resolution: plan.resolution
2051
+ };
2052
+ break;
2001
2053
  default:
2002
2054
  assertExhaustive(plan);
2003
2055
  }
@@ -2188,6 +2240,8 @@ function findShapePrimaryQueryOwner(plan) {
2188
2240
  case "importedMesh":
2189
2241
  case "sdf":
2190
2242
  case "fromSlices":
2243
+ case "nurbsSurface":
2244
+ case "importedStep":
2191
2245
  return null;
2192
2246
  default:
2193
2247
  assertExhaustive(plan);
@@ -2234,6 +2288,8 @@ function collectShapeQueryOwners(plan) {
2234
2288
  case "importedMesh":
2235
2289
  case "sdf":
2236
2290
  case "fromSlices":
2291
+ case "nurbsSurface":
2292
+ case "importedStep":
2237
2293
  return;
2238
2294
  default:
2239
2295
  assertExhaustive(current);
@@ -2288,6 +2344,8 @@ function findShapeWorkplanePlacement(plan) {
2288
2344
  case "importedMesh":
2289
2345
  case "sdf":
2290
2346
  case "fromSlices":
2347
+ case "nurbsSurface":
2348
+ case "importedStep":
2291
2349
  return null;
2292
2350
  default:
2293
2351
  assertExhaustive(plan);
@@ -2410,6 +2468,199 @@ function buildFromSlicesShapeCompilePlan(groups, options) {
2410
2468
  boundsPadding: canonicalNumber(options.boundsPadding)
2411
2469
  };
2412
2470
  }
2471
+ function generateClampedKnots(n, degree) {
2472
+ const m = n + degree + 1;
2473
+ const knots = new Array(m);
2474
+ for (let i = 0; i < m; i++) {
2475
+ if (i <= degree) knots[i] = 0;
2476
+ else if (i >= m - degree - 1) knots[i] = 1;
2477
+ else knots[i] = (i - degree) / (n - degree);
2478
+ }
2479
+ return knots;
2480
+ }
2481
+ function flatKnotsToKnotsMults(flatKnots) {
2482
+ const knots = [];
2483
+ const mults = [];
2484
+ let prev = -Infinity;
2485
+ for (const k of flatKnots) {
2486
+ if (Math.abs(k - prev) < 1e-12) {
2487
+ mults[mults.length - 1]++;
2488
+ } else {
2489
+ knots.push(k);
2490
+ mults.push(1);
2491
+ }
2492
+ prev = k;
2493
+ }
2494
+ return { knots, mults };
2495
+ }
2496
+ function findSpan(n, degree, u, knots) {
2497
+ if (u >= knots[n]) return n - 1;
2498
+ if (u <= knots[degree]) return degree;
2499
+ let lo = degree;
2500
+ let hi = n;
2501
+ let mid = lo + hi >>> 1;
2502
+ while (u < knots[mid] || u >= knots[mid + 1]) {
2503
+ if (u < knots[mid]) hi = mid;
2504
+ else lo = mid;
2505
+ mid = lo + hi >>> 1;
2506
+ }
2507
+ return mid;
2508
+ }
2509
+ function basisFuns(span, u, degree, knots) {
2510
+ const N = new Array(degree + 1);
2511
+ const left = new Array(degree + 1);
2512
+ const right = new Array(degree + 1);
2513
+ N[0] = 1;
2514
+ for (let j = 1; j <= degree; j++) {
2515
+ left[j] = u - knots[span + 1 - j];
2516
+ right[j] = knots[span + j] - u;
2517
+ let saved = 0;
2518
+ for (let r = 0; r < j; r++) {
2519
+ const denom = right[r + 1] + left[j - r];
2520
+ const temp = denom === 0 ? 0 : N[r] / denom;
2521
+ N[r] = saved + right[r + 1] * temp;
2522
+ saved = left[j - r] * temp;
2523
+ }
2524
+ N[j] = saved;
2525
+ }
2526
+ return N;
2527
+ }
2528
+ function basisFunsDeriv(span, u, degree, knots, nDeriv) {
2529
+ const ndu = Array.from({ length: degree + 1 }, () => new Array(degree + 1).fill(0));
2530
+ const a = Array.from({ length: 2 }, () => new Array(degree + 1).fill(0));
2531
+ const left = new Array(degree + 1);
2532
+ const right = new Array(degree + 1);
2533
+ ndu[0][0] = 1;
2534
+ for (let j = 1; j <= degree; j++) {
2535
+ left[j] = u - knots[span + 1 - j];
2536
+ right[j] = knots[span + j] - u;
2537
+ let saved = 0;
2538
+ for (let r2 = 0; r2 < j; r2++) {
2539
+ ndu[j][r2] = right[r2 + 1] + left[j - r2];
2540
+ const temp = ndu[j][r2] === 0 ? 0 : ndu[r2][j - 1] / ndu[j][r2];
2541
+ ndu[r2][j] = saved + right[r2 + 1] * temp;
2542
+ saved = left[j - r2] * temp;
2543
+ }
2544
+ ndu[j][j] = saved;
2545
+ }
2546
+ const ders = Array.from({ length: nDeriv + 1 }, () => new Array(degree + 1).fill(0));
2547
+ for (let j = 0; j <= degree; j++) {
2548
+ ders[0][j] = ndu[j][degree];
2549
+ }
2550
+ for (let r2 = 0; r2 <= degree; r2++) {
2551
+ let s1 = 0;
2552
+ let s2 = 1;
2553
+ a[0][0] = 1;
2554
+ for (let k = 1; k <= nDeriv; k++) {
2555
+ let d = 0;
2556
+ const rk = r2 - k;
2557
+ const pk = degree - k;
2558
+ if (r2 >= k) {
2559
+ a[s2][0] = ndu[pk + 1][rk] === 0 ? 0 : a[s1][0] / ndu[pk + 1][rk];
2560
+ d = a[s2][0] * ndu[rk][pk];
2561
+ }
2562
+ const j1 = rk >= -1 ? 1 : -rk;
2563
+ const j2 = r2 - 1 <= pk ? k - 1 : degree - r2;
2564
+ for (let j = j1; j <= j2; j++) {
2565
+ a[s2][j] = ndu[pk + 1][rk + j] === 0 ? 0 : (a[s1][j] - a[s1][j - 1]) / ndu[pk + 1][rk + j];
2566
+ d += a[s2][j] * ndu[rk + j][pk];
2567
+ }
2568
+ if (r2 <= pk) {
2569
+ a[s2][k] = ndu[pk + 1][r2] === 0 ? 0 : -a[s1][k - 1] / ndu[pk + 1][r2];
2570
+ d += a[s2][k] * ndu[r2][pk];
2571
+ }
2572
+ ders[k][r2] = d;
2573
+ const tmp = s1;
2574
+ s1 = s2;
2575
+ s2 = tmp;
2576
+ }
2577
+ }
2578
+ let r = degree;
2579
+ for (let k = 1; k <= nDeriv; k++) {
2580
+ for (let j = 0; j <= degree; j++) {
2581
+ ders[k][j] *= r;
2582
+ }
2583
+ r *= degree - k;
2584
+ }
2585
+ return ders;
2586
+ }
2587
+ function deBoor3D(controlPoints, weights, knots, degree, u) {
2588
+ const n = controlPoints.length;
2589
+ const span = findSpan(n, degree, u, knots);
2590
+ const N = basisFuns(span, u, degree, knots);
2591
+ let wx = 0, wy = 0, wz = 0, wSum = 0;
2592
+ for (let j = 0; j <= degree; j++) {
2593
+ const idx = span - degree + j;
2594
+ const w = weights[idx] * N[j];
2595
+ wx += w * controlPoints[idx][0];
2596
+ wy += w * controlPoints[idx][1];
2597
+ wz += w * controlPoints[idx][2];
2598
+ wSum += w;
2599
+ }
2600
+ if (wSum === 0) return [0, 0, 0];
2601
+ return [wx / wSum, wy / wSum, wz / wSum];
2602
+ }
2603
+ function deBoor3DDeriv(controlPoints, weights, knots, degree, u) {
2604
+ const n = controlPoints.length;
2605
+ const span = findSpan(n, degree, u, knots);
2606
+ const ders = basisFunsDeriv(span, u, degree, knots, 1);
2607
+ const N = ders[0];
2608
+ const dN = ders[1];
2609
+ let ax = 0, ay = 0, az = 0, wVal = 0;
2610
+ let dax = 0, day = 0, daz = 0, dwVal = 0;
2611
+ for (let j = 0; j <= degree; j++) {
2612
+ const idx = span - degree + j;
2613
+ const wi = weights[idx];
2614
+ const px = controlPoints[idx][0], py = controlPoints[idx][1], pz = controlPoints[idx][2];
2615
+ ax += N[j] * wi * px;
2616
+ ay += N[j] * wi * py;
2617
+ az += N[j] * wi * pz;
2618
+ wVal += N[j] * wi;
2619
+ dax += dN[j] * wi * px;
2620
+ day += dN[j] * wi * py;
2621
+ daz += dN[j] * wi * pz;
2622
+ dwVal += dN[j] * wi;
2623
+ }
2624
+ if (wVal === 0) return [0, 0, 0];
2625
+ const invW = 1 / wVal;
2626
+ return [
2627
+ (dax - dwVal * ax * invW) * invW,
2628
+ (day - dwVal * ay * invW) * invW,
2629
+ (daz - dwVal * az * invW) * invW
2630
+ ];
2631
+ }
2632
+ function deBoor2D(controlPoints, weights, knots, degree, u) {
2633
+ const n = controlPoints.length;
2634
+ const span = findSpan(n, degree, u, knots);
2635
+ const N = basisFuns(span, u, degree, knots);
2636
+ let wx = 0, wy = 0, wSum = 0;
2637
+ for (let j = 0; j <= degree; j++) {
2638
+ const idx = span - degree + j;
2639
+ const w = weights[idx] * N[j];
2640
+ wx += w * controlPoints[idx][0];
2641
+ wy += w * controlPoints[idx][1];
2642
+ wSum += w;
2643
+ }
2644
+ if (wSum === 0) return [0, 0];
2645
+ return [wx / wSum, wy / wSum];
2646
+ }
2647
+ function sampleNurbs3D(controlPoints, weights, knots, degree, count) {
2648
+ const n = controlPoints.length;
2649
+ const uMin = knots[degree];
2650
+ const uMax = knots[n];
2651
+ const result = new Array(count);
2652
+ for (let i = 0; i < count; i++) {
2653
+ const t = i / (count - 1);
2654
+ const u = uMin + t * (uMax - uMin);
2655
+ result[i] = deBoor3D(controlPoints, weights, knots, degree, u);
2656
+ }
2657
+ return result;
2658
+ }
2659
+ function remapToKnotDomain(t, n, degree, knots) {
2660
+ const uMin = knots[degree];
2661
+ const uMax = knots[n];
2662
+ return uMin + Math.max(0, Math.min(1, t)) * (uMax - uMin);
2663
+ }
2413
2664
  function catmullRom3D$2(p0, p1, p2, p3, t, tension) {
2414
2665
  const tt = t * t;
2415
2666
  const ttt = tt * t;
@@ -2544,6 +2795,10 @@ function evalPathAt(path2, t) {
2544
2795
  h00 * path2.p0[2] + h10 * path2.t0[2] + h20 * path2.c0[2] + h01 * path2.p1[2] + h11 * path2.t1[2] + h21 * path2.c1[2]
2545
2796
  ];
2546
2797
  }
2798
+ case "nurbs": {
2799
+ const u = remapToKnotDomain(Math.max(0, Math.min(1, t)), path2.controlPoints.length, path2.degree, path2.knots);
2800
+ return deBoor3D(path2.controlPoints, path2.weights, path2.knots, path2.degree, u);
2801
+ }
2547
2802
  }
2548
2803
  }
2549
2804
  function estimateCurvatureAt(path2, t) {
@@ -2578,6 +2833,18 @@ function sweepPathToPolyline(path2, samples = 48) {
2578
2833
  path2.c1,
2579
2834
  samples
2580
2835
  );
2836
+ case "nurbs": {
2837
+ const n = path2.controlPoints.length;
2838
+ const uMin = path2.knots[path2.degree];
2839
+ const uMax = path2.knots[n];
2840
+ const result = new Array(samples);
2841
+ for (let i = 0; i < samples; i++) {
2842
+ const t = i / (samples - 1);
2843
+ const u = uMin + t * (uMax - uMin);
2844
+ result[i] = deBoor3D(path2.controlPoints, path2.weights, path2.knots, path2.degree, u);
2845
+ }
2846
+ return result;
2847
+ }
2581
2848
  }
2582
2849
  }
2583
2850
  function sweepPathToPolylineAdaptive(path2, baseSamples = 48) {
@@ -2811,6 +3078,8 @@ function searchOwnerMatch(plan, owner) {
2811
3078
  case "importedMesh":
2812
3079
  case "sdf":
2813
3080
  case "fromSlices":
3081
+ case "nurbsSurface":
3082
+ case "importedStep":
2814
3083
  return {
2815
3084
  issue: {
2816
3085
  code: "edge-owner-not-found",
@@ -2892,7 +3161,7 @@ function propagateCandidateAcrossRewrite(plan, candidate) {
2892
3161
  return edgeSuccess(candidate.selection, preservedEntry.query);
2893
3162
  }
2894
3163
  function resolvePropagatedEdgeQueryAtOwnerBase(ownerBase, ref) {
2895
- if (ownerBase.kind === "box" || ownerBase.kind === "cylinder" || ownerBase.kind === "sphere" || ownerBase.kind === "extrude" || ownerBase.kind === "sheetMetal" || ownerBase.kind === "revolve" || ownerBase.kind === "loft" || ownerBase.kind === "sweep" || ownerBase.kind === "variableSweep" || ownerBase.kind === "transform" || ownerBase.kind === "queryOwner" || ownerBase.kind === "filletEdges" || ownerBase.kind === "chamferEdges" || ownerBase.kind === "importedMesh" || ownerBase.kind === "sdf" || ownerBase.kind === "fromSlices" || ownerBase.kind === "torus" || ownerBase.kind === "draft" || ownerBase.kind === "offsetSolid") {
3164
+ if (ownerBase.kind === "box" || ownerBase.kind === "cylinder" || ownerBase.kind === "sphere" || ownerBase.kind === "extrude" || ownerBase.kind === "sheetMetal" || ownerBase.kind === "revolve" || ownerBase.kind === "loft" || ownerBase.kind === "sweep" || ownerBase.kind === "variableSweep" || ownerBase.kind === "transform" || ownerBase.kind === "queryOwner" || ownerBase.kind === "filletEdges" || ownerBase.kind === "chamferEdges" || ownerBase.kind === "importedMesh" || ownerBase.kind === "sdf" || ownerBase.kind === "fromSlices" || ownerBase.kind === "nurbsSurface" || ownerBase.kind === "importedStep" || ownerBase.kind === "torus" || ownerBase.kind === "draft" || ownerBase.kind === "offsetSolid") {
2896
3165
  return edgeIssue(
2897
3166
  "edge-query-propagation-mismatch",
2898
3167
  "The selected propagated edge query does not point at a topology-rewrite result on this target shape."
@@ -3118,6 +3387,8 @@ function resolveSelectionFromOwnerBase(plan, edgeName) {
3118
3387
  case "importedMesh":
3119
3388
  case "sdf":
3120
3389
  case "fromSlices":
3390
+ case "nurbsSurface":
3391
+ case "importedStep":
3121
3392
  return edgeIssue(
3122
3393
  "unsupported-edge-base",
3123
3394
  "Edge finishing v1 currently supports tracked vertical edges from compile-covered box() bodies and rectangle extrusions before topology-changing edits."
@@ -3152,7 +3423,7 @@ function resolveSupportedEdgeFeatureSelection(plan, ref) {
3152
3423
  return edgeIssue("edge-query-unsupported-after-rewrite", descendant.reason);
3153
3424
  }
3154
3425
  function resolveEdgeChainAtOwnerBase(ownerBase, ref) {
3155
- if (ownerBase.kind === "box" || ownerBase.kind === "cylinder" || ownerBase.kind === "sphere" || ownerBase.kind === "extrude" || ownerBase.kind === "sheetMetal" || ownerBase.kind === "revolve" || ownerBase.kind === "loft" || ownerBase.kind === "sweep" || ownerBase.kind === "variableSweep" || ownerBase.kind === "transform" || ownerBase.kind === "queryOwner" || ownerBase.kind === "filletEdges" || ownerBase.kind === "chamferEdges" || ownerBase.kind === "importedMesh" || ownerBase.kind === "sdf" || ownerBase.kind === "fromSlices" || ownerBase.kind === "torus" || ownerBase.kind === "draft" || ownerBase.kind === "offsetSolid") {
3426
+ if (ownerBase.kind === "box" || ownerBase.kind === "cylinder" || ownerBase.kind === "sphere" || ownerBase.kind === "extrude" || ownerBase.kind === "sheetMetal" || ownerBase.kind === "revolve" || ownerBase.kind === "loft" || ownerBase.kind === "sweep" || ownerBase.kind === "variableSweep" || ownerBase.kind === "transform" || ownerBase.kind === "queryOwner" || ownerBase.kind === "filletEdges" || ownerBase.kind === "chamferEdges" || ownerBase.kind === "importedMesh" || ownerBase.kind === "sdf" || ownerBase.kind === "fromSlices" || ownerBase.kind === "nurbsSurface" || ownerBase.kind === "importedStep" || ownerBase.kind === "torus" || ownerBase.kind === "draft" || ownerBase.kind === "offsetSolid") {
3156
3427
  return {
3157
3428
  kind: "unsupported",
3158
3429
  query: cloneEdgeQueryRef(ref),
@@ -3185,7 +3456,7 @@ function resolveEdgeChainAtOwnerBase(ownerBase, ref) {
3185
3456
  };
3186
3457
  }
3187
3458
  function resolveCreatedEdgeChainAtOwnerBase(ownerBase, ref) {
3188
- if (ownerBase.kind === "box" || ownerBase.kind === "cylinder" || ownerBase.kind === "sphere" || ownerBase.kind === "extrude" || ownerBase.kind === "sheetMetal" || ownerBase.kind === "revolve" || ownerBase.kind === "loft" || ownerBase.kind === "sweep" || ownerBase.kind === "variableSweep" || ownerBase.kind === "transform" || ownerBase.kind === "queryOwner" || ownerBase.kind === "filletEdges" || ownerBase.kind === "chamferEdges" || ownerBase.kind === "importedMesh" || ownerBase.kind === "sdf" || ownerBase.kind === "fromSlices" || ownerBase.kind === "torus" || ownerBase.kind === "draft" || ownerBase.kind === "offsetSolid") {
3459
+ if (ownerBase.kind === "box" || ownerBase.kind === "cylinder" || ownerBase.kind === "sphere" || ownerBase.kind === "extrude" || ownerBase.kind === "sheetMetal" || ownerBase.kind === "revolve" || ownerBase.kind === "loft" || ownerBase.kind === "sweep" || ownerBase.kind === "variableSweep" || ownerBase.kind === "transform" || ownerBase.kind === "queryOwner" || ownerBase.kind === "filletEdges" || ownerBase.kind === "chamferEdges" || ownerBase.kind === "importedMesh" || ownerBase.kind === "sdf" || ownerBase.kind === "fromSlices" || ownerBase.kind === "nurbsSurface" || ownerBase.kind === "importedStep" || ownerBase.kind === "torus" || ownerBase.kind === "draft" || ownerBase.kind === "offsetSolid") {
3189
3460
  return {
3190
3461
  kind: "unsupported",
3191
3462
  query: cloneEdgeQueryRef(ref),
@@ -4678,6 +4949,8 @@ function lowerBaseShellPlanToConcretePlan(plan, thickness, openFaces) {
4678
4949
  case "importedMesh":
4679
4950
  case "sdf":
4680
4951
  case "fromSlices":
4952
+ case "nurbsSurface":
4953
+ case "importedStep":
4681
4954
  return {
4682
4955
  ok: false,
4683
4956
  reason: `Shape.shell() supports box(), cylinder(), straight extrude(), loft(), sweep(), and variableSweep() bases. "${plan.kind}" bases are not supported.`
@@ -5016,7 +5289,7 @@ const defaultPerm = doublePerm(p);
5016
5289
  const defaultPermMod12 = permMod12(defaultPerm);
5017
5290
  const F3 = 1 / 3;
5018
5291
  const G3 = 1 / 6;
5019
- function dot3$4(gi, x, y, z) {
5292
+ function dot3$5(gi, x, y, z) {
5020
5293
  const o = gi * 3;
5021
5294
  return grad3[o] * x + grad3[o + 1] * y + grad3[o + 2] * z;
5022
5295
  }
@@ -5126,28 +5399,28 @@ function simplex3Core(x, y, z, perm, pm12) {
5126
5399
  n0 = 0;
5127
5400
  } else {
5128
5401
  t0 *= t0;
5129
- n0 = t0 * t0 * dot3$4(gi0, x0, y0, z0);
5402
+ n0 = t0 * t0 * dot3$5(gi0, x0, y0, z0);
5130
5403
  }
5131
5404
  let t1 = 0.6 - x1 * x1 - y1 * y1 - z1 * z1;
5132
5405
  if (t1 < 0) {
5133
5406
  n1 = 0;
5134
5407
  } else {
5135
5408
  t1 *= t1;
5136
- n1 = t1 * t1 * dot3$4(gi1, x1, y1, z1);
5409
+ n1 = t1 * t1 * dot3$5(gi1, x1, y1, z1);
5137
5410
  }
5138
5411
  let t2 = 0.6 - x2 * x2 - y2 * y2 - z2 * z2;
5139
5412
  if (t2 < 0) {
5140
5413
  n2 = 0;
5141
5414
  } else {
5142
5415
  t2 *= t2;
5143
- n2 = t2 * t2 * dot3$4(gi2, x2, y2, z2);
5416
+ n2 = t2 * t2 * dot3$5(gi2, x2, y2, z2);
5144
5417
  }
5145
5418
  let t3 = 0.6 - x3 * x3 - y3 * y3 - z3 * z3;
5146
5419
  if (t3 < 0) {
5147
5420
  n3 = 0;
5148
5421
  } else {
5149
5422
  t3 *= t3;
5150
- n3 = t3 * t3 * dot3$4(gi3, x3, y3, z3);
5423
+ n3 = t3 * t3 * dot3$5(gi3, x3, y3, z3);
5151
5424
  }
5152
5425
  return 32 * (n0 + n1 + n2 + n3);
5153
5426
  }
@@ -5911,6 +6184,158 @@ function padBounds(b, pad) {
5911
6184
  max: [b.max[0] + pad, b.max[1] + pad, b.max[2] + pad]
5912
6185
  };
5913
6186
  }
6187
+ function requireFinite$6(v, label) {
6188
+ if (!Number.isFinite(v)) throw new Error(`nurbsSurface: ${label} must be finite, got ${v}`);
6189
+ }
6190
+ class NurbsSurface {
6191
+ // columns in control grid
6192
+ constructor(controlGrid, options = {}) {
6193
+ __publicField(this, "controlGrid");
6194
+ __publicField(this, "weightsGrid");
6195
+ __publicField(this, "knotsU");
6196
+ __publicField(this, "knotsV");
6197
+ __publicField(this, "degreeU");
6198
+ __publicField(this, "degreeV");
6199
+ __publicField(this, "nU");
6200
+ // rows in control grid
6201
+ __publicField(this, "nV");
6202
+ const nU = controlGrid.length;
6203
+ if (nU < 2) throw new Error("nurbsSurface: controlGrid must have at least 2 rows");
6204
+ const nV = controlGrid[0].length;
6205
+ if (nV < 2) throw new Error("nurbsSurface: controlGrid must have at least 2 columns");
6206
+ const degreeU = options.degreeU ?? Math.min(nU - 1, 3);
6207
+ const degreeV = options.degreeV ?? Math.min(nV - 1, 3);
6208
+ if (nU < degreeU + 1) throw new Error(`nurbsSurface: need at least ${degreeU + 1} rows for degreeU=${degreeU}, got ${nU}`);
6209
+ if (nV < degreeV + 1) throw new Error(`nurbsSurface: need at least ${degreeV + 1} columns for degreeV=${degreeV}, got ${nV}`);
6210
+ for (let i = 0; i < nU; i++) {
6211
+ if (controlGrid[i].length !== nV) throw new Error(`nurbsSurface: row ${i} has ${controlGrid[i].length} points, expected ${nV}`);
6212
+ for (let j = 0; j < nV; j++) {
6213
+ requireFinite$6(controlGrid[i][j][0], `controlGrid[${i}][${j}][0]`);
6214
+ requireFinite$6(controlGrid[i][j][1], `controlGrid[${i}][${j}][1]`);
6215
+ requireFinite$6(controlGrid[i][j][2], `controlGrid[${i}][${j}][2]`);
6216
+ }
6217
+ }
6218
+ const weightsGrid = options.weights ?? controlGrid.map((row) => row.map(() => 1));
6219
+ for (let i = 0; i < nU; i++) {
6220
+ if (weightsGrid[i].length !== nV) throw new Error(`nurbsSurface: weights row ${i} length mismatch`);
6221
+ for (let j = 0; j < nV; j++) {
6222
+ requireFinite$6(weightsGrid[i][j], `weights[${i}][${j}]`);
6223
+ if (weightsGrid[i][j] <= 0) throw new Error(`nurbsSurface: weights[${i}][${j}] must be > 0`);
6224
+ }
6225
+ }
6226
+ const knotsU = options.knotsU ?? generateClampedKnots(nU, degreeU);
6227
+ const knotsV = options.knotsV ?? generateClampedKnots(nV, degreeV);
6228
+ if (knotsU.length !== nU + degreeU + 1) throw new Error(`nurbsSurface: knotsU.length should be ${nU + degreeU + 1}, got ${knotsU.length}`);
6229
+ if (knotsV.length !== nV + degreeV + 1) throw new Error(`nurbsSurface: knotsV.length should be ${nV + degreeV + 1}, got ${knotsV.length}`);
6230
+ this.controlGrid = controlGrid.map((row) => row.map(([x, y, z]) => [x, y, z]));
6231
+ this.weightsGrid = weightsGrid.map((row) => [...row]);
6232
+ this.knotsU = [...knotsU];
6233
+ this.knotsV = [...knotsV];
6234
+ this.degreeU = degreeU;
6235
+ this.degreeV = degreeV;
6236
+ this.nU = nU;
6237
+ this.nV = nV;
6238
+ }
6239
+ /**
6240
+ * Evaluate the surface at parameters (u, v) ∈ [0, 1]².
6241
+ * Uses tensor product evaluation: evaluate basis functions in U and V independently.
6242
+ */
6243
+ pointAt(u, v) {
6244
+ const uu = this.remapU(Math.max(0, Math.min(1, u)));
6245
+ const vv = this.remapV(Math.max(0, Math.min(1, v)));
6246
+ const spanU = findSpan(this.nU, this.degreeU, uu, this.knotsU);
6247
+ const spanV = findSpan(this.nV, this.degreeV, vv, this.knotsV);
6248
+ const Nu = basisFuns(spanU, uu, this.degreeU, this.knotsU);
6249
+ const Nv = basisFuns(spanV, vv, this.degreeV, this.knotsV);
6250
+ let wx = 0, wy = 0, wz = 0, wSum = 0;
6251
+ for (let i = 0; i <= this.degreeU; i++) {
6252
+ const rowIdx = spanU - this.degreeU + i;
6253
+ for (let j = 0; j <= this.degreeV; j++) {
6254
+ const colIdx = spanV - this.degreeV + j;
6255
+ const w = Nu[i] * Nv[j] * this.weightsGrid[rowIdx][colIdx];
6256
+ const pt = this.controlGrid[rowIdx][colIdx];
6257
+ wx += w * pt[0];
6258
+ wy += w * pt[1];
6259
+ wz += w * pt[2];
6260
+ wSum += w;
6261
+ }
6262
+ }
6263
+ if (wSum === 0) return [0, 0, 0];
6264
+ return [wx / wSum, wy / wSum, wz / wSum];
6265
+ }
6266
+ /**
6267
+ * Evaluate the surface normal at (u, v) via cross product of partial derivatives.
6268
+ */
6269
+ normalAt(u, v) {
6270
+ const eps = 1e-5;
6271
+ const u0 = Math.max(0, u - eps), u1 = Math.min(1, u + eps);
6272
+ const v0 = Math.max(0, v - eps), v1 = Math.min(1, v + eps);
6273
+ const pu = this.pointAt(u1, v), pmu = this.pointAt(u0, v);
6274
+ const pv = this.pointAt(u, v1), pmv = this.pointAt(u, v0);
6275
+ const du = [pu[0] - pmu[0], pu[1] - pmu[1], pu[2] - pmu[2]];
6276
+ const dv = [pv[0] - pmv[0], pv[1] - pmv[1], pv[2] - pmv[2]];
6277
+ const nx = du[1] * dv[2] - du[2] * dv[1];
6278
+ const ny = du[2] * dv[0] - du[0] * dv[2];
6279
+ const nz = du[0] * dv[1] - du[1] * dv[0];
6280
+ const len = Math.sqrt(nx * nx + ny * ny + nz * nz);
6281
+ if (len < 1e-12) return [0, 0, 1];
6282
+ return [nx / len, ny / len, nz / len];
6283
+ }
6284
+ /**
6285
+ * Tessellate the surface into a triangle mesh.
6286
+ * Returns positions, normals, and triangle indices.
6287
+ */
6288
+ tessellate(resU = 32, resV = 32) {
6289
+ const positions = [];
6290
+ const normals = [];
6291
+ for (let i = 0; i <= resU; i++) {
6292
+ const u = i / resU;
6293
+ for (let j = 0; j <= resV; j++) {
6294
+ const v = j / resV;
6295
+ positions.push(this.pointAt(u, v));
6296
+ normals.push(this.normalAt(u, v));
6297
+ }
6298
+ }
6299
+ const indices = [];
6300
+ for (let i = 0; i < resU; i++) {
6301
+ for (let j = 0; j < resV; j++) {
6302
+ const a = i * (resV + 1) + j;
6303
+ const b = a + 1;
6304
+ const c = (i + 1) * (resV + 1) + j;
6305
+ const d = c + 1;
6306
+ indices.push(a, c, b);
6307
+ indices.push(b, c, d);
6308
+ }
6309
+ }
6310
+ return { positions, normals, indices };
6311
+ }
6312
+ remapU(t) {
6313
+ return this.knotsU[this.degreeU] + t * (this.knotsU[this.nU] - this.knotsU[this.degreeU]);
6314
+ }
6315
+ remapV(t) {
6316
+ return this.knotsV[this.degreeV] + t * (this.knotsV[this.nV] - this.knotsV[this.degreeV]);
6317
+ }
6318
+ }
6319
+ function nurbsSurface(controlGrid, options) {
6320
+ const surface = new NurbsSurface(controlGrid, options);
6321
+ const thickness = (options == null ? void 0 : options.thickness) ?? 1;
6322
+ const resolution = (options == null ? void 0 : options.resolution) ?? 32;
6323
+ return buildShapeFromCompilePlan(
6324
+ createOwnedShapeCompilePlan({
6325
+ kind: "nurbsSurface",
6326
+ controlGrid: surface.controlGrid.map(
6327
+ (row) => row.map(([x, y, z]) => [x, y, z])
6328
+ ),
6329
+ weightsGrid: surface.weightsGrid.map((row) => [...row]),
6330
+ knotsU: [...surface.knotsU],
6331
+ knotsV: [...surface.knotsV],
6332
+ degreeU: surface.degreeU,
6333
+ degreeV: surface.degreeV,
6334
+ thickness,
6335
+ resolution
6336
+ }, "nurbsSurface")
6337
+ );
6338
+ }
5914
6339
  let _simplifier = null;
5915
6340
  async function initMeshoptimizer() {
5916
6341
  if (_simplifier) return;
@@ -6314,6 +6739,8 @@ function sampleSweepPath(path2, edgeLengthHint) {
6314
6739
  return sweepPathToPolyline(path2, resolveCurveSampleCount(edgeLengthHint, path2.chordLength));
6315
6740
  case "quintic-hermite":
6316
6741
  return sweepPathToPolyline(path2, resolveCurveSampleCount(edgeLengthHint, path2.chordLength));
6742
+ case "nurbs":
6743
+ return sweepPathToPolyline(path2, edgeLengthHint != null ? Math.max(16, Math.round(path2.controlPoints.length * 12 / Math.max(0.1, edgeLengthHint))) : 48);
6317
6744
  }
6318
6745
  }
6319
6746
  function clamp$5(v, lo, hi) {
@@ -7169,11 +7596,15 @@ const SHAPE_BACKEND_MARKER = Symbol.for("forgecad.shapeBackend");
7169
7596
  function isShapeBackend(value) {
7170
7597
  return Boolean(value && typeof value === "object" && value[SHAPE_BACKEND_MARKER] === true);
7171
7598
  }
7599
+ function disposeShapeBackend(backend) {
7600
+ const dispose = backend.dispose;
7601
+ dispose == null ? void 0 : dispose.call(backend);
7602
+ }
7172
7603
  let _wasm = null;
7173
7604
  async function initManifoldWasm() {
7174
7605
  if (_wasm) return _wasm;
7175
7606
  performance.mark("manifold:start");
7176
- const Module = (await import("./manifold-G5sBaXzi.js")).default;
7607
+ const Module = (await import("./manifold-O2AAGXyj.js")).default;
7177
7608
  performance.mark("manifold:imported");
7178
7609
  const wasm = await Module();
7179
7610
  wasm.setup();
@@ -7190,79 +7621,115 @@ function getManifoldWasm() {
7190
7621
  if (!_wasm) throw new Error("Manifold WASM not initialized — call initKernel() first");
7191
7622
  return _wasm;
7192
7623
  }
7624
+ function getWasmHeapBytes() {
7625
+ if (!_wasm) return null;
7626
+ const heap = _wasm.HEAPU8;
7627
+ return heap ? heap.buffer.byteLength : null;
7628
+ }
7193
7629
  function isManifoldCapableBackend(b) {
7194
7630
  return typeof b.requireManifold === "function";
7195
7631
  }
7196
7632
  _a2 = SHAPE_BACKEND_MARKER;
7197
7633
  const _ManifoldShapeBackend = class _ManifoldShapeBackend {
7198
- constructor(manifold) {
7634
+ constructor(manifoldOrResource) {
7199
7635
  __publicField(this, _a2, true);
7200
- this.manifold = manifold;
7636
+ __publicField(this, "resource");
7637
+ __publicField(this, "released", false);
7638
+ this.resource = "refCount" in manifoldOrResource ? manifoldOrResource : {
7639
+ manifold: manifoldOrResource,
7640
+ refCount: 1,
7641
+ disposed: false
7642
+ };
7643
+ }
7644
+ getLiveManifold(apiName = "ManifoldShapeBackend") {
7645
+ if (this.released || this.resource.disposed) {
7646
+ throw new Error(`${apiName}: manifold backend was already disposed`);
7647
+ }
7648
+ return this.resource.manifold;
7201
7649
  }
7202
7650
  clone() {
7203
- return new _ManifoldShapeBackend(this.manifold);
7651
+ this.resource.refCount += 1;
7652
+ return new _ManifoldShapeBackend(this.resource);
7204
7653
  }
7205
7654
  translate(x, y, z) {
7206
- return new _ManifoldShapeBackend(this.manifold.translate(x, y, z));
7655
+ return new _ManifoldShapeBackend(this.getLiveManifold("translate()").translate(x, y, z));
7207
7656
  }
7208
7657
  rotate(x, y, z) {
7209
- return new _ManifoldShapeBackend(this.manifold.rotate(x, y, z));
7658
+ return new _ManifoldShapeBackend(this.getLiveManifold("rotate()").rotate(x, y, z));
7210
7659
  }
7211
7660
  transform(m) {
7212
- return new _ManifoldShapeBackend(this.manifold.transform(m));
7661
+ return new _ManifoldShapeBackend(this.getLiveManifold("transform()").transform(m));
7213
7662
  }
7214
7663
  scale(v) {
7215
- return new _ManifoldShapeBackend(this.manifold.scale(v));
7664
+ return new _ManifoldShapeBackend(this.getLiveManifold("scale()").scale(v));
7216
7665
  }
7217
7666
  mirror(normal) {
7218
- return new _ManifoldShapeBackend(this.manifold.mirror(normal));
7667
+ return new _ManifoldShapeBackend(this.getLiveManifold("mirror()").mirror(normal));
7219
7668
  }
7220
7669
  split(other) {
7221
- const [inside, outside] = this.manifold.split(requireManifoldShapeBackend(other, "ShapeBackend.split()"));
7670
+ const [inside, outside] = this.getLiveManifold("split()").split(requireManifoldShapeBackend(other, "ShapeBackend.split()"));
7222
7671
  return [new _ManifoldShapeBackend(inside), new _ManifoldShapeBackend(outside)];
7223
7672
  }
7224
7673
  splitByPlane(normal, originOffset) {
7225
- const [inside, outside] = this.manifold.splitByPlane(normal, originOffset);
7674
+ const [inside, outside] = this.getLiveManifold("splitByPlane()").splitByPlane(normal, originOffset);
7226
7675
  return [new _ManifoldShapeBackend(inside), new _ManifoldShapeBackend(outside)];
7227
7676
  }
7228
7677
  trimByPlane(normal, originOffset) {
7229
- return new _ManifoldShapeBackend(this.manifold.trimByPlane(normal, originOffset));
7678
+ return new _ManifoldShapeBackend(this.getLiveManifold("trimByPlane()").trimByPlane(normal, originOffset));
7230
7679
  }
7231
7680
  simplify(tolerance) {
7232
- return new _ManifoldShapeBackend(this.manifold.simplify(tolerance));
7681
+ return new _ManifoldShapeBackend(this.getLiveManifold("simplify()").simplify(tolerance));
7233
7682
  }
7234
7683
  boundingBox() {
7235
- return this.manifold.boundingBox();
7684
+ return this.getLiveManifold("boundingBox()").boundingBox();
7236
7685
  }
7237
7686
  volume() {
7238
- return this.manifold.volume();
7687
+ return this.getLiveManifold("volume()").volume();
7239
7688
  }
7240
7689
  surfaceArea() {
7241
- return this.manifold.surfaceArea();
7690
+ return this.getLiveManifold("surfaceArea()").surfaceArea();
7242
7691
  }
7243
7692
  minGap(other, searchLength) {
7244
- return this.manifold.minGap(requireManifoldShapeBackend(other, "ShapeBackend.minGap()"), searchLength);
7693
+ return this.getLiveManifold("minGap()").minGap(requireManifoldShapeBackend(other, "ShapeBackend.minGap()"), searchLength);
7245
7694
  }
7246
7695
  isEmpty() {
7247
- return this.manifold.isEmpty();
7696
+ return this.getLiveManifold("isEmpty()").isEmpty();
7248
7697
  }
7249
7698
  numBodies() {
7250
- return this.manifold.decompose().length;
7699
+ const parts = this.getLiveManifold("numBodies()").decompose();
7700
+ try {
7701
+ return parts.length;
7702
+ } finally {
7703
+ parts.forEach((part) => {
7704
+ var _a3;
7705
+ return (_a3 = part.delete) == null ? void 0 : _a3.call(part);
7706
+ });
7707
+ }
7251
7708
  }
7252
7709
  numTri() {
7253
- return this.manifold.numTri();
7710
+ return this.getLiveManifold("numTri()").numTri();
7254
7711
  }
7255
7712
  getMesh() {
7256
- return this.manifold.getMesh();
7713
+ return this.getLiveManifold("getMesh()").getMesh();
7257
7714
  }
7258
7715
  slice(offset2) {
7259
- return this.manifold.slice(offset2);
7716
+ return this.getLiveManifold("slice()").slice(offset2);
7260
7717
  }
7261
7718
  project() {
7262
- return this.manifold.project();
7719
+ return this.getLiveManifold("project()").project();
7263
7720
  }
7264
7721
  requireManifold() {
7265
- return this.manifold;
7722
+ return this.getLiveManifold("requireManifold()");
7723
+ }
7724
+ dispose() {
7725
+ var _a3, _b3;
7726
+ if (this.released) return;
7727
+ this.released = true;
7728
+ this.resource.refCount = Math.max(0, this.resource.refCount - 1);
7729
+ if (this.resource.refCount === 0 && !this.resource.disposed) {
7730
+ this.resource.disposed = true;
7731
+ (_b3 = (_a3 = this.resource.manifold).delete) == null ? void 0 : _b3.call(_a3);
7732
+ }
7266
7733
  }
7267
7734
  };
7268
7735
  let ManifoldShapeBackend = _ManifoldShapeBackend;
@@ -7481,6 +7948,12 @@ function rotateVector(v, axis, c, s) {
7481
7948
  v[2] * c + kCrossV[2] * s + axis[2] * kDotV * (1 - c)
7482
7949
  ];
7483
7950
  }
7951
+ function disposeWasmObject(value) {
7952
+ if (value != null && typeof value.delete === "function") value.delete();
7953
+ }
7954
+ function disposeWasmObjects(values) {
7955
+ for (const value of values) disposeWasmObject(value);
7956
+ }
7484
7957
  function applyProfileCompileTransform(crossSection, step) {
7485
7958
  switch (step.kind) {
7486
7959
  case "translate":
@@ -7496,7 +7969,9 @@ function applyProfileCompileTransform(crossSection, step) {
7496
7969
  function applyProfileCompileTransforms(crossSection, transforms) {
7497
7970
  let out = crossSection;
7498
7971
  for (const step of transforms) {
7972
+ const prev = out;
7499
7973
  out = applyProfileCompileTransform(out, step);
7974
+ if (out !== prev) disposeWasmObject(prev);
7500
7975
  }
7501
7976
  return out;
7502
7977
  }
@@ -7508,17 +7983,21 @@ function lowerProfileBooleanCompilePlan(plan, wasm) {
7508
7983
  if (profiles.length === 1) {
7509
7984
  return applyProfileCompileTransforms(profiles[0], plan.transforms);
7510
7985
  }
7511
- const combined = (() => {
7512
- switch (plan.op) {
7513
- case "union":
7514
- return wasm.CrossSection.union(profiles);
7515
- case "difference":
7516
- return wasm.CrossSection.difference(profiles);
7517
- case "intersection":
7518
- return wasm.CrossSection.intersection(profiles);
7519
- }
7520
- })();
7521
- return applyProfileCompileTransforms(combined, plan.transforms);
7986
+ try {
7987
+ const combined = (() => {
7988
+ switch (plan.op) {
7989
+ case "union":
7990
+ return wasm.CrossSection.union(profiles);
7991
+ case "difference":
7992
+ return wasm.CrossSection.difference(profiles);
7993
+ case "intersection":
7994
+ return wasm.CrossSection.intersection(profiles);
7995
+ }
7996
+ })();
7997
+ return applyProfileCompileTransforms(combined, plan.transforms);
7998
+ } finally {
7999
+ disposeWasmObjects(profiles);
8000
+ }
7522
8001
  }
7523
8002
  function lowerProfileCompilePlanToCrossSection(plan, wasm) {
7524
8003
  switch (plan.kind) {
@@ -7526,7 +8005,9 @@ function lowerProfileCompilePlanToCrossSection(plan, wasm) {
7526
8005
  return applyProfileCompileTransforms(wasm.CrossSection.square([plan.width, plan.height], true), plan.transforms);
7527
8006
  case "roundedRect": {
7528
8007
  const radius = Math.min(plan.radius, plan.width / 2, plan.height / 2);
7529
- const crossSection = wasm.CrossSection.square([plan.width - 2 * radius, plan.height - 2 * radius], true).offset(radius, "Round");
8008
+ const base = wasm.CrossSection.square([plan.width - 2 * radius, plan.height - 2 * radius], true);
8009
+ const crossSection = base.offset(radius, "Round");
8010
+ if (crossSection !== base) disposeWasmObject(base);
7530
8011
  return applyProfileCompileTransforms(crossSection, plan.transforms);
7531
8012
  }
7532
8013
  case "circle":
@@ -7535,15 +8016,29 @@ function lowerProfileCompilePlanToCrossSection(plan, wasm) {
7535
8016
  return applyProfileCompileTransforms(new wasm.CrossSection([plan.points]), plan.transforms);
7536
8017
  case "boolean":
7537
8018
  return lowerProfileBooleanCompilePlan(plan, wasm);
7538
- case "offset":
7539
- return applyProfileCompileTransforms(
7540
- lowerProfileCompilePlanToCrossSection(plan.base, wasm).offset(plan.delta, plan.join),
7541
- plan.transforms
7542
- );
8019
+ case "offset": {
8020
+ const base = lowerProfileCompilePlanToCrossSection(plan.base, wasm);
8021
+ try {
8022
+ return applyProfileCompileTransforms(base.offset(plan.delta, plan.join), plan.transforms);
8023
+ } finally {
8024
+ disposeWasmObject(base);
8025
+ }
8026
+ }
7543
8027
  case "project": {
7544
- const projected = lowerShapeCompilePlanToManifold(plan.sourceShape, wasm).transform(planeFrameToWorldToPlaneMatrix(plan.plane)).project();
7545
- return applyProfileCompileTransforms(projected, plan.transforms);
8028
+ const source = lowerShapeCompilePlanToManifold(plan.sourceShape, wasm);
8029
+ try {
8030
+ const transformed = source.transform(planeFrameToWorldToPlaneMatrix(plan.plane));
8031
+ try {
8032
+ return applyProfileCompileTransforms(transformed.project(), plan.transforms);
8033
+ } finally {
8034
+ if (transformed !== source) disposeWasmObject(transformed);
8035
+ }
8036
+ } finally {
8037
+ disposeWasmObject(source);
8038
+ }
7546
8039
  }
8040
+ case "pathProfile":
8041
+ return applyProfileCompileTransforms(new wasm.CrossSection([plan.points]), plan.transforms);
7547
8042
  default:
7548
8043
  assertExhaustive(plan);
7549
8044
  }
@@ -7567,7 +8062,9 @@ function applyShapeCompileTransform(manifold, step) {
7567
8062
  function applyShapeCompileTransforms(manifold, steps) {
7568
8063
  let out = manifold;
7569
8064
  for (const step of steps) {
8065
+ const prev = out;
7570
8066
  out = applyShapeCompileTransform(out, step);
8067
+ if (out !== prev) disposeWasmObject(prev);
7571
8068
  }
7572
8069
  return out;
7573
8070
  }
@@ -7582,22 +8079,36 @@ function lowerShapeBooleanCompilePlan(plan, wasm) {
7582
8079
  if (shapes.length === 1) {
7583
8080
  return shapes[0];
7584
8081
  }
7585
- switch (plan.op) {
7586
- case "union":
7587
- return wasm.Manifold.union(shapes);
7588
- case "difference":
7589
- return wasm.Manifold.difference(shapes);
7590
- case "intersection":
7591
- return wasm.Manifold.intersection(shapes);
8082
+ try {
8083
+ switch (plan.op) {
8084
+ case "union":
8085
+ return wasm.Manifold.union(shapes);
8086
+ case "difference":
8087
+ return wasm.Manifold.difference(shapes);
8088
+ case "intersection":
8089
+ return wasm.Manifold.intersection(shapes);
8090
+ }
8091
+ } finally {
8092
+ disposeWasmObjects(shapes);
7592
8093
  }
7593
8094
  }
7594
8095
  function lowerShapeTrimByPlaneCompilePlan(plan, wasm) {
7595
- return lowerShapeCompilePlanToManifold(plan.base, wasm).trimByPlane([plan.normalX, plan.normalY, plan.normalZ], plan.originOffset);
8096
+ const base = lowerShapeCompilePlanToManifold(plan.base, wasm);
8097
+ try {
8098
+ return base.trimByPlane([plan.normalX, plan.normalY, plan.normalZ], plan.originOffset);
8099
+ } finally {
8100
+ disposeWasmObject(base);
8101
+ }
7596
8102
  }
7597
8103
  function lowerShapeLoftCompilePlan(plan, wasm) {
7598
- const inputPolygons = plan.profiles.map(
7599
- (profile) => lowerProfileCompilePlanToCrossSection(profile, wasm).toPolygons()
7600
- );
8104
+ const inputPolygons = plan.profiles.map((profile) => {
8105
+ const crossSection = lowerProfileCompilePlanToCrossSection(profile, wasm);
8106
+ try {
8107
+ return crossSection.toPolygons();
8108
+ } finally {
8109
+ disposeWasmObject(crossSection);
8110
+ }
8111
+ });
7601
8112
  if (inputPolygons.length >= 2) {
7602
8113
  const stitched = loftStitched(inputPolygons, plan.heights, wasm);
7603
8114
  if (stitched) return stitched;
@@ -7606,7 +8117,14 @@ function lowerShapeLoftCompilePlan(plan, wasm) {
7606
8117
  return lowerSdfToManifold(levelSetFieldToStandardSdf(input.sdf), input.bounds, input.edgeLength, wasm);
7607
8118
  }
7608
8119
  function lowerShapeSweepCompilePlan(plan, wasm) {
7609
- const profilePolygons = lowerProfileCompilePlanToCrossSection(plan.profile, wasm).toPolygons();
8120
+ const crossSection = lowerProfileCompilePlanToCrossSection(plan.profile, wasm);
8121
+ const profilePolygons = (() => {
8122
+ try {
8123
+ return crossSection.toPolygons();
8124
+ } finally {
8125
+ disposeWasmObject(crossSection);
8126
+ }
8127
+ })();
7610
8128
  const pathPoints = sweepPathToPolylineAdaptive(plan.path, plan.pathSamples ?? 48);
7611
8129
  const up = [plan.up[0], plan.up[1], plan.up[2]];
7612
8130
  const stitched = sweepStitched(profilePolygons, pathPoints, up, wasm);
@@ -7618,77 +8136,318 @@ function lowerShapeSweepCompilePlan(plan, wasm) {
7618
8136
  });
7619
8137
  return lowerSdfToManifold(levelSetFieldToStandardSdf(input.sdf), input.bounds, input.edgeLength, wasm);
7620
8138
  }
7621
- function lowerFromSlicesToManifold(plan, wasm) {
7622
- if (plan.groups.length === 0) throw new Error("Shape.fromSlices requires at least one slice");
7623
- const groupSolids = [];
7624
- for (const group2 of plan.groups) {
8139
+ function buildPlaneFrame(normal) {
8140
+ const [nx, ny, nz] = normal;
8141
+ if (Math.abs(nz) > 1 - 1e-8) {
8142
+ const s = nz > 0 ? 1 : -1;
8143
+ return { u: [1, 0, 0], v: [0, s, 0] };
8144
+ }
8145
+ if (Math.abs(ny) > 1 - 1e-8) {
8146
+ const s = ny > 0 ? 1 : -1;
8147
+ return { u: [1, 0, 0], v: [0, 0, s] };
8148
+ }
8149
+ if (Math.abs(nx) > 1 - 1e-8) {
8150
+ const s = nx > 0 ? 1 : -1;
8151
+ return { u: [0, 1, 0], v: [0, 0, s] };
8152
+ }
8153
+ const ref = Math.abs(nx) < 0.9 ? [1, 0, 0] : [0, 1, 0];
8154
+ const uRaw = [
8155
+ ref[1] * nz - ref[2] * ny,
8156
+ ref[2] * nx - ref[0] * nz,
8157
+ ref[0] * ny - ref[1] * nx
8158
+ ];
8159
+ const uLen = Math.sqrt(uRaw[0] * uRaw[0] + uRaw[1] * uRaw[1] + uRaw[2] * uRaw[2]);
8160
+ const u = [uRaw[0] / uLen, uRaw[1] / uLen, uRaw[2] / uLen];
8161
+ const v = [
8162
+ ny * u[2] - nz * u[1],
8163
+ nz * u[0] - nx * u[2],
8164
+ nx * u[1] - ny * u[0]
8165
+ ];
8166
+ return { u, v };
8167
+ }
8168
+ function dot3$4(a, b) {
8169
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
8170
+ }
8171
+ function profileHalfWidths(loops, height) {
8172
+ let maxPos = 0;
8173
+ let maxNeg = 0;
8174
+ for (const loop of loops) {
8175
+ for (let i = 0; i < loop.length; i++) {
8176
+ const [x0, y0] = loop[i];
8177
+ const [x1, y1] = loop[(i + 1) % loop.length];
8178
+ if (y0 <= height && y1 > height || y1 <= height && y0 > height) {
8179
+ const t = (height - y0) / (y1 - y0);
8180
+ const x = x0 + t * (x1 - x0);
8181
+ if (x > maxPos) maxPos = x;
8182
+ if (x < 0 && -x > maxNeg) maxNeg = -x;
8183
+ }
8184
+ }
8185
+ }
8186
+ return [maxPos, maxNeg];
8187
+ }
8188
+ function interpolatedHalfWidths(slices, spineCoord) {
8189
+ if (slices.length === 1) return profileHalfWidths(slices[0].polygons, spineCoord);
8190
+ if (spineCoord <= slices[0].offset) return profileHalfWidths(slices[0].polygons, spineCoord);
8191
+ if (spineCoord >= slices[slices.length - 1].offset) {
8192
+ return profileHalfWidths(slices[slices.length - 1].polygons, spineCoord);
8193
+ }
8194
+ let seg = 0;
8195
+ while (seg + 1 < slices.length && spineCoord > slices[seg + 1].offset) seg++;
8196
+ const t = (spineCoord - slices[seg].offset) / (slices[seg + 1].offset - slices[seg].offset);
8197
+ const [p0, n0] = profileHalfWidths(slices[seg].polygons, spineCoord);
8198
+ const [p1, n1] = profileHalfWidths(slices[seg + 1].polygons, spineCoord);
8199
+ return [p0 * (1 - t) + p1 * t, n0 * (1 - t) + n1 * t];
8200
+ }
8201
+ function lowerMultiGroupFromSlicesToManifold(plan, wasm) {
8202
+ const n0 = plan.groups[0].normal;
8203
+ const n1 = plan.groups[1].normal;
8204
+ const spineRaw = [
8205
+ n0[1] * n1[2] - n0[2] * n1[1],
8206
+ n0[2] * n1[0] - n0[0] * n1[2],
8207
+ n0[0] * n1[1] - n0[1] * n1[0]
8208
+ ];
8209
+ const spineLen = Math.sqrt(spineRaw[0] ** 2 + spineRaw[1] ** 2 + spineRaw[2] ** 2);
8210
+ if (spineLen < 1e-8) {
8211
+ throw new Error("Shape.fromSlices: multi-group slices must have non-parallel normals.");
8212
+ }
8213
+ const spine = [spineRaw[0] / spineLen, spineRaw[1] / spineLen, spineRaw[2] / spineLen];
8214
+ const preparedGroups = plan.groups.map((group2) => {
8215
+ const frame = buildPlaneFrame(group2.normal);
7625
8216
  const sorted = [...group2.slices].sort((a, b) => a.offset - b.offset);
7626
- const [nx, ny, nz] = group2.normal;
7627
- let solid;
7628
- if (sorted.length === 1) {
7629
- const cross2 = lowerProfileCompilePlanToCrossSection(sorted[0].profile, wasm);
7630
- const extrudeHalf = 500;
7631
- solid = cross2.extrude(2 * extrudeHalf).translate(0, 0, sorted[0].offset - extrudeHalf);
7632
- } else {
7633
- const polygons = sorted.map((s) => lowerProfileCompilePlanToCrossSection(s.profile, wasm).toPolygons());
7634
- const heights = sorted.map((s) => s.offset);
7635
- const stitched = polygons.length >= 2 ? loftStitched(polygons, heights, wasm) : null;
7636
- if (stitched) {
7637
- solid = stitched;
8217
+ const sliceData = sorted.map((s) => {
8218
+ const cross2 = lowerProfileCompilePlanToCrossSection(s.profile, wasm);
8219
+ try {
8220
+ const polygons = cross2.toPolygons();
8221
+ return { offset: s.offset, polygons, sdf: compilePolygonsSdf(polygons) };
8222
+ } finally {
8223
+ disposeWasmObject(cross2);
8224
+ }
8225
+ });
8226
+ const spineOnU = Math.abs(dot3$4(spine, frame.u));
8227
+ const spineOnV = Math.abs(dot3$4(spine, frame.v));
8228
+ const vIsSpine = spineOnV >= spineOnU;
8229
+ const measAxis = vIsSpine ? frame.u : frame.v;
8230
+ const spineAxis = vIsSpine ? frame.v : frame.u;
8231
+ const spineSign = dot3$4(spine, spineAxis) >= 0 ? 1 : -1;
8232
+ let minMeas = Infinity;
8233
+ let maxMeas = -Infinity;
8234
+ let minSpine = Infinity;
8235
+ let maxSpine = -Infinity;
8236
+ for (const s of sliceData) {
8237
+ for (const loop of s.polygons) {
8238
+ for (const [x, y] of loop) {
8239
+ const meas = vIsSpine ? x : y;
8240
+ const sp = vIsSpine ? y : x;
8241
+ if (meas < minMeas) minMeas = meas;
8242
+ if (meas > maxMeas) maxMeas = meas;
8243
+ if (sp < minSpine) minSpine = sp;
8244
+ if (sp > maxSpine) maxSpine = sp;
8245
+ }
8246
+ }
8247
+ }
8248
+ const sliceDataForRayCast = vIsSpine ? sliceData : sliceData.map((s) => ({
8249
+ offset: s.offset,
8250
+ polygons: s.polygons.map((loop) => loop.map(([x, y]) => [y, x]))
8251
+ }));
8252
+ const profileSdf2d = (u, v) => {
8253
+ if (sliceData.length === 1) return sliceData[0].sdf(u, v);
8254
+ const w = vIsSpine ? v : u;
8255
+ const offsets = sliceData.map((s) => s.offset);
8256
+ if (w <= offsets[0]) return sliceData[0].sdf(u, v);
8257
+ if (w >= offsets[offsets.length - 1]) return sliceData[sliceData.length - 1].sdf(u, v);
8258
+ let seg = 0;
8259
+ while (seg + 1 < offsets.length && w > offsets[seg + 1]) seg++;
8260
+ const t = (w - offsets[seg]) / (offsets[seg + 1] - offsets[seg]);
8261
+ return sliceData[seg].sdf(u, v) * (1 - t) + sliceData[seg + 1].sdf(u, v) * t;
8262
+ };
8263
+ return {
8264
+ normal: group2.normal,
8265
+ frame,
8266
+ measAxis,
8267
+ spineSign,
8268
+ vIsSpine,
8269
+ sliceData: sliceDataForRayCast,
8270
+ profileSdf2d,
8271
+ minMeas,
8272
+ maxMeas,
8273
+ minSpine,
8274
+ maxSpine
8275
+ };
8276
+ });
8277
+ const sdf3d = (p2) => {
8278
+ const spineCoord = dot3$4(p2, spine);
8279
+ let sumRhoSq = 0;
8280
+ let profileCapSdf = Infinity;
8281
+ for (const g of preparedGroups) {
8282
+ const measCoord = dot3$4(p2, g.measAxis);
8283
+ const localSpine = spineCoord * g.spineSign;
8284
+ const [wPos, wNeg] = interpolatedHalfWidths(g.sliceData, localSpine);
8285
+ let rho;
8286
+ if (wPos <= 1e-10 && wNeg <= 1e-10) {
8287
+ rho = 2;
8288
+ } else if (measCoord >= 0) {
8289
+ rho = wPos > 1e-10 ? measCoord / wPos : 2;
7638
8290
  } else {
7639
- const input = buildLoftLevelSetInput(polygons, heights, {
7640
- edgeLength: plan.edgeLength,
7641
- boundsPadding: plan.boundsPadding
7642
- });
7643
- solid = lowerSdfToManifold(levelSetFieldToStandardSdf(input.sdf), input.bounds, input.edgeLength, wasm);
8291
+ rho = wNeg > 1e-10 ? -measCoord / wNeg : 2;
7644
8292
  }
8293
+ sumRhoSq += rho * rho;
8294
+ const u = dot3$4(p2, g.frame.u);
8295
+ const v = dot3$4(p2, g.frame.v);
8296
+ const dProfile = g.profileSdf2d(u, v);
8297
+ profileCapSdf = Math.min(profileCapSdf, dProfile);
7645
8298
  }
7646
- if (Math.abs(nx) > 1e-10 || Math.abs(ny) > 1e-10 || nz < 0) {
7647
- if (Math.abs(nx) < 1e-10 && Math.abs(ny) < 1e-10 && nz < 0) {
7648
- solid = solid.rotate([180, 0, 0]);
8299
+ const rhoSdf = sumRhoSq - 1;
8300
+ const capSdf = -profileCapSdf;
8301
+ return Math.max(rhoSdf, capSdf);
8302
+ };
8303
+ let bMinX = -Infinity;
8304
+ let bMaxX = Infinity;
8305
+ let bMinY = -Infinity;
8306
+ let bMaxY = Infinity;
8307
+ let bMinZ = -Infinity;
8308
+ let bMaxZ = Infinity;
8309
+ for (const g of preparedGroups) {
8310
+ for (let axis = 0; axis < 3; axis++) {
8311
+ const mc = g.measAxis[axis];
8312
+ if (Math.abs(mc) > 0.5) {
8313
+ const lo = mc > 0 ? g.minMeas : -g.maxMeas;
8314
+ const hi = mc > 0 ? g.maxMeas : -g.minMeas;
8315
+ if (axis === 0) {
8316
+ bMinX = Math.max(bMinX, lo);
8317
+ bMaxX = Math.min(bMaxX, hi);
8318
+ } else if (axis === 1) {
8319
+ bMinY = Math.max(bMinY, lo);
8320
+ bMaxY = Math.min(bMaxY, hi);
8321
+ } else {
8322
+ bMinZ = Math.max(bMinZ, lo);
8323
+ bMaxZ = Math.min(bMaxZ, hi);
8324
+ }
8325
+ }
8326
+ }
8327
+ }
8328
+ let spineMin = -Infinity;
8329
+ let spineMax = Infinity;
8330
+ for (const g of preparedGroups) {
8331
+ const lo = g.spineSign > 0 ? g.minSpine : -g.maxSpine;
8332
+ const hi = g.spineSign > 0 ? g.maxSpine : -g.minSpine;
8333
+ spineMin = Math.max(spineMin, lo);
8334
+ spineMax = Math.min(spineMax, hi);
8335
+ }
8336
+ for (let axis = 0; axis < 3; axis++) {
8337
+ if (Math.abs(spine[axis]) > 0.5) {
8338
+ const lo = spine[axis] > 0 ? spineMin : -spineMax;
8339
+ const hi = spine[axis] > 0 ? spineMax : -spineMin;
8340
+ if (axis === 0) {
8341
+ bMinX = Math.max(bMinX, lo);
8342
+ bMaxX = Math.min(bMaxX, hi);
8343
+ } else if (axis === 1) {
8344
+ bMinY = Math.max(bMinY, lo);
8345
+ bMaxY = Math.min(bMaxY, hi);
7649
8346
  } else {
7650
- const ax = -ny;
7651
- const ay = nx;
7652
- const len = Math.sqrt(ax * ax + ay * ay);
7653
- const c = nz;
7654
- const s = len;
7655
- const ux = ax / len;
7656
- const uy = ay / len;
7657
- const m00 = c + ux * ux * (1 - c);
7658
- const m01 = ux * uy * (1 - c);
7659
- const m02 = uy * s;
7660
- const m10 = uy * ux * (1 - c);
7661
- const m11 = c + uy * uy * (1 - c);
7662
- const m12 = -ux * s;
7663
- const m20 = -uy * s;
7664
- const m21 = ux * s;
7665
- const m22 = c;
7666
- solid = solid.transform([m00, m01, m02, 0, m10, m11, m12, 0, m20, m21, m22, 0, 0, 0, 0, 1]);
7667
- }
7668
- }
7669
- groupSolids.push(solid);
8347
+ bMinZ = Math.max(bMinZ, lo);
8348
+ bMaxZ = Math.min(bMaxZ, hi);
8349
+ }
8350
+ }
8351
+ }
8352
+ const fallback = 100;
8353
+ if (!isFinite(bMinX)) bMinX = -fallback;
8354
+ if (!isFinite(bMaxX)) bMaxX = fallback;
8355
+ if (!isFinite(bMinY)) bMinY = -fallback;
8356
+ if (!isFinite(bMaxY)) bMaxY = fallback;
8357
+ if (!isFinite(bMinZ)) bMinZ = -fallback;
8358
+ if (!isFinite(bMaxZ)) bMaxZ = fallback;
8359
+ const pad = plan.boundsPadding;
8360
+ const bounds = {
8361
+ min: [bMinX - pad, bMinY - pad, bMinZ - pad],
8362
+ max: [bMaxX + pad, bMaxY + pad, bMaxZ + pad]
8363
+ };
8364
+ return lowerSdfToManifold(sdf3d, bounds, plan.edgeLength, wasm);
8365
+ }
8366
+ function lowerFromSlicesToManifold(plan, wasm) {
8367
+ if (plan.groups.length === 0) throw new Error("Shape.fromSlices requires at least one slice");
8368
+ if (plan.groups.length > 1) {
8369
+ return lowerMultiGroupFromSlicesToManifold(plan, wasm);
7670
8370
  }
7671
- if (groupSolids.length === 1) return groupSolids[0];
7672
- let result = groupSolids[0];
7673
- for (let i = 1; i < groupSolids.length; i++) {
7674
- result = result.intersect(groupSolids[i]);
8371
+ const group2 = plan.groups[0];
8372
+ const sorted = [...group2.slices].sort((a, b) => a.offset - b.offset);
8373
+ const [nx, ny, nz] = group2.normal;
8374
+ let solid;
8375
+ if (sorted.length === 1) {
8376
+ const cross2 = lowerProfileCompilePlanToCrossSection(sorted[0].profile, wasm);
8377
+ const extrudeHalf = 500;
8378
+ try {
8379
+ const extruded = cross2.extrude(2 * extrudeHalf);
8380
+ solid = applyShapeCompileTransforms(extruded, [{ kind: "translate", x: 0, y: 0, z: sorted[0].offset - extrudeHalf }]);
8381
+ } finally {
8382
+ disposeWasmObject(cross2);
8383
+ }
8384
+ } else {
8385
+ const polygons = sorted.map((s) => {
8386
+ const crossSection = lowerProfileCompilePlanToCrossSection(s.profile, wasm);
8387
+ try {
8388
+ return crossSection.toPolygons();
8389
+ } finally {
8390
+ disposeWasmObject(crossSection);
8391
+ }
8392
+ });
8393
+ const heights = sorted.map((s) => s.offset);
8394
+ const stitched = polygons.length >= 2 ? loftStitched(polygons, heights, wasm) : null;
8395
+ if (stitched) {
8396
+ solid = stitched;
8397
+ } else {
8398
+ const input = buildLoftLevelSetInput(polygons, heights, {
8399
+ edgeLength: plan.edgeLength,
8400
+ boundsPadding: plan.boundsPadding
8401
+ });
8402
+ solid = lowerSdfToManifold(levelSetFieldToStandardSdf(input.sdf), input.bounds, input.edgeLength, wasm);
8403
+ }
7675
8404
  }
7676
- return result;
8405
+ if (Math.abs(nx) > 1e-10 || Math.abs(ny) > 1e-10 || nz < 0) {
8406
+ if (Math.abs(nx) < 1e-10 && Math.abs(ny) < 1e-10 && nz < 0) {
8407
+ const prev = solid;
8408
+ solid = solid.rotate([180, 0, 0]);
8409
+ if (solid !== prev) disposeWasmObject(prev);
8410
+ } else {
8411
+ const ax = -ny;
8412
+ const ay = nx;
8413
+ const len = Math.sqrt(ax * ax + ay * ay);
8414
+ const c = nz;
8415
+ const s = len;
8416
+ const ux = ax / len;
8417
+ const uy = ay / len;
8418
+ const m00 = c + ux * ux * (1 - c);
8419
+ const m01 = ux * uy * (1 - c);
8420
+ const m02 = uy * s;
8421
+ const m10 = uy * ux * (1 - c);
8422
+ const m11 = c + uy * uy * (1 - c);
8423
+ const m12 = -ux * s;
8424
+ const m20 = -uy * s;
8425
+ const m21 = ux * s;
8426
+ const m22 = c;
8427
+ const prev = solid;
8428
+ solid = solid.transform([m00, m01, m02, 0, m10, m11, m12, 0, m20, m21, m22, 0, 0, 0, 0, 1]);
8429
+ if (solid !== prev) disposeWasmObject(prev);
8430
+ }
8431
+ }
8432
+ return solid;
7677
8433
  }
7678
8434
  function lowerShapeVariableSweepCompilePlan(plan, wasm) {
7679
- const sectionPolygons = plan.sections.map((s) => ({
7680
- t: s.t,
7681
- polygons: lowerProfileCompilePlanToCrossSection(s.profile, wasm).toPolygons()
7682
- }));
7683
- const input = buildVariableSweepLevelSetInput(
7684
- sectionPolygons,
7685
- plan.path,
7686
- {
7687
- edgeLength: plan.edgeLength,
7688
- boundsPadding: plan.boundsPadding,
7689
- up: [plan.up[0], plan.up[1], plan.up[2]]
8435
+ const sectionPolygons = plan.sections.map((s) => {
8436
+ const crossSection = lowerProfileCompilePlanToCrossSection(s.profile, wasm);
8437
+ try {
8438
+ return {
8439
+ t: s.t,
8440
+ polygons: crossSection.toPolygons()
8441
+ };
8442
+ } finally {
8443
+ disposeWasmObject(crossSection);
7690
8444
  }
7691
- );
8445
+ });
8446
+ const input = buildVariableSweepLevelSetInput(sectionPolygons, plan.path, {
8447
+ edgeLength: plan.edgeLength,
8448
+ boundsPadding: plan.boundsPadding,
8449
+ up: [plan.up[0], plan.up[1], plan.up[2]]
8450
+ });
7692
8451
  return lowerSdfToManifold(levelSetFieldToStandardSdf(input.sdf), input.bounds, input.edgeLength, wasm);
7693
8452
  }
7694
8453
  function lowerShapeFilletCompilePlan(plan, wasm) {
@@ -7699,13 +8458,15 @@ function lowerShapeFilletCompilePlan(plan, wasm) {
7699
8458
  `fillet2d() currently supports ${selection.selection.edgeName} only with quadrant [${selection.selection.quadrant[0]}, ${selection.selection.quadrant[1]}].`
7700
8459
  );
7701
8460
  }
7702
- return applyFilletSelectionToManifold(
7703
- lowerShapeCompilePlanToManifold(plan.base, wasm),
7704
- selection.selection,
7705
- plan.radius,
7706
- plan.segments,
7707
- wasm
7708
- );
8461
+ const base = lowerShapeCompilePlanToManifold(plan.base, wasm);
8462
+ try {
8463
+ const result = applyFilletSelectionToManifold(base, selection.selection, plan.radius, plan.segments, wasm);
8464
+ if (result !== base) disposeWasmObject(base);
8465
+ return result;
8466
+ } catch (error) {
8467
+ disposeWasmObject(base);
8468
+ throw error;
8469
+ }
7709
8470
  }
7710
8471
  function lowerShapeChamferCompilePlan(plan, wasm) {
7711
8472
  const selection = resolveSupportedEdgeFeatureSelection(plan.base, plan.edge);
@@ -7715,7 +8476,15 @@ function lowerShapeChamferCompilePlan(plan, wasm) {
7715
8476
  `chamfer2d() currently supports ${selection.selection.edgeName} only with quadrant [${selection.selection.quadrant[0]}, ${selection.selection.quadrant[1]}].`
7716
8477
  );
7717
8478
  }
7718
- return applyChamferSelectionToManifold(lowerShapeCompilePlanToManifold(plan.base, wasm), selection.selection, plan.size, wasm);
8479
+ const base = lowerShapeCompilePlanToManifold(plan.base, wasm);
8480
+ try {
8481
+ const result = applyChamferSelectionToManifold(base, selection.selection, plan.size, wasm);
8482
+ if (result !== base) disposeWasmObject(base);
8483
+ return result;
8484
+ } catch (error) {
8485
+ disposeWasmObject(base);
8486
+ throw error;
8487
+ }
7719
8488
  }
7720
8489
  function edgeSegmentToSelection(segment) {
7721
8490
  const { start, end, direction: axis, normalA, normalB, convex } = segment;
@@ -7810,7 +8579,9 @@ function lowerFilletEdgesCompilePlan(plan, wasm) {
7810
8579
  try {
7811
8580
  const selection = edgeSegmentToSelection(seg);
7812
8581
  const apply = seg.convex ? applyFilletSelectionToManifold : applyConcaveFilletSelectionToManifold;
7813
- manifold = apply(manifold, selection, plan.radius, plan.segments, wasm);
8582
+ const prev = manifold;
8583
+ manifold = apply(prev, selection, plan.radius, plan.segments, wasm);
8584
+ if (manifold !== prev) disposeWasmObject(prev);
7814
8585
  } catch {
7815
8586
  }
7816
8587
  }
@@ -7835,7 +8606,9 @@ function lowerChamferEdgesCompilePlan(plan, wasm) {
7835
8606
  try {
7836
8607
  const selection = edgeSegmentToSelection(seg);
7837
8608
  const apply = seg.convex ? applyChamferSelectionToManifold : applyConcaveChamferSelectionToManifold;
7838
- manifold = apply(manifold, selection, plan.size, wasm);
8609
+ const prev = manifold;
8610
+ manifold = apply(prev, selection, plan.size, wasm);
8611
+ if (manifold !== prev) disposeWasmObject(prev);
7839
8612
  } catch {
7840
8613
  }
7841
8614
  }
@@ -7844,24 +8617,34 @@ function lowerChamferEdgesCompilePlan(plan, wasm) {
7844
8617
  function lowerShapeCompilePlanToManifold(plan, wasm) {
7845
8618
  switch (plan.kind) {
7846
8619
  case "box":
7847
- return wasm.Manifold.cube([plan.x, plan.y, plan.z], false).translate(-plan.x / 2, -plan.y / 2, 0);
8620
+ return applyShapeCompileTransforms(wasm.Manifold.cube([plan.x, plan.y, plan.z], false), [
8621
+ { kind: "translate", x: -plan.x / 2, y: -plan.y / 2, z: 0 }
8622
+ ]);
7848
8623
  case "cylinder":
7849
8624
  return wasm.Manifold.cylinder(plan.height, plan.radius, plan.radiusTop ?? -1, plan.segments ?? 0, false);
7850
8625
  case "sphere":
7851
8626
  return wasm.Manifold.sphere(plan.radius, plan.segments ?? 0);
7852
8627
  case "torus": {
7853
8628
  const circle2 = wasm.CrossSection.circle(plan.minorRadius, plan.segments ?? 0);
7854
- const translated = circle2.translate([plan.majorRadius, 0]);
7855
- return translated.revolve(plan.segments ?? 0, 360);
8629
+ try {
8630
+ const translated = circle2.translate([plan.majorRadius, 0]);
8631
+ try {
8632
+ return translated.revolve(plan.segments ?? 0, 360);
8633
+ } finally {
8634
+ disposeWasmObject(translated);
8635
+ }
8636
+ } finally {
8637
+ disposeWasmObject(circle2);
8638
+ }
8639
+ }
8640
+ case "extrude": {
8641
+ const profile = lowerProfileCompilePlanToCrossSection(plan.profile, wasm);
8642
+ try {
8643
+ return profile.extrude(plan.height, plan.twistSegments ?? 0, plan.twist ?? 0, plan.scaleTop, false);
8644
+ } finally {
8645
+ disposeWasmObject(profile);
8646
+ }
7856
8647
  }
7857
- case "extrude":
7858
- return lowerProfileCompilePlanToCrossSection(plan.profile, wasm).extrude(
7859
- plan.height,
7860
- plan.twistSegments ?? 0,
7861
- plan.twist ?? 0,
7862
- plan.scaleTop,
7863
- false
7864
- );
7865
8648
  case "sheetMetal":
7866
8649
  return lowerShapeCompilePlanToManifold(lowerSheetMetalBasePlan(plan.model, plan.output), wasm);
7867
8650
  case "shell": {
@@ -7879,8 +8662,14 @@ function lowerShapeCompilePlanToManifold(plan, wasm) {
7879
8662
  if (!lowered.ok) throw new Error(lowered.reason);
7880
8663
  return lowerShapeCompilePlanToManifold(lowered.plan, wasm);
7881
8664
  }
7882
- case "revolve":
7883
- return lowerProfileCompilePlanToCrossSection(plan.profile, wasm).revolve(plan.segments ?? 0, plan.degrees);
8665
+ case "revolve": {
8666
+ const profile = lowerProfileCompilePlanToCrossSection(plan.profile, wasm);
8667
+ try {
8668
+ return profile.revolve(plan.segments ?? 0, plan.degrees);
8669
+ } finally {
8670
+ disposeWasmObject(profile);
8671
+ }
8672
+ }
7884
8673
  case "loft":
7885
8674
  return lowerShapeLoftCompilePlan(plan, wasm);
7886
8675
  case "sweep":
@@ -7915,6 +8704,10 @@ function lowerShapeCompilePlanToManifold(plan, wasm) {
7915
8704
  }
7916
8705
  case "fromSlices":
7917
8706
  return lowerFromSlicesToManifold(plan, wasm);
8707
+ case "nurbsSurface":
8708
+ return lowerNurbsSurfaceToManifold(plan, wasm);
8709
+ case "importedStep":
8710
+ throw new Error(`importStep("${plan.filePath}") requires the OCCT backend. Add setActiveBackend('occt') at the top of your script.`);
7918
8711
  default:
7919
8712
  assertExhaustive(plan);
7920
8713
  }
@@ -7941,17 +8734,27 @@ function lowerSdfToManifold(evalFn, bounds, edgeLength2, wasm) {
7941
8734
  vertProperties: vertProps6,
7942
8735
  triVerts
7943
8736
  });
7944
- return new wasm.Manifold(wasmMesh);
8737
+ try {
8738
+ return new wasm.Manifold(wasmMesh);
8739
+ } finally {
8740
+ disposeWasmObject(wasmMesh);
8741
+ }
7945
8742
  }
7946
8743
  function simplifySdfMesh(triVerts, vertProperties, edgeLength2, wasm) {
7947
8744
  const maxError = edgeLength2 * 0.15;
7948
8745
  for (const ratio of [0.5, 0.75]) {
7949
8746
  let simplified = simplifyMesh(triVerts, vertProperties, ratio, maxError);
7950
8747
  simplified = filterDegenerateTriangles(simplified);
8748
+ let mesh = null;
8749
+ let manifold = null;
7951
8750
  try {
7952
- new wasm.Manifold(new wasm.Mesh({ numProp: 3, vertProperties, triVerts: simplified }));
8751
+ mesh = new wasm.Mesh({ numProp: 3, vertProperties, triVerts: simplified });
8752
+ manifold = new wasm.Manifold(mesh);
7953
8753
  return simplified;
7954
8754
  } catch {
8755
+ } finally {
8756
+ disposeWasmObject(manifold);
8757
+ disposeWasmObject(mesh);
7955
8758
  }
7956
8759
  }
7957
8760
  return triVerts;
@@ -8011,6 +8814,58 @@ function projectVerticesToSurfaceWithNormals(vertProperties, sdfFn, out6) {
8011
8814
  }
8012
8815
  }
8013
8816
  }
8817
+ function lowerNurbsSurfaceToManifold(plan, wasm) {
8818
+ const surface = new NurbsSurface(plan.controlGrid, {
8819
+ degreeU: plan.degreeU,
8820
+ degreeV: plan.degreeV,
8821
+ weights: plan.weightsGrid,
8822
+ knotsU: plan.knotsU,
8823
+ knotsV: plan.knotsV
8824
+ });
8825
+ const res = plan.resolution;
8826
+ const { positions, normals, indices } = surface.tessellate(res, res);
8827
+ const thickness = plan.thickness;
8828
+ const numVerts = positions.length;
8829
+ const allPositions = [];
8830
+ for (const [x, y, z] of positions) allPositions.push(x, y, z);
8831
+ for (let i = 0; i < numVerts; i++) {
8832
+ const [x, y, z] = positions[i];
8833
+ const [nx, ny, nz] = normals[i];
8834
+ allPositions.push(x - nx * thickness, y - ny * thickness, z - nz * thickness);
8835
+ }
8836
+ const allIndices = [];
8837
+ for (const idx of indices) allIndices.push(idx);
8838
+ for (let i = 0; i < indices.length; i += 3) {
8839
+ allIndices.push(indices[i] + numVerts, indices[i + 2] + numVerts, indices[i + 1] + numVerts);
8840
+ }
8841
+ const resU = res, resV = res;
8842
+ for (let i = 0; i < resU; i++) {
8843
+ const a = i * (resV + 1);
8844
+ const b = (i + 1) * (resV + 1);
8845
+ allIndices.push(a, a + numVerts, b, b, a + numVerts, b + numVerts);
8846
+ const c = a + resV;
8847
+ const d = b + resV;
8848
+ allIndices.push(c, d, c + numVerts, d, d + numVerts, c + numVerts);
8849
+ }
8850
+ for (let j = 0; j < resV; j++) {
8851
+ const a = j;
8852
+ const b = j + 1;
8853
+ allIndices.push(a, b, a + numVerts, b, b + numVerts, a + numVerts);
8854
+ const c = resU * (resV + 1) + j;
8855
+ const d = c + 1;
8856
+ allIndices.push(c, c + numVerts, d, d, c + numVerts, d + numVerts);
8857
+ }
8858
+ const mesh = new wasm.Mesh({
8859
+ numProp: 3,
8860
+ vertProperties: new Float32Array(allPositions),
8861
+ triVerts: new Uint32Array(allIndices)
8862
+ });
8863
+ try {
8864
+ return new wasm.Manifold(mesh);
8865
+ } finally {
8866
+ disposeWasmObject(mesh);
8867
+ }
8868
+ }
8014
8869
  function lowerImportedMeshToManifold(fileData, format, filePath, wasm) {
8015
8870
  const parsed = parseMeshFile(fileData, format);
8016
8871
  if (parsed.triVerts.length === 0) {
@@ -8029,6 +8884,8 @@ function lowerImportedMeshToManifold(fileData, format, filePath, wasm) {
8029
8884
  throw new Error(
8030
8885
  `importMesh("${filePath}"): Manifold rejected the mesh — it may be non-manifold (non-watertight, self-intersecting, or degenerate). Original error: ${e instanceof Error ? e.message : String(e)}`
8031
8886
  );
8887
+ } finally {
8888
+ disposeWasmObject(wasmMesh);
8032
8889
  }
8033
8890
  }
8034
8891
  function lowerShapeCompilePlanToShapeBackend(plan, wasm) {
@@ -8196,6 +9053,11 @@ function getOCCT() {
8196
9053
  if (!_occt) throw new Error("OCCT not initialized — call initOCCT() first");
8197
9054
  return _occt;
8198
9055
  }
9056
+ function getOcctHeapBytes() {
9057
+ if (!_occt) return null;
9058
+ const heap = _occt.HEAPU8;
9059
+ return heap ? heap.buffer.byteLength : null;
9060
+ }
8199
9061
  const DEFAULT_LINEAR_DEFLECTION = 0.1;
8200
9062
  const DEFAULT_ANGULAR_DEFLECTION = 0.5;
8201
9063
  const EDGE_CURVE_ANGULAR_DEFLECTION = 5 * Math.PI / 180;
@@ -8409,22 +9271,35 @@ function findEdgeByMidpoint(oc, shape, midpoint2) {
8409
9271
  }
8410
9272
  _c = SHAPE_BACKEND_MARKER;
8411
9273
  const _OCCTShapeBackend = class _OCCTShapeBackend {
8412
- constructor(_shape) {
9274
+ constructor(shapeOrResource) {
8413
9275
  __publicField(this, _c, true);
8414
- this._shape = _shape;
9276
+ __publicField(this, "resource");
9277
+ __publicField(this, "released", false);
9278
+ this.resource = shapeOrResource && typeof shapeOrResource === "object" && "refCount" in shapeOrResource && "shape" in shapeOrResource ? shapeOrResource : {
9279
+ shape: shapeOrResource,
9280
+ refCount: 1,
9281
+ disposed: false
9282
+ };
9283
+ }
9284
+ getLiveShape(apiName = "OCCTShapeBackend") {
9285
+ if (this.released || this.resource.disposed) {
9286
+ throw new Error(`${apiName}: OCCT backend was already disposed`);
9287
+ }
9288
+ return this.resource.shape;
8415
9289
  }
8416
9290
  /** Access the underlying TopoDS_Shape. */
8417
9291
  get shape() {
8418
- return this._shape;
9292
+ return this.getLiveShape("shape");
8419
9293
  }
8420
9294
  clone() {
8421
- return new _OCCTShapeBackend(this._shape);
9295
+ this.resource.refCount += 1;
9296
+ return new _OCCTShapeBackend(this.resource);
8422
9297
  }
8423
9298
  translate(x, y, z) {
8424
9299
  const oc = getOCCT();
8425
9300
  const trsf = new oc.gp_Trsf_1();
8426
9301
  trsf.SetTranslation_1(new oc.gp_Vec_4(x, y, z));
8427
- const transformed = new oc.BRepBuilderAPI_Transform_2(this._shape, trsf, true);
9302
+ const transformed = new oc.BRepBuilderAPI_Transform_2(this.getLiveShape("translate()"), trsf, true);
8428
9303
  return new _OCCTShapeBackend(transformed.Shape());
8429
9304
  }
8430
9305
  rotate(x, y, z) {
@@ -8432,21 +9307,21 @@ const _OCCTShapeBackend = class _OCCTShapeBackend {
8432
9307
  }
8433
9308
  transform(m) {
8434
9309
  const oc = getOCCT();
8435
- return new _OCCTShapeBackend(applyTransform(oc, this._shape, m));
9310
+ return new _OCCTShapeBackend(applyTransform(oc, this.getLiveShape("transform()"), m));
8436
9311
  }
8437
9312
  scale(v) {
8438
9313
  const oc = getOCCT();
8439
9314
  if (typeof v === "number") {
8440
9315
  const trsf = new oc.gp_Trsf_1();
8441
9316
  trsf.SetScaleFactor(v);
8442
- const transformed2 = new oc.BRepBuilderAPI_Transform_2(this._shape, trsf, true);
9317
+ const transformed2 = new oc.BRepBuilderAPI_Transform_2(this.getLiveShape("scale()"), trsf, true);
8443
9318
  return new _OCCTShapeBackend(transformed2.Shape());
8444
9319
  }
8445
9320
  const gtrsf = new oc.gp_GTrsf_1();
8446
9321
  gtrsf.SetValue(1, 1, v[0]);
8447
9322
  gtrsf.SetValue(2, 2, v[1]);
8448
9323
  gtrsf.SetValue(3, 3, v[2]);
8449
- const transformed = new oc.BRepBuilderAPI_GTransform_2(this._shape, gtrsf, true);
9324
+ const transformed = new oc.BRepBuilderAPI_GTransform_2(this.getLiveShape("scale()"), gtrsf, true);
8450
9325
  return new _OCCTShapeBackend(transformed.Shape());
8451
9326
  }
8452
9327
  mirror(normal) {
@@ -8454,15 +9329,15 @@ const _OCCTShapeBackend = class _OCCTShapeBackend {
8454
9329
  const ax2 = new oc.gp_Ax2_3(new oc.gp_Pnt_3(0, 0, 0), new oc.gp_Dir_4(normal[0], normal[1], normal[2]));
8455
9330
  const trsf = new oc.gp_Trsf_1();
8456
9331
  trsf.SetMirror_3(ax2);
8457
- const transformed = new oc.BRepBuilderAPI_Transform_2(this._shape, trsf, true);
9332
+ const transformed = new oc.BRepBuilderAPI_Transform_2(this.getLiveShape("mirror()"), trsf, true);
8458
9333
  return new _OCCTShapeBackend(transformed.Shape());
8459
9334
  }
8460
9335
  split(other) {
8461
9336
  const oc = getOCCT();
8462
9337
  const otherShape = requireOCCTShape(other, "split()");
8463
- const inside = new oc.BRepAlgoAPI_Common_3(this._shape, otherShape, new oc.Message_ProgressRange_1());
9338
+ const inside = new oc.BRepAlgoAPI_Common_3(this.getLiveShape("split()"), otherShape, new oc.Message_ProgressRange_1());
8464
9339
  inside.Build(new oc.Message_ProgressRange_1());
8465
- const outside = new oc.BRepAlgoAPI_Cut_3(this._shape, otherShape, new oc.Message_ProgressRange_1());
9340
+ const outside = new oc.BRepAlgoAPI_Cut_3(this.getLiveShape("split()"), otherShape, new oc.Message_ProgressRange_1());
8466
9341
  outside.Build(new oc.Message_ProgressRange_1());
8467
9342
  return [new _OCCTShapeBackend(inside.Shape()), new _OCCTShapeBackend(outside.Shape())];
8468
9343
  }
@@ -8474,9 +9349,9 @@ const _OCCTShapeBackend = class _OCCTShapeBackend {
8474
9349
  new oc.BRepBuilderAPI_MakeFace_9(pln, -1e6, 1e6, -1e6, 1e6).Face(),
8475
9350
  new oc.gp_Pnt_3(normal[0] * (originOffset + 1), normal[1] * (originOffset + 1), normal[2] * (originOffset + 1))
8476
9351
  );
8477
- const inside = new oc.BRepAlgoAPI_Common_3(this._shape, halfSpace.Solid(), new oc.Message_ProgressRange_1());
9352
+ const inside = new oc.BRepAlgoAPI_Common_3(this.getLiveShape("splitByPlane()"), halfSpace.Solid(), new oc.Message_ProgressRange_1());
8478
9353
  inside.Build(new oc.Message_ProgressRange_1());
8479
- const outside = new oc.BRepAlgoAPI_Cut_3(this._shape, halfSpace.Solid(), new oc.Message_ProgressRange_1());
9354
+ const outside = new oc.BRepAlgoAPI_Cut_3(this.getLiveShape("splitByPlane()"), halfSpace.Solid(), new oc.Message_ProgressRange_1());
8480
9355
  outside.Build(new oc.Message_ProgressRange_1());
8481
9356
  return [new _OCCTShapeBackend(inside.Shape()), new _OCCTShapeBackend(outside.Shape())];
8482
9357
  }
@@ -8485,28 +9360,32 @@ const _OCCTShapeBackend = class _OCCTShapeBackend {
8485
9360
  return inside;
8486
9361
  }
8487
9362
  boundingBox() {
8488
- return extractBoundingBox(getOCCT(), this._shape);
9363
+ return extractBoundingBox(getOCCT(), this.getLiveShape("boundingBox()"));
8489
9364
  }
8490
9365
  volume() {
8491
9366
  const oc = getOCCT();
8492
9367
  const props = new oc.GProp_GProps_1();
8493
- oc.BRepGProp.VolumeProperties_1(this._shape, props, false, false, false);
9368
+ oc.BRepGProp.VolumeProperties_1(this.getLiveShape("volume()"), props, false, false, false);
8494
9369
  return props.Mass();
8495
9370
  }
8496
9371
  surfaceArea() {
8497
9372
  const oc = getOCCT();
8498
9373
  const props = new oc.GProp_GProps_1();
8499
- oc.BRepGProp.SurfaceProperties_1(this._shape, props, false, false);
9374
+ oc.BRepGProp.SurfaceProperties_1(this.getLiveShape("surfaceArea()"), props, false, false);
8500
9375
  return props.Mass();
8501
9376
  }
8502
9377
  isEmpty() {
8503
- getOCCT();
8504
- return this._shape.IsNull() || this._shape.NbChildren() === 0;
9378
+ const shape = this.getLiveShape("isEmpty()");
9379
+ return shape.IsNull() || shape.NbChildren() === 0;
8505
9380
  }
8506
9381
  numBodies() {
8507
9382
  const oc = getOCCT();
8508
9383
  let count = 0;
8509
- const expl = new oc.TopExp_Explorer_2(this._shape, oc.TopAbs_ShapeEnum.TopAbs_SOLID, oc.TopAbs_ShapeEnum.TopAbs_SHAPE);
9384
+ const expl = new oc.TopExp_Explorer_2(
9385
+ this.getLiveShape("numBodies()"),
9386
+ oc.TopAbs_ShapeEnum.TopAbs_SOLID,
9387
+ oc.TopAbs_ShapeEnum.TopAbs_SHAPE
9388
+ );
8510
9389
  while (expl.More()) {
8511
9390
  count++;
8512
9391
  expl.Next();
@@ -8517,14 +9396,14 @@ const _OCCTShapeBackend = class _OCCTShapeBackend {
8517
9396
  return this.getMesh().numTri;
8518
9397
  }
8519
9398
  getMesh() {
8520
- return extractMeshFromShape(getOCCT(), this._shape).mesh;
9399
+ return extractMeshFromShape(getOCCT(), this.getLiveShape("getMesh()")).mesh;
8521
9400
  }
8522
9401
  /**
8523
9402
  * Extract mesh with per-vertex normals from the B-rep surface.
8524
9403
  * Returns both the Manifold-compatible mesh and smooth normals.
8525
9404
  */
8526
9405
  getMeshWithNormals() {
8527
- return extractMeshFromShape(getOCCT(), this._shape);
9406
+ return extractMeshFromShape(getOCCT(), this.getLiveShape("getMeshWithNormals()"));
8528
9407
  }
8529
9408
  /**
8530
9409
  * Extract smooth edge curves from the B-rep topology.
@@ -8532,7 +9411,7 @@ const _OCCTShapeBackend = class _OCCTShapeBackend {
8532
9411
  * as geometryEdgePositions (pairs of xyz points, 6 floats per segment).
8533
9412
  */
8534
9413
  getEdgeCurves() {
8535
- return extractEdgeCurvesFromShape(getOCCT(), this._shape);
9414
+ return extractEdgeCurvesFromShape(getOCCT(), this.getLiveShape("getEdgeCurves()"));
8536
9415
  }
8537
9416
  slice(_offset) {
8538
9417
  throw new Error("slice() not yet implemented for OCCT backend");
@@ -8542,9 +9421,9 @@ const _OCCTShapeBackend = class _OCCTShapeBackend {
8542
9421
  }
8543
9422
  filletEdgeByMidpoint(edge, radius) {
8544
9423
  const oc = getOCCT();
8545
- const matchedEdge = findEdgeByMidpoint(oc, this._shape, edge.midpoint);
9424
+ const matchedEdge = findEdgeByMidpoint(oc, this.getLiveShape("filletEdgeByMidpoint()"), edge.midpoint);
8546
9425
  if (!matchedEdge) throw new Error("OCCT filletEdgeByMidpoint: could not find matching edge");
8547
- const mkFillet = new oc.BRepFilletAPI_MakeFillet(this._shape, oc.ChFi3d_FilletShape.ChFi3d_Rational);
9426
+ const mkFillet = new oc.BRepFilletAPI_MakeFillet(this.getLiveShape("filletEdgeByMidpoint()"), oc.ChFi3d_FilletShape.ChFi3d_Rational);
8548
9427
  mkFillet.Add_2(radius, matchedEdge);
8549
9428
  mkFillet.Build(new oc.Message_ProgressRange_1());
8550
9429
  if (!mkFillet.IsDone()) {
@@ -8554,14 +9433,24 @@ const _OCCTShapeBackend = class _OCCTShapeBackend {
8554
9433
  }
8555
9434
  chamferEdgeByMidpoint(edge, size) {
8556
9435
  const oc = getOCCT();
8557
- const matchedEdge = findEdgeByMidpoint(oc, this._shape, edge.midpoint);
9436
+ const matchedEdge = findEdgeByMidpoint(oc, this.getLiveShape("chamferEdgeByMidpoint()"), edge.midpoint);
8558
9437
  if (!matchedEdge) throw new Error("OCCT chamferEdgeByMidpoint: could not find matching edge");
8559
- const mkChamfer = new oc.BRepFilletAPI_MakeChamfer(this._shape);
9438
+ const mkChamfer = new oc.BRepFilletAPI_MakeChamfer(this.getLiveShape("chamferEdgeByMidpoint()"));
8560
9439
  mkChamfer.Add_2(size, matchedEdge);
8561
9440
  mkChamfer.Build(new oc.Message_ProgressRange_1());
8562
9441
  if (!mkChamfer.IsDone()) throw new Error("OCCT chamfer operation failed");
8563
9442
  return new _OCCTShapeBackend(mkChamfer.Shape());
8564
9443
  }
9444
+ dispose() {
9445
+ var _a3, _b3;
9446
+ if (this.released) return;
9447
+ this.released = true;
9448
+ this.resource.refCount = Math.max(0, this.resource.refCount - 1);
9449
+ if (this.resource.refCount === 0 && !this.resource.disposed) {
9450
+ this.resource.disposed = true;
9451
+ (_b3 = (_a3 = this.resource.shape).delete) == null ? void 0 : _b3.call(_a3);
9452
+ }
9453
+ }
8565
9454
  };
8566
9455
  let OCCTShapeBackend = _OCCTShapeBackend;
8567
9456
  function wrapOCCTShapeBackend(shape) {
@@ -8893,6 +9782,70 @@ function buildWireFromPoints(oc, points) {
8893
9782
  }
8894
9783
  return mkWire.Wire();
8895
9784
  }
9785
+ function buildFaceFromProfileEdges(oc, plan) {
9786
+ try {
9787
+ const mkWire = new oc.BRepBuilderAPI_MakeWire_1();
9788
+ for (const edge of plan.edges) {
9789
+ switch (edge.kind) {
9790
+ case "line": {
9791
+ if (Math.abs(edge.x1 - edge.x2) < 1e-10 && Math.abs(edge.y1 - edge.y2) < 1e-10) continue;
9792
+ const e = new oc.BRepBuilderAPI_MakeEdge_3(
9793
+ new oc.gp_Pnt_3(edge.x1, edge.y1, 0),
9794
+ new oc.gp_Pnt_3(edge.x2, edge.y2, 0)
9795
+ ).Edge();
9796
+ mkWire.Add_1(e);
9797
+ break;
9798
+ }
9799
+ case "arc": {
9800
+ const dx1 = edge.x1 - edge.cx, dy1 = edge.y1 - edge.cy;
9801
+ const r = Math.hypot(dx1, dy1);
9802
+ const startAngle = Math.atan2(dy1, dx1);
9803
+ const dx2 = edge.x2 - edge.cx, dy2 = edge.y2 - edge.cy;
9804
+ let endAngle = Math.atan2(dy2, dx2);
9805
+ if (edge.clockwise) {
9806
+ if (endAngle >= startAngle) endAngle -= 2 * Math.PI;
9807
+ } else {
9808
+ if (endAngle <= startAngle) endAngle += 2 * Math.PI;
9809
+ }
9810
+ const axis = new oc.gp_Ax2_3(new oc.gp_Pnt_3(edge.cx, edge.cy, 0), new oc.gp_Dir_4(0, 0, 1));
9811
+ const circ = new oc.gp_Circ_2(axis, r);
9812
+ const e = new oc.BRepBuilderAPI_MakeEdge_9(circ, startAngle, endAngle).Edge();
9813
+ mkWire.Add_1(e);
9814
+ break;
9815
+ }
9816
+ case "bspline": {
9817
+ const n = edge.controlPoints.length;
9818
+ const poles = new oc.TColgp_Array1OfPnt_2(1, n);
9819
+ for (let i = 0; i < n; i++) {
9820
+ poles.SetValue(i + 1, new oc.gp_Pnt_3(edge.controlPoints[i][0], edge.controlPoints[i][1], 0));
9821
+ }
9822
+ const wts = new oc.TColStd_Array1OfReal_2(1, n);
9823
+ for (let i = 0; i < n; i++) {
9824
+ wts.SetValue(i + 1, edge.weights[i]);
9825
+ }
9826
+ const { knots, mults } = flatKnotsToKnotsMults(edge.knots);
9827
+ const knotsArr = new oc.TColStd_Array1OfReal_2(1, knots.length);
9828
+ for (let i = 0; i < knots.length; i++) {
9829
+ knotsArr.SetValue(i + 1, knots[i]);
9830
+ }
9831
+ const multsArr = new oc.TColStd_Array1OfInteger_2(1, mults.length);
9832
+ for (let i = 0; i < mults.length; i++) {
9833
+ multsArr.SetValue(i + 1, mults[i]);
9834
+ }
9835
+ const bspline = new oc.Geom_BSplineCurve_1(poles, wts, knotsArr, multsArr, edge.degree, false);
9836
+ const curveHandle = new oc.Handle_Geom_Curve_2(bspline);
9837
+ const e = new oc.BRepBuilderAPI_MakeEdge_24(curveHandle).Edge();
9838
+ mkWire.Add_1(e);
9839
+ break;
9840
+ }
9841
+ }
9842
+ }
9843
+ return buildFaceFromWire(oc, mkWire.Wire());
9844
+ } catch {
9845
+ const wire = buildWireFromPoints(oc, plan.points);
9846
+ return buildFaceFromWire(oc, wire);
9847
+ }
9848
+ }
8896
9849
  function buildFaceFromWire(oc, wire) {
8897
9850
  const face = new oc.BRepBuilderAPI_MakeFace_15(wire, true);
8898
9851
  return face.Face();
@@ -9056,6 +10009,10 @@ function lowerProfileToFace(oc, plan) {
9056
10009
  }
9057
10010
  case "project":
9058
10011
  throw new OCCTUnsupportedError("profile project");
10012
+ case "pathProfile": {
10013
+ face = buildFaceFromProfileEdges(oc, plan);
10014
+ break;
10015
+ }
9059
10016
  default:
9060
10017
  assertExhaustive(plan);
9061
10018
  }
@@ -9524,6 +10481,36 @@ function _lowerShapeCompilePlanToOCCTInner(plan, oc) {
9524
10481
  throw new Error("SDF shapes require the Manifold backend. Add setActiveBackend('manifold') at the top of your script.");
9525
10482
  case "fromSlices":
9526
10483
  return lowerFromSlicesPlan(oc, plan);
10484
+ case "nurbsSurface":
10485
+ return lowerNurbsSurfacePlan();
10486
+ case "importedStep":
10487
+ return lowerImportedStepPlan(oc, plan);
10488
+ }
10489
+ }
10490
+ function lowerNurbsSurfacePlan(_oc, _plan) {
10491
+ throw new Error("NURBS surfaces are not yet supported by the OCCT backend. Use setActiveBackend('manifold').");
10492
+ }
10493
+ function lowerImportedStepPlan(oc, plan) {
10494
+ const virtualPath = `/tmp/forgecad-step-import-${Date.now()}.step`;
10495
+ const bytes = new Uint8Array(plan.fileData);
10496
+ oc.FS.writeFile(virtualPath, bytes);
10497
+ try {
10498
+ const reader = new oc.STEPControl_Reader_1();
10499
+ const readStatus = reader.ReadFile(virtualPath);
10500
+ if (readStatus !== oc.IFSelect_ReturnStatus.IFSelect_RetDone) {
10501
+ throw new Error(`importStep("${plan.filePath}"): Failed to read STEP file (status ${readStatus}).`);
10502
+ }
10503
+ reader.TransferRoots(new oc.Message_ProgressRange_1());
10504
+ const shape = reader.OneShape();
10505
+ if (!shape || shape.IsNull()) {
10506
+ throw new Error(`importStep("${plan.filePath}"): STEP file produced no geometry.`);
10507
+ }
10508
+ return shape;
10509
+ } finally {
10510
+ try {
10511
+ oc.FS.unlink(virtualPath);
10512
+ } catch {
10513
+ }
9527
10514
  }
9528
10515
  }
9529
10516
  function extractOuterWire(oc, face) {
@@ -9647,6 +10634,7 @@ function buildSpineWireFromPlan(oc, path2, pathSamples) {
9647
10634
  case "catmull-rom":
9648
10635
  case "hermite":
9649
10636
  case "quintic-hermite":
10637
+ case "nurbs":
9650
10638
  return buildPolylineSpineWire(oc, sweepPathToPolyline(path2, pathSamples ?? 48));
9651
10639
  }
9652
10640
  }
@@ -10028,6 +11016,8 @@ function summarizeProfile(profile) {
10028
11016
  return "offset profile";
10029
11017
  case "project":
10030
11018
  return "projected profile";
11019
+ case "pathProfile":
11020
+ return `path profile (${profile.edges.length} edges)`;
10031
11021
  default:
10032
11022
  assertExhaustive(profile);
10033
11023
  }
@@ -11650,6 +12640,8 @@ function resolveShapeFaceTableInternal(plan, owner) {
11650
12640
  case "importedMesh":
11651
12641
  case "sdf":
11652
12642
  case "fromSlices":
12643
+ case "nurbsSurface":
12644
+ case "importedStep":
11653
12645
  return emptyFaceTable();
11654
12646
  default:
11655
12647
  assertExhaustive(plan);
@@ -12153,7 +13145,7 @@ function transformPortMap(ports, matrix) {
12153
13145
  }
12154
13146
  return out;
12155
13147
  }
12156
- function computeConnectFrame(childBase, childPort, parentPort, flip, childAlign = "middle", parentAlign = "middle") {
13148
+ function computeConnectFrame(childBase, childPort, parentPort, _flip, childAlign = "middle", parentAlign = "middle") {
12157
13149
  const childAlignPt = resolvePortAlignPoint(childPort, childAlign);
12158
13150
  const parentAlignPt = resolvePortAlignPoint(parentPort, parentAlign);
12159
13151
  const cI = childBase.point(childAlignPt);
@@ -12161,7 +13153,9 @@ function computeConnectFrame(childBase, childPort, parentPort, flip, childAlign
12161
13153
  const cUp = normalize3$2(childBase.vector(childPort.up));
12162
13154
  const cRight = normalize3$2(cross3$2(cAxis, cUp));
12163
13155
  const pOrigin = parentAlignPt;
12164
- const pAxis = flip ? negate3$1(parentPort.axis) : [...parentPort.axis];
13156
+ const jointKind = childPort.kind ?? parentPort.kind;
13157
+ const faceToFace = jointKind !== "prismatic";
13158
+ const pAxis = faceToFace ? negate3$1(parentPort.axis) : [...parentPort.axis];
12165
13159
  const pUp = [...parentPort.up];
12166
13160
  const pRight = normalize3$2(cross3$2(pAxis, pUp));
12167
13161
  const r00 = pRight[0] * cRight[0] + pUp[0] * cUp[0] + pAxis[0] * cAxis[0];
@@ -14001,6 +14995,8 @@ function rootTopologyRewritePropagation(plan) {
14001
14995
  case "importedMesh":
14002
14996
  case "sdf":
14003
14997
  case "fromSlices":
14998
+ case "nurbsSurface":
14999
+ case "importedStep":
14004
15000
  return null;
14005
15001
  default:
14006
15002
  assertExhaustive(plan);
@@ -14424,6 +15420,8 @@ function rootPlanPropagation(plan) {
14424
15420
  case "importedMesh":
14425
15421
  case "sdf":
14426
15422
  case "fromSlices":
15423
+ case "nurbsSurface":
15424
+ case "importedStep":
14427
15425
  return void 0;
14428
15426
  default:
14429
15427
  assertExhaustive(plan);
@@ -16268,46 +17266,82 @@ function injectVoronoiSurfaceChild(children) {
16268
17266
  return child;
16269
17267
  });
16270
17268
  }
16271
- var sdf = /* @__PURE__ */ Object.freeze({
16272
- __proto__: null,
16273
- SdfShape,
16274
- SurfacePattern,
16275
- basketWeave,
16276
- bend,
16277
- blend,
17269
+ const sdf = {
17270
+ /** Create an SDF sphere centered at the origin. */
17271
+ sphere: sphere$1,
17272
+ /** Create an SDF box centered at the origin with given full dimensions (not half-extents). */
16278
17273
  box: box$1,
16279
- brick,
17274
+ /** Create an SDF cylinder centered at the origin, axis along Z. */
17275
+ cylinder: cylinder$1,
17276
+ /** Create an SDF torus centered at the origin, lying in the XY plane. */
17277
+ torus: torus$1,
17278
+ /** Create an SDF capsule centered at the origin, axis along Z. */
16280
17279
  capsule,
17280
+ /** Create an SDF cone with base at z=0 and tip at z=height. */
16281
17281
  cone,
16282
- cylinder: cylinder$1,
16283
- diamond,
16284
- fromFunction,
17282
+ /** Smooth union — blends shapes together with a smooth transition radius. */
17283
+ smoothUnion,
17284
+ /** Smooth difference — smoothly subtracts b from a. */
17285
+ smoothDifference,
17286
+ /** Smooth intersection — smoothly intersects a and b. */
17287
+ smoothIntersection,
17288
+ /** Morph between two SDF shapes. t=0 → a, t=1 → b. */
17289
+ morph,
17290
+ /**
17291
+ * Spatially blend between two SDF patterns.
17292
+ * The blend function receives (x, y, z) and returns 0..1:
17293
+ * 0 = fully pattern `a`, 1 = fully pattern `b`.
17294
+ */
17295
+ blend,
17296
+ /** Gyroid TPMS lattice — the most common lattice for additive manufacturing. */
16285
17297
  gyroid,
16286
- honeycomb,
16287
- knurl,
17298
+ /** Schwarz-P TPMS lattice — isotropic pore structure. */
17299
+ schwarzP,
17300
+ /** Diamond TPMS lattice — stiffest TPMS structure. */
17301
+ diamond,
17302
+ /** Lidinoid TPMS lattice — visually distinct from gyroid, popular in research and art. */
16288
17303
  lidinoid,
16289
- morph,
17304
+ /** 3D Simplex noise field — produces organic, natural-looking displacements. */
16290
17305
  noise,
17306
+ /** 3D Voronoi pattern — organic cellular structures like bone, coral, or soap bubbles. */
17307
+ voronoi,
17308
+ /** Honeycomb (hexagonal) lattice pattern. Intersect with your shape to apply. */
17309
+ honeycomb,
17310
+ /** Sinusoidal wave ridges — parallel ridges along an axis. */
17311
+ waves,
17312
+ /** Knurl pattern — crossed helical grooves for grips and handles. */
17313
+ knurl,
17314
+ /** Perforated plate pattern — regular array of cylindrical holes. */
16291
17315
  perforated,
16292
- repeat,
17316
+ /** Fish/dragon scale pattern — overlapping circular scales in hex-packed rows. */
16293
17317
  scales,
16294
- schwarzP,
16295
- smoothDifference,
16296
- smoothIntersection,
16297
- smoothUnion,
16298
- sphere: sphere$1,
16299
- torus: torus$1,
17318
+ /** Brick/stone wall pattern — running bond with mortar grooves. */
17319
+ brick,
17320
+ /** Grid lattice pattern — two families of infinite slabs crossing at 90°. */
17321
+ weave,
17322
+ /** Basket weave surface pattern — threads with over-under crossings in UV space. Returns a SurfacePattern for use with `.surfaceDisplace()`. */
17323
+ basketWeave,
17324
+ /** Twist an SDF shape around the Z axis. */
16300
17325
  twist,
16301
- voronoi,
16302
- waves,
16303
- weave
16304
- });
17326
+ /** Bend an SDF shape around the Z axis. */
17327
+ bend,
17328
+ /** Repeat an SDF shape in space. */
17329
+ repeat,
17330
+ /** A 2D surface pattern — a heightmap function for use with `.surfaceDisplace()`. */
17331
+ SurfacePattern,
17332
+ /** Create an SDF shape from an arbitrary distance function. You must provide bounds since the function is opaque. */
17333
+ fromFunction
17334
+ };
16305
17335
  async function initKernel() {
16306
17336
  initOCCT().catch((e) => console.warn("[kernel] OCCT background init failed:", e));
16307
17337
  const [manifoldModule] = await Promise.all([initManifoldWasm(), initMeshoptimizer()]);
16308
17338
  return manifoldModule;
16309
17339
  }
16310
17340
  let _activeBackend = "manifold";
17341
+ let _runtimeWarn = (msg) => console.warn(msg);
17342
+ function setRuntimeWarnSink(fn) {
17343
+ _runtimeWarn = fn;
17344
+ }
16311
17345
  async function activateBackend(backend) {
16312
17346
  _activeBackend = backend;
16313
17347
  if (backend === "occt") {
@@ -17476,6 +18510,33 @@ class Shape {
17476
18510
  static _unwrap(value) {
17477
18511
  return unwrapShapeLike(value);
17478
18512
  }
18513
+ /**
18514
+ * Warn if a boolean operation had no geometric effect.
18515
+ * Compares volumes before and after; if they match within tolerance, the operation was a no-op.
18516
+ */
18517
+ static _checkBooleanNoOp(op, base, result, others) {
18518
+ try {
18519
+ if (op === "intersection") {
18520
+ if (result.isEmpty()) {
18521
+ _runtimeWarn(
18522
+ `intersection() produced an empty shape — the operands may not overlap.`
18523
+ );
18524
+ }
18525
+ return;
18526
+ }
18527
+ if (op === "difference") {
18528
+ const volBefore = base.volume();
18529
+ const volAfter = result.volume();
18530
+ const tol = Math.max(volBefore * 1e-4, 1e-3);
18531
+ if (Math.abs(volBefore - volAfter) < tol) {
18532
+ _runtimeWarn(
18533
+ `subtract() had no effect — the tool may not overlap the base shape. Base vol=${volBefore.toFixed(1)}mm³, result vol=${volAfter.toFixed(1)}mm³`
18534
+ );
18535
+ }
18536
+ }
18537
+ } catch {
18538
+ }
18539
+ }
17479
18540
  /** Union this shape with others (additive boolean). Method form of union(). */
17480
18541
  add(...others) {
17481
18542
  const shapes = [
@@ -17513,7 +18574,7 @@ class Shape {
17513
18574
  "boolean:difference",
17514
18575
  (owner) => buildBooleanTopologyRewritePropagation("difference", owner, operandPlans)
17515
18576
  );
17516
- return setShapeCompilePlanInternal(
18577
+ const resultShape = setShapeCompilePlanInternal(
17517
18578
  setShapeGeometryInfoInternal(
17518
18579
  withBaseDimensions(this, buildShapeFromCompilePlan(nextPlan, this.colorHex)),
17519
18580
  mergeGeometryInfos(
@@ -17524,6 +18585,8 @@ class Shape {
17524
18585
  ),
17525
18586
  nextPlan
17526
18587
  );
18588
+ Shape._checkBooleanNoOp("difference", this, resultShape, shapes.slice(1));
18589
+ return resultShape;
17527
18590
  }
17528
18591
  /** Keep only the overlap with other shapes. Method form of intersection(). */
17529
18592
  intersect(...others) {
@@ -17542,7 +18605,7 @@ class Shape {
17542
18605
  "boolean:intersection",
17543
18606
  (owner) => buildBooleanTopologyRewritePropagation("intersection", owner, operandPlans)
17544
18607
  );
17545
- return setShapeCompilePlanInternal(
18608
+ const resultShape = setShapeCompilePlanInternal(
17546
18609
  setShapeGeometryInfoInternal(
17547
18610
  withMergedDimensions(shapes, buildShapeFromCompilePlan(nextPlan, this.colorHex)),
17548
18611
  mergeGeometryInfos(
@@ -17553,6 +18616,8 @@ class Shape {
17553
18616
  ),
17554
18617
  nextPlan
17555
18618
  );
18619
+ Shape._checkBooleanNoOp("intersection", this, resultShape, shapes.slice(1));
18620
+ return resultShape;
17556
18621
  }
17557
18622
  /** Alias for add() — matches the free-function union() naming. */
17558
18623
  union(...others) {
@@ -18154,10 +19219,14 @@ function intersection(...inputs) {
18154
19219
  var define_process_env_default = {};
18155
19220
  let _wasm_solve = null;
18156
19221
  let _wasm_get_profile = null;
19222
+ let _solverMemory = null;
18157
19223
  let _sessionApi = null;
18158
19224
  function getSessionApi() {
18159
19225
  return _sessionApi;
18160
19226
  }
19227
+ function getSolverWasmHeapBytes() {
19228
+ return _solverMemory ? _solverMemory.buffer.byteLength : null;
19229
+ }
18161
19230
  let _initPromise = null;
18162
19231
  const MAX_EXCHANGE_HISTORY = 64;
18163
19232
  const DEBUG_STORAGE_KEY = "fc:solver-debug";
@@ -18464,9 +19533,11 @@ async function initSolverWasm() {
18464
19533
  }
18465
19534
  if (!wasmPath) throw new Error("solver_bg.wasm not found — run: npm run build:solver");
18466
19535
  const wasmBytes = readFileSync(wasmPath);
18467
- await solverModule.default(wasmBytes);
19536
+ const exports$1 = await solverModule.default(wasmBytes);
19537
+ _solverMemory = (exports$1 == null ? void 0 : exports$1.memory) ?? null;
18468
19538
  } else {
18469
- await solverModule.default();
19539
+ const exports$1 = await solverModule.default();
19540
+ _solverMemory = (exports$1 == null ? void 0 : exports$1.memory) ?? null;
18470
19541
  }
18471
19542
  performance.mark("solver:ready");
18472
19543
  performance.measure("solver:import", "solver:start", "solver:imported");
@@ -19453,7 +20524,7 @@ class MateBuilder {
19453
20524
  return this.constraints.reduce((sum, c) => sum + (CONSTRAINT_EQUATIONS[c.type] ?? 0), 0);
19454
20525
  }
19455
20526
  }
19456
- let _collected$5 = null;
20527
+ let _collected$7 = null;
19457
20528
  const isAxis = (value) => value === "x" || value === "y" || value === "z";
19458
20529
  const normalizeDirection = (value, label) => {
19459
20530
  if (value === "radial" || isAxis(value)) return value;
@@ -19512,16 +20583,16 @@ const mergeDirective = (target, patch, label) => {
19512
20583
  return out;
19513
20584
  };
19514
20585
  function resetExplodeView() {
19515
- _collected$5 = null;
20586
+ _collected$7 = null;
19516
20587
  }
19517
20588
  function getCollectedExplodeView() {
19518
- return _collected$5 ? cloneOptions(_collected$5) : null;
20589
+ return _collected$7 ? cloneOptions(_collected$7) : null;
19519
20590
  }
19520
20591
  function explodeView(options = {}) {
19521
20592
  if (!options || typeof options !== "object") {
19522
20593
  throw new Error("explodeView(options) expects an options object");
19523
20594
  }
19524
- const next = _collected$5 ? cloneOptions(_collected$5) : {};
20595
+ const next = _collected$7 ? cloneOptions(_collected$7) : {};
19525
20596
  if (options.enabled !== void 0) {
19526
20597
  if (typeof options.enabled !== "boolean") throw new Error("explodeView.enabled must be a boolean");
19527
20598
  next.enabled = options.enabled;
@@ -19569,9 +20640,9 @@ function explodeView(options = {}) {
19569
20640
  });
19570
20641
  next.byPath = byPath;
19571
20642
  }
19572
- _collected$5 = next;
20643
+ _collected$7 = next;
19573
20644
  }
19574
- let _collected$4 = null;
20645
+ let _collected$6 = null;
19575
20646
  const isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
19576
20647
  const isVec3$1 = (value) => Array.isArray(value) && value.length === 3 && isFiniteNumber(value[0]) && isFiniteNumber(value[1]) && isFiniteNumber(value[2]);
19577
20648
  const normalizeAxis = (axis) => {
@@ -19861,22 +20932,22 @@ const cloneCollected = (value) => ({
19861
20932
  defaultAnimation: value.defaultAnimation
19862
20933
  });
19863
20934
  function resetJointsView() {
19864
- _collected$4 = null;
20935
+ _collected$6 = null;
19865
20936
  }
19866
20937
  function getCollectedJointsView() {
19867
- return _collected$4 ? cloneCollected(_collected$4) : null;
20938
+ return _collected$6 ? cloneCollected(_collected$6) : null;
19868
20939
  }
19869
20940
  function saveJointsView() {
19870
- return _collected$4 ? cloneCollected(_collected$4) : null;
20941
+ return _collected$6 ? cloneCollected(_collected$6) : null;
19871
20942
  }
19872
20943
  function restoreJointsView(state) {
19873
- _collected$4 = state;
20944
+ _collected$6 = state;
19874
20945
  }
19875
20946
  function jointsView(options = {}) {
19876
20947
  if (!options || typeof options !== "object") {
19877
20948
  throw new Error("jointsView(options) expects an options object");
19878
20949
  }
19879
- const next = _collected$4 ? cloneCollected(_collected$4) : { joints: [], couplings: [], animations: [] };
20950
+ const next = _collected$6 ? cloneCollected(_collected$6) : { joints: [], couplings: [], animations: [] };
19880
20951
  if (options.enabled !== void 0) {
19881
20952
  if (typeof options.enabled !== "boolean") {
19882
20953
  throw new Error("jointsView.enabled must be a boolean");
@@ -19931,7 +21002,7 @@ function jointsView(options = {}) {
19931
21002
  if (next.defaultAnimation && !next.animations.some((animation) => animation.name === next.defaultAnimation)) {
19932
21003
  throw new Error(`jointsView defaultAnimation "${next.defaultAnimation}" does not exist in animations`);
19933
21004
  }
19934
- _collected$4 = next;
21005
+ _collected$6 = next;
19935
21006
  }
19936
21007
  function bomToCsv(rows) {
19937
21008
  const header = ["part", "qty", "material", "process", "tolerance", "notes"];
@@ -20638,8 +21709,16 @@ class Assembly {
20638
21709
  * origins (child connector lands exactly on parent connector) and derives the joint frame
20639
21710
  * and axis from the connector geometry — no manual `frame` or `axis` math needed.
20640
21711
  *
21712
+ * **Face-to-face convention:** Connectors always meet face-to-face, like a USB plug
21713
+ * meeting a socket. Each connector's axis points "outward" from its part. When two
21714
+ * connectors mate, the system brings them together so their axes oppose (anti-parallel).
21715
+ * This is the same convention used by `matchTo()`.
21716
+ *
21717
+ * For a revolute joint (hinge), both connectors' axes should point outward from their
21718
+ * respective parts along the hinge line. For a prismatic joint (slider), both axes
21719
+ * should point along the slide direction from their part's perspective.
21720
+ *
20641
21721
  * The joint type is inferred from the connector's `kind` field if not specified in `options`.
20642
- * Use `flip: true` for mirrored parts whose connector axis is reflected.
20643
21722
  *
20644
21723
  * When connectors are defined with `start`/`end`, you can control which point on each
20645
21724
  * connector meets via `align` / `parentAlign` / `childAlign` (`'start'`, `'middle'`, `'end'`).
@@ -20651,20 +21730,22 @@ class Assembly {
20651
21730
  * **Example**
20652
21731
  *
20653
21732
  * ```ts
20654
- * const mech = assembly("Arm")
20655
- * .addPart("Base", base)
20656
- * .addPart("Link", link)
20657
- * .connect("Base.top", "Link.shoulder", {
20658
- * as: "J1",
20659
- * min: -90, max: 90, default: 0,
20660
- * });
20661
- *
20662
- * return mech.solve({ J1: 45 }).toGroup();
21733
+ * // Hinge: both axes point outward along the hinge line
21734
+ * const frame = box(100, 10, 80).withConnectors({
21735
+ * hinge: connector("hinge", { origin: [0, 0, 40], axis: [0, 0, 1] }),
21736
+ * });
21737
+ * const door = box(60, 4, 80).withConnectors({
21738
+ * hinge: connector("hinge", { origin: [0, 0, 40], axis: [0, 0, -1] }),
21739
+ * });
21740
+ * assembly("Door")
21741
+ * .addPart("Frame", frame)
21742
+ * .addPart("Door", door)
21743
+ * .connect("Frame.hinge", "Door.hinge", { as: "swing", min: 0, max: 110 });
20663
21744
  * ```
20664
21745
  *
20665
21746
  * @param parentPortRef - `"PartName.connectorName"` on the parent side
20666
21747
  * @param childPortRef - `"PartName.connectorName"` on the child side
20667
- * @param options - `as` (joint name), `type`, `min`, `max`, `default`, `flip`, `align`, effort, velocity, etc.
21748
+ * @param options - `as` (joint name), `type`, `min`, `max`, `default`, `align`, effort, velocity, etc.
20668
21749
  * @returns `this` for chaining
20669
21750
  * @see {@link match} for typed connector matching with gender/type validation
20670
21751
  * @category Connectors
@@ -20681,7 +21762,7 @@ class Assembly {
20681
21762
  const childBase = childRecord.base;
20682
21763
  const childAlign = options.childAlign ?? options.align ?? "middle";
20683
21764
  const parentAlign = options.parentAlign ?? options.align ?? "middle";
20684
- const { frame, axis } = computeConnectFrame(childBase, child.port, parent.port, options.flip ?? false, childAlign, parentAlign);
21765
+ const { frame, axis } = computeConnectFrame(childBase, child.port, parent.port, false, childAlign, parentAlign);
20685
21766
  const min2 = options.min ?? child.port.min ?? parent.port.min;
20686
21767
  const max2 = options.max ?? child.port.max ?? parent.port.max;
20687
21768
  this._usedPortRefs.add(parentPortRef);
@@ -21599,12 +22680,12 @@ function bom(quantity, description, opts) {
21599
22680
  metadata
21600
22681
  });
21601
22682
  }
21602
- let _collected$3 = [];
22683
+ let _collected$5 = [];
21603
22684
  function resetCutPlanes() {
21604
- _collected$3 = [];
22685
+ _collected$5 = [];
21605
22686
  }
21606
22687
  function getCollectedCutPlanes() {
21607
- return _collected$3.slice();
22688
+ return _collected$5.slice();
21608
22689
  }
21609
22690
  function normalizeExcludedObjectNames(input) {
21610
22691
  if (input === void 0) return void 0;
@@ -21620,7 +22701,29 @@ function cutPlane(name, normal, offsetOrOptions = 0, maybeOptions = {}) {
21620
22701
  const offset2 = Number.isFinite(rawOffset) ? rawOffset : 0;
21621
22702
  const options = usingOffsetArg ? maybeOptions : offsetOrOptions;
21622
22703
  const excludeObjectNames = normalizeExcludedObjectNames(options.exclude);
21623
- _collected$3.push({ name, normal, offset: offset2, excludeObjectNames });
22704
+ _collected$5.push({ name, normal, offset: offset2, excludeObjectNames });
22705
+ }
22706
+ let _collected$4 = [];
22707
+ let _counter$1 = 0;
22708
+ function resetMocks() {
22709
+ _collected$4 = [];
22710
+ _counter$1 = 0;
22711
+ }
22712
+ function getCollectedMocks() {
22713
+ return _collected$4.slice();
22714
+ }
22715
+ function mock(shape, name) {
22716
+ if (!shape || typeof shape !== "object") {
22717
+ throw new Error("mock(shape): shape must be a Shape");
22718
+ }
22719
+ _counter$1 += 1;
22720
+ const displayName = name && typeof name === "string" && name.trim().length > 0 ? name.trim() : `Mock ${_counter$1}`;
22721
+ _collected$4.push({
22722
+ id: `mock-${_counter$1}`,
22723
+ name: displayName,
22724
+ shape
22725
+ });
22726
+ return shape;
21624
22727
  }
21625
22728
  const DEFAULT_PROFILE = {
21626
22729
  bedX: 220,
@@ -22988,11 +24091,13 @@ Shape.prototype.cutout = function cutout(sketch, opts = {}) {
22988
24091
  return shapeCutout(this, sketch, opts);
22989
24092
  };
22990
24093
  let _params = [];
24094
+ let _stringParams = [];
22991
24095
  let _listParams = [];
22992
24096
  let _overrides = {};
22993
24097
  let _scopeStack = [];
22994
24098
  function resetParams() {
22995
24099
  _params = [];
24100
+ _stringParams = [];
22996
24101
  _listParams = [];
22997
24102
  _scopeStack = [];
22998
24103
  }
@@ -23002,6 +24107,9 @@ function setParamOverrides(overrides) {
23002
24107
  function getCollectedParams() {
23003
24108
  return _params;
23004
24109
  }
24110
+ function getCollectedStringParams() {
24111
+ return _stringParams;
24112
+ }
23005
24113
  function getCollectedListParams() {
23006
24114
  return _listParams;
23007
24115
  }
@@ -23018,6 +24126,11 @@ function hasOwn(obj, key) {
23018
24126
  }
23019
24127
  function param(name, defaultValue, opts = {}) {
23020
24128
  var _a3;
24129
+ if (typeof defaultValue !== "number") {
24130
+ throw new Error(
24131
+ `param("${name}"): defaultValue must be a number, got ${typeof defaultValue}. For text parameters, use Param.string("${name}", ${JSON.stringify(defaultValue)}).`
24132
+ );
24133
+ }
23021
24134
  const scope = _scopeStack[_scopeStack.length - 1];
23022
24135
  const scopedName = (scope == null ? void 0 : scope.namePrefix) ? `${scope.namePrefix} / ${name}` : name;
23023
24136
  const scopedLocal = scope == null ? void 0 : scope.localOverrides;
@@ -23087,6 +24200,32 @@ function choiceParam(name, defaultValue, choices) {
23087
24200
  }
23088
24201
  return choices[index];
23089
24202
  }
24203
+ function stringParam(name, defaultValue, opts = {}) {
24204
+ var _a3;
24205
+ if (typeof defaultValue !== "string") {
24206
+ throw new Error(`Param.string("${name}"): defaultValue must be a string, got ${typeof defaultValue}.`);
24207
+ }
24208
+ const scope = _scopeStack[_scopeStack.length - 1];
24209
+ const scopedName = (scope == null ? void 0 : scope.namePrefix) ? `${scope.namePrefix} / ${name}` : name;
24210
+ const scopedLocal = scope == null ? void 0 : scope.localOverrides;
24211
+ const hasLocalOverride = !!(scopedLocal && Object.prototype.hasOwnProperty.call(scopedLocal, name));
24212
+ if (hasLocalOverride) (_a3 = scope.consumedKeys) == null ? void 0 : _a3.add(name);
24213
+ const rawOverride = (hasLocalOverride ? scopedLocal[name] : void 0) ?? _overrides[scopedName] ?? _overrides[name];
24214
+ const value = typeof rawOverride === "string" ? rawOverride : defaultValue;
24215
+ const maxLength = opts.maxLength;
24216
+ const clamped = maxLength !== void 0 ? value.slice(0, maxLength) : value;
24217
+ if (!hasLocalOverride) {
24218
+ _stringParams.push({ name: scopedName, value: clamped, defaultValue, maxLength });
24219
+ }
24220
+ return clamped;
24221
+ }
24222
+ const Param = {
24223
+ number: param,
24224
+ string: stringParam,
24225
+ bool: boolParam,
24226
+ choice: choiceParam,
24227
+ list: listParam
24228
+ };
23090
24229
  function listParam(name, defaultItems, opts) {
23091
24230
  if (!Array.isArray(defaultItems)) throw new Error(`listParam("${name}"): defaultItems must be an array`);
23092
24231
  const scope = _scopeStack[_scopeStack.length - 1];
@@ -23665,6 +24804,24 @@ function catmullRom2D$1(p0, p1, p2, p3, t, tension) {
23665
24804
  const h11 = ttt - tt;
23666
24805
  return [h00 * p1[0] + h10 * m1x + h01 * p2[0] + h11 * m2x, h00 * p1[1] + h10 * m1y + h01 * p2[1] + h11 * m2y];
23667
24806
  }
24807
+ function sampleBSpline2D(controlPoints, weights, degree, tol = DEFAULT_TOLERANCE$1) {
24808
+ const n = controlPoints.length;
24809
+ const knots = generateClampedKnots(n, degree);
24810
+ let polyLen = 0;
24811
+ for (let i = 1; i < n; i++) {
24812
+ polyLen += Math.hypot(controlPoints[i][0] - controlPoints[i - 1][0], controlPoints[i][1] - controlPoints[i - 1][1]);
24813
+ }
24814
+ const count = Math.max(8, Math.min(256, Math.ceil(polyLen / tol)));
24815
+ const uMin = knots[degree];
24816
+ const uMax = knots[n];
24817
+ const pts = new Array(count);
24818
+ for (let i = 0; i < count; i++) {
24819
+ const t = i / (count - 1);
24820
+ const u = uMin + t * (uMax - uMin);
24821
+ pts[i] = deBoor2D(controlPoints, weights, knots, degree, u);
24822
+ }
24823
+ return pts;
24824
+ }
23668
24825
  function ensureCCW(pts) {
23669
24826
  let signedArea2 = 0;
23670
24827
  for (let i = 0; i < pts.length; i++) {
@@ -24039,6 +25196,82 @@ class PathBuilder {
24039
25196
  this.y = last[1];
24040
25197
  return this;
24041
25198
  }
25199
+ // ── NURBS / exact curves ──────────────────────────────────────────────────
25200
+ /**
25201
+ * Rational B-spline edge to (x, y) with explicit control points and weights.
25202
+ *
25203
+ * The control points define the B-spline shape between the current position
25204
+ * and (x, y). The current position is NOT included in `controlPoints` — it is
25205
+ * automatically prepended. The endpoint (x, y) is the last control point.
25206
+ *
25207
+ * @param controlPoints — interior + endpoint control points (endpoint = last)
25208
+ * @param opts.weights — rational weights (default: all 1.0)
25209
+ * @param opts.degree — B-spline degree (default: control point count - 1, capped at 3)
25210
+ */
25211
+ nurbsTo(controlPoints, opts) {
25212
+ if (controlPoints.length < 1) throw new Error("nurbsTo: need at least 1 control point (the endpoint)");
25213
+ const allPts = [[this.x, this.y], ...controlPoints];
25214
+ const n = allPts.length;
25215
+ const degree = (opts == null ? void 0 : opts.degree) ?? Math.min(n - 1, 3);
25216
+ const weights = (opts == null ? void 0 : opts.weights) ?? new Array(n).fill(1);
25217
+ if (weights.length !== n) throw new Error(`nurbsTo: weights.length (${weights.length}) must match total control points (${n})`);
25218
+ const last = controlPoints[controlPoints.length - 1];
25219
+ this.segs.push({ kind: "bspline", x: last[0], y: last[1], controlPoints: allPts, weights, degree });
25220
+ const prevPt = allPts[n - 2];
25221
+ const tdx = last[0] - prevPt[0];
25222
+ const tdy = last[1] - prevPt[1];
25223
+ const tlen = Math.hypot(tdx, tdy);
25224
+ if (tlen > 1e-9) {
25225
+ this.dirX = tdx / tlen;
25226
+ this.dirY = tdy / tlen;
25227
+ }
25228
+ this.x = last[0];
25229
+ this.y = last[1];
25230
+ return this;
25231
+ }
25232
+ /**
25233
+ * Exact circular arc to (x, y) using a rational quadratic NURBS.
25234
+ *
25235
+ * Unlike `arcTo()` which tessellates to a polyline, this preserves the
25236
+ * exact arc definition. When extruded through the OCCT backend, it produces
25237
+ * a true cylindrical face — not a faceted approximation.
25238
+ *
25239
+ * @param x — endpoint X
25240
+ * @param y — endpoint Y
25241
+ * @param opts.radius — arc radius (default: auto-computed from chord)
25242
+ * @param opts.clockwise — winding direction (default: false = CCW)
25243
+ */
25244
+ exactArcTo(x, y, opts) {
25245
+ const clockwise = (opts == null ? void 0 : opts.clockwise) ?? false;
25246
+ const dx = x - this.x;
25247
+ const dy = y - this.y;
25248
+ const chordLen = Math.hypot(dx, dy);
25249
+ if (chordLen < 1e-9) return this;
25250
+ const radius = (opts == null ? void 0 : opts.radius) ?? chordLen;
25251
+ if (radius < chordLen / 2 - 1e-9) throw new Error("exactArcTo: radius too small for the chord");
25252
+ const mx = (this.x + x) / 2;
25253
+ const my = (this.y + y) / 2;
25254
+ let nx = -dy / chordLen;
25255
+ let ny = dx / chordLen;
25256
+ if (clockwise) {
25257
+ nx = -nx;
25258
+ ny = -ny;
25259
+ }
25260
+ const h = Math.sqrt(Math.max(0, radius * radius - chordLen / 2 * (chordLen / 2)));
25261
+ const cx = mx + nx * h;
25262
+ const cy = my + ny * h;
25263
+ const halfAngle = Math.asin(Math.min(1, chordLen / (2 * radius)));
25264
+ const w = Math.cos(halfAngle);
25265
+ const smx = (this.x + x) / 2 - cx;
25266
+ const smy = (this.y + y) / 2 - cy;
25267
+ const smLen = Math.hypot(smx, smy);
25268
+ const shoulderX = cx + smx / smLen * radius;
25269
+ const shoulderY = cy + smy / smLen * radius;
25270
+ return this.nurbsTo(
25271
+ [[shoulderX, shoulderY], [x, y]],
25272
+ { weights: [1, w, 1], degree: 2 }
25273
+ );
25274
+ }
24042
25275
  // ── Corner modifiers ──────────────────────────────────────────────────────
24043
25276
  /**
24044
25277
  * Round the last corner (the junction between the previous two segments)
@@ -24363,6 +25596,11 @@ class PathBuilder {
24363
25596
  const last = seg.points[seg.points.length - 1];
24364
25597
  px = last[0];
24365
25598
  py = last[1];
25599
+ } else if (seg.kind === "bspline") {
25600
+ const sampled = sampleBSpline2D(seg.controlPoints, seg.weights, seg.degree);
25601
+ for (let i = 1; i < sampled.length; i++) pts.push(sampled[i]);
25602
+ px = seg.x;
25603
+ py = seg.y;
24366
25604
  }
24367
25605
  }
24368
25606
  return pts;
@@ -24399,11 +25637,23 @@ class PathBuilder {
24399
25637
  close() {
24400
25638
  const subPaths = this.splitSubPaths();
24401
25639
  if (subPaths.length === 0) throw new Error("Path needs at least 3 points");
25640
+ const hasBSpline = this.segs.some((s) => s.kind === "bspline");
24402
25641
  const tessellated = subPaths.map((segs) => this.tessellateSegs(segs));
24403
25642
  const outer = tessellated[0];
24404
25643
  if (outer.length < 3) throw new Error("Path needs at least 3 points");
24405
25644
  ensureCCW(outer);
24406
- let result = polygon(outer);
25645
+ let result;
25646
+ if (hasBSpline && subPaths.length === 1) {
25647
+ const edges = this.buildProfileEdges(subPaths[0]);
25648
+ result = buildSketchFromCompileProfilePlan({
25649
+ kind: "pathProfile",
25650
+ points: outer,
25651
+ edges,
25652
+ transforms: []
25653
+ });
25654
+ } else {
25655
+ result = polygon(outer);
25656
+ }
24407
25657
  for (let i = 1; i < tessellated.length; i++) {
24408
25658
  const hole2 = tessellated[i];
24409
25659
  if (hole2.length < 3) continue;
@@ -24530,6 +25780,48 @@ class PathBuilder {
24530
25780
  if (current.length > 0) paths.push(current);
24531
25781
  return paths;
24532
25782
  }
25783
+ /** Build semantic ProfileEdge array from path segments (for pathProfile compile plan). */
25784
+ buildProfileEdges(segs) {
25785
+ const edges = [];
25786
+ let px = 0, py = 0;
25787
+ for (const seg of segs) {
25788
+ if (seg.kind === "move") {
25789
+ px = seg.x;
25790
+ py = seg.y;
25791
+ } else if (seg.kind === "line") {
25792
+ edges.push({ kind: "line", x1: px, y1: py, x2: seg.x, y2: seg.y });
25793
+ px = seg.x;
25794
+ py = seg.y;
25795
+ } else if (seg.kind === "arc") {
25796
+ edges.push({ kind: "arc", x1: px, y1: py, x2: seg.x, y2: seg.y, cx: seg.cx, cy: seg.cy, clockwise: seg.clockwise });
25797
+ px = seg.x;
25798
+ py = seg.y;
25799
+ } else if (seg.kind === "bspline") {
25800
+ edges.push({
25801
+ kind: "bspline",
25802
+ controlPoints: seg.controlPoints,
25803
+ weights: seg.weights,
25804
+ knots: generateClampedKnots(seg.controlPoints.length, seg.degree),
25805
+ degree: seg.degree
25806
+ });
25807
+ px = seg.x;
25808
+ py = seg.y;
25809
+ } else {
25810
+ edges.push({ kind: "line", x1: px, y1: py, x2: seg.x, y2: seg.y });
25811
+ px = seg.x;
25812
+ py = seg.y;
25813
+ }
25814
+ }
25815
+ if (segs.length > 0) {
25816
+ const first = segs[0];
25817
+ const firstX = first.kind === "move" ? first.x : 0;
25818
+ const firstY = first.kind === "move" ? first.y : 0;
25819
+ if (Math.hypot(px - firstX, py - firstY) > 1e-9) {
25820
+ edges.push({ kind: "line", x1: px, y1: py, x2: firstX, y2: firstY });
25821
+ }
25822
+ }
25823
+ return edges;
25824
+ }
24533
25825
  /** Tessellate a sub-path (sequence of segments). */
24534
25826
  tessellateSegs(segs) {
24535
25827
  const pts = [];
@@ -24557,6 +25849,11 @@ class PathBuilder {
24557
25849
  const last = seg.points[seg.points.length - 1];
24558
25850
  px = last[0];
24559
25851
  py = last[1];
25852
+ } else if (seg.kind === "bspline") {
25853
+ const sampled = sampleBSpline2D(seg.controlPoints, seg.weights, seg.degree);
25854
+ for (let i = 1; i < sampled.length; i++) pts.push(sampled[i]);
25855
+ px = seg.x;
25856
+ py = seg.y;
24560
25857
  }
24561
25858
  }
24562
25859
  return pts;
@@ -25843,7 +27140,20 @@ function spurGear(options) {
25843
27140
  profile = difference2d(profile, circle2d(normalized.boreDiameter * 0.5, Math.max(48, normalized.teeth * 2)));
25844
27141
  }
25845
27142
  const shape = sketchExtrude(profile, normalized.faceWidth);
25846
- return attachGearMeta(shape, meta2);
27143
+ const shapeWithConnectors = shape.withConnectors({
27144
+ bore: connectorFactory("gear-bore", {
27145
+ origin: [0, 0, normalized.faceWidth / 2],
27146
+ axis: [0, 0, 1],
27147
+ kind: "revolute"
27148
+ }, {
27149
+ module: normalized.module,
27150
+ teeth: normalized.teeth,
27151
+ pitchRadius: meta2.pitchRadius,
27152
+ outerRadius: meta2.outerRadius,
27153
+ faceWidth: normalized.faceWidth
27154
+ })
27155
+ });
27156
+ return attachGearMeta(shapeWithConnectors, meta2);
25847
27157
  }
25848
27158
  function normalizeSideGearOptions(options) {
25849
27159
  let normalizedSpur;
@@ -25929,7 +27239,23 @@ function sideGear(options) {
25929
27239
  );
25930
27240
  shape = shape.subtract(bore);
25931
27241
  }
25932
- return attachGearMeta(shape, meta2);
27242
+ const boreZ = normalized.side === "top" ? zBands.bodyMinZ : zBands.bodyMaxZ;
27243
+ const boreAxis = normalized.side === "top" ? [0, 0, -1] : [0, 0, 1];
27244
+ const shapeWithConnectors = shape.withConnectors({
27245
+ bore: connectorFactory("gear-bore", {
27246
+ origin: [0, 0, boreZ],
27247
+ axis: boreAxis,
27248
+ kind: "revolute"
27249
+ }, {
27250
+ module: normalized.module,
27251
+ teeth: normalized.teeth,
27252
+ pitchRadius: meta2.pitchRadius,
27253
+ outerRadius: meta2.outerRadius,
27254
+ faceWidth: normalized.faceWidth,
27255
+ toothSide: normalized.side
27256
+ })
27257
+ });
27258
+ return attachGearMeta(shapeWithConnectors, meta2);
25933
27259
  }
25934
27260
  function faceGear(options) {
25935
27261
  try {
@@ -26046,7 +27372,20 @@ function ringGear(options) {
26046
27372
  }
26047
27373
  const profile = difference2d(ringBlank, union2d(...spaces));
26048
27374
  const shape = sketchExtrude(profile, normalized.faceWidth);
26049
- return attachGearMeta(shape, meta2);
27375
+ const shapeWithConnectors = shape.withConnectors({
27376
+ bore: connectorFactory("ring-bore", {
27377
+ origin: [0, 0, normalized.faceWidth / 2],
27378
+ axis: [0, 0, 1]
27379
+ }, {
27380
+ module: normalized.module,
27381
+ teeth: normalized.teeth,
27382
+ pitchRadius,
27383
+ innerRadius: tipRadius,
27384
+ outerRadius: normalized.outerRadius,
27385
+ faceWidth: normalized.faceWidth
27386
+ })
27387
+ });
27388
+ return attachGearMeta(shapeWithConnectors, meta2);
26050
27389
  }
26051
27390
  function rackGear(options) {
26052
27391
  if (!isFinitePositive(options.module)) throw new Error('rackGear: "module" must be > 0');
@@ -26097,7 +27436,8 @@ function rackGear(options) {
26097
27436
  teethSketches.push(sketchTranslate(toothSketch, cx, 0));
26098
27437
  }
26099
27438
  const span = (options.teeth - 1) * pitch + halfRoot * 2;
26100
- const base = sketchTranslate(rect(span + module * 2, baseHeight), 0, -dedendum - baseHeight * 0.5);
27439
+ const length4 = span + module * 2;
27440
+ const base = sketchTranslate(rect(length4, baseHeight), 0, -dedendum - baseHeight * 0.5);
26101
27441
  const profile = union2d(base, ...teethSketches);
26102
27442
  const shape = sketchExtrude(profile, options.faceWidth);
26103
27443
  const meta2 = {
@@ -26116,7 +27456,20 @@ function rackGear(options) {
26116
27456
  backlash,
26117
27457
  centered: false
26118
27458
  };
26119
- return attachGearMeta(shape, meta2);
27459
+ const shapeWithConnectors = shape.withConnectors({
27460
+ teeth: connectorFactory("rack-teeth", {
27461
+ origin: [0, 0, options.faceWidth / 2],
27462
+ axis: [1, 0, 0],
27463
+ up: [0, 1, 0],
27464
+ kind: "prismatic"
27465
+ }, {
27466
+ module,
27467
+ teeth: options.teeth,
27468
+ faceWidth: options.faceWidth,
27469
+ length: length4
27470
+ })
27471
+ });
27472
+ return attachGearMeta(shapeWithConnectors, meta2);
26120
27473
  }
26121
27474
  function normalizeShaftAngle(label, value) {
26122
27475
  if (!isFinitePositive(value) || value >= 175) {
@@ -26195,7 +27548,27 @@ function bevelGear(options) {
26195
27548
  const shape = sketchExtrude(profile, normalized.faceWidth, {
26196
27549
  scaleTop: normalized.topScale
26197
27550
  });
26198
- return attachGearMeta(shape, {
27551
+ const apexZ = normalized.module * normalized.teeth * 0.5 / Math.tan(normalized.pitchAngleRad);
27552
+ const measurements = {
27553
+ module: normalized.module,
27554
+ teeth: normalized.teeth,
27555
+ pitchRadius: meta2.pitchRadius,
27556
+ pitchAngleDeg: normalized.pitchAngleDeg,
27557
+ coneDistance: normalized.coneDistance,
27558
+ faceWidth: normalized.faceWidth
27559
+ };
27560
+ const shapeWithConnectors = shape.withConnectors({
27561
+ bore: connectorFactory("gear-bore", {
27562
+ origin: [0, 0, 0],
27563
+ axis: [0, 0, -1],
27564
+ kind: "revolute"
27565
+ }, measurements),
27566
+ apex: connectorFactory("bevel-apex", {
27567
+ origin: [0, 0, apexZ],
27568
+ axis: [0, 0, 1]
27569
+ }, measurements)
27570
+ });
27571
+ return attachGearMeta(shapeWithConnectors, {
26199
27572
  ...meta2,
26200
27573
  kind: "bevel",
26201
27574
  centered: false,
@@ -26677,6 +28050,70 @@ function faceGearPair(options) {
26677
28050
  status: pairStatusFromDiagnostics(diagnostics)
26678
28051
  };
26679
28052
  }
28053
+ function gearRatio(teethA, teethB, options) {
28054
+ if (!Number.isFinite(teethA) || teethA <= 0) throw new Error("gearRatio: teethA must be > 0");
28055
+ if (!Number.isFinite(teethB) || teethB <= 0) throw new Error("gearRatio: teethB must be > 0");
28056
+ const sign = (options == null ? void 0 : options.internal) ? 1 : -1;
28057
+ return sign * teethA / teethB;
28058
+ }
28059
+ function rackRatio(module, pinionTeeth) {
28060
+ if (!Number.isFinite(module) || module <= 0) throw new Error("rackRatio: module must be > 0");
28061
+ if (!Number.isFinite(pinionTeeth) || pinionTeeth <= 0) throw new Error("rackRatio: pinionTeeth must be > 0");
28062
+ const pitchRadius = module * pinionTeeth / 2;
28063
+ return 180 / (Math.PI * pitchRadius);
28064
+ }
28065
+ function planetaryRatio(sunTeeth, ringTeeth) {
28066
+ if (!Number.isFinite(sunTeeth) || sunTeeth <= 0) throw new Error("planetaryRatio: sunTeeth must be > 0");
28067
+ if (!Number.isFinite(ringTeeth) || ringTeeth <= 0) throw new Error("planetaryRatio: ringTeeth must be > 0");
28068
+ return 1 + ringTeeth / sunTeeth;
28069
+ }
28070
+ function boltPattern(options) {
28071
+ const sizeData = METRIC_HOLE_TABLE[options.size];
28072
+ if (!sizeData) throw new Error(`boltPattern: unsupported size "${options.size}"`);
28073
+ const fit = options.fit ?? "normal";
28074
+ const dia = sizeData[fit];
28075
+ const segments = options.segments ?? 48;
28076
+ if (!Array.isArray(options.positions) || options.positions.length === 0) {
28077
+ throw new Error('boltPattern: "positions" must be a non-empty array of [x, y] pairs');
28078
+ }
28079
+ const positions = options.positions.map((p2, i) => {
28080
+ if (!Array.isArray(p2) || p2.length !== 2 || !Number.isFinite(p2[0]) || !Number.isFinite(p2[1])) {
28081
+ throw new Error(`boltPattern: position[${i}] must be a finite [x, y] pair`);
28082
+ }
28083
+ return [p2[0], p2[1]];
28084
+ });
28085
+ const xs = positions.map((p2) => p2[0]);
28086
+ const ys = positions.map((p2) => p2[1]);
28087
+ return {
28088
+ size: options.size,
28089
+ dia,
28090
+ positions,
28091
+ minX: Math.min(...xs),
28092
+ maxX: Math.max(...xs),
28093
+ minY: Math.min(...ys),
28094
+ maxY: Math.max(...ys),
28095
+ cut(shape, depth, cutOptions = {}) {
28096
+ if (!Number.isFinite(depth) || depth <= 0) {
28097
+ throw new Error("boltPattern.cut: depth must be > 0");
28098
+ }
28099
+ const from = cutOptions.from ?? 0;
28100
+ const cutter = fastenerHole({
28101
+ size: options.size,
28102
+ fit,
28103
+ depth,
28104
+ center: false,
28105
+ segments,
28106
+ counterbore: cutOptions.counterbore,
28107
+ countersink: cutOptions.countersink
28108
+ });
28109
+ let result = shape;
28110
+ for (const [x, y] of positions) {
28111
+ result = result.subtract(cutter.translate(x, y, from));
28112
+ }
28113
+ return result;
28114
+ }
28115
+ };
28116
+ }
26680
28117
  function thread(diameter, pitch, length4, options) {
26681
28118
  const r = diameter / 2;
26682
28119
  const depth = (options == null ? void 0 : options.depth) ?? pitch * 0.35;
@@ -26836,7 +28273,11 @@ const partLibrary = {
26836
28273
  gearPair,
26837
28274
  bevelGearPair,
26838
28275
  faceGearPair,
26839
- sideGearPair
28276
+ sideGearPair,
28277
+ gearRatio,
28278
+ rackRatio,
28279
+ planetaryRatio,
28280
+ boltPattern
26840
28281
  };
26841
28282
  new TextEncoder();
26842
28283
  let _collectedRobotExport = null;
@@ -27589,14 +29030,14 @@ class WoodBoard {
27589
29030
  return this._withShape(this.shape.clone());
27590
29031
  }
27591
29032
  }
27592
- function requireFinite$3(value, name) {
29033
+ function requireFinite$5(value, name) {
27593
29034
  if (!Number.isFinite(value)) {
27594
29035
  throw new Error(`${name} must be a finite number, got ${value}`);
27595
29036
  }
27596
29037
  return value;
27597
29038
  }
27598
29039
  function requirePositive$2(value, name) {
27599
- requireFinite$3(value, name);
29040
+ requireFinite$5(value, name);
27600
29041
  if (value <= 0) {
27601
29042
  throw new Error(`${name} must be positive, got ${value}`);
27602
29043
  }
@@ -27631,10 +29072,10 @@ function dado(host, guest, opts) {
27631
29072
  }
27632
29073
  let fromBottom;
27633
29074
  if (opts.fromBottom != null) {
27634
- fromBottom = requireFinite$3(opts.fromBottom, "fromBottom");
29075
+ fromBottom = requireFinite$5(opts.fromBottom, "fromBottom");
27635
29076
  } else {
27636
29077
  fromBottom = host.height - opts.fromTop - channelWidth;
27637
- requireFinite$3(fromBottom, "computed fromBottom");
29078
+ requireFinite$5(fromBottom, "computed fromBottom");
27638
29079
  }
27639
29080
  let dadoLength = host.width;
27640
29081
  let xOffset = 0;
@@ -27691,7 +29132,7 @@ function mortiseAndTenon(mortiseBoard, tenonBoard, opts) {
27691
29132
  const style = o.style ?? "blind";
27692
29133
  const fit = o.fit ?? "snug";
27693
29134
  const clearance = clearanceForFit(fit);
27694
- const cornerRadius = o.cornerRadius != null ? requireFinite$3(o.cornerRadius, "cornerRadius") : 0;
29135
+ const cornerRadius = o.cornerRadius != null ? requireFinite$5(o.cornerRadius, "cornerRadius") : 0;
27695
29136
  const tenonThickness = o.tenonThickness != null ? requirePositive$2(o.tenonThickness, "tenonThickness") : tenonBoard.thickness / 3;
27696
29137
  const tenonWidth = o.tenonWidth != null ? requirePositive$2(o.tenonWidth, "tenonWidth") : Math.min(tenonBoard.height * 0.6, mortiseBoard.height * 0.8);
27697
29138
  const tenonLength = o.tenonLength != null ? requirePositive$2(o.tenonLength, "tenonLength") : style === "through" ? mortiseBoard.thickness : mortiseBoard.thickness * 2 / 3;
@@ -27704,10 +29145,10 @@ function mortiseAndTenon(mortiseBoard, tenonBoard, opts) {
27704
29145
  throw new Error("mortiseAndTenon: specify position.fromTop or position.fromBottom, not both");
27705
29146
  }
27706
29147
  if (o.position.fromTop != null) {
27707
- requireFinite$3(o.position.fromTop, "position.fromTop");
29148
+ requireFinite$5(o.position.fromTop, "position.fromTop");
27708
29149
  mortiseCenterY = mortiseBoard.height / 2 - o.position.fromTop - mortiseH / 2;
27709
29150
  } else if (o.position.fromBottom != null) {
27710
- requireFinite$3(o.position.fromBottom, "position.fromBottom");
29151
+ requireFinite$5(o.position.fromBottom, "position.fromBottom");
27711
29152
  mortiseCenterY = -mortiseBoard.height / 2 + o.position.fromBottom + mortiseH / 2;
27712
29153
  }
27713
29154
  }
@@ -27777,6 +29218,94 @@ const Wood = {
27777
29218
  */
27778
29219
  mortiseAndTenon
27779
29220
  };
29221
+ let _collected$3 = null;
29222
+ function resetCameraTrajectory() {
29223
+ _collected$3 = null;
29224
+ }
29225
+ function getCollectedCameraTrajectory() {
29226
+ return _collected$3;
29227
+ }
29228
+ function isOrbitKeyframe(kf) {
29229
+ return "orbit" in kf;
29230
+ }
29231
+ function isCartesianKeyframe(kf) {
29232
+ return "position" in kf;
29233
+ }
29234
+ function requireFinite$4(value, label) {
29235
+ if (!Number.isFinite(value)) {
29236
+ throw new Error(`cameraTrajectory(): ${label} must be a finite number, got ${value}`);
29237
+ }
29238
+ }
29239
+ function validateOrbitKeyframe(kf, index) {
29240
+ requireFinite$4(kf.at, `keyframes[${index}].at`);
29241
+ requireFinite$4(kf.orbit.angle, `keyframes[${index}].orbit.angle`);
29242
+ requireFinite$4(kf.orbit.pitch, `keyframes[${index}].orbit.pitch`);
29243
+ requireFinite$4(kf.orbit.distance, `keyframes[${index}].orbit.distance`);
29244
+ }
29245
+ function validateCartesianKeyframe(kf, index) {
29246
+ requireFinite$4(kf.at, `keyframes[${index}].at`);
29247
+ if (!Array.isArray(kf.position) || kf.position.length !== 3) {
29248
+ throw new Error(`cameraTrajectory(): keyframes[${index}].position must be a 3-element array`);
29249
+ }
29250
+ if (!Array.isArray(kf.target) || kf.target.length !== 3) {
29251
+ throw new Error(`cameraTrajectory(): keyframes[${index}].target must be a 3-element array`);
29252
+ }
29253
+ for (let i = 0; i < 3; i++) {
29254
+ requireFinite$4(kf.position[i], `keyframes[${index}].position[${i}]`);
29255
+ requireFinite$4(kf.target[i], `keyframes[${index}].target[${i}]`);
29256
+ }
29257
+ }
29258
+ function validateKeyframeOrder(keyframes) {
29259
+ if (keyframes.length < 2) {
29260
+ throw new Error("cameraTrajectory(): keyframes must contain at least 2 entries");
29261
+ }
29262
+ for (let i = 0; i < keyframes.length; i++) {
29263
+ const at = keyframes[i].at;
29264
+ if (at < 0 || at > 1) {
29265
+ throw new Error(`cameraTrajectory(): keyframes[${i}].at must be in [0, 1], got ${at}`);
29266
+ }
29267
+ if (i > 0 && at < keyframes[i - 1].at) {
29268
+ throw new Error(
29269
+ `cameraTrajectory(): keyframes must be sorted by 'at' ascending — keyframes[${i - 1}].at=${keyframes[i - 1].at} > keyframes[${i}].at=${at}`
29270
+ );
29271
+ }
29272
+ }
29273
+ }
29274
+ function cameraTrajectory(defOrFn, options) {
29275
+ if (_collected$3 !== null) {
29276
+ console.warn("cameraTrajectory() called more than once — overwriting previous trajectory.");
29277
+ }
29278
+ if (typeof defOrFn === "function") {
29279
+ _collected$3 = {
29280
+ kind: "parametric",
29281
+ parametricFn: defOrFn,
29282
+ duration: options == null ? void 0 : options.duration,
29283
+ fps: options == null ? void 0 : options.fps
29284
+ };
29285
+ return;
29286
+ }
29287
+ const { keyframes, duration, fps, easing } = defOrFn;
29288
+ if (!Array.isArray(keyframes) || keyframes.length === 0) {
29289
+ throw new Error("cameraTrajectory(): keyframes must be a non-empty array");
29290
+ }
29291
+ validateKeyframeOrder(keyframes);
29292
+ const first = keyframes[0];
29293
+ if (isOrbitKeyframe(first)) {
29294
+ const orbitKeyframes = keyframes;
29295
+ for (let i = 0; i < orbitKeyframes.length; i++) {
29296
+ validateOrbitKeyframe(orbitKeyframes[i], i);
29297
+ }
29298
+ _collected$3 = { kind: "orbit-keyframes", orbitKeyframes, duration, fps, easing };
29299
+ } else if (isCartesianKeyframe(first)) {
29300
+ const cartesianKeyframes = keyframes;
29301
+ for (let i = 0; i < cartesianKeyframes.length; i++) {
29302
+ validateCartesianKeyframe(cartesianKeyframes[i], i);
29303
+ }
29304
+ _collected$3 = { kind: "cartesian-keyframes", cartesianKeyframes, duration, fps, easing };
29305
+ } else {
29306
+ throw new Error('cameraTrajectory(): each keyframe must have either an "orbit" or "position" property');
29307
+ }
29308
+ }
27780
29309
  function resolveEdges(shape, edges) {
27781
29310
  if (!edges) {
27782
29311
  return selectEdges(shape);
@@ -27884,7 +29413,7 @@ function offsetSolid(shape, thickness) {
27884
29413
  sources: ["offset-solid"]
27885
29414
  });
27886
29415
  }
27887
- function requireFinite$2(value, label) {
29416
+ function requireFinite$3(value, label) {
27888
29417
  if (typeof value !== "number" || !Number.isFinite(value)) {
27889
29418
  throw new Error(`${label} must be a finite number`);
27890
29419
  }
@@ -27894,7 +29423,7 @@ function requireVec3(value, label) {
27894
29423
  if (!Array.isArray(value) || value.length !== 3) {
27895
29424
  throw new Error(`${label} must be [x, y, z]`);
27896
29425
  }
27897
- return [requireFinite$2(value[0], `${label}[0]`), requireFinite$2(value[1], `${label}[1]`), requireFinite$2(value[2], `${label}[2]`)];
29426
+ return [requireFinite$3(value[0], `${label}[0]`), requireFinite$3(value[1], `${label}[1]`), requireFinite$3(value[2], `${label}[2]`)];
27898
29427
  }
27899
29428
  function requireColor(value, label) {
27900
29429
  if (typeof value !== "string" || !value.trim()) {
@@ -27922,7 +29451,7 @@ function validateCamera(cam, label) {
27922
29451
  if (cam.target !== void 0) out.target = requireVec3(cam.target, `${label}.target`);
27923
29452
  if (cam.up !== void 0) out.up = requireVec3(cam.up, `${label}.up`);
27924
29453
  if (cam.fov !== void 0) {
27925
- out.fov = requireFinite$2(cam.fov, `${label}.fov`);
29454
+ out.fov = requireFinite$3(cam.fov, `${label}.fov`);
27926
29455
  if (out.fov <= 0 || out.fov >= 180) throw new Error(`${label}.fov must be between 0 and 180`);
27927
29456
  }
27928
29457
  if (cam.type !== void 0) {
@@ -27940,15 +29469,15 @@ function validateLight(light, label) {
27940
29469
  }
27941
29470
  const out = { type: light.type };
27942
29471
  if (light.color !== void 0) out.color = requireColor(light.color, `${label}.color`);
27943
- if (light.intensity !== void 0) out.intensity = requireFinite$2(light.intensity, `${label}.intensity`);
29472
+ if (light.intensity !== void 0) out.intensity = requireFinite$3(light.intensity, `${label}.intensity`);
27944
29473
  if (light.position !== void 0) out.position = requireVec3(light.position, `${label}.position`);
27945
29474
  if (light.target !== void 0) out.target = requireVec3(light.target, `${label}.target`);
27946
29475
  if (light.groundColor !== void 0) out.groundColor = requireColor(light.groundColor, `${label}.groundColor`);
27947
29476
  if (light.skyColor !== void 0) out.skyColor = requireColor(light.skyColor, `${label}.skyColor`);
27948
- if (light.angle !== void 0) out.angle = requireFinite$2(light.angle, `${label}.angle`);
27949
- if (light.penumbra !== void 0) out.penumbra = requireFinite$2(light.penumbra, `${label}.penumbra`);
27950
- if (light.decay !== void 0) out.decay = requireFinite$2(light.decay, `${label}.decay`);
27951
- if (light.distance !== void 0) out.distance = requireFinite$2(light.distance, `${label}.distance`);
29477
+ if (light.angle !== void 0) out.angle = requireFinite$3(light.angle, `${label}.angle`);
29478
+ if (light.penumbra !== void 0) out.penumbra = requireFinite$3(light.penumbra, `${label}.penumbra`);
29479
+ if (light.decay !== void 0) out.decay = requireFinite$3(light.decay, `${label}.decay`);
29480
+ if (light.distance !== void 0) out.distance = requireFinite$3(light.distance, `${label}.distance`);
27952
29481
  if (light.castShadow !== void 0) {
27953
29482
  if (typeof light.castShadow !== "boolean") throw new Error(`${label}.castShadow must be a boolean`);
27954
29483
  out.castShadow = light.castShadow;
@@ -27963,7 +29492,7 @@ function validateEnvironment(env, label) {
27963
29492
  }
27964
29493
  out.preset = env.preset;
27965
29494
  }
27966
- if (env.intensity !== void 0) out.intensity = requireFinite$2(env.intensity, `${label}.intensity`);
29495
+ if (env.intensity !== void 0) out.intensity = requireFinite$3(env.intensity, `${label}.intensity`);
27967
29496
  if (env.background !== void 0) {
27968
29497
  if (typeof env.background !== "boolean") throw new Error(`${label}.background must be a boolean`);
27969
29498
  out.background = env.background;
@@ -27973,9 +29502,9 @@ function validateEnvironment(env, label) {
27973
29502
  function validateFog(fog, label) {
27974
29503
  const out = {};
27975
29504
  if (fog.color !== void 0) out.color = requireColor(fog.color, `${label}.color`);
27976
- if (fog.near !== void 0) out.near = requireFinite$2(fog.near, `${label}.near`);
27977
- if (fog.far !== void 0) out.far = requireFinite$2(fog.far, `${label}.far`);
27978
- if (fog.density !== void 0) out.density = requireFinite$2(fog.density, `${label}.density`);
29505
+ if (fog.near !== void 0) out.near = requireFinite$3(fog.near, `${label}.near`);
29506
+ if (fog.far !== void 0) out.far = requireFinite$3(fog.far, `${label}.far`);
29507
+ if (fog.density !== void 0) out.density = requireFinite$3(fog.density, `${label}.density`);
27979
29508
  return out;
27980
29509
  }
27981
29510
  function validatePostProcessing(pp, label) {
@@ -27983,23 +29512,23 @@ function validatePostProcessing(pp, label) {
27983
29512
  if (pp.bloom !== void 0) {
27984
29513
  if (!pp.bloom || typeof pp.bloom !== "object") throw new Error(`${label}.bloom must be an object`);
27985
29514
  out.bloom = {};
27986
- if (pp.bloom.intensity !== void 0) out.bloom.intensity = requireFinite$2(pp.bloom.intensity, `${label}.bloom.intensity`);
27987
- if (pp.bloom.threshold !== void 0) out.bloom.threshold = requireFinite$2(pp.bloom.threshold, `${label}.bloom.threshold`);
27988
- if (pp.bloom.radius !== void 0) out.bloom.radius = requireFinite$2(pp.bloom.radius, `${label}.bloom.radius`);
29515
+ if (pp.bloom.intensity !== void 0) out.bloom.intensity = requireFinite$3(pp.bloom.intensity, `${label}.bloom.intensity`);
29516
+ if (pp.bloom.threshold !== void 0) out.bloom.threshold = requireFinite$3(pp.bloom.threshold, `${label}.bloom.threshold`);
29517
+ if (pp.bloom.radius !== void 0) out.bloom.radius = requireFinite$3(pp.bloom.radius, `${label}.bloom.radius`);
27989
29518
  }
27990
29519
  if (pp.vignette !== void 0) {
27991
29520
  if (!pp.vignette || typeof pp.vignette !== "object") throw new Error(`${label}.vignette must be an object`);
27992
29521
  out.vignette = {};
27993
- if (pp.vignette.darkness !== void 0) out.vignette.darkness = requireFinite$2(pp.vignette.darkness, `${label}.vignette.darkness`);
27994
- if (pp.vignette.offset !== void 0) out.vignette.offset = requireFinite$2(pp.vignette.offset, `${label}.vignette.offset`);
29522
+ if (pp.vignette.darkness !== void 0) out.vignette.darkness = requireFinite$3(pp.vignette.darkness, `${label}.vignette.darkness`);
29523
+ if (pp.vignette.offset !== void 0) out.vignette.offset = requireFinite$3(pp.vignette.offset, `${label}.vignette.offset`);
27995
29524
  }
27996
29525
  if (pp.grain !== void 0) {
27997
29526
  if (!pp.grain || typeof pp.grain !== "object") throw new Error(`${label}.grain must be an object`);
27998
29527
  out.grain = {};
27999
- if (pp.grain.intensity !== void 0) out.grain.intensity = requireFinite$2(pp.grain.intensity, `${label}.grain.intensity`);
29528
+ if (pp.grain.intensity !== void 0) out.grain.intensity = requireFinite$3(pp.grain.intensity, `${label}.grain.intensity`);
28000
29529
  }
28001
29530
  if (pp.toneMappingExposure !== void 0) {
28002
- out.toneMappingExposure = requireFinite$2(pp.toneMappingExposure, `${label}.toneMappingExposure`);
29531
+ out.toneMappingExposure = requireFinite$3(pp.toneMappingExposure, `${label}.toneMappingExposure`);
28003
29532
  }
28004
29533
  return out;
28005
29534
  }
@@ -28010,7 +29539,7 @@ function validateGround(ground, label) {
28010
29539
  out.visible = ground.visible;
28011
29540
  }
28012
29541
  if (ground.color !== void 0) out.color = requireColor(ground.color, `${label}.color`);
28013
- if (ground.offset !== void 0) out.offset = requireFinite$2(ground.offset, `${label}.offset`);
29542
+ if (ground.offset !== void 0) out.offset = requireFinite$3(ground.offset, `${label}.offset`);
28014
29543
  if (ground.receiveShadow !== void 0) {
28015
29544
  if (typeof ground.receiveShadow !== "boolean") throw new Error(`${label}.receiveShadow must be a boolean`);
28016
29545
  out.receiveShadow = ground.receiveShadow;
@@ -28020,31 +29549,31 @@ function validateGround(ground, label) {
28020
29549
  function validateCapture(cap, label) {
28021
29550
  const out = {};
28022
29551
  if (cap.framesPerTurn !== void 0) {
28023
- out.framesPerTurn = requireFinite$2(cap.framesPerTurn, `${label}.framesPerTurn`);
29552
+ out.framesPerTurn = requireFinite$3(cap.framesPerTurn, `${label}.framesPerTurn`);
28024
29553
  if (out.framesPerTurn < 12 || out.framesPerTurn > 720) {
28025
29554
  throw new Error(`${label}.framesPerTurn must be between 12 and 720`);
28026
29555
  }
28027
29556
  }
28028
29557
  if (cap.holdFrames !== void 0) {
28029
- out.holdFrames = requireFinite$2(cap.holdFrames, `${label}.holdFrames`);
29558
+ out.holdFrames = requireFinite$3(cap.holdFrames, `${label}.holdFrames`);
28030
29559
  if (out.holdFrames < 0 || out.holdFrames > 300) {
28031
29560
  throw new Error(`${label}.holdFrames must be between 0 and 300`);
28032
29561
  }
28033
29562
  }
28034
29563
  if (cap.pitchDeg !== void 0) {
28035
- out.pitchDeg = requireFinite$2(cap.pitchDeg, `${label}.pitchDeg`);
29564
+ out.pitchDeg = requireFinite$3(cap.pitchDeg, `${label}.pitchDeg`);
28036
29565
  if (out.pitchDeg < -80 || out.pitchDeg > 80) {
28037
29566
  throw new Error(`${label}.pitchDeg must be between -80 and 80`);
28038
29567
  }
28039
29568
  }
28040
29569
  if (cap.fps !== void 0) {
28041
- out.fps = requireFinite$2(cap.fps, `${label}.fps`);
29570
+ out.fps = requireFinite$3(cap.fps, `${label}.fps`);
28042
29571
  if (out.fps < 1 || out.fps > 60) {
28043
29572
  throw new Error(`${label}.fps must be between 1 and 60`);
28044
29573
  }
28045
29574
  }
28046
29575
  if (cap.size !== void 0) {
28047
- out.size = requireFinite$2(cap.size, `${label}.size`);
29576
+ out.size = requireFinite$3(cap.size, `${label}.size`);
28048
29577
  if (out.size < 1) {
28049
29578
  throw new Error(`${label}.size must be positive`);
28050
29579
  }
@@ -28745,6 +30274,16 @@ function buildProjectionReplayContext(plan) {
28745
30274
  ok: false,
28746
30275
  reason: "projection replay cannot derive a planar projection basis from fromSlices shapes."
28747
30276
  };
30277
+ case "nurbsSurface":
30278
+ return {
30279
+ ok: false,
30280
+ reason: "projection replay cannot derive a planar projection basis from NURBS surface shapes."
30281
+ };
30282
+ case "importedStep":
30283
+ return {
30284
+ ok: false,
30285
+ reason: "projection replay cannot derive a planar projection basis from imported STEP files."
30286
+ };
28748
30287
  default:
28749
30288
  assertExhaustive(plan);
28750
30289
  }
@@ -33113,6 +34652,138 @@ function hermiteTransitionG2(a, b) {
33113
34652
  { point: b.point, tangent: b.tangent, curvature: b.curvature, weight: b.weight }
33114
34653
  );
33115
34654
  }
34655
+ function requireFinite$2(v, label) {
34656
+ if (!Number.isFinite(v)) throw new Error(`nurbs3d: ${label} must be finite, got ${v}`);
34657
+ }
34658
+ class NurbsCurve3D {
34659
+ constructor(points, options = {}) {
34660
+ __publicField(this, "controlPoints");
34661
+ __publicField(this, "weights");
34662
+ __publicField(this, "knots");
34663
+ __publicField(this, "degree");
34664
+ __publicField(this, "closed");
34665
+ const n = points.length;
34666
+ const degree = options.degree ?? 3;
34667
+ if (degree < 1) throw new Error("nurbs3d: degree must be ≥ 1");
34668
+ if (n < degree + 1) throw new Error(`nurbs3d: need at least ${degree + 1} control points for degree ${degree}, got ${n}`);
34669
+ for (let i = 0; i < n; i++) {
34670
+ requireFinite$2(points[i][0], `controlPoints[${i}][0]`);
34671
+ requireFinite$2(points[i][1], `controlPoints[${i}][1]`);
34672
+ requireFinite$2(points[i][2], `controlPoints[${i}][2]`);
34673
+ }
34674
+ const weights = options.weights ?? new Array(n).fill(1);
34675
+ if (weights.length !== n) throw new Error(`nurbs3d: weights.length (${weights.length}) must equal controlPoints.length (${n})`);
34676
+ for (let i = 0; i < n; i++) {
34677
+ requireFinite$2(weights[i], `weights[${i}]`);
34678
+ if (weights[i] <= 0) throw new Error(`nurbs3d: weights[${i}] must be > 0, got ${weights[i]}`);
34679
+ }
34680
+ const expectedKnotLength = n + degree + 1;
34681
+ const knots = options.knots ?? generateClampedKnots(n, degree);
34682
+ if (knots.length !== expectedKnotLength) {
34683
+ throw new Error(`nurbs3d: knots.length (${knots.length}) must be controlPoints.length + degree + 1 (${expectedKnotLength})`);
34684
+ }
34685
+ for (let i = 0; i < knots.length; i++) {
34686
+ requireFinite$2(knots[i], `knots[${i}]`);
34687
+ if (i > 0 && knots[i] < knots[i - 1]) {
34688
+ throw new Error(`nurbs3d: knot vector must be non-decreasing, but knots[${i - 1}]=${knots[i - 1]} > knots[${i}]=${knots[i]}`);
34689
+ }
34690
+ }
34691
+ this.controlPoints = points.map(([x, y, z]) => [x, y, z]);
34692
+ this.weights = [...weights];
34693
+ this.knots = [...knots];
34694
+ this.degree = degree;
34695
+ this.closed = options.closed ?? false;
34696
+ }
34697
+ /**
34698
+ * Evaluate the curve at parameter t ∈ [0, 1].
34699
+ * Uses De Boor's algorithm — exact, O(degree²).
34700
+ */
34701
+ pointAt(t) {
34702
+ const u = remapToKnotDomain(t, this.controlPoints.length, this.degree, this.knots);
34703
+ return deBoor3D(this.controlPoints, this.weights, this.knots, this.degree, u);
34704
+ }
34705
+ /**
34706
+ * Evaluate the unit tangent vector at parameter t ∈ [0, 1].
34707
+ */
34708
+ tangentAt(t) {
34709
+ const u = remapToKnotDomain(t, this.controlPoints.length, this.degree, this.knots);
34710
+ const d = deBoor3DDeriv(this.controlPoints, this.weights, this.knots, this.degree, u);
34711
+ const len = Math.sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]);
34712
+ if (len < 1e-12) return [0, 0, 1];
34713
+ return [d[0] / len, d[1] / len, d[2] / len];
34714
+ }
34715
+ /**
34716
+ * Sample the curve uniformly at `count` points.
34717
+ */
34718
+ sample(count = 48) {
34719
+ return sampleNurbs3D(this.controlPoints, this.weights, this.knots, this.degree, Math.max(2, count));
34720
+ }
34721
+ /**
34722
+ * Sample with adaptive density — more points in high-curvature regions.
34723
+ */
34724
+ sampleAdaptive(minCount = 32, maxCount = 128) {
34725
+ const probeCount = Math.max(minCount, 64);
34726
+ const curvatures = [];
34727
+ for (let i = 0; i <= probeCount; i++) {
34728
+ curvatures.push(this.estimateCurvature(i / probeCount));
34729
+ }
34730
+ const cumulative = [0];
34731
+ for (let i = 1; i < curvatures.length; i++) {
34732
+ const avgCurv = (curvatures[i - 1] + curvatures[i]) / 2;
34733
+ cumulative.push(cumulative[i - 1] + 1 + avgCurv);
34734
+ }
34735
+ const total = cumulative[cumulative.length - 1];
34736
+ const targetCount = Math.min(maxCount, Math.max(minCount, Math.round(minCount * 1.5)));
34737
+ const pts = [this.pointAt(0)];
34738
+ let probeIdx = 0;
34739
+ for (let i = 1; i < targetCount; i++) {
34740
+ const target = i / targetCount * total;
34741
+ while (probeIdx < cumulative.length - 1 && cumulative[probeIdx + 1] < target) {
34742
+ probeIdx++;
34743
+ }
34744
+ const frac = (target - cumulative[probeIdx]) / (cumulative[probeIdx + 1] - cumulative[probeIdx]);
34745
+ const t = (probeIdx + frac) / probeCount;
34746
+ pts.push(this.pointAt(t));
34747
+ }
34748
+ pts.push(this.pointAt(1));
34749
+ return pts;
34750
+ }
34751
+ /**
34752
+ * Approximate arc length by summing polyline segment lengths.
34753
+ */
34754
+ length(samples = 100) {
34755
+ const pts = this.sample(Math.max(10, samples));
34756
+ let len = 0;
34757
+ for (let i = 1; i < pts.length; i++) {
34758
+ const dx = pts[i][0] - pts[i - 1][0];
34759
+ const dy = pts[i][1] - pts[i - 1][1];
34760
+ const dz = pts[i][2] - pts[i - 1][2];
34761
+ len += Math.sqrt(dx * dx + dy * dy + dz * dz);
34762
+ }
34763
+ return len;
34764
+ }
34765
+ /** Convert to a format compatible with sweep() path input. */
34766
+ toPolyline(samples = 64) {
34767
+ return this.sampleAdaptive(Math.max(16, samples), samples * 2);
34768
+ }
34769
+ estimateCurvature(t) {
34770
+ const eps = 1e-4;
34771
+ const t0 = Math.max(0, t - eps);
34772
+ const t1 = Math.min(1, t + eps);
34773
+ const tm = (t0 + t1) / 2;
34774
+ const p0 = this.pointAt(t0);
34775
+ const pm = this.pointAt(tm);
34776
+ const p1 = this.pointAt(t1);
34777
+ const dt = (t1 - t0) / 2;
34778
+ const d2x = (p0[0] - 2 * pm[0] + p1[0]) / (dt * dt);
34779
+ const d2y = (p0[1] - 2 * pm[1] + p1[1]) / (dt * dt);
34780
+ const d2z = (p0[2] - 2 * pm[2] + p1[2]) / (dt * dt);
34781
+ return Math.sqrt(d2x * d2x + d2y * d2y + d2z * d2z);
34782
+ }
34783
+ }
34784
+ function nurbs3d(points, options) {
34785
+ return new NurbsCurve3D(points, options);
34786
+ }
33116
34787
  function clamp$4(v, lo, hi) {
33117
34788
  return Math.max(lo, Math.min(hi, v));
33118
34789
  }
@@ -33455,6 +35126,19 @@ function buildPathPlan(path2) {
33455
35126
  sampleForBounds: (samples) => path2.sample(samples)
33456
35127
  };
33457
35128
  }
35129
+ if (path2 instanceof NurbsCurve3D) {
35130
+ return {
35131
+ plan: {
35132
+ kind: "nurbs",
35133
+ controlPoints: path2.controlPoints.map(([x, y, z]) => [x, y, z]),
35134
+ weights: [...path2.weights],
35135
+ knots: [...path2.knots],
35136
+ degree: path2.degree,
35137
+ closed: path2.closed
35138
+ },
35139
+ sampleForBounds: (samples) => path2.sample(samples)
35140
+ };
35141
+ }
33458
35142
  throw new Error("sweep: unsupported path type");
33459
35143
  }
33460
35144
  function sweep(profile, path2, options = {}) {
@@ -48824,6 +50508,16 @@ function sheetMetal(options) {
48824
50508
  deriveSheetMetalModel(model);
48825
50509
  return new SheetMetalPart(model);
48826
50510
  }
50511
+ function importStepFromBuffer(fileData, displayName = "import.step") {
50512
+ return buildShapeFromCompilePlan(
50513
+ createOwnedShapeCompilePlan(
50514
+ { kind: "importedStep", filePath: displayName, fileData },
50515
+ "importStep"
50516
+ ),
50517
+ void 0,
50518
+ { fidelity: "exact", sources: ["imported"] }
50519
+ );
50520
+ }
48827
50521
  let collectedSheetStock = [];
48828
50522
  let sheetStockCounter = 0;
48829
50523
  function resetSheetStock() {
@@ -261689,6 +263383,75 @@ function resolveErrorLocation(stack, compiledFiles) {
261689
263383
  column: parseInt(anonymousMatch[2], 10)
261690
263384
  };
261691
263385
  }
263386
+ function statementTargetVar(stmt) {
263387
+ if (typescriptExports.isVariableStatement(stmt)) {
263388
+ const decl = stmt.declarationList.declarations[0];
263389
+ if (decl && typescriptExports.isIdentifier(decl.name)) return decl.name.text;
263390
+ }
263391
+ if (typescriptExports.isExpressionStatement(stmt)) {
263392
+ const expr = stmt.expression;
263393
+ if (typescriptExports.isBinaryExpression(expr) && expr.operatorToken.kind === typescriptExports.SyntaxKind.EqualsToken && typescriptExports.isIdentifier(expr.left)) {
263394
+ return expr.left.text;
263395
+ }
263396
+ }
263397
+ return null;
263398
+ }
263399
+ function collectReferencedNames(node, exclude, names) {
263400
+ if (typescriptExports.isIdentifier(node) && !exclude.has(node)) {
263401
+ names.add(node.text);
263402
+ }
263403
+ typescriptExports.forEachChild(node, (child) => collectReferencedNames(child, exclude, names));
263404
+ }
263405
+ function extractUnusedTopLevelVarNames(code) {
263406
+ const sourceFile = typescriptExports.createSourceFile("__implicit.js", code, typescriptExports.ScriptTarget.ES2020, false, typescriptExports.ScriptKind.JS);
263407
+ const declaredNames = [];
263408
+ const collectBindingNames = (node) => {
263409
+ if (typescriptExports.isIdentifier(node)) {
263410
+ declaredNames.push(node.text);
263411
+ } else if (typescriptExports.isObjectBindingPattern(node) || typescriptExports.isArrayBindingPattern(node)) {
263412
+ for (const element of node.elements) {
263413
+ if (typescriptExports.isBindingElement(element)) {
263414
+ collectBindingNames(element.name);
263415
+ }
263416
+ }
263417
+ }
263418
+ };
263419
+ for (const statement of sourceFile.statements) {
263420
+ if (typescriptExports.isVariableStatement(statement)) {
263421
+ for (const decl of statement.declarationList.declarations) {
263422
+ collectBindingNames(decl.name);
263423
+ }
263424
+ } else if (typescriptExports.isFunctionDeclaration(statement) && statement.name) {
263425
+ declaredNames.push(statement.name.text);
263426
+ }
263427
+ }
263428
+ const excluded = /* @__PURE__ */ new Set(["exports", "module", "require", "__filename", "__dirname"]);
263429
+ const topLevelNames = new Set(declaredNames.filter((n) => !excluded.has(n)));
263430
+ if (topLevelNames.size === 0) return [];
263431
+ const usedByOthers = /* @__PURE__ */ new Set();
263432
+ for (const statement of sourceFile.statements) {
263433
+ const target = statementTargetVar(statement);
263434
+ const refs = /* @__PURE__ */ new Set();
263435
+ const lhsNodes = /* @__PURE__ */ new Set();
263436
+ if (typescriptExports.isVariableStatement(statement)) {
263437
+ for (const decl of statement.declarationList.declarations) {
263438
+ if (typescriptExports.isIdentifier(decl.name)) lhsNodes.add(decl.name);
263439
+ }
263440
+ } else if (typescriptExports.isExpressionStatement(statement)) {
263441
+ const expr = statement.expression;
263442
+ if (typescriptExports.isBinaryExpression(expr) && typescriptExports.isIdentifier(expr.left)) {
263443
+ lhsNodes.add(expr.left);
263444
+ }
263445
+ }
263446
+ collectReferencedNames(statement, lhsNodes, refs);
263447
+ for (const ref of refs) {
263448
+ if (topLevelNames.has(ref) && ref !== target) {
263449
+ usedByOthers.add(ref);
263450
+ }
263451
+ }
263452
+ }
263453
+ return declaredNames.filter((n) => topLevelNames.has(n) && !usedByOthers.has(n));
263454
+ }
261692
263455
  function createForgeRuntimeModule(bindings) {
261693
263456
  const runtime = { ...bindings };
261694
263457
  Object.defineProperty(runtime, "__esModule", { value: true });
@@ -261743,6 +263506,34 @@ function finalizeForgeJsImport(moduleExports, importedDims) {
261743
263506
  if (importedDims.length === 0) return base;
261744
263507
  return setShapeDimensions(base, [...getShapeDimensions(base), ...importedDims]);
261745
263508
  }
263509
+ function rejectPathTraversal(fnName, userPath, resolvedPath) {
263510
+ if (resolvedPath.startsWith("..")) {
263511
+ throw new Error(`${fnName}("${userPath}"): path traversal blocked — resolved path escapes the project directory`);
263512
+ }
263513
+ }
263514
+ let _constructorLockdownDepth = 0;
263515
+ let _origConstructorDescriptor;
263516
+ function withConstructorChainLockdown(fn) {
263517
+ _constructorLockdownDepth++;
263518
+ if (_constructorLockdownDepth === 1) {
263519
+ _origConstructorDescriptor = Object.getOwnPropertyDescriptor(Function.prototype, "constructor");
263520
+ Object.defineProperty(Function.prototype, "constructor", {
263521
+ get() {
263522
+ throw new Error("Dynamic code generation is not allowed in ForgeCAD scripts");
263523
+ },
263524
+ configurable: true
263525
+ });
263526
+ }
263527
+ try {
263528
+ return fn();
263529
+ } finally {
263530
+ _constructorLockdownDepth--;
263531
+ if (_constructorLockdownDepth === 0 && _origConstructorDescriptor) {
263532
+ Object.defineProperty(Function.prototype, "constructor", _origConstructorDescriptor);
263533
+ _origConstructorDescriptor = void 0;
263534
+ }
263535
+ }
263536
+ }
261746
263537
  function executeFile(code, fileName, allFiles, visited, scope = {}, options, executionMode = "script", moduleCacheEntry) {
261747
263538
  const trackCircularImports = executionMode === "script";
261748
263539
  if (trackCircularImports) {
@@ -261790,6 +263581,7 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
261790
263581
  throw new Error("importMesh() requires a non-empty file path string");
261791
263582
  }
261792
263583
  const resolvedPath = resolveImportPath(fileName, name.trim());
263584
+ rejectPathTraversal("importMesh", name, resolvedPath);
261793
263585
  const format = detectMeshFormat(resolvedPath);
261794
263586
  if (!format) {
261795
263587
  const ext = resolvedPath.split(".").pop() ?? "";
@@ -261819,6 +263611,25 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
261819
263611
  sources: ["imported"]
261820
263612
  });
261821
263613
  };
263614
+ const importStep = (name) => {
263615
+ var _a3;
263616
+ if (typeof name !== "string" || name.trim().length === 0) {
263617
+ throw new Error("importStep() requires a non-empty file path string");
263618
+ }
263619
+ const resolvedPath = resolveImportPath(fileName, name.trim());
263620
+ rejectPathTraversal("importStep", name, resolvedPath);
263621
+ const ext = ((_a3 = resolvedPath.split(".").pop()) == null ? void 0 : _a3.toLowerCase()) ?? "";
263622
+ if (ext !== "step" && ext !== "stp") {
263623
+ throw new Error(`importStep("${name}"): unsupported extension ".${ext}". Expected .step or .stp`);
263624
+ }
263625
+ if (!options.readBinaryFile) {
263626
+ throw new Error(
263627
+ `importStep("${name}"): binary file reading is not available in this environment. Provide a readBinaryFile callback in RunScriptOptions.`
263628
+ );
263629
+ }
263630
+ const fileData = options.readBinaryFile(resolvedPath);
263631
+ return importStepFromBuffer(fileData, name);
263632
+ };
261822
263633
  const wrappedUnion = union;
261823
263634
  const wrappedDifference = difference;
261824
263635
  const wrappedIntersection = intersection;
@@ -261882,6 +263693,10 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
261882
263693
  filletCorners,
261883
263694
  chamfer2d,
261884
263695
  Curve3D,
263696
+ NurbsCurve3D,
263697
+ nurbs3d,
263698
+ NurbsSurface,
263699
+ nurbsSurface,
261885
263700
  spline2d,
261886
263701
  spline3d,
261887
263702
  loft,
@@ -261895,10 +263710,13 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
261895
263710
  surfacePatch,
261896
263711
  sheetMetal,
261897
263712
  SheetMetalPart,
263713
+ Param,
261898
263714
  param,
261899
263715
  boolParam,
261900
263716
  choiceParam,
261901
- listParam,
263717
+ listParam: () => {
263718
+ throw new Error("listParam() has been renamed to Param.list(). Update your script.");
263719
+ },
261902
263720
  sdf,
261903
263721
  Shape,
261904
263722
  Sketch,
@@ -261928,6 +263746,7 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
261928
263746
  offsetSolid,
261929
263747
  importSvgSketch,
261930
263748
  importMesh,
263749
+ importStep,
261931
263750
  text2d,
261932
263751
  textWidth,
261933
263752
  loadFont,
@@ -261944,12 +263763,14 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
261944
263763
  ShapeGroup,
261945
263764
  console: sandboxConsole,
261946
263765
  cutPlane,
263766
+ cameraTrajectory,
261947
263767
  explodeView,
261948
263768
  jointsView,
261949
263769
  viewConfig,
261950
263770
  scene,
261951
263771
  verify,
261952
263772
  spec,
263773
+ mock,
261953
263774
  gcode,
261954
263775
  GCodeBuilder,
261955
263776
  // ── Laser Kit ──────────────────────────────────────────────────
@@ -261962,7 +263783,19 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
261962
263783
  assemblyInstructions,
261963
263784
  formatInstructions,
261964
263785
  lookupKerf,
261965
- COMMON_KERFS
263786
+ COMMON_KERFS,
263787
+ // ── Sandbox safety: shadow dangerous globals ───────────────────
263788
+ // These prevent user code from accessing escape vectors directly.
263789
+ // The constructor-chain lockdown (below) covers indirect access.
263790
+ Function: void 0,
263791
+ globalThis: void 0,
263792
+ global: void 0,
263793
+ self: void 0,
263794
+ window: void 0,
263795
+ setTimeout: void 0,
263796
+ setInterval: void 0,
263797
+ setImmediate: void 0,
263798
+ queueMicrotask: void 0
261966
263799
  };
261967
263800
  const requireModule = (requestedName, paramOverrides) => {
261968
263801
  if (typeof requestedName !== "string" || requestedName.trim().length === 0) {
@@ -262076,6 +263909,15 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
262076
263909
  const compiled = compileScript(code, fileName, options);
262077
263910
  const bindingNames = Object.keys(runtimeBindings);
262078
263911
  const bindingValues = bindingNames.map((name) => runtimeBindings[name]);
263912
+ let scriptCode = compiled.code;
263913
+ if (executionMode === "script") {
263914
+ const varNames = extractUnusedTopLevelVarNames(compiled.code).filter((n) => !bindingNames.includes(n));
263915
+ if (varNames.length > 0) {
263916
+ const collector = varNames.map((n) => `${JSON.stringify(n)}: ${n}`).join(", ");
263917
+ scriptCode += `
263918
+ ; try { module.__implicitVars = {${collector}}; } catch(e) {}`;
263919
+ }
263920
+ }
262079
263921
  const fn = new Function(
262080
263922
  "exports",
262081
263923
  "module",
@@ -262083,16 +263925,19 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
262083
263925
  "__filename",
262084
263926
  "__dirname",
262085
263927
  ...bindingNames,
262086
- `${compiled.code}
263928
+ `"use strict";
263929
+ ${scriptCode}
262087
263930
  //# sourceURL=${fileName}`
262088
263931
  );
262089
263932
  const moduleValue = {
262090
263933
  exports: executionMode === "module" && moduleCacheEntry ? moduleCacheEntry.exports : {}
262091
263934
  };
262092
263935
  const initialExportsRef = moduleValue.exports;
262093
- const returnValue = runWithParamScope(
262094
- scope,
262095
- () => fn(moduleValue.exports, moduleValue, requireModule, fileName, dirnamePath(fileName), ...bindingValues)
263936
+ const returnValue = withConstructorChainLockdown(
263937
+ () => runWithParamScope(
263938
+ scope,
263939
+ () => fn(moduleValue.exports, moduleValue, requireModule, fileName, dirnamePath(fileName), ...bindingValues)
263940
+ )
262096
263941
  );
262097
263942
  if (executionMode === "module") {
262098
263943
  const hasExports = hasExplicitModuleExports(moduleValue.exports, initialExportsRef);
@@ -262117,7 +263962,18 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
262117
263962
  }
262118
263963
  const exportedResult = resolveExportedEntryResult(moduleValue.exports);
262119
263964
  if (returnValue === void 0) {
262120
- return exportedResult ?? null;
263965
+ if (exportedResult != null) return exportedResult;
263966
+ const implicitVars = moduleValue.__implicitVars;
263967
+ if (implicitVars) {
263968
+ const renderables = {};
263969
+ for (const [key, value] of Object.entries(implicitVars)) {
263970
+ if (isRenderableEntryResult(value)) {
263971
+ renderables[key] = value;
263972
+ }
263973
+ }
263974
+ if (Object.keys(renderables).length > 0) return renderables;
263975
+ }
263976
+ return null;
262121
263977
  }
262122
263978
  return returnValue;
262123
263979
  } finally {
@@ -262154,12 +264010,15 @@ function runScript(code, fileName = "main.forge.js", allFiles = {}, options = {}
262154
264010
  resetSheetStock();
262155
264011
  resetRobotExport();
262156
264012
  resetCutPlanes();
264013
+ resetCameraTrajectory();
262157
264014
  resetExplodeView();
262158
264015
  resetJointsView();
262159
264016
  resetViewConfig();
262160
264017
  resetScene();
262161
264018
  resetVerifications();
264019
+ resetMocks();
262162
264020
  _collectedLogs = [];
264021
+ setRuntimeWarnSink((msg) => _collectedLogs.push({ level: "warn", args: [msg], timestamp: Date.now() }));
262163
264022
  const t0 = performance.now();
262164
264023
  const execOptions = {
262165
264024
  debugImports: options.debugImports ?? envFlagEnabled("FORGECAD_DEBUG_IMPORTS"),
@@ -262399,7 +264258,19 @@ function runScript(code, fileName = "main.forge.js", allFiles = {}, options = {}
262399
264258
  } else {
262400
264259
  const entries = Object.entries(obj);
262401
264260
  entries.forEach(([key, value]) => {
262402
- if (value instanceof Shape) {
264261
+ if (value instanceof Assembly) {
264262
+ const items = value.solve().toSceneObjects();
264263
+ items.forEach((item, index) => {
264264
+ const label = `${key}.${index + 1}`;
264265
+ processNamedItem(item, label, label);
264266
+ });
264267
+ } else if (value instanceof SolvedAssembly) {
264268
+ const items = value.toSceneObjects();
264269
+ items.forEach((item, index) => {
264270
+ const label = `${key}.${index + 1}`;
264271
+ processNamedItem(item, label, label);
264272
+ });
264273
+ } else if (value instanceof Shape) {
262403
264274
  pushShape(value, key, void 0, void 0, void 0, [key]);
262404
264275
  } else if (value instanceof Sketch) {
262405
264276
  pushSketch(value, key, void 0, [key]);
@@ -262407,6 +264278,21 @@ function runScript(code, fileName = "main.forge.js", allFiles = {}, options = {}
262407
264278
  value.children.forEach((child, i) => {
262408
264279
  flattenGroupChild(child, groupChildLabel(value, key, i), void 0, [key, shapeGroupChildSegment(value, i)]);
262409
264280
  });
264281
+ } else if (Array.isArray(value)) {
264282
+ value.forEach((item, index) => {
264283
+ const label = `${key}.${index + 1}`;
264284
+ if (item instanceof ShapeGroup) {
264285
+ item.children.forEach((child, i) => {
264286
+ flattenGroupChild(child, groupChildLabel(item, label, i), void 0, [key, label, shapeGroupChildSegment(item, i)]);
264287
+ });
264288
+ } else if (item instanceof Shape) {
264289
+ pushShape(item, label, void 0, void 0, void 0, [key, label]);
264290
+ } else if (item instanceof Sketch) {
264291
+ pushSketch(item, label, void 0, [key, label]);
264292
+ } else if (isNamedObject(item)) {
264293
+ processNamedItem(item, label, label, void 0, [key]);
264294
+ }
264295
+ });
262410
264296
  }
262411
264297
  });
262412
264298
  }
@@ -262435,12 +264321,23 @@ function runScript(code, fileName = "main.forge.js", allFiles = {}, options = {}
262435
264321
  }
262436
264322
  const shape = objects.length === 1 ? objects[0].shape : null;
262437
264323
  const sketch = objects.length === 1 ? objects[0].sketch : null;
264324
+ const collectedMocks = getCollectedMocks();
264325
+ for (const m of collectedMocks) {
264326
+ objects.push({
264327
+ id: m.id,
264328
+ name: `${m.name} (mock)`,
264329
+ shape: m.shape,
264330
+ sketch: null,
264331
+ mock: true
264332
+ });
264333
+ }
262438
264334
  autoFillExplodeHints(objects);
262439
264335
  return {
262440
264336
  shape,
262441
264337
  sketch,
262442
264338
  objects,
262443
264339
  params: getCollectedParams(),
264340
+ stringParams: getCollectedStringParams(),
262444
264341
  listParams: getCollectedListParams(),
262445
264342
  dimensions: [...getCollectedDimensions(), ...shapeDimensions],
262446
264343
  highlights: getCollectedHighlights(),
@@ -262448,6 +264345,7 @@ function runScript(code, fileName = "main.forge.js", allFiles = {}, options = {}
262448
264345
  bom: getCollectedBom(),
262449
264346
  sheetStock: getCollectedSheetStock(),
262450
264347
  cutPlanes: getCollectedCutPlanes(),
264348
+ cameraTrajectory: getCollectedCameraTrajectory(),
262451
264349
  explodeView: getCollectedExplodeView(),
262452
264350
  jointsView: getCollectedJointsView(),
262453
264351
  viewConfig: getCollectedViewConfig(),
@@ -262457,7 +264355,8 @@ function runScript(code, fileName = "main.forge.js", allFiles = {}, options = {}
262457
264355
  error: objects.length > 0 || options.allowEmptyResult ? null : "Script must return a Shape or Sketch",
262458
264356
  timeMs: performance.now() - t0,
262459
264357
  logs: _collectedLogs.slice(),
262460
- verifications: getCollectedVerifications()
264358
+ verifications: getCollectedVerifications(),
264359
+ mocks: getCollectedMocks()
262461
264360
  };
262462
264361
  });
262463
264362
  } catch (e) {
@@ -262474,6 +264373,7 @@ function runScript(code, fileName = "main.forge.js", allFiles = {}, options = {}
262474
264373
  sketch: null,
262475
264374
  objects: [],
262476
264375
  params: getCollectedParams(),
264376
+ stringParams: getCollectedStringParams(),
262477
264377
  listParams: getCollectedListParams(),
262478
264378
  dimensions: getCollectedDimensions(),
262479
264379
  highlights: getCollectedHighlights(),
@@ -262481,6 +264381,7 @@ function runScript(code, fileName = "main.forge.js", allFiles = {}, options = {}
262481
264381
  bom: getCollectedBom(),
262482
264382
  sheetStock: getCollectedSheetStock(),
262483
264383
  cutPlanes: getCollectedCutPlanes(),
264384
+ cameraTrajectory: getCollectedCameraTrajectory(),
262484
264385
  explodeView: getCollectedExplodeView(),
262485
264386
  jointsView: getCollectedJointsView(),
262486
264387
  viewConfig: getCollectedViewConfig(),
@@ -262490,7 +264391,8 @@ function runScript(code, fileName = "main.forge.js", allFiles = {}, options = {}
262490
264391
  error: `${msg}${lineInfo}`,
262491
264392
  timeMs: performance.now() - t0,
262492
264393
  logs: _collectedLogs.slice(),
262493
- verifications: getCollectedVerifications()
264394
+ verifications: getCollectedVerifications(),
264395
+ mocks: getCollectedMocks()
262494
264396
  };
262495
264397
  }
262496
264398
  }
@@ -263061,6 +264963,19 @@ function isWasmCrash(error) {
263061
264963
  const msg = error instanceof Error ? error.message : String(error);
263062
264964
  return /abort|unreachable|out of bounds|out of memory|OOM/i.test(msg);
263063
264965
  }
264966
+ function disposeRunResult(runResult) {
264967
+ if (!runResult) return;
264968
+ const seen = /* @__PURE__ */ new Set();
264969
+ const disposeShape = (shape) => {
264970
+ if (!shape) return;
264971
+ const backend = getShapeRuntimeBackend(shape);
264972
+ if (seen.has(backend)) return;
264973
+ seen.add(backend);
264974
+ disposeShapeBackend(backend);
264975
+ };
264976
+ disposeShape(runResult.shape);
264977
+ runResult.objects.forEach((obj) => disposeShape(obj.shape));
264978
+ }
263064
264979
  async function runOnce(payload) {
263065
264980
  const { seq, code, file, files, quality, paramOverrides, activeBackend } = payload;
263066
264981
  try {
@@ -263078,6 +264993,7 @@ async function runOnce(payload) {
263078
264993
  console.log(`[worker] seq=${seq} stale (newer queued) — skipping serialize. run=${(tRun - tKernel).toFixed(0)}ms`);
263079
264994
  return;
263080
264995
  }
264996
+ disposeRunResult(lastRunResult);
263081
264997
  lastRunResult = runResult;
263082
264998
  worker.postMessage({ type: "progress", payload: { seq, phase: "serializing" } });
263083
264999
  const { serialized, transferables } = serializeRunResult(runResult, getSolverWasmRunDebugSnapshot());
@@ -263085,7 +265001,21 @@ async function runOnce(payload) {
263085
265001
  console.log(
263086
265002
  `[worker] seq=${seq} kernelInit=${(tKernel - t0).toFixed(0)}ms run=${(tRun - tKernel).toFixed(0)}ms serialize=${(tSerialize - tRun).toFixed(0)}ms total=${(tSerialize - t0).toFixed(0)}ms`
263087
265003
  );
263088
- worker.postMessage({ type: "run-success", payload: { seq, result: serialized } }, transferables);
265004
+ worker.postMessage(
265005
+ {
265006
+ type: "run-success",
265007
+ payload: {
265008
+ seq,
265009
+ result: serialized,
265010
+ wasmHeap: {
265011
+ manifoldBytes: getWasmHeapBytes(),
265012
+ solverBytes: getSolverWasmHeapBytes(),
265013
+ occtBytes: getOcctHeapBytes()
265014
+ }
265015
+ }
265016
+ },
265017
+ transferables
265018
+ );
263089
265019
  } catch (error) {
263090
265020
  const fatal = isWasmCrash(error);
263091
265021
  const message = error instanceof Error ? error.message : String(error);
@@ -263108,7 +265038,9 @@ async function handleExportExact(data) {
263108
265038
  resetSolverWasmStats();
263109
265039
  setParamOverrides(paramOverrides);
263110
265040
  await activateBackend("occt");
263111
- lastRunResult = runScript(code, file, files, { quality, readBinaryFile });
265041
+ const nextRunResult = runScript(code, file, files, { quality, readBinaryFile });
265042
+ disposeRunResult(lastRunResult);
265043
+ lastRunResult = nextRunResult;
263112
265044
  if (lastRunResult.error) {
263113
265045
  throw new Error(`Script has errors: ${lastRunResult.error}`);
263114
265046
  }