import { createSimulationStore } from "./state/store.js"; import { mountAxisShell } from "./ui/axis-shell.js"; import { createLinuxCncInterpreterRuntime } from "./runtime/linuxcnc-interpreter-runtime.js"; import { createLinuxCncInterpreterWorkerRuntime } from "./runtime/linuxcnc-interpreter-worker-client.js"; import { createLinuxCncKinematicsRuntime } from "./runtime/linuxcnc-kinematics-runtime.js"; import { createLinuxCncKinematicsWorkerRuntime } from "./runtime/linuxcnc-kinematics-worker-client.js"; import { loadLinuxCncIniConfig } from "./runtime/linuxcnc-ini-runtime.js"; import { createLinuxCncTaskHalRuntime } from "./runtime/linuxcnc-task-hal-runtime.js"; import { createLinuxCncTaskHalWorkerRuntime } from "./runtime/linuxcnc-task-hal-worker-client.js"; const app = document.querySelector("#app"); if (!app) { throw new Error("Missing #app mount point"); } const store = createSimulationStore(); const shell = mountAxisShell(app, store); const iniConfigReady = attachProfileIniConfig(store, store.getState().profile); const kinematicsRuntimeReady = attachDefaultKinematicsRuntime(store, store.getState().profile.kinematicsModuleId || store.getState().machineProfile); const interpreterRuntimeReady = attachDefaultInterpreterRuntime(store); const taskHalRuntimeReady = attachDefaultTaskHalRuntime(store); const machineFileSeedReady = ensureMachineFilesForProfile(store); let attachedKinematicsProfile = store.getState().machineProfile; let attachedIniProfile = store.getState().machineProfile; let attachedMachineFileProfile = store.getState().machineProfile; let machineFileSeedPromise = machineFileSeedReady; const autoLoadedProgramProfiles = new Set(); Promise.allSettled([interpreterRuntimeReady, machineFileSeedReady]).then(() => { ensureDefaultLinuxCncProgramPreview(store).catch(() => {}); }); store.subscribe((state) => { if (state.machineProfile !== attachedIniProfile) { attachedIniProfile = state.machineProfile; attachProfileIniConfig(store, state.profile).catch(() => {}); } if (state.machineProfile !== attachedKinematicsProfile) { attachedKinematicsProfile = state.machineProfile; const profileId = state.machineProfile; attachDefaultKinematicsRuntime(store, state.profile.kinematicsModuleId || state.machineProfile) .then(() => store.refreshKinematicsFrame({ operatorMessage: `LinuxCNC kinematics ${profileId} profile frame refreshed`, })) .catch(() => {}); } if (state.machineProfile !== attachedMachineFileProfile) { attachedMachineFileProfile = state.machineProfile; machineFileSeedPromise = ensureMachineFilesForProfile(store); window.webRtcp5AxisSimulation.machineFileSeedReady = machineFileSeedPromise; machineFileSeedPromise.finally(() => { ensureDefaultLinuxCncProgramPreview(store).catch(() => {}); }); } }); window.webRtcp5AxisSimulation = { getState: store.getState, dispatch: store.dispatch, refreshKinematicsFrame: store.refreshKinematicsFrame, saveSession: store.saveSession, restoreSession: store.restoreSession, stageMachineFiles: store.stageMachineFiles, queryToolDb: store.queryToolDb, editToolDb: store.editToolDb, saveToolDb: store.saveToolDb, runFullBoundaryAudit: store.runFullBoundaryAudit, getRegions: shell.getRegions, getButtonParity: shell.getButtonParity, iniConfigReady, kinematicsRuntimeReady, interpreterRuntimeReady, taskHalRuntimeReady, machineFileSeedReady, }; store.dispatch({ type: "BOOT_READY" }); async function attachProfileIniConfig(store, profile) { try { const iniConfig = await loadLinuxCncIniConfig(profile); store.dispatch({ type: "ATTACH_INI_CONFIG", profileId: profile.id, iniConfig }); return store.getState().iniConfigReadiness; } catch (error) { store.dispatch({ type: "INI_CONFIG_FAILED", path: profile.iniPath, error: error.message }); return store.getState().iniConfigReadiness; } } async function ensureMachineFilesForProfile(store) { try { const state = store.getState(); if (state.machineFileStaging?.status === "staged" && state.machineFileStaging?.profileId === state.machineProfile) { return state.machineFileStaging; } return await store.stageMachineFiles({ requireOpfs: true }); } catch (error) { return { apiName: "web-rtcp-5axis-machine-file-staging-seed", status: "error", error: error instanceof Error ? error.message : String(error), }; } } async function ensureDefaultLinuxCncProgramPreview(store) { const state = store.getState(); if (!state.interpreterRuntime?.loaded) return null; if (state.machineFileStaging?.status !== "staged" || !state.machineFileStaging?.gcodeSources?.length) return null; const profileId = state.machineProfile; if (autoLoadedProgramProfiles.has(profileId)) return state.programExecution; const defaultSource = selectDefaultLinuxCncSource(state); if (!defaultSource) return null; autoLoadedProgramProfiles.add(profileId); store.dispatch({ type: "LOAD_LINUXCNC_GCODE_SOURCE", sourceRel: defaultSource.sourceRel }); await waitForStore( store, (nextState) => !nextState.interpreterExecutionPending && nextState.programExecution?.sourceMode === "linuxcnc-interpreter-wasm", 15000, ).catch((error) => { autoLoadedProgramProfiles.delete(profileId); throw error; }); return store.getState().programExecution; } function selectDefaultLinuxCncSource(state) { const sources = state.machineFileStaging?.gcodeSources || []; const profileDefault = state.profile?.machineFileStaging?.defaultProgramFilename; if (profileDefault) { const match = sources.find((source) => source.filename === profileDefault); if (match) return match; } const preferredFilename = `${state.machineProfile}_switchkins.ngc`; return sources.find((source) => source.filename === preferredFilename) || sources.find((source) => source.filename.includes(state.machineProfile)) || sources[0] || null; } function runtimeSdkUrls(filename) { const inDist = import.meta.url.includes("/app/dist/"); return [ inDist ? new URL(`../wasm-port/runtime/sdk/src/${filename}`, import.meta.url).href : new URL(`../../../wasm-port/runtime/sdk/src/${filename}`, import.meta.url).href, ]; } function waitForStore(store, predicate, timeoutMs = 10000) { const initialState = store.getState(); if (predicate(initialState)) { return Promise.resolve(initialState); } return new Promise((resolve, reject) => { const timeoutId = window.setTimeout(() => { unsubscribe(); reject(new Error(`Timed out after ${timeoutMs}ms waiting for store state`)); }, timeoutMs); const unsubscribe = store.subscribe((state) => { if (!predicate(state)) return; window.clearTimeout(timeoutId); unsubscribe(); resolve(state); }); }); } async function attachDefaultKinematicsRuntime(store, moduleId = "xyzbc-trt") { const state = store.getState(); if (state.profile?.tcpCapable === false) { store.dispatch({ type: "SET_FRAME_SOURCE", sourceMode: "fixture-ui-only", operatorMessage: `LinuxCNC kinematics disabled for ${state.machineProfile} reference profile`, }); return { apiName: "web-rtcp-5axis-linuxcnc-kinematics-runtime-readiness", moduleId, loaded: false, sourceMode: "fixture-ui-only", semanticBoundary: "reference-profile-no-linuxcnc-kinematics-runtime", }; } const sdkModuleUrls = runtimeSdkUrls("linuxcnc-kinematics.js"); const errors = []; for (const sdkModuleUrl of sdkModuleUrls) { if (typeof Worker === "function") { try { const runtime = await createLinuxCncKinematicsWorkerRuntime({ moduleId, sdkModuleUrl }); store.dispatch({ type: "ATTACH_KINEMATICS_RUNTIME", runtime }); await store.refreshKinematicsFrame({ operatorMessage: `LinuxCNC kinematics ${runtime.moduleId} worker ready` }); return runtime.readiness(); } catch (error) { errors.push(`${sdkModuleUrl} worker: ${error.message}`); } } try { const runtime = await createLinuxCncKinematicsRuntime({ moduleId, sdkModuleUrl }); store.dispatch({ type: "ATTACH_KINEMATICS_RUNTIME", runtime }); return runtime.readiness(); } catch (error) { errors.push(`${sdkModuleUrl}: ${error.message}`); } } try { const runtime = await createLinuxCncKinematicsRuntime({ moduleId }); store.dispatch({ type: "ATTACH_KINEMATICS_RUNTIME", runtime }); return runtime.readiness(); } catch (error) { errors.push(`default runtime: ${error.message}`); store.dispatch({ type: "SET_FRAME_SOURCE", sourceMode: "fixture-ui-only", operatorMessage: `LinuxCNC kinematics runtime unavailable: ${errors.join(" | ")}`, }); return { apiName: "web-rtcp-5axis-linuxcnc-kinematics-runtime-readiness", moduleId, loaded: false, sourceMode: "fixture-ui-only", error: errors.join(" | "), }; } } async function attachDefaultInterpreterRuntime(store) { const sdkModuleUrls = runtimeSdkUrls("linuxcnc-interp.js"); const errors = []; for (const sdkModuleUrl of sdkModuleUrls) { if (typeof Worker === "function") { try { const runtime = await createLinuxCncInterpreterWorkerRuntime({ sdkModuleUrl }); store.dispatch({ type: "ATTACH_INTERPRETER_RUNTIME", runtime }); return runtime.readiness(); } catch (error) { errors.push(`${sdkModuleUrl} worker: ${error.message}`); } } try { const runtime = await createLinuxCncInterpreterRuntime({ sdkModuleUrl }); store.dispatch({ type: "ATTACH_INTERPRETER_RUNTIME", runtime }); return runtime.readiness(); } catch (error) { errors.push(`${sdkModuleUrl}: ${error.message}`); } } try { const runtime = await createLinuxCncInterpreterRuntime(); store.dispatch({ type: "ATTACH_INTERPRETER_RUNTIME", runtime }); return runtime.readiness(); } catch (error) { errors.push(`default runtime: ${error.message}`); return { apiName: "web-rtcp-5axis-linuxcnc-interpreter-runtime-readiness", loaded: false, sourceMode: "fixture-line-playback", error: errors.join(" | "), }; } } async function attachDefaultTaskHalRuntime(store) { const sdkModuleUrls = runtimeSdkUrls("linuxcnc-task-hal.js"); const errors = []; for (const sdkModuleUrl of sdkModuleUrls) { if (typeof Worker === "function") { try { const runtime = await createLinuxCncTaskHalWorkerRuntime({ sdkModuleUrl }); const readiness = await runtime.readiness(); store.dispatch({ type: "ATTACH_TASK_HAL_RUNTIME", runtime, readiness }); return readiness; } catch (error) { errors.push(`${sdkModuleUrl} worker: ${error.message}`); } } try { const runtime = await createLinuxCncTaskHalRuntime({ sdkModuleUrl }); const readiness = runtime.readiness(); store.dispatch({ type: "ATTACH_TASK_HAL_RUNTIME", runtime, readiness }); return readiness; } catch (error) { errors.push(`${sdkModuleUrl}: ${error.message}`); } } store.dispatch({ type: "TASK_HAL_RUNTIME_FAILED", error: errors.join(" | ") }); return { apiName: "web-rtcp-5axis-linuxcnc-task-hal-runtime-readiness", loaded: false, taskRuntimeReady: false, motionRuntimeReady: false, halRuntimeReady: false, error: errors.join(" | "), }; }