Stabilize machine manager WebGL resources
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 67 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 299 KiB After Width: | Height: | Size: 235 KiB |
@@ -26,6 +26,14 @@
|
|||||||
- MILL700 验证 132 节点、126 网格、深度 6 的树显示;选择 `MILL700-A-主轴-JT2` 得到父级局部原点 `X=-3.969, Y=-204.276, Z=405.633 mm`;内部节点拖放及八部件根节点拖放通过。
|
- MILL700 验证 132 节点、126 网格、深度 6 的树显示;选择 `MILL700-A-主轴-JT2` 得到父级局部原点 `X=-3.969, Y=-204.276, Z=405.633 mm`;内部节点拖放及八部件根节点拖放通过。
|
||||||
- 为避免大模型选择卡顿,树选择只更新坐标面板;预览使用共享只读几何/材质的节点克隆,层级变更后才重建变换树。MGR-12 verified。
|
- 为避免大模型选择卡顿,树选择只更新坐标面板;预览使用共享只读几何/材质的节点克隆,层级变更后才重建变换树。MGR-12 verified。
|
||||||
|
|
||||||
|
## R06 / 2026-09-20 / WebGL framebuffer 稳定性
|
||||||
|
|
||||||
|
- 用户截图显示 `texImage2D width or height out of range`、`glRenderbufferStorageMultisample` 超限和 framebuffer zero-size 连续错误,导致层级管理界面不稳定。
|
||||||
|
- MILL700 三张纹理实际为 `1024x1024`、`1604x1205`、`1604x1205`,不是纹理超限;风险来自特定 GPU、DPR 和 DevTools 短视口组合下的多重采样 drawing buffer。
|
||||||
|
- 管理器 WebGL renderer 关闭 MSAA,读取 `MAX_RENDERBUFFER_SIZE`、`MAX_TEXTURE_SIZE` 和 `MAX_VIEWPORT_DIMS`,按 90% 安全边界及最多 4096² 像素动态降低 DPR;宽高不足 2px 时暂停 resize/render。
|
||||||
|
- 发布 `render-state`、实际 pixel ratio、buffer size 和 GPU 上限诊断;context lost 后停止动画循环,避免重复 framebuffer 错误刷屏。
|
||||||
|
- 模拟页面 `1154x220`、DPR 3:CSS `656x655`、drawing buffer `984x982`、GPU 上限 `8192`,控制台警告/错误 0。MILL700 层级树及 AC/BC 生产回归通过。MGR-13 verified。
|
||||||
|
|
||||||
## R02 / 2026-09-20 / 部件装配、标定、应用与验收完成
|
## R02 / 2026-09-20 / 部件装配、标定、应用与验收完成
|
||||||
|
|
||||||
- 新增 `machine-manager.ts`:通用自包含 GLB 部件加载、SHA-256、深复制、八角色和两条机械链校验、AC/BC URDF 生成、组合场景、底座/整机 userData、组合哈希。
|
- 新增 `machine-manager.ts`:通用自包含 GLB 部件加载、SHA-256、深复制、八角色和两条机械链校验、AC/BC URDF 生成、组合场景、底座/整机 userData、组合哈希。
|
||||||
|
|||||||
@@ -14,3 +14,4 @@
|
|||||||
|MGR-10 PBR 预览照明|verified|MGR-04|高金属度 GLB 在管理器中具有可辨识表面;PMREM、色彩空间、曝光和多向补光生效|
|
|MGR-10 PBR 预览照明|verified|MGR-04|高金属度 GLB 在管理器中具有可辨识表面;PMREM、色彩空间、曝光和多向补光生效|
|
||||||
|MGR-11 Z-up 机床坐标|verified|MGR-04,MGR-05|`camera.up=(0,0,1)`,机床 Z 竖直、XY 地面;导入模型信息和原始变换不修改|
|
|MGR-11 Z-up 机床坐标|verified|MGR-04,MGR-05|`camera.up=(0,0,1)`,机床 Z 竖直、XY 地面;导入模型信息和原始变换不修改|
|
||||||
|MGR-12 GLB 层级树管理|verified|MGR-02,MGR-04|原始层级按 tree 显示;根/内部节点拖放、循环拒绝、节点原点坐标和 MILL700 深层结构验收通过|
|
|MGR-12 GLB 层级树管理|verified|MGR-02,MGR-04|原始层级按 tree 显示;根/内部节点拖放、循环拒绝、节点原点坐标和 MILL700 深层结构验收通过|
|
||||||
|
|MGR-13 WebGL 资源稳定性|verified|MGR-10,MGR-12|禁用超限 MSAA;drawing buffer 按 GPU 上限和像素预算缩放;零尺寸/context lost 不连续绘制;短视口 DPR3 无警告|
|
||||||
|
|||||||
@@ -16,8 +16,8 @@
|
|||||||
- 状态:verified;浏览器错误 0。
|
- 状态:verified;浏览器错误 0。
|
||||||
- 覆盖:8 部件导入和预览、原点显隐、数值/TransformControls 变换、组合 GLB 导出、AC/BC 应用、底座 userData、物理零点、非法层级拒绝、BC 五轴程序。
|
- 覆盖:8 部件导入和预览、原点显隐、数值/TransformControls 变换、组合 GLB 导出、AC/BC 应用、底座 userData、物理零点、非法层级拒绝、BC 五轴程序。
|
||||||
- 截图:
|
- 截图:
|
||||||
- `coverage/glb-machine-manager/manager-ac-config.png`,SHA-256 `70238293d81a0063e413d4bf3b2b0f0bc4c8453e45256e32fb625ff9e92b9609`
|
- `coverage/glb-machine-manager/manager-ac-config.png`,SHA-256 `6e53968722744d8fc4dc753d83ee8bad3c6c3ce0d41c34d64214b5586faf2dc5`
|
||||||
- `coverage/glb-machine-manager/managed-bc-running.png`,SHA-256 `6e14ff6bc4e493f57e55bb601ded84319ce86f064f7a811595c1e4498b1827a0`
|
- `coverage/glb-machine-manager/managed-bc-running.png`,SHA-256 `2a28290ff58b3a0d3250ee5ebb1bc636a2cc8b8f62828b41764435501f97d385`
|
||||||
|
|
||||||
## E03 / R02 / 生产构建浏览器闭环
|
## E03 / R02 / 生产构建浏览器闭环
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
- report_id:`GLB-MGR-E05`;报告 SHA-256 `7c479b36396abc6f7466ed8b54cc83216dd72c30ba11fa0c667e6491d47c2171`。
|
- report_id:`GLB-MGR-E05`;报告 SHA-256 `7c479b36396abc6f7466ed8b54cc83216dd72c30ba11fa0c667e6491d47c2171`。
|
||||||
- 结构:132 节点、126 网格、深度 6、126 材质,其中 110 个 `metalness >= 0.9`。
|
- 结构:132 节点、126 网格、深度 6、126 材质,其中 110 个 `metalness >= 0.9`。
|
||||||
- 检查:已有内部层级完整保留;管理器使用 `studio-pmrem`、sRGB 输出、ACES、曝光 `1.45` 和多向补光;浏览器错误为 0。
|
- 检查:已有内部层级完整保留;管理器使用 `studio-pmrem`、sRGB 输出、ACES、曝光 `1.45` 和多向补光;浏览器错误为 0。
|
||||||
- 截图:`coverage/glb-machine-manager/mill700/mill700-studio-lighting.png`,SHA-256 `e741ae55623f28399565c4296e5d679a71fba346232ce043edc5ab0c8901d6b4`。
|
- 截图:`coverage/glb-machine-manager/mill700/mill700-studio-lighting.png`,SHA-256 `289255511eccce697ec0c6f002a44588475e9042fa12f23646ea02c4c63a3b60`。
|
||||||
|
|
||||||
## E06 / R04 / Z-up 坐标
|
## E06 / R04 / Z-up 坐标
|
||||||
|
|
||||||
@@ -62,3 +62,11 @@
|
|||||||
- 拖放:`MILL700-A-门1-JT1` 拖到 `MILL700-A底座` 后,草稿父路径等于目标稳定源路径;原始 GLB SHA-256 保持 `bf37130681cc6462412e52744519516e8ade2a3212dd69153c0b1b3e415bcf66`。
|
- 拖放:`MILL700-A-门1-JT1` 拖到 `MILL700-A底座` 后,草稿父路径等于目标稳定源路径;原始 GLB SHA-256 保持 `bf37130681cc6462412e52744519516e8ade2a3212dd69153c0b1b3e415bcf66`。
|
||||||
- 八个独立 GLB 的根树节点拖放通过;随后 AC/BC 应用、导出和程序运动回归仍通过。
|
- 八个独立 GLB 的根树节点拖放通过;随后 AC/BC 应用、导出和程序运动回归仍通过。
|
||||||
- 控件契约:101 个控件,新增 `machine-tree-node` 和 `machine-tree-origin`;TypeScript、生产构建、双画布测试通过,浏览器错误 0。
|
- 控件契约:101 个控件,新增 `machine-tree-node` 和 `machine-tree-origin`;TypeScript、生产构建、双画布测试通过,浏览器错误 0。
|
||||||
|
|
||||||
|
## E08 / R06 / WebGL framebuffer 稳定性
|
||||||
|
|
||||||
|
- 状态:verified。
|
||||||
|
- 触发环境:页面 viewport `1154x220`,deviceScaleFactor 3,模拟底部停靠 DevTools 和高 DPI。
|
||||||
|
- 修复后:管理器 CSS `656x655`,drawing buffer `984x982`,`MAX_RENDERBUFFER_SIZE=8192`,MSAA=false,控制台 warning/error=0。
|
||||||
|
- renderer 诊断字段:`machine-manager-webgl-antialias=false`、`render-state=ready`、动态 `pixel-ratio/buffer-size/max-buffer`。
|
||||||
|
- MILL700 132 节点树选择/拖放通过;生产 AC/BC 管理器闭环通过,浏览器错误 0。
|
||||||
|
|||||||
@@ -39,3 +39,7 @@ UI 允许用户选择父级以建立层级和发现错误;应用时只接受
|
|||||||
## D10 / 层级编辑使用稳定源路径覆盖
|
## D10 / 层级编辑使用稳定源路径覆盖
|
||||||
|
|
||||||
GLB 树节点 ID 使用导入时的子节点索引路径,不使用可能重复或缺失的节点名称。拖放保存 `sourcePath -> targetPath` 草稿覆盖,每次从原始场景克隆后应用,因此重复编辑稳定且不污染导入资产。根 GLB 之间继续使用部件 `parentId`,内部节点不允许跨 GLB 直接重挂。
|
GLB 树节点 ID 使用导入时的子节点索引路径,不使用可能重复或缺失的节点名称。拖放保存 `sourcePath -> targetPath` 草稿覆盖,每次从原始场景克隆后应用,因此重复编辑稳定且不污染导入资产。根 GLB 之间继续使用部件 `parentId`,内部节点不允许跨 GLB 直接重挂。
|
||||||
|
|
||||||
|
## D11 / 管理器优先保证 framebuffer 可创建
|
||||||
|
|
||||||
|
管理器不依赖 MSAA;高 DPI 清晰度由受限 DPR 提供。drawing buffer 必须同时满足 GPU 维度上限和 4096² 像素预算,不能把异常 DOM 尺寸直接传给 WebGL。零尺寸和 context lost 视为暂停状态,不继续调用 render 产生重复错误。
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
现有整机 GLB + 内嵌 URDF 导入能力继续保留。新管理器生成与现有 `linuxcnc.machine/1` 相同的严格结构,再通过已有配置、Worker、WASM 和 Three.js 投影链运行。
|
现有整机 GLB + 内嵌 URDF 导入能力继续保留。新管理器生成与现有 `linuxcnc.machine/1` 相同的严格结构,再通过已有配置、Worker、WASM 和 Three.js 投影链运行。
|
||||||
|
|
||||||
当前状态:MGR-01 至 MGR-12 全部 verified,MILL700 实测覆盖照明、Z-up、132 节点层级树、拖放和节点原点。最新台账 R05,浏览器证据 `GLB-MGR-E01/E02/E05/E07`,截图和报告位于 `coverage/glb-machine-manager/`。
|
当前状态:MGR-01 至 MGR-13 全部 verified,MILL700 实测覆盖照明、Z-up、132 节点层级树、拖放、节点原点和 WebGL 资源降级。最新台账 R06,浏览器证据 `GLB-MGR-E01/E02/E05/E07/E08`,截图和报告位于 `coverage/glb-machine-manager/`。
|
||||||
|
|
||||||
文档:
|
文档:
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const imported = await import(pathToFileURL(modulePath).href); const playwright
|
|||||||
const output = new URL("../coverage/glb-machine-manager/", import.meta.url); await mkdir(output, { recursive: true });
|
const output = new URL("../coverage/glb-machine-manager/", import.meta.url); await mkdir(output, { recursive: true });
|
||||||
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 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 = [];
|
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()}`); });
|
page.on("pageerror", (error) => errors.push(`pageerror:${error.message}`)); page.on("console", (message) => { if (["warning", "error"].includes(message.type())) errors.push(`console:${message.type()}:${message.text()}`); });
|
||||||
const control = (id) => page.locator(`[data-axis-control="${id}"]`); const main = page.locator("main.axis-app");
|
const control = (id) => page.locator(`[data-axis-control="${id}"]`); const main = page.locator("main.axis-app");
|
||||||
const production = process.env.MACHINE_MANAGER_SOURCE_MODULES === "0";
|
const production = process.env.MACHINE_MANAGER_SOURCE_MODULES === "0";
|
||||||
const report = { schema: "machine-manager-browser.v1", report_id: production ? "GLB-MGR-E02" : "GLB-MGR-E01", mode: production ? "production-preview" : "development-source", status: "running", checks: [] };
|
const report = { schema: "machine-manager-browser.v1", report_id: production ? "GLB-MGR-E02" : "GLB-MGR-E01", mode: production ? "production-preview" : "development-source", status: "running", checks: [] };
|
||||||
@@ -30,7 +30,7 @@ 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-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 control("machine-manager-files").setInputFiles(fixtures);
|
||||||
await page.waitForFunction(() => document.querySelector("[data-machine-manager-preview]")?.dataset.machineManagerPartCount === "8");
|
await page.waitForFunction(() => document.querySelector("[data-machine-manager-preview]")?.dataset.machineManagerPartCount === "8");
|
||||||
assert.equal(await page.locator('[data-machine-tree-path="scene"]').count(), 8); assert.ok(await page.locator('[data-axis-control="machine-tree-node"]').count() >= 16); 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");
|
assert.equal(await page.locator('[data-machine-tree-path="scene"]').count(), 8); assert.ok(await page.locator('[data-axis-control="machine-tree-node"]').count() >= 16); 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"); assert.equal(await page.locator("[data-machine-manager-preview]").getAttribute("data-machine-manager-webgl-antialias"), "false"); assert.equal(await page.locator("[data-machine-manager-preview]").getAttribute("data-machine-manager-render-state"), "ready"); assert.equal(await page.locator("[data-machine-manager-preview]").getAttribute("data-machine-manager-context-generation"), "1");
|
||||||
report.checks.push("eight GLB parts rendered with studio PMREM lighting and calibrated exposure");
|
report.checks.push("eight GLB parts rendered with studio PMREM lighting and calibrated exposure");
|
||||||
const roots = page.locator('[data-machine-tree-path="scene"]'); const sourceRoot = roots.nth(7), targetRoot = roots.nth(6), sourcePart = await sourceRoot.getAttribute("data-machine-tree-part"), targetPart = await targetRoot.getAttribute("data-machine-tree-part"), transfer = await page.evaluateHandle(() => new DataTransfer()); await sourceRoot.locator("button").first().dispatchEvent("dragstart", { dataTransfer: transfer }); await targetRoot.locator("button").first().dispatchEvent("dragover", { dataTransfer: transfer }); await targetRoot.locator("button").first().dispatchEvent("drop", { dataTransfer: transfer }); await page.waitForFunction(({ sourcePart, targetPart }) => document.querySelector(`[data-machine-tree-part="${sourcePart}"][data-machine-tree-path="scene"]`)?.getAttribute("data-machine-tree-parent") === targetPart, { sourcePart, targetPart });
|
const roots = page.locator('[data-machine-tree-path="scene"]'); const sourceRoot = roots.nth(7), targetRoot = roots.nth(6), sourcePart = await sourceRoot.getAttribute("data-machine-tree-part"), targetPart = await targetRoot.getAttribute("data-machine-tree-part"), transfer = await page.evaluateHandle(() => new DataTransfer()); await sourceRoot.locator("button").first().dispatchEvent("dragstart", { dataTransfer: transfer }); await targetRoot.locator("button").first().dispatchEvent("dragover", { dataTransfer: transfer }); await targetRoot.locator("button").first().dispatchEvent("drop", { dataTransfer: transfer }); await page.waitForFunction(({ sourcePart, targetPart }) => document.querySelector(`[data-machine-tree-part="${sourcePart}"][data-machine-tree-path="scene"]`)?.getAttribute("data-machine-tree-parent") === targetPart, { sourcePart, targetPart });
|
||||||
report.checks.push("dragging GLB root tree nodes updates component parent hierarchy");
|
report.checks.push("dragging GLB root tree nodes updates component parent hierarchy");
|
||||||
@@ -39,6 +39,7 @@ try {
|
|||||||
const translation = page.locator("fieldset.machine-vector").filter({ hasText: "部件平移" }); await translation.getByLabel("X", { exact: true }).fill("10");
|
const translation = page.locator("fieldset.machine-vector").filter({ hasText: "部件平移" }); await translation.getByLabel("X", { exact: true }).fill("10");
|
||||||
const rotation = page.locator("fieldset.machine-vector").filter({ hasText: "部件旋转" }); await rotation.getByLabel("Z", { exact: true }).fill("15");
|
const rotation = page.locator("fieldset.machine-vector").filter({ hasText: "部件旋转" }); await rotation.getByLabel("Z", { exact: true }).fill("15");
|
||||||
await control("machine-manager-rotate").click(); assert.equal(await page.locator("[data-machine-manager-preview]").getAttribute("data-transform-mode"), "rotate");
|
await control("machine-manager-rotate").click(); assert.equal(await page.locator("[data-machine-manager-preview]").getAttribute("data-transform-mode"), "rotate");
|
||||||
|
assert.equal(await page.locator("[data-machine-manager-preview]").getAttribute("data-machine-manager-context-generation"), "1");
|
||||||
await page.locator(".machine-manager-dialog").screenshot({ path: new URL("manager-ac-config.png", output).pathname });
|
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");
|
report.checks.push("origin visibility, numeric translation/rotation and TransformControls mode update");
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const output = new URL("../coverage/glb-machine-manager/mill700/", import.meta.u
|
|||||||
const imported = await import(pathToFileURL(modulePath).href); const playwright = imported.default ?? imported;
|
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 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 = [];
|
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()}`); });
|
page.on("pageerror", (error) => errors.push(`pageerror:${error.message}`)); page.on("console", (message) => { if (["warning", "error"].includes(message.type())) errors.push(`console:${message.type()}:${message.text()}`); });
|
||||||
const control = (id) => page.locator(`[data-axis-control="${id}"]`);
|
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: [] };
|
const report = { schema: "machine-manager-mill700-browser.v1", report_id: "GLB-MGR-E05", status: "running", model: { path: modelPath, bytes: model.length, sha256 }, checks: [] };
|
||||||
try {
|
try {
|
||||||
@@ -27,9 +27,9 @@ try {
|
|||||||
await page.waitForFunction(() => document.querySelector("[data-machine-manager-preview]")?.dataset.machineManagerPartCount === "1" && document.querySelector("[data-machine-manager-preview]")?.dataset.machineManagerRendered === "true" && document.querySelectorAll('[data-axis-control="machine-tree-node"]').length >= 132, null, { timeout: 60000 });
|
await page.waitForFunction(() => document.querySelector("[data-machine-manager-preview]")?.dataset.machineManagerPartCount === "1" && document.querySelector("[data-machine-manager-preview]")?.dataset.machineManagerRendered === "true" && document.querySelectorAll('[data-axis-control="machine-tree-node"]').length >= 132, null, { timeout: 60000 });
|
||||||
assert.equal(hierarchy.root, "MILL700-U_1"); assert.equal(hierarchy.nodes, 132); assert.equal(hierarchy.meshes, 126); assert.ok(hierarchy.maxDepth >= 6); assert.ok(hierarchy.metallic > 20);
|
assert.equal(hierarchy.root, "MILL700-U_1"); assert.equal(hierarchy.nodes, 132); assert.equal(hierarchy.meshes, 126); assert.ok(hierarchy.maxDepth >= 6); assert.ok(hierarchy.metallic > 20);
|
||||||
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 .*Y .*Z /s); await page.screenshot({ path: new URL("mill700-studio-lighting.png", output).pathname, timeout: 60000 });
|
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 .*Y .*Z /s); 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 });
|
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("existing 132-node, depth-6 hierarchy displayed as a draggable tree with node origins");
|
report.hierarchy = hierarchy; report.checks.push("existing 132-node, depth-6 hierarchy displayed as a draggable tree with node origins");
|
||||||
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:y,Z:z"); assert.equal(await page.locator("[data-machine-manager-preview]").getAttribute("data-machine-manager-camera-up"), "0,0,1"); assert.equal(await page.locator("[data-machine-manager-preview]").getAttribute("data-machine-manager-model-transform"), "none-on-import");
|
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"), "ready"); assert.equal(await preview.getAttribute("data-machine-manager-context-generation"), "1"); 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);
|
||||||
report.checks.push("metallic MILL700 surfaces rendered with studio PMREM lighting; screenshot captured for visual review"); assert.deepEqual(errors, []); report.status = "verified";
|
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, timeout: 5000 }).catch(() => undefined); throw error; }
|
} catch (error) { report.status = "failed"; report.error = error instanceof Error ? error.stack : String(error); await page.screenshot({ path: new URL("failure.png", output).pathname, timeout: 5000 }).catch(() => undefined); throw error; }
|
||||||
finally { report.browser_errors = errors; await writeFile(new URL("browser.report.json", output), JSON.stringify(report, null, 2) + "\n"); await browser.close(); }
|
finally { report.browser_errors = errors; await writeFile(new URL("browser.report.json", output), JSON.stringify(report, null, 2) + "\n"); await browser.close(); }
|
||||||
|
|||||||
@@ -14,12 +14,21 @@ const zero3 = (): Vec3 => [0, 0, 0];
|
|||||||
const number = (value: string) => Number.isFinite(Number(value)) ? Number(value) : 0;
|
const number = (value: string) => Number.isFinite(Number(value)) ? Number(value) : 0;
|
||||||
|
|
||||||
function ManagerPreview({ parts, selectedId, mode, onTransform }: { parts: MachinePartDraft[]; selectedId: string | null; mode: "translate" | "rotate"; onTransform: (id: string, translationMm: Vec3, rotationDeg: Vec3) => void }) {
|
function ManagerPreview({ parts, selectedId, mode, onTransform }: { parts: MachinePartDraft[]; selectedId: string | null; mode: "translate" | "rotate"; onTransform: (id: string, translationMm: Vec3, rotationDeg: Vec3) => void }) {
|
||||||
const host = useRef<HTMLDivElement>(null);
|
type PreviewHost = HTMLDivElement & { machineManagerSync?: (parts: MachinePartDraft[], selectedId: string | null, mode: "translate" | "rotate") => void };
|
||||||
|
const host = useRef<PreviewHost>(null);
|
||||||
|
const stateRef = useRef({ parts, selectedId, mode }); stateRef.current = { parts, selectedId, mode };
|
||||||
const onTransformRef = useRef(onTransform); onTransformRef.current = onTransform;
|
const onTransformRef = useRef(onTransform); onTransformRef.current = onTransform;
|
||||||
|
useEffect(() => { host.current?.machineManagerSync?.(parts, selectedId, mode); }, [parts, selectedId, mode]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const element = host.current; if (!element) return;
|
const element = host.current; if (!element) return;
|
||||||
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
// Large industrial GLBs can exceed small-GPU multisample renderbuffer
|
||||||
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);
|
// limits when DevTools or display scaling changes the drawing-buffer size.
|
||||||
|
const renderer = new THREE.WebGLRenderer({ antialias: false, alpha: false, depth: true, stencil: false, powerPreference: "high-performance" });
|
||||||
|
renderer.outputColorSpace = THREE.SRGBColorSpace; renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 1.45; renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFShadowMap; element.appendChild(renderer.domElement);
|
||||||
|
const gl = renderer.getContext(), viewportLimit = gl.getParameter(gl.MAX_VIEWPORT_DIMS) as Int32Array;
|
||||||
|
const maxDimension = Math.max(1, Math.floor(Math.min(Number(gl.getParameter(gl.MAX_RENDERBUFFER_SIZE) || 4096), Number(gl.getParameter(gl.MAX_TEXTURE_SIZE) || 4096), Number(viewportLimit?.[0] || 4096), Number(viewportLimit?.[1] || 4096)) * .9));
|
||||||
|
const maxPixels = Math.min(maxDimension * maxDimension, 4096 * 4096);
|
||||||
|
element.dataset.machineManagerWebglAntialias = String(Boolean(gl.getContextAttributes()?.antialias)); element.dataset.machineManagerMaxBuffer = String(maxDimension);
|
||||||
const scene = new THREE.Scene(); scene.background = new THREE.Color(0x263438);
|
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();
|
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));
|
scene.add(new THREE.HemisphereLight(0xffffff, 0x728083, 2.6)); scene.add(new THREE.AmbientLight(0xffffff, .65));
|
||||||
@@ -30,30 +39,54 @@ function ManagerPreview({ parts, selectedId, mode, onTransform }: { parts: Machi
|
|||||||
const camera = new THREE.PerspectiveCamera(42, 1, .005, 100); camera.up.set(0, 0, 1); camera.position.set(2.4, -2.8, 1.8);
|
const camera = new THREE.PerspectiveCamera(42, 1, .005, 100); camera.up.set(0, 0, 1); camera.position.set(2.4, -2.8, 1.8);
|
||||||
const orbit = new OrbitControls(camera, renderer.domElement); orbit.enableDamping = true;
|
const orbit = new OrbitControls(camera, renderer.domElement); orbit.enableDamping = true;
|
||||||
const root = new THREE.Group(); root.name = "machine-manager-preview"; scene.add(root);
|
const root = new THREE.Group(); root.name = "machine-manager-preview"; scene.add(root);
|
||||||
const nodes = new Map<string, THREE.Group>(); const helpers: THREE.AxesHelper[] = [];
|
let nodes = new Map<string, THREE.Group>(), helpers: THREE.AxesHelper[] = [], selectedObject: THREE.Group | null = null, selectedPartId: string | null = null, lastParts: MachinePartDraft[] | null = null, fitSignature = "";
|
||||||
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);
|
|
||||||
// Preview clones only Object3D transforms/hierarchy. Geometry, material,
|
|
||||||
// texture and source node transforms remain untouched on the imported GLB.
|
|
||||||
const visual = part.scene.clone(true); applyMachineNodeParents(visual, part.nodeParents); visual.position.fromArray(part.originMm).multiplyScalar(-.001); node.add(visual);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
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 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);
|
|
||||||
transform.addEventListener("dragging-changed", ((event: { value?: boolean }) => { orbit.enabled = !event.value; }) as never);
|
transform.addEventListener("dragging-changed", ((event: { value?: boolean }) => { orbit.enabled = !event.value; }) as never);
|
||||||
transform.addEventListener("objectChange", (() => {
|
transform.addEventListener("objectChange", (() => {
|
||||||
if (!selectedId || !selected) return;
|
if (!selectedPartId || !selectedObject) 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)]);
|
onTransformRef.current(selectedPartId, selectedObject.position.toArray().map((value) => value * 1000) as Vec3, [THREE.MathUtils.radToDeg(selectedObject.rotation.x), THREE.MathUtils.radToDeg(selectedObject.rotation.y), THREE.MathUtils.radToDeg(selectedObject.rotation.z)]);
|
||||||
}) as never);
|
}) 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); };
|
element.machineManagerSync = (nextParts, nextSelectedId, nextMode) => {
|
||||||
|
transform.setMode(nextMode);
|
||||||
|
if (nextParts !== lastParts) {
|
||||||
|
transform.detach(); root.clear(); helpers.forEach((helper) => helper.dispose()); helpers = []; nodes = new Map();
|
||||||
|
for (const part of nextParts) {
|
||||||
|
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); applyMachineNodeParents(visual, part.nodeParents); visual.position.fromArray(part.originMm).multiplyScalar(-.001); node.add(visual);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
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 = nextParts.find((part) => part.id === cursor)?.parentId ?? null; } return false; };
|
||||||
|
for (const part of nextParts) if (part.parentId && nodes.has(part.parentId) && !cycle(part.id, part.parentId)) nodes.get(part.parentId)!.add(nodes.get(part.id)!);
|
||||||
|
const signature = nextParts.map((part) => `${part.id}:${part.hash}`).join("|");
|
||||||
|
if (signature !== fitSignature) {
|
||||||
|
const bounds = new THREE.Box3().setFromObject(root), center = bounds.isEmpty() ? new THREE.Vector3() : bounds.getCenter(new THREE.Vector3()), 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(); fitSignature = signature;
|
||||||
|
}
|
||||||
|
lastParts = nextParts;
|
||||||
|
}
|
||||||
|
selectedPartId = nextSelectedId; selectedObject = nextSelectedId ? nodes.get(nextSelectedId) ?? null : null; transform.detach(); if (selectedObject) transform.attach(selectedObject);
|
||||||
|
element.dataset.machineManagerContextGeneration = "1";
|
||||||
|
};
|
||||||
|
element.machineManagerSync(stateRef.current.parts, stateRef.current.selectedId, stateRef.current.mode);
|
||||||
|
let renderable = false, contextLost = false;
|
||||||
|
const resize = () => {
|
||||||
|
const width = element.clientWidth, height = element.clientHeight;
|
||||||
|
if (!Number.isFinite(width) || !Number.isFinite(height) || width < 2 || height < 2) { renderable = false; element.dataset.machineManagerRenderState = "suspended-zero-size"; return; }
|
||||||
|
const requested = Math.min(globalThis.devicePixelRatio || 1, 1.5);
|
||||||
|
const scale = Math.min(1, maxDimension / (width * requested), maxDimension / (height * requested), Math.sqrt(maxPixels / (width * height * requested * requested)));
|
||||||
|
const pixelRatio = Math.max(.5, requested * scale);
|
||||||
|
renderer.setPixelRatio(pixelRatio); camera.aspect = width / height; camera.updateProjectionMatrix(); renderer.setSize(width, height, false);
|
||||||
|
renderable = renderer.domElement.width > 0 && renderer.domElement.height > 0 && renderer.domElement.width <= maxDimension && renderer.domElement.height <= maxDimension;
|
||||||
|
element.dataset.machineManagerPixelRatio = pixelRatio.toFixed(3); element.dataset.machineManagerBufferSize = `${renderer.domElement.width}x${renderer.domElement.height}`; element.dataset.machineManagerRenderState = renderable ? "ready" : "suspended-limit";
|
||||||
|
};
|
||||||
|
const contextLostHandler = (event: Event) => { event.preventDefault(); contextLost = true; renderable = false; element.dataset.machineManagerRenderState = "context-lost"; };
|
||||||
|
renderer.domElement.addEventListener("webglcontextlost", contextLostHandler);
|
||||||
const observer = new ResizeObserver(resize); observer.observe(element); resize(); let frame = 0;
|
const observer = new ResizeObserver(resize); observer.observe(element); resize(); let frame = 0;
|
||||||
const render = () => { orbit.update(); renderer.render(scene, camera); element.dataset.machineManagerRendered = "true"; frame = requestAnimationFrame(render); }; render();
|
const render = () => { if (!contextLost && renderable) { orbit.update(); renderer.render(scene, camera); element.dataset.machineManagerRendered = "true"; } frame = contextLost ? 0 : requestAnimationFrame(render); }; render();
|
||||||
return () => { cancelAnimationFrame(frame); observer.disconnect(); transform.dispose(); orbit.dispose(); helpers.forEach((helper) => helper.dispose()); environment.dispose(); renderer.dispose(); renderer.domElement.remove(); };
|
return () => { if (frame) cancelAnimationFrame(frame); observer.disconnect(); delete element.machineManagerSync; renderer.domElement.removeEventListener("webglcontextlost", contextLostHandler); transform.dispose(); orbit.dispose(); helpers.forEach((helper) => helper.dispose()); 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-coordinate-system="machine-z-up" data-machine-manager-axis-map="X:x,Y:y,Z:z" data-machine-manager-camera-up="0,0,1" data-machine-manager-model-transform="none-on-import" 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 轴朝上" />;
|
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:y,Z:z" data-machine-manager-camera-up="0,0,1" data-machine-manager-model-transform="none-on-import" 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 轴朝上" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user