# 03 程序修复详细步骤

生成时间：2026-06-22

本文档面向实际编码人员，按问题给出具体修改步骤。执行前建议先创建修复分支。

```bash
git status --short
git switch -c fix/web-rtcp-5axis-working1
```

## 1. D1：自动挂接 LinuxCNC kinematics frame

### Step 1.1 增加期望 frame source 状态

文件：

```text
web-rtcp-5axis-sim-plan/app/src/state/store.js
```

在 `initialState` 增加字段：

```js
desiredFrameSourceMode: "fixture-ui-only",
```

保留现有：

```js
sourceMode
frameSourceMode
```

三者语义：

```text
desiredFrameSourceMode: 用户/运行时希望使用的 frame 来源
frameSourceMode: 当前 rtcpFrame 实际来源
sourceMode: UI 总体展示来源，可继续跟当前 frame source 同步
```

### Step 1.2 修改 ATTACH_KINEMATICS_RUNTIME

位置：

```text
store.js -> case "ATTACH_KINEMATICS_RUNTIME"
```

runtime loaded 时设置：

```js
desiredFrameSourceMode: "source-derived-kinematics-wasm",
```

runtime missing 时设置：

```js
desiredFrameSourceMode: "fixture-ui-only",
```

不要只依赖 `sourceMode` / `frameSourceMode`。

### Step 1.3 修改 buildFrameForState

当前逻辑大意：

```js
const requestedSourceMode = state.frameSourceMode || state.sourceMode;
...
if (requestedSourceMode === "source-derived-kinematics-wasm") {
  if (runtime loaded && !async) {
    ...
  } else {
    sourceMode = "fixture-ui-only";
  }
}
```

建议改为：

```js
const requestedSourceMode =
  state.desiredFrameSourceMode ||
  state.frameSourceMode ||
  state.sourceMode;
```

async runtime 已加载但尚未返回 frame 时，可以临时生成 fixture frame，但必须带上可诊断原因，不要覆盖 desired source。

### Step 1.4 修改 setState 写回策略

当前 `setState()` 中：

```js
sourceMode: frame.sourceMode,
frameSourceMode: frame.sourceMode,
```

建议改为：

```js
sourceMode: frame.sourceMode,
frameSourceMode: frame.sourceMode,
desiredFrameSourceMode: next.desiredFrameSourceMode || frame.sourceMode,
```

关键点：不要因为临时 fixture frame 把 `desiredFrameSourceMode` 变成 fixture。

### Step 1.5 修改 scheduleAsyncKinematicsRefresh

当前 guard 不应依赖已解析 frame source：

```js
if (state.frameSourceMode !== "source-derived-kinematics-wasm") return null;
```

改为：

```js
if (state.desiredFrameSourceMode !== "source-derived-kinematics-wasm") return null;
if (!state.kinematicsRuntime?.loaded) return null;
if (!isAsyncKinematicsRuntime(state.kinematicsRuntime)) return null;
```

如果当前 frame 已经 ready 且 activeLine/axisPose/kinsType 未变化，可继续跳过刷新。

### Step 1.6 修改 refreshAsyncKinematicsFrame 成功写回

成功后确保：

```js
sourceMode: "source-derived-kinematics-wasm",
frameSourceMode: "source-derived-kinematics-wasm",
desiredFrameSourceMode: "source-derived-kinematics-wasm",
```

失败时：

```js
operatorMessage: `LinuxCNC kinematics refresh failed: ${error.message}`
```

并保留 retry 能力。

### Step 1.7 修改 main.js 初始化顺序

文件：

```text
web-rtcp-5axis-sim-plan/app/src/main.js
```

`attachDefaultKinematicsRuntime()` 已调用：

```js
await store.refreshKinematicsFrame(...)
```

修复后保留该调用，并在 profile/INI 变更订阅中，runtime attach 完成后再次刷新：

