Add AC/BC RTCP programs and 3D machine manager
This commit is contained in:
96
web/react/src/RtcpProgramLibrary.tsx
Normal file
96
web/react/src/RtcpProgramLibrary.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import catalogJson from "./rtcp-program-manifest.json";
|
||||
import { DraggableDialog } from "./DraggableDialog";
|
||||
|
||||
export type RtcpProgram = {
|
||||
id: string;
|
||||
group: string;
|
||||
label: string;
|
||||
path: string;
|
||||
coordinates: string[];
|
||||
role: "main" | "support" | "reference";
|
||||
features: string[];
|
||||
note: string;
|
||||
};
|
||||
|
||||
type Catalog = {
|
||||
schema: string;
|
||||
total: number;
|
||||
groups: Array<{ id: string; label: string; count: number }>;
|
||||
programs: RtcpProgram[];
|
||||
};
|
||||
|
||||
export const RTCP_PROGRAM_CATALOG = catalogJson as Catalog;
|
||||
|
||||
const roleLabel = (role: RtcpProgram["role"]) => role === "main" ? "主程序" : role === "support" ? "支持/子程序" : "参考程序";
|
||||
const assetUrl = (path: string) => `${import.meta.env.BASE_URL}${path}`;
|
||||
const machineLabel = (coordinates: string) => coordinates === "XYZAC" ? "AC(XYZAC)" : coordinates === "XYZBC" ? "BC(XYZBC)" : coordinates;
|
||||
|
||||
export function RtcpProgramLibrary({ coordinates, disabled, disabledReason, onLoad }: {
|
||||
coordinates: string;
|
||||
disabled: boolean;
|
||||
disabledReason: string;
|
||||
onLoad: (program: RtcpProgram, source: string) => Promise<void>;
|
||||
}) {
|
||||
const defaultProgram = RTCP_PROGRAM_CATALOG.programs.find((program) => program.role === "main" && program.coordinates.includes(coordinates)) ?? RTCP_PROGRAM_CATALOG.programs[0];
|
||||
const [open, setOpen] = useState(false);
|
||||
const [scope, setScope] = useState<"machine" | "all">("machine");
|
||||
const [selectedId, setSelectedId] = useState(defaultProgram.id);
|
||||
const [source, setSource] = useState("");
|
||||
const [sourcePath, setSourcePath] = useState("");
|
||||
const [message, setMessage] = useState("");
|
||||
const [pending, setPending] = useState(false);
|
||||
const selected = RTCP_PROGRAM_CATALOG.programs.find((program) => program.id === selectedId) ?? defaultProgram;
|
||||
const compatible = selected.role === "main" && selected.coordinates.includes(coordinates);
|
||||
const machinePrograms = useMemo(() => RTCP_PROGRAM_CATALOG.programs.filter((program) => program.coordinates.includes(coordinates)), [coordinates]);
|
||||
const visiblePrograms = scope === "machine" ? machinePrograms : RTCP_PROGRAM_CATALOG.programs;
|
||||
const grouped = RTCP_PROGRAM_CATALOG.groups.map((group) => ({ ...group, programs: visiblePrograms.filter((program) => program.group === group.id) })).filter((group) => group.programs.length > 0);
|
||||
useEffect(() => {
|
||||
setScope("machine"); setSelectedId(defaultProgram.id); setSource(""); setSourcePath(""); setMessage("");
|
||||
}, [coordinates, defaultProgram.id]);
|
||||
|
||||
const readSource = async () => {
|
||||
if (sourcePath === selected.path && source) return source;
|
||||
const response = await fetch(assetUrl(selected.path));
|
||||
if (!response.ok) throw new Error(`程序资源读取失败: HTTP ${response.status}`);
|
||||
const text = (await response.text()).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
||||
if (!text.trim()) throw new Error("程序资源为空");
|
||||
setSource(text);
|
||||
setSourcePath(selected.path);
|
||||
return text;
|
||||
};
|
||||
const run = async (action: "source" | "load") => {
|
||||
setPending(true); setMessage(action === "source" ? "正在读取 LinuxCNC 源程序…" : "正在交给 LinuxCNC WASM 解析…");
|
||||
try {
|
||||
const text = await readSource();
|
||||
if (action === "load") { await onLoad(selected, text); setMessage("程序已加载到 LinuxCNC WASM 控制器"); }
|
||||
else setMessage(`已读取 ${text.split("\n").length} 行`);
|
||||
} catch (error) { setMessage(error instanceof Error ? error.message : String(error)); }
|
||||
finally { setPending(false); }
|
||||
};
|
||||
const compatibility = compatible
|
||||
? `${coordinates} 当前机型可加载;运动由 LinuxCNC WASM 解释器与运动学执行。`
|
||||
: selected.role !== "main"
|
||||
? `${roleLabel(selected.role)}用于 remap、O-call 或算法查阅,不作为独立主程序加载。`
|
||||
: `需要 ${selected.coordinates.join("/")} 运动学;当前机型为 ${coordinates}。`;
|
||||
|
||||
return <section className="rtcp-program-library" aria-label={`${machineLabel(coordinates)} RTCP 程序库`} data-rtcp-machine-family={coordinates === "XYZAC" ? "AC" : coordinates === "XYZBC" ? "BC" : coordinates}>
|
||||
<button type="button" data-axis-control="workspace-rtcp-programs" aria-haspopup="dialog" onClick={() => setOpen(true)}>{machineLabel(coordinates)} RTCP 程序 ({machinePrograms.length})</button>
|
||||
<DraggableDialog open={open} title={`${machineLabel(coordinates)} LinuxCNC RTCP 程序库`} className="rtcp-program-dialog" onClose={() => setOpen(false)}>
|
||||
<header className="rtcp-program-summary"><strong>{machineLabel(coordinates)} 程序</strong><span>当前分类:{machinePrograms.length} 个</span><span>可直接加载:{machinePrograms.filter((program) => program.role === "main").length}</span><span>完整源程序:{RTCP_PROGRAM_CATALOG.total} 个</span></header>
|
||||
<label className="rtcp-program-scope">显示范围<select data-axis-control="rtcp-program-scope" value={scope} onChange={(event) => { const next = event.target.value as "machine" | "all"; setScope(next); if (next === "machine" && !selected.coordinates.includes(coordinates)) setSelectedId(defaultProgram.id); setSource(""); setSourcePath(""); setMessage(""); }}><option value="machine">当前 {machineLabel(coordinates)} 机床</option><option value="all">全部 32 个源程序</option></select></label>
|
||||
<label className="rtcp-program-select">程序<select data-axis-control="rtcp-program-select" value={selected.id} onChange={(event) => { setSelectedId(event.target.value); setSource(""); setSourcePath(""); setMessage(""); }}>
|
||||
{grouped.map((group) => <optgroup key={group.id} label={`${group.label} (${group.programs.length})`}>{group.programs.map((program) => <option key={program.id} value={program.id}>{program.label} · {roleLabel(program.role)}</option>)}</optgroup>)}
|
||||
</select></label>
|
||||
<dl className="rtcp-program-meta"><div><dt>类型</dt><dd>{roleLabel(selected.role)}</dd></div><div><dt>机型</dt><dd>{selected.coordinates.join(" / ")}</dd></div><div><dt>特点</dt><dd>{selected.features.join(" · ")}</dd></div><div><dt>源路径</dt><dd><code>{selected.path}</code></dd></div></dl>
|
||||
<p>{selected.note}</p><p className={compatible ? "rtcp-compatible" : "rtcp-incompatible"}>{compatibility}</p>
|
||||
<div className="rtcp-program-actions">
|
||||
<button type="button" data-axis-control="rtcp-program-source" disabled={pending} onClick={() => void run("source")}>查看源程序</button>
|
||||
<button type="button" data-axis-control="rtcp-program-load" disabled={pending || disabled || !compatible} title={disabled ? disabledReason : !compatible ? compatibility : undefined} onClick={() => void run("load")}>加载到控制器</button>
|
||||
<a data-axis-control="rtcp-program-download" href={assetUrl(selected.path)} download={selected.path.split("/").at(-1)}>下载 .ngc</a>
|
||||
</div>
|
||||
<output role="status">{message}</output>
|
||||
{source && sourcePath === selected.path && <textarea className="rtcp-program-source" readOnly value={source} aria-label={`${selected.label} 源程序`} spellCheck={false} />}
|
||||
</DraggableDialog>
|
||||
</section>;
|
||||
}
|
||||
Reference in New Issue
Block a user