Support XYZAC and XYZBC in shared web machine UI
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
|
||||
当前 AXIS 风格面板的 Preview 区域需要同时表达两种不同的信息:
|
||||
|
||||
1. 刀具在工件坐标中的 canonical 程序轨迹,以及执行过程中已经走过和正在执行的实时刀具轨迹。
|
||||
1. 刀具在工件坐标中的 canonical 程序轨迹,以及执行过程中已经走过和正在执行的实时刀具轨迹;XYZAC 使用 A 轴,XYZBC 使用 B 轴。
|
||||
2. 由五个关节(table-x、saddle-y、spindle-z、tilt-a、rotate-c)驱动的完整机床三维模型运动。
|
||||
|
||||
本方案将 Preview 明确拆为两个相互独立的 `<canvas>`:
|
||||
@@ -93,9 +93,9 @@ Worker 每个 logical tick 依次执行 interpreter、TP、motion、kinematics
|
||||
|
||||
### 5.2 Canvas B:机床三维模型运动
|
||||
|
||||
* 场景节点必须按 `XYZAC_MACHINE_GROUPS` 建立:`xyzac-machine`、`table-x`、`saddle-y`、`spindle-z`、`tilt-a`、`rotate-c`、`tool-offset`。
|
||||
* 场景节点按当前机床定义建立:`xyzac-machine`/`xyzbc-machine`、`table-x`、`saddle-y`、`spindle-z`、`tilt-a` 或 `tilt-b`、`rotate-c`、`tool-offset`。
|
||||
* 模型描述来自 `visual.machine_model`:`id`、`source_path`、`source_hash`、`components`、`joints`、`coordinate_system`。模型缺失或哈希不匹配时显示结构化错误并保留轴姿态 HUD。
|
||||
* 关节变换顺序固定为 table-x -> saddle-y -> spindle-z -> tilt-a -> rotate-c -> tool-offset;方向和单位从模型 manifest 定义,禁止组件自行猜测。
|
||||
* 关节变换顺序固定为 table-x -> saddle-y -> spindle-z -> tilt-a/tilt-b -> rotate-c -> tool-offset;方向和单位从模型 manifest 定义,禁止组件自行猜测。
|
||||
* 使用 `visual.joints` 驱动机械层级,使用 `visual.pose` 驱动刀尖/姿态 HUD;二者必须在同一 tick 通过一致性检查。
|
||||
* 默认不绘制 Canvas A 的完整路径;`showPath` 仅作为明确的诊断层,且数据仍来自同一 snapshot。
|
||||
|
||||
@@ -121,7 +121,7 @@ type DualCanvasSnapshot = {
|
||||
visual: {
|
||||
schema: "axis-visual.v1";
|
||||
source: "wasm_canonical_motion";
|
||||
pose: Record<"x"|"y"|"z"|"a"|"c", number>;
|
||||
pose: Record<"x"|"y"|"z"|"a"|"b"|"c", number>;
|
||||
joints: number[]; // exactly 5: mm, mm, mm, deg, deg
|
||||
toolpath?: ToolpathSegment[]; // omitted in motion_delta
|
||||
motion_trace?: MotionSample[];
|
||||
@@ -143,7 +143,7 @@ type DualCanvasSnapshot = {
|
||||
};
|
||||
```
|
||||
|
||||
`ToolpathSegment` 的数值单位是 mm/deg,坐标顺序为 XYZAC;`MotionSample` 必须含 `tick`、五关节、pose、活动段和进度。`machine_model.source` 必须为 `linuxcnc-wasm-worker`,模型 `source_hash` 用于恢复和 native 对账。`projectXyzacVisual()` 负责默认值、有限值和长度校验;校验失败时 `authoritative=false`。
|
||||
`ToolpathSegment` 的数值单位是 mm/deg,坐标顺序为当前机床定义的 XYZAC 或 XYZBC;`MotionSample` 必须含 `tick`、五关节、pose、活动段和进度。`machine_model.source` 必须为 `linuxcnc-wasm-worker`,模型 `source_hash` 用于恢复和 native 对账。`projectXyzacVisual()` 负责默认值、有限值和长度校验;校验失败时 `authoritative=false`。
|
||||
|
||||
### 6.2 消息时序与一致性
|
||||
|
||||
|
||||
38
scripts/test_xyzbc_panel_browser.mjs
Normal file
38
scripts/test_xyzbc_panel_browser.mjs
Normal file
@@ -0,0 +1,38 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const modulePath = process.env.PLAYWRIGHT_MODULE;
|
||||
if (!modulePath) { console.log(JSON.stringify({ schema: "xyzbc-panel-browser.v1", status: "blocked", code: "playwright_unavailable" })); process.exit(0); }
|
||||
const { chromium } = await import(pathToFileURL(modulePath).href);
|
||||
const browser = await chromium.launch({ headless: true, executablePath: process.env.BROWSER_EXECUTABLE_PATH ?? "/usr/bin/google-chrome", args: ["--no-sandbox", "--use-angle=swiftshader", "--enable-unsafe-swiftshader"] });
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
|
||||
const errors = [];
|
||||
page.on("pageerror", (error) => errors.push(error.message));
|
||||
const control = (id) => page.locator(`[data-axis-control="${id}"]`);
|
||||
try {
|
||||
await page.goto(process.env.XYZAC_PANEL_URL ?? "http://127.0.0.1:5174/");
|
||||
await page.waitForFunction(() => document.querySelector("main")?.dataset.axisProgramLoading === "false");
|
||||
await control("toolbar-power").click();
|
||||
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 });
|
||||
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),
|
||||
summary: document.querySelector(".xyzac-manual-summary")?.textContent,
|
||||
toolPreview: document.querySelector(".xyzac-tool-preview")?.getAttribute("aria-label"),
|
||||
model: document.querySelector(".xyzac-machine-view")?.dataset.xyzacModelId,
|
||||
coordinates: document.querySelector(".xyzac-machine-view")?.dataset.xyzacModelSourcePath,
|
||||
canvases: document.querySelectorAll(".axis-preview-split canvas[data-engine]").length,
|
||||
}));
|
||||
assert.equal(result.machine, "XYZBC authoritative position");
|
||||
assert.deepEqual(result.axes, ["X", "Y", "Z", "B", "C"]);
|
||||
assert.match(result.summary, /XYZBC authority/);
|
||||
assert.equal(result.toolPreview, "XYZBC authoritative tool preview");
|
||||
assert.equal(result.model, "xyzbc-trt");
|
||||
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 }));
|
||||
} finally { await browser.close(); }
|
||||
@@ -123,7 +123,7 @@ export function xyzacGate(action, input = {}) {
|
||||
const offsets = xyzac.finite_offsets !== false;
|
||||
const pinVars = xyzac.hal_pin_vars === true;
|
||||
const net = xyzac.switchkins_net === true;
|
||||
const coordinates = xyzac.coordinates === "XYZAC" && xyzac.joint_count === 5;
|
||||
const coordinates = ["XYZAC", "XYZBC"].includes(String(xyzac.coordinates ?? "").toUpperCase()) && xyzac.joint_count === 5;
|
||||
if (action === "vismachClear") return Object.freeze({ action, enabled: true, reason: "axis_visual_only", visual_only: true, taskState: state.taskState, interpState: state.interpState });
|
||||
const common = idle && pinVars && net && offsets;
|
||||
const commonReason = state.taskState === "ESTOP" ? "xyzac_estop_active" : state.taskState !== "ON" ? "xyzac_machine_off" : !idle ? "xyzac_interp_not_idle" : !pinVars ? "xyzac_hal_pin_vars_missing" : !net ? "xyzac_switchkins_net_missing" : !offsets ? "xyzac_offsets_nonfinite" : "xyzac_switchkins_common_gate";
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -46,14 +46,15 @@ export function XyzacMachineView({ visual, toolpath, view = "p", zoom = 1, rotat
|
||||
const floor = new THREE.Mesh(new THREE.PlaneGeometry(18, 18), new THREE.ShadowMaterial({ opacity: .24 }));
|
||||
floor.rotation.x = -Math.PI / 2; floor.position.y = -2.64; floor.receiveShadow = true; scene.add(floor);
|
||||
|
||||
const assembly = buildXyzacMachine(); scene.add(assembly.machine);
|
||||
const coordinates = visualRef.current.machineModel?.coordinate_system ?? "XYZAC";
|
||||
const assembly = buildXyzacMachine(coordinates); scene.add(assembly.machine);
|
||||
assembly.update(visualRef.current);
|
||||
const nameplateCanvas = document.createElement("canvas"); nameplateCanvas.width = 512; nameplateCanvas.height = 128;
|
||||
const label = nameplateCanvas.getContext("2d");
|
||||
if (label) {
|
||||
label.fillStyle = "#176b67"; label.fillRect(0, 0, 512, 128);
|
||||
label.fillStyle = "#f0f4f3"; label.font = "bold 68px sans-serif"; label.fillText("AXIS", 18, 82);
|
||||
label.font = "24px sans-serif"; label.fillText("XYZAC / TRT", 235, 76);
|
||||
label.font = "24px sans-serif"; label.fillText(`${coordinates} / TRT`, 235, 76);
|
||||
}
|
||||
const nameplateTexture = new THREE.CanvasTexture(nameplateCanvas); nameplateTexture.colorSpace = THREE.SRGBColorSpace;
|
||||
const nameplate = new THREE.Mesh(new THREE.PlaneGeometry(.84, .21), new THREE.MeshBasicMaterial({ map: nameplateTexture }));
|
||||
@@ -158,7 +159,8 @@ export function XyzacMachineView({ visual, toolpath, view = "p", zoom = 1, rotat
|
||||
limits.box.setFromPoints([toView({ x: x[0], y: y[0], z: z[0] }), toView({ x: x[1], y: y[1], z: z[1] })]);
|
||||
}
|
||||
offsets.visible = config.showOffsets;
|
||||
offsets.position.set(0, state.offsets.z_offset * .01, state.offsets.y_offset * .01);
|
||||
const coordinates = state.machineModel?.coordinate_system ?? "XYZAC";
|
||||
offsets.position.set(coordinates === "XYZBC" ? Number(state.offsets.x_offset ?? 0) * .01 : 0, state.offsets.z_offset * .01, state.offsets.y_offset * .01);
|
||||
extents.visible = config.showExtents;
|
||||
const preset = `${config.view}:${config.zoom}`;
|
||||
if (preset !== lastPreset) { presetCamera(); lastPreset = preset; }
|
||||
@@ -179,8 +181,10 @@ export function XyzacMachineView({ visual, toolpath, view = "p", zoom = 1, rotat
|
||||
}, []);
|
||||
|
||||
const model = visual.machineModel;
|
||||
const coordinates = model?.coordinate_system ?? "XYZAC";
|
||||
const axisLabels = [...coordinates];
|
||||
const controllerButton = (action: string, label: string, asset?: string, glyph?: string) => <button type="button" data-axis-control={`machine-${action}`} data-xyzac-machine-controller={action} title={label} aria-label={label} onClick={() => host.current?.xyzacMachineController?.[action]?.()}>{asset ? <img src={axisIcon(asset)} alt="" /> : glyph}</button>;
|
||||
return <div className="three-viewer xyzac-machine-view" ref={host} data-xyzac-preview="machine" data-xyzac-model-source="linuxcnc-wasm" data-xyzac-mouse-controls="left-rotate-middle-zoom-right-pan-wheel-zoom" data-xyzac-model-id={model?.id ?? ""} data-xyzac-model-source-path={model?.source_path ?? ""} data-xyzac-model-source-hash={model?.source_hash ?? ""} data-xyzac-authoritative={String(visual.authoritative)} data-xyzac-tick={visual.tick} data-xyzac-state-hash={visual.stateHash} data-xyzac-active-segment={visual.activeSegmentId ?? ""} data-xyzac-joints={JSON.stringify(visual.joints)} data-xyzac-offsets={JSON.stringify(visual.offsets)} aria-label="XYZAC TRT Vismach machine model">
|
||||
return <div className="three-viewer xyzac-machine-view" ref={host} data-xyzac-preview="machine" data-xyzac-model-source="linuxcnc-wasm" data-xyzac-mouse-controls="left-rotate-middle-zoom-right-pan-wheel-zoom" data-xyzac-model-id={model?.id ?? ""} data-xyzac-model-source-path={model?.source_path ?? ""} data-xyzac-model-source-hash={model?.source_hash ?? ""} data-xyzac-authoritative={String(visual.authoritative)} data-xyzac-tick={visual.tick} data-xyzac-state-hash={visual.stateHash} data-xyzac-active-segment={visual.activeSegmentId ?? ""} data-xyzac-joints={JSON.stringify(visual.joints)} data-xyzac-offsets={JSON.stringify(visual.offsets)} aria-label={`${coordinates} TRT Vismach machine model`}>
|
||||
<div className="xyzac-view-controller xyzac-machine-controller" role="toolbar" aria-label="Machine 3D view controller">
|
||||
{controllerButton("zoomIn", "Zoom in", "tool_zoomin.gif")}{controllerButton("zoomOut", "Zoom out", "tool_zoomout.gif")}
|
||||
{controllerButton("panLeft", "Pan left", undefined, "\u2190")}{controllerButton("panRight", "Pan right", undefined, "\u2192")}
|
||||
@@ -189,10 +193,11 @@ export function XyzacMachineView({ visual, toolpath, view = "p", zoom = 1, rotat
|
||||
{controllerButton("rotateUp", "Rotate upward", undefined, "\u2b06")}{controllerButton("rotateDown", "Rotate downward", undefined, "\u2b07")}
|
||||
{controllerButton("reset", "Reset machine view", undefined, "\u21ba")}{controllerButton("tool", "Tool close-up", "tool_axis_z.gif")}
|
||||
</div>
|
||||
<pre className="axis-preview-hud xyzac-machine-hud" aria-label="XYZAC authoritative visual state">{visual.authoritative ? `XYZAC TRT\n${visual.joints.map((value, index) => `${["X", "Y", "Z", "A", "C"][index]} ${value.toFixed(3)}`).join("\n")}\n${visual.joints.map((value, index) => `J${index} ${value.toFixed(3)}`).join(" ")}${showVelocity ? `\nV ${Number(visual.velocityMmMin ?? 0).toFixed(1)}` : ""}` : "XYZAC TRT\nWASM state pending"}</pre>
|
||||
<pre className="axis-preview-hud xyzac-machine-hud" aria-label={`${coordinates} authoritative visual state`}>{visual.authoritative ? `${coordinates} TRT\n${visual.joints.map((value, index) => `${axisLabels[index] ?? `J${index}`} ${value.toFixed(3)}`).join("\n")}\n${visual.joints.map((value, index) => `J${index} ${value.toFixed(3)}`).join(" ")}${showVelocity ? `\nV ${Number(visual.velocityMmMin ?? 0).toFixed(1)}` : ""}` : `${coordinates} TRT\nWASM state pending`}</pre>
|
||||
</div>;
|
||||
}
|
||||
|
||||
export function XyzacMachineViewFromSnapshot({ snapshot, ...props }: Omit<Props, "visual"> & { snapshot: XyzacVisualSource | null | undefined }) {
|
||||
return <XyzacMachineView visual={projectXyzacVisual(snapshot)} {...props} />;
|
||||
const visual = projectXyzacVisual(snapshot);
|
||||
return <XyzacMachineView key={visual.machineModel?.id ?? "machine-model-pending"} visual={visual} {...props} />;
|
||||
}
|
||||
|
||||
@@ -385,7 +385,10 @@ export function XyzacToolPreview({ visual, view = "p", zoom = 1, rotateMode = fa
|
||||
originGroup.scale.setScalar(Math.max(.015, bounds.radius * .35));
|
||||
const pose = sourcePoint(renderedPose);
|
||||
toolGroup.position.copy(pose);
|
||||
toolGroup.rotation.set(-Number(renderedPose.a ?? 0) * DEG, -Number(renderedPose.c ?? 0) * DEG, 0);
|
||||
const isXyzbc = String(state.machineModel?.coordinate_system ?? "XYZAC") === "XYZBC";
|
||||
const tilt = -Number(renderedPose[isXyzbc ? "b" : "a"] ?? 0) * DEG;
|
||||
const rotary = -Number(renderedPose.c ?? 0) * DEG;
|
||||
toolGroup.rotation.set(isXyzbc ? 0 : tilt, rotary, isXyzbc ? tilt : 0);
|
||||
const toolScale = Math.min(1, Math.max(0.015, bounds.radius * 0.35));
|
||||
toolGroup.scale.setScalar(toolScale);
|
||||
tool.position.set(0, .57, 0); tip.position.set(0, 0, 0);
|
||||
@@ -404,7 +407,8 @@ export function XyzacToolPreview({ visual, view = "p", zoom = 1, rotateMode = fa
|
||||
element.dataset.xyzacLimitsVisible = String(limitGrid.visible);
|
||||
element.dataset.xyzacExtentsVisible = String(extents.visible);
|
||||
element.dataset.xyzacOffset = JSON.stringify(offset);
|
||||
if (hudRef.current) hudRef.current.textContent = `TOOL PREVIEW\nX ${Number(renderedPose.x).toFixed(3)} Y ${Number(renderedPose.y).toFixed(3)} Z ${Number(renderedPose.z).toFixed(3)}\nA ${Number(renderedPose.a).toFixed(3)} C ${Number(renderedPose.c).toFixed(3)}\nJ0 ${Number(renderedJoints[0] ?? 0).toFixed(3)} J1 ${Number(renderedJoints[1] ?? 0).toFixed(3)} J2 ${Number(renderedJoints[2] ?? 0).toFixed(3)}\nJ3 ${Number(renderedJoints[3] ?? 0).toFixed(3)} J4 ${Number(renderedJoints[4] ?? 0).toFixed(3)}`;
|
||||
const rotaryTiltAxis = String(state.machineModel?.coordinate_system ?? "XYZAC") === "XYZBC" ? "b" : "a";
|
||||
if (hudRef.current) hudRef.current.textContent = `TOOL PREVIEW\nX ${Number(renderedPose.x).toFixed(3)} Y ${Number(renderedPose.y).toFixed(3)} Z ${Number(renderedPose.z).toFixed(3)}\n${rotaryTiltAxis.toUpperCase()} ${Number(renderedPose[rotaryTiltAxis]).toFixed(3)} C ${Number(renderedPose.c).toFixed(3)}\nJ0 ${Number(renderedJoints[0] ?? 0).toFixed(3)} J1 ${Number(renderedJoints[1] ?? 0).toFixed(3)} J2 ${Number(renderedJoints[2] ?? 0).toFixed(3)}\nJ3 ${Number(renderedJoints[3] ?? 0).toFixed(3)} J4 ${Number(renderedJoints[4] ?? 0).toFixed(3)}`;
|
||||
if (layer.showVelocity && hudRef.current) hudRef.current.textContent += `\nV ${Number(state.velocityMmMin ?? 0).toFixed(1)} mm/min`;
|
||||
controls.mouseButtons.LEFT = rotateRef.current ? THREE.MOUSE.ROTATE : THREE.MOUSE.PAN;
|
||||
controls.update(); renderer.render(scene, camera); frame = requestAnimationFrame(render);
|
||||
@@ -417,7 +421,10 @@ export function XyzacToolPreview({ visual, view = "p", zoom = 1, rotateMode = fa
|
||||
const helixCount = visual.toolpath.filter((segment) => (segment as { kind?: string }).kind === "helix").length;
|
||||
const sampleCount = visual.toolpath.reduce((count, segment) => count + (Array.isArray((segment as { samples?: unknown[] }).samples) ? (segment as { samples: unknown[] }).samples.length : 0), 0);
|
||||
const controllerButton = (action: string, label: string, glyph: string) => ["zoomIn", "zoomOut"].includes(action) ? null : <button type="button" data-axis-control={`preview-${action}`} data-xyzac-controller={action} title={label} aria-label={label} onClick={() => { const controller = (host.current as (HTMLDivElement & { xyzacController?: Record<string, () => void> }) | null)?.xyzacController; controller?.[action]?.(); }}>{glyph}</button>;
|
||||
return <div className="three-viewer xyzac-tool-preview" ref={host} data-xyzac-authoritative={String(visual.authoritative)} data-xyzac-preview="toolpath" data-xyzac-controller="orbit" data-xyzac-origin="0,0,0" data-xyzac-origin-axes="X,Y,Z" data-xyzac-toolpath-count={visual.toolpath.length} data-xyzac-helix-count={helixCount} data-xyzac-sample-count={sampleCount} data-xyzac-samples-source="linuxcnc-wasm-sim_posemath_segment_point" data-xyzac-live-source="linuxcnc-source-tp" data-xyzac-live-progress={visual.activeProgress} data-xyzac-live-pose={JSON.stringify(visual.livePose)} data-xyzac-tick={visual.tick} data-xyzac-state-hash={visual.stateHash} data-xyzac-active-segment={visual.activeSegmentId ?? ""} data-xyzac-joints={JSON.stringify(visual.joints)} aria-label="XYZAC authoritative tool preview"><div className="xyzac-view-controller" role="toolbar" aria-label="3D preview controller">{controllerButton("zoomIn", "Zoom in", "+")}{controllerButton("zoomOut", "Zoom out", "-")}<span className="xyzac-controller-divider" aria-hidden="true" />{controllerButton("panLeft", "Pan left", "L")}{controllerButton("panRight", "Pan right", "R")}{controllerButton("panUp", "Pan up", "U")}{controllerButton("panDown", "Pan down", "D")}<span className="xyzac-controller-divider" aria-hidden="true" />{controllerButton("reset", "Reset 3D view", "0")}</div><pre ref={hudRef} className="axis-preview-hud" aria-label="XYZAC tool preview state">{visual.authoritative ? `TOOL PREVIEW\nX ${Number(pose.x).toFixed(3)} Y ${Number(pose.y).toFixed(3)} Z ${Number(pose.z).toFixed(3)}\nA ${Number(pose.a).toFixed(3)} C ${Number(pose.c).toFixed(3)}\nJ0 ${Number(visual.joints[0] ?? 0).toFixed(3)} J1 ${Number(visual.joints[1] ?? 0).toFixed(3)} J2 ${Number(visual.joints[2] ?? 0).toFixed(3)}\nJ3 ${Number(visual.joints[3] ?? 0).toFixed(3)} J4 ${Number(visual.joints[4] ?? 0).toFixed(3)}` : "TOOL PREVIEW\nWASM state pending"}</pre></div>;
|
||||
const coordinates = String(visual.machineModel?.coordinate_system ?? "XYZAC");
|
||||
const tiltAxis = coordinates === "XYZBC" ? "b" : "a";
|
||||
const tiltLabel = tiltAxis.toUpperCase();
|
||||
return <div className="three-viewer xyzac-tool-preview" ref={host} data-xyzac-authoritative={String(visual.authoritative)} data-xyzac-preview="toolpath" data-xyzac-controller="orbit" data-xyzac-origin="0,0,0" data-xyzac-origin-axes="X,Y,Z" data-xyzac-toolpath-count={visual.toolpath.length} data-xyzac-helix-count={helixCount} data-xyzac-sample-count={sampleCount} data-xyzac-samples-source="linuxcnc-wasm-sim_posemath_segment_point" data-xyzac-live-source="linuxcnc-source-tp" data-xyzac-live-progress={visual.activeProgress} data-xyzac-live-pose={JSON.stringify(visual.livePose)} data-xyzac-tick={visual.tick} data-xyzac-state-hash={visual.stateHash} data-xyzac-active-segment={visual.activeSegmentId ?? ""} data-xyzac-joints={JSON.stringify(visual.joints)} aria-label={`${coordinates} authoritative tool preview`}><div className="xyzac-view-controller" role="toolbar" aria-label="3D preview controller">{controllerButton("zoomIn", "Zoom in", "+")}{controllerButton("zoomOut", "Zoom out", "-")}<span className="xyzac-controller-divider" aria-hidden="true" />{controllerButton("panLeft", "Pan left", "L")}{controllerButton("panRight", "Pan right", "R")}{controllerButton("panUp", "Pan up", "U")}{controllerButton("panDown", "Pan down", "D")}<span className="xyzac-controller-divider" aria-hidden="true" />{controllerButton("reset", "Reset 3D view", "0")}</div><pre ref={hudRef} className="axis-preview-hud" aria-label={`${coordinates} tool preview state`}>{visual.authoritative ? `TOOL PREVIEW\nX ${Number(pose.x).toFixed(3)} Y ${Number(pose.y).toFixed(3)} Z ${Number(pose.z).toFixed(3)}\n${tiltLabel} ${Number(pose[tiltAxis] ?? 0).toFixed(3)} C ${Number(pose.c).toFixed(3)}\nJ0 ${Number(visual.joints[0] ?? 0).toFixed(3)} J1 ${Number(visual.joints[1] ?? 0).toFixed(3)} J2 ${Number(visual.joints[2] ?? 0).toFixed(3)}\nJ3 ${Number(visual.joints[3] ?? 0).toFixed(3)} J4 ${Number(visual.joints[4] ?? 0).toFixed(3)}` : "TOOL PREVIEW\nWASM state pending"}</pre></div>;
|
||||
}
|
||||
|
||||
export function XyzacToolPreviewFromSnapshot({ snapshot, ...props }: Omit<Props, "visual"> & { snapshot: XyzacVisualSource | null | undefined }) {
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { XYZAC_KINEMATICS, type XyzacOperationTrace, type XyzacPanelView } from "./xyzac-panel-contract";
|
||||
import { type XyzacOperationTrace, type XyzacPanelView } from "./xyzac-panel-contract";
|
||||
import type { SimSnapshotV1 } from "./protocol";
|
||||
import { DEFAULT_MACHINE } from "../../workspace/machine-definitions.mjs";
|
||||
|
||||
type Command = (kind: string, payload?: Record<string, unknown>, action?: string) => void;
|
||||
|
||||
export function XyzacKinematicsControl({ view, command, loading = false }: { view: XyzacPanelView; command: Command; loading?: boolean }) {
|
||||
const active = view.kinematics?.active_type;
|
||||
return <section className="xyzac-kinematics" aria-label="XYZAC kinematics">
|
||||
<div className="xyzac-section-title"><strong>Switchkins</strong><span>{view.kinematics?.native_type ?? "Unavailable"}</span></div>
|
||||
const machine = view.machine ?? DEFAULT_MACHINE;
|
||||
const modes = [{ type: 0, action: "switchkinsIdentity", label: "Identity", mcode: "M429" }, { type: 1, action: "switchkinsXyzac", label: `TCP: ${machine.coordinates}`, mcode: "M428" }, { type: 2, action: "switchkinsUserk", label: "User K", mcode: "M430" }];
|
||||
return <section className="xyzac-kinematics" aria-label={`${machine.coordinates} kinematics`}>
|
||||
<div className="xyzac-section-title"><strong>{machine.label}</strong><span>{view.kinematics?.native_type ?? "Unavailable"}</span></div>
|
||||
<div className="xyzac-segments" role="group" aria-label="Kinematics mode">
|
||||
{XYZAC_KINEMATICS.map((item) => {
|
||||
{modes.map((item) => {
|
||||
const gate = view.gates[item.action];
|
||||
const disabled = loading || view.status !== "ready" || gate?.enabled === false;
|
||||
return <button key={item.type} type="button" data-axis-control={`switchkins-${item.type}`} className={active === item.type ? "active" : ""} aria-pressed={active === item.type} disabled={disabled} title={gate?.reason ?? item.mcode} onClick={() => command("KINEMATICS_SELECT", { type: item.type }, item.action)}><span>{item.label}</span><small>{item.mcode}</small></button>;
|
||||
@@ -20,18 +23,23 @@ export function XyzacKinematicsControl({ view, command, loading = false }: { vie
|
||||
|
||||
export function XyzacManualPanel({ view }: { view: XyzacPanelView }) {
|
||||
const pose = view.pose;
|
||||
const axes = ["x", "y", "z", "a", "c"] as const;
|
||||
return <section className="xyzac-manual-summary" aria-label="XYZAC authoritative position">
|
||||
<div className="xyzac-section-title"><strong>XYZAC authority</strong><span>{view.kinematics?.linear_units ?? "mm"} / {view.kinematics?.angular_units ?? "deg"}</span></div>
|
||||
const machine = view.machine ?? DEFAULT_MACHINE;
|
||||
const axes = machine.axes;
|
||||
const offsets = view.kinematics?.offsets;
|
||||
const offsetRows = machine.coordinates === "XYZBC"
|
||||
? [`X offset ${offsets ? Number(offsets.x_offset ?? 0).toFixed(3) : "-"}`, `Z offset ${offsets ? Number(offsets.z_offset).toFixed(3) : "-"}`, `Tool ${offsets ? Number(offsets.tool_offset).toFixed(3) : "-"}`]
|
||||
: [`Y offset ${offsets ? Number(offsets.y_offset).toFixed(3) : "-"}`, `Z offset ${offsets ? Number(offsets.z_offset).toFixed(3) : "-"}`, `Tool ${offsets ? Number(offsets.tool_offset).toFixed(3) : "-"}`];
|
||||
return <section className="xyzac-manual-summary" aria-label={`${machine.coordinates} authoritative position`}>
|
||||
<div className="xyzac-section-title"><strong>{machine.coordinates} authority</strong><span>{view.kinematics?.linear_units ?? "mm"} / {view.kinematics?.angular_units ?? "deg"}</span></div>
|
||||
<div className="xyzac-position-grid">
|
||||
{axes.map((axis, index) => <div key={axis}><b>{axis.toUpperCase()}</b><output>{pose ? Number(pose[axis] ?? Number.NaN).toFixed(axis === "a" || axis === "c" ? 3 : 4) : "-"}</output><small>J{index} {view.joints.length === 5 ? Number(view.joints[index]).toFixed(4) : "-"}</small></div>)}
|
||||
{axes.map((axis, index) => <div key={axis}><b>{axis.toUpperCase()}</b><output>{pose ? Number(pose[axis as keyof typeof pose] ?? Number.NaN).toFixed(["a", "b", "c"].includes(axis) ? 3 : 4) : "-"}</output><small>J{index} {view.joints.length === machine.joint_count ? Number(view.joints[index]).toFixed(4) : "-"}</small></div>)}
|
||||
</div>
|
||||
<div className="xyzac-offset-row"><span>Tool {view.kinematics ? Number(view.kinematics.offsets.tool_offset).toFixed(3) : "-"}</span><span>Y offset {view.kinematics ? Number(view.kinematics.offsets.y_offset).toFixed(3) : "-"}</span><span>Z offset {view.kinematics ? Number(view.kinematics.offsets.z_offset).toFixed(3) : "-"}</span></div>
|
||||
<div className="xyzac-offset-row">{offsetRows.map((row) => <span key={row}>{row}</span>)}</div>
|
||||
</section>;
|
||||
}
|
||||
|
||||
export function XyzacProgramPane({ status, currentLine, sourceHash, children }: { status: string; currentLine: number | null; sourceHash?: string | null; children: ReactNode }) {
|
||||
return <section className="xyzac-program" aria-label="XYZAC program"><header><strong>XYZAC Program</strong><span>{status}</span><span>Line {currentLine ?? "-"}</span><code>{sourceHash?.slice(0, 18) ?? "no source"}</code></header>{children}</section>;
|
||||
return <section className="xyzac-program" aria-label="TRT program"><header><strong>TRT Program</strong><span>{status}</span><span>Line {currentLine ?? "-"}</span><code>{sourceHash?.slice(0, 18) ?? "no source"}</code></header>{children}</section>;
|
||||
}
|
||||
|
||||
export function XyzacLiveParameters({ snapshot }: { snapshot: SimSnapshotV1 | null }) {
|
||||
@@ -56,15 +64,18 @@ export function XyzacLiveParameters({ snapshot }: { snapshot: SimSnapshotV1 | nu
|
||||
|
||||
export function XyzacDiagnostics({ view, operations, alarms, error }: { view: XyzacPanelView; operations: XyzacOperationTrace[]; alarms: Array<Record<string, unknown>>; error: { code: string; message?: string } | null }) {
|
||||
const disabled = Object.entries(view.gates).filter(([, gate]) => gate.enabled === false);
|
||||
return <section className="xyzac-diagnostics" aria-label="XYZAC diagnostics">
|
||||
return <section className="xyzac-diagnostics" aria-label={`${view.machine.coordinates} diagnostics`}>
|
||||
<div className="xyzac-diagnostic-summary"><strong>{view.status.toUpperCase()}</strong><span>Tick {view.tick}</span><code>{view.stateHash || "no hash"}</code></div>
|
||||
{error && <div className="xyzac-error" role="alert"><b>{error.code}</b><span>{error.message}</span></div>}
|
||||
<div><h3>Operation trace</h3>{operations.length ? operations.slice(-8).reverse().map((item, index) => <div className="xyzac-diagnostic-row" key={`${item.operation_id ?? "operation"}-${index}`}><code>{item.operation_id ?? "-"}</code><span>{item.kind}</span><b>{item.phase}</b><span>{item.reason ?? item.response_kind}</span></div>) : <span>No operations</span>}</div>
|
||||
<div><h3>Alarms / recovery</h3>{alarms.length ? alarms.slice(-5).map((alarm, index) => <div className="xyzac-diagnostic-row" key={`${String(alarm.id ?? alarm.code ?? "alarm")}-${index}`}><code>{String(alarm.code ?? "alarm")}</code><span>{String(alarm.severity ?? "error")}</span></div>) : <span>No alarms</span>}<div className="xyzac-diagnostic-row"><code>recovery</code><span>{view.recovery ? JSON.stringify(view.recovery) : "idle"}</span></div></div>
|
||||
<div><h3>Gate reasons</h3>{disabled.length ? disabled.slice(0, 10).map(([name, gate]) => <div className="xyzac-diagnostic-row" key={name}><code>{name}</code><span>{gate.reason}</span></div>) : <span>All XYZAC gates enabled</span>}</div>
|
||||
<div><h3>Gate reasons</h3>{disabled.length ? disabled.slice(0, 10).map(([name, gate]) => <div className="xyzac-diagnostic-row" key={name}><code>{name}</code><span>{gate.reason}</span></div>) : <span>All {view.machine.coordinates} gates enabled</span>}</div>
|
||||
</section>;
|
||||
}
|
||||
|
||||
export function XyzacTrtPanel({ view, command, loading = false }: { view: XyzacPanelView; command: Command; loading?: boolean }) {
|
||||
return <section className="xyzac-control-band" aria-label="XYZAC TRT controls"><XyzacKinematicsControl view={view} command={command} loading={loading} /><XyzacManualPanel view={view} /></section>;
|
||||
export function XyzacTrtPanel({ view, command, loading = false, machineOptions = [], selectedMachine, onMachineChange }: { view: XyzacPanelView; command: Command; loading?: boolean; machineOptions?: readonly { entrypoint: string; label: string }[]; selectedMachine?: string; onMachineChange?: (entrypoint: string) => void }) {
|
||||
return <section className="xyzac-control-band" aria-label={`${view.machine?.coordinates ?? "TRT"} controls`}>
|
||||
{machineOptions.length > 1 && <label className="xyzac-machine-selector">Machine type<select data-axis-control="machine-type-select" value={selectedMachine ?? view.machine?.entrypoint} onChange={(event) => onMachineChange?.(event.target.value)}>{machineOptions.map((machine) => <option key={machine.entrypoint} value={machine.entrypoint}>{machine.label}</option>)}</select></label>}
|
||||
<XyzacKinematicsControl view={view} command={command} loading={loading} /><XyzacManualPanel view={view} />
|
||||
</section>;
|
||||
}
|
||||
|
||||
4
web/react/src/legacy.d.ts
vendored
4
web/react/src/legacy.d.ts
vendored
@@ -1,4 +1,8 @@
|
||||
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<string, number>; axis_limits: Record<string, number[]> };
|
||||
export const MACHINE_DEFINITIONS: readonly MachineDefinition[];
|
||||
export const DEFAULT_MACHINE: MachineDefinition;
|
||||
export function machineDefinition(value?: string): MachineDefinition | null;
|
||||
export const SIM_CASE_CATALOG: Array<{ path: string; label?: string; category?: string; status?: string }>;
|
||||
export function loadSimulationCase(options: { path: string; baseUrl?: string }): Promise<{ status: string; entrypoint: string; files: Record<string, string>; missing: string[]; fetched: string[]; state_hash: string; halManifest: unknown[] }>;
|
||||
export function toWorkerConfiguration(bundle: unknown): Record<string, unknown>;
|
||||
|
||||
@@ -99,26 +99,28 @@ export type SimSnapshotV1 = {
|
||||
task_authority?: string;
|
||||
};
|
||||
|
||||
export type XyzacKinematicsSnapshot = {
|
||||
export type TrtKinematicsSnapshot = {
|
||||
schema: "wasm-xyzac-kinematics.v1";
|
||||
status: "verified" | "error" | "blocked";
|
||||
active_type: 0 | 1 | 2;
|
||||
native_type: string;
|
||||
coordinates: "XYZAC";
|
||||
coordinates: "XYZAC" | "XYZBC";
|
||||
joint_count: 5;
|
||||
linear_units: "mm";
|
||||
angular_units: "deg";
|
||||
pose: Record<"x" | "y" | "z" | "a" | "b" | "c" | "u" | "v" | "w", number>;
|
||||
joints: [number, number, number, number, number];
|
||||
offsets: { x_rot_point: number; y_offset: number; z_offset: number; tool_offset: number };
|
||||
offsets: { x_rot_point: number; y_offset: number; z_offset: number; tool_offset: number; y_rot_point?: number; z_rot_point?: number; x_offset?: number };
|
||||
generation: number;
|
||||
switch_count: number;
|
||||
state_hash: string;
|
||||
wasm_state_hash: string;
|
||||
};
|
||||
|
||||
export type XyzacKinematicsSnapshot = TrtKinematicsSnapshot;
|
||||
|
||||
export type XyzacCapabilitySnapshot = {
|
||||
coordinates: "XYZAC";
|
||||
coordinates: "XYZAC" | "XYZBC";
|
||||
joint_count: 5;
|
||||
active_type: 0 | 1 | 2;
|
||||
hal_pin_vars: boolean;
|
||||
@@ -176,7 +178,7 @@ export type XyzacMachineModel = {
|
||||
source: "linuxcnc-wasm-worker";
|
||||
source_path: string;
|
||||
source_hash: string | null;
|
||||
coordinate_system: "XYZAC";
|
||||
coordinate_system: "XYZAC" | "XYZBC";
|
||||
joints: string[];
|
||||
components: string[];
|
||||
};
|
||||
|
||||
@@ -71,6 +71,7 @@ function projection(result: Record<string, unknown>, message: MessageEnvelope, p
|
||||
const executionTrace = resetExecutionTrace ? trace.slice(-EXECUTION_TRACE_CACHE_LIMIT) : trace.length ? [...previous.executionTrace, ...trace].slice(-EXECUTION_TRACE_CACHE_LIMIT) : previous.executionTrace;
|
||||
const delta = result.motion_sample as { tick?: number; pose?: Record<string, number>; joints?: number[]; active_segment_id?: number; active_progress?: number } | undefined;
|
||||
if (!panel && delta && previous.snapshot) {
|
||||
const previousSnapshot = previous.snapshot;
|
||||
const pose = delta.pose ?? {};
|
||||
const joints = Array.isArray(delta.joints) ? [...delta.joints] : previous.snapshot.motion?.joints ?? [];
|
||||
const visual = previous.snapshot.visual ? {
|
||||
@@ -97,16 +98,8 @@ function projection(result: Record<string, unknown>, message: MessageEnvelope, p
|
||||
motion: {
|
||||
...previous.snapshot.motion,
|
||||
...(result.motion as SimSnapshotV1["motion"] | undefined),
|
||||
// Keep the XYZAC axis vector intact on every motion delta. Older
|
||||
// projection code rebuilt only XYZ, which made A/C disappear from
|
||||
// consumers that read the canonical motion array.
|
||||
axes: [
|
||||
Number(pose.x ?? previous.snapshot.motion?.axes?.[0] ?? 0),
|
||||
Number(pose.y ?? previous.snapshot.motion?.axes?.[1] ?? 0),
|
||||
Number(pose.z ?? previous.snapshot.motion?.axes?.[2] ?? 0),
|
||||
Number(pose.a ?? previous.snapshot.motion?.axes?.[3] ?? 0),
|
||||
Number(pose.c ?? previous.snapshot.motion?.axes?.[4] ?? 0),
|
||||
],
|
||||
// Keep the active machine's axis vector intact on every motion delta.
|
||||
axes: [...String(previousSnapshot.kinematics?.coordinates ?? "XYZAC").toLowerCase()].map((axis, index) => Number(pose[axis] ?? previousSnapshot.motion?.axes?.[index] ?? 0)),
|
||||
joints,
|
||||
},
|
||||
wasm_motion: (result.wasm_motion as Record<string, unknown>) ?? previous.snapshot.wasm_motion,
|
||||
@@ -150,7 +143,8 @@ function projection(result: Record<string, unknown>, message: MessageEnvelope, p
|
||||
})
|
||||
: previous.toolpath;
|
||||
const pose = panel.visual.pose;
|
||||
const axes = [pose.x, pose.y, pose.z, pose.a, pose.c].map((value) => Number.isFinite(value) ? value : 0);
|
||||
const coordinateSystem = String(panel.kinematics?.coordinates ?? panel.xyzac?.coordinates ?? "XYZAC");
|
||||
const axes = [...coordinateSystem.toLowerCase()].map((axis) => Number.isFinite(pose?.[axis]) ? Number(pose[axis]) : 0);
|
||||
const snapshot = {
|
||||
schema: panel.schema,
|
||||
source_status: panel.source_status,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { INITIAL_STATE, reduceFrontendState } from "./store.ts";
|
||||
import { projectXyzacPanel } from "./xyzac-panel-contract.ts";
|
||||
|
||||
const envelope = (kind, seq, payload, tick) => ({ schema_version: 1, kind, context_id: INITIAL_STATE.contextId, session_id: "motion-projection", seq, simulation_tick: tick, payload });
|
||||
const panel = {
|
||||
@@ -36,4 +37,8 @@ const next = reduceFrontendState(initial, envelope("COMMAND_RESULT", 2, {
|
||||
assert.deepEqual(next.snapshot.motion.axes, [11, 21, 31, 41, 51]);
|
||||
assert.equal(next.snapshot.visual.pose.a, 41);
|
||||
assert.equal(next.snapshot.visual.pose.c, 51);
|
||||
const xyzbcPanel = projectXyzacPanel({ simulation_tick: 3, state_hash: "bc", kinematics: { schema: "wasm-xyzac-kinematics.v1", status: "verified", active_type: 1, native_type: "xyzbc-trt-kins", coordinates: "XYZBC", joint_count: 5, linear_units: "mm", angular_units: "deg", pose: { x: 0, y: 0, z: 0, a: 0, b: 12, c: 20, u: 0, v: 0, w: 0 }, joints: [1, 2, 3, 12, 20], 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 }, generation: 1, switch_count: 1, state_hash: "k", wasm_state_hash: "w" }, xyzac: { coordinates: "XYZBC", joint_count: 5, active_type: 1, hal_pin_vars: true, switchkins_net: true, finite_offsets: true, userk_capability: true }, visual: { pose: { x: 0, y: 0, z: 0, a: 0, b: 12, c: 20 }, joints: [1, 2, 3, 12, 20] } });
|
||||
assert.equal(xyzbcPanel.machine.coordinates, "XYZBC");
|
||||
assert.deepEqual(xyzbcPanel.machine.axes, ["x", "y", "z", "b", "c"]);
|
||||
assert.deepEqual(xyzbcPanel.joints, [1, 2, 3, 12, 20]);
|
||||
console.log(JSON.stringify({ schema: "xyzac-motion-projection.v1", status: "verified", axes: next.snapshot.motion.axes, pose: { a: next.snapshot.visual.pose.a, c: next.snapshot.visual.pose.c } }));
|
||||
|
||||
@@ -6,7 +6,9 @@ const MM = .01;
|
||||
const DEG = Math.PI / 180;
|
||||
|
||||
// Source coordinates remain millimetres, Z up. The existing viewer uses Y up.
|
||||
export function buildXyzacMachine() {
|
||||
export function buildXyzacMachine(coordinates = "XYZAC") {
|
||||
// XYZAC retains the established tilt-a group identity; XYZBC uses tilt-b.
|
||||
const rotaryAxis = coordinates[3]?.toLowerCase() ?? "a";
|
||||
const geometries = new Map<string, THREE.BufferGeometry>();
|
||||
const materials = {
|
||||
enamel: new THREE.MeshStandardMaterial({ color: 0xd9e1e1, roughness: .32, metalness: .25 }),
|
||||
@@ -51,7 +53,7 @@ export function buildXyzacMachine() {
|
||||
const lathe = (parent: THREE.Object3D, name: string, profile: number[][], finish: Finish) =>
|
||||
mesh(parent, name, geometry(name, () => new THREE.LatheGeometry(profile.map(([r, z]) => new THREE.Vector2(r * MM, z * MM)), 64)), finish, 0, 0, 0);
|
||||
|
||||
const machine = group(null, "xyzac-machine");
|
||||
const machine = group(null, coordinates === "XYZBC" ? "xyzbc-machine" : "xyzac-machine");
|
||||
for (const x of [-95, 95]) for (const y of [-72, 135]) {
|
||||
cylinder(machine, "levelling-pad", x, y, -259, 17, 9, "rubber");
|
||||
cylinder(machine, "levelling-stud", x, y, -249, 8, 15, "steel");
|
||||
@@ -141,14 +143,14 @@ export function buildXyzacMachine() {
|
||||
cylinder(brackets, "bearing-seal", x < 0 ? -82 : 82, 0, 0, 12, 3, "dark", "x");
|
||||
for (const y of [-27, 27]) bolt(brackets, x, y, -38);
|
||||
}
|
||||
box(brackets, "a-axis-servo", -94, 0, 0, 23, 48, 46, "teal", 4);
|
||||
cylinder(brackets, "a-axis-end-cap", -107, 0, 0, 16, 3, "dark", "x");
|
||||
box(brackets, `${rotaryAxis}-axis-servo`, -94, 0, 0, 23, 48, 46, "teal", 4);
|
||||
cylinder(brackets, `${rotaryAxis}-axis-end-cap`, -107, 0, 0, 16, 3, "dark", "x");
|
||||
|
||||
const tilt = group(tableX, "tilt-a");
|
||||
const tilt = group(tableX, `tilt-${rotaryAxis}`);
|
||||
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);
|
||||
cylinder(tilt, "a-trunnion-shaft", x, 0, 0, 10, 22, "steel", "x");
|
||||
cylinder(tilt, `${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);
|
||||
@@ -178,15 +180,34 @@ export function buildXyzacMachine() {
|
||||
let toolLength = NaN;
|
||||
const update = (state: Pick<XyzacVisualState, "joints" | "offsets">) => {
|
||||
const [tableXJoint, saddleYJoint, spindleZJoint, tiltAJoint, rotateCJoint] = state.joints;
|
||||
const xOffset = Number(state.offsets.x_offset ?? 0);
|
||||
const yOffset = Number(state.offsets.y_offset ?? 0);
|
||||
const zOffset = Number(state.offsets.z_offset ?? 0);
|
||||
tableX.position.x = -tableXJoint * MM;
|
||||
saddle.position.z = -saddleYJoint * MM;
|
||||
zslide.position.y = (200 - spindleZJoint) * MM;
|
||||
tilt.rotation.x = -tiltAJoint * DEG;
|
||||
if (coordinates === "XYZBC") {
|
||||
head.position.y = (150 + spindleZJoint) * MM;
|
||||
// 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;
|
||||
tilt.rotation.set(0, 0, -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);
|
||||
brackets.position.copy(tilt.position);
|
||||
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);
|
||||
tilt.rotation.x = -tiltAJoint * DEG;
|
||||
tilt.position.set(0, zOffset * MM, yOffset * MM);
|
||||
brackets.position.copy(tilt.position);
|
||||
rotaryBase.position.copy(tilt.position).negate();
|
||||
}
|
||||
rotate.rotation.y = -rotateCJoint * DEG;
|
||||
tool.position.y = -state.offsets.tool_offset * MM;
|
||||
tilt.position.set(0, state.offsets.z_offset * MM, state.offsets.y_offset * MM);
|
||||
brackets.position.copy(tilt.position);
|
||||
rotaryBase.position.copy(tilt.position).negate();
|
||||
if (toolLength !== state.offsets.tool_offset) {
|
||||
toolLength = state.offsets.tool_offset;
|
||||
const reach = Math.max(.5, 10 + toolLength);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { XyzacCapabilitySnapshot, XyzacKinematicsSnapshot } from "./protocol";
|
||||
import { DEFAULT_MACHINE, MACHINE_DEFINITIONS } from "../../workspace/machine-definitions.mjs";
|
||||
|
||||
export const XYZAC_CASE = "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini" as const;
|
||||
export const XYZAC_AXES = ["x", "y", "z", "a", "c"] as const;
|
||||
export const MACHINE_CASES = MACHINE_DEFINITIONS;
|
||||
export const XYZAC_CASE = DEFAULT_MACHINE.entrypoint;
|
||||
export const XYZAC_AXES: readonly string[] = DEFAULT_MACHINE.axes;
|
||||
export const XYZAC_COMMANDS = {
|
||||
power: "POWER",
|
||||
selectKinematics: "KINEMATICS_SELECT",
|
||||
@@ -25,6 +27,7 @@ export type XyzacPanelView = {
|
||||
joints: number[];
|
||||
gates: Record<string, XyzacGate>;
|
||||
recovery: Record<string, unknown> | null;
|
||||
machine: typeof DEFAULT_MACHINE;
|
||||
};
|
||||
|
||||
type XyzacPanelSource = {
|
||||
@@ -38,13 +41,18 @@ type XyzacPanelSource = {
|
||||
};
|
||||
|
||||
export function projectXyzacPanel(snapshot: XyzacPanelSource | null | undefined): XyzacPanelView {
|
||||
const kinematics = snapshot?.kinematics?.coordinates === "XYZAC" && snapshot.kinematics.joint_count === 5 ? snapshot.kinematics : null;
|
||||
const capability = snapshot?.xyzac?.coordinates === "XYZAC" && snapshot.xyzac.joint_count === 5 ? snapshot.xyzac : null;
|
||||
const rawKinematics = snapshot?.kinematics;
|
||||
const rawCapability = snapshot?.xyzac;
|
||||
const machine = MACHINE_DEFINITIONS.find((entry) => entry.coordinates === rawKinematics?.coordinates) ?? DEFAULT_MACHINE;
|
||||
const kinematics = rawKinematics && rawKinematics.coordinates === machine.coordinates && rawKinematics.joint_count === machine.joint_count ? rawKinematics : null;
|
||||
const capability = rawCapability && rawCapability.coordinates === machine.coordinates && rawCapability.joint_count === machine.joint_count ? rawCapability : null;
|
||||
const visualPose = snapshot?.visual?.pose;
|
||||
const pose = visualPose && [visualPose.x, visualPose.y, visualPose.z, visualPose.a, visualPose.c].every((value) => Number.isFinite(value))
|
||||
const poseAxes = [...machine.coordinates.toLowerCase()];
|
||||
const pose = visualPose && poseAxes.every((axis) => Number.isFinite(visualPose[axis]))
|
||||
? { ...(kinematics?.pose ?? { x: 0, y: 0, z: 0, a: 0, b: 0, c: 0, u: 0, v: 0, w: 0 }), ...visualPose }
|
||||
: kinematics?.pose ?? null;
|
||||
const joints = snapshot?.visual?.joints?.length === 5 ? snapshot.visual.joints.slice(0, 5) : kinematics?.joints ? [...kinematics.joints] : [];
|
||||
const visualJoints = snapshot?.visual?.joints;
|
||||
const joints = visualJoints?.length === machine.joint_count ? visualJoints.slice(0, machine.joint_count) : kinematics?.joints ? [...kinematics.joints] : [];
|
||||
return {
|
||||
status: kinematics && capability ? "ready" : "unavailable",
|
||||
tick: snapshot?.simulation_tick ?? 0,
|
||||
@@ -55,5 +63,6 @@ export function projectXyzacPanel(snapshot: XyzacPanelSource | null | undefined)
|
||||
joints,
|
||||
gates: snapshot?.gates ?? {},
|
||||
recovery: (snapshot?.authoritative?.recovery as Record<string, unknown> | undefined) ?? null,
|
||||
machine,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -35,8 +35,10 @@ export type XyzacVisualSource = {
|
||||
|
||||
export function projectXyzacVisual(snapshot: XyzacVisualSource | null | undefined): XyzacVisualState {
|
||||
const kinematics = snapshot?.kinematics;
|
||||
const coordinateSystem = String(kinematics?.coordinates ?? snapshot?.visual?.machine_model?.coordinate_system ?? "XYZAC");
|
||||
const poseAxes = [...coordinateSystem.toLowerCase()];
|
||||
const visualPose = snapshot?.visual?.pose;
|
||||
const pose = visualPose && [visualPose.x, visualPose.y, visualPose.z, visualPose.a, visualPose.c].every((value) => Number.isFinite(value))
|
||||
const pose = visualPose && poseAxes.every((axis) => Number.isFinite(visualPose[axis]))
|
||||
? { ...ZERO_POSE, ...visualPose }
|
||||
: kinematics?.pose ?? ZERO_POSE;
|
||||
const visualJoints = snapshot?.visual?.joints;
|
||||
|
||||
@@ -40,6 +40,7 @@ function linuxcncAssets(): Plugin {
|
||||
["linuxcnc/share/axis/images", "linuxcnc/share/axis/images"],
|
||||
["linuxcnc/lib/hallib", "linuxcnc/lib/hallib"],
|
||||
["linuxcnc/src/hal/user_comps/vismach/xyzac-trt-gui.py", "linuxcnc/src/hal/user_comps/vismach/xyzac-trt-gui.py"],
|
||||
["linuxcnc/src/hal/user_comps/vismach/xyzbc-trt-gui.py", "linuxcnc/src/hal/user_comps/vismach/xyzbc-trt-gui.py"],
|
||||
["linuxcnc/configs/sim/qtvcp_screens/qtaxis_lathe/qtaxis/images", "linuxcnc/configs/sim/qtvcp_screens/qtaxis_lathe/qtaxis/images"],
|
||||
];
|
||||
for (const [from, to] of copies) cpSync(resolve(repoRoot, from), resolve(output, to), { recursive: true, dereference: false });
|
||||
|
||||
@@ -31,6 +31,7 @@ import { WasmXyzacIo } from "./wasm-xyzac-io.mjs";
|
||||
import { WasmXyzacDeviceIo } from "./wasm-xyzac-device-io.mjs";
|
||||
import { WasmProgramRuntime } from "./wasm-program-runtime.mjs";
|
||||
import { axisGate, xyzacGate, xyzacGateMatrix } from "../app/axis-contract.mjs";
|
||||
import { machineDefinition } from "../workspace/machine-definitions.mjs";
|
||||
|
||||
const PROTOCOL_SCHEMA = 1;
|
||||
const CONTEXT_ID = "linuxcnc-2.10.0-pre0-wasm-m1-py-c1";
|
||||
@@ -135,6 +136,7 @@ export class SimSession {
|
||||
this.xyzacIo = null;
|
||||
this.xyzacDeviceIo = null;
|
||||
this.xyzacProgram = null;
|
||||
this.machineDefinition = null;
|
||||
this.homingHistory = [];
|
||||
this.xyzacProgramKinematicsMode = XYZAC_MODES.identity;
|
||||
this.xyzacRapidVelocity = 35;
|
||||
@@ -293,7 +295,7 @@ export class SimSession {
|
||||
source: wasmMotion.state.source ?? "wasm_motion_queue",
|
||||
tick: wasmMotion.status === "verified" ? wasmMotion.state.tick : this.tick,
|
||||
axes: { x: position.x, y: position.y, z: position.z },
|
||||
joints: kinematicsJoints ?? (home.status === "verified" && Array.isArray(home.state.joints) ? [...home.state.joints] : [position.x, position.y, position.z, position.a ?? 0, position.c ?? 0]),
|
||||
joints: kinematicsJoints ?? (home.status === "verified" && Array.isArray(home.state.joints) ? [...home.state.joints] : [position.x, position.y, position.z, position[this.machineDefinition?.coordinates === "XYZBC" ? "b" : "a"] ?? 0, position.c ?? 0]),
|
||||
velocity: wasmMotion.status === "verified" ? wasmMotion.state.velocity : 0,
|
||||
velocity_mm_min: Number(wasmMotion.state?.velocity_mm_min ?? 0),
|
||||
feed_command_mm_min: Number(sourceProgram?.feed ?? 0),
|
||||
@@ -310,7 +312,7 @@ export class SimSession {
|
||||
max_velocity: Number(wasmMotion.state?.velocity_limit ?? 0),
|
||||
homed: Boolean(home.state?.homed),
|
||||
homed_axes: Array.from({ length: 5 }, (_, index) => Boolean(Number(home.state?.homed_mask ?? 0) & (1 << index))),
|
||||
limits: { x: Boolean(home.state?.limit_error), y: Boolean(home.state?.limit_error), z: Boolean(home.state?.limit_error), a: Boolean(home.state?.limit_error), c: Boolean(home.state?.limit_error) },
|
||||
limits: { x: Boolean(home.state?.limit_error), y: Boolean(home.state?.limit_error), z: Boolean(home.state?.limit_error), [this.machineDefinition?.coordinates === "XYZBC" ? "b" : "a"]: Boolean(home.state?.limit_error), c: Boolean(home.state?.limit_error) },
|
||||
on_any_limit: Boolean(home.state?.limit_error),
|
||||
following_error: 0,
|
||||
motion_mode: this.xyzacProgram ? this.wasmHomeLimit.mode : (home.state?.homed ? "TELEOP" : "FREE"),
|
||||
@@ -356,7 +358,9 @@ export class SimSession {
|
||||
if (requirement.status !== "verified") return requirement;
|
||||
if (!requirement.required) return { status: "verified", count: 0 };
|
||||
const basePath = String(programPath ?? "").split("/").slice(0, -1).join("/");
|
||||
const rows = [["remap_subs/xyzac_switchkins_sub.ngc", "xyzac_switchkins_sub"], ["remap_subs/helix_ac.ngc", "helix_ac"]];
|
||||
const coordinates = this.machineDefinition?.coordinates ?? "XYZAC";
|
||||
const stem = coordinates.toLowerCase();
|
||||
const rows = [[`remap_subs/${stem}_switchkins_sub.ngc`, `${stem}_switchkins_sub`], [`remap_subs/helix_${stem.slice(3)}.ngc`, `helix_${stem.slice(3)}`]];
|
||||
for (const [suffix, name] of rows) {
|
||||
const path = `${basePath.replace(/\/demos$/, "")}/${suffix}`;
|
||||
const source = this.workspaceFiles[path];
|
||||
@@ -402,7 +406,7 @@ export class SimSession {
|
||||
? { schema: "wasm_authority_boundary.v1", source: "linuxcnc-wasm-worker", state_hash: fnv1a64({ motion: wasmMotion, kinematics: this.xyzacKinematics?.snapshot() ?? null, task }) }
|
||||
: this.authoritative.export();
|
||||
const axis = axisState({ task, program: program ?? {}, motion, devices, io: this.xyzacIo ? this.xyzacIo.export() : {}, device_io: deviceIo });
|
||||
const xyzac = this.xyzacKinematics ? { coordinates: "XYZAC", joint_count: 5, hal_pin_vars: true, switchkins_net: true, finite_offsets: true, userk_capability: true, active_type: this.xyzacKinematics.snapshot().active_type } : null;
|
||||
const xyzac = this.xyzacKinematics ? { coordinates: this.machineDefinition?.coordinates ?? "XYZAC", joint_count: 5, hal_pin_vars: true, switchkins_net: true, finite_offsets: true, userk_capability: true, active_type: this.xyzacKinematics.snapshot().active_type } : null;
|
||||
const gateInput = { task, program: program ?? {}, motion, devices, io: this.xyzacIo ? this.xyzacIo.export() : {}, device_io: deviceIo, xyzac, taskfile: Boolean(this.xyzacProgram ? this.xyzacProgram.snapshot().status !== 0 : this.program.loaded), kinematicsIdentity: this.motionAuthority === "wasm" };
|
||||
const gateActions = ["load", "run", "runLine", "step", "pause", "resume", "abort", "restart", "reload", "edit", "mdi", "jogStart", "jogStop", "home", "estop", "recover", "override", "overrideLimits", "reset", "spindle", "coolant", "touchOff", "toolTouchOff"];
|
||||
const ui = axisSourceUi({ task_state: axis.source_task_state, interp_state: axis.source_interp_state,
|
||||
@@ -459,7 +463,7 @@ export class SimSession {
|
||||
: { x: wasmPosition.x, y: wasmPosition.y, z: wasmPosition.z };
|
||||
const inverseJoints = this.xyzacKinematics?.snapshot?.()?.joints;
|
||||
const joints = this.motionAuthority === "wasm" && wasmMotion.status === "verified"
|
||||
? (Array.isArray(wasmMotion.state.joint_commanded) && wasmMotion.state.joint_commanded.length === 5 ? [...wasmMotion.state.joint_commanded] : inverseJoints?.length === 5 ? [...inverseJoints] : [position.x, position.y, position.z, position.a ?? 0, position.c ?? 0])
|
||||
? (Array.isArray(wasmMotion.state.joint_commanded) && wasmMotion.state.joint_commanded.length === 5 ? [...wasmMotion.state.joint_commanded] : inverseJoints?.length === 5 ? [...inverseJoints] : [position.x, position.y, position.z, position[this.machineDefinition?.coordinates === "XYZBC" ? "b" : "a"] ?? 0, position.c ?? 0])
|
||||
: authoritative.kinematics.joints;
|
||||
const preview = this.previewState?.status === "verified" ? this.previewState : null;
|
||||
// Keep the Vismach model identity in the same authoritative visual
|
||||
@@ -467,16 +471,17 @@ export class SimSession {
|
||||
// hash comes from the exact resource resolved during WASM startup, so a
|
||||
// renderer can audit that its geometry matches the loaded LinuxCNC case.
|
||||
const modelResource = this.configurationState?.exact_resources?.find?.((row) => row.role === "vismach_source");
|
||||
const modelSourcePath = "linuxcnc/src/hal/user_comps/vismach/xyzac-trt-gui.py";
|
||||
const machineDefinition = this.machineDefinition;
|
||||
const modelSourcePath = this.machineDefinition?.source_path ?? "linuxcnc/src/hal/user_comps/vismach/xyzac-trt-gui.py";
|
||||
const modelSourceText = this.workspaceFiles?.[modelSourcePath];
|
||||
const machineModel = this.xyzacProgram ? {
|
||||
schema: "linuxcnc-vismach-model.v1",
|
||||
id: "xyzac-trt",
|
||||
id: machineDefinition?.id ?? "xyzac-trt",
|
||||
source: "linuxcnc-wasm-worker",
|
||||
source_path: modelResource?.path ?? modelSourcePath,
|
||||
source_hash: modelResource?.data_hash ?? this.visualModelSourceHash ?? (typeof modelSourceText === "string" ? fnv1a64(modelSourceText) : null),
|
||||
coordinate_system: "XYZAC",
|
||||
joints: ["table-x", "saddle-y", "spindle-z", "tilt-a", "rotate-c"],
|
||||
coordinate_system: machineDefinition?.coordinates ?? "XYZAC",
|
||||
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;
|
||||
const visual = {
|
||||
@@ -498,10 +503,11 @@ export class SimSession {
|
||||
coordinate_offset: program?.coordinate_context?.offsets?.[(program?.coordinate_context?.system ?? 1) - 1]?.slice(0, 3) ?? null,
|
||||
...(machineModel ? { machine_model: machineModel } : {}),
|
||||
};
|
||||
const axisCoordinates = [position.x, position.y, position.z, position.a ?? 0, position.c ?? 0];
|
||||
const coordinateAxes = machineDefinition?.axes ?? ["x", "y", "z", "a", "c"];
|
||||
const axisCoordinates = coordinateAxes.map((axis) => position[axis] ?? 0);
|
||||
const coordinateContext = program?.coordinate_context;
|
||||
const activeOffsets = coordinateContext?.offsets?.[coordinateContext.system - 1] ?? Array(9).fill(0);
|
||||
const displayedOffsets = [0, 1, 2, 3, 5].map((index) => activeOffsets[index]);
|
||||
const displayedOffsets = coordinateAxes.map((axis) => activeOffsets[["x", "y", "z", "a", "b", "c", "u", "v", "w"].indexOf(axis)] ?? 0);
|
||||
const unsigned = {
|
||||
schema: "axis-panel-snapshot.v1",
|
||||
context_id: this.contextId,
|
||||
@@ -532,7 +538,7 @@ export class SimSession {
|
||||
task_authority: this.task.authority(),
|
||||
...(this.xyzacKinematics ? { kinematics: this.xyzacKinematics.snapshot() } : {}),
|
||||
...(includeStableRuntime && this.xyzacIo ? { io: this.xyzacIo.export() } : {}),
|
||||
...(xyzac ? { xyzac } : {}),
|
||||
...(xyzac ? { xyzac: { ...xyzac, coordinates: machineDefinition?.coordinates ?? xyzac.coordinates } } : {}),
|
||||
};
|
||||
return { ...unsigned, state_hash: fnv1a64(unsigned) };
|
||||
}
|
||||
@@ -572,7 +578,7 @@ export class SimSession {
|
||||
const type = Number(input.payload?.type);
|
||||
const action = input.kind === "VISUAL_CLEAR" ? "vismachClear" : type === 0 ? "switchkinsIdentity" : type === 1 ? "switchkinsXyzac" : type === 2 ? "switchkinsUserk" : null;
|
||||
if (!action) return { action: null, enabled: false, reason: "xyzac_kinematics_type_invalid" };
|
||||
return xyzacGate(action, { task, program, motion, devices, xyzac: this.xyzacKinematics ? { coordinates: "XYZAC", joint_count: 5, hal_pin_vars: true, switchkins_net: true, finite_offsets: true, userk_capability: true } : null });
|
||||
return xyzacGate(action, { task, program, motion, devices, xyzac: this.xyzacKinematics ? { coordinates: this.machineDefinition?.coordinates ?? "XYZAC", joint_count: 5, hal_pin_vars: true, switchkins_net: true, finite_offsets: true, userk_capability: true } : null });
|
||||
}
|
||||
const action = PANEL_GATE_ACTIONS[input.kind];
|
||||
if (!action) return { action: null, enabled: true, reason: "axis_internal_command" };
|
||||
@@ -675,7 +681,8 @@ export class SimSession {
|
||||
if (!this.xyzacProgram) return { status: "error", code: "xyzac_program_unavailable" };
|
||||
const payload = input.payload ?? {};
|
||||
const axis = String(payload.axis ?? "").trim().toUpperCase();
|
||||
if (!["X", "Y", "Z", "A", "C"].includes(axis)) return { status: "error", code: "touch_off_axis_invalid" };
|
||||
const coordinateAxes = this.machineDefinition?.axes ?? ["x", "y", "z", "a", "c"];
|
||||
if (!coordinateAxes.includes(axis.toLowerCase())) return { status: "error", code: "touch_off_axis_invalid" };
|
||||
const value = Number(payload.value);
|
||||
if (!Number.isFinite(value)) return { status: "error", code: "touch_off_value_invalid" };
|
||||
const selectedSystem = String(payload.system ?? "P1 G54").trim().toUpperCase();
|
||||
@@ -1586,8 +1593,9 @@ export class SimSession {
|
||||
let startupLoaded = false;
|
||||
const configuration = input.payload?.configuration ?? null;
|
||||
if (configuration) {
|
||||
this.machineDefinition = machineDefinition(configuration.entrypoint) ?? machineDefinition(configuration.kinematics?.type) ?? this.machineDefinition;
|
||||
this.workspaceFiles = { ...(configuration.files ?? configuration.workspace ?? configuration.workspace_files ?? {}) };
|
||||
const visualSourcePath = "linuxcnc/src/hal/user_comps/vismach/xyzac-trt-gui.py";
|
||||
const visualSourcePath = (this.machineDefinition ?? machineDefinition(configuration.entrypoint))?.source_path ?? "linuxcnc/src/hal/user_comps/vismach/xyzac-trt-gui.py";
|
||||
this.visualModelSourceHash = typeof this.workspaceFiles[visualSourcePath] === "string" ? fnv1a64(this.workspaceFiles[visualSourcePath]) : null;
|
||||
const startupWorkspace = configuration.workspace ?? configuration.files ?? configuration.workspace_files;
|
||||
if (startupWorkspace) {
|
||||
@@ -1633,10 +1641,12 @@ export class SimSession {
|
||||
};
|
||||
}
|
||||
}
|
||||
this.motionAuthority = configuration?.entrypoint?.includes("3axis-tutorial") || configuration?.kinematics?.type === "xyzac-trt" ? "wasm" : "js";
|
||||
if (configuration?.kinematics?.type === "xyzac-trt") {
|
||||
const configuredMachine = this.machineDefinition ?? machineDefinition(configuration?.kinematics?.type);
|
||||
const isTrt = configuredMachine?.runtime === "trt" || configuration?.kinematics?.type === "xyzac-trt" || configuration?.kinematics?.type === "xyzbc-trt";
|
||||
this.motionAuthority = configuration?.entrypoint?.includes("3axis-tutorial") || isTrt ? "wasm" : "js";
|
||||
if (isTrt) {
|
||||
this.xyzacKinematics?.destroy();
|
||||
this.xyzacKinematics = new WasmXyzacKinematics(this.core, { ...configuration.kinematics.offsets, limits: configuration.home_limits });
|
||||
this.xyzacKinematics = new WasmXyzacKinematics(this.core, { ...configuration.kinematics.offsets, coordinates: configuredMachine?.coordinates ?? configuration.kinematics.coordinates ?? "XYZAC", limits: configuration.home_limits });
|
||||
this.xyzacIo?.destroy();
|
||||
this.xyzacIo = new WasmXyzacIo(this.core, { spindleCount: 2, toolCapacity: 1001 });
|
||||
const initialPower = this.xyzacIo.command("POWER", { active: false });
|
||||
@@ -1652,7 +1662,7 @@ export class SimSession {
|
||||
// The selected XYZAC TRT case always uses the upstream LinuxCNC TP;
|
||||
// a caller must not silently downgrade it to the compatibility
|
||||
// motion queue by omitting the planner option.
|
||||
this.wasmMotion = configuration.kinematics.type === "xyzac-trt"
|
||||
this.wasmMotion = isTrt
|
||||
? new WasmLinuxcncTp(this.core, { cycleTime: this.xyzacMotionCycleTime, velocityLimit: this.xyzacRapidVelocity, accelerationLimit: Number(configuration.kinematics?.max_acceleration ?? 300), limits: configuration.home_limits ?? {} })
|
||||
: new WasmMotionQueue(this.core, { cycleTime: this.xyzacMotionCycleTime, velocityLimit: 1e9 });
|
||||
this.xyzacProgram = new WasmProgramRuntime(this.core);
|
||||
@@ -1662,9 +1672,10 @@ export class SimSession {
|
||||
this.xyzacRapidVelocity = Number(configuration.kinematics?.max_linear_velocity ?? 35);
|
||||
const linearLimit = Number(configuration.kinematics?.axis_linear_velocity_limit ?? 20);
|
||||
const angularLimit = Number(configuration.kinematics?.axis_angular_velocity_limit ?? 30);
|
||||
this.xyzacAxisVelocityLimits = { x: linearLimit, y: linearLimit, z: linearLimit, a: angularLimit, c: angularLimit };
|
||||
const rotaryAxis = (configuredMachine?.coordinates ?? "XYZAC")[3].toLowerCase();
|
||||
this.xyzacAxisVelocityLimits = { x: linearLimit, y: linearLimit, z: linearLimit, [rotaryAxis]: angularLimit, c: angularLimit };
|
||||
const accelerationLimit = Number(configuration.kinematics?.max_acceleration ?? 300);
|
||||
this.xyzacAxisAccelerationLimits = { x: accelerationLimit, y: accelerationLimit, z: accelerationLimit, a: accelerationLimit, c: accelerationLimit };
|
||||
this.xyzacAxisAccelerationLimits = { x: accelerationLimit, y: accelerationLimit, z: accelerationLimit, [rotaryAxis]: accelerationLimit, c: accelerationLimit };
|
||||
this.wasmMotion.setDynamics(Number(configuration.kinematics?.max_acceleration ?? 300), Number(configuration.kinematics?.max_jerk ?? 1000000000));
|
||||
this.xyzacProgram.setRapidVelocity(this.xyzacRapidVelocity);
|
||||
}
|
||||
@@ -1696,7 +1707,7 @@ export class SimSession {
|
||||
};
|
||||
}
|
||||
const configuredKinematics = input.payload?.kinematics ?? configuration?.kinematics?.type ?? null;
|
||||
if (configuredKinematics && !(this.motionAuthority === "wasm" && configuredKinematics === "xyzac-trt")) {
|
||||
if (configuredKinematics && !(this.motionAuthority === "wasm" && isTrt)) {
|
||||
const kinematics = this.authoritative.setKinematics(configuredKinematics);
|
||||
if (kinematics.status === "blocked") return finish(this.envelope("ERROR", input, { status: "blocked", code: kinematics.code, kinematics: this.authoritative.export() }), "failed");
|
||||
}
|
||||
@@ -1858,9 +1869,10 @@ export class SimSession {
|
||||
const currentState = this.wasmMotion.read().state;
|
||||
const current = currentState.position;
|
||||
this.xyzacProgramKinematicsMode = { 428: XYZAC_MODES.xyzac, 429: XYZAC_MODES.identity, 430: XYZAC_MODES.userk }[pendingControl];
|
||||
const rotaryAxis = this.machineDefinition?.coordinates === "XYZBC" ? "b" : "a";
|
||||
const joints = Array.isArray(currentState.joint_commanded) && currentState.joint_commanded.length === 5
|
||||
? currentState.joint_commanded
|
||||
: [current.x, current.y, current.z, current.a ?? 0, current.c ?? 0];
|
||||
: [current.x, current.y, current.z, current[rotaryAxis] ?? 0, current.c ?? 0];
|
||||
const selected = typeof this.xyzacKinematics.selectPhysical === "function"
|
||||
? this.xyzacKinematics.selectPhysical(this.xyzacProgramKinematicsMode, joints)
|
||||
: this.xyzacKinematics.select(this.xyzacProgramKinematicsMode, joints);
|
||||
@@ -2602,7 +2614,8 @@ export class SimSession {
|
||||
}
|
||||
if (input.kind === "FOLLOWING_ERROR") return finish(this.error(input, "motion_following_error", "wasm_following_error_unavailable"), "failed");
|
||||
const axis = String(input.payload?.axis ?? "").toLowerCase();
|
||||
if (!["x", "y", "z", "a", "c"].includes(axis)) return finish(this.error(input, "motion_axis", "motion_axis_invalid"), "failed");
|
||||
const availableAxes = this.machineDefinition?.axes ?? ["x", "y", "z", "a", "c"];
|
||||
if (!availableAxes.includes(axis)) return finish(this.error(input, "motion_axis", "motion_axis_invalid"), "failed");
|
||||
const direction = input.payload?.direction === -1 ? -1 : input.payload?.direction === 1 ? 1 : null;
|
||||
if (direction === null) return finish(this.error(input, "motion_jog", "motion_jog_range"), "failed");
|
||||
if (this.xyzacProgram) this.wasmHomeLimit.setMode("TELEOP");
|
||||
@@ -2618,7 +2631,7 @@ export class SimSession {
|
||||
// sample as a normal queued line and retain the same JOG_STOP abort
|
||||
// path. This compatibility branch is never used by XYZAC TRT.
|
||||
const jog = typeof this.wasmMotion.jog === "function"
|
||||
? this.wasmMotion.jog({ x: 0, y: 1, z: 2, a: 3, c: 5 }[axis], direction, distance, input.seq)
|
||||
? this.wasmMotion.jog({ x: 0, y: 1, z: 2, a: 3, b: 4, c: 5 }[axis], direction, distance, input.seq)
|
||||
: (() => {
|
||||
const current = this.wasmMotion.read();
|
||||
const start = current.state?.position ?? {};
|
||||
|
||||
@@ -152,7 +152,10 @@ export class WasmLinuxcncTp {
|
||||
}
|
||||
|
||||
commandedJoints(position, activeTag) {
|
||||
if (Number(activeTag) === 0) return [position.x, position.y, position.z, position.a ?? 0, position.c ?? 0];
|
||||
if (Number(activeTag) === 0) {
|
||||
const tilt = this.kinematics?.coordinates === "XYZBC" ? position.b : position.a;
|
||||
return [position.x, position.y, position.z, tilt ?? 0, position.c ?? 0];
|
||||
}
|
||||
const inverse = this.kinematics?.inverseMode?.(Number(activeTag), position) ?? this.kinematics?.inverse?.(position);
|
||||
if (inverse?.status === "verified" && Array.isArray(inverse.joints)) return inverse.joints.slice(0, 5);
|
||||
throw new Error(`linuxcnc_tp_joint_inverse_${inverse?.code ?? "unavailable"}`);
|
||||
@@ -204,7 +207,8 @@ export class WasmLinuxcncTp {
|
||||
const position = toPose(doubles);
|
||||
const joints = this.commandedJoints(position, this.activeTag);
|
||||
const velocityMmMin = Number(call(this.core, "sim_linuxcnc_tp_velocity_mm_min", "number", ["number"], [this.planner]));
|
||||
const state = { position, velocity: doubles[9], velocity_mm_min: velocityMmMin, acceleration: this.accelerationLimit, progress: doubles[10], tick: ints[0], queue_depth: this.queueDepthOverride === null ? ints[1] : this.queueDepthOverride, active_queue: activeId ? 1 : 0, active_id: activeId, active_tag: this.activeTag, joint_commanded: joints, joint_actual: joints.slice(), motion_type: this.core.HEAP32[this.statePointer / 4 + 25], status: this.core.HEAP32[this.statePointer / 4 + 26], exec_state: this.executionState, velocity_limit: velocityLimit, velocity_scale: velocityScale, jog_velocity_linear: this.jogVelocity("x"), jog_velocity_angular: this.jogVelocity("a"), feed_override: feedOverride, jog_scale: jogScale, source: "linuxcnc-source-tp" };
|
||||
const rotaryAxis = this.kinematics?.coordinates === "XYZBC" ? "b" : "a";
|
||||
const state = { position, velocity: doubles[9], velocity_mm_min: velocityMmMin, acceleration: this.accelerationLimit, progress: doubles[10], tick: ints[0], queue_depth: this.queueDepthOverride === null ? ints[1] : this.queueDepthOverride, active_queue: activeId ? 1 : 0, active_id: activeId, active_tag: this.activeTag, joint_commanded: joints, joint_actual: joints.slice(), motion_type: this.core.HEAP32[this.statePointer / 4 + 25], status: this.core.HEAP32[this.statePointer / 4 + 26], exec_state: this.executionState, velocity_limit: velocityLimit, velocity_scale: velocityScale, jog_velocity_linear: this.jogVelocity("x"), jog_velocity_angular: this.jogVelocity(rotaryAxis), feed_override: feedOverride, jog_scale: jogScale, source: "linuxcnc-source-tp" };
|
||||
this.lastState = state;
|
||||
return { status: "verified", state };
|
||||
}
|
||||
|
||||
@@ -13,11 +13,10 @@ const CONFIG = Object.freeze({
|
||||
limit_enabled: 1448, joint_min: 1464, joint_max: 1536,
|
||||
});
|
||||
const SWITCH = Object.freeze({ switching: 16, active_mode: 20, pending_mode: 24, generation: 28, switch_count: 32, abort_count: 36 });
|
||||
const TYPE = Object.freeze({ identity: 1, xyzac: 2, userk: 1 });
|
||||
const TYPE = Object.freeze({ identity: 1, xyzac: 2, xyzbc: 3, userk: 1 });
|
||||
const MODE = Object.freeze({ identity: 0, xyzac: 1, userk: 2 });
|
||||
const AXES = Object.freeze(["x", "y", "z", "a", "b", "c", "u", "v", "w"]);
|
||||
const COORDINATES = Object.freeze(["x", "y", "z", "a", "c"]);
|
||||
const DEFAULT_LIMITS = Object.freeze({ x: [-200, 200], y: [-100, 100], z: [-120, 120], a: [-100, 50], c: [-36000, 36000] });
|
||||
const DEFAULT_LIMITS = Object.freeze({ x: [-200, 200], y: [-100, 100], z: [-120, 120], a: [-100, 50], b: [-100, 50], c: [-36000, 36000] });
|
||||
|
||||
function call(core, name, returnType, argTypes, args) {
|
||||
const direct = core?.[`_${name}`] ?? core?.[name];
|
||||
@@ -31,11 +30,11 @@ function pose(core, pointer) {
|
||||
return Object.fromEntries(AXES.map((axis, index) => [axis, doubles[index]]));
|
||||
}
|
||||
|
||||
function setCoordinates(core, pointer) {
|
||||
for (const [index, value] of [..."XYZAC"].entries()) core.HEAPU8[pointer + CONFIG.coordinates + index] = value.charCodeAt(0);
|
||||
function setCoordinates(core, pointer, coordinates) {
|
||||
for (const [index, value] of [...coordinates].entries()) core.HEAPU8[pointer + CONFIG.coordinates + index] = value.charCodeAt(0);
|
||||
}
|
||||
|
||||
function configFor(core, pointer, type, options) {
|
||||
function configFor(core, pointer, type, options, coordinates) {
|
||||
core.HEAPU8.fill(0, pointer, pointer + CONFIG_SIZE);
|
||||
const view = new DataView(core.HEAPU8.buffer, pointer, CONFIG_SIZE);
|
||||
view.setInt32(CONFIG.type, type, true);
|
||||
@@ -43,14 +42,22 @@ function configFor(core, pointer, type, options) {
|
||||
view.setUint32(CONFIG.joint_count, 5, true);
|
||||
view.setUint32(CONFIG.kinematics_joint_count, 5, true);
|
||||
view.setUint32(CONFIG.extra_joint_count, 0, true);
|
||||
setCoordinates(core, pointer);
|
||||
for (const [index, axis] of COORDINATES.entries()) {
|
||||
setCoordinates(core, pointer, coordinates);
|
||||
for (const [index, axis] of coordinates.toLowerCase().split("").entries()) {
|
||||
const limit = options.limits?.[axis] ?? DEFAULT_LIMITS[axis];
|
||||
core.HEAPU8[pointer + CONFIG.limit_enabled + index] = 1;
|
||||
view.setFloat64(CONFIG.joint_min + index * 8, Number(limit[0]), true);
|
||||
view.setFloat64(CONFIG.joint_max + index * 8, Number(limit[1]), true);
|
||||
}
|
||||
for (const [key, offset] of Object.entries({ x_rot_point: CONFIG.x_rot_point, y_offset: CONFIG.y_offset, z_offset: CONFIG.z_offset, tool_offset: CONFIG.tool_offset })) {
|
||||
for (const [key, offset] of Object.entries({
|
||||
x_rot_point: CONFIG.x_rot_point,
|
||||
y_rot_point: CONFIG.y_rot_point,
|
||||
z_rot_point: CONFIG.z_rot_point,
|
||||
x_offset: CONFIG.x_offset,
|
||||
y_offset: CONFIG.y_offset,
|
||||
z_offset: CONFIG.z_offset,
|
||||
tool_offset: CONFIG.tool_offset,
|
||||
})) {
|
||||
view.setFloat64(offset, Number(options[key] ?? 0), true);
|
||||
}
|
||||
}
|
||||
@@ -62,17 +69,21 @@ function resultCode(operation, result) {
|
||||
export class WasmXyzacKinematics {
|
||||
constructor(core, options = {}) {
|
||||
this.core = core;
|
||||
this.options = { x_rot_point: 0, y_offset: 20, z_offset: 10, tool_offset: 0, limits: DEFAULT_LIMITS, ...options };
|
||||
this.coordinates = String(options.coordinates ?? "XYZAC").toUpperCase();
|
||||
if (!["XYZAC", "XYZBC"].includes(this.coordinates)) throw new Error("wasm_trt_coordinates_invalid");
|
||||
this.kinematicsType = this.coordinates === "XYZBC" ? "xyzbc-trt-kins" : "xyzac-trt-kins";
|
||||
this.options = { x_rot_point: 0, y_rot_point: 0, z_rot_point: 0, x_offset: 0, y_offset: 20, z_offset: 10, tool_offset: 0, limits: DEFAULT_LIMITS, ...options };
|
||||
if (this.coordinates === "XYZBC" && options.limits === undefined) this.options.limits = { x: [-200, 200], y: [-100, 100], z: [-120, 120], b: [-100, 50], c: [-36000, 36000] };
|
||||
this.available = ["sim_kinematics_init", "sim_kinematics_switch_init", "sim_kinematics_switch_register", "sim_kinematics_switch_select", "sim_kinematics_switch_select_physical", "sim_kinematics_switch_forward", "sim_kinematics_switch_inverse", "sim_kinematics_switch_inverse_mode", "sim_kinematics_switch_active", "sim_kinematics_switch_state_hash"].every((name) => typeof core?.[`_${name}`] === "function" || typeof core?.[name] === "function");
|
||||
this.lastCommand = null;
|
||||
this.modules = new Map();
|
||||
this.pointers = [];
|
||||
if (!this.available || typeof core?._malloc !== "function") return;
|
||||
for (const [mode, type] of [[MODE.identity, TYPE.identity], [MODE.xyzac, TYPE.xyzac], [MODE.userk, TYPE.userk]]) {
|
||||
for (const [mode, type] of [[MODE.identity, TYPE.identity], [MODE.xyzac, this.coordinates === "XYZBC" ? TYPE.xyzbc : TYPE.xyzac], [MODE.userk, TYPE.userk]]) {
|
||||
const config = core._malloc(CONFIG_SIZE);
|
||||
const kinematics = core._malloc(KINEMATICS_SIZE);
|
||||
if (!config || !kinematics) throw new Error("wasm_xyzac_allocation_failed");
|
||||
configFor(core, config, type, this.options);
|
||||
configFor(core, config, type, this.options, this.coordinates);
|
||||
const result = call(core, "sim_kinematics_init", "number", ["number", "number"], [kinematics, config]);
|
||||
if (result !== 0) throw new Error(`wasm_xyzac_init_${result}`);
|
||||
this.modules.set(mode, { config, kinematics });
|
||||
@@ -185,10 +196,10 @@ export class WasmXyzacKinematics {
|
||||
|
||||
snapshot(currentPose = pose(this.core, this.posePointer), joints = [...this.core.HEAPF64.subarray(this.jointPointer / 8, this.jointPointer / 8 + 5)]) {
|
||||
const activeMode = this.activeMode;
|
||||
const nativeType = activeMode === MODE.xyzac ? "xyzac-trt-kins" : activeMode === MODE.userk ? "userk-identity-template" : "identity";
|
||||
const nativeType = activeMode === MODE.xyzac ? this.kinematicsType : activeMode === MODE.userk ? "userk-identity-template" : "identity";
|
||||
const view = new DataView(this.core.HEAPU8.buffer, this.switchPointer, SWITCH_SIZE);
|
||||
const limits = Object.fromEntries(COORDINATES.map((axis) => [axis, [...(this.options.limits?.[axis] ?? DEFAULT_LIMITS[axis])]]));
|
||||
const unsigned = { schema: "wasm-xyzac-kinematics.v1", active_type: activeMode, native_type: nativeType, coordinates: "XYZAC", joint_count: 5, linear_units: "mm", angular_units: "deg", pose: currentPose, joints: joints.slice(0, 5), offsets: { x_rot_point: this.options.x_rot_point, y_offset: this.options.y_offset, z_offset: this.options.z_offset, tool_offset: this.options.tool_offset }, limits, generation: view.getUint32(SWITCH.generation, true), switch_count: view.getUint32(SWITCH.switch_count, true), abort_count: view.getUint32(SWITCH.abort_count, true), last_command: this.lastCommand, capability: { userk: "linked_identity_template", dynamic_modules: "denied" }, source: "linuxcnc-wasm-sim_kinematics_switch" };
|
||||
const limits = Object.fromEntries(this.coordinates.toLowerCase().split("").map((axis) => [axis, [...(this.options.limits?.[axis] ?? DEFAULT_LIMITS[axis])]]));
|
||||
const unsigned = { schema: "wasm-xyzac-kinematics.v1", active_type: activeMode, native_type: nativeType, coordinates: this.coordinates, joint_count: 5, linear_units: "mm", angular_units: "deg", pose: currentPose, joints: joints.slice(0, 5), offsets: { x_rot_point: this.options.x_rot_point, y_rot_point: this.options.y_rot_point, z_rot_point: this.options.z_rot_point, x_offset: this.options.x_offset, y_offset: this.options.y_offset, z_offset: this.options.z_offset, tool_offset: this.options.tool_offset }, limits, generation: view.getUint32(SWITCH.generation, true), switch_count: view.getUint32(SWITCH.switch_count, true), abort_count: view.getUint32(SWITCH.abort_count, true), last_command: this.lastCommand, capability: { userk: "linked_identity_template", dynamic_modules: "denied" }, source: "linuxcnc-wasm-sim_kinematics_switch" };
|
||||
const hash = call(this.core, "sim_kinematics_switch_state_hash", "number", ["number", "number", "number", "number"], [this.switchPointer, this.jointPointer, 5, this.posePointer]);
|
||||
return { status: "verified", ...unsigned, wasm_state_hash: `fnv1a64:${BigInt.asUintN(64, BigInt(hash >>> 0)).toString(16).padStart(16, "0")}`, state_hash: fnv1a64(unsigned) };
|
||||
}
|
||||
|
||||
28
web/workspace/machine-definitions.mjs
Normal file
28
web/workspace/machine-definitions.mjs
Normal file
@@ -0,0 +1,28 @@
|
||||
const TRT_ROOT = "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting";
|
||||
|
||||
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`,
|
||||
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)}`],
|
||||
offsets,
|
||||
joint_limits: { x: [-200, 200], y: [-100, 100], z: [-120, 120], [stem[3]]: [-100, 50], c: [-36000, 36000] },
|
||||
axis_limits: { x: [-200, 200], y: [-100, 100], z: [-120, 120], [stem[3]]: coordinates === "XYZAC" ? [-100, 50] : [-36000, 36000], c: [-36000, 36000] },
|
||||
});
|
||||
}
|
||||
|
||||
export const MACHINE_DEFINITIONS = Object.freeze([
|
||||
trt("xyzac-trt", "XYZAC", 2, { x_rot_point: 0, y_offset: 20, z_offset: 10, tool_offset: 0 }, "trt-moving-knee"),
|
||||
trt("xyzbc-trt", "XYZBC", 3, { x_rot_point: 0, y_rot_point: 0, z_rot_point: 0, x_offset: -20, y_offset: 0, z_offset: -15, tool_offset: 0 }, "trt-moving-head"),
|
||||
]);
|
||||
export const DEFAULT_MACHINE = MACHINE_DEFINITIONS[0];
|
||||
export function machineDefinition(value) {
|
||||
return MACHINE_DEFINITIONS.find((entry) => [entry.id, entry.entrypoint, entry.coordinates].includes(value)) ?? null;
|
||||
}
|
||||
export function machineAxes(snapshot) {
|
||||
const coordinates = snapshot?.kinematics?.coordinates ?? snapshot?.visual?.machine_model?.coordinate_system;
|
||||
return machineDefinition(coordinates)?.axes ?? DEFAULT_MACHINE.axes;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { fnv1a64 } from "../worker/replay.mjs";
|
||||
import { runtimeResourceFixture } from "./resource-resolver.mjs";
|
||||
import { SIM_CASE_CATALOG, SIM_FILE_INDEX, SIM_HAL_COMPONENTS } from "./sim-case-catalog.mjs";
|
||||
import { isOptionalSourceKey } from "./linuxcnc-source-semantics.mjs";
|
||||
import { MACHINE_DEFINITIONS, machineDefinition } from "./machine-definitions.mjs";
|
||||
|
||||
export const SIMULATION_CASE_SCHEMA = "simulation-case-bundle.v1";
|
||||
const RESOURCE_KEYS = new Set([
|
||||
@@ -17,6 +18,7 @@ const CASE_KINEMATICS = Object.freeze({
|
||||
"linuxcnc/configs/sim/axis/ldelta.ini": Object.freeze({ type: "lineardelta", path_mode: "kins" }),
|
||||
"linuxcnc/configs/sim/axis/rdelta.ini": Object.freeze({ type: "rotarydelta", path_mode: "kins" }),
|
||||
"linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini": Object.freeze({ type: "xyzac-trt", path_mode: "wasm", coordinates: "XYZAC", joint_count: 5, max_linear_velocity: 35, axis_linear_velocity_limit: 20, max_acceleration: 300, max_jerk: 1000000000, servo_cycle_time: 0.001, task_cycle_time: 0.01, cycle_time: 0.001, switchkins: { 0: "identity", 1: "xyzac", 2: "userk" }, offsets: { x_rot_point: 0, y_offset: 20, z_offset: 10, tool_offset: 0 } }),
|
||||
"linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini": Object.freeze({ type: "xyzbc-trt", path_mode: "wasm", coordinates: "XYZBC", joint_count: 5, max_linear_velocity: 35, axis_linear_velocity_limit: 20, axis_angular_velocity_limit: 30, max_acceleration: 300, max_jerk: 1000000000, servo_cycle_time: 0.001, task_cycle_time: 0.01, cycle_time: 0.001, switchkins: { 0: "identity", 1: "xyzbc", 2: "userk" }, 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 } }),
|
||||
});
|
||||
const RUNTIME_FIXTURES = Object.freeze({
|
||||
"linuxcnc/configs/sim/axis/minimal_xyz.ini": Object.freeze({
|
||||
@@ -49,6 +51,12 @@ const RUNTIME_FIXTURES = Object.freeze({
|
||||
with: "HALFILE = xyzac-trt-headless_cmds.hal",
|
||||
reason: "LinuxCNC basic_sim.tcl generated XYZAC HAL command fixture",
|
||||
}),
|
||||
"linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini": Object.freeze({
|
||||
generated_hal: "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt_cmds.hal",
|
||||
replace: "HALFILE = LIB:basic_sim.tcl",
|
||||
with: "HALFILE = xyzbc-trt_cmds.hal",
|
||||
reason: "LinuxCNC basic_sim.tcl generated XYZBC HAL command fixture",
|
||||
}),
|
||||
});
|
||||
|
||||
function clone(value) { return structuredClone(value); }
|
||||
@@ -117,7 +125,7 @@ function generatedResource(path, key) {
|
||||
return generated ? { ...generated, data_hash: fnv1a64(generated.content) } : null;
|
||||
}
|
||||
function inManifest(path) {
|
||||
return MANIFEST.has(path) || path === "linuxcnc/configs/nc_files/3dtest.ngc";
|
||||
return MANIFEST.has(path) || path === "linuxcnc/configs/nc_files/3dtest.ngc" || path === "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt_cmds.hal";
|
||||
}
|
||||
function responseUrl(baseUrl, path) { return `${String(baseUrl ?? "").replace(/\/$/, "")}/${path}`.replace(/^\//, "/"); }
|
||||
|
||||
@@ -218,8 +226,9 @@ export async function loadSimulationCase({ path, fetchImpl = globalThis.fetch, b
|
||||
// the Web renderer. It is not referenced as a normal INI resource, so
|
||||
// resolve it explicitly and carry it through the Worker configuration for
|
||||
// source hashing and model provenance.
|
||||
if (catalog.path === "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini") {
|
||||
const visualSource = "linuxcnc/src/hal/user_comps/vismach/xyzac-trt-gui.py";
|
||||
const machine = machineDefinition(catalog.path);
|
||||
if (machine) {
|
||||
const visualSource = machine.source_path;
|
||||
if (!(visualSource in files)) {
|
||||
const content = await fetchText(fetchImpl, baseUrl, visualSource);
|
||||
if (content === null) missing.push(visualSource);
|
||||
@@ -269,7 +278,7 @@ export function toWorkerConfiguration(bundle) {
|
||||
optional_missing: clone(bundle.optional_missing ?? []),
|
||||
runtime_fixtures: clone(bundle.runtime_fixtures ?? []),
|
||||
...(bundle.entrypoint === "linuxcnc/configs/sim/axis/vismach/3axis-tutorial/3axis-tutorial.ini" ? { home_limits: { z: [-240, 100] } } : {}),
|
||||
...(bundle.entrypoint === "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini" ? { home_limits: { joint_count: 5, kinematics_type: 2, coordinates: "XYZAC", x: [-200, 200], y: [-100, 100], z: [-120, 120], a: [-100, 50], c: [-36000, 36000] } } : {}),
|
||||
...(machineDefinition(bundle.entrypoint) ? { home_limits: { joint_count: 5, kinematics_type: machineDefinition(bundle.entrypoint).kinematics_type, coordinates: machineDefinition(bundle.entrypoint).coordinates, ...machineDefinition(bundle.entrypoint).joint_limits } } : {}),
|
||||
...(CASE_KINEMATICS[bundle.entrypoint] ? { kinematics: clone(CASE_KINEMATICS[bundle.entrypoint]) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user