```js
await attachDefaultKinematicsRuntime(...)
await store.refreshKinematicsFrame({ operatorMessage: "..." })
```

注意避免无限刷新。可通过 `asyncFrameRefreshSequence` 或 readiness 状态判断。

### Step 1.8 D1 测试

新增或更新测试：

```text
web-rtcp-5axis-sim-plan/tests/browser/gmoccapy_shell_smoke.html
```

或新增：

```text
web-rtcp-5axis-sim-plan/tests/browser/kinematics_auto_refresh_smoke.html
```

断言：

```js
await waitUntil(() => window.webRtcp5AxisSimulation.getState().sourceMode === "source-derived-kinematics-wasm")
assertText('[data-rtcp-diagnostic="boundary"]', 'linuxcnc_kinematics_wasm_c_abi')
assertText('[data-rtcp-diagnostic="kinematics-ready"]', 'ready')
```

## 2. D2：修复 3D 预览可见性

### Step 2.1 修复 updateToolpathPreview 未定义变量

文件：

```text
web-rtcp-5axis-sim-plan/app/src/visualization/five-axis-scene.js
```

在 `updateToolpathPreview(preview, state)` 中补齐：

```js
const toolPosition = executionToolPosition(state, previewPoints);
const fitPoints = collectFitPoints(previewPoints, executedPoints, currentSegmentPoints, toolPosition);
const fitKey = [
  previewPoints.length,
  executedPoints.length,
  currentSegmentPoints.length,
  previewSourceMode(state),
  state.programExecutionMotionIndex || 0,
  state.programExecutionSampleIndex || 0,
].join(":");
```

确保 `updateToolExecutionMarker()` 使用同一个 `toolPosition`。

### Step 2.2 增加常驻机床参考模型

新增函数：

```js
function createMachineReferenceModel() { ... }
function updateMachineReferenceModel(preview, state) { ... }
```

推荐对象：

```text
grid/table base: dark gray plane/box
X axis: red line
Y axis: green line
Z axis: blue line
rotary ring: cyan/yellow ring
tool holder: small cylinder/cone
TCP marker: existing sphere
```

在 `createScene()` 中：

```js
const machineModel = createMachineReferenceModel();
scene.add(machineModel.root);
```

在 preview 对象中保存：

```js
machineModel
```

在 `updateToolpathPreview()` 中：

```js
updateMachineReferenceModel(preview, state);
```

### Step 2.3 提高 fallback 可见性

在 `renderFallbackPreview()` 中，路径为空也要绘制：

```text
工作台矩形
XYZ 坐标轴
旋转中心
TCP 点
```

要求颜色和尺寸足够明显，避免黑底上不可见。

### Step 2.4 强化 canvas dataset

`exposePreviewDataset()` 已存在，修复后确保任意路径都稳定输出：

```text
data-three-ready="true"
data-three-renderer
data-three-scene-objects
data-three-path-points
data-three-tool-execution-marker
```

如果 fallback：

```text
data-three-fallback-reason
```

如果 WebGL：

```text
data-three-renderer="webgl"
```

### Step 2.5 D2 测试

新增 pixel 检查：

```js
const stats = canvasPixelStats(canvas)
assert(stats.nonBlackRatio > 0.02)
assert(stats.averageLuminance > 5)
```

同时检查：

```js
Number(canvas.dataset.threeSceneObjects) > 0
canvas.dataset.threeReady === "true"
```

建议覆盖 desktop 和 mobile viewport。

## 3. D3：修复 HOME/JOG 坐标连续性

### Step 3.1 记录 pending JOG 上下文

文件：

```text
web-rtcp-5axis-sim-plan/app/src/state/store.js
```

在 `initialState` 增加：

```js
pendingJogCommand: null,
```

在 `case "JOG"` 的 task/HAL runtime 分支，发送命令前记录：

