import { Union, Record } from "./fable_modules/fable-library-js.4.24.0/Types.js"; import { union_type, obj_type, class_type, record_type, bool_type, string_type } from "./fable_modules/fable-library-js.4.24.0/Reflection.js"; import { comparePrimitives, defaultOf, equals, createAtom } from "./fable_modules/fable-library-js.4.24.0/Util.js"; import { Replay_frames } from "./SomhairlesDream.Shared/Replay.js"; import { sortBy, map, equalsWith, tryFind, iterateIndexed, item } from "./fable_modules/fable-library-js.4.24.0/Array.js"; import { join, isNullOrWhiteSpace, printf, toText } from "./fable_modules/fable-library-js.4.24.0/String.js"; import { MeshData_triangleIndices, MeshData_positionData } from "./SomhairlesDream.Shared/Domain.js"; import { min, max } from "./fable_modules/fable-library-js.4.24.0/Double.js"; import { some, defaultArg } from "./fable_modules/fable-library-js.4.24.0/Option.js"; import { awaitPromise, startImmediate } from "./fable_modules/fable-library-js.4.24.0/Async.js"; import { singleton } from "./fable_modules/fable-library-js.4.24.0/AsyncBuilder.js"; export class RunSelection extends Record { constructor(ProjectId, RunId, BaseId, TargetId, Render) { super(); this.ProjectId = ProjectId; this.RunId = RunId; this.BaseId = BaseId; this.TargetId = TargetId; this.Render = Render; } } export function RunSelection_$reflection() { return record_type("SomhairlesDream.Frontend.App.RunSelection", [], RunSelection, () => [["ProjectId", string_type], ["RunId", string_type], ["BaseId", string_type], ["TargetId", string_type], ["Render", bool_type]]); } export class ViewState extends Record { constructor(Renderer, Scene, Camera, Group, Canvas, Controls) { super(); this.Renderer = Renderer; this.Scene = Scene; this.Camera = Camera; this.Group = Group; this.Canvas = Canvas; this.Controls = Controls; } } export function ViewState_$reflection() { return record_type("SomhairlesDream.Frontend.App.ViewState", [], ViewState, () => [["Renderer", string_type], ["Scene", string_type], ["Camera", string_type], ["Group", string_type], ["Canvas", class_type("Browser.Types.HTMLCanvasElement", undefined)], ["Controls", obj_type]]); } export let lastInteractionMs = createAtom(0); export const idleSpinDelayMs = 3500; export class LoadOwner extends Record { constructor(ProjectId, RunId, Path) { super(); this.ProjectId = ProjectId; this.RunId = RunId; this.Path = Path; } } export function LoadOwner_$reflection() { return record_type("SomhairlesDream.Frontend.App.LoadOwner", [], LoadOwner, () => [["ProjectId", string_type], ["RunId", string_type], ["Path", string_type]]); } export class ViewMode extends Union { constructor(tag, fields) { super(); this.tag = tag; this.fields = fields; } cases() { return ["FrameMode", "StepMode"]; } } export function ViewMode_$reflection() { return union_type("SomhairlesDream.Frontend.App.ViewMode", [], ViewMode, () => [[], []]); } export const frames = Replay_frames(); export let viewState = createAtom(undefined); export let selectedFrame = createAtom(item(0, frames)); export let replayTimer = createAtom(undefined); export let replayIndex = createAtom(0); export let eventSource = createAtom(undefined); export let currentProjectId = createAtom(""); export let currentRunId = createAtom(""); export let historicalSteps = createAtom([]); export let pinnedIndex = createAtom(undefined); export let loadGeneration = createAtom(0); export let pendingLoad = createAtom(undefined); export let loadedStep = createAtom(undefined); export let viewMode = createAtom(new ViewMode(0, [])); export let failedLoad = createAtom(undefined); export let displayedNode = createAtom(undefined); export let freezeRotation = createAtom(false); export function element(id) { return document.getElementById(id); } export function setText(id, value) { element(id).textContent = value; } export function setWidth(id, value) { element(id).setAttribute("style", toText(printf("width: %s"))(value)); } export function setStatusClass(status) { const host = element("status-chip"); const array = ["idle", "queued", "running", "succeeded", "failed", "stale"]; array.forEach((name) => { host.classList.remove(`status-${name}`); }); host.classList.add(`status-${status}`); host.setAttribute("data-status", status); } export function statusName(payload) { if (payload.isStale) { return "stale"; } else { const matchValue = payload.status; let matchResult, status; switch (matchValue) { case "complete": { matchResult = 0; break; } case "idle": { matchResult = 1; status = matchValue; break; } case "queued": { matchResult = 1; status = matchValue; break; } case "running": { matchResult = 1; status = matchValue; break; } case "failed": { matchResult = 1; status = matchValue; break; } default: matchResult = 2; } switch (matchResult) { case 0: return "succeeded"; case 1: return status; default: return "unknown"; } } } export function statusLabel(payload) { const matchValue = payload.status; const matchValue_1 = payload.isStale; if (matchValue_1) { return "心跳超时"; } else { switch (matchValue) { case "idle": return "待机"; case "queued": return "排队中"; case "running": return "运行中"; case "complete": return "已完成"; case "failed": return "失败"; default: return "未知状态"; } } } export function addMesh(view, frame) { const geometry = new THREE.BufferGeometry(); const positions = MeshData_positionData(frame.Mesh); const indices = MeshData_triangleIndices(frame.Mesh); geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3)); geometry.setIndex(new THREE.Uint32BufferAttribute(indices, 1)); geometry.computeVertexNormals(); let color; const matchValue = frame.Version | 0; color = ((matchValue === 1) ? "#d7a948" : ((matchValue === 2) ? "#efe4c4" : "#a7c7c0")); const material = new THREE.MeshStandardMaterial({ color: color, roughness: 0.36, metalness: 0.72, }); const mesh = new THREE.Mesh(geometry, material); view.Group.add(mesh); } export function clearGroup(group) { const array = group.children; array.forEach((node) => { node.traverse(function (o) { if (o.geometry && o.geometry.dispose) { o.geometry.dispose(); } var mats = Array.isArray(o.material) ? o.material : (o.material ? [o.material] : []); mats.forEach(function (m) { ['map','normalMap','roughnessMap','metalnessMap','aoMap','emissiveMap'].forEach(function (key) { var t = m[key]; if (t && t.dispose) { t.dispose(); } }); if (m.dispose) { m.dispose(); } }); }); }); group.clear(); } export function invalidateLoads() { loadGeneration(loadGeneration() + 1); pendingLoad(undefined); failedLoad(undefined); } export function renderFrame(frame) { selectedFrame(frame); displayedNode(undefined); invalidateLoads(); viewMode(new ViewMode(0, [])); if (viewState() != null) { const view = viewState(); clearGroup(view.Group); addMesh(view, frame); view.Renderer.render(view.Scene, view.Camera); } } export function frameObject(view, node) { const box = new THREE.Box3(); box.setFromObject(node); if (!box.isEmpty()) { const center = new THREE.Vector3(); const size = new THREE.Vector3(); box.getCenter(center); box.getSize(size); const radius = 0.5 * size.length(); if (radius > 1E-06) { node.position.set(-center.x, -center.y, -center.z); const camera = view.Camera; const aspect = max(1, view.Canvas.clientWidth) / max(1, view.Canvas.clientHeight); camera.aspect = aspect; const fovY = (camera.fov * 3.141592653589793) / 180; const limitingFov = min(fovY, 2 * Math.atan(Math.tan(fovY / 2) * aspect)); const distance = (1.28 * radius) / Math.sin(limitingFov / 2); const norm = Math.sqrt(((0.62 * 0.62) + (0.45 * 0.45)) + (0.7 * 0.7)); camera.position.set((0.62 / norm) * distance, (0.45 / norm) * distance, (0.7 / norm) * distance); camera.near = max(distance * 0.01, distance - (radius * 2.5)); camera.far = (distance + (radius * 4)); camera.lookAt(0, 0, 0); camera.updateProjectionMatrix(); const matchValue_3 = view.Controls; if (equals(matchValue_3, defaultOf())) { } else { const controls = matchValue_3; controls.target.copy({ x: 0, y: 0, z: 0 }); controls.update(); } } } } export function resizeView(view) { const width = max(1, view.Canvas.clientWidth); const height = max(1, view.Canvas.clientHeight); view.Renderer.setSize(width, height, false); view.Camera.aspect = (width / height); view.Camera.updateProjectionMatrix(); if (displayedNode() == null) { view.Renderer.render(view.Scene, view.Camera); } else { frameObject(view, displayedNode()); view.Renderer.render(view.Scene, view.Camera); } } export function initializeView() { const canvas = document.getElementById("viewport-canvas"); const width = max(1, canvas.clientWidth); const height = max(1, canvas.clientHeight); const renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true, alpha: true, }); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(width, height, false); const scene = new THREE.Scene(); scene.background = (new THREE.Color("#11171a")); const camera = new THREE.PerspectiveCamera(34, (width / height), 0.1, 100); camera.position.set(6.4, 4.6, 7.2); camera.lookAt(0, 0, 0); const group = new THREE.Group(); scene.add(group); scene.add(new THREE.AmbientLight("#f0e6c8", 1.9)); const keyLight = new THREE.DirectionalLight("#f2c76b", 2.8); keyLight.position.set(4, 7, 5); scene.add(keyLight); const controls = new OrbitControls(camera, canvas); controls.target.copy({ x: 0, y: 0, z: 0 }); controls.enableDamping = true; controls.autoRotateSpeed = 0.7; const view = new ViewState(renderer, scene, camera, group, canvas, controls); viewState(view); freezeRotation((new URLSearchParams(window.location.search).get("freeze")) === "1"); controls.autoRotate = !freezeRotation(); window.SOMHAIRLES_VIEWER = { camera: camera, scene: scene, group: group, canvas: canvas, controls: controls }; renderFrame(selectedFrame()); window.addEventListener("resize", (_arg) => { resizeView(view); }); canvas.addEventListener("pointerdown", (_arg_1) => { lastInteractionMs(Date.now()); }); canvas.addEventListener("wheel", (_arg_2) => { lastInteractionMs(Date.now()); }); canvas.addEventListener("touchstart", (_arg_3) => { lastInteractionMs(Date.now()); }); const animate = (nowMs) => { controls.update(); if (!freezeRotation()) { group.rotation.y = (group.rotation.y + 0.003); } const interacting = (nowMs - lastInteractionMs()) < idleSpinDelayMs; controls.autoRotate = (!freezeRotation() && !interacting); renderer.render(scene, camera); requestAnimationFrame(animate); }; requestAnimationFrame(animate); } export function fallbackView() { setText("viewport-note", "WebGL 视图待命 · 已保留重建数据"); element("viewport-panel").classList.add("viewport-fallback"); } export function setNoteRetryable(retryable) { const note = element("viewport-note"); if (retryable) { note.classList.add("is-retryable"); } else { note.classList.remove("is-retryable"); } } export function updateFollowButton() { if ((historicalSteps().length > 0) && (pinnedIndex() != null)) { element("follow-button").removeAttribute("disabled"); setText("follow-button", "跟随最新"); } else { element("follow-button").setAttribute("disabled", "disabled"); if (historicalSteps().length > 0) { setText("follow-button", "跟随最新 · 已开启"); } else { setText("follow-button", "跟随最新"); } } } export function stopReplay() { if (replayTimer() == null) { } else { clearInterval(replayTimer()); replayTimer(undefined); } } export function showFrameMetadata(frame) { setText("mesh-label", toText(printf("占位 · %s"))(frame.Label)); setText("timeline-current", toText(printf("V%02d · 占位 · %s"))(frame.Version)(frame.Label)); } export function setManifestLink(payload) { const link = element("artifact-manifest-link"); const matchValue = payload.manifestUrl; if (isNullOrWhiteSpace(matchValue)) { link.removeAttribute("href"); link.classList.add("is-hidden"); } else { link.setAttribute("href", matchValue); link.classList.remove("is-hidden"); } } export function startStepLoad(view, owner, historical) { const generation = (loadGeneration() + 1) | 0; loadGeneration(generation); pendingLoad([generation, owner]); viewMode(new ViewMode(1, [])); failedLoad(undefined); setNoteRetryable(false); const loader = new GLTFLoader(); const projectId = encodeURIComponent(owner.ProjectId); const runId = encodeURIComponent(owner.RunId); let url; const arg_2 = encodeURIComponent(owner.Path); url = toText(printf("/api/artifacts/steps?projectId=%s&runId=%s&path=%s"))(projectId)(runId)(arg_2); if (historical) { setText("viewport-note", toText(printf("历史几何加载中 · %s"))(owner.Path)); } else { setText("viewport-note", toText(printf("管线几何加载中 · %s"))(owner.Path)); } loader.load(url, ((gltf) => { if (equals(pendingLoad(), [generation, owner])) { pendingLoad(undefined); clearGroup(view.Group); view.Group.add(gltf.scene); displayedNode(gltf.scene); loadedStep(owner); frameObject(view, gltf.scene); view.Renderer.render(view.Scene, view.Camera); if (historical) { setText("viewport-note", toText(printf("历史几何 · %s"))(owner.Path)); } else { setText("viewport-note", toText(printf("管线几何 · %s"))(owner.Path)); } } else { gltf.scene.traverse(function (o) { if (o.geometry && o.geometry.dispose) { o.geometry.dispose(); } var mats = Array.isArray(o.material) ? o.material : (o.material ? [o.material] : []); mats.forEach(function (m) { ['map','normalMap','roughnessMap','metalnessMap','aoMap','emissiveMap'].forEach(function (key) { var t = m[key]; if (t && t.dispose) { t.dispose(); } }); if (m.dispose) { m.dispose(); } }); }); } }), null, ((_arg) => { if (equals(pendingLoad(), [generation, owner])) { pendingLoad(undefined); loadedStep(undefined); failedLoad([owner, historical]); setNoteRetryable(true); if (historical) { setText("viewport-note", toText(printf("历史几何加载失败 · %s"))(owner.Path)); } else { setText("viewport-note", toText(printf("管线几何加载失败 · %s"))(owner.Path)); } } })); } export function loadStepArtifact(view, stepPath, historical) { const owner = new LoadOwner(currentProjectId(), currentRunId(), stepPath); let matchResult, pending_1; if (pendingLoad() != null) { if (equals(pendingLoad()[1], owner)) { matchResult = 0; pending_1 = pendingLoad()[1]; } else { matchResult = 1; } } else { matchResult = 1; } switch (matchResult) { case 0: { break; } case 1: { startStepLoad(view, owner, historical); break; } } } export function retryFailedLoad() { let view, owner, historical; const matchValue = viewState(); const matchValue_1 = failedLoad(); let matchResult, historical_1, owner_1, view_1; if (matchValue != null) { if (matchValue_1 != null) { if ((view = matchValue, (owner = matchValue_1[0], (historical = matchValue_1[1], (owner.ProjectId === currentProjectId()) && (owner.RunId === currentRunId()))))) { matchResult = 0; historical_1 = matchValue_1[1]; owner_1 = matchValue_1[0]; view_1 = matchValue; } else { matchResult = 1; } } else { matchResult = 1; } } else { matchResult = 1; } switch (matchResult) { case 0: { loadStepArtifact(view_1, owner_1.Path, historical_1); break; } case 1: { break; } } } export function applyHistoricalStep(index, duringPlayback) { let arg_5, view; let matchResult, view_1; if (viewState() != null) { if ((view = viewState(), (index >= 0) && (index < historicalSteps().length))) { matchResult = 0; view_1 = viewState(); } else { matchResult = 1; } } else { matchResult = 1; } switch (matchResult) { case 0: { pinnedIndex(index); const path = item(index, historicalSteps()); const version = (index + 1) | 0; setText("mesh-label", toText(printf("历史 · %s"))(path)); setText("timeline-current", toText(printf("V%02d · 历史回放 · %s"))(version)(path)); loadStepArtifact(view_1, path, true); if (duringPlayback) { setText("replay-state", (arg_5 = (historicalSteps().length | 0), toText(printf("历史回放中 · V%02d (%d/%d)"))(version)(version)(arg_5))); } else { setText("replay-state", toText(printf("历史回放 · 已固定 V%02d"))(version)); } break; } case 1: { break; } } } export function renderTimeline() { let arg; const track = element("timeline-track"); var child; while ((child = track.firstChild)) { track.removeChild(child); }; const line = document.createElement("span"); line.className = "timeline-line"; line.setAttribute("aria-hidden", "true"); track.appendChild(line); if (historicalSteps().length > 0) { track.setAttribute("style", (arg = (historicalSteps().length | 0), toText(printf("grid-template-columns: repeat(%d, 1fr)"))(arg))); iterateIndexed((i, path) => { const active = (pinnedIndex() == null) ? (i === (historicalSteps().length - 1)) : (pinnedIndex() === i); track.appendChild(createStepNode(i + 1, path, active, true, i)); }, historicalSteps()); } else { track.setAttribute("style", "grid-template-columns: repeat(3, 1fr)"); frames.forEach((frame) => { track.appendChild(createStepNode(frame.Version, toText(printf("占位 · %s"))(frame.Label), false, frame.Version < frames.length, undefined)); }); } } export function selectStep(index) { if ((index >= 0) && (index < historicalSteps().length)) { stopReplay(); applyHistoricalStep(index, false); updateFollowButton(); renderTimeline(); } } export function createStepNode(version, label, active, complete, onClickIndex) { const classes = ["timeline-step"]; if (complete) { void (classes.push("timeline-step-complete")); } if (onClickIndex != null) { void (classes.push("timeline-step-real")); } if (active) { void (classes.push("timeline-step-active")); } const step = document.createElement("div"); step.className = join(" ", classes); const dot = document.createElement("span"); dot.className = "timeline-dot"; dot.setAttribute("aria-hidden", "true"); step.appendChild(dot); const versionLabel = document.createElement("span"); versionLabel.className = "timeline-version"; versionLabel.textContent = toText(printf("V%02d"))(version); step.appendChild(versionLabel); const text = document.createElement("span"); text.className = "timeline-label"; text.textContent = label; step.appendChild(text); if (onClickIndex == null) { } else { const index = onClickIndex | 0; step.addEventListener("click", (_arg) => { selectStep(index); }); } return step; } export function followLatestStep() { let view; let matchResult, view_1; if (viewState() != null) { if ((view = viewState(), historicalSteps().length > 0)) { matchResult = 0; view_1 = viewState(); } else { matchResult = 1; } } else { matchResult = 1; } switch (matchResult) { case 0: { stopReplay(); pinnedIndex(undefined); const total = historicalSteps().length | 0; const latest = item(total - 1, historicalSteps()); loadStepArtifact(view_1, latest, false); setText("mesh-label", latest); setText("timeline-current", toText(printf("V%02d · 实时跟随 · %s"))(total)(latest)); setText("replay-state", toText(printf("实时跟随 · 最新 V%02d"))(total)); updateFollowButton(); renderTimeline(); break; } case 1: { break; } } } export function updateState(payload) { let arg, arg_3, testExpr, arg_5, arg_7; setStatusClass(statusName(payload)); setText("status-label", statusLabel(payload)); setText("run-message", ((payload.status === "failed") && !isNullOrWhiteSpace(payload.error)) ? payload.error : payload.message); setText("project-id", payload.projectId); setText("run-id", payload.runId); if ((currentProjectId() !== payload.projectId) ? true : (currentRunId() !== payload.runId)) { invalidateLoads(); displayedNode(undefined); stopReplay(); pinnedIndex(undefined); historicalSteps([]); setText("replay-button", "占位演示"); setText("replay-state", "占位示例 · 未选择历史"); updateFollowButton(); } currentProjectId(payload.projectId); currentRunId(payload.runId); const currentVersion = max(0, payload.currentStepIndex + 1) | 0; setText("target-version", (arg = (payload.totalSteps | 0), toText(printf("V%02d"))(arg))); setText("current-version", toText(printf("V%02d"))(currentVersion)); setText("version-readout", (arg_3 = (payload.totalSteps | 0), toText(printf("%02d / %02d"))(currentVersion)(arg_3))); setText("updated-at", payload.updatedAt); const progress = (payload.totalSteps <= 0) ? 0 : min(100, (currentVersion / payload.totalSteps) * 100); setWidth("progress-fill", toText(printf("%.0f%%"))(progress)); const frame = defaultArg(tryFind((candidate) => (candidate.Version === currentVersion), frames), selectedFrame()); setManifestLink(payload); const completedSteps = (payload.completedSteps == null) ? [] : payload.completedSteps; const matchValue = viewState(); if (matchValue == null) { if (completedSteps.length > 0) { setText("mesh-label", item(completedSteps.length - 1, completedSteps)); } else { showFrameMetadata(frame); } } else if ((testExpr = completedSteps, !equalsWith((x, y) => (x === y), testExpr, defaultOf()) && (testExpr.length === 0))) { const view = matchValue; if (historicalSteps().length > 0) { stopReplay(); historicalSteps([]); pinnedIndex(undefined); setText("replay-button", "占位演示"); renderTimeline(); updateFollowButton(); } showFrameMetadata(frame); renderFrame(frame); } else { const view_1 = matchValue; if (!equalsWith((x_1, y_1) => (x_1 === y_1), completedSteps, historicalSteps())) { if (historicalSteps().length === 0) { stopReplay(); setText("replay-button", "播放历史"); } historicalSteps(completedSteps); let matchResult, pinned_1; if (pinnedIndex() != null) { if (pinnedIndex() >= completedSteps.length) { matchResult = 0; pinned_1 = pinnedIndex(); } else { matchResult = 1; } } else { matchResult = 1; } switch (matchResult) { case 0: { pinnedIndex(undefined); break; } } renderTimeline(); updateFollowButton(); } if (pinnedIndex() == null) { const latest = item(completedSteps.length - 1, completedSteps); const owner = new LoadOwner(payload.projectId, payload.runId, latest); if (!equals(loadedStep(), owner)) { loadStepArtifact(view_1, latest, false); } setText("mesh-label", latest); setText("timeline-current", (arg_5 = (completedSteps.length | 0), toText(printf("V%02d · 实时跟随 · %s"))(arg_5)(latest))); setText("replay-state", (arg_7 = (completedSteps.length | 0), toText(printf("实时跟随 · 最新 V%02d"))(arg_7))); } } } export function updateStreamState(message) { setText("stream-state", message); } export function startPlaceholderPlayback() { replayIndex(0); setText("replay-button", "暂停占位演示"); setText("replay-state", "占位演示中 · 未选择历史"); const tick = () => { if (replayIndex() >= frames.length) { stopReplay(); setText("replay-button", "占位演示"); setText("replay-state", "占位演示完成 · 未选择历史"); } else { const frame = item(replayIndex(), frames); showFrameMetadata(frame); renderFrame(frame); setText("replay-state", toText(printf("占位演示中 · V%02d %s"))(frame.Version)(frame.Label)); replayIndex(replayIndex() + 1); } }; tick(); replayTimer(setInterval(tick, 900)); } export function startHistoricalPlayback() { if (historicalSteps().length > 0) { setText("replay-button", "暂停历史回放"); applyHistoricalStep(0, true); updateFollowButton(); renderTimeline(); const total = historicalSteps().length | 0; replayTimer(setInterval(() => { let arg; if (pinnedIndex() == null) { stopReplay(); setText("replay-button", "播放历史"); } else if ((pinnedIndex() + 1) < total) { const pinned_1 = pinnedIndex() | 0; applyHistoricalStep(pinned_1 + 1, true); renderTimeline(); } else { const pinned_2 = pinnedIndex() | 0; stopReplay(); setText("replay-button", "播放历史"); setText("replay-state", (arg = ((pinned_2 + 1) | 0), toText(printf("历史回放完成 · 停在 V%02d"))(arg))); } }, 1500)); } } export function toggleReplay() { let arg; if (replayTimer() == null) { if (historicalSteps().length > 0) { startHistoricalPlayback(); } else { startPlaceholderPlayback(); } } else { clearInterval(replayTimer()); replayTimer(undefined); if (historicalSteps().length > 0) { const current = defaultArg(pinnedIndex(), 0) | 0; setText("replay-button", "播放历史"); setText("replay-state", (arg = ((current + 1) | 0), toText(printf("历史回放 · 已暂停 · V%02d"))(arg))); } else { setText("replay-button", "占位演示"); setText("replay-state", "占位演示 · 已暂停"); } } } export function disconnectFromServer() { if (eventSource() == null) { } else { const source = eventSource(); source.close(); eventSource(undefined); } } export function connectToServer(selection) { disconnectFromServer(); const projectId = encodeURIComponent(selection.ProjectId); const runId = encodeURIComponent(selection.RunId); const source = new EventSource(toText(printf("/api/runs/events?projectId=%s&runId=%s"))(projectId)(runId)); eventSource(source); source.onmessage = ((event) => { updateState(JSON.parse(event.data)); updateStreamState("实时链路 · SSE 已连接"); }); source.onerror = ((_arg) => { updateStreamState("实时链路 · 等待自动重连"); }); } export function inputValue(id) { return element(id).value; } export const probeSelection = new RunSelection("probe-001", "run-probe-real-2", "probe-base-000", "probe-target-007", false); export const shipSelection = new RunSelection("ship-001", "run-ship-001", "ship-base-000", "ship-target-007", false); export let activeModel = createAtom(probeSelection); export function selectedRun() { return activeModel(); } export function highlightActiveCard(selection) { const library = element("model-library"); for (let index = 0; index <= (library.children.length - 1); index++) { const card = library.children.item(some(index)); if (card.classList.contains("model-card")) { if ((card.getAttribute("data-project") === selection.ProjectId) && (card.getAttribute("data-run") === selection.RunId)) { card.classList.add("is-active"); } else { card.classList.remove("is-active"); } } } } export function freshLiveRunId(base_) { const arg_1 = ~~(Date.now() % 1000000) | 0; return toText(printf("%s-live-%d"))(base_)(arg_1); } export function selectionForLiveStart(selection) { if (selection.RunId === "run-probe-real-2") { return new RunSelection(selection.ProjectId, freshLiveRunId(selection.RunId), selection.BaseId, selection.TargetId, selection.Render); } else { return selection; } } export function loadHistoryFor(selection) { const projectId = encodeURIComponent(selection.ProjectId); const runId = encodeURIComponent(selection.RunId); const url = toText(printf("/api/artifacts/manifest?projectId=%s&runId=%s"))(projectId)(runId); const requestOptions = { method: "GET", }; startImmediate(singleton.Delay(() => singleton.TryWith(singleton.Delay(() => singleton.Bind(awaitPromise(fetch(url, requestOptions)), (_arg) => { const response = _arg; return response.ok ? singleton.Bind(awaitPromise(response.json()), (_arg_1) => { let arg_2, arg_3, arg_4, arg_5, arg_6, arg_7, arg_8; const payload = _arg_1; if ((payload.status === "complete") && (payload.steps.length > 0)) { const steps = map((step_1) => step_1.artifactPath, sortBy((step) => step.stepIndex, payload.steps, { Compare: comparePrimitives, })); if (steps.length > 0) { currentProjectId(selection.ProjectId); currentRunId(selection.RunId); historicalSteps(steps); setText("project-id", selection.ProjectId); setText("run-id", selection.RunId); setText("target-version", (arg_2 = (steps.length | 0), toText(printf("V%02d"))(arg_2))); setText("current-version", (arg_3 = (steps.length | 0), toText(printf("V%02d"))(arg_3))); setText("version-readout", (arg_4 = (steps.length | 0), (arg_5 = (steps.length | 0), toText(printf("%02d / %02d"))(arg_4)(arg_5)))); setWidth("progress-fill", "100%"); setText("updated-at", "预发布历史"); setText("mesh-label", (arg_6 = item(steps.length - 1, steps), toText(printf("历史 · %s"))(arg_6))); setText("timeline-current", (arg_7 = (steps.length | 0), (arg_8 = item(steps.length - 1, steps), toText(printf("V%02d · 构建重演 · %s"))(arg_7)(arg_8)))); setText("replay-button", "播放历史"); setText("replay-state", "构建重演 · 预发布历史"); renderTimeline(); updateFollowButton(); if (viewState() == null) { return singleton.Zero(); } else { loadStepArtifact(viewState(), item(steps.length - 1, steps), true); return singleton.Zero(); } } else { return singleton.Zero(); } } else { return singleton.Zero(); } }) : singleton.Zero(); })), (_arg_2) => { return singleton.Zero(); }))); } export function startRun(selection) { const requestBody = { projectId: selection.ProjectId, runId: selection.RunId, baseId: selection.BaseId, targetId: selection.TargetId, render: selection.Render, }; const requestOptions = { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(requestBody), }; setText("run-message", "正在启动设计运行"); updateStreamState("实时链路 · 等待服务端确认"); startImmediate(singleton.Delay(() => singleton.TryWith(singleton.Delay(() => singleton.Bind(awaitPromise(fetch("/api/runs/start", requestOptions)), (_arg) => { let arg; const response = _arg; if (response.ok) { return singleton.Bind(awaitPromise(response.json()), (_arg_1) => { updateState(_arg_1); connectToServer(selection); updateStreamState("实时链路 · SSE 已连接"); return singleton.Zero(); }); } else if (response.status === 409) { setText("run-message", "该运行已完成 · 载入历史回放"); loadHistoryFor(selection); return singleton.Zero(); } else { setText("run-message", (arg = (response.status | 0), toText(printf("启动失败 · HTTP %d"))(arg))); updateStreamState("实时链路 · 启动失败"); return singleton.Zero(); } })), (_arg_2) => { setText("run-message", "启动失败 · 无法连接服务端"); updateStreamState("实时链路 · 连接失败"); return singleton.Zero(); }))); } /** * Boot-time historical load: surface the pre-published probe build replay so * first-time visitors see the real vehicle instead of an empty placeholder. * Any failure leaves the honest placeholder idle state untouched. */ export function loadBootHistory() { loadHistoryFor(selectedRun()); } export function selectModel(card) { const selection = new RunSelection(card.getAttribute("data-project"), card.getAttribute("data-run"), card.getAttribute("data-base"), card.getAttribute("data-target"), false); activeModel(selection); highlightActiveCard(selection); setText("run-message", "载入模型历史"); loadHistoryFor(selection); } export function boot() { showFrameMetadata(item(0, frames)); try { initializeView(); } catch (matchValue) { fallbackView(); } element("replay-button").addEventListener("click", (_arg) => { toggleReplay(); }); element("follow-button").addEventListener("click", (_arg_1) => { followLatestStep(); }); element("viewport-note").addEventListener("click", (_arg_2) => { retryFailedLoad(); }); const library = element("model-library"); for (let index = 0; index <= (library.children.length - 1); index++) { const card = library.children.item(some(index)); if (card.classList.contains("model-card")) { card.addEventListener("click", (_arg_3) => { selectModel(card); }); } } highlightActiveCard(activeModel()); loadBootHistory(); renderTimeline(); } boot();