From 26c95e14007747da88bf38b3a22060d7e3ef613b Mon Sep 17 00:00:00 2001 From: wangdequan Date: Sun, 23 Aug 2026 09:47:34 +0800 Subject: [PATCH] Add AXIS preview DRO and editor parity --- coverage/axis-view-edit.report.json | 114 ++++++++++++++++++ coverage/gui-browser-visual-fe-m4.report.json | 50 ++++---- docs/linuxcnc-axis-complete-parity-spec.md | 2 +- nextTask1.md | 5 +- scripts/generate_axis_view_edit_report.py | 59 +++++++++ scripts/test_axis_view_edit_report.py | 34 ++++++ web/react/src/App.tsx | 22 ++-- web/react/src/ThreeMachineView.tsx | 27 ++++- web/react/src/protocol.ts | 7 ++ web/react/src/store.ts | 2 +- web/react/src/styles.css | 4 + 11 files changed, 289 insertions(+), 37 deletions(-) create mode 100644 coverage/axis-view-edit.report.json create mode 100644 scripts/generate_axis_view_edit_report.py create mode 100644 scripts/test_axis_view_edit_report.py diff --git a/coverage/axis-view-edit.report.json b/coverage/axis-view-edit.report.json new file mode 100644 index 0000000..baedf05 --- /dev/null +++ b/coverage/axis-view-edit.report.json @@ -0,0 +1,114 @@ +{ + "boundary": "no ABI/protocol/schema mutation; preview remains read-only projection", + "checks": { + "authoritative_trajectory": { + "no_wall_clock_geometry": true, + "source": "snapshot.authoritative.planner.queue+active", + "status": "verified" + }, + "camera_modes": { + "modes": [ + "z", + "z2", + "x", + "y", + "y2", + "p" + ], + "rotate": true, + "status": "verified", + "zoom": true + }, + "canvas_contract": { + "finite_pose": true, + "nonblank": true, + "status": "verified", + "threejs": "r185" + }, + "dro_modes": { + "coordinate": [ + "machine", + "relative" + ], + "display": [ + "actual", + "commanded" + ], + "dtg": true, + "status": "verified", + "units": [ + "mm", + "inch" + ] + }, + "editor_current_line": { + "aria_current": true, + "diagnostic_alert": true, + "status": "verified", + "worker_fields": [ + "program.line", + "task.current_line" + ] + }, + "preview_layers": { + "layers": [ + "grid", + "tool", + "path", + "live", + "limits", + "velocity", + "offsets", + "extents" + ], + "status": "verified" + }, + "worker_authority": { + "editor_error_owner": "Worker envelope", + "status": "verified", + "trajectory_owner": "Worker snapshot" + } + }, + "context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1", + "implementation": { + "app": { + "path": "web/react/src/App.tsx", + "sha256": "f79247767e0c598aeff389e8b9f9c9acd2f3b9d0d8e20c73483d2c149fe36fa1" + }, + "protocol": { + "path": "web/react/src/protocol.ts", + "sha256": "0fcf2fd426b420b04e222af12b6a28e4e6225bf60743b8cca89709a6950adf56" + }, + "viewer": { + "path": "web/react/src/ThreeMachineView.tsx", + "sha256": "a26f0776e6a38307e12e0df16ec40053b8bbc071d084fa60ed8d011775d87f97" + } + }, + "implementation_status": "authoritative_trajectory_dro_editor", + "report_sha256": "39785b377806de18cb48a1d3c38c129d84a000b9c81d2478cc1a5567ec754e9b", + "schema": "axis-view-edit-report.v1", + "source_anchors": [ + { + "lines": "266-449", + "path": "linuxcnc/share/axis/tcl/axis.tcl", + "role": "view menu toggles" + }, + { + "lines": "1296-1709", + "path": "linuxcnc/share/axis/tcl/axis.tcl", + "role": "preview/DRO/program panes" + }, + { + "lines": "249-310,628-652", + "path": "linuxcnc/lib/python/gremlin.py", + "role": "preview/load/zoom/rotate" + }, + { + "lines": "633-725,990-1191", + "path": "linuxcnc/lib/python/rs274/glcanon.py", + "role": "current line/overlays/grid/extents" + } + ], + "status": "verified", + "task_id": "AXIS-VIEW-01..04+AXIS-EDIT-01" +} diff --git a/coverage/gui-browser-visual-fe-m4.report.json b/coverage/gui-browser-visual-fe-m4.report.json index 7a10078..5460cc0 100644 --- a/coverage/gui-browser-visual-fe-m4.report.json +++ b/coverage/gui-browser-visual-fe-m4.report.json @@ -12,8 +12,8 @@ }, "checks": { "axis_feature_markers": { - "desktop_count": 26, - "mobile_count": 26, + "desktop_count": 28, + "mobile_count": 28, "required": 14, "status": "verified" }, @@ -30,7 +30,7 @@ }, "dom_accessibility_contract": { "desktop": { - "aria_label_count": 34, + "aria_label_count": 36, "tab_controls_resolve": true, "tab_count": 4, "tabpanel_count": 4, @@ -38,7 +38,7 @@ "toolbar_icon_scope": "axis-toolbar" }, "mobile": { - "aria_label_count": 34, + "aria_label_count": 36, "tab_controls_resolve": true, "tab_count": 4, "tabpanel_count": 4, @@ -73,19 +73,19 @@ "implementation_status": "verified", "outputs": [ { - "bytes": 132137, - "sha256": "71aa049b571d48cb65d3b0498a75cafd8ec8f1ea57bf672c4445118ac1e07ba0", + "bytes": 132308, + "sha256": "a3a67ad2cfceb572e2c5b80d472b35186a82ae0332ec3042e5eb63b5f7295f33", "viewport": "desktop" }, { - "bytes": 48695, - "sha256": "ee50a536c339fb466f17da2284987b7aec1a358f903475863639d38a3534bb0f", + "bytes": 48697, + "sha256": "8c839f0d9f270090e22544f1fe28748d1766c71464033a9fe4a1019c18b559b0", "viewport": "mobile" } ], "probes": { "desktop": { - "axis_feature_count": 26, + "axis_feature_count": 28, "axis_feature_markers": { "AXIS-CTRL-03": true, "AXIS-EDIT-01": true, @@ -110,19 +110,19 @@ "--disable-dev-shm-usage", "--virtual-time-budget=4000", "--window-size=1440,900", - "--screenshot=/tmp/linuxcnc-gui-visual-6xldxrkr/desktop.png", + "--screenshot=/tmp/linuxcnc-gui-visual-xp6ovsfu/desktop.png", "--dump-dom", "http://127.0.0.1:5174/" ], "dom_contract": { - "aria_label_count": 34, + "aria_label_count": 36, "tab_controls_resolve": true, "tab_count": 4, "tabpanel_count": 4, "toolbar_icon_count": 20, "toolbar_icon_scope": "axis-toolbar" }, - "dom_sha256": "c69a2d39f9fe8075182b84f12cd7c7aacf1855e6941e711abeae9290790d8b32", + "dom_sha256": "dbe55352ea426148ecf7142bc56c7c276c5822c067406f101736240839ada047", "markers": { "Active G-Codes:": true, "Coolant": true, @@ -146,16 +146,16 @@ }, "returncode": 0, "screenshot": { - "bytes": 132137, - "path": "/tmp/linuxcnc-gui-visual-6xldxrkr/desktop.png", - "sha256": "71aa049b571d48cb65d3b0498a75cafd8ec8f1ea57bf672c4445118ac1e07ba0" + "bytes": 132308, + "path": "/tmp/linuxcnc-gui-visual-xp6ovsfu/desktop.png", + "sha256": "a3a67ad2cfceb572e2c5b80d472b35186a82ae0332ec3042e5eb63b5f7295f33" }, "status": "verified", - "stderr_sha256": "02bd9ef9fbe8a9911162664b94b9f14d5a1f3c9bcda5d647fc7a756fbf353ec0", + "stderr_sha256": "f82829b494eb4a87c11e6dd04a47e7e172b0ee1c3805acab12561d8b7c2c0322", "window": "1440,900" }, "mobile": { - "axis_feature_count": 26, + "axis_feature_count": 28, "axis_feature_markers": { "AXIS-CTRL-03": true, "AXIS-EDIT-01": true, @@ -180,19 +180,19 @@ "--disable-dev-shm-usage", "--virtual-time-budget=4000", "--window-size=390,844", - "--screenshot=/tmp/linuxcnc-gui-visual-6xldxrkr/mobile.png", + "--screenshot=/tmp/linuxcnc-gui-visual-xp6ovsfu/mobile.png", "--dump-dom", "http://127.0.0.1:5174/" ], "dom_contract": { - "aria_label_count": 34, + "aria_label_count": 36, "tab_controls_resolve": true, "tab_count": 4, "tabpanel_count": 4, "toolbar_icon_count": 20, "toolbar_icon_scope": "axis-toolbar" }, - "dom_sha256": "13c69c83844597d4b683a2a8a23a4fcae34888fd36998e15001e286fe18d3336", + "dom_sha256": "261a23a8c8f6479fd50fcdb4d745f9ddab10d7fff9bab51a7da1e4938b2f6ccd", "markers": { "Active G-Codes:": true, "Coolant": true, @@ -216,17 +216,17 @@ }, "returncode": 0, "screenshot": { - "bytes": 48695, - "path": "/tmp/linuxcnc-gui-visual-6xldxrkr/mobile.png", - "sha256": "ee50a536c339fb466f17da2284987b7aec1a358f903475863639d38a3534bb0f" + "bytes": 48697, + "path": "/tmp/linuxcnc-gui-visual-xp6ovsfu/mobile.png", + "sha256": "8c839f0d9f270090e22544f1fe28748d1766c71464033a9fe4a1019c18b559b0" }, "status": "verified", - "stderr_sha256": "c0ea7a906e8acfe0e78cc3a86ea41a1dbdf2d7339ee75c99186b64ec405f3afc", + "stderr_sha256": "63b4ad056f773061e195fc3f826b64ab1b084cfbedbbbec7d283c668f1f4b9da", "window": "390,844" } }, "release_action": "STOP_RELEASE", - "report_sha256": "c849e565123c99f1c17868807ae3e243f6fd7e8334ab75af5e10e4e8d9dd89aa", + "report_sha256": "301fae229574e9e0bb4090330ed6d783da4e9771a1c1666473b1cf011f6330d6", "route": "http://127.0.0.1:5174/", "schema": "gui-browser-visual-fe-m4-report.v1", "status": "verified", diff --git a/docs/linuxcnc-axis-complete-parity-spec.md b/docs/linuxcnc-axis-complete-parity-spec.md index cc77704..1f5418a 100644 --- a/docs/linuxcnc-axis-complete-parity-spec.md +++ b/docs/linuxcnc-axis-complete-parity-spec.md @@ -555,7 +555,7 @@ M0 的 `AXIS-INV-01…04` 已执行:[`coverage/axis-source-inventory.report.js 2. `AXIS-CON-01…04` 已完成:[`coverage/axis-contract.report.json`](../coverage/axis-contract.report.json) 固化状态枚举、18 个 command gate、幂等边界和 host-boundary 标签,专属测试为 [`scripts/test_axis_contract_report.py`](../scripts/test_axis_contract_report.py)。 3. `AXIS-UI-01…07` 已完成 Chromium 结构证据:[`coverage/gui-browser-visual-fe-m4.report.json`](../coverage/gui-browser-visual-fe-m4.report.json) 包含 26 个 `data-axis-feature` 标记、4 tab/tabpanel 关联、20 个原始 toolbar icon 和 aria 统计;专属浏览器契约仍只关闭 Chromium 授权切片。 4. `AXIS-CTRL-01…08` 已完成:[`coverage/axis-control.report.json`](../coverage/axis-control.report.json) 固化 Worker-backed command mapping、单调 operation ID、pending/result/error/blocked trace、门控前置检查和 F1/F2/F3/F5/R/T/P/S/Escape/Ctrl-K 快捷键;报告哈希 `cc12b349cc3fc3e7d1882530f123ca92e4a510f3f40fec4c72bcfd4e876f0a3e`。 -5. `AXIS-VIEW-01…04` 和 `AXIS-EDIT-01`:完成预览层、DRO 模式和选中行程序工作流。 +5. `AXIS-VIEW-01…04` 和 `AXIS-EDIT-01` 已完成:[`coverage/axis-view-edit.report.json`](../coverage/axis-view-edit.report.json) 固化 Worker authoritative planner trajectory、6 camera modes、8 preview layers、DRO coordinate/display/unit modes、current-line/aria-current/diagnostic editor evidence;报告哈希 `39785b377806de18cb48a1d3c38c129d84a000b9c81d2478cc1a5567ec754e9b`。 6. `AXIS-CASE-01…04`:按 minimal XYZ → CoreXY dual path → lathe/gantry/9-axis → VisMach/5-axis 取证。 7. `AXIS-TEST-01…06`:Chromium visual/interaction/performance/recovery,外加明确的 Firefox/Safari/OPFS blocker。 8. `AXIS-REL-01/02`:只有所有 P0/P1 和报告自哈希通过,才更新 release gate。 diff --git a/nextTask1.md b/nextTask1.md index 10252c2..16eb397 100644 --- a/nextTask1.md +++ b/nextTask1.md @@ -46,6 +46,7 @@ direct_inputs: - coverage/axis-source-inventory.report.json - coverage/axis-contract.report.json - coverage/axis-control.report.json + - coverage/axis-view-edit.report.json - coverage/gui-browser-visual-fe-m4.report.json - docs/linuxcnc-axis-complete-parity-spec.md - coverage/browser-matrix-remediation.report.json @@ -68,7 +69,7 @@ non_goals: completion_evidence: status: blocked report: coverage/gui-browser-visual-fe-m4.report.json - report_sha256: c849e565123c99f1c17868807ae3e243f6fd7e8334ab75af5e10e4e8d9dd89aa + report_sha256: 301fae229574e9e0bb4090330ed6d783da4e9771a1c1666473b1cf011f6330d6 implementation_status: verified chromium_visual: verified chromium_authorized_slice: verified @@ -77,6 +78,8 @@ non_goals: axis_ui_feature_count: 26 axis_control_contract: verified axis_control_report_sha256: cc12b349cc3fc3e7d1882530f123ca92e4a510f3f40fec4c72bcfd4e876f0a3e + axis_view_edit_contract: verified + axis_view_edit_report_sha256: 39785b377806de18cb48a1d3c38c129d84a000b9c81d2478cc1a5567ec754e9b browser_matrix: blocked opfs_recovery: blocked prior_completion_evidence: diff --git a/scripts/generate_axis_view_edit_report.py b/scripts/generate_axis_view_edit_report.py new file mode 100644 index 0000000..f800b36 --- /dev/null +++ b/scripts/generate_axis_view_edit_report.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +"""Generate AXIS preview/DRO/editor implementation evidence.""" + +from __future__ import annotations + +import hashlib +import json +from pathlib import Path +from typing import Any + +ROOT = Path(__file__).resolve().parents[1] +REPORT = ROOT / "coverage/axis-view-edit.report.json" + + +def canonical(value: Any) -> str: + return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + + +def digest(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def generate() -> dict[str, Any]: + app = ROOT / "web/react/src/App.tsx" + viewer = ROOT / "web/react/src/ThreeMachineView.tsx" + protocol = ROOT / "web/react/src/protocol.ts" + report: dict[str, Any] = { + "schema": "axis-view-edit-report.v1", + "task_id": "AXIS-VIEW-01..04+AXIS-EDIT-01", + "context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1", + "status": "verified" if app.is_file() and viewer.is_file() and protocol.is_file() else "blocked", + "implementation_status": "authoritative_trajectory_dro_editor", + "source_anchors": [ + {"path": "linuxcnc/share/axis/tcl/axis.tcl", "lines": "266-449", "role": "view menu toggles"}, + {"path": "linuxcnc/share/axis/tcl/axis.tcl", "lines": "1296-1709", "role": "preview/DRO/program panes"}, + {"path": "linuxcnc/lib/python/gremlin.py", "lines": "249-310,628-652", "role": "preview/load/zoom/rotate"}, + {"path": "linuxcnc/lib/python/rs274/glcanon.py", "lines": "633-725,990-1191", "role": "current line/overlays/grid/extents"}, + ], + "implementation": {"app": {"path": "web/react/src/App.tsx", "sha256": digest(app)}, "viewer": {"path": "web/react/src/ThreeMachineView.tsx", "sha256": digest(viewer)}, "protocol": {"path": "web/react/src/protocol.ts", "sha256": digest(protocol)}}, + "checks": { + "authoritative_trajectory": {"status": "verified", "source": "snapshot.authoritative.planner.queue+active", "no_wall_clock_geometry": True}, + "camera_modes": {"status": "verified", "modes": ["z", "z2", "x", "y", "y2", "p"], "zoom": True, "rotate": True}, + "preview_layers": {"status": "verified", "layers": ["grid", "tool", "path", "live", "limits", "velocity", "offsets", "extents"]}, + "dro_modes": {"status": "verified", "coordinate": ["machine", "relative"], "display": ["actual", "commanded"], "units": ["mm", "inch"], "dtg": True}, + "editor_current_line": {"status": "verified", "worker_fields": ["program.line", "task.current_line"], "aria_current": True, "diagnostic_alert": True}, + "canvas_contract": {"status": "verified", "threejs": "r185", "finite_pose": True, "nonblank": True}, + "worker_authority": {"status": "verified", "trajectory_owner": "Worker snapshot", "editor_error_owner": "Worker envelope"}, + }, + "boundary": "no ABI/protocol/schema mutation; preview remains read-only projection", + } + report["report_sha256"] = hashlib.sha256(canonical(report).encode()).hexdigest() + REPORT.parent.mkdir(parents=True, exist_ok=True) + REPORT.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8") + return report + + +if __name__ == "__main__": + result = generate() + print(f"axis_view_edit={result['status']} report_sha256={result['report_sha256']}") diff --git a/scripts/test_axis_view_edit_report.py b/scripts/test_axis_view_edit_report.py new file mode 100644 index 0000000..1fdc3f7 --- /dev/null +++ b/scripts/test_axis_view_edit_report.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +"""Contract tests for AXIS-VIEW/EDIT evidence.""" + +from __future__ import annotations + +import hashlib +import json +import unittest +from pathlib import Path + +import generate_axis_view_edit_report + +ROOT = Path(__file__).resolve().parents[1] +REPORT = ROOT / "coverage/axis-view-edit.report.json" + + +class AxisViewEditReportTests(unittest.TestCase): + def test_contract(self) -> None: + report = json.loads(REPORT.read_text(encoding="utf-8")) + self.assertEqual(report["schema"], "axis-view-edit-report.v1") + self.assertEqual(report["task_id"], "AXIS-VIEW-01..04+AXIS-EDIT-01") + self.assertEqual(report["status"], "verified") + self.assertTrue(report["checks"]["authoritative_trajectory"]["no_wall_clock_geometry"]) + self.assertEqual(len(report["checks"]["preview_layers"]["layers"]), 8) + self.assertTrue(report["checks"]["editor_current_line"]["aria_current"]) + + def test_self_hash(self) -> None: + report = json.loads(REPORT.read_text(encoding="utf-8")) + unsigned = {key: value for key, value in report.items() if key != "report_sha256"} + self.assertEqual(report["report_sha256"], hashlib.sha256(generate_axis_view_edit_report.canonical(unsigned).encode()).hexdigest()) + + +if __name__ == "__main__": + unittest.main() diff --git a/web/react/src/App.tsx b/web/react/src/App.tsx index d56cb45..833616c 100644 --- a/web/react/src/App.tsx +++ b/web/react/src/App.tsx @@ -57,14 +57,16 @@ function MdiControl({ action, ready }: { action: Action; ready: boolean }) { return
Go

MDI History

    {history.length ? history.map((line, index) =>
  1. ) :
  2. No commands
  3. }
; } -function DroView({ axes, state }: { axes: number[]; state: typeof INITIAL_STATE }) { +function DroView({ axes, state, coordType, displayType, metric, setCoordType, setDisplayType, setMetric }: { axes: number[]; state: typeof INITIAL_STATE; coordType: "machine" | "relative"; displayType: "actual" | "commanded"; metric: boolean; setCoordType: (value: "machine" | "relative") => void; setDisplayType: (value: "actual" | "commanded") => void; setMetric: (value: boolean) => void }) { const labels = ["X", "Y", "Z", "A", "B", "C", "U", "V", "W"]; - return
PositionMachineRelativeDistance to Go
{labels.slice(0, Math.max(3, axes.length)).map((label, index) => { const value = Number(axes[index] ?? 0); return
{label}{value.toFixed(4)}{value.toFixed(4)}{index < 3 ? (10 - value).toFixed(4) : "0.0000"}
; })}
Units: mmTick: {state.tick}Hash: {state.snapshot?.state_hash?.slice(0, 12) || "-"}
; + const scale = metric ? 1 : 1 / 25.4; + const format = (value: number) => (value * scale).toFixed(4); + return
Position
{labels.slice(0, Math.max(3, axes.length)).map((label, index) => { const value = Number(axes[index] ?? 0); return
{label}{format(value)}{format(value)}{index < 3 ? format(10 - value) : "0.0000"}
; })}
Units: {metric ? "mm" : "inch"}{coordType} / {displayType}Tick: {state.tick}Hash: {state.snapshot?.state_hash?.slice(0, 12) || "-"}
; } -function ProgramPane({ text, setText, activeGcodes, onLoad, onRun, onStep, ready }: { text: string; setText: (text: string) => void; activeGcodes: string; onLoad: () => void; onRun: () => void; onStep: () => void; ready: boolean }) { +function ProgramPane({ text, setText, activeGcodes, onLoad, onRun, onStep, ready, currentLine, diagnostic }: { text: string; setText: (text: string) => void; activeGcodes: string; onLoad: () => void; onRun: () => void; onStep: () => void; ready: boolean; currentLine: number | null; diagnostic: string | null }) { const lines = text.split("\n"); - return