forgecad 0.10.2 → 0.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -6
- package/dist/assets/{AdminPage-CHY6ZN-p.js → AdminPage-CK7ObBz3.js} +1 -1
- package/dist/assets/{BenchmarkPage-BcRT5iGN.js → BenchmarkPage-Ds7Z2doN.js} +1 -1
- package/dist/assets/{BlogPage-BssBbnb-.js → BlogPage-DlPbpt6A.js} +1 -1
- package/dist/assets/{DocsPage-DsvdiRNK.js → DocsPage-vZb3b3Y0.js} +9 -14
- package/dist/assets/{EditorApp-BpjZgzk0.css → EditorApp-C5f24ZN9.css} +8 -0
- package/dist/assets/{EditorApp-Bfd3jbtC.js → EditorApp-HLoKfe15.js} +141 -12
- package/dist/assets/{EmbedViewer-D5t8WamV.js → EmbedViewer--KnqBKrJ.js} +2 -2
- package/dist/assets/{LandingPageProofDriven-DbN7o-Be.js → LandingPageProofDriven-C_LssmnA.js} +1 -1
- package/dist/assets/{LegalPage-DNGrrY0p.js → LegalPage-DGsyo4n1.js} +1 -1
- package/dist/assets/{PricingPage-Nczr3pRz.js → PricingPage-BOE27B-R.js} +1 -1
- package/dist/assets/{SettingsPage-DZlyu4d4.js → SettingsPage-f47cnk39.js} +1 -1
- package/dist/assets/{app-C9ct2hRD.js → app-D6ccu2Xx.js} +6854 -7373
- package/dist/assets/{backendInit-ymjonyQp.js → backendInit-DbTkQN9J.js} +2557 -809
- package/dist/assets/cli/{render-B_0lQwKU.js → render-BsngirjC.js} +114 -9
- package/dist/assets/{constructionHistoryWorker-CZ42Dksy.js → constructionHistoryWorker-PCwXrTDB.js} +175 -36
- package/dist/assets/{evalWorker-C2pm8LHP.js → evalWorker-CS63PfZu.js} +1125 -447
- package/dist/assets/{forgecad_geometry-BlMtqluF.js → forgecad_geometry-CZ_IfuvA.js} +1 -9
- package/dist/assets/{forgecad_geometry_bg-BllP_WiL.wasm → forgecad_geometry_bg-C3rQHfwg.wasm} +0 -0
- package/dist/assets/{inspectWorker-D5T5VbfK.js → inspectWorker-Y4cOzNyA.js} +4345 -373
- package/dist/assets/{jointPose-4r8ed8_5.js → jointPose-AMvCywzS.js} +1 -1
- package/dist/assets/{manifold-C4r6B-XY.js → manifold-CBry38ly.js} +2 -2
- package/dist/assets/{manifold-5PP1eGLN.js → manifold-Crd_F2qx.js} +1 -1
- package/dist/assets/{manifold-DjBkyIc8.js → manifold-k2kRcc85.js} +1 -1
- package/dist/assets/{reportWorker-CwenM7wB.js → reportWorker-CWvn0CEv.js} +1095 -400
- package/dist/cli/render.html +1 -1
- package/dist/docs/index.html +2 -2
- package/dist/docs-raw/AI/usage.md +2 -4
- package/dist/docs-raw/CLI.md +9 -7
- package/dist/docs-raw/README.md +1 -1
- package/dist/docs-raw/component-model.md +1 -1
- package/dist/docs-raw/generated/assembly.md +1 -1
- package/dist/docs-raw/generated/concepts.md +5 -3
- package/dist/docs-raw/generated/core.md +70 -1
- package/dist/docs-raw/generated/curves.md +8 -1
- package/dist/docs-raw/generated/output.md +0 -64
- package/dist/docs-raw/generated/runtime-names.md +6 -6
- package/dist/docs-raw/generated/viewport.md +3 -12
- package/dist/docs-raw/guides/inspection-bundles.md +1 -1
- package/dist/docs-raw/simulation-workflow.md +58 -0
- package/dist/docs-raw/skills/forgecad-blockout-model.md +1 -1
- package/dist/docs-raw/skills/forgecad-image-replicator.md +2 -2
- package/dist/docs-raw/skills/forgecad-mujoco-verify.md +78 -0
- package/dist/docs-raw/skills/forgecad-spec-by-walking-through-it.md +145 -0
- package/dist/docs-raw/skills/forgecad-visual-spec.md +1 -1
- package/dist/docs-raw/skills/forgecad.md +24 -24
- package/dist/docs-raw/skills/index.md +2 -3
- package/dist/index.html +1 -1
- package/dist/sitemap.xml +15 -15
- package/dist-cli/{check-compiler-SP7FAL7R.js → check-compiler-HPF2T2FS.js} +1 -1
- package/dist-cli/{check-query-propagation-BRLSHP22.js → check-query-propagation-HYSLTXAB.js} +1 -1
- package/dist-cli/{chunk-RQQ42YCP.js → chunk-WLUKAW3H.js} +1025 -158
- package/dist-cli/forgecad.js +2621 -232
- package/dist-cli/{forgecad_geometry-7TVSNVUB.js → forgecad_geometry-2IMYCUWW.js} +0 -8
- package/dist-cli/forgecad_geometry_bg.wasm +0 -0
- package/dist-skill/CONTEXT.md +85 -73
- package/dist-skill/SKILL.md +1 -1
- package/dist-skill/docs/CLI.md +9 -7
- package/dist-skill/docs/generated/assembly.md +1 -1
- package/dist-skill/docs/generated/core.md +70 -1
- package/dist-skill/docs/generated/curves.md +8 -1
- package/dist-skill/docs/generated/output.md +0 -64
- package/dist-skill/docs/generated/runtime-names.md +6 -6
- package/dist-skill/docs/generated/viewport.md +3 -12
- package/dist-skill/docs/guides/inspection-bundles.md +1 -1
- package/dist-skill/library/README.md +2 -3
- package/dist-skill/library/forgecad-blockout-model/SKILL.md +1 -1
- package/dist-skill/library/forgecad-image-replicator/SKILL.md +2 -2
- package/dist-skill/library/forgecad-mujoco-verify/SKILL.md +66 -0
- package/dist-skill/library/forgecad-mujoco-verify/scripts/mujoco_verify.py +385 -0
- package/dist-skill/library/forgecad-spec-by-walking-through-it/SKILL.md +132 -0
- package/dist-skill/library/forgecad-visual-spec/SKILL.md +1 -1
- package/dist-skill/website/skills/forgecad-blockout-model.md +1 -1
- package/dist-skill/website/skills/forgecad-image-replicator.md +2 -2
- package/dist-skill/website/skills/forgecad-mujoco-verify.md +78 -0
- package/dist-skill/website/skills/forgecad-spec-by-walking-through-it.md +145 -0
- package/dist-skill/website/skills/forgecad-visual-spec.md +1 -1
- package/dist-skill/website/skills/forgecad.md +24 -24
- package/dist-skill/website/skills/index.md +2 -3
- package/examples/analysis/clearance-fit.forge.js +31 -0
- package/examples/analysis/lever-arm-actuator.forge.js +43 -0
- package/examples/analysis/tipping-tripod.forge.js +35 -0
- package/examples/products/sportscar.forge.js +77 -0
- package/package.json +1 -3
- package/dist/docs-raw/skills/forgecad-high-level-spec.md +0 -101
- package/dist/docs-raw/skills/forgecad-lld.md +0 -41
- package/dist/docs-raw/skills/forgecad-prepare-prompt.md +0 -63
- package/dist-skill/library/forgecad-high-level-spec/SKILL.md +0 -94
- package/dist-skill/library/forgecad-lld/SKILL.md +0 -34
- package/dist-skill/library/forgecad-prepare-prompt/SKILL.md +0 -50
- package/dist-skill/website/skills/forgecad-high-level-spec.md +0 -101
- package/dist-skill/website/skills/forgecad-lld.md +0 -41
- package/dist-skill/website/skills/forgecad-prepare-prompt.md +0 -63
- /package/dist-skill/library/{forgecad-prepare-prompt → forgecad-spec-by-walking-through-it}/references/default-profiles.md +0 -0
- /package/dist-skill/library/{forgecad-prepare-prompt → forgecad-spec-by-walking-through-it}/references/master-prompt.md +0 -0
package/dist-cli/forgecad.js
CHANGED
|
@@ -58,7 +58,6 @@ import {
|
|
|
58
58
|
getCollectedIntentionalOverlaps,
|
|
59
59
|
getCollectedJointsView,
|
|
60
60
|
getCollectedPhysicalComponentExpectations,
|
|
61
|
-
getCollectedRobotExport,
|
|
62
61
|
getCompilerRegressionCorpusPart,
|
|
63
62
|
getDesignTraceInputs,
|
|
64
63
|
getDesignTraceUsedBy,
|
|
@@ -135,11 +134,11 @@ import {
|
|
|
135
134
|
updateConstraintValue,
|
|
136
135
|
validateSimulationModel,
|
|
137
136
|
wrapOCCTShapeBackend
|
|
138
|
-
} from "./chunk-
|
|
137
|
+
} from "./chunk-WLUKAW3H.js";
|
|
139
138
|
|
|
140
139
|
// cli/forgecad.ts
|
|
141
140
|
import { Command, Flags, Help, flush as flushOclif, handle as handleOclif, run as runOclif } from "@oclif/core";
|
|
142
|
-
import { readFileSync as
|
|
141
|
+
import { readFileSync as readFileSync28 } from "fs";
|
|
143
142
|
|
|
144
143
|
// cli/check-api-contracts.ts
|
|
145
144
|
import assert from "assert/strict";
|
|
@@ -5065,12 +5064,6 @@ function applyNonPartExpectations(entryPath, result, expectations) {
|
|
|
5065
5064
|
assertMinimum(entryPath, result.cutPlanes.length, expectations.minCutPlanes, "cut plane(s)");
|
|
5066
5065
|
assertMinimum(entryPath, jointCount, expectations.minJoints, "joint control(s)");
|
|
5067
5066
|
assertMinimum(entryPath, animationCount, expectations.minAnimations, "joint animation(s)");
|
|
5068
|
-
if (expectations.requireRobotExport || expectations.minRobotParts != null || expectations.minRobotJoints != null) {
|
|
5069
|
-
assert4(result.robotExport, `${entryPath}: expected robotExport(...) data to stay available to the example gate`);
|
|
5070
|
-
if (!result.robotExport) return;
|
|
5071
|
-
assertMinimum(entryPath, result.robotExport.assembly.parts.length, expectations.minRobotParts, "robot part(s)");
|
|
5072
|
-
assertMinimum(entryPath, result.robotExport.assembly.joints.length, expectations.minRobotJoints, "robot joint(s)");
|
|
5073
|
-
}
|
|
5074
5067
|
}
|
|
5075
5068
|
function validateAssemblyEntry(entry) {
|
|
5076
5069
|
const result = executeExample(entry);
|
|
@@ -5536,8 +5529,8 @@ function requireSingleInputForOutputPath(inputPaths, outputPath, flag = "--outpu
|
|
|
5536
5529
|
throw new Error(`Cannot use ${flag} <${valueKind}> with multiple inputs. Omit ${flag} to use per-input defaults.`);
|
|
5537
5530
|
}
|
|
5538
5531
|
}
|
|
5539
|
-
function batchOutputPath(inputPath, explicitOutputPath,
|
|
5540
|
-
return explicitOutputPath ??
|
|
5532
|
+
function batchOutputPath(inputPath, explicitOutputPath, defaultOutputPath13) {
|
|
5533
|
+
return explicitOutputPath ?? defaultOutputPath13(inputPath);
|
|
5541
5534
|
}
|
|
5542
5535
|
function printBatchHeader(inputPath, index, total) {
|
|
5543
5536
|
if (total <= 1) return;
|
|
@@ -14743,10 +14736,10 @@ function polygonCentroid(points) {
|
|
|
14743
14736
|
for (let i = 0; i < points.length; i += 1) {
|
|
14744
14737
|
const [x1, y1] = points[i];
|
|
14745
14738
|
const [x2, y2] = points[(i + 1) % points.length];
|
|
14746
|
-
const
|
|
14747
|
-
a2 +=
|
|
14748
|
-
cx += (x1 + x2) *
|
|
14749
|
-
cy += (y1 + y2) *
|
|
14739
|
+
const cross8 = x1 * y2 - x2 * y1;
|
|
14740
|
+
a2 += cross8;
|
|
14741
|
+
cx += (x1 + x2) * cross8;
|
|
14742
|
+
cy += (y1 + y2) * cross8;
|
|
14750
14743
|
}
|
|
14751
14744
|
if (Math.abs(a2) < EPS5) {
|
|
14752
14745
|
return points.length > 0 ? [points.reduce((sum2, p) => sum2 + p[0], 0) / points.length, points.reduce((sum2, p) => sum2 + p[1], 0) / points.length] : [0, 0];
|
|
@@ -17080,7 +17073,6 @@ var ESTABLISHED_LOWERCASE_RUNTIME_GLOBALS = /* @__PURE__ */ new Set([
|
|
|
17080
17073
|
"sketchToDxf",
|
|
17081
17074
|
"bom",
|
|
17082
17075
|
"sheetStock",
|
|
17083
|
-
"robotExport",
|
|
17084
17076
|
"group",
|
|
17085
17077
|
"console",
|
|
17086
17078
|
"cutPlane",
|
|
@@ -17293,7 +17285,7 @@ function checkRuntimeNamesDoc(runtimeNames) {
|
|
|
17293
17285
|
const compatibilityList = renderRuntimeNameList(collisionReservedNames.filter((name) => SKILL_SUPPRESSED_RUNTIME_NAMES.has(name)));
|
|
17294
17286
|
let doc;
|
|
17295
17287
|
try {
|
|
17296
|
-
doc = readText("docs/
|
|
17288
|
+
doc = readText("docs/skill/generated/runtime-names.md");
|
|
17297
17289
|
} catch {
|
|
17298
17290
|
return "Generated runtime names doc is missing. Run npm run gen:docs.";
|
|
17299
17291
|
}
|
|
@@ -17372,6 +17364,7 @@ import { readFileSync as readFileSync10 } from "fs";
|
|
|
17372
17364
|
import { resolve as resolve12 } from "path";
|
|
17373
17365
|
function parseArgs4(argv) {
|
|
17374
17366
|
const inputPaths = [];
|
|
17367
|
+
const paramOverrides = {};
|
|
17375
17368
|
const jointOverrides = {};
|
|
17376
17369
|
let json = false;
|
|
17377
17370
|
let compact = false;
|
|
@@ -17397,14 +17390,24 @@ function parseArgs4(argv) {
|
|
|
17397
17390
|
i += 1;
|
|
17398
17391
|
continue;
|
|
17399
17392
|
}
|
|
17393
|
+
if (arg === "--param" || arg === "-p") {
|
|
17394
|
+
const value = argv[i + 1];
|
|
17395
|
+
if (!value || value.startsWith("--")) throw new Error(`${arg} requires Key=Value`);
|
|
17396
|
+
addCliParamOverride(value, paramOverrides);
|
|
17397
|
+
i += 1;
|
|
17398
|
+
continue;
|
|
17399
|
+
}
|
|
17400
17400
|
if (arg.startsWith("--")) {
|
|
17401
17401
|
throw new Error(`Unknown flag: ${arg}`);
|
|
17402
17402
|
}
|
|
17403
17403
|
inputPaths.push(arg);
|
|
17404
17404
|
}
|
|
17405
|
-
requireInputPaths(
|
|
17405
|
+
requireInputPaths(
|
|
17406
|
+
inputPaths,
|
|
17407
|
+
"Usage: forgecad check simready <script.forge.js> [input ...] [--json] [--param Key=Value] [--joint JointName=Value]"
|
|
17408
|
+
);
|
|
17406
17409
|
requireScriptInputPaths(inputPaths);
|
|
17407
|
-
return { inputPaths, json, compact, noFailExit, jointOverrides };
|
|
17410
|
+
return { inputPaths, json, compact, noFailExit, paramOverrides, jointOverrides };
|
|
17408
17411
|
}
|
|
17409
17412
|
function finding2(code, message, path5) {
|
|
17410
17413
|
return { level: "error", code, message, path: path5 };
|
|
@@ -17422,10 +17425,6 @@ function failedReport(source, modelName, errorMessage2, code = "SIM.CHECK.ERROR"
|
|
|
17422
17425
|
};
|
|
17423
17426
|
}
|
|
17424
17427
|
function resolveSimulationModel(result, jointOverrides) {
|
|
17425
|
-
const legacy = getCollectedRobotExport();
|
|
17426
|
-
if (legacy) {
|
|
17427
|
-
return { ...legacy, state: { ...legacy.state, ...jointOverrides } };
|
|
17428
|
-
}
|
|
17429
17428
|
const assembly2 = getRunResultAssemblySource(result);
|
|
17430
17429
|
if (!assembly2) return null;
|
|
17431
17430
|
return collectSimulationModel(assembly2.describe(), { state: jointOverrides });
|
|
@@ -17477,6 +17476,7 @@ function runCheckSimReadyInput(options, scriptPath) {
|
|
|
17477
17476
|
const scriptAbs = resolve12(scriptPath);
|
|
17478
17477
|
const code = readFileSync10(scriptAbs, "utf-8");
|
|
17479
17478
|
const { allFiles, fileName } = collectProjectFiles(scriptAbs);
|
|
17479
|
+
setParamOverrides(options.paramOverrides);
|
|
17480
17480
|
const runResult = applyCliJointOverrides(
|
|
17481
17481
|
runScript(code, fileName, allFiles, { assemblyState: options.jointOverrides }),
|
|
17482
17482
|
options.jointOverrides
|
|
@@ -17488,12 +17488,7 @@ function runCheckSimReadyInput(options, scriptPath) {
|
|
|
17488
17488
|
try {
|
|
17489
17489
|
const model = resolveSimulationModel(runResult, options.jointOverrides);
|
|
17490
17490
|
if (!model) {
|
|
17491
|
-
report = failedReport(
|
|
17492
|
-
scriptPath,
|
|
17493
|
-
scriptPath,
|
|
17494
|
-
"Model must return assembly(...).withSimulation(...), or call legacy robotExport({...}).",
|
|
17495
|
-
"SIM.SIMULATION.MISSING"
|
|
17496
|
-
);
|
|
17491
|
+
report = failedReport(scriptPath, scriptPath, "Model must return assembly(...).withSimulation(...).", "SIM.SIMULATION.MISSING");
|
|
17497
17492
|
} else {
|
|
17498
17493
|
report = { source: scriptPath, ...validateSimulationModel(model) };
|
|
17499
17494
|
}
|
|
@@ -17504,6 +17499,647 @@ function runCheckSimReadyInput(options, scriptPath) {
|
|
|
17504
17499
|
return report;
|
|
17505
17500
|
}
|
|
17506
17501
|
|
|
17502
|
+
// src/forge/analysis/massProperties.ts
|
|
17503
|
+
function meshSurfaceAreaMm2(mesh) {
|
|
17504
|
+
const { numProp, numTri, triVerts, vertProperties } = mesh;
|
|
17505
|
+
let area = 0;
|
|
17506
|
+
for (let t = 0; t < numTri; t += 1) {
|
|
17507
|
+
const i0 = triVerts[t * 3] * numProp;
|
|
17508
|
+
const i1 = triVerts[t * 3 + 1] * numProp;
|
|
17509
|
+
const i2 = triVerts[t * 3 + 2] * numProp;
|
|
17510
|
+
const e1x = vertProperties[i1] - vertProperties[i0];
|
|
17511
|
+
const e1y = vertProperties[i1 + 1] - vertProperties[i0 + 1];
|
|
17512
|
+
const e1z = vertProperties[i1 + 2] - vertProperties[i0 + 2];
|
|
17513
|
+
const e2x = vertProperties[i2] - vertProperties[i0];
|
|
17514
|
+
const e2y = vertProperties[i2 + 1] - vertProperties[i0 + 1];
|
|
17515
|
+
const e2z = vertProperties[i2 + 2] - vertProperties[i0 + 2];
|
|
17516
|
+
const cx = e1y * e2z - e1z * e2y;
|
|
17517
|
+
const cy = e1z * e2x - e1x * e2z;
|
|
17518
|
+
const cz = e1x * e2y - e1y * e2x;
|
|
17519
|
+
area += 0.5 * Math.sqrt(cx * cx + cy * cy + cz * cz);
|
|
17520
|
+
}
|
|
17521
|
+
return area;
|
|
17522
|
+
}
|
|
17523
|
+
function accumulateBounds(mesh, min, max) {
|
|
17524
|
+
const { numProp, vertProperties } = mesh;
|
|
17525
|
+
const numVerts = vertProperties.length / numProp;
|
|
17526
|
+
for (let v = 0; v < numVerts; v += 1) {
|
|
17527
|
+
for (let axis = 0; axis < 3; axis += 1) {
|
|
17528
|
+
const value = vertProperties[v * numProp + axis];
|
|
17529
|
+
if (value < min[axis]) min[axis] = value;
|
|
17530
|
+
if (value > max[axis]) max[axis] = value;
|
|
17531
|
+
}
|
|
17532
|
+
}
|
|
17533
|
+
}
|
|
17534
|
+
function symmetricEigen3(m) {
|
|
17535
|
+
const a = [
|
|
17536
|
+
[m[0][0], m[0][1], m[0][2]],
|
|
17537
|
+
[m[1][0], m[1][1], m[1][2]],
|
|
17538
|
+
[m[2][0], m[2][1], m[2][2]]
|
|
17539
|
+
];
|
|
17540
|
+
const v = [
|
|
17541
|
+
[1, 0, 0],
|
|
17542
|
+
[0, 1, 0],
|
|
17543
|
+
[0, 0, 1]
|
|
17544
|
+
];
|
|
17545
|
+
for (let sweep = 0; sweep < 100; sweep += 1) {
|
|
17546
|
+
const off = Math.abs(a[0][1]) + Math.abs(a[0][2]) + Math.abs(a[1][2]);
|
|
17547
|
+
if (off < 1e-20) break;
|
|
17548
|
+
for (let p = 0; p < 2; p += 1) {
|
|
17549
|
+
for (let q = p + 1; q < 3; q += 1) {
|
|
17550
|
+
if (Math.abs(a[p][q]) < 1e-300) continue;
|
|
17551
|
+
const theta = (a[q][q] - a[p][p]) / (2 * a[p][q]);
|
|
17552
|
+
const t = Math.sign(theta || 1) / (Math.abs(theta) + Math.sqrt(theta * theta + 1));
|
|
17553
|
+
const c = 1 / Math.sqrt(t * t + 1);
|
|
17554
|
+
const s = t * c;
|
|
17555
|
+
for (let i = 0; i < 3; i += 1) {
|
|
17556
|
+
const aip = a[i][p];
|
|
17557
|
+
const aiq = a[i][q];
|
|
17558
|
+
a[i][p] = c * aip - s * aiq;
|
|
17559
|
+
a[i][q] = s * aip + c * aiq;
|
|
17560
|
+
}
|
|
17561
|
+
for (let i = 0; i < 3; i += 1) {
|
|
17562
|
+
const api = a[p][i];
|
|
17563
|
+
const aqi = a[q][i];
|
|
17564
|
+
a[p][i] = c * api - s * aqi;
|
|
17565
|
+
a[q][i] = s * api + c * aqi;
|
|
17566
|
+
}
|
|
17567
|
+
for (let i = 0; i < 3; i += 1) {
|
|
17568
|
+
const vip = v[i][p];
|
|
17569
|
+
const viq = v[i][q];
|
|
17570
|
+
v[i][p] = c * vip - s * viq;
|
|
17571
|
+
v[i][q] = s * vip + c * viq;
|
|
17572
|
+
}
|
|
17573
|
+
}
|
|
17574
|
+
}
|
|
17575
|
+
}
|
|
17576
|
+
const eigen = [
|
|
17577
|
+
{ value: a[0][0], vector: [v[0][0], v[1][0], v[2][0]] },
|
|
17578
|
+
{ value: a[1][1], vector: [v[0][1], v[1][1], v[2][1]] },
|
|
17579
|
+
{ value: a[2][2], vector: [v[0][2], v[1][2], v[2][2]] }
|
|
17580
|
+
];
|
|
17581
|
+
eigen.sort((x, y) => y.value - x.value);
|
|
17582
|
+
return {
|
|
17583
|
+
values: [eigen[0].value, eigen[1].value, eigen[2].value],
|
|
17584
|
+
vectors: [eigen[0].vector, eigen[1].vector, eigen[2].vector]
|
|
17585
|
+
};
|
|
17586
|
+
}
|
|
17587
|
+
function analyzeMassProperties(objects) {
|
|
17588
|
+
if (objects.length === 0) {
|
|
17589
|
+
throw new Error("analyzeMassProperties requires at least one object");
|
|
17590
|
+
}
|
|
17591
|
+
const perObject = [];
|
|
17592
|
+
let surfaceAreaMm2 = 0;
|
|
17593
|
+
const min = [Infinity, Infinity, Infinity];
|
|
17594
|
+
const max = [-Infinity, -Infinity, -Infinity];
|
|
17595
|
+
const inertiaAboutOwnCom = [];
|
|
17596
|
+
for (const obj of objects) {
|
|
17597
|
+
const massPlaceholder = 1;
|
|
17598
|
+
const probe = computeMeshInertia(obj.mesh, massPlaceholder);
|
|
17599
|
+
const volumeMm3 = probe.volumeMm3;
|
|
17600
|
+
const volumeM3 = volumeMm3 * 1e-9;
|
|
17601
|
+
const massKg = obj.densityKgM3 * volumeM3;
|
|
17602
|
+
inertiaAboutOwnCom.push({
|
|
17603
|
+
ixx: probe.ixx * massKg,
|
|
17604
|
+
iyy: probe.iyy * massKg,
|
|
17605
|
+
izz: probe.izz * massKg,
|
|
17606
|
+
ixy: probe.ixy * massKg,
|
|
17607
|
+
ixz: probe.ixz * massKg,
|
|
17608
|
+
iyz: probe.iyz * massKg
|
|
17609
|
+
});
|
|
17610
|
+
perObject.push({
|
|
17611
|
+
name: obj.name,
|
|
17612
|
+
volumeMm3,
|
|
17613
|
+
massKg,
|
|
17614
|
+
densityKgM3: obj.densityKgM3,
|
|
17615
|
+
centerOfMassMm: probe.centerOfMass
|
|
17616
|
+
});
|
|
17617
|
+
surfaceAreaMm2 += meshSurfaceAreaMm2(obj.mesh);
|
|
17618
|
+
accumulateBounds(obj.mesh, min, max);
|
|
17619
|
+
}
|
|
17620
|
+
const totalVolumeMm3 = perObject.reduce((sum2, o) => sum2 + o.volumeMm3, 0);
|
|
17621
|
+
const totalMassKg = perObject.reduce((sum2, o) => sum2 + o.massKg, 0);
|
|
17622
|
+
const weights = totalMassKg > 0 ? perObject.map((o) => o.massKg) : perObject.map((o) => o.volumeMm3);
|
|
17623
|
+
const weightTotal = weights.reduce((a, b) => a + b, 0) || 1;
|
|
17624
|
+
const com = [0, 0, 0];
|
|
17625
|
+
perObject.forEach((o, i) => {
|
|
17626
|
+
com[0] += weights[i] * o.centerOfMassMm[0] / weightTotal;
|
|
17627
|
+
com[1] += weights[i] * o.centerOfMassMm[1] / weightTotal;
|
|
17628
|
+
com[2] += weights[i] * o.centerOfMassMm[2] / weightTotal;
|
|
17629
|
+
});
|
|
17630
|
+
const tensor = { ixx: 0, iyy: 0, izz: 0, ixy: 0, ixz: 0, iyz: 0 };
|
|
17631
|
+
perObject.forEach((o, i) => {
|
|
17632
|
+
const own = inertiaAboutOwnCom[i];
|
|
17633
|
+
const dx = (o.centerOfMassMm[0] - com[0]) * 1e-3;
|
|
17634
|
+
const dy = (o.centerOfMassMm[1] - com[1]) * 1e-3;
|
|
17635
|
+
const dz = (o.centerOfMassMm[2] - com[2]) * 1e-3;
|
|
17636
|
+
const m = o.massKg;
|
|
17637
|
+
tensor.ixx += own.ixx + m * (dy * dy + dz * dz);
|
|
17638
|
+
tensor.iyy += own.iyy + m * (dx * dx + dz * dz);
|
|
17639
|
+
tensor.izz += own.izz + m * (dx * dx + dy * dy);
|
|
17640
|
+
tensor.ixy += own.ixy - m * dx * dy;
|
|
17641
|
+
tensor.ixz += own.ixz - m * dx * dz;
|
|
17642
|
+
tensor.iyz += own.iyz - m * dy * dz;
|
|
17643
|
+
});
|
|
17644
|
+
const eig = symmetricEigen3([
|
|
17645
|
+
[tensor.ixx, tensor.ixy, tensor.ixz],
|
|
17646
|
+
[tensor.ixy, tensor.iyy, tensor.iyz],
|
|
17647
|
+
[tensor.ixz, tensor.iyz, tensor.izz]
|
|
17648
|
+
]);
|
|
17649
|
+
const principalMomentsKgM2 = eig.values;
|
|
17650
|
+
const radiusOfGyrationM = totalMassKg > 0 ? [
|
|
17651
|
+
Math.sqrt(Math.max(0, principalMomentsKgM2[0]) / totalMassKg),
|
|
17652
|
+
Math.sqrt(Math.max(0, principalMomentsKgM2[1]) / totalMassKg),
|
|
17653
|
+
Math.sqrt(Math.max(0, principalMomentsKgM2[2]) / totalMassKg)
|
|
17654
|
+
] : [0, 0, 0];
|
|
17655
|
+
const size = [max[0] - min[0], max[1] - min[1], max[2] - min[2]];
|
|
17656
|
+
return {
|
|
17657
|
+
totalVolumeMm3,
|
|
17658
|
+
totalMassKg,
|
|
17659
|
+
centerOfMassMm: com,
|
|
17660
|
+
inertiaTensorKgM2: tensor,
|
|
17661
|
+
principalMomentsKgM2,
|
|
17662
|
+
principalAxes: eig.vectors,
|
|
17663
|
+
radiusOfGyrationM,
|
|
17664
|
+
surfaceAreaMm2,
|
|
17665
|
+
boundingBoxMm: { minMm: min, maxMm: max, sizeMm: size },
|
|
17666
|
+
perObject
|
|
17667
|
+
};
|
|
17668
|
+
}
|
|
17669
|
+
|
|
17670
|
+
// src/forge/analysis/stability.ts
|
|
17671
|
+
function axisIndex(up) {
|
|
17672
|
+
switch (up) {
|
|
17673
|
+
case "x":
|
|
17674
|
+
return { upIdx: 0, planeA: 1, planeB: 2 };
|
|
17675
|
+
case "y":
|
|
17676
|
+
return { upIdx: 1, planeA: 2, planeB: 0 };
|
|
17677
|
+
default:
|
|
17678
|
+
return { upIdx: 2, planeA: 0, planeB: 1 };
|
|
17679
|
+
}
|
|
17680
|
+
}
|
|
17681
|
+
function convexHull2D(points) {
|
|
17682
|
+
const pts = points.slice().sort((p, q) => p[0] === q[0] ? p[1] - q[1] : p[0] - q[0]);
|
|
17683
|
+
const unique = [];
|
|
17684
|
+
for (const p of pts) {
|
|
17685
|
+
const last = unique[unique.length - 1];
|
|
17686
|
+
if (!last || last[0] !== p[0] || last[1] !== p[1]) unique.push(p);
|
|
17687
|
+
}
|
|
17688
|
+
if (unique.length < 3) return unique;
|
|
17689
|
+
const cross8 = (o, a, b) => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
|
|
17690
|
+
const lower2 = [];
|
|
17691
|
+
for (const p of unique) {
|
|
17692
|
+
while (lower2.length >= 2 && cross8(lower2[lower2.length - 2], lower2[lower2.length - 1], p) <= 0) lower2.pop();
|
|
17693
|
+
lower2.push(p);
|
|
17694
|
+
}
|
|
17695
|
+
const upper = [];
|
|
17696
|
+
for (let i = unique.length - 1; i >= 0; i -= 1) {
|
|
17697
|
+
const p = unique[i];
|
|
17698
|
+
while (upper.length >= 2 && cross8(upper[upper.length - 2], upper[upper.length - 1], p) <= 0) upper.pop();
|
|
17699
|
+
upper.push(p);
|
|
17700
|
+
}
|
|
17701
|
+
lower2.pop();
|
|
17702
|
+
upper.pop();
|
|
17703
|
+
return lower2.concat(upper);
|
|
17704
|
+
}
|
|
17705
|
+
function distancePointToSegment(p, a, b) {
|
|
17706
|
+
const dx = b[0] - a[0];
|
|
17707
|
+
const dy = b[1] - a[1];
|
|
17708
|
+
const lenSq = dx * dx + dy * dy;
|
|
17709
|
+
if (lenSq < 1e-18) return Math.hypot(p[0] - a[0], p[1] - a[1]);
|
|
17710
|
+
let t = ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / lenSq;
|
|
17711
|
+
t = Math.max(0, Math.min(1, t));
|
|
17712
|
+
return Math.hypot(p[0] - (a[0] + t * dx), p[1] - (a[1] + t * dy));
|
|
17713
|
+
}
|
|
17714
|
+
function signedDistanceToPolygon(p, poly) {
|
|
17715
|
+
let minDist = Infinity;
|
|
17716
|
+
let nearestEdge = 0;
|
|
17717
|
+
let inside = true;
|
|
17718
|
+
for (let i = 0; i < poly.length; i += 1) {
|
|
17719
|
+
const a = poly[i];
|
|
17720
|
+
const b = poly[(i + 1) % poly.length];
|
|
17721
|
+
const cross8 = (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]);
|
|
17722
|
+
if (cross8 < 0) inside = false;
|
|
17723
|
+
const d = distancePointToSegment(p, a, b);
|
|
17724
|
+
if (d < minDist) {
|
|
17725
|
+
minDist = d;
|
|
17726
|
+
nearestEdge = i;
|
|
17727
|
+
}
|
|
17728
|
+
}
|
|
17729
|
+
return { distance: inside ? minDist : -minDist, nearestEdge };
|
|
17730
|
+
}
|
|
17731
|
+
function analyzeStability(meshes, centerOfMassMm, options = {}) {
|
|
17732
|
+
const up = options.up ?? "z";
|
|
17733
|
+
const { upIdx, planeA, planeB } = axisIndex(up);
|
|
17734
|
+
const minMarginMm = options.minMarginMm ?? 0;
|
|
17735
|
+
let groundLevelMm = Infinity;
|
|
17736
|
+
for (const mesh of meshes) {
|
|
17737
|
+
const numVerts = mesh.vertProperties.length / mesh.numProp;
|
|
17738
|
+
for (let v = 0; v < numVerts; v += 1) {
|
|
17739
|
+
const value = mesh.vertProperties[v * mesh.numProp + upIdx];
|
|
17740
|
+
if (value < groundLevelMm) groundLevelMm = value;
|
|
17741
|
+
}
|
|
17742
|
+
}
|
|
17743
|
+
if (!Number.isFinite(groundLevelMm)) {
|
|
17744
|
+
throw new Error("analyzeStability: geometry has no vertices");
|
|
17745
|
+
}
|
|
17746
|
+
let contactToleranceMm = options.contactToleranceMm ?? 0;
|
|
17747
|
+
if (!options.contactToleranceMm) {
|
|
17748
|
+
let span = 0;
|
|
17749
|
+
for (const mesh of meshes) {
|
|
17750
|
+
const numVerts = mesh.vertProperties.length / mesh.numProp;
|
|
17751
|
+
for (let v = 0; v < numVerts; v += 1) {
|
|
17752
|
+
const value = mesh.vertProperties[v * mesh.numProp + upIdx];
|
|
17753
|
+
span = Math.max(span, value - groundLevelMm);
|
|
17754
|
+
}
|
|
17755
|
+
}
|
|
17756
|
+
contactToleranceMm = Math.max(0.05, span * 2e-3);
|
|
17757
|
+
}
|
|
17758
|
+
const contactPoints = [];
|
|
17759
|
+
for (const mesh of meshes) {
|
|
17760
|
+
const numVerts = mesh.vertProperties.length / mesh.numProp;
|
|
17761
|
+
for (let v = 0; v < numVerts; v += 1) {
|
|
17762
|
+
const base = v * mesh.numProp;
|
|
17763
|
+
if (mesh.vertProperties[base + upIdx] <= groundLevelMm + contactToleranceMm) {
|
|
17764
|
+
contactPoints.push([mesh.vertProperties[base + planeA], mesh.vertProperties[base + planeB]]);
|
|
17765
|
+
}
|
|
17766
|
+
}
|
|
17767
|
+
}
|
|
17768
|
+
const supportPolygonMm = convexHull2D(contactPoints);
|
|
17769
|
+
const comProjectionMm = [centerOfMassMm[planeA], centerOfMassMm[planeB]];
|
|
17770
|
+
const comHeightMm = centerOfMassMm[upIdx] - groundLevelMm;
|
|
17771
|
+
const degenerate = supportPolygonMm.length < 3;
|
|
17772
|
+
if (degenerate) {
|
|
17773
|
+
return {
|
|
17774
|
+
up,
|
|
17775
|
+
groundLevelMm,
|
|
17776
|
+
centerOfMassMm,
|
|
17777
|
+
comProjectionMm,
|
|
17778
|
+
comHeightMm,
|
|
17779
|
+
supportPolygonMm,
|
|
17780
|
+
contactVertexCount: contactPoints.length,
|
|
17781
|
+
comInsideSupport: false,
|
|
17782
|
+
tipOverMarginMm: 0,
|
|
17783
|
+
tipOverAngleDeg: 0,
|
|
17784
|
+
criticalEdge: null,
|
|
17785
|
+
tipDirection: null,
|
|
17786
|
+
comShiftToTargetMm: null,
|
|
17787
|
+
degenerate: true
|
|
17788
|
+
};
|
|
17789
|
+
}
|
|
17790
|
+
const { distance: distance2, nearestEdge } = signedDistanceToPolygon(comProjectionMm, supportPolygonMm);
|
|
17791
|
+
const a = supportPolygonMm[nearestEdge];
|
|
17792
|
+
const b = supportPolygonMm[(nearestEdge + 1) % supportPolygonMm.length];
|
|
17793
|
+
const ex = b[0] - a[0];
|
|
17794
|
+
const ey = b[1] - a[1];
|
|
17795
|
+
const edgeLen = Math.hypot(ex, ey) || 1;
|
|
17796
|
+
const nx = ey / edgeLen;
|
|
17797
|
+
const ny = -ex / edgeLen;
|
|
17798
|
+
const tipDirection = [nx, ny];
|
|
17799
|
+
const tipOverAngleDeg = comHeightMm > 1e-9 ? Math.atan2(distance2, comHeightMm) * 180 / Math.PI : distance2 >= 0 ? 90 : -90;
|
|
17800
|
+
let comShiftToTargetMm = null;
|
|
17801
|
+
if (distance2 < minMarginMm) {
|
|
17802
|
+
const deficit = minMarginMm - distance2;
|
|
17803
|
+
comShiftToTargetMm = [-nx * deficit, -ny * deficit];
|
|
17804
|
+
}
|
|
17805
|
+
return {
|
|
17806
|
+
up,
|
|
17807
|
+
groundLevelMm,
|
|
17808
|
+
centerOfMassMm,
|
|
17809
|
+
comProjectionMm,
|
|
17810
|
+
comHeightMm,
|
|
17811
|
+
supportPolygonMm,
|
|
17812
|
+
contactVertexCount: contactPoints.length,
|
|
17813
|
+
comInsideSupport: distance2 >= 0,
|
|
17814
|
+
tipOverMarginMm: distance2,
|
|
17815
|
+
tipOverAngleDeg,
|
|
17816
|
+
criticalEdge: { a, b },
|
|
17817
|
+
tipDirection,
|
|
17818
|
+
comShiftToTargetMm,
|
|
17819
|
+
degenerate: false
|
|
17820
|
+
};
|
|
17821
|
+
}
|
|
17822
|
+
|
|
17823
|
+
// src/forge/analysis/feedback.ts
|
|
17824
|
+
var FEEDBACK_SCHEMA = "forgecad.feedback/v1";
|
|
17825
|
+
function evaluateTarget(value, target) {
|
|
17826
|
+
switch (target.op) {
|
|
17827
|
+
case ">=":
|
|
17828
|
+
return value >= target.value;
|
|
17829
|
+
case "<=":
|
|
17830
|
+
return value <= target.value;
|
|
17831
|
+
case ">":
|
|
17832
|
+
return value > target.value;
|
|
17833
|
+
case "<":
|
|
17834
|
+
return value < target.value;
|
|
17835
|
+
case "==":
|
|
17836
|
+
return value === target.value;
|
|
17837
|
+
default:
|
|
17838
|
+
return false;
|
|
17839
|
+
}
|
|
17840
|
+
}
|
|
17841
|
+
function makeMetric(key, label, value, options) {
|
|
17842
|
+
const { unit, target, status: forced, warnFraction = 0.1 } = options;
|
|
17843
|
+
let status = forced ?? "info";
|
|
17844
|
+
if (!forced && target && value !== null && Number.isFinite(value)) {
|
|
17845
|
+
const passed = evaluateTarget(value, target);
|
|
17846
|
+
if (!passed) {
|
|
17847
|
+
status = "fail";
|
|
17848
|
+
} else {
|
|
17849
|
+
const span = Math.abs(target.value) || 1;
|
|
17850
|
+
const slack = Math.abs(value - target.value) / span;
|
|
17851
|
+
status = slack < warnFraction ? "warn" : "pass";
|
|
17852
|
+
}
|
|
17853
|
+
} else if (!forced && value === null) {
|
|
17854
|
+
status = "info";
|
|
17855
|
+
}
|
|
17856
|
+
return target ? { key, label, value, unit, status, target } : { key, label, value, unit, status };
|
|
17857
|
+
}
|
|
17858
|
+
function reportOk(metrics, findings) {
|
|
17859
|
+
if (metrics.some((m) => m.status === "fail")) return false;
|
|
17860
|
+
if (findings.some((f2) => f2.level === "error")) return false;
|
|
17861
|
+
return true;
|
|
17862
|
+
}
|
|
17863
|
+
function statusGlyph(status) {
|
|
17864
|
+
switch (status) {
|
|
17865
|
+
case "pass":
|
|
17866
|
+
return "\u2713";
|
|
17867
|
+
case "warn":
|
|
17868
|
+
return "\u26A0";
|
|
17869
|
+
case "fail":
|
|
17870
|
+
return "\u2717";
|
|
17871
|
+
default:
|
|
17872
|
+
return "\xB7";
|
|
17873
|
+
}
|
|
17874
|
+
}
|
|
17875
|
+
function formatValue(value) {
|
|
17876
|
+
if (value === null || !Number.isFinite(value)) return "n/a";
|
|
17877
|
+
const abs = Math.abs(value);
|
|
17878
|
+
if (abs !== 0 && (abs < 1e-3 || abs >= 1e6)) return value.toExponential(3);
|
|
17879
|
+
return Number.parseFloat(value.toPrecision(6)).toString();
|
|
17880
|
+
}
|
|
17881
|
+
function renderFeedbackText(report) {
|
|
17882
|
+
const lines = [];
|
|
17883
|
+
const head = report.ok ? "\u2713" : "\u2717";
|
|
17884
|
+
lines.push(`${head} ${report.category}: ${report.source}`);
|
|
17885
|
+
lines.push(` ${report.summary}`);
|
|
17886
|
+
for (const metric of report.metrics) {
|
|
17887
|
+
const target = metric.target ? ` (target ${metric.target.op} ${formatValue(metric.target.value)} ${metric.unit})` : "";
|
|
17888
|
+
lines.push(` ${statusGlyph(metric.status)} ${metric.label}: ${formatValue(metric.value)} ${metric.unit}${target}`);
|
|
17889
|
+
}
|
|
17890
|
+
for (const finding3 of report.findings) {
|
|
17891
|
+
const glyph = finding3.level === "error" ? "\u2717" : finding3.level === "warning" ? "\u26A0" : "\xB7";
|
|
17892
|
+
const suggest = finding3.suggest ? ` \u2192 ${finding3.suggest}` : "";
|
|
17893
|
+
const path5 = finding3.path ? ` (${finding3.path})` : "";
|
|
17894
|
+
lines.push(` ${glyph} [${finding3.code}] ${finding3.message}${path5}${suggest}`);
|
|
17895
|
+
}
|
|
17896
|
+
if (report.trust.assumptions.length > 0) {
|
|
17897
|
+
lines.push(` assumptions: ${report.trust.assumptions.join("; ")}`);
|
|
17898
|
+
}
|
|
17899
|
+
return lines.join("\n");
|
|
17900
|
+
}
|
|
17901
|
+
|
|
17902
|
+
// cli/analysis-shared.ts
|
|
17903
|
+
async function loadAnalysisObjectsWithAssembly(scriptPath, options) {
|
|
17904
|
+
const input = loadCliScriptInput(scriptPath);
|
|
17905
|
+
await initCliBackend(resolveCliBackend(options.backend, input) ?? CLI_DEFAULT_BACKEND);
|
|
17906
|
+
const qualityPreset = options.quality && options.quality !== "default" ? options.quality : void 0;
|
|
17907
|
+
setParamOverrides(options.paramOverrides);
|
|
17908
|
+
const runResult = applyCliJointOverrides(
|
|
17909
|
+
runScript(input.code, input.fileName, input.allFiles, {
|
|
17910
|
+
...qualityPreset ? { quality: qualityPreset } : {},
|
|
17911
|
+
readBinaryFile: input.readBinaryFile,
|
|
17912
|
+
assemblyState: options.jointOverrides
|
|
17913
|
+
}),
|
|
17914
|
+
options.jointOverrides
|
|
17915
|
+
);
|
|
17916
|
+
if (runResult.error) {
|
|
17917
|
+
return { objects: [], assembly: null, runResult, error: runResult.error };
|
|
17918
|
+
}
|
|
17919
|
+
const objects = runResult.objects.filter((obj) => obj.shape && !obj.mock).map((obj) => ({ name: obj.name, mesh: obj.shape.getMesh() }));
|
|
17920
|
+
if (objects.length === 0) {
|
|
17921
|
+
return { objects: [], assembly: null, runResult, error: "No solid geometry found in the model." };
|
|
17922
|
+
}
|
|
17923
|
+
return { objects, assembly: getRunResultAssemblySource(runResult), runResult, error: null };
|
|
17924
|
+
}
|
|
17925
|
+
async function loadAnalysisObjects(scriptPath, options) {
|
|
17926
|
+
const { objects, error } = await loadAnalysisObjectsWithAssembly(scriptPath, options);
|
|
17927
|
+
return { objects, error };
|
|
17928
|
+
}
|
|
17929
|
+
function printFeedback(report, json, compact) {
|
|
17930
|
+
if (json) {
|
|
17931
|
+
console.log(JSON.stringify(report, null, compact ? 0 : 2));
|
|
17932
|
+
return;
|
|
17933
|
+
}
|
|
17934
|
+
const reports = "runs" in report ? report.runs : [report];
|
|
17935
|
+
for (const entry of reports) {
|
|
17936
|
+
const text = renderFeedbackText(entry);
|
|
17937
|
+
if (entry.ok) console.log(text);
|
|
17938
|
+
else console.error(text);
|
|
17939
|
+
}
|
|
17940
|
+
}
|
|
17941
|
+
|
|
17942
|
+
// cli/check-stability.ts
|
|
17943
|
+
function parseArgs5(argv) {
|
|
17944
|
+
const inputPaths = [];
|
|
17945
|
+
const paramOverrides = {};
|
|
17946
|
+
const jointOverrides = {};
|
|
17947
|
+
let json = false;
|
|
17948
|
+
let compact = false;
|
|
17949
|
+
let minMarginMm = 0;
|
|
17950
|
+
let up = "z";
|
|
17951
|
+
let contactToleranceMm = null;
|
|
17952
|
+
let noFailExit = false;
|
|
17953
|
+
let backend;
|
|
17954
|
+
let quality;
|
|
17955
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
17956
|
+
const arg = argv[i];
|
|
17957
|
+
if (arg === "--json") {
|
|
17958
|
+
json = true;
|
|
17959
|
+
continue;
|
|
17960
|
+
}
|
|
17961
|
+
if (arg === "--compact") {
|
|
17962
|
+
compact = true;
|
|
17963
|
+
continue;
|
|
17964
|
+
}
|
|
17965
|
+
if (arg === "--no-fail-exit") {
|
|
17966
|
+
noFailExit = true;
|
|
17967
|
+
continue;
|
|
17968
|
+
}
|
|
17969
|
+
if (arg === "--min-margin-mm") {
|
|
17970
|
+
const value = Number(argv[i + 1]);
|
|
17971
|
+
if (!Number.isFinite(value)) throw new Error("--min-margin-mm requires a number");
|
|
17972
|
+
minMarginMm = value;
|
|
17973
|
+
i += 1;
|
|
17974
|
+
continue;
|
|
17975
|
+
}
|
|
17976
|
+
if (arg === "--up") {
|
|
17977
|
+
const val = argv[i + 1];
|
|
17978
|
+
if (val !== "x" && val !== "y" && val !== "z") throw new Error("--up must be x, y, or z");
|
|
17979
|
+
up = val;
|
|
17980
|
+
i += 1;
|
|
17981
|
+
continue;
|
|
17982
|
+
}
|
|
17983
|
+
if (arg === "--contact-tol") {
|
|
17984
|
+
const value = Number(argv[i + 1]);
|
|
17985
|
+
if (!Number.isFinite(value) || value < 0) throw new Error("--contact-tol requires a non-negative number (mm)");
|
|
17986
|
+
contactToleranceMm = value;
|
|
17987
|
+
i += 1;
|
|
17988
|
+
continue;
|
|
17989
|
+
}
|
|
17990
|
+
if (arg === "--quality" || arg === "-q") {
|
|
17991
|
+
const val = argv[i + 1];
|
|
17992
|
+
if (val !== "default" && val !== "live" && val !== "high") throw new Error("--quality must be default, live, or high");
|
|
17993
|
+
quality = val;
|
|
17994
|
+
i += 1;
|
|
17995
|
+
continue;
|
|
17996
|
+
}
|
|
17997
|
+
if (arg === "--backend") {
|
|
17998
|
+
const val = argv[i + 1];
|
|
17999
|
+
if (val !== "manifold" && val !== "occt" && val !== "truck" && val !== "sdf") {
|
|
18000
|
+
throw new Error("--backend must be manifold, occt, truck, or sdf");
|
|
18001
|
+
}
|
|
18002
|
+
backend = val;
|
|
18003
|
+
i += 1;
|
|
18004
|
+
continue;
|
|
18005
|
+
}
|
|
18006
|
+
if (arg === "--joint") {
|
|
18007
|
+
const value = argv[i + 1];
|
|
18008
|
+
if (!value || value.startsWith("--")) throw new Error("--joint requires JointName=Value");
|
|
18009
|
+
addCliJointOverride(value, jointOverrides);
|
|
18010
|
+
i += 1;
|
|
18011
|
+
continue;
|
|
18012
|
+
}
|
|
18013
|
+
if (arg === "--param" || arg === "-p") {
|
|
18014
|
+
const value = argv[i + 1];
|
|
18015
|
+
if (!value || value.startsWith("--")) throw new Error(`${arg} requires Key=Value`);
|
|
18016
|
+
addCliParamOverride(value, paramOverrides);
|
|
18017
|
+
i += 1;
|
|
18018
|
+
continue;
|
|
18019
|
+
}
|
|
18020
|
+
if (arg.startsWith("--")) throw new Error(`Unknown flag: ${arg}`);
|
|
18021
|
+
inputPaths.push(arg);
|
|
18022
|
+
}
|
|
18023
|
+
requireInputPaths(
|
|
18024
|
+
inputPaths,
|
|
18025
|
+
"Usage: forgecad check stability <model.forge.js> [input ...] [--min-margin-mm N] [--up x|y|z] [--json] [--param Key=Value]"
|
|
18026
|
+
);
|
|
18027
|
+
requireScriptInputPaths(inputPaths);
|
|
18028
|
+
return { inputPaths, json, compact, minMarginMm, up, contactToleranceMm, noFailExit, backend, quality, paramOverrides, jointOverrides };
|
|
18029
|
+
}
|
|
18030
|
+
function failedReport2(source, message, code = "STABILITY.ERROR") {
|
|
18031
|
+
return {
|
|
18032
|
+
schema: FEEDBACK_SCHEMA,
|
|
18033
|
+
category: "stability",
|
|
18034
|
+
source,
|
|
18035
|
+
ok: false,
|
|
18036
|
+
summary: "stability analysis failed",
|
|
18037
|
+
metrics: [],
|
|
18038
|
+
findings: [{ level: "error", code, message }],
|
|
18039
|
+
trust: { assumptions: [] },
|
|
18040
|
+
data: {},
|
|
18041
|
+
timeMs: 0
|
|
18042
|
+
};
|
|
18043
|
+
}
|
|
18044
|
+
function round3(value, digits = 4) {
|
|
18045
|
+
const factor = 10 ** digits;
|
|
18046
|
+
return Math.round(value * factor) / factor;
|
|
18047
|
+
}
|
|
18048
|
+
function analyze(source, options, objects) {
|
|
18049
|
+
const start = Date.now();
|
|
18050
|
+
const props = analyzeMassProperties(objects.map((o) => ({ name: o.name, mesh: o.mesh, densityKgM3: 1e3 })));
|
|
18051
|
+
const stab = analyzeStability(objects.map((o) => o.mesh), props.centerOfMassMm, {
|
|
18052
|
+
up: options.up,
|
|
18053
|
+
minMarginMm: options.minMarginMm,
|
|
18054
|
+
contactToleranceMm: options.contactToleranceMm ?? void 0
|
|
18055
|
+
});
|
|
18056
|
+
const findings = [];
|
|
18057
|
+
const assumptions = [
|
|
18058
|
+
"Rigid body at rest under gravity; center of mass is the uniform-density volume centroid.",
|
|
18059
|
+
`Up axis is +${options.up}; the support footprint is the convex hull of the lowest geometry.`
|
|
18060
|
+
];
|
|
18061
|
+
if (stab.degenerate) {
|
|
18062
|
+
findings.push({
|
|
18063
|
+
level: "error",
|
|
18064
|
+
code: "STABILITY.SUPPORT.DEGENERATE",
|
|
18065
|
+
message: `Support footprint collapses to ${stab.contactVertexCount === 0 ? "no" : "a line/point of"} contact \u2014 no stable base.`,
|
|
18066
|
+
suggest: "Add a flat resting face, widen the base, or check that the model rests on the ground plane."
|
|
18067
|
+
});
|
|
18068
|
+
} else if (stab.tipOverMarginMm < options.minMarginMm) {
|
|
18069
|
+
const dir = stab.comShiftToTargetMm;
|
|
18070
|
+
findings.push({
|
|
18071
|
+
level: "error",
|
|
18072
|
+
code: stab.comInsideSupport ? "STABILITY.MARGIN.LOW" : "STABILITY.TIPS_OVER",
|
|
18073
|
+
message: stab.comInsideSupport ? `Tip-over margin ${round3(stab.tipOverMarginMm, 2)} mm is below the required ${options.minMarginMm} mm.` : `Center of mass falls outside the support polygon by ${round3(-stab.tipOverMarginMm, 2)} mm \u2014 the part tips over.`,
|
|
18074
|
+
suggest: dir ? `Shift the center of mass by (${round3(dir[0], 1)}, ${round3(dir[1], 1)}) mm in the ground plane \u2014 widen the base, move mass inboard, or add ballast on the opposite side.` : "Widen the base or move mass toward the support center."
|
|
18075
|
+
});
|
|
18076
|
+
}
|
|
18077
|
+
const metrics = [
|
|
18078
|
+
makeMetric("tipOverMarginMm", "Tip-over margin", round3(stab.tipOverMarginMm, 3), {
|
|
18079
|
+
unit: "mm",
|
|
18080
|
+
target: { op: ">=", value: options.minMarginMm },
|
|
18081
|
+
status: stab.degenerate ? "fail" : void 0
|
|
18082
|
+
}),
|
|
18083
|
+
makeMetric("tipOverAngleDeg", "Tip-over angle", round3(stab.tipOverAngleDeg, 3), { unit: "deg" }),
|
|
18084
|
+
makeMetric("comHeightMm", "CoM height", round3(stab.comHeightMm, 3), { unit: "mm" }),
|
|
18085
|
+
makeMetric("comInsideSupport", "CoM inside support", stab.comInsideSupport ? 1 : 0, {
|
|
18086
|
+
unit: "bool",
|
|
18087
|
+
target: { op: ">=", value: 1 },
|
|
18088
|
+
status: stab.comInsideSupport ? "pass" : "fail"
|
|
18089
|
+
})
|
|
18090
|
+
];
|
|
18091
|
+
return {
|
|
18092
|
+
schema: FEEDBACK_SCHEMA,
|
|
18093
|
+
category: "stability",
|
|
18094
|
+
source,
|
|
18095
|
+
ok: reportOk(metrics, findings),
|
|
18096
|
+
summary: stab.degenerate ? "no stable support footprint" : stab.comInsideSupport ? `stable \xB7 margin ${round3(stab.tipOverMarginMm, 2)} mm \xB7 tips at ${round3(stab.tipOverAngleDeg, 1)}\xB0 tilt` : `TIPS OVER \xB7 CoM ${round3(-stab.tipOverMarginMm, 2)} mm past the support edge`,
|
|
18097
|
+
metrics,
|
|
18098
|
+
findings,
|
|
18099
|
+
trust: { assumptions, watertight: true },
|
|
18100
|
+
data: {
|
|
18101
|
+
up: stab.up,
|
|
18102
|
+
groundLevelMm: round3(stab.groundLevelMm, 4),
|
|
18103
|
+
centerOfMassMm: stab.centerOfMassMm.map((v) => round3(v, 4)),
|
|
18104
|
+
comProjectionMm: stab.comProjectionMm.map((v) => round3(v, 4)),
|
|
18105
|
+
comHeightMm: round3(stab.comHeightMm, 4),
|
|
18106
|
+
tipOverMarginMm: round3(stab.tipOverMarginMm, 4),
|
|
18107
|
+
tipOverAngleDeg: round3(stab.tipOverAngleDeg, 4),
|
|
18108
|
+
comInsideSupport: stab.comInsideSupport,
|
|
18109
|
+
contactVertexCount: stab.contactVertexCount,
|
|
18110
|
+
supportPolygonMm: stab.supportPolygonMm.map((p) => [round3(p[0], 3), round3(p[1], 3)]),
|
|
18111
|
+
criticalEdge: stab.criticalEdge ? { a: stab.criticalEdge.a.map((v) => round3(v, 3)), b: stab.criticalEdge.b.map((v) => round3(v, 3)) } : null,
|
|
18112
|
+
tipDirection: stab.tipDirection ? stab.tipDirection.map((v) => round3(v, 5)) : null,
|
|
18113
|
+
comShiftToTargetMm: stab.comShiftToTargetMm ? stab.comShiftToTargetMm.map((v) => round3(v, 3)) : null
|
|
18114
|
+
},
|
|
18115
|
+
timeMs: Date.now() - start
|
|
18116
|
+
};
|
|
18117
|
+
}
|
|
18118
|
+
async function runCheckStabilityCli(argv = process.argv.slice(2)) {
|
|
18119
|
+
const options = parseArgs5(argv);
|
|
18120
|
+
requireExistingInputPaths(options.inputPaths);
|
|
18121
|
+
await init();
|
|
18122
|
+
const reports = [];
|
|
18123
|
+
for (const [index, scriptPath] of options.inputPaths.entries()) {
|
|
18124
|
+
if (!options.json) printBatchHeader(scriptPath, index, options.inputPaths.length);
|
|
18125
|
+
let report;
|
|
18126
|
+
try {
|
|
18127
|
+
const { objects, error } = await loadAnalysisObjects(scriptPath, {
|
|
18128
|
+
backend: options.backend,
|
|
18129
|
+
quality: options.quality,
|
|
18130
|
+
paramOverrides: options.paramOverrides,
|
|
18131
|
+
jointOverrides: options.jointOverrides
|
|
18132
|
+
});
|
|
18133
|
+
report = error ? failedReport2(scriptPath, error, "STABILITY.MODEL.EMPTY") : analyze(scriptPath, options, objects);
|
|
18134
|
+
} catch (error) {
|
|
18135
|
+
report = failedReport2(scriptPath, error instanceof Error ? error.message : String(error));
|
|
18136
|
+
}
|
|
18137
|
+
reports.push(report);
|
|
18138
|
+
}
|
|
18139
|
+
printFeedback(reports.length === 1 ? reports[0] : { runs: reports }, options.json, options.compact);
|
|
18140
|
+
if (reports.some((report) => !report.ok) && !options.noFailExit) process.exitCode = 1;
|
|
18141
|
+
}
|
|
18142
|
+
|
|
17507
18143
|
// cli/check-text.ts
|
|
17508
18144
|
import assert6 from "assert/strict";
|
|
17509
18145
|
var EPS6 = 1e-3;
|
|
@@ -18546,11 +19182,12 @@ async function buildStepBlob(objects) {
|
|
|
18546
19182
|
}
|
|
18547
19183
|
|
|
18548
19184
|
// cli/forge-brep.ts
|
|
18549
|
-
function
|
|
19185
|
+
function parseArgs6(argv) {
|
|
18550
19186
|
let format = "step";
|
|
18551
19187
|
let backend = "occt";
|
|
18552
19188
|
let outputPath;
|
|
18553
19189
|
const inputPaths = [];
|
|
19190
|
+
const paramOverrides = {};
|
|
18554
19191
|
const jointOverrides = {};
|
|
18555
19192
|
for (let i = 0; i < argv.length; i += 1) {
|
|
18556
19193
|
const arg = argv[i];
|
|
@@ -18585,6 +19222,13 @@ function parseArgs5(argv) {
|
|
|
18585
19222
|
i += 1;
|
|
18586
19223
|
continue;
|
|
18587
19224
|
}
|
|
19225
|
+
if (arg === "--param" || arg === "-p") {
|
|
19226
|
+
const value = argv[i + 1];
|
|
19227
|
+
if (!value || value.startsWith("--")) throw new Error(`${arg} requires Key=Value`);
|
|
19228
|
+
addCliParamOverride(value, paramOverrides);
|
|
19229
|
+
i += 1;
|
|
19230
|
+
continue;
|
|
19231
|
+
}
|
|
18588
19232
|
if (arg === "--python") {
|
|
18589
19233
|
throw new Error("--python is no longer supported. STEP/BREP export now uses the native OCCT runtime exporter.");
|
|
18590
19234
|
}
|
|
@@ -18603,14 +19247,14 @@ function parseArgs5(argv) {
|
|
|
18603
19247
|
}
|
|
18604
19248
|
requireInputPaths(
|
|
18605
19249
|
inputPaths,
|
|
18606
|
-
"Usage: forgecad export <step|brep> <model.forge.js|asset.step|asset.stp> [input ...] [--output path] [--backend occt|truck] [--joint JointName=Value]"
|
|
19250
|
+
"Usage: forgecad export <step|brep> <model.forge.js|asset.step|asset.stp> [input ...] [--output path] [--backend occt|truck] [--param Key=Value] [--joint JointName=Value]"
|
|
18607
19251
|
);
|
|
18608
19252
|
requireExactExportInputPaths(inputPaths);
|
|
18609
19253
|
requireSingleInputForOutputPath(inputPaths, outputPath);
|
|
18610
19254
|
if (backend === "truck" && format !== "step") {
|
|
18611
19255
|
throw new Error("The native Truck BREP exporter currently supports STEP only (use --format step).");
|
|
18612
19256
|
}
|
|
18613
|
-
return { format, backend, outputPath, inputPaths, jointOverrides };
|
|
19257
|
+
return { format, backend, outputPath, inputPaths, paramOverrides, jointOverrides };
|
|
18614
19258
|
}
|
|
18615
19259
|
function defaultOutputPath(scriptPath, sourcePath, format) {
|
|
18616
19260
|
const abs = resolve16(scriptPath);
|
|
@@ -18618,8 +19262,9 @@ function defaultOutputPath(scriptPath, sourcePath, format) {
|
|
|
18618
19262
|
const target = `${stem}.${format}`;
|
|
18619
19263
|
return resolve16(target) === resolve16(sourcePath) ? `${stem}.forgecad.${format}` : target;
|
|
18620
19264
|
}
|
|
18621
|
-
async function buildRuntimeExactExportBlob(format, code, fileName, allFiles, readBinaryFile, jointOverrides) {
|
|
19265
|
+
async function buildRuntimeExactExportBlob(format, code, fileName, allFiles, readBinaryFile, paramOverrides, jointOverrides) {
|
|
18622
19266
|
await activateBackend("occt");
|
|
19267
|
+
setParamOverrides(paramOverrides);
|
|
18623
19268
|
const result = applyCliJointOverrides(
|
|
18624
19269
|
runScript(code, fileName, allFiles, { readBinaryFile, assemblyState: jointOverrides }),
|
|
18625
19270
|
jointOverrides
|
|
@@ -18652,8 +19297,9 @@ async function buildRuntimeExactExportBlob(format, code, fileName, allFiles, rea
|
|
|
18652
19297
|
objectCount: exactObjects.length
|
|
18653
19298
|
};
|
|
18654
19299
|
}
|
|
18655
|
-
async function buildTruckStepBlob(code, fileName, allFiles, readBinaryFile, jointOverrides) {
|
|
19300
|
+
async function buildTruckStepBlob(code, fileName, allFiles, readBinaryFile, paramOverrides, jointOverrides) {
|
|
18656
19301
|
await activateBackend("truck");
|
|
19302
|
+
setParamOverrides(paramOverrides);
|
|
18657
19303
|
const result = applyCliJointOverrides(
|
|
18658
19304
|
runScript(code, fileName, allFiles, { readBinaryFile, assemblyState: jointOverrides }),
|
|
18659
19305
|
jointOverrides
|
|
@@ -18669,13 +19315,13 @@ async function buildTruckStepBlob(code, fileName, allFiles, readBinaryFile, join
|
|
|
18669
19315
|
return { blob: new Blob([step], { type: "application/step" }), objectCount: handles.length };
|
|
18670
19316
|
}
|
|
18671
19317
|
async function runBrepCli(argv = process.argv.slice(2)) {
|
|
18672
|
-
const { format, backend, outputPath, inputPaths, jointOverrides } =
|
|
19318
|
+
const { format, backend, outputPath, inputPaths, paramOverrides, jointOverrides } = parseArgs6(argv);
|
|
18673
19319
|
requireExistingInputPaths(inputPaths);
|
|
18674
19320
|
let failures = 0;
|
|
18675
19321
|
for (const [index, scriptPath] of inputPaths.entries()) {
|
|
18676
19322
|
printBatchHeader(scriptPath, index, inputPaths.length);
|
|
18677
19323
|
try {
|
|
18678
|
-
await exportBrepInput(format, backend, outputPath, scriptPath, jointOverrides);
|
|
19324
|
+
await exportBrepInput(format, backend, outputPath, scriptPath, paramOverrides, jointOverrides);
|
|
18679
19325
|
} catch (error) {
|
|
18680
19326
|
failures += 1;
|
|
18681
19327
|
console.error(error instanceof Error ? error.message : String(error));
|
|
@@ -18685,11 +19331,19 @@ async function runBrepCli(argv = process.argv.slice(2)) {
|
|
|
18685
19331
|
process.exit(1);
|
|
18686
19332
|
}
|
|
18687
19333
|
}
|
|
18688
|
-
async function exportBrepInput(format, backend, outputPath, scriptPath, jointOverrides) {
|
|
19334
|
+
async function exportBrepInput(format, backend, outputPath, scriptPath, paramOverrides, jointOverrides) {
|
|
18689
19335
|
const input = loadCliScriptInput(scriptPath);
|
|
18690
19336
|
await init();
|
|
18691
19337
|
try {
|
|
18692
|
-
const exactExport = backend === "truck" ? await buildTruckStepBlob(input.code, input.fileName, input.allFiles, input.readBinaryFile, jointOverrides) : await buildRuntimeExactExportBlob(
|
|
19338
|
+
const exactExport = backend === "truck" ? await buildTruckStepBlob(input.code, input.fileName, input.allFiles, input.readBinaryFile, paramOverrides, jointOverrides) : await buildRuntimeExactExportBlob(
|
|
19339
|
+
format,
|
|
19340
|
+
input.code,
|
|
19341
|
+
input.fileName,
|
|
19342
|
+
input.allFiles,
|
|
19343
|
+
input.readBinaryFile,
|
|
19344
|
+
paramOverrides,
|
|
19345
|
+
jointOverrides
|
|
19346
|
+
);
|
|
18693
19347
|
if (outputPath && resolve16(outputPath) === input.sourcePath) {
|
|
18694
19348
|
console.error("Output path would overwrite the input file. Pass a different --output path.");
|
|
18695
19349
|
process.exit(1);
|
|
@@ -18699,6 +19353,7 @@ async function exportBrepInput(format, backend, outputPath, scriptPath, jointOve
|
|
|
18699
19353
|
console.log(`\u2713 Exported ${exactExport.objectCount} object(s) to ${finalOutput}`);
|
|
18700
19354
|
return;
|
|
18701
19355
|
} catch (runtimeError) {
|
|
19356
|
+
setParamOverrides(paramOverrides);
|
|
18702
19357
|
const result = applyCliJointOverrides(
|
|
18703
19358
|
runScript(input.code, input.fileName, input.allFiles, {
|
|
18704
19359
|
readBinaryFile: input.readBinaryFile,
|
|
@@ -19656,7 +20311,7 @@ async function waitForRenderHtml2(port, timeoutMs) {
|
|
|
19656
20311
|
const deadline = Date.now() + timeoutMs;
|
|
19657
20312
|
while (Date.now() < deadline) {
|
|
19658
20313
|
if (await fetchRenderHtml2(port)) return true;
|
|
19659
|
-
await new Promise((
|
|
20314
|
+
await new Promise((resolve46) => setTimeout(resolve46, 250));
|
|
19660
20315
|
}
|
|
19661
20316
|
return false;
|
|
19662
20317
|
}
|
|
@@ -19710,7 +20365,7 @@ ${detail}` : `Timed out waiting for Vite on port ${port}.`);
|
|
|
19710
20365
|
async function stopDevServer2(proc) {
|
|
19711
20366
|
if (!proc || proc.killed) return;
|
|
19712
20367
|
proc.kill("SIGTERM");
|
|
19713
|
-
await new Promise((
|
|
20368
|
+
await new Promise((resolve46) => setTimeout(resolve46, 150));
|
|
19714
20369
|
if (!proc.killed) {
|
|
19715
20370
|
proc.kill("SIGKILL");
|
|
19716
20371
|
}
|
|
@@ -20925,9 +21580,9 @@ function toViteArgs(options) {
|
|
|
20925
21580
|
return args;
|
|
20926
21581
|
}
|
|
20927
21582
|
function waitForExit(child) {
|
|
20928
|
-
return new Promise((
|
|
21583
|
+
return new Promise((resolve46, reject) => {
|
|
20929
21584
|
child.once("error", reject);
|
|
20930
|
-
child.once("exit", (code) =>
|
|
21585
|
+
child.once("exit", (code) => resolve46(code ?? 0));
|
|
20931
21586
|
});
|
|
20932
21587
|
}
|
|
20933
21588
|
async function runDevCli(argv = process.argv.slice(2)) {
|
|
@@ -20953,9 +21608,10 @@ async function runDevCli(argv = process.argv.slice(2)) {
|
|
|
20953
21608
|
// cli/forge-gcode.ts
|
|
20954
21609
|
import { writeFileSync as writeFileSync6 } from "fs";
|
|
20955
21610
|
import { extname as extname5, resolve as resolve20 } from "path";
|
|
20956
|
-
function
|
|
21611
|
+
function parseArgs7(argv) {
|
|
20957
21612
|
const inputPaths = [];
|
|
20958
21613
|
let outputPath;
|
|
21614
|
+
const paramOverrides = {};
|
|
20959
21615
|
const jointOverrides = {};
|
|
20960
21616
|
for (let i = 0; i < argv.length; i += 1) {
|
|
20961
21617
|
const arg = argv[i];
|
|
@@ -20972,15 +21628,25 @@ function parseArgs6(argv) {
|
|
|
20972
21628
|
i += 1;
|
|
20973
21629
|
continue;
|
|
20974
21630
|
}
|
|
21631
|
+
if (arg === "--param" || arg === "-p") {
|
|
21632
|
+
const value = argv[i + 1];
|
|
21633
|
+
if (!value || value.startsWith("--")) throw new Error(`${arg} requires Key=Value`);
|
|
21634
|
+
addCliParamOverride(value, paramOverrides);
|
|
21635
|
+
i += 1;
|
|
21636
|
+
continue;
|
|
21637
|
+
}
|
|
20975
21638
|
if (arg.startsWith("--")) {
|
|
20976
21639
|
throw new Error(`Unknown flag: ${arg}`);
|
|
20977
21640
|
}
|
|
20978
21641
|
inputPaths.push(arg);
|
|
20979
21642
|
}
|
|
20980
|
-
requireInputPaths(
|
|
21643
|
+
requireInputPaths(
|
|
21644
|
+
inputPaths,
|
|
21645
|
+
"Usage: forgecad export gcode <script.forge.js> [input ...] [--output path] [--param Key=Value] [--joint JointName=Value]"
|
|
21646
|
+
);
|
|
20981
21647
|
requireScriptInputPaths(inputPaths);
|
|
20982
21648
|
requireSingleInputForOutputPath(inputPaths, outputPath);
|
|
20983
|
-
return { inputPaths, outputPath, jointOverrides };
|
|
21649
|
+
return { inputPaths, outputPath, paramOverrides, jointOverrides };
|
|
20984
21650
|
}
|
|
20985
21651
|
function defaultOutputPath3(scriptPath) {
|
|
20986
21652
|
const abs = resolve20(scriptPath);
|
|
@@ -20989,13 +21655,13 @@ function defaultOutputPath3(scriptPath) {
|
|
|
20989
21655
|
return `${stem}.gcode`;
|
|
20990
21656
|
}
|
|
20991
21657
|
async function runGcodeExportCli(argv) {
|
|
20992
|
-
const { inputPaths, outputPath, jointOverrides } =
|
|
21658
|
+
const { inputPaths, outputPath, paramOverrides, jointOverrides } = parseArgs7(argv);
|
|
20993
21659
|
requireExistingInputPaths(inputPaths);
|
|
20994
21660
|
let failures = 0;
|
|
20995
21661
|
for (const [index, scriptPath] of inputPaths.entries()) {
|
|
20996
21662
|
printBatchHeader(scriptPath, index, inputPaths.length);
|
|
20997
21663
|
try {
|
|
20998
|
-
await exportGcodeInput(scriptPath, outputPath, jointOverrides);
|
|
21664
|
+
await exportGcodeInput(scriptPath, outputPath, paramOverrides, jointOverrides);
|
|
20999
21665
|
} catch (error) {
|
|
21000
21666
|
failures += 1;
|
|
21001
21667
|
console.error(error instanceof Error ? error.message : String(error));
|
|
@@ -21005,10 +21671,11 @@ async function runGcodeExportCli(argv) {
|
|
|
21005
21671
|
process.exit(1);
|
|
21006
21672
|
}
|
|
21007
21673
|
}
|
|
21008
|
-
async function exportGcodeInput(scriptPath, explicitOutputPath, jointOverrides) {
|
|
21674
|
+
async function exportGcodeInput(scriptPath, explicitOutputPath, paramOverrides, jointOverrides) {
|
|
21009
21675
|
const code = (await import("fs")).readFileSync(resolve20(scriptPath), "utf-8");
|
|
21010
21676
|
const { allFiles, fileName, readBinaryFile } = collectProjectFiles(scriptPath);
|
|
21011
21677
|
await initKernel();
|
|
21678
|
+
setParamOverrides(paramOverrides);
|
|
21012
21679
|
const result = applyCliJointOverrides(
|
|
21013
21680
|
runScript(code, fileName, allFiles, { readBinaryFile, allowEmptyResult: true, assemblyState: jointOverrides }),
|
|
21014
21681
|
jointOverrides
|
|
@@ -21156,7 +21823,7 @@ function parseImplicitExportArgs(argv) {
|
|
|
21156
21823
|
}
|
|
21157
21824
|
if (!scriptPath) {
|
|
21158
21825
|
throw new Error(
|
|
21159
|
-
"Usage: forgecad export implicit <model.forge.js|asset.step|asset.stp> [--output path] [--format json|webgpu-brick] [--quality live|default|high] [--backend manifold|occt|truck|sdf]"
|
|
21826
|
+
"Usage: forgecad export implicit <model.forge.js|asset.step|asset.stp> [--output path] [--format json|webgpu-brick] [--quality live|default|high] [--backend manifold|occt|truck|sdf] [--param Key=Value] [--joint JointName=Value]"
|
|
21160
21827
|
);
|
|
21161
21828
|
}
|
|
21162
21829
|
return { scriptPath, outputPath, format, backend, quality, workgroupSize, paramOverrides, jointOverrides };
|
|
@@ -21248,7 +21915,7 @@ async function runImplicitExportCli(argv = process.argv.slice(2)) {
|
|
|
21248
21915
|
const args = parseImplicitExportArgs(argv);
|
|
21249
21916
|
const input = loadCliScriptInput(args.scriptPath);
|
|
21250
21917
|
await initCliBackend(args.backend);
|
|
21251
|
-
|
|
21918
|
+
setParamOverrides(args.paramOverrides);
|
|
21252
21919
|
const runResult = runScript(input.code, input.fileName, input.allFiles, {
|
|
21253
21920
|
...args.quality ? { quality: args.quality } : {},
|
|
21254
21921
|
readBinaryFile: input.readBinaryFile,
|
|
@@ -21483,11 +22150,12 @@ async function runLsInput(options, scriptPath, overrides, jointOverrides) {
|
|
|
21483
22150
|
// cli/forge-mesh.ts
|
|
21484
22151
|
import { writeFileSync as writeFileSync8 } from "fs";
|
|
21485
22152
|
import { extname as extname6, resolve as resolve22 } from "path";
|
|
21486
|
-
function
|
|
22153
|
+
function parseArgs8(argv) {
|
|
21487
22154
|
const inputPaths = [];
|
|
21488
22155
|
let outputPath;
|
|
21489
22156
|
let quality;
|
|
21490
22157
|
let backend;
|
|
22158
|
+
const paramOverrides = {};
|
|
21491
22159
|
const jointOverrides = {};
|
|
21492
22160
|
for (let i = 0; i < argv.length; i += 1) {
|
|
21493
22161
|
const arg = argv[i];
|
|
@@ -21522,6 +22190,13 @@ function parseArgs7(argv) {
|
|
|
21522
22190
|
i += 1;
|
|
21523
22191
|
continue;
|
|
21524
22192
|
}
|
|
22193
|
+
if (arg === "--param" || arg === "-p") {
|
|
22194
|
+
const value = argv[i + 1];
|
|
22195
|
+
if (!value || value.startsWith("--")) throw new Error(`${arg} requires Key=Value`);
|
|
22196
|
+
addCliParamOverride(value, paramOverrides);
|
|
22197
|
+
i += 1;
|
|
22198
|
+
continue;
|
|
22199
|
+
}
|
|
21525
22200
|
if (arg.startsWith("--")) {
|
|
21526
22201
|
throw new Error(`Unknown flag: ${arg}`);
|
|
21527
22202
|
}
|
|
@@ -21529,11 +22204,11 @@ function parseArgs7(argv) {
|
|
|
21529
22204
|
}
|
|
21530
22205
|
requireInputPaths(
|
|
21531
22206
|
inputPaths,
|
|
21532
|
-
"Usage: forgecad export <3mf|stl> <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [input ...] [--output path] [--quality default|live|high] [--backend manifold|occt|truck|sdf] [--joint JointName=Value]"
|
|
22207
|
+
"Usage: forgecad export <3mf|stl> <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [input ...] [--output path] [--quality default|live|high] [--backend manifold|occt|truck|sdf] [--param Key=Value] [--joint JointName=Value]"
|
|
21533
22208
|
);
|
|
21534
22209
|
requireRenderableInputPaths(inputPaths);
|
|
21535
22210
|
requireSingleInputForOutputPath(inputPaths, outputPath);
|
|
21536
|
-
return { inputPaths, outputPath, quality, backend, jointOverrides };
|
|
22211
|
+
return { inputPaths, outputPath, quality, backend, paramOverrides, jointOverrides };
|
|
21537
22212
|
}
|
|
21538
22213
|
function defaultOutputPath5(scriptPath, sourcePath, format) {
|
|
21539
22214
|
const abs = resolve22(scriptPath);
|
|
@@ -21550,13 +22225,13 @@ function extractMeshObjects(result) {
|
|
|
21550
22225
|
}));
|
|
21551
22226
|
}
|
|
21552
22227
|
async function runMeshExportCli(format, argv) {
|
|
21553
|
-
const { inputPaths, outputPath, quality, backend, jointOverrides } =
|
|
22228
|
+
const { inputPaths, outputPath, quality, backend, paramOverrides, jointOverrides } = parseArgs8(argv);
|
|
21554
22229
|
requireExistingInputPaths(inputPaths);
|
|
21555
22230
|
let failures = 0;
|
|
21556
22231
|
for (const [index, scriptPath] of inputPaths.entries()) {
|
|
21557
22232
|
printBatchHeader(scriptPath, index, inputPaths.length);
|
|
21558
22233
|
try {
|
|
21559
|
-
await exportMeshInput(format, scriptPath, outputPath, quality, backend, jointOverrides);
|
|
22234
|
+
await exportMeshInput(format, scriptPath, outputPath, quality, backend, paramOverrides, jointOverrides);
|
|
21560
22235
|
} catch (error) {
|
|
21561
22236
|
failures += 1;
|
|
21562
22237
|
console.error(error instanceof Error ? error.message : String(error));
|
|
@@ -21566,10 +22241,11 @@ async function runMeshExportCli(format, argv) {
|
|
|
21566
22241
|
process.exit(1);
|
|
21567
22242
|
}
|
|
21568
22243
|
}
|
|
21569
|
-
async function exportMeshInput(format, scriptPath, outputPath, quality, backend, jointOverrides) {
|
|
22244
|
+
async function exportMeshInput(format, scriptPath, outputPath, quality, backend, paramOverrides, jointOverrides) {
|
|
21570
22245
|
const input = loadCliScriptInput(scriptPath);
|
|
21571
22246
|
await initCliBackend(resolveCliBackend(backend, input) ?? CLI_DEFAULT_BACKEND);
|
|
21572
22247
|
const qualityPreset = quality && quality !== "default" ? quality : void 0;
|
|
22248
|
+
setParamOverrides(paramOverrides);
|
|
21573
22249
|
const runResult = runScript(input.code, input.fileName, input.allFiles, {
|
|
21574
22250
|
...qualityPreset ? { quality: qualityPreset } : {},
|
|
21575
22251
|
readBinaryFile: input.readBinaryFile,
|
|
@@ -23604,9 +24280,10 @@ function buildMjcfRobotPackage(spec) {
|
|
|
23604
24280
|
}
|
|
23605
24281
|
|
|
23606
24282
|
// cli/forge-mjcf.ts
|
|
23607
|
-
function
|
|
24283
|
+
function parseArgs9(argv) {
|
|
23608
24284
|
const inputPaths = [];
|
|
23609
24285
|
let outputPath;
|
|
24286
|
+
const paramOverrides = {};
|
|
23610
24287
|
const jointOverrides = {};
|
|
23611
24288
|
for (let i = 0; i < argv.length; i += 1) {
|
|
23612
24289
|
const arg = argv[i];
|
|
@@ -23623,25 +24300,31 @@ function parseArgs8(argv) {
|
|
|
23623
24300
|
i += 1;
|
|
23624
24301
|
continue;
|
|
23625
24302
|
}
|
|
24303
|
+
if (arg === "--param" || arg === "-p") {
|
|
24304
|
+
const value = argv[i + 1];
|
|
24305
|
+
if (!value || value.startsWith("--")) throw new Error(`${arg} requires Key=Value`);
|
|
24306
|
+
addCliParamOverride(value, paramOverrides);
|
|
24307
|
+
i += 1;
|
|
24308
|
+
continue;
|
|
24309
|
+
}
|
|
23626
24310
|
if (arg.startsWith("--")) {
|
|
23627
24311
|
throw new Error(`Unknown flag: ${arg}`);
|
|
23628
24312
|
}
|
|
23629
24313
|
inputPaths.push(arg);
|
|
23630
24314
|
}
|
|
23631
|
-
requireInputPaths(
|
|
24315
|
+
requireInputPaths(
|
|
24316
|
+
inputPaths,
|
|
24317
|
+
"Usage: forgecad export mjcf <script.forge.js> [input ...] [--output dir] [--param Key=Value] [--joint JointName=Value]"
|
|
24318
|
+
);
|
|
23632
24319
|
requireScriptInputPaths(inputPaths);
|
|
23633
24320
|
requireSingleInputForOutputPath(inputPaths, outputPath, "--output", "dir");
|
|
23634
|
-
return { inputPaths, outputPath, jointOverrides };
|
|
24321
|
+
return { inputPaths, outputPath, paramOverrides, jointOverrides };
|
|
23635
24322
|
}
|
|
23636
24323
|
function defaultOutputPath6(scriptPath) {
|
|
23637
24324
|
const abs = resolve23(scriptPath);
|
|
23638
24325
|
return abs.slice(0, abs.length - extname7(abs).length) + ".mjcfpkg";
|
|
23639
24326
|
}
|
|
23640
24327
|
function resolveSimulationModel2(result, jointOverrides) {
|
|
23641
|
-
const legacy = getCollectedRobotExport();
|
|
23642
|
-
if (legacy) {
|
|
23643
|
-
return { ...legacy, state: { ...legacy.state, ...jointOverrides } };
|
|
23644
|
-
}
|
|
23645
24328
|
const assembly2 = getRunResultAssemblySource(result);
|
|
23646
24329
|
if (!assembly2) return null;
|
|
23647
24330
|
const def = assembly2.describe();
|
|
@@ -23649,13 +24332,13 @@ function resolveSimulationModel2(result, jointOverrides) {
|
|
|
23649
24332
|
return collectSimulationModel(def, { state: jointOverrides });
|
|
23650
24333
|
}
|
|
23651
24334
|
async function runMjcfCli(argv = process.argv.slice(2)) {
|
|
23652
|
-
const { inputPaths, outputPath, jointOverrides } =
|
|
24335
|
+
const { inputPaths, outputPath, paramOverrides, jointOverrides } = parseArgs9(argv);
|
|
23653
24336
|
requireExistingInputPaths(inputPaths);
|
|
23654
24337
|
let failures = 0;
|
|
23655
24338
|
for (const [index, scriptPath] of inputPaths.entries()) {
|
|
23656
24339
|
printBatchHeader(scriptPath, index, inputPaths.length);
|
|
23657
24340
|
try {
|
|
23658
|
-
await exportMjcfInput(scriptPath, outputPath, jointOverrides);
|
|
24341
|
+
await exportMjcfInput(scriptPath, outputPath, paramOverrides, jointOverrides);
|
|
23659
24342
|
} catch (error) {
|
|
23660
24343
|
failures += 1;
|
|
23661
24344
|
console.error(error instanceof Error ? error.message : String(error));
|
|
@@ -23665,17 +24348,18 @@ async function runMjcfCli(argv = process.argv.slice(2)) {
|
|
|
23665
24348
|
process.exit(1);
|
|
23666
24349
|
}
|
|
23667
24350
|
}
|
|
23668
|
-
async function exportMjcfInput(scriptPath, explicitOutputPath, jointOverrides) {
|
|
24351
|
+
async function exportMjcfInput(scriptPath, explicitOutputPath, paramOverrides, jointOverrides) {
|
|
23669
24352
|
const code = readFileSync15(resolve23(scriptPath), "utf-8");
|
|
23670
24353
|
const { allFiles, fileName } = collectProjectFiles(scriptPath);
|
|
23671
24354
|
await init();
|
|
24355
|
+
setParamOverrides(paramOverrides);
|
|
23672
24356
|
const result = applyCliJointOverrides(runScript(code, fileName, allFiles, { assemblyState: jointOverrides }), jointOverrides);
|
|
23673
24357
|
if (result.error) {
|
|
23674
24358
|
throw new Error(`ERROR: ${result.error}`);
|
|
23675
24359
|
}
|
|
23676
24360
|
const robot = resolveSimulationModel2(result, jointOverrides);
|
|
23677
24361
|
if (!robot) {
|
|
23678
|
-
throw new Error("MJCF export requires the script to return assembly(...).withSimulation(...)
|
|
24362
|
+
throw new Error("MJCF export requires the script to return assembly(...).withSimulation(...).");
|
|
23679
24363
|
}
|
|
23680
24364
|
const packageOut = buildMjcfRobotPackage(robot);
|
|
23681
24365
|
const targetDir = resolve23(batchOutputPath(scriptPath, explicitOutputPath, defaultOutputPath6));
|
|
@@ -24072,7 +24756,7 @@ function readArgValue(argv, idx, flag) {
|
|
|
24072
24756
|
}
|
|
24073
24757
|
return next;
|
|
24074
24758
|
}
|
|
24075
|
-
function
|
|
24759
|
+
function parseArgs10(argv) {
|
|
24076
24760
|
const inputPaths = [];
|
|
24077
24761
|
let outputPath;
|
|
24078
24762
|
let width = 1920;
|
|
@@ -24346,7 +25030,7 @@ function viewportCameraToBlenderConfig(camera) {
|
|
|
24346
25030
|
};
|
|
24347
25031
|
}
|
|
24348
25032
|
async function runRenderHqCli(argv) {
|
|
24349
|
-
const args =
|
|
25033
|
+
const args = parseArgs10(argv);
|
|
24350
25034
|
requireExistingInputPaths(args.inputPaths);
|
|
24351
25035
|
let failures = 0;
|
|
24352
25036
|
for (const [index, scriptPath] of args.inputPaths.entries()) {
|
|
@@ -24492,7 +25176,9 @@ function defaultPngOutput(scriptPath) {
|
|
|
24492
25176
|
return scriptPath.replace(/\.(forge|sketch)\.js$/, ".png").replace(/\.js$/, ".png");
|
|
24493
25177
|
}
|
|
24494
25178
|
function usage14() {
|
|
24495
|
-
console.error(
|
|
25179
|
+
console.error(
|
|
25180
|
+
"Usage: forgecad render sketch <script.forge.js> [input ...] [--output path] [--size <px>] [--chrome-path <path>] [--param Key=Value]"
|
|
25181
|
+
);
|
|
24496
25182
|
process.exit(1);
|
|
24497
25183
|
}
|
|
24498
25184
|
function parseCli2(argv) {
|
|
@@ -24502,6 +25188,7 @@ function parseCli2(argv) {
|
|
|
24502
25188
|
let size = 1024;
|
|
24503
25189
|
let chromePath = process.env.CHROME_PATH;
|
|
24504
25190
|
let background = "#2a2a2a";
|
|
25191
|
+
const paramOverrides = {};
|
|
24505
25192
|
for (let i = 0; i < argv.length; i++) {
|
|
24506
25193
|
const arg = argv[i];
|
|
24507
25194
|
if (arg === "--size") {
|
|
@@ -24516,6 +25203,13 @@ function parseCli2(argv) {
|
|
|
24516
25203
|
background = argv[++i];
|
|
24517
25204
|
continue;
|
|
24518
25205
|
}
|
|
25206
|
+
if (arg === "--param" || arg === "-p") {
|
|
25207
|
+
const value = argv[i + 1];
|
|
25208
|
+
if (!value || value.startsWith("--")) throw new Error(`${arg} requires Key=Value.`);
|
|
25209
|
+
addCliParamOverride(value, paramOverrides);
|
|
25210
|
+
i += 1;
|
|
25211
|
+
continue;
|
|
25212
|
+
}
|
|
24519
25213
|
if (arg === "--output" || arg === "-o") {
|
|
24520
25214
|
outputPath = argv[++i];
|
|
24521
25215
|
if (!outputPath || outputPath.startsWith("--")) throw new Error(`${arg} requires a file path.`);
|
|
@@ -24533,7 +25227,8 @@ function parseCli2(argv) {
|
|
|
24533
25227
|
outputPath,
|
|
24534
25228
|
size,
|
|
24535
25229
|
chromePath,
|
|
24536
|
-
background
|
|
25230
|
+
background,
|
|
25231
|
+
paramOverrides
|
|
24537
25232
|
};
|
|
24538
25233
|
}
|
|
24539
25234
|
async function runRender2dCli(argv = process.argv.slice(2)) {
|
|
@@ -24570,6 +25265,7 @@ async function renderSketchInput(options, scriptPath, chromePath) {
|
|
|
24570
25265
|
const code = await readFile(resolve26(scriptPath), "utf-8");
|
|
24571
25266
|
const { allFiles, fileName } = collectProjectFiles(scriptPath);
|
|
24572
25267
|
await init();
|
|
25268
|
+
setParamOverrides(options.paramOverrides);
|
|
24573
25269
|
const result = runScript(code, fileName, allFiles);
|
|
24574
25270
|
if (result.error) {
|
|
24575
25271
|
throw new Error(`Script error: ${result.error}`);
|
|
@@ -24677,14 +25373,22 @@ function formatSheetCutList(input) {
|
|
|
24677
25373
|
|
|
24678
25374
|
// cli/forge-cut-list.ts
|
|
24679
25375
|
function usage15() {
|
|
24680
|
-
console.error("Usage: forgecad cut-list <script.forge.js> [input ...] [--joint JointName=Value]");
|
|
25376
|
+
console.error("Usage: forgecad cut-list <script.forge.js> [input ...] [--param Key=Value] [--joint JointName=Value]");
|
|
24681
25377
|
process.exit(1);
|
|
24682
25378
|
}
|
|
24683
|
-
function
|
|
25379
|
+
function parseArgs11(argv) {
|
|
24684
25380
|
const positionals = [];
|
|
25381
|
+
const paramOverrides = {};
|
|
24685
25382
|
const jointOverrides = {};
|
|
24686
25383
|
for (let index = 0; index < argv.length; index += 1) {
|
|
24687
25384
|
const arg = argv[index];
|
|
25385
|
+
if (arg === "--param" || arg === "-p") {
|
|
25386
|
+
const value = argv[index + 1];
|
|
25387
|
+
if (!value || value.startsWith("--")) throw new Error(`${arg} requires Key=Value`);
|
|
25388
|
+
addCliParamOverride(value, paramOverrides);
|
|
25389
|
+
index += 1;
|
|
25390
|
+
continue;
|
|
25391
|
+
}
|
|
24688
25392
|
if (arg === "--joint") {
|
|
24689
25393
|
const value = argv[index + 1];
|
|
24690
25394
|
if (!value || value.startsWith("--")) throw new Error("--joint requires JointName=Value");
|
|
@@ -24698,16 +25402,16 @@ function parseArgs10(argv) {
|
|
|
24698
25402
|
if (positionals.length === 0) usage15();
|
|
24699
25403
|
const inputPaths = requireInputPaths(positionals, "Missing input path.");
|
|
24700
25404
|
requireScriptInputPaths(inputPaths);
|
|
24701
|
-
return { inputPaths, jointOverrides };
|
|
25405
|
+
return { inputPaths, paramOverrides, jointOverrides };
|
|
24702
25406
|
}
|
|
24703
25407
|
async function runCutListCli(argv = process.argv.slice(2)) {
|
|
24704
|
-
const { inputPaths, jointOverrides } =
|
|
25408
|
+
const { inputPaths, paramOverrides, jointOverrides } = parseArgs11(argv);
|
|
24705
25409
|
requireExistingInputPaths(inputPaths);
|
|
24706
25410
|
let failures = 0;
|
|
24707
25411
|
for (const [index, scriptPath] of inputPaths.entries()) {
|
|
24708
25412
|
printBatchHeader(scriptPath, index, inputPaths.length);
|
|
24709
25413
|
try {
|
|
24710
|
-
await printCutListInput(scriptPath, jointOverrides);
|
|
25414
|
+
await printCutListInput(scriptPath, paramOverrides, jointOverrides);
|
|
24711
25415
|
} catch (error) {
|
|
24712
25416
|
failures += 1;
|
|
24713
25417
|
console.error(error instanceof Error ? error.message : String(error));
|
|
@@ -24717,10 +25421,11 @@ async function runCutListCli(argv = process.argv.slice(2)) {
|
|
|
24717
25421
|
process.exit(1);
|
|
24718
25422
|
}
|
|
24719
25423
|
}
|
|
24720
|
-
async function printCutListInput(scriptPath, jointOverrides) {
|
|
25424
|
+
async function printCutListInput(scriptPath, paramOverrides, jointOverrides) {
|
|
24721
25425
|
const source = await readFile2(resolve27(scriptPath), "utf-8");
|
|
24722
25426
|
const { allFiles, fileName } = collectProjectFiles(scriptPath);
|
|
24723
25427
|
await init();
|
|
25428
|
+
setParamOverrides(paramOverrides);
|
|
24724
25429
|
const result = applyCliJointOverrides(runScript(source, fileName, allFiles, { assemblyState: jointOverrides }), jointOverrides);
|
|
24725
25430
|
if (result.error) {
|
|
24726
25431
|
throw new Error(`Script error: ${result.error}`);
|
|
@@ -24735,7 +25440,7 @@ async function printCutListInput(scriptPath, jointOverrides) {
|
|
|
24735
25440
|
import { readFile as readFile3, writeFile as writeFile5 } from "fs/promises";
|
|
24736
25441
|
import { basename as basename11, resolve as resolve28 } from "path";
|
|
24737
25442
|
var FORMAT_VALUES = ["pdf", "dxf"];
|
|
24738
|
-
var VALUE_FLAGS = /* @__PURE__ */ new Set(["--format", "--sheet-width", "--sheet-height", "--kerf", "--joint", "--output", "--out"]);
|
|
25443
|
+
var VALUE_FLAGS = /* @__PURE__ */ new Set(["--format", "--sheet-width", "--sheet-height", "--kerf", "--param", "-p", "--joint", "--output", "--out"]);
|
|
24739
25444
|
function argValue(argv, name) {
|
|
24740
25445
|
const prefix = `${name}=`;
|
|
24741
25446
|
const equalsArg = argv.find((arg) => arg.startsWith(prefix));
|
|
@@ -24749,7 +25454,7 @@ function hasFlag(argv, name) {
|
|
|
24749
25454
|
return argv.some((arg) => arg === name || arg.startsWith(prefix));
|
|
24750
25455
|
}
|
|
24751
25456
|
function usageText() {
|
|
24752
|
-
return "Usage: forgecad export cutting-layout <script.forge.js> [input ...] [--output output.pdf|output.dxf]\n [--format pdf|dxf] [--sheet-width <mm>] [--sheet-height <mm>] [--kerf <mm>] [--joint JointName=Value]";
|
|
25457
|
+
return "Usage: forgecad export cutting-layout <script.forge.js> [input ...] [--output output.pdf|output.dxf]\n [--format pdf|dxf] [--sheet-width <mm>] [--sheet-height <mm>] [--kerf <mm>] [--param Key=Value] [--joint JointName=Value]";
|
|
24753
25458
|
}
|
|
24754
25459
|
function usage16() {
|
|
24755
25460
|
console.error(usageText());
|
|
@@ -24779,6 +25484,18 @@ function parseJointOverrides(argv) {
|
|
|
24779
25484
|
}
|
|
24780
25485
|
return jointOverrides;
|
|
24781
25486
|
}
|
|
25487
|
+
function parseParamOverrides(argv) {
|
|
25488
|
+
const paramOverrides = {};
|
|
25489
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
25490
|
+
const arg = argv[i];
|
|
25491
|
+
if (arg !== "--param" && arg !== "-p") continue;
|
|
25492
|
+
const value = argv[i + 1];
|
|
25493
|
+
if (!value || value.startsWith("--")) throw new Error(`${arg} requires Key=Value`);
|
|
25494
|
+
addCliParamOverride(value, paramOverrides);
|
|
25495
|
+
i += 1;
|
|
25496
|
+
}
|
|
25497
|
+
return paramOverrides;
|
|
25498
|
+
}
|
|
24782
25499
|
function normalizeFormat(value) {
|
|
24783
25500
|
if (value == null) return void 0;
|
|
24784
25501
|
const normalized = value.toLowerCase();
|
|
@@ -24850,6 +25567,7 @@ async function runCuttingLayoutCli(argv = process.argv.slice(2)) {
|
|
|
24850
25567
|
if (failures > 0) process.exit(1);
|
|
24851
25568
|
}
|
|
24852
25569
|
async function exportCuttingLayoutInput(argv, scriptPath, explicitOutputPath) {
|
|
25570
|
+
const paramOverrides = parseParamOverrides(argv);
|
|
24853
25571
|
const jointOverrides = parseJointOverrides(argv);
|
|
24854
25572
|
const formatArg = argValue(argv, "--format");
|
|
24855
25573
|
if (hasFlag(argv, "--format") && formatArg == null) {
|
|
@@ -24870,6 +25588,7 @@ async function exportCuttingLayoutInput(argv, scriptPath, explicitOutputPath) {
|
|
|
24870
25588
|
const source = await readFile3(resolve28(scriptPath), "utf-8");
|
|
24871
25589
|
const { allFiles, fileName } = collectProjectFiles(scriptPath);
|
|
24872
25590
|
await init();
|
|
25591
|
+
setParamOverrides(paramOverrides);
|
|
24873
25592
|
const result = applyCliJointOverrides(runScript(source, fileName, allFiles, { assemblyState: jointOverrides }), jointOverrides);
|
|
24874
25593
|
if (result.error) {
|
|
24875
25594
|
throw new Error(`Script error: ${result.error}`);
|
|
@@ -24907,7 +25626,7 @@ import { writeFile as writeFile6 } from "fs/promises";
|
|
|
24907
25626
|
import { basename as basename12, resolve as resolve29 } from "path";
|
|
24908
25627
|
function usage17() {
|
|
24909
25628
|
console.error(
|
|
24910
|
-
"Usage: forgecad export report <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [input ...] [--output path] [--dim-angle-tol <deg>] [--joint JointName=Value]"
|
|
25629
|
+
"Usage: forgecad export report <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [input ...] [--output path] [--dim-angle-tol <deg>] [--param Key=Value] [--joint JointName=Value]"
|
|
24911
25630
|
);
|
|
24912
25631
|
process.exit(1);
|
|
24913
25632
|
}
|
|
@@ -24916,8 +25635,9 @@ function readValue7(argv, index, flag) {
|
|
|
24916
25635
|
if (!value || value.startsWith("--")) throw new Error(`${flag} requires a value.`);
|
|
24917
25636
|
return value;
|
|
24918
25637
|
}
|
|
24919
|
-
function
|
|
25638
|
+
function parseArgs12(argv) {
|
|
24920
25639
|
const positionals = [];
|
|
25640
|
+
const paramOverrides = {};
|
|
24921
25641
|
const jointOverrides = {};
|
|
24922
25642
|
let dimAngleToleranceDeg;
|
|
24923
25643
|
let outputPath;
|
|
@@ -24939,6 +25659,11 @@ function parseArgs11(argv) {
|
|
|
24939
25659
|
index += 1;
|
|
24940
25660
|
continue;
|
|
24941
25661
|
}
|
|
25662
|
+
if (arg === "--param" || arg === "-p") {
|
|
25663
|
+
addCliParamOverride(readValue7(argv, index, arg), paramOverrides);
|
|
25664
|
+
index += 1;
|
|
25665
|
+
continue;
|
|
25666
|
+
}
|
|
24942
25667
|
if (arg.startsWith("--")) throw new Error(`Unknown option: ${arg}`);
|
|
24943
25668
|
positionals.push(arg);
|
|
24944
25669
|
}
|
|
@@ -24946,16 +25671,16 @@ function parseArgs11(argv) {
|
|
|
24946
25671
|
const inputPaths = requireInputPaths(positionals, "Missing input path.");
|
|
24947
25672
|
requireRenderableInputPaths(inputPaths);
|
|
24948
25673
|
requireSingleInputForOutputPath(inputPaths, outputPath);
|
|
24949
|
-
return { inputPaths, outputPath, dimAngleToleranceDeg, jointOverrides };
|
|
25674
|
+
return { inputPaths, outputPath, dimAngleToleranceDeg, paramOverrides, jointOverrides };
|
|
24950
25675
|
}
|
|
24951
25676
|
async function runReportCli(argv = process.argv.slice(2)) {
|
|
24952
|
-
const { inputPaths, outputPath: parsedOutputPath, jointOverrides, dimAngleToleranceDeg } =
|
|
25677
|
+
const { inputPaths, outputPath: parsedOutputPath, paramOverrides, jointOverrides, dimAngleToleranceDeg } = parseArgs12(argv);
|
|
24953
25678
|
requireExistingInputPaths(inputPaths);
|
|
24954
25679
|
let failures = 0;
|
|
24955
25680
|
for (const [index, scriptPath] of inputPaths.entries()) {
|
|
24956
25681
|
printBatchHeader(scriptPath, index, inputPaths.length);
|
|
24957
25682
|
try {
|
|
24958
|
-
await exportReportInput(scriptPath, parsedOutputPath, jointOverrides, dimAngleToleranceDeg);
|
|
25683
|
+
await exportReportInput(scriptPath, parsedOutputPath, paramOverrides, jointOverrides, dimAngleToleranceDeg);
|
|
24959
25684
|
} catch (error) {
|
|
24960
25685
|
failures += 1;
|
|
24961
25686
|
console.error(error instanceof Error ? error.message : String(error));
|
|
@@ -24965,10 +25690,11 @@ async function runReportCli(argv = process.argv.slice(2)) {
|
|
|
24965
25690
|
process.exit(1);
|
|
24966
25691
|
}
|
|
24967
25692
|
}
|
|
24968
|
-
async function exportReportInput(scriptPath, explicitOutputPath, jointOverrides, dimAngleToleranceDeg) {
|
|
25693
|
+
async function exportReportInput(scriptPath, explicitOutputPath, paramOverrides, jointOverrides, dimAngleToleranceDeg) {
|
|
24969
25694
|
const outputPath = batchOutputPath(scriptPath, explicitOutputPath, (inputPath) => replaceCliInputExtension(inputPath, ".report.pdf"));
|
|
24970
25695
|
const input = loadCliScriptInput(scriptPath);
|
|
24971
25696
|
await initCliBackend(resolveCliBackend(void 0, input) ?? CLI_DEFAULT_BACKEND);
|
|
25697
|
+
setParamOverrides(paramOverrides);
|
|
24972
25698
|
const result = applyCliJointOverrides(
|
|
24973
25699
|
runScript(input.code, input.fileName, input.allFiles, {
|
|
24974
25700
|
readBinaryFile: input.readBinaryFile,
|
|
@@ -25828,9 +26554,10 @@ function buildSdfRobotPackage(spec) {
|
|
|
25828
26554
|
}
|
|
25829
26555
|
|
|
25830
26556
|
// cli/forge-sdf.ts
|
|
25831
|
-
function
|
|
26557
|
+
function parseArgs13(argv) {
|
|
25832
26558
|
const inputPaths = [];
|
|
25833
26559
|
let outputPath;
|
|
26560
|
+
const paramOverrides = {};
|
|
25834
26561
|
const jointOverrides = {};
|
|
25835
26562
|
for (let i = 0; i < argv.length; i += 1) {
|
|
25836
26563
|
const arg = argv[i];
|
|
@@ -25847,25 +26574,31 @@ function parseArgs12(argv) {
|
|
|
25847
26574
|
i += 1;
|
|
25848
26575
|
continue;
|
|
25849
26576
|
}
|
|
26577
|
+
if (arg === "--param" || arg === "-p") {
|
|
26578
|
+
const value = argv[i + 1];
|
|
26579
|
+
if (!value || value.startsWith("--")) throw new Error(`${arg} requires Key=Value`);
|
|
26580
|
+
addCliParamOverride(value, paramOverrides);
|
|
26581
|
+
i += 1;
|
|
26582
|
+
continue;
|
|
26583
|
+
}
|
|
25850
26584
|
if (arg.startsWith("--")) {
|
|
25851
26585
|
throw new Error(`Unknown flag: ${arg}`);
|
|
25852
26586
|
}
|
|
25853
26587
|
inputPaths.push(arg);
|
|
25854
26588
|
}
|
|
25855
|
-
requireInputPaths(
|
|
26589
|
+
requireInputPaths(
|
|
26590
|
+
inputPaths,
|
|
26591
|
+
"Usage: forgecad export sdf <script.forge.js> [input ...] [--output dir] [--param Key=Value] [--joint JointName=Value]"
|
|
26592
|
+
);
|
|
25856
26593
|
requireScriptInputPaths(inputPaths);
|
|
25857
26594
|
requireSingleInputForOutputPath(inputPaths, outputPath, "--output", "dir");
|
|
25858
|
-
return { inputPaths, outputPath, jointOverrides };
|
|
26595
|
+
return { inputPaths, outputPath, paramOverrides, jointOverrides };
|
|
25859
26596
|
}
|
|
25860
26597
|
function defaultOutputPath9(scriptPath) {
|
|
25861
26598
|
const abs = resolve30(scriptPath);
|
|
25862
26599
|
return abs.slice(0, abs.length - extname9(abs).length) + ".sdfpkg";
|
|
25863
26600
|
}
|
|
25864
26601
|
function resolveSimulationModel3(result, jointOverrides) {
|
|
25865
|
-
const legacy = getCollectedRobotExport();
|
|
25866
|
-
if (legacy) {
|
|
25867
|
-
return { ...legacy, state: { ...legacy.state, ...jointOverrides } };
|
|
25868
|
-
}
|
|
25869
26602
|
const assembly2 = getRunResultAssemblySource(result);
|
|
25870
26603
|
if (!assembly2) return null;
|
|
25871
26604
|
const def = assembly2.describe();
|
|
@@ -25873,13 +26606,13 @@ function resolveSimulationModel3(result, jointOverrides) {
|
|
|
25873
26606
|
return collectSimulationModel(def, { state: jointOverrides });
|
|
25874
26607
|
}
|
|
25875
26608
|
async function runSdfCli(argv = process.argv.slice(2)) {
|
|
25876
|
-
const { inputPaths, outputPath, jointOverrides } =
|
|
26609
|
+
const { inputPaths, outputPath, paramOverrides, jointOverrides } = parseArgs13(argv);
|
|
25877
26610
|
requireExistingInputPaths(inputPaths);
|
|
25878
26611
|
let failures = 0;
|
|
25879
26612
|
for (const [index, scriptPath] of inputPaths.entries()) {
|
|
25880
26613
|
printBatchHeader(scriptPath, index, inputPaths.length);
|
|
25881
26614
|
try {
|
|
25882
|
-
await exportSdfInput(scriptPath, outputPath, jointOverrides);
|
|
26615
|
+
await exportSdfInput(scriptPath, outputPath, paramOverrides, jointOverrides);
|
|
25883
26616
|
} catch (error) {
|
|
25884
26617
|
failures += 1;
|
|
25885
26618
|
console.error(error instanceof Error ? error.message : String(error));
|
|
@@ -25889,17 +26622,18 @@ async function runSdfCli(argv = process.argv.slice(2)) {
|
|
|
25889
26622
|
process.exit(1);
|
|
25890
26623
|
}
|
|
25891
26624
|
}
|
|
25892
|
-
async function exportSdfInput(scriptPath, explicitOutputPath, jointOverrides) {
|
|
26625
|
+
async function exportSdfInput(scriptPath, explicitOutputPath, paramOverrides, jointOverrides) {
|
|
25893
26626
|
const code = readFileSync16(resolve30(scriptPath), "utf-8");
|
|
25894
26627
|
const { allFiles, fileName } = collectProjectFiles(scriptPath);
|
|
25895
26628
|
await init();
|
|
26629
|
+
setParamOverrides(paramOverrides);
|
|
25896
26630
|
const result = applyCliJointOverrides(runScript(code, fileName, allFiles, { assemblyState: jointOverrides }), jointOverrides);
|
|
25897
26631
|
if (result.error) {
|
|
25898
26632
|
throw new Error(`ERROR: ${result.error}`);
|
|
25899
26633
|
}
|
|
25900
26634
|
const robot = resolveSimulationModel3(result, jointOverrides);
|
|
25901
26635
|
if (!robot) {
|
|
25902
|
-
throw new Error("SDF export requires the script to return assembly(...).withSimulation(...)
|
|
26636
|
+
throw new Error("SDF export requires the script to return assembly(...).withSimulation(...).");
|
|
25903
26637
|
}
|
|
25904
26638
|
const packageOut = buildSdfRobotPackage(robot);
|
|
25905
26639
|
const targetDir = resolve30(batchOutputPath(scriptPath, explicitOutputPath, defaultOutputPath9));
|
|
@@ -26625,15 +27359,16 @@ function generateSketchPdf(meta, options) {
|
|
|
26625
27359
|
|
|
26626
27360
|
// cli/forge-sketch-pdf.ts
|
|
26627
27361
|
function usage19() {
|
|
26628
|
-
console.error("Usage: forgecad export sketch-pdf <script.forge.js> [input ...] [--output path]");
|
|
27362
|
+
console.error("Usage: forgecad export sketch-pdf <script.forge.js> [input ...] [--output path] [--param Key=Value]");
|
|
26629
27363
|
process.exit(1);
|
|
26630
27364
|
}
|
|
26631
27365
|
function defaultPdfOutput(scriptPath) {
|
|
26632
27366
|
return scriptPath.replace(/\.(forge|sketch)\.js$/, ".sketch.pdf").replace(/\.js$/, ".sketch.pdf");
|
|
26633
27367
|
}
|
|
26634
|
-
function
|
|
27368
|
+
function parseArgs14(argv) {
|
|
26635
27369
|
const inputPaths = [];
|
|
26636
27370
|
let outputPath;
|
|
27371
|
+
const paramOverrides = {};
|
|
26637
27372
|
for (let index = 0; index < argv.length; index += 1) {
|
|
26638
27373
|
const arg = argv[index];
|
|
26639
27374
|
if (arg === "--output" || arg === "-o") {
|
|
@@ -26642,18 +27377,25 @@ function parseArgs13(argv) {
|
|
|
26642
27377
|
index += 1;
|
|
26643
27378
|
continue;
|
|
26644
27379
|
}
|
|
27380
|
+
if (arg === "--param" || arg === "-p") {
|
|
27381
|
+
const value = argv[index + 1];
|
|
27382
|
+
if (!value || value.startsWith("--")) throw new Error(`${arg} requires Key=Value.`);
|
|
27383
|
+
addCliParamOverride(value, paramOverrides);
|
|
27384
|
+
index += 1;
|
|
27385
|
+
continue;
|
|
27386
|
+
}
|
|
26645
27387
|
if (arg.startsWith("--")) throw new Error(`Unknown option: ${arg}`);
|
|
26646
27388
|
inputPaths.push(arg);
|
|
26647
27389
|
}
|
|
26648
27390
|
requireInputPaths(inputPaths, "Missing input path.");
|
|
26649
27391
|
requireScriptInputPaths(inputPaths);
|
|
26650
27392
|
requireSingleInputForOutputPath(inputPaths, outputPath);
|
|
26651
|
-
return { inputPaths, outputPath };
|
|
27393
|
+
return { inputPaths, outputPath, paramOverrides };
|
|
26652
27394
|
}
|
|
26653
27395
|
async function runSketchPdfCli(argv = process.argv.slice(2)) {
|
|
26654
27396
|
let parsed;
|
|
26655
27397
|
try {
|
|
26656
|
-
parsed =
|
|
27398
|
+
parsed = parseArgs14(argv);
|
|
26657
27399
|
requireExistingInputPaths(parsed.inputPaths);
|
|
26658
27400
|
} catch (error) {
|
|
26659
27401
|
console.error(error instanceof Error ? error.message : String(error));
|
|
@@ -26663,7 +27405,7 @@ async function runSketchPdfCli(argv = process.argv.slice(2)) {
|
|
|
26663
27405
|
for (const [index, scriptPath] of parsed.inputPaths.entries()) {
|
|
26664
27406
|
printBatchHeader(scriptPath, index, parsed.inputPaths.length);
|
|
26665
27407
|
try {
|
|
26666
|
-
await exportSketchPdfInput(scriptPath, parsed.outputPath);
|
|
27408
|
+
await exportSketchPdfInput(scriptPath, parsed.outputPath, parsed.paramOverrides);
|
|
26667
27409
|
} catch (error) {
|
|
26668
27410
|
failures += 1;
|
|
26669
27411
|
console.error(error instanceof Error ? error.message : String(error));
|
|
@@ -26673,7 +27415,7 @@ async function runSketchPdfCli(argv = process.argv.slice(2)) {
|
|
|
26673
27415
|
process.exit(1);
|
|
26674
27416
|
}
|
|
26675
27417
|
}
|
|
26676
|
-
async function exportSketchPdfInput(scriptPath, explicitOutputPath) {
|
|
27418
|
+
async function exportSketchPdfInput(scriptPath, explicitOutputPath, paramOverrides) {
|
|
26677
27419
|
const outputPath = batchOutputPath(scriptPath, explicitOutputPath, defaultPdfOutput);
|
|
26678
27420
|
if (resolve31(outputPath) === resolve31(scriptPath)) {
|
|
26679
27421
|
throw new Error(`ERROR: output path would overwrite the input script. Specify an explicit output path.`);
|
|
@@ -26681,6 +27423,7 @@ async function exportSketchPdfInput(scriptPath, explicitOutputPath) {
|
|
|
26681
27423
|
const code = await readFile4(resolve31(scriptPath), "utf-8");
|
|
26682
27424
|
const { allFiles, fileName } = collectProjectFiles(scriptPath);
|
|
26683
27425
|
await init();
|
|
27426
|
+
setParamOverrides(paramOverrides);
|
|
26684
27427
|
const result = runScript(code, fileName, allFiles);
|
|
26685
27428
|
if (result.error) {
|
|
26686
27429
|
throw new Error(`Script error: ${result.error}`);
|
|
@@ -27221,15 +27964,15 @@ function getEnvToken() {
|
|
|
27221
27964
|
}
|
|
27222
27965
|
function prompt(question) {
|
|
27223
27966
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
27224
|
-
return new Promise((
|
|
27967
|
+
return new Promise((resolve46) => {
|
|
27225
27968
|
rl.question(question, (answer) => {
|
|
27226
27969
|
rl.close();
|
|
27227
|
-
|
|
27970
|
+
resolve46(answer.trim());
|
|
27228
27971
|
});
|
|
27229
27972
|
});
|
|
27230
27973
|
}
|
|
27231
27974
|
function promptPassword(question) {
|
|
27232
|
-
return new Promise((
|
|
27975
|
+
return new Promise((resolve46) => {
|
|
27233
27976
|
process.stderr.write(question);
|
|
27234
27977
|
const stdin = process.stdin;
|
|
27235
27978
|
const wasRaw = stdin.isRaw;
|
|
@@ -27244,7 +27987,7 @@ function promptPassword(question) {
|
|
|
27244
27987
|
stdin.removeListener("end", finish);
|
|
27245
27988
|
stdin.pause();
|
|
27246
27989
|
process.stderr.write("\n");
|
|
27247
|
-
|
|
27990
|
+
resolve46(password);
|
|
27248
27991
|
};
|
|
27249
27992
|
const onData = (ch) => {
|
|
27250
27993
|
for (const c of ch.toString("utf-8")) {
|
|
@@ -27801,7 +28544,7 @@ import { existsSync as existsSync14, readdirSync as readdirSync8, statSync as st
|
|
|
27801
28544
|
import { createServer as createServer2 } from "net";
|
|
27802
28545
|
import { join as join12 } from "path";
|
|
27803
28546
|
var startedComputeServer = null;
|
|
27804
|
-
var REQUIRED_ENGINE_VERSION = "native-occt-node-api-0.3.
|
|
28547
|
+
var REQUIRED_ENGINE_VERSION = "native-occt-node-api-0.3.1";
|
|
27805
28548
|
var REQUIRED_PLAN_KINDS = [
|
|
27806
28549
|
"box",
|
|
27807
28550
|
"cylinder",
|
|
@@ -27809,6 +28552,7 @@ var REQUIRED_PLAN_KINDS = [
|
|
|
27809
28552
|
"torus",
|
|
27810
28553
|
"extrude",
|
|
27811
28554
|
"boolean",
|
|
28555
|
+
"trimByPlane",
|
|
27812
28556
|
"transform",
|
|
27813
28557
|
"queryOwner",
|
|
27814
28558
|
"revolve",
|
|
@@ -27847,17 +28591,17 @@ async function waitForHealthy(computeUrl, child) {
|
|
|
27847
28591
|
for (let attempt = 0; attempt < 120; attempt += 1) {
|
|
27848
28592
|
if (await isHealthy(computeUrl)) return;
|
|
27849
28593
|
if (child.exitCode != null) break;
|
|
27850
|
-
await new Promise((
|
|
28594
|
+
await new Promise((resolve46) => setTimeout(resolve46, 50));
|
|
27851
28595
|
}
|
|
27852
28596
|
throw new Error(`Native OCCT compute backend did not become healthy at ${computeUrl}.`);
|
|
27853
28597
|
}
|
|
27854
28598
|
function closeChild(child) {
|
|
27855
|
-
return new Promise((
|
|
28599
|
+
return new Promise((resolve46) => {
|
|
27856
28600
|
if (child.exitCode != null || child.killed) {
|
|
27857
|
-
|
|
28601
|
+
resolve46();
|
|
27858
28602
|
return;
|
|
27859
28603
|
}
|
|
27860
|
-
child.once("exit", () =>
|
|
28604
|
+
child.once("exit", () => resolve46());
|
|
27861
28605
|
child.kill();
|
|
27862
28606
|
});
|
|
27863
28607
|
}
|
|
@@ -27896,12 +28640,12 @@ async function pickComputeUrl(preferred) {
|
|
|
27896
28640
|
const candidate = computeUrlWithPort(preferred, port);
|
|
27897
28641
|
if (await isHealthy(candidate)) return candidate;
|
|
27898
28642
|
try {
|
|
27899
|
-
const server = await new Promise((
|
|
28643
|
+
const server = await new Promise((resolve46, reject) => {
|
|
27900
28644
|
const probe = createServer2();
|
|
27901
28645
|
probe.once("error", reject);
|
|
27902
|
-
probe.listen(port, listenHost, () =>
|
|
28646
|
+
probe.listen(port, listenHost, () => resolve46(probe));
|
|
27903
28647
|
});
|
|
27904
|
-
await new Promise((
|
|
28648
|
+
await new Promise((resolve46) => server.close(() => resolve46()));
|
|
27905
28649
|
return candidate;
|
|
27906
28650
|
} catch {
|
|
27907
28651
|
}
|
|
@@ -28328,14 +29072,14 @@ function sendJson(res, status, payload) {
|
|
|
28328
29072
|
res.end(JSON.stringify(payload));
|
|
28329
29073
|
}
|
|
28330
29074
|
function readJsonBody(req) {
|
|
28331
|
-
return new Promise((
|
|
29075
|
+
return new Promise((resolve46, reject) => {
|
|
28332
29076
|
let body = "";
|
|
28333
29077
|
req.on("data", (chunk) => {
|
|
28334
29078
|
body += chunk;
|
|
28335
29079
|
});
|
|
28336
29080
|
req.on("end", () => {
|
|
28337
29081
|
try {
|
|
28338
|
-
|
|
29082
|
+
resolve46(body ? JSON.parse(body) : {});
|
|
28339
29083
|
} catch (e) {
|
|
28340
29084
|
reject(e);
|
|
28341
29085
|
}
|
|
@@ -28344,12 +29088,12 @@ function readJsonBody(req) {
|
|
|
28344
29088
|
});
|
|
28345
29089
|
}
|
|
28346
29090
|
function readBinaryBody(req) {
|
|
28347
|
-
return new Promise((
|
|
29091
|
+
return new Promise((resolve46, reject) => {
|
|
28348
29092
|
const chunks = [];
|
|
28349
29093
|
req.on("data", (chunk) => {
|
|
28350
29094
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
28351
29095
|
});
|
|
28352
|
-
req.on("end", () =>
|
|
29096
|
+
req.on("end", () => resolve46(Buffer.concat(chunks)));
|
|
28353
29097
|
req.on("error", reject);
|
|
28354
29098
|
});
|
|
28355
29099
|
}
|
|
@@ -28449,10 +29193,10 @@ function serveStatic(distDir, req, res) {
|
|
|
28449
29193
|
return true;
|
|
28450
29194
|
}
|
|
28451
29195
|
function isPortAvailable(port, host) {
|
|
28452
|
-
return new Promise((
|
|
29196
|
+
return new Promise((resolve46) => {
|
|
28453
29197
|
const s = http.createServer();
|
|
28454
|
-
s.listen(port, host, () => s.close(() =>
|
|
28455
|
-
s.on("error", () =>
|
|
29198
|
+
s.listen(port, host, () => s.close(() => resolve46(true)));
|
|
29199
|
+
s.on("error", () => resolve46(false));
|
|
28456
29200
|
});
|
|
28457
29201
|
}
|
|
28458
29202
|
async function pickPort(preferred, host, strict) {
|
|
@@ -28545,10 +29289,10 @@ data: ${JSON.stringify(data)}
|
|
|
28545
29289
|
);
|
|
28546
29290
|
}
|
|
28547
29291
|
function waitForWatcherReady(watcher) {
|
|
28548
|
-
return new Promise((
|
|
29292
|
+
return new Promise((resolve46, reject) => {
|
|
28549
29293
|
const handleReady = () => {
|
|
28550
29294
|
watcher.off("error", handleError);
|
|
28551
|
-
|
|
29295
|
+
resolve46();
|
|
28552
29296
|
};
|
|
28553
29297
|
const handleError = (error) => {
|
|
28554
29298
|
watcher.off("ready", handleReady);
|
|
@@ -29163,12 +29907,12 @@ data: ${JSON.stringify(data)}
|
|
|
29163
29907
|
res.end("Not found");
|
|
29164
29908
|
}
|
|
29165
29909
|
});
|
|
29166
|
-
await new Promise((
|
|
29910
|
+
await new Promise((resolve46) => server.listen(port, host, resolve46));
|
|
29167
29911
|
const displayHost = host === "0.0.0.0" ? "localhost" : host;
|
|
29168
29912
|
const url = `http://${displayHost}:${port}`;
|
|
29169
29913
|
if (open) openBrowser(url);
|
|
29170
29914
|
const close4 = async () => {
|
|
29171
|
-
await new Promise((
|
|
29915
|
+
await new Promise((resolve46) => {
|
|
29172
29916
|
for (const timer of snapshotTimers.values()) clearTimeout(timer);
|
|
29173
29917
|
snapshotTimers.clear();
|
|
29174
29918
|
for (const w of watchers) w.close();
|
|
@@ -29181,7 +29925,7 @@ data: ${JSON.stringify(data)}
|
|
|
29181
29925
|
}
|
|
29182
29926
|
}
|
|
29183
29927
|
sseClients.clear();
|
|
29184
|
-
server.close(() =>
|
|
29928
|
+
server.close(() => resolve46());
|
|
29185
29929
|
});
|
|
29186
29930
|
await computeServer?.close();
|
|
29187
29931
|
};
|
|
@@ -29299,7 +30043,7 @@ function usage21(command) {
|
|
|
29299
30043
|
if (command === "render") return "Usage: forgecad context render <model.forge.js> [--out <dir>] [--force] [--size <px>]";
|
|
29300
30044
|
return `Usage: forgecad context ${command} <model.forge.js> [--format text|json]`;
|
|
29301
30045
|
}
|
|
29302
|
-
function
|
|
30046
|
+
function parseArgs15(command, args) {
|
|
29303
30047
|
let filePath;
|
|
29304
30048
|
let format = "text";
|
|
29305
30049
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -29584,7 +30328,7 @@ async function writeContextBundle(projectRoot, bundleDir, context, views) {
|
|
|
29584
30328
|
}
|
|
29585
30329
|
async function runContextGetCli(args = process.argv.slice(2)) {
|
|
29586
30330
|
try {
|
|
29587
|
-
const parsed =
|
|
30331
|
+
const parsed = parseArgs15("get", args);
|
|
29588
30332
|
const loaded = await loadAgentContext(parsed.filePath);
|
|
29589
30333
|
if (!loaded.context) {
|
|
29590
30334
|
console.error(`No submitted agent context for ${loaded.filePath}.`);
|
|
@@ -29603,7 +30347,7 @@ async function runContextGetCli(args = process.argv.slice(2)) {
|
|
|
29603
30347
|
}
|
|
29604
30348
|
async function runContextStatusCli(args = process.argv.slice(2)) {
|
|
29605
30349
|
try {
|
|
29606
|
-
const parsed =
|
|
30350
|
+
const parsed = parseArgs15("status", args);
|
|
29607
30351
|
const loaded = await loadAgentContext(parsed.filePath);
|
|
29608
30352
|
const status = statusForLoaded(loaded);
|
|
29609
30353
|
if (parsed.format === "json") {
|
|
@@ -29663,15 +30407,16 @@ async function runContextRenderCli(args = process.argv.slice(2)) {
|
|
|
29663
30407
|
import { readFile as readFile5, writeFile as writeFile9 } from "fs/promises";
|
|
29664
30408
|
import { basename as basename14, resolve as resolve34 } from "path";
|
|
29665
30409
|
function usage22() {
|
|
29666
|
-
console.error("Usage: forgecad export svg <script.forge.js> [input ...] [--output path]");
|
|
30410
|
+
console.error("Usage: forgecad export svg <script.forge.js> [input ...] [--output path] [--param Key=Value]");
|
|
29667
30411
|
process.exit(1);
|
|
29668
30412
|
}
|
|
29669
30413
|
function defaultSvgOutput(scriptPath) {
|
|
29670
30414
|
return scriptPath.replace(/\.(forge|sketch)\.js$/, ".svg").replace(/\.js$/, ".svg");
|
|
29671
30415
|
}
|
|
29672
|
-
function
|
|
30416
|
+
function parseArgs16(argv) {
|
|
29673
30417
|
const inputPaths = [];
|
|
29674
30418
|
let outputPath;
|
|
30419
|
+
const paramOverrides = {};
|
|
29675
30420
|
for (let index = 0; index < argv.length; index += 1) {
|
|
29676
30421
|
const arg = argv[index];
|
|
29677
30422
|
if (arg === "--output" || arg === "-o") {
|
|
@@ -29680,18 +30425,25 @@ function parseArgs15(argv) {
|
|
|
29680
30425
|
index += 1;
|
|
29681
30426
|
continue;
|
|
29682
30427
|
}
|
|
30428
|
+
if (arg === "--param" || arg === "-p") {
|
|
30429
|
+
const value = argv[index + 1];
|
|
30430
|
+
if (!value || value.startsWith("--")) throw new Error(`${arg} requires Key=Value.`);
|
|
30431
|
+
addCliParamOverride(value, paramOverrides);
|
|
30432
|
+
index += 1;
|
|
30433
|
+
continue;
|
|
30434
|
+
}
|
|
29683
30435
|
if (arg.startsWith("--")) throw new Error(`Unknown option: ${arg}`);
|
|
29684
30436
|
inputPaths.push(arg);
|
|
29685
30437
|
}
|
|
29686
30438
|
requireInputPaths(inputPaths, "Missing input path.");
|
|
29687
30439
|
requireScriptInputPaths(inputPaths);
|
|
29688
30440
|
requireSingleInputForOutputPath(inputPaths, outputPath);
|
|
29689
|
-
return { inputPaths, outputPath };
|
|
30441
|
+
return { inputPaths, outputPath, paramOverrides };
|
|
29690
30442
|
}
|
|
29691
30443
|
async function runSvgCli(argv = process.argv.slice(2)) {
|
|
29692
30444
|
let parsed;
|
|
29693
30445
|
try {
|
|
29694
|
-
parsed =
|
|
30446
|
+
parsed = parseArgs16(argv);
|
|
29695
30447
|
requireExistingInputPaths(parsed.inputPaths);
|
|
29696
30448
|
} catch (error) {
|
|
29697
30449
|
console.error(error instanceof Error ? error.message : String(error));
|
|
@@ -29701,7 +30453,7 @@ async function runSvgCli(argv = process.argv.slice(2)) {
|
|
|
29701
30453
|
for (const [index, scriptPath] of parsed.inputPaths.entries()) {
|
|
29702
30454
|
printBatchHeader(scriptPath, index, parsed.inputPaths.length);
|
|
29703
30455
|
try {
|
|
29704
|
-
await exportSvgInput(scriptPath, parsed.outputPath);
|
|
30456
|
+
await exportSvgInput(scriptPath, parsed.outputPath, parsed.paramOverrides);
|
|
29705
30457
|
} catch (error) {
|
|
29706
30458
|
failures += 1;
|
|
29707
30459
|
console.error(error instanceof Error ? error.message : String(error));
|
|
@@ -29711,7 +30463,7 @@ async function runSvgCli(argv = process.argv.slice(2)) {
|
|
|
29711
30463
|
process.exit(1);
|
|
29712
30464
|
}
|
|
29713
30465
|
}
|
|
29714
|
-
async function exportSvgInput(scriptPath, explicitOutputPath) {
|
|
30466
|
+
async function exportSvgInput(scriptPath, explicitOutputPath, paramOverrides) {
|
|
29715
30467
|
const outputPath = batchOutputPath(scriptPath, explicitOutputPath, defaultSvgOutput);
|
|
29716
30468
|
if (resolve34(outputPath) === resolve34(scriptPath)) {
|
|
29717
30469
|
throw new Error(`ERROR: output path would overwrite the input script. Specify an explicit output path.`);
|
|
@@ -29719,6 +30471,7 @@ async function exportSvgInput(scriptPath, explicitOutputPath) {
|
|
|
29719
30471
|
const code = await readFile5(resolve34(scriptPath), "utf-8");
|
|
29720
30472
|
const { allFiles, fileName } = collectProjectFiles(scriptPath);
|
|
29721
30473
|
await init();
|
|
30474
|
+
setParamOverrides(paramOverrides);
|
|
29722
30475
|
const result = runScript(code, fileName, allFiles);
|
|
29723
30476
|
if (result.error) {
|
|
29724
30477
|
throw new Error(`Script error: ${result.error}`);
|
|
@@ -30145,9 +30898,10 @@ function buildUrdfRobotPackage(spec) {
|
|
|
30145
30898
|
}
|
|
30146
30899
|
|
|
30147
30900
|
// cli/forge-urdf.ts
|
|
30148
|
-
function
|
|
30901
|
+
function parseArgs17(argv) {
|
|
30149
30902
|
const inputPaths = [];
|
|
30150
30903
|
let outputPath;
|
|
30904
|
+
const paramOverrides = {};
|
|
30151
30905
|
const jointOverrides = {};
|
|
30152
30906
|
for (let i = 0; i < argv.length; i += 1) {
|
|
30153
30907
|
const arg = argv[i];
|
|
@@ -30164,25 +30918,31 @@ function parseArgs16(argv) {
|
|
|
30164
30918
|
i += 1;
|
|
30165
30919
|
continue;
|
|
30166
30920
|
}
|
|
30921
|
+
if (arg === "--param" || arg === "-p") {
|
|
30922
|
+
const value = argv[i + 1];
|
|
30923
|
+
if (!value || value.startsWith("--")) throw new Error(`${arg} requires Key=Value`);
|
|
30924
|
+
addCliParamOverride(value, paramOverrides);
|
|
30925
|
+
i += 1;
|
|
30926
|
+
continue;
|
|
30927
|
+
}
|
|
30167
30928
|
if (arg.startsWith("--")) {
|
|
30168
30929
|
throw new Error(`Unknown flag: ${arg}`);
|
|
30169
30930
|
}
|
|
30170
30931
|
inputPaths.push(arg);
|
|
30171
30932
|
}
|
|
30172
|
-
requireInputPaths(
|
|
30933
|
+
requireInputPaths(
|
|
30934
|
+
inputPaths,
|
|
30935
|
+
"Usage: forgecad export urdf <script.forge.js> [input ...] [--output dir] [--param Key=Value] [--joint JointName=Value]"
|
|
30936
|
+
);
|
|
30173
30937
|
requireScriptInputPaths(inputPaths);
|
|
30174
30938
|
requireSingleInputForOutputPath(inputPaths, outputPath, "--output", "dir");
|
|
30175
|
-
return { inputPaths, outputPath, jointOverrides };
|
|
30939
|
+
return { inputPaths, outputPath, paramOverrides, jointOverrides };
|
|
30176
30940
|
}
|
|
30177
30941
|
function defaultOutputPath10(scriptPath) {
|
|
30178
30942
|
const abs = resolve35(scriptPath);
|
|
30179
30943
|
return abs.slice(0, abs.length - extname11(abs).length) + ".urdfpkg";
|
|
30180
30944
|
}
|
|
30181
30945
|
function resolveSimulationModel4(result, jointOverrides) {
|
|
30182
|
-
const legacy = getCollectedRobotExport();
|
|
30183
|
-
if (legacy) {
|
|
30184
|
-
return { ...legacy, state: { ...legacy.state, ...jointOverrides } };
|
|
30185
|
-
}
|
|
30186
30946
|
const assembly2 = getRunResultAssemblySource(result);
|
|
30187
30947
|
if (!assembly2) return null;
|
|
30188
30948
|
const def = assembly2.describe();
|
|
@@ -30190,13 +30950,13 @@ function resolveSimulationModel4(result, jointOverrides) {
|
|
|
30190
30950
|
return collectSimulationModel(def, { state: jointOverrides });
|
|
30191
30951
|
}
|
|
30192
30952
|
async function runUrdfCli(argv = process.argv.slice(2)) {
|
|
30193
|
-
const { inputPaths, outputPath, jointOverrides } =
|
|
30953
|
+
const { inputPaths, outputPath, paramOverrides, jointOverrides } = parseArgs17(argv);
|
|
30194
30954
|
requireExistingInputPaths(inputPaths);
|
|
30195
30955
|
let failures = 0;
|
|
30196
30956
|
for (const [index, scriptPath] of inputPaths.entries()) {
|
|
30197
30957
|
printBatchHeader(scriptPath, index, inputPaths.length);
|
|
30198
30958
|
try {
|
|
30199
|
-
await exportUrdfInput(scriptPath, outputPath, jointOverrides);
|
|
30959
|
+
await exportUrdfInput(scriptPath, outputPath, paramOverrides, jointOverrides);
|
|
30200
30960
|
} catch (error) {
|
|
30201
30961
|
failures += 1;
|
|
30202
30962
|
console.error(error instanceof Error ? error.message : String(error));
|
|
@@ -30206,17 +30966,18 @@ async function runUrdfCli(argv = process.argv.slice(2)) {
|
|
|
30206
30966
|
process.exit(1);
|
|
30207
30967
|
}
|
|
30208
30968
|
}
|
|
30209
|
-
async function exportUrdfInput(scriptPath, explicitOutputPath, jointOverrides) {
|
|
30969
|
+
async function exportUrdfInput(scriptPath, explicitOutputPath, paramOverrides, jointOverrides) {
|
|
30210
30970
|
const code = readFileSync20(resolve35(scriptPath), "utf-8");
|
|
30211
30971
|
const { allFiles, fileName } = collectProjectFiles(scriptPath);
|
|
30212
30972
|
await init();
|
|
30973
|
+
setParamOverrides(paramOverrides);
|
|
30213
30974
|
const result = applyCliJointOverrides(runScript(code, fileName, allFiles, { assemblyState: jointOverrides }), jointOverrides);
|
|
30214
30975
|
if (result.error) {
|
|
30215
30976
|
throw new Error(`ERROR: ${result.error}`);
|
|
30216
30977
|
}
|
|
30217
30978
|
const robot = resolveSimulationModel4(result, jointOverrides);
|
|
30218
30979
|
if (!robot) {
|
|
30219
|
-
throw new Error("URDF export requires the script to return assembly(...).withSimulation(...)
|
|
30980
|
+
throw new Error("URDF export requires the script to return assembly(...).withSimulation(...).");
|
|
30220
30981
|
}
|
|
30221
30982
|
const packageOut = buildUrdfRobotPackage(robot);
|
|
30222
30983
|
const targetDir = resolve35(batchOutputPath(scriptPath, explicitOutputPath, defaultOutputPath10));
|
|
@@ -30777,9 +31538,10 @@ ${jointResults.map((result) => result.text).join("\n\n")}
|
|
|
30777
31538
|
}
|
|
30778
31539
|
|
|
30779
31540
|
// cli/forge-usd.ts
|
|
30780
|
-
function
|
|
31541
|
+
function parseArgs18(argv) {
|
|
30781
31542
|
const inputPaths = [];
|
|
30782
31543
|
let outputPath;
|
|
31544
|
+
const paramOverrides = {};
|
|
30783
31545
|
const jointOverrides = {};
|
|
30784
31546
|
for (let i = 0; i < argv.length; i += 1) {
|
|
30785
31547
|
const arg = argv[i];
|
|
@@ -30796,21 +31558,29 @@ function parseArgs17(argv) {
|
|
|
30796
31558
|
i += 1;
|
|
30797
31559
|
continue;
|
|
30798
31560
|
}
|
|
31561
|
+
if (arg === "--param" || arg === "-p") {
|
|
31562
|
+
const value = argv[i + 1];
|
|
31563
|
+
if (!value || value.startsWith("--")) throw new Error(`${arg} requires Key=Value`);
|
|
31564
|
+
addCliParamOverride(value, paramOverrides);
|
|
31565
|
+
i += 1;
|
|
31566
|
+
continue;
|
|
31567
|
+
}
|
|
30799
31568
|
if (arg.startsWith("--")) throw new Error(`Unknown flag: ${arg}`);
|
|
30800
31569
|
inputPaths.push(arg);
|
|
30801
31570
|
}
|
|
30802
|
-
requireInputPaths(
|
|
31571
|
+
requireInputPaths(
|
|
31572
|
+
inputPaths,
|
|
31573
|
+
"Usage: forgecad export usd <script.forge.js> [input ...] [--output dir] [--param Key=Value] [--joint JointName=Value]"
|
|
31574
|
+
);
|
|
30803
31575
|
requireScriptInputPaths(inputPaths);
|
|
30804
31576
|
requireSingleInputForOutputPath(inputPaths, outputPath, "--output", "dir");
|
|
30805
|
-
return { inputPaths, outputPath, jointOverrides };
|
|
31577
|
+
return { inputPaths, outputPath, paramOverrides, jointOverrides };
|
|
30806
31578
|
}
|
|
30807
31579
|
function defaultOutputPath11(scriptPath) {
|
|
30808
31580
|
const abs = resolve36(scriptPath);
|
|
30809
31581
|
return abs.slice(0, abs.length - extname12(abs).length) + ".usdpkg";
|
|
30810
31582
|
}
|
|
30811
31583
|
function resolveSimulationModel5(result, jointOverrides) {
|
|
30812
|
-
const legacy = getCollectedRobotExport();
|
|
30813
|
-
if (legacy) return { ...legacy, state: { ...legacy.state, ...jointOverrides } };
|
|
30814
31584
|
const assembly2 = getRunResultAssemblySource(result);
|
|
30815
31585
|
if (!assembly2) return null;
|
|
30816
31586
|
const def = assembly2.describe();
|
|
@@ -30818,13 +31588,13 @@ function resolveSimulationModel5(result, jointOverrides) {
|
|
|
30818
31588
|
return collectSimulationModel(def, { state: jointOverrides });
|
|
30819
31589
|
}
|
|
30820
31590
|
async function runUsdCli(argv = process.argv.slice(2)) {
|
|
30821
|
-
const { inputPaths, outputPath, jointOverrides } =
|
|
31591
|
+
const { inputPaths, outputPath, paramOverrides, jointOverrides } = parseArgs18(argv);
|
|
30822
31592
|
requireExistingInputPaths(inputPaths);
|
|
30823
31593
|
let failures = 0;
|
|
30824
31594
|
for (const [index, scriptPath] of inputPaths.entries()) {
|
|
30825
31595
|
printBatchHeader(scriptPath, index, inputPaths.length);
|
|
30826
31596
|
try {
|
|
30827
|
-
await exportUsdInput(scriptPath, outputPath, jointOverrides);
|
|
31597
|
+
await exportUsdInput(scriptPath, outputPath, paramOverrides, jointOverrides);
|
|
30828
31598
|
} catch (error) {
|
|
30829
31599
|
failures += 1;
|
|
30830
31600
|
console.error(error instanceof Error ? error.message : String(error));
|
|
@@ -30834,17 +31604,18 @@ async function runUsdCli(argv = process.argv.slice(2)) {
|
|
|
30834
31604
|
process.exit(1);
|
|
30835
31605
|
}
|
|
30836
31606
|
}
|
|
30837
|
-
async function exportUsdInput(scriptPath, explicitOutputPath, jointOverrides) {
|
|
31607
|
+
async function exportUsdInput(scriptPath, explicitOutputPath, paramOverrides, jointOverrides) {
|
|
30838
31608
|
const code = readFileSync21(resolve36(scriptPath), "utf-8");
|
|
30839
31609
|
const { allFiles, fileName } = collectProjectFiles(scriptPath);
|
|
30840
31610
|
await init();
|
|
31611
|
+
setParamOverrides(paramOverrides);
|
|
30841
31612
|
const result = applyCliJointOverrides(runScript(code, fileName, allFiles, { assemblyState: jointOverrides }), jointOverrides);
|
|
30842
31613
|
if (result.error) {
|
|
30843
31614
|
throw new Error(`ERROR: ${result.error}`);
|
|
30844
31615
|
}
|
|
30845
31616
|
const model = resolveSimulationModel5(result, jointOverrides);
|
|
30846
31617
|
if (!model) {
|
|
30847
|
-
throw new Error("USD export requires the script to return assembly(...).withSimulation(...)
|
|
31618
|
+
throw new Error("USD export requires the script to return assembly(...).withSimulation(...).");
|
|
30848
31619
|
}
|
|
30849
31620
|
const packageOut = buildUsdRobotPackage(model);
|
|
30850
31621
|
const targetDir = resolve36(batchOutputPath(scriptPath, explicitOutputPath, defaultOutputPath11));
|
|
@@ -30886,9 +31657,9 @@ function parseWebArgs(argv) {
|
|
|
30886
31657
|
return options;
|
|
30887
31658
|
}
|
|
30888
31659
|
function waitForExit2(child) {
|
|
30889
|
-
return new Promise((
|
|
31660
|
+
return new Promise((resolve46, reject) => {
|
|
30890
31661
|
child.once("error", reject);
|
|
30891
|
-
child.once("exit", (code) =>
|
|
31662
|
+
child.once("exit", (code) => resolve46(code ?? 0));
|
|
30892
31663
|
});
|
|
30893
31664
|
}
|
|
30894
31665
|
async function runWebCli(argv = process.argv.slice(2)) {
|
|
@@ -31502,10 +32273,10 @@ function padRight(s, w) {
|
|
|
31502
32273
|
}
|
|
31503
32274
|
function confirm(question) {
|
|
31504
32275
|
const rl = createInterface2({ input: process.stdin, output: process.stderr });
|
|
31505
|
-
return new Promise((
|
|
32276
|
+
return new Promise((resolve46) => {
|
|
31506
32277
|
rl.question(`${question} [y/N] `, (answer) => {
|
|
31507
32278
|
rl.close();
|
|
31508
|
-
|
|
32279
|
+
resolve46(answer.trim().toLowerCase() === "y");
|
|
31509
32280
|
});
|
|
31510
32281
|
});
|
|
31511
32282
|
}
|
|
@@ -31515,10 +32286,10 @@ function moveCandidateReasonLabel(candidate) {
|
|
|
31515
32286
|
}
|
|
31516
32287
|
function promptChoice(question) {
|
|
31517
32288
|
const rl = createInterface2({ input: process.stdin, output: process.stderr });
|
|
31518
|
-
return new Promise((
|
|
32289
|
+
return new Promise((resolve46) => {
|
|
31519
32290
|
rl.question(question, (answer) => {
|
|
31520
32291
|
rl.close();
|
|
31521
|
-
|
|
32292
|
+
resolve46(answer.trim());
|
|
31522
32293
|
});
|
|
31523
32294
|
});
|
|
31524
32295
|
}
|
|
@@ -33440,7 +34211,7 @@ function takeValue(argv, index, name) {
|
|
|
33440
34211
|
}
|
|
33441
34212
|
return value;
|
|
33442
34213
|
}
|
|
33443
|
-
function
|
|
34214
|
+
function parseArgs19(argv) {
|
|
33444
34215
|
const { consumed: focusConsumed } = parseFocusFlags(argv);
|
|
33445
34216
|
const consumed = new Set(focusConsumed);
|
|
33446
34217
|
const profileOverrides = {};
|
|
@@ -33619,7 +34390,7 @@ function scriptErrorReport(scriptPath, message, profile, elapsedMs) {
|
|
|
33619
34390
|
};
|
|
33620
34391
|
}
|
|
33621
34392
|
async function runPrintCheckCli(argv = process.argv.slice(2)) {
|
|
33622
|
-
const args =
|
|
34393
|
+
const args = parseArgs19(argv);
|
|
33623
34394
|
requireExistingInputPaths(args.inputPaths);
|
|
33624
34395
|
const profile = resolvePrintProfile(args.profileId, args.profileOverrides);
|
|
33625
34396
|
const reports = [];
|
|
@@ -33687,12 +34458,1481 @@ async function runPrintCheckInput(args, scriptPath, profile, argv) {
|
|
|
33687
34458
|
return report;
|
|
33688
34459
|
}
|
|
33689
34460
|
|
|
34461
|
+
// cli/forge-pinocchio.ts
|
|
34462
|
+
import { mkdirSync as mkdirSync13, readFileSync as readFileSync25, writeFileSync as writeFileSync20 } from "fs";
|
|
34463
|
+
import { dirname as dirname13, extname as extname13, resolve as resolve41 } from "path";
|
|
34464
|
+
|
|
34465
|
+
// src/forge/export/pinocchioExport.ts
|
|
34466
|
+
var DEG2RAD = Math.PI / 180;
|
|
34467
|
+
function defaultRange(type) {
|
|
34468
|
+
return type === "prismatic" ? { min: 0, max: 0.1 } : { min: -Math.PI / 2, max: Math.PI / 2 };
|
|
34469
|
+
}
|
|
34470
|
+
function toModelUnits(type, value) {
|
|
34471
|
+
return type === "prismatic" ? value * 1e-3 : value * DEG2RAD;
|
|
34472
|
+
}
|
|
34473
|
+
function buildPinocchioPackage(spec) {
|
|
34474
|
+
const urdf = buildUrdfRobotPackage(spec);
|
|
34475
|
+
const assemblyJoints = new Map(spec.assembly.joints.map((j) => [j.name, j]));
|
|
34476
|
+
const actuated = urdf.manifest.joints.filter((j) => j.type === "revolute" || j.type === "prismatic");
|
|
34477
|
+
if (actuated.length === 0) {
|
|
34478
|
+
throw new Error("PINOCCHIO.MOTION.EMPTY: the assembly has no revolute or prismatic joints to drive.");
|
|
34479
|
+
}
|
|
34480
|
+
const joints = actuated.map((j) => {
|
|
34481
|
+
const source = assemblyJoints.get(j.sourceName);
|
|
34482
|
+
const range = source && source.min !== void 0 && source.max !== void 0 ? { min: toModelUnits(j.type, source.min), max: toModelUnits(j.type, source.max) } : defaultRange(j.type);
|
|
34483
|
+
const effort = spec.joints[j.sourceName]?.effort;
|
|
34484
|
+
return {
|
|
34485
|
+
sourceName: j.sourceName,
|
|
34486
|
+
urdfName: j.urdfName,
|
|
34487
|
+
type: j.type,
|
|
34488
|
+
min: range.min,
|
|
34489
|
+
max: range.max,
|
|
34490
|
+
samples: 17,
|
|
34491
|
+
budget: typeof effort === "number" && effort > 0 ? effort : null
|
|
34492
|
+
};
|
|
34493
|
+
});
|
|
34494
|
+
const manifest = {
|
|
34495
|
+
format: "forgecad-pinocchio-package",
|
|
34496
|
+
modelName: urdf.modelName,
|
|
34497
|
+
sourceModelName: spec.modelName,
|
|
34498
|
+
urdfPath: urdf.manifest.urdfPath,
|
|
34499
|
+
feedbackPath: "feedback.json",
|
|
34500
|
+
expectedDof: actuated.length,
|
|
34501
|
+
gravity: [0, 0, -9.80665],
|
|
34502
|
+
joints,
|
|
34503
|
+
install: {
|
|
34504
|
+
linuxMac: "uv run --python 3.11 --with pin --with numpy python scripts/run_pinocchio.py",
|
|
34505
|
+
windows: "conda install pinocchio coal -c conda-forge # then: python scripts/run_pinocchio.py"
|
|
34506
|
+
},
|
|
34507
|
+
runCommand: "uv run --python 3.11 --with pin --with numpy python scripts/run_pinocchio.py"
|
|
34508
|
+
};
|
|
34509
|
+
const files = [
|
|
34510
|
+
...urdf.files,
|
|
34511
|
+
{ path: "pinocchio-manifest.json", text: JSON.stringify(manifest, null, 2) },
|
|
34512
|
+
{ path: "scripts/run_pinocchio.py", text: runPinocchioScript() },
|
|
34513
|
+
{ path: "README.md", text: readme(manifest) }
|
|
34514
|
+
];
|
|
34515
|
+
return { modelName: urdf.modelName, manifest, files };
|
|
34516
|
+
}
|
|
34517
|
+
function readme(manifest) {
|
|
34518
|
+
return [
|
|
34519
|
+
`# ${manifest.sourceModelName} \u2014 Pinocchio package`,
|
|
34520
|
+
"",
|
|
34521
|
+
"Generated by ForgeCAD. The model is a standard URDF with per-link inertials computed from the",
|
|
34522
|
+
"solid geometry; Pinocchio (BSD-2-Clause) builds its dynamics model from it.",
|
|
34523
|
+
"",
|
|
34524
|
+
"## Run",
|
|
34525
|
+
"",
|
|
34526
|
+
"Linux / macOS:",
|
|
34527
|
+
"",
|
|
34528
|
+
"```bash",
|
|
34529
|
+
manifest.install.linuxMac,
|
|
34530
|
+
"```",
|
|
34531
|
+
"",
|
|
34532
|
+
"Windows (no PyPI wheel \u2014 use conda-forge):",
|
|
34533
|
+
"",
|
|
34534
|
+
"```bash",
|
|
34535
|
+
manifest.install.windows,
|
|
34536
|
+
"```",
|
|
34537
|
+
"",
|
|
34538
|
+
"`run_pinocchio.py` sweeps each joint across its range, computes the gravity-hold torque with",
|
|
34539
|
+
"`pin.computeGeneralizedGravity`, and writes `feedback.json` (the `forgecad.feedback/v1` contract).",
|
|
34540
|
+
"Re-surface it in ForgeCAD with `forgecad sim dynamics <this-dir>`.",
|
|
34541
|
+
"",
|
|
34542
|
+
"Coal (BSD-3-Clause) provides collision via convex hulls / BVH over `.obj`/`.stl`/`.dae` meshes;",
|
|
34543
|
+
"it performs no convex decomposition. This package ships visual + collision STLs from the URDF export.",
|
|
34544
|
+
"",
|
|
34545
|
+
"Cross-check: the gravity torque at pose 0 should match `forgecad sim mechanism` static-hold torque."
|
|
34546
|
+
].join("\n");
|
|
34547
|
+
}
|
|
34548
|
+
function runPinocchioScript() {
|
|
34549
|
+
return `#!/usr/bin/env python3
|
|
34550
|
+
"""Compute per-joint gravity-hold torque budgets with Pinocchio and emit feedback.json."""
|
|
34551
|
+
import json
|
|
34552
|
+
import os
|
|
34553
|
+
import sys
|
|
34554
|
+
|
|
34555
|
+
import numpy as np
|
|
34556
|
+
|
|
34557
|
+
try:
|
|
34558
|
+
import pinocchio as pin
|
|
34559
|
+
except ImportError:
|
|
34560
|
+
raise SystemExit(
|
|
34561
|
+
"Pinocchio not found. Linux/macOS: pip install pin coal. "
|
|
34562
|
+
"Windows: conda install pinocchio coal -c conda-forge."
|
|
34563
|
+
)
|
|
34564
|
+
|
|
34565
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
34566
|
+
ROOT = os.path.dirname(HERE)
|
|
34567
|
+
manifest = json.load(open(os.path.join(ROOT, "pinocchio-manifest.json")))
|
|
34568
|
+
|
|
34569
|
+
model = pin.buildModelFromUrdf(os.path.join(ROOT, manifest["urdfPath"]))
|
|
34570
|
+
data = model.createData()
|
|
34571
|
+
model.gravity.linear = np.array(manifest["gravity"], dtype=float)
|
|
34572
|
+
|
|
34573
|
+
metrics = [{"key": "dofCount", "label": "DOF (model.nv)", "value": int(model.nv), "unit": "", "status": "info"}]
|
|
34574
|
+
findings = []
|
|
34575
|
+
joint_data = []
|
|
34576
|
+
|
|
34577
|
+
for spec in manifest["joints"]:
|
|
34578
|
+
if not model.existJointName(spec["urdfName"]):
|
|
34579
|
+
findings.append({"level": "warning", "code": "PINOCCHIO.JOINT.MISSING",
|
|
34580
|
+
"message": "URDF joint %s not found in the built model" % spec["urdfName"],
|
|
34581
|
+
"path": spec["sourceName"]})
|
|
34582
|
+
continue
|
|
34583
|
+
jid = model.getJointId(spec["urdfName"])
|
|
34584
|
+
iq = model.joints[jid].idx_q
|
|
34585
|
+
iv = model.joints[jid].idx_v
|
|
34586
|
+
peak = 0.0
|
|
34587
|
+
for value in np.linspace(spec["min"], spec["max"], int(spec["samples"])):
|
|
34588
|
+
q = pin.neutral(model)
|
|
34589
|
+
q[iq] = value
|
|
34590
|
+
g = pin.computeGeneralizedGravity(model, data, q)
|
|
34591
|
+
peak = max(peak, abs(float(g[iv])))
|
|
34592
|
+
is_revolute = spec["type"] == "revolute"
|
|
34593
|
+
unit = "N\xB7m" if is_revolute else "N"
|
|
34594
|
+
key = ("peakGravityTorqueNm." if is_revolute else "peakGravityForceN.") + spec["sourceName"]
|
|
34595
|
+
metrics.append({"key": key, "label": "Peak gravity load (%s)" % spec["sourceName"],
|
|
34596
|
+
"value": round(peak, 4), "unit": unit, "status": "info"})
|
|
34597
|
+
entry = {"joint": spec["sourceName"], "type": spec["type"], "peak": round(peak, 4), "budget": spec["budget"]}
|
|
34598
|
+
if spec["budget"]:
|
|
34599
|
+
margin = (spec["budget"] - peak) / spec["budget"] * 100.0
|
|
34600
|
+
entry["marginPct"] = round(margin, 2)
|
|
34601
|
+
metrics.append({"key": "torqueMarginPct." + spec["sourceName"], "label": "Budget margin (%s)" % spec["sourceName"],
|
|
34602
|
+
"value": round(margin, 2), "unit": "%", "status": "pass" if margin >= 0 else "fail",
|
|
34603
|
+
"target": {"op": ">=", "value": 0}})
|
|
34604
|
+
if margin < 0:
|
|
34605
|
+
findings.append({"level": "error", "code": "PINOCCHIO.HOLD.OVER_BUDGET", "path": spec["sourceName"],
|
|
34606
|
+
"message": "Joint %s peaks at %.3f %s over its %.3f %s budget" % (
|
|
34607
|
+
spec["sourceName"], peak, unit, spec["budget"], unit),
|
|
34608
|
+
"suggest": "Increase the Sim.drive effort or reduce the distal moment."})
|
|
34609
|
+
joint_data.append(entry)
|
|
34610
|
+
|
|
34611
|
+
ok = all(m.get("status") != "fail" for m in metrics) and not any(f["level"] == "error" for f in findings)
|
|
34612
|
+
report = {
|
|
34613
|
+
"schema": "forgecad.feedback/v1", "category": "dynamics", "source": manifest["sourceModelName"], "ok": ok,
|
|
34614
|
+
"summary": "%d DOF, %d joint(s) swept" % (model.nv, len(joint_data)),
|
|
34615
|
+
"metrics": metrics, "findings": findings,
|
|
34616
|
+
"trust": {"assumptions": ["Pinocchio gravity-hold torque swept over joint ranges; no contact or friction."],
|
|
34617
|
+
"converged": True},
|
|
34618
|
+
"data": {"dof": int(model.nv), "joints": joint_data}, "timeMs": 0,
|
|
34619
|
+
}
|
|
34620
|
+
out = os.path.join(ROOT, manifest["feedbackPath"])
|
|
34621
|
+
json.dump(report, open(out, "w"), indent=2)
|
|
34622
|
+
print("Wrote %s (ok=%s)" % (out, ok))
|
|
34623
|
+
sys.exit(0 if ok else 1)
|
|
34624
|
+
`;
|
|
34625
|
+
}
|
|
34626
|
+
|
|
34627
|
+
// cli/forge-pinocchio.ts
|
|
34628
|
+
function parseArgs20(argv) {
|
|
34629
|
+
const inputPaths = [];
|
|
34630
|
+
let outputPath;
|
|
34631
|
+
const paramOverrides = {};
|
|
34632
|
+
const jointOverrides = {};
|
|
34633
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
34634
|
+
const arg = argv[i];
|
|
34635
|
+
if (arg === "--output") {
|
|
34636
|
+
outputPath = argv[i + 1];
|
|
34637
|
+
if (!outputPath) throw new Error("--output requires a directory path");
|
|
34638
|
+
i += 1;
|
|
34639
|
+
continue;
|
|
34640
|
+
}
|
|
34641
|
+
if (arg === "--joint") {
|
|
34642
|
+
const value = argv[i + 1];
|
|
34643
|
+
if (!value || value.startsWith("--")) throw new Error("--joint requires JointName=Value");
|
|
34644
|
+
addCliJointOverride(value, jointOverrides);
|
|
34645
|
+
i += 1;
|
|
34646
|
+
continue;
|
|
34647
|
+
}
|
|
34648
|
+
if (arg === "--param" || arg === "-p") {
|
|
34649
|
+
const value = argv[i + 1];
|
|
34650
|
+
if (!value || value.startsWith("--")) throw new Error(`${arg} requires Key=Value`);
|
|
34651
|
+
addCliParamOverride(value, paramOverrides);
|
|
34652
|
+
i += 1;
|
|
34653
|
+
continue;
|
|
34654
|
+
}
|
|
34655
|
+
if (arg.startsWith("--")) throw new Error(`Unknown flag: ${arg}`);
|
|
34656
|
+
inputPaths.push(arg);
|
|
34657
|
+
}
|
|
34658
|
+
requireInputPaths(inputPaths, "Usage: forgecad export pinocchio <script.forge.js> [input ...] [--output dir] [--param Key=Value] [--joint JointName=Value]");
|
|
34659
|
+
requireScriptInputPaths(inputPaths);
|
|
34660
|
+
requireSingleInputForOutputPath(inputPaths, outputPath, "--output", "dir");
|
|
34661
|
+
return { inputPaths, outputPath, paramOverrides, jointOverrides };
|
|
34662
|
+
}
|
|
34663
|
+
function defaultOutputPath12(scriptPath) {
|
|
34664
|
+
const abs = resolve41(scriptPath);
|
|
34665
|
+
return abs.slice(0, abs.length - extname13(abs).length) + ".pinocchiopkg";
|
|
34666
|
+
}
|
|
34667
|
+
function resolveSimulationModel6(result, jointOverrides) {
|
|
34668
|
+
const assembly2 = getRunResultAssemblySource(result);
|
|
34669
|
+
if (!assembly2) return null;
|
|
34670
|
+
const def = assembly2.describe();
|
|
34671
|
+
if (!def.sim) return null;
|
|
34672
|
+
return collectSimulationModel(def, { state: jointOverrides });
|
|
34673
|
+
}
|
|
34674
|
+
async function runPinocchioCli(argv = process.argv.slice(2)) {
|
|
34675
|
+
const { inputPaths, outputPath, paramOverrides, jointOverrides } = parseArgs20(argv);
|
|
34676
|
+
requireExistingInputPaths(inputPaths);
|
|
34677
|
+
let failures = 0;
|
|
34678
|
+
for (const [index, scriptPath] of inputPaths.entries()) {
|
|
34679
|
+
printBatchHeader(scriptPath, index, inputPaths.length);
|
|
34680
|
+
try {
|
|
34681
|
+
await exportPinocchioInput(scriptPath, outputPath, paramOverrides, jointOverrides);
|
|
34682
|
+
} catch (error) {
|
|
34683
|
+
failures += 1;
|
|
34684
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
34685
|
+
}
|
|
34686
|
+
}
|
|
34687
|
+
if (failures > 0) process.exit(1);
|
|
34688
|
+
}
|
|
34689
|
+
async function exportPinocchioInput(scriptPath, explicitOutputPath, paramOverrides, jointOverrides) {
|
|
34690
|
+
const code = readFileSync25(resolve41(scriptPath), "utf-8");
|
|
34691
|
+
const { allFiles, fileName } = collectProjectFiles(scriptPath);
|
|
34692
|
+
await init();
|
|
34693
|
+
setParamOverrides(paramOverrides);
|
|
34694
|
+
const result = applyCliJointOverrides(runScript(code, fileName, allFiles, { assemblyState: jointOverrides }), jointOverrides);
|
|
34695
|
+
if (result.error) throw new Error(`ERROR: ${result.error}`);
|
|
34696
|
+
const robot = resolveSimulationModel6(result, jointOverrides);
|
|
34697
|
+
if (!robot) throw new Error("Pinocchio export requires the script to return assembly(...).withSimulation(...).");
|
|
34698
|
+
const packageOut = buildPinocchioPackage(robot);
|
|
34699
|
+
const targetDir = resolve41(batchOutputPath(scriptPath, explicitOutputPath, defaultOutputPath12));
|
|
34700
|
+
packageOut.files.forEach((file) => {
|
|
34701
|
+
const absPath = resolve41(targetDir, file.path);
|
|
34702
|
+
mkdirSync13(dirname13(absPath), { recursive: true });
|
|
34703
|
+
if (file.text !== void 0) writeFileSync20(absPath, file.text, "utf-8");
|
|
34704
|
+
else if (file.bytes) writeFileSync20(absPath, Buffer.from(file.bytes));
|
|
34705
|
+
});
|
|
34706
|
+
console.log(`\u2713 Exported Pinocchio package to ${targetDir}`);
|
|
34707
|
+
console.log(` urdf: ${packageOut.manifest.urdfPath}`);
|
|
34708
|
+
console.log(` run: ${packageOut.manifest.runCommand}`);
|
|
34709
|
+
console.log(` then: forgecad sim dynamics ${targetDir}`);
|
|
34710
|
+
}
|
|
34711
|
+
|
|
34712
|
+
// cli/sim-dynamics.ts
|
|
34713
|
+
import { existsSync as existsSync21, readFileSync as readFileSync26, statSync as statSync9 } from "fs";
|
|
34714
|
+
import { join as join15, resolve as resolve42 } from "path";
|
|
34715
|
+
function parseArgs21(argv) {
|
|
34716
|
+
const inputPaths = [];
|
|
34717
|
+
let json = false;
|
|
34718
|
+
let compact = false;
|
|
34719
|
+
let noFailExit = false;
|
|
34720
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
34721
|
+
const arg = argv[i];
|
|
34722
|
+
if (arg === "--json") {
|
|
34723
|
+
json = true;
|
|
34724
|
+
continue;
|
|
34725
|
+
}
|
|
34726
|
+
if (arg === "--compact") {
|
|
34727
|
+
compact = true;
|
|
34728
|
+
continue;
|
|
34729
|
+
}
|
|
34730
|
+
if (arg === "--no-fail-exit") {
|
|
34731
|
+
noFailExit = true;
|
|
34732
|
+
continue;
|
|
34733
|
+
}
|
|
34734
|
+
if (arg.startsWith("--")) throw new Error(`Unknown flag: ${arg}`);
|
|
34735
|
+
inputPaths.push(arg);
|
|
34736
|
+
}
|
|
34737
|
+
requireInputPaths(inputPaths, "Usage: forgecad sim dynamics <pinocchio-package-dir|feedback.json> [--json]");
|
|
34738
|
+
return { inputPaths, json, compact, noFailExit };
|
|
34739
|
+
}
|
|
34740
|
+
function failedReport3(source, message, code, suggest) {
|
|
34741
|
+
return {
|
|
34742
|
+
schema: FEEDBACK_SCHEMA,
|
|
34743
|
+
category: "dynamics",
|
|
34744
|
+
source,
|
|
34745
|
+
ok: false,
|
|
34746
|
+
summary: "dynamics results unavailable",
|
|
34747
|
+
metrics: [],
|
|
34748
|
+
findings: [{ level: "error", code, message, ...suggest ? { suggest } : {} }],
|
|
34749
|
+
trust: { assumptions: [] },
|
|
34750
|
+
data: {},
|
|
34751
|
+
timeMs: 0
|
|
34752
|
+
};
|
|
34753
|
+
}
|
|
34754
|
+
function resolveFeedbackPath(inputPath) {
|
|
34755
|
+
const abs = resolve42(inputPath);
|
|
34756
|
+
if (existsSync21(abs) && statSync9(abs).isDirectory()) return join15(abs, "feedback.json");
|
|
34757
|
+
return abs;
|
|
34758
|
+
}
|
|
34759
|
+
function ingest(inputPath) {
|
|
34760
|
+
const feedbackPath = resolveFeedbackPath(inputPath);
|
|
34761
|
+
if (!existsSync21(feedbackPath)) {
|
|
34762
|
+
return failedReport3(
|
|
34763
|
+
inputPath,
|
|
34764
|
+
`No results at ${feedbackPath}.`,
|
|
34765
|
+
"DYNAMICS.NOT_RUN",
|
|
34766
|
+
"Run the exported scripts/run_pinocchio.py first (see the package README), then re-run sim dynamics."
|
|
34767
|
+
);
|
|
34768
|
+
}
|
|
34769
|
+
let parsed;
|
|
34770
|
+
try {
|
|
34771
|
+
parsed = JSON.parse(readFileSync26(feedbackPath, "utf-8"));
|
|
34772
|
+
} catch (error) {
|
|
34773
|
+
return failedReport3(inputPath, `Could not parse ${feedbackPath}: ${error instanceof Error ? error.message : String(error)}`, "DYNAMICS.PARSE_ERROR");
|
|
34774
|
+
}
|
|
34775
|
+
const report = parsed;
|
|
34776
|
+
if (!report || report.schema !== FEEDBACK_SCHEMA || !Array.isArray(report.metrics)) {
|
|
34777
|
+
return failedReport3(
|
|
34778
|
+
inputPath,
|
|
34779
|
+
`${feedbackPath} is not a ${FEEDBACK_SCHEMA} report.`,
|
|
34780
|
+
"DYNAMICS.BAD_SCHEMA",
|
|
34781
|
+
"Regenerate it with the exported run_pinocchio.py."
|
|
34782
|
+
);
|
|
34783
|
+
}
|
|
34784
|
+
return { ...report, source: inputPath };
|
|
34785
|
+
}
|
|
34786
|
+
async function runSimDynamicsCli(argv = process.argv.slice(2)) {
|
|
34787
|
+
const options = parseArgs21(argv);
|
|
34788
|
+
const reports = options.inputPaths.map((inputPath) => ingest(inputPath));
|
|
34789
|
+
printFeedback(reports.length === 1 ? reports[0] : { runs: reports }, options.json, options.compact);
|
|
34790
|
+
if (reports.some((report) => !report.ok) && !options.noFailExit) process.exitCode = 1;
|
|
34791
|
+
}
|
|
34792
|
+
|
|
34793
|
+
// cli/sim-mass.ts
|
|
34794
|
+
var DEFAULT_DENSITY_KG_M35 = 1e3;
|
|
34795
|
+
function parseArgs22(argv) {
|
|
34796
|
+
const inputPaths = [];
|
|
34797
|
+
const paramOverrides = {};
|
|
34798
|
+
const jointOverrides = {};
|
|
34799
|
+
let json = false;
|
|
34800
|
+
let compact = false;
|
|
34801
|
+
let densityKgM3 = null;
|
|
34802
|
+
let backend;
|
|
34803
|
+
let quality;
|
|
34804
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
34805
|
+
const arg = argv[i];
|
|
34806
|
+
if (arg === "--json") {
|
|
34807
|
+
json = true;
|
|
34808
|
+
continue;
|
|
34809
|
+
}
|
|
34810
|
+
if (arg === "--compact") {
|
|
34811
|
+
compact = true;
|
|
34812
|
+
continue;
|
|
34813
|
+
}
|
|
34814
|
+
if (arg === "--density") {
|
|
34815
|
+
const value = Number(argv[i + 1]);
|
|
34816
|
+
if (!Number.isFinite(value) || value <= 0) throw new Error("--density requires a positive number (kg/m\xB3)");
|
|
34817
|
+
densityKgM3 = value;
|
|
34818
|
+
i += 1;
|
|
34819
|
+
continue;
|
|
34820
|
+
}
|
|
34821
|
+
if (arg === "--quality" || arg === "-q") {
|
|
34822
|
+
const val = argv[i + 1];
|
|
34823
|
+
if (val !== "default" && val !== "live" && val !== "high") throw new Error("--quality must be default, live, or high");
|
|
34824
|
+
quality = val;
|
|
34825
|
+
i += 1;
|
|
34826
|
+
continue;
|
|
34827
|
+
}
|
|
34828
|
+
if (arg === "--backend") {
|
|
34829
|
+
const val = argv[i + 1];
|
|
34830
|
+
if (val !== "manifold" && val !== "occt" && val !== "truck" && val !== "sdf") {
|
|
34831
|
+
throw new Error("--backend must be manifold, occt, truck, or sdf");
|
|
34832
|
+
}
|
|
34833
|
+
backend = val;
|
|
34834
|
+
i += 1;
|
|
34835
|
+
continue;
|
|
34836
|
+
}
|
|
34837
|
+
if (arg === "--joint") {
|
|
34838
|
+
const value = argv[i + 1];
|
|
34839
|
+
if (!value || value.startsWith("--")) throw new Error("--joint requires JointName=Value");
|
|
34840
|
+
addCliJointOverride(value, jointOverrides);
|
|
34841
|
+
i += 1;
|
|
34842
|
+
continue;
|
|
34843
|
+
}
|
|
34844
|
+
if (arg === "--param" || arg === "-p") {
|
|
34845
|
+
const value = argv[i + 1];
|
|
34846
|
+
if (!value || value.startsWith("--")) throw new Error(`${arg} requires Key=Value`);
|
|
34847
|
+
addCliParamOverride(value, paramOverrides);
|
|
34848
|
+
i += 1;
|
|
34849
|
+
continue;
|
|
34850
|
+
}
|
|
34851
|
+
if (arg.startsWith("--")) throw new Error(`Unknown flag: ${arg}`);
|
|
34852
|
+
inputPaths.push(arg);
|
|
34853
|
+
}
|
|
34854
|
+
requireInputPaths(
|
|
34855
|
+
inputPaths,
|
|
34856
|
+
"Usage: forgecad sim mass <model.forge.js> [input ...] [--density kg/m3] [--json] [--param Key=Value] [--joint JointName=Value]"
|
|
34857
|
+
);
|
|
34858
|
+
requireScriptInputPaths(inputPaths);
|
|
34859
|
+
return { inputPaths, json, compact, densityKgM3, backend, quality, paramOverrides, jointOverrides };
|
|
34860
|
+
}
|
|
34861
|
+
function failedReport4(source, message, code = "MASS.ERROR") {
|
|
34862
|
+
return {
|
|
34863
|
+
schema: FEEDBACK_SCHEMA,
|
|
34864
|
+
category: "mass-properties",
|
|
34865
|
+
source,
|
|
34866
|
+
ok: false,
|
|
34867
|
+
summary: "mass-properties analysis failed",
|
|
34868
|
+
metrics: [],
|
|
34869
|
+
findings: [{ level: "error", code, message }],
|
|
34870
|
+
trust: { assumptions: [] },
|
|
34871
|
+
data: {},
|
|
34872
|
+
timeMs: 0
|
|
34873
|
+
};
|
|
34874
|
+
}
|
|
34875
|
+
function round4(value, digits = 6) {
|
|
34876
|
+
const factor = 10 ** digits;
|
|
34877
|
+
return Math.round(value * factor) / factor;
|
|
34878
|
+
}
|
|
34879
|
+
function analyze2(source, options, objects) {
|
|
34880
|
+
const start = Date.now();
|
|
34881
|
+
const density = options.densityKgM3 ?? DEFAULT_DENSITY_KG_M35;
|
|
34882
|
+
const props = analyzeMassProperties(objects.map((o) => ({ name: o.name, mesh: o.mesh, densityKgM3: density })));
|
|
34883
|
+
const findings = [];
|
|
34884
|
+
const assumptions = [
|
|
34885
|
+
"Mass properties integrated from the triangulated mesh (exact for the mesh; curved features carry tessellation error).",
|
|
34886
|
+
"Manifold kernel meshes are watertight by construction."
|
|
34887
|
+
];
|
|
34888
|
+
if (options.densityKgM3 === null) {
|
|
34889
|
+
findings.push({
|
|
34890
|
+
level: "warning",
|
|
34891
|
+
code: "MASS.DENSITY.DEFAULTED",
|
|
34892
|
+
message: `No density given; using default ${DEFAULT_DENSITY_KG_M35} kg/m\xB3 (water). Volume, center of mass, and principal axes are exact regardless.`,
|
|
34893
|
+
suggest: "Pass --density <kg/m3> (e.g. 2700 aluminium, 7850 steel, 1240 ABS) for accurate mass and inertia magnitude."
|
|
34894
|
+
});
|
|
34895
|
+
assumptions.push(`uniform density ${DEFAULT_DENSITY_KG_M35} kg/m\xB3 (default)`);
|
|
34896
|
+
} else {
|
|
34897
|
+
assumptions.push(`uniform density ${density} kg/m\xB3`);
|
|
34898
|
+
}
|
|
34899
|
+
const metrics = [
|
|
34900
|
+
makeMetric("totalMassKg", "Total mass", round4(props.totalMassKg, 6), { unit: "kg" }),
|
|
34901
|
+
makeMetric("totalVolumeCm3", "Volume", round4(props.totalVolumeMm3 / 1e3, 4), { unit: "cm\xB3" }),
|
|
34902
|
+
makeMetric("surfaceAreaCm2", "Surface area", round4(props.surfaceAreaMm2 / 100, 4), { unit: "cm\xB2" }),
|
|
34903
|
+
makeMetric("comXmm", "Center of mass X", round4(props.centerOfMassMm[0], 4), { unit: "mm" }),
|
|
34904
|
+
makeMetric("comYmm", "Center of mass Y", round4(props.centerOfMassMm[1], 4), { unit: "mm" }),
|
|
34905
|
+
makeMetric("comZmm", "Center of mass Z", round4(props.centerOfMassMm[2], 4), { unit: "mm" }),
|
|
34906
|
+
makeMetric("principalI1", "Principal moment I1", round4(props.principalMomentsKgM2[0], 9), { unit: "kg\xB7m\xB2" }),
|
|
34907
|
+
makeMetric("principalI2", "Principal moment I2", round4(props.principalMomentsKgM2[1], 9), { unit: "kg\xB7m\xB2" }),
|
|
34908
|
+
makeMetric("principalI3", "Principal moment I3", round4(props.principalMomentsKgM2[2], 9), { unit: "kg\xB7m\xB2" })
|
|
34909
|
+
];
|
|
34910
|
+
return {
|
|
34911
|
+
schema: FEEDBACK_SCHEMA,
|
|
34912
|
+
category: "mass-properties",
|
|
34913
|
+
source,
|
|
34914
|
+
ok: reportOk(metrics, findings),
|
|
34915
|
+
summary: `mass ${round4(props.totalMassKg, 4)} kg \xB7 volume ${round4(props.totalVolumeMm3 / 1e3, 2)} cm\xB3 \xB7 CoM (${round4(
|
|
34916
|
+
props.centerOfMassMm[0],
|
|
34917
|
+
2
|
|
34918
|
+
)}, ${round4(props.centerOfMassMm[1], 2)}, ${round4(props.centerOfMassMm[2], 2)}) mm`,
|
|
34919
|
+
metrics,
|
|
34920
|
+
findings,
|
|
34921
|
+
trust: { assumptions, watertight: true },
|
|
34922
|
+
data: {
|
|
34923
|
+
densityKgM3: density,
|
|
34924
|
+
totalVolumeMm3: round4(props.totalVolumeMm3, 4),
|
|
34925
|
+
totalMassKg: round4(props.totalMassKg, 6),
|
|
34926
|
+
centerOfMassMm: props.centerOfMassMm.map((v) => round4(v, 4)),
|
|
34927
|
+
inertiaTensorKgM2: {
|
|
34928
|
+
ixx: round4(props.inertiaTensorKgM2.ixx, 9),
|
|
34929
|
+
iyy: round4(props.inertiaTensorKgM2.iyy, 9),
|
|
34930
|
+
izz: round4(props.inertiaTensorKgM2.izz, 9),
|
|
34931
|
+
ixy: round4(props.inertiaTensorKgM2.ixy, 9),
|
|
34932
|
+
ixz: round4(props.inertiaTensorKgM2.ixz, 9),
|
|
34933
|
+
iyz: round4(props.inertiaTensorKgM2.iyz, 9)
|
|
34934
|
+
},
|
|
34935
|
+
principalMomentsKgM2: props.principalMomentsKgM2.map((v) => round4(v, 9)),
|
|
34936
|
+
principalAxes: props.principalAxes.map((axis) => axis.map((v) => round4(v, 6))),
|
|
34937
|
+
radiusOfGyrationM: props.radiusOfGyrationM.map((v) => round4(v, 6)),
|
|
34938
|
+
boundingBoxMm: {
|
|
34939
|
+
minMm: props.boundingBoxMm.minMm.map((v) => round4(v, 4)),
|
|
34940
|
+
maxMm: props.boundingBoxMm.maxMm.map((v) => round4(v, 4)),
|
|
34941
|
+
sizeMm: props.boundingBoxMm.sizeMm.map((v) => round4(v, 4))
|
|
34942
|
+
},
|
|
34943
|
+
perObject: props.perObject.map((o) => ({
|
|
34944
|
+
name: o.name,
|
|
34945
|
+
volumeMm3: round4(o.volumeMm3, 4),
|
|
34946
|
+
massKg: round4(o.massKg, 6),
|
|
34947
|
+
centerOfMassMm: o.centerOfMassMm.map((v) => round4(v, 4))
|
|
34948
|
+
}))
|
|
34949
|
+
},
|
|
34950
|
+
timeMs: Date.now() - start
|
|
34951
|
+
};
|
|
34952
|
+
}
|
|
34953
|
+
async function runSimMassCli(argv = process.argv.slice(2)) {
|
|
34954
|
+
const options = parseArgs22(argv);
|
|
34955
|
+
requireExistingInputPaths(options.inputPaths);
|
|
34956
|
+
await init();
|
|
34957
|
+
const reports = [];
|
|
34958
|
+
for (const [index, scriptPath] of options.inputPaths.entries()) {
|
|
34959
|
+
if (!options.json) printBatchHeader(scriptPath, index, options.inputPaths.length);
|
|
34960
|
+
let report;
|
|
34961
|
+
try {
|
|
34962
|
+
const { objects, error } = await loadAnalysisObjects(scriptPath, {
|
|
34963
|
+
backend: options.backend,
|
|
34964
|
+
quality: options.quality,
|
|
34965
|
+
paramOverrides: options.paramOverrides,
|
|
34966
|
+
jointOverrides: options.jointOverrides
|
|
34967
|
+
});
|
|
34968
|
+
report = error ? failedReport4(scriptPath, error, "MASS.MODEL.EMPTY") : analyze2(scriptPath, options, objects);
|
|
34969
|
+
} catch (error) {
|
|
34970
|
+
report = failedReport4(scriptPath, error instanceof Error ? error.message : String(error));
|
|
34971
|
+
}
|
|
34972
|
+
reports.push(report);
|
|
34973
|
+
}
|
|
34974
|
+
printFeedback(reports.length === 1 ? reports[0] : { runs: reports }, options.json, options.compact);
|
|
34975
|
+
if (reports.some((report) => !report.ok)) process.exitCode = 1;
|
|
34976
|
+
}
|
|
34977
|
+
|
|
34978
|
+
// src/forge/analysis/mechanism.ts
|
|
34979
|
+
var GRAVITY_MS2 = 9.80665;
|
|
34980
|
+
var DEFAULT_GRAVITY = [0, 0, -GRAVITY_MS2];
|
|
34981
|
+
function cross7(a, b) {
|
|
34982
|
+
return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
|
|
34983
|
+
}
|
|
34984
|
+
function dot5(a, b) {
|
|
34985
|
+
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
34986
|
+
}
|
|
34987
|
+
function normalize4(v) {
|
|
34988
|
+
const len = Math.hypot(v[0], v[1], v[2]);
|
|
34989
|
+
if (len < 1e-12) throw new Error("mechanism joint axis has zero length");
|
|
34990
|
+
return [v[0] / len, v[1] / len, v[2] / len];
|
|
34991
|
+
}
|
|
34992
|
+
function freedoms(type) {
|
|
34993
|
+
return type === "fixed" ? 0 : 1;
|
|
34994
|
+
}
|
|
34995
|
+
function distalLinks(jointChild, childrenOf) {
|
|
34996
|
+
const names = /* @__PURE__ */ new Set();
|
|
34997
|
+
let cycle = false;
|
|
34998
|
+
const stack = [jointChild];
|
|
34999
|
+
while (stack.length > 0) {
|
|
35000
|
+
const link = stack.pop();
|
|
35001
|
+
if (names.has(link)) {
|
|
35002
|
+
cycle = true;
|
|
35003
|
+
continue;
|
|
35004
|
+
}
|
|
35005
|
+
names.add(link);
|
|
35006
|
+
for (const j of childrenOf.get(link) ?? []) stack.push(j.child);
|
|
35007
|
+
}
|
|
35008
|
+
return { names, cycle };
|
|
35009
|
+
}
|
|
35010
|
+
function analyzeMechanism(input) {
|
|
35011
|
+
const { links, joints } = input;
|
|
35012
|
+
if (links.length === 0) throw new Error("mechanism analysis needs at least one link");
|
|
35013
|
+
const gravity = input.gravity ?? DEFAULT_GRAVITY;
|
|
35014
|
+
const couplings = input.couplings ?? 0;
|
|
35015
|
+
const linkByName = new Map(links.map((l) => [l.name, l]));
|
|
35016
|
+
for (const j of joints) {
|
|
35017
|
+
if (!linkByName.has(j.parent)) throw new Error(`joint "${j.name}" references unknown parent link "${j.parent}"`);
|
|
35018
|
+
if (!linkByName.has(j.child)) throw new Error(`joint "${j.name}" references unknown child link "${j.child}"`);
|
|
35019
|
+
}
|
|
35020
|
+
const childrenOf = /* @__PURE__ */ new Map();
|
|
35021
|
+
for (const j of joints) {
|
|
35022
|
+
if (!childrenOf.has(j.parent)) childrenOf.set(j.parent, []);
|
|
35023
|
+
childrenOf.get(j.parent).push(j);
|
|
35024
|
+
}
|
|
35025
|
+
const freedomSum = joints.reduce((sum2, j) => sum2 + freedoms(j.type), 0);
|
|
35026
|
+
const actuatedJointCount = joints.filter((j) => j.type !== "fixed").length;
|
|
35027
|
+
const fixedJointCount = joints.length - actuatedJointCount;
|
|
35028
|
+
const hasClosedLoops = joints.length > links.length - 1;
|
|
35029
|
+
const actuatedDof = freedomSum - couplings;
|
|
35030
|
+
const grueblerSpatial = 6 * (links.length - 1 - joints.length) + freedomSum - couplings;
|
|
35031
|
+
const totalMassKg = links.reduce((sum2, l) => sum2 + l.massKg, 0);
|
|
35032
|
+
const jointLoads = [];
|
|
35033
|
+
for (const joint2 of joints) {
|
|
35034
|
+
if (joint2.type === "fixed") continue;
|
|
35035
|
+
const { names } = distalLinks(joint2.child, childrenOf);
|
|
35036
|
+
let mass = 0;
|
|
35037
|
+
const weightedCom = [0, 0, 0];
|
|
35038
|
+
for (const name of names) {
|
|
35039
|
+
const link = linkByName.get(name);
|
|
35040
|
+
mass += link.massKg;
|
|
35041
|
+
weightedCom[0] += link.massKg * link.comWorldMm[0];
|
|
35042
|
+
weightedCom[1] += link.massKg * link.comWorldMm[1];
|
|
35043
|
+
weightedCom[2] += link.massKg * link.comWorldMm[2];
|
|
35044
|
+
}
|
|
35045
|
+
const com = mass > 0 ? [weightedCom[0] / mass, weightedCom[1] / mass, weightedCom[2] / mass] : [...joint2.pointWorldMm];
|
|
35046
|
+
const axis = normalize4(joint2.axisWorld);
|
|
35047
|
+
const force = [mass * gravity[0], mass * gravity[1], mass * gravity[2]];
|
|
35048
|
+
const load = { joint: joint2.name, type: joint2.type, distalMassKg: mass, distalComWorldMm: com, marginPct: null };
|
|
35049
|
+
if (joint2.type === "revolute") {
|
|
35050
|
+
const r = [(com[0] - joint2.pointWorldMm[0]) / 1e3, (com[1] - joint2.pointWorldMm[1]) / 1e3, (com[2] - joint2.pointWorldMm[2]) / 1e3];
|
|
35051
|
+
load.holdTorqueNm = Math.abs(dot5(axis, cross7(r, force)));
|
|
35052
|
+
if (joint2.effort !== void 0 && joint2.effort > 0) {
|
|
35053
|
+
load.budget = joint2.effort;
|
|
35054
|
+
load.marginPct = (joint2.effort - load.holdTorqueNm) / joint2.effort * 100;
|
|
35055
|
+
}
|
|
35056
|
+
} else {
|
|
35057
|
+
load.holdForceN = Math.abs(dot5(force, axis));
|
|
35058
|
+
if (joint2.effort !== void 0 && joint2.effort > 0) {
|
|
35059
|
+
load.budget = joint2.effort;
|
|
35060
|
+
load.marginPct = (joint2.effort - load.holdForceN) / joint2.effort * 100;
|
|
35061
|
+
}
|
|
35062
|
+
}
|
|
35063
|
+
jointLoads.push(load);
|
|
35064
|
+
}
|
|
35065
|
+
return {
|
|
35066
|
+
linkCount: links.length,
|
|
35067
|
+
jointCount: joints.length,
|
|
35068
|
+
actuatedJointCount,
|
|
35069
|
+
fixedJointCount,
|
|
35070
|
+
couplings,
|
|
35071
|
+
hasClosedLoops,
|
|
35072
|
+
actuatedDof,
|
|
35073
|
+
grueblerSpatial,
|
|
35074
|
+
gravity,
|
|
35075
|
+
totalMassKg,
|
|
35076
|
+
jointLoads
|
|
35077
|
+
};
|
|
35078
|
+
}
|
|
35079
|
+
|
|
35080
|
+
// cli/sim-mechanism.ts
|
|
35081
|
+
var DEFAULT_DENSITY_KG_M36 = 1e3;
|
|
35082
|
+
function parseArgs23(argv) {
|
|
35083
|
+
const inputPaths = [];
|
|
35084
|
+
const paramOverrides = {};
|
|
35085
|
+
const jointOverrides = {};
|
|
35086
|
+
let json = false;
|
|
35087
|
+
let compact = false;
|
|
35088
|
+
let minTorqueMarginPct = 0;
|
|
35089
|
+
let densityKgM3 = DEFAULT_DENSITY_KG_M36;
|
|
35090
|
+
let noFailExit = false;
|
|
35091
|
+
let backend;
|
|
35092
|
+
let quality;
|
|
35093
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
35094
|
+
const arg = argv[i];
|
|
35095
|
+
if (arg === "--json") {
|
|
35096
|
+
json = true;
|
|
35097
|
+
continue;
|
|
35098
|
+
}
|
|
35099
|
+
if (arg === "--compact") {
|
|
35100
|
+
compact = true;
|
|
35101
|
+
continue;
|
|
35102
|
+
}
|
|
35103
|
+
if (arg === "--no-fail-exit") {
|
|
35104
|
+
noFailExit = true;
|
|
35105
|
+
continue;
|
|
35106
|
+
}
|
|
35107
|
+
if (arg === "--min-torque-margin-pct") {
|
|
35108
|
+
const n = Number(argv[++i]);
|
|
35109
|
+
if (!Number.isFinite(n)) throw new Error("--min-torque-margin-pct requires a number");
|
|
35110
|
+
minTorqueMarginPct = n;
|
|
35111
|
+
continue;
|
|
35112
|
+
}
|
|
35113
|
+
if (arg === "--density") {
|
|
35114
|
+
const n = Number(argv[++i]);
|
|
35115
|
+
if (!Number.isFinite(n) || n <= 0) throw new Error("--density requires a positive number (kg/m\xB3)");
|
|
35116
|
+
densityKgM3 = n;
|
|
35117
|
+
continue;
|
|
35118
|
+
}
|
|
35119
|
+
if (arg === "--quality" || arg === "-q") {
|
|
35120
|
+
const v = argv[i + 1];
|
|
35121
|
+
if (v !== "default" && v !== "live" && v !== "high") throw new Error("--quality must be default, live, or high");
|
|
35122
|
+
quality = v;
|
|
35123
|
+
i += 1;
|
|
35124
|
+
continue;
|
|
35125
|
+
}
|
|
35126
|
+
if (arg === "--backend") {
|
|
35127
|
+
const v = argv[i + 1];
|
|
35128
|
+
if (v !== "manifold" && v !== "occt" && v !== "truck" && v !== "sdf") throw new Error("--backend must be manifold, occt, truck, or sdf");
|
|
35129
|
+
backend = v;
|
|
35130
|
+
i += 1;
|
|
35131
|
+
continue;
|
|
35132
|
+
}
|
|
35133
|
+
if (arg === "--joint") {
|
|
35134
|
+
const value = argv[i + 1];
|
|
35135
|
+
if (!value || value.startsWith("--")) throw new Error("--joint requires JointName=Value");
|
|
35136
|
+
addCliJointOverride(value, jointOverrides);
|
|
35137
|
+
i += 1;
|
|
35138
|
+
continue;
|
|
35139
|
+
}
|
|
35140
|
+
if (arg === "--param" || arg === "-p") {
|
|
35141
|
+
const value = argv[i + 1];
|
|
35142
|
+
if (!value || value.startsWith("--")) throw new Error(`${arg} requires Key=Value`);
|
|
35143
|
+
addCliParamOverride(value, paramOverrides);
|
|
35144
|
+
i += 1;
|
|
35145
|
+
continue;
|
|
35146
|
+
}
|
|
35147
|
+
if (arg.startsWith("--")) throw new Error(`Unknown flag: ${arg}`);
|
|
35148
|
+
inputPaths.push(arg);
|
|
35149
|
+
}
|
|
35150
|
+
requireInputPaths(inputPaths, "Usage: forgecad sim mechanism <model.forge.js> [--min-torque-margin-pct N] [--json]");
|
|
35151
|
+
requireScriptInputPaths(inputPaths);
|
|
35152
|
+
return { inputPaths, json, compact, minTorqueMarginPct, densityKgM3, noFailExit, backend, quality, paramOverrides, jointOverrides };
|
|
35153
|
+
}
|
|
35154
|
+
function failedReport5(source, message, code = "MECHANISM.ERROR", suggest) {
|
|
35155
|
+
return {
|
|
35156
|
+
schema: FEEDBACK_SCHEMA,
|
|
35157
|
+
category: "mechanism",
|
|
35158
|
+
source,
|
|
35159
|
+
ok: false,
|
|
35160
|
+
summary: "mechanism analysis failed",
|
|
35161
|
+
metrics: [],
|
|
35162
|
+
findings: [{ level: "error", code, message, ...suggest ? { suggest } : {} }],
|
|
35163
|
+
trust: { assumptions: [] },
|
|
35164
|
+
data: {},
|
|
35165
|
+
timeMs: 0
|
|
35166
|
+
};
|
|
35167
|
+
}
|
|
35168
|
+
function round5(value, digits = 4) {
|
|
35169
|
+
const factor = 10 ** digits;
|
|
35170
|
+
return Math.round(value * factor) / factor;
|
|
35171
|
+
}
|
|
35172
|
+
function flattenShapes(part, out) {
|
|
35173
|
+
if (part instanceof Shape) out.push(part);
|
|
35174
|
+
else if (part instanceof ShapeGroup) part.children.forEach((child) => flattenShapes(child, out));
|
|
35175
|
+
}
|
|
35176
|
+
async function runSimMechanismCli(argv = process.argv.slice(2)) {
|
|
35177
|
+
const options = parseArgs23(argv);
|
|
35178
|
+
requireExistingInputPaths(options.inputPaths);
|
|
35179
|
+
await init();
|
|
35180
|
+
const reports = [];
|
|
35181
|
+
for (const [index, scriptPath] of options.inputPaths.entries()) {
|
|
35182
|
+
if (!options.json) printBatchHeader(scriptPath, index, options.inputPaths.length);
|
|
35183
|
+
let report;
|
|
35184
|
+
try {
|
|
35185
|
+
report = analyze3(scriptPath, options, await loadAnalysisObjectsWithAssembly(scriptPath, options));
|
|
35186
|
+
} catch (error) {
|
|
35187
|
+
report = failedReport5(scriptPath, error instanceof Error ? error.message : String(error));
|
|
35188
|
+
}
|
|
35189
|
+
reports.push(report);
|
|
35190
|
+
}
|
|
35191
|
+
printFeedback(reports.length === 1 ? reports[0] : { runs: reports }, options.json, options.compact);
|
|
35192
|
+
if (reports.some((report) => !report.ok) && !options.noFailExit) process.exitCode = 1;
|
|
35193
|
+
}
|
|
35194
|
+
function analyze3(source, options, loaded) {
|
|
35195
|
+
const start = Date.now();
|
|
35196
|
+
if (loaded.error) return failedReport5(source, loaded.error, "MECHANISM.MODEL.EMPTY");
|
|
35197
|
+
if (!loaded.assembly || !loaded.runResult) {
|
|
35198
|
+
return failedReport5(
|
|
35199
|
+
source,
|
|
35200
|
+
"Model must return an assembly(...) with joints.",
|
|
35201
|
+
"MECHANISM.NO_ASSEMBLY",
|
|
35202
|
+
'Build the mechanism with assembly(...).addPart(...).connect(..., { type: "revolute" }).'
|
|
35203
|
+
);
|
|
35204
|
+
}
|
|
35205
|
+
const assembly2 = loaded.assembly;
|
|
35206
|
+
const def = assembly2.describe();
|
|
35207
|
+
if (def.joints.length === 0) {
|
|
35208
|
+
return failedReport5(source, "Assembly has no joints to analyze.", "MECHANISM.NO_JOINTS");
|
|
35209
|
+
}
|
|
35210
|
+
const simModel = collectSimulationModel(def, { state: options.jointOverrides });
|
|
35211
|
+
const solved = assembly2.solve(options.jointOverrides);
|
|
35212
|
+
const warnings = [];
|
|
35213
|
+
const worldTransform = (partName) => {
|
|
35214
|
+
try {
|
|
35215
|
+
return solved.getTransform(partName);
|
|
35216
|
+
} catch {
|
|
35217
|
+
return null;
|
|
35218
|
+
}
|
|
35219
|
+
};
|
|
35220
|
+
const links = [];
|
|
35221
|
+
for (const part of def.parts) {
|
|
35222
|
+
const shapes = [];
|
|
35223
|
+
flattenShapes(part.part, shapes);
|
|
35224
|
+
let volumeMm3 = 0;
|
|
35225
|
+
let localCom = [0, 0, 0];
|
|
35226
|
+
if (shapes.length > 0) {
|
|
35227
|
+
const mp = analyzeMassProperties(shapes.map((shape, i) => ({ name: `${part.name}#${i}`, mesh: shape.getMesh(), densityKgM3: 1 })));
|
|
35228
|
+
volumeMm3 = mp.totalVolumeMm3;
|
|
35229
|
+
localCom = mp.centerOfMassMm;
|
|
35230
|
+
}
|
|
35231
|
+
const simLink = simModel.links[part.name];
|
|
35232
|
+
const massKg = simLink?.massKg ?? (simLink?.densityKgM3 ?? options.densityKgM3) * volumeMm3 * 1e-9;
|
|
35233
|
+
const transform = worldTransform(part.name);
|
|
35234
|
+
const comWorldMm = transform ? transform.point(localCom) : localCom;
|
|
35235
|
+
links.push({ name: part.name, massKg, comWorldMm });
|
|
35236
|
+
}
|
|
35237
|
+
const jointView = new Map((loaded.runResult.jointsView?.joints ?? []).map((j) => [j.name, j]));
|
|
35238
|
+
const joints = [];
|
|
35239
|
+
for (const dj of def.joints) {
|
|
35240
|
+
if (dj.type === "fixed") {
|
|
35241
|
+
joints.push({ name: dj.name, type: "fixed", parent: dj.parent, child: dj.child, axisWorld: [0, 0, 1], pointWorldMm: [0, 0, 0] });
|
|
35242
|
+
continue;
|
|
35243
|
+
}
|
|
35244
|
+
const jv = jointView.get(dj.name);
|
|
35245
|
+
const parentTransform = worldTransform(dj.parent);
|
|
35246
|
+
if (!jv || !parentTransform) {
|
|
35247
|
+
warnings.push({
|
|
35248
|
+
level: "warning",
|
|
35249
|
+
code: "MECHANISM.JOINT.UNRESOLVED",
|
|
35250
|
+
path: dj.name,
|
|
35251
|
+
message: `Could not resolve a world frame for joint "${dj.name}"; excluded from torque analysis.`
|
|
35252
|
+
});
|
|
35253
|
+
continue;
|
|
35254
|
+
}
|
|
35255
|
+
joints.push({
|
|
35256
|
+
name: dj.name,
|
|
35257
|
+
type: dj.type,
|
|
35258
|
+
parent: dj.parent,
|
|
35259
|
+
child: dj.child,
|
|
35260
|
+
axisWorld: parentTransform.vector(jv.axis),
|
|
35261
|
+
pointWorldMm: parentTransform.point(jv.pivot),
|
|
35262
|
+
effort: simModel.joints[dj.name]?.effort ?? dj.effort
|
|
35263
|
+
});
|
|
35264
|
+
}
|
|
35265
|
+
const result = analyzeMechanism({ links, joints, couplings: def.jointCouplings?.length ?? 0 });
|
|
35266
|
+
const findings = [...warnings];
|
|
35267
|
+
const metrics = [
|
|
35268
|
+
makeMetric("actuatedDof", "Actuated DOF", result.actuatedDof, { unit: "" }),
|
|
35269
|
+
makeMetric("totalMassKg", "Total mass", round5(result.totalMassKg, 6), { unit: "kg" })
|
|
35270
|
+
];
|
|
35271
|
+
if (result.hasClosedLoops) {
|
|
35272
|
+
findings.push({
|
|
35273
|
+
level: "info",
|
|
35274
|
+
code: "MECHANISM.CLOSED_LOOP",
|
|
35275
|
+
message: `Graph has closed loops; actuatedDof counts joint freedoms and the spatial Gr\xFCbler mobility is ${result.grueblerSpatial}.`
|
|
35276
|
+
});
|
|
35277
|
+
}
|
|
35278
|
+
for (const load of result.jointLoads) {
|
|
35279
|
+
if (load.type === "revolute") {
|
|
35280
|
+
metrics.push(makeMetric(`holdTorqueNm.${load.joint}`, `Hold torque (${load.joint})`, round5(load.holdTorqueNm ?? 0, 4), { unit: "N\xB7m" }));
|
|
35281
|
+
} else {
|
|
35282
|
+
metrics.push(makeMetric(`holdForceN.${load.joint}`, `Hold force (${load.joint})`, round5(load.holdForceN ?? 0, 4), { unit: "N" }));
|
|
35283
|
+
}
|
|
35284
|
+
if (load.marginPct !== null) {
|
|
35285
|
+
const metric = makeMetric(`torqueMarginPct.${load.joint}`, `Budget margin (${load.joint})`, round5(load.marginPct, 2), {
|
|
35286
|
+
unit: "%",
|
|
35287
|
+
target: { op: ">=", value: options.minTorqueMarginPct }
|
|
35288
|
+
});
|
|
35289
|
+
metrics.push(metric);
|
|
35290
|
+
if (load.marginPct < options.minTorqueMarginPct) {
|
|
35291
|
+
const required = load.type === "revolute" ? `${round5(load.holdTorqueNm ?? 0, 3)} N\xB7m` : `${round5(load.holdForceN ?? 0, 3)} N`;
|
|
35292
|
+
findings.push({
|
|
35293
|
+
level: "error",
|
|
35294
|
+
code: load.marginPct < 0 ? "MECHANISM.HOLD.OVER_BUDGET" : "MECHANISM.HOLD.LOW_MARGIN",
|
|
35295
|
+
path: load.joint,
|
|
35296
|
+
message: `Joint "${load.joint}" needs ${required} to hold its pose; budget margin ${round5(load.marginPct, 1)}% is below ${options.minTorqueMarginPct}%.`,
|
|
35297
|
+
suggest: "Increase the Sim.drive effort, shorten the moment arm, or lighten the distal mass."
|
|
35298
|
+
});
|
|
35299
|
+
}
|
|
35300
|
+
}
|
|
35301
|
+
}
|
|
35302
|
+
const assumptions = [
|
|
35303
|
+
"Static gravity hold at the current pose; no dynamics, friction, or contact.",
|
|
35304
|
+
"Per-link mass from Sim.body (massKg/density) when set, else uniform default density.",
|
|
35305
|
+
"Joint world axes/pivots taken from the solved assembly pose."
|
|
35306
|
+
];
|
|
35307
|
+
return {
|
|
35308
|
+
schema: FEEDBACK_SCHEMA,
|
|
35309
|
+
category: "mechanism",
|
|
35310
|
+
source,
|
|
35311
|
+
ok: reportOk(metrics, findings),
|
|
35312
|
+
summary: `${result.actuatedDof} DOF \xB7 ${result.jointLoads.length} actuated joint(s) \xB7 total mass ${round5(result.totalMassKg, 3)} kg`,
|
|
35313
|
+
metrics,
|
|
35314
|
+
findings,
|
|
35315
|
+
trust: { assumptions, watertight: true },
|
|
35316
|
+
data: {
|
|
35317
|
+
linkCount: result.linkCount,
|
|
35318
|
+
jointCount: result.jointCount,
|
|
35319
|
+
actuatedJointCount: result.actuatedJointCount,
|
|
35320
|
+
fixedJointCount: result.fixedJointCount,
|
|
35321
|
+
couplings: result.couplings,
|
|
35322
|
+
hasClosedLoops: result.hasClosedLoops,
|
|
35323
|
+
actuatedDof: result.actuatedDof,
|
|
35324
|
+
grueblerSpatial: result.grueblerSpatial,
|
|
35325
|
+
gravity: result.gravity,
|
|
35326
|
+
totalMassKg: round5(result.totalMassKg, 6),
|
|
35327
|
+
jointLoads: result.jointLoads.map((l) => ({
|
|
35328
|
+
joint: l.joint,
|
|
35329
|
+
type: l.type,
|
|
35330
|
+
distalMassKg: round5(l.distalMassKg, 6),
|
|
35331
|
+
distalComWorldMm: l.distalComWorldMm.map((v) => round5(v, 4)),
|
|
35332
|
+
holdTorqueNm: l.holdTorqueNm === void 0 ? void 0 : round5(l.holdTorqueNm, 4),
|
|
35333
|
+
holdForceN: l.holdForceN === void 0 ? void 0 : round5(l.holdForceN, 4),
|
|
35334
|
+
budget: l.budget === void 0 ? void 0 : round5(l.budget, 4),
|
|
35335
|
+
marginPct: l.marginPct === null ? null : round5(l.marginPct, 2)
|
|
35336
|
+
}))
|
|
35337
|
+
},
|
|
35338
|
+
timeMs: Date.now() - start
|
|
35339
|
+
};
|
|
35340
|
+
}
|
|
35341
|
+
|
|
35342
|
+
// src/forge/analysis/statistics.ts
|
|
35343
|
+
function createRng2(seed) {
|
|
35344
|
+
let state = seed >>> 0;
|
|
35345
|
+
return () => {
|
|
35346
|
+
state = state + 1831565813 >>> 0;
|
|
35347
|
+
let t = state;
|
|
35348
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
35349
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
35350
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
35351
|
+
};
|
|
35352
|
+
}
|
|
35353
|
+
function sampleNormal(rng, mean2, sigma) {
|
|
35354
|
+
const u1 = rng() || Number.MIN_VALUE;
|
|
35355
|
+
const u2 = rng();
|
|
35356
|
+
const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
|
|
35357
|
+
return mean2 + sigma * z;
|
|
35358
|
+
}
|
|
35359
|
+
function sampleUniform(rng, lo, hi) {
|
|
35360
|
+
return lo + (hi - lo) * rng();
|
|
35361
|
+
}
|
|
35362
|
+
function mean(values) {
|
|
35363
|
+
if (values.length === 0) return 0;
|
|
35364
|
+
return values.reduce((sum2, v) => sum2 + v, 0) / values.length;
|
|
35365
|
+
}
|
|
35366
|
+
function stdDev(values) {
|
|
35367
|
+
if (values.length < 2) return 0;
|
|
35368
|
+
const m = mean(values);
|
|
35369
|
+
const ss = values.reduce((sum2, v) => sum2 + (v - m) * (v - m), 0);
|
|
35370
|
+
return Math.sqrt(ss / (values.length - 1));
|
|
35371
|
+
}
|
|
35372
|
+
function capabilityIndices(meanValue, sigma, lsl, usl) {
|
|
35373
|
+
if (!(sigma > 0)) return { cp: null, cpk: null };
|
|
35374
|
+
const cp = lsl !== null && usl !== null ? (usl - lsl) / (6 * sigma) : null;
|
|
35375
|
+
const upper = usl !== null ? (usl - meanValue) / (3 * sigma) : Infinity;
|
|
35376
|
+
const lower2 = lsl !== null ? (meanValue - lsl) / (3 * sigma) : Infinity;
|
|
35377
|
+
const cpk = Math.min(upper, lower2);
|
|
35378
|
+
return { cp, cpk: Number.isFinite(cpk) ? cpk : null };
|
|
35379
|
+
}
|
|
35380
|
+
function dpmoFromYield(yieldFraction) {
|
|
35381
|
+
return Math.max(0, (1 - yieldFraction) * 1e6);
|
|
35382
|
+
}
|
|
35383
|
+
|
|
35384
|
+
// src/forge/analysis/tolerance.ts
|
|
35385
|
+
var ToleranceNonlinearError = class extends Error {
|
|
35386
|
+
constructor(response, input, ratio, tolerance) {
|
|
35387
|
+
super(
|
|
35388
|
+
`Response "${response}" is nonlinear in input "${input}" across its tolerance band (curvature ratio ${ratio.toFixed(3)} > ${tolerance}). The linearized method would report a wrong yield. Re-run with --method full-rebuild.`
|
|
35389
|
+
);
|
|
35390
|
+
this.response = response;
|
|
35391
|
+
this.input = input;
|
|
35392
|
+
this.ratio = ratio;
|
|
35393
|
+
this.tolerance = tolerance;
|
|
35394
|
+
this.name = "ToleranceNonlinearError";
|
|
35395
|
+
}
|
|
35396
|
+
};
|
|
35397
|
+
function readResponse(map, name) {
|
|
35398
|
+
const value = map[name];
|
|
35399
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
35400
|
+
throw new Error(`tolerance sample did not return a finite value for response "${name}"`);
|
|
35401
|
+
}
|
|
35402
|
+
return value;
|
|
35403
|
+
}
|
|
35404
|
+
function analyzeTolerance(sample, inputs, responses, options) {
|
|
35405
|
+
if (inputs.length === 0) throw new Error("tolerance analysis needs at least one varying input");
|
|
35406
|
+
if (responses.length === 0) throw new Error("tolerance analysis needs at least one measured response");
|
|
35407
|
+
const responseNames = responses.map((r) => r.name);
|
|
35408
|
+
const baseline = sample({});
|
|
35409
|
+
const nominalResp = {};
|
|
35410
|
+
for (const name of responseNames) nominalResp[name] = readResponse(baseline, name);
|
|
35411
|
+
const jac = {};
|
|
35412
|
+
for (const name of responseNames) jac[name] = {};
|
|
35413
|
+
for (const input of inputs) {
|
|
35414
|
+
const delta = input.halfWidth > 0 ? input.halfWidth : Math.max(1e-6, Math.abs(input.nominal) * 1e-3);
|
|
35415
|
+
const plus = sample({ [input.name]: input.nominal + delta });
|
|
35416
|
+
const minus = sample({ [input.name]: input.nominal - delta });
|
|
35417
|
+
for (const name of responseNames) {
|
|
35418
|
+
const fp = readResponse(plus, name);
|
|
35419
|
+
const fm = readResponse(minus, name);
|
|
35420
|
+
const f0 = nominalResp[name];
|
|
35421
|
+
jac[name][input.name] = (fp - fm) / (2 * delta);
|
|
35422
|
+
if (options.method === "linearized") {
|
|
35423
|
+
const curvature = Math.abs(fp - 2 * f0 + fm);
|
|
35424
|
+
const slopeSpan = Math.abs(fp - f0) + Math.abs(fm - f0);
|
|
35425
|
+
const ratio = slopeSpan > 1e-12 ? curvature / slopeSpan : curvature > 1e-9 ? Infinity : 0;
|
|
35426
|
+
if (ratio > options.nonlinearityTol) {
|
|
35427
|
+
throw new ToleranceNonlinearError(name, input.name, ratio, options.nonlinearityTol);
|
|
35428
|
+
}
|
|
35429
|
+
}
|
|
35430
|
+
}
|
|
35431
|
+
}
|
|
35432
|
+
const rng = createRng2(options.seed);
|
|
35433
|
+
const draws = {};
|
|
35434
|
+
for (const name of responseNames) draws[name] = [];
|
|
35435
|
+
let inSpecAll = 0;
|
|
35436
|
+
for (let s = 0; s < options.samples; s += 1) {
|
|
35437
|
+
const overrides = {};
|
|
35438
|
+
for (const input of inputs) {
|
|
35439
|
+
overrides[input.name] = input.dist === "uniform" ? sampleUniform(rng, input.nominal - input.halfWidth, input.nominal + input.halfWidth) : sampleNormal(rng, input.nominal, input.sigma);
|
|
35440
|
+
}
|
|
35441
|
+
let allIn = true;
|
|
35442
|
+
for (const response of responses) {
|
|
35443
|
+
let value;
|
|
35444
|
+
if (options.method === "full-rebuild") {
|
|
35445
|
+
value = readResponse(sample(overrides), response.name);
|
|
35446
|
+
} else {
|
|
35447
|
+
value = nominalResp[response.name];
|
|
35448
|
+
for (const input of inputs) value += jac[response.name][input.name] * (overrides[input.name] - input.nominal);
|
|
35449
|
+
}
|
|
35450
|
+
draws[response.name].push(value);
|
|
35451
|
+
if (response.lsl !== null && value < response.lsl) allIn = false;
|
|
35452
|
+
if (response.usl !== null && value > response.usl) allIn = false;
|
|
35453
|
+
}
|
|
35454
|
+
if (allIn) inSpecAll += 1;
|
|
35455
|
+
}
|
|
35456
|
+
const responseStats = responses.map((response) => reduceResponse(response, draws[response.name], nominalResp[response.name], inputs, jac[response.name], options));
|
|
35457
|
+
return {
|
|
35458
|
+
method: options.method,
|
|
35459
|
+
samples: options.samples,
|
|
35460
|
+
seed: options.seed,
|
|
35461
|
+
targetCpk: options.targetCpk,
|
|
35462
|
+
inputs,
|
|
35463
|
+
responses: responseStats,
|
|
35464
|
+
overallYieldPct: inSpecAll / options.samples * 100
|
|
35465
|
+
};
|
|
35466
|
+
}
|
|
35467
|
+
function reduceResponse(response, values, nominal, inputs, sensitivities, options) {
|
|
35468
|
+
const m = mean(values);
|
|
35469
|
+
const sigma = stdDev(values);
|
|
35470
|
+
const { cp, cpk } = capabilityIndices(m, sigma, response.lsl, response.usl);
|
|
35471
|
+
let inSpec = 0;
|
|
35472
|
+
for (const v of values) {
|
|
35473
|
+
if ((response.lsl === null || v >= response.lsl) && (response.usl === null || v <= response.usl)) inSpec += 1;
|
|
35474
|
+
}
|
|
35475
|
+
const yieldFraction = values.length > 0 ? inSpec / values.length : 1;
|
|
35476
|
+
let worstHalf = 0;
|
|
35477
|
+
let rssVariance = 0;
|
|
35478
|
+
const variancePerInput = inputs.map((input) => {
|
|
35479
|
+
const s = sensitivities[input.name];
|
|
35480
|
+
worstHalf += Math.abs(s) * input.halfWidth;
|
|
35481
|
+
rssVariance += s * input.halfWidth * (s * input.halfWidth);
|
|
35482
|
+
return { input: input.name, sensitivity: s, variance: s * input.sigma * (s * input.sigma) };
|
|
35483
|
+
});
|
|
35484
|
+
const totalVariance = variancePerInput.reduce((sum2, v) => sum2 + v.variance, 0);
|
|
35485
|
+
const contributions = variancePerInput.map((v) => ({ input: v.input, sensitivity: v.sensitivity, fraction: totalVariance > 0 ? v.variance / totalVariance : 0 })).sort((a, b) => b.fraction - a.fraction);
|
|
35486
|
+
const rssHalf = Math.sqrt(rssVariance);
|
|
35487
|
+
let allocation = null;
|
|
35488
|
+
if (cpk !== null && cpk < options.targetCpk && cpk > 0) {
|
|
35489
|
+
const scale2 = cpk / options.targetCpk;
|
|
35490
|
+
allocation = inputs.map((input) => ({
|
|
35491
|
+
input: input.name,
|
|
35492
|
+
currentHalfWidth: input.halfWidth,
|
|
35493
|
+
recommendedHalfWidth: input.halfWidth * scale2
|
|
35494
|
+
}));
|
|
35495
|
+
}
|
|
35496
|
+
return {
|
|
35497
|
+
name: response.name,
|
|
35498
|
+
nominal,
|
|
35499
|
+
mean: m,
|
|
35500
|
+
sigma,
|
|
35501
|
+
lsl: response.lsl,
|
|
35502
|
+
usl: response.usl,
|
|
35503
|
+
cp,
|
|
35504
|
+
cpk,
|
|
35505
|
+
pp: cp,
|
|
35506
|
+
ppk: cpk,
|
|
35507
|
+
yieldPct: yieldFraction * 100,
|
|
35508
|
+
dpmo: dpmoFromYield(yieldFraction),
|
|
35509
|
+
worstCaseMin: nominal - worstHalf,
|
|
35510
|
+
worstCaseMax: nominal + worstHalf,
|
|
35511
|
+
rssMin: nominal - rssHalf,
|
|
35512
|
+
rssMax: nominal + rssHalf,
|
|
35513
|
+
contributions,
|
|
35514
|
+
allocation
|
|
35515
|
+
};
|
|
35516
|
+
}
|
|
35517
|
+
|
|
35518
|
+
// cli/tolerance-inputs.ts
|
|
35519
|
+
function extractNumericActual(value) {
|
|
35520
|
+
if (value === void 0) return null;
|
|
35521
|
+
const match = value.match(/-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/);
|
|
35522
|
+
if (!match) return null;
|
|
35523
|
+
const n = Number(match[0]);
|
|
35524
|
+
return Number.isFinite(n) ? n : null;
|
|
35525
|
+
}
|
|
35526
|
+
function parseTolFlag(raw) {
|
|
35527
|
+
const eq = raw.indexOf("=");
|
|
35528
|
+
if (eq <= 0) throw new Error(`--tol expects Name=\xB1X or Name=lo..hi, got "${raw}"`);
|
|
35529
|
+
const name = raw.slice(0, eq).trim();
|
|
35530
|
+
const spec = raw.slice(eq + 1).trim();
|
|
35531
|
+
const range = spec.match(/^(-?[\d.eE+-]+)\.\.(-?[\d.eE+-]+)$/);
|
|
35532
|
+
if (range) {
|
|
35533
|
+
const lo = Number(range[1]);
|
|
35534
|
+
const hi = Number(range[2]);
|
|
35535
|
+
if (!Number.isFinite(lo) || !Number.isFinite(hi) || hi <= lo) throw new Error(`--tol ${name}: invalid range "${spec}"`);
|
|
35536
|
+
return { name, band: { lo, hi } };
|
|
35537
|
+
}
|
|
35538
|
+
const half = Number(spec.replace(/^[±+]/, ""));
|
|
35539
|
+
if (!Number.isFinite(half) || half <= 0) throw new Error(`--tol ${name}: expected \xB1X or lo..hi, got "${spec}"`);
|
|
35540
|
+
return { name, band: { halfWidth: half } };
|
|
35541
|
+
}
|
|
35542
|
+
function parseSpecFlag(raw) {
|
|
35543
|
+
const eq = raw.indexOf("=");
|
|
35544
|
+
if (eq <= 0) throw new Error(`--spec expects Name=lsl..usl, got "${raw}"`);
|
|
35545
|
+
const name = raw.slice(0, eq).trim();
|
|
35546
|
+
const m = raw.slice(eq + 1).trim().match(/^(\*|-?[\d.eE+-]+)\.\.(\*|-?[\d.eE+-]+)$/);
|
|
35547
|
+
if (!m) throw new Error(`--spec ${name}: expected lsl..usl (use * for an open bound), got "${raw.slice(eq + 1)}"`);
|
|
35548
|
+
const lsl = m[1] === "*" ? null : Number(m[1]);
|
|
35549
|
+
const usl = m[2] === "*" ? null : Number(m[2]);
|
|
35550
|
+
if (lsl !== null && !Number.isFinite(lsl) || usl !== null && !Number.isFinite(usl)) throw new Error(`--spec ${name}: non-finite bound`);
|
|
35551
|
+
if (lsl === null && usl === null) throw new Error(`--spec ${name}: at least one bound must be a number`);
|
|
35552
|
+
return { name, lsl, usl };
|
|
35553
|
+
}
|
|
35554
|
+
function parseDistFlag(raw) {
|
|
35555
|
+
const eq = raw.indexOf("=");
|
|
35556
|
+
if (eq <= 0) throw new Error(`--dist expects Name=normal|uniform, got "${raw}"`);
|
|
35557
|
+
const name = raw.slice(0, eq).trim();
|
|
35558
|
+
const dist3 = raw.slice(eq + 1).trim();
|
|
35559
|
+
if (dist3 !== "normal" && dist3 !== "uniform") throw new Error(`--dist ${name}: expected normal or uniform`);
|
|
35560
|
+
return { name, dist: dist3 };
|
|
35561
|
+
}
|
|
35562
|
+
function resolveToleranceInputs(params, options) {
|
|
35563
|
+
const inputs = [];
|
|
35564
|
+
for (const param of params) {
|
|
35565
|
+
if (options.pinned.has(param.name)) continue;
|
|
35566
|
+
if (param.integer || param.boolean || param.choices) continue;
|
|
35567
|
+
const override = options.tolOverrides.get(param.name);
|
|
35568
|
+
let nominal = param.defaultValue;
|
|
35569
|
+
let halfWidth;
|
|
35570
|
+
if (override?.lo !== void 0 && override?.hi !== void 0) {
|
|
35571
|
+
nominal = (override.lo + override.hi) / 2;
|
|
35572
|
+
halfWidth = (override.hi - override.lo) / 2;
|
|
35573
|
+
} else if (override?.halfWidth !== void 0) {
|
|
35574
|
+
halfWidth = override.halfWidth;
|
|
35575
|
+
} else {
|
|
35576
|
+
halfWidth = (param.max - param.min) / 2;
|
|
35577
|
+
}
|
|
35578
|
+
if (!(halfWidth > 0)) {
|
|
35579
|
+
if (override) throw new Error(`tolerance input "${param.name}" has a non-positive band`);
|
|
35580
|
+
continue;
|
|
35581
|
+
}
|
|
35582
|
+
const dist3 = options.distOverrides.get(param.name) ?? "normal";
|
|
35583
|
+
const sigma = dist3 === "uniform" ? halfWidth / Math.sqrt(3) : halfWidth / options.sigmaLevel;
|
|
35584
|
+
inputs.push({ name: param.name, nominal, halfWidth, sigma, dist: dist3, unit: param.unit });
|
|
35585
|
+
}
|
|
35586
|
+
return inputs;
|
|
35587
|
+
}
|
|
35588
|
+
var RANGE_RE = /^\[\s*(-?[\d.eE+-]+)\s*,\s*(-?[\d.eE+-]+)\s*\]$/;
|
|
35589
|
+
var GT_RE = /^>\s*(-?[\d.eE+-]+)$/;
|
|
35590
|
+
var LT_RE = /^<\s*(-?[\d.eE+-]+)$/;
|
|
35591
|
+
function limitsFromExpected(expected) {
|
|
35592
|
+
if (!expected) return { lsl: null, usl: null };
|
|
35593
|
+
const range = expected.match(RANGE_RE);
|
|
35594
|
+
if (range) return { lsl: Number(range[1]), usl: Number(range[2]) };
|
|
35595
|
+
const gt = expected.match(GT_RE);
|
|
35596
|
+
if (gt) return { lsl: Number(gt[1]), usl: null };
|
|
35597
|
+
const lt = expected.match(LT_RE);
|
|
35598
|
+
if (lt) return { lsl: null, usl: Number(lt[1]) };
|
|
35599
|
+
return { lsl: null, usl: null };
|
|
35600
|
+
}
|
|
35601
|
+
function resolveToleranceResponses(verifications, specOverrides) {
|
|
35602
|
+
const byLabel = /* @__PURE__ */ new Map();
|
|
35603
|
+
for (const v of verifications) {
|
|
35604
|
+
if (extractNumericActual(v.actual) !== null) byLabel.set(v.label, v);
|
|
35605
|
+
}
|
|
35606
|
+
for (const name of specOverrides.keys()) {
|
|
35607
|
+
if (!byLabel.has(name)) {
|
|
35608
|
+
throw new Error(`--spec names response "${name}", but the model has no numeric verify.* result with that label`);
|
|
35609
|
+
}
|
|
35610
|
+
}
|
|
35611
|
+
const responses = [];
|
|
35612
|
+
const unbounded = [];
|
|
35613
|
+
for (const [label, v] of byLabel) {
|
|
35614
|
+
const override = specOverrides.get(label);
|
|
35615
|
+
const limits = override ?? limitsFromExpected(v.expected);
|
|
35616
|
+
if (limits.lsl === null && limits.usl === null) unbounded.push(label);
|
|
35617
|
+
responses.push({ name: label, lsl: limits.lsl, usl: limits.usl });
|
|
35618
|
+
}
|
|
35619
|
+
return { responses, unbounded };
|
|
35620
|
+
}
|
|
35621
|
+
|
|
35622
|
+
// cli/sim-tolerance.ts
|
|
35623
|
+
function parseArgs24(argv) {
|
|
35624
|
+
const inputPaths = [];
|
|
35625
|
+
const paramOverrides = {};
|
|
35626
|
+
const tolOverrides = /* @__PURE__ */ new Map();
|
|
35627
|
+
const distOverrides = /* @__PURE__ */ new Map();
|
|
35628
|
+
const specOverrides = /* @__PURE__ */ new Map();
|
|
35629
|
+
let json = false;
|
|
35630
|
+
let compact = false;
|
|
35631
|
+
let method = "linearized";
|
|
35632
|
+
let samples = null;
|
|
35633
|
+
let seed = 12345;
|
|
35634
|
+
let sigmaLevel = 3;
|
|
35635
|
+
let targetCpk = 1.33;
|
|
35636
|
+
let nonlinearityTol = 0.05;
|
|
35637
|
+
let noFailExit = false;
|
|
35638
|
+
let backend;
|
|
35639
|
+
let quality;
|
|
35640
|
+
const numberFlag = (value, label, predicate) => {
|
|
35641
|
+
const n = Number(value);
|
|
35642
|
+
if (!Number.isFinite(n) || !predicate(n)) throw new Error(`${label} requires a valid number`);
|
|
35643
|
+
return n;
|
|
35644
|
+
};
|
|
35645
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
35646
|
+
const arg = argv[i];
|
|
35647
|
+
if (arg === "--json") {
|
|
35648
|
+
json = true;
|
|
35649
|
+
continue;
|
|
35650
|
+
}
|
|
35651
|
+
if (arg === "--compact") {
|
|
35652
|
+
compact = true;
|
|
35653
|
+
continue;
|
|
35654
|
+
}
|
|
35655
|
+
if (arg === "--no-fail-exit") {
|
|
35656
|
+
noFailExit = true;
|
|
35657
|
+
continue;
|
|
35658
|
+
}
|
|
35659
|
+
if (arg === "--method") {
|
|
35660
|
+
const v = argv[i + 1];
|
|
35661
|
+
if (v !== "linearized" && v !== "full-rebuild") throw new Error("--method must be linearized or full-rebuild");
|
|
35662
|
+
method = v;
|
|
35663
|
+
i += 1;
|
|
35664
|
+
continue;
|
|
35665
|
+
}
|
|
35666
|
+
if (arg === "--samples") {
|
|
35667
|
+
samples = numberFlag(argv[++i], "--samples", (n) => n >= 1);
|
|
35668
|
+
continue;
|
|
35669
|
+
}
|
|
35670
|
+
if (arg === "--seed") {
|
|
35671
|
+
seed = numberFlag(argv[++i], "--seed", () => true) >>> 0;
|
|
35672
|
+
continue;
|
|
35673
|
+
}
|
|
35674
|
+
if (arg === "--sigma") {
|
|
35675
|
+
sigmaLevel = numberFlag(argv[++i], "--sigma", (n) => n > 0);
|
|
35676
|
+
continue;
|
|
35677
|
+
}
|
|
35678
|
+
if (arg === "--target-cpk") {
|
|
35679
|
+
targetCpk = numberFlag(argv[++i], "--target-cpk", (n) => n > 0);
|
|
35680
|
+
continue;
|
|
35681
|
+
}
|
|
35682
|
+
if (arg === "--nonlinearity-tol") {
|
|
35683
|
+
nonlinearityTol = numberFlag(argv[++i], "--nonlinearity-tol", (n) => n > 0);
|
|
35684
|
+
continue;
|
|
35685
|
+
}
|
|
35686
|
+
if (arg === "--tol") {
|
|
35687
|
+
const { name, band } = parseTolFlag(argv[++i] ?? "");
|
|
35688
|
+
tolOverrides.set(name, band);
|
|
35689
|
+
continue;
|
|
35690
|
+
}
|
|
35691
|
+
if (arg === "--dist") {
|
|
35692
|
+
const { name, dist: dist3 } = parseDistFlag(argv[++i] ?? "");
|
|
35693
|
+
distOverrides.set(name, dist3);
|
|
35694
|
+
continue;
|
|
35695
|
+
}
|
|
35696
|
+
if (arg === "--spec") {
|
|
35697
|
+
const { name, lsl, usl } = parseSpecFlag(argv[++i] ?? "");
|
|
35698
|
+
specOverrides.set(name, { lsl, usl });
|
|
35699
|
+
continue;
|
|
35700
|
+
}
|
|
35701
|
+
if (arg === "--quality" || arg === "-q") {
|
|
35702
|
+
const v = argv[i + 1];
|
|
35703
|
+
if (v !== "default" && v !== "live" && v !== "high") throw new Error("--quality must be default, live, or high");
|
|
35704
|
+
quality = v;
|
|
35705
|
+
i += 1;
|
|
35706
|
+
continue;
|
|
35707
|
+
}
|
|
35708
|
+
if (arg === "--backend") {
|
|
35709
|
+
const v = argv[i + 1];
|
|
35710
|
+
if (v !== "manifold" && v !== "occt" && v !== "truck" && v !== "sdf") throw new Error("--backend must be manifold, occt, truck, or sdf");
|
|
35711
|
+
backend = v;
|
|
35712
|
+
i += 1;
|
|
35713
|
+
continue;
|
|
35714
|
+
}
|
|
35715
|
+
if (arg === "--param" || arg === "-p") {
|
|
35716
|
+
const value = argv[i + 1];
|
|
35717
|
+
if (!value || value.startsWith("--")) throw new Error(`${arg} requires Key=Value`);
|
|
35718
|
+
addCliParamOverride(value, paramOverrides);
|
|
35719
|
+
i += 1;
|
|
35720
|
+
continue;
|
|
35721
|
+
}
|
|
35722
|
+
if (arg.startsWith("--")) throw new Error(`Unknown flag: ${arg}`);
|
|
35723
|
+
inputPaths.push(arg);
|
|
35724
|
+
}
|
|
35725
|
+
requireInputPaths(inputPaths, "Usage: forgecad sim tolerance <model.forge.js> [--tol Param=\xB1X] [--spec Resp=lo..hi] [--json]");
|
|
35726
|
+
requireScriptInputPaths(inputPaths);
|
|
35727
|
+
return {
|
|
35728
|
+
inputPaths,
|
|
35729
|
+
json,
|
|
35730
|
+
compact,
|
|
35731
|
+
method,
|
|
35732
|
+
samples,
|
|
35733
|
+
seed,
|
|
35734
|
+
sigmaLevel,
|
|
35735
|
+
targetCpk,
|
|
35736
|
+
nonlinearityTol,
|
|
35737
|
+
noFailExit,
|
|
35738
|
+
tolOverrides,
|
|
35739
|
+
distOverrides,
|
|
35740
|
+
specOverrides,
|
|
35741
|
+
backend,
|
|
35742
|
+
quality,
|
|
35743
|
+
paramOverrides
|
|
35744
|
+
};
|
|
35745
|
+
}
|
|
35746
|
+
function failedReport6(source, message, code = "TOLERANCE.ERROR", suggest) {
|
|
35747
|
+
return {
|
|
35748
|
+
schema: FEEDBACK_SCHEMA,
|
|
35749
|
+
category: "tolerance",
|
|
35750
|
+
source,
|
|
35751
|
+
ok: false,
|
|
35752
|
+
summary: "tolerance analysis failed",
|
|
35753
|
+
metrics: [],
|
|
35754
|
+
findings: [{ level: "error", code, message, ...suggest ? { suggest } : {} }],
|
|
35755
|
+
trust: { assumptions: [] },
|
|
35756
|
+
data: {},
|
|
35757
|
+
timeMs: 0
|
|
35758
|
+
};
|
|
35759
|
+
}
|
|
35760
|
+
function round6(value, digits = 6) {
|
|
35761
|
+
const factor = 10 ** digits;
|
|
35762
|
+
return Math.round(value * factor) / factor;
|
|
35763
|
+
}
|
|
35764
|
+
async function analyze4(scriptPath, options) {
|
|
35765
|
+
const start = Date.now();
|
|
35766
|
+
const input = loadCliScriptInput(scriptPath);
|
|
35767
|
+
await initCliBackend(resolveCliBackend(options.backend, input) ?? CLI_DEFAULT_BACKEND);
|
|
35768
|
+
const qualityPreset = options.quality && options.quality !== "default" ? options.quality : void 0;
|
|
35769
|
+
const buildResponses = (overrides) => {
|
|
35770
|
+
setParamOverrides({ ...options.paramOverrides, ...overrides });
|
|
35771
|
+
const rr = runScript(input.code, input.fileName, input.allFiles, {
|
|
35772
|
+
...qualityPreset ? { quality: qualityPreset } : {},
|
|
35773
|
+
readBinaryFile: input.readBinaryFile
|
|
35774
|
+
});
|
|
35775
|
+
if (rr.error) throw new Error(`model run failed: ${rr.error}`);
|
|
35776
|
+
const map = {};
|
|
35777
|
+
for (const v of rr.verifications) {
|
|
35778
|
+
const n = extractNumericActual(v.actual);
|
|
35779
|
+
if (n !== null) map[v.label] = n;
|
|
35780
|
+
}
|
|
35781
|
+
return map;
|
|
35782
|
+
};
|
|
35783
|
+
setParamOverrides({ ...options.paramOverrides });
|
|
35784
|
+
const nominal = runScript(input.code, input.fileName, input.allFiles, {
|
|
35785
|
+
...qualityPreset ? { quality: qualityPreset } : {},
|
|
35786
|
+
readBinaryFile: input.readBinaryFile
|
|
35787
|
+
});
|
|
35788
|
+
if (nominal.error) return failedReport6(scriptPath, nominal.error, "TOLERANCE.MODEL.ERROR");
|
|
35789
|
+
const inputs = resolveToleranceInputs(nominal.params, {
|
|
35790
|
+
pinned: new Set(Object.keys(options.paramOverrides)),
|
|
35791
|
+
sigmaLevel: options.sigmaLevel,
|
|
35792
|
+
tolOverrides: options.tolOverrides,
|
|
35793
|
+
distOverrides: options.distOverrides
|
|
35794
|
+
});
|
|
35795
|
+
if (inputs.length === 0) {
|
|
35796
|
+
return failedReport6(
|
|
35797
|
+
scriptPath,
|
|
35798
|
+
"No continuous parameters to vary.",
|
|
35799
|
+
"TOLERANCE.NO_INPUTS",
|
|
35800
|
+
"Add a number param with a min/max range, or pass --tol Param=\xB1X."
|
|
35801
|
+
);
|
|
35802
|
+
}
|
|
35803
|
+
const { responses, unbounded } = resolveToleranceResponses(nominal.verifications, options.specOverrides);
|
|
35804
|
+
if (responses.length === 0) {
|
|
35805
|
+
return failedReport6(
|
|
35806
|
+
scriptPath,
|
|
35807
|
+
"No numeric verify.* responses to measure.",
|
|
35808
|
+
"TOLERANCE.NO_RESPONSES",
|
|
35809
|
+
'Add a verify.inRange("Name", measured, lo, hi) call (or similar) to the model.'
|
|
35810
|
+
);
|
|
35811
|
+
}
|
|
35812
|
+
const a = buildResponses({});
|
|
35813
|
+
const b = buildResponses({});
|
|
35814
|
+
for (const r of responses) {
|
|
35815
|
+
if (Math.abs((a[r.name] ?? NaN) - (b[r.name] ?? NaN)) > 1e-9) {
|
|
35816
|
+
return failedReport6(
|
|
35817
|
+
scriptPath,
|
|
35818
|
+
`Response "${r.name}" changed between identical builds; the model is non-deterministic.`,
|
|
35819
|
+
"TOLERANCE.MODEL.NONDETERMINISTIC",
|
|
35820
|
+
"Remove randomness (e.g. Math.random) from the model before tolerance analysis."
|
|
35821
|
+
);
|
|
35822
|
+
}
|
|
35823
|
+
}
|
|
35824
|
+
const samples = options.samples ?? (options.method === "full-rebuild" ? 2e3 : 1e5);
|
|
35825
|
+
let result;
|
|
35826
|
+
try {
|
|
35827
|
+
result = analyzeTolerance(buildResponses, inputs, responses, {
|
|
35828
|
+
method: options.method,
|
|
35829
|
+
samples,
|
|
35830
|
+
seed: options.seed,
|
|
35831
|
+
nonlinearityTol: options.nonlinearityTol,
|
|
35832
|
+
targetCpk: options.targetCpk
|
|
35833
|
+
});
|
|
35834
|
+
} catch (error) {
|
|
35835
|
+
if (error instanceof ToleranceNonlinearError) {
|
|
35836
|
+
return failedReport6(scriptPath, error.message, "TOLERANCE.NONLINEAR", "Re-run with --method full-rebuild.");
|
|
35837
|
+
}
|
|
35838
|
+
throw error;
|
|
35839
|
+
}
|
|
35840
|
+
const findings = [];
|
|
35841
|
+
for (const name of unbounded) {
|
|
35842
|
+
findings.push({
|
|
35843
|
+
level: "warning",
|
|
35844
|
+
code: "TOLERANCE.SPEC.UNBOUNDED",
|
|
35845
|
+
path: name,
|
|
35846
|
+
message: `Response "${name}" has no spec limits; reporting variation only.`,
|
|
35847
|
+
suggest: `Bound it with verify.inRange in the model, or pass --spec ${name}=lo..hi`
|
|
35848
|
+
});
|
|
35849
|
+
}
|
|
35850
|
+
for (const r of result.responses) {
|
|
35851
|
+
if (r.cpk !== null && r.cpk < options.targetCpk) {
|
|
35852
|
+
const top = r.contributions[0];
|
|
35853
|
+
const rec = r.allocation?.find((alloc) => alloc.input === top?.input);
|
|
35854
|
+
findings.push({
|
|
35855
|
+
level: "error",
|
|
35856
|
+
code: "TOLERANCE.CPK.LOW",
|
|
35857
|
+
path: r.name,
|
|
35858
|
+
message: `Response "${r.name}" Cpk ${round6(r.cpk, 3)} is below target ${options.targetCpk} (yield ${round6(r.yieldPct, 2)}%).`,
|
|
35859
|
+
suggest: top ? `Tighten "${top.input}" (${round6(top.fraction * 100, 0)}% of the variation)${rec ? `, e.g. \xB1${round6(rec.recommendedHalfWidth, 4)}` : ""}.` : "Tighten the dominant input tolerance."
|
|
35860
|
+
});
|
|
35861
|
+
}
|
|
35862
|
+
}
|
|
35863
|
+
const metrics = [makeMetric("overallYieldPct", "Overall yield", round6(result.overallYieldPct, 4), { unit: "%" })];
|
|
35864
|
+
for (const r of result.responses) {
|
|
35865
|
+
if (r.cpk !== null) {
|
|
35866
|
+
metrics.push(makeMetric(`cpk.${r.name}`, `Cpk (${r.name})`, round6(r.cpk, 4), { unit: "", target: { op: ">=", value: options.targetCpk } }));
|
|
35867
|
+
}
|
|
35868
|
+
metrics.push(makeMetric(`yieldPct.${r.name}`, `Yield (${r.name})`, round6(r.yieldPct, 4), { unit: "%" }));
|
|
35869
|
+
}
|
|
35870
|
+
const assumptions = [
|
|
35871
|
+
`${result.method === "linearized" ? "Linearized surrogate" : "Full model rebuild"} Monte Carlo, ${samples} samples, seed ${result.seed}.`,
|
|
35872
|
+
"Normal input distributions unless --dist sets uniform; default band = (max-min)/2 of each param, read as \xB1" + options.sigmaLevel + "\u03C3.",
|
|
35873
|
+
"Single population: Cp/Cpk equal Pp/Ppk (no subgroup short-vs-long-term split)."
|
|
35874
|
+
];
|
|
35875
|
+
return {
|
|
35876
|
+
schema: FEEDBACK_SCHEMA,
|
|
35877
|
+
category: "tolerance",
|
|
35878
|
+
source: scriptPath,
|
|
35879
|
+
ok: reportOk(metrics, findings),
|
|
35880
|
+
summary: `${result.responses.length} response(s) \xB7 overall yield ${round6(result.overallYieldPct, 2)}% \xB7 ${result.responses.filter((r) => r.cpk !== null && r.cpk >= options.targetCpk).length}/${result.responses.filter((r) => r.cpk !== null).length} meet Cpk ${options.targetCpk}`,
|
|
35881
|
+
metrics,
|
|
35882
|
+
findings,
|
|
35883
|
+
trust: { assumptions, watertight: true },
|
|
35884
|
+
data: {
|
|
35885
|
+
method: result.method,
|
|
35886
|
+
samples: result.samples,
|
|
35887
|
+
seed: result.seed,
|
|
35888
|
+
targetCpk: result.targetCpk,
|
|
35889
|
+
overallYieldPct: round6(result.overallYieldPct, 4),
|
|
35890
|
+
inputs: result.inputs.map((i) => ({ name: i.name, nominal: round6(i.nominal, 6), halfWidth: round6(i.halfWidth, 6), sigma: round6(i.sigma, 6), dist: i.dist, unit: i.unit })),
|
|
35891
|
+
responses: result.responses.map((r) => ({
|
|
35892
|
+
name: r.name,
|
|
35893
|
+
nominal: round6(r.nominal, 6),
|
|
35894
|
+
mean: round6(r.mean, 6),
|
|
35895
|
+
sigma: round6(r.sigma, 6),
|
|
35896
|
+
lsl: r.lsl,
|
|
35897
|
+
usl: r.usl,
|
|
35898
|
+
cp: r.cp === null ? null : round6(r.cp, 4),
|
|
35899
|
+
cpk: r.cpk === null ? null : round6(r.cpk, 4),
|
|
35900
|
+
yieldPct: round6(r.yieldPct, 4),
|
|
35901
|
+
dpmo: round6(r.dpmo, 1),
|
|
35902
|
+
worstCase: [round6(r.worstCaseMin, 6), round6(r.worstCaseMax, 6)],
|
|
35903
|
+
rss: [round6(r.rssMin, 6), round6(r.rssMax, 6)],
|
|
35904
|
+
contributions: r.contributions.map((c) => ({ input: c.input, percent: round6(c.fraction * 100, 2), sensitivity: round6(c.sensitivity, 6) })),
|
|
35905
|
+
allocation: r.allocation?.map((alloc) => ({ input: alloc.input, currentHalfWidth: round6(alloc.currentHalfWidth, 6), recommendedHalfWidth: round6(alloc.recommendedHalfWidth, 6) })) ?? null
|
|
35906
|
+
}))
|
|
35907
|
+
},
|
|
35908
|
+
timeMs: Date.now() - start
|
|
35909
|
+
};
|
|
35910
|
+
}
|
|
35911
|
+
async function runSimToleranceCli(argv = process.argv.slice(2)) {
|
|
35912
|
+
const options = parseArgs24(argv);
|
|
35913
|
+
requireExistingInputPaths(options.inputPaths);
|
|
35914
|
+
await init();
|
|
35915
|
+
const reports = [];
|
|
35916
|
+
for (const [index, scriptPath] of options.inputPaths.entries()) {
|
|
35917
|
+
if (!options.json) printBatchHeader(scriptPath, index, options.inputPaths.length);
|
|
35918
|
+
let report;
|
|
35919
|
+
try {
|
|
35920
|
+
report = await analyze4(scriptPath, options);
|
|
35921
|
+
} catch (error) {
|
|
35922
|
+
report = failedReport6(scriptPath, error instanceof Error ? error.message : String(error));
|
|
35923
|
+
}
|
|
35924
|
+
reports.push(report);
|
|
35925
|
+
}
|
|
35926
|
+
printFeedback(reports.length === 1 ? reports[0] : { runs: reports }, options.json, options.compact);
|
|
35927
|
+
if (reports.some((report) => !report.ok) && !options.noFailExit) process.exitCode = 1;
|
|
35928
|
+
}
|
|
35929
|
+
|
|
33690
35930
|
// cli/test-run.ts
|
|
33691
|
-
import { resolve as
|
|
35931
|
+
import { resolve as resolve44 } from "path";
|
|
33692
35932
|
|
|
33693
35933
|
// cli/solver-debug-artifacts.ts
|
|
33694
35934
|
import { mkdir as mkdir5, writeFile as writeFile10 } from "fs/promises";
|
|
33695
|
-
import { basename as basename16, join as
|
|
35935
|
+
import { basename as basename16, join as join16, resolve as resolve43 } from "path";
|
|
33696
35936
|
|
|
33697
35937
|
// cli/solver-debug-html.ts
|
|
33698
35938
|
function escapeHtml(s) {
|
|
@@ -34393,7 +36633,7 @@ function appendResiduals(lines, step) {
|
|
|
34393
36633
|
}
|
|
34394
36634
|
}
|
|
34395
36635
|
async function writeSolverDebugArtifacts(outputRoot, scriptPath, objects) {
|
|
34396
|
-
const root =
|
|
36636
|
+
const root = resolve43(outputRoot);
|
|
34397
36637
|
await mkdir5(root, { recursive: true });
|
|
34398
36638
|
const bundles = [];
|
|
34399
36639
|
const usedNames = /* @__PURE__ */ new Map();
|
|
@@ -34407,14 +36647,14 @@ async function writeSolverDebugArtifacts(outputRoot, scriptPath, objects) {
|
|
|
34407
36647
|
const seen = usedNames.get(baseName) ?? 0;
|
|
34408
36648
|
usedNames.set(baseName, seen + 1);
|
|
34409
36649
|
const dirName = seen === 0 ? baseName : `${baseName}-${seen + 1}`;
|
|
34410
|
-
const dir =
|
|
34411
|
-
const svgDir =
|
|
36650
|
+
const dir = join16(root, dirName);
|
|
36651
|
+
const svgDir = join16(dir, "svg");
|
|
34412
36652
|
await mkdir5(svgDir, { recursive: true });
|
|
34413
|
-
const transcriptPath =
|
|
36653
|
+
const transcriptPath = join16(dir, "constructive-transcript.md");
|
|
34414
36654
|
await writeFile10(transcriptPath, buildConstructiveTranscriptMarkdown(object.name || dirName, meta), "utf8");
|
|
34415
36655
|
const snapshotPaths = [];
|
|
34416
36656
|
for (const snapshot of debug.svgSnapshots) {
|
|
34417
|
-
const snapshotPath =
|
|
36657
|
+
const snapshotPath = join16(svgDir, `${snapshot.label}.svg`);
|
|
34418
36658
|
await writeFile10(snapshotPath, snapshot.svg, "utf8");
|
|
34419
36659
|
snapshotPaths.push(snapshotPath);
|
|
34420
36660
|
}
|
|
@@ -34439,7 +36679,7 @@ async function writeSolverDebugArtifacts(outputRoot, scriptPath, objects) {
|
|
|
34439
36679
|
for (const [index, html] of htmlMap) {
|
|
34440
36680
|
const bundle = bundles[index] ?? bundles[bundles.length - 1];
|
|
34441
36681
|
if (bundle) {
|
|
34442
|
-
const htmlPath =
|
|
36682
|
+
const htmlPath = join16(bundle.dir, "dashboard.html");
|
|
34443
36683
|
await writeFile10(htmlPath, html, "utf8");
|
|
34444
36684
|
}
|
|
34445
36685
|
}
|
|
@@ -34447,7 +36687,7 @@ async function writeSolverDebugArtifacts(outputRoot, scriptPath, objects) {
|
|
|
34447
36687
|
return bundles;
|
|
34448
36688
|
}
|
|
34449
36689
|
async function writeSolverDebugIndex(root, scriptPath, bundles) {
|
|
34450
|
-
const lines = ["# Solver debug artifacts", "", `script: ${
|
|
36690
|
+
const lines = ["# Solver debug artifacts", "", `script: ${resolve43(scriptPath)}`, `artifacts: ${bundles.length}`, ""];
|
|
34451
36691
|
if (bundles.length === 0) {
|
|
34452
36692
|
lines.push("No solver debug artifacts were captured.");
|
|
34453
36693
|
} else {
|
|
@@ -34458,7 +36698,7 @@ async function writeSolverDebugIndex(root, scriptPath, bundles) {
|
|
|
34458
36698
|
lines.push(` svg_snapshots: ${bundle.snapshotPaths.length}`);
|
|
34459
36699
|
}
|
|
34460
36700
|
}
|
|
34461
|
-
await writeFile10(
|
|
36701
|
+
await writeFile10(join16(root, "README.md"), lines.join("\n") + "\n", "utf8");
|
|
34462
36702
|
}
|
|
34463
36703
|
|
|
34464
36704
|
// cli/test-run.ts
|
|
@@ -35062,7 +37302,7 @@ async function runScriptCli(argv = process.argv.slice(2)) {
|
|
|
35062
37302
|
if (!printSolverProfile && solverDebugOut) {
|
|
35063
37303
|
const bundles = await writeSolverDebugArtifacts(solverDebugOut, scriptPath, result.objects);
|
|
35064
37304
|
console.log(`
|
|
35065
|
-
\u2713 Solver debug artifacts: ${bundles.length} sketch(es) \u2192 ${
|
|
37305
|
+
\u2713 Solver debug artifacts: ${bundles.length} sketch(es) \u2192 ${resolve44(solverDebugOut)}`);
|
|
35066
37306
|
for (const bundle of bundles) {
|
|
35067
37307
|
console.log(` ${bundle.name}: transcript=${bundle.transcriptPath} svg=${bundle.snapshotPaths.length}`);
|
|
35068
37308
|
}
|
|
@@ -35191,7 +37431,7 @@ async function runScriptCli(argv = process.argv.slice(2)) {
|
|
|
35191
37431
|
if (solverDebugOut) {
|
|
35192
37432
|
const bundles = await writeSolverDebugArtifacts(solverDebugOut, scriptPath, result.objects);
|
|
35193
37433
|
console.log(`
|
|
35194
|
-
\u2713 Solver debug artifacts: ${bundles.length} sketch(es) \u2192 ${
|
|
37434
|
+
\u2713 Solver debug artifacts: ${bundles.length} sketch(es) \u2192 ${resolve44(solverDebugOut)}`);
|
|
35195
37435
|
for (const bundle of bundles) {
|
|
35196
37436
|
console.log(` ${bundle.name}: transcript=${bundle.transcriptPath} svg=${bundle.snapshotPaths.length}`);
|
|
35197
37437
|
}
|
|
@@ -35217,7 +37457,7 @@ async function runScriptCli(argv = process.argv.slice(2)) {
|
|
|
35217
37457
|
|
|
35218
37458
|
// cli/forge-render-section.ts
|
|
35219
37459
|
import { writeFile as writeFile11 } from "fs/promises";
|
|
35220
|
-
import { basename as basename17, extname as
|
|
37460
|
+
import { basename as basename17, extname as extname14, resolve as resolve45 } from "path";
|
|
35221
37461
|
function usage26() {
|
|
35222
37462
|
console.error(
|
|
35223
37463
|
"Usage: forgecad render section <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [input ...] [--output path] [--format svg|png] [--param Key=Value] [--joint JointName=Value] [--plane XY|XZ|YZ] [--offset <number>] [--size <px>] [--edges <off|thin|bold>] [--chrome-path <path>] [--background <color>]"
|
|
@@ -35309,7 +37549,7 @@ function parseRenderSectionArgs(argv) {
|
|
|
35309
37549
|
async function runRenderSectionCli(argv = process.argv.slice(2)) {
|
|
35310
37550
|
const options = parseRenderSectionArgs(argv);
|
|
35311
37551
|
requireExistingInputPaths(options.inputPaths);
|
|
35312
|
-
const needsPng = options.format === "png" || (options.outputPath ?
|
|
37552
|
+
const needsPng = options.format === "png" || (options.outputPath ? extname14(options.outputPath).toLowerCase() === ".png" : false);
|
|
35313
37553
|
let resolvedChromePath;
|
|
35314
37554
|
if (needsPng) {
|
|
35315
37555
|
resolvedChromePath = findChromePath(options.chromePath) ?? void 0;
|
|
@@ -35333,9 +37573,9 @@ async function runRenderSectionCli(argv = process.argv.slice(2)) {
|
|
|
35333
37573
|
}
|
|
35334
37574
|
}
|
|
35335
37575
|
async function renderSectionInput(options, scriptPath, resolvedChromePath) {
|
|
35336
|
-
const outputFormat = options.format ?? (options.outputPath &&
|
|
37576
|
+
const outputFormat = options.format ?? (options.outputPath && extname14(options.outputPath).toLowerCase() === ".png" ? "png" : "svg");
|
|
35337
37577
|
const outputPath = batchOutputPath(scriptPath, options.outputPath, (inputPath) => defaultOutput(inputPath, outputFormat));
|
|
35338
|
-
if (
|
|
37578
|
+
if (resolve45(outputPath) === resolve45(scriptPath)) {
|
|
35339
37579
|
throw new Error(`ERROR: output path would overwrite the input script. Specify an explicit output path.`);
|
|
35340
37580
|
}
|
|
35341
37581
|
const input = loadCliScriptInput(scriptPath);
|
|
@@ -35374,9 +37614,9 @@ async function renderSectionInput(options, scriptPath, resolvedChromePath) {
|
|
|
35374
37614
|
background: options.background,
|
|
35375
37615
|
chromePath: resolvedChromePath
|
|
35376
37616
|
});
|
|
35377
|
-
await writeFile11(
|
|
37617
|
+
await writeFile11(resolve45(outputPath), png);
|
|
35378
37618
|
} else {
|
|
35379
|
-
await writeFile11(
|
|
37619
|
+
await writeFile11(resolve45(outputPath), svgDocument.svg);
|
|
35380
37620
|
}
|
|
35381
37621
|
const sz = [svgDocument.width.toFixed(1), svgDocument.height.toFixed(1)];
|
|
35382
37622
|
const totalArea = sectionSketches.reduce((sum2, s) => sum2 + s.sketch.area(), 0);
|
|
@@ -35387,20 +37627,20 @@ async function renderSectionInput(options, scriptPath, resolvedChromePath) {
|
|
|
35387
37627
|
|
|
35388
37628
|
// cli/update-check.ts
|
|
35389
37629
|
import { spawn as spawn4 } from "child_process";
|
|
35390
|
-
import { existsSync as
|
|
37630
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync14, readFileSync as readFileSync27, writeFileSync as writeFileSync21 } from "fs";
|
|
35391
37631
|
import { homedir as homedir9 } from "os";
|
|
35392
|
-
import { dirname as
|
|
37632
|
+
import { dirname as dirname14, join as join17 } from "path";
|
|
35393
37633
|
var PACKAGE_NAME = "forgecad";
|
|
35394
37634
|
var REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
35395
37635
|
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
35396
37636
|
var FETCH_TIMEOUT_MS = 3e3;
|
|
35397
37637
|
var WORKER_FLAG = "--__update-check-worker";
|
|
35398
37638
|
function cachePath() {
|
|
35399
|
-
return
|
|
37639
|
+
return join17(homedir9(), ".forgecad", "update-check.json");
|
|
35400
37640
|
}
|
|
35401
37641
|
function readCache() {
|
|
35402
37642
|
try {
|
|
35403
|
-
const text =
|
|
37643
|
+
const text = readFileSync27(cachePath(), "utf-8");
|
|
35404
37644
|
const parsed = JSON.parse(text);
|
|
35405
37645
|
if (typeof parsed.checkedAt !== "number" || typeof parsed.latest !== "string") return null;
|
|
35406
37646
|
return parsed;
|
|
@@ -35410,9 +37650,9 @@ function readCache() {
|
|
|
35410
37650
|
}
|
|
35411
37651
|
function writeCache(cache) {
|
|
35412
37652
|
const p = cachePath();
|
|
35413
|
-
const dir =
|
|
35414
|
-
if (!
|
|
35415
|
-
|
|
37653
|
+
const dir = dirname14(p);
|
|
37654
|
+
if (!existsSync22(dir)) mkdirSync14(dir, { recursive: true });
|
|
37655
|
+
writeFileSync21(p, JSON.stringify(cache), "utf-8");
|
|
35416
37656
|
}
|
|
35417
37657
|
function shouldSkip() {
|
|
35418
37658
|
if (process.env.FORGE_NO_UPDATE_CHECK) return true;
|
|
@@ -35633,7 +37873,9 @@ var PARAM_OPTIONS = [
|
|
|
35633
37873
|
repeatable: true
|
|
35634
37874
|
}
|
|
35635
37875
|
];
|
|
35636
|
-
var
|
|
37876
|
+
var PARAM_OVERRIDE_OPTIONS = PARAM_OPTIONS.filter(
|
|
37877
|
+
(option) => option.name === "--param" || option.name === "-p"
|
|
37878
|
+
);
|
|
35637
37879
|
var RENDER_OPTIONS = [
|
|
35638
37880
|
...PARAM_OPTIONS,
|
|
35639
37881
|
{
|
|
@@ -36510,7 +38752,7 @@ var HISTORY_INSPECT_COMMANDS = [
|
|
|
36510
38752
|
{ name: "--params", description: "Print resolved parameter values after the history" },
|
|
36511
38753
|
{ name: "--source", description: "Show source file and line for each printed feature step" },
|
|
36512
38754
|
{ name: "--trace-ids", description: "Show trace ids for feature steps and final objects" },
|
|
36513
|
-
...
|
|
38755
|
+
...PARAM_OVERRIDE_OPTIONS,
|
|
36514
38756
|
{
|
|
36515
38757
|
name: "--backend",
|
|
36516
38758
|
description: "Geometry backend (default: manifold; STEP inputs default to OCCT)",
|
|
@@ -36555,7 +38797,7 @@ var HISTORY_INSPECT_COMMANDS = [
|
|
|
36555
38797
|
},
|
|
36556
38798
|
{ name: "--anchor", description: "Print one agent-ready feedback anchor when the query matches exactly one trace node" },
|
|
36557
38799
|
{ name: "--full", description: "Print all matching trace nodes" },
|
|
36558
|
-
...
|
|
38800
|
+
...PARAM_OVERRIDE_OPTIONS,
|
|
36559
38801
|
{
|
|
36560
38802
|
name: "--backend",
|
|
36561
38803
|
description: "Geometry backend (default: manifold; STEP inputs default to OCCT)",
|
|
@@ -36916,7 +39158,7 @@ var commands = [
|
|
|
36916
39158
|
valueLabel: "<live|default|high>",
|
|
36917
39159
|
values: QUALITY_VALUES
|
|
36918
39160
|
},
|
|
36919
|
-
...
|
|
39161
|
+
...PARAM_OVERRIDE_OPTIONS
|
|
36920
39162
|
],
|
|
36921
39163
|
positionals: [{ description: "Forge script or CAD asset", valueKind: "renderable", repeatable: true }]
|
|
36922
39164
|
},
|
|
@@ -37006,7 +39248,7 @@ var commands = [
|
|
|
37006
39248
|
completion: {
|
|
37007
39249
|
options: [
|
|
37008
39250
|
{ name: "--json", description: "Print machine-readable JSON" },
|
|
37009
|
-
...
|
|
39251
|
+
...PARAM_OVERRIDE_OPTIONS,
|
|
37010
39252
|
{
|
|
37011
39253
|
name: "--quality",
|
|
37012
39254
|
description: "Mesh quality preset",
|
|
@@ -37052,7 +39294,7 @@ var commands = [
|
|
|
37052
39294
|
valueLabel: "<name>",
|
|
37053
39295
|
values: HQ_PRESET_VALUES
|
|
37054
39296
|
},
|
|
37055
|
-
...
|
|
39297
|
+
...PARAM_OVERRIDE_OPTIONS,
|
|
37056
39298
|
{
|
|
37057
39299
|
name: "--edges",
|
|
37058
39300
|
description: "Edge overlay preset (default: off)",
|
|
@@ -37124,7 +39366,7 @@ var commands = [
|
|
|
37124
39366
|
path: ["render", "sketch"],
|
|
37125
39367
|
summary: "Render a 2D sketch .forge.js to PNG.",
|
|
37126
39368
|
usage: [
|
|
37127
|
-
"forgecad render sketch <script.forge.js> [input ...] [--output path] [--size <px>] [--background <color>] [--chrome-path <path>]"
|
|
39369
|
+
"forgecad render sketch <script.forge.js> [input ...] [--output path] [--size <px>] [--background <color>] [--chrome-path <path>] [--param Key=Value]"
|
|
37128
39370
|
],
|
|
37129
39371
|
examples: [
|
|
37130
39372
|
"forgecad render sketch examples/01-fully-constrained-rect.forge.js",
|
|
@@ -37133,6 +39375,7 @@ var commands = [
|
|
|
37133
39375
|
],
|
|
37134
39376
|
completion: {
|
|
37135
39377
|
options: [
|
|
39378
|
+
...PARAM_OVERRIDE_OPTIONS,
|
|
37136
39379
|
{ name: "--size", description: "Output image size in pixels (square)", argument: "required", valueLabel: "<px>" },
|
|
37137
39380
|
{ name: "--background", description: "Background color (CSS color)", argument: "required", valueLabel: "<color>" },
|
|
37138
39381
|
{ name: "--chrome-path", description: "Chrome executable path", argument: "required", valueLabel: "<path>" },
|
|
@@ -37211,7 +39454,7 @@ var commands = [
|
|
|
37211
39454
|
],
|
|
37212
39455
|
completion: {
|
|
37213
39456
|
options: [
|
|
37214
|
-
...
|
|
39457
|
+
...PARAM_OVERRIDE_OPTIONS,
|
|
37215
39458
|
{ name: "--plane", description: "Cut plane", argument: "required", valueLabel: "<XY|XZ|YZ>", values: SECTION_PLANE_VALUES },
|
|
37216
39459
|
{ name: "--offset", description: "Shift the cut position along the plane normal", argument: "required", valueLabel: "<mm>" },
|
|
37217
39460
|
{ name: "--size", description: "Output PNG size in pixels (square)", argument: "required", valueLabel: "<px>" },
|
|
@@ -37274,13 +39517,13 @@ var commands = [
|
|
|
37274
39517
|
path: ["cut-list"],
|
|
37275
39518
|
summary: "Print a grouped sheet-material cut list from `sheetStock()` declarations.",
|
|
37276
39519
|
description: 'Runs the script, collects every `sheetStock(...)` declaration, and prints a grouped terminal cut list with quantity, part name, size, and material.\n\nUse this when you want to answer "what rectangles do I actually need to cut?" without generating a PDF layout. For sheet packing, kerf-aware nesting, and cut sequencing, use `forgecad export cutting-layout` instead.',
|
|
37277
|
-
usage: ["forgecad cut-list <script.forge.js> [input ...] [--joint JointName=Value]"],
|
|
39520
|
+
usage: ["forgecad cut-list <script.forge.js> [input ...] [--param Key=Value] [--joint JointName=Value]"],
|
|
37278
39521
|
examples: [
|
|
37279
39522
|
"forgecad cut-list examples/api/sheet-stock-cut-list.forge.js",
|
|
37280
39523
|
"forgecad cut-list examples/api/sheet-stock-cut-list.forge.js path/to/my-cabinet.forge.js"
|
|
37281
39524
|
],
|
|
37282
39525
|
completion: {
|
|
37283
|
-
options:
|
|
39526
|
+
options: PARAM_OPTIONS,
|
|
37284
39527
|
positionals: [{ description: "Forge script", valueKind: "forge-script", repeatable: true }]
|
|
37285
39528
|
},
|
|
37286
39529
|
run: runCutListCli
|
|
@@ -37289,7 +39532,7 @@ var commands = [
|
|
|
37289
39532
|
group: "Export",
|
|
37290
39533
|
path: ["export", "svg"],
|
|
37291
39534
|
summary: "Export a sketch `.forge.js` file to SVG.",
|
|
37292
|
-
usage: ["forgecad export svg <script.forge.js> [input ...] [--output path]"],
|
|
39535
|
+
usage: ["forgecad export svg <script.forge.js> [input ...] [--output path] [--param Key=Value]"],
|
|
37293
39536
|
examples: [
|
|
37294
39537
|
"forgecad export svg examples/01-fully-constrained-rect.forge.js",
|
|
37295
39538
|
"forgecad export svg examples/01-fully-constrained-rect.forge.js --output out/rect.svg",
|
|
@@ -37297,6 +39540,7 @@ var commands = [
|
|
|
37297
39540
|
],
|
|
37298
39541
|
completion: {
|
|
37299
39542
|
options: [
|
|
39543
|
+
...PARAM_OVERRIDE_OPTIONS,
|
|
37300
39544
|
{
|
|
37301
39545
|
name: "--output",
|
|
37302
39546
|
description: "Output SVG path for a single input",
|
|
@@ -37313,7 +39557,7 @@ var commands = [
|
|
|
37313
39557
|
group: "Export",
|
|
37314
39558
|
path: ["export", "sketch-pdf"],
|
|
37315
39559
|
summary: "Export a constrained sketch `.forge.js` to a single-page PDF with dimensions, constraints, and surfaces.",
|
|
37316
|
-
usage: ["forgecad export sketch-pdf <script.forge.js> [input ...] [--output path]"],
|
|
39560
|
+
usage: ["forgecad export sketch-pdf <script.forge.js> [input ...] [--output path] [--param Key=Value]"],
|
|
37317
39561
|
examples: [
|
|
37318
39562
|
"forgecad export sketch-pdf examples/constraints/01-fully-constrained-rect.forge.js",
|
|
37319
39563
|
"forgecad export sketch-pdf examples/constraints/01-fully-constrained-rect.forge.js --output out/rect.pdf",
|
|
@@ -37321,6 +39565,7 @@ var commands = [
|
|
|
37321
39565
|
],
|
|
37322
39566
|
completion: {
|
|
37323
39567
|
options: [
|
|
39568
|
+
...PARAM_OVERRIDE_OPTIONS,
|
|
37324
39569
|
{
|
|
37325
39570
|
name: "--output",
|
|
37326
39571
|
description: "Output PDF path for a single input",
|
|
@@ -37338,7 +39583,7 @@ var commands = [
|
|
|
37338
39583
|
path: ["export", "step"],
|
|
37339
39584
|
summary: "Export exact STEP through the native OCCT or Truck runtime exporter.",
|
|
37340
39585
|
usage: [
|
|
37341
|
-
"forgecad export step <model.forge.js|asset.step|asset.stp> [input ...] [--output path] [--backend occt|truck] [--joint JointName=Value]"
|
|
39586
|
+
"forgecad export step <model.forge.js|asset.step|asset.stp> [input ...] [--output path] [--backend occt|truck] [--param Key=Value] [--joint JointName=Value]"
|
|
37342
39587
|
],
|
|
37343
39588
|
examples: [
|
|
37344
39589
|
"forgecad export step examples/api/boolean-operations.forge.js",
|
|
@@ -37348,7 +39593,7 @@ var commands = [
|
|
|
37348
39593
|
],
|
|
37349
39594
|
completion: {
|
|
37350
39595
|
options: [
|
|
37351
|
-
...
|
|
39596
|
+
...PARAM_OPTIONS,
|
|
37352
39597
|
{ name: "--output", description: "Output STEP path", argument: "required", valueLabel: "<path>", valueKind: "path" },
|
|
37353
39598
|
{
|
|
37354
39599
|
name: "--backend",
|
|
@@ -37365,14 +39610,16 @@ var commands = [
|
|
|
37365
39610
|
group: "Export",
|
|
37366
39611
|
path: ["export", "brep"],
|
|
37367
39612
|
summary: "Export exact BREP through the native OCCT runtime exporter.",
|
|
37368
|
-
usage: [
|
|
39613
|
+
usage: [
|
|
39614
|
+
"forgecad export brep <model.forge.js|asset.step|asset.stp> [input ...] [--output path] [--param Key=Value] [--joint JointName=Value]"
|
|
39615
|
+
],
|
|
37369
39616
|
examples: [
|
|
37370
39617
|
"forgecad export brep examples/api/boolean-operations.forge.js",
|
|
37371
39618
|
"forgecad export brep examples/api/boolean-operations.forge.js examples/api/mixed-edge-finishes-proof.forge.js"
|
|
37372
39619
|
],
|
|
37373
39620
|
completion: {
|
|
37374
39621
|
options: [
|
|
37375
|
-
...
|
|
39622
|
+
...PARAM_OPTIONS,
|
|
37376
39623
|
{ name: "--output", description: "Output BREP path", argument: "required", valueLabel: "<path>", valueKind: "path" }
|
|
37377
39624
|
],
|
|
37378
39625
|
positionals: [{ description: "Forge script or STEP file", valueKind: "exact-cad-input", repeatable: true }]
|
|
@@ -37384,7 +39631,7 @@ var commands = [
|
|
|
37384
39631
|
path: ["export", "3mf"],
|
|
37385
39632
|
summary: "Export a Forge script to 3MF (3D Manufacturing Format) for 3D printing.",
|
|
37386
39633
|
usage: [
|
|
37387
|
-
"forgecad export 3mf <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [input ...] [--output path] [--quality default|live|high] [--backend manifold|occt|truck|sdf] [--joint JointName=Value]"
|
|
39634
|
+
"forgecad export 3mf <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [input ...] [--output path] [--quality default|live|high] [--backend manifold|occt|truck|sdf] [--param Key=Value] [--joint JointName=Value]"
|
|
37388
39635
|
],
|
|
37389
39636
|
examples: [
|
|
37390
39637
|
"forgecad export 3mf examples/products/cup.forge.js",
|
|
@@ -37395,7 +39642,7 @@ var commands = [
|
|
|
37395
39642
|
],
|
|
37396
39643
|
completion: {
|
|
37397
39644
|
options: [
|
|
37398
|
-
...
|
|
39645
|
+
...PARAM_OPTIONS,
|
|
37399
39646
|
{ name: "--output", description: "Output 3MF path", argument: "required", valueLabel: "<path>", valueKind: "path" },
|
|
37400
39647
|
{
|
|
37401
39648
|
name: "--quality",
|
|
@@ -37421,18 +39668,19 @@ var commands = [
|
|
|
37421
39668
|
path: ["export", "stl"],
|
|
37422
39669
|
summary: "Export a Forge script to binary STL.",
|
|
37423
39670
|
usage: [
|
|
37424
|
-
"forgecad export stl <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [input ...] [--output path] [--quality default|live|high] [--backend manifold|occt|truck|sdf] [--joint JointName=Value]"
|
|
39671
|
+
"forgecad export stl <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [input ...] [--output path] [--quality default|live|high] [--backend manifold|occt|truck|sdf] [--param Key=Value] [--joint JointName=Value]"
|
|
37425
39672
|
],
|
|
37426
39673
|
examples: [
|
|
37427
39674
|
"forgecad export stl examples/products/cup.forge.js",
|
|
37428
39675
|
"forgecad export stl examples/products/cup.forge.js --output out/cup.stl",
|
|
39676
|
+
"forgecad export stl examples/products/cup.forge.js --param Diameter=80 --output out/cup.stl",
|
|
37429
39677
|
"forgecad export stl examples/products/cup.forge.js examples/api/boolean-operations.forge.js",
|
|
37430
39678
|
"forgecad export stl examples/products/cup.forge.js --quality high",
|
|
37431
39679
|
"forgecad export stl examples/products/cup.forge.js --backend occt"
|
|
37432
39680
|
],
|
|
37433
39681
|
completion: {
|
|
37434
39682
|
options: [
|
|
37435
|
-
...
|
|
39683
|
+
...PARAM_OPTIONS,
|
|
37436
39684
|
{ name: "--output", description: "Output STL path", argument: "required", valueLabel: "<path>", valueKind: "path" },
|
|
37437
39685
|
{
|
|
37438
39686
|
name: "--quality",
|
|
@@ -37457,7 +39705,7 @@ var commands = [
|
|
|
37457
39705
|
group: "Export",
|
|
37458
39706
|
path: ["export", "gcode"],
|
|
37459
39707
|
summary: "Export a G-code toolpath script to .gcode for direct 3D printing.",
|
|
37460
|
-
usage: ["forgecad export gcode <script.forge.js> [input ...] [--output path] [--joint JointName=Value]"],
|
|
39708
|
+
usage: ["forgecad export gcode <script.forge.js> [input ...] [--output path] [--param Key=Value] [--joint JointName=Value]"],
|
|
37461
39709
|
examples: [
|
|
37462
39710
|
"forgecad export gcode examples/gcode/parametric-vase.forge.js",
|
|
37463
39711
|
"forgecad export gcode examples/gcode/parametric-vase.forge.js --output out/vase.gcode",
|
|
@@ -37465,7 +39713,7 @@ var commands = [
|
|
|
37465
39713
|
],
|
|
37466
39714
|
completion: {
|
|
37467
39715
|
options: [
|
|
37468
|
-
...
|
|
39716
|
+
...PARAM_OPTIONS,
|
|
37469
39717
|
{ name: "--output", description: "Output file path for a single input", argument: "required", valueLabel: "<path>" },
|
|
37470
39718
|
{ name: "-o", description: "Shorthand for --output", argument: "required", valueLabel: "<path>" }
|
|
37471
39719
|
],
|
|
@@ -37479,7 +39727,7 @@ var commands = [
|
|
|
37479
39727
|
summary: "Export direct SDF scene data for GPU or server-side implicit rendering.",
|
|
37480
39728
|
description: "Writes JSON SDF scene programs or a WebGPU distance-brick compute package without converting the model to STL/3MF meshes. Defaults to the SDF backend.",
|
|
37481
39729
|
usage: [
|
|
37482
|
-
"forgecad export implicit <model.forge.js|asset.step|asset.stp> [--output path] [--format json|webgpu-brick] [--quality live|default|high] [--backend manifold|occt|truck|sdf] [--workgroup-size 4x4x4]"
|
|
39730
|
+
"forgecad export implicit <model.forge.js|asset.step|asset.stp> [--output path] [--format json|webgpu-brick] [--quality live|default|high] [--backend manifold|occt|truck|sdf] [--workgroup-size 4x4x4] [--param Key=Value] [--joint JointName=Value]"
|
|
37483
39731
|
],
|
|
37484
39732
|
examples: [
|
|
37485
39733
|
"forgecad export implicit examples/products/bottle.forge.js --quality live",
|
|
@@ -37488,7 +39736,6 @@ var commands = [
|
|
|
37488
39736
|
completion: {
|
|
37489
39737
|
options: [
|
|
37490
39738
|
...PARAM_OPTIONS,
|
|
37491
|
-
...JOINT_OPTIONS,
|
|
37492
39739
|
{ name: "--output", description: "Output file or package directory", argument: "required", valueLabel: "<path>" },
|
|
37493
39740
|
{ name: "-o", description: "Shorthand for --output", argument: "required", valueLabel: "<path>" },
|
|
37494
39741
|
{
|
|
@@ -37529,7 +39776,7 @@ var commands = [
|
|
|
37529
39776
|
group: "Export",
|
|
37530
39777
|
path: ["export", "sdf"],
|
|
37531
39778
|
summary: "Export a robot assembly as a Gazebo-ready SDF package.",
|
|
37532
|
-
usage: ["forgecad export sdf <script.forge.js> [input ...] [--output dir] [--joint JointName=Value]"],
|
|
39779
|
+
usage: ["forgecad export sdf <script.forge.js> [input ...] [--output dir] [--param Key=Value] [--joint JointName=Value]"],
|
|
37533
39780
|
examples: [
|
|
37534
39781
|
"forgecad export sdf path/to/robot.forge.js",
|
|
37535
39782
|
"forgecad export sdf path/to/robot.forge.js --output out/robot_sdf",
|
|
@@ -37537,7 +39784,7 @@ var commands = [
|
|
|
37537
39784
|
],
|
|
37538
39785
|
completion: {
|
|
37539
39786
|
options: [
|
|
37540
|
-
...
|
|
39787
|
+
...PARAM_OPTIONS,
|
|
37541
39788
|
{
|
|
37542
39789
|
name: "--output",
|
|
37543
39790
|
description: "Output package directory for a single input",
|
|
@@ -37554,15 +39801,16 @@ var commands = [
|
|
|
37554
39801
|
group: "Export",
|
|
37555
39802
|
path: ["export", "mjcf"],
|
|
37556
39803
|
summary: "Export a robot assembly as a native MuJoCo MJCF package.",
|
|
37557
|
-
usage: ["forgecad export mjcf <script.forge.js> [input ...] [--output dir] [--joint JointName=Value]"],
|
|
39804
|
+
usage: ["forgecad export mjcf <script.forge.js> [input ...] [--output dir] [--param Key=Value] [--joint JointName=Value]"],
|
|
37558
39805
|
examples: [
|
|
37559
39806
|
"forgecad export mjcf path/to/robot.forge.js",
|
|
37560
39807
|
"forgecad export mjcf path/to/robot.forge.js --output out/robot_mjcf",
|
|
39808
|
+
"forgecad export mjcf path/to/robot.forge.js --param Wheelbase=180 --output out/robot_mjcf",
|
|
37561
39809
|
"forgecad export mjcf path/to/robot.forge.js path/to/second-robot.forge.js"
|
|
37562
39810
|
],
|
|
37563
39811
|
completion: {
|
|
37564
39812
|
options: [
|
|
37565
|
-
...
|
|
39813
|
+
...PARAM_OPTIONS,
|
|
37566
39814
|
{
|
|
37567
39815
|
name: "--output",
|
|
37568
39816
|
description: "Output package directory for a single input",
|
|
@@ -37579,7 +39827,7 @@ var commands = [
|
|
|
37579
39827
|
group: "Export",
|
|
37580
39828
|
path: ["export", "urdf"],
|
|
37581
39829
|
summary: "Export a robot assembly as a URDF package (ROS/PyBullet/MuJoCo).",
|
|
37582
|
-
usage: ["forgecad export urdf <script.forge.js> [input ...] [--output dir] [--joint JointName=Value]"],
|
|
39830
|
+
usage: ["forgecad export urdf <script.forge.js> [input ...] [--output dir] [--param Key=Value] [--joint JointName=Value]"],
|
|
37583
39831
|
examples: [
|
|
37584
39832
|
"forgecad export urdf path/to/robot.forge.js",
|
|
37585
39833
|
"forgecad export urdf path/to/robot.forge.js --output out/robot_urdf",
|
|
@@ -37587,7 +39835,7 @@ var commands = [
|
|
|
37587
39835
|
],
|
|
37588
39836
|
completion: {
|
|
37589
39837
|
options: [
|
|
37590
|
-
...
|
|
39838
|
+
...PARAM_OPTIONS,
|
|
37591
39839
|
{
|
|
37592
39840
|
name: "--output",
|
|
37593
39841
|
description: "Output package directory for a single input",
|
|
@@ -37600,11 +39848,30 @@ var commands = [
|
|
|
37600
39848
|
},
|
|
37601
39849
|
run: runUrdfCli
|
|
37602
39850
|
},
|
|
39851
|
+
{
|
|
39852
|
+
group: "Export",
|
|
39853
|
+
path: ["export", "pinocchio"],
|
|
39854
|
+
summary: "Export a robot assembly as a Pinocchio dynamics package (URDF + gravity-torque harness).",
|
|
39855
|
+
description: "Reuses the URDF package (with per-link inertials computed from the solid) and adds a runnable Pinocchio harness. `scripts/run_pinocchio.py` sweeps each joint, computes the gravity-hold torque via `pin.computeGeneralizedGravity`, and writes `feedback.json` (the `forgecad.feedback/v1` contract); re-surface it with `forgecad sim dynamics <dir>`. Pinocchio (BSD-2) and Coal (BSD-3) are user-installed \u2014 Linux/macOS `pip install pin coal`, Windows conda-forge \u2014 never bundled.",
|
|
39856
|
+
usage: ["forgecad export pinocchio <script.forge.js> [input ...] [--output dir] [--param Key=Value] [--joint JointName=Value]"],
|
|
39857
|
+
examples: [
|
|
39858
|
+
"forgecad export pinocchio examples/analysis/lever-arm-actuator.forge.js",
|
|
39859
|
+
"forgecad export pinocchio path/to/robot.forge.js --output out/robot_pinocchio"
|
|
39860
|
+
],
|
|
39861
|
+
completion: {
|
|
39862
|
+
options: [
|
|
39863
|
+
...PARAM_OPTIONS,
|
|
39864
|
+
{ name: "--output", description: "Output package directory for a single input", argument: "required", valueLabel: "<dir>", valueKind: "directory" }
|
|
39865
|
+
],
|
|
39866
|
+
positionals: [{ description: "Forge script", valueKind: "forge-script", repeatable: true }]
|
|
39867
|
+
},
|
|
39868
|
+
run: runPinocchioCli
|
|
39869
|
+
},
|
|
37603
39870
|
{
|
|
37604
39871
|
group: "Export",
|
|
37605
39872
|
path: ["export", "usd"],
|
|
37606
39873
|
summary: "Export a SimReady assembly as an Isaac Sim-ready USD package.",
|
|
37607
|
-
usage: ["forgecad export usd <script.forge.js> [input ...] [--output dir] [--joint JointName=Value]"],
|
|
39874
|
+
usage: ["forgecad export usd <script.forge.js> [input ...] [--output dir] [--param Key=Value] [--joint JointName=Value]"],
|
|
37608
39875
|
examples: [
|
|
37609
39876
|
"forgecad export usd path/to/robot.forge.js",
|
|
37610
39877
|
"forgecad export usd path/to/robot.forge.js path/to/arm.forge.js",
|
|
@@ -37612,7 +39879,7 @@ var commands = [
|
|
|
37612
39879
|
],
|
|
37613
39880
|
completion: {
|
|
37614
39881
|
options: [
|
|
37615
|
-
...
|
|
39882
|
+
...PARAM_OPTIONS,
|
|
37616
39883
|
{
|
|
37617
39884
|
name: "--output",
|
|
37618
39885
|
description: "Output package directory for a single input",
|
|
@@ -37630,7 +39897,7 @@ var commands = [
|
|
|
37630
39897
|
path: ["export", "report"],
|
|
37631
39898
|
summary: "Generate a multi-view PDF report with BOM and dimensions.",
|
|
37632
39899
|
usage: [
|
|
37633
|
-
"forgecad export report <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [input ...] [--output path] [--dim-angle-tol <deg>] [--joint JointName=Value]"
|
|
39900
|
+
"forgecad export report <model.forge.js|asset.stl|asset.obj|asset.3mf|asset.step|asset.stp> [input ...] [--output path] [--dim-angle-tol <deg>] [--param Key=Value] [--joint JointName=Value]"
|
|
37634
39901
|
],
|
|
37635
39902
|
examples: [
|
|
37636
39903
|
"forgecad export report examples/products/cup.forge.js",
|
|
@@ -37639,7 +39906,7 @@ var commands = [
|
|
|
37639
39906
|
],
|
|
37640
39907
|
completion: {
|
|
37641
39908
|
options: [
|
|
37642
|
-
...
|
|
39909
|
+
...PARAM_OPTIONS,
|
|
37643
39910
|
{
|
|
37644
39911
|
name: "--output",
|
|
37645
39912
|
description: "Output PDF path for a single input",
|
|
@@ -37660,7 +39927,7 @@ var commands = [
|
|
|
37660
39927
|
description: "Packs `sheetStock(...)` pieces onto stock sheets, writes a cutting-layout PDF or DXF, and prints the guillotine cut sequence in the terminal.\n\nPDF is the default review format. Use `--format dxf` or an `.dxf` output path when sending the packed layout to CAD/CAM tools; DXF output contains only the placed cut outlines by default. For the simpler grouped parts list without packing or layout export, use `forgecad cut-list`.",
|
|
37661
39928
|
usage: [
|
|
37662
39929
|
"forgecad export cutting-layout <script.forge.js> [input ...] [--output output.pdf|output.dxf]",
|
|
37663
|
-
" [--format pdf|dxf] [--sheet-width <mm>] [--sheet-height <mm>] [--kerf <mm>] [--joint JointName=Value]"
|
|
39930
|
+
" [--format pdf|dxf] [--sheet-width <mm>] [--sheet-height <mm>] [--kerf <mm>] [--param Key=Value] [--joint JointName=Value]"
|
|
37664
39931
|
],
|
|
37665
39932
|
examples: [
|
|
37666
39933
|
"forgecad export cutting-layout examples/api/sheet-stock-cut-list.forge.js",
|
|
@@ -37670,7 +39937,7 @@ var commands = [
|
|
|
37670
39937
|
],
|
|
37671
39938
|
completion: {
|
|
37672
39939
|
options: [
|
|
37673
|
-
...
|
|
39940
|
+
...PARAM_OPTIONS,
|
|
37674
39941
|
{ name: "--output", description: "Output PDF or DXF path (single input only)", argument: "required", valueLabel: "<path>" },
|
|
37675
39942
|
{ name: "--out", description: "Alias for --output", argument: "required", valueLabel: "<path>" },
|
|
37676
39943
|
{
|
|
@@ -38423,23 +40690,145 @@ var commands = [
|
|
|
38423
40690
|
path: ["check", "simready"],
|
|
38424
40691
|
summary: "Validate source-authored robot and physics metadata before simulation export.",
|
|
38425
40692
|
description: "Runs a Forge script and checks the returned `assembly(...).withSimulation(...)` contract without Isaac Sim, OpenUSD, or NVIDIA validators. The gate validates Sim.body metadata, explicit colliders, contact connectors, controller joints, numeric physics values, and the robot joint graph.",
|
|
38426
|
-
usage: ["forgecad check simready <script.forge.js> [input ...] [--json] [--joint JointName=Value]"],
|
|
40693
|
+
usage: ["forgecad check simready <script.forge.js> [input ...] [--json] [--param Key=Value] [--joint JointName=Value]"],
|
|
38427
40694
|
examples: [
|
|
38428
40695
|
"forgecad check simready examples/robotics/simready-diff-drive-rover.forge.js",
|
|
38429
40696
|
"forgecad check simready examples/robotics/simready-diff-drive-rover.forge.js path/to/second-robot.forge.js",
|
|
38430
|
-
"forgecad check simready robot.forge.js --json"
|
|
40697
|
+
"forgecad check simready robot.forge.js --param Wheelbase=180 --json"
|
|
38431
40698
|
],
|
|
38432
40699
|
completion: {
|
|
38433
40700
|
options: [
|
|
40701
|
+
...PARAM_OPTIONS,
|
|
38434
40702
|
{ name: "--json", description: "Emit the machine-readable report to stdout" },
|
|
38435
40703
|
{ name: "--compact", description: "Minify JSON output" },
|
|
38436
|
-
{ name: "--no-fail-exit", description: "Always exit 0 after writing the report" }
|
|
38437
|
-
...JOINT_OPTIONS
|
|
40704
|
+
{ name: "--no-fail-exit", description: "Always exit 0 after writing the report" }
|
|
38438
40705
|
],
|
|
38439
40706
|
positionals: [{ description: "Forge script", valueKind: "forge-script", repeatable: true }]
|
|
38440
40707
|
},
|
|
38441
40708
|
run: runCheckSimReadyCli
|
|
38442
40709
|
},
|
|
40710
|
+
{
|
|
40711
|
+
group: "Inspect",
|
|
40712
|
+
path: ["sim", "mass"],
|
|
40713
|
+
summary: "Compute mass properties: volume, mass, center of mass, inertia tensor, principal moments.",
|
|
40714
|
+
description: "Native-analytic mass-properties analysis computed directly from the model geometry \u2014 no external solver. Integrates the triangulated mesh (Mirtich/Eberly divergence theorem) to report volume, surface area, bounding box, mass-weighted center of mass, the full inertia tensor about the center of mass, principal moments and axes, and a per-object breakdown.\n\nVolume, center of mass, and principal axes are exact regardless of material. Pass `--density <kg/m3>` for accurate mass and inertia magnitude (default 1000 kg/m\xB3 water, reported as a warning). Emits the `forgecad.feedback/v1` JSON contract with `--json`.",
|
|
40715
|
+
usage: ["forgecad sim mass <model.forge.js> [input ...] [--density kg/m3] [--json] [--param Key=Value] [--joint JointName=Value]"],
|
|
40716
|
+
examples: [
|
|
40717
|
+
"forgecad sim mass examples/analysis/tipping-tripod.forge.js",
|
|
40718
|
+
"forgecad sim mass bracket.forge.js --density 2700 --json",
|
|
40719
|
+
"forgecad sim mass rover.forge.js --param Wheelbase=180 --json"
|
|
40720
|
+
],
|
|
40721
|
+
completion: {
|
|
40722
|
+
options: [
|
|
40723
|
+
...PARAM_OPTIONS,
|
|
40724
|
+
{ name: "--density", description: "Uniform density in kg/m\xB3 for mass and inertia", argument: "required", valueLabel: "<kg/m3>" },
|
|
40725
|
+
{ name: "--json", description: "Emit the forgecad.feedback/v1 report to stdout" },
|
|
40726
|
+
{ name: "--compact", description: "Minify JSON output" }
|
|
40727
|
+
],
|
|
40728
|
+
positionals: [{ description: "Forge script", valueKind: "forge-script", repeatable: true }]
|
|
40729
|
+
},
|
|
40730
|
+
run: runSimMassCli
|
|
40731
|
+
},
|
|
40732
|
+
{
|
|
40733
|
+
group: "Inspect",
|
|
40734
|
+
path: ["sim", "tolerance"],
|
|
40735
|
+
summary: "Tolerance / GD&T stack-up: yield, Cp/Cpk, and which dimension to tighten.",
|
|
40736
|
+
description: "Native-analytic tolerance stack-up \u2014 no external solver. Treats the model's continuous parameters as the varying manufacturing inputs and its numeric `verify.*` results as the measured responses (with spec limits from the verify call or `--spec`). Builds a finite-difference Jacobian, runs a deterministic Monte Carlo over the linearized surrogate, and reports per-response mean/sigma, Cp/Cpk, yield % and DPMO, worst-case and RSS envelopes, a ranked variance-contribution list, and a tolerance-allocation recommendation to reach the target Cpk.\n\nA nonlinearity guard errors (telling you to pass `--method full-rebuild`) rather than silently reporting a wrong yield. Exits non-zero when any response's Cpk is below `--target-cpk` (default 1.33). Emits the `forgecad.feedback/v1` JSON contract with `--json`.",
|
|
40737
|
+
usage: ["forgecad sim tolerance <model.forge.js> [--tol Param=\xB1X] [--spec Resp=lo..hi] [--method linearized|full-rebuild] [--json]"],
|
|
40738
|
+
examples: [
|
|
40739
|
+
"forgecad sim tolerance examples/analysis/clearance-fit.forge.js --json",
|
|
40740
|
+
"forgecad sim tolerance fit.forge.js --tol BoreDia=\xB10.02 --target-cpk 1.33 --json",
|
|
40741
|
+
"forgecad sim tolerance fit.forge.js --method full-rebuild --samples 5000 --json"
|
|
40742
|
+
],
|
|
40743
|
+
completion: {
|
|
40744
|
+
options: [
|
|
40745
|
+
...PARAM_OPTIONS,
|
|
40746
|
+
{ name: "--method", description: "linearized (default) or full-rebuild", argument: "required", valueLabel: "<linearized|full-rebuild>" },
|
|
40747
|
+
{ name: "--samples", description: "Monte Carlo sample count", argument: "required", valueLabel: "<n>" },
|
|
40748
|
+
{ name: "--seed", description: "Deterministic RNG seed", argument: "required", valueLabel: "<n>" },
|
|
40749
|
+
{ name: "--sigma", description: "Tolerance band = N sigma (default 3)", argument: "required", valueLabel: "<n>" },
|
|
40750
|
+
{ name: "--target-cpk", description: "Target Cpk for pass/allocation (default 1.33)", argument: "required", valueLabel: "<n>" },
|
|
40751
|
+
{ name: "--tol", description: "Override an input band: Param=\xB1X or Param=lo..hi", argument: "required", valueLabel: "<spec>" },
|
|
40752
|
+
{ name: "--dist", description: "Input distribution: Param=normal|uniform", argument: "required", valueLabel: "<spec>" },
|
|
40753
|
+
{ name: "--spec", description: "Declare response spec limits: Resp=lsl..usl (* for open)", argument: "required", valueLabel: "<spec>" },
|
|
40754
|
+
{ name: "--nonlinearity-tol", description: "Curvature threshold tripping the guard (default 0.05)", argument: "required", valueLabel: "<n>" },
|
|
40755
|
+
{ name: "--json", description: "Emit the forgecad.feedback/v1 report to stdout" },
|
|
40756
|
+
{ name: "--compact", description: "Minify JSON output" },
|
|
40757
|
+
{ name: "--no-fail-exit", description: "Always exit 0 after writing the report" }
|
|
40758
|
+
],
|
|
40759
|
+
positionals: [{ description: "Forge script", valueKind: "forge-script", repeatable: true }]
|
|
40760
|
+
},
|
|
40761
|
+
run: runSimToleranceCli
|
|
40762
|
+
},
|
|
40763
|
+
{
|
|
40764
|
+
group: "Inspect",
|
|
40765
|
+
path: ["sim", "mechanism"],
|
|
40766
|
+
summary: "Mechanism budget: DOF, and the gravity-hold torque each joint must supply.",
|
|
40767
|
+
description: "Native-analytic mechanism analysis from the assembly joint graph \u2014 no external solver, no time-stepping. Reports the actuated degrees of freedom (and a spatial Gr\xFCbler mobility diagnostic for closed loops), and for each actuated joint the gravity-hold torque (revolute) or force (prismatic) it must supply at the current pose, computed from the mass of the distal subtree. When a joint carries a `Sim.drive` effort budget, the report includes the margin and flags over-budget joints.\n\nThis is the deterministic budget tier; the dynamic/contact tier is `export pinocchio`. Exits non-zero when any joint exceeds its budget (or falls below `--min-torque-margin-pct`). Use `--joint Name=Value` to evaluate a specific pose. Emits the `forgecad.feedback/v1` JSON contract with `--json`.",
|
|
40768
|
+
usage: ["forgecad sim mechanism <model.forge.js> [--min-torque-margin-pct N] [--joint Name=Value] [--json]"],
|
|
40769
|
+
examples: [
|
|
40770
|
+
"forgecad sim mechanism examples/analysis/lever-arm-actuator.forge.js --json",
|
|
40771
|
+
"forgecad sim mechanism arm.forge.js --joint shoulder=45 --json",
|
|
40772
|
+
"forgecad sim mechanism arm.forge.js --min-torque-margin-pct 20 --json"
|
|
40773
|
+
],
|
|
40774
|
+
completion: {
|
|
40775
|
+
options: [
|
|
40776
|
+
...PARAM_OPTIONS,
|
|
40777
|
+
{ name: "--min-torque-margin-pct", description: "Required actuator budget margin %", argument: "required", valueLabel: "<pct>" },
|
|
40778
|
+
{ name: "--density", description: "Fallback density kg/m\xB3 for parts without Sim.body mass", argument: "required", valueLabel: "<kg/m3>" },
|
|
40779
|
+
{ name: "--json", description: "Emit the forgecad.feedback/v1 report to stdout" },
|
|
40780
|
+
{ name: "--compact", description: "Minify JSON output" },
|
|
40781
|
+
{ name: "--no-fail-exit", description: "Always exit 0 after writing the report" }
|
|
40782
|
+
],
|
|
40783
|
+
positionals: [{ description: "Forge script", valueKind: "forge-script", repeatable: true }]
|
|
40784
|
+
},
|
|
40785
|
+
run: runSimMechanismCli
|
|
40786
|
+
},
|
|
40787
|
+
{
|
|
40788
|
+
group: "Inspect",
|
|
40789
|
+
path: ["sim", "dynamics"],
|
|
40790
|
+
summary: "Read back a Pinocchio dynamics run (feedback.json) as the forgecad.feedback/v1 contract.",
|
|
40791
|
+
description: "Ingests the `feedback.json` written by an exported Pinocchio package's `run_pinocchio.py` and re-surfaces it through ForgeCAD, closing the read-back side of the dynamics loop. Pass the package directory (or the feedback.json directly). Reports `DYNAMICS.NOT_RUN` when the results are missing, and exits non-zero when the run reported a failing metric.",
|
|
40792
|
+
usage: ["forgecad sim dynamics <pinocchio-package-dir|feedback.json> [--json] [--compact]"],
|
|
40793
|
+
examples: [
|
|
40794
|
+
"forgecad sim dynamics examples/analysis/lever-arm-actuator.pinocchiopkg",
|
|
40795
|
+
"forgecad sim dynamics out/robot_pinocchio/feedback.json --json"
|
|
40796
|
+
],
|
|
40797
|
+
completion: {
|
|
40798
|
+
options: [
|
|
40799
|
+
{ name: "--json", description: "Emit the forgecad.feedback/v1 report to stdout" },
|
|
40800
|
+
{ name: "--compact", description: "Minify JSON output" },
|
|
40801
|
+
{ name: "--no-fail-exit", description: "Always exit 0 after writing the report" }
|
|
40802
|
+
],
|
|
40803
|
+
positionals: [{ description: "Pinocchio package directory or feedback.json", valueKind: "path", repeatable: true }]
|
|
40804
|
+
},
|
|
40805
|
+
run: runSimDynamicsCli
|
|
40806
|
+
},
|
|
40807
|
+
{
|
|
40808
|
+
group: "Checks",
|
|
40809
|
+
path: ["check", "stability"],
|
|
40810
|
+
summary: "Check static tip-over stability: center of mass vs the support footprint.",
|
|
40811
|
+
description: "Native-analytic static stability check \u2014 no external solver. Computes the uniform-density center of mass, derives the support footprint as the convex hull of the lowest geometry, and reports the signed tip-over margin (distance from the CoM projection to the footprint edge; negative means it tips), the tilt angle at which it tips, the critical edge, and the center-of-mass shift needed to reach the required margin.\n\nExits non-zero when the margin is below `--min-margin-mm` (default 0). Use `--up x|y|z` to set the up axis (default z). Emits the `forgecad.feedback/v1` JSON contract with `--json`.",
|
|
40812
|
+
usage: ["forgecad check stability <model.forge.js> [input ...] [--min-margin-mm N] [--up x|y|z] [--json] [--param Key=Value]"],
|
|
40813
|
+
examples: [
|
|
40814
|
+
"forgecad check stability examples/analysis/tipping-tripod.forge.js",
|
|
40815
|
+
"forgecad check stability tower.forge.js --min-margin-mm 5 --json",
|
|
40816
|
+
"forgecad check stability tower.forge.js --param BaseSpreadMm=120 --json"
|
|
40817
|
+
],
|
|
40818
|
+
completion: {
|
|
40819
|
+
options: [
|
|
40820
|
+
...PARAM_OPTIONS,
|
|
40821
|
+
{ name: "--min-margin-mm", description: "Required signed tip-over margin in mm", argument: "required", valueLabel: "<mm>" },
|
|
40822
|
+
{ name: "--up", description: "Up axis (gravity is its negative)", argument: "required", valueLabel: "<x|y|z>" },
|
|
40823
|
+
{ name: "--contact-tol", description: "Footprint contact tolerance in mm", argument: "required", valueLabel: "<mm>" },
|
|
40824
|
+
{ name: "--json", description: "Emit the forgecad.feedback/v1 report to stdout" },
|
|
40825
|
+
{ name: "--compact", description: "Minify JSON output" },
|
|
40826
|
+
{ name: "--no-fail-exit", description: "Always exit 0 after writing the report" }
|
|
40827
|
+
],
|
|
40828
|
+
positionals: [{ description: "Forge script", valueKind: "forge-script", repeatable: true }]
|
|
40829
|
+
},
|
|
40830
|
+
run: runCheckStabilityCli
|
|
40831
|
+
},
|
|
38443
40832
|
{
|
|
38444
40833
|
group: "Checks",
|
|
38445
40834
|
path: ["check", "params"],
|
|
@@ -38805,7 +41194,7 @@ var commands = [
|
|
|
38805
41194
|
{ name: "--update", description: "Regenerate compiler snapshots" }
|
|
38806
41195
|
]
|
|
38807
41196
|
},
|
|
38808
|
-
run: async (args) => (await import("./check-compiler-
|
|
41197
|
+
run: async (args) => (await import("./check-compiler-HPF2T2FS.js")).runCheckCompilerCli(args)
|
|
38809
41198
|
},
|
|
38810
41199
|
{
|
|
38811
41200
|
group: "Checks",
|
|
@@ -38828,7 +41217,7 @@ var commands = [
|
|
|
38828
41217
|
{ name: "--update", description: "Regenerate query-propagation snapshots" }
|
|
38829
41218
|
]
|
|
38830
41219
|
},
|
|
38831
|
-
run: async (args) => (await import("./check-query-propagation-
|
|
41220
|
+
run: async (args) => (await import("./check-query-propagation-HYSLTXAB.js")).runCheckQueryPropagationCli(args)
|
|
38832
41221
|
},
|
|
38833
41222
|
{
|
|
38834
41223
|
group: "Checks",
|
|
@@ -39057,7 +41446,7 @@ var commands = [
|
|
|
39057
41446
|
];
|
|
39058
41447
|
function readVersion() {
|
|
39059
41448
|
try {
|
|
39060
|
-
const pkg = JSON.parse(
|
|
41449
|
+
const pkg = JSON.parse(readFileSync28(resolvePackagePath(import.meta.url, "package.json"), "utf-8"));
|
|
39061
41450
|
return pkg.version || "0.0.0";
|
|
39062
41451
|
} catch {
|
|
39063
41452
|
return "0.0.0";
|
|
@@ -39555,7 +41944,7 @@ function oclifCommandTarget() {
|
|
|
39555
41944
|
return import.meta.url.includes("/dist-cli/") ? "./dist-cli/forgecad.js" : "./cli/forgecad.ts";
|
|
39556
41945
|
}
|
|
39557
41946
|
function forgeCadOclifPjson() {
|
|
39558
|
-
const pkg = JSON.parse(
|
|
41947
|
+
const pkg = JSON.parse(readFileSync28(resolvePackagePath(import.meta.url, "package.json"), "utf-8"));
|
|
39559
41948
|
return {
|
|
39560
41949
|
...pkg,
|
|
39561
41950
|
oclif: {
|