继续完成 web-rtcp-5axis-sim-plan

结论:完成 LinuxCNC kinematics WASM ABI 覆盖,并将 web-rtcp-5axis-sim-plan 的 RTCP frame/boundary adapter 接到 xyzac-trt kinematics SDK;Node、build、browser smoke 验证通过。
This commit is contained in:
2026-06-21 16:44:29 +08:00
parent a6eda3fbff
commit 626bcfe8e3
101 changed files with 101586 additions and 770 deletions

View File

@@ -0,0 +1,355 @@
import { renderFiveAxisScene } from "../visualization/five-axis-scene.js";
const REGIONS = [
"titlebar",
"preview",
"dro",
"gcode",
"status-sidebar",
"info-tabs",
"override",
"spindle-coolant",
"bottom-controls",
];
export function mountGmoccapyShell(root, store) {
root.innerHTML = `
<section class="gmoccapy-shell" data-shell="gmoccapy-5axis">
<header class="titlebar" data-region="titlebar"></header>
<section class="preview-panel" data-region="preview"></section>
<section class="dro-panel" data-region="dro"></section>
<section class="gcode-panel" data-region="gcode"></section>
<aside class="status-sidebar" data-region="status-sidebar"></aside>
<section class="info-tabs" data-region="info-tabs"></section>
<section class="override-panel" data-region="override"></section>
<section class="spindle-coolant-panel" data-region="spindle-coolant"></section>
<footer class="bottom-controls" data-region="bottom-controls"></footer>
</section>
`;
const regions = Object.fromEntries(
REGIONS.map((name) => [name, root.querySelector(`[data-region="${name}"]`)]),
);
store.subscribe((state) => render(regions, state, store.dispatch));
return {
getRegions() {
return Object.fromEntries(
Object.entries(regions).map(([name, element]) => [name, Boolean(element)]),
);
},
};
}
function render(regions, state, dispatch) {
renderTitlebar(regions.titlebar, state);
renderPreview(regions.preview, state, dispatch);
renderDro(regions.dro, state);
renderGcode(regions.gcode, state);
renderSidebar(regions["status-sidebar"], state, dispatch);
renderInfoTabs(regions["info-tabs"], state);
renderOverride(regions.override, state, dispatch);
renderSpindleCoolant(regions["spindle-coolant"], state, dispatch);
renderBottomControls(regions["bottom-controls"], state, dispatch);
}
function renderTitlebar(element, state) {
element.innerHTML = `
<div class="brand-dot" aria-hidden="true">NS</div>
<div class="title-stack">
<strong>gmoccapy Web 5 Axis for LinuxCNC RTCP Simulation</strong>
<span>${state.machineProfile} | ${state.sessionName} | ${state.sourceMode} | ${state.machine.mode}</span>
</div>
<div class="run-state" data-run-state="${state.runState}">${state.runState}</div>
`;
}
function renderPreview(element, state, dispatch) {
const tcp = state.tcpPose;
const tool = state.toolAxisVector;
element.innerHTML = `
<div class="program-path">${escapeHtml(state.activeProgram)}</div>
<canvas class="machine-preview" data-five-axis-canvas="true" aria-label="5 axis Three.js preview"></canvas>
<div class="tool-preview-card" data-tool-preview="summary">
<strong>T${state.toolPreview.toolNumber}</strong>
<span>D ${formatNumber(state.toolPreview.diameter, 2)} ${state.toolPreview.units}</span>
<span>L ${formatNumber(state.toolPreview.length, 3)} ${state.toolPreview.units}</span>
<span>${state.toolPreview.holder}</span>
</div>
<div class="rtcp-preview-badge" data-rtcp-preview-state="${state.rtcpState}">
RTCP ${state.rtcpState} | TCP ${formatNumber(tcp.x)} ${formatNumber(tcp.y)} ${formatNumber(tcp.z)}
| V ${formatNumber(tool.x, 3)} ${formatNumber(tool.y, 3)} ${formatNumber(tool.z, 3)}
</div>
<div class="preview-toolbar" data-preview-points="${state.preview.pathPoints}">
<button type="button" data-action="view-x">X</button>
<button type="button" data-action="view-y">Y</button>
<button type="button" data-action="view-z">Z</button>
<button type="button" data-action="reset-view">Fit</button>
<button type="button" data-action="clear-preview">Clear</button>
</div>
`;
element.querySelector('[data-action="reset-view"]').addEventListener("click", () => {
dispatch({ type: "RESET_VIEW" });
});
element.querySelector('[data-action="clear-preview"]').addEventListener("click", () => {
dispatch({ type: "CLEAR_PREVIEW" });
});
for (const view of ["x", "y", "z"]) {
element.querySelector(`[data-action="view-${view}"]`).addEventListener("click", () => {
dispatch({ type: "SET_VIEW", view });
});
}
renderFiveAxisScene(element.querySelector("[data-five-axis-canvas]"), state);
}
function renderDro(element, state) {
const { dro } = state;
const tool = state.toolAxisVector;
element.innerHTML = `
<div class="dro-grid">
${droRow("X", dro.x, dro.dtgX)}
${droRow("Y", dro.y, dro.dtgY)}
${droRow("Z", dro.z, dro.dtgZ)}
${droRow("A", dro.a, 0)}
${droRow("B", dro.b, 0)}
${droRow("C", dro.c, 0)}
</div>
<div class="tcp-strip">
<span data-rtcp-value="tcp">TCP ${formatNumber(dro.tcpX)} / ${formatNumber(dro.tcpY)} / ${formatNumber(dro.tcpZ)}</span>
<span data-rtcp-value="tool-axis">V ${formatNumber(tool.x, 3)} / ${formatNumber(tool.y, 3)} / ${formatNumber(tool.z, 3)}</span>
<span data-rtcp-value="state">RTCP ${state.rtcpState}</span>
<span>KINS ${state.kinsType}</span>
</div>
`;
}
function droRow(axis, value, dtg) {
return `
<div class="dro-row">
<span class="dro-axis">${axis}</span>
<span class="dro-mode">G54<br />Abs</span>
<strong>${formatNumber(value)}</strong>
<span class="dro-dtg">DTG<br />${formatNumber(dtg, 3)}</span>
</div>
`;
}
function renderGcode(element, state) {
const rows = state.programLines
.map((line, index) => {
const lineNumber = state.programStartLine + index;
const active = lineNumber === state.activeLine ? " active" : "";
return `<li class="gcode-row${active}" data-program-line="${lineNumber}"><span>${lineNumber}</span><code>${escapeHtml(line)}</code></li>`;
})
.join("");
const programEndLine = state.programStartLine + Math.max(state.programLines.length - 1, 0);
const progressSpan = Math.max(programEndLine - state.programStartLine, 1);
const progress = Math.min(
Math.max(((state.activeLine - state.programStartLine) / progressSpan) * 100, 0),
100,
);
element.innerHTML = `
<div class="gcode-header">
<strong>${escapeHtml(state.activeProgram)}</strong>
<span data-program-source="${state.programSource}">${state.programSource}</span>
<span data-active-program-line="${state.activeLine}">Current line ${state.activeLine}</span>
</div>
<ol class="gcode-list" start="${state.programStartLine}">${rows}</ol>
<div class="gcode-progress">
<span>${state.activeLine} / ${programEndLine}</span>
<div><i style="width: ${progress}%"></i></div>
</div>
`;
}
function renderSidebar(element, state, dispatch) {
element.innerHTML = `
<button type="button" class="sidebar-button estop" data-action="estop" data-active="${state.machine.estopActive}">E-STOP</button>
<button type="button" class="sidebar-button power" data-action="power" data-active="${state.machine.powerOn}">POWER</button>
<button type="button" class="sidebar-button" data-action="reset">RESET</button>
<button type="button" class="sidebar-button" data-action="mode-auto" data-active="${state.machine.mode === "auto"}">AUTO</button>
<button type="button" class="sidebar-button" data-action="mode-manual" data-active="${state.machine.mode === "manual"}">MANUAL</button>
<button type="button" class="sidebar-button" data-action="mode-jog" data-active="${state.machine.mode === "jog"}">JOG</button>
<button type="button" class="sidebar-button" data-action="mode-mdi" data-active="${state.machine.mode === "mdi"}">MDI</button>
<button type="button" class="sidebar-button" data-action="kins-identity" data-active="${state.kinsType === "identity"}">IDENTITY</button>
<button type="button" class="sidebar-button" data-action="kins-tcp" data-active="${state.kinsType === "tcp-xyzac"}">TCP</button>
<time>13:30:31<br />20.06.2026</time>
`;
element.querySelector('[data-action="estop"]').addEventListener("click", () => dispatch({ type: "ESTOP" }));
element.querySelector('[data-action="power"]').addEventListener("click", () => dispatch({ type: "TOGGLE_POWER" }));
element.querySelector('[data-action="reset"]').addEventListener("click", () => dispatch({ type: "RESET" }));
element.querySelector('[data-action="mode-auto"]').addEventListener("click", () => dispatch({ type: "SET_MODE", mode: "auto" }));
element.querySelector('[data-action="mode-manual"]').addEventListener("click", () => dispatch({ type: "SET_MODE", mode: "manual" }));
element.querySelector('[data-action="mode-jog"]').addEventListener("click", () => dispatch({ type: "SET_MODE", mode: "jog" }));
element.querySelector('[data-action="mode-mdi"]').addEventListener("click", () => dispatch({ type: "SET_MODE", mode: "mdi" }));
element.querySelector('[data-action="kins-identity"]').addEventListener("click", () => {
dispatch({ type: "SET_KINS_TYPE", kinsType: "identity" });
});
element.querySelector('[data-action="kins-tcp"]').addEventListener("click", () => {
dispatch({ type: "SET_KINS_TYPE", kinsType: "tcp-xyzac" });
});
}
function renderInfoTabs(element, state) {
const frame = state.rtcpFrame;
element.innerHTML = `
<nav class="tabs">
<button type="button" class="active">Tool info and G-codes</button>
<button type="button">G-code properties</button>
<button type="button">RTCP diagnostics</button>
</nav>
<dl class="info-grid">
<dt>Size:</dt><dd>${state.fileSizeBytes} bytes</dd>
<dt>Lines:</dt><dd>${state.lineCount} gcode lines</dd>
<dt>Machine:</dt><dd data-machine-state="summary">${state.machine.powerOn ? "power on" : "power off"} / ${state.machine.estopActive ? "estop" : "clear"} / ${state.machine.mode}</dd>
<dt>Current line:</dt><dd data-program-current-line="${state.activeLine}">${state.activeLine}</dd>
<dt>Tool preview:</dt><dd data-tool-preview="detail">T${state.toolPreview.toolNumber} D${formatNumber(state.toolPreview.diameter, 2)} L${formatNumber(state.toolPreview.length, 3)} ${state.toolPreview.units}</dd>
<dt>Rapid distance:</dt><dd>37.634 mm</dd>
<dt>Feed distance:</dt><dd>5814.069 mm</dd>
<dt>X bounds:</dt><dd>8.000 to 113.000 = 105.000 mm</dd>
<dt>Y bounds:</dt><dd>3.872 to 116.128 = 112.256 mm</dd>
<dt>Z bounds:</dt><dd>-90.500 to -50.000 = 40.500 mm</dd>
<dt>RTCP frame:</dt><dd data-rtcp-diagnostic="frame">${frame.apiName} ${frame.rtcpState}</dd>
<dt>Boundary:</dt><dd data-rtcp-diagnostic="boundary">${frame.semanticBoundary}</dd>
<dt>LinuxCNC kins:</dt><dd data-rtcp-diagnostic="kinematics-ready">${frame.readiness.linuxCncKinematicsReady ? "ready" : "pending"}</dd>
<dt>Profile refs:</dt><dd>${state.profile.sourceReferences.length} source references</dd>
<dt>Adapter:</dt><dd data-linuxcnc-boundary="adapter">${state.linuxCncBoundaryAdapter.apiName}</dd>
<dt>Panel schema:</dt><dd data-linuxcnc-boundary="panel">${state.linuxCncBoundaryAdapter.panelSummary.schemaId} / ${state.linuxCncBoundaryAdapter.panelSummary.buttonCount} buttons</dd>
<dt>Source map:</dt><dd data-linuxcnc-boundary="source-map">${state.linuxCncBoundaryAdapter.sourceSummary.referenceCount} refs / ${state.linuxCncBoundaryAdapter.sourceSummary.sourceRequiredCount} source files</dd>
<dt>Profile summary:</dt><dd data-linuxcnc-boundary="profile-summary">${state.linuxCncBoundaryAdapter.profileSummary.coordinates} / ${state.linuxCncBoundaryAdapter.profileSummary.jointCount} joints / ${state.linuxCncBoundaryAdapter.profileSummary.toolCount} tools</dd>
<dt>Boundary ready:</dt><dd data-linuxcnc-boundary="readiness">${state.linuxCncBoundaryReadiness.ready ? "ready" : "blocked"} (${state.linuxCncBoundaryReadiness.missing.join(", ")})</dd>
</dl>
`;
}
function renderOverride(element, state, dispatch) {
element.innerHTML = `
<section class="meter-card">
<h2>Current Velocity</h2>
<strong>${state.feed.currentVelocity}</strong><span> mm/min</span>
</section>
${overrideControl("Rapid Override", "rapid", state.feed.rapidOverride)}
${overrideControl("Feed Rate", "feed", state.feed.feedOverride, `F ${state.feed.feedRate}`)}
`;
for (const target of ["rapid", "feed"]) {
element.querySelector(`[data-action="${target}-override-down"]`).addEventListener("click", () => {
dispatch({ type: "ADJUST_OVERRIDE", target, delta: -10 });
});
element.querySelector(`[data-action="${target}-override-up"]`).addEventListener("click", () => {
dispatch({ type: "ADJUST_OVERRIDE", target, delta: 10 });
});
}
}
function overrideControl(label, target, value, prefix = "") {
return `
<section class="override-control" data-control="${target}-override">
<h2>${label}</h2>
${prefix ? `<strong>${prefix}</strong>` : ""}
<div class="stepper">
<button type="button" data-action="${target}-override-down">-</button>
<div data-value="${target}-override">${value} %</div>
<button type="button" data-action="${target}-override-up">+</button>
</div>
</section>
`;
}
function renderSpindleCoolant(element, state, dispatch) {
element.innerHTML = `
<section class="cooling">
<h2>Cooling</h2>
<button type="button" data-action="toggle-flood" class="${state.coolant.flood ? "active" : ""}">Flood</button>
<button type="button" data-action="toggle-mist" class="${state.coolant.mist ? "active" : ""}">Mist</button>
</section>
<section class="spindle">
<h2>Spindle</h2>
<strong>${state.spindle.rpm}</strong><span> rpm</span>
<div class="stepper">
<button type="button" data-action="spindle-override-down">-</button>
<div data-value="spindle-override">${state.spindle.override} %</div>
<button type="button" data-action="spindle-override-up">+</button>
</div>
<div class="spindle-range"><i style="width: ${(state.spindle.rpm / 6000) * 100}%"></i></div>
</section>
`;
element.querySelector('[data-action="toggle-flood"]').addEventListener("click", () => {
dispatch({ type: "TOGGLE_COOLANT", kind: "flood" });
});
element.querySelector('[data-action="toggle-mist"]').addEventListener("click", () => {
dispatch({ type: "TOGGLE_COOLANT", kind: "mist" });
});
element.querySelector('[data-action="spindle-override-down"]').addEventListener("click", () => {
dispatch({ type: "ADJUST_SPINDLE_OVERRIDE", delta: -10 });
});
element.querySelector('[data-action="spindle-override-up"]').addEventListener("click", () => {
dispatch({ type: "ADJUST_SPINDLE_OVERRIDE", delta: 10 });
});
}
function renderBottomControls(element, state, dispatch) {
const controls = [
["Open", "OPEN", null],
["Reload", "RELOAD", () => dispatch({ type: "RELOAD_PROGRAM" })],
["Run", "RUN", () => dispatch({ type: "RUN" })],
["Stop", "STOP", () => dispatch({ type: "STOP" })],
["Pause", "PAUSE", () => dispatch({ type: "PAUSE" })],
["Step", "STEP", () => dispatch({ type: "STEP" })],
["Home", "HOME", () => dispatch({ type: "HOME" })],
["X-", "JOG_X_NEG", () => dispatch({ type: "JOG", axis: "x", direction: -1 })],
["X+", "JOG_X_POS", () => dispatch({ type: "JOG", axis: "x", direction: 1 })],
["Y-", "JOG_Y_NEG", () => dispatch({ type: "JOG", axis: "y", direction: -1 })],
["Y+", "JOG_Y_POS", () => dispatch({ type: "JOG", axis: "y", direction: 1 })],
["MDI", "MDI_RUN", () => dispatch({ type: "RUN_MDI" })],
["Full", "FULL", () => dispatch({ type: "TOGGLE_FULLSCREEN" })],
];
element.innerHTML = `
<input type="file" class="program-file-input" data-action="OPEN_FILE" accept=".ngc,.nc,.tap,.gcode,.txt" />
${controls
.map(([label, action]) => `<button type="button" data-action="${action}">${label}</button>`)
.join("")}
`;
element.querySelector('[data-action="OPEN"]').addEventListener("click", () => {
element.querySelector('[data-action="OPEN_FILE"]').click();
});
element.querySelector('[data-action="OPEN_FILE"]').addEventListener("change", async (event) => {
const [file] = event.target.files || [];
if (!file) return;
const content = await file.text();
dispatch({
type: "LOAD_PROGRAM",
filename: file.name,
content,
});
event.target.value = "";
});
for (const [label, action, handler] of controls) {
if (!handler) continue;
const button = element.querySelector(`[data-action="${action}"]`);
button.addEventListener("click", handler);
button.setAttribute("aria-label", label);
}
}
function formatNumber(value, digits = 3) {
return Number(value).toFixed(digits);
}
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}