forgecad 0.9.13 → 0.9.14

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 (57) hide show
  1. package/dist/assets/{AdminPage-DramHHDf.js → AdminPage-eWGs2K6H.js} +1 -1
  2. package/dist/assets/{BenchmarkPage-Bjgkh5m9.js → BenchmarkPage-CTrLKfpo.js} +1 -1
  3. package/dist/assets/{BlogPage-n_HGP3Qm.js → BlogPage-5nPesyds.js} +1 -1
  4. package/dist/assets/{DocsPage-WCIkPmzC.js → DocsPage-C4Y3nbYc.js} +1 -1
  5. package/dist/assets/{EditorApp-CP9Za6tm.js → EditorApp-lXv53A1m.js} +9 -29
  6. package/dist/assets/{EmbedViewer-DEZKqdfW.js → EmbedViewer-C8fB4n5U.js} +2 -2
  7. package/dist/assets/{LandingPageProofDriven-CeRIctuj.js → LandingPageProofDriven-jSz0LaMM.js} +1 -1
  8. package/dist/assets/{PricingPage-rIRa8p4Y.js → PricingPage-B83B90zh.js} +1 -1
  9. package/dist/assets/{SettingsPage-BqCUvEXM.js → SettingsPage-DY889pcu.js} +1 -1
  10. package/dist/assets/{app-BUZqJvSO.js → app-bEww1ic4.js} +26 -28
  11. package/dist/assets/cli/{render-lhGxj50Y.js → render-Cho2uKG_.js} +88 -25
  12. package/dist/assets/{constructionHistoryWorker-ipD1jcIv.js → constructionHistoryWorker-HYwzJY4m.js} +1 -1
  13. package/dist/assets/{evalWorker-CHXSe_-u.js → evalWorker-CjQwJSE-.js} +3 -3
  14. package/dist/assets/{forgecad_geometry-BVnIeXMG.js → forgecad_geometry-CH2nvuLA.js} +1 -1
  15. package/dist/assets/forgecad_geometry_bg-C5_E9Oa9.wasm +0 -0
  16. package/dist/assets/{manifold-D1LZIHqn.js → manifold-CG9Fokx-.js} +1 -1
  17. package/dist/assets/{manifold-BTkzxi9V.js → manifold-rmfAcdwF.js} +1 -1
  18. package/dist/assets/{manifold-C2fwoTgd.js → manifold-uRzgk5O8.js} +2 -2
  19. package/dist/assets/{reportWorker-Cq1qGmg0.js → reportWorker-4cW_ZpoS.js} +3 -3
  20. package/dist/assets/{scalar-sampling-budget-D9Qv_UlJ.js → scalar-sampling-budget-CfDiFvh7.js} +12 -18
  21. package/dist/assets/{solver-BZ9LPTHs.js → solver-DuJAO8S6.js} +1 -1
  22. package/dist/assets/solver_bg-CWvv4lnN.wasm +0 -0
  23. package/dist/assets/{renderSceneState-Dr0xPq1A.js → targets-D6PWsv6X.js} +27 -1
  24. package/dist/cli/render.html +1 -1
  25. package/dist/docs/index.html +2 -2
  26. package/dist/docs-raw/AI/usage.md +6 -5
  27. package/dist/docs-raw/CLI.md +41 -11
  28. package/dist/docs-raw/generated/concepts.md +3 -3
  29. package/dist/docs-raw/generated/viewport.md +3 -3
  30. package/dist/docs-raw/harbor-cli.md +854 -0
  31. package/dist/docs-raw/rl-environments.md +100 -258
  32. package/dist/docs-raw/skills/forgecad-3d-reconstruction.md +2 -2
  33. package/dist/docs-raw/skills/forgecad-make-a-model.md +3 -3
  34. package/dist/docs-raw/skills/forgecad-reconstruction-benchmark.md +3 -3
  35. package/dist/index.html +1 -1
  36. package/dist/sitemap.xml +7 -7
  37. package/dist-cli/{check-compiler-LOXCPEOI.js → check-compiler-U5SOPN7X.js} +2 -2
  38. package/dist-cli/{check-query-propagation-BAKNVWXR.js → check-query-propagation-XOKNSSYU.js} +2 -2
  39. package/dist-cli/{chunk-RY43WF46.js → chunk-EXWGNL6K.js} +342 -2
  40. package/dist-cli/{chunk-RY43WF46.js.map → chunk-EXWGNL6K.js.map} +1 -1
  41. package/dist-cli/forgecad.js +733 -352
  42. package/dist-cli/forgecad.js.map +1 -1
  43. package/dist-cli/forgecad_geometry_bg.wasm +0 -0
  44. package/dist-cli/solver_bg.wasm +0 -0
  45. package/dist-skill/CONTEXT.md +3 -3
  46. package/dist-skill/docs/CLI.md +41 -11
  47. package/dist-skill/docs/generated/viewport.md +3 -3
  48. package/dist-skill/docs-dev/CLI.md +41 -11
  49. package/dist-skill/docs-dev/generated/viewport.md +3 -3
  50. package/dist-skill/library/forgecad-3d-reconstruction/SKILL.md +2 -2
  51. package/dist-skill/library/forgecad-make-a-model/SKILL.md +3 -3
  52. package/dist-skill/library/forgecad-reconstruction-benchmark/SKILL.md +3 -3
  53. package/package.json +1 -6
  54. package/dist/assets/forgecad_geometry_bg-DufhhCBV.wasm +0 -0
  55. package/dist/assets/solver_bg-DAHZJ_rw.wasm +0 -0
  56. /package/dist-cli/{check-compiler-LOXCPEOI.js.map → check-compiler-U5SOPN7X.js.map} +0 -0
  57. /package/dist-cli/{check-query-propagation-BAKNVWXR.js.map → check-query-propagation-XOKNSSYU.js.map} +0 -0
