Add INI-driven XYZAC and XYZBC parametric machine design
This commit is contained in:
@@ -3,6 +3,7 @@ import { runtimeResourceFixture } from "./resource-resolver.mjs";
|
||||
import { SIM_CASE_CATALOG, SIM_FILE_INDEX, SIM_HAL_COMPONENTS } from "./sim-case-catalog.mjs";
|
||||
import { isOptionalSourceKey } from "./linuxcnc-source-semantics.mjs";
|
||||
import { MACHINE_DEFINITIONS, machineDefinition } from "./machine-definitions.mjs";
|
||||
import { readTrtGeometry } from "./trt-geometry.mjs";
|
||||
|
||||
export const SIMULATION_CASE_SCHEMA = "simulation-case-bundle.v1";
|
||||
const RESOURCE_KEYS = new Set([
|
||||
@@ -17,8 +18,8 @@ const CASE_KINEMATICS = Object.freeze({
|
||||
"linuxcnc/configs/sim/axis/corexy/corexy_by_hal.ini": Object.freeze({ type: "corexy", path_mode: "hal" }),
|
||||
"linuxcnc/configs/sim/axis/ldelta.ini": Object.freeze({ type: "lineardelta", path_mode: "kins" }),
|
||||
"linuxcnc/configs/sim/axis/rdelta.ini": Object.freeze({ type: "rotarydelta", path_mode: "kins" }),
|
||||
"linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini": Object.freeze({ type: "xyzac-trt", path_mode: "wasm", coordinates: "XYZAC", joint_count: 5, max_linear_velocity: 35, axis_linear_velocity_limit: 20, max_acceleration: 300, max_jerk: 1000000000, servo_cycle_time: 0.001, task_cycle_time: 0.01, cycle_time: 0.001, switchkins: { 0: "identity", 1: "xyzac", 2: "userk" }, offsets: { x_rot_point: 0, y_offset: 20, z_offset: 10, tool_offset: 0 } }),
|
||||
"linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini": Object.freeze({ type: "xyzbc-trt", path_mode: "wasm", coordinates: "XYZBC", joint_count: 5, max_linear_velocity: 35, axis_linear_velocity_limit: 20, axis_angular_velocity_limit: 30, max_acceleration: 300, max_jerk: 1000000000, servo_cycle_time: 0.001, task_cycle_time: 0.01, cycle_time: 0.001, switchkins: { 0: "identity", 1: "xyzbc", 2: "userk" }, offsets: { x_rot_point: 0, y_rot_point: 0, z_rot_point: 0, x_offset: -20, y_offset: 0, z_offset: -15, tool_offset: 0 } }),
|
||||
"linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini": Object.freeze({ type: "xyzac-trt", path_mode: "wasm", coordinates: "XYZAC", joint_count: 5, max_linear_velocity: 35, axis_linear_velocity_limit: 20, max_acceleration: 300, max_jerk: 1000000000, servo_cycle_time: 0.001, task_cycle_time: 0.01, cycle_time: 0.001, switchkins: { 0: "identity", 1: "xyzac", 2: "userk" } }),
|
||||
"linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini": Object.freeze({ type: "xyzbc-trt", path_mode: "wasm", coordinates: "XYZBC", joint_count: 5, max_linear_velocity: 35, axis_linear_velocity_limit: 20, axis_angular_velocity_limit: 30, max_acceleration: 300, max_jerk: 1000000000, servo_cycle_time: 0.001, task_cycle_time: 0.01, cycle_time: 0.001, switchkins: { 0: "identity", 1: "xyzbc", 2: "userk" } }),
|
||||
});
|
||||
const RUNTIME_FIXTURES = Object.freeze({
|
||||
"linuxcnc/configs/sim/axis/minimal_xyz.ini": Object.freeze({
|
||||
@@ -130,7 +131,7 @@ function inManifest(path) {
|
||||
function responseUrl(baseUrl, path) { return `${String(baseUrl ?? "").replace(/\/$/, "")}/${path}`.replace(/^\//, "/"); }
|
||||
|
||||
async function fetchText(fetchImpl, baseUrl, path) {
|
||||
const response = await fetchImpl(responseUrl(baseUrl, path));
|
||||
const response = await fetchImpl(responseUrl(baseUrl, path), { cache: "no-store" });
|
||||
if (!response?.ok) return null;
|
||||
return response.text();
|
||||
}
|
||||
@@ -241,6 +242,7 @@ export async function loadSimulationCase({ path, fetchImpl = globalThis.fetch, b
|
||||
else { files[visualSource] = content; fetched.push(visualSource); }
|
||||
}
|
||||
}
|
||||
const sourceIni = files[catalog.path];
|
||||
const fixture = RUNTIME_FIXTURES[catalog.path];
|
||||
if (fixture) {
|
||||
if (!inManifest(fixture.generated_hal)) missing.push(fixture.generated_hal);
|
||||
@@ -267,6 +269,7 @@ export async function loadSimulationCase({ path, fetchImpl = globalThis.fetch, b
|
||||
};
|
||||
return {
|
||||
...clone(unsigned),
|
||||
source_ini: sourceIni,
|
||||
catalog,
|
||||
halManifest: [...new Set([...SIM_HAL_COMPONENTS, ...PRELOADED_HAL_COMPONENTS])].sort().map((component) => ({ component })),
|
||||
status: missing.length ? "blocked" : "ready",
|
||||
@@ -276,6 +279,8 @@ export async function loadSimulationCase({ path, fetchImpl = globalThis.fetch, b
|
||||
|
||||
export function toWorkerConfiguration(bundle) {
|
||||
if (!bundle || bundle.schema !== SIMULATION_CASE_SCHEMA) throw new Error("simulation_case_bundle_invalid");
|
||||
const kinematics = CASE_KINEMATICS[bundle.entrypoint] ? clone(CASE_KINEMATICS[bundle.entrypoint]) : null;
|
||||
if (machineDefinition(bundle.entrypoint)) kinematics.offsets = readTrtGeometry(bundle).offsets;
|
||||
return {
|
||||
entrypoint: bundle.entrypoint,
|
||||
files: clone(bundle.files),
|
||||
@@ -285,7 +290,7 @@ export function toWorkerConfiguration(bundle) {
|
||||
runtime_fixtures: clone(bundle.runtime_fixtures ?? []),
|
||||
...(bundle.entrypoint === "linuxcnc/configs/sim/axis/vismach/3axis-tutorial/3axis-tutorial.ini" ? { home_limits: { z: [-240, 100] } } : {}),
|
||||
...(machineDefinition(bundle.entrypoint) ? { home_limits: { joint_count: 5, kinematics_type: machineDefinition(bundle.entrypoint).kinematics_type, coordinates: machineDefinition(bundle.entrypoint).coordinates, ...machineDefinition(bundle.entrypoint).joint_limits } } : {}),
|
||||
...(CASE_KINEMATICS[bundle.entrypoint] ? { kinematics: clone(CASE_KINEMATICS[bundle.entrypoint]) } : {}),
|
||||
...(kinematics ? { kinematics } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,11 @@ assert.equal(runtimeGenerated.status, "ready");
|
||||
assert.equal(runtimeGenerated.missing.length, 0);
|
||||
assert.equal(runtimeGenerated.files["linuxcnc/configs/sim/axis/sim-lathe.var"], "");
|
||||
assert.ok(runtimeGenerated.runtime_fixtures.some((row) => row.kind === "parameter_file"));
|
||||
const optionalGraphic = await loadSimulationCase({ path: "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini", fetchImpl });
|
||||
const optionalGraphic = await loadSimulationCase({ path: "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini", fetchImpl: async (url) => {
|
||||
const response = await fetchImpl(url);
|
||||
if (!String(url).endsWith("xyzac-trt.ini")) return response;
|
||||
return { ok: response.ok, text: async () => (await response.text()).replace(/INTRO_GRAPHIC\s*=\s*[^\r\n]+/, "INTRO_GRAPHIC = emc2.gif") };
|
||||
} });
|
||||
assert.equal(optionalGraphic.status, "ready");
|
||||
assert.ok(optionalGraphic.optional_missing.some((row) => row.path.endsWith("/emc2.gif")));
|
||||
const emptyOpenFile = await loadSimulationCase({ path: "linuxcnc/configs/sim/axis/anglejog/anglejog.ini", fetchImpl });
|
||||
|
||||
76
web/workspace/trt-geometry.mjs
Normal file
76
web/workspace/trt-geometry.mjs
Normal file
@@ -0,0 +1,76 @@
|
||||
import { parseIni } from "./ini-parser.mjs";
|
||||
import { parseHalLine } from "./hal-configuration.mjs";
|
||||
import { WorkspaceRuntime } from "./runtime.mjs";
|
||||
import { machineDefinition } from "./machine-definitions.mjs";
|
||||
import { fnv1a64 } from "../worker/replay.mjs";
|
||||
|
||||
export const TRT_GEOMETRY_KEYS = Object.freeze(["x_rot_point", "y_rot_point", "z_rot_point", "x_offset", "y_offset", "z_offset"]);
|
||||
const numeric = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
|
||||
function fail(code, detail) { throw Object.assign(new Error(`${code}: ${detail}`), { code }); }
|
||||
|
||||
export function readTrtGeometry({ entrypoint, files }) {
|
||||
const machine = machineDefinition(entrypoint);
|
||||
if (!machine) fail("trt_geometry_machine", entrypoint);
|
||||
const parsed = parseIni({ workspace: new WorkspaceRuntime({ files }), entrypoint });
|
||||
if (parsed.sections.TRAJ?.COORDINATES?.toUpperCase() !== machine.coordinates || parsed.sections.KINS?.KINEMATICS?.split(/\s+/)[0] !== `${machine.id}-kins`) fail("trt_geometry_machine_mismatch", entrypoint);
|
||||
const commands = parsed.entries.filter((e) => e.section.toUpperCase() === "HAL" && e.key.toUpperCase() === "HALCMD")
|
||||
.map((entry) => ({ entry, record: parseHalLine(entry.value, entry.line, parsed.sections) }));
|
||||
const unsupported = machine.coordinates === "XYZAC" ? "x_offset" : "y_offset";
|
||||
const offsets = { tool_offset: 0 };
|
||||
const sources = {};
|
||||
for (const key of TRT_GEOMETRY_KEYS) {
|
||||
const pin = `${machine.id}-kins.${key.replaceAll("_", "-")}`;
|
||||
const nets = commands.filter(({ record: r }) => r?.command === "net" && r.tokens.includes(pin));
|
||||
const direct = commands.filter(({ record: r }) => r?.command === "setp" && r.tokens[0] === pin);
|
||||
if (nets.length > 1 || nets.length && direct.length) fail("trt_geometry_ambiguous", pin);
|
||||
const assignments = nets.length ? commands.filter(({ record: r }) => r?.command === "sets" && r.tokens[0] === nets[0].record.tokens[0]) : direct;
|
||||
if (assignments.length > 1) fail("trt_geometry_duplicate", pin);
|
||||
if (!assignments.length) {
|
||||
if (key !== unsupported) fail("trt_geometry_missing", pin);
|
||||
offsets[key] = 0;
|
||||
continue;
|
||||
}
|
||||
const { entry, record } = assignments[0];
|
||||
const value = record.tokens[1];
|
||||
if (record.tokens.length !== 2 || !numeric.test(value) || !Number.isFinite(Number(value))) fail("trt_geometry_number", `${pin} @ ${entry.source}:${entry.line}`);
|
||||
if (key === unsupported && Number(value) !== 0) fail("trt_geometry_unsupported", pin);
|
||||
offsets[key] = Number(value);
|
||||
// Follow INI references so editing updates the original scalar definition.
|
||||
const rawValue = entry.value.trim().split(/\s+/).at(-1);
|
||||
const reference = /^\[([^\]]+)\](.+)$/.exec(rawValue);
|
||||
let source = entry;
|
||||
if (reference) {
|
||||
const definitions = parsed.entries.filter((e) => e.section === reference[1] && e.key === reference[2]);
|
||||
if (definitions.length !== 1) fail("trt_geometry_reference", rawValue);
|
||||
source = definitions[0];
|
||||
}
|
||||
sources[key] = { path: source.source, line: source.line, value: reference ? source.value : rawValue };
|
||||
if (Object.entries(sources).some(([other, location]) => other !== key && location.path === source.source && location.line === source.line)) fail("trt_geometry_shared_definition", key);
|
||||
}
|
||||
return { coordinates: machine.coordinates, offsets, sources, unsupported, state_hash: fnv1a64({ entrypoint, offsets, sources }) };
|
||||
}
|
||||
|
||||
export function writeTrtGeometry(configuration, values) {
|
||||
const geometry = readTrtGeometry(configuration);
|
||||
const files = { ...configuration.files };
|
||||
for (const key of TRT_GEOMETRY_KEYS) {
|
||||
const raw = String(values[key] ?? geometry.offsets[key]).trim();
|
||||
if (!numeric.test(raw) || !Number.isFinite(Number(raw))) fail("trt_geometry_number", key);
|
||||
if (key === geometry.unsupported && Number(raw) !== 0) fail("trt_geometry_unsupported", key);
|
||||
const source = geometry.sources[key];
|
||||
if (!source) continue;
|
||||
const lines = files[source.path].split("\n");
|
||||
const line = lines[source.line - 1];
|
||||
const comment = line.search(/[;#]/);
|
||||
const end = comment < 0 ? line.length : comment;
|
||||
const body = line.slice(0, end);
|
||||
const index = body.lastIndexOf(source.value);
|
||||
if (index < 0) fail("trt_geometry_source_changed", key);
|
||||
lines[source.line - 1] = body.slice(0, index) + String(Number(raw)) + body.slice(index + source.value.length) + line.slice(end);
|
||||
files[source.path] = lines.join("\n");
|
||||
}
|
||||
const result = { ...configuration, files };
|
||||
if ("state_hash" in result) result.state_hash = fnv1a64({ entrypoint: configuration.entrypoint, files });
|
||||
readTrtGeometry(result);
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user