Advance simulation parity and 3-axis tutorial coverage
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { fnv1a64 } from "../worker/replay.mjs";
|
||||
import { runtimeResourceFixture } from "./resource-resolver.mjs";
|
||||
import { SIM_CASE_CATALOG, SIM_FILE_INDEX, SIM_HAL_COMPONENTS } from "./sim-case-catalog.mjs";
|
||||
|
||||
export const SIMULATION_CASE_SCHEMA = "simulation-case-bundle.v1";
|
||||
@@ -61,26 +62,39 @@ function tokens(value) {
|
||||
function candidates(source, raw, key = null, section = null) {
|
||||
const value = stripQuotes(raw);
|
||||
const first = tokens(value)[0] ?? value;
|
||||
if (first.startsWith("/") || first.startsWith("~") || first.startsWith("$")) return [first];
|
||||
if (String(key).toUpperCase() === "INTRO_GRAPHIC" && String(section).toUpperCase() === "DISPLAY" && first === "linuxcnc.gif") return ["linuxcnc/linuxcnc.gif"];
|
||||
if (first.startsWith("LIB:")) {
|
||||
const name = first.slice(4);
|
||||
if (!name || name.includes("..") || name.startsWith("/")) return [];
|
||||
const libraryCandidates = [`linuxcnc/lib/hallib/${name}`, `linuxcnc/lib/${name}`].map(normalize).filter(Boolean);
|
||||
const indexed = libraryCandidates.filter(inManifest);
|
||||
return indexed.length ? indexed : libraryCandidates;
|
||||
const libraryPath = normalize(`linuxcnc/lib/hallib/${name}`);
|
||||
return libraryPath ? [libraryPath] : [];
|
||||
}
|
||||
if (["HAL", "HALFILE"].includes(String(key).toUpperCase()) && !first.includes("/") && !first.startsWith("linuxcnc/")) {
|
||||
const halCandidates = [normalize(`${dirname(source)}/${first}`), `linuxcnc/lib/hallib/${first}`, `linuxcnc/lib/${first}`].filter(Boolean);
|
||||
const halCandidates = [normalize(`${dirname(source)}/${first}`), `linuxcnc/lib/hallib/${first}`].filter(Boolean);
|
||||
const indexed = halCandidates.filter(inManifest);
|
||||
return indexed.length ? indexed : halCandidates;
|
||||
return indexed.length ? [indexed[0]] : halCandidates;
|
||||
}
|
||||
if (first.startsWith("linuxcnc/")) return [normalize(first)].filter(Boolean);
|
||||
return [normalize(`${dirname(source)}/${first}`)].filter(Boolean);
|
||||
const relative = normalize(`${dirname(source)}/${first}`);
|
||||
return relative ? [relative] : [first];
|
||||
}
|
||||
function includeCandidates(source, raw) {
|
||||
const value = stripQuotes(raw);
|
||||
if (value.startsWith("linuxcnc/")) return [normalize(value)].filter(Boolean);
|
||||
return [normalize(`${dirname(source)}/${value}`)].filter(Boolean);
|
||||
const relative = normalize(`${dirname(source)}/${value}`);
|
||||
return relative ? [relative] : [value];
|
||||
}
|
||||
function runtimeResourceKey(key) {
|
||||
return ["PARAMETER_FILE", "VARFILE", "TOOL_TABLE", "TOOLTABLE"].includes(String(key ?? "").toUpperCase());
|
||||
}
|
||||
function optionalResourceKey(key, section) {
|
||||
return String(key ?? "").toUpperCase() === "INTRO_GRAPHIC" && String(section ?? "").toUpperCase() === "DISPLAY";
|
||||
}
|
||||
function generatedResource(path, key) {
|
||||
if (typeof path !== "string" || path.startsWith("/") || path.startsWith("~") || path.startsWith("$") || path.split("/").includes("..")) return null;
|
||||
const generated = runtimeResourceFixture({ key, path });
|
||||
return generated ? { ...generated, data_hash: fnv1a64(generated.content) } : null;
|
||||
}
|
||||
function inManifest(path) { return MANIFEST.has(path); }
|
||||
function responseUrl(baseUrl, path) { return `${String(baseUrl ?? "").replace(/\/$/, "")}/${path}`.replace(/^\//, "/"); }
|
||||
@@ -97,7 +111,7 @@ function resourceReferences(source, content) {
|
||||
for (const rawLine of content.split(/\r?\n/)) {
|
||||
const include = rawLine.trim().match(/^#?INCLUDE\s*(?:=\s*)?(.+)$/i);
|
||||
if (include) {
|
||||
references.push(...includeCandidates(source, include[1]));
|
||||
references.push(...includeCandidates(source, include[1]).map((path) => ({ path, key: null, source })));
|
||||
continue;
|
||||
}
|
||||
const line = rawLine.split("#", 1)[0].trim();
|
||||
@@ -108,11 +122,18 @@ function resourceReferences(source, content) {
|
||||
const key = assignment[1].trim().toUpperCase();
|
||||
const values = tokens(assignment[2]);
|
||||
const resourceValues = key === "PROGRAMS" ? values : values.slice(0, 1);
|
||||
for (const value of resourceValues) references.push(...candidates(source, value, key, section));
|
||||
for (const value of resourceValues) references.push(...candidates(source, value, key, section).map((path) => ({ path, key, section, source })));
|
||||
continue;
|
||||
}
|
||||
const halSource = line.match(/^source\s+(.+)$/i);
|
||||
if (halSource && !halSource[1].trim().startsWith("[")) references.push(...candidates(source, halSource[1], "HAL", section));
|
||||
if (halSource && !halSource[1].trim().startsWith("[")) references.push(...candidates(source, halSource[1], "HAL", section).map((path) => ({ path, key: "HAL", section, source })));
|
||||
const halUser = line.match(/^loadusr\s+(?:-\S+\s+)*(.*)$/i);
|
||||
if (halUser && source === "linuxcnc/configs/sim/axis/vismach/3axis-tutorial/3axis-tutorial.hal") {
|
||||
const program = tokens(halUser[1])[0] ?? "";
|
||||
if (["./3axis-tutorial", "3axis-tutorial"].includes(program)) {
|
||||
references.push(...candidates(source, program, "PYTHON", section).map((path) => ({ path, key: "PYTHON", section, source, capability: "WEB-EQUIV" })));
|
||||
}
|
||||
}
|
||||
}
|
||||
return references;
|
||||
}
|
||||
@@ -128,17 +149,34 @@ export async function loadSimulationCase({ path, fetchImpl = globalThis.fetch, b
|
||||
const files = {};
|
||||
const fetched = [];
|
||||
const missing = [];
|
||||
const optionalMissing = [];
|
||||
const generatedResources = new Map();
|
||||
const runtimeFixtures = [];
|
||||
const queue = [catalog.path];
|
||||
const queued = new Set(queue);
|
||||
while (queue.length) {
|
||||
const current = queue.shift();
|
||||
const content = await fetchText(fetchImpl, baseUrl, current);
|
||||
const generated = generatedResources.get(current);
|
||||
const content = generated ? generated.content : await fetchText(fetchImpl, baseUrl, current);
|
||||
if (content === null) { missing.push(current); continue; }
|
||||
files[current] = content;
|
||||
fetched.push(current);
|
||||
for (const candidate of resourceReferences(current, content)) {
|
||||
if (!inManifest(candidate)) { missing.push(candidate); continue; }
|
||||
for (const reference of resourceReferences(current, content)) {
|
||||
const candidate = reference.path;
|
||||
if (!inManifest(candidate)) {
|
||||
const generated = runtimeResourceKey(reference.key) ? generatedResource(candidate, reference.key) : null;
|
||||
if (generated) {
|
||||
if (!generatedResources.has(candidate)) {
|
||||
generatedResources.set(candidate, generated);
|
||||
files[candidate] = generated.content;
|
||||
fetched.push(candidate);
|
||||
runtimeFixtures.push({ case_path: catalog.path, resource_path: candidate, key: reference.key, kind: generated.kind, reason: generated.reason, data_hash: generated.data_hash });
|
||||
}
|
||||
} else if (optionalResourceKey(reference.key, reference.section)) {
|
||||
optionalMissing.push({ path: candidate, key: reference.key, section: reference.section ?? null, source: reference.source ?? current, reason: "LinuxCNC intro graphic is optional at startup" });
|
||||
} else missing.push(candidate);
|
||||
continue;
|
||||
}
|
||||
if (queued.has(candidate)) continue;
|
||||
queued.add(candidate);
|
||||
queue.push(candidate);
|
||||
@@ -165,6 +203,7 @@ export async function loadSimulationCase({ path, fetchImpl = globalThis.fetch, b
|
||||
files,
|
||||
fetched: [...fetched].sort(),
|
||||
missing: [...new Set(missing)].sort(),
|
||||
optional_missing: [...new Map(optionalMissing.map((row) => [`${row.source}:${row.path}:${row.key}`, row])).values()].sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right))),
|
||||
runtime_fixtures: runtimeFixtures,
|
||||
};
|
||||
return {
|
||||
@@ -183,6 +222,7 @@ export function toWorkerConfiguration(bundle) {
|
||||
files: clone(bundle.files),
|
||||
halManifest: clone(bundle.halManifest ?? []),
|
||||
state_hash: bundle.state_hash,
|
||||
optional_missing: clone(bundle.optional_missing ?? []),
|
||||
runtime_fixtures: clone(bundle.runtime_fixtures ?? []),
|
||||
...(CASE_KINEMATICS[bundle.entrypoint] ? { kinematics: clone(CASE_KINEMATICS[bundle.entrypoint]) } : {}),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user