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,184 @@ 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 findSpan(n, degree, u, knots) {
2482
+ if (u >= knots[n]) return n - 1;
2483
+ if (u <= knots[degree]) return degree;
2484
+ let lo = degree;
2485
+ let hi = n;
2486
+ let mid = lo + hi >>> 1;
2487
+ while (u < knots[mid] || u >= knots[mid + 1]) {
2488
+ if (u < knots[mid]) hi = mid;
2489
+ else lo = mid;
2490
+ mid = lo + hi >>> 1;
2491
+ }
2492
+ return mid;
2493
+ }
2494
+ function basisFuns(span, u, degree, knots) {
2495
+ const N = new Array(degree + 1);
2496
+ const left = new Array(degree + 1);
2497
+ const right = new Array(degree + 1);
2498
+ N[0] = 1;
2499
+ for (let j = 1; j <= degree; j++) {
2500
+ left[j] = u - knots[span + 1 - j];
2501
+ right[j] = knots[span + j] - u;
2502
+ let saved = 0;
2503
+ for (let r = 0; r < j; r++) {
2504
+ const denom = right[r + 1] + left[j - r];
2505
+ const temp = denom === 0 ? 0 : N[r] / denom;
2506
+ N[r] = saved + right[r + 1] * temp;
2507
+ saved = left[j - r] * temp;
2508
+ }
2509
+ N[j] = saved;
2510
+ }
2511
+ return N;
2512
+ }
2513
+ function basisFunsDeriv(span, u, degree, knots, nDeriv) {
2514
+ const ndu = Array.from({ length: degree + 1 }, () => new Array(degree + 1).fill(0));
2515
+ const a = Array.from({ length: 2 }, () => new Array(degree + 1).fill(0));
2516
+ const left = new Array(degree + 1);
2517
+ const right = new Array(degree + 1);
2518
+ ndu[0][0] = 1;
2519
+ for (let j = 1; j <= degree; j++) {
2520
+ left[j] = u - knots[span + 1 - j];
2521
+ right[j] = knots[span + j] - u;
2522
+ let saved = 0;
2523
+ for (let r2 = 0; r2 < j; r2++) {
2524
+ ndu[j][r2] = right[r2 + 1] + left[j - r2];
2525
+ const temp = ndu[j][r2] === 0 ? 0 : ndu[r2][j - 1] / ndu[j][r2];
2526
+ ndu[r2][j] = saved + right[r2 + 1] * temp;
2527
+ saved = left[j - r2] * temp;
2528
+ }
2529
+ ndu[j][j] = saved;
2530
+ }
2531
+ const ders = Array.from({ length: nDeriv + 1 }, () => new Array(degree + 1).fill(0));
2532
+ for (let j = 0; j <= degree; j++) {
2533
+ ders[0][j] = ndu[j][degree];
2534
+ }
2535
+ for (let r2 = 0; r2 <= degree; r2++) {
2536
+ let s1 = 0;
2537
+ let s2 = 1;
2538
+ a[0][0] = 1;
2539
+ for (let k = 1; k <= nDeriv; k++) {
2540
+ let d = 0;
2541
+ const rk = r2 - k;
2542
+ const pk = degree - k;
2543
+ if (r2 >= k) {
2544
+ a[s2][0] = ndu[pk + 1][rk] === 0 ? 0 : a[s1][0] / ndu[pk + 1][rk];
2545
+ d = a[s2][0] * ndu[rk][pk];
2546
+ }
2547
+ const j1 = rk >= -1 ? 1 : -rk;
2548
+ const j2 = r2 - 1 <= pk ? k - 1 : degree - r2;
2549
+ for (let j = j1; j <= j2; j++) {
2550
+ a[s2][j] = ndu[pk + 1][rk + j] === 0 ? 0 : (a[s1][j] - a[s1][j - 1]) / ndu[pk + 1][rk + j];
2551
+ d += a[s2][j] * ndu[rk + j][pk];
2552
+ }
2553
+ if (r2 <= pk) {
2554
+ a[s2][k] = ndu[pk + 1][r2] === 0 ? 0 : -a[s1][k - 1] / ndu[pk + 1][r2];
2555
+ d += a[s2][k] * ndu[r2][pk];
2556
+ }
2557
+ ders[k][r2] = d;
2558
+ const tmp = s1;
2559
+ s1 = s2;
2560
+ s2 = tmp;
2561
+ }
2562
+ }
2563
+ let r = degree;
2564
+ for (let k = 1; k <= nDeriv; k++) {
2565
+ for (let j = 0; j <= degree; j++) {
2566
+ ders[k][j] *= r;
2567
+ }
2568
+ r *= degree - k;
2569
+ }
2570
+ return ders;
2571
+ }
2572
+ function deBoor3D(controlPoints, weights, knots, degree, u) {
2573
+ const n = controlPoints.length;
2574
+ const span = findSpan(n, degree, u, knots);
2575
+ const N = basisFuns(span, u, degree, knots);
2576
+ let wx = 0, wy = 0, wz = 0, wSum = 0;
2577
+ for (let j = 0; j <= degree; j++) {
2578
+ const idx = span - degree + j;
2579
+ const w = weights[idx] * N[j];
2580
+ wx += w * controlPoints[idx][0];
2581
+ wy += w * controlPoints[idx][1];
2582
+ wz += w * controlPoints[idx][2];
2583
+ wSum += w;
2584
+ }
2585
+ if (wSum === 0) return [0, 0, 0];
2586
+ return [wx / wSum, wy / wSum, wz / wSum];
2587
+ }
2588
+ function deBoor3DDeriv(controlPoints, weights, knots, degree, u) {
2589
+ const n = controlPoints.length;
2590
+ const span = findSpan(n, degree, u, knots);
2591
+ const ders = basisFunsDeriv(span, u, degree, knots, 1);
2592
+ const N = ders[0];
2593
+ const dN = ders[1];
2594
+ let ax = 0, ay = 0, az = 0, wVal = 0;
2595
+ let dax = 0, day = 0, daz = 0, dwVal = 0;
2596
+ for (let j = 0; j <= degree; j++) {
2597
+ const idx = span - degree + j;
2598
+ const wi = weights[idx];
2599
+ const px = controlPoints[idx][0], py = controlPoints[idx][1], pz = controlPoints[idx][2];
2600
+ ax += N[j] * wi * px;
2601
+ ay += N[j] * wi * py;
2602
+ az += N[j] * wi * pz;
2603
+ wVal += N[j] * wi;
2604
+ dax += dN[j] * wi * px;
2605
+ day += dN[j] * wi * py;
2606
+ daz += dN[j] * wi * pz;
2607
+ dwVal += dN[j] * wi;
2608
+ }
2609
+ if (wVal === 0) return [0, 0, 0];
2610
+ const invW = 1 / wVal;
2611
+ return [
2612
+ (dax - dwVal * ax * invW) * invW,
2613
+ (day - dwVal * ay * invW) * invW,
2614
+ (daz - dwVal * az * invW) * invW
2615
+ ];
2616
+ }
2617
+ function deBoor2D(controlPoints, weights, knots, degree, u) {
2618
+ const n = controlPoints.length;
2619
+ const span = findSpan(n, degree, u, knots);
2620
+ const N = basisFuns(span, u, degree, knots);
2621
+ let wx = 0, wy = 0, wSum = 0;
2622
+ for (let j = 0; j <= degree; j++) {
2623
+ const idx = span - degree + j;
2624
+ const w = weights[idx] * N[j];
2625
+ wx += w * controlPoints[idx][0];
2626
+ wy += w * controlPoints[idx][1];
2627
+ wSum += w;
2628
+ }
2629
+ if (wSum === 0) return [0, 0];
2630
+ return [wx / wSum, wy / wSum];
2631
+ }
2632
+ function sampleNurbs3D(controlPoints, weights, knots, degree, count) {
2633
+ const n = controlPoints.length;
2634
+ const uMin = knots[degree];
2635
+ const uMax = knots[n];
2636
+ const result = new Array(count);
2637
+ for (let i = 0; i < count; i++) {
2638
+ const t = i / (count - 1);
2639
+ const u = uMin + t * (uMax - uMin);
2640
+ result[i] = deBoor3D(controlPoints, weights, knots, degree, u);
2641
+ }
2642
+ return result;
2643
+ }
2644
+ function remapToKnotDomain(t, n, degree, knots) {
2645
+ const uMin = knots[degree];
2646
+ const uMax = knots[n];
2647
+ return uMin + Math.max(0, Math.min(1, t)) * (uMax - uMin);
2648
+ }
2413
2649
  function catmullRom3D$2(p0, p1, p2, p3, t, tension) {
2414
2650
  const tt = t * t;
2415
2651
  const ttt = tt * t;
@@ -2544,6 +2780,10 @@ function evalPathAt(path2, t) {
2544
2780
  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
2781
  ];
2546
2782
  }
2783
+ case "nurbs": {
2784
+ const u = remapToKnotDomain(Math.max(0, Math.min(1, t)), path2.controlPoints.length, path2.degree, path2.knots);
2785
+ return deBoor3D(path2.controlPoints, path2.weights, path2.knots, path2.degree, u);
2786
+ }
2547
2787
  }
2548
2788
  }
2549
2789
  function estimateCurvatureAt(path2, t) {
@@ -2578,6 +2818,18 @@ function sweepPathToPolyline(path2, samples = 48) {
2578
2818
  path2.c1,
2579
2819
  samples
2580
2820
  );
2821
+ case "nurbs": {
2822
+ const n = path2.controlPoints.length;
2823
+ const uMin = path2.knots[path2.degree];
2824
+ const uMax = path2.knots[n];
2825
+ const result = new Array(samples);
2826
+ for (let i = 0; i < samples; i++) {
2827
+ const t = i / (samples - 1);
2828
+ const u = uMin + t * (uMax - uMin);
2829
+ result[i] = deBoor3D(path2.controlPoints, path2.weights, path2.knots, path2.degree, u);
2830
+ }
2831
+ return result;
2832
+ }
2581
2833
  }
2582
2834
  }
