diff --git a/scripts/test_xyzbc_panel_browser.mjs b/scripts/test_xyzbc_panel_browser.mjs index 73b54a8..448b1a0 100644 --- a/scripts/test_xyzbc_panel_browser.mjs +++ b/scripts/test_xyzbc_panel_browser.mjs @@ -16,7 +16,7 @@ try { await page.waitForTimeout(300); await control("machine-type-select").selectOption({ label: "XYZBC TRT" }); assert.equal(await control("machine-type-select").inputValue(), "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini"); - await page.waitForFunction(() => document.querySelector("main")?.dataset.axisProgramPath?.includes("xyzbc_switchkins.ngc") && document.querySelector("main")?.dataset.axisProgramLoading === "false", null, { timeout: 45000 }); + await page.waitForFunction(() => document.querySelector("main")?.dataset.axisProgramPath?.includes("boat-xyzbc.ngc") && document.querySelector("main")?.dataset.axisProgramLoading === "false", null, { timeout: 45000 }); const result = await page.evaluate(() => ({ machine: document.querySelector(".xyzac-manual-summary")?.getAttribute("aria-label"), axes: [...document.querySelectorAll(".manual-axis-block .axis-radio span")].map((node) => node.textContent), @@ -34,5 +34,5 @@ try { assert.equal(result.coordinates, "linuxcnc/src/hal/user_comps/vismach/xyzbc-trt-gui.py"); assert.equal(result.canvases, 2); assert.deepEqual(errors, []); - console.log(JSON.stringify({ schema: "xyzbc-panel-browser.v1", status: "verified", program: "xyzbc_switchkins.ngc", machine: result.model, axes: result.axes, canvases: result.canvases, browser_errors: errors })); + console.log(JSON.stringify({ schema: "xyzbc-panel-browser.v1", status: "verified", program: "boat-xyzbc.ngc", machine: result.model, axes: result.axes, canvases: result.canvases, browser_errors: errors })); } finally { await browser.close(); } diff --git a/web/react/src/App.tsx b/web/react/src/App.tsx index d955c9b..e83ee95 100644 --- a/web/react/src/App.tsx +++ b/web/react/src/App.tsx @@ -3,14 +3,13 @@ import { INITIAL_STATE, reduceFrontendState } from "./store"; import { ReactWorkerClient } from "./worker-client"; import { XyzacToolPreviewFromSnapshot } from "./XyzacToolPreview"; import { XyzacMachineViewFromSnapshot } from "./XyzacMachineView"; -import { SIM_CASE_CATALOG } from "../../workspace/sim-case-catalog.mjs"; import { loadSimulationCase, toWorkerConfiguration } from "../../workspace/sim-case-loader.mjs"; import { axisIcon } from "./sourceAssets"; import { HoldButton } from "./HoldButton"; import { MachineStatus } from "./MachineStatus"; import { AxisOperationController } from "../../app/axis-operation-controller.mjs"; import { XyzacDiagnostics, XyzacLiveParameters, XyzacProgramPane, XyzacTrtPanel } from "./XyzacTrtPanel"; -import { XYZAC_CASE, projectXyzacPanel, type XyzacOperationTrace } from "./xyzac-panel-contract"; +import { projectXyzacPanel, type XyzacOperationTrace } from "./xyzac-panel-contract"; import { DEFAULT_MACHINE, MACHINE_DEFINITIONS, machineDefinition } from "../../workspace/machine-definitions.mjs"; export const SELECTED_XYZAC_CASE = DEFAULT_MACHINE.entrypoint; @@ -507,15 +506,13 @@ export function App() { const setProgramText = (value: string) => { setProgramTextState(value); }; const loadPackagedProgram = async () => { if (!canLoad) return; - const path = selectedMachine.coordinates === "XYZAC" ? "linuxcnc/nc_files/3D_Chips.ngc" : selectedMachine.program; + const path = selectedMachine.program; setProgramLoading(true); startPreviewProgress(`${selectedMachine.coordinates} demo`); setCommandStatus("PROGRAM_LOAD pending: waiting for LinuxCNC WASM preview"); setCaseStatus(`${path} loading from published workspace...`); try { - const source = path === selectedMachine.program - ? normalizeProgramSource((await loadSimulationCase({ path: casePath, baseUrl: "." })).files?.[path] ?? "") - : normalizeProgramSource(await (async () => { const response = await fetch(`./${path}`); if (!response.ok) throw new Error(`program_asset_http_${response.status}`); return response.text(); })()); + const source = normalizeProgramSource((await loadSimulationCase({ path: casePath, baseUrl: "." })).files?.[path] ?? ""); if (!source.trim()) throw new Error("program_asset_empty"); if (!latestLoadGate.current.enabled) throw new Error(latestLoadGate.current.reason); setProgramText(source); setProgramPath(path); @@ -761,7 +758,7 @@ export function App() {
AXIS AXISLinuxCNC Web/WASM
Connection: {state.status.toUpperCase()}
execute("PROGRAM_STEP")} /> execute(programPaused ? "PROGRAM_RESUME" : "PROGRAM_PAUSE")} pressed={relief.pauseResume} /> stopProgram()} pressed={relief.abort} /> setZoom((value) => Math.min(2, value + .2))} /> setZoom((value) => Math.max(.5, value - .2))} /> { setView("z"); setRightTab("preview"); }} /> { setView("z2"); setRightTab("preview"); }} /> { setView("x"); setRightTab("preview"); }} /> { setView("y"); setRightTab("preview"); }} /> { setView("y2"); setRightTab("preview"); }} /> { setView("p"); setRightTab("preview"); }} /> setRotateMode((value) => !value)} /> xyzacCommand("VISUAL_CLEAR", {}, "vismachClear")} />
-
Load 3D_Chips.ngc{programLoading ? "Loading program..." : caseStatus.startsWith("blocked:") ? caseStatus : `Program: ${loadedProgramName}`}{previewProgress &&
{previewProgress.detail}{previewProgress.percent}%{previewProgress.percent}%
}
+
Load {selectedMachine.coordinates} boat program{programLoading ? "Loading program..." : caseStatus.startsWith("blocked:") ? caseStatus : `Program: ${loadedProgramName}`}{previewProgress &&
{previewProgress.detail}{previewProgress.percent}%{previewProgress.percent}%
}
{ setCasePath(path); void loadCase(path); }} /> diff --git a/web/react/src/XyzacMachineView.tsx b/web/react/src/XyzacMachineView.tsx index 74fe854..b7d98c1 100644 --- a/web/react/src/XyzacMachineView.tsx +++ b/web/react/src/XyzacMachineView.tsx @@ -3,7 +3,7 @@ import * as THREE from "three"; import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; import { RoomEnvironment } from "three/examples/jsm/environments/RoomEnvironment.js"; import { axisIcon } from "./sourceAssets"; -import { buildXyzacMachine } from "./xyzac-machine-model"; +import { buildXyzacMachine, buildXyzbcMachine } from "./xyzac-machine-model"; import { projectXyzacVisual, type XyzacVisualSource, type XyzacVisualState } from "./xyzac-visual-adapter"; type Point = Record; @@ -47,7 +47,7 @@ export function XyzacMachineView({ visual, toolpath, view = "p", zoom = 1, rotat floor.rotation.x = -Math.PI / 2; floor.position.y = -2.64; floor.receiveShadow = true; scene.add(floor); const coordinates = visualRef.current.machineModel?.coordinate_system ?? "XYZAC"; - const assembly = buildXyzacMachine(coordinates); scene.add(assembly.machine); + const assembly = coordinates === "XYZBC" ? buildXyzbcMachine() : buildXyzacMachine(); scene.add(assembly.machine); assembly.update(visualRef.current); const nameplateCanvas = document.createElement("canvas"); nameplateCanvas.width = 512; nameplateCanvas.height = 128; const label = nameplateCanvas.getContext("2d"); @@ -199,5 +199,5 @@ export function XyzacMachineView({ visual, toolpath, view = "p", zoom = 1, rotat export function XyzacMachineViewFromSnapshot({ snapshot, ...props }: Omit & { snapshot: XyzacVisualSource | null | undefined }) { const visual = projectXyzacVisual(snapshot); - return ; + return ; } diff --git a/web/react/src/legacy.d.ts b/web/react/src/legacy.d.ts index 8436592..bd9825f 100644 --- a/web/react/src/legacy.d.ts +++ b/web/react/src/legacy.d.ts @@ -1,5 +1,5 @@ declare module "*.mjs" { - export type MachineDefinition = { id: string; label: string; coordinates: string; axes: readonly string[]; joint_count: number; entrypoint: string; program: string; kinematics_type: number; runtime: string; model: string; source_path: string; offsets: Record; axis_limits: Record }; + 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; joint_limits: Record; axis_limits: Record }; export const MACHINE_DEFINITIONS: readonly MachineDefinition[]; export const DEFAULT_MACHINE: MachineDefinition; export function machineDefinition(value?: string): MachineDefinition | null; diff --git a/web/react/src/protocol.ts b/web/react/src/protocol.ts index 97f9d22..0bd218d 100644 --- a/web/react/src/protocol.ts +++ b/web/react/src/protocol.ts @@ -179,6 +179,9 @@ export type XyzacMachineModel = { source_path: string; source_hash: string | null; coordinate_system: "XYZAC" | "XYZBC"; + geometry_variant?: "moving-knee-x-tilt" | "moving-head-y-tilt"; + tilt_axis?: "X" | "Y"; + rotary_axis?: "Z"; joints: string[]; components: string[]; }; diff --git a/web/react/src/test_xyzac_threejs.mjs b/web/react/src/test_xyzac_threejs.mjs index 835731d..0cb2ac3 100644 --- a/web/react/src/test_xyzac_threejs.mjs +++ b/web/react/src/test_xyzac_threejs.mjs @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import { projectXyzacVisual, XYZAC_MACHINE_GROUPS } from "./xyzac-visual-adapter.ts"; import * as THREE from "three"; -import { buildXyzacMachine } from "./xyzac-machine-model.ts"; +import { buildXyzacMachine, buildXyzbcMachine } from "./xyzac-machine-model.ts"; const source = await readFile(new URL("./XyzacMachineView.tsx", import.meta.url), "utf8") + await readFile(new URL("./xyzac-machine-model.ts", import.meta.url), "utf8"); const panelSource = await readFile(new URL("./XyzacTrtPanel.tsx", import.meta.url), "utf8"); @@ -52,10 +52,18 @@ for (const token of ["J0", "J1", "J2", "J3", "J4"]) { assert.ok(toolSource.includes(token), `tool preview ${token}`); assert.ok(source.includes("J${index}"), "machine preview joint label template"); } +const close = (actual, expected) => assert.ok(Math.abs(actual - expected) < 1e-7, `${actual} != ${expected}`); const model = buildXyzacMachine(); +const xyzbcModel = buildXyzbcMachine(); +assert.equal(xyzbcModel.machine.name, "xyzbc-machine"); +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); +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 } }; -const close = (actual, expected) => assert.ok(Math.abs(actual - expected) < 1e-7, `${actual} != ${expected}`); const position = (node) => node.getWorldPosition(new THREE.Vector3()); model.update(zero); close(position(model.toolTip).y, 2); diff --git a/web/react/src/xyzac-machine-model.ts b/web/react/src/xyzac-machine-model.ts index 70b4412..9447c1d 100644 --- a/web/react/src/xyzac-machine-model.ts +++ b/web/react/src/xyzac-machine-model.ts @@ -6,9 +6,10 @@ const MM = .01; const DEG = Math.PI / 180; // Source coordinates remain millimetres, Z up. The existing viewer uses Y up. -export function buildXyzacMachine(coordinates = "XYZAC") { - // XYZAC retains the established tilt-a group identity; XYZBC uses tilt-b. - const rotaryAxis = coordinates[3]?.toLowerCase() ?? "a"; +function buildTrtMachine(coordinates = "XYZAC") { + const isXyzbc = coordinates === "XYZBC"; + // Separate source identities: XYZAC uses tilt-a, XYZBC uses tilt-b. + const rotaryAxis = isXyzbc ? "b" : "a"; const geometries = new Map(); const materials = { enamel: new THREE.MeshStandardMaterial({ color: 0xd9e1e1, roughness: .32, metalness: .25 }), @@ -58,7 +59,7 @@ export function buildXyzacMachine(coordinates = "XYZAC") { cylinder(machine, "levelling-pad", x, y, -259, 17, 9, "rubber"); cylinder(machine, "levelling-stud", x, y, -249, 8, 15, "steel"); } - box(machine, "base", 0, 30, -178, 240, 260, 146, "enamel", 9); + box(machine, "base", 0, 30, isXyzbc ? -115 : -178, 240, 260, isXyzbc ? 170 : 146, "enamel", 9); box(machine, "base-plinth", 0, 30, -239, 242, 262, 20, "dark", 4); box(machine, "base-front-panel", 0, -101, -177, 205, 4, 96, "teal", 4); box(machine, "service-door", 68, -104, -177, 48, 2, 71, "dark", 2); @@ -69,7 +70,7 @@ export function buildXyzacMachine(coordinates = "XYZAC") { for (const x of [-123, 123]) box(machine, "tray-side-lip", x, 18, -93, 5, 244, 16, "dark"); box(machine, "tray-front-lip", 0, -102, -93, 246, 5, 16, "dark"); - box(machine, "column", 0, 150, 5, 100, 100, 510, "enamel", 5); + box(machine, "column", 0, isXyzbc ? 170 : 150, isXyzbc ? 80 : 5, 100, isXyzbc ? 100 : 100, isXyzbc ? 560 : 510, "enamel", 5); for (const x of [-51, 51]) { box(machine, "column-side-insert", x, 155, 20, 3, 62, 413, "teal"); box(machine, "column-rib", x, 190, -120, 10, 16, 244, "dark", 2); @@ -83,21 +84,24 @@ export function buildXyzacMachine(coordinates = "XYZAC") { box(machine, "z-servo", 0, -105, -169, 49, 21, 50, "dark", 3); box(machine, "z-servo-cover", 0, -117, -169, 40, 3, 38, "teal", 2); - const head = group(machine, "head", 0, 0, 200); - // Native head spans Y=-30..240; this bridge physically connects the spindle to the column. - box(head, "head-body", 0, 105, 97.5, 70, 270, 75, "enamel", 5); - box(head, "head-top-cover", 0, 107, 137, 72, 269, 9, "teal", 3); + const head = group(machine, "head", 0, 0, isXyzbc ? 150 : 200); + // XYZAC has the long moving knee bridge; XYZBC has the shorter moving + // spindle head from xyzbc-trt-gui.py. + const headDepth = isXyzbc ? 130 : 270; + box(head, "head-body", 0, isXyzbc ? 50 : 105, 97.5, 70, headDepth, 75, "enamel", 5); + box(head, "head-top-cover", 0, isXyzbc ? 52 : 107, 137, 72, headDepth - 1, 9, "teal", 3); box(head, "spindle-front-panel", 0, -31, 96, 57, 3, 54, "teal", 2); for (const x of [-23, 23]) for (const z of [75, 119]) bolt(head, x, -34, z, "y"); for (let i = 0; i < 6; i++) box(head, "head-cooling-vent", 36, 70 + i * 11, 103, 1, 6, 29, "recess", .2); - cylinder(head, "motor", 0, 200, 168, 29, 61, "dark"); - cylinder(head, "motor-cap", 0, 200, 200, 30, 7, "teal"); + cylinder(head, "motor", 0, isXyzbc ? 60 : 200, 168, 29, 61, "dark"); + cylinder(head, "motor-cap", 0, isXyzbc ? 60 : 200, 200, 30, 7, "teal"); for (let i = 0; i < 16; i++) { const angle = i * Math.PI / 8; - const fin = box(head, "motor-cooling-fin", 30 * Math.cos(angle), 200 + 30 * Math.sin(angle), 170, 3, 4, 48, "steel", .5); + const motorY = isXyzbc ? 60 : 200; + const fin = box(head, "motor-cooling-fin", 30 * Math.cos(angle), motorY + 30 * Math.sin(angle), 170, 3, 4, 48, "steel", .5); fin.rotation.y = -angle; } - box(head, "motor-terminal-box", 32, 200, 168, 16, 32, 24, "dark", 2); + box(head, "motor-terminal-box", 32, isXyzbc ? 60 : 200, 168, 16, 32, 24, "dark", 2); const spindle = group(head, "spindle", 0, 0, 20); lathe(spindle, "spindle-housing", [[0,20],[20,20],[22,23],[22,38],[20,40],[20,115],[0,115]], "steel"); @@ -121,10 +125,12 @@ export function buildXyzacMachine(coordinates = "XYZAC") { const shank = cylinder(tool, "ground-tool-shank", 0, 0, 8, 3, 1, "polished"); const zslide = group(machine, "spindle-z"); - box(zslide, "knee-casting", 0, 10, -142.5, 100, 220, 75, "enamel", 5); - box(zslide, "knee-front-cover", 0, -102, -143, 85, 5, 56, "teal", 3); - for (const x of [-34, 34]) bolt(zslide, x, -106, -144, "y"); - for (const x of [-33, 33]) box(zslide, "y-guideway", x, 8, -103, 14, 181, 6, "steel"); + box(zslide, isXyzbc ? "y-slide" : "knee-casting", 0, 10, -142.5, 100, 220, 75, isXyzbc ? "teal" : "enamel", 5); + if (!isXyzbc) { + box(zslide, "knee-front-cover", 0, -102, -143, 85, 5, 56, "teal", 3); + for (const x of [-34, 34]) bolt(zslide, x, -106, -144, "y"); + for (const x of [-33, 33]) box(zslide, "y-guideway", x, 8, -103, 14, 181, 6, "steel"); + } const saddle = group(zslide, "saddle-y"); box(saddle, "saddle-body", 0, 0, -89, 150, 106, 32, "dark", 3); for (const y of [-32, 32]) box(saddle, "x-guideway", 0, y, -73, 148, 12, 5, "polished"); @@ -147,6 +153,11 @@ export function buildXyzacMachine(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; + if (isXyzbc) brackets.rotation.y = Math.PI / 2; box(tilt, "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); @@ -191,7 +202,7 @@ export function buildXyzacMachine(coordinates = "XYZAC") { // around machine Y. The Web scene maps source Y to Three.js Z. head.rotation.set(0, 0, 0); zslide.position.y = 0; - tilt.rotation.set(0, 0, -tiltAJoint * DEG); + 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); @@ -201,6 +212,7 @@ export function buildXyzacMachine(coordinates = "XYZAC") { 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); brackets.position.copy(tilt.position); @@ -224,6 +236,12 @@ export function buildXyzacMachine(coordinates = "XYZAC") { return { machine, update, dispose, toolTip, workOrigin, joints: { tableX, saddle, zslide, tilt, rotate }, tool }; } +// Keep the two machine architectures as explicit entry points. They share +// only primitive material/geometry helpers; each entry point selects its own +// source model, rotary axis and moving assembly. +export function buildXyzacMachine() { return buildTrtMachine("XYZAC"); } +export function buildXyzbcMachine() { return buildTrtMachine("XYZBC"); } + // Four helical reliefs with narrow cutting lands, closed end faces, and a flat cutter tip. function createFlutedCutter() { const radial = 96, axial = 32; diff --git a/web/worker/sim-session.mjs b/web/worker/sim-session.mjs index 4b4d621..edd398e 100644 --- a/web/worker/sim-session.mjs +++ b/web/worker/sim-session.mjs @@ -481,6 +481,9 @@ export class SimSession { source_path: modelResource?.path ?? modelSourcePath, source_hash: modelResource?.data_hash ?? this.visualModelSourceHash ?? (typeof modelSourceText === "string" ? fnv1a64(modelSourceText) : null), coordinate_system: machineDefinition?.coordinates ?? "XYZAC", + geometry_variant: machineDefinition?.coordinates === "XYZBC" ? "moving-head-y-tilt" : "moving-knee-x-tilt", + tilt_axis: machineDefinition?.coordinates === "XYZBC" ? "Y" : "X", + rotary_axis: "Z", joints: machineDefinition?.coordinates ? ["table-x", "saddle-y", "spindle-z", `tilt-${machineDefinition.coordinates[3].toLowerCase()}`, "rotate-c"] : ["table-x", "saddle-y", "spindle-z", "tilt-a", "rotate-c"], components: ["base", "column", "head", "spindle", "tool", "table", "saddle", "zslide"], } : null; diff --git a/web/worker/test_boat_trt_programs.mjs b/web/worker/test_boat_trt_programs.mjs new file mode 100644 index 0000000..816c4fe --- /dev/null +++ b/web/worker/test_boat_trt_programs.mjs @@ -0,0 +1,41 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import createModule from "../../build-wasm/linuxcnc-sim-core.js"; +import { loadSimulationCase, toWorkerConfiguration } from "../workspace/sim-case-loader.mjs"; +import { SimSession, CONTEXT_ID } from "./sim-session.mjs"; + +const ROOT = "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting"; +const fetchImpl = async (url) => { + try { const body = await readFile(new URL(`../../${String(url).replace(/^\//, "")}`, import.meta.url)); return { ok: true, text: async () => body.toString("utf8") }; } + catch { return { ok: false, text: async () => "" }; } +}; +const core = await createModule({ wasmBinary: await readFile(new URL("../../build-wasm/linuxcnc-sim-core.wasm", import.meta.url)) }); +const session = new SimSession(core, { sessionId: "boat-trt-programs" }); 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: `boat-${seq}`, payload }); +send("HELLO"); send("SESSION_ATTACH"); +const rows = []; +for (const coordinates of ["XYZAC", "XYZBC"]) { + const ini = `${ROOT}/${coordinates.toLowerCase()}-trt.ini`; + const programPath = `${ROOT}/demos/boat-${coordinates.toLowerCase()}.ngc`; + const bundle = await loadSimulationCase({ path: ini, fetchImpl }); + assert.equal(bundle.status, "ready"); assert.ok(bundle.files[programPath]); + const workspace = send("LOAD_WORKSPACE", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) }); + assert.equal(workspace.kind, "COMMAND_RESULT"); assert.equal(workspace.payload.panel_snapshot.kinematics.coordinates, coordinates); + assert.equal(send("POWER", { active: true }).kind, "COMMAND_RESULT"); send("HOME", { all: true }); + for (let tick = 0; tick < 100; tick += 1) send("SIM_STEP", { cycles: 1 }); + const loaded = send("PROGRAM_LOAD", { source: bundle.files[programPath], path: programPath }); + assert.equal(loaded.kind, "COMMAND_RESULT"); assert.ok(loaded.payload.preview.segments.length > 0); + assert.equal(send("PROGRAM_RUN").kind, "COMMAND_RESULT"); + let result = null; + for (let tick = 0; tick < 5000; tick += 1) { + result = send("SIM_STEP", { cycles: 100 }); + assert.notEqual(result.kind, "ERROR"); + if (result.payload.program.program_status === "completed" && Number(result.payload.wasm_motion.state.queue_depth) === 0) break; + } + assert.equal(result.payload.program.program_status, "completed"); + const rotary = coordinates === "XYZBC" ? "b" : "a"; + assert.ok(Number.isFinite(result.payload.panel_snapshot.visual.pose[rotary])); + assert.equal(result.payload.panel_snapshot.visual.machine_model.coordinate_system, coordinates); + rows.push({ coordinates, program: programPath, status: result.payload.program.program_status, rotary }); +} +console.log(JSON.stringify({ schema: "boat-trt-programs.v1", status: "verified", rows })); diff --git a/web/workspace/machine-definitions.mjs b/web/workspace/machine-definitions.mjs index 5d4cf29..7333546 100644 --- a/web/workspace/machine-definitions.mjs +++ b/web/workspace/machine-definitions.mjs @@ -4,7 +4,8 @@ function trt(id, coordinates, type, offsets, model) { const stem = coordinates.toLowerCase(); return Object.freeze({ id, label: `${coordinates} TRT`, coordinates, axes: [...stem], joint_count: coordinates.length, - entrypoint: `${TRT_ROOT}/${id}.ini`, program: `${TRT_ROOT}/demos/${stem}_switchkins.ngc`, + entrypoint: `${TRT_ROOT}/${id}.ini`, program: `${TRT_ROOT}/demos/boat-${stem}.ngc`, + switchkins_program: `${TRT_ROOT}/demos/${stem}_switchkins.ngc`, kinematics_type: type, runtime: "trt", model, source_path: `linuxcnc/src/hal/user_comps/vismach/${id}-gui.py`, subroutines: [`${stem}_switchkins_sub`, `helix_${stem.slice(3)}`], diff --git a/web/workspace/sim-case-loader.mjs b/web/workspace/sim-case-loader.mjs index 7ded913..0ddf7b6 100644 --- a/web/workspace/sim-case-loader.mjs +++ b/web/workspace/sim-case-loader.mjs @@ -228,6 +228,12 @@ export async function loadSimulationCase({ path, fetchImpl = globalThis.fetch, b // source hashing and model provenance. const machine = machineDefinition(catalog.path); if (machine) { + const programPath = machine.program; + if (!(programPath in files)) { + const content = await fetchText(fetchImpl, baseUrl, programPath); + if (content === null) missing.push(programPath); + else { files[programPath] = content; fetched.push(programPath); } + } const visualSource = machine.source_path; if (!(visualSource in files)) { const content = await fetchText(fetchImpl, baseUrl, visualSource);