Files
linuxcncWebWASM/docs/linuxcnc-web-wasm-dual-canvas-design.md

235 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# LinuxCNC Web/WASM 双 Canvas 预览设计与实施方案
**文档状态**:实施设计 v1.0
**适用案例**:`linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini`(同样适用于 `xyzbc-trt.ini`)
**关联入口**:[`run-5axis-rtcp.sh`](../run-5axis-rtcp.sh)
**现有前端**:[`web/react/src/App.tsx`](../web/react/src/App.tsx)、[`XyzacToolPreview.tsx`](../web/react/src/XyzacToolPreview.tsx)、[`XyzacMachineView.tsx`](../web/react/src/XyzacMachineView.tsx)
**协议类型**:[`web/react/src/protocol.ts`](../web/react/src/protocol.ts)、[`xyzac-visual-adapter.ts`](../web/react/src/xyzac-visual-adapter.ts)
## 1. 背景与目标
当前 AXIS 风格面板的 Preview 区域需要同时表达两种不同的信息:
1. 刀具在工件坐标中的 canonical 程序轨迹,以及执行过程中已经走过和正在执行的实时刀具轨迹;XYZAC 使用 A 轴,XYZBC 使用 B 轴。
2. 由五个关节(table-x、saddle-y、spindle-z、tilt-a、rotate-c)驱动的完整机床三维模型运动。
本方案将 Preview 明确拆为两个相互独立的 `<canvas>`:
* **Canvas A:Toolpath/Simulation**。只绘制程序预览段、快速移动、圆弧/螺旋插补、实时执行轨迹、当前段和刀尖;不绘制机床结构。
* **Canvas B:Machine 3D**。只绘制 `machine_model` 结构、关节层级、刀具、转台、限位盒和当前姿态;可选显示从同一快照投影的当前刀具路径,但默认关闭以避免与 Canvas A 重复。
两个 canvas 不共享 WebGL 场景、相机、动画循环或可变对象。它们共享同一个经过校验的 `XyzacVisualState`,该状态由 LinuxCNC WASM 在 Dedicated Worker 中生成。React 不解析 G-code、不计算 RTCP、不根据墙钟推导姿态。
目标是:在运行、暂停、单步、急停、回零、M428/M429/M430 切换、清除可视轨迹和 Worker 恢复后,两个画面都能用相同的 `simulation_tick`、`state_hash` 和姿态数据重现同一时刻;任一画面缺数据时显示 pending/error,而不是使用本地猜测值。
## 2. 范围与非目标
### 2.1 范围
* XYZAC/XYZBC TRT 案例的 exact-path 启动、配置资源闭包和 RTCP remap。
* canonical 程序轨迹生成(直线、快速、圆弧、螺旋),每段的源行、采样和运动类型。
* 固定 logical tick 的实时运动样本、活动段进度、姿态和五个 joint 值。
* Vismach 模型身份、组件层级、来源文件哈希和关节到模型节点的映射。
* 两个 canvas 的尺寸、生命周期、ResizeObserver、帧丢弃/补快照、相机控制和清理。
* native/WASM/Worker 对照、浏览器验收和恢复测试。
### 2.2 非目标
* 在浏览器中复刻 AXIS 的 Tk 绘图实现或像素级截图。
* 用 JS preview 引擎替代 LinuxCNC RS274/TP/kinematics。
* 通过宿主 LinuxCNC GUI、NML socket、shell 或真实硬件取得位置。
* 在两个 canvas 之间复制一份可写的运动状态。
## 3. `run-5axis-rtcp.sh` 执行链
脚本是 native oracle 和案例资源定义的入口;Web/WASM 必须保留同样的路径和语义。
```text
run-5axis-rtcp.sh [xyzac-trt.ini|xyzbc-trt.ini]
-> 解析 REPO_ROOT/linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting
-> 选择 demo/*.ngc 和 remap_subs/helix_{ac,bc}.ngc
-> 校验 INI、demo、subroutine、M428 和 scripts/linuxcnc
-> cd linuxcnc
-> scripts/rip-environment linuxcnc -r <case>.ini
-> [DISPLAY]/[RS274NGC]/[KINS]/[HAL]/[TRAJ]/[EMCIO] 初始化
-> milltask 解析 G-code,TP 生成 canonical motion
-> xyzac-trt-kins 做 forward/inverse kinematics 和 RTCP
-> motion/servo-thread 每个周期更新 joints、pose、反馈和 HAL
-> xyzac-trt-gui.py(native vismach)读取五个 joint 与 offset 绘制机床
```
`xyzac-trt.ini` 的关键输入必须进入 Web 资源图:`OPEN_FILE`、`SUBROUTINE_PATH`、`PARAMETER_FILE`、三个 `REMAP`、`KINEMATICS=xyzac-trt-kins sparm=identityfirst`、`JOINTS=5`、`POSTGUI_HALFILE`、`TOOL_TABLE`、`SERVO_PERIOD=1000000` 和 `TASK CYCLE_TIME=0.010`。`xyzac-trt_cmds.hal` 中五个 `joint.*.pos-fb` 到 `xyzac-trt-gui.*` 的 net 是 Canvas B 的关节数据来源;`motion.switchkins-type` 由 HAL analog-out 驱动,不能由 UI 直接改写。
demo 中的 `helix_ac` 先执行 M429(identity),设置 G54,再执行 M428(XYZAC),改变 A/C 后进行 G2 螺旋,最后恢复 M429 并回到起点。Web runner 必须记录每个 M428/M429/M430 的命令、HAL 反馈和确认 tick,以便两个 canvas 在模式切换处建立姿态屏障。
## 4. 总体架构
```text
React App
├─ Panel snapshot reducer / projectXyzacVisual()
├─ Canvas A: ToolpathCanvas (独立 renderer/scene/RAF)
└─ Canvas B: MachineCanvas (独立 renderer/scene/RAF)
│ 只读 XyzacVisualState
▼
WorkerClient -> Dedicated sim worker
├─ ProtocolGateway / schema validator
├─ LinuxCNC WASM task + RS274 + TP + motion + kinematics
├─ HAL/IO/Python virtual runtime
├─ Snapshot publisher / trajectory stream / backpressure
└─ checkpoint / recovery / event ledger
```
Worker 每个 logical tick 依次执行 interpreter、TP、motion、kinematics、HAL/IO,再生成 snapshot。`StatePublisher` 可发送完整 `STATE_SNAPSHOT` 或 motion delta;React reducer 只接受 tick 不倒退且 schema/hash 合法的数据。Canvas A/B 在自己的 RAF 中读取同一个不可变快照引用:渲染时间可以不同,数据时间必须由 `simulation_tick` 标识。
## 5. 双 Canvas 设计
### 5.1 Canvas A:刀具预览和实时仿真路径
* 场景节点:`origin`、`grid`、`canonicalPath`、`rapidPath`、`liveHistory`、`activeLivePath`、`tool`、可选 `limits/offsets/extents`。
* canonical 段来自 `visual.toolpath`,保留 `id`、`source_line`、`kind`、`motion_type`、`start`、`end`、`samples`、圆心/法向/圈数和 `samples_source`。
* 实时轨迹来自 `visual.motion_trace`;当前刀具位置使用 `visual.pose`,当前段和进度使用 `active_segment_id/active_progress`。
* 使用正交投影显示 XY/XZ/YZ 和透视投影;相机状态只属于 Canvas A。暂停时保持最后权威点,清除命令只清除 live history,不删除 canonical 预览。
* 轨迹颜色约定:canonical 完成段浅灰,rapid 虚线红,实时历史橙色,当前段/刀尖青色,错误或越限红色。颜色是显示约定,不改变数据语义。
### 5.2 Canvas B:机床三维模型运动
* 场景节点按当前机床定义建立:`xyzac-machine`/`xyzbc-machine`、`table-x`、`saddle-y`、`spindle-z`、`tilt-a` 或 `tilt-b`、`rotate-c`、`tool-offset`。
* 模型描述来自 `visual.machine_model`:`id`、`source_path`、`source_hash`、`components`、`joints`、`coordinate_system`。模型缺失或哈希不匹配时显示结构化错误并保留轴姿态 HUD。
* 关节变换顺序固定为 table-x -> saddle-y -> spindle-z -> tilt-a/tilt-b -> rotate-c -> tool-offset;方向和单位从模型 manifest 定义,禁止组件自行猜测。
* 使用 `visual.joints` 驱动机械层级,使用 `visual.pose` 驱动刀尖/姿态 HUD;二者必须在同一 tick 通过一致性检查。
* 默认不绘制 Canvas A 的完整路径;`showPath` 仅作为明确的诊断层,且数据仍来自同一 snapshot。
### 5.3 生命周期与性能
每个组件在 mount 时创建自己的 renderer、scene、camera、controls 和 RAF,在 unmount 时停止 RAF、解除 ResizeObserver、释放 geometry/material/texture/render target 并移除 canvas。`devicePixelRatio` 上限为 2;尺寸小于 280x260 时保持稳定最小绘制区域。
Worker 流可以高于显示刷新率。Canvas 只消费最新可绘制快照,中间 motion chunk 可丢弃;发现 tick gap 时标记 `data-visual-gap` 并请求 `SNAPSHOT_REQUEST`。最终 pose、state hash、报警和事件不可丢失。渲染异常只影响对应 canvas,必须通过诊断事件上报且不能停止仿真。
## 6. 数据契约
### 6.1 权威快照
两个 canvas 的唯一输入是 `panel_snapshot.visual` 与配套的 `kinematics/motion`:
```ts
type DualCanvasSnapshot = {
schema: "axis-panel-snapshot.v1";
context_id: string;
session_id: string;
simulation_tick: number;
state_hash: string;
visual: {
schema: "axis-visual.v1";
source: "wasm_canonical_motion";
pose: Record<"x"|"y"|"z"|"a"|"b"|"c", number>;
joints: number[]; // exactly 5: mm, mm, mm, deg, deg
toolpath?: ToolpathSegment[]; // omitted in motion_delta
motion_trace?: MotionSample[];
active_segment_id?: number | null;
active_progress?: number; // [0, 1]
pose_epoch?: number;
live_path_epoch?: number;
machine_model?: MachineModel;
};
kinematics: {
schema: "wasm-xyzac-kinematics.v1";
active_type: 0|1|2;
pose: Record<string, number>;
joints: number[];
offsets: { x_rot_point: number; y_offset: number; z_offset: number; tool_offset: number };
state_hash: string;
wasm_state_hash: string;
};
};
```
`ToolpathSegment` 的数值单位是 mm/deg,坐标顺序为当前机床定义的 XYZAC 或 XYZBC;`MotionSample` 必须含 `tick`、五关节、pose、活动段和进度。`machine_model.source` 必须为 `linuxcnc-wasm-worker`,模型 `source_hash` 用于恢复和 native 对账。`projectXyzacVisual()` 负责默认值、有限值和长度校验;校验失败时 `authoritative=false`。
### 6.2 消息时序与一致性
```text
HELLO -> CAPABILITIES -> SESSION_ATTACH -> SESSION_READY
PROGRAM_LOAD -> ACK -> STATE_SNAPSHOT(toolpath, model, tick T)
PROGRAM_RUN -> ACK
每个 tick: WASM step -> motion delta/event -> STATE_SNAPSHOT/CHUNK
PROGRAM_PAUSE/ABORT/HOME/M428... -> ACK -> RESULT + full snapshot
WORKER_RESTART -> RECOVER -> SNAPSHOT_REQUEST -> SESSION_READY
```
`seq` 在 session 内递增,副作用命令带 `operation_id`。Canvas 只接受 `simulation_tick >= lastTick` 的数据;`pose_epoch` 变化清空插值队列但不清理 canonical path,`live_path_epoch` 变化清理实时历史并建立新轨迹段。`state_hash` 不一致触发诊断和完整快照请求。
## 7. 精美机床模型与刀具实施
`xyzac-machine-model.ts` 以原生 `xyzac-trt-gui.py` 的层级和轴方向为基准,使用圆角铸件、喷涂/钢件/硬质合金材质、环境反射、软阴影和工作平面构成完整模型。模型包含床身、底座前门、排屑盘、立柱侧筋、Z 导轨和丝杠、主轴箱、冷却喷嘴、工作灯、X/Y 导轨、A 轴支架、C 轴转台、T 型槽、沉头螺栓、转台刻度和 AXIS 铭牌。
刀具采用带夹头螺母、磨削刀柄、四条螺旋刃槽和平底刃口的 Ø6 mm 硬质合金立铣刀示意模型。直径和刃形是展示参数,目前不自动映射刀具表中的刀号/直径。
机床属于固定主轴、升降工作台结构,刀尖按原生 Vismach 基准位于 `Z = 200 - tool_offset` mm;X/Y/Z/A/C 相对运动由工作台关节完成,因此机床世界坐标中的刀尖不能直接等同于轨迹画布的 `visual.pose`。夹头保持在主轴上,刀柄随刀长偏置调整伸出量,刀尖仍落在原生基准位置。A 轴中心跟随 `y_offset/z_offset`,C 轴仅旋转台面,上游传动箱不随 C 轴转动。
`tool` 控制器提供刀具特写;重置按模型边界和 canvas 比例恢复整机取景。相机拖动不改变任何关节或 WASM 快照。geometry/material 缓存在模型创建阶段,运行只更新节点变换,卸载时统一释放。
机床 canvas 使用 Three.js `OrbitControls` 的标准鼠标映射:左键拖动旋转模型,中键拖动缩放,右键拖动平移,滚轮缩放;触摸单指旋转、双指缩放/平移。按钮控制与鼠标控制共用同一相机和 target,互不创建第二套视图状态。
已完成验证:`npm run build`;`test_xyzac_threejs.mjs` 验证轴向、转台层级、0/15/30/-5 mm 刀长的刀尖基准和资源释放;`scripts/test_xyzac_machine_model_browser.mjs` 在 Chromium 1600/1024/768/390 px 验证两个 canvas、八个相机操作、取景、WASM 运行和停止后的画面稳定性。运行浏览器测试需将 `PLAYWRIGHT_MODULE` 指向本机 Playwright 的 `index.mjs`;截图和 JSON 报告默认保存到 `/tmp/xyzac-machine-model/`。
## 8. 实施步骤
1. **契约冻结**:确认 `axis-panel-snapshot.v1`、`axis-visual.v1`、模型 manifest、单位、节点顺序、tick/hash 规则,并为字段增加运行时校验。
2. **Worker 资源闭包**:实现 `run-5axis-rtcp.sh` 对应的 exact resolver;加载 INI、HAL、XML、TBL、VAR、NGC、remap 和模型来源,输出 source hash 和缺失边。
3. **WASM 轨迹/姿态发布**:在固定 tick 中生成 canonical segments、motion trace、XYZAC kinematics、machine_model;支持 full snapshot 与 motion delta,保留 M428/M429/M430 的验证结果。
4. **Canvas A 重构**:将 `XyzacToolPreview` 的 DOM host 改为显式 `<canvas>`,封装 `ToolpathRenderer`,实现路径层、实时层、插值队列、gap 恢复和视觉清除。
5. **Canvas B 重构**:将 `XyzacMachineView` 的 DOM host 改为独立 `<canvas>`,封装 `MachineRenderer`,按 manifest 创建层级、驱动五关节和 tool offset,并实现模型缺失/哈希错误状态。
6. **App 接线**:Preview split 保持两个独立 pane;通过同一个 store snapshot 调用两个 renderer。禁止组件之间传递可写场景对象;保留控制器、DRO、图层开关和无障碍标签。
7. **性能与恢复**:加入 transferable trajectory buffer、RAF 限频、ResizeObserver、WebGL context lost 恢复、Worker 重启恢复和 OPFS checkpoint 对账。
8. **测试与发布门禁**:先跑 Worker/source parity,再跑 React、canvas 像素和浏览器矩阵;所有阻断项写入 report,不以 mock 结果替代 WASM 证据。
## 9. 任务分配
| 任务 | 交付物 | 负责人角色 | 依赖 |
|---|---|---|---|
| DUAL-01 契约/manifest | schema、校验器、模型节点和单位表 | 协议与数据负责人 | 无 |
| DUAL-02 exact runner | 配置资源图、启动日志、source hash | LinuxCNC/WASM 负责人 | DUAL-01 |
| DUAL-03 canonical preview | segment、圆弧/螺旋采样、source line | TP/解释器负责人 | DUAL-02 |
| DUAL-04 live snapshot | fixed tick、motion trace、pose/joints、epoch | motion/kinematics 负责人 | DUAL-03 |
| DUAL-05 ToolpathCanvas | Canvas A renderer、图层和 gap 恢复 | 前端可视化负责人 | DUAL-01、04 |
| DUAL-06 MachineCanvas | Canvas B 模型层级、关节变换、模型校验 | Three.js/模型负责人 | DUAL-01、04 |
| DUAL-07 App/UX | 双 pane 接线、控制器、无障碍、响应式 | React 负责人 | DUAL-05、06 |
| DUAL-08 验收/回归 | native/WASM diff、浏览器截图、恢复报告 | QA/发布负责人 | 全部 |
任务状态使用 `planned -> implementing -> verified`;任一任务发现缺少 WASM ABI、oracle 或模型资源时标记 `blocked`,不得用 JS 近似结果升级状态。
## 10. 验收标准
### 10.1 功能验收
* 加载 `xyzac-trt.ini` 后,Canvas A 的 canonical segment 数、源行、螺旋半径/圈数与 Worker golden 一致;Canvas B 显示 `xyzac-trt` 模型和五个关节。
* 执行 demo 时,两个 canvas 的 `data-xyzac-tick` 和 `state_hash` 始终相同;A 的刀尖与 `visual.pose` 相同,B 的节点姿态与 `visual.joints` 相同。
* M428/M429/M430 切换在相同 tick 产生姿态屏障;不允许出现旧关节队列回放或刀具跳回旧 pose。
* Pause/Resume/Step/Abort/Home、feed hold、limit 和 ESTOP 后,实时轨迹和机床姿态均符合 LinuxCNC 状态;`VISUAL_CLEAR` 只清除实时层。
* Worker 重启后恢复到最近 checkpoint,canonical path、model hash、active segment、pose、joints 和 epoch 可重建,无重复 IO/tool/remap 副作用。
### 10.2 自动化与浏览器验收
* `web/worker/test_xyzac_exact_preview.mjs`、`test_xyzac_machine_sequence.mjs`、`test_xyzac_live_path_lifecycle.mjs` 及 source/native parity 报告通过。
* React 测试断言两个 `<canvas>` 节点数量为 2、不同 DOM 节点/renderer、相同 tick/hash 输入;卸载后无 RAF、ResizeObserver 或 WebGL 资源泄漏。
* Playwright 在 Chromium、Firefox、Safari(可用环境)验证桌面和窄屏布局;canvas 像素非空、模型和轨迹不重叠、相机控制有效、context lost 可恢复。
* 性能目标:正常程序执行时主线程渲染 >= 30 FPS;Worker 流发生背压时 UI 不阻塞,最终快照延迟 < 200 ms;单帧内存和轨迹长度受 `CAPABILITIES.limits` 限制。
## 11. 风险与缓解
| 风险 | 影响 | 缓解/阻断条件 |
|---|---|---|
| Canvas A/B 读取不同快照 | 视觉分裂、错误姿态 | 单 store snapshot 引用、tick/hash 断言;不一致即请求 full snapshot |
| 用墙钟插值改变结果 | 与 LinuxCNC 不一致 | 插值仅用于显示,权威位置来自 logical tick;禁止 wall-clock 驱动 WASM |
| 轨迹 chunk 丢失 | 实时线断裂 | gap 标记 + SNAPSHOT_REQUEST;最终 pose/hash 不可丢 |
| M428/M429 后旧队列回放 | 刀具跳变 | `pose_epoch` 屏障、清空 playback queue、记录首个 divergent tick |
| 模型来源或轴方向错误 | 机床镜像/错位 | manifest source hash、固定节点顺序、native screenshot 对照 |
| WebGL context lost/设备限制 | 单 canvas 黑屏 | 独立 renderer 重建;仿真继续运行;报告 `visual_renderer_error` |
| 大型程序导致复制和 GC | 卡顿/内存峰值 | transferable buffer、分段缓存、限长 live history、按需加载 canonical path |
| Worker 恢复重复副作用 | 状态损坏 | operation ledger、checkpoint tick/hash、结果重查后才重试 |
| 浏览器/ABI 缺失 | 无法宣称 parity | `CAPABILITIES` 明确 exclusions;保持 blocked 并附证据 |
## 12. 交付物与回滚
交付必须包含本设计文档、协议/schema 变更、Canvas A/B 组件、模型 manifest、Worker/source parity 报告、浏览器截图和性能报告。发布包保留上一版本 renderer 和协议 schema;若任一 canvas 出现不可恢复错误,可降级为静态快照显示,但不得降级为本地 JS 运动 authority。回滚后仍应能读取旧 checkpoint,并在恢复报告中记录版本、context、source hash、tick 和 state hash。