2583
2835
  function sweepPathToPolylineAdaptive(path2, baseSamples = 48) {
@@ -2811,6 +3063,8 @@ function searchOwnerMatch(plan, owner) {
2811
3063
  case "importedMesh":
2812
3064
  case "sdf":
2813
3065
  case "fromSlices":
3066
+ case "nurbsSurface":
3067
+ case "importedStep":
2814
3068
  return {
2815
3069
  issue: {
2816
3070
  code: "edge-owner-not-found",
@@ -2892,7 +3146,7 @@ function propagateCandidateAcrossRewrite(plan, candidate) {
2892
3146
  return edgeSuccess(candidate.selection, preservedEntry.query);
2893
3147
  }
2894
3148
  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") {
3149
+ 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
3150
  return edgeIssue(
2897
3151
  "edge-query-propagation-mismatch",
2898
3152
  "The selected propagated edge query does not point at a topology-rewrite result on this target shape."
@@ -3118,6 +3372,8 @@ function resolveSelectionFromOwnerBase(plan, edgeName) {
3118
3372
  case "importedMesh":
3119
3373
  case "sdf":
3120
3374
  case "fromSlices":
3375
+ case "nurbsSurface":
3376
+ case "importedStep":
3121
3377
  return edgeIssue(
3122
3378
  "unsupported-edge-base",
3123
3379
  "Edge finishing v1 currently supports tracked vertical edges from compile-covered box() bodies and rectangle extrusions before topology-changing edits."
@@ -3152,7 +3408,7 @@ function resolveSupportedEdgeFeatureSelection(plan, ref) {
3152
3408
  return edgeIssue("edge-query-unsupported-after-rewrite", descendant.reason);
3153
3409
  }
3154
3410
  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") {
3411
+ 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
3412
  return {
3157
3413
  kind: "unsupported",
3158
3414
  query: cloneEdgeQueryRef(ref),
@@ -3185,7 +3441,7 @@ function resolveEdgeChainAtOwnerBase(ownerBase, ref) {
3185
3441
  };
3186
3442
  }
3187
3443
  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") {
3444
+ 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
3445
  return {
3190
3446
  kind: "unsupported",
3191
3447
  query: cloneEdgeQueryRef(ref),
@@ -4678,6 +4934,8 @@ function lowerBaseShellPlanToConcretePlan(plan, thickness, openFaces) {
4678
4934
  case "importedMesh":
4679
4935
  case "sdf":
4680
4936
  case "fromSlices":
4937
+ case "nurbsSurface":
4938
+ case "importedStep":
4681
4939
  return {
4682
4940
  ok: false,
4683
4941
  reason: `Shape.shell() supports box(), cylinder(), straight extrude(), loft(), sweep(), and variableSweep() bases. "${plan.kind}" bases are not supported.`
@@ -5016,7 +5274,7 @@ const defaultPerm = doublePerm(p);
5016
5274
  const defaultPermMod12 = permMod12(defaultPerm);
5017
5275
  const F3 = 1 / 3;
5018
5276
  const G3 = 1 / 6;
5019
- function dot3$5(gi, x, y, z) {
5277
+ function dot3$6(gi, x, y, z) {
5020
5278
  const o = gi * 3;
5021
5279
  return grad3[o] * x + grad3[o + 1] * y + grad3[o + 2] * z;
5022
5280
  }
@@ -5126,28 +5384,28 @@ function simplex3Core(x, y, z, perm, pm12) {
5126
5384
  n0 = 0;
5127
5385
  } else {
5128
5386
  t0 *= t0;
5129
- n0 = t0 * t0 * dot3$5(gi0, x0, y0, z0);
5387
+ n0 = t0 * t0 * dot3$6(gi0, x0, y0, z0);
5130
5388
  }
5131
5389
  let t1 = 0.6 - x1 * x1 - y1 * y1 - z1 * z1;
5132
5390
  if (t1 < 0) {
5133
5391
  n1 = 0;
5134
5392
  } else {
5135
5393
  t1 *= t1;
5136
- n1 = t1 * t1 * dot3$5(gi1, x1, y1, z1);
5394
+ n1 = t1 * t1 * dot3$6(gi1, x1, y1, z1);
5137
5395
  }
5138
5396
  let t2 = 0.6 - x2 * x2 - y2 * y2 - z2 * z2;
5139
5397
  if (t2 < 0) {
5140
5398
  n2 = 0;
5141
5399
  } else {
5142
5400
  t2 *= t2;
5143
- n2 = t2 * t2 * dot3$5(gi2, x2, y2, z2);
5401
+ n2 = t2 * t2 * dot3$6(gi2, x2, y2, z2);
5144
5402
  }
5145
5403
  let t3 = 0.6 - x3 * x3 - y3 * y3 - z3 * z3;
5146
5404
  if (t3 < 0) {
5147
5405
  n3 = 0;
5148
5406
  } else {
5149
5407
  t3 *= t3;
5150
- n3 = t3 * t3 * dot3$5(gi3, x3, y3, z3);
5408
+ n3 = t3 * t3 * dot3$6(gi3, x3, y3, z3);
5151
5409
  }
5152
5410
  return 32 * (n0 + n1 + n2 + n3);
5153
5411
  }
@@ -5911,6 +6169,158 @@ function padBounds(b, pad) {
5911
6169
  max: [b.max[0] + pad, b.max[1] + pad, b.max[2] + pad]
5912
6170
  };
5913
6171
  }
6172
+ function requireFinite$6(v, label) {
6173
+ if (!Number.isFinite(v)) throw new Error(`nurbsSurface: ${label} must be finite, got ${v}`);
6174
+ }
6175
+ class NurbsSurface {
6176
+ // columns in control grid
6177
+ constructor(controlGrid, options = {}) {
6178
+ __publicField(this, "controlGrid");
6179
+ __publicField(this, "weightsGrid");
6180
+ __publicField(this, "knotsU");
6181
+ __publicField(this, "knotsV");
6182
+ __publicField(this, "degreeU");
6183
+ __publicField(this, "degreeV");
6184
+ __publicField(this, "nU");
6185
+ // rows in control grid
6186
+ __publicField(this, "nV");
6187
+ const nU = controlGrid.length;
6188
+ if (nU < 2) throw new Error("nurbsSurface: controlGrid must have at least 2 rows");
6189
+ const nV = controlGrid[0].length;
6190
+ if (nV < 2) throw new Error("nurbsSurface: controlGrid must have at least 2 columns");
6191
+ const degreeU = options.degreeU ?? Math.min(nU - 1, 3);
6192
+ const degreeV = options.degreeV ?? Math.min(nV - 1, 3);
6193
+ if (nU < degreeU + 1) throw new Error(`nurbsSurface: need at least ${degreeU + 1} rows for degreeU=${degreeU}, got ${nU}`);
6194
+ if (nV < degreeV + 1) throw new Error(`nurbsSurface: need at least ${degreeV + 1} columns for degreeV=${degreeV}, got ${nV}`);
6195
+ for (let i = 0; i < nU; i++) {
6196
+ if (controlGrid[i].length !== nV) throw new Error(`nurbsSurface: row ${i} has ${controlGrid[i].length} points, expected ${nV}`);
6197
+ for (let j = 0; j < nV; j++) {
6198
+ requireFinite$6(controlGrid[i][j][0], `controlGrid[${i}][${j}][0]`);
6199
+ requireFinite$6(controlGrid[i][j][1], `controlGrid[${i}][${j}][1]`);
6200
+ requireFinite$6(controlGrid[i][j][2], `controlGrid[${i}][${j}][2]`);
6201
+ }
6202
+ }
6203
+ const weightsGrid = options.weights ?? controlGrid.map((row) => row.map(() => 1));
6204
+ for (let i = 0; i < nU; i++) {
6205
+ if (weightsGrid[i].length !== nV) throw new Error(`nurbsSurface: weights row ${i} length mismatch`);
6206
+ for (let j = 0; j < nV; j++) {
6207
+ requireFinite$6(weightsGrid[i][j], `weights[${i}][${j}]`);
6208
+ if (weightsGrid[i][j] <= 0) throw new Error(`nurbsSurface: weights[${i}][${j}] must be > 0`);
6209
+ }
6210
+ }
6211
+ const knotsU = options.knotsU ?? generateClampedKnots(nU, degreeU);
6212
+ const knotsV = options.knotsV ?? generateClampedKnots(nV, degreeV);
6213
+ if (knotsU.length !== nU + degreeU + 1) throw new Error(`nurbsSurface: knotsU.length should be ${nU + degreeU + 1}, got ${knotsU.length}`);
6214
+ if (knotsV.length !== nV + degreeV + 1) throw new Error(`nurbsSurface: knotsV.length should be ${nV + degreeV + 1}, got ${knotsV.length}`);
6215
+ this.controlGrid = controlGrid.map((row) => row.map(([x, y, z]) => [x, y, z]));
6216
+ this.weightsGrid = weightsGrid.map((row) => [...row]);
6217
+ this.knotsU = [...knotsU];
6218
+ this.knotsV = [...knotsV];
6219
+ this.degreeU = degreeU;
6220
+ this.degreeV = degreeV;
6221
+ this.nU = nU;
6222
+ this.nV = nV;
6223
+ }
6224
+ /**
6225
+ * Evaluate the surface at parameters (u, v) ∈ [0, 1]².
6226
+ * Uses tensor product evaluation: evaluate basis functions in U and V independently.
6227
+ */
6228
+ pointAt(u, v) {
6229
+ const uu = this.remapU(Math.max(0, Math.min(1, u)));
6230
+ const vv = this.remapV(Math.max(0, Math.min(1, v)));
6231
+ const spanU = findSpan(this.nU, this.degreeU, uu, this.knotsU);
6232
+ const spanV = findSpan(this.nV, this.degreeV, vv, this.knotsV);
6233
+ const Nu = basisFuns(spanU, uu, this.degreeU, this.knotsU);
6234
+ const Nv = basisFuns(spanV, vv, this.degreeV, this.knotsV);
6235
+ let wx = 0, wy = 0, wz = 0, wSum = 0;
6236
+ for (let i = 0; i <= this.degreeU; i++) {
6237
+ const rowIdx = spanU - this.degreeU + i;
6238
+ for (let j = 0; j <= this.degreeV; j++) {
6239
+ const colIdx = spanV - this.degreeV + j;
6240
+ const w = Nu[i] * Nv[j] * this.weightsGrid[rowIdx][colIdx];
6241
+ const pt = this.controlGrid[rowIdx][colIdx];
6242
+ wx += w * pt[0];
6243
+ wy += w * pt[1];
6244
+ wz += w * pt[2];
6245
+ wSum += w;
6246
+ }
6247
+ }
6248
+ if (wSum === 0) return [0, 0, 0];
6249
+ return [wx / wSum, wy / wSum, wz / wSum];
6250
+ }
6251
+ /**
6252
+ * Evaluate the surface normal at (u, v) via cross product of partial derivatives.
6253
+ */
6254
+ normalAt(u, v) {
6255
+ const eps = 1e-5;
6256
+ const u0 = Math.max(0, u - eps), u1 = Math.min(1, u + eps);
6257
+ const v0 = Math.max(0, v - eps), v1 = Math.min(1, v + eps);
6258
+ const pu = this.pointAt(u1, v), pmu = this.pointAt(u0, v);
6259
+ const pv = this.pointAt(u, v1), pmv = this.pointAt(u, v0);
6260
+ const du = [pu[0] - pmu[0], pu[1] - pmu[1], pu[2] - pmu[2]];
6261
+ const dv = [pv[0] - pmv[0], pv[1] - pmv[1], pv[2] - pmv[2]];
6262
+ const nx = du[1] * dv[2] - du[2] * dv[1];
6263
+ const ny = du[2] * dv[0] - du[0] * dv[2];
6264
+ const nz = du[0] * dv[1] - du[1] * dv[0];
6265
+ const len = Math.sqrt(nx * nx + ny * ny + nz * nz);
6266
+ if (len < 1e-12) return [0, 0, 1];
6267
+ return [nx / len, ny / len, nz / len];
6268
+ }
6269
+ /**
6270
+ * Tessellate the surface into a triangle mesh.
6271
+ * Returns positions, normals, and triangle indices.
6272
+ */
6273
+ tessellate(resU = 32, resV = 32) {
6274
+ const positions = [];
6275
+ const normals = [];
6276
+ for (let i = 0; i <= resU; i++) {
6277
+ const u = i / resU;
6278
+ for (let j = 0; j <= resV; j++) {
6279
+ const v = j / resV;
6280
+ positions.push(this.pointAt(u, v));
6281
+ normals.push(this.normalAt(u, v));
6282
+ }
6283
+ }
6284
+ const indices = [];
6285
+ for (let i = 0; i < resU; i++) {
6286
+ for (let j = 0; j < resV; j++) {
6287
+ const a = i * (resV + 1) + j;
6288
+ const b = a + 1;
6289
+ const c = (i + 1) * (resV + 1) + j;
6290
+ const d = c + 1;
6291
+ indices.push(a, c, b);
6292
+ indices.push(b, c, d);
6293
+ }
6294
+ }
6295
+ return { positions, normals, indices };
6296
+ }
6297
+ remapU(t) {
6298
+ return this.knotsU[this.degreeU] + t * (this.knotsU[this.nU] - this.knotsU[this.degreeU]);
6299
+ }
6300
+ remapV(t) {
6301
+ return this.knotsV[this.degreeV] + t * (this.knotsV[this.nV] - this.knotsV[this.degreeV]);
6302
+ }
6303
+ }
6304
+ function nurbsSurface(controlGrid, options) {
6305
+ const surface = new NurbsSurface(controlGrid, options);
6306
+ const thickness = (options == null ? void 0 : options.thickness) ?? 1;
6307
+ const resolution = (options == null ? void 0 : options.resolution) ?? 32;
6308
+ return buildShapeFromCompilePlan(
6309
+ createOwnedShapeCompilePlan({
6310
+ kind: "nurbsSurface",
6311
+ controlGrid: surface.controlGrid.map(
6312
+ (row) => row.map(([x, y, z]) => [x, y, z])
6313
+ ),
6314
+ weightsGrid: surface.weightsGrid.map((row) => [...row]),
6315
+ knotsU: [...surface.knotsU],
6316
+ knotsV: [...surface.knotsV],
6317
+ degreeU: surface.degreeU,
6318
+ degreeV: surface.degreeV,
6319
+ thickness,
6320
+ resolution
6321
+ }, "nurbsSurface")
6322
+ );
6323
+ }
5914
6324
  let _simplifier = null;
5915
6325
  async function initMeshoptimizer() {
5916
6326
  if (_simplifier) return;
@@ -6314,6 +6724,8 @@ function sampleSweepPath(path2, edgeLengthHint) {
6314
6724
  return sweepPathToPolyline(path2, resolveCurveSampleCount(edgeLengthHint, path2.chordLength));
6315
6725
  case "quintic-hermite":
6316
6726
  return sweepPathToPolyline(path2, resolveCurveSampleCount(edgeLengthHint, path2.chordLength));
6727
+ case "nurbs":
6728
+ return sweepPathToPolyline(path2, edgeLengthHint != null ? Math.max(16, Math.round(path2.controlPoints.length * 12 / Math.max(0.1, edgeLengthHint))) : 48);
6317
6729
  }
6318
6730
  }
6319
6731
  function clamp$7(v, lo, hi) {
@@ -7173,7 +7585,7 @@ let _wasm = null;
7173
7585
  async function initManifoldWasm() {
7174
7586
  if (_wasm) return _wasm;
7175
7587
  performance.mark("manifold:start");
7176
- const Module = (await import("./manifold-D7o0N50J.js")).default;
7588
+ const Module = (await import("./manifold-DBckbFgx.js")).default;
7177
7589
  performance.mark("manifold:imported");
7178
7590
  const wasm = await Module();
7179
7591
  wasm.setup();
@@ -7195,74 +7607,105 @@ function isManifoldCapableBackend(b) {
7195
7607
  }
7196
7608
  _a2 = SHAPE_BACKEND_MARKER;
7197
7609
  const _ManifoldShapeBackend = class _ManifoldShapeBackend {
7198
- constructor(manifold) {
7610
+ constructor(manifoldOrResource) {
7199
7611
  __publicField(this, _a2, true);
7200
- this.manifold = manifold;
7612
+ __publicField(this, "resource");
7613
+ __publicField(this, "released", false);
7614
+ this.resource = "refCount" in manifoldOrResource ? manifoldOrResource : {
7615
+ manifold: manifoldOrResource,
7616
+ refCount: 1,
7617
+ disposed: false
7618
+ };
7619
+ }
7620
+ getLiveManifold(apiName = "ManifoldShapeBackend") {
7621
+ if (this.released || this.resource.disposed) {
7622
+ throw new Error(`${apiName}: manifold backend was already disposed`);
7623
+ }
7624
+ return this.resource.manifold;
7201
7625
  }
7202
7626
  clone() {
7203
- return new _ManifoldShapeBackend(this.manifold);
7627
+ this.resource.refCount += 1;
7628
+ return new _ManifoldShapeBackend(this.resource);
7204
7629
  }
7205
7630
  translate(x, y, z) {
7206
- return new _ManifoldShapeBackend(this.manifold.translate(x, y, z));
7631
+ return new _ManifoldShapeBackend(this.getLiveManifold("translate()").translate(x, y, z));
7207
7632
  }
7208
7633
  rotate(x, y, z) {
7209
- return new _ManifoldShapeBackend(this.manifold.rotate(x, y, z));
7634
+ return new _ManifoldShapeBackend(this.getLiveManifold("rotate()").rotate(x, y, z));
7210
7635
  }
7211
7636
  transform(m) {
7212
- return new _ManifoldShapeBackend(this.manifold.transform(m));
7637
+ return new _ManifoldShapeBackend(this.getLiveManifold("transform()").transform(m));
7213
7638
  }
7214
7639
  scale(v) {
7215
- return new _ManifoldShapeBackend(this.manifold.scale(v));
7640
+ return new _ManifoldShapeBackend(this.getLiveManifold("scale()").scale(v));
7216
7641
  }
7217
7642
  mirror(normal) {
7218
- return new _ManifoldShapeBackend(this.manifold.mirror(normal));
7643
+ return new _ManifoldShapeBackend(this.getLiveManifold("mirror()").mirror(normal));
7219
7644
  }
7220
7645
  split(other) {
7221
- const [inside, outside] = this.manifold.split(requireManifoldShapeBackend(other, "ShapeBackend.split()"));
7646
+ const [inside, outside] = this.getLiveManifold("split()").split(requireManifoldShapeBackend(other, "ShapeBackend.split()"));
7222
7647
  return [new _ManifoldShapeBackend(inside), new _ManifoldShapeBackend(outside)];
7223
7648
  }
7224
7649
  splitByPlane(normal, originOffset) {
7225
- const [inside, outside] = this.manifold.splitByPlane(normal, originOffset);
7650
+ const [inside, outside] = this.getLiveManifold("splitByPlane()").splitByPlane(normal, originOffset);
7226
7651
  return [new _ManifoldShapeBackend(inside), new _ManifoldShapeBackend(outside)];
7227
7652
  }
7228
7653
  trimByPlane(normal, originOffset) {
7229
- return new _ManifoldShapeBackend(this.manifold.trimByPlane(normal, originOffset));
7654
+ return new _ManifoldShapeBackend(this.getLiveManifold("trimByPlane()").trimByPlane(normal, originOffset));
7230
7655
  }
7231
7656
  simplify(tolerance) {
7232
- return new _ManifoldShapeBackend(this.manifold.simplify(tolerance));
7657
+ return new _ManifoldShapeBackend(this.getLiveManifold("simplify()").simplify(tolerance));
7233
7658
  }
7234
7659
  boundingBox() {
7235
- return this.manifold.boundingBox();
7660
+ return this.getLiveManifold("boundingBox()").boundingBox();
7236
7661
  }
7237
7662
  volume() {
7238
- return this.manifold.volume();
7663
+ return this.getLiveManifold("volume()").volume();
7239
7664
  }
7240
7665
  surfaceArea() {
7241
- return this.manifold.surfaceArea();
7666
+ return this.getLiveManifold("surfaceArea()").surfaceArea();
7242
7667
  }
7243
7668
  minGap(other, searchLength) {
7244
- return this.manifold.minGap(requireManifoldShapeBackend(other, "ShapeBackend.minGap()"), searchLength);
7669
+ return this.getLiveManifold("minGap()").minGap(requireManifoldShapeBackend(other, "ShapeBackend.minGap()"), searchLength);
7245
7670
  }
7246
7671
  isEmpty() {
7247
- return this.manifold.isEmpty();
7672
+ return this.getLiveManifold("isEmpty()").isEmpty();
7248
7673
  }
7249
7674
  numBodies() {
7250
- return this.manifold.decompose().length;
7675
+ const parts = this.getLiveManifold("numBodies()").decompose();
7676
+ try {
7677
+ return parts.length;
7678
+ } finally {
7679
+ parts.forEach((part) => {
7680
+ var _a3;
7681
+ return (_a3 = part.delete) == null ? void 0 : _a3.call(part);
7682
+ });
7683
+ }
7251
7684
  }
7252
7685
  numTri() {
7253
- return this.manifold.numTri();
7686
+ return this.getLiveManifold("numTri()").numTri();
7254
7687
  }
7255
7688
  getMesh() {
7256
- return this.manifold.getMesh();
7689
+ return this.getLiveManifold("getMesh()").getMesh();
7257
7690
  }
7258
7691
  slice(offset2) {
7259
- return this.manifold.slice(offset2);
7692
+ return this.getLiveManifold("slice()").slice(offset2);
7260
7693
  }
7261
7694
  project() {
7262
- return this.manifold.project();
7695
+ return this.getLiveManifold("project()").project();
7263
7696
  }
7264
7697
  requireManifold() {
7265
- return this.manifold;
7698
+ return this.getLiveManifold("requireManifold()");
7699
+ }
7700
+ dispose() {
7701
+ var _a3, _b3;
7702
+ if (this.released) return;
7703
+ this.released = true;
7704
+ this.resource.refCount = Math.max(0, this.resource.refCount - 1);
7705
+ if (this.resource.refCount === 0 && !this.resource.disposed) {
7706
+ this.resource.disposed = true;
7707
+ (_b3 = (_a3 = this.resource.manifold).delete) == null ? void 0 : _b3.call(_a3);
7708
+ }
7266
7709
  }
7267
7710
  };
7268
7711
  let ManifoldShapeBackend = _ManifoldShapeBackend;
@@ -7498,6 +7941,12 @@ function rotateVector(v, axis, c, s) {
7498
7941
  v[2] * c + kCrossV[2] * s + axis[2] * kDotV * (1 - c)
7499
7942
  ];
7500
7943
  }
7944
+ function disposeWasmObject(value) {
7945
+ if (value != null && typeof value.delete === "function") value.delete();
7946
+ }
7947
+ function disposeWasmObjects(values) {
7948
+ for (const value of values) disposeWasmObject(value);
7949
+ }
7501
7950
  function applyProfileCompileTransform(crossSection, step) {
7502
7951
  switch (step.kind) {
7503
7952
  case "translate":
@@ -7513,7 +7962,9 @@ function applyProfileCompileTransform(crossSection, step) {
7513
7962
  function applyProfileCompileTransforms(crossSection, transforms) {
7514
7963
  let out = crossSection;
7515
7964
  for (const step of transforms) {
7965
+ const prev = out;
7516
7966
  out = applyProfileCompileTransform(out, step);
7967
+ if (out !== prev) disposeWasmObject(prev);
7517
7968
  }
7518
7969
  return out;
7519
7970
  }
@@ -7525,17 +7976,21 @@ function lowerProfileBooleanCompilePlan(plan, wasm) {
7525
7976
  if (profiles.length === 1) {
7526
7977
  return applyProfileCompileTransforms(profiles[0], plan.transforms);
7527
7978
  }
7528
- const combined = (() => {
7529
- switch (plan.op) {
7530
- case "union":
7531
- return wasm.CrossSection.union(profiles);
7532
- case "difference":
7533
- return wasm.CrossSection.difference(profiles);
7534
- case "intersection":
7535
- return wasm.CrossSection.intersection(profiles);
7536
- }
7537
- })();
7538
- return applyProfileCompileTransforms(combined, plan.transforms);
7979
+ try {
7980
+ const combined = (() => {
7981
+ switch (plan.op) {
7982
+ case "union":
7983
+ return wasm.CrossSection.union(profiles);
7984
+ case "difference":
7985
+ return wasm.CrossSection.difference(profiles);
7986
+ case "intersection":
7987
+ return wasm.CrossSection.intersection(profiles);
7988
+ }
7989
+ })();
7990
+ return applyProfileCompileTransforms(combined, plan.transforms);
7991
+ } finally {
7992
+ disposeWasmObjects(profiles);
7993
+ }
7539
7994
  }
7540
7995
  function lowerProfileCompilePlanToCrossSection(plan, wasm) {
7541
7996
  switch (plan.kind) {
@@ -7543,7 +7998,9 @@ function lowerProfileCompilePlanToCrossSection(plan, wasm) {
7543
7998
  return applyProfileCompileTransforms(wasm.CrossSection.square([plan.width, plan.height], true), plan.transforms);
7544
7999
  case "roundedRect": {
7545
8000
  const radius = Math.min(plan.radius, plan.width / 2, plan.height / 2);
7546
- const crossSection = wasm.CrossSection.square([plan.width - 2 * radius, plan.height - 2 * radius], true).offset(radius, "Round");
8001
+ const base = wasm.CrossSection.square([plan.width - 2 * radius, plan.height - 2 * radius], true);
8002
+ const crossSection = base.offset(radius, "Round");
8003
+ if (crossSection !== base) disposeWasmObject(base);
7547
8004
  return applyProfileCompileTransforms(crossSection, plan.transforms);
7548
8005
  }
7549
8006
  case "circle":
@@ -7552,15 +8009,29 @@ function lowerProfileCompilePlanToCrossSection(plan, wasm) {
7552
8009
  return applyProfileCompileTransforms(new wasm.CrossSection([plan.points]), plan.transforms);
7553
8010
  case "boolean":
7554
8011
  return lowerProfileBooleanCompilePlan(plan, wasm);
7555
- case "offset":
7556
- return applyProfileCompileTransforms(
7557
- lowerProfileCompilePlanToCrossSection(plan.base, wasm).offset(plan.delta, plan.join),
7558
- plan.transforms
7559
- );
8012
+ case "offset": {
8013
+ const base = lowerProfileCompilePlanToCrossSection(plan.base, wasm);
8014
+ try {
8015
+ return applyProfileCompileTransforms(base.offset(plan.delta, plan.join), plan.transforms);
8016
+ } finally {
8017
+ disposeWasmObject(base);
8018
+ }
8019
+ }
7560
8020
  case "project": {
7561
- const projected = lowerShapeCompilePlanToManifold(plan.sourceShape, wasm).transform(planeFrameToWorldToPlaneMatrix(plan.plane)).project();
7562
- return applyProfileCompileTransforms(projected, plan.transforms);
8021
+ const source = lowerShapeCompilePlanToManifold(plan.sourceShape, wasm);
8022
+ try {
8023
+ const transformed = source.transform(planeFrameToWorldToPlaneMatrix(plan.plane));
8024
+ try {
8025
+ return applyProfileCompileTransforms(transformed.project(), plan.transforms);
8026
+ } finally {
8027
+ if (transformed !== source) disposeWasmObject(transformed);
8028
+ }
8029
+ } finally {
8030
+ disposeWasmObject(source);
8031
+ }
7563
8032
  }
8033
+ case "pathProfile":
8034
+ return applyProfileCompileTransforms(new wasm.CrossSection([plan.points]), plan.transforms);
7564
8035
  default:
7565
8036
  assertExhaustive(plan);
7566
8037
  }
@@ -7584,7 +8055,9 @@ function applyShapeCompileTransform(manifold, step) {
7584
8055
  function applyShapeCompileTransforms(manifold, steps) {
7585
8056
  let out = manifold;
7586
8057
  for (const step of steps) {
8058
+ const prev = out;
7587
8059
  out = applyShapeCompileTransform(out, step);
8060
+ if (out !== prev) disposeWasmObject(prev);
7588
8061
  }
7589
8062
  return out;
7590
8063
  }
@@ -7599,22 +8072,36 @@ function lowerShapeBooleanCompilePlan(plan, wasm) {
7599
8072
  if (shapes.length === 1) {
7600
8073
  return shapes[0];
7601
8074
  }
7602
- switch (plan.op) {
7603
- case "union":
7604
- return wasm.Manifold.union(shapes);
7605
- case "difference":
7606
- return wasm.Manifold.difference(shapes);
7607
- case "intersection":
7608
- return wasm.Manifold.intersection(shapes);
8075
+ try {
8076
+ switch (plan.op) {
8077
+ case "union":
8078
+ return wasm.Manifold.union(shapes);
8079
+ case "difference":
8080
+ return wasm.Manifold.difference(shapes);
8081
+ case "intersection":
8082
+ return wasm.Manifold.intersection(shapes);
8083
+ }
8084
+ } finally {
8085
+ disposeWasmObjects(shapes);
7609
8086
  }
7610
8087
  }
7611
8088
  function lowerShapeTrimByPlaneCompilePlan(plan, wasm) {
7612
- return lowerShapeCompilePlanToManifold(plan.base, wasm).trimByPlane([plan.normalX, plan.normalY, plan.normalZ], plan.originOffset);
8089
+ const base = lowerShapeCompilePlanToManifold(plan.base, wasm);
8090
+ try {
8091
+ return base.trimByPlane([plan.normalX, plan.normalY, plan.normalZ], plan.originOffset);
8092
+ } finally {
8093
+ disposeWasmObject(base);
8094
+ }
7613
8095
  }
7614
8096
  function lowerShapeLoftCompilePlan(plan, wasm) {
7615
- const inputPolygons = plan.profiles.map(
7616
- (profile) => lowerProfileCompilePlanToCrossSection(profile, wasm).toPolygons()
7617
- );
8097
+ const inputPolygons = plan.profiles.map((profile) => {
8098
+ const crossSection = lowerProfileCompilePlanToCrossSection(profile, wasm);
8099
+ try {
8100
+ return crossSection.toPolygons();
8101
+ } finally {
8102
+ disposeWasmObject(crossSection);
8103
+ }
8104
+ });
7618
8105
  if (inputPolygons.length >= 2) {
7619
8106
  const stitched = loftStitched(inputPolygons, plan.heights, wasm);
7620
8107
  if (stitched) return stitched;
@@ -7623,7 +8110,14 @@ function lowerShapeLoftCompilePlan(plan, wasm) {
7623
8110
  return lowerSdfToManifold(levelSetFieldToStandardSdf(input.sdf), input.bounds, input.edgeLength, wasm);
7624
8111
  }
7625
8112
  function lowerShapeSweepCompilePlan(plan, wasm) {
7626
- const profilePolygons = lowerProfileCompilePlanToCrossSection(plan.profile, wasm).toPolygons();
8113
+ const crossSection = lowerProfileCompilePlanToCrossSection(plan.profile, wasm);
8114
+ const profilePolygons = (() => {
8115
+ try {
8116
+ return crossSection.toPolygons();
8117
+ } finally {
8118
+ disposeWasmObject(crossSection);
8119
+ }
8120
+ })();
7627
8121
  const pathPoints = sweepPathToPolylineAdaptive(plan.path, plan.pathSamples ?? 48);
7628
8122
  const up = [plan.up[0], plan.up[1], plan.up[2]];
7629
8123
  const stitched = sweepStitched(profilePolygons, pathPoints, up, wasm);
@@ -7635,77 +8129,318 @@ function lowerShapeSweepCompilePlan(plan, wasm) {
7635
8129
  });
7636
8130
  return lowerSdfToManifold(levelSetFieldToStandardSdf(input.sdf), input.bounds, input.edgeLength, wasm);
7637
8131
  }
7638
- function lowerFromSlicesToManifold(plan, wasm) {
7639
- if (plan.groups.length === 0) throw new Error("Shape.fromSlices requires at least one slice");
7640
- const groupSolids = [];
7641
- for (const group2 of plan.groups) {
8132
+ function buildPlaneFrame(normal) {
8133
+ const [nx, ny, nz] = normal;
8134
+ if (Math.abs(nz) > 1 - 1e-8) {
8135
+ const s = nz > 0 ? 1 : -1;
8136
+ return { u: [1, 0, 0], v: [0, s, 0] };
8137
+ }
8138
+ if (Math.abs(ny) > 1 - 1e-8) {
8139
+ const s = ny > 0 ? 1 : -1;
8140
+ return { u: [1, 0, 0], v: [0, 0, s] };
8141
+ }
8142
+ if (Math.abs(nx) > 1 - 1e-8) {
8143
+ const s = nx > 0 ? 1 : -1;
8144
+ return { u: [0, 1, 0], v: [0, 0, s] };
8145
+ }
8146
+ const ref = Math.abs(nx) < 0.9 ? [1, 0, 0] : [0, 1, 0];
8147
+ const uRaw = [
8148
+ ref[1] * nz - ref[2] * ny,
8149
+ ref[2] * nx - ref[0] * nz,
8150
+ ref[0] * ny - ref[1] * nx
8151
+ ];
8152
+ const uLen = Math.sqrt(uRaw[0] * uRaw[0] + uRaw[1] * uRaw[1] + uRaw[2] * uRaw[2]);
8153
+ const u = [uRaw[0] / uLen, uRaw[1] / uLen, uRaw[2] / uLen];
8154
+ const v = [
8155
+ ny * u[2] - nz * u[1],
8156
+ nz * u[0] - nx * u[2],
8157
+ nx * u[1] - ny * u[0]
8158
+ ];
8159
+ return { u, v };
8160
+ }
8161
+ function dot3$5(a, b) {
8162
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
8163
+ }
8164
+ function profileHalfWidths(loops, height) {
8165
+ let maxPos = 0;
8166
+ let maxNeg = 0;
8167
+ for (const loop of loops) {
8168
+ for (let i = 0; i < loop.length; i++) {
8169
+ const [x0, y0] = loop[i];
8170
+ const [x1, y1] = loop[(i + 1) % loop.length];
8171
+ if (y0 <= height && y1 > height || y1 <= height && y0 > height) {
8172
+ const t = (height - y0) / (y1 - y0);
8173
+ const x = x0 + t * (x1 - x0);
8174
+ if (x > maxPos) maxPos = x;
8175
+ if (x < 0 && -x > maxNeg) maxNeg = -x;
8176
+ }
8177
+ }
8178
+ }
8179
+ return [maxPos, maxNeg];
8180
+ }
8181
+ function interpolatedHalfWidths(slices, spineCoord) {
8182
+ if (slices.length === 1) return profileHalfWidths(slices[0].polygons, spineCoord);
8183
+ if (spineCoord <= slices[0].offset) return profileHalfWidths(slices[0].polygons, spineCoord);
8184
+ if (spineCoord >= slices[slices.length - 1].offset) {
8185
+ return profileHalfWidths(slices[slices.length - 1].polygons, spineCoord);
8186
+ }
8187
+ let seg = 0;
8188
+ while (seg + 1 < slices.length && spineCoord > slices[seg + 1].offset) seg++;
8189
+ const t = (spineCoord - slices[seg].offset) / (slices[seg + 1].offset - slices[seg].offset);
8190
+ const [p0, n0] = profileHalfWidths(slices[seg].polygons, spineCoord);
8191
+ const [p1, n1] = profileHalfWidths(slices[seg + 1].polygons, spineCoord);
8192
+ return [p0 * (1 - t) + p1 * t, n0 * (1 - t) + n1 * t];
8193
+ }
8194
+ function lowerMultiGroupFromSlicesToManifold(plan, wasm) {
8195
+ const n0 = plan.groups[0].normal;
8196
+ const n1 = plan.groups[1].normal;
8197
+ const spineRaw = [
8198
+ n0[1] * n1[2] - n0[2] * n1[1],
8199
+ n0[2] * n1[0] - n0[0] * n1[2],
8200
+ n0[0] * n1[1] - n0[1] * n1[0]
8201
+ ];
8202
+ const spineLen = Math.sqrt(spineRaw[0] ** 2 + spineRaw[1] ** 2 + spineRaw[2] ** 2);
8203
+ if (spineLen < 1e-8) {
8204
+ throw new Error("Shape.fromSlices: multi-group slices must have non-parallel normals.");
8205
+ }
8206
+ const spine = [spineRaw[0] / spineLen, spineRaw[1] / spineLen, spineRaw[2] / spineLen];
8207
+ const preparedGroups = plan.groups.map((group2) => {
8208
+ const frame = buildPlaneFrame(group2.normal);
7642
8209
  const sorted = [...group2.slices].sort((a, b) => a.offset - b.offset);
7643
- const [nx, ny, nz] = group2.normal;
7644
- let solid;
7645
- if (sorted.length === 1) {
7646
- const cross2 = lowerProfileCompilePlanToCrossSection(sorted[0].profile, wasm);
7647
- const extrudeHalf = 500;
7648
- solid = cross2.extrude(2 * extrudeHalf).translate(0, 0, sorted[0].offset - extrudeHalf);
7649
- } else {
7650
- const polygons = sorted.map((s) => lowerProfileCompilePlanToCrossSection(s.profile, wasm).toPolygons());
7651
- const heights = sorted.map((s) => s.offset);
7652
- const stitched = polygons.length >= 2 ? loftStitched(polygons, heights, wasm) : null;
7653
- if (stitched) {
7654
- solid = stitched;
8210
+ const sliceData = sorted.map((s) => {
8211
+ const cross2 = lowerProfileCompilePlanToCrossSection(s.profile, wasm);
8212
+ try {
8213
+ const polygons = cross2.toPolygons();
8214
+ return { offset: s.offset, polygons, sdf: compilePolygonsSdf(polygons) };
8215
+ } finally {
8216
+ disposeWasmObject(cross2);
8217
+ }
8218
+ });
8219
+ const spineOnU = Math.abs(dot3$5(spine, frame.u));
8220
+ const spineOnV = Math.abs(dot3$5(spine, frame.v));
8221
+ const vIsSpine = spineOnV >= spineOnU;
8222
+ const measAxis = vIsSpine ? frame.u : frame.v;
8223
+ const spineAxis = vIsSpine ? frame.v : frame.u;
8224
+ const spineSign = dot3$5(spine, spineAxis) >= 0 ? 1 : -1;
8225
+ let minMeas = Infinity;
8226
+ let maxMeas = -Infinity;
8227
+ let minSpine = Infinity;
8228
+ let maxSpine = -Infinity;
8229
+ for (const s of sliceData) {
8230
+ for (const loop of s.polygons) {
8231
+ for (const [x, y] of loop) {
8232
+ const meas = vIsSpine ? x : y;
8233
+ const sp = vIsSpine ? y : x;
8234
+ if (meas < minMeas) minMeas = meas;
8235
+ if (meas > maxMeas) maxMeas = meas;
8236
+ if (sp < minSpine) minSpine = sp;
8237
+ if (sp > maxSpine) maxSpine = sp;
8238
+ }
8239
+ }
8240
+ }
8241
+ const sliceDataForRayCast = vIsSpine ? sliceData : sliceData.map((s) => ({
8242
+ offset: s.offset,
8243
+ polygons: s.polygons.map((loop) => loop.map(([x, y]) => [y, x]))
8244
+ }));
8245
+ const profileSdf2d = (u, v) => {
8246
+ if (sliceData.length === 1) return sliceData[0].sdf(u, v);
8247
+ const w = vIsSpine ? v : u;
8248
+ const offsets = sliceData.map((s) => s.offset);
8249
+ if (w <= offsets[0]) return sliceData[0].sdf(u, v);
8250
+ if (w >= offsets[offsets.length - 1]) return sliceData[sliceData.length - 1].sdf(u, v);
8251
+ let seg = 0;
8252
+ while (seg + 1 < offsets.length && w > offsets[seg + 1]) seg++;
8253
+ const t = (w - offsets[seg]) / (offsets[seg + 1] - offsets[seg]);
8254
+ return sliceData[seg].sdf(u, v) * (1 - t) + sliceData[seg + 1].sdf(u, v) * t;
8255
+ };
8256
+ return {
8257
+ normal: group2.normal,
8258
+ frame,
8259
+ measAxis,
8260
+ spineSign,
8261
+ vIsSpine,
8262
+ sliceData: sliceDataForRayCast,
8263
+ profileSdf2d,
8264
+ minMeas,
8265
+ maxMeas,
8266
+ minSpine,
8267
+ maxSpine
8268
+ };
8269
+ });
8270
+ const sdf3d = (p2) => {
8271
+ const spineCoord = dot3$5(p2, spine);
8272
+ let sumRhoSq = 0;
8273
+ let profileCapSdf = Infinity;
8274
+ for (const g of preparedGroups) {
8275
+ const measCoord = dot3$5(p2, g.measAxis);
8276
+ const localSpine = spineCoord * g.spineSign;
8277
+ const [wPos, wNeg] = interpolatedHalfWidths(g.sliceData, localSpine);
8278
+ let rho;
8279
+ if (wPos <= 1e-10 && wNeg <= 1e-10) {
8280
+ rho = 2;
8281
+ } else if (measCoord >= 0) {
8282
+ rho = wPos > 1e-10 ? measCoord / wPos : 2;
7655
8283
  } else {
7656
- const input = buildLoftLevelSetInput(polygons, heights, {
7657
- edgeLength: plan.edgeLength,
7658
- boundsPadding: plan.boundsPadding
7659
- });
7660
- solid = lowerSdfToManifold(levelSetFieldToStandardSdf(input.sdf), input.bounds, input.edgeLength, wasm);
8284
+ rho = wNeg > 1e-10 ? -measCoord / wNeg : 2;
7661
8285
  }
8286
+ sumRhoSq += rho * rho;
8287
+ const u = dot3$5(p2, g.frame.u);
8288
+ const v = dot3$5(p2, g.frame.v);
8289
+ const dProfile = g.profileSdf2d(u, v);
8290
+ profileCapSdf = Math.min(profileCapSdf, dProfile);
7662
8291
  }
7663
- if (Math.abs(nx) > 1e-10 || Math.abs(ny) > 1e-10 || nz < 0) {
7664
- if (Math.abs(nx) < 1e-10 && Math.abs(ny) < 1e-10 && nz < 0) {
7665
- solid = solid.rotate([180, 0, 0]);
8292
+ const rhoSdf = sumRhoSq - 1;
8293
+ const capSdf = -profileCapSdf;
8294
+ return Math.max(rhoSdf, capSdf);
8295
+ };
8296
+ let bMinX = -Infinity;
8297
+ let bMaxX = Infinity;
8298
+ let bMinY = -Infinity;
8299
+ let bMaxY = Infinity;
8300
+ let bMinZ = -Infinity;
8301
+ let bMaxZ = Infinity;
8302
+ for (const g of preparedGroups) {
8303
+ for (let axis = 0; axis < 3; axis++) {
8304
+ const mc = g.measAxis[axis];
8305
+ if (Math.abs(mc) > 0.5) {
8306
+ const lo = mc > 0 ? g.minMeas : -g.maxMeas;
8307
+ const hi = mc > 0 ? g.maxMeas : -g.minMeas;
8308
+ if (axis === 0) {
8309
+ bMinX = Math.max(bMinX, lo);
8310
+ bMaxX = Math.min(bMaxX, hi);
8311
+ } else if (axis === 1) {
8312
+ bMinY = Math.max(bMinY, lo);
8313
+ bMaxY = Math.min(bMaxY, hi);
8314
+ } else {
8315
+ bMinZ = Math.max(bMinZ, lo);
8316
+ bMaxZ = Math.min(bMaxZ, hi);
8317
+ }
8318
+ }
8319
+ }
8320
+ }
8321
+ let spineMin = -Infinity;
8322
+ let spineMax = Infinity;
8323
+ for (const g of preparedGroups) {
8324
+ const lo = g.spineSign > 0 ? g.minSpine : -g.maxSpine;
8325
+ const hi = g.spineSign > 0 ? g.maxSpine : -g.minSpine;
8326
+ spineMin = Math.max(spineMin, lo);
8327
+ spineMax = Math.min(spineMax, hi);
8328
+ }
8329
+ for (let axis = 0; axis < 3; axis++) {
8330
+ if (Math.abs(spine[axis]) > 0.5) {
8331
+ const lo = spine[axis] > 0 ? spineMin : -spineMax;
8332
+ const hi = spine[axis] > 0 ? spineMax : -spineMin;
8333
+ if (axis === 0) {
8334
+ bMinX = Math.max(bMinX, lo);
8335
+ bMaxX = Math.min(bMaxX, hi);
8336
+ } else if (axis === 1) {
8337
+ bMinY = Math.max(bMinY, lo);
8338
+ bMaxY = Math.min(bMaxY, hi);
7666
8339
  } else {
7667
- const ax = -ny;
7668
- const ay = nx;
7669
- const len = Math.sqrt(ax * ax + ay * ay);
7670
- const c = nz;
7671
- const s = len;
7672
- const ux = ax / len;
7673
- const uy = ay / len;
7674
- const m00 = c + ux * ux * (1 - c);
7675
- const m01 = ux * uy * (1 - c);
7676
- const m02 = uy * s;
7677
- const m10 = uy * ux * (1 - c);
7678
- const m11 = c + uy * uy * (1 - c);
7679
- const m12 = -ux * s;
7680
- const m20 = -uy * s;
7681
- const m21 = ux * s;
7682
- const m22 = c;
7683
- solid = solid.transform([m00, m01, m02, 0, m10, m11, m12, 0, m20, m21, m22, 0, 0, 0, 0, 1]);
7684
- }
7685
- }
7686
- groupSolids.push(solid);
7687
- }
7688
- if (groupSolids.length === 1) return groupSolids[0];
7689
- let result = groupSolids[0];
7690
- for (let i = 1; i < groupSolids.length; i++) {
7691
- result = result.intersect(groupSolids[i]);
8340
+ bMinZ = Math.max(bMinZ, lo);
8341
+ bMaxZ = Math.min(bMaxZ, hi);
8342
+ }
8343
+ }
7692
8344
  }
7693
- return result;
8345
+ const fallback = 100;
8346
+ if (!isFinite(bMinX)) bMinX = -fallback;
8347
+ if (!isFinite(bMaxX)) bMaxX = fallback;
8348
+ if (!isFinite(bMinY)) bMinY = -fallback;
8349
+ if (!isFinite(bMaxY)) bMaxY = fallback;
8350
+ if (!isFinite(bMinZ)) bMinZ = -fallback;
8351
+ if (!isFinite(bMaxZ)) bMaxZ = fallback;
8352
+ const pad = plan.boundsPadding;
8353
+ const bounds = {
8354
+ min: [bMinX - pad, bMinY - pad, bMinZ - pad],
8355
+ max: [bMaxX + pad, bMaxY + pad, bMaxZ + pad]
8356
+ };
8357
+ return lowerSdfToManifold(sdf3d, bounds, plan.edgeLength, wasm);
8358
+ }
8359
+ function lowerFromSlicesToManifold(plan, wasm) {
8360
+ if (plan.groups.length === 0) throw new Error("Shape.fromSlices requires at least one slice");
8361
+ if (plan.groups.length > 1) {
8362
+ return lowerMultiGroupFromSlicesToManifold(plan, wasm);
8363
+ }
8364
+ const group2 = plan.groups[0];
8365
+ const sorted = [...group2.slices].sort((a, b) => a.offset - b.offset);
8366
+ const [nx, ny, nz] = group2.normal;
8367
+ let solid;
8368
+ if (sorted.length === 1) {
8369
+ const cross2 = lowerProfileCompilePlanToCrossSection(sorted[0].profile, wasm);
8370
+ const extrudeHalf = 500;
8371
+ try {
8372
+ const extruded = cross2.extrude(2 * extrudeHalf);
8373
+ solid = applyShapeCompileTransforms(extruded, [{ kind: "translate", x: 0, y: 0, z: sorted[0].offset - extrudeHalf }]);
8374
+ } finally {
8375
+ disposeWasmObject(cross2);
8376
+ }
8377
+ } else {
8378
+ const polygons = sorted.map((s) => {
8379
+ const crossSection = lowerProfileCompilePlanToCrossSection(s.profile, wasm);
8380
+ try {
8381
+ return crossSection.toPolygons();
8382
+ } finally {
8383
+ disposeWasmObject(crossSection);
8384
+ }
8385
+ });
8386
+ const heights = sorted.map((s) => s.offset);
8387
+ const stitched = polygons.length >= 2 ? loftStitched(polygons, heights, wasm) : null;
8388
+ if (stitched) {
8389
+ solid = stitched;
8390
+ } else {
8391
+ const input = buildLoftLevelSetInput(polygons, heights, {
8392
+ edgeLength: plan.edgeLength,
8393
+ boundsPadding: plan.boundsPadding
8394
+ });
8395
+ solid = lowerSdfToManifold(levelSetFieldToStandardSdf(input.sdf), input.bounds, input.edgeLength, wasm);
8396
+ }
8397
+ }
8398
+ if (Math.abs(nx) > 1e-10 || Math.abs(ny) > 1e-10 || nz < 0) {
8399
+ if (Math.abs(nx) < 1e-10 && Math.abs(ny) < 1e-10 && nz < 0) {
8400
+ const prev = solid;
8401
+ solid = solid.rotate([180, 0, 0]);
8402
+ if (solid !== prev) disposeWasmObject(prev);
8403
+ } else {
8404
+ const ax = -ny;
8405
+ const ay = nx;
8406
+ const len = Math.sqrt(ax * ax + ay * ay);
8407
+ const c = nz;
8408
+ const s = len;
8409
+ const ux = ax / len;
8410
+ const uy = ay / len;
8411
+ const m00 = c + ux * ux * (1 - c);
8412
+ const m01 = ux * uy * (1 - c);
8413
+ const m02 = uy * s;
8414
+ const m10 = uy * ux * (1 - c);
8415
+ const m11 = c + uy * uy * (1 - c);
8416
+ const m12 = -ux * s;
8417
+ const m20 = -uy * s;
8418
+ const m21 = ux * s;
8419
+ const m22 = c;
8420
+ const prev = solid;
8421
+ solid = solid.transform([m00, m01, m02, 0, m10, m11, m12, 0, m20, m21, m22, 0, 0, 0, 0, 1]);
8422
+ if (solid !== prev) disposeWasmObject(prev);
8423
+ }
8424
+ }
8425
+ return solid;
7694
8426
  }
7695
8427
  function lowerShapeVariableSweepCompilePlan(plan, wasm) {
7696
- const sectionPolygons = plan.sections.map((s) => ({
7697
- t: s.t,
7698
- polygons: lowerProfileCompilePlanToCrossSection(s.profile, wasm).toPolygons()
7699
- }));
7700
- const input = buildVariableSweepLevelSetInput(
7701
- sectionPolygons,
7702
- plan.path,
7703
- {
7704
- edgeLength: plan.edgeLength,
7705
- boundsPadding: plan.boundsPadding,
7706
- up: [plan.up[0], plan.up[1], plan.up[2]]
8428
+ const sectionPolygons = plan.sections.map((s) => {
8429
+ const crossSection = lowerProfileCompilePlanToCrossSection(s.profile, wasm);
8430
+ try {
8431
+ return {
8432
+ t: s.t,
8433
+ polygons: crossSection.toPolygons()
8434
+ };
8435
+ } finally {
8436
+ disposeWasmObject(crossSection);
7707
8437
  }
7708
- );
8438
+ });
8439
+ const input = buildVariableSweepLevelSetInput(sectionPolygons, plan.path, {
8440
+ edgeLength: plan.edgeLength,
8441
+ boundsPadding: plan.boundsPadding,
8442
+ up: [plan.up[0], plan.up[1], plan.up[2]]
8443
+ });
7709
8444
  return lowerSdfToManifold(levelSetFieldToStandardSdf(input.sdf), input.bounds, input.edgeLength, wasm);
7710
8445
  }
7711
8446
  function lowerShapeFilletCompilePlan(plan, wasm) {
@@ -7716,13 +8451,15 @@ function lowerShapeFilletCompilePlan(plan, wasm) {
7716
8451
  `fillet2d() currently supports ${selection.selection.edgeName} only with quadrant [${selection.selection.quadrant[0]}, ${selection.selection.quadrant[1]}].`
7717
8452
  );
7718
8453
  }
7719
- return applyFilletSelectionToManifold(
7720
- lowerShapeCompilePlanToManifold(plan.base, wasm),
7721
- selection.selection,
7722
- plan.radius,
7723
- plan.segments,
7724
- wasm
7725
- );
8454
+ const base = lowerShapeCompilePlanToManifold(plan.base, wasm);
8455
+ try {
8456
+ const result = applyFilletSelectionToManifold(base, selection.selection, plan.radius, plan.segments, wasm);
8457
+ if (result !== base) disposeWasmObject(base);
8458
+ return result;
8459
+ } catch (error) {
8460
+ disposeWasmObject(base);
8461
+ throw error;
8462
+ }
7726
8463
  }
7727
8464
  function lowerShapeChamferCompilePlan(plan, wasm) {
7728
8465
  const selection = resolveSupportedEdgeFeatureSelection(plan.base, plan.edge);
@@ -7732,7 +8469,15 @@ function lowerShapeChamferCompilePlan(plan, wasm) {
7732
8469
  `chamfer2d() currently supports ${selection.selection.edgeName} only with quadrant [${selection.selection.quadrant[0]}, ${selection.selection.quadrant[1]}].`
7733
8470
  );
7734
8471
  }
7735
- return applyChamferSelectionToManifold(lowerShapeCompilePlanToManifold(plan.base, wasm), selection.selection, plan.size, wasm);
8472
+ const base = lowerShapeCompilePlanToManifold(plan.base, wasm);
8473
+ try {
8474
+ const result = applyChamferSelectionToManifold(base, selection.selection, plan.size, wasm);
8475
+ if (result !== base) disposeWasmObject(base);
8476
+ return result;
8477
+ } catch (error) {
8478
+ disposeWasmObject(base);
8479
+ throw error;
8480
+ }
7736
8481
  }
7737
8482
  function edgeSegmentToSelection(segment) {
7738
8483
  const { start, end, direction: axis, normalA, normalB, convex } = segment;
@@ -7827,7 +8572,9 @@ function lowerFilletEdgesCompilePlan(plan, wasm) {
7827
8572
  try {
7828
8573
  const selection = edgeSegmentToSelection(seg);
7829
8574
  const apply = seg.convex ? applyFilletSelectionToManifold : applyConcaveFilletSelectionToManifold;
7830
- manifold = apply(manifold, selection, plan.radius, plan.segments, wasm);
8575
+ const prev = manifold;
8576
+ manifold = apply(prev, selection, plan.radius, plan.segments, wasm);
8577
+ if (manifold !== prev) disposeWasmObject(prev);
7831
8578
  } catch {
7832
8579
  }
7833
8580
  }
@@ -7852,7 +8599,9 @@ function lowerChamferEdgesCompilePlan(plan, wasm) {
7852
8599
  try {
7853
8600
  const selection = edgeSegmentToSelection(seg);
7854
8601
  const apply = seg.convex ? applyChamferSelectionToManifold : applyConcaveChamferSelectionToManifold;
7855
- manifold = apply(manifold, selection, plan.size, wasm);
8602
+ const prev = manifold;
8603
+ manifold = apply(prev, selection, plan.size, wasm);
8604
+ if (manifold !== prev) disposeWasmObject(prev);
7856
8605
  } catch {
7857
8606
  }
7858
8607
  }
@@ -7861,24 +8610,34 @@ function lowerChamferEdgesCompilePlan(plan, wasm) {
7861
8610
  function lowerShapeCompilePlanToManifold(plan, wasm) {
7862
8611
  switch (plan.kind) {
7863
8612
  case "box":
7864
- return wasm.Manifold.cube([plan.x, plan.y, plan.z], false).translate(-plan.x / 2, -plan.y / 2, 0);
8613
+ return applyShapeCompileTransforms(wasm.Manifold.cube([plan.x, plan.y, plan.z], false), [
8614
+ { kind: "translate", x: -plan.x / 2, y: -plan.y / 2, z: 0 }
8615
+ ]);
7865
8616
  case "cylinder":
7866
8617
  return wasm.Manifold.cylinder(plan.height, plan.radius, plan.radiusTop ?? -1, plan.segments ?? 0, false);
7867
8618
  case "sphere":
7868
8619
  return wasm.Manifold.sphere(plan.radius, plan.segments ?? 0);
7869
8620
  case "torus": {
7870
8621
  const circle2 = wasm.CrossSection.circle(plan.minorRadius, plan.segments ?? 0);
7871
- const translated = circle2.translate([plan.majorRadius, 0]);
7872
- return translated.revolve(plan.segments ?? 0, 360);
8622
+ try {
8623
+ const translated = circle2.translate([plan.majorRadius, 0]);
8624
+ try {
8625
+ return translated.revolve(plan.segments ?? 0, 360);
8626
+ } finally {
8627
+ disposeWasmObject(translated);
8628
+ }
8629
+ } finally {
8630
+ disposeWasmObject(circle2);
8631
+ }
8632
+ }
8633
+ case "extrude": {
8634
+ const profile = lowerProfileCompilePlanToCrossSection(plan.profile, wasm);
8635
+ try {
8636
+ return profile.extrude(plan.height, plan.twistSegments ?? 0, plan.twist ?? 0, plan.scaleTop, false);
8637
+ } finally {
8638
+ disposeWasmObject(profile);
8639
+ }
7873
8640
  }
7874
- case "extrude":
7875
- return lowerProfileCompilePlanToCrossSection(plan.profile, wasm).extrude(
7876
- plan.height,
7877
- plan.twistSegments ?? 0,
7878
- plan.twist ?? 0,
7879
- plan.scaleTop,
7880
- false
7881
- );
7882
8641
  case "sheetMetal":
7883
8642
  return lowerShapeCompilePlanToManifold(lowerSheetMetalBasePlan(plan.model, plan.output), wasm);
7884
8643
  case "shell": {
@@ -7896,8 +8655,14 @@ function lowerShapeCompilePlanToManifold(plan, wasm) {
7896
8655
  if (!lowered.ok) throw new Error(lowered.reason);
7897
8656
  return lowerShapeCompilePlanToManifold(lowered.plan, wasm);
7898
8657
  }
7899
- case "revolve":
7900
- return lowerProfileCompilePlanToCrossSection(plan.profile, wasm).revolve(plan.segments ?? 0, plan.degrees);
8658
+ case "revolve": {
8659
+ const profile = lowerProfileCompilePlanToCrossSection(plan.profile, wasm);
8660
+ try {
8661
+ return profile.revolve(plan.segments ?? 0, plan.degrees);
8662
+ } finally {
8663
+ disposeWasmObject(profile);
8664
+ }
8665
+ }
7901
8666
  case "loft":
7902
8667
  return lowerShapeLoftCompilePlan(plan, wasm);
7903
8668
  case "sweep":
@@ -7932,6 +8697,10 @@ function lowerShapeCompilePlanToManifold(plan, wasm) {
7932
8697
  }
7933
8698
  case "fromSlices":
7934
8699
  return lowerFromSlicesToManifold(plan, wasm);
8700
+ case "nurbsSurface":
8701
+ return lowerNurbsSurfaceToManifold(plan, wasm);
8702
+ case "importedStep":
8703
+ throw new Error(`importStep("${plan.filePath}") requires the OCCT backend. Add setActiveBackend('occt') at the top of your script.`);
7935
8704
  default:
7936
8705
  assertExhaustive(plan);
7937
8706
  }
@@ -7958,17 +8727,27 @@ function lowerSdfToManifold(evalFn, bounds, edgeLength2, wasm) {
7958
8727
  vertProperties: vertProps6,
7959
8728
  triVerts
7960
8729
  });
7961
- return new wasm.Manifold(wasmMesh);
8730
+ try {
8731
+ return new wasm.Manifold(wasmMesh);
8732
+ } finally {
8733
+ disposeWasmObject(wasmMesh);
8734
+ }
7962
8735
  }
7963
8736
  function simplifySdfMesh(triVerts, vertProperties, edgeLength2, wasm) {
7964
8737
  const maxError = edgeLength2 * 0.15;
7965
8738
  for (const ratio of [0.5, 0.75]) {
7966
8739
  let simplified = simplifyMesh(triVerts, vertProperties, ratio, maxError);
7967
8740
  simplified = filterDegenerateTriangles(simplified);
8741
+ let mesh = null;
8742
+ let manifold = null;
7968
8743
  try {
7969
- new wasm.Manifold(new wasm.Mesh({ numProp: 3, vertProperties, triVerts: simplified }));
8744
+ mesh = new wasm.Mesh({ numProp: 3, vertProperties, triVerts: simplified });
8745
+ manifold = new wasm.Manifold(mesh);
7970
8746
  return simplified;
7971
8747
  } catch {
8748
+ } finally {
8749
+ disposeWasmObject(manifold);
8750
+ disposeWasmObject(mesh);
7972
8751
  }
7973
8752
  }
7974
8753
  return triVerts;
@@ -8028,6 +8807,58 @@ function projectVerticesToSurfaceWithNormals(vertProperties, sdfFn, out6) {
8028
8807
  }
8029
8808
  }
8030
8809
  }
8810
+ function lowerNurbsSurfaceToManifold(plan, wasm) {
8811
+ const surface = new NurbsSurface(plan.controlGrid, {
8812
+ degreeU: plan.degreeU,
8813
+ degreeV: plan.degreeV,
8814
+ weights: plan.weightsGrid,
8815
+ knotsU: plan.knotsU,
8816
+ knotsV: plan.knotsV
8817
+ });
8818
+ const res = plan.resolution;
8819
+ const { positions, normals, indices } = surface.tessellate(res, res);
8820
+ const thickness = plan.thickness;
8821
+ const numVerts = positions.length;
8822
+ const allPositions = [];
8823
+ for (const [x, y, z] of positions) allPositions.push(x, y, z);
8824
+ for (let i = 0; i < numVerts; i++) {
8825
+ const [x, y, z] = positions[i];
8826
+ const [nx, ny, nz] = normals[i];
8827
+ allPositions.push(x - nx * thickness, y - ny * thickness, z - nz * thickness);
8828
+ }
8829
+ const allIndices = [];
8830
+ for (const idx of indices) allIndices.push(idx);
8831
+ for (let i = 0; i < indices.length; i += 3) {
8832
+ allIndices.push(indices[i] + numVerts, indices[i + 2] + numVerts, indices[i + 1] + numVerts);
8833
+ }
8834
+ const resU = res, resV = res;
8835
+ for (let i = 0; i < resU; i++) {
8836
+ const a = i * (resV + 1);
8837
+ const b = (i + 1) * (resV + 1);
8838
+ allIndices.push(a, a + numVerts, b, b, a + numVerts, b + numVerts);
8839
+ const c = a + resV;
8840
+ const d = b + resV;
8841
+ allIndices.push(c, d, c + numVerts, d, d + numVerts, c + numVerts);
8842
+ }
8843
+ for (let j = 0; j < resV; j++) {
8844
+ const a = j;
8845
+ const b = j + 1;
8846
+ allIndices.push(a, b, a + numVerts, b, b + numVerts, a + numVerts);
8847
+ const c = resU * (resV + 1) + j;
8848
+ const d = c + 1;
8849
+ allIndices.push(c, c + numVerts, d, d, c + numVerts, d + numVerts);
8850
+ }
8851
+ const mesh = new wasm.Mesh({
8852
+ numProp: 3,
8853
+ vertProperties: new Float32Array(allPositions),
8854
+ triVerts: new Uint32Array(allIndices)
8855
+ });
8856
+ try {
8857
+ return new wasm.Manifold(mesh);
8858
+ } finally {
8859
+ disposeWasmObject(mesh);
8860
+ }
8861
+ }
8031
8862
  function lowerImportedMeshToManifold(fileData, format, filePath, wasm) {
8032
8863
  const parsed = parseMeshFile(fileData, format);
8033
8864
  if (parsed.triVerts.length === 0) {
@@ -8046,6 +8877,8 @@ function lowerImportedMeshToManifold(fileData, format, filePath, wasm) {
8046
8877
  throw new Error(
8047
8878
  `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)}`
8048
8879
  );
8880
+ } finally {
8881
+ disposeWasmObject(wasmMesh);
8049
8882
  }
8050
8883
  }
8051
8884
  function lowerShapeCompilePlanToShapeBackend(plan, wasm) {
@@ -8480,6 +9313,8 @@ function summarizeProfile(profile) {
8480
9313
  return "offset profile";
8481
9314
  case "project":
8482
9315
  return "projected profile";
9316
+ case "pathProfile":
9317
+ return `path profile (${profile.edges.length} edges)`;
8483
9318
  default:
8484
9319
  assertExhaustive(profile);
8485
9320
  }
@@ -10102,6 +10937,8 @@ function resolveShapeFaceTableInternal(plan, owner) {
10102
10937
  case "importedMesh":
10103
10938
  case "sdf":
10104
10939
  case "fromSlices":
10940
+ case "nurbsSurface":
10941
+ case "importedStep":
10105
10942
  return emptyFaceTable();
10106
10943
  default:
10107
10944
  assertExhaustive(plan);
@@ -10605,7 +11442,7 @@ function transformPortMap(ports, matrix) {
10605
11442
  }
10606
11443
  return out;
10607
11444
  }
10608
- function computeConnectFrame(childBase, childPort, parentPort, flip, childAlign = "middle", parentAlign = "middle") {
11445
+ function computeConnectFrame(childBase, childPort, parentPort, _flip, childAlign = "middle", parentAlign = "middle") {
10609
11446
  const childAlignPt = resolvePortAlignPoint(childPort, childAlign);
10610
11447
  const parentAlignPt = resolvePortAlignPoint(parentPort, parentAlign);
10611
11448
  const cI = childBase.point(childAlignPt);
@@ -10613,7 +11450,9 @@ function computeConnectFrame(childBase, childPort, parentPort, flip, childAlign
10613
11450
  const cUp = normalize3$2(childBase.vector(childPort.up));
10614
11451
  const cRight = normalize3$2(cross3$3(cAxis, cUp));
10615
11452
  const pOrigin = parentAlignPt;
10616
- const pAxis = flip ? negate3$1(parentPort.axis) : [...parentPort.axis];
11453
+ const jointKind = childPort.kind ?? parentPort.kind;
11454
+ const faceToFace = jointKind !== "prismatic";
11455
+ const pAxis = faceToFace ? negate3$1(parentPort.axis) : [...parentPort.axis];
10617
11456
  const pUp = [...parentPort.up];
10618
11457
  const pRight = normalize3$2(cross3$3(pAxis, pUp));
10619
11458
  const r00 = pRight[0] * cRight[0] + pUp[0] * cUp[0] + pAxis[0] * cAxis[0];
@@ -12409,6 +13248,8 @@ function rootTopologyRewritePropagation(plan) {
12409
13248
  case "importedMesh":
12410
13249
  case "sdf":
12411
13250
  case "fromSlices":
13251
+ case "nurbsSurface":
13252
+ case "importedStep":
12412
13253
  return null;
12413
13254
  default:
12414
13255
  assertExhaustive(plan);
@@ -12832,6 +13673,8 @@ function rootPlanPropagation(plan) {
12832
13673
  case "importedMesh":
12833
13674
  case "sdf":
12834
13675
  case "fromSlices":
13676
+ case "nurbsSurface":
13677
+ case "importedStep":
12835
13678
  return void 0;
12836
13679
  default:
12837
13680
  assertExhaustive(plan);
@@ -14676,46 +15519,82 @@ function injectVoronoiSurfaceChild(children) {
14676
15519
  return child;
14677
15520
  });
14678
15521
  }
14679
- var sdf = /* @__PURE__ */ Object.freeze({
14680
- __proto__: null,
14681
- SdfShape,
14682
- SurfacePattern,
14683
- basketWeave,
14684
- bend,
14685
- blend,
15522
+ const sdf = {
15523
+ /** Create an SDF sphere centered at the origin. */
15524
+ sphere: sphere$1,
15525
+ /** Create an SDF box centered at the origin with given full dimensions (not half-extents). */
14686
15526
  box: box$1,
14687
- brick,
15527
+ /** Create an SDF cylinder centered at the origin, axis along Z. */
15528
+ cylinder: cylinder$1,
15529
+ /** Create an SDF torus centered at the origin, lying in the XY plane. */
15530
+ torus: torus$1,
15531
+ /** Create an SDF capsule centered at the origin, axis along Z. */
14688
15532
  capsule,
15533
+ /** Create an SDF cone with base at z=0 and tip at z=height. */
14689
15534
  cone,
14690
- cylinder: cylinder$1,
14691
- diamond,
14692
- fromFunction,
15535
+ /** Smooth union — blends shapes together with a smooth transition radius. */
15536
+ smoothUnion,
15537
+ /** Smooth difference — smoothly subtracts b from a. */
15538
+ smoothDifference,
15539
+ /** Smooth intersection — smoothly intersects a and b. */
15540
+ smoothIntersection,
15541
+ /** Morph between two SDF shapes. t=0 → a, t=1 → b. */
15542
+ morph,
15543
+ /**
15544
+ * Spatially blend between two SDF patterns.
15545
+ * The blend function receives (x, y, z) and returns 0..1:
15546
+ * 0 = fully pattern `a`, 1 = fully pattern `b`.
15547
+ */
15548
+ blend,
15549
+ /** Gyroid TPMS lattice — the most common lattice for additive manufacturing. */
14693
15550
  gyroid,
14694
- honeycomb,
14695
- knurl,
15551
+ /** Schwarz-P TPMS lattice — isotropic pore structure. */
15552
+ schwarzP,
15553
+ /** Diamond TPMS lattice — stiffest TPMS structure. */
15554
+ diamond,
15555
+ /** Lidinoid TPMS lattice — visually distinct from gyroid, popular in research and art. */
14696
15556
  lidinoid,
14697
- morph,
15557
+ /** 3D Simplex noise field — produces organic, natural-looking displacements. */
14698
15558
  noise,
15559
+ /** 3D Voronoi pattern — organic cellular structures like bone, coral, or soap bubbles. */
15560
+ voronoi,
15561
+ /** Honeycomb (hexagonal) lattice pattern. Intersect with your shape to apply. */
15562
+ honeycomb,
15563
+ /** Sinusoidal wave ridges — parallel ridges along an axis. */
15564
+ waves,
15565
+ /** Knurl pattern — crossed helical grooves for grips and handles. */
15566
+ knurl,
15567
+ /** Perforated plate pattern — regular array of cylindrical holes. */
14699
15568
  perforated,
14700
- repeat,
15569
+ /** Fish/dragon scale pattern — overlapping circular scales in hex-packed rows. */
14701
15570
  scales,
14702
- schwarzP,
14703
- smoothDifference,
14704
- smoothIntersection,
14705
- smoothUnion,
14706
- sphere: sphere$1,
14707
- torus: torus$1,
15571
+ /** Brick/stone wall pattern — running bond with mortar grooves. */
15572
+ brick,
15573
+ /** Grid lattice pattern — two families of infinite slabs crossing at 90°. */
15574
+ weave,
15575
+ /** Basket weave surface pattern — threads with over-under crossings in UV space. Returns a SurfacePattern for use with `.surfaceDisplace()`. */
15576
+ basketWeave,
15577
+ /** Twist an SDF shape around the Z axis. */
14708
15578
  twist,
14709
- voronoi,
14710
- waves,
14711
- weave
14712
- });
15579
+ /** Bend an SDF shape around the Z axis. */
15580
+ bend,
15581
+ /** Repeat an SDF shape in space. */
15582
+ repeat,
15583
+ /** A 2D surface pattern — a heightmap function for use with `.surfaceDisplace()`. */
15584
+ SurfacePattern,
15585
+ /** Create an SDF shape from an arbitrary distance function. You must provide bounds since the function is opaque. */
15586
+ fromFunction
15587
+ };
14713
15588
  async function initKernel() {
14714
15589
  initOCCT().catch((e) => console.warn("[kernel] OCCT background init failed:", e));
14715
15590
  const [manifoldModule] = await Promise.all([initManifoldWasm(), initMeshoptimizer()]);
14716
15591
  return manifoldModule;
14717
15592
  }
14718
15593
  let _activeBackend = "manifold";
15594
+ let _runtimeWarn = (msg) => console.warn(msg);
15595
+ function setRuntimeWarnSink(fn) {
15596
+ _runtimeWarn = fn;
15597
+ }
14719
15598
  function unwrapShapeLike(value) {
14720
15599
  if (value instanceof Shape) return value;
14721
15600
  if (value && typeof value === "object" && typeof value.toShape === "function") {
@@ -15873,6 +16752,33 @@ class Shape {
15873
16752
  static _unwrap(value) {
15874
16753
  return unwrapShapeLike(value);
15875
16754
  }
16755
+ /**
16756
+ * Warn if a boolean operation had no geometric effect.
16757
+ * Compares volumes before and after; if they match within tolerance, the operation was a no-op.
16758
+ */
16759
+ static _checkBooleanNoOp(op, base, result, others) {
16760
+ try {
16761
+ if (op === "intersection") {
16762
+ if (result.isEmpty()) {
16763
+ _runtimeWarn(
16764
+ `intersection() produced an empty shape — the operands may not overlap.`
16765
+ );
16766
+ }
16767
+ return;
16768
+ }
16769
+ if (op === "difference") {
16770
+ const volBefore = base.volume();
16771
+ const volAfter = result.volume();
16772
+ const tol = Math.max(volBefore * 1e-4, 1e-3);
16773
+ if (Math.abs(volBefore - volAfter) < tol) {
16774
+ _runtimeWarn(
16775
+ `subtract() had no effect — the tool may not overlap the base shape. Base vol=${volBefore.toFixed(1)}mm³, result vol=${volAfter.toFixed(1)}mm³`
16776
+ );
16777
+ }
16778
+ }
16779
+ } catch {
16780
+ }
16781
+ }
15876
16782
  /** Union this shape with others (additive boolean). Method form of union(). */
15877
16783
  add(...others) {
15878
16784
  const shapes = [
@@ -15910,7 +16816,7 @@ class Shape {
15910
16816
  "boolean:difference",
15911
16817
  (owner) => buildBooleanTopologyRewritePropagation("difference", owner, operandPlans)
15912
16818
  );
15913
- return setShapeCompilePlanInternal(
16819
+ const resultShape = setShapeCompilePlanInternal(
15914
16820
  setShapeGeometryInfoInternal(
15915
16821
  withBaseDimensions(this, buildShapeFromCompilePlan(nextPlan, this.colorHex)),
15916
16822
  mergeGeometryInfos(
@@ -15921,6 +16827,8 @@ class Shape {
15921
16827
  ),
15922
16828
  nextPlan
15923
16829
  );
16830
+ Shape._checkBooleanNoOp("difference", this, resultShape, shapes.slice(1));
16831
+ return resultShape;
15924
16832
  }
15925
16833
  /** Keep only the overlap with other shapes. Method form of intersection(). */
15926
16834
  intersect(...others) {
@@ -15939,7 +16847,7 @@ class Shape {
15939
16847
  "boolean:intersection",
15940
16848
  (owner) => buildBooleanTopologyRewritePropagation("intersection", owner, operandPlans)
15941
16849
  );
15942
- return setShapeCompilePlanInternal(
16850
+ const resultShape = setShapeCompilePlanInternal(
15943
16851
  setShapeGeometryInfoInternal(
15944
16852
  withMergedDimensions(shapes, buildShapeFromCompilePlan(nextPlan, this.colorHex)),
15945
16853
  mergeGeometryInfos(
@@ -15950,6 +16858,8 @@ class Shape {
15950
16858
  ),
15951
16859
  nextPlan
15952
16860
  );
16861
+ Shape._checkBooleanNoOp("intersection", this, resultShape, shapes.slice(1));
16862
+ return resultShape;
15953
16863
  }
15954
16864
  /** Alias for add() — matches the free-function union() naming. */
15955
16865
  union(...others) {
@@ -16551,6 +17461,7 @@ function intersection(...inputs) {
16551
17461
  var define_process_env_default = {};
16552
17462
  let _wasm_solve = null;
16553
17463
  let _wasm_get_profile = null;
17464
+ let _solverMemory = null;
16554
17465
  let _sessionApi = null;
16555
17466
  function getSessionApi() {
16556
17467
  return _sessionApi;
@@ -16850,9 +17761,11 @@ async function initSolverWasm() {
16850
17761
  }
16851
17762
  if (!wasmPath) throw new Error("solver_bg.wasm not found — run: npm run build:solver");
16852
17763
  const wasmBytes = readFileSync(wasmPath);
16853
- await solverModule.default(wasmBytes);
17764
+ const exports$1 = await solverModule.default(wasmBytes);
17765
+ _solverMemory = (exports$1 == null ? void 0 : exports$1.memory) ?? null;
16854
17766
  } else {
16855
- await solverModule.default();
17767
+ const exports$1 = await solverModule.default();
17768
+ _solverMemory = (exports$1 == null ? void 0 : exports$1.memory) ?? null;
16856
17769
  }
16857
17770
  performance.mark("solver:ready");
16858
17771
  performance.measure("solver:import", "solver:start", "solver:imported");
@@ -17839,7 +18752,7 @@ class MateBuilder {
17839
18752
  return this.constraints.reduce((sum, c) => sum + (CONSTRAINT_EQUATIONS[c.type] ?? 0), 0);
17840
18753
  }
17841
18754
  }
17842
- let _collected$5 = null;
18755
+ let _collected$7 = null;
17843
18756
  const isAxis = (value) => value === "x" || value === "y" || value === "z";
17844
18757
  const normalizeDirection = (value, label) => {
17845
18758
  if (value === "radial" || isAxis(value)) return value;
@@ -17898,16 +18811,16 @@ const mergeDirective = (target, patch, label) => {
17898
18811
  return out;
17899
18812
  };
17900
18813
  function resetExplodeView() {
17901
- _collected$5 = null;
18814
+ _collected$7 = null;
17902
18815
  }
17903
18816
  function getCollectedExplodeView() {
17904
- return _collected$5 ? cloneOptions(_collected$5) : null;
18817
+ return _collected$7 ? cloneOptions(_collected$7) : null;
17905
18818
  }
17906
18819
  function explodeView(options = {}) {
17907
18820
  if (!options || typeof options !== "object") {
17908
18821
  throw new Error("explodeView(options) expects an options object");
17909
18822
  }
17910
- const next = _collected$5 ? cloneOptions(_collected$5) : {};
18823
+ const next = _collected$7 ? cloneOptions(_collected$7) : {};
17911
18824
  if (options.enabled !== void 0) {
17912
18825
  if (typeof options.enabled !== "boolean") throw new Error("explodeView.enabled must be a boolean");
17913
18826
  next.enabled = options.enabled;
@@ -17955,9 +18868,9 @@ function explodeView(options = {}) {
17955
18868
  });
17956
18869
  next.byPath = byPath;
17957
18870
  }
17958
- _collected$5 = next;
18871
+ _collected$7 = next;
17959
18872
  }
17960
- let _collected$4 = null;
18873
+ let _collected$6 = null;
17961
18874
  const isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
17962
18875
  const isVec3$1 = (value) => Array.isArray(value) && value.length === 3 && isFiniteNumber(value[0]) && isFiniteNumber(value[1]) && isFiniteNumber(value[2]);
17963
18876
  const normalizeAxis = (axis) => {
@@ -18247,22 +19160,22 @@ const cloneCollected = (value) => ({
18247
19160
  defaultAnimation: value.defaultAnimation
18248
19161
  });
18249
19162
  function resetJointsView() {
18250
- _collected$4 = null;
19163
+ _collected$6 = null;
18251
19164
  }
18252
19165
  function getCollectedJointsView() {
18253
- return _collected$4 ? cloneCollected(_collected$4) : null;
19166
+ return _collected$6 ? cloneCollected(_collected$6) : null;
18254
19167
  }
18255
19168
  function saveJointsView() {
18256
- return _collected$4 ? cloneCollected(_collected$4) : null;
19169
+ return _collected$6 ? cloneCollected(_collected$6) : null;
18257
19170
  }
18258
19171
  function restoreJointsView(state) {
18259
- _collected$4 = state;
19172
+ _collected$6 = state;
18260
19173
  }
18261
19174
  function jointsView(options = {}) {
18262
19175
  if (!options || typeof options !== "object") {
18263
19176
  throw new Error("jointsView(options) expects an options object");
18264
19177
  }
18265
- const next = _collected$4 ? cloneCollected(_collected$4) : { joints: [], couplings: [], animations: [] };
19178
+ const next = _collected$6 ? cloneCollected(_collected$6) : { joints: [], couplings: [], animations: [] };
18266
19179
  if (options.enabled !== void 0) {
18267
19180
  if (typeof options.enabled !== "boolean") {
18268
19181
  throw new Error("jointsView.enabled must be a boolean");
@@ -18317,7 +19230,7 @@ function jointsView(options = {}) {
18317
19230
  if (next.defaultAnimation && !next.animations.some((animation) => animation.name === next.defaultAnimation)) {
18318
19231
  throw new Error(`jointsView defaultAnimation "${next.defaultAnimation}" does not exist in animations`);
18319
19232
  }
18320
- _collected$4 = next;
19233
+ _collected$6 = next;
18321
19234
  }
18322
19235
  function bomToCsv(rows) {
18323
19236
  const header = ["part", "qty", "material", "process", "tolerance", "notes"];
@@ -19024,8 +19937,16 @@ class Assembly {
19024
19937
  * origins (child connector lands exactly on parent connector) and derives the joint frame
19025
19938
  * and axis from the connector geometry — no manual `frame` or `axis` math needed.
19026
19939
  *
19940
+ * **Face-to-face convention:** Connectors always meet face-to-face, like a USB plug
19941
+ * meeting a socket. Each connector's axis points "outward" from its part. When two
19942
+ * connectors mate, the system brings them together so their axes oppose (anti-parallel).
19943
+ * This is the same convention used by `matchTo()`.
19944
+ *
19945
+ * For a revolute joint (hinge), both connectors' axes should point outward from their
19946
+ * respective parts along the hinge line. For a prismatic joint (slider), both axes
19947
+ * should point along the slide direction from their part's perspective.
19948
+ *
19027
19949
  * The joint type is inferred from the connector's `kind` field if not specified in `options`.
19028
- * Use `flip: true` for mirrored parts whose connector axis is reflected.
19029
19950
  *
19030
19951
  * When connectors are defined with `start`/`end`, you can control which point on each
19031
19952
  * connector meets via `align` / `parentAlign` / `childAlign` (`'start'`, `'middle'`, `'end'`).
@@ -19037,20 +19958,22 @@ class Assembly {
19037
19958
  * **Example**
19038
19959
  *
19039
19960
  * ```ts
19040
- * const mech = assembly("Arm")
19041
- * .addPart("Base", base)
19042
- * .addPart("Link", link)
19043
- * .connect("Base.top", "Link.shoulder", {
19044
- * as: "J1",
19045
- * min: -90, max: 90, default: 0,
19046
- * });
19047
- *
19048
- * return mech.solve({ J1: 45 }).toGroup();
19961
+ * // Hinge: both axes point outward along the hinge line
19962
+ * const frame = box(100, 10, 80).withConnectors({
19963
+ * hinge: connector("hinge", { origin: [0, 0, 40], axis: [0, 0, 1] }),
19964
+ * });
19965
+ * const door = box(60, 4, 80).withConnectors({
19966
+ * hinge: connector("hinge", { origin: [0, 0, 40], axis: [0, 0, -1] }),
19967
+ * });
19968
+ * assembly("Door")
19969
+ * .addPart("Frame", frame)
19970
+ * .addPart("Door", door)
19971
+ * .connect("Frame.hinge", "Door.hinge", { as: "swing", min: 0, max: 110 });
19049
19972
  * ```
19050
19973
  *
19051
19974
  * @param parentPortRef - `"PartName.connectorName"` on the parent side
19052
19975
  * @param childPortRef - `"PartName.connectorName"` on the child side
19053
- * @param options - `as` (joint name), `type`, `min`, `max`, `default`, `flip`, `align`, effort, velocity, etc.
19976
+ * @param options - `as` (joint name), `type`, `min`, `max`, `default`, `align`, effort, velocity, etc.
19054
19977
  * @returns `this` for chaining
19055
19978
  * @see {@link match} for typed connector matching with gender/type validation
19056
19979
  * @category Connectors
@@ -19067,7 +19990,7 @@ class Assembly {
19067
19990
  const childBase = childRecord.base;
19068
19991
  const childAlign = options.childAlign ?? options.align ?? "middle";
19069
19992
  const parentAlign = options.parentAlign ?? options.align ?? "middle";
19070
- const { frame, axis } = computeConnectFrame(childBase, child.port, parent.port, options.flip ?? false, childAlign, parentAlign);
19993
+ const { frame, axis } = computeConnectFrame(childBase, child.port, parent.port, false, childAlign, parentAlign);
19071
19994
  const min2 = options.min ?? child.port.min ?? parent.port.min;
19072
19995
  const max2 = options.max ?? child.port.max ?? parent.port.max;
19073
19996
  this._usedPortRefs.add(parentPortRef);
@@ -19985,12 +20908,12 @@ function bom(quantity, description, opts) {
19985
20908
  metadata
19986
20909
  });
19987
20910
  }
19988
- let _collected$3 = [];
20911
+ let _collected$5 = [];
19989
20912
  function resetCutPlanes() {
19990
- _collected$3 = [];
20913
+ _collected$5 = [];
19991
20914
  }
19992
20915
  function getCollectedCutPlanes() {
19993
- return _collected$3.slice();
20916
+ return _collected$5.slice();
19994
20917
  }
19995
20918
  function normalizeExcludedObjectNames(input) {
19996
20919
  if (input === void 0) return void 0;
@@ -20006,7 +20929,29 @@ function cutPlane(name, normal, offsetOrOptions = 0, maybeOptions = {}) {
20006
20929
  const offset2 = Number.isFinite(rawOffset) ? rawOffset : 0;
20007
20930
  const options = usingOffsetArg ? maybeOptions : offsetOrOptions;
20008
20931
  const excludeObjectNames = normalizeExcludedObjectNames(options.exclude);
20009
- _collected$3.push({ name, normal, offset: offset2, excludeObjectNames });
20932
+ _collected$5.push({ name, normal, offset: offset2, excludeObjectNames });
20933
+ }
20934
+ let _collected$4 = [];
20935
+ let _counter$1 = 0;
20936
+ function resetMocks() {
20937
+ _collected$4 = [];
20938
+ _counter$1 = 0;
20939
+ }
20940
+ function getCollectedMocks() {
20941
+ return _collected$4.slice();
20942
+ }
20943
+ function mock(shape, name) {
20944
+ if (!shape || typeof shape !== "object") {
20945
+ throw new Error("mock(shape): shape must be a Shape");
20946
+ }
20947
+ _counter$1 += 1;
20948
+ const displayName = name && typeof name === "string" && name.trim().length > 0 ? name.trim() : `Mock ${_counter$1}`;
20949
+ _collected$4.push({
20950
+ id: `mock-${_counter$1}`,
20951
+ name: displayName,
20952
+ shape
20953
+ });
20954
+ return shape;
20010
20955
  }
20011
20956
  const DEFAULT_PROFILE = {
20012
20957
  bedX: 220,
@@ -21374,11 +22319,13 @@ Shape.prototype.cutout = function cutout(sketch, opts = {}) {
21374
22319
  return shapeCutout(this, sketch, opts);
21375
22320
  };
21376
22321
  let _params = [];
22322
+ let _stringParams = [];
21377
22323
  let _listParams = [];
21378
22324
  let _overrides = {};
21379
22325
  let _scopeStack = [];
21380
22326
  function resetParams() {
21381
22327
  _params = [];
22328
+ _stringParams = [];
21382
22329
  _listParams = [];
21383
22330
  _scopeStack = [];
21384
22331
  }
@@ -21388,6 +22335,9 @@ function setParamOverrides(overrides) {
21388
22335
  function getCollectedParams() {
21389
22336
  return _params;
21390
22337
  }
22338
+ function getCollectedStringParams() {
22339
+ return _stringParams;
22340
+ }
21391
22341
  function getCollectedListParams() {
21392
22342
  return _listParams;
21393
22343
  }
@@ -21404,6 +22354,11 @@ function hasOwn(obj, key) {
21404
22354
  }
21405
22355
  function param(name, defaultValue, opts = {}) {
21406
22356
  var _a3;
22357
+ if (typeof defaultValue !== "number") {
22358
+ throw new Error(
22359
+ `param("${name}"): defaultValue must be a number, got ${typeof defaultValue}. For text parameters, use Param.string("${name}", ${JSON.stringify(defaultValue)}).`
22360
+ );
22361
+ }
21407
22362
  const scope = _scopeStack[_scopeStack.length - 1];
21408
22363
  const scopedName = (scope == null ? void 0 : scope.namePrefix) ? `${scope.namePrefix} / ${name}` : name;
21409
22364
  const scopedLocal = scope == null ? void 0 : scope.localOverrides;
@@ -21473,6 +22428,32 @@ function choiceParam(name, defaultValue, choices) {
21473
22428
  }
21474
22429
  return choices[index];
21475
22430
  }
22431
+ function stringParam(name, defaultValue, opts = {}) {
22432
+ var _a3;
22433
+ if (typeof defaultValue !== "string") {
22434
+ throw new Error(`Param.string("${name}"): defaultValue must be a string, got ${typeof defaultValue}.`);
22435
+ }
22436
+ const scope = _scopeStack[_scopeStack.length - 1];
22437
+ const scopedName = (scope == null ? void 0 : scope.namePrefix) ? `${scope.namePrefix} / ${name}` : name;
22438
+ const scopedLocal = scope == null ? void 0 : scope.localOverrides;
22439
+ const hasLocalOverride = !!(scopedLocal && Object.prototype.hasOwnProperty.call(scopedLocal, name));
22440
+ if (hasLocalOverride) (_a3 = scope.consumedKeys) == null ? void 0 : _a3.add(name);
22441
+ const rawOverride = (hasLocalOverride ? scopedLocal[name] : void 0) ?? _overrides[scopedName] ?? _overrides[name];
22442
+ const value = typeof rawOverride === "string" ? rawOverride : defaultValue;
22443
+ const maxLength = opts.maxLength;
22444
+ const clamped = maxLength !== void 0 ? value.slice(0, maxLength) : value;
22445
+ if (!hasLocalOverride) {
22446
+ _stringParams.push({ name: scopedName, value: clamped, defaultValue, maxLength });
22447
+ }
22448
+ return clamped;
22449
+ }
22450
+ const Param = {
22451
+ number: param,
22452
+ string: stringParam,
22453
+ bool: boolParam,
22454
+ choice: choiceParam,
22455
+ list: listParam
22456
+ };
21476
22457
  function listParam(name, defaultItems, opts) {
21477
22458
  if (!Array.isArray(defaultItems)) throw new Error(`listParam("${name}"): defaultItems must be an array`);
21478
22459
  const scope = _scopeStack[_scopeStack.length - 1];
@@ -22051,6 +23032,24 @@ function catmullRom2D$1(p0, p1, p2, p3, t, tension) {
22051
23032
  const h11 = ttt - tt;
22052
23033
  return [h00 * p1[0] + h10 * m1x + h01 * p2[0] + h11 * m2x, h00 * p1[1] + h10 * m1y + h01 * p2[1] + h11 * m2y];
22053
23034
  }
23035
+ function sampleBSpline2D(controlPoints, weights, degree, tol = DEFAULT_TOLERANCE$1) {
23036
+ const n = controlPoints.length;
23037
+ const knots = generateClampedKnots(n, degree);
23038
+ let polyLen = 0;
23039
+ for (let i = 1; i < n; i++) {
23040
+ polyLen += Math.hypot(controlPoints[i][0] - controlPoints[i - 1][0], controlPoints[i][1] - controlPoints[i - 1][1]);
23041
+ }
23042
+ const count = Math.max(8, Math.min(256, Math.ceil(polyLen / tol)));
23043
+ const uMin = knots[degree];
23044
+ const uMax = knots[n];
23045
+ const pts = new Array(count);
23046
+ for (let i = 0; i < count; i++) {
23047
+ const t = i / (count - 1);
23048
+ const u = uMin + t * (uMax - uMin);
23049
+ pts[i] = deBoor2D(controlPoints, weights, knots, degree, u);
23050
+ }
23051
+ return pts;
23052
+ }
22054
23053
  function ensureCCW(pts) {
22055
23054
  let signedArea2 = 0;
22056
23055
  for (let i = 0; i < pts.length; i++) {
@@ -22425,6 +23424,82 @@ class PathBuilder {
22425
23424
  this.y = last[1];
22426
23425
  return this;
22427
23426
  }
23427
+ // ── NURBS / exact curves ──────────────────────────────────────────────────
23428
+ /**
23429
+ * Rational B-spline edge to (x, y) with explicit control points and weights.
23430
+ *
23431
+ * The control points define the B-spline shape between the current position
23432
+ * and (x, y). The current position is NOT included in `controlPoints` — it is
23433
+ * automatically prepended. The endpoint (x, y) is the last control point.
23434
+ *
23435
+ * @param controlPoints — interior + endpoint control points (endpoint = last)
23436
+ * @param opts.weights — rational weights (default: all 1.0)
23437
+ * @param opts.degree — B-spline degree (default: control point count - 1, capped at 3)
23438
+ */
23439
+ nurbsTo(controlPoints, opts) {
23440
+ if (controlPoints.length < 1) throw new Error("nurbsTo: need at least 1 control point (the endpoint)");
23441
+ const allPts = [[this.x, this.y], ...controlPoints];
23442
+ const n = allPts.length;
23443
+ const degree = (opts == null ? void 0 : opts.degree) ?? Math.min(n - 1, 3);
23444
+ const weights = (opts == null ? void 0 : opts.weights) ?? new Array(n).fill(1);
23445
+ if (weights.length !== n) throw new Error(`nurbsTo: weights.length (${weights.length}) must match total control points (${n})`);
23446
+ const last = controlPoints[controlPoints.length - 1];
23447
+ this.segs.push({ kind: "bspline", x: last[0], y: last[1], controlPoints: allPts, weights, degree });
23448
+ const prevPt = allPts[n - 2];
23449
+ const tdx = last[0] - prevPt[0];
23450
+ const tdy = last[1] - prevPt[1];
23451
+ const tlen = Math.hypot(tdx, tdy);
23452
+ if (tlen > 1e-9) {
23453
+ this.dirX = tdx / tlen;
23454
+ this.dirY = tdy / tlen;
23455
+ }
23456
+ this.x = last[0];
23457
+ this.y = last[1];
23458
+ return this;
23459
+ }
23460
+ /**
23461
+ * Exact circular arc to (x, y) using a rational quadratic NURBS.
23462
+ *
23463
+ * Unlike `arcTo()` which tessellates to a polyline, this preserves the
23464
+ * exact arc definition. When extruded through the OCCT backend, it produces
23465
+ * a true cylindrical face — not a faceted approximation.
23466
+ *
23467
+ * @param x — endpoint X
23468
+ * @param y — endpoint Y
23469
+ * @param opts.radius — arc radius (default: auto-computed from chord)
23470
+ * @param opts.clockwise — winding direction (default: false = CCW)
23471
+ */
23472
+ exactArcTo(x, y, opts) {
23473
+ const clockwise = (opts == null ? void 0 : opts.clockwise) ?? false;
23474
+ const dx = x - this.x;
23475
+ const dy = y - this.y;
23476
+ const chordLen = Math.hypot(dx, dy);
23477
+ if (chordLen < 1e-9) return this;
23478
+ const radius = (opts == null ? void 0 : opts.radius) ?? chordLen;
23479
+ if (radius < chordLen / 2 - 1e-9) throw new Error("exactArcTo: radius too small for the chord");
23480
+ const mx = (this.x + x) / 2;
23481
+ const my = (this.y + y) / 2;
23482
+ let nx = -dy / chordLen;
23483
+ let ny = dx / chordLen;
23484
+ if (clockwise) {
23485
+ nx = -nx;
23486
+ ny = -ny;
23487
+ }
23488
+ const h = Math.sqrt(Math.max(0, radius * radius - chordLen / 2 * (chordLen / 2)));
23489
+ const cx = mx + nx * h;
23490
+ const cy = my + ny * h;
23491
+ const halfAngle = Math.asin(Math.min(1, chordLen / (2 * radius)));
23492
+ const w = Math.cos(halfAngle);
23493
+ const smx = (this.x + x) / 2 - cx;
23494
+ const smy = (this.y + y) / 2 - cy;
23495
+ const smLen = Math.hypot(smx, smy);
23496
+ const shoulderX = cx + smx / smLen * radius;
23497
+ const shoulderY = cy + smy / smLen * radius;
23498
+ return this.nurbsTo(
23499
+ [[shoulderX, shoulderY], [x, y]],
23500
+ { weights: [1, w, 1], degree: 2 }
23501
+ );
23502
+ }
22428
23503
  // ── Corner modifiers ──────────────────────────────────────────────────────
22429
23504
  /**
22430
23505
  * Round the last corner (the junction between the previous two segments)
@@ -22749,6 +23824,11 @@ class PathBuilder {
22749
23824
  const last = seg.points[seg.points.length - 1];
22750
23825
  px = last[0];
22751
23826
  py = last[1];
23827
+ } else if (seg.kind === "bspline") {
23828
+ const sampled = sampleBSpline2D(seg.controlPoints, seg.weights, seg.degree);
23829
+ for (let i = 1; i < sampled.length; i++) pts.push(sampled[i]);
23830
+ px = seg.x;
23831
+ py = seg.y;
22752
23832
  }
22753
23833
  }
22754
23834
  return pts;
@@ -22785,11 +23865,23 @@ class PathBuilder {
22785
23865
  close() {
22786
23866
  const subPaths = this.splitSubPaths();
22787
23867
  if (subPaths.length === 0) throw new Error("Path needs at least 3 points");
23868
+ const hasBSpline = this.segs.some((s) => s.kind === "bspline");
22788
23869
  const tessellated = subPaths.map((segs) => this.tessellateSegs(segs));
22789
23870
  const outer = tessellated[0];
22790
23871
  if (outer.length < 3) throw new Error("Path needs at least 3 points");
22791
23872
  ensureCCW(outer);
22792
- let result = polygon(outer);
23873
+ let result;
23874
+ if (hasBSpline && subPaths.length === 1) {
23875
+ const edges = this.buildProfileEdges(subPaths[0]);
23876
+ result = buildSketchFromCompileProfilePlan({
23877
+ kind: "pathProfile",
23878
+ points: outer,
23879
+ edges,
23880
+ transforms: []
23881
+ });
23882
+ } else {
23883
+ result = polygon(outer);
23884
+ }
22793
23885
  for (let i = 1; i < tessellated.length; i++) {
22794
23886
  const hole2 = tessellated[i];
22795
23887
  if (hole2.length < 3) continue;
@@ -22916,6 +24008,48 @@ class PathBuilder {
22916
24008
  if (current.length > 0) paths.push(current);
22917
24009
  return paths;
22918
24010
  }
24011
+ /** Build semantic ProfileEdge array from path segments (for pathProfile compile plan). */
24012
+ buildProfileEdges(segs) {
24013
+ const edges = [];
24014
+ let px = 0, py = 0;
24015
+ for (const seg of segs) {
24016
+ if (seg.kind === "move") {
24017
+ px = seg.x;
24018
+ py = seg.y;
24019
+ } else if (seg.kind === "line") {
24020
+ edges.push({ kind: "line", x1: px, y1: py, x2: seg.x, y2: seg.y });
24021
+ px = seg.x;
24022
+ py = seg.y;
24023
+ } else if (seg.kind === "arc") {
24024
+ edges.push({ kind: "arc", x1: px, y1: py, x2: seg.x, y2: seg.y, cx: seg.cx, cy: seg.cy, clockwise: seg.clockwise });
24025
+ px = seg.x;
24026
+ py = seg.y;
24027
+ } else if (seg.kind === "bspline") {
24028
+ edges.push({
24029
+ kind: "bspline",
24030
+ controlPoints: seg.controlPoints,
24031
+ weights: seg.weights,
24032
+ knots: generateClampedKnots(seg.controlPoints.length, seg.degree),
24033
+ degree: seg.degree
24034
+ });
24035
+ px = seg.x;
24036
+ py = seg.y;
24037
+ } else {
24038
+ edges.push({ kind: "line", x1: px, y1: py, x2: seg.x, y2: seg.y });
24039
+ px = seg.x;
24040
+ py = seg.y;
24041
+ }
24042
+ }
24043
+ if (segs.length > 0) {
24044
+ const first = segs[0];
24045
+ const firstX = first.kind === "move" ? first.x : 0;
24046
+ const firstY = first.kind === "move" ? first.y : 0;
24047
+ if (Math.hypot(px - firstX, py - firstY) > 1e-9) {
24048
+ edges.push({ kind: "line", x1: px, y1: py, x2: firstX, y2: firstY });
24049
+ }
24050
+ }
24051
+ return edges;
24052
+ }
22919
24053
  /** Tessellate a sub-path (sequence of segments). */
22920
24054
  tessellateSegs(segs) {
22921
24055
  const pts = [];
@@ -22943,6 +24077,11 @@ class PathBuilder {
22943
24077
  const last = seg.points[seg.points.length - 1];
22944
24078
  px = last[0];
22945
24079
  py = last[1];
24080
+ } else if (seg.kind === "bspline") {
24081
+ const sampled = sampleBSpline2D(seg.controlPoints, seg.weights, seg.degree);
24082
+ for (let i = 1; i < sampled.length; i++) pts.push(sampled[i]);
24083
+ px = seg.x;
24084
+ py = seg.y;
22946
24085
  }
22947
24086
  }
22948
24087
  return pts;
@@ -24229,7 +25368,20 @@ function spurGear(options) {
24229
25368
  profile = difference2d(profile, circle2d(normalized.boreDiameter * 0.5, Math.max(48, normalized.teeth * 2)));
24230
25369
  }
24231
25370
  const shape = sketchExtrude(profile, normalized.faceWidth);
24232
- return attachGearMeta(shape, meta2);
25371
+ const shapeWithConnectors = shape.withConnectors({
25372
+ bore: connectorFactory("gear-bore", {
25373
+ origin: [0, 0, normalized.faceWidth / 2],
25374
+ axis: [0, 0, 1],
25375
+ kind: "revolute"
25376
+ }, {
25377
+ module: normalized.module,
25378
+ teeth: normalized.teeth,
25379
+ pitchRadius: meta2.pitchRadius,
25380
+ outerRadius: meta2.outerRadius,
25381
+ faceWidth: normalized.faceWidth
25382
+ })
25383
+ });
25384
+ return attachGearMeta(shapeWithConnectors, meta2);
24233
25385
  }
24234
25386
  function normalizeSideGearOptions(options) {
24235
25387
  let normalizedSpur;
@@ -24315,7 +25467,23 @@ function sideGear(options) {
24315
25467
  );
24316
25468
  shape = shape.subtract(bore);
24317
25469
  }
24318
- return attachGearMeta(shape, meta2);
25470
+ const boreZ = normalized.side === "top" ? zBands.bodyMinZ : zBands.bodyMaxZ;
25471
+ const boreAxis = normalized.side === "top" ? [0, 0, -1] : [0, 0, 1];
25472
+ const shapeWithConnectors = shape.withConnectors({
25473
+ bore: connectorFactory("gear-bore", {
25474
+ origin: [0, 0, boreZ],
25475
+ axis: boreAxis,
25476
+ kind: "revolute"
25477
+ }, {
25478
+ module: normalized.module,
25479
+ teeth: normalized.teeth,
25480
+ pitchRadius: meta2.pitchRadius,
25481
+ outerRadius: meta2.outerRadius,
25482
+ faceWidth: normalized.faceWidth,
25483
+ toothSide: normalized.side
25484
+ })
25485
+ });
25486
+ return attachGearMeta(shapeWithConnectors, meta2);
24319
25487
  }
24320
25488
  function faceGear(options) {
24321
25489
  try {
@@ -24432,7 +25600,20 @@ function ringGear(options) {
24432
25600
  }
24433
25601
  const profile = difference2d(ringBlank, union2d(...spaces));
24434
25602
  const shape = sketchExtrude(profile, normalized.faceWidth);
24435
- return attachGearMeta(shape, meta2);
25603
+ const shapeWithConnectors = shape.withConnectors({
25604
+ bore: connectorFactory("ring-bore", {
25605
+ origin: [0, 0, normalized.faceWidth / 2],
25606
+ axis: [0, 0, 1]
25607
+ }, {
25608
+ module: normalized.module,
25609
+ teeth: normalized.teeth,
25610
+ pitchRadius,
25611
+ innerRadius: tipRadius,
25612
+ outerRadius: normalized.outerRadius,
25613
+ faceWidth: normalized.faceWidth
25614
+ })
25615
+ });
25616
+ return attachGearMeta(shapeWithConnectors, meta2);
24436
25617
  }
24437
25618
  function rackGear(options) {
24438
25619
  if (!isFinitePositive(options.module)) throw new Error('rackGear: "module" must be > 0');
@@ -24483,7 +25664,8 @@ function rackGear(options) {
24483
25664
  teethSketches.push(sketchTranslate(toothSketch, cx, 0));
24484
25665
  }
24485
25666
  const span = (options.teeth - 1) * pitch + halfRoot * 2;
24486
- const base = sketchTranslate(rect(span + module * 2, baseHeight), 0, -dedendum - baseHeight * 0.5);
25667
+ const length4 = span + module * 2;
25668
+ const base = sketchTranslate(rect(length4, baseHeight), 0, -dedendum - baseHeight * 0.5);
24487
25669
  const profile = union2d(base, ...teethSketches);
24488
25670
  const shape = sketchExtrude(profile, options.faceWidth);
24489
25671
  const meta2 = {
@@ -24502,7 +25684,20 @@ function rackGear(options) {
24502
25684
  backlash,
24503
25685
  centered: false
24504
25686
  };
24505
- return attachGearMeta(shape, meta2);
25687
+ const shapeWithConnectors = shape.withConnectors({
25688
+ teeth: connectorFactory("rack-teeth", {
25689
+ origin: [0, 0, options.faceWidth / 2],
25690
+ axis: [1, 0, 0],
25691
+ up: [0, 1, 0],
25692
+ kind: "prismatic"
25693
+ }, {
25694
+ module,
25695
+ teeth: options.teeth,
25696
+ faceWidth: options.faceWidth,
25697
+ length: length4
25698
+ })
25699
+ });
25700
+ return attachGearMeta(shapeWithConnectors, meta2);
24506
25701
  }
24507
25702
  function normalizeShaftAngle(label, value) {
24508
25703
  if (!isFinitePositive(value) || value >= 175) {
@@ -24581,7 +25776,27 @@ function bevelGear(options) {
24581
25776
  const shape = sketchExtrude(profile, normalized.faceWidth, {
24582
25777
  scaleTop: normalized.topScale
24583
25778
  });
24584
- return attachGearMeta(shape, {
25779
+ const apexZ = normalized.module * normalized.teeth * 0.5 / Math.tan(normalized.pitchAngleRad);
25780
+ const measurements = {
25781
+ module: normalized.module,
25782
+ teeth: normalized.teeth,
25783
+ pitchRadius: meta2.pitchRadius,
25784
+ pitchAngleDeg: normalized.pitchAngleDeg,
25785
+ coneDistance: normalized.coneDistance,
25786
+ faceWidth: normalized.faceWidth
25787
+ };
25788
+ const shapeWithConnectors = shape.withConnectors({
25789
+ bore: connectorFactory("gear-bore", {
25790
+ origin: [0, 0, 0],
25791
+ axis: [0, 0, -1],
25792
+ kind: "revolute"
25793
+ }, measurements),
25794
+ apex: connectorFactory("bevel-apex", {
25795
+ origin: [0, 0, apexZ],
25796
+ axis: [0, 0, 1]
25797
+ }, measurements)
25798
+ });
25799
+ return attachGearMeta(shapeWithConnectors, {
24585
25800
  ...meta2,
24586
25801
  kind: "bevel",
24587
25802
  centered: false,
@@ -25063,6 +26278,70 @@ function faceGearPair(options) {
25063
26278
  status: pairStatusFromDiagnostics(diagnostics)
25064
26279
  };
25065
26280
  }
26281
+ function gearRatio(teethA, teethB, options) {
26282
+ if (!Number.isFinite(teethA) || teethA <= 0) throw new Error("gearRatio: teethA must be > 0");
26283
+ if (!Number.isFinite(teethB) || teethB <= 0) throw new Error("gearRatio: teethB must be > 0");
26284
+ const sign = (options == null ? void 0 : options.internal) ? 1 : -1;
26285
+ return sign * teethA / teethB;
26286
+ }
26287
+ function rackRatio(module, pinionTeeth) {
26288
+ if (!Number.isFinite(module) || module <= 0) throw new Error("rackRatio: module must be > 0");
26289
+ if (!Number.isFinite(pinionTeeth) || pinionTeeth <= 0) throw new Error("rackRatio: pinionTeeth must be > 0");
26290
+ const pitchRadius = module * pinionTeeth / 2;
26291
+ return 180 / (Math.PI * pitchRadius);
26292
+ }
26293
+ function planetaryRatio(sunTeeth, ringTeeth) {
26294
+ if (!Number.isFinite(sunTeeth) || sunTeeth <= 0) throw new Error("planetaryRatio: sunTeeth must be > 0");
26295
+ if (!Number.isFinite(ringTeeth) || ringTeeth <= 0) throw new Error("planetaryRatio: ringTeeth must be > 0");
26296
+ return 1 + ringTeeth / sunTeeth;
26297
+ }
26298
+ function boltPattern(options) {
26299
+ const sizeData = METRIC_HOLE_TABLE[options.size];
26300
+ if (!sizeData) throw new Error(`boltPattern: unsupported size "${options.size}"`);
26301
+ const fit = options.fit ?? "normal";
26302
+ const dia = sizeData[fit];
26303
+ const segments = options.segments ?? 48;
26304
+ if (!Array.isArray(options.positions) || options.positions.length === 0) {
26305
+ throw new Error('boltPattern: "positions" must be a non-empty array of [x, y] pairs');
26306
+ }
26307
+ const positions = options.positions.map((p2, i) => {
26308
+ if (!Array.isArray(p2) || p2.length !== 2 || !Number.isFinite(p2[0]) || !Number.isFinite(p2[1])) {
26309
+ throw new Error(`boltPattern: position[${i}] must be a finite [x, y] pair`);
26310
+ }
26311
+ return [p2[0], p2[1]];
26312
+ });
26313
+ const xs = positions.map((p2) => p2[0]);
26314
+ const ys = positions.map((p2) => p2[1]);
26315
+ return {
26316
+ size: options.size,
26317
+ dia,
26318
+ positions,
26319
+ minX: Math.min(...xs),
26320
+ maxX: Math.max(...xs),
26321
+ minY: Math.min(...ys),
26322
+ maxY: Math.max(...ys),
26323
+ cut(shape, depth, cutOptions = {}) {
26324
+ if (!Number.isFinite(depth) || depth <= 0) {
26325
+ throw new Error("boltPattern.cut: depth must be > 0");
26326
+ }
26327
+ const from = cutOptions.from ?? 0;
26328
+ const cutter = fastenerHole({
26329
+ size: options.size,
26330
+ fit,
26331
+ depth,
26332
+ center: false,
26333
+ segments,
26334
+ counterbore: cutOptions.counterbore,
26335
+ countersink: cutOptions.countersink
26336
+ });
26337
+ let result = shape;
26338
+ for (const [x, y] of positions) {
26339
+ result = result.subtract(cutter.translate(x, y, from));
26340
+ }
26341
+ return result;
26342
+ }
26343
+ };
26344
+ }
25066
26345
  function thread(diameter, pitch, length4, options) {
25067
26346
  const r = diameter / 2;
25068
26347
  const depth = (options == null ? void 0 : options.depth) ?? pitch * 0.35;
@@ -25222,7 +26501,11 @@ const partLibrary = {
25222
26501
  gearPair,
25223
26502
  bevelGearPair,
25224
26503
  faceGearPair,
25225
- sideGearPair
26504
+ sideGearPair,
26505
+ gearRatio,
26506
+ rackRatio,
26507
+ planetaryRatio,
26508
+ boltPattern
25226
26509
  };
25227
26510
  /**
25228
26511
  * @license
@@ -36725,14 +38008,14 @@ class WoodBoard {
36725
38008
  return this._withShape(this.shape.clone());
36726
38009
  }
36727
38010
  }
36728
- function requireFinite$3(value, name) {
38011
+ function requireFinite$5(value, name) {
36729
38012
  if (!Number.isFinite(value)) {
36730
38013
  throw new Error(`${name} must be a finite number, got ${value}`);
36731
38014
  }
36732
38015
  return value;
36733
38016
  }
36734
38017
  function requirePositive$2(value, name) {
36735
- requireFinite$3(value, name);
38018
+ requireFinite$5(value, name);
36736
38019
  if (value <= 0) {
36737
38020
  throw new Error(`${name} must be positive, got ${value}`);
36738
38021
  }
@@ -36767,10 +38050,10 @@ function dado(host, guest, opts) {
36767
38050
  }
36768
38051
  let fromBottom;
36769
38052
  if (opts.fromBottom != null) {
36770
- fromBottom = requireFinite$3(opts.fromBottom, "fromBottom");
38053
+ fromBottom = requireFinite$5(opts.fromBottom, "fromBottom");
36771
38054
  } else {
36772
38055
  fromBottom = host.height - opts.fromTop - channelWidth;
36773
- requireFinite$3(fromBottom, "computed fromBottom");
38056
+ requireFinite$5(fromBottom, "computed fromBottom");
36774
38057
  }
36775
38058
  let dadoLength = host.width;
36776
38059
  let xOffset = 0;
@@ -36827,7 +38110,7 @@ function mortiseAndTenon(mortiseBoard, tenonBoard, opts) {
36827
38110
  const style = o.style ?? "blind";
36828
38111
  const fit = o.fit ?? "snug";
36829
38112
  const clearance = clearanceForFit(fit);
36830
- const cornerRadius = o.cornerRadius != null ? requireFinite$3(o.cornerRadius, "cornerRadius") : 0;
38113
+ const cornerRadius = o.cornerRadius != null ? requireFinite$5(o.cornerRadius, "cornerRadius") : 0;
36831
38114
  const tenonThickness = o.tenonThickness != null ? requirePositive$2(o.tenonThickness, "tenonThickness") : tenonBoard.thickness / 3;
36832
38115
  const tenonWidth = o.tenonWidth != null ? requirePositive$2(o.tenonWidth, "tenonWidth") : Math.min(tenonBoard.height * 0.6, mortiseBoard.height * 0.8);
36833
38116
  const tenonLength = o.tenonLength != null ? requirePositive$2(o.tenonLength, "tenonLength") : style === "through" ? mortiseBoard.thickness : mortiseBoard.thickness * 2 / 3;
@@ -36840,10 +38123,10 @@ function mortiseAndTenon(mortiseBoard, tenonBoard, opts) {
36840
38123
  throw new Error("mortiseAndTenon: specify position.fromTop or position.fromBottom, not both");
36841
38124
  }
36842
38125
  if (o.position.fromTop != null) {
36843
- requireFinite$3(o.position.fromTop, "position.fromTop");
38126
+ requireFinite$5(o.position.fromTop, "position.fromTop");
36844
38127
  mortiseCenterY = mortiseBoard.height / 2 - o.position.fromTop - mortiseH / 2;
36845
38128
  } else if (o.position.fromBottom != null) {
36846
- requireFinite$3(o.position.fromBottom, "position.fromBottom");
38129
+ requireFinite$5(o.position.fromBottom, "position.fromBottom");
36847
38130
  mortiseCenterY = -mortiseBoard.height / 2 + o.position.fromBottom + mortiseH / 2;
36848
38131
  }
36849
38132
  }
@@ -36913,6 +38196,94 @@ const Wood = {
36913
38196
  */
36914
38197
  mortiseAndTenon
36915
38198
  };
38199
+ let _collected$3 = null;
38200
+ function resetCameraTrajectory() {
38201
+ _collected$3 = null;
38202
+ }
38203
+ function getCollectedCameraTrajectory() {
38204
+ return _collected$3;
38205
+ }
38206
+ function isOrbitKeyframe(kf) {
38207
+ return "orbit" in kf;
38208
+ }
38209
+ function isCartesianKeyframe(kf) {
38210
+ return "position" in kf;
38211
+ }
38212
+ function requireFinite$4(value, label) {
38213
+ if (!Number.isFinite(value)) {
38214
+ throw new Error(`cameraTrajectory(): ${label} must be a finite number, got ${value}`);
38215
+ }
38216
+ }
38217
+ function validateOrbitKeyframe(kf, index) {
38218
+ requireFinite$4(kf.at, `keyframes[${index}].at`);
38219
+ requireFinite$4(kf.orbit.angle, `keyframes[${index}].orbit.angle`);
38220
+ requireFinite$4(kf.orbit.pitch, `keyframes[${index}].orbit.pitch`);
38221
+ requireFinite$4(kf.orbit.distance, `keyframes[${index}].orbit.distance`);
38222
+ }
38223
+ function validateCartesianKeyframe(kf, index) {
38224
+ requireFinite$4(kf.at, `keyframes[${index}].at`);
38225
+ if (!Array.isArray(kf.position) || kf.position.length !== 3) {
38226
+ throw new Error(`cameraTrajectory(): keyframes[${index}].position must be a 3-element array`);
38227
+ }
38228
+ if (!Array.isArray(kf.target) || kf.target.length !== 3) {
38229
+ throw new Error(`cameraTrajectory(): keyframes[${index}].target must be a 3-element array`);
38230
+ }
38231
+ for (let i = 0; i < 3; i++) {
38232
+ requireFinite$4(kf.position[i], `keyframes[${index}].position[${i}]`);
38233
+ requireFinite$4(kf.target[i], `keyframes[${index}].target[${i}]`);
38234
+ }
38235
+ }
38236
+ function validateKeyframeOrder(keyframes) {
38237
+ if (keyframes.length < 2) {
38238
+ throw new Error("cameraTrajectory(): keyframes must contain at least 2 entries");
38239
+ }
38240
+ for (let i = 0; i < keyframes.length; i++) {
38241
+ const at = keyframes[i].at;
38242
+ if (at < 0 || at > 1) {
38243
+ throw new Error(`cameraTrajectory(): keyframes[${i}].at must be in [0, 1], got ${at}`);
38244
+ }
38245
+ if (i > 0 && at < keyframes[i - 1].at) {
38246
+ throw new Error(
38247
+ `cameraTrajectory(): keyframes must be sorted by 'at' ascending — keyframes[${i - 1}].at=${keyframes[i - 1].at} > keyframes[${i}].at=${at}`
38248
+ );
38249
+ }
38250
+ }
38251
+ }
38252
+ function cameraTrajectory(defOrFn, options) {
38253
+ if (_collected$3 !== null) {
38254
+ console.warn("cameraTrajectory() called more than once — overwriting previous trajectory.");
38255
+ }
38256
+ if (typeof defOrFn === "function") {
38257
+ _collected$3 = {
38258
+ kind: "parametric",
38259
+ parametricFn: defOrFn,
38260
+ duration: options == null ? void 0 : options.duration,
38261
+ fps: options == null ? void 0 : options.fps
38262
+ };
38263
+ return;
38264
+ }
38265
+ const { keyframes, duration, fps, easing } = defOrFn;
38266
+ if (!Array.isArray(keyframes) || keyframes.length === 0) {
38267
+ throw new Error("cameraTrajectory(): keyframes must be a non-empty array");
38268
+ }
38269
+ validateKeyframeOrder(keyframes);
38270
+ const first = keyframes[0];
38271
+ if (isOrbitKeyframe(first)) {
38272
+ const orbitKeyframes = keyframes;
38273
+ for (let i = 0; i < orbitKeyframes.length; i++) {
38274
+ validateOrbitKeyframe(orbitKeyframes[i], i);
38275
+ }
38276
+ _collected$3 = { kind: "orbit-keyframes", orbitKeyframes, duration, fps, easing };
38277
+ } else if (isCartesianKeyframe(first)) {
38278
+ const cartesianKeyframes = keyframes;
38279
+ for (let i = 0; i < cartesianKeyframes.length; i++) {
38280
+ validateCartesianKeyframe(cartesianKeyframes[i], i);
38281
+ }
38282
+ _collected$3 = { kind: "cartesian-keyframes", cartesianKeyframes, duration, fps, easing };
38283
+ } else {
38284
+ throw new Error('cameraTrajectory(): each keyframe must have either an "orbit" or "position" property');
38285
+ }
38286
+ }
36916
38287
  function resolveEdges(shape, edges) {
36917
38288
  if (!edges) {
36918
38289
  return selectEdges(shape);
@@ -37020,7 +38391,7 @@ function offsetSolid(shape, thickness) {
37020
38391
  sources: ["offset-solid"]
37021
38392
  });
37022
38393
  }
37023
- function requireFinite$2(value, label) {
38394
+ function requireFinite$3(value, label) {
37024
38395
  if (typeof value !== "number" || !Number.isFinite(value)) {
37025
38396
  throw new Error(`${label} must be a finite number`);
37026
38397
  }
@@ -37030,7 +38401,7 @@ function requireVec3(value, label) {
37030
38401
  if (!Array.isArray(value) || value.length !== 3) {
37031
38402
  throw new Error(`${label} must be [x, y, z]`);
37032
38403
  }
37033
- return [requireFinite$2(value[0], `${label}[0]`), requireFinite$2(value[1], `${label}[1]`), requireFinite$2(value[2], `${label}[2]`)];
38404
+ return [requireFinite$3(value[0], `${label}[0]`), requireFinite$3(value[1], `${label}[1]`), requireFinite$3(value[2], `${label}[2]`)];
37034
38405
  }
37035
38406
  function requireColor(value, label) {
37036
38407
  if (typeof value !== "string" || !value.trim()) {
@@ -37058,7 +38429,7 @@ function validateCamera(cam, label) {
37058
38429
  if (cam.target !== void 0) out.target = requireVec3(cam.target, `${label}.target`);
37059
38430
  if (cam.up !== void 0) out.up = requireVec3(cam.up, `${label}.up`);
37060
38431
  if (cam.fov !== void 0) {
37061
- out.fov = requireFinite$2(cam.fov, `${label}.fov`);
38432
+ out.fov = requireFinite$3(cam.fov, `${label}.fov`);
37062
38433
  if (out.fov <= 0 || out.fov >= 180) throw new Error(`${label}.fov must be between 0 and 180`);
37063
38434
  }
37064
38435
  if (cam.type !== void 0) {
@@ -37076,15 +38447,15 @@ function validateLight(light, label) {
37076
38447
  }
37077
38448
  const out = { type: light.type };
37078
38449
  if (light.color !== void 0) out.color = requireColor(light.color, `${label}.color`);
37079
- if (light.intensity !== void 0) out.intensity = requireFinite$2(light.intensity, `${label}.intensity`);
38450
+ if (light.intensity !== void 0) out.intensity = requireFinite$3(light.intensity, `${label}.intensity`);
37080
38451
  if (light.position !== void 0) out.position = requireVec3(light.position, `${label}.position`);
37081
38452
  if (light.target !== void 0) out.target = requireVec3(light.target, `${label}.target`);
37082
38453
  if (light.groundColor !== void 0) out.groundColor = requireColor(light.groundColor, `${label}.groundColor`);
37083
38454
  if (light.skyColor !== void 0) out.skyColor = requireColor(light.skyColor, `${label}.skyColor`);
37084
- if (light.angle !== void 0) out.angle = requireFinite$2(light.angle, `${label}.angle`);
37085
- if (light.penumbra !== void 0) out.penumbra = requireFinite$2(light.penumbra, `${label}.penumbra`);
37086
- if (light.decay !== void 0) out.decay = requireFinite$2(light.decay, `${label}.decay`);
37087
- if (light.distance !== void 0) out.distance = requireFinite$2(light.distance, `${label}.distance`);
38455
+ if (light.angle !== void 0) out.angle = requireFinite$3(light.angle, `${label}.angle`);
38456
+ if (light.penumbra !== void 0) out.penumbra = requireFinite$3(light.penumbra, `${label}.penumbra`);
38457
+ if (light.decay !== void 0) out.decay = requireFinite$3(light.decay, `${label}.decay`);
38458
+ if (light.distance !== void 0) out.distance = requireFinite$3(light.distance, `${label}.distance`);
37088
38459
  if (light.castShadow !== void 0) {
37089
38460
  if (typeof light.castShadow !== "boolean") throw new Error(`${label}.castShadow must be a boolean`);
37090
38461
  out.castShadow = light.castShadow;
@@ -37099,7 +38470,7 @@ function validateEnvironment(env, label) {
37099
38470
  }
37100
38471
  out.preset = env.preset;
37101
38472
  }
37102
- if (env.intensity !== void 0) out.intensity = requireFinite$2(env.intensity, `${label}.intensity`);
38473
+ if (env.intensity !== void 0) out.intensity = requireFinite$3(env.intensity, `${label}.intensity`);
37103
38474
  if (env.background !== void 0) {
37104
38475
  if (typeof env.background !== "boolean") throw new Error(`${label}.background must be a boolean`);
37105
38476
  out.background = env.background;
@@ -37109,9 +38480,9 @@ function validateEnvironment(env, label) {
37109
38480
  function validateFog(fog, label) {
37110
38481
  const out = {};
37111
38482
  if (fog.color !== void 0) out.color = requireColor(fog.color, `${label}.color`);
37112
- if (fog.near !== void 0) out.near = requireFinite$2(fog.near, `${label}.near`);
37113
- if (fog.far !== void 0) out.far = requireFinite$2(fog.far, `${label}.far`);
37114
- if (fog.density !== void 0) out.density = requireFinite$2(fog.density, `${label}.density`);
38483
+ if (fog.near !== void 0) out.near = requireFinite$3(fog.near, `${label}.near`);
38484
+ if (fog.far !== void 0) out.far = requireFinite$3(fog.far, `${label}.far`);
38485
+ if (fog.density !== void 0) out.density = requireFinite$3(fog.density, `${label}.density`);
37115
38486
  return out;
37116
38487
  }
37117
38488
  function validatePostProcessing(pp, label) {
@@ -37119,23 +38490,23 @@ function validatePostProcessing(pp, label) {
37119
38490
  if (pp.bloom !== void 0) {
37120
38491
  if (!pp.bloom || typeof pp.bloom !== "object") throw new Error(`${label}.bloom must be an object`);
37121
38492
  out.bloom = {};
37122
- if (pp.bloom.intensity !== void 0) out.bloom.intensity = requireFinite$2(pp.bloom.intensity, `${label}.bloom.intensity`);
37123
- if (pp.bloom.threshold !== void 0) out.bloom.threshold = requireFinite$2(pp.bloom.threshold, `${label}.bloom.threshold`);
37124
- if (pp.bloom.radius !== void 0) out.bloom.radius = requireFinite$2(pp.bloom.radius, `${label}.bloom.radius`);
38493
+ if (pp.bloom.intensity !== void 0) out.bloom.intensity = requireFinite$3(pp.bloom.intensity, `${label}.bloom.intensity`);
38494
+ if (pp.bloom.threshold !== void 0) out.bloom.threshold = requireFinite$3(pp.bloom.threshold, `${label}.bloom.threshold`);
38495
+ if (pp.bloom.radius !== void 0) out.bloom.radius = requireFinite$3(pp.bloom.radius, `${label}.bloom.radius`);
37125
38496
  }
37126
38497
  if (pp.vignette !== void 0) {
37127
38498
  if (!pp.vignette || typeof pp.vignette !== "object") throw new Error(`${label}.vignette must be an object`);
37128
38499
  out.vignette = {};
37129
- if (pp.vignette.darkness !== void 0) out.vignette.darkness = requireFinite$2(pp.vignette.darkness, `${label}.vignette.darkness`);
37130
- if (pp.vignette.offset !== void 0) out.vignette.offset = requireFinite$2(pp.vignette.offset, `${label}.vignette.offset`);
38500
+ if (pp.vignette.darkness !== void 0) out.vignette.darkness = requireFinite$3(pp.vignette.darkness, `${label}.vignette.darkness`);
38501
+ if (pp.vignette.offset !== void 0) out.vignette.offset = requireFinite$3(pp.vignette.offset, `${label}.vignette.offset`);
37131
38502
  }
37132
38503
  if (pp.grain !== void 0) {
37133
38504
  if (!pp.grain || typeof pp.grain !== "object") throw new Error(`${label}.grain must be an object`);
37134
38505
  out.grain = {};
37135
- if (pp.grain.intensity !== void 0) out.grain.intensity = requireFinite$2(pp.grain.intensity, `${label}.grain.intensity`);
38506
+ if (pp.grain.intensity !== void 0) out.grain.intensity = requireFinite$3(pp.grain.intensity, `${label}.grain.intensity`);
37136
38507
  }
37137
38508
  if (pp.toneMappingExposure !== void 0) {
37138
- out.toneMappingExposure = requireFinite$2(pp.toneMappingExposure, `${label}.toneMappingExposure`);
38509
+ out.toneMappingExposure = requireFinite$3(pp.toneMappingExposure, `${label}.toneMappingExposure`);
37139
38510
  }
37140
38511
  return out;
37141
38512
  }
@@ -37146,7 +38517,7 @@ function validateGround(ground, label) {
37146
38517
  out.visible = ground.visible;
37147
38518
  }
37148
38519
  if (ground.color !== void 0) out.color = requireColor(ground.color, `${label}.color`);
37149
- if (ground.offset !== void 0) out.offset = requireFinite$2(ground.offset, `${label}.offset`);
38520
+ if (ground.offset !== void 0) out.offset = requireFinite$3(ground.offset, `${label}.offset`);
37150
38521
  if (ground.receiveShadow !== void 0) {
37151
38522
  if (typeof ground.receiveShadow !== "boolean") throw new Error(`${label}.receiveShadow must be a boolean`);
37152
38523
  out.receiveShadow = ground.receiveShadow;
@@ -37156,31 +38527,31 @@ function validateGround(ground, label) {
37156
38527
  function validateCapture(cap, label) {
37157
38528
  const out = {};
37158
38529
  if (cap.framesPerTurn !== void 0) {
37159
- out.framesPerTurn = requireFinite$2(cap.framesPerTurn, `${label}.framesPerTurn`);
38530
+ out.framesPerTurn = requireFinite$3(cap.framesPerTurn, `${label}.framesPerTurn`);
37160
38531
  if (out.framesPerTurn < 12 || out.framesPerTurn > 720) {
37161
38532
  throw new Error(`${label}.framesPerTurn must be between 12 and 720`);
37162
38533
  }
37163
38534
  }
37164
38535
  if (cap.holdFrames !== void 0) {
37165
- out.holdFrames = requireFinite$2(cap.holdFrames, `${label}.holdFrames`);
38536
+ out.holdFrames = requireFinite$3(cap.holdFrames, `${label}.holdFrames`);
37166
38537
  if (out.holdFrames < 0 || out.holdFrames > 300) {
37167
38538
  throw new Error(`${label}.holdFrames must be between 0 and 300`);
37168
38539
  }
37169
38540
  }
37170
38541
  if (cap.pitchDeg !== void 0) {
37171
- out.pitchDeg = requireFinite$2(cap.pitchDeg, `${label}.pitchDeg`);
38542
+ out.pitchDeg = requireFinite$3(cap.pitchDeg, `${label}.pitchDeg`);
37172
38543
  if (out.pitchDeg < -80 || out.pitchDeg > 80) {
37173
38544
  throw new Error(`${label}.pitchDeg must be between -80 and 80`);
37174
38545
  }
37175
38546
  }
37176
38547
  if (cap.fps !== void 0) {
37177
- out.fps = requireFinite$2(cap.fps, `${label}.fps`);
38548
+ out.fps = requireFinite$3(cap.fps, `${label}.fps`);
37178
38549
  if (out.fps < 1 || out.fps > 60) {
37179
38550
  throw new Error(`${label}.fps must be between 1 and 60`);
37180
38551
  }
37181
38552
  }
37182
38553
  if (cap.size !== void 0) {
37183
- out.size = requireFinite$2(cap.size, `${label}.size`);
38554
+ out.size = requireFinite$3(cap.size, `${label}.size`);
37184
38555
  if (out.size < 1) {
37185
38556
  throw new Error(`${label}.size must be positive`);
37186
38557
  }
@@ -37881,6 +39252,16 @@ function buildProjectionReplayContext(plan) {
37881
39252
  ok: false,
37882
39253
  reason: "projection replay cannot derive a planar projection basis from fromSlices shapes."
37883
39254
  };
39255
+ case "nurbsSurface":
39256
+ return {
39257
+ ok: false,
39258
+ reason: "projection replay cannot derive a planar projection basis from NURBS surface shapes."
39259
+ };
39260
+ case "importedStep":
39261
+ return {
39262
+ ok: false,
39263
+ reason: "projection replay cannot derive a planar projection basis from imported STEP files."
39264
+ };
37884
39265
  default:
37885
39266
  assertExhaustive(plan);
37886
39267
  }
@@ -42249,6 +43630,138 @@ function hermiteTransitionG2(a, b) {
42249
43630
  { point: b.point, tangent: b.tangent, curvature: b.curvature, weight: b.weight }
42250
43631
  );
42251
43632
  }
43633
+ function requireFinite$2(v, label) {
43634
+ if (!Number.isFinite(v)) throw new Error(`nurbs3d: ${label} must be finite, got ${v}`);
43635
+ }
43636
+ class NurbsCurve3D {
43637
+ constructor(points, options = {}) {
43638
+ __publicField(this, "controlPoints");
43639
+ __publicField(this, "weights");
43640
+ __publicField(this, "knots");
43641
+ __publicField(this, "degree");
43642
+ __publicField(this, "closed");
43643
+ const n = points.length;
43644
+ const degree = options.degree ?? 3;
43645
+ if (degree < 1) throw new Error("nurbs3d: degree must be ≥ 1");
43646
+ if (n < degree + 1) throw new Error(`nurbs3d: need at least ${degree + 1} control points for degree ${degree}, got ${n}`);
43647
+ for (let i = 0; i < n; i++) {
43648
+ requireFinite$2(points[i][0], `controlPoints[${i}][0]`);
43649
+ requireFinite$2(points[i][1], `controlPoints[${i}][1]`);
43650
+ requireFinite$2(points[i][2], `controlPoints[${i}][2]`);
43651
+ }
43652
+ const weights = options.weights ?? new Array(n).fill(1);
43653
+ if (weights.length !== n) throw new Error(`nurbs3d: weights.length (${weights.length}) must equal controlPoints.length (${n})`);
43654
+ for (let i = 0; i < n; i++) {
43655
+ requireFinite$2(weights[i], `weights[${i}]`);
43656
+ if (weights[i] <= 0) throw new Error(`nurbs3d: weights[${i}] must be > 0, got ${weights[i]}`);
43657
+ }
43658
+ const expectedKnotLength = n + degree + 1;
43659
+ const knots = options.knots ?? generateClampedKnots(n, degree);
43660
+ if (knots.length !== expectedKnotLength) {
43661
+ throw new Error(`nurbs3d: knots.length (${knots.length}) must be controlPoints.length + degree + 1 (${expectedKnotLength})`);
43662
+ }
43663
+ for (let i = 0; i < knots.length; i++) {
43664
+ requireFinite$2(knots[i], `knots[${i}]`);
43665
+ if (i > 0 && knots[i] < knots[i - 1]) {
43666
+ throw new Error(`nurbs3d: knot vector must be non-decreasing, but knots[${i - 1}]=${knots[i - 1]} > knots[${i}]=${knots[i]}`);
43667
+ }
43668
+ }
43669
+ this.controlPoints = points.map(([x, y, z]) => [x, y, z]);
43670
+ this.weights = [...weights];
43671
+ this.knots = [...knots];
43672
+ this.degree = degree;
43673
+ this.closed = options.closed ?? false;
43674
+ }
43675
+ /**
43676
+ * Evaluate the curve at parameter t ∈ [0, 1].
43677
+ * Uses De Boor's algorithm — exact, O(degree²).
43678
+ */
43679
+ pointAt(t) {
43680
+ const u = remapToKnotDomain(t, this.controlPoints.length, this.degree, this.knots);
43681
+ return deBoor3D(this.controlPoints, this.weights, this.knots, this.degree, u);
43682
+ }
43683
+ /**
43684
+ * Evaluate the unit tangent vector at parameter t ∈ [0, 1].
43685
+ */
43686
+ tangentAt(t) {
43687
+ const u = remapToKnotDomain(t, this.controlPoints.length, this.degree, this.knots);
43688
+ const d = deBoor3DDeriv(this.controlPoints, this.weights, this.knots, this.degree, u);
43689
+ const len = Math.sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]);
43690
+ if (len < 1e-12) return [0, 0, 1];
43691
+ return [d[0] / len, d[1] / len, d[2] / len];
43692
+ }
43693
+ /**
43694
+ * Sample the curve uniformly at `count` points.
43695
+ */
43696
+ sample(count = 48) {
43697
+ return sampleNurbs3D(this.controlPoints, this.weights, this.knots, this.degree, Math.max(2, count));
43698
+ }
43699
+ /**
43700
+ * Sample with adaptive density — more points in high-curvature regions.
43701
+ */
43702
+ sampleAdaptive(minCount = 32, maxCount = 128) {
43703
+ const probeCount = Math.max(minCount, 64);
43704
+ const curvatures = [];
43705
+ for (let i = 0; i <= probeCount; i++) {
43706
+ curvatures.push(this.estimateCurvature(i / probeCount));
43707
+ }
43708
+ const cumulative = [0];
43709
+ for (let i = 1; i < curvatures.length; i++) {
43710
+ const avgCurv = (curvatures[i - 1] + curvatures[i]) / 2;
43711
+ cumulative.push(cumulative[i - 1] + 1 + avgCurv);
43712
+ }
43713
+ const total = cumulative[cumulative.length - 1];
43714
+ const targetCount = Math.min(maxCount, Math.max(minCount, Math.round(minCount * 1.5)));
43715
+ const pts = [this.pointAt(0)];
43716
+ let probeIdx = 0;
43717
+ for (let i = 1; i < targetCount; i++) {
43718
+ const target = i / targetCount * total;
43719
+ while (probeIdx < cumulative.length - 1 && cumulative[probeIdx + 1] < target) {
43720
+ probeIdx++;
43721
+ }
43722
+ const frac = (target - cumulative[probeIdx]) / (cumulative[probeIdx + 1] - cumulative[probeIdx]);
43723
+ const t = (probeIdx + frac) / probeCount;
43724
+ pts.push(this.pointAt(t));
43725
+ }
43726
+ pts.push(this.pointAt(1));
43727
+ return pts;
43728
+ }
43729
+ /**
43730
+ * Approximate arc length by summing polyline segment lengths.
43731
+ */
43732
+ length(samples = 100) {
43733
+ const pts = this.sample(Math.max(10, samples));
43734
+ let len = 0;
43735
+ for (let i = 1; i < pts.length; i++) {
43736
+ const dx = pts[i][0] - pts[i - 1][0];
43737
+ const dy = pts[i][1] - pts[i - 1][1];
43738
+ const dz = pts[i][2] - pts[i - 1][2];
43739
+ len += Math.sqrt(dx * dx + dy * dy + dz * dz);
43740
+ }
43741
+ return len;
43742
+ }
43743
+ /** Convert to a format compatible with sweep() path input. */
43744
+ toPolyline(samples = 64) {
43745
+ return this.sampleAdaptive(Math.max(16, samples), samples * 2);
43746
+ }
43747
+ estimateCurvature(t) {
43748
+ const eps = 1e-4;
43749
+ const t0 = Math.max(0, t - eps);
43750
+ const t1 = Math.min(1, t + eps);
43751
+ const tm = (t0 + t1) / 2;
43752
+ const p0 = this.pointAt(t0);
43753
+ const pm = this.pointAt(tm);
43754
+ const p1 = this.pointAt(t1);
43755
+ const dt = (t1 - t0) / 2;
43756
+ const d2x = (p0[0] - 2 * pm[0] + p1[0]) / (dt * dt);
43757
+ const d2y = (p0[1] - 2 * pm[1] + p1[1]) / (dt * dt);
43758
+ const d2z = (p0[2] - 2 * pm[2] + p1[2]) / (dt * dt);
43759
+ return Math.sqrt(d2x * d2x + d2y * d2y + d2z * d2z);
43760
+ }
43761
+ }
43762
+ function nurbs3d(points, options) {
43763
+ return new NurbsCurve3D(points, options);
43764
+ }
42252
43765
  function clamp$4(v, lo, hi) {
42253
43766
  return Math.max(lo, Math.min(hi, v));
42254
43767
  }
@@ -42590,6 +44103,19 @@ function buildPathPlan(path2) {
42590
44103
  sampleForBounds: (samples) => path2.sample(samples)
42591
44104
  };
42592
44105
  }
44106
+ if (path2 instanceof NurbsCurve3D) {
44107
+ return {
44108
+ plan: {
44109
+ kind: "nurbs",
44110
+ controlPoints: path2.controlPoints.map(([x, y, z]) => [x, y, z]),
44111
+ weights: [...path2.weights],
44112
+ knots: [...path2.knots],
44113
+ degree: path2.degree,
44114
+ closed: path2.closed
44115
+ },
44116
+ sampleForBounds: (samples) => path2.sample(samples)
44117
+ };
44118
+ }
42593
44119
  throw new Error("sweep: unsupported path type");
42594
44120
  }
42595
44121
  function sweep(profile, path2, options = {}) {
@@ -57948,6 +59474,16 @@ function sheetMetal(options) {
57948
59474
  deriveSheetMetalModel(model);
57949
59475
  return new SheetMetalPart(model);
57950
59476
  }
59477
+ function importStepFromBuffer(fileData, displayName = "import.step") {
59478
+ return buildShapeFromCompilePlan(
59479
+ createOwnedShapeCompilePlan(
59480
+ { kind: "importedStep", filePath: displayName, fileData },
59481
+ "importStep"
59482
+ ),
59483
+ void 0,
59484
+ { fidelity: "exact", sources: ["imported"] }
59485
+ );
59486
+ }
57951
59487
  let collectedSheetStock = [];
57952
59488
  let sheetStockCounter = 0;
57953
59489
  function resetSheetStock() {
@@ -270813,6 +272349,75 @@ function resolveErrorLocation(stack, compiledFiles) {
270813
272349
  column: parseInt(anonymousMatch[2], 10)
270814
272350
  };
270815
272351
  }
272352
+ function statementTargetVar(stmt) {
272353
+ if (typescriptExports.isVariableStatement(stmt)) {
272354
+ const decl = stmt.declarationList.declarations[0];
272355
+ if (decl && typescriptExports.isIdentifier(decl.name)) return decl.name.text;
272356
+ }
272357
+ if (typescriptExports.isExpressionStatement(stmt)) {
272358
+ const expr = stmt.expression;
272359
+ if (typescriptExports.isBinaryExpression(expr) && expr.operatorToken.kind === typescriptExports.SyntaxKind.EqualsToken && typescriptExports.isIdentifier(expr.left)) {
272360
+ return expr.left.text;
272361
+ }
272362
+ }
272363
+ return null;
272364
+ }
272365
+ function collectReferencedNames(node, exclude, names) {
272366
+ if (typescriptExports.isIdentifier(node) && !exclude.has(node)) {
272367
+ names.add(node.text);
272368
+ }
272369
+ typescriptExports.forEachChild(node, (child) => collectReferencedNames(child, exclude, names));
272370
+ }
272371
+ function extractUnusedTopLevelVarNames(code) {
272372
+ const sourceFile = typescriptExports.createSourceFile("__implicit.js", code, typescriptExports.ScriptTarget.ES2020, false, typescriptExports.ScriptKind.JS);
272373
+ const declaredNames = [];
272374
+ const collectBindingNames = (node) => {
272375
+ if (typescriptExports.isIdentifier(node)) {
272376
+ declaredNames.push(node.text);
272377
+ } else if (typescriptExports.isObjectBindingPattern(node) || typescriptExports.isArrayBindingPattern(node)) {
272378
+ for (const element of node.elements) {
272379
+ if (typescriptExports.isBindingElement(element)) {
272380
+ collectBindingNames(element.name);
272381
+ }
272382
+ }
272383
+ }
272384
+ };
272385
+ for (const statement of sourceFile.statements) {
272386
+ if (typescriptExports.isVariableStatement(statement)) {
272387
+ for (const decl of statement.declarationList.declarations) {
272388
+ collectBindingNames(decl.name);
272389
+ }
272390
+ } else if (typescriptExports.isFunctionDeclaration(statement) && statement.name) {
272391
+ declaredNames.push(statement.name.text);
272392
+ }
272393
+ }
272394
+ const excluded = /* @__PURE__ */ new Set(["exports", "module", "require", "__filename", "__dirname"]);
272395
+ const topLevelNames = new Set(declaredNames.filter((n) => !excluded.has(n)));
272396
+ if (topLevelNames.size === 0) return [];
272397
+ const usedByOthers = /* @__PURE__ */ new Set();
272398
+ for (const statement of sourceFile.statements) {
272399
+ const target = statementTargetVar(statement);
272400
+ const refs = /* @__PURE__ */ new Set();
272401
+ const lhsNodes = /* @__PURE__ */ new Set();
272402
+ if (typescriptExports.isVariableStatement(statement)) {
272403
+ for (const decl of statement.declarationList.declarations) {
272404
+ if (typescriptExports.isIdentifier(decl.name)) lhsNodes.add(decl.name);
272405
+ }
272406
+ } else if (typescriptExports.isExpressionStatement(statement)) {
272407
+ const expr = statement.expression;
272408
+ if (typescriptExports.isBinaryExpression(expr) && typescriptExports.isIdentifier(expr.left)) {
272409
+ lhsNodes.add(expr.left);
272410
+ }
272411
+ }
272412
+ collectReferencedNames(statement, lhsNodes, refs);
272413
+ for (const ref of refs) {
272414
+ if (topLevelNames.has(ref) && ref !== target) {
272415
+ usedByOthers.add(ref);
272416
+ }
272417
+ }
272418
+ }
272419
+ return declaredNames.filter((n) => topLevelNames.has(n) && !usedByOthers.has(n));
272420
+ }
270816
272421
  function createForgeRuntimeModule(bindings) {
270817
272422
  const runtime = { ...bindings };
270818
272423
  Object.defineProperty(runtime, "__esModule", { value: true });
@@ -270867,6 +272472,34 @@ function finalizeForgeJsImport(moduleExports, importedDims) {
270867
272472
  if (importedDims.length === 0) return base;
270868
272473
  return setShapeDimensions(base, [...getShapeDimensions(base), ...importedDims]);
270869
272474
  }
272475
+ function rejectPathTraversal(fnName, userPath, resolvedPath) {
272476
+ if (resolvedPath.startsWith("..")) {
272477
+ throw new Error(`${fnName}("${userPath}"): path traversal blocked — resolved path escapes the project directory`);
272478
+ }
272479
+ }
272480
+ let _constructorLockdownDepth = 0;
272481
+ let _origConstructorDescriptor;
272482
+ function withConstructorChainLockdown(fn) {
272483
+ _constructorLockdownDepth++;
272484
+ if (_constructorLockdownDepth === 1) {
272485
+ _origConstructorDescriptor = Object.getOwnPropertyDescriptor(Function.prototype, "constructor");
272486
+ Object.defineProperty(Function.prototype, "constructor", {
272487
+ get() {
272488
+ throw new Error("Dynamic code generation is not allowed in ForgeCAD scripts");
272489
+ },
272490
+ configurable: true
272491
+ });
272492
+ }
272493
+ try {
272494
+ return fn();
272495
+ } finally {
272496
+ _constructorLockdownDepth--;
272497
+ if (_constructorLockdownDepth === 0 && _origConstructorDescriptor) {
272498
+ Object.defineProperty(Function.prototype, "constructor", _origConstructorDescriptor);
272499
+ _origConstructorDescriptor = void 0;
272500
+ }
272501
+ }
272502
+ }
270870
272503
  function executeFile(code, fileName, allFiles, visited, scope = {}, options, executionMode = "script", moduleCacheEntry) {
270871
272504
  const trackCircularImports = executionMode === "script";
270872
272505
  if (trackCircularImports) {
@@ -270914,6 +272547,7 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
270914
272547
  throw new Error("importMesh() requires a non-empty file path string");
270915
272548
  }
270916
272549
  const resolvedPath = resolveImportPath(fileName, name.trim());
272550
+ rejectPathTraversal("importMesh", name, resolvedPath);
270917
272551
  const format = detectMeshFormat(resolvedPath);
270918
272552
  if (!format) {
270919
272553
  const ext = resolvedPath.split(".").pop() ?? "";
@@ -270943,6 +272577,25 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
270943
272577
  sources: ["imported"]
270944
272578
  });
270945
272579
  };
272580
+ const importStep = (name) => {
272581
+ var _a3;
272582
+ if (typeof name !== "string" || name.trim().length === 0) {
272583
+ throw new Error("importStep() requires a non-empty file path string");
272584
+ }
272585
+ const resolvedPath = resolveImportPath(fileName, name.trim());
272586
+ rejectPathTraversal("importStep", name, resolvedPath);
272587
+ const ext = ((_a3 = resolvedPath.split(".").pop()) == null ? void 0 : _a3.toLowerCase()) ?? "";
272588
+ if (ext !== "step" && ext !== "stp") {
272589
+ throw new Error(`importStep("${name}"): unsupported extension ".${ext}". Expected .step or .stp`);
272590
+ }
272591
+ if (!options.readBinaryFile) {
272592
+ throw new Error(
272593
+ `importStep("${name}"): binary file reading is not available in this environment. Provide a readBinaryFile callback in RunScriptOptions.`
272594
+ );
272595
+ }
272596
+ const fileData = options.readBinaryFile(resolvedPath);
272597
+ return importStepFromBuffer(fileData, name);
272598
+ };
270946
272599
  const wrappedUnion = union;
270947
272600
  const wrappedDifference = difference;
270948
272601
  const wrappedIntersection = intersection;
@@ -271006,6 +272659,10 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
271006
272659
  filletCorners,
271007
272660
  chamfer2d,
271008
272661
  Curve3D,
272662
+ NurbsCurve3D,
272663
+ nurbs3d,
272664
+ NurbsSurface,
272665
+ nurbsSurface,
271009
272666
  spline2d,
271010
272667
  spline3d,
271011
272668
  loft,
@@ -271019,10 +272676,13 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
271019
272676
  surfacePatch,
271020
272677
  sheetMetal,
271021
272678
  SheetMetalPart,
272679
+ Param,
271022
272680
  param,
271023
272681
  boolParam,
271024
272682
  choiceParam,
271025
- listParam,
272683
+ listParam: () => {
272684
+ throw new Error("listParam() has been renamed to Param.list(). Update your script.");
272685
+ },
271026
272686
  sdf,
271027
272687
  Shape,
271028
272688
  Sketch,
@@ -271052,6 +272712,7 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
271052
272712
  offsetSolid,
271053
272713
  importSvgSketch,
271054
272714
  importMesh,
272715
+ importStep,
271055
272716
  text2d,
271056
272717
  textWidth,
271057
272718
  loadFont,
@@ -271068,12 +272729,14 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
271068
272729
  ShapeGroup,
271069
272730
  console: sandboxConsole,
271070
272731
  cutPlane,
272732
+ cameraTrajectory,
271071
272733
  explodeView,
271072
272734
  jointsView,
271073
272735
  viewConfig,
271074
272736
  scene,
271075
272737
  verify,
271076
272738
  spec,
272739
+ mock,
271077
272740
  gcode,
271078
272741
  GCodeBuilder,
271079
272742
  // ── Laser Kit ──────────────────────────────────────────────────
@@ -271086,7 +272749,19 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
271086
272749
  assemblyInstructions,
271087
272750
  formatInstructions,
271088
272751
  lookupKerf,
271089
- COMMON_KERFS
272752
+ COMMON_KERFS,
272753
+ // ── Sandbox safety: shadow dangerous globals ───────────────────
272754
+ // These prevent user code from accessing escape vectors directly.
272755
+ // The constructor-chain lockdown (below) covers indirect access.
272756
+ Function: void 0,
272757
+ globalThis: void 0,
272758
+ global: void 0,
272759
+ self: void 0,
272760
+ window: void 0,
272761
+ setTimeout: void 0,
272762
+ setInterval: void 0,
272763
+ setImmediate: void 0,
272764
+ queueMicrotask: void 0
271090
272765
  };
271091
272766
  const requireModule = (requestedName, paramOverrides) => {
271092
272767
  if (typeof requestedName !== "string" || requestedName.trim().length === 0) {
@@ -271200,6 +272875,15 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
271200
272875
  const compiled = compileScript(code, fileName, options);
271201
272876
  const bindingNames = Object.keys(runtimeBindings);
271202
272877
  const bindingValues = bindingNames.map((name) => runtimeBindings[name]);
272878
+ let scriptCode = compiled.code;
272879
+ if (executionMode === "script") {
272880
+ const varNames = extractUnusedTopLevelVarNames(compiled.code).filter((n) => !bindingNames.includes(n));
272881
+ if (varNames.length > 0) {
272882
+ const collector = varNames.map((n) => `${JSON.stringify(n)}: ${n}`).join(", ");
272883
+ scriptCode += `
272884
+ ; try { module.__implicitVars = {${collector}}; } catch(e) {}`;
272885
+ }
272886
+ }
271203
272887
  const fn = new Function(
271204
272888
  "exports",
271205
272889
  "module",
@@ -271207,16 +272891,19 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
271207
272891
  "__filename",
271208
272892
  "__dirname",
271209
272893
  ...bindingNames,
271210
- `${compiled.code}
272894
+ `"use strict";
272895
+ ${scriptCode}
271211
272896
  //# sourceURL=${fileName}`
271212
272897
  );
271213
272898
  const moduleValue = {
271214
272899
  exports: executionMode === "module" && moduleCacheEntry ? moduleCacheEntry.exports : {}
271215
272900
  };
271216
272901
  const initialExportsRef = moduleValue.exports;
271217
- const returnValue = runWithParamScope(
271218
- scope,
271219
- () => fn(moduleValue.exports, moduleValue, requireModule, fileName, dirnamePath(fileName), ...bindingValues)
272902
+ const returnValue = withConstructorChainLockdown(
272903
+ () => runWithParamScope(
272904
+ scope,
272905
+ () => fn(moduleValue.exports, moduleValue, requireModule, fileName, dirnamePath(fileName), ...bindingValues)
272906
+ )
271220
272907
  );
271221
272908
  if (executionMode === "module") {
271222
272909
  const hasExports = hasExplicitModuleExports(moduleValue.exports, initialExportsRef);
@@ -271241,7 +272928,18 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
271241
272928
  }
271242
272929
  const exportedResult = resolveExportedEntryResult(moduleValue.exports);
271243
272930
  if (returnValue === void 0) {
271244
- return exportedResult ?? null;
272931
+ if (exportedResult != null) return exportedResult;
272932
+ const implicitVars = moduleValue.__implicitVars;
272933
+ if (implicitVars) {
272934
+ const renderables = {};
272935
+ for (const [key, value] of Object.entries(implicitVars)) {
272936
+ if (isRenderableEntryResult(value)) {
272937
+ renderables[key] = value;
272938
+ }
272939
+ }
272940
+ if (Object.keys(renderables).length > 0) return renderables;
272941
+ }
272942
+ return null;
271245
272943
  }
271246
272944
  return returnValue;
271247
272945
  } finally {
@@ -271278,12 +272976,15 @@ function runScript(code, fileName = "main.forge.js", allFiles = {}, options = {}
271278
272976
  resetSheetStock();
271279
272977
  resetRobotExport();
271280
272978
  resetCutPlanes();
272979
+ resetCameraTrajectory();
271281
272980
  resetExplodeView();
271282
272981
  resetJointsView();
271283
272982
  resetViewConfig();
271284
272983
  resetScene();
271285
272984
  resetVerifications();
272985
+ resetMocks();
271286
272986
  _collectedLogs = [];
272987
+ setRuntimeWarnSink((msg) => _collectedLogs.push({ level: "warn", args: [msg], timestamp: Date.now() }));
271287
272988
  const t0 = performance.now();
271288
272989
  const execOptions = {
271289
272990
  debugImports: options.debugImports ?? envFlagEnabled("FORGECAD_DEBUG_IMPORTS"),
@@ -271523,7 +273224,19 @@ function runScript(code, fileName = "main.forge.js", allFiles = {}, options = {}
271523
273224
  } else {
271524
273225
  const entries = Object.entries(obj);
271525
273226
  entries.forEach(([key, value]) => {
271526
- if (value instanceof Shape) {
273227
+ if (value instanceof Assembly) {
273228
+ const items = value.solve().toSceneObjects();
273229
+ items.forEach((item, index) => {
273230
+ const label = `${key}.${index + 1}`;
273231
+ processNamedItem(item, label, label);
273232
+ });
273233
+ } else if (value instanceof SolvedAssembly) {
273234
+ const items = value.toSceneObjects();
273235
+ items.forEach((item, index) => {
273236
+ const label = `${key}.${index + 1}`;
273237
+ processNamedItem(item, label, label);
273238
+ });
273239
+ } else if (value instanceof Shape) {
271527
273240
  pushShape(value, key, void 0, void 0, void 0, [key]);
271528
273241
  } else if (value instanceof Sketch) {
271529
273242
  pushSketch(value, key, void 0, [key]);
@@ -271531,6 +273244,21 @@ function runScript(code, fileName = "main.forge.js", allFiles = {}, options = {}
271531
273244
  value.children.forEach((child, i) => {
271532
273245
  flattenGroupChild(child, groupChildLabel(value, key, i), void 0, [key, shapeGroupChildSegment(value, i)]);
271533
273246
  });
273247
+ } else if (Array.isArray(value)) {
273248
+ value.forEach((item, index) => {
273249
+ const label = `${key}.${index + 1}`;
273250
+ if (item instanceof ShapeGroup) {
273251
+ item.children.forEach((child, i) => {
273252
+ flattenGroupChild(child, groupChildLabel(item, label, i), void 0, [key, label, shapeGroupChildSegment(item, i)]);
273253
+ });
273254
+ } else if (item instanceof Shape) {
273255
+ pushShape(item, label, void 0, void 0, void 0, [key, label]);
273256
+ } else if (item instanceof Sketch) {
273257
+ pushSketch(item, label, void 0, [key, label]);
273258
+ } else if (isNamedObject(item)) {
273259
+ processNamedItem(item, label, label, void 0, [key]);
273260
+ }
273261
+ });
271534
273262
  }
271535
273263
  });
271536
273264
  }
@@ -271559,12 +273287,23 @@ function runScript(code, fileName = "main.forge.js", allFiles = {}, options = {}
271559
273287
  }
271560
273288
  const shape = objects.length === 1 ? objects[0].shape : null;
271561
273289
  const sketch = objects.length === 1 ? objects[0].sketch : null;
273290
+ const collectedMocks = getCollectedMocks();
273291
+ for (const m of collectedMocks) {
273292
+ objects.push({
273293
+ id: m.id,
273294
+ name: `${m.name} (mock)`,
273295
+ shape: m.shape,
273296
+ sketch: null,
273297
+ mock: true
273298
+ });
273299
+ }
271562
273300
  autoFillExplodeHints(objects);
271563
273301
  return {
271564
273302
  shape,
271565
273303
  sketch,
271566
273304
  objects,
271567
273305
  params: getCollectedParams(),
273306
+ stringParams: getCollectedStringParams(),
271568
273307
  listParams: getCollectedListParams(),
271569
273308
  dimensions: [...getCollectedDimensions(), ...shapeDimensions],
271570
273309
  highlights: getCollectedHighlights(),
@@ -271572,6 +273311,7 @@ function runScript(code, fileName = "main.forge.js", allFiles = {}, options = {}
271572
273311
  bom: getCollectedBom(),
271573
273312
  sheetStock: getCollectedSheetStock(),
271574
273313
  cutPlanes: getCollectedCutPlanes(),
273314
+ cameraTrajectory: getCollectedCameraTrajectory(),
271575
273315
  explodeView: getCollectedExplodeView(),
271576
273316
  jointsView: getCollectedJointsView(),
271577
273317
  viewConfig: getCollectedViewConfig(),
@@ -271581,7 +273321,8 @@ function runScript(code, fileName = "main.forge.js", allFiles = {}, options = {}
271581
273321
  error: objects.length > 0 || options.allowEmptyResult ? null : "Script must return a Shape or Sketch",
271582
273322
  timeMs: performance.now() - t0,
271583
273323
  logs: _collectedLogs.slice(),
271584
- verifications: getCollectedVerifications()
273324
+ verifications: getCollectedVerifications(),
273325
+ mocks: getCollectedMocks()
271585
273326
  };
271586
273327
  });
271587
273328
  } catch (e) {
@@ -271598,6 +273339,7 @@ function runScript(code, fileName = "main.forge.js", allFiles = {}, options = {}
271598
273339
  sketch: null,
271599
273340
  objects: [],
271600
273341
  params: getCollectedParams(),
273342
+ stringParams: getCollectedStringParams(),
271601
273343
  listParams: getCollectedListParams(),
271602
273344
  dimensions: getCollectedDimensions(),
271603
273345
  highlights: getCollectedHighlights(),
@@ -271605,6 +273347,7 @@ function runScript(code, fileName = "main.forge.js", allFiles = {}, options = {}
271605
273347
  bom: getCollectedBom(),
271606
273348
  sheetStock: getCollectedSheetStock(),
271607
273349
  cutPlanes: getCollectedCutPlanes(),
273350
+ cameraTrajectory: getCollectedCameraTrajectory(),
271608
273351
  explodeView: getCollectedExplodeView(),
271609
273352
  jointsView: getCollectedJointsView(),
271610
273353
  viewConfig: getCollectedViewConfig(),
@@ -271614,7 +273357,8 @@ function runScript(code, fileName = "main.forge.js", allFiles = {}, options = {}
271614
273357
  error: `${msg}${lineInfo}`,
271615
273358
  timeMs: performance.now() - t0,
271616
273359
  logs: _collectedLogs.slice(),
271617
- verifications: getCollectedVerifications()
273360
+ verifications: getCollectedVerifications(),
273361
+ mocks: getCollectedMocks()
271618
273362
  };
271619
273363
  }
271620
273364
  }