@@ -4,6 +4,7 @@ import {
4
4
  CLI_DEFAULT_BACKEND,
5
5
  COMPILER_REGRESSION_CORPUS,
6
6
  FILLET_EDGE_WORKFLOW_CODE,
7
+ MathUtils,
7
8
  OCCTUnsupportedError,
8
9
  Rectangle2D,
9
10
  Sculpt,
@@ -11,6 +12,7 @@ import {
11
12
  Shape,
12
13
  ShapeGroup,
13
14
  Transform,
15
+ Vector3,
14
16
  activateBackend,
15
17
  addPolygon,
16
18
  addRect,
@@ -119,7 +121,7 @@ import {
119
121
  union2d,
120
122
  updateConstraintValue,
121
123
  wrapOCCTShapeBackend
122
- } from "./chunk-RY43WF46.js";
124
+ } from "./chunk-EXWGNL6K.js";
123
125
 
124
126
  // cli/forgecad.ts
125
127
  import { Command, Flags, Help, flush as flushOclif, handle as handleOclif, run as runOclif } from "@oclif/core";
@@ -7141,7 +7143,7 @@ var RENDER_STYLES = new Set(RENDER_STYLE_NAMES);
7141
7143
  var RENDER_STYLE_LABEL = RENDER_STYLE_NAMES.join("|");
7142
7144
  var SCAN_GRANULARITY_MIN = 12;
7143
7145
  var SCAN_GRANULARITY_DEFAULT = 32;
7144
- var SCAN_GRANULARITY_MAX = 72;
7146
+ var SCAN_GRANULARITY_MAX = 144;
7145
7147
  var COMPARISON_VIEW_MODES = /* @__PURE__ */ new Set(["difference-only"]);
7146
7148
  function evidenceNameForChannel(channel) {
7147
7149
  return INSPECT_EVIDENCE_BY_CHANNEL.get(channel)?.name ?? channel;
@@ -19106,6 +19108,281 @@ async function runGcodeExportCli(argv) {
19106
19108
  console.log(` Bounds: [${tp.bounds.min.map((v) => v.toFixed(1)).join(", ")}] \u2192 [${tp.bounds.max.map((v) => v.toFixed(1)).join(", ")}]`);
19107
19109
  }
19108
19110
 
19111
+ // src/forge/targets.ts
19112
+ var cleanSceneTargetPathSegments = (segments) => (segments ?? []).map((segment) => segment.trim()).filter((segment) => segment.length > 0);
19113
+ function getSceneObjectTreePath(object) {
19114
+ const explicitTreePath = cleanSceneTargetPathSegments(object.treePath);
19115
+ if (explicitTreePath.length > 0) return explicitTreePath;
19116
+ const name = object.name.trim() || object.id;
19117
+ const groupName = object.groupName?.trim();
19118
+ if (!groupName) return [name];
19119
+ const groupPath = groupName.split(".").map((segment) => segment.trim()).filter((segment) => segment.length > 0);
19120
+ const prefixedLeaf = `${groupName}.`;
19121
+ if (name.startsWith(prefixedLeaf)) {
19122
+ const leafName = name.slice(prefixedLeaf.length).trim();
19123
+ return [...groupPath, leafName || name];
19124
+ }
19125
+ return [...groupPath, name];
19126
+ }
19127
+ function getSceneObjectKind(object) {
19128
+ if (object.mock) return "mock";
19129
+ if (object.sketch) return "sketch";
19130
+ if (object.toolpath) return "toolpath";
19131
+ if (object.sdf) return "sdf";
19132
+ if (object.shape) return "shape";
19133
+ return "object";
19134
+ }
19135
+ function buildSceneTargetEntries(objects) {
19136
+ return objects.map((object) => {
19137
+ const pathSegments = getSceneObjectTreePath(object);
19138
+ return {
19139
+ id: object.id,
19140
+ name: object.name,
19141
+ kind: getSceneObjectKind(object),
19142
+ pathSegments,
19143
+ path: pathSegments.join("/"),
19144
+ dottedPath: pathSegments.join("."),
19145
+ group: object.groupName?.trim() || void 0,
19146
+ tags: object.tags ?? [],
19147
+ mock: object.mock === true,
19148
+ object
19149
+ };
19150
+ });
19151
+ }
19152
+ function formatSceneTargetPath(entry) {
19153
+ return entry.path || entry.name;
19154
+ }
19155
+ function resolveSceneTargets(entries, selector) {
19156
+ const needle = normalizeTargetText(selector);
19157
+ if (!needle) throw new Error("Target selector must not be empty.");
19158
+ const hasGlob = /[*?]/.test(selector);
19159
+ if (hasGlob) {
19160
+ const pattern = globToRegExp(needle);
19161
+ const matches = entries.filter((entry) => targetCandidateTexts(entry).some((candidate) => pattern.test(candidate)));
19162
+ if (matches.length > 0) return matches;
19163
+ throw targetNotFound(selector, entries);
19164
+ }
19165
+ const idMatches = entries.filter((entry) => normalizeTargetText(entry.id) === needle);
19166
+ if (idMatches.length > 0) return idMatches;
19167
+ const exactPathMatches = entries.filter(
19168
+ (entry) => normalizeTargetText(entry.path) === needle || normalizeTargetText(entry.dottedPath) === needle
19169
+ );
19170
+ if (exactPathMatches.length > 0) return exactPathMatches;
19171
+ const groupMatches = entries.filter((entry) => {
19172
+ const groupPath = entry.pathSegments.slice(0, -1);
19173
+ return normalizeTargetText(groupPath.join("/")) === needle || normalizeTargetText(groupPath.join(".")) === needle;
19174
+ });
19175
+ if (groupMatches.length > 0) return groupMatches;
19176
+ const nameMatches2 = entries.filter(
19177
+ (entry) => normalizeTargetText(entry.name) === needle || normalizeTargetText(entry.pathSegments[entry.pathSegments.length - 1] ?? "") === needle
19178
+ );
19179
+ if (nameMatches2.length === 1) return nameMatches2;
19180
+ if (nameMatches2.length > 1) throw ambiguousTarget(selector, nameMatches2);
19181
+ throw targetNotFound(selector, entries);
19182
+ }
19183
+ function suggestSceneTargets(selector, entries, limit = 8) {
19184
+ const needle = normalizeTargetText(selector);
19185
+ if (!needle) return entries.slice(0, limit);
19186
+ return entries.map((entry) => ({ entry, score: scoreTarget(entry, needle) })).filter((ranked) => ranked.score > 0).sort((a, b) => b.score - a.score || a.entry.path.localeCompare(b.entry.path)).slice(0, limit).map((ranked) => ranked.entry);
19187
+ }
19188
+ function targetCandidateTexts(entry) {
19189
+ return [
19190
+ entry.id,
19191
+ entry.name,
19192
+ entry.path,
19193
+ entry.dottedPath,
19194
+ entry.group ?? "",
19195
+ entry.pathSegments[entry.pathSegments.length - 1] ?? ""
19196
+ ].map(normalizeTargetText);
19197
+ }
19198
+ function normalizeTargetText(value) {
19199
+ return value.trim().replace(/\\/g, "/").replace(/\s*\/\s*/g, "/").replace(/\s*\.\s*/g, ".").toLowerCase();
19200
+ }
19201
+ function globToRegExp(value) {
19202
+ const escaped = value.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
19203
+ return new RegExp(`^${escaped}$`, "i");
19204
+ }
19205
+ function scoreTarget(entry, needle) {
19206
+ const candidates = targetCandidateTexts(entry);
19207
+ if (candidates.some((candidate) => candidate === needle)) return 100;
19208
+ if (candidates.some((candidate) => candidate.startsWith(needle))) return 60;
19209
+ if (candidates.some((candidate) => candidate.includes(needle))) return 30;
19210
+ const tokens = needle.split(/[\s/._-]+/).filter(Boolean);
19211
+ if (tokens.length > 0 && tokens.every((token) => candidates.some((candidate) => candidate.includes(token)))) return 15;
19212
+ return 0;
19213
+ }
19214
+ function targetSummary(entry) {
19215
+ return `${formatSceneTargetPath(entry)} (${entry.kind}, ${entry.id})`;
19216
+ }
19217
+ function targetNotFound(selector, entries) {
19218
+ const suggestions = suggestSceneTargets(selector, entries);
19219
+ const detail = suggestions.length > 0 ? `
19220
+ Did you mean:
19221
+ ${suggestions.map((entry) => ` ${targetSummary(entry)}`).join("\n")}` : "";
19222
+ return new Error(`Target "${selector}" matched no scene objects.${detail}`);
19223
+ }
19224
+ function ambiguousTarget(selector, matches) {
19225
+ return new Error(
19226
+ `Target "${selector}" is ambiguous.
19227
+ Use a full path:
19228
+ ${matches.map((entry) => ` ${targetSummary(entry)}`).join("\n")}`
19229
+ );
19230
+ }
19231
+
19232
+ // cli/forge-ls.ts
19233
+ var TARGET_KINDS = ["shape", "sketch", "sdf", "toolpath", "mock", "object"];
19234
+ function usage12() {
19235
+ return [
19236
+ "Usage: forgecad ls <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [target] [--tree] [--long] [--json] [--kind shape,sketch,mock] [--param Key=Value] [--backend manifold|occt|truck] [--quality live|default|high]"
19237
+ ].join("\n");
19238
+ }
19239
+ function parseKindFilter(raw) {
19240
+ const values = raw.split(",").map((value) => value.trim()).filter(Boolean);
19241
+ const kinds = /* @__PURE__ */ new Set();
19242
+ for (const value of values) {
19243
+ if (!TARGET_KINDS.includes(value)) {
19244
+ throw new Error(`Invalid --kind value: ${value}. Expected one of ${TARGET_KINDS.join(", ")}.`);
19245
+ }
19246
+ kinds.add(value);
19247
+ }
19248
+ if (kinds.size === 0) throw new Error("--kind requires at least one kind.");
19249
+ return kinds;
19250
+ }
19251
+ function parseLsOptions(argv) {
19252
+ if (argv.length === 0 || argv.includes("-h") || argv.includes("--help")) {
19253
+ console.log(usage12());
19254
+ process.exit(0);
19255
+ }
19256
+ const { overrides: _paramOverrides, consumed: paramConsumed } = parseParamFlags(argv);
19257
+ const { backend, consumed: backendConsumed } = parseBackendArg(argv);
19258
+ const { quality, consumed: qualityConsumed } = parseQualityArg(argv);
19259
+ const consumed = /* @__PURE__ */ new Set([...paramConsumed, ...backendConsumed, ...qualityConsumed]);
19260
+ let json = false;
19261
+ let compact = false;
19262
+ let tree = false;
19263
+ let long = false;
19264
+ let kinds = null;
19265
+ for (let i = 0; i < argv.length; i += 1) {
19266
+ if (consumed.has(i)) continue;
19267
+ const arg = argv[i];
19268
+ if (arg === "--json") {
19269
+ json = true;
19270
+ consumed.add(i);
19271
+ } else if (arg === "--compact") {
19272
+ compact = true;
19273
+ consumed.add(i);
19274
+ } else if (arg === "--tree") {
19275
+ tree = true;
19276
+ consumed.add(i);
19277
+ } else if (arg === "--long") {
19278
+ long = true;
19279
+ consumed.add(i);
19280
+ } else if (arg === "--kind") {
19281
+ const raw = argv[i + 1];
19282
+ if (!raw || raw.startsWith("--")) throw new Error("--kind requires a comma-separated value.");
19283
+ kinds = parseKindFilter(raw);
19284
+ consumed.add(i);
19285
+ consumed.add(i + 1);
19286
+ i += 1;
19287
+ } else if (arg.startsWith("--")) {
19288
+ throw new Error(`Unknown option: ${arg}`);
19289
+ }
19290
+ }
19291
+ const positionals = argv.filter((_, index) => !consumed.has(index));
19292
+ if (!positionals[0]) throw new Error("Missing input path.");
19293
+ if (positionals.length > 2) throw new Error(`Unexpected argument: ${positionals[2]}`);
19294
+ return { scriptPath: positionals[0], selector: positionals[1], json, compact, tree, long, kinds, backend, quality, consumed };
19295
+ }
19296
+ function filteredTargets(entries, options) {
19297
+ let targets = options.selector ? resolveSceneTargets(entries, options.selector) : entries;
19298
+ if (options.kinds) targets = targets.filter((entry) => options.kinds?.has(entry.kind));
19299
+ return targets;
19300
+ }
19301
+ function formatNumber(value, digits = 1) {
19302
+ if (!Number.isFinite(value)) return String(value);
19303
+ return value.toFixed(digits).replace(/\.0$/, "");
19304
+ }
19305
+ function formatBounds(min, max) {
19306
+ return `[${min.map((v) => formatNumber(v)).join(",")}]..[${max.map((v) => formatNumber(v)).join(",")}]`;
19307
+ }
19308
+ function targetMetrics(entry) {
19309
+ const object = entry.object;
19310
+ if (object.shape) {
19311
+ const bounds2 = object.shape.boundingBox();
19312
+ return {
19313
+ volume: object.shape.volume(),
19314
+ triangles: object.shape.numTri(),
19315
+ bodies: object.shape.numBodies(),
19316
+ bounds: { min: bounds2.min, max: bounds2.max }
19317
+ };
19318
+ }
19319
+ if (object.sketch) return { area: object.sketch.area(), regions: object.sketch.regions().length };
19320
+ if (object.sdf) return { bounds: object.sdf.bounds };
19321
+ return null;
19322
+ }
19323
+ function targetJson(entry, includeMetrics) {
19324
+ return {
19325
+ id: entry.id,
19326
+ path: entry.path,
19327
+ name: entry.name,
19328
+ kind: entry.kind,
19329
+ group: entry.group ?? null,
19330
+ tags: entry.tags,
19331
+ mock: entry.mock,
19332
+ ...includeMetrics ? { metrics: targetMetrics(entry) } : {}
19333
+ };
19334
+ }
19335
+ function printLineList(entries, long) {
19336
+ for (const entry of entries) {
19337
+ const base = `${entry.kind.padEnd(8)} ${formatSceneTargetPath(entry)}`;
19338
+ if (!long) {
19339
+ console.log(base);
19340
+ continue;
19341
+ }
19342
+ const metrics = targetMetrics(entry);
19343
+ const detail = metrics && "volume" in metrics ? ` id=${entry.id} vol=${formatNumber(metrics.volume)} tris=${metrics.triangles.toLocaleString()} bodies=${metrics.bodies} bbox=${formatBounds(metrics.bounds.min, metrics.bounds.max)}` : metrics && "area" in metrics ? ` id=${entry.id} area=${formatNumber(metrics.area)} regions=${metrics.regions}` : ` id=${entry.id}`;
19344
+ console.log(`${base}${detail}`);
19345
+ }
19346
+ }
19347
+ function printTree(entries) {
19348
+ const printedGroups = /* @__PURE__ */ new Set();
19349
+ for (const entry of entries) {
19350
+ for (let depth = 1; depth < entry.pathSegments.length; depth += 1) {
19351
+ const groupPath = entry.pathSegments.slice(0, depth).join("/");
19352
+ if (printedGroups.has(groupPath)) continue;
19353
+ printedGroups.add(groupPath);
19354
+ console.log(`${" ".repeat(depth - 1)}${entry.pathSegments[depth - 1]}/`);
19355
+ }
19356
+ const indent = " ".repeat(Math.max(0, entry.pathSegments.length - 1));
19357
+ const label = entry.pathSegments[entry.pathSegments.length - 1] ?? entry.name;
19358
+ console.log(`${indent}${entry.kind.padEnd(8)} ${label}`);
19359
+ }
19360
+ }
19361
+ async function runLsCli(argv = process.argv.slice(2)) {
19362
+ let options;
19363
+ try {
19364
+ options = parseLsOptions(argv);
19365
+ } catch (error) {
19366
+ console.error(error instanceof Error ? error.message : String(error));
19367
+ console.error(usage12());
19368
+ process.exit(1);
19369
+ }
19370
+ const { overrides } = parseParamFlags(argv);
19371
+ const result = await runModelForInspect(options.scriptPath, options.backend, options.quality, overrides);
19372
+ if (result.error) {
19373
+ console.error("ERROR:", result.error);
19374
+ process.exit(1);
19375
+ }
19376
+ const entries = filteredTargets(buildSceneTargetEntries(result.objects), options);
19377
+ if (options.json) {
19378
+ const body = { script: options.scriptPath, count: entries.length, targets: entries.map((entry) => targetJson(entry, options.long)) };
19379
+ console.log(JSON.stringify(body, null, options.compact ? 0 : 2));
19380
+ return;
19381
+ }
19382
+ if (options.tree) printTree(entries);
19383
+ else printLineList(entries, options.long);
19384
+ }
19385
+
19109
19386
  // cli/forge-mesh.ts
19110
19387
  import { writeFileSync as writeFileSync7 } from "fs";
19111
19388
  import { extname as extname6, resolve as resolve20 } from "path";
@@ -19409,6 +19686,74 @@ import { execSync as execSync2, spawnSync } from "child_process";
19409
19686
  import { tmpdir as tmpdir2 } from "os";
19410
19687
  import { fileURLToPath as fileURLToPath2 } from "url";
19411
19688
 
19689
+ // cli/framing-diagnostics.ts
19690
+ var DEFAULT_PERSPECTIVE_FIT_MARGIN = 1.1;
19691
+ var DEFAULT_MIN_CAMERA_DISTANCE = 1;
19692
+ function bboxCorners(bbox) {
19693
+ const [minX, minY, minZ] = bbox.min;
19694
+ const [maxX, maxY, maxZ] = bbox.max;
19695
+ return [
19696
+ new Vector3(minX, minY, minZ),
19697
+ new Vector3(minX, minY, maxZ),
19698
+ new Vector3(minX, maxY, minZ),
19699
+ new Vector3(minX, maxY, maxZ),
19700
+ new Vector3(maxX, minY, minZ),
19701
+ new Vector3(maxX, minY, maxZ),
19702
+ new Vector3(maxX, maxY, minZ),
19703
+ new Vector3(maxX, maxY, maxZ)
19704
+ ];
19705
+ }
19706
+ function bboxCenter(bbox) {
19707
+ return new Vector3((bbox.min[0] + bbox.max[0]) * 0.5, (bbox.min[1] + bbox.max[1]) * 0.5, (bbox.min[2] + bbox.max[2]) * 0.5);
19708
+ }
19709
+ function finitePositive(value, label) {
19710
+ if (!Number.isFinite(value) || value <= 0) {
19711
+ throw new Error(`${label} must be a positive finite number.`);
19712
+ }
19713
+ return value;
19714
+ }
19715
+ function cameraRightFor(backward, up) {
19716
+ const normalizedUp = up.lengthSq() > 1e-8 ? up.clone().normalize() : new Vector3(0, 0, 1);
19717
+ let right = new Vector3().crossVectors(normalizedUp, backward);
19718
+ if (right.lengthSq() > 1e-8) return right.normalize();
19719
+ const fallbackUp = Math.abs(backward.z) > 0.92 ? new Vector3(0, 1, 0) : new Vector3(0, 0, 1);
19720
+ right = new Vector3().crossVectors(fallbackUp, backward);
19721
+ if (right.lengthSq() > 1e-8) return right.normalize();
19722
+ return new Vector3(1, 0, 0);
19723
+ }
19724
+ function perspectiveCameraDistanceForBounds(bbox, directionFromTargetToCamera, options) {
19725
+ const backward = new Vector3(...directionFromTargetToCamera);
19726
+ if (backward.lengthSq() <= 1e-8) {
19727
+ throw new Error("directionFromTargetToCamera must be a non-zero vector.");
19728
+ }
19729
+ backward.normalize();
19730
+ const fovDeg = finitePositive(options.fovDeg, "fovDeg");
19731
+ if (fovDeg >= 180) {
19732
+ throw new Error("fovDeg must be less than 180 degrees.");
19733
+ }
19734
+ const aspect = finitePositive(options.aspect ?? 1, "aspect");
19735
+ const margin = finitePositive(options.margin ?? DEFAULT_PERSPECTIVE_FIT_MARGIN, "margin");
19736
+ const minDistance = finitePositive(options.minDistance ?? DEFAULT_MIN_CAMERA_DISTANCE, "minDistance");
19737
+ const tanHalfVerticalFov = Math.tan(MathUtils.degToRad(fovDeg) * 0.5);
19738
+ const tanHalfHorizontalFov = tanHalfVerticalFov * aspect;
19739
+ const right = cameraRightFor(backward, new Vector3(...options.up ?? [0, 0, 1]));
19740
+ const screenUp = new Vector3().crossVectors(backward, right).normalize();
19741
+ const center = bboxCenter(bbox);
19742
+ let requiredDistance = minDistance;
19743
+ for (const corner of bboxCorners(bbox)) {
19744
+ const offset = corner.sub(center);
19745
+ const depthOffset = offset.dot(backward);
19746
+ const horizontalOffset = Math.abs(offset.dot(right));
19747
+ const verticalOffset = Math.abs(offset.dot(screenUp));
19748
+ requiredDistance = Math.max(
19749
+ requiredDistance,
19750
+ horizontalOffset * margin / tanHalfHorizontalFov + depthOffset,
19751
+ verticalOffset * margin / tanHalfVerticalFov + depthOffset
19752
+ );
19753
+ }
19754
+ return requiredDistance;
19755
+ }
19756
+
19412
19757
  // cli/render-camera-tokens.ts
19413
19758
  var CAMERA_TOKEN_DIRECTIONS = {
19414
19759
  front: [0, -1, 0.2],
@@ -19442,17 +19787,13 @@ function parseCameraToken(token) {
19442
19787
  `Unknown camera "${token}". Use a preset (front, back, side, right, top, iso) or azimuth:elevation in degrees (e.g. 45:30).`
19443
19788
  );
19444
19789
  }
19445
- function cameraStateFromToken(token, center, defaultDistance, fov) {
19790
+ function cameraStateFromToken(token, center, defaultDistance, fov, bounds2) {
19446
19791
  const parsed = parseCameraToken(token);
19447
19792
  const dir = normalizeCameraDirection(parsed.dir);
19448
- const distance2 = parsed.distance ?? defaultDistance;
19793
+ const distance2 = parsed.distance ?? (bounds2 ? perspectiveCameraDistanceForBounds(bounds2, dir, { fovDeg: fov }) : defaultDistance);
19449
19794
  return {
19450
19795
  projectionMode: "perspective",
19451
- position: [
19452
- center[0] + dir[0] * distance2,
19453
- center[1] + dir[1] * distance2,
19454
- center[2] + dir[2] * distance2
19455
- ],
19796
+ position: [center[0] + dir[0] * distance2, center[1] + dir[1] * distance2, center[2] + dir[2] * distance2],
19456
19797
  target: center,
19457
19798
  up: [0, 0, 1],
19458
19799
  fov
@@ -19710,17 +20051,17 @@ function computeMeshCameraFrame(objects) {
19710
20051
  const min = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY];
19711
20052
  const max = [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY];
19712
20053
  for (const obj of objects) {
19713
- const bbox = obj.shape.boundingBox();
20054
+ const bbox2 = obj.shape.boundingBox();
19714
20055
  for (let axis = 0; axis < 3; axis += 1) {
19715
- min[axis] = Math.min(min[axis], bbox.min[axis]);
19716
- max[axis] = Math.max(max[axis], bbox.max[axis]);
20056
+ min[axis] = Math.min(min[axis], bbox2.min[axis]);
20057
+ max[axis] = Math.max(max[axis], bbox2.max[axis]);
19717
20058
  }
19718
20059
  }
19719
20060
  const center = [(min[0] + max[0]) * 0.5, (min[1] + max[1]) * 0.5, (min[2] + max[2]) * 0.5];
19720
- const maxDim = Math.max(1, max[0] - min[0], max[1] - min[1], max[2] - min[2]);
19721
20061
  const fov = 45;
19722
- const distance2 = maxDim / (2 * Math.tan(fov * Math.PI / 360)) * 1.6;
19723
- return { center, distance: distance2, fov };
20062
+ const bbox = { min, max };
20063
+ const distance2 = perspectiveCameraDistanceForBounds(bbox, [0, -1, 0.32], { fovDeg: fov });
20064
+ return { center, distance: distance2, fov, bbox };
19724
20065
  }
19725
20066
  function sceneConfigCameraToViewportCamera(sceneConfig, frame) {
19726
20067
  const camera = sceneConfig.camera;
@@ -19737,7 +20078,10 @@ function resolveHqCamera(args, sceneConfig, meshObjects) {
19737
20078
  const frame = computeMeshCameraFrame(meshObjects);
19738
20079
  if (args.scene?.camera) return args.scene.camera;
19739
20080
  if (args.viewName) return resolveNamedSceneViewCamera(sceneConfig, args.viewName);
19740
- if (args.cameraToken) return cameraStateFromToken(args.cameraToken, frame.center, frame.distance, sceneConfig?.camera?.fov ?? frame.fov);
20081
+ if (args.cameraToken) {
20082
+ const fov = sceneConfig?.camera?.fov ?? frame.fov;
20083
+ return cameraStateFromToken(args.cameraToken, frame.center, frame.distance, fov, frame.bbox);
20084
+ }
19741
20085
  if (sceneConfig) return sceneConfigCameraToViewportCamera(sceneConfig, frame);
19742
20086
  return null;
19743
20087
  }
@@ -19881,12 +20225,12 @@ import { basename as basename9, resolve as resolve23 } from "path";
19881
20225
  function defaultPngOutput(scriptPath) {
19882
20226
  return scriptPath.replace(/\.(forge|sketch)\.js$/, ".png").replace(/\.js$/, ".png");
19883
20227
  }
19884
- function usage12() {
20228
+ function usage13() {
19885
20229
  console.error("Usage: forgecad render sketch <script.forge.js> [output.png] [--size <px>] [--chrome-path <path>]");
19886
20230
  process.exit(1);
19887
20231
  }
19888
20232
  function parseCli2(argv) {
19889
- if (argv.length === 0 || argv.includes("-h") || argv.includes("--help")) usage12();
20233
+ if (argv.length === 0 || argv.includes("-h") || argv.includes("--help")) usage13();
19890
20234
  let scriptPath;
19891
20235
  let outputPath;
19892
20236
  let size = 1024;
@@ -19911,7 +20255,7 @@ function parseCli2(argv) {
19911
20255
  else if (!outputPath) outputPath = arg;
19912
20256
  else throw new Error(`Unexpected argument: ${arg}`);
19913
20257
  }
19914
- if (!scriptPath) usage12();
20258
+ if (!scriptPath) usage13();
19915
20259
  if (!Number.isFinite(size) || size < 128 || size > 4096) throw new Error(`--size must be between 128 and 4096 (got ${size})`);
19916
20260
  return {
19917
20261
  scriptPath,
@@ -19927,7 +20271,7 @@ async function runRender2dCli(argv = process.argv.slice(2)) {
19927
20271
  options = parseCli2(argv);
19928
20272
  } catch (err) {
19929
20273
  console.error(String(err));
19930
- usage12();
20274
+ usage13();
19931
20275
  }
19932
20276
  const chromePath = findChromePath(options.chromePath);
19933
20277
  if (!chromePath) {
@@ -20046,13 +20390,13 @@ function formatSheetCutList(input) {
20046
20390
  }
20047
20391
 
20048
20392
  // cli/forge-cut-list.ts
20049
- function usage13() {
20393
+ function usage14() {
20050
20394
  console.error("Usage: forgecad cut-list <script.forge.js>");
20051
20395
  process.exit(1);
20052
20396
  }
20053
20397
  async function runCutListCli(argv = process.argv.slice(2)) {
20054
20398
  const scriptPath = argv[0];
20055
- if (!scriptPath) usage13();
20399
+ if (!scriptPath) usage14();
20056
20400
  const source = await readFile2(resolve24(scriptPath), "utf-8");
20057
20401
  const { allFiles, fileName } = collectProjectFiles(scriptPath);
20058
20402
  await init();
@@ -20085,7 +20429,7 @@ function hasFlag(argv, name) {
20085
20429
  const prefix = `${name}=`;
20086
20430
  return argv.some((arg) => arg === name || arg.startsWith(prefix));
20087
20431
  }
20088
- function usage14() {
20432
+ function usage15() {
20089
20433
  console.error(
20090
20434
  "Usage: forgecad export cutting-layout <script.forge.js> [output.pdf|output.dxf]\n [--format pdf|dxf] [--sheet-width <mm>] [--sheet-height <mm>] [--kerf <mm>]"
20091
20435
  );
@@ -20149,7 +20493,7 @@ function smartDefaults(entries) {
20149
20493
  }
20150
20494
  async function runCuttingLayoutCli(argv = process.argv.slice(2)) {
20151
20495
  const scriptPath = argv[0];
20152
- if (!scriptPath) usage14();
20496
+ if (!scriptPath) usage15();
20153
20497
  const explicitOutputPath = outputPathArg(argv);
20154
20498
  const formatArg = argValue(argv, "--format");
20155
20499
  if (hasFlag(argv, "--format") && formatArg == null) {
@@ -20212,7 +20556,7 @@ function argValue2(argv, name) {
20212
20556
  if (idx === -1) return void 0;
20213
20557
  return argv[idx + 1];
20214
20558
  }
20215
- function usage15() {
20559
+ function usage16() {
20216
20560
  console.error(
20217
20561
  "Usage: forgecad export report <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [output.pdf] [--dim-angle-tol <deg>]"
20218
20562
  );
@@ -20220,7 +20564,7 @@ function usage15() {
20220
20564
  }
20221
20565
  async function runReportCli(argv = process.argv.slice(2)) {
20222
20566
  const scriptPath = argv[0];
20223
- if (!scriptPath) usage15();
20567
+ if (!scriptPath) usage16();
20224
20568
  const defaultOut = replaceCliInputExtension(scriptPath, ".report.pdf");
20225
20569
  const outputPath = argv[1] && !argv[1].startsWith("--") ? argv[1] : defaultOut;
20226
20570
  const toleranceArg = argValue2(argv, "--dim-angle-tol");
@@ -20284,16 +20628,16 @@ function mmToM(valueMm) {
20284
20628
  function degToRad(valueDeg) {
20285
20629
  return valueDeg * Math.PI / 180;
20286
20630
  }
20287
- function formatNumber(value, digits = 6) {
20631
+ function formatNumber2(value, digits = 6) {
20288
20632
  if (!Number.isFinite(value)) return "0";
20289
20633
  const normalized = Math.abs(value) < 1e-12 ? 0 : value;
20290
20634
  return normalized.toFixed(digits).replace(/\.?0+$/, "");
20291
20635
  }
20292
20636
  function formatPose(parts) {
20293
- return [...parts.xyzM.map((value) => formatNumber(value, 6)), ...parts.rpyRad.map((value) => formatNumber(value, 6))].join(" ");
20637
+ return [...parts.xyzM.map((value) => formatNumber2(value, 6)), ...parts.rpyRad.map((value) => formatNumber2(value, 6))].join(" ");
20294
20638
  }
20295
20639
  function axisToText(axis) {
20296
- return axis.map((value) => formatNumber(value, 6)).join(" ");
20640
+ return axis.map((value) => formatNumber2(value, 6)).join(" ");
20297
20641
  }
20298
20642
  function transformToPose(transform) {
20299
20643
  const m = transform.toArray();
@@ -20439,15 +20783,15 @@ function inertiaFromBounds(geometry, massKg) {
20439
20783
  }
20440
20784
  function jointTypeLimitUnits(joint2, value) {
20441
20785
  if (value === void 0) return null;
20442
- if (joint2.type === "revolute") return formatNumber(degToRad(value), 6);
20443
- if (joint2.type === "prismatic") return formatNumber(mmToM(value), 6);
20786
+ if (joint2.type === "revolute") return formatNumber2(degToRad(value), 6);
20787
+ if (joint2.type === "prismatic") return formatNumber2(mmToM(value), 6);
20444
20788
  return null;
20445
20789
  }
20446
20790
  function jointVelocityUnits(joint2, value) {
20447
20791
  if (value === void 0) return null;
20448
- if (joint2.type === "revolute") return formatNumber(degToRad(value), 6);
20449
- if (joint2.type === "prismatic") return formatNumber(mmToM(value), 6);
20450
- return formatNumber(value, 6);
20792
+ if (joint2.type === "revolute") return formatNumber2(degToRad(value), 6);
20793
+ if (joint2.type === "prismatic") return formatNumber2(mmToM(value), 6);
20794
+ return formatNumber2(value, 6);
20451
20795
  }
20452
20796
  function sRgbFloat(hex) {
20453
20797
  if (!hex || !/^#([0-9a-f]{6})$/i.test(hex)) return null;
@@ -20476,14 +20820,14 @@ function demoWorldName(world, modelName) {
20476
20820
  }
20477
20821
  function keyboardPluginXml(cmdVelTopic, linearStep, angularStep) {
20478
20822
  const bindings = [
20479
- { key: 87, twist: `linear: {x: ${formatNumber(linearStep, 3)}}, angular: {z: 0.0}` },
20480
- { key: 88, twist: `linear: {x: ${formatNumber(-linearStep, 3)}}, angular: {z: 0.0}` },
20481
- { key: 65, twist: `linear: {x: 0.0}, angular: {z: ${formatNumber(angularStep, 3)}}` },
20482
- { key: 68, twist: `linear: {x: 0.0}, angular: {z: ${formatNumber(-angularStep, 3)}}` },
20483
- { key: 81, twist: `linear: {x: ${formatNumber(linearStep, 3)}}, angular: {z: ${formatNumber(angularStep, 3)}}` },
20484
- { key: 69, twist: `linear: {x: ${formatNumber(linearStep, 3)}}, angular: {z: ${formatNumber(-angularStep, 3)}}` },
20485
- { key: 90, twist: `linear: {x: ${formatNumber(-linearStep, 3)}}, angular: {z: ${formatNumber(angularStep, 3)}}` },
20486
- { key: 67, twist: `linear: {x: ${formatNumber(-linearStep, 3)}}, angular: {z: ${formatNumber(-angularStep, 3)}}` },
20823
+ { key: 87, twist: `linear: {x: ${formatNumber2(linearStep, 3)}}, angular: {z: 0.0}` },
20824
+ { key: 88, twist: `linear: {x: ${formatNumber2(-linearStep, 3)}}, angular: {z: 0.0}` },
20825
+ { key: 65, twist: `linear: {x: 0.0}, angular: {z: ${formatNumber2(angularStep, 3)}}` },
20826
+ { key: 68, twist: `linear: {x: 0.0}, angular: {z: ${formatNumber2(-angularStep, 3)}}` },
20827
+ { key: 81, twist: `linear: {x: ${formatNumber2(linearStep, 3)}}, angular: {z: ${formatNumber2(angularStep, 3)}}` },
20828
+ { key: 69, twist: `linear: {x: ${formatNumber2(linearStep, 3)}}, angular: {z: ${formatNumber2(-angularStep, 3)}}` },
20829
+ { key: 90, twist: `linear: {x: ${formatNumber2(-linearStep, 3)}}, angular: {z: ${formatNumber2(angularStep, 3)}}` },
20830
+ { key: 67, twist: `linear: {x: ${formatNumber2(-linearStep, 3)}}, angular: {z: ${formatNumber2(-angularStep, 3)}}` },
20487
20831
  { key: 83, twist: "linear: {x: 0.0}, angular: {z: 0.0}" },
20488
20832
  { key: 32, twist: "linear: {x: 0.0}, angular: {z: 0.0}" }
20489
20833
  ];
@@ -20643,12 +20987,12 @@ ${keyboardGuiPluginXml()}` : "";
20643
20987
  function demoWorldXml(worldName, modelName, cmdVelTopic, world) {
20644
20988
  const spawnPose = world?.spawnPose ?? [0, 0, 120, 0, 0, 0];
20645
20989
  const spawnPoseText = [
20646
- formatNumber(mmToM(spawnPose[0]), 6),
20647
- formatNumber(mmToM(spawnPose[1]), 6),
20648
- formatNumber(mmToM(spawnPose[2]), 6),
20649
- formatNumber(degToRad(spawnPose[3]), 6),
20650
- formatNumber(degToRad(spawnPose[4]), 6),
20651
- formatNumber(degToRad(spawnPose[5]), 6)
20990
+ formatNumber2(mmToM(spawnPose[0]), 6),
20991
+ formatNumber2(mmToM(spawnPose[1]), 6),
20992
+ formatNumber2(mmToM(spawnPose[2]), 6),
20993
+ formatNumber2(degToRad(spawnPose[3]), 6),
20994
+ formatNumber2(degToRad(spawnPose[4]), 6),
20995
+ formatNumber2(degToRad(spawnPose[5]), 6)
20652
20996
  ].join(" ");
20653
20997
  const keyboardEnabled = world?.keyboardTeleop?.enabled ?? true;
20654
20998
  const linearStep = world?.keyboardTeleop?.linearStep ?? 0.9;
@@ -20818,28 +21162,28 @@ function modelXml(spec, modelName, linkNameMap, jointNameMap, geometries, linkWo
20818
21162
  const color = sRgbFloat(geometry.shapes[0]?.colorHex);
20819
21163
  const materialXml = color ? `
20820
21164
  <material>
20821
- <ambient>${formatNumber(color[0], 3)} ${formatNumber(color[1], 3)} ${formatNumber(color[2], 3)} 1</ambient>
20822
- <diffuse>${formatNumber(color[0], 3)} ${formatNumber(color[1], 3)} ${formatNumber(color[2], 3)} 1</diffuse>
21165
+ <ambient>${formatNumber2(color[0], 3)} ${formatNumber2(color[1], 3)} ${formatNumber2(color[2], 3)} 1</ambient>
21166
+ <diffuse>${formatNumber2(color[0], 3)} ${formatNumber2(color[1], 3)} ${formatNumber2(color[2], 3)} 1</diffuse>
20823
21167
  </material>` : "";
20824
21168
  return ` <link name="${escapeXml(sdfLinkName)}">
20825
21169
  <pose relative_to="__model__">${formatPose(worldPose)}</pose>
20826
21170
  <inertial>
20827
21171
  <pose>${formatPose(inertia.pose)}</pose>
20828
- <mass>${formatNumber(massKg, 6)}</mass>
21172
+ <mass>${formatNumber2(massKg, 6)}</mass>
20829
21173
  <inertia>
20830
- <ixx>${formatNumber(inertia.ixx, 8)}</ixx>
20831
- <ixy>${formatNumber(inertia.ixy, 8)}</ixy>
20832
- <ixz>${formatNumber(inertia.ixz, 8)}</ixz>
20833
- <iyy>${formatNumber(inertia.iyy, 8)}</iyy>
20834
- <iyz>${formatNumber(inertia.iyz, 8)}</iyz>
20835
- <izz>${formatNumber(inertia.izz, 8)}</izz>
21174
+ <ixx>${formatNumber2(inertia.ixx, 8)}</ixx>
21175
+ <ixy>${formatNumber2(inertia.ixy, 8)}</ixy>
21176
+ <ixz>${formatNumber2(inertia.ixz, 8)}</ixz>
21177
+ <iyy>${formatNumber2(inertia.iyy, 8)}</iyy>
21178
+ <iyz>${formatNumber2(inertia.iyz, 8)}</iyz>
21179
+ <izz>${formatNumber2(inertia.izz, 8)}</izz>
20836
21180
  </inertia>
20837
21181
  </inertial>
20838
21182
  <visual name="${escapeXml(sdfLinkName)}_visual">
20839
21183
  <geometry>
20840
21184
  <mesh>
20841
21185
  <uri>${escapeXml(meshPath)}</uri>
20842
- <scale>${formatNumber(STL_SCALE_METERS, 6)} ${formatNumber(STL_SCALE_METERS, 6)} ${formatNumber(STL_SCALE_METERS, 6)}</scale>
21186
+ <scale>${formatNumber2(STL_SCALE_METERS, 6)} ${formatNumber2(STL_SCALE_METERS, 6)} ${formatNumber2(STL_SCALE_METERS, 6)}</scale>
20843
21187
  </mesh>
20844
21188
  </geometry>${materialXml}
20845
21189
  </visual>${(() => {
@@ -20849,7 +21193,7 @@ function modelXml(spec, modelName, linkNameMap, jointNameMap, geometries, linkWo
20849
21193
  <geometry>
20850
21194
  <mesh>
20851
21195
  <uri>${escapeXml(meshPath)}</uri>
20852
- <scale>${formatNumber(STL_SCALE_METERS, 6)} ${formatNumber(STL_SCALE_METERS, 6)} ${formatNumber(STL_SCALE_METERS, 6)}</scale>
21196
+ <scale>${formatNumber2(STL_SCALE_METERS, 6)} ${formatNumber2(STL_SCALE_METERS, 6)} ${formatNumber2(STL_SCALE_METERS, 6)}</scale>
20853
21197
  </mesh>
20854
21198
  </geometry>
20855
21199
  </collision>`;
@@ -20861,7 +21205,7 @@ function modelXml(spec, modelName, linkNameMap, jointNameMap, geometries, linkWo
20861
21205
  <geometry>
20862
21206
  <mesh>
20863
21207
  <uri>${escapeXml(collisionMeshPath)}</uri>
20864
- <scale>${formatNumber(STL_SCALE_METERS, 6)} ${formatNumber(STL_SCALE_METERS, 6)} ${formatNumber(STL_SCALE_METERS, 6)}</scale>
21208
+ <scale>${formatNumber2(STL_SCALE_METERS, 6)} ${formatNumber2(STL_SCALE_METERS, 6)} ${formatNumber2(STL_SCALE_METERS, 6)}</scale>
20865
21209
  </mesh>
20866
21210
  </geometry>
20867
21211
  </collision>`;
@@ -20875,9 +21219,9 @@ function modelXml(spec, modelName, linkNameMap, jointNameMap, geometries, linkWo
20875
21219
  const bCz = mmToM((geometry.bboxMin[2] + geometry.bboxMax[2]) * 0.5);
20876
21220
  return `
20877
21221
  <collision name="${escapeXml(sdfLinkName)}_collision">
20878
- <pose>${formatNumber(bCx, 6)} ${formatNumber(bCy, 6)} ${formatNumber(bCz, 6)} 0 0 0</pose>
21222
+ <pose>${formatNumber2(bCx, 6)} ${formatNumber2(bCy, 6)} ${formatNumber2(bCz, 6)} 0 0 0</pose>
20879
21223
  <geometry>
20880
- <box><size>${formatNumber(bDx, 6)} ${formatNumber(bDy, 6)} ${formatNumber(bDz, 6)}</size></box>
21224
+ <box><size>${formatNumber2(bDx, 6)} ${formatNumber2(bDy, 6)} ${formatNumber2(bDz, 6)}</size></box>
20881
21225
  </geometry>
20882
21226
  </collision>`;
20883
21227
  }
@@ -20904,8 +21248,8 @@ function modelXml(spec, modelName, linkNameMap, jointNameMap, geometries, linkWo
20904
21248
  const leaderSdfName = jointNameMap.get(`${primary.joint}_joint`);
20905
21249
  mimicXml = `
20906
21250
  <mimic joint="${escapeXml(leaderSdfName)}">
20907
- <multiplier>${formatNumber(primary.ratio, 6)}</multiplier>
20908
- <offset>${formatNumber(coupling.offset, 6)}</offset>
21251
+ <multiplier>${formatNumber2(primary.ratio, 6)}</multiplier>
21252
+ <offset>${formatNumber2(coupling.offset, 6)}</offset>
20909
21253
  </mimic>`;
20910
21254
  if (coupling.terms.length > 1) {
20911
21255
  warnings.push(
@@ -20922,12 +21266,12 @@ function modelXml(spec, modelName, linkNameMap, jointNameMap, geometries, linkWo
20922
21266
  <limit>${limitLower !== null ? `
20923
21267
  <lower>${limitLower}</lower>` : ""}${limitUpper !== null ? `
20924
21268
  <upper>${limitUpper}</upper>` : ""}${effort !== void 0 ? `
20925
- <effort>${formatNumber(effort, 6)}</effort>` : ""}${velocity !== null ? `
21269
+ <effort>${formatNumber2(effort, 6)}</effort>` : ""}${velocity !== null ? `
20926
21270
  <velocity>${velocity}</velocity>` : ""}
20927
21271
  </limit>` : ""}${damping !== void 0 || friction !== void 0 ? `
20928
21272
  <dynamics>${damping !== void 0 ? `
20929
- <damping>${formatNumber(damping, 6)}</damping>` : ""}${friction !== void 0 ? `
20930
- <friction>${formatNumber(friction, 6)}</friction>` : ""}
21273
+ <damping>${formatNumber2(damping, 6)}</damping>` : ""}${friction !== void 0 ? `
21274
+ <friction>${formatNumber2(friction, 6)}</friction>` : ""}
20931
21275
  </dynamics>` : ""}
20932
21276
  </axis>` : ""}
20933
21277
  </joint>`;
@@ -20937,17 +21281,17 @@ function modelXml(spec, modelName, linkNameMap, jointNameMap, geometries, linkWo
20937
21281
  plugins.push(` <plugin filename="gz-sim-diff-drive-system" name="gz::sim::systems::DiffDrive">
20938
21282
  ${spec.plugins.diffDrive.leftJoints.map((jointName) => `<left_joint>${escapeXml(jointNameMap.get(`${jointName}_joint`))}</left_joint>`).join("\n ")}
20939
21283
  ${spec.plugins.diffDrive.rightJoints.map((jointName) => `<right_joint>${escapeXml(jointNameMap.get(`${jointName}_joint`))}</right_joint>`).join("\n ")}
20940
- <wheel_separation>${formatNumber(mmToM(spec.plugins.diffDrive.wheelSeparationMm), 6)}</wheel_separation>
20941
- <wheel_radius>${formatNumber(mmToM(spec.plugins.diffDrive.wheelRadiusMm), 6)}</wheel_radius>
21284
+ <wheel_separation>${formatNumber2(mmToM(spec.plugins.diffDrive.wheelSeparationMm), 6)}</wheel_separation>
21285
+ <wheel_radius>${formatNumber2(mmToM(spec.plugins.diffDrive.wheelRadiusMm), 6)}</wheel_radius>
20942
21286
  <topic>${escapeXml(cmdVelTopic)}</topic>${spec.plugins.diffDrive.odomTopic ? `
20943
21287
  <odom_topic>${escapeXml(spec.plugins.diffDrive.odomTopic)}</odom_topic>` : ""}${spec.plugins.diffDrive.tfTopic ? `
20944
21288
  <tf_topic>${escapeXml(spec.plugins.diffDrive.tfTopic)}</tf_topic>` : ""}${spec.plugins.diffDrive.frameId ? `
20945
21289
  <frame_id>${escapeXml(spec.plugins.diffDrive.frameId)}</frame_id>` : ""}${spec.plugins.diffDrive.odomFrameId ? `
20946
21290
  <odom_frame>${escapeXml(spec.plugins.diffDrive.odomFrameId)}</odom_frame>` : ""}${spec.plugins.diffDrive.maxLinearVelocity !== void 0 ? `
20947
- <max_linear_velocity>${formatNumber(spec.plugins.diffDrive.maxLinearVelocity, 6)}</max_linear_velocity>` : ""}${spec.plugins.diffDrive.maxAngularVelocity !== void 0 ? `
20948
- <max_angular_velocity>${formatNumber(spec.plugins.diffDrive.maxAngularVelocity, 6)}</max_angular_velocity>` : ""}${spec.plugins.diffDrive.linearAcceleration !== void 0 ? `
20949
- <linear_acceleration>${formatNumber(spec.plugins.diffDrive.linearAcceleration, 6)}</linear_acceleration>` : ""}${spec.plugins.diffDrive.angularAcceleration !== void 0 ? `
20950
- <angular_acceleration>${formatNumber(spec.plugins.diffDrive.angularAcceleration, 6)}</angular_acceleration>` : ""}
21291
+ <max_linear_velocity>${formatNumber2(spec.plugins.diffDrive.maxLinearVelocity, 6)}</max_linear_velocity>` : ""}${spec.plugins.diffDrive.maxAngularVelocity !== void 0 ? `
21292
+ <max_angular_velocity>${formatNumber2(spec.plugins.diffDrive.maxAngularVelocity, 6)}</max_angular_velocity>` : ""}${spec.plugins.diffDrive.linearAcceleration !== void 0 ? `
21293
+ <linear_acceleration>${formatNumber2(spec.plugins.diffDrive.linearAcceleration, 6)}</linear_acceleration>` : ""}${spec.plugins.diffDrive.angularAcceleration !== void 0 ? `
21294
+ <angular_acceleration>${formatNumber2(spec.plugins.diffDrive.angularAcceleration, 6)}</angular_acceleration>` : ""}
20951
21295
  </plugin>`);
20952
21296
  }
20953
21297
  const jointState = spec.plugins.jointStatePublisher;
@@ -20957,7 +21301,7 @@ function modelXml(spec, modelName, linkNameMap, jointNameMap, geometries, linkWo
20957
21301
  (jointName) => `
20958
21302
  <joint_name>${escapeXml(jointNameMap.get(`${jointName}_joint`))}</joint_name>`
20959
21303
  ).join("")}${jointState?.updateRate !== void 0 ? `
20960
- <update_rate>${formatNumber(jointState.updateRate, 6)}</update_rate>` : ""}
21304
+ <update_rate>${formatNumber2(jointState.updateRate, 6)}</update_rate>` : ""}
20961
21305
  </plugin>`);
20962
21306
  }
20963
21307
  const rootNames = spec.assembly.parts.filter((part) => !spec.assembly.joints.some((joint2) => joint2.child === part.name)).map((part) => linkNameMap.get(part.name));
@@ -21149,6 +21493,129 @@ async function runSdfCli(argv = process.argv.slice(2)) {
21149
21493
  }
21150
21494
  }
21151
21495
 
21496
+ // cli/forge-show.ts
21497
+ var FORWARDED_VALUE_OPTIONS = /* @__PURE__ */ new Set([
21498
+ "--camera",
21499
+ "--camera-json",
21500
+ "--view",
21501
+ "--scene",
21502
+ "--background",
21503
+ "--render-style",
21504
+ "--edges",
21505
+ "--size",
21506
+ "--port",
21507
+ "--chrome-path",
21508
+ "--backend",
21509
+ "--param",
21510
+ "-p",
21511
+ "--render-mode",
21512
+ "--scan-granularity"
21513
+ ]);
21514
+ var FORWARDED_FLAG_OPTIONS = /* @__PURE__ */ new Set(["--fresh-server"]);
21515
+ var CAMERA_SOURCE_OPTIONS = /* @__PURE__ */ new Set(["--camera", "--camera-json", "--view", "--scene"]);
21516
+ function usage17() {
21517
+ return [
21518
+ "Usage: forgecad show <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [target] [output.png] [--from front|back|side|right|top|iso|az:el|az:el:dist] [--out output.png] [--param Key=Value] [--backend manifold|occt|truck] [render options]"
21519
+ ].join("\n");
21520
+ }
21521
+ function isImageOutput(value) {
21522
+ return /\.(png|jpg|jpeg|webp)$/i.test(value);
21523
+ }
21524
+ function readValue6(argv, index, flag) {
21525
+ const value = argv[index + 1];
21526
+ if (!value || value.startsWith("--")) throw new Error(`${flag} requires a value.`);
21527
+ return value;
21528
+ }
21529
+ function parseShowOptions(argv) {
21530
+ if (argv.length === 0 || argv.includes("-h") || argv.includes("--help")) {
21531
+ console.log(usage17());
21532
+ process.exit(0);
21533
+ }
21534
+ const consumed = /* @__PURE__ */ new Set();
21535
+ const renderArgs = [];
21536
+ let output;
21537
+ let from;
21538
+ let hasRenderCameraSource = false;
21539
+ for (let i = 0; i < argv.length; i += 1) {
21540
+ const arg = argv[i];
21541
+ if (arg === "--from") {
21542
+ from = readValue6(argv, i, arg);
21543
+ consumed.add(i);
21544
+ consumed.add(i + 1);
21545
+ i += 1;
21546
+ continue;
21547
+ }
21548
+ if (arg === "--out" || arg === "--output") {
21549
+ output = readValue6(argv, i, arg);
21550
+ consumed.add(i);
21551
+ consumed.add(i + 1);
21552
+ i += 1;
21553
+ continue;
21554
+ }
21555
+ if (FORWARDED_VALUE_OPTIONS.has(arg)) {
21556
+ const value = readValue6(argv, i, arg);
21557
+ renderArgs.push(arg, value);
21558
+ if (CAMERA_SOURCE_OPTIONS.has(arg)) hasRenderCameraSource = true;
21559
+ consumed.add(i);
21560
+ consumed.add(i + 1);
21561
+ i += 1;
21562
+ continue;
21563
+ }
21564
+ if (FORWARDED_FLAG_OPTIONS.has(arg)) {
21565
+ renderArgs.push(arg);
21566
+ consumed.add(i);
21567
+ continue;
21568
+ }
21569
+ if (arg.startsWith("--")) throw new Error(`Unknown option: ${arg}`);
21570
+ }
21571
+ if (from && hasRenderCameraSource) throw new Error("Use either --from or an explicit render camera option, not both.");
21572
+ const positionals = argv.filter((_, index) => !consumed.has(index));
21573
+ const scriptPath = positionals[0];
21574
+ if (!scriptPath) throw new Error("Missing input path.");
21575
+ let target;
21576
+ if (positionals.length === 2) {
21577
+ if (!output && isImageOutput(positionals[1])) output = positionals[1];
21578
+ else target = positionals[1];
21579
+ } else if (positionals.length === 3) {
21580
+ target = positionals[1];
21581
+ if (output) throw new Error("Pass output either positionally or with --out, not both.");
21582
+ output = positionals[2];
21583
+ } else if (positionals.length > 3) {
21584
+ throw new Error(`Unexpected argument: ${positionals[3]}`);
21585
+ }
21586
+ return { scriptPath, target, output, from, renderArgs, consumed };
21587
+ }
21588
+ function renderFocusForTargetNames(names) {
21589
+ return names.join(",");
21590
+ }
21591
+ async function runShowCli(argv = process.argv.slice(2)) {
21592
+ let options;
21593
+ try {
21594
+ options = parseShowOptions(argv);
21595
+ } catch (error) {
21596
+ console.error(error instanceof Error ? error.message : String(error));
21597
+ console.error(usage17());
21598
+ process.exit(1);
21599
+ }
21600
+ const renderArgs = [options.scriptPath];
21601
+ if (options.output) renderArgs.push(options.output);
21602
+ if (options.from) renderArgs.push("--camera", options.from);
21603
+ renderArgs.push(...options.renderArgs);
21604
+ if (options.target) {
21605
+ const { overrides } = parseParamFlags(argv);
21606
+ const { backend } = parseBackendArg(argv);
21607
+ const result = await runModelForInspect(options.scriptPath, backend, void 0, overrides);
21608
+ if (result.error) {
21609
+ console.error("ERROR:", result.error);
21610
+ process.exit(1);
21611
+ }
21612
+ const targets = resolveSceneTargets(buildSceneTargetEntries(result.objects), options.target);
21613
+ renderArgs.push("--focus", renderFocusForTargetNames(targets.map((target) => target.name)));
21614
+ console.log(`Target: ${targets.map(formatSceneTargetPath).join(", ")}`);
21615
+ }
21616
+ await runRenderCli(renderArgs);
21617
+ }
21618
+
21152
21619
  // cli/forge-sketch-pdf.ts
21153
21620
  import { readFile as readFile4, writeFile as writeFile7 } from "fs/promises";
21154
21621
  import { basename as basename12, resolve as resolve28 } from "path";
@@ -21708,7 +22175,7 @@ function generateSketchPdf(meta, options) {
21708
22175
  }
21709
22176
 
21710
22177
  // cli/forge-sketch-pdf.ts
21711
- function usage16() {
22178
+ function usage18() {
21712
22179
  console.error("Usage: forgecad export sketch-pdf <script.forge.js> [output.pdf]");
21713
22180
  process.exit(1);
21714
22181
  }
@@ -21717,7 +22184,7 @@ function defaultPdfOutput(scriptPath) {
21717
22184
  }
21718
22185
  async function runSketchPdfCli(argv = process.argv.slice(2)) {
21719
22186
  const scriptPath = argv[0];
21720
- if (!scriptPath) usage16();
22187
+ if (!scriptPath) usage18();
21721
22188
  const outputPath = argv[1] || defaultPdfOutput(scriptPath);
21722
22189
  if (resolve28(outputPath) === resolve28(scriptPath)) {
21723
22190
  console.error(`ERROR: output path would overwrite the input script. Specify an explicit output path.`);
@@ -22157,7 +22624,7 @@ function getServerUrl() {
22157
22624
  }
22158
22625
  function cliAuthRequiredMessage() {
22159
22626
  return [
22160
- "ForgeCAD CLI commands require a ForgeCAD account.",
22627
+ "This ForgeCAD CLI command requires a ForgeCAD account.",
22161
22628
  "",
22162
22629
  " New account: forgecad signup",
22163
22630
  " Interactive sign-in: forgecad login",
@@ -23545,7 +24012,7 @@ data: ${JSON.stringify(data)}
23545
24012
  }
23546
24013
 
23547
24014
  // cli/forge-studio.ts
23548
- function usage17() {
24015
+ function usage19() {
23549
24016
  console.error(`ForgeCAD Studio
23550
24017
 
23551
24018
  Usage:
@@ -23575,7 +24042,7 @@ function parseStudioArgs(argv) {
23575
24042
  if (argv.length === 0) {
23576
24043
  throw new Error("Missing project path. Use `forgecad studio <project-path> [project-path ...]`.");
23577
24044
  }
23578
- if (argv.includes("-h") || argv.includes("--help")) usage17();
24045
+ if (argv.includes("-h") || argv.includes("--help")) usage19();
23579
24046
  const options = { open: false, strictPort: false, projectPaths: [] };
23580
24047
  for (let i = 0; i < argv.length; i += 1) {
23581
24048
  const arg = argv[i];
@@ -23650,7 +24117,7 @@ async function runStudioCli(argv = process.argv.slice(2)) {
23650
24117
  // cli/forge-svg.ts
23651
24118
  import { readFile as readFile5, writeFile as writeFile8 } from "fs/promises";
23652
24119
  import { basename as basename13, resolve as resolve31 } from "path";
23653
- function usage18() {
24120
+ function usage20() {
23654
24121
  console.error("Usage: forgecad export svg <script.forge.js> [output.svg]");
23655
24122
  process.exit(1);
23656
24123
  }
@@ -23659,7 +24126,7 @@ function defaultSvgOutput(scriptPath) {
23659
24126
  }
23660
24127
  async function runSvgCli(argv = process.argv.slice(2)) {
23661
24128
  const scriptPath = argv[0];
23662
- if (!scriptPath) usage18();
24129
+ if (!scriptPath) usage20();
23663
24130
  const outputPath = argv[1] || defaultSvgOutput(scriptPath);
23664
24131
  if (resolve31(outputPath) === resolve31(scriptPath)) {
23665
24132
  console.error(`ERROR: output path would overwrite the input script. Specify an explicit output path.`);
@@ -23727,7 +24194,7 @@ function mmToM2(valueMm) {
23727
24194
  function degToRad2(valueDeg) {
23728
24195
  return valueDeg * Math.PI / 180;
23729
24196
  }
23730
- function formatNumber2(value, digits = 6) {
24197
+ function formatNumber3(value, digits = 6) {
23731
24198
  if (!Number.isFinite(value)) return "0";
23732
24199
  const normalized = Math.abs(value) < 1e-12 ? 0 : value;
23733
24200
  return normalized.toFixed(digits).replace(/\.?0+$/, "");
@@ -23850,7 +24317,7 @@ function transformToOrigin(transform) {
23850
24317
  yaw = Math.atan2(r10, r00);
23851
24318
  }
23852
24319
  const x = mmToM2(m[12]), y = mmToM2(m[13]), z = mmToM2(m[14]);
23853
- return `xyz="${formatNumber2(x)} ${formatNumber2(y)} ${formatNumber2(z)}" rpy="${formatNumber2(roll)} ${formatNumber2(pitch)} ${formatNumber2(yaw)}"`;
24320
+ return `xyz="${formatNumber3(x)} ${formatNumber3(y)} ${formatNumber3(z)}" rpy="${formatNumber3(roll)} ${formatNumber3(pitch)} ${formatNumber3(yaw)}"`;
23854
24321
  }
23855
24322
  function sRgbFloat2(hex) {
23856
24323
  if (!hex || !/^#([0-9a-f]{6})$/i.test(hex)) return null;
@@ -23911,7 +24378,7 @@ function urdfXml(spec, modelName, linkNameMap, jointNameMap, geometries, _linkWo
23911
24378
  const color = sRgbFloat2(geometry.shapes[0]?.colorHex);
23912
24379
  const materialXml = color ? `
23913
24380
  <material name="${escapeXml2(urdfLinkName)}_material">
23914
- <color rgba="${formatNumber2(color[0], 3)} ${formatNumber2(color[1], 3)} ${formatNumber2(color[2], 3)} 1"/>
24381
+ <color rgba="${formatNumber3(color[0], 3)} ${formatNumber3(color[1], 3)} ${formatNumber3(color[2], 3)} 1"/>
23915
24382
  </material>` : "";
23916
24383
  let collisionXml = "";
23917
24384
  if (collisionMode === "visual") {
@@ -23940,17 +24407,17 @@ function urdfXml(spec, modelName, linkNameMap, jointNameMap, geometries, _linkWo
23940
24407
  const bCz = mmToM2((geometry.bboxMin[2] + geometry.bboxMax[2]) * 0.5);
23941
24408
  collisionXml = `
23942
24409
  <collision>
23943
- <origin xyz="${formatNumber2(bCx)} ${formatNumber2(bCy)} ${formatNumber2(bCz)}" rpy="0 0 0"/>
24410
+ <origin xyz="${formatNumber3(bCx)} ${formatNumber3(bCy)} ${formatNumber3(bCz)}" rpy="0 0 0"/>
23944
24411
  <geometry>
23945
- <box size="${formatNumber2(bDx)} ${formatNumber2(bDy)} ${formatNumber2(bDz)}"/>
24412
+ <box size="${formatNumber3(bDx)} ${formatNumber3(bDy)} ${formatNumber3(bDz)}"/>
23946
24413
  </geometry>
23947
24414
  </collision>`;
23948
24415
  }
23949
24416
  return ` <link name="${escapeXml2(urdfLinkName)}">
23950
24417
  <inertial>
23951
- <origin xyz="${formatNumber2(comX)} ${formatNumber2(comY)} ${formatNumber2(comZ)}" rpy="0 0 0"/>
23952
- <mass value="${formatNumber2(massKg, 6)}"/>
23953
- <inertia ixx="${formatNumber2(ixx, 8)}" ixy="${formatNumber2(ixy, 8)}" ixz="${formatNumber2(ixz, 8)}" iyy="${formatNumber2(iyy, 8)}" iyz="${formatNumber2(iyz, 8)}" izz="${formatNumber2(izz, 8)}"/>
24418
+ <origin xyz="${formatNumber3(comX)} ${formatNumber3(comY)} ${formatNumber3(comZ)}" rpy="0 0 0"/>
24419
+ <mass value="${formatNumber3(massKg, 6)}"/>
24420
+ <inertia ixx="${formatNumber3(ixx, 8)}" ixy="${formatNumber3(ixy, 8)}" ixz="${formatNumber3(ixz, 8)}" iyy="${formatNumber3(iyy, 8)}" iyz="${formatNumber3(iyz, 8)}" izz="${formatNumber3(izz, 8)}"/>
23954
24421
  </inertial>
23955
24422
  <visual>
23956
24423
  <geometry>
@@ -23967,7 +24434,7 @@ function urdfXml(spec, modelName, linkNameMap, jointNameMap, geometries, _linkWo
23967
24434
  const jType = urdfJointHasContinuous(joint2) ? "continuous" : urdfJointType(joint2.type);
23968
24435
  const originAttr = transformToOrigin(joint2.frame);
23969
24436
  const axisXml = joint2.type !== "fixed" ? `
23970
- <axis xyz="${joint2.axis.map((v) => formatNumber2(v)).join(" ")}"/>` : "";
24437
+ <axis xyz="${joint2.axis.map((v) => formatNumber3(v)).join(" ")}"/>` : "";
23971
24438
  let limitXml = "";
23972
24439
  if (joint2.type !== "fixed" && jType !== "continuous") {
23973
24440
  const lower2 = joint2.min !== void 0 ? joint2.type === "revolute" ? degToRad2(joint2.min) : mmToM2(joint2.min) : void 0;
@@ -23976,20 +24443,20 @@ function urdfXml(spec, modelName, linkNameMap, jointNameMap, geometries, _linkWo
23976
24443
  const velocity = sourceOverrides?.velocity ?? joint2.velocity;
23977
24444
  const vel = velocity !== void 0 ? joint2.type === "revolute" ? degToRad2(velocity) : mmToM2(velocity) : 10;
23978
24445
  limitXml = `
23979
- <limit${lower2 !== void 0 ? ` lower="${formatNumber2(lower2)}"` : ""}${upper !== void 0 ? ` upper="${formatNumber2(upper)}"` : ""} effort="${formatNumber2(effort)}" velocity="${formatNumber2(vel)}"/>`;
24446
+ <limit${lower2 !== void 0 ? ` lower="${formatNumber3(lower2)}"` : ""}${upper !== void 0 ? ` upper="${formatNumber3(upper)}"` : ""} effort="${formatNumber3(effort)}" velocity="${formatNumber3(vel)}"/>`;
23980
24447
  } else if (jType === "continuous") {
23981
24448
  const effort = sourceOverrides?.effort ?? joint2.effort ?? 100;
23982
24449
  const velocity = sourceOverrides?.velocity ?? joint2.velocity;
23983
24450
  const vel = velocity !== void 0 ? degToRad2(velocity) : 10;
23984
24451
  limitXml = `
23985
- <limit effort="${formatNumber2(effort)}" velocity="${formatNumber2(vel)}"/>`;
24452
+ <limit effort="${formatNumber3(effort)}" velocity="${formatNumber3(vel)}"/>`;
23986
24453
  }
23987
24454
  let dynamicsXml = "";
23988
24455
  const damping = sourceOverrides?.damping ?? joint2.damping;
23989
24456
  const friction = sourceOverrides?.friction ?? joint2.friction;
23990
24457
  if (damping !== void 0 || friction !== void 0) {
23991
24458
  dynamicsXml = `
23992
- <dynamics${damping !== void 0 ? ` damping="${formatNumber2(damping)}"` : ""}${friction !== void 0 ? ` friction="${formatNumber2(friction)}"` : ""}/>`;
24459
+ <dynamics${damping !== void 0 ? ` damping="${formatNumber3(damping)}"` : ""}${friction !== void 0 ? ` friction="${formatNumber3(friction)}"` : ""}/>`;
23993
24460
  }
23994
24461
  let mimicXml = "";
23995
24462
  const coupling = couplingByJoint.get(joint2.name);
@@ -23997,7 +24464,7 @@ function urdfXml(spec, modelName, linkNameMap, jointNameMap, geometries, _linkWo
23997
24464
  const primary = coupling.terms.reduce((a, b) => Math.abs(a.ratio) >= Math.abs(b.ratio) ? a : b);
23998
24465
  const leaderUrdfName = jointNameMap.get(`${primary.joint}_joint`);
23999
24466
  mimicXml = `
24000
- <mimic joint="${escapeXml2(leaderUrdfName)}" multiplier="${formatNumber2(primary.ratio)}" offset="${formatNumber2(coupling.offset)}"/>`;
24467
+ <mimic joint="${escapeXml2(leaderUrdfName)}" multiplier="${formatNumber3(primary.ratio)}" offset="${formatNumber3(coupling.offset)}"/>`;
24001
24468
  if (coupling.terms.length > 1) {
24002
24469
  warnings.push(
24003
24470
  `Joint "${joint2.name}" coupling has ${coupling.terms.length} terms but URDF mimic only supports 1. Using primary term (ratio=${primary.ratio} from "${primary.joint}").`
@@ -25932,13 +26399,13 @@ function findCollisions(entries) {
25932
26399
  }
25933
26400
  return collisions;
25934
26401
  }
25935
- function usage19() {
26402
+ function usage21() {
25936
26403
  console.error("Usage: forgecad check params <script.forge.js> [--samples N]");
25937
26404
  process.exit(1);
25938
26405
  }
25939
26406
  async function runParamCheckCli(argv = process.argv.slice(2)) {
25940
26407
  const scriptPath = argv[0];
25941
- if (!scriptPath) usage19();
26408
+ if (!scriptPath) usage21();
25942
26409
  const samplesArg = argv.indexOf("--samples");
25943
26410
  const numSamples = samplesArg >= 0 ? parseInt(argv[samplesArg + 1], 10) : 8;
25944
26411
  const code = readFileSync21(resolve35(scriptPath), "utf-8");
@@ -26618,7 +27085,7 @@ function buildPrintCheckReport(objects, verifications, options) {
26618
27085
  }
26619
27086
 
26620
27087
  // cli/print-check.ts
26621
- function usage20() {
27088
+ function usage22() {
26622
27089
  console.error(
26623
27090
  "Usage: forgecad check print <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [--json] [--output report.json] [--profile fdm-pla-0.4mm] [--param Key=Value]"
26624
27091
  );
@@ -26745,7 +27212,7 @@ function parseArgs10(argv) {
26745
27212
  }
26746
27213
  const positional = argv.filter((arg, index) => !consumed.has(index) && !arg.startsWith("-"));
26747
27214
  const scriptPath = positional[0];
26748
- if (!scriptPath) usage20();
27215
+ if (!scriptPath) usage22();
26749
27216
  return {
26750
27217
  scriptPath,
26751
27218
  json,
@@ -27651,25 +28118,7 @@ async function writeSolverDebugIndex(root, scriptPath, bundles) {
27651
28118
  }
27652
28119
 
27653
28120
  // cli/test-run.ts
27654
- var DEFAULT_EXACT_SPATIAL_OBJECT_LIMIT = 250;
27655
- var SPATIAL_BBOX_PADDING = 0.1;
27656
- var MAX_SPATIAL_COLLISION_LINES = 50;
27657
28121
  var MAX_COMPACT_OBJECT_NAMES = 24;
27658
- function parseSpatialMode(argv, defaultMode) {
27659
- const consumed = /* @__PURE__ */ new Set();
27660
- const idx = argv.indexOf("--spatial");
27661
- if (idx === -1) return { mode: defaultMode, consumed, explicit: false };
27662
- consumed.add(idx);
27663
- const raw = argv[idx + 1];
27664
- if (!raw || raw.startsWith("-")) {
27665
- console.error("Missing value for --spatial. Expected bounded, exact, or off.");
27666
- process.exit(1);
27667
- }
27668
- consumed.add(idx + 1);
27669
- if (raw === "bounded" || raw === "exact" || raw === "off") return { mode: raw, consumed, explicit: true };
27670
- console.error(`Invalid --spatial value: ${raw}. Expected bounded, exact, or off.`);
27671
- process.exit(1);
27672
- }
27673
28122
  function buildJourneyInspection(journeys) {
27674
28123
  return {
27675
28124
  journeys: Object.entries(journeys ?? {}).map(([id, journey]) => ({
@@ -27807,137 +28256,6 @@ function formatFeatureSummary(counts) {
27807
28256
  collect("imports", imports);
27808
28257
  return groups.join(" | ");
27809
28258
  }
27810
- function analyzeSpatial(entries, mode) {
27811
- const lines = [];
27812
- const _axisLabel = ["X", "Y", "Z"];
27813
- const dirLabels = {
27814
- 0: ["LEFT of", "RIGHT of"],
27815
- 1: ["IN FRONT of", "BEHIND"],
27816
- 2: ["BELOW", "ABOVE"]
27817
- };
27818
- const allMin = [Infinity, Infinity, Infinity];
27819
- const allMax = [-Infinity, -Infinity, -Infinity];
27820
- for (const s of entries) {
27821
- for (let ax = 0; ax < 3; ax++) {
27822
- allMin[ax] = Math.min(allMin[ax], s.min[ax]);
27823
- allMax[ax] = Math.max(allMax[ax], s.max[ax]);
27824
- }
27825
- }
27826
- const sceneSize = Math.max(...allMax.map((v, i) => v - allMin[i]));
27827
- const proximityThreshold = sceneSize * 0.15;
27828
- const exactCollisionChecksEnabled = mode === "exact" || entries.length <= DEFAULT_EXACT_SPATIAL_OBJECT_LIMIT;
27829
- const collisionReport = analyzeCollisionIntersections(entries, {
27830
- bboxPadding: SPATIAL_BBOX_PADDING,
27831
- skipIntraGroup: true,
27832
- skipMockPairs: true,
27833
- includeBBoxCandidates: false,
27834
- ...exactCollisionChecksEnabled ? {} : { maxCandidatePairs: 0 }
27835
- });
27836
- if (!exactCollisionChecksEnabled && collisionReport.candidatePairCount > 0) {
27837
- lines.push(
27838
- ` Exact collision checks skipped for ${collisionReport.candidatePairCount} bbox-overlapping pair(s) in this ${entries.length}-object scene. Re-run with --spatial exact for exhaustive pairwise intersections.`
27839
- );
27840
- } else if (collisionReport.candidatePairCount > 0 && exactCollisionChecksEnabled) {
27841
- lines.push(
27842
- ` Exact collision checks: ${collisionReport.testedPairCount} bbox-overlapping pair(s) tested in ${collisionReport.exactCheckMs.toFixed(0)}ms.`
27843
- );
27844
- }
27845
- for (const collision of collisionReport.collisions.slice(0, MAX_SPATIAL_COLLISION_LINES)) {
27846
- lines.push(` \u26A0 COLLISION: ${collision.sourceName} \u2229 ${collision.targetName} (shared vol: ${collision.overlapVolume.toFixed(1)}mm\xB3)`);
27847
- }
27848
- if (collisionReport.collisionCount > MAX_SPATIAL_COLLISION_LINES) {
27849
- lines.push(
27850
- ` ... ${collisionReport.collisionCount - MAX_SPATIAL_COLLISION_LINES} more collision(s) omitted; use --focus/--hide or inspect fit interference to narrow the scene.`
27851
- );
27852
- }
27853
- for (const warning of collisionReport.warnings) {
27854
- if (warning.includes("maxCandidatePairs=0")) continue;
27855
- lines.push(` Warning: ${warning}`);
27856
- }
27857
- for (let i = 0; i < entries.length; i++) {
27858
- const a = entries[i];
27859
- const nearest = Array.from({ length: 6 }, () => ({ idx: -1, gap: Infinity }));
27860
- for (let j = 0; j < entries.length; j++) {
27861
- if (i === j) continue;
27862
- const b = entries[j];
27863
- for (let ax = 0; ax < 3; ax++) {
27864
- if (a.max[ax] < b.min[ax]) {
27865
- const gap = b.min[ax] - a.max[ax];
27866
- const d = ax * 2;
27867
- if (gap < nearest[d].gap) nearest[d] = { idx: j, gap };
27868
- } else if (b.max[ax] < a.min[ax]) {
27869
- const gap = a.min[ax] - b.max[ax];
27870
- const d = ax * 2 + 1;
27871
- if (gap < nearest[d].gap) nearest[d] = { idx: j, gap };
27872
- }
27873
- }
27874
- }
27875
- for (let d = 0; d < 6; d++) {
27876
- const n = nearest[d];
27877
- if (n.idx === -1 || n.gap > proximityThreshold) continue;
27878
- const b = entries[n.idx];
27879
- const ax = Math.floor(d / 2);
27880
- const isPositive = d % 2 === 0;
27881
- const label = isPositive ? dirLabels[ax][0] : dirLabels[ax][1];
27882
- lines.push(` ${a.name} is ${label} ${b.name} (gap: ${n.gap.toFixed(0)}mm)`);
27883
- }
27884
- }
27885
- const seen = /* @__PURE__ */ new Set();
27886
- const deduped = [];
27887
- for (const line of lines) {
27888
- if (line.includes("COLLISION")) {
27889
- deduped.push(line);
27890
- continue;
27891
- }
27892
- const match = line.match(/^\s+(.+?) is (?:LEFT of|RIGHT of|IN FRONT of|BEHIND|BELOW|ABOVE) (.+?) \(gap: (\d+)mm\)/);
27893
- if (match) {
27894
- const key = [match[1], match[2]].sort().join("|") + "|" + match[3];
27895
- if (!seen.has(key)) {
27896
- seen.add(key);
27897
- deduped.push(line);
27898
- }
27899
- } else {
27900
- deduped.push(line);
27901
- }
27902
- }
27903
- const groups = /* @__PURE__ */ new Map();
27904
- for (const e of entries) {
27905
- if (!e.groupName) continue;
27906
- const g = groups.get(e.groupName) || { min: [Infinity, Infinity, Infinity], max: [-Infinity, -Infinity, -Infinity] };
27907
- for (let ax = 0; ax < 3; ax++) {
27908
- g.min[ax] = Math.min(g.min[ax], e.min[ax]);
27909
- g.max[ax] = Math.max(g.max[ax], e.max[ax]);
27910
- }
27911
- groups.set(e.groupName, g);
27912
- }
27913
- if (groups.size > 1) {
27914
- deduped.push("");
27915
- deduped.push(" Groups:");
27916
- const groupNames = [...groups.keys()];
27917
- for (let i = 0; i < groupNames.length; i++) {
27918
- const aName = groupNames[i], a = groups.get(aName);
27919
- for (let j = i + 1; j < groupNames.length; j++) {
27920
- const bName = groupNames[j], b = groups.get(bName);
27921
- for (let ax = 0; ax < 3; ax++) {
27922
- if (a.max[ax] < b.min[ax]) {
27923
- const gap = b.min[ax] - a.max[ax];
27924
- if (gap <= proximityThreshold) {
27925
- const label = dirLabels[ax][0];
27926
- deduped.push(` ${aName} is ${label} ${bName} (gap: ${gap.toFixed(0)}mm)`);
27927
- }
27928
- } else if (b.max[ax] < a.min[ax]) {
27929
- const gap = a.min[ax] - b.max[ax];
27930
- if (gap <= proximityThreshold) {
27931
- const label = dirLabels[ax][1];
27932
- deduped.push(` ${aName} is ${label} ${bName} (gap: ${gap.toFixed(0)}mm)`);
27933
- }
27934
- }
27935
- }
27936
- }
27937
- }
27938
- }
27939
- return deduped;
27940
- }
27941
28259
  function parseParamFlags2(argv) {
27942
28260
  const overrides = {};
27943
28261
  const consumed = /* @__PURE__ */ new Set();
@@ -27956,9 +28274,9 @@ function parseParamFlags2(argv) {
27956
28274
  }
27957
28275
  return { overrides, consumed };
27958
28276
  }
27959
- function usage21() {
28277
+ function usage23() {
27960
28278
  console.error(
27961
- "Usage: forgecad run <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [--details] [--history] [--features] [--connectivity] [--connectivity-tolerance <mm>] [--spatial bounded|exact|off] [--param Key=Value] [--debug-imports] [--verbose|-v] [--backend manifold|occt|truck] [--quality live|default|high] [--solver-profile] [--solver-debug-out <dir>]"
28279
+ "Usage: forgecad run <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [--details] [--history] [--features] [--connectivity] [--connectivity-tolerance <mm>] [--param Key=Value] [--debug-imports] [--verbose|-v] [--backend manifold|occt|truck] [--quality live|default|high] [--solver-profile] [--solver-debug-out <dir>]"
27962
28280
  );
27963
28281
  process.exit(1);
27964
28282
  }
@@ -28115,7 +28433,13 @@ function printCompactObjectSummary(objects, focusSummary) {
28115
28433
  async function runScriptCli(argv = process.argv.slice(2)) {
28116
28434
  if (argv.includes("--full")) {
28117
28435
  console.error("`forgecad run --full` has been removed.");
28118
- console.error("Use explicit diagnostics: `--details --history --features --solver-profile --spatial bounded`.");
28436
+ console.error("Use explicit diagnostics: `--details --history --features --solver-profile`.");
28437
+ process.exit(1);
28438
+ }
28439
+ const removedSpatialFlag = argv.find((arg) => arg === "--spatial" || arg.startsWith("--spatial="));
28440
+ if (removedSpatialFlag) {
28441
+ console.error("`forgecad run --spatial` has been removed.");
28442
+ console.error("Use `forgecad inspect fit interference <model>` for collision evidence or `forgecad inspect physical gaps <model>` for spatial gap evidence.");
28119
28443
  process.exit(1);
28120
28444
  }
28121
28445
  const { overrides: paramCliOverrides, consumed: paramConsumed } = parseParamFlags2(argv);
@@ -28142,12 +28466,11 @@ async function runScriptCli(argv = process.argv.slice(2)) {
28142
28466
  const explicitBackend = parseBackendArg2(argv);
28143
28467
  const quality = parseQualityArg2(argv);
28144
28468
  const solverDebugOut = parseRequiredArg(argv, "--solver-debug-out");
28145
- const { mode: spatialMode, consumed: spatialConsumed, explicit: spatialExplicit } = parseSpatialMode(argv, "off");
28146
28469
  const positional = argv.filter(
28147
- (arg, i) => !paramConsumed.has(i) && !focusConsumed.has(i) && !connectivityConsumed.has(i) && !spatialConsumed.has(i) && arg !== "--details" && arg !== "--history" && arg !== "--features" && arg !== "--solver-profile" && arg !== "--debug-imports" && arg !== "--journeys" && arg !== "--journeys-json" && arg !== "--verbose" && arg !== "-v" && arg !== "--backend" && argv[i - 1] !== "--backend" && arg !== "--quality" && argv[i - 1] !== "--quality" && arg !== "-q" && argv[i - 1] !== "-q" && arg !== "--solver-debug-out" && argv[i - 1] !== "--solver-debug-out"
28470
+ (arg, i) => !paramConsumed.has(i) && !focusConsumed.has(i) && !connectivityConsumed.has(i) && arg !== "--details" && arg !== "--history" && arg !== "--features" && arg !== "--solver-profile" && arg !== "--debug-imports" && arg !== "--journeys" && arg !== "--journeys-json" && arg !== "--verbose" && arg !== "-v" && arg !== "--backend" && argv[i - 1] !== "--backend" && arg !== "--quality" && argv[i - 1] !== "--quality" && arg !== "-q" && argv[i - 1] !== "-q" && arg !== "--solver-debug-out" && argv[i - 1] !== "--solver-debug-out"
28148
28471
  );
28149
28472
  const scriptPath = positional[0];
28150
- if (!scriptPath) usage21();
28473
+ if (!scriptPath) usage23();
28151
28474
  let activeBackend = CLI_DEFAULT_BACKEND;
28152
28475
  try {
28153
28476
  if (solverDebugOut) {
@@ -28319,8 +28642,7 @@ async function runScriptCli(argv = process.argv.slice(2)) {
28319
28642
  console.log(` ${log.args.join(" ")}`);
28320
28643
  }
28321
28644
  }
28322
- const needsShapeEntries = connectivityEnabled || spatialMode !== "off";
28323
- const entries = needsShapeEntries ? visibleObjects.filter((o) => o.shape).map((o) => {
28645
+ const entries = connectivityEnabled ? visibleObjects.filter((o) => o.shape).map((o) => {
28324
28646
  const bb = o.shape.boundingBox();
28325
28647
  return {
28326
28648
  id: o.id,
@@ -28340,18 +28662,6 @@ async function runScriptCli(argv = process.argv.slice(2)) {
28340
28662
  const connectivity = analyzePhysicalConnectivity2(entries, connectivityOptions);
28341
28663
  for (const line of formatPhysicalConnectivity(connectivity)) console.log(line);
28342
28664
  }
28343
- if (entries.length > 1 && spatialMode !== "off") {
28344
- console.log(`
28345
- \u2713 Spatial analysis:`);
28346
- const spatialLines = analyzeSpatial(entries, spatialMode);
28347
- for (const line of spatialLines) console.log(line);
28348
- if (spatialLines.length === 0) {
28349
- console.log(` (no collisions, all objects well-separated)`);
28350
- }
28351
- } else if (spatialExplicit && visibleObjects.filter((o) => o.shape).length > 1 && spatialMode === "off") {
28352
- console.log(`
28353
- \u2713 Spatial analysis: skipped (--spatial off)`);
28354
- }
28355
28665
  console.log(
28356
28666
  `\u2713 Params: ${result.params.map((p) => {
28357
28667
  const label = p.choices ? `${p.choices[p.value]}` : `${p.value}`;
@@ -28512,7 +28822,7 @@ async function runScriptCli(argv = process.argv.slice(2)) {
28512
28822
  // cli/forge-render-section.ts
28513
28823
  import { writeFile as writeFile10 } from "fs/promises";
28514
28824
  import { basename as basename16, extname as extname11, resolve as resolve39 } from "path";
28515
- function usage22() {
28825
+ function usage24() {
28516
28826
  console.error(
28517
28827
  "Usage: forgecad render section <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [output.svg|.png] [--param Key=Value] [--plane XY|XZ|YZ] [--offset <number>] [--size <px>] [--edges <off|thin|bold>] [--chrome-path <path>] [--background <color>]"
28518
28828
  );
@@ -28573,12 +28883,12 @@ async function runRenderSectionCli(argv = process.argv.slice(2)) {
28573
28883
  background = argv[++i];
28574
28884
  } else if (argv[i].startsWith("-")) {
28575
28885
  console.error(`Unknown option: ${argv[i]}`);
28576
- usage22();
28886
+ usage24();
28577
28887
  } else {
28578
28888
  args.push(argv[i]);
28579
28889
  }
28580
28890
  }
28581
- if (args.length === 0) usage22();
28891
+ if (args.length === 0) usage24();
28582
28892
  scriptPath = args[0];
28583
28893
  const wantsPng = args[1] ? extname11(args[1]).toLowerCase() === ".png" : false;
28584
28894
  outputPath = args[1] || defaultOutput(scriptPath, wantsPng ? "png" : "svg");
@@ -28919,7 +29229,7 @@ var RENDER_OPTIONS = [
28919
29229
  valueLabel: "<classic|studio|fast|glass|inspection|precision|hybrid|scan>",
28920
29230
  values: RENDER_STYLE_VALUES
28921
29231
  },
28922
- { name: "--scan-granularity", description: "Scan cells across the scene longest axis", argument: "required", valueLabel: "<12-72>" },
29232
+ { name: "--scan-granularity", description: "Scan cells across the scene longest axis", argument: "required", valueLabel: "<12-144>" },
28923
29233
  { name: "--port", description: "Vite dev server port", argument: "required", valueLabel: "<n>" },
28924
29234
  { name: "--fresh-server", description: "Start a fresh renderer instead of reusing an existing one" },
28925
29235
  {
@@ -28931,6 +29241,51 @@ var RENDER_OPTIONS = [
28931
29241
  },
28932
29242
  { name: "--output", description: "Output file path", argument: "required", valueLabel: "<path>", valueKind: "png" }
28933
29243
  ];
29244
+ var LS_OPTIONS = [
29245
+ { name: "--tree", description: "Print targets as a compact object tree" },
29246
+ { name: "--long", description: "Include geometry metrics such as bounds, volume, area, bodies, and triangle counts" },
29247
+ { name: "--json", description: "Print machine-readable target inventory" },
29248
+ { name: "--compact", description: "Minify JSON output" },
29249
+ {
29250
+ name: "--kind",
29251
+ description: "Filter targets by kind",
29252
+ argument: "required",
29253
+ valueLabel: "<shape|sketch|sdf|toolpath|mock|object>"
29254
+ },
29255
+ ...PARAM_OPTIONS,
29256
+ {
29257
+ name: "--backend",
29258
+ description: "Geometry backend",
29259
+ argument: "required",
29260
+ valueLabel: "<manifold|occt|truck>",
29261
+ values: BACKEND_VALUES
29262
+ },
29263
+ {
29264
+ name: "--quality",
29265
+ description: "Geometry quality preset",
29266
+ argument: "required",
29267
+ valueLabel: "<live|default|high>",
29268
+ values: QUALITY_VALUES
29269
+ }
29270
+ ];
29271
+ var SHOW_OPTIONS = [
29272
+ {
29273
+ name: "--from",
29274
+ description: "Quick camera direction",
29275
+ argument: "required",
29276
+ valueLabel: "<front|back|side|right|top|iso|az:el|az:el:dist>",
29277
+ values: RENDER_ANGLE_VALUES
29278
+ },
29279
+ { name: "--out", description: "Output file path", argument: "required", valueLabel: "<path>", valueKind: "png" },
29280
+ {
29281
+ name: "--backend",
29282
+ description: "Geometry backend",
29283
+ argument: "required",
29284
+ valueLabel: "<manifold|occt|truck>",
29285
+ values: BACKEND_VALUES
29286
+ },
29287
+ ...RENDER_OPTIONS.filter((option) => option.name !== "--focus" && option.name !== "--hide")
29288
+ ];
28934
29289
  var INSPECT_EVIDENCE_OPTIONS = [
28935
29290
  ...PARAM_OPTIONS,
28936
29291
  {
@@ -29972,13 +30327,61 @@ var commands = [
29972
30327
  hidden: true,
29973
30328
  run: async (args) => runHiddenCompletionCli(args, commands)
29974
30329
  },
30330
+ {
30331
+ group: "Modeling",
30332
+ path: ["ls"],
30333
+ summary: "List targetable scene objects for a model.",
30334
+ description: "Runs the model headlessly and prints the exact object paths that CLI tools can target. Use this before focused renders or inspections when object names, groups, or assembly paths are not obvious.\n\nThe default output is a compact line-oriented list. Use `--tree` for an indented hierarchy, `--long` for geometry metrics, or `--json` for automation.",
30335
+ usage: [
30336
+ "forgecad ls <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [target] [--tree] [--long] [--json] [--kind shape,sketch,mock] [--param Key=Value] [--backend manifold|occt|truck] [--quality live|default|high]"
30337
+ ],
30338
+ examples: [
30339
+ "forgecad ls examples/api/static-assembly-connectors.forge.js",
30340
+ "forgecad ls examples/api/static-assembly-connectors.forge.js --tree",
30341
+ "forgecad ls examples/api/static-assembly-connectors.forge.js Bench",
30342
+ 'forgecad ls examples/api/static-assembly-connectors.forge.js "Bench/Slat0" --long',
30343
+ "forgecad ls examples/api/static-assembly-connectors.forge.js --json"
30344
+ ],
30345
+ completion: {
30346
+ options: LS_OPTIONS,
30347
+ positionals: [
30348
+ { description: "Forge script or CAD asset", valueKind: "renderable" },
30349
+ { description: "optional target path, object name, id, group, or glob" }
30350
+ ]
30351
+ },
30352
+ run: runLsCli
30353
+ },
30354
+ {
30355
+ group: "Modeling",
30356
+ path: ["show"],
30357
+ summary: "Render a quick target-focused viewport PNG.",
30358
+ description: "The main quick visual path for agents. Pass a model and, optionally, a target path from `forgecad ls`; ForgeCAD resolves the target and renders that object or group through the existing viewport renderer. Use `--from` for fast camera directions, or pass the lower-level render camera flags when you need exact reproducibility.\n\nWithout a target, `show` renders the whole scene and behaves like a shorter, intent-first wrapper around `render 3d`.",
30359
+ usage: [
30360
+ "forgecad show <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [target] [output.png] [--from front|back|side|right|top|iso|az:el|az:el:dist] [--out output.png] [--param Key=Value] [--backend manifold|occt|truck] [render options]"
30361
+ ],
30362
+ examples: [
30363
+ "forgecad show examples/api/static-assembly-connectors.forge.js",
30364
+ "forgecad show examples/api/static-assembly-connectors.forge.js Bench",
30365
+ 'forgecad show examples/api/static-assembly-connectors.forge.js "Bench/Slat0" --from front --out slat-front.png',
30366
+ 'forgecad show model.forge.js --camera "proj=perspective;pos=200,-160,120;target=0,0,20;up=0,0,1;fov=38"'
30367
+ ],
30368
+ completion: {
30369
+ options: SHOW_OPTIONS,
30370
+ positionals: [
30371
+ { description: "Forge script or CAD asset", valueKind: "renderable" },
30372
+ { description: "optional target path, object name, id, group, or glob" },
30373
+ { description: "output PNG path", valueKind: "png" }
30374
+ ]
30375
+ },
30376
+ run: runShowCli
30377
+ },
29975
30378
  {
29976
30379
  group: "Modeling",
29977
30380
  path: ["run"],
29978
- summary: "Execute a Forge script quickly and print the inner-loop build summary: returned objects, verification results, parameters, and timing.",
29979
- description: "The fast validation command. Runs your script with the real geometry kernel (no browser needed) and reports whether it built, which objects came back, any `verify.*` results, parameter values, script logs, and elapsed script time. This is the command agents should run frequently while editing a model.\n\n**Fast by default** \u2014 a bare `forgecad run model.forge.js` does not compute per-object volumes, bounding boxes, construction history, feature tallies, spatial relationships, collision intersections, or solver profiles. Those diagnostics are useful, but they are no longer part of the hot path.\n\n**Opt-in diagnostics** \u2014 use `--details` for volume/bounding-box/object geometry summaries, `--history` for the construction tree, `--features` for feature tallies, or `--solver-profile` for constraint solver timing. Use `--spatial bounded|exact` only when you want directional relationships and collision intersections from the run command itself.\n\n**Verification results** \u2014 runs any `verify.*` checks in the script and reports pass/fail with expected vs actual values. Verification failures remain non-fatal so the model can still render and be inspected.\n\n**Physical connectivity** \u2014 pass `--connectivity` to list physically connected components across visible objects. Overlapping bbox candidates are checked with exact geometry by default, while bbox-only contact is treated as evidence rather than proof of one connected component. This helps answer whether the model is one continuous assembly or several separate islands.\n\n**Quality preset** \u2014 pass `--quality live|default|high` to select the same geometry quality profile used by the editor and export tools. `live` is the fastest preset for large audit models.\n\n**Direct CAD inputs** \u2014 pass `.stl`, `.obj`, `.3mf`, `.step`, or `.stp` directly when you just want to inspect an external asset. Mesh files are imported with `importMesh(...)`; STEP/STP files are imported with `importStep(...)` and auto-select OCCT unless you pass `--backend`.\n\nFor deeper confidence gates, prefer `forgecad inspect mechanical-integrity`, `forgecad check print`, or targeted evidence commands such as `forgecad inspect fit interference` instead of turning `run` back into a catch-all audit command.",
30381
+ summary: "Execute a Forge script quickly and print the inner-loop build summary: build count, verification results, parameters, and timing.",
30382
+ description: "The fast validation command. Runs your script with the real geometry kernel (no browser needed) and reports whether it built, how many objects came back, any `verify.*` results, parameter values, script logs, and elapsed script time. This is the command agents should run frequently while editing a model.\n\n**Fast by default** \u2014 a bare `forgecad run model.forge.js` does not compute per-object volumes, bounding boxes, construction history, feature tallies, or solver profiles. Those run diagnostics are useful, but they are no longer part of the hot path.\n\n**Opt-in diagnostics** \u2014 use `--details` for volume/bounding-box/object geometry summaries, `--history` for the construction tree, `--features` for feature tallies, or `--solver-profile` for constraint solver timing. Use `inspect fit interference`, `inspect physical gaps`, or `inspect mechanical-integrity --collisions` for spatial and collision evidence.\n\n**Verification results** \u2014 runs any `verify.*` checks in the script and reports pass/fail with expected vs actual values. Verification failures remain non-fatal so the model can still render and be inspected.\n\n**Physical connectivity** \u2014 pass `--connectivity` to list physically connected components across visible objects. Overlapping bbox candidates are checked with exact geometry by default, while bbox-only contact is treated as evidence rather than proof of one connected component. This helps answer whether the model is one continuous assembly or several separate islands.\n\n**Quality preset** \u2014 pass `--quality live|default|high` to select the same geometry quality profile used by the editor and export tools. `live` is the fastest preset for large audit models.\n\n**Direct CAD inputs** \u2014 pass `.stl`, `.obj`, `.3mf`, `.step`, or `.stp` directly when you just want to inspect an external asset. Mesh files are imported with `importMesh(...)`; STEP/STP files are imported with `importStep(...)` and auto-select OCCT unless you pass `--backend`.\n\nFor deeper confidence gates, prefer `forgecad inspect mechanical-integrity`, `forgecad check print`, or targeted evidence commands such as `forgecad inspect fit interference` instead of turning `run` back into a catch-all audit command.",
29980
30383
  usage: [
29981
- "forgecad run <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [--details] [--history] [--features] [--connectivity] [--connectivity-tolerance <mm>] [--spatial bounded|exact|off] [--focus [names]] [--hide names] [--journeys] [--journeys-json] [--param Key=Value] [--debug-imports] [--verbose] [--backend manifold|occt|truck] [--quality live|default|high] [--solver-profile] [--solver-debug-out <dir>]"
30384
+ "forgecad run <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [--details] [--history] [--features] [--connectivity] [--connectivity-tolerance <mm>] [--focus [names]] [--hide names] [--journeys] [--journeys-json] [--param Key=Value] [--debug-imports] [--verbose] [--backend manifold|occt|truck] [--quality live|default|high] [--solver-profile] [--solver-debug-out <dir>]"
29982
30385
  ],
29983
30386
  examples: [
29984
30387
  "forgecad run examples/api/static-assembly-connectors.forge.js",
@@ -29986,7 +30389,6 @@ var commands = [
29986
30389
  'forgecad run examples/api/static-assembly-connectors.forge.js --focus "Bench.Slat*"',
29987
30390
  'forgecad run examples/api/static-assembly-connectors.forge.js --hide "Bench.Slat0,Bench.Slat1"',
29988
30391
  "forgecad run examples/api/static-assembly-connectors.forge.js --details --history",
29989
- "forgecad run examples/api/static-assembly-connectors.forge.js --spatial bounded",
29990
30392
  "forgecad run examples/products/cup.forge.js --connectivity",
29991
30393
  "forgecad run examples/products/cup.forge.js --journeys",
29992
30394
  "forgecad run examples/products/cup.forge.js --backend occt",
@@ -30017,17 +30419,6 @@ var commands = [
30017
30419
  argument: "required",
30018
30420
  valueLabel: "<mm>"
30019
30421
  },
30020
- {
30021
- name: "--spatial",
30022
- description: "Spatial diagnostics mode (default: off)",
30023
- argument: "required",
30024
- valueLabel: "<bounded|exact|off>",
30025
- values: [
30026
- { value: "bounded", description: "Exact checks only for bounded scene sizes" },
30027
- { value: "exact", description: "Exhaustive pairwise exact collision intersections" },
30028
- { value: "off", description: "Skip spatial diagnostics" }
30029
- ]
30030
- },
30031
30422
  { name: "--debug-imports", description: "Print the import trace" },
30032
30423
  { name: "--journeys", description: "Print model journey summaries declared with scene({ journeys })" },
30033
30424
  { name: "--journeys-json", description: "Emit machine-readable journey metadata and diagnostics only" },
@@ -30147,7 +30538,9 @@ var commands = [
30147
30538
  path: ["render", "wireframe"],
30148
30539
  summary: "Render a Forge scene as a wireframe (edges only, no shading).",
30149
30540
  description: "Same as `render 3d` but renders only the edge geometry \u2014 no shaded surfaces. Useful for construction-style documentation or highlighting structural features without material detail.",
30150
- usage: ["forgecad render wireframe <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [output.png] [--param Key=Value] [options]"],
30541
+ usage: [
30542
+ "forgecad render wireframe <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [output.png] [--param Key=Value] [options]"
30543
+ ],
30151
30544
  examples: [
30152
30545
  "forgecad render wireframe examples/products/cup.forge.js",
30153
30546
  "forgecad render wireframe examples/products/cup.forge.js --camera iso"
@@ -30198,7 +30591,9 @@ var commands = [
30198
30591
  path: ["render", "hq"],
30199
30592
  summary: "High-quality render via Blender Cycles \u2014 path-traced, HDRI, material presets.",
30200
30593
  description: "Exports the scene to Blender and renders with Cycles (path tracer). Requires Blender installed and on PATH.\n\nChoose a `--preset` for the look: `studio` (neutral product shot), `dramatic` (high-contrast), `clay` (matte, no color), `glass`, `metallic`, `toon`, `xray`, `normals`, `silhouette`, and more. Control quality vs speed with `--samples` (default 256). Use `--view`, `--camera`, `--camera-json`, or `--scene <file>` for still camera control, matching `render 3d`. Use `--transparent` for a transparent background (compositing-ready).\n\nOutput defaults to `<script-name>-hq.png`. Great for documentation, marketing renders, and social media.",
30201
- usage: ["forgecad render hq <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [output.png] [--param Key=Value] [options]"],
30594
+ usage: [
30595
+ "forgecad render hq <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [output.png] [--param Key=Value] [options]"
30596
+ ],
30202
30597
  examples: [
30203
30598
  "forgecad render hq examples/products/cup.forge.js",
30204
30599
  "forgecad render hq examples/products/cup.forge.js hero.png --preset dramatic --samples 1024",
@@ -31461,7 +31856,9 @@ var commands = [
31461
31856
  path: ["inspect", "replay"],
31462
31857
  summary: "Replay a saved section inspection result on a source model.",
31463
31858
  description: "Reads a previous `forgecad inspect section` result, reruns the same section and ray rulers on the requested source, and writes a fresh unique result directory with measurement deltas against the original.",
31464
- usage: ["forgecad inspect replay <result.json> --source <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [--param Key=Value] [options]"],
31859
+ usage: [
31860
+ "forgecad inspect replay <result.json> --source <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [--param Key=Value] [options]"
31861
+ ],
31465
31862
  examples: [
31466
31863
  "forgecad inspect replay outputs/inspect/2026-05-28T14-32-09.184Z__section__bearing__a8f3c1/result.json --source submission/main.forge.js"
31467
31864
  ],
@@ -31671,7 +32068,7 @@ var commands = [
31671
32068
  { name: "--update", description: "Regenerate compiler snapshots" }
31672
32069
  ]
31673
32070
  },
31674
- run: async (args) => (await import("./check-compiler-LOXCPEOI.js")).runCheckCompilerCli(args)
32071
+ run: async (args) => (await import("./check-compiler-U5SOPN7X.js")).runCheckCompilerCli(args)
31675
32072
  },
31676
32073
  {
31677
32074
  group: "Checks",
@@ -31694,7 +32091,7 @@ var commands = [
31694
32091
  { name: "--update", description: "Regenerate query-propagation snapshots" }
31695
32092
  ]
31696
32093
  },
31697
- run: async (args) => (await import("./check-query-propagation-BAKNVWXR.js")).runCheckQueryPropagationCli(args)
32094
+ run: async (args) => (await import("./check-query-propagation-XOKNSSYU.js")).runCheckQueryPropagationCli(args)
31698
32095
  },
31699
32096
  {
31700
32097
  group: "Checks",
@@ -31929,64 +32326,37 @@ function readVersion() {
31929
32326
  return "0.0.0";
31930
32327
  }
31931
32328
  }
31932
- var AUTH_EXEMPT_COMMANDS = /* @__PURE__ */ new Set([
31933
- "signup",
31934
- "login",
31935
- "logout",
31936
- "whoami",
31937
- "completion",
31938
- "__complete",
31939
- "doctor",
31940
- "studio",
31941
- "dev",
31942
- "web",
31943
- // Local model validation must stay available to agents and CI-like repo workflows
31944
- // without requiring a hosted account. Hosted project, publish, and paid export
31945
- // commands still go through the auth/tier path.
31946
- "run",
31947
- "render",
31948
- "render 3d",
31949
- "render wireframe",
31950
- "render views",
31951
- "render section",
31952
- "render sketch",
31953
- "compare 3d",
31954
- "debug assembly",
31955
- "debug compiler",
31956
- "debug dimensions",
31957
- "debug faces",
31958
- "check print",
31959
- "inspect",
31960
- "inspect evidence",
31961
- "inspect history",
31962
- "inspect design-trace",
31963
- "inspect section",
31964
- "inspect replay",
31965
- "inspect visual",
31966
- "inspect visual image",
31967
- "inspect visual cutaway",
31968
- "inspect visual depth",
31969
- "inspect visual normals",
31970
- "inspect visual objects",
31971
- "inspect surface",
31972
- "inspect surface zebra",
31973
- "inspect surface roughness",
31974
- "inspect physical",
31975
- "inspect physical components",
31976
- "inspect physical floating",
31977
- "inspect physical gaps",
31978
- "inspect fit",
31979
- "inspect fit interference",
31980
- "inspect manufacture",
31981
- "inspect manufacture thickness",
31982
- "inspect compare",
31983
- "inspect compare overlay",
31984
- "inspect sections at",
31985
- "inspect sections stack",
31986
- "inspect sections sample",
31987
- "inspect mechanical-integrity",
31988
- "project file",
31989
- "project shares"
32329
+ var AUTH_REQUIRED_COMMANDS = /* @__PURE__ */ new Set([
32330
+ // Hosted project, publish, token, and account-license mutations touch ForgeCAD server state.
32331
+ // Local modeling, inspection, export, and skill setup commands stay available without sign-in.
32332
+ "project init",
32333
+ "project clone",
32334
+ "project pull",
32335
+ "project push",
32336
+ "project status",
32337
+ "project list",
32338
+ "project publish",
32339
+ "project info",
32340
+ "project rename",
32341
+ "project set-visibility",
32342
+ "project delete",
32343
+ "project members",
32344
+ "project add-member",
32345
+ "project remove-member",
32346
+ "project set-role",
32347
+ "project file list",
32348
+ "project file read",
32349
+ "project file save",
32350
+ "project file delete",
32351
+ "project file rename",
32352
+ "project file mkdir",
32353
+ "project file copy",
32354
+ "project shares list",
32355
+ "project shares delete",
32356
+ "token create",
32357
+ "token list",
32358
+ "token revoke",
32359
+ "license activate"
31990
32360
  ]);
31991
32361
  var LONG_RUNNING_COMMANDS = /* @__PURE__ */ new Set(["dev", "studio", "web"]);
31992
32362
  var INTERNAL_CHECK_COMMANDS = /* @__PURE__ */ new Set([
@@ -32036,6 +32406,8 @@ var TOPIC_DESCRIPTIONS = {
32036
32406
  };
32037
32407
  var ROOT_WORKFLOWS = [
32038
32408
  ["forgecad run <model.forge.js>", "Execute a model and print the inner-loop build summary."],
32409
+ ["forgecad ls <model.forge.js>", "List targetable object paths before narrowing a render or inspection."],
32410
+ ["forgecad show <model.forge.js> [target]", "Render a quick viewport PNG for the whole scene or a target."],
32039
32411
  ["forgecad check print <model.forge.js>", "Catch printability problems before export."],
32040
32412
  ["forgecad render 3d <model.forge.js> --output preview.png", "Render a viewport PNG."],
32041
32413
  ["forgecad export step <model.forge.js> --output part.step", "Export exact CAD for downstream tools."],
@@ -32045,7 +32417,7 @@ var ROOT_HELP_GROUPS = [
32045
32417
  { heading: "Studio", paths: [["studio"], ["dev"], ["web"]] },
32046
32418
  {
32047
32419
  heading: "Modeling",
32048
- paths: [["run"], ["render"], ["capture"], ["check"], ["compare"], ["inspect"]]
32420
+ paths: [["run"], ["ls"], ["show"], ["render"], ["capture"], ["check"], ["compare"], ["inspect"]]
32049
32421
  },
32050
32422
  { heading: "Export", paths: [["export"], ["cut-list"], ["link"]] },
32051
32423
  {
@@ -32073,7 +32445,7 @@ function commandKey(commandPath) {
32073
32445
  return commandPath.join(" ");
32074
32446
  }
32075
32447
  function commandRequiresCliAuth(commandPath) {
32076
- return !AUTH_EXEMPT_COMMANDS.has(commandKey(commandPath));
32448
+ return AUTH_REQUIRED_COMMANDS.has(commandKey(commandPath));
32077
32449
  }
32078
32450
  function isLongRunningCommand(commandPath) {
32079
32451
  return LONG_RUNNING_COMMANDS.has(commandKey(commandPath));
@@ -32505,7 +32877,11 @@ function printMovedSharesTopicMessage(argv) {
32505
32877
  }
32506
32878
  function printRemovedRunFullFlagMessage() {
32507
32879
  console.error("`forgecad run --full` has been removed.");
32508
- console.error("Use explicit diagnostics: `--details --history --features --solver-profile --spatial bounded`.");
32880
+ console.error("Use explicit diagnostics: `--details --history --features --solver-profile`.");
32881
+ }
32882
+ function printRemovedRunSpatialFlagMessage() {
32883
+ console.error("`forgecad run --spatial` has been removed.");
32884
+ console.error("Use `forgecad inspect fit interference <model>` for collision evidence or `forgecad inspect physical gaps <model>` for spatial gap evidence.");
32509
32885
  }
32510
32886
  function printInternalCheckCommandMessage(commandName) {
32511
32887
  if (commandName === "params") {
@@ -32618,6 +32994,11 @@ async function runForgeCadCli(argv = process.argv.slice(2)) {
32618
32994
  process.exitCode = 1;
32619
32995
  return;
32620
32996
  }
32997
+ if (argv[0] === "run" && argv.some((arg) => arg === "--spatial" || arg.startsWith("--spatial=")) && !argv.some(isHelpFlag)) {
32998
+ printRemovedRunSpatialFlagMessage();
32999
+ process.exitCode = 1;
33000
+ return;
33001
+ }
32621
33002
  const wantsHelp = argv.some(isHelpFlag);
32622
33003
  const wantsVersion = argv[0] === "-v" || argv[0] === "--version";
32623
33004
  const skipUpdateMachinery = argv[0] === "__complete" || argv[0] === "completion" || wantsVersion || wantsHelp;