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
@@ -6736,7 +6736,7 @@ function add$3(a, b) {
6736
6736
  function scale$4(v, s) {
6737
6737
  return [v[0] * s, v[1] * s, v[2] * s];
6738
6738
  }
6739
- function sub$4(a, b) {
6739
+ function sub$5(a, b) {
6740
6740
  return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
6741
6741
  }
6742
6742
  function cross$4(a, b) {
@@ -6775,8 +6775,8 @@ function buildSurfaceSheetTopology(boundaries, options = {}) {
6775
6775
  const [v1Start, v1End] = boundaries.v1;
6776
6776
  const corners = [u0Start, u0End, u1Start, u1End];
6777
6777
  const center = options.center ?? average$1(corners);
6778
- const uAxis = normalizeAxis$1(sub$4(midpoint$3(u1Start, u1End), midpoint$3(u0Start, u0End)));
6779
- const vAxis = normalizeAxis$1(sub$4(midpoint$3(v1Start, v1End), midpoint$3(v0Start, v0End)));
6778
+ const uAxis = normalizeAxis$1(sub$5(midpoint$3(u1Start, u1End), midpoint$3(u0Start, u0End)));
6779
+ const vAxis = normalizeAxis$1(sub$5(midpoint$3(v1Start, v1End), midpoint$3(v0Start, v0End)));
6780
6780
  const normal = normalizeAxis$1(options.normal ?? cross$4(uAxis, vAxis));
6781
6781
  const faces = /* @__PURE__ */ new Map();
6782
6782
  faces.set(faceName, {
@@ -6808,7 +6808,7 @@ function attachSurfaceSheetTopology(shape, boundaries, options = {}) {
6808
6808
  });
6809
6809
  return shape;
6810
6810
  }
6811
- function requireFinite$6(v, label) {
6811
+ function requireFinite$7(v, label) {
6812
6812
  if (!Number.isFinite(v)) throw new Error(`nurbsSurface: ${label} must be finite, got ${v}`);
6813
6813
  }
6814
6814
  class NurbsSurface {
@@ -6834,16 +6834,16 @@ class NurbsSurface {
6834
6834
  for (let i = 0; i < nU; i++) {
6835
6835
  if (controlGrid[i].length !== nV) throw new Error(`nurbsSurface: row ${i} has ${controlGrid[i].length} points, expected ${nV}`);
6836
6836
  for (let j = 0; j < nV; j++) {
6837
- requireFinite$6(controlGrid[i][j][0], `controlGrid[${i}][${j}][0]`);
6838
- requireFinite$6(controlGrid[i][j][1], `controlGrid[${i}][${j}][1]`);
6839
- requireFinite$6(controlGrid[i][j][2], `controlGrid[${i}][${j}][2]`);
6837
+ requireFinite$7(controlGrid[i][j][0], `controlGrid[${i}][${j}][0]`);
6838
+ requireFinite$7(controlGrid[i][j][1], `controlGrid[${i}][${j}][1]`);
6839
+ requireFinite$7(controlGrid[i][j][2], `controlGrid[${i}][${j}][2]`);
6840
6840
  }
6841
6841
  }
6842
6842
  const weightsGrid = options.weights ?? controlGrid.map((row) => row.map(() => 1));
6843
6843
  for (let i = 0; i < nU; i++) {
6844
6844
  if (weightsGrid[i].length !== nV) throw new Error(`nurbsSurface: weights row ${i} length mismatch`);
6845
6845
  for (let j = 0; j < nV; j++) {
6846
- requireFinite$6(weightsGrid[i][j], `weights[${i}][${j}]`);
6846
+ requireFinite$7(weightsGrid[i][j], `weights[${i}][${j}]`);
6847
6847
  if (weightsGrid[i][j] <= 0) throw new Error(`nurbsSurface: weights[${i}][${j}] must be > 0`);
6848
6848
  }
6849
6849
  }
@@ -8250,7 +8250,7 @@ let _wasm = null;
8250
8250
  async function initManifoldWasm() {
8251
8251
  if (_wasm) return _wasm;
8252
8252
  performance.mark("manifold:start");
8253
- const Module = (await import("./manifold-jlYQ6E5R.js")).default;
8253
+ const Module = (await import("./manifold-CU0G1yYL.js")).default;
8254
8254
  performance.mark("manifold:imported");
8255
8255
  const wasm = await Module();
8256
8256
  wasm.setup();
@@ -8471,7 +8471,7 @@ function sweepStitched(profilePolygons, pathPoints, up, wasm) {
8471
8471
  function computeParallelTransportFrames(path2, preferredUp) {
8472
8472
  const n = path2.length;
8473
8473
  const frames = [];
8474
- const firstTangent = normalize$6(sub$3(path2[1], path2[0]));
8474
+ const firstTangent = normalize$6(sub$4(path2[1], path2[0]));
8475
8475
  if (!firstTangent) return null;
8476
8476
  let x = normalize$6(cross$3(preferredUp, firstTangent));
8477
8477
  if (!x || length(x) < 1e-8) {
@@ -8485,18 +8485,18 @@ function computeParallelTransportFrames(path2, preferredUp) {
8485
8485
  const prevT = frames[i - 1].t;
8486
8486
  let nextT;
8487
8487
  if (i < n - 1) {
8488
- const t1 = normalize$6(sub$3(path2[i], path2[i - 1]));
8489
- const t2 = normalize$6(sub$3(path2[i + 1], path2[i]));
8488
+ const t1 = normalize$6(sub$4(path2[i], path2[i - 1]));
8489
+ const t2 = normalize$6(sub$4(path2[i + 1], path2[i]));
8490
8490
  if (!t1 || !t2) return null;
8491
8491
  nextT = normalize$6(add$2(t1, t2)) || t1;
8492
8492
  } else {
8493
- const nt = normalize$6(sub$3(path2[i], path2[i - 1]));
8493
+ const nt = normalize$6(sub$4(path2[i], path2[i - 1]));
8494
8494
  if (!nt) return null;
8495
8495
  nextT = nt;
8496
8496
  }
8497
8497
  const v = cross$3(prevT, nextT);
8498
8498
  const vLen = length(v);
8499
- const c = dot$4(prevT, nextT);
8499
+ const c = dot$5(prevT, nextT);
8500
8500
  if (vLen > 1e-10) {
8501
8501
  const axis = scale$3(v, 1 / vLen);
8502
8502
  x = rotateVector(frames[i - 1].x, axis, c, vLen);
@@ -8570,7 +8570,7 @@ function signedArea$4(loop) {
8570
8570
  }
8571
8571
  return area * 0.5;
8572
8572
  }
8573
- function sub$3(a, b) {
8573
+ function sub$4(a, b) {
8574
8574
  return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
8575
8575
  }
8576
8576
  function add$2(a, b) {
@@ -8579,7 +8579,7 @@ function add$2(a, b) {
8579
8579
  function scale$3(v, s) {
8580
8580
  return [v[0] * s, v[1] * s, v[2] * s];
8581
8581
  }
8582
- function dot$4(a, b) {
8582
+ function dot$5(a, b) {
8583
8583
  return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
8584
8584
  }
8585
8585
  function cross$3(a, b) {
@@ -8598,7 +8598,7 @@ function normalize$6(v) {
8598
8598
  return [v[0] / len2, v[1] / len2, v[2] / len2];
8599
8599
  }
8600
8600
  function rotateVector(v, axis, c, s) {
8601
- const kDotV = dot$4(axis, v);
8601
+ const kDotV = dot$5(axis, v);
8602
8602
  const kCrossV = cross$3(axis, v);
8603
8603
  return [
8604
8604
  v[0] * c + kCrossV[0] * s + axis[0] * kDotV * (1 - c),
@@ -12296,7 +12296,7 @@ function normalizeFaceSelector(selector) {
12296
12296
  function cross$2(a, b) {
12297
12297
  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]];
12298
12298
  }
12299
- function dot$3(a, b) {
12299
+ function dot$4(a, b) {
12300
12300
  return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
12301
12301
  }
12302
12302
  function normVec3(v) {
@@ -12331,11 +12331,11 @@ function clusterMeshFaces(shape) {
12331
12331
  if (!normal) continue;
12332
12332
  const crossLen = Math.sqrt(rawCross[0] * rawCross[0] + rawCross[1] * rawCross[1] + rawCross[2] * rawCross[2]);
12333
12333
  const triArea = crossLen / 2;
12334
- const planeOffset = dot$3(normal, v0);
12334
+ const planeOffset = dot$4(normal, v0);
12335
12335
  const triCentroid = [(v0[0] + v1[0] + v2[0]) / 3, (v0[1] + v1[1] + v2[1]) / 3, (v0[2] + v1[2] + v2[2]) / 3];
12336
12336
  let merged = false;
12337
12337
  for (const c of clusters) {
12338
- if (dot$3(c.normal, normal) > NORMAL_COS_EPS$1 && Math.abs(c.planeOffset - planeOffset) < PLANE_OFFSET_EPS$1) {
12338
+ if (dot$4(c.normal, normal) > NORMAL_COS_EPS$1 && Math.abs(c.planeOffset - planeOffset) < PLANE_OFFSET_EPS$1) {
12339
12339
  c.centroidSum[0] += triCentroid[0];
12340
12340
  c.centroidSum[1] += triCentroid[1];
12341
12341
  c.centroidSum[2] += triCentroid[2];
@@ -12377,7 +12377,7 @@ function queryMeshFaces(shape, query) {
12377
12377
  let clusters = clusterMeshFaces(shape);
12378
12378
  if (query.normal) {
12379
12379
  const qn = query.normal;
12380
- clusters = clusters.filter((c) => dot$3(c.normal, qn) > NORMAL_COS_EPS$1);
12380
+ clusters = clusters.filter((c) => dot$4(c.normal, qn) > NORMAL_COS_EPS$1);
12381
12381
  }
12382
12382
  if (query.planar !== false) {
12383
12383
  clusters = clusters.filter((c) => c.normal !== null);
@@ -12393,7 +12393,7 @@ function queryMeshFace(shape, query) {
12393
12393
  let clusters = clusterMeshFaces(shape);
12394
12394
  if (query.normal) {
12395
12395
  const qn = query.normal;
12396
- clusters = clusters.filter((c) => dot$3(c.normal, qn) > NORMAL_COS_EPS$1);
12396
+ clusters = clusters.filter((c) => dot$4(c.normal, qn) > NORMAL_COS_EPS$1);
12397
12397
  }
12398
12398
  if (query.planar !== false) {
12399
12399
  clusters = clusters.filter((c) => c.normal !== null);
@@ -19341,6 +19341,7 @@ Fix: pass a path like [[0,0,0], [20,0,8,4], [40,0,0,2]].`
19341
19341
  const curveMode = shouldSmoothCurve(options, defaultCurve) && sourcePoints.length >= 3;
19342
19342
  const normalized = compactPathPoints(resampleCurve(sourcePoints, options, defaultCurve));
19343
19343
  const minRadius = Math.min(...normalized.map((point2) => point2.radius));
19344
+ const hasExplicitBlend = options.blend !== void 0;
19344
19345
  const blendRadius = positiveFinite(options.blend, "Sculpt.tube() blend", Math.max(0.5, minRadius));
19345
19346
  let segmentCount = 0;
19346
19347
  for (let i = 0; i < normalized.length - 1; i += 1) {
@@ -19349,8 +19350,8 @@ Fix: pass a path like [[0,0,0], [20,0,8,4], [40,0,0,2]].`
19349
19350
  if (segmentCount === 0) {
19350
19351
  throw new Error("Sculpt.tube() points must include at least one non-zero-length segment.");
19351
19352
  }
19352
- const curveBlendRadius = curveMode ? Math.max(blendRadius, minRadius * 1.5) : Math.min(blendRadius, Math.max(0.1, minRadius * 0.8));
19353
- let out = polylineSweep(normalized, curveMode ? curveBlendRadius : blendRadius);
19353
+ const effectiveBlendRadius = curveMode && !hasExplicitBlend ? Math.max(blendRadius, minRadius * 1.5) : blendRadius;
19354
+ let out = polylineSweep(normalized, effectiveBlendRadius);
19354
19355
  if (options.polish !== void 0) out = out.polish(options.polish);
19355
19356
  return out;
19356
19357
  }
@@ -21738,7 +21739,7 @@ function intersection(...inputs) {
21738
21739
  nextPlan
21739
21740
  );
21740
21741
  }
21741
- var define_process_env_default = {};
21742
+ var define_process_env_default$2 = {};
21742
21743
  let _wasm_solve = null;
21743
21744
  let _wasm_get_profile = null;
21744
21745
  let _solverMemory = null;
@@ -21773,7 +21774,7 @@ function readInitialConsoleDebug() {
21773
21774
  return false;
21774
21775
  }
21775
21776
  function readEnvFlag(name) {
21776
- const value = typeof process !== "undefined" ? define_process_env_default == null ? void 0 : define_process_env_default[name] : void 0;
21777
+ const value = typeof process !== "undefined" ? define_process_env_default$2 == null ? void 0 : define_process_env_default$2[name] : void 0;
21777
21778
  if (typeof value !== "string") return false;
21778
21779
  return ["1", "true", "yes", "on"].includes(value.toLowerCase());
21779
21780
  }
@@ -23038,7 +23039,7 @@ class MateBuilder {
23038
23039
  return this.constraints.reduce((sum, c) => sum + (CONSTRAINT_EQUATIONS[c.type] ?? 0), 0);
23039
23040
  }
23040
23041
  }
23041
- let _collected$7 = null;
23042
+ let _collected$8 = null;
23042
23043
  const isAxis = (value) => value === "x" || value === "y" || value === "z";
23043
23044
  const normalizeDirection = (value, label) => {
23044
23045
  if (value === "radial" || isAxis(value)) return value;
@@ -23097,16 +23098,16 @@ const mergeDirective = (target, patch, label) => {
23097
23098
  return out;
23098
23099
  };
23099
23100
  function resetExplodeView() {
23100
- _collected$7 = null;
23101
+ _collected$8 = null;
23101
23102
  }
23102
23103
  function getCollectedExplodeView() {
23103
- return _collected$7 ? cloneOptions(_collected$7) : null;
23104
+ return _collected$8 ? cloneOptions(_collected$8) : null;
23104
23105
  }
23105
23106
  function explodeView(options = {}) {
23106
23107
  if (!options || typeof options !== "object") {
23107
23108
  throw new Error("explodeView(options) expects an options object");
23108
23109
  }
23109
- const next = _collected$7 ? cloneOptions(_collected$7) : {};
23110
+ const next = _collected$8 ? cloneOptions(_collected$8) : {};
23110
23111
  if (options.enabled !== void 0) {
23111
23112
  if (typeof options.enabled !== "boolean") throw new Error("explodeView.enabled must be a boolean");
23112
23113
  next.enabled = options.enabled;
@@ -23154,9 +23155,9 @@ function explodeView(options = {}) {
23154
23155
  });
23155
23156
  next.byPath = byPath;
23156
23157
  }
23157
- _collected$7 = next;
23158
+ _collected$8 = next;
23158
23159
  }
23159
- let _collected$6 = null;
23160
+ let _collected$7 = null;
23160
23161
  const isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
23161
23162
  const isVec3$1 = (value) => Array.isArray(value) && value.length === 3 && isFiniteNumber(value[0]) && isFiniteNumber(value[1]) && isFiniteNumber(value[2]);
23162
23163
  const normalizeAxis = (axis) => {
@@ -23446,22 +23447,22 @@ const cloneCollected = (value) => ({
23446
23447
  defaultAnimation: value.defaultAnimation
23447
23448
  });
23448
23449
  function resetJointsView() {
23449
- _collected$6 = null;
23450
+ _collected$7 = null;
23450
23451
  }
23451
23452
  function getCollectedJointsView() {
23452
- return _collected$6 ? cloneCollected(_collected$6) : null;
23453
+ return _collected$7 ? cloneCollected(_collected$7) : null;
23453
23454
  }
23454
23455
  function saveJointsView() {
23455
- return _collected$6 ? cloneCollected(_collected$6) : null;
23456
+ return _collected$7 ? cloneCollected(_collected$7) : null;
23456
23457
  }
23457
23458
  function restoreJointsView(state) {
23458
- _collected$6 = state;
23459
+ _collected$7 = state;
23459
23460
  }
23460
23461
  function jointsView(options = {}) {
23461
23462
  if (!options || typeof options !== "object") {
23462
23463
  throw new Error("jointsView(options) expects an options object");
23463
23464
  }
23464
- const next = _collected$6 ? cloneCollected(_collected$6) : { joints: [], couplings: [], animations: [] };
23465
+ const next = _collected$7 ? cloneCollected(_collected$7) : { joints: [], couplings: [], animations: [] };
23465
23466
  if (options.enabled !== void 0) {
23466
23467
  if (typeof options.enabled !== "boolean") {
23467
23468
  throw new Error("jointsView.enabled must be a boolean");
@@ -23516,8 +23517,14 @@ function jointsView(options = {}) {
23516
23517
  if (next.defaultAnimation && !next.animations.some((animation) => animation.name === next.defaultAnimation)) {
23517
23518
  throw new Error(`jointsView defaultAnimation "${next.defaultAnimation}" does not exist in animations`);
23518
23519
  }
23519
- _collected$6 = next;
23520
+ _collected$7 = next;
23520
23521
  }
23522
+ var define_process_env_default$1 = {};
23523
+ const SWEEP_JOINT_DEFAULT_STEP_LIMIT = {
23524
+ live: 1,
23525
+ default: 4,
23526
+ high: Number.POSITIVE_INFINITY
23527
+ };
23521
23528
  let collectedAssemblies = [];
23522
23529
  function resetCollectedAssemblies() {
23523
23530
  collectedAssemblies = [];
@@ -23558,6 +23565,33 @@ function collisionShape(part) {
23558
23565
  if (shapes.length === 1) return shapes[0];
23559
23566
  return union(...shapes);
23560
23567
  }
23568
+ function boundsOverlap(a, b) {
23569
+ for (let axis = 0; axis < 3; axis++) {
23570
+ if (a.max[axis] <= b.min[axis] || b.max[axis] <= a.min[axis]) return false;
23571
+ }
23572
+ return true;
23573
+ }
23574
+ function readAssemblyPerfEnv(name) {
23575
+ return typeof process !== "undefined" ? define_process_env_default$1 == null ? void 0 : define_process_env_default$1[name] : void 0;
23576
+ }
23577
+ function resolveSweepJointStepLimit() {
23578
+ if (readAssemblyPerfEnv("FORGECAD_ALLOW_FULL_SWEEP_JOINT") === "1") return Number.POSITIVE_INFINITY;
23579
+ const override = readAssemblyPerfEnv("FORGECAD_SWEEP_JOINT_STEP_LIMIT");
23580
+ if (override != null && override.trim() !== "") {
23581
+ const parsed = Number(override);
23582
+ if (Number.isFinite(parsed) && parsed >= 1) return Math.floor(parsed);
23583
+ }
23584
+ return SWEEP_JOINT_DEFAULT_STEP_LIMIT[getForgeQualityPreset()];
23585
+ }
23586
+ function boundSweepJointSteps(jointName, requestedSteps) {
23587
+ const limit = resolveSweepJointStepLimit();
23588
+ if (!Number.isFinite(limit) || requestedSteps <= limit) return requestedSteps;
23589
+ const bounded = Math.max(1, Math.floor(limit));
23590
+ emitRuntimeWarning(
23591
+ `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.`
23592
+ );
23593
+ return bounded;
23594
+ }
23561
23595
  const FASTENER_PATTERN = /\b(bolt|screw|nut|washer|pin|rivet|fastener|standoff|insert)\b/i;
23562
23596
  function isFastenerName(name) {
23563
23597
  return FASTENER_PATTERN.test(name);
@@ -23890,16 +23924,23 @@ class SolvedAssembly {
23890
23924
  const minOverlap = options.minOverlapVolume ?? 0.1;
23891
23925
  const ignore = new Set((options.ignorePairs ?? []).map(([a, b]) => [a, b].sort().join("|")));
23892
23926
  const findings = [];
23893
- for (let i = 0; i < names.length; i++) {
23894
- for (let j = i + 1; j < names.length; j++) {
23895
- const aName = names[i];
23896
- const bName = names[j];
23927
+ const entries = [];
23928
+ for (const name of names) {
23929
+ const shape = collisionShape(this.getPart(name));
23930
+ if (!shape) continue;
23931
+ try {
23932
+ entries.push({ name, shape, bounds: shape.boundingBox() });
23933
+ } catch {
23934
+ }
23935
+ }
23936
+ for (let i = 0; i < entries.length; i++) {
23937
+ for (let j = i + 1; j < entries.length; j++) {
23938
+ const aName = entries[i].name;
23939
+ const bName = entries[j].name;
23897
23940
  if (ignore.has([aName, bName].sort().join("|"))) continue;
23898
- const a = collisionShape(this.getPart(aName));
23899
- const b = collisionShape(this.getPart(bName));
23900
- if (!a || !b) continue;
23941
+ if (!boundsOverlap(entries[i].bounds, entries[j].bounds)) continue;
23901
23942
  try {
23902
- const hit = a.intersect(b);
23943
+ const hit = entries[i].shape.intersect(entries[j].shape);
23903
23944
  if (hit.isEmpty()) continue;
23904
23945
  const vol = hit.volume();
23905
23946
  if (vol > minOverlap) {
@@ -24876,7 +24917,7 @@ class Assembly {
24876
24917
  if (this.jointCouplings.has(jointName)) {
24877
24918
  throw new Error(`Cannot sweep coupled joint "${jointName}". Sweep one of its source joints instead.`);
24878
24919
  }
24879
- const n = Math.max(1, Math.floor(steps));
24920
+ const n = boundSweepJointSteps(jointName, Math.max(1, Math.floor(steps)));
24880
24921
  const frames = [];
24881
24922
  for (let i = 0; i <= n; i++) {
24882
24923
  const t = n === 0 ? 0 : i / n;
@@ -25292,12 +25333,12 @@ function bom(quantity, description, opts) {
25292
25333
  metadata
25293
25334
  });
25294
25335
  }
25295
- let _collected$5 = [];
25336
+ let _collected$6 = [];
25296
25337
  function resetCutPlanes() {
25297
- _collected$5 = [];
25338
+ _collected$6 = [];
25298
25339
  }
25299
25340
  function getCollectedCutPlanes() {
25300
- return _collected$5.slice();
25341
+ return _collected$6.slice();
25301
25342
  }
25302
25343
  function normalizeExcludedObjectNames(input) {
25303
25344
  if (input === void 0) return void 0;
@@ -25313,16 +25354,16 @@ function cutPlane(name, normal, offsetOrOptions = 0, maybeOptions = {}) {
25313
25354
  const offset2 = Number.isFinite(rawOffset) ? rawOffset : 0;
25314
25355
  const options = usingOffsetArg ? maybeOptions : offsetOrOptions;
25315
25356
  const excludeObjectNames = normalizeExcludedObjectNames(options.exclude);
25316
- _collected$5.push({ name, normal, offset: offset2, excludeObjectNames });
25357
+ _collected$6.push({ name, normal, offset: offset2, excludeObjectNames });
25317
25358
  }
25318
- let _collected$4 = [];
25359
+ let _collected$5 = [];
25319
25360
  let _counter$1 = 0;
25320
25361
  function resetMocks() {
25321
- _collected$4 = [];
25362
+ _collected$5 = [];
25322
25363
  _counter$1 = 0;
25323
25364
  }
25324
25365
  function getCollectedMocks() {
25325
- return _collected$4.slice();
25366
+ return _collected$5.slice();
25326
25367
  }
25327
25368
  function mock(shape, name) {
25328
25369
  if (!shape || typeof shape !== "object") {
@@ -25330,7 +25371,7 @@ function mock(shape, name) {
25330
25371
  }
25331
25372
  _counter$1 += 1;
25332
25373
  const displayName = name && typeof name === "string" && name.trim().length > 0 ? name.trim() : `Mock ${_counter$1}`;
25333
- _collected$4.push({
25374
+ _collected$5.push({
25334
25375
  id: `mock-${_counter$1}`,
25335
25376
  name: displayName,
25336
25377
  shape
@@ -29471,7 +29512,7 @@ function shapeToBounds(shape) {
29471
29512
  max: [bb.max[0], bb.max[1], bb.max[2]]
29472
29513
  };
29473
29514
  }
29474
- function sub$2(a, b) {
29515
+ function sub$3(a, b) {
29475
29516
  return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
29476
29517
  }
29477
29518
  function addVec(a, b) {
@@ -29480,7 +29521,7 @@ function addVec(a, b) {
29480
29521
  function scale$2(v, s) {
29481
29522
  return [v[0] * s, v[1] * s, v[2] * s];
29482
29523
  }
29483
- function dot$2(a, b) {
29524
+ function dot$3(a, b) {
29484
29525
  return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
29485
29526
  }
29486
29527
  function cross$1(a, b) {
@@ -29504,9 +29545,9 @@ function pipeRoute(points, radius, options) {
29504
29545
  const bends = new Array(points.length).fill(null);
29505
29546
  for (let i = 1; i < points.length - 1; i++) {
29506
29547
  const prev = points[i - 1], cur = points[i], next = points[i + 1];
29507
- const dIn = normalize$3(sub$2(cur, prev));
29508
- const dOut = normalize$3(sub$2(next, cur));
29509
- const dotVal = clampDot(dot$2(dIn, dOut));
29548
+ const dIn = normalize$3(sub$3(cur, prev));
29549
+ const dOut = normalize$3(sub$3(next, cur));
29550
+ const dotVal = clampDot(dot$3(dIn, dOut));
29510
29551
  const bendAngle = Math.acos(dotVal);
29511
29552
  if (bendAngle < 1e-6) {
29512
29553
  continue;
@@ -29525,7 +29566,7 @@ function pipeRoute(points, radius, options) {
29525
29566
  }
29526
29567
  const parts = [];
29527
29568
  const makeSeg = (a, b) => {
29528
- const d = sub$2(b, a);
29569
+ const d = sub$3(b, a);
29529
29570
  const len2 = vecLen(d);
29530
29571
  if (len2 < 0.01) return null;
29531
29572
  const dir = normalize$3(d);
@@ -29570,7 +29611,7 @@ function pipeRoute(points, radius, options) {
29570
29611
  const innerBend = buildShapeFromCompilePlan(innerPlan);
29571
29612
  bendShape = bendShape.subtract(innerBend);
29572
29613
  }
29573
- const radialDir = normalize$3(sub$2(info.startPt, info.center));
29614
+ const radialDir = normalize$3(sub$3(info.startPt, info.center));
29574
29615
  const tangentDir = cross$1(info.axis, radialDir);
29575
29616
  const c = info.center;
29576
29617
  bendShape = bendShape.transform([
@@ -29631,7 +29672,7 @@ function elbow(pipeRadius, bendRadius, angle, options) {
29631
29672
  if (fromDir && toDir) {
29632
29673
  const nFrom = normalize$3(fromDir);
29633
29674
  const nTo = normalize$3(toDir);
29634
- const d = clampDot(dot$2(nFrom, nTo));
29675
+ const d = clampDot(dot$3(nFrom, nTo));
29635
29676
  angleDeg = Math.acos(d) * 180 / Math.PI;
29636
29677
  }
29637
29678
  if (angleDeg < 0.01) throw new Error("elbow: angle too small");
@@ -29702,20 +29743,20 @@ function assertFinitePositive(apiName, name, value) {
29702
29743
  function add$1(a, b) {
29703
29744
  return [a[0] + b[0], a[1] + b[1]];
29704
29745
  }
29705
- function sub$1(a, b) {
29746
+ function sub$2(a, b) {
29706
29747
  return [a[0] - b[0], a[1] - b[1]];
29707
29748
  }
29708
29749
  function scale$1(v, s) {
29709
29750
  return [v[0] * s, v[1] * s];
29710
29751
  }
29711
- function dot$1(a, b) {
29752
+ function dot$2(a, b) {
29712
29753
  return a[0] * b[0] + a[1] * b[1];
29713
29754
  }
29714
29755
  function len(v) {
29715
29756
  return Math.hypot(v[0], v[1]);
29716
29757
  }
29717
29758
  function dist$2(a, b) {
29718
- return len(sub$1(b, a));
29759
+ return len(sub$2(b, a));
29719
29760
  }
29720
29761
  function norm$2(v) {
29721
29762
  const l = len(v);
@@ -29740,7 +29781,7 @@ function chooseSweepDeg(center, start, end, incomingDir) {
29740
29781
  const endAngle = angleOf(center, end);
29741
29782
  const cwTangent = tangentAt(startAngle, true);
29742
29783
  const ccwTangent = tangentAt(startAngle, false);
29743
- const clockwise = dot$1(cwTangent, incomingDir) >= dot$1(ccwTangent, incomingDir);
29784
+ const clockwise = dot$2(cwTangent, incomingDir) >= dot$2(ccwTangent, incomingDir);
29744
29785
  const sweep2 = clockwise ? -normalizePositiveRadians(startAngle - endAngle) : normalizePositiveRadians(endAngle - startAngle);
29745
29786
  return sweep2 * 180 / Math.PI;
29746
29787
  }
@@ -29783,8 +29824,8 @@ function normalizePulleyAsCircle(pulley, index, radiusOverride) {
29783
29824
  );
29784
29825
  }
29785
29826
  function commonTangents(a, b, mode) {
29786
- const delta = sub$1(b.center, a.center);
29787
- const z = dot$1(delta, delta);
29827
+ const delta = sub$2(b.center, a.center);
29828
+ const z = dot$2(delta, delta);
29788
29829
  if (z < EPS$5) {
29789
29830
  throw new Error(`beltDrive: pulleys "${a.name}" and "${b.name}" have the same center.`);
29790
29831
  }
@@ -29814,8 +29855,8 @@ function commonTangents(a, b, mode) {
29814
29855
  });
29815
29856
  }
29816
29857
  function buildSegmentsForTangentOrder(a, b, t0, t1) {
29817
- const span0Dir = norm$2(sub$1(t0.b, t0.a));
29818
- const span1Dir = norm$2(sub$1(t1.a, t1.b));
29858
+ const span0Dir = norm$2(sub$2(t0.b, t0.a));
29859
+ const span1Dir = norm$2(sub$2(t1.a, t1.b));
29819
29860
  const bSweepDeg = chooseSweepDeg(b.center, t0.b, t1.b, span0Dir);
29820
29861
  const aSweepDeg = chooseSweepDeg(a.center, t1.a, t0.a, span1Dir);
29821
29862
  const span0 = {
@@ -40252,6 +40293,130 @@ function shapeToGeometryFallback(shape) {
40252
40293
  const edges = new EdgesGeometry(solid, 1);
40253
40294
  return { solid, edges, hasSmoothNormals: false };
40254
40295
  }
40296
+ let _collected$4 = [];
40297
+ let _nextId = 1;
40298
+ function resetRenderLabels() {
40299
+ _collected$4 = [];
40300
+ _nextId = 1;
40301
+ }
40302
+ function getCollectedRenderLabels() {
40303
+ return _collected$4.map((label) => ({ ...label, at: [...label.at], offset: [...label.offset] }));
40304
+ }
40305
+ function requireFinite$6(value, label) {
40306
+ if (typeof value !== "number" || !Number.isFinite(value)) {
40307
+ throw new Error(`${label} must be a finite number`);
40308
+ }
40309
+ return value;
40310
+ }
40311
+ function requireVec3$2(value, label) {
40312
+ if (!Array.isArray(value) || value.length !== 3) {
40313
+ throw new Error(`${label} must be [x, y, z]`);
40314
+ }
40315
+ return [requireFinite$6(value[0], `${label}[0]`), requireFinite$6(value[1], `${label}[1]`), requireFinite$6(value[2], `${label}[2]`)];
40316
+ }
40317
+ function optionalColor(value, label) {
40318
+ if (value === void 0) return void 0;
40319
+ if (typeof value !== "string" || value.trim().length === 0) {
40320
+ throw new Error(`${label} must be a non-empty CSS color string`);
40321
+ }
40322
+ return value.trim();
40323
+ }
40324
+ const VALID_ANCHORS = /* @__PURE__ */ new Set([
40325
+ "center",
40326
+ "top",
40327
+ "bottom",
40328
+ "left",
40329
+ "right",
40330
+ "top-left",
40331
+ "top-right",
40332
+ "bottom-left",
40333
+ "bottom-right"
40334
+ ]);
40335
+ function normalizeOptions(options) {
40336
+ if (options === void 0) {
40337
+ return { offset: [0, 0, 0], anchor: "center", alwaysOnTop: true };
40338
+ }
40339
+ if (!options || typeof options !== "object" || Array.isArray(options)) {
40340
+ throw new Error("Viewport.label options must be an object");
40341
+ }
40342
+ const out = {
40343
+ offset: [0, 0, 0],
40344
+ anchor: "center",
40345
+ alwaysOnTop: true
40346
+ };
40347
+ const color = optionalColor(options.color, "Viewport.label options.color");
40348
+ if (color !== void 0) out.color = color;
40349
+ const background = optionalColor(options.background, "Viewport.label options.background");
40350
+ if (background !== void 0) out.background = background;
40351
+ if (options.size !== void 0) {
40352
+ out.size = requireFinite$6(options.size, "Viewport.label options.size");
40353
+ if (out.size <= 0) throw new Error("Viewport.label options.size must be positive");
40354
+ }
40355
+ if (options.offset !== void 0) out.offset = requireVec3$2(options.offset, "Viewport.label options.offset");
40356
+ if (options.anchor !== void 0) {
40357
+ if (!VALID_ANCHORS.has(options.anchor)) {
40358
+ throw new Error(`Viewport.label options.anchor must be one of: ${Array.from(VALID_ANCHORS).join(", ")}`);
40359
+ }
40360
+ out.anchor = options.anchor;
40361
+ }
40362
+ if (options.alwaysOnTop !== void 0) {
40363
+ if (typeof options.alwaysOnTop !== "boolean") throw new Error("Viewport.label options.alwaysOnTop must be a boolean");
40364
+ out.alwaysOnTop = options.alwaysOnTop;
40365
+ }
40366
+ return out;
40367
+ }
40368
+ function collectRenderLabel(text, at, options) {
40369
+ if (typeof text !== "string" || text.trim().length === 0) {
40370
+ throw new Error("Viewport.label text must be a non-empty string");
40371
+ }
40372
+ const normalizedAt = requireVec3$2(at, "Viewport.label at");
40373
+ const normalizedOptions = normalizeOptions(options);
40374
+ _collected$4.push({
40375
+ id: `render-label-${_nextId++}`,
40376
+ text: text.trim(),
40377
+ at: normalizedAt,
40378
+ ...normalizedOptions
40379
+ });
40380
+ }
40381
+ const Viewport = {
40382
+ /**
40383
+ * Add a render-only viewport label at a world-space point.
40384
+ *
40385
+ * **Details**
40386
+ *
40387
+ * `Viewport.label()` is for explanatory text that helps a viewer understand
40388
+ * the model. It does not create sketches, meshes, B-rep topology, exported
40389
+ * text, or face labels, so it stays off the OCCT path. Use `text2d()` only
40390
+ * when the letters should become manufactured geometry, such as raised
40391
+ * lettering, engraved serial numbers, or exported nameplates.
40392
+ *
40393
+ * Labels are collected during script execution and rendered by the viewport
40394
+ * as lightweight overlay annotations. They are ignored by exports and do not
40395
+ * appear in `objects`.
40396
+ *
40397
+ * **Example**
40398
+ *
40399
+ * ```js
40400
+ * Viewport.label('Bearing bore', [0, 0, 18], {
40401
+ * color: '#f8fafc',
40402
+ * background: '#0f172acc',
40403
+ * offset: [0, 0, 8],
40404
+ * anchor: 'bottom',
40405
+ * });
40406
+ *
40407
+ * return box(40, 30, 12);
40408
+ * ```
40409
+ *
40410
+ * @param text - Label text to display in the viewport
40411
+ * @param at - World-space anchor point `[x, y, z]`
40412
+ * @param options - Visual label options
40413
+ * @returns void
40414
+ * @category Viewport Labels
40415
+ */
40416
+ label(text, at, options) {
40417
+ collectRenderLabel(text, at, options);
40418
+ }
40419
+ };
40255
40420
  const RENDER_STYLE_OPTIONS = [
40256
40421
  {
40257
40422
  id: "classic",
@@ -40438,6 +40603,120 @@ function validateCamera(cam, label) {
40438
40603
  }
40439
40604
  return out;
40440
40605
  }
40606
+ function validateViewCamera(cam, label) {
40607
+ const validated = validateCamera(cam, label);
40608
+ if (!validated.position) {
40609
+ throw new Error(`${label}.position is required for named render views`);
40610
+ }
40611
+ if (!validated.target) {
40612
+ throw new Error(`${label}.target is required for named render views`);
40613
+ }
40614
+ return {
40615
+ ...validated,
40616
+ position: validated.position,
40617
+ target: validated.target
40618
+ };
40619
+ }
40620
+ function validateViews(views, label) {
40621
+ if (!views || typeof views !== "object" || Array.isArray(views)) {
40622
+ throw new Error(`${label} must be an object mapping view names to cameras`);
40623
+ }
40624
+ const out = {};
40625
+ for (const [name, view] of Object.entries(views)) {
40626
+ if (!name.trim()) {
40627
+ throw new Error(`${label} names must be non-empty strings`);
40628
+ }
40629
+ const viewLabel = `${label}.${name}`;
40630
+ if (!view || typeof view !== "object" || Array.isArray(view)) {
40631
+ throw new Error(`${viewLabel} must be a camera object or an object with a camera property`);
40632
+ }
40633
+ const hasExplicitCamera = Object.prototype.hasOwnProperty.call(view, "camera");
40634
+ if (hasExplicitCamera) {
40635
+ const camera = view.camera;
40636
+ if (!camera || typeof camera !== "object" || Array.isArray(camera)) {
40637
+ throw new Error(`${viewLabel}.camera must be an object`);
40638
+ }
40639
+ out[name] = { camera: validateViewCamera(camera, `${viewLabel}.camera`) };
40640
+ continue;
40641
+ }
40642
+ out[name] = { camera: validateViewCamera(view, viewLabel) };
40643
+ }
40644
+ return out;
40645
+ }
40646
+ function requireString(value, label) {
40647
+ if (typeof value !== "string" || !value.trim()) {
40648
+ throw new Error(`${label} must be a non-empty string`);
40649
+ }
40650
+ return value.trim();
40651
+ }
40652
+ function optionalString(value, label) {
40653
+ if (value === void 0) return void 0;
40654
+ return requireString(value, label);
40655
+ }
40656
+ function validateJourneyStep(step, label) {
40657
+ if (!step || typeof step !== "object" || Array.isArray(step)) {
40658
+ throw new Error(`${label} must be an object`);
40659
+ }
40660
+ const out = {
40661
+ id: requireString(step.id, `${label}.id`)
40662
+ };
40663
+ const title = optionalString(step.title, `${label}.title`);
40664
+ if (title !== void 0) out.title = title;
40665
+ const focus = optionalString(step.focus, `${label}.focus`);
40666
+ if (focus !== void 0) out.focus = focus;
40667
+ const caption = optionalString(step.caption, `${label}.caption`);
40668
+ if (caption !== void 0) out.caption = caption;
40669
+ if (step.camera !== void 0) {
40670
+ if (!step.camera || typeof step.camera !== "object" || Array.isArray(step.camera)) {
40671
+ throw new Error(`${label}.camera must be an object`);
40672
+ }
40673
+ out.camera = validateViewCamera(step.camera, `${label}.camera`);
40674
+ }
40675
+ return out;
40676
+ }
40677
+ function validateJourney(journey, label) {
40678
+ if (!journey || typeof journey !== "object" || Array.isArray(journey)) {
40679
+ throw new Error(`${label} must be an object`);
40680
+ }
40681
+ if (!Array.isArray(journey.steps) || journey.steps.length === 0) {
40682
+ throw new Error(`${label}.steps must be a non-empty array`);
40683
+ }
40684
+ const out = {
40685
+ steps: journey.steps.map((step, index) => validateJourneyStep(step, `${label}.steps[${index}]`))
40686
+ };
40687
+ const title = optionalString(journey.title, `${label}.title`);
40688
+ if (title !== void 0) out.title = title;
40689
+ const startsAt = optionalString(journey.startsAt, `${label}.startsAt`);
40690
+ if (startsAt !== void 0) out.startsAt = startsAt;
40691
+ if (journey.behavior !== void 0) {
40692
+ if (journey.behavior !== "opt-in" && journey.behavior !== "auto") {
40693
+ throw new Error(`${label}.behavior must be "opt-in" or "auto"`);
40694
+ }
40695
+ out.behavior = journey.behavior;
40696
+ }
40697
+ const seen = /* @__PURE__ */ new Set();
40698
+ for (const step of out.steps) {
40699
+ if (seen.has(step.id)) {
40700
+ throw new Error(`${label}.steps contains duplicate step id "${step.id}"`);
40701
+ }
40702
+ seen.add(step.id);
40703
+ }
40704
+ if (out.startsAt && !seen.has(out.startsAt)) {
40705
+ throw new Error(`${label}.startsAt "${out.startsAt}" does not match any step id`);
40706
+ }
40707
+ return out;
40708
+ }
40709
+ function validateJourneys(journeys, label) {
40710
+ if (!journeys || typeof journeys !== "object" || Array.isArray(journeys)) {
40711
+ throw new Error(`${label} must be an object mapping journey ids to journey configs`);
40712
+ }
40713
+ const out = {};
40714
+ for (const [id, journey] of Object.entries(journeys)) {
40715
+ const normalizedId = requireString(id, `${label} journey id`);
40716
+ out[normalizedId] = validateJourney(journey, `${label}.${normalizedId}`);
40717
+ }
40718
+ return out;
40719
+ }
40441
40720
  function validateLight(light, label) {
40442
40721
  if (!light || typeof light !== "object") throw new Error(`${label} must be an object`);
40443
40722
  if (!VALID_LIGHT_TYPES.has(light.type)) {
@@ -40581,7 +40860,18 @@ function scene(options) {
40581
40860
  if (!options || typeof options !== "object") {
40582
40861
  throw new Error("scene(options) expects an options object");
40583
40862
  }
40584
- const current = _collected$3 ? { ..._collected$3 } : { background: null, camera: null, lights: null, environment: null, fog: null, postProcessing: null, ground: null, capture: null };
40863
+ const current = _collected$3 ? { ..._collected$3 } : {
40864
+ background: null,
40865
+ camera: null,
40866
+ views: null,
40867
+ journeys: null,
40868
+ lights: null,
40869
+ environment: null,
40870
+ fog: null,
40871
+ postProcessing: null,
40872
+ ground: null,
40873
+ capture: null
40874
+ };
40585
40875
  if (options.background !== void 0) {
40586
40876
  current.background = validateBackground(options.background, "scene.background");
40587
40877
  }
@@ -40592,6 +40882,14 @@ function scene(options) {
40592
40882
  const validated = validateCamera(options.camera, "scene.camera");
40593
40883
  current.camera = current.camera ? { ...current.camera, ...validated } : validated;
40594
40884
  }
40885
+ if (options.views !== void 0) {
40886
+ const validated = validateViews(options.views, "scene.views");
40887
+ current.views = current.views ? { ...current.views, ...validated } : validated;
40888
+ }
40889
+ if (options.journeys !== void 0) {
40890
+ const validated = validateJourneys(options.journeys, "scene.journeys");
40891
+ current.journeys = current.journeys ? { ...current.journeys, ...validated } : validated;
40892
+ }
40595
40893
  if (options.lights !== void 0) {
40596
40894
  if (!Array.isArray(options.lights)) {
40597
40895
  throw new Error("scene.lights must be an array");
@@ -40632,6 +40930,74 @@ function scene(options) {
40632
40930
  }
40633
40931
  _collected$3 = current;
40634
40932
  }
40933
+ const targetPath = (target) => {
40934
+ var _a3;
40935
+ const path2 = (_a3 = target.treePath) == null ? void 0 : _a3.filter((entry) => entry.trim());
40936
+ return path2 && path2.length > 0 ? path2.join("/") : target.name;
40937
+ };
40938
+ const hasErrorDiagnostic = (diagnostics) => diagnostics.some((diagnostic) => diagnostic.level === "error");
40939
+ function resolveJourneyFocus(focus, targets) {
40940
+ return targets.filter((target) => target.name === focus || targetPath(target) === focus);
40941
+ }
40942
+ function formatAvailableTargets(targets) {
40943
+ return targets.map((target) => targetPath(target)).filter((value, index, values) => values.indexOf(value) === index).sort().slice(0, 8);
40944
+ }
40945
+ function resolveSceneJourneyTargets(config, targets) {
40946
+ if (!(config == null ? void 0 : config.journeys)) return config;
40947
+ const journeys = {};
40948
+ for (const [journeyId, journey] of Object.entries(config.journeys)) {
40949
+ const journeyDiagnostics = [...journey.diagnostics ?? []];
40950
+ const steps = journey.steps.map((step) => {
40951
+ const stepDiagnostics = [...step.diagnostics ?? []];
40952
+ let resolvedFocusId = null;
40953
+ let resolvedFocusPath = null;
40954
+ if (step.focus) {
40955
+ const matches = resolveJourneyFocus(step.focus, targets);
40956
+ if (matches.length === 1) {
40957
+ resolvedFocusId = matches[0].id;
40958
+ resolvedFocusPath = targetPath(matches[0]);
40959
+ } else if (matches.length === 0) {
40960
+ stepDiagnostics.push({
40961
+ level: "error",
40962
+ stepId: step.id,
40963
+ message: `focus "${step.focus}" did not match any returned object by name or tree path.`,
40964
+ suggestions: formatAvailableTargets(targets)
40965
+ });
40966
+ } else {
40967
+ stepDiagnostics.push({
40968
+ level: "error",
40969
+ stepId: step.id,
40970
+ message: `focus "${step.focus}" matched ${matches.length} objects. Use a slash-separated tree path.`,
40971
+ suggestions: matches.map((match) => targetPath(match))
40972
+ });
40973
+ }
40974
+ } else if (!step.camera) {
40975
+ stepDiagnostics.push({
40976
+ level: "warning",
40977
+ stepId: step.id,
40978
+ message: "step has no focus or explicit camera, so the viewer can show the caption but cannot move the camera."
40979
+ });
40980
+ }
40981
+ journeyDiagnostics.push(...stepDiagnostics);
40982
+ return {
40983
+ ...step,
40984
+ resolvedFocusId,
40985
+ resolvedFocusPath,
40986
+ diagnostics: stepDiagnostics.length > 0 ? stepDiagnostics : void 0
40987
+ };
40988
+ });
40989
+ journeys[journeyId] = {
40990
+ ...journey,
40991
+ steps,
40992
+ valid: !hasErrorDiagnostic(journeyDiagnostics),
40993
+ diagnostics: journeyDiagnostics.length > 0 ? journeyDiagnostics : void 0
40994
+ };
40995
+ }
40996
+ return {
40997
+ ...config,
40998
+ journeys
40999
+ };
41000
+ }
40635
41001
  const validateColor = (value, label) => {
40636
41002
  if (typeof value !== "string") throw new Error(`${label} must be a string`);
40637
41003
  const trimmed = value.trim();
@@ -41877,9 +42243,15 @@ function cross(a, b) {
41877
42243
  function add(a, b) {
41878
42244
  return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
41879
42245
  }
42246
+ function sub$1(a, b) {
42247
+ return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
42248
+ }
41880
42249
  function scale(v, s) {
41881
42250
  return [v[0] * s, v[1] * s, v[2] * s];
41882
42251
  }
42252
+ function dot$1(a, b) {
42253
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
42254
+ }
41883
42255
  function lerp$1(a, b, t) {
41884
42256
  return a + (b - a) * t;
41885
42257
  }
@@ -41912,6 +42284,65 @@ function sideVectors(axis) {
41912
42284
  function cloneQuery(query) {
41913
42285
  return { side: query.side, u: query.u, v: query.v, offset: query.offset };
41914
42286
  }
42287
+ function normalizedSide(side) {
42288
+ return side === "back" ? "rear" : side;
42289
+ }
42290
+ function profileExponent(station) {
42291
+ if (station.profile.kind === "superEllipse") return station.profile.exponent ?? 3.2;
42292
+ if (station.profile.kind === "roundedRect") return 4.5;
42293
+ return 2;
42294
+ }
42295
+ function superEllipsePoint(rx, ry, exponent, angle) {
42296
+ const cos2 = Math.cos(angle);
42297
+ const sin2 = Math.sin(angle);
42298
+ const x = rx * Math.sign(cos2) * Math.abs(cos2) ** (2 / exponent);
42299
+ const y = ry * Math.sign(sin2) * Math.abs(sin2) ** (2 / exponent);
42300
+ const nx = Math.sign(x) * Math.abs(x / Math.max(rx, EPS$4)) ** Math.max(exponent - 1, 1e-3);
42301
+ const ny = Math.sign(y) * Math.abs(y / Math.max(ry, EPS$4)) ** Math.max(exponent - 1, 1e-3);
42302
+ const nLen = Math.hypot(nx, ny);
42303
+ return {
42304
+ point: [x, y],
42305
+ normal: nLen < EPS$4 ? [Math.sign(cos2), Math.sign(sin2)] : [nx / nLen, ny / nLen]
42306
+ };
42307
+ }
42308
+ function angleForSide(side, u) {
42309
+ const t = clamp$5(u, 0, 1);
42310
+ if (side === "right") return -Math.PI / 2 + t * Math.PI;
42311
+ if (side === "left") return Math.PI / 2 + t * Math.PI;
42312
+ if (side === "top") return Math.PI - t * Math.PI;
42313
+ if (side === "bottom") return Math.PI + t * Math.PI;
42314
+ return null;
42315
+ }
42316
+ function sideSpan(side, width, depth) {
42317
+ if (side === "left" || side === "right") return Math.max(depth, EPS$4);
42318
+ if (side === "top" || side === "bottom") return Math.max(width, EPS$4);
42319
+ return Math.max(width, depth, EPS$4);
42320
+ }
42321
+ function interpolateQuery(a, b, t) {
42322
+ const sideA = normalizedSide(a.side);
42323
+ const sideB = normalizedSide(b.side);
42324
+ if (sideA !== sideB) {
42325
+ throw new Error(
42326
+ `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.`
42327
+ );
42328
+ }
42329
+ return {
42330
+ side: sideA,
42331
+ u: lerp$1(a.u ?? 0.5, b.u ?? 0.5, t),
42332
+ v: lerp$1(a.v ?? 0.5, b.v ?? 0.5, t),
42333
+ offset: lerp$1(a.offset ?? 0, b.offset ?? 0, t)
42334
+ };
42335
+ }
42336
+ function resolvePathQueries(points) {
42337
+ return points.map((point2) => point2 instanceof ProductSurfaceRef ? point2.querySpec() : cloneQuery(point2));
42338
+ }
42339
+ function orientGridToNormal(grid, desiredNormal) {
42340
+ if (grid.length < 2 || grid[0].length < 2) return grid;
42341
+ const widthEdge = sub$1(grid[grid.length - 1][0], grid[0][0]);
42342
+ const lengthEdge = sub$1(grid[0][grid[0].length - 1], grid[0][0]);
42343
+ const actual = norm$1(cross(widthEdge, lengthEdge));
42344
+ return dot$1(actual, desiredNormal) < 0 ? [...grid].reverse() : grid;
42345
+ }
41915
42346
  function isStationBuilder(input) {
41916
42347
  return typeof input.toSpec === "function";
41917
42348
  }
@@ -42013,6 +42444,15 @@ class ProductSkin {
42013
42444
  curveOnSurface(name, points) {
42014
42445
  return points.map((point2, index) => new ProductSurfaceRef(this, { u: 0.5, v: 0.5, ...point2 }, `${name}/${index}`));
42015
42446
  }
42447
+ /**
42448
+ * Create a fluent surface helper for refs and conformal features on one side of this skin.
42449
+ *
42450
+ * Use this when several refs or ribbons share the same skin side; side-local helpers keep
42451
+ * path points concise and make it harder to mix sides accidentally.
42452
+ */
42453
+ surface(side) {
42454
+ return new ProductSurfaceBuilder(this, side);
42455
+ }
42016
42456
  /** Interpolate center, width, and depth at a normalized v or absolute axis value. */
42017
42457
  stationAt(vOrAxis) {
42018
42458
  const axisValue = vOrAxis >= 0 && vOrAxis <= 1 ? lerp$1(this.axisMin, this.axisMax, vOrAxis) : clamp$5(vOrAxis, this.axisMin, this.axisMax);
@@ -42031,7 +42471,9 @@ class ProductSkin {
42031
42471
  width: lerp$1(a.profile.width, b.profile.width, t),
42032
42472
  depth: lerp$1(a.profile.depth, b.profile.depth, t),
42033
42473
  dWidth: (b.profile.width - a.profile.width) / span,
42034
- dDepth: (b.profile.depth - a.profile.depth) / span
42474
+ dDepth: (b.profile.depth - a.profile.depth) / span,
42475
+ exponent: lerp$1(profileExponent(a), profileExponent(b), t),
42476
+ kind: a.profile.kind === b.profile.kind ? a.profile.kind : "custom"
42035
42477
  };
42036
42478
  }
42037
42479
  const last = sorted[sorted.length - 1];
@@ -42041,12 +42483,14 @@ class ProductSkin {
42041
42483
  width: last.profile.width,
42042
42484
  depth: last.profile.depth,
42043
42485
  dWidth: 0,
42044
- dDepth: 0
42486
+ dDepth: 0,
42487
+ exponent: profileExponent(last),
42488
+ kind: last.profile.kind
42045
42489
  };
42046
42490
  }
42047
42491
  /** Build a local surface frame from a side/u/v query. */
42048
42492
  frame(query) {
42049
- const side = query.side === "back" ? "rear" : query.side;
42493
+ const side = normalizedSide(query.side);
42050
42494
  const offset2 = query.offset ?? 0;
42051
42495
  const basis = sideVectors(this.axis);
42052
42496
  const isFrontCap = side === "front";
@@ -42070,11 +42514,31 @@ class ProductSkin {
42070
42514
  }
42071
42515
  const station = this.stationAt(query.v ?? 0.5);
42072
42516
  const u = clamp$5(query.u ?? 0.5, 0, 1) - 0.5;
42517
+ const sideAngle = angleForSide(side, query.u ?? 0.5);
42073
42518
  let crossA = 0;
42074
42519
  let crossB = 0;
42075
42520
  let normal = [0, 0, 1];
42076
42521
  let tangentU = basis.crossA;
42077
- if (side === "right") {
42522
+ if (sideAngle != null) {
42523
+ const section = superEllipsePoint(station.width / 2, station.depth / 2, station.exponent, sideAngle);
42524
+ crossA = section.point[0];
42525
+ crossB = section.point[1];
42526
+ normal = norm$1(add(scale(basis.crossA, section.normal[0]), scale(basis.crossB, section.normal[1])));
42527
+ const delta = 2e-3;
42528
+ const prev = superEllipsePoint(
42529
+ station.width / 2,
42530
+ station.depth / 2,
42531
+ station.exponent,
42532
+ angleForSide(side, clamp$5((query.u ?? 0.5) - delta, 0, 1)) ?? sideAngle
42533
+ ).point;
42534
+ const next = superEllipsePoint(
42535
+ station.width / 2,
42536
+ station.depth / 2,
42537
+ station.exponent,
42538
+ angleForSide(side, clamp$5((query.u ?? 0.5) + delta, 0, 1)) ?? sideAngle
42539
+ ).point;
42540
+ tangentU = norm$1(add(scale(basis.crossA, next[0] - prev[0]), scale(basis.crossB, next[1] - prev[1])));
42541
+ } else if (side === "right") {
42078
42542
  crossA = station.width / 2;
42079
42543
  crossB = u * station.depth;
42080
42544
  normal = basis.crossA;
@@ -42096,7 +42560,7 @@ class ProductSkin {
42096
42560
  tangentU = basis.crossA;
42097
42561
  }
42098
42562
  normal = norm$1(normal);
42099
- tangentU = norm$1(tangentU);
42563
+ tangentU = norm$1(sub$1(tangentU, scale(normal, dot$1(tangentU, normal))));
42100
42564
  const tangentV = norm$1(cross(normal, tangentU));
42101
42565
  const point2 = add(add(station.center, add(scale(basis.crossA, crossA), scale(basis.crossB, crossB))), scale(normal, offset2));
42102
42566
  return {
@@ -42416,6 +42880,303 @@ class ProductHandleBuilder {
42416
42880
  return new ProductHandleFeature(grip, upperPad, lowerPad);
42417
42881
  }
42418
42882
  }
42883
+ class ProductSurfaceBuilder {
42884
+ constructor(skin, side) {
42885
+ this.skin = skin;
42886
+ this.side = side;
42887
+ }
42888
+ /** Create a ref on this skin side. */
42889
+ ref(u = 0.5, v = 0.5, offset2) {
42890
+ return Product.ref(this.skin, {
42891
+ side: this.side,
42892
+ u,
42893
+ v,
42894
+ ...offset2 != null ? { offset: offset2 } : {}
42895
+ });
42896
+ }
42897
+ /** Create a side/u/v query on this skin side. */
42898
+ uv(u = 0.5, v = 0.5, offset2) {
42899
+ return {
42900
+ side: this.side,
42901
+ u,
42902
+ v,
42903
+ ...offset2 != null ? { offset: offset2 } : {}
42904
+ };
42905
+ }
42906
+ /**
42907
+ * Start a conformal ribbon on this skin side.
42908
+ *
42909
+ * Path points use side-local `u`/`v` coordinates; this builder supplies the side.
42910
+ * The returned ProductRibbonBuilder is already bound to the source skin and can be further
42911
+ * configured before build(). Use `widthSamples` >= 3 when the ribbon must visibly wrap over
42912
+ * curved product sections instead of behaving like a flat strip.
42913
+ */
42914
+ ribbon(name, points, options = {}) {
42915
+ if (points.length < 2) throw new Error("Product.surface(...).ribbon(name, points) requires at least two path points");
42916
+ const path2 = points.map((point2) => ({
42917
+ side: this.side,
42918
+ u: point2.u ?? 0.5,
42919
+ v: point2.v ?? 0.5,
42920
+ ...point2.offset != null ? { offset: point2.offset } : {}
42921
+ }));
42922
+ return new ProductRibbonBuilder(name).on(this.skin, path2, options);
42923
+ }
42924
+ }
42925
+ class ProductRibbonBuilder {
42926
+ constructor(name) {
42927
+ __publicField(this, "skinValue");
42928
+ __publicField(this, "queryPath", []);
42929
+ __publicField(this, "refPath", []);
42930
+ __publicField(this, "widthValue", 6);
42931
+ __publicField(this, "thicknessValue", 0.8);
42932
+ __publicField(this, "offsetValue", 0.25);
42933
+ __publicField(this, "samplesValue", 24);
42934
+ __publicField(this, "widthSamplesValue", 5);
42935
+ __publicField(this, "resolutionValue");
42936
+ __publicField(this, "materialValue");
42937
+ __publicField(this, "colorValue");
42938
+ __publicField(this, "lastDiagnosticsValue");
42939
+ this.name = name;
42940
+ if (!name || !name.trim()) throw new Error("Product.ribbon(name) requires a non-empty name");
42941
+ }
42942
+ /**
42943
+ * Follow a ProductSkin with side/u/v path queries or refs.
42944
+ *
42945
+ * This is the highest-fidelity mode because every interpolated sample is resolved through
42946
+ * ProductSkin.frame(), so the ribbon bends along the selected side as station width/depth changes.
42947
+ * All query path points must stay on one side; split side transitions into separate ribbons.
42948
+ */
42949
+ on(skin, points, options = {}) {
42950
+ if (points.length < 2) throw new Error("Product.ribbon().on(skin, points) requires at least two path points");
42951
+ this.skinValue = skin;
42952
+ this.queryPath = resolvePathQueries(points);
42953
+ this.refPath = [];
42954
+ return this.applyOptions(options);
42955
+ }
42956
+ /**
42957
+ * Follow explicit surface refs.
42958
+ *
42959
+ * Useful for named refs or paths assembled elsewhere. The builder resolves each ref frame and
42960
+ * interpolates between those frames; use on(skin, points) when you need full skin-side sampling
42961
+ * between sparse control points.
42962
+ */
42963
+ fromRefs(points, options = {}) {
42964
+ if (points.length < 2) throw new Error("Product.ribbon().fromRefs(points) requires at least two refs");
42965
+ this.skinValue = void 0;
42966
+ this.queryPath = [];
42967
+ this.refPath = [...points];
42968
+ return this.applyOptions(options);
42969
+ }
42970
+ /** Set ribbon width in millimeters. */
42971
+ width(width) {
42972
+ if (!Number.isFinite(width) || width <= 0) throw new Error("Product.ribbon().width(width) requires a positive finite number");
42973
+ this.widthValue = width;
42974
+ return this;
42975
+ }
42976
+ /** Set solid thickness outward from the source surface in millimeters. */
42977
+ thickness(thickness) {
42978
+ if (!Number.isFinite(thickness) || thickness <= 0)
42979
+ throw new Error("Product.ribbon().thickness(thickness) requires a positive finite number");
42980
+ this.thicknessValue = thickness;
42981
+ return this;
42982
+ }
42983
+ /** Set positive clearance between the source surface and the ribbon's inner face. */
42984
+ offset(offset2) {
42985
+ if (!Number.isFinite(offset2)) throw new Error("Product.ribbon().offset(offset) requires a finite number");
42986
+ this.offsetValue = offset2;
42987
+ return this;
42988
+ }
42989
+ /** Set samples along the path. */
42990
+ samples(samples) {
42991
+ if (!Number.isFinite(samples) || samples < 2) throw new Error("Product.ribbon().samples(samples) requires a value >= 2");
42992
+ this.samplesValue = Math.round(samples);
42993
+ return this;
42994
+ }
42995
+ /** Set samples across the width. Use 3+ to bend over curved cross-sections. */
42996
+ widthSamples(samples) {
42997
+ if (!Number.isFinite(samples) || samples < 2) throw new Error("Product.ribbon().widthSamples(samples) requires a value >= 2");
42998
+ this.widthSamplesValue = Math.round(samples);
42999
+ return this;
43000
+ }
43001
+ /** Set NURBS tessellation resolution. */
43002
+ resolution(resolution) {
43003
+ if (!Number.isFinite(resolution) || resolution < 2) throw new Error("Product.ribbon().resolution(resolution) requires a value >= 2");
43004
+ this.resolutionValue = Math.round(resolution);
43005
+ return this;
43006
+ }
43007
+ /** Apply a product material preset. */
43008
+ material(material) {
43009
+ this.materialValue = material;
43010
+ return this;
43011
+ }
43012
+ /** Apply a simple color override. */
43013
+ color(color) {
43014
+ this.colorValue = color;
43015
+ return this;
43016
+ }
43017
+ /** Build a conformal ribbon as a thin NURBS surface solid. */
43018
+ build(options = {}) {
43019
+ return this.buildWithDiagnostics(options).shape;
43020
+ }
43021
+ /**
43022
+ * Build a conformal ribbon and return surface-feature diagnostics.
43023
+ *
43024
+ * Use this while validating API usage or model fidelity; diagnostics report sampling counts,
43025
+ * side-span clamping, lowering mode, and warnings that should be visible in reviews.
43026
+ */
43027
+ buildWithDiagnostics(options = {}) {
43028
+ this.applyOptions(options);
43029
+ const gridResult = this.skinValue ? this.buildSkinGrid(this.skinValue, this.queryPath) : this.buildRefGrid(this.refPath);
43030
+ const desiredNormal = this.centerDesiredNormal();
43031
+ let ribbon = nurbsSurface(orientGridToNormal(gridResult.grid, desiredNormal), {
43032
+ degreeU: Math.min(3, this.widthSamplesValue - 1),
43033
+ degreeV: Math.min(3, this.samplesValue - 1),
43034
+ thickness: this.thicknessValue,
43035
+ resolution: this.resolutionValue ?? Math.max(this.samplesValue, this.widthSamplesValue, 12),
43036
+ approximate: true
43037
+ }).as(this.name);
43038
+ if (this.colorValue) ribbon = ribbon.color(this.colorValue);
43039
+ const shape = applyMaterial(ribbon, this.materialValue);
43040
+ this.lastDiagnosticsValue = gridResult.diagnostics;
43041
+ return { shape, diagnostics: this.cloneDiagnostics(gridResult.diagnostics) };
43042
+ }
43043
+ /** Return diagnostics from the most recent build, if this builder has been built. */
43044
+ diagnostics() {
43045
+ return this.lastDiagnosticsValue ? this.cloneDiagnostics(this.lastDiagnosticsValue) : void 0;
43046
+ }
43047
+ applyOptions(options) {
43048
+ if (options.width != null) this.width(options.width);
43049
+ if (options.thickness != null) this.thickness(options.thickness);
43050
+ if (options.offset != null) this.offset(options.offset);
43051
+ if (options.samples != null) this.samples(options.samples);
43052
+ if (options.widthSamples != null) this.widthSamples(options.widthSamples);
43053
+ if (options.resolution != null) this.resolution(options.resolution);
43054
+ if (options.material) this.material(options.material);
43055
+ if (options.color) this.color(options.color);
43056
+ return this;
43057
+ }
43058
+ centerDesiredNormal() {
43059
+ if (this.skinValue && this.queryPath.length > 0) {
43060
+ const mid = this.samplePathQuery(0.5);
43061
+ return this.skinValue.frame({ ...mid, offset: (mid.offset ?? 0) + this.offsetValue }).normal;
43062
+ }
43063
+ if (this.refPath.length > 0) return this.refPath[Math.floor(this.refPath.length / 2)].frame({ offset: this.offsetValue }).normal;
43064
+ return [0, 0, 1];
43065
+ }
43066
+ samplePathQuery(t) {
43067
+ if (this.queryPath.length < 2) throw new Error("Product.ribbon().on(...) must be called before .build()");
43068
+ const segmentCount = this.queryPath.length - 1;
43069
+ const scaled = clamp$5(t, 0, 1) * segmentCount;
43070
+ const segment = Math.min(segmentCount - 1, Math.floor(scaled));
43071
+ const localT = scaled - segment;
43072
+ return interpolateQuery(this.queryPath[segment], this.queryPath[segment + 1], localT);
43073
+ }
43074
+ buildSkinGrid(skin, path2) {
43075
+ if (path2.length < 2) throw new Error("Product.ribbon().on(skin, points) must be called before .build()");
43076
+ const side = normalizedSide(path2[0].side);
43077
+ if (side === "front" || side === "rear") {
43078
+ throw new Error(
43079
+ "Product.ribbon().on(...) supports side ribbons on left/right/top/bottom surfaces. Use Product.panel() for front/rear caps."
43080
+ );
43081
+ }
43082
+ for (const point2 of path2) {
43083
+ if (normalizedSide(point2.side) !== side) {
43084
+ throw new Error("Product.ribbon().on(...) currently supports one side per ribbon. Split ribbons at side transitions.");
43085
+ }
43086
+ }
43087
+ const rows = Array.from({ length: this.widthSamplesValue }, () => []);
43088
+ let clampedUCount = 0;
43089
+ let maxUClampDistance = 0;
43090
+ for (let i = 0; i < this.samplesValue; i += 1) {
43091
+ const along = this.samplesValue === 1 ? 0 : i / (this.samplesValue - 1);
43092
+ const center = this.samplePathQuery(along);
43093
+ const station = skin.stationAt(center.v ?? 0.5);
43094
+ const span = sideSpan(side, station.width, station.depth);
43095
+ for (let j = 0; j < this.widthSamplesValue; j += 1) {
43096
+ const across = this.widthSamplesValue === 1 ? 0 : j / (this.widthSamplesValue - 1) - 0.5;
43097
+ const rawU = (center.u ?? 0.5) + across * this.widthValue / span;
43098
+ const u = clamp$5(rawU, 0, 1);
43099
+ const clampDistance = Math.abs(rawU - u) * span;
43100
+ if (clampDistance > EPS$4) {
43101
+ clampedUCount += 1;
43102
+ maxUClampDistance = Math.max(maxUClampDistance, clampDistance);
43103
+ }
43104
+ const query = {
43105
+ ...center,
43106
+ side,
43107
+ u,
43108
+ offset: (center.offset ?? 0) + this.offsetValue + this.thicknessValue
43109
+ };
43110
+ rows[j].push(skin.frame(query).point);
43111
+ }
43112
+ }
43113
+ return {
43114
+ grid: rows,
43115
+ diagnostics: this.makeDiagnostics({
43116
+ skin: skin.name,
43117
+ side,
43118
+ pathPointCount: path2.length,
43119
+ clampedUCount,
43120
+ maxUClampDistance
43121
+ })
43122
+ };
43123
+ }
43124
+ buildRefGrid(refs) {
43125
+ if (refs.length < 2) throw new Error("Product.ribbon().fromRefs(points) must be called before .build()");
43126
+ const frames = refs.map((ref) => ref.frame({ offset: this.offsetValue + this.thicknessValue }));
43127
+ const rows = Array.from({ length: this.widthSamplesValue }, () => []);
43128
+ for (const frame of frames) {
43129
+ for (let j = 0; j < this.widthSamplesValue; j += 1) {
43130
+ const across = this.widthSamplesValue === 1 ? 0 : j / (this.widthSamplesValue - 1) - 0.5;
43131
+ rows[j].push(add(frame.point, scale(frame.tangentU, across * this.widthValue)));
43132
+ }
43133
+ }
43134
+ this.samplesValue = refs.length;
43135
+ return {
43136
+ grid: rows,
43137
+ diagnostics: this.makeDiagnostics({
43138
+ pathPointCount: refs.length,
43139
+ clampedUCount: 0,
43140
+ maxUClampDistance: 0
43141
+ })
43142
+ };
43143
+ }
43144
+ makeDiagnostics(input) {
43145
+ const resolution = this.resolutionValue ?? Math.max(this.samplesValue, this.widthSamplesValue, 12);
43146
+ const warnings = [];
43147
+ if (input.clampedUCount > 0) {
43148
+ warnings.push(
43149
+ `Ribbon '${this.name}' was clipped to the ${input.side ?? "surface"} UV bounds at ${input.clampedUCount} sampled point(s).`
43150
+ );
43151
+ }
43152
+ if (this.samplesValue < 8) warnings.push(`Ribbon '${this.name}' uses low along-path sampling; increase samples() for smoother bends.`);
43153
+ if (this.widthSamplesValue < 3)
43154
+ warnings.push(`Ribbon '${this.name}' uses low width sampling; use widthSamples(3+) to show cross-surface curvature.`);
43155
+ return {
43156
+ name: this.name,
43157
+ ...input.skin ? { skin: input.skin } : {},
43158
+ ...input.side ? { side: input.side } : {},
43159
+ pathPointCount: input.pathPointCount,
43160
+ width: this.widthValue,
43161
+ thickness: this.thicknessValue,
43162
+ offset: this.offsetValue,
43163
+ samples: this.samplesValue,
43164
+ widthSamples: this.widthSamplesValue,
43165
+ resolution,
43166
+ lowering: "nurbsSurface",
43167
+ expectedFidelity: "mixed",
43168
+ clampedUCount: input.clampedUCount,
43169
+ maxUClampDistance: input.maxUClampDistance,
43170
+ warnings
43171
+ };
43172
+ }
43173
+ cloneDiagnostics(diagnostics) {
43174
+ return {
43175
+ ...diagnostics,
43176
+ warnings: [...diagnostics.warnings]
43177
+ };
43178
+ }
43179
+ }
42419
43180
  const Product = {
42420
43181
  /** Start a named product skin builder. */
42421
43182
  skin(name) {
@@ -42488,10 +43249,27 @@ const Product = {
42488
43249
  ref(skin, query) {
42489
43250
  return new ProductSurfaceRef(skin, query);
42490
43251
  },
43252
+ /**
43253
+ * Create a fluent surface helper for refs and conformal features on one side of a skin.
43254
+ *
43255
+ * Equivalent to skin.surface(side), useful when writing in Product.* namespace style.
43256
+ */
43257
+ surface(skin, side) {
43258
+ return skin.surface(side);
43259
+ },
42491
43260
  /** Start a panel feature builder. */
42492
43261
  panel(name) {
42493
43262
  return new ProductPanelBuilder(name);
42494
43263
  },
43264
+ /**
43265
+ * Start a conformal ribbon/trim builder for details that should bend with a ProductSkin.
43266
+ *
43267
+ * Call .on(skin, points) for side/u/v sampling or .fromRefs(points) for explicit surface refs,
43268
+ * then configure width, thickness, offset, sampling, material, and color before build().
43269
+ */
43270
+ ribbon(name) {
43271
+ return new ProductRibbonBuilder(name);
43272
+ },
42495
43273
  /** Start a spout/nozzle feature builder. */
42496
43274
  spout(name) {
42497
43275
  return new ProductSpoutBuilder(name);
@@ -45352,6 +46130,7 @@ function cameraTrajectory(defOrFn, options) {
45352
46130
  throw new Error('cameraTrajectory(): each keyframe must have either an "orbit" or "position" property');
45353
46131
  }
45354
46132
  }
46133
+ var define_process_env_default = {};
45355
46134
  function resolveEdges(shape, edges) {
45356
46135
  if (!edges) {
45357
46136
  return selectEdges(shape);
@@ -45373,6 +46152,76 @@ function isEdgeSegment(value) {
45373
46152
  function isEdgeReferenceLike(value) {
45374
46153
  return typeof value === "object" && value !== null && "edges" in value && typeof value.edges === "function";
45375
46154
  }
46155
+ const BROAD_EDGE_FEATURE_DEFAULT_BUDGET = {
46156
+ live: 0,
46157
+ default: 12,
46158
+ high: Number.POSITIVE_INFINITY
46159
+ };
46160
+ let broadEdgeFeatureBudget = null;
46161
+ function readBroadEdgeFeatureEnv(name) {
46162
+ return typeof process !== "undefined" ? define_process_env_default == null ? void 0 : define_process_env_default[name] : void 0;
46163
+ }
46164
+ function resolveBroadEdgeFeatureBudget() {
46165
+ if (readBroadEdgeFeatureEnv("FORGECAD_ALLOW_BROAD_EDGE_FEATURES") === "1") return Number.POSITIVE_INFINITY;
46166
+ const override = readBroadEdgeFeatureEnv("FORGECAD_BROAD_EDGE_FEATURE_BUDGET");
46167
+ if (override != null && override.trim() !== "") {
46168
+ const parsed = Number(override);
46169
+ if (Number.isFinite(parsed) && parsed >= 0) return parsed;
46170
+ }
46171
+ return BROAD_EDGE_FEATURE_DEFAULT_BUDGET[getForgeQualityPreset()];
46172
+ }
46173
+ function resetBroadEdgeFeatureBudget() {
46174
+ broadEdgeFeatureBudget = null;
46175
+ }
46176
+ function remainingBroadEdgeFeatureBudget() {
46177
+ if (broadEdgeFeatureBudget === null) broadEdgeFeatureBudget = resolveBroadEdgeFeatureBudget();
46178
+ return broadEdgeFeatureBudget;
46179
+ }
46180
+ function consumeBroadEdgeFeatureBudget(edgeCount) {
46181
+ const remaining = remainingBroadEdgeFeatureBudget();
46182
+ if (!Number.isFinite(remaining)) return true;
46183
+ if (edgeCount > remaining) return false;
46184
+ broadEdgeFeatureBudget = Math.max(0, remaining - edgeCount);
46185
+ return true;
46186
+ }
46187
+ function shouldSkipBroadEdgeFeature(operation, edgeCount) {
46188
+ if (consumeBroadEdgeFeatureBudget(edgeCount)) return false;
46189
+ const remaining = Math.max(0, remainingBroadEdgeFeatureBudget());
46190
+ emitRuntimeWarning(
46191
+ `${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.`
46192
+ );
46193
+ return true;
46194
+ }
46195
+ function shouldSkipExhaustedBroadEdgeFeature(operation) {
46196
+ const remaining = remainingBroadEdgeFeatureBudget();
46197
+ if (!Number.isFinite(remaining) || remaining > 0) return false;
46198
+ emitRuntimeWarning(
46199
+ `${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.`
46200
+ );
46201
+ return true;
46202
+ }
46203
+ function estimateSelectorlessEdgeCount(plan) {
46204
+ if (!plan) return null;
46205
+ switch (plan.kind) {
46206
+ case "box":
46207
+ return 12;
46208
+ case "queryOwner":
46209
+ case "transform":
46210
+ return estimateSelectorlessEdgeCount(plan.base);
46211
+ default:
46212
+ return null;
46213
+ }
46214
+ }
46215
+ function shouldSkipUnestimatedBroadEdgeFeature(operation, target) {
46216
+ const remaining = remainingBroadEdgeFeatureBudget();
46217
+ if (!Number.isFinite(remaining)) return false;
46218
+ const estimatedEdges = estimateSelectorlessEdgeCount(getShapeCompilePlan(target));
46219
+ if (estimatedEdges !== null && estimatedEdges <= remaining) return false;
46220
+ emitRuntimeWarning(
46221
+ `${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.`
46222
+ );
46223
+ return true;
46224
+ }
45376
46225
  function edgesToTargets(edges) {
45377
46226
  return edges.map((e) => ({
45378
46227
  midpoint: [e.midpoint[0], e.midpoint[1], e.midpoint[2]],
@@ -45386,10 +46235,13 @@ function fillet(shape, radius, edges, segments = 16) {
45386
46235
  throw new Error("fillet() requires a positive finite radius.");
45387
46236
  }
45388
46237
  const target = shape;
46238
+ if (edges === void 0 && shouldSkipExhaustedBroadEdgeFeature("fillet")) return target;
46239
+ if (edges === void 0 && shouldSkipUnestimatedBroadEdgeFeature("fillet", target)) return target;
45389
46240
  const resolvedEdges = resolveEdges(target, edges);
45390
46241
  if (resolvedEdges.length === 0) {
45391
46242
  throw new Error("fillet(): no edges match the given selection.");
45392
46243
  }
46244
+ if (edges === void 0 && shouldSkipBroadEdgeFeature("fillet", resolvedEdges.length)) return target;
45393
46245
  const basePlan = getShapeCompilePlan(target);
45394
46246
  const plan = {
45395
46247
  kind: "filletEdges",
@@ -45409,10 +46261,13 @@ function chamfer(shape, size, edges) {
45409
46261
  throw new Error("chamfer() requires a positive finite size.");
45410
46262
  }
45411
46263
  const target = shape;
46264
+ if (edges === void 0 && shouldSkipExhaustedBroadEdgeFeature("chamfer")) return target;
46265
+ if (edges === void 0 && shouldSkipUnestimatedBroadEdgeFeature("chamfer", target)) return target;
45412
46266
  const resolvedEdges = resolveEdges(target, edges);
45413
46267
  if (resolvedEdges.length === 0) {
45414
46268
  throw new Error("chamfer(): no edges match the given selection.");
45415
46269
  }
46270
+ if (edges === void 0 && shouldSkipBroadEdgeFeature("chamfer", resolvedEdges.length)) return target;
45416
46271
  const basePlan = getShapeCompilePlan(target);
45417
46272
  const plan = {
45418
46273
  kind: "chamferEdges",
@@ -68100,6 +68955,7 @@ const verify = {
68100
68955
  };
68101
68956
  function resetExecutionSession(logs) {
68102
68957
  resetCollectedAssemblies();
68958
+ resetBroadEdgeFeatureBudget();
68103
68959
  resetParams();
68104
68960
  resetShapeQueryOwnerIds();
68105
68961
  resetDimensions();
@@ -68108,6 +68964,7 @@ function resetExecutionSession(logs) {
68108
68964
  resetSheetStock();
68109
68965
  resetRobotExport();
68110
68966
  resetCutPlanes();
68967
+ resetRenderLabels();
68111
68968
  resetCameraTrajectory();
68112
68969
  resetExplodeView();
68113
68970
  resetJointsView();
@@ -68171,6 +69028,7 @@ function collectSuccessfulExecutionSnapshot(args) {
68171
69028
  bom: getCollectedBom(),
68172
69029
  sheetStock: getCollectedSheetStock(),
68173
69030
  cutPlanes: getCollectedCutPlanes(),
69031
+ renderLabels: getCollectedRenderLabels(),
68174
69032
  cameraTrajectory: getCollectedCameraTrajectory(),
68175
69033
  explodeView: getCollectedExplodeView(),
68176
69034
  jointsView: getCollectedJointsView(),
@@ -68194,6 +69052,7 @@ function collectFailedExecutionSnapshot(args) {
68194
69052
  bom: getCollectedBom(),
68195
69053
  sheetStock: getCollectedSheetStock(),
68196
69054
  cutPlanes: getCollectedCutPlanes(),
69055
+ renderLabels: getCollectedRenderLabels(),
68197
69056
  cameraTrajectory: getCollectedCameraTrajectory(),
68198
69057
  explodeView: getCollectedExplodeView(),
68199
69058
  jointsView: getCollectedJointsView(),
@@ -68339,13 +69198,15 @@ function formatLogArg(value) {
68339
69198
  return `[Log serialization failed: ${formatLogError(error)}]`;
68340
69199
  }
68341
69200
  }
68342
- function makeSandboxConsole(collectedLogs) {
69201
+ function makeSandboxConsole(collectedLogs, mirror) {
68343
69202
  const capture = (level) => (...args) => {
69203
+ const formattedArgs = args.map(formatLogArg);
68344
69204
  collectedLogs.push({
68345
69205
  level,
68346
- args: args.map(formatLogArg),
69206
+ args: formattedArgs,
68347
69207
  timestamp: Date.now()
68348
69208
  });
69209
+ mirror == null ? void 0 : mirror(level, formattedArgs);
68349
69210
  };
68350
69211
  return { log: capture("log"), warn: capture("warn"), error: capture("error"), info: capture("info") };
68351
69212
  }
@@ -68507,6 +69368,18 @@ function buildFileIndex(allFiles) {
68507
69368
  }
68508
69369
  return fileIndex;
68509
69370
  }
69371
+ function hasPathExtension(path2) {
69372
+ const fileName = path2.split("/").pop() ?? path2;
69373
+ return /\.[^/.]+$/.test(fileName);
69374
+ }
69375
+ function explicitExtensionHint(requestedName, resolvedPath, fileIndex) {
69376
+ if (!resolvedPath || hasPathExtension(resolvedPath)) return "";
69377
+ const requestedBase = requestedName.trim();
69378
+ const suggestions = [".forge.js", ".js"].map((ext) => ({ ext, resolved: `${resolvedPath}${ext}` })).filter(({ resolved }) => fileIndex.has(resolved)).map(({ ext }) => `"${requestedBase}${ext}"`);
69379
+ if (suggestions.length === 0) return "";
69380
+ const joined = suggestions.length === 1 ? suggestions[0] : suggestions.join(" or ");
69381
+ return ` Did you mean ${joined}? ForgeCAD requires explicit file extensions in project imports.`;
69382
+ }
68510
69383
  function resolveImportSource(fromFile, requestedName, allFiles, options) {
68511
69384
  if (typeof requestedName !== "string" || requestedName.trim().length === 0) {
68512
69385
  throw new Error("Import path must be a non-empty string");
@@ -68515,7 +69388,8 @@ function resolveImportSource(fromFile, requestedName, allFiles, options) {
68515
69388
  const lookupKey = options.fileIndex.get(resolvedPath);
68516
69389
  if (!lookupKey) {
68517
69390
  const suffix = resolvedPath && resolvedPath !== requestedName ? ` (resolved to "${resolvedPath}" from "${fromFile}")` : ` (from "${fromFile}")`;
68518
- throw new Error(`File not found: "${requestedName}"${suffix}`);
69391
+ const hint = explicitExtensionHint(requestedName, resolvedPath, options.fileIndex);
69392
+ throw new Error(`File not found: "${requestedName}"${suffix}.${hint}`);
68519
69393
  }
68520
69394
  const source = allFiles[lookupKey];
68521
69395
  if (typeof source !== "string") {
@@ -122821,7 +123695,7 @@ ${lanes.join("\n")}
122821
123695
  }
122822
123696
  const normalizedSourcePaths = getPathsRelativeToRootDirs(sourceDirectory, rootDirs, getCanonicalFileName);
122823
123697
  const relativePaths = flatMap(normalizedSourcePaths, (sourcePath) => {
122824
- return map(normalizedTargetPaths, (targetPath) => ensurePathIsNonModuleName(getRelativePathFromDirectory(sourcePath, targetPath, getCanonicalFileName)));
123698
+ return map(normalizedTargetPaths, (targetPath2) => ensurePathIsNonModuleName(getRelativePathFromDirectory(sourcePath, targetPath2, getCanonicalFileName)));
122825
123699
  });
122826
123700
  const shortest = min2(relativePaths, compareNumberOfDirectorySeparators);
122827
123701
  if (!shortest) {
@@ -279391,6 +280265,36 @@ function extractUnusedTopLevelVarNames(code) {
279391
280265
  }
279392
280266
  return declaredNames.filter((n) => topLevelNames.has(n) && (!usedByOthers.has(n) || explicitImplicitResultNames.has(n)));
279393
280267
  }
280268
+ function collectBindingNameLocations(node, sourceFile, names) {
280269
+ if (typescriptExports.isIdentifier(node)) {
280270
+ const { line: line2, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
280271
+ names.push({ name: node.text, line: line2 + 1, column: character + 1 });
280272
+ return;
280273
+ }
280274
+ for (const element of node.elements) {
280275
+ if (typescriptExports.isBindingElement(element)) {
280276
+ collectBindingNameLocations(element.name, sourceFile, names);
280277
+ }
280278
+ }
280279
+ }
280280
+ function findTopLevelRuntimeGlobalCollision(code, runtimeGlobalNames) {
280281
+ const runtimeGlobals = new Set(runtimeGlobalNames);
280282
+ const sourceFile = typescriptExports.createSourceFile("__runtime-globals.js", code, typescriptExports.ScriptTarget.ES2020, false, typescriptExports.ScriptKind.JS);
280283
+ const declarations = [];
280284
+ for (const statement of sourceFile.statements) {
280285
+ if (typescriptExports.isVariableStatement(statement)) {
280286
+ const isLexical = (statement.declarationList.flags & (typescriptExports.NodeFlags.Let | typescriptExports.NodeFlags.Const)) !== 0;
280287
+ if (!isLexical) continue;
280288
+ for (const decl of statement.declarationList.declarations) {
280289
+ collectBindingNameLocations(decl.name, sourceFile, declarations);
280290
+ }
280291
+ } else if (typescriptExports.isClassDeclaration(statement) && statement.name) {
280292
+ const { line: line2, character } = sourceFile.getLineAndCharacterOfPosition(statement.name.getStart(sourceFile));
280293
+ declarations.push({ name: statement.name.text, line: line2 + 1, column: character + 1 });
280294
+ }
280295
+ }
280296
+ return declarations.find((declaration) => runtimeGlobals.has(declaration.name)) ?? null;
280297
+ }
279394
280298
  function createForgeRuntimeModule(bindings) {
279395
280299
  const runtime = { ...bindings };
279396
280300
  Object.defineProperty(runtime, "__esModule", { value: true });
@@ -279931,6 +280835,12 @@ function withConstructorChainLockdown(fn) {
279931
280835
  }
279932
280836
  }
279933
280837
  function executeFile(code, fileName, allFiles, visited, scope = {}, options, executionMode = "script", moduleCacheEntry) {
280838
+ var _a3, _b3, _c2, _d2, _e2, _f;
280839
+ (_a3 = options.debug) == null ? void 0 : _a3.call(options, "executeFile:start", {
280840
+ fileName,
280841
+ executionMode,
280842
+ scope: scope.namePrefix ?? fileName
280843
+ });
279934
280844
  const trackCircularImports = executionMode === "script";
279935
280845
  if (trackCircularImports) {
279936
280846
  if (visited.has(fileName)) {
@@ -280008,13 +280918,13 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
280008
280918
  });
280009
280919
  };
280010
280920
  const importStep = (name) => {
280011
- var _a3;
280921
+ var _a4;
280012
280922
  if (typeof name !== "string" || name.trim().length === 0) {
280013
280923
  throw new Error("importStep() requires a non-empty file path string");
280014
280924
  }
280015
280925
  const resolvedPath = resolveImportPath(fileName, name.trim());
280016
280926
  rejectPathTraversal("importStep", name, resolvedPath);
280017
- const ext = ((_a3 = resolvedPath.split(".").pop()) == null ? void 0 : _a3.toLowerCase()) ?? "";
280927
+ const ext = ((_a4 = resolvedPath.split(".").pop()) == null ? void 0 : _a4.toLowerCase()) ?? "";
280018
280928
  if (ext !== "step" && ext !== "stp") {
280019
280929
  throw new Error(`importStep("${name}"): unsupported extension ".${ext}". Expected .step or .stp`);
280020
280930
  }
@@ -280043,7 +280953,39 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
280043
280953
  setShapeTopology(shape, topo);
280044
280954
  return shape;
280045
280955
  };
280046
- const sandboxConsole = makeSandboxConsole(_collectedLogs);
280956
+ const sandboxConsole = makeSandboxConsole(_collectedLogs, options.debug ? (level, args) => {
280957
+ var _a4;
280958
+ return (_a4 = options.debug) == null ? void 0 : _a4.call(options, "console", { level, args });
280959
+ } : void 0);
280960
+ const runtimeVerify = options.debug ? {
280961
+ ...verify,
280962
+ that(label, check2, message) {
280963
+ var _a4, _b4;
280964
+ (_a4 = options.debug) == null ? void 0 : _a4.call(options, "verify:that:start", { label });
280965
+ const verifyStart = performance.now();
280966
+ try {
280967
+ return verify.that(label, check2, message);
280968
+ } finally {
280969
+ (_b4 = options.debug) == null ? void 0 : _b4.call(options, "verify:that:end", {
280970
+ label,
280971
+ ms: Number((performance.now() - verifyStart).toFixed(1))
280972
+ });
280973
+ }
280974
+ },
280975
+ equal(label, actual, expected, tolerance = 0, message) {
280976
+ var _a4, _b4;
280977
+ (_a4 = options.debug) == null ? void 0 : _a4.call(options, "verify:equal:start", { label, actual, expected, tolerance });
280978
+ const verifyStart = performance.now();
280979
+ try {
280980
+ return verify.equal(label, actual, expected, tolerance, message);
280981
+ } finally {
280982
+ (_b4 = options.debug) == null ? void 0 : _b4.call(options, "verify:equal:end", {
280983
+ label,
280984
+ ms: Number((performance.now() - verifyStart).toFixed(1))
280985
+ });
280986
+ }
280987
+ }
280988
+ } : verify;
280047
280989
  setShowLabelsHighlight(highlight);
280048
280990
  const runtimeBindings = {
280049
280991
  box: trackedBox,
@@ -280184,7 +281126,8 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
280184
281126
  jointsView,
280185
281127
  viewConfig,
280186
281128
  scene,
280187
- verify,
281129
+ Viewport,
281130
+ verify: runtimeVerify,
280188
281131
  spec,
280189
281132
  mock,
280190
281133
  gcode,
@@ -280322,8 +281265,21 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
280322
281265
  throw error;
280323
281266
  }
280324
281267
  };
280325
- const compiled = compileScript(code, fileName, options);
280326
281268
  const bindingNames = Object.keys(runtimeBindings);
281269
+ const collision = findTopLevelRuntimeGlobalCollision(code, bindingNames);
281270
+ if (collision) {
281271
+ throw new Error(
281272
+ `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}").`
281273
+ );
281274
+ }
281275
+ (_b3 = options.debug) == null ? void 0 : _b3.call(options, "executeFile:compile:start", { fileName, executionMode });
281276
+ const compileStart = performance.now();
281277
+ const compiled = compileScript(code, fileName, options);
281278
+ (_c2 = options.debug) == null ? void 0 : _c2.call(options, "executeFile:compile:end", {
281279
+ fileName,
281280
+ executionMode,
281281
+ ms: Number((performance.now() - compileStart).toFixed(1))
281282
+ });
280327
281283
  const bindingValues = bindingNames.map((name) => runtimeBindings[name]);
280328
281284
  let scriptCode = compiled.code;
280329
281285
  if (executionMode === "script") {
@@ -280349,12 +281305,20 @@ ${scriptCode}
280349
281305
  exports: executionMode === "module" && moduleCacheEntry ? moduleCacheEntry.exports : {}
280350
281306
  };
280351
281307
  const initialExportsRef = moduleValue.exports;
281308
+ (_d2 = options.debug) == null ? void 0 : _d2.call(options, "executeFile:invoke:start", { fileName, executionMode });
281309
+ const invokeStart = performance.now();
280352
281310
  const returnValue = withConstructorChainLockdown(
280353
281311
  () => runWithParamScope(
280354
281312
  scope,
280355
281313
  () => fn(moduleValue.exports, moduleValue, requireModule, fileName, dirnamePath(fileName), ...bindingValues)
280356
281314
  )
280357
281315
  );
281316
+ (_e2 = options.debug) == null ? void 0 : _e2.call(options, "executeFile:invoke:end", {
281317
+ fileName,
281318
+ executionMode,
281319
+ ms: Number((performance.now() - invokeStart).toFixed(1)),
281320
+ returned: returnValue === void 0 ? "undefined" : returnValue === null ? "null" : typeof returnValue
281321
+ });
280358
281322
  if (executionMode === "module") {
280359
281323
  const hasExports = hasExplicitModuleExports(moduleValue.exports, initialExportsRef);
280360
281324
  if (returnValue !== void 0 && hasExports) {
@@ -280393,12 +281357,14 @@ ${scriptCode}
280393
281357
  }
280394
281358
  return returnValue;
280395
281359
  } finally {
281360
+ (_f = options.debug) == null ? void 0 : _f.call(options, "executeFile:end", { fileName, executionMode });
280396
281361
  if (trackCircularImports) {
280397
281362
  visited.delete(fileName);
280398
281363
  }
280399
281364
  }
280400
281365
  }
280401
281366
  function runScript(code, fileName = "main.forge.js", allFiles = {}, options = {}) {
281367
+ var _a3, _b3;
280402
281368
  _collectedLogs = [];
280403
281369
  resetExecutionSession(_collectedLogs);
280404
281370
  const t0 = performance.now();
@@ -280407,14 +281373,25 @@ function runScript(code, fileName = "main.forge.js", allFiles = {}, options = {}
280407
281373
  fileIndex: buildFileIndex(allFiles),
280408
281374
  compiledFiles: persistentCompiledFiles,
280409
281375
  moduleCache: /* @__PURE__ */ new Map(),
280410
- readBinaryFile: options.readBinaryFile
281376
+ readBinaryFile: options.readBinaryFile,
281377
+ debug: options.debug
280411
281378
  };
280412
281379
  const quality = resolveForgeQualityPreset(options.quality);
281380
+ (_a3 = options.debug) == null ? void 0 : _a3.call(options, "runScript:start", { fileName, quality, fileCount: Object.keys(allFiles).length });
280413
281381
  try {
280414
281382
  return runWithForgeQuality(quality, () => {
281383
+ var _a4, _b4, _c2, _d2, _e2, _f, _g, _h, _i, _j;
281384
+ (_a4 = options.debug) == null ? void 0 : _a4.call(options, "runScript:execute:start", { fileName });
281385
+ const executeStart = performance.now();
280415
281386
  const result = executeFile(code, fileName, allFiles, /* @__PURE__ */ new Set(), {}, execOptions);
281387
+ (_b4 = options.debug) == null ? void 0 : _b4.call(options, "runScript:execute:end", {
281388
+ fileName,
281389
+ ms: Number((performance.now() - executeStart).toFixed(1)),
281390
+ resultType: result === void 0 ? "undefined" : result === null ? "null" : typeof result
281391
+ });
280416
281392
  const highlights = getCollectedHighlights();
280417
281393
  const mocks = getCollectedMocks();
281394
+ (_c2 = options.debug) == null ? void 0 : _c2.call(options, "runScript:map:start", { highlights: highlights.length, mocks: mocks.length });
280418
281395
  const mapped = mapScriptResultToScene({
280419
281396
  result,
280420
281397
  fileName,
@@ -280423,24 +281400,43 @@ function runScript(code, fileName = "main.forge.js", allFiles = {}, options = {}
280423
281400
  mocks,
280424
281401
  logs: _collectedLogs
280425
281402
  });
281403
+ (_d2 = options.debug) == null ? void 0 : _d2.call(options, "runScript:map:end", {
281404
+ objects: mapped.objects.length,
281405
+ hasShape: Boolean(mapped.shape),
281406
+ hasSketch: Boolean(mapped.sketch),
281407
+ hasError: Boolean(mapped.error)
281408
+ });
281409
+ (_e2 = options.debug) == null ? void 0 : _e2.call(options, "runScript:explodeHints:start", { objects: mapped.objects.length });
280426
281410
  autoFillExplodeHints(mapped.objects);
281411
+ (_f = options.debug) == null ? void 0 : _f.call(options, "runScript:explodeHints:end");
281412
+ (_g = options.debug) == null ? void 0 : _g.call(options, "runScript:snapshot:start", { objects: mapped.objects.length });
281413
+ const snapshot = collectSuccessfulExecutionSnapshot({
281414
+ quality,
281415
+ objects: mapped.objects,
281416
+ logs: _collectedLogs,
281417
+ extraDimensions: mapped.extraDimensions,
281418
+ highlights,
281419
+ mocks
281420
+ });
281421
+ (_h = options.debug) == null ? void 0 : _h.call(options, "runScript:snapshot:end", {
281422
+ params: snapshot.params.length,
281423
+ cutPlanes: snapshot.cutPlanes.length,
281424
+ verifications: snapshot.verifications.length
281425
+ });
281426
+ (_i = options.debug) == null ? void 0 : _i.call(options, "runScript:sceneTargets:start");
281427
+ snapshot.sceneConfig = resolveSceneJourneyTargets(snapshot.sceneConfig, mapped.objects);
281428
+ (_j = options.debug) == null ? void 0 : _j.call(options, "runScript:sceneTargets:end");
280427
281429
  return {
280428
281430
  shape: mapped.shape,
280429
281431
  sketch: mapped.sketch,
280430
281432
  objects: mapped.objects,
280431
- ...collectSuccessfulExecutionSnapshot({
280432
- quality,
280433
- objects: mapped.objects,
280434
- logs: _collectedLogs,
280435
- extraDimensions: mapped.extraDimensions,
280436
- highlights,
280437
- mocks
280438
- }),
281433
+ ...snapshot,
280439
281434
  error: mapped.error,
280440
281435
  timeMs: performance.now() - t0
280441
281436
  };
280442
281437
  });
280443
281438
  } catch (e) {
281439
+ (_b3 = options.debug) == null ? void 0 : _b3.call(options, "runScript:error", { error: (e == null ? void 0 : e.message) || String(e) });
280444
281440
  const msg = e.message || String(e);
280445
281441
  const stack = e.stack || "";
280446
281442
  let lineInfo = "";