Apply hierarchy node transforms relative to parents
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
"eight GLB parts rendered with studio PMREM lighting and calibrated exposure",
|
||||
"dragging GLB root tree nodes updates component parent hierarchy",
|
||||
"origin visibility, numeric translation/rotation and TransformControls mode update",
|
||||
"internal mesh parent-relative position and rotation survive GLB export without moving its parent",
|
||||
"validated assembly exports a self-contained managed GLB",
|
||||
"validated hierarchy applied through LOAD_WORKSPACE with geometry and physical zero in userData",
|
||||
"same component assembly switches to BC hierarchy and applies BC geometry",
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"eight GLB parts rendered with studio PMREM lighting and calibrated exposure",
|
||||
"dragging GLB root tree nodes updates component parent hierarchy",
|
||||
"origin visibility, numeric translation/rotation and TransformControls mode update",
|
||||
"internal mesh parent-relative position and rotation survive GLB export without moving its parent",
|
||||
"validated assembly exports a self-contained managed GLB",
|
||||
"validated hierarchy applied through LOAD_WORKSPACE with geometry and physical zero in userData",
|
||||
"same component assembly switches to BC hierarchy and applies BC geometry",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 26 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 96 KiB |
@@ -8,7 +8,7 @@
|
||||
"sha256": "bf37130681cc6462412e52744519516e8ade2a3212dd69153c0b1b3e415bcf66"
|
||||
},
|
||||
"checks": [
|
||||
"part translation and rotation inputs update the selected GLB assembly root",
|
||||
"root and internal node transforms update their local position and posture relative to their parent",
|
||||
"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",
|
||||
@@ -34,26 +34,26 @@
|
||||
"baseline": {
|
||||
"base": {
|
||||
"local": [
|
||||
0.12,
|
||||
0.1,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"localQuaternion": [
|
||||
0,
|
||||
0,
|
||||
0.21643961393810288,
|
||||
0.9762960071199334
|
||||
0.17364817766693033,
|
||||
0.984807753012208
|
||||
],
|
||||
"world": [
|
||||
0.12,
|
||||
0.1,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"worldQuaternion": [
|
||||
0,
|
||||
0,
|
||||
0.21643961393810288,
|
||||
0.9762960071199335
|
||||
0.17364817766693033,
|
||||
0.9848077530122079
|
||||
]
|
||||
},
|
||||
"x": {
|
||||
@@ -69,15 +69,15 @@
|
||||
1
|
||||
],
|
||||
"world": [
|
||||
-1.7247304318819034,
|
||||
-0.4850368286962905,
|
||||
-1.7799844207142423,
|
||||
-0.3224122661666029,
|
||||
1.488389492034912
|
||||
],
|
||||
"worldQuaternion": [
|
||||
0,
|
||||
0,
|
||||
0.21643961393810288,
|
||||
0.9762960071199335
|
||||
0.17364817766693033,
|
||||
0.9848077530122079
|
||||
]
|
||||
},
|
||||
"y": {
|
||||
@@ -93,39 +93,39 @@
|
||||
1
|
||||
],
|
||||
"world": [
|
||||
-1.6419970838670583,
|
||||
-0.6718512577331518,
|
||||
-1.7138478483851618,
|
||||
-0.515726496296395,
|
||||
1.8940227031707764
|
||||
],
|
||||
"worldQuaternion": [
|
||||
0,
|
||||
0,
|
||||
0.21643961393810288,
|
||||
0.9762960071199335
|
||||
0.17364817766693033,
|
||||
0.9848077530122079
|
||||
]
|
||||
},
|
||||
"z": {
|
||||
"local": [
|
||||
-0.02080368995666504,
|
||||
0.12,
|
||||
-0.9040489196777344,
|
||||
0.3689253330230713
|
||||
],
|
||||
"localQuaternion": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1
|
||||
0.21643961393810288,
|
||||
0.9762960071199334
|
||||
],
|
||||
"world": [
|
||||
-1.2787840471111187,
|
||||
-1.4999898527864315,
|
||||
-1.2918817928092583,
|
||||
-1.3242121777479543,
|
||||
2.2629480361938477
|
||||
],
|
||||
"worldQuaternion": [
|
||||
0,
|
||||
0,
|
||||
0.21643961393810288,
|
||||
0.9762960071199335
|
||||
0.38268343236508984,
|
||||
0.9238795325112867
|
||||
]
|
||||
},
|
||||
"tilt": {
|
||||
@@ -141,15 +141,15 @@
|
||||
1
|
||||
],
|
||||
"world": [
|
||||
-1.4729858525275663,
|
||||
-0.9649055930417467,
|
||||
-1.571021124066044,
|
||||
-0.8223959707837081,
|
||||
0.8832522630691528
|
||||
],
|
||||
"worldQuaternion": [
|
||||
0,
|
||||
0,
|
||||
0.21643961393810288,
|
||||
0.9762960071199335
|
||||
0.17364817766693033,
|
||||
0.9848077530122079
|
||||
]
|
||||
},
|
||||
"c": {
|
||||
@@ -165,41 +165,41 @@
|
||||
1
|
||||
],
|
||||
"world": [
|
||||
-1.2050917487774973,
|
||||
-1.5396173432583025,
|
||||
-1.3542358677161157,
|
||||
-1.418269278870524,
|
||||
0.796804666519165
|
||||
],
|
||||
"worldQuaternion": [
|
||||
0,
|
||||
0,
|
||||
0.21643961393810288,
|
||||
0.9762960071199335
|
||||
0.17364817766693033,
|
||||
0.9848077530122079
|
||||
]
|
||||
}
|
||||
},
|
||||
"driven": {
|
||||
"base": {
|
||||
"local": [
|
||||
0.12,
|
||||
0.1,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"localQuaternion": [
|
||||
0,
|
||||
0,
|
||||
0.21643961393810288,
|
||||
0.9762960071199334
|
||||
0.17364817766693033,
|
||||
0.984807753012208
|
||||
],
|
||||
"world": [
|
||||
0.12,
|
||||
0.1,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"worldQuaternion": [
|
||||
0,
|
||||
0,
|
||||
0.21643961393810288,
|
||||
0.9762960071199335
|
||||
0.17364817766693033,
|
||||
0.9848077530122079
|
||||
]
|
||||
},
|
||||
"x": {
|
||||
@@ -215,15 +215,15 @@
|
||||
1
|
||||
],
|
||||
"world": [
|
||||
-1.715667354011537,
|
||||
-0.48081064607888346,
|
||||
-1.7705874945063833,
|
||||
-0.3189920647333462,
|
||||
1.488389492034912
|
||||
],
|
||||
"worldQuaternion": [
|
||||
0,
|
||||
0,
|
||||
0.21643961393810288,
|
||||
0.9762960071199335
|
||||
0.17364817766693033,
|
||||
0.9848077530122079
|
||||
]
|
||||
},
|
||||
"y": {
|
||||
@@ -239,39 +239,39 @@
|
||||
1
|
||||
],
|
||||
"world": [
|
||||
-1.641386371231506,
|
||||
-0.6494989193750118,
|
||||
-1.7112913250438162,
|
||||
-0.4935124424474202,
|
||||
1.8940227031707764
|
||||
],
|
||||
"worldQuaternion": [
|
||||
0,
|
||||
0,
|
||||
0.21643961393810288,
|
||||
0.9762960071199335
|
||||
0.17364817766693033,
|
||||
0.9848077530122079
|
||||
]
|
||||
},
|
||||
"z": {
|
||||
"local": [
|
||||
-0.02080368995666504,
|
||||
0.12,
|
||||
-0.9040489196777344,
|
||||
0.3989253330230713
|
||||
],
|
||||
"localQuaternion": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1
|
||||
0.21643961393810288,
|
||||
0.9762960071199334
|
||||
],
|
||||
"world": [
|
||||
-1.2781733344755664,
|
||||
-1.4776375144282916,
|
||||
-1.2893252694679127,
|
||||
-1.3019981238989793,
|
||||
2.292948036193848
|
||||
],
|
||||
"worldQuaternion": [
|
||||
0,
|
||||
0,
|
||||
0.21643961393810288,
|
||||
0.9762960071199335
|
||||
0.38268343236508984,
|
||||
0.9238795325112867
|
||||
]
|
||||
},
|
||||
"tilt": {
|
||||
@@ -287,15 +287,15 @@
|
||||
0.9914448613738104
|
||||
],
|
||||
"world": [
|
||||
-1.4729858525275663,
|
||||
-0.9649055930417467,
|
||||
-1.571021124066044,
|
||||
-0.8223959707837081,
|
||||
0.8832522630691528
|
||||
],
|
||||
"worldQuaternion": [
|
||||
0.12743220028900526,
|
||||
0.02825103865291857,
|
||||
0.21458794303666343,
|
||||
0.9679436594388271
|
||||
0.12854320606946854,
|
||||
0.022665635416815412,
|
||||
0.1721625934348045,
|
||||
0.9763825861650425
|
||||
]
|
||||
},
|
||||
"c": {
|
||||
@@ -311,15 +311,15 @@
|
||||
0.9238795325112867
|
||||
],
|
||||
"world": [
|
||||
-1.2236785538569896,
|
||||
-1.4997578111351686,
|
||||
-1.369277957263005,
|
||||
-1.3769414774990314,
|
||||
0.6356376244864839
|
||||
],
|
||||
"worldQuaternion": [
|
||||
0.12854320606946854,
|
||||
-0.022665635416815422,
|
||||
0.5686694104253471,
|
||||
0.812144084994173
|
||||
0.12743220028900526,
|
||||
-0.02825103865291858,
|
||||
0.5327029357136194,
|
||||
0.8361761150778141
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 338 KiB After Width: | Height: | Size: 334 KiB |
@@ -12,5 +12,6 @@
|
||||
10. `MGR-12`:按 GLB 原始 `Object3D.children` 显示树,支持根部件和内部节点拖放,选择节点显示父级局部原点;用 MILL700 六层结构验收。
|
||||
11. `MGR-14`:新增导入模型删除、树节点轴绑定和 XYZ/ABC 层级运动测试,输出局部/世界变换;MILL700 主轴链与工作台链验收。
|
||||
12. `MGR-15`:树选择关节后,将 XYZ 辅助轴挂在该节点局部原点,并验证轴系跟随关节及父链旋转。
|
||||
13. `MGR-16`:将根节点部件变换和内部节点 `nodeTransforms[path]` 分开保存;编辑当前节点相对父级的位置和姿态,验证子节点继承、父兄节点不受影响、拖放换父级后保持世界姿态,并验证组合 GLB 与 AC/BC 应用结果。
|
||||
|
||||
每轮只推进任务矩阵中依赖已满足的任务。代码完成后必须同步更新 03、04、05;若改变数据契约或坐标定义,先更新 06。
|
||||
|
||||
@@ -71,10 +71,11 @@
|
||||
- 当前任务:已完成。
|
||||
- 下一步:按新需求继续;本专项不重复执行。
|
||||
|
||||
## R09 / 2026-09-21 / 部件平移与旋转实时同步
|
||||
## 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`。
|
||||
- 用户明确要求“部件平移”和“部件旋转”修改当前选中节点相对其父节点的位置和姿态。
|
||||
- 根节点继续使用部件级 `translationMm`、`rotationDeg`;GLB 内部节点使用稳定源路径保存到 `nodeTransforms[path]`。输入值是当前节点的父级局部位置(mm)和 XYZ 欧拉姿态(deg),旋转中心是节点自身原点。
|
||||
- 预览克隆、组合导出和应用运行场景统一应用同一组局部节点变换;父节点运动时子节点继承,编辑子节点不会错误移动整个 GLB 根部件。
|
||||
- 拖放内部节点后保存当前局部姿态并重新计算新的父级局部姿态,保持拖放前的世界姿态。
|
||||
- MILL700 选择 `MILL700-A-主轴-JT3`,输入 X=120 mm、RZ=25° 后,节点局部位置和四元数分别变为 `0.120 m` 和 `25°`;8 部件 AC/BC 生产回归、导出和应用继续通过。
|
||||
- MGR-16 verified;验收证据 E11。
|
||||
|
||||
@@ -17,4 +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 应用保持一致|
|
||||
|MGR-16 部件节点相对父级变换|verified|MGR-05,MGR-14|编辑树中当前节点的“部件平移/部件旋转”后,该节点相对父级的位置和姿态改变;父节点与兄弟节点不被误改;子节点继承;预览、导出和 AC/BC 应用保持一致|
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
- 命令:`PLAYWRIGHT_MODULE=/home/mes006/.npm/_npx/e41f203b7505f1fb/node_modules/playwright/index.mjs node scripts/test_machine_manager_browser.mjs`
|
||||
- report_id:`GLB-MGR-E01`
|
||||
- 报告:`coverage/glb-machine-manager/browser.report.json`
|
||||
- SHA-256:`486108398c757421d59fd5311239675b82d6c688458975b7c613118920ceafbf`
|
||||
- SHA-256:`a24ae854c1817f13b2c6c4fc03d33b855952e92eaa54f900482eaba9f3acd6de`
|
||||
- 状态:verified;浏览器错误 0。
|
||||
- 覆盖:8 部件导入和预览、原点显隐、数值/TransformControls 变换、组合 GLB 导出、AC/BC 应用、底座 userData、物理零点、非法层级拒绝、BC 五轴程序。
|
||||
- 截图:
|
||||
- `coverage/glb-machine-manager/manager-ac-config.png`,SHA-256 `b2fc2e00497ddb1e72e3170ee83ed54c21c8da9099036d84eeb4d6d6eef8619b`
|
||||
- `coverage/glb-machine-manager/managed-bc-running.png`,SHA-256 `4b2d76cd505f1e31847d09650b052a0a9c393515cb7c71ad55ac649072634005`
|
||||
- `coverage/glb-machine-manager/manager-ac-config.png`,SHA-256 `ddfc10ebc03d0c72860ce4f21a3c7e290f9c8eeaa9dcaa97ff0a1c8b3e2bc860`
|
||||
- `coverage/glb-machine-manager/managed-bc-running.png`,SHA-256 `f05cd2f3701c68fe82c5c57d6aa92b54c92d6783d1a395efb5fe96ec5a9bfe4f`
|
||||
|
||||
## E03 / R02 / 生产构建浏览器闭环
|
||||
|
||||
@@ -25,12 +25,12 @@
|
||||
- 命令:`MACHINE_MANAGER_SOURCE_MODULES=0 ... node scripts/test_machine_manager_browser.mjs`
|
||||
- report_id:`GLB-MGR-E02`
|
||||
- 报告:`coverage/glb-machine-manager/browser-production.report.json`
|
||||
- SHA-256:`7ea2ba7c09a2480973e63aedb8da1b8b0d46914eb86519781d99b718c5d3fca6`
|
||||
- SHA-256:`9549e8e731803c86780b1113afda3adb78e8846626bf4b6006d8c9241ac457a9`
|
||||
- 状态:verified;生产资产执行 AC/BC UI 应用和 BC WASM 程序运动,浏览器错误 0。
|
||||
|
||||
## E04 / R02 / 回归与静态契约
|
||||
|
||||
- `node scripts/test_axis_control_inventory.mjs`:verified,99 个文档化控件。
|
||||
- `node scripts/test_axis_control_inventory.mjs`:verified,114 个文档化控件。
|
||||
- `node web/react/src/test_dual_canvas_contract.mjs`:verified。
|
||||
- `node web/react/src/test_xyzac_threejs.mjs`:verified。
|
||||
- `npm --prefix web/react run typecheck`:通过。
|
||||
@@ -61,7 +61,7 @@
|
||||
- 节点选择:`MILL700-A-主轴-JT2` 显示相对父节点原点 `-3.969 / -204.276 / 405.633 mm`。
|
||||
- 拖放:`MILL700-A-门1-JT1` 拖到 `MILL700-A底座` 后,草稿父路径等于目标稳定源路径;原始 GLB SHA-256 保持 `bf37130681cc6462412e52744519516e8ade2a3212dd69153c0b1b3e415bcf66`。
|
||||
- 八个独立 GLB 的根树节点拖放通过;随后 AC/BC 应用、导出和程序运动回归仍通过。
|
||||
- 控件契约:101 个控件,新增 `machine-tree-node` 和 `machine-tree-origin`;TypeScript、生产构建、双画布测试通过,浏览器错误 0。
|
||||
- 控件契约:114 个控件,包含节点相对位置/姿态输入 `machine-manager-translation`、`machine-manager-rotation`;TypeScript、生产构建、双画布测试通过,浏览器错误 0。
|
||||
|
||||
## E08 / R06 / WebGL framebuffer 稳定性
|
||||
|
||||
@@ -87,12 +87,12 @@
|
||||
- 三轴箭头组的局部 position=(0,0,0)、quaternion=identity,父对象为所选 GLB 节点;X 红/Y 绿/Z 蓝且按整机尺寸缩放,表示关节自己的局部轴并继承完整父链。
|
||||
- 节点选择和轴值驱动期间 `context-generation=1`,浏览器 warning/error=0。
|
||||
|
||||
## E11 / R09 / 部件平移旋转同步
|
||||
## 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。
|
||||
- MILL700 report_id:`GLB-MGR-E05`;报告 SHA-256:`d76e64bd946af2a2c5ea0b2e93230a62cd7455d547db041223ba3a77f1c8ae29`。
|
||||
- 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。
|
||||
- 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`。
|
||||
- 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 程序运行通过。
|
||||
- 截图:`coverage/glb-machine-manager/mill700/mill700-studio-lighting.png`,SHA-256 `d422f2f19525d772cfb15c817f943fed6da2b183cfaa3dd9fc626cabed25a365`;AC 配置 `coverage/glb-machine-manager/manager-ac-config.png`,SHA-256 `ddfc10ebc03d0c72860ce4f21a3c7e290f9c8eeaa9dcaa97ff0a1c8b3e2bc860`。
|
||||
|
||||
@@ -52,6 +52,6 @@ GLB 树节点 ID 使用导入时的子节点索引路径,不使用可能重复
|
||||
|
||||
选中关节的坐标辅助必须作为该节点子对象放在局部 `(0,0,0)`,不可复制世界位置或使用整机坐标轴冒充。这样关节本身旋转时辅助轴同步旋转,父关节运动时又能自然继承父链,符合 GLB 层级矩阵语义。
|
||||
|
||||
## D14 / 部件编辑变换作用于 GLB 部件根节点
|
||||
## D14 / 部件编辑变换作用于当前树节点的父级局部空间
|
||||
|
||||
“部件平移”和“部件旋转”属于导入 GLB 的装配变换,作用对象是该 GLB 的外层部件根节点,因此选中内部关节时也移动或旋转该关节所在的完整部件。内部关节自己的轴绑定和 XYZ/ABC 诊断运动在此装配姿态之上叠加,不能把编辑器值误写到内部节点或用诊断输入覆盖编辑器值。导出和应用复用同一组 `translationMm`、`rotationDeg`,保证预览、组合 GLB 和运行机床保持一致。
|
||||
“部件平移”和“部件旋转”编辑当前选中的树节点。根节点的值保存为部件级 `translationMm`、`rotationDeg`;内部节点的值按稳定源路径保存为 `nodeTransforms[path]`,直接表示该节点相对当前父节点的位置和姿态。节点自身局部原点是旋转中心,子节点自然继承父节点变换,父节点和兄弟节点不因编辑子节点而改变。拖放改变父级后,先保持世界姿态,再重新计算局部值。导出和应用复用同一套局部变换,保证预览、组合 GLB 和运行机床一致。
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
现有整机 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 资源降级。最新台账 R09,证据 `GLB-MGR-E01/E02/E05/E07/E08/E09/E10/E11` 位于 `coverage/glb-machine-manager/`。
|
||||
|
||||
文档:
|
||||
|
||||
|
||||
@@ -449,7 +449,7 @@ history rows intentionally share the family ID `mdi-history-select`.
|
||||
| `workspace-case-select` | Web selection of the INI/configuration input | Local selection; selected catalog is restricted to supported XYZAC/XYZBC cases | Does not load or change machine state until `toolbar-load-case` |
|
||||
| `workspace-load-3d-chips` | Web packaged-file equivalent of `open_file_guts` | Fetch published `3D_Chips.ngc`, then `PROGRAM_LOAD`; IDLE only | Same replacement path as Open; retains Live Plot, no automatic Run/Home |
|
||||
| `workspace-rtcp-programs`, `rtcp-program-scope`, `rtcp-program-select`, `rtcp-program-source`, `rtcp-program-load`, `rtcp-program-download` | Web RTCP source-program catalog backed by the locked LinuxCNC tree | The default scope follows the active machine: AC=`XYZAC`, BC=`XYZBC`; catalog/source/download are local file operations, while compatible main programs use the same `PROGRAM_LOAD` transaction and IDLE gate as Open | AC and BC programs are separated by kinematics. The optional all-source scope retains all 32 files for inspection; remap/subroutine/reference and mismatched kinematics cannot be sent as an executable main program |
|
||||
| `machine-manager-settings`, `machine-manager-import`, `machine-manager-files`, `machine-manager-delete`, `machine-manager-translate`, `machine-manager-rotate`, `machine-manager-role`, `machine-manager-parent`, `machine-manager-show-origin`, `machine-tree-node`, `machine-tree-origin`, `machine-tree-axis-role`, `machine-test-x`, `machine-test-y`, `machine-test-z`, `machine-test-a`, `machine-test-b`, `machine-test-c`, `machine-test-reset`, `machine-test-state`, `machine-manager-apply`, `machine-manager-export` | Web component-level machine assembly extension | GLB `Object3D.children` render as a tree; selecting a node shows its parent-relative origin, dragging records parent overrides, and axis-test inputs drive bound nodes through their actual parent hierarchy without changing the source scene. Imported roots can be deleted. Apply validates AC/BC and then uses `LOAD_WORKSPACE` + `PROGRAM_LOAD` while OFF and IDLE | Axis-test motion is a local hierarchy diagnostic; production task motion, tool offset and RTCP remain Worker/WASM authoritative |
|
||||
| `machine-manager-settings`, `machine-manager-import`, `machine-manager-files`, `machine-manager-delete`, `machine-manager-translate`, `machine-manager-rotate`, `machine-manager-origin`, `machine-manager-translation`, `machine-manager-rotation`, `machine-manager-role`, `machine-manager-parent`, `machine-manager-show-origin`, `machine-tree-node`, `machine-tree-origin`, `machine-tree-axis-role`, `machine-test-x`, `machine-test-y`, `machine-test-z`, `machine-test-a`, `machine-test-b`, `machine-test-c`, `machine-test-reset`, `machine-test-state`, `machine-manager-apply`, `machine-manager-export` | Web component-level machine assembly extension | GLB `Object3D.children` render as a tree; selecting a node shows its parent-relative origin, numeric translation and rotation edit that node's local position and posture relative to its current parent, dragging records parent overrides, and axis-test inputs drive bound nodes through their actual parent hierarchy without changing the source scene. Imported roots can be deleted. Apply validates AC/BC and then uses `LOAD_WORKSPACE` + `PROGRAM_LOAD` while OFF and IDLE | Axis-test motion is a local hierarchy diagnostic; production task motion, tool offset and RTCP remain Worker/WASM authoritative |
|
||||
| `manual-home-all` | `home_all_joints` | `HOME {all:true}`; ON+IDLE, no concurrent homing/jog | Native homemod selects the configured sequences. No artificial coordinated return-to-zero motion. |
|
||||
| `switchkins-0`, `switchkins-1`, `switchkins-2` | `M429`, `M428`, `M430` remaps and `motion.switchkins-type` | `KINEMATICS_SELECT`; ON, homed, IDLE, empty TP | Identity/XYZAC/User K canonical type, or stable gate reason |
|
||||
| `tab-manual`, `tab-mdi` | `ensure_manual` / `ensure_mdi` | `SET_MODE`; manual_ok and motion exclusion | Task mode acknowledgement and WASM motion mode are monitored separately; no local tab is mode authority. |
|
||||
|
||||
@@ -13,7 +13,7 @@ const [contract, app, panel, preview] = await Promise.all([
|
||||
const controlIds = [
|
||||
"menu-file", "menu-file-edit-tool-data", "menu-file-quit", "menu-help", "menu-help-about", "about-close",
|
||||
"toolbar-estop", "toolbar-power", "toolbar-open", "toolbar-load-case", "toolbar-reload", "toolbar-run", "toolbar-step", "toolbar-pause-resume", "toolbar-stop", "toolbar-block-delete", "toolbar-optional-pause", "toolbar-zoom-in", "toolbar-zoom-out", "toolbar-view-z", "toolbar-view-z2", "toolbar-view-x", "toolbar-view-y", "toolbar-view-y2", "toolbar-view-p", "toolbar-rotate", "toolbar-clear-live-plot",
|
||||
"workspace-case-select", "workspace-load-3d-chips", "workspace-rtcp-programs", "rtcp-program-scope", "rtcp-program-select", "rtcp-program-source", "rtcp-program-load", "rtcp-program-download", "machine-manager-settings", "machine-manager-import", "machine-manager-files", "machine-manager-delete", "machine-manager-translate", "machine-manager-rotate", "machine-manager-role", "machine-manager-parent", "machine-manager-show-origin", "machine-tree-node", "machine-tree-origin", "machine-tree-axis-role", "machine-test-x", "machine-test-y", "machine-test-z", "machine-test-a", "machine-test-b", "machine-test-c", "machine-test-reset", "machine-test-state", "machine-manager-apply", "machine-manager-export", "manual-home-all", "manual-home-joint-select",
|
||||
"workspace-case-select", "workspace-load-3d-chips", "workspace-rtcp-programs", "rtcp-program-scope", "rtcp-program-select", "rtcp-program-source", "rtcp-program-load", "rtcp-program-download", "machine-manager-settings", "machine-manager-import", "machine-manager-files", "machine-manager-delete", "machine-manager-translate", "machine-manager-rotate", "machine-manager-origin", "machine-manager-translation", "machine-manager-rotation", "machine-manager-role", "machine-manager-parent", "machine-manager-show-origin", "machine-tree-node", "machine-tree-origin", "machine-tree-axis-role", "machine-test-x", "machine-test-y", "machine-test-z", "machine-test-a", "machine-test-b", "machine-test-c", "machine-test-reset", "machine-test-state", "machine-manager-apply", "machine-manager-export", "manual-home-all", "manual-home-joint-select",
|
||||
"switchkins-0", "switchkins-1", "switchkins-2",
|
||||
"tab-manual", "tab-mdi", "tab-diagnostics", "tab-preview", "tab-dro",
|
||||
"manual-axis-x", "manual-axis-y", "manual-axis-z", "manual-axis-a", "manual-axis-c",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import * as THREE from "../web/react/node_modules/three/build/three.module.js";
|
||||
import { GLTFExporter } from "../web/react/node_modules/three/examples/jsm/exporters/GLTFExporter.js";
|
||||
|
||||
@@ -43,16 +43,34 @@ try {
|
||||
await page.locator(".machine-manager-dialog").screenshot({ path: new URL("manager-ac-config.png", output).pathname });
|
||||
report.checks.push("origin visibility, numeric translation/rotation and TransformControls mode update");
|
||||
|
||||
// Editing an imported mesh must preserve the enclosing GLB placement.
|
||||
const meshNode = control("machine-tree-node").filter({ hasText: "mesh_0" }).first();
|
||||
const meshPath = await meshNode.locator("xpath=ancestor::li[1]").getAttribute("data-machine-tree-path");
|
||||
await meshNode.dispatchEvent("click");
|
||||
await control("machine-manager-translation").getByLabel("X", { exact: true }).fill("35");
|
||||
await control("machine-manager-rotation").getByLabel("Z", { exact: true }).fill("30");
|
||||
const baseRoot = page.locator('[data-machine-tree-path="scene"]').first();
|
||||
await baseRoot.locator('[data-axis-control="machine-tree-node"]').first().dispatchEvent("click");
|
||||
assert.equal(await translation.getByLabel("X", { exact: true }).inputValue(), "10");
|
||||
assert.equal(await rotation.getByLabel("Z", { exact: true }).inputValue(), "15");
|
||||
|
||||
const calibrations = page.locator(".machine-manager-calibration fieldset.machine-vector");
|
||||
const cOrigin = calibrations.filter({ hasText: "C 轴原点" }); await cOrigin.getByLabel("X", { exact: true }).fill("24"); await cOrigin.getByLabel("Y", { exact: true }).fill("-12"); await cOrigin.getByLabel("Z", { exact: true }).fill("8");
|
||||
const offset = calibrations.filter({ hasText: "A 到 C 轴偏置" }); await offset.getByLabel("X", { exact: true }).fill("0"); await offset.getByLabel("Y", { exact: true }).fill("18"); await offset.getByLabel("Z", { exact: true }).fill("-45");
|
||||
const zeros = page.locator("fieldset.machine-zero"); await zeros.getByLabel("X", { exact: true }).fill("5"); await zeros.getByLabel("A", { exact: true }).fill("10"); await zeros.getByLabel("C", { exact: true }).fill("20");
|
||||
assert.equal(await control("machine-manager-export").isDisabled(), false); assert.equal(await page.locator("[data-machine-manager-preview]").getAttribute("data-machine-manager-coordinate-system"), "machine-z-up"); assert.equal(await page.locator("[data-machine-manager-preview]").getAttribute("data-machine-manager-axis-map"), "X:x,Y:y,Z:z"); assert.equal(await page.locator("[data-machine-manager-preview]").getAttribute("data-machine-manager-camera-up"), "0,0,1"); const downloadPromise = page.waitForEvent("download"); await control("machine-manager-export").click(); const download = await downloadPromise; assert.equal(download.suggestedFilename(), "managed-xyzac.glb");
|
||||
const exportedBytes = await readFile(await download.path());
|
||||
const exported = JSON.parse(exportedBytes.toString("utf8", 20, 20 + exportedBytes.readUInt32LE(12)));
|
||||
const exportedMesh = exported.nodes.find((node) => node.name === "mesh_0");
|
||||
assert.ok(Math.abs(exportedMesh.translation[0] - .035) < 1e-7);
|
||||
assert.ok(Math.abs(exportedMesh.rotation[2] - Math.sin(Math.PI / 12)) < 1e-7);
|
||||
report.checks.push("internal mesh parent-relative position and rotation survive GLB export without moving its parent");
|
||||
report.checks.push("validated assembly exports a self-contained managed GLB");
|
||||
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.equal(geometry.parts[0].rotationDeg[2], 15);
|
||||
assert.deepEqual(geometry.parts[0].nodeTransforms[meshPath], { translationMm: [35, 0, 0], rotationDeg: [0, 0, 30] });
|
||||
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");
|
||||
|
||||
@@ -77,7 +95,7 @@ try {
|
||||
const projection = await page.evaluate(async () => {
|
||||
const manager = await import("/src/machine-manager.ts"); const importedMachine = await import("/src/imported-machine.ts");
|
||||
const sources = await Promise.all(Array.from({ length: 8 }, async (_, index) => manager.loadMachinePart(await (await fetch("/machines/demo-ac-xyz-head.glb")).arrayBuffer(), `source-${index}.glb`)));
|
||||
const roles = manager.MACHINE_PART_ROLES; const drafts = sources.map((source, index) => { const role = roles[index]; const parentRole = manager.expectedParentRole(role); return { ...source, role, parentId: parentRole ? sources[roles.indexOf(parentRole)].id : null, visible: true, showOrigin: true, originMm: [0, 0, 0], translationMm: [0, 0, 0], rotationDeg: [0, 0, 0] }; });
|
||||
const roles = manager.MACHINE_PART_ROLES; const drafts = sources.map((source, index) => { const role = roles[index]; const parentRole = manager.expectedParentRole(role); return { ...source, role, parentId: parentRole ? sources[roles.indexOf(parentRole)].id : null, visible: true, showOrigin: true, originMm: [0, 0, 0], translationMm: [0, 0, 0], rotationDeg: [0, 0, 0], nodeParents: {}, nodeRoles: {}, nodeTransforms: {} }; });
|
||||
const run = async (coordinates, cOriginMm, tiltOffsetMm, physicalZero, joints) => {
|
||||
const asset = await manager.buildManagedMachine({ coordinates, parts: drafts, cOriginMm, tiltOffsetMm, physicalZero }); const built = importedMachine.buildImportedMachine(asset);
|
||||
built.update({ joints, offsets: { x_rot_point: cOriginMm[0], y_rot_point: cOriginMm[1], z_rot_point: cOriginMm[2], x_offset: tiltOffsetMm[0], y_offset: tiltOffsetMm[1], z_offset: tiltOffsetMm[2], tool_offset: 0 } });
|
||||
|
||||
@@ -26,8 +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);
|
||||
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 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 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);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { GLTFExporter } from "three/examples/jsm/exporters/GLTFExporter.js";
|
||||
import { RoomEnvironment } from "three/examples/jsm/environments/RoomEnvironment.js";
|
||||
import { DraggableDialog } from "./DraggableDialog";
|
||||
import { disposeMachineScene, type ImportedMachine } from "./imported-machine";
|
||||
import { applyMachineNodeParents, buildManagedMachine, expectedParentRole, loadMachinePart, machinePartTree, MACHINE_PART_ROLES, roleLabel, type MachinePartDraft, type MachinePartRole, type MachineTreeNode, type PhysicalZero } from "./machine-manager";
|
||||
import { configureMachinePartScene, reparentMachineNode, buildManagedMachine, expectedParentRole, loadMachinePart, machinePartTree, MACHINE_PART_ROLES, roleLabel, type MachinePartDraft, type MachinePartRole, type MachineTreeNode, type PhysicalZero } from "./machine-manager";
|
||||
import type { Vec3 } from "./machine-urdf";
|
||||
|
||||
type Props = { coordinates: "XYZAC" | "XYZBC"; disabled: boolean; onApply: (machine: ImportedMachine) => Promise<void> };
|
||||
@@ -68,7 +68,9 @@ function ManagerPreview({ coordinates, parts, selectedId, selectedNodePath, mode
|
||||
const node = new THREE.Group(); node.name = `draft_${part.id}`; node.visible = part.visible; node.position.fromArray(part.translationMm).multiplyScalar(.001); node.rotation.set(...part.rotationDeg.map(THREE.MathUtils.degToRad) as Vec3);
|
||||
// Clone transforms/hierarchy only. Geometry, materials, textures and
|
||||
// source-node transforms remain untouched on the imported GLB.
|
||||
const visual = part.scene.clone(true), pathNodes = new Map<string, THREE.Object3D>(); const indexNodes = (object: THREE.Object3D, path: string) => { pathNodes.set(path, object); object.children.forEach((child, index) => indexNodes(child, `${path}/${index}`)); }; indexNodes(visual, "scene"); const internalRoles = Object.entries(part.nodeRoles ?? {}).map(([path, role]) => ({ object: pathNodes.get(path) ?? null, role })); applyMachineNodeParents(visual, part.nodeParents); visual.position.fromArray(part.originMm).multiplyScalar(-.001); node.add(visual); visualNodes.set(part.id, pathNodes);
|
||||
const { scene: visual, nodes: pathNodes } = configureMachinePartScene(part.scene.clone(true), part);
|
||||
const internalRoles = Object.entries(part.nodeRoles ?? {}).map(([path, role]) => ({ object: pathNodes.get(path) ?? null, role }));
|
||||
visual.position.add(new THREE.Vector3(...part.originMm).multiplyScalar(-.001)); node.add(visual); visualNodes.set(part.id, pathNodes);
|
||||
const axes = new THREE.AxesHelper(.12); helpers.push(axes); axes.name = `origin_${part.id}`; axes.visible = part.showOrigin; node.add(axes); root.add(node); nodes.set(part.id, node);
|
||||
if (part.role) roleNodes.set(part.role, { object: node, position: node.position.clone(), quaternion: node.quaternion.clone() });
|
||||
for (const binding of internalRoles) if (binding.object && binding.role) roleNodes.set(binding.role, { object: binding.object, position: binding.object.position.clone(), quaternion: binding.object.quaternion.clone() });
|
||||
@@ -83,23 +85,15 @@ 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); }
|
||||
const translate = (role: MachinePartRole, axis: "x" | "y" | "z", value: number) => { const binding = roleNodes.get(role); if (binding) { const direction = new THREE.Vector3(axis === "x" ? 1 : 0, axis === "y" ? 1 : 0, axis === "z" ? 1 : 0).applyQuaternion(binding.quaternion); binding.object.position.addScaledVector(direction, value / 1000); } };
|
||||
const rotate = (role: MachinePartRole, axis: THREE.Vector3, degrees: number) => { const binding = roleNodes.get(role); if (binding) binding.object.quaternion.copy(binding.quaternion).multiply(new THREE.Quaternion().setFromAxisAngle(axis, THREE.MathUtils.degToRad(degrees))); };
|
||||
translate("x", "x", nextPose.x); translate("y", "y", nextPose.y); translate("z", "z", nextPose.z); rotate("tilt", nextCoordinates === "XYZBC" ? new THREE.Vector3(0, 1, 0) : new THREE.Vector3(1, 0, 0), nextCoordinates === "XYZBC" ? nextPose.b : nextPose.a); rotate("c", new THREE.Vector3(0, 0, 1), nextPose.c);
|
||||
root.updateMatrixWorld(true); let objectCount = 0, meshCount = 0; root.traverse((object) => { let cursor: THREE.Object3D | null = object, isJointHelper = false; while (cursor) { if (cursor === selectedJointAxes) { isJointHelper = true; break; } cursor = cursor.parent; } if (!isJointHelper) { objectCount += 1; if ((object as THREE.Mesh).isMesh) meshCount += 1; } }); const transforms = Object.fromEntries([...roleNodes].map(([role, binding]) => [role, { local: binding.object.position.toArray(), localQuaternion: binding.object.quaternion.toArray(), world: binding.object.getWorldPosition(new THREE.Vector3()).toArray(), worldQuaternion: binding.object.getWorldQuaternion(new THREE.Quaternion()).toArray() }])); element.dataset.machineManagerSceneObjects = String(objectCount); element.dataset.machineManagerSceneMeshes = String(meshCount); element.dataset.machineManagerRoleTransforms = JSON.stringify(transforms); if (selectedJointObject) { element.dataset.machineManagerSelectedJointWorldOrigin = JSON.stringify(selectedJointObject.getWorldPosition(new THREE.Vector3()).toArray()); element.dataset.machineManagerSelectedJointWorldQuaternion = JSON.stringify(selectedJointObject.getWorldQuaternion(new THREE.Quaternion()).toArray()); element.dataset.machineManagerSelectedJointAxesScale = selectedAxesScale.toFixed(6); } onMotionStateRef.current(transforms); element.dataset.machineManagerTestPose = JSON.stringify(nextPose); element.dataset.machineManagerContextGeneration = "1"; dirty = true; draw();
|
||||
root.updateMatrixWorld(true); let objectCount = 0, meshCount = 0; root.traverse((object) => { let cursor: THREE.Object3D | null = object, isJointHelper = false; while (cursor) { if (cursor === selectedJointAxes) { isJointHelper = true; break; } cursor = cursor.parent; } if (!isJointHelper) { objectCount += 1; if ((object as THREE.Mesh).isMesh) meshCount += 1; } }); const transforms = Object.fromEntries([...roleNodes].map(([role, binding]) => [role, { local: binding.object.position.toArray(), localQuaternion: binding.object.quaternion.toArray(), world: binding.object.getWorldPosition(new THREE.Vector3()).toArray(), worldQuaternion: binding.object.getWorldQuaternion(new THREE.Quaternion()).toArray() }])); element.dataset.machineManagerSceneObjects = String(objectCount); element.dataset.machineManagerSceneMeshes = String(meshCount); element.dataset.machineManagerRoleTransforms = JSON.stringify(transforms); if (selectedJointObject) { element.dataset.machineManagerSelectedJointWorldOrigin = JSON.stringify(selectedJointObject.getWorldPosition(new THREE.Vector3()).toArray()); element.dataset.machineManagerSelectedJointWorldQuaternion = JSON.stringify(selectedJointObject.getWorldQuaternion(new THREE.Quaternion()).toArray()); element.dataset.machineManagerSelectedJointAxesScale = selectedAxesScale.toFixed(6); } const selectedState = selectedJointObject ? { local: selectedJointObject.position.toArray(), localQuaternion: selectedJointObject.quaternion.toArray(), world: selectedJointObject.getWorldPosition(new THREE.Vector3()).toArray(), worldQuaternion: selectedJointObject.getWorldQuaternion(new THREE.Quaternion()).toArray() } : null;
|
||||
element.dataset.machineManagerSelectedTransform = JSON.stringify(selectedState);
|
||||
onMotionStateRef.current(selectedState ? { ...transforms, selected: selectedState } : transforms); element.dataset.machineManagerTestPose = JSON.stringify(nextPose); element.dataset.machineManagerContextGeneration = "1"; dirty = true; draw();
|
||||
};
|
||||
element.machineManagerSync(stateRef.current.parts, stateRef.current.selectedId, stateRef.current.selectedNodePath, stateRef.current.mode, stateRef.current.testPose, stateRef.current.coordinates);
|
||||
const resize = () => {
|
||||
@@ -149,6 +143,15 @@ export function MachineManagerPanel({ coordinates, disabled, onApply }: Props) {
|
||||
const selectedTreeNode = selected ? findTreeNode(trees.get(selected.id), selectedNodePath) : null;
|
||||
const update = (id: string, values: Partial<MachinePartDraft>) => setParts((current) => current.map((part) => part.id === id ? { ...part, ...values } : part));
|
||||
const vectorInput = (label: string, value: Vec3, set: (value: Vec3) => void, unit: string, control?: string) => <fieldset className="machine-vector" data-axis-control={control}><legend>{label} ({unit})</legend>{["X", "Y", "Z"].map((axis, index) => <label key={axis}>{axis}<input type="number" step="any" value={value[index]} onChange={(event) => { const next = [...value] as Vec3; next[index] = number(event.target.value); set(next); }} /></label>)}</fieldset>;
|
||||
const updateSelectedTransform = (field: "translationMm" | "rotationDeg", value: Vec3) => {
|
||||
if (!selected || !selectedTreeNode) return;
|
||||
if (selectedNodePath === "scene") { update(selected.id, { [field]: value }); return; }
|
||||
setParts((current) => current.map((part) => {
|
||||
if (part.id !== selected.id) return part;
|
||||
const currentTransform = part.nodeTransforms?.[selectedNodePath] ?? { translationMm: selectedTreeNode!.originMm, rotationDeg: selectedTreeNode!.rotationDeg };
|
||||
return { ...part, nodeTransforms: { ...(part.nodeTransforms ?? {}), [selectedNodePath]: { ...currentTransform, [field]: value } } };
|
||||
}));
|
||||
};
|
||||
const importFiles = async (list: FileList | File[] | null) => {
|
||||
if (!list?.length) return; setPending(true); setMessage("正在读取 GLB 部件…");
|
||||
try {
|
||||
@@ -159,7 +162,7 @@ export function MachineManagerPanel({ coordinates, disabled, onApply }: Props) {
|
||||
const role = MACHINE_PART_ROLES.find((candidate) => !next.some((part) => part.role === candidate)) ?? "";
|
||||
const parentRole = role ? expectedParentRole(role) : null;
|
||||
const parentId = parentRole ? next.find((part) => part.role === parentRole)?.id ?? null : null;
|
||||
next.push({ ...source, role, parentId, visible: true, showOrigin: true, originMm: zero3(), translationMm: zero3(), rotationDeg: zero3(), nodeParents: {}, nodeRoles: {} });
|
||||
next.push({ ...source, role, parentId, visible: true, showOrigin: true, originMm: zero3(), translationMm: zero3(), rotationDeg: zero3(), nodeParents: {}, nodeRoles: {}, nodeTransforms: {} });
|
||||
}
|
||||
return next;
|
||||
});
|
||||
@@ -179,8 +182,11 @@ export function MachineManagerPanel({ coordinates, disabled, onApply }: Props) {
|
||||
}
|
||||
if (source.partId !== target.partId) { setMessage("GLB 内部节点只能在同一 GLB 层级树内拖动"); return; }
|
||||
const part = parts.find((item) => item.id === source.partId)!;
|
||||
const candidate = { ...part, nodeParents: { ...part.nodeParents, [source.path]: target.path === "scene" ? null : target.path } };
|
||||
try { machinePartTree(candidate); update(part.id, { nodeParents: candidate.nodeParents }); setSelectedId(part.id); setSelectedNodePath(source.path); setMessage("GLB 内部层级已更新;原始导入模型未修改"); }
|
||||
try {
|
||||
const changes = reparentMachineNode(part, source.path, target.path);
|
||||
machinePartTree({ ...part, ...changes }); update(part.id, changes);
|
||||
setSelectedId(part.id); setSelectedNodePath(source.path); setMessage("GLB 内部层级已更新,保留当前装配姿态");
|
||||
}
|
||||
catch (error) { setMessage(error instanceof Error ? error.message : String(error)); }
|
||||
};
|
||||
const apply = async () => {
|
||||
@@ -219,7 +225,8 @@ export function MachineManagerPanel({ coordinates, disabled, onApply }: Props) {
|
||||
};
|
||||
const poseInput = (axis: keyof TestPose, unit: string) => <label key={axis}>{axis.toUpperCase()}<input data-axis-control={`machine-test-${axis}`} type="number" step="any" value={testPose[axis]} onChange={(event) => setTestPose({ ...testPose, [axis]: number(event.target.value) })} /><small>{unit}</small></label>;
|
||||
const receiveMotionState = (state: Record<string, MotionTransform>) => { const hash = JSON.stringify(state); if (hash !== motionStateHash.current) { motionStateHash.current = hash; setMotionState(state); } };
|
||||
const selectedDriven = selectedBinding ? motionState[selectedBinding] : null;
|
||||
const selectedDriven = motionState.selected ?? null;
|
||||
const selectedTransform = selectedNodePath === "scene" ? selected : selected?.nodeTransforms?.[selectedNodePath] ?? (selectedTreeNode ? { translationMm: selectedTreeNode.originMm, rotationDeg: selectedTreeNode.rotationDeg } : null);
|
||||
const renderPartTree = (part: MachinePartDraft): React.ReactNode => <TreeNode key={part.id} part={part} node={trees.get(part.id)!} selected={`${selectedId}:${selectedNodePath}`} onSelect={selectTreeNode} onDrop={reparentTreeNode} extra={parts.filter((child) => child.parentId === part.id).map(renderPartTree)} />;
|
||||
return <section className="machine-manager" aria-label="三维机床管理">
|
||||
<button type="button" data-axis-control="machine-manager-settings" aria-haspopup="dialog" onClick={() => setOpen(true)}>三维机床管理</button>
|
||||
@@ -229,7 +236,8 @@ export function MachineManagerPanel({ coordinates, disabled, onApply }: Props) {
|
||||
{open && <MachineManagerPanelPreview coordinates={coordinates} parts={parts} selectedId={selectedId} selectedNodePath={selectedNodePath} mode={mode} testPose={testPose} onTransform={(id, translationMm, rotationDeg) => update(id, { translationMm, rotationDeg })} onMotionState={receiveMotionState} />}
|
||||
<div className="machine-manager-editor">
|
||||
<section className="machine-part-list"><h3>部件与层级</h3>{parts.length ? <ul className="machine-tree" role="tree">{parts.filter((part) => !part.parentId).map(renderPartTree)}</ul> : <p>导入机床或部件后显示 GLB 内部层级。</p>}</section>
|
||||
{selected && <section className="machine-part-editor"><header><h3>{selected.filename}</h3><button type="button" data-axis-control="machine-manager-delete" onClick={() => remove(selected)}>删除导入模型</button></header>{selectedTreeNode && <div className="machine-tree-origin" data-axis-control="machine-tree-origin"><strong>{selectedTreeNode.name}</strong><span>节点自身局部原点</span><output>X 0.000 Y 0.000 Z 0.000</output><span>关节零姿态原点(相对父节点,mm)</span><output>X {selectedTreeNode.originMm[0].toFixed(3)} Y {selectedTreeNode.originMm[1].toFixed(3)} Z {selectedTreeNode.originMm[2].toFixed(3)}</output><span>零姿态世界原点(mm)</span><output>X {selectedTreeNode.worldOriginMm[0].toFixed(3)} Y {selectedTreeNode.worldOriginMm[1].toFixed(3)} Z {selectedTreeNode.worldOriginMm[2].toFixed(3)}</output><span>零姿态局部转角(deg)</span><output>RX {selectedTreeNode.rotationDeg[0].toFixed(3)} RY {selectedTreeNode.rotationDeg[1].toFixed(3)} RZ {selectedTreeNode.rotationDeg[2].toFixed(3)}</output>{selectedDriven && <><span>当前驱动后的世界原点(mm)</span><output>X {(selectedDriven.world[0] * 1000).toFixed(3)} Y {(selectedDriven.world[1] * 1000).toFixed(3)} Z {(selectedDriven.world[2] * 1000).toFixed(3)}</output></>}</div>}<label>轴绑定<select data-axis-control="machine-tree-axis-role" value={selectedBinding} onChange={(event) => assignTreeRole(event.target.value as MachinePartRole | "")}><option value="">未绑定</option>{MACHINE_PART_ROLES.map((role) => <option key={role} value={role}>{roleLabel(role, coordinates)}</option>)}</select></label><label>机床角色<select data-axis-control="machine-manager-role" value={selected.role} onChange={(event) => roleChange(selected, event.target.value as MachinePartRole | "")}><option value="">未分配</option>{MACHINE_PART_ROLES.map((role) => <option key={role} value={role}>{roleLabel(role, coordinates)}</option>)}</select></label><label>父部件<select data-axis-control="machine-manager-parent" value={selected.parentId ?? ""} onChange={(event) => update(selected.id, { parentId: event.target.value || null })}><option value="">无</option>{parts.filter((part) => part.id !== selected.id).map((part) => <option key={part.id} value={part.id}>{part.role ? roleLabel(part.role, coordinates) : part.filename}</option>)}</select></label><div className="machine-part-flags"><label><input type="checkbox" checked={selected.visible} onChange={(event) => update(selected.id, { visible: event.target.checked })} />显示模型</label><label><input data-axis-control="machine-manager-show-origin" type="checkbox" checked={selected.showOrigin} onChange={(event) => update(selected.id, { showOrigin: event.target.checked })} />显示坐标原点</label></div>{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")}</section>}
|
||||
{selected && <section className="machine-part-editor"><header><h3>{selected.filename}</h3><button type="button" data-axis-control="machine-manager-delete" onClick={() => remove(selected)}>删除导入模型</button></header>{selectedTreeNode && <div className="machine-tree-origin" data-axis-control="machine-tree-origin"><strong>{selectedTreeNode.name}</strong><span>节点自身局部原点</span><output>X 0.000 Y 0.000 Z 0.000</output><span>装配位置(相对父节点,mm)</span><output>X {selectedTransform!.translationMm[0].toFixed(3)} Y {selectedTransform!.translationMm[1].toFixed(3)} Z {selectedTransform!.translationMm[2].toFixed(3)}</output><span>装配姿态(相对父节点,deg)</span><output>RX {selectedTransform!.rotationDeg[0].toFixed(3)} RY {selectedTransform!.rotationDeg[1].toFixed(3)} RZ {selectedTransform!.rotationDeg[2].toFixed(3)}</output>{selectedDriven && <><span>当前世界原点(mm)</span><output>X {(selectedDriven.world[0] * 1000).toFixed(3)} Y {(selectedDriven.world[1] * 1000).toFixed(3)} Z {(selectedDriven.world[2] * 1000).toFixed(3)}</output></>}</div>}<label>轴绑定<select data-axis-control="machine-tree-axis-role" value={selectedBinding} onChange={(event) => assignTreeRole(event.target.value as MachinePartRole | "")}><option value="">未绑定</option>{MACHINE_PART_ROLES.map((role) => <option key={role} value={role}>{roleLabel(role, coordinates)}</option>)}</select></label><label>机床角色<select data-axis-control="machine-manager-role" value={selected.role} onChange={(event) => roleChange(selected, event.target.value as MachinePartRole | "")}><option value="">未分配</option>{MACHINE_PART_ROLES.map((role) => <option key={role} value={role}>{roleLabel(role, coordinates)}</option>)}</select></label><label>父部件<select data-axis-control="machine-manager-parent" value={selected.parentId ?? ""} onChange={(event) => update(selected.id, { parentId: event.target.value || null })}><option value="">无</option>{parts.filter((part) => part.id !== selected.id).map((part) => <option key={part.id} value={part.id}>{part.role ? roleLabel(part.role, coordinates) : part.filename}</option>)}</select></label><div className="machine-part-flags"><label><input type="checkbox" checked={selected.visible} onChange={(event) => update(selected.id, { visible: event.target.checked })} />显示模型</label><label><input data-axis-control="machine-manager-show-origin" type="checkbox" checked={selected.showOrigin} onChange={(event) => update(selected.id, { showOrigin: event.target.checked })} />显示坐标原点</label></div>{selectedNodePath === "scene" && vectorInput("导入文件装配原点", selected.originMm, (originMm) => update(selected.id, { originMm }), "mm", "machine-manager-origin")}
|
||||
{selectedTransform && <><p className="machine-transform-reference">编辑对象:{selectedTreeNode?.name}。数值为相对父级的位置和 XYZ 欧拉角;旋转中心为本节点原点。</p>{vectorInput("部件平移(相对父级位置)", selectedTransform.translationMm, (value) => updateSelectedTransform("translationMm", value), "mm", "machine-manager-translation")}{vectorInput("部件旋转(相对父级姿态)", selectedTransform.rotationDeg, (value) => updateSelectedTransform("rotationDeg", value), "deg", "machine-manager-rotation")}</>}</section>}
|
||||
</div>
|
||||
</div>
|
||||
<section className="machine-motion-test"><header><h3>层级运动测试</h3><button type="button" data-axis-control="machine-test-reset" onClick={() => setTestPose({ x: 0, y: 0, z: 0, a: 0, b: 0, c: 0 })}>复位</button></header><div>{(["x", "y", "z"] as const).map((axis) => poseInput(axis, "mm"))}{poseInput(coordinates === "XYZBC" ? "b" : "a", "deg")}{poseInput("c", "deg")}</div><output data-axis-control="machine-test-state">{JSON.stringify(testPose)}</output></section>
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { ImportedMachine } from "./imported-machine";
|
||||
|
||||
export type MachinePartRole = "base" | "x" | "y" | "z" | "tool" | "tilt" | "c" | "work";
|
||||
export type MachinePartSource = { id: string; filename: string; hash: string; scene: THREE.Group };
|
||||
export type MachineNodeTransform = { translationMm: Vec3; rotationDeg: Vec3 };
|
||||
export type MachinePartDraft = MachinePartSource & {
|
||||
role: MachinePartRole | "";
|
||||
parentId: string | null;
|
||||
@@ -15,6 +16,7 @@ export type MachinePartDraft = MachinePartSource & {
|
||||
rotationDeg: Vec3;
|
||||
nodeParents: Record<string, string | null>;
|
||||
nodeRoles: Record<string, MachinePartRole | "">;
|
||||
nodeTransforms: Record<string, MachineNodeTransform>;
|
||||
};
|
||||
export type MachineTreeNode = { path: string; name: string; type: "group" | "mesh"; originMm: Vec3; worldOriginMm: Vec3; rotationDeg: Vec3; children: MachineTreeNode[] };
|
||||
export type PhysicalZero = { x: number; y: number; z: number; a: number; b: number; c: number };
|
||||
@@ -24,7 +26,7 @@ export type ManagedMachineGeometry = {
|
||||
cOriginMm: Vec3;
|
||||
tiltOffsetMm: Vec3;
|
||||
physicalZero: PhysicalZero;
|
||||
parts: Array<{ id: string; filename: string; hash: string; role: MachinePartRole; parentId: string | null; originMm: Vec3; translationMm: Vec3; rotationDeg: Vec3; visible: boolean; nodeParents: Record<string, string | null>; nodeRoles: Record<string, MachinePartRole | ""> }>;
|
||||
parts: Array<{ id: string; filename: string; hash: string; role: MachinePartRole; parentId: string | null; originMm: Vec3; translationMm: Vec3; rotationDeg: Vec3; visible: boolean; nodeParents: Record<string, string | null>; nodeRoles: Record<string, MachinePartRole | "">; nodeTransforms: Record<string, MachineNodeTransform> }>;
|
||||
};
|
||||
|
||||
export const MACHINE_PART_ROLES: MachinePartRole[] = ["base", "x", "y", "z", "tool", "tilt", "c", "work"];
|
||||
@@ -68,12 +70,46 @@ export function applyMachineNodeParents(scene: THREE.Object3D, overrides: Record
|
||||
return scene;
|
||||
}
|
||||
|
||||
// Index source paths before reparenting so edits and axis bindings keep their identity.
|
||||
export function configureMachinePartScene(scene: THREE.Object3D, part: Pick<MachinePartDraft, "nodeParents" | "nodeTransforms">) {
|
||||
const { nodes, paths } = scenePathMap(scene);
|
||||
applyMachineNodeParents(scene, part.nodeParents ?? {});
|
||||
for (const [path, transform] of Object.entries(part.nodeTransforms ?? {})) {
|
||||
const node = nodes.get(path);
|
||||
if (!node || node === scene) throw new Error("部件变换节点无效");
|
||||
finite3(transform.translationMm, "节点相对父级位置");
|
||||
finite3(transform.rotationDeg, "节点相对父级姿态");
|
||||
node.position.fromArray(transform.translationMm).multiplyScalar(.001);
|
||||
node.rotation.set(...transform.rotationDeg.map(THREE.MathUtils.degToRad) as Vec3, "XYZ");
|
||||
node.updateMatrix();
|
||||
}
|
||||
scene.updateMatrixWorld(true);
|
||||
return { scene, nodes, paths };
|
||||
}
|
||||
|
||||
export function reparentMachineNode(part: MachinePartDraft, sourcePath: string, targetPath: string) {
|
||||
const { scene, nodes } = configureMachinePartScene(part.scene.clone(true), part);
|
||||
const source = nodes.get(sourcePath), target = nodes.get(targetPath);
|
||||
if (!source || source === scene || !target || source === target) throw new Error("GLB 层级拖放目标无效");
|
||||
for (let cursor: THREE.Object3D | null = target; cursor; cursor = cursor.parent) {
|
||||
if (cursor === source) throw new Error("GLB 层级不能形成循环");
|
||||
}
|
||||
target.attach(source);
|
||||
// Capture edited local poses after attach: changing parent preserves the current
|
||||
// assembly pose, and subsequent numeric edits use the new parent's frame.
|
||||
const nodeTransforms = Object.fromEntries([...nodes].filter(([path]) => path !== "scene").map(([path, node]) => [path, {
|
||||
translationMm: node.position.toArray().map((value) => value * 1000) as Vec3,
|
||||
rotationDeg: new THREE.Euler().setFromQuaternion(node.quaternion, "XYZ").toArray().slice(0, 3).map((value) => THREE.MathUtils.radToDeg(Number(value))) as Vec3,
|
||||
}]));
|
||||
return { nodeParents: { ...part.nodeParents, [sourcePath]: targetPath === "scene" ? null : targetPath }, nodeTransforms };
|
||||
}
|
||||
|
||||
export function machineSceneNode(scene: THREE.Object3D, path: string) {
|
||||
return scenePathMap(scene).nodes.get(path) ?? null;
|
||||
}
|
||||
|
||||
export function machinePartTree(part: Pick<MachinePartDraft, "filename" | "scene" | "nodeParents">): MachineTreeNode {
|
||||
const scene = part.scene.clone(true), { paths } = scenePathMap(scene); applyMachineNodeParents(scene, part.nodeParents ?? {});
|
||||
export function machinePartTree(part: Pick<MachinePartDraft, "filename" | "scene" | "nodeParents" | "nodeTransforms">): MachineTreeNode {
|
||||
const { scene, paths } = configureMachinePartScene(part.scene.clone(true), part);
|
||||
scene.updateMatrixWorld(true);
|
||||
const build = (node: THREE.Object3D): MachineTreeNode => ({
|
||||
path: paths.get(node)!,
|
||||
@@ -106,6 +142,7 @@ export function validateMachineAssembly(coordinates: "XYZAC" | "XYZBC", parts: M
|
||||
if (!part.role) throw new Error(`${part.filename} 尚未指定机床角色`);
|
||||
if (byRole.has(part.role)) throw new Error(`${roleLabel(part.role, coordinates)}角色重复`);
|
||||
finite3(part.originMm, `${part.filename}局部原点`); finite3(part.translationMm, `${part.filename}平移`); finite3(part.rotationDeg, `${part.filename}旋转`);
|
||||
for (const [path, transform] of Object.entries(part.nodeTransforms ?? {})) { if (path === "scene") throw new Error(`${part.filename} 的 scene 节点不能重复定义部件变换`); finite3(transform.translationMm, `${part.filename}/${path} 相对父级位置`); finite3(transform.rotationDeg, `${part.filename}/${path} 相对父级姿态`); }
|
||||
byRole.set(part.role, part);
|
||||
}
|
||||
for (const role of MACHINE_PART_ROLES) if (!byRole.has(role)) throw new Error(`缺少${roleLabel(role, coordinates)}`);
|
||||
@@ -149,7 +186,7 @@ export async function buildManagedMachine(input: { coordinates: "XYZAC" | "XYZBC
|
||||
const structure: MachineStructure = parseMachineUrdf(urdf);
|
||||
const metadata: ManagedMachineGeometry = {
|
||||
schema: "linuxcnc.machine-manager/1", coordinates: input.coordinates, cOriginMm: [...input.cOriginMm], tiltOffsetMm: [...input.tiltOffsetMm], physicalZero: { ...input.physicalZero },
|
||||
parts: MACHINE_PART_ROLES.map((role) => { const part = byRole.get(role)!; return { id: part.id, filename: part.filename, hash: part.hash, role, parentId: part.parentId, originMm: [...part.originMm], translationMm: [...part.translationMm], rotationDeg: [...part.rotationDeg], visible: part.visible, nodeParents: { ...(part.nodeParents ?? {}) }, nodeRoles: { ...(part.nodeRoles ?? {}) } }; }),
|
||||
parts: MACHINE_PART_ROLES.map((role) => { const part = byRole.get(role)!; return { id: part.id, filename: part.filename, hash: part.hash, role, parentId: part.parentId, originMm: [...part.originMm], translationMm: [...part.translationMm], rotationDeg: [...part.rotationDeg], visible: part.visible, nodeParents: { ...(part.nodeParents ?? {}) }, nodeRoles: { ...(part.nodeRoles ?? {}) }, nodeTransforms: Object.fromEntries(Object.entries(part.nodeTransforms ?? {}).map(([path, transform]) => [path, { translationMm: [...transform.translationMm] as Vec3, rotationDeg: [...transform.rotationDeg] as Vec3 }])) }; }),
|
||||
};
|
||||
const scene = new THREE.Group(); scene.name = `Managed_${input.coordinates}`;
|
||||
scene.userData.linuxcncMachine = { schema: "linuxcnc.machine/1", urdf };
|
||||
@@ -164,7 +201,7 @@ export async function buildManagedMachine(input: { coordinates: "XYZAC" | "XYZBC
|
||||
placement.position.fromArray(part.translationMm).multiplyScalar(.001);
|
||||
placement.rotation.set(...part.rotationDeg.map(THREE.MathUtils.degToRad) as Vec3);
|
||||
placement.userData.machinePart = { role, filename: part.filename, hash: part.hash, originMm: [...part.originMm] };
|
||||
const visual = applyMachineNodeParents(cloneMachinePartScene(part.scene), part.nodeParents ?? {}); visual.position.fromArray(part.originMm).multiplyScalar(-.001); placement.add(visual); links[role].add(placement);
|
||||
const { scene: visual } = configureMachinePartScene(cloneMachinePartScene(part.scene), part); visual.position.add(new THREE.Vector3(...part.originMm).multiplyScalar(-.001)); placement.add(visual); links[role].add(placement);
|
||||
}
|
||||
links.base.userData.linuxcncMachineGeometry = metadata;
|
||||
const signature = JSON.stringify({ metadata, hashes: MACHINE_PART_ROLES.map((role) => byRole.get(role)!.hash) });
|
||||
|
||||
@@ -391,6 +391,7 @@ output { font: 12px ui-monospace, monospace; text-align: right; }
|
||||
.machine-part-editor > header, .machine-motion-test > header { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
||||
.machine-part-editor > header button { color: #8b2d24; }
|
||||
.machine-part-editor select { height: 30px; min-width: 0; }
|
||||
.machine-transform-reference { margin: 8px 0; color: #536467; font-size: 11px; line-height: 1.45; }
|
||||
.machine-part-flags { display: flex; gap: 16px; margin: 8px 0; }
|
||||
.machine-vector { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0; padding: 6px; }
|
||||
.machine-vector legend { font-weight: 700; }
|
||||
|
||||
Reference in New Issue
Block a user