forgecad 0.9.7 → 0.9.8

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 (47) hide show
  1. package/README.md +1 -0
  2. package/dist/assets/{AdminPage-DX0mpSZT.js → AdminPage-CXaVLMiV.js} +1 -1
  3. package/dist/assets/{BlogPage-CI_P0_Pf.js → BlogPage-Crpr3JjH.js} +1 -1
  4. package/dist/assets/{DocsPage-DLhIIZyJ.js → DocsPage-CNBKuitP.js} +2 -2
  5. package/dist/assets/{EditorApp-DfFT2Dn8.css → EditorApp-D11wL4Qn.css} +51 -0
  6. package/dist/assets/{EditorApp-BujZvuwX.js → EditorApp-DVMnXOmO.js} +151 -9
  7. package/dist/assets/{EmbedViewer-0S0qXKog.js → EmbedViewer-KXFLSnpo.js} +2 -2
  8. package/dist/assets/{LandingPageProofDriven-O_yMtAri.js → LandingPageProofDriven-2q2sn7aW.js} +1 -1
  9. package/dist/assets/{PricingPage-DGkX3Ahr.js → PricingPage-CVvgdv0i.js} +1 -1
  10. package/dist/assets/{SettingsPage-DBsqTB_y.js → SettingsPage-BVj1FtEv.js} +1 -1
  11. package/dist/assets/__vite-browser-external-Dhvy_jtL.js +4 -0
  12. package/dist/assets/{app-BE2nD6Yz.js → app-Dn4EwHhN.js} +707 -458
  13. package/dist/assets/cli/{render-iP9qh475.js → render-BI3gLMXz.js} +1011 -145
  14. package/dist/assets/constructionHistoryWorker-z9_LGiRd.js +42984 -0
  15. package/dist/assets/{evalWorker-Ds5U4xtN.js → evalWorker-CtO7GsJR.js} +42 -9
  16. package/dist/assets/{inspectWorker-Dll4eVyD.js → inspectWorker-BZ2CkQZr.js} +785 -111
  17. package/dist/assets/{manifold-sJ-axdXM.js → manifold-BVi4_OeB.js} +1 -1
  18. package/dist/assets/{manifold-DjYsd7A_.js → manifold-C6-sZYQN.js} +2 -2
  19. package/dist/assets/manifold-Cp_dCC7i.js +3018 -0
  20. package/dist/assets/{manifold-Bk26ViCr.js → manifold-DAzn2Fsa.js} +1 -1
  21. package/dist/assets/{renderSceneState-Bngp5MrQ.js → renderSceneState-BIvOkPK3.js} +1 -1
  22. package/dist/assets/{reportWorker-CU8RZ4O0.js → reportWorker-Bz9tGiHb.js} +42 -9
  23. package/dist/assets/{sectionPlaneMath-BdTjyVfs.js → scalar-sampling-budget-iBAeF8RM.js} +483 -71
  24. package/dist/cli/render.html +1 -1
  25. package/dist/docs/index.html +1 -1
  26. package/dist/docs-raw/CLI.md +10 -10
  27. package/dist/docs-raw/coding-best-practices.md +1 -1
  28. package/dist/docs-raw/guides/inspection-bundles.md +77 -19
  29. package/dist/docs-raw/guides/skill-maintenance.md +1 -1
  30. package/dist/docs-raw/runbook.md +2 -2
  31. package/dist/docs-raw/skills/forgecad-make-a-model.md +11 -0
  32. package/dist/docs-raw/skills/forgecad-render-inspect.md +12 -6
  33. package/dist/docs-raw/skills/index.md +1 -1
  34. package/dist/index.html +1 -1
  35. package/dist/sitemap.xml +6 -6
  36. package/dist-cli/forgecad.js +596 -354
  37. package/dist-cli/forgecad.js.map +1 -1
  38. package/dist-skill/CONTEXT.md +77 -19
  39. package/dist-skill/docs/CLI.md +10 -10
  40. package/dist-skill/docs/guides/inspection-bundles.md +77 -19
  41. package/dist-skill/docs-dev/CLI.md +10 -10
  42. package/dist-skill/docs-dev/coding-best-practices.md +1 -1
  43. package/dist-skill/docs-dev/guides/inspection-bundles.md +77 -19
  44. package/dist-skill/docs-dev/guides/skill-maintenance.md +1 -1
  45. package/dist-skill/library/forgecad-make-a-model/SKILL.md +11 -0
  46. package/dist-skill/library/forgecad-render-inspect/SKILL.md +12 -6
  47. package/package.json +6 -3
@@ -58033,6 +58033,16 @@ var RENDER_STYLE_OPTIONS = [
58033
58033
  id: "glass",
58034
58034
  label: "Glass",
58035
58035
  description: "Showcase lighting for authored transparent materials."
58036
+ },
58037
+ {
58038
+ id: "precision",
58039
+ label: "Precision",
58040
+ description: "Dense, thin surface-field contours for technical inspection."
58041
+ },
58042
+ {
58043
+ id: "hybrid",
58044
+ label: "Hybrid",
58045
+ description: "Topo-led surface contours with a quiet orthogonal field."
58036
58046
  }
58037
58047
  ];
58038
58048
  var RENDER_STYLE_IDS = new Set(RENDER_STYLE_OPTIONS.map((option) => option.id));
@@ -82694,6 +82704,16 @@ function createForgeRuntimeModule(bindings) {
82694
82704
  return runtime;
82695
82705
  }
82696
82706
 
82707
+ // src/forge/script-runtime/importFinalizer.ts
82708
+ function finalizeForgeJsImport(moduleExports, importedDims) {
82709
+ if (moduleExports instanceof Assembly) {
82710
+ return new ImportedAssembly(moduleExports, moduleExports.getReferences());
82711
+ }
82712
+ if (!(moduleExports instanceof Shape)) return moduleExports;
82713
+ if (importedDims.length === 0) return moduleExports;
82714
+ return setShapeDimensions(moduleExports, [...getShapeDimensions(moduleExports), ...importedDims]);
82715
+ }
82716
+
82697
82717
  // src/forge/sdf/sdfShader.ts
