forgecad 0.10.2 → 0.10.3

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 (95) hide show
  1. package/README.md +7 -6
  2. package/dist/assets/{AdminPage-CHY6ZN-p.js → AdminPage-CK7ObBz3.js} +1 -1
  3. package/dist/assets/{BenchmarkPage-BcRT5iGN.js → BenchmarkPage-Ds7Z2doN.js} +1 -1
  4. package/dist/assets/{BlogPage-BssBbnb-.js → BlogPage-DlPbpt6A.js} +1 -1
  5. package/dist/assets/{DocsPage-DsvdiRNK.js → DocsPage-vZb3b3Y0.js} +9 -14
  6. package/dist/assets/{EditorApp-BpjZgzk0.css → EditorApp-C5f24ZN9.css} +8 -0
  7. package/dist/assets/{EditorApp-Bfd3jbtC.js → EditorApp-HLoKfe15.js} +141 -12
  8. package/dist/assets/{EmbedViewer-D5t8WamV.js → EmbedViewer--KnqBKrJ.js} +2 -2
  9. package/dist/assets/{LandingPageProofDriven-DbN7o-Be.js → LandingPageProofDriven-C_LssmnA.js} +1 -1
  10. package/dist/assets/{LegalPage-DNGrrY0p.js → LegalPage-DGsyo4n1.js} +1 -1
  11. package/dist/assets/{PricingPage-Nczr3pRz.js → PricingPage-BOE27B-R.js} +1 -1
  12. package/dist/assets/{SettingsPage-DZlyu4d4.js → SettingsPage-f47cnk39.js} +1 -1
  13. package/dist/assets/{app-C9ct2hRD.js → app-D6ccu2Xx.js} +6854 -7373
  14. package/dist/assets/{backendInit-ymjonyQp.js → backendInit-DbTkQN9J.js} +2557 -809
  15. package/dist/assets/cli/{render-B_0lQwKU.js → render-BsngirjC.js} +114 -9
  16. package/dist/assets/{constructionHistoryWorker-CZ42Dksy.js → constructionHistoryWorker-PCwXrTDB.js} +175 -36
  17. package/dist/assets/{evalWorker-C2pm8LHP.js → evalWorker-CS63PfZu.js} +1125 -447
  18. package/dist/assets/{forgecad_geometry-BlMtqluF.js → forgecad_geometry-CZ_IfuvA.js} +1 -9
  19. package/dist/assets/{forgecad_geometry_bg-BllP_WiL.wasm → forgecad_geometry_bg-C3rQHfwg.wasm} +0 -0
  20. package/dist/assets/{inspectWorker-D5T5VbfK.js → inspectWorker-Y4cOzNyA.js} +4345 -373
  21. package/dist/assets/{jointPose-4r8ed8_5.js → jointPose-AMvCywzS.js} +1 -1
  22. package/dist/assets/{manifold-C4r6B-XY.js → manifold-CBry38ly.js} +2 -2
  23. package/dist/assets/{manifold-5PP1eGLN.js → manifold-Crd_F2qx.js} +1 -1
  24. package/dist/assets/{manifold-DjBkyIc8.js → manifold-k2kRcc85.js} +1 -1
  25. package/dist/assets/{reportWorker-CwenM7wB.js → reportWorker-CWvn0CEv.js} +1095 -400
  26. package/dist/cli/render.html +1 -1
  27. package/dist/docs/index.html +2 -2
  28. package/dist/docs-raw/AI/usage.md +2 -4
  29. package/dist/docs-raw/CLI.md +9 -7
  30. package/dist/docs-raw/README.md +1 -1
  31. package/dist/docs-raw/component-model.md +1 -1
  32. package/dist/docs-raw/generated/assembly.md +1 -1
  33. package/dist/docs-raw/generated/concepts.md +5 -3
  34. package/dist/docs-raw/generated/core.md +70 -1
  35. package/dist/docs-raw/generated/curves.md +8 -1
  36. package/dist/docs-raw/generated/output.md +0 -64
  37. package/dist/docs-raw/generated/runtime-names.md +6 -6
  38. package/dist/docs-raw/generated/viewport.md +3 -12
  39. package/dist/docs-raw/guides/inspection-bundles.md +1 -1
  40. package/dist/docs-raw/simulation-workflow.md +58 -0
  41. package/dist/docs-raw/skills/forgecad-blockout-model.md +1 -1
  42. package/dist/docs-raw/skills/forgecad-image-replicator.md +2 -2
  43. package/dist/docs-raw/skills/forgecad-mujoco-verify.md +78 -0
  44. package/dist/docs-raw/skills/forgecad-spec-by-walking-through-it.md +145 -0
  45. package/dist/docs-raw/skills/forgecad-visual-spec.md +1 -1
  46. package/dist/docs-raw/skills/forgecad.md +24 -24
  47. package/dist/docs-raw/skills/index.md +2 -3
  48. package/dist/index.html +1 -1
  49. package/dist/sitemap.xml +15 -15
  50. package/dist-cli/{check-compiler-SP7FAL7R.js → check-compiler-HPF2T2FS.js} +1 -1
  51. package/dist-cli/{check-query-propagation-BRLSHP22.js → check-query-propagation-HYSLTXAB.js} +1 -1
  52. package/dist-cli/{chunk-RQQ42YCP.js → chunk-WLUKAW3H.js} +1025 -158
  53. package/dist-cli/forgecad.js +2621 -232
  54. package/dist-cli/{forgecad_geometry-7TVSNVUB.js → forgecad_geometry-2IMYCUWW.js} +0 -8
  55. package/dist-cli/forgecad_geometry_bg.wasm +0 -0
  56. package/dist-skill/CONTEXT.md +85 -73
  57. package/dist-skill/SKILL.md +1 -1
  58. package/dist-skill/docs/CLI.md +9 -7
  59. package/dist-skill/docs/generated/assembly.md +1 -1
  60. package/dist-skill/docs/generated/core.md +70 -1
  61. package/dist-skill/docs/generated/curves.md +8 -1
  62. package/dist-skill/docs/generated/output.md +0 -64
  63. package/dist-skill/docs/generated/runtime-names.md +6 -6
  64. package/dist-skill/docs/generated/viewport.md +3 -12
  65. package/dist-skill/docs/guides/inspection-bundles.md +1 -1
  66. package/dist-skill/library/README.md +2 -3
  67. package/dist-skill/library/forgecad-blockout-model/SKILL.md +1 -1
  68. package/dist-skill/library/forgecad-image-replicator/SKILL.md +2 -2
  69. package/dist-skill/library/forgecad-mujoco-verify/SKILL.md +66 -0
  70. package/dist-skill/library/forgecad-mujoco-verify/scripts/mujoco_verify.py +385 -0
  71. package/dist-skill/library/forgecad-spec-by-walking-through-it/SKILL.md +132 -0
  72. package/dist-skill/library/forgecad-visual-spec/SKILL.md +1 -1
  73. package/dist-skill/website/skills/forgecad-blockout-model.md +1 -1
  74. package/dist-skill/website/skills/forgecad-image-replicator.md +2 -2
  75. package/dist-skill/website/skills/forgecad-mujoco-verify.md +78 -0
  76. package/dist-skill/website/skills/forgecad-spec-by-walking-through-it.md +145 -0
  77. package/dist-skill/website/skills/forgecad-visual-spec.md +1 -1
  78. package/dist-skill/website/skills/forgecad.md +24 -24
  79. package/dist-skill/website/skills/index.md +2 -3
  80. package/examples/analysis/clearance-fit.forge.js +31 -0
  81. package/examples/analysis/lever-arm-actuator.forge.js +43 -0
  82. package/examples/analysis/tipping-tripod.forge.js +35 -0
  83. package/examples/products/sportscar.forge.js +77 -0
  84. package/package.json +1 -3
  85. package/dist/docs-raw/skills/forgecad-high-level-spec.md +0 -101
  86. package/dist/docs-raw/skills/forgecad-lld.md +0 -41
  87. package/dist/docs-raw/skills/forgecad-prepare-prompt.md +0 -63
  88. package/dist-skill/library/forgecad-high-level-spec/SKILL.md +0 -94
  89. package/dist-skill/library/forgecad-lld/SKILL.md +0 -34
  90. package/dist-skill/library/forgecad-prepare-prompt/SKILL.md +0 -50
  91. package/dist-skill/website/skills/forgecad-high-level-spec.md +0 -101
  92. package/dist-skill/website/skills/forgecad-lld.md +0 -41
  93. package/dist-skill/website/skills/forgecad-prepare-prompt.md +0 -63
  94. /package/dist-skill/library/{forgecad-prepare-prompt → forgecad-spec-by-walking-through-it}/references/default-profiles.md +0 -0
  95. /package/dist-skill/library/{forgecad-prepare-prompt → forgecad-spec-by-walking-through-it}/references/master-prompt.md +0 -0
@@ -3190,20 +3190,20 @@ function flatKnotsToKnotsMults(flatKnots) {
3190
3190
  return { knots, mults };
3191
3191
  }
3192
3192
  const KNOT_EPSILON = 1e-10;
