Add INI-driven XYZAC and XYZBC parametric machine design

This commit is contained in:
2026-09-17 19:16:48 +08:00
parent 9232f29027
commit e4600b34e3
31 changed files with 2359 additions and 29 deletions

View File

@@ -0,0 +1,69 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { readTrtGeometry, writeTrtGeometry, TRT_GEOMETRY_KEYS } from "../../workspace/trt-geometry.mjs";
import { axisIcon } from "./sourceAssets";
type Bundle = { entrypoint: string; files: Record<string, string>; source_ini?: string };
type Props = { bundle: Bundle | null; disabled: boolean; busy: boolean; onApply: (bundle: Bundle) => Promise<void>; onReload: () => Promise<void> };
export function TrtGeometryPanel({ bundle, disabled, busy, onApply, onReload }: Props) {
const [values, setValues] = useState<Record<string, string>>({});
const [message, setMessage] = useState("");
const [pending, setPending] = useState(false);
const file = useRef<HTMLInputElement>(null);
const geometry = useMemo(() => bundle ? readTrtGeometry(bundle) : null, [bundle]);
useEffect(() => {
setValues(Object.fromEntries(TRT_GEOMETRY_KEYS.map((key: string) => [key, String(geometry?.offsets[key] ?? 0)])));
setMessage("");
}, [bundle]);
const locked = disabled || busy || pending || !bundle;
const dirty = TRT_GEOMETRY_KEYS.some((key: string) => values[key] !== String(geometry?.offsets[key] ?? 0));
const run = async (action: () => Promise<void>, success: string) => {
setPending(true); setMessage("");
try { await action(); setMessage(success); }
catch (error) { setMessage(error instanceof Error ? error.message : String(error)); }
finally { setPending(false); }
};
const edited = () => {
if (!bundle) throw new Error("INI unavailable");
if (Object.values(geometry?.sources ?? {}).some((source) => source.path !== bundle.entrypoint)) throw new Error("参数位于包含文件,请修改源文件后重新读取 INI");
const next = writeTrtGeometry(bundle, values);
// Export the native INI, retaining LIB:basic_sim.tcl instead of the Web fixture.
next.source_ini = writeTrtGeometry({ ...bundle, files: { ...bundle.files, [bundle.entrypoint]: bundle.source_ini ?? bundle.files[bundle.entrypoint] } }, values).files[bundle.entrypoint];
return next;
};
const exportIni = async () => {
const next = edited();
const content = next.source_ini ?? next.files[next.entrypoint];
const name = next.entrypoint.split("/").at(-1)!;
const picker = (window as unknown as { showSaveFilePicker?: (options: unknown) => Promise<{ createWritable: () => Promise<{ write: (data: string) => Promise<void>; close: () => Promise<void> }> }> }).showSaveFilePicker;
if (picker) {
const handle = await picker({ suggestedName: name, types: [{ description: "LinuxCNC INI", accept: { "text/plain": [".ini"] } }] });
const stream = await handle.createWritable(); await stream.write(content); await stream.close();
} else {
const url = URL.createObjectURL(new Blob([content], { type: "text/plain" }));
const link = document.createElement("a"); link.href = url; link.download = name; link.click();
setTimeout(() => URL.revokeObjectURL(url), 1000);
}
};
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}>
<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())}
<button type="button" data-axis-control="geometry-save" disabled={locked} onClick={() => void run(exportIni, "INI 已保存或导出")}>保存 INI</button>
<output role="status">{message || (disabled ? "关机且程序空闲后可修改" : "")}</output>
</footer>
<input ref={file} type="file" accept=".ini" hidden onChange={(event) => {
const selected = event.target.files?.[0]; event.target.value = "";
if (!selected || !bundle || locked) return;
void run(async () => {
if (selected.name !== bundle.entrypoint.split("/").at(-1)) throw new Error("INI 文件名与机型不一致");
const imported = readTrtGeometry({ ...bundle, files: { ...bundle.files, [bundle.entrypoint]: await selected.text() } });
setValues(Object.fromEntries(TRT_GEOMETRY_KEYS.map((key: string) => [key, String(imported.offsets[key])])));
}, "INI 参数已导入,待应用");
}} />
</section>;
}