Compare commits
2 Commits
1076bfa5e0
...
8cafbb326b
| Author | SHA1 | Date | |
|---|---|---|---|
| 8cafbb326b | |||
| 6d04425121 |
23
coverage/glb-machine-manager/mill700/browser.report.json
Normal file
23
coverage/glb-machine-manager/mill700/browser.report.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"schema": "machine-manager-mill700-browser.v1",
|
||||
"report_id": "GLB-MGR-E05",
|
||||
"status": "verified",
|
||||
"model": {
|
||||
"path": "/home/mes006/linuxcncWebWASM/MILL700-U_1.glb",
|
||||
"bytes": 9418852,
|
||||
"sha256": "bf37130681cc6462412e52744519516e8ade2a3212dd69153c0b1b3e415bcf66"
|
||||
},
|
||||
"checks": [
|
||||
"existing 132-node, depth-6 hierarchy preserved",
|
||||
"metallic MILL700 surfaces rendered with studio PMREM lighting; screenshot captured for visual review"
|
||||
],
|
||||
"hierarchy": {
|
||||
"root": "MILL700-U_1",
|
||||
"nodes": 132,
|
||||
"meshes": 126,
|
||||
"maxDepth": 6,
|
||||
"materials": 126,
|
||||
"metallic": 110
|
||||
},
|
||||
"browser_errors": []
|
||||
}
|
||||
BIN
coverage/glb-machine-manager/mill700/failure.png
Normal file
BIN
coverage/glb-machine-manager/mill700/failure.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 304 KiB |
BIN
coverage/glb-machine-manager/mill700/mill700-studio-lighting.png
Normal file
BIN
coverage/glb-machine-manager/mill700/mill700-studio-lighting.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 131 KiB |
@@ -13,7 +13,7 @@
|
||||
- 转台链:底座 → A/B → C → 工件。
|
||||
4. 每个部件可定义局部坐标原点。模型网格相对该原点偏移,运动关节围绕该原点运动。
|
||||
5. 每个部件可独立显示或隐藏原点坐标轴辅助线。
|
||||
6. 每个部件可用数值和 Three.js 操纵器平移、旋转;变换只修改装配配置,不修改原 GLB 字节。
|
||||
6. 每个部件可用数值和 Three.js 操纵器平移、旋转;变换只修改装配配置,不修改原 GLB 字节。Three.js 管理器使用机床右手坐标:机床 Z 轴朝上,机床 XY 为地面平面;GLB Y-up 坐标映射为 `Three(x,y,z) = Machine(x,z,-y)`。编辑器坐标轴、网格、平移、旋转和原点辅助统一使用此映射。
|
||||
7. 可定义 C 轴原点位置和 A/B 到 C 的运动学偏置。应用时写入底座场景节点及整机场景的 `userData.linuxcncMachineGeometry`,同时生成 URDF:
|
||||
- `C 原点 P = (x-rot-point, y-rot-point, z-rot-point)`。
|
||||
- `A/B 偏置 D = (x-offset, y-offset, z-offset)`。
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
# 推进台账
|
||||
|
||||
## R03 / 2026-09-20 / 管理器 GLB 预览亮度修复
|
||||
|
||||
- 用户反馈三维机床管理器导入 GLB 后模型太黑。
|
||||
- 根因:管理器使用 MeshStandard/Physical PBR 材质,但独立编辑场景没有环境贴图、输出色彩空间和色调映射,高金属度部件几乎只能反射黑色背景。
|
||||
- 修复:增加 RoomEnvironment → PMREM 工作室环境、sRGB 输出、ACES Filmic tone mapping、1.45 专用曝光,并增加半球光、环境光、暖主光、冷补光和轮廓光;背景和网格提升为中性工作区灰色。
|
||||
- 原 GLB 的 color、metalness、roughness 和纹理不改写;运行模型材质也不受影响。
|
||||
- 使用仓库根目录 `/home/mes006/linuxcncWebWASM/MILL700-U_1.glb` 实测:9,418,852 bytes,SHA-256 `bf37130681cc6462412e52744519516e8ade2a3212dd69153c0b1b3e415bcf66`;132 节点、126 网格、最大层级深度 6,110 个高金属度材质。
|
||||
- E05 浏览器验收通过,截图显示原有 MILL700 层级模型在管理器中可辨识;报告和截图位于 `coverage/glb-machine-manager/mill700/`。
|
||||
- 当前任务:MGR-10 已验证,等待保存和发布。
|
||||
|
||||
## R04 / 2026-09-20 / Z-up 机床坐标映射
|
||||
|
||||
- 用户要求将 Three.js 管理器坐标调整为 Z 轴朝上的机床坐标系。
|
||||
- 固定映射:`Machine(x,y,z) -> Three(x,z,-y)`,保持右手系;机床 XY 作为默认地面网格,机床 Z 对应 Three.js 竖直方向。
|
||||
- 管理器部件平移、局部原点、组合运行场景、原点辅助和 TransformControls 统一使用该映射;TransformControls 使用机床坐标代理,拖动结果反算为机床坐标。
|
||||
- MILL700 重新验收通过,蓝色 Z 轴在截图中竖直,浏览器错误为 0。MGR-11 verified。
|
||||
|
||||
## R02 / 2026-09-20 / 部件装配、标定、应用与验收完成
|
||||
|
||||
- 新增 `machine-manager.ts`:通用自包含 GLB 部件加载、SHA-256、深复制、八角色和两条机械链校验、AC/BC URDF 生成、组合场景、底座/整机 userData、组合哈希。
|
||||
|
||||
@@ -11,3 +11,5 @@
|
||||
|MGR-07 控制面板接入|verified|MGR-03,MGR-06|“三维机床管理”入口;应用复用 LOAD_WORKSPACE/PROGRAM_LOAD,失败不改旧模型|
|
||||
|MGR-08 坐标一致性|verified|MGR-07|AC/BC 的 X/Y/Z/A或B/C、刀长、工件和路径由 WASM 状态正确驱动|
|
||||
|MGR-09 浏览器验收收口|verified|MGR-04,MGR-05,MGR-08|交互/错误/构建/浏览器截图报告通过,03–05 完整更新|
|
||||
|MGR-10 PBR 预览照明|verified|MGR-04|高金属度 GLB 在管理器中具有可辨识表面;PMREM、色彩空间、曝光和多向补光生效|
|
||||
|MGR-11 Z-up 机床坐标|verified|MGR-04,MGR-05|机床 Z 竖直、XY 地面、原点辅助与 TransformControls 统一使用 `Three(x,y,z)=Machine(x,z,-y)`|
|
||||
|
||||
@@ -37,3 +37,19 @@
|
||||
- `node scripts/test_glb_machine_motion.mjs`:verified;报告 `coverage/glb-machine-manager/regression-motion/motion.report.json`,SHA-256 `91c3fa8ed8037aac8e71b8c76d54c38db9dc011ae25f63553966860b624aa626`。AC/BC 各 41 个同时五轴变化 tick、42 个样本,最大误差分别约 `2.26e-16`、`2.48e-16`。
|
||||
- `git diff --check`:通过。
|
||||
- `scripts/validate_glb_machines.mjs` 未重跑:本地缺少 `gltf-validator` 包;旧示例 GLB 未修改,历史 Khronos 校验报告仍为 verified。该依赖边界不影响新管理器的浏览器导入、导出和运行证据。
|
||||
|
||||
## E05 / R03 / MILL700 PBR 预览照明
|
||||
|
||||
- 状态:verified。
|
||||
- 命令:`PLAYWRIGHT_MODULE=/home/mes006/.npm/_npx/e41f203b7505f1fb/node_modules/playwright/index.mjs node scripts/test_machine_manager_mill700_browser.mjs`
|
||||
- 模型:`MILL700-U_1.glb`,9,418,852 bytes,SHA-256 `bf37130681cc6462412e52744519516e8ade2a3212dd69153c0b1b3e415bcf66`。
|
||||
- report_id:`GLB-MGR-E05`;报告 SHA-256 `bdb39d0a44c2561f806c268c791b7fd03d6e6f2a0dca51d0e1bc9ce3069a4db1`。
|
||||
- 结构:132 节点、126 网格、深度 6、126 材质,其中 110 个 `metalness >= 0.9`。
|
||||
- 检查:已有内部层级完整保留;管理器使用 `studio-pmrem`、sRGB 输出、ACES、曝光 `1.45` 和多向补光;浏览器错误为 0。
|
||||
- 截图:`coverage/glb-machine-manager/mill700/mill700-studio-lighting.png`,SHA-256 `0a20bac14708634ddf8e7267236488c3d28526477669ba16dcb880182e2ce11a`。
|
||||
|
||||
## E06 / R04 / Z-up 坐标
|
||||
|
||||
- 状态:verified。
|
||||
- 检查:管理器预览 DOM 标记 `data-machine-manager-coordinate-system=machine-z-up`、`data-machine-manager-axis-map=X:x,Y:-z,Z:y`;MILL700 截图中蓝色 Z 轴竖直,机床 XY 网格水平。
|
||||
- AC/BC 管理器浏览器闭环、MILL700 浏览器闭环、TypeScript、生产构建和控件契约继续通过。
|
||||
|
||||
@@ -27,3 +27,11 @@ UI 允许用户选择父级以建立层级和发现错误;应用时只接受
|
||||
## D07 / 组合 GLB 只通过显式导出生成
|
||||
|
||||
普通编辑和“应用到 Web LinuxCNC”不触碰用户原文件。只有用户点击“导出组合 GLB”时才用 GLTFExporter 生成新文件。导出包含部件层级、URDF、底座标定 userData 和整机场景标定 userData。
|
||||
|
||||
## D08 / 编辑器照明不修改资产材质
|
||||
|
||||
模型过暗通过 Three.js 场景的 PMREM 环境、色彩管理、曝光和补光解决。不得为了预览可见而覆盖导入材质的颜色、金属度、粗糙度或纹理,否则导出结果会偏离用户资产。
|
||||
|
||||
## D09 / 管理器采用 Z-up 机床坐标
|
||||
|
||||
Three.js/glTF 资产保持 Y-up 原始数据。管理器输入和显示采用机床右手坐标,映射为 `machine (x,y,z) -> Three (x,z,-y)`;这样机床 Z 对应 Three.js 竖直方向,机床 XY 落在默认地面网格,且不破坏右手系。所有部件平移、局部原点和旋转变换共用这套映射。
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
现有整机 GLB + 内嵌 URDF 导入能力继续保留。新管理器生成与现有 `linuxcnc.machine/1` 相同的严格结构,再通过已有配置、Worker、WASM 和 Three.js 投影链运行。
|
||||
|
||||
当前状态:MGR-01 至 MGR-09 全部 verified,功能提交为 `8aab531`。最新台账 R02,浏览器证据 `GLB-MGR-E01/E02`,截图和报告位于 `coverage/glb-machine-manager/`。
|
||||
当前状态:MGR-01 至 MGR-11 全部 verified,MILL700 实测证据为 `GLB-MGR-E05`。最新台账 R04,浏览器证据 `GLB-MGR-E01/E02/E05`,截图和报告位于 `coverage/glb-machine-manager/`。
|
||||
|
||||
文档:
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@ try {
|
||||
await control("machine-manager-settings").click(); assert.equal(await page.locator(".machine-manager-dialog[open]").count(), 1); assert.equal(await page.locator("[data-machine-manager-preview] canvas").count(), 1);
|
||||
await control("machine-manager-files").setInputFiles(fixtures);
|
||||
await page.waitForFunction(() => document.querySelector("[data-machine-manager-preview]")?.dataset.machineManagerPartCount === "8");
|
||||
assert.equal(await page.locator(".machine-part-list > div").count(), 8); assert.equal(await page.locator("[data-machine-manager-preview]").getAttribute("data-machine-manager-origin-count"), "8");
|
||||
report.checks.push("eight GLB parts imported and rendered in the Three.js editor");
|
||||
assert.equal(await page.locator(".machine-part-list > div").count(), 8); assert.equal(await page.locator("[data-machine-manager-preview]").getAttribute("data-machine-manager-origin-count"), "8"); assert.equal(await page.locator("[data-machine-manager-preview]").getAttribute("data-machine-manager-lighting"), "studio-pmrem"); assert.equal(await page.locator("[data-machine-manager-preview]").getAttribute("data-machine-manager-exposure"), "1.45");
|
||||
report.checks.push("eight GLB parts rendered with studio PMREM lighting and calibrated exposure");
|
||||
|
||||
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");
|
||||
@@ -44,7 +44,7 @@ try {
|
||||
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); const downloadPromise = page.waitForEvent("download"); await control("machine-manager-export").click(); const download = await downloadPromise; assert.equal(download.suggestedFilename(), "managed-xyzac.glb");
|
||||
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:-z,Z:y"); const downloadPromise = page.waitForEvent("download"); await control("machine-manager-export").click(); const download = await downloadPromise; assert.equal(download.suggestedFilename(), "managed-xyzac.glb");
|
||||
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 });
|
||||
|
||||
34
scripts/test_machine_manager_mill700_browser.mjs
Normal file
34
scripts/test_machine_manager_mill700_browser.mjs
Normal file
@@ -0,0 +1,34 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { createHash } from "node:crypto";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const modulePath = process.env.PLAYWRIGHT_MODULE;
|
||||
if (!modulePath) { console.log(JSON.stringify({ schema: "machine-manager-mill700-browser.v1", status: "blocked", code: "playwright_unavailable" })); process.exit(0); }
|
||||
const root = new URL("../", import.meta.url);
|
||||
const modelPath = process.env.MILL700_GLB ?? new URL("MILL700-U_1.glb", root).pathname;
|
||||
const model = await readFile(modelPath); const sha256 = createHash("sha256").update(model).digest("hex");
|
||||
let offset = 12, json = null;
|
||||
while (offset < model.length) { const length = model.readUInt32LE(offset), type = model.readUInt32LE(offset + 4); if (type === 0x4e4f534a) json = JSON.parse(model.toString("utf8", offset + 8, offset + 8 + length)); offset += 8 + length; }
|
||||
assert.ok(json); const parents = new Map(); json.nodes.forEach((node, index) => (node.children ?? []).forEach((child) => parents.set(child, index)));
|
||||
const depth = (index) => parents.has(index) ? 1 + depth(parents.get(index)) : 0;
|
||||
const hierarchy = { root: json.nodes[json.scenes[json.scene ?? 0].nodes[0]].name, nodes: json.nodes.length, meshes: json.meshes.length, maxDepth: Math.max(...json.nodes.map((_, index) => depth(index))), materials: json.materials.length, metallic: json.materials.filter((material) => Number(material.pbrMetallicRoughness?.metallicFactor ?? 1) >= .9).length };
|
||||
const output = new URL("../coverage/glb-machine-manager/mill700/", import.meta.url); await mkdir(output, { recursive: true });
|
||||
const imported = await import(pathToFileURL(modulePath).href); const playwright = imported.default ?? imported;
|
||||
const browser = await playwright.chromium.launch({ headless: true, executablePath: process.env.BROWSER_EXECUTABLE_PATH ?? "/usr/bin/google-chrome", args: ["--no-sandbox", "--use-angle=swiftshader", "--enable-unsafe-swiftshader"] });
|
||||
const page = await browser.newPage({ viewport: { width: 1600, height: 1050 } }); const errors = [];
|
||||
page.on("pageerror", (error) => errors.push(`pageerror:${error.message}`)); page.on("console", (message) => { if (message.type() === "error") errors.push(`console:${message.text()}`); });
|
||||
const control = (id) => page.locator(`[data-axis-control="${id}"]`);
|
||||
const report = { schema: "machine-manager-mill700-browser.v1", report_id: "GLB-MGR-E05", status: "running", model: { path: modelPath, bytes: model.length, sha256 }, checks: [] };
|
||||
try {
|
||||
await page.goto(process.env.MACHINE_MANAGER_URL ?? "http://127.0.0.1:5174/", { waitUntil: "domcontentloaded", timeout: 30000 });
|
||||
await page.waitForFunction(() => document.querySelector("main.axis-app")?.dataset.axisProgramLoading === "false" && document.querySelector("main.axis-app")?.dataset.axisStateSource === "linuxcnc-wasm", null, { timeout: 45000 });
|
||||
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(".machine-part-list small")?.textContent === "MILL700-U_1.glb", 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);
|
||||
report.hierarchy = hierarchy; report.checks.push("existing 132-node, depth-6 hierarchy preserved");
|
||||
assert.equal(await page.locator("[data-machine-manager-preview]").getAttribute("data-machine-manager-lighting"), "studio-pmrem"); 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:-z,Z:y");
|
||||
await page.locator(".machine-manager-dialog").screenshot({ path: new URL("mill700-studio-lighting.png", output).pathname }); report.checks.push("metallic MILL700 surfaces rendered with studio PMREM lighting; screenshot captured for visual review"); assert.deepEqual(errors, []); report.status = "verified";
|
||||
} catch (error) { report.status = "failed"; report.error = error instanceof Error ? error.stack : String(error); await page.screenshot({ path: new URL("failure.png", output).pathname, fullPage: true }); throw error; }
|
||||
finally { report.browser_errors = errors; await writeFile(new URL("browser.report.json", output), JSON.stringify(report, null, 2) + "\n"); await browser.close(); }
|
||||
console.log(JSON.stringify(report));
|
||||
@@ -3,9 +3,10 @@ import * as THREE from "three";
|
||||
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
||||
import { TransformControls } from "three/examples/jsm/controls/TransformControls.js";
|
||||
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 { buildManagedMachine, cloneMachinePartScene, expectedParentRole, loadMachinePart, MACHINE_PART_ROLES, roleLabel, type MachinePartDraft, type MachinePartRole, type PhysicalZero } from "./machine-manager";
|
||||
import { buildManagedMachine, cloneMachinePartScene, expectedParentRole, loadMachinePart, machineEulerToThreeQuaternion, machineFrameQuaternion, machineToThree, threeQuaternionToMachineEuler, threeToMachine, MACHINE_PART_ROLES, roleLabel, type MachinePartDraft, type MachinePartRole, type PhysicalZero } from "./machine-manager";
|
||||
import type { Vec3 } from "./machine-urdf";
|
||||
|
||||
type Props = { coordinates: "XYZAC" | "XYZBC"; disabled: boolean; onApply: (machine: ImportedMachine) => Promise<void> };
|
||||
@@ -17,36 +18,47 @@ function ManagerPreview({ parts, selectedId, mode, onTransform }: { parts: Machi
|
||||
const onTransformRef = useRef(onTransform); onTransformRef.current = onTransform;
|
||||
useEffect(() => {
|
||||
const element = host.current; if (!element) return;
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setPixelRatio(Math.min(devicePixelRatio || 1, 2)); renderer.shadowMap.enabled = true; renderer.setSize(Math.max(element.clientWidth, 320), Math.max(element.clientHeight, 360), false); element.appendChild(renderer.domElement);
|
||||
const scene = new THREE.Scene(); scene.background = new THREE.Color(0x10191d); scene.add(new THREE.HemisphereLight(0xffffff, 0x405057, 2));
|
||||
const light = new THREE.DirectionalLight(0xffffff, 3); light.position.set(3, 5, 4); light.castShadow = true; scene.add(light);
|
||||
const grid = new THREE.GridHelper(4, 40, 0x55777b, 0x294347); scene.add(grid);
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
renderer.setPixelRatio(Math.min(devicePixelRatio || 1, 2)); renderer.outputColorSpace = THREE.SRGBColorSpace; renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 1.45; renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFShadowMap; renderer.setSize(Math.max(element.clientWidth, 320), Math.max(element.clientHeight, 360), false); element.appendChild(renderer.domElement);
|
||||
const scene = new THREE.Scene(); scene.background = new THREE.Color(0x263438);
|
||||
const studio = new RoomEnvironment(); const pmrem = new THREE.PMREMGenerator(renderer); const environment = pmrem.fromScene(studio, .04); scene.environment = environment.texture; scene.environmentIntensity = 1.25; studio.dispose(); pmrem.dispose();
|
||||
scene.add(new THREE.HemisphereLight(0xffffff, 0x728083, 2.6)); scene.add(new THREE.AmbientLight(0xffffff, .65));
|
||||
const light = new THREE.DirectionalLight(0xfff5e5, 4.2); light.position.set(3, 5, 4); light.castShadow = true; scene.add(light);
|
||||
const fill = new THREE.DirectionalLight(0xdcefff, 2.4); fill.position.set(-4, 2, 3); scene.add(fill);
|
||||
const rim = new THREE.DirectionalLight(0xffffff, 1.8); rim.position.set(2, 4, -5); scene.add(rim);
|
||||
const grid = new THREE.GridHelper(4, 40, 0x8eb0b2, 0x466266); grid.name = "machine-xy-ground-grid-z-up"; scene.add(grid);
|
||||
const camera = new THREE.PerspectiveCamera(42, 1, .005, 100); camera.position.set(2.4, 1.8, 2.8);
|
||||
const orbit = new OrbitControls(camera, renderer.domElement); orbit.enableDamping = true;
|
||||
const root = new THREE.Group(); root.name = "machine-manager-preview"; scene.add(root);
|
||||
const nodes = new Map<string, THREE.Group>(); const disposable: THREE.Object3D[] = [];
|
||||
for (const part of parts) {
|
||||
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);
|
||||
const visual = cloneMachinePartScene(part.scene); visual.position.fromArray(part.originMm).multiplyScalar(-.001); node.add(visual); disposable.push(visual);
|
||||
const axes = new THREE.AxesHelper(.12); axes.name = `origin_${part.id}`; axes.visible = part.showOrigin; node.add(axes); root.add(node); nodes.set(part.id, node);
|
||||
const node = new THREE.Group(); node.name = `draft_${part.id}`; node.visible = part.visible; node.position.fromArray(machineToThree(part.translationMm)).multiplyScalar(.001); node.quaternion.copy(machineEulerToThreeQuaternion(part.rotationDeg));
|
||||
const visual = cloneMachinePartScene(part.scene); visual.position.fromArray(machineToThree(part.originMm).map((value) => -value) as Vec3).multiplyScalar(.001); node.add(visual); disposable.push(visual);
|
||||
const axes = new THREE.Group(); axes.name = `origin_${part.id}`; axes.visible = part.showOrigin;
|
||||
const axis = (end: THREE.Vector3, color: number) => { const geometry = new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(), end]); axes.add(new THREE.Line(geometry, new THREE.LineBasicMaterial({ color }))); };
|
||||
axis(new THREE.Vector3(.12, 0, 0), 0xff3333); axis(new THREE.Vector3(0, 0, -.12), 0x33cc66); axis(new THREE.Vector3(0, .12, 0), 0x3388ff); node.add(axes); root.add(node); nodes.set(part.id, node);
|
||||
}
|
||||
const cycle = (child: string, parent: string) => { let cursor: string | null = parent; const seen = new Set([child]); while (cursor) { if (seen.has(cursor)) return true; seen.add(cursor); cursor = parts.find((part) => part.id === cursor)?.parentId ?? null; } return false; };
|
||||
for (const part of parts) if (part.parentId && nodes.has(part.parentId) && !cycle(part.id, part.parentId)) nodes.get(part.parentId)!.add(nodes.get(part.id)!);
|
||||
const bounds = new THREE.Box3().setFromObject(root); const center = bounds.isEmpty() ? new THREE.Vector3() : bounds.getCenter(new THREE.Vector3()); const size = bounds.isEmpty() ? 1 : Math.max(bounds.getSize(new THREE.Vector3()).length(), .2);
|
||||
orbit.target.copy(center); camera.position.copy(center).add(new THREE.Vector3(size, size * .7, size * 1.15)); orbit.update();
|
||||
const transform = new TransformControls(camera, renderer.domElement); transform.setMode(mode); transform.setSpace("local"); scene.add(transform.getHelper());
|
||||
const selected = selectedId ? nodes.get(selectedId) : null; if (selected) transform.attach(selected);
|
||||
const selected = selectedId ? nodes.get(selectedId) : null; const proxy = selected ? new THREE.Group() : null;
|
||||
const frameQuat = machineFrameQuaternion();
|
||||
if (selected && proxy) { proxy.name = "machine-coordinate-transform-proxy"; proxy.position.copy(selected.getWorldPosition(new THREE.Vector3())); proxy.quaternion.copy(frameQuat).multiply(new THREE.Quaternion().setFromEuler(new THREE.Euler(...parts.find((part) => part.id === selectedId)!.rotationDeg.map(THREE.MathUtils.degToRad) as Vec3, "XYZ"))); scene.add(proxy); transform.attach(proxy); }
|
||||
transform.addEventListener("dragging-changed", ((event: { value?: boolean }) => { orbit.enabled = !event.value; }) as never);
|
||||
transform.addEventListener("mouseUp", (() => {
|
||||
if (!selectedId || !selected) return;
|
||||
onTransformRef.current(selectedId, selected.position.toArray().map((value) => value * 1000) as Vec3, [THREE.MathUtils.radToDeg(selected.rotation.x), THREE.MathUtils.radToDeg(selected.rotation.y), THREE.MathUtils.radToDeg(selected.rotation.z)]);
|
||||
transform.addEventListener("objectChange", (() => {
|
||||
if (!selectedId || !selected || !proxy) return;
|
||||
const local = selected.parent ? selected.parent.worldToLocal(proxy.position.clone()) : proxy.position.clone(); selected.position.copy(local);
|
||||
const machineRotation = threeQuaternionToMachineEuler(proxy.quaternion); selected.quaternion.copy(machineEulerToThreeQuaternion(machineRotation));
|
||||
onTransformRef.current(selectedId, threeToMachine(selected.position.toArray() as Vec3).map((value) => value * 1000) as Vec3, machineRotation);
|
||||
}) as never);
|
||||
const resize = () => { const width = Math.max(element.clientWidth, 1), height = Math.max(element.clientHeight, 1); camera.aspect = width / height; camera.updateProjectionMatrix(); renderer.setSize(width, height, false); };
|
||||
const observer = new ResizeObserver(resize); observer.observe(element); resize(); let frame = 0;
|
||||
const render = () => { orbit.update(); renderer.render(scene, camera); frame = requestAnimationFrame(render); }; render();
|
||||
return () => { cancelAnimationFrame(frame); observer.disconnect(); transform.dispose(); orbit.dispose(); for (const object of disposable) disposeMachineScene(object); renderer.dispose(); renderer.domElement.remove(); };
|
||||
return () => { cancelAnimationFrame(frame); observer.disconnect(); transform.dispose(); orbit.dispose(); if (proxy) proxy.removeFromParent(); for (const object of disposable) disposeMachineScene(object); environment.dispose(); renderer.dispose(); renderer.domElement.remove(); };
|
||||
}, [parts, selectedId, mode]);
|
||||
return <div ref={host} className="machine-manager-preview" data-machine-manager-preview data-machine-manager-part-count={parts.length} data-machine-manager-origin-count={parts.filter((part) => part.showOrigin).length} data-transform-mode={mode} aria-label="三维机床装配预览" />;
|
||||
return <div ref={host} className="machine-manager-preview" data-machine-manager-preview data-machine-manager-coordinate-system="machine-z-up" data-machine-manager-axis-map="X:x,Y:-z,Z:y" data-machine-manager-lighting="studio-pmrem" data-machine-manager-exposure="1.45" data-machine-manager-part-count={parts.length} data-machine-manager-origin-count={parts.filter((part) => part.showOrigin).length} data-transform-mode={mode} aria-label="三维机床装配预览,机床 Z 轴朝上" />;
|
||||
}
|
||||
|
||||
export function MachineManagerPanel({ coordinates, disabled, onApply }: Props) {
|
||||
|
||||
@@ -28,6 +28,23 @@ export const MACHINE_PART_ROLES: MachinePartRole[] = ["base", "x", "y", "z", "to
|
||||
export const roleLabel = (role: MachinePartRole, coordinates: string) => ({ base: "床身底座", x: "X 轴部件", y: "Y 轴部件", z: "Z 轴部件", tool: "主轴/刀具", tilt: `${coordinates === "XYZBC" ? "B" : "A"} 轴部件`, c: "C 轴部件", work: "工件/工作台" }[role]);
|
||||
export const expectedParentRole = (role: MachinePartRole): MachinePartRole | null => ({ base: null, x: "base", y: "x", z: "y", tool: "z", tilt: "base", c: "tilt", work: "c" }[role] as MachinePartRole | null);
|
||||
|
||||
// glTF uses Y-up. The editor exposes the machine's right-handed X/Y/Z frame
|
||||
// with Z-up: machine (x, y, z) -> Three.js (x, z, -y).
|
||||
export const machineToThree = (value: Vec3): Vec3 => [value[0], value[2], -value[1]];
|
||||
export const threeToMachine = (value: Vec3): Vec3 => [value[0], -value[2], value[1]];
|
||||
export const machineFrameQuaternion = () => new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().set(1, 0, 0, 0, 0, 0, -1, 0, 0, 1, 0, 0, 0, 0, 0, 1));
|
||||
export function machineEulerToThreeQuaternion(rotationDeg: Vec3) {
|
||||
const machine = new THREE.Euler(...rotationDeg.map(THREE.MathUtils.degToRad) as Vec3, "XYZ");
|
||||
const source = new THREE.Matrix4().makeRotationFromEuler(machine);
|
||||
const frame = new THREE.Matrix4().set(1, 0, 0, 0, 0, 0, -1, 0, 0, 1, 0, 0, 0, 0, 0, 1);
|
||||
return new THREE.Quaternion().setFromRotationMatrix(frame.clone().multiply(source).multiply(frame.clone().invert()));
|
||||
}
|
||||
export function threeQuaternionToMachineEuler(rotation: THREE.Quaternion) {
|
||||
const frame = machineFrameQuaternion();
|
||||
const machine = frame.clone().invert().multiply(rotation);
|
||||
return new THREE.Euler().setFromQuaternion(machine, "XYZ").toArray().slice(0, 3).map((radians) => THREE.MathUtils.radToDeg(Number(radians))) as Vec3;
|
||||
}
|
||||
|
||||
const finite3 = (value: Vec3, label: string) => {
|
||||
if (value.length !== 3 || value.some((number) => !Number.isFinite(number))) throw new Error(`${label}需要三个有限数值`);
|
||||
};
|
||||
@@ -120,10 +137,10 @@ export async function buildManagedMachine(input: { coordinates: "XYZAC" | "XYZBC
|
||||
for (const role of MACHINE_PART_ROLES) {
|
||||
const part = byRole.get(role)!;
|
||||
const placement = new THREE.Group(); placement.name = `component_${part.id}`; placement.visible = part.visible;
|
||||
placement.position.fromArray(part.translationMm).multiplyScalar(.001);
|
||||
placement.rotation.set(...part.rotationDeg.map(THREE.MathUtils.degToRad) as Vec3);
|
||||
placement.position.fromArray(machineToThree(part.translationMm)).multiplyScalar(.001);
|
||||
placement.quaternion.copy(machineEulerToThreeQuaternion(part.rotationDeg));
|
||||
placement.userData.machinePart = { role, filename: part.filename, hash: part.hash, originMm: [...part.originMm] };
|
||||
const visual = cloneMachinePartScene(part.scene); visual.position.fromArray(part.originMm).multiplyScalar(-.001); placement.add(visual); links[role].add(placement);
|
||||
const visual = cloneMachinePartScene(part.scene); visual.position.fromArray(machineToThree(part.originMm).map((value) => -value) as Vec3).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) });
|
||||
|
||||
@@ -362,7 +362,7 @@ output { font: 12px ui-monospace, monospace; text-align: right; }
|
||||
.machine-manager-toolbar { margin-bottom: 10px; }
|
||||
.machine-manager-toolbar button[aria-pressed="true"] { background: #27585d; color: #fff; }
|
||||
.machine-manager-layout { display: grid; grid-template-columns: minmax(380px, 1.35fr) minmax(330px, .9fr); gap: 12px; min-height: 430px; }
|
||||
.machine-manager-preview { min-width: 0; min-height: 430px; border: 1px solid #52666a; background: #10191d; overflow: hidden; }
|
||||
.machine-manager-preview { min-width: 0; min-height: 430px; border: 1px solid #52666a; background: #263438; overflow: hidden; }
|
||||
.machine-manager-preview canvas { display: block; width: 100%; height: 100%; }
|
||||
.machine-manager-editor { display: grid; grid-template-rows: minmax(120px, .7fr) minmax(260px, 1.3fr); min-height: 0; gap: 8px; }
|
||||
.machine-part-list, .machine-part-editor, .machine-manager-calibration { border: 1px solid #9babad; background: #f6f9f8; padding: 8px; min-height: 0; overflow: auto; }
|
||||
|
||||
Reference in New Issue
Block a user