Verify isolated joint edits and parent frame inheritance

This commit is contained in:
2026-09-21 09:19:45 +08:00
parent a472911ad1
commit 91ebd56d6d
7 changed files with 384 additions and 14 deletions

View File

@@ -9,12 +9,310 @@
}, },
"checks": [ "checks": [
"root and internal node transforms update their local position and posture relative to their parent", "root and internal node transforms update their local position and posture relative to their parent",
"MILL700 JT3 edits preserve parent and sibling transforms; child inherits movement and rotation around the joint origin; values survive reselection and dialog reopening",
"reparenting an edited node preserves world pose and subsequent inputs edit that node under its new parent",
"selected MILL700 joint displays its local XYZ coordinate system at local origin", "selected MILL700 joint displays its local XYZ coordinate system at local origin",
"existing 132-node, depth-6 hierarchy displayed as a draggable tree with node origins", "existing 132-node, depth-6 hierarchy displayed as a draggable tree with node origins",
"each joint rotates about its own origin while children inherit parent motion", "each joint rotates about its own origin while children inherit parent motion",
"metallic MILL700 surfaces rendered with studio PMREM lighting; screenshot captured for visual review", "metallic MILL700 surfaces rendered with studio PMREM lighting; screenshot captured for visual review",
"imported model can be deleted and its tree is removed" "imported model can be deleted and its tree is removed"
], ],
"nodeEditing": {
"before": {
"parent": {
"local": [
-0.003969311714172363,
-0.20427599549293518,
0.40563321113586426
],
"localQuaternion": [
0,
0,
0,
1
],
"world": [
-1.7138478483851618,
-0.515726496296395,
1.8940227031707764
],
"worldQuaternion": [
0,
0,
0.17364817766693033,
0.9848077530122079
]
},
"sibling": {
"local": [
0.0031614303588867188,
0.12106472253799438,
-0.2689887285232544
],
"localQuaternion": [
0,
0,
0,
1
],
"world": [
-1.7522836493599145,
-0.40088159702548765,
1.625033974647522
],
"worldQuaternion": [
0,
0,
0.17364817766693033,
0.9848077530122079
]
},
"child": {
"local": [
0.0013986825942993164,
0.0009530782699584961,
-0.8500750064849854
],
"localQuaternion": [
0,
0,
0,
1
],
"world": [
-1.4232056214947701,
-1.3709958977290349,
1.4128730297088623
],
"worldQuaternion": [
0,
0,
0.17364817766693033,
0.9848077530122079
]
},
"selected": {
"local": [
-0.02080368995666504,
-0.9040489196777344,
0.3689253330230713
],
"localQuaternion": [
0,
0,
0,
1
],
"world": [
-1.4241939812409632,
-1.3723698759677156,
2.2629480361938477
],
"worldQuaternion": [
0,
0,
0.17364817766693033,
0.9848077530122079
]
}
},
"translated": {
"parent": {
"local": [
-0.003969311714172363,
-0.20427599549293518,
0.40563321113586426
],
"localQuaternion": [
0,
0,
0,
1
],
"world": [
-1.7138478483851618,
-0.515726496296395,
1.8940227031707764
],
"worldQuaternion": [
0,
0,
0.17364817766693033,
0.9848077530122079
]
},
"sibling": {
"local": [
0.0031614303588867188,
0.12106472253799438,
-0.2689887285232544
],
"localQuaternion": [
0,
0,
0,
1
],
"world": [
-1.7522836493599145,
-0.40088159702548765,
1.625033974647522
],
"worldQuaternion": [
0,
0,
0.17364817766693033,
0.9848077530122079
]
},
"child": {
"local": [
0.0013986825942993164,
0.0009530782699584961,
-0.8500750064849854
],
"localQuaternion": [
0,
0,
0,
1
],
"world": [
-1.2908934330630653,
-1.3228381995092735,
1.3688730297088623
],
"worldQuaternion": [
0,
0,
0.17364817766693033,
0.9848077530122079
]
},
"selected": {
"local": [
0.12,
-0.9040489196777344,
0.3249253330230713
],
"localQuaternion": [
0,
0,
0,
1
],
"world": [
-1.2918817928092583,
-1.3242121777479543,
2.2189480361938476
],
"worldQuaternion": [
0,
0,
0.17364817766693033,
0.9848077530122079
]
}
},
"rotated": {
"parent": {
"local": [
-0.003969311714172363,
-0.20427599549293518,
0.40563321113586426
],
"localQuaternion": [
0,
0,
0,
1
],
"world": [
-1.7138478483851618,
-0.515726496296395,
1.8940227031707764
],
"worldQuaternion": [
0,
0,
0.17364817766693033,
0.9848077530122079
]
},
"sibling": {
"local": [
0.0031614303588867188,
0.12106472253799438,
-0.2689887285232544
],
"localQuaternion": [
0,
0,
0,
1
],
"world": [
-1.7522836493599145,
-0.40088159702548765,
1.625033974647522
],
"worldQuaternion": [
0,
0,
0.17364817766693033,
0.9848077530122079
]
},
"child": {
"local": [
0.0013986825942993164,
0.0009530782699584961,
-0.8500750064849854
],
"localQuaternion": [
0,
0,
0,
1
],
"world": [
-1.2915667029697908,
-1.3225492316931085,
1.3688730297088623
],
"worldQuaternion": [
0,
0,
0.38268343236508984,
0.9238795325112867
]
},
"selected": {
"local": [
0.12,
-0.9040489196777344,
0.3249253330230713
],
"localQuaternion": [
0,
0,
0.21643961393810288,
0.9762960071199334
],
"world": [
-1.2918817928092583,
-1.3242121777479543,
2.2189480361938476
],
"worldQuaternion": [
0,
0,
0.38268343236508984,
0.9238795325112867
]
}
}
},
"hierarchy": { "hierarchy": {
"root": "MILL700-U_1", "root": "MILL700-U_1",
"nodes": 132, "nodes": 132,
@@ -108,7 +406,7 @@
"local": [ "local": [
0.12, 0.12,
-0.9040489196777344, -0.9040489196777344,
0.3689253330230713 0.3249253330230713
], ],
"localQuaternion": [ "localQuaternion": [
0, 0,
@@ -119,7 +417,7 @@
"world": [ "world": [
-1.2918817928092583, -1.2918817928092583,
-1.3242121777479543, -1.3242121777479543,
2.2629480361938477 2.2189480361938476
], ],
"worldQuaternion": [ "worldQuaternion": [
0, 0,
@@ -254,7 +552,7 @@
"local": [ "local": [
0.12, 0.12,
-0.9040489196777344, -0.9040489196777344,
0.3989253330230713 0.3549253330230713
], ],
"localQuaternion": [ "localQuaternion": [
0, 0,
@@ -265,7 +563,7 @@
"world": [ "world": [
-1.2893252694679127, -1.2893252694679127,
-1.3019981238989793, -1.3019981238989793,
2.292948036193848 2.2489480361938474
], ],
"worldQuaternion": [ "worldQuaternion": [
0, 0,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 334 KiB

After

Width:  |  Height:  |  Size: 296 KiB

View File

@@ -48,3 +48,5 @@
- 底座 `userData` 含完整几何及物理零点字段。 - 底座 `userData` 含完整几何及物理零点字段。
- 运行控制面板后,X/Y/Z/A或B/C Jog 和五轴程序驱动对应部件;工件路径继续附着 C 轴,刀具长度来自 WASM。 - 运行控制面板后,X/Y/Z/A或B/C Jog 和五轴程序驱动对应部件;工件路径继续附着 C 轴,刀具长度来自 WASM。
- 非法层级、重复角色、缺失部件、错误机型和外部资源 GLB 明确拒绝,已生效模型不改变。 - 非法层级、重复角色、缺失部件、错误机型和外部资源 GLB 明确拒绝,已生效模型不改变。
15. 树中每个节点独立编辑相对父级的位置(mm)和 XYZ 欧拉姿态(deg)。数值输入回显节点当前局部值,旋转以自身原点为中心;子节点随动,父级和兄弟节点不变。源 GLB 保留,节点编辑保存在草稿并进入组合导出。

View File

@@ -71,11 +71,14 @@
- 当前任务:已完成。 - 当前任务:已完成。
- 下一步:按新需求继续;本专项不重复执行。 - 下一步:按新需求继续;本专项不重复执行。
## R09 / 2026-09-21 / 部件节点相对父级变换 ## R10 / 2026-09-21 / 部件节点相对父级变换
- 更正 R09(`63f726d`):上一轮错误地让内部节点输入移动整个 GLB 根。用户要求编辑当前选中节点,原 R09 的根节点验收不足以证明内部关节编辑正确。
- 用户明确要求“部件平移”和“部件旋转”修改当前选中节点相对其父节点的位置和姿态。 - 用户明确要求“部件平移”和“部件旋转”修改当前选中节点相对其父节点的位置和姿态。
- 根节点继续使用部件级 `translationMm`、`rotationDeg`;GLB 内部节点使用稳定源路径保存到 `nodeTransforms[path]`。输入值是当前节点的父级局部位置(mm)和 XYZ 欧拉姿态(deg),旋转中心是节点自身原点。 - 根节点继续使用部件级 `translationMm`、`rotationDeg`;GLB 内部节点使用稳定源路径保存到 `nodeTransforms[path]`。输入值是当前节点的父级局部位置(mm)和 XYZ 欧拉姿态(deg),旋转中心是节点自身原点。
- 预览克隆、组合导出和应用运行场景统一应用同一组局部节点变换;父节点运动时子节点继承,编辑子节点不会错误移动整个 GLB 根部件。 - 预览克隆、组合导出和应用运行场景统一应用同一组局部节点变换;父节点运动时子节点继承,编辑子节点不会错误移动整个 GLB 根部件。
- 拖放内部节点后保存当前局部姿态并重新计算新的父级局部姿态,保持拖放前的世界姿态。 - 拖放内部节点后保存当前局部姿态并重新计算新的父级局部姿态,保持拖放前的世界姿态。
- MILL700 选择 `MILL700-A-主轴-JT3`,输入 X=120 mm、RZ=25° 后,节点局部位置和四元数分别变为 `0.120 m` 和 `25°`;8 部件 AC/BC 生产回归、导出和应用继续通过。 - MILL700 选择 `MILL700-A-主轴-JT3`,输入 X=120 mm、RZ=25° 后,节点局部位置和四元数分别变为 `0.120 m` 和 `25°`;8 部件 AC/BC 生产回归、导出和应用继续通过。
- MGR-16 verified;验收证据 E11。 - MILL700 补充断言:JT3 局部 Z 减少 44 mm 时 JT2 与兄弟滑轨不动、JT4 同步移动;RZ=25° 时 JT3 原点不动、JT4 绕该原点运动;父链预先旋转 20°,位移仍按父坐标系计算。切换选择和重开对话框保留输入;带编辑姿态的门节点换父级后世界姿态不变且能继续编辑。
- MGR-16 verified;验收证据 E12。

View File

@@ -46,7 +46,7 @@
- report_id:`GLB-MGR-E05`;报告 SHA-256 `09fd47f80dd2f741aa1d585ce5b1b40c2ff32e211b7b6e08b1b0da18327d798e`。 - report_id:`GLB-MGR-E05`;报告 SHA-256 `09fd47f80dd2f741aa1d585ce5b1b40c2ff32e211b7b6e08b1b0da18327d798e`。
- 结构:132 节点、126 网格、深度 6、126 材质,其中 110 个 `metalness >= 0.9`。 - 结构:132 节点、126 网格、深度 6、126 材质,其中 110 个 `metalness >= 0.9`。
- 检查:已有内部层级完整保留;管理器使用 `studio-pmrem`、sRGB 输出、ACES、曝光 `1.45` 和多向补光;浏览器错误为 0。 - 检查:已有内部层级完整保留;管理器使用 `studio-pmrem`、sRGB 输出、ACES、曝光 `1.45` 和多向补光;浏览器错误为 0。
- 截图:`coverage/glb-machine-manager/mill700/mill700-studio-lighting.png`,SHA-256 `8943b16fe774427306ccafb5dcbc9817bc87538edbf67bbd5807dd59bd700d8b`。 - 截图:`coverage/glb-machine-manager/mill700/mill700-studio-lighting.png`,SHA-256 `98aa3915cc80255467c6be42d40da685ef43d4ed91441ec051e1830a8d5fafd4`。
## E06 / R04 / Z-up 坐标 ## E06 / R04 / Z-up 坐标
@@ -87,12 +87,17 @@
- 三轴箭头组的局部 position=(0,0,0)、quaternion=identity,父对象为所选 GLB 节点;X 红/Y 绿/Z 蓝且按整机尺寸缩放,表示关节自己的局部轴并继承完整父链。 - 三轴箭头组的局部 position=(0,0,0)、quaternion=identity,父对象为所选 GLB 节点;X 红/Y 绿/Z 蓝且按整机尺寸缩放,表示关节自己的局部轴并继承完整父链。
- 节点选择和轴值驱动期间 `context-generation=1`,浏览器 warning/error=0。 - 节点选择和轴值驱动期间 `context-generation=1`,浏览器 warning/error=0。
## E11 / R09 / 部件节点相对父级变换 ## E12 / R10 / 部件节点相对父级变换
- 替代 E11 的根节点测试结论;本轮同时验证内部关节及其父、兄弟、子节点。
- MILL700 命令:`PLAYWRIGHT_MODULE=/home/mes006/.npm/_npx/e41f203b7505f1fb/node_modules/playwright/index.js MACHINE_MANAGER_URL=http://127.0.0.1:5180/ node scripts/test_machine_manager_mill700_browser.mjs`。 - MILL700 命令:`PLAYWRIGHT_MODULE=/home/mes006/.npm/_npx/e41f203b7505f1fb/node_modules/playwright/index.js MACHINE_MANAGER_URL=http://127.0.0.1:5180/ node scripts/test_machine_manager_mill700_browser.mjs`。
- MILL700 report_id:`GLB-MGR-E05`;报告 SHA-256:`d76e64bd946af2a2c5ea0b2e93230a62cd7455d547db041223ba3a77f1c8ae29`。 - MILL700 report_id:`GLB-MGR-E05`;报告 SHA-256:`3555f9efe0cebba79a0bec8afbff8a4a44cc1278d0f36ff737fae9a74e18b9e1`。
- MILL700:先验证根节点 X 平移 `100 mm`、Z 旋转 `20°`;再选择内部节点 `MILL700-A-主轴-JT3`,输入相对父级 X=`120 mm`、RZ=`25°`,预览发布的 selected transform 为 local X=`0.120 m`、local quaternion 对应 `25°`;132 节点、126 网格和局部坐标系检查继续通过,浏览器 warning/error=0。 - MILL700:先验证根节点 X 平移 `100 mm`、Z 旋转 `20°`;再选择内部节点 `MILL700-A-主轴-JT3`,输入相对父级 X=`120 mm`、Z 从 `368.925 mm` 减少到 `324.925 mm`、RZ=`25°`,预览发布的 selected transform 为 local X=`0.120 m`、local quaternion 对应 `25°`;132 节点、126 网格和局部坐标系检查继续通过,浏览器 warning/error=0。
- AC/BC 生产回归命令:`PLAYWRIGHT_MODULE=/home/mes006/.npm/_npx/e41f203b7505f1fb/node_modules/playwright/index.js MACHINE_MANAGER_SOURCE_MODULES=0 MACHINE_MANAGER_URL=http://127.0.0.1:5180/ node scripts/test_machine_manager_browser.mjs`。 - AC/BC 生产回归命令:`PLAYWRIGHT_MODULE=/home/mes006/.npm/_npx/e41f203b7505f1fb/node_modules/playwright/index.js MACHINE_MANAGER_SOURCE_MODULES=0 MACHINE_MANAGER_URL=http://127.0.0.1:5180/ node scripts/test_machine_manager_browser.mjs`。
- AC/BC report_id:`GLB-MGR-E02`;报告 SHA-256:`9549e8e731803c86780b1113afda3adb78e8846626bf4b6006d8c9241ac457a9`;浏览器错误 0。 - AC/BC report_id:`GLB-MGR-E02`;报告 SHA-256:`9549e8e731803c86780b1113afda3adb78e8846626bf4b6006d8c9241ac457a9`;浏览器错误 0。
- 8 部件验证:根部件输入 X 平移 `10 mm`、Z 旋转 `15°` 后,预览中的根节点位置和姿态变化;应用生成的 geometry userData 保存 `translationMm[0]=10`、`rotationDeg[2]=15`,随后 XYZAC 切换 XYZBC 和 BC 五轴 WASM 程序运行通过。 - 8 部件验证:根部件输入 X 平移 `10 mm`、Z 旋转 `15°` 后,预览中的根节点位置和姿态变化;应用生成的 geometry userData 保存 `translationMm[0]=10`、`rotationDeg[2]=15`,随后 XYZAC 切换 XYZBC 和 BC 五轴 WASM 程序运行通过。
- 截图:`coverage/glb-machine-manager/mill700/mill700-studio-lighting.png`,SHA-256 `d422f2f19525d772cfb15c817f943fed6da2b183cfaa3dd9fc626cabed25a365`;AC 配置 `coverage/glb-machine-manager/manager-ac-config.png`,SHA-256 `ddfc10ebc03d0c72860ce4f21a3c7e290f9c8eeaa9dcaa97ff0a1c8b3e2bc860`。 - 截图:`coverage/glb-machine-manager/mill700/mill700-studio-lighting.png`,SHA-256 `98aa3915cc80255467c6be42d40da685ef43d4ed91441ec051e1830a8d5fafd4`;AC 配置 `coverage/glb-machine-manager/manager-ac-config.png`,SHA-256 `ddfc10ebc03d0c72860ce4f21a3c7e290f9c8eeaa9dcaa97ff0a1c8b3e2bc860`。
- 新增内部网格验收:mesh_0 相对父级 X=35 mm、RZ=30°;根部件仍为 X=10 mm、RZ=15°。直接解析导出 GLB 的节点 translation/rotation 并检查应用后 nodeTransforms 元数据,均一致。
- JT3 的父、兄弟、子节点编辑前后位置与四元数记录于 MILL700 报告的 `nodeEditing`;验证重选、重开对话框、编辑后换父级和再次输入。

View File

@@ -11,7 +11,7 @@
现有整机 GLB + 内嵌 URDF 导入能力继续保留。新管理器生成与现有 `linuxcnc.machine/1` 相同的严格结构,再通过已有配置、Worker、WASM 和 Three.js 投影链运行。 现有整机 GLB + 内嵌 URDF 导入能力继续保留。新管理器生成与现有 `linuxcnc.machine/1` 相同的严格结构,再通过已有配置、Worker、WASM 和 Three.js 投影链运行。
当前状态:MGR-01 至 MGR-16 全部 verified,MILL700 实测覆盖照明、Z-up、132 节点树、拖放、节点原点、关节局部坐标系、节点相对父级平移旋转、模型删除、轴绑定、XYZ/AC 层级运动和 WebGL 资源降级。最新台账 R09,证据 `GLB-MGR-E01/E02/E05/E07/E08/E09/E10/E11` 位于 `coverage/glb-machine-manager/`。 当前状态:MGR-01 至 MGR-16 全部 verified,MILL700 实测覆盖照明、Z-up、132 节点树、拖放、节点原点、关节局部坐标系、节点相对父级平移旋转、模型删除、轴绑定、XYZ/AC 层级运动和 WebGL 资源降级。最新台账 R10,证据 `GLB-MGR-E01/E02/E05/E07/E08/E09/E10/E12` 位于 `coverage/glb-machine-manager/`。
文档: 文档:

View File

@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
import { mkdir, readFile, writeFile } from "node:fs/promises"; import { mkdir, readFile, writeFile } from "node:fs/promises";
import { createHash } from "node:crypto"; import { createHash } from "node:crypto";
import { pathToFileURL } from "node:url"; import { pathToFileURL } from "node:url";
import * as THREE from "../web/react/node_modules/three/build/three.module.js";
const modulePath = process.env.PLAYWRIGHT_MODULE; const modulePath = process.env.PLAYWRIGHT_MODULE;
if (!modulePath) { console.log(JSON.stringify({ schema: "machine-manager-mill700-browser.v1", status: "blocked", code: "playwright_unavailable" })); process.exit(0); } if (!modulePath) { console.log(JSON.stringify({ schema: "machine-manager-mill700-browser.v1", status: "blocked", code: "playwright_unavailable" })); process.exit(0); }
@@ -27,8 +28,69 @@ try {
await page.waitForFunction(() => document.querySelector("[data-machine-manager-preview]")?.dataset.machineManagerPartCount === "1" && document.querySelector("[data-machine-manager-preview]")?.dataset.machineManagerRendered === "true" && document.querySelectorAll('[data-axis-control="machine-tree-node"]').length >= 132, null, { timeout: 60000 }); await page.waitForFunction(() => document.querySelector("[data-machine-manager-preview]")?.dataset.machineManagerPartCount === "1" && document.querySelector("[data-machine-manager-preview]")?.dataset.machineManagerRendered === "true" && document.querySelectorAll('[data-axis-control="machine-tree-node"]').length >= 132, null, { timeout: 60000 });
assert.equal(hierarchy.root, "MILL700-U_1"); assert.equal(hierarchy.nodes, 132); assert.equal(hierarchy.meshes, 126); assert.ok(hierarchy.maxDepth >= 6); assert.ok(hierarchy.metallic > 20); assert.equal(hierarchy.root, "MILL700-U_1"); assert.equal(hierarchy.nodes, 132); assert.equal(hierarchy.meshes, 126); assert.ok(hierarchy.maxDepth >= 6); assert.ok(hierarchy.metallic > 20);
const transformPreview = page.locator("[data-machine-manager-preview]"); const beforePartTransform = JSON.parse(await transformPreview.getAttribute("data-machine-manager-selected-joint-world-origin")); await control("machine-manager-translation").locator("input").nth(0).fill("100"); await page.waitForFunction(({ before }) => { const current = JSON.parse(document.querySelector("[data-machine-manager-preview]")?.getAttribute("data-machine-manager-selected-joint-world-origin") ?? "[]"); return current.length === 3 && Math.abs(current[0] - before[0] - .1) < 1e-7; }, { before: beforePartTransform }); const beforePartRotation = JSON.parse(await transformPreview.getAttribute("data-machine-manager-selected-joint-world-quaternion")); await control("machine-manager-rotation").locator("input").nth(2).fill("20"); await page.waitForFunction(({ before }) => { const current = JSON.parse(document.querySelector("[data-machine-manager-preview]")?.getAttribute("data-machine-manager-selected-joint-world-quaternion") ?? "[]"); return current.length === 4 && Math.abs(current[3] - before[3]) > 1e-4; }, { before: beforePartRotation }); report.checks.push("root and internal node transforms update their local position and posture relative to their parent"); const transformPreview = page.locator("[data-machine-manager-preview]"); const beforePartTransform = JSON.parse(await transformPreview.getAttribute("data-machine-manager-selected-joint-world-origin")); await control("machine-manager-translation").locator("input").nth(0).fill("100"); await page.waitForFunction(({ before }) => { const current = JSON.parse(document.querySelector("[data-machine-manager-preview]")?.getAttribute("data-machine-manager-selected-joint-world-origin") ?? "[]"); return current.length === 3 && Math.abs(current[0] - before[0] - .1) < 1e-7; }, { before: beforePartTransform }); const beforePartRotation = JSON.parse(await transformPreview.getAttribute("data-machine-manager-selected-joint-world-quaternion")); await control("machine-manager-rotation").locator("input").nth(2).fill("20"); await page.waitForFunction(({ before }) => { const current = JSON.parse(document.querySelector("[data-machine-manager-preview]")?.getAttribute("data-machine-manager-selected-joint-world-quaternion") ?? "[]"); return current.length === 4 && Math.abs(current[3] - before[3]) > 1e-4; }, { before: beforePartRotation }); report.checks.push("root and internal node transforms update their local position and posture relative to their parent");
assert.ok(await page.locator('[data-axis-control="machine-tree-node"]').count() >= 132); const selectedNode = page.locator('[data-axis-control="machine-tree-node"]').filter({ hasText: "MILL700-A-主轴-JT3" }).first(); await selectedNode.dispatchEvent("click"); assert.match(await control("machine-tree-origin").textContent(), /节点自身局部原点.*装配位置.*装配姿态/s); const selectedPath = await selectedNode.locator("xpath=ancestor::li[1]").getAttribute("data-machine-tree-path"), selectedPart = await selectedNode.locator("xpath=ancestor::li[1]").getAttribute("data-machine-tree-part"); await page.waitForFunction(({ selectedPart, selectedPath }) => { const preview = document.querySelector("[data-machine-manager-preview]"); return preview?.dataset.machineManagerSelectedJoint === `${selectedPart}:${selectedPath}` && preview?.dataset.machineManagerSelectedJointAxes === "local-xyz" && preview?.dataset.machineManagerTransformTarget === "none-internal-joint"; }, { selectedPart, selectedPath }); await control("machine-manager-translation").locator("input").nth(0).fill("120"); await page.waitForFunction(() => { const current = JSON.parse(document.querySelector("[data-machine-manager-preview]")?.getAttribute("data-machine-manager-selected-transform") ?? "{}"); return Math.abs(current.local?.[0] - .12) < 1e-7; }); await control("machine-manager-rotation").locator("input").nth(2).fill("25"); await page.waitForFunction(() => { const current = JSON.parse(document.querySelector("[data-machine-manager-preview]")?.getAttribute("data-machine-manager-selected-transform") ?? "{}"); return Math.abs(current.localQuaternion?.[2] - Math.sin(25 * Math.PI / 360)) < 1e-7 && Math.abs(current.localQuaternion?.[3] - Math.cos(25 * Math.PI / 360)) < 1e-7; }); const jointWorldOrigin = JSON.parse(await page.locator("[data-machine-manager-preview]").getAttribute("data-machine-manager-selected-joint-world-origin")); assert.ok(jointWorldOrigin.length === 3 && jointWorldOrigin.every(Number.isFinite)); assert.ok(Number(await page.locator("[data-machine-manager-preview]").getAttribute("data-machine-manager-selected-joint-axes-scale")) >= 1); await page.screenshot({ path: new URL("mill700-studio-lighting.png", output).pathname, timeout: 60000 }); const readSelected = async () => JSON.parse(await transformPreview.getAttribute("data-machine-manager-selected-transform"));
const source = page.locator('[data-axis-control="machine-tree-node"]').filter({ hasText: "MILL700-A-门1-JT1" }).first(), target = page.locator('[data-axis-control="machine-tree-node"]').filter({ hasText: "MILL700-A底座" }).first(); const sourceItem = source.locator("xpath=ancestor::li[1]"), targetItem = target.locator("xpath=ancestor::li[1]"), sourcePath = await sourceItem.getAttribute("data-machine-tree-path"), targetPath = await targetItem.getAttribute("data-machine-tree-path"), partId = await sourceItem.getAttribute("data-machine-tree-part"); await target.evaluate((element, payload) => { const transfer = new DataTransfer(); transfer.setData("application/x-linuxcnc-machine-tree", JSON.stringify({ partId: payload.partId, path: payload.sourcePath })); element.dispatchEvent(new DragEvent("drop", { bubbles: true, cancelable: true, dataTransfer: transfer })); }, { partId, sourcePath }); await page.waitForFunction(({ sourcePath, targetPath }) => document.querySelector(`[data-machine-tree-path="${sourcePath}"]`)?.getAttribute("data-machine-tree-parent") === targetPath, { sourcePath, targetPath }, { timeout: 60000 }); assert.equal(await page.locator("[data-machine-manager-preview]").getAttribute("data-machine-manager-context-generation"), "1"); const choose = async (name) => {
const button = control("machine-tree-node").filter({ has: page.locator("span").filter({ hasText: new RegExp(`^${name}$`) }) }).first();
const path = await button.locator("xpath=ancestor::li[1]").getAttribute("data-machine-tree-path");
await button.dispatchEvent("click");
await page.waitForFunction((path) => document.querySelector("[data-machine-manager-preview]")?.dataset.machineManagerSelectedJoint?.endsWith(`:${path}`), path);
return readSelected();
};
const closeVector = (actual, expected, label) => actual.forEach((value, index) => assert.ok(Math.abs(value - expected[index]) < 1e-7, `${label}[${index}]: ${value} != ${expected[index]}`));
const sampleBranch = async () => ({
parent: await choose("MILL700-A-主轴-JT2"),
sibling: await choose("SHS25C100020_Track_TSXXX-0000058522002"),
child: await choose("MILL700-A-主轴-JT4"),
selected: await choose("MILL700-A-主轴-JT3"),
});
const beforeEdit = await sampleBranch();
const translationInputs = control("machine-manager-translation").locator("input");
for (let i = 0; i < 3; i++) assert.ok(Math.abs(Number(await translationInputs.nth(i).inputValue()) - beforeEdit.selected.local[i] * 1000) < 1e-7);
assert.match(await control("machine-tree-origin").textContent(), /节点自身局部原点.*装配位置.*装配姿态/s);
await translationInputs.nth(0).fill("120");
// Screenshot complaint: move only the selected joint by -44 mm in its parent's Z direction.
await translationInputs.nth(2).fill(String(beforeEdit.selected.local[2] * 1000 - 44));
const translated = await sampleBranch();
const delta = new THREE.Vector3(.12 - beforeEdit.selected.local[0], 0, -.044)
.applyQuaternion(new THREE.Quaternion(...beforeEdit.parent.worldQuaternion));
closeVector(translated.selected.world, new THREE.Vector3(...beforeEdit.selected.world).add(delta).toArray(), "translation follows rotated parent axes");
closeVector(translated.child.world, new THREE.Vector3(...beforeEdit.child.world).add(delta).toArray(), "child inherits translation");
for (const key of ["parent", "sibling"]) assert.deepEqual(translated[key], beforeEdit[key], `${key} must not move`);
closeVector(translated.child.local, beforeEdit.child.local, "child local position unchanged");
await control("machine-manager-rotation").locator("input").nth(2).fill("25");
const rotated = await sampleBranch();
closeVector(rotated.selected.localQuaternion, new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), 25 * Math.PI / 180).toArray(), "selected local rotation");
closeVector(rotated.selected.world, translated.selected.world, "rotation about own origin");
for (const key of ["parent", "sibling"]) assert.deepEqual(rotated[key], beforeEdit[key], `${key} must not rotate`);
closeVector(rotated.child.local, beforeEdit.child.local, "child local origin unchanged");
const childWorld = new THREE.Vector3(...rotated.child.local).applyQuaternion(new THREE.Quaternion(...rotated.selected.worldQuaternion)).add(new THREE.Vector3(...rotated.selected.world));
closeVector(rotated.child.world, childWorld.toArray(), "child inherits selected rotation");
assert.ok(rotated.child.world.some((value, i) => Math.abs(value - translated.child.world[i]) > 1e-6));
assert.equal(await transformPreview.getAttribute("data-machine-manager-selected-joint-axes"), "local-xyz");
await choose("MILL700-A-主轴-JT2");
assert.equal(Number(await control("machine-manager-rotation").locator("input").nth(2).inputValue()), 0);
await choose("MILL700-A-主轴-JT3");
assert.equal(Number(await translationInputs.nth(0).inputValue()), 120);
assert.equal(Number(await control("machine-manager-rotation").locator("input").nth(2).inputValue()), 25);
await page.keyboard.press("Escape");
await control("machine-manager-settings").click();
await page.waitForFunction(() => document.querySelector("[data-machine-manager-preview]")?.dataset.machineManagerRendered === "true");
closeVector((await readSelected()).local, rotated.selected.local, "reopening preserves node edit");
report.nodeEditing = { before: beforeEdit, translated, rotated };
report.checks.push("MILL700 JT3 edits preserve parent and sibling transforms; child inherits movement and rotation around the joint origin; values survive reselection and dialog reopening");
await page.screenshot({ path: new URL("mill700-studio-lighting.png", output).pathname, timeout: 60000 });
const source = page.locator('[data-axis-control="machine-tree-node"]').filter({ hasText: "MILL700-A-门1-JT1" }).first(), target = page.locator('[data-axis-control="machine-tree-node"]').filter({ hasText: "MILL700-A底座" }).first(); await source.dispatchEvent("click");
await control("machine-manager-rotation").locator("input").nth(2).fill("-15");
const doorBefore = await readSelected();
const sourceItem = source.locator("xpath=ancestor::li[1]"), targetItem = target.locator("xpath=ancestor::li[1]"), sourcePath = await sourceItem.getAttribute("data-machine-tree-path"), targetPath = await targetItem.getAttribute("data-machine-tree-path"), partId = await sourceItem.getAttribute("data-machine-tree-part"); await target.evaluate((element, payload) => { const transfer = new DataTransfer(); transfer.setData("application/x-linuxcnc-machine-tree", JSON.stringify({ partId: payload.partId, path: payload.sourcePath })); element.dispatchEvent(new DragEvent("drop", { bubbles: true, cancelable: true, dataTransfer: transfer })); }, { partId, sourcePath }); await page.waitForFunction(({ sourcePath, targetPath }) => document.querySelector(`[data-machine-tree-path="${sourcePath}"]`)?.getAttribute("data-machine-tree-parent") === targetPath, { sourcePath, targetPath }, { timeout: 60000 }); assert.equal(await page.locator("[data-machine-manager-preview]").getAttribute("data-machine-manager-context-generation"), "1");
const doorAfter = await readSelected();
closeVector(doorAfter.world, doorBefore.world, "reparent preserves edited world position");
closeVector(doorAfter.worldQuaternion, doorBefore.worldQuaternion, "reparent preserves edited world orientation");
const doorLocalX = Number(await translationInputs.nth(0).inputValue());
await translationInputs.nth(0).fill(String(doorLocalX + 12));
const doorEdited = await readSelected();
closeVector(doorEdited.local, [doorAfter.local[0] + .012, ...doorAfter.local.slice(1)], "editing uses stable path after reparent");
report.checks.push("reparenting an edited node preserves world pose and subsequent inputs edit that node under its new parent");
report.hierarchy = hierarchy; report.checks.push("selected MILL700 joint displays its local XYZ coordinate system at local origin"); report.checks.push("existing 132-node, depth-6 hierarchy displayed as a draggable tree with node origins"); report.hierarchy = hierarchy; report.checks.push("selected MILL700 joint displays its local XYZ coordinate system at local origin"); report.checks.push("existing 132-node, depth-6 hierarchy displayed as a draggable tree with node origins");
const preview = page.locator("[data-machine-manager-preview]"); assert.equal(await preview.getAttribute("data-machine-manager-lighting"), "studio-pmrem"); assert.equal(await preview.getAttribute("data-machine-manager-coordinate-system"), "machine-z-up"); assert.equal(await preview.getAttribute("data-machine-manager-axis-map"), "X:x,Y:y,Z:z"); assert.equal(await preview.getAttribute("data-machine-manager-camera-up"), "0,0,1"); assert.equal(await preview.getAttribute("data-machine-manager-model-transform"), "none-on-import"); assert.equal(await preview.getAttribute("data-machine-manager-webgl-antialias"), "false"); assert.equal(await preview.getAttribute("data-machine-manager-render-state"), "rendered"); assert.equal(await preview.getAttribute("data-machine-manager-context-generation"), "1"); assert.equal(Number(await preview.getAttribute("data-machine-manager-scene-meshes")), 126); assert.ok(Number(await preview.getAttribute("data-machine-manager-scene-objects")) >= 132); const bounds = JSON.parse(await preview.getAttribute("data-machine-manager-bounds")); assert.ok(bounds.min.every(Number.isFinite) && bounds.max.every(Number.isFinite) && bounds.size > 0); const [bufferWidth, bufferHeight] = (await preview.getAttribute("data-machine-manager-buffer-size")).split("x").map(Number), maxBuffer = Number(await preview.getAttribute("data-machine-manager-max-buffer")); assert.ok(bufferWidth > 0 && bufferHeight > 0 && bufferWidth <= maxBuffer && bufferHeight <= maxBuffer); const preview = page.locator("[data-machine-manager-preview]"); assert.equal(await preview.getAttribute("data-machine-manager-lighting"), "studio-pmrem"); assert.equal(await preview.getAttribute("data-machine-manager-coordinate-system"), "machine-z-up"); assert.equal(await preview.getAttribute("data-machine-manager-axis-map"), "X:x,Y:y,Z:z"); assert.equal(await preview.getAttribute("data-machine-manager-camera-up"), "0,0,1"); assert.equal(await preview.getAttribute("data-machine-manager-model-transform"), "none-on-import"); assert.equal(await preview.getAttribute("data-machine-manager-webgl-antialias"), "false"); assert.equal(await preview.getAttribute("data-machine-manager-render-state"), "rendered"); assert.equal(await preview.getAttribute("data-machine-manager-context-generation"), "1"); assert.equal(Number(await preview.getAttribute("data-machine-manager-scene-meshes")), 126); assert.ok(Number(await preview.getAttribute("data-machine-manager-scene-objects")) >= 132); const bounds = JSON.parse(await preview.getAttribute("data-machine-manager-bounds")); assert.ok(bounds.min.every(Number.isFinite) && bounds.max.every(Number.isFinite) && bounds.size > 0); const [bufferWidth, bufferHeight] = (await preview.getAttribute("data-machine-manager-buffer-size")).split("x").map(Number), maxBuffer = Number(await preview.getAttribute("data-machine-manager-max-buffer")); assert.ok(bufferWidth > 0 && bufferHeight > 0 && bufferWidth <= maxBuffer && bufferHeight <= maxBuffer);
const bind = async (name, role) => { const node = page.locator('[data-axis-control="machine-tree-node"]').filter({ hasText: name }).first(); await node.dispatchEvent("click"); await control("machine-tree-axis-role").selectOption(role); }; const bind = async (name, role) => { const node = page.locator('[data-axis-control="machine-tree-node"]').filter({ hasText: name }).first(); await node.dispatchEvent("click"); await control("machine-tree-axis-role").selectOption(role); };