summaryrefslogtreecommitdiff
path: root/public/App.js
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-21 02:59:53 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-21 02:59:53 +0800
commit822c092790bad4b1c0d780c1fafee063d70c91f6 (patch)
treec4a7d9f740cc1c8bba39c4ded0fc3da15fcc5a5f /public/App.js
parent183c4869e6baf03ad12d10ab8deeaec656a7d348 (diff)
downloadsomhairles-dream-fsharp-822c092790bad4b1c0d780c1fafee063d70c91f6.tar.gz
fix(viewer): guard step loads by generation and run identity, add stale-load regression
Diffstat (limited to 'public/App.js')
-rw-r--r--public/App.js132
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);