Add OPFS G-code staging and browser simulation checks
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
"build": "node scripts/build-static.mjs",
|
||||
"dev": "python3 -m http.server 4173",
|
||||
"smoke": "bash ../tests/browser/verify_gmoccapy_shell_browser.sh && bash ../tests/browser/verify_gmoccapy_dist_browser.sh",
|
||||
"smoke:node": "node ../tests/node/verify_linuxcnc_kinematics_runtime.mjs && node ../tests/node/verify_linuxcnc_interpreter_runtime.mjs && node ../tests/node/verify_linuxcnc_ini_runtime.mjs && node ../tests/node/verify_linuxcnc_task_hal_runtime.mjs && node ../tests/node/verify_full_linuxcnc_5axis_source.mjs && node ../tests/node/verify_full_execution_boundary.mjs && node ../tests/node/verify_machine_file_staging.mjs && node ../tests/node/verify_five_axis_session.mjs && node ../tests/node/verify_rtcp_store.mjs && node ../tests/node/verify_profile_boundary.mjs"
|
||||
"smoke:node": "node ../tests/node/verify_linuxcnc_kinematics_runtime.mjs && node ../tests/node/verify_linuxcnc_interpreter_runtime.mjs && node ../tests/node/verify_linuxcnc_ini_runtime.mjs && node ../tests/node/verify_linuxcnc_task_hal_runtime.mjs && node ../tests/node/verify_native_task_hal_audit.mjs && node ../tests/node/verify_full_linuxcnc_5axis_source.mjs && node ../tests/node/verify_real_linuxcnc_5axis_program_cases.mjs && node ../tests/node/verify_full_execution_boundary.mjs && node ../tests/node/verify_machine_file_staging.mjs && node ../tests/node/verify_five_axis_session.mjs && node ../tests/node/verify_rtcp_store.mjs && node ../tests/node/verify_profile_boundary.mjs"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {}
|
||||
|
||||
@@ -61,8 +61,11 @@ console.log("gmoccapy_static_build=ok");
|
||||
async function copyLinuxCncConfigAssets() {
|
||||
const configSrcDir = join(repoRoot, "wasm-port/vendor/linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting");
|
||||
const configDistDir = join(distDir, "configs/sim/axis/vismach/5axis/table-rotary-tilting");
|
||||
const vendorConfigDistDir = join(distDir, "wasm-port/vendor/linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting");
|
||||
await mkdir(configDistDir, { recursive: true });
|
||||
await mkdir(vendorConfigDistDir, { recursive: true });
|
||||
await cp(configSrcDir, configDistDir, { recursive: true });
|
||||
await cp(configSrcDir, vendorConfigDistDir, { recursive: true });
|
||||
}
|
||||
|
||||
async function copyLinuxCncManifest() {
|
||||
|
||||
@@ -20,8 +20,11 @@ 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;
|
||||
store.subscribe((state) => {
|
||||
if (state.machineProfile !== attachedIniProfile) {
|
||||
attachedIniProfile = state.machineProfile;
|
||||
@@ -31,6 +34,11 @@ store.subscribe((state) => {
|
||||
attachedKinematicsProfile = state.machineProfile;
|
||||
attachDefaultKinematicsRuntime(store, state.profile.kinematicsModuleId || state.machineProfile).catch(() => {});
|
||||
}
|
||||
if (state.machineProfile !== attachedMachineFileProfile) {
|
||||
attachedMachineFileProfile = state.machineProfile;
|
||||
machineFileSeedPromise = ensureMachineFilesForProfile(store);
|
||||
window.webRtcp5AxisSimulation.machineFileSeedReady = machineFileSeedPromise;
|
||||
}
|
||||
});
|
||||
|
||||
window.webRtcp5AxisSimulation = {
|
||||
@@ -46,6 +54,7 @@ window.webRtcp5AxisSimulation = {
|
||||
kinematicsRuntimeReady,
|
||||
interpreterRuntimeReady,
|
||||
taskHalRuntimeReady,
|
||||
machineFileSeedReady,
|
||||
};
|
||||
|
||||
store.dispatch({ type: "BOOT_READY" });
|
||||
@@ -61,6 +70,22 @@ async function attachProfileIniConfig(store, profile) {
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
} catch (error) {
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-machine-file-staging-seed",
|
||||
status: "error",
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function attachDefaultKinematicsRuntime(store, moduleId = "xyzac-trt") {
|
||||
const sdkModuleUrls = [
|
||||
new URL("../../../wasm-port/runtime/sdk/src/linuxcnc-kinematics.js", import.meta.url).href,
|
||||
|
||||
@@ -90,7 +90,7 @@ export async function saveFiveAxisSessionSnapshot(sessionId, payload, options =
|
||||
const path = sessionSnapshotPath(sessionId, options.filename);
|
||||
const storage = resolveSessionStorage(options);
|
||||
await saveTextFile(path, `${JSON.stringify(snapshot, null, 2)}\n`, storage.storage);
|
||||
return { snapshot, path, storageMode: storage.mode };
|
||||
return { snapshot, path, storageMode: storage.mode, storageCapability: storage.capability };
|
||||
}
|
||||
|
||||
export async function loadFiveAxisSessionSnapshot(sessionId, options = {}) {
|
||||
@@ -103,7 +103,12 @@ export async function loadFiveAxisSessionSnapshot(sessionId, options = {}) {
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid five-axis session snapshot JSON: ${error.message}`);
|
||||
}
|
||||
return { snapshot: validateFiveAxisSessionSnapshot(snapshot, sessionId), path, storageMode: storage.mode };
|
||||
return {
|
||||
snapshot: validateFiveAxisSessionSnapshot(snapshot, sessionId),
|
||||
path,
|
||||
storageMode: storage.mode,
|
||||
storageCapability: storage.capability,
|
||||
};
|
||||
}
|
||||
|
||||
export function restoreFiveAxisSessionState(snapshot) {
|
||||
@@ -150,24 +155,65 @@ export function createMemorySessionStorage(seed = {}) {
|
||||
|
||||
let browserMemorySessionStorage = null;
|
||||
|
||||
export function detectBrowserStorageCapability(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-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",
|
||||
};
|
||||
}
|
||||
|
||||
function resolveSessionStorage(options = {}) {
|
||||
if (options.storage) {
|
||||
return {
|
||||
storage: options.storage,
|
||||
mode: options.storageMode || storageModeFor(options.storage),
|
||||
capability: {
|
||||
apiName: "web-rtcp-5axis-storage-capability",
|
||||
opfsAvailable: options.storageMode === "opfs",
|
||||
opfsUnavailable: options.storageMode !== "opfs",
|
||||
secureContext: true,
|
||||
forcedUnavailable: false,
|
||||
hasNavigatorStorage: false,
|
||||
hasGetDirectory: typeof options.storage.getDirectory === "function",
|
||||
fallbackMode: options.storageMode === "opfs" ? null : options.storageMode || "custom",
|
||||
reason: "explicit_storage",
|
||||
},
|
||||
};
|
||||
}
|
||||
const capability = detectBrowserStorageCapability();
|
||||
const browserStorage = globalThis.navigator?.storage;
|
||||
if (browserStorage?.getDirectory) {
|
||||
if (capability.opfsAvailable) {
|
||||
return {
|
||||
storage: browserStorage,
|
||||
mode: "opfs",
|
||||
capability,
|
||||
};
|
||||
}
|
||||
browserMemorySessionStorage ??= createMemorySessionStorage();
|
||||
return {
|
||||
storage: browserMemorySessionStorage,
|
||||
mode: "memory-fallback",
|
||||
capability,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -200,13 +200,14 @@ function runPlannerTiming(tpRuntime, motion) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const sampleStride = motion.length > 1000 ? Math.ceil(motion.length / 90) : 10;
|
||||
return tpRuntime.sdk.runCanonicalMotionTiming({
|
||||
motion,
|
||||
options: {
|
||||
cycleTime: 0.001,
|
||||
queueSize: 32,
|
||||
maxCycles: 2000000,
|
||||
sampleStride: 10,
|
||||
sampleStride,
|
||||
maxVelocity: 35,
|
||||
maxAcceleration: 500,
|
||||
maxJerk: 1000,
|
||||
@@ -334,6 +335,9 @@ export function prepareLinuxCncProgramForRuntime(programText) {
|
||||
const switchkinsEvents = [];
|
||||
const runtimeLines = String(programText).split(/\r?\n/).map((line, index) => {
|
||||
const lineNumber = index + 1;
|
||||
if (String(line).trim() === "%") {
|
||||
return "(linuxcnc program delimiter)";
|
||||
}
|
||||
const events = readSwitchkinsEventsFromLine(line, lineNumber);
|
||||
if (events.length === 0) return line;
|
||||
switchkinsEvents.push(...events);
|
||||
|
||||
@@ -10,6 +10,7 @@ const DEFAULT_VENDOR_ROOT_URLS = [
|
||||
const TRT_MACHINE_REL = "axis/vismach/5axis/table-rotary-tilting";
|
||||
const TRT_DEMO_SOURCE_PREFIX = `configs/sim/${TRT_MACHINE_REL}/demos/`;
|
||||
const OPFS_ROOT = "web-rtcp-5axis-sim-plan/machines";
|
||||
let browserMemoryMachineFileStorage = null;
|
||||
|
||||
export async function createMachineFileStagingPlan({
|
||||
profile,
|
||||
@@ -79,6 +80,23 @@ export function listLinuxCncGcodeSources(save) {
|
||||
.sort((left, right) => left.filename.localeCompare(right.filename));
|
||||
}
|
||||
|
||||
export function listProjectGcodeFiles(save) {
|
||||
return [...(save?.files || [])]
|
||||
.filter((file) => file.kind === "demo" || file.kind === "remap")
|
||||
.map((file) => ({
|
||||
sourceRel: file.sourceRel,
|
||||
wasmPath: file.wasmPath,
|
||||
opfsPath: file.opfsPath,
|
||||
filename: basename(file.sourceRel),
|
||||
bytes: file.bytes,
|
||||
kind: file.kind,
|
||||
semanticBoundary: file.kind === "demo"
|
||||
? "linuxcnc_vendored_5axis_gcode_source_file"
|
||||
: "linuxcnc_vendored_5axis_remap_subroutine_file",
|
||||
}))
|
||||
.sort((left, right) => left.sourceRel.localeCompare(right.sourceRel));
|
||||
}
|
||||
|
||||
export function selectMachineFileProgram(plan, save, sourceRel) {
|
||||
if (!isLinuxCncFiveAxisGcodeSourceRel(sourceRel)) {
|
||||
throw new Error(`5-axis G-code source must come from LinuxCNC source demos: ${sourceRel}`);
|
||||
@@ -106,10 +124,11 @@ export async function saveMachineFileStagingPlan(plan, options = {}) {
|
||||
if (plan?.apiName !== "web-rtcp-5axis-machine-file-staging-plan") {
|
||||
throw new Error("saveMachineFileStagingPlan requires a machine-file staging plan");
|
||||
}
|
||||
const storage = resolveMachineFileStorage(options);
|
||||
const savedFiles = [];
|
||||
for (const file of plan.files) {
|
||||
const text = await readTextFromCandidateUrls(sourceUrlsFor(file.sourceRel));
|
||||
await saveTextFile(file.opfsPath, text, options.storage);
|
||||
await saveTextFile(file.opfsPath, text, storage.storage);
|
||||
savedFiles.push({
|
||||
sourceRel: file.sourceRel,
|
||||
opfsPath: file.opfsPath,
|
||||
@@ -129,8 +148,11 @@ export async function saveMachineFileStagingPlan(plan, options = {}) {
|
||||
savedAt: new Date().toISOString(),
|
||||
fileCount: savedFiles.length,
|
||||
opfsRoot: `${OPFS_ROOT}/${plan.profileId}`,
|
||||
storageMode: storage.mode,
|
||||
storageCapability: storage.capability,
|
||||
files: savedFiles,
|
||||
gcodeSources: listLinuxCncGcodeSources({ files: savedFiles }),
|
||||
gcodeFiles: listProjectGcodeFiles({ files: savedFiles }),
|
||||
summary: summarizeSavedFiles(savedFiles),
|
||||
taskHalSession: {
|
||||
...(plan.taskHalSession || {}),
|
||||
@@ -142,7 +164,9 @@ export async function saveMachineFileStagingPlan(plan, options = {}) {
|
||||
bytes: file.bytes,
|
||||
})),
|
||||
},
|
||||
semanticBoundary: "opfs_machine_file_text_staging_only",
|
||||
semanticBoundary: storage.mode === "opfs"
|
||||
? "opfs_machine_file_text_staging_only"
|
||||
: "memory_machine_file_text_staging_current_page_lifecycle_only",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -155,15 +179,95 @@ export async function stageProfileMachineFiles(profile, options = {}) {
|
||||
manifestUrl: options.manifestUrl,
|
||||
wasmDir: options.wasmDir,
|
||||
});
|
||||
const save = await saveMachineFileStagingPlan(plan, { storage: options.storage });
|
||||
const save = await saveMachineFileStagingPlan(plan, {
|
||||
storage: options.storage,
|
||||
storageMode: options.storageMode,
|
||||
});
|
||||
return { plan, save };
|
||||
}
|
||||
|
||||
export function detectMachineFileStorageCapability(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-machine-file-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",
|
||||
};
|
||||
}
|
||||
|
||||
function resolveMachineFileStorage(options = {}) {
|
||||
if (options.storage) {
|
||||
return {
|
||||
storage: options.storage,
|
||||
mode: options.storageMode || storageModeFor(options.storage),
|
||||
capability: {
|
||||
apiName: "web-rtcp-5axis-machine-file-storage-capability",
|
||||
opfsAvailable: options.storageMode === "opfs",
|
||||
opfsUnavailable: options.storageMode !== "opfs",
|
||||
secureContext: true,
|
||||
fallbackMode: options.storageMode === "opfs" ? null : options.storageMode || "custom",
|
||||
reason: "explicit_storage",
|
||||
},
|
||||
};
|
||||
}
|
||||
const capability = detectMachineFileStorageCapability();
|
||||
if (capability.opfsAvailable) {
|
||||
return {
|
||||
storage: globalThis.navigator.storage,
|
||||
mode: "opfs",
|
||||
capability,
|
||||
};
|
||||
}
|
||||
browserMemoryMachineFileStorage ??= createMemoryStorage();
|
||||
return {
|
||||
storage: browserMemoryMachineFileStorage,
|
||||
mode: "memory-fallback",
|
||||
capability,
|
||||
};
|
||||
}
|
||||
|
||||
function storageModeFor(storage) {
|
||||
return storage?.apiName === "web-rtcp-5axis-memory-machine-file-storage" ||
|
||||
storage?.apiName === "web-rtcp-5axis-memory-session-storage"
|
||||
? "memory"
|
||||
: "custom";
|
||||
}
|
||||
|
||||
function createMemoryStorage(seed = {}) {
|
||||
const files = new Map(Object.entries(seed));
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-memory-machine-file-storage",
|
||||
files,
|
||||
async getDirectory() {
|
||||
return createDirectoryHandle(files, []);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function summarizePlan(files) {
|
||||
const kinds = countKinds(files.map((file) => classifySourceRel(file.sourceRel)));
|
||||
return {
|
||||
fileCount: files.length,
|
||||
requiredFileCount: files.filter((file) => file.sourceRel.endsWith(".ini") || file.sourceRel.includes("/demos/")).length,
|
||||
gcodeFileCount: (kinds.demo || 0) + (kinds.remap || 0),
|
||||
remapFileCount: kinds.remap || 0,
|
||||
demoFileCount: kinds.demo || 0,
|
||||
toolTableFileCount: kinds.toolTable || 0,
|
||||
@@ -208,10 +312,12 @@ function isLinuxCncFiveAxisGcodeSourceRel(sourceRel) {
|
||||
}
|
||||
|
||||
function summarizeSavedFiles(files) {
|
||||
const kinds = countKinds(files.map((file) => file.kind));
|
||||
return {
|
||||
fileCount: files.length,
|
||||
totalBytes: files.reduce((total, file) => total + file.bytes, 0),
|
||||
kinds: countKinds(files.map((file) => file.kind)),
|
||||
gcodeFileCount: (kinds.demo || 0) + (kinds.remap || 0),
|
||||
kinds,
|
||||
opfsPaths: files.map((file) => file.opfsPath),
|
||||
};
|
||||
}
|
||||
@@ -266,7 +372,11 @@ async function readTextFromUrl(url) {
|
||||
if (!response.ok) {
|
||||
throw new Error(`failed to fetch machine staging asset ${url}: ${response.status}`);
|
||||
}
|
||||
return response.text();
|
||||
const text = await response.text();
|
||||
if (/^\s*<!doctype html/i.test(text) || /^\s*<html[\s>]/i.test(text)) {
|
||||
throw new Error(`machine staging asset resolved to HTML fallback ${url}`);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
async function saveTextFile(path, text, storage = globalThis.navigator?.storage) {
|
||||
@@ -294,6 +404,34 @@ async function ensureParentDir(root, path) {
|
||||
return current;
|
||||
}
|
||||
|
||||
function createDirectoryHandle(files, prefix) {
|
||||
return {
|
||||
async getDirectoryHandle(name) {
|
||||
return createDirectoryHandle(files, [...prefix, name]);
|
||||
},
|
||||
async getFileHandle(name) {
|
||||
const path = [...prefix, name].join("/");
|
||||
return {
|
||||
async createWritable() {
|
||||
let content = "";
|
||||
return {
|
||||
async write(text) {
|
||||
content += String(text);
|
||||
},
|
||||
async close() {
|
||||
files.set(path, content);
|
||||
},
|
||||
};
|
||||
},
|
||||
async getFile() {
|
||||
if (!files.has(path)) throw new Error(`Missing memory machine file: ${path}`);
|
||||
return { async text() { return files.get(path); } };
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function splitPath(path) {
|
||||
const parts = String(path || "").replaceAll("\\", "/").split("/").filter(Boolean);
|
||||
if (parts.length === 0 || parts.some((part) => part === "." || part === "..")) {
|
||||
|
||||
161
web-rtcp-5axis-sim-plan/app/src/runtime/native-task-hal-audit.js
Normal file
161
web-rtcp-5axis-sim-plan/app/src/runtime/native-task-hal-audit.js
Normal file
@@ -0,0 +1,161 @@
|
||||
const WEB_SIMULATION_BOUNDARY = "linuxcnc_task_motion_hal_wasm_simulation_runtime";
|
||||
const ALLOWED_NATIVE_PROBE_STATUSES = new Set([
|
||||
"passed",
|
||||
"ready_disabled_by_default",
|
||||
"skipped_missing_host_runtime",
|
||||
]);
|
||||
|
||||
export function createNativeTaskHalReadinessAudit({
|
||||
sourceManifest = {},
|
||||
nativeProbe = {},
|
||||
fullExecutionBoundary = {},
|
||||
taskHalRuntimeReadiness = {},
|
||||
taskHalStatus = {},
|
||||
generatedAt = new Date().toISOString(),
|
||||
artifactPaths = {},
|
||||
} = {}) {
|
||||
const taskSourceCount = numberFrom(sourceManifest.task_source_count);
|
||||
const halSourceCount = numberFrom(sourceManifest.hal_source_count);
|
||||
const motionSourceCount = numberFrom(sourceManifest.motion_source_count);
|
||||
const sourceManifestReady = truthy(sourceManifest.task_hal_source_manifest_ready)
|
||||
&& truthy(sourceManifest.task_hal_reference_source_ready)
|
||||
&& taskSourceCount > 0
|
||||
&& halSourceCount > 0
|
||||
&& motionSourceCount > 0;
|
||||
|
||||
const nativeProbeStatus = String(
|
||||
nativeProbe.native_probe_status
|
||||
|| nativeProbe.trt_task_hal_runtime_probe_status
|
||||
|| "missing",
|
||||
);
|
||||
const nativeProbeOk = String(nativeProbe.native_task_hal_probe || "") === "ok"
|
||||
&& ALLOWED_NATIVE_PROBE_STATUSES.has(nativeProbeStatus);
|
||||
const nativePromotionBlocked = !truthy(nativeProbe.trt_task_hal_promotion_allowed)
|
||||
&& String(nativeProbe.nativeTaskReady) !== "true"
|
||||
&& String(nativeProbe.nativeHalSyncReady) !== "true";
|
||||
|
||||
const taskReady = Boolean(
|
||||
fullExecutionBoundary.taskRuntimeReady
|
||||
|| taskHalRuntimeReadiness.taskRuntimeReady
|
||||
|| taskHalStatus.summary?.taskRuntimeReady,
|
||||
);
|
||||
const motionReady = Boolean(
|
||||
fullExecutionBoundary.motionRuntimeReady
|
||||
|| taskHalRuntimeReadiness.motionRuntimeReady
|
||||
|| taskHalStatus.summary?.motionRuntimeReady,
|
||||
);
|
||||
const halReady = Boolean(
|
||||
fullExecutionBoundary.halRuntimeReady
|
||||
|| taskHalRuntimeReadiness.halRuntimeReady
|
||||
|| taskHalStatus.summary?.halRuntimeReady,
|
||||
);
|
||||
const halSyncReady = Boolean(
|
||||
fullExecutionBoundary.halSyncReady
|
||||
|| taskHalRuntimeReadiness.halSyncReady
|
||||
|| taskHalStatus.summary?.halSyncReady,
|
||||
);
|
||||
|
||||
const fullBoundarySimulationReady = Boolean(
|
||||
fullExecutionBoundary.semanticBoundary === WEB_SIMULATION_BOUNDARY
|
||||
&& fullExecutionBoundary.fullLinuxCncProgramExecutionReady === true
|
||||
&& fullExecutionBoundary.promotionAllowed === true,
|
||||
);
|
||||
const hardwareBlocked = fullExecutionBoundary.hardwareDrive === false
|
||||
&& fullExecutionBoundary.hostRealtimeKernel === false
|
||||
&& fullExecutionBoundary.externalUserMProcessReady === false
|
||||
&& fullExecutionBoundary.toolDbProcessReady === false;
|
||||
const webSimulationConsistent = sourceManifestReady
|
||||
&& nativeProbeOk
|
||||
&& nativePromotionBlocked
|
||||
&& taskReady
|
||||
&& motionReady
|
||||
&& halReady
|
||||
&& halSyncReady
|
||||
&& fullBoundarySimulationReady
|
||||
&& hardwareBlocked;
|
||||
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-native-task-hal-readiness-audit",
|
||||
batch: "M18-native-task-hal-source-and-artifact-audit",
|
||||
generatedAt,
|
||||
status: webSimulationConsistent ? "ok" : "blocked",
|
||||
semanticBoundary: WEB_SIMULATION_BOUNDARY,
|
||||
promotionScope: "web_simulation_only",
|
||||
taskHalWebSimulationBoundaryConsistent: webSimulationConsistent,
|
||||
webSimulation: {
|
||||
promoted: fullBoundarySimulationReady,
|
||||
taskRuntimeReady: taskReady,
|
||||
motionRuntimeReady: motionReady,
|
||||
halRuntimeReady: halReady,
|
||||
nativeTaskReady: fullExecutionBoundary.nativeTaskReady === true,
|
||||
nativeHalSyncReady: fullExecutionBoundary.nativeHalSyncReady === true,
|
||||
fullLinuxCncProgramExecutionReady:
|
||||
fullExecutionBoundary.fullLinuxCncProgramExecutionReady === true,
|
||||
promotionAllowed: fullExecutionBoundary.promotionAllowed === true,
|
||||
},
|
||||
nativeHostAndHardware: {
|
||||
nativeProbe: nativeProbeOk ? "ok" : "blocked",
|
||||
nativeProbeStatus,
|
||||
nativePromotionAllowed: truthy(nativeProbe.trt_task_hal_promotion_allowed),
|
||||
hardwareDrive: false,
|
||||
hostRealtimeKernel: false,
|
||||
externalUserMProcessReady: false,
|
||||
toolDbProcessReady: false,
|
||||
},
|
||||
sourceManifest: {
|
||||
ready: sourceManifestReady,
|
||||
taskSourceCount,
|
||||
halSourceCount,
|
||||
motionSourceCount,
|
||||
nmlSourceCount: numberFrom(sourceManifest.nml_source_count),
|
||||
libnmlSourceCount: numberFrom(sourceManifest.libnml_source_count),
|
||||
referenceSourceReady: truthy(sourceManifest.task_hal_reference_source_ready),
|
||||
vendorSourceReady: truthy(sourceManifest.task_hal_vendor_source_ready),
|
||||
vendorHashMatchReady: truthy(sourceManifest.task_hal_vendor_hash_match_ready),
|
||||
},
|
||||
gates: {
|
||||
task_hal_web_simulation_boundary_consistent: webSimulationConsistent ? 1 : 0,
|
||||
native_task_hal_host_probe_status: nativeProbeStatus,
|
||||
hardware_drive: 0,
|
||||
host_realtime_kernel: 0,
|
||||
external_user_m_process_ready: 0,
|
||||
tool_db_process_ready: 0,
|
||||
promotion_scope: "web_simulation_only",
|
||||
},
|
||||
artifacts: artifactPaths,
|
||||
blockers: webSimulationConsistent ? [] : buildBlockers({
|
||||
sourceManifestReady,
|
||||
nativeProbeOk,
|
||||
nativePromotionBlocked,
|
||||
taskReady,
|
||||
motionReady,
|
||||
halReady,
|
||||
halSyncReady,
|
||||
fullBoundarySimulationReady,
|
||||
hardwareBlocked,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function buildBlockers(checks) {
|
||||
const blockers = [];
|
||||
if (!checks.sourceManifestReady) blockers.push("task/HAL source manifest proof is incomplete");
|
||||
if (!checks.nativeProbeOk) blockers.push("native host probe did not produce an accepted default status");
|
||||
if (!checks.nativePromotionBlocked) blockers.push("native probe unexpectedly allowed promotion");
|
||||
if (!checks.taskReady) blockers.push("task runtime is not ready in the Web simulation boundary");
|
||||
if (!checks.motionReady) blockers.push("motion runtime is not ready in the Web simulation boundary");
|
||||
if (!checks.halReady) blockers.push("HAL runtime is not ready in the Web simulation boundary");
|
||||
if (!checks.halSyncReady) blockers.push("task/motion/HAL sync is not ready in the Web simulation boundary");
|
||||
if (!checks.fullBoundarySimulationReady) blockers.push("full execution boundary is not promoted for Web simulation");
|
||||
if (!checks.hardwareBlocked) blockers.push("hardware/native process blocked fields are not explicitly false");
|
||||
return blockers;
|
||||
}
|
||||
|
||||
function truthy(value) {
|
||||
return value === true || value === 1 || value === "1" || value === "true";
|
||||
}
|
||||
|
||||
function numberFrom(value) {
|
||||
const numeric = Number(value);
|
||||
return Number.isFinite(numeric) ? numeric : 0;
|
||||
}
|
||||
@@ -60,6 +60,7 @@ const initialState = {
|
||||
status: "not-saved",
|
||||
path: null,
|
||||
storageMode: null,
|
||||
storageCapability: null,
|
||||
savedAt: null,
|
||||
restoredAt: null,
|
||||
lastError: null,
|
||||
@@ -70,6 +71,8 @@ const initialState = {
|
||||
profileId: null,
|
||||
fileCount: 0,
|
||||
opfsRoot: null,
|
||||
storageMode: null,
|
||||
storageCapability: null,
|
||||
savedAt: null,
|
||||
lastError: null,
|
||||
plan: null,
|
||||
@@ -632,6 +635,7 @@ export function createSimulationStore(seed = {}) {
|
||||
status: "saved",
|
||||
path: action.path,
|
||||
storageMode: action.storageMode || null,
|
||||
storageCapability: action.storageCapability || null,
|
||||
savedAt: action.savedAt,
|
||||
lastError: null,
|
||||
},
|
||||
@@ -661,6 +665,19 @@ export function createSimulationStore(seed = {}) {
|
||||
setState({
|
||||
...action.restoredState,
|
||||
profile: restoredProfile,
|
||||
linuxCncIniConfig: state.linuxCncIniConfig,
|
||||
iniConfigReadiness: state.iniConfigReadiness,
|
||||
kinematicsRuntime: state.kinematicsRuntime,
|
||||
kinematicsRuntimeReadiness: state.kinematicsRuntimeReadiness,
|
||||
kinematicsExecutionContext: state.kinematicsExecutionContext,
|
||||
interpreterRuntime: state.interpreterRuntime,
|
||||
interpreterRuntimeReadiness: state.interpreterRuntimeReadiness,
|
||||
taskHalRuntime: state.taskHalRuntime,
|
||||
taskHalRuntimeReadiness: state.taskHalRuntimeReadiness,
|
||||
taskHalStatus: state.taskHalStatus,
|
||||
taskHalSession: state.taskHalSession,
|
||||
machineFileStaging: state.machineFileStaging,
|
||||
machineFileExecution: state.machineFileExecution,
|
||||
linuxCncBoundaryAdapter: adapter,
|
||||
linuxCncBoundaryReadiness: createLinuxCncBoundaryReadiness(adapter),
|
||||
sessionPersistence: {
|
||||
@@ -668,6 +685,7 @@ export function createSimulationStore(seed = {}) {
|
||||
status: "restored",
|
||||
path: action.path,
|
||||
storageMode: action.storageMode || null,
|
||||
storageCapability: action.storageCapability || null,
|
||||
restoredAt: action.restoredAt,
|
||||
lastError: null,
|
||||
},
|
||||
@@ -704,6 +722,8 @@ export function createSimulationStore(seed = {}) {
|
||||
profileId: action.plan.profileId,
|
||||
fileCount: action.save.fileCount,
|
||||
opfsRoot: action.save.opfsRoot,
|
||||
storageMode: action.save.storageMode || null,
|
||||
storageCapability: action.save.storageCapability || null,
|
||||
savedAt: action.save.savedAt,
|
||||
lastError: null,
|
||||
plan: action.plan,
|
||||
@@ -1407,7 +1427,7 @@ export function createSimulationStore(seed = {}) {
|
||||
const sessionId = options.sessionId || state.sessionPersistence.sessionId;
|
||||
const filename = options.filename || state.sessionPersistence.filename;
|
||||
const payload = createFiveAxisSessionPayload(state);
|
||||
const { snapshot, path, storageMode } = await saveFiveAxisSessionSnapshot(sessionId, payload, {
|
||||
const { snapshot, path, storageMode, storageCapability } = await saveFiveAxisSessionSnapshot(sessionId, payload, {
|
||||
filename,
|
||||
storage: options.storage,
|
||||
storageMode: options.storageMode,
|
||||
@@ -1417,9 +1437,10 @@ export function createSimulationStore(seed = {}) {
|
||||
type: "SESSION_SAVE_COMPLETE",
|
||||
path,
|
||||
storageMode,
|
||||
storageCapability,
|
||||
savedAt: snapshot.createdAt,
|
||||
});
|
||||
return { snapshot, path, storageMode };
|
||||
return { snapshot, path, storageMode, storageCapability };
|
||||
} catch (error) {
|
||||
dispatch({ type: "SESSION_PERSISTENCE_FAILED", error: error.message });
|
||||
throw error;
|
||||
@@ -1431,7 +1452,7 @@ export function createSimulationStore(seed = {}) {
|
||||
try {
|
||||
const sessionId = options.sessionId || state.sessionPersistence.sessionId;
|
||||
const filename = options.filename || state.sessionPersistence.filename;
|
||||
const { snapshot, path, storageMode } = await loadFiveAxisSessionSnapshot(sessionId, {
|
||||
const { snapshot, path, storageMode, storageCapability } = await loadFiveAxisSessionSnapshot(sessionId, {
|
||||
filename,
|
||||
storage: options.storage,
|
||||
storageMode: options.storageMode,
|
||||
@@ -1441,10 +1462,11 @@ export function createSimulationStore(seed = {}) {
|
||||
restoredState: restoreFiveAxisSessionState(snapshot),
|
||||
path,
|
||||
storageMode,
|
||||
storageCapability,
|
||||
restoredAt: new Date().toISOString(),
|
||||
});
|
||||
await refreshAsyncKinematicsFrame({ operatorMessage: `5-axis session restored ${path}` });
|
||||
return { snapshot, path, storageMode };
|
||||
return { snapshot, path, storageMode, storageCapability };
|
||||
} catch (error) {
|
||||
dispatch({ type: "SESSION_PERSISTENCE_FAILED", error: error.message });
|
||||
throw error;
|
||||
|
||||
@@ -324,7 +324,7 @@ function renderInfoTabs(element, state) {
|
||||
<dt>Canonical:</dt><dd data-program-execution-summary="${state.programExecution?.summary?.motionEventCount ?? 0}">${state.programExecution?.summary?.motionEventCount ?? 0} motion / ${state.programExecution?.summary?.canonicalEventCount ?? 0} events</dd>
|
||||
<dt>Switchkins:</dt><dd data-program-switchkins-summary="${state.programExecution?.summary?.switchkinsEventCount ?? 0}">${formatSwitchkinsSummary(state.programExecution)}</dd>
|
||||
<dt>Machine run:</dt><dd data-machine-file-execution="status">${formatMachineFileExecution(state.machineFileExecution)}</dd>
|
||||
<dt>Session:</dt><dd data-session-persistence="status">${state.sessionPersistence.status} / ${state.sessionPersistence.storageMode ?? "-"} / ${state.sessionPersistence.path ?? "-"}</dd>
|
||||
<dt>Session:</dt><dd data-session-persistence="status">${formatSessionPersistence(state.sessionPersistence)}</dd>
|
||||
<dt>Tool preview:</dt><dd data-tool-preview="detail">T${state.toolPreview.toolNumber} D${formatNumber(state.toolPreview.diameter, 2)} L${formatNumber(state.toolPreview.length, 3)} ${state.toolPreview.units}</dd>
|
||||
<dt>Program time:</dt><dd data-program-timing="summary">${formatProgramTiming(state)}</dd>
|
||||
<dt>Segment time:</dt><dd data-program-timing="segment">${formatProgramTimingSegment(state)}</dd>
|
||||
@@ -348,6 +348,7 @@ function renderInfoTabs(element, state) {
|
||||
<dt>Full boundary:</dt><dd data-full-execution-boundary="status">${formatFullExecutionBoundary(state.fullExecutionBoundary)}</dd>
|
||||
<dt>Planner/task:</dt><dd data-full-execution-boundary="blockers">${formatFullExecutionBlockers(state.fullExecutionBoundary)}</dd>
|
||||
<dt>Boundary evidence:</dt><dd data-full-execution-boundary="evidence">${formatFullExecutionEvidence(state.fullExecutionBoundary)}</dd>
|
||||
<dt>Host/native:</dt><dd data-full-execution-boundary="host-native">${formatHostNativeBoundary(state.fullExecutionBoundary)}</dd>
|
||||
<dt>LinuxCNC kins:</dt><dd data-rtcp-diagnostic="kinematics-ready">${frame.readiness.linuxCncKinematicsReady ? "ready" : "pending"}</dd>
|
||||
<dt>Kins context:</dt><dd data-rtcp-diagnostic="execution-context">${state.kinematicsExecutionContext}</dd>
|
||||
<dt>Interpreter:</dt><dd data-linuxcnc-boundary="interpreter">${state.interpreterRuntimeReadiness?.loaded ? state.interpreterRuntimeReadiness.semanticBoundary : "pending"}</dd>
|
||||
@@ -427,7 +428,21 @@ function formatMachineFileStaging(machineFileStaging) {
|
||||
if (machineFileStaging.status === "error") {
|
||||
return `error ${machineFileStaging.lastError || "-"}`;
|
||||
}
|
||||
return `${machineFileStaging.status} ${machineFileStaging.fileCount || 0} files ${machineFileStaging.opfsRoot || "-"}`;
|
||||
const storage = machineFileStaging.storageMode || machineFileStaging.save?.storageMode || "-";
|
||||
const reason = machineFileStaging.storageCapability?.reason || machineFileStaging.save?.storageCapability?.reason || "-";
|
||||
const gcodeFiles = machineFileStaging.save?.summary?.gcodeFileCount ?? 0;
|
||||
return `${machineFileStaging.status} ${machineFileStaging.fileCount || 0} files ${gcodeFiles} gcode ${storage} ${reason} ${machineFileStaging.opfsRoot || "-"}`;
|
||||
}
|
||||
|
||||
function formatSessionPersistence(sessionPersistence) {
|
||||
const capability = sessionPersistence.storageCapability || {};
|
||||
return [
|
||||
sessionPersistence.status,
|
||||
sessionPersistence.storageMode ?? "-",
|
||||
capability.opfsUnavailable ? "opfs unavailable" : capability.opfsAvailable ? "opfs available" : "storage pending",
|
||||
capability.reason || "-",
|
||||
sessionPersistence.path ?? "-",
|
||||
].join(" / ");
|
||||
}
|
||||
|
||||
function formatMachineFileExecution(machineFileExecution) {
|
||||
@@ -471,6 +486,16 @@ function formatFullExecutionEvidence(boundary) {
|
||||
return `${boundary.satisfied.length} satisfied / ${boundary.missing.length} missing / ${boundary.semanticBoundary}`;
|
||||
}
|
||||
|
||||
function formatHostNativeBoundary(boundary) {
|
||||
if (!boundary) return "pending";
|
||||
return [
|
||||
boundary.hardwareDrive ? "hardware drive enabled" : "hardware drive false",
|
||||
boundary.hostRealtimeKernel ? "host realtime enabled" : "host realtime false",
|
||||
boundary.externalUserMProcessReady ? "external user-M ready" : "external user-M false",
|
||||
boundary.toolDbProcessReady ? "tool DB ready" : "tool DB false",
|
||||
].join(" / ");
|
||||
}
|
||||
|
||||
function renderOverride(element, state, dispatch) {
|
||||
element.innerHTML = `
|
||||
<section class="meter-card">
|
||||
|
||||
@@ -38,12 +38,17 @@ export function renderFiveAxisScene(canvas, state) {
|
||||
exposePreviewDataset(canvas, state, {
|
||||
pointCount,
|
||||
executedPointCount,
|
||||
feedPointCount: preview.feedPath.geometry.getAttribute("position").count,
|
||||
rapidPointCount: preview.rapidPath.geometry.getAttribute("position").count,
|
||||
arcPointCount: preview.arcPath.geometry.getAttribute("position").count,
|
||||
currentSegmentPointCount: preview.currentSegmentPath.geometry.getAttribute("position").count,
|
||||
sceneObjectCount: countSceneObjects(preview.scene),
|
||||
toolhead: preview.currentToolhead,
|
||||
renderer: "webgl",
|
||||
sceneMode: "program-preview-and-tool-execution",
|
||||
cameraControls: preview.controls.enabled,
|
||||
toolExecutionMarker: preview.toolMarker.visible,
|
||||
pathFitBounds: preview.pathFitBoundsReady,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -68,8 +73,11 @@ function createScene(canvas) {
|
||||
const camera = new THREE.PerspectiveCamera(42, 1, 0.1, 100);
|
||||
|
||||
const previewPath = createLine(0x808892, 0.56);
|
||||
const feedPath = createLine(0x4fb3ff, 0.92);
|
||||
const executedPath = createLine(0x1ffff4, 1);
|
||||
const rapidPath = createLine(0xffb13b, 0.82);
|
||||
const arcPath = createLine(0xd7ff62, 0.95);
|
||||
const currentSegmentPath = createLine(0xff4fd8, 1);
|
||||
const toolMarker = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(0.065, 18, 12),
|
||||
new THREE.MeshBasicMaterial({ color: 0x1ffff4 }),
|
||||
@@ -78,7 +86,7 @@ function createScene(canvas) {
|
||||
EMPTY_GEOMETRY.clone(),
|
||||
new THREE.LineBasicMaterial({ color: 0x1ffff4, transparent: true, opacity: 0.9 }),
|
||||
);
|
||||
scene.add(previewPath, rapidPath, executedPath, toolAxis, toolMarker);
|
||||
scene.add(previewPath, feedPath, rapidPath, arcPath, executedPath, currentSegmentPath, toolAxis, toolMarker);
|
||||
|
||||
const controls = createToolpathCameraControls(canvas, camera, () => {
|
||||
renderer.render(scene, camera);
|
||||
@@ -90,13 +98,18 @@ function createScene(canvas) {
|
||||
camera,
|
||||
controls,
|
||||
previewPath,
|
||||
feedPath,
|
||||
executedPath,
|
||||
rapidPath,
|
||||
arcPath,
|
||||
currentSegmentPath,
|
||||
toolMarker,
|
||||
toolAxis,
|
||||
currentToolhead: new THREE.Vector3(),
|
||||
lastSelectedView: null,
|
||||
lastCameraRevision: null,
|
||||
lastFitKey: null,
|
||||
pathFitBoundsReady: false,
|
||||
};
|
||||
resizeRenderer(preview);
|
||||
resetCamera(preview, "iso");
|
||||
@@ -133,6 +146,10 @@ function renderFallbackPreview(preview, state) {
|
||||
|
||||
const previewPoints = buildProgramPreviewPoints(state);
|
||||
const executedPoints = buildExecutedProgramPoints(state, previewPoints);
|
||||
const rapidPoints = buildRapidPreviewPoints(state);
|
||||
const feedPoints = buildTypedPreviewPoints(state, "STRAIGHT_FEED");
|
||||
const arcPoints = buildTypedPreviewPoints(state, "ARC_FEED");
|
||||
const currentSegmentPoints = buildCurrentSegmentPoints(state);
|
||||
const pointCount = previewPoints.length;
|
||||
const executedPointCount = executedPoints.length;
|
||||
if (pointCount > 0) {
|
||||
@@ -150,8 +167,24 @@ function renderFallbackPreview(preview, state) {
|
||||
drawFallbackPolyline(ctx, executedPoints, cx, cy, scale);
|
||||
ctx.stroke();
|
||||
}
|
||||
if (currentSegmentPoints.length > 0) {
|
||||
ctx.strokeStyle = "#ff4fd8";
|
||||
ctx.lineWidth = 4;
|
||||
ctx.beginPath();
|
||||
drawFallbackPolyline(ctx, currentSegmentPoints, cx, cy, scale);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
const toolPosition = executionToolPosition(state, previewPoints);
|
||||
const fitPoints = collectFitPoints(previewPoints, executedPoints, currentSegmentPoints, toolPosition);
|
||||
const fitKey = [
|
||||
previewPoints.length,
|
||||
executedPoints.length,
|
||||
currentSegmentPoints.length,
|
||||
previewSourceMode(state),
|
||||
state.programExecutionMotionIndex || 0,
|
||||
state.programExecutionSampleIndex || 0,
|
||||
].join(":");
|
||||
if (toolPosition) {
|
||||
const toolX = cx + toolPosition.x * scale;
|
||||
const toolY = cy - toolPosition.y * scale;
|
||||
@@ -174,6 +207,11 @@ function renderFallbackPreview(preview, state) {
|
||||
sceneMode: "program-preview-and-tool-execution",
|
||||
cameraControls: false,
|
||||
toolExecutionMarker: Boolean(toolPosition),
|
||||
feedPointCount: feedPoints.length,
|
||||
rapidPointCount: rapidPoints.length,
|
||||
arcPointCount: arcPoints.length,
|
||||
currentSegmentPointCount: currentSegmentPoints.length,
|
||||
pathFitBounds: computePointBounds(previewPoints.concat(executedPoints, currentSegmentPoints)) !== null,
|
||||
});
|
||||
canvas.dataset.threeFallbackReason = preview.errorMessage;
|
||||
}
|
||||
@@ -195,6 +233,17 @@ function exposePreviewDataset(canvas, state, preview) {
|
||||
canvas.dataset.threeCameraControls = preview.cameraControls ? "orbit-pan-zoom" : "none";
|
||||
canvas.dataset.threeProgramPreviewSource = previewSourceMode(state);
|
||||
canvas.dataset.threeToolExecutionMarker = preview.toolExecutionMarker ? "true" : "false";
|
||||
canvas.dataset.threeToolpathPreviewSource = toolpathPreviewSource(state);
|
||||
canvas.dataset.threeToolExecutionTraceSource = toolExecutionTraceSource(state);
|
||||
canvas.dataset.threePathFitBounds = preview.pathFitBounds ? "ok" : "pending";
|
||||
canvas.dataset.threeCurrentSegmentHighlight = preview.currentSegmentPointCount > 0 ? "ok" : "pending";
|
||||
canvas.dataset.threeRapidFeedVisualDistinction = preview.rapidPointCount > 0 || preview.feedPointCount > 0 || preview.arcPointCount > 0 ? "ok" : "pending";
|
||||
canvas.dataset.threeNoGcodeSemanticsGeneration = "ok";
|
||||
canvas.dataset.threeRapidPathPoints = String(preview.rapidPointCount || 0);
|
||||
canvas.dataset.threeFeedPathPoints = String(preview.feedPointCount || 0);
|
||||
canvas.dataset.threeArcPathPoints = String(preview.arcPointCount || 0);
|
||||
canvas.dataset.threeCurrentSegmentPoints = String(preview.currentSegmentPointCount || 0);
|
||||
canvas.dataset.threeCurrentSegmentType = currentSegmentType(state);
|
||||
}
|
||||
|
||||
function createLine(color, opacity) {
|
||||
@@ -212,21 +261,29 @@ function updateToolpathPreview(preview, state) {
|
||||
const previewPoints = buildProgramPreviewPoints(state);
|
||||
const executedPoints = buildExecutedProgramPoints(state, previewPoints);
|
||||
const rapidPoints = buildRapidPreviewPoints(state);
|
||||
const feedPoints = buildTypedPreviewPoints(state, "STRAIGHT_FEED");
|
||||
const arcPoints = buildTypedPreviewPoints(state, "ARC_FEED");
|
||||
const currentSegmentPoints = buildCurrentSegmentPoints(state);
|
||||
const toolPosition = executionToolPosition(state, previewPoints);
|
||||
|
||||
updateLineGeometry(preview.previewPath, previewPoints);
|
||||
updateLineGeometry(preview.feedPath, feedPoints);
|
||||
updateLineGeometry(preview.executedPath, executedPoints);
|
||||
updateLineGeometry(preview.rapidPath, rapidPoints);
|
||||
updateLineGeometry(preview.arcPath, arcPoints);
|
||||
updateLineGeometry(preview.currentSegmentPath, currentSegmentPoints);
|
||||
updateToolExecutionMarker(preview, state, toolPosition);
|
||||
|
||||
const cameraRevision = state.preview.cameraRevision ?? 0;
|
||||
if (
|
||||
preview.lastSelectedView !== state.preview.selectedView ||
|
||||
preview.lastCameraRevision !== cameraRevision
|
||||
preview.lastCameraRevision !== cameraRevision ||
|
||||
preview.lastFitKey !== fitKey
|
||||
) {
|
||||
resetCamera(preview, state.preview.selectedView);
|
||||
resetCamera(preview, state.preview.selectedView, fitPoints);
|
||||
preview.lastSelectedView = state.preview.selectedView;
|
||||
preview.lastCameraRevision = cameraRevision;
|
||||
preview.lastFitKey = fitKey;
|
||||
} else {
|
||||
applyCameraControls(preview.controls);
|
||||
}
|
||||
@@ -285,15 +342,32 @@ function buildExecutedProgramPoints(state, previewPoints) {
|
||||
}
|
||||
|
||||
function buildRapidPreviewPoints(state) {
|
||||
return buildTypedPreviewPoints(state, "STRAIGHT_TRAVERSE");
|
||||
}
|
||||
|
||||
function buildTypedPreviewPoints(state, type) {
|
||||
const motion = state.programExecution?.motion;
|
||||
if (!Array.isArray(motion) || state.preview.pathPoints === 0) return [];
|
||||
return limitPoints(
|
||||
motion
|
||||
.filter((event) => event.type === "STRAIGHT_TRAVERSE")
|
||||
.filter((event) => event.type === type)
|
||||
.map((event) => vectorFromAxes(event.axes)),
|
||||
);
|
||||
}
|
||||
|
||||
function buildCurrentSegmentPoints(state) {
|
||||
if (state.preview.pathPoints === 0) return [];
|
||||
const motion = state.programExecution?.motion;
|
||||
if (!Array.isArray(motion) || motion.length === 0) return [];
|
||||
const motionIndex = clampMotionIndex(state, currentMotionIndex(state));
|
||||
const current = motion[motionIndex];
|
||||
const previous = motion[Math.max(motionIndex - 1, 0)];
|
||||
if (!current) return [];
|
||||
const start = motionIndex === 0 ? vectorFromAxes(previous?.axes || current.axes) : vectorFromAxes(previous.axes);
|
||||
const end = vectorFromAxes(current.axes);
|
||||
return start.distanceTo(end) > 0 ? [start, end] : [end];
|
||||
}
|
||||
|
||||
function buildFixturePreviewPoints(pointCount, tcpPosition) {
|
||||
const points = [];
|
||||
for (let index = 0; index < pointCount; index += 1) {
|
||||
@@ -367,6 +441,60 @@ function previewSourceMode(state) {
|
||||
return state.programExecutionSourceMode || "fixture-line-playback";
|
||||
}
|
||||
|
||||
function toolpathPreviewSource(state) {
|
||||
if (state.programExecution?.sourceMode === "linuxcnc-interpreter-wasm") {
|
||||
return "linuxcnc_interpreter_canonical_motion";
|
||||
}
|
||||
if (state.programExecution?.sourceMode === "linuxcnc-machine-file-remap-wasm") {
|
||||
return "linuxcnc_machine_file_remap_canonical_motion";
|
||||
}
|
||||
return "fixture_line_playback_not_promoted";
|
||||
}
|
||||
|
||||
function toolExecutionTraceSource(state) {
|
||||
if (Array.isArray(state.programExecutionTiming?.samples) && state.programExecutionTiming.samples.length > 0) {
|
||||
return "linuxcnc_tp_samples_or_task_motion_hal_feedback";
|
||||
}
|
||||
if (state.programRuntimeFeedback?.sourceMode === "linuxcnc-task-motion-hal-wasm") {
|
||||
return "linuxcnc_tp_samples_or_task_motion_hal_feedback";
|
||||
}
|
||||
return "fixture_line_playback_not_promoted";
|
||||
}
|
||||
|
||||
function currentSegmentType(state) {
|
||||
const motion = state.programExecution?.motion;
|
||||
if (!Array.isArray(motion) || motion.length === 0) return "-";
|
||||
return motion[clampMotionIndex(state, currentMotionIndex(state))]?.type || "-";
|
||||
}
|
||||
|
||||
function currentMotionIndex(state) {
|
||||
const sample = state.programExecutionTiming?.samples?.[Number(state.programExecutionSampleIndex || 0)];
|
||||
if (Number.isFinite(Number(sample?.motionIndex))) return Number(sample.motionIndex);
|
||||
return Number(state.programExecutionMotionIndex || 0);
|
||||
}
|
||||
|
||||
function clampMotionIndex(state, index) {
|
||||
const count = state.programExecution?.motion?.length || 0;
|
||||
if (count <= 0) return 0;
|
||||
return clamp(Math.round(Number(index) || 0), 0, count - 1);
|
||||
}
|
||||
|
||||
function collectFitPoints(...groups) {
|
||||
return groups.flatMap((group) => {
|
||||
if (!group) return [];
|
||||
if (Array.isArray(group)) return group.filter(Boolean);
|
||||
return [group];
|
||||
});
|
||||
}
|
||||
|
||||
function computePointBounds(points) {
|
||||
const valid = points.filter((point) => point && Number.isFinite(point.x) && Number.isFinite(point.y) && Number.isFinite(point.z));
|
||||
if (valid.length === 0) return null;
|
||||
const box = new THREE.Box3().setFromPoints(valid);
|
||||
if (box.isEmpty()) return null;
|
||||
return box;
|
||||
}
|
||||
|
||||
function drawFallbackPolyline(ctx, points, cx, cy, scale) {
|
||||
for (let index = 0; index < points.length; index += 1) {
|
||||
const point = points[index];
|
||||
@@ -513,15 +641,30 @@ function panCamera(controls, dx, dy, canvas) {
|
||||
controls.target.addScaledVector(up, dy * speed);
|
||||
}
|
||||
|
||||
function resetCamera(preview, selectedView) {
|
||||
function resetCamera(preview, selectedView, fitPoints = []) {
|
||||
const preset = CAMERA_PRESETS[selectedView] || CAMERA_PRESETS.iso;
|
||||
preview.controls.theta = preset.theta;
|
||||
preview.controls.phi = preset.phi;
|
||||
preview.controls.radius = preset.radius;
|
||||
preview.controls.target.copy(preset.target);
|
||||
preview.pathFitBoundsReady = applyFitBounds(preview.controls, selectedView, fitPoints);
|
||||
applyCameraControls(preview.controls);
|
||||
}
|
||||
|
||||
function applyFitBounds(controls, selectedView, fitPoints) {
|
||||
const bounds = computePointBounds(fitPoints);
|
||||
if (!bounds) return false;
|
||||
const center = new THREE.Vector3();
|
||||
const size = new THREE.Vector3();
|
||||
bounds.getCenter(center);
|
||||
bounds.getSize(size);
|
||||
controls.target.copy(center);
|
||||
const maxSpan = Math.max(size.x, size.y, size.z, 0.8);
|
||||
const fitRadius = clamp(maxSpan * 1.8, 2.2, 28);
|
||||
controls.radius = selectedView === "z" ? Math.max(fitRadius, 4.2) : fitRadius;
|
||||
return true;
|
||||
}
|
||||
|
||||
function applyCameraControls(controls) {
|
||||
const sinPhiRadius = Math.sin(controls.phi) * controls.radius;
|
||||
controls.camera.position.set(
|
||||
|
||||
Reference in New Issue
Block a user