diff --git a/coverage/glb-machine-manager/managed-bc-running.png b/coverage/glb-machine-manager/managed-bc-running.png index 0866698..335392e 100644 Binary files a/coverage/glb-machine-manager/managed-bc-running.png and b/coverage/glb-machine-manager/managed-bc-running.png differ diff --git a/coverage/glb-machine-manager/mill700/browser.report.json b/coverage/glb-machine-manager/mill700/browser.report.json index 67cd708..60fcad1 100644 --- a/coverage/glb-machine-manager/mill700/browser.report.json +++ b/coverage/glb-machine-manager/mill700/browser.report.json @@ -8,6 +8,7 @@ "sha256": "bf37130681cc6462412e52744519516e8ade2a3212dd69153c0b1b3e415bcf66" }, "checks": [ + "part translation and rotation inputs update the selected GLB assembly root", "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", "each joint rotates about its own origin while children inherit parent motion", @@ -33,26 +34,26 @@ "baseline": { "base": { "local": [ - 0, + 0.12, 0, 0 ], "localQuaternion": [ 0, 0, - 0, - 1 + 0.21643961393810288, + 0.9762960071199334 ], "world": [ - 0, + 0.12, 0, 0 ], "worldQuaternion": [ 0, 0, - 0, - 1 + 0.21643961393810288, + 0.9762960071199335 ] }, "x": { @@ -68,15 +69,15 @@ 1 ], "world": [ - -1.8768789768218994, - 0.34002411365509033, + -1.7247304318819034, + -0.4850368286962905, 1.488389492034912 ], "worldQuaternion": [ 0, 0, - 0, - 1 + 0.21643961393810288, + 0.9762960071199335 ] }, "y": { @@ -92,15 +93,15 @@ 1 ], "world": [ - -1.8808482885360718, - 0.13574811816215515, + -1.6419970838670583, + -0.6718512577331518, 1.8940227031707764 ], "worldQuaternion": [ 0, 0, - 0, - 1 + 0.21643961393810288, + 0.9762960071199335 ] }, "z": { @@ -116,15 +117,15 @@ 1 ], "world": [ - -1.9016519784927368, - -0.7683008015155792, + -1.2787840471111187, + -1.4999898527864315, 2.2629480361938477 ], "worldQuaternion": [ 0, 0, - 0, - 1 + 0.21643961393810288, + 0.9762960071199335 ] }, "tilt": { @@ -140,15 +141,15 @@ 1 ], "world": [ - -1.8515222072601318, - -0.20127654075622559, + -1.4729858525275663, + -0.9649055930417467, 0.8832522630691528 ], "worldQuaternion": [ 0, 0, - 0, - 1 + 0.21643961393810288, + 0.9762960071199335 ] }, "c": { @@ -164,41 +165,41 @@ 1 ], "world": [ - -1.8516113758087158, - -0.8353592157363892, + -1.2050917487774973, + -1.5396173432583025, 0.796804666519165 ], "worldQuaternion": [ 0, 0, - 0, - 1 + 0.21643961393810288, + 0.9762960071199335 ] } }, "driven": { "base": { "local": [ - 0, + 0.12, 0, 0 ], "localQuaternion": [ 0, 0, - 0, - 1 + 0.21643961393810288, + 0.9762960071199334 ], "world": [ - 0, + 0.12, 0, 0 ], "worldQuaternion": [ 0, 0, - 0, - 1 + 0.21643961393810288, + 0.9762960071199335 ] }, "x": { @@ -214,15 +215,15 @@ 1 ], "world": [ - -1.8668789768218994, - 0.34002411365509033, + -1.715667354011537, + -0.48081064607888346, 1.488389492034912 ], "worldQuaternion": [ 0, 0, - 0, - 1 + 0.21643961393810288, + 0.9762960071199335 ] }, "y": { @@ -238,15 +239,15 @@ 1 ], "world": [ - -1.8708482885360718, - 0.15574811816215514, + -1.641386371231506, + -0.6494989193750118, 1.8940227031707764 ], "worldQuaternion": [ 0, 0, - 0, - 1 + 0.21643961393810288, + 0.9762960071199335 ] }, "z": { @@ -262,15 +263,15 @@ 1 ], "world": [ - -1.8916519784927368, - -0.7483008015155792, + -1.2781733344755664, + -1.4776375144282916, 2.292948036193848 ], "worldQuaternion": [ 0, 0, - 0, - 1 + 0.21643961393810288, + 0.9762960071199335 ] }, "tilt": { @@ -286,15 +287,15 @@ 0.9914448613738104 ], "world": [ - -1.8515222072601318, - -0.20127654075622559, + -1.4729858525275663, + -0.9649055930417467, 0.8832522630691528 ], "worldQuaternion": [ - 0.1305261922200516, - 0, - 0, - 0.9914448613738103 + 0.12743220028900526, + 0.02825103865291857, + 0.21458794303666343, + 0.9679436594388271 ] }, "c": { @@ -310,15 +311,15 @@ 0.9238795325112867 ], "world": [ - -1.8516113758087158, - -0.791379088131547, + -1.2236785538569896, + -1.4997578111351686, 0.6356376244864839 ], "worldQuaternion": [ - 0.1205904774487396, - -0.04995021125231482, - 0.3794095225512604, - 0.9159756150367535 + 0.12854320606946854, + -0.022665635416815422, + 0.5686694104253471, + 0.812144084994173 ] } } diff --git a/coverage/glb-machine-manager/mill700/mill700-studio-lighting.png b/coverage/glb-machine-manager/mill700/mill700-studio-lighting.png index 3f5f5de..4abebfd 100644 Binary files a/coverage/glb-machine-manager/mill700/mill700-studio-lighting.png and b/coverage/glb-machine-manager/mill700/mill700-studio-lighting.png differ diff --git a/docs/glb-machine-manager/03-推进台账.md b/docs/glb-machine-manager/03-推进台账.md index ff95644..6591c1e 100644 --- a/docs/glb-machine-manager/03-推进台账.md +++ b/docs/glb-machine-manager/03-推进台账.md @@ -70,3 +70,11 @@ - 决定复用 `linuxcnc.machine/1` 结构:管理器生成合法场景和 URDF,现有 INI/Worker/WASM 继续作为运动和 RTCP 权威。 - 当前任务:已完成。 - 下一步:按新需求继续;本专项不重复执行。 + +## R09 / 2026-09-21 / 部件平移与旋转实时同步 + +- 用户反馈编辑器中的“部件平移”和“部件旋转”改变后,预览中的对应 GLB 部件没有稳定更新。 +- 预览同步逻辑新增部件根节点变换步骤:每次状态同步都把 `translationMm` 转换为米写入导入部件根节点位置,把 `rotationDeg` 转换为弧度写入根节点姿态;当前选中内部关节时仍变换该关节所属的完整 GLB 部件。 +- 轴运动诊断随后在部件装配姿态上叠加,不覆盖编辑器已经设置的部件变换;组合导出的 geometry userData 同步保存平移和旋转值。 +- MILL700 输入 X 平移 100 mm、Z 旋转 20° 后,选中关节的世界原点和世界四元数均发生对应变化;8 部件 AC/BC 生产回归输入 X=10 mm、Z=15° 后,预览、导出和应用后的 geometry 分别保存 `translationMm[0]=10`、`rotationDeg[2]=15`。 +- MGR-16 verified;验收证据 E11。 diff --git a/docs/glb-machine-manager/04-任务矩阵.md b/docs/glb-machine-manager/04-任务矩阵.md index 466996d..6762a1a 100644 --- a/docs/glb-machine-manager/04-任务矩阵.md +++ b/docs/glb-machine-manager/04-任务矩阵.md @@ -17,3 +17,4 @@ |MGR-13 WebGL 资源稳定性|verified|MGR-10,MGR-12|禁用超限 MSAA;drawing buffer 按 GPU 上限和像素预算缩放;零尺寸/context lost 不连续绘制;短视口 DPR3 无警告| |MGR-14 层级运动验证|verified|MGR-12,MGR-13|模型删除;树节点轴绑定;XYZ mm、A/B/C deg 输入驱动局部层级;local/world 变换和 MILL700 父子联动验收| |MGR-15 关节局部坐标系|verified|MGR-12,MGR-14|选中树节点时局部 XYZ 辅助轴挂在节点 `(0,0,0)`,随关节和父链变换,不重建 WebGL| +|MGR-16 部件平移旋转同步|verified|MGR-05,MGR-14|编辑“部件平移/部件旋转”后对应 GLB 部件根节点实时改变位置和姿态;内部关节选择不改变变换归属;预览、导出和 AC/BC 应用保持一致| diff --git a/docs/glb-machine-manager/05-验收证据.md b/docs/glb-machine-manager/05-验收证据.md index c8d4a16..46615e7 100644 --- a/docs/glb-machine-manager/05-验收证据.md +++ b/docs/glb-machine-manager/05-验收证据.md @@ -86,3 +86,13 @@ - MILL700 选择 `MILL700-A-主轴-JT2` 后,预览发布稳定源路径,`selected-joint-axes=local-xyz`。 - 三轴箭头组的局部 position=(0,0,0)、quaternion=identity,父对象为所选 GLB 节点;X 红/Y 绿/Z 蓝且按整机尺寸缩放,表示关节自己的局部轴并继承完整父链。 - 节点选择和轴值驱动期间 `context-generation=1`,浏览器 warning/error=0。 + +## E11 / R09 / 部件平移旋转同步 + +- 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:`1a9489e9df2633f54de395a4a116d4726a56cc0bfc8f2115681e7340e52b9fbf`。 +- MILL700:输入部件 X 平移 `100 mm` 后,选中根节点世界 X 坐标增加 `0.1 m`;输入 Z 旋转 `20°` 后,选中根节点世界四元数发生变化;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 report_id:`GLB-MGR-E02`;报告 SHA-256:`7ea2ba7c09a2480973e63aedb8da1b8b0d46914eb86519781d99b718c5d3fca6`;浏览器错误 0。 +- 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 `2b40f1e6a92e09d54bde17543686cc2fcecee3e2b7d7d3d71296a63737a85c8d`;AC 配置 `coverage/glb-machine-manager/manager-ac-config.png`,SHA-256 `b2fc2e00497ddb1e72e3170ee83ed54c21c8da9099036d84eeb4d6d6eef8619b`。 diff --git a/docs/glb-machine-manager/06-决策记录.md b/docs/glb-machine-manager/06-决策记录.md index a93d27e..f3ff8dc 100644 --- a/docs/glb-machine-manager/06-决策记录.md +++ b/docs/glb-machine-manager/06-决策记录.md @@ -51,3 +51,7 @@ GLB 树节点 ID 使用导入时的子节点索引路径,不使用可能重复 ## D13 / 关节坐标轴属于节点局部空间 选中关节的坐标辅助必须作为该节点子对象放在局部 `(0,0,0)`,不可复制世界位置或使用整机坐标轴冒充。这样关节本身旋转时辅助轴同步旋转,父关节运动时又能自然继承父链,符合 GLB 层级矩阵语义。 + +## D14 / 部件编辑变换作用于 GLB 部件根节点 + +“部件平移”和“部件旋转”属于导入 GLB 的装配变换,作用对象是该 GLB 的外层部件根节点,因此选中内部关节时也移动或旋转该关节所在的完整部件。内部关节自己的轴绑定和 XYZ/ABC 诊断运动在此装配姿态之上叠加,不能把编辑器值误写到内部节点或用诊断输入覆盖编辑器值。导出和应用复用同一组 `translationMm`、`rotationDeg`,保证预览、组合 GLB 和运行机床保持一致。 diff --git a/docs/glb-machine-manager/README.md b/docs/glb-machine-manager/README.md index ffec821..71db39a 100644 --- a/docs/glb-machine-manager/README.md +++ b/docs/glb-machine-manager/README.md @@ -11,7 +11,7 @@ 现有整机 GLB + 内嵌 URDF 导入能力继续保留。新管理器生成与现有 `linuxcnc.machine/1` 相同的严格结构,再通过已有配置、Worker、WASM 和 Three.js 投影链运行。 -当前状态:MGR-01 至 MGR-15 全部 verified,MILL700 实测覆盖照明、Z-up、132 节点树、拖放、节点原点、关节局部坐标系、模型删除、轴绑定、XYZ/AC 层级运动和 WebGL 资源降级。最新台账 R08,证据 `GLB-MGR-E01/E02/E05/E07/E08/E09/E10` 位于 `coverage/glb-machine-manager/`。 +当前状态: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/`。 文档: diff --git a/scripts/test_machine_manager_browser.mjs b/scripts/test_machine_manager_browser.mjs index 91258c1..14ffe90 100644 --- a/scripts/test_machine_manager_browser.mjs +++ b/scripts/test_machine_manager_browser.mjs @@ -36,8 +36,8 @@ try { report.checks.push("dragging GLB root tree nodes updates component parent hierarchy"); await control("machine-manager-show-origin").uncheck(); assert.equal(await page.locator("[data-machine-manager-preview]").getAttribute("data-machine-manager-origin-count"), "7"); - const translation = page.locator("fieldset.machine-vector").filter({ hasText: "部件平移" }); await translation.getByLabel("X", { exact: true }).fill("10"); - const rotation = page.locator("fieldset.machine-vector").filter({ hasText: "部件旋转" }); await rotation.getByLabel("Z", { exact: true }).fill("15"); + const beforePartOrigin = JSON.parse(await page.locator("[data-machine-manager-preview]").getAttribute("data-machine-manager-selected-joint-world-origin")); const translation = page.locator("fieldset.machine-vector").filter({ hasText: "部件平移" }); await translation.getByLabel("X", { exact: true }).fill("10"); 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] - .01) < 1e-7; }, { before: beforePartOrigin }); + const beforePartQuaternion = JSON.parse(await page.locator("[data-machine-manager-preview]").getAttribute("data-machine-manager-selected-joint-world-quaternion")); const rotation = page.locator("fieldset.machine-vector").filter({ hasText: "部件旋转" }); await rotation.getByLabel("Z", { exact: true }).fill("15"); 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: beforePartQuaternion }); await control("machine-manager-rotate").click(); assert.equal(await page.locator("[data-machine-manager-preview]").getAttribute("data-transform-mode"), "rotate"); assert.equal(await page.locator("[data-machine-manager-preview]").getAttribute("data-machine-manager-context-generation"), "1"); await page.locator(".machine-manager-dialog").screenshot({ path: new URL("manager-ac-config.png", output).pathname }); @@ -52,7 +52,7 @@ try { assert.equal(await control("machine-manager-apply").isDisabled(), false); await control("machine-manager-apply").click(); await page.waitForFunction(() => document.querySelector(".xyzac-machine-view")?.dataset.machineManagerSchema === "linuxcnc.machine-manager/1" && document.querySelector("main.axis-app")?.dataset.axisProgramLoading === "false", null, { timeout: 45000 }); const geometry = JSON.parse(await page.locator(".xyzac-machine-view").getAttribute("data-machine-manager-geometry")); - assert.deepEqual(geometry.cOriginMm, [24, -12, 8]); assert.deepEqual(geometry.tiltOffsetMm, [0, 18, -45]); assert.equal(geometry.physicalZero.x, 5); assert.equal(geometry.physicalZero.a, 10); assert.equal(geometry.physicalZero.c, 20); assert.equal(geometry.parts.length, 8); assert.equal(geometry.parts[0].translationMm[0], 10); + assert.deepEqual(geometry.cOriginMm, [24, -12, 8]); assert.deepEqual(geometry.tiltOffsetMm, [0, 18, -45]); assert.equal(geometry.physicalZero.x, 5); assert.equal(geometry.physicalZero.a, 10); assert.equal(geometry.physicalZero.c, 20); assert.equal(geometry.parts.length, 8); assert.equal(geometry.parts[0].translationMm[0], 10); assert.equal(geometry.parts[0].rotationDeg[2], 15); assert.match(await page.locator(".xyzac-machine-view").getAttribute("data-xyzac-model-source-path"), /^managed-xyzac\.glb$/); report.checks.push("validated hierarchy applied through LOAD_WORKSPACE with geometry and physical zero in userData"); diff --git a/scripts/test_machine_manager_mill700_browser.mjs b/scripts/test_machine_manager_mill700_browser.mjs index 2976c2c..315b138 100644 --- a/scripts/test_machine_manager_mill700_browser.mjs +++ b/scripts/test_machine_manager_mill700_browser.mjs @@ -26,7 +26,8 @@ try { await control("machine-manager-settings").click(); await control("machine-manager-files").setInputFiles({ name: "MILL700-U_1.glb", mimeType: "model/gltf-binary", buffer: model }); 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.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-主轴-JT2" }).first(); await selectedNode.dispatchEvent("click"); assert.match(await control("machine-tree-origin").textContent(), /节点自身局部原点.*X 0\.000.*关节零姿态原点.*零姿态世界原点.*零姿态局部转角/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 }); 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 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("part translation and rotation inputs update the selected GLB assembly root"); + 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-主轴-JT2" }).first(); await selectedNode.dispatchEvent("click"); assert.match(await control("machine-tree-origin").textContent(), /节点自身局部原点.*X 0\.000.*关节零姿态原点.*零姿态世界原点.*零姿态局部转角/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 }); const internalBeforePartOrigin = JSON.parse(await page.locator("[data-machine-manager-preview]").getAttribute("data-machine-manager-selected-joint-world-origin")); await control("machine-manager-translation").locator("input").nth(0).fill("120"); 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] - .02) < 1e-7; }, { before: internalBeforePartOrigin }); const internalBeforePartRotation = JSON.parse(await page.locator("[data-machine-manager-preview]").getAttribute("data-machine-manager-selected-joint-world-quaternion")); await control("machine-manager-rotation").locator("input").nth(2).fill("25"); 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: internalBeforePartRotation }); 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 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"); 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); diff --git a/web/react/src/MachineManagerPanel.tsx b/web/react/src/MachineManagerPanel.tsx index f313155..f756958 100644 --- a/web/react/src/MachineManagerPanel.tsx +++ b/web/react/src/MachineManagerPanel.tsx @@ -83,6 +83,16 @@ function ManagerPreview({ coordinates, parts, selectedId, selectedNodePath, mode } lastParts = nextParts; } + // Apply the editor's part transform on every sync. The selected tree node + // may be an internal joint, but translation and rotation belong to the + // imported GLB part root and must move the complete part assembly. + for (const part of nextParts) { + const node = nodes.get(part.id); + if (!node) continue; + node.visible = part.visible; + node.position.fromArray(part.translationMm).multiplyScalar(.001); + node.rotation.set(...part.rotationDeg.map(THREE.MathUtils.degToRad) as Vec3); + } selectedPartId = nextSelectedId; selectedObject = nextSelectedId && nextNodePath === "scene" ? nodes.get(nextSelectedId) ?? null : null; transform.detach(); if (selectedObject) transform.attach(selectedObject); selectedJointAxes.removeFromParent(); selectedJointObject = nextSelectedId ? nextNodePath === "scene" ? nodes.get(nextSelectedId) ?? null : visualNodes.get(nextSelectedId)?.get(nextNodePath) ?? null : null; if (selectedJointObject) { selectedJointObject.add(selectedJointAxes); selectedJointAxes.position.set(0, 0, 0); selectedJointAxes.quaternion.identity(); selectedJointAxes.scale.setScalar(selectedAxesScale); selectedJointAxes.visible = true; element.dataset.machineManagerSelectedJoint = `${nextSelectedId}:${nextNodePath}`; element.dataset.machineManagerSelectedJointAxes = "local-xyz"; element.dataset.machineManagerTransformTarget = nextNodePath === "scene" ? "part-root" : "none-internal-joint"; } else { selectedJointAxes.visible = false; delete element.dataset.machineManagerSelectedJoint; delete element.dataset.machineManagerSelectedJointAxes; delete element.dataset.machineManagerTransformTarget; } for (const binding of roleNodes.values()) { binding.object.position.copy(binding.position); binding.object.quaternion.copy(binding.quaternion); } @@ -138,7 +148,7 @@ export function MachineManagerPanel({ coordinates, disabled, onApply }: Props) { const findTreeNode = (node: MachineTreeNode | undefined, path: string): MachineTreeNode | null => node ? node.path === path ? node : node.children.map((child) => findTreeNode(child, path)).find(Boolean) ?? null : null; const selectedTreeNode = selected ? findTreeNode(trees.get(selected.id), selectedNodePath) : null; const update = (id: string, values: Partial) => setParts((current) => current.map((part) => part.id === id ? { ...part, ...values } : part)); - const vectorInput = (label: string, value: Vec3, set: (value: Vec3) => void, unit: string) =>
{label} ({unit}){["X", "Y", "Z"].map((axis, index) => )}
; + const vectorInput = (label: string, value: Vec3, set: (value: Vec3) => void, unit: string, control?: string) =>
{label} ({unit}){["X", "Y", "Z"].map((axis, index) => )}
; const importFiles = async (list: FileList | File[] | null) => { if (!list?.length) return; setPending(true); setMessage("正在读取 GLB 部件…"); try { @@ -219,7 +229,7 @@ export function MachineManagerPanel({ coordinates, disabled, onApply }: Props) { {open && update(id, { translationMm, rotationDeg })} onMotionState={receiveMotionState} />}

部件与层级

{parts.length ?
    {parts.filter((part) => !part.parentId).map(renderPartTree)}
:

导入机床或部件后显示 GLB 内部层级。

}
- {selected &&

{selected.filename}

{selectedTreeNode &&
{selectedTreeNode.name}节点自身局部原点X 0.000 Y 0.000 Z 0.000关节零姿态原点(相对父节点,mm)X {selectedTreeNode.originMm[0].toFixed(3)} Y {selectedTreeNode.originMm[1].toFixed(3)} Z {selectedTreeNode.originMm[2].toFixed(3)}零姿态世界原点(mm)X {selectedTreeNode.worldOriginMm[0].toFixed(3)} Y {selectedTreeNode.worldOriginMm[1].toFixed(3)} Z {selectedTreeNode.worldOriginMm[2].toFixed(3)}零姿态局部转角(deg)RX {selectedTreeNode.rotationDeg[0].toFixed(3)} RY {selectedTreeNode.rotationDeg[1].toFixed(3)} RZ {selectedTreeNode.rotationDeg[2].toFixed(3)}{selectedDriven && <>当前驱动后的世界原点(mm)X {(selectedDriven.world[0] * 1000).toFixed(3)} Y {(selectedDriven.world[1] * 1000).toFixed(3)} Z {(selectedDriven.world[2] * 1000).toFixed(3)}}
}
{vectorInput("导入文件装配原点", selected.originMm, (originMm) => update(selected.id, { originMm }), "mm")}{vectorInput("部件平移", selected.translationMm, (translationMm) => update(selected.id, { translationMm }), "mm")}{vectorInput("部件旋转", selected.rotationDeg, (rotationDeg) => update(selected.id, { rotationDeg }), "deg")}
} + {selected &&

{selected.filename}

{selectedTreeNode &&
{selectedTreeNode.name}节点自身局部原点X 0.000 Y 0.000 Z 0.000关节零姿态原点(相对父节点,mm)X {selectedTreeNode.originMm[0].toFixed(3)} Y {selectedTreeNode.originMm[1].toFixed(3)} Z {selectedTreeNode.originMm[2].toFixed(3)}零姿态世界原点(mm)X {selectedTreeNode.worldOriginMm[0].toFixed(3)} Y {selectedTreeNode.worldOriginMm[1].toFixed(3)} Z {selectedTreeNode.worldOriginMm[2].toFixed(3)}零姿态局部转角(deg)RX {selectedTreeNode.rotationDeg[0].toFixed(3)} RY {selectedTreeNode.rotationDeg[1].toFixed(3)} RZ {selectedTreeNode.rotationDeg[2].toFixed(3)}{selectedDriven && <>当前驱动后的世界原点(mm)X {(selectedDriven.world[0] * 1000).toFixed(3)} Y {(selectedDriven.world[1] * 1000).toFixed(3)} Z {(selectedDriven.world[2] * 1000).toFixed(3)}}
}
{vectorInput("导入文件装配原点", selected.originMm, (originMm) => update(selected.id, { originMm }), "mm", "machine-manager-origin")}{vectorInput("部件平移", selected.translationMm, (translationMm) => update(selected.id, { translationMm }), "mm", "machine-manager-translation")}{vectorInput("部件旋转", selected.rotationDeg, (rotationDeg) => update(selected.id, { rotationDeg }), "deg", "machine-manager-rotation")}
}

层级运动测试

{(["x", "y", "z"] as const).map((axis) => poseInput(axis, "mm"))}{poseInput(coordinates === "XYZBC" ? "b" : "a", "deg")}{poseInput("c", "deg")}
{JSON.stringify(testPose)}