diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-21 02:59:53 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-21 02:59:53 +0800 |
| commit | 822c092790bad4b1c0d780c1fafee063d70c91f6 (patch) | |
| tree | c4a7d9f740cc1c8bba39c4ded0fc3da15fcc5a5f /public | |
| parent | 183c4869e6baf03ad12d10ab8deeaec656a7d348 (diff) | |
| download | somhairles-dream-fsharp-822c092790bad4b1c0d780c1fafee063d70c91f6.tar.gz | |
fix(viewer): guard step loads by generation and run identity, add stale-load regression
Diffstat (limited to 'public')
| -rw-r--r-- | public/App.js | 132 |
1 files changed, 113 insertions, 19 deletions
diff --git a/public/App.js b/public/App.js index f953832..4b842d1 100644 --- a/public/App.js +++ b/public/App.js @@ -1,5 +1,5 @@ -import { Record } from "./fable_modules/fable-library-js.4.24.0/Types.js"; -import { class_type, record_type, bool_type, string_type } from "./fable_modules/fable-library-js.4.24.0/Reflection.js"; +import { Union, Record } from "./fable_modules/fable-library-js.4.24.0/Types.js"; +import { union_type, class_type, record_type, bool_type, string_type } from "./fable_modules/fable-library-js.4.24.0/Reflection.js"; import { Replay_frames } from "./SomhairlesDream.Shared/Replay.js"; import { equals, createAtom } from "./fable_modules/fable-library-js.4.24.0/Util.js"; import { tryFind, item } from "./fable_modules/fable-library-js.4.24.0/Array.js"; @@ -40,6 +40,34 @@ 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)]]); } +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); @@ -56,7 +84,13 @@ export let currentProjectId = createAtom(""); export let currentRunId = createAtom(""); -export let loadedStepPath = 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 displayedNode = createAtom(undefined); @@ -173,12 +207,27 @@ export function addMesh(view, frame) { 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); +} + export function renderFrame(frame) { selectedFrame(frame); displayedNode(undefined); + invalidateLoads(); + viewMode(new ViewMode(0, [])); if (viewState() != null) { const view = viewState(); - view.Group.clear(); + clearGroup(view.Group); addMesh(view, frame); view.Renderer.render(view.Scene, view.Camera); } @@ -218,7 +267,7 @@ export function resizeView(view) { view.Camera.aspect = (width / height); view.Camera.updateProjectionMatrix(); if (displayedNode() == null) { - renderFrame(selectedFrame()); + view.Renderer.render(view.Scene, view.Camera); } else { frameObject(view, displayedNode()); @@ -289,27 +338,67 @@ export function setManifestLink(payload) { } } -export function loadStepArtifact(view, stepPath) { - loadedStepPath(stepPath); +export function startStepLoad(view, owner) { + const generation = (loadGeneration() + 1) | 0; + loadGeneration(generation); + pendingLoad([generation, owner]); + viewMode(new ViewMode(1, [])); const loader = new GLTFLoader(); - const projectId = encodeURIComponent(currentProjectId()); - const runId = encodeURIComponent(currentRunId()); + const projectId = encodeURIComponent(owner.ProjectId); + const runId = encodeURIComponent(owner.RunId); let url; - const arg_2 = encodeURIComponent(stepPath); + const arg_2 = encodeURIComponent(owner.Path); url = toText(printf("/api/artifacts/steps?projectId=%s&runId=%s&path=%s"))(projectId)(runId)(arg_2); - setText("viewport-note", toText(printf("管线几何加载中 · %s"))(stepPath)); + setText("viewport-note", toText(printf("管线几何加载中 · %s"))(owner.Path)); loader.load(url, ((gltf) => { - view.Group.clear(); - view.Group.add(gltf.scene); - displayedNode(gltf.scene); - frameObject(view, gltf.scene); - view.Renderer.render(view.Scene, view.Camera); - setText("viewport-note", toText(printf("管线几何 · %s"))(stepPath)); + 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); + 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) => { - setText("viewport-note", toText(printf("管线几何加载失败 · %s"))(stepPath)); + if (equals(pendingLoad(), [generation, owner])) { + pendingLoad(undefined); + loadedStep(undefined); + setText("viewport-note", toText(printf("管线几何加载失败 · %s"))(owner.Path)); + } })); } +export function loadStepArtifact(view, stepPath) { + 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); + break; + } + } +} + export function updateState(payload) { let arg, arg_3; setStatusClass(statusName(payload)); @@ -317,6 +406,10 @@ export function updateState(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); + } currentProjectId(payload.projectId); currentRunId(payload.runId); const currentVersion = max(0, payload.currentStepIndex + 1) | 0; @@ -348,7 +441,8 @@ export function updateState(payload) { } else { const latest = item(steps.length - 1, steps); - if (!equals(loadedStepPath(), latest)) { + const owner = new LoadOwner(payload.projectId, payload.runId, latest); + if (!equals(loadedStep(), owner)) { loadStepArtifact(view, latest); } setText("mesh-label", latest); |
