diff --git a/coverage/glb-machine-manager/workbench/browser.report.json b/coverage/glb-machine-manager/workbench/browser.report.json index bd37c9e..654790b 100644 --- a/coverage/glb-machine-manager/workbench/browser.report.json +++ b/coverage/glb-machine-manager/workbench/browser.report.json @@ -3,6 +3,7 @@ "status": "verified", "url": "http://127.0.0.1:5180/", "checks": [ + "Open Files Cancel leaves the machine manager dialog open", "search expands real nested nodes; axis overview locates selection; binding undo/redo", "empty input preserves last pose; repeated selection/input does not concatenate; single-axis step and reset", "TRT forbidden offset blocks export before build; calibration undo/redo", diff --git a/coverage/glb-machine-manager/workbench/desktop.png b/coverage/glb-machine-manager/workbench/desktop.png index d1bbf39..a75b867 100644 Binary files a/coverage/glb-machine-manager/workbench/desktop.png and b/coverage/glb-machine-manager/workbench/desktop.png differ diff --git a/coverage/glb-machine-manager/workbench/viewport-1154.png b/coverage/glb-machine-manager/workbench/viewport-1154.png index a35722b..58bdf37 100644 Binary files a/coverage/glb-machine-manager/workbench/viewport-1154.png and b/coverage/glb-machine-manager/workbench/viewport-1154.png differ diff --git a/coverage/glb-machine-manager/workbench/viewport-720.png b/coverage/glb-machine-manager/workbench/viewport-720.png index 6897c68..6541c13 100644 Binary files a/coverage/glb-machine-manager/workbench/viewport-720.png and b/coverage/glb-machine-manager/workbench/viewport-720.png differ diff --git a/docs/glb-machine-manager/03-推进台账.md b/docs/glb-machine-manager/03-推进台账.md index 5da12c2..1e7bc0b 100644 --- a/docs/glb-machine-manager/03-推进台账.md +++ b/docs/glb-machine-manager/03-推进台账.md @@ -127,3 +127,9 @@ - 增加 `linuxcnc.machine-editor/1` GLB 元数据:原部件配置与源节点零姿态拓扑,打开时根据持久节点 ID 重建原始部件树,再应用编辑覆盖,避免合并分组与双重变换。校验恢复结果与运动 JSON 一致。 - 文件和元数据在 IndexedDB 一次事务提交;写入失败仍允许下载并明确提示。库打开校验 SHA256;库副本独立于磁盘文件,清站点数据会删除库记录。 - 8 部件文件往返测试已通过:刷新后逐项回填、再次保存元数据完全等价、控制面板导入再送回编辑、旧 GLB 兼容、存储失败反馈。MILL700 AC/BC 完整回归通过,各 8 个五轴动态 tick,浏览器错误 0。工作台布局与八部件生产回归通过。MGR-21 verified。 + +## R16 / 2026-09-22 / 文件选择器取消路径 + +- 修复原生文件选择器 Cancel/项目 Escape 与父 native dialog 的取消事件串扰;文件选择期间父对话框以 cancelGuard 保持打开。 +- 三维管理“导入 GLB 部件”“打开已保存机床”、机床导入模型和机床参数导入统一保护;取消后草稿、选择节点、页签和配置不变。 +- E18 工作台回归新增真实 filechooser 取消验证,浏览器错误 0。 diff --git a/docs/glb-machine-manager/04-任务矩阵.md b/docs/glb-machine-manager/04-任务矩阵.md index 75bf38a..f40ae6a 100644 --- a/docs/glb-machine-manager/04-任务矩阵.md +++ b/docs/glb-machine-manager/04-任务矩阵.md @@ -24,3 +24,4 @@ |MGR-19 对话框机型与保存整机|verified|MGR-18|先在对话框选择设计机型;绑定/标定受机型约束;草稿独立/可撤销;保存完整 GLB 可由控制面板导入并自动切换机型| |MGR-20 机型程序一致性|verified|MGR-19|导入/应用跨机型时加载目标默认程序;同机型保留;错轴程序加载/运行/单步/继续拒绝;AC↔BC 浏览器及 Worker 验收通过| |MGR-21 保存位置与完整文档恢复|verified|MGR-20|命名 GLB 下载与本地机床库;显式打开/刷新恢复;多部件配置精确回填;二次保存等价;标准导入、旧版兼容、存储失败和 AC/BC 回归| +|MGR-22 文件选择器取消路径|verified|MGR-18|三维管理、机床导入、几何参数三类文件选择器取消/回焦不关闭父对话框;没有文件时保持配置和焦点| diff --git a/docs/glb-machine-manager/05-验收证据.md b/docs/glb-machine-manager/05-验收证据.md index 02d82d5..40a6fb9 100644 --- a/docs/glb-machine-manager/05-验收证据.md +++ b/docs/glb-machine-manager/05-验收证据.md @@ -192,3 +192,9 @@ |`mill700/browser.report.json`|`7f5c87b1a2d5491242aa87248d0cb4085e5d849065520ad56e4e64c6453216b2`| - E17 已发布功能代码 `8443930`,线上资源 `index-EHYipPfn.js`;线上完整复测保存、刷新、库打开、多部件字段回填、标准导入、再次编辑、旧版文件及存储失败分支,全部 verified,浏览器错误 0。报告 `document/cloud.report.json`、`document/deployment.report.json`。 + +## E18 / R16 / 文件选择器取消 + +- `node scripts/test_machine_manager_workbench.mjs`:report `GLB-MGR-E14` 更新,首项检查真实 `filechooser` 打开后取消,三维机床管理对话框仍保持打开;随后搜索、撤销、标定、布局回归继续通过,浏览器错误 0。 +- 三个文件入口统一保护:管理器 GLB/已保存机床、控制面板机床模型、机床几何 INI。 +- 工作台报告 SHA-256:`0f6db50633be732a4b65162aff3373eccb23cfe319a1dd7d19fa4260098bde64`。 diff --git a/docs/glb-machine-manager/README.md b/docs/glb-machine-manager/README.md index 50146ed..3f5ceaa 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-21 文件管理与恢复 verified;MGR-20 机型程序一致性 verified;MGR-19 对话框选择机型/整机保存已 verified;MGR-17、MGR-18 已 verified;MGR-01 至 MGR-16 已完成历史验收,MILL700 实测覆盖照明、Z-up、132 节点树、拖放、节点原点、关节局部坐标系、节点相对父级平移旋转、模型删除、轴绑定、XYZ/AC 层级运动和 WebGL 资源降级。最新台账 R15,证据 `GLB-MGR-E01/E02/E05/E07/E08/E09/E10/E12/E13/E14/E15/E16/E17` 位于 `coverage/glb-machine-manager/`。 +当前状态:MGR-21 文件管理与恢复 verified;MGR-20 机型程序一致性 verified;MGR-22 文件选择器取消 verified;MGR-19 对话框选择机型/整机保存已 verified;MGR-17、MGR-18 已 verified;MGR-01 至 MGR-16 已完成历史验收,MILL700 实测覆盖照明、Z-up、132 节点树、拖放、节点原点、关节局部坐标系、节点相对父级平移旋转、模型删除、轴绑定、XYZ/AC 层级运动和 WebGL 资源降级。最新台账 R15,证据 `GLB-MGR-E01/E02/E05/E07/E08/E09/E10/E12/E13/E14/E15/E16/E17/E18` 位于 `coverage/glb-machine-manager/`。 文档: diff --git a/scripts/test_machine_manager_workbench.mjs b/scripts/test_machine_manager_workbench.mjs index 345f4d9..2234803 100644 --- a/scripts/test_machine_manager_workbench.mjs +++ b/scripts/test_machine_manager_workbench.mjs @@ -14,6 +14,7 @@ const choose = async (name) => { await c('machine-manager-tab-structure').click( try { await page.goto(report.url); await page.waitForFunction(() => document.querySelector('main.axis-app')?.dataset.axisProgramLoading === 'false' && document.querySelector('main.axis-app')?.dataset.axisStateSource === 'linuxcnc-wasm'); await c('machine-manager-settings').click(); assert.equal(await page.locator('.machine-preview-empty').isVisible(), true); + const fileDialog = page.locator('.machine-manager-dialog'); const chooser = page.waitForEvent('filechooser'); await c('machine-manager-import').click(); await chooser; await fileDialog.dispatchEvent('cancel'); assert.equal(await fileDialog.isVisible(), true); await page.evaluate(() => window.dispatchEvent(new Event('focus'))); await page.waitForTimeout(20); report.checks.push('Open Files Cancel leaves the machine manager dialog open'); const bytes = await readFile(new URL('../MILL700-U_1.glb', import.meta.url)); const upload = { name: 'MILL700-U_1.glb', mimeType: 'model/gltf-binary', buffer: bytes }; await c('machine-manager-files').setInputFiles(upload); await page.waitForFunction(() => document.querySelectorAll('[data-axis-control="machine-tree-node"]').length >= 132); diff --git a/web/react/src/DraggableDialog.tsx b/web/react/src/DraggableDialog.tsx index 276f1be..7775421 100644 --- a/web/react/src/DraggableDialog.tsx +++ b/web/react/src/DraggableDialog.tsx @@ -1,10 +1,10 @@ import { useEffect, useId, useLayoutEffect, useRef, type PointerEvent, type ReactNode } from "react"; -type Props = { open: boolean; title: string; className?: string; onClose: () => void; children: ReactNode }; +type Props = { open: boolean; title: string; className?: string; onClose: () => void; cancelGuard?: () => boolean; children: ReactNode }; // Native dialog provides focus containment, inert background, Escape and focus return. // Keeping children mounted preserves drafts and pending import results when closed. -export function DraggableDialog({ open, title, className = "", onClose, children }: Props) { +export function DraggableDialog({ open, title, className = "", onClose, cancelGuard, children }: Props) { const ref = useRef(null); const titleId = useId(); const drag = useRef<{ id: number; x: number; y: number; left: number; top: number } | null>(null); @@ -40,7 +40,7 @@ export function DraggableDialog({ open, title, className = "", onClose, children position(from.left + event.clientX - from.x, from.top + event.clientY - from.y); }; const stop = () => { drag.current = null; }; - return { event.preventDefault(); onClose(); }} onKeyDown={(event) => { + return { event.preventDefault(); if (!cancelGuard?.()) onClose(); }} onKeyDown={(event) => { if (event.key !== "Tab") return; const elements = [...event.currentTarget.querySelectorAll('button:not(:disabled), a[href], input:not(:disabled):not([type="hidden"]), select:not(:disabled), textarea:not(:disabled), summary, [tabindex="0"]')].filter((element) => element.getClientRects().length > 0); const first = elements[0], last = elements.at(-1); diff --git a/web/react/src/MachineImportPanel.tsx b/web/react/src/MachineImportPanel.tsx index 41764bd..414efcd 100644 --- a/web/react/src/MachineImportPanel.tsx +++ b/web/react/src/MachineImportPanel.tsx @@ -8,6 +8,8 @@ export function MachineImportPanel({ model, disabled, onImport }: { model: Impor const file = useRef(null); const [open, setOpen] = useState(false); const [message, setMessage] = useState(""); + const filePickerOpen = useRef(false); + const openFilePicker = () => { if (disabled || !file.current) return; filePickerOpen.current = true; file.current.click(); const clear = () => { window.removeEventListener("focus", clear); setTimeout(() => { filePickerOpen.current = false; }, 0); }; window.addEventListener("focus", clear, { once: true }); }; const run = async (source: File | string) => { if (disabled) return; setMessage("正在读取机床结构…"); @@ -17,10 +19,10 @@ export function MachineImportPanel({ model, disabled, onImport }: { model: Impor const s = model?.structure; return
- setOpen(false)}> + filePickerOpen.current} onClose={() => setOpen(false)}>
三维机床{s ? `${s.coordinates} · ${model.filename}` : "内置模型"}
- + {examples.map(({ id, label, filename }) => 下载 {label.replace(" 示例", "")})}
{s &&
{`底座 → X → Y → Z${s.profile === "trt-xyz-gantry/1" ? "" : " → 主轴箱"};底座 → ${s.coordinates[3]} → C`}

旋转中心 (mm):{["x", "y", "z"].map((c) => s.offsets[`${c}_rot_point`].toFixed(3)).join(" / ")}

轴间偏置 (mm):{["x", "y", "z"].map((c) => s.offsets[`${c}_offset`].toFixed(3)).join(" / ")}

运行限位、速度、回零和周期使用匹配的 INI;模型行程已校验。

} diff --git a/web/react/src/MachineManagerPanel.tsx b/web/react/src/MachineManagerPanel.tsx index 93f3e48..54a3e1a 100644 --- a/web/react/src/MachineManagerPanel.tsx +++ b/web/react/src/MachineManagerPanel.tsx @@ -156,7 +156,13 @@ export function MachineManagerPanel({ coordinates: controllerCoordinates, disabl const setCOriginMm = (value: Vec3) => edit((current) => ({ ...current, cOriginMm: value })); const setTiltOffsetMm = (value: Vec3) => edit((current) => ({ ...current, tiltOffsetMm: value })); const setPhysicalZero = (value: PhysicalZero) => edit((current) => ({ ...current, physicalZero: value })); - const file = useRef(null), openFile = useRef(null); + const file = useRef(null), openFile = useRef(null), filePickerOpen = useRef(false); + const beginFilePicker = (input: HTMLInputElement | null) => { + if (!input || pending || followLive) return; + filePickerOpen.current = true; input.click(); + const clear = () => { window.removeEventListener("focus", clear); setTimeout(() => { filePickerOpen.current = false; }, 0); }; + window.addEventListener("focus", clear, { once: true }); + }; const documentInfo = draft.documentInfo ?? { name: "", description: "" }; const [library, setLibrary] = useState([]), [libraryError, setLibraryError] = useState(""); const [saveLocation, setSaveLocation] = useState("尚未保存文件"), [savedSignature, setSavedSignature] = useState(""); @@ -349,19 +355,19 @@ export function MachineManagerPanel({ coordinates: controllerCoordinates, disabl const renderPartTree = (part: MachinePartDraft): React.ReactNode => child.parentId === part.id).map(renderPartTree)} />; return
- setOpen(false)}> + filePickerOpen.current} onClose={() => setOpen(false)}>
2 · 结构与关节 → 3 · 验证与标定 → 4 · 保存 GLB{coordinates === "XYZAC" ? "A 绕 X 倾转,C 绕 Z 回转" : "B 绕 Y 倾转,C 绕 Z 回转"} · 控制面板当前 {controllerCoordinates}{coordinates !== controllerCoordinates ? ";应用或导入模型时切换" : ""}
-
{parts.length} 个模型 · {bindings.filter((b) => coordinates.includes(b.dof.axis)).length}/5 轴已绑定{!parts.length ? "新建机床" : savedSignature === signature ? "与保存版本一致" : applied === signature ? "已应用当前配置" : exported === signature ? "已导出 · 尚未应用" : "配置尚未应用"}
+
{parts.length} 个模型 · {bindings.filter((b) => coordinates.includes(b.dof.axis)).length}/5 轴已绑定{!parts.length ? "新建机床" : savedSignature === signature ? "与保存版本一致" : applied === signature ? "已应用当前配置" : exported === signature ? "已导出 · 尚未应用" : "配置尚未应用"}
{[...coordinates].map((axis) => { const binding = bindings.find((b) => b.dof.axis === axis); return ; })}
{([ ["fit", "整机适配"], ["selected", "定位选中"], ["front", "正视"], ["top", "俯视"] ] as const).map(([action, label]) => )}Z ↑ · X 红 / Y 绿 / Z 蓝
{open && } - {!parts.length &&
建立机床的数字装配

选择机床类型 → 导入 GLB → 绑定关节 → 保存机床模型

支持完整层级 · 单文件不超过 32 MiB{library.length > 0 && }
} + {!parts.length &&
建立机床的数字装配

选择机床类型 → 导入 GLB → 绑定关节 → 保存机床模型

支持完整层级 · 单文件不超过 32 MiB{library.length > 0 && }
}

