Make machine configuration authoritative and restore native checkpoints exactly

This commit is contained in:
2026-09-18 09:09:41 +08:00
parent 6a69511276
commit 4a568f1f4c
94 changed files with 10882 additions and 301 deletions

View File

@@ -0,0 +1,35 @@
{
"default_machine": "xyzac-trt",
"machines": [
{
"id": "xyzac-trt",
"coordinates": "XYZAC",
"kinematics_type": 2,
"entrypoint": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
"program": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/boat-xyzac.ngc",
"switchkins_program": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins.ngc",
"source_path": "linuxcnc/src/hal/user_comps/vismach/xyzac-trt-gui.py",
"model": "trt-moving-knee",
"subroutines": [
"xyzac_switchkins_sub",
"helix_ac"
],
"runtime_fixture": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini"
},
{
"id": "xyzbc-trt",
"coordinates": "XYZBC",
"kinematics_type": 3,
"entrypoint": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini",
"program": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/boat-xyzbc.ngc",
"switchkins_program": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzbc_switchkins.ngc",
"source_path": "linuxcnc/src/hal/user_comps/vismach/xyzbc-trt-gui.py",
"model": "trt-moving-head",
"subroutines": [
"xyzbc_switchkins_sub",
"helix_bc"
],
"runtime_fixture": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini"
}
]
}

View File

@@ -1,25 +1,12 @@
const TRT_ROOT = "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting";
import catalog from "./machine-catalog.json" with { type: "json" };
function trt(id, coordinates, type, offsets, model) {
const stem = coordinates.toLowerCase();
return Object.freeze({
id, label: `${coordinates} TRT`, coordinates, axes: [...stem], joint_count: coordinates.length,
entrypoint: `${TRT_ROOT}/${id}.ini`, program: `${TRT_ROOT}/demos/boat-${stem}.ngc`,
switchkins_program: `${TRT_ROOT}/demos/${stem}_switchkins.ngc`,
kinematics_type: type, runtime: "trt", model,
source_path: `linuxcnc/src/hal/user_comps/vismach/${id}-gui.py`,
subroutines: [`${stem}_switchkins_sub`, `helix_${stem.slice(3)}`],
offsets,
joint_limits: { x: [-200, 200], y: [-100, 100], z: [-120, 120], [stem[3]]: [-100, 50], c: [-36000, 36000] },
axis_limits: { x: [-200, 200], y: [-100, 100], z: [-120, 120], [stem[3]]: coordinates === "XYZAC" ? [-100, 50] : [-36000, 36000], c: [-36000, 36000] },
});
}
export const MACHINE_DEFINITIONS = Object.freeze([
trt("xyzac-trt", "XYZAC", 2, { x_rot_point: 0, y_offset: 20, z_offset: 10, tool_offset: 0 }, "trt-moving-knee"),
trt("xyzbc-trt", "XYZBC", 3, { x_rot_point: 0, y_rot_point: 0, z_rot_point: 0, x_offset: -20, y_offset: 0, z_offset: -15, tool_offset: 0 }, "trt-moving-head"),
]);
export const DEFAULT_MACHINE = MACHINE_DEFINITIONS[0];
export const MACHINE_DEFINITIONS = Object.freeze(catalog.machines.map((entry) => {
if (!entry.id || !entry.entrypoint || !entry.program || !entry.source_path || !["XYZAC", "XYZBC"].includes(entry.coordinates)) throw new Error("machine_catalog_invalid");
return Object.freeze({ ...entry, label: `${entry.coordinates} TRT`, axes: [...entry.coordinates.toLowerCase()], joint_count: entry.coordinates.length, runtime: "trt" });
}));
if (new Set(MACHINE_DEFINITIONS.map((entry) => entry.id)).size !== MACHINE_DEFINITIONS.length) throw new Error("machine_catalog_duplicate");
export const DEFAULT_MACHINE = MACHINE_DEFINITIONS.find((entry) => entry.id === catalog.default_machine);
if (!DEFAULT_MACHINE) throw new Error("machine_catalog_default_missing");
export function machineDefinition(value) {
return MACHINE_DEFINITIONS.find((entry) => [entry.id, entry.entrypoint, entry.coordinates].includes(value)) ?? null;
}

