(function () { const root = document.querySelector(".workbench"); const fields = new Map( Array.from(document.querySelectorAll("[data-field]")).map((item) => [item.dataset.field, item]) ); const tabButtons = Array.from(document.querySelectorAll("[data-tab]")); const programSelect = document.querySelector("[data-program-select]"); const frameSlider = document.querySelector("[data-frame-slider]"); const frameTable = document.querySelector("[data-frame-table]"); const stepTable = document.querySelector("[data-step-table]"); const objectTree = document.querySelector("[data-object-tree]"); const pathCurrent = document.querySelector("[data-path-current]"); const parserProgress = document.querySelector("[data-parser-progress]"); const fallbackManifest = { release_id: "local", suite_id: "working2-abb120-spec", job_id: "W2-JOB-latest", robot_id: "abb_irb120_3_58", report_html: "test-results/abb120-spec/W2-JOB-latest/report.html", programs: [ { program_id: "W2_60_IOWaitPulse", file: "runtime/W2_60_IOWaitPulse.grl", source: "", coverage_level: "Runtime", status: "pass", spec_sections: [13], diagnostics: [], artifacts: {} } ] }; const fallbackProgramData = { sourceText: [ "language grl 0.1", "module W2IOWaitPulse", " const speed v = joint(35 %)", " const zone zf = fine", " target home = joint_target { joints: [0 deg, 0 deg, 0 deg, 0 deg, 0 deg, 0 deg] }", " path io_path {", " defaults { speed: v, zone: zf }", " point p0 movej home", " event at p0 distance 5 + 5 mm pulse io.do[20] duration 50 + 50 ms", " }", " proc main()", " io.do[1] = true", " io.ao[1] = 0.75", " io.go[1] = 3", " wait all(io.di[1] == true, io.di[2] == false) timeout 1 s on_timeout alarm \"all timeout\"", " wait any(io.di[3] == true, io.di[4] == true) timeout 1 s", " wait rising(io.di[5]) timeout 1 s", " wait falling(io.di[6]) timeout 1 s", " wait changed(io.di[7]) timeout 1 s", " pulse io.do[2] duration 50 + 50 ms", " run_path io_path", " end", "end" ].join("\n"), trajectory: { sampleCount: 4, duration: 0.24, startJoints: [0, 0, 0, 0, 0, 0], endJoints: [0.12, 0.03, -0.08, 0, 0.02, 0], frames: [ frame(0, 0, "MOVEJ", "home", [0, 0, 0, 0, 0, 0], [0.374, 0, 0.63], { line: 8, column: 14 }), frame(1, 0.08, "MOVEJ", "home", [0.04, 0.01, -0.026667, 0, 0.006667, 0], [0.377333, 0, 0.63], { line: 8, column: 14 }), frame(2, 0.16, "MOVEJ", "home", [0.08, 0.02, -0.053333, 0, 0.013333, 0], [0.380667, 0, 0.63], { line: 8, column: 14 }), frame(3, 0.24, "MOVEJ", "home", [0.12, 0.03, -0.08, 0, 0.02, 0], [0.384, 0, 0.63], { line: 8, column: 14 }) ], diagnostics: [] }, controller: { commands: [], snapshot: { runtime: { pc: 0, trace: [] }, state: { state: "stopped", diagnostics: [] } } }, motionQueue: { items: [ { id: "mq_local", source: { kind: "path", pathId: "io_path", sourceMap: { line: 21, column: 5 } }, request: { pathId: "io_path", segments: [{ id: "p0", motion: "MOVEJ", targetId: "home", sourceMap: { line: 8, column: 14 } }], events: [{ id: "event_0", timing: "at", pointId: "p0", kind: "pulse", sourceMap: { line: 9, column: 35 } }] } } ] }, trace: [ traceEntry(0, "io", "IO_WRITE", 12, 0), traceEntry(0, "io", "IO_WRITE", 13, 1), traceEntry(0, "io", "IO_WRITE", 14, 2), traceEntry(0, "wait", "WAIT", 15, 3, { status: "satisfied" }), traceEntry(0, "wait", "WAIT", 16, 4, { status: "satisfied" }), traceEntry(0.1, "wait", "WAIT", 17, 5, { status: "satisfied" }), traceEntry(0.1, "wait", "WAIT", 18, 6, { status: "satisfied" }), traceEntry(0.1, "wait", "WAIT", 19, 7, { status: "satisfied" }), traceEntry(0.1, "io", "PULSE", 20, 8), traceEntry(0.1, "motion", "RUN_PATH", 21, 9, { queued: true }) ], io: { image: { "io.di[1]": false, "io.do[1]": false }, events: [] }, compile: { moduleName: "W2IOWaitPulse", pathCount: 1, operationCount: 0 }, postReport: { filenames: ["W2IOWaitPulse.mod", "W2IOWaitPulse.ls", "W2IOWaitPulse.src"], report: [] }, roundtrip: {}, isFallback: true, releaseId: "local", errors: [] }; const app = { manifest: fallbackManifest, programs: fallbackManifest.programs, selectedProgramId: "W2_60_IOWaitPulse", programCache: new Map([["W2_60_IOWaitPulse", fallbackProgramData]]), command: "ready", mode: "auto", motors: "on", tab: "grl", currentFrameIndex: 0, currentSampleIndex: 0, playbackTime: 0, playbackStartedAt: 0, playbackTimeStart: 0, playbackSampleIndex: 0, samplePeriodSec: 0.05, samplePeriodMs: 50, di1: false, do1: false, wait: "none", report: "W2-REPORT working2-abb120-spec", localTrace: ["W2-JOB-latest ready"], playbackTimer: 0, programLoads: new Map(), assetTextCache: new Map(), assetRefreshes: new Set(), parseStream: { active: false, timer: 0, programId: "", cursorLine: 0, totalLines: 0, linesPerTick: 1, startedAt: 0, completedProgramId: "", lastSummary: "idle" }, storage: { mode: "checking", hits: 0, network: 0, writes: 0, errors: 0, last: "checking browser storage" } }; window.__virtualController = app; window.__vcErrors = []; function frame(index, time, motion, targetId, joints, position, sourceMap) { return { index, time, motion, targetId, joints, tcp: { position, quaternion: [0, 0.707107, 0, 0.707107] }, sourceMap, diagnostics: [] }; } function traceEntry(time, kind, instructionKind, line, pc, data) { return { time, kind, procedure: "main", pc, instructionKind, source: { kind: instructionKind, id: `main:${instructionKind}:${line}:5`, sourceMap: { line, column: 5 }, procedureId: "main" }, data, id: pc + 1 }; } function selectedProgram() { return app.programs.find((program) => program.program_id === app.selectedProgramId) ?? app.programs[0]; } function cachedProgramData(programId = app.selectedProgramId) { return app.programCache.get(programId) ?? fallbackProgramData; } function selectedData() { const data = cachedProgramData(); if (app.parseStream.active && app.parseStream.programId === app.selectedProgramId) { return projectedProgramData(data, app.parseStream.cursorLine); } return data; } function projectedProgramData(data, cursorLine) { const lines = (data.sourceText || fallbackProgramData.sourceText).split(/\r?\n/); const visibleLineCount = Math.min(Math.max(Math.round(numberOr(cursorLine, 0)), 0), lines.length); const sourceText = [ ...lines.slice(0, visibleLineCount), ...(visibleLineCount < lines.length ? [`... parsing ${visibleLineCount}/${lines.length}`] : []) ].join("\n"); return { ...data, sourceText, trajectory: projectedTrajectory(data.trajectory, visibleLineCount), controller: projectedController(data.controller, visibleLineCount), motionQueue: projectedMotionQueue(data.motionQueue, visibleLineCount), trace: projectedTrace(data.trace, visibleLineCount), postReport: visibleLineCount >= lines.length ? data.postReport : { filenames: data.postReport?.filenames ?? [], report: [] }, isStreaming: true }; } function projectedTrajectory(trajectory, cursorLine) { const sourceFrames = Array.isArray(trajectory?.frames) ? trajectory.frames : []; const visibleFrames = sourceFrames.filter((item) => numberOr(item?.sourceMap?.line, 0) <= cursorLine); const lastTime = numberOr(visibleFrames.at(-1)?.time, 0); return { sampleCount: visibleFrames.length, duration: lastTime, frames: visibleFrames, diagnostics: Array.isArray(trajectory?.diagnostics) ? trajectory.diagnostics.filter((item) => numberOr(item?.sourceMap?.line, 0) <= cursorLine) : [] }; } function projectedController(controller, cursorLine) { const trace = Array.isArray(controller?.snapshot?.runtime?.trace) ? controller.snapshot.runtime.trace.filter((item) => numberOr(item?.source?.sourceMap?.line, 0) <= cursorLine) : []; return { ...controller, snapshot: { ...(controller?.snapshot ?? {}), runtime: { ...(controller?.snapshot?.runtime ?? {}), trace } } }; } function projectedTrace(trace, cursorLine) { return Array.isArray(trace) ? trace.filter((item) => numberOr(item?.source?.sourceMap?.line, 0) <= cursorLine) : []; } function projectedMotionQueue(motionQueue, cursorLine) { const sourceItems = Array.isArray(motionQueue?.items) ? motionQueue.items : []; const items = []; for (const item of sourceItems) { const parentLine = numberOr(item?.source?.sourceMap?.line ?? item?.source?.instruction?.sourceMap?.line, 0); const request = item.request ?? {}; const segments = Array.isArray(request.segments) ? request.segments.filter((segment) => numberOr(segment?.sourceMap?.line, 0) <= cursorLine) : []; const events = Array.isArray(request.events) ? request.events.filter((event) => numberOr(event?.sourceMap?.line, 0) <= cursorLine) : []; if (parentLine > cursorLine && segments.length === 0 && events.length === 0) continue; items.push({ ...item, request: { ...request, segments, events } }); } return { ...motionQueue, items }; } function frames() { const data = selectedData(); return normalizeFrames(data.trajectory, data.isFallback === true && data.isStreaming !== true); } function trajectoryDuration(items = frames()) { const duration = Number(selectedData().trajectory?.duration); const lastTime = Number(items.at(-1)?.time ?? 0); const trace = selectedData().trace; const traceTime = Array.isArray(trace) ? trace.reduce((max, item) => Math.max(max, numberOr(item?.time, 0)), 0) : 0; return Math.max( 0, Number.isFinite(duration) ? duration : 0, Number.isFinite(lastTime) ? lastTime : 0, traceTime ); } function executionSamples() { const items = frames(); const duration = trajectoryDuration(items); const step = app.samplePeriodSec; const times = []; if (duration <= 0) { times.push(0); } else { for (let time = 0; time < duration - 1e-9; time += step) { times.push(Number(time.toFixed(6))); } if (times.length === 0 || Math.abs(times.at(-1) - duration) > 1e-9) { times.push(Number(duration.toFixed(6))); } } return times.map((time, index) => interpolateSample(items, time, index)); } function hasTrajectoryData(data) { const trajectory = data?.trajectory; return Array.isArray(trajectory?.frames) && trajectory.frames.length > 0 || Array.isArray(trajectory?.startJoints) || Array.isArray(trajectory?.endJoints); } function currentFrame() { const samples = executionSamples(); return samples[Math.min(app.currentSampleIndex, Math.max(samples.length - 1, 0))] ?? fallbackProgramData.trajectory.frames[0]; } function currentSamples() { return executionSamples(); } function syncFrameIndexFromTime(time = app.playbackTime) { const items = frames(); if (items.length === 0) { app.currentFrameIndex = 0; return; } let index = 0; for (let i = 0; i < items.length; i += 1) { if (items[i].time <= time + 1e-9) index = i; else break; } app.currentFrameIndex = index; } function setSampleIndex(index) { const samples = currentSamples(); const total = samples.length; app.currentSampleIndex = Math.min(Math.max(Math.round(numberOr(index, 0)), 0), Math.max(total - 1, 0)); app.playbackTime = samples[app.currentSampleIndex]?.time ?? 0; syncFrameIndexFromTime(app.playbackTime); } function setPlaybackTime(time) { const samples = currentSamples(); if (samples.length === 0) { app.currentSampleIndex = 0; app.playbackTime = 0; syncFrameIndexFromTime(0); return; } const duration = samples.at(-1).time; const clamped = Math.min(Math.max(numberOr(time, 0), 0), duration); const sampleIndex = Math.min(Math.round(clamped / app.samplePeriodSec), samples.length - 1); app.currentSampleIndex = sampleIndex; app.playbackTime = samples[sampleIndex]?.time ?? clamped; syncFrameIndexFromTime(app.playbackTime); } function resetExecutionClock() { app.currentSampleIndex = 0; app.currentFrameIndex = 0; app.playbackTime = 0; app.playbackStartedAt = 0; app.playbackTimeStart = 0; app.playbackSampleIndex = 0; } function syncExecutionClockToLatestSample() { const samples = currentSamples(); app.currentSampleIndex = Math.max(samples.length - 1, 0); app.playbackTime = samples.at(-1)?.time ?? 0; syncFrameIndexFromTime(app.playbackTime); } function interpolateSample(items, time, sampleIndex) { if (!Array.isArray(items) || items.length === 0) { return frame(sampleIndex, time, "NO_MOTION", "no planned segment", [0, 0, 0, 0, 0, 0], [0, 0, 0]); } const first = items[0]; if (time <= first.time) return sampleFromFrame(first, time, sampleIndex); const last = items.at(-1); if (time >= last.time) return sampleFromFrame(last, time, sampleIndex); let nextIndex = items.findIndex((item) => item.time >= time); if (nextIndex <= 0) nextIndex = 1; const previous = items[nextIndex - 1]; const next = items[nextIndex]; const span = Math.max(next.time - previous.time, 1e-9); const ratio = Math.min(Math.max((time - previous.time) / span, 0), 1); const source = ratio < 1 ? previous : next; return { index: sampleIndex, time, motion: source.motion || next.motion || previous.motion || "MOVEJ", segmentId: source.segmentId ?? next.segmentId ?? previous.segmentId, targetId: source.targetId ?? next.targetId ?? previous.targetId, joints: interpolateArray(previous.joints, next.joints, ratio), tcp: { position: interpolateArray(previous.tcp.position, next.tcp.position, ratio), quaternion: normalizeQuaternion(interpolateArray(previous.tcp.quaternion, next.tcp.quaternion, ratio)) }, sourceMap: source.sourceMap, trajectoryFrameIndex: source.index, diagnostics: [...(previous.diagnostics ?? []), ...(next.diagnostics ?? [])] }; } function sampleFromFrame(item, time, sampleIndex) { return { ...item, index: sampleIndex, time, joints: [...item.joints], tcp: { position: [...item.tcp.position], quaternion: [...item.tcp.quaternion] }, trajectoryFrameIndex: item.index }; } function interpolateArray(start, end, ratio) { const a = Array.isArray(start) ? start : []; const b = Array.isArray(end) ? end : []; return Array.from({ length: Math.max(a.length, b.length) }, (_item, index) => { const left = numberOr(a[index], 0); const right = numberOr(b[index], left); return left + (right - left) * ratio; }); } function normalizeQuaternion(values) { const q = padNumbers(values, 4, [0, 0.707107, 0, 0.707107]); const length = Math.hypot(q[0], q[1], q[2], q[3]); if (length <= 1e-9) return [0, 0.707107, 0, 0.707107]; return q.map((value) => value / length); } function normalizeFrames(trajectory, allowFallback) { if (Array.isArray(trajectory?.frames) && trajectory.frames.length > 0) { return trajectory.frames.map((item, index) => ({ index: numberOr(item.index, index), time: numberOr(item.time, index * 0.02), motion: item.motion || "MOVEJ", segmentId: item.segmentId, targetId: item.targetId, joints: padNumbers(item.joints, 6), tcp: { position: padNumbers(item.tcp?.position, 3), quaternion: padNumbers(item.tcp?.quaternion, 4, [0, 0.707107, 0, 0.707107]) }, sourceMap: item.sourceMap, diagnostics: Array.isArray(item.diagnostics) ? item.diagnostics : [] })); } if (Array.isArray(trajectory?.startJoints) || Array.isArray(trajectory?.endJoints)) { return [ frame(0, 0, "MOVEJ", "start", padNumbers(trajectory.startJoints, 6), [0.374, 0, 0.63]), frame(1, numberOr(trajectory.duration, 0), "MOVEJ", "end", padNumbers(trajectory.endJoints, 6), [0.384, 0, 0.63]) ]; } if (allowFallback) return fallbackProgramData.trajectory.frames; return [frame(0, 0, "NO_MOTION", "no planned segment", [0, 0, 0, 0, 0, 0], [0, 0, 0])]; } function padNumbers(values, length, fallback) { const source = Array.isArray(values) ? values : fallback ?? []; return Array.from({ length }, (_item, index) => numberOr(source[index], 0)); } function numberOr(value, fallback) { const number = Number(value); return Number.isFinite(number) ? number : fallback; } function sourceMapLine(value) { return numberOr(value?.sourceMap?.line ?? value?.source?.sourceMap?.line, 0); } function formatNumber(value, digits) { const fixed = Number(value).toFixed(digits); return fixed.replace(/\.?0+$/, ""); } function formatArray(values, digits) { return `[${values.map((value) => formatNumber(value, digits)).join(", ")}]`; } function setField(name, value) { const item = fields.get(name); if (item) item.textContent = value; } function resolvePackageUrl(relativePath) { if (!relativePath) return ""; return new URL(`../${relativePath.replace(/^\/+/, "")}`, window.location.href).href; } function packageCacheKey(relativePath) { if (!relativePath) return ""; try { const url = new URL(resolvePackageUrl(relativePath)); return `package:${url.origin}${url.pathname}`; } catch (_error) { return `package:${relativePath}`; } } function cacheFileName(key) { try { return window.btoa(key).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, ""); } catch (_error) { return key.replace(/[^a-z0-9_.-]/gi, "_").slice(0, 180); } } function shortName(value) { const name = String(value || "").split(/[?#]/)[0].split("/").pop() || String(value || "asset"); return name.length > 48 ? `${name.slice(0, 45)}...` : name; } const opfs = { directory: undefined }; async function opfsDirectory() { if (opfs.directory) return opfs.directory; opfs.directory = (async () => { if (!navigator.storage?.getDirectory) { app.storage.mode = "localStorage"; app.storage.last = "OPFS unavailable"; return null; } try { const rootDirectory = await navigator.storage.getDirectory(); const directory = await rootDirectory.getDirectoryHandle("kdl-virtual-controller", { create: true }); app.storage.mode = "OPFS"; app.storage.last = "OPFS ready"; return directory; } catch (error) { app.storage.mode = "localStorage"; app.storage.errors += 1; app.storage.last = `OPFS blocked: ${error instanceof Error ? error.name : "error"}`; return null; } })(); return opfs.directory; } async function readOpfsText(key) { const directory = await opfsDirectory(); if (!directory) return undefined; try { const handle = await directory.getFileHandle(cacheFileName(key)); const file = await handle.getFile(); return await file.text(); } catch (_error) { return undefined; } } async function writeOpfsText(key, text) { const directory = await opfsDirectory(); if (!directory) return false; try { const handle = await directory.getFileHandle(cacheFileName(key), { create: true }); const writable = await handle.createWritable(); await writable.write(text); await writable.close(); return true; } catch (error) { app.storage.errors += 1; app.storage.last = `OPFS write failed: ${error instanceof Error ? error.name : "error"}`; return false; } } function readLocalStorageText(key) { try { const value = window.localStorage?.getItem(`kdl-vc-cache:${key}`); return value ?? undefined; } catch (_error) { return undefined; } } function writeLocalStorageText(key, text) { try { if (text.length > 2_000_000) return false; window.localStorage?.setItem(`kdl-vc-cache:${key}`, text); return true; } catch (_error) { return false; } } async function readCachedTextByKey(key, label) { if (!key) return undefined; if (app.assetTextCache.has(key)) { app.storage.hits += 1; app.storage.last = `memory ${shortName(label ?? key)}`; return app.assetTextCache.get(key); } const opfsText = await readOpfsText(key); if (typeof opfsText === "string") { app.assetTextCache.set(key, opfsText); app.storage.hits += 1; app.storage.last = `OPFS ${shortName(label ?? key)}`; return opfsText; } const localStorageText = readLocalStorageText(key); if (typeof localStorageText === "string") { app.assetTextCache.set(key, localStorageText); app.storage.hits += 1; app.storage.last = `localStorage ${shortName(label ?? key)}`; return localStorageText; } return undefined; } async function writeCachedTextByKey(key, text, label) { if (!key || typeof text !== "string") return; app.assetTextCache.set(key, text); const opfsWritten = await writeOpfsText(key, text); const localStorageWritten = writeLocalStorageText(key, text); if (opfsWritten || localStorageWritten) { app.storage.writes += 1; app.storage.last = `${opfsWritten ? "OPFS" : "localStorage"} saved ${shortName(label ?? key)}`; } } async function fetchTextFromNetwork(relativePath, options = {}) { if (!relativePath) return undefined; const controller = typeof AbortController === "function" ? new AbortController() : undefined; const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 3500; const timeout = controller ? window.setTimeout(() => controller.abort(), timeoutMs) : 0; try { const response = await fetch(resolvePackageUrl(relativePath), { cache: "no-store", signal: controller?.signal }); if (!response.ok) throw new Error(`${response.status} ${response.statusText}`); const text = await response.text(); app.storage.network += 1; app.storage.last = `network ${shortName(relativePath)}`; void writeCachedTextByKey(packageCacheKey(relativePath), text, relativePath); return text; } catch (error) { app.storage.errors += 1; app.storage.last = `network ${shortName(relativePath)} ${error instanceof Error ? error.name : "error"}`; return undefined; } finally { if (timeout) window.clearTimeout(timeout); } } function refreshPackageAsset(relativePath, options = {}) { const key = packageCacheKey(relativePath); if (!key || app.assetRefreshes.has(key)) return; app.assetRefreshes.add(key); void fetchTextFromNetwork(relativePath, options).finally(() => { app.assetRefreshes.delete(key); }); } async function optionalText(relativePath, options = {}) { if (!relativePath) return ""; let triedNetwork = false; if (options.networkFirst) { triedNetwork = true; const text = await fetchTextFromNetwork(relativePath, options); if (typeof text === "string") return text; } const key = packageCacheKey(relativePath); const cached = await readCachedTextByKey(key, relativePath); if (typeof cached === "string") { if (!options.networkFirst && options.refresh !== false) refreshPackageAsset(relativePath, options); return cached; } if (triedNetwork) return ""; const text = await fetchTextFromNetwork(relativePath, options); return text ?? ""; } async function optionalJson(relativePath, options = {}) { const text = await optionalText(relativePath, options); if (!text) return undefined; try { return JSON.parse(text); } catch (error) { app.storage.errors += 1; app.storage.last = `JSON ${shortName(relativePath)} ${error instanceof Error ? error.name : "error"}`; return undefined; } } function applyManifest(manifest) { app.manifest = manifest; if (manifest.release_id !== "local") { for (const [programId, data] of app.programCache) { if (data.releaseId && data.releaseId !== manifest.release_id && data.releaseId !== "local") { app.programCache.delete(programId); } } } app.programs = Array.isArray(manifest.programs) && manifest.programs.length > 0 ? manifest.programs : fallbackManifest.programs; app.selectedProgramId = chooseDefaultProgram(app.selectedProgramId); app.report = manifest.report_html ? `${manifest.job_id} report` : app.report; root?.setAttribute("data-robot", manifest.robot_id ?? "abb_irb120_3_58"); root?.setAttribute("data-suite", manifest.suite_id ?? "working2-abb120-spec"); root?.setAttribute("data-job", manifest.job_id ?? "W2-JOB-latest"); populateProgramSelect(); } function chooseDefaultProgram(current) { const ids = app.programs.map((program) => program.program_id); if (current && ids.includes(current)) return current; if (ids.includes("W2_60_IOWaitPulse")) return "W2_60_IOWaitPulse"; return ids[0] ?? "W2_60_IOWaitPulse"; } function populateProgramSelect() { if (!programSelect) return; programSelect.textContent = ""; for (const program of app.programs) { const option = document.createElement("option"); option.value = program.program_id; option.textContent = `${program.program_id} (${program.coverage_level}/${program.status})`; programSelect.append(option); } programSelect.value = app.selectedProgramId; } async function loadManifest() { const manifest = await optionalJson("demo-manifest.json", { networkFirst: true, timeoutMs: 2500, refresh: false }); if (!manifest) return; applyManifest(manifest); void ensureProgramLoaded(app.selectedProgramId).then((data) => { if (!data.isFallback) { if (!["ready", "loading"].includes(app.command)) return; startParseStream(app.selectedProgramId, "manifest"); } }); resetExecutionClock(); render(); } async function ensureProgramLoaded(programId) { const cachedData = app.programCache.get(programId); if (cachedData && cachedData.releaseId === app.manifest.release_id && (cachedData.isFallback || hasTrajectoryData(cachedData))) { return cachedData; } if (app.programLoads.has(programId)) return app.programLoads.get(programId); const load = loadProgramData(programId).finally(() => { app.programLoads.delete(programId); }); app.programLoads.set(programId, load); return load; } async function loadProgramData(programId) { const program = app.programs.find((item) => item.program_id === programId); if (!program) return fallbackProgramData; const artifacts = program.artifacts ?? {}; const [ sourceText, trajectory, controller, motionQueue, trace, io, compile, postReport, roundtrip ] = await Promise.all([ optionalText(program.source), optionalJson(artifacts.trajectory), optionalJson(artifacts.controller), optionalJson(artifacts.motion_queue), optionalJson(artifacts.trace), optionalJson(artifacts.io), optionalJson(artifacts.compile), optionalJson(artifacts.post_report), optionalJson(artifacts.roundtrip) ]); const data = { sourceText: sourceText || fallbackProgramData.sourceText, trajectory: trajectory ?? {}, controller: controller ?? {}, motionQueue: motionQueue ?? {}, trace: Array.isArray(trace) ? trace : [], io: io ?? {}, compile: compile ?? {}, postReport: postReport ?? {}, roundtrip: roundtrip ?? {}, isFallback: false, releaseId: app.manifest.release_id ?? "local", errors: [] }; if (hasTrajectoryData(data) || !app.programCache.has(programId)) { app.programCache.set(programId, data); } return data; } function sourceLineCount(data) { return (data.sourceText || fallbackProgramData.sourceText).split(/\r?\n/).length; } function sourceFrameCount(data) { return Array.isArray(data?.trajectory?.frames) ? data.trajectory.frames.length : 0; } function visibleSourceFrameCount() { const data = selectedData(); return Array.isArray(data?.trajectory?.frames) ? data.trajectory.frames.length : 0; } function stopParseStream() { if (app.parseStream.timer) { window.clearInterval(app.parseStream.timer); app.parseStream.timer = 0; } app.parseStream.active = false; } function resetParseStreamSummary(message = "idle") { stopParseStream(); app.parseStream.programId = ""; app.parseStream.cursorLine = 0; app.parseStream.totalLines = 0; app.parseStream.linesPerTick = 1; app.parseStream.startedAt = 0; app.parseStream.lastSummary = message; } function startParseStream(programId, reason) { const data = cachedProgramData(programId); const totalLines = sourceLineCount(data); stopParseStream(); resetExecutionClock(); app.command = "parsing"; app.parseStream.active = true; app.parseStream.programId = programId; app.parseStream.cursorLine = 0; app.parseStream.totalLines = totalLines; app.parseStream.linesPerTick = Math.max(1, Math.ceil(totalLines / 24)); app.parseStream.startedAt = performance.now(); app.parseStream.completedProgramId = ""; app.parseStream.lastSummary = `parsing 0/${totalLines} lines`; pushTrace(`${programId} parsing ${reason} from ${storageModeLabel()}`); render(); app.parseStream.timer = window.setInterval(advanceParseStream, app.samplePeriodMs); advanceParseStream(); } function advanceParseStream() { if (!app.parseStream.active) return; const data = cachedProgramData(app.parseStream.programId); app.parseStream.totalLines = sourceLineCount(data); app.parseStream.cursorLine = Math.min( app.parseStream.totalLines, app.parseStream.cursorLine + app.parseStream.linesPerTick ); syncExecutionClockToLatestSample(); const parsedFrames = visibleSourceFrameCount(); const totalFrames = sourceFrameCount(data); app.parseStream.lastSummary = [ `parsing ${app.parseStream.cursorLine}/${app.parseStream.totalLines} lines`, `frames ${Math.min(parsedFrames, totalFrames)}/${totalFrames}`, `t=${formatNumber(app.playbackTime, 3)} s` ].join(", "); if (app.parseStream.cursorLine >= app.parseStream.totalLines) { finishParseStream("ready"); return; } render(); } function finishParseStream(nextCommand = "ready") { const programId = app.parseStream.programId || app.selectedProgramId; const data = cachedProgramData(programId); stopParseStream(); app.parseStream.cursorLine = sourceLineCount(data); app.parseStream.totalLines = app.parseStream.cursorLine; app.parseStream.completedProgramId = programId; app.parseStream.lastSummary = `parsed ${app.parseStream.totalLines} lines, ${sourceFrameCount(data)} frames`; app.command = nextCommand; resetExecutionClock(); pushTrace(`${programId} parsed (${sourceFrameCount(data)} frames, ${executionSamples().length} samples @50ms)`); render(); } function finishParseStreamForCommand() { if (!app.parseStream.active) return; finishParseStream("ready"); } function parserText() { return app.parseStream.active ? app.parseStream.lastSummary : app.parseStream.lastSummary || "idle"; } function parserProgressValue() { if (app.parseStream.totalLines <= 0) return app.parseStream.active ? 0 : 1; return Math.min(Math.max(app.parseStream.cursorLine / app.parseStream.totalLines, 0), 1); } async function selectProgram(programId) { stopPlayback(); stopParseStream(); app.selectedProgramId = programId; app.command = "loading"; resetExecutionClock(); app.di1 = false; app.do1 = false; app.wait = "none"; pushTrace(`${programId} loading from ${storageModeLabel()}`); render(); try { await ensureProgramLoaded(programId); if (app.selectedProgramId !== programId) return; startParseStream(programId, "selection"); } catch (error) { if (app.selectedProgramId === programId) reportRuntimeError(`${programId} select`, error); } } function commandTrace() { const trace = selectedData().trace; if (Array.isArray(trace) && trace.length > 0) return trace; return []; } function diagnosticText() { const data = selectedData(); const diagnostics = [ ...((data.controller?.snapshot?.state?.diagnostics) ?? []), ...((data.trajectory?.diagnostics) ?? []), ...((data.postReport?.report) ?? []) ]; const latest = diagnostics.at(-1); if (!latest) { const program = selectedProgram(); return `${program?.status ?? "ready"} / ${program?.coverage_level ?? "Runtime"}`; } return `${latest.code ?? latest.severity}: ${latest.message ?? ""}`.trim(); } function queueItems() { const items = selectedData().motionQueue?.items; return Array.isArray(items) ? items : []; } function queueText() { const items = queueItems(); if (items.length === 0) return "empty"; return items.map((item) => { const pathId = item.request?.pathId ?? item.source?.pathId ?? item.id; const motions = Array.isArray(item.request?.segments) ? item.request.segments.map((segment) => segment.motion).join("/") : item.kind ?? "motion"; return `${pathId}:${motions}`; }).join(" -> "); } function ioText() { const image = selectedData().io?.image ?? {}; const do1 = app.do1 || image["io.do[1]"] === true; return `do[1]=${do1}, di[1]=${app.di1}, io=${Object.keys(image).length}`; } function storageModeLabel() { const mode = app.storage.mode === "checking" ? "cache" : app.storage.mode; return `${mode} h${app.storage.hits}/n${app.storage.network}/w${app.storage.writes}/e${app.storage.errors}`; } function storageText() { return `${storageModeLabel()} ${app.storage.last}`; } function reportRuntimeError(context, error) { const message = `${context}: ${error instanceof Error ? error.message : String(error)}`; window.__vcErrors.push(message); app.storage.errors += 1; app.storage.last = message; pushTrace(message); render(); } function sourceTextForTab(tab) { const program = selectedProgram(); const data = selectedData(); if (tab === "grl") return data.sourceText || fallbackProgramData.sourceText; const moduleName = data.compile?.moduleName ?? (program?.program_id ?? "W2Program").replace(/^W2_/, "W2"); const filenames = Array.isArray(data.postReport?.filenames) ? data.postReport.filenames : []; if (tab === "abb") { return [ `MODULE ${moduleName}`, ` ! ${filenames.find((name) => name.endsWith(".mod")) ?? `${moduleName}.mod`}`, " PROC main()", " ! See post-report.json for generated brand diagnostics.", " ENDPROC", "ENDMODULE" ].join("\n"); } if (tab === "fanuc") { return [ `/PROG ${moduleName.toUpperCase()}`, `/ATTR`, `COMMENT = "${filenames.find((name) => name.endsWith(".ls")) ?? `${moduleName}.ls`}";`, "/MN", " 1: ! See post-report.json ;", "/END" ].join("\n"); } return [ `DEF ${moduleName}()`, ` ; ${filenames.find((name) => name.endsWith(".src")) ?? `${moduleName}.src`}`, " ; See post-report.json", "END" ].join("\n"); } function updateObjectTree() { if (!objectTree) return; const program = selectedProgram(); const items = queueItems(); const pathNames = [...new Set(items.map((item) => item.request?.pathId ?? item.source?.pathId).filter(Boolean))]; const entries = [ `Robot: ${app.manifest.robot_id ?? "abb_irb120_3_58"}`, "Tool: tool0", "Frame: world", `Path: ${pathNames.join(", ") || "pick_place"}`, `Program: ${program?.program_id ?? "W2_60_IOWaitPulse"}`, `Coverage: ${program?.coverage_level ?? "Runtime"}`, `Status: ${program?.status ?? "pass"}`, `Frames: ${frames().length}`, `Suite: ${app.manifest.suite_id ?? "working2-abb120-spec"}`, `Job: ${app.manifest.job_id ?? "W2-JOB-latest"}`, "Reports" ]; objectTree.textContent = ""; for (const entry of entries) { const item = document.createElement("li"); item.textContent = entry; objectTree.append(item); } } function updateFrameSlider() { if (!frameSlider) return; const samples = currentSamples(); frameSlider.max = String(Math.max(samples.length - 1, 0)); frameSlider.value = String(Math.min(app.currentSampleIndex, Math.max(samples.length - 1, 0))); } function updatePathMarker() { if (!pathCurrent) return; const duration = trajectoryDuration(); const ratio = duration > 0 ? Math.min(Math.max(app.playbackTime / duration, 0), 1) : 0; const x = 70 + ratio * 340; const y = 170 - Math.sin(ratio * Math.PI) * 80 - ratio * 20; pathCurrent.setAttribute("cx", String(formatNumber(x, 2))); pathCurrent.setAttribute("cy", String(formatNumber(y, 2))); } function sourceLines() { return (selectedData().sourceText || fallbackProgramData.sourceText).split(/\r?\n/); } function sourceLineText(line) { const index = Math.round(numberOr(line, 0)) - 1; return sourceLines()[index]?.trim() ?? ""; } function mainProcedureRange() { const lines = sourceLines(); const startIndex = lines.findIndex((line) => /^\s*proc\s+main\s*\(/i.test(line)); if (startIndex < 0) return { start: 0, end: 0 }; const startIndent = leadingSpaces(lines[startIndex]); for (let index = startIndex + 1; index < lines.length; index += 1) { if (/^\s*end\b/i.test(lines[index]) && leadingSpaces(lines[index]) <= startIndent) { return { start: startIndex + 1, end: index + 1 }; } } return { start: startIndex + 1, end: lines.length }; } function leadingSpaces(text) { return String(text ?? "").match(/^\s*/)?.[0].length ?? 0; } function setupStepsBefore(firstLine) { const range = mainProcedureRange(); if (range.start <= 0) return []; const steps = []; const lines = sourceLines(); const limit = Math.max(numberOr(firstLine, range.end + 1), range.start + 1); for (let line = range.start + 1; line < Math.min(limit, range.end); line += 1) { const text = lines[line - 1]?.trim() ?? ""; if (!/^(set_tool|set_frame)\b/i.test(text)) continue; steps.push({ kind: "setup", label: "Setup", line, text, timeStart: 0, timeEnd: 0, sampleStart: 0, sampleEnd: 0, detail: text.replace(/\s+/g, " ") }); } return steps; } function traceInstructionSteps() { const steps = []; let lastKey = ""; for (const entry of commandTrace()) { const line = numberOr(entry?.source?.sourceMap?.line, 0); if (line <= 0 || entry?.kind === "load") continue; const kind = entry.instructionKind ?? entry.source?.kind ?? entry.kind ?? "instruction"; const key = `${line}:${kind}`; const time = numberOr(entry.time, 0); if (key === lastKey && steps.length > 0) { const previous = steps.at(-1); previous.timeEnd = Math.max(numberOr(previous.timeEnd, time), time); previous.detail = mergeDetails(previous.detail, traceDetail(entry)); continue; } const text = sourceLineText(line); steps.push({ kind: String(kind).toLowerCase(), label: stepLabel(kind, text), line, text, timeStart: time, timeEnd: time, pc: numberOr(entry.pc, 0), traceKind: entry.kind, detail: traceDetail(entry) }); lastKey = key; } return steps; } function traceDetail(entry) { const data = entry?.data ?? {}; if (Array.isArray(data.result) && data.result.length > 0) { return data.result.map((item) => { const target = item.target?.raw ?? item.target?.domain ?? item.kind ?? "io"; return `${target}=${String(item.value)}`; }).join(", "); } if (data.status) return `status=${data.status}`; if (data.queued === true) return "queued"; if (data.alarmId) return `alarm=${data.alarmId}`; if (entry?.diagnostic?.code) return entry.diagnostic.code; if (entry?.message) return entry.message; return ""; } function mergeDetails(left, right) { if (!right) return left ?? ""; if (!left) return right; return left.includes(right) ? left : `${left}; ${right}`; } function stepLabel(kind, text) { const source = String(text ?? "").trim().toLowerCase(); const normalized = String(kind ?? "").toUpperCase(); if (/^set_(tool|frame)\b/.test(source)) return "Setup"; if (/^run_operation\b/.test(source) || normalized === "RUN_OPERATION") return "Operation"; if (/^run_path\b/.test(source) || normalized === "RUN_PATH") return "Path"; if (/^point\b/.test(source)) return "Path Point"; if (/^event\b/.test(source)) return "Path Event"; if (/^(movej|movel|movec|moveabsj)\b/.test(source) || /^MOVE/.test(normalized)) return "Motion"; if (/^wait\b/.test(source) || normalized === "WAIT") return "Wait"; if (/^pulse\b/.test(source) || normalized === "PULSE") return "Pulse"; if (/^io\./.test(source) || normalized === "IO_WRITE") return "IO"; if (/^(if|elseif|else|while|for|switch|case|break|continue)\b/.test(source) || normalized.startsWith("EXEC_")) return "Control"; if (/^call\b/.test(source)) return "Call"; if (/^return\b/.test(source)) return "Return"; if (normalized === "ALARM") return "Alarm"; return normalized.replace(/^RAW_/, "").replace(/_/g, " ").toLowerCase(); } function sampleGroups(samples = currentSamples()) { const groups = []; for (const sample of samples) { const line = numberOr(sample?.sourceMap?.line, 0); if (line <= 0) continue; const key = [ line, sample.motion ?? "", sample.targetId ?? sample.segmentId ?? "" ].join(":"); const previous = groups.at(-1); if (previous?.key === key && previous.sampleEnd + 1 === sample.index) { previous.sampleEnd = sample.index; previous.timeEnd = numberOr(sample.time, previous.timeEnd); } else { groups.push({ id: `sample:${groups.length}`, key, line, motion: sample.motion ?? "", target: sample.targetId ?? sample.segmentId ?? "", sampleStart: sample.index, sampleEnd: sample.index, timeStart: numberOr(sample.time, 0), timeEnd: numberOr(sample.time, 0) }); } } return groups; } function applySampleGroup(step, group) { if (!group) return step; step.motion = group.motion || step.motion; step.target = group.target || step.target; step.sampleStart = group.sampleStart; step.sampleEnd = group.sampleEnd; step.timeStart = Math.min(numberOr(step.timeStart, group.timeStart), group.timeStart); step.timeEnd = Math.max(numberOr(step.timeEnd, group.timeEnd), group.timeEnd); return step; } function nearestSampleIndex(time, samples = currentSamples()) { if (samples.length === 0) return 0; let bestIndex = 0; let bestDistance = Infinity; for (const sample of samples) { const distance = Math.abs(numberOr(sample.time, 0) - numberOr(time, 0)); if (distance < bestDistance) { bestDistance = distance; bestIndex = sample.index; } } return bestIndex; } function findUnusedGroup(line, groups, usedGroupIds) { const group = groups.find((item) => item.line === line && !usedGroupIds.has(item.id)) ?? groups.find((item) => item.line === line); if (group) usedGroupIds.add(group.id); return group; } function queueChildrenForStep(step, groups, usedGroupIds) { const children = []; for (const item of queueItems()) { const parentLine = numberOr(item?.source?.sourceMap?.line ?? item?.source?.instruction?.sourceMap?.line, 0); if (parentLine !== step.line) continue; const request = item.request ?? {}; const segments = Array.isArray(request.segments) ? request.segments : []; const events = Array.isArray(request.events) ? request.events : []; const eventsByPoint = new Map(); const looseEvents = []; for (const event of events) { const pointId = event.pointId ?? ""; if (!pointId) { looseEvents.push(event); continue; } if (!eventsByPoint.has(pointId)) eventsByPoint.set(pointId, []); eventsByPoint.get(pointId).push(event); } for (const segment of segments) { const beforeEvents = (eventsByPoint.get(segment.id) ?? []).filter((event) => event.timing === "before"); for (const event of beforeEvents) children.push(queueEventStep(event, step)); children.push(queueSegmentStep(segment, step, groups, usedGroupIds)); const afterEvents = (eventsByPoint.get(segment.id) ?? []).filter((event) => event.timing !== "before"); for (const event of afterEvents) children.push(queueEventStep(event, step, segment, groups)); } for (const event of looseEvents) children.push(queueEventStep(event, step)); } return children.filter((child) => child.line > 0 && child.line !== step.line); } function queueSegmentStep(segment, parent, groups, usedGroupIds) { const line = numberOr(segment?.sourceMap?.line, 0); const text = sourceLineText(line); const step = { kind: "path-point", label: stepLabel(segment?.motion ?? "MOVE", text), line, text, timeStart: numberOr(parent.timeStart, 0), timeEnd: numberOr(parent.timeEnd, 0), motion: segment?.motion ?? "", target: segment?.targetId ?? segment?.target?.id ?? segment?.id ?? "", detail: `${segment?.motion ?? "MOVE"} ${segment?.targetId ?? segment?.target?.id ?? segment?.id ?? ""}`.trim() }; return applySampleGroup(step, findUnusedGroup(line, groups, usedGroupIds)); } function queueEventStep(event, parent, segment, groups = []) { const line = numberOr(event?.sourceMap?.line, 0); const text = sourceLineText(line); const segmentGroup = segment ? groups.find((group) => group.line === numberOr(segment?.sourceMap?.line, 0)) : undefined; const time = event?.timing === "before" ? numberOr(segmentGroup?.timeStart, numberOr(parent.timeStart, 0)) : numberOr(segmentGroup?.timeEnd, numberOr(parent.timeEnd, numberOr(parent.timeStart, 0))); const sample = event?.timing === "before" ? numberOr(segmentGroup?.sampleStart, parent.sampleStart) : numberOr(segmentGroup?.sampleEnd, parent.sampleEnd); return { kind: "path-event", label: stepLabel("EVENT", text), line, text, timeStart: time, timeEnd: time, sampleStart: Number.isFinite(sample) ? sample : undefined, sampleEnd: Number.isFinite(sample) ? sample : undefined, detail: `${event?.timing ?? "event"} ${event?.pointId ?? ""} ${event?.kind ?? ""}`.trim() }; } function buildProgramSteps(samples = currentSamples()) { const groups = sampleGroups(samples); const usedGroupIds = new Set(); const traceSteps = traceInstructionSteps(); const firstTraceLine = traceSteps.find((step) => step.line > 0)?.line ?? 0; const steps = setupStepsBefore(firstTraceLine); const seenSourceRows = new Set(steps.map((step) => `${step.line}:${step.kind}`)); for (const traceStep of traceSteps) { const step = { ...traceStep }; applySampleGroup(step, findUnusedGroup(step.line, groups, usedGroupIds)); if (step.sampleStart === undefined && step.timeStart !== undefined) { const sample = nearestSampleIndex(step.timeStart, samples); step.sampleStart = sample; step.sampleEnd = sample; } const childSteps = queueChildrenForStep(step, groups, usedGroupIds); const childGroups = childSteps.filter((child) => child.sampleStart !== undefined); if (childGroups.length > 0 && !step.motion) { step.sampleStart = Math.min(...childGroups.map((child) => child.sampleStart)); step.sampleEnd = Math.max(...childGroups.map((child) => child.sampleEnd)); step.timeStart = Math.min(...childGroups.map((child) => child.timeStart)); step.timeEnd = Math.max(...childGroups.map((child) => child.timeEnd)); } const key = `${step.line}:${step.kind}:${step.pc ?? ""}`; if (!seenSourceRows.has(key)) { steps.push(step); seenSourceRows.add(key); } for (const child of childSteps) { const childKey = `${child.line}:${child.kind}:${child.detail ?? ""}`; if (seenSourceRows.has(childKey)) continue; steps.push(child); seenSourceRows.add(childKey); } } for (const group of groups) { if (usedGroupIds.has(group.id)) continue; const text = sourceLineText(group.line); steps.push({ kind: "motion", label: stepLabel(group.motion, text), line: group.line, text, timeStart: group.timeStart, timeEnd: group.timeEnd, sampleStart: group.sampleStart, sampleEnd: group.sampleEnd, motion: group.motion, target: group.target, detail: `${group.motion} ${group.target}`.trim() }); usedGroupIds.add(group.id); } if (steps.length === 0) { const item = currentFrame(); steps.push({ kind: "sample", label: "Sample", line: numberOr(item?.sourceMap?.line, 0), text: item?.sourceMap ? sourceLineText(item.sourceMap.line) : "sample trajectory", timeStart: app.playbackTime, timeEnd: app.playbackTime, sampleStart: app.currentSampleIndex, sampleEnd: app.currentSampleIndex, motion: item?.motion ?? "", target: item?.targetId ?? item?.segmentId ?? "" }); } return steps.map((step, index) => ({ ...step, index: index + 1, id: `step:${index + 1}:${step.line}:${step.kind}` })); } function activeProgramStep(steps) { if (steps.length === 0) return undefined; const item = currentFrame(); const line = numberOr(item?.sourceMap?.line, 0); if (line > 0) { const matchingMotion = steps.find((step) => { return step.line === line && step.sampleStart !== undefined && step.sampleEnd !== undefined && step.sampleStart <= app.currentSampleIndex && step.sampleEnd >= app.currentSampleIndex; }); if (matchingMotion) return matchingMotion; } const matchingSample = steps.find((step) => { return step.sampleStart !== undefined && step.sampleEnd !== undefined && step.sampleStart <= app.currentSampleIndex && step.sampleEnd >= app.currentSampleIndex; }); if (matchingSample) return matchingSample; const elapsedSteps = steps.filter((step) => numberOr(step.timeStart, 0) <= app.playbackTime + 1e-9); return elapsedSteps.at(-1) ?? steps[0]; } function stepState(step, active) { if (!active) return "pending"; if (step.id === active.id) return "active"; if (step.sampleEnd !== undefined && active.sampleStart !== undefined && step.sampleEnd < active.sampleStart) { return "complete"; } if (step.index < active.index && numberOr(step.timeEnd, 0) <= app.playbackTime + 1e-9) { return "complete"; } return "pending"; } function renderStepTable(steps, active) { if (!stepTable) return; stepTable.textContent = ""; for (const step of steps) { const row = document.createElement("tr"); row.dataset.stepState = stepState(step, active); if (step.id === active?.id) row.setAttribute("aria-current", "true"); const cells = [ step.index, stepTimeText(step), step.line > 0 ? step.line : "", step.label, step.text, stepMotionText(step), stepSampleText(step) ]; for (const cell of cells) { const td = document.createElement("td"); td.textContent = String(cell); row.append(td); } stepTable.append(row); } } function stepTimeText(step) { const start = numberOr(step.timeStart, 0); const end = numberOr(step.timeEnd, start); if (Math.abs(end - start) > 1e-9) { return `${formatNumber(start, 3)}-${formatNumber(end, 3)} s`; } return `${formatNumber(start, 3)} s`; } function stepMotionText(step) { if (step.motion || step.target) { return `${step.motion ?? ""}${step.target ? ` / ${step.target}` : ""}`.trim(); } return step.detail ?? ""; } function stepSampleText(step) { if (step.sampleStart === undefined || step.sampleEnd === undefined) return "-"; const start = numberOr(step.sampleStart, 0) + 1; const end = numberOr(step.sampleEnd, step.sampleStart) + 1; return start === end ? `${start} @50ms` : `${start}-${end} @50ms`; } function renderFrameTable() { if (!frameTable) return; const items = frames(); frameTable.textContent = ""; for (const item of items) { const row = document.createElement("tr"); if (item.index === app.currentFrameIndex) row.setAttribute("aria-current", "true"); const cells = [ item.index, formatNumber(item.time, 4), item.motion, item.targetId ?? item.segmentId ?? "", formatArray(item.tcp.position, 6), formatArray(item.tcp.quaternion, 6), ...item.joints.map((joint) => formatNumber(joint, 6)) ]; for (const cell of cells) { const td = document.createElement("td"); td.textContent = String(cell); row.append(td); } frameTable.append(row); } } function render() { const program = selectedProgram(); const data = selectedData(); const item = currentFrame(); const trace = commandTrace(); const traceLast = trace.at(-1); const totalFrames = frames().length; const samples = currentSamples(); const programSteps = buildProgramSteps(samples); const activeStep = activeProgramStep(programSteps); const totalSamples = samples.length; const duration = samples.at(-1)?.time ?? trajectoryDuration(); const sourceMap = item.sourceMap ? `:${item.sourceMap.line ?? 0}` : ""; root?.setAttribute("data-last-command", app.command); root?.setAttribute("data-running", String(app.command === "running")); if (programSelect && programSelect.value !== app.selectedProgramId) { programSelect.value = app.selectedProgramId; } setField("job", app.manifest.job_id ?? "W2-JOB-latest"); setField("state", app.command); setField("program", program?.program_id ?? "W2_60_IOWaitPulse"); setField("mode", app.mode); setField("motors", app.motors); setField("pc", `sample:${app.currentSampleIndex}${sourceMap}`); setField( "runtime", `${formatNumber(app.playbackTime, 3)} / ${formatNumber(duration, 3)} s @ ${app.samplePeriodMs} ms` ); setField("joints", formatArray(item.joints, 6)); setField("tcp", `pos ${formatArray(item.tcp.position, 6)} q ${formatArray(item.tcp.quaternion, 6)}`); setField("log", app.localTrace.at(-1) ?? traceLast?.message ?? "ready"); setField("diagnostics", diagnosticText()); setField("io", ioText()); setField("wait", app.wait); setField("queue", queueText()); setField("trace", `${trace.length + app.localTrace.length} events`); setField("reports", app.report); setField("parser", parserText()); setField("storage", storageText()); setField("editor", sourceTextForTab(app.tab)); setField("frame", totalSamples > 0 ? `${app.currentSampleIndex + 1} / ${totalSamples}` : "0 / 0"); setField( "step-status", activeStep ? `${activeStep.index}/${programSteps.length} ${activeStep.label} line ${activeStep.line || "-"} t=${formatNumber(app.playbackTime, 3)} s` : "0 / 0" ); const sampleCount = Number.isFinite(Number(data.trajectory?.sampleCount)) ? Number(data.trajectory.sampleCount) : (data.isFallback ? totalFrames : 0); setField( "frame-time", `t=${formatNumber(app.playbackTime, 3)} / ${formatNumber(duration, 3)} s, step=${formatNumber(app.samplePeriodSec, 2)} s, source ${app.currentFrameIndex + 1}/${totalFrames} (${sampleCount} trajectory frames)` ); for (const button of tabButtons) { button.setAttribute("aria-pressed", String(button.dataset.tab === app.tab)); } if (parserProgress) { parserProgress.value = parserProgressValue(); } updateObjectTree(); updateFrameSlider(); updatePathMarker(); renderStepTable(programSteps, activeStep); renderFrameTable(); } function pushTrace(message) { app.localTrace.push(message); if (app.localTrace.length > 100) app.localTrace.shift(); } function advanceSample(stepSize) { setSampleIndex(app.currentSampleIndex + stepSize); } function startPlayback() { finishParseStreamForCommand(); stopPlayback(); app.playbackStartedAt = performance.now(); app.playbackTimeStart = app.playbackTime; app.playbackSampleIndex = app.currentSampleIndex; app.playbackTimer = window.setInterval(() => { if (app.command !== "running") { stopPlayback(); return; } const elapsed = (performance.now() - app.playbackStartedAt) / 1000; const before = app.currentSampleIndex; setPlaybackTime(app.playbackTimeStart + elapsed); if (before !== app.currentSampleIndex) { app.playbackSampleIndex = app.currentSampleIndex; } if (app.currentSampleIndex >= Math.max(currentSamples().length - 1, 0)) { stopPlayback(); app.command = "ready"; pushTrace(`${app.selectedProgramId} completed t=${formatNumber(app.playbackTime, 3)}s`); } render(); }, app.samplePeriodMs); } function stopPlayback() { if (app.playbackTimer) { window.clearInterval(app.playbackTimer); app.playbackTimer = 0; } } async function handleCommand(command) { root?.setAttribute("data-last-command", command); if (command === "load") { const programId = app.selectedProgramId; stopPlayback(); stopParseStream(); app.command = "load"; resetExecutionClock(); app.di1 = false; app.do1 = false; app.wait = "none"; pushTrace(`${programId} loading from ${storageModeLabel()}`); render(); void ensureProgramLoaded(programId).then(() => { if (app.selectedProgramId !== programId) return; if (!["load", "loading"].includes(app.command)) return; startParseStream(programId, "load"); }).catch((error) => reportRuntimeError(`${programId} load`, error)); return; } else if (command === "run") { const programId = app.selectedProgramId; finishParseStreamForCommand(); app.command = "running"; app.do1 = true; if (app.currentSampleIndex >= Math.max(currentSamples().length - 1, 0)) { resetExecutionClock(); } advanceSample(1); app.wait = app.di1 ? "satisfied" : "waiting di[1]"; pushTrace(`${programId} running sample ${app.currentSampleIndex + 1} t=${formatNumber(app.playbackTime, 3)}s`); startPlayback(); render(); void ensureProgramLoaded(programId).then(() => { if (app.selectedProgramId === programId && app.command === "running") render(); }).catch((error) => reportRuntimeError(`${programId} run load`, error)); return; } else if (command === "pause") { stopPlayback(); app.command = "paused"; pushTrace(`${app.selectedProgramId} paused`); } else if (command === "step") { stopPlayback(); finishParseStreamForCommand(); app.command = "paused"; advanceSample(1); pushTrace(`${app.selectedProgramId} stepped sample ${app.currentSampleIndex + 1} t=${formatNumber(app.playbackTime, 3)}s`); } else if (command === "stop") { stopPlayback(); stopParseStream(); app.command = "stopped"; app.wait = "none"; pushTrace(`${app.selectedProgramId} stopped`); } else if (command === "reset") { stopPlayback(); stopParseStream(); app.command = "ready"; resetExecutionClock(); app.di1 = false; app.do1 = false; app.wait = "none"; pushTrace("controller reset"); } else if (command === "export") { app.report = `${app.manifest.job_id ?? "W2-JOB-latest"} report exported`; pushTrace("delivery package exported"); } render(); } for (const button of document.querySelectorAll("[data-command]")) { button.addEventListener("click", () => { const command = button.dataset.command ?? "ready"; void handleCommand(command).catch((error) => reportRuntimeError(command, error)); }); } for (const button of tabButtons) { button.addEventListener("click", () => { app.tab = button.dataset.tab ?? "grl"; pushTrace(`${app.tab.toUpperCase()} source selected`); render(); }); } programSelect?.addEventListener("change", () => { void selectProgram(programSelect.value).catch((error) => reportRuntimeError("program select", error)); }); frameSlider?.addEventListener("input", () => { stopPlayback(); setSampleIndex(numberOr(frameSlider.value, 0)); app.command = app.command === "running" ? "paused" : app.command; pushTrace(`${app.selectedProgramId} sample ${app.currentSampleIndex + 1} t=${formatNumber(app.playbackTime, 3)}s`); render(); }); document.querySelector("[data-io='di1']")?.addEventListener("click", () => { app.di1 = !app.di1; app.wait = app.di1 ? "satisfied" : "waiting di[1]"; pushTrace(`di[1]=${app.di1}`); render(); }); applyManifest(fallbackManifest); render(); void loadManifest(); })();