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
@@ -7310,7 +7310,7 @@ function add$3(a, b) {
7310
7310
  function scale$4(v, s) {
7311
7311
  return [v[0] * s, v[1] * s, v[2] * s];
7312
7312
  }
7313
- function sub$4(a, b) {
7313
+ function sub$5(a, b) {
7314
7314
  return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
7315
7315
  }
7316
7316
  function cross$4(a, b) {
@@ -7349,8 +7349,8 @@ function buildSurfaceSheetTopology(boundaries, options = {}) {
7349
7349
  const [v1Start, v1End] = boundaries.v1;
7350
7350
  const corners = [u0Start, u0End, u1Start, u1End];
7351
7351
  const center = options.center ?? average$1(corners);
7352
- const uAxis = normalizeAxis$1(sub$4(midpoint$3(u1Start, u1End), midpoint$3(u0Start, u0End)));
7353
- const vAxis = normalizeAxis$1(sub$4(midpoint$3(v1Start, v1End), midpoint$3(v0Start, v0End)));
7352
+ const uAxis = normalizeAxis$1(sub$5(midpoint$3(u1Start, u1End), midpoint$3(u0Start, u0End)));
7353
+ const vAxis = normalizeAxis$1(sub$5(midpoint$3(v1Start, v1End), midpoint$3(v0Start, v0End)));
7354
7354
  const normal = normalizeAxis$1(options.normal ?? cross$4(uAxis, vAxis));
7355
7355
  const faces = /* @__PURE__ */ new Map();
7356
7356
  faces.set(faceName, {
@@ -7382,7 +7382,7 @@ function attachSurfaceSheetTopology(shape, boundaries, options = {}) {
7382
7382
  });
7383
7383
  return shape;
7384
7384
  }
7385
- function requireFinite$6(v, label) {
7385
+ function requireFinite$7(v, label) {
7386
7386
  if (!Number.isFinite(v)) throw new Error(`nurbsSurface: ${label} must be finite, got ${v}`);
7387
7387
  }
7388
7388
  class NurbsSurface {
@@ -7408,16 +7408,16 @@ class NurbsSurface {
7408
7408
  for (let i = 0; i < nU; i++) {
7409
7409
  if (controlGrid[i].length !== nV) throw new Error(`nurbsSurface: row ${i} has ${controlGrid[i].length} points, expected ${nV}`);
7410
7410
  for (let j = 0; j < nV; j++) {
7411
- requireFinite$6(controlGrid[i][j][0], `controlGrid[${i}][${j}][0]`);
7412
- requireFinite$6(controlGrid[i][j][1], `controlGrid[${i}][${j}][1]`);
7413
- requireFinite$6(controlGrid[i][j][2], `controlGrid[${i}][${j}][2]`);
7411
+ requireFinite$7(controlGrid[i][j][0], `controlGrid[${i}][${j}][0]`);
7412
+ requireFinite$7(controlGrid[i][j][1], `controlGrid[${i}][${j}][1]`);
7413
+ requireFinite$7(controlGrid[i][j][2], `controlGrid[${i}][${j}][2]`);
7414
7414
  }
7415
7415
  }
7416
7416
  const weightsGrid = options.weights ?? controlGrid.map((row) => row.map(() => 1));
7417
7417
  for (let i = 0; i < nU; i++) {
7418
7418
  if (weightsGrid[i].length !== nV) throw new Error(`nurbsSurface: weights row ${i} length mismatch`);
7419
7419
  for (let j = 0; j < nV; j++) {
7420
- requireFinite$6(weightsGrid[i][j], `weights[${i}][${j}]`);
7420
+ requireFinite$7(weightsGrid[i][j], `weights[${i}][${j}]`);
7421
7421
  if (weightsGrid[i][j] <= 0) throw new Error(`nurbsSurface: weights[${i}][${j}] must be > 0`);
7422
7422
  }
7423
7423
  }
@@ -8825,7 +8825,7 @@ async function initManifoldWasm() {
8825
8825
  if (_wasm) return _wasm;
8826
8826
  performance.mark("manifold:start");
8827
8827
  const Module = (await __vitePreload(async () => {
8828
- const { default: __vite_default__ } = await import("./manifold-DbyILno4.js");
8828
+ const { default: __vite_default__ } = await import("./manifold-CYWZMfjB.js");
8829
8829
  return { default: __vite_default__ };
8830
8830
  }, true ? [] : void 0)).default;
8831
8831
  performance.mark("manifold:imported");
@@ -9048,7 +9048,7 @@ function sweepStitched(profilePolygons, pathPoints, up, wasm) {
9048
9048
  function computeParallelTransportFrames(path2, preferredUp) {
9049
9049
  const n = path2.length;
9050
9050
  const frames = [];
9051
- const firstTangent = normalize$6(sub$3(path2[1], path2[0]));
9051
+ const firstTangent = normalize$6(sub$4(path2[1], path2[0]));
9052
9052
  if (!firstTangent) return null;
9053
9053
  let x = normalize$6(cross$3(preferredUp, firstTangent));
9054
9054
  if (!x || length(x) < 1e-8) {
@@ -9062,18 +9062,18 @@ function computeParallelTransportFrames(path2, preferredUp) {
9062
9062
  const prevT = frames[i - 1].t;
9063
9063
  let nextT;
9064
9064
  if (i < n - 1) {
9065
- const t1 = normalize$6(sub$3(path2[i], path2[i - 1]));
9066
- const t2 = normalize$6(sub$3(path2[i + 1], path2[i]));
9065
+ const t1 = normalize$6(sub$4(path2[i], path2[i - 1]));
9066
+ const t2 = normalize$6(sub$4(path2[i + 1], path2[i]));
9067
9067
  if (!t1 || !t2) return null;
9068
9068
  nextT = normalize$6(add$2(t1, t2)) || t1;
9069
9069
  } else {
9070
- const nt = normalize$6(sub$3(path2[i], path2[i - 1]));
9070
+ const nt = normalize$6(sub$4(path2[i], path2[i - 1]));
9071
9071
  if (!nt) return null;
9072
9072
  nextT = nt;
9073
9073
  }
9074
9074
  const v = cross$3(prevT, nextT);
9075
9075
  const vLen = length(v);
9076
- const c = dot$4(prevT, nextT);
9076
+ const c = dot$5(prevT, nextT);
9077
9077
  if (vLen > 1e-10) {
9078
9078
  const axis = scale$3(v, 1 / vLen);
9079
9079
  x = rotateVector(frames[i - 1].x, axis, c, vLen);
@@ -9147,7 +9147,7 @@ function signedArea$5(loop) {
9147
9147
  }
9148
9148
  return area2 * 0.5;
9149
9149
  }
9150
- function sub$3(a, b) {
9150
+ function sub$4(a, b) {
9151
9151
  return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
9152
9152
  }
9153
9153
  function add$2(a, b) {
@@ -9156,7 +9156,7 @@ function add$2(a, b) {
9156
9156
  function scale$3(v, s) {
9157
9157
  return [v[0] * s, v[1] * s, v[2] * s];
9158
9158
  }
9159
- function dot$4(a, b) {
9159
+ function dot$5(a, b) {
9160
9160
  return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
9161
9161
  }
9162
9162
  function cross$3(a, b) {
@@ -9175,7 +9175,7 @@ function normalize$6(v) {
9175
9175
  return [v[0] / len2, v[1] / len2, v[2] / len2];
9176
9176
  }
9177
9177
  function rotateVector(v, axis, c, s) {
9178
- const kDotV = dot$4(axis, v);
9178
+ const kDotV = dot$5(axis, v);
9179
9179
  const kCrossV = cross$3(axis, v);
9180
9180
  return [
9181
9181
  v[0] * c + kCrossV[0] * s + axis[0] * kDotV * (1 - c),
@@ -12876,7 +12876,7 @@ function normalizeFaceSelector(selector) {
12876
12876
  function cross$2(a, b) {
12877
12877
  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]];
12878
12878
  }
12879
- function dot$3(a, b) {
12879
+ function dot$4(a, b) {
12880
12880
  return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
12881
12881
  }
12882
12882
  function normVec3(v) {
@@ -12911,11 +12911,11 @@ function clusterMeshFaces(shape) {
12911
12911
  if (!normal) continue;
12912
12912
  const crossLen = Math.sqrt(rawCross[0] * rawCross[0] + rawCross[1] * rawCross[1] + rawCross[2] * rawCross[2]);
12913
12913
  const triArea = crossLen / 2;
12914
- const planeOffset = dot$3(normal, v0);
12914
+ const planeOffset = dot$4(normal, v0);
12915
12915
  const triCentroid = [(v0[0] + v1[0] + v2[0]) / 3, (v0[1] + v1[1] + v2[1]) / 3, (v0[2] + v1[2] + v2[2]) / 3];
12916
12916
  let merged = false;
12917
12917
  for (const c of clusters) {
12918
- if (dot$3(c.normal, normal) > NORMAL_COS_EPS$1 && Math.abs(c.planeOffset - planeOffset) < PLANE_OFFSET_EPS$1) {
12918
+ if (dot$4(c.normal, normal) > NORMAL_COS_EPS$1 && Math.abs(c.planeOffset - planeOffset) < PLANE_OFFSET_EPS$1) {
12919
12919
  c.centroidSum[0] += triCentroid[0];
12920
12920
  c.centroidSum[1] += triCentroid[1];
12921
12921
  c.centroidSum[2] += triCentroid[2];
@@ -12957,7 +12957,7 @@ function queryMeshFaces(shape, query) {
12957
12957
  let clusters = clusterMeshFaces(shape);
12958
12958
  if (query.normal) {
12959
12959
  const qn = query.normal;
12960
- clusters = clusters.filter((c) => dot$3(c.normal, qn) > NORMAL_COS_EPS$1);
12960
+ clusters = clusters.filter((c) => dot$4(c.normal, qn) > NORMAL_COS_EPS$1);
12961
12961
  }
12962
12962
  if (query.planar !== false) {
12963
12963
  clusters = clusters.filter((c) => c.normal !== null);
@@ -12973,7 +12973,7 @@ function queryMeshFace(shape, query) {
12973
12973
  let clusters = clusterMeshFaces(shape);
12974
12974
  if (query.normal) {
12975
12975
  const qn = query.normal;
12976
- clusters = clusters.filter((c) => dot$3(c.normal, qn) > NORMAL_COS_EPS$1);
12976
+ clusters = clusters.filter((c) => dot$4(c.normal, qn) > NORMAL_COS_EPS$1);
12977
12977
  }
12978
12978
  if (query.planar !== false) {
12979
12979
  clusters = clusters.filter((c) => c.normal !== null);
@@ -19925,6 +19925,7 @@ Fix: pass a path like [[0,0,0], [20,0,8,4], [40,0,0,2]].`
19925
19925
  const curveMode = shouldSmoothCurve(options, defaultCurve) && sourcePoints.length >= 3;
19926
19926
  const normalized = compactPathPoints(resampleCurve(sourcePoints, options, defaultCurve));
19927
19927
  const minRadius = Math.min(...normalized.map((point2) => point2.radius));
19928
+ const hasExplicitBlend = options.blend !== void 0;
19928
19929
  const blendRadius = positiveFinite$1(options.blend, "Sculpt.tube() blend", Math.max(0.5, minRadius));
19929
19930
  let segmentCount = 0;
19930
19931
  for (let i = 0; i < normalized.length - 1; i += 1) {
@@ -19933,8 +19934,8 @@ Fix: pass a path like [[0,0,0], [20,0,8,4], [40,0,0,2]].`
19933
19934
  if (segmentCount === 0) {
19934
19935
  throw new Error("Sculpt.tube() points must include at least one non-zero-length segment.");
19935
19936
  }
19936
- const curveBlendRadius = curveMode ? Math.max(blendRadius, minRadius * 1.5) : Math.min(blendRadius, Math.max(0.1, minRadius * 0.8));
19937
- let out = polylineSweep(normalized, curveMode ? curveBlendRadius : blendRadius);
19937
+ const effectiveBlendRadius = curveMode && !hasExplicitBlend ? Math.max(blendRadius, minRadius * 1.5) : blendRadius;
19938
+ let out = polylineSweep(normalized, effectiveBlendRadius);
19938
19939
  if (options.polish !== void 0) out = out.polish(options.polish);
19939
19940
  return out;
19940
19941
  }
@@ -22326,7 +22327,7 @@ function intersection(...inputs) {
22326
22327
  nextPlan
22327
22328
  );
22328
22329
  }
22329
- var define_process_env_default = {};
22330
+ var define_process_env_default$2 = {};
22330
22331
  let _wasm_solve = null;
22331
22332
  let _wasm_get_profile = null;
22332
22333
  let _solverMemory = null;
@@ -22361,7 +22362,7 @@ function readInitialConsoleDebug() {
22361
22362
  return false;
22362
22363
  }
22363
22364
  function readEnvFlag(name) {
22364
- const value = typeof process !== "undefined" ? define_process_env_default == null ? void 0 : define_process_env_default[name] : void 0;
22365
+ const value = typeof process !== "undefined" ? define_process_env_default$2 == null ? void 0 : define_process_env_default$2[name] : void 0;
22365
22366
  if (typeof value !== "string") return false;
22366
22367
  return ["1", "true", "yes", "on"].includes(value.toLowerCase());
22367
22368
  }
@@ -23648,7 +23649,7 @@ class MateBuilder {
23648
23649
  return this.constraints.reduce((sum, c) => sum + (CONSTRAINT_EQUATIONS[c.type] ?? 0), 0);
23649
23650
  }
23650
23651
  }
23651
- let _collected$7 = null;
23652
+ let _collected$8 = null;
23652
23653
  const isAxis = (value) => value === "x" || value === "y" || value === "z";
23653
23654
  const normalizeDirection = (value, label) => {
23654
23655
  if (value === "radial" || isAxis(value)) return value;
@@ -23707,16 +23708,16 @@ const mergeDirective = (target, patch, label) => {
23707
23708
  return out;
23708
23709
  };
23709
23710
  function resetExplodeView() {
23710
- _collected$7 = null;
23711
+ _collected$8 = null;
23711
23712
  }
23712
23713
  function getCollectedExplodeView() {
23713
- return _collected$7 ? cloneOptions(_collected$7) : null;
23714
+ return _collected$8 ? cloneOptions(_collected$8) : null;
23714
23715
  }
23715
23716
  function explodeView(options = {}) {
23716
23717
  if (!options || typeof options !== "object") {
23717
23718
  throw new Error("explodeView(options) expects an options object");
23718
23719
  }
23719
- const next = _collected$7 ? cloneOptions(_collected$7) : {};
23720
+ const next = _collected$8 ? cloneOptions(_collected$8) : {};
23720
23721
  if (options.enabled !== void 0) {
23721
23722
  if (typeof options.enabled !== "boolean") throw new Error("explodeView.enabled must be a boolean");
23722
23723
  next.enabled = options.enabled;
@@ -23764,9 +23765,9 @@ function explodeView(options = {}) {
23764
23765
  });
23765
23766
  next.byPath = byPath;
23766
23767
  }
23767
- _collected$7 = next;
23768
+ _collected$8 = next;
23768
23769
  }
23769
- let _collected$6 = null;
23770
+ let _collected$7 = null;
23770
23771
  const isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
23771
23772
  const isVec3$1 = (value) => Array.isArray(value) && value.length === 3 && isFiniteNumber(value[0]) && isFiniteNumber(value[1]) && isFiniteNumber(value[2]);
23772
23773
  const normalizeAxis = (axis) => {
@@ -24099,22 +24100,22 @@ const cloneCollected = (value) => ({
24099
24100
  defaultAnimation: value.defaultAnimation
24100
24101
  });
24101
24102
  function resetJointsView() {
24102
- _collected$6 = null;
24103
+ _collected$7 = null;
24103
24104
  }
24104
24105
  function getCollectedJointsView() {
24105
- return _collected$6 ? cloneCollected(_collected$6) : null;
24106
+ return _collected$7 ? cloneCollected(_collected$7) : null;
24106
24107
  }
24107
24108
  function saveJointsView() {
24108
- return _collected$6 ? cloneCollected(_collected$6) : null;
24109
+ return _collected$7 ? cloneCollected(_collected$7) : null;
24109
24110
  }
24110
24111
  function restoreJointsView(state) {
24111
- _collected$6 = state;
24112
+ _collected$7 = state;
24112
24113
  }
24113
24114
  function jointsView(options = {}) {
24114
24115
  if (!options || typeof options !== "object") {
24115
24116
  throw new Error("jointsView(options) expects an options object");
24116
24117
  }
24117
- const next = _collected$6 ? cloneCollected(_collected$6) : { joints: [], couplings: [], animations: [] };
24118
+ const next = _collected$7 ? cloneCollected(_collected$7) : { joints: [], couplings: [], animations: [] };
24118
24119
  if (options.enabled !== void 0) {
24119
24120
  if (typeof options.enabled !== "boolean") {
24120
24121
  throw new Error("jointsView.enabled must be a boolean");
@@ -24169,8 +24170,14 @@ function jointsView(options = {}) {
24169
24170
  if (next.defaultAnimation && !next.animations.some((animation) => animation.name === next.defaultAnimation)) {
24170
24171
  throw new Error(`jointsView defaultAnimation "${next.defaultAnimation}" does not exist in animations`);
24171
24172
  }
24172
- _collected$6 = next;
24173
+ _collected$7 = next;
24173
24174
  }
24175
+ var define_process_env_default$1 = {};
24176
+ const SWEEP_JOINT_DEFAULT_STEP_LIMIT = {
24177
+ live: 1,
24178
+ default: 4,
24179
+ high: Number.POSITIVE_INFINITY
24180
+ };
24174
24181
  let collectedAssemblies = [];
24175
24182
  function resetCollectedAssemblies() {
24176
24183
  collectedAssemblies = [];
@@ -24211,6 +24218,33 @@ function collisionShape(part) {
24211
24218
  if (shapes.length === 1) return shapes[0];
24212
24219
  return union(...shapes);
24213
24220
  }
24221
+ function boundsOverlap(a, b) {
24222
+ for (let axis = 0; axis < 3; axis++) {
24223
+ if (a.max[axis] <= b.min[axis] || b.max[axis] <= a.min[axis]) return false;
24224
+ }
24225
+ return true;
24226
+ }
24227
+ function readAssemblyPerfEnv(name) {
24228
+ return typeof process !== "undefined" ? define_process_env_default$1 == null ? void 0 : define_process_env_default$1[name] : void 0;
24229
+ }
24230
+ function resolveSweepJointStepLimit() {
24231
+ if (readAssemblyPerfEnv("FORGECAD_ALLOW_FULL_SWEEP_JOINT") === "1") return Number.POSITIVE_INFINITY;
24232
+ const override = readAssemblyPerfEnv("FORGECAD_SWEEP_JOINT_STEP_LIMIT");
24233
+ if (override != null && override.trim() !== "") {
24234
+ const parsed = Number(override);
24235
+ if (Number.isFinite(parsed) && parsed >= 1) return Math.floor(parsed);
24236
+ }
24237
+ return SWEEP_JOINT_DEFAULT_STEP_LIMIT[getForgeQualityPreset()];
24238
+ }
24239
+ function boundSweepJointSteps(jointName, requestedSteps) {
24240
+ const limit = resolveSweepJointStepLimit();
24241
+ if (!Number.isFinite(limit) || requestedSteps <= limit) return requestedSteps;
24242
+ const bounded = Math.max(1, Math.floor(limit));
24243
+ emitRuntimeWarning(
24244
+ `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.`
24245
+ );
24246
+ return bounded;
24247
+ }
24214
24248
  const FASTENER_PATTERN = /\b(bolt|screw|nut|washer|pin|rivet|fastener|standoff|insert)\b/i;
24215
24249
  function isFastenerName(name) {
24216
24250
  return FASTENER_PATTERN.test(name);
@@ -24543,16 +24577,23 @@ class SolvedAssembly {
24543
24577
  const minOverlap = options.minOverlapVolume ?? 0.1;
24544
24578
  const ignore = new Set((options.ignorePairs ?? []).map(([a, b]) => [a, b].sort().join("|")));
24545
24579
  const findings = [];
24546
- for (let i = 0; i < names.length; i++) {
24547
- for (let j = i + 1; j < names.length; j++) {
24548
- const aName = names[i];
24549
- const bName = names[j];
24580
+ const entries = [];
24581
+ for (const name of names) {
24582
+ const shape = collisionShape(this.getPart(name));
24583
+ if (!shape) continue;
24584
+ try {
24585
+ entries.push({ name, shape, bounds: shape.boundingBox() });
24586
+ } catch {
24587
+ }
24588
+ }
24589
+ for (let i = 0; i < entries.length; i++) {
24590
+ for (let j = i + 1; j < entries.length; j++) {
24591
+ const aName = entries[i].name;
24592
+ const bName = entries[j].name;
24550
24593
  if (ignore.has([aName, bName].sort().join("|"))) continue;
24551
- const a = collisionShape(this.getPart(aName));
24552
- const b = collisionShape(this.getPart(bName));
24553
- if (!a || !b) continue;
24594
+ if (!boundsOverlap(entries[i].bounds, entries[j].bounds)) continue;
24554
24595
  try {
24555
- const hit = a.intersect(b);
24596
+ const hit = entries[i].shape.intersect(entries[j].shape);
24556
24597
  if (hit.isEmpty()) continue;
24557
24598
  const vol = hit.volume();
24558
24599
  if (vol > minOverlap) {
@@ -25529,7 +25570,7 @@ class Assembly {
25529
25570
  if (this.jointCouplings.has(jointName)) {
25530
25571
  throw new Error(`Cannot sweep coupled joint "${jointName}". Sweep one of its source joints instead.`);
25531
25572
  }
25532
- const n = Math.max(1, Math.floor(steps));
25573
+ const n = boundSweepJointSteps(jointName, Math.max(1, Math.floor(steps)));
25533
25574
  const frames = [];
25534
25575
  for (let i = 0; i <= n; i++) {
25535
25576
  const t = n === 0 ? 0 : i / n;
@@ -25945,12 +25986,12 @@ function bom(quantity, description, opts) {
25945
25986
  metadata
25946
25987
  });
25947
25988
  }
25948
- let _collected$5 = [];
25989
+ let _collected$6 = [];
25949
25990
  function resetCutPlanes() {
25950
- _collected$5 = [];
25991
+ _collected$6 = [];
25951
25992
  }
25952
25993
  function getCollectedCutPlanes() {
25953
- return _collected$5.slice();
25994
+ return _collected$6.slice();
25954
25995
  }
25955
25996
  function normalizeExcludedObjectNames(input) {
25956
25997
  if (input === void 0) return void 0;
@@ -25966,16 +26007,16 @@ function cutPlane(name, normal, offsetOrOptions = 0, maybeOptions = {}) {
25966
26007
  const offset2 = Number.isFinite(rawOffset) ? rawOffset : 0;
25967
26008
  const options = usingOffsetArg ? maybeOptions : offsetOrOptions;
25968
26009
  const excludeObjectNames = normalizeExcludedObjectNames(options.exclude);
25969
- _collected$5.push({ name, normal, offset: offset2, excludeObjectNames });
26010
+ _collected$6.push({ name, normal, offset: offset2, excludeObjectNames });
25970
26011
  }
25971
- let _collected$4 = [];
26012
+ let _collected$5 = [];
25972
26013
  let _counter$1 = 0;
25973
26014
  function resetMocks() {
25974
- _collected$4 = [];
26015
+ _collected$5 = [];
25975
26016
  _counter$1 = 0;
25976
26017
  }
25977
26018
  function getCollectedMocks() {
25978
- return _collected$4.slice();
26019
+ return _collected$5.slice();
25979
26020
  }
25980
26021
  function mock(shape, name) {
25981
26022
  if (!shape || typeof shape !== "object") {
@@ -25983,7 +26024,7 @@ function mock(shape, name) {
25983
26024
  }
25984
26025
  _counter$1 += 1;
25985
26026
  const displayName = name && typeof name === "string" && name.trim().length > 0 ? name.trim() : `Mock ${_counter$1}`;
25986
- _collected$4.push({
26027
+ _collected$5.push({
25987
26028
  id: `mock-${_counter$1}`,
25988
26029
  name: displayName,
25989
26030
  shape
@@ -30185,7 +30226,7 @@ function shapeToBounds(shape) {
30185
30226
  max: [bb.max[0], bb.max[1], bb.max[2]]
30186
30227
  };
30187
30228
  }
30188
- function sub$2(a, b) {
30229
+ function sub$3(a, b) {
30189
30230
  return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
30190
30231
  }
30191
30232
  function addVec(a, b) {
@@ -30194,7 +30235,7 @@ function addVec(a, b) {
30194
30235
  function scale$2(v, s) {
30195
30236
  return [v[0] * s, v[1] * s, v[2] * s];
30196
30237
  }
30197
- function dot$2(a, b) {
30238
+ function dot$3(a, b) {
30198
30239
  return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
30199
30240
  }
30200
30241
  function cross$1(a, b) {
@@ -30218,9 +30259,9 @@ function pipeRoute(points, radius, options) {
30218
30259
  const bends = new Array(points.length).fill(null);
30219
30260
  for (let i = 1; i < points.length - 1; i++) {
30220
30261
  const prev = points[i - 1], cur = points[i], next = points[i + 1];
30221
- const dIn = normalize$3(sub$2(cur, prev));
30222
- const dOut = normalize$3(sub$2(next, cur));
30223
- const dotVal = clampDot(dot$2(dIn, dOut));
30262
+ const dIn = normalize$3(sub$3(cur, prev));
30263
+ const dOut = normalize$3(sub$3(next, cur));
30264
+ const dotVal = clampDot(dot$3(dIn, dOut));
30224
30265
  const bendAngle = Math.acos(dotVal);
30225
30266
  if (bendAngle < 1e-6) {
30226
30267
  continue;
@@ -30239,7 +30280,7 @@ function pipeRoute(points, radius, options) {
30239
30280
  }
30240
30281
  const parts = [];
30241
30282
  const makeSeg = (a, b) => {
30242
- const d = sub$2(b, a);
30283
+ const d = sub$3(b, a);
30243
30284
  const len2 = vecLen(d);
30244
30285
  if (len2 < 0.01) return null;
30245
30286
  const dir = normalize$3(d);
@@ -30284,7 +30325,7 @@ function pipeRoute(points, radius, options) {
30284
30325
  const innerBend = buildShapeFromCompilePlan(innerPlan);
30285
30326
  bendShape = bendShape.subtract(innerBend);
30286
30327
  }
30287
- const radialDir = normalize$3(sub$2(info.startPt, info.center));
30328
+ const radialDir = normalize$3(sub$3(info.startPt, info.center));
30288
30329
  const tangentDir = cross$1(info.axis, radialDir);
30289
30330
  const c = info.center;
30290
30331
  bendShape = bendShape.transform([
@@ -30345,7 +30386,7 @@ function elbow(pipeRadius, bendRadius, angle, options) {
30345
30386
  if (fromDir && toDir) {
30346
30387
  const nFrom = normalize$3(fromDir);
30347
30388
  const nTo = normalize$3(toDir);
30348
- const d = clampDot(dot$2(nFrom, nTo));
30389
+ const d = clampDot(dot$3(nFrom, nTo));
30349
30390
  angleDeg = Math.acos(d) * 180 / Math.PI;
30350
30391
  }
30351
30392
  if (angleDeg < 0.01) throw new Error("elbow: angle too small");
@@ -30416,20 +30457,20 @@ function assertFinitePositive(apiName, name, value) {
30416
30457
  function add$1(a, b) {
30417
30458
  return [a[0] + b[0], a[1] + b[1]];
30418
30459
  }
30419
- function sub$1(a, b) {
30460
+ function sub$2(a, b) {
30420
30461
  return [a[0] - b[0], a[1] - b[1]];
30421
30462
  }
30422
30463
  function scale$1(v, s) {
30423
30464
  return [v[0] * s, v[1] * s];
30424
30465
  }
30425
- function dot$1(a, b) {
30466
+ function dot$2(a, b) {
30426
30467
  return a[0] * b[0] + a[1] * b[1];
30427
30468
  }
30428
30469
  function len(v) {
30429
30470
  return Math.hypot(v[0], v[1]);
30430
30471
  }
30431
30472
  function dist$2(a, b) {
30432
- return len(sub$1(b, a));
30473
+ return len(sub$2(b, a));
30433
30474
  }
30434
30475
  function norm$1(v) {
30435
30476
  const l = len(v);
@@ -30454,7 +30495,7 @@ function chooseSweepDeg(center, start, end, incomingDir) {
30454
30495
  const endAngle = angleOf(center, end);
30455
30496
  const cwTangent = tangentAt(startAngle, true);
30456
30497
  const ccwTangent = tangentAt(startAngle, false);
30457
- const clockwise = dot$1(cwTangent, incomingDir) >= dot$1(ccwTangent, incomingDir);
30498
+ const clockwise = dot$2(cwTangent, incomingDir) >= dot$2(ccwTangent, incomingDir);
30458
30499
  const sweep2 = clockwise ? -normalizePositiveRadians(startAngle - endAngle) : normalizePositiveRadians(endAngle - startAngle);
30459
30500
  return sweep2 * 180 / Math.PI;
30460
30501
  }
@@ -30497,8 +30538,8 @@ function normalizePulleyAsCircle(pulley, index, radiusOverride) {
30497
30538
  );
30498
30539
  }
30499
30540
  function commonTangents(a, b, mode) {
30500
- const delta = sub$1(b.center, a.center);
30501
- const z = dot$1(delta, delta);
30541
+ const delta = sub$2(b.center, a.center);
30542
+ const z = dot$2(delta, delta);
30502
30543
  if (z < EPS$5) {
30503
30544
  throw new Error(`beltDrive: pulleys "${a.name}" and "${b.name}" have the same center.`);
30504
30545
  }
@@ -30528,8 +30569,8 @@ function commonTangents(a, b, mode) {
30528
30569
  });
30529
30570
  }
30530
30571
  function buildSegmentsForTangentOrder(a, b, t0, t1) {
30531
- const span0Dir = norm$1(sub$1(t0.b, t0.a));
30532
- const span1Dir = norm$1(sub$1(t1.a, t1.b));
30572
+ const span0Dir = norm$1(sub$2(t0.b, t0.a));
30573
+ const span1Dir = norm$1(sub$2(t1.a, t1.b));
30533
30574
  const bSweepDeg = chooseSweepDeg(b.center, t0.b, t1.b, span0Dir);
30534
30575
  const aSweepDeg = chooseSweepDeg(a.center, t1.a, t0.a, span1Dir);
30535
30576
  const span0 = {
@@ -73204,6 +73245,130 @@ function shapeToGeometryFallback(shape) {
73204
73245
  const edges = new EdgesGeometry(solid, 1);
73205
73246
  return { solid, edges, hasSmoothNormals: false };
73206
73247
  }
73248
+ let _collected$4 = [];
73249
+ let _nextId = 1;
73250
+ function resetRenderLabels() {
73251
+ _collected$4 = [];
73252
+ _nextId = 1;
73253
+ }
73254
+ function getCollectedRenderLabels() {
73255
+ return _collected$4.map((label) => ({ ...label, at: [...label.at], offset: [...label.offset] }));
73256
+ }
73257
+ function requireFinite$6(value, label) {
73258
+ if (typeof value !== "number" || !Number.isFinite(value)) {
73259
+ throw new Error(`${label} must be a finite number`);
73260
+ }
73261
+ return value;
73262
+ }
73263
+ function requireVec3$2(value, label) {
73264
+ if (!Array.isArray(value) || value.length !== 3) {
73265
+ throw new Error(`${label} must be [x, y, z]`);
73266
+ }
73267
+ return [requireFinite$6(value[0], `${label}[0]`), requireFinite$6(value[1], `${label}[1]`), requireFinite$6(value[2], `${label}[2]`)];
73268
+ }
73269
+ function optionalColor(value, label) {
73270
+ if (value === void 0) return void 0;
73271
+ if (typeof value !== "string" || value.trim().length === 0) {
73272
+ throw new Error(`${label} must be a non-empty CSS color string`);
73273
+ }
73274
+ return value.trim();
73275
+ }
73276
+ const VALID_ANCHORS = /* @__PURE__ */ new Set([
73277
+ "center",
73278
+ "top",
73279
+ "bottom",
73280
+ "left",
73281
+ "right",
73282
+ "top-left",
73283
+ "top-right",
73284
+ "bottom-left",
73285
+ "bottom-right"
73286
+ ]);
73287
+ function normalizeOptions(options) {
73288
+ if (options === void 0) {
73289
+ return { offset: [0, 0, 0], anchor: "center", alwaysOnTop: true };
73290
+ }
73291
+ if (!options || typeof options !== "object" || Array.isArray(options)) {
73292
+ throw new Error("Viewport.label options must be an object");
73293
+ }
73294
+ const out = {
73295
+ offset: [0, 0, 0],
73296
+ anchor: "center",
73297
+ alwaysOnTop: true
73298
+ };
73299
+ const color = optionalColor(options.color, "Viewport.label options.color");
73300
+ if (color !== void 0) out.color = color;
73301
+ const background = optionalColor(options.background, "Viewport.label options.background");
73302
+ if (background !== void 0) out.background = background;
73303
+ if (options.size !== void 0) {
73304
+ out.size = requireFinite$6(options.size, "Viewport.label options.size");
73305
+ if (out.size <= 0) throw new Error("Viewport.label options.size must be positive");
73306
+ }
73307
+ if (options.offset !== void 0) out.offset = requireVec3$2(options.offset, "Viewport.label options.offset");
73308
+ if (options.anchor !== void 0) {
73309
+ if (!VALID_ANCHORS.has(options.anchor)) {
73310
+ throw new Error(`Viewport.label options.anchor must be one of: ${Array.from(VALID_ANCHORS).join(", ")}`);
73311
+ }
73312
+ out.anchor = options.anchor;
73313
+ }
73314
+ if (options.alwaysOnTop !== void 0) {
73315
+ if (typeof options.alwaysOnTop !== "boolean") throw new Error("Viewport.label options.alwaysOnTop must be a boolean");
73316
+ out.alwaysOnTop = options.alwaysOnTop;
73317
+ }
73318
+ return out;
73319
+ }
73320
+ function collectRenderLabel(text, at, options) {
73321
+ if (typeof text !== "string" || text.trim().length === 0) {
73322
+ throw new Error("Viewport.label text must be a non-empty string");
73323
+ }
73324
+ const normalizedAt = requireVec3$2(at, "Viewport.label at");
73325
+ const normalizedOptions = normalizeOptions(options);
73326
+ _collected$4.push({
73327
+ id: `render-label-${_nextId++}`,
73328
+ text: text.trim(),
73329
+ at: normalizedAt,
73330
+ ...normalizedOptions
73331
+ });
73332
+ }
73333
+ const Viewport = {
73334
+ /**
73335
+ * Add a render-only viewport label at a world-space point.
73336
+ *
73337
+ * **Details**
73338
+ *
73339
+ * `Viewport.label()` is for explanatory text that helps a viewer understand
73340
+ * the model. It does not create sketches, meshes, B-rep topology, exported
73341
+ * text, or face labels, so it stays off the OCCT path. Use `text2d()` only
73342
+ * when the letters should become manufactured geometry, such as raised
73343
+ * lettering, engraved serial numbers, or exported nameplates.
73344
+ *
73345
+ * Labels are collected during script execution and rendered by the viewport
73346
+ * as lightweight overlay annotations. They are ignored by exports and do not
73347
+ * appear in `objects`.
73348
+ *
73349
+ * **Example**
73350
+ *
73351
+ * ```js
73352
+ * Viewport.label('Bearing bore', [0, 0, 18], {
73353
+ * color: '#f8fafc',
73354
+ * background: '#0f172acc',
73355
+ * offset: [0, 0, 8],
73356
+ * anchor: 'bottom',
73357
+ * });
73358
+ *
73359
+ * return box(40, 30, 12);
73360
+ * ```
73361
+ *
73362
+ * @param text - Label text to display in the viewport
73363
+ * @param at - World-space anchor point `[x, y, z]`
73364
+ * @param options - Visual label options
73365
+ * @returns void
73366
+ * @category Viewport Labels
73367
+ */
73368
+ label(text, at, options) {
73369
+ collectRenderLabel(text, at, options);
73370
+ }
73371
+ };
73207
73372
  const DEFAULT_RENDER_STYLE = "classic";
73208
73373
  const RENDER_STYLE_OPTIONS = [
73209
73374
  {
@@ -73614,6 +73779,120 @@ function validateCamera(cam, label) {
73614
73779
  }
73615
73780
  return out;
73616
73781
  }
73782
+ function validateViewCamera(cam, label) {
73783
+ const validated = validateCamera(cam, label);
73784
+ if (!validated.position) {
73785
+ throw new Error(`${label}.position is required for named render views`);
73786
+ }
73787
+ if (!validated.target) {
73788
+ throw new Error(`${label}.target is required for named render views`);
73789
+ }
73790
+ return {
73791
+ ...validated,
73792
+ position: validated.position,
73793
+ target: validated.target
73794
+ };
73795
+ }
73796
+ function validateViews(views, label) {
73797
+ if (!views || typeof views !== "object" || Array.isArray(views)) {
73798
+ throw new Error(`${label} must be an object mapping view names to cameras`);
73799
+ }
73800
+ const out = {};
73801
+ for (const [name, view] of Object.entries(views)) {
73802
+ if (!name.trim()) {
73803
+ throw new Error(`${label} names must be non-empty strings`);
73804
+ }
73805
+ const viewLabel = `${label}.${name}`;
73806
+ if (!view || typeof view !== "object" || Array.isArray(view)) {
73807
+ throw new Error(`${viewLabel} must be a camera object or an object with a camera property`);
73808
+ }
73809
+ const hasExplicitCamera = Object.prototype.hasOwnProperty.call(view, "camera");
73810
+ if (hasExplicitCamera) {
73811
+ const camera = view.camera;
73812
+ if (!camera || typeof camera !== "object" || Array.isArray(camera)) {
73813
+ throw new Error(`${viewLabel}.camera must be an object`);
73814
+ }
73815
+ out[name] = { camera: validateViewCamera(camera, `${viewLabel}.camera`) };
73816
+ continue;
73817
+ }
73818
+ out[name] = { camera: validateViewCamera(view, viewLabel) };
73819
+ }
73820
+ return out;
73821
+ }
73822
+ function requireString(value, label) {
73823
+ if (typeof value !== "string" || !value.trim()) {
73824
+ throw new Error(`${label} must be a non-empty string`);
73825
+ }
73826
+ return value.trim();
73827
+ }
73828
+ function optionalString(value, label) {
73829
+ if (value === void 0) return void 0;
73830
+ return requireString(value, label);
73831
+ }
73832
+ function validateJourneyStep(step, label) {
73833
+ if (!step || typeof step !== "object" || Array.isArray(step)) {
73834
+ throw new Error(`${label} must be an object`);
73835
+ }
73836
+ const out = {
73837
+ id: requireString(step.id, `${label}.id`)
73838
+ };
73839
+ const title = optionalString(step.title, `${label}.title`);
73840
+ if (title !== void 0) out.title = title;
73841
+ const focus = optionalString(step.focus, `${label}.focus`);
73842
+ if (focus !== void 0) out.focus = focus;
73843
+ const caption = optionalString(step.caption, `${label}.caption`);
73844
+ if (caption !== void 0) out.caption = caption;
73845
+ if (step.camera !== void 0) {
73846
+ if (!step.camera || typeof step.camera !== "object" || Array.isArray(step.camera)) {
73847
+ throw new Error(`${label}.camera must be an object`);
73848
+ }
73849
+ out.camera = validateViewCamera(step.camera, `${label}.camera`);
73850
+ }
73851
+ return out;
73852
+ }
73853
+ function validateJourney(journey, label) {
73854
+ if (!journey || typeof journey !== "object" || Array.isArray(journey)) {
73855
+ throw new Error(`${label} must be an object`);
73856
+ }
73857
+ if (!Array.isArray(journey.steps) || journey.steps.length === 0) {
73858
+ throw new Error(`${label}.steps must be a non-empty array`);
73859
+ }
73860
+ const out = {
73861
+ steps: journey.steps.map((step, index) => validateJourneyStep(step, `${label}.steps[${index}]`))
73862
+ };
73863
+ const title = optionalString(journey.title, `${label}.title`);
73864
+ if (title !== void 0) out.title = title;
73865
+ const startsAt = optionalString(journey.startsAt, `${label}.startsAt`);
73866
+ if (startsAt !== void 0) out.startsAt = startsAt;
73867
+ if (journey.behavior !== void 0) {
73868
+ if (journey.behavior !== "opt-in" && journey.behavior !== "auto") {
73869
+ throw new Error(`${label}.behavior must be "opt-in" or "auto"`);
73870
+ }
73871
+ out.behavior = journey.behavior;
73872
+ }
73873
+ const seen2 = /* @__PURE__ */ new Set();
73874
+ for (const step of out.steps) {
73875
+ if (seen2.has(step.id)) {
73876
+ throw new Error(`${label}.steps contains duplicate step id "${step.id}"`);
73877
+ }
73878
+ seen2.add(step.id);
73879
+ }
73880
+ if (out.startsAt && !seen2.has(out.startsAt)) {
73881
+ throw new Error(`${label}.startsAt "${out.startsAt}" does not match any step id`);
73882
+ }
73883
+ return out;
73884
+ }
73885
+ function validateJourneys(journeys, label) {
73886
+ if (!journeys || typeof journeys !== "object" || Array.isArray(journeys)) {
73887
+ throw new Error(`${label} must be an object mapping journey ids to journey configs`);
73888
+ }
73889
+ const out = {};
73890
+ for (const [id, journey] of Object.entries(journeys)) {
73891
+ const normalizedId = requireString(id, `${label} journey id`);
73892
+ out[normalizedId] = validateJourney(journey, `${label}.${normalizedId}`);
73893
+ }
73894
+ return out;
73895
+ }
73617
73896
  function validateLight(light, label) {
73618
73897
  if (!light || typeof light !== "object") throw new Error(`${label} must be an object`);
73619
73898
  if (!VALID_LIGHT_TYPES.has(light.type)) {
@@ -73757,7 +74036,18 @@ function scene(options) {
73757
74036
  if (!options || typeof options !== "object") {
73758
74037
  throw new Error("scene(options) expects an options object");
73759
74038
  }
73760
- const current = _collected$3 ? { ..._collected$3 } : { background: null, camera: null, lights: null, environment: null, fog: null, postProcessing: null, ground: null, capture: null };
74039
+ const current = _collected$3 ? { ..._collected$3 } : {
74040
+ background: null,
74041
+ camera: null,
74042
+ views: null,
74043
+ journeys: null,
74044
+ lights: null,
74045
+ environment: null,
74046
+ fog: null,
74047
+ postProcessing: null,
74048
+ ground: null,
74049
+ capture: null
74050
+ };
73761
74051
  if (options.background !== void 0) {
73762
74052
  current.background = validateBackground(options.background, "scene.background");
73763
74053
  }
@@ -73768,6 +74058,14 @@ function scene(options) {
73768
74058
  const validated = validateCamera(options.camera, "scene.camera");
73769
74059
  current.camera = current.camera ? { ...current.camera, ...validated } : validated;
73770
74060
  }
74061
+ if (options.views !== void 0) {
74062
+ const validated = validateViews(options.views, "scene.views");
74063
+ current.views = current.views ? { ...current.views, ...validated } : validated;
74064
+ }
74065
+ if (options.journeys !== void 0) {
74066
+ const validated = validateJourneys(options.journeys, "scene.journeys");
74067
+ current.journeys = current.journeys ? { ...current.journeys, ...validated } : validated;
74068
+ }
73771
74069
  if (options.lights !== void 0) {
73772
74070
  if (!Array.isArray(options.lights)) {
73773
74071
  throw new Error("scene.lights must be an array");
@@ -73808,6 +74106,74 @@ function scene(options) {
73808
74106
  }
73809
74107
  _collected$3 = current;
73810
74108
  }
74109
+ const targetPath = (target) => {
74110
+ var _a3;
74111
+ const path2 = (_a3 = target.treePath) == null ? void 0 : _a3.filter((entry) => entry.trim());
74112
+ return path2 && path2.length > 0 ? path2.join("/") : target.name;
74113
+ };
74114
+ const hasErrorDiagnostic = (diagnostics) => diagnostics.some((diagnostic) => diagnostic.level === "error");
74115
+ function resolveJourneyFocus(focus, targets) {
74116
+ return targets.filter((target) => target.name === focus || targetPath(target) === focus);
74117
+ }
74118
+ function formatAvailableTargets(targets) {
74119
+ return targets.map((target) => targetPath(target)).filter((value, index, values) => values.indexOf(value) === index).sort().slice(0, 8);
74120
+ }
74121
+ function resolveSceneJourneyTargets(config, targets) {
74122
+ if (!(config == null ? void 0 : config.journeys)) return config;
74123
+ const journeys = {};
74124
+ for (const [journeyId, journey] of Object.entries(config.journeys)) {
74125
+ const journeyDiagnostics = [...journey.diagnostics ?? []];
74126
+ const steps = journey.steps.map((step) => {
74127
+ const stepDiagnostics = [...step.diagnostics ?? []];
74128
+ let resolvedFocusId = null;
74129
+ let resolvedFocusPath = null;
74130
+ if (step.focus) {
74131
+ const matches = resolveJourneyFocus(step.focus, targets);
74132
+ if (matches.length === 1) {
74133
+ resolvedFocusId = matches[0].id;
74134
+ resolvedFocusPath = targetPath(matches[0]);
74135
+ } else if (matches.length === 0) {
74136
+ stepDiagnostics.push({
74137
+ level: "error",
74138
+ stepId: step.id,
74139
+ message: `focus "${step.focus}" did not match any returned object by name or tree path.`,
74140
+ suggestions: formatAvailableTargets(targets)
74141
+ });
74142
+ } else {
74143
+ stepDiagnostics.push({
74144
+ level: "error",
74145
+ stepId: step.id,
74146
+ message: `focus "${step.focus}" matched ${matches.length} objects. Use a slash-separated tree path.`,
74147
+ suggestions: matches.map((match) => targetPath(match))
74148
+ });
74149
+ }
74150
+ } else if (!step.camera) {
74151
+ stepDiagnostics.push({
74152
+ level: "warning",
74153
+ stepId: step.id,
74154
+ message: "step has no focus or explicit camera, so the viewer can show the caption but cannot move the camera."
74155
+ });
74156
+ }
74157
+ journeyDiagnostics.push(...stepDiagnostics);
74158
+ return {
74159
+ ...step,
74160
+ resolvedFocusId,
74161
+ resolvedFocusPath,
74162
+ diagnostics: stepDiagnostics.length > 0 ? stepDiagnostics : void 0
74163
+ };
74164
+ });
74165
+ journeys[journeyId] = {
74166
+ ...journey,
74167
+ steps,
74168
+ valid: !hasErrorDiagnostic(journeyDiagnostics),
74169
+ diagnostics: journeyDiagnostics.length > 0 ? journeyDiagnostics : void 0
74170
+ };
74171
+ }
74172
+ return {
74173
+ ...config,
74174
+ journeys
74175
+ };
74176
+ }
73811
74177
  const validateColor = (value, label) => {
73812
74178
  if (typeof value !== "string") throw new Error(`${label} must be a string`);
73813
74179
  const trimmed = value.trim();
@@ -75053,9 +75419,15 @@ function cross(a, b) {
75053
75419
  function add(a, b) {
75054
75420
  return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
75055
75421
  }
75422
+ function sub$1(a, b) {
75423
+ return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
75424
+ }
75056
75425
  function scale(v, s) {
75057
75426
  return [v[0] * s, v[1] * s, v[2] * s];
75058
75427
  }
75428
+ function dot$1(a, b) {
75429
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
75430
+ }
75059
75431
  function lerp$1(a, b, t) {
75060
75432
  return a + (b - a) * t;
75061
75433
  }
@@ -75088,6 +75460,65 @@ function sideVectors(axis) {
75088
75460
  function cloneQuery(query) {
75089
75461
  return { side: query.side, u: query.u, v: query.v, offset: query.offset };
75090
75462
  }
75463
+ function normalizedSide(side) {
75464
+ return side === "back" ? "rear" : side;
75465
+ }
75466
+ function profileExponent(station) {
75467
+ if (station.profile.kind === "superEllipse") return station.profile.exponent ?? 3.2;
75468
+ if (station.profile.kind === "roundedRect") return 4.5;
75469
+ return 2;
75470
+ }
75471
+ function superEllipsePoint(rx, ry, exponent, angle) {
75472
+ const cos2 = Math.cos(angle);
75473
+ const sin2 = Math.sin(angle);
75474
+ const x = rx * Math.sign(cos2) * Math.abs(cos2) ** (2 / exponent);
75475
+ const y = ry * Math.sign(sin2) * Math.abs(sin2) ** (2 / exponent);
75476
+ const nx = Math.sign(x) * Math.abs(x / Math.max(rx, EPS$4)) ** Math.max(exponent - 1, 1e-3);
75477
+ const ny = Math.sign(y) * Math.abs(y / Math.max(ry, EPS$4)) ** Math.max(exponent - 1, 1e-3);
75478
+ const nLen = Math.hypot(nx, ny);
75479
+ return {
75480
+ point: [x, y],
75481
+ normal: nLen < EPS$4 ? [Math.sign(cos2), Math.sign(sin2)] : [nx / nLen, ny / nLen]
75482
+ };
75483
+ }
75484
+ function angleForSide(side, u) {
75485
+ const t = clamp$4(u, 0, 1);
75486
+ if (side === "right") return -Math.PI / 2 + t * Math.PI;
75487
+ if (side === "left") return Math.PI / 2 + t * Math.PI;
75488
+ if (side === "top") return Math.PI - t * Math.PI;
75489
+ if (side === "bottom") return Math.PI + t * Math.PI;
75490
+ return null;
75491
+ }
75492
+ function sideSpan(side, width, depth) {
75493
+ if (side === "left" || side === "right") return Math.max(depth, EPS$4);
75494
+ if (side === "top" || side === "bottom") return Math.max(width, EPS$4);
75495
+ return Math.max(width, depth, EPS$4);
75496
+ }
75497
+ function interpolateQuery(a, b, t) {
75498
+ const sideA = normalizedSide(a.side);
75499
+ const sideB = normalizedSide(b.side);
75500
+ if (sideA !== sideB) {
75501
+ throw new Error(
75502
+ `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.`
75503
+ );
75504
+ }
75505
+ return {
75506
+ side: sideA,
75507
+ u: lerp$1(a.u ?? 0.5, b.u ?? 0.5, t),
75508
+ v: lerp$1(a.v ?? 0.5, b.v ?? 0.5, t),
75509
+ offset: lerp$1(a.offset ?? 0, b.offset ?? 0, t)
75510
+ };
75511
+ }
75512
+ function resolvePathQueries(points) {
75513
+ return points.map((point2) => point2 instanceof ProductSurfaceRef ? point2.querySpec() : cloneQuery(point2));
75514
+ }
75515
+ function orientGridToNormal(grid, desiredNormal) {
75516
+ if (grid.length < 2 || grid[0].length < 2) return grid;
75517
+ const widthEdge = sub$1(grid[grid.length - 1][0], grid[0][0]);
75518
+ const lengthEdge = sub$1(grid[0][grid[0].length - 1], grid[0][0]);
75519
+ const actual = norm(cross(widthEdge, lengthEdge));
75520
+ return dot$1(actual, desiredNormal) < 0 ? [...grid].reverse() : grid;
75521
+ }
75091
75522
  function isStationBuilder(input) {
75092
75523
  return typeof input.toSpec === "function";
75093
75524
  }
@@ -75189,6 +75620,15 @@ class ProductSkin {
75189
75620
  curveOnSurface(name, points) {
75190
75621
  return points.map((point2, index) => new ProductSurfaceRef(this, { u: 0.5, v: 0.5, ...point2 }, `${name}/${index}`));
75191
75622
  }
75623
+ /**
75624
+ * Create a fluent surface helper for refs and conformal features on one side of this skin.
75625
+ *
75626
+ * Use this when several refs or ribbons share the same skin side; side-local helpers keep
75627
+ * path points concise and make it harder to mix sides accidentally.
75628
+ */
75629
+ surface(side) {
75630
+ return new ProductSurfaceBuilder(this, side);
75631
+ }
75192
75632
  /** Interpolate center, width, and depth at a normalized v or absolute axis value. */
75193
75633
  stationAt(vOrAxis) {
75194
75634
  const axisValue = vOrAxis >= 0 && vOrAxis <= 1 ? lerp$1(this.axisMin, this.axisMax, vOrAxis) : clamp$4(vOrAxis, this.axisMin, this.axisMax);
@@ -75207,7 +75647,9 @@ class ProductSkin {
75207
75647
  width: lerp$1(a.profile.width, b.profile.width, t),
75208
75648
  depth: lerp$1(a.profile.depth, b.profile.depth, t),
75209
75649
  dWidth: (b.profile.width - a.profile.width) / span,
75210
- dDepth: (b.profile.depth - a.profile.depth) / span
75650
+ dDepth: (b.profile.depth - a.profile.depth) / span,
75651
+ exponent: lerp$1(profileExponent(a), profileExponent(b), t),
75652
+ kind: a.profile.kind === b.profile.kind ? a.profile.kind : "custom"
75211
75653
  };
75212
75654
  }
75213
75655
  const last = sorted[sorted.length - 1];
@@ -75217,12 +75659,14 @@ class ProductSkin {
75217
75659
  width: last.profile.width,
75218
75660
  depth: last.profile.depth,
75219
75661
  dWidth: 0,
75220
- dDepth: 0
75662
+ dDepth: 0,
75663
+ exponent: profileExponent(last),
75664
+ kind: last.profile.kind
75221
75665
  };
75222
75666
  }
75223
75667
  /** Build a local surface frame from a side/u/v query. */
75224
75668
  frame(query) {
75225
- const side = query.side === "back" ? "rear" : query.side;
75669
+ const side = normalizedSide(query.side);
75226
75670
  const offset2 = query.offset ?? 0;
75227
75671
  const basis = sideVectors(this.axis);
75228
75672
  const isFrontCap = side === "front";
@@ -75246,11 +75690,31 @@ class ProductSkin {
75246
75690
  }
75247
75691
  const station = this.stationAt(query.v ?? 0.5);
75248
75692
  const u = clamp$4(query.u ?? 0.5, 0, 1) - 0.5;
75693
+ const sideAngle = angleForSide(side, query.u ?? 0.5);
75249
75694
  let crossA = 0;
75250
75695
  let crossB = 0;
75251
75696
  let normal = [0, 0, 1];
75252
75697
  let tangentU = basis.crossA;
75253
- if (side === "right") {
75698
+ if (sideAngle != null) {
75699
+ const section = superEllipsePoint(station.width / 2, station.depth / 2, station.exponent, sideAngle);
75700
+ crossA = section.point[0];
75701
+ crossB = section.point[1];
75702
+ normal = norm(add(scale(basis.crossA, section.normal[0]), scale(basis.crossB, section.normal[1])));
75703
+ const delta = 2e-3;
75704
+ const prev = superEllipsePoint(
75705
+ station.width / 2,
75706
+ station.depth / 2,
75707
+ station.exponent,
75708
+ angleForSide(side, clamp$4((query.u ?? 0.5) - delta, 0, 1)) ?? sideAngle
75709
+ ).point;
75710
+ const next = superEllipsePoint(
75711
+ station.width / 2,
75712
+ station.depth / 2,
75713
+ station.exponent,
75714
+ angleForSide(side, clamp$4((query.u ?? 0.5) + delta, 0, 1)) ?? sideAngle
75715
+ ).point;
75716
+ tangentU = norm(add(scale(basis.crossA, next[0] - prev[0]), scale(basis.crossB, next[1] - prev[1])));
75717
+ } else if (side === "right") {
75254
75718
  crossA = station.width / 2;
75255
75719
  crossB = u * station.depth;
75256
75720
  normal = basis.crossA;
@@ -75272,7 +75736,7 @@ class ProductSkin {
75272
75736
  tangentU = basis.crossA;
75273
75737
  }
75274
75738
  normal = norm(normal);
75275
- tangentU = norm(tangentU);
75739
+ tangentU = norm(sub$1(tangentU, scale(normal, dot$1(tangentU, normal))));
75276
75740
  const tangentV = norm(cross(normal, tangentU));
75277
75741
  const point2 = add(add(station.center, add(scale(basis.crossA, crossA), scale(basis.crossB, crossB))), scale(normal, offset2));
75278
75742
  return {
@@ -75592,6 +76056,303 @@ class ProductHandleBuilder {
75592
76056
  return new ProductHandleFeature(grip, upperPad, lowerPad);
75593
76057
  }
75594
76058
  }
76059
+ class ProductSurfaceBuilder {
76060
+ constructor(skin, side) {
76061
+ this.skin = skin;
76062
+ this.side = side;
76063
+ }
76064
+ /** Create a ref on this skin side. */
76065
+ ref(u = 0.5, v = 0.5, offset2) {
76066
+ return Product.ref(this.skin, {
76067
+ side: this.side,
76068
+ u,
76069
+ v,
76070
+ ...offset2 != null ? { offset: offset2 } : {}
76071
+ });
76072
+ }
76073
+ /** Create a side/u/v query on this skin side. */
76074
+ uv(u = 0.5, v = 0.5, offset2) {
76075
+ return {
76076
+ side: this.side,
76077
+ u,
76078
+ v,
76079
+ ...offset2 != null ? { offset: offset2 } : {}
76080
+ };
76081
+ }
76082
+ /**
76083
+ * Start a conformal ribbon on this skin side.
76084
+ *
76085
+ * Path points use side-local `u`/`v` coordinates; this builder supplies the side.
76086
+ * The returned ProductRibbonBuilder is already bound to the source skin and can be further
76087
+ * configured before build(). Use `widthSamples` >= 3 when the ribbon must visibly wrap over
76088
+ * curved product sections instead of behaving like a flat strip.
76089
+ */
76090
+ ribbon(name, points, options = {}) {
76091
+ if (points.length < 2) throw new Error("Product.surface(...).ribbon(name, points) requires at least two path points");
76092
+ const path2 = points.map((point2) => ({
76093
+ side: this.side,
76094
+ u: point2.u ?? 0.5,
76095
+ v: point2.v ?? 0.5,
76096
+ ...point2.offset != null ? { offset: point2.offset } : {}
76097
+ }));
76098
+ return new ProductRibbonBuilder(name).on(this.skin, path2, options);
76099
+ }
76100
+ }
76101
+ class ProductRibbonBuilder {
76102
+ constructor(name) {
76103
+ __publicField(this, "skinValue");
76104
+ __publicField(this, "queryPath", []);
76105
+ __publicField(this, "refPath", []);
76106
+ __publicField(this, "widthValue", 6);
76107
+ __publicField(this, "thicknessValue", 0.8);
76108
+ __publicField(this, "offsetValue", 0.25);
76109
+ __publicField(this, "samplesValue", 24);
76110
+ __publicField(this, "widthSamplesValue", 5);
76111
+ __publicField(this, "resolutionValue");
76112
+ __publicField(this, "materialValue");
76113
+ __publicField(this, "colorValue");
76114
+ __publicField(this, "lastDiagnosticsValue");
76115
+ this.name = name;
76116
+ if (!name || !name.trim()) throw new Error("Product.ribbon(name) requires a non-empty name");
76117
+ }
76118
+ /**
76119
+ * Follow a ProductSkin with side/u/v path queries or refs.
76120
+ *
76121
+ * This is the highest-fidelity mode because every interpolated sample is resolved through
76122
+ * ProductSkin.frame(), so the ribbon bends along the selected side as station width/depth changes.
76123
+ * All query path points must stay on one side; split side transitions into separate ribbons.
76124
+ */
76125
+ on(skin, points, options = {}) {
76126
+ if (points.length < 2) throw new Error("Product.ribbon().on(skin, points) requires at least two path points");
76127
+ this.skinValue = skin;
76128
+ this.queryPath = resolvePathQueries(points);
76129
+ this.refPath = [];
76130
+ return this.applyOptions(options);
76131
+ }
76132
+ /**
76133
+ * Follow explicit surface refs.
76134
+ *
76135
+ * Useful for named refs or paths assembled elsewhere. The builder resolves each ref frame and
76136
+ * interpolates between those frames; use on(skin, points) when you need full skin-side sampling
76137
+ * between sparse control points.
76138
+ */
76139
+ fromRefs(points, options = {}) {
76140
+ if (points.length < 2) throw new Error("Product.ribbon().fromRefs(points) requires at least two refs");
76141
+ this.skinValue = void 0;
76142
+ this.queryPath = [];
76143
+ this.refPath = [...points];
76144
+ return this.applyOptions(options);
76145
+ }
76146
+ /** Set ribbon width in millimeters. */
76147
+ width(width) {
76148
+ if (!Number.isFinite(width) || width <= 0) throw new Error("Product.ribbon().width(width) requires a positive finite number");
76149
+ this.widthValue = width;
76150
+ return this;
76151
+ }
76152
+ /** Set solid thickness outward from the source surface in millimeters. */
76153
+ thickness(thickness) {
76154
+ if (!Number.isFinite(thickness) || thickness <= 0)
76155
+ throw new Error("Product.ribbon().thickness(thickness) requires a positive finite number");
76156
+ this.thicknessValue = thickness;
76157
+ return this;
76158
+ }
76159
+ /** Set positive clearance between the source surface and the ribbon's inner face. */
76160
+ offset(offset2) {
76161
+ if (!Number.isFinite(offset2)) throw new Error("Product.ribbon().offset(offset) requires a finite number");
76162
+ this.offsetValue = offset2;
76163
+ return this;
76164
+ }
76165
+ /** Set samples along the path. */
76166
+ samples(samples) {
76167
+ if (!Number.isFinite(samples) || samples < 2) throw new Error("Product.ribbon().samples(samples) requires a value >= 2");
76168
+ this.samplesValue = Math.round(samples);
76169
+ return this;
76170
+ }
76171
+ /** Set samples across the width. Use 3+ to bend over curved cross-sections. */
76172
+ widthSamples(samples) {
76173
+ if (!Number.isFinite(samples) || samples < 2) throw new Error("Product.ribbon().widthSamples(samples) requires a value >= 2");
76174
+ this.widthSamplesValue = Math.round(samples);
76175
+ return this;
76176
+ }
76177
+ /** Set NURBS tessellation resolution. */
76178
+ resolution(resolution) {
76179
+ if (!Number.isFinite(resolution) || resolution < 2) throw new Error("Product.ribbon().resolution(resolution) requires a value >= 2");
76180
+ this.resolutionValue = Math.round(resolution);
76181
+ return this;
76182
+ }
76183
+ /** Apply a product material preset. */
76184
+ material(material) {
76185
+ this.materialValue = material;
76186
+ return this;
76187
+ }
76188
+ /** Apply a simple color override. */
76189
+ color(color) {
76190
+ this.colorValue = color;
76191
+ return this;
76192
+ }
76193
+ /** Build a conformal ribbon as a thin NURBS surface solid. */
76194
+ build(options = {}) {
76195
+ return this.buildWithDiagnostics(options).shape;
76196
+ }
76197
+ /**
76198
+ * Build a conformal ribbon and return surface-feature diagnostics.
76199
+ *
76200
+ * Use this while validating API usage or model fidelity; diagnostics report sampling counts,
76201
+ * side-span clamping, lowering mode, and warnings that should be visible in reviews.
76202
+ */
76203
+ buildWithDiagnostics(options = {}) {
76204
+ this.applyOptions(options);
76205
+ const gridResult = this.skinValue ? this.buildSkinGrid(this.skinValue, this.queryPath) : this.buildRefGrid(this.refPath);
76206
+ const desiredNormal = this.centerDesiredNormal();
76207
+ let ribbon = nurbsSurface(orientGridToNormal(gridResult.grid, desiredNormal), {
76208
+ degreeU: Math.min(3, this.widthSamplesValue - 1),
76209
+ degreeV: Math.min(3, this.samplesValue - 1),
76210
+ thickness: this.thicknessValue,
76211
+ resolution: this.resolutionValue ?? Math.max(this.samplesValue, this.widthSamplesValue, 12),
76212
+ approximate: true
76213
+ }).as(this.name);
76214
+ if (this.colorValue) ribbon = ribbon.color(this.colorValue);
76215
+ const shape = applyMaterial(ribbon, this.materialValue);
76216
+ this.lastDiagnosticsValue = gridResult.diagnostics;
76217
+ return { shape, diagnostics: this.cloneDiagnostics(gridResult.diagnostics) };
76218
+ }
76219
+ /** Return diagnostics from the most recent build, if this builder has been built. */
76220
+ diagnostics() {
76221
+ return this.lastDiagnosticsValue ? this.cloneDiagnostics(this.lastDiagnosticsValue) : void 0;
76222
+ }
76223
+ applyOptions(options) {
76224
+ if (options.width != null) this.width(options.width);
76225
+ if (options.thickness != null) this.thickness(options.thickness);
76226
+ if (options.offset != null) this.offset(options.offset);
76227
+ if (options.samples != null) this.samples(options.samples);
76228
+ if (options.widthSamples != null) this.widthSamples(options.widthSamples);
76229
+ if (options.resolution != null) this.resolution(options.resolution);
76230
+ if (options.material) this.material(options.material);
76231
+ if (options.color) this.color(options.color);
76232
+ return this;
76233
+ }
76234
+ centerDesiredNormal() {
76235
+ if (this.skinValue && this.queryPath.length > 0) {
76236
+ const mid = this.samplePathQuery(0.5);
76237
+ return this.skinValue.frame({ ...mid, offset: (mid.offset ?? 0) + this.offsetValue }).normal;
76238
+ }
76239
+ if (this.refPath.length > 0) return this.refPath[Math.floor(this.refPath.length / 2)].frame({ offset: this.offsetValue }).normal;
76240
+ return [0, 0, 1];
76241
+ }
76242
+ samplePathQuery(t) {
76243
+ if (this.queryPath.length < 2) throw new Error("Product.ribbon().on(...) must be called before .build()");
76244
+ const segmentCount = this.queryPath.length - 1;
76245
+ const scaled = clamp$4(t, 0, 1) * segmentCount;
76246
+ const segment = Math.min(segmentCount - 1, Math.floor(scaled));
76247
+ const localT = scaled - segment;
76248
+ return interpolateQuery(this.queryPath[segment], this.queryPath[segment + 1], localT);
76249
+ }
76250
+ buildSkinGrid(skin, path2) {
76251
+ if (path2.length < 2) throw new Error("Product.ribbon().on(skin, points) must be called before .build()");
76252
+ const side = normalizedSide(path2[0].side);
76253
+ if (side === "front" || side === "rear") {
76254
+ throw new Error(
76255
+ "Product.ribbon().on(...) supports side ribbons on left/right/top/bottom surfaces. Use Product.panel() for front/rear caps."
76256
+ );
76257
+ }
76258
+ for (const point2 of path2) {
76259
+ if (normalizedSide(point2.side) !== side) {
76260
+ throw new Error("Product.ribbon().on(...) currently supports one side per ribbon. Split ribbons at side transitions.");
76261
+ }
76262
+ }
76263
+ const rows = Array.from({ length: this.widthSamplesValue }, () => []);
76264
+ let clampedUCount = 0;
76265
+ let maxUClampDistance = 0;
76266
+ for (let i = 0; i < this.samplesValue; i += 1) {
76267
+ const along = this.samplesValue === 1 ? 0 : i / (this.samplesValue - 1);
76268
+ const center = this.samplePathQuery(along);
76269
+ const station = skin.stationAt(center.v ?? 0.5);
76270
+ const span = sideSpan(side, station.width, station.depth);
76271
+ for (let j = 0; j < this.widthSamplesValue; j += 1) {
76272
+ const across = this.widthSamplesValue === 1 ? 0 : j / (this.widthSamplesValue - 1) - 0.5;
76273
+ const rawU = (center.u ?? 0.5) + across * this.widthValue / span;
76274
+ const u = clamp$4(rawU, 0, 1);
76275
+ const clampDistance = Math.abs(rawU - u) * span;
76276
+ if (clampDistance > EPS$4) {
76277
+ clampedUCount += 1;
76278
+ maxUClampDistance = Math.max(maxUClampDistance, clampDistance);
76279
+ }
76280
+ const query = {
76281
+ ...center,
76282
+ side,
76283
+ u,
76284
+ offset: (center.offset ?? 0) + this.offsetValue + this.thicknessValue
76285
+ };
76286
+ rows[j].push(skin.frame(query).point);
76287
+ }
76288
+ }
76289
+ return {
76290
+ grid: rows,
76291
+ diagnostics: this.makeDiagnostics({
76292
+ skin: skin.name,
76293
+ side,
76294
+ pathPointCount: path2.length,
76295
+ clampedUCount,
76296
+ maxUClampDistance
76297
+ })
76298
+ };
76299
+ }
76300
+ buildRefGrid(refs) {
76301
+ if (refs.length < 2) throw new Error("Product.ribbon().fromRefs(points) must be called before .build()");
76302
+ const frames = refs.map((ref) => ref.frame({ offset: this.offsetValue + this.thicknessValue }));
76303
+ const rows = Array.from({ length: this.widthSamplesValue }, () => []);
76304
+ for (const frame of frames) {
76305
+ for (let j = 0; j < this.widthSamplesValue; j += 1) {
76306
+ const across = this.widthSamplesValue === 1 ? 0 : j / (this.widthSamplesValue - 1) - 0.5;
76307
+ rows[j].push(add(frame.point, scale(frame.tangentU, across * this.widthValue)));
76308
+ }
76309
+ }
76310
+ this.samplesValue = refs.length;
76311
+ return {
76312
+ grid: rows,
76313
+ diagnostics: this.makeDiagnostics({
76314
+ pathPointCount: refs.length,
76315
+ clampedUCount: 0,
76316
+ maxUClampDistance: 0
76317
+ })
76318
+ };
76319
+ }
76320
+ makeDiagnostics(input) {
76321
+ const resolution = this.resolutionValue ?? Math.max(this.samplesValue, this.widthSamplesValue, 12);
76322
+ const warnings = [];
76323
+ if (input.clampedUCount > 0) {
76324
+ warnings.push(
76325
+ `Ribbon '${this.name}' was clipped to the ${input.side ?? "surface"} UV bounds at ${input.clampedUCount} sampled point(s).`
76326
+ );
76327
+ }
76328
+ if (this.samplesValue < 8) warnings.push(`Ribbon '${this.name}' uses low along-path sampling; increase samples() for smoother bends.`);
76329
+ if (this.widthSamplesValue < 3)
76330
+ warnings.push(`Ribbon '${this.name}' uses low width sampling; use widthSamples(3+) to show cross-surface curvature.`);
76331
+ return {
76332
+ name: this.name,
76333
+ ...input.skin ? { skin: input.skin } : {},
76334
+ ...input.side ? { side: input.side } : {},
76335
+ pathPointCount: input.pathPointCount,
76336
+ width: this.widthValue,
76337
+ thickness: this.thicknessValue,
76338
+ offset: this.offsetValue,
76339
+ samples: this.samplesValue,
76340
+ widthSamples: this.widthSamplesValue,
76341
+ resolution,
76342
+ lowering: "nurbsSurface",
76343
+ expectedFidelity: "mixed",
76344
+ clampedUCount: input.clampedUCount,
76345
+ maxUClampDistance: input.maxUClampDistance,
76346
+ warnings
76347
+ };
76348
+ }
76349
+ cloneDiagnostics(diagnostics) {
76350
+ return {
76351
+ ...diagnostics,
76352
+ warnings: [...diagnostics.warnings]
76353
+ };
76354
+ }
76355
+ }
75595
76356
  const Product = {
75596
76357
  /** Start a named product skin builder. */
75597
76358
  skin(name) {
@@ -75664,10 +76425,27 @@ const Product = {
75664
76425
  ref(skin, query) {
75665
76426
  return new ProductSurfaceRef(skin, query);
75666
76427
  },
76428
+ /**
76429
+ * Create a fluent surface helper for refs and conformal features on one side of a skin.
76430
+ *
76431
+ * Equivalent to skin.surface(side), useful when writing in Product.* namespace style.
76432
+ */
76433
+ surface(skin, side) {
76434
+ return skin.surface(side);
76435
+ },
75667
76436
  /** Start a panel feature builder. */
75668
76437
  panel(name) {
75669
76438
  return new ProductPanelBuilder(name);
75670
76439
  },
76440
+ /**
76441
+ * Start a conformal ribbon/trim builder for details that should bend with a ProductSkin.
76442
+ *
76443
+ * Call .on(skin, points) for side/u/v sampling or .fromRefs(points) for explicit surface refs,
76444
+ * then configure width, thickness, offset, sampling, material, and color before build().
76445
+ */
76446
+ ribbon(name) {
76447
+ return new ProductRibbonBuilder(name);
76448
+ },
75671
76449
  /** Start a spout/nozzle feature builder. */
75672
76450
  spout(name) {
75673
76451
  return new ProductSpoutBuilder(name);
@@ -76810,6 +77588,7 @@ function cameraTrajectory(defOrFn, options) {
76810
77588
  throw new Error('cameraTrajectory(): each keyframe must have either an "orbit" or "position" property');
76811
77589
  }
76812
77590
  }
77591
+ var define_process_env_default = {};
76813
77592
  function resolveEdges(shape, edges) {
76814
77593
  if (!edges) {
76815
77594
  return selectEdges(shape);
@@ -76831,6 +77610,76 @@ function isEdgeSegment(value) {
76831
77610
  function isEdgeReferenceLike(value) {
76832
77611
  return typeof value === "object" && value !== null && "edges" in value && typeof value.edges === "function";
76833
77612
  }
77613
+ const BROAD_EDGE_FEATURE_DEFAULT_BUDGET = {
77614
+ live: 0,
77615
+ default: 12,
77616
+ high: Number.POSITIVE_INFINITY
77617
+ };
77618
+ let broadEdgeFeatureBudget = null;
77619
+ function readBroadEdgeFeatureEnv(name) {
77620
+ return typeof process !== "undefined" ? define_process_env_default == null ? void 0 : define_process_env_default[name] : void 0;
77621
+ }
77622
+ function resolveBroadEdgeFeatureBudget() {
77623
+ if (readBroadEdgeFeatureEnv("FORGECAD_ALLOW_BROAD_EDGE_FEATURES") === "1") return Number.POSITIVE_INFINITY;
77624
+ const override = readBroadEdgeFeatureEnv("FORGECAD_BROAD_EDGE_FEATURE_BUDGET");
77625
+ if (override != null && override.trim() !== "") {
77626
+ const parsed = Number(override);
77627
+ if (Number.isFinite(parsed) && parsed >= 0) return parsed;
77628
+ }
77629
+ return BROAD_EDGE_FEATURE_DEFAULT_BUDGET[getForgeQualityPreset()];
77630
+ }
77631
+ function resetBroadEdgeFeatureBudget() {
77632
+ broadEdgeFeatureBudget = null;
77633
+ }
77634
+ function remainingBroadEdgeFeatureBudget() {
77635
+ if (broadEdgeFeatureBudget === null) broadEdgeFeatureBudget = resolveBroadEdgeFeatureBudget();
77636
+ return broadEdgeFeatureBudget;
77637
+ }
77638
+ function consumeBroadEdgeFeatureBudget(edgeCount) {
77639
+ const remaining = remainingBroadEdgeFeatureBudget();
77640
+ if (!Number.isFinite(remaining)) return true;
77641
+ if (edgeCount > remaining) return false;
77642
+ broadEdgeFeatureBudget = Math.max(0, remaining - edgeCount);
77643
+ return true;
77644
+ }
77645
+ function shouldSkipBroadEdgeFeature(operation, edgeCount) {
77646
+ if (consumeBroadEdgeFeatureBudget(edgeCount)) return false;
77647
+ const remaining = Math.max(0, remainingBroadEdgeFeatureBudget());
77648
+ emitRuntimeWarning(
77649
+ `${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.`
77650
+ );
77651
+ return true;
77652
+ }
77653
+ function shouldSkipExhaustedBroadEdgeFeature(operation) {
77654
+ const remaining = remainingBroadEdgeFeatureBudget();
77655
+ if (!Number.isFinite(remaining) || remaining > 0) return false;
77656
+ emitRuntimeWarning(
77657
+ `${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.`
77658
+ );
77659
+ return true;
77660
+ }
77661
+ function estimateSelectorlessEdgeCount(plan) {
77662
+ if (!plan) return null;
77663
+ switch (plan.kind) {
77664
+ case "box":
77665
+ return 12;
77666
+ case "queryOwner":
77667
+ case "transform":
77668
+ return estimateSelectorlessEdgeCount(plan.base);
77669
+ default:
77670
+ return null;
77671
+ }
77672
+ }
77673
+ function shouldSkipUnestimatedBroadEdgeFeature(operation, target) {
77674
+ const remaining = remainingBroadEdgeFeatureBudget();
77675
+ if (!Number.isFinite(remaining)) return false;
77676
+ const estimatedEdges = estimateSelectorlessEdgeCount(getShapeCompilePlan(target));
77677
+ if (estimatedEdges !== null && estimatedEdges <= remaining) return false;
77678
+ emitRuntimeWarning(
77679
+ `${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.`
77680
+ );
77681
+ return true;
77682
+ }
76834
77683
  function edgesToTargets(edges) {
76835
77684
  return edges.map((e) => ({
76836
77685
  midpoint: [e.midpoint[0], e.midpoint[1], e.midpoint[2]],
@@ -76844,10 +77693,13 @@ function fillet(shape, radius, edges, segments = 16) {
76844
77693
  throw new Error("fillet() requires a positive finite radius.");
76845
77694
  }
76846
77695
  const target = shape;
77696
+ if (edges === void 0 && shouldSkipExhaustedBroadEdgeFeature("fillet")) return target;
77697
+ if (edges === void 0 && shouldSkipUnestimatedBroadEdgeFeature("fillet", target)) return target;
76847
77698
  const resolvedEdges = resolveEdges(target, edges);
76848
77699
  if (resolvedEdges.length === 0) {
76849
77700
  throw new Error("fillet(): no edges match the given selection.");
76850
77701
  }
77702
+ if (edges === void 0 && shouldSkipBroadEdgeFeature("fillet", resolvedEdges.length)) return target;
76851
77703
  const basePlan = getShapeCompilePlan(target);
76852
77704
  const plan = {
76853
77705
  kind: "filletEdges",
@@ -76867,10 +77719,13 @@ function chamfer(shape, size, edges) {
76867
77719
  throw new Error("chamfer() requires a positive finite size.");
76868
77720
  }
76869
77721
  const target = shape;
77722
+ if (edges === void 0 && shouldSkipExhaustedBroadEdgeFeature("chamfer")) return target;
77723
+ if (edges === void 0 && shouldSkipUnestimatedBroadEdgeFeature("chamfer", target)) return target;
76870
77724
  const resolvedEdges = resolveEdges(target, edges);
76871
77725
  if (resolvedEdges.length === 0) {
76872
77726
  throw new Error("chamfer(): no edges match the given selection.");
76873
77727
  }
77728
+ if (edges === void 0 && shouldSkipBroadEdgeFeature("chamfer", resolvedEdges.length)) return target;
76874
77729
  const basePlan = getShapeCompilePlan(target);
76875
77730
  const plan = {
76876
77731
  kind: "chamferEdges",
@@ -100016,6 +100871,7 @@ const verify = {
100016
100871
  };
100017
100872
  function resetExecutionSession(logs) {
100018
100873
  resetCollectedAssemblies();
100874
+ resetBroadEdgeFeatureBudget();
100019
100875
  resetParams();
100020
100876
  resetShapeQueryOwnerIds();
100021
100877
  resetDimensions();
@@ -100024,6 +100880,7 @@ function resetExecutionSession(logs) {
100024
100880
  resetSheetStock();
100025
100881
  resetRobotExport();
100026
100882
  resetCutPlanes();
100883
+ resetRenderLabels();
100027
100884
  resetCameraTrajectory();
100028
100885
  resetExplodeView();
100029
100886
  resetJointsView();
@@ -100087,6 +100944,7 @@ function collectSuccessfulExecutionSnapshot(args) {
100087
100944
  bom: getCollectedBom(),
100088
100945
  sheetStock: getCollectedSheetStock(),
100089
100946
  cutPlanes: getCollectedCutPlanes(),
100947
+ renderLabels: getCollectedRenderLabels(),
100090
100948
  cameraTrajectory: getCollectedCameraTrajectory(),
100091
100949
  explodeView: getCollectedExplodeView(),
100092
100950
  jointsView: getCollectedJointsView(),
@@ -100110,6 +100968,7 @@ function collectFailedExecutionSnapshot(args) {
100110
100968
  bom: getCollectedBom(),
100111
100969
  sheetStock: getCollectedSheetStock(),
100112
100970
  cutPlanes: getCollectedCutPlanes(),
100971
+ renderLabels: getCollectedRenderLabels(),
100113
100972
  cameraTrajectory: getCollectedCameraTrajectory(),
100114
100973
  explodeView: getCollectedExplodeView(),
100115
100974
  jointsView: getCollectedJointsView(),
@@ -100255,13 +101114,15 @@ function formatLogArg(value) {
100255
101114
  return `[Log serialization failed: ${formatLogError(error)}]`;
100256
101115
  }
100257
101116
  }
100258
- function makeSandboxConsole(collectedLogs) {
101117
+ function makeSandboxConsole(collectedLogs, mirror) {
100259
101118
  const capture = (level) => (...args) => {
101119
+ const formattedArgs = args.map(formatLogArg);
100260
101120
  collectedLogs.push({
100261
101121
  level,
100262
- args: args.map(formatLogArg),
101122
+ args: formattedArgs,
100263
101123
  timestamp: Date.now()
100264
101124
  });
101125
+ mirror == null ? void 0 : mirror(level, formattedArgs);
100265
101126
  };
100266
101127
  return { log: capture("log"), warn: capture("warn"), error: capture("error"), info: capture("info") };
100267
101128
  }
@@ -100423,6 +101284,18 @@ function buildFileIndex(allFiles) {
100423
101284
  }
100424
101285
  return fileIndex;
100425
101286
  }
101287
+ function hasPathExtension(path2) {
101288
+ const fileName = path2.split("/").pop() ?? path2;
101289
+ return /\.[^/.]+$/.test(fileName);
101290
+ }
101291
+ function explicitExtensionHint(requestedName, resolvedPath, fileIndex) {
101292
+ if (!resolvedPath || hasPathExtension(resolvedPath)) return "";
101293
+ const requestedBase = requestedName.trim();
101294
+ const suggestions = [".forge.js", ".js"].map((ext) => ({ ext, resolved: `${resolvedPath}${ext}` })).filter(({ resolved }) => fileIndex.has(resolved)).map(({ ext }) => `"${requestedBase}${ext}"`);
101295
+ if (suggestions.length === 0) return "";
101296
+ const joined = suggestions.length === 1 ? suggestions[0] : suggestions.join(" or ");
101297
+ return ` Did you mean ${joined}? ForgeCAD requires explicit file extensions in project imports.`;
101298
+ }
100426
101299
  function resolveImportSource(fromFile, requestedName, allFiles, options) {
100427
101300
  if (typeof requestedName !== "string" || requestedName.trim().length === 0) {
100428
101301
  throw new Error("Import path must be a non-empty string");
@@ -100431,7 +101304,8 @@ function resolveImportSource(fromFile, requestedName, allFiles, options) {
100431
101304
  const lookupKey = options.fileIndex.get(resolvedPath);
100432
101305
  if (!lookupKey) {
100433
101306
  const suffix = resolvedPath && resolvedPath !== requestedName ? ` (resolved to "${resolvedPath}" from "${fromFile}")` : ` (from "${fromFile}")`;
100434
- throw new Error(`File not found: "${requestedName}"${suffix}`);
101307
+ const hint = explicitExtensionHint(requestedName, resolvedPath, options.fileIndex);
101308
+ throw new Error(`File not found: "${requestedName}"${suffix}.${hint}`);
100435
101309
  }
100436
101310
  const source = allFiles[lookupKey];
100437
101311
  if (typeof source !== "string") {
@@ -154712,7 +155586,7 @@ ${lanes.join("\n")}
154712
155586
  }
154713
155587
  const normalizedSourcePaths = getPathsRelativeToRootDirs(sourceDirectory, rootDirs, getCanonicalFileName);
154714
155588
  const relativePaths = flatMap(normalizedSourcePaths, (sourcePath) => {
154715
- return map(normalizedTargetPaths, (targetPath) => ensurePathIsNonModuleName(getRelativePathFromDirectory(sourcePath, targetPath, getCanonicalFileName)));
155589
+ return map(normalizedTargetPaths, (targetPath2) => ensurePathIsNonModuleName(getRelativePathFromDirectory(sourcePath, targetPath2, getCanonicalFileName)));
154716
155590
  });
154717
155591
  const shortest = min2(relativePaths, compareNumberOfDirectorySeparators);
154718
155592
  if (!shortest) {
@@ -311282,6 +312156,36 @@ function extractUnusedTopLevelVarNames(code) {
311282
312156
  }
311283
312157
  return declaredNames.filter((n) => topLevelNames.has(n) && (!usedByOthers.has(n) || explicitImplicitResultNames.has(n)));
311284
312158
  }
312159
+ function collectBindingNameLocations(node, sourceFile, names) {
312160
+ if (typescriptExports.isIdentifier(node)) {
312161
+ const { line: line2, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
312162
+ names.push({ name: node.text, line: line2 + 1, column: character + 1 });
312163
+ return;
312164
+ }
312165
+ for (const element of node.elements) {
312166
+ if (typescriptExports.isBindingElement(element)) {
312167
+ collectBindingNameLocations(element.name, sourceFile, names);
312168
+ }
312169
+ }
312170
+ }
312171
+ function findTopLevelRuntimeGlobalCollision(code, runtimeGlobalNames) {
312172
+ const runtimeGlobals = new Set(runtimeGlobalNames);
312173
+ const sourceFile = typescriptExports.createSourceFile("__runtime-globals.js", code, typescriptExports.ScriptTarget.ES2020, false, typescriptExports.ScriptKind.JS);
312174
+ const declarations = [];
312175
+ for (const statement of sourceFile.statements) {
312176
+ if (typescriptExports.isVariableStatement(statement)) {
312177
+ const isLexical = (statement.declarationList.flags & (typescriptExports.NodeFlags.Let | typescriptExports.NodeFlags.Const)) !== 0;
312178
+ if (!isLexical) continue;
312179
+ for (const decl of statement.declarationList.declarations) {
312180
+ collectBindingNameLocations(decl.name, sourceFile, declarations);
312181
+ }
312182
+ } else if (typescriptExports.isClassDeclaration(statement) && statement.name) {
312183
+ const { line: line2, character } = sourceFile.getLineAndCharacterOfPosition(statement.name.getStart(sourceFile));
312184
+ declarations.push({ name: statement.name.text, line: line2 + 1, column: character + 1 });
312185
+ }
312186
+ }
312187
+ return declarations.find((declaration) => runtimeGlobals.has(declaration.name)) ?? null;
312188
+ }
311285
312189
  function createForgeRuntimeModule(bindings) {
311286
312190
  const runtime = { ...bindings };
311287
312191
  Object.defineProperty(runtime, "__esModule", { value: true });
@@ -312437,6 +313341,12 @@ function withConstructorChainLockdown(fn) {
312437
313341
  }
312438
313342
  }
312439
313343
  function executeFile(code, fileName, allFiles, visited, scope = {}, options, executionMode = "script", moduleCacheEntry) {
313344
+ var _a3, _b3, _c2, _d2, _e2, _f;
313345
+ (_a3 = options.debug) == null ? void 0 : _a3.call(options, "executeFile:start", {
313346
+ fileName,
313347
+ executionMode,
313348
+ scope: scope.namePrefix ?? fileName
313349
+ });
312440
313350
  const trackCircularImports = executionMode === "script";
312441
313351
  if (trackCircularImports) {
312442
313352
  if (visited.has(fileName)) {
@@ -312514,13 +313424,13 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
312514
313424
  });
312515
313425
  };
312516
313426
  const importStep = (name) => {
312517
- var _a3;
313427
+ var _a4;
312518
313428
  if (typeof name !== "string" || name.trim().length === 0) {
312519
313429
  throw new Error("importStep() requires a non-empty file path string");
312520
313430
  }
312521
313431
  const resolvedPath = resolveImportPath(fileName, name.trim());
312522
313432
  rejectPathTraversal("importStep", name, resolvedPath);
312523
- const ext = ((_a3 = resolvedPath.split(".").pop()) == null ? void 0 : _a3.toLowerCase()) ?? "";
313433
+ const ext = ((_a4 = resolvedPath.split(".").pop()) == null ? void 0 : _a4.toLowerCase()) ?? "";
312524
313434
  if (ext !== "step" && ext !== "stp") {
312525
313435
  throw new Error(`importStep("${name}"): unsupported extension ".${ext}". Expected .step or .stp`);
312526
313436
  }
@@ -312549,7 +313459,39 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
312549
313459
  setShapeTopology(shape, topo);
312550
313460
  return shape;
312551
313461
  };
312552
- const sandboxConsole = makeSandboxConsole(_collectedLogs);
313462
+ const sandboxConsole = makeSandboxConsole(_collectedLogs, options.debug ? (level, args) => {
313463
+ var _a4;
313464
+ return (_a4 = options.debug) == null ? void 0 : _a4.call(options, "console", { level, args });
313465
+ } : void 0);
313466
+ const runtimeVerify = options.debug ? {
313467
+ ...verify,
313468
+ that(label, check2, message) {
313469
+ var _a4, _b4;
313470
+ (_a4 = options.debug) == null ? void 0 : _a4.call(options, "verify:that:start", { label });
313471
+ const verifyStart = performance.now();
313472
+ try {
313473
+ return verify.that(label, check2, message);
313474
+ } finally {
313475
+ (_b4 = options.debug) == null ? void 0 : _b4.call(options, "verify:that:end", {
313476
+ label,
313477
+ ms: Number((performance.now() - verifyStart).toFixed(1))
313478
+ });
313479
+ }
313480
+ },
313481
+ equal(label, actual, expected, tolerance = 0, message) {
313482
+ var _a4, _b4;
313483
+ (_a4 = options.debug) == null ? void 0 : _a4.call(options, "verify:equal:start", { label, actual, expected, tolerance });
313484
+ const verifyStart = performance.now();
313485
+ try {
313486
+ return verify.equal(label, actual, expected, tolerance, message);
313487
+ } finally {
313488
+ (_b4 = options.debug) == null ? void 0 : _b4.call(options, "verify:equal:end", {
313489
+ label,
313490
+ ms: Number((performance.now() - verifyStart).toFixed(1))
313491
+ });
313492
+ }
313493
+ }
313494
+ } : verify;
312553
313495
  setShowLabelsHighlight(highlight);
312554
313496
  const runtimeBindings = {
312555
313497
  box: trackedBox,
@@ -312690,7 +313632,8 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
312690
313632
  jointsView,
312691
313633
  viewConfig,
312692
313634
  scene,
312693
- verify,
313635
+ Viewport,
313636
+ verify: runtimeVerify,
312694
313637
  spec,
312695
313638
  mock,
312696
313639
  gcode,
@@ -312828,8 +313771,21 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
312828
313771
  throw error;
312829
313772
  }
312830
313773
  };
312831
- const compiled = compileScript(code, fileName, options);
312832
313774
  const bindingNames = Object.keys(runtimeBindings);
313775
+ const collision = findTopLevelRuntimeGlobalCollision(code, bindingNames);
313776
+ if (collision) {
313777
+ throw new Error(
313778
+ `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}").`
313779
+ );
313780
+ }
313781
+ (_b3 = options.debug) == null ? void 0 : _b3.call(options, "executeFile:compile:start", { fileName, executionMode });
313782
+ const compileStart = performance.now();
313783
+ const compiled = compileScript(code, fileName, options);
313784
+ (_c2 = options.debug) == null ? void 0 : _c2.call(options, "executeFile:compile:end", {
313785
+ fileName,
313786
+ executionMode,
313787
+ ms: Number((performance.now() - compileStart).toFixed(1))
313788
+ });
312833
313789
  const bindingValues = bindingNames.map((name) => runtimeBindings[name]);
312834
313790
  let scriptCode = compiled.code;
312835
313791
  if (executionMode === "script") {
@@ -312855,12 +313811,20 @@ ${scriptCode}
312855
313811
  exports: executionMode === "module" && moduleCacheEntry ? moduleCacheEntry.exports : {}
312856
313812
  };
312857
313813
  const initialExportsRef = moduleValue.exports;
313814
+ (_d2 = options.debug) == null ? void 0 : _d2.call(options, "executeFile:invoke:start", { fileName, executionMode });
313815
+ const invokeStart = performance.now();
312858
313816
  const returnValue = withConstructorChainLockdown(
312859
313817
  () => runWithParamScope(
312860
313818
  scope,
312861
313819
  () => fn(moduleValue.exports, moduleValue, requireModule, fileName, dirnamePath(fileName), ...bindingValues)
312862
313820
  )
312863
313821
  );
313822
+ (_e2 = options.debug) == null ? void 0 : _e2.call(options, "executeFile:invoke:end", {
313823
+ fileName,
313824
+ executionMode,
313825
+ ms: Number((performance.now() - invokeStart).toFixed(1)),
313826
+ returned: returnValue === void 0 ? "undefined" : returnValue === null ? "null" : typeof returnValue
313827
+ });
312864
313828
  if (executionMode === "module") {
312865
313829
  const hasExports = hasExplicitModuleExports(moduleValue.exports, initialExportsRef);
312866
313830
  if (returnValue !== void 0 && hasExports) {
@@ -312899,12 +313863,14 @@ ${scriptCode}
312899
313863
  }
312900
313864
  return returnValue;
312901
313865
  } finally {
313866
+ (_f = options.debug) == null ? void 0 : _f.call(options, "executeFile:end", { fileName, executionMode });
312902
313867
  if (trackCircularImports) {
312903
313868
  visited.delete(fileName);
312904
313869
  }
312905
313870
  }
312906
313871
  }
312907
313872
  function runScript(code, fileName = "main.forge.js", allFiles = {}, options = {}) {
313873
+ var _a3, _b3;
312908
313874
  _collectedLogs = [];
312909
313875
  resetExecutionSession(_collectedLogs);
312910
313876
  const t0 = performance.now();
@@ -312913,14 +313879,25 @@ function runScript(code, fileName = "main.forge.js", allFiles = {}, options = {}
312913
313879
  fileIndex: buildFileIndex(allFiles),
312914
313880
  compiledFiles: persistentCompiledFiles,
312915
313881
  moduleCache: /* @__PURE__ */ new Map(),
312916
- readBinaryFile: options.readBinaryFile
313882
+ readBinaryFile: options.readBinaryFile,
313883
+ debug: options.debug
312917
313884
  };
312918
313885
  const quality = resolveForgeQualityPreset(options.quality);
313886
+ (_a3 = options.debug) == null ? void 0 : _a3.call(options, "runScript:start", { fileName, quality, fileCount: Object.keys(allFiles).length });
312919
313887
  try {
312920
313888
  return runWithForgeQuality(quality, () => {
313889
+ var _a4, _b4, _c2, _d2, _e2, _f, _g, _h, _i, _j;
313890
+ (_a4 = options.debug) == null ? void 0 : _a4.call(options, "runScript:execute:start", { fileName });
313891
+ const executeStart = performance.now();
312921
313892
  const result = executeFile(code, fileName, allFiles, /* @__PURE__ */ new Set(), {}, execOptions);
313893
+ (_b4 = options.debug) == null ? void 0 : _b4.call(options, "runScript:execute:end", {
313894
+ fileName,
313895
+ ms: Number((performance.now() - executeStart).toFixed(1)),
313896
+ resultType: result === void 0 ? "undefined" : result === null ? "null" : typeof result
313897
+ });
312922
313898
  const highlights = getCollectedHighlights();
312923
313899
  const mocks = getCollectedMocks();
313900
+ (_c2 = options.debug) == null ? void 0 : _c2.call(options, "runScript:map:start", { highlights: highlights.length, mocks: mocks.length });
312924
313901
  const mapped = mapScriptResultToScene({
312925
313902
  result,
312926
313903
  fileName,
@@ -312929,24 +313906,43 @@ function runScript(code, fileName = "main.forge.js", allFiles = {}, options = {}
312929
313906
  mocks,
312930
313907
  logs: _collectedLogs
312931
313908
  });
313909
+ (_d2 = options.debug) == null ? void 0 : _d2.call(options, "runScript:map:end", {
313910
+ objects: mapped.objects.length,
313911
+ hasShape: Boolean(mapped.shape),
313912
+ hasSketch: Boolean(mapped.sketch),
313913
+ hasError: Boolean(mapped.error)
313914
+ });
313915
+ (_e2 = options.debug) == null ? void 0 : _e2.call(options, "runScript:explodeHints:start", { objects: mapped.objects.length });
312932
313916
  autoFillExplodeHints(mapped.objects);
313917
+ (_f = options.debug) == null ? void 0 : _f.call(options, "runScript:explodeHints:end");
313918
+ (_g = options.debug) == null ? void 0 : _g.call(options, "runScript:snapshot:start", { objects: mapped.objects.length });
313919
+ const snapshot = collectSuccessfulExecutionSnapshot({
313920
+ quality,
313921
+ objects: mapped.objects,
313922
+ logs: _collectedLogs,
313923
+ extraDimensions: mapped.extraDimensions,
313924
+ highlights,
313925
+ mocks
313926
+ });
313927
+ (_h = options.debug) == null ? void 0 : _h.call(options, "runScript:snapshot:end", {
313928
+ params: snapshot.params.length,
313929
+ cutPlanes: snapshot.cutPlanes.length,
313930
+ verifications: snapshot.verifications.length
313931
+ });
313932
+ (_i = options.debug) == null ? void 0 : _i.call(options, "runScript:sceneTargets:start");
313933
+ snapshot.sceneConfig = resolveSceneJourneyTargets(snapshot.sceneConfig, mapped.objects);
313934
+ (_j = options.debug) == null ? void 0 : _j.call(options, "runScript:sceneTargets:end");
312933
313935
  return {
312934
313936
  shape: mapped.shape,
312935
313937
  sketch: mapped.sketch,
312936
313938
  objects: mapped.objects,
312937
- ...collectSuccessfulExecutionSnapshot({
312938
- quality,
312939
- objects: mapped.objects,
312940
- logs: _collectedLogs,
312941
- extraDimensions: mapped.extraDimensions,
312942
- highlights,
312943
- mocks
312944
- }),
313939
+ ...snapshot,
312945
313940
  error: mapped.error,
312946
313941
  timeMs: performance.now() - t0
312947
313942
  };
312948
313943
  });
312949
313944
  } catch (e) {
313945
+ (_b3 = options.debug) == null ? void 0 : _b3.call(options, "runScript:error", { error: (e == null ? void 0 : e.message) || String(e) });
312950
313946
  const msg = e.message || String(e);
312951
313947
  const stack = e.stack || "";
312952
313948
  let lineInfo = "";