const DIALECT = { linuxcnc: 0, fanuc: 1, siemens: 2, }; const EVENT_TYPE = { 1: "error", 2: "comment", 3: "set-units", 4: "set-plane", 5: "set-feed", 6: "set-spindle", 7: "tool-change", 8: "dwell", 9: "rapid", 10: "linear-feed", 11: "arc-feed", 12: "probe", 13: "program-end", 14: "rtcp-pivot", 15: "kinematics-switch", 16: "rtcp-state", 17: "set-g5x-offset", 18: "set-g92-offset", 19: "set-xy-rotation", }; const EVENT_OFFSETS = { type: 4, line: 8, plane: 12, tool: 16, feed: 24, spindle: 32, dwellSeconds: 40, start: 48, end: 120, center: 192, arcTurns: 264, reserved: 268, }; const DEFAULT_OPFS_MOUNT_POINT = "/cnc"; const LINUXCNC_DEFAULT_PARAMETER_FILE = "rs274ngc.var"; const LINUXCNC_PARAMETER_SCRATCH_FILES = [ LINUXCNC_DEFAULT_PARAMETER_FILE, `${LINUXCNC_DEFAULT_PARAMETER_FILE}.new`, `${LINUXCNC_DEFAULT_PARAMETER_FILE}.bak`, ]; function isOpfsAvailable() { return Boolean(globalThis.navigator?.storage?.getDirectory); } function assertRelativePath(path) { if (!path || typeof path !== "string") { throw new Error("OPFS path must be a non-empty relative path"); } if ( path.startsWith("/") || path.includes("\\") || path.includes("\0") || path.split("/").some((part) => part === "" || part === "." || part === "..") ) { throw new Error(`OPFS path must stay inside the CNC workspace: ${path}`); } } function assertWorkspacePath(path) { assertRelativePath(path); } function assertMountPoint(path) { if (!path || typeof path !== "string") { throw new Error("OPFS mount point must be a non-empty absolute path"); } const parts = path.split("/").slice(1); if ( !path.startsWith("/") || path.includes("\\") || path.includes("\0") || parts.some((part) => part === "" || part === "." || part === "..") ) { throw new Error(`OPFS mount point must stay inside the Emscripten filesystem: ${path}`); } } async function getOpfsDirectoryHandle(path, create) { if (!isOpfsAvailable()) { throw new Error("OPFS is not available in this browser context"); } let directory = await globalThis.navigator.storage.getDirectory(); for (const part of path.split("/").filter(Boolean)) { directory = await directory.getDirectoryHandle(part, { create }); } return directory; } async function readOpfsFile(workspacePath, relativePath) { assertRelativePath(relativePath); const parts = relativePath.split("/"); const fileName = parts.pop(); const directory = await getOpfsDirectoryHandle([workspacePath, ...parts].filter(Boolean).join("/"), false); const file = await directory.getFileHandle(fileName, { create: false }); return new Uint8Array(await (await file.getFile()).arrayBuffer()); } async function writeOpfsFile(workspacePath, relativePath, data) { assertRelativePath(relativePath); const parts = relativePath.split("/"); const fileName = parts.pop(); const directory = await getOpfsDirectoryHandle([workspacePath, ...parts].filter(Boolean).join("/"), true); const file = await directory.getFileHandle(fileName, { create: true }); const writable = await file.createWritable(); try { await writable.truncate(0); await writable.write(data); await writable.close(); } catch (error) { try { await writable.abort(); } catch (_abortError) { // Preserve the original OPFS write failure. } throw error; } } async function removeOpfsEntry(workspacePath, relativePath, recursive) { assertRelativePath(relativePath); const parts = relativePath.split("/"); const entryName = parts.pop(); try { const directory = await getOpfsDirectoryHandle([workspacePath, ...parts].filter(Boolean).join("/"), false); await directory.removeEntry(entryName, { recursive }); } catch (error) { if (error?.name !== "NotFoundError") { throw error; } } } async function removeOpfsFileEntry(workspacePath, relativePath) { assertRelativePath(relativePath); const parts = relativePath.split("/"); const entryName = parts.pop(); try { const directory = await getOpfsDirectoryHandle([workspacePath, ...parts].filter(Boolean).join("/"), false); await directory.getFileHandle(entryName, { create: false }); await directory.removeEntry(entryName, { recursive: false }); } catch (error) { if (error?.name !== "NotFoundError" && error?.name !== "TypeMismatchError") { throw error; } } } async function opfsEntryExists(workspacePath, relativePath) { assertRelativePath(relativePath); const parts = relativePath.split("/"); const entryName = parts.pop(); try { const directory = await getOpfsDirectoryHandle([workspacePath, ...parts].filter(Boolean).join("/"), false); try { await directory.getFileHandle(entryName, { create: false }); return true; } catch (fileError) { if (fileError?.name !== "NotFoundError" && fileError?.name !== "TypeMismatchError") { throw fileError; } } try { await directory.getDirectoryHandle(entryName, { create: false }); return true; } catch (directoryError) { if (directoryError?.name !== "NotFoundError" && directoryError?.name !== "TypeMismatchError") { throw directoryError; } } return false; } catch (error) { if (error?.name === "NotFoundError") { return false; } throw error; } } async function statOpfsEntry(workspacePath, relativePath) { assertRelativePath(relativePath); const parts = relativePath.split("/"); const entryName = parts.pop(); const directory = await getOpfsDirectoryHandle([workspacePath, ...parts].filter(Boolean).join("/"), false); try { const file = await directory.getFileHandle(entryName, { create: false }); return { kind: "file", size: (await file.getFile()).size }; } catch (fileError) { if (fileError?.name !== "TypeMismatchError") { throw fileError; } } await directory.getDirectoryHandle(entryName, { create: false }); return { kind: "directory", size: null }; } async function clearOpfsWorkspace(workspacePath) { const parts = workspacePath.split("/").filter(Boolean); if (parts.length === 0) { return; } const entryName = parts.pop(); const directory = await getOpfsDirectoryHandle(parts.join("/"), true); try { await directory.removeEntry(entryName, { recursive: true }); } catch (error) { if (error?.name !== "NotFoundError") { throw error; } } await getOpfsDirectoryHandle(workspacePath, true); } async function replaceOpfsDirectory(workspacePath, relativePath) { assertRelativePath(relativePath); await removeOpfsEntry(workspacePath, relativePath, true); const directory = await getOpfsDirectoryHandle([workspacePath, relativePath].filter(Boolean).join("/"), true); for await (const [name] of directory.entries()) { await directory.removeEntry(name, { recursive: true }); } } async function readOpfsDirectoryTree(workspacePath, relativePath, relativeRoot = "") { assertRelativePath(relativePath); const directory = await getOpfsDirectoryHandle([workspacePath, relativePath, relativeRoot].filter(Boolean).join("/"), false); const files = []; const directories = []; for await (const [name, handle] of directory.entries()) { const path = relativeRoot ? `${relativeRoot}/${name}` : name; if (handle.kind === "directory") { directories.push(path); const child = await readOpfsDirectoryTree(workspacePath, relativePath, path); directories.push(...child.directories); files.push(...child.files); } else if (handle.kind === "file") { files.push(path); } } return { directories: directories.sort(), files: files.sort(), }; } function ensureWasmDirectory(module, path) { const parts = path.split("/").filter(Boolean); let current = ""; for (const part of parts) { current += `/${part}`; try { module.FS.mkdir(current); } catch (error) { if (error?.errno !== 20) { throw error; } } } } function readWasmDirectoryTree(module, rootPath, relativeRoot = "") { const files = []; const directories = []; for (const entry of module.FS.readdir(rootPath)) { if (entry === "." || entry === "..") { continue; } const path = `${rootPath}/${entry}`; const relativePath = relativeRoot ? `${relativeRoot}/${entry}` : entry; const mode = module.FS.stat(path).mode; if (module.FS.isDir(mode)) { directories.push(relativePath); const child = readWasmDirectoryTree(module, path, relativePath); directories.push(...child.directories); files.push(...child.files); } else if (module.FS.isFile(mode)) { files.push(relativePath); } } return { directories: directories.sort(), files: files.sort(), }; } function removeWasmPath(module, path, recursive) { let mode; try { mode = module.FS.stat(path).mode; } catch (error) { if (error?.errno !== 44) { throw error; } return; } if (module.FS.isDir(mode)) { if (!recursive) { module.FS.rmdir(path); return; } for (const entry of module.FS.readdir(path)) { if (entry !== "." && entry !== "..") { removeWasmPath(module, `${path}/${entry}`, true); } } module.FS.rmdir(path); } else { module.FS.unlink(path); } } function removeWasmFilePath(module, path) { let mode; try { mode = module.FS.stat(path).mode; } catch (error) { if (error?.errno !== 44) { throw error; } return; } if (module.FS.isFile(mode)) { module.FS.unlink(path); } } function wasmPathExists(module, path) { try { module.FS.stat(path); return true; } catch (error) { if (error?.errno !== 44) { throw error; } return false; } } function readFirstExistingWasmFile(module, paths) { for (const path of paths) { try { return module.FS.readFile(path); } catch (error) { if (error?.errno !== 44) { throw error; } } } return null; } function writeWasmFileReplacingPath(module, path, data) { removeWasmPath(module, path, true); const parentIndex = path.lastIndexOf("/"); if (parentIndex > 0) { ensureWasmDirectory(module, path.slice(0, parentIndex)); } module.FS.writeFile(path, data); } function cleanupLinuxCncParameterFiles(module) { for (const path of LINUXCNC_PARAMETER_SCRATCH_FILES) { removeWasmPath(module, path, true); removeWasmPath(module, `/${path}`, true); } } function installOpfsWorkspace(module, options) { const opfsOptions = options.opfs ?? (isOpfsAvailable() ? {} : false); if (opfsOptions === false) { if (isOpfsAvailable()) { throw new Error("OPFS cannot be disabled in browser contexts with OPFS support"); } return null; } if (!module.FS) { throw new Error("cnc_sim.js was not built with Emscripten FS export"); } if (!isOpfsAvailable()) { if (opfsOptions.required) { throw new Error("OPFS is only available in browser contexts"); } return null; } const mountPoint = opfsOptions.mountPoint ?? DEFAULT_OPFS_MOUNT_POINT; const workspacePath = opfsOptions.workspacePath ?? "cnc-simulator"; assertMountPoint(mountPoint); assertWorkspacePath(workspacePath); ensureWasmDirectory(module, mountPoint); return { mountPoint, workspacePath, resolvePath(path) { assertRelativePath(path); return `${mountPoint}/${path}`; }, async readFile(path) { const data = await readOpfsFile(workspacePath, path); const wasmPath = this.resolvePath(path); writeWasmFileReplacingPath(module, wasmPath, data); return data; }, async readDirectory(path) { assertRelativePath(path); let tree; try { tree = await readOpfsDirectoryTree(workspacePath, path); } catch (error) { if (error?.name === "NotFoundError") { removeWasmPath(module, this.resolvePath(path), true); } throw error; } const wasmPath = this.resolvePath(path); removeWasmPath(module, wasmPath, true); ensureWasmDirectory(module, wasmPath); for (const directory of tree.directories) { ensureWasmDirectory(module, `${wasmPath}/${directory}`); } for (const file of tree.files) { await this.readFile(`${path}/${file}`); } return tree.files; }, async persistFile(path) { const wasmPath = this.resolvePath(path); const data = module.FS.readFile(wasmPath); await writeOpfsFile(workspacePath, path, data); return data; }, async persistDirectory(path) { assertRelativePath(path); const wasmPath = this.resolvePath(path); const tree = readWasmDirectoryTree(module, wasmPath); await replaceOpfsDirectory(workspacePath, path); for (const directory of tree.directories) { await getOpfsDirectoryHandle([workspacePath, path, directory].filter(Boolean).join("/"), true); } for (const file of tree.files) { await this.persistFile(`${path}/${file}`); } return tree.files; }, async writeFile(path, data) { await writeOpfsFile(workspacePath, path, data); const wasmPath = this.resolvePath(path); writeWasmFileReplacingPath(module, wasmPath, data); }, // LinuxCNC source basis: rs274ngc_pre.cc save_parameters() links the // current parameter file to filename + ".bak" before replacing it. async copyFile(fromPath, toPath) { assertRelativePath(fromPath); assertRelativePath(toPath); const data = await readOpfsFile(workspacePath, fromPath); await writeOpfsFile(workspacePath, toPath, data); const fromWasmPath = this.resolvePath(fromPath); const toWasmPath = this.resolvePath(toPath); writeWasmFileReplacingPath(module, fromWasmPath, data); writeWasmFileReplacingPath(module, toWasmPath, data); return data; }, // LinuxCNC source basis: rs274ngc_pre.cc save_parameters() writes a // temporary filename + ".new" and renames it over the parameter file. async moveFile(fromPath, toPath) { assertRelativePath(fromPath); assertRelativePath(toPath); if (fromPath === toPath) { return; } const data = await readOpfsFile(workspacePath, fromPath); await writeOpfsFile(workspacePath, toPath, data); await removeOpfsEntry(workspacePath, fromPath, false); const fromWasmPath = this.resolvePath(fromPath); const toWasmPath = this.resolvePath(toPath); writeWasmFileReplacingPath(module, toWasmPath, data); removeWasmPath(module, fromWasmPath, false); }, async exists(path) { return opfsEntryExists(workspacePath, path); }, async stat(path) { return statOpfsEntry(workspacePath, path); }, // LinuxCNC source basis: rs274ngc_pre.cc restore_parameters() reads the // configured parameter file, and save_parameters() writes filename + ".new" // before replacing the main file and managing filename + ".bak". async loadParameterFile(path) { assertRelativePath(path); try { const data = await readOpfsFile(workspacePath, path); writeWasmFileReplacingPath(module, LINUXCNC_DEFAULT_PARAMETER_FILE, data); writeWasmFileReplacingPath(module, `/${LINUXCNC_DEFAULT_PARAMETER_FILE}`, data); return data; } catch (error) { if (error?.name !== "NotFoundError") { throw error; } cleanupLinuxCncParameterFiles(module); return null; } }, async persistParameterFile(path) { assertRelativePath(path); const data = readFirstExistingWasmFile(module, [ LINUXCNC_DEFAULT_PARAMETER_FILE, `/${LINUXCNC_DEFAULT_PARAMETER_FILE}`, ]); if (data === null) { throw new Error("LinuxCNC parameter file was not produced by the interpreter"); } await writeOpfsFile(workspacePath, path, data); // LinuxCNC source basis: rs274ngc_pre.cc save_parameters() // creates filename + ".new" with fopen() and later renames that // file; OPFS cleanup must not remove a directory at that path. await removeOpfsFileEntry(workspacePath, `${path}.new`); removeWasmFilePath(module, `${LINUXCNC_DEFAULT_PARAMETER_FILE}.new`); removeWasmFilePath(module, `/${LINUXCNC_DEFAULT_PARAMETER_FILE}.new`); const backupPath = `${LINUXCNC_DEFAULT_PARAMETER_FILE}.bak`; const backupData = readFirstExistingWasmFile(module, [backupPath, `/${backupPath}`]); if (backupData !== null) { await writeOpfsFile(workspacePath, `${path}.bak`, backupData); } else { await removeOpfsFileEntry(workspacePath, `${path}.bak`); } return data; }, async removeFile(path) { // LinuxCNC source basis: rs274ngc_pre.cc save_parameters() uses // unlink() for backup files, so the browser bridge keeps file-only // removal separate from recursive directory cleanup. await removeOpfsFileEntry(workspacePath, path); removeWasmFilePath(module, this.resolvePath(path)); }, async removeDirectory(path) { await removeOpfsEntry(workspacePath, path, true); removeWasmPath(module, this.resolvePath(path), true); }, async clear() { await clearOpfsWorkspace(workspacePath); for (const entry of module.FS.readdir(mountPoint)) { if (entry !== "." && entry !== "..") { removeWasmPath(module, `${mountPoint}/${entry}`, true); } } }, }; } function readPose(view, offset) { return { x: view.getFloat64(offset + 0, true), y: view.getFloat64(offset + 8, true), z: view.getFloat64(offset + 16, true), a: view.getFloat64(offset + 24, true), b: view.getFloat64(offset + 32, true), c: view.getFloat64(offset + 40, true), u: view.getFloat64(offset + 48, true), v: view.getFloat64(offset + 56, true), w: view.getFloat64(offset + 64, true), }; } function readEvent(module, ptr) { const view = new DataView(module.HEAPU8.buffer, ptr, 272); return { type: EVENT_TYPE[view.getInt32(EVENT_OFFSETS.type, true)] ?? "unknown", line: view.getInt32(EVENT_OFFSETS.line, true), plane: view.getInt32(EVENT_OFFSETS.plane, true), tool: view.getInt32(EVENT_OFFSETS.tool, true), feed: view.getFloat64(EVENT_OFFSETS.feed, true), spindle: view.getFloat64(EVENT_OFFSETS.spindle, true), dwellSeconds: view.getFloat64(EVENT_OFFSETS.dwellSeconds, true), start: readPose(view, EVENT_OFFSETS.start), end: readPose(view, EVENT_OFFSETS.end), center: readPose(view, EVENT_OFFSETS.center), arcTurns: view.getInt32(EVENT_OFFSETS.arcTurns, true), reserved: view.getInt32(EVENT_OFFSETS.reserved, true), }; } async function loadModuleFactory() { if (globalThis.createCncSimModule) { return globalThis.createCncSimModule; } const module = await import("/cnc_sim.js"); return module.default ?? module.createCncSimModule ?? globalThis.createCncSimModule; } export async function createWasmSimulator(moduleOptions = {}) { if (moduleOptions.opfs === false && isOpfsAvailable()) { throw new Error("OPFS cannot be disabled in browser contexts with OPFS support"); } const hasOpfsOptions = Object.prototype.hasOwnProperty.call(moduleOptions, "opfs"); const emscriptenModuleOptions = hasOpfsOptions ? (({ opfs: _opfs, ...rest }) => rest)(moduleOptions) : moduleOptions; const createModule = await loadModuleFactory(); if (!createModule) { throw new Error("cnc_sim.js did not export createCncSimModule"); } const module = hasOpfsOptions ? await createModule(emscriptenModuleOptions) : await createModule(moduleOptions); const opfs = installOpfsWorkspace(module, moduleOptions); const create = module.cwrap("cnc_sim_create", "number", []); const destroy = module.cwrap("cnc_sim_destroy", null, ["number"]); const reset = module.cwrap("cnc_sim_reset", null, ["number"]); const setDialect = module.cwrap("cnc_sim_set_dialect", "number", ["number", "number"]); const setCallback = module.cwrap("cnc_sim_set_event_callback", "number", ["number", "number", "number"]); const loadConfig = module.cwrap("cnc_sim_load_config_json", "number", ["number", "number", "number"]); const parseProgram = module.cwrap("cnc_sim_parse_program", "number", ["number", "number", "number"]); const lastError = module.cwrap("cnc_sim_last_error", "number", ["number"]); const handle = create(); let callbackPtr = 0; const parseText = (program, dialect = "linuxcnc", options = {}, parseOptions = {}) => { const events = []; try { reset(handle); setDialect(handle, DIALECT[dialect] ?? DIALECT.linuxcnc); const config = JSON.stringify({ backend: options.backend ?? "linuxcnc-rs274", ...(options.rtcp ? { rtcp: options.rtcp } : {}), ...(options.blockDelete !== undefined ? { blockDelete: options.blockDelete } : {}), ...(options.block_delete !== undefined ? { block_delete: options.block_delete } : {}), ...(options.switchkins ? { switchkins: options.switchkins } : {}), ...(options.remap ? { remap: options.remap } : {}), ...(options.machine ? { machine: options.machine } : {}), ...(options.config ? { config: options.config } : {}), ...(options.configPath ? { configPath: options.configPath } : {}), ...(options.configpath ? { configpath: options.configpath } : {}), ...(options.config_path ? { config_path: options.config_path } : {}), ...(options.ini ? { ini: options.ini } : {}), ...(options.iniFile ? { iniFile: options.iniFile } : {}), ...(options.inifile ? { inifile: options.inifile } : {}), ...(options.iniFileName ? { iniFileName: options.iniFileName } : {}), ...(options.inifilename ? { inifilename: options.inifilename } : {}), ...(options.ini_file ? { ini_file: options.ini_file } : {}), ...(options.ini_file_name ? { ini_file_name: options.ini_file_name } : {}), ...(options.INI_FILE_NAME ? { INI_FILE_NAME: options.INI_FILE_NAME } : {}), ...(options.halFile ? { halFile: options.halFile } : {}), ...(options.halfile ? { halfile: options.halfile } : {}), ...(options.hal_file ? { hal_file: options.hal_file } : {}), ...(options.HALFILE ? { HALFILE: options.HALFILE } : {}), ...(options.postguiHalFile ? { postguiHalFile: options.postguiHalFile } : {}), ...(options.postguihalfile ? { postguihalfile: options.postguihalfile } : {}), ...(options.postgui_halfile ? { postgui_halfile: options.postgui_halfile } : {}), ...(options.postgui_hal_file ? { postgui_hal_file: options.postgui_hal_file } : {}), ...(options.POSTGUI_HALFILE ? { POSTGUI_HALFILE: options.POSTGUI_HALFILE } : {}), ...(options.subroutinePath ? { subroutinePath: options.subroutinePath } : {}), ...(options.subroutinepath ? { subroutinepath: options.subroutinepath } : {}), ...(options.subroutine_path ? { subroutine_path: options.subroutine_path } : {}), ...(options.SUBROUTINE_PATH ? { SUBROUTINE_PATH: options.SUBROUTINE_PATH } : {}), ...(options.kinematics ? { kinematics: options.kinematics } : {}), ...(options.trt ? { trt: options.trt } : {}), ...(options.pivotLength !== undefined ? { pivotLength: options.pivotLength } : {}), ...(options.pivot_length !== undefined ? { pivot_length: options.pivot_length } : {}), ...(options.kinematicsType !== undefined ? { kinematicsType: options.kinematicsType } : {}), ...(options.kinematics_type !== undefined ? { kinematics_type: options.kinematics_type } : {}), ...(options.fiveaxisKinematicsType !== undefined ? { fiveaxisKinematicsType: options.fiveaxisKinematicsType } : {}), ...(options.fiveaxis_kinematics_type !== undefined ? { fiveaxis_kinematics_type: options.fiveaxis_kinematics_type } : {}), ...(options.xyzbcTrt ? { xyzbcTrt: options.xyzbcTrt } : {}), }); const configBytes = module.lengthBytesUTF8(config) + 1; const configPtr = module._malloc(configBytes); let configRc; try { module.stringToUTF8(config, configPtr, configBytes); configRc = loadConfig(handle, configPtr, configBytes - 1); } finally { module._free(configPtr); } if (configRc !== 0) { throw new Error(module.UTF8ToString(lastError(handle))); } if (callbackPtr) { module.removeFunction(callbackPtr); } callbackPtr = module.addFunction((eventPtr) => { events.push(readEvent(module, eventPtr)); return 0; }, "iii"); setCallback(handle, callbackPtr, 0); const bytes = module.lengthBytesUTF8(program) + 1; const ptr = module._malloc(bytes); let rc; try { module.stringToUTF8(program, ptr, bytes); rc = parseProgram(handle, ptr, bytes - 1); } finally { module._free(ptr); } if (rc !== 0) { throw new Error(module.UTF8ToString(lastError(handle))); } return events; } finally { if (opfs && !parseOptions.keepLinuxCncParameterFiles) { cleanupLinuxCncParameterFiles(module); } } }; return { parse(program, dialect = "linuxcnc", options = {}) { return parseText(program, dialect, options); }, async parseFile(path, dialect = "linuxcnc", options = {}) { if (!opfs) { throw new Error("OPFS workspace is not available for program file parsing"); } const program = new TextDecoder().decode(await opfs.readFile(path)); return parseText(program, dialect, options); }, async parseWithParameterFile(program, parameterPath, dialect = "linuxcnc", options = {}) { if (!opfs) { throw new Error("OPFS workspace is not available for parameter file persistence"); } cleanupLinuxCncParameterFiles(module); const previousParameterFile = await opfs.loadParameterFile(parameterPath); try { const events = parseText(program, dialect, options, { keepLinuxCncParameterFiles: true }); await opfs.persistParameterFile(parameterPath); // LinuxCNC source basis: rs274ngc_pre.cc save_parameters() // links the previous parameter file to filename + ".bak" before // replacing filename. Browser filesystems may not expose that link // through the WASM mirror, so preserve the loaded OPFS file here. if (previousParameterFile === null) { try { const backupStat = await opfs.stat(`${parameterPath}.bak`); if (backupStat.kind === "file") { await opfs.removeFile(`${parameterPath}.bak`); } } catch (error) { if (error?.name !== "NotFoundError") { throw error; } } } else { try { await opfs.writeFile(`${parameterPath}.bak`, previousParameterFile); } catch (_backupError) { // LinuxCNC treats link(filename, filename + ".bak") failure as non-fatal. } } return events; } finally { cleanupLinuxCncParameterFiles(module); } }, async parseFileWithParameterFile(path, parameterPath, dialect = "linuxcnc", options = {}) { if (!opfs) { throw new Error("OPFS workspace is not available for program and parameter file parsing"); } const program = new TextDecoder().decode(await opfs.readFile(path)); return this.parseWithParameterFile(program, parameterPath, dialect, options); }, dispose() { if (callbackPtr) { module.removeFunction(callbackPtr); callbackPtr = 0; } destroy(handle); }, fs: { opfs, module, }, }; }