左键旋转 · 滚轮缩放 · 右键平移 · 选中关节显示局部坐标系

{([ ["structure", "结构与关节"], ["motion", "运动验证"], ["calibration", "机床标定"], ["files", "文件与恢复"] ] as const).map(([id, label], index) => )}
diff --git a/web/react/src/TrtGeometryPanel.tsx b/web/react/src/TrtGeometryPanel.tsx index 59e2124..ceb9e22 100644 --- a/web/react/src/TrtGeometryPanel.tsx +++ b/web/react/src/TrtGeometryPanel.tsx @@ -12,6 +12,8 @@ export function TrtGeometryPanel({ bundle, disabled, busy, onApply, onReload }: const [message, setMessage] = useState(""); const [pending, setPending] = useState(false); const file = useRef(null); + const filePickerOpen = useRef(false); + const openFilePicker = () => { if (locked || !file.current) return; filePickerOpen.current = true; file.current.click(); const clear = () => { window.removeEventListener("focus", clear); setTimeout(() => { filePickerOpen.current = false; }, 0); }; window.addEventListener("focus", clear, { once: true }); }; const geometry = useMemo(() => bundle ? readTrtGeometry(bundle) : null, [bundle]); useEffect(() => { setValues(Object.fromEntries(TRT_GEOMETRY_KEYS.map((key: string) => [key, String(geometry?.offsets[key] ?? 0)]))); @@ -50,13 +52,13 @@ export function TrtGeometryPanel({ bundle, disabled, busy, onApply, onReload }: const tool = (id: string, label: string, asset: string, action: () => void, off = locked) => ; return
- setOpen(false)}> + filePickerOpen.current} onClose={() => setOpen(false)}>
{bundle?.entrypoint.split("/").at(-1) ?? "INI"}mm{dirty ? "待应用" : "已生效"}
{["旋转中心", "轴间偏置"].map((title, index) =>
{title}{TRT_GEOMETRY_KEYS.slice(index * 3, index * 3 + 3).map((key: string) => )}
)}