Files
linuxcncWebWASM/web/react/src/RtcpProgramLibrary.tsx

97 lines
7.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>;
}