继续完成 web-rtcp-5axis-sim-plan
结论:完成 LinuxCNC kinematics WASM ABI 覆盖,并将 web-rtcp-5axis-sim-plan 的 RTCP frame/boundary adapter 接到 xyzac-trt kinematics SDK;Node、build、browser smoke 验证通过。
This commit is contained in:
13
web-rtcp-5axis-sim-plan/app/index.html
Normal file
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
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
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
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
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
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
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
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
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
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
59470
web-rtcp-5axis-sim-plan/app/src/vendor/three/three.core.js
vendored
Normal file
File diff suppressed because one or more lines are too long
19306
web-rtcp-5axis-sim-plan/app/src/vendor/three/three.module.js
vendored
Normal file
19306
web-rtcp-5axis-sim-plan/app/src/vendor/three/three.module.js
vendored
Normal file
File diff suppressed because one or more lines are too long
203
web-rtcp-5axis-sim-plan/app/src/visualization/five-axis-scene.js
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
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"]
|
||||
}
|
||||
Reference in New Issue
Block a user