forgecad 0.9.2 → 0.9.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 (79) hide show
  1. package/LICENSE +7 -5
  2. package/README.md +1 -1
  3. package/README.public.md +24 -2
  4. package/dist/assets/{AdminPage-Bs4PiK00.js → AdminPage-4jihcEk_.js} +1 -1
  5. package/dist/assets/{BlogPage-DVmgN0ma.js → BlogPage-BvzruKtw.js} +1 -1
  6. package/dist/assets/{DocsPage-BP6wlsBN.js → DocsPage-DHbd-WS-.js} +13 -13
  7. package/dist/assets/{EditorApp-Arw2NnGJ.js → EditorApp-C5P2rBfh.js} +433 -84
  8. package/dist/assets/{EditorApp-VY9lXx0N.css → EditorApp-DS0AIUrZ.css} +25 -0
  9. package/dist/assets/{EmbedViewer-qgQiOahL.js → EmbedViewer-B70wQwlE.js} +2 -2
  10. package/dist/assets/{LandingPageProofDriven-DvhtmWOz.js → LandingPageProofDriven-DIsYTnep.js} +1 -1
  11. package/dist/assets/{PricingPage-Ck3CP2ti.css → PricingPage-BMedqFef.css} +48 -0
  12. package/dist/assets/{PricingPage-657oLvWh.js → PricingPage-YPOr12pP.js} +34 -6
  13. package/dist/assets/{SettingsPage-wNy3_2yn.js → SettingsPage-rntoyJ3b.js} +10 -13
  14. package/dist/assets/{app-BdBoMQeO.js → app-CWucmnLZ.js} +801 -1208
  15. package/dist/assets/cli/{render-Ci3jjyT1.js → render-DZHmUySW.js} +214 -23
  16. package/dist/assets/copy-CQKQppF-.js +8 -0
  17. package/dist/assets/{evalWorker-CMCAbK8r.js → evalWorker-C3dKxi9Y.js} +1117 -95
  18. package/dist/assets/{manifold-BMn-8Vf8.js → manifold-CQ3FhfWB.js} +1 -1
  19. package/dist/assets/{manifold-jlYQ6E5R.js → manifold-CU0G1yYL.js} +1 -1
  20. package/dist/assets/{manifold-DbyILno4.js → manifold-CYWZMfjB.js} +2 -2
  21. package/dist/assets/{renderSceneState-DAnqvxSt.js → renderSceneState-BBUrnsUN.js} +1 -1
  22. package/dist/assets/{reportWorker-BcRVMHK-.js → reportWorker-BhZ7DjxQ.js} +1091 -95
  23. package/dist/assets/{sectionPlaneMath-DXJ_TdIW.js → sectionPlaneMath-BxfokaJE.js} +1091 -95
  24. package/dist/cli/render.html +1 -1
  25. package/dist/docs/index.html +2 -2
  26. package/dist/docs-raw/AI/usage.md +182 -89
  27. package/dist/docs-raw/API/core/concepts.md +26 -0
  28. package/dist/docs-raw/CLI.md +58 -37
  29. package/dist/docs-raw/INDEX.md +81 -64
  30. package/dist/docs-raw/cli-monetization.md +9 -8
  31. package/dist/docs-raw/generated/concepts.md +111 -4
  32. package/dist/docs-raw/generated/core.md +2 -0
  33. package/dist/docs-raw/generated/curves.md +480 -1
  34. package/dist/docs-raw/generated/output.md +1 -0
  35. package/dist/docs-raw/generated/sketch.md +2 -0
  36. package/dist/docs-raw/generated/viewport.md +81 -3
  37. package/dist/docs-raw/product/user-outreach-email-templates.md +159 -0
  38. package/dist/docs-raw/skills/forgecad-image-replicator.md +1 -1
  39. package/dist/docs-raw/skills/forgecad-make-a-model.md +33 -4
  40. package/dist/docs-raw/skills/forgecad-prepare-prompt.md +1 -1
  41. package/dist/docs-raw/skills/forgecad-project.md +1 -1
  42. package/dist/docs-raw/skills/forgecad-render-inspect.md +1 -1
  43. package/dist/docs-raw/skills/forgecad.md +2 -1
  44. package/dist/docs-raw/welcome.md +85 -137
  45. package/dist/index.html +1 -1
  46. package/dist/llms.txt +4 -3
  47. package/dist/sitemap.xml +6 -6
  48. package/dist-cli/forgecad.js +1413 -219
  49. package/dist-cli/forgecad.js.map +1 -1
  50. package/dist-skill/CONTEXT.md +594 -5
  51. package/dist-skill/SKILL-dev.md +2 -1
  52. package/dist-skill/SKILL.md +2 -1
  53. package/dist-skill/docs/API/core/concepts.md +26 -0
  54. package/dist-skill/docs/CLI.md +58 -37
  55. package/dist-skill/docs/generated/core.md +2 -0
  56. package/dist-skill/docs/generated/curves.md +480 -1
  57. package/dist-skill/docs/generated/output.md +1 -0
  58. package/dist-skill/docs/generated/sketch.md +2 -0
  59. package/dist-skill/docs/generated/viewport.md +81 -3
  60. package/dist-skill/docs-dev/API/core/concepts.md +26 -0
  61. package/dist-skill/docs-dev/CLI.md +58 -37
  62. package/dist-skill/docs-dev/generated/core.md +2 -0
  63. package/dist-skill/docs-dev/generated/curves.md +480 -1
  64. package/dist-skill/docs-dev/generated/output.md +1 -0
  65. package/dist-skill/docs-dev/generated/sketch.md +2 -0
  66. package/dist-skill/docs-dev/generated/viewport.md +81 -3
  67. package/dist-skill/library/README.md +0 -1
  68. package/dist-skill/library/forgecad-image-replicator/SKILL.md +1 -1
  69. package/dist-skill/library/forgecad-make-a-model/SKILL.md +33 -4
  70. package/dist-skill/library/forgecad-prepare-prompt/SKILL.md +1 -1
  71. package/dist-skill/library/forgecad-project/SKILL.md +1 -1
  72. package/dist-skill/library/forgecad-render-inspect/SKILL.md +1 -1
  73. package/examples/api/conformal-product-ribbon.forge.js +77 -0
  74. package/examples/api/render-labels.forge.js +33 -0
  75. package/examples/api/text2d-basics.forge.js +6 -3
  76. package/package.json +1 -1
  77. package/dist-skill/library/forgecad-deep-dive/SKILL.md +0 -120
  78. package/dist-skill/library/forgecad-deep-dive/agents/openai.yaml +0 -4
  79. package/dist-skill/library/forgecad-deep-dive/references/output-shape.md +0 -64
