Add INI-driven XYZAC and XYZBC parametric machine design
This commit is contained in:
File diff suppressed because one or more lines are too long
69
web/react/src/TrtGeometryPanel.tsx
Normal file
69
web/react/src/TrtGeometryPanel.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { readTrtGeometry, writeTrtGeometry, TRT_GEOMETRY_KEYS } from "../../workspace/trt-geometry.mjs";
|
||||
import { axisIcon } from "./sourceAssets";
|
||||
|
||||
type Bundle = { entrypoint: string; files: Record<string, string>; source_ini?: string };
|
||||
type Props = { bundle: Bundle | null; disabled: boolean; busy: boolean; onApply: (bundle: Bundle) => Promise<void>; onReload: () => Promise<void> };
|
||||
|
||||
export function TrtGeometryPanel({ bundle, disabled, busy, onApply, onReload }: Props) {
|
||||
const [values, setValues] = useState<Record<string, string>>({});
|
||||
const [message, setMessage] = useState("");
|
||||
const [pending, setPending] = useState(false);
|
||||
const file = useRef<HTMLInputElement>(null);
|
||||
const geometry = useMemo(() => bundle ? readTrtGeometry(bundle) : null, [bundle]);
|
||||
useEffect(() => {
|
||||
setValues(Object.fromEntries(TRT_GEOMETRY_KEYS.map((key: string) => [key, String(geometry?.offsets[key] ?? 0)])));
|
||||
setMessage("");
|
||||
}, [bundle]);
|
||||
const locked = disabled || busy || pending || !bundle;
|
||||
const dirty = TRT_GEOMETRY_KEYS.some((key: string) => values[key] !== String(geometry?.offsets[key] ?? 0));
|
||||
const run = async (action: () => Promise<void>, success: string) => {
|
||||
setPending(true); setMessage("");
|
||||
try { await action(); setMessage(success); }
|
||||
catch (error) { setMessage(error instanceof Error ? error.message : String(error)); }
|
||||
finally { setPending(false); }
|
||||
};
|
||||
const edited = () => {
|
||||
if (!bundle) throw new Error("INI unavailable");
|
||||
if (Object.values(geometry?.sources ?? {}).some((source) => source.path !== bundle.entrypoint)) throw new Error("参数位于包含文件,请修改源文件后重新读取 INI");
|
||||
const next = writeTrtGeometry(bundle, values);
|
||||
// Export the native INI, retaining LIB:basic_sim.tcl instead of the Web fixture.
|
||||
next.source_ini = writeTrtGeometry({ ...bundle, files: { ...bundle.files, [bundle.entrypoint]: bundle.source_ini ?? bundle.files[bundle.entrypoint] } }, values).files[bundle.entrypoint];
|
||||
return next;
|
||||
};
|
||||
const exportIni = async () => {
|
||||
const next = edited();
|
||||
const content = next.source_ini ?? next.files[next.entrypoint];
|
||||
const name = next.entrypoint.split("/").at(-1)!;
|
||||
const picker = (window as unknown as { showSaveFilePicker?: (options: unknown) => Promise<{ createWritable: () => Promise<{ write: (data: string) => Promise<void>; close: () => Promise<void> }> }> }).showSaveFilePicker;
|
||||
if (picker) {
|
||||
const handle = await picker({ suggestedName: name, types: [{ description: "LinuxCNC INI", accept: { "text/plain": [".ini"] } }] });
|
||||
const stream = await handle.createWritable(); await stream.write(content); await stream.close();
|
||||
} else {
|
||||
const url = URL.createObjectURL(new Blob([content], { type: "text/plain" }));
|
||||
const link = document.createElement("a"); link.href = url; link.download = name; link.click();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
}
|
||||
};
|
||||
const tool = (id: string, label: string, asset: string, action: () => void, off = locked) => <button type="button" data-axis-control={`geometry-${id}`} aria-label={label} title={label} disabled={off} onClick={action}><img src={axisIcon(asset)} alt="" /></button>;
|
||||
return <section className="trt-geometry" aria-label="机床几何参数" aria-busy={pending || busy} data-dirty={dirty}>
|
||||
<header><strong>{bundle?.entrypoint.split("/").at(-1) ?? "INI"}</strong><span>mm</span><output>{dirty ? "待应用" : "已生效"}</output></header>
|
||||
<div className="trt-geometry-fields">{["旋转中心", "轴间偏置"].map((title, index) => <fieldset key={title}><legend>{title}</legend>{TRT_GEOMETRY_KEYS.slice(index * 3, index * 3 + 3).map((key: string) => <label key={key} title={key === geometry?.unsupported ? "此机型原生运动学不使用该偏置" : key.replaceAll("_", "-")}><span>{key.replaceAll("_", "-")}</span><input aria-label={key.replaceAll("_", "-")} type="number" step="any" disabled={locked || key === geometry?.unsupported} value={values[key] ?? "0"} onChange={(e) => setValues({ ...values, [key]: e.target.value })} /></label>)}</fieldset>)}</div>
|
||||
<footer>
|
||||
<button type="button" data-axis-control="geometry-apply" disabled={locked || !dirty} onClick={() => void run(() => onApply(edited()), "参数已应用,待重新回零")}><img src={axisIcon("tool_verify.gif")} alt="" />应用参数</button>
|
||||
{tool("reload", "重新读取 INI", "tool_reload.gif", () => void run(onReload, "已重新读取 INI"))}
|
||||
{tool("import", "导入 INI 参数", "tool_open.gif", () => file.current?.click())}
|
||||
<button type="button" data-axis-control="geometry-save" disabled={locked} onClick={() => void run(exportIni, "INI 已保存或导出")}>保存 INI</button>
|
||||
<output role="status">{message || (disabled ? "关机且程序空闲后可修改" : "")}</output>
|
||||
</footer>
|
||||
<input ref={file} type="file" accept=".ini" hidden onChange={(event) => {
|
||||
const selected = event.target.files?.[0]; event.target.value = "";
|
||||
if (!selected || !bundle || locked) return;
|
||||
void run(async () => {
|
||||
if (selected.name !== bundle.entrypoint.split("/").at(-1)) throw new Error("INI 文件名与机型不一致");
|
||||
const imported = readTrtGeometry({ ...bundle, files: { ...bundle.files, [bundle.entrypoint]: await selected.text() } });
|
||||
setValues(Object.fromEntries(TRT_GEOMETRY_KEYS.map((key: string) => [key, String(imported.offsets[key])])));
|
||||
}, "INI 参数已导入,待应用");
|
||||
}} />
|
||||
</section>;
|
||||
}
|
||||
@@ -145,6 +145,8 @@ export function XyzacMachineView({ visual, toolpath, view = "p", zoom = 1, rotat
|
||||
const state = visualRef.current, config = settings.current;
|
||||
assembly.update(state);
|
||||
const paths = (config.toolpath ?? state.toolpath) as Segment[];
|
||||
pathGroup.position.copy(assembly.workOrigin.position);
|
||||
liveGroup.position.copy(assembly.workOrigin.position);
|
||||
const pathKey = `${config.showPath}:${config.showLive}:${state.activeSegmentId}`;
|
||||
if (paths !== lastGeometry || pathKey !== lastPathKey) {
|
||||
clearLines(pathGroup); clearLines(liveGroup);
|
||||
@@ -202,5 +204,6 @@ export function XyzacMachineView({ visual, toolpath, view = "p", zoom = 1, rotat
|
||||
|
||||
export function XyzacMachineViewFromSnapshot({ snapshot, ...props }: Omit<Props, "visual"> & { snapshot: XyzacVisualSource | null | undefined }) {
|
||||
const visual = projectXyzacVisual(snapshot);
|
||||
return <XyzacMachineView key={`${visual.machineModel?.id ?? "machine-model-pending"}:${visual.machineModel?.source_hash ?? ""}`} visual={visual} {...props} />;
|
||||
const geometryKey = [visual.offsets.x_rot_point, visual.offsets.y_rot_point, visual.offsets.z_rot_point, visual.offsets.x_offset, visual.offsets.y_offset, visual.offsets.z_offset].join(":");
|
||||
return <XyzacMachineView key={`${visual.machineModel?.id ?? "machine-model-pending"}:${visual.machineModel?.source_hash ?? ""}:${geometryKey}`} visual={visual} {...props} />;
|
||||
}
|
||||
|
||||
3
web/react/src/legacy.d.ts
vendored
3
web/react/src/legacy.d.ts
vendored
@@ -1,4 +1,7 @@
|
||||
declare module "*.mjs" {
|
||||
export const TRT_GEOMETRY_KEYS: readonly string[];
|
||||
export function readTrtGeometry(configuration: { entrypoint: string; files: Record<string, string> }): { coordinates: string; offsets: Record<string, number>; sources: Record<string, { path: string; line: number; value: string }>; unsupported: string; state_hash: string };
|
||||
export function writeTrtGeometry<T extends { entrypoint: string; files: Record<string, string> }>(configuration: T, values: Record<string, string | number>): T;
|
||||
export type MachineDefinition = { id: string; label: string; coordinates: string; axes: readonly string[]; joint_count: number; entrypoint: string; program: string; switchkins_program: string; kinematics_type: number; runtime: string; model: string; source_path: string; offsets: Record<string, number>; joint_limits: Record<string, number[]>; axis_limits: Record<string, number[]> };
|
||||
export const MACHINE_DEFINITIONS: readonly MachineDefinition[];
|
||||
export const DEFAULT_MACHINE: MachineDefinition;
|
||||
|
||||
@@ -173,6 +173,23 @@ output { font: 12px ui-monospace, monospace; text-align: right; }
|
||||
.axis-preview { display: flex; min-height: 0; flex: 1; padding: 4px; overflow: hidden; background: #000; }
|
||||
.axis-preview-split { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 4px; width: 100%; min-width: 0; min-height: 0; }
|
||||
.axis-preview-pane { display: flex; min-width: 0; min-height: 0; overflow: hidden; }
|
||||
.axis-preview-machine { flex-direction: column; }
|
||||
.axis-preview-machine > .three-viewer { height: auto; min-height: 220px; }
|
||||
.trt-geometry { flex: 0 0 auto; background: #edf0f0; color: #202829; padding: 6px; font-size: 11px; border-top: 1px solid #899596; }
|
||||
.trt-geometry header, .trt-geometry footer { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }
|
||||
.trt-geometry header strong { overflow-wrap: anywhere; }
|
||||
.trt-geometry header output { margin-left: auto; color: #276557; }
|
||||
.trt-geometry[data-dirty="true"] header output { color: #955015; }
|
||||
.trt-geometry-fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin: 5px 0; }
|
||||
.trt-geometry fieldset { border: 0; padding: 0; min-width: 0; }
|
||||
.trt-geometry legend { color: #586467; padding: 0 0 3px; }
|
||||
.trt-geometry label { display: grid; grid-template-columns: minmax(0, 1fr) 66px; align-items: center; gap: 3px; margin: 3px 0; }
|
||||
.trt-geometry label span { overflow-wrap: anywhere; }
|
||||
.trt-geometry input { width: 100%; min-width: 0; height: 24px; padding: 2px 3px; border: 1px solid #a1adae; background: #fff; color: #202829; }
|
||||
.trt-geometry input:disabled { color: #879293; background: #e4e8e8; }
|
||||
.trt-geometry footer button { display: inline-flex; align-items: center; justify-content: center; gap: 4px; height: 27px; min-width: 28px; padding: 3px 5px; }
|
||||
.trt-geometry button img { width: 18px; height: 18px; object-fit: contain; }
|
||||
.trt-geometry footer output { flex: 1 1 100px; min-width: 0; overflow-wrap: anywhere; }
|
||||
.axis-preview-pane > .three-viewer { min-width: 0; min-height: 0; }
|
||||
.three-viewer { position: relative; width: 100%; height: 100%; min-height: 0; flex: 1; overflow: hidden; border: 1px solid #303030; background: #000; }
|
||||
.three-viewer canvas { display: block; width: 100%; height: 100%; }
|
||||
@@ -258,7 +275,7 @@ output { font: 12px ui-monospace, monospace; text-align: right; }
|
||||
.axis-top-pane { grid-template-columns: 1fr; height: auto; min-height: 0; flex: 0 0 auto; }
|
||||
.axis-notebook { min-height: 330px; }
|
||||
.axis-preview { min-height: 732px; }
|
||||
.axis-preview-split { grid-template-columns: 1fr; grid-template-rows: repeat(2, 360px); align-self: start; }
|
||||
.axis-preview-split { grid-template-columns: 1fr; grid-template-rows: 360px 550px; align-self: start; }
|
||||
.axis-program-pane { flex: 0 0 auto; height: 740px; min-height: 740px; grid-template-columns: 1fr; grid-template-rows: minmax(0, 1fr) 140px; }
|
||||
.program-main-layout { grid-template-columns: 1fr; grid-template-rows: minmax(0, 1fr) minmax(0, 1fr); grid-column: 1; grid-row: 1; overflow: hidden; }
|
||||
.program-editor-wrap { grid-column: 1; grid-row: 1; }
|
||||
|
||||
128
web/react/src/test_trt_parametric.mjs
Normal file
128
web/react/src/test_trt_parametric.mjs
Normal file
@@ -0,0 +1,128 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile, mkdir, writeFile } from "node:fs/promises";
|
||||
import * as THREE from "three";
|
||||
import createModule from "../../../build-wasm/linuxcnc-sim-core.js";
|
||||
import { readTrtGeometry, writeTrtGeometry, TRT_GEOMETRY_KEYS } from "../../workspace/trt-geometry.mjs";
|
||||
import { loadSimulationCase, toWorkerConfiguration } from "../../workspace/sim-case-loader.mjs";
|
||||
import { MACHINE_DEFINITIONS } from "../../workspace/machine-definitions.mjs";
|
||||
import { SimSession, CONTEXT_ID } from "../../worker/sim-session.mjs";
|
||||
import { fnv1a64 } from "../../worker/replay.mjs";
|
||||
import { buildXyzacMachine, buildXyzbcMachine } from "./xyzac-machine-model.ts";
|
||||
|
||||
const root = new URL("../../../", import.meta.url);
|
||||
const fetchImpl = async (url) => {
|
||||
try { const body = await readFile(new URL(String(url).replace(/^\//, ""), root), "utf8"); return { ok: true, text: async () => body }; }
|
||||
catch { return { ok: false }; }
|
||||
};
|
||||
const wasmBinary = await readFile(new URL("build-wasm/linuxcnc-sim-core.wasm", root));
|
||||
const close = (a, b) => assert.ok(Math.abs(a - b) < 1e-7, `${a} != ${b}`);
|
||||
const rows = [];
|
||||
async function execute(bundle, source) {
|
||||
const core = await createModule({ wasmBinary });
|
||||
const session = new SimSession(core, { sessionId: "trt-parametric" }); let seq = 0;
|
||||
const send = (kind, payload = {}) => session.accept({ schema_version: 1, context_id: CONTEXT_ID, session_id: session.sessionId, kind, seq: ++seq, operation_id: `param-${seq}`, payload });
|
||||
send("HELLO"); send("SESSION_ATTACH");
|
||||
const configuration = toWorkerConfiguration(bundle);
|
||||
const expected = readTrtGeometry(bundle).offsets;
|
||||
configuration.kinematics.offsets.x_rot_point = 999; // Files must win at the Worker boundary.
|
||||
const loaded = send("LOAD_WORKSPACE", { root_path: "/workspace", configuration });
|
||||
assert.equal(loaded.kind, "COMMAND_RESULT", JSON.stringify(loaded.payload));
|
||||
assert.deepEqual(loaded.payload.panel_snapshot.kinematics.offsets, expected);
|
||||
assert.equal(loaded.payload.panel_snapshot.visual.toolpath.length, 0);
|
||||
assert.equal(session.wasmHomeLimit.read().state.homed, false);
|
||||
const malformed = structuredClone(configuration);
|
||||
malformed.files[bundle.entrypoint] = malformed.files[bundle.entrypoint].replace(/(x-rot-point\s+)[^\r\n;#]+/, "$1NaN");
|
||||
assert.equal(send("LOAD_WORKSPACE", { root_path: "/workspace", configuration: malformed }).kind, "ERROR");
|
||||
assert.deepEqual(session.xyzacKinematics.snapshot().offsets, expected);
|
||||
const preview = send("PROGRAM_LOAD", { source, path: "parametric.ngc" });
|
||||
assert.equal(preview.kind, "COMMAND_RESULT", JSON.stringify(preview.payload));
|
||||
assert.ok(preview.payload.preview.segments.length > 0);
|
||||
assert.equal(send("POWER", { active: true }).kind, "COMMAND_RESULT");
|
||||
assert.equal(send("LOAD_WORKSPACE", { root_path: "/workspace", configuration }).kind, "ERROR");
|
||||
assert.equal(send("HOME", { all: true }).kind, "COMMAND_RESULT");
|
||||
for (let i = 0; i < 500 && !session.wasmHomeLimit.read().state.homed; i++) send("SIM_STEP", { cycles: 1 });
|
||||
assert.equal(session.wasmHomeLimit.read().state.homed, true);
|
||||
assert.equal(send("PROGRAM_RUN").kind, "COMMAND_RESULT");
|
||||
assert.equal(send("PROGRAM_PAUSE").kind, "COMMAND_RESULT");
|
||||
assert.equal(send("LOAD_WORKSPACE", { root_path: "/workspace", configuration }).kind, "ERROR");
|
||||
assert.equal(send("PROGRAM_RESUME").kind, "COMMAND_RESULT");
|
||||
const trace = [];
|
||||
let result;
|
||||
for (let i = 0; i < 4000; i++) {
|
||||
result = send("SIM_STEP", { cycles: 10 });
|
||||
assert.equal(result.kind, "COMMAND_RESULT", JSON.stringify(result.payload));
|
||||
trace.push({ tick: result.simulation_tick, pose: result.payload.wasm_motion.state.position, joints: result.payload.wasm_motion.state.joint_commanded });
|
||||
if (result.payload.program.program_status === "completed" && result.payload.wasm_motion.state.queue_depth === 0) break;
|
||||
}
|
||||
assert.equal(result.payload.program.program_status, "completed");
|
||||
const coordinates = readTrtGeometry(bundle).coordinates;
|
||||
const model = coordinates === "XYZAC" ? buildXyzacMachine() : buildXyzbcMachine();
|
||||
const samples = [[0, 0], [20, 0], [0, 30], [-25, 45]];
|
||||
for (const [tilt, c] of samples) {
|
||||
const pose = { x: 12, y: 8, z: 6, a: 0, b: 0, c, u: 0, v: 0, w: 0, [coordinates[3].toLowerCase()]: tilt };
|
||||
const inverse = session.xyzacKinematics.inverseMode(1, pose);
|
||||
assert.equal(inverse.status, "verified");
|
||||
model.update({ joints: inverse.joints, offsets: expected });
|
||||
model.machine.updateMatrixWorld(true);
|
||||
const point = model.workOrigin.localToWorld(new THREE.Vector3(pose.x * .01, pose.z * .01, pose.y * .01));
|
||||
const tip = model.toolTip.getWorldPosition(new THREE.Vector3());
|
||||
close(point.x, tip.x); close(point.y, tip.y); close(point.z, tip.z);
|
||||
}
|
||||
model.dispose();
|
||||
send("POWER", { active: false });
|
||||
const savedCheckpoint = session.createCheckpoint();
|
||||
const savedCompact = session.createPowerOffCheckpoint();
|
||||
const savedTrace = session.exportTrace();
|
||||
const reloaded = send("LOAD_WORKSPACE", { root_path: "/workspace", configuration });
|
||||
assert.equal(reloaded.kind, "COMMAND_RESULT");
|
||||
assert.equal(session.wasmHomeLimit.read().state.homed, false);
|
||||
assert.deepEqual(reloaded.payload.panel_snapshot.visual.toolpath, []);
|
||||
const other = toWorkerConfiguration(writeTrtGeometry(bundle, { x_rot_point: expected.x_rot_point + 1 }));
|
||||
assert.equal(send("LOAD_WORKSPACE", { root_path: "/workspace", configuration: other }).kind, "COMMAND_RESULT");
|
||||
const beforeRecovery = session.xyzacKinematics.snapshot();
|
||||
assert.equal(session.restoreCheckpoint(savedCheckpoint).status, "blocked");
|
||||
assert.equal(session.restoreCompactPowerOff(savedCompact, seq + 1).status, "blocked");
|
||||
assert.throws(() => session.importTrace(savedTrace), /trt_recovery_geometry_mismatch/);
|
||||
assert.deepEqual(session.xyzacKinematics.snapshot(), beforeRecovery);
|
||||
assert.equal(session.wasmHomeLimit.read().state.homed, false);
|
||||
return { trace, offsets: expected, final: trace.at(-1), preview_hash: fnv1a64(preview.payload.preview), trace_hash: fnv1a64(trace), model_samples: samples.length };
|
||||
}
|
||||
|
||||
for (const machine of MACHINE_DEFINITIONS) {
|
||||
const bundle = await loadSimulationCase({ path: machine.entrypoint, fetchImpl });
|
||||
assert.equal(bundle.status, "ready");
|
||||
const initial = readTrtGeometry(bundle);
|
||||
const values = { ...initial.offsets, x_rot_point: 9, y_rot_point: -7, z_rot_point: 13, z_offset: -4, [machine.coordinates === "XYZAC" ? "y_offset" : "x_offset"]: 15 };
|
||||
const changed = writeTrtGeometry(bundle, values);
|
||||
assert.notEqual(changed.state_hash, bundle.state_hash);
|
||||
assert.deepEqual(readTrtGeometry(changed).offsets, values);
|
||||
for (const bad of ["", "NaN", "Infinity", "1e309", "1+2", "0x10"]) assert.throws(() => writeTrtGeometry(bundle, { x_rot_point: bad }), /trt_geometry_number/);
|
||||
assert.throws(() => writeTrtGeometry(bundle, { [initial.unsupported]: 1 }), /trt_geometry_unsupported/);
|
||||
const edit = (fn) => ({ ...bundle, files: { ...bundle.files, [bundle.entrypoint]: fn(bundle.files[bundle.entrypoint]) } });
|
||||
assert.throws(() => readTrtGeometry(edit((s) => s.replace(/(HALCMD = setp [^\n]*x-rot-point[^\n]*)/, "$1\n$1"))), /trt_geometry_duplicate/);
|
||||
assert.throws(() => readTrtGeometry(edit((s) => s.replace(/HALCMD = setp [^\n]*x-rot-point[^\n]*\n/, ""))), /trt_geometry_missing/);
|
||||
const crlf = edit((s) => s.replaceAll("\n", "\r\n"));
|
||||
assert.ok(writeTrtGeometry(crlf, values).files[bundle.entrypoint].includes("\r\n"));
|
||||
const referenced = edit((s) => s.replace(/(x-rot-point\s+)[^\r\n]+/, "$1[GEOMETRY]X\n").concat("\n[GEOMETRY]\nX = 4 # retained comment\n"));
|
||||
const referencedEdit = writeTrtGeometry(referenced, { x_rot_point: 7 });
|
||||
assert.equal(readTrtGeometry(referencedEdit).offsets.x_rot_point, 7);
|
||||
assert.ok(referencedEdit.files[bundle.entrypoint].includes("X = 7 # retained comment"));
|
||||
assert.ok(bundle.source_ini.includes("LIB:basic_sim.tcl"));
|
||||
for (const key of TRT_GEOMETRY_KEYS.filter((key) => key !== initial.unsupported)) {
|
||||
assert.equal(readTrtGeometry(writeTrtGeometry(bundle, { [key]: 3.25 })).offsets[key], 3.25);
|
||||
}
|
||||
const source = `G21 G90 G94\nM428\nG1 X12 Y8 Z6 ${machine.coordinates[3]}20 C30 F600\nM2\n`;
|
||||
const baseline = await execute(bundle, source);
|
||||
const modified = await execute(changed, source);
|
||||
const repeated = await execute(changed, source);
|
||||
assert.deepEqual(repeated.trace, modified.trace, "fixed tick replay must match");
|
||||
assert.notDeepEqual(baseline.final.joints, modified.final.joints, "INI must change program machine joints");
|
||||
assert.deepEqual(baseline.final.pose, modified.final.pose, "programming coordinates remain unchanged");
|
||||
const divergent = baseline.trace.findIndex((sample, i) => JSON.stringify(sample.joints) !== JSON.stringify(modified.trace[i]?.joints));
|
||||
assert.ok(divergent >= 0);
|
||||
rows.push({ machine: machine.id, source, baseline, modified, repeat_hash: repeated.trace_hash, first_divergent_tick: baseline.trace[divergent].tick });
|
||||
}
|
||||
const report = { report_id: "trt-parametric-worker-v1", status: "verified", rows };
|
||||
const dir = new URL("coverage/trt-parametric/", root); await mkdir(dir, { recursive: true });
|
||||
await writeFile(new URL("worker.report.json", dir), JSON.stringify({ ...report, state_hash: fnv1a64(report) }, null, 2) + "\n");
|
||||
console.log(JSON.stringify({ report_id: report.report_id, status: report.status, machines: rows.map((r) => ({ machine: r.machine, first_divergent_tick: r.first_divergent_tick, trace_hash: r.modified.trace_hash })) }));
|
||||
@@ -54,7 +54,8 @@ assert.ok(xyzbcModel.machine.getObjectByName("tilt-b"));
|
||||
assert.equal(xyzbcModel.machine.getObjectByName("tilt-a"), undefined);
|
||||
xyzbcModel.update({ joints: [0, 0, 0, 30, 45], offsets: { x_rot_point: 0, y_rot_point: 0, z_rot_point: 0, x_offset: -20, y_offset: 0, z_offset: -15, tool_offset: 0 } });
|
||||
close(xyzbcModel.joints.tilt.rotation.z, -30 * Math.PI / 180);
|
||||
assert.equal(xyzbcModel.joints.tilt.rotation.y, Math.PI / 2);
|
||||
assert.equal(xyzbcModel.joints.tilt.rotation.y, 0);
|
||||
assert.equal(xyzbcModel.machine.getObjectByName("tilting-casting").rotation.y, Math.PI / 2);
|
||||
xyzbcModel.dispose();
|
||||
const original = structuredClone(visual);
|
||||
const zero = { joints: [0, 0, 0, 0, 0], offsets: { x_rot_point: 0, y_offset: 20, z_offset: 10, tool_offset: 0 } };
|
||||
|
||||
@@ -153,15 +153,14 @@ function buildTrtMachine(coordinates = "XYZAC") {
|
||||
cylinder(brackets, `${rotaryAxis}-axis-end-cap`, -107, 0, 0, 16, 3, "dark", "x");
|
||||
|
||||
const tilt = group(tableX, `tilt-${rotaryAxis}`);
|
||||
// The XYZBC source model applies a fixed 90 degree Z rotation before the
|
||||
// B (source-Y) tilt. In the Three.js source-coordinate mapping that is a
|
||||
// fixed Y rotation followed by a Z-axis dynamic tilt.
|
||||
if (isXyzbc) tilt.rotation.y = Math.PI / 2;
|
||||
// Rotate the B-axis casting without rotating the machine-coordinate frame.
|
||||
const yoke = group(tilt, "tilting-casting");
|
||||
if (isXyzbc) yoke.rotation.y = Math.PI / 2;
|
||||
if (isXyzbc) brackets.rotation.y = Math.PI / 2;
|
||||
box(tilt, "yoke-plate", 0, 0, -30, 130, 80, 10, "teal", 2);
|
||||
box(yoke, "yoke-plate", 0, 0, -30, 130, 80, 10, "teal", 2);
|
||||
for (const x of [-60, 60]) {
|
||||
box(tilt, "yoke-side-plate", x, 0, -15, 10, 80, 34, "teal", 2);
|
||||
cylinder(tilt, `${rotaryAxis}-trunnion-shaft`, x, 0, 0, 10, 22, "steel", "x");
|
||||
box(yoke, "yoke-side-plate", x, 0, -15, 10, 80, 34, "teal", 2);
|
||||
cylinder(yoke, `${rotaryAxis}-trunnion-shaft`, x, 0, 0, 10, 22, "steel", "x");
|
||||
}
|
||||
const rotaryBase = group(tilt, "c-rotary-base");
|
||||
box(rotaryBase, "c-drive-housing", 0, 0, -24, 104, 100, 12, "dark", 3);
|
||||
@@ -194,6 +193,7 @@ function buildTrtMachine(coordinates = "XYZAC") {
|
||||
const xOffset = Number(state.offsets.x_offset ?? 0);
|
||||
const yOffset = Number(state.offsets.y_offset ?? 0);
|
||||
const zOffset = Number(state.offsets.z_offset ?? 0);
|
||||
const pivot = new THREE.Vector3(state.offsets.x_rot_point * MM, Number(state.offsets.z_rot_point ?? 0) * MM, Number(state.offsets.y_rot_point ?? 0) * MM);
|
||||
tableX.position.x = -tableXJoint * MM;
|
||||
saddle.position.z = -saddleYJoint * MM;
|
||||
if (coordinates === "XYZBC") {
|
||||
@@ -201,24 +201,23 @@ function buildTrtMachine(coordinates = "XYZAC") {
|
||||
// XYZBC is a table tilt: the source model rotates the B assembly
|
||||
// around machine Y. The Web scene maps source Y to Three.js Z.
|
||||
head.rotation.set(0, 0, 0);
|
||||
zslide.position.y = 0;
|
||||
zslide.position.y = 150 * MM;
|
||||
tilt.rotation.z = -tiltAJoint * DEG;
|
||||
// xyzbc-trt-gui applies x-offset to both sides of the B table and
|
||||
// applies z-offset with opposite signs to the inner/outer groups.
|
||||
tilt.position.set(xOffset * MM, zOffset * MM, yOffset * MM);
|
||||
tilt.position.set(xOffset * MM, zOffset * MM, 0).add(pivot);
|
||||
brackets.position.copy(tilt.position);
|
||||
rotaryBase.position.set(xOffset * MM, -zOffset * MM, 0);
|
||||
rotaryBase.position.set(-xOffset * MM, -zOffset * MM, 0);
|
||||
} else {
|
||||
zslide.position.y = (200 - spindleZJoint) * MM;
|
||||
head.position.y = 2;
|
||||
head.rotation.set(0, 0, 0);
|
||||
// XYZAC source transform: tilt.rotation.x = -tiltAJoint * DEG.
|
||||
tilt.rotation.x = -tiltAJoint * DEG;
|
||||
tilt.position.set(0, zOffset * MM, yOffset * MM);
|
||||
tilt.position.set(0, zOffset * MM, yOffset * MM).add(pivot);
|
||||
brackets.position.copy(tilt.position);
|
||||
rotaryBase.position.copy(tilt.position).negate();
|
||||
rotaryBase.position.set(0, -zOffset * MM, -yOffset * MM);
|
||||
}
|
||||
rotate.rotation.y = -rotateCJoint * DEG;
|
||||
workOrigin.position.copy(pivot).negate();
|
||||
tool.position.y = -state.offsets.tool_offset * MM;
|
||||
if (toolLength !== state.offsets.tool_offset) {
|
||||
toolLength = state.offsets.tool_offset;
|
||||
|
||||
Reference in New Issue
Block a user