View File

@@ -3,7 +3,10 @@ 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";
import resources from "./simulation-resources.json" with { type: "json" };
import { parseIni } from "./ini-parser.mjs";
import { WorkspaceRuntime } from "./runtime.mjs";
import { readTrtConfiguration } from "./trt-configuration.mjs";
export const SIMULATION_CASE_SCHEMA = "simulation-case-bundle.v1";
const RESOURCE_KEYS = new Set([
@@ -12,53 +15,9 @@ const RESOURCE_KEYS = new Set([
"PYVCP", "NGCGUI_SUBFILE", "INTRO_GRAPHIC", "SUBROUTINE_PATH", "REMAP",
]);
const MANIFEST = new Set(SIM_FILE_INDEX);
const PRELOADED_HAL_COMPONENTS = Object.freeze(["corexykins", "lineardeltakins", "motmod", "pid", "rotarydeltakins", "stepgen", "trivkins", "xyzac-trt-kins", "xyzbc-trt-kins", "tpmod", "homemod"]);
const CASE_KINEMATICS = Object.freeze({
"linuxcnc/configs/sim/axis/corexy/corexy_by_kins.ini": Object.freeze({ type: "corexy", path_mode: "kins" }),
"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" } }),
"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({
generated_hal: "linuxcnc/configs/sim/axis/minimal_xyz_cmds.hal",
replace: "HALFILE = LIB:basic_sim.tcl",
with: "HALFILE = minimal_xyz_cmds.hal",
reason: "LinuxCNC basic_sim.tcl generated HAL command fixture",
}),
"linuxcnc/configs/sim/axis/ja_tests/xz/xz.ini": Object.freeze({
generated_hal: "linuxcnc/configs/sim/axis/ja_tests/xz/xz_cmds.hal",
replace: "HALFILE = LIB:basic_sim.tcl",
with: "HALFILE = xz_cmds.hal",
reason: "LinuxCNC basic_sim.tcl generated XZ HAL command fixture",
}),
"linuxcnc/configs/sim/axis/ja_tests/xz/xz_both.ini": Object.freeze({
generated_hal: "linuxcnc/configs/sim/axis/ja_tests/xz/xz_both_cmds.hal",
replace: "HALFILE = LIB:basic_sim.tcl",
with: "HALFILE = xz_both_cmds.hal",
reason: "LinuxCNC basic_sim.tcl generated XZ BOTH HAL command fixture",
}),
"linuxcnc/configs/sim/axis/ja_tests/xz/xz_both_auto.ini": Object.freeze({
generated_hal: "linuxcnc/configs/sim/axis/ja_tests/xz/xz_both_auto_cmds.hal",
replace: "HALFILE = LIB:basic_sim.tcl",
with: "HALFILE = xz_both_auto_cmds.hal",
reason: "LinuxCNC basic_sim.tcl generated XZ BOTH AUTO HAL command fixture",
}),
"linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini": Object.freeze({
generated_hal: "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt-headless_cmds.hal",
replace: "HALFILE = LIB:basic_sim.tcl",
with: "HALFILE = xyzac-trt-headless_cmds.hal",
reason: "LinuxCNC basic_sim.tcl generated XYZAC HAL command fixture",
}),
"linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini": Object.freeze({
generated_hal: "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt_cmds.hal",
replace: "HALFILE = LIB:basic_sim.tcl",
with: "HALFILE = xyzbc-trt_cmds.hal",
reason: "LinuxCNC basic_sim.tcl generated XYZBC HAL command fixture",
}),
});
const PRELOADED_HAL_COMPONENTS = Object.freeze(resources.preloaded_hal_components);
const CASE_KINEMATICS = Object.freeze(resources.kinematics);
const RUNTIME_FIXTURES = Object.freeze(resources.runtime_fixtures);
function clone(value) { return structuredClone(value); }
function text(value) { return typeof value === "string" && value.trim() ? value.trim() : null; }
@@ -180,7 +139,8 @@ function resourceReferences(source, content) {
}
export function findSimulationCase(path) {
return SIM_CASE_CATALOG.find((entry) => entry.path === path) ?? null;
const registered = machineDefinition(path);
return SIM_CASE_CATALOG.find((entry) => entry.path === path) ?? (registered ? { path: registered.entrypoint, coordinates: registered.coordinates, label: registered.label, status: "registered" } : null);
}
export async function loadSimulationCase({ path, fetchImpl = globalThis.fetch, baseUrl = "" } = {}) {
@@ -243,7 +203,7 @@ export async function loadSimulationCase({ path, fetchImpl = globalThis.fetch, b
}
}
const sourceIni = files[catalog.path];
const fixture = RUNTIME_FIXTURES[catalog.path];
const fixture = RUNTIME_FIXTURES[catalog.path] ?? RUNTIME_FIXTURES[machine?.runtime_fixture];
if (fixture) {
if (!inManifest(fixture.generated_hal)) missing.push(fixture.generated_hal);
else if (!files[fixture.generated_hal]) {
@@ -280,7 +240,23 @@ 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;
const trt = machineDefinition(bundle.entrypoint) ? readTrtConfiguration(bundle) : null;
let homeLimits = null;
if (!trt) {
const parsed = parseIni({ workspace: new WorkspaceRuntime({ files: bundle.files }), entrypoint: bundle.entrypoint });
const coordinates = parsed.sections.TRAJ?.COORDINATES?.replace(/\s/g, "").toUpperCase();
if (coordinates && /^[XYZABCUVW]+$/.test(coordinates)) {
const limits = {};
for (const [index, axis] of [...coordinates].entries()) {
const section = parsed.sections[`JOINT_${index}`] ?? parsed.sections[`AXIS_${axis}`];
if (!section || section.MIN_LIMIT === undefined || section.MAX_LIMIT === undefined) continue;
const range = [Number(section.MIN_LIMIT), Number(section.MAX_LIMIT)];
if (!range.every(Number.isFinite) || range[0] >= range[1]) throw new Error(`configuration_joint_limits:${index}`);
limits[axis.toLowerCase()] = range;
}
if (Object.keys(limits).length) homeLimits = { coordinates, joint_count: coordinates.length, ...limits };
}
}
return {
entrypoint: bundle.entrypoint,
files: clone(bundle.files),
@@ -288,9 +264,8 @@ export function toWorkerConfiguration(bundle) {
state_hash: bundle.state_hash,
optional_missing: clone(bundle.optional_missing ?? []),
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 } } : {}),
...(kinematics ? { kinematics } : {}),
...(homeLimits ? { home_limits: homeLimits } : {}),
...(trt ?? (kinematics ? { kinematics } : {})),
};
}