@@ -7232,7 +7232,7 @@ function add$3(a, b) {
7232
7232
  function scale$4(v, s) {
7233
7233
  return [v[0] * s, v[1] * s, v[2] * s];
7234
7234
  }
7235
- function sub$4(a, b) {
7235
+ function sub$5(a, b) {
7236
7236
  return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
7237
7237
  }
7238
7238
  function cross$4(a, b) {
@@ -7271,8 +7271,8 @@ function buildSurfaceSheetTopology(boundaries, options = {}) {
7271
7271
  const [v1Start, v1End] = boundaries.v1;
7272
7272
  const corners = [u0Start, u0End, u1Start, u1End];
7273
7273
  const center = options.center ?? average$1(corners);
7274
- const uAxis = normalizeAxis$1(sub$4(midpoint$3(u1Start, u1End), midpoint$3(u0Start, u0End)));
7275
- const vAxis = normalizeAxis$1(sub$4(midpoint$3(v1Start, v1End), midpoint$3(v0Start, v0End)));
7274
+ const uAxis = normalizeAxis$1(sub$5(midpoint$3(u1Start, u1End), midpoint$3(u0Start, u0End)));
7275
+ const vAxis = normalizeAxis$1(sub$5(midpoint$3(v1Start, v1End), midpoint$3(v0Start, v0End)));
7276
7276
  const normal = normalizeAxis$1(options.normal ?? cross$4(uAxis, vAxis));
7277
7277
  const faces = /* @__PURE__ */ new Map();
7278
7278
  faces.set(faceName, {
@@ -7304,7 +7304,7 @@ function attachSurfaceSheetTopology(shape, boundaries, options = {}) {
7304
7304
  });
7305
7305
  return shape;
7306
7306
  }
7307
- function requireFinite$6(v, label) {
7307
+ function requireFinite$7(v, label) {
7308
7308
  if (!Number.isFinite(v)) throw new Error(`nurbsSurface: ${label} must be finite, got ${v}`);
7309
7309
  }
7310
7310
  class NurbsSurface {
@@ -7330,16 +7330,16 @@ class NurbsSurface {
7330
7330
  for (let i = 0; i < nU; i++) {
7331
7331
  if (controlGrid[i].length !== nV) throw new Error(`nurbsSurface: row ${i} has ${controlGrid[i].length} points, expected ${nV}`);
7332
7332
  for (let j = 0; j < nV; j++) {
7333
- requireFinite$6(controlGrid[i][j][0], `controlGrid[${i}][${j}][0]`);
7334
- requireFinite$6(controlGrid[i][j][1], `controlGrid[${i}][${j}][1]`);
7335
- requireFinite$6(controlGrid[i][j][2], `controlGrid[${i}][${j}][2]`);
7333
+ requireFinite$7(controlGrid[i][j][0], `controlGrid[${i}][${j}][0]`);
7334
+ requireFinite$7(controlGrid[i][j][1], `controlGrid[${i}][${j}][1]`);
7335
+ requireFinite$7(controlGrid[i][j][2], `controlGrid[${i}][${j}][2]`);
7336
7336
  }
7337
7337
  }
7338
7338
  const weightsGrid = options.weights ?? controlGrid.map((row) => row.map(() => 1));
7339
7339
  for (let i = 0; i < nU; i++) {
7340
7340
  if (weightsGrid[i].length !== nV) throw new Error(`nurbsSurface: weights row ${i} length mismatch`);
7341
7341
  for (let j = 0; j < nV; j++) {
7342
- requireFinite$6(weightsGrid[i][j], `weights[${i}][${j}]`);
7342
+ requireFinite$7(weightsGrid[i][j], `weights[${i}][${j}]`);
7343
7343
  if (weightsGrid[i][j] <= 0) throw new Error(`nurbsSurface: weights[${i}][${j}] must be > 0`);
7344
7344
  }
7345
7345
  }
@@ -8750,7 +8750,7 @@ let _wasm = null;
8750
8750
  async function initManifoldWasm() {
8751
8751
  if (_wasm) return _wasm;
8752
8752
  performance.mark("manifold:start");
8753
- const Module = (await import("./manifold-BMn-8Vf8.js")).default;
8753
+ const Module = (await import("./manifold-CQ3FhfWB.js")).default;
8754
8754
  performance.mark("manifold:imported");
8755
8755
  const wasm = await Module();
8756
8756
  wasm.setup();
@@ -8959,7 +8959,7 @@ function sweepStitched(profilePolygons, pathPoints, up, wasm) {
8959
8959
  function computeParallelTransportFrames(path2, preferredUp) {
8960
8960
  const n = path2.length;
8961
8961
  const frames = [];
8962
- const firstTangent = normalize$5(sub$3(path2[1], path2[0]));
8962
+ const firstTangent = normalize$5(sub$4(path2[1], path2[0]));
8963
8963
  if (!firstTangent) return null;
8964
8964
  let x = normalize$5(cross$3(preferredUp, firstTangent));
8965
8965
  if (!x || length(x) < 1e-8) {
@@ -8973,18 +8973,18 @@ function computeParallelTransportFrames(path2, preferredUp) {
8973
8973
  const prevT = frames[i - 1].t;
8974
8974
  let nextT;
8975
8975
  if (i < n - 1) {
8976
- const t1 = normalize$5(sub$3(path2[i], path2[i - 1]));
8977
- const t2 = normalize$5(sub$3(path2[i + 1], path2[i]));
8976
+ const t1 = normalize$5(sub$4(path2[i], path2[i - 1]));
8977
+ const t2 = normalize$5(sub$4(path2[i + 1], path2[i]));
8978
8978
  if (!t1 || !t2) return null;
8979
8979
  nextT = normalize$5(add$2(t1, t2)) || t1;
8980
8980
  } else {
8981
- const nt = normalize$5(sub$3(path2[i], path2[i - 1]));
8981
+ const nt = normalize$5(sub$4(path2[i], path2[i - 1]));
8982
8982
  if (!nt) return null;
8983
8983
  nextT = nt;
8984
8984
  }
8985
8985
  const v = cross$3(prevT, nextT);
8986
8986
  const vLen = length(v);
8987
- const c = dot$4(prevT, nextT);
8987
+ const c = dot$5(prevT, nextT);
8988
8988
  if (vLen > 1e-10) {
8989
8989
  const axis = scale$3(v, 1 / vLen);
8990
8990
  x = rotateVector(frames[i - 1].x, axis, c, vLen);
@@ -9058,7 +9058,7 @@ function signedArea$4(loop) {
9058
9058
  }
9059
9059
  return area * 0.5;
9060
9060
  }
9061
- function sub$3(a, b) {
9061
+ function sub$4(a, b) {
9062
9062
  return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
9063
9063
  }
9064
9064
  function add$2(a, b) {
@@ -9067,7 +9067,7 @@ function add$2(a, b) {
9067
9067
  function scale$3(v, s) {
9068
9068
  return [v[0] * s, v[1] * s, v[2] * s];
9069
9069
  }
9070
- function dot$4(a, b) {
9070
+ function dot$5(a, b) {
9071
9071
  return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
9072
9072
  }
9073
9073
  function cross$3(a, b) {
@@ -9086,7 +9086,7 @@ function normalize$5(v) {
9086
9086
  return [v[0] / len2, v[1] / len2, v[2] / len2];
9087
9087
  }
9088
9088
  function rotateVector(v, axis, c, s) {
9089
- const kDotV = dot$4(axis, v);
9089
+ const kDotV = dot$5(axis, v);
9090
9090
  const kCrossV = cross$3(axis, v);
9091
9091
  return [
9092
9092
  v[0] * c + kCrossV[0] * s + axis[0] * kDotV * (1 - c),
@@ -12792,7 +12792,7 @@ function normalizeFaceSelector(selector) {
12792
12792
  function cross$2(a, b) {
12793
12793
  return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
12794
12794
  }
12795
- function dot$3(a, b) {
12795
+ function dot$4(a, b) {
12796
12796
  return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
12797
12797
  }
12798
12798
  function normVec3(v) {
@@ -12827,11 +12827,11 @@ function clusterMeshFaces(shape) {
12827
12827
  if (!normal) continue;
12828
12828
  const crossLen = Math.sqrt(rawCross[0] * rawCross[0] + rawCross[1] * rawCross[1] + rawCross[2] * rawCross[2]);
12829
12829
  const triArea = crossLen / 2;
12830
- const planeOffset = dot$3(normal, v0);
12830
+ const planeOffset = dot$4(normal, v0);
12831
12831
  const triCentroid = [(v0[0] + v1[0] + v2[0]) / 3, (v0[1] + v1[1] + v2[1]) / 3, (v0[2] + v1[2] + v2[2]) / 3];
12832
12832
  let merged = false;
12833
12833
  for (const c of clusters) {
12834
- if (dot$3(c.normal, normal) > NORMAL_COS_EPS$1 && Math.abs(c.planeOffset - planeOffset) < PLANE_OFFSET_EPS$1) {
12834
+ if (dot$4(c.normal, normal) > NORMAL_COS_EPS$1 && Math.abs(c.planeOffset - planeOffset) < PLANE_OFFSET_EPS$1) {
12835
12835
  c.centroidSum[0] += triCentroid[0];
12836
12836
  c.centroidSum[1] += triCentroid[1];
12837
12837
  c.centroidSum[2] += triCentroid[2];
@@ -12873,7 +12873,7 @@ function queryMeshFaces(shape, query) {
12873
12873
  let clusters = clusterMeshFaces(shape);
12874
12874
  if (query.normal) {
12875
12875
  const qn = query.normal;
12876
- clusters = clusters.filter((c) => dot$3(c.normal, qn) > NORMAL_COS_EPS$1);
12876
+ clusters = clusters.filter((c) => dot$4(c.normal, qn) > NORMAL_COS_EPS$1);
12877
12877
  }
12878
12878
  if (query.planar !== false) {
12879
12879
  clusters = clusters.filter((c) => c.normal !== null);
@@ -12889,7 +12889,7 @@ function queryMeshFace(shape, query) {
12889
12889
  let clusters = clusterMeshFaces(shape);
12890
12890
  if (query.normal) {
12891
12891
  const qn = query.normal;
12892
- clusters = clusters.filter((c) => dot$3(c.normal, qn) > NORMAL_COS_EPS$1);
12892
+ clusters = clusters.filter((c) => dot$4(c.normal, qn) > NORMAL_COS_EPS$1);
12893
12893
  }
12894
12894
  if (query.planar !== false) {
12895
12895
  clusters = clusters.filter((c) => c.normal !== null);
@@ -19841,6 +19841,7 @@ Fix: pass a path like [[0,0,0], [20,0,8,4], [40,0,0,2]].`
19841
19841
  const curveMode = shouldSmoothCurve(options, defaultCurve) && sourcePoints.length >= 3;
19842
19842
  const normalized = compactPathPoints(resampleCurve(sourcePoints, options, defaultCurve));
19843
19843
  const minRadius = Math.min(...normalized.map((point2) => point2.radius));
19844
+ const hasExplicitBlend = options.blend !== void 0;
19844
19845
  const blendRadius = positiveFinite(options.blend, "Sculpt.tube() blend", Math.max(0.5, minRadius));
19845
19846
  let segmentCount = 0;
19846
19847
  for (let i = 0; i < normalized.length - 1; i += 1) {
@@ -19849,8 +19850,8 @@ Fix: pass a path like [[0,0,0], [20,0,8,4], [40,0,0,2]].`
19849
19850
  if (segmentCount === 0) {
19850
19851
  throw new Error("Sculpt.tube() points must include at least one non-zero-length segment.");
19851
19852
  }
19852
- const curveBlendRadius = curveMode ? Math.max(blendRadius, minRadius * 1.5) : Math.min(blendRadius, Math.max(0.1, minRadius * 0.8));
19853
- let out = polylineSweep(normalized, curveMode ? curveBlendRadius : blendRadius);
19853
+ const effectiveBlendRadius = curveMode && !hasExplicitBlend ? Math.max(blendRadius, minRadius * 1.5) : blendRadius;
19854
+ let out = polylineSweep(normalized, effectiveBlendRadius);
19854
19855
  if (options.polish !== void 0) out = out.polish(options.polish);
19855
19856
  return out;
19856
19857
  }
@@ -22238,7 +22239,7 @@ function intersection(...inputs) {
22238
22239
  nextPlan
22239
22240
  );
22240
22241
  }
22241
- var define_process_env_default = {};
22242
+ var define_process_env_default$2 = {};
22242
22243
  let _wasm_solve = null;
22243
22244
  let _wasm_get_profile = null;
22244
22245
  let _solverMemory = null;
@@ -22276,7 +22277,7 @@ function readInitialConsoleDebug() {
22276
22277
  return false;
22277
22278
  }
22278
22279
  function readEnvFlag(name) {
22279
- const value = typeof process !== "undefined" ? define_process_env_default == null ? void 0 : define_process_env_default[name] : void 0;
22280
+ const value = typeof process !== "undefined" ? define_process_env_default$2 == null ? void 0 : define_process_env_default$2[name] : void 0;
22280
22281
  if (typeof value !== "string") return false;
22281
22282
  return ["1", "true", "yes", "on"].includes(value.toLowerCase());
22282
22283
  }
@@ -23552,7 +23553,7 @@ class MateBuilder {
23552
23553
  return this.constraints.reduce((sum, c) => sum + (CONSTRAINT_EQUATIONS[c.type] ?? 0), 0);
23553
23554
  }
23554
23555
  }
23555
- let _collected$7 = null;
23556
+ let _collected$8 = null;
23556
23557
  const isAxis = (value) => value === "x" || value === "y" || value === "z";
23557
23558
  const normalizeDirection = (value, label) => {
23558
23559
  if (value === "radial" || isAxis(value)) return value;
@@ -23611,16 +23612,16 @@ const mergeDirective = (target, patch, label) => {
23611
23612
  return out;
23612
23613
  };
23613
23614
  function resetExplodeView() {
23614
- _collected$7 = null;
23615
+ _collected$8 = null;
23615
23616
  }
23616
23617
  function getCollectedExplodeView() {
23617
- return _collected$7 ? cloneOptions(_collected$7) : null;
23618
+ return _collected$8 ? cloneOptions(_collected$8) : null;
23618
23619
  }
23619
23620
  function explodeView(options = {}) {
23620
23621
  if (!options || typeof options !== "object") {
23621
23622
  throw new Error("explodeView(options) expects an options object");
23622
23623
  }
23623
- const next = _collected$7 ? cloneOptions(_collected$7) : {};
23624
+ const next = _collected$8 ? cloneOptions(_collected$8) : {};
23624
23625
  if (options.enabled !== void 0) {
23625
23626
  if (typeof options.enabled !== "boolean") throw new Error("explodeView.enabled must be a boolean");
23626
23627
  next.enabled = options.enabled;
@@ -23668,9 +23669,9 @@ function explodeView(options = {}) {
23668
23669
  });
23669
23670
  next.byPath = byPath;
23670
23671
  }
23671
- _collected$7 = next;
23672
+ _collected$8 = next;
23672
23673
  }
23673
- let _collected$6 = null;
23674
+ let _collected$7 = null;
23674
23675
  const isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
23675
23676
  const isVec3$1 = (value) => Array.isArray(value) && value.length === 3 && isFiniteNumber(value[0]) && isFiniteNumber(value[1]) && isFiniteNumber(value[2]);
23676
23677
  const normalizeAxis = (axis) => {
@@ -23960,22 +23961,22 @@ const cloneCollected = (value) => ({
23960
23961
  defaultAnimation: value.defaultAnimation
23961
23962
  });
23962
23963
  function resetJointsView() {
23963
- _collected$6 = null;
23964
+ _collected$7 = null;
23964
23965
  }
23965
23966
  function getCollectedJointsView() {
23966
- return _collected$6 ? cloneCollected(_collected$6) : null;
23967
+ return _collected$7 ? cloneCollected(_collected$7) : null;
23967
23968
  }
23968
23969
  function saveJointsView() {
23969
- return _collected$6 ? cloneCollected(_collected$6) : null;
23970
+ return _collected$7 ? cloneCollected(_collected$7) : null;
23970
23971
  }
23971
23972
  function restoreJointsView(state) {
23972
- _collected$6 = state;
23973
+ _collected$7 = state;
23973
23974
  }
23974
23975
  function jointsView(options = {}) {
23975
23976
  if (!options || typeof options !== "object") {
23976
23977
  throw new Error("jointsView(options) expects an options object");
23977
23978
  }
23978
- const next = _collected$6 ? cloneCollected(_collected$6) : { joints: [], couplings: [], animations: [] };
23979
+ const next = _collected$7 ? cloneCollected(_collected$7) : { joints: [], couplings: [], animations: [] };
23979
23980
  if (options.enabled !== void 0) {
23980
23981
  if (typeof options.enabled !== "boolean") {
23981
23982
  throw new Error("jointsView.enabled must be a boolean");
@@ -24030,8 +24031,14 @@ function jointsView(options = {}) {
24030
24031
  if (next.defaultAnimation && !next.animations.some((animation) => animation.name === next.defaultAnimation)) {
24031
24032
  throw new Error(`jointsView defaultAnimation "${next.defaultAnimation}" does not exist in animations`);
24032
24033
  }
24033
- _collected$6 = next;
24034
+ _collected$7 = next;
24034
24035
  }
24036
+ var define_process_env_default$1 = {};
24037
+ const SWEEP_JOINT_DEFAULT_STEP_LIMIT = {
24038
+ live: 1,
24039
+ default: 4,
24040
+ high: Number.POSITIVE_INFINITY
24041
+ };
24035
24042
  let collectedAssemblies = [];
24036
24043
  function resetCollectedAssemblies() {
24037
24044
  collectedAssemblies = [];
@@ -24072,6 +24079,33 @@ function collisionShape(part) {
24072
24079
  if (shapes.length === 1) return shapes[0];
24073
24080
  return union(...shapes);
24074
24081
  }
24082
+ function boundsOverlap(a, b) {
24083
+ for (let axis = 0; axis < 3; axis++) {
24084
+ if (a.max[axis] <= b.min[axis] || b.max[axis] <= a.min[axis]) return false;
24085
+ }
24086
+ return true;
24087
+ }
24088
+ function readAssemblyPerfEnv(name) {
24089
+ return typeof process !== "undefined" ? define_process_env_default$1 == null ? void 0 : define_process_env_default$1[name] : void 0;
24090
+ }
24091
+ function resolveSweepJointStepLimit() {
24092
+ if (readAssemblyPerfEnv("FORGECAD_ALLOW_FULL_SWEEP_JOINT") === "1") return Number.POSITIVE_INFINITY;
24093
+ const override = readAssemblyPerfEnv("FORGECAD_SWEEP_JOINT_STEP_LIMIT");
24094
+ if (override != null && override.trim() !== "") {
24095
+ const parsed = Number(override);
24096
+ if (Number.isFinite(parsed) && parsed >= 1) return Math.floor(parsed);
24097
+ }
24098
+ return SWEEP_JOINT_DEFAULT_STEP_LIMIT[getForgeQualityPreset()];
24099
+ }
24100
+ function boundSweepJointSteps(jointName, requestedSteps) {
24101
+ const limit = resolveSweepJointStepLimit();
24102
+ if (!Number.isFinite(limit) || requestedSteps <= limit) return requestedSteps;
24103
+ const bounded = Math.max(1, Math.floor(limit));
24104
+ emitRuntimeWarning(
24105
+ `sweepJoint("${jointName}") requested ${requestedSteps} step(s), exceeding the ${getForgeQualityPreset()} quality sweep limit (${bounded}). Using ${bounded} step(s) for responsiveness. Use high quality/export, set FORGECAD_SWEEP_JOINT_STEP_LIMIT, or set FORGECAD_ALLOW_FULL_SWEEP_JOINT=1 for the full sweep.`
24106
+ );
24107
+ return bounded;
24108
+ }
24075
24109
  const FASTENER_PATTERN = /\b(bolt|screw|nut|washer|pin|rivet|fastener|standoff|insert)\b/i;
24076
24110
  function isFastenerName(name) {
24077
24111
  return FASTENER_PATTERN.test(name);
@@ -24404,16 +24438,23 @@ class SolvedAssembly {
24404
24438
  const minOverlap = options.minOverlapVolume ?? 0.1;
24405
24439
  const ignore = new Set((options.ignorePairs ?? []).map(([a, b]) => [a, b].sort().join("|")));
24406
24440
  const findings = [];
24407
- for (let i = 0; i < names.length; i++) {
24408
- for (let j = i + 1; j < names.length; j++) {
24409
- const aName = names[i];
24410
- const bName = names[j];
24441
+ const entries = [];
24442
+ for (const name of names) {
24443
+ const shape = collisionShape(this.getPart(name));
24444
+ if (!shape) continue;
24445
+ try {
24446
+ entries.push({ name, shape, bounds: shape.boundingBox() });
24447
+ } catch {
24448
+ }
24449
+ }
24450
+ for (let i = 0; i < entries.length; i++) {
24451
+ for (let j = i + 1; j < entries.length; j++) {
24452
+ const aName = entries[i].name;
24453
+ const bName = entries[j].name;
24411
24454
  if (ignore.has([aName, bName].sort().join("|"))) continue;
24412
- const a = collisionShape(this.getPart(aName));
24413
- const b = collisionShape(this.getPart(bName));
24414
- if (!a || !b) continue;
24455
+ if (!boundsOverlap(entries[i].bounds, entries[j].bounds)) continue;
24415
24456
  try {
24416
- const hit = a.intersect(b);
24457
+ const hit = entries[i].shape.intersect(entries[j].shape);
24417
24458
  if (hit.isEmpty()) continue;
24418
24459
  const vol = hit.volume();
24419
24460
  if (vol > minOverlap) {
@@ -25390,7 +25431,7 @@ class Assembly {
25390
25431
  if (this.jointCouplings.has(jointName)) {
25391
25432
  throw new Error(`Cannot sweep coupled joint "${jointName}". Sweep one of its source joints instead.`);
25392
25433
  }
25393
- const n = Math.max(1, Math.floor(steps));
25434
+ const n = boundSweepJointSteps(jointName, Math.max(1, Math.floor(steps)));
25394
25435
  const frames = [];
25395
25436
  for (let i = 0; i <= n; i++) {
25396
25437
  const t = n === 0 ? 0 : i / n;
@@ -25806,12 +25847,12 @@ function bom(quantity, description, opts) {
25806
25847
  metadata
25807
25848
  });
25808
25849
  }
25809
- let _collected$5 = [];
25850
+ let _collected$6 = [];
25810
25851
  function resetCutPlanes() {
25811
- _collected$5 = [];
25852
+ _collected$6 = [];
25812
25853
  }
25813
25854
  function getCollectedCutPlanes() {
25814
- return _collected$5.slice();
25855
+ return _collected$6.slice();
25815
25856
  }
25816
25857
  function normalizeExcludedObjectNames(input) {
25817
25858
  if (input === void 0) return void 0;
@@ -25827,16 +25868,16 @@ function cutPlane(name, normal, offsetOrOptions = 0, maybeOptions = {}) {
25827
25868
  const offset2 = Number.isFinite(rawOffset) ? rawOffset : 0;
25828
25869
  const options = usingOffsetArg ? maybeOptions : offsetOrOptions;
25829
25870
  const excludeObjectNames = normalizeExcludedObjectNames(options.exclude);
25830
- _collected$5.push({ name, normal, offset: offset2, excludeObjectNames });
25871
+ _collected$6.push({ name, normal, offset: offset2, excludeObjectNames });
25831
25872
  }
25832
- let _collected$4 = [];
25873
+ let _collected$5 = [];
25833
25874
  let _counter$1 = 0;
25834
25875
  function resetMocks() {
25835
- _collected$4 = [];
25876
+ _collected$5 = [];
25836
25877
  _counter$1 = 0;
25837
25878
  }
25838
25879
  function getCollectedMocks() {
25839
- return _collected$4.slice();
25880
+ return _collected$5.slice();
25840
25881
  }
25841
25882
  function mock(shape, name) {
25842
25883
  if (!shape || typeof shape !== "object") {
@@ -25844,7 +25885,7 @@ function mock(shape, name) {
25844
25885
  }
25845
25886
  _counter$1 += 1;
25846
25887
  const displayName = name && typeof name === "string" && name.trim().length > 0 ? name.trim() : `Mock ${_counter$1}`;
25847
- _collected$4.push({
25888
+ _collected$5.push({
25848
25889
  id: `mock-${_counter$1}`,
25849
25890
  name: displayName,
25850
25891
  shape
@@ -30236,7 +30277,7 @@ function shapeToBounds(shape) {
30236
30277
  max: [bb.max[0], bb.max[1], bb.max[2]]
30237
30278
  };
30238
30279
  }
30239
- function sub$2(a, b) {
30280
+ function sub$3(a, b) {
30240
30281
  return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
30241
30282
  }
30242
30283
  function addVec(a, b) {
@@ -30245,7 +30286,7 @@ function addVec(a, b) {
30245
30286
  function scale$2(v, s) {
30246
30287
  return [v[0] * s, v[1] * s, v[2] * s];
30247
30288
  }
30248
- function dot$2(a, b) {
30289
+ function dot$3(a, b) {
30249
30290
  return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
30250
30291
  }
30251
30292
  function cross$1(a, b) {
@@ -30269,9 +30310,9 @@ function pipeRoute(points, radius, options) {
30269
30310
  const bends = new Array(points.length).fill(null);
30270
30311
  for (let i = 1; i < points.length - 1; i++) {
30271
30312
  const prev = points[i - 1], cur = points[i], next = points[i + 1];
30272
- const dIn = normalize$2(sub$2(cur, prev));
30273
- const dOut = normalize$2(sub$2(next, cur));
30274
- const dotVal = clampDot(dot$2(dIn, dOut));
30313
+ const dIn = normalize$2(sub$3(cur, prev));
30314
+ const dOut = normalize$2(sub$3(next, cur));
30315
+ const dotVal = clampDot(dot$3(dIn, dOut));
30275
30316
  const bendAngle = Math.acos(dotVal);
30276
30317
  if (bendAngle < 1e-6) {
30277
30318
  continue;
@@ -30290,7 +30331,7 @@ function pipeRoute(points, radius, options) {
30290
30331
  }
30291
30332
  const parts = [];
30292
30333
  const makeSeg = (a, b) => {
30293
- const d = sub$2(b, a);
30334
+ const d = sub$3(b, a);
30294
30335
  const len2 = vecLen(d);
30295
30336
  if (len2 < 0.01) return null;
30296
30337
  const dir = normalize$2(d);
@@ -30335,7 +30376,7 @@ function pipeRoute(points, radius, options) {
30335
30376
  const innerBend = buildShapeFromCompilePlan(innerPlan);
30336
30377
  bendShape = bendShape.subtract(innerBend);
30337
30378
  }
30338
- const radialDir = normalize$2(sub$2(info.startPt, info.center));
30379
+ const radialDir = normalize$2(sub$3(info.startPt, info.center));
30339
30380
  const tangentDir = cross$1(info.axis, radialDir);
30340
30381
  const c = info.center;
30341
30382
  bendShape = bendShape.transform([
@@ -30396,7 +30437,7 @@ function elbow(pipeRadius, bendRadius, angle, options) {
30396
30437
  if (fromDir && toDir) {
30397
30438
  const nFrom = normalize$2(fromDir);
30398
30439
  const nTo = normalize$2(toDir);
30399
- const d = clampDot(dot$2(nFrom, nTo));
30440
+ const d = clampDot(dot$3(nFrom, nTo));
30400
30441
  angleDeg = Math.acos(d) * 180 / Math.PI;
30401
30442
  }
30402
30443
  if (angleDeg < 0.01) throw new Error("elbow: angle too small");
@@ -30467,20 +30508,20 @@ function assertFinitePositive(apiName, name, value) {
30467
30508
  function add$1(a, b) {
30468
30509
  return [a[0] + b[0], a[1] + b[1]];
30469
30510
  }
30470
- function sub$1(a, b) {
30511
+ function sub$2(a, b) {
30471
30512
  return [a[0] - b[0], a[1] - b[1]];
30472
30513
  }
30473
30514
  function scale$1(v, s) {
30474
30515
  return [v[0] * s, v[1] * s];
30475
30516
  }
30476
- function dot$1(a, b) {
30517
+ function dot$2(a, b) {
30477
30518
  return a[0] * b[0] + a[1] * b[1];
30478
30519
  }
30479
30520
  function len(v) {
30480
30521
  return Math.hypot(v[0], v[1]);
30481
30522
  }
30482
30523
  function dist$2(a, b) {
30483
- return len(sub$1(b, a));
30524
+ return len(sub$2(b, a));
30484
30525
  }
30485
30526
  function norm$1(v) {
30486
30527
  const l = len(v);
@@ -30505,7 +30546,7 @@ function chooseSweepDeg(center, start, end, incomingDir) {
30505
30546
  const endAngle = angleOf(center, end);
30506
30547
  const cwTangent = tangentAt(startAngle, true);
30507
30548
  const ccwTangent = tangentAt(startAngle, false);
30508
- const clockwise = dot$1(cwTangent, incomingDir) >= dot$1(ccwTangent, incomingDir);
30549
+ const clockwise = dot$2(cwTangent, incomingDir) >= dot$2(ccwTangent, incomingDir);
30509
30550
  const sweep2 = clockwise ? -normalizePositiveRadians(startAngle - endAngle) : normalizePositiveRadians(endAngle - startAngle);
30510
30551
  return sweep2 * 180 / Math.PI;
30511
30552
  }
@@ -30548,8 +30589,8 @@ function normalizePulleyAsCircle(pulley, index, radiusOverride) {
30548
30589
  );
30549
30590
  }
30550
30591
  function commonTangents(a, b, mode) {
30551
- const delta = sub$1(b.center, a.center);
30552
- const z = dot$1(delta, delta);
30592
+ const delta = sub$2(b.center, a.center);
30593
+ const z = dot$2(delta, delta);
30553
30594
  if (z < EPS$5) {
30554
30595
  throw new Error(`beltDrive: pulleys "${a.name}" and "${b.name}" have the same center.`);
30555
30596
  }
@@ -30579,8 +30620,8 @@ function commonTangents(a, b, mode) {
30579
30620
  });
30580
30621
  }
30581
30622
  function buildSegmentsForTangentOrder(a, b, t0, t1) {
30582
- const span0Dir = norm$1(sub$1(t0.b, t0.a));
30583
- const span1Dir = norm$1(sub$1(t1.a, t1.b));
30623
+ const span0Dir = norm$1(sub$2(t0.b, t0.a));
30624
+ const span1Dir = norm$1(sub$2(t1.a, t1.b));
30584
30625
  const bSweepDeg = chooseSweepDeg(b.center, t0.b, t1.b, span0Dir);
30585
30626
  const aSweepDeg = chooseSweepDeg(a.center, t1.a, t0.a, span1Dir);
30586
30627
  const span0 = {
@@ -32081,6 +32122,130 @@ const partLibrary = {
32081
32122
  planetaryRatio,
32082
32123
  boltPattern
32083
32124
  };
32125
+ let _collected$4 = [];
32126
+ let _nextId = 1;
32127
+ function resetRenderLabels() {
32128
+ _collected$4 = [];
32129
+ _nextId = 1;
32130
+ }
32131
+ function getCollectedRenderLabels() {
32132
+ return _collected$4.map((label) => ({ ...label, at: [...label.at], offset: [...label.offset] }));
32133
+ }
32134
+ function requireFinite$6(value, label) {
32135
+ if (typeof value !== "number" || !Number.isFinite(value)) {
32136
+ throw new Error(`${label} must be a finite number`);
32137
+ }
32138
+ return value;
32139
+ }
32140
+ function requireVec3$2(value, label) {
32141
+ if (!Array.isArray(value) || value.length !== 3) {
32142
+ throw new Error(`${label} must be [x, y, z]`);
32143
+ }
32144
+ return [requireFinite$6(value[0], `${label}[0]`), requireFinite$6(value[1], `${label}[1]`), requireFinite$6(value[2], `${label}[2]`)];
32145
+ }
32146
+ function optionalColor(value, label) {
32147
+ if (value === void 0) return void 0;
32148
+ if (typeof value !== "string" || value.trim().length === 0) {
32149
+ throw new Error(`${label} must be a non-empty CSS color string`);
32150
+ }
32151
+ return value.trim();
32152
+ }
32153
+ const VALID_ANCHORS = /* @__PURE__ */ new Set([
32154
+ "center",
32155
+ "top",
32156
+ "bottom",
32157
+ "left",
32158
+ "right",
32159
+ "top-left",
32160
+ "top-right",
32161
+ "bottom-left",
32162
+ "bottom-right"
32163
+ ]);
32164
+ function normalizeOptions(options) {
32165
+ if (options === void 0) {
32166
+ return { offset: [0, 0, 0], anchor: "center", alwaysOnTop: true };
32167
+ }
32168
+ if (!options || typeof options !== "object" || Array.isArray(options)) {
32169
+ throw new Error("Viewport.label options must be an object");
32170
+ }
32171
+ const out = {
32172
+ offset: [0, 0, 0],
32173
+ anchor: "center",
32174
+ alwaysOnTop: true
32175
+ };
32176
+ const color = optionalColor(options.color, "Viewport.label options.color");
32177
+ if (color !== void 0) out.color = color;
32178
+ const background = optionalColor(options.background, "Viewport.label options.background");
32179
+ if (background !== void 0) out.background = background;
32180
+ if (options.size !== void 0) {
32181
+ out.size = requireFinite$6(options.size, "Viewport.label options.size");
32182
+ if (out.size <= 0) throw new Error("Viewport.label options.size must be positive");
32183
+ }
32184
+ if (options.offset !== void 0) out.offset = requireVec3$2(options.offset, "Viewport.label options.offset");
32185
+ if (options.anchor !== void 0) {
32186
+ if (!VALID_ANCHORS.has(options.anchor)) {
32187
+ throw new Error(`Viewport.label options.anchor must be one of: ${Array.from(VALID_ANCHORS).join(", ")}`);
32188
+ }
32189
+ out.anchor = options.anchor;
32190
+ }
32191
+ if (options.alwaysOnTop !== void 0) {
32192
+ if (typeof options.alwaysOnTop !== "boolean") throw new Error("Viewport.label options.alwaysOnTop must be a boolean");
32193
+ out.alwaysOnTop = options.alwaysOnTop;
32194
+ }
32195
+ return out;
32196
+ }
32197
+ function collectRenderLabel(text, at, options) {
32198
+ if (typeof text !== "string" || text.trim().length === 0) {
32199
+ throw new Error("Viewport.label text must be a non-empty string");
32200
+ }
32201
+ const normalizedAt = requireVec3$2(at, "Viewport.label at");
32202
+ const normalizedOptions = normalizeOptions(options);
32203
+ _collected$4.push({
32204
+ id: `render-label-${_nextId++}`,
32205
+ text: text.trim(),
32206
+ at: normalizedAt,
32207
+ ...normalizedOptions
32208
+ });
32209
+ }
32210
+ const Viewport = {
32211
+ /**
32212
+ * Add a render-only viewport label at a world-space point.
32213
+ *
32214
+ * **Details**
32215
+ *
32216
+ * `Viewport.label()` is for explanatory text that helps a viewer understand
32217
+ * the model. It does not create sketches, meshes, B-rep topology, exported
32218
+ * text, or face labels, so it stays off the OCCT path. Use `text2d()` only
32219
+ * when the letters should become manufactured geometry, such as raised
32220
+ * lettering, engraved serial numbers, or exported nameplates.
32221
+ *
32222
+ * Labels are collected during script execution and rendered by the viewport
32223
+ * as lightweight overlay annotations. They are ignored by exports and do not
32224
+ * appear in `objects`.
32225
+ *
32226
+ * **Example**
32227
+ *
32228
+ * ```js
32229
+ * Viewport.label('Bearing bore', [0, 0, 18], {
32230
+ * color: '#f8fafc',
32231
+ * background: '#0f172acc',
32232
+ * offset: [0, 0, 8],
32233
+ * anchor: 'bottom',
32234
+ * });
32235
+ *
32236
+ * return box(40, 30, 12);
32237
+ * ```
32238
+ *
32239
+ * @param text - Label text to display in the viewport
32240
+ * @param at - World-space anchor point `[x, y, z]`
32241
+ * @param options - Visual label options
32242
+ * @returns void
32243
+ * @category Viewport Labels
32244
+ */
32245
+ label(text, at, options) {
32246
+ collectRenderLabel(text, at, options);
32247
+ }
32248
+ };
32084
32249
  const RENDER_STYLE_OPTIONS = [
32085
32250
  {
32086
32251
  id: "classic",
@@ -32267,6 +32432,120 @@ function validateCamera(cam, label) {
32267
32432
  }
32268
32433
  return out;
32269
32434
  }
32435
+ function validateViewCamera(cam, label) {
32436
+ const validated = validateCamera(cam, label);
32437
+ if (!validated.position) {
32438
+ throw new Error(`${label}.position is required for named render views`);
32439
+ }
32440
+ if (!validated.target) {
32441
+ throw new Error(`${label}.target is required for named render views`);
32442
+ }
32443
+ return {
32444
+ ...validated,
32445
+ position: validated.position,
32446
+ target: validated.target
32447
+ };
32448
+ }
32449
+ function validateViews(views, label) {
32450
+ if (!views || typeof views !== "object" || Array.isArray(views)) {
32451
+ throw new Error(`${label} must be an object mapping view names to cameras`);
32452
+ }
32453
+ const out = {};
32454
+ for (const [name, view] of Object.entries(views)) {
32455
+ if (!name.trim()) {
32456
+ throw new Error(`${label} names must be non-empty strings`);
32457
+ }
32458
+ const viewLabel = `${label}.${name}`;
32459
+ if (!view || typeof view !== "object" || Array.isArray(view)) {
32460
+ throw new Error(`${viewLabel} must be a camera object or an object with a camera property`);
32461
+ }
32462
+ const hasExplicitCamera = Object.prototype.hasOwnProperty.call(view, "camera");
32463
+ if (hasExplicitCamera) {
32464
+ const camera = view.camera;
32465
+ if (!camera || typeof camera !== "object" || Array.isArray(camera)) {
32466
+ throw new Error(`${viewLabel}.camera must be an object`);
32467
+ }
32468
+ out[name] = { camera: validateViewCamera(camera, `${viewLabel}.camera`) };
32469
+ continue;
32470
+ }
32471
+ out[name] = { camera: validateViewCamera(view, viewLabel) };
32472
+ }
32473
+ return out;
32474
+ }
32475
+ function requireString(value, label) {
32476
+ if (typeof value !== "string" || !value.trim()) {
32477
+ throw new Error(`${label} must be a non-empty string`);
32478
+ }
32479
+ return value.trim();
32480
+ }
32481
+ function optionalString(value, label) {
32482
+ if (value === void 0) return void 0;
32483
+ return requireString(value, label);
32484
+ }
32485
+ function validateJourneyStep(step, label) {
32486
+ if (!step || typeof step !== "object" || Array.isArray(step)) {
32487
+ throw new Error(`${label} must be an object`);
32488
+ }
32489
+ const out = {
32490
+ id: requireString(step.id, `${label}.id`)
32491
+ };
32492
+ const title = optionalString(step.title, `${label}.title`);
32493
+ if (title !== void 0) out.title = title;
32494
+ const focus = optionalString(step.focus, `${label}.focus`);
32495
+ if (focus !== void 0) out.focus = focus;
32496
+ const caption = optionalString(step.caption, `${label}.caption`);
32497
+ if (caption !== void 0) out.caption = caption;
32498
+ if (step.camera !== void 0) {
32499
+ if (!step.camera || typeof step.camera !== "object" || Array.isArray(step.camera)) {
32500
+ throw new Error(`${label}.camera must be an object`);
32501
+ }
32502
+ out.camera = validateViewCamera(step.camera, `${label}.camera`);
32503
+ }
32504
+ return out;
32505
+ }
32506
+ function validateJourney(journey, label) {
32507
+ if (!journey || typeof journey !== "object" || Array.isArray(journey)) {
32508
+ throw new Error(`${label} must be an object`);
32509
+ }
32510
+ if (!Array.isArray(journey.steps) || journey.steps.length === 0) {
32511
+ throw new Error(`${label}.steps must be a non-empty array`);
32512
+ }
32513
+ const out = {
32514
+ steps: journey.steps.map((step, index) => validateJourneyStep(step, `${label}.steps[${index}]`))
32515
+ };
32516
+ const title = optionalString(journey.title, `${label}.title`);
32517
+ if (title !== void 0) out.title = title;
32518
+ const startsAt = optionalString(journey.startsAt, `${label}.startsAt`);
32519
+ if (startsAt !== void 0) out.startsAt = startsAt;
32520
+ if (journey.behavior !== void 0) {
32521
+ if (journey.behavior !== "opt-in" && journey.behavior !== "auto") {
32522
+ throw new Error(`${label}.behavior must be "opt-in" or "auto"`);
32523
+ }
32524
+ out.behavior = journey.behavior;
32525
+ }
32526
+ const seen = /* @__PURE__ */ new Set();
32527
+ for (const step of out.steps) {
32528
+ if (seen.has(step.id)) {
32529
+ throw new Error(`${label}.steps contains duplicate step id "${step.id}"`);
32530
+ }
32531
+ seen.add(step.id);
32532
+ }
32533
+ if (out.startsAt && !seen.has(out.startsAt)) {
32534
+ throw new Error(`${label}.startsAt "${out.startsAt}" does not match any step id`);
32535
+ }
32536
+ return out;
32537
+ }
32538
+ function validateJourneys(journeys, label) {
32539
+ if (!journeys || typeof journeys !== "object" || Array.isArray(journeys)) {
32540
+ throw new Error(`${label} must be an object mapping journey ids to journey configs`);
32541
+ }
32542
+ const out = {};
32543
+ for (const [id, journey] of Object.entries(journeys)) {
32544
+ const normalizedId = requireString(id, `${label} journey id`);
32545
+ out[normalizedId] = validateJourney(journey, `${label}.${normalizedId}`);
32546
+ }
32547
+ return out;
32548
+ }
32270
32549
  function validateLight(light, label) {
32271
32550
  if (!light || typeof light !== "object") throw new Error(`${label} must be an object`);
32272
32551
  if (!VALID_LIGHT_TYPES.has(light.type)) {
@@ -32410,7 +32689,18 @@ function scene(options) {
32410
32689
  if (!options || typeof options !== "object") {
32411
32690
  throw new Error("scene(options) expects an options object");
32412
32691
  }
32413
- const current = _collected$3 ? { ..._collected$3 } : { background: null, camera: null, lights: null, environment: null, fog: null, postProcessing: null, ground: null, capture: null };
32692
+ const current = _collected$3 ? { ..._collected$3 } : {
32693
+ background: null,
32694
+ camera: null,
32695
+ views: null,
32696
+ journeys: null,
32697
+ lights: null,
32698
+ environment: null,
32699
+ fog: null,
32700
+ postProcessing: null,
32701
+ ground: null,
32702
+ capture: null
32703
+ };
32414
32704
  if (options.background !== void 0) {
32415
32705
  current.background = validateBackground(options.background, "scene.background");
32416
32706
  }
@@ -32421,6 +32711,14 @@ function scene(options) {
32421
32711
  const validated = validateCamera(options.camera, "scene.camera");
32422
32712
  current.camera = current.camera ? { ...current.camera, ...validated } : validated;
32423
32713
  }
32714
+ if (options.views !== void 0) {
32715
+ const validated = validateViews(options.views, "scene.views");
32716
+ current.views = current.views ? { ...current.views, ...validated } : validated;
32717
+ }
32718
+ if (options.journeys !== void 0) {
32719
+ const validated = validateJourneys(options.journeys, "scene.journeys");
32720
+ current.journeys = current.journeys ? { ...current.journeys, ...validated } : validated;
32721
+ }
32424
32722
  if (options.lights !== void 0) {
32425
32723
  if (!Array.isArray(options.lights)) {
32426
32724
  throw new Error("scene.lights must be an array");
@@ -32461,6 +32759,74 @@ function scene(options) {
32461
32759
  }
32462
32760
  _collected$3 = current;
32463
32761
  }
32762
+ const targetPath = (target) => {
32763
+ var _a3;
32764
+ const path2 = (_a3 = target.treePath) == null ? void 0 : _a3.filter((entry) => entry.trim());
32765
+ return path2 && path2.length > 0 ? path2.join("/") : target.name;
32766
+ };
32767
+ const hasErrorDiagnostic = (diagnostics) => diagnostics.some((diagnostic) => diagnostic.level === "error");
32768
+ function resolveJourneyFocus(focus, targets) {
32769
+ return targets.filter((target) => target.name === focus || targetPath(target) === focus);
32770
+ }
32771
+ function formatAvailableTargets(targets) {
32772
+ return targets.map((target) => targetPath(target)).filter((value, index, values) => values.indexOf(value) === index).sort().slice(0, 8);
32773
+ }
32774
+ function resolveSceneJourneyTargets(config, targets) {
32775
+ if (!(config == null ? void 0 : config.journeys)) return config;
32776
+ const journeys = {};
32777
+ for (const [journeyId, journey] of Object.entries(config.journeys)) {
32778
+ const journeyDiagnostics = [...journey.diagnostics ?? []];
32779
+ const steps = journey.steps.map((step) => {
32780
+ const stepDiagnostics = [...step.diagnostics ?? []];
32781
+ let resolvedFocusId = null;
32782
+ let resolvedFocusPath = null;
32783
+ if (step.focus) {
32784
+ const matches = resolveJourneyFocus(step.focus, targets);
32785
+ if (matches.length === 1) {
32786
+ resolvedFocusId = matches[0].id;
32787
+ resolvedFocusPath = targetPath(matches[0]);
32788
+ } else if (matches.length === 0) {
32789
+ stepDiagnostics.push({
32790
+ level: "error",
32791
+ stepId: step.id,
32792
+ message: `focus "${step.focus}" did not match any returned object by name or tree path.`,
32793
+ suggestions: formatAvailableTargets(targets)
32794
+ });
32795
+ } else {
32796
+ stepDiagnostics.push({
32797
+ level: "error",
32798
+ stepId: step.id,
32799
+ message: `focus "${step.focus}" matched ${matches.length} objects. Use a slash-separated tree path.`,
32800
+ suggestions: matches.map((match) => targetPath(match))
32801
+ });
32802
+ }
32803
+ } else if (!step.camera) {
32804
+ stepDiagnostics.push({
32805
+ level: "warning",
32806
+ stepId: step.id,
32807
+ message: "step has no focus or explicit camera, so the viewer can show the caption but cannot move the camera."
32808
+ });
32809
+ }
32810
+ journeyDiagnostics.push(...stepDiagnostics);
32811
+ return {
32812
+ ...step,
32813
+ resolvedFocusId,
32814
+ resolvedFocusPath,
32815
+ diagnostics: stepDiagnostics.length > 0 ? stepDiagnostics : void 0
32816
+ };
32817
+ });
32818
+ journeys[journeyId] = {
32819
+ ...journey,
32820
+ steps,
32821
+ valid: !hasErrorDiagnostic(journeyDiagnostics),
32822
+ diagnostics: journeyDiagnostics.length > 0 ? journeyDiagnostics : void 0
32823
+ };
32824
+ }
32825
+ return {
32826
+ ...config,
32827
+ journeys
32828
+ };
32829
+ }
32464
32830
  const validateColor = (value, label) => {
32465
32831
  if (typeof value !== "string") throw new Error(`${label} must be a string`);
32466
32832
  const trimmed = value.trim();
@@ -33706,9 +34072,15 @@ function cross(a, b) {
33706
34072
  function add(a, b) {
33707
34073
  return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
33708
34074
  }
34075
+ function sub$1(a, b) {
34076
+ return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
34077
+ }
33709
34078
  function scale(v, s) {
33710
34079
  return [v[0] * s, v[1] * s, v[2] * s];
33711
34080
  }
34081
+ function dot$1(a, b) {
34082
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
34083
+ }
33712
34084
  function lerp$1(a, b, t) {
33713
34085
  return a + (b - a) * t;
33714
34086
  }
@@ -33741,6 +34113,65 @@ function sideVectors(axis) {
33741
34113
  function cloneQuery(query) {
33742
34114
  return { side: query.side, u: query.u, v: query.v, offset: query.offset };
33743
34115
  }
34116
+ function normalizedSide(side) {
34117
+ return side === "back" ? "rear" : side;
34118
+ }
34119
+ function profileExponent(station) {
34120
+ if (station.profile.kind === "superEllipse") return station.profile.exponent ?? 3.2;
34121
+ if (station.profile.kind === "roundedRect") return 4.5;
34122
+ return 2;
34123
+ }
34124
+ function superEllipsePoint(rx, ry, exponent, angle) {
34125
+ const cos2 = Math.cos(angle);
34126
+ const sin2 = Math.sin(angle);
34127
+ const x = rx * Math.sign(cos2) * Math.abs(cos2) ** (2 / exponent);
34128
+ const y = ry * Math.sign(sin2) * Math.abs(sin2) ** (2 / exponent);
34129
+ const nx = Math.sign(x) * Math.abs(x / Math.max(rx, EPS$4)) ** Math.max(exponent - 1, 1e-3);
34130
+ const ny = Math.sign(y) * Math.abs(y / Math.max(ry, EPS$4)) ** Math.max(exponent - 1, 1e-3);
34131
+ const nLen = Math.hypot(nx, ny);
34132
+ return {
34133
+ point: [x, y],
34134
+ normal: nLen < EPS$4 ? [Math.sign(cos2), Math.sign(sin2)] : [nx / nLen, ny / nLen]
34135
+ };
34136
+ }
34137
+ function angleForSide(side, u) {
34138
+ const t = clamp$4(u, 0, 1);
34139
+ if (side === "right") return -Math.PI / 2 + t * Math.PI;
34140
+ if (side === "left") return Math.PI / 2 + t * Math.PI;
34141
+ if (side === "top") return Math.PI - t * Math.PI;
34142
+ if (side === "bottom") return Math.PI + t * Math.PI;
34143
+ return null;
34144
+ }
34145
+ function sideSpan(side, width, depth) {
34146
+ if (side === "left" || side === "right") return Math.max(depth, EPS$4);
34147
+ if (side === "top" || side === "bottom") return Math.max(width, EPS$4);
34148
+ return Math.max(width, depth, EPS$4);
34149
+ }
34150
+ function interpolateQuery(a, b, t) {
34151
+ const sideA = normalizedSide(a.side);
34152
+ const sideB = normalizedSide(b.side);
34153
+ if (sideA !== sideB) {
34154
+ throw new Error(
34155
+ `Product.ribbon().on(...) currently samples one skin side per ribbon; got '${a.side}' then '${b.side}'. Split this into separate ribbons at the side transition.`
34156
+ );
34157
+ }
34158
+ return {
34159
+ side: sideA,
34160
+ u: lerp$1(a.u ?? 0.5, b.u ?? 0.5, t),
34161
+ v: lerp$1(a.v ?? 0.5, b.v ?? 0.5, t),
34162
+ offset: lerp$1(a.offset ?? 0, b.offset ?? 0, t)
34163
+ };
34164
+ }
34165
+ function resolvePathQueries(points) {
34166
+ return points.map((point2) => point2 instanceof ProductSurfaceRef ? point2.querySpec() : cloneQuery(point2));
34167
+ }
34168
+ function orientGridToNormal(grid, desiredNormal) {
34169
+ if (grid.length < 2 || grid[0].length < 2) return grid;
34170
+ const widthEdge = sub$1(grid[grid.length - 1][0], grid[0][0]);
34171
+ const lengthEdge = sub$1(grid[0][grid[0].length - 1], grid[0][0]);
34172
+ const actual = norm(cross(widthEdge, lengthEdge));
34173
+ return dot$1(actual, desiredNormal) < 0 ? [...grid].reverse() : grid;
34174
+ }
33744
34175
  function isStationBuilder(input) {
33745
34176
  return typeof input.toSpec === "function";
33746
34177
  }
@@ -33842,6 +34273,15 @@ class ProductSkin {
33842
34273
  curveOnSurface(name, points) {
33843
34274
  return points.map((point2, index) => new ProductSurfaceRef(this, { u: 0.5, v: 0.5, ...point2 }, `${name}/${index}`));
33844
34275
  }
34276
+ /**
34277
+ * Create a fluent surface helper for refs and conformal features on one side of this skin.
34278
+ *
34279
+ * Use this when several refs or ribbons share the same skin side; side-local helpers keep
34280
+ * path points concise and make it harder to mix sides accidentally.
34281
+ */
34282
+ surface(side) {
34283
+ return new ProductSurfaceBuilder(this, side);
34284
+ }
33845
34285
  /** Interpolate center, width, and depth at a normalized v or absolute axis value. */
33846
34286
  stationAt(vOrAxis) {
33847
34287
  const axisValue = vOrAxis >= 0 && vOrAxis <= 1 ? lerp$1(this.axisMin, this.axisMax, vOrAxis) : clamp$4(vOrAxis, this.axisMin, this.axisMax);
@@ -33860,7 +34300,9 @@ class ProductSkin {
33860
34300
  width: lerp$1(a.profile.width, b.profile.width, t),
33861
34301
  depth: lerp$1(a.profile.depth, b.profile.depth, t),
33862
34302
  dWidth: (b.profile.width - a.profile.width) / span,
33863
- dDepth: (b.profile.depth - a.profile.depth) / span
34303
+ dDepth: (b.profile.depth - a.profile.depth) / span,
34304
+ exponent: lerp$1(profileExponent(a), profileExponent(b), t),
34305
+ kind: a.profile.kind === b.profile.kind ? a.profile.kind : "custom"
33864
34306
  };
33865
34307
  }
33866
34308
  const last = sorted[sorted.length - 1];
@@ -33870,12 +34312,14 @@ class ProductSkin {
33870
34312
  width: last.profile.width,
33871
34313
  depth: last.profile.depth,
33872
34314
  dWidth: 0,
33873
- dDepth: 0
34315
+ dDepth: 0,
34316
+ exponent: profileExponent(last),
34317
+ kind: last.profile.kind
33874
34318
  };
33875
34319
  }
33876
34320
  /** Build a local surface frame from a side/u/v query. */
33877
34321
  frame(query) {
33878
- const side = query.side === "back" ? "rear" : query.side;
34322
+ const side = normalizedSide(query.side);
33879
34323
  const offset2 = query.offset ?? 0;
33880
34324
  const basis = sideVectors(this.axis);
33881
34325
  const isFrontCap = side === "front";
@@ -33899,11 +34343,31 @@ class ProductSkin {
33899
34343
  }
33900
34344
  const station = this.stationAt(query.v ?? 0.5);
33901
34345
  const u = clamp$4(query.u ?? 0.5, 0, 1) - 0.5;
34346
+ const sideAngle = angleForSide(side, query.u ?? 0.5);
33902
34347
  let crossA = 0;
33903
34348
  let crossB = 0;
33904
34349
  let normal = [0, 0, 1];
33905
34350
  let tangentU = basis.crossA;
33906
- if (side === "right") {
34351
+ if (sideAngle != null) {
34352
+ const section = superEllipsePoint(station.width / 2, station.depth / 2, station.exponent, sideAngle);
34353
+ crossA = section.point[0];
34354
+ crossB = section.point[1];
34355
+ normal = norm(add(scale(basis.crossA, section.normal[0]), scale(basis.crossB, section.normal[1])));
34356
+ const delta = 2e-3;
34357
+ const prev = superEllipsePoint(
34358
+ station.width / 2,
34359
+ station.depth / 2,
34360
+ station.exponent,
34361
+ angleForSide(side, clamp$4((query.u ?? 0.5) - delta, 0, 1)) ?? sideAngle
34362
+ ).point;
34363
+ const next = superEllipsePoint(
34364
+ station.width / 2,
34365
+ station.depth / 2,
34366
+ station.exponent,
34367
+ angleForSide(side, clamp$4((query.u ?? 0.5) + delta, 0, 1)) ?? sideAngle
34368
+ ).point;
34369
+ tangentU = norm(add(scale(basis.crossA, next[0] - prev[0]), scale(basis.crossB, next[1] - prev[1])));
34370
+ } else if (side === "right") {
33907
34371
  crossA = station.width / 2;
33908
34372
  crossB = u * station.depth;
33909
34373
  normal = basis.crossA;
@@ -33925,7 +34389,7 @@ class ProductSkin {
33925
34389
  tangentU = basis.crossA;
33926
34390
  }
33927
34391
  normal = norm(normal);
33928
- tangentU = norm(tangentU);
34392
+ tangentU = norm(sub$1(tangentU, scale(normal, dot$1(tangentU, normal))));
33929
34393
  const tangentV = norm(cross(normal, tangentU));
33930
34394
  const point2 = add(add(station.center, add(scale(basis.crossA, crossA), scale(basis.crossB, crossB))), scale(normal, offset2));
33931
34395
  return {
@@ -34245,6 +34709,303 @@ class ProductHandleBuilder {
34245
34709
  return new ProductHandleFeature(grip, upperPad, lowerPad);
34246
34710
  }
34247
34711
  }
34712
+ class ProductSurfaceBuilder {
34713
+ constructor(skin, side) {
34714
+ this.skin = skin;
34715
+ this.side = side;
34716
+ }
34717
+ /** Create a ref on this skin side. */
34718
+ ref(u = 0.5, v = 0.5, offset2) {
34719
+ return Product.ref(this.skin, {
34720
+ side: this.side,
34721
+ u,
34722
+ v,
34723
+ ...offset2 != null ? { offset: offset2 } : {}
34724
+ });
34725
+ }
34726
+ /** Create a side/u/v query on this skin side. */
34727
+ uv(u = 0.5, v = 0.5, offset2) {
34728
+ return {
34729
+ side: this.side,
34730
+ u,
34731
+ v,
34732
+ ...offset2 != null ? { offset: offset2 } : {}
34733
+ };
34734
+ }
34735
+ /**
34736
+ * Start a conformal ribbon on this skin side.
34737
+ *
34738
+ * Path points use side-local `u`/`v` coordinates; this builder supplies the side.
34739
+ * The returned ProductRibbonBuilder is already bound to the source skin and can be further
34740
+ * configured before build(). Use `widthSamples` >= 3 when the ribbon must visibly wrap over
34741
+ * curved product sections instead of behaving like a flat strip.
34742
+ */
34743
+ ribbon(name, points, options = {}) {
34744
+ if (points.length < 2) throw new Error("Product.surface(...).ribbon(name, points) requires at least two path points");
34745
+ const path2 = points.map((point2) => ({
34746
+ side: this.side,
34747
+ u: point2.u ?? 0.5,
34748
+ v: point2.v ?? 0.5,
34749
+ ...point2.offset != null ? { offset: point2.offset } : {}
34750
+ }));
34751
+ return new ProductRibbonBuilder(name).on(this.skin, path2, options);
34752
+ }
34753
+ }
34754
+ class ProductRibbonBuilder {
34755
+ constructor(name) {
34756
+ __publicField(this, "skinValue");
34757
+ __publicField(this, "queryPath", []);
34758
+ __publicField(this, "refPath", []);
34759
+ __publicField(this, "widthValue", 6);
34760
+ __publicField(this, "thicknessValue", 0.8);
34761
+ __publicField(this, "offsetValue", 0.25);
34762
+ __publicField(this, "samplesValue", 24);
34763
+ __publicField(this, "widthSamplesValue", 5);
34764
+ __publicField(this, "resolutionValue");
34765
+ __publicField(this, "materialValue");
34766
+ __publicField(this, "colorValue");
34767
+ __publicField(this, "lastDiagnosticsValue");
34768
+ this.name = name;
34769
+ if (!name || !name.trim()) throw new Error("Product.ribbon(name) requires a non-empty name");
34770
+ }
34771
+ /**
34772
+ * Follow a ProductSkin with side/u/v path queries or refs.
34773
+ *
34774
+ * This is the highest-fidelity mode because every interpolated sample is resolved through
34775
+ * ProductSkin.frame(), so the ribbon bends along the selected side as station width/depth changes.
34776
+ * All query path points must stay on one side; split side transitions into separate ribbons.
34777
+ */
34778
+ on(skin, points, options = {}) {
34779
+ if (points.length < 2) throw new Error("Product.ribbon().on(skin, points) requires at least two path points");
34780
+ this.skinValue = skin;
34781
+ this.queryPath = resolvePathQueries(points);
34782
+ this.refPath = [];
34783
+ return this.applyOptions(options);
34784
+ }
34785
+ /**
34786
+ * Follow explicit surface refs.
34787
+ *
34788
+ * Useful for named refs or paths assembled elsewhere. The builder resolves each ref frame and
34789
+ * interpolates between those frames; use on(skin, points) when you need full skin-side sampling
34790
+ * between sparse control points.
34791
+ */
34792
+ fromRefs(points, options = {}) {
34793
+ if (points.length < 2) throw new Error("Product.ribbon().fromRefs(points) requires at least two refs");
34794
+ this.skinValue = void 0;
34795
+ this.queryPath = [];
34796
+ this.refPath = [...points];
34797
+ return this.applyOptions(options);
34798
+ }
34799
+ /** Set ribbon width in millimeters. */
34800
+ width(width) {
34801
+ if (!Number.isFinite(width) || width <= 0) throw new Error("Product.ribbon().width(width) requires a positive finite number");
34802
+ this.widthValue = width;
34803
+ return this;
34804
+ }
34805
+ /** Set solid thickness outward from the source surface in millimeters. */
34806
+ thickness(thickness) {
34807
+ if (!Number.isFinite(thickness) || thickness <= 0)
34808
+ throw new Error("Product.ribbon().thickness(thickness) requires a positive finite number");
34809
+ this.thicknessValue = thickness;
34810
+ return this;
34811
+ }
34812
+ /** Set positive clearance between the source surface and the ribbon's inner face. */
34813
+ offset(offset2) {
34814
+ if (!Number.isFinite(offset2)) throw new Error("Product.ribbon().offset(offset) requires a finite number");
34815
+ this.offsetValue = offset2;
34816
+ return this;
34817
+ }
34818
+ /** Set samples along the path. */
34819
+ samples(samples) {
34820
+ if (!Number.isFinite(samples) || samples < 2) throw new Error("Product.ribbon().samples(samples) requires a value >= 2");
34821
+ this.samplesValue = Math.round(samples);
34822
+ return this;
34823
+ }
34824
+ /** Set samples across the width. Use 3+ to bend over curved cross-sections. */
34825
+ widthSamples(samples) {
34826
+ if (!Number.isFinite(samples) || samples < 2) throw new Error("Product.ribbon().widthSamples(samples) requires a value >= 2");
34827
+ this.widthSamplesValue = Math.round(samples);
34828
+ return this;
34829
+ }
34830
+ /** Set NURBS tessellation resolution. */
34831
+ resolution(resolution) {
34832
+ if (!Number.isFinite(resolution) || resolution < 2) throw new Error("Product.ribbon().resolution(resolution) requires a value >= 2");
34833
+ this.resolutionValue = Math.round(resolution);
34834
+ return this;
34835
+ }
34836
+ /** Apply a product material preset. */
34837
+ material(material) {
34838
+ this.materialValue = material;
34839
+ return this;
34840
+ }
34841
+ /** Apply a simple color override. */
34842
+ color(color) {
34843
+ this.colorValue = color;
34844
+ return this;
34845
+ }
34846
+ /** Build a conformal ribbon as a thin NURBS surface solid. */
34847
+ build(options = {}) {
34848
+ return this.buildWithDiagnostics(options).shape;
34849
+ }
34850
+ /**
34851
+ * Build a conformal ribbon and return surface-feature diagnostics.
34852
+ *
34853
+ * Use this while validating API usage or model fidelity; diagnostics report sampling counts,
34854
+ * side-span clamping, lowering mode, and warnings that should be visible in reviews.
34855
+ */
34856
+ buildWithDiagnostics(options = {}) {
34857
+ this.applyOptions(options);
34858
+ const gridResult = this.skinValue ? this.buildSkinGrid(this.skinValue, this.queryPath) : this.buildRefGrid(this.refPath);
34859
+ const desiredNormal = this.centerDesiredNormal();
34860
+ let ribbon = nurbsSurface(orientGridToNormal(gridResult.grid, desiredNormal), {
34861
+ degreeU: Math.min(3, this.widthSamplesValue - 1),
34862
+ degreeV: Math.min(3, this.samplesValue - 1),
34863
+ thickness: this.thicknessValue,
34864
+ resolution: this.resolutionValue ?? Math.max(this.samplesValue, this.widthSamplesValue, 12),
34865
+ approximate: true
34866
+ }).as(this.name);
34867
+ if (this.colorValue) ribbon = ribbon.color(this.colorValue);
34868
+ const shape = applyMaterial(ribbon, this.materialValue);
34869
+ this.lastDiagnosticsValue = gridResult.diagnostics;
34870
+ return { shape, diagnostics: this.cloneDiagnostics(gridResult.diagnostics) };
34871
+ }
34872
+ /** Return diagnostics from the most recent build, if this builder has been built. */
34873
+ diagnostics() {
34874
+ return this.lastDiagnosticsValue ? this.cloneDiagnostics(this.lastDiagnosticsValue) : void 0;
34875
+ }
34876
+ applyOptions(options) {
34877
+ if (options.width != null) this.width(options.width);
34878
+ if (options.thickness != null) this.thickness(options.thickness);
34879
+ if (options.offset != null) this.offset(options.offset);
34880
+ if (options.samples != null) this.samples(options.samples);
34881
+ if (options.widthSamples != null) this.widthSamples(options.widthSamples);
34882
+ if (options.resolution != null) this.resolution(options.resolution);
34883
+ if (options.material) this.material(options.material);
34884
+ if (options.color) this.color(options.color);
34885
+ return this;
34886
+ }
34887
+ centerDesiredNormal() {
34888
+ if (this.skinValue && this.queryPath.length > 0) {
34889
+ const mid = this.samplePathQuery(0.5);
34890
+ return this.skinValue.frame({ ...mid, offset: (mid.offset ?? 0) + this.offsetValue }).normal;
34891
+ }
34892
+ if (this.refPath.length > 0) return this.refPath[Math.floor(this.refPath.length / 2)].frame({ offset: this.offsetValue }).normal;
34893
+ return [0, 0, 1];
34894
+ }
34895
+ samplePathQuery(t) {
34896
+ if (this.queryPath.length < 2) throw new Error("Product.ribbon().on(...) must be called before .build()");
34897
+ const segmentCount = this.queryPath.length - 1;
34898
+ const scaled = clamp$4(t, 0, 1) * segmentCount;
34899
+ const segment = Math.min(segmentCount - 1, Math.floor(scaled));
34900
+ const localT = scaled - segment;
34901
+ return interpolateQuery(this.queryPath[segment], this.queryPath[segment + 1], localT);
34902
+ }
34903
+ buildSkinGrid(skin, path2) {
34904
+ if (path2.length < 2) throw new Error("Product.ribbon().on(skin, points) must be called before .build()");
34905
+ const side = normalizedSide(path2[0].side);
34906
+ if (side === "front" || side === "rear") {
34907
+ throw new Error(
34908
+ "Product.ribbon().on(...) supports side ribbons on left/right/top/bottom surfaces. Use Product.panel() for front/rear caps."
34909
+ );
34910
+ }
34911
+ for (const point2 of path2) {
34912
+ if (normalizedSide(point2.side) !== side) {
34913
+ throw new Error("Product.ribbon().on(...) currently supports one side per ribbon. Split ribbons at side transitions.");
34914
+ }
34915
+ }
34916
+ const rows = Array.from({ length: this.widthSamplesValue }, () => []);
34917
+ let clampedUCount = 0;
34918
+ let maxUClampDistance = 0;
34919
+ for (let i = 0; i < this.samplesValue; i += 1) {
34920
+ const along = this.samplesValue === 1 ? 0 : i / (this.samplesValue - 1);
34921
+ const center = this.samplePathQuery(along);
34922
+ const station = skin.stationAt(center.v ?? 0.5);
34923
+ const span = sideSpan(side, station.width, station.depth);
34924
+ for (let j = 0; j < this.widthSamplesValue; j += 1) {
34925
+ const across = this.widthSamplesValue === 1 ? 0 : j / (this.widthSamplesValue - 1) - 0.5;
34926
+ const rawU = (center.u ?? 0.5) + across * this.widthValue / span;
34927
+ const u = clamp$4(rawU, 0, 1);
34928
+ const clampDistance = Math.abs(rawU - u) * span;
34929
+ if (clampDistance > EPS$4) {
34930
+ clampedUCount += 1;
34931
+ maxUClampDistance = Math.max(maxUClampDistance, clampDistance);
34932
+ }
34933
+ const query = {
34934
+ ...center,
34935
+ side,
34936
+ u,
34937
+ offset: (center.offset ?? 0) + this.offsetValue + this.thicknessValue
34938
+ };
34939
+ rows[j].push(skin.frame(query).point);
34940
+ }
34941
+ }
34942
+ return {
34943
+ grid: rows,
34944
+ diagnostics: this.makeDiagnostics({
34945
+ skin: skin.name,
34946
+ side,
34947
+ pathPointCount: path2.length,
34948
+ clampedUCount,
34949
+ maxUClampDistance
34950
+ })
34951
+ };
34952
+ }
34953
+ buildRefGrid(refs) {
34954
+ if (refs.length < 2) throw new Error("Product.ribbon().fromRefs(points) must be called before .build()");
34955
+ const frames = refs.map((ref) => ref.frame({ offset: this.offsetValue + this.thicknessValue }));
34956
+ const rows = Array.from({ length: this.widthSamplesValue }, () => []);
34957
+ for (const frame of frames) {
34958
+ for (let j = 0; j < this.widthSamplesValue; j += 1) {
34959
+ const across = this.widthSamplesValue === 1 ? 0 : j / (this.widthSamplesValue - 1) - 0.5;
34960
+ rows[j].push(add(frame.point, scale(frame.tangentU, across * this.widthValue)));
34961
+ }
34962
+ }
34963
+ this.samplesValue = refs.length;
34964
+ return {
34965
+ grid: rows,
34966
+ diagnostics: this.makeDiagnostics({
34967
+ pathPointCount: refs.length,
34968
+ clampedUCount: 0,
34969
+ maxUClampDistance: 0
34970
+ })
34971
+ };
34972
+ }
34973
+ makeDiagnostics(input) {
34974
+ const resolution = this.resolutionValue ?? Math.max(this.samplesValue, this.widthSamplesValue, 12);
34975
+ const warnings = [];
34976
+ if (input.clampedUCount > 0) {
34977
+ warnings.push(
34978
+ `Ribbon '${this.name}' was clipped to the ${input.side ?? "surface"} UV bounds at ${input.clampedUCount} sampled point(s).`
34979
+ );
34980
+ }
34981
+ if (this.samplesValue < 8) warnings.push(`Ribbon '${this.name}' uses low along-path sampling; increase samples() for smoother bends.`);
34982
+ if (this.widthSamplesValue < 3)
34983
+ warnings.push(`Ribbon '${this.name}' uses low width sampling; use widthSamples(3+) to show cross-surface curvature.`);
34984
+ return {
34985
+ name: this.name,
34986
+ ...input.skin ? { skin: input.skin } : {},
34987
+ ...input.side ? { side: input.side } : {},
34988
+ pathPointCount: input.pathPointCount,
34989
+ width: this.widthValue,
34990
+ thickness: this.thicknessValue,
34991
+ offset: this.offsetValue,
34992
+ samples: this.samplesValue,
34993
+ widthSamples: this.widthSamplesValue,
34994
+ resolution,
34995
+ lowering: "nurbsSurface",
34996
+ expectedFidelity: "mixed",
34997
+ clampedUCount: input.clampedUCount,
34998
+ maxUClampDistance: input.maxUClampDistance,
34999
+ warnings
35000
+ };
35001
+ }
35002
+ cloneDiagnostics(diagnostics) {
35003
+ return {
35004
+ ...diagnostics,
35005
+ warnings: [...diagnostics.warnings]
35006
+ };
35007
+ }
35008
+ }
34248
35009
  const Product = {
34249
35010
  /** Start a named product skin builder. */
34250
35011
  skin(name) {
@@ -34317,10 +35078,27 @@ const Product = {
34317
35078
  ref(skin, query) {
34318
35079
  return new ProductSurfaceRef(skin, query);
34319
35080
  },
35081
+ /**
35082
+ * Create a fluent surface helper for refs and conformal features on one side of a skin.
35083
+ *
35084
+ * Equivalent to skin.surface(side), useful when writing in Product.* namespace style.
35085
+ */
35086
+ surface(skin, side) {
35087
+ return skin.surface(side);
35088
+ },
34320
35089
  /** Start a panel feature builder. */
34321
35090
  panel(name) {
34322
35091
  return new ProductPanelBuilder(name);
34323
35092
  },
35093
+ /**
35094
+ * Start a conformal ribbon/trim builder for details that should bend with a ProductSkin.
35095
+ *
35096
+ * Call .on(skin, points) for side/u/v sampling or .fromRefs(points) for explicit surface refs,
35097
+ * then configure width, thickness, offset, sampling, material, and color before build().
35098
+ */
35099
+ ribbon(name) {
35100
+ return new ProductRibbonBuilder(name);
35101
+ },
34324
35102
  /** Start a spout/nozzle feature builder. */
34325
35103
  spout(name) {
34326
35104
  return new ProductSpoutBuilder(name);
@@ -35367,6 +36145,7 @@ function cameraTrajectory(defOrFn, options) {
35367
36145
  throw new Error('cameraTrajectory(): each keyframe must have either an "orbit" or "position" property');
35368
36146
  }
35369
36147
  }
36148
+ var define_process_env_default = {};
35370
36149
  function resolveEdges(shape, edges) {
35371
36150
  if (!edges) {
35372
36151
  return selectEdges(shape);
@@ -35388,6 +36167,76 @@ function isEdgeSegment(value) {
35388
36167
  function isEdgeReferenceLike(value) {
35389
36168
  return typeof value === "object" && value !== null && "edges" in value && typeof value.edges === "function";
35390
36169
  }
36170
+ const BROAD_EDGE_FEATURE_DEFAULT_BUDGET = {
36171
+ live: 0,
36172
+ default: 12,
36173
+ high: Number.POSITIVE_INFINITY
36174
+ };
36175
+ let broadEdgeFeatureBudget = null;
36176
+ function readBroadEdgeFeatureEnv(name) {
36177
+ return typeof process !== "undefined" ? define_process_env_default == null ? void 0 : define_process_env_default[name] : void 0;
36178
+ }
36179
+ function resolveBroadEdgeFeatureBudget() {
36180
+ if (readBroadEdgeFeatureEnv("FORGECAD_ALLOW_BROAD_EDGE_FEATURES") === "1") return Number.POSITIVE_INFINITY;
36181
+ const override = readBroadEdgeFeatureEnv("FORGECAD_BROAD_EDGE_FEATURE_BUDGET");
36182
+ if (override != null && override.trim() !== "") {
36183
+ const parsed = Number(override);
36184
+ if (Number.isFinite(parsed) && parsed >= 0) return parsed;
36185
+ }
36186
+ return BROAD_EDGE_FEATURE_DEFAULT_BUDGET[getForgeQualityPreset()];
36187
+ }
36188
+ function resetBroadEdgeFeatureBudget() {
36189
+ broadEdgeFeatureBudget = null;
36190
+ }
36191
+ function remainingBroadEdgeFeatureBudget() {
36192
+ if (broadEdgeFeatureBudget === null) broadEdgeFeatureBudget = resolveBroadEdgeFeatureBudget();
36193
+ return broadEdgeFeatureBudget;
36194
+ }
36195
+ function consumeBroadEdgeFeatureBudget(edgeCount) {
36196
+ const remaining = remainingBroadEdgeFeatureBudget();
36197
+ if (!Number.isFinite(remaining)) return true;
36198
+ if (edgeCount > remaining) return false;
36199
+ broadEdgeFeatureBudget = Math.max(0, remaining - edgeCount);
36200
+ return true;
36201
+ }
36202
+ function shouldSkipBroadEdgeFeature(operation, edgeCount) {
36203
+ if (consumeBroadEdgeFeatureBudget(edgeCount)) return false;
36204
+ const remaining = Math.max(0, remainingBroadEdgeFeatureBudget());
36205
+ emitRuntimeWarning(
36206
+ `${operation}() without an edge selector matched ${edgeCount} edge(s), exceeding the remaining broad edge-feature budget (${remaining}). Skipped this cosmetic edge finish for responsiveness. Pass an explicit edge selector, use high quality/export, or set FORGECAD_BROAD_EDGE_FEATURE_BUDGET to opt into more broad edge finishing.`
36207
+ );
36208
+ return true;
36209
+ }
36210
+ function shouldSkipExhaustedBroadEdgeFeature(operation) {
36211
+ const remaining = remainingBroadEdgeFeatureBudget();
36212
+ if (!Number.isFinite(remaining) || remaining > 0) return false;
36213
+ emitRuntimeWarning(
36214
+ `${operation}() without an edge selector was skipped because the broad edge-feature budget is exhausted. Skipped this cosmetic edge finish for responsiveness. Pass an explicit edge selector, use high quality/export, or set FORGECAD_BROAD_EDGE_FEATURE_BUDGET to opt into more broad edge finishing.`
36215
+ );
36216
+ return true;
36217
+ }
36218
+ function estimateSelectorlessEdgeCount(plan) {
36219
+ if (!plan) return null;
36220
+ switch (plan.kind) {
36221
+ case "box":
36222
+ return 12;
36223
+ case "queryOwner":
36224
+ case "transform":
36225
+ return estimateSelectorlessEdgeCount(plan.base);
36226
+ default:
36227
+ return null;
36228
+ }
36229
+ }
36230
+ function shouldSkipUnestimatedBroadEdgeFeature(operation, target) {
36231
+ const remaining = remainingBroadEdgeFeatureBudget();
36232
+ if (!Number.isFinite(remaining)) return false;
36233
+ const estimatedEdges = estimateSelectorlessEdgeCount(getShapeCompilePlan(target));
36234
+ if (estimatedEdges !== null && estimatedEdges <= remaining) return false;
36235
+ emitRuntimeWarning(
36236
+ `${operation}() without an edge selector was skipped before broad edge enumeration because this shape is too complex for the remaining broad edge-feature budget (${Math.max(0, remaining)}). Skipped this cosmetic edge finish for responsiveness. Pass an explicit edge selector, use high quality/export, or set FORGECAD_BROAD_EDGE_FEATURE_BUDGET to opt into more broad edge finishing.`
36237
+ );
36238
+ return true;
36239
+ }
35391
36240
  function edgesToTargets(edges) {
35392
36241
  return edges.map((e) => ({
35393
36242
  midpoint: [e.midpoint[0], e.midpoint[1], e.midpoint[2]],
@@ -35401,10 +36250,13 @@ function fillet(shape, radius, edges, segments = 16) {
35401
36250
  throw new Error("fillet() requires a positive finite radius.");
35402
36251
  }
35403
36252
  const target = shape;
36253
+ if (edges === void 0 && shouldSkipExhaustedBroadEdgeFeature("fillet")) return target;
36254
+ if (edges === void 0 && shouldSkipUnestimatedBroadEdgeFeature("fillet", target)) return target;
35404
36255
  const resolvedEdges = resolveEdges(target, edges);
35405
36256
  if (resolvedEdges.length === 0) {
35406
36257
  throw new Error("fillet(): no edges match the given selection.");
35407
36258
  }
36259
+ if (edges === void 0 && shouldSkipBroadEdgeFeature("fillet", resolvedEdges.length)) return target;
35408
36260
  const basePlan = getShapeCompilePlan(target);
35409
36261
  const plan = {
35410
36262
  kind: "filletEdges",
@@ -35424,10 +36276,13 @@ function chamfer(shape, size, edges) {
35424
36276
  throw new Error("chamfer() requires a positive finite size.");
35425
36277
  }
35426
36278
  const target = shape;
36279
+ if (edges === void 0 && shouldSkipExhaustedBroadEdgeFeature("chamfer")) return target;
36280
+ if (edges === void 0 && shouldSkipUnestimatedBroadEdgeFeature("chamfer", target)) return target;
35427
36281
  const resolvedEdges = resolveEdges(target, edges);
35428
36282
  if (resolvedEdges.length === 0) {
35429
36283
  throw new Error("chamfer(): no edges match the given selection.");
35430
36284
  }
36285
+ if (edges === void 0 && shouldSkipBroadEdgeFeature("chamfer", resolvedEdges.length)) return target;
35431
36286
  const basePlan = getShapeCompilePlan(target);
35432
36287
  const plan = {
35433
36288
  kind: "chamferEdges",
@@ -58115,6 +58970,7 @@ const verify = {
58115
58970
  };
58116
58971
  function resetExecutionSession(logs) {
58117
58972
  resetCollectedAssemblies();
58973
+ resetBroadEdgeFeatureBudget();
58118
58974
  resetParams();
58119
58975
  resetShapeQueryOwnerIds();
58120
58976
  resetDimensions();
@@ -58123,6 +58979,7 @@ function resetExecutionSession(logs) {
58123
58979
  resetSheetStock();
58124
58980
  resetRobotExport();
58125
58981
  resetCutPlanes();
58982
+ resetRenderLabels();
58126
58983
  resetCameraTrajectory();
58127
58984
  resetExplodeView();
58128
58985
  resetJointsView();
@@ -58186,6 +59043,7 @@ function collectSuccessfulExecutionSnapshot(args) {
58186
59043
  bom: getCollectedBom(),
58187
59044
  sheetStock: getCollectedSheetStock(),
58188
59045
  cutPlanes: getCollectedCutPlanes(),
59046
+ renderLabels: getCollectedRenderLabels(),
58189
59047
  cameraTrajectory: getCollectedCameraTrajectory(),
58190
59048
  explodeView: getCollectedExplodeView(),
58191
59049
  jointsView: getCollectedJointsView(),
@@ -58209,6 +59067,7 @@ function collectFailedExecutionSnapshot(args) {
58209
59067
  bom: getCollectedBom(),
58210
59068
  sheetStock: getCollectedSheetStock(),
58211
59069
  cutPlanes: getCollectedCutPlanes(),
59070
+ renderLabels: getCollectedRenderLabels(),
58212
59071
  cameraTrajectory: getCollectedCameraTrajectory(),
58213
59072
  explodeView: getCollectedExplodeView(),
58214
59073
  jointsView: getCollectedJointsView(),
@@ -58354,13 +59213,15 @@ function formatLogArg(value) {
58354
59213
  return `[Log serialization failed: ${formatLogError(error)}]`;
58355
59214
  }
58356
59215
  }
58357
- function makeSandboxConsole(collectedLogs) {
59216
+ function makeSandboxConsole(collectedLogs, mirror) {
58358
59217
  const capture = (level) => (...args) => {
59218
+ const formattedArgs = args.map(formatLogArg);
58359
59219
  collectedLogs.push({
58360
59220
  level,
58361
- args: args.map(formatLogArg),
59221
+ args: formattedArgs,
58362
59222
  timestamp: Date.now()
58363
59223
  });
59224
+ mirror == null ? void 0 : mirror(level, formattedArgs);
58364
59225
  };
58365
59226
  return { log: capture("log"), warn: capture("warn"), error: capture("error"), info: capture("info") };
58366
59227
  }
@@ -58522,6 +59383,18 @@ function buildFileIndex(allFiles) {
58522
59383
  }
58523
59384
  return fileIndex;
58524
59385
  }
59386
+ function hasPathExtension(path2) {
59387
+ const fileName = path2.split("/").pop() ?? path2;
59388
+ return /\.[^/.]+$/.test(fileName);
59389
+ }
59390
+ function explicitExtensionHint(requestedName, resolvedPath, fileIndex) {
59391
+ if (!resolvedPath || hasPathExtension(resolvedPath)) return "";
59392
+ const requestedBase = requestedName.trim();
59393
+ const suggestions = [".forge.js", ".js"].map((ext) => ({ ext, resolved: `${resolvedPath}${ext}` })).filter(({ resolved }) => fileIndex.has(resolved)).map(({ ext }) => `"${requestedBase}${ext}"`);
59394
+ if (suggestions.length === 0) return "";
59395
+ const joined = suggestions.length === 1 ? suggestions[0] : suggestions.join(" or ");
59396
+ return ` Did you mean ${joined}? ForgeCAD requires explicit file extensions in project imports.`;
59397
+ }
58525
59398
  function resolveImportSource(fromFile, requestedName, allFiles, options) {
58526
59399
  if (typeof requestedName !== "string" || requestedName.trim().length === 0) {
58527
59400
  throw new Error("Import path must be a non-empty string");
@@ -58530,7 +59403,8 @@ function resolveImportSource(fromFile, requestedName, allFiles, options) {
58530
59403
  const lookupKey = options.fileIndex.get(resolvedPath);
58531
59404
  if (!lookupKey) {
58532
59405
  const suffix = resolvedPath && resolvedPath !== requestedName ? ` (resolved to "${resolvedPath}" from "${fromFile}")` : ` (from "${fromFile}")`;
58533
- throw new Error(`File not found: "${requestedName}"${suffix}`);
59406
+ const hint = explicitExtensionHint(requestedName, resolvedPath, options.fileIndex);
59407
+ throw new Error(`File not found: "${requestedName}"${suffix}.${hint}`);
58534
59408
  }
58535
59409
  const source = allFiles[lookupKey];
58536
59410
  if (typeof source !== "string") {
@@ -112836,7 +113710,7 @@ ${lanes.join("\n")}
112836
113710
  }
112837
113711
  const normalizedSourcePaths = getPathsRelativeToRootDirs(sourceDirectory, rootDirs, getCanonicalFileName);
112838
113712
  const relativePaths = flatMap(normalizedSourcePaths, (sourcePath) => {
112839
- return map(normalizedTargetPaths, (targetPath) => ensurePathIsNonModuleName(getRelativePathFromDirectory(sourcePath, targetPath, getCanonicalFileName)));
113713
+ return map(normalizedTargetPaths, (targetPath2) => ensurePathIsNonModuleName(getRelativePathFromDirectory(sourcePath, targetPath2, getCanonicalFileName)));
112840
113714
  });
112841
113715
  const shortest = min2(relativePaths, compareNumberOfDirectorySeparators);
112842
113716
  if (!shortest) {
@@ -269406,6 +270280,36 @@ function extractUnusedTopLevelVarNames(code) {
269406
270280
  }
269407
270281
  return declaredNames.filter((n) => topLevelNames.has(n) && (!usedByOthers.has(n) || explicitImplicitResultNames.has(n)));
269408
270282
  }
270283
+ function collectBindingNameLocations(node, sourceFile, names) {
270284
+ if (typescriptExports.isIdentifier(node)) {
270285
+ const { line: line2, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
270286
+ names.push({ name: node.text, line: line2 + 1, column: character + 1 });
270287
+ return;
270288
+ }
270289
+ for (const element of node.elements) {
270290
+ if (typescriptExports.isBindingElement(element)) {
270291
+ collectBindingNameLocations(element.name, sourceFile, names);
270292
+ }
270293
+ }
270294
+ }
270295
+ function findTopLevelRuntimeGlobalCollision(code, runtimeGlobalNames) {
270296
+ const runtimeGlobals = new Set(runtimeGlobalNames);
270297
+ const sourceFile = typescriptExports.createSourceFile("__runtime-globals.js", code, typescriptExports.ScriptTarget.ES2020, false, typescriptExports.ScriptKind.JS);
270298
+ const declarations = [];
270299
+ for (const statement of sourceFile.statements) {
270300
+ if (typescriptExports.isVariableStatement(statement)) {
270301
+ const isLexical = (statement.declarationList.flags & (typescriptExports.NodeFlags.Let | typescriptExports.NodeFlags.Const)) !== 0;
270302
+ if (!isLexical) continue;
270303
+ for (const decl of statement.declarationList.declarations) {
270304
+ collectBindingNameLocations(decl.name, sourceFile, declarations);
270305
+ }
270306
+ } else if (typescriptExports.isClassDeclaration(statement) && statement.name) {
270307
+ const { line: line2, character } = sourceFile.getLineAndCharacterOfPosition(statement.name.getStart(sourceFile));
270308
+ declarations.push({ name: statement.name.text, line: line2 + 1, column: character + 1 });
270309
+ }
270310
+ }
270311
+ return declarations.find((declaration) => runtimeGlobals.has(declaration.name)) ?? null;
270312
+ }
269409
270313
  function createForgeRuntimeModule(bindings) {
269410
270314
  const runtime = { ...bindings };
269411
270315
  Object.defineProperty(runtime, "__esModule", { value: true });
@@ -269946,6 +270850,12 @@ function withConstructorChainLockdown(fn) {
269946
270850
  }
269947
270851
  }
269948
270852
  function executeFile(code, fileName, allFiles, visited, scope = {}, options, executionMode = "script", moduleCacheEntry) {
270853
+ var _a3, _b3, _c2, _d2, _e, _f;
270854
+ (_a3 = options.debug) == null ? void 0 : _a3.call(options, "executeFile:start", {
270855
+ fileName,
270856
+ executionMode,
270857
+ scope: scope.namePrefix ?? fileName
270858
+ });
269949
270859
  const trackCircularImports = executionMode === "script";
269950
270860
  if (trackCircularImports) {
269951
270861
  if (visited.has(fileName)) {
@@ -270023,13 +270933,13 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
270023
270933
  });
270024
270934
  };
270025
270935
  const importStep = (name) => {
270026
- var _a3;
270936
+ var _a4;
270027
270937
  if (typeof name !== "string" || name.trim().length === 0) {
270028
270938
  throw new Error("importStep() requires a non-empty file path string");
270029
270939
  }
270030
270940
  const resolvedPath = resolveImportPath(fileName, name.trim());
270031
270941
  rejectPathTraversal("importStep", name, resolvedPath);
270032
- const ext = ((_a3 = resolvedPath.split(".").pop()) == null ? void 0 : _a3.toLowerCase()) ?? "";
270942
+ const ext = ((_a4 = resolvedPath.split(".").pop()) == null ? void 0 : _a4.toLowerCase()) ?? "";
270033
270943
  if (ext !== "step" && ext !== "stp") {
270034
270944
  throw new Error(`importStep("${name}"): unsupported extension ".${ext}". Expected .step or .stp`);
270035
270945
  }
@@ -270058,7 +270968,39 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
270058
270968
  setShapeTopology(shape, topo);
270059
270969
  return shape;
270060
270970
  };
270061
- const sandboxConsole = makeSandboxConsole(_collectedLogs);
270971
+ const sandboxConsole = makeSandboxConsole(_collectedLogs, options.debug ? (level, args) => {
270972
+ var _a4;
270973
+ return (_a4 = options.debug) == null ? void 0 : _a4.call(options, "console", { level, args });
270974
+ } : void 0);
270975
+ const runtimeVerify = options.debug ? {
270976
+ ...verify,
270977
+ that(label, check2, message) {
270978
+ var _a4, _b4;
270979
+ (_a4 = options.debug) == null ? void 0 : _a4.call(options, "verify:that:start", { label });
270980
+ const verifyStart = performance.now();
270981
+ try {
270982
+ return verify.that(label, check2, message);
270983
+ } finally {
270984
+ (_b4 = options.debug) == null ? void 0 : _b4.call(options, "verify:that:end", {
270985
+ label,
270986
+ ms: Number((performance.now() - verifyStart).toFixed(1))
270987
+ });
270988
+ }
270989
+ },
270990
+ equal(label, actual, expected, tolerance = 0, message) {
270991
+ var _a4, _b4;
270992
+ (_a4 = options.debug) == null ? void 0 : _a4.call(options, "verify:equal:start", { label, actual, expected, tolerance });
270993
+ const verifyStart = performance.now();
270994
+ try {
270995
+ return verify.equal(label, actual, expected, tolerance, message);
270996
+ } finally {
270997
+ (_b4 = options.debug) == null ? void 0 : _b4.call(options, "verify:equal:end", {
270998
+ label,
270999
+ ms: Number((performance.now() - verifyStart).toFixed(1))
271000
+ });
271001
+ }
271002
+ }
271003
+ } : verify;
270062
271004
  setShowLabelsHighlight(highlight);
270063
271005
  const runtimeBindings = {
270064
271006
  box: trackedBox,
@@ -270199,7 +271141,8 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
270199
271141
  jointsView,
270200
271142
  viewConfig,
270201
271143
  scene,
270202
- verify,
271144
+ Viewport,
271145
+ verify: runtimeVerify,
270203
271146
  spec,
270204
271147
  mock,
270205
271148
  gcode,
@@ -270337,8 +271280,21 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
270337
271280
  throw error;
270338
271281
  }
270339
271282
  };
270340
- const compiled = compileScript(code, fileName, options);
270341
271283
  const bindingNames = Object.keys(runtimeBindings);
271284
+ const collision = findTopLevelRuntimeGlobalCollision(code, bindingNames);
271285
+ if (collision) {
271286
+ throw new Error(
271287
+ `Local declaration "${collision.name}" at ${fileName}:${collision.line}:${collision.column} collides with the built-in ForgeCAD runtime name "${collision.name}". Use the ForgeCAD API directly, or rename your local variable/import (for example, import the helper module object instead of destructuring "${collision.name}").`
271288
+ );
271289
+ }
271290
+ (_b3 = options.debug) == null ? void 0 : _b3.call(options, "executeFile:compile:start", { fileName, executionMode });
271291
+ const compileStart = performance.now();
271292
+ const compiled = compileScript(code, fileName, options);
271293
+ (_c2 = options.debug) == null ? void 0 : _c2.call(options, "executeFile:compile:end", {
271294
+ fileName,
271295
+ executionMode,
271296
+ ms: Number((performance.now() - compileStart).toFixed(1))
271297
+ });
270342
271298
  const bindingValues = bindingNames.map((name) => runtimeBindings[name]);
270343
271299
  let scriptCode = compiled.code;
270344
271300
  if (executionMode === "script") {
@@ -270364,12 +271320,20 @@ ${scriptCode}
270364
271320
  exports: executionMode === "module" && moduleCacheEntry ? moduleCacheEntry.exports : {}
270365
271321
  };
270366
271322
  const initialExportsRef = moduleValue.exports;
271323
+ (_d2 = options.debug) == null ? void 0 : _d2.call(options, "executeFile:invoke:start", { fileName, executionMode });
271324
+ const invokeStart = performance.now();
270367
271325
  const returnValue = withConstructorChainLockdown(
270368
271326
  () => runWithParamScope(
270369
271327
  scope,
270370
271328
  () => fn(moduleValue.exports, moduleValue, requireModule, fileName, dirnamePath(fileName), ...bindingValues)
270371
271329
  )
270372
271330
  );
271331
+ (_e = options.debug) == null ? void 0 : _e.call(options, "executeFile:invoke:end", {
271332
+ fileName,
271333
+ executionMode,
271334
+ ms: Number((performance.now() - invokeStart).toFixed(1)),
271335
+ returned: returnValue === void 0 ? "undefined" : returnValue === null ? "null" : typeof returnValue
271336
+ });
270373
271337
  if (executionMode === "module") {
270374
271338
  const hasExports = hasExplicitModuleExports(moduleValue.exports, initialExportsRef);
270375
271339
  if (returnValue !== void 0 && hasExports) {
@@ -270408,12 +271372,14 @@ ${scriptCode}
270408
271372
  }
270409
271373
  return returnValue;
270410
271374
  } finally {
271375
+ (_f = options.debug) == null ? void 0 : _f.call(options, "executeFile:end", { fileName, executionMode });
270411
271376
  if (trackCircularImports) {
270412
271377
  visited.delete(fileName);
270413
271378
  }
270414
271379
  }
270415
271380
  }
270416
271381
  function runScript(code, fileName = "main.forge.js", allFiles = {}, options = {}) {
271382
+ var _a3, _b3;
270417
271383
  _collectedLogs = [];
270418
271384
  resetExecutionSession(_collectedLogs);
270419
271385
  const t0 = performance.now();
@@ -270422,14 +271388,25 @@ function runScript(code, fileName = "main.forge.js", allFiles = {}, options = {}
270422
271388
  fileIndex: buildFileIndex(allFiles),
270423
271389
  compiledFiles: persistentCompiledFiles,
270424
271390
  moduleCache: /* @__PURE__ */ new Map(),
270425
- readBinaryFile: options.readBinaryFile
271391
+ readBinaryFile: options.readBinaryFile,
271392
+ debug: options.debug
270426
271393
  };
270427
271394
  const quality = resolveForgeQualityPreset(options.quality);
271395
+ (_a3 = options.debug) == null ? void 0 : _a3.call(options, "runScript:start", { fileName, quality, fileCount: Object.keys(allFiles).length });
270428
271396
  try {
270429
271397
  return runWithForgeQuality(quality, () => {
271398
+ var _a4, _b4, _c2, _d2, _e, _f, _g, _h, _i, _j;
271399
+ (_a4 = options.debug) == null ? void 0 : _a4.call(options, "runScript:execute:start", { fileName });
271400
+ const executeStart = performance.now();
270430
271401
  const result = executeFile(code, fileName, allFiles, /* @__PURE__ */ new Set(), {}, execOptions);
271402
+ (_b4 = options.debug) == null ? void 0 : _b4.call(options, "runScript:execute:end", {
271403
+ fileName,
271404
+ ms: Number((performance.now() - executeStart).toFixed(1)),
271405
+ resultType: result === void 0 ? "undefined" : result === null ? "null" : typeof result
271406
+ });
270431
271407
  const highlights = getCollectedHighlights();
270432
271408
  const mocks = getCollectedMocks();
271409
+ (_c2 = options.debug) == null ? void 0 : _c2.call(options, "runScript:map:start", { highlights: highlights.length, mocks: mocks.length });
270433
271410
  const mapped = mapScriptResultToScene({
270434
271411
  result,
270435
271412
  fileName,
@@ -270438,24 +271415,43 @@ function runScript(code, fileName = "main.forge.js", allFiles = {}, options = {}
270438
271415
  mocks,
270439
271416
  logs: _collectedLogs
270440
271417
  });
271418
+ (_d2 = options.debug) == null ? void 0 : _d2.call(options, "runScript:map:end", {
271419
+ objects: mapped.objects.length,
271420
+ hasShape: Boolean(mapped.shape),
271421
+ hasSketch: Boolean(mapped.sketch),
271422
+ hasError: Boolean(mapped.error)
271423
+ });
271424
+ (_e = options.debug) == null ? void 0 : _e.call(options, "runScript:explodeHints:start", { objects: mapped.objects.length });
270441
271425
  autoFillExplodeHints(mapped.objects);
271426
+ (_f = options.debug) == null ? void 0 : _f.call(options, "runScript:explodeHints:end");
271427
+ (_g = options.debug) == null ? void 0 : _g.call(options, "runScript:snapshot:start", { objects: mapped.objects.length });
271428
+ const snapshot = collectSuccessfulExecutionSnapshot({
271429
+ quality,
271430
+ objects: mapped.objects,
271431
+ logs: _collectedLogs,
271432
+ extraDimensions: mapped.extraDimensions,
271433
+ highlights,
271434
+ mocks
271435
+ });
271436
+ (_h = options.debug) == null ? void 0 : _h.call(options, "runScript:snapshot:end", {
271437
+ params: snapshot.params.length,
271438
+ cutPlanes: snapshot.cutPlanes.length,
271439
+ verifications: snapshot.verifications.length
271440
+ });
271441
+ (_i = options.debug) == null ? void 0 : _i.call(options, "runScript:sceneTargets:start");
271442
+ snapshot.sceneConfig = resolveSceneJourneyTargets(snapshot.sceneConfig, mapped.objects);
271443
+ (_j = options.debug) == null ? void 0 : _j.call(options, "runScript:sceneTargets:end");
270442
271444
  return {
270443
271445
  shape: mapped.shape,
270444
271446
  sketch: mapped.sketch,
270445
271447
  objects: mapped.objects,
270446
- ...collectSuccessfulExecutionSnapshot({
270447
- quality,
270448
- objects: mapped.objects,
270449
- logs: _collectedLogs,
270450
- extraDimensions: mapped.extraDimensions,
270451
- highlights,
270452
- mocks
270453
- }),
271448
+ ...snapshot,
270454
271449
  error: mapped.error,
270455
271450
  timeMs: performance.now() - t0
270456
271451
  };
270457
271452
  });
270458
271453
  } catch (e) {
271454
+ (_b3 = options.debug) == null ? void 0 : _b3.call(options, "runScript:error", { error: (e == null ? void 0 : e.message) || String(e) });
270459
271455
  const msg = e.message || String(e);
270460
271456
  const stack = e.stack || "";
270461
271457
  let lineInfo = "";
@@ -271051,6 +272047,31 @@ function serializeRunResult(result, solverDebug = null) {
271051
272047
  };
271052
272048
  return { serialized, transferables };
271053
272049
  }
272050
+ function serializeRunMetadata(result) {
272051
+ const { shape: _shape, sketch: _sketch, objects, debugHighlights3D: _debugHighlights3D, mocks: _mocks, ...passthrough } = result;
272052
+ return {
272053
+ ...passthrough,
272054
+ objects: objects.map((obj) => ({
272055
+ id: obj.id,
272056
+ name: obj.name,
272057
+ hasShape: Boolean(obj.shape),
272058
+ hasSketch: Boolean(obj.sketch),
272059
+ sdfData: obj.sdf ?? null,
272060
+ toolpathData: obj.toolpath ?? null,
272061
+ color: obj.color,
272062
+ materialProps: obj.materialProps,
272063
+ geometryInfo: obj.geometryInfo,
272064
+ sketchMeta: obj.sketchMeta,
272065
+ groupName: obj.groupName,
272066
+ treePath: obj.treePath,
272067
+ mock: obj.mock,
272068
+ serverShapeRef: obj.serverShapeRef,
272069
+ exactState: obj.exactState
272070
+ })),
272071
+ debugHighlights3D: [],
272072
+ mocks: []
272073
+ };
272074
+ }
271054
272075
  let currentProjectId = null;
271055
272076
  function readBinaryFile(resolvedPath) {
271056
272077
  const xhr = new XMLHttpRequest();
@@ -271124,6 +272145,7 @@ async function runOnce(payload) {
271124
272145
  console.log(`[worker] seq=${seq} stale (newer queued) — skipping serialize. run=${(tRun - tKernel).toFixed(0)}ms`);
271125
272146
  return;
271126
272147
  }
272148
+ worker.postMessage({ type: "run-metadata", payload: { seq, metadata: serializeRunMetadata(runResult) } });
271127
272149
  disposeRunResult(lastRunResult);
271128
272150
  lastRunResult = runResult;
271129
272151
  worker.postMessage({ type: "progress", payload: { seq, phase: "serializing" } });