1094 lines
38 KiB
JavaScript
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) || "";
|
|
},
|
|
};
|
|
},
|
|
};
|
|
},
|
|
};
|
|
}
|