simview 6.2.1__tar.gz → 6.2.2__tar.gz
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-6.2.1/simview.egg-info → simview-6.2.2}/PKG-INFO +1 -1
- {simview-6.2.1 → simview-6.2.2}/pyproject.toml +1 -1
- {simview-6.2.1 → simview-6.2.2}/simview/columnar.py +31 -16
- {simview-6.2.1 → simview-6.2.2}/simview/merge.py +8 -16
- {simview-6.2.1 → simview-6.2.2}/simview/server.py +20 -39
- {simview-6.2.1 → simview-6.2.2}/simview/static/css/controls.css +0 -1
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/SimView.js +15 -13
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/components/AnimationController.js +0 -16
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/components/StateStore.js +7 -16
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/config.js +0 -4
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/objects/Body.js +3 -9
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/objects/colormap.js +3 -2
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/objects/utils.js +4 -15
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/ui/AnalysisPanel.js +0 -12
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/ui/BatchLegend.js +1 -3
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/ui/BodyStateWindow.js +21 -40
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/ui/Controls.js +38 -94
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/ui/ErrorMetrics.js +41 -58
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/ui/Legend.js +4 -7
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/ui/PlaybackControls.js +7 -7
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/ui/ScalarPlotter.js +5 -4
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/ui/TerrainProfile.js +6 -6
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/ui/chartControls.js +25 -12
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/utils/episodes.js +3 -3
- simview-6.2.2/simview/static/js/utils/html.js +5 -0
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/utils/scalarSeries.js +2 -2
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/utils/terrainSample.js +6 -8
- {simview-6.2.1 → simview-6.2.2}/simview/static/lib/js-colormaps.js +3 -30
- {simview-6.2.1 → simview-6.2.2/simview.egg-info}/PKG-INFO +1 -1
- {simview-6.2.1 → simview-6.2.2}/simview.egg-info/SOURCES.txt +1 -0
- {simview-6.2.1 → simview-6.2.2}/LICENSE +0 -0
- {simview-6.2.1 → simview-6.2.2}/MANIFEST.in +0 -0
- {simview-6.2.1 → simview-6.2.2}/README.md +0 -0
- {simview-6.2.1 → simview-6.2.2}/setup.cfg +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/__init__.py +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/__main__.py +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/diff.py +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/info.py +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/launcher.py +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/live.py +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/model.py +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/py.typed +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/remote.py +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/render.py +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/scene.py +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/state.py +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/components/BatchManager.js +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/components/InteractionController.js +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/components/Scene.js +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/components/WindowedField.js +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/main.js +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/objects/Polyline.js +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/objects/StaticObject.js +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/objects/Terrain.js +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/objects/similarity.js +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/utils/batchPresets.js +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/utils/batchVisibility.js +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/utils/blobCodec.js +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/utils/blobWindow.js +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/utils/bodyTransforms.js +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/utils/cameraRange.js +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/utils/csv.js +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/utils/errorMath.js +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/utils/interpolate.js +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/utils/keyboard.js +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/utils/liveFollow.js +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/utils/uplot.js +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/utils/viewState.js +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/utils/viewerDefaults.js +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/static/js/utils/visibleRanges.js +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/static/lib/three-0.174.0/addons/controls/OrbitControls.js +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/static/lib/three-0.174.0/addons/libs/lil-gui.module.min.js +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/static/lib/three-0.174.0/three.core.js +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/static/lib/three-0.174.0/three.module.js +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/static/lib/uPlot.esm.js +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/static/lib/uPlot.min.css +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/static/textures/contacts/red-cross0.png +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/static/textures/points/ball1.png +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/templates/index.html +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/terrain.py +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/utils.py +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview/view.py +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview.egg-info/dependency_links.txt +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview.egg-info/entry_points.txt +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview.egg-info/requires.txt +0 -0
- {simview-6.2.1 → simview-6.2.2}/simview.egg-info/top_level.txt +0 -0
- {simview-6.2.1 → simview-6.2.2}/tests/test_batch_selection.py +0 -0
- {simview-6.2.1 → simview-6.2.2}/tests/test_cli.py +0 -0
- {simview-6.2.1 → simview-6.2.2}/tests/test_columnar_ondisk.py +0 -0
- {simview-6.2.1 → simview-6.2.2}/tests/test_columnar_states.py +0 -0
- {simview-6.2.1 → simview-6.2.2}/tests/test_contacts.py +0 -0
- {simview-6.2.1 → simview-6.2.2}/tests/test_diff.py +0 -0
- {simview-6.2.1 → simview-6.2.2}/tests/test_embed_defaults.py +0 -0
- {simview-6.2.1 → simview-6.2.2}/tests/test_episodes.py +0 -0
- {simview-6.2.1 → simview-6.2.2}/tests/test_info.py +0 -0
- {simview-6.2.1 → simview-6.2.2}/tests/test_launcher.py +0 -0
- {simview-6.2.1 → simview-6.2.2}/tests/test_live.py +0 -0
- {simview-6.2.1 → simview-6.2.2}/tests/test_merge.py +0 -0
- {simview-6.2.1 → simview-6.2.2}/tests/test_pointcloud_features.py +0 -0
- {simview-6.2.1 → simview-6.2.2}/tests/test_remote.py +0 -0
- {simview-6.2.1 → simview-6.2.2}/tests/test_roundtrip.py +0 -0
- {simview-6.2.1 → simview-6.2.2}/tests/test_scene.py +0 -0
- {simview-6.2.1 → simview-6.2.2}/tests/test_server.py +0 -0
- {simview-6.2.1 → simview-6.2.2}/tests/test_show.py +0 -0
- {simview-6.2.1 → simview-6.2.2}/tests/test_terrain.py +0 -0
- {simview-6.2.1 → simview-6.2.2}/tests/test_terrain_query.py +0 -0
- {simview-6.2.1 → simview-6.2.2}/tests/test_trajectory.py +0 -0
- {simview-6.2.1 → simview-6.2.2}/tests/test_utils.py +0 -0
- {simview-6.2.1 → simview-6.2.2}/tests/test_view.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: simview
|
|
3
|
-
Version: 6.2.
|
|
3
|
+
Version: 6.2.2
|
|
4
4
|
Summary: THREE.js-based simulation visualization tool
|
|
5
5
|
Author-email: Jan Vlk <vlkjan6@fel.cvut.cz>, Ales Kucera <kuceral4@fel.cvut.cz>, David Korcak <david.korcak@gmail.com>
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -12,7 +12,7 @@ same document; only how a blob is referenced differs:
|
|
|
12
12
|
URL the browser fetches in parallel as a `Float32Array`.
|
|
13
13
|
|
|
14
14
|
That symmetry is the whole point: a columnar file needs no repacking at all
|
|
15
|
-
to be served, and `
|
|
15
|
+
to be served, and `columnar.rewrite_blobs` (shared by `server.py`) does the
|
|
16
16
|
rewriting for free.
|
|
17
17
|
|
|
18
18
|
Shape (see README.md "JSON Format Specification" for the authoritative spec)::
|
|
@@ -122,6 +122,29 @@ def encode_floats(flat) -> str:
|
|
|
122
122
|
return inline_blob(struct.pack(f"<{len(flat)}f", *flat))
|
|
123
123
|
|
|
124
124
|
|
|
125
|
+
def batch_rows(flat: list[float], per_row: int, batch_size: int) -> list | None:
|
|
126
|
+
"""Split a decoded blob into `batch_size` rows of `per_row` floats: either
|
|
127
|
+
one row shared by every batch (replicated) or one row per batch. None if
|
|
128
|
+
`flat` is neither length -- callers raise their own error."""
|
|
129
|
+
if len(flat) == per_row:
|
|
130
|
+
return [flat] * batch_size
|
|
131
|
+
if len(flat) == batch_size * per_row:
|
|
132
|
+
return [flat[i : i + per_row] for i in range(0, len(flat), per_row)]
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def rewrite_blobs(obj: Any, register_blob) -> None:
|
|
137
|
+
"""Replace, in place, every inline ``__b64__`` blob string nested anywhere
|
|
138
|
+
in `obj` with `register_blob(raw_bytes)`."""
|
|
139
|
+
if not isinstance(obj, (dict, list)):
|
|
140
|
+
return
|
|
141
|
+
for k, v in obj.items() if isinstance(obj, dict) else enumerate(obj):
|
|
142
|
+
if is_blob(v):
|
|
143
|
+
obj[k] = register_blob(blob_bytes(v))
|
|
144
|
+
else:
|
|
145
|
+
rewrite_blobs(v, register_blob)
|
|
146
|
+
|
|
147
|
+
|
|
125
148
|
def decode_transform_row(value: Any, batch_size: int, batch_idx: int) -> list[float]:
|
|
126
149
|
"""Decode one state's `bodyTransform` field value for a single batch into
|
|
127
150
|
a flat 7-element `[x, y, z, w, qx, qy, qz]` row, without numpy.
|
|
@@ -134,8 +157,6 @@ def decode_transform_row(value: Any, batch_size: int, batch_idx: int) -> list[fl
|
|
|
134
157
|
if len(flat) == batch_size * width:
|
|
135
158
|
start = batch_idx * width
|
|
136
159
|
return flat[start : start + width]
|
|
137
|
-
if len(flat) == width and batch_size == 1:
|
|
138
|
-
return flat
|
|
139
160
|
raise ValueError(
|
|
140
161
|
f"bodyTransform has {len(flat)} floats; expected {width} "
|
|
141
162
|
f"(batch_size=1, flat) or {batch_size * width} "
|
|
@@ -451,23 +472,17 @@ def write_static_bundle(data: dict, directory) -> None:
|
|
|
451
472
|
(out / "blob").mkdir(parents=True, exist_ok=True)
|
|
452
473
|
count = 0
|
|
453
474
|
|
|
454
|
-
def
|
|
475
|
+
def write_blob(raw: bytes) -> str:
|
|
455
476
|
nonlocal count
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
obj[k] = f"/blob/{count}"
|
|
462
|
-
count += 1
|
|
463
|
-
else:
|
|
464
|
-
externalize(v)
|
|
465
|
-
|
|
466
|
-
externalize(data["model"])
|
|
477
|
+
(out / "blob" / str(count)).write_bytes(raw)
|
|
478
|
+
count += 1
|
|
479
|
+
return f"/blob/{count - 1}"
|
|
480
|
+
|
|
481
|
+
rewrite_blobs(data["model"], write_blob)
|
|
467
482
|
# The viewer only resolves /blob/ URLs in the model and a columnar states
|
|
468
483
|
# object; a legacy per-frame array's inline __b64__ fields are decoded
|
|
469
484
|
# in place, so they must stay inline.
|
|
470
485
|
if is_columnar(data["states"]):
|
|
471
|
-
|
|
486
|
+
rewrite_blobs(data["states"], write_blob)
|
|
472
487
|
for name in ("model", "states"):
|
|
473
488
|
(out / f"{name}.json").write_text(json.dumps(data[name]))
|
|
@@ -27,6 +27,7 @@ import orjson
|
|
|
27
27
|
|
|
28
28
|
from .columnar import (
|
|
29
29
|
STATE_FIELD_WIDTHS,
|
|
30
|
+
batch_rows,
|
|
30
31
|
blob_floats,
|
|
31
32
|
encode_floats,
|
|
32
33
|
expand_columnar_states,
|
|
@@ -212,12 +213,8 @@ def _decode_per_batch(
|
|
|
212
213
|
return value
|
|
213
214
|
flat = blob_floats(value)
|
|
214
215
|
per_row = resolution * (vector_width or 1)
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
chunks = [list(flat)] * batch_size
|
|
218
|
-
elif len(flat) == batch_size * per_row:
|
|
219
|
-
chunks = [list(flat[i : i + per_row]) for i in range(0, len(flat), per_row)]
|
|
220
|
-
else:
|
|
216
|
+
chunks = batch_rows(flat, per_row, batch_size)
|
|
217
|
+
if chunks is None:
|
|
221
218
|
raise ValueError(
|
|
222
219
|
f"terrain blob has {len(flat)} floats; expected {per_row} (one "
|
|
223
220
|
f"shared row) or {batch_size * per_row} ({batch_size} batches "
|
|
@@ -679,15 +676,6 @@ def _nearest_index(sorted_times: list[float], t: float) -> int:
|
|
|
679
676
|
return i - 1 if (t - before) <= (after - t) else i
|
|
680
677
|
|
|
681
678
|
|
|
682
|
-
def _state_body_lookup(
|
|
683
|
-
states: list[dict], file_idx: int, state_idx: int, cache: dict
|
|
684
|
-
) -> dict[str, dict]:
|
|
685
|
-
key = (file_idx, state_idx)
|
|
686
|
-
if key not in cache:
|
|
687
|
-
cache[key] = bodies_by_name(states[state_idx].get("bodies"))
|
|
688
|
-
return cache[key]
|
|
689
|
-
|
|
690
|
-
|
|
691
679
|
def _merge_states(
|
|
692
680
|
states_list: list[list[dict]],
|
|
693
681
|
batch_sizes: list[int],
|
|
@@ -731,7 +719,11 @@ def _merge_states(
|
|
|
731
719
|
zip(states_list, batch_sizes, selections)
|
|
732
720
|
):
|
|
733
721
|
state_idx = state_idx_by_file[file_idx]
|
|
734
|
-
lookup =
|
|
722
|
+
lookup = lookup_cache.get((file_idx, state_idx))
|
|
723
|
+
if lookup is None:
|
|
724
|
+
lookup = lookup_cache[(file_idx, state_idx)] = bodies_by_name(
|
|
725
|
+
states[state_idx].get("bodies")
|
|
726
|
+
)
|
|
735
727
|
body_state = lookup.get(name)
|
|
736
728
|
if body_state is None:
|
|
737
729
|
raise ValueError(
|
|
@@ -18,12 +18,12 @@ import orjson
|
|
|
18
18
|
import uvicorn
|
|
19
19
|
from fastapi import FastAPI, Request, Response, WebSocket, WebSocketDisconnect
|
|
20
20
|
from fastapi.middleware.cors import CORSMiddleware
|
|
21
|
-
from fastapi.responses import FileResponse
|
|
21
|
+
from fastapi.responses import FileResponse, JSONResponse
|
|
22
22
|
from fastapi.staticfiles import StaticFiles
|
|
23
23
|
from pydantic import BaseModel
|
|
24
24
|
from starlette.types import Scope
|
|
25
25
|
|
|
26
|
-
from simview.columnar import
|
|
26
|
+
from simview.columnar import columnarize_states, is_columnar, rewrite_blobs
|
|
27
27
|
from simview.utils import dialable_host, find_free_port, read_maybe_gzipped_bytes
|
|
28
28
|
|
|
29
29
|
logger = logging.getLogger("simview.server")
|
|
@@ -95,6 +95,10 @@ def _parse_byte_range(header: str | None, size: int) -> tuple[int, int] | None:
|
|
|
95
95
|
return start, min(end, size - 1)
|
|
96
96
|
|
|
97
97
|
|
|
98
|
+
def _not_found(what: str) -> JSONResponse:
|
|
99
|
+
return JSONResponse({"message": f"{what} data not available"}, status_code=404)
|
|
100
|
+
|
|
101
|
+
|
|
98
102
|
class BatchNamesRequest(BaseModel):
|
|
99
103
|
names: list[str]
|
|
100
104
|
|
|
@@ -192,9 +196,7 @@ class SimViewServer:
|
|
|
192
196
|
self.app = FastAPI(lifespan=lifespan)
|
|
193
197
|
# Instance-scoped state (self.model_data, self.model_bytes, ...) lives on this
|
|
194
198
|
# object rather than in module-level globals, so multiple SimViewServer
|
|
195
|
-
# instances (e.g. in tests) never share or clobber each other's data.
|
|
196
|
-
# also mirrored onto app.state for the FastAPI-idiomatic access pattern.
|
|
197
|
-
self.app.state.server = self
|
|
199
|
+
# instances (e.g. in tests) never share or clobber each other's data.
|
|
198
200
|
|
|
199
201
|
# Local viewer only: restrict cross-origin requests to localhost/127.0.0.1.
|
|
200
202
|
# No allow_credentials: the API doesn't use cookies or auth headers, and
|
|
@@ -225,6 +227,9 @@ class SimViewServer:
|
|
|
225
227
|
|
|
226
228
|
self.setup_routes()
|
|
227
229
|
|
|
230
|
+
def _pack_model(self) -> None:
|
|
231
|
+
self.model_bytes = gzip.compress(orjson.dumps(self.model_data), compresslevel=1)
|
|
232
|
+
|
|
228
233
|
def _names_sidecar_path(self) -> Path | None:
|
|
229
234
|
"""Where custom batch names get persisted, so they survive a server restart.
|
|
230
235
|
|
|
@@ -273,7 +278,7 @@ class SimViewServer:
|
|
|
273
278
|
# In-memory callers (SimulationScene.show, LiveViewer, SimViewLauncher)
|
|
274
279
|
# hand over dicts that still share structure with the live scene --
|
|
275
280
|
# SimViewBody.to_json() returns `shape` by reference, for example.
|
|
276
|
-
#
|
|
281
|
+
# rewrite_blobs below rewrites `__b64__` strings in place, which would
|
|
277
282
|
# otherwise corrupt the caller's scene (a later scene.save() would
|
|
278
283
|
# write dead "/blob/..." URLs, and a second show() would serve stale
|
|
279
284
|
# blob references). Deep-copying just the model is cheap: the big
|
|
@@ -323,16 +328,6 @@ class SimViewServer:
|
|
|
323
328
|
self.blobs.append(raw)
|
|
324
329
|
return f"/blob/{self._blob_token}/{blob_id}"
|
|
325
330
|
|
|
326
|
-
def extract_blobs(obj):
|
|
327
|
-
"""Rewrite every inline `__b64__` blob in `obj` into a /blob/ URL."""
|
|
328
|
-
if not isinstance(obj, (dict, list)):
|
|
329
|
-
return
|
|
330
|
-
for k, v in obj.items() if isinstance(obj, dict) else enumerate(obj):
|
|
331
|
-
if is_blob(v):
|
|
332
|
-
obj[k] = register_blob(blob_bytes(v))
|
|
333
|
-
else:
|
|
334
|
-
extract_blobs(v)
|
|
335
|
-
|
|
336
331
|
self.model_data = model_data
|
|
337
332
|
|
|
338
333
|
if model_data is not None:
|
|
@@ -343,15 +338,15 @@ class SimViewServer:
|
|
|
343
338
|
for k in ("metadata", "batchNames")
|
|
344
339
|
if k in model_data
|
|
345
340
|
}
|
|
346
|
-
|
|
341
|
+
rewrite_blobs(model_data, register_blob)
|
|
347
342
|
model_data.update(keep)
|
|
348
343
|
|
|
349
344
|
if is_columnar(states_data):
|
|
350
345
|
# The file is already columnar (SimulationScene.save's default) --
|
|
351
346
|
# no repack at all, just rewrite its inline __b64__ blobs into
|
|
352
|
-
# /blob/ URLs, which is exactly what
|
|
347
|
+
# /blob/ URLs, which is exactly what rewrite_blobs does. This is
|
|
353
348
|
# the cheap path the on-disk format exists for.
|
|
354
|
-
|
|
349
|
+
rewrite_blobs(states_data, register_blob)
|
|
355
350
|
elif isinstance(states_data, list) and states_data:
|
|
356
351
|
# Legacy per-frame file: repack into whole-trajectory columnar
|
|
357
352
|
# blobs so the viewer parses one lightweight JSON index plus raw
|
|
@@ -371,7 +366,7 @@ class SimViewServer:
|
|
|
371
366
|
# itself is kept around (it's small, unlike states_data) so /batch-names can
|
|
372
367
|
# patch and re-serialize it without re-reading the source file.
|
|
373
368
|
if model_data is not None:
|
|
374
|
-
self.
|
|
369
|
+
self._pack_model()
|
|
375
370
|
if self.live:
|
|
376
371
|
# Live mode: frames arrive over /ws/states instead, so /states just
|
|
377
372
|
# tells the client to open the socket (see loadData in SimView.js).
|
|
@@ -401,11 +396,7 @@ class SimViewServer:
|
|
|
401
396
|
media_type="application/json",
|
|
402
397
|
headers=_gzip_headers,
|
|
403
398
|
)
|
|
404
|
-
return
|
|
405
|
-
content=b'{"message":"Model data not available"}',
|
|
406
|
-
media_type="application/json",
|
|
407
|
-
status_code=404,
|
|
408
|
-
)
|
|
399
|
+
return _not_found("Model")
|
|
409
400
|
|
|
410
401
|
@self.app.get("/states")
|
|
411
402
|
async def get_states():
|
|
@@ -416,11 +407,7 @@ class SimViewServer:
|
|
|
416
407
|
media_type="application/json",
|
|
417
408
|
headers=_gzip_headers,
|
|
418
409
|
)
|
|
419
|
-
return
|
|
420
|
-
content=b'{"message":"States data not available"}',
|
|
421
|
-
media_type="application/json",
|
|
422
|
-
status_code=404,
|
|
423
|
-
)
|
|
410
|
+
return _not_found("States")
|
|
424
411
|
|
|
425
412
|
@self.app.get("/blob/{token}/{blob_id}")
|
|
426
413
|
async def get_blob(token: str, blob_id: int, request: Request):
|
|
@@ -462,11 +449,7 @@ class SimViewServer:
|
|
|
462
449
|
@self.app.post("/batch-names")
|
|
463
450
|
async def set_batch_names(body: BatchNamesRequest):
|
|
464
451
|
if self.model_data is None:
|
|
465
|
-
return
|
|
466
|
-
content=b'{"message":"Model data not available"}',
|
|
467
|
-
media_type="application/json",
|
|
468
|
-
status_code=404,
|
|
469
|
-
)
|
|
452
|
+
return _not_found("Model")
|
|
470
453
|
names = body.names
|
|
471
454
|
sim_batches = int(self.model_data.get("simBatches", 1))
|
|
472
455
|
if len(names) != sim_batches:
|
|
@@ -477,9 +460,7 @@ class SimViewServer:
|
|
|
477
460
|
)
|
|
478
461
|
|
|
479
462
|
self.model_data["batchNames"] = names
|
|
480
|
-
self.
|
|
481
|
-
orjson.dumps(self.model_data), compresslevel=1
|
|
482
|
-
)
|
|
463
|
+
self._pack_model()
|
|
483
464
|
|
|
484
465
|
names_path = self._names_sidecar_path()
|
|
485
466
|
persisted = False
|
|
@@ -563,7 +544,7 @@ class SimViewServer:
|
|
|
563
544
|
if self.model_data is None:
|
|
564
545
|
return
|
|
565
546
|
self.model_data["episodes"] = episodes
|
|
566
|
-
self.
|
|
547
|
+
self._pack_model()
|
|
567
548
|
|
|
568
549
|
async def broadcast_episodes(self, episodes: list[dict]) -> None:
|
|
569
550
|
"""Push updated episode boundaries to every connected /ws/states client.
|
|
@@ -19,8 +19,9 @@ import { InteractionController } from "./components/InteractionController.js";
|
|
|
19
19
|
import { buildBodyMeta, resolveStateBodies, topoSortBodies } from "./utils/bodyTransforms.js";
|
|
20
20
|
import { hasBodyTrajectory } from "./utils/terrainSample.js";
|
|
21
21
|
import { decodeStatesChunk, STATE_FIELD_WIDTHS } from "./utils/blobCodec.js";
|
|
22
|
-
import {
|
|
22
|
+
import { ColumnarStateStore, LegacyStateStore } from "./components/StateStore.js";
|
|
23
23
|
import { WindowedField } from "./components/WindowedField.js";
|
|
24
|
+
import { escapeHtml } from "./utils/html.js";
|
|
24
25
|
import { bytesPerFrame, shouldWindowField } from "./utils/blobWindow.js";
|
|
25
26
|
import { shouldFollowLive } from "./utils/liveFollow.js";
|
|
26
27
|
import { shiftEpisodes } from "./utils/episodes.js";
|
|
@@ -144,7 +145,7 @@ export class SimView {
|
|
|
144
145
|
// Live streaming mode (see simview.live.LiveViewer): no states
|
|
145
146
|
// yet, they arrive incrementally over /ws/states instead.
|
|
146
147
|
console.debug("Live mode: opening /ws/states");
|
|
147
|
-
this.store =
|
|
148
|
+
this.store = new LegacyStateStore([]);
|
|
148
149
|
this.startLiveStream(splash);
|
|
149
150
|
} else if (statesPayload && statesPayload.version === 4) {
|
|
150
151
|
// Columnar wire shape (see server.py::_columnarize_states):
|
|
@@ -158,7 +159,7 @@ export class SimView {
|
|
|
158
159
|
console.time("fetch_state_blobs");
|
|
159
160
|
await this.fetchBlobs(statesPayload);
|
|
160
161
|
console.timeEnd("fetch_state_blobs");
|
|
161
|
-
this.store =
|
|
162
|
+
this.store = new ColumnarStateStore(statesPayload, this.batchManager.simBatches);
|
|
162
163
|
this.onStoreReady();
|
|
163
164
|
} else {
|
|
164
165
|
throw new Error("Unrecognized /states payload shape");
|
|
@@ -172,7 +173,7 @@ export class SimView {
|
|
|
172
173
|
} catch (error) {
|
|
173
174
|
console.error("Critical error during initial data fetch:", error);
|
|
174
175
|
if (splash) {
|
|
175
|
-
splash.innerHTML = `<h1 class="sv-splash-error">Couldn't load the simulation</h1><p>${error.message}</p><p>See the browser console for details.</p>`;
|
|
176
|
+
splash.innerHTML = `<h1 class="sv-splash-error">Couldn't load the simulation</h1><p>${escapeHtml(error.message)}</p><p>See the browser console for details.</p>`;
|
|
176
177
|
}
|
|
177
178
|
throw error;
|
|
178
179
|
}
|
|
@@ -354,7 +355,7 @@ export class SimView {
|
|
|
354
355
|
decodeStatesChunk(chunk);
|
|
355
356
|
const firstLoad = !this.animationController || !this.animationController.store;
|
|
356
357
|
if (!this.store) {
|
|
357
|
-
this.store =
|
|
358
|
+
this.store = new LegacyStateStore(chunk);
|
|
358
359
|
} else if (firstLoad) {
|
|
359
360
|
this.store.append(chunk);
|
|
360
361
|
} else {
|
|
@@ -417,7 +418,7 @@ export class SimView {
|
|
|
417
418
|
this.analysisPanel = new AnalysisPanel(this);
|
|
418
419
|
}
|
|
419
420
|
this.terrainProfile = new TerrainProfile(this);
|
|
420
|
-
this.analysisPanel.
|
|
421
|
+
this.analysisPanel.attach("terrain", this.terrainProfile, this.terrainProfile.content);
|
|
421
422
|
}
|
|
422
423
|
|
|
423
424
|
// Resolves and records position/orientation (and contacts, vectors, ...)
|
|
@@ -540,11 +541,12 @@ export class SimView {
|
|
|
540
541
|
model.scalarNames
|
|
541
542
|
);
|
|
542
543
|
this.scalarPlotter = new ScalarPlotter(this, model.scalarNames);
|
|
543
|
-
this.
|
|
544
|
+
const { tabBar, plotArea } = this.scalarPlotter;
|
|
545
|
+
this.analysisPanel.attach("scalars", this.scalarPlotter, tabBar, plotArea);
|
|
544
546
|
}
|
|
545
547
|
if (hasErrorMetrics) {
|
|
546
548
|
this.errorMetrics = new ErrorMetrics(this);
|
|
547
|
-
this.analysisPanel.
|
|
549
|
+
this.analysisPanel.attach("errorMetrics", this.errorMetrics, this.errorMetrics.content);
|
|
548
550
|
}
|
|
549
551
|
this.interactionController = new InteractionController(this);
|
|
550
552
|
this.uiControls = new UIControls(this);
|
|
@@ -559,7 +561,7 @@ export class SimView {
|
|
|
559
561
|
console.error("Error during initFromModel:", error);
|
|
560
562
|
const splash = document.getElementById("loading-splash");
|
|
561
563
|
if (splash) {
|
|
562
|
-
splash.innerHTML = `<h1 class="sv-splash-error">Error during initialization</h1><p>${error.message}</p>`;
|
|
564
|
+
splash.innerHTML = `<h1 class="sv-splash-error">Error during initialization</h1><p>${escapeHtml(error.message)}</p>`;
|
|
563
565
|
}
|
|
564
566
|
throw error;
|
|
565
567
|
}
|
|
@@ -584,7 +586,7 @@ export class SimView {
|
|
|
584
586
|
console.error("Initialization failed:", error);
|
|
585
587
|
const splash = document.getElementById("loading-splash");
|
|
586
588
|
if (splash) {
|
|
587
|
-
splash.innerHTML = `<h1 class="sv-splash-error">Failed to connect or initialize</h1><p>${error.message}</p>`;
|
|
589
|
+
splash.innerHTML = `<h1 class="sv-splash-error">Failed to connect or initialize</h1><p>${escapeHtml(error.message)}</p>`;
|
|
588
590
|
}
|
|
589
591
|
}
|
|
590
592
|
}
|
|
@@ -662,7 +664,7 @@ export class SimView {
|
|
|
662
664
|
getViewState() {
|
|
663
665
|
const { camera, controls } = this.scene;
|
|
664
666
|
return {
|
|
665
|
-
time: this.animationController ? this.animationController.
|
|
667
|
+
time: this.animationController ? this.animationController.currentTime : undefined,
|
|
666
668
|
camera: {
|
|
667
669
|
position: camera.position.clone(),
|
|
668
670
|
target: controls.target.clone(),
|
|
@@ -770,11 +772,11 @@ export class SimView {
|
|
|
770
772
|
// 1. Update states and time
|
|
771
773
|
if (this.animationController) {
|
|
772
774
|
this.animationController.animate(now);
|
|
773
|
-
const index = this.animationController.
|
|
775
|
+
const index = this.animationController.currentStateIndex;
|
|
774
776
|
if (index !== this._lastDispatchedFrame && this.animationController.store) {
|
|
775
777
|
this._lastDispatchedFrame = index;
|
|
776
778
|
window.dispatchEvent(
|
|
777
|
-
new CustomEvent("simview:frame", { detail: { index, time: this.animationController.
|
|
779
|
+
new CustomEvent("simview:frame", { detail: { index, time: this.animationController.currentTime } })
|
|
778
780
|
);
|
|
779
781
|
}
|
|
780
782
|
}
|
|
@@ -309,22 +309,6 @@ export class AnimationController {
|
|
|
309
309
|
return true;
|
|
310
310
|
}
|
|
311
311
|
|
|
312
|
-
getTotalTime() {
|
|
313
|
-
return this.totalTime;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
getCurrentTime() {
|
|
317
|
-
return this.currentTime;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
getCurrentStateIndex() {
|
|
321
|
-
return this.currentStateIndex;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
getFirstTime() {
|
|
325
|
-
return this.firstTime;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
312
|
// A windowed field (see WindowedField.js) just landed. While playing the
|
|
329
313
|
// next tick picks it up anyway; paused, nothing would re-render the frame
|
|
330
314
|
// the user scrubbed to, so redo it now (dropping the interpolation memo,
|
|
@@ -76,11 +76,11 @@ function validateColumnar(times, bodies, scalars, simBatches) {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
class ColumnarStateStore {
|
|
79
|
+
export class ColumnarStateStore {
|
|
80
80
|
// `bodies`: [{name, fields: {fieldName: Float32Array}, contacts?}], each
|
|
81
81
|
// fields Float32Array already reshaped to flat (T * B * k) row-major.
|
|
82
82
|
// `scalars`: {scalarName: Float32Array}, each flat (T * B) row-major.
|
|
83
|
-
constructor(times, bodies, scalars, simBatches) {
|
|
83
|
+
constructor({ times, bodies, scalars }, simBatches) {
|
|
84
84
|
validateColumnar(times, bodies, scalars, simBatches);
|
|
85
85
|
this.times = times;
|
|
86
86
|
this._bodies = bodies;
|
|
@@ -168,7 +168,8 @@ class ColumnarStateStore {
|
|
|
168
168
|
return state;
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
// Per-batch series for the scalar plotter:
|
|
171
|
+
// Per-batch series for the scalar plotter: one per-frame value array per
|
|
172
|
+
// batch, indexed like `times`.
|
|
172
173
|
// `simBatches` is accepted (and ignored) for API parity with
|
|
173
174
|
// LegacyStateStore.getScalarSeries -- the columnar store already knows
|
|
174
175
|
// its own batch count.
|
|
@@ -179,14 +180,14 @@ class ColumnarStateStore {
|
|
|
179
180
|
for (let i = 0; i < this.length; i++) {
|
|
180
181
|
const base = i * B;
|
|
181
182
|
for (let b = 0; b < B; b++) {
|
|
182
|
-
series[b][i] =
|
|
183
|
+
series[b][i] = flat[base + b];
|
|
183
184
|
}
|
|
184
185
|
}
|
|
185
186
|
return series;
|
|
186
187
|
}
|
|
187
188
|
}
|
|
188
189
|
|
|
189
|
-
class LegacyStateStore {
|
|
190
|
+
export class LegacyStateStore {
|
|
190
191
|
// `statesArray`: the classic array of per-frame state objects (already
|
|
191
192
|
// decoded -- __b64__ fields expanded via decodeStatesChunk).
|
|
192
193
|
constructor(statesArray) {
|
|
@@ -234,7 +235,7 @@ class LegacyStateStore {
|
|
|
234
235
|
if (values[b] === undefined) {
|
|
235
236
|
throw new Error(`Scalar "${name}" not found in state at index ${b}.`);
|
|
236
237
|
}
|
|
237
|
-
series[b].push(
|
|
238
|
+
series[b].push(values[b]);
|
|
238
239
|
}
|
|
239
240
|
}
|
|
240
241
|
return series;
|
|
@@ -250,13 +251,3 @@ class LegacyStateStore {
|
|
|
250
251
|
return startIndex;
|
|
251
252
|
}
|
|
252
253
|
}
|
|
253
|
-
|
|
254
|
-
export class StateStore {
|
|
255
|
-
static fromColumnar({ times, bodies, scalars }, simBatches) {
|
|
256
|
-
return new ColumnarStateStore(times, bodies, scalars, simBatches);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
static fromLegacy(statesArray) {
|
|
260
|
-
return new LegacyStateStore(statesArray);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
@@ -155,22 +155,18 @@ export const BODY_VECTOR_CONFIG = {
|
|
|
155
155
|
// Linear velocity
|
|
156
156
|
velocity: {
|
|
157
157
|
color: THEME.green,
|
|
158
|
-
scale: 1.0,
|
|
159
158
|
},
|
|
160
159
|
// Angular velocity
|
|
161
160
|
angularVelocity: {
|
|
162
161
|
color: THEME.yellow,
|
|
163
|
-
scale: 1.0,
|
|
164
162
|
},
|
|
165
163
|
// Force
|
|
166
164
|
force: {
|
|
167
165
|
color: THEME.peach,
|
|
168
|
-
scale: 1.0,
|
|
169
166
|
},
|
|
170
167
|
// Torque
|
|
171
168
|
torque: {
|
|
172
169
|
color: THEME.red,
|
|
173
|
-
scale: 1.0,
|
|
174
170
|
},
|
|
175
171
|
};
|
|
176
172
|
|
|
@@ -193,7 +193,7 @@ export class Body {
|
|
|
193
193
|
}
|
|
194
194
|
this._trailStateIndex = -1;
|
|
195
195
|
const currentIndex = this.app.animationController
|
|
196
|
-
? this.app.animationController.
|
|
196
|
+
? this.app.animationController.currentStateIndex
|
|
197
197
|
: 0;
|
|
198
198
|
this.updateTrails(currentIndex);
|
|
199
199
|
}
|
|
@@ -679,7 +679,7 @@ export class Body {
|
|
|
679
679
|
this.updateAllInstances();
|
|
680
680
|
|
|
681
681
|
if (this.trails.length > 0 && this.app.animationController) {
|
|
682
|
-
this.updateTrails(this.app.animationController.
|
|
682
|
+
this.updateTrails(this.app.animationController.currentStateIndex);
|
|
683
683
|
}
|
|
684
684
|
}
|
|
685
685
|
|
|
@@ -696,7 +696,6 @@ export class Body {
|
|
|
696
696
|
config
|
|
697
697
|
);
|
|
698
698
|
vector.visible = this.app.uiState.attributeVisible[attr] || false;
|
|
699
|
-
vector.userData = { scale: config.scale };
|
|
700
699
|
batchGroup.add(vector);
|
|
701
700
|
this.bodyVectors[batchIndex].set(attr, vector);
|
|
702
701
|
}
|
|
@@ -706,8 +705,7 @@ export class Body {
|
|
|
706
705
|
updateBodyVector(type, vector, batchIndex) {
|
|
707
706
|
const arrow = this.bodyVectors[batchIndex]?.get(type);
|
|
708
707
|
if (!arrow) return;
|
|
709
|
-
const
|
|
710
|
-
const length = vector.length() * scale;
|
|
708
|
+
const length = vector.length();
|
|
711
709
|
// Written as !(>=) so a NaN vector is treated like a zero one instead
|
|
712
710
|
// of feeding NaN into setDirection/setLength.
|
|
713
711
|
if (!(length >= 1e-6)) {
|
|
@@ -850,10 +848,6 @@ export class Body {
|
|
|
850
848
|
return this.group;
|
|
851
849
|
}
|
|
852
850
|
|
|
853
|
-
getAvailableAttributes() {
|
|
854
|
-
return this.availableAttributes;
|
|
855
|
-
}
|
|
856
|
-
|
|
857
851
|
// Which of the mesh/wireframe/points representations this body actually has,
|
|
858
852
|
// so the UI can only offer visualization modes that will show something.
|
|
859
853
|
// A point-cloud body reports none: its cloud is the object itself, not a
|
|
@@ -29,8 +29,9 @@ export function getCallableFromColorMapName(cmapName) {
|
|
|
29
29
|
return (value) => {
|
|
30
30
|
// A non-finite value (NaN cell in a property blob, NaN embedding)
|
|
31
31
|
// is "no data": neutral grey rather than a colormap end. Finite
|
|
32
|
-
// values are clamped because js-colormaps
|
|
33
|
-
// outside [0, 1]
|
|
32
|
+
// values are clamped because js-colormaps indexes its table with
|
|
33
|
+
// x unchecked -- anything outside [0, 1], including float
|
|
34
|
+
// rounding past 1, reads past the end.
|
|
34
35
|
if (!Number.isFinite(value)) return NO_DATA_COLOR.clone();
|
|
35
36
|
const [r, g, b] = evaluate_cmap(
|
|
36
37
|
Math.min(1, Math.max(0, value)),
|