simview 3.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- simview/__init__.py +69 -0
- simview/__main__.py +174 -0
- simview/launcher.py +109 -0
- simview/live.py +204 -0
- simview/merge.py +593 -0
- simview/model.py +690 -0
- simview/py.typed +0 -0
- simview/scene.py +603 -0
- simview/server.py +739 -0
- simview/state.py +190 -0
- simview/static/css/controls.css +36 -0
- simview/static/js/SimView.js +601 -0
- simview/static/js/components/AnimationController.js +654 -0
- simview/static/js/components/BatchManager.js +198 -0
- simview/static/js/components/InteractionController.js +446 -0
- simview/static/js/components/InteractionControls.js +116 -0
- simview/static/js/components/Scene.js +166 -0
- simview/static/js/components/StateStore.js +188 -0
- simview/static/js/config.js +233 -0
- simview/static/js/main.js +3 -0
- simview/static/js/objects/Body.js +616 -0
- simview/static/js/objects/StaticObject.js +181 -0
- simview/static/js/objects/Terrain.js +514 -0
- simview/static/js/objects/utils.js +378 -0
- simview/static/js/ui/AnalysisPanel.js +233 -0
- simview/static/js/ui/BatchLegend.js +207 -0
- simview/static/js/ui/BodyStateWindow.js +521 -0
- simview/static/js/ui/Controls.js +586 -0
- simview/static/js/ui/ErrorMetrics.js +664 -0
- simview/static/js/ui/Legend.js +87 -0
- simview/static/js/ui/PlaybackControls.js +314 -0
- simview/static/js/ui/ScalarPlotter.js +575 -0
- simview/static/js/utils/blobCodec.js +73 -0
- simview/static/js/utils/bodyTransforms.js +151 -0
- simview/static/js/utils/csv.js +46 -0
- simview/static/js/utils/errorMath.js +69 -0
- simview/static/js/utils/injectStyles.js +16 -0
- simview/static/js/utils/interpolate.js +65 -0
- simview/static/js/utils/liveFollow.js +17 -0
- simview/static/js/utils/loadRecordingLibs.js +34 -0
- simview/static/js/utils/viewState.js +226 -0
- simview/static/lib/chroma-js-3.1.2/index.min.js +8 -0
- simview/static/lib/chroma-js-3.1.2/src/Color.js +49 -0
- simview/static/lib/chroma-js-3.1.2/src/chroma.js +10 -0
- simview/static/lib/chroma-js-3.1.2/src/colors/colorbrewer.js +81 -0
- simview/static/lib/chroma-js-3.1.2/src/colors/w3cx11.js +164 -0
- simview/static/lib/chroma-js-3.1.2/src/generator/average.js +88 -0
- simview/static/lib/chroma-js-3.1.2/src/generator/bezier.js +86 -0
- simview/static/lib/chroma-js-3.1.2/src/generator/blend.js +57 -0
- simview/static/lib/chroma-js-3.1.2/src/generator/cubehelix.js +87 -0
- simview/static/lib/chroma-js-3.1.2/src/generator/mix.js +19 -0
- simview/static/lib/chroma-js-3.1.2/src/generator/random.js +12 -0
- simview/static/lib/chroma-js-3.1.2/src/generator/scale.js +394 -0
- simview/static/lib/chroma-js-3.1.2/src/interpolator/_hsx.js +59 -0
- simview/static/lib/chroma-js-3.1.2/src/interpolator/hcg.js +12 -0
- simview/static/lib/chroma-js-3.1.2/src/interpolator/hsi.js +12 -0
- simview/static/lib/chroma-js-3.1.2/src/interpolator/hsl.js +12 -0
- simview/static/lib/chroma-js-3.1.2/src/interpolator/hsv.js +12 -0
- simview/static/lib/chroma-js-3.1.2/src/interpolator/index.js +1 -0
- simview/static/lib/chroma-js-3.1.2/src/interpolator/lab.js +19 -0
- simview/static/lib/chroma-js-3.1.2/src/interpolator/lch.js +13 -0
- simview/static/lib/chroma-js-3.1.2/src/interpolator/lrgb.js +19 -0
- simview/static/lib/chroma-js-3.1.2/src/interpolator/num.js +15 -0
- simview/static/lib/chroma-js-3.1.2/src/interpolator/oklab.js +19 -0
- simview/static/lib/chroma-js-3.1.2/src/interpolator/oklch.js +12 -0
- simview/static/lib/chroma-js-3.1.2/src/interpolator/rgb.js +18 -0
- simview/static/lib/chroma-js-3.1.2/src/io/cmyk/cmyk2rgb.js +16 -0
- simview/static/lib/chroma-js-3.1.2/src/io/cmyk/index.js +27 -0
- simview/static/lib/chroma-js-3.1.2/src/io/cmyk/rgb2cmyk.js +17 -0
- simview/static/lib/chroma-js-3.1.2/src/io/css/css2rgb.js +238 -0
- simview/static/lib/chroma-js-3.1.2/src/io/css/hsl2css.js +26 -0
- simview/static/lib/chroma-js-3.1.2/src/io/css/index.js +27 -0
- simview/static/lib/chroma-js-3.1.2/src/io/css/lab2css.js +24 -0
- simview/static/lib/chroma-js-3.1.2/src/io/css/lch2css.js +24 -0
- simview/static/lib/chroma-js-3.1.2/src/io/css/oklab2css.js +16 -0
- simview/static/lib/chroma-js-3.1.2/src/io/css/oklch2css.js +16 -0
- simview/static/lib/chroma-js-3.1.2/src/io/css/rgb2css.js +61 -0
- simview/static/lib/chroma-js-3.1.2/src/io/gl/index.js +22 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hcg/hcg2rgb.js +55 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hcg/index.js +27 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hcg/rgb2hcg.js +23 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hex/hex2rgb.js +56 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hex/index.js +29 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hex/rgb2hex.js +29 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hsi/hsi2rgb.js +45 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hsi/index.js +27 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hsi/rgb2hsi.js +31 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hsl/hsl2rgb.js +35 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hsl/index.js +27 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hsl/rgb2hsl.js +45 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hsv/hsv2rgb.js +47 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hsv/index.js +27 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hsv/rgb2hsv.js +32 -0
- simview/static/lib/chroma-js-3.1.2/src/io/input.js +4 -0
- simview/static/lib/chroma-js-3.1.2/src/io/lab/index.js +28 -0
- simview/static/lib/chroma-js-3.1.2/src/io/lab/lab-constants.js +120 -0
- simview/static/lib/chroma-js-3.1.2/src/io/lab/lab2rgb.js +101 -0
- simview/static/lib/chroma-js-3.1.2/src/io/lab/rgb2lab.js +67 -0
- simview/static/lib/chroma-js-3.1.2/src/io/lch/hcl2rgb.js +9 -0
- simview/static/lib/chroma-js-3.1.2/src/io/lch/index.js +35 -0
- simview/static/lib/chroma-js-3.1.2/src/io/lch/lab2lch.js +12 -0
- simview/static/lib/chroma-js-3.1.2/src/io/lch/lch2lab.js +18 -0
- simview/static/lib/chroma-js-3.1.2/src/io/lch/lch2rgb.js +13 -0
- simview/static/lib/chroma-js-3.1.2/src/io/lch/rgb2lch.js +12 -0
- simview/static/lib/chroma-js-3.1.2/src/io/named/index.js +30 -0
- simview/static/lib/chroma-js-3.1.2/src/io/num/index.js +32 -0
- simview/static/lib/chroma-js-3.1.2/src/io/num/num2rgb.js +13 -0
- simview/static/lib/chroma-js-3.1.2/src/io/num/rgb2num.js +8 -0
- simview/static/lib/chroma-js-3.1.2/src/io/oklab/index.js +27 -0
- simview/static/lib/chroma-js-3.1.2/src/io/oklab/oklab2rgb.js +34 -0
- simview/static/lib/chroma-js-3.1.2/src/io/oklab/rgb2oklab.js +37 -0
- simview/static/lib/chroma-js-3.1.2/src/io/oklch/index.js +27 -0
- simview/static/lib/chroma-js-3.1.2/src/io/oklch/oklch2rgb.js +13 -0
- simview/static/lib/chroma-js-3.1.2/src/io/oklch/rgb2oklch.js +12 -0
- simview/static/lib/chroma-js-3.1.2/src/io/rgb/index.js +44 -0
- simview/static/lib/chroma-js-3.1.2/src/io/temp/index.js +22 -0
- simview/static/lib/chroma-js-3.1.2/src/io/temp/rgb2temperature.js +30 -0
- simview/static/lib/chroma-js-3.1.2/src/io/temp/temperature2rgb.js +39 -0
- simview/static/lib/chroma-js-3.1.2/src/ops/alpha.js +13 -0
- simview/static/lib/chroma-js-3.1.2/src/ops/clipped.js +5 -0
- simview/static/lib/chroma-js-3.1.2/src/ops/darken.js +17 -0
- simview/static/lib/chroma-js-3.1.2/src/ops/get.js +13 -0
- simview/static/lib/chroma-js-3.1.2/src/ops/luminance.js +54 -0
- simview/static/lib/chroma-js-3.1.2/src/ops/mix.js +10 -0
- simview/static/lib/chroma-js-3.1.2/src/ops/premultiply.js +12 -0
- simview/static/lib/chroma-js-3.1.2/src/ops/saturate.js +15 -0
- simview/static/lib/chroma-js-3.1.2/src/ops/set.js +43 -0
- simview/static/lib/chroma-js-3.1.2/src/ops/shade.js +11 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/analyze.js +191 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/clip_rgb.js +15 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/contrast.js +12 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/contrastAPCA.js +68 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/delta-e.js +62 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/distance.js +17 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/index.js +27 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/last.js +8 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/limit.js +5 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/multiply-matrices.js +36 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/scales.js +15 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/type.js +18 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/unpack.js +17 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/valid.js +11 -0
- simview/static/lib/chroma-js-3.1.2/src/version.js +2 -0
- simview/static/lib/download.js +132 -0
- simview/static/lib/js-colormaps.js +19335 -0
- simview/static/lib/tar.js +334 -0
- simview/static/lib/three-0.174.0/addons/controls/OrbitControls.js +1556 -0
- simview/static/lib/three-0.174.0/addons/libs/lil-gui.module.min.js +8 -0
- simview/static/lib/three-0.174.0/three.core.js +48830 -0
- simview/static/lib/three-0.174.0/three.module.js +17313 -0
- simview/static/lib/uPlot.esm.js +6140 -0
- simview/static/lib/uPlot.min.css +2 -0
- simview/static/textures/contacts/red-cross0.png +0 -0
- simview/static/textures/points/ball0.png +0 -0
- simview/static/textures/points/ball1.png +0 -0
- simview/templates/index.html +81 -0
- simview/utils.py +41 -0
- simview-3.2.dist-info/METADATA +552 -0
- simview-3.2.dist-info/RECORD +163 -0
- simview-3.2.dist-info/WHEEL +5 -0
- simview-3.2.dist-info/entry_points.txt +2 -0
- simview-3.2.dist-info/licenses/LICENSE +28 -0
- simview-3.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { Scene } from "./components/Scene.js";
|
|
3
|
+
import { UIControls } from "./ui/Controls.js";
|
|
4
|
+
import { BodyStateWindow } from "./ui/BodyStateWindow.js";
|
|
5
|
+
import { AnimationController } from "./components/AnimationController.js";
|
|
6
|
+
import { UI_DEFAULT_CONFIG } from "./config.js";
|
|
7
|
+
import { Body } from "./objects/Body.js";
|
|
8
|
+
import { Terrain } from "./objects/Terrain.js";
|
|
9
|
+
import { BatchManager } from "./components/BatchManager.js";
|
|
10
|
+
import { ScalarPlotter } from "./ui/ScalarPlotter.js";
|
|
11
|
+
import { StaticObject } from "./objects/StaticObject.js";
|
|
12
|
+
import { Legend } from "./ui/Legend.js";
|
|
13
|
+
import { BatchLegend } from "./ui/BatchLegend.js";
|
|
14
|
+
import { ErrorMetrics } from "./ui/ErrorMetrics.js";
|
|
15
|
+
import { AnalysisPanel } from "./ui/AnalysisPanel.js";
|
|
16
|
+
import { InteractionController } from "./components/InteractionController.js";
|
|
17
|
+
import { buildBodyMeta, resolveStateBodies, topoSortBodies } from "./utils/bodyTransforms.js";
|
|
18
|
+
import {
|
|
19
|
+
decodeFloat32Blob,
|
|
20
|
+
decodeStateField,
|
|
21
|
+
decodeStatesChunk,
|
|
22
|
+
STATE_FIELD_WIDTHS,
|
|
23
|
+
} from "./utils/blobCodec.js";
|
|
24
|
+
import { StateStore } from "./components/StateStore.js";
|
|
25
|
+
import { shouldFollowLive } from "./utils/liveFollow.js";
|
|
26
|
+
import { parseViewState } from "./utils/viewState.js";
|
|
27
|
+
|
|
28
|
+
export class SimView {
|
|
29
|
+
constructor() {
|
|
30
|
+
this.scene = null;
|
|
31
|
+
this.uiControls = null;
|
|
32
|
+
this.bodyStateWindow = null;
|
|
33
|
+
this.animationController = null;
|
|
34
|
+
this.scalarPlotter = null;
|
|
35
|
+
this.errorMetrics = null;
|
|
36
|
+
this.analysisPanel = null;
|
|
37
|
+
this.legend = null;
|
|
38
|
+
this.batchLegend = null;
|
|
39
|
+
this.batchManager = null;
|
|
40
|
+
this.interactionController = null;
|
|
41
|
+
this.terrain = null;
|
|
42
|
+
this.bodies = null;
|
|
43
|
+
this.staticObjects = null;
|
|
44
|
+
this.uiState = structuredClone(UI_DEFAULT_CONFIG);
|
|
45
|
+
this.animate = this.animate.bind(this);
|
|
46
|
+
// Live streaming mode (see startLiveStream): the open WebSocket (or
|
|
47
|
+
// null once closed/if never opened) and its "LIVE" badge element.
|
|
48
|
+
this.liveSocket = null;
|
|
49
|
+
this.liveBadge = null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
static run() {
|
|
53
|
+
const simView = new SimView();
|
|
54
|
+
simView.initAndAnimate();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Fetch the model and states over HTTP (both served pre-gzipped by the server,
|
|
58
|
+
// transparently decompressed by the browser) and build the scene from them.
|
|
59
|
+
async loadData() {
|
|
60
|
+
const splash = document.getElementById("loading-splash");
|
|
61
|
+
try {
|
|
62
|
+
if (splash) splash.innerHTML = "<h1>Loading Model (HTTP)...</h1>";
|
|
63
|
+
console.time("fetch_model");
|
|
64
|
+
const modelResponse = await fetch("/model");
|
|
65
|
+
console.timeEnd("fetch_model");
|
|
66
|
+
if (!modelResponse.ok)
|
|
67
|
+
throw new Error(`Failed to fetch model: ${modelResponse.status} ${modelResponse.statusText}`);
|
|
68
|
+
|
|
69
|
+
if (splash) splash.innerHTML = "<h1>Parsing Model JSON...</h1>";
|
|
70
|
+
console.time("parse_model");
|
|
71
|
+
const model = await modelResponse.json();
|
|
72
|
+
console.timeEnd("parse_model");
|
|
73
|
+
|
|
74
|
+
console.time("fetch_blobs");
|
|
75
|
+
await this.fetchBlobs(model);
|
|
76
|
+
console.timeEnd("fetch_blobs");
|
|
77
|
+
|
|
78
|
+
console.log("Model received, initializing components...");
|
|
79
|
+
this.initFromModel(model);
|
|
80
|
+
|
|
81
|
+
if (splash) splash.innerHTML = "<h1>Loading States (HTTP)...</h1>";
|
|
82
|
+
console.time("fetch_states");
|
|
83
|
+
const statesResponse = await fetch("/states");
|
|
84
|
+
console.timeEnd("fetch_states");
|
|
85
|
+
if (!statesResponse.ok)
|
|
86
|
+
throw new Error(`Failed to fetch states: ${statesResponse.status} ${statesResponse.statusText}`);
|
|
87
|
+
|
|
88
|
+
console.time("parse_states");
|
|
89
|
+
const statesPayload = await statesResponse.json();
|
|
90
|
+
console.timeEnd("parse_states");
|
|
91
|
+
|
|
92
|
+
if (Array.isArray(statesPayload)) {
|
|
93
|
+
// Legacy wire shape: a plain per-frame array, possibly with
|
|
94
|
+
// inline __b64__ fields to expand.
|
|
95
|
+
console.debug(`Received ${statesPayload.length} states (legacy)`);
|
|
96
|
+
this.processStates(statesPayload);
|
|
97
|
+
} else if (statesPayload && statesPayload.live === true) {
|
|
98
|
+
// Live streaming mode (see simview.live.LiveViewer): no states
|
|
99
|
+
// yet, they arrive incrementally over /ws/states instead.
|
|
100
|
+
console.debug("Live mode: opening /ws/states");
|
|
101
|
+
this.store = StateStore.fromLegacy([]);
|
|
102
|
+
this.startLiveStream(splash);
|
|
103
|
+
} else if (statesPayload && statesPayload.version === 4) {
|
|
104
|
+
// Columnar wire shape (see server.py::_columnarize_states):
|
|
105
|
+
// a lightweight index plus /blob/... URLs for the actual
|
|
106
|
+
// whole-trajectory float32 data, fetched in parallel below.
|
|
107
|
+
console.debug(`Received ${statesPayload.times.length} states (columnar)`);
|
|
108
|
+
console.time("fetch_state_blobs");
|
|
109
|
+
await this.fetchBlobs(statesPayload);
|
|
110
|
+
console.timeEnd("fetch_state_blobs");
|
|
111
|
+
this.store = StateStore.fromColumnar(statesPayload, this.batchManager.simBatches);
|
|
112
|
+
this.onStoreReady();
|
|
113
|
+
} else {
|
|
114
|
+
throw new Error("Unrecognized /states payload shape");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Live mode keeps the splash up (repurposed as a "waiting for
|
|
118
|
+
// first state" indicator by startLiveStream) until the first
|
|
119
|
+
// frame actually arrives -- see the onmessage handler there.
|
|
120
|
+
if (splash && !this.liveSocket) splash.remove();
|
|
121
|
+
console.log("Initialization complete!");
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error("Critical error during initial data fetch:", error);
|
|
124
|
+
if (splash) {
|
|
125
|
+
splash.innerHTML = `<h1 style="color: red;">Load Error</h1><p>${error.message}</p><p>Check browser console for details.</p>`;
|
|
126
|
+
}
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Walks any JSON-shaped object/array (the model, or the columnar states
|
|
132
|
+
// payload) collecting every "/blob/..." reference, then fetches them all
|
|
133
|
+
// in parallel and replaces each in place with its decoded Float32Array --
|
|
134
|
+
// sequential awaits here would serialize what's otherwise an
|
|
135
|
+
// embarrassingly parallel set of independent HTTP requests.
|
|
136
|
+
async fetchBlobs(obj) {
|
|
137
|
+
const refs = [];
|
|
138
|
+
const collect = (node) => {
|
|
139
|
+
if (!node || typeof node !== 'object') return;
|
|
140
|
+
for (const key of Object.keys(node)) {
|
|
141
|
+
const val = node[key];
|
|
142
|
+
if (typeof val === 'string' && val.startsWith('/blob/')) {
|
|
143
|
+
refs.push({ container: node, key, url: val });
|
|
144
|
+
} else if (typeof val === 'object') {
|
|
145
|
+
collect(val);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
collect(obj);
|
|
150
|
+
|
|
151
|
+
await Promise.all(
|
|
152
|
+
refs.map(async ({ container, key, url }) => {
|
|
153
|
+
const res = await fetch(url);
|
|
154
|
+
const arrayBuffer = await res.arrayBuffer();
|
|
155
|
+
container[key] = SimView.decodeFloat32Blob(arrayBuffer);
|
|
156
|
+
})
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Opens the live-streaming WebSocket (see simview.live.LiveViewer): each
|
|
161
|
+
// message is `{states: [<frame>, ...]}` -- one catch-up message with
|
|
162
|
+
// every frame buffered so far, then one message per subsequently pushed
|
|
163
|
+
// frame -- and is run through the same processStatesChunk path the
|
|
164
|
+
// static per-frame legacy wire shape uses. `splash` (the loading-splash
|
|
165
|
+
// element, already repurposed as a "waiting for first state" message by
|
|
166
|
+
// the caller) is removed once the first frame actually arrives.
|
|
167
|
+
startLiveStream(splash) {
|
|
168
|
+
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
|
|
169
|
+
const socket = new WebSocket(`${protocol}//${location.host}/ws/states`);
|
|
170
|
+
this.liveSocket = socket;
|
|
171
|
+
this.showLiveBadge();
|
|
172
|
+
|
|
173
|
+
socket.onmessage = (event) => {
|
|
174
|
+
const { states } = JSON.parse(event.data);
|
|
175
|
+
if (!states || states.length === 0) return;
|
|
176
|
+
this.processStatesChunk(states);
|
|
177
|
+
if (splash) splash.remove();
|
|
178
|
+
};
|
|
179
|
+
socket.onclose = () => {
|
|
180
|
+
console.log("Live stream closed (simulation finished serving).");
|
|
181
|
+
this.liveSocket = null;
|
|
182
|
+
this.hideLiveBadge();
|
|
183
|
+
};
|
|
184
|
+
socket.onerror = (event) => {
|
|
185
|
+
console.error("Live stream error:", event);
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
showLiveBadge() {
|
|
190
|
+
if (this.liveBadge) return;
|
|
191
|
+
const badge = document.createElement("div");
|
|
192
|
+
badge.textContent = "LIVE";
|
|
193
|
+
Object.assign(badge.style, {
|
|
194
|
+
position: "absolute",
|
|
195
|
+
bottom: "20px",
|
|
196
|
+
right: "20px",
|
|
197
|
+
padding: "4px 10px",
|
|
198
|
+
borderRadius: "4px",
|
|
199
|
+
backgroundColor: "rgba(200, 0, 0, 0.8)",
|
|
200
|
+
color: "white",
|
|
201
|
+
fontFamily: "monospace",
|
|
202
|
+
fontWeight: "bold",
|
|
203
|
+
letterSpacing: "1px",
|
|
204
|
+
zIndex: 1000,
|
|
205
|
+
});
|
|
206
|
+
document.body.appendChild(badge);
|
|
207
|
+
this.liveBadge = badge;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
hideLiveBadge() {
|
|
211
|
+
if (this.liveBadge) {
|
|
212
|
+
this.liveBadge.remove();
|
|
213
|
+
this.liveBadge = null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Thin delegates to utils/blobCodec.js -- kept as methods since other code
|
|
218
|
+
// calls through `this`/`SimView.*` and the pure decoding logic lives there
|
|
219
|
+
// so it can be unit-tested without a SimView instance.
|
|
220
|
+
static decodeFloat32Blob(arrayBuffer) {
|
|
221
|
+
return decodeFloat32Blob(arrayBuffer);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
static STATE_FIELD_WIDTHS = STATE_FIELD_WIDTHS;
|
|
225
|
+
|
|
226
|
+
decodeStateField(str, width) {
|
|
227
|
+
return decodeStateField(str, width);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
decodeStatesChunk(chunk) {
|
|
231
|
+
decodeStatesChunk(chunk);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Legacy wire shape entry point: decodes any inline __b64__ fields, wraps
|
|
235
|
+
// the array in a LegacyStateStore, and (dis)patches it exactly like the
|
|
236
|
+
// columnar path below via onStoreReady.
|
|
237
|
+
//
|
|
238
|
+
// Branches on whether the animation has been loaded yet (not on whether
|
|
239
|
+
// `this.store` exists) so the live-streaming path works too: there
|
|
240
|
+
// `this.store` is created empty up front (see startLiveStream), and the
|
|
241
|
+
// first inbound chunk must still go through the "first load" branch
|
|
242
|
+
// (loadAnimation/initFromStore) rather than being treated as an append to
|
|
243
|
+
// an already-running animation.
|
|
244
|
+
processStatesChunk(chunk) {
|
|
245
|
+
this.decodeStatesChunk(chunk);
|
|
246
|
+
const firstLoad = !this.animationController || !this.animationController.store;
|
|
247
|
+
if (!this.store) {
|
|
248
|
+
this.store = StateStore.fromLegacy(chunk);
|
|
249
|
+
} else if (firstLoad) {
|
|
250
|
+
this.store.append(chunk);
|
|
251
|
+
} else {
|
|
252
|
+
const wasFollowingLive = shouldFollowLive(this.animationController);
|
|
253
|
+
const startIndex = this.store.append(chunk);
|
|
254
|
+
this.animationController.onStatesAppended();
|
|
255
|
+
this.appendBodyHistories(startIndex);
|
|
256
|
+
if (this.errorMetrics) {
|
|
257
|
+
this.errorMetrics.onHistoryReady();
|
|
258
|
+
}
|
|
259
|
+
if (wasFollowingLive) {
|
|
260
|
+
this.animationController.goToTime(this.store.lastTime());
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (firstLoad) {
|
|
264
|
+
this.onStoreReady();
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Called once the store (legacy or columnar) has its full initial
|
|
269
|
+
// timeline available -- wires it into the animation/scalar/history
|
|
270
|
+
// consumers that were previously handed the raw states array directly.
|
|
271
|
+
onStoreReady() {
|
|
272
|
+
if (this.animationController) {
|
|
273
|
+
this.animationController.loadAnimation(this.store);
|
|
274
|
+
if (this.scalarPlotter) {
|
|
275
|
+
this.scalarPlotter.initFromStore(this.store);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
this.appendBodyHistories(0);
|
|
279
|
+
if (this.errorMetrics) {
|
|
280
|
+
this.errorMetrics.onHistoryReady();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Resolves and records position/orientation (and contacts, vectors, ...)
|
|
285
|
+
// history for every frame from `startIndex` to the end of the store, for
|
|
286
|
+
// trails and the error metrics panel. Each frame is materialized
|
|
287
|
+
// transiently via store.getFrame(i) (not retained) so this works
|
|
288
|
+
// identically whether the store is legacy or columnar.
|
|
289
|
+
appendBodyHistories(startIndex) {
|
|
290
|
+
if (!this.bodies || this.bodies.size === 0) return;
|
|
291
|
+
for (let s = startIndex; s < this.store.length; s++) {
|
|
292
|
+
const bodyStates = this.store.getFrame(s).bodies;
|
|
293
|
+
if (!bodyStates) continue;
|
|
294
|
+
// Resolve off this historical state's own data (not any Body's
|
|
295
|
+
// current/live pose, which reflects whatever frame is currently
|
|
296
|
+
// displayed) -- a child's parent pose must come from the same
|
|
297
|
+
// state index `s` being appended here.
|
|
298
|
+
const resolved = resolveStateBodies(
|
|
299
|
+
this.bodyMeta,
|
|
300
|
+
this.bodyTopoOrder,
|
|
301
|
+
this.batchManager.simBatches,
|
|
302
|
+
bodyStates
|
|
303
|
+
);
|
|
304
|
+
resolved.forEach((resolvedBodyState, name) => {
|
|
305
|
+
const body = this.bodies.get(name);
|
|
306
|
+
if (body) {
|
|
307
|
+
if (body.appendHistoryPointAt) {
|
|
308
|
+
body.appendHistoryPointAt(s, resolvedBodyState);
|
|
309
|
+
} else if (body.setHistoryPointAt) {
|
|
310
|
+
body.setHistoryPointAt(s, resolvedBodyState);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
this.bodies.forEach((body) => {
|
|
316
|
+
if (body.finalizeTrails) body.finalizeTrails();
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
processStates(statesData) {
|
|
321
|
+
this.processStatesChunk(statesData);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
initFromModel(model) {
|
|
325
|
+
try {
|
|
326
|
+
this.disposeOfAll();
|
|
327
|
+
|
|
328
|
+
this.batchManager = new BatchManager(this, model);
|
|
329
|
+
this.bodies = new Map();
|
|
330
|
+
|
|
331
|
+
// Auto-detect visualization mode based on first body: anything with a
|
|
332
|
+
// real surface (box/sphere/cylinder/mesh) defaults to mesh; pointclouds
|
|
333
|
+
// keep the default points mode.
|
|
334
|
+
if (Array.isArray(model.bodies) && model.bodies.length > 0) {
|
|
335
|
+
const firstShape = model.bodies[0].shape;
|
|
336
|
+
if (firstShape && firstShape.type !== "pointcloud") {
|
|
337
|
+
console.log("Auto-switching visualization mode to 'mesh' based on body type");
|
|
338
|
+
this.uiState.bodyVisualizationMode = "mesh";
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (Array.isArray(model.bodies)) {
|
|
343
|
+
model.bodies.forEach((bodyData) => {
|
|
344
|
+
const body = new Body(bodyData, this);
|
|
345
|
+
this.bodies.set(bodyData.name, body);
|
|
346
|
+
this.scene.addObject3D(body.getObject3D());
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
// Parent-relative transforms (rigid/articulated attachments, see
|
|
350
|
+
// README.md): resolved once here so per-frame/per-state consumers
|
|
351
|
+
// below only ever deal with ordinary absolute-world transforms.
|
|
352
|
+
this.bodyMeta = buildBodyMeta(model.bodies);
|
|
353
|
+
this.bodyTopoOrder = topoSortBodies(this.bodyMeta);
|
|
354
|
+
if (Array.isArray(model.staticObjects)) {
|
|
355
|
+
this.staticObjects = model.staticObjects.map((staticObjectData) => {
|
|
356
|
+
const staticObject = new StaticObject(staticObjectData, this);
|
|
357
|
+
this.scene.addObject3D(staticObject.getObject3D());
|
|
358
|
+
return staticObject;
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
if (model.terrain) {
|
|
362
|
+
console.debug("Using terrain data");
|
|
363
|
+
this.terrain = new Terrain(model.terrain, this);
|
|
364
|
+
this.scene.addObject3D(this.terrain.getObject3D());
|
|
365
|
+
} else {
|
|
366
|
+
throw new Error("Terrain data is missing in model");
|
|
367
|
+
}
|
|
368
|
+
const hasScalars = model.scalarNames && model.scalarNames.length > 0;
|
|
369
|
+
const hasErrorMetrics = this.batchManager.simBatches >= 2;
|
|
370
|
+
if (hasScalars || hasErrorMetrics) {
|
|
371
|
+
this.analysisPanel = new AnalysisPanel(this);
|
|
372
|
+
}
|
|
373
|
+
if (hasScalars) {
|
|
374
|
+
console.debug(
|
|
375
|
+
"Initializing scalar plotter for scalars",
|
|
376
|
+
model.scalarNames
|
|
377
|
+
);
|
|
378
|
+
this.scalarPlotter = new ScalarPlotter(this, model.scalarNames);
|
|
379
|
+
this.analysisPanel.attachScalarPlotter(this.scalarPlotter);
|
|
380
|
+
}
|
|
381
|
+
if (hasErrorMetrics) {
|
|
382
|
+
this.errorMetrics = new ErrorMetrics(this);
|
|
383
|
+
this.analysisPanel.attachErrorMetrics(this.errorMetrics);
|
|
384
|
+
}
|
|
385
|
+
this.interactionController = new InteractionController(this);
|
|
386
|
+
this.uiControls = new UIControls(this);
|
|
387
|
+
this.bodyStateWindow = new BodyStateWindow(this);
|
|
388
|
+
this.legend = new Legend(this);
|
|
389
|
+
if (this.batchManager.simBatches >= 2) {
|
|
390
|
+
this.batchLegend = new BatchLegend(this);
|
|
391
|
+
}
|
|
392
|
+
this.animationController = new AnimationController(this, model.dt);
|
|
393
|
+
} catch (error) {
|
|
394
|
+
console.error("Error during initFromModel:", error);
|
|
395
|
+
const splash = document.getElementById("loading-splash");
|
|
396
|
+
if (splash) {
|
|
397
|
+
splash.innerHTML = `<h1 style="color: red;">Error during initialization</h1><p>${error.message}</p>`;
|
|
398
|
+
}
|
|
399
|
+
throw error;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async initAndAnimate() {
|
|
404
|
+
try {
|
|
405
|
+
this.scene = new Scene(this);
|
|
406
|
+
await this.loadData();
|
|
407
|
+
this.applyViewStateFromHash();
|
|
408
|
+
this.animate();
|
|
409
|
+
const splash = document.getElementById("loading-splash");
|
|
410
|
+
if (splash) splash.remove();
|
|
411
|
+
} catch (error) {
|
|
412
|
+
console.error("Initialization failed:", error);
|
|
413
|
+
const splash = document.getElementById("loading-splash");
|
|
414
|
+
if (splash) {
|
|
415
|
+
splash.innerHTML = `<h1 style="color: red;">Failed to connect or initialize</h1><p>${error.message}</p>`;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Shareable-view-link apply-on-load (see utils/viewState.js and
|
|
421
|
+
// ui/Controls.js's "Copy view link" button that produces these hashes).
|
|
422
|
+
// Called once from initAndAnimate() right after loadData() resolves --
|
|
423
|
+
// by then the animation/scene/uiControls are all fully wired (loadData
|
|
424
|
+
// awaits through to onStoreReady/animationController.loadAnimation for
|
|
425
|
+
// every non-live wire shape), so goToTime/camera/batch focus are all
|
|
426
|
+
// safe to apply here. Live-streaming mode is the one case where
|
|
427
|
+
// loadAnimation may not have run yet (see startLiveStream) -- there's no
|
|
428
|
+
// timeline to seek yet, so this just skips silently rather than
|
|
429
|
+
// crashing; the view link's toggles/camera still get applied via the
|
|
430
|
+
// early-return guards below where possible.
|
|
431
|
+
//
|
|
432
|
+
// A malformed/missing hash (parseViewState returning null) or a bad
|
|
433
|
+
// partial state must never break page load -- every step below is
|
|
434
|
+
// independently guarded.
|
|
435
|
+
applyViewStateFromHash() {
|
|
436
|
+
let state;
|
|
437
|
+
try {
|
|
438
|
+
state = parseViewState(location.hash);
|
|
439
|
+
} catch (e) {
|
|
440
|
+
console.warn("Failed to parse view-state hash:", e);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
if (!state) return;
|
|
444
|
+
|
|
445
|
+
try {
|
|
446
|
+
if (this.batchManager && Number.isInteger(state.batchIndex)) {
|
|
447
|
+
this.batchManager.setActiveBatch(state.batchIndex);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (this.animationController && this.animationController.store) {
|
|
451
|
+
this.animationController.pause();
|
|
452
|
+
if (Number.isFinite(state.time)) {
|
|
453
|
+
this.animationController.goToTime(state.time);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (this.scene && state.camera) {
|
|
458
|
+
const { camera, controls } = this.scene;
|
|
459
|
+
if (state.camera.position) {
|
|
460
|
+
camera.position.set(state.camera.position.x, state.camera.position.y, state.camera.position.z);
|
|
461
|
+
}
|
|
462
|
+
if (state.camera.target) {
|
|
463
|
+
controls.target.set(state.camera.target.x, state.camera.target.y, state.camera.target.z);
|
|
464
|
+
}
|
|
465
|
+
if (Number.isFinite(state.camera.fov)) {
|
|
466
|
+
camera.fov = state.camera.fov;
|
|
467
|
+
camera.updateProjectionMatrix();
|
|
468
|
+
}
|
|
469
|
+
controls.update();
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Routed through UIControls.applyViewState so lil-gui's own
|
|
473
|
+
// controllers (and their onChange handlers, which is what
|
|
474
|
+
// actually flips body/terrain visuals) stay in sync instead of
|
|
475
|
+
// uiState silently drifting out from under the displayed panel.
|
|
476
|
+
if (this.uiControls) {
|
|
477
|
+
this.uiControls.applyViewState(state);
|
|
478
|
+
}
|
|
479
|
+
} catch (e) {
|
|
480
|
+
console.warn("Failed to apply view state from URL hash:", e);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
disposeOfAll() {
|
|
485
|
+
if (this.bodies) {
|
|
486
|
+
for (const body of this.bodies.values()) {
|
|
487
|
+
body.dispose();
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
if (this.staticObjects) {
|
|
491
|
+
for (const staticObject of this.staticObjects) {
|
|
492
|
+
staticObject.dispose();
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (this.terrain) {
|
|
496
|
+
this.terrain.dispose();
|
|
497
|
+
}
|
|
498
|
+
if (this.uiControls) {
|
|
499
|
+
this.uiControls.dispose();
|
|
500
|
+
}
|
|
501
|
+
if (this.animationController) {
|
|
502
|
+
this.animationController.dispose();
|
|
503
|
+
}
|
|
504
|
+
if (this.scalarPlotter) {
|
|
505
|
+
this.scalarPlotter.dispose();
|
|
506
|
+
this.scalarPlotter = null;
|
|
507
|
+
}
|
|
508
|
+
if (this.errorMetrics) {
|
|
509
|
+
this.errorMetrics.dispose();
|
|
510
|
+
this.errorMetrics = null;
|
|
511
|
+
}
|
|
512
|
+
if (this.analysisPanel) {
|
|
513
|
+
this.analysisPanel.dispose();
|
|
514
|
+
this.analysisPanel = null;
|
|
515
|
+
}
|
|
516
|
+
if (this.bodyStateWindow) {
|
|
517
|
+
this.bodyStateWindow.dispose();
|
|
518
|
+
}
|
|
519
|
+
if (this.legend) {
|
|
520
|
+
this.legend.dispose();
|
|
521
|
+
}
|
|
522
|
+
if (this.batchLegend) {
|
|
523
|
+
this.batchLegend.dispose();
|
|
524
|
+
this.batchLegend = null;
|
|
525
|
+
}
|
|
526
|
+
if (this.interactionController) {
|
|
527
|
+
this.interactionController.cleanup();
|
|
528
|
+
this.interactionController = null;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
animate() {
|
|
533
|
+
requestAnimationFrame(this.animate);
|
|
534
|
+
const now = performance.now();
|
|
535
|
+
|
|
536
|
+
// 1. Update states and time
|
|
537
|
+
if (this.animationController) {
|
|
538
|
+
this.animationController.animate(now);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// 2. Update UI components
|
|
542
|
+
if (this.scalarPlotter) {
|
|
543
|
+
this.scalarPlotter.animate(now);
|
|
544
|
+
}
|
|
545
|
+
if (this.bodyStateWindow) {
|
|
546
|
+
this.bodyStateWindow.animate(now);
|
|
547
|
+
}
|
|
548
|
+
if (this.errorMetrics) {
|
|
549
|
+
this.errorMetrics.animate(now);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// 3. Render the scene
|
|
553
|
+
if (this.scene) {
|
|
554
|
+
if (this.uiState && this.uiState.trackBody && this.uiState.trackBody !== "None") {
|
|
555
|
+
const body = this.bodies.get(this.uiState.trackBody);
|
|
556
|
+
if (body && this.batchManager) {
|
|
557
|
+
const activeBatch = this.batchManager.currentlyActiveBatch;
|
|
558
|
+
if (body.positions && body.positions[activeBatch]) {
|
|
559
|
+
const pos = body.positions[activeBatch];
|
|
560
|
+
const offset = this.batchManager.getBatchOffset(activeBatch);
|
|
561
|
+
const currentBodyPos = new THREE.Vector3(pos.x + offset.x, pos.y + offset.y, pos.z + offset.z);
|
|
562
|
+
|
|
563
|
+
try {
|
|
564
|
+
if (this._lastTrackedBody !== this.uiState.trackBody) {
|
|
565
|
+
// On first tracking or switch, center the target on the body, preserving the viewing angle
|
|
566
|
+
const delta = currentBodyPos.clone().sub(this.scene.controls.target);
|
|
567
|
+
this.scene.camera.position.add(delta);
|
|
568
|
+
this.scene.controls.target.copy(currentBodyPos);
|
|
569
|
+
this.scene.controls.update();
|
|
570
|
+
console.log("Started tracking", this.uiState.trackBody);
|
|
571
|
+
} else if (this._lastTrackedPosition) {
|
|
572
|
+
// On subsequent frames, shift camera and target by the exact movement delta of the body
|
|
573
|
+
const delta = currentBodyPos.clone().sub(this._lastTrackedPosition);
|
|
574
|
+
// Only update if it actually moved
|
|
575
|
+
if (delta.lengthSq() > 0) {
|
|
576
|
+
this.scene.camera.position.add(delta);
|
|
577
|
+
this.scene.controls.target.add(delta);
|
|
578
|
+
this.scene.controls.update();
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
} catch (e) {
|
|
582
|
+
console.error("Error in tracking logic:", e);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
this._lastTrackedBody = this.uiState.trackBody;
|
|
586
|
+
this._lastTrackedPosition = currentBodyPos.clone();
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
} else {
|
|
590
|
+
this._lastTrackedBody = null;
|
|
591
|
+
this._lastTrackedPosition = null;
|
|
592
|
+
}
|
|
593
|
+
this.scene.animate(now);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// 4. Capture the frame if recording
|
|
597
|
+
if (this.animationController && this.animationController.isRecording) {
|
|
598
|
+
this.animationController.captureFrame(now);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|