diff --git a/README.md b/README.md index 6e9f84c..a4a095b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # LinuxCNC Web/WASM +五轴机床 INI 参数化设计(XYZAC / XYZBC)的当前工作入口: +[`项目文档、任务矩阵与验收证据`](docs/trt-parametric/README.md)。本专项按该索引接续。 + 本仓库的目标是将 LinuxCNC `2.10.0~pre0` 的仿真行为移植到浏览器。当前已完成 posemath/TP、rs274ngc interpreter 和 fixed-tick motion queue 核心切片;这仍不是 LinuxCNC 全功能完成声明。 完整仿真功能范围、任务分解和 M0-M8 里程碑见 [`完整仿真移植实施计划`](docs/linuxcnc-web-wasm-complete-simulation-plan.md);技术实施接续入口为 [`nextTask1.md`](nextTask1.md)。 diff --git a/coverage/trt-parametric/browser.report.json b/coverage/trt-parametric/browser.report.json new file mode 100644 index 0000000..2359e5c --- /dev/null +++ b/coverage/trt-parametric/browser.report.json @@ -0,0 +1,73 @@ +{ + "report_id": "trt-parametric-browser-v1", + "status": "verified", + "machines": [ + { + "id": "xyzac-trt", + "offsets": { + "x_rot_point": 24, + "y_rot_point": -12, + "z_rot_point": 8, + "x_offset": 0, + "y_offset": 20, + "z_offset": 10, + "tool_offset": 0 + }, + "export_bytes": 4867, + "preserved_program": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/boat-xyzac.ngc", + "canvas": { + "hash": 310202204, + "colors": 1616, + "width": 472, + "height": 360 + } + }, + { + "id": "xyzbc-trt", + "offsets": { + "x_rot_point": 24, + "y_rot_point": -12, + "z_rot_point": 8, + "x_offset": -20, + "y_offset": 0, + "z_offset": -15, + "tool_offset": 0 + }, + "export_bytes": 4875, + "preserved_program": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/boat-xyzbc.ngc", + "canvas": { + "hash": 2009856167, + "colors": 1668, + "width": 472, + "height": 360 + } + } + ], + "viewports": [ + { + "width": 1440, + "height": 1000, + "canvas": { + "hash": 1393462626, + "colors": 1691, + "width": 472, + "height": 360 + }, + "controls_fit": true, + "interactive": true + }, + { + "width": 390, + "height": 844, + "canvas": { + "hash": 3850596258, + "colors": 1804, + "width": 368, + "height": 378 + }, + "controls_fit": true, + "interactive": true + } + ], + "browser_errors": [] +} diff --git a/coverage/trt-parametric/failure.png b/coverage/trt-parametric/failure.png new file mode 100644 index 0000000..a5aef68 Binary files /dev/null and b/coverage/trt-parametric/failure.png differ diff --git a/coverage/trt-parametric/layout-1440.png b/coverage/trt-parametric/layout-1440.png new file mode 100644 index 0000000..767fc94 Binary files /dev/null and b/coverage/trt-parametric/layout-1440.png differ diff --git a/coverage/trt-parametric/layout-390.png b/coverage/trt-parametric/layout-390.png new file mode 100644 index 0000000..c424dca Binary files /dev/null and b/coverage/trt-parametric/layout-390.png differ diff --git a/coverage/trt-parametric/worker.report.json b/coverage/trt-parametric/worker.report.json new file mode 100644 index 0000000..6e53886 --- /dev/null +++ b/coverage/trt-parametric/worker.report.json @@ -0,0 +1,1595 @@ +{ + "report_id": "trt-parametric-worker-v1", + "status": "verified", + "rows": [ + { + "machine": "xyzac-trt", + "source": "G21 G90 G94\nM428\nG1 X12 Y8 Z6 A20 C30 F600\nM2\n", + "baseline": { + "trace": [ + { + "tick": 11, + "pose": { + "x": 0.43531258808385337, + "y": 0.29020839205590226, + "z": 0.21765629404192668, + "a": 0.7255209801397556, + "b": 0, + "c": 1.0882814702096335, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 0.42972215107433426, + 0.4238712815951402, + -0.031028512333440617, + 0.7255209801397556, + 1.0882814702096335 + ] + }, + { + "tick": 21, + "pose": { + "x": 1.20353386768123, + "y": 0.8023559117874867, + "z": 0.601766933840615, + "a": 2.0058897794687165, + "b": 0, + "c": 3.0088346692030745, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 1.1597591273061187, + 1.2051079866402112, + -0.06226200807547322, + 2.0058897794687165, + 3.0088346692030745 + ] + }, + { + "tick": 31, + "pose": { + "x": 1.9717551472785966, + "y": 1.3145034315190645, + "z": 0.9858775736392983, + "a": 3.286258578797661, + "b": 0, + "c": 4.929387868196492, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 1.8515097337411197, + 2.0262572363257014, + -0.06100423933171584, + 3.286258578797661, + 4.929387868196492 + ] + }, + { + "tick": 41, + "pose": { + "x": 2.739976426875956, + "y": 1.8266509512506375, + "z": 1.369988213437978, + "a": 4.5666273781265945, + "b": 0, + "c": 6.849941067189891, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 2.5025546580305478, + 2.8842113485890977, + -0.02456597127233273, + 4.5666273781265945, + 6.849941067189891 + ] + }, + { + "tick": 51, + "pose": { + "x": 3.5081977064733154, + "y": 2.3387984709822103, + "z": 1.7540988532366577, + "a": 5.846996177455527, + "b": 0, + "c": 8.77049426618329, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 3.1105637151574843, + 3.775641078639424, + 0.04951852031841675, + 5.846996177455527, + 8.77049426618329 + ] + }, + { + "tick": 61, + "pose": { + "x": 4.276418986070675, + "y": 2.8509459907137833, + "z": 2.1382094930353377, + "a": 7.12736497678446, + "b": 0, + "c": 10.69104746517669, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 3.6733002615273795, + 4.697011687538765, + 0.16347608480270992, + 7.12736497678446, + 10.69104746517669 + ] + }, + { + "tick": 71, + "pose": { + "x": 5.0446402656680345, + "y": 3.3630935104453563, + "z": 2.5223201328340172, + "a": 8.407733776113393, + "b": 0, + "c": 12.611600664170087, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 4.188625453194591, + 5.644600028114457, + 0.31928043445841503, + 8.407733776113393, + 12.611600664170087 + ] + }, + { + "tick": 81, + "pose": { + "x": 5.812861545265394, + "y": 3.8752410301769293, + "z": 2.906430772632697, + "a": 9.688102575442324, + "b": 0, + "c": 14.532153863163485, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 4.654502341479983, + 6.614512574589231, + 0.5186390489451398, + 9.688102575442324, + 14.532153863163485 + ] + }, + { + "tick": 91, + "pose": { + "x": 6.581082824862754, + "y": 4.387388549908502, + "z": 3.290541412431377, + "a": 10.968471374771257, + "b": 0, + "c": 16.452707062156886, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 5.068999799476512, + 7.6027043183064205, + 0.7629813063444324, + 10.968471374771257, + 16.452707062156886 + ] + }, + { + "tick": 101, + "pose": { + "x": 7.3493041044601135, + "y": 4.899536069640075, + "z": 3.6746520522300568, + "a": 12.24884017410019, + "b": 0, + "c": 18.373260261150286, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 5.430296273191196, + 8.604998448225977, + 1.0534478955970403, + 12.24884017410019, + 18.373260261150286 + ] + }, + { + "tick": 111, + "pose": { + "x": 8.117525384057473, + "y": 5.411683589371648, + "z": 4.058762692028736, + "a": 13.529208973429121, + "b": 0, + "c": 20.29381346014368, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 5.736683351332234, + 9.617106731489649, + 1.3908815598812971, + 13.529208973429121, + 20.29381346014368 + ] + }, + { + "tick": 121, + "pose": { + "x": 8.885746663654832, + "y": 5.923831109103221, + "z": 4.442873331827416, + "a": 14.809577772758056, + "b": 0, + "c": 22.214366659137085, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 5.986569148019269, + 10.634650506313353, + 1.7758192149941703, + 14.809577772758056, + 22.214366659137085 + ] + }, + { + "tick": 131, + "pose": { + "x": 9.653967943252193, + "y": 6.435978628834794, + "z": 4.826983971626096, + "a": 16.089946572086987, + "b": 0, + "c": 24.13491985813048, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 6.178481492972385, + 11.653182196773486, + 2.2084854811311843, + 16.089946572086987, + 24.13491985813048 + ] + }, + { + "tick": 141, + "pose": { + "x": 10.422189222849552, + "y": 6.9481261485663675, + "z": 5.211094611424776, + "a": 17.37031537141592, + "b": 0, + "c": 26.05547305712388, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 6.3110709240208225, + 12.66820725672306, + 2.6887876606358327, + 17.37031537141592, + 26.05547305712388 + ] + }, + { + "tick": 151, + "pose": { + "x": 11.19041050244691, + "y": 7.4602736682979405, + "z": 5.595205251223455, + "a": 18.650684170744853, + "b": 0, + "c": 27.97602625611728, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 6.383113477065606, + 13.675206448111613, + 3.216312188325154, + 18.650684170744853, + 27.97602625611728 + ] + }, + { + "tick": 161, + "pose": { + "x": 11.913625063560401, + "y": 7.942416709040268, + "z": 5.9568125317802005, + "a": 19.85604177260067, + "b": 0, + "c": 29.784062658901004, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 6.394621744187004, + 14.611824272990901, + 3.755429285890239, + 19.85604177260067, + 29.784062658901004 + ] + }, + { + "tick": 171, + "pose": { + "x": 12, + "y": 8, + "z": 6, + "a": 20, + "b": 0, + "c": 30, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 6.392304845413264, + 14.722765333094944, + 3.822532572105194, + 20, + 30 + ] + } + ], + "offsets": { + "tool_offset": 0, + "x_rot_point": 0, + "y_rot_point": 0, + "z_rot_point": 0, + "x_offset": 0, + "y_offset": 20, + "z_offset": 10 + }, + "final": { + "tick": 171, + "pose": { + "x": 12, + "y": 8, + "z": 6, + "a": 20, + "b": 0, + "c": 30, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 6.392304845413264, + 14.722765333094944, + 3.822532572105194, + 20, + 30 + ] + }, + "preview_hash": "fnv1a64:0740b3272128b1bb", + "trace_hash": "fnv1a64:ce697b4fadfad0aa", + "model_samples": 4 + }, + "modified": { + "trace": [ + { + "tick": 11, + "pose": { + "x": 0.43531258808385337, + "y": 0.29020839205590226, + "z": 0.21765629404192668, + "a": 0.7255209801397556, + "b": 0, + "c": 1.0882814702096335, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 0.29839492329883655, + 0.23806138837691382, + 0.1186595600110536, + 0.7255209801397556, + 1.0882814702096335 + ] + }, + { + "tick": 21, + "pose": { + "x": 1.20353386768123, + "y": 0.8023559117874867, + "z": 0.601766933840615, + "a": 2.0058897794687165, + "b": 0, + "c": 3.0088346692030745, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 0.8047364818248148, + 0.6809886822424431, + 0.34027883586112573, + 2.0058897794687165, + 3.0088346692030745 + ] + }, + { + "tick": 31, + "pose": { + "x": 1.9717551472785966, + "y": 1.3145034315190645, + "z": 0.9858775736392983, + "a": 3.286258578797661, + "b": 0, + "c": 4.929387868196492, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 1.283301885476961, + 1.1512719521033148, + 0.5794302082437426, + 3.286258578797661, + 4.929387868196492 + ] + }, + { + "tick": 41, + "pose": { + "x": 2.739976426875956, + "y": 1.8266509512506375, + "z": 1.369988213437978, + "a": 4.5666273781265945, + "b": 0, + "c": 6.849941067189891, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 1.7319113319947625, + 1.6466730139855557, + 0.8382368096709492, + 4.5666273781265945, + 6.849941067189891 + ] + }, + { + "tick": 51, + "pose": { + "x": 3.5081977064733154, + "y": 2.3387984709822103, + "z": 1.7540988532366577, + "a": 5.846996177455527, + "b": 0, + "c": 8.77049426618329, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 2.148462069309356, + 2.1647608699183998, + 1.118646742957452, + 5.846996177455527, + 8.77049426618329 + ] + }, + { + "tick": 61, + "pose": { + "x": 4.276418986070675, + "y": 2.8509459907137833, + "z": 2.1382094930353377, + "a": 7.12736497678446, + "b": 0, + "c": 10.69104746517669, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 2.5309325541168324, + 2.702925032227178, + 1.4224192021870863, + 7.12736497678446, + 10.69104746517669 + ] + }, + { + "tick": 71, + "pose": { + "x": 5.0446402656680345, + "y": 3.3630935104453563, + "z": 2.5223201328340172, + "a": 8.407733776113393, + "b": 0, + "c": 12.611600664170087, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 2.877386468443123, + 3.2583897873761813, + 1.7511115404754882, + 8.407733776113393, + 12.611600664170087 + ] + }, + { + "tick": 81, + "pose": { + "x": 5.812861545265394, + "y": 3.8752410301769293, + "z": 2.906430772632697, + "a": 9.688102575442324, + "b": 0, + "c": 14.532153863163485, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 3.185976587726967, + 3.828229334530791, + 2.106067345136779, + 9.688102575442324, + 14.532153863163485 + ] + }, + { + "tick": 91, + "pose": { + "x": 6.581082824862754, + "y": 4.387388549908502, + "z": 3.290541412431377, + "a": 10.968471374771257, + "b": 0, + "c": 16.452707062156886, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 3.454948494173575, + 4.409383730211715, + 2.4884055765479562, + 10.968471374771257, + 16.452707062156886 + ] + }, + { + "tick": 101, + "pose": { + "x": 7.3493041044601135, + "y": 4.899536069640075, + "z": 3.6746520522300568, + "a": 12.24884017410019, + "b": 0, + "c": 18.373260261150286, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 3.68264412936678, + 4.998675566898388, + 2.8990108224578464, + 12.24884017410019, + 18.373260261150286 + ] + }, + { + "tick": 111, + "pose": { + "x": 8.117525384057473, + "y": 5.411683589371648, + "z": 4.058762692028736, + "a": 13.529208973429121, + "b": 0, + "c": 20.29381346014368, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 3.8675051803715617, + 5.592827310218196, + 3.338524714726148, + 13.529208973429121, + 20.29381346014368 + ] + }, + { + "tick": 121, + "pose": { + "x": 8.885746663654832, + "y": 5.923831109103221, + "z": 4.442873331827416, + "a": 14.809577772758056, + "b": 0, + "c": 22.214366659137085, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 4.008076293811263, + 6.188479216445758, + 3.8073385505204715, + 14.809577772758056, + 22.214366659137085 + ] + }, + { + "tick": 131, + "pose": { + "x": 9.653967943252193, + "y": 6.435978628834794, + "z": 4.826983971626096, + "a": 16.089946572086987, + "b": 0, + "c": 24.13491985813048, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 4.103008112664548, + 6.78220774944357, + 4.305587154865833, + 16.089946572086987, + 24.13491985813048 + ] + }, + { + "tick": 141, + "pose": { + "x": 10.422189222849552, + "y": 6.9481261485663675, + "z": 5.211094611424776, + "a": 17.37031537141592, + "b": 0, + "c": 26.05547305712388, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 4.151060130795425, + 7.370544413912912, + 4.83314401615139, + 17.37031537141592, + 26.05547305712388 + ] + }, + { + "tick": 151, + "pose": { + "x": 11.19041050244691, + "y": 7.4602736682979405, + "z": 5.595205251223455, + "a": 18.650684170744853, + "b": 0, + "c": 27.97602625611728, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 4.151103360505562, + 7.949994919900462, + 5.389617720774595, + 18.650684170744853, + 27.97602625611728 + ] + }, + { + "tick": 161, + "pose": { + "x": 11.913625063560401, + "y": 7.942416709040268, + "z": 5.9568125317802005, + "a": 19.85604177260067, + "b": 0, + "c": 29.784062658901004, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 4.10636087088259, + 8.484240758195993, + 5.939328823631666, + 19.85604177260067, + 29.784062658901004 + ] + }, + { + "tick": 171, + "pose": { + "x": 12, + "y": 8, + "z": 6, + "a": 20, + "b": 0, + "c": 30, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 4.098076211353317, + 8.54717526960785, + 6.0066221936361055, + 20, + 30 + ] + } + ], + "offsets": { + "tool_offset": 0, + "x_rot_point": 9, + "y_rot_point": -7, + "z_rot_point": 13, + "x_offset": 0, + "y_offset": 15, + "z_offset": -4 + }, + "final": { + "tick": 171, + "pose": { + "x": 12, + "y": 8, + "z": 6, + "a": 20, + "b": 0, + "c": 30, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 4.098076211353317, + 8.54717526960785, + 6.0066221936361055, + 20, + 30 + ] + }, + "preview_hash": "fnv1a64:0740b3272128b1bb", + "trace_hash": "fnv1a64:10c0665d1c3fa753", + "model_samples": 4 + }, + "repeat_hash": "fnv1a64:10c0665d1c3fa753", + "first_divergent_tick": 11 + }, + { + "machine": "xyzbc-trt", + "source": "G21 G90 G94\nM428\nG1 X12 Y8 Z6 B20 C30 F600\nM2\n", + "baseline": { + "trace": [ + { + "tick": 11, + "pose": { + "x": 0.43531258808385337, + "y": 0.29020839205590226, + "z": 0.21765629404192668, + "a": 0, + "b": 0.7255209801397556, + "c": 1.0882814702096335, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 0.6207761955139439, + 0.2984239149933897, + -0.04225286521340266, + 0.7255209801397556, + 1.0882814702096335 + ] + }, + { + "tick": 21, + "pose": { + "x": 1.20353386768123, + "y": 0.8023559117874867, + "z": 0.601766933840615, + "a": 0, + "b": 2.0058897794687165, + "c": 3.0088346692030745, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 1.692889784467745, + 0.8644232449825833, + -0.14843204969818302, + 2.0058897794687165, + 3.0088346692030745 + ] + }, + { + "tick": 31, + "pose": { + "x": 1.9717551472785966, + "y": 1.3145034315190645, + "z": 0.9858775736392983, + "a": 0, + "b": 3.286258578797661, + "c": 4.929387868196492, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 2.7319608494340777, + 1.4790704304247735, + -0.29303854010070474, + 3.286258578797661, + 4.929387868196492 + ] + }, + { + "tick": 41, + "pose": { + "x": 2.739976426875956, + "y": 1.8266509512506375, + "z": 1.369988213437978, + "a": 0, + "b": 4.5666273781265945, + "c": 6.849941067189891, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 3.734469586356081, + 2.1404076016288105, + -0.47359529339455797, + 4.5666273781265945, + 6.849941067189891 + ] + }, + { + "tick": 51, + "pose": { + "x": 3.5081977064733154, + "y": 2.3387984709822103, + "z": 1.7540988532366577, + "a": 0, + "b": 5.846996177455527, + "c": 8.77049426618329, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 4.697109256078299, + 2.8463701111273325, + -0.6873909433076797, + 5.846996177455527, + 8.77049426618329 + ] + }, + { + "tick": 61, + "pose": { + "x": 4.276418986070675, + "y": 2.8509459907137833, + "z": 2.1382094930353377, + "a": 0, + "b": 7.12736497678446, + "c": 10.69104746517669, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 5.616802091780549, + 3.5947903381220483, + -0.9314954422957076, + 7.12736497678446, + 10.69104746517669 + ] + }, + { + "tick": 71, + "pose": { + "x": 5.0446402656680345, + "y": 3.3630935104453563, + "z": 2.5223201328340172, + "a": 0, + "b": 8.407733776113393, + "c": 12.611600664170087, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 6.490714114233835, + 4.383401667979941, + -1.2027767333283421, + 8.407733776113393, + 12.611600664170087 + ] + }, + { + "tick": 81, + "pose": { + "x": 5.812861545265394, + "y": 3.8752410301769293, + "z": 2.906430772632697, + "a": 0, + "b": 9.688102575442324, + "c": 14.532153863163485, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 7.3162687898287935, + 5.209842640574597, + -1.4979183781553669, + 9.688102575442324, + 14.532153863163485 + ] + }, + { + "tick": 91, + "pose": { + "x": 6.581082824862754, + "y": 4.387388549908502, + "z": 3.290541412431377, + "a": 0, + "b": 10.968471374771257, + "c": 16.452707062156886, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 8.091159471131004, + 6.071661261012413, + -1.813438064699831, + 10.968471374771257, + 16.452707062156886 + ] + }, + { + "tick": 101, + "pose": { + "x": 7.3493041044601135, + "y": 4.899536069640075, + "z": 3.6746520522300568, + "a": 0, + "b": 12.24884017410019, + "c": 18.373260261150286, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 8.813360564765546, + 6.966319466038303, + -2.145706912515344, + 12.24884017410019, + 18.373260261150286 + ] + }, + { + "tick": 111, + "pose": { + "x": 8.117525384057473, + "y": 5.411683589371648, + "z": 4.058762692028736, + "a": 0, + "b": 13.529208973429121, + "c": 20.29381346014368, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 9.48113737670538, + 7.891197739179992, + -2.4909694918577667, + 13.529208973429121, + 20.29381346014368 + ] + }, + { + "tick": 121, + "pose": { + "x": 8.885746663654832, + "y": 5.923831109103221, + "z": 4.442873331827416, + "a": 0, + "b": 14.809577772758056, + "c": 22.214366659137085, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 10.093054590514853, + 8.843599867464375, + -2.8453644688717326, + 14.809577772758056, + 22.214366659137085 + ] + }, + { + "tick": 131, + "pose": { + "x": 9.653967943252193, + "y": 6.435978628834794, + "z": 4.826983971626096, + "a": 0, + "b": 16.089946572086987, + "c": 24.13491985813048, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 10.647983339758778, + 9.820757832324189, + -3.2049457866916047, + 16.089946572086987, + 24.13491985813048 + ] + }, + { + "tick": 141, + "pose": { + "x": 10.422189222849552, + "y": 6.9481261485663675, + "z": 5.211094611424776, + "a": 0, + "b": 17.37031537141592, + "c": 26.05547305712388, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 11.145106841607806, + 10.819836827108439, + -3.5657042899157076, + 17.37031537141592, + 26.05547305712388 + ] + }, + { + "tick": 151, + "pose": { + "x": 11.19041050244691, + "y": 7.4602736682979405, + "z": 5.595205251223455, + "a": 0, + "b": 18.650684170744853, + "c": 27.97602625611728, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 11.5839245646291, + 11.837940393416295, + -3.92358969794088, + 18.650684170744853, + 27.97602625611728 + ] + }, + { + "tick": 161, + "pose": { + "x": 11.913625063560401, + "y": 7.942416709040268, + "z": 5.9568125317802005, + "a": 0, + "b": 19.85604177260067, + "c": 29.784062658901004, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 11.943584949172966, + 12.811138047960743, + -4.254236690404012, + 19.85604177260067, + 29.784062658901004 + ] + }, + { + "tick": 171, + "pose": { + "x": 12, + "y": 8, + "z": 6, + "a": 0, + "b": 20, + "c": 30, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 11.983077118606069, + 12.928203230275509, + -4.293154849418908, + 20, + 30 + ] + } + ], + "offsets": { + "tool_offset": 0, + "x_rot_point": 0, + "y_rot_point": 0, + "z_rot_point": 0, + "x_offset": -20, + "y_offset": 0, + "z_offset": -15 + }, + "final": { + "tick": 171, + "pose": { + "x": 12, + "y": 8, + "z": 6, + "a": 0, + "b": 20, + "c": 30, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 11.983077118606069, + 12.928203230275509, + -4.293154849418908, + 20, + 30 + ] + }, + "preview_hash": "fnv1a64:2595e1a5d1cf33eb", + "trace_hash": "fnv1a64:ff4a1d69daa88c50", + "model_samples": 4 + }, + "modified": { + "trace": [ + { + "tick": 11, + "pose": { + "x": 0.43531258808385337, + "y": 0.29020839205590226, + "z": 0.21765629404192668, + "a": 0, + "b": 0.7255209801397556, + "c": 1.0882814702096335, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 0.1890896373468145, + 0.1262246633519588, + 0.5184793885068615, + 0.7255209801397556, + 1.0882814702096335 + ] + }, + { + "tick": 21, + "pose": { + "x": 1.20353386768123, + "y": 0.8023559117874867, + "z": 0.601766933840615, + "a": 0, + "b": 2.0058897794687165, + "c": 3.0088346692030745, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 0.524992860948668, + 0.3823639705900179, + 1.4187990309839122, + 2.0058897794687165, + 3.0088346692030745 + ] + }, + { + "tick": 31, + "pose": { + "x": 1.9717551472785966, + "y": 1.3145034315190645, + "z": 0.9858775736392983, + "a": 0, + "b": 3.286258578797661, + "c": 4.929387868196492, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 0.8612510860412996, + 0.6798283334892181, + 2.3012814518113096, + 3.286258578797661, + 4.929387868196492 + ] + }, + { + "tick": 41, + "pose": { + "x": 2.739976426875956, + "y": 1.8266509512506375, + "z": 1.369988213437978, + "a": 0, + "b": 4.5666273781265945, + "c": 6.849941067189891, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 1.1951143551048027, + 1.0170162320714038, + 3.1671581958301296, + 4.5666273781265945, + 6.849941067189891 + ] + }, + { + "tick": 51, + "pose": { + "x": 3.5081977064733154, + "y": 2.3387984709822103, + "z": 1.7540988532366577, + "a": 0, + "b": 5.846996177455527, + "c": 8.77049426618329, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 1.5239884156349035, + 1.3922271952749217, + 4.017862243517918, + 5.846996177455527, + 8.77049426618329 + ] + }, + { + "tick": 61, + "pose": { + "x": 4.276418986070675, + "y": 2.8509459907137833, + "z": 2.1382094930353377, + "a": 0, + "b": 7.12736497678446, + "c": 10.69104746517669, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 1.8454490485581694, + 1.8036651962536698, + 4.855015307951234, + 7.12736497678446, + 10.69104746517669 + ] + }, + { + "tick": 71, + "pose": { + "x": 5.0446402656680345, + "y": 3.3630935104453563, + "z": 2.5223201328340172, + "a": 0, + "b": 8.407733776113393, + "c": 12.611600664170087, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 2.157255464108168, + 2.249442214393575, + 5.680414181744255, + 8.407733776113393, + 12.611600664170087 + ] + }, + { + "tick": 81, + "pose": { + "x": 5.812861545265394, + "y": 3.8752410301769293, + "z": 2.906430772632697, + "a": 0, + "b": 9.688102575442324, + "c": 14.532153863163485, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 2.4573627042376387, + 2.7275819583097274, + 6.496016198415771, + 9.688102575442324, + 14.532153863163485 + ] + }, + { + "tick": 91, + "pose": { + "x": 6.581082824862754, + "y": 4.387388549908502, + "z": 3.290541412431377, + "a": 0, + "b": 10.968471374771257, + "c": 16.452707062156886, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 2.7439329949466504, + 3.2360237438418107, + 7.303923876441846, + 10.968471374771257, + 16.452707062156886 + ] + }, + { + "tick": 101, + "pose": { + "x": 7.3493041044601135, + "y": 4.899536069640075, + "z": 3.6746520522300568, + "a": 0, + "b": 12.24884017410019, + "c": 18.373260261150286, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 3.0153459964447684, + 3.772626520828604, + 8.106368817774932, + 12.24884017410019, + 18.373260261150286 + ] + }, + { + "tick": 111, + "pose": { + "x": 8.117525384057473, + "y": 5.411683589371648, + "z": 4.058762692028736, + "a": 0, + "b": 13.529208973429121, + "c": 20.29381346014368, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 3.270207903818072, + 4.335173042214558, + 8.905694935839493, + 13.529208973429121, + 20.29381346014368 + ] + }, + { + "tick": 121, + "pose": { + "x": 8.885746663654832, + "y": 5.923831109103221, + "z": 4.442873331827416, + "a": 0, + "b": 14.809577772758056, + "c": 22.214366659137085, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 3.5073593558208955, + 4.9213741688230375, + 9.704341090935115, + 14.809577772758056, + 22.214366659137085 + ] + }, + { + "tick": 131, + "pose": { + "x": 9.653967943252193, + "y": 6.435978628834794, + "z": 4.826983971626096, + "a": 0, + "b": 16.089946572086987, + "c": 24.13491985813048, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 3.725882114537516, + 5.528873302922237, + 10.504823213579794, + 16.089946572086987, + 24.13491985813048 + ] + }, + { + "tick": 141, + "pose": { + "x": 10.422189222849552, + "y": 6.9481261485663675, + "z": 5.211094611424776, + "a": 0, + "b": 17.37031537141592, + "c": 26.05547305712388, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 3.925104483940789, + 6.1552509435110565, + 11.309715998597861, + 17.37031537141592, + 26.05547305712388 + ] + }, + { + "tick": 151, + "pose": { + "x": 11.19041050244691, + "y": 7.4602736682979405, + "z": 5.595205251223455, + "a": 0, + "b": 18.650684170744853, + "c": 27.97602625611728, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 4.104605440791584, + 6.798029356063859, + 12.121634254689384, + 18.650684170744853, + 27.97602625611728 + ] + }, + { + "tick": 161, + "pose": { + "x": 11.913625063560401, + "y": 7.942416709040268, + "z": 5.9568125317802005, + "a": 0, + "b": 19.85604177260067, + "c": 29.784062658901004, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 4.255414604167775, + 7.415870566552133, + 12.894767684082103, + 19.85604177260067, + 29.784062658901004 + ] + }, + { + "tick": 171, + "pose": { + "x": 12, + "y": 8, + "z": 6, + "a": 0, + "b": 20, + "c": 30, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 4.272248646388174, + 7.49038105676658, + 12.98778096429175, + 20, + 30 + ] + } + ], + "offsets": { + "tool_offset": 0, + "x_rot_point": 9, + "y_rot_point": -7, + "z_rot_point": 13, + "x_offset": 15, + "y_offset": 0, + "z_offset": -4 + }, + "final": { + "tick": 171, + "pose": { + "x": 12, + "y": 8, + "z": 6, + "a": 0, + "b": 20, + "c": 30, + "u": 0, + "v": 0, + "w": 0 + }, + "joints": [ + 4.272248646388174, + 7.49038105676658, + 12.98778096429175, + 20, + 30 + ] + }, + "preview_hash": "fnv1a64:2595e1a5d1cf33eb", + "trace_hash": "fnv1a64:f89774102510f899", + "model_samples": 4 + }, + "repeat_hash": "fnv1a64:f89774102510f899", + "first_divergent_tick": 11 + } + ], + "state_hash": "fnv1a64:d6d3dd87c1c1c0e2" +} diff --git a/coverage/trt-parametric/xyzac-trt-model.png b/coverage/trt-parametric/xyzac-trt-model.png new file mode 100644 index 0000000..db8262f Binary files /dev/null and b/coverage/trt-parametric/xyzac-trt-model.png differ diff --git a/coverage/trt-parametric/xyzbc-trt-model.png b/coverage/trt-parametric/xyzbc-trt-model.png new file mode 100644 index 0000000..4c8e6b5 Binary files /dev/null and b/coverage/trt-parametric/xyzbc-trt-model.png differ diff --git a/docs/trt-parametric/01-项目功能内容.md b/docs/trt-parametric/01-项目功能内容.md new file mode 100644 index 0000000..5855d08 --- /dev/null +++ b/docs/trt-parametric/01-项目功能内容.md @@ -0,0 +1,34 @@ +# 项目功能内容 + +## 目标与边界 + +在同一控制界面支持 XYZAC/XYZBC 参数化机床。INI 是机床几何输入的唯一来源;LinuxCNC WASM 是解释器、运动学、轨迹和状态的唯一计算权威。React 负责参数草稿、命令和状态显示,Three.js 负责几何投影。 + +配置文件位于 `linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/`。 + +| 参数 | XYZAC | XYZBC | 单位 | +| --- | --- | --- | --- | +| x-rot-point / y-rot-point / z-rot-point | 支持 | 支持 | mm | +| x-offset | 原生算法不支持 | 支持 | mm | +| y-offset | 支持 | 原生算法不支持 | mm | +| z-offset | 支持 | 支持 | mm | + +不支持的偏置在界面禁用;不得把无效参数呈现为可影响运动的参数。刀长仍由现有刀具链路管理,不混入这六项几何参数。 + +## 用户流程 + +1. 选择机床,读取对应 INI 的 `[HAL] HALCMD` 参数及源位置。 +2. 结构预览旁按“旋转中心”和“轴间偏置”分组,显示 mm、已生效值及待应用状态。 +3. 修改参数,验证有限数字、支持范围和文件一致性。 +4. 在机床关闭、程序空闲时应用,重新初始化配置和解析当前 G 代码;应用后重新回零。 +5. 保存/导出修改后的 INI;重新读取磁盘文件时使用最新参数。 + +浏览器虚拟工作区与宿主磁盘须明确区分:应用到 Worker 不等于已写回仓库文件;支持文件句柄的浏览器可保存已选文件,其他环境导出 INI。 + +## 验收行为 + +- 修改有效参数后,Worker 快照参数、结构旋转中心及非零旋转姿态的 WASM 关节解一致变化。 +- G 代码文本和编程坐标不必变化;同一程序的机器关节轨迹应随有效参数变化。禁止人为平移 G 代码来伪造结果。 +- 参数错误、程序运行/暂停、机床开启时不允许应用;失败不得显示成功或展示过期参数为已生效。 +- 两种机床参数相互隔离;保留当前程序;重新加载产生新预览,清理旧运行轨迹。 +- 桌面和移动布局无重叠,3D 非空、可交互,保存和重新读取可复现参数。 diff --git a/docs/trt-parametric/02-项目程序开发详细步骤.md b/docs/trt-parametric/02-项目程序开发详细步骤.md new file mode 100644 index 0000000..229505c --- /dev/null +++ b/docs/trt-parametric/02-项目程序开发详细步骤.md @@ -0,0 +1,25 @@ +# 项目程序开发详细步骤 + +## TRT-P01 配置源与上下文 + +审计 `sim-case-loader.mjs` 的固定参数、`SimSession.LOAD_WORKSPACE`、`WasmXyzacKinematics`、原生 TRT 方程和 Three.js 模型。建立六份文档及索引,记录工作区原有改动。 + +## TRT-P02 INI 参数入口 + +复用 `parseIni` 和 `WorkspaceRuntime`,按顺序读取重复 HALCMD;识别旋转中心 setp 和偏置 net/sets。数值、重复赋值、缺项和不支持项必须可诊断,不默默回落到固定几何。编辑时仅替换定位到的参数数值,保留文件其他内容。配置读取器同时供加载器和 Worker 使用,Worker 从收到的文件再次读取,拒绝外部独立 offsets 覆盖。 + +## TRT-P03 应用与 G 代码 + +复用 `LOAD_WORKSPACE` 与 `PROGRAM_LOAD`,不新增 JS 运动算法。重新加载配置前验证输入及状态;重建 WASM 运动学、预览和运行环境。验证 RTCP 开启下非零 A/B/C 的同一段 G 代码产生不同关节轨迹,身份运动学模式不应伪装为 RTCP。 + +## TRT-P04 结构预览 + +从 Worker 快照读取六项参数。用旋转中心父节点及局部偏置表达模型层级,保持线性轴和刀具来源不变。验证零角度、单轴旋转和复合旋转,路径仍附着在转台坐标系。这里的矩阵只用于图形投影。 + +## TRT-P05 参数界面 + +在机床结构预览处放置紧凑参数区:旋转中心三项、偏置三项;无效偏置禁用。提供应用、重新读取、导入和保存 INI。维护草稿与已生效状态,异步期间禁止重复应用或切换,错误可见。保持当前 G 代码,应用后要求重新回零。 + +## TRT-P06 验收与接续 + +执行参数读取/写回负例、真实 WASM Worker 两机型测试、结构模型断言、React typecheck/build 和 Playwright 桌面/移动截图及 canvas 像素检查。证据统一写入 `coverage/trt-parametric/`,实际命令和产物登记到 05。记录未满足项,不用旧报告替代本轮测试。 diff --git a/docs/trt-parametric/03-推进台账.md b/docs/trt-parametric/03-推进台账.md new file mode 100644 index 0000000..13a44ed --- /dev/null +++ b/docs/trt-parametric/03-推进台账.md @@ -0,0 +1,23 @@ +# 推进台账 + +## R02 / 2026-09-17 / 仓库保存 + +- 用户授权:将本轮成果保存到 `https://gitea.meswork.com/wangdequan/linuxcncWebWASM.git`。 +- 保存范围:TRT 参数化代码、测试、六份项目文档及索引、`coverage/trt-parametric/` 验收报告与截图。 +- 检查:远程 `origin` 与指定仓库一致;提交前已获取远程 main,差异格式检查通过。功能验证沿用 E02/E03/E04,代码未再变更。 +- 原有两份大体积 tick trace、linuxcnc 子模块本地改动、临时脚本和用户图片继续留在本地,不并入本次专项提交。 +- 目标分支:`main`;推送完成以 Git 远程分支提交 ID 与本地 HEAD 一致为准。 +- 后续开发仍按 TRT-R01 接续。 + +## R01 / 2026-09-17 / 已完成 + +- 目标:建立上下文,完成 INI → WASM → G 代码/机床预览参数链路。 +- 已检查:加载器当前使用固定 offsets;WASM ABI 已有全部旋转中心和偏置字段;模型缺失旋转中心投影。 +- 已改文件:本目录六份文档、README;根 README 增加入口;新增 `web/workspace/trt-geometry.mjs`、`web/react/src/TrtGeometryPanel.tsx`、`web/react/src/test_trt_parametric.mjs`、`scripts/test_trt_parametric_browser.mjs`。 +- 接入改动:`sim-case-loader.mjs`、`sim-session.mjs`、`App.tsx`、`legacy.d.ts`、`styles.css`、`xyzac-machine-model.ts`、`XyzacMachineView.tsx`;更新相关加载、回零、两机型程序、模型和浏览器回归测试。 +- 完成行为:INI 参数读写和 Worker 二次校验;应用时重载当前程序、清理轨迹并失效回零;旧几何 checkpoint/trace 拒绝恢复;分组参数界面、文件导入和原生 INI 保存。 +- 结构修正:三个 rot-point 进入转台层级;XYZBC 的固定 90 度仅作用于铸件,旋转坐标系按 WASM 保持一致;补齐原生 yslide 的 150 mm 高度。 +- 验证:E02/E03/E04。两机型真实 WASM 程序、确定性重放、结构/刀尖一致性、船体程序、参数保存导入、桌面/移动布局和 3D 像素/交互通过。React 类型和构建通过。 +- 中间失败及处理:旧测试假定 HOME 同步完成,改为等待真实 homing tick;缺失图片测试改用显式 emc2.gif 输入,避免依赖用户修改过的 INI;机型选择测试改为等待 Worker 成功后断言。浏览器新测试改为等待重载完成并排除 hidden 文件 input 的布局断言。 +- 原有改动:两份 coverage tick trace、linuxcnc 子模块内多处 INI/C++/Tcl 改动、临时测试脚本和 PNG 均保留,不作为本轮产物。 +- 下一步:TRT-R01。既有 `test_xyzac_recovery.mjs` 虽退出 0,但报告 `endpoint_at_checkpoint_equal=false`;不能作为完整恢复等价证据。该问题单独登记,不扩展本轮到 TP 恢复重构。 diff --git a/docs/trt-parametric/04-任务矩阵.md b/docs/trt-parametric/04-任务矩阵.md new file mode 100644 index 0000000..95b7a42 --- /dev/null +++ b/docs/trt-parametric/04-任务矩阵.md @@ -0,0 +1,13 @@ +# 任务矩阵 + +状态只使用:待办、进行中、已验收、阻塞。已验收必须引用 05 中的证据编号。 + +| 编号 | 内容 | 依赖 | 状态 | 验收标准 | 证据 | +| --- | --- | --- | --- | --- | --- | +| TRT-P01 | 来源审计与上下文文档 | 无 | 已验收 | 六份文档、索引、源语义和现存缺口可追溯 | E01 | +| TRT-P02 | INI 唯一参数入口 | P01 | 已验收 | 两机型参数读写、无效输入拒绝、取消固定值覆盖 | E02 | +| TRT-P03 | Worker 与 G 代码重新解析 | P02 | 已验收 | 文件参数进入 WASM;同程序旋转姿态关节解随参数改变;状态门控有效 | E02 | +| TRT-P04 | 参数化结构投影 | P02 | 已验收 | 旋转中心及偏置驱动两模型;转台路径层级一致 | E02 | +| TRT-P05 | 参数编辑及文件流程 | P03,P04 | 已验收 | 分组布局、应用/重新读取/导入/保存、保留程序、错误反馈 | E03 | +| TRT-P06 | 回归及证据收口 | P03,P04,P05 | 已验收 | 专项、相关回归、构建、桌面/移动交互截图和像素检查通过;既有恢复偏差独立登记 | E02,E03,E04 | +| TRT-R01 | 既有 checkpoint 恢复状态精度 | P06 | 待办 | 定位 HAL tick 与 TP queue/progress 恢复差异;将恢复测试的相等性变为有效断言,再验证固定 tick 连续与恢复轨迹 | E04:遗留项 | diff --git a/docs/trt-parametric/05-验收证据.md b/docs/trt-parametric/05-验收证据.md new file mode 100644 index 0000000..92beb18 --- /dev/null +++ b/docs/trt-parametric/05-验收证据.md @@ -0,0 +1,58 @@ +# 验收证据 + +只登记实际执行结果。job_id/report_id 由真实运行产生;没有的字段记“不适用”,不编造 PDF 或截图。 + +## E01 来源审计 / 2026-09-17 + +- 命令:`git status --short --branch`、`git -C linuxcnc status --short`、`rg -n 'offset|rot_point' wasm-core/src/sim_kinematics.c`。 +- 基线:主仓库 HEAD `9232f290279459765333b2ac274aa762ec06d6c0`。 +- 发现:`web/workspace/sim-case-loader.mjs` 中 CASE_KINEMATICS 固定几何值;`web/react/src/xyzac-machine-model.ts` 未投影 rot-point。 +- 方程:`wasm-core/src/sim_kinematics.c` 的 XYZAC 使用 dy/dz,XYZBC 使用 dx/dz,二者均使用三个 rot-point。 +- 结果:审计完成;这不是功能验收。 +- job_id / report_id / PDF / 截图:不适用。 + +## E02 参数链路与结构投影 + +- 命令:`node web/react/src/test_trt_parametric.mjs`。 +- report_id:`trt-parametric-worker-v1`;产物:`coverage/trt-parametric/worker.report.json`。 +- 结果:XYZAC/XYZBC 的参数读写、无效数值/缺项/重复项拒绝、CRLF/注释保留、INI 引用、Worker 忽略外部 offsets 覆盖通过。 +- 同程序基线与新参数在 tick 11 出现首个关节差异;编程终点相同;新参数的固定 tick 重放逐项一致。 +- 每个配置检查零角度、单倾转、单 C 旋转、复合旋转共四种姿态,WASM 逆解驱动的模型编程点与刀尖误差小于 `1e-7` 场景单位。 +- 运行后的重载清空预览、重置回零状态;机床开启时重载被拒绝。 +- `ctest --test-dir build-native -R 'sim_kinematics' --output-on-failure`:已有 native 二进制 5/5 通过,不等同于新建官方 native trace 对照。 +- `node web/react/src/test_xyzac_threejs.mjs`:通过。 +- 参数修改后恢复旧 checkpoint、compact power-off checkpoint、trace 均拒绝,当前几何及未回零状态不变。 +- WASM 文件 SHA-256:`ab1b90a6a2474b954849ee75d1163e11ee37c4653dd074bba3483d52d9dec2a9`。本轮使用已有核心,不修改 C ABI。 +- worker.report.json SHA-256:`e5cba3c7fe4aa04408cc1f7a574d4044090bbdc4bf85fbcb8f0cd3cd48b27aa0`。 + +## E03 浏览器端到端 + +命令: + +```bash +PLAYWRIGHT_MODULE=/home/mes006/.npm/_npx/e41f203b7505f1fb/node_modules/playwright/index.mjs node scripts/test_trt_parametric_browser.mjs +``` + +- 页面:`http://127.0.0.1:5174/`;report_id:`trt-parametric-browser-v1`。 +- 报告:[browser.report.json](../../coverage/trt-parametric/browser.report.json)。SHA-256:`957c93d24202823880630937e74b8c3f23e5786bc9d1d7a7219dff98f9d573aa`。 +- 两机型均验证应用、保持程序路径、导出原生 INI、重新读取、重新导入应用、开机禁用/关机解锁。 +- 导出的 INI 保留 `LIB:basic_sim.tcl`,不泄露 Web `_cmds.hal` 替换;导出分别为 4867 和 4875 字符。 +- 1440×1000 与 390×844 布局无页面横向溢出;参数控件均位于视口内;canvas 分别为 472×360 和 368×378,颜色数 1691/1804,旋转前后像素 hash 改变;pageerror 为零。 +- 最终截图:[桌面](../../coverage/trt-parametric/layout-1440.png)、[移动端](../../coverage/trt-parametric/layout-390.png)、[XYZAC](../../coverage/trt-parametric/xyzac-trt-model.png)、[XYZBC](../../coverage/trt-parametric/xyzbc-trt-model.png)。均已人工查看。 +- `failure.png` 保留中间测试失败截图;最终结论以 report 和 layout 截图为准。 +- job_id / PDF:不适用。浏览器保存对话框本身需要用户选择磁盘位置;自动测试验证下载回退及其文件内容。 + +## E04 相关回归与边界 + +| 命令 | 结果 | +| --- | --- | +| `npm --prefix web/react run build` | TypeScript 和生产构建通过;现有大 chunk 警告仍在 | +| `node web/workspace/test_sim_case_loader.mjs` | 通过 | +| `node web/worker/test_xyzac_trt_case.mjs` | 通过,TASK_READY、五关节、M428/M429/M430 和直接切换 | +| `node web/worker/test_boat_trt_programs.mjs` | XYZAC/XYZBC 均完成程序 | +| `node web/worker/test_xyzac_program_replace.mjs` | 通过,失败解析不保留旧预览 | +| `PLAYWRIGHT_MODULE=... node scripts/test_xyzbc_panel_browser.mjs` | 通过,双 canvas、轴序、转台路径附着、无 HUD | +| `git diff --check -- README.md web scripts docs/trt-parametric` | 通过 | +| `node web/worker/test_xyzac_recovery.mjs` | 退出 0,但其内部输出恢复前后 HAL tick / TP progress / queue 状态不同,`endpoint_at_checkpoint_equal=false`;登记 TRT-R01,不能声称恢复完全等价 | + +本次未重跑全仓历史发布 gate、全部浏览器矩阵或官方 native 对照。专项测试验证浏览器仿真,不是实际机床控制验收。 diff --git a/docs/trt-parametric/06-决策记录.md b/docs/trt-parametric/06-决策记录.md new file mode 100644 index 0000000..ff40bb0 --- /dev/null +++ b/docs/trt-parametric/06-决策记录.md @@ -0,0 +1,33 @@ +# 决策记录 + +## D01 / 2026-09-17 / 几何参数唯一入口 + +INI 文件内容是几何输入来源,Worker 再次解析文件后传入已有 WASM ABI。移除生产加载链路的固定 offsets,避免界面、预览和执行各自持有参数。 + +## D02 / 2026-09-17 / 保持原生参数含义 + +XYZAC 不支持 x-offset,XYZBC 不支持 y-offset。六项都可见,但不支持项禁用且不允许非零值。三个 rot-point 在 WASM 已有支持;原 Vismach 不支持不构成本次 Web 参数化的限制。新需求覆盖旧设计文档中不得开放 rot-point 编辑的历史约束。 + +## D03 / 2026-09-17 / 配置更改采用重载 + +先做关机、空闲状态下的完整重载,不在运行中热修改几何。重建配置、重新解析当前程序并重新回零,以免旧轨迹或回零结果与新结构混用。参数变化改变机器解,编程坐标的路径允许保持相同。 + +## D04 / 2026-09-17 / 文件保存边界 + +浏览器应用到 Worker 只修改虚拟配置,不冒充宿主文件已保存。采用显式文件导入和保存/导出;直接修改仓库 INI 后重新读取也应生效。静态部署无需增加写磁盘服务。 + +## D05 / 2026-09-17 / 证据与范围 + +本专项不改历史发布 gate 或 context lock,不宣称全 LinuxCNC 对等。每轮以任务矩阵和实测证据接续,未运行验证标为待办。保留工作区已有改动。 + +## D06 / 2026-09-17 / 图形变换与原生方程一致 + +转台层级采用旋转中心平移、轴间偏置平移、倾转、反向偏置、C 旋转及编程原点回移。XYZBC 的 90 度铸件朝向与 B 倾转坐标系分开;原生 `xyzbc-trt-gui.py` 的 yslide 基准是 150 mm。用 WASM 逆解输出驱动模型,在四种姿态验证模型上的编程点与刀尖重合,不在 JavaScript 中复制逆解方程。 + +## D07 / 2026-09-17 / 恢复与文件能力边界 + +参数改变后旧 checkpoint/trace 不得恢复到新几何,恢复前检查机型及六项几何值。完整恢复状态精度另由 TRT-R01 跟踪。INI 读取支持现有 include/引用解析;单文件编辑导出不支持把分散在 include 中的参数打包,界面对此明确报错,需修改实际包含文件后重新读取。 + +## D08 / 2026-09-17 / 异步界面状态 + +机型选择在 Worker 成功载入后提交;重新读取期间锁定主界面加载动作。参数应用成功但当前程序解析失败时,保留已生效参数并显示程序解析错误,旧路径已清理;不声称整笔操作回滚成功。 diff --git a/docs/trt-parametric/README.md b/docs/trt-parametric/README.md new file mode 100644 index 0000000..a6430fc --- /dev/null +++ b/docs/trt-parametric/README.md @@ -0,0 +1,18 @@ +# 五轴 TRT 参数化设计接续索引 + +本目录对应 2026-09-17 用户要求:在机床结构预览中配置 `xyzac-trt.ini`、`xyzbc-trt.ini`,统一几何参数、LinuxCNC WASM 运动学和 G 代码执行结果。 + +## 读取顺序 + +1. 本索引 → [04-任务矩阵](04-任务矩阵.md):选择首个依赖满足且未验收的任务。 +2. [03-推进台账](03-推进台账.md):只读最新一轮和下一步。 +3. 按任务读取 [01-项目功能内容](01-项目功能内容.md)、[02-项目程序开发详细步骤](02-项目程序开发详细步骤.md) 和直接相关代码。 +4. 实施前核对 [06-决策记录](06-决策记录.md),验收后更新 [05-验收证据](05-验收证据.md)、矩阵和台账。 + +每轮必须记录实际改动、命令、结果和下一任务。未运行的检查不能记为通过;历史通过任务仅在相关代码改变时回归。此目录是本专项接续入口,历史仿真/发布轨道仍保留原有指针,不据此扩大发布范围。 + +当前状态:`TRT-P01` 至 `TRT-P06` 已验收。下一项为矩阵中的 `TRT-R01`,属于既有恢复精度问题,不影响本专项参数应用流程。实现不依赖聊天记录。用户文字提及 Image #1,但本轮未收到可读取的图片资源,布局按现有结构预览实现。 + +本地体验地址:`http://127.0.0.1:5174/`。启动命令:`npm --prefix web/react run dev -- --port 5174 --strictPort`;该端口被占用时选择其他端口。 + +在机床结构预览下方修改参数,关机且空闲时应用。保存 INI 会打开浏览器保存对话框,或下载文件;应用本身不覆盖磁盘。直接修改仓库里的 INI 后,点击“重新读取 INI”即可重新解析当前程序。 diff --git a/scripts/test_trt_parametric_browser.mjs b/scripts/test_trt_parametric_browser.mjs new file mode 100644 index 0000000..007df55 --- /dev/null +++ b/scripts/test_trt_parametric_browser.mjs @@ -0,0 +1,90 @@ +import assert from "node:assert/strict"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { pathToFileURL } from "node:url"; + +const { chromium } = await import(pathToFileURL(process.env.PLAYWRIGHT_MODULE).href); +const output = new URL("../coverage/trt-parametric/", import.meta.url); +await mkdir(output, { recursive: true }); +const browser = await chromium.launch({ executablePath: "/usr/bin/google-chrome", headless: true, args: ["--no-sandbox", "--use-angle=swiftshader", "--enable-unsafe-swiftshader"] }); +const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } }); +await page.addInitScript(() => { window.showSaveFilePicker = undefined; }); +const errors = []; +page.on("pageerror", (error) => errors.push(error.message)); +const control = (id) => page.locator(`[data-axis-control="${id}"]`); +const machine = page.locator(".xyzac-machine-view"); +const panel = page.locator(".trt-geometry"); +const settled = async () => { + await page.waitForFunction(() => document.querySelector("main")?.dataset.axisProgramLoading === "false" && document.querySelector(".trt-geometry")?.getAttribute("aria-busy") === "false"); + await page.waitForTimeout(350); +}; +const pixels = () => machine.locator("canvas[data-engine]").evaluate((canvas) => { + const gl = canvas.getContext("webgl2"); const bytes = new Uint8Array(canvas.width * canvas.height * 4); + gl.readPixels(0, 0, canvas.width, canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, bytes); + let hash = 2166136261; const colors = new Set(); + for (let i = 0; i < bytes.length; i += 16) { hash = Math.imul(hash ^ bytes[i], 16777619); colors.add(`${bytes[i]},${bytes[i + 1]},${bytes[i + 2]}`); } + return { hash: hash >>> 0, colors: colors.size, width: canvas.width, height: canvas.height }; +}); +const report = { report_id: "trt-parametric-browser-v1", status: "running", machines: [], viewports: [] }; +try { + await page.goto(process.env.XYZAC_PANEL_URL ?? "http://127.0.0.1:5174/"); await settled(); + for (const [id, unsupported] of [["xyzac-trt", "x-offset"], ["xyzbc-trt", "y-offset"]]) { + if (id === "xyzbc-trt") { await control("machine-type-select").selectOption({ label: "XYZBC TRT" }); await settled(); } + assert.equal(await machine.getAttribute("data-xyzac-model-id"), id); + assert.equal(await panel.getByLabel(unsupported, { exact: true }).isDisabled(), true); + const beforePath = await page.locator("main").getAttribute("data-axis-program-path"); + const original = JSON.parse(await machine.getAttribute("data-xyzac-offsets")); + const before = await pixels(); assert.ok(before.colors > 100); + await panel.getByLabel("x-rot-point", { exact: true }).fill("24"); + await panel.getByLabel("y-rot-point", { exact: true }).fill("-12"); + await panel.getByLabel("z-rot-point", { exact: true }).fill("8"); + await control("geometry-apply").click(); await settled(); + const offsets = JSON.parse(await machine.getAttribute("data-xyzac-offsets")); + assert.equal(offsets.x_rot_point, 24); assert.equal(offsets.y_rot_point, -12); assert.equal(offsets.z_rot_point, 8); + assert.equal(await page.locator("main").getAttribute("data-axis-program-path"), beforePath); + assert.notEqual((await pixels()).hash, before.hash); + await machine.screenshot({ path: new URL(`${id}-model.png`, output).pathname }); + const downloading = page.waitForEvent("download"); await control("geometry-save").click(); + const download = await downloading; + assert.equal(download.suggestedFilename(), `${id}.ini`); + const exported = await readFile(await download.path(), "utf8"); + assert.match(exported, /x-rot-point\s+24/); assert.match(exported, /LIB:basic_sim.tcl/); + assert.doesNotMatch(exported, /HALFILE = .*_cmds.hal/); + await control("geometry-reload").click(); await settled(); + assert.deepEqual(JSON.parse(await machine.getAttribute("data-xyzac-offsets")), original); + await panel.locator('input[type="file"]').setInputFiles({ name: `${id}.ini`, mimeType: "text/plain", buffer: Buffer.from(exported) }); + await page.waitForFunction(() => document.querySelector('.trt-geometry input[aria-label="x-rot-point"]')?.value === "24"); + await control("geometry-apply").click(); await settled(); + assert.equal(JSON.parse(await machine.getAttribute("data-xyzac-offsets")).x_rot_point, 24); + await control("toolbar-power").click(); + await page.waitForFunction(() => document.querySelector('.trt-geometry input[aria-label="x-rot-point"]')?.disabled === true); + assert.equal(await panel.getByLabel("x-rot-point", { exact: true }).isDisabled(), true); + await control("toolbar-power").click(); + await page.waitForFunction(() => document.querySelector('.trt-geometry input[aria-label="x-rot-point"]')?.disabled === false); + assert.equal(await panel.getByLabel("x-rot-point", { exact: true }).isDisabled(), false); + report.machines.push({ id, offsets, export_bytes: exported.length, preserved_program: beforePath, canvas: before }); + } + for (const [width, height] of [[1440, 1000], [390, 844]]) { + await page.setViewportSize({ width, height }); await panel.scrollIntoViewIfNeeded(); await page.waitForTimeout(400); + const canvas = await pixels(); assert.ok(canvas.colors > 100 && canvas.width > 100 && canvas.height >= 220); + const boxes = await panel.locator("label, input:not([hidden]), button").evaluateAll((elements) => elements.map((element) => { + const r = element.getBoundingClientRect(); return { x: r.x, right: r.right, width: r.width }; + })); + assert.ok(boxes.every((r) => r.width > 0 && r.x >= 0 && r.right <= width), JSON.stringify(boxes)); + assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)); + const before = await pixels(); + await control("machine-rotateLeft").click(); await page.waitForTimeout(400); + assert.notEqual((await pixels()).hash, before.hash); + await page.screenshot({ path: new URL(`layout-${width}.png`, output).pathname, fullPage: true }); + report.viewports.push({ width, height, canvas, controls_fit: true, interactive: true }); + } + assert.deepEqual(errors, []); report.status = "verified"; +} catch (error) { + report.status = "failed"; report.error = String(error.stack ?? error); + await page.screenshot({ path: new URL("failure.png", output).pathname, fullPage: true }); + throw error; +} finally { + report.browser_errors = errors; + await writeFile(new URL("browser.report.json", output), JSON.stringify(report, null, 2) + "\n"); + await browser.close(); +} +console.log(JSON.stringify(report)); diff --git a/scripts/test_xyzbc_panel_browser.mjs b/scripts/test_xyzbc_panel_browser.mjs index 9ffbce6..c723065 100644 --- a/scripts/test_xyzbc_panel_browser.mjs +++ b/scripts/test_xyzbc_panel_browser.mjs @@ -15,8 +15,8 @@ try { await control("toolbar-power").click(); await page.waitForTimeout(300); await control("machine-type-select").selectOption({ label: "XYZBC TRT" }); - assert.equal(await control("machine-type-select").inputValue(), "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini"); await page.waitForFunction(() => document.querySelector("main")?.dataset.axisProgramPath?.includes("boat-xyzbc.ngc") && document.querySelector("main")?.dataset.axisProgramLoading === "false", null, { timeout: 45000 }); + assert.equal(await control("machine-type-select").inputValue(), "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini"); const result = await page.evaluate(() => ({ machine: document.querySelector(".xyzac-manual-summary")?.getAttribute("aria-label"), axes: [...document.querySelectorAll(".manual-axis-block .axis-radio span")].map((node) => node.textContent), diff --git a/web/react/src/App.tsx b/web/react/src/App.tsx index db610aa..11725d4 100644 --- a/web/react/src/App.tsx +++ b/web/react/src/App.tsx @@ -3,6 +3,8 @@ import { INITIAL_STATE, reduceFrontendState } from "./store"; import { ReactWorkerClient } from "./worker-client"; import { XyzacToolPreviewFromSnapshot } from "./XyzacToolPreview"; import { XyzacMachineViewFromSnapshot } from "./XyzacMachineView"; +import { TrtGeometryPanel } from "./TrtGeometryPanel"; +import { readTrtGeometry } from "../../workspace/trt-geometry.mjs"; import { loadSimulationCase, toWorkerConfiguration } from "../../workspace/sim-case-loader.mjs"; import { axisIcon } from "./sourceAssets"; import { HoldButton } from "./HoldButton"; @@ -226,6 +228,7 @@ export function App() { const [programText, setProgramTextState] = useState(""); const [programPath, setProgramPath] = useState(DEFAULT_MACHINE.program); const [programLoading, setProgramLoading] = useState(true); + const [geometryBundle, setGeometryBundle] = useState(null); const [selectedAxis, setSelectedAxis] = useState("x"); const [selectedHomeJoint, setSelectedHomeJoint] = useState(0); const [leftTab, setLeftTab] = useState<"manual" | "mdi" | "diagnostics">("manual"); @@ -317,6 +320,7 @@ export function App() { setProgramTextState(normalizedSource); setProgramPath(DEFAULT_MACHINE.program); const configuration = toWorkerConfiguration(bundle); await operationController.execute("LOAD_WORKSPACE", { root_path: "/workspace", configuration }, { state: {}, gate: false }); + setGeometryBundle(bundle); await operationController.execute("PROGRAM_LOAD", { source: normalizedSource, path: DEFAULT_MACHINE.program }, { state: {}, gate: false }); uiCacheHydrated.current = true; setOperationTrace(operationController.exportTrace()); @@ -487,7 +491,10 @@ export function App() { const normalizedSource = normalizeProgramSource(source); setCaseStatus("starting exact case..."); if (!latestLoadGate.current.enabled) throw new Error(latestLoadGate.current.reason); + if (machineOn) await operationController.execute("POWER", { active: false }, { gate: false }); const operation = await operationController.execute("LOAD_WORKSPACE", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) }, { state: axisState, gate: false }); + setGeometryBundle(bundle); + setCasePath(requestedCasePath); const resultPayload = (operation.response as { payload?: { configuration?: { state?: string } } }).payload; markPreviewProgress(28, "wasm", "LinuxCNC WASM 配置已加载,准备解析刀具路径"); setProgramText(normalizedSource); setProgramPath(programPathForCase); @@ -504,6 +511,32 @@ export function App() { } finally { setProgramLoading(false); } }; const setProgramText = (value: string) => { setProgramTextState(value); }; + const applyGeometry = async (source: any) => { + if (programLoading || machineOn || programRunning || programPaused) throw new Error("关机且程序空闲后可修改"); + setProgramLoading(true); + startPreviewProgress("机床参数"); + try { + const bundle = typeof source === "function" ? await source() : source; + readTrtGeometry(bundle); + const configuration = toWorkerConfiguration(bundle); + await operationController.execute("LOAD_WORKSPACE", { root_path: "/workspace", configuration }, { gate: false }); + setGeometryBundle(bundle); + const loaded = await operationController.execute("PROGRAM_LOAD", { source: programText, path: programPath }, { gate: false }); + finishPreviewProgress(programPath, loaded.response); + setCommandStatus("机床参数已应用,程序已重新解析,待重新回零"); + } catch (error) { + failPreviewProgress(error instanceof Error ? error.message : String(error)); + throw error; + } finally { + setOperationTrace(operationController.exportTrace()); + setProgramLoading(false); + } + }; + const reloadGeometry = () => applyGeometry(async () => { + const bundle = await loadSimulationCase({ path: casePath, baseUrl: "." }); + if (bundle.status !== "ready") throw new Error(`INI blocked: ${bundle.missing.join(",")}`); + return bundle; + }); const loadPackagedProgram = async () => { if (!canLoad) return; const path = selectedMachine.program; @@ -758,11 +791,11 @@ export function App() {
AXIS AXISLinuxCNC Web/WASM
Connection: {state.status.toUpperCase()}
execute("PROGRAM_STEP")} /> execute(programPaused ? "PROGRAM_RESUME" : "PROGRAM_PAUSE")} pressed={relief.pauseResume} /> stopProgram()} pressed={relief.abort} /> setZoom((value) => Math.min(2, value + .2))} /> setZoom((value) => Math.max(.5, value - .2))} /> { setView("z"); setRightTab("preview"); }} /> { setView("z2"); setRightTab("preview"); }} /> { setView("x"); setRightTab("preview"); }} /> { setView("y"); setRightTab("preview"); }} /> { setView("y2"); setRightTab("preview"); }} /> { setView("p"); setRightTab("preview"); }} /> setRotateMode((value) => !value)} /> xyzacCommand("VISUAL_CLEAR", {}, "vismachClear")} />
-
Load {selectedMachine.coordinates} boat program{programLoading ? "Loading program..." : caseStatus.startsWith("blocked:") ? caseStatus : `Program: ${loadedProgramName}`}{previewProgress &&
{previewProgress.detail}{previewProgress.percent}%{previewProgress.percent}%
}
+
Load {selectedMachine.coordinates} boat program{programLoading ? "Loading program..." : caseStatus.startsWith("blocked:") ? caseStatus : `Program: ${loadedProgramName}`}{previewProgress &&
{previewProgress.detail}{previewProgress.percent}%{previewProgress.percent}%
}
- { setCasePath(path); void loadCase(path); }} /> + { void loadCase(path); }} /> -
+
{(["path", "live", "limits", "velocity", "offsets", "extents"] as const).map((layer) => )}
diff --git a/web/react/src/TrtGeometryPanel.tsx b/web/react/src/TrtGeometryPanel.tsx new file mode 100644 index 0000000..ed6ddb8 --- /dev/null +++ b/web/react/src/TrtGeometryPanel.tsx @@ -0,0 +1,69 @@ +import { useEffect, useMemo, useRef, useState } from "react"; +import { readTrtGeometry, writeTrtGeometry, TRT_GEOMETRY_KEYS } from "../../workspace/trt-geometry.mjs"; +import { axisIcon } from "./sourceAssets"; + +type Bundle = { entrypoint: string; files: Record; source_ini?: string }; +type Props = { bundle: Bundle | null; disabled: boolean; busy: boolean; onApply: (bundle: Bundle) => Promise; onReload: () => Promise }; + +export function TrtGeometryPanel({ bundle, disabled, busy, onApply, onReload }: Props) { + const [values, setValues] = useState>({}); + const [message, setMessage] = useState(""); + const [pending, setPending] = useState(false); + const file = useRef(null); + const geometry = useMemo(() => bundle ? readTrtGeometry(bundle) : null, [bundle]); + useEffect(() => { + setValues(Object.fromEntries(TRT_GEOMETRY_KEYS.map((key: string) => [key, String(geometry?.offsets[key] ?? 0)]))); + setMessage(""); + }, [bundle]); + const locked = disabled || busy || pending || !bundle; + const dirty = TRT_GEOMETRY_KEYS.some((key: string) => values[key] !== String(geometry?.offsets[key] ?? 0)); + const run = async (action: () => Promise, success: string) => { + setPending(true); setMessage(""); + try { await action(); setMessage(success); } + catch (error) { setMessage(error instanceof Error ? error.message : String(error)); } + finally { setPending(false); } + }; + const edited = () => { + if (!bundle) throw new Error("INI unavailable"); + if (Object.values(geometry?.sources ?? {}).some((source) => source.path !== bundle.entrypoint)) throw new Error("参数位于包含文件,请修改源文件后重新读取 INI"); + const next = writeTrtGeometry(bundle, values); + // Export the native INI, retaining LIB:basic_sim.tcl instead of the Web fixture. + next.source_ini = writeTrtGeometry({ ...bundle, files: { ...bundle.files, [bundle.entrypoint]: bundle.source_ini ?? bundle.files[bundle.entrypoint] } }, values).files[bundle.entrypoint]; + return next; + }; + const exportIni = async () => { + const next = edited(); + const content = next.source_ini ?? next.files[next.entrypoint]; + const name = next.entrypoint.split("/").at(-1)!; + const picker = (window as unknown as { showSaveFilePicker?: (options: unknown) => Promise<{ createWritable: () => Promise<{ write: (data: string) => Promise; close: () => Promise }> }> }).showSaveFilePicker; + if (picker) { + const handle = await picker({ suggestedName: name, types: [{ description: "LinuxCNC INI", accept: { "text/plain": [".ini"] } }] }); + const stream = await handle.createWritable(); await stream.write(content); await stream.close(); + } else { + const url = URL.createObjectURL(new Blob([content], { type: "text/plain" })); + const link = document.createElement("a"); link.href = url; link.download = name; link.click(); + setTimeout(() => URL.revokeObjectURL(url), 1000); + } + }; + const tool = (id: string, label: string, asset: string, action: () => void, off = locked) => ; + return
+
{bundle?.entrypoint.split("/").at(-1) ?? "INI"}mm{dirty ? "待应用" : "已生效"}
+
{["旋转中心", "轴间偏置"].map((title, index) =>
{title}{TRT_GEOMETRY_KEYS.slice(index * 3, index * 3 + 3).map((key: string) => )}
)}
+
+ + {tool("reload", "重新读取 INI", "tool_reload.gif", () => void run(onReload, "已重新读取 INI"))} + {tool("import", "导入 INI 参数", "tool_open.gif", () => file.current?.click())} + + {message || (disabled ? "关机且程序空闲后可修改" : "")} +
+ { + const selected = event.target.files?.[0]; event.target.value = ""; + if (!selected || !bundle || locked) return; + void run(async () => { + if (selected.name !== bundle.entrypoint.split("/").at(-1)) throw new Error("INI 文件名与机型不一致"); + const imported = readTrtGeometry({ ...bundle, files: { ...bundle.files, [bundle.entrypoint]: await selected.text() } }); + setValues(Object.fromEntries(TRT_GEOMETRY_KEYS.map((key: string) => [key, String(imported.offsets[key])]))); + }, "INI 参数已导入,待应用"); + }} /> +
; +} diff --git a/web/react/src/XyzacMachineView.tsx b/web/react/src/XyzacMachineView.tsx index afcf4f6..90682e2 100644 --- a/web/react/src/XyzacMachineView.tsx +++ b/web/react/src/XyzacMachineView.tsx @@ -145,6 +145,8 @@ export function XyzacMachineView({ visual, toolpath, view = "p", zoom = 1, rotat const state = visualRef.current, config = settings.current; assembly.update(state); const paths = (config.toolpath ?? state.toolpath) as Segment[]; + pathGroup.position.copy(assembly.workOrigin.position); + liveGroup.position.copy(assembly.workOrigin.position); const pathKey = `${config.showPath}:${config.showLive}:${state.activeSegmentId}`; if (paths !== lastGeometry || pathKey !== lastPathKey) { clearLines(pathGroup); clearLines(liveGroup); @@ -202,5 +204,6 @@ export function XyzacMachineView({ visual, toolpath, view = "p", zoom = 1, rotat export function XyzacMachineViewFromSnapshot({ snapshot, ...props }: Omit & { snapshot: XyzacVisualSource | null | undefined }) { const visual = projectXyzacVisual(snapshot); - return ; + const geometryKey = [visual.offsets.x_rot_point, visual.offsets.y_rot_point, visual.offsets.z_rot_point, visual.offsets.x_offset, visual.offsets.y_offset, visual.offsets.z_offset].join(":"); + return ; } diff --git a/web/react/src/legacy.d.ts b/web/react/src/legacy.d.ts index bd9825f..bf137c9 100644 --- a/web/react/src/legacy.d.ts +++ b/web/react/src/legacy.d.ts @@ -1,4 +1,7 @@ declare module "*.mjs" { + export const TRT_GEOMETRY_KEYS: readonly string[]; + export function readTrtGeometry(configuration: { entrypoint: string; files: Record }): { coordinates: string; offsets: Record; sources: Record; unsupported: string; state_hash: string }; + export function writeTrtGeometry }>(configuration: T, values: Record): T; export type MachineDefinition = { id: string; label: string; coordinates: string; axes: readonly string[]; joint_count: number; entrypoint: string; program: string; switchkins_program: string; kinematics_type: number; runtime: string; model: string; source_path: string; offsets: Record; joint_limits: Record; axis_limits: Record }; export const MACHINE_DEFINITIONS: readonly MachineDefinition[]; export const DEFAULT_MACHINE: MachineDefinition; diff --git a/web/react/src/styles.css b/web/react/src/styles.css index 1b87fb2..5cba5a0 100644 --- a/web/react/src/styles.css +++ b/web/react/src/styles.css @@ -173,6 +173,23 @@ output { font: 12px ui-monospace, monospace; text-align: right; } .axis-preview { display: flex; min-height: 0; flex: 1; padding: 4px; overflow: hidden; background: #000; } .axis-preview-split { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 4px; width: 100%; min-width: 0; min-height: 0; } .axis-preview-pane { display: flex; min-width: 0; min-height: 0; overflow: hidden; } +.axis-preview-machine { flex-direction: column; } +.axis-preview-machine > .three-viewer { height: auto; min-height: 220px; } +.trt-geometry { flex: 0 0 auto; background: #edf0f0; color: #202829; padding: 6px; font-size: 11px; border-top: 1px solid #899596; } +.trt-geometry header, .trt-geometry footer { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; } +.trt-geometry header strong { overflow-wrap: anywhere; } +.trt-geometry header output { margin-left: auto; color: #276557; } +.trt-geometry[data-dirty="true"] header output { color: #955015; } +.trt-geometry-fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin: 5px 0; } +.trt-geometry fieldset { border: 0; padding: 0; min-width: 0; } +.trt-geometry legend { color: #586467; padding: 0 0 3px; } +.trt-geometry label { display: grid; grid-template-columns: minmax(0, 1fr) 66px; align-items: center; gap: 3px; margin: 3px 0; } +.trt-geometry label span { overflow-wrap: anywhere; } +.trt-geometry input { width: 100%; min-width: 0; height: 24px; padding: 2px 3px; border: 1px solid #a1adae; background: #fff; color: #202829; } +.trt-geometry input:disabled { color: #879293; background: #e4e8e8; } +.trt-geometry footer button { display: inline-flex; align-items: center; justify-content: center; gap: 4px; height: 27px; min-width: 28px; padding: 3px 5px; } +.trt-geometry button img { width: 18px; height: 18px; object-fit: contain; } +.trt-geometry footer output { flex: 1 1 100px; min-width: 0; overflow-wrap: anywhere; } .axis-preview-pane > .three-viewer { min-width: 0; min-height: 0; } .three-viewer { position: relative; width: 100%; height: 100%; min-height: 0; flex: 1; overflow: hidden; border: 1px solid #303030; background: #000; } .three-viewer canvas { display: block; width: 100%; height: 100%; } @@ -258,7 +275,7 @@ output { font: 12px ui-monospace, monospace; text-align: right; } .axis-top-pane { grid-template-columns: 1fr; height: auto; min-height: 0; flex: 0 0 auto; } .axis-notebook { min-height: 330px; } .axis-preview { min-height: 732px; } - .axis-preview-split { grid-template-columns: 1fr; grid-template-rows: repeat(2, 360px); align-self: start; } + .axis-preview-split { grid-template-columns: 1fr; grid-template-rows: 360px 550px; align-self: start; } .axis-program-pane { flex: 0 0 auto; height: 740px; min-height: 740px; grid-template-columns: 1fr; grid-template-rows: minmax(0, 1fr) 140px; } .program-main-layout { grid-template-columns: 1fr; grid-template-rows: minmax(0, 1fr) minmax(0, 1fr); grid-column: 1; grid-row: 1; overflow: hidden; } .program-editor-wrap { grid-column: 1; grid-row: 1; } diff --git a/web/react/src/test_trt_parametric.mjs b/web/react/src/test_trt_parametric.mjs new file mode 100644 index 0000000..de5ffd2 --- /dev/null +++ b/web/react/src/test_trt_parametric.mjs @@ -0,0 +1,128 @@ +import assert from "node:assert/strict"; +import { readFile, mkdir, writeFile } from "node:fs/promises"; +import * as THREE from "three"; +import createModule from "../../../build-wasm/linuxcnc-sim-core.js"; +import { readTrtGeometry, writeTrtGeometry, TRT_GEOMETRY_KEYS } from "../../workspace/trt-geometry.mjs"; +import { loadSimulationCase, toWorkerConfiguration } from "../../workspace/sim-case-loader.mjs"; +import { MACHINE_DEFINITIONS } from "../../workspace/machine-definitions.mjs"; +import { SimSession, CONTEXT_ID } from "../../worker/sim-session.mjs"; +import { fnv1a64 } from "../../worker/replay.mjs"; +import { buildXyzacMachine, buildXyzbcMachine } from "./xyzac-machine-model.ts"; + +const root = new URL("../../../", import.meta.url); +const fetchImpl = async (url) => { + try { const body = await readFile(new URL(String(url).replace(/^\//, ""), root), "utf8"); return { ok: true, text: async () => body }; } + catch { return { ok: false }; } +}; +const wasmBinary = await readFile(new URL("build-wasm/linuxcnc-sim-core.wasm", root)); +const close = (a, b) => assert.ok(Math.abs(a - b) < 1e-7, `${a} != ${b}`); +const rows = []; +async function execute(bundle, source) { + const core = await createModule({ wasmBinary }); + const session = new SimSession(core, { sessionId: "trt-parametric" }); let seq = 0; + const send = (kind, payload = {}) => session.accept({ schema_version: 1, context_id: CONTEXT_ID, session_id: session.sessionId, kind, seq: ++seq, operation_id: `param-${seq}`, payload }); + send("HELLO"); send("SESSION_ATTACH"); + const configuration = toWorkerConfiguration(bundle); + const expected = readTrtGeometry(bundle).offsets; + configuration.kinematics.offsets.x_rot_point = 999; // Files must win at the Worker boundary. + const loaded = send("LOAD_WORKSPACE", { root_path: "/workspace", configuration }); + assert.equal(loaded.kind, "COMMAND_RESULT", JSON.stringify(loaded.payload)); + assert.deepEqual(loaded.payload.panel_snapshot.kinematics.offsets, expected); + assert.equal(loaded.payload.panel_snapshot.visual.toolpath.length, 0); + assert.equal(session.wasmHomeLimit.read().state.homed, false); + const malformed = structuredClone(configuration); + malformed.files[bundle.entrypoint] = malformed.files[bundle.entrypoint].replace(/(x-rot-point\s+)[^\r\n;#]+/, "$1NaN"); + assert.equal(send("LOAD_WORKSPACE", { root_path: "/workspace", configuration: malformed }).kind, "ERROR"); + assert.deepEqual(session.xyzacKinematics.snapshot().offsets, expected); + const preview = send("PROGRAM_LOAD", { source, path: "parametric.ngc" }); + assert.equal(preview.kind, "COMMAND_RESULT", JSON.stringify(preview.payload)); + assert.ok(preview.payload.preview.segments.length > 0); + assert.equal(send("POWER", { active: true }).kind, "COMMAND_RESULT"); + assert.equal(send("LOAD_WORKSPACE", { root_path: "/workspace", configuration }).kind, "ERROR"); + assert.equal(send("HOME", { all: true }).kind, "COMMAND_RESULT"); + for (let i = 0; i < 500 && !session.wasmHomeLimit.read().state.homed; i++) send("SIM_STEP", { cycles: 1 }); + assert.equal(session.wasmHomeLimit.read().state.homed, true); + assert.equal(send("PROGRAM_RUN").kind, "COMMAND_RESULT"); + assert.equal(send("PROGRAM_PAUSE").kind, "COMMAND_RESULT"); + assert.equal(send("LOAD_WORKSPACE", { root_path: "/workspace", configuration }).kind, "ERROR"); + assert.equal(send("PROGRAM_RESUME").kind, "COMMAND_RESULT"); + const trace = []; + let result; + for (let i = 0; i < 4000; i++) { + result = send("SIM_STEP", { cycles: 10 }); + assert.equal(result.kind, "COMMAND_RESULT", JSON.stringify(result.payload)); + trace.push({ tick: result.simulation_tick, pose: result.payload.wasm_motion.state.position, joints: result.payload.wasm_motion.state.joint_commanded }); + if (result.payload.program.program_status === "completed" && result.payload.wasm_motion.state.queue_depth === 0) break; + } + assert.equal(result.payload.program.program_status, "completed"); + const coordinates = readTrtGeometry(bundle).coordinates; + const model = coordinates === "XYZAC" ? buildXyzacMachine() : buildXyzbcMachine(); + const samples = [[0, 0], [20, 0], [0, 30], [-25, 45]]; + for (const [tilt, c] of samples) { + const pose = { x: 12, y: 8, z: 6, a: 0, b: 0, c, u: 0, v: 0, w: 0, [coordinates[3].toLowerCase()]: tilt }; + const inverse = session.xyzacKinematics.inverseMode(1, pose); + assert.equal(inverse.status, "verified"); + model.update({ joints: inverse.joints, offsets: expected }); + model.machine.updateMatrixWorld(true); + const point = model.workOrigin.localToWorld(new THREE.Vector3(pose.x * .01, pose.z * .01, pose.y * .01)); + const tip = model.toolTip.getWorldPosition(new THREE.Vector3()); + close(point.x, tip.x); close(point.y, tip.y); close(point.z, tip.z); + } + model.dispose(); + send("POWER", { active: false }); + const savedCheckpoint = session.createCheckpoint(); + const savedCompact = session.createPowerOffCheckpoint(); + const savedTrace = session.exportTrace(); + const reloaded = send("LOAD_WORKSPACE", { root_path: "/workspace", configuration }); + assert.equal(reloaded.kind, "COMMAND_RESULT"); + assert.equal(session.wasmHomeLimit.read().state.homed, false); + assert.deepEqual(reloaded.payload.panel_snapshot.visual.toolpath, []); + const other = toWorkerConfiguration(writeTrtGeometry(bundle, { x_rot_point: expected.x_rot_point + 1 })); + assert.equal(send("LOAD_WORKSPACE", { root_path: "/workspace", configuration: other }).kind, "COMMAND_RESULT"); + const beforeRecovery = session.xyzacKinematics.snapshot(); + assert.equal(session.restoreCheckpoint(savedCheckpoint).status, "blocked"); + assert.equal(session.restoreCompactPowerOff(savedCompact, seq + 1).status, "blocked"); + assert.throws(() => session.importTrace(savedTrace), /trt_recovery_geometry_mismatch/); + assert.deepEqual(session.xyzacKinematics.snapshot(), beforeRecovery); + assert.equal(session.wasmHomeLimit.read().state.homed, false); + return { trace, offsets: expected, final: trace.at(-1), preview_hash: fnv1a64(preview.payload.preview), trace_hash: fnv1a64(trace), model_samples: samples.length }; +} + +for (const machine of MACHINE_DEFINITIONS) { + const bundle = await loadSimulationCase({ path: machine.entrypoint, fetchImpl }); + assert.equal(bundle.status, "ready"); + const initial = readTrtGeometry(bundle); + const values = { ...initial.offsets, x_rot_point: 9, y_rot_point: -7, z_rot_point: 13, z_offset: -4, [machine.coordinates === "XYZAC" ? "y_offset" : "x_offset"]: 15 }; + const changed = writeTrtGeometry(bundle, values); + assert.notEqual(changed.state_hash, bundle.state_hash); + assert.deepEqual(readTrtGeometry(changed).offsets, values); + for (const bad of ["", "NaN", "Infinity", "1e309", "1+2", "0x10"]) assert.throws(() => writeTrtGeometry(bundle, { x_rot_point: bad }), /trt_geometry_number/); + assert.throws(() => writeTrtGeometry(bundle, { [initial.unsupported]: 1 }), /trt_geometry_unsupported/); + const edit = (fn) => ({ ...bundle, files: { ...bundle.files, [bundle.entrypoint]: fn(bundle.files[bundle.entrypoint]) } }); + assert.throws(() => readTrtGeometry(edit((s) => s.replace(/(HALCMD = setp [^\n]*x-rot-point[^\n]*)/, "$1\n$1"))), /trt_geometry_duplicate/); + assert.throws(() => readTrtGeometry(edit((s) => s.replace(/HALCMD = setp [^\n]*x-rot-point[^\n]*\n/, ""))), /trt_geometry_missing/); + const crlf = edit((s) => s.replaceAll("\n", "\r\n")); + assert.ok(writeTrtGeometry(crlf, values).files[bundle.entrypoint].includes("\r\n")); + const referenced = edit((s) => s.replace(/(x-rot-point\s+)[^\r\n]+/, "$1[GEOMETRY]X\n").concat("\n[GEOMETRY]\nX = 4 # retained comment\n")); + const referencedEdit = writeTrtGeometry(referenced, { x_rot_point: 7 }); + assert.equal(readTrtGeometry(referencedEdit).offsets.x_rot_point, 7); + assert.ok(referencedEdit.files[bundle.entrypoint].includes("X = 7 # retained comment")); + assert.ok(bundle.source_ini.includes("LIB:basic_sim.tcl")); + for (const key of TRT_GEOMETRY_KEYS.filter((key) => key !== initial.unsupported)) { + assert.equal(readTrtGeometry(writeTrtGeometry(bundle, { [key]: 3.25 })).offsets[key], 3.25); + } + const source = `G21 G90 G94\nM428\nG1 X12 Y8 Z6 ${machine.coordinates[3]}20 C30 F600\nM2\n`; + const baseline = await execute(bundle, source); + const modified = await execute(changed, source); + const repeated = await execute(changed, source); + assert.deepEqual(repeated.trace, modified.trace, "fixed tick replay must match"); + assert.notDeepEqual(baseline.final.joints, modified.final.joints, "INI must change program machine joints"); + assert.deepEqual(baseline.final.pose, modified.final.pose, "programming coordinates remain unchanged"); + const divergent = baseline.trace.findIndex((sample, i) => JSON.stringify(sample.joints) !== JSON.stringify(modified.trace[i]?.joints)); + assert.ok(divergent >= 0); + rows.push({ machine: machine.id, source, baseline, modified, repeat_hash: repeated.trace_hash, first_divergent_tick: baseline.trace[divergent].tick }); +} +const report = { report_id: "trt-parametric-worker-v1", status: "verified", rows }; +const dir = new URL("coverage/trt-parametric/", root); await mkdir(dir, { recursive: true }); +await writeFile(new URL("worker.report.json", dir), JSON.stringify({ ...report, state_hash: fnv1a64(report) }, null, 2) + "\n"); +console.log(JSON.stringify({ report_id: report.report_id, status: report.status, machines: rows.map((r) => ({ machine: r.machine, first_divergent_tick: r.first_divergent_tick, trace_hash: r.modified.trace_hash })) })); diff --git a/web/react/src/test_xyzac_threejs.mjs b/web/react/src/test_xyzac_threejs.mjs index 6199dd5..9d5681d 100644 --- a/web/react/src/test_xyzac_threejs.mjs +++ b/web/react/src/test_xyzac_threejs.mjs @@ -54,7 +54,8 @@ assert.ok(xyzbcModel.machine.getObjectByName("tilt-b")); assert.equal(xyzbcModel.machine.getObjectByName("tilt-a"), undefined); xyzbcModel.update({ joints: [0, 0, 0, 30, 45], offsets: { x_rot_point: 0, y_rot_point: 0, z_rot_point: 0, x_offset: -20, y_offset: 0, z_offset: -15, tool_offset: 0 } }); close(xyzbcModel.joints.tilt.rotation.z, -30 * Math.PI / 180); -assert.equal(xyzbcModel.joints.tilt.rotation.y, Math.PI / 2); +assert.equal(xyzbcModel.joints.tilt.rotation.y, 0); +assert.equal(xyzbcModel.machine.getObjectByName("tilting-casting").rotation.y, Math.PI / 2); xyzbcModel.dispose(); const original = structuredClone(visual); const zero = { joints: [0, 0, 0, 0, 0], offsets: { x_rot_point: 0, y_offset: 20, z_offset: 10, tool_offset: 0 } }; diff --git a/web/react/src/xyzac-machine-model.ts b/web/react/src/xyzac-machine-model.ts index 9447c1d..c44562c 100644 --- a/web/react/src/xyzac-machine-model.ts +++ b/web/react/src/xyzac-machine-model.ts @@ -153,15 +153,14 @@ function buildTrtMachine(coordinates = "XYZAC") { cylinder(brackets, `${rotaryAxis}-axis-end-cap`, -107, 0, 0, 16, 3, "dark", "x"); const tilt = group(tableX, `tilt-${rotaryAxis}`); - // The XYZBC source model applies a fixed 90 degree Z rotation before the - // B (source-Y) tilt. In the Three.js source-coordinate mapping that is a - // fixed Y rotation followed by a Z-axis dynamic tilt. - if (isXyzbc) tilt.rotation.y = Math.PI / 2; + // Rotate the B-axis casting without rotating the machine-coordinate frame. + const yoke = group(tilt, "tilting-casting"); + if (isXyzbc) yoke.rotation.y = Math.PI / 2; if (isXyzbc) brackets.rotation.y = Math.PI / 2; - box(tilt, "yoke-plate", 0, 0, -30, 130, 80, 10, "teal", 2); + box(yoke, "yoke-plate", 0, 0, -30, 130, 80, 10, "teal", 2); for (const x of [-60, 60]) { - box(tilt, "yoke-side-plate", x, 0, -15, 10, 80, 34, "teal", 2); - cylinder(tilt, `${rotaryAxis}-trunnion-shaft`, x, 0, 0, 10, 22, "steel", "x"); + box(yoke, "yoke-side-plate", x, 0, -15, 10, 80, 34, "teal", 2); + cylinder(yoke, `${rotaryAxis}-trunnion-shaft`, x, 0, 0, 10, 22, "steel", "x"); } const rotaryBase = group(tilt, "c-rotary-base"); box(rotaryBase, "c-drive-housing", 0, 0, -24, 104, 100, 12, "dark", 3); @@ -194,6 +193,7 @@ function buildTrtMachine(coordinates = "XYZAC") { const xOffset = Number(state.offsets.x_offset ?? 0); const yOffset = Number(state.offsets.y_offset ?? 0); const zOffset = Number(state.offsets.z_offset ?? 0); + const pivot = new THREE.Vector3(state.offsets.x_rot_point * MM, Number(state.offsets.z_rot_point ?? 0) * MM, Number(state.offsets.y_rot_point ?? 0) * MM); tableX.position.x = -tableXJoint * MM; saddle.position.z = -saddleYJoint * MM; if (coordinates === "XYZBC") { @@ -201,24 +201,23 @@ function buildTrtMachine(coordinates = "XYZAC") { // XYZBC is a table tilt: the source model rotates the B assembly // around machine Y. The Web scene maps source Y to Three.js Z. head.rotation.set(0, 0, 0); - zslide.position.y = 0; + zslide.position.y = 150 * MM; tilt.rotation.z = -tiltAJoint * DEG; - // xyzbc-trt-gui applies x-offset to both sides of the B table and - // applies z-offset with opposite signs to the inner/outer groups. - tilt.position.set(xOffset * MM, zOffset * MM, yOffset * MM); + tilt.position.set(xOffset * MM, zOffset * MM, 0).add(pivot); brackets.position.copy(tilt.position); - rotaryBase.position.set(xOffset * MM, -zOffset * MM, 0); + rotaryBase.position.set(-xOffset * MM, -zOffset * MM, 0); } else { zslide.position.y = (200 - spindleZJoint) * MM; head.position.y = 2; head.rotation.set(0, 0, 0); // XYZAC source transform: tilt.rotation.x = -tiltAJoint * DEG. tilt.rotation.x = -tiltAJoint * DEG; - tilt.position.set(0, zOffset * MM, yOffset * MM); + tilt.position.set(0, zOffset * MM, yOffset * MM).add(pivot); brackets.position.copy(tilt.position); - rotaryBase.position.copy(tilt.position).negate(); + rotaryBase.position.set(0, -zOffset * MM, -yOffset * MM); } rotate.rotation.y = -rotateCJoint * DEG; + workOrigin.position.copy(pivot).negate(); tool.position.y = -state.offsets.tool_offset * MM; if (toolLength !== state.offsets.tool_offset) { toolLength = state.offsets.tool_offset; diff --git a/web/worker/sim-session.mjs b/web/worker/sim-session.mjs index edd398e..6d33092 100644 --- a/web/worker/sim-session.mjs +++ b/web/worker/sim-session.mjs @@ -32,6 +32,7 @@ import { WasmXyzacDeviceIo } from "./wasm-xyzac-device-io.mjs"; import { WasmProgramRuntime } from "./wasm-program-runtime.mjs"; import { axisGate, xyzacGate, xyzacGateMatrix } from "../app/axis-contract.mjs"; import { machineDefinition } from "../workspace/machine-definitions.mjs"; +import { readTrtGeometry, TRT_GEOMETRY_KEYS } from "../workspace/trt-geometry.mjs"; const PROTOCOL_SCHEMA = 1; const CONTEXT_ID = "linuxcnc-2.10.0-pre0-wasm-m1-py-c1"; @@ -196,7 +197,7 @@ export class SimSession { // thousands of immutable segments on unrelated control responses makes a // loaded program delay abort/power/home commands behind structured-clone // work. Explicit preview transactions still publish the full replacement. - const publishXyzacToolpath = ["PROGRAM_LOAD", "PROGRAM_RESTART", "RECOVER", "SNAPSHOT_REQUEST", "VISUAL_CLEAR"].includes(input.kind); + const publishXyzacToolpath = ["LOAD_WORKSPACE", "PROGRAM_LOAD", "PROGRAM_RESTART", "RECOVER", "SNAPSHOT_REQUEST", "VISUAL_CLEAR"].includes(input.kind); const panelSnapshot = motionDelta || traceOnlyStep ? null : this.panelSnapshot({ task, program, @@ -600,6 +601,9 @@ export class SimSession { const program = this.xyzacProgram.snapshot(); const io = this.xyzacIo?.canonical?.() ?? {}; const wasmMotion = this.wasmMotion.read(); + if (input.kind === "LOAD_WORKSPACE" && (io.ready || ["running", "paused"].includes(program.program_status) || Number(wasmMotion.state?.queue_depth) > 0)) { + return { enabled: false, reason: "trt_configuration_requires_power_off_idle" }; + } if (["HOME", "SET_MODE"].includes(input.kind) && Number(wasmMotion.state?.queue_depth) > 0) return { enabled: false, reason: "xyzac_motion_active" }; if (input.kind === "JOG_START" && !this.wasmHomeLimit.read().state.homed) return { enabled: false, reason: "xyzac_not_all_homed" }; if (input.kind === "PROGRAM_LOAD" && wasmMotion.status === "verified" && Number(wasmMotion.state.queue_depth ?? 0) > 0) { @@ -830,6 +834,12 @@ export class SimSession { return { ...compact, checkpoint_hash: fnv1a64(compact) }; } + validateRecoveryGeometry(state) { + if (!this.xyzacKinematics) return; + const current = this.xyzacKinematics.snapshot(); + if (!state?.kinematics || state.kinematics.coordinates !== current.coordinates || TRT_GEOMETRY_KEYS.some((key) => Number(state.kinematics.offsets?.[key] ?? 0) !== current.offsets[key])) throw new Error("trt_recovery_geometry_mismatch"); + } + restoreCompactPowerOff(record, transportSeq) { if (!record || record.schema !== "xyzac-power-off-checkpoint.v1" || record.context_id !== this.contextId || !record.state) return { status: "blocked", code: "runtime_checkpoint_schema", reason: "power_off_checkpoint_schema_invalid" }; if (fnv1a64(record.state) !== record.state_hash || fnv1a64(Object.fromEntries(Object.entries(record).filter(([key]) => key !== "checkpoint_hash"))) !== record.checkpoint_hash) return { status: "blocked", code: "runtime_checkpoint_hash", reason: "power_off_checkpoint_hash_mismatch" }; @@ -839,6 +849,7 @@ export class SimSession { const liveLedger = this.ledger; const liveOperations = [...this.operations.values()]; try { + this.validateRecoveryGeometry(state); if (!this.xyzacProgram || !state.program?.source_text) throw new Error("power_off_program_missing"); const loaded = this.xyzacProgram.load(state.program.source_text, state.program.path ?? null); if (loaded.status !== "verified") throw new Error(loaded.code); @@ -941,6 +952,7 @@ export class SimSession { importTrace(trace) { const validated = validateTrace(trace, { contextId: this.contextId }); + this.validateRecoveryGeometry(trace.state); this.timeline = validated.timeline; this.created = Boolean(trace.state?.created); if (trace.state?.ledger?.entries) this.ledger = new OperationLedger(trace.state.ledger.entries); @@ -977,6 +989,7 @@ export class SimSession { restoreCheckpoint(checkpoint, events = null, reconcile = undefined) { try { validateCheckpoint(checkpoint, { contextId: this.contextId }); + this.validateRecoveryGeometry(checkpoint.state); } catch (error) { return { schema: "simulation-recovery.v1", status: "blocked", context_id: this.contextId, checkpoint_hash: checkpoint?.checkpoint_hash ?? null, blocked: [{ reason: String(error?.message ?? error) }] }; } @@ -1594,7 +1607,15 @@ export class SimSession { const rootPath = input.payload?.root_path; if (!text(rootPath) || !rootPath.startsWith("/") || rootPath.includes("..")) return finish(this.error(input, "workspace_path", "invalid_workspace_path"), "failed"); let startupLoaded = false; - const configuration = input.payload?.configuration ?? null; + let configuration = input.payload?.configuration ?? null; + if (machineDefinition(configuration?.entrypoint) || machineDefinition(configuration?.kinematics?.type)) { + try { + const geometry = readTrtGeometry(configuration); + configuration = { ...configuration, kinematics: { ...configuration.kinematics, offsets: geometry.offsets } }; + } catch (error) { + return finish(this.error(input, "trt_geometry", error.message), "failed"); + } + } if (configuration) { this.machineDefinition = machineDefinition(configuration.entrypoint) ?? machineDefinition(configuration.kinematics?.type) ?? this.machineDefinition; this.workspaceFiles = { ...(configuration.files ?? configuration.workspace ?? configuration.workspace_files ?? {}) }; @@ -1669,6 +1690,9 @@ export class SimSession { ? new WasmLinuxcncTp(this.core, { cycleTime: this.xyzacMotionCycleTime, velocityLimit: this.xyzacRapidVelocity, accelerationLimit: Number(configuration.kinematics?.max_acceleration ?? 300), limits: configuration.home_limits ?? {} }) : new WasmMotionQueue(this.core, { cycleTime: this.xyzacMotionCycleTime, velocityLimit: 1e9 }); this.xyzacProgram = new WasmProgramRuntime(this.core); + this.previewState = null; + this.pendingXyzacMotion = null; + this.clearLivePlot(); this.homingHistory = []; this.xyzacProgramKinematicsMode = XYZAC_MODES.identity; if (typeof this.wasmMotion.setKinematics === "function") this.wasmMotion.setKinematics(this.xyzacKinematics); diff --git a/web/worker/test_boat_trt_programs.mjs b/web/worker/test_boat_trt_programs.mjs index 816c4fe..38d37d2 100644 --- a/web/worker/test_boat_trt_programs.mjs +++ b/web/worker/test_boat_trt_programs.mjs @@ -37,5 +37,6 @@ for (const coordinates of ["XYZAC", "XYZBC"]) { assert.ok(Number.isFinite(result.payload.panel_snapshot.visual.pose[rotary])); assert.equal(result.payload.panel_snapshot.visual.machine_model.coordinate_system, coordinates); rows.push({ coordinates, program: programPath, status: result.payload.program.program_status, rotary }); + assert.equal(send("POWER", { active: false }).kind, "COMMAND_RESULT"); } console.log(JSON.stringify({ schema: "boat-trt-programs.v1", status: "verified", rows })); diff --git a/web/worker/test_xyzac_trt_case.mjs b/web/worker/test_xyzac_trt_case.mjs index c553fb3..833f69f 100644 --- a/web/worker/test_xyzac_trt_case.mjs +++ b/web/worker/test_xyzac_trt_case.mjs @@ -35,7 +35,9 @@ assert.equal(loaded.payload.motion_authority, "wasm"); assert.equal(loaded.payload.configuration.state, "TASK_READY"); assert.equal(loaded.payload.panel_snapshot.kinematics.joint_count, 5); assert.equal(send("POWER", "power-on", { active: true }).payload.status, "verified"); -assert.equal(send("HOME", "home-all", { all: true }).payload.wasm_home_limit.state.homed, true); +assert.equal(send("HOME", "home-all", { all: true }).kind, "COMMAND_RESULT"); +for (let tick = 0; tick < 500 && !session.wasmHomeLimit.read().state.homed; tick++) send("SIM_STEP", `home-step-${tick}`, { cycles: 1 }); +assert.equal(session.wasmHomeLimit.read().state.homed, true); const switchRows = []; for (const [code, expected] of [["M429", 0], ["M428", 1], ["M430", 2]]) { diff --git a/web/workspace/sim-case-loader.mjs b/web/workspace/sim-case-loader.mjs index 0ddf7b6..57c8200 100644 --- a/web/workspace/sim-case-loader.mjs +++ b/web/workspace/sim-case-loader.mjs @@ -3,6 +3,7 @@ import { runtimeResourceFixture } from "./resource-resolver.mjs"; import { SIM_CASE_CATALOG, SIM_FILE_INDEX, SIM_HAL_COMPONENTS } from "./sim-case-catalog.mjs"; import { isOptionalSourceKey } from "./linuxcnc-source-semantics.mjs"; import { MACHINE_DEFINITIONS, machineDefinition } from "./machine-definitions.mjs"; +import { readTrtGeometry } from "./trt-geometry.mjs"; export const SIMULATION_CASE_SCHEMA = "simulation-case-bundle.v1"; const RESOURCE_KEYS = new Set([ @@ -17,8 +18,8 @@ const CASE_KINEMATICS = Object.freeze({ "linuxcnc/configs/sim/axis/corexy/corexy_by_hal.ini": Object.freeze({ type: "corexy", path_mode: "hal" }), "linuxcnc/configs/sim/axis/ldelta.ini": Object.freeze({ type: "lineardelta", path_mode: "kins" }), "linuxcnc/configs/sim/axis/rdelta.ini": Object.freeze({ type: "rotarydelta", path_mode: "kins" }), - "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini": Object.freeze({ type: "xyzac-trt", path_mode: "wasm", coordinates: "XYZAC", joint_count: 5, max_linear_velocity: 35, axis_linear_velocity_limit: 20, max_acceleration: 300, max_jerk: 1000000000, servo_cycle_time: 0.001, task_cycle_time: 0.01, cycle_time: 0.001, switchkins: { 0: "identity", 1: "xyzac", 2: "userk" }, offsets: { x_rot_point: 0, y_offset: 20, z_offset: 10, tool_offset: 0 } }), - "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini": Object.freeze({ type: "xyzbc-trt", path_mode: "wasm", coordinates: "XYZBC", joint_count: 5, max_linear_velocity: 35, axis_linear_velocity_limit: 20, axis_angular_velocity_limit: 30, max_acceleration: 300, max_jerk: 1000000000, servo_cycle_time: 0.001, task_cycle_time: 0.01, cycle_time: 0.001, switchkins: { 0: "identity", 1: "xyzbc", 2: "userk" }, offsets: { x_rot_point: 0, y_rot_point: 0, z_rot_point: 0, x_offset: -20, y_offset: 0, z_offset: -15, tool_offset: 0 } }), + "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini": Object.freeze({ type: "xyzac-trt", path_mode: "wasm", coordinates: "XYZAC", joint_count: 5, max_linear_velocity: 35, axis_linear_velocity_limit: 20, max_acceleration: 300, max_jerk: 1000000000, servo_cycle_time: 0.001, task_cycle_time: 0.01, cycle_time: 0.001, switchkins: { 0: "identity", 1: "xyzac", 2: "userk" } }), + "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini": Object.freeze({ type: "xyzbc-trt", path_mode: "wasm", coordinates: "XYZBC", joint_count: 5, max_linear_velocity: 35, axis_linear_velocity_limit: 20, axis_angular_velocity_limit: 30, max_acceleration: 300, max_jerk: 1000000000, servo_cycle_time: 0.001, task_cycle_time: 0.01, cycle_time: 0.001, switchkins: { 0: "identity", 1: "xyzbc", 2: "userk" } }), }); const RUNTIME_FIXTURES = Object.freeze({ "linuxcnc/configs/sim/axis/minimal_xyz.ini": Object.freeze({ @@ -130,7 +131,7 @@ function inManifest(path) { function responseUrl(baseUrl, path) { return `${String(baseUrl ?? "").replace(/\/$/, "")}/${path}`.replace(/^\//, "/"); } async function fetchText(fetchImpl, baseUrl, path) { - const response = await fetchImpl(responseUrl(baseUrl, path)); + const response = await fetchImpl(responseUrl(baseUrl, path), { cache: "no-store" }); if (!response?.ok) return null; return response.text(); } @@ -241,6 +242,7 @@ export async function loadSimulationCase({ path, fetchImpl = globalThis.fetch, b else { files[visualSource] = content; fetched.push(visualSource); } } } + const sourceIni = files[catalog.path]; const fixture = RUNTIME_FIXTURES[catalog.path]; if (fixture) { if (!inManifest(fixture.generated_hal)) missing.push(fixture.generated_hal); @@ -267,6 +269,7 @@ export async function loadSimulationCase({ path, fetchImpl = globalThis.fetch, b }; return { ...clone(unsigned), + source_ini: sourceIni, catalog, halManifest: [...new Set([...SIM_HAL_COMPONENTS, ...PRELOADED_HAL_COMPONENTS])].sort().map((component) => ({ component })), status: missing.length ? "blocked" : "ready", @@ -276,6 +279,8 @@ export async function loadSimulationCase({ path, fetchImpl = globalThis.fetch, b export function toWorkerConfiguration(bundle) { if (!bundle || bundle.schema !== SIMULATION_CASE_SCHEMA) throw new Error("simulation_case_bundle_invalid"); + const kinematics = CASE_KINEMATICS[bundle.entrypoint] ? clone(CASE_KINEMATICS[bundle.entrypoint]) : null; + if (machineDefinition(bundle.entrypoint)) kinematics.offsets = readTrtGeometry(bundle).offsets; return { entrypoint: bundle.entrypoint, files: clone(bundle.files), @@ -285,7 +290,7 @@ export function toWorkerConfiguration(bundle) { runtime_fixtures: clone(bundle.runtime_fixtures ?? []), ...(bundle.entrypoint === "linuxcnc/configs/sim/axis/vismach/3axis-tutorial/3axis-tutorial.ini" ? { home_limits: { z: [-240, 100] } } : {}), ...(machineDefinition(bundle.entrypoint) ? { home_limits: { joint_count: 5, kinematics_type: machineDefinition(bundle.entrypoint).kinematics_type, coordinates: machineDefinition(bundle.entrypoint).coordinates, ...machineDefinition(bundle.entrypoint).joint_limits } } : {}), - ...(CASE_KINEMATICS[bundle.entrypoint] ? { kinematics: clone(CASE_KINEMATICS[bundle.entrypoint]) } : {}), + ...(kinematics ? { kinematics } : {}), }; } diff --git a/web/workspace/test_sim_case_loader.mjs b/web/workspace/test_sim_case_loader.mjs index 8085f88..247bc7d 100644 --- a/web/workspace/test_sim_case_loader.mjs +++ b/web/workspace/test_sim_case_loader.mjs @@ -35,7 +35,11 @@ assert.equal(runtimeGenerated.status, "ready"); assert.equal(runtimeGenerated.missing.length, 0); assert.equal(runtimeGenerated.files["linuxcnc/configs/sim/axis/sim-lathe.var"], ""); assert.ok(runtimeGenerated.runtime_fixtures.some((row) => row.kind === "parameter_file")); -const optionalGraphic = await loadSimulationCase({ path: "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini", fetchImpl }); +const optionalGraphic = await loadSimulationCase({ path: "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini", fetchImpl: async (url) => { + const response = await fetchImpl(url); + if (!String(url).endsWith("xyzac-trt.ini")) return response; + return { ok: response.ok, text: async () => (await response.text()).replace(/INTRO_GRAPHIC\s*=\s*[^\r\n]+/, "INTRO_GRAPHIC = emc2.gif") }; +} }); assert.equal(optionalGraphic.status, "ready"); assert.ok(optionalGraphic.optional_missing.some((row) => row.path.endsWith("/emc2.gif"))); const emptyOpenFile = await loadSimulationCase({ path: "linuxcnc/configs/sim/axis/anglejog/anglejog.ini", fetchImpl }); diff --git a/web/workspace/trt-geometry.mjs b/web/workspace/trt-geometry.mjs new file mode 100644 index 0000000..3053fb5 --- /dev/null +++ b/web/workspace/trt-geometry.mjs @@ -0,0 +1,76 @@ +import { parseIni } from "./ini-parser.mjs"; +import { parseHalLine } from "./hal-configuration.mjs"; +import { WorkspaceRuntime } from "./runtime.mjs"; +import { machineDefinition } from "./machine-definitions.mjs"; +import { fnv1a64 } from "../worker/replay.mjs"; + +export const TRT_GEOMETRY_KEYS = Object.freeze(["x_rot_point", "y_rot_point", "z_rot_point", "x_offset", "y_offset", "z_offset"]); +const numeric = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i; +function fail(code, detail) { throw Object.assign(new Error(`${code}: ${detail}`), { code }); } + +export function readTrtGeometry({ entrypoint, files }) { + const machine = machineDefinition(entrypoint); + if (!machine) fail("trt_geometry_machine", entrypoint); + const parsed = parseIni({ workspace: new WorkspaceRuntime({ files }), entrypoint }); + if (parsed.sections.TRAJ?.COORDINATES?.toUpperCase() !== machine.coordinates || parsed.sections.KINS?.KINEMATICS?.split(/\s+/)[0] !== `${machine.id}-kins`) fail("trt_geometry_machine_mismatch", entrypoint); + const commands = parsed.entries.filter((e) => e.section.toUpperCase() === "HAL" && e.key.toUpperCase() === "HALCMD") + .map((entry) => ({ entry, record: parseHalLine(entry.value, entry.line, parsed.sections) })); + const unsupported = machine.coordinates === "XYZAC" ? "x_offset" : "y_offset"; + const offsets = { tool_offset: 0 }; + const sources = {}; + for (const key of TRT_GEOMETRY_KEYS) { + const pin = `${machine.id}-kins.${key.replaceAll("_", "-")}`; + const nets = commands.filter(({ record: r }) => r?.command === "net" && r.tokens.includes(pin)); + const direct = commands.filter(({ record: r }) => r?.command === "setp" && r.tokens[0] === pin); + if (nets.length > 1 || nets.length && direct.length) fail("trt_geometry_ambiguous", pin); + const assignments = nets.length ? commands.filter(({ record: r }) => r?.command === "sets" && r.tokens[0] === nets[0].record.tokens[0]) : direct; + if (assignments.length > 1) fail("trt_geometry_duplicate", pin); + if (!assignments.length) { + if (key !== unsupported) fail("trt_geometry_missing", pin); + offsets[key] = 0; + continue; + } + const { entry, record } = assignments[0]; + const value = record.tokens[1]; + if (record.tokens.length !== 2 || !numeric.test(value) || !Number.isFinite(Number(value))) fail("trt_geometry_number", `${pin} @ ${entry.source}:${entry.line}`); + if (key === unsupported && Number(value) !== 0) fail("trt_geometry_unsupported", pin); + offsets[key] = Number(value); + // Follow INI references so editing updates the original scalar definition. + const rawValue = entry.value.trim().split(/\s+/).at(-1); + const reference = /^\[([^\]]+)\](.+)$/.exec(rawValue); + let source = entry; + if (reference) { + const definitions = parsed.entries.filter((e) => e.section === reference[1] && e.key === reference[2]); + if (definitions.length !== 1) fail("trt_geometry_reference", rawValue); + source = definitions[0]; + } + sources[key] = { path: source.source, line: source.line, value: reference ? source.value : rawValue }; + if (Object.entries(sources).some(([other, location]) => other !== key && location.path === source.source && location.line === source.line)) fail("trt_geometry_shared_definition", key); + } + return { coordinates: machine.coordinates, offsets, sources, unsupported, state_hash: fnv1a64({ entrypoint, offsets, sources }) }; +} + +export function writeTrtGeometry(configuration, values) { + const geometry = readTrtGeometry(configuration); + const files = { ...configuration.files }; + for (const key of TRT_GEOMETRY_KEYS) { + const raw = String(values[key] ?? geometry.offsets[key]).trim(); + if (!numeric.test(raw) || !Number.isFinite(Number(raw))) fail("trt_geometry_number", key); + if (key === geometry.unsupported && Number(raw) !== 0) fail("trt_geometry_unsupported", key); + const source = geometry.sources[key]; + if (!source) continue; + const lines = files[source.path].split("\n"); + const line = lines[source.line - 1]; + const comment = line.search(/[;#]/); + const end = comment < 0 ? line.length : comment; + const body = line.slice(0, end); + const index = body.lastIndexOf(source.value); + if (index < 0) fail("trt_geometry_source_changed", key); + lines[source.line - 1] = body.slice(0, index) + String(Number(raw)) + body.slice(index + source.value.length) + line.slice(end); + files[source.path] = lines.join("\n"); + } + const result = { ...configuration, files }; + if ("state_hash" in result) result.state_hash = fnv1a64({ entrypoint: configuration.entrypoint, files }); + readTrtGeometry(result); + return result; +}