Compare commits
2 Commits
3cb5950d1f
...
b15af28034
| Author | SHA1 | Date | |
|---|---|---|---|
| b15af28034 | |||
| d4d0088daf |
|
Before Width: | Height: | Size: 155 KiB After Width: | Height: | Size: 155 KiB |
@@ -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",
|
||||
|
||||
|
Before Width: | Height: | Size: 204 KiB After Width: | Height: | Size: 187 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 97 KiB |
@@ -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。
|
||||
|
||||
@@ -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|三维管理、机床导入、几何参数三类文件选择器取消/回焦不关闭父对话框;没有文件时保持配置和焦点|
|
||||
|
||||
@@ -149,10 +149,10 @@
|
||||
|
||||
|文件(相对 coverage/glb-machine-manager/machine-type)|SHA-256|
|
||||
|---|---|
|
||||
|`browser.report.json`|`222c43f2ef36d28f31171773efe2cbe49eff383e766c5086f18bac5bae62047a`|
|
||||
|`browser.report.json`|`5f45884a75acf14d7a3ecff6a9a50b306842216cb2c26f91d44a781aff3f0073`|
|
||||
|`dialog-type.png`|`c24f8b3fd445cf5e886b8189e13e0830e6fecf341e57499e7cf73528fd803b9d`|
|
||||
|`controller-import.png`|`071f8d9a9ac4ac85fe64bfcd51f1fb605112515eb4aaa0e00ceacd539347d67b`|
|
||||
|`mill700/browser.report.json`|`05fb403ef854aba37c1ab64e0334a843d4839a6c2ce559031d25589a68346f3a`|
|
||||
|`mill700/browser.report.json`|`7f5c87b1a2d5491242aa87248d0cb4085e5d849065520ad56e4e64c6453216b2`|
|
||||
|`saved-models/MILL700-xyzac.glb`|`25ba366a6550c3a613a63623b8e25216d5a3a6dd2781b69481d80d0bb87033e4`|
|
||||
|`saved-models/MILL700-xyzbc.glb`|`3b87cb8504815c13127608c513eacb3536e1be1c10f9a58fcd59920939fbe0c1`|
|
||||
|
||||
@@ -166,7 +166,7 @@
|
||||
|
||||
|证据(相对 program-compatibility)|SHA-256|
|
||||
|---|---|
|
||||
|`browser.report.json`|`c931be222b8c3f541174c26f9892083481f9dfe5c75a372e3d20b222b8a9e89f`|
|
||||
|`browser.report.json`|`5f45884a75acf14d7a3ecff6a9a50b306842216cb2c26f91d44a781aff3f0073`|
|
||||
|`worker.report.json`|`986fb9e127894aaf477ac6e5879c06b7084a112c840549532d39fdd6d3e8694e`|
|
||||
|`catalog.report.json`|`ff2e32be3b006e2d8de902012ed89724aada54c425503027ae5e027eaf9cafe2`|
|
||||
|`replace.report.json`|`fe92778b144b226cea1e91393d10e4905cfd5becf56ba9b3021b5842514c8aa7`|
|
||||
@@ -187,8 +187,14 @@
|
||||
|文件(相对 document)|SHA-256|
|
||||
|---|---|
|
||||
|`browser.report.json`|`5f45884a75acf14d7a3ecff6a9a50b306842216cb2c26f91d44a781aff3f0073`|
|
||||
|`files-and-recovery.png`|`ae48e693668a8e100b81fa2cbfe1a6d4dbc21600a230994f73f2d85d2876e389`|
|
||||
|`saved-eight-parts.glb`|`f155f46e6d5566e407a2f708a66ba61332366fb2ba5007b01ac99ed04494769e`|
|
||||
|`files-and-recovery.png`|`cef9836f21a56b79916ea0b206e3013553688f988947b701ac68332dffb93af4`|
|
||||
|`saved-eight-parts.glb`|`57e268f7278119493004848b97262a1042ada6f81f30103b72f2fb0a8db66456`|
|
||||
|`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`。
|
||||
|
||||
@@ -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/`。
|
||||
|
||||
文档:
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<HTMLDialogElement>(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 <dialog ref={ref} className={`machine-dialog ${className}`} aria-modal="true" aria-labelledby={titleId} onCancel={(event) => { event.preventDefault(); onClose(); }} onKeyDown={(event) => {
|
||||
return <dialog ref={ref} className={`machine-dialog ${className}`} aria-modal="true" aria-labelledby={titleId} onCancel={(event) => { event.preventDefault(); if (!cancelGuard?.()) onClose(); }} onKeyDown={(event) => {
|
||||
if (event.key !== "Tab") return;
|
||||
const elements = [...event.currentTarget.querySelectorAll<HTMLElement>('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);
|
||||
|
||||
@@ -8,6 +8,8 @@ export function MachineImportPanel({ model, disabled, onImport }: { model: Impor
|
||||
const file = useRef<HTMLInputElement>(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 <section className="machine-import" aria-label="GLB 机床导入" data-machine-import={model?.hash ?? "builtin"}>
|
||||
<div className="machine-import-toolbar"><button type="button" data-axis-control="machine-glb-settings" aria-haspopup="dialog" onClick={() => setOpen(true)}>导入机床模型</button></div>
|
||||
<DraggableDialog open={open} title="三维机床模型" className="model-dialog" onClose={() => setOpen(false)}>
|
||||
<DraggableDialog open={open} title="三维机床模型" className="model-dialog" cancelGuard={() => filePickerOpen.current} onClose={() => setOpen(false)}>
|
||||
<header><strong>三维机床</strong><span>{s ? `${s.coordinates} · ${model.filename}` : "内置模型"}</span></header>
|
||||
<div className="machine-import-actions">
|
||||
<button type="button" data-axis-control="machine-glb-import" disabled={disabled} onClick={() => file.current?.click()}>导入 GLB</button>
|
||||
<button type="button" data-axis-control="machine-glb-import" disabled={disabled} onClick={openFilePicker}>导入 GLB</button>
|
||||
{examples.map(({ id, label, filename }) => <span key={id}><button type="button" data-axis-control={`machine-glb-${id}`} disabled={disabled} onClick={() => void run(`${import.meta.env.BASE_URL}machines/${filename}`)}>{label}</button><a href={`${import.meta.env.BASE_URL}machines/${filename}`} download>下载 {label.replace(" 示例", "")}</a></span>)}
|
||||
</div>
|
||||
{s && <details data-machine-structure><summary>{`底座 → X → Y → Z${s.profile === "trt-xyz-gantry/1" ? "" : " → 主轴箱"};底座 → ${s.coordinates[3]} → C`}</summary><p>旋转中心 (mm):{["x", "y", "z"].map((c) => s.offsets[`${c}_rot_point`].toFixed(3)).join(" / ")}</p><p>轴间偏置 (mm):{["x", "y", "z"].map((c) => s.offsets[`${c}_offset`].toFixed(3)).join(" / ")}</p><p>运行限位、速度、回零和周期使用匹配的 INI;模型行程已校验。</p></details>}
|
||||
|
||||
@@ -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<HTMLInputElement>(null), openFile = useRef<HTMLInputElement>(null);
|
||||
const file = useRef<HTMLInputElement>(null), openFile = useRef<HTMLInputElement>(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<SavedMachine[]>([]), [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 => <TreeNode key={part.id} part={part} node={trees.get(part.id)!} selected={`${selectedId}:${selectedNodePath}`} onSelect={selectTreeNode} onDrop={reparentTreeNode} query={query} expansion={expansion} locked={pending || followLive} extra={parts.filter((child) => child.parentId === part.id).map(renderPartTree)} />;
|
||||
return <section className="machine-manager" aria-label="三维机床管理">
|
||||
<button type="button" data-axis-control="machine-manager-settings" aria-haspopup="dialog" onClick={() => setOpen(true)}>三维机床管理</button>
|
||||
<DraggableDialog open={open} title={`三维机床管理 · ${coordinates}`} className="machine-manager-dialog" onClose={() => setOpen(false)}>
|
||||
<DraggableDialog open={open} title={`三维机床管理 · ${coordinates}`} className="machine-manager-dialog" cancelGuard={() => filePickerOpen.current} onClose={() => setOpen(false)}>
|
||||
<div className="machine-manager-workbench" aria-busy={pending}>
|
||||
<div className="machine-type-step"><label><strong>1 · 机床类型</strong><select data-axis-control="machine-manager-type" aria-label="设计机床类型" value={coordinates} disabled={pending || followLive} onChange={(event) => {
|
||||
const next = event.target.value as "XYZAC" | "XYZBC";
|
||||
edit((draft) => changeMachineType(draft, next)); setTab("structure");
|
||||
setMessage(`设计机型已选择 ${next};保留模型层级和 XYZ/C 绑定,请重新配置 ${next[3]} 轴并核对标定。可撤销恢复。`);
|
||||
}}><option value="XYZAC">XYZAC · A/C 五轴转台</option><option value="XYZBC">XYZBC · B/C 五轴转台</option></select></label><div><span>2 · 结构与关节 → 3 · 验证与标定 → 4 · 保存 GLB</span><small data-axis-control="machine-manager-type-note">{coordinates === "XYZAC" ? "A 绕 X 倾转,C 绕 Z 回转" : "B 绕 Y 倾转,C 绕 Z 回转"} · 控制面板当前 {controllerCoordinates}{coordinates !== controllerCoordinates ? ";应用或导入模型时切换" : ""}</small></div></div>
|
||||
<div className="machine-manager-toolbar"><button type="button" data-axis-control="machine-document-new" disabled={pending || followLive} onClick={newDocument}>新建机床</button><button type="button" data-axis-control="machine-document-open" disabled={pending || followLive} onClick={requestOpen}>打开已保存机床</button><button type="button" data-axis-control="machine-document-from-controller" disabled={!currentMachine?.scene.userData.linuxcncJointModel || pending || followLive} onClick={() => void editControllerModel()}>编辑控制面板机床</button><button type="button" data-axis-control="machine-manager-import" disabled={pending || followLive} onClick={() => file.current?.click()}>导入 GLB 部件</button><button type="button" data-axis-control="machine-manager-undo" disabled={!history.past.length || pending || followLive} onClick={() => changeHistory("undo")}>撤销</button><button type="button" data-axis-control="machine-manager-redo" disabled={!history.future.length || pending || followLive} onClick={() => changeHistory("redo")}>重做</button><span>{parts.length} 个模型 · {bindings.filter((b) => coordinates.includes(b.dof.axis)).length}/5 轴已绑定</span><span className="machine-save-state" data-axis-control="machine-manager-draft-state">{!parts.length ? "新建机床" : savedSignature === signature ? "与保存版本一致" : applied === signature ? "已应用当前配置" : exported === signature ? "已导出 · 尚未应用" : "配置尚未应用"}</span></div>
|
||||
<div className="machine-manager-toolbar"><button type="button" data-axis-control="machine-document-new" disabled={pending || followLive} onClick={newDocument}>新建机床</button><button type="button" data-axis-control="machine-document-open" disabled={pending || followLive} onClick={() => beginFilePicker(openFile.current)}>打开已保存机床</button><button type="button" data-axis-control="machine-document-from-controller" disabled={!currentMachine?.scene.userData.linuxcncJointModel || pending || followLive} onClick={() => void editControllerModel()}>编辑控制面板机床</button><button type="button" data-axis-control="machine-manager-import" disabled={pending || followLive} onClick={() => beginFilePicker(file.current)}>导入 GLB 部件</button><button type="button" data-axis-control="machine-manager-undo" disabled={!history.past.length || pending || followLive} onClick={() => changeHistory("undo")}>撤销</button><button type="button" data-axis-control="machine-manager-redo" disabled={!history.future.length || pending || followLive} onClick={() => changeHistory("redo")}>重做</button><span>{parts.length} 个模型 · {bindings.filter((b) => coordinates.includes(b.dof.axis)).length}/5 轴已绑定</span><span className="machine-save-state" data-axis-control="machine-manager-draft-state">{!parts.length ? "新建机床" : savedSignature === signature ? "与保存版本一致" : applied === signature ? "已应用当前配置" : exported === signature ? "已导出 · 尚未应用" : "配置尚未应用"}</span></div>
|
||||
<div className="machine-axis-overview" aria-label="数控轴绑定总览">{[...coordinates].map((axis) => { const binding = bindings.find((b) => b.dof.axis === axis); return <button type="button" key={axis} data-axis-control={`machine-axis-locate-${axis.toLowerCase()}`} className={binding ? "bound" : "missing"} onClick={() => moveToBinding(axis)} title={binding ? `${binding.name} · 父级 ${binding.dof.direction.toUpperCase()} ${binding.dof.sign > 0 ? "+" : "−"}` : `待绑定 ${axis} 轴`}><strong>{axis}</strong><span>{binding?.name ?? "待绑定"}</span><small>{binding ? `${binding.dof.sign > 0 ? "+" : "−"}${binding.dof.direction.toUpperCase()}` : "选择节点"}</small></button>; })}</div>
|
||||
<div className="machine-manager-layout">
|
||||
<section className="machine-preview-area" aria-label="机床三维预览"><div className="machine-preview-tools">{([ ["fit", "整机适配"], ["selected", "定位选中"], ["front", "正视"], ["top", "俯视"] ] as const).map(([action, label]) => <button key={action} type="button" data-axis-control={`machine-view-${action}`} disabled={!parts.length || (action === "selected" && !selected)} onClick={() => setCommand((current) => ({ action, serial: current.serial + 1 }))}>{label}</button>)}<small>Z ↑ · X 红 / Y 绿 / Z 蓝</small></div>
|
||||
{open && <MachineManagerPanelPreview command={command} coordinates={coordinates} parts={parts} selectedId={selectedId} selectedNodePath={selectedNodePath} testPose={effectivePose} onMotionState={receiveMotionState} />}
|
||||
{!parts.length && <div className="machine-preview-empty"><strong>建立机床的数字装配</strong><p>选择机床类型 → 导入 GLB → 绑定关节 → 保存机床模型</p><button type="button" disabled={pending} onClick={() => file.current?.click()}>导入机床或部件</button><small>支持完整层级 · 单文件不超过 32 MiB</small>{library.length > 0 && <button type="button" onClick={() => setTab("files")}>恢复本地机床库({library.length} 个版本)</button>}</div>}
|
||||
{!parts.length && <div className="machine-preview-empty"><strong>建立机床的数字装配</strong><p>选择机床类型 → 导入 GLB → 绑定关节 → 保存机床模型</p><button type="button" disabled={pending} onClick={() => beginFilePicker(file.current)}>导入机床或部件</button><small>支持完整层级 · 单文件不超过 32 MiB</small>{library.length > 0 && <button type="button" onClick={() => setTab("files")}>恢复本地机床库({library.length} 个版本)</button>}</div>}
|
||||
<p className="machine-preview-hint">左键旋转 · 滚轮缩放 · 右键平移 · 选中关节显示局部坐标系</p></section>
|
||||
<div className="machine-manager-editor">
|
||||
<div className="machine-editor-tabs" role="tablist" aria-label="机床配置步骤">{([ ["structure", "结构与关节"], ["motion", "运动验证"], ["calibration", "机床标定"], ["files", "文件与恢复"] ] as const).map(([id, label], index) => <button id={`machine-tab-${id}`} role="tab" aria-selected={tab === id} aria-controls={`machine-panel-${id}`} tabIndex={tab === id ? 0 : -1} data-axis-control={`machine-manager-tab-${id}`} key={id} type="button" onClick={() => setTab(id)} onKeyDown={(event) => { const ids: EditorTab[] = ["structure", "motion", "calibration", "files"]; const next = event.key === "ArrowRight" ? ids[(index + 1) % 4] : event.key === "ArrowLeft" ? ids[(index + 3) % 4] : event.key === "Home" ? ids[0] : event.key === "End" ? ids[3] : null; if (next) { event.preventDefault(); setTab(next); document.getElementById(`machine-tab-${next}`)?.focus(); } }}>{label}</button>)}</div>
|
||||
|
||||
@@ -12,6 +12,8 @@ export function TrtGeometryPanel({ bundle, disabled, busy, onApply, onReload }:
|
||||
const [message, setMessage] = useState("");
|
||||
const [pending, setPending] = useState(false);
|
||||
const file = useRef<HTMLInputElement>(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) => <button type="button" data-axis-control={`geometry-${id}`} aria-label={label} title={label} disabled={off} onClick={action}><img src={axisIcon(asset)} alt="" /></button>;
|
||||
return <section className="trt-geometry" aria-label="机床几何参数" aria-busy={pending || busy} data-dirty={dirty}>
|
||||
<div className="trt-geometry-toolbar"><button type="button" data-axis-control="geometry-settings" aria-haspopup="dialog" onClick={() => setOpen(true)}>机床参数</button></div>
|
||||
<DraggableDialog open={open} title="机床几何参数" className="geometry-dialog" onClose={() => setOpen(false)}>
|
||||
<DraggableDialog open={open} title="机床几何参数" className="geometry-dialog" cancelGuard={() => filePickerOpen.current} onClose={() => setOpen(false)}>
|
||||
<header><strong>{bundle?.entrypoint.split("/").at(-1) ?? "INI"}</strong><span>mm</span><output>{dirty ? "待应用" : "已生效"}</output></header>
|
||||
<div className="trt-geometry-fields">{["旋转中心", "轴间偏置"].map((title, index) => <fieldset key={title}><legend>{title}</legend>{TRT_GEOMETRY_KEYS.slice(index * 3, index * 3 + 3).map((key: string) => <label key={key} title={key === geometry?.unsupported ? "此机型原生运动学不使用该偏置" : key.replaceAll("_", "-")}><span>{key.replaceAll("_", "-")}</span><input aria-label={key.replaceAll("_", "-")} type="number" step="any" disabled={locked || key === geometry?.unsupported} value={values[key] ?? "0"} onChange={(e) => setValues({ ...values, [key]: e.target.value })} /></label>)}</fieldset>)}</div>
|
||||
<footer>
|
||||
<button type="button" data-axis-control="geometry-apply" disabled={locked || !dirty} onClick={() => void run(() => onApply(edited()), "参数已应用,待重新回零")}><img src={axisIcon("tool_verify.gif")} alt="" />应用参数</button>
|
||||
{tool("reload", "重新读取 INI", "tool_reload.gif", () => void run(onReload, "已重新读取 INI"))}
|
||||
{tool("import", "导入 INI 参数", "tool_open.gif", () => file.current?.click())}
|
||||
{tool("import", "导入 INI 参数", "tool_open.gif", openFilePicker)}
|
||||
<button type="button" data-axis-control="geometry-save" disabled={locked} onClick={() => void run(exportIni, "INI 已保存或导出")}>保存 INI</button>
|
||||
<output role="status">{message || (disabled ? "关机且程序空闲后可修改" : "")}</output>
|
||||
</footer>
|
||||
|
||||