```js
setState({
  pendingJogCommand: {
    axis,
    direction,
    increment,
    basePose: state.axisPose,
    createdAtLine: state.activeLine,
  },
  operatorMessage: `task/HAL jog ${axis.toUpperCase()} ...`,
})
```

注意现有代码直接调用 `runTaskHalCommandSequence()`，需要避免两次 setState 引发顺序混乱。可把 pending context 作为 `runTaskHalCommandSequence()` 的 options 传入，最终在 command started patch 中写入。

### Step 3.2 给 task/HAL status 增加坐标系元数据

文件：

```text
web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-task-hal-runtime.js
```

在 `ui` 对象中增加：

```js
axisPoseFrame: "task-local",
```

如果 runtime 能确定是 work pose，则写：

```js
axisPoseFrame: "work",
```

如果能计算增量，增加：

```js
axisPoseDelta: { x, y, z, a, b, c }
```

短期不能准确判断时，不要伪装成 work。

### Step 3.3 修改 applyTaskHalStatusPatch

当前：

```js
const axisPose = clampAxisPoseToProfile({
  ...state.axisPose,
  ...ui.axisPose,
}, state.profile);
```

改为单独函数：

```js
const axisPose = resolveTaskHalAxisPose(state, status);
```

建议实现：

```js
function resolveTaskHalAxisPose(state, status) {
  const ui = status?.ui || {};
  if (ui.axisPoseFrame === "work") {
    return clampAxisPoseToProfile({ ...state.axisPose, ...ui.axisPose }, state.profile);
  }

  if (ui.axisPoseDelta) {
    return addAxisDelta(state.axisPose, ui.axisPoseDelta, state.profile);
  }

  if (state.pendingJogCommand && isJogStatus(status)) {
    const { axis, direction, increment, basePose } = state.pendingJogCommand;
    return clampAxisPoseToProfile({
      ...basePose,
      [axis]: Number(basePose[axis] || 0) + direction * increment,
    }, state.profile);
  }

  if (!ui.axisPoseFrame && wouldResetNonZeroPoseToLocalZero(state.axisPose, ui.axisPose)) {
    return state.axisPose;
  }

  return clampAxisPoseToProfile({ ...state.axisPose, ...ui.axisPose }, state.profile);
}
```

### Step 3.4 清理 pending JOG

`TASK_HAL_STATUS_APPLIED` 后，如果使用了 pending JOG：

```js
pendingJogCommand: null
```

如果 command failed：

```js
pendingJogCommand: null
```

### Step 3.5 HOME 同步

HOME 成功后，UI 和 task/HAL runtime 必须同一坐标基准。短期可在 HOME fallback patch 中明确：

```js
axisPose: initialAxisPose
```

task/HAL HOME status 如果返回局部原点，不能覆盖 `initialAxisPose`，除非 status 标记 `axisPoseFrame="work"`。

### Step 3.6 D3 测试

新增测试步骤：

```js
power on
manual
home
capture X/Y/Z
jog X+
assert X === previousX + jogIncrement
assert Y/Z unchanged
jog Y-
assert Y === previousY - jogIncrement
assert X/Z unchanged
```

同时验证 DRO 文本：

```js
document.querySelector('[data-region="dro"]').textContent
```

## 4. 本地验证命令

建议按顺序执行：

```bash
npm --prefix web-rtcp-5axis-sim-plan/app run build
node web-rtcp-5axis-sim-plan/tests/node/verify_rtcp_store.mjs
node web-rtcp-5axis-sim-plan/tests/node/verify_five_axis_session.mjs
node web-rtcp-5axis-sim-plan/tests/node/verify_linuxcnc_kinematics_runtime.mjs
node web-rtcp-5axis-sim-plan/tests/node/verify_linuxcnc_task_hal_runtime.mjs
```

如果项目已有 browser smoke：

```bash
web-rtcp-5axis-sim-plan/tests/browser/verify_gmoccapy_shell_browser.sh
```

修复后再执行 QA 站点测试脚本或新增等价本地测试。

