Files
cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/runtime/tool-db-simulation.js
2026-07-02 10:10:54 -04:00

1094 lines
38 KiB
JavaScript

const TOOL_WORD_PATTERN = /([A-Z])\s*([-+]?(?:\d+(?:\.\d*)?|\.\d+))/gi;
const TOOL_DB_STORAGE_ROOT = "web-rtcp-5axis-xyzbc-trt-sim-plan/tool-db";
const TOOL_OFFSET_AXES = ["x", "y", "z", "a", "b", "c", "u", "v", "w"];
const TOOL_PARAMETER_KEYS = {
toolNumber: 5400,
x: 5401,
y: 5402,
z: 5403,
a: 5404,
b: 5405,
c: 5406,
u: 5407,
v: 5408,
w: 5409,
diameter: 5410,
frontAngle: 5411,
backAngle: 5412,
orientation: 5413,
};
let browserMemoryToolDbStorage = null;
export function parseLinuxCncToolTable(text = "", options = {}) {
const randomToolChanger = Boolean(options.randomToolChanger);
const pockets = [createEmptyToolEntry({ idx: 0, pocket: 0, toolNumber: 0 })];
const diagnostics = [];
const lines = String(text).split(/\r?\n/);
let nonRandomIdx = 1;
lines.forEach((rawLine, index) => {
const lineNumber = index + 1;
const [bodyPart, ...commentParts] = rawLine.split(";");
const body = bodyPart.trim();
const comment = commentParts.join(";").trim();
if (!body) return;
const words = {};
for (const match of body.matchAll(TOOL_WORD_PATTERN)) {
words[match[1].toUpperCase()] = Number(match[2]);
}
if (!Number.isFinite(words.T)) {
diagnostics.push({
line: lineNumber,
severity: "warning",
message: "tool table row missing T word",
rawLine,
});
return;
}
if (!Number.isFinite(words.P)) {
diagnostics.push({
line: lineNumber,
severity: "warning",
message: "tool table row missing P word",
rawLine,
});
}
const realPocket = Number.isFinite(words.P) ? words.P : words.T;
const idx = randomToolChanger ? realPocket : nonRandomIdx;
if (!randomToolChanger) nonRandomIdx += 1;
const entry = normalizeToolEntry({
idx,
line: lineNumber,
rawLine,
comment,
toolNumber: words.T,
pocket: realPocket,
diameter: Number.isFinite(words.D) ? words.D : 0,
orientation: Number.isFinite(words.Q) ? words.Q : 0,
frontAngle: Number.isFinite(words.I) ? words.I : 0,
backAngle: Number.isFinite(words.J) ? words.J : 0,
offset: offsetFromWords(words),
});
if (pockets[idx]?.toolNumber > 0) {
diagnostics.push({
line: lineNumber,
severity: "warning",
message: `multiple tools assigned to internal pocket/index ${idx}`,
rawLine,
});
}
pockets[idx] = entry;
});
const entries = compactToolEntries(pockets);
return {
apiName: "web-rtcp-5axis-tool-table",
sourceRel: options.sourceRel || null,
path: options.path || null,
randomToolChanger,
entries,
pockets: normalizePockets(pockets),
toolCount: entries.length,
diagnostics,
rawText: String(text),
semanticBoundary: "linuxcnc_tool_table_text_parsed_for_web_simulation",
linuxCncReferences: [
"linuxcnc/src/emc/tooldata/tooldata_common.cc:70",
"linuxcnc/src/emc/tooldata/tooldata_common.cc:365",
"linuxcnc/src/emc/nml_intf/emctool.h:29",
],
};
}
export function serializeLinuxCncToolTable(toolTable) {
const table = normalizeToolTable(toolTable);
const startIdx = table.randomToolChanger ? 0 : 1;
const lines = [];
for (let idx = startIdx; idx < table.pockets.length; idx += 1) {
const entry = table.pockets[idx];
if (!entry || entry.toolNumber < 0) continue;
lines.push(formatLinuxCncToolLine(entry, table.randomToolChanger));
}
return `${lines.join("\n")}${lines.length ? "\n" : ""}`;
}
export function createToolDbSimulation({ toolTable = null, profile = null, storageMode = null } = {}) {
const normalizedTable = normalizeToolTable(toolTable?.apiName === "web-rtcp-5axis-tool-table"
? toolTable
: parseLinuxCncToolTable("", { sourceRel: profile?.toolTablePath || null }));
const spindleEntry = normalizedTable.pockets[0] || createEmptyToolEntry({ idx: 0, pocket: 0, toolNumber: 0 });
return {
apiName: "web-rtcp-5axis-tool-db-simulation",
ready: true,
processReady: true,
processScope: "web_simulation_only",
hostProcessReady: false,
hostProcessExecution: false,
dbProgramReady: false,
storageMode: storageMode || null,
storageCapability: null,
profileId: profile?.id || null,
toolTablePath: normalizedTable.sourceRel || profile?.toolTablePath || null,
randomToolChanger: normalizedTable.randomToolChanger,
toolTable: normalizedTable,
toolInSpindle: spindleEntry.toolNumber > 0 ? spindleEntry.toolNumber : 0,
toolFromPocket: spindleEntry.toolNumber > 0 ? spindleEntry.pocket : 0,
pocketPrepped: -1,
preparedTool: null,
preparedPocket: null,
preparedIndex: null,
currentPocket: spindleEntry.toolNumber > 0 ? spindleEntry.idx : 0,
activeToolOffset: null,
toolTableCurrent: cloneToolEntry(spindleEntry),
iocontrol: createInitialIoControlPins(spindleEntry),
emcioStatus: createInitialEmcioStatus(spindleEntry),
interpreterParameters: createToolParameters(null),
events: [],
lastSavedText: null,
lastSavedPath: null,
semanticBoundary: "linuxcnc_tool_db_web_simulation_not_native_db_program",
linuxCncReferences: [
"linuxcnc/src/emc/task/taskclass.cc:281",
"linuxcnc/src/emc/task/taskclass.cc:425",
"linuxcnc/src/emc/task/taskclass.cc:482",
"linuxcnc/src/emc/task/taskclass.cc:614",
"linuxcnc/src/emc/rs274ngc/interp_convert.cc:6236",
"linuxcnc/src/emc/task/emccanon.cc:3075",
],
};
}
export function listToolEntries(toolDb) {
return compactToolEntries(normalizeToolTable(toolDb?.toolTable).pockets).map(cloneToolEntry);
}
export function queryToolEntry(toolDb, selector = {}) {
const table = normalizeToolTable(toolDb?.toolTable);
const entry = resolveToolEntry(table, selector);
return entry ? cloneToolEntry(entry) : null;
}
export function prepareTool(toolDb, toolNumber, options = {}) {
let state = cloneToolDb(toolDb);
const idx = findToolIndexForTool(state.toolTable, toolNumber, state.randomToolChanger);
const entry = idx >= 0 ? state.toolTable.pockets[idx] : null;
state.preparedTool = Number(toolNumber);
state.preparedIndex = idx >= 0 ? idx : null;
state.preparedPocket = entry?.pocket ?? null;
if (idx === -1) {
state.preparedTool = null;
state.pocketPrepped = -1;
state.iocontrol.toolPrepIndex = -1;
state.iocontrol.toolPrepNumber = 0;
state.iocontrol.toolPrepPocket = 0;
state.iocontrol.toolPrepare = false;
state.iocontrol.toolPrepared = false;
state.emcioStatus.status = "ERROR";
state.events.push(createToolDbEvent("T", {
toolNumber: Number(toolNumber),
idx,
pocket: null,
found: false,
error: "tool_number_not_found",
linuxCncReference: "linuxcnc/src/emc/task/taskclass.cc:425",
}));
return state;
} else {
state.iocontrol.toolPrepIndex = idx;
if (state.randomToolChanger || idx !== 0) {
state.iocontrol.toolPrepNumber = entry?.toolNumber ?? 0;
state.iocontrol.toolPrepPocket = entry?.pocket ?? 0;
} else {
state.iocontrol.toolPrepNumber = 0;
state.iocontrol.toolPrepPocket = 0;
}
}
state.iocontrol.toolPrepare = true;
state.iocontrol.toolPrepared = options.toolPrepared ?? true;
state.emcioStatus.status = "EXEC";
state.events.push(createToolDbEvent("T", {
toolNumber: Number(toolNumber),
idx,
pocket: entry?.pocket ?? null,
found: Boolean(entry),
linuxCncReference: "linuxcnc/src/emc/task/taskclass.cc:425",
}));
if (options.autoReadToolInputs !== false) {
state = readToolInputs(state);
}
return state;
}
export function loadPreparedTool(toolDb, options = {}) {
let state = cloneToolDb(toolDb);
if (state.pocketPrepped === -1 && state.iocontrol.toolPrepare && state.iocontrol.toolPrepared) {
state = readToolInputs(state);
}
const idx = state.pocketPrepped;
const entry = idx >= 0 ? state.toolTable.pockets[idx] : null;
if (state.randomToolChanger && idx === 0) {
state.events.push(createToolDbEvent("M6", {
idx,
noOp: true,
reason: "random_toolchanger_spindle_pocket",
}));
return state;
}
if (!state.randomToolChanger && idx > 0 && state.toolInSpindle === entry?.toolNumber) {
state.events.push(createToolDbEvent("M6", {
idx,
toolNumber: state.toolInSpindle,
noOp: true,
reason: "tool_already_in_spindle",
}));
return state;
}
if (idx !== -1) {
state.iocontrol.toolChange = true;
state.iocontrol.toolChanged = options.toolChanged ?? true;
state.emcioStatus.status = "EXEC";
}
state.events.push(createToolDbEvent("M6", {
idx,
toolNumber: entry?.toolNumber ?? 0,
pocket: entry?.pocket ?? 0,
found: Boolean(entry),
linuxCncReference: "linuxcnc/src/emc/task/taskclass.cc:482",
}));
if (options.autoReadToolInputs !== false) {
state = readToolInputs(state);
}
return state;
}
export function readToolInputs(toolDb) {
let state = cloneToolDb(toolDb);
if (state.iocontrol.toolPrepare && state.iocontrol.toolPrepared) {
state.pocketPrepped = state.iocontrol.toolPrepIndex;
state.emcioStatus.tool.pocketPrepped = state.pocketPrepped;
state.iocontrol.toolPrepare = false;
state.emcioStatus.status = "DONE";
state.events.push(createToolDbEvent("READ_TOOL_INPUTS_PREPARED", {
pocketPrepped: state.pocketPrepped,
linuxCncReference: "linuxcnc/src/emc/task/taskclass.cc:616",
}));
}
if (state.iocontrol.toolChange && state.iocontrol.toolChanged) {
const idx = state.pocketPrepped;
let loaded = createEmptyToolEntry({ idx: 0, pocket: 0, toolNumber: 0 });
if (!state.randomToolChanger && idx === 0) {
state.toolTable.pockets[0] = loaded;
} else {
const entry = state.toolTable.pockets[idx] || createEmptyToolEntry({ idx, pocket: 0, toolNumber: 0 });
loaded = cloneToolEntry({
...entry,
idx: 0,
});
if (state.randomToolChanger) {
loaded.pocket = 0;
const previousSpindle = state.toolTable.pockets[0] || createEmptyToolEntry({ idx: 0, pocket: 0, toolNumber: 0 });
state.toolTable.pockets[idx] = normalizeToolEntry({
...previousSpindle,
idx,
pocket: entry.pocket,
});
}
state.toolTable.pockets[0] = loaded;
}
state.toolTable = refreshToolTable(state.toolTable);
state.toolInSpindle = loaded.toolNumber > 0 ? loaded.toolNumber : 0;
state.toolFromPocket = state.toolInSpindle > 0 ? loaded.pocket : 0;
state.currentPocket = state.toolInSpindle > 0 ? idx : 0;
state.toolTableCurrent = cloneToolEntry(loaded);
state.iocontrol.toolNumber = state.toolInSpindle;
state.iocontrol.toolFromPocket = state.toolFromPocket;
state.emcioStatus.tool.toolInSpindle = state.toolInSpindle;
state.emcioStatus.tool.toolFromPocket = state.toolFromPocket;
state.emcioStatus.tool.pocketPrepped = -1;
state.pocketPrepped = -1;
state.preparedTool = null;
state.preparedPocket = null;
state.preparedIndex = null;
state.iocontrol.toolPrepNumber = 0;
state.iocontrol.toolPrepPocket = 0;
state.iocontrol.toolPrepIndex = 0;
state.iocontrol.toolChange = false;
state.emcioStatus.status = "DONE";
state.events.push(createToolDbEvent("READ_TOOL_INPUTS_CHANGED", {
toolNumber: state.toolInSpindle,
toolFromPocket: state.toolFromPocket,
currentPocket: state.currentPocket,
linuxCncReference: "linuxcnc/src/emc/task/taskclass.cc:623",
}));
}
return state;
}
export function applyToolLengthOffset(toolDb, toolNumber = null) {
const state = cloneToolDb(toolDb);
const table = normalizeToolTable(state.toolTable);
const idx = Number.isFinite(Number(toolNumber))
? findToolIndexForTool(table, Number(toolNumber), state.randomToolChanger)
: 0;
const entry = idx >= 0 ? table.pockets[idx] : null;
state.activeToolOffset = entry
? createActiveToolOffset(entry, idx)
: null;
state.interpreterParameters = createToolParameters(state.activeToolOffset);
state.events.push(createToolDbEvent("G43", {
idx,
toolNumber: toolNumber ?? state.toolInSpindle,
activeToolOffset: state.activeToolOffset,
found: Boolean(entry),
linuxCncReference: "linuxcnc/src/emc/rs274ngc/interp_convert.cc:6236",
}));
return state;
}
export function setToolNumber(toolDb, toolNumber) {
let state = cloneToolDb(toolDb);
if (!state.randomToolChanger && Number(toolNumber) === 0) {
const unloaded = createEmptyToolEntry({ idx: 0, pocket: 0, toolNumber: 0 });
state.toolTable.pockets[0] = unloaded;
state.toolTable = refreshToolTable(state.toolTable);
state.toolInSpindle = 0;
state.toolFromPocket = 0;
state.currentPocket = 0;
state.toolTableCurrent = cloneToolEntry(unloaded);
state.iocontrol.toolNumber = 0;
state.iocontrol.toolFromPocket = 0;
state.emcioStatus.tool.toolInSpindle = 0;
state.emcioStatus.tool.toolFromPocket = 0;
state.events.push(createToolDbEvent("M61", {
toolNumber: 0,
idx: 0,
pocket: 0,
found: true,
linuxCncReference: "linuxcnc/src/emc/task/taskclass.cc:572",
}));
return state;
}
const idx = findToolIndexForTool(state.toolTable, toolNumber, state.randomToolChanger);
const entry = idx >= 0 ? state.toolTable.pockets[idx] : null;
if (!entry) {
state.events.push(createToolDbEvent("M61", {
toolNumber: Number(toolNumber),
idx,
found: false,
}));
return state;
}
state.toolTable.pockets[0] = cloneToolEntry({ ...entry, idx: 0 });
state.toolTable = refreshToolTable(state.toolTable);
state.toolInSpindle = entry.toolNumber;
state.toolFromPocket = entry.toolNumber > 0 ? entry.pocket : 0;
state.currentPocket = idx;
state.toolTableCurrent = cloneToolEntry(state.toolTable.pockets[0]);
state.iocontrol.toolNumber = state.toolInSpindle;
state.iocontrol.toolFromPocket = state.toolFromPocket;
state.emcioStatus.tool.toolInSpindle = state.toolInSpindle;
state.emcioStatus.tool.toolFromPocket = state.toolFromPocket;
state.events.push(createToolDbEvent("M61", {
toolNumber: state.toolInSpindle,
idx,
pocket: state.toolFromPocket,
found: true,
linuxCncReference: "linuxcnc/src/emc/task/taskclass.cc:572",
}));
return state;
}
export function editToolEntry(toolDb, patch = {}) {
const state = cloneToolDb(toolDb);
const selector = {
idx: patch.idx,
toolNumber: patch.toolNumber ?? patch.toolno,
pocket: patch.pocket ?? patch.pocketno,
};
const current = resolveToolEntry(state.toolTable, selector);
const idx = Number.isFinite(Number(patch.idx))
? Number(patch.idx)
: current?.idx ?? nextAvailableToolIndex(state.toolTable);
const nextEntry = normalizeToolEntry({
...(current || createEmptyToolEntry({ idx, pocket: patch.pocket ?? idx, toolNumber: patch.toolNumber ?? idx })),
...patch,
idx,
toolNumber: patch.toolNumber ?? patch.toolno ?? current?.toolNumber ?? idx,
pocket: patch.pocket ?? patch.pocketno ?? current?.pocket ?? idx,
diameter: patch.diameter ?? current?.diameter ?? 0,
frontAngle: patch.frontAngle ?? patch.frontangle ?? current?.frontAngle ?? 0,
backAngle: patch.backAngle ?? patch.backangle ?? current?.backAngle ?? 0,
orientation: patch.orientation ?? current?.orientation ?? 0,
offset: {
...(current?.offset || createZeroOffset()),
...(patch.offset || {}),
...(Number.isFinite(Number(patch.length)) ? { z: Number(patch.length) } : {}),
...(Number.isFinite(Number(patch.toolLength)) ? { z: Number(patch.toolLength) } : {}),
...(Number.isFinite(Number(patch.zOffset)) ? { z: Number(patch.zOffset) } : {}),
},
});
state.toolTable.pockets[idx] = nextEntry;
if (state.currentPocket === idx || state.toolInSpindle === nextEntry.toolNumber) {
state.toolTable.pockets[0] = cloneToolEntry({ ...nextEntry, idx: 0 });
state.toolTableCurrent = cloneToolEntry(state.toolTable.pockets[0]);
state.toolInSpindle = nextEntry.toolNumber;
state.toolFromPocket = nextEntry.pocket;
state.iocontrol.toolNumber = state.toolInSpindle;
state.iocontrol.toolFromPocket = state.toolFromPocket;
state.emcioStatus.tool.toolInSpindle = state.toolInSpindle;
state.emcioStatus.tool.toolFromPocket = state.toolFromPocket;
}
if (state.activeToolOffset?.idx === idx || state.activeToolOffset?.toolNumber === nextEntry.toolNumber) {
state.activeToolOffset = createActiveToolOffset(nextEntry, idx);
state.interpreterParameters = createToolParameters(state.activeToolOffset);
}
state.toolTable = refreshToolTable(state.toolTable);
state.events.push(createToolDbEvent("EDIT_TOOL", {
idx,
toolNumber: nextEntry.toolNumber,
pocket: nextEntry.pocket,
linuxCncReference: "linuxcnc/src/emc/task/taskclass.cc:527",
}));
return state;
}
export async function saveToolDbSimulation(toolDb, options = {}) {
const state = cloneToolDb(toolDb);
const text = serializeLinuxCncToolTable(state.toolTable);
const path = options.path || toolDbStoragePath(state);
const storageInfo = resolveToolDbStorage(options);
await saveTextFile(path, text, storageInfo.storage);
state.lastSavedText = text;
state.lastSavedPath = path;
state.storageMode = options.storageMode || storageInfo.mode;
state.storageCapability = storageInfo.capability;
state.events.push(createToolDbEvent("SAVE_TOOL_TABLE", {
path,
bytes: text.length,
storageMode: state.storageMode,
linuxCncReference: "linuxcnc/src/emc/tooldata/tooldata_common.cc:383",
}));
return {
toolDb: state,
path,
text,
storageMode: state.storageMode,
storageCapability: storageInfo.capability,
semanticBoundary: "tool_table_saved_in_web_storage_boundary",
};
}
export function applyToolCommandSequence(toolDb, commands = []) {
return commands.reduce((state, command) => {
const code = String(command.code || command).toUpperCase();
if (code === "T") return prepareTool(state, command.toolNumber);
if (code === "M6") return loadPreparedTool(state);
if (code === "G43") return applyToolLengthOffset(state, command.toolNumber ?? command.h ?? null);
if (code === "M61") return setToolNumber(state, command.toolNumber ?? command.q);
throw new Error(`unsupported tool DB simulation command: ${code}`);
}, toolDb);
}
export function extractToolCommandSequenceFromProgram(programText = "") {
const commands = [];
const lines = String(programText).split(/\r?\n/);
lines.forEach((rawLine, index) => {
const line = rawLine
.replace(/\([^)]*\)/g, " ")
.split(";")[0];
const words = [...line.matchAll(/([GMTQH])\s*([-+]?(?:\d+(?:\.\d*)?|\.\d+))/gi)]
.map((match) => ({
letter: match[1].toUpperCase(),
value: Number(match[2]),
index: match.index ?? 0,
}))
.filter((word) => Number.isFinite(word.value));
if (!words.length) return;
const hWord = words.find((word) => word.letter === "H");
const qWord = words.find((word) => word.letter === "Q");
for (const word of words) {
if (word.letter === "T") {
commands.push(createProgramToolCommand("T", index + 1, rawLine, {
toolNumber: word.value,
}));
} else if (word.letter === "M" && word.value === 6) {
commands.push(createProgramToolCommand("M6", index + 1, rawLine));
} else if (word.letter === "M" && word.value === 61 && Number.isFinite(qWord?.value)) {
commands.push(createProgramToolCommand("M61", index + 1, rawLine, {
q: qWord?.value,
toolNumber: qWord?.value,
}));
} else if (word.letter === "G" && word.value === 43) {
commands.push(createProgramToolCommand("G43", index + 1, rawLine, {
h: hWord?.value,
toolNumber: hWord?.value,
}));
}
}
});
return commands;
}
export function createToolDbReadiness(toolDb) {
const ready = toolDb?.processReady === true && toolDb?.processScope === "web_simulation_only";
return {
apiName: "web-rtcp-5axis-tool-db-readiness",
ready,
toolDbProcessReady: ready,
toolDbProcessScope: ready ? "web_simulation_only" : "not_ready",
hostToolDbProcessReady: false,
hostProcessExecution: false,
dbProgramReady: false,
toolCount: toolDb?.toolTable?.toolCount || 0,
toolTablePath: toolDb?.toolTablePath || null,
storageMode: toolDb?.storageMode || null,
semanticBoundary: "tool_db_process_ready_for_web_simulation_only",
};
}
export function createToolRuntimeState(toolDb, options = {}) {
if (!toolDb) {
return {
apiName: "web-rtcp-5axis-tool-runtime-state",
ready: false,
activeOffsetApplied: false,
toolInSpindle: 0,
toolFromPocket: 0,
currentPocket: 0,
activeToolNumber: 0,
activePocket: 0,
currentTool: null,
activeToolOffset: null,
kinematics: {
toolOffsetZ: 0,
source: "tool_db_not_ready",
},
pathTool: createPathTool(null, 0),
vismach: {
toolOffset: finiteNumber(options.fallbackToolLength, 0),
source: "fallback_tool_preview_length",
},
interpreterParameters: createToolParameters(null),
semanticBoundary: "linuxcnc_current_tool_offset_runtime_state_for_web_simulation",
};
}
const state = cloneToolDb(toolDb);
const activeOffset = state.activeToolOffset
? {
...state.activeToolOffset,
offset: { ...state.activeToolOffset.offset },
}
: null;
const currentTool = state.toolTableCurrent
? cloneToolEntry(state.toolTableCurrent)
: cloneToolEntry(state.toolTable.pockets[0] || createEmptyToolEntry({ idx: 0, pocket: 0, toolNumber: 0 }));
const activeToolNumber = activeOffset?.toolNumber
?? (currentTool.toolNumber > 0 ? currentTool.toolNumber : 0);
const activePocket = activeOffset?.pocket
?? (currentTool.toolNumber > 0 ? currentTool.pocket : 0);
const activeOffsetZ = activeOffset ? finiteNumber(activeOffset.offset?.z, 0) : 0;
return {
apiName: "web-rtcp-5axis-tool-runtime-state",
ready: state.processReady === true,
activeOffsetApplied: Boolean(activeOffset),
toolInSpindle: state.toolInSpindle,
toolFromPocket: state.toolFromPocket,
currentPocket: state.currentPocket,
activeToolNumber,
activePocket,
currentTool,
activeToolOffset: activeOffset,
kinematics: {
toolOffsetZ: activeOffsetZ,
source: activeOffset ? "activeToolOffset.offset.z" : "no_active_g43_tool_length_offset",
halPin: "motion.tooloffset.z",
downstreamPins: ["xyzbc-trt-kins.tool-offset", "xyzbc-trt-gui.tool-offset"],
},
pathTool: createPathTool(activeOffset || currentTool, activeOffsetZ),
vismach: {
toolOffset: activeOffset
? activeOffsetZ
: finiteNumber(options.fallbackToolLength, 0),
source: activeOffset ? "activeToolOffset.offset.z" : "fallback_tool_preview_length",
},
interpreterParameters: { ...state.interpreterParameters },
semanticBoundary: "linuxcnc_current_tool_offset_runtime_state_for_web_simulation",
};
}
export function detectToolDbStorageCapability(globalScope = globalThis) {
const forcedUnavailable = Boolean(globalScope.__WEB_RTCP_FORCE_OPFS_UNAVAILABLE__);
const isBrowser = typeof globalScope.window === "object" || typeof globalScope.document === "object";
const secureContext = !isBrowser || globalScope.isSecureContext !== false;
const hasOpfs = typeof globalScope.navigator?.storage?.getDirectory === "function";
const opfsAvailable = hasOpfs && secureContext && !forcedUnavailable;
return {
apiName: "web-rtcp-5axis-tool-db-storage-capability",
opfsAvailable,
opfsUnavailable: !opfsAvailable,
secureContext,
forcedUnavailable,
hasNavigatorStorage: Boolean(globalScope.navigator?.storage),
hasGetDirectory: hasOpfs,
fallbackMode: opfsAvailable ? null : "memory-fallback",
reason: opfsAvailable
? "opfs_available"
: forcedUnavailable
? "forced_unavailable"
: !secureContext
? "non_secure_context"
: !hasOpfs
? "missing_opfs_get_directory"
: "opfs_unavailable",
};
}
export function createMemoryToolDbStorage(seed = {}) {
const files = new Map(Object.entries(seed));
return {
apiName: "web-rtcp-5axis-memory-tool-db-storage",
files,
async getDirectory() {
return createDirectoryHandle(files, []);
},
};
}
function createProgramToolCommand(code, line, rawLine, fields = {}) {
return {
code,
line,
rawLine,
...fields,
semanticBoundary: "linuxcnc_tool_command_scanned_for_web_simulation",
};
}
function normalizeToolTable(toolTable = {}) {
const randomToolChanger = Boolean(toolTable.randomToolChanger);
const rawPockets = Array.isArray(toolTable.pockets)
? toolTable.pockets
: pocketsFromEntries(toolTable.entries || [], randomToolChanger);
const pockets = normalizePockets(rawPockets);
const entries = compactToolEntries(pockets);
return {
apiName: "web-rtcp-5axis-tool-table",
sourceRel: toolTable.sourceRel || null,
path: toolTable.path || null,
randomToolChanger,
entries,
pockets,
toolCount: entries.length,
diagnostics: [...(toolTable.diagnostics || [])],
rawText: typeof toolTable.rawText === "string" ? toolTable.rawText : "",
semanticBoundary: toolTable.semanticBoundary || "linuxcnc_tool_table_text_parsed_for_web_simulation",
linuxCncReferences: toolTable.linuxCncReferences || [],
};
}
function refreshToolTable(toolTable) {
const table = normalizeToolTable(toolTable);
return {
...table,
rawText: serializeLinuxCncToolTable(table),
};
}
function pocketsFromEntries(entries = [], randomToolChanger = false) {
const pockets = [createEmptyToolEntry({ idx: 0, pocket: 0, toolNumber: 0 })];
let nextIdx = 1;
for (const entry of entries) {
const idx = Number.isFinite(Number(entry.idx))
? Number(entry.idx)
: randomToolChanger
? Number(entry.pocket ?? entry.pocketno ?? entry.P ?? nextIdx)
: nextIdx;
pockets[idx] = normalizeToolEntry({ ...entry, idx });
if (!randomToolChanger && idx >= nextIdx) nextIdx = idx + 1;
}
return pockets;
}
function normalizePockets(rawPockets = []) {
const pockets = [...rawPockets];
pockets[0] = normalizeToolEntry(pockets[0] || createEmptyToolEntry({ idx: 0, pocket: 0, toolNumber: 0 }));
for (let idx = 1; idx < pockets.length; idx += 1) {
if (pockets[idx]) pockets[idx] = normalizeToolEntry({ ...pockets[idx], idx });
}
return pockets;
}
function compactToolEntries(pockets = []) {
return pockets
.slice(1)
.filter((entry) => entry && entry.toolNumber >= 0)
.map(cloneToolEntry)
.sort((left, right) => left.idx - right.idx);
}
function normalizeToolEntry(entry = {}) {
const toolNumber = Number(entry.toolNumber ?? entry.toolno ?? entry.T ?? 0);
const idx = Number(entry.idx ?? entry.index ?? entry.P ?? entry.pocket ?? 0);
return {
idx,
line: entry.line || null,
rawLine: entry.rawLine || null,
comment: entry.comment || "",
toolNumber,
toolno: toolNumber,
pocket: Number(entry.pocket ?? entry.pocketno ?? entry.P ?? idx),
pocketno: Number(entry.pocket ?? entry.pocketno ?? entry.P ?? idx),
diameter: Number(entry.diameter ?? entry.D ?? 0),
orientation: Number(entry.orientation ?? entry.Q ?? 0),
frontAngle: Number(entry.frontAngle ?? entry.frontangle ?? entry.I ?? 0),
backAngle: Number(entry.backAngle ?? entry.backangle ?? entry.J ?? 0),
offset: {
x: Number(entry.offset?.x ?? entry.x ?? entry.X ?? 0),
y: Number(entry.offset?.y ?? entry.y ?? entry.Y ?? 0),
z: Number(entry.offset?.z ?? entry.z ?? entry.Z ?? entry.length ?? entry.toolLength ?? 0),
a: Number(entry.offset?.a ?? entry.a ?? entry.A ?? 0),
b: Number(entry.offset?.b ?? entry.b ?? entry.B ?? 0),
c: Number(entry.offset?.c ?? entry.c ?? entry.C ?? 0),
u: Number(entry.offset?.u ?? entry.u ?? entry.U ?? 0),
v: Number(entry.offset?.v ?? entry.v ?? entry.V ?? 0),
w: Number(entry.offset?.w ?? entry.w ?? entry.W ?? 0),
},
};
}
function createEmptyToolEntry({ idx = 0, pocket = -1, toolNumber = -1 } = {}) {
return normalizeToolEntry({
idx,
pocket,
toolNumber,
diameter: 0,
frontAngle: 0,
backAngle: 0,
orientation: 0,
offset: createZeroOffset(),
});
}
function cloneToolEntry(entry) {
const normalized = normalizeToolEntry(entry);
return {
...normalized,
offset: { ...normalized.offset },
};
}
function createZeroOffset() {
return {
x: 0,
y: 0,
z: 0,
a: 0,
b: 0,
c: 0,
u: 0,
v: 0,
w: 0,
};
}
function offsetFromWords(words) {
return Object.fromEntries(TOOL_OFFSET_AXES.map((axis) => {
const letter = axis.toUpperCase();
return [axis, Number.isFinite(words[letter]) ? words[letter] : 0];
}));
}
function resolveToolEntry(toolTable, selector = {}) {
const table = normalizeToolTable(toolTable);
if (Number.isFinite(Number(selector.idx))) {
return table.pockets[Number(selector.idx)] || null;
}
if (Number.isFinite(Number(selector.toolNumber ?? selector.toolno))) {
const idx = findToolIndexForTool(table, Number(selector.toolNumber ?? selector.toolno), table.randomToolChanger);
return idx >= 0 ? table.pockets[idx] : null;
}
if (Number.isFinite(Number(selector.pocket ?? selector.pocketno))) {
const pocket = Number(selector.pocket ?? selector.pocketno);
return table.pockets.find((entry, idx) => idx > 0 && entry?.pocket === pocket) ||
table.pockets.find((entry) => entry?.pocket === pocket) ||
null;
}
return null;
}
function findToolIndexForTool(toolTable, toolNumber, randomToolChanger = false) {
const number = Number(toolNumber);
if (!randomToolChanger && number === 0) return 0;
const table = normalizeToolTable(toolTable);
const nonSpindleIndex = table.pockets.findIndex((entry, idx) => idx > 0 && entry?.toolNumber === number);
if (nonSpindleIndex >= 0) return nonSpindleIndex;
const index = table.pockets.findIndex((entry) => entry?.toolNumber === number);
return index >= 0 ? index : -1;
}
function nextAvailableToolIndex(toolTable) {
const table = normalizeToolTable(toolTable);
for (let idx = 1; idx < table.pockets.length; idx += 1) {
if (!table.pockets[idx] || table.pockets[idx].toolNumber < 0) return idx;
}
return Math.max(table.pockets.length, 1);
}
function createInitialIoControlPins(spindleEntry = null) {
const loaded = spindleEntry?.toolNumber > 0 ? spindleEntry : null;
return {
toolPrepare: false,
toolPrepared: false,
toolPrepNumber: 0,
toolPrepPocket: 0,
toolPrepIndex: 0,
toolChange: false,
toolChanged: false,
toolNumber: loaded?.toolNumber || 0,
toolFromPocket: loaded?.pocket || 0,
};
}
function createInitialEmcioStatus(spindleEntry = null) {
const loaded = spindleEntry?.toolNumber > 0 ? spindleEntry : null;
return {
status: "DONE",
tool: {
toolInSpindle: loaded?.toolNumber || 0,
toolFromPocket: loaded?.pocket || 0,
pocketPrepped: -1,
},
};
}
function cloneToolDb(toolDb) {
const toolTable = normalizeToolTable(toolDb?.toolTable);
const spindleEntry = toolTable.pockets[0] || createEmptyToolEntry({ idx: 0, pocket: 0, toolNumber: 0 });
return {
...toolDb,
randomToolChanger: Boolean(toolDb?.randomToolChanger ?? toolTable.randomToolChanger),
toolTable,
toolTableCurrent: toolDb?.toolTableCurrent
? cloneToolEntry(toolDb.toolTableCurrent)
: cloneToolEntry(spindleEntry),
iocontrol: { ...createInitialIoControlPins(spindleEntry), ...(toolDb?.iocontrol || {}) },
emcioStatus: {
...createInitialEmcioStatus(spindleEntry),
...(toolDb?.emcioStatus || {}),
tool: {
...createInitialEmcioStatus(spindleEntry).tool,
...(toolDb?.emcioStatus?.tool || {}),
},
},
activeToolOffset: toolDb?.activeToolOffset
? {
...toolDb.activeToolOffset,
offset: { ...toolDb.activeToolOffset.offset },
}
: null,
interpreterParameters: { ...(toolDb?.interpreterParameters || createToolParameters(null)) },
events: [...(toolDb?.events || [])],
};
}
function createActiveToolOffset(entry, idx) {
return {
idx,
toolNumber: entry.toolNumber,
pocket: entry.pocket,
offset: { ...entry.offset },
diameter: entry.diameter,
frontAngle: entry.frontAngle,
backAngle: entry.backAngle,
orientation: entry.orientation,
};
}
function createToolParameters(activeOffset) {
const parameters = {
[TOOL_PARAMETER_KEYS.toolNumber]: activeOffset?.toolNumber || 0,
};
for (const axis of TOOL_OFFSET_AXES) {
parameters[TOOL_PARAMETER_KEYS[axis]] = activeOffset?.offset?.[axis] || 0;
}
parameters[TOOL_PARAMETER_KEYS.diameter] = activeOffset?.diameter || 0;
parameters[TOOL_PARAMETER_KEYS.frontAngle] = activeOffset?.frontAngle || 0;
parameters[TOOL_PARAMETER_KEYS.backAngle] = activeOffset?.backAngle || 0;
parameters[TOOL_PARAMETER_KEYS.orientation] = activeOffset?.orientation || 0;
return parameters;
}
function createPathTool(tool, length) {
return {
id: Number(tool?.toolNumber) || 0,
pocket: Number(tool?.pocket) || 0,
length: finiteNumber(length, 0),
diameter: finiteNumber(tool?.diameter, 0),
};
}
function finiteNumber(...values) {
for (const value of values) {
const number = Number(value);
if (Number.isFinite(number)) return number;
}
return 0;
}
function createToolDbEvent(code, payload = {}) {
return {
apiName: "web-rtcp-5axis-tool-db-event",
code,
payload,
createdAt: new Date().toISOString(),
semanticBoundary: "linuxcnc_tool_task_semantics_mirrored_for_web_simulation",
promotionScope: "web_simulation_only",
hostProcessExecution: false,
};
}
function formatLinuxCncToolLine(entry, randomToolChanger) {
const normalized = normalizeToolEntry(entry);
const pocketWord = randomToolChanger ? normalized.idx : normalized.pocket;
const words = [
`T${String(formatInteger(normalized.toolNumber)).padEnd(3, " ")}`,
`P${String(formatInteger(pocketWord)).padEnd(3, " ")}`,
];
if (normalized.diameter !== 0) words.push(`D${formatLinuxCncFloat(normalized.diameter)}`);
for (const axis of TOOL_OFFSET_AXES) {
const value = normalized.offset[axis];
if (value !== 0) words.push(`${axis.toUpperCase()}${formatLinuxCncFloat(value)}`);
}
if (normalized.frontAngle !== 0) words.push(`I${formatLinuxCncFloat(normalized.frontAngle)}`);
if (normalized.backAngle !== 0) words.push(`J${formatLinuxCncFloat(normalized.backAngle)}`);
if (normalized.orientation !== 0) words.push(`Q${formatInteger(normalized.orientation)}`);
return `${words.join(" ")}${normalized.comment ? ` ;${normalized.comment}` : ""}`;
}
function formatInteger(value) {
const number = Number(value);
if (!Number.isFinite(number)) return 0;
return Math.trunc(number);
}
function formatLinuxCncFloat(value) {
const number = Number(value);
if (!Number.isFinite(number)) return "+0.000000";
return `${number >= 0 ? "+" : ""}${number.toFixed(6)}`;
}
function toolDbStoragePath(toolDb) {
const profileId = toolDb.profileId || "unknown-profile";
const filename = basename(toolDb.toolTablePath || "tool.tbl");
return `${TOOL_DB_STORAGE_ROOT}/${profileId}/${filename}`;
}
function basename(path = "") {
return String(path).split("/").filter(Boolean).pop() || "";
}
function resolveToolDbStorage(options = {}) {
if (options.storage) {
const mode = options.storageMode || storageModeFor(options.storage);
if (options.requireOpfs === true && mode !== "opfs") {
throw new Error(`OPFS storage required for tool DB, got ${mode}`);
}
return {
storage: options.storage,
mode,
capability: {
apiName: "web-rtcp-5axis-tool-db-storage-capability",
opfsAvailable: mode === "opfs",
opfsUnavailable: mode !== "opfs",
fallbackMode: mode === "opfs" ? null : mode || "custom",
reason: "explicit_storage",
},
};
}
const capability = detectToolDbStorageCapability();
if (capability.opfsAvailable) {
return {
storage: globalThis.navigator.storage,
mode: "opfs",
capability,
};
}
if (options.requireOpfs === true) {
throw new Error(`OPFS storage required for tool DB: ${capability.reason}`);
}
browserMemoryToolDbStorage ??= createMemoryToolDbStorage();
return {
storage: browserMemoryToolDbStorage,
mode: "memory-fallback",
capability,
};
}
function storageModeFor(storage) {
return storage?.apiName === "web-rtcp-5axis-memory-tool-db-storage" ||
storage?.apiName === "web-rtcp-5axis-memory-session-storage"
? "memory"
: "custom";
}
async function saveTextFile(path, text, storage) {
const root = await storage.getDirectory();
const parts = path.split("/").filter(Boolean);
let directory = root;
for (const part of parts.slice(0, -1)) {
directory = await directory.getDirectoryHandle(part, { create: true });
}
const handle = await directory.getFileHandle(parts[parts.length - 1], { create: true });
const writable = await handle.createWritable();
await writable.write(text);
await writable.close();
}
function createDirectoryHandle(files, prefix) {
return {
async getDirectoryHandle(name, options = {}) {
const nextPrefix = [...prefix, name];
if (!options.create) {
const hasChild = [...files.keys()].some((path) => path.startsWith(`${nextPrefix.join("/")}/`));
if (!hasChild) throw new Error(`directory not found: ${nextPrefix.join("/")}`);
}
return createDirectoryHandle(files, nextPrefix);
},
async getFileHandle(name, options = {}) {
const path = [...prefix, name].join("/");
if (!options.create && !files.has(path)) {
throw new Error(`file not found: ${path}`);
}
return {
async createWritable() {
let buffer = "";
return {
async write(chunk) {
buffer += String(chunk);
},
async close() {
files.set(path, buffer);
},
};
},
async getFile() {
return {
async text() {
return files.get(path) || "";
},
};
},
};
},
};
}