diff --git a/coverage/glb-machine-manager/mill700/browser.report.json b/coverage/glb-machine-manager/mill700/browser.report.json new file mode 100644 index 0000000..18e8fa3 --- /dev/null +++ b/coverage/glb-machine-manager/mill700/browser.report.json @@ -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": [] +} diff --git a/coverage/glb-machine-manager/mill700/failure.png b/coverage/glb-machine-manager/mill700/failure.png new file mode 100644 index 0000000..b407977 Binary files /dev/null and b/coverage/glb-machine-manager/mill700/failure.png differ diff --git a/coverage/glb-machine-manager/mill700/mill700-studio-lighting.png b/coverage/glb-machine-manager/mill700/mill700-studio-lighting.png new file mode 100644 index 0000000..e564fc0 Binary files /dev/null and b/coverage/glb-machine-manager/mill700/mill700-studio-lighting.png differ diff --git a/docs/glb-machine-manager/03-推进台账.md b/docs/glb-machine-manager/03-推进台账.md index 1a8f7f2..5be61f3 100644 --- a/docs/glb-machine-manager/03-推进台账.md +++ b/docs/glb-machine-manager/03-推进台账.md @@ -1,5 +1,15 @@ # 推进台账 +## 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 已验证,等待保存和发布。 + ## R02 / 2026-09-20 / 部件装配、标定、应用与验收完成 - 新增 `machine-manager.ts`:通用自包含 GLB 部件加载、SHA-256、深复制、八角色和两条机械链校验、AC/BC URDF 生成、组合场景、底座/整机 userData、组合哈希。 diff --git a/docs/glb-machine-manager/04-任务矩阵.md b/docs/glb-machine-manager/04-任务矩阵.md index ab66e5f..991669f 100644 --- a/docs/glb-machine-manager/04-任务矩阵.md +++ b/docs/glb-machine-manager/04-任务矩阵.md @@ -11,3 +11,4 @@ |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、色彩空间、曝光和多向补光生效| diff --git a/docs/glb-machine-manager/05-验收证据.md b/docs/glb-machine-manager/05-验收证据.md index 0b3c535..e23f5b4 100644 --- a/docs/glb-machine-manager/05-验收证据.md +++ b/docs/glb-machine-manager/05-验收证据.md @@ -37,3 +37,13 @@ - `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`。 diff --git a/docs/glb-machine-manager/06-决策记录.md b/docs/glb-machine-manager/06-决策记录.md index e709eca..cc47174 100644 --- a/docs/glb-machine-manager/06-决策记录.md +++ b/docs/glb-machine-manager/06-决策记录.md @@ -27,3 +27,7 @@ UI 允许用户选择父级以建立层级和发现错误;应用时只接受 ## D07 / 组合 GLB 只通过显式导出生成 普通编辑和“应用到 Web LinuxCNC”不触碰用户原文件。只有用户点击“导出组合 GLB”时才用 GLTFExporter 生成新文件。导出包含部件层级、URDF、底座标定 userData 和整机场景标定 userData。 + +## D08 / 编辑器照明不修改资产材质 + +模型过暗通过 Three.js 场景的 PMREM 环境、色彩管理、曝光和补光解决。不得为了预览可见而覆盖导入材质的颜色、金属度、粗糙度或纹理,否则导出结果会偏离用户资产。 diff --git a/docs/glb-machine-manager/README.md b/docs/glb-machine-manager/README.md index c20c901..367643b 100644 --- a/docs/glb-machine-manager/README.md +++ b/docs/glb-machine-manager/README.md @@ -11,7 +11,7 @@ 现有整机 GLB + 内嵌 URDF 导入能力继续保留。新管理器生成与现有 `linuxcnc.machine/1` 相同的严格结构,再通过已有配置、Worker、WASM 和 Three.js 投影链运行。 -当前状态:MGR-01 至 MGR-09 全部 verified,功能提交为 `8aab531`。最新台账 R02,浏览器证据 `GLB-MGR-E01/E02`,截图和报告位于 `coverage/glb-machine-manager/`。 +当前状态:MGR-01 至 MGR-10 全部 verified,MILL700 实测证据为 `GLB-MGR-E05`。最新台账 R03,浏览器证据 `GLB-MGR-E01/E02/E05`,截图和报告位于 `coverage/glb-machine-manager/`。 文档: diff --git a/scripts/test_machine_manager_mill700_browser.mjs b/scripts/test_machine_manager_mill700_browser.mjs new file mode 100644 index 0000000..5bb533a --- /dev/null +++ b/scripts/test_machine_manager_mill700_browser.mjs @@ -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"); + 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)); diff --git a/web/react/src/MachineManagerPanel.tsx b/web/react/src/MachineManagerPanel.tsx index 64cfe92..0fad3b0 100644 --- a/web/react/src/MachineManagerPanel.tsx +++ b/web/react/src/MachineManagerPanel.tsx @@ -3,6 +3,7 @@ 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"; @@ -17,10 +18,15 @@ 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); 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); @@ -44,9 +50,9 @@ function ManagerPreview({ parts, selectedId, mode, onTransform }: { parts: Machi 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(); for (const object of disposable) disposeMachineScene(object); environment.dispose(); renderer.dispose(); renderer.domElement.remove(); }; }, [parts, selectedId, mode]); - return