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.
Files changed (163) hide show
  1. simview/__init__.py +69 -0
  2. simview/__main__.py +174 -0
  3. simview/launcher.py +109 -0
  4. simview/live.py +204 -0
  5. simview/merge.py +593 -0
  6. simview/model.py +690 -0
  7. simview/py.typed +0 -0
  8. simview/scene.py +603 -0
  9. simview/server.py +739 -0
  10. simview/state.py +190 -0
  11. simview/static/css/controls.css +36 -0
  12. simview/static/js/SimView.js +601 -0
  13. simview/static/js/components/AnimationController.js +654 -0
  14. simview/static/js/components/BatchManager.js +198 -0
  15. simview/static/js/components/InteractionController.js +446 -0
  16. simview/static/js/components/InteractionControls.js +116 -0
  17. simview/static/js/components/Scene.js +166 -0
  18. simview/static/js/components/StateStore.js +188 -0
  19. simview/static/js/config.js +233 -0
  20. simview/static/js/main.js +3 -0
  21. simview/static/js/objects/Body.js +616 -0
  22. simview/static/js/objects/StaticObject.js +181 -0
  23. simview/static/js/objects/Terrain.js +514 -0
  24. simview/static/js/objects/utils.js +378 -0
  25. simview/static/js/ui/AnalysisPanel.js +233 -0
  26. simview/static/js/ui/BatchLegend.js +207 -0
  27. simview/static/js/ui/BodyStateWindow.js +521 -0
  28. simview/static/js/ui/Controls.js +586 -0
  29. simview/static/js/ui/ErrorMetrics.js +664 -0
  30. simview/static/js/ui/Legend.js +87 -0
  31. simview/static/js/ui/PlaybackControls.js +314 -0
  32. simview/static/js/ui/ScalarPlotter.js +575 -0
  33. simview/static/js/utils/blobCodec.js +73 -0
  34. simview/static/js/utils/bodyTransforms.js +151 -0
  35. simview/static/js/utils/csv.js +46 -0
  36. simview/static/js/utils/errorMath.js +69 -0
  37. simview/static/js/utils/injectStyles.js +16 -0
  38. simview/static/js/utils/interpolate.js +65 -0
  39. simview/static/js/utils/liveFollow.js +17 -0
  40. simview/static/js/utils/loadRecordingLibs.js +34 -0
  41. simview/static/js/utils/viewState.js +226 -0
  42. simview/static/lib/chroma-js-3.1.2/index.min.js +8 -0
  43. simview/static/lib/chroma-js-3.1.2/src/Color.js +49 -0
  44. simview/static/lib/chroma-js-3.1.2/src/chroma.js +10 -0
  45. simview/static/lib/chroma-js-3.1.2/src/colors/colorbrewer.js +81 -0
  46. simview/static/lib/chroma-js-3.1.2/src/colors/w3cx11.js +164 -0
  47. simview/static/lib/chroma-js-3.1.2/src/generator/average.js +88 -0
  48. simview/static/lib/chroma-js-3.1.2/src/generator/bezier.js +86 -0
  49. simview/static/lib/chroma-js-3.1.2/src/generator/blend.js +57 -0
  50. simview/static/lib/chroma-js-3.1.2/src/generator/cubehelix.js +87 -0
  51. simview/static/lib/chroma-js-3.1.2/src/generator/mix.js +19 -0
  52. simview/static/lib/chroma-js-3.1.2/src/generator/random.js +12 -0
  53. simview/static/lib/chroma-js-3.1.2/src/generator/scale.js +394 -0
  54. simview/static/lib/chroma-js-3.1.2/src/interpolator/_hsx.js +59 -0
  55. simview/static/lib/chroma-js-3.1.2/src/interpolator/hcg.js +12 -0
  56. simview/static/lib/chroma-js-3.1.2/src/interpolator/hsi.js +12 -0
  57. simview/static/lib/chroma-js-3.1.2/src/interpolator/hsl.js +12 -0
  58. simview/static/lib/chroma-js-3.1.2/src/interpolator/hsv.js +12 -0
  59. simview/static/lib/chroma-js-3.1.2/src/interpolator/index.js +1 -0
  60. simview/static/lib/chroma-js-3.1.2/src/interpolator/lab.js +19 -0
  61. simview/static/lib/chroma-js-3.1.2/src/interpolator/lch.js +13 -0
  62. simview/static/lib/chroma-js-3.1.2/src/interpolator/lrgb.js +19 -0
  63. simview/static/lib/chroma-js-3.1.2/src/interpolator/num.js +15 -0
  64. simview/static/lib/chroma-js-3.1.2/src/interpolator/oklab.js +19 -0
  65. simview/static/lib/chroma-js-3.1.2/src/interpolator/oklch.js +12 -0
  66. simview/static/lib/chroma-js-3.1.2/src/interpolator/rgb.js +18 -0
  67. simview/static/lib/chroma-js-3.1.2/src/io/cmyk/cmyk2rgb.js +16 -0
  68. simview/static/lib/chroma-js-3.1.2/src/io/cmyk/index.js +27 -0
  69. simview/static/lib/chroma-js-3.1.2/src/io/cmyk/rgb2cmyk.js +17 -0
  70. simview/static/lib/chroma-js-3.1.2/src/io/css/css2rgb.js +238 -0
  71. simview/static/lib/chroma-js-3.1.2/src/io/css/hsl2css.js +26 -0
  72. simview/static/lib/chroma-js-3.1.2/src/io/css/index.js +27 -0
  73. simview/static/lib/chroma-js-3.1.2/src/io/css/lab2css.js +24 -0
  74. simview/static/lib/chroma-js-3.1.2/src/io/css/lch2css.js +24 -0
  75. simview/static/lib/chroma-js-3.1.2/src/io/css/oklab2css.js +16 -0
  76. simview/static/lib/chroma-js-3.1.2/src/io/css/oklch2css.js +16 -0
  77. simview/static/lib/chroma-js-3.1.2/src/io/css/rgb2css.js +61 -0
  78. simview/static/lib/chroma-js-3.1.2/src/io/gl/index.js +22 -0
  79. simview/static/lib/chroma-js-3.1.2/src/io/hcg/hcg2rgb.js +55 -0
  80. simview/static/lib/chroma-js-3.1.2/src/io/hcg/index.js +27 -0
  81. simview/static/lib/chroma-js-3.1.2/src/io/hcg/rgb2hcg.js +23 -0
  82. simview/static/lib/chroma-js-3.1.2/src/io/hex/hex2rgb.js +56 -0
  83. simview/static/lib/chroma-js-3.1.2/src/io/hex/index.js +29 -0
  84. simview/static/lib/chroma-js-3.1.2/src/io/hex/rgb2hex.js +29 -0
  85. simview/static/lib/chroma-js-3.1.2/src/io/hsi/hsi2rgb.js +45 -0
  86. simview/static/lib/chroma-js-3.1.2/src/io/hsi/index.js +27 -0
  87. simview/static/lib/chroma-js-3.1.2/src/io/hsi/rgb2hsi.js +31 -0
  88. simview/static/lib/chroma-js-3.1.2/src/io/hsl/hsl2rgb.js +35 -0
  89. simview/static/lib/chroma-js-3.1.2/src/io/hsl/index.js +27 -0
  90. simview/static/lib/chroma-js-3.1.2/src/io/hsl/rgb2hsl.js +45 -0
  91. simview/static/lib/chroma-js-3.1.2/src/io/hsv/hsv2rgb.js +47 -0
  92. simview/static/lib/chroma-js-3.1.2/src/io/hsv/index.js +27 -0
  93. simview/static/lib/chroma-js-3.1.2/src/io/hsv/rgb2hsv.js +32 -0
  94. simview/static/lib/chroma-js-3.1.2/src/io/input.js +4 -0
  95. simview/static/lib/chroma-js-3.1.2/src/io/lab/index.js +28 -0
  96. simview/static/lib/chroma-js-3.1.2/src/io/lab/lab-constants.js +120 -0
  97. simview/static/lib/chroma-js-3.1.2/src/io/lab/lab2rgb.js +101 -0
  98. simview/static/lib/chroma-js-3.1.2/src/io/lab/rgb2lab.js +67 -0
  99. simview/static/lib/chroma-js-3.1.2/src/io/lch/hcl2rgb.js +9 -0
  100. simview/static/lib/chroma-js-3.1.2/src/io/lch/index.js +35 -0
  101. simview/static/lib/chroma-js-3.1.2/src/io/lch/lab2lch.js +12 -0
  102. simview/static/lib/chroma-js-3.1.2/src/io/lch/lch2lab.js +18 -0
  103. simview/static/lib/chroma-js-3.1.2/src/io/lch/lch2rgb.js +13 -0
  104. simview/static/lib/chroma-js-3.1.2/src/io/lch/rgb2lch.js +12 -0
  105. simview/static/lib/chroma-js-3.1.2/src/io/named/index.js +30 -0
  106. simview/static/lib/chroma-js-3.1.2/src/io/num/index.js +32 -0
  107. simview/static/lib/chroma-js-3.1.2/src/io/num/num2rgb.js +13 -0
  108. simview/static/lib/chroma-js-3.1.2/src/io/num/rgb2num.js +8 -0
  109. simview/static/lib/chroma-js-3.1.2/src/io/oklab/index.js +27 -0
  110. simview/static/lib/chroma-js-3.1.2/src/io/oklab/oklab2rgb.js +34 -0
  111. simview/static/lib/chroma-js-3.1.2/src/io/oklab/rgb2oklab.js +37 -0
  112. simview/static/lib/chroma-js-3.1.2/src/io/oklch/index.js +27 -0
  113. simview/static/lib/chroma-js-3.1.2/src/io/oklch/oklch2rgb.js +13 -0
  114. simview/static/lib/chroma-js-3.1.2/src/io/oklch/rgb2oklch.js +12 -0
  115. simview/static/lib/chroma-js-3.1.2/src/io/rgb/index.js +44 -0
  116. simview/static/lib/chroma-js-3.1.2/src/io/temp/index.js +22 -0
  117. simview/static/lib/chroma-js-3.1.2/src/io/temp/rgb2temperature.js +30 -0
  118. simview/static/lib/chroma-js-3.1.2/src/io/temp/temperature2rgb.js +39 -0
  119. simview/static/lib/chroma-js-3.1.2/src/ops/alpha.js +13 -0
  120. simview/static/lib/chroma-js-3.1.2/src/ops/clipped.js +5 -0
  121. simview/static/lib/chroma-js-3.1.2/src/ops/darken.js +17 -0
  122. simview/static/lib/chroma-js-3.1.2/src/ops/get.js +13 -0
  123. simview/static/lib/chroma-js-3.1.2/src/ops/luminance.js +54 -0
  124. simview/static/lib/chroma-js-3.1.2/src/ops/mix.js +10 -0
  125. simview/static/lib/chroma-js-3.1.2/src/ops/premultiply.js +12 -0
  126. simview/static/lib/chroma-js-3.1.2/src/ops/saturate.js +15 -0
  127. simview/static/lib/chroma-js-3.1.2/src/ops/set.js +43 -0
  128. simview/static/lib/chroma-js-3.1.2/src/ops/shade.js +11 -0
  129. simview/static/lib/chroma-js-3.1.2/src/utils/analyze.js +191 -0
  130. simview/static/lib/chroma-js-3.1.2/src/utils/clip_rgb.js +15 -0
  131. simview/static/lib/chroma-js-3.1.2/src/utils/contrast.js +12 -0
  132. simview/static/lib/chroma-js-3.1.2/src/utils/contrastAPCA.js +68 -0
  133. simview/static/lib/chroma-js-3.1.2/src/utils/delta-e.js +62 -0
  134. simview/static/lib/chroma-js-3.1.2/src/utils/distance.js +17 -0
  135. simview/static/lib/chroma-js-3.1.2/src/utils/index.js +27 -0
  136. simview/static/lib/chroma-js-3.1.2/src/utils/last.js +8 -0
  137. simview/static/lib/chroma-js-3.1.2/src/utils/limit.js +5 -0
  138. simview/static/lib/chroma-js-3.1.2/src/utils/multiply-matrices.js +36 -0
  139. simview/static/lib/chroma-js-3.1.2/src/utils/scales.js +15 -0
  140. simview/static/lib/chroma-js-3.1.2/src/utils/type.js +18 -0
  141. simview/static/lib/chroma-js-3.1.2/src/utils/unpack.js +17 -0
  142. simview/static/lib/chroma-js-3.1.2/src/utils/valid.js +11 -0
  143. simview/static/lib/chroma-js-3.1.2/src/version.js +2 -0
  144. simview/static/lib/download.js +132 -0
  145. simview/static/lib/js-colormaps.js +19335 -0
  146. simview/static/lib/tar.js +334 -0
  147. simview/static/lib/three-0.174.0/addons/controls/OrbitControls.js +1556 -0
  148. simview/static/lib/three-0.174.0/addons/libs/lil-gui.module.min.js +8 -0
  149. simview/static/lib/three-0.174.0/three.core.js +48830 -0
  150. simview/static/lib/three-0.174.0/three.module.js +17313 -0
  151. simview/static/lib/uPlot.esm.js +6140 -0
  152. simview/static/lib/uPlot.min.css +2 -0
  153. simview/static/textures/contacts/red-cross0.png +0 -0
  154. simview/static/textures/points/ball0.png +0 -0
  155. simview/static/textures/points/ball1.png +0 -0
  156. simview/templates/index.html +81 -0
  157. simview/utils.py +41 -0
  158. simview-3.2.dist-info/METADATA +552 -0
  159. simview-3.2.dist-info/RECORD +163 -0
  160. simview-3.2.dist-info/WHEEL +5 -0
  161. simview-3.2.dist-info/entry_points.txt +2 -0
  162. simview-3.2.dist-info/licenses/LICENSE +28 -0
  163. 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
+ }