Add LinuxCNC five-axis case catalog
This commit is contained in:
34
docs/linuxcnc-five-axis-case-catalog.md
Normal file
34
docs/linuxcnc-five-axis-case-catalog.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# LinuxCNC 五轴机床案例目录
|
||||||
|
|
||||||
|
本目录来自仓库内的 LinuxCNC 源码、仿真配置和 G 代码案例。网页机床管理器读取同一份 [five-axis-machine-catalog.json](../web/workspace/five-axis-machine-catalog.json),因此案例清单、运动学名称、坐标轴、源码路径和 Web/WASM 能力状态不会在界面中重复硬编码。
|
||||||
|
|
||||||
|
## 案例与适用范围
|
||||||
|
|
||||||
|
| 结构族 | LinuxCNC 运动学 | 坐标/关节 | 本地案例 | Web/WASM 状态 |
|
||||||
|
|---|---|---:|---|---|
|
||||||
|
| 转台倾转 AC | `xyzac-trt-kins` | XYZAC / 5 | `configs/sim/axis/vismach/5axis/table-rotary-tilting`、gmoccapy TRT | 已支持正解、逆解、RTCP |
|
||||||
|
| 转台倾转 BC | `xyzbc-trt-kins` | XYZBC / 5 | `configs/sim/axis/vismach/5axis/table-rotary-tilting`、gmoccapy TRT | 已支持正解、逆解、RTCP |
|
||||||
|
| 双转台 AB | `xyzab_tdr_kins` | XYZAB / 5 | `configs/sim/axis/vismach/5axis/table-dual-rotary` | 已有 LinuxCNC 案例;需要 Web/WASM 适配器 |
|
||||||
|
| Bridge Mill | `5axiskins` | XYZBCW/Y / 7 | `configs/sim/axis/vismach/5axis/bridgemill` | 需要支持重复轴和 W 轴的适配器 |
|
||||||
|
| MAX5 | `maxkins` | XYZBC / 6 | `configs/sim/axis/vismach/5axis/max5` | 需要 maxkins WASM 适配器 |
|
||||||
|
| Hexapod | `genhexkins` | XYZABC / 6 | `configs/sim/axis/vismach/hexapod-sim` | 需要并联平台参数和迭代求解适配器 |
|
||||||
|
| Pentapod | `pentakins` | XYZAB / 5 | `configs/sim/axis/pentapod` | 需要五支链参数和迭代求解适配器 |
|
||||||
|
| PUMA | `pumakins` | XYZABC / 6 | `configs/sim/axis/vismach/puma/puma.ini` | 需要串联机器人适配器 |
|
||||||
|
| PUMA 560 | `genserkins` | XYZABC / 6 | `configs/sim/axis/vismach/puma/puma560.ini` | 需要 DH/串联机器人适配器 |
|
||||||
|
| 直角坐标基线 | `trivkins` | XYZAB / 5 | qtdragon、woodpecker、MAX5 trivkins | 仅用于轴映射验证,不提供非线性 RTCP |
|
||||||
|
|
||||||
|
PUMA 和 Hexapod 是六自由度案例,仍然纳入目录是因为 LinuxCNC 的五轴加工/RTCP 工作流可能使用其中的五轴姿态子集;在配置器中会显示“六自由度”标签。SCARA、MELFA、SCORBOT 等同样是 LinuxCNC 的机器人运动学案例,但不属于本目录的五轴机床案例,因此不会伪装成五轴铣床类型。
|
||||||
|
|
||||||
|
## 适配边界
|
||||||
|
|
||||||
|
当前 Web/WASM 运动链已经真实接通的是 XYZAC/XYZBC TRT。其它案例已经进入机床管理器的案例库,可以查看源码配置、G 代码和所需结构参数,但在对应适配器完成以前不能在控制面板中声称已经具备正解、逆解或 RTCP。每个 GLB 机床文档应保存 `kinematics`、`coordinates`、`joints`、轴层级、局部原点、关节参数、物理零点和运动学专用参数,适配器再根据这些数据驱动 Three.js 层级。
|
||||||
|
|
||||||
|
## 校验
|
||||||
|
|
||||||
|
在仓库根目录执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node scripts/validate_five_axis_catalog.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
该命令逐项读取本地案例配置、示例 G 代码、README 和运动学源码,确认目录中的路径仍然存在。官方依据包括 [LinuxCNC 5-Axis Kinematics](https://linuxcnc.org/docs/html/motion/5-axis-kinematics.html)、[kins(9)](https://linuxcnc.org/docs/html/man/man9/kins.9.html) 和 [switchable kinematics](https://linuxcnc.org/docs/html/motion/switchkins.html)。
|
||||||
21
scripts/validate_five_axis_catalog.mjs
Normal file
21
scripts/validate_five_axis_catalog.mjs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { readFile } from "node:fs/promises";
|
||||||
|
import { resolve } from "node:path";
|
||||||
|
import { FIVE_AXIS_CASES } from "../web/workspace/five-axis-machine-catalog.mjs";
|
||||||
|
|
||||||
|
const root = resolve(new URL("..", import.meta.url).pathname);
|
||||||
|
const checks = [];
|
||||||
|
const errors = [];
|
||||||
|
for (const item of FIVE_AXIS_CASES) {
|
||||||
|
const paths = [...item.localCases, item.kinematicsSource];
|
||||||
|
for (const relative of paths) {
|
||||||
|
try {
|
||||||
|
const text = await readFile(resolve(root, relative), "utf8");
|
||||||
|
checks.push({ case: item.id, path: relative, bytes: Buffer.byteLength(text) });
|
||||||
|
} catch (error) {
|
||||||
|
errors.push({ case: item.id, path: relative, error: error instanceof Error ? error.message : String(error) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const report = { report_id: "FIVE-AXIS-CATALOG-LOCAL-SOURCE", status: errors.length ? "failed" : "verified", caseCount: FIVE_AXIS_CASES.length, fileCount: checks.length, checks, errors };
|
||||||
|
console.log(JSON.stringify(report, null, 2));
|
||||||
|
if (errors.length) process.exitCode = 1;
|
||||||
@@ -13,6 +13,7 @@ import { downloadMachine, listSavedMachines, saveMachineCopy, readMachineCopy, d
|
|||||||
import { machineDocumentFilename } from "./machine-document";
|
import { machineDocumentFilename } from "./machine-document";
|
||||||
import { MachineNumberInput } from "./MachineManagerInputs";
|
import { MachineNumberInput } from "./MachineManagerInputs";
|
||||||
import { inspectMachineGlb, MAX_MACHINE_BYTES, type Vec3 } from "./machine-urdf";
|
import { inspectMachineGlb, MAX_MACHINE_BYTES, type Vec3 } from "./machine-urdf";
|
||||||
|
import { FIVE_AXIS_CASES, FIVE_AXIS_STATUS_LABELS } from "../../workspace/five-axis-machine-catalog.mjs";
|
||||||
|
|
||||||
type Props = { currentMachine?: ImportedMachine | null; snapshot?: XyzacVisualSource | null; coordinates: "XYZAC" | "XYZBC"; disabled: boolean; onApply: (machine: ImportedMachine) => Promise<void> };
|
type Props = { currentMachine?: ImportedMachine | null; snapshot?: XyzacVisualSource | null; coordinates: "XYZAC" | "XYZBC"; disabled: boolean; onApply: (machine: ImportedMachine) => Promise<void> };
|
||||||
type TestPose = { x: number; y: number; z: number; a: number; b: number; c: number };
|
type TestPose = { x: number; y: number; z: number; a: number; b: number; c: number };
|
||||||
@@ -21,6 +22,8 @@ const zero3 = (): Vec3 => [0, 0, 0];
|
|||||||
type EditorTab = "structure" | "motion" | "calibration" | "files";
|
type EditorTab = "structure" | "motion" | "calibration" | "files";
|
||||||
type PreviewCommand = { action: "fit" | "selected" | "front" | "top"; serial: number };
|
type PreviewCommand = { action: "fit" | "selected" | "front" | "top"; serial: number };
|
||||||
const formatted = (value: number) => Number.isFinite(value) ? value.toFixed(3) : "—";
|
const formatted = (value: number) => Number.isFinite(value) ? value.toFixed(3) : "—";
|
||||||
|
const catalogCoordinateOptions = [...new Set(FIVE_AXIS_CASES.map((item) => item.coordinates))];
|
||||||
|
const supportedCoordinateOptions = new Set(FIVE_AXIS_CASES.filter((item) => item.webWasm === "supported").map((item) => item.coordinates));
|
||||||
|
|
||||||
function ManagerPreview({ coordinates, parts, selectedId, selectedNodePath, testPose, onMotionState, command }: { command: PreviewCommand; coordinates: "XYZAC" | "XYZBC"; parts: MachinePartDraft[]; selectedId: string | null; selectedNodePath: string; testPose: TestPose; onMotionState: (state: Record<string, MotionTransform>) => void }) {
|
function ManagerPreview({ coordinates, parts, selectedId, selectedNodePath, testPose, onMotionState, command }: { command: PreviewCommand; coordinates: "XYZAC" | "XYZBC"; parts: MachinePartDraft[]; selectedId: string | null; selectedNodePath: string; testPose: TestPose; onMotionState: (state: Record<string, MotionTransform>) => void }) {
|
||||||
type PreviewHost = HTMLDivElement & { machineManagerView?: (action: PreviewCommand["action"]) => void; machineManagerSync?: (parts: MachinePartDraft[], selectedId: string | null, selectedNodePath: string, testPose: TestPose, coordinates: "XYZAC" | "XYZBC") => void };
|
type PreviewHost = HTMLDivElement & { machineManagerView?: (action: PreviewCommand["action"]) => void; machineManagerSync?: (parts: MachinePartDraft[], selectedId: string | null, selectedNodePath: string, testPose: TestPose, coordinates: "XYZAC" | "XYZBC") => void };
|
||||||
@@ -358,10 +361,11 @@ export function MachineManagerPanel({ coordinates: controllerCoordinates, disabl
|
|||||||
<DraggableDialog open={open} title={`三维机床管理 · ${coordinates}`} className="machine-manager-dialog" cancelGuard={() => filePickerOpen.current} 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-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) => {
|
<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";
|
const next = event.target.value;
|
||||||
edit((draft) => changeMachineType(draft, next)); setTab("structure");
|
if (!supportedCoordinateOptions.has(next)) { setMessage(`${next} 已登记源码案例,但当前 Web/WASM 运动学适配尚未完成`); return; }
|
||||||
|
edit((draft) => changeMachineType(draft, next as "XYZAC" | "XYZBC")); setTab("structure");
|
||||||
setMessage(`设计机型已选择 ${next};保留模型层级和 XYZ/C 绑定,请重新配置 ${next[3]} 轴并核对标定。可撤销恢复。`);
|
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>
|
}}>{catalogCoordinateOptions.map((axisSet) => { const caseItem = FIVE_AXIS_CASES.find((item) => item.coordinates === axisSet); return <option key={axisSet} value={axisSet} disabled={!supportedCoordinateOptions.has(axisSet)}>{caseItem?.label ?? axisSet}{supportedCoordinateOptions.has(axisSet) ? "" : " · 待 WASM 适配"}</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={() => 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-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-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">
|
<div className="machine-manager-layout">
|
||||||
@@ -386,6 +390,7 @@ export function MachineManagerPanel({ coordinates: controllerCoordinates, disabl
|
|||||||
<section id="machine-panel-files" role="tabpanel" aria-labelledby="machine-tab-files" hidden={tab !== "files"} className="machine-document-panel">
|
<section id="machine-panel-files" role="tabpanel" aria-labelledby="machine-tab-files" hidden={tab !== "files"} className="machine-document-panel">
|
||||||
<h3>机床文档</h3><label>模型名称<input data-axis-control="machine-document-name" maxLength={80} value={documentInfo.name} disabled={pending || followLive} placeholder={`${coordinates} 机床`} onChange={(event) => { const name = event.target.value; edit((draft) => ({ ...draft, documentInfo: { ...documentInfo, name } })); }} /></label>
|
<h3>机床文档</h3><label>模型名称<input data-axis-control="machine-document-name" maxLength={80} value={documentInfo.name} disabled={pending || followLive} placeholder={`${coordinates} 机床`} onChange={(event) => { const name = event.target.value; edit((draft) => ({ ...draft, documentInfo: { ...documentInfo, name } })); }} /></label>
|
||||||
<label>设计说明<textarea data-axis-control="machine-document-description" maxLength={2000} value={documentInfo.description} disabled={pending || followLive} placeholder="用途、标定说明、版本变更…" onChange={(event) => { const description = event.target.value; edit((draft) => ({ ...draft, documentInfo: { ...documentInfo, description } })); }} /></label>
|
<label>设计说明<textarea data-axis-control="machine-document-description" maxLength={2000} value={documentInfo.description} disabled={pending || followLive} placeholder="用途、标定说明、版本变更…" onChange={(event) => { const description = event.target.value; edit((draft) => ({ ...draft, documentInfo: { ...documentInfo, description } })); }} /></label>
|
||||||
|
<section className="machine-case-catalog" data-axis-control="machine-case-catalog"><header><h3>LinuxCNC 五轴案例库</h3><span>{FIVE_AXIS_CASES.length} 个结构/运动学案例</span></header><p>案例来源于仓库内 LinuxCNC 配置、示例 G 代码和运动学源码。选中 XYZAC/XYZBC 可直接进入当前 Web/WASM 工作流;其它类型保留完整参数和源码索引,待对应运动学适配器接入后启用。</p><ul>{FIVE_AXIS_CASES.map((item) => <li key={item.id} data-machine-case={item.id} className={item.coordinates === coordinates ? "active" : ""}><div><strong>{item.label}</strong><span>{item.kinematics} · {item.coordinates} · {item.joints} 关节</span><small>{item.description}</small></div><em className={`machine-case-status machine-case-status-${item.webWasm}`}>{FIVE_AXIS_STATUS_LABELS[item.webWasm]}</em><details><summary>查看本地案例与参数</summary><code>{item.localCases.join("\n")}</code><small>运动学源码:{item.kinematicsSource}<br />结构参数:{item.designParameters.join("、")}</small></details></li>)}</ul><small>官方参考:<a href="https://linuxcnc.org/docs/html/motion/5-axis-kinematics.html" target="_blank" rel="noreferrer">5-Axis Kinematics</a> · <a href="https://linuxcnc.org/docs/html/man/man9/kins.9.html" target="_blank" rel="noreferrer">kins(9)</a> · <a href="https://linuxcnc.org/docs/html/motion/switchkins.html" target="_blank" rel="noreferrer">switchkins</a></small></section>
|
||||||
<div className="machine-save-location" data-axis-control="machine-document-location"><strong>文件保存在哪里?</strong><p>{saveLocation}</p><p>GLB 下载到浏览器设置的目录,通常为“下载”。请打开浏览器下载记录查看实际位置;网页无法读取磁盘完整路径。</p><p>保存时另存一份到本浏览器机床库,可在刷新页面后重新打开。本地机床库仅属于当前浏览器及网站地址,不会上传云服务器;清理网站数据会删除这些副本。</p></div>
|
<div className="machine-save-location" data-axis-control="machine-document-location"><strong>文件保存在哪里?</strong><p>{saveLocation}</p><p>GLB 下载到浏览器设置的目录,通常为“下载”。请打开浏览器下载记录查看实际位置;网页无法读取磁盘完整路径。</p><p>保存时另存一份到本浏览器机床库,可在刷新页面后重新打开。本地机床库仅属于当前浏览器及网站地址,不会上传云服务器;清理网站数据会删除这些副本。</p></div>
|
||||||
<h3>本浏览器机床库 <small>{library.length} 个保存版本</small></h3><div className="machine-library-tools"><input data-axis-control="machine-library-search" aria-label="搜索已保存机床" placeholder="搜索名称 / 机型" value={libraryQuery} onChange={(event) => setLibraryQuery(event.target.value)} /><button type="button" onClick={() => void refreshLibrary()}>刷新</button></div>
|
<h3>本浏览器机床库 <small>{library.length} 个保存版本</small></h3><div className="machine-library-tools"><input data-axis-control="machine-library-search" aria-label="搜索已保存机床" placeholder="搜索名称 / 机型" value={libraryQuery} onChange={(event) => setLibraryQuery(event.target.value)} /><button type="button" onClick={() => void refreshLibrary()}>刷新</button></div>
|
||||||
{libraryError && <p role="alert">{libraryError}</p>}
|
{libraryError && <p role="alert">{libraryError}</p>}
|
||||||
|
|||||||
3
web/react/src/legacy.d.ts
vendored
3
web/react/src/legacy.d.ts
vendored
@@ -10,6 +10,9 @@ declare module "*.mjs" {
|
|||||||
export const MACHINE_DEFINITIONS: readonly MachineDefinition[];
|
export const MACHINE_DEFINITIONS: readonly MachineDefinition[];
|
||||||
export const DEFAULT_MACHINE: MachineDefinition;
|
export const DEFAULT_MACHINE: MachineDefinition;
|
||||||
export function machineDefinition(value?: string): MachineDefinition | null;
|
export function machineDefinition(value?: string): MachineDefinition | null;
|
||||||
|
export type FiveAxisCase = { id: string; label: string; family: string; machineClass: string; coordinates: string; joints: number; kinematics: string; rtcp: string; webWasm: "supported" | "adapter-required" | "identity-only"; description: string; localCases: readonly string[]; kinematicsSource: string; designParameters: readonly string[] };
|
||||||
|
export const FIVE_AXIS_CASES: readonly FiveAxisCase[];
|
||||||
|
export const FIVE_AXIS_STATUS_LABELS: Record<FiveAxisCase["webWasm"], string>;
|
||||||
export const SIM_CASE_CATALOG: Array<{ path: string; label?: string; category?: string; status?: string }>;
|
export const SIM_CASE_CATALOG: Array<{ path: string; label?: string; category?: string; status?: string }>;
|
||||||
export function loadSimulationCase(options: { path: string; baseUrl?: string }): Promise<{ status: string; entrypoint: string; files: Record<string, string>; missing: string[]; fetched: string[]; state_hash: string; halManifest: unknown[] }>;
|
export function loadSimulationCase(options: { path: string; baseUrl?: string }): Promise<{ status: string; entrypoint: string; files: Record<string, string>; missing: string[]; fetched: string[]; state_hash: string; halManifest: unknown[] }>;
|
||||||
export function toWorkerConfiguration(bundle: unknown): Record<string, unknown>;
|
export function toWorkerConfiguration(bundle: unknown): Record<string, unknown>;
|
||||||
|
|||||||
@@ -530,3 +530,21 @@ output { font: 12px ui-monospace, monospace; text-align: right; }
|
|||||||
.machine-library-list span, .machine-library-list time, .machine-library-list small { font-size: 11px; color: #526c6d; }
|
.machine-library-list span, .machine-library-list time, .machine-library-list small { font-size: 11px; color: #526c6d; }
|
||||||
.machine-library-list li > div { display: flex; gap: 6px; }
|
.machine-library-list li > div { display: flex; gap: 6px; }
|
||||||
.machine-library-list button, .machine-library-tools button { padding: 5px 8px; border: 1px solid #9babad; border-radius: 3px; background: #eaf2f0; }
|
.machine-library-list button, .machine-library-tools button { padding: 5px 8px; border: 1px solid #9babad; border-radius: 3px; background: #eaf2f0; }
|
||||||
|
.machine-case-catalog { margin: 12px 0; padding: 10px; border: 1px solid #adc8c1; border-radius: 4px; background: #edf5f3; }
|
||||||
|
.machine-case-catalog > header { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
||||||
|
.machine-case-catalog > header h3 { margin: 0; }
|
||||||
|
.machine-case-catalog > header span { color: #526c6d; font-size: 11px; }
|
||||||
|
.machine-case-catalog > p { margin: 7px 0; color: #526c6d; font-size: 11px; line-height: 1.5; }
|
||||||
|
.machine-case-catalog ul { display: grid; gap: 6px; margin: 8px 0; padding: 0; list-style: none; }
|
||||||
|
.machine-case-catalog li { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; padding: 8px; border: 1px solid #c1d2ce; border-radius: 4px; background: #fff; }
|
||||||
|
.machine-case-catalog li.active { border-color: #398187; box-shadow: inset 3px 0 #398187; }
|
||||||
|
.machine-case-catalog li > div { display: grid; gap: 3px; min-width: 0; }
|
||||||
|
.machine-case-catalog li strong { overflow-wrap: anywhere; }
|
||||||
|
.machine-case-catalog li span, .machine-case-catalog li small { color: #526c6d; font-size: 11px; line-height: 1.45; }
|
||||||
|
.machine-case-catalog li em { align-self: start; padding: 3px 6px; border-radius: 12px; background: #dcebe8; color: #285d5c; font-size: 10px; font-style: normal; white-space: nowrap; }
|
||||||
|
.machine-case-catalog li em.machine-case-status-adapter-required { background: #fff0d4; color: #78521c; }
|
||||||
|
.machine-case-catalog li em.machine-case-status-identity-only { background: #e8e8e8; color: #5d6262; }
|
||||||
|
.machine-case-catalog details { grid-column: 1 / -1; color: #526c6d; font-size: 11px; }
|
||||||
|
.machine-case-catalog details code { display: block; margin: 5px 0; padding: 6px; overflow: auto; white-space: pre-wrap; background: #f2f6f5; color: #315d61; }
|
||||||
|
.machine-case-catalog details small { display: block; }
|
||||||
|
.machine-case-catalog > small a { color: #246c73; }
|
||||||
|
|||||||
222
web/workspace/five-axis-machine-catalog.json
Normal file
222
web/workspace/five-axis-machine-catalog.json
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
{
|
||||||
|
"schema": "linuxcnc.five-axis-machine-catalog/1",
|
||||||
|
"sourceRoot": "linuxcnc",
|
||||||
|
"officialSources": [
|
||||||
|
{
|
||||||
|
"title": "LinuxCNC 5-Axis Kinematics",
|
||||||
|
"url": "https://linuxcnc.org/docs/html/motion/5-axis-kinematics.html"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "LinuxCNC kins(9)",
|
||||||
|
"url": "https://linuxcnc.org/docs/html/man/man9/kins.9.html"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "LinuxCNC switchable kinematics",
|
||||||
|
"url": "https://linuxcnc.org/docs/html/motion/switchkins.html"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"id": "xyzac-trt",
|
||||||
|
"label": "XYZAC · 转台倾转(A/C)",
|
||||||
|
"family": "table-rotary-tilting",
|
||||||
|
"machineClass": "five-axis-mill",
|
||||||
|
"coordinates": "XYZAC",
|
||||||
|
"joints": 5,
|
||||||
|
"kinematics": "xyzac-trt-kins",
|
||||||
|
"rtcp": "native",
|
||||||
|
"webWasm": "supported",
|
||||||
|
"description": "A 轴绕 X 倾转,C 轴绕 Z 回转,工件安装在复合转台上。",
|
||||||
|
"localCases": [
|
||||||
|
"linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
|
||||||
|
"linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/boat-xyzac.ngc",
|
||||||
|
"linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/impeller-7bl-xyzac.ngc",
|
||||||
|
"linuxcnc/configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/xyzac-trt.ini",
|
||||||
|
"linuxcnc/configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/boat-xyzac.ngc",
|
||||||
|
"linuxcnc/configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/impeller-7bl-xyzac.ngc",
|
||||||
|
"linuxcnc/configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/test-xyzac.ngc"
|
||||||
|
],
|
||||||
|
"kinematicsSource": "linuxcnc/src/emc/kinematics/xyzac-trt-kins.c",
|
||||||
|
"designParameters": ["rotaryPoint", "tiltToRotaryOffset", "toolOffset", "physicalZero"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "xyzbc-trt",
|
||||||
|
"label": "XYZBC · 转台倾转(B/C)",
|
||||||
|
"family": "table-rotary-tilting",
|
||||||
|
"machineClass": "five-axis-mill",
|
||||||
|
"coordinates": "XYZBC",
|
||||||
|
"joints": 5,
|
||||||
|
"kinematics": "xyzbc-trt-kins",
|
||||||
|
"rtcp": "native",
|
||||||
|
"webWasm": "supported",
|
||||||
|
"description": "B 轴绕 Y 倾转,C 轴绕 Z 回转,适合 BC 类转台或相应摆头结构。",
|
||||||
|
"localCases": [
|
||||||
|
"linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini",
|
||||||
|
"linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/boat-xyzbc.ngc",
|
||||||
|
"linuxcnc/configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/test-xyzbc.ngc",
|
||||||
|
"linuxcnc/configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/boat-xyzbc.ngc"
|
||||||
|
],
|
||||||
|
"kinematicsSource": "linuxcnc/src/emc/kinematics/xyzbc-trt-kins.c",
|
||||||
|
"designParameters": ["rotaryPoint", "tiltToRotaryOffset", "toolOffset", "physicalZero"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "xyzab-tdr",
|
||||||
|
"label": "XYZAB · 双转台(A/B)",
|
||||||
|
"family": "table-dual-rotary",
|
||||||
|
"machineClass": "five-axis-mill",
|
||||||
|
"coordinates": "XYZAB",
|
||||||
|
"joints": 5,
|
||||||
|
"kinematics": "xyzab_tdr_kins",
|
||||||
|
"rtcp": "native-linuxcnc",
|
||||||
|
"webWasm": "adapter-required",
|
||||||
|
"description": "A/B 两个转轴组成双转台,源码案例包含旋转点、X/Z 偏置和 TCP 演示。",
|
||||||
|
"localCases": [
|
||||||
|
"linuxcnc/configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini",
|
||||||
|
"linuxcnc/configs/sim/axis/vismach/5axis/table-dual-rotary/demos/xyzab-tdr-demo.ngc",
|
||||||
|
"linuxcnc/configs/sim/gmoccapy/gmoccapy_XYZAB.ini",
|
||||||
|
"linuxcnc/configs/sim/qtdragon/qtdragon_xyz45/qtdragon_xyzab.ini"
|
||||||
|
],
|
||||||
|
"kinematicsSource": "linuxcnc/src/hal/components/xyzab_tdr_kins.comp",
|
||||||
|
"designParameters": ["xRotPoint", "yRotPoint", "zRotPoint", "xOffset", "zOffset", "toolOffset", "physicalZero"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "bridge-mill-5axiskins",
|
||||||
|
"label": "Bridge Mill · 5axiskins(XYZBCW/Y)",
|
||||||
|
"family": "bridge-mill",
|
||||||
|
"machineClass": "five-axis-mill-with-extra-coordinate",
|
||||||
|
"coordinates": "XYZBCWY",
|
||||||
|
"joints": 7,
|
||||||
|
"kinematics": "5axiskins",
|
||||||
|
"rtcp": "native-linuxcnc",
|
||||||
|
"webWasm": "adapter-required",
|
||||||
|
"description": "桥式五轴机床,B/C 为姿态轴,W 为刀具方向运动;示例还演示 Y 轴双关节。",
|
||||||
|
"localCases": [
|
||||||
|
"linuxcnc/configs/sim/axis/vismach/5axis/bridgemill/5axis.ini",
|
||||||
|
"linuxcnc/configs/sim/axis/vismach/5axis/bridgemill/5axisgui.ngc",
|
||||||
|
"linuxcnc/configs/sim/gmoccapy/gmoccapy_XYZCW.ini"
|
||||||
|
],
|
||||||
|
"kinematicsSource": "linuxcnc/src/emc/kinematics/5axiskins.c",
|
||||||
|
"designParameters": ["jointCoordinateMap", "duplicateAxisMap", "toolDirectionAxis", "toolOffset", "physicalZero"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "max5-maxkins",
|
||||||
|
"label": "MAX5 · maxkins(XYZBC)",
|
||||||
|
"family": "table-and-head-rotary",
|
||||||
|
"machineClass": "five-axis-mill",
|
||||||
|
"coordinates": "XYZBC",
|
||||||
|
"joints": 6,
|
||||||
|
"kinematics": "maxkins",
|
||||||
|
"rtcp": "native-linuxcnc",
|
||||||
|
"webWasm": "adapter-required",
|
||||||
|
"description": "LinuxCNC 的 MAX5 非恒等五轴案例;同时提供 trivkins 对照配置,适合验证坐标映射。",
|
||||||
|
"localCases": [
|
||||||
|
"linuxcnc/configs/sim/axis/vismach/5axis/max5/max5kins.ini",
|
||||||
|
"linuxcnc/configs/sim/axis/vismach/5axis/max5/max5triv.ini",
|
||||||
|
"linuxcnc/configs/sim/axis/vismach/5axis/max5/README",
|
||||||
|
"linuxcnc/configs/sim/gmoccapy/gmoccapy_XYZAB.ini"
|
||||||
|
],
|
||||||
|
"kinematicsSource": "linuxcnc/src/emc/kinematics/maxkins.c",
|
||||||
|
"designParameters": ["rotaryAxisOrder", "rotaryCenters", "uvwTransform", "toolOffset", "physicalZero"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "hexapod-genhexkins",
|
||||||
|
"label": "Hexapod · genhexkins(Stewart 平台)",
|
||||||
|
"family": "parallel-platform",
|
||||||
|
"machineClass": "six-dof-rtcp-capable",
|
||||||
|
"coordinates": "XYZABC",
|
||||||
|
"joints": 6,
|
||||||
|
"kinematics": "genhexkins",
|
||||||
|
"rtcp": "native-linuxcnc",
|
||||||
|
"webWasm": "adapter-required",
|
||||||
|
"description": "六支链并联平台,具有 XYZABC 六自由度;它是五轴 CAM/RTCP 可覆盖的六自由度机型。",
|
||||||
|
"localCases": [
|
||||||
|
"linuxcnc/configs/sim/axis/vismach/hexapod-sim/hexapod.ini",
|
||||||
|
"linuxcnc/configs/sim/axis/vismach/hexapod-sim/kinematics.hal",
|
||||||
|
"linuxcnc/configs/sim/axis/vismach/hexapod-sim/README",
|
||||||
|
"linuxcnc/src/emc/kinematics/genhexkins.c"
|
||||||
|
],
|
||||||
|
"kinematicsSource": "linuxcnc/src/emc/kinematics/genhexkins.c",
|
||||||
|
"designParameters": ["baseJointPoints", "platformJointPoints", "spindleOffset", "screwLead", "iterationLimits", "physicalZero"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pentapod-pentakins",
|
||||||
|
"label": "Pentapod · pentakins(五支链平台)",
|
||||||
|
"family": "parallel-platform",
|
||||||
|
"machineClass": "five-axis-parallel",
|
||||||
|
"coordinates": "XYZAB",
|
||||||
|
"joints": 5,
|
||||||
|
"kinematics": "pentakins",
|
||||||
|
"rtcp": "native-linuxcnc",
|
||||||
|
"webWasm": "adapter-required",
|
||||||
|
"description": "五支链并联平台,LinuxCNC 源码提供 base/effector 几何参数和迭代求解案例。",
|
||||||
|
"localCases": [
|
||||||
|
"linuxcnc/configs/sim/axis/pentapod/pentapod.ini",
|
||||||
|
"linuxcnc/configs/sim/axis/pentapod/kinematics.hal",
|
||||||
|
"linuxcnc/configs/sim/axis/pentapod/README",
|
||||||
|
"linuxcnc/src/emc/kinematics/pentakins.c"
|
||||||
|
],
|
||||||
|
"kinematicsSource": "linuxcnc/src/emc/kinematics/pentakins.c",
|
||||||
|
"designParameters": ["baseJointPoints", "effectorGeometry", "toolOffset", "convergenceCriterion", "physicalZero"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "puma-pumakins",
|
||||||
|
"label": "PUMA · pumakins(串联机器人)",
|
||||||
|
"family": "serial-robot",
|
||||||
|
"machineClass": "six-axis-robot",
|
||||||
|
"coordinates": "XYZABC",
|
||||||
|
"joints": 6,
|
||||||
|
"kinematics": "pumakins",
|
||||||
|
"rtcp": "robot-tool-center-point",
|
||||||
|
"webWasm": "adapter-required",
|
||||||
|
"description": "PUMA 型六关节串联机器人;可承载五轴刀具姿态任务,但不是五关节铣床拓扑。",
|
||||||
|
"localCases": [
|
||||||
|
"linuxcnc/configs/sim/axis/vismach/puma/puma.ini",
|
||||||
|
"linuxcnc/configs/sim/axis/vismach/puma/puma_cube.ini",
|
||||||
|
"linuxcnc/configs/sim/axis/vismach/puma/puma_cube.ngc",
|
||||||
|
"linuxcnc/src/emc/kinematics/pumakins.c"
|
||||||
|
],
|
||||||
|
"kinematicsSource": "linuxcnc/src/emc/kinematics/pumakins.c",
|
||||||
|
"designParameters": ["A2", "A3", "D3", "D4", "toolOffset", "physicalZero"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "puma-genserkins",
|
||||||
|
"label": "PUMA 560 · genserkins(DH 串联)",
|
||||||
|
"family": "serial-robot",
|
||||||
|
"machineClass": "six-axis-robot",
|
||||||
|
"coordinates": "XYZABC",
|
||||||
|
"joints": 6,
|
||||||
|
"kinematics": "genserkins",
|
||||||
|
"rtcp": "robot-tool-center-point",
|
||||||
|
"webWasm": "adapter-required",
|
||||||
|
"description": "PUMA 560 的通用串联运动学案例,用 DH 参数描述各关节。",
|
||||||
|
"localCases": [
|
||||||
|
"linuxcnc/configs/sim/axis/vismach/puma/puma560.ini",
|
||||||
|
"linuxcnc/configs/sim/axis/vismach/puma/puma560_uvw.ini",
|
||||||
|
"linuxcnc/configs/sim/axis/vismach/puma/puma560_dh.hal",
|
||||||
|
"linuxcnc/src/emc/kinematics/genserkins.c"
|
||||||
|
],
|
||||||
|
"kinematicsSource": "linuxcnc/src/emc/kinematics/genserkins.c",
|
||||||
|
"designParameters": ["DHParameters", "jointLimits", "toolOffset", "physicalZero"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "trivkins-xyzab",
|
||||||
|
"label": "XYZAB · trivkins(直角坐标映射基线)",
|
||||||
|
"family": "identity-baseline",
|
||||||
|
"machineClass": "five-axis-linear-or-rotary",
|
||||||
|
"coordinates": "XYZAB",
|
||||||
|
"joints": 5,
|
||||||
|
"kinematics": "trivkins",
|
||||||
|
"rtcp": "none",
|
||||||
|
"webWasm": "identity-only",
|
||||||
|
"description": "五轴一一对应的基线案例,用于校验 GLB 关节树和坐标映射;本身不提供非线性 RTCP。",
|
||||||
|
"localCases": [
|
||||||
|
"linuxcnc/configs/sim/qtdragon/qtdragon_xyz45/qtdragon_xyzab.ini",
|
||||||
|
"linuxcnc/configs/sim/woodpecker/woodpecker_xyzab.ini",
|
||||||
|
"linuxcnc/configs/sim/axis/vismach/5axis/max5/max5triv.ini",
|
||||||
|
"linuxcnc/src/emc/kinematics/trivkins.c"
|
||||||
|
],
|
||||||
|
"kinematicsSource": "linuxcnc/src/emc/kinematics/trivkins.c",
|
||||||
|
"designParameters": ["coordinateOrder", "duplicateAxisMap", "physicalZero"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
20
web/workspace/five-axis-machine-catalog.mjs
Normal file
20
web/workspace/five-axis-machine-catalog.mjs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import catalog from "./five-axis-machine-catalog.json" with { type: "json" };
|
||||||
|
|
||||||
|
const allowedStatus = new Set(["supported", "adapter-required", "identity-only"]);
|
||||||
|
const allowedCoordinates = /^[XYZABCUVW]+$/;
|
||||||
|
|
||||||
|
for (const item of catalog.cases) {
|
||||||
|
if (!item.id || !item.label || !item.family || !item.kinematics || !allowedCoordinates.test(item.coordinates)) throw new Error("five_axis_catalog_invalid");
|
||||||
|
if (!Number.isInteger(item.joints) || item.joints < 5 || !allowedStatus.has(item.webWasm)) throw new Error("five_axis_catalog_case_invalid");
|
||||||
|
if (!Array.isArray(item.localCases) || item.localCases.length === 0 || !item.kinematicsSource) throw new Error("five_axis_catalog_sources_missing");
|
||||||
|
}
|
||||||
|
if (new Set(catalog.cases.map((item) => item.id)).size !== catalog.cases.length) throw new Error("five_axis_catalog_duplicate");
|
||||||
|
|
||||||
|
export const FIVE_AXIS_CATALOG = Object.freeze(catalog);
|
||||||
|
export const FIVE_AXIS_CASES = Object.freeze(catalog.cases.map((item) => Object.freeze(item)));
|
||||||
|
export const FIVE_AXIS_STATUS_LABELS = Object.freeze({
|
||||||
|
supported: "Web/WASM 已支持",
|
||||||
|
"adapter-required": "源码案例 · 待 WASM 运动学适配",
|
||||||
|
"identity-only": "仅直角坐标基线 · 无非线性 RTCP"
|
||||||
|
});
|
||||||
|
export const fiveAxisCase = (value) => FIVE_AXIS_CASES.find((item) => [item.id, item.coordinates, item.kinematics].includes(value)) ?? null;
|
||||||
Reference in New Issue
Block a user