3193
- function sameKnot(a2, b) {
3193
+ function sameKnot$1(a2, b) {
3194
3194
  return Math.abs(a2 - b) <= KNOT_EPSILON;
3195
3195
  }
3196
- function knotMultiplicity(knots, u2) {
3197
- return knots.reduce((count, knot) => count + (sameKnot(knot, u2) ? 1 : 0), 0);
3196
+ function knotMultiplicity$1(knots, u2) {
3197
+ return knots.reduce((count, knot) => count + (sameKnot$1(knot, u2) ? 1 : 0), 0);
3198
3198
  }
3199
3199
  function firstKnotIndex(knots, u2) {
3200
- const index2 = knots.findIndex((knot) => sameKnot(knot, u2));
3200
+ const index2 = knots.findIndex((knot) => sameKnot$1(knot, u2));
3201
3201
  if (index2 < 0) throw new Error(`NURBS subdomain extraction could not find boundary knot ${u2}.`);
3202
3202
  return index2;
3203
3203
  }
3204
3204
  function lastKnotIndex(knots, u2) {
3205
3205
  for (let index2 = knots.length - 1; index2 >= 0; index2 -= 1) {
3206
- if (sameKnot(knots[index2], u2)) return index2;
3206
+ if (sameKnot$1(knots[index2], u2)) return index2;
3207
3207
  }
3208
3208
  throw new Error(`NURBS subdomain extraction could not find boundary knot ${u2}.`);
3209
3209
  }
@@ -3216,7 +3216,7 @@ function insertKnotOnceHomogeneous(points, knots, degree, u2) {
3216
3216
  const lastPoint = count - 1;
3217
3217
  const lastKnot = knots.length - 1;
3218
3218
  const span = findSpan(count, degree, u2, knots);
3219
- const multiplicity = knotMultiplicity(knots, u2);
3219
+ const multiplicity = knotMultiplicity$1(knots, u2);
3220
3220
  if (multiplicity >= degree) {
3221
3221
  throw new Error(`NURBS subdomain extraction cannot insert knot ${u2}: multiplicity ${multiplicity} already reaches degree ${degree}.`);
3222
3222
  }
@@ -3240,7 +3240,7 @@ function insertKnotOnceHomogeneous(points, knots, degree, u2) {
3240
3240
  function insertBoundaryToMultiplicity(points, knots, degree, u2, targetMultiplicity) {
3241
3241
  let refinedPoints = points;
3242
3242
  let refinedKnots = knots;
3243
- while (knotMultiplicity(refinedKnots, u2) < targetMultiplicity) {
3243
+ while (knotMultiplicity$1(refinedKnots, u2) < targetMultiplicity) {
3244
3244
  const refined = insertKnotOnceHomogeneous(refinedPoints, refinedKnots, degree, u2);
3245
3245
  refinedPoints = refined.points;
3246
3246
  refinedKnots = refined.knots;
@@ -3260,8 +3260,8 @@ function extractNurbsCurveSubdomain(controlPoints, weights, knots, degree, uStar
3260
3260
  }
3261
3261
  const activeStart = knots[degree];
3262
3262
  const activeEnd = knots[controlPoints.length];
3263
- const start = sameKnot(uStart, activeStart) ? activeStart : sameKnot(uStart, activeEnd) ? activeEnd : uStart;
3264
- const end = sameKnot(uEnd, activeEnd) ? activeEnd : sameKnot(uEnd, activeStart) ? activeStart : uEnd;
3263
+ const start = sameKnot$1(uStart, activeStart) ? activeStart : sameKnot$1(uStart, activeEnd) ? activeEnd : uStart;
3264
+ const end = sameKnot$1(uEnd, activeEnd) ? activeEnd : sameKnot$1(uEnd, activeStart) ? activeStart : uEnd;
3265
3265
  if (start < activeStart - KNOT_EPSILON || end > activeEnd + KNOT_EPSILON) {
3266
3266
  throw new Error(`NURBS subdomain [${uStart}, ${uEnd}] must stay inside active knot domain [${activeStart}, ${activeEnd}].`);
3267
3267
  }
@@ -10172,7 +10172,7 @@ async function initTruckGeometryWasm() {
10172
10172
  if (_initPromise$2) return _initPromise$2;
10173
10173
  _initPromise$2 = (async () => {
10174
10174
  try {
10175
- const geometryModule = await import("./forgecad_geometry-BlMtqluF.js");
10175
+ const geometryModule = await import("./forgecad_geometry-CZ_IfuvA.js");
10176
10176
  const isNode = isNodeRuntime();
10177
10177
  if (isNode) {
10178
10178
  const { readFileSync, existsSync } = await Promise.resolve().then(function() {
@@ -11100,23 +11100,62 @@ class NurbsSurface {
11100
11100
  return [wx / wSum, wy / wSum, wz / wSum];
11101
11101
  }
11102
11102
  /**
11103
- * Evaluate the surface normal at (u, v) via cross product of partial derivatives.
11103
+ * Evaluate the surface unit normal at (u, v) from analytic first derivatives.
11104
+ *
11105
+ * Uses Algorithm A2.3 basis-function derivatives with the rational quotient
11106
+ * rule, so the normal is exact (no finite-difference epsilon, no error near
11107
+ * the boundary). Constant chain-rule factors from the parameter remap scale Su
11108
+ * and Sv positively and cancel under normalization, so they are omitted.
11104
11109
  */
11105
11110
  normalAt(u2, v) {
11106
- const eps = 1e-5;
11107
- const u0 = Math.max(0, u2 - eps), u1 = Math.min(1, u2 + eps);
11108
- const v0 = Math.max(0, v - eps), v1 = Math.min(1, v + eps);
11109
- const pu = this.pointAt(u1, v), pmu = this.pointAt(u0, v);
11110
- const pv = this.pointAt(u2, v1), pmv = this.pointAt(u2, v0);
11111
- const du = [pu[0] - pmu[0], pu[1] - pmu[1], pu[2] - pmu[2]];
11112
- const dv = [pv[0] - pmv[0], pv[1] - pmv[1], pv[2] - pmv[2]];
11113
- const nx = du[1] * dv[2] - du[2] * dv[1];
11114
- const ny = du[2] * dv[0] - du[0] * dv[2];
11115
- const nz = du[0] * dv[1] - du[1] * dv[0];
11111
+ const { Su, Sv } = this.derivativesAt(u2, v);
11112
+ const nx = Su[1] * Sv[2] - Su[2] * Sv[1];
11113
+ const ny = Su[2] * Sv[0] - Su[0] * Sv[2];
11114
+ const nz = Su[0] * Sv[1] - Su[1] * Sv[0];
11116
11115
  const len2 = Math.sqrt(nx * nx + ny * ny + nz * nz);
11117
11116
  if (len2 < 1e-12) return [0, 0, 1];
11118
11117
  return [nx / len2, ny / len2, nz / len2];
11119
11118
  }
11119
+ /** Analytic first partial derivatives S_u, S_v (rational quotient rule). */
11120
+ derivativesAt(u2, v) {
11121
+ const uu = this.remapU(Math.max(0, Math.min(1, u2)));
11122
+ const vv = this.remapV(Math.max(0, Math.min(1, v)));
11123
+ const spanU = findSpan(this.nU, this.degreeU, uu, this.knotsU);
11124
+ const spanV = findSpan(this.nV, this.degreeV, vv, this.knotsV);
11125
+ const dU = basisFunsDeriv(spanU, uu, this.degreeU, this.knotsU, 1);
11126
+ const dV = basisFunsDeriv(spanV, vv, this.degreeV, this.knotsV, 1);
11127
+ const A = [0, 0, 0];
11128
+ const Au = [0, 0, 0];
11129
+ const Av = [0, 0, 0];
11130
+ let w2 = 0;
11131
+ let wu = 0;
11132
+ let wv = 0;
11133
+ for (let i = 0; i <= this.degreeU; i++) {
11134
+ const rowIdx = spanU - this.degreeU + i;
11135
+ for (let j = 0; j <= this.degreeV; j++) {
11136
+ const colIdx = spanV - this.degreeV + j;
11137
+ const weight = this.weightsGrid[rowIdx][colIdx];
11138
+ const pt = this.controlGrid[rowIdx][colIdx];
11139
+ const n00 = dU[0][i] * dV[0][j] * weight;
11140
+ const n10 = dU[1][i] * dV[0][j] * weight;
11141
+ const n01 = dU[0][i] * dV[1][j] * weight;
11142
+ w2 += n00;
11143
+ wu += n10;
11144
+ wv += n01;
11145
+ for (let c2 = 0; c2 < 3; c2++) {
11146
+ A[c2] += n00 * pt[c2];
11147
+ Au[c2] += n10 * pt[c2];
11148
+ Av[c2] += n01 * pt[c2];
11149
+ }
11150
+ }
11151
+ }
11152
+ if (w2 === 0) return { S: [0, 0, 0], Su: [0, 0, 0], Sv: [0, 0, 0] };
11153
+ const invW = 1 / w2;
11154
+ const S = [A[0] * invW, A[1] * invW, A[2] * invW];
11155
+ const Su = [(Au[0] - wu * S[0]) * invW, (Au[1] - wu * S[1]) * invW, (Au[2] - wu * S[2]) * invW];
11156
+ const Sv = [(Av[0] - wv * S[0]) * invW, (Av[1] - wv * S[1]) * invW, (Av[2] - wv * S[2]) * invW];
11157
+ return { S, Su, Sv };
11158
+ }
11120
11159
  /**
11121
11160
  * Tessellate the surface into a triangle mesh.
11122
11161
  * Returns positions, normals, and triangle indices.
@@ -12487,7 +12526,7 @@ function maxQuadDeviation(rings, heights, colA, colB) {
12487
12526
  const b = [rings[i][colB][0], rings[i][colB][1], heights[i]];
12488
12527
  const c2 = [rings[i + 1][colB][0], rings[i + 1][colB][1], heights[i + 1]];
12489
12528
  const d2 = [rings[i + 1][colA][0], rings[i + 1][colA][1], heights[i + 1]];
12490
- const n = cross3$7(sub3$6(b, a2), sub3$6(d2, a2));
12529
+ const n = cross3$7(sub3$7(b, a2), sub3$7(d2, a2));
12491
12530
  const len2 = Math.hypot(n[0], n[1], n[2]);
12492
12531
  if (len2 < 1e-12) continue;
12493
12532
  const deviation = Math.abs((n[0] * (c2[0] - a2[0]) + n[1] * (c2[1] - a2[1]) + n[2] * (c2[2] - a2[2])) / len2);
@@ -12763,15 +12802,15 @@ function buildSpanRows(rings, heights) {
12763
12802
  function stationTangent(stations, t, i, j) {
12764
12803
  const R = stations.length;
12765
12804
  if (i === 0) {
12766
- return scale3$2(sub3$6(stations[1][j], stations[0][j]), 1 / (t[1] - t[0]));
12805
+ return scale3$2(sub3$7(stations[1][j], stations[0][j]), 1 / (t[1] - t[0]));
12767
12806
  }
12768
12807
  if (i === R - 1) {
12769
- return scale3$2(sub3$6(stations[R - 1][j], stations[R - 2][j]), 1 / (t[R - 1] - t[R - 2]));
12808
+ return scale3$2(sub3$7(stations[R - 1][j], stations[R - 2][j]), 1 / (t[R - 1] - t[R - 2]));
12770
12809
  }
12771
12810
  const hPrev = t[i] - t[i - 1];
12772
12811
  const hNext = t[i + 1] - t[i];
12773
- const dPrev = scale3$2(sub3$6(stations[i][j], stations[i - 1][j]), 1 / hPrev);
12774
- const dNext = scale3$2(sub3$6(stations[i + 1][j], stations[i][j]), 1 / hNext);
12812
+ const dPrev = scale3$2(sub3$7(stations[i][j], stations[i - 1][j]), 1 / hPrev);
12813
+ const dNext = scale3$2(sub3$7(stations[i + 1][j], stations[i][j]), 1 / hNext);
12775
12814
  return scale3$2(add3$2(scale3$2(dPrev, hNext), scale3$2(dNext, hPrev)), 1 / (hPrev + hNext));
12776
12815
  }
12777
12816
  function hermite(p0, m0, p1, m1, h, u2) {
@@ -12827,12 +12866,12 @@ function stitchSingleLoopLoft(loops, heights, wasm, options) {
12827
12866
  const curr = points[j];
12828
12867
  const next = points[(j + 1) % N];
12829
12868
  if (cornerSet.has(j)) {
12830
- const nFwd = surfaceNormal(sub3$6(next, curr), tangents[j]);
12831
- const nBwd = surfaceNormal(sub3$6(curr, prev), tangents[j]);
12869
+ const nFwd = surfaceNormal(sub3$7(next, curr), tangents[j]);
12870
+ const nBwd = surfaceNormal(sub3$7(curr, prev), tangents[j]);
12832
12871
  fwd[j] = pushVert(curr, nFwd);
12833
12872
  bwd[j] = pushVert(curr, nBwd);
12834
12873
  } else {
12835
- const idx = pushVert(curr, surfaceNormal(sub3$6(next, prev), tangents[j]));
12874
+ const idx = pushVert(curr, surfaceNormal(sub3$7(next, prev), tangents[j]));
12836
12875
  fwd[j] = idx;
12837
12876
  bwd[j] = idx;
12838
12877
  }
@@ -12893,7 +12932,7 @@ function surfaceNormal(chord, span) {
12893
12932
  }
12894
12933
  return [n[0] / len2, n[1] / len2, n[2] / len2];
12895
12934
  }
12896
- function sub3$6(a2, b) {
12935
+ function sub3$7(a2, b) {
12897
12936
  return [a2[0] - b[0], a2[1] - b[1], a2[2] - b[2]];
12898
12937
  }
12899
12938
  function add3$2(a2, b) {
@@ -12919,7 +12958,7 @@ let _wasm = null;
12919
12958
  async function initManifoldWasm() {
12920
12959
  if (_wasm) return _wasm;
12921
12960
  performance.mark("manifold:start");
12922
- const Module = (await import("./manifold-DjBkyIc8.js")).default;
12961
+ const Module = (await import("./manifold-k2kRcc85.js")).default;
12923
12962
  performance.mark("manifold:imported");
12924
12963
  const wasm = await Module();
12925
12964
  wasm.setup();
@@ -13053,23 +13092,6 @@ let ManifoldShapeBackend = _ManifoldShapeBackend;
13053
13092
  function wrapManifoldShapeBackend(manifold) {
13054
13093
  return new ManifoldShapeBackend(manifold);
13055
13094
  }
13056
- function reconstructBackendFromMesh(mesh) {
13057
- const wasm = getManifoldWasm();
13058
- const wasmMesh = new wasm.Mesh({
13059
- numProp: mesh.numProp,
13060
- triVerts: mesh.triVerts,
13061
- vertProperties: mesh.vertProperties,
13062
- mergeFromVert: mesh.mergeFromVert.length > 0 ? mesh.mergeFromVert : void 0,
13063
- mergeToVert: mesh.mergeToVert.length > 0 ? mesh.mergeToVert : void 0
13064
- });
13065
- let manifold;
13066
- try {
13067
- manifold = new wasm.Manifold(wasmMesh);
13068
- } catch {
13069
- manifold = wasm.Manifold.cube([0, 0, 0]);
13070
- }
13071
- return new ManifoldShapeBackend(manifold);
13072
- }
13073
13095
  function requireManifoldShapeBackend(backend, apiName = "requireManifoldShapeBackend()") {
13074
13096
  if (isManifoldCapableBackend(backend)) {
13075
13097
  return backend.requireManifold(apiName);
@@ -13549,6 +13571,20 @@ function fromSlicesSingleSliceHalfExtentForManifold(plan) {
13549
13571
  }
13550
13572
  return Math.max(1, radius + maxOffsetMagnitude + plan.boundsPadding + plan.edgeLength * 3);
13551
13573
  }
13574
+ const BOOLEAN_OPERAND_NORMAL_SHARP_ANGLE_DEG = 60;
13575
+ function promoteBooleanOperandNormals(shapes) {
13576
+ let maxExtra = 0;
13577
+ for (const shape of shapes) maxExtra = Math.max(maxExtra, shape.numProp());
13578
+ if (maxExtra < 3) return { operands: shapes, created: [] };
13579
+ const created = [];
13580
+ const operands = shapes.map((shape) => {
13581
+ if (shape.numProp() >= 3) return shape;
13582
+ const promoted = shape.calculateNormals(0, BOOLEAN_OPERAND_NORMAL_SHARP_ANGLE_DEG);
13583
+ created.push(promoted);
13584
+ return promoted;
13585
+ });
13586
+ return { operands, created };
13587
+ }
13552
13588
  function lowerShapeBooleanCompilePlan(plan, wasm) {
13553
13589
  const shapes = plan.shapes.map((shape) => lowerShapeCompilePlanToManifold(shape, wasm));
13554
13590
  if (shapes.length === 0) {
@@ -13557,16 +13593,18 @@ function lowerShapeBooleanCompilePlan(plan, wasm) {
13557
13593
  if (shapes.length === 1) {
13558
13594
  return shapes[0];
13559
13595
  }
13596
+ const { operands, created } = promoteBooleanOperandNormals(shapes);
13560
13597
  try {
13561
13598
  switch (plan.op) {
13562
13599
  case "union":
13563
- return wasm.Manifold.union(shapes);
13600
+ return wasm.Manifold.union(operands);
13564
13601
  case "difference":
13565
- return wasm.Manifold.difference(shapes);
13602
+ return wasm.Manifold.difference(operands);
13566
13603
  case "intersection":
13567
- return wasm.Manifold.intersection(shapes);
13604
+ return wasm.Manifold.intersection(operands);
13568
13605
  }
13569
13606
  } finally {
13607
+ disposeWasmObjects(created);
13570
13608
  disposeWasmObjects(shapes);
13571
13609
  }
13572
13610
  }
@@ -14916,12 +14954,16 @@ function lowerNurbsSurfaceToManifold(plan, wasm) {
14916
14954
  const { positions, normals, indices } = surface.tessellate(res, res);
14917
14955
  const thickness = plan.thickness;
14918
14956
  const numVerts = positions.length;
14919
- const allPositions = [];
14920
- for (const [x2, y2, z2] of positions) allPositions.push(x2, y2, z2);
14957
+ const vertProps = [];
14921
14958
  for (let i = 0; i < numVerts; i++) {
14922
14959
  const [x2, y2, z2] = positions[i];
14923
14960
  const [nx, ny, nz] = normals[i];
14924
- allPositions.push(x2 - nx * thickness, y2 - ny * thickness, z2 - nz * thickness);
14961
+ vertProps.push(x2, y2, z2, nx, ny, nz);
14962
+ }
14963
+ for (let i = 0; i < numVerts; i++) {
14964
+ const [x2, y2, z2] = positions[i];
14965
+ const [nx, ny, nz] = normals[i];
14966
+ vertProps.push(x2 - nx * thickness, y2 - ny * thickness, z2 - nz * thickness, -nx, -ny, -nz);
14925
14967
  }
14926
14968
  const allIndices = [];
14927
14969
  for (const idx of indices) allIndices.push(idx);
@@ -14946,11 +14988,12 @@ function lowerNurbsSurfaceToManifold(plan, wasm) {
14946
14988
  allIndices.push(c2, c2 + numVerts, d2, d2, c2 + numVerts, d2 + numVerts);
14947
14989
  }
14948
14990
  const mesh = new wasm.Mesh({
14949
- numProp: 3,
14950
- vertProperties: new Float32Array(allPositions),
14991
+ numProp: 6,
14992
+ vertProperties: new Float32Array(vertProps),
14951
14993
  triVerts: new Uint32Array(allIndices)
14952
14994
  });
14953
14995
  try {
14996
+ mesh.merge();
14954
14997
  return new wasm.Manifold(mesh);
14955
14998
  } finally {
14956
14999
  disposeWasmObject(mesh);
@@ -20196,7 +20239,7 @@ function uvInsideTrim(uv, trim) {
20196
20239
  function add3$1(a2, b) {
20197
20240
  return [a2[0] + b[0], a2[1] + b[1], a2[2] + b[2]];
20198
20241
  }
20199
- function sub3$5(a2, b) {
20242
+ function sub3$6(a2, b) {
20200
20243
  return [a2[0] - b[0], a2[1] - b[1], a2[2] - b[2]];
20201
20244
  }
20202
20245
  function scale3$1(v, scale2) {
@@ -20242,7 +20285,7 @@ function resamplePolyline$1(points, count) {
20242
20285
  if (points.length === 1 || count <= 1) return [points[0]];
20243
20286
  const cumulative = [0];
20244
20287
  for (let index2 = 1; index2 < points.length; index2 += 1) {
20245
- cumulative.push(cumulative[index2 - 1] + Math.hypot(...sub3$5(points[index2], points[index2 - 1])));
20288
+ cumulative.push(cumulative[index2 - 1] + Math.hypot(...sub3$6(points[index2], points[index2 - 1])));
20246
20289
  }
20247
20290
  const total = cumulative[cumulative.length - 1];
20248
20291
  if (total <= 1e-12) return Array.from({ length: count }, () => points[0]);
@@ -20403,7 +20446,7 @@ function computeGridNormals(positions, indices) {
20403
20446
  const a2 = positions[indices[index2]];
20404
20447
  const b = positions[indices[index2 + 1]];
20405
20448
  const c2 = positions[indices[index2 + 2]];
20406
- const normal2 = cross3$5(sub3$5(b, a2), sub3$5(c2, a2));
20449
+ const normal2 = cross3$5(sub3$6(b, a2), sub3$6(c2, a2));
20407
20450
  for (const vertexIndex of [indices[index2], indices[index2 + 1], indices[index2 + 2]]) {
20408
20451
  normals[vertexIndex][0] += normal2[0];
20409
20452
  normals[vertexIndex][1] += normal2[1];
@@ -20496,16 +20539,16 @@ function planarProjectionFrame(points, context) {
20496
20539
  const tolerance = pointTolerance(points) * 100;
20497
20540
  const uSource = points.find((point2) => distance3$1(point2, origin) > tolerance);
20498
20541
  if (!uSource) throw new Error(`${context} boundary loop collapsed to one point.`);
20499
- const u2 = normalize3$5(sub3$5(uSource, origin), `${context} u axis`);
20542
+ const u2 = normalize3$5(sub3$6(uSource, origin), `${context} u axis`);
20500
20543
  const v = cross3$5(normal2, u2);
20501
- const maxPlaneError = points.reduce((max2, point2) => Math.max(max2, Math.abs(dot3$6(sub3$5(point2, origin), normal2))), 0);
20544
+ const maxPlaneError = points.reduce((max2, point2) => Math.max(max2, Math.abs(dot3$6(sub3$6(point2, origin), normal2))), 0);
20502
20545
  if (maxPlaneError > tolerance) throw new Error(`${context} boundary loop must be planar for sampled SDF triangulation.`);
20503
20546
  return { origin, u: u2, v, tolerance };
20504
20547
  }
20505
20548
  function meshFromPlanarBoundaryLoop(loop, context) {
20506
20549
  const frame = planarProjectionFrame(loop, context);
20507
20550
  const projected = loop.map((point2) => {
20508
- const relative = sub3$5(point2, frame.origin);
20551
+ const relative = sub3$6(point2, frame.origin);
20509
20552
  return new Vector2(dot3$6(relative, frame.u), dot3$6(relative, frame.v));
20510
20553
  });
20511
20554
  const triangles = ShapeUtils.triangulateShape(projected, []);
@@ -20514,7 +20557,7 @@ function meshFromPlanarBoundaryLoop(loop, context) {
20514
20557
  const indices = [];
20515
20558
  for (const triangle of triangles) {
20516
20559
  const [a2, b, c2] = triangle;
20517
- const areaNormal = cross3$5(sub3$5(positions[b], positions[a2]), sub3$5(positions[c2], positions[a2]));
20560
+ const areaNormal = cross3$5(sub3$6(positions[b], positions[a2]), sub3$6(positions[c2], positions[a2]));
20518
20561
  if (Math.hypot(...areaNormal) > frame.tolerance * frame.tolerance) indices.push(a2, b, c2);
20519
20562
  }
20520
20563
  if (indices.length === 0) throw new Error(`${context} boundary loop collapsed to zero area.`);
@@ -29031,7 +29074,7 @@ function wrapSdfMeshShapeBackend(field2, mesh) {
29031
29074
  return new SdfShapeBackend(field2, mesh);
29032
29075
  }
29033
29076
  const EPS$c = 1e-8;
29034
- function sub3$4(a2, b) {
29077
+ function sub3$5(a2, b) {
29035
29078
  return [a2[0] - b[0], a2[1] - b[1], a2[2] - b[2]];
29036
29079
  }
29037
29080
  function unit(v, context) {
@@ -29054,13 +29097,13 @@ function loopNormal(points) {
29054
29097
  }
29055
29098
  function firstEdgeDirection(points, filePath, context) {
29056
29099
  for (let index2 = 0; index2 < points.length; index2 += 1) {
29057
- const edge = sub3$4(points[(index2 + 1) % points.length], points[index2]);
29100
+ const edge = sub3$5(points[(index2 + 1) % points.length], points[index2]);
29058
29101
  if (Math.hypot(edge[0], edge[1], edge[2]) > EPS$c) return unit(edge, context);
29059
29102
  }
29060
29103
  failStepImport(filePath, `${context} face loop has no non-zero edges.`);
29061
29104
  }
29062
29105
  function project(point2, origin, u2, v) {
29063
- const local = sub3$4(point2, origin);
29106
+ const local = sub3$5(point2, origin);
29064
29107
  return new Vector2(dot3$6(local, u2), dot3$6(local, v));
29065
29108
  }
29066
29109
  function triangulateStepFace(face, filePath) {
@@ -29084,7 +29127,7 @@ function triangulateStepFace(face, filePath) {
29084
29127
  const a2 = allPoints[ia];
29085
29128
  const b = allPoints[ib];
29086
29129
  const c2 = allPoints[ic];
29087
- const triNormal = cross3$5(sub3$4(b, a2), sub3$4(c2, a2));
29130
+ const triNormal = cross3$5(sub3$5(b, a2), sub3$5(c2, a2));
29088
29131
  if (Math.hypot(triNormal[0], triNormal[1], triNormal[2]) <= EPS$c) {
29089
29132
  failStepImport(filePath, `${face.context} triangulates to a degenerate triangle.`);
29090
29133
  }
@@ -30126,7 +30169,7 @@ function angleDistance(a2, b) {
30126
30169
  return Math.abs(normalizeAngleDelta(a2 - b));
30127
30170
  }
30128
30171
  function torusMajorAngle(frame, point2, context) {
30129
- const local = sub3$3(point2, frame.origin);
30172
+ const local = sub3$4(point2, frame.origin);
30130
30173
  const x2 = dot3$6(local, frame.xAxis);
30131
30174
  const y2 = dot3$6(local, frame.yAxis);
30132
30175
  if (Math.hypot(x2, y2) <= 1e-12) throw new Error(`${context} torus major angle is degenerate.`);
@@ -30142,11 +30185,11 @@ function torusRadialDirection(frame, u2) {
30142
30185
  function torusTubeAngle(frame, majorRadius, u2, point2) {
30143
30186
  const radial = torusRadialDirection(frame, u2);
30144
30187
  const tubeCenter = addScaled$2(frame.origin, radial, majorRadius);
30145
- const local = sub3$3(point2, tubeCenter);
30188
+ const local = sub3$4(point2, tubeCenter);
30146
30189
  return Math.atan2(dot3$6(local, frame.axis), dot3$6(local, radial));
30147
30190
  }
30148
30191
  function rotateAroundAxis(point2, axis, angle) {
30149
- const local = sub3$3(point2, axis.origin);
30192
+ const local = sub3$4(point2, axis.origin);
30150
30193
  const cos2 = Math.cos(angle);
30151
30194
  const sin2 = Math.sin(angle);
30152
30195
  const cross4 = cross3$5(axis.axis, local);
@@ -30158,12 +30201,12 @@ function rotateAroundAxis(point2, axis, angle) {
30158
30201
  ];
30159
30202
  }
30160
30203
  function distanceFromAxis(point2, axis) {
30161
- const local = sub3$3(point2, axis.origin);
30204
+ const local = sub3$4(point2, axis.origin);
30162
30205
  const axial = dot3$6(local, axis.axis);
30163
30206
  return Math.hypot(local[0] - axis.axis[0] * axial, local[1] - axis.axis[1] * axial, local[2] - axis.axis[2] * axial);
30164
30207
  }
30165
30208
  function radialFromAxis(frame, point2, context) {
30166
- const local = sub3$3(point2, frame.origin);
30209
+ const local = sub3$4(point2, frame.origin);
30167
30210
  const axial = dot3$6(local, frame.axis);
30168
30211
  const radialVector = [local[0] - frame.axis[0] * axial, local[1] - frame.axis[1] * axial, local[2] - frame.axis[2] * axial];
30169
30212
  return { axial, radial: Math.hypot(radialVector[0], radialVector[1], radialVector[2]), direction: normalize3$5(radialVector, context) };
@@ -30232,9 +30275,9 @@ function triangulateBoundedLinearSurfaceOfRevolution(records, curveId, axis, loo
30232
30275
  return triangulateBoundedLineCurveSurfaceOfRevolution(line2, axis, loop, sameSense, filePath, context);
30233
30276
  }
30234
30277
  function triangulateBoundedLineCurveSurfaceOfRevolution(line2, axis, loop, sameSense, filePath, context) {
30235
- const startAxial = dot3$6(sub3$3(line2.point, axis.origin), axis.axis);
30278
+ const startAxial = dot3$6(sub3$4(line2.point, axis.origin), axis.axis);
30236
30279
  const end = linePoint(line2, 1);
30237
- const endAxial = dot3$6(sub3$3(end, axis.origin), axis.axis);
30280
+ const endAxial = dot3$6(sub3$4(end, axis.origin), axis.axis);
30238
30281
  const axialSpan = endAxial - startAxial;
30239
30282
  if (Math.abs(axialSpan) <= 1e-8)
30240
30283
  failStepImport(filePath, `${context} bounded SURFACE_OF_REVOLUTION LINE profile must change along the revolution axis.`);
@@ -30263,7 +30306,7 @@ function trimmedLineCurveForSurfaceOfRevolution(records, curve, filePath, contex
30263
30306
  const sameSense = stepBoolean(curve.args[4], `TRIMMED_CURVE #${curve.id} sense agreement`);
30264
30307
  const point2 = sameSense ? start : end;
30265
30308
  const target = sameSense ? end : start;
30266
- const delta = sub3$3(target, point2);
30309
+ const delta = sub3$4(target, point2);
30267
30310
  const magnitude = Math.hypot(delta[0], delta[1], delta[2]);
30268
30311
  if (magnitude <= 1e-10) failStepImport(filePath, `${context} TRIMMED_CURVE LINE profile has zero length.`);
30269
30312
  return {
@@ -30274,10 +30317,10 @@ function trimmedLineCurveForSurfaceOfRevolution(records, curve, filePath, contex
30274
30317
  }
30275
30318
  function revolvedCircleTorus(records, curveId, axis, filePath, context) {
30276
30319
  const circle2 = resolveCircle(records, curveId, filePath);
30277
- const centerOffset = sub3$3(circle2.frame.origin, axis.origin);
30320
+ const centerOffset = sub3$4(circle2.frame.origin, axis.origin);
30278
30321
  const axial = dot3$6(centerOffset, axis.axis);
30279
30322
  const axisPoint = addScaled$2(axis.origin, axis.axis, axial);
30280
- const radial = sub3$3(circle2.frame.origin, axisPoint);
30323
+ const radial = sub3$4(circle2.frame.origin, axisPoint);
30281
30324
  const majorRadius = Math.hypot(radial[0], radial[1], radial[2]);
30282
30325
  if (majorRadius <= 1e-8) failStepImport(filePath, `${context} SURFACE_OF_REVOLUTION circle profile is centered on the axis.`);
30283
30326
  if (majorRadius <= circle2.radius)
@@ -30289,7 +30332,7 @@ function revolvedCircleTorus(records, curveId, axis, filePath, context) {
30289
30332
  return { frame, majorRadius, minorRadius: circle2.radius };
30290
30333
  }
30291
30334
  function revolutionAngle(frame, point2, filePath, context) {
30292
- const local = sub3$3(point2, frame.origin);
30335
+ const local = sub3$4(point2, frame.origin);
30293
30336
  const x2 = dot3$6(local, frame.xAxis);
30294
30337
  const y2 = dot3$6(local, frame.yAxis);
30295
30338
  if (Math.hypot(x2, y2) <= 1e-12) failStepImport(filePath, `${context} major angle is degenerate.`);
@@ -30331,10 +30374,10 @@ function orientRevolvedProfileTriangles(triangles, frame, axis, profileCenter, p
30331
30374
  ];
30332
30375
  const angle = revolutionAngle(frame, sample, filePath, "SURFACE_OF_REVOLUTION");
30333
30376
  const center = rotateAroundAxis(profileCenter, axis, normalizeAngleDelta(angle - profileAngle));
30334
- const outward = sub3$3(sample, center);
30377
+ const outward = sub3$4(sample, center);
30335
30378
  if (Math.hypot(outward[0], outward[1], outward[2]) <= 1e-12)
30336
30379
  failStepImport(filePath, "SURFACE_OF_REVOLUTION sample hit degenerate profile centerline.");
30337
- const normal2 = cross3$5(sub3$3(triangle[1], triangle[0]), sub3$3(triangle[2], triangle[0]));
30380
+ const normal2 = cross3$5(sub3$4(triangle[1], triangle[0]), sub3$4(triangle[2], triangle[0]));
30338
30381
  const outwardFacing = dot3$6(normal2, outward) >= 0;
30339
30382
  return sameSense === outwardFacing ? triangle : reverseTriangle(triangle);
30340
30383
  });
@@ -30418,7 +30461,7 @@ function triangulateBoundedToroidalTubeSegment(frame, majorRadius, minorRadius,
30418
30461
  const loop = compactLoops[loopIndex];
30419
30462
  const majorAngle = majorAngles[loopIndex];
30420
30463
  for (const point2 of loop) {
30421
- const local = sub3$3(point2, frame.origin);
30464
+ const local = sub3$4(point2, frame.origin);
30422
30465
  const x2 = dot3$6(local, frame.xAxis);
30423
30466
  const y2 = dot3$6(local, frame.yAxis);
30424
30467
  const radialLength = Math.hypot(x2, y2);
@@ -30953,7 +30996,7 @@ function triangulateClosedVBSplineSurfaceBand(surface, loops, sameSense, filePat
30953
30996
  }
30954
30997
  return orientBridgeTriangles(triangles, centroid(a2.points), centroid(b.points), sameSense);
30955
30998
  }
30956
- function sub3$3(a2, b) {
30999
+ function sub3$4(a2, b) {
30957
31000
  return [a2[0] - b[0], a2[1] - b[1], a2[2] - b[2]];
30958
31001
  }
30959
31002
  function newellNormal(points) {
@@ -30992,9 +31035,9 @@ function triangulateBoundedSphericalCap(frame, radius, loop, sameSense, filePath
30992
31035
  }
30993
31036
  const boundaryCenter = centroid(boundary);
30994
31037
  const capDirection = normalize3$5(newellNormal(boundary), `${context} bounded SPHERICAL_SURFACE boundary normal`);
30995
- const maxPlaneError = boundary.reduce((max2, point2) => Math.max(max2, Math.abs(dot3$6(sub3$3(point2, boundaryCenter), capDirection))), 0);
31038
+ const maxPlaneError = boundary.reduce((max2, point2) => Math.max(max2, Math.abs(dot3$6(sub3$4(point2, boundaryCenter), capDirection))), 0);
30996
31039
  if (maxPlaneError > tolerance) failStepImport(filePath, `${context} bounded SPHERICAL_SURFACE boundary loop is not planar.`);
30997
- const boundaryDirections = boundary.map((point2) => normalize3$5(sub3$3(point2, frame.origin), `${context} bounded SPHERICAL_SURFACE point`));
31040
+ const boundaryDirections = boundary.map((point2) => normalize3$5(sub3$4(point2, frame.origin), `${context} bounded SPHERICAL_SURFACE point`));
30998
31041
  const maxAngle = boundaryDirections.reduce((max2, direction2) => Math.max(max2, Math.acos(clamp$8(dot3$6(direction2, capDirection), -1, 1))), 0);
30999
31042
  if (Math.PI - maxAngle <= 1e-6) failStepImport(filePath, `${context} bounded SPHERICAL_SURFACE cap is degenerate.`);
31000
31043
  const radialSegments = Math.max(4, Math.ceil(maxAngle / (Math.PI / 24)));
@@ -31033,17 +31076,17 @@ function sphericalLoopInfo(frame, radius, loop, filePath, context) {
31033
31076
  }
31034
31077
  const center = centroid(points);
31035
31078
  const normal2 = normalize3$5(newellNormal(points), `${context} bounded SPHERICAL_SURFACE boundary normal`);
31036
- const maxPlaneError = points.reduce((max2, point2) => Math.max(max2, Math.abs(dot3$6(sub3$3(point2, center), normal2))), 0);
31079
+ const maxPlaneError = points.reduce((max2, point2) => Math.max(max2, Math.abs(dot3$6(sub3$4(point2, center), normal2))), 0);
31037
31080
  if (maxPlaneError > tolerance) failStepImport(filePath, `${context} bounded SPHERICAL_SURFACE boundary loop is not planar.`);
31038
31081
  return { points, center, normal: normal2 };
31039
31082
  }
31040
31083
  function triangulateBoundedSphericalBand(frame, radius, loops, sameSense, filePath, context) {
31041
31084
  if (loops.length !== 2) failStepImport(filePath, `${context} bounded SPHERICAL_SURFACE band supports exactly two loops right now.`);
31042
31085
  const [aInfo, bInfo] = loops.map((loop) => sphericalLoopInfo(frame, radius, loop, filePath, context));
31043
- const axis = normalize3$5(sub3$3(bInfo.center, aInfo.center), `${context} bounded SPHERICAL_SURFACE band axis`);
31086
+ const axis = normalize3$5(sub3$4(bInfo.center, aInfo.center), `${context} bounded SPHERICAL_SURFACE band axis`);
31044
31087
  const tolerance = Math.max(1e-5, radius * 1e-4);
31045
31088
  const axisPointError = (point2) => {
31046
- const local = sub3$3(point2, frame.origin);
31089
+ const local = sub3$4(point2, frame.origin);
31047
31090
  const projected = addScaled$2(frame.origin, axis, dot3$6(local, axis));
31048
31091
  return Math.hypot(point2[0] - projected[0], point2[1] - projected[1], point2[2] - projected[2]);
31049
31092
  };
@@ -31052,12 +31095,12 @@ function triangulateBoundedSphericalBand(frame, radius, loops, sameSense, filePa
31052
31095
  const count = Math.max(aInfo.points.length, bInfo.points.length, 8);
31053
31096
  const aLoop = resampleClosedLoop(aInfo.points, count);
31054
31097
  let bLoop = resampleClosedLoop(bInfo.points, count);
31055
- const rawX = sub3$3(aLoop[0], aInfo.center);
31098
+ const rawX = sub3$4(aLoop[0], aInfo.center);
31056
31099
  const xProjection = dot3$6(rawX, axis);
31057
31100
  const xAxis = normalize3$5([rawX[0] - axis[0] * xProjection, rawX[1] - axis[1] * xProjection, rawX[2] - axis[2] * xProjection], context);
31058
31101
  const yAxis = cross3$5(axis, xAxis);
31059
31102
  const loopAngle = (point2, center) => {
31060
- const local = sub3$3(point2, center);
31103
+ const local = sub3$4(point2, center);
31061
31104
  return Math.atan2(dot3$6(local, yAxis), dot3$6(local, xAxis));
31062
31105
  };
31063
31106
  const rotateLoop = (loop, center, targetAngle) => {
@@ -31078,8 +31121,8 @@ function triangulateBoundedSphericalBand(frame, radius, loops, sameSense, filePa
31078
31121
  const reversed = rotateLoop([...bLoop].reverse(), bInfo.center, aAngles[0]);
31079
31122
  bLoop = sequenceMismatch(forward) <= sequenceMismatch(reversed) ? forward : reversed;
31080
31123
  const angularSpan = aLoop.reduce((max2, point2, index2) => {
31081
- const aDirection = normalize3$5(sub3$3(point2, frame.origin), `${context} bounded SPHERICAL_SURFACE band point`);
31082
- const bDirection = normalize3$5(sub3$3(bLoop[index2], frame.origin), `${context} bounded SPHERICAL_SURFACE band point`);
31124
+ const aDirection = normalize3$5(sub3$4(point2, frame.origin), `${context} bounded SPHERICAL_SURFACE band point`);
31125
+ const bDirection = normalize3$5(sub3$4(bLoop[index2], frame.origin), `${context} bounded SPHERICAL_SURFACE band point`);
31083
31126
  return Math.max(max2, Math.acos(clamp$8(dot3$6(aDirection, bDirection), -1, 1)));
31084
31127
  }, 0);
31085
31128
  if (angularSpan <= 1e-6) failStepImport(filePath, `${context} bounded SPHERICAL_SURFACE band has zero angular span.`);
@@ -31089,8 +31132,8 @@ function triangulateBoundedSphericalBand(frame, radius, loops, sameSense, filePa
31089
31132
  const t = ringIndex / radialSegments;
31090
31133
  rings.push(
31091
31134
  aLoop.map((point2, index2) => {
31092
- const aDirection = normalize3$5(sub3$3(point2, frame.origin), `${context} bounded SPHERICAL_SURFACE band point`);
31093
- const bDirection = normalize3$5(sub3$3(bLoop[index2], frame.origin), `${context} bounded SPHERICAL_SURFACE band point`);
31135
+ const aDirection = normalize3$5(sub3$4(point2, frame.origin), `${context} bounded SPHERICAL_SURFACE band point`);
31136
+ const bDirection = normalize3$5(sub3$4(bLoop[index2], frame.origin), `${context} bounded SPHERICAL_SURFACE band point`);
31094
31137
  return addScaled$2(frame.origin, slerpUnit(aDirection, bDirection, t), radius);
31095
31138
  })
31096
31139
  );
@@ -33139,14 +33182,14 @@ function cross$6(a2, b) {
33139
33182
  function isFiniteNumber$1(value) {
33140
33183
  return typeof value === "number" && Number.isFinite(value);
33141
33184
  }
33142
- function isVec3$2(value) {
33185
+ function isVec3$3(value) {
33143
33186
  return Array.isArray(value) && value.length === 3 && value.every(isFiniteNumber$1);
33144
33187
  }
33145
33188
  function isVec2(value) {
33146
33189
  return Array.isArray(value) && value.length === 2 && value.every(isFiniteNumber$1);
33147
33190
  }
33148
33191
  function isRuledRails(value) {
33149
- return Array.isArray(value) && value.length === 2 && value.every((rail2) => Array.isArray(rail2) && rail2.length === 2 && rail2.every(isVec3$2));
33192
+ return Array.isArray(value) && value.length === 2 && value.every((rail2) => Array.isArray(rail2) && rail2.length === 2 && rail2.every(isVec3$3));
33150
33193
  }
33151
33194
  function axisSpan(face, axis) {
33152
33195
  if (face.vertices.length === 0) return 0;
@@ -33163,14 +33206,14 @@ function isRationalWeights(weights) {
33163
33206
  return Boolean(weights == null ? void 0 : weights.some((row) => row.some((weight) => Math.abs(weight - 1) > 1e-9)));
33164
33207
  }
33165
33208
  function explicitGeometrySurface(geometry, face) {
33166
- if ("Plane" in geometry && isVec3$2(geometry.Plane.normal)) {
33209
+ if ("Plane" in geometry && isVec3$3(geometry.Plane.normal)) {
33167
33210
  return { kind: "plane", normal: normalizeVec3$3(geometry.Plane.normal) };
33168
33211
  }
33169
33212
  if ("AnalyticCylinder" in geometry) {
33170
33213
  const origin = geometry.AnalyticCylinder.axis_origin;
33171
33214
  const axis = geometry.AnalyticCylinder.axis_direction;
33172
33215
  const radius = geometry.AnalyticCylinder.radius;
33173
- if (isVec3$2(origin) && isVec3$2(axis) && isFiniteNumber$1(radius) && radius > 0) {
33216
+ if (isVec3$3(origin) && isVec3$3(axis) && isFiniteNumber$1(radius) && radius > 0) {
33174
33217
  const normalizedAxis = normalizeVec3$3(axis);
33175
33218
  return { kind: "cylinder", origin, axis: normalizedAxis, radius, height: axisSpan(face, normalizedAxis) };
33176
33219
  }
@@ -33180,7 +33223,7 @@ function explicitGeometrySurface(geometry, face) {
33180
33223
  const axis = geometry.AnalyticCone.axis_direction;
33181
33224
  const radiusBottom = geometry.AnalyticCone.radius_start;
33182
33225
  const radiusTop = geometry.AnalyticCone.radius_end;
33183
- if (isVec3$2(origin) && isVec3$2(axis) && isFiniteNumber$1(radiusBottom) && isFiniteNumber$1(radiusTop)) {
33226
+ if (isVec3$3(origin) && isVec3$3(axis) && isFiniteNumber$1(radiusBottom) && isFiniteNumber$1(radiusTop)) {
33184
33227
  const normalizedAxis = normalizeVec3$3(axis);
33185
33228
  return {
33186
33229
  kind: "cone",
@@ -33195,7 +33238,7 @@ function explicitGeometrySurface(geometry, face) {
33195
33238
  if ("AnalyticSphere" in geometry) {
33196
33239
  const center = geometry.AnalyticSphere.center;
33197
33240
  const radius = geometry.AnalyticSphere.radius;
33198
- if (isVec3$2(center) && isFiniteNumber$1(radius) && radius > 0) {
33241
+ if (isVec3$3(center) && isFiniteNumber$1(radius) && radius > 0) {
33199
33242
  return { kind: "sphere", center, radius };
33200
33243
  }
33201
33244
  }
@@ -33204,7 +33247,7 @@ function explicitGeometrySurface(geometry, face) {
33204
33247
  const axis = geometry.AnalyticTorus.axis;
33205
33248
  const majorRadius = geometry.AnalyticTorus.major_radius;
33206
33249
  const minorRadius = geometry.AnalyticTorus.minor_radius;
33207
- if (isVec3$2(center) && isVec3$2(axis) && isFiniteNumber$1(majorRadius) && isFiniteNumber$1(minorRadius) && majorRadius > 0 && minorRadius > 0) {
33250
+ if (isVec3$3(center) && isVec3$3(axis) && isFiniteNumber$1(majorRadius) && isFiniteNumber$1(minorRadius) && majorRadius > 0 && minorRadius > 0) {
33208
33251
  return { kind: "torus", center, axis: normalizeVec3$3(axis), majorRadius, minorRadius };
33209
33252
  }
33210
33253
  }
@@ -33302,7 +33345,7 @@ function explicitEdgeCurve(geometry, faceName) {
33302
33345
  const center = (_a3 = geometry.CircularArc) == null ? void 0 : _a3.center;
33303
33346
  const axis = (_b3 = geometry.CircularArc) == null ? void 0 : _b3.axis;
33304
33347
  const radius = (_c2 = geometry.CircularArc) == null ? void 0 : _c2.radius;
33305
- if (center !== void 0 && axis !== void 0 && radius !== void 0 && isVec3$2(center) && isVec3$2(axis) && isFiniteNumber$1(radius) && radius > 0) {
33348
+ if (center !== void 0 && axis !== void 0 && radius !== void 0 && isVec3$3(center) && isVec3$3(axis) && isFiniteNumber$1(radius) && radius > 0) {
33306
33349
  return {
33307
33350
  kind: "circle",
33308
33351
  center,
@@ -33313,7 +33356,7 @@ function explicitEdgeCurve(geometry, faceName) {
33313
33356
  }
33314
33357
  const start = (_d2 = geometry.Line) == null ? void 0 : _d2.start;
33315
33358
  const end = (_e2 = geometry.Line) == null ? void 0 : _e2.end;
33316
- if (isVec3$2(start) && isVec3$2(end)) return makeLineEdgeCurve(start, end, faceName);
33359
+ if (isVec3$3(start) && isVec3$3(end)) return makeLineEdgeCurve(start, end, faceName);
33317
33360
  const polyline = geometry.PolylineUv;
33318
33361
  if (polyline && Array.isArray(polyline.points)) {
33319
33362
  const points = polyline.points.filter(isVec2);
@@ -33658,7 +33701,7 @@ function topologyPayloadToTopology(payload) {
33658
33701
  if (explicitEdge.visual === false) continue;
33659
33702
  const start = explicitVertices[explicitEdge.vertices[0]];
33660
33703
  const end = explicitVertices[explicitEdge.vertices[1]];
33661
- if (!isVec3$2(start) || !isVec3$2(end)) continue;
33704
+ if (!isVec3$3(start) || !isVec3$3(end)) continue;
33662
33705
  const key2 = edgeKey(start, end);
33663
33706
  if (seenEdges.has(key2)) continue;
33664
33707
  seenEdges.set(key2, edges.size);
@@ -33822,6 +33865,7 @@ const _TruckShapeBackend = class _TruckShapeBackend {
33822
33865
  const payload = JSON.parse(getTruckGeometryWasm().geometry_mesh(this.getLiveHandle("getMesh()")));
33823
33866
  const numTri = payload.triangles.length / 3;
33824
33867
  const numVert = payload.positions.length / 3;
33868
+ const cornerNormals = payload.normals && payload.normals.length === numTri * 9 ? new Float32Array(payload.normals) : void 0;
33825
33869
  this.resource.mesh = {
33826
33870
  numProp: 3,
33827
33871
  numTri,
@@ -33835,7 +33879,8 @@ const _TruckShapeBackend = class _TruckShapeBackend {
33835
33879
  runTransform: new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0]),
33836
33880
  faceID: new Int32Array(payload.face_ids),
33837
33881
  faceIdNames: payload.face_id_names ?? [],
33838
- halfedgeTangent: new Float32Array(0)
33882
+ halfedgeTangent: new Float32Array(0),
33883
+ cornerNormals
33839
33884
  };
33840
33885
  return this.resource.mesh;
33841
33886
  }
@@ -35056,6 +35101,16 @@ function boundsInteriorOverlap(a2, b) {
35056
35101
  const tolerance = 1e-8;
35057
35102
  return Math.min(a2.max[0], b.max[0]) - Math.max(a2.min[0], b.min[0]) > tolerance && Math.min(a2.max[1], b.max[1]) - Math.max(a2.min[1], b.min[1]) > tolerance && Math.min(a2.max[2], b.max[2]) - Math.max(a2.min[2], b.min[2]) > tolerance;
35058
35103
  }
35104
+ function boundsFaceOrInteriorOverlap(a2, b) {
35105
+ const tolerance = 1e-8;
35106
+ const overlaps = [
35107
+ Math.min(a2.max[0], b.max[0]) - Math.max(a2.min[0], b.min[0]),
35108
+ Math.min(a2.max[1], b.max[1]) - Math.max(a2.min[1], b.min[1]),
35109
+ Math.min(a2.max[2], b.max[2]) - Math.max(a2.min[2], b.min[2])
35110
+ ];
35111
+ if (overlaps.some((overlap) => overlap < -tolerance)) return false;
35112
+ return overlaps.filter((overlap) => overlap <= tolerance).length <= 1;
35113
+ }
35059
35114
  function boundsFromPoints(points) {
35060
35115
  const first = points[0];
35061
35116
  if (!first) return null;
@@ -35138,16 +35193,16 @@ function shapePlanBounds(plan) {
35138
35193
  return null;
35139
35194
  }
35140
35195
  }
35141
- function hasPairwiseInteriorBoundsOverlap(shapes) {
35196
+ function hasPairwiseFaceOrInteriorBoundsOverlap(shapes) {
35142
35197
  const bounds = shapes.map((shape) => shape.boundingBox());
35143
35198
  for (let i = 0; i < bounds.length; i++) {
35144
35199
  for (let j = i + 1; j < bounds.length; j++) {
35145
- if (boundsInteriorOverlap(bounds[i], bounds[j])) return true;
35200
+ if (boundsFaceOrInteriorOverlap(bounds[i], bounds[j])) return true;
35146
35201
  }
35147
35202
  }
35148
35203
  return false;
35149
35204
  }
35150
- function interiorOverlapComponents(shapes) {
35205
+ function faceOrInteriorOverlapComponents(shapes) {
35151
35206
  const bounds = shapes.map((shape) => shape.boundingBox());
35152
35207
  const visited = /* @__PURE__ */ new Set();
35153
35208
  const components = [];
@@ -35161,7 +35216,7 @@ function interiorOverlapComponents(shapes) {
35161
35216
  component.push(current);
35162
35217
  for (let next = 0; next < shapes.length; next++) {
35163
35218
  if (visited.has(next)) continue;
35164
- if (boundsInteriorOverlap(bounds[current], bounds[next])) {
35219
+ if (boundsFaceOrInteriorOverlap(bounds[current], bounds[next])) {
35165
35220
  visited.add(next);
35166
35221
  queue.push(next);
35167
35222
  }
@@ -35206,10 +35261,10 @@ function lowerGenericBooleanPlan(plan) {
35206
35261
  let returned = null;
35207
35262
  try {
35208
35263
  if (plan.op === "union") {
35209
- if (!hasPairwiseInteriorBoundsOverlap(shapes)) {
35264
+ if (!hasPairwiseFaceOrInteriorBoundsOverlap(shapes)) {
35210
35265
  return null;
35211
35266
  }
35212
- const components = interiorOverlapComponents(shapes);
35267
+ const components = faceOrInteriorOverlapComponents(shapes);
35213
35268
  if (components.length > 1) {
35214
35269
  returned = lowerClusteredUnion(shapes, components);
35215
35270
  return returned;
@@ -35739,9 +35794,13 @@ function shapeHasClosedNativeTopology(shape) {
35739
35794
  function normalizeTruckShapeForBooleanInput(shape) {
35740
35795
  if (shapeHasClosedNativeTopology(shape)) return shape;
35741
35796
  if (!meshHasRawBoundaryEdges(shape.getMesh())) return shape;
35742
- const normalized = normalizeFacetedTruckShape(shape);
35743
- disposeShapeBackend(shape);
35744
- return normalized;
35797
+ try {
35798
+ const normalized = normalizeFacetedTruckShape(shape);
35799
+ disposeShapeBackend(shape);
35800
+ return normalized;
35801
+ } catch {
35802
+ return shape;
35803
+ }
35745
35804
  }
35746
35805
  function lowerSdfPlan(plan) {
35747
35806
  if (getUnsupportedSdfProgramReason(plan.tree) === void 0) {
@@ -37625,7 +37684,7 @@ function circleProfilePlan(radius, segments) {
37625
37684
  }
37626
37685
  function annulusProfilePlan(outerRadius, innerRadius, segments) {
37627
37686
  const outer = Math.abs(outerRadius);
37628
- const inner = Math.abs(innerRadius);
37687
+ const inner = Math.max(0, innerRadius);
37629
37688
  if (outer <= EXACT_PROFILE_EPS) return emptyProfilePlan();
37630
37689
  if (inner <= EXACT_PROFILE_EPS) return circleProfilePlan(outer, segments);
37631
37690
  return {
@@ -42563,11 +42622,11 @@ function requireFiniteVec3$3(v, label) {
42563
42622
  }
42564
42623
  return [x2, y2, z2];
42565
42624
  }
42566
- function len3$2(v) {
42625
+ function len3$3(v) {
42567
42626
  return Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
42568
42627
  }
42569
42628
  function normalize3$4(v) {
42570
- const l = len3$2(v);
42629
+ const l = len3$3(v);
42571
42630
  if (l < 1e-10) throw new Error("Cannot normalize zero-length vector");
42572
42631
  return [v[0] / l, v[1] / l, v[2] / l];
42573
42632
  }
@@ -42577,7 +42636,7 @@ function dot3$4(a2, b) {
42577
42636
  function cross3$2(a2, b) {
42578
42637
  return [a2[1] * b[2] - a2[2] * b[1], a2[2] * b[0] - a2[0] * b[2], a2[0] * b[1] - a2[1] * b[0]];
42579
42638
  }
42580
- function sub3$2(a2, b) {
42639
+ function sub3$3(a2, b) {
42581
42640
  return [a2[0] - b[0], a2[1] - b[1], a2[2] - b[2]];
42582
42641
  }
42583
42642
  function negate3$1(v) {
@@ -42600,7 +42659,7 @@ function normalizePortInput(input) {
42600
42659
  const end = requireFiniteVec3$3(input.end, "port end");
42601
42660
  origin = [(start[0] + end[0]) / 2, (start[1] + end[1]) / 2, (start[2] + end[2]) / 2];
42602
42661
  const dir = [end[0] - start[0], end[1] - start[1], end[2] - start[2]];
42603
- const dirLen = len3$2(dir);
42662
+ const dirLen = len3$3(dir);
42604
42663
  if (dirLen < 1e-10) {
42605
42664
  throw new Error("Port start and end must not be the same point");
42606
42665
  }
@@ -42611,7 +42670,7 @@ function normalizePortInput(input) {
42611
42670
  } else if (hasOriginAxis) {
42612
42671
  origin = requireFiniteVec3$3(input.origin, "port origin");
42613
42672
  const rawAxis = requireFiniteVec3$3(input.axis, "port axis");
42614
- if (len3$2(rawAxis) < 1e-10) {
42673
+ if (len3$3(rawAxis) < 1e-10) {
42615
42674
  throw new Error("Port axis must be non-zero");
42616
42675
  }
42617
42676
  axis = normalize3$4(rawAxis);
@@ -42627,12 +42686,12 @@ function normalizePortInput(input) {
42627
42686
  let up;
42628
42687
  if (input.up != null) {
42629
42688
  const rawUp = requireFiniteVec3$3(input.up, "port up");
42630
- if (len3$2(rawUp) < 1e-10) {
42689
+ if (len3$3(rawUp) < 1e-10) {
42631
42690
  throw new Error("Port up vector must be non-zero");
42632
42691
  }
42633
42692
  const proj = dot3$4(rawUp, axis);
42634
42693
  const ortho = [rawUp[0] - proj * axis[0], rawUp[1] - proj * axis[1], rawUp[2] - proj * axis[2]];
42635
- if (len3$2(ortho) < 1e-10) {
42694
+ if (len3$3(ortho) < 1e-10) {
42636
42695
  throw new Error("Port up vector must not be parallel to axis");
42637
42696
  }
42638
42697
  up = normalize3$4(ortho);
@@ -42696,8 +42755,8 @@ function transformPort(port, matrix) {
42696
42755
  const newOrigin = tx.point(port.origin);
42697
42756
  const rawAxis = tx.vector(port.axis);
42698
42757
  const rawUp = tx.vector(port.up);
42699
- const axisLen = len3$2(rawAxis);
42700
- const upLen = len3$2(rawUp);
42758
+ const axisLen = len3$3(rawAxis);
42759
+ const upLen = len3$3(rawUp);
42701
42760
  const out = {
42702
42761
  origin: newOrigin,
42703
42762
  axis: axisLen > 1e-10 ? normalize3$4(rawAxis) : port.axis,
@@ -42749,7 +42808,7 @@ function computeConnectFrame(childBase, childPort, parentPort, _flip, childAlign
42749
42808
  r10 * cI[0] + r11 * cI[1] + r12 * cI[2],
42750
42809
  r20 * cI[0] + r21 * cI[1] + r22 * cI[2]
42751
42810
  ];
42752
- const t = sub3$2(pOrigin, rcI);
42811
+ const t = sub3$3(pOrigin, rcI);
42753
42812
  const frame = Transform.from([r00, r10, r20, 0, r01, r11, r21, 0, r02, r12, r22, 0, t[0], t[1], t[2], 1]);
42754
42813
  const axis = cAxis;
42755
42814
  return { frame, axis };
@@ -42834,18 +42893,18 @@ function validateConnectorMatch(selfName, selfPort, targetName, targetPort, forc
42834
42893
  }
42835
42894
  }
42836
42895
  }
42837
- function len3$1(v) {
42896
+ function len3$2(v) {
42838
42897
  return Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
42839
42898
  }
42840
42899
  function normalize3$3(v) {
42841
- const l = len3$1(v);
42900
+ const l = len3$2(v);
42842
42901
  if (l < 1e-10) throw new Error("Cannot normalize zero-length vector");
42843
42902
  return [v[0] / l, v[1] / l, v[2] / l];
42844
42903
  }
42845
42904
  function cross3$1(a2, b) {
42846
42905
  return [a2[1] * b[2] - a2[2] * b[1], a2[2] * b[0] - a2[0] * b[2], a2[0] * b[1] - a2[1] * b[0]];
42847
42906
  }
42848
- function sub3$1(a2, b) {
42907
+ function sub3$2(a2, b) {
42849
42908
  return [a2[0] - b[0], a2[1] - b[1], a2[2] - b[2]];
42850
42909
  }
42851
42910
  function negate3(v) {
@@ -42868,7 +42927,7 @@ function alignmentMatrix(childOrigin, childAxis, childUp, parentOrigin, parentAx
42868
42927
  r10 * childOrigin[0] + r11 * childOrigin[1] + r12 * childOrigin[2],
42869
42928
  r20 * childOrigin[0] + r21 * childOrigin[1] + r22 * childOrigin[2]
42870
42929
  ];
42871
- const t = sub3$1(parentOrigin, rc);
42930
+ const t = sub3$2(parentOrigin, rc);
42872
42931
  return Transform.from([r00, r10, r20, 0, r01, r11, r21, 0, r02, r12, r22, 0, t[0], t[1], t[2], 1]);
42873
42932
  }
42874
42933
  function computeSinglePairAlignment(childPort, targetPort) {
@@ -42907,8 +42966,8 @@ function computeMultiPairAlignment(pairs, childPorts, targetPorts, tolerance = 0
42907
42966
  [0, 0, 0]
42908
42967
  ];
42909
42968
  for (const p2 of pairs) {
42910
- const s = sub3$1(p2.childOrigin, srcCentroid);
42911
- const t2 = sub3$1(p2.targetOrigin, tgtCentroid);
42969
+ const s = sub3$2(p2.childOrigin, srcCentroid);
42970
+ const t2 = sub3$2(p2.targetOrigin, tgtCentroid);
42912
42971
  for (let i = 0; i < 3; i++) {
42913
42972
  for (let j = 0; j < 3; j++) {
42914
42973
  h[i][j] += s[i] * t2[j];
@@ -42921,7 +42980,7 @@ function computeMultiPairAlignment(pairs, childPorts, targetPorts, tolerance = 0
42921
42980
  R[1][0] * srcCentroid[0] + R[1][1] * srcCentroid[1] + R[1][2] * srcCentroid[2],
42922
42981
  R[2][0] * srcCentroid[0] + R[2][1] * srcCentroid[1] + R[2][2] * srcCentroid[2]
42923
42982
  ];
42924
- const t = sub3$1(tgtCentroid, rSrc);
42983
+ const t = sub3$2(tgtCentroid, rSrc);
42925
42984
  const transform = Transform.from([
42926
42985
  R[0][0],
42927
42986
  R[1][0],
@@ -42943,8 +43002,8 @@ function computeMultiPairAlignment(pairs, childPorts, targetPorts, tolerance = 0
42943
43002
  const residuals = [];
42944
43003
  for (const p2 of pairs) {
42945
43004
  const transformed = transform.point(p2.childOrigin);
42946
- const diff = sub3$1(transformed, p2.targetOrigin);
42947
- residuals.push(len3$1(diff));
43005
+ const diff = sub3$2(transformed, p2.targetOrigin);
43006
+ residuals.push(len3$2(diff));
42948
43007
  }
42949
43008
  const maxResidual = Math.max(...residuals);
42950
43009
  if (maxResidual > tolerance) {
@@ -43144,8 +43203,8 @@ function getConnectorDistance(ports, nameA, nameB) {
43144
43203
  const b = ports[nameB];
43145
43204
  if (!a2) throw new Error(`connectorDistance: unknown connector "${nameA}"`);
43146
43205
  if (!b) throw new Error(`connectorDistance: unknown connector "${nameB}"`);
43147
- const d2 = sub3$1(a2.origin, b.origin);
43148
- return len3$1(d2);
43206
+ const d2 = sub3$2(a2.origin, b.origin);
43207
+ return len3$2(d2);
43149
43208
  }
43150
43209
  function getConnectorMeasurements(ports, name) {
43151
43210
  const p2 = ports[name];
@@ -48750,8 +48809,7 @@ function sculptLook(preset = "gallery") {
48750
48809
  { type: "directional", position: [90, -110, 150], color: "#ffffff", intensity: 1.4 },
48751
48810
  { type: "directional", position: [-120, 70, 80], color: "#dcecff", intensity: 0.65 },
48752
48811
  { type: "hemisphere", skyColor: "#d9edff", groundColor: "#f0e6d4", intensity: 0.45 }
48753
- ],
48754
- postProcessing: { toneMappingExposure: 1.12, vignette: { darkness: 0.16, offset: 0.6 } }
48812
+ ]
48755
48813
  };
48756
48814
  case "candy-shop":
48757
48815
  return {
@@ -48762,8 +48820,7 @@ function sculptLook(preset = "gallery") {
48762
48820
  { type: "point", position: [70, -60, 90], color: "#ff8ac8", intensity: 2.2, distance: 280, decay: 1.2 },
48763
48821
  { type: "point", position: [-85, 80, 70], color: "#70e1ff", intensity: 1.8, distance: 260, decay: 1.4 },
48764
48822
  { type: "directional", position: [30, -80, 140], color: "#fff6dd", intensity: 0.9 }
48765
- ],
48766
- postProcessing: { toneMappingExposure: 1.25, bloom: { intensity: 0.35, threshold: 0.78, radius: 0.45 } }
48823
+ ]
48767
48824
  };
48768
48825
  case "midnight":
48769
48826
  return {
@@ -48775,8 +48832,7 @@ function sculptLook(preset = "gallery") {
48775
48832
  { type: "point", position: [-90, 70, 50], color: "#ff7ac8", intensity: 1.4, distance: 300, decay: 1.3 },
48776
48833
  { type: "directional", position: [30, -30, 160], color: "#d7e9ff", intensity: 0.65 }
48777
48834
  ],
48778
- fog: { color: "#060814", near: 180, far: 520 },
48779
- postProcessing: { toneMappingExposure: 1.35, bloom: { intensity: 0.65, threshold: 0.65, radius: 0.6 } }
48835
+ fog: { color: "#060814", near: 180, far: 520 }
48780
48836
  };
48781
48837
  case "workbench":
48782
48838
  return {
@@ -48787,8 +48843,7 @@ function sculptLook(preset = "gallery") {
48787
48843
  { type: "directional", position: [80, -100, 130], color: "#fff5df", intensity: 1.25 },
48788
48844
  { type: "hemisphere", skyColor: "#eef6ff", groundColor: "#d8d0c0", intensity: 0.35 }
48789
48845
  ],
48790
- ground: { visible: true, color: "#d8d4ca", offset: 1, receiveShadow: true },
48791
- postProcessing: { toneMappingExposure: 1.05 }
48846
+ ground: { visible: true, color: "#d8d4ca", offset: 1, receiveShadow: true }
48792
48847
  };
48793
48848
  default:
48794
48849
  return {
@@ -48799,8 +48854,7 @@ function sculptLook(preset = "gallery") {
48799
48854
  { type: "directional", position: [110, -130, 150], color: "#ffffff", intensity: 1.5 },
48800
48855
  { type: "directional", position: [-90, 80, 90], color: "#b8d8ff", intensity: 0.55 },
48801
48856
  { type: "hemisphere", skyColor: "#ddefff", groundColor: "#e8e2d8", intensity: 0.45 }
48802
- ],
48803
- postProcessing: { toneMappingExposure: 1.18, vignette: { darkness: 0.18, offset: 0.55 } }
48857
+ ]
48804
48858
  };
48805
48859
  }
48806
48860
  }
@@ -50621,7 +50675,7 @@ class Shape {
50621
50675
  *
50622
50676
  * Use `.color()` to set the base diffuse color; `.material()` controls how that color behaves
50623
50677
  * under light (metalness, roughness, clearcoat) and can add emissive glow independent of
50624
- * lighting. Emissive glow pairs naturally with the `postProcessing.bloom` effect in `scene()`.
50678
+ * lighting.
50625
50679
  *
50626
50680
  * **Example**
50627
50681
  *
@@ -53031,14 +53085,14 @@ function dot3$2(a2, b) {
53031
53085
  function cross3(a2, b) {
53032
53086
  return [a2[1] * b[2] - a2[2] * b[1], a2[2] * b[0] - a2[0] * b[2], a2[0] * b[1] - a2[1] * b[0]];
53033
53087
  }
53034
- function sub3(a2, b) {
53088
+ function sub3$1(a2, b) {
53035
53089
  return [a2[0] - b[0], a2[1] - b[1], a2[2] - b[2]];
53036
53090
  }
53037
- function len3(v) {
53091
+ function len3$1(v) {
53038
53092
  return Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
53039
53093
  }
53040
53094
  function normalize3(v) {
53041
- const l = len3(v);
53095
+ const l = len3$1(v);
53042
53096
  if (l < EPS$8) return [0, 0, 0];
53043
53097
  return [v[0] / l, v[1] / l, v[2] / l];
53044
53098
  }
@@ -53050,7 +53104,7 @@ const alignDef = {
53050
53104
  const faceB = ctx.worldFace(constraint.refB.bodyId, constraint.refB.featureName);
53051
53105
  const n1 = faceA.normal;
53052
53106
  const n2 = faceB.normal;
53053
- const delta = sub3(faceB.center, faceA.center);
53107
+ const delta = sub3$1(faceB.center, faceA.center);
53054
53108
  const parallel = dot3$2(n1, n2) - 1;
53055
53109
  const normalDist = dot3$2(delta, n1);
53056
53110
  const cx = n1[1] * n2[2] - n1[2] * n2[1];
@@ -53093,7 +53147,7 @@ const concentricDef = {
53093
53147
  const axisA = ctx.worldAxis(constraint.refA.bodyId, constraint.refA.featureName);
53094
53148
  const axisB = ctx.worldAxis(constraint.refB.bodyId, constraint.refB.featureName);
53095
53149
  const dirCross = cross3(axisA.direction, axisB.direction);
53096
- const delta = sub3(axisB.origin, axisA.origin);
53150
+ const delta = sub3$1(axisB.origin, axisA.origin);
53097
53151
  const offsetCross = cross3(delta, axisA.direction);
53098
53152
  const pickTwo = (v) => {
53099
53153
  const ax = Math.abs(v[0]);
@@ -53117,9 +53171,9 @@ const faceDistanceDef = {
53117
53171
  const distance2 = constraint.value ?? 0;
53118
53172
  const n1 = faceA.normal;
53119
53173
  const n2 = faceB.normal;
53120
- const delta = sub3(faceB.center, faceA.center);
53174
+ const delta = sub3$1(faceB.center, faceA.center);
53121
53175
  const antiParallel = dot3$2(n1, n2) + 1;
53122
- const crossMag = len3(cross3(n1, n2));
53176
+ const crossMag = len3$1(cross3(n1, n2));
53123
53177
  const signedDist = dot3$2(delta, n1) - distance2;
53124
53178
  return [antiParallel, crossMag, signedDist];
53125
53179
  }
@@ -53139,7 +53193,7 @@ const flushDef = {
53139
53193
  const faceB = ctx.worldFace(constraint.refB.bodyId, constraint.refB.featureName);
53140
53194
  const n1 = faceA.normal;
53141
53195
  const n2 = faceB.normal;
53142
- const delta = sub3(faceB.center, faceA.center);
53196
+ const delta = sub3$1(faceB.center, faceA.center);
53143
53197
  const antiParallel = dot3$2(n1, n2) + 1;
53144
53198
  const normalDist = dot3$2(delta, n1);
53145
53199
  const cx = n1[1] * n2[2] - n1[2] * n2[1];
@@ -53179,7 +53233,7 @@ const pointOnAxisDef = {
53179
53233
  residual(constraint, ctx) {
53180
53234
  const point2 = ctx.worldPoint(constraint.refA.bodyId, constraint.refA.featureName);
53181
53235
  const axis = ctx.worldAxis(constraint.refB.bodyId, constraint.refB.featureName);
53182
- const delta = sub3(point2, axis.origin);
53236
+ const delta = sub3$1(point2, axis.origin);
53183
53237
  const c2 = cross3(delta, axis.direction);
53184
53238
  const ax = Math.abs(c2[0]);
53185
53239
  const ay = Math.abs(c2[1]);
@@ -53195,7 +53249,7 @@ const pointOnFaceDef = {
53195
53249
  residual(constraint, ctx) {
53196
53250
  const point2 = ctx.worldPoint(constraint.refA.bodyId, constraint.refA.featureName);
53197
53251
  const face = ctx.worldFace(constraint.refB.bodyId, constraint.refB.featureName);
53198
- const delta = sub3(point2, face.center);
53252
+ const delta = sub3$1(point2, face.center);
53199
53253
  return [dot3$2(delta, face.normal)];
53200
53254
  }
53201
53255
  };
@@ -53744,7 +53798,7 @@ function runWithForgeValidationPolicy(policy, fn) {
53744
53798
  }
53745
53799
  let _collected$8 = null;
53746
53800
  const isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
53747
- const isVec3$1 = (value) => Array.isArray(value) && value.length === 3 && isFiniteNumber(value[0]) && isFiniteNumber(value[1]) && isFiniteNumber(value[2]);
53801
+ const isVec3$2 = (value) => Array.isArray(value) && value.length === 3 && isFiniteNumber(value[0]) && isFiniteNumber(value[1]) && isFiniteNumber(value[2]);
53748
53802
  const normalizeAxis = (axis) => {
53749
53803
  const len2 = Math.hypot(axis[0], axis[1], axis[2]);
53750
53804
  if (len2 <= 1e-8) throw new Error("jointsView joint axis must be non-zero");
@@ -53779,10 +53833,10 @@ const normalizeJoint = (joint2) => {
53779
53833
  throw new Error(`jointsView joint "${name}" type must be "revolute" or "prismatic"`);
53780
53834
  }
53781
53835
  const axisRaw = joint2.axis ?? [0, 0, 1];
53782
- if (!isVec3$1(axisRaw)) throw new Error(`jointsView joint "${name}" axis must be [x, y, z]`);
53836
+ if (!isVec3$2(axisRaw)) throw new Error(`jointsView joint "${name}" axis must be [x, y, z]`);
53783
53837
  const axis = normalizeAxis([axisRaw[0], axisRaw[1], axisRaw[2]]);
53784
53838
  const pivotRaw = joint2.pivot ?? [0, 0, 0];
53785
- if (!isVec3$1(pivotRaw)) throw new Error(`jointsView joint "${name}" pivot must be [x, y, z]`);
53839
+ if (!isVec3$2(pivotRaw)) throw new Error(`jointsView joint "${name}" pivot must be [x, y, z]`);
53786
53840
  const pivot = [pivotRaw[0], pivotRaw[1], pivotRaw[2]];
53787
53841
  if (joint2.min !== void 0 && !isFiniteNumber(joint2.min)) {
53788
53842
  throw new Error(`jointsView joint "${name}" min must be a finite number`);
@@ -54701,7 +54755,7 @@ function deriveExplodeHintsFromMates(constraints, result, bodies, ctx) {
54701
54755
  const posA = result.transforms.get(c2.refA.bodyId);
54702
54756
  const posB = result.transforms.get(c2.refB.bodyId);
54703
54757
  if (posA && posB) {
54704
- const raw = sub3(bodyA.grounded ? posB.position : posA.position, bodyA.grounded ? posA.position : posB.position);
54758
+ const raw = sub3$1(bodyA.grounded ? posB.position : posA.position, bodyA.grounded ? posA.position : posB.position);
54705
54759
  dir = normalize3(raw);
54706
54760
  }
54707
54761
  break;
@@ -55227,8 +55281,8 @@ class Assembly {
55227
55281
  *
55228
55282
  * Use this after adding physical parts and joints. Robot-body profiles require
55229
55283
  * `rootPart`; asset profiles can describe one-part or multi-part physical assets.
55230
- * URDF/SDF exporters and `forgecad check simready` read this contract directly,
55231
- * so model files no longer need a separate `robotExport(...)` side effect.
55284
+ * URDF/SDF/MJCF/USD exporters and `forgecad check simready` read this
55285
+ * contract directly from the returned assembly.
55232
55286
  *
55233
55287
  * @category Assembly
55234
55288
  */
@@ -64532,7 +64586,7 @@ function beltDrive(options) {
64532
64586
  }
64533
64587
  const GEAR_META_KEY = Symbol.for("forgecad.library.gearMeta");
64534
64588
  const EPSILON$1 = 1e-9;
64535
- function clamp01$1(value) {
64589
+ function clamp01$2(value) {
64536
64590
  return Math.max(-1, Math.min(1, value));
64537
64591
  }
64538
64592
  function isFinitePositive(value) {
@@ -64552,7 +64606,7 @@ function addArcPoints(target, radius, startAngle, endAngle, steps, includeStart
64552
64606
  }
64553
64607
  }
64554
64608
  function flankAngleAtRadius(radius, baseRadius, halfThicknessAtPitch, pressureAngleRad) {
64555
- const alphaAtRadius = Math.acos(clamp01$1(baseRadius / Math.max(radius, baseRadius)));
64609
+ const alphaAtRadius = Math.acos(clamp01$2(baseRadius / Math.max(radius, baseRadius)));
64556
64610
  return halfThicknessAtPitch + involuteFn(pressureAngleRad) - involuteFn(alphaAtRadius);
64557
64611
  }
64558
64612
  function addRootFilletPoints(target, rootRadius, filletRadius, flankAngle, sign2, fromFlank, steps) {
@@ -65672,7 +65726,7 @@ function gearPair(options) {
65672
65726
  message: `Center distance ${centerDistance.toFixed(4)} exceeds addendum reach ${addendumReach.toFixed(4)} (no mesh contact)`
65673
65727
  });
65674
65728
  }
65675
- const cosWorking = clamp01$1(baseSum / Math.max(centerDistance, EPSILON$1));
65729
+ const cosWorking = clamp01$2(baseSum / Math.max(centerDistance, EPSILON$1));
65676
65730
  const alphaWorking = Math.acos(cosWorking);
65677
65731
  const basePitch = Math.PI * module * Math.cos(alpha);
65678
65732
  const pathLength = Math.sqrt(Math.max(0, pinion.meta.outerRadius ** 2 - pinion.meta.baseRadius ** 2)) + Math.sqrt(Math.max(0, gear.meta.outerRadius ** 2 - gear.meta.baseRadius ** 2)) - centerDistance * Math.sin(alphaWorking);
@@ -66385,7 +66439,8 @@ const partLibrary = {
66385
66439
  const EDGE_THRESHOLD_DOT = Math.cos(Math.PI / 180);
66386
66440
  const SMOOTH_THRESHOLD_DOT = Math.cos(30 * Math.PI / 180);
66387
66441
  function computeGeometryArrays(mesh, options = {}) {
66388
- const { numProp, numTri: triCount, triVerts, vertProperties, vertNormals } = mesh;
66442
+ const { numProp, numTri: triCount, triVerts, vertProperties, vertNormals, cornerNormals } = mesh;
66443
+ const useCornerNormals = !!cornerNormals && cornerNormals.length === triCount * 9;
66389
66444
  const positions = new Float32Array(triCount * 9);
66390
66445
  const normals = new Float32Array(triCount * 9);
66391
66446
  const faceNx = new Float32Array(triCount);
@@ -66417,7 +66472,9 @@ function computeGeometryArrays(mesh, options = {}) {
66417
66472
  positions[o + 6] = cx;
66418
66473
  positions[o + 7] = cy;
66419
66474
  positions[o + 8] = cz;
66420
- if (vertNormals) {
66475
+ if (useCornerNormals) {
66476
+ for (let k2 = 0; k2 < 9; k2++) normals[o + k2] = cornerNormals[o + k2];
66477
+ } else if (vertNormals) {
66421
66478
  normals[o] = vertNormals[i0 * 3];
66422
66479
  normals[o + 1] = vertNormals[i0 * 3 + 1];
66423
66480
  normals[o + 2] = vertNormals[i0 * 3 + 2];
@@ -66460,7 +66517,7 @@ function computeGeometryArrays(mesh, options = {}) {
66460
66517
  faceNy[t] = fny;
66461
66518
  faceNz[t] = fnz;
66462
66519
  }
66463
- if (!vertNormals && numProp < 6 && triCount > 0) {
66520
+ if (!useCornerNormals && !vertNormals && numProp < 6 && triCount > 0) {
66464
66521
  computeAutoSmoothNormals(
66465
66522
  triVerts,
66466
66523
  vertProperties,
@@ -66625,26 +66682,26 @@ function getPendingShapeHighlights() {
66625
66682
  function resetPendingShapeHighlights() {
66626
66683
  pendingShapeHighlights = [];
66627
66684
  }
66628
- function isVec3(v) {
66685
+ function isVec3$1(v) {
66629
66686
  return Array.isArray(v) && v.length === 3 && typeof v[0] === "number" && typeof v[1] === "number" && typeof v[2] === "number";
66630
66687
  }
66631
66688
  function isEdgePair(v) {
66632
- return Array.isArray(v) && v.length === 2 && isVec3(v[0]) && isVec3(v[1]);
66689
+ return Array.isArray(v) && v.length === 2 && isVec3$1(v[0]) && isVec3$1(v[1]);
66633
66690
  }
66634
66691
  function isPlaneSpec(v) {
66635
66692
  if (typeof v !== "object" || v === null) return false;
66636
66693
  const obj = v;
66637
- return isVec3(obj.normal) && (typeof obj.offset === "number" || isVec3(obj.point));
66694
+ return isVec3$1(obj.normal) && (typeof obj.offset === "number" || isVec3$1(obj.point));
66638
66695
  }
66639
66696
  function isFaceRef$1(v) {
66640
66697
  if (typeof v !== "object" || v === null) return false;
66641
66698
  const obj = v;
66642
- return isVec3(obj.normal) && isVec3(obj.center) && typeof obj.name === "string";
66699
+ return isVec3$1(obj.normal) && isVec3$1(obj.center) && typeof obj.name === "string";
66643
66700
  }
66644
66701
  function isEdgeRef(v) {
66645
66702
  if (typeof v !== "object" || v === null) return false;
66646
66703
  const obj = v;
66647
- return isVec3(obj.start) && isVec3(obj.end) && typeof obj.name === "string";
66704
+ return isVec3$1(obj.start) && isVec3$1(obj.end) && typeof obj.name === "string";
66648
66705
  }
66649
66706
  function requireFiniteVec3(v, name) {
66650
66707
  for (let i = 0; i < 3; i++) {
@@ -66683,7 +66740,7 @@ function highlight(target, opts) {
66683
66740
  });
66684
66741
  return;
66685
66742
  }
66686
- if (isVec3(target)) {
66743
+ if (isVec3$1(target)) {
66687
66744
  requireFiniteVec3(target, "point");
66688
66745
  collectedDebugHighlights3D.push({
66689
66746
  kind: "point",
@@ -67727,7 +67784,7 @@ const PRESETS = {
67727
67784
  }
67728
67785
  };
67729
67786
  new Set(Object.keys(PRESETS));
67730
- function clamp01(value) {
67787
+ function clamp01$1(value) {
67731
67788
  return Math.max(0, Math.min(1, value));
67732
67789
  }
67733
67790
  function applyMaterial(shape, preset) {
@@ -67749,7 +67806,7 @@ const materials = {
67749
67806
  clearPolycarbonate(options = {}) {
67750
67807
  return {
67751
67808
  color: options.tint ?? "#bdefff",
67752
- material: { opacity: clamp01(options.opacity ?? 0.34), roughness: 0.08, metalness: 0, clearcoat: 1, clearcoatRoughness: 0.03 }
67809
+ material: { opacity: clamp01$1(options.opacity ?? 0.34), roughness: 0.08, metalness: 0, clearcoat: 1, clearcoatRoughness: 0.03 }
67753
67810
  };
67754
67811
  },
67755
67812
  /** Brushed steel-like material for trim, soleplates, and hardware. */
@@ -68226,31 +68283,6 @@ function validateFog(fog, label) {
68226
68283
  if (fog.density !== void 0) out.density = requireFinite$5(fog.density, `${label}.density`);
68227
68284
  return out;
68228
68285
  }
68229
- function validatePostProcessing(pp, label) {
68230
- const out = {};
68231
- if (pp.bloom !== void 0) {
68232
- if (!pp.bloom || typeof pp.bloom !== "object") throw new Error(`${label}.bloom must be an object`);
68233
- out.bloom = {};
68234
- if (pp.bloom.intensity !== void 0) out.bloom.intensity = requireFinite$5(pp.bloom.intensity, `${label}.bloom.intensity`);
68235
- if (pp.bloom.threshold !== void 0) out.bloom.threshold = requireFinite$5(pp.bloom.threshold, `${label}.bloom.threshold`);
68236
- if (pp.bloom.radius !== void 0) out.bloom.radius = requireFinite$5(pp.bloom.radius, `${label}.bloom.radius`);
68237
- }
68238
- if (pp.vignette !== void 0) {
68239
- if (!pp.vignette || typeof pp.vignette !== "object") throw new Error(`${label}.vignette must be an object`);
68240
- out.vignette = {};
68241
- if (pp.vignette.darkness !== void 0) out.vignette.darkness = requireFinite$5(pp.vignette.darkness, `${label}.vignette.darkness`);
68242
- if (pp.vignette.offset !== void 0) out.vignette.offset = requireFinite$5(pp.vignette.offset, `${label}.vignette.offset`);
68243
- }
68244
- if (pp.grain !== void 0) {
68245
- if (!pp.grain || typeof pp.grain !== "object") throw new Error(`${label}.grain must be an object`);
68246
- out.grain = {};
68247
- if (pp.grain.intensity !== void 0) out.grain.intensity = requireFinite$5(pp.grain.intensity, `${label}.grain.intensity`);
68248
- }
68249
- if (pp.toneMappingExposure !== void 0) {
68250
- out.toneMappingExposure = requireFinite$5(pp.toneMappingExposure, `${label}.toneMappingExposure`);
68251
- }
68252
- return out;
68253
- }
68254
68286
  function validateGround(ground, label) {
68255
68287
  const out = {};
68256
68288
  if (ground.visible !== void 0) {
@@ -68332,7 +68364,6 @@ function scene(options) {
68332
68364
  lights: null,
68333
68365
  environment: null,
68334
68366
  fog: null,
68335
- postProcessing: null,
68336
68367
  ground: null,
68337
68368
  capture: null
68338
68369
  };
@@ -68372,12 +68403,9 @@ function scene(options) {
68372
68403
  }
68373
68404
  current.fog = validateFog(options.fog, "scene.fog");
68374
68405
  }
68375
- if (options.postProcessing !== void 0) {
68376
- if (!options.postProcessing || typeof options.postProcessing !== "object") {
68377
- throw new Error("scene.postProcessing must be an object");
68378
- }
68379
- const validated = validatePostProcessing(options.postProcessing, "scene.postProcessing");
68380
- current.postProcessing = current.postProcessing ? { ...current.postProcessing, ...validated } : validated;
68406
+ const disabledPostProcessing = options.postProcessing;
68407
+ if (disabledPostProcessing !== void 0) {
68408
+ console.warn("scene.postProcessing is disabled for now while the browser post-processing path is being rebuilt.");
68381
68409
  }
68382
68410
  if (options.ground !== void 0) {
68383
68411
  if (!options.ground || typeof options.ground !== "object") {
@@ -68537,8 +68565,7 @@ function scenePreset(name) {
68537
68565
  { type: "hemisphere", skyColor: "#ffffff", groundColor: "#d5d9de", intensity: 0.75 },
68538
68566
  { type: "directional", position: [90, -120, 180], color: "#ffffff", intensity: 1.8, castShadow: true },
68539
68567
  { type: "directional", position: [-140, 100, 80], color: "#dfe9ff", intensity: 0.55 }
68540
- ],
68541
- postProcessing: { toneMappingExposure: 1.08 }
68568
+ ]
68542
68569
  });
68543
68570
  return;
68544
68571
  }
@@ -68780,8 +68807,8 @@ function shapeBoundsCenter(shape) {
68780
68807
  ];
68781
68808
  }
68782
68809
  class ProductSurfaceRef {
68783
- constructor(skin, query, name) {
68784
- this.skin = skin;
68810
+ constructor(skin2, query, name) {
68811
+ this.skin = skin2;
68785
68812
  this.query = query;
68786
68813
  this.name = name;
68787
68814
  }
@@ -69298,8 +69325,8 @@ class ProductHandleBuilder {
69298
69325
  }
69299
69326
  }
69300
69327
  class ProductSurfaceBuilder {
69301
- constructor(skin, side) {
69302
- this.skin = skin;
69328
+ constructor(skin2, side) {
69329
+ this.skin = skin2;
69303
69330
  this.side = side;
69304
69331
  }
69305
69332
  /** Create a ref on this skin side. */
@@ -69367,9 +69394,9 @@ class ProductRibbonBuilder {
69367
69394
  * ProductSkin.frame(), so the ribbon bends along the selected side as station width/depth changes.
69368
69395
  * All query path points must stay on one side; split side transitions into separate ribbons.
69369
69396
  */
69370
- on(skin, points, options = {}) {
69397
+ on(skin2, points, options = {}) {
69371
69398
  if (points.length < 2) throw new Error("Product.ribbon().on(skin, points) requires at least two path points");
69372
- this.skinValue = skin;
69399
+ this.skinValue = skin2;
69373
69400
  this.queryPath = resolvePathQueries(points);
69374
69401
  this.refPath = [];
69375
69402
  return this.applyOptions(options);
@@ -69492,7 +69519,7 @@ class ProductRibbonBuilder {
69492
69519
  const localT = scaled - segment;
69493
69520
  return interpolateQuery(this.queryPath[segment], this.queryPath[segment + 1], localT);
69494
69521
  }
69495
- buildSkinGrid(skin, path2) {
69522
+ buildSkinGrid(skin2, path2) {
69496
69523
  if (path2.length < 2) throw new Error("Product.ribbon().on(skin, points) must be called before .build()");
69497
69524
  const side = normalizedSide(path2[0].side);
69498
69525
  if (side === "front" || side === "rear") {
@@ -69511,7 +69538,7 @@ class ProductRibbonBuilder {
69511
69538
  for (let i = 0; i < this.samplesValue; i += 1) {
69512
69539
  const along = this.samplesValue === 1 ? 0 : i / (this.samplesValue - 1);
69513
69540
  const center = this.samplePathQuery(along);
69514
- const station = skin.stationAt(center.v ?? 0.5);
69541
+ const station = skin2.stationAt(center.v ?? 0.5);
69515
69542
  const span = sideSpan(side, station.width, station.depth);
69516
69543
  for (let j = 0; j < this.widthSamplesValue; j += 1) {
69517
69544
  const across = this.widthSamplesValue === 1 ? 0 : j / (this.widthSamplesValue - 1) - 0.5;
@@ -69528,13 +69555,13 @@ class ProductRibbonBuilder {
69528
69555
  u: u2,
69529
69556
  offset: (center.offset ?? 0) + this.offsetValue + this.thicknessValue
69530
69557
  };
69531
- rows[j].push(skin.frame(query).point);
69558
+ rows[j].push(skin2.frame(query).point);
69532
69559
  }
69533
69560
  }
69534
69561
  return {
69535
69562
  grid: rows,
69536
69563
  diagnostics: this.makeDiagnostics({
69537
- skin: skin.name,
69564
+ skin: skin2.name,
69538
69565
  side,
69539
69566
  pathPointCount: path2.length,
69540
69567
  clampedUCount,
@@ -69691,16 +69718,16 @@ const Product = {
69691
69718
  return scaleProfileTo(sketch, width, depth);
69692
69719
  },
69693
69720
  /** Create an ad-hoc ProductSurfaceRef from a skin and side/u/v query. */
69694
- ref(skin, query) {
69695
- return new ProductSurfaceRef(skin, query);
69721
+ ref(skin2, query) {
69722
+ return new ProductSurfaceRef(skin2, query);
69696
69723
  },
69697
69724
  /**
69698
69725
  * Create a fluent surface helper for refs and conformal features on one side of a skin.
69699
69726
  *
69700
69727
  * Equivalent to skin.surface(side), useful when writing in Product.* namespace style.
69701
69728
  */
69702
- surface(skin, side) {
69703
- return skin.surface(side);
69729
+ surface(skin2, side) {
69730
+ return skin2.surface(side);
69704
69731
  },
69705
69732
  /** Start a panel feature builder. */
69706
69733
  panel(name) {
@@ -70169,9 +70196,9 @@ function coordinateOnSide(coordinate, side, label) {
70169
70196
  return { ...coordinate, kind: "productSkin", side };
70170
70197
  }
70171
70198
  class ProductSkinCarrier {
70172
- constructor(skin, name = skin.name, sideValue2, offsetValue = 0) {
70199
+ constructor(skin2, name = skin2.name, sideValue2, offsetValue = 0) {
70173
70200
  __publicField(this, "kind", "productSkin");
70174
- this.skin = skin;
70201
+ this.skin = skin2;
70175
70202
  this.name = name;
70176
70203
  this.sideValue = sideValue2;
70177
70204
  this.offsetValue = offsetValue;
@@ -73105,8 +73132,8 @@ const Carrier = {
73105
73132
  return new PlaneCarrier(name);
73106
73133
  },
73107
73134
  /** Adapt an existing ProductSkin into the general surface-member carrier protocol. */
73108
- productSkin(skin) {
73109
- return new ProductSkinCarrier(skin);
73135
+ productSkin(skin2) {
73136
+ return new ProductSkinCarrier(skin2);
73110
73137
  },
73111
73138
  /** Reserved stub for future parameterized mesh carriers; arbitrary mesh parameterization is not implemented yet. */
73112
73139
  mesh(name) {
@@ -73174,206 +73201,6 @@ const SurfaceMembers = {
73174
73201
  }
73175
73202
  };
73176
73203
  new TextEncoder();
73177
- let _collectedRobotExport = null;
73178
- function cloneLinkOptions(input) {
73179
- if (!input) return {};
73180
- return Object.fromEntries(Object.entries(input).map(([name, opts]) => [name, { ...opts }]));
73181
- }
73182
- function cloneJointOptions(input) {
73183
- if (!input) return {};
73184
- return Object.fromEntries(Object.entries(input).map(([name, opts]) => [name, { ...opts }]));
73185
- }
73186
- function cloneDiffDrive(input) {
73187
- if (!input) return void 0;
73188
- return {
73189
- ...input,
73190
- leftJoints: [...input.leftJoints],
73191
- rightJoints: [...input.rightJoints]
73192
- };
73193
- }
73194
- function cloneJointStatePublisher(input) {
73195
- if (!input) return void 0;
73196
- return {
73197
- ...input,
73198
- joints: input.joints ? [...input.joints] : void 0
73199
- };
73200
- }
73201
- function cloneWorld(input) {
73202
- if (!input) return null;
73203
- return {
73204
- ...input,
73205
- spawnPose: input.spawnPose ? [...input.spawnPose] : void 0,
73206
- keyboardTeleop: input.keyboardTeleop ? { ...input.keyboardTeleop } : void 0
73207
- };
73208
- }
73209
- function assertFinite(value, label) {
73210
- if (value !== void 0 && !Number.isFinite(value)) {
73211
- throw new Error(`${label} must be finite`);
73212
- }
73213
- }
73214
- function metadataNumber(value) {
73215
- return typeof value === "number" ? value : void 0;
73216
- }
73217
- function metadataCollision(value) {
73218
- return value === "none" || value === "visual" || value === "box" || value === "convex" ? value : void 0;
73219
- }
73220
- function colliderCollision(collider2) {
73221
- if (!collider2) return void 0;
73222
- if (collider2.mode === "box") return "box";
73223
- if (collider2.mode === "visual") return "visual";
73224
- return collider2.mode;
73225
- }
73226
- function velocityDriveEffort(drive) {
73227
- return (drive == null ? void 0 : drive.kind) === "velocity" ? drive.maxTorqueNm : void 0;
73228
- }
73229
- function velocityDriveVelocityDegS(drive) {
73230
- return (drive == null ? void 0 : drive.kind) === "velocity" ? drive.maxSpeedRpm * 6 : void 0;
73231
- }
73232
- function driveDamping(drive) {
73233
- return drive == null ? void 0 : drive.damping;
73234
- }
73235
- function driveFriction(drive) {
73236
- return drive == null ? void 0 : drive.friction;
73237
- }
73238
- function jointByName(assembly2) {
73239
- return new Map(assembly2.joints.map((joint2) => [joint2.name, joint2]));
73240
- }
73241
- function diffDriveFromControllers(controllers) {
73242
- if (!controllers) return void 0;
73243
- const diffDrives = controllers.filter((controller) => controller.kind === "diffDrive");
73244
- if (diffDrives.length > 1) {
73245
- throw new Error("assembly.withSimulation(...) currently supports one Sim.controller.diffDrive(...) controller");
73246
- }
73247
- const diffDrive2 = diffDrives[0];
73248
- return diffDrive2 ? {
73249
- leftJoints: [...diffDrive2.leftJoints],
73250
- rightJoints: [...diffDrive2.rightJoints],
73251
- wheelSeparationMm: diffDrive2.wheelSeparationMm,
73252
- wheelRadiusMm: diffDrive2.wheelRadiusMm,
73253
- topic: diffDrive2.topic,
73254
- odomTopic: diffDrive2.odomTopic,
73255
- tfTopic: diffDrive2.tfTopic,
73256
- frameId: diffDrive2.frameId,
73257
- odomFrameId: diffDrive2.odomFrameId,
73258
- maxLinearVelocity: diffDrive2.maxLinearVelocity,
73259
- maxAngularVelocity: diffDrive2.maxAngularVelocity,
73260
- linearAcceleration: diffDrive2.linearAcceleration,
73261
- angularAcceleration: diffDrive2.angularAcceleration
73262
- } : void 0;
73263
- }
73264
- function resetRobotExport() {
73265
- _collectedRobotExport = null;
73266
- }
73267
- function getCollectedRobotExport() {
73268
- return _collectedRobotExport;
73269
- }
73270
- function collectSimulationModel(assemblyInput, options = {}) {
73271
- var _a3, _b3, _c2, _d2, _e2, _f2, _g2, _h2, _i2;
73272
- const assembly2 = typeof assemblyInput.describe === "function" ? assemblyInput.describe() : assemblyInput;
73273
- const partNames = new Set(assembly2.parts.map((part) => part.name));
73274
- const joints = jointByName(assembly2);
73275
- const links = cloneLinkOptions(options.links);
73276
- for (const part of assembly2.parts) {
73277
- const fromSim = part.sim;
73278
- const fromMaterialDensity = (_a3 = fromSim == null ? void 0 : fromSim.material) == null ? void 0 : _a3.densityKgM3;
73279
- const current = links[part.name] ?? {};
73280
- const next = {
73281
- massKg: current.massKg ?? (fromSim == null ? void 0 : fromSim.massKg) ?? metadataNumber((_b3 = part.metadata) == null ? void 0 : _b3.massKg),
73282
- densityKgM3: current.densityKgM3 ?? (fromSim == null ? void 0 : fromSim.densityKgM3) ?? fromMaterialDensity ?? metadataNumber((_c2 = part.metadata) == null ? void 0 : _c2.densityKgM3),
73283
- collision: current.collision ?? colliderCollision(fromSim == null ? void 0 : fromSim.collider) ?? metadataCollision((_d2 = part.metadata) == null ? void 0 : _d2.collision)
73284
- };
73285
- if (next.massKg !== void 0 || next.densityKgM3 !== void 0 || next.collision !== void 0) {
73286
- links[part.name] = next;
73287
- }
73288
- }
73289
- for (const [partName, link] of Object.entries(links)) {
73290
- if (!partNames.has(partName)) throw new Error(`simulation model references unknown link "${partName}"`);
73291
- assertFinite(link.massKg, `simulation model link "${partName}" massKg`);
73292
- assertFinite(link.densityKgM3, `simulation model link "${partName}" densityKgM3`);
73293
- }
73294
- const jointOpts = cloneJointOptions(options.joints);
73295
- for (const joint2 of assembly2.joints) {
73296
- const drive = (_e2 = joint2.sim) == null ? void 0 : _e2.drive;
73297
- const current = jointOpts[joint2.name] ?? {};
73298
- const next = {
73299
- effort: current.effort ?? velocityDriveEffort(drive) ?? joint2.effort,
73300
- velocity: current.velocity ?? velocityDriveVelocityDegS(drive) ?? joint2.velocity,
73301
- damping: current.damping ?? driveDamping(drive) ?? joint2.damping,
73302
- friction: current.friction ?? driveFriction(drive) ?? joint2.friction
73303
- };
73304
- if (next.effort !== void 0 || next.velocity !== void 0 || next.damping !== void 0 || next.friction !== void 0) {
73305
- jointOpts[joint2.name] = next;
73306
- }
73307
- }
73308
- for (const [jointName, joint2] of Object.entries(jointOpts)) {
73309
- if (!joints.has(jointName)) throw new Error(`simulation model references unknown joint "${jointName}"`);
73310
- assertFinite(joint2.effort, `simulation model joint "${jointName}" effort`);
73311
- assertFinite(joint2.velocity, `simulation model joint "${jointName}" velocity`);
73312
- assertFinite(joint2.damping, `simulation model joint "${jointName}" damping`);
73313
- assertFinite(joint2.friction, `simulation model joint "${jointName}" friction`);
73314
- }
73315
- const simulation = cloneSimAssemblySimulation(assembly2.sim) ?? null;
73316
- const simulationDiffDrive = diffDriveFromControllers(simulation == null ? void 0 : simulation.controllers);
73317
- const diffDrive2 = cloneDiffDrive((_f2 = options.plugins) == null ? void 0 : _f2.diffDrive) ?? simulationDiffDrive;
73318
- if (diffDrive2) {
73319
- if (diffDrive2.leftJoints.length === 0 || diffDrive2.rightJoints.length === 0) {
73320
- throw new Error("simulation model diffDrive requires at least one left joint and one right joint");
73321
- }
73322
- assertFinite(diffDrive2.wheelSeparationMm, "simulation model diffDrive wheelSeparationMm");
73323
- assertFinite(diffDrive2.wheelRadiusMm, "simulation model diffDrive wheelRadiusMm");
73324
- if (diffDrive2.wheelSeparationMm <= 0 || diffDrive2.wheelRadiusMm <= 0) {
73325
- throw new Error("simulation model diffDrive wheel separation and radius must be > 0");
73326
- }
73327
- [...diffDrive2.leftJoints, ...diffDrive2.rightJoints].forEach((jointName) => {
73328
- const joint2 = joints.get(jointName);
73329
- if (!joint2) throw new Error(`simulation model diffDrive references unknown joint "${jointName}"`);
73330
- if (joint2.type !== "revolute") {
73331
- throw new Error(`simulation model diffDrive joint "${jointName}" must be revolute`);
73332
- }
73333
- });
73334
- }
73335
- const jointStatePublisher = cloneJointStatePublisher((_g2 = options.plugins) == null ? void 0 : _g2.jointStatePublisher);
73336
- if (jointStatePublisher == null ? void 0 : jointStatePublisher.joints) {
73337
- jointStatePublisher.joints.forEach((jointName) => {
73338
- if (!joints.has(jointName)) {
73339
- throw new Error(`simulation model jointStatePublisher references unknown joint "${jointName}"`);
73340
- }
73341
- });
73342
- }
73343
- const world = cloneWorld(options.world);
73344
- if (world == null ? void 0 : world.spawnPose) {
73345
- world.spawnPose.forEach((value, index2) => assertFinite(value, `simulation model world spawnPose[${index2}]`));
73346
- }
73347
- assertFinite((_h2 = world == null ? void 0 : world.keyboardTeleop) == null ? void 0 : _h2.linearStep, "simulation model world keyboardTeleop.linearStep");
73348
- assertFinite((_i2 = world == null ? void 0 : world.keyboardTeleop) == null ? void 0 : _i2.angularStep, "simulation model world keyboardTeleop.angularStep");
73349
- return {
73350
- modelName: (options.modelName ?? assembly2.name ?? "ForgeCAD Simulation").trim() || "ForgeCAD Simulation",
73351
- assembly: assembly2,
73352
- simulation,
73353
- source: options.source ?? "assembly",
73354
- state: { ...options.state ?? {} },
73355
- static: options.static ?? false,
73356
- selfCollide: options.selfCollide ?? false,
73357
- allowAutoDisable: options.allowAutoDisable ?? true,
73358
- links,
73359
- joints: jointOpts,
73360
- plugins: {
73361
- diffDrive: diffDrive2,
73362
- jointStatePublisher
73363
- },
73364
- world
73365
- };
73366
- }
73367
- function robotExport(options) {
73368
- if (!options || typeof options !== "object") {
73369
- throw new Error("robotExport(...) expects an options object");
73370
- }
73371
- if (!options.assembly || typeof options.assembly.describe !== "function") {
73372
- throw new Error("robotExport(...) requires an assembly");
73373
- }
73374
- _collectedRobotExport = collectSimulationModel(options.assembly, { ...options, source: "robotExport" });
73375
- return _collectedRobotExport;
73376
- }
73377
73204
  class Point2D {
73378
73205
  constructor(x2, y2) {
73379
73206
  this.x = x2;
@@ -95647,6 +95474,847 @@ function sketchOnFace(sketch, parentOrFace, faceOrOpts, maybeOpts = {}) {
95647
95474
  Sketch.prototype.onFace = function(parentOrFace, faceOrOpts, maybeOpts = {}) {
95648
95475
  return sketchOnFace(this, parentOrFace, faceOrOpts, maybeOpts);
95649
95476
  };
95477
+ const KNOT_EPS = 1e-10;
95478
+ const KNOT_MERGE_EPS = 1e-7;
95479
+ function sameKnot(a2, b) {
95480
+ return Math.abs(a2 - b) <= KNOT_EPS;
95481
+ }
95482
+ function knotMultiplicity(knots, u2) {
95483
+ let count = 0;
95484
+ for (const k2 of knots) if (sameKnot(k2, u2)) count++;
95485
+ return count;
95486
+ }
95487
+ function computeParamsCentripetal(points, alpha = 0.5) {
95488
+ const n = points.length;
95489
+ const params = new Array(n).fill(0);
95490
+ const seg = new Array(n).fill(0);
95491
+ let total = 0;
95492
+ for (let k2 = 1; k2 < n; k2++) {
95493
+ const dx = points[k2][0] - points[k2 - 1][0];
95494
+ const dy = points[k2][1] - points[k2 - 1][1];
95495
+ const dz = points[k2][2] - points[k2 - 1][2];
95496
+ seg[k2] = Math.hypot(dx, dy, dz) ** alpha;
95497
+ total += seg[k2];
95498
+ }
95499
+ if (total === 0) {
95500
+ for (let k2 = 0; k2 < n; k2++) params[k2] = n > 1 ? k2 / (n - 1) : 0;
95501
+ return params;
95502
+ }
95503
+ let acc = 0;
95504
+ for (let k2 = 1; k2 < n - 1; k2++) {
95505
+ acc += seg[k2];
95506
+ params[k2] = acc / total;
95507
+ }
95508
+ params[n - 1] = 1;
95509
+ return params;
95510
+ }
95511
+ function knotsFromParamsAveraging(params, degree) {
95512
+ const n = params.length - 1;
95513
+ const m2 = n + degree + 1;
95514
+ const knots = new Array(m2 + 1).fill(0);
95515
+ for (let i = m2 - degree; i <= m2; i++) knots[i] = 1;
95516
+ for (let j = 1; j <= n - degree; j++) {
95517
+ let s = 0;
95518
+ for (let i = j; i <= j + degree - 1; i++) s += params[i];
95519
+ knots[j + degree] = s / degree;
95520
+ }
95521
+ return knots;
95522
+ }
95523
+ function solveLinear(A, B2) {
95524
+ const n = A.length;
95525
+ const k2 = B2[0].length;
95526
+ const M = A.map((row, i) => [...row, ...B2[i]]);
95527
+ for (let col = 0; col < n; col++) {
95528
+ let piv = col;
95529
+ for (let r = col + 1; r < n; r++) if (Math.abs(M[r][col]) > Math.abs(M[piv][col])) piv = r;
95530
+ if (Math.abs(M[piv][col]) < 1e-14) throw new Error(`Singular interpolation matrix at column ${col}.`);
95531
+ [M[col], M[piv]] = [M[piv], M[col]];
95532
+ const inv = 1 / M[col][col];
95533
+ for (let j = col; j < n + k2; j++) M[col][j] *= inv;
95534
+ for (let r = 0; r < n; r++) {
95535
+ if (r === col) continue;
95536
+ const f3 = M[r][col];
95537
+ if (f3 === 0) continue;
95538
+ for (let j = col; j < n + k2; j++) M[r][j] -= f3 * M[col][j];
95539
+ }
95540
+ }
95541
+ return Array.from({ length: n }, (_2, i) => M[i].slice(n));
95542
+ }
95543
+ function globalCurveInterp(points, degree, paramsIn) {
95544
+ const last = points.length - 1;
95545
+ if (last < degree) throw new Error(`Curve interpolation needs at least ${degree + 1} points, got ${points.length}.`);
95546
+ const params = paramsIn ? paramsIn.slice() : computeParamsCentripetal(points, 0.5);
95547
+ const knots = knotsFromParamsAveraging(params, degree);
95548
+ const count = last + 1;
95549
+ const N = Array.from({ length: count }, () => new Array(count).fill(0));
95550
+ for (let i = 0; i <= last; i++) {
95551
+ const span = findSpan(count, degree, params[i], knots);
95552
+ const basis = basisFuns(span, params[i], degree, knots);
95553
+ for (let j = 0; j <= degree; j++) N[i][span - degree + j] = basis[j];
95554
+ }
95555
+ const Q = points.map((p2) => [p2[0], p2[1], p2[2]]);
95556
+ const cps = solveLinear(N, Q);
95557
+ return { cps, knots, degree };
95558
+ }
95559
+ function insertKnotCurve(cps, knots, degree, u2) {
95560
+ const n = cps.length;
95561
+ const span = findSpan(n, degree, u2, knots);
95562
+ const mult = knotMultiplicity(knots, u2);
95563
+ if (mult >= degree) return { cps: cps.slice(), knots: knots.slice() };
95564
+ const newCps = new Array(n + 1);
95565
+ const newKnots = new Array(knots.length + 1);
95566
+ for (let i = 0; i <= span; i++) newKnots[i] = knots[i];
95567
+ newKnots[span + 1] = u2;
95568
+ for (let i = span + 1; i < knots.length; i++) newKnots[i + 1] = knots[i];
95569
+ for (let i = 0; i <= span - degree; i++) newCps[i] = [...cps[i]];
95570
+ for (let i = span - mult; i < n; i++) newCps[i + 1] = [...cps[i]];
95571
+ for (let i = span - degree + 1; i <= span - mult; i++) {
95572
+ const denom = knots[i + degree] - knots[i];
95573
+ const alpha = denom === 0 ? 0 : (u2 - knots[i]) / denom;
95574
+ const a2 = cps[i - 1];
95575
+ const b = cps[i];
95576
+ newCps[i] = [(1 - alpha) * a2[0] + alpha * b[0], (1 - alpha) * a2[1] + alpha * b[1], (1 - alpha) * a2[2] + alpha * b[2]];
95577
+ }
95578
+ return { cps: newCps, knots: newKnots };
95579
+ }
95580
+ function interiorKnotBuckets(kv, mergeEps = KNOT_MERGE_EPS) {
95581
+ const buckets = [];
95582
+ for (const k2 of kv) {
95583
+ if (k2 <= KNOT_EPS || k2 >= 1 - KNOT_EPS) continue;
95584
+ const b = buckets.find((x2) => Math.abs(x2.value - k2) <= mergeEps);
95585
+ if (b) b.mult++;
95586
+ else buckets.push({ value: k2, mult: 1 });
95587
+ }
95588
+ return buckets;
95589
+ }
95590
+ function mergeKnotVectors(knotVectors, degree, mergeEps = KNOT_MERGE_EPS) {
95591
+ const merged = [];
95592
+ for (const kv of knotVectors) {
95593
+ for (const { value, mult } of interiorKnotBuckets(kv, mergeEps)) {
95594
+ const b = merged.find((x2) => Math.abs(x2.value - value) <= mergeEps);
95595
+ if (b) b.mult = Math.max(b.mult, mult);
95596
+ else merged.push({ value, mult });
95597
+ }
95598
+ }
95599
+ merged.sort((a2, b) => a2.value - b.value);
95600
+ const out = [];
95601
+ for (let i = 0; i <= degree; i++) out.push(0);
95602
+ for (const { value, mult } of merged) for (let i = 0; i < mult; i++) out.push(value);
95603
+ for (let i = 0; i <= degree; i++) out.push(1);
95604
+ return out;
95605
+ }
95606
+ function refineKnotsToTarget(cps, knots, degree, targetKnots, mergeEps = KNOT_MERGE_EPS) {
95607
+ let curCps = cps.map((p2) => [...p2]);
95608
+ let curKnots = knots.slice();
95609
+ for (const { value, mult } of interiorKnotBuckets(targetKnots, mergeEps)) {
95610
+ const curMult = () => curKnots.reduce((c2, k2) => c2 + (Math.abs(k2 - value) <= mergeEps ? 1 : 0), 0);
95611
+ let guard = 0;
95612
+ while (curMult() < mult) {
95613
+ const r = insertKnotCurve(curCps, curKnots, degree, value);
95614
+ if (r.cps.length === curCps.length) break;
95615
+ curCps = r.cps;
95616
+ curKnots = r.knots;
95617
+ if (++guard > degree + 2) break;
95618
+ }
95619
+ }
95620
+ return { cps: curCps, knots: curKnots };
95621
+ }
95622
+ function binomial(a2, b) {
95623
+ if (b < 0 || b > a2) return 0;
95624
+ let r = 1;
95625
+ for (let i = 0; i < b; i++) r = r * (a2 - i) / (i + 1);
95626
+ return r;
95627
+ }
95628
+ function bezierElevate(bez, p2, t) {
95629
+ const m2 = p2 + t;
95630
+ const out = [];
95631
+ for (let i = 0; i <= m2; i++) {
95632
+ const acc = [0, 0, 0];
95633
+ const lo = Math.max(0, i - t);
95634
+ const hi = Math.min(p2, i);
95635
+ for (let j = lo; j <= hi; j++) {
95636
+ const w2 = binomial(p2, j) * binomial(t, i - j) / binomial(m2, i);
95637
+ acc[0] += w2 * bez[j][0];
95638
+ acc[1] += w2 * bez[j][1];
95639
+ acc[2] += w2 * bez[j][2];
95640
+ }
95641
+ out.push(acc);
95642
+ }
95643
+ return out;
95644
+ }
95645
+ function elevateCurveDegree(cps, knots, degree, targetDegree) {
95646
+ if (targetDegree === degree) return { cps: cps.map((p2) => [...p2]), knots: knots.slice(), degree };
95647
+ if (targetDegree < degree) throw new Error("Degree reduction is not supported.");
95648
+ const t = targetDegree - degree;
95649
+ let curCps = cps.map((p2) => [...p2]);
95650
+ let curKnots = knots.slice();
95651
+ const breaks = interiorKnotBuckets(curKnots).map((b) => b.value);
95652
+ for (const v of breaks) {
95653
+ let guard = 0;
95654
+ while (knotMultiplicity(curKnots, v) < degree) {
95655
+ const r = insertKnotCurve(curCps, curKnots, degree, v);
95656
+ curCps = r.cps;
95657
+ curKnots = r.knots;
95658
+ if (++guard > degree + 2) break;
95659
+ }
95660
+ }
95661
+ const segVals = [0, ...breaks.slice().sort((a2, b) => a2 - b), 1];
95662
+ const nSeg = segVals.length - 1;
95663
+ const newCps = [];
95664
+ for (let s = 0; s < nSeg; s++) {
95665
+ const bez = curCps.slice(s * degree, s * degree + degree + 1);
95666
+ const elevated = bezierElevate(bez, degree, t);
95667
+ if (s === 0) newCps.push(...elevated);
95668
+ else newCps.push(...elevated.slice(1));
95669
+ }
95670
+ const newKnots = [];
95671
+ for (let i = 0; i <= targetDegree; i++) newKnots.push(0);
95672
+ for (let s = 1; s < nSeg; s++) for (let i = 0; i < targetDegree; i++) newKnots.push(segVals[s]);
95673
+ for (let i = 0; i <= targetDegree; i++) newKnots.push(1);
95674
+ if (newKnots.length !== newCps.length + targetDegree + 1) {
95675
+ throw new Error(
95676
+ `Degree elevation produced inconsistent knots (${newKnots.length}) for ${newCps.length} control points at degree ${targetDegree}.`
95677
+ );
95678
+ }
95679
+ return { cps: newCps, knots: newKnots, degree: targetDegree };
95680
+ }
95681
+ function unifyCurves(curves) {
95682
+ const targetDeg = Math.max(...curves.map((c2) => c2.degree));
95683
+ const elevated = curves.map((c2) => elevateCurveDegree(c2.cps, c2.knots, c2.degree, targetDeg));
95684
+ const common = mergeKnotVectors(
95685
+ elevated.map((c2) => c2.knots),
95686
+ targetDeg
95687
+ );
95688
+ const refined = elevated.map((c2) => refineKnotsToTarget(c2.cps, c2.knots, targetDeg, common));
95689
+ const ncp = common.length - targetDeg - 1;
95690
+ for (const r of refined) {
95691
+ if (r.cps.length !== ncp) throw new Error(`Curve unification produced ${r.cps.length} control points, expected ${ncp}.`);
95692
+ }
95693
+ return { degree: targetDeg, knots: common, curves: refined.map((r) => r.cps) };
95694
+ }
95695
+ function transpose(g2) {
95696
+ const out = [];
95697
+ for (let j = 0; j < g2[0].length; j++) {
95698
+ const row = [];
95699
+ for (let i = 0; i < g2.length; i++) row.push(g2[i][j]);
95700
+ out.push(row);
95701
+ }
95702
+ return out;
95703
+ }
95704
+ function skin(curveCps, spanParams, spanDegree) {
95705
+ const ncp = curveCps[0].length;
95706
+ const cols = [];
95707
+ for (let j = 0; j < ncp; j++) {
95708
+ const pts = curveCps.map((row) => row[j]);
95709
+ cols.push(globalCurveInterp(pts, spanDegree, spanParams).cps);
95710
+ }
95711
+ const spanCp = cols[0].length;
95712
+ const grid = [];
95713
+ for (let s = 0; s < spanCp; s++) {
95714
+ const row = [];
95715
+ for (let j = 0; j < ncp; j++) row.push(cols[j][s]);
95716
+ grid.push(row);
95717
+ }
95718
+ const spanKnots = knotsFromParamsAveraging(spanParams, spanDegree);
95719
+ return { grid, spanKnots };
95720
+ }
95721
+ function tensorInterp(gridPts, uParams, vParams, degreeU, degreeV) {
95722
+ const rowInterp = gridPts.map((rowPts) => globalCurveInterp(rowPts, degreeV, vParams));
95723
+ const knotsV = rowInterp[0].knots;
95724
+ const ncpV = rowInterp[0].cps.length;
95725
+ const cols = [];
95726
+ for (let j = 0; j < ncpV; j++) {
95727
+ const pts = rowInterp.map((ri) => ri.cps[j]);
95728
+ cols.push(globalCurveInterp(pts, degreeU, uParams));
95729
+ }
95730
+ const knotsU = cols[0].knots;
95731
+ const cpU = cols[0].cps.length;
95732
+ const grid = [];
95733
+ for (let i = 0; i < cpU; i++) {
95734
+ const row = [];
95735
+ for (let j = 0; j < ncpV; j++) row.push(cols[j].cps[i]);
95736
+ grid.push(row);
95737
+ }
95738
+ return { grid, knotsU, knotsV };
95739
+ }
95740
+ function refineSurface(surf, targetKnotsU, targetKnotsV) {
95741
+ const { grid, knotsU, knotsV, degreeU, degreeV } = surf;
95742
+ const rows = grid.map((row) => refineKnotsToTarget(row, knotsV, degreeV, targetKnotsV).cps);
95743
+ const ncpV = rows[0].length;
95744
+ const cols = [];
95745
+ for (let j = 0; j < ncpV; j++) {
95746
+ const colPts = rows.map((r) => r[j]);
95747
+ cols.push(refineKnotsToTarget(colPts, knotsU, degreeU, targetKnotsU).cps);
95748
+ }
95749
+ const cpU = cols[0].length;
95750
+ const out = [];
95751
+ for (let i = 0; i < cpU; i++) {
95752
+ const row = [];
95753
+ for (let j = 0; j < ncpV; j++) row.push(cols[j][i]);
95754
+ out.push(row);
95755
+ }
95756
+ return { grid: out, knotsU: targetKnotsU, knotsV: targetKnotsV, degreeU, degreeV };
95757
+ }
95758
+ function averageParams(polylines, alpha = 0.5) {
95759
+ const count = polylines[0].length;
95760
+ const acc = new Array(count).fill(0);
95761
+ for (const pl of polylines) {
95762
+ const p2 = computeParamsCentripetal(pl, alpha);
95763
+ for (let i = 0; i < count; i++) acc[i] += p2[i];
95764
+ }
95765
+ return acc.map((s) => s / polylines.length);
95766
+ }
95767
+ function gordonGridParams(gridPts) {
95768
+ const rows = gridPts;
95769
+ const cols = gridPts[0].map((_2, l) => gridPts.map((r) => r[l]));
95770
+ return { vParams: averageParams(rows, 0.5), uParams: averageParams(cols, 0.5) };
95771
+ }
95772
+ function buildGordonFromGrid(gridPts, degreeU = 3, degreeV = 3) {
95773
+ const m2 = gridPts.length - 1;
95774
+ const n = gridPts[0].length - 1;
95775
+ if (m2 < 1 || n < 1) throw new Error("A Gordon surface needs at least a 2x2 curve network.");
95776
+ if (m2 < degreeU) degreeU = m2;
95777
+ if (n < degreeV) degreeV = n;
95778
+ const rows = gridPts;
95779
+ const cols = gridPts[0].map((_2, l) => gridPts.map((r) => r[l]));
95780
+ const { uParams, vParams } = gordonGridParams(gridPts);
95781
+ const uCurves = rows.map((rowPts) => globalCurveInterp(rowPts, degreeV, vParams));
95782
+ const vCurves = cols.map((colPts) => globalCurveInterp(colPts, degreeU, uParams));
95783
+ const uFam = unifyCurves(uCurves);
95784
+ const vFam = unifyCurves(vCurves);
95785
+ const Lu = skin(uFam.curves, uParams, degreeU);
95786
+ const LvRaw = skin(vFam.curves, vParams, degreeV);
95787
+ const Lv = { grid: transpose(LvRaw.grid), knotsU: vFam.knots, knotsV: LvRaw.spanKnots };
95788
+ const T = tensorInterp(gridPts, uParams, vParams, degreeU, degreeV);
95789
+ const knotsU = mergeKnotVectors([Lu.spanKnots, Lv.knotsU, T.knotsU], degreeU);
95790
+ const knotsV = mergeKnotVectors([uFam.knots, Lv.knotsV, T.knotsV], degreeV);
95791
+ const LuS = refineSurface({ grid: Lu.grid, knotsU: Lu.spanKnots, knotsV: uFam.knots, degreeU, degreeV }, knotsU, knotsV);
95792
+ const LvS = refineSurface({ grid: Lv.grid, knotsU: Lv.knotsU, knotsV: Lv.knotsV, degreeU, degreeV }, knotsU, knotsV);
95793
+ const TS = refineSurface({ grid: T.grid, knotsU: T.knotsU, knotsV: T.knotsV, degreeU, degreeV }, knotsU, knotsV);
95794
+ const nuc = LuS.grid.length;
95795
+ const nvc = LuS.grid[0].length;
95796
+ if (LvS.grid.length !== nuc || TS.grid.length !== nuc || LvS.grid[0].length !== nvc || TS.grid[0].length !== nvc) {
95797
+ throw new Error(
95798
+ `Gordon blend grid mismatch: Lu ${nuc}x${nvc}, Lv ${LvS.grid.length}x${LvS.grid[0].length}, T ${TS.grid.length}x${TS.grid[0].length}.`
95799
+ );
95800
+ }
95801
+ const grid = [];
95802
+ for (let i = 0; i < nuc; i++) {
95803
+ const row = [];
95804
+ for (let j = 0; j < nvc; j++) {
95805
+ const a2 = LuS.grid[i][j];
95806
+ const b = LvS.grid[i][j];
95807
+ const c2 = TS.grid[i][j];
95808
+ row.push([a2[0] + b[0] - c2[0], a2[1] + b[1] - c2[1], a2[2] + b[2] - c2[2]]);
95809
+ }
95810
+ grid.push(row);
95811
+ }
95812
+ return { grid, knotsU, knotsV, degreeU, degreeV };
95813
+ }
95814
+ function evalSurface(surf, u2, v) {
95815
+ const { grid, knotsU, knotsV, degreeU, degreeV } = surf;
95816
+ const nu = grid.length;
95817
+ const nv = grid[0].length;
95818
+ const su = findSpan(nu, degreeU, u2, knotsU);
95819
+ const sv = findSpan(nv, degreeV, v, knotsV);
95820
+ const Nu = basisFuns(su, u2, degreeU, knotsU);
95821
+ const Nv = basisFuns(sv, v, degreeV, knotsV);
95822
+ const out = [0, 0, 0];
95823
+ for (let i = 0; i <= degreeU; i++) {
95824
+ const ui = su - degreeU + i;
95825
+ for (let j = 0; j <= degreeV; j++) {
95826
+ const vj = sv - degreeV + j;
95827
+ const w2 = Nu[i] * Nv[j];
95828
+ const p2 = grid[ui][vj];
95829
+ out[0] += w2 * p2[0];
95830
+ out[1] += w2 * p2[1];
95831
+ out[2] += w2 * p2[2];
95832
+ }
95833
+ }
95834
+ return out;
95835
+ }
95836
+ function evalSurfaceJet(surf, u2, v) {
95837
+ const { grid, knotsU, knotsV, degreeU, degreeV } = surf;
95838
+ const nu = grid.length;
95839
+ const nv = grid[0].length;
95840
+ const su = findSpan(nu, degreeU, u2, knotsU);
95841
+ const sv = findSpan(nv, degreeV, v, knotsV);
95842
+ const dU = basisFunsDeriv(su, u2, degreeU, knotsU, 2);
95843
+ const dV = basisFunsDeriv(sv, v, degreeV, knotsV, 2);
95844
+ const S = [0, 0, 0];
95845
+ const Su = [0, 0, 0];
95846
+ const Sv = [0, 0, 0];
95847
+ const Suu = [0, 0, 0];
95848
+ const Suv = [0, 0, 0];
95849
+ const Svv = [0, 0, 0];
95850
+ for (let i = 0; i <= degreeU; i++) {
95851
+ const ui = su - degreeU + i;
95852
+ for (let j = 0; j <= degreeV; j++) {
95853
+ const vj = sv - degreeV + j;
95854
+ const p2 = grid[ui][vj];
95855
+ const w00 = dU[0][i] * dV[0][j];
95856
+ const w10 = dU[1][i] * dV[0][j];
95857
+ const w01 = dU[0][i] * dV[1][j];
95858
+ const w20 = dU[2][i] * dV[0][j];
95859
+ const w11 = dU[1][i] * dV[1][j];
95860
+ const w02 = dU[0][i] * dV[2][j];
95861
+ for (let c2 = 0; c2 < 3; c2++) {
95862
+ S[c2] += w00 * p2[c2];
95863
+ Su[c2] += w10 * p2[c2];
95864
+ Sv[c2] += w01 * p2[c2];
95865
+ Suu[c2] += w20 * p2[c2];
95866
+ Suv[c2] += w11 * p2[c2];
95867
+ Svv[c2] += w02 * p2[c2];
95868
+ }
95869
+ }
95870
+ }
95871
+ let nx = Su[1] * Sv[2] - Su[2] * Sv[1];
95872
+ let ny = Su[2] * Sv[0] - Su[0] * Sv[2];
95873
+ let nz = Su[0] * Sv[1] - Su[1] * Sv[0];
95874
+ const len2 = Math.hypot(nx, ny, nz) || 1;
95875
+ nx /= len2;
95876
+ ny /= len2;
95877
+ nz /= len2;
95878
+ return { S, Su, Sv, Suu, Suv, Svv, normal: [nx, ny, nz] };
95879
+ }
95880
+ function surfaceCurvature(jet) {
95881
+ const { Su, Sv, Suu, Suv, Svv, normal: normal2 } = jet;
95882
+ const E = Su[0] * Su[0] + Su[1] * Su[1] + Su[2] * Su[2];
95883
+ const F = Su[0] * Sv[0] + Su[1] * Sv[1] + Su[2] * Sv[2];
95884
+ const G = Sv[0] * Sv[0] + Sv[1] * Sv[1] + Sv[2] * Sv[2];
95885
+ const e = Suu[0] * normal2[0] + Suu[1] * normal2[1] + Suu[2] * normal2[2];
95886
+ const f3 = Suv[0] * normal2[0] + Suv[1] * normal2[1] + Suv[2] * normal2[2];
95887
+ const g2 = Svv[0] * normal2[0] + Svv[1] * normal2[1] + Svv[2] * normal2[2];
95888
+ const denom = E * G - F * F;
95889
+ if (Math.abs(denom) < 1e-20) return { k1: 0, k2: 0, K: 0, H: 0 };
95890
+ const K = (e * g2 - f3 * f3) / denom;
95891
+ const H = (e * G - 2 * f3 * F + g2 * E) / (2 * denom);
95892
+ const disc = Math.max(0, H * H - K);
95893
+ const root = Math.sqrt(disc);
95894
+ return { k1: H + root, k2: H - root, K, H };
95895
+ }
95896
+ function isVec3(v) {
95897
+ return Array.isArray(v) && v.length === 3 && typeof v[0] === "number" && typeof v[1] === "number" && typeof v[2] === "number";
95898
+ }
95899
+ function requireFiniteVec(p2, label) {
95900
+ for (let i = 0; i < 3; i++)
95901
+ if (!Number.isFinite(p2[i])) throw new Error(`Surface.Net: ${label} component ${i} must be finite, got ${p2[i]}`);
95902
+ return [p2[0], p2[1], p2[2]];
95903
+ }
95904
+ function toSampler(input, label) {
95905
+ if (input instanceof NurbsCurve3D) {
95906
+ return (t) => input.pointAt(t);
95907
+ }
95908
+ if (Array.isArray(input) && input.length > 0 && isVec3(input[0])) {
95909
+ const pts = input.map((p2, i) => requireFiniteVec(p2, `${label}[${i}]`));
95910
+ if (pts.length < 2) throw new Error(`Surface.Net: ${label} needs at least 2 points.`);
95911
+ if (pts.length === 2) {
95912
+ const [a2, b] = pts;
95913
+ return (t) => [a2[0] + (b[0] - a2[0]) * t, a2[1] + (b[1] - a2[1]) * t, a2[2] + (b[2] - a2[2]) * t];
95914
+ }
95915
+ const curve = globalCurveInterp(pts, Math.min(3, pts.length - 1));
95916
+ const nc = new NurbsCurve3D(curve.cps, { degree: curve.degree, knots: curve.knots });
95917
+ return (t) => nc.pointAt(t);
95918
+ }
95919
+ throw new Error(`Surface.Net: ${label} must be a Curve.Fit/Curve.Nurbs value or an array of [x,y,z] points.`);
95920
+ }
95921
+ function sampleCurve(sampler, count) {
95922
+ const out = [];
95923
+ for (let i = 0; i < count; i++) out.push(sampler(i / (count - 1)));
95924
+ return out;
95925
+ }
95926
+ const DEFAULT_THICKEN_RESOLUTION = 48;
95927
+ class Sheet {
95928
+ constructor(surface) {
95929
+ this.surface = surface;
95930
+ }
95931
+ /** Edge naming follows parameter direction (documented): front=v0, rear=v1, left=u0, right=u1. */
95932
+ get frontEdge() {
95933
+ return { sheet: this, fixed: "v", value: 0 };
95934
+ }
95935
+ get rearEdge() {
95936
+ return { sheet: this, fixed: "v", value: 1 };
95937
+ }
95938
+ get leftEdge() {
95939
+ return { sheet: this, fixed: "u", value: 0 };
95940
+ }
95941
+ get rightEdge() {
95942
+ return { sheet: this, fixed: "u", value: 1 };
95943
+ }
95944
+ pointAt(u2, v) {
95945
+ return evalSurface(this.surface, clamp01(u2), clamp01(v));
95946
+ }
95947
+ normalAt(u2, v) {
95948
+ return evalSurfaceJet(this.surface, clamp01(u2), clamp01(v)).normal;
95949
+ }
95950
+ curvatureAt(u2, v) {
95951
+ return surfaceCurvature(evalSurfaceJet(this.surface, clamp01(u2), clamp01(v)));
95952
+ }
95953
+ /** Largest principal curvature magnitude over a sampling grid (for offset safety). */
95954
+ maxAbsPrincipalCurvature(samples = 9) {
95955
+ let maxK = 0;
95956
+ for (let i = 0; i <= samples; i++) {
95957
+ for (let j = 0; j <= samples; j++) {
95958
+ const c2 = this.curvatureAt(i / samples, j / samples);
95959
+ maxK = Math.max(maxK, Math.abs(c2.k1), Math.abs(c2.k2));
95960
+ }
95961
+ }
95962
+ return maxK;
95963
+ }
95964
+ /**
95965
+ * Offset the sheet along its analytic normals into a watertight solid shell of
95966
+ * the given wall thickness. Throws if the wall would self-intersect on a
95967
+ * concave region (no silent degenerate solid).
95968
+ */
95969
+ thicken(wall, options = {}) {
95970
+ if (!Number.isFinite(wall) || wall <= 0) throw new Error(`Sheet.thicken: wall must be a positive finite number, got ${wall}`);
95971
+ const maxK = this.maxAbsPrincipalCurvature();
95972
+ if (wall * maxK >= 1) {
95973
+ throw new Error(
95974
+ `Sheet.thicken: wall ${wall} exceeds the minimum radius of curvature (1/${maxK.toFixed(4)} = ${(1 / maxK).toFixed(3)}). The offset surface would self-intersect. Reduce the wall thickness or relax the curvature.`
95975
+ );
95976
+ }
95977
+ const resolution = options.resolution ?? DEFAULT_THICKEN_RESOLUTION;
95978
+ return nurbsSurface(this.surface.grid, {
95979
+ degreeU: this.surface.degreeU,
95980
+ degreeV: this.surface.degreeV,
95981
+ knotsU: this.surface.knotsU,
95982
+ knotsV: this.surface.knotsV,
95983
+ thickness: wall,
95984
+ resolution
95985
+ });
95986
+ }
95987
+ /** Per-edge continuity match against a neighbor (returns a NEW Sheet). */
95988
+ matchEdge(edge) {
95989
+ if (edge.sheet !== this) throw new Error("Sheet.matchEdge: the edge must belong to this sheet.");
95990
+ return new MatchEdgeBuilder(this, edge);
95991
+ }
95992
+ }
95993
+ function clamp01(t) {
95994
+ return t < 0 ? 0 : t > 1 ? 1 : t;
95995
+ }
95996
+ class CurveNetBuilder {
95997
+ constructor() {
95998
+ __publicField(this, "lengthwiseCurves", []);
95999
+ __publicField(this, "crosswiseCurves", []);
96000
+ __publicField(this, "railCurves", []);
96001
+ __publicField(this, "cageGrid", null);
96002
+ __publicField(this, "degU");
96003
+ __publicField(this, "degV");
96004
+ __publicField(this, "built", null);
96005
+ }
96006
+ lengthwise(...curves) {
96007
+ this.lengthwiseCurves = curves;
96008
+ this.built = null;
96009
+ return this;
96010
+ }
96011
+ crosswise(...curves) {
96012
+ this.crosswiseCurves = curves;
96013
+ this.built = null;
96014
+ return this;
96015
+ }
96016
+ alongRails(railA, railB) {
96017
+ this.railCurves = [railA, railB];
96018
+ this.built = null;
96019
+ return this;
96020
+ }
96021
+ sections(...curves) {
96022
+ this.crosswiseCurves = curves;
96023
+ this.built = null;
96024
+ return this;
96025
+ }
96026
+ cage(grid) {
96027
+ if (!Array.isArray(grid) || grid.length < 2 || !Array.isArray(grid[0]) || grid[0].length < 2) {
96028
+ throw new Error("Surface.Net().cage: grid must be at least a 2x2 array of [x,y,z] points.");
96029
+ }
96030
+ const cols = grid[0].length;
96031
+ this.cageGrid = grid.map((row, k2) => {
96032
+ if (row.length !== cols) throw new Error(`Surface.Net().cage: row ${k2} has ${row.length} points, expected ${cols}.`);
96033
+ return row.map((p2, l) => requireFiniteVec(p2, `cage[${k2}][${l}]`));
96034
+ });
96035
+ this.built = null;
96036
+ return this;
96037
+ }
96038
+ degree(u2, v) {
96039
+ if (!Number.isInteger(u2) || u2 < 1) throw new Error(`Surface.Net().degree: degree must be a positive integer, got ${u2}`);
96040
+ this.degU = u2;
96041
+ this.degV = v === void 0 ? u2 : v;
96042
+ if (v !== void 0 && (!Number.isInteger(v) || v < 1))
96043
+ throw new Error(`Surface.Net().degree: degree must be a positive integer, got ${v}`);
96044
+ this.built = null;
96045
+ return this;
96046
+ }
96047
+ /** Build (once) and return the Sheet. */
96048
+ toSheet() {
96049
+ if (this.built) return this.built;
96050
+ const grid = this.buildGrid();
96051
+ const m2 = grid.length - 1;
96052
+ const n = grid[0].length - 1;
96053
+ const degreeU = Math.max(1, Math.min(this.degU ?? 3, m2));
96054
+ const degreeV = Math.max(1, Math.min(this.degV ?? 3, n));
96055
+ const surface = buildGordonFromGrid(grid, degreeU, degreeV);
96056
+ this.built = new Sheet(surface);
96057
+ return this.built;
96058
+ }
96059
+ buildGrid() {
96060
+ if (this.cageGrid) return this.cageGrid;
96061
+ const lengthwise = this.railCurves.length > 0 ? this.railCurves : this.lengthwiseCurves;
96062
+ const crosswise = this.crosswiseCurves;
96063
+ const SAMPLES = 17;
96064
+ if (lengthwise.length >= 2) {
96065
+ const samplers = lengthwise.map((c2, l) => toSampler(c2, `lengthwise[${l}]`));
96066
+ const grid = [];
96067
+ for (let i = 0; i < SAMPLES; i++) {
96068
+ const u2 = i / (SAMPLES - 1);
96069
+ grid.push(samplers.map((s) => s(u2)));
96070
+ }
96071
+ return grid;
96072
+ }
96073
+ if (crosswise.length >= 2) {
96074
+ const samplers = crosswise.map((c2, k2) => toSampler(c2, `crosswise[${k2}]`));
96075
+ const grid = [];
96076
+ for (const s of samplers) grid.push(sampleCurve(s, SAMPLES));
96077
+ return grid;
96078
+ }
96079
+ throw new Error(
96080
+ "Surface.Net: provide a .cage(grid), or a family of at least 2 curves via .lengthwise(...) / .crosswise(...) / .alongRails(a,b).sections(...). Intersecting two independent hand-drawn families is not available yet — supply the denser family or a cage."
96081
+ );
96082
+ }
96083
+ // ── Sheet delegation (build on first access) ──────────────────────────────
96084
+ get frontEdge() {
96085
+ return this.toSheet().frontEdge;
96086
+ }
96087
+ get rearEdge() {
96088
+ return this.toSheet().rearEdge;
96089
+ }
96090
+ get leftEdge() {
96091
+ return this.toSheet().leftEdge;
96092
+ }
96093
+ get rightEdge() {
96094
+ return this.toSheet().rightEdge;
96095
+ }
96096
+ get surface() {
96097
+ return this.toSheet().surface;
96098
+ }
96099
+ pointAt(u2, v) {
96100
+ return this.toSheet().pointAt(u2, v);
96101
+ }
96102
+ normalAt(u2, v) {
96103
+ return this.toSheet().normalAt(u2, v);
96104
+ }
96105
+ curvatureAt(u2, v) {
96106
+ return this.toSheet().curvatureAt(u2, v);
96107
+ }
96108
+ thicken(wall, options) {
96109
+ return this.toSheet().thicken(wall, options);
96110
+ }
96111
+ matchEdge(edge) {
96112
+ return this.toSheet().matchEdge(edge);
96113
+ }
96114
+ }
96115
+ function createCurveNet() {
96116
+ return new CurveNetBuilder();
96117
+ }
96118
+ class MatchEdgeBuilder {
96119
+ constructor(sheet, edge) {
96120
+ this.sheet = sheet;
96121
+ this.edge = edge;
96122
+ }
96123
+ toG0(neighbor) {
96124
+ return applyEdgeMatch(this.sheet, this.edge, neighbor, 0);
96125
+ }
96126
+ toG1(neighbor) {
96127
+ return applyEdgeMatch(this.sheet, this.edge, neighbor, 1);
96128
+ }
96129
+ toG2(neighbor) {
96130
+ return applyEdgeMatch(this.sheet, this.edge, neighbor, 2);
96131
+ }
96132
+ }
96133
+ function boundaryRows(surf, fixed, value, depth) {
96134
+ const rows = [];
96135
+ if (fixed === "u") {
96136
+ const nU = surf.grid.length;
96137
+ for (let d2 = 0; d2 <= depth; d2++) {
96138
+ const i = value === 0 ? d2 : nU - 1 - d2;
96139
+ rows.push(surf.grid[i].map((p2) => [p2[0], p2[1], p2[2]]));
96140
+ }
96141
+ } else {
96142
+ const nV = surf.grid[0].length;
96143
+ for (let d2 = 0; d2 <= depth; d2++) {
96144
+ const j = value === 0 ? d2 : nV - 1 - d2;
96145
+ rows.push(surf.grid.map((p2) => [p2[j][0], p2[j][1], p2[j][2]]));
96146
+ }
96147
+ }
96148
+ return rows;
96149
+ }
96150
+ function setBoundaryRow(surf, fixed, value, depth, row) {
96151
+ if (fixed === "u") {
96152
+ const nU = surf.grid.length;
96153
+ const i = value === 0 ? depth : nU - 1 - depth;
96154
+ for (let l = 0; l < surf.grid[i].length; l++) surf.grid[i][l] = [row[l][0], row[l][1], row[l][2]];
96155
+ } else {
96156
+ const nV = surf.grid[0].length;
96157
+ const j = value === 0 ? depth : nV - 1 - depth;
96158
+ for (let k2 = 0; k2 < surf.grid.length; k2++) surf.grid[k2][j] = [row[k2][0], row[k2][1], row[k2][2]];
96159
+ }
96160
+ }
96161
+ function cloneSurface(surf) {
96162
+ return {
96163
+ grid: surf.grid.map((row) => row.map((p2) => [p2[0], p2[1], p2[2]])),
96164
+ knotsU: [...surf.knotsU],
96165
+ knotsV: [...surf.knotsV],
96166
+ degreeU: surf.degreeU,
96167
+ degreeV: surf.degreeV
96168
+ };
96169
+ }
96170
+ function applyEdgeMatch(sheet, edge, neighbor, order) {
96171
+ const result = cloneSurface(sheet.surface);
96172
+ const my = boundaryRows(result, edge.fixed, edge.value, order);
96173
+ const their = boundaryRows(neighbor.sheet.surface, neighbor.fixed, neighbor.value, order);
96174
+ if (my[0].length !== their[0].length) {
96175
+ throw new Error(
96176
+ `Sheet.matchEdge: edge control-point counts differ (${my[0].length} vs ${their[0].length}). Both sheets must share section sampling along the matched edge for continuity.`
96177
+ );
96178
+ }
96179
+ const len2 = my[0].length;
96180
+ const b0 = their[0].map((p2) => [p2[0], p2[1], p2[2]]);
96181
+ setBoundaryRow(result, edge.fixed, edge.value, 0, b0);
96182
+ if (order === 0) return new Sheet(result);
96183
+ const b1 = [];
96184
+ for (let i = 0; i < len2; i++) {
96185
+ const p0 = their[0][i];
96186
+ const p1 = their[1][i];
96187
+ b1.push([2 * p0[0] - p1[0], 2 * p0[1] - p1[1], 2 * p0[2] - p1[2]]);
96188
+ }
96189
+ setBoundaryRow(result, edge.fixed, edge.value, 1, b1);
96190
+ if (order === 1) return new Sheet(result);
96191
+ const b22 = [];
96192
+ for (let i = 0; i < len2; i++) {
96193
+ const p0 = their[0][i];
96194
+ const p1 = their[1][i];
96195
+ const p2 = their[2][i];
96196
+ b22.push([3 * p0[0] - 3 * p1[0] + p2[0], 3 * p0[1] - 3 * p1[1] + p2[1], 3 * p0[2] - 3 * p1[2] + p2[2]]);
96197
+ }
96198
+ setBoundaryRow(result, edge.fixed, edge.value, 2, b22);
96199
+ return new Sheet(result);
96200
+ }
96201
+ function edgeJet(edge, t) {
96202
+ const surf = edge.sheet.surface;
96203
+ if (edge.fixed === "u") {
96204
+ const j2 = evalSurfaceJet(surf, edge.value, clamp01(t));
96205
+ return { point: j2.S, cross: j2.Su, cross2: j2.Suu };
96206
+ }
96207
+ const j = evalSurfaceJet(surf, clamp01(t), edge.value);
96208
+ return { point: j.S, cross: j.Sv, cross2: j.Svv };
96209
+ }
96210
+ function sub3(a2, b) {
96211
+ return [a2[0] - b[0], a2[1] - b[1], a2[2] - b[2]];
96212
+ }
96213
+ function len3(a2) {
96214
+ return Math.hypot(a2[0], a2[1], a2[2]);
96215
+ }
96216
+ function unit3(a2) {
96217
+ const l = len3(a2) || 1;
96218
+ return [a2[0] / l, a2[1] / l, a2[2] / l];
96219
+ }
96220
+ function edgeMatchReport(edgeA, edgeB, samples = 24) {
96221
+ let maxPositionGap = 0;
96222
+ let maxTangentAngleDeg = 0;
96223
+ let maxCurvatureRelErr = 0;
96224
+ for (let i = 0; i <= samples; i++) {
96225
+ const t = i / samples;
96226
+ const a2 = edgeJet(edgeA, t);
96227
+ const b = edgeJet(edgeB, t);
96228
+ maxPositionGap = Math.max(maxPositionGap, len3(sub3(a2.point, b.point)));
96229
+ const ua = unit3(a2.cross);
96230
+ const ub = unit3(b.cross);
96231
+ const dot2 = Math.max(-1, Math.min(1, ua[0] * ub[0] + ua[1] * ub[1] + ua[2] * ub[2]));
96232
+ const angle = Math.acos(Math.abs(dot2)) * 180 / Math.PI;
96233
+ maxTangentAngleDeg = Math.max(maxTangentAngleDeg, angle);
96234
+ const la = len3(a2.cross) ** 2 || 1;
96235
+ const lb = len3(b.cross) ** 2 || 1;
96236
+ const ka = (a2.cross2[0] * ua[0] + a2.cross2[1] * ua[1] + a2.cross2[2] * ua[2]) / la;
96237
+ const kb = (b.cross2[0] * ub[0] + b.cross2[1] * ub[1] + b.cross2[2] * ub[2]) / lb;
96238
+ maxCurvatureRelErr = Math.max(maxCurvatureRelErr, Math.abs(ka - kb) / (Math.abs(ka) + Math.abs(kb) + 1e-9));
96239
+ }
96240
+ return { maxPositionGap, maxTangentAngleDeg, maxCurvatureRelErr };
96241
+ }
96242
+ function bridgeBetween(edgeA, edgeB) {
96243
+ return new BridgeBuilder(edgeA, edgeB);
96244
+ }
96245
+ class BridgeBuilder {
96246
+ constructor(edgeA, edgeB) {
96247
+ __publicField(this, "bulgeA", 0.5);
96248
+ __publicField(this, "bulgeB", 0.5);
96249
+ this.edgeA = edgeA;
96250
+ this.edgeB = edgeB;
96251
+ }
96252
+ /** Tune the influence of each side (Rhino-style bulge). */
96253
+ bulge(a2, b) {
96254
+ if (!Number.isFinite(a2) || !Number.isFinite(b)) throw new Error("Surfaces.bridge.bulge: both factors must be finite.");
96255
+ this.bulgeA = a2;
96256
+ this.bulgeB = b;
96257
+ return this;
96258
+ }
96259
+ g0() {
96260
+ return this.build(0);
96261
+ }
96262
+ g1() {
96263
+ return this.build(1);
96264
+ }
96265
+ g2() {
96266
+ return this.build(2);
96267
+ }
96268
+ build(order) {
96269
+ const SAMPLES = 21;
96270
+ const CROSS = 11;
96271
+ const cage = [];
96272
+ for (let i = 0; i < SAMPLES; i++) {
96273
+ const t = i / (SAMPLES - 1);
96274
+ const a2 = edgeJet(this.edgeA, t);
96275
+ const b = edgeJet(this.edgeB, t);
96276
+ const chord = len3(sub3(b.point, a2.point));
96277
+ const toward = unit3(sub3(b.point, a2.point));
96278
+ const tA = orientToward(unit3(a2.cross), toward);
96279
+ const tB = orientToward(unit3(b.cross), [-toward[0], -toward[1], -toward[2]]);
96280
+ const poles = bridgePoles(a2.point, b.point, tA, tB, chord, this.bulgeA, this.bulgeB, order);
96281
+ const row = [];
96282
+ for (let c2 = 0; c2 < CROSS; c2++) row.push(deCasteljau(poles, c2 / (CROSS - 1)));
96283
+ cage.push(row);
96284
+ }
96285
+ return new CurveNetBuilder().cage(cage).degree(Math.min(3, SAMPLES - 1), Math.min(2 * order + 1 || 1, CROSS - 1)).toSheet();
96286
+ }
96287
+ }
96288
+ function orientToward(v, toward) {
96289
+ const dot2 = v[0] * toward[0] + v[1] * toward[1] + v[2] * toward[2];
96290
+ return dot2 < 0 ? [-v[0], -v[1], -v[2]] : v;
96291
+ }
96292
+ function bridgePoles(a2, b, tA, tB, chord, bulgeA, bulgeB, order) {
96293
+ if (order === 0) return [a2, b];
96294
+ const dA = chord * bulgeA / (order === 1 ? 3 : 5);
96295
+ const dB = chord * bulgeB / (order === 1 ? 3 : 5);
96296
+ const a1 = [a2[0] + tA[0] * dA, a2[1] + tA[1] * dA, a2[2] + tA[2] * dA];
96297
+ const b1 = [b[0] + tB[0] * dB, b[1] + tB[1] * dB, b[2] + tB[2] * dB];
96298
+ if (order === 1) return [a2, a1, b1, b];
96299
+ const a22 = [a1[0] + tA[0] * dA, a1[1] + tA[1] * dA, a1[2] + tA[2] * dA];
96300
+ const b22 = [b1[0] + tB[0] * dB, b1[1] + tB[1] * dB, b1[2] + tB[2] * dB];
96301
+ return [a2, a1, a22, b22, b1, b];
96302
+ }
96303
+ function deCasteljau(poles, s) {
96304
+ let pts = poles.map((p2) => [p2[0], p2[1], p2[2]]);
96305
+ while (pts.length > 1) {
96306
+ const next = [];
96307
+ for (let i = 0; i < pts.length - 1; i++) {
96308
+ next.push([
96309
+ pts[i][0] + (pts[i + 1][0] - pts[i][0]) * s,
96310
+ pts[i][1] + (pts[i + 1][1] - pts[i][1]) * s,
96311
+ pts[i][2] + (pts[i + 1][2] - pts[i][2]) * s
96312
+ ]);
96313
+ }
96314
+ pts = next;
96315
+ }
96316
+ return pts[0];
96317
+ }
95650
96318
  const CORNER_Y_ALPHA_ISSUE_URL = "https://github.com/KoStard/forgecad-private/issues/162";
95651
96319
  function isVec3Array(value) {
95652
96320
  return Array.isArray(value) && (value.length === 0 || Array.isArray(value[0]));
@@ -97038,7 +97706,16 @@ const Surface = {
97038
97706
  "Surface.MatchEdge",
97039
97707
  "Surface.Match()",
97040
97708
  (shape, options) => Surface.Match(shape, options)
97041
- )
97709
+ ),
97710
+ /**
97711
+ * Begin a curve-network (Gordon) surface — the class-A keystone. Chain
97712
+ * `.lengthwise(...)/.crosswise(...)` (or `.alongRails(a,b).sections(...)`, or
97713
+ * `.cage(grid)`), then `.thicken(wall)` to get a solid Shape. Returns a fluent
97714
+ * `Sheet` builder with analytic point/normal/curvature queries and named edges.
97715
+ */
97716
+ Net() {
97717
+ return createCurveNet();
97718
+ }
97042
97719
  };
97043
97720
  const Blend = {
97044
97721
  Edge(options) {
@@ -97058,6 +97735,14 @@ const Blend = {
97058
97735
  Surface(options) {
97059
97736
  return Surface.Fill(options);
97060
97737
  },
97738
+ /**
97739
+ * Build a transition strip between two `Surface.Net` sheet edges. Chain
97740
+ * `.bulge(a, b)` then `.g0()/.g1()/.g2()` for the continuity order. Returns a
97741
+ * `Sheet`; verify the seam with `Analysis.EdgeMatch`.
97742
+ */
97743
+ Bridge(edgeA, edgeB) {
97744
+ return bridgeBetween(edgeA, edgeB);
97745
+ },
97061
97746
  /**
97062
97747
  * @alpha
97063
97748
  * Current implementation uses continuity-controlled edge fillets on solid edges.
@@ -97092,6 +97777,14 @@ const Analysis = {
97092
97777
  SurfaceContinuity(shape, options = {}) {
97093
97778
  return evaluateEdgeContinuityReport(shape, options, "Analysis.SurfaceContinuity()");
97094
97779
  },
97780
+ /**
97781
+ * Measure G0/G1/G2 agreement between two `Surface.Net` sheet edges: worst
97782
+ * position gap, cross-boundary tangent angle (0 = G1), and normal-curvature
97783
+ * mismatch (0 = G2). The reflection/fairness check for matched panel seams.
97784
+ */
97785
+ EdgeMatch(edgeA, edgeB, options = {}) {
97786
+ return edgeMatchReport(edgeA, edgeB, options.samples);
97787
+ },
97095
97788
  CurvatureComb(input, options = {}) {
97096
97789
  if (input instanceof NurbsCurve3D) {
97097
97790
  const count = Math.max(8, options.samples ?? 32);
@@ -100544,22 +101237,10 @@ function roundNum(n, digits = 4) {
100544
101237
  return Number.isFinite(n) ? n.toFixed(digits).replace(/\.?0+$/, "") : String(n);
100545
101238
  }
100546
101239
  const COLLISION_OVERLAP_VOLUME_TOLERANCE = 1e-6;
100547
- function meshDerivedManifoldBackend(shape) {
100548
- const mesh = getShapeRuntimeBackend(shape).getMesh();
100549
- return reconstructBackendFromMesh({
100550
- numProp: mesh.numProp,
100551
- triVerts: mesh.triVerts,
100552
- vertProperties: mesh.vertProperties,
100553
- mergeFromVert: mesh.mergeFromVert ?? new Uint32Array(),
100554
- mergeToVert: mesh.mergeToVert ?? new Uint32Array()
100555
- });
100556
- }
100557
101240
  function backendForMinGap(shape) {
100558
101241
  const backend = getShapeRuntimeBackend(shape);
100559
101242
  if (isManifoldCapableBackend(backend)) return { kind: "manifold", backend, method: "exact", dispose: false };
100560
- if (isSdfCapableBackend(backend) || getActiveBackend() === "sdf")
100561
- return { kind: "mesh", backend, method: "mesh-derived", dispose: false };
100562
- return { kind: "manifold", backend: meshDerivedManifoldBackend(shape), method: "mesh-derived", dispose: true };
101243
+ return { kind: "mesh", backend, method: "mesh-derived", dispose: false };
100563
101244
  }
100564
101245
  function meshHasPointInsideSdf(source, target) {
100565
101246
  const mesh = source.getMesh();
@@ -101732,7 +102413,6 @@ function resetExecutionSession(logs) {
101732
102413
  resetHighlights();
101733
102414
  resetBom();
101734
102415
  resetSheetStock();
101735
- resetRobotExport();
101736
102416
  resetCutPlanes();
101737
102417
  resetRenderLabels();
101738
102418
  resetCameraTrajectory();
@@ -101815,7 +102495,6 @@ function collectSuccessfulExecutionSnapshot(args) {
101815
102495
  jointsView: getCollectedJointsView(),
101816
102496
  viewConfig: getCollectedViewConfig(),
101817
102497
  sceneConfig: getCollectedScene(),
101818
- robotExport: getCollectedRobotExport(),
101819
102498
  quality: args.quality,
101820
102499
  logs: args.logs.slice(),
101821
102500
  verifications: getCollectedVerifications(),
@@ -101840,7 +102519,6 @@ function collectFailedExecutionSnapshot(args) {
101840
102519
  jointsView: getCollectedJointsView(),
101841
102520
  viewConfig: getCollectedViewConfig(),
101842
102521
  sceneConfig: getCollectedScene(),
101843
- robotExport: getCollectedRobotExport(),
101844
102522
  quality: args.quality,
101845
102523
  logs: args.logs.slice(),
101846
102524
  verifications: getCollectedVerifications(),
@@ -314439,7 +315117,6 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
314439
315117
  sketchToDxf,
314440
315118
  bom,
314441
315119
  sheetStock,
314442
- robotExport,
314443
315120
  Sim,
314444
315121
  group,
314445
315122
  ShapeGroup,
@@ -315142,7 +315819,8 @@ function serializeShape(obj, timings) {
315142
315819
  vertProperties: meshVertProperties,
315143
315820
  mergeFromVert: meshMergeFromVert.length > 0 ? meshMergeFromVert : void 0,
315144
315821
  mergeToVert: meshMergeToVert.length > 0 ? meshMergeToVert : void 0,
315145
- vertNormals: vertNormals ?? void 0
315822
+ vertNormals: vertNormals ?? void 0,
315823
+ cornerNormals: rawMesh.cornerNormals && rawMesh.cornerNormals.length === numTriangles * 9 ? rawMesh.cornerNormals : void 0
315146
315824
  },
315147
315825
  { skipEdges: occtBackend !== null }
315148
315826
  );