View File

@@ -0,0 +1,71 @@
{
"kinematics": {
"linuxcnc/configs/sim/axis/corexy/corexy_by_kins.ini": {
"type": "corexy",
"path_mode": "kins"
},
"linuxcnc/configs/sim/axis/corexy/corexy_by_hal.ini": {
"type": "corexy",
"path_mode": "hal"
},
"linuxcnc/configs/sim/axis/ldelta.ini": {
"type": "lineardelta",
"path_mode": "kins"
},
"linuxcnc/configs/sim/axis/rdelta.ini": {
"type": "rotarydelta",
"path_mode": "kins"
}
},
"runtime_fixtures": {
"linuxcnc/configs/sim/axis/minimal_xyz.ini": {
"generated_hal": "linuxcnc/configs/sim/axis/minimal_xyz_cmds.hal",
"replace": "HALFILE = LIB:basic_sim.tcl",
"with": "HALFILE = minimal_xyz_cmds.hal",
"reason": "LinuxCNC basic_sim.tcl generated HAL command fixture"
},
"linuxcnc/configs/sim/axis/ja_tests/xz/xz.ini": {
"generated_hal": "linuxcnc/configs/sim/axis/ja_tests/xz/xz_cmds.hal",
"replace": "HALFILE = LIB:basic_sim.tcl",
"with": "HALFILE = xz_cmds.hal",
"reason": "LinuxCNC basic_sim.tcl generated XZ HAL command fixture"
},
"linuxcnc/configs/sim/axis/ja_tests/xz/xz_both.ini": {
"generated_hal": "linuxcnc/configs/sim/axis/ja_tests/xz/xz_both_cmds.hal",
"replace": "HALFILE = LIB:basic_sim.tcl",
"with": "HALFILE = xz_both_cmds.hal",
"reason": "LinuxCNC basic_sim.tcl generated XZ BOTH HAL command fixture"
},
"linuxcnc/configs/sim/axis/ja_tests/xz/xz_both_auto.ini": {
"generated_hal": "linuxcnc/configs/sim/axis/ja_tests/xz/xz_both_auto_cmds.hal",
"replace": "HALFILE = LIB:basic_sim.tcl",
"with": "HALFILE = xz_both_auto_cmds.hal",
"reason": "LinuxCNC basic_sim.tcl generated XZ BOTH AUTO HAL command fixture"
},
"linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini": {
"generated_hal": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt-headless_cmds.hal",
"replace": "HALFILE = LIB:basic_sim.tcl",
"with": "HALFILE = xyzac-trt-headless_cmds.hal",
"reason": "LinuxCNC basic_sim.tcl generated XYZAC HAL command fixture"
},
"linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini": {
"generated_hal": "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt_cmds.hal",
"replace": "HALFILE = LIB:basic_sim.tcl",
"with": "HALFILE = xyzbc-trt_cmds.hal",
"reason": "LinuxCNC basic_sim.tcl generated XYZBC HAL command fixture"
}
},
"preloaded_hal_components": [
"corexykins",
"lineardeltakins",
"motmod",
"pid",
"rotarydeltakins",
"stepgen",
"trivkins",
"xyzac-trt-kins",
"xyzbc-trt-kins",
"tpmod",
"homemod"
]
}

