继续完成 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 验证通过。
1
web-rtcp-5axis-sim-plan/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
app/dist/
|
||||
29
web-rtcp-5axis-sim-plan/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Web RTCP 五轴联动数控系统仿真方案
|
||||
|
||||
生成时间:2026-06-20 CST
|
||||
|
||||
本目录是新的独立方案目录,用于规划一个基于 Web 的、带 RTCP 功能的五轴联动数控系统仿真界面。
|
||||
|
||||
项目目标不是开发真实机床控制器,也不是在浏览器中替代 LinuxCNC 实时内核;目标是做一个面向 Web 的数控系统仿真产品:浏览器中完成机床配置加载、G-code 执行仿真、五轴/RTCP 运动展示、DRO/状态面板、刀路预览、会话持久化和可验证的 LinuxCNC 源码参考链路。
|
||||
|
||||
界面设计也参考 LinuxCNC 使用 Python 编写的图形界面和机床仿真程序,包括 AXIS、vismach、PyVCP、QtVCP/QtDragon、gmoccapy 以及 5 轴 vismach 示例。Web 项目不直接运行这些 Python GUI,而是参考其操作布局、HAL 绑定、机床模型层级、DRO/MDI/预览结构和五轴可视化表达方式。
|
||||
|
||||
当前先交付这些文档:
|
||||
|
||||
- [实现方案](docs/implementation-plan.md)
|
||||
- [技术路线](docs/technical-roadmap.md)
|
||||
- [程序具体实施文档](docs/program-implementation-guide.md)
|
||||
- [编写过程接续文档](docs/development-continuation.md)
|
||||
- [实现追溯文档](docs/traceability-matrix.md)
|
||||
- [LinuxCNC Python 图形界面参考](docs/linuxcnc-python-gui-reference.md)
|
||||
- [LinuxCNC 原始界面参考图册](docs/linuxcnc-gui-reference-gallery.md)
|
||||
|
||||
核心原则:
|
||||
|
||||
```text
|
||||
LinuxCNC 源码和配置案例作为 CNC 语义与五轴运动学参考源;
|
||||
Web 侧负责 UI、文件会话、可视化、运行编排和仿真状态展示;
|
||||
前端界面采用原生 HTML/CSS + TypeScript/JavaScript ES modules,不引入 React/Vue 等 UI 框架;
|
||||
RTCP/五轴运动学必须通过 LinuxCNC source-derived 的 C/WASM 边界或等价验证链路落地;
|
||||
不得用浏览器脚本随意重写 G-code、刀补、参数、重映射、规划器或运动学语义。
|
||||
```
|
||||
13
web-rtcp-5axis-sim-plan/app/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Web RTCP 5 Axis Simulation</title>
|
||||
<link rel="stylesheet" href="./src/styles/gmoccapy.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main id="app" data-app="gmoccapy-5axis-shell"></main>
|
||||
<script type="module" src="./src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
14
web-rtcp-5axis-sim-plan/app/package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "web-rtcp-5axis-sim",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "node scripts/build-static.mjs",
|
||||
"dev": "python3 -m http.server 4173",
|
||||
"smoke": "bash ../tests/browser/verify_gmoccapy_shell_browser.sh",
|
||||
"smoke:node": "node ../tests/node/verify_linuxcnc_kinematics_runtime.mjs && node ../tests/node/verify_rtcp_store.mjs && node ../tests/node/verify_profile_boundary.mjs"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {}
|
||||
}
|
||||
25
web-rtcp-5axis-sim-plan/app/scripts/build-static.mjs
Normal file
@@ -0,0 +1,25 @@
|
||||
import { cp, mkdir, rm, readFile } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const appRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||
const distDir = join(appRoot, "dist");
|
||||
|
||||
await rm(distDir, { recursive: true, force: true });
|
||||
await mkdir(distDir, { recursive: true });
|
||||
await cp(join(appRoot, "index.html"), join(distDir, "index.html"));
|
||||
await cp(join(appRoot, "src"), join(distDir, "src"), { recursive: true });
|
||||
|
||||
const packageJson = JSON.parse(await readFile(join(appRoot, "package.json"), "utf8"));
|
||||
const forbiddenDependencies = ["react", "vue", "@angular/core", "svelte"];
|
||||
const dependencyNames = [
|
||||
...Object.keys(packageJson.dependencies || {}),
|
||||
...Object.keys(packageJson.devDependencies || {}),
|
||||
];
|
||||
const forbidden = dependencyNames.filter((name) => forbiddenDependencies.includes(name));
|
||||
|
||||
if (forbidden.length > 0) {
|
||||
throw new Error(`forbidden frontend framework dependency detected: ${forbidden.join(", ")}`);
|
||||
}
|
||||
|
||||
console.log("gmoccapy_static_build=ok");
|
||||
19
web-rtcp-5axis-sim-plan/app/src/main.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { createSimulationStore } from "./state/store.js";
|
||||
import { mountGmoccapyShell } from "./ui/gmoccapy-shell.js";
|
||||
|
||||
const app = document.querySelector("#app");
|
||||
|
||||
if (!app) {
|
||||
throw new Error("Missing #app mount point");
|
||||
}
|
||||
|
||||
const store = createSimulationStore();
|
||||
const shell = mountGmoccapyShell(app, store);
|
||||
|
||||
window.webRtcp5AxisSimulation = {
|
||||
getState: store.getState,
|
||||
dispatch: store.dispatch,
|
||||
getRegions: shell.getRegions,
|
||||
};
|
||||
|
||||
store.dispatch({ type: "BOOT_READY" });
|
||||
110
web-rtcp-5axis-sim-plan/app/src/panel-schema/xyzac-trt-pyvcp.js
Normal file
@@ -0,0 +1,110 @@
|
||||
export const xyzacTrtPyvcpPanelSchema = {
|
||||
id: "xyzac-trt-switchkins-pyvcp",
|
||||
profileId: "xyzac-trt",
|
||||
title: "SWITCHKINS",
|
||||
sourceXmlPath: "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.xml",
|
||||
postguiHalPath: "configs/sim/axis/vismach/5axis/table-rotary-tilting/switchkins_postgui.hal",
|
||||
boundary: "pyvcp_hal_schema_reference_only",
|
||||
promotionAllowed: false,
|
||||
groups: [
|
||||
{
|
||||
id: "switchkins-mode",
|
||||
type: "vbox",
|
||||
label: "SWITCHKINS",
|
||||
controls: [
|
||||
{
|
||||
id: "kinstype-legends",
|
||||
type: "multilabel",
|
||||
legends: ["0:IDENTITY", "1: XYZAC ", "2: USERK "],
|
||||
halNets: [
|
||||
{
|
||||
signal: "kinstype.is-0",
|
||||
source: "kinstype.is-0",
|
||||
target: "pyvcp.multilabel.0.legend0",
|
||||
},
|
||||
{
|
||||
signal: "kinstype.is-1",
|
||||
source: "kinstype.is-1",
|
||||
target: "pyvcp.multilabel.0.legend1",
|
||||
},
|
||||
{
|
||||
signal: "kinstype.is-2",
|
||||
source: "kinstype.is-2",
|
||||
target: "pyvcp.multilabel.0.legend2",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "type0-button",
|
||||
type: "button",
|
||||
text: "IDENTITY",
|
||||
halpin: "pyvcp.type0-button",
|
||||
halNet: "type0-button",
|
||||
mdiCommandIndex: 0,
|
||||
mdiCommand: "M429",
|
||||
webAction: { type: "SET_KINS_TYPE", kinsType: "identity" },
|
||||
},
|
||||
{
|
||||
id: "type1-button",
|
||||
type: "button",
|
||||
text: "TCP:XYZAC",
|
||||
halpin: "pyvcp.type1-button",
|
||||
halNet: "type1-button",
|
||||
mdiCommandIndex: 1,
|
||||
mdiCommand: "M428",
|
||||
webAction: { type: "SET_KINS_TYPE", kinsType: "tcp-xyzac" },
|
||||
},
|
||||
{
|
||||
id: "type2-button",
|
||||
type: "button",
|
||||
text: "userk",
|
||||
halpin: "pyvcp.type2-button",
|
||||
halNet: "type2-button",
|
||||
mdiCommandIndex: 2,
|
||||
mdiCommand: "M430",
|
||||
webAction: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "vismach-actions",
|
||||
type: "vbox",
|
||||
controls: [
|
||||
{
|
||||
id: "vismach-clear",
|
||||
type: "button",
|
||||
text: "vismach-clear",
|
||||
halpin: "pyvcp.vismach-clear",
|
||||
halNet: "vismach-clear",
|
||||
target: "vismach.plotclear",
|
||||
webAction: { type: "CLEAR_PREVIEW" },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export function createPyvcpHalBindingSummary(schema = xyzacTrtPyvcpPanelSchema) {
|
||||
const controls = schema.groups.flatMap((group) => group.controls);
|
||||
const buttons = controls.filter((control) => control.type === "button");
|
||||
const halNets = [
|
||||
...controls.flatMap((control) => control.halNets || []),
|
||||
...buttons.map((button) => ({
|
||||
signal: button.halNet,
|
||||
source: button.halpin,
|
||||
target: button.target || `halui.mdi-command-${String(button.mdiCommandIndex).padStart(2, "0")}`,
|
||||
})),
|
||||
];
|
||||
|
||||
return {
|
||||
schemaId: schema.id,
|
||||
profileId: schema.profileId,
|
||||
boundary: schema.boundary,
|
||||
promotionAllowed: schema.promotionAllowed,
|
||||
controlCount: controls.length,
|
||||
buttonCount: buttons.length,
|
||||
halNetCount: halNets.length,
|
||||
mdiCommands: buttons.filter((button) => button.mdiCommand).map((button) => button.mdiCommand),
|
||||
halNets,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
export const linuxCncSourceReferenceMap = [
|
||||
{
|
||||
id: "xyzac-trt-ini",
|
||||
profileId: "xyzac-trt",
|
||||
kind: "ini",
|
||||
path: "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
|
||||
boundary: "linuxcnc_config_reference",
|
||||
usage: "machine profile, KINS, TRAJ coordinates, HAL and remap declarations",
|
||||
},
|
||||
{
|
||||
id: "xyzac-trt-pyvcp",
|
||||
profileId: "xyzac-trt",
|
||||
kind: "pyvcp_xml",
|
||||
path: "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.xml",
|
||||
boundary: "ui_hal_binding_reference",
|
||||
usage: "SWITCHKINS labels and operator buttons",
|
||||
},
|
||||
{
|
||||
id: "switchkins-postgui-hal",
|
||||
profileId: "xyzac-trt",
|
||||
kind: "postgui_hal",
|
||||
path: "configs/sim/axis/vismach/5axis/table-rotary-tilting/switchkins_postgui.hal",
|
||||
boundary: "hal_binding_reference",
|
||||
usage: "PyVCP button and legend nets to HALUI MDI commands and kinstype signals",
|
||||
},
|
||||
{
|
||||
id: "xyzac-trt-basic-sim-hal",
|
||||
profileId: "xyzac-trt",
|
||||
kind: "generated_hal",
|
||||
path: "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt_cmds.hal",
|
||||
boundary: "generated_hal_reference",
|
||||
usage: "source-visible basic_sim HAL loadrt/net proof for xyzac-trt-kins",
|
||||
},
|
||||
{
|
||||
id: "xyzac-trt-table",
|
||||
profileId: "xyzac-trt",
|
||||
kind: "tool_table",
|
||||
path: "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.tbl",
|
||||
boundary: "linuxcnc_config_reference",
|
||||
usage: "tool table source for future LinuxCNC-backed runtime session",
|
||||
},
|
||||
{
|
||||
id: "xyzac-trt-kins",
|
||||
profileId: "xyzac-trt",
|
||||
kind: "kinematics_source",
|
||||
path: "src/emc/kinematics/xyzac-trt-kins.c",
|
||||
boundary: "linuxcnc_source_required",
|
||||
usage: "final source-derived kinematics implementation source",
|
||||
},
|
||||
{
|
||||
id: "trtfuncs",
|
||||
profileId: "xyzac-trt",
|
||||
kind: "kinematics_source",
|
||||
path: "src/emc/kinematics/trtfuncs.c",
|
||||
boundary: "linuxcnc_source_required",
|
||||
usage: "shared table-rotary-tilting kinematics functions",
|
||||
},
|
||||
{
|
||||
id: "switchkins-source",
|
||||
profileId: "xyzac-trt",
|
||||
kind: "kinematics_source",
|
||||
path: "src/emc/kinematics/switchkins.c",
|
||||
boundary: "linuxcnc_source_required",
|
||||
usage: "switchable kinematics behavior backing M428/M429/M430",
|
||||
},
|
||||
{
|
||||
id: "xyzac-switchkins-demo",
|
||||
profileId: "xyzac-trt",
|
||||
kind: "gcode_demo",
|
||||
path: "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins.ngc",
|
||||
boundary: "linuxcnc_program_reference",
|
||||
usage: "representative XYZAC switchkins demonstration program",
|
||||
},
|
||||
];
|
||||
|
||||
export function getSourceReferencesForProfile(profileId) {
|
||||
return linuxCncSourceReferenceMap.filter((reference) => reference.profileId === profileId);
|
||||
}
|
||||
|
||||
export function createProfileSourceReferenceSummary(profileId) {
|
||||
const references = getSourceReferencesForProfile(profileId);
|
||||
const kinds = [...new Set(references.map((reference) => reference.kind))];
|
||||
const sourceRequiredCount = references.filter((reference) => {
|
||||
return reference.boundary === "linuxcnc_source_required";
|
||||
}).length;
|
||||
|
||||
return {
|
||||
profileId,
|
||||
referenceCount: references.length,
|
||||
kinds,
|
||||
sourceRequiredCount,
|
||||
configReferenceCount: references.length - sourceRequiredCount,
|
||||
promotionAllowed: false,
|
||||
linuxCncKinematicsReady: false,
|
||||
semanticBoundary: "profile_source_map_only_not_runtime_proof",
|
||||
references,
|
||||
};
|
||||
}
|
||||
183
web-rtcp-5axis-sim-plan/app/src/profiles/xyzac-trt.js
Normal file
@@ -0,0 +1,183 @@
|
||||
import { getSourceReferencesForProfile } from "./source-reference-map.js";
|
||||
import { xyzacTrtPyvcpPanelSchema } from "../panel-schema/xyzac-trt-pyvcp.js";
|
||||
|
||||
const sourceReferenceObjects = getSourceReferencesForProfile("xyzac-trt");
|
||||
|
||||
export const xyzacTrtProfile = {
|
||||
id: "xyzac-trt",
|
||||
title: "XYZAC table rotary tilting",
|
||||
iniPath: "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
|
||||
pyvcpXmlPath: "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.xml",
|
||||
postguiHalPath: "configs/sim/axis/vismach/5axis/table-rotary-tilting/switchkins_postgui.hal",
|
||||
generatedHalPath: "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt_cmds.hal",
|
||||
toolTablePath: "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.tbl",
|
||||
machineName: "sim-xyzac-trt-kins (switchkins)",
|
||||
display: {
|
||||
geometry: "XYZ-A",
|
||||
display: "axis",
|
||||
jogAxes: ["X", "Y", "Z", "C"],
|
||||
pyvcp: "./xyzac-trt.xml",
|
||||
openFile: "./demos/xyzac_switchkins.ngc",
|
||||
programPrefix: "../../nc_files",
|
||||
positionOffset: "RELATIVE",
|
||||
positionFeedback: "ACTUAL",
|
||||
maxFeedOverride: 2,
|
||||
},
|
||||
rs274ngc: {
|
||||
subroutinePath: "./remap_subs",
|
||||
halPinVars: true,
|
||||
parameterFile: "xyzac.var",
|
||||
},
|
||||
coordinates: ["X", "Y", "Z", "A", "C"],
|
||||
joints: ["joint.0", "joint.1", "joint.2", "joint.3", "joint.4"],
|
||||
kinematics: "xyzac-trt-kins",
|
||||
kinematicsParameters: {
|
||||
sparm: "identityfirst",
|
||||
defaultSwitchkinsType: 0,
|
||||
switchkinsTypes: [
|
||||
{ value: 0, label: "identity", mdiCommand: "M429", webKinsType: "identity" },
|
||||
{ value: 1, label: "XYZAC TCP", mdiCommand: "M428", webKinsType: "tcp-xyzac" },
|
||||
{ value: 2, label: "USERK", mdiCommand: "M430", webKinsType: "userk" },
|
||||
],
|
||||
},
|
||||
remaps: [
|
||||
{
|
||||
code: "M428",
|
||||
modalGroup: 10,
|
||||
ngc: "428remap",
|
||||
path: "configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/428remap.ngc",
|
||||
switchkinsType: 1,
|
||||
analogOutputIndex: 3,
|
||||
syncInputIndex: 0,
|
||||
requiresHalPinVars: true,
|
||||
},
|
||||
{
|
||||
code: "M429",
|
||||
modalGroup: 10,
|
||||
ngc: "429remap",
|
||||
path: "configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/429remap.ngc",
|
||||
switchkinsType: 0,
|
||||
analogOutputIndex: 3,
|
||||
syncInputIndex: 0,
|
||||
requiresHalPinVars: true,
|
||||
},
|
||||
{
|
||||
code: "M430",
|
||||
modalGroup: 10,
|
||||
ngc: "430remap",
|
||||
path: "configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/430remap.ngc",
|
||||
switchkinsType: 2,
|
||||
analogOutputIndex: 3,
|
||||
syncInputIndex: 0,
|
||||
requiresHalPinVars: true,
|
||||
},
|
||||
],
|
||||
hal: {
|
||||
halui: true,
|
||||
halFiles: ["LIB:basic_sim.tcl"],
|
||||
postguiHalFiles: ["switchkins_postgui.hal"],
|
||||
halcmd: {
|
||||
switchkinsSelectNet: {
|
||||
signal: "kinstype-select",
|
||||
source: "motion.analog-out-03",
|
||||
target: "motion.switchkins-type",
|
||||
},
|
||||
loadusr: ["xyzac-trt-gui"],
|
||||
feedbackNets: [
|
||||
{ signal: "table-x", source: "joint.0.pos-fb", target: "xyzac-trt-gui.table-x" },
|
||||
{ signal: "saddle-y", source: "joint.1.pos-fb", target: "xyzac-trt-gui.saddle-y" },
|
||||
{ signal: "spindle-z", source: "joint.2.pos-fb", target: "xyzac-trt-gui.spindle-z" },
|
||||
{ signal: "tilt-a", source: "joint.3.pos-fb", target: "xyzac-trt-gui.tilt-a" },
|
||||
{ signal: "rotate-c", source: "joint.4.pos-fb", target: "xyzac-trt-gui.rotate-c" },
|
||||
],
|
||||
offsetNets: [
|
||||
{ signal: "tool-offset", source: "motion.tooloffset.z", target: "xyzac-trt-kins.tool-offset" },
|
||||
{ signal: "tool-offset", source: "xyzac-trt-kins.tool-offset", target: "xyzac-trt-gui.tool-offset" },
|
||||
{ signal: "y-offset", source: "xyzac-trt-kins.y-offset", target: "xyzac-trt-gui.y-offset" },
|
||||
{ signal: "z-offset", source: "xyzac-trt-kins.z-offset", target: "xyzac-trt-gui.z-offset" },
|
||||
],
|
||||
initialSets: [
|
||||
{ pin: "y-offset", value: 20 },
|
||||
{ pin: "z-offset", value: 10 },
|
||||
{ pin: "xyzac-trt-kins.x-rot-point", value: 0 },
|
||||
{ pin: "xyzac-trt-kins.y-rot-point", value: 0 },
|
||||
{ pin: "xyzac-trt-kins.z-rot-point", value: 0 },
|
||||
{ pin: "xyzac-trt-kins.conventional-directions", value: 0 },
|
||||
],
|
||||
},
|
||||
},
|
||||
halui: {
|
||||
mdiCommands: ["M429", "M428", "M430"],
|
||||
},
|
||||
traj: {
|
||||
coordinates: "XYZAC",
|
||||
linearUnits: "mm",
|
||||
angularUnits: "deg",
|
||||
defaultLinearVelocity: 20,
|
||||
maxLinearVelocity: 35,
|
||||
defaultLinearAcceleration: 300,
|
||||
maxLinearAcceleration: 400,
|
||||
},
|
||||
axisLimits: {
|
||||
X: { min: -200, max: 200, maxVelocity: 20, maxAcceleration: 300 },
|
||||
Y: { min: -100, max: 100, maxVelocity: 20, maxAcceleration: 300 },
|
||||
Z: { min: -120, max: 120, maxVelocity: 20, maxAcceleration: 300 },
|
||||
A: { min: -100, max: 50, maxVelocity: 30, maxAcceleration: 300 },
|
||||
C: { min: -36000, max: 36000, maxVelocity: 30, maxAcceleration: 300 },
|
||||
},
|
||||
jointConfig: [
|
||||
{ id: 0, axis: "X", type: "LINEAR", home: 0, min: -200, max: 200, maxVelocity: 20, maxAcceleration: 300 },
|
||||
{ id: 1, axis: "Y", type: "LINEAR", home: 0, min: -100, max: 100, maxVelocity: 20, maxAcceleration: 300 },
|
||||
{ id: 2, axis: "Z", type: "LINEAR", home: 0, min: -120, max: 120, maxVelocity: 20, maxAcceleration: 300 },
|
||||
{ id: 3, axis: "A", type: "ANGULAR", home: 0, min: -100, max: 50, maxVelocity: 30, maxAcceleration: 300 },
|
||||
{ id: 4, axis: "C", type: "ANGULAR", home: 0, min: -36000, max: 36000, maxVelocity: 30, maxAcceleration: 300 },
|
||||
],
|
||||
halPins: [
|
||||
"motion.switchkins-type",
|
||||
"motion.analog-out-03",
|
||||
"motion.tooloffset.z",
|
||||
"xyzac-trt-kins.tool-offset",
|
||||
"xyzac-trt-kins.y-offset",
|
||||
"xyzac-trt-kins.z-offset",
|
||||
"xyzac-trt-kins.x-rot-point",
|
||||
"xyzac-trt-kins.y-rot-point",
|
||||
"xyzac-trt-kins.z-rot-point",
|
||||
"xyzac-trt-kins.conventional-directions",
|
||||
"halui.mdi-command-00",
|
||||
"halui.mdi-command-01",
|
||||
"halui.mdi-command-02",
|
||||
],
|
||||
offsets: {
|
||||
y: 20,
|
||||
z: 10,
|
||||
xRotPoint: 0,
|
||||
yRotPoint: 0,
|
||||
zRotPoint: 0,
|
||||
conventionalDirections: 0,
|
||||
},
|
||||
sourceReferences: sourceReferenceObjects.map((reference) => reference.path),
|
||||
sourceReferenceObjects,
|
||||
panelSchema: xyzacTrtPyvcpPanelSchema,
|
||||
toolTable: {
|
||||
toolCount: 10,
|
||||
tools: [
|
||||
{ tool: 1, pocket: 1, zOffset: 0, diameter: 1, comment: "end mill" },
|
||||
{ tool: 2, pocket: 2, zOffset: 15, diameter: 8, comment: "end mill" },
|
||||
{ tool: 3, pocket: 3, zOffset: 0, diameter: 4.2, comment: "#7 tap drill" },
|
||||
{ tool: 4, pocket: 4, zOffset: 0, diameter: 10, comment: null },
|
||||
{ tool: 5, pocket: 5, zOffset: 30, diameter: 10, comment: null },
|
||||
{ tool: 6, pocket: 6, zOffset: 30, diameter: 10, comment: null },
|
||||
{ tool: 7, pocket: 7, zOffset: 30, diameter: 10, comment: null },
|
||||
{ tool: 8, pocket: 8, zOffset: 30, diameter: 10, comment: null },
|
||||
{ tool: 9, pocket: 9, zOffset: 30, diameter: 10, comment: null },
|
||||
{ tool: 10, pocket: 10, zOffset: 0, diameter: 0.5, comment: null },
|
||||
],
|
||||
},
|
||||
samplePrograms: [
|
||||
"configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins.ngc",
|
||||
"linuxcnc/nc_files/3D_Chips.ngc",
|
||||
],
|
||||
semanticBoundary: "profile_source_map_only_not_runtime_proof",
|
||||
linuxCncKinematicsReady: false,
|
||||
promotionAllowed: false,
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
import { xyzacTrtProfile } from "../profiles/xyzac-trt.js";
|
||||
import { createPyvcpHalBindingSummary, xyzacTrtPyvcpPanelSchema } from "../panel-schema/xyzac-trt-pyvcp.js";
|
||||
import { createProfileSourceReferenceSummary } from "../profiles/source-reference-map.js";
|
||||
|
||||
export function createLinuxCncBoundaryAdapter({
|
||||
profile = xyzacTrtProfile,
|
||||
panelSchema = xyzacTrtPyvcpPanelSchema,
|
||||
runtime = null,
|
||||
} = {}) {
|
||||
const sourceSummary = createProfileSourceReferenceSummary(profile.id);
|
||||
const panelSummary = createPyvcpHalBindingSummary(panelSchema);
|
||||
const kinematicsRuntimeReady = Boolean(runtime?.kinematicsWasm?.loaded);
|
||||
const interpreterRuntimeReady = Boolean(runtime?.interpreterWasm?.loaded);
|
||||
const runtimeReady = kinematicsRuntimeReady && interpreterRuntimeReady;
|
||||
const linuxCncKinematicsReady = kinematicsRuntimeReady
|
||||
&& runtime.kinematicsWasm.sourceMode === "source-derived-kinematics-wasm";
|
||||
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-linuxcnc-boundary-adapter",
|
||||
profileId: profile.id,
|
||||
profile,
|
||||
sourceSummary,
|
||||
panelSummary,
|
||||
runtimeReady,
|
||||
kinematicsRuntimeReady,
|
||||
interpreterRuntimeReady,
|
||||
profileSummary: createProfileSummary(profile),
|
||||
linuxCncKinematicsReady,
|
||||
promotionAllowed: linuxCncKinematicsReady,
|
||||
fullLinuxCncProgramExecutionReady: false,
|
||||
semanticBoundary: linuxCncKinematicsReady
|
||||
? interpreterRuntimeReady
|
||||
? "linuxcnc_runtime_supplied_but_interpreter_or_remap_not_promoted"
|
||||
: "linuxcnc_kinematics_wasm_runtime_connected"
|
||||
: "adapter_entrypoint_only_runtime_not_connected",
|
||||
adapterPoints: {
|
||||
kinematicsWasm: runtime?.kinematicsWasm || null,
|
||||
interpreterWasm: runtime?.interpreterWasm || null,
|
||||
halPins: profile.halPins,
|
||||
pyvcpSchemaId: panelSchema.id,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createProfileSummary(profile) {
|
||||
return {
|
||||
machineName: profile.machineName,
|
||||
coordinates: profile.traj.coordinates,
|
||||
jointCount: profile.jointConfig.length,
|
||||
axisCount: Object.keys(profile.axisLimits).length,
|
||||
mdiCommandCount: profile.halui.mdiCommands.length,
|
||||
remapCount: profile.remaps.length,
|
||||
toolCount: profile.toolTable.toolCount,
|
||||
offsetNetCount: profile.hal.halcmd.offsetNets.length,
|
||||
feedbackNetCount: profile.hal.halcmd.feedbackNets.length,
|
||||
halFileCount: profile.hal.halFiles.length + profile.hal.postguiHalFiles.length,
|
||||
sourceKinds: profile.sourceReferenceObjects.map((reference) => reference.kind),
|
||||
};
|
||||
}
|
||||
|
||||
export function createLinuxCncBoundaryReadiness(adapter = createLinuxCncBoundaryAdapter()) {
|
||||
const missing = [];
|
||||
if (!adapter.kinematicsRuntimeReady) missing.push("kinematics runtime");
|
||||
if (!adapter.interpreterRuntimeReady) missing.push("interpreter/remap runtime");
|
||||
if (!adapter.sourceSummary.referenceCount) missing.push("source reference map");
|
||||
if (!adapter.panelSummary.controlCount) missing.push("PyVCP/HAL panel schema");
|
||||
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-linuxcnc-boundary-readiness",
|
||||
profileId: adapter.profileId,
|
||||
ready: adapter.linuxCncKinematicsReady
|
||||
&& !missing.includes("kinematics runtime")
|
||||
&& !missing.includes("source reference map")
|
||||
&& !missing.includes("PyVCP/HAL panel schema"),
|
||||
missing,
|
||||
linuxCncKinematicsReady: adapter.linuxCncKinematicsReady,
|
||||
promotionAllowed: adapter.promotionAllowed,
|
||||
fullLinuxCncProgramExecutionReady: adapter.fullLinuxCncProgramExecutionReady,
|
||||
semanticBoundary: adapter.semanticBoundary,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import {
|
||||
createLinuxCncKinematicsSdk,
|
||||
linuxCncKinematicsWasmFile,
|
||||
supportedLinuxCncKinematicsModules,
|
||||
} from "../../../../wasm-port/runtime/sdk/src/index.js";
|
||||
|
||||
const DEFAULT_MODULE_ID = "xyzac-trt";
|
||||
const DEFAULT_JOINT_COUNT = 5;
|
||||
const SOURCE_MODE = "source-derived-kinematics-wasm";
|
||||
const SEMANTIC_BOUNDARY = "linuxcnc_kinematics_wasm_c_abi";
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const defaultWasmRoot = resolve(__dirname, "../../../../wasm-port/build/wasm/kinematics");
|
||||
|
||||
export async function createLinuxCncKinematicsRuntime({
|
||||
moduleId = DEFAULT_MODULE_ID,
|
||||
moduleOptions = null,
|
||||
switchkinsType = 0,
|
||||
jointCount = DEFAULT_JOINT_COUNT,
|
||||
wasmRoot = defaultWasmRoot,
|
||||
} = {}) {
|
||||
const wasmFile = linuxCncKinematicsWasmFile(moduleId);
|
||||
if (!wasmFile) {
|
||||
throw new Error(`unsupported LinuxCNC kinematics module: ${moduleId}`);
|
||||
}
|
||||
|
||||
const resolvedModuleOptions = moduleOptions || {
|
||||
wasmBinary: readFileSync(resolve(wasmRoot, wasmFile)),
|
||||
print() {},
|
||||
printErr() {},
|
||||
};
|
||||
const sdk = await createLinuxCncKinematicsSdk({ moduleId, moduleOptions: resolvedModuleOptions });
|
||||
const switchRc = typeof sdk.switchKinematics === "function"
|
||||
? sdk.switchKinematics(switchkinsType)
|
||||
: 0;
|
||||
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-linuxcnc-kinematics-runtime",
|
||||
moduleId,
|
||||
wasmFile,
|
||||
supportedModules: supportedLinuxCncKinematicsModules(),
|
||||
loaded: true,
|
||||
sourceMode: SOURCE_MODE,
|
||||
semanticBoundary: SEMANTIC_BOUNDARY,
|
||||
switchkinsType,
|
||||
switchRc,
|
||||
jointCount,
|
||||
sdk,
|
||||
|
||||
readiness() {
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-linuxcnc-kinematics-runtime-readiness",
|
||||
moduleId,
|
||||
wasmFile,
|
||||
supportedModules: supportedLinuxCncKinematicsModules(),
|
||||
loaded: true,
|
||||
sourceMode: SOURCE_MODE,
|
||||
semanticBoundary: SEMANTIC_BOUNDARY,
|
||||
switchkinsType,
|
||||
switchRc,
|
||||
};
|
||||
},
|
||||
|
||||
forward(joints, options = {}) {
|
||||
return sdk.forward(joints, options);
|
||||
},
|
||||
|
||||
inverse(pose, count = jointCount, options = {}) {
|
||||
return sdk.inverse(pose, count, options);
|
||||
},
|
||||
|
||||
frameForJoints(joints, options = {}) {
|
||||
const jointValues = Array.from(joints, Number);
|
||||
const forward = sdk.forward(jointValues, options.forwardOptions || {});
|
||||
const inverse = sdk.inverse(
|
||||
forward.pose,
|
||||
options.jointCount || jointCount,
|
||||
{ seedJoints: jointValues, ...(options.inverseOptions || {}) },
|
||||
);
|
||||
return {
|
||||
moduleId,
|
||||
switchkinsType,
|
||||
forward,
|
||||
inverse,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function createLinuxCncKinematicsRuntimeDescriptor(runtime) {
|
||||
if (!runtime?.loaded) return null;
|
||||
return {
|
||||
apiName: runtime.apiName,
|
||||
moduleId: runtime.moduleId,
|
||||
wasmFile: runtime.wasmFile,
|
||||
supportedModules: runtime.supportedModules,
|
||||
loaded: runtime.loaded,
|
||||
sourceMode: runtime.sourceMode,
|
||||
semanticBoundary: runtime.semanticBoundary,
|
||||
switchkinsType: runtime.switchkinsType,
|
||||
switchRc: runtime.switchRc,
|
||||
};
|
||||
}
|
||||
196
web-rtcp-5axis-sim-plan/app/src/runtime/rtcp-frame.js
Normal file
@@ -0,0 +1,196 @@
|
||||
import { xyzacTrtProfile } from "../profiles/xyzac-trt.js";
|
||||
|
||||
const DEG_TO_RAD = Math.PI / 180;
|
||||
|
||||
export function buildRtcpFrame({
|
||||
axisPose,
|
||||
activeLine,
|
||||
kinsType = "identity",
|
||||
rtcpEnabled = false,
|
||||
sourceMode = "fixture-ui-only",
|
||||
profile = xyzacTrtProfile,
|
||||
linuxCncKinematicsResult = null,
|
||||
}) {
|
||||
if (linuxCncKinematicsResult) {
|
||||
return buildLinuxCncKinematicsFrame({
|
||||
axisPose,
|
||||
activeLine,
|
||||
kinsType,
|
||||
rtcpEnabled,
|
||||
sourceMode,
|
||||
profile,
|
||||
linuxCncKinematicsResult,
|
||||
});
|
||||
}
|
||||
|
||||
const pose = normalizeAxisPose(axisPose);
|
||||
const toolLength = 84.019;
|
||||
const toolAxisVector = computeToolAxisVector(pose.a, pose.c);
|
||||
const compensation = rtcpEnabled
|
||||
? {
|
||||
x: -toolAxisVector.x * toolLength,
|
||||
y: -toolAxisVector.y * toolLength,
|
||||
z: -toolAxisVector.z * toolLength,
|
||||
}
|
||||
: { x: 0, y: 0, z: 0 };
|
||||
const tcpPose = {
|
||||
x: pose.x + compensation.x,
|
||||
y: pose.y + compensation.y,
|
||||
z: pose.z + compensation.z,
|
||||
a: pose.a,
|
||||
c: pose.c,
|
||||
};
|
||||
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-motion-frame",
|
||||
profileId: profile.id,
|
||||
sourceMode,
|
||||
semanticBoundary:
|
||||
sourceMode === "fixture-ui-only"
|
||||
? "fixture_frame_ui_plumbing_not_linuxcnc_kinematics_proof"
|
||||
: "linuxcnc_or_source_derived_runtime_required",
|
||||
activeLine,
|
||||
kinsType,
|
||||
rtcpEnabled,
|
||||
rtcpState: rtcpEnabled ? "on" : "off",
|
||||
axisPose: pose,
|
||||
jointPose: buildJointPose(pose),
|
||||
tcpPose,
|
||||
toolAxisVector,
|
||||
compensation,
|
||||
toolLength,
|
||||
readiness: {
|
||||
frameReady: true,
|
||||
uiReady: true,
|
||||
profileReady: true,
|
||||
linuxCncKinematicsReady: false,
|
||||
promotionAllowed: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildLinuxCncKinematicsFrame({
|
||||
axisPose,
|
||||
activeLine,
|
||||
kinsType,
|
||||
rtcpEnabled,
|
||||
profile,
|
||||
linuxCncKinematicsResult,
|
||||
}) {
|
||||
const forward = linuxCncKinematicsResult.forward || {};
|
||||
const inverse = linuxCncKinematicsResult.inverse || {};
|
||||
const wasmPose = normalizeLinuxCncPose(forward.pose);
|
||||
const pose = normalizeAxisPose({
|
||||
...axisPose,
|
||||
...wasmPose,
|
||||
});
|
||||
const jointValues = Array.isArray(inverse.joints) ? inverse.joints : [];
|
||||
const toolAxisVector = computeToolAxisVector(pose.a, pose.c);
|
||||
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-motion-frame",
|
||||
profileId: profile.id,
|
||||
sourceMode: "source-derived-kinematics-wasm",
|
||||
semanticBoundary: "linuxcnc_kinematics_wasm_c_abi",
|
||||
activeLine,
|
||||
kinsType,
|
||||
rtcpEnabled,
|
||||
rtcpState: rtcpEnabled ? "on" : "off",
|
||||
axisPose: pose,
|
||||
jointPose: buildJointPoseFromLinuxCncJoints(jointValues, pose),
|
||||
tcpPose: {
|
||||
x: pose.x,
|
||||
y: pose.y,
|
||||
z: pose.z,
|
||||
a: pose.a,
|
||||
c: pose.c,
|
||||
},
|
||||
toolAxisVector,
|
||||
compensation: { x: 0, y: 0, z: 0 },
|
||||
toolLength: 84.019,
|
||||
kinematicsModuleId: linuxCncKinematicsResult.moduleId,
|
||||
kinematicsSwitchkinsType: linuxCncKinematicsResult.switchkinsType,
|
||||
kinematicsForwardRc: forward.rc,
|
||||
kinematicsInverseRc: inverse.rc,
|
||||
kinematicsFlags: {
|
||||
fflags: forward.fflags,
|
||||
iflags: forward.iflags,
|
||||
inverseFflags: inverse.fflags,
|
||||
inverseIflags: inverse.iflags,
|
||||
},
|
||||
readiness: {
|
||||
frameReady: true,
|
||||
uiReady: true,
|
||||
profileReady: true,
|
||||
linuxCncKinematicsReady: forward.rc === 0 && inverse.rc === 0,
|
||||
promotionAllowed: forward.rc === 0 && inverse.rc === 0,
|
||||
fullLinuxCncProgramExecutionReady: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeAxisPose(axisPose) {
|
||||
return {
|
||||
x: Number(axisPose?.x ?? 0),
|
||||
y: Number(axisPose?.y ?? 0),
|
||||
z: Number(axisPose?.z ?? 0),
|
||||
a: Number(axisPose?.a ?? 0),
|
||||
b: Number(axisPose?.b ?? 0),
|
||||
c: Number(axisPose?.c ?? 0),
|
||||
};
|
||||
}
|
||||
|
||||
function buildJointPose(pose) {
|
||||
return [
|
||||
{ joint: 0, axis: "X", value: pose.x },
|
||||
{ joint: 1, axis: "Y", value: pose.y },
|
||||
{ joint: 2, axis: "Z", value: pose.z },
|
||||
{ joint: 3, axis: "A", value: pose.a },
|
||||
{ joint: 4, axis: "C", value: pose.c },
|
||||
];
|
||||
}
|
||||
|
||||
function buildJointPoseFromLinuxCncJoints(joints, fallbackPose) {
|
||||
const axes = ["X", "Y", "Z", "A", "C"];
|
||||
const fallbackValues = [fallbackPose.x, fallbackPose.y, fallbackPose.z, fallbackPose.a, fallbackPose.c];
|
||||
return axes.map((axis, joint) => ({
|
||||
joint,
|
||||
axis,
|
||||
value: Number(joints[joint] ?? fallbackValues[joint] ?? 0),
|
||||
}));
|
||||
}
|
||||
|
||||
function normalizeLinuxCncPose(pose = {}) {
|
||||
return {
|
||||
x: Number(pose.x ?? pose.tran?.x ?? 0),
|
||||
y: Number(pose.y ?? pose.tran?.y ?? 0),
|
||||
z: Number(pose.z ?? pose.tran?.z ?? 0),
|
||||
a: Number(pose.a ?? 0),
|
||||
b: Number(pose.b ?? 0),
|
||||
c: Number(pose.c ?? 0),
|
||||
};
|
||||
}
|
||||
|
||||
function computeToolAxisVector(aDegrees, cDegrees) {
|
||||
const a = aDegrees * DEG_TO_RAD;
|
||||
const c = cDegrees * DEG_TO_RAD;
|
||||
const sinA = Math.sin(a);
|
||||
const cosA = Math.cos(a);
|
||||
const sinC = Math.sin(c);
|
||||
const cosC = Math.cos(c);
|
||||
|
||||
return normalizeVector({
|
||||
x: sinA * sinC,
|
||||
y: -sinA * cosC,
|
||||
z: cosA,
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeVector(vector) {
|
||||
const length = Math.hypot(vector.x, vector.y, vector.z) || 1;
|
||||
return {
|
||||
x: vector.x / length,
|
||||
y: vector.y / length,
|
||||
z: vector.z / length,
|
||||
};
|
||||
}
|
||||
616
web-rtcp-5axis-sim-plan/app/src/state/store.js
Normal file
@@ -0,0 +1,616 @@
|
||||
import { buildRtcpFrame } from "../runtime/rtcp-frame.js";
|
||||
import { createLinuxCncBoundaryAdapter, createLinuxCncBoundaryReadiness } from "../runtime/linuxcnc-boundary-adapter.js";
|
||||
import { xyzacTrtProfile } from "../profiles/xyzac-trt.js";
|
||||
|
||||
const initialLinuxCncBoundaryAdapter = createLinuxCncBoundaryAdapter({
|
||||
profile: xyzacTrtProfile,
|
||||
});
|
||||
const initialLinuxCncBoundaryReadiness = createLinuxCncBoundaryReadiness(initialLinuxCncBoundaryAdapter);
|
||||
|
||||
const initialAxisPose = {
|
||||
x: 43.0,
|
||||
y: -32.15,
|
||||
z: -11.306,
|
||||
a: 0.0,
|
||||
b: 0.0,
|
||||
c: 0.0,
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
machineProfile: "xyzac-trt",
|
||||
profile: xyzacTrtProfile,
|
||||
sessionName: "gmoccapy-web-session",
|
||||
sourceMode: "fixture-ui-only",
|
||||
frameSourceMode: "fixture-ui-only",
|
||||
machine: {
|
||||
powerOn: false,
|
||||
estopActive: false,
|
||||
mode: "manual",
|
||||
jogAxis: "x",
|
||||
jogIncrement: 1,
|
||||
mdiCommand: "G0 X0 Y0 Z0",
|
||||
resetCount: 0,
|
||||
},
|
||||
runState: "idle",
|
||||
activeProgram: "../../../linuxcnc/nc_files/3D_Chips.ngc",
|
||||
programSource: "fixture",
|
||||
programStartLine: 496,
|
||||
activeLine: 501,
|
||||
lineCount: 4711,
|
||||
fileSizeBytes: 200509,
|
||||
kinsType: "identity",
|
||||
rtcpState: "off",
|
||||
axisPose: initialAxisPose,
|
||||
jointPose: [],
|
||||
tcpPose: {
|
||||
x: 43.0,
|
||||
y: -32.15,
|
||||
z: -11.306,
|
||||
a: 0.0,
|
||||
c: 0.0,
|
||||
},
|
||||
toolAxisVector: {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
z: 1.0,
|
||||
},
|
||||
rtcpFrame: null,
|
||||
kinematicsRuntime: null,
|
||||
kinematicsRuntimeReadiness: null,
|
||||
lastKinematicsResult: null,
|
||||
linuxCncBoundaryAdapter: initialLinuxCncBoundaryAdapter,
|
||||
linuxCncBoundaryReadiness: initialLinuxCncBoundaryReadiness,
|
||||
dro: {
|
||||
x: 43.0,
|
||||
y: -32.15,
|
||||
z: -11.306,
|
||||
a: 0.0,
|
||||
b: 0.0,
|
||||
c: 0.0,
|
||||
tcpX: 43.0,
|
||||
tcpY: -32.15,
|
||||
tcpZ: -11.306,
|
||||
dtgX: 0.0,
|
||||
dtgY: 0.01,
|
||||
dtgZ: 2.25,
|
||||
},
|
||||
feed: {
|
||||
currentVelocity: 2621,
|
||||
rapidOverride: 100,
|
||||
feedRate: 4500000,
|
||||
feedOverride: 100,
|
||||
},
|
||||
spindle: {
|
||||
rpm: 1600,
|
||||
override: 100,
|
||||
enabled: true,
|
||||
},
|
||||
coolant: {
|
||||
flood: true,
|
||||
mist: false,
|
||||
},
|
||||
preview: {
|
||||
pathPoints: 64,
|
||||
selectedView: "iso",
|
||||
fullscreen: false,
|
||||
},
|
||||
toolPreview: {
|
||||
toolNumber: 1,
|
||||
diameter: 6,
|
||||
length: 84.019,
|
||||
units: "mm",
|
||||
holder: "CAT40",
|
||||
},
|
||||
operatorMessage: "ready",
|
||||
};
|
||||
|
||||
const programLines = [
|
||||
"N4860 Y[#<yscale>*-39.009]",
|
||||
"N4870 Y[#<yscale>*-32.524]",
|
||||
"N4880 Y[#<yscale>*-32.384]",
|
||||
"N4890 Y[#<yscale>*-32.267]",
|
||||
"N4900 Y[#<yscale>*-32.235] Z[#<zscale>*-11.306]",
|
||||
"N4910 Y[#<yscale>*-32.118] Z[#<zscale>*-11.312]",
|
||||
"N4920 Y[#<yscale>*-32.103] Z[#<zscale>*-11.314]",
|
||||
"N4930 Y[#<yscale>*-32.071] Z[#<zscale>*-11.316]",
|
||||
"N4940 Y[#<yscale>*-31.972] Z[#<zscale>*-11.318]",
|
||||
"N4950 Y[#<yscale>*-31.759] Z[#<zscale>*-11.320]",
|
||||
"N4960 Y[#<yscale>*-31.509] Z[#<zscale>*-11.324]",
|
||||
];
|
||||
|
||||
export function createSimulationStore(seed = {}) {
|
||||
const seedAxisPose = seed.axisPose || initialAxisPose;
|
||||
const seedKinsType = seed.kinsType || initialState.kinsType;
|
||||
const seedRtcpState = seed.rtcpState || initialState.rtcpState;
|
||||
const seedFrame = buildRtcpFrame({
|
||||
axisPose: seedAxisPose,
|
||||
activeLine: seed.activeLine || initialState.activeLine,
|
||||
kinsType: seedKinsType,
|
||||
rtcpEnabled: seedRtcpState === "on" || seedKinsType === "tcp-xyzac",
|
||||
sourceMode: seed.sourceMode || seed.frameSourceMode || initialState.sourceMode,
|
||||
});
|
||||
let state = {
|
||||
...initialState,
|
||||
...seed,
|
||||
axisPose: seedFrame.axisPose,
|
||||
jointPose: seedFrame.jointPose,
|
||||
tcpPose: seedFrame.tcpPose,
|
||||
toolAxisVector: seedFrame.toolAxisVector,
|
||||
rtcpState: seedFrame.rtcpState,
|
||||
rtcpFrame: seedFrame,
|
||||
dro: buildDroFromFrame(seedFrame),
|
||||
programLines: seed.programLines || programLines,
|
||||
};
|
||||
const listeners = new Set();
|
||||
|
||||
const notify = () => {
|
||||
for (const listener of listeners) {
|
||||
listener(state);
|
||||
}
|
||||
};
|
||||
|
||||
const setState = (patch) => {
|
||||
const next = { ...state, ...patch };
|
||||
const frameState = buildFrameForState(next, patch);
|
||||
const frame = patch.rtcpFrame || frameState.frame;
|
||||
state = {
|
||||
...next,
|
||||
sourceMode: frame.sourceMode,
|
||||
frameSourceMode: frame.sourceMode,
|
||||
axisPose: frame.axisPose,
|
||||
jointPose: frame.jointPose,
|
||||
tcpPose: frame.tcpPose,
|
||||
toolAxisVector: frame.toolAxisVector,
|
||||
rtcpState: frame.rtcpState,
|
||||
rtcpFrame: frame,
|
||||
lastKinematicsResult: frameState.lastKinematicsResult,
|
||||
dro: buildDroFromFrame(frame),
|
||||
};
|
||||
notify();
|
||||
};
|
||||
|
||||
const dispatch = (action) => {
|
||||
switch (action.type) {
|
||||
case "BOOT_READY":
|
||||
setState({ bootReady: true });
|
||||
break;
|
||||
case "ATTACH_KINEMATICS_RUNTIME":
|
||||
{
|
||||
const runtime = action.runtime || null;
|
||||
const readiness = runtime?.readiness ? runtime.readiness() : null;
|
||||
const runtimeDescriptor = runtime?.loaded
|
||||
? {
|
||||
apiName: runtime.apiName,
|
||||
moduleId: runtime.moduleId,
|
||||
wasmFile: runtime.wasmFile,
|
||||
supportedModules: runtime.supportedModules,
|
||||
loaded: runtime.loaded,
|
||||
sourceMode: runtime.sourceMode,
|
||||
semanticBoundary: runtime.semanticBoundary,
|
||||
switchkinsType: runtime.switchkinsType,
|
||||
switchRc: runtime.switchRc,
|
||||
}
|
||||
: null;
|
||||
const adapter = createLinuxCncBoundaryAdapter({
|
||||
profile: state.profile,
|
||||
runtime: {
|
||||
kinematicsWasm: runtimeDescriptor,
|
||||
interpreterWasm: null,
|
||||
},
|
||||
});
|
||||
setState({
|
||||
kinematicsRuntime: runtime,
|
||||
kinematicsRuntimeReadiness: readiness,
|
||||
linuxCncBoundaryAdapter: adapter,
|
||||
linuxCncBoundaryReadiness: createLinuxCncBoundaryReadiness(adapter),
|
||||
sourceMode: runtime?.loaded ? "source-derived-kinematics-wasm" : "fixture-ui-only",
|
||||
frameSourceMode: runtime?.loaded ? "source-derived-kinematics-wasm" : "fixture-ui-only",
|
||||
operatorMessage: runtime?.loaded
|
||||
? `LinuxCNC kinematics ${runtime.moduleId} ready`
|
||||
: "LinuxCNC kinematics runtime missing",
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "SET_FRAME_SOURCE":
|
||||
setState({
|
||||
sourceMode: action.sourceMode,
|
||||
frameSourceMode: action.sourceMode,
|
||||
operatorMessage: `frame source ${action.sourceMode}`,
|
||||
});
|
||||
break;
|
||||
case "REFRESH_KINEMATICS_FRAME":
|
||||
setState({
|
||||
sourceMode: "source-derived-kinematics-wasm",
|
||||
frameSourceMode: "source-derived-kinematics-wasm",
|
||||
operatorMessage: "LinuxCNC kinematics frame refreshed",
|
||||
});
|
||||
break;
|
||||
case "TOGGLE_POWER":
|
||||
setState({
|
||||
machine: {
|
||||
...state.machine,
|
||||
powerOn: !state.machine.powerOn,
|
||||
},
|
||||
runState: state.machine.powerOn ? "idle" : "powered-off",
|
||||
operatorMessage: state.machine.powerOn ? "machine power off" : "machine power on",
|
||||
});
|
||||
break;
|
||||
case "ESTOP":
|
||||
setState({
|
||||
machine: {
|
||||
...state.machine,
|
||||
powerOn: false,
|
||||
estopActive: true,
|
||||
},
|
||||
runState: "estopped",
|
||||
feed: {
|
||||
...state.feed,
|
||||
currentVelocity: 0,
|
||||
},
|
||||
operatorMessage: "emergency stop active",
|
||||
});
|
||||
break;
|
||||
case "RESET":
|
||||
setState({
|
||||
machine: {
|
||||
...state.machine,
|
||||
estopActive: false,
|
||||
resetCount: state.machine.resetCount + 1,
|
||||
},
|
||||
runState: "idle",
|
||||
operatorMessage: "machine reset complete",
|
||||
});
|
||||
break;
|
||||
case "SET_MODE":
|
||||
setState({
|
||||
machine: {
|
||||
...state.machine,
|
||||
mode: action.mode,
|
||||
},
|
||||
runState: state.runState === "running" ? "paused" : state.runState,
|
||||
operatorMessage: `mode ${action.mode}`,
|
||||
});
|
||||
break;
|
||||
case "JOG":
|
||||
if (!canMoveMachine(state)) {
|
||||
setState({ operatorMessage: "jog blocked: power or estop state" });
|
||||
break;
|
||||
}
|
||||
{
|
||||
const axis = action.axis || state.machine.jogAxis;
|
||||
const direction = Number(action.direction || 1);
|
||||
const increment = Number(action.increment || state.machine.jogIncrement);
|
||||
setState({
|
||||
machine: {
|
||||
...state.machine,
|
||||
mode: "jog",
|
||||
jogAxis: axis,
|
||||
jogIncrement: increment,
|
||||
},
|
||||
axisPose: {
|
||||
...state.axisPose,
|
||||
[axis]: Number(state.axisPose[axis] || 0) + direction * increment,
|
||||
},
|
||||
runState: "jogging",
|
||||
operatorMessage: `jog ${axis.toUpperCase()} ${direction > 0 ? "+" : "-"}${increment}`,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "RUN_MDI":
|
||||
if (!canMoveMachine(state)) {
|
||||
setState({ operatorMessage: "MDI blocked: power or estop state" });
|
||||
break;
|
||||
}
|
||||
setState({
|
||||
machine: {
|
||||
...state.machine,
|
||||
mode: "mdi",
|
||||
mdiCommand: action.command || state.machine.mdiCommand,
|
||||
},
|
||||
runState: "mdi",
|
||||
operatorMessage: `MDI ${action.command || state.machine.mdiCommand}`,
|
||||
});
|
||||
break;
|
||||
case "LOAD_PROGRAM":
|
||||
{
|
||||
const loadedProgram = buildLoadedProgram(action);
|
||||
setState({
|
||||
...loadedProgram,
|
||||
machine: {
|
||||
...state.machine,
|
||||
mode: "auto",
|
||||
},
|
||||
axisPose: initialAxisPose,
|
||||
runState: "idle",
|
||||
preview: {
|
||||
...state.preview,
|
||||
pathPoints: Math.max(loadedProgram.programLines.length, 1),
|
||||
},
|
||||
operatorMessage: `loaded ${loadedProgram.activeProgram}`,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "RUN":
|
||||
if (!canMoveMachine(state)) {
|
||||
setState({ operatorMessage: "run blocked: power or estop state" });
|
||||
break;
|
||||
}
|
||||
{
|
||||
const nextLine = getNextProgramLine(state, 5);
|
||||
const nextAxisPose = buildFixtureAxisPoseForLine(state.axisPose, nextLine);
|
||||
setState({
|
||||
machine: {
|
||||
...state.machine,
|
||||
mode: "auto",
|
||||
},
|
||||
runState: nextLine >= getProgramEndLine(state) ? "complete" : "running",
|
||||
activeLine: nextLine,
|
||||
axisPose: nextAxisPose,
|
||||
operatorMessage: `executing line ${nextLine}`,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "STOP":
|
||||
setState({
|
||||
runState: "stopped",
|
||||
operatorMessage: "program stopped",
|
||||
});
|
||||
break;
|
||||
case "PAUSE":
|
||||
setState({ runState: "paused", operatorMessage: "program paused" });
|
||||
break;
|
||||
case "STEP":
|
||||
if (!canMoveMachine(state)) {
|
||||
setState({ operatorMessage: "step blocked: power or estop state" });
|
||||
break;
|
||||
}
|
||||
{
|
||||
const nextLine = getNextProgramLine(state, 1);
|
||||
const nextAxisPose = buildFixtureAxisPoseForLine(state.axisPose, nextLine);
|
||||
setState({
|
||||
machine: {
|
||||
...state.machine,
|
||||
mode: "auto",
|
||||
},
|
||||
runState: "stepping",
|
||||
activeLine: nextLine,
|
||||
axisPose: nextAxisPose,
|
||||
operatorMessage: `stepped to line ${nextLine}`,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "RUN_FRAME":
|
||||
if (!canMoveMachine(state)) {
|
||||
setState({ operatorMessage: "run frame blocked: power or estop state" });
|
||||
break;
|
||||
}
|
||||
{
|
||||
const nextLine = getNextProgramLine(state, 5);
|
||||
const nextAxisPose = buildFixtureAxisPoseForLine(state.axisPose, nextLine);
|
||||
setState({
|
||||
runState: "running",
|
||||
activeLine: nextLine,
|
||||
axisPose: nextAxisPose,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "SET_RTCP":
|
||||
setState({
|
||||
kinsType: action.enabled ? "tcp-xyzac" : "identity",
|
||||
rtcpState: action.enabled ? "on" : "off",
|
||||
});
|
||||
break;
|
||||
case "RESET_VIEW":
|
||||
setState({
|
||||
preview: { ...state.preview, selectedView: "iso" },
|
||||
operatorMessage: "preview fit to program",
|
||||
});
|
||||
break;
|
||||
case "CLEAR_PREVIEW":
|
||||
setState({
|
||||
preview: { ...state.preview, pathPoints: 0 },
|
||||
operatorMessage: "preview path cleared",
|
||||
});
|
||||
break;
|
||||
case "SET_VIEW":
|
||||
setState({
|
||||
preview: { ...state.preview, selectedView: action.view },
|
||||
operatorMessage: `preview view ${action.view}`,
|
||||
});
|
||||
break;
|
||||
case "TOGGLE_FULLSCREEN":
|
||||
setState({
|
||||
preview: { ...state.preview, fullscreen: !state.preview.fullscreen },
|
||||
operatorMessage: state.preview.fullscreen ? "fullscreen preview off" : "fullscreen preview on",
|
||||
});
|
||||
break;
|
||||
case "SET_KINS_TYPE":
|
||||
setState({
|
||||
kinsType: action.kinsType,
|
||||
rtcpState: action.kinsType === "tcp-xyzac" ? "on" : "off",
|
||||
operatorMessage: `kinematics ${action.kinsType}`,
|
||||
});
|
||||
break;
|
||||
case "ADJUST_OVERRIDE":
|
||||
setState({
|
||||
feed: {
|
||||
...state.feed,
|
||||
[`${action.target}Override`]: clampPercent(
|
||||
state.feed[`${action.target}Override`] + action.delta,
|
||||
0,
|
||||
200,
|
||||
),
|
||||
},
|
||||
operatorMessage: `${action.target} override adjusted`,
|
||||
});
|
||||
break;
|
||||
case "ADJUST_SPINDLE_OVERRIDE":
|
||||
setState({
|
||||
spindle: {
|
||||
...state.spindle,
|
||||
override: clampPercent(state.spindle.override + action.delta, 0, 150),
|
||||
},
|
||||
operatorMessage: "spindle override adjusted",
|
||||
});
|
||||
break;
|
||||
case "TOGGLE_COOLANT":
|
||||
setState({
|
||||
coolant: {
|
||||
...state.coolant,
|
||||
[action.kind]: !state.coolant[action.kind],
|
||||
},
|
||||
operatorMessage: `${action.kind} coolant toggled`,
|
||||
});
|
||||
break;
|
||||
case "HOME":
|
||||
if (!canMoveMachine(state)) {
|
||||
setState({ operatorMessage: "home blocked: power or estop state" });
|
||||
break;
|
||||
}
|
||||
setState({
|
||||
runState: "idle",
|
||||
axisPose: initialAxisPose,
|
||||
operatorMessage: "machine homed to fixture origin",
|
||||
});
|
||||
break;
|
||||
case "RELOAD_PROGRAM":
|
||||
setState({
|
||||
runState: "idle",
|
||||
activeLine: state.programStartLine === 496 ? 501 : state.programStartLine,
|
||||
axisPose: initialAxisPose,
|
||||
preview: { ...state.preview, pathPoints: Math.max(state.programLines.length, 1) },
|
||||
operatorMessage: "program reloaded",
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown action type: ${action.type}`);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
getState: () => state,
|
||||
subscribe(listener) {
|
||||
listeners.add(listener);
|
||||
listener(state);
|
||||
return () => listeners.delete(listener);
|
||||
},
|
||||
dispatch,
|
||||
};
|
||||
}
|
||||
|
||||
function buildFrameForState(state, patch = {}) {
|
||||
const requestedSourceMode = state.frameSourceMode || state.sourceMode;
|
||||
let linuxCncKinematicsResult = patch.lastKinematicsResult || null;
|
||||
let sourceMode = requestedSourceMode;
|
||||
let operatorMessage = state.operatorMessage;
|
||||
|
||||
if (requestedSourceMode === "source-derived-kinematics-wasm") {
|
||||
if (state.kinematicsRuntime?.loaded) {
|
||||
linuxCncKinematicsResult = state.kinematicsRuntime.frameForJoints(
|
||||
jointsFromAxisPose(state.axisPose),
|
||||
{ jointCount: state.kinematicsRuntime.jointCount || 5 },
|
||||
);
|
||||
} else {
|
||||
sourceMode = "fixture-ui-only";
|
||||
linuxCncKinematicsResult = null;
|
||||
operatorMessage = "LinuxCNC kinematics runtime missing; using fixture frame";
|
||||
}
|
||||
}
|
||||
|
||||
const frame = buildRtcpFrame({
|
||||
axisPose: state.axisPose,
|
||||
activeLine: state.activeLine,
|
||||
kinsType: state.kinsType,
|
||||
rtcpEnabled: state.rtcpState === "on" || state.kinsType === "tcp-xyzac",
|
||||
sourceMode,
|
||||
profile: state.profile,
|
||||
linuxCncKinematicsResult,
|
||||
});
|
||||
|
||||
if (operatorMessage !== state.operatorMessage) {
|
||||
state.operatorMessage = operatorMessage;
|
||||
}
|
||||
|
||||
return {
|
||||
frame,
|
||||
lastKinematicsResult: linuxCncKinematicsResult,
|
||||
};
|
||||
}
|
||||
|
||||
function canMoveMachine(state) {
|
||||
return state.machine.powerOn && !state.machine.estopActive;
|
||||
}
|
||||
|
||||
function getProgramEndLine(state) {
|
||||
return state.programStartLine + Math.max(state.programLines.length - 1, 0);
|
||||
}
|
||||
|
||||
function getNextProgramLine(state, step) {
|
||||
return Math.min(state.activeLine + step, getProgramEndLine(state));
|
||||
}
|
||||
|
||||
function buildLoadedProgram(action) {
|
||||
const content = String(action.content || "");
|
||||
const lines = parseProgramLines(content);
|
||||
const filename = action.filename || "operator-program.ngc";
|
||||
return {
|
||||
activeProgram: filename,
|
||||
programSource: "operator-file",
|
||||
programStartLine: 1,
|
||||
activeLine: 1,
|
||||
lineCount: lines.length,
|
||||
fileSizeBytes: content.length,
|
||||
programLines: lines,
|
||||
};
|
||||
}
|
||||
|
||||
function parseProgramLines(content) {
|
||||
const lines = content
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trimEnd())
|
||||
.filter((line) => line.trim().length > 0);
|
||||
return lines.length > 0 ? lines : ["(empty program)"];
|
||||
}
|
||||
|
||||
function clampPercent(value, min, max) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
function buildDroFromFrame(frame) {
|
||||
return {
|
||||
x: frame.axisPose.x,
|
||||
y: frame.axisPose.y,
|
||||
z: frame.axisPose.z,
|
||||
a: frame.axisPose.a,
|
||||
b: frame.axisPose.b,
|
||||
c: frame.axisPose.c,
|
||||
tcpX: frame.tcpPose.x,
|
||||
tcpY: frame.tcpPose.y,
|
||||
tcpZ: frame.tcpPose.z,
|
||||
dtgX: frame.rtcpEnabled ? Math.abs(frame.compensation.x) : 0,
|
||||
dtgY: frame.rtcpEnabled ? Math.abs(frame.compensation.y) : 0.01,
|
||||
dtgZ: frame.rtcpEnabled ? Math.abs(frame.compensation.z) : 2.25,
|
||||
};
|
||||
}
|
||||
|
||||
function jointsFromAxisPose(axisPose) {
|
||||
return [
|
||||
Number(axisPose.x || 0),
|
||||
Number(axisPose.y || 0),
|
||||
Number(axisPose.z || 0),
|
||||
Number(axisPose.a || 0),
|
||||
Number(axisPose.c || 0),
|
||||
];
|
||||
}
|
||||
|
||||
function buildFixtureAxisPoseForLine(axisPose, line) {
|
||||
const phase = (line - 496) * 0.17;
|
||||
return {
|
||||
...axisPose,
|
||||
x: 43 + Math.sin(phase) * 4,
|
||||
y: -32.15 + Math.cos(phase) * 2.5,
|
||||
z: -11.306 + Math.sin(phase * 0.7) * 1.2,
|
||||
a: Math.sin(phase * 0.45) * 18,
|
||||
c: Math.cos(phase * 0.33) * 32,
|
||||
};
|
||||
}
|
||||
710
web-rtcp-5axis-sim-plan/app/src/styles/gmoccapy.css
Normal file
@@ -0,0 +1,710 @@
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--panel: #d7d4ce;
|
||||
--panel-dark: #bdb9b1;
|
||||
--border: #aaa59b;
|
||||
--button: #ece9e3;
|
||||
--orange: #ff8618;
|
||||
--green: #18ff28;
|
||||
--green-dark: #02bf19;
|
||||
--black: #050505;
|
||||
--text: #2e2e2e;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
min-width: 1024px;
|
||||
min-height: 768px;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
background: #c8c4bc;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
button {
|
||||
border: 1px solid #bdb8ad;
|
||||
border-radius: 5px;
|
||||
background: linear-gradient(#f7f5ef, #ddd9d1);
|
||||
color: #303030;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.gmoccapy-shell {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
minmax(350px, 0.76fr)
|
||||
minmax(160px, 0.36fr)
|
||||
minmax(128px, 0.28fr)
|
||||
78px
|
||||
minmax(176px, 0.4fr)
|
||||
104px;
|
||||
grid-template-rows: 28px minmax(156px, 0.36fr) minmax(240px, 0.64fr) 224px 62px;
|
||||
grid-template-areas:
|
||||
"title title title title title title"
|
||||
"preview preview dro dro dro side"
|
||||
"preview preview gcode gcode gcode side"
|
||||
"info override override spindle spindle side"
|
||||
"bottom bottom bottom bottom bottom side";
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
min-width: 1024px;
|
||||
min-height: 768px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.titlebar {
|
||||
grid-area: title;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
padding: 2px 10px;
|
||||
background: #cfcbc3;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.brand-dot {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: 2px solid #ffcf00;
|
||||
border-radius: 50%;
|
||||
color: #e21d1d;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.title-stack {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.title-stack strong {
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.title-stack span,
|
||||
.run-state {
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
color: #4d4d4d;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.run-state {
|
||||
min-width: 76px;
|
||||
text-align: right;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.preview-panel {
|
||||
grid-area: preview;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: var(--black);
|
||||
border-right: 2px solid #8d887f;
|
||||
border-bottom: 2px solid #8d887f;
|
||||
}
|
||||
|
||||
.program-path {
|
||||
position: absolute;
|
||||
inset: 9px 0 auto 0;
|
||||
z-index: 2;
|
||||
color: #f6f6f6;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
line-height: 1.2;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.machine-preview {
|
||||
width: 100%;
|
||||
height: calc(100% - 54px);
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.tool-preview-card {
|
||||
position: absolute;
|
||||
top: 34px;
|
||||
left: 10px;
|
||||
z-index: 2;
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
gap: 2px 8px;
|
||||
min-width: 154px;
|
||||
max-width: 230px;
|
||||
padding: 7px 9px;
|
||||
border: 1px solid #316e74;
|
||||
background: rgba(4, 20, 22, 0.86);
|
||||
color: #e7ffff;
|
||||
font: 12px/1.25 "Courier New", monospace;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tool-preview-card strong {
|
||||
grid-row: span 3;
|
||||
align-self: center;
|
||||
color: #21f2f2;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.tool-preview-card span {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.machine-envelope,
|
||||
.machine-grid {
|
||||
fill: none;
|
||||
stroke: #d21f1f;
|
||||
stroke-width: 1.4;
|
||||
}
|
||||
|
||||
.machine-grid {
|
||||
stroke: #3d3d3d;
|
||||
}
|
||||
|
||||
.machine-rapid {
|
||||
fill: none;
|
||||
stroke: #9e6b00;
|
||||
stroke-width: 1.6;
|
||||
}
|
||||
|
||||
.toolpath {
|
||||
fill: none;
|
||||
stroke: #f7f7f7;
|
||||
stroke-width: 1.8;
|
||||
}
|
||||
|
||||
.tool-axis {
|
||||
stroke: #22e6e6;
|
||||
stroke-width: 1.4;
|
||||
}
|
||||
|
||||
.tcp-point {
|
||||
fill: #21f2f2;
|
||||
}
|
||||
|
||||
.axis-label {
|
||||
fill: #3864ff;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.rtcp-preview-badge {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
bottom: 58px;
|
||||
left: 8px;
|
||||
z-index: 2;
|
||||
overflow: hidden;
|
||||
padding: 4px 6px;
|
||||
border: 1px solid #215b2b;
|
||||
background: rgba(2, 16, 5, 0.82);
|
||||
color: var(--green);
|
||||
font: 11px/1.25 "Courier New", monospace;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.rtcp-preview-badge[data-rtcp-preview-state="off"] {
|
||||
border-color: #4b4b4b;
|
||||
color: #b7c7b8;
|
||||
}
|
||||
|
||||
.preview-toolbar {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
background: var(--panel);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.preview-toolbar button,
|
||||
.bottom-controls button,
|
||||
.status-sidebar button {
|
||||
min-height: 46px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.dro-panel {
|
||||
grid-area: dro;
|
||||
display: grid;
|
||||
grid-template-rows: 1fr auto;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid #151515;
|
||||
background: var(--black);
|
||||
}
|
||||
|
||||
.dro-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: repeat(3, minmax(0, 1fr));
|
||||
gap: 1px;
|
||||
min-height: 0;
|
||||
background: #111111;
|
||||
}
|
||||
|
||||
.dro-row {
|
||||
display: grid;
|
||||
grid-template-columns: 24px 40px minmax(74px, 1fr) 48px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
padding: 3px 5px;
|
||||
background: var(--black);
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
.dro-axis {
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.dro-mode,
|
||||
.dro-dtg {
|
||||
color: var(--green);
|
||||
font-size: 9px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.dro-row strong {
|
||||
overflow: hidden;
|
||||
text-align: right;
|
||||
font-size: clamp(20px, 2.4vw, 30px);
|
||||
line-height: 1;
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-overflow: clip;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tcp-strip {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
justify-content: space-between;
|
||||
min-width: 0;
|
||||
padding: 5px 8px;
|
||||
background: #080808;
|
||||
color: var(--green);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.tcp-strip span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tcp-strip [data-rtcp-value="tcp"] {
|
||||
flex: 1.2 1 190px;
|
||||
}
|
||||
|
||||
.tcp-strip [data-rtcp-value="tool-axis"] {
|
||||
flex: 1 1 150px;
|
||||
}
|
||||
|
||||
.tcp-strip [data-rtcp-value="state"] {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.gcode-panel {
|
||||
grid-area: gcode;
|
||||
display: grid;
|
||||
grid-template-rows: 30px minmax(0, 1fr) 22px;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
background: #f4f2ed;
|
||||
border-top: 2px solid var(--border);
|
||||
border-bottom: 2px solid var(--border);
|
||||
}
|
||||
|
||||
.gcode-header {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
padding: 5px 8px;
|
||||
border-bottom: 1px solid #d5d0c7;
|
||||
background: #eeeae3;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.gcode-header strong,
|
||||
.gcode-header span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.gcode-header [data-active-program-line] {
|
||||
color: #005bab;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.gcode-list {
|
||||
min-height: 0;
|
||||
margin: 0;
|
||||
padding: 4px 6px 2px;
|
||||
overflow: auto;
|
||||
list-style: none;
|
||||
font-family: "Courier New", monospace;
|
||||
font-size: 15px;
|
||||
color: #8b8b8b;
|
||||
}
|
||||
|
||||
.gcode-row {
|
||||
display: grid;
|
||||
grid-template-columns: 42px minmax(0, 1fr);
|
||||
gap: 6px;
|
||||
min-height: 20px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.gcode-row.active {
|
||||
background: #e8e8e8;
|
||||
color: #202020;
|
||||
}
|
||||
|
||||
.gcode-row code {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.gcode-progress {
|
||||
display: grid;
|
||||
grid-template-columns: 120px 1fr;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 2px 8px 4px;
|
||||
font-size: 12px;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.gcode-progress div {
|
||||
height: 6px;
|
||||
background: #d0d0d0;
|
||||
}
|
||||
|
||||
.gcode-progress i {
|
||||
display: block;
|
||||
height: 100%;
|
||||
background: #3888df;
|
||||
}
|
||||
|
||||
.status-sidebar {
|
||||
grid-area: side;
|
||||
display: grid;
|
||||
grid-template-rows: repeat(9, minmax(44px, 1fr)) minmax(46px, auto);
|
||||
gap: 5px;
|
||||
padding: 6px;
|
||||
border-left: 2px solid var(--border);
|
||||
background: var(--panel);
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.sidebar-button {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
padding: 3px;
|
||||
font-size: 12px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.sidebar-button.estop {
|
||||
color: #c30000;
|
||||
}
|
||||
|
||||
.sidebar-button.power {
|
||||
color: var(--green-dark);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.sidebar-button[data-active="true"] {
|
||||
border-color: #278b37;
|
||||
background: linear-gradient(#f6fff5, #aee9ae);
|
||||
color: #075f16;
|
||||
}
|
||||
|
||||
.status-sidebar time {
|
||||
padding: 4px 0;
|
||||
text-align: center;
|
||||
color: #4c4c4c;
|
||||
font-size: 14px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.info-tabs {
|
||||
grid-area: info;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
border-top: 2px solid var(--border);
|
||||
border-right: 2px solid var(--border);
|
||||
background: #eeeae3;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.tabs button {
|
||||
border: 0;
|
||||
border-right: 1px solid var(--border);
|
||||
border-radius: 0;
|
||||
background: #e0ddd6;
|
||||
min-height: 37px;
|
||||
padding: 4px 5px;
|
||||
font-size: 14px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tabs button.active {
|
||||
background: #f3f0ea;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: max-content 1fr;
|
||||
gap: 3px 10px;
|
||||
margin: 8px;
|
||||
font-size: 14px;
|
||||
line-height: 1.22;
|
||||
}
|
||||
|
||||
.info-grid dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.info-grid dd {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.info-grid [data-rtcp-diagnostic] {
|
||||
font-family: "Courier New", monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.override-panel {
|
||||
grid-area: override;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 56px 64px minmax(78px, 1fr);
|
||||
gap: 6px;
|
||||
padding: 6px;
|
||||
border-top: 2px solid var(--border);
|
||||
border-right: 1px solid var(--border);
|
||||
background: #eeeae3;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.meter-card,
|
||||
.override-control,
|
||||
.cooling,
|
||||
.spindle {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 2px;
|
||||
padding: 5px;
|
||||
background: #f8f5ef;
|
||||
border: 1px solid var(--border);
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.meter-card h2,
|
||||
.override-control h2,
|
||||
.cooling h2,
|
||||
.spindle h2 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.15;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.meter-card strong,
|
||||
.override-control strong,
|
||||
.spindle strong {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
font-size: clamp(18px, 1.8vw, 25px);
|
||||
line-height: 1.05;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.meter-card {
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
align-content: center;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.meter-card h2 {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.meter-card strong {
|
||||
display: inline;
|
||||
font-size: clamp(20px, 2vw, 25px);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.meter-card span {
|
||||
padding-left: 5px;
|
||||
font-size: 14px;
|
||||
line-height: 1.1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.stepper {
|
||||
display: grid;
|
||||
grid-template-columns: 32px minmax(54px, 1fr) 32px;
|
||||
gap: 2px;
|
||||
align-items: center;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.stepper div {
|
||||
min-width: 0;
|
||||
padding: 6px 4px;
|
||||
background: var(--orange);
|
||||
border: 1px solid #b4651c;
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.stepper button {
|
||||
min-width: 0;
|
||||
min-height: 28px;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.spindle-coolant-panel {
|
||||
grid-area: spindle;
|
||||
display: grid;
|
||||
grid-template-columns: 76px minmax(0, 1fr);
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
padding: 6px;
|
||||
background: #eeeae3;
|
||||
border-top: 2px solid var(--border);
|
||||
}
|
||||
|
||||
.cooling {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr 1fr;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.cooling button.active {
|
||||
background: #39ef37;
|
||||
}
|
||||
|
||||
.spindle-range {
|
||||
height: 24px;
|
||||
margin-top: 8px;
|
||||
background: #bdbdbd;
|
||||
border: 1px solid #898989;
|
||||
}
|
||||
|
||||
.spindle-range i {
|
||||
display: block;
|
||||
height: 100%;
|
||||
background: #24e83d;
|
||||
}
|
||||
|
||||
.bottom-controls {
|
||||
grid-area: bottom;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(13, minmax(48px, 1fr));
|
||||
gap: 5px;
|
||||
padding: 6px 8px;
|
||||
border-top: 2px solid var(--border);
|
||||
background: var(--panel);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.bottom-controls button {
|
||||
font-size: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.program-file-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.gmoccapy-shell {
|
||||
grid-template-columns: 350px 160px 128px 78px 176px 96px;
|
||||
}
|
||||
|
||||
.dro-row strong {
|
||||
font-size: 21px;
|
||||
}
|
||||
|
||||
.dro-row {
|
||||
grid-template-columns: 22px 36px minmax(66px, 1fr) 44px;
|
||||
}
|
||||
|
||||
.info-grid,
|
||||
.gcode-list {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tabs button,
|
||||
.meter-card h2,
|
||||
.override-control h2,
|
||||
.cooling h2,
|
||||
.spindle h2 {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
355
web-rtcp-5axis-sim-plan/app/src/ui/gmoccapy-shell.js
Normal 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("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
59470
web-rtcp-5axis-sim-plan/app/src/vendor/three/three.core.js
vendored
Normal file
19306
web-rtcp-5axis-sim-plan/app/src/vendor/three/three.module.js
vendored
Normal file
203
web-rtcp-5axis-sim-plan/app/src/visualization/five-axis-scene.js
Normal file
@@ -0,0 +1,203 @@
|
||||
import * as THREE from "../vendor/three/three.module.js";
|
||||
|
||||
const scenes = new WeakMap();
|
||||
|
||||
export function renderFiveAxisScene(canvas, state) {
|
||||
const preview = scenes.get(canvas) || createScene(canvas);
|
||||
scenes.set(canvas, preview);
|
||||
|
||||
resizeRenderer(preview);
|
||||
updateMachinePose(preview, state);
|
||||
preview.renderer.render(preview.scene, preview.camera);
|
||||
|
||||
const pointCount = preview.pathLine.geometry.getAttribute("position").count;
|
||||
canvas.dataset.threeReady = "true";
|
||||
canvas.dataset.threeRevision = THREE.REVISION;
|
||||
canvas.dataset.threePathPoints = String(pointCount);
|
||||
canvas.dataset.threeSceneObjects = String(countSceneObjects(preview.scene));
|
||||
canvas.dataset.threeToolhead = JSON.stringify(toRoundedVector(preview.toolGroup.position));
|
||||
canvas.dataset.threeToolAxis = JSON.stringify(toRoundedVector(state.toolAxisVector));
|
||||
canvas.dataset.threeTcpPose = JSON.stringify(toRoundedPose(state.tcpPose));
|
||||
canvas.dataset.threeRtcpState = state.rtcpState;
|
||||
canvas.dataset.threeSelectedView = state.preview.selectedView;
|
||||
canvas.dataset.threeFrameApi = state.rtcpFrame.apiName;
|
||||
}
|
||||
|
||||
function createScene(canvas) {
|
||||
const renderer = new THREE.WebGLRenderer({
|
||||
canvas,
|
||||
antialias: true,
|
||||
preserveDrawingBuffer: true,
|
||||
});
|
||||
renderer.setClearColor(0x050505, 1);
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.PerspectiveCamera(38, 1, 0.1, 100);
|
||||
camera.position.set(4.2, -6.4, 4.8);
|
||||
camera.lookAt(0, 0, 0);
|
||||
|
||||
const ambient = new THREE.AmbientLight(0xffffff, 0.46);
|
||||
const key = new THREE.DirectionalLight(0xffffff, 1.1);
|
||||
key.position.set(3, -5, 7);
|
||||
scene.add(ambient, key);
|
||||
|
||||
const grid = new THREE.GridHelper(6.8, 12, 0x444444, 0x242424);
|
||||
grid.rotation.x = Math.PI / 2;
|
||||
scene.add(grid);
|
||||
|
||||
const axes = new THREE.AxesHelper(1.25);
|
||||
axes.position.set(-2.7, -2.25, -1.05);
|
||||
scene.add(axes);
|
||||
|
||||
const envelope = buildEnvelope();
|
||||
scene.add(envelope);
|
||||
|
||||
const tableGroup = new THREE.Group();
|
||||
const table = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(2.55, 1.9, 0.16),
|
||||
new THREE.MeshStandardMaterial({ color: 0x353535, roughness: 0.72, metalness: 0.2 }),
|
||||
);
|
||||
const platter = new THREE.Mesh(
|
||||
new THREE.CylinderGeometry(0.72, 0.72, 0.13, 48),
|
||||
new THREE.MeshStandardMaterial({ color: 0x4f5960, roughness: 0.62, metalness: 0.35 }),
|
||||
);
|
||||
platter.rotation.x = Math.PI / 2;
|
||||
platter.position.z = 0.14;
|
||||
tableGroup.add(table, platter);
|
||||
scene.add(tableGroup);
|
||||
|
||||
const pathLine = buildToolpath();
|
||||
scene.add(pathLine);
|
||||
|
||||
const toolGroup = new THREE.Group();
|
||||
const toolBody = new THREE.Mesh(
|
||||
new THREE.CylinderGeometry(0.035, 0.055, 0.86, 24),
|
||||
new THREE.MeshStandardMaterial({ color: 0x26d7df, emissive: 0x073a3d, roughness: 0.32 }),
|
||||
);
|
||||
toolBody.rotation.x = Math.PI / 2;
|
||||
toolBody.position.z = 0.43;
|
||||
const tcpPoint = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(0.075, 24, 16),
|
||||
new THREE.MeshStandardMaterial({ color: 0x1ffff4, emissive: 0x094f4f, roughness: 0.2 }),
|
||||
);
|
||||
const toolAxis = new THREE.Line(
|
||||
new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(), new THREE.Vector3(0, 0, 1)]),
|
||||
new THREE.LineBasicMaterial({ color: 0x21f2f2, linewidth: 2 }),
|
||||
);
|
||||
toolGroup.add(toolBody, tcpPoint, toolAxis);
|
||||
scene.add(toolGroup);
|
||||
|
||||
const preview = {
|
||||
renderer,
|
||||
scene,
|
||||
camera,
|
||||
tableGroup,
|
||||
toolGroup,
|
||||
toolAxis,
|
||||
pathLine,
|
||||
};
|
||||
resizeRenderer(preview);
|
||||
return preview;
|
||||
}
|
||||
|
||||
function buildEnvelope() {
|
||||
const geometry = new THREE.BoxGeometry(5.8, 4.3, 2.6);
|
||||
const edges = new THREE.EdgesGeometry(geometry);
|
||||
const line = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: 0xcc2525 }));
|
||||
line.position.z = 0.2;
|
||||
return line;
|
||||
}
|
||||
|
||||
function buildToolpath() {
|
||||
const points = [];
|
||||
for (let index = 0; index < 72; index += 1) {
|
||||
const t = index / 71;
|
||||
const x = -2.3 + t * 4.6;
|
||||
const y = Math.sin(t * Math.PI * 13) * 0.28;
|
||||
const z = -0.75 + Math.sin(t * Math.PI * 2) * 0.36;
|
||||
points.push(new THREE.Vector3(x, y, z));
|
||||
}
|
||||
const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
||||
return new THREE.Line(geometry, new THREE.LineBasicMaterial({ color: 0xf4f4f4 }));
|
||||
}
|
||||
|
||||
function updateMachinePose(preview, state) {
|
||||
const tcp = state.tcpPose;
|
||||
const tool = state.toolAxisVector;
|
||||
const tcpPosition = new THREE.Vector3(
|
||||
clamp(tcp.x * 0.035, -2.7, 2.7),
|
||||
clamp(tcp.y * 0.035, -2.0, 2.0),
|
||||
clamp(tcp.z * 0.04 + 0.35, -1.1, 1.9),
|
||||
);
|
||||
const toolAxisVector = new THREE.Vector3(tool.x, tool.y, tool.z || 1).normalize();
|
||||
|
||||
preview.toolGroup.position.copy(tcpPosition);
|
||||
preview.toolGroup.quaternion.setFromUnitVectors(new THREE.Vector3(0, 0, 1), toolAxisVector);
|
||||
preview.toolAxis.geometry.setFromPoints([
|
||||
new THREE.Vector3(0, 0, 0),
|
||||
toolAxisVector.clone().multiplyScalar(state.rtcpState === "on" ? 1.2 : 0.85),
|
||||
]);
|
||||
preview.tableGroup.rotation.x = THREE.MathUtils.degToRad(state.axisPose.a);
|
||||
preview.tableGroup.rotation.z = THREE.MathUtils.degToRad(state.axisPose.c);
|
||||
setCameraView(preview.camera, state.preview.selectedView);
|
||||
}
|
||||
|
||||
function setCameraView(camera, selectedView) {
|
||||
if (selectedView === "x") {
|
||||
camera.position.set(6, 0.02, 0.4);
|
||||
} else if (selectedView === "y") {
|
||||
camera.position.set(0.02, -6, 0.6);
|
||||
} else if (selectedView === "z") {
|
||||
camera.position.set(0.01, -0.02, 7);
|
||||
} else {
|
||||
camera.position.set(4.2, -6.4, 4.8);
|
||||
}
|
||||
camera.lookAt(0, 0, 0);
|
||||
camera.updateProjectionMatrix();
|
||||
}
|
||||
|
||||
function resizeRenderer(preview) {
|
||||
const { canvas } = preview.renderer.domElement;
|
||||
const width = Math.max(canvas.clientWidth, 320);
|
||||
const height = Math.max(canvas.clientHeight, 240);
|
||||
if (canvas.width !== width || canvas.height !== height) {
|
||||
preview.renderer.setSize(width, height, false);
|
||||
}
|
||||
preview.camera.aspect = width / height;
|
||||
preview.camera.updateProjectionMatrix();
|
||||
}
|
||||
|
||||
function countSceneObjects(object) {
|
||||
let count = 1;
|
||||
for (const child of object.children) {
|
||||
count += countSceneObjects(child);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
function toRoundedVector(vector) {
|
||||
return {
|
||||
x: round(vector.x),
|
||||
y: round(vector.y),
|
||||
z: round(vector.z),
|
||||
};
|
||||
}
|
||||
|
||||
function toRoundedPose(pose) {
|
||||
return {
|
||||
x: round(pose.x),
|
||||
y: round(pose.y),
|
||||
z: round(pose.z),
|
||||
a: round(pose.a),
|
||||
c: round(pose.c),
|
||||
};
|
||||
}
|
||||
|
||||
function round(value) {
|
||||
return Math.round(Number(value) * 1000) / 1000;
|
||||
}
|
||||
|
||||
function clamp(value, min, max) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
12
web-rtcp-5axis-sim-plan/app/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"target": "ES2022"
|
||||
},
|
||||
"include": ["src/**/*.js"]
|
||||
}
|
||||
|
After Width: | Height: | Size: 61 KiB |
BIN
web-rtcp-5axis-sim-plan/assets/reference/linuxcnc-gui/axis.png
Normal file
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 204 KiB |
|
After Width: | Height: | Size: 240 KiB |
|
After Width: | Height: | Size: 172 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 20 KiB |
357
web-rtcp-5axis-sim-plan/docs/development-continuation.md
Normal file
@@ -0,0 +1,357 @@
|
||||
# 5 轴数控系统 Web 仿真编写过程接续文档
|
||||
|
||||
生成时间:2026-06-20 CST
|
||||
|
||||
## 1. 当前结论
|
||||
|
||||
已确定:
|
||||
|
||||
- Web 前端界面可实现;
|
||||
- 首选界面风格为 `gmoccapy_5_axis.png`;
|
||||
- 前端不使用 React/Vue 等框架,采用原生 HTML/CSS + TypeScript/JavaScript ES modules;
|
||||
- 3D 使用 Three.js;
|
||||
- LinuxCNC Python GUI 只做界面参考;
|
||||
- LinuxCNC C/C++/WASM/source-derived boundary 才能作为 CNC 语义和五轴运动学依据。
|
||||
|
||||
当前准备文档已完成:
|
||||
|
||||
```text
|
||||
README.md
|
||||
docs/implementation-plan.md
|
||||
docs/technical-roadmap.md
|
||||
docs/program-implementation-guide.md
|
||||
docs/development-continuation.md
|
||||
docs/traceability-matrix.md
|
||||
docs/linuxcnc-python-gui-reference.md
|
||||
docs/linuxcnc-gui-reference-gallery.md
|
||||
```
|
||||
|
||||
## 2. 下一轮直接开工任务
|
||||
|
||||
下一轮不再继续扩展方案,直接进入实现。
|
||||
|
||||
第一批任务:
|
||||
|
||||
```text
|
||||
M1-web-shell-gmoccapy
|
||||
```
|
||||
|
||||
状态:
|
||||
|
||||
```text
|
||||
completed_with_build_gate
|
||||
```
|
||||
|
||||
目标:
|
||||
|
||||
- 创建 `app/`;
|
||||
- 创建原生 HTML/CSS/TS 项目;
|
||||
- 实现 gmoccapy 风格静态 shell;
|
||||
- 复制必要的参考图路径到 docs;
|
||||
- 加 browser smoke 验证页面区域存在。
|
||||
|
||||
已补齐 M1 工程化验收:
|
||||
|
||||
- `app/tsconfig.json` 已创建;
|
||||
- `npm run build` 已可输出静态产物;
|
||||
- build 产物目录 `app/dist/` 已加入 `.gitignore`;
|
||||
- browser smoke 已检查无 React/Vue/Angular/Svelte 依赖和 DOM 标记。
|
||||
|
||||
## 3. M1 任务拆分
|
||||
|
||||
### M1.1 初始化 app
|
||||
|
||||
创建:
|
||||
|
||||
```text
|
||||
app/index.html
|
||||
app/package.json
|
||||
app/tsconfig.json
|
||||
app/src/main.ts
|
||||
app/src/styles/gmoccapy.css
|
||||
```
|
||||
|
||||
要求:
|
||||
|
||||
- 不安装 React/Vue;
|
||||
- 可使用 Vite;
|
||||
- `npm run dev` 能启动;
|
||||
- `npm run build` 能输出静态产物。
|
||||
|
||||
### M1.2 静态 shell
|
||||
|
||||
创建:
|
||||
|
||||
```text
|
||||
app/src/ui/gmoccapy-shell.ts
|
||||
```
|
||||
|
||||
渲染区域:
|
||||
|
||||
```text
|
||||
titlebar
|
||||
preview
|
||||
dro
|
||||
gcode
|
||||
status-sidebar
|
||||
info-tabs
|
||||
override
|
||||
spindle-coolant
|
||||
bottom-controls
|
||||
```
|
||||
|
||||
### M1.3 CSS layout
|
||||
|
||||
目标:
|
||||
|
||||
- 视觉接近 `gmoccapy_5_axis.png`;
|
||||
- 黑底 preview;
|
||||
- 绿色 DRO;
|
||||
- 灰色面板;
|
||||
- 橙色 override;
|
||||
- 右侧竖向大按钮;
|
||||
- 底部大按钮栏。
|
||||
|
||||
### M1.4 smoke
|
||||
|
||||
创建:
|
||||
|
||||
```text
|
||||
tests/browser/gmoccapy_shell_smoke.html
|
||||
tests/browser/verify_gmoccapy_shell_browser.sh
|
||||
```
|
||||
|
||||
检查:
|
||||
|
||||
- 页面加载;
|
||||
- 关键 `data-region` 存在;
|
||||
- DRO 文本非空;
|
||||
- G-code rows 存在;
|
||||
- preview 容器非空;
|
||||
- 无 React/Vue 依赖标记。
|
||||
|
||||
当前 gate:
|
||||
|
||||
```text
|
||||
gmoccapy_static_build=ok
|
||||
gmoccapy_shell_smoke=ok
|
||||
```
|
||||
|
||||
## 4. M2 任务
|
||||
|
||||
```text
|
||||
M2-state-and-controls
|
||||
```
|
||||
|
||||
状态:
|
||||
|
||||
```text
|
||||
completed_with_rtcp_frame_and_control_wiring
|
||||
```
|
||||
|
||||
目标:
|
||||
|
||||
- 实现 store;
|
||||
- 实现 Run/Stop/Pause/Step action;
|
||||
- G-code active line;
|
||||
- DRO state update;
|
||||
- Node smoke。
|
||||
|
||||
本轮追加 RTCP 最小可验证链路:
|
||||
|
||||
- 新增 `xyzac-trt` profile source reference;
|
||||
- 新增 `web-rtcp-5axis-motion-frame`;
|
||||
- store 输出 `axisPose`、`jointPose`、`tcpPose`、`toolAxisVector`、`rtcpFrame`;
|
||||
- 右侧 TCP/IDENTITY 按钮可切换 `rtcpState=on/off`;
|
||||
- DRO 和 info tabs 显示 TCP pose、tool axis vector、RTCP frame readiness;
|
||||
- browser smoke 验证 RTCP DOM 和状态同步;
|
||||
- node smoke 验证 frame/store 行为。
|
||||
|
||||
本轮继续补齐 M2 控件接线:
|
||||
|
||||
- preview X/Y/Z/Fit/Clear 按钮接入 store;
|
||||
- Rapid Override / Feed Rate 加减按钮接入 store;
|
||||
- Spindle override 加减按钮接入 store;
|
||||
- Flood / Mist 冷却按钮接入 store;
|
||||
- Reload / Home / Full 底部按钮接入 store;
|
||||
- `operatorMessage` 记录最近一次仿真操作;
|
||||
- Node smoke 和 browser smoke 覆盖上述控制链路。
|
||||
|
||||
边界说明:
|
||||
|
||||
```text
|
||||
sourceMode=fixture-ui-only
|
||||
semanticBoundary=fixture_frame_ui_plumbing_not_linuxcnc_kinematics_proof
|
||||
linuxCncKinematicsReady=false
|
||||
promotionAllowed=false
|
||||
```
|
||||
|
||||
也就是说,Web 仿真界面现在已经具备 RTCP 状态链路和显示链路,但尚未把 LinuxCNC/source-derived 五轴运动学 WASM 接入为最终语义源。
|
||||
|
||||
## 5. M3 任务
|
||||
|
||||
```text
|
||||
M3-threejs-preview
|
||||
```
|
||||
|
||||
状态:
|
||||
|
||||
```text
|
||||
completed_with_canvas_smoke
|
||||
```
|
||||
|
||||
目标:
|
||||
|
||||
- Three.js canvas;
|
||||
- 基础五轴机床;
|
||||
- tool marker;
|
||||
- simple path;
|
||||
- canvas nonblank browser smoke。
|
||||
|
||||
M3 需要消费 M2 的 `rtcpFrame`:
|
||||
|
||||
- Three.js tool marker 使用 `tcpPose`;
|
||||
- 刀轴显示使用 `toolAxisVector`;
|
||||
- RTCP on/off 需要在预览中产生可见姿态差异;
|
||||
- canvas smoke 需要检查 frame 与预览数据同步。
|
||||
|
||||
已完成:
|
||||
|
||||
- 新增 `app/src/vendor/three/three.module.js` 和 `three.core.js`;
|
||||
- 新增 `app/src/visualization/five-axis-scene.js`;
|
||||
- gmoccapy preview 区域由真实 WebGL canvas 渲染;
|
||||
- 预览显示基础五轴工作区、工作台、刀具/TCP marker、刀轴和刀路;
|
||||
- Three.js canvas 消费 `tcpPose`、`toolAxisVector`、`rtcpState`、`rtcpFrame`;
|
||||
- browser smoke 验证 canvas nonblank、scene objects、path points、RTCP on/off 同步和 STEP 后 TCP pose 更新。
|
||||
|
||||
当前 gate:
|
||||
|
||||
```text
|
||||
gmoccapy_static_build=ok
|
||||
rtcp_store_smoke=ok
|
||||
gmoccapy_shell_smoke=ok
|
||||
```
|
||||
|
||||
## 6. M4 任务
|
||||
|
||||
```text
|
||||
M4-profile-and-linuxcnc-boundary
|
||||
```
|
||||
|
||||
状态:
|
||||
|
||||
```text
|
||||
completed_with_profile_boundary_smoke
|
||||
```
|
||||
|
||||
目标:
|
||||
|
||||
- `xyzac-trt` profile;
|
||||
- source reference map;
|
||||
- PyVCP/HAL panel schema;
|
||||
- LinuxCNC adapter 接入点;
|
||||
- traceability update。
|
||||
|
||||
已完成:
|
||||
|
||||
- 扩展 `app/src/profiles/xyzac-trt.js`,记录 INI、PyVCP XML、postgui HAL、generated HAL、tool table、remap、switchkins type、HAL pins、offsets、sample programs;
|
||||
- 进一步按 LinuxCNC `xyzac-trt.ini`/`xyzac-trt_cmds.hal`/`*.tbl`/`428-430remap.ngc` 整理 machine name、DISPLAY/RS274NGC/TRAJ、axis/joint limits、HALCMD nets、HALUI MDI commands、tool table 条目;
|
||||
- 新增 `app/src/profiles/source-reference-map.js`,建立 `xyzac-trt` source/config/reference map;
|
||||
- 新增 `app/src/panel-schema/xyzac-trt-pyvcp.js`,把 `xyzac-trt.xml` 与 `switchkins_postgui.hal` 的 SWITCHKINS 控件整理为 Web panel schema;
|
||||
- 新增 `app/src/runtime/linuxcnc-boundary-adapter.js`,作为后续 LinuxCNC interpreter/kinematics WASM 的接入点;
|
||||
- store 输出 `linuxCncBoundaryAdapter` 和 `linuxCncBoundaryReadiness`;
|
||||
- info tabs 显示 adapter、panel schema、source map 和 boundary readiness;
|
||||
- 新增 `tests/node/verify_profile_boundary.mjs`,并接入 `npm run smoke:node`;
|
||||
- browser smoke 验证 LinuxCNC boundary adapter/schema/readiness DOM 状态。
|
||||
|
||||
边界说明:
|
||||
|
||||
```text
|
||||
sourceMapBoundary=profile_source_map_only_not_runtime_proof
|
||||
panelSchemaBoundary=pyvcp_hal_schema_reference_only
|
||||
adapterBoundary=adapter_entrypoint_only_runtime_not_connected
|
||||
linuxCncKinematicsReady=false
|
||||
promotionAllowed=false
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
```text
|
||||
M4 现在可视为在 profile/source-map/panel-schema/adapter 层面完全满足;
|
||||
但这不等于 LinuxCNC interpreter/kinematics runtime 已接入。
|
||||
```
|
||||
|
||||
## 7. M5 任务
|
||||
|
||||
```text
|
||||
M5-operator-program-workflow
|
||||
```
|
||||
|
||||
状态:
|
||||
|
||||
```text
|
||||
completed_with_operator_program_smoke
|
||||
```
|
||||
|
||||
目标:
|
||||
|
||||
- 上电、急停、自动、手动、JOG、MDI、复位等操作;
|
||||
- 加载 G-code 文件;
|
||||
- 刀具预览;
|
||||
- 执行程序并显示执行的当前行。
|
||||
|
||||
已完成:
|
||||
|
||||
- store 新增 `machine.powerOn`、`machine.estopActive`、`machine.mode`、JOG/MDI/reset 状态;
|
||||
- 右侧按钮栏新增 POWER、E-STOP、RESET、AUTO、MANUAL、JOG、MDI;
|
||||
- 底部控制栏新增本地 G-code 文件输入、JOG X/Y、MDI run;
|
||||
- `LOAD_PROGRAM` 可从浏览器 FileReader 或 public dispatch 加载 operator G-code 文本;
|
||||
- G-code 面板显示程序来源、当前执行行,并按当前行高亮;
|
||||
- Three.js 预览区域新增刀具预览卡,显示 T 号、直径、长度和 holder;
|
||||
- RUN/STEP 会在上电且非急停状态下推进当前行,未上电时明确 blocked;
|
||||
- node/browser smoke 覆盖上电、加载程序、运行、高亮当前行、JOG、MDI、复位和急停。
|
||||
|
||||
边界说明:
|
||||
|
||||
```text
|
||||
operatorWorkflowBoundary=browser_ui_runtime_fixture
|
||||
gcodeLoadBoundary=file_text_staging_only
|
||||
programExecutionBoundary=fixture_line_playback_not_linuxcnc_interpreter
|
||||
linuxCncKinematicsReady=false
|
||||
promotionAllowed=false
|
||||
```
|
||||
|
||||
## 8. 每批完成后必须更新
|
||||
|
||||
每批完成后更新:
|
||||
|
||||
- 本文件的“当前状态”;
|
||||
- `docs/traceability-matrix.md`;
|
||||
- 如果新增 UI 或 runtime 约束,更新 `docs/program-implementation-guide.md`;
|
||||
- 如果新增参考来源,更新 `docs/linuxcnc-python-gui-reference.md` 或 `docs/linuxcnc-gui-reference-gallery.md`。
|
||||
|
||||
## 9. 当前状态
|
||||
|
||||
```text
|
||||
status=M6_linuxcnc_kinematics_frame_proof_complete
|
||||
active_style=gmoccapy_5_axis
|
||||
frontend_framework=none
|
||||
ui_stack=html_css_typescript_es_modules
|
||||
preview_stack=threejs
|
||||
semantic_boundary=linuxcnc_owned
|
||||
latest_batch=M6-linuxcnc-kinematics-frame-proof
|
||||
latest_gate=linuxcnc_kinematics_runtime_smoke=ok,profile_boundary_smoke=ok,rtcp_store_smoke=ok,gmoccapy_shell_smoke=ok,gmoccapy_static_build=ok
|
||||
rtcp_ui_state=implemented_fixture_fallback_and_node_kinematics_wasm_frame
|
||||
control_wiring=power_estop_reset_auto_manual_jog_mdi_run_stop_pause_step_overrides_coolant_spindle_preview_home_reload_full
|
||||
gcode_loading=implemented_browser_file_text_staging
|
||||
program_current_line=implemented_fixture_line_playback_and_highlight
|
||||
tool_preview=implemented_tool_card_and_threejs_marker
|
||||
threejs_preview=implemented_basic_canvas_scene
|
||||
profile_source_map=implemented_xyzac_trt
|
||||
pyvcp_hal_schema=implemented_xyzac_trt_switchkins
|
||||
linuxcnc_boundary_adapter=kinematics_runtime_connected_node_interpreter_remap_missing
|
||||
linuxcnc_kinematics_wasm=node_proof_ready_xyzac_trt
|
||||
browser_kinematics_wasm=not_connected_fixture_fallback
|
||||
full_program_execution=not_promoted_fixture_line_playback
|
||||
next_batch=browser_kinematics_wasm_asset_worker_or_interpreter_execution_source
|
||||
```
|
||||
396
web-rtcp-5axis-sim-plan/docs/implementation-plan.md
Normal file
@@ -0,0 +1,396 @@
|
||||
# 基于 Web 的 RTCP 五轴联动数控系统仿真实现方案
|
||||
|
||||
生成时间:2026-06-20 CST
|
||||
|
||||
## 1. 项目定位
|
||||
|
||||
本项目建设一个独立的 Web 数控系统仿真界面,重点支持五轴联动与 RTCP/TCP 模式展示。系统以 LinuxCNC 源码、`configs/sim` 案例和 LinuxCNC Python 图形界面程序为参考基础,复用当前项目已经完成的 WASM、OPFS、virtual HAL、AXIS 风格 UI、G-code 执行仿真和 5 轴 source coverage 成果。
|
||||
|
||||
边界必须清晰:
|
||||
|
||||
- 这是 Web 数控系统仿真,不是硬实时机床控制器。
|
||||
- 浏览器不直接驱动真实伺服、IO 或 Linux kernel realtime ABI。
|
||||
- G-code 解释、canonical event、五轴运动学、remap 语义、tool/parameter 行为应尽量保持 LinuxCNC-owned。
|
||||
- JavaScript 只做 UI、会话、文件 staging、WASM 调用、状态编排和可视化映射。
|
||||
- 前端界面采用原生 HTML/CSS + TypeScript/JavaScript ES modules,不使用 React/Vue 等 UI 框架。
|
||||
- LinuxCNC Python GUI 是界面和仿真结构参考,不是浏览器 runtime 依赖;Web 侧不直接运行 Tk/OpenGLTk、PyQt、GTK/Glade 或 native HAL GUI 进程。
|
||||
|
||||
## 2. 当前项目完成情况基线
|
||||
|
||||
当前 `wasm-port` 已经具备以下可继承能力:
|
||||
|
||||
- 浏览器真实仿真页面:`wasm-port/runtime/ui/simulation/index.html` 已实现 AXIS 风格界面、程序选择、编辑器、DRO、状态栏、Three.js 预览、播放控制和 browser smoke。
|
||||
- LinuxCNC 解释器 WASM:已有 `createLinuxCncInterpSdk()` 路径,可运行内置和用户输入的 G-code,并输出 LinuxCNC canonical 事件。
|
||||
- OPFS/session:已有 INI、参数文件、刀具表、G-code 程序、会话快照的浏览器侧持久化和加载工作流。
|
||||
- virtual HAL:已有仿真级 HAL pin/signal/param、halcmd 风格命令、motion feedback stepping 和浏览器状态展示。
|
||||
- 5 轴源码覆盖:已 vendored LinuxCNC `5axiskins.c`、`trtfuncs.c`、`xyzac-trt-kins.c`、`xyzbc-trt-kins.c`、`switchkins.*`、`userkfuncs.c` 等,且 source reuse 文档明确这些文件的验证边界。
|
||||
- 5 轴案例覆盖:已纳入 LinuxCNC `configs/sim/axis/vismach/5axis/bridgemill`、`table-dual-rotary`、`table-rotary-tilting` 的 INI/HAL/NGC/remap assets。
|
||||
- 当前 sim config inventory 基线:`executed=82`、`passed=82`、`skipped=77`、`unexpected_fail=0`;77 个 skipped row 已有实现覆盖账本,但不是全部 promotion。
|
||||
- 本地 LinuxCNC 源码中可参考 Python 图形界面:`src/emc/usr_intf/axis/scripts/axis.py`、`lib/python/vismach.py`、`configs/sim/axis/vismach/5axis/*`、`configs/sim/gmoccapy/*`、`configs/sim/qtvcp_screens/*`。
|
||||
|
||||
未完成或不能直接宣称完成的点:
|
||||
|
||||
- RTCP/TCP 五轴联动还没有作为独立 Web 产品能力完整闭环。
|
||||
- 当前浏览器页面主要是通用 AXIS 风格仿真,还不是专门的五轴/RTCP 操作界面。
|
||||
- TRT/bridge 五轴运动学虽然有 source coverage 和 probe 基础,但需要形成面向 UI 的统一五轴 pose/joint/TCP 数据模型。
|
||||
- Python remap、tool DB、external user-M process 等 hard block 仍不能伪装成浏览器 PASS。
|
||||
- Python GUI 的 native 控件、HAL component 进程、Tk/PyQt/GTK 主循环和 OpenGLTk 不能直接作为 Web 运行时使用,需要转换成 Web 组件、Three.js 场景和 virtual HAL 数据绑定。
|
||||
|
||||
## 3. LinuxCNC 参考案例选择
|
||||
|
||||
第一阶段建议聚焦 LinuxCNC 已有 5 轴 sample,不泛化到任意机型。
|
||||
|
||||
优先参考案例:
|
||||
|
||||
```text
|
||||
configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini
|
||||
configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini
|
||||
configs/sim/axis/vismach/5axis/bridgemill/5axis.ini
|
||||
configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini
|
||||
```
|
||||
|
||||
优先参考源码:
|
||||
|
||||
```text
|
||||
src/emc/kinematics/trtfuncs.c
|
||||
src/emc/kinematics/xyzac-trt-kins.c
|
||||
src/emc/kinematics/xyzbc-trt-kins.c
|
||||
src/emc/kinematics/5axiskins.c
|
||||
src/emc/kinematics/switchkins.c
|
||||
src/emc/kinematics/switchkins.h
|
||||
src/emc/kinematics/userkfuncs.c
|
||||
src/emc/kinematics/kins_util.c
|
||||
```
|
||||
|
||||
关键 LinuxCNC 机制:
|
||||
|
||||
- `M428`:切换到 TCP/RTCP 相关五轴运动学模式,示例中设置 `motion.switchkins-type=1`。
|
||||
- `M429`:恢复 identity/trivkins 模式,示例中设置 `motion.switchkins-type=0`。
|
||||
- `M430`:切换到 user kinematics,作为后续扩展,不作为第一阶段必做。
|
||||
- `HAL_PIN_VARS=1`:remap 子程序通过 `_hal[motion.switchkins-type]` 读取 HAL 状态。
|
||||
- `M68`/`M66`:设置 analog output 并同步 HAL 状态。
|
||||
- TRT 参数:`x-rot-point`、`y-rot-point`、`z-rot-point`、`x/y/z-offset`、`tool-offset`、`conventional-directions`。
|
||||
|
||||
## 4. Python 图形界面参考范围
|
||||
|
||||
LinuxCNC 的五轴仿真界面大量使用 Python 图形界面程序和 XML/HAL 配置组合。Web 项目应参考这些界面形态,但不把 Python GUI 作为浏览器依赖。
|
||||
|
||||
优先参考对象:
|
||||
|
||||
```text
|
||||
src/emc/usr_intf/axis/scripts/axis.py
|
||||
lib/python/vismach.py
|
||||
configs/sim/axis/vismach/5axis/bridgemill/5axis.xml
|
||||
configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.xml
|
||||
configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.xml
|
||||
configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.xml
|
||||
configs/sim/axis/vismach/5axis/*/*postgui.hal
|
||||
configs/sim/gmoccapy/gmoccapy_XYZAC.ini
|
||||
configs/sim/qtvcp_screens/qtdragon/*
|
||||
```
|
||||
|
||||
可借鉴的界面能力:
|
||||
|
||||
- AXIS 的菜单、工具栏、Manual/MDI、预览、DRO、G-code 高亮、状态栏。
|
||||
- vismach 的层级机床模型:`Translate`、`Rotate`、`HalTranslate`、`HalRotate`,通过 HAL pin 驱动几何体姿态。
|
||||
- PyVCP XML 的 `SWITCHKINS` 多状态标签、`TCP:XYZAC`/`IDENTITY`/`USERK` 按钮、joint 数值显示和 `vismach-clear` 操作。
|
||||
- gmoccapy 的操作员面板、jog increment、override、嵌入式右侧面板和多轴配置。
|
||||
- QtVCP/QtDragon 的现代面板布局、探测/刀具/状态区和大屏操作方式。
|
||||
- LinuxCNC 文档中已有实际界面截图,可先用 `docs/linuxcnc-gui-reference-gallery.md` 中的图册做界面选型。
|
||||
|
||||
Web 转换规则:
|
||||
|
||||
```text
|
||||
Python Tk/OpenGLTk/PyQt/GTK 控件 -> Web components
|
||||
vismach OpenGL scene graph -> Three.js scene graph
|
||||
HAL pin widget binding -> virtual HAL subscription/render binding
|
||||
PyVCP XML controls -> declarative Web panel schema
|
||||
AXIS/gmoccapy workflow -> browser operator workflow
|
||||
native GUI process -> no direct browser dependency
|
||||
```
|
||||
|
||||
前端实现规则:
|
||||
|
||||
```text
|
||||
HTML/CSS: 页面结构、布局、主题、响应式约束
|
||||
TypeScript/JavaScript ES modules: UI 控件、状态订阅、WASM/OPFS 调用、事件编排
|
||||
Three.js: 五轴机床和刀路 3D 预览
|
||||
Web Worker: LinuxCNC interpreter/kinematics WASM 计算隔离
|
||||
Vite/esbuild: 只做开发服务器、TypeScript 编译和模块打包
|
||||
```
|
||||
|
||||
禁止引入 React/Vue 等框架作为默认 UI 架构,避免把 CNC runtime 状态包进框架生命周期,增加调试和验证复杂度。
|
||||
|
||||
## 5. 总体架构
|
||||
|
||||
建议独立项目采用四层架构。
|
||||
|
||||
### 5.1 Web UI 层
|
||||
|
||||
职责:
|
||||
|
||||
- 五轴数控系统主界面;
|
||||
- 程序编辑/加载/保存;
|
||||
- 机床配置选择;
|
||||
- RTCP 开关状态、kins 类型、刀具长度、旋转中心、A/B/C 角度展示;
|
||||
- DRO、关节坐标、工件坐标、TCP 坐标、距离到达、运行状态;
|
||||
- 三维机床模型、刀具姿态、刀尖中心点轨迹、已执行轨迹与完整轨迹;
|
||||
- 报警、边界、未支持功能提示。
|
||||
|
||||
界面首选按 `gmoccapy_5_axis.png` 的工业操作台风格实现,并同时吸收 vismach/PyVCP/QtVCP 的界面结构。新项目界面要更偏向“五轴联动仿真工作台”:
|
||||
|
||||
```text
|
||||
顶部:gmoccapy 风格标题栏,显示 machine/profile/session/run state
|
||||
左侧:黑底 Three.js 五轴机床和 TCP 刀路预览
|
||||
右上:大号绿色 DRO,显示 X/Y/Z/A/B/C、TCP、RTCP、DTG
|
||||
中右:G-code 当前行列表和运行进度
|
||||
右侧:竖向模式按钮栏,参考 gmoccapy 大按钮
|
||||
中下:tool info / G-code properties / RTCP diagnostics tabs
|
||||
下方:velocity、rapid/feed override、coolant、spindle 面板
|
||||
底部:open、reload、run、stop、pause、step、home、fullscreen
|
||||
```
|
||||
|
||||
额外要求:
|
||||
|
||||
- 左侧或右侧保留 PyVCP 风格的 `SWITCHKINS` 面板,清楚显示 `IDENTITY`、`TCP:XYZAC`、`TCP:XYZBC`、`USERK`。
|
||||
- DRO 同时显示 axis pose、joint pose、TCP pose,不把关节坐标和工件坐标混在一起。
|
||||
- Three.js 机床模型按 vismach 层级构建,所有旋转/平移节点都能追溯到 HAL pin 或 kinematics frame。
|
||||
- 操作按钮参考 AXIS/gmoccapy 的 F1/F2/home/jog/run/step/stop/reset 逻辑,但只触发仿真 runtime。
|
||||
|
||||
### 5.2 Web Runtime/Session 层
|
||||
|
||||
职责:
|
||||
|
||||
- OPFS 会话管理;
|
||||
- INI、HAL、tool table、parameter、G-code 文件 staging;
|
||||
- 运行模式管理:standalone、machine-session、5axis-rtcp-session;
|
||||
- program run summary;
|
||||
- status/event history;
|
||||
- diagnostics artifact。
|
||||
|
||||
已有 `runtime/opfs` 能作为参考,但新目录后续应建立独立命名空间,避免直接把原页面做成越来越大的单文件。
|
||||
|
||||
### 5.3 LinuxCNC WASM/Source Boundary 层
|
||||
|
||||
职责:
|
||||
|
||||
- 调用 LinuxCNC 解释器 WASM;
|
||||
- 调用 LinuxCNC 五轴运动学 C/WASM ABI;
|
||||
- 提供 `forwardKinematics()`、`inverseKinematics()`、`switchKinsType()` 这类窄接口;
|
||||
- 捕获 canonical events、modal state、named parameters、tool offsets;
|
||||
- 输出 source-derived motion frames。
|
||||
|
||||
关键要求:
|
||||
|
||||
```text
|
||||
RTCP/五轴姿态计算必须优先由 vendored LinuxCNC kinematics 源码编译到 WASM 暴露;
|
||||
JavaScript 不直接复写 trtfuncs.c / 5axiskins.c 的公式作为最终语义源。
|
||||
```
|
||||
|
||||
### 5.4 Visualization/Playback 层
|
||||
|
||||
职责:
|
||||
|
||||
- 将 LinuxCNC 输出的 motion frame 映射为 Three.js 对象;
|
||||
- 插值播放已验证 motion events;
|
||||
- 显示 TCP 点、刀轴向量、刀具长度补偿、旋转中心、工作台/主轴姿态;
|
||||
- 区分 programmed path、joint path、TCP executed path;
|
||||
- 显示 RTCP on/off 对比。
|
||||
- 用 Three.js 重建 vismach 的机床树,不直接移植 Python OpenGLTk 绘制代码。
|
||||
- 支持显示 PyVCP/vismach 示例中的旋转中心、偏置点、真实旋转点、刀具长度和关节反馈。
|
||||
|
||||
注意:插值只用于显示,不能成为新的运动规划器。
|
||||
|
||||
## 6. RTCP 功能实现定义
|
||||
|
||||
本项目中的 RTCP 功能定义为仿真级 RTCP/TCP 能力:
|
||||
|
||||
- 读取或配置五轴机床类型:XYZAC、XYZBC、XYZBCW、XYZAB 等;
|
||||
- 能识别 `M428/M429` 或 UI 切换产生的 kinstype 状态;
|
||||
- 在 RTCP 模式下,以刀尖中心点为显示核心,展示旋转轴变化时 TCP 位置保持或按程序轨迹运动;
|
||||
- 同时展示关节坐标和笛卡尔/TCP 坐标;
|
||||
- 支持刀具长度、旋转中心、偏置参数改变后重新计算预览;
|
||||
- 以 LinuxCNC forward/inverse kinematics 结果作为验算基础;
|
||||
- 浏览器 smoke 能证明同一段 5 轴程序在 RTCP 模式下产生非空 A/B/C 姿态和 TCP 轨迹。
|
||||
|
||||
第一阶段不承诺:
|
||||
|
||||
- 真实伺服周期硬实时;
|
||||
- 完整 LinuxCNC task/motion 进程;
|
||||
- 任意 Python remap runtime;
|
||||
- 任意 external user-M process;
|
||||
- 完整工业级碰撞检测;
|
||||
- CAM 后处理器。
|
||||
|
||||
## 7. 数据模型
|
||||
|
||||
建议定义统一 frame:
|
||||
|
||||
```text
|
||||
FiveAxisMotionFrame
|
||||
sequence
|
||||
sourceLine
|
||||
time
|
||||
kinsType
|
||||
rtcpEnabled
|
||||
workPose: X/Y/Z/A/B/C/U/V/W
|
||||
jointPose: joint[0..N]
|
||||
tcpPose: x/y/z + toolAxisVector
|
||||
toolOffset
|
||||
pivot/rotPoint
|
||||
feed
|
||||
spindle
|
||||
canonicalEventRef
|
||||
diagnostics
|
||||
```
|
||||
|
||||
建议定义 machine profile:
|
||||
|
||||
```text
|
||||
FiveAxisMachineProfile
|
||||
id
|
||||
title
|
||||
family: xyzac-trt | xyzbc-trt | bridge-xyzbcw | tdr-xyzab
|
||||
iniPath
|
||||
coordinates
|
||||
joints
|
||||
kinematicsSource
|
||||
halPins
|
||||
remapCodes
|
||||
limits
|
||||
toolTable
|
||||
samplePrograms
|
||||
pythonGuiReferences
|
||||
pyvcpControls
|
||||
vismachModelNodes
|
||||
```
|
||||
|
||||
## 8. 阶段计划
|
||||
|
||||
### Phase 0:方案和边界文档
|
||||
|
||||
当前交付。
|
||||
|
||||
输出:
|
||||
|
||||
- 独立目录;
|
||||
- 实现方案;
|
||||
- 技术路线;
|
||||
- 明确当前完成情况和未完成边界。
|
||||
|
||||
### Phase 1:独立 Web 仿真骨架
|
||||
|
||||
目标:
|
||||
|
||||
- 新建独立 Web app;
|
||||
- 复用或引入现有 SDK/WASM 产物;
|
||||
- 页面能加载、选择五轴机床、显示 G-code、运行普通 LinuxCNC-backed 程序;
|
||||
- Three.js 显示一个基础五轴机床模型和非空刀路。
|
||||
- UI 布局参考 AXIS/gmoccapy,包含菜单、工具栏、Manual/MDI、预览、DRO、G-code、状态栏。
|
||||
|
||||
验收:
|
||||
|
||||
- 本地 dev server 可启动;
|
||||
- browser smoke 截图非空;
|
||||
- 运行现有线性/圆弧/G81 程序不回退。
|
||||
|
||||
### Phase 2:五轴 profile 和 5 轴 demo 接入
|
||||
|
||||
目标:
|
||||
|
||||
- 接入 `xyzac-trt`、`xyzbc-trt` 两个 profile;
|
||||
- staging 对应 INI/HAL/tool table/remap_subs/demo;
|
||||
- UI 能显示 kinematics 参数、HAL pins、M428/M429 状态;
|
||||
- 执行 LinuxCNC 5 轴 sample 或经过裁剪的 representative demo。
|
||||
- 解析 PyVCP XML/HAL 参考,生成 `SWITCHKINS`、joint value、offset/rot-point 等 Web 面板。
|
||||
|
||||
验收:
|
||||
|
||||
- 能展示 A/C 或 B/C 角度;
|
||||
- 能显示 kinstype 0/1 切换;
|
||||
- browser smoke 验证 `motion.switchkins-type` 状态。
|
||||
|
||||
### Phase 3:LinuxCNC 五轴运动学 WASM ABI
|
||||
|
||||
目标:
|
||||
|
||||
- 将 `trtfuncs.c`、`xyzac-trt-kins.c`、`xyzbc-trt-kins.c`、`5axiskins.c` 的 forward/inverse 调用通过窄 C ABI 暴露给 Web runtime;
|
||||
- 建立 HAL pin shim,为旋转中心、偏置、刀具长度提供输入;
|
||||
- 输出 joint/work/TCP frame。
|
||||
|
||||
验收:
|
||||
|
||||
- Node smoke:forward -> inverse roundtrip;
|
||||
- browser smoke:同一 frame 显示 joint pose、work pose、TCP pose;
|
||||
- source sync guard 仍通过。
|
||||
|
||||
### Phase 4:RTCP/TCP 预览与播放
|
||||
|
||||
目标:
|
||||
|
||||
- RTCP mode on/off 可视化;
|
||||
- 显示 programmed path、TCP path、joint path;
|
||||
- 刀轴方向随 A/B/C 变化;
|
||||
- 刀尖中心点轨迹与 LinuxCNC kinematics frame 对齐。
|
||||
- Three.js 机床层级与 vismach Python 示例的几何层级一致:工作台、转台、摆头、主轴、刀具和工件分别建模。
|
||||
|
||||
验收:
|
||||
|
||||
- RTCP 开启时,旋转轴变化不导致 UI 随机漂移;
|
||||
- 对 `M428/M429` 状态有明确显示;
|
||||
- 截图 smoke 校验 canvas 非空、刀具姿态非默认、路径点数非零。
|
||||
|
||||
### Phase 5:操作级数控系统仿真
|
||||
|
||||
目标:
|
||||
|
||||
- Manual/MDI、单段、连续、暂停、复位;
|
||||
- 程序行高亮、motion segment 高亮;
|
||||
- 坐标系、刀具长度、旋转中心参数编辑和重新仿真;
|
||||
- OPFS 保存/恢复完整 5 轴会话。
|
||||
|
||||
验收:
|
||||
|
||||
- 保存 -> 恢复 -> 重新运行结果一致;
|
||||
- UI 提供 operator-facing 状态,不只是 diagnostics dashboard。
|
||||
|
||||
### Phase 6:扩展验证和发布
|
||||
|
||||
目标:
|
||||
|
||||
- 建立 release gate;
|
||||
- 建立 regression fixtures;
|
||||
- 添加更多 LinuxCNC 5 轴案例;
|
||||
- 文档化不支持范围。
|
||||
|
||||
验收:
|
||||
|
||||
- `git diff --check`;
|
||||
- source/vendor sync;
|
||||
- Node/browser smoke;
|
||||
- screenshot/canvas-pixel check;
|
||||
- release readiness artifact。
|
||||
|
||||
## 9. 风险与处理
|
||||
|
||||
主要风险:
|
||||
|
||||
- 把 RTCP 公式写在 JS 中,破坏 LinuxCNC semantic boundary。
|
||||
- 5 轴 demo 依赖 remap/HAL 同步,直接跑全量程序可能遇到 runtime boundary。
|
||||
- Vismach Python GUI 不能直接迁移到浏览器。
|
||||
- 把 Python GUI 参考误解成要在浏览器中运行 Python/Tk/PyQt,会导致架构失控。
|
||||
- WebGL 3D 和 G-code 执行状态容易脱节。
|
||||
- 过早承诺完整工业 RTCP 会扩大范围。
|
||||
|
||||
处理策略:
|
||||
|
||||
- 第一阶段只做 source-derived kinematics ABI,不做 JS-owned CNC semantics。
|
||||
- demo 程序分 representative subset 和 full upstream demo 两级。
|
||||
- Vismach 只作为机床结构参考,浏览器用 Three.js 重建可视化,不移植 Python GUI。
|
||||
- PyVCP/gmoccapy/QtVCP 只作为 panel schema 和 operator workflow 参考,控件用 Web 原生实现。
|
||||
- 所有 UI frame 都要带 canonical/ref 或 kinematics/ref 追踪字段。
|
||||
- hard block 保持显式 blocked,不因为 UI 能显示就 promotion。
|
||||
|
||||
## 10. 结论
|
||||
|
||||
当前项目已经具备 Web 数控系统仿真的基础设施,但 RTCP 五轴联动还需要单独产品化。下一步应在独立目录中先搭建五轴 Web 仿真骨架,界面形态参考 LinuxCNC 的 AXIS、vismach、PyVCP、gmoccapy 和 QtVCP Python 图形界面,再把 LinuxCNC TRT/bridge 五轴运动学通过 WASM ABI 接入,最后形成 RTCP/TCP 轨迹、关节姿态、刀尖中心点和机床模型同步显示的浏览器仿真界面。
|
||||
226
web-rtcp-5axis-sim-plan/docs/linuxcnc-gui-reference-gallery.md
Normal file
@@ -0,0 +1,226 @@
|
||||
# LinuxCNC 原始界面参考图册
|
||||
|
||||
生成时间:2026-06-20 CST
|
||||
|
||||
## 1. 目的
|
||||
|
||||
本图册收集 LinuxCNC 源码树自带的实际界面截图,用于选择 Web 五轴数控系统仿真界面的视觉和布局方向。
|
||||
|
||||
图片来源:
|
||||
|
||||
```text
|
||||
linuxcnc/docs/src/gui/images/
|
||||
```
|
||||
|
||||
已复制到本项目:
|
||||
|
||||
```text
|
||||
web-rtcp-5axis-sim-plan/assets/reference/linuxcnc-gui/
|
||||
```
|
||||
|
||||
这些图片只作为界面参考,不代表 Web 项目会直接移植 LinuxCNC 原生 GUI runtime。
|
||||
|
||||
## 2. 优先参考图
|
||||
|
||||
### 2.1 QtVCP Vismach 5 Axis Gantry
|
||||
|
||||

|
||||
|
||||
原始文件:
|
||||
|
||||
```text
|
||||
linuxcnc/docs/src/gui/images/qtvismach_5axis_gantry.png
|
||||
```
|
||||
|
||||
推荐用途:
|
||||
|
||||
- 五轴机床模型参考优先级最高。
|
||||
- 可参考其 3D 机床视图、动态轴显示和仿真模型表达。
|
||||
- 适合作为 Web 版 Three.js 五轴机床主视口的方向。
|
||||
|
||||
建议选择程度:高。
|
||||
|
||||
### 2.2 gmoccapy 5 Axis
|
||||
|
||||

|
||||
|
||||
原始文件:
|
||||
|
||||
```text
|
||||
linuxcnc/docs/src/gui/images/gmoccapy_5_axis.png
|
||||
```
|
||||
|
||||
推荐用途:
|
||||
|
||||
- 操作员界面参考。
|
||||
- 大按钮、DRO、运行控制、状态区、手动操作面板可以借鉴。
|
||||
- 适合触控屏或工业操作台风格。
|
||||
|
||||
建议选择程度:高。
|
||||
|
||||
### 2.3 AXIS
|
||||
|
||||

|
||||
|
||||
原始文件:
|
||||
|
||||
```text
|
||||
linuxcnc/docs/src/gui/images/axis.png
|
||||
```
|
||||
|
||||
推荐用途:
|
||||
|
||||
- 经典 LinuxCNC 操作布局参考。
|
||||
- 菜单、工具栏、G-code、预览、DRO、状态栏结构清晰。
|
||||
- 适合作为第一版 Web 五轴仿真工作台的主布局骨架。
|
||||
|
||||
建议选择程度:高。
|
||||
|
||||
### 2.4 AXIS + PyVCP
|
||||
|
||||

|
||||
|
||||
原始文件:
|
||||
|
||||
```text
|
||||
linuxcnc/docs/src/gui/images/axis-pyvcp.png
|
||||
```
|
||||
|
||||
推荐用途:
|
||||
|
||||
- 右侧 PyVCP 面板参考。
|
||||
- `SWITCHKINS`、joint value、offset/rot-point、RTCP 状态面板可以采用类似侧栏。
|
||||
- 适合把五轴专用控件放在主预览旁边。
|
||||
|
||||
建议选择程度:高。
|
||||
|
||||
## 3. 现代操作屏参考
|
||||
|
||||
### 3.1 QtDragon
|
||||
|
||||

|
||||
|
||||
原始文件:
|
||||
|
||||
```text
|
||||
linuxcnc/docs/src/gui/images/qtdragon.png
|
||||
```
|
||||
|
||||
推荐用途:
|
||||
|
||||
- 现代化大屏 CNC 操作界面参考。
|
||||
- 面板区、状态区、按钮区和探测/刀具相关布局可借鉴。
|
||||
- 如果目标是更接近工业触控屏,可参考此方向。
|
||||
|
||||
建议选择程度:中高。
|
||||
|
||||
### 3.2 QtDragon HD
|
||||
|
||||

|
||||
|
||||
原始文件:
|
||||
|
||||
```text
|
||||
linuxcnc/docs/src/gui/images/qtdragon_hd.png
|
||||
```
|
||||
|
||||
推荐用途:
|
||||
|
||||
- 大屏布局参考。
|
||||
- 适合后续做宽屏 Web 版操作台。
|
||||
|
||||
建议选择程度:中。
|
||||
|
||||
## 4. Vismach 机床仿真参考
|
||||
|
||||
### 4.1 Vismach
|
||||
|
||||

|
||||
|
||||
原始文件:
|
||||
|
||||
```text
|
||||
linuxcnc/docs/src/gui/images/vismach.png
|
||||
```
|
||||
|
||||
推荐用途:
|
||||
|
||||
- Python `vismach.py` 原始机床仿真窗口参考。
|
||||
- 可参考机床模型、坐标轴、视角控制和独立仿真窗口表达。
|
||||
|
||||
建议选择程度:中。
|
||||
|
||||
### 4.2 QtVismach
|
||||
|
||||

|
||||
|
||||
原始文件:
|
||||
|
||||
```text
|
||||
linuxcnc/docs/src/gui/images/qtvismach.png
|
||||
```
|
||||
|
||||
推荐用途:
|
||||
|
||||
- QtVCP 嵌入式机床仿真面板参考。
|
||||
- 适合思考 Three.js 视口如何嵌入主操作界面。
|
||||
|
||||
建议选择程度:中。
|
||||
|
||||
## 5. 推荐界面方案
|
||||
|
||||
用户已选择 `gmoccapy_5_axis.png` 作为目标界面风格。建议第一版 Web 五轴 RTCP 仿真界面以 gmoccapy 5 轴界面为主风格,再吸收 QtVismach 5 Axis Gantry 的机床模型表达和 PyVCP 的五轴状态控件。
|
||||
|
||||
```text
|
||||
主风格:gmoccapy_5_axis.png
|
||||
3D 机床视口:QtVCP Vismach 5 Axis Gantry
|
||||
五轴专用状态:PyVCP SWITCHKINS / joint values
|
||||
经典布局参考:AXIS
|
||||
后续大屏扩展:QtDragon / QtDragon HD
|
||||
```
|
||||
|
||||
具体布局建议:
|
||||
|
||||
```text
|
||||
顶部:gmoccapy 风格标题栏 + machine/session/run state
|
||||
左侧:黑底 Three.js 五轴机床和刀路预览
|
||||
右上:大号绿色 DRO,显示 X/Y/Z/A/B/C、TCP、RTCP、DTG
|
||||
中右:G-code 当前行列表和运行进度
|
||||
右侧:竖向模式按钮栏,参考 gmoccapy 大按钮
|
||||
中下:tool info / G-code properties / RTCP diagnostics tabs
|
||||
下方:velocity、rapid/feed override、coolant、spindle 面板
|
||||
底部:open、reload、run、stop、pause、step、home、fullscreen
|
||||
```
|
||||
|
||||
## 6. 图片清单
|
||||
|
||||
```text
|
||||
assets/reference/linuxcnc-gui/qtvismach_5axis_gantry.png
|
||||
assets/reference/linuxcnc-gui/gmoccapy_5_axis.png
|
||||
assets/reference/linuxcnc-gui/axis.png
|
||||
assets/reference/linuxcnc-gui/axis-pyvcp.png
|
||||
assets/reference/linuxcnc-gui/qtdragon.png
|
||||
assets/reference/linuxcnc-gui/qtdragon_hd.png
|
||||
assets/reference/linuxcnc-gui/vismach.png
|
||||
assets/reference/linuxcnc-gui/qtvismach.png
|
||||
```
|
||||
|
||||
## 7. 选择建议
|
||||
|
||||
当前选择:
|
||||
|
||||
```text
|
||||
gmoccapy 5 Axis + QtVismach 5 Axis Gantry + PyVCP SWITCHKINS
|
||||
```
|
||||
|
||||
如果后续需要更经典 LinuxCNC 桌面风格,可退回:
|
||||
|
||||
```text
|
||||
AXIS + PyVCP + QtVismach 5 Axis Gantry
|
||||
```
|
||||
|
||||
如果后续需要更现代大屏触控风格,可扩展:
|
||||
|
||||
```text
|
||||
gmoccapy + QtDragon HD
|
||||
```
|
||||
194
web-rtcp-5axis-sim-plan/docs/linuxcnc-python-gui-reference.md
Normal file
@@ -0,0 +1,194 @@
|
||||
# LinuxCNC Python 图形界面参考
|
||||
|
||||
生成时间:2026-06-20 CST
|
||||
|
||||
## 1. 目的
|
||||
|
||||
本文件补充说明:五轴数控系统仿真界面的设计不仅参考 LinuxCNC 的 C/C++ 解释器、运动学和配置案例,也参考 LinuxCNC 使用 Python 编写的图形界面和机床仿真程序。
|
||||
|
||||
这些 Python GUI 是 Web 界面的产品和结构参考,不是 Web 运行时依赖。浏览器版本不直接运行 Tk、OpenGLTk、PyQt、GTK/Glade、native HAL component 或 Python GUI process。
|
||||
|
||||
## 2. 参考对象
|
||||
|
||||
### AXIS
|
||||
|
||||
参考文件:
|
||||
|
||||
```text
|
||||
src/emc/usr_intf/axis/scripts/axis.py
|
||||
share/axis/*
|
||||
configs/sim/axis/*
|
||||
```
|
||||
|
||||
参考内容:
|
||||
|
||||
- 菜单栏、工具栏、运行/暂停/单段/停止/复位。
|
||||
- Manual、MDI、Preview、DRO、G-code 文本区。
|
||||
- G-code 当前行高亮。
|
||||
- OpenGL 刀路预览和视图控制。
|
||||
- E-stop、machine on、homing、坐标模式、状态栏。
|
||||
|
||||
Web 对应实现:
|
||||
|
||||
```text
|
||||
AXIS main window -> 单页 Web operator workspace
|
||||
AXIS toolbar -> Web icon toolbar
|
||||
AXIS preview -> Three.js viewport
|
||||
AXIS DRO -> Web DRO panel
|
||||
AXIS G-code list -> Web editor/source pane
|
||||
```
|
||||
|
||||
### vismach
|
||||
|
||||
参考文件:
|
||||
|
||||
```text
|
||||
lib/python/vismach.py
|
||||
configs/sim/axis/vismach/5axis/bridgemill/*
|
||||
configs/sim/axis/vismach/5axis/table-dual-rotary/*
|
||||
configs/sim/axis/vismach/5axis/table-rotary-tilting/*
|
||||
configs/sim/axis/vismach/5axis/table-rotary_spindle-rotary-nutating/vismach/*.py
|
||||
```
|
||||
|
||||
参考内容:
|
||||
|
||||
- 机床几何树。
|
||||
- `Translate`、`Rotate` 静态变换。
|
||||
- `HalTranslate`、`HalRotate` 基于 HAL pin 的动态变换。
|
||||
- 工作台、转台、摆头、主轴、刀具、工件的层级关系。
|
||||
- `vismach-clear` 清空轨迹。
|
||||
- STL/几何体组合机床模型。
|
||||
|
||||
Web 对应实现:
|
||||
|
||||
```text
|
||||
vismach Collection -> Three.js Group
|
||||
vismach Translate/Rotate -> Three.js transform node
|
||||
vismach HalTranslate/HalRotate -> virtual HAL-bound transform node
|
||||
vismach component pins -> profile-declared observable values
|
||||
vismach OpenGLTk scene -> Three.js scene
|
||||
```
|
||||
|
||||
### PyVCP
|
||||
|
||||
参考文件:
|
||||
|
||||
```text
|
||||
configs/sim/axis/vismach/5axis/bridgemill/5axis.xml
|
||||
configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.xml
|
||||
configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.xml
|
||||
configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.xml
|
||||
configs/sim/axis/vismach/5axis/*/*postgui.hal
|
||||
```
|
||||
|
||||
参考内容:
|
||||
|
||||
- `SWITCHKINS` multilabel。
|
||||
- `IDENTITY`、`TCP:XYZAC`、`TCP:XYZBC`、`USERK` 按钮。
|
||||
- joint 数值显示。
|
||||
- offset / rot-point 参数显示或调节。
|
||||
- HAL pin 到 GUI 控件的绑定。
|
||||
|
||||
Web 对应实现:
|
||||
|
||||
```text
|
||||
PyVCP XML -> Web panel schema
|
||||
halpin -> virtual HAL binding
|
||||
multilabel -> segmented/status indicator
|
||||
button -> command/action button
|
||||
number -> read-only numeric DRO field
|
||||
```
|
||||
|
||||
### gmoccapy
|
||||
|
||||
参考文件:
|
||||
|
||||
```text
|
||||
configs/sim/gmoccapy/gmoccapy_XYZAC.ini
|
||||
configs/sim/gmoccapy/gmoccapy_*.ini
|
||||
configs/sim/gmoccapy/*.glade
|
||||
configs/sim/gmoccapy/*postgui.hal
|
||||
```
|
||||
|
||||
参考内容:
|
||||
|
||||
- 面向操作员的大按钮布局。
|
||||
- jog increment、feed override、spindle override、rapid override。
|
||||
- 右侧嵌入面板。
|
||||
- 多轴配置和状态显示。
|
||||
|
||||
Web 对应实现:
|
||||
|
||||
```text
|
||||
gmoccapy operator panel -> compact touch-friendly control panel
|
||||
gladevcp embedded tab -> Web side panel
|
||||
override controls -> sliders/steppers
|
||||
jog increment -> segmented controls
|
||||
```
|
||||
|
||||
### QtVCP / QtDragon
|
||||
|
||||
参考文件:
|
||||
|
||||
```text
|
||||
configs/sim/qtvcp_screens/*
|
||||
configs/sim/qtvcp_screens/qtdragon/*
|
||||
share/qtvcp/*
|
||||
```
|
||||
|
||||
参考内容:
|
||||
|
||||
- 现代化 CNC 操作屏布局。
|
||||
- 状态区、工具区、探测区、程序区。
|
||||
- 大屏/触控操作方式。
|
||||
- panel handler 和 widget 分层。
|
||||
|
||||
Web 对应实现:
|
||||
|
||||
```text
|
||||
QtVCP screen -> responsive Web layout
|
||||
QtVCP widgets -> reusable Web components
|
||||
handler state -> runtime store/actions
|
||||
```
|
||||
|
||||
## 3. 五轴界面重点参考项
|
||||
|
||||
第一阶段必须吸收这些界面元素:
|
||||
|
||||
- `SWITCHKINS` 状态:显示当前 kinstype。
|
||||
- `M428/M429/M430` 操作:切换 TCP/identity/userk 模式。
|
||||
- joint values:至少显示 J0-J6 或当前 profile joints。
|
||||
- axis pose:显示 X/Y/Z/A/B/C/W。
|
||||
- TCP pose:显示刀尖中心点和刀轴方向。
|
||||
- offset/rot-point:显示旋转点和几何偏置。
|
||||
- tool offset:显示刀具长度补偿。
|
||||
- preview clear:清空已执行轨迹。
|
||||
- status bar:显示 E-stop、machine on、session、run mode、RTCP state。
|
||||
|
||||
## 4. 不移植内容
|
||||
|
||||
以下内容不直接移植到 Web runtime:
|
||||
|
||||
- Tkinter 主循环。
|
||||
- `rs274.OpenGLTk`。
|
||||
- PyQt/QTVCP native widget。
|
||||
- GTK/Glade native UI。
|
||||
- native `hal.component()` 进程。
|
||||
- LinuxCNC GUI 与 task/motion 的 native IPC。
|
||||
- Python remap runtime。
|
||||
|
||||
如果后续需要 Python runtime,只能作为单独受控 milestone,不得混入 UI 参考转换。
|
||||
|
||||
## 5. 验收方式
|
||||
|
||||
文档和实现需要用机器可验证方式证明参考关系:
|
||||
|
||||
- profile 中记录 Python GUI source references。
|
||||
- panel schema 中记录 PyVCP XML 和 postgui HAL source path。
|
||||
- browser smoke 检查 `SWITCHKINS`、joint values、DRO、preview、statusbar 存在。
|
||||
- canvas smoke 检查 Three.js 机床模型非空。
|
||||
- state smoke 检查 `M428/M429` 或 kinstype 切换能反映到 UI。
|
||||
|
||||
## 6. 结论
|
||||
|
||||
Web 五轴仿真界面应以 LinuxCNC Python GUI 为交互和可视化参考,以 LinuxCNC C/C++ 源码为 CNC 语义和运动学参考。Python GUI 提供“界面长什么样、机床模型如何组织、HAL 控件如何连接”的依据;浏览器实现负责把这些参考转换为 Web 组件、Three.js 场景、virtual HAL 绑定和 WASM 调用链。
|
||||
445
web-rtcp-5axis-sim-plan/docs/program-implementation-guide.md
Normal file
@@ -0,0 +1,445 @@
|
||||
# 5 轴数控系统 Web 仿真程序具体实施文档
|
||||
|
||||
生成时间:2026-06-20 CST
|
||||
|
||||
## 1. 实施目标
|
||||
|
||||
本文件用于指导后续正式编写 5 轴数控系统 Web 仿真程序。第一版目标是做出一个可运行、可验证、可继续扩展的浏览器前端:
|
||||
|
||||
- 界面风格按 `gmoccapy_5_axis.png` 实现;
|
||||
- 前端使用原生 HTML/CSS + TypeScript/JavaScript ES modules;
|
||||
- 3D 预览使用 Three.js;
|
||||
- G-code 执行、五轴运动学、RTCP/TCP 相关计算必须来自 LinuxCNC/WASM 或 source-derived 边界;
|
||||
- 不引入 React/Vue/Angular/Svelte;
|
||||
- 不把 Python GUI、GTK/Glade、Tk/OpenGLTk、native HAL process 直接作为浏览器 runtime。
|
||||
|
||||
## 2. 第一版完成定义
|
||||
|
||||
第一版完成时应具备:
|
||||
|
||||
- 一个可启动的 Web app;
|
||||
- gmoccapy 风格布局:黑底 3D 预览、大号绿色 DRO、G-code 列表、右侧模式按钮、override/spindle/coolant 区、底部运行控制;
|
||||
- 至少一个 5 轴 profile:优先 `xyzac-trt`;
|
||||
- 能加载 representative G-code;
|
||||
- 能显示 X/Y/Z/A/B/C、joint pose、TCP pose、RTCP state、kins type;
|
||||
- Three.js 视口非空,能显示机床、刀具、刀路;
|
||||
- browser smoke 能截图、检查 canvas 非空、检查关键 DOM 区域存在;
|
||||
- 文档明确 LinuxCNC source references 和 unsupported runtime boundary。
|
||||
|
||||
## 3. 推荐目录结构
|
||||
|
||||
后续直接在本目录中扩展:
|
||||
|
||||
```text
|
||||
web-rtcp-5axis-sim-plan/
|
||||
app/
|
||||
index.html
|
||||
package.json
|
||||
tsconfig.json
|
||||
src/
|
||||
main.ts
|
||||
state/
|
||||
store.ts
|
||||
events.ts
|
||||
ui/
|
||||
gmoccapy-shell.ts
|
||||
gmoccapy-dro-panel.ts
|
||||
gmoccapy-gcode-panel.ts
|
||||
gmoccapy-status-sidebar.ts
|
||||
gmoccapy-override-panel.ts
|
||||
gmoccapy-spindle-coolant-panel.ts
|
||||
gmoccapy-bottom-controls.ts
|
||||
gmoccapy-info-tabs.ts
|
||||
visualization/
|
||||
five-axis-scene.ts
|
||||
machine-model.ts
|
||||
toolpath-layer.ts
|
||||
camera-controls.ts
|
||||
runtime/
|
||||
simulation-runtime.ts
|
||||
frame-builder.ts
|
||||
playback-controller.ts
|
||||
linuxcnc-adapter.ts
|
||||
profiles/
|
||||
index.ts
|
||||
xyzac-trt.ts
|
||||
xyzbc-trt.ts
|
||||
panel-schema/
|
||||
controls.ts
|
||||
pyvcp-reference.ts
|
||||
workers/
|
||||
linuxcnc-worker.ts
|
||||
styles/
|
||||
gmoccapy.css
|
||||
core/
|
||||
linuxcnc_kinematics_wasm/
|
||||
tests/
|
||||
browser/
|
||||
node/
|
||||
```
|
||||
|
||||
## 4. 实施顺序
|
||||
|
||||
### Step 1:Web shell
|
||||
|
||||
目标:
|
||||
|
||||
- 创建 `app/index.html`;
|
||||
- 创建 CSS layout;
|
||||
- 创建 `gmoccapy-shell.ts`;
|
||||
- 页面静态呈现 gmoccapy 风格区域。
|
||||
|
||||
必须有的 DOM 区域:
|
||||
|
||||
```text
|
||||
data-region="titlebar"
|
||||
data-region="preview"
|
||||
data-region="dro"
|
||||
data-region="gcode"
|
||||
data-region="status-sidebar"
|
||||
data-region="info-tabs"
|
||||
data-region="override"
|
||||
data-region="spindle-coolant"
|
||||
data-region="bottom-controls"
|
||||
```
|
||||
|
||||
验收:
|
||||
|
||||
- 浏览器打开页面非空;
|
||||
- 页面区域与 `gmoccapy_5_axis.png` 基本一致;
|
||||
- 无 React/Vue 依赖。
|
||||
|
||||
### Step 2:状态模型
|
||||
|
||||
目标:
|
||||
|
||||
- 实现 `GmoccapySimulationState`;
|
||||
- 实现 `createStore()`、`getState()`、`subscribe()`、`dispatch()`;
|
||||
- UI 面板从 state 渲染,不直接互相读写 DOM。
|
||||
|
||||
初始 state:
|
||||
|
||||
```text
|
||||
machineProfile=xyzac-trt
|
||||
runState=idle
|
||||
rtcpState=off
|
||||
kinsType=identity
|
||||
axisPose={X,Y,Z,A,B,C}
|
||||
jointPose=[]
|
||||
tcpPose={x,y,z,toolAxisVector}
|
||||
```
|
||||
|
||||
验收:
|
||||
|
||||
- Node smoke 验证 store 更新;
|
||||
- DOM renderer 能响应 state 变化。
|
||||
|
||||
当前 M2 已实现:
|
||||
|
||||
```text
|
||||
app/src/runtime/rtcp-frame.js
|
||||
app/src/profiles/xyzac-trt.js
|
||||
tests/node/verify_rtcp_store.mjs
|
||||
```
|
||||
|
||||
状态模型已经输出:
|
||||
|
||||
```text
|
||||
axisPose
|
||||
jointPose
|
||||
tcpPose
|
||||
toolAxisVector
|
||||
rtcpFrame
|
||||
feed
|
||||
spindle
|
||||
coolant
|
||||
preview
|
||||
operatorMessage
|
||||
```
|
||||
|
||||
当前 RTCP frame 已支持双来源:
|
||||
|
||||
```text
|
||||
apiName=web-rtcp-5axis-motion-frame
|
||||
fixture fallback:
|
||||
sourceMode=fixture-ui-only
|
||||
semanticBoundary=fixture_frame_ui_plumbing_not_linuxcnc_kinematics_proof
|
||||
linuxCncKinematicsReady=false
|
||||
promotionAllowed=false
|
||||
LinuxCNC kinematics proof:
|
||||
sourceMode=source-derived-kinematics-wasm
|
||||
semanticBoundary=linuxcnc_kinematics_wasm_c_abi
|
||||
linuxCncKinematicsReady=true
|
||||
promotionAllowed=true for kinematics frame source only
|
||||
```
|
||||
|
||||
这表示 Web 仿真已经具备 RTCP 状态链路、TCP pose 显示、刀轴向量显示和控制按钮切换;Node proof 路径已通过 `createLinuxCncKinematicsSdk({ moduleId: "xyzac-trt" })` 加载 LinuxCNC kinematics WASM 并生成 frame。浏览器 smoke 仍保留 fixture fallback,不把 fallback 冒充 LinuxCNC runtime proof。
|
||||
|
||||
### Step 3:gmoccapy UI 组件
|
||||
|
||||
目标:
|
||||
|
||||
- DRO;
|
||||
- G-code panel;
|
||||
- right status sidebar;
|
||||
- override panel;
|
||||
- spindle/coolant panel;
|
||||
- bottom controls;
|
||||
- info tabs。
|
||||
|
||||
要求:
|
||||
|
||||
- 按 gmoccapy 风格做大按钮、大数字、黑底预览、灰色面板;
|
||||
- 所有按钮先连接仿真 action,不连接真实机床控制;
|
||||
- 文本不能溢出按钮或面板。
|
||||
|
||||
验收:
|
||||
|
||||
- browser smoke 检查关键按钮、DRO、G-code rows;
|
||||
- `Run/Stop/Pause/Step` 能改变仿真 state。
|
||||
|
||||
当前 M2 已接入的 UI action:
|
||||
|
||||
```text
|
||||
RUN
|
||||
STOP
|
||||
PAUSE
|
||||
STEP
|
||||
SET_KINS_TYPE
|
||||
SET_VIEW
|
||||
RESET_VIEW
|
||||
CLEAR_PREVIEW
|
||||
ADJUST_OVERRIDE
|
||||
ADJUST_SPINDLE_OVERRIDE
|
||||
TOGGLE_COOLANT
|
||||
RELOAD_PROGRAM
|
||||
HOME
|
||||
TOGGLE_FULLSCREEN
|
||||
```
|
||||
|
||||
这些 action 只改变 Web 仿真状态,不连接真实机床控制。
|
||||
|
||||
当前 M5 已补齐 operator workflow:
|
||||
|
||||
```text
|
||||
TOGGLE_POWER
|
||||
ESTOP
|
||||
RESET
|
||||
SET_MODE(auto/manual/jog/mdi)
|
||||
JOG
|
||||
RUN_MDI
|
||||
LOAD_PROGRAM
|
||||
```
|
||||
|
||||
实现状态:
|
||||
|
||||
- 右侧按钮栏提供 POWER、E-STOP、RESET、AUTO、MANUAL、JOG、MDI;
|
||||
- 底部控制栏提供 Open、Run/Stop/Pause/Step/Home、JOG X/Y、MDI;
|
||||
- Open 使用浏览器 FileReader 读取本地 G-code 文本并进入 `LOAD_PROGRAM`;
|
||||
- G-code 面板显示当前程序来源、当前执行行和高亮行;
|
||||
- preview 区域显示刀具预览卡;
|
||||
- RUN/STEP 仍是 fixture line playback,不是 LinuxCNC interpreter execution proof。
|
||||
|
||||
### Step 4:Three.js 五轴预览
|
||||
|
||||
目标:
|
||||
|
||||
- 创建基础五轴机床模型;
|
||||
- 显示坐标轴、工作空间、刀具、TCP 点、刀路;
|
||||
- 支持 fit/reset/clear path。
|
||||
|
||||
模型优先参考:
|
||||
|
||||
```text
|
||||
qtvismach_5axis_gantry.png
|
||||
lib/python/vismach.py
|
||||
src/hal/user_comps/vismach/5axisgui.py
|
||||
src/hal/user_comps/vismach/xyzac-trt-gui.py
|
||||
src/hal/user_comps/vismach/xyzbc-trt-gui.py
|
||||
```
|
||||
|
||||
验收:
|
||||
|
||||
- canvas 非空;
|
||||
- tool marker 可见;
|
||||
- path points 非零;
|
||||
- 视口尺寸变化不破坏布局。
|
||||
|
||||
当前 M3 已实现:
|
||||
|
||||
```text
|
||||
app/src/vendor/three/three.module.js
|
||||
app/src/vendor/three/three.core.js
|
||||
app/src/visualization/five-axis-scene.js
|
||||
```
|
||||
|
||||
当前 Three.js 预览会渲染基础五轴工作区、工作台、刀具/TCP marker、刀轴和刀路,并消费:
|
||||
|
||||
```text
|
||||
tcpPose
|
||||
toolAxisVector
|
||||
rtcpState
|
||||
rtcpFrame.apiName
|
||||
preview.selectedView
|
||||
```
|
||||
|
||||
browser smoke 已检查 canvas nonblank、scene objects、path points、RTCP on/off 同步和 STEP 后 TCP pose 更新。
|
||||
|
||||
### Step 5:profile 和 panel schema
|
||||
|
||||
目标:
|
||||
|
||||
- 建立 `xyzac-trt` profile;
|
||||
- 后续补 `xyzbc-trt`;
|
||||
- 把 PyVCP XML 和 HAL 绑定整理成 Web panel schema。
|
||||
|
||||
`xyzac-trt` 必须记录:
|
||||
|
||||
```text
|
||||
iniPath
|
||||
coordinates=XYZAC
|
||||
kinematics=xyzac-trt-kins
|
||||
remap=M428/M429/M430
|
||||
halPins=motion.switchkins-type, xyzac-trt-kins.tool-offset, y-offset, z-offset
|
||||
samplePrograms
|
||||
sourceReferences
|
||||
```
|
||||
|
||||
验收:
|
||||
|
||||
- Node smoke 验证 profile 完整;
|
||||
- UI 能显示 profile title、coordinates、kins type、source references。
|
||||
|
||||
当前 M4 已实现:
|
||||
|
||||
```text
|
||||
app/src/profiles/xyzac-trt.js
|
||||
app/src/profiles/source-reference-map.js
|
||||
app/src/panel-schema/xyzac-trt-pyvcp.js
|
||||
tests/node/verify_profile_boundary.mjs
|
||||
```
|
||||
|
||||
`xyzac-trt` profile 现在记录:
|
||||
|
||||
```text
|
||||
iniPath
|
||||
pyvcpXmlPath
|
||||
postguiHalPath
|
||||
generatedHalPath
|
||||
toolTablePath
|
||||
coordinates=XYZAC
|
||||
kinematics=xyzac-trt-kins
|
||||
sparm=identityfirst
|
||||
remaps=M428/M429/M430
|
||||
switchkinsTypes=identity/TCP:XYZAC/USERK
|
||||
halPins
|
||||
offsets
|
||||
samplePrograms
|
||||
sourceReferences
|
||||
```
|
||||
|
||||
`xyzac-trt-switchkins-pyvcp` panel schema 记录 SWITCHKINS multilabel、IDENTITY/TCP:XYZAC/USERK/vismach-clear buttons、HAL nets 和对应 MDI commands。
|
||||
|
||||
### Step 6:LinuxCNC adapter
|
||||
|
||||
目标:
|
||||
|
||||
- 第一阶段可接现有 interpreter WASM SDK;
|
||||
- 若未接入完整 WASM,则先用明确标记的 fixture frame 验证 UI,不声称 CNC semantics pass;
|
||||
- 所有 runtime result 必须带 `sourceMode` 字段。
|
||||
|
||||
允许:
|
||||
|
||||
```text
|
||||
sourceMode=linuxcnc-wasm
|
||||
sourceMode=source-derived-kinematics-wasm
|
||||
sourceMode=fixture-ui-only
|
||||
```
|
||||
|
||||
禁止:
|
||||
|
||||
```text
|
||||
sourceMode=js-cnc-semantics
|
||||
```
|
||||
|
||||
验收:
|
||||
|
||||
- UI-only fixture 不能被标记为 LinuxCNC pass;
|
||||
- LinuxCNC/WASM 接入后更新 traceability。
|
||||
|
||||
当前 M4 已实现 adapter 接入点:
|
||||
|
||||
```text
|
||||
app/src/runtime/linuxcnc-boundary-adapter.js
|
||||
apiName=web-rtcp-5axis-linuxcnc-boundary-adapter
|
||||
readinessApi=web-rtcp-5axis-linuxcnc-boundary-readiness
|
||||
semanticBoundary=adapter_entrypoint_only_runtime_not_connected | linuxcnc_kinematics_wasm_runtime_connected
|
||||
linuxCncKinematicsReady=false for fixture fallback, true for loaded kinematics WASM
|
||||
promotionAllowed=true only for kinematics frame source proof
|
||||
fullLinuxCncProgramExecutionReady=false until interpreter/remap is connected
|
||||
```
|
||||
|
||||
store 已输出:
|
||||
|
||||
```text
|
||||
linuxCncBoundaryAdapter
|
||||
linuxCncBoundaryReadiness
|
||||
```
|
||||
|
||||
info tabs 和 browser smoke 会检查 adapter、panel schema、source map 和 boundary readiness。Node smoke 已验证 kinematics-only runtime ready;interpreter/remap 仍显示为 missing,不得声明 full LinuxCNC program execution ready。
|
||||
|
||||
### Step 7:RTCP/kinematics frame
|
||||
|
||||
目标:
|
||||
|
||||
- 定义 `FiveAxisMotionFrame`;
|
||||
- 将 canonical event 和 kinematics output 转成统一 frame;
|
||||
- 显示 RTCP on/off、TCP pose、tool axis vector。
|
||||
|
||||
M2 阶段先落地 `web-rtcp-5axis-motion-frame` 的 fixture contract,M6 阶段已加入 LinuxCNC kinematics WASM frame contract:
|
||||
|
||||
```text
|
||||
profileId
|
||||
sourceMode
|
||||
semanticBoundary
|
||||
activeLine
|
||||
kinsType
|
||||
rtcpState
|
||||
axisPose
|
||||
jointPose
|
||||
tcpPose
|
||||
toolAxisVector
|
||||
compensation
|
||||
readiness
|
||||
```
|
||||
|
||||
后续 LinuxCNC/source-derived kinematics WASM 接入时,必须替换 frame builder 的运动学来源,并把 `sourceMode` 从 `fixture-ui-only` 改为明确的 LinuxCNC/WASM 边界值。
|
||||
当前 Node proof 已使用 `linuxCncKinematicsResult.forward.pose` 和 `linuxCncKinematicsResult.inverse.joints` 填充 `tcpPose` / `jointPose`,并输出 `kinematicsModuleId`、forward/inverse rc、flags 和 `linuxcnc_kinematics_wasm_c_abi`。
|
||||
|
||||
验收:
|
||||
|
||||
- Node smoke:frame schema;
|
||||
- Browser smoke:DRO 和 Three.js 同步显示同一 frame;
|
||||
- fixture fallback 必须显示 pending;Node LinuxCNC kinematics proof 必须显示 ready;interpreter/remap 未接入时仍不得显示 full program execution ready。
|
||||
|
||||
### Step 8:测试和验收
|
||||
|
||||
至少需要:
|
||||
|
||||
- `git diff --check`;
|
||||
- Node profile/store/frame smoke;
|
||||
- Browser shell smoke;
|
||||
- Browser canvas nonblank smoke;
|
||||
- 文档 traceability 检查。
|
||||
|
||||
## 5. 禁止事项
|
||||
|
||||
- 不在 JavaScript 中实现 G-code 解释器。
|
||||
- 不在 JavaScript 中实现 LinuxCNC 五轴运动学公式作为最终语义源。
|
||||
- 不把 gmoccapy Python/GTK runtime 移植进浏览器。
|
||||
- 不把 Python remap/tool DB/external user-M process 伪装成已支持。
|
||||
- 不用 UI fixture 结果冒充 LinuxCNC runtime proof。
|
||||
|
||||
## 6. 开工建议
|
||||
|
||||
当前已完成 Step 1 到 Step 7 的 Node 侧 LinuxCNC kinematics proof。下一轮应把 browser asset copy/worker 接入完成,让真实浏览器也能加载 kinematics WASM;或继续推进 interpreter/remap/planner,使 program execution 从 fixture line playback 升级。
|
||||
551
web-rtcp-5axis-sim-plan/docs/technical-roadmap.md
Normal file
@@ -0,0 +1,551 @@
|
||||
# RTCP 五轴 Web 仿真技术路线
|
||||
|
||||
生成时间:2026-06-20 CST
|
||||
|
||||
## 1. 技术选型
|
||||
|
||||
推荐技术栈:
|
||||
|
||||
```text
|
||||
UI: 原生 HTML + CSS + TypeScript/JavaScript ES modules
|
||||
Build: Vite 或 esbuild,仅作为 TypeScript/ES module 打包工具,不引入 React/Vue 等 UI 框架
|
||||
3D: Three.js
|
||||
WASM: Emscripten
|
||||
Core CNC reference: vendored LinuxCNC source
|
||||
GUI reference: LinuxCNC Python GUI sources, PyVCP XML, HAL postgui wiring
|
||||
Storage: OPFS
|
||||
Tests: Node smoke + Playwright/Chromium browser smoke
|
||||
Docs/gates: Markdown + machine-readable JSON/TSV artifacts
|
||||
```
|
||||
|
||||
前端界面不使用 React、Vue、Angular、Svelte 等 UI 框架。原因是本项目核心复杂度在 LinuxCNC/WASM、五轴运动学、RTCP 数据流、Three.js 机床模型和验证链路,不在通用 UI 框架。直接使用 HTML/CSS + TypeScript/JavaScript ES modules 更贴近当前 `wasm-port/runtime/ui/simulation` 的实现方式,也更容易保持 WASM、OPFS、virtual HAL 和 Three.js 的边界清晰。
|
||||
|
||||
不使用框架不等于写成单个巨大脚本。必须按模块拆分 `ui/`、`runtime/`、`visualization/`、`profiles/`、`panel-schema/`、`workers/`、`state/`。LinuxCNC 的 Python GUI 只作为参考输入:AXIS 的操作界面、vismach 的机床模型层级、PyVCP XML 面板、gmoccapy/QtVCP 的操作面板都需要转换成 Web 组件和 Three.js 场景。
|
||||
|
||||
## 2. 目录建议
|
||||
|
||||
后续实现可在本目录扩展为:
|
||||
|
||||
```text
|
||||
web-rtcp-5axis-sim-plan/
|
||||
README.md
|
||||
docs/
|
||||
implementation-plan.md
|
||||
technical-roadmap.md
|
||||
linuxcnc-python-gui-reference.md
|
||||
linuxcnc-gui-reference-gallery.md
|
||||
linuxcnc-reference-map.md
|
||||
rtcp-data-contract.md
|
||||
validation-plan.md
|
||||
app/
|
||||
index.html
|
||||
package.json
|
||||
src/
|
||||
main.ts
|
||||
ui/
|
||||
ui-reference/
|
||||
runtime/
|
||||
visualization/
|
||||
profiles/
|
||||
workers/
|
||||
panel-schema/
|
||||
state/
|
||||
core/
|
||||
linuxcnc_kinematics_wasm/
|
||||
include/
|
||||
src/
|
||||
build.sh
|
||||
tests/
|
||||
node/
|
||||
browser/
|
||||
```
|
||||
|
||||
第一轮实现可以只创建 `app/`,后续再把 C/WASM ABI 放入 `core/`。
|
||||
|
||||
## 3. gmoccapy 5 轴风格 Web 实现方案
|
||||
|
||||
结论:`gmoccapy_5_axis.png` 的界面风格可以实现为 Web 前端,而且适合作为本项目第一版数控系统仿真界面的首选风格。
|
||||
|
||||
参考图:
|
||||
|
||||

|
||||
|
||||
原始来源:
|
||||
|
||||
```text
|
||||
linuxcnc/docs/src/gui/images/gmoccapy_5_axis.png
|
||||
```
|
||||
|
||||
### 3.1 可实现性判断
|
||||
|
||||
gmoccapy 5 轴界面由清晰的操作区域组成,适合直接拆成 Web 组件:
|
||||
|
||||
- 左侧大面积刀路/机床预览:可用 Three.js 实现。
|
||||
- 右上大号绿色 DRO:可用 HTML/CSS 数字面板实现。
|
||||
- 中右 G-code 当前行列表:可用虚拟滚动或普通 scroll list 实现。
|
||||
- 右侧竖向模式按钮:可用原生 button + icon 实现。
|
||||
- 下方信息面板:可用 tabs 和 table 实现。
|
||||
- 中下 override、spindle、coolant 面板:可用 slider、stepper、toggle 实现。
|
||||
- 底部运行控制:可用 icon button 实现。
|
||||
|
||||
这些控件不需要 React/Vue;原生 HTML/CSS + TypeScript/JavaScript ES modules 足够实现。核心状态由一个小型 runtime store 驱动,所有 CNC 语义仍来自 LinuxCNC/WASM/kinematics 边界。
|
||||
|
||||
### 3.2 页面布局
|
||||
|
||||
建议第一版按 gmoccapy 截图拆为固定操作台布局:
|
||||
|
||||
```text
|
||||
┌──────────────────────────────────────────────────────────────────────┐
|
||||
│ 顶部标题栏:machine/profile/session/run state │
|
||||
├───────────────────────────────┬──────────────────────────────┬───────┤
|
||||
│ │ DRO + active G-code │ 右侧 │
|
||||
│ Three.js 五轴机床/刀路预览 │ X/Y/Z/A/B/C + G-code list │ 模式栏 │
|
||||
│ │ │ │
|
||||
├───────────────────────────────┼──────────────────────────────┴───────┤
|
||||
│ Tool info / G-code properties │ Velocity / Override / Spindle/Coolant │
|
||||
├───────────────────────────────┴──────────────────────────────────────┤
|
||||
│ 底部:open / reload / run / stop / pause / step / home / fullscreen │
|
||||
└──────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
适配 Web 五轴 RTCP 后,右上 DRO 应扩展为:
|
||||
|
||||
```text
|
||||
X Y Z A B C
|
||||
Abs / Rel / DTG
|
||||
TCP X/Y/Z
|
||||
Tool axis vector
|
||||
RTCP: on/off
|
||||
Kins: identity / TCP / userk
|
||||
```
|
||||
|
||||
### 3.3 Web 组件映射
|
||||
|
||||
建议模块:
|
||||
|
||||
```text
|
||||
ui/gmoccapy-shell.ts
|
||||
ui/gmoccapy-dro-panel.ts
|
||||
ui/gmoccapy-preview-toolbar.ts
|
||||
ui/gmoccapy-gcode-panel.ts
|
||||
ui/gmoccapy-status-sidebar.ts
|
||||
ui/gmoccapy-override-panel.ts
|
||||
ui/gmoccapy-spindle-coolant-panel.ts
|
||||
ui/gmoccapy-bottom-controls.ts
|
||||
ui/gmoccapy-info-tabs.ts
|
||||
```
|
||||
|
||||
组件职责:
|
||||
|
||||
- `gmoccapy-shell`:整体布局和区域挂载。
|
||||
- `dro-panel`:大号绿色坐标显示,显示 axis/joint/TCP/DTG。
|
||||
- `preview-toolbar`:视角、缩放、清除预览、适配窗口。
|
||||
- `gcode-panel`:程序行、当前行、运行进度。
|
||||
- `status-sidebar`:E-stop、machine on、manual/mdi/auto、settings、tool 等模式按钮。
|
||||
- `override-panel`:current velocity、rapid override、feed override。
|
||||
- `spindle-coolant-panel`:spindle rpm、spindle override、coolant state。
|
||||
- `bottom-controls`:open、reload、run、stop、pause、step、home、fullscreen。
|
||||
- `info-tabs`:tool info、G-code properties、RTCP diagnostics、session readiness。
|
||||
|
||||
### 3.4 状态模型
|
||||
|
||||
gmoccapy 风格页面需要一个小型 store,不需要前端框架:
|
||||
|
||||
```text
|
||||
GmoccapySimulationState
|
||||
machineProfile
|
||||
sessionReadiness
|
||||
runState
|
||||
activeProgram
|
||||
activeLine
|
||||
dro
|
||||
jointPose
|
||||
tcpPose
|
||||
rtcpState
|
||||
kinsType
|
||||
feedOverride
|
||||
rapidOverride
|
||||
spindleOverride
|
||||
coolantState
|
||||
previewState
|
||||
diagnostics
|
||||
```
|
||||
|
||||
状态更新路线:
|
||||
|
||||
```text
|
||||
LinuxCNC/WASM canonical events
|
||||
-> frame builder
|
||||
-> GmoccapySimulationState
|
||||
-> DOM renderers + Three.js scene
|
||||
```
|
||||
|
||||
### 3.5 样式原则
|
||||
|
||||
保持 gmoccapy 的工业操作台气质:
|
||||
|
||||
- 黑色 3D 预览背景。
|
||||
- 大号绿色 DRO 数字。
|
||||
- 灰色面板和分区边框。
|
||||
- 橙色 override 进度条。
|
||||
- 大尺寸图标按钮。
|
||||
- 底部运行控制固定高度。
|
||||
- 右侧模式栏固定宽度。
|
||||
|
||||
Web 版需要改进的点:
|
||||
|
||||
- 适配 1366x768、1920x1080 和平板尺寸。
|
||||
- 字体和按钮要避免溢出。
|
||||
- 3D 预览和 G-code 面板可以响应式分配宽度。
|
||||
- RTCP/TCP 专用状态必须比原图更明确。
|
||||
|
||||
### 3.6 与 LinuxCNC 源码边界
|
||||
|
||||
可 Web 化:
|
||||
|
||||
- 页面布局。
|
||||
- 操作按钮。
|
||||
- DRO 显示。
|
||||
- override/coolant/spindle 仿真状态。
|
||||
- G-code 当前行高亮。
|
||||
- Three.js 五轴预览。
|
||||
- RTCP/TCP 状态展示。
|
||||
|
||||
必须仍由 LinuxCNC/WASM 或 source-derived runtime 提供:
|
||||
|
||||
- G-code 解释。
|
||||
- canonical event。
|
||||
- modal state。
|
||||
- tool/parameter 语义。
|
||||
- 五轴 kinematics。
|
||||
- RTCP/TCP frame。
|
||||
- M428/M429/M430 remap 状态。
|
||||
|
||||
不直接实现:
|
||||
|
||||
- gmoccapy native GTK/Glade runtime。
|
||||
- LinuxCNC native task/motion IPC。
|
||||
- 真实 HAL component process。
|
||||
- Python remap runtime。
|
||||
- 真实机床 IO。
|
||||
|
||||
### 3.7 第一版验收标准
|
||||
|
||||
第一版 gmoccapy 风格 Web 前端完成时,应满足:
|
||||
|
||||
- 页面整体布局与 `gmoccapy_5_axis.png` 对齐:预览、DRO、G-code、右侧模式栏、底部控制、override/spindle/coolant 区都存在。
|
||||
- 使用原生 HTML/CSS + TypeScript/JavaScript ES modules,无 React/Vue 等框架。
|
||||
- Three.js 预览非空,能显示五轴机床、刀具和路径。
|
||||
- DRO 显示 X/Y/Z/A/B/C 和 TCP 坐标。
|
||||
- G-code 面板能高亮当前行。
|
||||
- Run/Stop/Pause/Step 控件能驱动仿真 playback。
|
||||
- RTCP/TCP 和 kinstype 状态在界面中可见。
|
||||
- Browser smoke 做截图和 canvas 非空验证。
|
||||
|
||||
## 4. 核心模块拆分
|
||||
|
||||
### 4.1 `profiles`
|
||||
|
||||
管理 LinuxCNC 五轴机型 profile:
|
||||
|
||||
- `xyzac-trt`
|
||||
- `xyzbc-trt`
|
||||
- `bridge-xyzbcw`
|
||||
- `tdr-xyzab`
|
||||
|
||||
每个 profile 记录:
|
||||
|
||||
- LinuxCNC config source path;
|
||||
- coordinates/joints;
|
||||
- kinematics source files;
|
||||
- HAL pins;
|
||||
- M428/M429/M430 remap;
|
||||
- sample programs;
|
||||
- limits 和默认参数。
|
||||
- Python GUI 参考文件;
|
||||
- PyVCP/HAL panel 控件;
|
||||
- vismach model node 映射。
|
||||
|
||||
### 4.2 `runtime`
|
||||
|
||||
管理运行状态:
|
||||
|
||||
- machine session;
|
||||
- G-code staging;
|
||||
- interpreter run;
|
||||
- kinematics frame calculation;
|
||||
- RTCP mode state;
|
||||
- diagnostics。
|
||||
|
||||
建议 API:
|
||||
|
||||
```text
|
||||
loadMachineProfile(profileId)
|
||||
stageProgram(programText)
|
||||
runProgram()
|
||||
buildFiveAxisFrames(canonicalEvents, machineProfile)
|
||||
setRtcpEnabled(enabled)
|
||||
setKinsType(type)
|
||||
updateMachineParameter(name, value)
|
||||
exportSessionSnapshot()
|
||||
restoreSessionSnapshot(snapshot)
|
||||
```
|
||||
|
||||
### 4.3 `linuxcnc_kinematics_wasm`
|
||||
|
||||
提供窄 C ABI:
|
||||
|
||||
```text
|
||||
linuxcnc_xyzac_trt_forward(input, output)
|
||||
linuxcnc_xyzac_trt_inverse(input, output)
|
||||
linuxcnc_xyzbc_trt_forward(input, output)
|
||||
linuxcnc_xyzbc_trt_inverse(input, output)
|
||||
linuxcnc_bridge_5axis_forward(input, output)
|
||||
linuxcnc_bridge_5axis_inverse(input, output)
|
||||
linuxcnc_set_hal_float(pin, value)
|
||||
linuxcnc_set_hal_bit(pin, value)
|
||||
linuxcnc_get_last_error()
|
||||
```
|
||||
|
||||
输入输出应使用稳定结构,不让 UI 直接理解 LinuxCNC 内部全局状态。
|
||||
|
||||
### 4.4 `visualization`
|
||||
|
||||
Three.js 负责:
|
||||
|
||||
- 机床底座、工作台、转台、摆头、主轴、刀具;
|
||||
- 坐标轴和工作空间;
|
||||
- TCP path;
|
||||
- joint path;
|
||||
- active segment;
|
||||
- tool axis vector;
|
||||
- RTCP on/off overlay。
|
||||
|
||||
机床模型初期可以用几何体组合,不要先追求 CAD 级模型。重点是五轴姿态和 TCP 点正确、可验证。
|
||||
|
||||
vismach 转换规则:
|
||||
|
||||
- `Collection` -> Three.js `Group`;
|
||||
- `Translate` / `Rotate` -> 静态 transform node;
|
||||
- `HalTranslate` / `HalRotate` -> 绑定 virtual HAL pin 的动态 transform node;
|
||||
- STL/workpiece asset -> glTF/STL loader 或简化几何体;
|
||||
- vismach plot clear -> Web runtime 的 path clear action;
|
||||
- HAL component pin -> profile 中声明的 observable runtime value。
|
||||
|
||||
### 4.5 `ui`
|
||||
|
||||
界面组件:
|
||||
|
||||
- machine profile selector;
|
||||
- RTCP/kins panel;
|
||||
- G-code editor;
|
||||
- run controls;
|
||||
- DRO;
|
||||
- joint position panel;
|
||||
- HAL/diagnostics;
|
||||
- Three.js viewport;
|
||||
- session save/restore。
|
||||
- gmoccapy-style shell;
|
||||
- gmoccapy-style DRO / override / spindle / coolant / status sidebar。
|
||||
|
||||
UI 参考转换:
|
||||
|
||||
- AXIS:菜单、工具栏、Manual/MDI、预览、DRO、G-code、状态栏。
|
||||
- PyVCP:`SWITCHKINS`、joint 数值、offset/rot-point 控件、`vismach-clear`。
|
||||
- gmoccapy:首选界面风格;大按钮操作、jog increment、override、右侧模式栏、底部运行控制、黑底预览和绿色 DRO。
|
||||
- QtVCP/QtDragon:现代触控面板、状态区、工具/探测/大屏布局。
|
||||
|
||||
### 4.6 `panel-schema`
|
||||
|
||||
把 PyVCP XML 和 postgui HAL 的参考关系整理为 Web 面板 schema。
|
||||
|
||||
建议 schema:
|
||||
|
||||
```text
|
||||
PanelControl
|
||||
id
|
||||
type: label | multilabel | button | number | slider | toggle
|
||||
halpin
|
||||
signal
|
||||
sourceFile
|
||||
command
|
||||
displayFormat
|
||||
readonly
|
||||
```
|
||||
|
||||
第一阶段不需要完整 XML parser,可以先把 `xyzac-trt.xml`、`xyzbc-trt.xml`、`5axis.xml`、`xyzab-tdr.xml` 手工整理成 JSON/TS profile。后续再决定是否写 PyVCP XML importer。
|
||||
|
||||
## 5. LinuxCNC 源码接入路线
|
||||
|
||||
### Step 1:建立引用清单
|
||||
|
||||
形成 `linuxcnc-reference-map.md`,记录每个能力引用哪些 LinuxCNC 文件。
|
||||
|
||||
第一批:
|
||||
|
||||
```text
|
||||
trtfuncs.c
|
||||
xyzac-trt-kins.c
|
||||
xyzbc-trt-kins.c
|
||||
5axiskins.c
|
||||
switchkins.c / switchkins.h
|
||||
kins_util.c
|
||||
kinematics.h
|
||||
emcpose.h / emcpose.c
|
||||
```
|
||||
|
||||
Python GUI / panel 第一批:
|
||||
|
||||
```text
|
||||
src/emc/usr_intf/axis/scripts/axis.py
|
||||
lib/python/vismach.py
|
||||
configs/sim/axis/vismach/5axis/bridgemill/5axis.xml
|
||||
configs/sim/axis/vismach/5axis/bridgemill/5axisgui.hal
|
||||
configs/sim/axis/vismach/5axis/bridgemill/5axis_postgui.hal
|
||||
configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.xml
|
||||
configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.xml
|
||||
configs/sim/axis/vismach/5axis/table-rotary-tilting/switchkins_postgui.hal
|
||||
configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.xml
|
||||
configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr-postgui.hal
|
||||
configs/sim/gmoccapy/gmoccapy_XYZAC.ini
|
||||
configs/sim/qtvcp_screens/qtdragon/README
|
||||
```
|
||||
|
||||
### Step 2:建立 C ABI shim
|
||||
|
||||
不要把 LinuxCNC HAL module lifecycle 原样暴露给浏览器。应构造最小 shim:
|
||||
|
||||
- 初始化 profile;
|
||||
- 分配 HAL 参数存储;
|
||||
- 设置 HAL pin 值;
|
||||
- 调用 forward/inverse;
|
||||
- 输出 frame。
|
||||
|
||||
### Step 3:编译 WASM
|
||||
|
||||
使用 Emscripten 编译 kinematics core。初期可与当前 `wasm-port` 构建脚本分离,等稳定后再合并。
|
||||
|
||||
### Step 4:Node 验证
|
||||
|
||||
至少验证:
|
||||
|
||||
- `xyzac` forward/inverse;
|
||||
- `xyzbc` forward/inverse;
|
||||
- tool offset 变化影响 TCP;
|
||||
- rot point 变化影响结果;
|
||||
- kinstype 0/1 切换状态;
|
||||
- source file hash 与 vendor 一致。
|
||||
|
||||
### Step 5:Browser 验证
|
||||
|
||||
至少验证:
|
||||
|
||||
- WASM 在浏览器加载;
|
||||
- Three.js canvas 非空;
|
||||
- 五轴姿态非默认;
|
||||
- RTCP 开关改变显示状态;
|
||||
- 加载 sample program 后 path 点数非零。
|
||||
- `SWITCHKINS` 面板能显示 `IDENTITY` / `TCP` / `USERK` 状态。
|
||||
- joint value 面板与 five-axis frame 中的 joint pose 对齐。
|
||||
- `vismach-clear` 类操作能清空预览路径,不改变 LinuxCNC semantic state。
|
||||
|
||||
### Step 6:Python GUI 参考验收
|
||||
|
||||
该步骤不是运行 Python GUI,而是证明 Web 界面已经吸收其关键设计:
|
||||
|
||||
- AXIS 区域齐全:toolbar、Manual/MDI、preview、DRO、G-code、statusbar。
|
||||
- PyVCP 控件齐全:switchkins multilabel、type buttons、joint values、clear path。
|
||||
- vismach 模型层级齐全:动态平移、动态旋转、工具、工作台、旋转点。
|
||||
- gmoccapy/QtVCP 操作形态齐全:大按钮、override、jog increment、operator status。
|
||||
|
||||
## 6. RTCP 数据流
|
||||
|
||||
推荐数据流:
|
||||
|
||||
```text
|
||||
LinuxCNC G-code text
|
||||
-> interpreter WASM
|
||||
-> canonical events / modal output
|
||||
-> five-axis frame builder
|
||||
-> LinuxCNC kinematics WASM forward/inverse
|
||||
-> FiveAxisMotionFrame[]
|
||||
-> Three.js visualization + DRO + status panels
|
||||
```
|
||||
|
||||
`FiveAxisMotionFrame` 是 UI 的唯一运动数据输入。这样可以避免 UI 从多个地方拼状态导致错位。
|
||||
|
||||
## 7. 测试路线
|
||||
|
||||
### Node smoke
|
||||
|
||||
- profile inventory;
|
||||
- source reference map;
|
||||
- kinematics ABI;
|
||||
- frame builder;
|
||||
- OPFS/session pure helpers;
|
||||
- no JS CNC semantics guard。
|
||||
|
||||
### Browser smoke
|
||||
|
||||
- 页面加载;
|
||||
- profile 切换;
|
||||
- sample program run;
|
||||
- RTCP status;
|
||||
- canvas pixel 非空;
|
||||
- tool marker 移动;
|
||||
- active G-code line;
|
||||
- session save/restore。
|
||||
|
||||
### Visual regression
|
||||
|
||||
先做轻量检查:
|
||||
|
||||
- canvas 非空;
|
||||
- tool marker 坐标在视口内;
|
||||
- path bounding box 合理;
|
||||
- A/B/C 非零时刀轴方向变化。
|
||||
|
||||
## 8. 里程碑
|
||||
|
||||
建议按以下顺序推进:
|
||||
|
||||
1. `M0-docs`:当前方案和技术路线。
|
||||
2. `M1-web-shell`:独立 Web app、Three.js 空机床、普通 G-code 执行接通。
|
||||
3. `M2-python-gui-reference-panels`:AXIS/PyVCP/vismach/gmoccapy/QtVCP 参考界面转换为 Web panel/schema。
|
||||
4. `M3-profile-loader`:`xyzac-trt` / `xyzbc-trt` profile、INI/HAL/remap asset 可见。
|
||||
5. `M4-kinematics-wasm`:LinuxCNC 五轴运动学 C/WASM ABI。
|
||||
6. `M5-rtcp-preview`:RTCP/TCP path、joint/work/TCP 同步显示。
|
||||
7. `M6-kinematics-frame-proof`:Web RTCP frame/boundary adapter 接入 `xyzac-trt` kinematics WASM Node proof。
|
||||
8. `M7-browser-kinematics-runtime`:浏览器 asset copy/worker 加载 kinematics WASM。
|
||||
9. `M8-session`:OPFS 保存/恢复五轴仿真会话。
|
||||
10. `M9-release-gate`:Node/browser/docs/release gate。
|
||||
|
||||
## 9. 验收标准
|
||||
|
||||
最小可验收版本:
|
||||
|
||||
- 浏览器中可以选择 `xyzac-trt`;
|
||||
- 可以加载并运行一个 5 轴 sample 或 representative program;
|
||||
- 可以看到 XYZAC 或 XYZBC 轴值;
|
||||
- 可以切换或识别 RTCP/TCP 状态;
|
||||
- Three.js 中显示机床、刀具姿态和 TCP 轨迹;
|
||||
- UI 中有参考 PyVCP 的 `SWITCHKINS` 面板和 joint 数值区;
|
||||
- Three.js 机床模型能体现 vismach Python 示例的动态层级;
|
||||
- 结果来自 LinuxCNC interpreter + LinuxCNC kinematics WASM 边界;
|
||||
- browser smoke 通过;
|
||||
- 文档明确哪些功能 blocked。
|
||||
|
||||
## 10. 下一步动作
|
||||
|
||||
当前已完成到 `M6-kinematics-frame-proof`。建议下一轮优先做:
|
||||
|
||||
- 在 build-static 中复制 `wasm-port/build/wasm/kinematics` 所需产物,或新增 worker 隔离 kinematics WASM 加载;
|
||||
- 让 `app/src/main.js` 在浏览器中 attach `xyzac-trt` kinematics runtime;
|
||||
- 保持 fixture fallback,不把 browser fixture smoke 标记为 LinuxCNC proof;
|
||||
- 或转向 LinuxCNC interpreter/remap/planner 接入,把 program execution 从 fixture line playback 升级。
|
||||
- 使用 Vite + Three.js;
|
||||
- 先接入现有普通 G-code program inventory;
|
||||
- 画出五轴机床基本结构;
|
||||
- 按 AXIS/PyVCP/gmoccapy 参考放置 toolbar、Manual/MDI、DRO、SWITCHKINS、joint values、preview;
|
||||
- 加 Playwright smoke;
|
||||
- 启动本地 dev server 给出访问地址。
|
||||
467
web-rtcp-5axis-sim-plan/docs/traceability-matrix.md
Normal file
@@ -0,0 +1,467 @@
|
||||
# 5 轴数控系统 Web 仿真实现追溯文档
|
||||
|
||||
生成时间:2026-06-20 CST
|
||||
|
||||
## 1. 目的
|
||||
|
||||
本文件用于追溯后续实现中的每个主要功能来自哪里、参考了哪些 LinuxCNC 源文件/配置/图片、属于什么边界、如何验证。
|
||||
|
||||
追溯原则:
|
||||
|
||||
```text
|
||||
界面形态可参考 LinuxCNC Python GUI;
|
||||
CNC 语义必须来自 LinuxCNC source/WASM/source-derived boundary;
|
||||
浏览器代码只负责 UI、状态编排、文件会话、可视化和调用边界;
|
||||
任何 fixture 或 UI-only 结果不得冒充 LinuxCNC runtime proof。
|
||||
```
|
||||
|
||||
## 2. 总体追溯表
|
||||
|
||||
| 功能 | Web 实现位置 | LinuxCNC 参考 | 边界分类 | 验证方式 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| gmoccapy 风格 shell | `app/src/ui/gmoccapy-shell.ts` | `docs/src/gui/images/gmoccapy_5_axis.png`, `configs/sim/gmoccapy/gmoccapy_XYZAC.ini` | UI reference | browser shell smoke |
|
||||
| 大号 DRO | `app/src/ui/gmoccapy-dro-panel.ts` | gmoccapy 5 axis screenshot, LinuxCNC DRO conventions | UI rendering of runtime state | DOM smoke + state smoke |
|
||||
| 右侧模式按钮栏 | `app/src/ui/gmoccapy-status-sidebar.ts` | gmoccapy screenshot | UI action dispatch | browser button/action smoke |
|
||||
| 底部运行控制 | `app/src/ui/gmoccapy-bottom-controls.ts` | gmoccapy/AXIS run controls | UI action dispatch | playback smoke |
|
||||
| G-code 当前行 | `app/src/ui/gmoccapy-gcode-panel.ts` | AXIS/gmoccapy program display | LinuxCNC output rendering | active-line smoke |
|
||||
| 上电/急停/复位/模式操作 | `app/src/state/store.js`, `app/src/ui/gmoccapy-shell.js` | gmoccapy/AXIS operator workflow | browser UI runtime fixture | node smoke + browser operator smoke |
|
||||
| JOG/MDI 操作 | `app/src/state/store.js`, `app/src/ui/gmoccapy-shell.js` | AXIS Manual/MDI workflow | browser UI runtime fixture | node smoke + browser operator smoke |
|
||||
| G-code 文件加载 | `app/src/ui/gmoccapy-shell.js`, `app/src/state/store.js` | AXIS/gmoccapy open program workflow | file text staging only, not LinuxCNC interpreter proof | node smoke + browser operator smoke |
|
||||
| 程序执行当前行显示 | `app/src/state/store.js`, `app/src/ui/gmoccapy-shell.js` | AXIS/gmoccapy current line display | fixture line playback until LinuxCNC interpreter is connected | node smoke + browser operator smoke |
|
||||
| 刀具预览 | `app/src/ui/gmoccapy-shell.js`, `app/src/visualization/five-axis-scene.js` | gmoccapy/vismach tool display | visualization/runtime state display | browser operator smoke |
|
||||
| 3D 五轴预览 | `app/src/visualization/five-axis-scene.js` | `qtvismach_5axis_gantry.png`, `lib/python/vismach.py` | visualization | canvas nonblank smoke |
|
||||
| Vismach transform tree | `app/src/visualization/machine-model.ts` | `lib/python/vismach.py`, `src/hal/user_comps/vismach/*.py` | visualization from GUI reference | scene graph smoke |
|
||||
| `xyzac-trt` profile | `app/src/profiles/xyzac-trt.js` | `configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini` | source/config reference | profile boundary node smoke |
|
||||
| `xyzac-trt` source reference map | `app/src/profiles/source-reference-map.js` | `xyzac-trt.ini`, `xyzac-trt.xml`, `switchkins_postgui.hal`, `xyzac-trt_cmds.hal`, `xyzac-trt-kins.c`, `trtfuncs.c`, `switchkins.c` | profile/source map only, not runtime proof | profile boundary node smoke |
|
||||
| `xyzbc-trt` profile | `app/src/profiles/xyzbc-trt.ts` | `configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini` | source/config reference | profile node smoke |
|
||||
| SWITCHKINS panel | `app/src/panel-schema/xyzac-trt-pyvcp.js` | `xyzac-trt.xml`, `switchkins_postgui.hal` | UI/HAL binding reference | profile boundary node smoke + browser DOM smoke |
|
||||
| M428/M429/M430 state | `app/src/profiles/xyzac-trt.js`, `app/src/panel-schema/xyzac-trt-pyvcp.js` | `remap_subs/428remap.ngc`, `429remap.ngc`, `430remap.ngc` | LinuxCNC remap/source reference only until runtime adapter is connected | profile boundary node smoke |
|
||||
| LinuxCNC boundary adapter | `app/src/runtime/linuxcnc-boundary-adapter.js` | LinuxCNC interpreter/kinematics WASM future adapter point | adapter entrypoint only, runtime not connected | profile boundary node smoke + browser DOM smoke |
|
||||
| Five-axis kinematics | `core/linuxcnc_kinematics_wasm` | `trtfuncs.c`, `xyzac-trt-kins.c`, `xyzbc-trt-kins.c`, `5axiskins.c` | LinuxCNC source-derived WASM | Node roundtrip smoke |
|
||||
| RTCP/TCP frame | `app/src/runtime/rtcp-frame.js` | LinuxCNC kinematics output + canonical events | fixture frame plumbing until kinematics WASM is ready | RTCP/store node smoke + browser DOM smoke |
|
||||
| OPFS session | `app/src/runtime/session-*` | current `wasm-port/runtime/opfs` | host-side persistence | save/restore smoke |
|
||||
|
||||
## 3. 源文件追溯清单
|
||||
|
||||
### UI 图片
|
||||
|
||||
```text
|
||||
linuxcnc/docs/src/gui/images/gmoccapy_5_axis.png
|
||||
linuxcnc/docs/src/gui/images/qtvismach_5axis_gantry.png
|
||||
linuxcnc/docs/src/gui/images/axis.png
|
||||
linuxcnc/docs/src/gui/images/axis-pyvcp.png
|
||||
linuxcnc/docs/src/gui/images/qtdragon.png
|
||||
linuxcnc/docs/src/gui/images/qtdragon_hd.png
|
||||
```
|
||||
|
||||
本项目副本:
|
||||
|
||||
```text
|
||||
assets/reference/linuxcnc-gui/
|
||||
```
|
||||
|
||||
### Python GUI
|
||||
|
||||
```text
|
||||
src/emc/usr_intf/axis/scripts/axis.py
|
||||
lib/python/vismach.py
|
||||
src/hal/user_comps/vismach/5axisgui.py
|
||||
src/hal/user_comps/vismach/xyzac-trt-gui.py
|
||||
src/hal/user_comps/vismach/xyzbc-trt-gui.py
|
||||
configs/sim/gmoccapy/gmoccapy_XYZAC.ini
|
||||
configs/sim/qtvcp_screens/qtdragon/README
|
||||
```
|
||||
|
||||
使用方式:
|
||||
|
||||
```text
|
||||
UI/visualization reference only
|
||||
```
|
||||
|
||||
### 5 轴配置
|
||||
|
||||
```text
|
||||
configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini
|
||||
configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini
|
||||
configs/sim/axis/vismach/5axis/bridgemill/5axis.ini
|
||||
configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini
|
||||
```
|
||||
|
||||
### PyVCP/HAL
|
||||
|
||||
```text
|
||||
configs/sim/axis/vismach/5axis/bridgemill/5axis.xml
|
||||
configs/sim/axis/vismach/5axis/bridgemill/5axis_postgui.hal
|
||||
configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.xml
|
||||
configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.xml
|
||||
configs/sim/axis/vismach/5axis/table-rotary-tilting/switchkins_postgui.hal
|
||||
configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.xml
|
||||
configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr-postgui.hal
|
||||
```
|
||||
|
||||
### 5 轴运动学源码
|
||||
|
||||
```text
|
||||
src/emc/kinematics/trtfuncs.c
|
||||
src/emc/kinematics/xyzac-trt-kins.c
|
||||
src/emc/kinematics/xyzbc-trt-kins.c
|
||||
src/emc/kinematics/5axiskins.c
|
||||
src/emc/kinematics/switchkins.c
|
||||
src/emc/kinematics/switchkins.h
|
||||
src/emc/kinematics/userkfuncs.c
|
||||
src/emc/kinematics/kins_util.c
|
||||
```
|
||||
|
||||
## 4. 边界状态
|
||||
|
||||
| 边界 | 当前状态 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| gmoccapy UI style | ready | 可直接 Web 化 |
|
||||
| Three.js preview | implemented_basic_canvas_scene | 已显示基础五轴机床、刀具/TCP marker、刀轴、刀路,并消费 `rtcpFrame` |
|
||||
| LinuxCNC interpreter WASM | existing_project_capability | 可参考 `wasm-port` 现有 SDK |
|
||||
| LinuxCNC 5-axis kinematics WASM | node_proof_ready | `createLinuxCncKinematicsSdk({ moduleId: "xyzac-trt" })` 已由 web adapter 加载,Node smoke 验证 forward/inverse frame |
|
||||
| RTCP frame UI plumbing | implemented_fixture_and_kinematics_wasm | fixture fallback 仍为 `linuxCncKinematicsReady=false`;Node kinematics proof 为 `source-derived-kinematics-wasm` / `linuxcnc_kinematics_wasm_c_abi` |
|
||||
| Operator workflow | implemented_fixture_only | 上电/急停/复位/模式/JOG/MDI/G-code 加载/当前行显示已闭环;执行仍是 fixture line playback |
|
||||
| LinuxCNC boundary adapter | kinematics_runtime_connected_node | `web-rtcp-5axis-linuxcnc-boundary-adapter` 可区分 kinematics-only ready 与 interpreter/remap missing |
|
||||
| PyVCP/HAL panel schema | implemented_reference_only | `xyzac-trt-switchkins-pyvcp` 已整理 SWITCHKINS 控件与 HAL nets;不执行 native HAL |
|
||||
| Python GUI runtime | not_ported | 只参考,不运行 |
|
||||
| Python remap runtime | blocked | 不在第一版实现 |
|
||||
| tool DB runtime | blocked | 不在第一版实现 |
|
||||
| external user-M process | blocked | 不在第一版实现 |
|
||||
| native hard realtime | out_of_scope | Web 仿真不实现 |
|
||||
|
||||
## 5. 每批开发追溯记录模板
|
||||
|
||||
后续每批完成后追加:
|
||||
|
||||
```text
|
||||
Batch:
|
||||
Date:
|
||||
Files changed:
|
||||
Feature:
|
||||
LinuxCNC references:
|
||||
Boundary:
|
||||
Tests:
|
||||
Result:
|
||||
Remaining risk:
|
||||
Next:
|
||||
```
|
||||
|
||||
## 6. 首批追溯记录
|
||||
|
||||
```text
|
||||
Batch: M0-docs-preparation
|
||||
Date: 2026-06-20 CST
|
||||
Files changed:
|
||||
README.md
|
||||
docs/implementation-plan.md
|
||||
docs/technical-roadmap.md
|
||||
docs/program-implementation-guide.md
|
||||
docs/development-continuation.md
|
||||
docs/traceability-matrix.md
|
||||
docs/linuxcnc-python-gui-reference.md
|
||||
docs/linuxcnc-gui-reference-gallery.md
|
||||
Feature:
|
||||
完成 5 轴数控系统 Web 仿真程序开发前准备文档。
|
||||
LinuxCNC references:
|
||||
gmoccapy_5_axis.png
|
||||
qtvismach_5axis_gantry.png
|
||||
axis.py
|
||||
vismach.py
|
||||
xyzac-trt.ini
|
||||
xyzbc-trt.ini
|
||||
trtfuncs.c
|
||||
5axiskins.c
|
||||
Boundary:
|
||||
docs_only
|
||||
Tests:
|
||||
git diff --check -- web-rtcp-5axis-sim-plan
|
||||
Result:
|
||||
ready_for_M1_web_shell_gmoccapy
|
||||
Remaining risk:
|
||||
尚未实现 app 代码;LinuxCNC kinematics WASM ABI 尚未建立。
|
||||
Next:
|
||||
M1-web-shell-gmoccapy
|
||||
```
|
||||
|
||||
## 7. M1 追溯记录
|
||||
|
||||
```text
|
||||
Batch: M1-web-shell-gmoccapy
|
||||
Date: 2026-06-20 CST
|
||||
Files changed:
|
||||
.gitignore
|
||||
app/index.html
|
||||
app/package.json
|
||||
app/tsconfig.json
|
||||
app/scripts/build-static.mjs
|
||||
app/src/main.js
|
||||
app/src/state/store.js
|
||||
app/src/ui/gmoccapy-shell.js
|
||||
app/src/styles/gmoccapy.css
|
||||
tests/browser/gmoccapy_shell_smoke.html
|
||||
tests/browser/verify_gmoccapy_shell_browser.sh
|
||||
Feature:
|
||||
实现 gmoccapy 5 轴风格 Web shell,并补齐 M1 build gate。
|
||||
LinuxCNC references:
|
||||
docs/src/gui/images/gmoccapy_5_axis.png
|
||||
docs/src/gui/images/qtvismach_5axis_gantry.png
|
||||
configs/sim/gmoccapy/gmoccapy_XYZAC.ini
|
||||
Boundary:
|
||||
ui_reference_only
|
||||
sourceMode=fixture-ui-only
|
||||
Tests:
|
||||
npm --prefix web-rtcp-5axis-sim-plan/app run build
|
||||
web-rtcp-5axis-sim-plan/tests/browser/verify_gmoccapy_shell_browser.sh
|
||||
git diff --check -- web-rtcp-5axis-sim-plan
|
||||
Result:
|
||||
gmoccapy_static_build=ok
|
||||
gmoccapy_shell_smoke=ok
|
||||
Remaining risk:
|
||||
M1 shell/build gate 已闭环;Three.js 真实场景、LinuxCNC interpreter/WASM、5 轴 kinematics ABI 仍未接入。
|
||||
Next:
|
||||
M2-state-and-controls
|
||||
```
|
||||
|
||||
## 8. M2 追溯记录
|
||||
|
||||
```text
|
||||
Batch: M2-state-and-controls
|
||||
Date: 2026-06-20 CST
|
||||
Files changed:
|
||||
app/package.json
|
||||
app/src/profiles/xyzac-trt.js
|
||||
app/src/runtime/rtcp-frame.js
|
||||
app/src/state/store.js
|
||||
app/src/ui/gmoccapy-shell.js
|
||||
app/src/styles/gmoccapy.css
|
||||
tests/node/verify_rtcp_store.mjs
|
||||
tests/browser/gmoccapy_shell_smoke.html
|
||||
docs/development-continuation.md
|
||||
docs/traceability-matrix.md
|
||||
Feature:
|
||||
实现 gmoccapy store/control 链路,并加入 RTCP fixture frame plumbing。
|
||||
Run/Step 会推进 fixture motion frame;TCP/IDENTITY 会切换 RTCP on/off;
|
||||
DRO、preview badge 和 info diagnostics 显示 TCP pose、tool axis vector、frame readiness。
|
||||
Preview view/Fit/Clear、Rapid/Feed override、Spindle override、Flood/Mist、
|
||||
Reload/Home/Full 已接入 store action,并由 node/browser smoke 覆盖。
|
||||
LinuxCNC references:
|
||||
configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini
|
||||
configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.xml
|
||||
configs/sim/axis/vismach/5axis/table-rotary-tilting/switchkins_postgui.hal
|
||||
src/emc/kinematics/xyzac-trt-kins.c
|
||||
src/emc/kinematics/trtfuncs.c
|
||||
Boundary:
|
||||
sourceMode=fixture-ui-only
|
||||
semanticBoundary=fixture_frame_ui_plumbing_not_linuxcnc_kinematics_proof
|
||||
linuxCncKinematicsReady=false
|
||||
promotionAllowed=false
|
||||
Tests:
|
||||
npm --prefix web-rtcp-5axis-sim-plan/app run build
|
||||
npm --prefix web-rtcp-5axis-sim-plan/app run smoke:node
|
||||
npm --prefix web-rtcp-5axis-sim-plan/app run smoke
|
||||
Result:
|
||||
gmoccapy_static_build=ok
|
||||
rtcp_store_smoke=ok
|
||||
gmoccapy_shell_smoke=ok
|
||||
Remaining risk:
|
||||
RTCP 当前是 UI/runtime frame plumbing,不是 LinuxCNC/source-derived kinematics WASM proof;
|
||||
Three.js 仍是 SVG placeholder,M3 需要接入真实 canvas scene 并消费 rtcpFrame。
|
||||
Next:
|
||||
M3-threejs-preview
|
||||
```
|
||||
|
||||
## 9. M3 追溯记录
|
||||
|
||||
```text
|
||||
Batch: M3-threejs-preview
|
||||
Date: 2026-06-21 CST
|
||||
Files changed:
|
||||
app/src/vendor/three/three.module.js
|
||||
app/src/vendor/three/three.core.js
|
||||
app/src/visualization/five-axis-scene.js
|
||||
app/src/ui/gmoccapy-shell.js
|
||||
tests/browser/gmoccapy_shell_smoke.html
|
||||
docs/development-continuation.md
|
||||
docs/traceability-matrix.md
|
||||
Feature:
|
||||
将 preview 区域从占位内容升级为 Three.js WebGL canvas。
|
||||
场景显示基础五轴工作区、工作台、刀具/TCP marker、刀轴和刀路;
|
||||
tool marker 消费 store 的 tcpPose,刀轴消费 toolAxisVector,RTCP on/off 影响可见刀轴长度和姿态数据。
|
||||
LinuxCNC references:
|
||||
docs/src/gui/images/qtvismach_5axis_gantry.png
|
||||
lib/python/vismach.py
|
||||
src/hal/user_comps/vismach/xyzac-trt-gui.py
|
||||
Boundary:
|
||||
visualization_only
|
||||
sourceMode=fixture-ui-only
|
||||
semanticBoundary=fixture_frame_ui_plumbing_not_linuxcnc_kinematics_proof
|
||||
Tests:
|
||||
npm --prefix web-rtcp-5axis-sim-plan/app run build
|
||||
npm --prefix web-rtcp-5axis-sim-plan/app run smoke:node
|
||||
npm --prefix web-rtcp-5axis-sim-plan/app run smoke
|
||||
Result:
|
||||
gmoccapy_static_build=ok
|
||||
rtcp_store_smoke=ok
|
||||
gmoccapy_shell_smoke=ok
|
||||
Remaining risk:
|
||||
Three.js 当前消费 fixture frame;五轴运动学仍未接入 LinuxCNC/source-derived WASM。
|
||||
Next:
|
||||
M4-profile-and-linuxcnc-boundary
|
||||
```
|
||||
|
||||
## 10. M4 追溯记录
|
||||
|
||||
```text
|
||||
Batch: M4-profile-and-linuxcnc-boundary
|
||||
Date: 2026-06-21 CST
|
||||
Files changed:
|
||||
app/package.json
|
||||
app/src/profiles/xyzac-trt.js
|
||||
app/src/profiles/source-reference-map.js
|
||||
app/src/panel-schema/xyzac-trt-pyvcp.js
|
||||
app/src/runtime/linuxcnc-boundary-adapter.js
|
||||
app/src/state/store.js
|
||||
app/src/ui/gmoccapy-shell.js
|
||||
tests/node/verify_rtcp_store.mjs
|
||||
tests/node/verify_profile_boundary.mjs
|
||||
tests/browser/gmoccapy_shell_smoke.html
|
||||
docs/development-continuation.md
|
||||
docs/program-implementation-guide.md
|
||||
docs/traceability-matrix.md
|
||||
Feature:
|
||||
建立 `xyzac-trt` profile/source reference map、PyVCP/HAL SWITCHKINS panel schema
|
||||
和 LinuxCNC boundary adapter 接入点。store/UI 暴露 boundary readiness,
|
||||
并明确保持 `linuxCncKinematicsReady=false`、`promotionAllowed=false`。
|
||||
本批后续已进一步按 LinuxCNC `xyzac-trt.ini`、`xyzac-trt_cmds.hal`、
|
||||
`switchkins_postgui.hal`、`xyzac-trt.tbl`、`428/429/430remap.ngc` 显式整理
|
||||
DISPLAY/RS274NGC/TRAJ、axis/joint limits、HALCMD feedback/offset nets、
|
||||
HALUI MDI commands、tool table 和 remap IO 约束,使 M4 在 source-aligned
|
||||
profile/schema/adapter 层面完整闭环。
|
||||
LinuxCNC references:
|
||||
configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini
|
||||
configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.xml
|
||||
configs/sim/axis/vismach/5axis/table-rotary-tilting/switchkins_postgui.hal
|
||||
configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt_cmds.hal
|
||||
configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/428remap.ngc
|
||||
configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/429remap.ngc
|
||||
configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/430remap.ngc
|
||||
src/emc/kinematics/xyzac-trt-kins.c
|
||||
src/emc/kinematics/trtfuncs.c
|
||||
src/emc/kinematics/switchkins.c
|
||||
Boundary:
|
||||
sourceMapBoundary=profile_source_map_only_not_runtime_proof
|
||||
panelSchemaBoundary=pyvcp_hal_schema_reference_only
|
||||
adapterBoundary=adapter_entrypoint_only_runtime_not_connected
|
||||
linuxCncKinematicsReady=false
|
||||
promotionAllowed=false
|
||||
Tests:
|
||||
npm --prefix web-rtcp-5axis-sim-plan/app run build
|
||||
npm --prefix web-rtcp-5axis-sim-plan/app run smoke:node
|
||||
npm --prefix web-rtcp-5axis-sim-plan/app run smoke
|
||||
Result:
|
||||
gmoccapy_static_build=ok
|
||||
rtcp_store_smoke=ok
|
||||
profile_boundary_smoke=ok
|
||||
gmoccapy_shell_smoke=ok
|
||||
Remaining risk:
|
||||
Adapter 目前只是接入点;尚未连接 LinuxCNC interpreter/kinematics WASM ABI。
|
||||
Next:
|
||||
linuxcnc_kinematics_wasm_abi
|
||||
```
|
||||
|
||||
## 11. M5 追溯记录
|
||||
|
||||
```text
|
||||
Batch: M5-operator-program-workflow
|
||||
Date: 2026-06-21 CST
|
||||
Files changed:
|
||||
app/src/state/store.js
|
||||
app/src/ui/gmoccapy-shell.js
|
||||
app/src/styles/gmoccapy.css
|
||||
tests/node/verify_rtcp_store.mjs
|
||||
tests/browser/gmoccapy_shell_smoke.html
|
||||
docs/development-continuation.md
|
||||
docs/traceability-matrix.md
|
||||
Feature:
|
||||
补齐 operator workflow:上电、急停、AUTO/MANUAL/JOG/MDI、复位、
|
||||
本地 G-code 文件加载、刀具预览、程序执行当前行显示和高亮。
|
||||
LinuxCNC references:
|
||||
gmoccapy operator workflow
|
||||
AXIS program loading/current-line workflow
|
||||
vismach tool preview conventions
|
||||
Boundary:
|
||||
operatorWorkflowBoundary=browser_ui_runtime_fixture
|
||||
gcodeLoadBoundary=file_text_staging_only
|
||||
programExecutionBoundary=fixture_line_playback_not_linuxcnc_interpreter
|
||||
linuxCncKinematicsReady=false
|
||||
promotionAllowed=false
|
||||
Tests:
|
||||
npm --prefix web-rtcp-5axis-sim-plan/app run build
|
||||
npm --prefix web-rtcp-5axis-sim-plan/app run smoke:node
|
||||
npm --prefix web-rtcp-5axis-sim-plan/app run smoke
|
||||
Result:
|
||||
gmoccapy_static_build=ok
|
||||
rtcp_store_smoke=ok
|
||||
profile_boundary_smoke=ok
|
||||
gmoccapy_shell_smoke=ok
|
||||
Remaining risk:
|
||||
程序执行仍是 fixture line playback;下一步必须接 LinuxCNC interpreter/WASM 或
|
||||
source-derived kinematics WASM,才能把执行来源升级为 LinuxCNC-owned。
|
||||
Next:
|
||||
linuxcnc_interpreter_or_kinematics_wasm_execution_source
|
||||
```
|
||||
|
||||
## 12. M6 追溯记录
|
||||
|
||||
```text
|
||||
Batch: M6-linuxcnc-kinematics-frame-proof
|
||||
Date: 2026-06-21 CST
|
||||
Files changed:
|
||||
app/package.json
|
||||
app/src/runtime/linuxcnc-kinematics-runtime.js
|
||||
app/src/runtime/linuxcnc-boundary-adapter.js
|
||||
app/src/runtime/rtcp-frame.js
|
||||
app/src/state/store.js
|
||||
tests/node/verify_linuxcnc_kinematics_runtime.mjs
|
||||
tests/node/verify_rtcp_store.mjs
|
||||
tests/node/verify_profile_boundary.mjs
|
||||
docs/program-implementation-guide.md
|
||||
docs/development-continuation.md
|
||||
docs/traceability-matrix.md
|
||||
Feature:
|
||||
将 RTCP frame/boundary adapter 从 fixture-only 接到
|
||||
createLinuxCncKinematicsSdk({ moduleId: "xyzac-trt" })。
|
||||
新增 web runtime adapter,只加载共享 wasm-port SDK 并返回 LinuxCNC
|
||||
forward/inverse 结果;frame builder 使用 forward.pose 和 inverse.joints
|
||||
生成 source-derived kinematics frame。
|
||||
LinuxCNC references:
|
||||
wasm-port/runtime/sdk/src/linuxcnc-kinematics.js
|
||||
wasm-port/build/wasm/kinematics/linuxcnc_xyzac_trt_kinematics.wasm
|
||||
src/emc/kinematics/xyzac-trt-kins.c
|
||||
src/emc/kinematics/trtfuncs.c
|
||||
src/emc/kinematics/switchkins.c
|
||||
Boundary:
|
||||
sourceMode=source-derived-kinematics-wasm
|
||||
semanticBoundary=linuxcnc_kinematics_wasm_c_abi
|
||||
linuxCncKinematicsReady=true
|
||||
kinematicsFramePromotionAllowed=true
|
||||
fullLinuxCncProgramExecutionReady=false
|
||||
browserFallback=fixture-ui-only
|
||||
Tests:
|
||||
npm --prefix web-rtcp-5axis-sim-plan/app run build
|
||||
npm --prefix web-rtcp-5axis-sim-plan/app run smoke:node
|
||||
npm --prefix web-rtcp-5axis-sim-plan/app run smoke
|
||||
Result:
|
||||
gmoccapy_static_build=ok
|
||||
linuxcnc_kinematics_runtime_smoke=ok
|
||||
rtcp_store_smoke=ok
|
||||
profile_boundary_smoke=ok
|
||||
gmoccapy_shell_smoke=ok
|
||||
Remaining risk:
|
||||
Browser path still runs fixture fallback because kinematics WASM asset copy/worker
|
||||
is not wired into app/src/main.js. Program RUN/STEP still advances fixture lines;
|
||||
LinuxCNC interpreter/remap/planner execution is not promoted.
|
||||
Next:
|
||||
browser_kinematics_wasm_asset_worker_or_interpreter_execution_source
|
||||
```
|
||||
315
web-rtcp-5axis-sim-plan/tests/browser/gmoccapy_shell_smoke.html
Normal file
@@ -0,0 +1,315 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>gmoccapy shell browser smoke</title>
|
||||
</head>
|
||||
<body>
|
||||
<pre id="result">gmoccapy_shell_smoke=pending</pre>
|
||||
<iframe id="app-frame" src="../../app/index.html" title="gmoccapy shell"></iframe>
|
||||
<script type="module">
|
||||
const result = document.querySelector("#result");
|
||||
const frame = document.querySelector("#app-frame");
|
||||
|
||||
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
async function runSmoke() {
|
||||
await new Promise((resolve, reject) => {
|
||||
frame.addEventListener("load", resolve, { once: true });
|
||||
frame.addEventListener("error", reject, { once: true });
|
||||
});
|
||||
await wait(250);
|
||||
|
||||
const doc = frame.contentDocument;
|
||||
const win = frame.contentWindow;
|
||||
const regions = [
|
||||
"titlebar",
|
||||
"preview",
|
||||
"dro",
|
||||
"gcode",
|
||||
"status-sidebar",
|
||||
"info-tabs",
|
||||
"override",
|
||||
"spindle-coolant",
|
||||
"bottom-controls",
|
||||
];
|
||||
|
||||
for (const region of regions) {
|
||||
const element = doc.querySelector(`[data-region="${region}"]`);
|
||||
if (!element) {
|
||||
throw new Error(`missing region: ${region}`);
|
||||
}
|
||||
if (!element.textContent.trim() && region !== "preview") {
|
||||
throw new Error(`empty region: ${region}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!doc.querySelector(".machine-preview")) {
|
||||
throw new Error("missing machine preview");
|
||||
}
|
||||
let canvas = doc.querySelector("[data-five-axis-canvas]");
|
||||
if (!canvas) {
|
||||
throw new Error("missing Three.js preview canvas");
|
||||
}
|
||||
if (
|
||||
canvas.dataset.threeReady !== "true" ||
|
||||
canvas.dataset.threeFrameApi !== "web-rtcp-5axis-motion-frame" ||
|
||||
Number(canvas.dataset.threePathPoints ?? 0) < 64 ||
|
||||
Number(canvas.dataset.threeSceneObjects ?? 0) < 12 ||
|
||||
!canvas.dataset.threeToolhead ||
|
||||
!canvas.dataset.threeToolAxis ||
|
||||
!canvas.dataset.threeTcpPose
|
||||
) {
|
||||
throw new Error(`Three.js preview did not expose ready render state: ${JSON.stringify(canvas.dataset)}`);
|
||||
}
|
||||
assertCanvasNonblank(canvas, "initial Three.js preview");
|
||||
if (!doc.querySelector(".dro-row")) {
|
||||
throw new Error("missing DRO rows");
|
||||
}
|
||||
if (!doc.querySelector(".gcode-row.active")) {
|
||||
throw new Error("missing active gcode row");
|
||||
}
|
||||
if (!win.webRtcp5AxisSimulation) {
|
||||
throw new Error("missing public simulation API");
|
||||
}
|
||||
if (win.React || win.Vue || win.angular || win.Svelte) {
|
||||
throw new Error("forbidden frontend framework global detected");
|
||||
}
|
||||
if (doc.querySelector("[data-reactroot], [data-v-app], [ng-version], [svelte]")) {
|
||||
throw new Error("forbidden frontend framework DOM marker detected");
|
||||
}
|
||||
const packageJson = await fetch("../../app/package.json").then((response) => response.json());
|
||||
const dependencyNames = [
|
||||
...Object.keys(packageJson.dependencies || {}),
|
||||
...Object.keys(packageJson.devDependencies || {}),
|
||||
];
|
||||
const forbiddenDependencies = ["react", "vue", "@angular/core", "svelte"];
|
||||
const forbidden = dependencyNames.filter((name) => forbiddenDependencies.includes(name));
|
||||
if (forbidden.length > 0) {
|
||||
throw new Error(`forbidden frontend framework dependency detected: ${forbidden.join(", ")}`);
|
||||
}
|
||||
|
||||
const state = win.webRtcp5AxisSimulation.getState();
|
||||
if (state.sourceMode !== "fixture-ui-only") {
|
||||
throw new Error(`unexpected source mode: ${state.sourceMode}`);
|
||||
}
|
||||
if (state.machineProfile !== "xyzac-trt") {
|
||||
throw new Error(`unexpected profile: ${state.machineProfile}`);
|
||||
}
|
||||
if (state.rtcpFrame?.apiName !== "web-rtcp-5axis-motion-frame") {
|
||||
throw new Error("missing RTCP frame state");
|
||||
}
|
||||
if (state.rtcpFrame?.readiness?.linuxCncKinematicsReady !== false) {
|
||||
throw new Error("fixture RTCP frame must not claim LinuxCNC kinematics readiness");
|
||||
}
|
||||
if (!doc.querySelector('[data-rtcp-value="tcp"]')?.textContent.includes("TCP")) {
|
||||
throw new Error("missing TCP DRO strip");
|
||||
}
|
||||
if (!doc.querySelector('[data-rtcp-diagnostic="boundary"]')?.textContent.includes("fixture_frame_ui_plumbing")) {
|
||||
throw new Error("missing RTCP boundary diagnostic");
|
||||
}
|
||||
if (!doc.querySelector('[data-tool-preview="summary"]')?.textContent.includes("T1")) {
|
||||
throw new Error("missing tool preview summary");
|
||||
}
|
||||
if (!doc.querySelector('[data-action="OPEN_FILE"]')) {
|
||||
throw new Error("missing G-code file input");
|
||||
}
|
||||
if (!doc.querySelector('[data-machine-state="summary"]')?.textContent.includes("power off")) {
|
||||
throw new Error("initial machine state should show power off");
|
||||
}
|
||||
|
||||
win.webRtcp5AxisSimulation.dispatch({ type: "RUN" });
|
||||
await wait(50);
|
||||
if (!win.webRtcp5AxisSimulation.getState().operatorMessage.includes("blocked")) {
|
||||
throw new Error("RUN should be blocked before power on");
|
||||
}
|
||||
doc.querySelector('[data-action="power"]').click();
|
||||
await wait(50);
|
||||
if (win.webRtcp5AxisSimulation.getState().machine.powerOn !== true) {
|
||||
throw new Error("POWER action did not turn machine on");
|
||||
}
|
||||
if (!doc.querySelector('[data-machine-state="summary"]')?.textContent.includes("power on")) {
|
||||
throw new Error("machine state did not render power on");
|
||||
}
|
||||
|
||||
win.webRtcp5AxisSimulation.dispatch({
|
||||
type: "LOAD_PROGRAM",
|
||||
filename: "operator-demo.ngc",
|
||||
content: [
|
||||
"G0 X0 Y0 Z0",
|
||||
"G1 X10 F100",
|
||||
"G1 Y10",
|
||||
"G1 X0",
|
||||
"G1 Y0",
|
||||
"G0 Z5",
|
||||
"M5",
|
||||
"M30",
|
||||
].join("\n"),
|
||||
});
|
||||
await wait(50);
|
||||
if (win.webRtcp5AxisSimulation.getState().activeProgram !== "operator-demo.ngc") {
|
||||
throw new Error("LOAD_PROGRAM did not update active program");
|
||||
}
|
||||
if (doc.querySelector('[data-active-program-line]')?.textContent !== "Current line 1") {
|
||||
throw new Error("loaded program did not render current line 1");
|
||||
}
|
||||
if (doc.querySelector(".gcode-row.active")?.dataset.programLine !== "1") {
|
||||
throw new Error("loaded program active row should be line 1");
|
||||
}
|
||||
|
||||
win.webRtcp5AxisSimulation.dispatch({ type: "RUN" });
|
||||
await wait(50);
|
||||
if (win.webRtcp5AxisSimulation.getState().runState !== "running") {
|
||||
throw new Error("RUN action did not update state after power on");
|
||||
}
|
||||
if (doc.querySelector(".gcode-row.active")?.dataset.programLine !== "6") {
|
||||
throw new Error("RUN did not highlight the executing current line");
|
||||
}
|
||||
const tcpButton = doc.querySelector('[data-action="kins-tcp"]');
|
||||
tcpButton.click();
|
||||
await wait(50);
|
||||
const rtcpState = win.webRtcp5AxisSimulation.getState();
|
||||
if (rtcpState.rtcpState !== "on" || rtcpState.kinsType !== "tcp-xyzac") {
|
||||
throw new Error(`TCP mode did not enable RTCP: ${rtcpState.rtcpState}/${rtcpState.kinsType}`);
|
||||
}
|
||||
if (!doc.querySelector('[data-rtcp-value="state"]')?.textContent.includes("RTCP on")) {
|
||||
throw new Error("RTCP DRO strip did not render enabled state");
|
||||
}
|
||||
if (!doc.querySelector('[data-rtcp-diagnostic="frame"]')?.textContent.includes("on")) {
|
||||
throw new Error("RTCP diagnostics did not render enabled frame");
|
||||
}
|
||||
if (canvas.dataset.threeRtcpState !== "on") {
|
||||
throw new Error("Three.js preview did not consume RTCP enabled frame");
|
||||
}
|
||||
if (doc.querySelector('[data-rtcp-diagnostic="kinematics-ready"]')?.textContent !== "pending") {
|
||||
throw new Error("RTCP diagnostics must keep LinuxCNC kinematics pending for fixture mode");
|
||||
}
|
||||
if (!doc.querySelector('[data-linuxcnc-boundary="adapter"]')?.textContent.includes("linuxcnc-boundary-adapter")) {
|
||||
throw new Error("missing LinuxCNC boundary adapter diagnostic");
|
||||
}
|
||||
if (!doc.querySelector('[data-linuxcnc-boundary="panel"]')?.textContent.includes("xyzac-trt-switchkins-pyvcp")) {
|
||||
throw new Error("missing PyVCP panel schema diagnostic");
|
||||
}
|
||||
if (!doc.querySelector('[data-linuxcnc-boundary="profile-summary"]')?.textContent.includes("XYZAC / 5 joints / 10 tools")) {
|
||||
throw new Error("missing LinuxCNC profile summary diagnostic");
|
||||
}
|
||||
if (!doc.querySelector('[data-linuxcnc-boundary="readiness"]')?.textContent.includes("blocked")) {
|
||||
throw new Error("LinuxCNC boundary readiness should remain blocked");
|
||||
}
|
||||
canvas = doc.querySelector("[data-five-axis-canvas]");
|
||||
const tcpPoseBeforeStep = canvas.dataset.threeTcpPose;
|
||||
win.webRtcp5AxisSimulation.dispatch({ type: "STEP" });
|
||||
await wait(50);
|
||||
if (win.webRtcp5AxisSimulation.getState().activeLine <= rtcpState.activeLine) {
|
||||
throw new Error("STEP did not advance RTCP frame line");
|
||||
}
|
||||
canvas = doc.querySelector("[data-five-axis-canvas]");
|
||||
if (canvas.dataset.threeTcpPose === tcpPoseBeforeStep) {
|
||||
throw new Error("Three.js preview did not update TCP pose after STEP");
|
||||
}
|
||||
assertCanvasNonblank(canvas, "updated Three.js preview");
|
||||
doc.querySelector('[data-action="mode-jog"]').click();
|
||||
await wait(50);
|
||||
const xBeforeJog = win.webRtcp5AxisSimulation.getState().axisPose.x;
|
||||
doc.querySelector('[data-action="JOG_X_POS"]').click();
|
||||
await wait(50);
|
||||
if (win.webRtcp5AxisSimulation.getState().axisPose.x <= xBeforeJog) {
|
||||
throw new Error("JOG X+ did not move the axis");
|
||||
}
|
||||
doc.querySelector('[data-action="mode-mdi"]').click();
|
||||
doc.querySelector('[data-action="MDI_RUN"]').click();
|
||||
await wait(50);
|
||||
if (win.webRtcp5AxisSimulation.getState().runState !== "mdi") {
|
||||
throw new Error("MDI action did not update run state");
|
||||
}
|
||||
doc.querySelector('[data-action="reset"]').click();
|
||||
await wait(50);
|
||||
if (win.webRtcp5AxisSimulation.getState().runState !== "idle") {
|
||||
throw new Error("RESET did not return to idle");
|
||||
}
|
||||
doc.querySelector('[data-action="feed-override-down"]').click();
|
||||
await wait(50);
|
||||
if (win.webRtcp5AxisSimulation.getState().feed.feedOverride !== 90) {
|
||||
throw new Error("feed override button did not update state");
|
||||
}
|
||||
if (doc.querySelector('[data-value="feed-override"]')?.textContent.trim() !== "90 %") {
|
||||
throw new Error("feed override DOM did not update");
|
||||
}
|
||||
doc.querySelector('[data-action="rapid-override-up"]').click();
|
||||
await wait(50);
|
||||
if (win.webRtcp5AxisSimulation.getState().feed.rapidOverride !== 110) {
|
||||
throw new Error("rapid override button did not update state");
|
||||
}
|
||||
doc.querySelector('[data-action="spindle-override-up"]').click();
|
||||
await wait(50);
|
||||
if (win.webRtcp5AxisSimulation.getState().spindle.override !== 110) {
|
||||
throw new Error("spindle override button did not update state");
|
||||
}
|
||||
doc.querySelector('[data-action="toggle-flood"]').click();
|
||||
doc.querySelector('[data-action="toggle-mist"]').click();
|
||||
await wait(50);
|
||||
const coolantState = win.webRtcp5AxisSimulation.getState().coolant;
|
||||
if (coolantState.flood !== false || coolantState.mist !== true) {
|
||||
throw new Error("coolant buttons did not update state");
|
||||
}
|
||||
doc.querySelector('[data-action="view-x"]').click();
|
||||
await wait(50);
|
||||
if (win.webRtcp5AxisSimulation.getState().preview.selectedView !== "x") {
|
||||
throw new Error("preview view button did not update state");
|
||||
}
|
||||
doc.querySelector('[data-action="clear-preview"]').click();
|
||||
await wait(50);
|
||||
if (win.webRtcp5AxisSimulation.getState().preview.pathPoints !== 0) {
|
||||
throw new Error("clear preview button did not update path points");
|
||||
}
|
||||
doc.querySelector('[data-action="RELOAD"]').click();
|
||||
await wait(50);
|
||||
if (win.webRtcp5AxisSimulation.getState().preview.pathPoints !== 8) {
|
||||
throw new Error("reload button did not restore path points");
|
||||
}
|
||||
doc.querySelector('[data-action="FULL"]').click();
|
||||
await wait(50);
|
||||
if (win.webRtcp5AxisSimulation.getState().preview.fullscreen !== true) {
|
||||
throw new Error("fullscreen button did not update state");
|
||||
}
|
||||
doc.querySelector('[data-action="HOME"]').click();
|
||||
await wait(50);
|
||||
const homedState = win.webRtcp5AxisSimulation.getState();
|
||||
if (homedState.axisPose.x !== 43) {
|
||||
throw new Error("home button did not restore fixture origin");
|
||||
}
|
||||
doc.querySelector('[data-action="estop"]').click();
|
||||
await wait(50);
|
||||
if (win.webRtcp5AxisSimulation.getState().runState !== "estopped") {
|
||||
throw new Error("E-STOP did not update run state");
|
||||
}
|
||||
|
||||
result.textContent = "gmoccapy_shell_smoke=ok";
|
||||
}
|
||||
|
||||
runSmoke().catch((error) => {
|
||||
result.textContent = `gmoccapy_shell_smoke=fail ${error.message}`;
|
||||
});
|
||||
|
||||
function assertCanvasNonblank(canvas, context) {
|
||||
const gl = canvas.getContext("webgl2") || canvas.getContext("webgl");
|
||||
if (!gl) {
|
||||
throw new Error(`${context}: missing WebGL context`);
|
||||
}
|
||||
const pixel = new Uint8Array(4);
|
||||
gl.readPixels(
|
||||
Math.floor(canvas.width / 2),
|
||||
Math.floor(canvas.height / 2),
|
||||
1,
|
||||
1,
|
||||
gl.RGBA,
|
||||
gl.UNSIGNED_BYTE,
|
||||
pixel,
|
||||
);
|
||||
if (pixel[0] === 0 && pixel[1] === 0 && pixel[2] === 0 && pixel[3] === 0) {
|
||||
throw new Error(`${context}: center pixel was blank`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
74
web-rtcp-5axis-sim-plan/tests/browser/verify_gmoccapy_shell_browser.sh
Executable file
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
|
||||
CHROMIUM="${CHROMIUM:-$(command -v chromium || command -v chromium-browser || command -v google-chrome || command -v google-chrome-stable || true)}"
|
||||
|
||||
if [[ -z "$CHROMIUM" ]]; then
|
||||
echo "missing Chromium-compatible browser; set CHROMIUM=/path/to/browser" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
PORT_FILE="$TMP_DIR/port"
|
||||
SERVER_LOG="$TMP_DIR/server.log"
|
||||
CHROME_PROFILE="$TMP_DIR/chrome-profile"
|
||||
mkdir -p "$CHROME_PROFILE"
|
||||
|
||||
cleanup() {
|
||||
if [[ -n "${SERVER_PID:-}" ]]; then
|
||||
kill "$SERVER_PID" 2>/dev/null || true
|
||||
wait "$SERVER_PID" 2>/dev/null || true
|
||||
fi
|
||||
rm -rf "$TMP_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
python3 - <<'PY' "$ROOT_DIR" "$PORT_FILE" >"$SERVER_LOG" 2>&1 &
|
||||
import functools
|
||||
import http.server
|
||||
import pathlib
|
||||
import socketserver
|
||||
import sys
|
||||
|
||||
root = pathlib.Path(sys.argv[1])
|
||||
port_file = pathlib.Path(sys.argv[2])
|
||||
|
||||
handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory=str(root))
|
||||
with socketserver.TCPServer(("127.0.0.1", 0), handler) as httpd:
|
||||
port_file.write_text(str(httpd.server_address[1]), encoding="ascii")
|
||||
httpd.serve_forever()
|
||||
PY
|
||||
SERVER_PID=$!
|
||||
|
||||
for _ in $(seq 1 100); do
|
||||
[[ -s "$PORT_FILE" ]] && break
|
||||
sleep 0.05
|
||||
done
|
||||
|
||||
if [[ ! -s "$PORT_FILE" ]]; then
|
||||
echo "gmoccapy shell browser smoke HTTP server did not start" >&2
|
||||
cat "$SERVER_LOG" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PORT="$(cat "$PORT_FILE")"
|
||||
URL="http://127.0.0.1:$PORT/web-rtcp-5axis-sim-plan/tests/browser/gmoccapy_shell_smoke.html"
|
||||
OUT="$TMP_DIR/chromium-gmoccapy-shell.out"
|
||||
|
||||
"$CHROMIUM" \
|
||||
--headless=new \
|
||||
--disable-gpu \
|
||||
--no-sandbox \
|
||||
--user-data-dir="$CHROME_PROFILE" \
|
||||
--virtual-time-budget=10000 \
|
||||
--dump-dom \
|
||||
"$URL" >"$OUT" 2>&1
|
||||
|
||||
if ! grep -Fq "gmoccapy_shell_smoke=ok" "$OUT"; then
|
||||
echo "gmoccapy shell browser smoke failed" >&2
|
||||
sed -n '1,260p' "$OUT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "gmoccapy_shell_smoke=ok"
|
||||
@@ -0,0 +1,50 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { createLinuxCncKinematicsRuntime } from "../../app/src/runtime/linuxcnc-kinematics-runtime.js";
|
||||
import { buildRtcpFrame } from "../../app/src/runtime/rtcp-frame.js";
|
||||
|
||||
const runtime = await createLinuxCncKinematicsRuntime({ moduleId: "xyzac-trt" });
|
||||
const readiness = runtime.readiness();
|
||||
|
||||
assert.equal(runtime.apiName, "web-rtcp-5axis-linuxcnc-kinematics-runtime");
|
||||
assert.equal(runtime.moduleId, "xyzac-trt");
|
||||
assert.equal(runtime.loaded, true);
|
||||
assert.equal(runtime.sourceMode, "source-derived-kinematics-wasm");
|
||||
assert.equal(runtime.semanticBoundary, "linuxcnc_kinematics_wasm_c_abi");
|
||||
assert.equal(runtime.wasmFile, "linuxcnc_xyzac_trt_kinematics.wasm");
|
||||
assert.equal(readiness.loaded, true);
|
||||
assert.equal(readiness.supportedModules.includes("xyzac-trt"), true);
|
||||
assert.equal(runtime.switchRc, 0);
|
||||
|
||||
const linuxCncKinematicsResult = runtime.frameForJoints([10, 20, 30, 25, 40]);
|
||||
assert.equal(linuxCncKinematicsResult.moduleId, "xyzac-trt");
|
||||
assert.equal(linuxCncKinematicsResult.forward.rc, 0);
|
||||
assert.equal(linuxCncKinematicsResult.inverse.rc, 0);
|
||||
assert.deepEqual(
|
||||
linuxCncKinematicsResult.inverse.joints.map((value) => Math.round(value * 1e6) / 1e6),
|
||||
[10, 20, 30, 25, 40],
|
||||
);
|
||||
|
||||
const frame = buildRtcpFrame({
|
||||
axisPose: { x: 10, y: 20, z: 30, a: 25, b: 0, c: 40 },
|
||||
activeLine: 777,
|
||||
kinsType: "tcp-xyzac",
|
||||
rtcpEnabled: true,
|
||||
linuxCncKinematicsResult,
|
||||
});
|
||||
|
||||
assert.equal(frame.sourceMode, "source-derived-kinematics-wasm");
|
||||
assert.equal(frame.semanticBoundary, "linuxcnc_kinematics_wasm_c_abi");
|
||||
assert.equal(frame.readiness.linuxCncKinematicsReady, true);
|
||||
assert.equal(frame.readiness.promotionAllowed, true);
|
||||
assert.equal(frame.readiness.fullLinuxCncProgramExecutionReady, false);
|
||||
assert.equal(frame.kinematicsModuleId, "xyzac-trt");
|
||||
assert.equal(frame.kinematicsForwardRc, 0);
|
||||
assert.equal(frame.kinematicsInverseRc, 0);
|
||||
assert.equal(frame.jointPose[3].value, 25);
|
||||
assert.equal(frame.jointPose[4].value, 40);
|
||||
assert.equal(frame.tcpPose.x, linuxCncKinematicsResult.forward.pose.x);
|
||||
assert.equal(frame.tcpPose.y, linuxCncKinematicsResult.forward.pose.y);
|
||||
assert.equal(frame.tcpPose.z, linuxCncKinematicsResult.forward.pose.z);
|
||||
|
||||
console.log("linuxcnc_kinematics_runtime_smoke=ok");
|
||||
@@ -0,0 +1,93 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { xyzacTrtProfile } from "../../app/src/profiles/xyzac-trt.js";
|
||||
import { createPyvcpHalBindingSummary, xyzacTrtPyvcpPanelSchema } from "../../app/src/panel-schema/xyzac-trt-pyvcp.js";
|
||||
import { createLinuxCncBoundaryAdapter, createLinuxCncBoundaryReadiness } from "../../app/src/runtime/linuxcnc-boundary-adapter.js";
|
||||
import { createLinuxCncKinematicsRuntime } from "../../app/src/runtime/linuxcnc-kinematics-runtime.js";
|
||||
import { createProfileSourceReferenceSummary } from "../../app/src/profiles/source-reference-map.js";
|
||||
|
||||
assert.equal(xyzacTrtProfile.id, "xyzac-trt");
|
||||
assert.equal(xyzacTrtProfile.iniPath.endsWith("xyzac-trt.ini"), true);
|
||||
assert.deepEqual(xyzacTrtProfile.coordinates, ["X", "Y", "Z", "A", "C"]);
|
||||
assert.equal(xyzacTrtProfile.kinematics, "xyzac-trt-kins");
|
||||
assert.equal(xyzacTrtProfile.machineName, "sim-xyzac-trt-kins (switchkins)");
|
||||
assert.equal(xyzacTrtProfile.display.jogAxes.join(""), "XYZC");
|
||||
assert.equal(xyzacTrtProfile.rs274ngc.halPinVars, true);
|
||||
assert.equal(xyzacTrtProfile.traj.coordinates, "XYZAC");
|
||||
assert.equal(xyzacTrtProfile.axisLimits.A.max, 50);
|
||||
assert.equal(xyzacTrtProfile.jointConfig.length, 5);
|
||||
assert.equal(xyzacTrtProfile.halui.mdiCommands.join(","), "M429,M428,M430");
|
||||
assert.equal(xyzacTrtProfile.hal.halcmd.switchkinsSelectNet.target, "motion.switchkins-type");
|
||||
assert.equal(xyzacTrtProfile.hal.halcmd.feedbackNets.length, 5);
|
||||
assert.equal(xyzacTrtProfile.hal.halcmd.offsetNets.length, 4);
|
||||
assert.equal(xyzacTrtProfile.toolTable.toolCount, 10);
|
||||
assert.equal(xyzacTrtProfile.toolTable.tools[1].zOffset, 15);
|
||||
assert.equal(xyzacTrtProfile.kinematicsParameters.sparm, "identityfirst");
|
||||
assert.equal(xyzacTrtProfile.remaps.map((remap) => remap.code).join(","), "M428,M429,M430");
|
||||
assert.equal(xyzacTrtProfile.remaps.every((remap) => remap.analogOutputIndex === 3), true);
|
||||
assert.equal(xyzacTrtProfile.halPins.includes("motion.switchkins-type"), true);
|
||||
assert.equal(xyzacTrtProfile.halPins.includes("xyzac-trt-kins.tool-offset"), true);
|
||||
assert.equal(xyzacTrtProfile.promotionAllowed, false);
|
||||
assert.equal(xyzacTrtProfile.linuxCncKinematicsReady, false);
|
||||
|
||||
const sourceSummary = createProfileSourceReferenceSummary("xyzac-trt");
|
||||
assert.equal(sourceSummary.referenceCount >= 8, true);
|
||||
assert.equal(sourceSummary.kinds.includes("tool_table"), true);
|
||||
assert.equal(sourceSummary.sourceRequiredCount >= 3, true);
|
||||
assert.equal(sourceSummary.promotionAllowed, false);
|
||||
assert.equal(sourceSummary.semanticBoundary, "profile_source_map_only_not_runtime_proof");
|
||||
assert.ok(sourceSummary.references.some((reference) => reference.path.endsWith("xyzac-trt.ini")));
|
||||
assert.ok(sourceSummary.references.some((reference) => reference.path.endsWith("trtfuncs.c")));
|
||||
|
||||
const panelSummary = createPyvcpHalBindingSummary(xyzacTrtPyvcpPanelSchema);
|
||||
assert.equal(panelSummary.schemaId, "xyzac-trt-switchkins-pyvcp");
|
||||
assert.equal(panelSummary.controlCount, 5);
|
||||
assert.equal(panelSummary.buttonCount, 4);
|
||||
assert.deepEqual(panelSummary.mdiCommands, ["M429", "M428", "M430"]);
|
||||
assert.ok(panelSummary.halNets.some((net) => net.target === "halui.mdi-command-01"));
|
||||
assert.equal(panelSummary.promotionAllowed, false);
|
||||
|
||||
const adapter = createLinuxCncBoundaryAdapter();
|
||||
assert.equal(adapter.apiName, "web-rtcp-5axis-linuxcnc-boundary-adapter");
|
||||
assert.equal(adapter.profileId, "xyzac-trt");
|
||||
assert.equal(adapter.runtimeReady, false);
|
||||
assert.equal(adapter.kinematicsRuntimeReady, false);
|
||||
assert.equal(adapter.interpreterRuntimeReady, false);
|
||||
assert.equal(adapter.linuxCncKinematicsReady, false);
|
||||
assert.equal(adapter.promotionAllowed, false);
|
||||
assert.equal(adapter.profileSummary.coordinates, "XYZAC");
|
||||
assert.equal(adapter.profileSummary.jointCount, 5);
|
||||
assert.equal(adapter.profileSummary.mdiCommandCount, 3);
|
||||
assert.equal(adapter.profileSummary.toolCount, 10);
|
||||
assert.equal(adapter.profileSummary.feedbackNetCount, 5);
|
||||
assert.equal(adapter.adapterPoints.pyvcpSchemaId, "xyzac-trt-switchkins-pyvcp");
|
||||
assert.equal(adapter.adapterPoints.halPins.includes("halui.mdi-command-00"), true);
|
||||
|
||||
const readiness = createLinuxCncBoundaryReadiness(adapter);
|
||||
assert.equal(readiness.ready, false);
|
||||
assert.equal(readiness.linuxCncKinematicsReady, false);
|
||||
assert.equal(readiness.promotionAllowed, false);
|
||||
assert.ok(readiness.missing.includes("kinematics runtime"));
|
||||
assert.ok(readiness.missing.includes("interpreter/remap runtime"));
|
||||
|
||||
const runtime = await createLinuxCncKinematicsRuntime({ moduleId: "xyzac-trt" });
|
||||
const kinematicsAdapter = createLinuxCncBoundaryAdapter({
|
||||
runtime: {
|
||||
kinematicsWasm: runtime.readiness(),
|
||||
interpreterWasm: null,
|
||||
},
|
||||
});
|
||||
const kinematicsReadiness = createLinuxCncBoundaryReadiness(kinematicsAdapter);
|
||||
assert.equal(kinematicsAdapter.runtimeReady, false);
|
||||
assert.equal(kinematicsAdapter.kinematicsRuntimeReady, true);
|
||||
assert.equal(kinematicsAdapter.interpreterRuntimeReady, false);
|
||||
assert.equal(kinematicsAdapter.linuxCncKinematicsReady, true);
|
||||
assert.equal(kinematicsAdapter.promotionAllowed, true);
|
||||
assert.equal(kinematicsAdapter.fullLinuxCncProgramExecutionReady, false);
|
||||
assert.equal(kinematicsAdapter.semanticBoundary, "linuxcnc_kinematics_wasm_runtime_connected");
|
||||
assert.equal(kinematicsReadiness.ready, true);
|
||||
assert.equal(kinematicsReadiness.linuxCncKinematicsReady, true);
|
||||
assert.equal(kinematicsReadiness.fullLinuxCncProgramExecutionReady, false);
|
||||
assert.ok(kinematicsReadiness.missing.includes("interpreter/remap runtime"));
|
||||
|
||||
console.log("profile_boundary_smoke=ok");
|
||||
214
web-rtcp-5axis-sim-plan/tests/node/verify_rtcp_store.mjs
Normal file
@@ -0,0 +1,214 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { createLinuxCncKinematicsRuntime } from "../../app/src/runtime/linuxcnc-kinematics-runtime.js";
|
||||
import { buildRtcpFrame } from "../../app/src/runtime/rtcp-frame.js";
|
||||
import { createSimulationStore } from "../../app/src/state/store.js";
|
||||
|
||||
const identityFrame = buildRtcpFrame({
|
||||
axisPose: { x: 43, y: -32.15, z: -11.306, a: 0, b: 0, c: 0 },
|
||||
activeLine: 501,
|
||||
kinsType: "identity",
|
||||
rtcpEnabled: false,
|
||||
});
|
||||
|
||||
assert.equal(identityFrame.apiName, "web-rtcp-5axis-motion-frame");
|
||||
assert.equal(identityFrame.rtcpState, "off");
|
||||
assert.equal(identityFrame.readiness.frameReady, true);
|
||||
assert.equal(identityFrame.readiness.linuxCncKinematicsReady, false);
|
||||
assert.equal(identityFrame.readiness.promotionAllowed, false);
|
||||
assert.equal(identityFrame.semanticBoundary, "fixture_frame_ui_plumbing_not_linuxcnc_kinematics_proof");
|
||||
assert.deepEqual(identityFrame.compensation, { x: 0, y: 0, z: 0 });
|
||||
|
||||
const tcpFrame = buildRtcpFrame({
|
||||
axisPose: { x: 43, y: -32.15, z: -11.306, a: 15, b: 0, c: 30 },
|
||||
activeLine: 502,
|
||||
kinsType: "tcp-xyzac",
|
||||
rtcpEnabled: true,
|
||||
});
|
||||
|
||||
assert.equal(tcpFrame.rtcpState, "on");
|
||||
assert.equal(tcpFrame.kinsType, "tcp-xyzac");
|
||||
assert.notEqual(tcpFrame.compensation.z, 0);
|
||||
assert.ok(Number.isFinite(tcpFrame.tcpPose.x));
|
||||
assert.ok(Number.isFinite(tcpFrame.toolAxisVector.z));
|
||||
assert.equal(tcpFrame.jointPose.length, 5);
|
||||
|
||||
const runtime = await createLinuxCncKinematicsRuntime({ moduleId: "xyzac-trt" });
|
||||
const linuxCncKinematicsResult = runtime.frameForJoints([10, 20, 30, 25, 40]);
|
||||
const linuxCncFrame = buildRtcpFrame({
|
||||
axisPose: { x: 10, y: 20, z: 30, a: 25, b: 0, c: 40 },
|
||||
activeLine: 503,
|
||||
kinsType: "tcp-xyzac",
|
||||
rtcpEnabled: true,
|
||||
linuxCncKinematicsResult,
|
||||
});
|
||||
assert.equal(linuxCncFrame.sourceMode, "source-derived-kinematics-wasm");
|
||||
assert.equal(linuxCncFrame.semanticBoundary, "linuxcnc_kinematics_wasm_c_abi");
|
||||
assert.equal(linuxCncFrame.readiness.linuxCncKinematicsReady, true);
|
||||
assert.equal(linuxCncFrame.readiness.promotionAllowed, true);
|
||||
assert.equal(linuxCncFrame.readiness.fullLinuxCncProgramExecutionReady, false);
|
||||
assert.equal(linuxCncFrame.kinematicsModuleId, "xyzac-trt");
|
||||
assert.equal(linuxCncFrame.jointPose[3].value, 25);
|
||||
assert.equal(linuxCncFrame.jointPose[4].value, 40);
|
||||
|
||||
const fixtureInitialStore = createSimulationStore();
|
||||
assert.equal(fixtureInitialStore.getState().rtcpState, "off");
|
||||
assert.equal(fixtureInitialStore.getState().machine.powerOn, false);
|
||||
assert.equal(fixtureInitialStore.getState().machine.mode, "manual");
|
||||
assert.equal(
|
||||
fixtureInitialStore.getState().linuxCncBoundaryAdapter.apiName,
|
||||
"web-rtcp-5axis-linuxcnc-boundary-adapter",
|
||||
);
|
||||
assert.equal(fixtureInitialStore.getState().linuxCncBoundaryAdapter.panelSummary.schemaId, "xyzac-trt-switchkins-pyvcp");
|
||||
assert.equal(fixtureInitialStore.getState().linuxCncBoundaryAdapter.sourceSummary.referenceCount >= 8, true);
|
||||
assert.equal(fixtureInitialStore.getState().linuxCncBoundaryAdapter.profileSummary.toolCount, 10);
|
||||
assert.equal(fixtureInitialStore.getState().linuxCncBoundaryAdapter.profileSummary.coordinates, "XYZAC");
|
||||
assert.equal(fixtureInitialStore.getState().linuxCncBoundaryReadiness.ready, false);
|
||||
assert.equal(fixtureInitialStore.getState().linuxCncBoundaryReadiness.promotionAllowed, false);
|
||||
|
||||
const kinematicsStore = createSimulationStore();
|
||||
kinematicsStore.dispatch({ type: "ATTACH_KINEMATICS_RUNTIME", runtime });
|
||||
let kinematicsState = kinematicsStore.getState();
|
||||
assert.equal(kinematicsState.linuxCncBoundaryAdapter.linuxCncKinematicsReady, true);
|
||||
assert.equal(kinematicsState.linuxCncBoundaryAdapter.interpreterRuntimeReady, false);
|
||||
assert.equal(kinematicsState.linuxCncBoundaryReadiness.ready, true);
|
||||
assert.equal(kinematicsState.linuxCncBoundaryReadiness.fullLinuxCncProgramExecutionReady, false);
|
||||
assert.equal(kinematicsState.linuxCncBoundaryReadiness.missing.includes("interpreter/remap runtime"), true);
|
||||
assert.equal(kinematicsState.rtcpFrame.sourceMode, "source-derived-kinematics-wasm");
|
||||
assert.equal(kinematicsState.rtcpFrame.semanticBoundary, "linuxcnc_kinematics_wasm_c_abi");
|
||||
assert.equal(kinematicsState.rtcpFrame.readiness.linuxCncKinematicsReady, true);
|
||||
|
||||
kinematicsStore.dispatch({ type: "SET_RTCP", enabled: true });
|
||||
kinematicsState = kinematicsStore.getState();
|
||||
assert.equal(kinematicsState.rtcpState, "on");
|
||||
assert.equal(kinematicsState.rtcpFrame.sourceMode, "source-derived-kinematics-wasm");
|
||||
assert.equal(kinematicsState.lastKinematicsResult.moduleId, "xyzac-trt");
|
||||
assert.equal(kinematicsState.dro.tcpX, kinematicsState.rtcpFrame.tcpPose.x);
|
||||
|
||||
kinematicsStore.dispatch({ type: "TOGGLE_POWER" });
|
||||
kinematicsStore.dispatch({ type: "STEP" });
|
||||
kinematicsState = kinematicsStore.getState();
|
||||
assert.equal(kinematicsState.runState, "stepping");
|
||||
assert.equal(kinematicsState.rtcpFrame.sourceMode, "source-derived-kinematics-wasm");
|
||||
assert.equal(kinematicsState.rtcpFrame.readiness.fullLinuxCncProgramExecutionReady, false);
|
||||
|
||||
kinematicsStore.dispatch({ type: "SET_FRAME_SOURCE", sourceMode: "fixture-ui-only" });
|
||||
kinematicsState = kinematicsStore.getState();
|
||||
assert.equal(kinematicsState.rtcpFrame.sourceMode, "fixture-ui-only");
|
||||
assert.equal(kinematicsState.rtcpFrame.readiness.linuxCncKinematicsReady, false);
|
||||
|
||||
const store = createSimulationStore();
|
||||
store.dispatch({ type: "RUN" });
|
||||
assert.equal(store.getState().activeLine, 501);
|
||||
assert.equal(store.getState().operatorMessage, "run blocked: power or estop state");
|
||||
|
||||
store.dispatch({ type: "TOGGLE_POWER" });
|
||||
assert.equal(store.getState().machine.powerOn, true);
|
||||
|
||||
store.dispatch({ type: "SET_RTCP", enabled: true });
|
||||
let state = store.getState();
|
||||
assert.equal(state.rtcpState, "on");
|
||||
assert.equal(state.kinsType, "tcp-xyzac");
|
||||
assert.equal(state.rtcpFrame.readiness.linuxCncKinematicsReady, false);
|
||||
assert.notEqual(state.dro.tcpZ, state.dro.z);
|
||||
|
||||
const previousLine = state.activeLine;
|
||||
const previousTcpX = state.tcpPose.x;
|
||||
store.dispatch({ type: "STEP" });
|
||||
state = store.getState();
|
||||
assert.equal(state.activeLine, previousLine + 1);
|
||||
assert.equal(state.runState, "stepping");
|
||||
assert.notEqual(state.tcpPose.x, previousTcpX);
|
||||
|
||||
store.dispatch({ type: "JOG", axis: "x", direction: 1, increment: 2 });
|
||||
state = store.getState();
|
||||
assert.equal(state.machine.mode, "jog");
|
||||
assert.equal(state.runState, "jogging");
|
||||
assert.equal(Math.round(state.axisPose.x), Math.round(state.rtcpFrame.axisPose.x));
|
||||
|
||||
store.dispatch({ type: "RUN_MDI", command: "G0 X1" });
|
||||
state = store.getState();
|
||||
assert.equal(state.machine.mode, "mdi");
|
||||
assert.equal(state.machine.mdiCommand, "G0 X1");
|
||||
assert.equal(state.runState, "mdi");
|
||||
|
||||
store.dispatch({
|
||||
type: "LOAD_PROGRAM",
|
||||
filename: "operator-demo.ngc",
|
||||
content: "G0 X0 Y0\nG1 X10 F100\nM30\n",
|
||||
});
|
||||
state = store.getState();
|
||||
assert.equal(state.activeProgram, "operator-demo.ngc");
|
||||
assert.equal(state.programSource, "operator-file");
|
||||
assert.equal(state.programStartLine, 1);
|
||||
assert.equal(state.activeLine, 1);
|
||||
assert.equal(state.programLines.length, 3);
|
||||
assert.equal(state.machine.mode, "auto");
|
||||
|
||||
store.dispatch({ type: "RUN" });
|
||||
state = store.getState();
|
||||
assert.equal(state.activeLine, 3);
|
||||
assert.equal(state.runState, "complete");
|
||||
|
||||
store.dispatch({ type: "SET_RTCP", enabled: false });
|
||||
state = store.getState();
|
||||
assert.equal(state.rtcpState, "off");
|
||||
assert.equal(state.kinsType, "identity");
|
||||
assert.equal(state.dro.tcpX, state.dro.x);
|
||||
|
||||
store.dispatch({ type: "ADJUST_OVERRIDE", target: "feed", delta: -10 });
|
||||
state = store.getState();
|
||||
assert.equal(state.feed.feedOverride, 90);
|
||||
assert.equal(state.operatorMessage, "feed override adjusted");
|
||||
|
||||
store.dispatch({ type: "ADJUST_OVERRIDE", target: "rapid", delta: 20 });
|
||||
state = store.getState();
|
||||
assert.equal(state.feed.rapidOverride, 120);
|
||||
|
||||
store.dispatch({ type: "ADJUST_SPINDLE_OVERRIDE", delta: 10 });
|
||||
state = store.getState();
|
||||
assert.equal(state.spindle.override, 110);
|
||||
|
||||
store.dispatch({ type: "TOGGLE_COOLANT", kind: "flood" });
|
||||
state = store.getState();
|
||||
assert.equal(state.coolant.flood, false);
|
||||
|
||||
store.dispatch({ type: "TOGGLE_COOLANT", kind: "mist" });
|
||||
state = store.getState();
|
||||
assert.equal(state.coolant.mist, true);
|
||||
|
||||
store.dispatch({ type: "SET_VIEW", view: "x" });
|
||||
state = store.getState();
|
||||
assert.equal(state.preview.selectedView, "x");
|
||||
|
||||
store.dispatch({ type: "CLEAR_PREVIEW" });
|
||||
state = store.getState();
|
||||
assert.equal(state.preview.pathPoints, 0);
|
||||
|
||||
store.dispatch({ type: "RELOAD_PROGRAM" });
|
||||
state = store.getState();
|
||||
assert.equal(state.preview.pathPoints, 3);
|
||||
assert.equal(state.activeLine, 1);
|
||||
assert.equal(state.runState, "idle");
|
||||
|
||||
store.dispatch({ type: "TOGGLE_FULLSCREEN" });
|
||||
state = store.getState();
|
||||
assert.equal(state.preview.fullscreen, true);
|
||||
|
||||
store.dispatch({ type: "HOME" });
|
||||
state = store.getState();
|
||||
assert.equal(state.axisPose.x, 43);
|
||||
assert.equal(state.operatorMessage, "machine homed to fixture origin");
|
||||
|
||||
store.dispatch({ type: "ESTOP" });
|
||||
state = store.getState();
|
||||
assert.equal(state.machine.powerOn, false);
|
||||
assert.equal(state.machine.estopActive, true);
|
||||
assert.equal(state.runState, "estopped");
|
||||
|
||||
store.dispatch({ type: "RESET" });
|
||||
state = store.getState();
|
||||
assert.equal(state.machine.estopActive, false);
|
||||
assert.equal(state.operatorMessage, "machine reset complete");
|
||||
|
||||
console.log("rtcp_store_smoke=ok");
|
||||