82698
82718
  function classifySdfPreviewNode(node) {
82699
82719
  switch (node.kind) {
@@ -82871,7 +82891,7 @@ function isRenderableEntryResult(value) {
82871
82891
  return containsRenderableLeaf(value, /* @__PURE__ */ new WeakSet());
82872
82892
  }
82873
82893
  function containsRenderableLeaf(value, seen) {
82874
- if (value instanceof Shape || value instanceof Sketch || value instanceof SdfShape || value instanceof ShapeGroup || value instanceof GCodeBuilder || value instanceof Assembly || value instanceof SolvedAssembly) {
82894
+ if (value instanceof Shape || value instanceof Sketch || value instanceof SdfShape || value instanceof ShapeGroup || value instanceof GCodeBuilder || value instanceof Assembly || value instanceof ImportedAssembly || value instanceof SolvedAssembly) {
82875
82895
  return true;
82876
82896
  }
82877
82897
  if (!value || typeof value !== "object") return false;
@@ -82893,11 +82913,6 @@ function hasExplicitModuleExports(exportsValue, initialExportsRef) {
82893
82913
  const keys = Object.keys(exportsValue);
82894
82914
  return keys.some((key) => key !== "__esModule");
82895
82915
  }
82896
- function finalizeForgeJsImport(moduleExports, importedDims) {
82897
- if (!(moduleExports instanceof Shape)) return moduleExports;
82898
- if (importedDims.length === 0) return moduleExports;
82899
- return setShapeDimensions(moduleExports, [...getShapeDimensions(moduleExports), ...importedDims]);
82900
- }
82901
82916
  function mapScriptResultToScene(args) {
82902
82917
  const objects = [];
82903
82918
  const shapeDimensions = [];
@@ -83024,6 +83039,19 @@ function mapScriptResultToScene(args) {
83024
83039
  value.solve().toSceneObjects().forEach((item, index) => processNamedItem(item, `${name}.${index + 1}`, `${index + 1}`, name, treePath, inheritedTags));
83025
83040
  return;
83026
83041
  }
83042
+ if (value instanceof ImportedAssembly) {
83043
+ const group2 = value.toGroup();
83044
+ group2.children.forEach((child, index) => {
83045
+ flattenGroupChild(
83046
+ child,
83047
+ groupChildLabel(group2, name, index),
83048
+ name,
83049
+ [...treePath, shapeGroupChildSegment(group2, index)],
83050
+ mergeSceneTags(inheritedTags, group2.tagsForChild(index))
83051
+ );
83052
+ });
83053
+ return;
83054
+ }
83027
83055
  if (value instanceof SolvedAssembly) {
83028
83056
  value.toSceneObjects().forEach((item, index) => processNamedItem(item, `${name}.${index + 1}`, `${index + 1}`, name, treePath, inheritedTags));
83029
83057
  return;
@@ -83178,6 +83206,12 @@ function mapScriptResultToScene(args) {
83178
83206
  const label = `Object ${index + 1}`;
83179
83207
  processNamedItem(item, label, label);
83180
83208
  });
83209
+ } else if (result instanceof ImportedAssembly) {
83210
+ const group2 = result.toGroup();
83211
+ group2.children.forEach((child, index) => {
83212
+ const label = rootGroupChildLabel(group2, index);
83213
+ flattenGroupChild(child, label, void 0, [label], group2.tagsForChild(index));
83214
+ });
83181
83215
  } else if (result instanceof ShapeGroup) {
83182
83216
  result.children.forEach((child, i) => {
83183
83217
  const label = rootGroupChildLabel(result, i);
@@ -83216,7 +83250,7 @@ function mapScriptResultToScene(args) {
83216
83250
  }
83217
83251
  processRenderableTree(item, label, label);
83218
83252
  });
83219
- } else if (result !== null && typeof result === "object" && !Array.isArray(result) && !(result instanceof Shape) && !(result instanceof Sketch) && !(result instanceof SdfShape) && !(result instanceof ShapeGroup) && !(result instanceof GCodeBuilder) && !(result instanceof Assembly) && !(result instanceof SolvedAssembly)) {
83253
+ } else if (result !== null && typeof result === "object" && !Array.isArray(result) && !(result instanceof Shape) && !(result instanceof Sketch) && !(result instanceof SdfShape) && !(result instanceof ShapeGroup) && !(result instanceof GCodeBuilder) && !(result instanceof Assembly) && !(result instanceof ImportedAssembly) && !(result instanceof SolvedAssembly)) {
83220
83254
  const obj = result;
83221
83255
  const defaultValue = obj.default;
83222
83256
  if (defaultValue && isRenderableEntryResult(defaultValue)) {
@@ -83301,7 +83335,7 @@ Fix one:
83301
83335
  sketch: objects.length === 1 ? objects[0].sketch : null,
83302
83336
  objects,
83303
83337
  extraDimensions: shapeDimensions,
83304
- error: objects.length > 0 || args.allowEmptyResult ? null : "No renderable objects found. Return a Shape, Sketch, SdfShape, array, or object tree containing renderable values."
83338
+ error: objects.length > 0 || args.allowEmptyResult ? null : "No renderable objects found. Return a Shape, Sketch, SdfShape, Assembly, ImportedAssembly, array, or object tree containing renderable values."
83305
83339
  };
83306
83340
  }
83307
83341
 
@@ -83593,6 +83627,7 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
83593
83627
  composeChain,
83594
83628
  assembly,
83595
83629
  Assembly,
83630
+ ImportedAssembly,
83596
83631
  port: () => {
83597
83632
  throw new Error("port() has been renamed to connector(). Update your script.");
83598
83633
  },
@@ -84003,6 +84038,11 @@ import { execSync, spawn } from "child_process";
84003
84038
  import { existsSync, realpathSync } from "fs";
84004
84039
  import { dirname, resolve } from "path";
84005
84040
  import { fileURLToPath } from "url";
84041
+
84042
+ // cli/default-backend.ts
84043
+ var CLI_DEFAULT_BACKEND = "manifold";
84044
+
84045
+ // cli/package-runtime.ts
84006
84046
  function isCompiledBinary() {
84007
84047
  return import.meta.url.includes("$bunfs");
84008
84048
  }
@@ -84040,7 +84080,10 @@ function isDirectSourceCliRun(metaUrl, sourceEntryPath) {
84040
84080
  }
84041
84081
  function runDirectCliMain(metaUrl, sourceEntryPath, run) {
84042
84082
  if (!isDirectSourceCliRun(metaUrl, sourceEntryPath)) return;
84043
- Promise.resolve().then(run).catch((error) => {
84083
+ Promise.resolve().then(() => {
84084
+ setActiveBackend(CLI_DEFAULT_BACKEND);
84085
+ return run();
84086
+ }).catch((error) => {
84044
84087
  const message = error instanceof Error ? error.message : String(error);
84045
84088
  console.error(message);
84046
84089
  process.exit(1);
@@ -86273,7 +86316,15 @@ return [
86273
86316
  ];
86274
86317
  `
86275
86318
  };
86276
- const result = runScript(files["main.forge.js"], "main.forge.js", files);
86319
+ const previousBackend = getActiveBackend();
86320
+ setActiveBackend("truck");
86321
+ let result = null;
86322
+ try {
86323
+ result = runScript(files["main.forge.js"], "main.forge.js", files);
86324
+ } finally {
86325
+ setActiveBackend(previousBackend);
86326
+ }
86327
+ assert2(result, "runScript did not return a result");
86277
86328
  assert2.equal(result.error, null, `runScript failed: ${result.error ?? "unknown error"}`);
86278
86329
  const exactManifest = buildBrepExportManifest(result.objects);
86279
86330
  assert2.equal(exactManifest.objects.length, 0, "Broad native edge finishes must not export as their unfinished base shape");
@@ -89390,14 +89441,36 @@ function listExampleArtifacts() {
89390
89441
  }
89391
89442
 
89392
89443
  // cli/sketch-svg.ts
89393
- function edgesStrokeAttrs(preset) {
89444
+ function paletteForTheme(theme) {
89445
+ if (theme === "cad-section") {
89446
+ return {
89447
+ background: "#f7f7f4",
89448
+ fill: "#e6e8eb",
89449
+ stroke: "#20262e",
89450
+ hatchStroke: "#a8afb7",
89451
+ strokeThin: 0.22,
89452
+ strokeBold: 0.55,
89453
+ useHatch: true
89454
+ };
89455
+ }
89456
+ return {
89457
+ background: "#2a2a2a",
89458
+ fill: "#4488cc",
89459
+ stroke: "#224466",
89460
+ hatchStroke: "#224466",
89461
+ strokeThin: 0.3,
89462
+ strokeBold: 0.8,
89463
+ useHatch: false
89464
+ };
89465
+ }
89466
+ function edgesStrokeAttrs(preset, palette) {
89394
89467
  switch (preset) {
89395
89468
  case "off":
89396
89469
  return 'stroke="none"';
89397
89470
  case "bold":
89398
- return 'stroke="#224466" stroke-width="0.8"';
89471
+ return `stroke="${palette.stroke}" stroke-width="${palette.strokeBold}"`;
89399
89472
  default:
89400
- return 'stroke="#224466" stroke-width="0.3"';
89473
+ return `stroke="${palette.stroke}" stroke-width="${palette.strokeThin}"`;
89401
89474
  }
89402
89475
  }
89403
89476
  function combineBounds(left, right) {
@@ -89412,6 +89485,19 @@ function escapeAttribute(value) {
89412
89485
  function polygonPath(poly) {
89413
89486
  return `${poly.map((point2, index) => `${index === 0 ? "M" : "L"}${point2[0].toFixed(3)},${(-point2[1]).toFixed(3)}`).join(" ")} Z`;
89414
89487
  }
89488
+ function compoundPolygonPath(polygons) {
89489
+ return polygons.map(polygonPath).join(" ");
89490
+ }
89491
+ function hatchDefs(palette) {
89492
+ if (!palette.useHatch) return "";
89493
+ return ` <defs>
89494
+ <pattern id="forge-section-hatch" patternUnits="userSpaceOnUse" width="4" height="4" patternTransform="rotate(45)">
89495
+ <rect width="4" height="4" fill="${palette.fill}"/>
89496
+ <line x1="0" y1="0" x2="0" y2="4" stroke="${palette.hatchStroke}" stroke-width="0.28"/>
89497
+ </pattern>
89498
+ </defs>
89499
+ `;
89500
+ }
89415
89501
  function prepareEntry(entry) {
89416
89502
  const polygons = entry.sketch.toPolygons();
89417
89503
  if (polygons.length === 0) {
@@ -89433,7 +89519,9 @@ function buildSketchSvgDocument(entries, options = {}) {
89433
89519
  throw new Error("Sketch SVG export requires at least one sketch payload.");
89434
89520
  }
89435
89521
  const edges = options.edges ?? "thin";
89436
- const strokeAttrs = edgesStrokeAttrs(edges);
89522
+ const palette = paletteForTheme(options.theme ?? "debug");
89523
+ const strokeAttrs = edgesStrokeAttrs(edges, palette);
89524
+ const fill = palette.useHatch ? "url(#forge-section-hatch)" : palette.fill;
89437
89525
  const prepared = entries.map(prepareEntry);
89438
89526
  const combinedBounds = prepared.reduce((acc, entry) => combineBounds(acc, entry.bounds), prepared[0].bounds);
89439
89527
  const margin = 2;
@@ -89446,17 +89534,15 @@ function buildSketchSvgDocument(entries, options = {}) {
89446
89534
  let pathCount = 0;
89447
89535
  const body = prepared.map((entry) => {
89448
89536
  const label = entry.name ? ` data-name="${escapeAttribute(entry.name)}"` : "";
89449
- const paths = entry.polygons.map((poly) => {
89450
- pathCount += 1;
89451
- return ` <path d="${polygonPath(poly)}" fill="#4488cc" ${strokeAttrs}/>`;
89452
- }).join("\n");
89537
+ pathCount += entry.polygons.length;
89453
89538
  return ` <g${label}>
89454
- ${paths}
89539
+ <path d="${compoundPolygonPath(entry.polygons)}" fill="${fill}" fill-rule="evenodd" ${strokeAttrs}/>
89455
89540
  </g>`;
89456
89541
  }).join("\n");
89457
89542
  return {
89458
89543
  svg: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${minX.toFixed(1)} ${(-maxY).toFixed(1)} ${width.toFixed(1)} ${height.toFixed(1)}" width="${Math.max(width * 4, 400)}" height="${Math.max(height * 4, 400)}">
89459
- <rect x="${minX.toFixed(1)}" y="${(-maxY).toFixed(1)}" width="${width.toFixed(1)}" height="${height.toFixed(1)}" fill="#2a2a2a"/>
89544
+ <rect x="${minX.toFixed(1)}" y="${(-maxY).toFixed(1)}" width="${width.toFixed(1)}" height="${height.toFixed(1)}" fill="${palette.background}"/>
89545
+ ${hatchDefs(palette)}
89460
89546
  ${body}
89461
89547
  </svg>`,
89462
89548
  width,
@@ -90257,9 +90343,11 @@ var INSPECT_CHANNEL_DESCRIPTIONS = {
90257
90343
  rgb: "Canonical viewport RGB renders.",
90258
90344
  depth: "Packed linear view-depth maps.",
90259
90345
  normals: "View-space normal maps.",
90346
+ zebra: "Reflective zebra stripes for surface-continuity inspection.",
90260
90347
  roughness: "Surface roughness and sharp-feature heatmap.",
90261
90348
  mask: "Object identity masks.",
90262
90349
  connectivity: "Physical connected-component masks.",
90350
+ floating: "Disconnected floating body highlights.",
90263
90351
  distance: "Rooted physical-component distance heatmap.",
90264
90352
  collisions: "Ghosted source objects with highlighted intersection volumes.",
90265
90353
  thickness: "Local wall-thickness heatmap.",
@@ -90269,16 +90357,18 @@ var ALL_INSPECT_CHANNELS = [
90269
90357
  "rgb",
90270
90358
  "depth",
90271
90359
  "normals",
90360
+ "zebra",
90272
90361
  "roughness",
90273
90362
  "mask",
90274
90363
  "connectivity",
90364
+ "floating",
90275
90365
  "distance",
90276
90366
  "collisions",
90277
90367
  "thickness",
90278
90368
  "section"
90279
90369
  ];
90280
90370
  var SUPPORTED_INSPECT_CHANNELS = new Set(ALL_INSPECT_CHANNELS);
90281
- var RENDER_STYLES = /* @__PURE__ */ new Set(["classic", "studio", "fast", "glass"]);
90371
+ var RENDER_STYLES = /* @__PURE__ */ new Set(["classic", "studio", "fast", "glass", "precision", "hybrid"]);
90282
90372
  function resolveChromePath(explicitPath) {
90283
90373
  if (explicitPath && existsSync5(explicitPath)) return explicitPath;
90284
90374
  return CHROME_PATHS.find((p2) => existsSync5(p2)) || null;
@@ -90303,7 +90393,7 @@ Options:
90303
90393
  --focus [names] Focus: no arg hides mocks; comma-separated names/globs show only those
90304
90394
  --hide <names> Hide comma-separated object names/globs, show everything else
90305
90395
  --background <color> Canvas background override
90306
- --render-style <classic|studio|fast|glass>
90396
+ --render-style <classic|studio|fast|glass|precision|hybrid>
90307
90397
  Visual render style (default: classic)
90308
90398
  --render-mode <solid|wireframe> Render as shaded solid (default) or wireframe only
90309
90399
  --edges <off|thin|bold> Edge overlay preset for solid mode (default: off)
@@ -90340,14 +90430,16 @@ function splitCsv(value) {
90340
90430
  }
90341
90431
  function parseRenderStyle(value) {
90342
90432
  if (RENDER_STYLES.has(value)) return value;
90343
- throw new Error(`--render-style must be 'classic', 'studio', 'fast', or 'glass' (got '${value}')`);
90433
+ throw new Error(`--render-style must be 'classic', 'studio', 'fast', 'glass', 'precision', or 'hybrid' (got '${value}')`);
90344
90434
  }
90345
90435
  function sceneSpecHasCamera(sceneSpec) {
90346
90436
  let parsed;
90347
90437
  try {
90348
90438
  parsed = JSON.parse(sceneSpec);
90349
90439
  } catch (err2) {
90350
- throw new Error(`--scene must be valid JSON when combined with --camera or --view: ${err2 instanceof Error ? err2.message : String(err2)}`);
90440
+ throw new Error(
90441
+ `--scene must be valid JSON when combined with --camera or --view: ${err2 instanceof Error ? err2.message : String(err2)}`
90442
+ );
90351
90443
  }
90352
90444
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) && Object.prototype.hasOwnProperty.call(parsed, "camera");
90353
90445
  }
@@ -90530,7 +90622,9 @@ function parseRenderCliOptions(argv) {
90530
90622
  throw new Error("Cannot use --view with --scene JSON that includes camera. Remove the camera field from --scene or use --camera.");
90531
90623
  }
90532
90624
  if ((cameraSpec || cameras.length > 0) && sceneHasCamera) {
90533
- throw new Error("Cannot use --camera with --scene JSON that includes camera. Remove the camera field from --scene or use --scene by itself.");
90625
+ throw new Error(
90626
+ "Cannot use --camera with --scene JSON that includes camera. Remove the camera field from --scene or use --scene by itself."
90627
+ );
90534
90628
  }
90535
90629
  if (cameras.length === 0 && !cameraSpec && !sceneSpec && !viewName) {
90536
90630
  cameras.push("iso");
@@ -90572,8 +90666,8 @@ Options:
90572
90666
  --min-thickness <mm> Critical thickness threshold (default: 1.2)
90573
90667
  --warn-thickness <mm> Warning thickness threshold (default: 2.0)
90574
90668
  --max-thickness <mm> Thick/blue heatmap threshold (default: 6.0)
90575
- --thickness-samples <n> Max thickness point samples per object (default: 5000)
90576
- --roughness-samples <n> Max roughness point samples per object (default: 5000)
90669
+ --thickness-samples <n> Max thickness point samples per object (overrides the default scene budget)
90670
+ --roughness-samples <n> Max roughness point samples per object (overrides the default scene budget)
90577
90671
  --size <px> Image size in pixels (default: ${DEFAULT_SIZE})
90578
90672
  --quality <default|live|high> Mesh/render quality (default: default)
90579
90673
  --force Replace an existing bundle directory
@@ -91014,6 +91108,16 @@ function buildInspectManifest({ options, result, scriptPath, projectRoot, emitte
91014
91108
  views: buildViewPathEntries(emittedPaths.normals)
91015
91109
  };
91016
91110
  }
91111
+ if (emittedPaths.zebra) {
91112
+ channels.zebra = {
91113
+ format: "png",
91114
+ encoding: "rgb8-reflected-zebra-stripes",
91115
+ coordinateSpace: "camera-view",
91116
+ background: { rgb: [0, 0, 0], stripe: null },
91117
+ decode: "procedural reflected stripe visualization from camera-view normals; use visually to spot surface-continuity breaks",
91118
+ views: buildViewPathEntries(emittedPaths.zebra)
91119
+ };
91120
+ }
91017
91121
  if (emittedPaths.roughness) {
91018
91122
  channels.roughness = {
91019
91123
  format: "png",
@@ -91023,6 +91127,7 @@ function buildInspectManifest({ options, result, scriptPath, projectRoot, emitte
91023
91127
  decode: "smooth and moderate triangles render as a translucent shadow; colored regions mark triangles adjacent to sharp, harsh, boundary, or non-manifold mesh edges",
91024
91128
  method: result.roughness?.method,
91025
91129
  options: result.roughness?.options,
91130
+ sampleBudget: result.roughness?.sampleBudget,
91026
91131
  style: result.roughness?.style,
91027
91132
  objectCount: result.roughness?.objectCount ?? 0,
91028
91133
  objects: result.roughness?.objects ?? [],
@@ -91066,6 +91171,28 @@ function buildInspectManifest({ options, result, scriptPath, projectRoot, emitte
91066
91171
  views: buildViewPathEntries(emittedPaths.connectivity)
91067
91172
  };
91068
91173
  }
91174
+ if (emittedPaths.floating) {
91175
+ channels.floating = {
91176
+ format: "png",
91177
+ encoding: "rgb8-floating-body-highlight",
91178
+ backgroundColor: [0, 0, 0],
91179
+ decode: "only bodies without a mesh-contact path to the ground plane render in the highlight color; black means background or ground-reachable geometry",
91180
+ method: result.floating?.method,
91181
+ options: result.floating?.options,
91182
+ rootComponentIndex: result.floating?.rootComponentIndex ?? null,
91183
+ style: result.floating?.style,
91184
+ objectCount: result.floating?.objectCount ?? 0,
91185
+ componentCount: result.floating?.componentCount ?? 0,
91186
+ floatingComponentCount: result.floating?.floatingComponentCount ?? 0,
91187
+ floatingObjectCount: result.floating?.floatingObjectCount ?? 0,
91188
+ floatingBodyCount: result.floating?.floatingBodyCount ?? 0,
91189
+ components: result.floating?.components ?? [],
91190
+ objects: result.floating?.objects ?? [],
91191
+ contacts: result.floating?.contacts ?? [],
91192
+ warnings: result.floating?.warnings ?? [],
91193
+ views: buildViewPathEntries(emittedPaths.floating)
91194
+ };
91195
+ }
91069
91196
  if (emittedPaths.distance) {
91070
91197
  channels.distance = {
91071
91198
  format: "png",
@@ -91099,6 +91226,13 @@ function buildInspectManifest({ options, result, scriptPath, projectRoot, emitte
91099
91226
  method: result.collisions?.method,
91100
91227
  options: result.collisions?.options,
91101
91228
  broadphase: result.collisions?.broadphase,
91229
+ candidatePairCount: result.collisions?.candidatePairCount ?? 0,
91230
+ bboxVolumePrunedPairCount: result.collisions?.bboxVolumePrunedPairCount ?? 0,
91231
+ testedPairCount: result.collisions?.testedPairCount ?? 0,
91232
+ skippedPairCount: result.collisions?.skippedPairCount ?? 0,
91233
+ pairLimitSkippedPairCount: result.collisions?.pairLimitSkippedPairCount ?? 0,
91234
+ timeBudgetSkippedPairCount: result.collisions?.timeBudgetSkippedPairCount ?? 0,
91235
+ exactCheckMs: result.collisions?.exactCheckMs ?? 0,
91102
91236
  style: result.collisions?.style,
91103
91237
  objectCount: result.collisions?.objectCount ?? 0,
91104
91238
  collisionCount: result.collisions?.collisionCount ?? 0,
@@ -91114,9 +91248,10 @@ function buildInspectManifest({ options, result, scriptPath, projectRoot, emitte
91114
91248
  encoding: "rgb8-thickness-heatmap",
91115
91249
  units: "model",
91116
91250
  backgroundColor: [0, 0, 0],
91117
- decode: "surface heatmap from mesh-normal raycast thickness; red <= minThickness, orange <= warnThickness, green-to-blue through maxThickness, gray unresolved",
91251
+ decode: "surface heatmap from contact-aware mesh-normal raycast thickness; red <= minThickness, orange <= warnThickness, green-to-blue through maxThickness, gray unresolved",
91118
91252
  method: result.thickness?.method,
91119
91253
  options: result.thickness?.options,
91254
+ sampleBudget: result.thickness?.sampleBudget,
91120
91255
  style: result.thickness?.style,
91121
91256
  objectCount: result.thickness?.objectCount ?? 0,
91122
91257
  objects: result.thickness?.objects ?? [],
@@ -91536,10 +91671,10 @@ async function runRenderInspectCli(argv = process.argv.slice(2)) {
91536
91671
  }
91537
91672
  console.log(`[inspect] Wrote ${channel} channel files.`);
91538
91673
  };
91539
- const writeChannelJson = async (channel, filename, source) => {
91674
+ const writeChannelJson = async (channel, filename, source, { pretty = true } = {}) => {
91540
91675
  if (!source) return null;
91541
91676
  const relPath = toPortablePath(join5("channels", channel, filename));
91542
- await writeFile(join5(tempDir, relPath), `${JSON.stringify(source, null, 2)}
91677
+ await writeFile(join5(tempDir, relPath), `${pretty ? JSON.stringify(source, null, 2) : JSON.stringify(source)}
91543
91678
  `, "utf-8");
91544
91679
  return relPath;
91545
91680
  };
@@ -91552,9 +91687,14 @@ async function runRenderInspectCli(argv = process.argv.slice(2)) {
91552
91687
  if (requested.has("normals")) {
91553
91688
  await writeViewChannel("normals", result.normals);
91554
91689
  }
91690
+ if (requested.has("zebra")) {
91691
+ await writeViewChannel("zebra", result.zebra);
91692
+ }
91555
91693
  if (requested.has("roughness")) {
91556
91694
  await writeViewChannel("roughness", result.roughness?.views);
91557
- emittedPaths.roughnessPointCloud = await writeChannelJson("roughness", "point-cloud.json", result.roughness?.pointCloud);
91695
+ emittedPaths.roughnessPointCloud = await writeChannelJson("roughness", "point-cloud.json", result.roughness?.pointCloud, {
91696
+ pretty: false
91697
+ });
91558
91698
  }
91559
91699
  if (requested.has("mask")) {
91560
91700
  await writeViewChannel("mask", result.mask?.views);
@@ -91562,6 +91702,9 @@ async function runRenderInspectCli(argv = process.argv.slice(2)) {
91562
91702
  if (requested.has("connectivity")) {
91563
91703
  await writeViewChannel("connectivity", result.connectivity?.views);
91564
91704
  }
91705
+ if (requested.has("floating")) {
91706
+ await writeViewChannel("floating", result.floating?.views);
91707
+ }
91565
91708
  if (requested.has("distance")) {
91566
91709
  await writeViewChannel("distance", result.distance?.views);
91567
91710
  }
@@ -91570,7 +91713,9 @@ async function runRenderInspectCli(argv = process.argv.slice(2)) {
91570
91713
  }
91571
91714
  if (requested.has("thickness")) {
91572
91715
  await writeViewChannel("thickness", result.thickness?.views);
91573
- emittedPaths.thicknessPointCloud = await writeChannelJson("thickness", "point-cloud.json", result.thickness?.pointCloud);
91716
+ emittedPaths.thicknessPointCloud = await writeChannelJson("thickness", "point-cloud.json", result.thickness?.pointCloud, {
91717
+ pretty: false
91718
+ });
91574
91719
  }
91575
91720
  if (requested.has("section")) {
91576
91721
  console.log("[inspect] Writing section channel files...");
@@ -92783,10 +92928,16 @@ function analyzeCollisionIntersections(entries, rawOptions = {}) {
92783
92928
  const collisions = [];
92784
92929
  const preparedEntries = entries.map((entry) => prepareEntry2(entry));
92785
92930
  const { pairs: candidatePairs, bboxPairChecks } = collectCandidatePairs(preparedEntries, options);
92786
- const limitedCandidatePairs = options.maxCandidatePairs === null ? candidatePairs : candidatePairs.slice(0, options.maxCandidatePairs);
92787
- const pairLimitSkippedPairCount = candidatePairs.length - limitedCandidatePairs.length;
92931
+ const exactCandidatePairs = candidatePairs.filter((pair) => pair.bboxOverlapVolume > options.minOverlapVolume);
92932
+ const bboxVolumePrunedPairCount = candidatePairs.length - exactCandidatePairs.length;
92933
+ const limitedCandidatePairs = options.maxCandidatePairs === null ? exactCandidatePairs : exactCandidatePairs.slice(0, options.maxCandidatePairs);
92934
+ const pairLimitSkippedPairCount = exactCandidatePairs.length - limitedCandidatePairs.length;
92788
92935
  let testedPairCount = 0;
92789
92936
  let timeBudgetStopped = false;
92937
+ const exactCheckedKeys = /* @__PURE__ */ new Set();
92938
+ const pairKey2 = (pair) => `${pair.sourceIndex}:${pair.targetIndex}`;
92939
+ const exactCandidateKeys = new Set(exactCandidatePairs.map(pairKey2));
92940
+ const limitedCandidateKeys = new Set(limitedCandidatePairs.map(pairKey2));
92790
92941
  const exactStarted = performance.now();
92791
92942
  for (const pair of limitedCandidatePairs) {
92792
92943
  if (options.maxElapsedMs !== null && (options.maxElapsedMs <= 0 || performance.now() - exactStarted >= options.maxElapsedMs)) {
@@ -92794,6 +92945,7 @@ function analyzeCollisionIntersections(entries, rawOptions = {}) {
92794
92945
  break;
92795
92946
  }
92796
92947
  testedPairCount += 1;
92948
+ exactCheckedKeys.add(pairKey2(pair));
92797
92949
  const a = preparedEntries[pair.sourceIndex];
92798
92950
  const b = preparedEntries[pair.targetIndex];
92799
92951
  try {
@@ -92824,10 +92976,11 @@ function analyzeCollisionIntersections(entries, rawOptions = {}) {
92824
92976
  const bboxCandidates = options.includeBBoxCandidates ? candidatePairs.map((pair, index) => {
92825
92977
  const source = preparedEntries[pair.sourceIndex];
92826
92978
  const target = preparedEntries[pair.targetIndex];
92827
- const exactChecked = index < testedPairCount;
92979
+ const exactChecked = exactCheckedKeys.has(pairKey2(pair));
92828
92980
  let skippedBy;
92829
92981
  if (!exactChecked) {
92830
- skippedBy = index >= limitedCandidatePairs.length ? "pair-limit" : "time-budget";
92982
+ const key = pairKey2(pair);
92983
+ skippedBy = !exactCandidateKeys.has(key) ? "bbox-volume-threshold" : limitedCandidateKeys.has(key) ? "time-budget" : "pair-limit";
92831
92984
  }
92832
92985
  return {
92833
92986
  index: index + 1,
@@ -92871,10 +93024,12 @@ function analyzeCollisionIntersections(entries, rawOptions = {}) {
92871
93024
  method: "aabb-bvh",
92872
93025
  allPairCount: preparedEntries.length * (preparedEntries.length - 1) / 2,
92873
93026
  bboxPairChecks,
93027
+ bboxVolumePrunedPairCount,
92874
93028
  booleanPairChecks: testedPairCount
92875
93029
  },
92876
93030
  objectCount: objects.length,
92877
93031
  candidatePairCount: candidatePairs.length,
93032
+ bboxVolumePrunedPairCount,
92878
93033
  testedPairCount,
92879
93034
  skippedPairCount,
92880
93035
  pairLimitSkippedPairCount,
@@ -92888,13 +93043,335 @@ function analyzeCollisionIntersections(entries, rawOptions = {}) {
92888
93043
  };
92889
93044
  }
92890
93045
 
93046
+ // src/forge/inspection/contact.ts
93047
+ var AXIS_NAMES = ["x", "y", "z"];
93048
+ function nearestBoundaryGap(a, b, axis) {
93049
+ return Math.min(Math.abs(a.max[axis] - b.min[axis]), Math.abs(b.max[axis] - a.min[axis]));
93050
+ }
93051
+ function hasPositiveGap(gaps) {
93052
+ return gaps[0] > 0 || gaps[1] > 0 || gaps[2] > 0;
93053
+ }
93054
+ function contactFromBBoxes(a, b, tolerance) {
93055
+ const gaps = aabbGaps(a, b);
93056
+ const largestGap = Math.max(gaps[0], gaps[1], gaps[2]);
93057
+ if (largestGap > tolerance) return { touching: false, gap: largestGap };
93058
+ const separatedAxes = gaps.map((gap, axis) => ({ gap, axis })).filter((entry) => entry.gap > 0);
93059
+ if (separatedAxes.length > 0) {
93060
+ const nearest2 = separatedAxes.reduce((best, entry) => entry.gap > best.gap ? entry : best, separatedAxes[0]);
93061
+ return { touching: true, gap: nearest2.gap, axis: AXIS_NAMES[nearest2.axis] };
93062
+ }
93063
+ const boundaryAxes = AXIS_NAMES.map((axisName, axis) => ({
93064
+ axis,
93065
+ axisName,
93066
+ gap: nearestBoundaryGap(a, b, axis)
93067
+ })).filter((entry) => entry.gap <= tolerance);
93068
+ if (boundaryAxes.length === 0) return { touching: false, gap: 0 };
93069
+ const nearest = boundaryAxes.reduce((best, entry) => entry.gap < best.gap ? entry : best, boundaryAxes[0]);
93070
+ return { touching: true, gap: nearest.gap, axis: nearest.axisName };
93071
+ }
93072
+ function isFiniteTriangle(positions, offset2) {
93073
+ for (let index = 0; index < 9; index += 1) {
93074
+ if (!Number.isFinite(positions[offset2 + index])) return false;
93075
+ }
93076
+ return true;
93077
+ }
93078
+ function triangleRef(positions, offset2) {
93079
+ if (!isFiniteTriangle(positions, offset2)) return null;
93080
+ const ax = positions[offset2];
93081
+ const ay = positions[offset2 + 1];
93082
+ const az = positions[offset2 + 2];
93083
+ const bx = positions[offset2 + 3];
93084
+ const by = positions[offset2 + 4];
93085
+ const bz = positions[offset2 + 5];
93086
+ const cx = positions[offset2 + 6];
93087
+ const cy = positions[offset2 + 7];
93088
+ const cz = positions[offset2 + 8];
93089
+ return {
93090
+ offset: offset2,
93091
+ min: [Math.min(ax, bx, cx), Math.min(ay, by, cy), Math.min(az, bz, cz)],
93092
+ max: [Math.max(ax, bx, cx), Math.max(ay, by, cy), Math.max(az, bz, cz)]
93093
+ };
93094
+ }
93095
+ function meshContactDataFor(entry) {
93096
+ const positions = entry.positions;
93097
+ if (!positions || positions.length < 9) return null;
93098
+ const triangleCount = Math.floor(positions.length / 9);
93099
+ const triangles = [];
93100
+ for (let triangleIndex = 0; triangleIndex < triangleCount; triangleIndex += 1) {
93101
+ const triangle = triangleRef(positions, triangleIndex * 9);
93102
+ if (triangle) triangles.push(triangle);
93103
+ }
93104
+ if (triangles.length === 0) return null;
93105
+ return {
93106
+ positions,
93107
+ vertexCount: triangleCount * 3,
93108
+ triangles
93109
+ };
93110
+ }
93111
+ function distSq2(px, py, pz, qx, qy, qz) {
93112
+ const dx = px - qx;
93113
+ const dy = py - qy;
93114
+ const dz = pz - qz;
93115
+ return dx * dx + dy * dy + dz * dz;
93116
+ }
93117
+ function pointSegmentDistanceSq(point2, start, end) {
93118
+ const dx = end[0] - start[0];
93119
+ const dy = end[1] - start[1];
93120
+ const dz = end[2] - start[2];
93121
+ const lengthSq = dx * dx + dy * dy + dz * dz;
93122
+ if (lengthSq <= 1e-20) return distSq2(point2[0], point2[1], point2[2], start[0], start[1], start[2]);
93123
+ const t = Math.max(0, Math.min(1, ((point2[0] - start[0]) * dx + (point2[1] - start[1]) * dy + (point2[2] - start[2]) * dz) / lengthSq));
93124
+ return distSq2(point2[0], point2[1], point2[2], start[0] + t * dx, start[1] + t * dy, start[2] + t * dz);
93125
+ }
93126
+ function pointTriangleDistanceSq(px, py, pz, ax, ay, az, bx, by, bz, cx, cy, cz) {
93127
+ const abx = bx - ax;
93128
+ const aby = by - ay;
93129
+ const abz = bz - az;
93130
+ const acx = cx - ax;
93131
+ const acy = cy - ay;
93132
+ const acz = cz - az;
93133
+ const apx = px - ax;
93134
+ const apy = py - ay;
93135
+ const apz = pz - az;
93136
+ const d1 = abx * apx + aby * apy + abz * apz;
93137
+ const d2 = acx * apx + acy * apy + acz * apz;
93138
+ if (d1 <= 0 && d2 <= 0) return distSq2(px, py, pz, ax, ay, az);
93139
+ const bpx = px - bx;
93140
+ const bpy = py - by;
93141
+ const bpz = pz - bz;
93142
+ const d3 = abx * bpx + aby * bpy + abz * bpz;
93143
+ const d4 = acx * bpx + acy * bpy + acz * bpz;
93144
+ if (d3 >= 0 && d4 <= d3) return distSq2(px, py, pz, bx, by, bz);
93145
+ const vc = d1 * d4 - d3 * d2;
93146
+ if (vc <= 0 && d1 >= 0 && d3 <= 0) {
93147
+ const v2 = d1 / (d1 - d3);
93148
+ return distSq2(px, py, pz, ax + v2 * abx, ay + v2 * aby, az + v2 * abz);
93149
+ }
93150
+ const cpx = px - cx;
93151
+ const cpy = py - cy;
93152
+ const cpz = pz - cz;
93153
+ const d5 = abx * cpx + aby * cpy + abz * cpz;
93154
+ const d6 = acx * cpx + acy * cpy + acz * cpz;
93155
+ if (d6 >= 0 && d5 <= d6) return distSq2(px, py, pz, cx, cy, cz);
93156
+ const vb = d5 * d2 - d1 * d6;
93157
+ if (vb <= 0 && d2 >= 0 && d6 <= 0) {
93158
+ const w2 = d2 / (d2 - d6);
93159
+ return distSq2(px, py, pz, ax + w2 * acx, ay + w2 * acy, az + w2 * acz);
93160
+ }
93161
+ const va = d3 * d6 - d5 * d4;
93162
+ if (va <= 0 && d4 - d3 >= 0 && d5 - d6 >= 0) {
93163
+ const bcx = cx - bx;
93164
+ const bcy = cy - by;
93165
+ const bcz = cz - bz;
93166
+ const w2 = (d4 - d3) / (d4 - d3 + d5 - d6);
93167
+ return distSq2(px, py, pz, bx + w2 * bcx, by + w2 * bcy, bz + w2 * bcz);
93168
+ }
93169
+ const barycentricTotal = va + vb + vc;
93170
+ if (!Number.isFinite(barycentricTotal) || Math.abs(barycentricTotal) < 1e-20) {
93171
+ return Math.min(distSq2(px, py, pz, ax, ay, az), distSq2(px, py, pz, bx, by, bz), distSq2(px, py, pz, cx, cy, cz));
93172
+ }
93173
+ const denom = 1 / barycentricTotal;
93174
+ const v = vb * denom;
93175
+ const w = vc * denom;
93176
+ return distSq2(px, py, pz, ax + abx * v + acx * w, ay + aby * v + acy * w, az + abz * v + acz * w);
93177
+ }
93178
+ function segmentSegmentDistanceSq(a0, a1, b0, b1) {
93179
+ const ux = a1[0] - a0[0];
93180
+ const uy = a1[1] - a0[1];
93181
+ const uz = a1[2] - a0[2];
93182
+ const vx = b1[0] - b0[0];
93183
+ const vy = b1[1] - b0[1];
93184
+ const vz = b1[2] - b0[2];
93185
+ const wx = a0[0] - b0[0];
93186
+ const wy = a0[1] - b0[1];
93187
+ const wz = a0[2] - b0[2];
93188
+ const a = ux * ux + uy * uy + uz * uz;
93189
+ const b = ux * vx + uy * vy + uz * vz;
93190
+ const c = vx * vx + vy * vy + vz * vz;
93191
+ const d = ux * wx + uy * wy + uz * wz;
93192
+ const e = vx * wx + vy * wy + vz * wz;
93193
+ const denom = a * c - b * b;
93194
+ let sNumerator = denom;
93195
+ let sDenominator = denom;
93196
+ let tNumerator = denom;
93197
+ let tDenominator = denom;
93198
+ if (a <= 1e-20) return pointSegmentDistanceSq(a0, b0, b1);
93199
+ if (c <= 1e-20) return pointSegmentDistanceSq(b0, a0, a1);
93200
+ if (denom < 1e-20) {
93201
+ sNumerator = 0;
93202
+ sDenominator = 1;
93203
+ tNumerator = e;
93204
+ tDenominator = c;
93205
+ } else {
93206
+ sNumerator = b * e - c * d;
93207
+ tNumerator = a * e - b * d;
93208
+ if (sNumerator < 0) {
93209
+ sNumerator = 0;
93210
+ tNumerator = e;
93211
+ tDenominator = c;
93212
+ } else if (sNumerator > sDenominator) {
93213
+ sNumerator = sDenominator;
93214
+ tNumerator = e + b;
93215
+ tDenominator = c;
93216
+ }
93217
+ }
93218
+ if (tNumerator < 0) {
93219
+ tNumerator = 0;
93220
+ if (-d < 0) {
93221
+ sNumerator = 0;
93222
+ } else if (-d > a) {
93223
+ sNumerator = sDenominator;
93224
+ } else {
93225
+ sNumerator = -d;
93226
+ sDenominator = a;
93227
+ }
93228
+ } else if (tNumerator > tDenominator) {
93229
+ tNumerator = tDenominator;
93230
+ if (-d + b < 0) {
93231
+ sNumerator = 0;
93232
+ } else if (-d + b > a) {
93233
+ sNumerator = sDenominator;
93234
+ } else {
93235
+ sNumerator = -d + b;
93236
+ sDenominator = a;
93237
+ }
93238
+ }
93239
+ const s = Math.abs(sNumerator) < 1e-20 ? 0 : sNumerator / sDenominator;
93240
+ const t = Math.abs(tNumerator) < 1e-20 ? 0 : tNumerator / tDenominator;
93241
+ const dx = wx + s * ux - t * vx;
93242
+ const dy = wy + s * uy - t * vy;
93243
+ const dz = wz + s * uz - t * vz;
93244
+ return dx * dx + dy * dy + dz * dz;
93245
+ }
93246
+ function trianglePoint(positions, triangle, corner) {
93247
+ const offset2 = triangle.offset + corner * 3;
93248
+ return [positions[offset2], positions[offset2 + 1], positions[offset2 + 2]];
93249
+ }
93250
+ function bboxDistanceSq(a, b) {
93251
+ let total = 0;
93252
+ for (let axis = 0; axis < 3; axis += 1) {
93253
+ const gap = Math.max(0, a.min[axis] - b.max[axis], b.min[axis] - a.max[axis]);
93254
+ total += gap * gap;
93255
+ }
93256
+ return total;
93257
+ }
93258
+ function triangleDistanceSq(aPositions, a, bPositions, b) {
93259
+ const a0 = trianglePoint(aPositions, a, 0);
93260
+ const a1 = trianglePoint(aPositions, a, 1);
93261
+ const a2 = trianglePoint(aPositions, a, 2);
93262
+ const b0 = trianglePoint(bPositions, b, 0);
93263
+ const b1 = trianglePoint(bPositions, b, 1);
93264
+ const b22 = trianglePoint(bPositions, b, 2);
93265
+ return Math.min(
93266
+ pointTriangleDistanceSq(a0[0], a0[1], a0[2], b0[0], b0[1], b0[2], b1[0], b1[1], b1[2], b22[0], b22[1], b22[2]),
93267
+ pointTriangleDistanceSq(a1[0], a1[1], a1[2], b0[0], b0[1], b0[2], b1[0], b1[1], b1[2], b22[0], b22[1], b22[2]),
93268
+ pointTriangleDistanceSq(a2[0], a2[1], a2[2], b0[0], b0[1], b0[2], b1[0], b1[1], b1[2], b22[0], b22[1], b22[2]),
93269
+ pointTriangleDistanceSq(b0[0], b0[1], b0[2], a0[0], a0[1], a0[2], a1[0], a1[1], a1[2], a2[0], a2[1], a2[2]),
93270
+ pointTriangleDistanceSq(b1[0], b1[1], b1[2], a0[0], a0[1], a0[2], a1[0], a1[1], a1[2], a2[0], a2[1], a2[2]),
93271
+ pointTriangleDistanceSq(b22[0], b22[1], b22[2], a0[0], a0[1], a0[2], a1[0], a1[1], a1[2], a2[0], a2[1], a2[2]),
93272
+ segmentSegmentDistanceSq(a0, a1, b0, b1),
93273
+ segmentSegmentDistanceSq(a0, a1, b1, b22),
93274
+ segmentSegmentDistanceSq(a0, a1, b22, b0),
93275
+ segmentSegmentDistanceSq(a1, a2, b0, b1),
93276
+ segmentSegmentDistanceSq(a1, a2, b1, b22),
93277
+ segmentSegmentDistanceSq(a1, a2, b22, b0),
93278
+ segmentSegmentDistanceSq(a2, a0, b0, b1),
93279
+ segmentSegmentDistanceSq(a2, a0, b1, b22),
93280
+ segmentSegmentDistanceSq(a2, a0, b22, b0)
93281
+ );
93282
+ }
93283
+ function meshEntriesContactDistance(a, b, tolerance) {
93284
+ const toleranceSq = tolerance * tolerance;
93285
+ let bestDistanceSq = Infinity;
93286
+ for (const aTriangle of a.triangles) {
93287
+ for (const bTriangle of b.triangles) {
93288
+ if (bboxDistanceSq(aTriangle, bTriangle) > toleranceSq) continue;
93289
+ const distanceSq = triangleDistanceSq(a.positions, aTriangle, b.positions, bTriangle);
93290
+ if (distanceSq > toleranceSq) continue;
93291
+ if (distanceSq <= 1e-20) return 0;
93292
+ bestDistanceSq = Math.min(bestDistanceSq, distanceSq);
93293
+ }
93294
+ }
93295
+ return Number.isFinite(bestDistanceSq) ? Math.sqrt(bestDistanceSq) : null;
93296
+ }
93297
+ function intersectionVolume(a, b) {
93298
+ try {
93299
+ const hit = a.shape.intersect(b.shape);
93300
+ if (hit.isEmpty()) return { volume: 0 };
93301
+ const volume = hit.volume();
93302
+ return { volume: Number.isFinite(volume) ? volume : 0 };
93303
+ } catch (err2) {
93304
+ const message = err2 instanceof Error ? err2.message : String(err2);
93305
+ return { volume: null, warning: `Could not boolean-test ${a.name} against ${b.name}: ${message}` };
93306
+ }
93307
+ }
93308
+ function detectPhysicalContact(a, b, options, meshCache = {}) {
93309
+ const gaps = aabbGaps(a, b);
93310
+ const largestGap = Math.max(gaps[0], gaps[1], gaps[2]);
93311
+ if (largestGap > options.contactTolerance) return { contact: null };
93312
+ const sourceMesh = meshCache.sourceMesh !== void 0 ? meshCache.sourceMesh : meshContactDataFor(a);
93313
+ const targetMesh = meshCache.targetMesh !== void 0 ? meshCache.targetMesh : meshContactDataFor(b);
93314
+ if (sourceMesh && targetMesh) {
93315
+ const distance5 = meshEntriesContactDistance(sourceMesh, targetMesh, options.contactTolerance);
93316
+ if (distance5 != null) {
93317
+ return {
93318
+ contact: {
93319
+ kind: "touching",
93320
+ method: "mesh-surface-distance",
93321
+ gap: distance5
93322
+ }
93323
+ };
93324
+ }
93325
+ }
93326
+ const bboxOverlaps = !hasPositiveGap(gaps) && aabbInteriorOverlaps(a, b);
93327
+ if (options.exactGeometry && bboxOverlaps) {
93328
+ if (aabbOverlapVolume(a, b) <= options.minOverlapVolume) return { contact: null };
93329
+ const overlap = intersectionVolume(a, b);
93330
+ if (overlap.volume != null && overlap.volume > options.minOverlapVolume) {
93331
+ return {
93332
+ contact: {
93333
+ kind: "overlap",
93334
+ method: "boolean-intersection",
93335
+ gap: 0,
93336
+ overlapVolume: overlap.volume
93337
+ },
93338
+ warning: overlap.warning
93339
+ };
93340
+ }
93341
+ return { contact: null, warning: overlap.warning };
93342
+ }
93343
+ if (bboxOverlaps && options.mergeOverlappingBBoxes) {
93344
+ return {
93345
+ contact: {
93346
+ kind: "overlap",
93347
+ method: "bbox-overlap",
93348
+ gap: 0,
93349
+ overlapVolume: aabbOverlapVolume(a, b)
93350
+ }
93351
+ };
93352
+ }
93353
+ if (options.mergeTouchingBBoxes) {
93354
+ const contact = contactFromBBoxes(a, b, options.contactTolerance);
93355
+ if (contact.touching) {
93356
+ return {
93357
+ contact: {
93358
+ kind: "touching",
93359
+ method: "bbox-contact",
93360
+ gap: contact.gap,
93361
+ axis: contact.axis
93362
+ }
93363
+ };
93364
+ }
93365
+ }
93366
+ return { contact: null };
93367
+ }
93368
+
92891
93369
  // src/forge/inspection/physical-connectivity.ts
92892
93370
  var DEFAULT_PHYSICAL_CONNECTIVITY_OPTIONS = {
92893
93371
  contactTolerance: 0.05,
92894
93372
  minOverlapVolume: 0.1,
92895
93373
  exactGeometry: true
92896
93374
  };
92897
- var AXIS_NAMES = ["x", "y", "z"];
92898
93375
  var UnionFind2 = class {
92899
93376
  parent;
92900
93377
  rank;
@@ -92940,12 +93417,6 @@ function expandBBox(target, min2, max4) {
92940
93417
  target.max[axis] = Math.max(target.max[axis], max4[axis]);
92941
93418
  }
92942
93419
  }
92943
- function nearestBoundaryGap(a, b, axis) {
92944
- return Math.min(Math.abs(a.max[axis] - b.min[axis]), Math.abs(b.max[axis] - a.min[axis]));
92945
- }
92946
- function hasPositiveGap(gaps) {
92947
- return gaps[0] > 0 || gaps[1] > 0 || gaps[2] > 0;
92948
- }
92949
93420
  function collectCandidatePairs2(entries, tolerance) {
92950
93421
  if (entries.length < 2) return [];
92951
93422
  const index = new AabbSpatialIndex(entries);
@@ -92957,35 +93428,6 @@ function collectCandidatePairs2(entries, tolerance) {
92957
93428
  pairs.sort((a, b) => a.sourceIndex - b.sourceIndex || a.targetIndex - b.targetIndex);
92958
93429
  return pairs;
92959
93430
  }
92960
- function contactFromBBoxes(a, b, tolerance) {
92961
- const gaps = aabbGaps(a, b);
92962
- const largestGap = Math.max(gaps[0], gaps[1], gaps[2]);
92963
- if (largestGap > tolerance) return { touching: false, gap: largestGap };
92964
- const separatedAxes = gaps.map((gap, axis) => ({ gap, axis })).filter((entry) => entry.gap > 0);
92965
- if (separatedAxes.length > 0) {
92966
- const nearest2 = separatedAxes.reduce((best, entry) => entry.gap > best.gap ? entry : best, separatedAxes[0]);
92967
- return { touching: true, gap: nearest2.gap, axis: AXIS_NAMES[nearest2.axis] };
92968
- }
92969
- const boundaryAxes = AXIS_NAMES.map((axisName, axis) => ({
92970
- axis,
92971
- axisName,
92972
- gap: nearestBoundaryGap(a, b, axis)
92973
- })).filter((entry) => entry.gap <= tolerance);
92974
- if (boundaryAxes.length === 0) return { touching: false, gap: 0 };
92975
- const nearest = boundaryAxes.reduce((best, entry) => entry.gap < best.gap ? entry : best, boundaryAxes[0]);
92976
- return { touching: true, gap: nearest.gap, axis: nearest.axisName };
92977
- }
92978
- function intersectionVolume(a, b) {
92979
- try {
92980
- const hit = a.shape.intersect(b.shape);
92981
- if (hit.isEmpty()) return { volume: 0 };
92982
- const volume = hit.volume();
92983
- return { volume: Number.isFinite(volume) ? volume : 0 };
92984
- } catch (err2) {
92985
- const message = err2 instanceof Error ? err2.message : String(err2);
92986
- return { volume: null, warning: `Could not boolean-test ${a.name} against ${b.name}: ${message}` };
92987
- }
92988
- }
92989
93431
  function bodyCountForEntry(entry) {
92990
93432
  if (typeof entry.bodyCount === "number" && Number.isFinite(entry.bodyCount)) {
92991
93433
  return Math.max(0, Math.round(entry.bodyCount));
@@ -93017,52 +93459,22 @@ function analyzePhysicalConnectivity2(entries, rawOptions = {}) {
93017
93459
  const warnings = [];
93018
93460
  const edges = [];
93019
93461
  const unionFind = new UnionFind2(entries.length);
93462
+ const meshDataByIndex = /* @__PURE__ */ new Map();
93463
+ entries.forEach((entry, index) => {
93464
+ const meshData = meshContactDataFor(entry);
93465
+ if (meshData) meshDataByIndex.set(index, meshData);
93466
+ });
93020
93467
  for (const pair of collectCandidatePairs2(entries, options.contactTolerance)) {
93021
93468
  const i = pair.sourceIndex;
93022
93469
  const j = pair.targetIndex;
93023
- const a = entries[i];
93024
- const b = entries[j];
93025
- const bboxOverlaps = !hasPositiveGap(pair.gaps) && aabbInteriorOverlaps(a, b);
93026
- if (options.exactGeometry && bboxOverlaps) {
93027
- const overlap = intersectionVolume(a, b);
93028
- if (overlap.warning) warnings.push(overlap.warning);
93029
- if (overlap.volume != null && overlap.volume > options.minOverlapVolume) {
93030
- unionFind.union(i, j);
93031
- edges.push(
93032
- makeEdge2(entries, i, j, {
93033
- kind: "overlap",
93034
- method: "boolean-intersection",
93035
- gap: 0,
93036
- overlapVolume: overlap.volume
93037
- })
93038
- );
93039
- continue;
93040
- }
93041
- continue;
93042
- }
93043
- if (bboxOverlaps && options.mergeOverlappingBBoxes) {
93044
- unionFind.union(i, j);
93045
- edges.push(
93046
- makeEdge2(entries, i, j, {
93047
- kind: "overlap",
93048
- method: "bbox-overlap",
93049
- gap: 0,
93050
- overlapVolume: aabbOverlapVolume(a, b)
93051
- })
93052
- );
93053
- } else {
93054
- const contact = contactFromBBoxes(a, b, options.contactTolerance);
93055
- if (!contact.touching || !options.mergeTouchingBBoxes) continue;
93056
- unionFind.union(i, j);
93057
- edges.push(
93058
- makeEdge2(entries, i, j, {
93059
- kind: "touching",
93060
- method: "bbox-contact",
93061
- gap: contact.gap,
93062
- axis: contact.axis
93063
- })
93064
- );
93065
- }
93470
+ const detection = detectPhysicalContact(entries[i], entries[j], options, {
93471
+ sourceMesh: meshDataByIndex.get(i) ?? null,
93472
+ targetMesh: meshDataByIndex.get(j) ?? null
93473
+ });
93474
+ if (detection.warning) warnings.push(detection.warning);
93475
+ if (!detection.contact) continue;
93476
+ unionFind.union(i, j);
93477
+ edges.push(makeEdge2(entries, i, j, detection.contact));
93066
93478
  }
93067
93479
  const objects = entries.map((entry, index) => ({
93068
93480
  index,
@@ -93107,7 +93519,7 @@ function analyzePhysicalConnectivity2(entries, rawOptions = {}) {
93107
93519
  }
93108
93520
  const components = [...componentByRoot.values()];
93109
93521
  return {
93110
- method: options.exactGeometry ? "boolean-overlap-plus-bbox-contact" : "bbox-neighborhood",
93522
+ method: options.exactGeometry ? "mesh-contact-plus-boolean-overlap" : "bbox-neighborhood",
93111
93523
  options,
93112
93524
  objectCount: objects.length,
93113
93525
  componentCount: components.length,
@@ -96661,9 +97073,6 @@ ${errors.join("\n\n")}`);
96661
97073
  }
96662
97074
  runDirectCliMain(import.meta.url, "cli/check-runtime-globals.ts", () => runCheckRuntimeGlobalsCli());
96663
97075
 
96664
- // cli/check-suite.ts
96665
- import { execSync as execSync2 } from "child_process";
96666
-
96667
97076
  // cli/check-text.ts
96668
97077
  import assert8 from "assert/strict";
96669
97078
  var EPS17 = 1e-3;
@@ -97238,181 +97647,6 @@ async function runCheckTransformsCli() {
97238
97647
  }
97239
97648
  runDirectCliMain(import.meta.url, "cli/check-transforms.ts", () => runCheckTransformsCli());
97240
97649
 
97241
- // cli/check-suite.ts
97242
- var BRAILLE_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
97243
- var ANSI = {
97244
- dim: "\x1B[2m",
97245
- bold: "\x1B[1m",
97246
- green: "\x1B[32m",
97247
- cyan: "\x1B[36m",
97248
- yellow: "\x1B[33m",
97249
- red: "\x1B[31m",
97250
- reset: "\x1B[0m",
97251
- clearLine: "\x1B[2K",
97252
- cursorUp: "\x1B[1A",
97253
- hideCursor: "\x1B[?25l",
97254
- showCursor: "\x1B[?25h"
97255
- };
97256
- function createSmokeStages() {
97257
- return [
97258
- { label: "Constraints", insight: "solver convergence & snapshot fidelity", run: () => runCheckConstraintsCli([]) },
97259
- { label: "Transforms", insight: "matrix composition & assembly trees", run: () => runCheckTransformsCli() },
97260
- { label: "Dimensions", insight: "parametric dimension propagation", run: () => runCheckDimensionsCli() },
97261
- { label: "JS modules", insight: "import graph & bundle hygiene", run: () => runCheckJsModulesCli() },
97262
- { label: "Runtime globals", insight: "sandbox namespace allowlist", run: () => runCheckRuntimeGlobalsCli() },
97263
- { label: "Text", insight: "text2d rendering contracts", run: () => runCheckTextCli() },
97264
- {
97265
- label: "Examples",
97266
- insight: "maintained example smoke surface",
97267
- run: () => runCheckExamplesCli(["--profile", "smoke"])
97268
- }
97269
- ];
97270
- }
97271
- function createStages(profile) {
97272
- const smokeStages = createSmokeStages();
97273
- if (profile === "smoke") return smokeStages;
97274
- return [
97275
- ...smokeStages,
97276
- {
97277
- label: "Unit tests",
97278
- insight: "vitest \u2014 pure geometry and integration contracts",
97279
- run: async () => {
97280
- execSync2("npx vitest run", { stdio: "pipe" });
97281
- }
97282
- },
97283
- { label: "Placement refs", insight: "reference stability across edits", run: () => runCheckPlacementReferencesCli() },
97284
- { label: "BREP export", insight: "solid topology & face-history integrity", run: () => runCheckBrepExportCli() },
97285
- { label: "Compiler", insight: "AST snapshots & code-gen correctness", run: () => runCheckCompilerCli([]) },
97286
- { label: "Query propagation", insight: "reactive data-flow graph consistency", run: () => runCheckQueryPropagationCli([]) },
97287
- { label: "API contracts", insight: "public script API surface stability", run: () => runCheckApiContractsCli() },
97288
- { label: "OCCT lowerer", insight: "compile-plan \u2192 OCCT geometry invariants", run: () => runCheckOcctLowerCli() },
97289
- {
97290
- label: "Lint & format",
97291
- insight: "Biome lint + formatting consistency",
97292
- run: async () => {
97293
- execSync2("npx biome check .", { stdio: "pipe" });
97294
- }
97295
- }
97296
- ];
97297
- }
97298
- function parseProfile(argv) {
97299
- let profile = "full";
97300
- for (let i = 0; i < argv.length; i += 1) {
97301
- const arg = argv[i];
97302
- if (arg === "--profile") {
97303
- const value = argv[i + 1];
97304
- if (value !== "full" && value !== "smoke" && value !== "local") {
97305
- throw new Error(`Unknown suite profile "${value ?? ""}". Expected "smoke" or "full" (legacy alias: "local").`);
97306
- }
97307
- profile = value === "local" ? "smoke" : value;
97308
- i += 1;
97309
- continue;
97310
- }
97311
- throw new Error(`Unknown flag: ${arg}`);
97312
- }
97313
- return profile;
97314
- }
97315
- function progressBar(done, total, width) {
97316
- const filled = Math.round(done / total * width);
97317
- const empty = width - filled;
97318
- const bar = "\x1B[32m" + "\u2501".repeat(filled) + "\x1B[2m" + "\u2500".repeat(empty) + "\x1B[0m";
97319
- return bar;
97320
- }
97321
- function formatDuration2(ms) {
97322
- if (ms < 1e3) return `${ms}ms`;
97323
- return `${(ms / 1e3).toFixed(1)}s`;
97324
- }
97325
- function startSpinner(stage, index, total) {
97326
- const isTTY = process.stdout.isTTY;
97327
- if (!isTTY) {
97328
- process.stdout.write(`[${index + 1}/${total}] ${stage.label} ...
97329
- `);
97330
- return () => {
97331
- };
97332
- }
97333
- let frame = 0;
97334
- const startMs = Date.now();
97335
- const render = () => {
97336
- const spinner = `${ANSI.cyan}${BRAILLE_FRAMES[frame % BRAILLE_FRAMES.length]}${ANSI.reset}`;
97337
- const bar = progressBar(index, total, 20);
97338
- const pct = `${Math.round(index / total * 100)}%`;
97339
- const elapsed = formatDuration2(Date.now() - startMs);
97340
- const line2 = `${ANSI.clearLine}\r ${spinner} ${bar} ${ANSI.dim}${pct}${ANSI.reset} ${ANSI.bold}${stage.label}${ANSI.reset} ${ANSI.dim}\u2014 ${stage.insight}${ANSI.reset} ${ANSI.dim}(${elapsed})${ANSI.reset}`;
97341
- process.stdout.write(line2);
97342
- frame++;
97343
- };
97344
- render();
97345
- const interval = setInterval(render, 80);
97346
- return () => {
97347
- clearInterval(interval);
97348
- process.stdout.write(`${ANSI.clearLine}\r`);
97349
- };
97350
- }
97351
- function printStageResult(result, index, total) {
97352
- const icon = result.ok ? `${ANSI.green}\u2713${ANSI.reset}` : `${ANSI.red}\u2717${ANSI.reset}`;
97353
- const dur = `${ANSI.dim}${formatDuration2(result.durationMs)}${ANSI.reset}`;
97354
- const counter = `${ANSI.dim}[${index + 1}/${total}]${ANSI.reset}`;
97355
- console.log(` ${icon} ${counter} ${result.label} ${dur}`);
97356
- }
97357
- function printSummary(results, totalMs) {
97358
- const passed = results.filter((r) => r.ok).length;
97359
- const failed = results.filter((r) => !r.ok).length;
97360
- const slowest = results.reduce((a, b) => a.durationMs > b.durationMs ? a : b);
97361
- console.log("");
97362
- console.log(` ${progressBar(results.length, results.length, 20)} ${ANSI.bold}100%${ANSI.reset}`);
97363
- console.log("");
97364
- if (failed === 0) {
97365
- console.log(
97366
- ` ${ANSI.green}${ANSI.bold}\u2713 Invariant suite passed${ANSI.reset} ${ANSI.dim}\u2014 ${passed} checks in ${formatDuration2(totalMs)}${ANSI.reset}`
97367
- );
97368
- } else {
97369
- console.log(
97370
- ` ${ANSI.red}${ANSI.bold}\u2717 Suite failed${ANSI.reset} ${ANSI.dim}\u2014 ${passed} passed, ${failed} failed in ${formatDuration2(totalMs)}${ANSI.reset}`
97371
- );
97372
- }
97373
- console.log(` ${ANSI.dim}slowest: ${slowest.label} (${formatDuration2(slowest.durationMs)})${ANSI.reset}`);
97374
- console.log("");
97375
- }
97376
- async function runCheckSuiteCli(argv = process.argv.slice(2)) {
97377
- const profile = parseProfile(argv);
97378
- const stages = createStages(profile);
97379
- const isTTY = process.stdout.isTTY;
97380
- if (isTTY) process.stdout.write(ANSI.hideCursor);
97381
- console.log("");
97382
- console.log(` ${ANSI.bold}ForgeCAD Invariant Suite${ANSI.reset} ${ANSI.dim}(${profile} profile, ${stages.length} checks)${ANSI.reset}`);
97383
- console.log("");
97384
- const results = [];
97385
- const suiteStart = Date.now();
97386
- for (let i = 0; i < stages.length; i++) {
97387
- const stage = stages[i];
97388
- const stopSpinner = startSpinner(stage, i, stages.length);
97389
- const t0 = Date.now();
97390
- let ok = true;
97391
- let error;
97392
- try {
97393
- await stage.run();
97394
- } catch (err2) {
97395
- ok = false;
97396
- error = err2.message;
97397
- }
97398
- stopSpinner();
97399
- const result = { label: stage.label, durationMs: Date.now() - t0, ok, error };
97400
- results.push(result);
97401
- printStageResult(result, i, stages.length);
97402
- if (!ok && error) {
97403
- console.error(` ${ANSI.red}${error}${ANSI.reset}`);
97404
- }
97405
- }
97406
- const totalMs = Date.now() - suiteStart;
97407
- printSummary(results, totalMs);
97408
- if (isTTY) process.stdout.write(ANSI.showCursor);
97409
- const failed = results.filter((r) => !r.ok);
97410
- if (failed.length > 0) {
97411
- process.exit(1);
97412
- }
97413
- }
97414
- runDirectCliMain(import.meta.url, "cli/check-suite.ts", () => runCheckSuiteCli(process.argv.slice(2)));
97415
-
97416
97650
  // cli/debug-compiler.ts
97417
97651
  function usage3() {
97418
97652
  console.error("Usage: forgecad debug compiler <script.forge.js> [--compact]");
@@ -97511,7 +97745,7 @@ async function runDebugAssemblyCli(argv = process.argv.slice(2)) {
97511
97745
  const code = readFileSync12(resolve12(scriptPath), "utf-8");
97512
97746
  const { allFiles, fileName, readBinaryFile } = collectProjectFiles(scriptPath);
97513
97747
  await init();
97514
- if (backend) await activateBackend(backend);
97748
+ await activateBackend(backend ?? CLI_DEFAULT_BACKEND);
97515
97749
  const result = runScript(code, fileName, allFiles, { readBinaryFile, allowEmptyResult: true });
97516
97750
  if (result.error) {
97517
97751
  console.error("ERROR:", result.error);
@@ -97971,7 +98205,7 @@ async function runBrepCli(argv = process.argv.slice(2)) {
97971
98205
  }
97972
98206
 
97973
98207
  // cli/forge-capture.ts
97974
- import { execSync as execSync3, spawn as spawn3 } from "child_process";
98208
+ import { execSync as execSync2, spawn as spawn3 } from "child_process";
97975
98209
  import { once } from "events";
97976
98210
  import { existsSync as existsSync7 } from "fs";
97977
98211
  import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
@@ -98823,7 +99057,7 @@ function findExecutablePath(explicitPath, staticCandidates, binCandidates) {
98823
99057
  for (const bin of binCandidates) {
98824
99058
  try {
98825
99059
  const cmd = process.platform === "win32" ? `where ${bin}` : `command -v ${bin}`;
98826
- const found = execSync3(cmd, { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
99060
+ const found = execSync2(cmd, { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
98827
99061
  if (found && existsSync7(found)) return found;
98828
99062
  } catch {
98829
99063
  }
@@ -99389,7 +99623,7 @@ function printList(scriptPath, init2) {
99389
99623
  );
99390
99624
  console.log(` cut planes: ${cutPlanes.length === 0 ? "(none)" : cutPlanes.join(", ")}`);
99391
99625
  }
99392
- function printSummary2(options, init2, encoderMode) {
99626
+ function printSummary(options, init2, encoderMode) {
99393
99627
  const lines = [
99394
99628
  `${options.format.toUpperCase()} capture: ${basename4(options.outputPath)}`,
99395
99629
  ` capture=${options.capture} render=${options.renderMode}${options.includeWireframePass && options.renderMode !== "wireframe" ? "+wireframe" : ""}`,
@@ -99581,7 +99815,7 @@ async function runCaptureCli(config, argv = process.argv.slice(2)) {
99581
99815
  return;
99582
99816
  }
99583
99817
  const framePlan = buildFramePlan(options, init2);
99584
- printSummary2(options, init2, encoderMode);
99818
+ printSummary(options, init2, encoderMode);
99585
99819
  if (options.format === "gif" && encoderMode === "js" && options.encoder !== "js") {
99586
99820
  console.log(" ffmpeg not found; using the fallback 256-color JS GIF encoder.");
99587
99821
  }
@@ -100293,7 +100527,7 @@ async function runMeshExportCli(format, argv) {
100293
100527
  const code = (await import("fs")).readFileSync(resolve20(scriptPath), "utf-8");
100294
100528
  const { allFiles, fileName, readBinaryFile } = collectProjectFiles(scriptPath);
100295
100529
  await initKernel();
100296
- if (backend) await activateBackend(backend);
100530
+ await activateBackend(backend ?? CLI_DEFAULT_BACKEND);
100297
100531
  const qualityPreset = quality && quality !== "default" ? quality : void 0;
100298
100532
  const result = runScript(code, fileName, allFiles, { ...qualityPreset ? { quality: qualityPreset } : {}, readBinaryFile });
100299
100533
  if (result.error) {
@@ -100429,7 +100663,7 @@ async function runRenderViewsCli(argv) {
100429
100663
  const code = readFileSync16(scriptPath, "utf-8");
100430
100664
  const { allFiles, fileName, readBinaryFile } = collectProjectFiles(scriptPath);
100431
100665
  await init();
100432
- if (args.backend) await activateBackend(args.backend);
100666
+ await activateBackend(args.backend ?? CLI_DEFAULT_BACKEND);
100433
100667
  const result = runScript(code, fileName, allFiles, {
100434
100668
  ...args.quality && args.quality !== "default" ? { quality: args.quality } : {},
100435
100669
  allowEmptyResult: true,
@@ -100487,7 +100721,7 @@ async function runRenderInspectChannelsCli(argv) {
100487
100721
  // cli/forge-render-hq.ts
100488
100722
  import { writeFileSync as writeFileSync9, readFileSync as readFileSync17, mkdtempSync as mkdtempSync2, rmSync as rmSync2, existsSync as existsSync9 } from "fs";
100489
100723
  import { resolve as resolve22, dirname as dirname8, join as join6, extname as extname6 } from "path";
100490
- import { execSync as execSync4, spawnSync } from "child_process";
100724
+ import { execSync as execSync3, spawnSync } from "child_process";
100491
100725
  import { tmpdir as tmpdir2 } from "os";
100492
100726
  import { fileURLToPath as fileURLToPath3 } from "url";
100493
100727
 
@@ -100692,7 +100926,7 @@ function parseArgs8(argv) {
100692
100926
  }
100693
100927
  if (!scriptPath) {
100694
100928
  throw new Error(
100695
- "Usage: forgecad render hq <script.forge.js> [output.png] [options]\n --preset <studio|outdoor|dramatic|clay|wireframe|glass|metallic>\n --edges <off|thin|bold> Edge overlay preset (default: off)\n --size <px> Square output (sets both width and height)\n --width <px> Output width (default: 1920)\n --height <px> Output height (default: 1080)\n --samples <n> Render samples (default: 256)\n --engine <cycles|eevee> Render engine (default: cycles)\n --camera <front|back|side|right|top|iso|az:el|spec> Camera preset, angle, or exact spec\n --camera-json <file> Exact viewport camera JSON file\n --view <name> Named camera view from scene({ views })\n --scene <json|file> Viewport scene JSON or JSON file with camera/object overrides\n --transparent Transparent background\n --hdri <path.hdr> Custom HDRI environment map\n --video Render orbit turntable video (MP4)\n --frames <n> Video frames (default: 72)\n --fps <n> Video FPS (default: 24)\n --pitch <deg> Camera pitch angle (default: 25)\n --quality <default|live|high> Mesh tessellation quality\n --backend <manifold|occt|truck> Geometry backend"
100929
+ "Usage: forgecad render hq <script.forge.js> [output.png] [options]\n --preset <studio|outdoor|dramatic|clay|wireframe|glass|metallic>\n --edges <off|thin|bold> Edge overlay preset (default: off)\n --size <px> Square output (sets both width and height)\n --width <px> Output width (default: 1920)\n --height <px> Output height (default: 1080)\n --samples <n> Render samples (default: 256)\n --engine <cycles|eevee> Render engine (default: cycles)\n --camera <front|back|side|right|top|iso|az:el|spec> Camera preset, angle, or exact spec\n --camera-json <file> Exact viewport camera JSON file\n --view <name> Named camera view from scene({ views })\n --scene <json|file> Viewport scene JSON or JSON file with camera/object overrides\n --transparent Transparent background\n --hdri <path.hdr> Custom HDRI environment map\n --video Render orbit turntable video (MP4)\n --frames <n> Video frames (default: 72)\n --fps <n> Video FPS (default: 24)\n --pitch <deg> Camera pitch angle (default: 25)\n --quality <default|live|high> Mesh tessellation quality\n --backend <manifold|occt|truck> Geometry backend (default: manifold)"
100696
100930
  );
100697
100931
  }
100698
100932
  if (viewName && (cameraToken || scene2?.camera)) {
@@ -100729,7 +100963,7 @@ function parseArgs8(argv) {
100729
100963
  }
100730
100964
  function findBlender() {
100731
100965
  try {
100732
- const which = execSync4("which blender 2>/dev/null", { encoding: "utf-8" }).trim();
100966
+ const which = execSync3("which blender 2>/dev/null", { encoding: "utf-8" }).trim();
100733
100967
  if (which) return which;
100734
100968
  } catch {
100735
100969
  }
@@ -100836,7 +101070,7 @@ async function runRenderHqCli(argv) {
100836
101070
  const { allFiles, fileName, readBinaryFile } = collectProjectFiles(args.scriptPath);
100837
101071
  console.log("Evaluating ForgeCAD script...");
100838
101072
  await initKernel();
100839
- if (args.backend) await activateBackend(args.backend);
101073
+ await activateBackend(args.backend ?? CLI_DEFAULT_BACKEND);
100840
101074
  const qualityPreset = args.quality && args.quality !== "default" ? args.quality : void 0;
100841
101075
  const result = runScript(code, fileName, allFiles, {
100842
101076
  ...qualityPreset ? { quality: qualityPreset } : {},
@@ -105347,7 +105581,7 @@ async function runWebCli(argv = process.argv.slice(2)) {
105347
105581
  }
105348
105582
 
105349
105583
  // cli/forge-link.ts
105350
- import { execSync as execSync5 } from "child_process";
105584
+ import { execSync as execSync4 } from "child_process";
105351
105585
  var PROD_BASE = "https://forgecad.io/app";
105352
105586
  function extractGistId(input) {
105353
105587
  const match = input.match(/gist\.github\.com\/(?:[^/]+\/)?([a-f0-9]+)/i);
@@ -105363,7 +105597,7 @@ async function runLinkCli(args) {
105363
105597
  const gistId = extractGistId(input);
105364
105598
  const url = `${PROD_BASE}?gist=${encodeURIComponent(gistId)}`;
105365
105599
  try {
105366
- execSync5('printf %s "$URL" | pbcopy', { env: { ...process.env, URL: url }, stdio: "ignore" });
105600
+ execSync4('printf %s "$URL" | pbcopy', { env: { ...process.env, URL: url }, stdio: "ignore" });
105367
105601
  console.log(`Copied to clipboard:
105368
105602
  ${url}`);
105369
105603
  } catch {
@@ -105372,7 +105606,7 @@ ${url}`);
105372
105606
  }
105373
105607
 
105374
105608
  // cli/forge-publish.ts
105375
- import { execSync as execSync6 } from "child_process";
105609
+ import { execSync as execSync5 } from "child_process";
105376
105610
  import { existsSync as existsSync15 } from "fs";
105377
105611
  import { dirname as dirname11, relative as relative7, resolve as resolve33 } from "path";
105378
105612
  function findProjectRoot3(dir) {
@@ -105389,14 +105623,14 @@ function findProjectRoot3(dir) {
105389
105623
  }
105390
105624
  function copyToClipboard(text) {
105391
105625
  try {
105392
- execSync6('printf %s "$TEXT" | pbcopy', {
105626
+ execSync5('printf %s "$TEXT" | pbcopy', {
105393
105627
  env: { ...process.env, TEXT: text },
105394
105628
  stdio: "ignore"
105395
105629
  });
105396
105630
  return true;
105397
105631
  } catch {
105398
105632
  try {
105399
- execSync6('printf %s "$TEXT" | xclip -selection clipboard', {
105633
+ execSync5('printf %s "$TEXT" | xclip -selection clipboard', {
105400
105634
  env: { ...process.env, TEXT: text },
105401
105635
  stdio: "ignore"
105402
105636
  });
@@ -105500,7 +105734,7 @@ async function runPublishCli(args) {
105500
105734
  }
105501
105735
 
105502
105736
  // cli/forge-doctor.ts
105503
- import { execSync as execSync7 } from "child_process";
105737
+ import { execSync as execSync6 } from "child_process";
105504
105738
  import { existsSync as existsSync16 } from "fs";
105505
105739
  function findExecutablePath2(staticCandidates, binCandidates) {
105506
105740
  for (const candidate of staticCandidates) {
@@ -105509,7 +105743,7 @@ function findExecutablePath2(staticCandidates, binCandidates) {
105509
105743
  for (const bin of binCandidates) {
105510
105744
  try {
105511
105745
  const cmd = process.platform === "win32" ? `where ${bin}` : `command -v ${bin}`;
105512
- const found = execSync7(cmd, { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
105746
+ const found = execSync6(cmd, { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
105513
105747
  if (found && existsSync16(found)) return found;
105514
105748
  } catch {
105515
105749
  }
@@ -105565,7 +105799,7 @@ function findFfmpeg() {
105565
105799
  }
105566
105800
  function getVersion(cmd) {
105567
105801
  try {
105568
- return execSync7(cmd, { stdio: ["ignore", "pipe", "ignore"] }).toString().trim().split("\n")[0];
105802
+ return execSync6(cmd, { stdio: ["ignore", "pipe", "ignore"] }).toString().trim().split("\n")[0];
105569
105803
  } catch {
105570
105804
  return null;
105571
105805
  }
@@ -107991,7 +108225,7 @@ async function runPrintCheckCli(argv = process.argv.slice(2)) {
107991
108225
  const code = readFileSync26(resolve36(args.scriptPath), "utf-8");
107992
108226
  const { allFiles, fileName, readBinaryFile } = collectProjectFiles(args.scriptPath);
107993
108227
  await init();
107994
- if (args.backend) await activateBackend(args.backend);
108228
+ await activateBackend(args.backend ?? CLI_DEFAULT_BACKEND);
107995
108229
  if (Object.keys(args.paramOverrides).length > 0) setParamOverrides(args.paramOverrides);
107996
108230
  const result = runScript(code, fileName, allFiles, {
107997
108231
  debugImports: args.debugImports,
@@ -108947,7 +109181,6 @@ var DEFAULT_EXACT_SPATIAL_OBJECT_LIMIT = 250;
108947
109181
  var SPATIAL_BBOX_PADDING = 0.1;
108948
109182
  var MAX_SPATIAL_COLLISION_LINES = 50;
108949
109183
  var MAX_COMPACT_OBJECT_NAMES = 24;
108950
- var RUN_DEFAULT_BACKEND = "manifold";
108951
109184
  function parseSpatialMode(argv, defaultMode) {
108952
109185
  const consumed = /* @__PURE__ */ new Set();
108953
109186
  const idx = argv.indexOf("--spatial");
@@ -109404,7 +109637,7 @@ async function runScriptCli(argv = process.argv.slice(2)) {
109404
109637
  const printConstructionHistory = fullReport || verbose || argv.includes("--history");
109405
109638
  const printFeatureSummary = fullReport || verbose || argv.includes("--features");
109406
109639
  const printSolverProfile = fullReport || verbose || argv.includes("--solver-profile");
109407
- const backend = parseBackendArg(argv) ?? RUN_DEFAULT_BACKEND;
109640
+ const backend = parseBackendArg(argv) ?? CLI_DEFAULT_BACKEND;
109408
109641
  const quality = parseQualityArg(argv);
109409
109642
  const solverDebugOut = parseRequiredArg(argv, "--solver-debug-out");
109410
109643
  const {
@@ -109767,7 +110000,7 @@ async function runScriptCli(argv = process.argv.slice(2)) {
109767
110000
  if (solverDebugOut) {
109768
110001
  setSolveOptionOverrides(null);
109769
110002
  }
109770
- if (backend && backend !== DEFAULT_ACTIVE_BACKEND) await activateBackend(DEFAULT_ACTIVE_BACKEND);
110003
+ if (backend !== CLI_DEFAULT_BACKEND) setActiveBackend(CLI_DEFAULT_BACKEND);
109771
110004
  }
109772
110005
  }
109773
110006
 
@@ -109796,7 +110029,7 @@ async function runRenderSectionCli(argv = process.argv.slice(2)) {
109796
110029
  let offset2 = 0;
109797
110030
  let size = 1024;
109798
110031
  let chromePath = process.env.CHROME_PATH;
109799
- let background = "#2a2a2a";
110032
+ let background = "#f7f7f4";
109800
110033
  let edges = "thin";
109801
110034
  const args = [];
109802
110035
  for (let i = 0; i < argv.length; i++) {
@@ -109875,7 +110108,7 @@ async function runRenderSectionCli(argv = process.argv.slice(2)) {
109875
110108
  console.error("No intersection found.");
109876
110109
  process.exit(1);
109877
110110
  }
109878
- const svgDocument = buildSketchSvgDocument(sectionSketches, { edges });
110111
+ const svgDocument = buildSketchSvgDocument(sectionSketches, { edges, theme: "cad-section" });
109879
110112
  if (wantsPng) {
109880
110113
  const png = await svgToPng(svgDocument.svg, { size, background, chromePath: resolvedChromePath });
109881
110114
  await writeFile9(resolve39(outputPath), png);
@@ -110038,9 +110271,11 @@ var INSPECT_CHANNEL_VALUES = [
110038
110271
  { value: "rgb", description: "Canonical RGB viewport renders" },
110039
110272
  { value: "depth", description: "Packed linear view-depth maps" },
110040
110273
  { value: "normals", description: "View-space normal maps" },
110274
+ { value: "zebra", description: "Reflective zebra stripes for surface-continuity inspection" },
110041
110275
  { value: "roughness", description: "Surface roughness and sharp-feature heatmap" },
110042
110276
  { value: "mask", description: "Object identity masks" },
110043
110277
  { value: "connectivity", description: "Physical connected-component masks" },
110278
+ { value: "floating", description: "Disconnected floating body highlights" },
110044
110279
  { value: "distance", description: "Rooted physical-component distance heatmap" },
110045
110280
  { value: "collisions", description: "Ghosted source objects with highlighted intersection volumes" },
110046
110281
  { value: "thickness", description: "Local wall-thickness heatmap" },
@@ -110079,7 +110314,9 @@ var RENDER_STYLE_VALUES = [
110079
110314
  { value: "classic", description: "Technical CAD shading with crisp overlay edges" },
110080
110315
  { value: "studio", description: "Polished product-preview lighting and reflections" },
110081
110316
  { value: "fast", description: "Low-cost attractive live preview shading" },
110082
- { value: "glass", description: "Transparent inspection view with rim shell and readable edges" }
110317
+ { value: "glass", description: "Transparent inspection view with rim shell and readable edges" },
110318
+ { value: "precision", description: "Dense technical surface-field contours" },
110319
+ { value: "hybrid", description: "Topo-led surface contours with a quiet orthogonal field" }
110083
110320
  ];
110084
110321
  var RENDER_OPTIONS = [
110085
110322
  {
@@ -110124,7 +110361,7 @@ var RENDER_OPTIONS = [
110124
110361
  name: "--render-style",
110125
110362
  description: "Visual render style (default: classic)",
110126
110363
  argument: "required",
110127
- valueLabel: "<classic|studio|fast|glass>",
110364
+ valueLabel: "<classic|studio|fast|glass|precision|hybrid>",
110128
110365
  values: RENDER_STYLE_VALUES
110129
110366
  },
110130
110367
  { name: "--port", description: "Vite dev server port", argument: "required", valueLabel: "<n>" },
@@ -110144,7 +110381,7 @@ var RENDER_INSPECT_OPTIONS = [
110144
110381
  name: "--channels",
110145
110382
  description: "Required inspection channels to emit; no default",
110146
110383
  argument: "required",
110147
- valueLabel: "<rgb,depth,normals,roughness,mask,connectivity,distance,collisions,thickness,section>",
110384
+ valueLabel: "<rgb,depth,normals,zebra,roughness,mask,connectivity,floating,distance,collisions,thickness,section>",
110148
110385
  values: INSPECT_CHANNEL_VALUES
110149
110386
  },
110150
110387
  {
@@ -110166,8 +110403,18 @@ var RENDER_INSPECT_OPTIONS = [
110166
110403
  { name: "--min-thickness", description: "Critical thickness threshold in model units", argument: "required", valueLabel: "<mm>" },
110167
110404
  { name: "--warn-thickness", description: "Warning thickness threshold in model units", argument: "required", valueLabel: "<mm>" },
110168
110405
  { name: "--max-thickness", description: "Thick/blue heatmap threshold in model units", argument: "required", valueLabel: "<mm>" },
110169
- { name: "--thickness-samples", description: "Maximum thickness point samples per object", argument: "required", valueLabel: "<n>" },
110170
- { name: "--roughness-samples", description: "Maximum roughness point samples per object", argument: "required", valueLabel: "<n>" },
110406
+ {
110407
+ name: "--thickness-samples",
110408
+ description: "Override default scene budget with max thickness point samples per object",
110409
+ argument: "required",
110410
+ valueLabel: "<n>"
110411
+ },
110412
+ {
110413
+ name: "--roughness-samples",
110414
+ description: "Override default scene budget with max roughness point samples per object",
110415
+ argument: "required",
110416
+ valueLabel: "<n>"
110417
+ },
110171
110418
  { name: "--port", description: "Vite dev server port", argument: "required", valueLabel: "<n>" },
110172
110419
  { name: "--fresh-server", description: "Start a fresh renderer instead of reusing an existing one" },
110173
110420
  {
@@ -110497,13 +110744,13 @@ var commands = [
110497
110744
  },
110498
110745
  {
110499
110746
  name: "--backend",
110500
- description: "Geometry backend",
110747
+ description: "Geometry backend (default: manifold)",
110501
110748
  argument: "required",
110502
110749
  valueLabel: "<manifold|occt|truck>",
110503
110750
  values: [
110504
- { value: "manifold", description: "Manifold backend" },
110751
+ { value: "manifold", description: "Manifold backend (default)" },
110505
110752
  { value: "occt", description: "OCCT backend" },
110506
- { value: "truck", description: "Rust topology backend (default)" }
110753
+ { value: "truck", description: "Rust topology backend" }
110507
110754
  ]
110508
110755
  },
110509
110756
  {
@@ -110636,7 +110883,7 @@ var commands = [
110636
110883
  },
110637
110884
  {
110638
110885
  name: "--backend",
110639
- description: "Geometry backend",
110886
+ description: "Geometry backend (default: manifold)",
110640
110887
  argument: "required",
110641
110888
  valueLabel: "<manifold|occt|truck>",
110642
110889
  values: ["manifold", "occt", "truck"]
@@ -110661,12 +110908,13 @@ var commands = [
110661
110908
  group: "Modeling",
110662
110909
  path: ["render", "inspect"],
110663
110910
  summary: "Render a machine-readable inspection bundle with geometry channels and a manifest.",
110664
- description: "Launches the headless viewport renderer and writes a directory bundle for agent and automation workflows. Every channel is opt-in with `--channels`; there is no default bundle. Selected channels emit canonical `front`, `right`, `top`, and `iso` views for RGB, depth, normals, surface roughness, object masks, physical connectivity, rooted component distance, collisions, and wall thickness, or a principal-plane section atlas, plus a root `manifest.json` with scene metadata, filters, object visibility, and relative file paths.\n\nUse `--focus` to isolate specific parts or hide mocks, and `--hide` to remove named clutter. Output defaults to `<script-name>-inspect/` next to the input file.\n\nFor bundle layout, channel encodings, and manifest semantics, see [Inspection Bundles](guides/inspection-bundles.md).",
110911
+ description: "Launches the headless viewport renderer and writes a directory bundle for agent and automation workflows. Every channel is opt-in with `--channels`; there is no default bundle. Selected channels emit canonical `front`, `right`, `top`, and `iso` views for RGB, depth, normals, Zebra surface-continuity stripes, surface roughness, object masks, physical connectivity, floating body highlights, rooted component distance, collisions, and wall thickness, or a principal-plane section atlas, plus a root `manifest.json` with scene metadata, filters, object visibility, and relative file paths.\n\nUse `--focus` to isolate specific parts or hide mocks, and `--hide` to remove named clutter. Output defaults to `<script-name>-inspect/` next to the input file.\n\nFor bundle layout, channel encodings, and manifest semantics, see [Inspection Bundles](guides/inspection-bundles.md).",
110665
110912
  usage: [
110666
- "forgecad render inspect <script.forge.js> [output-dir] --channels rgb,depth,normals,roughness,mask,connectivity,distance,collisions,thickness,section [--focus [names]] [--hide names] [--min-thickness <mm>] [--warn-thickness <mm>] [options]"
110913
+ "forgecad render inspect <script.forge.js> [output-dir] --channels rgb,depth,normals,zebra,roughness,mask,connectivity,floating,distance,collisions,thickness,section [--focus [names]] [--hide names] [--min-thickness <mm>] [--warn-thickness <mm>] [options]"
110667
110914
  ],
110668
110915
  examples: [
110669
110916
  "forgecad render inspect examples/api/static-assembly-connectors.forge.js --channels rgb,mask",
110917
+ "forgecad render inspect examples/api/static-assembly-connectors.forge.js --channels rgb,zebra,normals",
110670
110918
  'forgecad render inspect examples/api/static-assembly-connectors.forge.js out/bench-inspect --channels collisions --focus "Bench.*"',
110671
110919
  'forgecad render inspect examples/api/static-assembly-connectors.forge.js --channels rgb,mask,collisions --hide "Bench.Slat0" --force'
110672
110920
  ],
@@ -110751,7 +110999,7 @@ var commands = [
110751
110999
  },
110752
111000
  {
110753
111001
  name: "--backend",
110754
- description: "Geometry backend",
111002
+ description: "Geometry backend (default: manifold)",
110755
111003
  argument: "required",
110756
111004
  valueLabel: "<manifold|occt|truck>",
110757
111005
  values: ["manifold", "occt", "truck"]
@@ -110989,13 +111237,13 @@ var commands = [
110989
111237
  },
110990
111238
  {
110991
111239
  name: "--backend",
110992
- description: "Geometry backend",
111240
+ description: "Geometry backend (default: manifold)",
110993
111241
  argument: "required",
110994
111242
  valueLabel: "<manifold|occt|truck>",
110995
111243
  values: [
110996
- { value: "manifold", description: "Manifold backend" },
111244
+ { value: "manifold", description: "Manifold backend (default)" },
110997
111245
  { value: "occt", description: "OCCT backend" },
110998
- { value: "truck", description: "Rust topology backend (default)" }
111246
+ { value: "truck", description: "Rust topology backend" }
110999
111247
  ]
111000
111248
  }
111001
111249
  ],
@@ -111026,13 +111274,13 @@ var commands = [
111026
111274
  },
111027
111275
  {
111028
111276
  name: "--backend",
111029
- description: "Geometry backend",
111277
+ description: "Geometry backend (default: manifold)",
111030
111278
  argument: "required",
111031
111279
  valueLabel: "<manifold|occt|truck>",
111032
111280
  values: [
111033
- { value: "manifold", description: "Manifold backend" },
111281
+ { value: "manifold", description: "Manifold backend (default)" },
111034
111282
  { value: "occt", description: "OCCT backend" },
111035
- { value: "truck", description: "Rust topology backend (default)" }
111283
+ { value: "truck", description: "Rust topology backend" }
111036
111284
  ]
111037
111285
  }
111038
111286
  ],
@@ -111663,11 +111911,11 @@ var commands = [
111663
111911
  { name: "--debug-imports", description: "Include import trace logs during script execution" },
111664
111912
  {
111665
111913
  name: "--backend",
111666
- description: "Geometry backend",
111914
+ description: "Geometry backend (default: manifold)",
111667
111915
  argument: "required",
111668
111916
  valueLabel: "<manifold|occt|truck>",
111669
111917
  values: [
111670
- { value: "manifold", description: "Manifold backend" },
111918
+ { value: "manifold", description: "Manifold backend (default)" },
111671
111919
  { value: "occt", description: "OCCT backend" },
111672
111920
  { value: "truck", description: "Rust topology backend" }
111673
111921
  ]
@@ -111773,11 +112021,11 @@ var commands = [
111773
112021
  { name: "--all-forge", description: "Scan every *.forge.js file instead of only main.forge.js files" },
111774
112022
  {
111775
112023
  name: "--backend",
111776
- description: "Geometry backend",
112024
+ description: "Geometry backend (default: manifold)",
111777
112025
  argument: "required",
111778
112026
  valueLabel: "<manifold|occt|truck>",
111779
112027
  values: [
111780
- { value: "manifold", description: "Manifold backend" },
112028
+ { value: "manifold", description: "Manifold backend (default)" },
111781
112029
  { value: "occt", description: "OCCT backend" },
111782
112030
  { value: "truck", description: "Rust topology backend" }
111783
112031
  ]
@@ -111787,17 +112035,6 @@ var commands = [
111787
112035
  },
111788
112036
  run: runInspectMechanicalIntegrityCli
111789
112037
  },
111790
- {
111791
- group: "Checks",
111792
- path: ["check", "suite"],
111793
- summary: "Run the repo invariant suite, with smoke and full profiles for the fast merge lane vs the broader regression sweep.",
111794
- usage: ["forgecad check suite", "forgecad check suite --profile smoke"],
111795
- examples: ["forgecad check suite", "forgecad check suite --profile smoke"],
111796
- completion: {
111797
- options: [{ name: "--profile", description: "Suite profile: smoke or full", argument: "required", valueLabel: "<profile>" }]
111798
- },
111799
- run: (args) => runCheckSuiteCli(args)
111800
- },
111801
112038
  {
111802
112039
  group: "Checks",
111803
112040
  path: ["check", "runtime-globals"],
@@ -111989,7 +112226,12 @@ var commands = [
111989
112226
  argument: "required",
111990
112227
  valueLabel: "<error|warning>"
111991
112228
  },
111992
- { name: "--backend", description: "Geometry backend to use", argument: "required", valueLabel: "<manifold|occt|truck>" },
112229
+ {
112230
+ name: "--backend",
112231
+ description: "Geometry backend to use (default: manifold)",
112232
+ argument: "required",
112233
+ valueLabel: "<manifold|occt|truck>"
112234
+ },
111993
112235
  {
111994
112236
  name: "--sweep-steps",
111995
112237
  description: "Samples per joint range sweep; enables exact geometry checks",
@@ -112154,7 +112396,6 @@ var AUTH_EXEMPT_COMMANDS = /* @__PURE__ */ new Set([
112154
112396
  "debug compiler",
112155
112397
  "debug dimensions",
112156
112398
  "debug faces",
112157
- "check suite",
112158
112399
  "check print",
112159
112400
  "inspect mechanical-integrity"
112160
112401
  ]);
@@ -112426,6 +112667,7 @@ async function runForgeCadCli(argv = process.argv.slice(2)) {
112426
112667
  return;
112427
112668
  }
112428
112669
  validateCommandArgs(match.command, match.args);
112670
+ setActiveBackend(CLI_DEFAULT_BACKEND);
112429
112671
  const shouldRequireAuth = commandRequiresCliAuth(match.command.path);
112430
112672
  if (shouldRequireAuth) {
112431
112673
  requireCliAuth();