View File

@@ -0,0 +1,74 @@
import { parseIni } from "./ini-parser.mjs";
import { WorkspaceRuntime } from "./runtime.mjs";
import { machineDefinition } from "./machine-definitions.mjs";
import { readTrtGeometry } from "./trt-geometry.mjs";
import { WEB_SETTINGS } from "./web-settings.mjs";
const NUMBER = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
// ABI capacity constraints from sim_io.h and sim_device_io.h, not machine defaults.
const IO_CAPACITY = Object.freeze({ spindle_count: 8, tool_capacity: 1001, encoder_count: 8 });
export function readTrtConfiguration(configuration) {
const machine = machineDefinition(configuration.entrypoint);
if (!machine) throw new Error("trt_configuration_machine_missing");
const parsed = parseIni({ workspace: new WorkspaceRuntime({ files: configuration.files }), entrypoint: configuration.entrypoint });
const sections = parsed.sections;
const number = (section, key, { positive = false, fallback } = {}) => {
const raw = sections[section]?.[key];
if (raw === undefined && fallback !== undefined) return fallback;
if (typeof raw !== "string" || !NUMBER.test(raw) || !Number.isFinite(Number(raw)) || positive && Number(raw) <= 0) throw new Error(`trt_configuration_number:[${section}]${key}`);
return Number(raw);
};
const geometry = readTrtGeometry(configuration);
const coordinates = sections.TRAJ.COORDINATES.replace(/\s/g, "").toUpperCase();
const axes = [...coordinates.toLowerCase()];
const jointCount = number("KINS", "JOINTS", { positive: true });
if (jointCount !== axes.length) throw new Error("trt_configuration_joint_count");
if (sections.TRAJ.LINEAR_UNITS !== "mm" || sections.TRAJ.ANGULAR_UNITS !== "deg") throw new Error("trt_configuration_units_unsupported");
if (!/\bsparm=identityfirst\b/.test(sections.KINS.KINEMATICS)) throw new Error("trt_configuration_requires_identityfirst");
const limits = (section) => {
const range = [number(section, "MIN_LIMIT"), number(section, "MAX_LIMIT")];
if (range[0] >= range[1]) throw new Error(`trt_configuration_limits:${section}`);
return range;
};
const jointLimits = {}, axisLimits = {}, velocity = {}, acceleration = {};
for (const [joint, axis] of axes.entries()) {
jointLimits[axis] = limits(`JOINT_${joint}`);
axisLimits[axis] = limits(`AXIS_${axis.toUpperCase()}`);
velocity[axis] = number(`AXIS_${axis.toUpperCase()}`, "MAX_VELOCITY", { positive: true });
acceleration[axis] = number(`AXIS_${axis.toUpperCase()}`, "MAX_ACCELERATION", { positive: true });
number(`JOINT_${joint}`, "MAX_VELOCITY", { positive: true });
number(`JOINT_${joint}`, "MAX_ACCELERATION", { positive: true });
for (const key of ["HOME", "HOME_OFFSET", "HOME_FINAL_VEL", "HOME_SEARCH_VEL", "HOME_LATCH_VEL", "HOME_SEQUENCE"]) if (sections[`JOINT_${joint}`][key] !== undefined) number(`JOINT_${joint}`, key);
}
const servo = number("EMCMOT", "SERVO_PERIOD", { positive: true }) / 1e9;
const task = number("TASK", "CYCLE_TIME", { positive: true });
const ticks = task / servo;
if (!Number.isSafeInteger(Math.round(ticks)) || ticks < 1 || Math.abs(ticks - Math.round(ticks)) > 1e-9) throw new Error("trt_configuration_task_servo_ratio");
if (servo > 60 || task > 10) throw new Error("trt_configuration_cycle_capacity");
const maxFeed = number("DISPLAY", "MAX_FEED_OVERRIDE", { positive: true });
if (maxFeed < 1) throw new Error("trt_configuration_feed_override");
const blendKeys = { enable: "ARC_BLEND_ENABLE", fallback_enable: "ARC_BLEND_FALLBACK_ENABLE", optimization_depth: "ARC_BLEND_OPTIMIZATION_DEPTH", gap_cycles: "ARC_BLEND_GAP_CYCLES", ramp_frequency: "ARC_BLEND_RAMP_FREQ", tangent_kink_ratio: "ARC_BLEND_TANGENT_KINK_RATIO" };
const blend = Object.fromEntries(Object.entries(blendKeys).map(([key, iniKey]) => [key, number("TRAJ", iniKey, { fallback: WEB_SETTINGS.runtime.planner.blend[key] })]));
if (![0, 1].includes(blend.enable) || ![0, 1].includes(blend.fallback_enable) || !Number.isSafeInteger(blend.optimization_depth) || blend.optimization_depth < 1 || !Number.isSafeInteger(blend.gap_cycles) || blend.gap_cycles < 0 || blend.ramp_frequency <= 0 || blend.tangent_kink_ratio <= 0) throw new Error("trt_configuration_blend");
const io = { ...Object.fromEntries(Object.entries(WEB_SETTINGS.runtime.io).filter(([key]) => key !== "cycle_time").map(([key, fallback]) => [key, number("WEB_RUNTIME", key.toUpperCase(), { positive: key !== "spindle_tolerance", fallback })])), cycle_time: task };
if (io.spindle_tolerance < 0) throw new Error("trt_configuration_spindle_tolerance");
for (const [key, maximum] of Object.entries(IO_CAPACITY)) if (!Number.isSafeInteger(io[key]) || io[key] < 1 || io[key] > maximum) throw new Error(`trt_configuration_io_capacity:${key}`);
const defaultAcceleration = number("TRAJ", "DEFAULT_LINEAR_ACCELERATION", { positive: true });
const maxAcceleration = number("TRAJ", "MAX_LINEAR_ACCELERATION", { positive: true });
return {
home_limits: { joint_count: jointCount, kinematics_type: machine.kinematics_type, coordinates, ...jointLimits },
kinematics: {
type: machine.id, path_mode: "wasm", coordinates, joint_count: jointCount, offsets: geometry.offsets,
ini_hash: parsed.state_hash,
servo_cycle_time: servo, cycle_time: servo, task_cycle_time: task,
max_linear_velocity: number("TRAJ", "MAX_LINEAR_VELOCITY", { positive: true }),
max_acceleration: Math.min(defaultAcceleration, maxAcceleration),
max_linear_acceleration: maxAcceleration,
axis_limits: axisLimits, axis_velocity_limits: velocity, axis_acceleration_limits: acceleration,
max_feed_override: maxFeed,
blend,
io,
switchkins: { 0: "identity", 1: coordinates.toLowerCase(), 2: "userk" },
},
};
}

View File

@@ -12,7 +12,7 @@ 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);
if (parsed.sections.TRAJ?.COORDINATES?.replace(/\s/g, "").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";

View File

@@ -0,0 +1,180 @@
{
"assets": {
"axis_images": "linuxcnc/share/axis/images"
},
"ui": {
"poll_ms": 50,
"jog_cycles": 1,
"run_cycles": 5,
"preview_progress_ms": 100,
"jog_increments": [
"0.1000",
"0.0100",
"0.0010",
"0.0001"
],
"initial_view": "p",
"zoom": 1,
"zoom_min": 0.5,
"zoom_max": 2,
"zoom_step": 0.2,
"layers": {
"grid": true,
"tool": true,
"path": true,
"live": true,
"limits": false,
"velocity": false,
"offsets": false,
"extents": false
},
"motion_scale_min": 0.06,
"override_debounce_ms": 80
},
"runtime": {
"planner": {
"cycle_time": 0.001,
"velocity_limit": 35,
"acceleration_limit": 300,
"max_feed_override": 2,
"compatibility_jerk_limit": 1000000000,
"blend": {
"enable": 1,
"fallback_enable": 0,
"optimization_depth": 50,
"gap_cycles": 4,
"ramp_frequency": 100,
"tangent_kink_ratio": 0.1
}
},
"io": {
"spindle_count": 2,
"tool_capacity": 1001,
"cycle_time": 0.01,
"spindle_acceleration": 600,
"spindle_tolerance": 5,
"encoder_count": 2
},
"compatibility": {
"step_fraction": 0.25,
"max_position": 1000000000.0,
"max_speed": 1000000.0,
"cycle_time": 0.001,
"tripod_base": [
[
0,
0
],
[
10,
0
],
[
5,
7.071
]
],
"linear_delta": {
"radius": 130.25,
"rod": 269
},
"rotary_delta": {
"platform": 10,
"thigh": 10,
"shin": 14,
"foot": 6
}
}
},
"viewer": {
"mm_scale": 0.01,
"pixel_ratio_limit": 2,
"camera": {
"fov": 36,
"near": 0.02,
"far": 150,
"min_distance": 0.25,
"max_distance": 65,
"damping": 0.12,
"fit_margin": 1.12,
"pan_fraction": 0.035,
"zoom_in": 0.8,
"zoom_out": 1.25,
"rotate_degrees": 15
},
"colors": {
"background": "#252b2c",
"path": "#ffff00",
"live": "#ff9a55",
"rapid": "#5c9d9a",
"limits": "#dd7566",
"extents": "#e6c56b"
},
"models": {
"XYZAC": {
"datum_z_mm": 200
},
"XYZBC": {
"datum_z_mm": 150
}
},
"materials": {
"enamel": {
"color": "#d9e1e1",
"roughness": 0.32,
"metalness": 0.25
},
"teal": {
"color": "#167f79",
"roughness": 0.3,
"metalness": 0.3
},
"dark": {
"color": "#303638",
"roughness": 0.42,
"metalness": 0.35
},
"steel": {
"color": "#b2bdc4",
"roughness": 0.27,
"metalness": 0.85
},
"polished": {
"color": "#e2e8ec",
"roughness": 0.19,
"metalness": 0.9
},
"recess": {
"color": "#111718",
"roughness": 0.66,
"metalness": 0.2
},
"rubber": {
"color": "#222729",
"roughness": 0.88
},
"carbide": {
"color": "#b9aa79",
"roughness": 0.29,
"metalness": 0.8
},
"warning": {
"color": "#e7bd48",
"roughness": 0.48,
"metalness": 0.12
},
"lamp": {
"color": "#e2f8ff",
"emissive": "#d4f2ff",
"emissiveIntensity": 1.5
}
},
"toolpath_colors": {
"background": "#050a0e",
"completed": "#d8dfe3",
"rapid": "#a85d5d",
"active": "#00ffff",
"live": "#ff6b4a"
}
}
}

View File

@@ -0,0 +1,34 @@
import values from "./web-settings.json" with { type: "json" };
function freeze(value) {
if (value && typeof value === "object") { Object.values(value).forEach(freeze); Object.freeze(value); }
return value;
}
function finitePositive(value, name) {
if (!Number.isFinite(value) || value <= 0) throw new Error(`web_settings_invalid:${name}`);
}
for (const [name, value] of Object.entries(values.runtime.planner)) if (name !== "blend") finitePositive(value, `planner.${name}`);
for (const [name, value] of Object.entries(values.runtime.io)) {
if (name === "spindle_tolerance") {
if (!Number.isFinite(value) || value < 0) throw new Error("web_settings_invalid:io.spindle_tolerance");
} else finitePositive(value, `io.${name}`);
}
for (const [name, value] of Object.entries(values.viewer.camera)) finitePositive(value, `camera.${name}`);
finitePositive(values.viewer.mm_scale, "mm_scale");
finitePositive(values.viewer.pixel_ratio_limit, "pixel_ratio_limit");
for (const key of ["poll_ms", "jog_cycles", "run_cycles", "preview_progress_ms", "zoom", "zoom_min", "zoom_max", "zoom_step"]) finitePositive(values.ui[key], key);
finitePositive(values.ui.override_debounce_ms, "override_debounce_ms");
if (!(values.ui.motion_scale_min > 0 && values.ui.motion_scale_min <= 1)) throw new Error("web_settings_invalid:motion_scale_min");
if (!values.ui.jog_increments.every((value) => Number.isFinite(Number(value)) && Number(value) > 0)) throw new Error("web_settings_invalid:jog_increments");
if (values.ui.zoom_min > values.ui.zoom_max || values.ui.zoom < values.ui.zoom_min || values.ui.zoom > values.ui.zoom_max) throw new Error("web_settings_invalid:zoom_range");
if (!["x", "y", "y2", "z", "z2", "p"].includes(values.ui.initial_view)) throw new Error("web_settings_invalid:initial_view");
for (const key of ["jog_cycles", "run_cycles"]) if (!Number.isSafeInteger(values.ui[key])) throw new Error(`web_settings_invalid:${key}`);
if (values.viewer.camera.near >= values.viewer.camera.far || values.viewer.camera.min_distance >= values.viewer.camera.max_distance) throw new Error("web_settings_invalid:camera_range");
for (const model of Object.values(values.viewer.models)) finitePositive(model.datum_z_mm, "model_datum");
if (values.runtime.planner.max_feed_override < 1) throw new Error("web_settings_invalid:feed_limit");
for (const key of ["spindle_count", "tool_capacity", "encoder_count"]) if (!Number.isSafeInteger(values.runtime.io[key])) throw new Error(`web_settings_invalid:io.${key}`);
export const WEB_SETTINGS = freeze(values);
export function axisImagePath(name, base = ".") {
return `${base.replace(/\/$/, "")}/${WEB_SETTINGS.assets.axis_images}/${name}`;
}