Skip to content

Changelog

Changelog

All notable changes to this project are documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

Unreleased

7.0.0 - 2026-11-03

Breaking

  • simview terrain --area no longer prints the per-cell grids by default; add --full to get them (see Changed below).

Added

  • simview sample: world-space body poses, stored velocities/forces/torques, contact counts and scalars at chosen frames (--frames 0:2000:100, --t, or 11 evenly spaced by default), as text, single-line JSON or CSV.
  • simview info reports value statistics per batch: scalar min/mean/max/percentiles, body world-space position bounds, and contact frame counts. It also lists episodes and the polyline count; --full lifts the 20-entry list cap.
  • simview terrain --area reports per-layer statistics (count, NaN count, min, p05/p50/p95, mean, max, std); with --batches A B it adds both batches' statistics plus delta mean/RMSE/p95. --mask "known>0.5" restricts the statistics to cells passing a test.
  • Batches can be given by name wherever an index is taken (terrain --batch, --batches, diff --batches, sample --batch, render --batch).
  • simview diff accepts several files, or file#batches selections, and merges them first (simview diff gt.json pred.json). --batches is optional for a 2-batch scene. New --t0/--t1 time window and --align-first-frame. terrain --along-body also takes --t0/--t1.
  • simview render: --t (repeatable; all shots in one browser session), --camera top, --track BODY, --batch, --terrain-color, --hide-ui.
  • Docs: "Scripts & Coding Agents" page with output shapes and a DRIFT recipe.

Changed

  • A single-frame scene's playback bar shows only the screenshot button and the format select, which then picks the screenshot's image type (PNG, JPG or WebP): play/step, speed, the time readout, episode controls, the progress bar and REC (a recording lasts one timeline loop, so it would be empty) are hidden, and their shortcuts do nothing. A live run gains them once a second frame arrives. Hiding the recording panel hides such a bar entirely.
  • simview terrain --area prints only the sampled extent and statistics by default; the per-cell grids (and x_coords/y_coords) need --full, whose --json is single-line. --area --csv without --full is one row per layer. Scripts reading the grids must add --full.

Fixed

  • Every command exits quietly when its output pipe closes early (e.g. | head) instead of printing a BrokenPipeError traceback.

6.4.1 - 2026-11-02

Changed

  • Terrain property layers are no longer downloaded with the scene. Each is fetched the first time it's needed: picking its color mode, diffing it, plotting it in the Terrain profile, or hovering the terrain (the tooltip lists every property). A terrain with seven ~1.9 MB layers loads ~13 MB less up front.
  • The server refuses HTTP requests whose Host is not localhost or an IP address (400), the rule the live WebSocket already applied. This blocks DNS rebinding; open the viewer by IP (e.g. http://10.0.0.5:5420), not by hostname.

Fixed

  • A terrain saved with create_terrain(compute_normals=False) draws its "normals" arrows from the normals the viewer computes for shading, instead of drawing nothing.
  • NaN/Infinity values are written as null, so saved scenes, static bundles, live WebSocket messages and info/diff/terrain/merge --json output are valid JSON. A NaN in live mode no longer drops frames. Files already written with bare NaN still load and merge.
  • A viewer that connects mid-run in live mode no longer receives duplicate frames, and concurrent episode/batch-name updates can't leave /model stale.
  • SimViewLauncher runs the same checks as show()/save(), so undeclared attributes (velocity, force, contacts) show up in the viewer.
  • Merging: polylines in any input (not only the first) are rejected instead of silently dropped; frames without contacts merge as empty contacts; inputs sharing a file name keep separate metadata.sources entries.
  • Terrain sampling at a NaN position returns NaN instead of raising.
  • simview host:~ expands the remote home directory.
  • Contact markers clear when a frame has an empty or shorter contacts list.
  • Recording a single-frame scene captures the frame instead of an empty video.
  • The scalar plot tooltip shows the episode sum/mean of the hovered batch.
  • Resetting similarity colors on a point cloud without base colors removes them.
  • A failed windowed-field fetch backs off for 5 s instead of retrying every frame.
  • Point and contact-marker textures are disposed with their materials.
  • A body omitted from some frames holds its last pose in trails, error metrics and the terrain profile instead of jumping to the origin.
  • Focus mode hides other batches' copies of a singleton static mesh.
  • CSV export prefixes text cells starting with =, +, - or @ with ', so spreadsheets don't evaluate batch names as formulas.
  • The Jupyter iframe HTML escapes the view string.
  • The Terrain profile and Error Metrics tabs need at least two frames, so a single-frame scene no longer shows empty tabs; a live run that starts on one frame gains them once it has two.

6.4.0 - 2026-10-28

Added

  • Point clouds accept named color layers (create_pointcloud(color_layers=)), selectable under "Point Color Mode", and a "Link Similarity" toggle that makes a similarity click recolor every point cloud with the same point count.
  • Hovering a control in the Controls panel shows a short description of it.

Changed

  • Recoloring a point cloud by similarity samples a colormap lookup table instead of calling the colormap per point: about 2x faster (200k points: 19 -> 9 ms).

Fixed

  • The terrain legend is hidden together with the terrain surface.
  • A static scene (no body trajectories) no longer gets an empty Analysis panel or Body states window; Error Metrics and Terrain now appear only once a body has a trajectory.
  • In split screen, clicking a point or terrain cell picks from the batch shown in that half.

6.3.1 - 2026-10-25

Changed

  • Viewer load and idle cost, measured in headless Chromium (software GL) on generated 64-env (5000 frames) and 512-env (500 frames) recordings:
  • Terrain normal arrows are built the first time they're shown, and the per-batch lookups index the batch group directly instead of searching the scene graph by name. 512-env load 7.2 s -> 1.7 s; scene objects 190k -> 4.1k; per-frame updateMatrixWorld 40 ms -> 1.2 ms.
  • The startup history walk reads only bodyTransform (getTransformFrame), so it no longer pulls every window of every windowed field (20 -> 8 Range requests on the 64-env file with windowing forced on).
  • /model and /states are requested from the page head and in parallel, overlapping the module download (64-env load at 50 ms RTT 2.43 s -> 2.04 s).
  • The server lists the viewer's ES modules as modulepreload links (also in simview static-viewer output), and the state blobs download while the scene is still being built instead of after it (64-env load at 50 ms RTT 2.10 s -> 1.81 s; no change on localhost).
  • /static is gzipped on the fly (2455 KB -> 570 KB for the initial assets).
  • The canvas redraws only on input, camera moves, playback, a new frame, or a 250 ms idle heartbeat (idle renders per 3 s: 78 -> 12), and the 60 Hz frame throttle no longer drops frames to timestamp jitter.

6.3.0 - 2026-10-22

Fixed

  • The Terrain profile panel now keeps updating during live streaming: it previously froze at the frame it was opened on (the panel had no hook for newly appended states, unlike the scalar plot and error metrics). While open it now recomputes on the same throttled tick those panels use.
  • A narrow race in live mode could shift a just-connected viewer's catch-up replay (and its episode boundaries) by one frame: the frame buffer and the pushed-frame counter were updated as two separate statements read by the event loop, so a catch-up could observe them mid-update. Each buffered frame now carries its own absolute index, so the offset is never reconstructed from the two.
  • Serving a preloaded columnar data= dict no longer rewrites its inline __b64__ state blobs into server-local /blob/ URLs in place -- the same protection the scene model already had (a later save() of that dict would have written dead references).

Changed

  • simview merge's --save-merged now writes the compact columnar states layout (same as SimulationScene.save), instead of the legacy per-frame JSON with decoded blobs -- merged files are many times smaller for the same data. Irregular scenes that can't be packed fall back to the old layout.
  • simview info reads the scene file once instead of twice (the whole-file gzip check and the JSON parse each read it), and summarizes scalar fields in one pass over the frames instead of two -- noticeable on the multi-hundred-MB files the command exists for.
  • CI/docs/publish workflow actions bumped to their newest majors: actions/checkout v5→v7, actions/setup-node v4→v7, actions/upload-artifact v4→v7, actions/download-artifact v4→v8, astral-sh/setup-uv v8.3.1→v10.3.2. peaceiris/actions-gh-pages@v4 and pypa/gh-action-pypi-publish@release/v1 were already current. None of the upstream breaking changes affect this repo's usage (the workflows set enable-cache: true and cache: npm explicitly).

Security

  • The live-mode /ws/states WebSocket now checks the browser's Origin header. CORS does not apply to WebSockets, so any site open in the user's browser could previously connect to a running live viewer and read every pushed frame. Localhost origins and the viewer's own page (same-origin, with the Host header naming an address) are allowed; non-browser clients, which send no Origin, are unaffected. A live viewer reached by hostname is now refused -- reach it by address (e.g. http://10.0.0.5:5420) instead, which is what the origin check can prove is this machine. A refused connection replaces the loading splash with an error instead of hanging on it.
  • POST /batch-names now rejects names longer than 256 characters, and any request body over 1 MiB is answered with 413 before it is parsed -- bounding what a rogue or runaway client can write to memory and to the on-disk sidecar.
  • The /blob/ URL token is now 128-bit (was 32-bit). It doubles as the only gate on blob responses, so the extra margin costs nothing.
  • The data= startup hash key is now restricted to same-origin URLs. A crafted link could previously make the viewer fetch and render scene data from an arbitrary server. The window.__simviewStaticBase global set by the hosting page is unaffected.
  • Binding the server to a non-loopback host (e.g. --host 0.0.0.0) now logs a warning that the server has no authentication; the --host help text says the same.

6.2.3 - 2026-10-21

Security

  • The JavaScript-error splash screen inserted the error message and URL as raw HTML; they are now rendered as plain text.

Fixed

  • The point-similarity legend is now shown in scenes without terrain. Previously it was hidden there, so a recolored point cloud had no legend.
  • viewerDefaults.ui can now set pointColorMode; it was rejected as an unknown key.
  • uPlot and js-colormaps.js were served unversioned under the immutable-cached static/lib/, so updating them never reached browsers with a cached copy. uPlot now lives in lib/uPlot-1.6.32/ and the locally trimmed js-colormaps.js in js/vendor/ (normal short cache).
  • js-colormaps.js now carries its upstream MIT license notice.
  • The scene's duplicate directional light is folded into LIGHTING_CONFIG (rendering unchanged).

Documentation

  • Comments and docstrings unified in style and stale references fixed across the codebase; public classes and methods that lacked docstrings now have them.
  • User and developer docs brought up to date with the code (terrain-similarity clicks need Data Probe on, Error Metrics "Details" toggle, terrain diff mode, chart click-to-seek, Body States panel, inline binary model fields, missing test files). The docs build now runs with --strict in CI.

6.2.2 - 2026-10-19

Removed

  • AnimationController.getCurrentStateIndex(), getCurrentTime(), getTotalTime() and getFirstTime(). Read the currentStateIndex, currentTime, totalTime and firstTime properties instead. (This entry was added later; the release notes originally left it out.)

Security

  • Batch names, episode labels, terrain layer/property names and load errors are now HTML-escaped in chart tooltips, the colour legend and the splash screen. A scene file (or a batch rename) could previously inject markup and script into the viewer page.
  • Locked dependencies bumped past known advisories: starlette 1.0.0 → 1.9.0, anyio 4.13.0 → 4.14.2, idna 3.11 → 3.20.

6.2.1 - 2026-10-19

Changed

  • Terrain geometry is built directly into typed arrays instead of through THREE.PlaneGeometry, and surface colours come from a sampled colormap lookup table instead of a colormap evaluation per vertex. On a 917x1808, 2-batch terrain the terrain-build stall drops from ~1.3 s to ~0.25 s and time-to-interactive from ~3.0 s to ~1.9 s; rendering is unchanged. The unused uv attribute is no longer allocated.
  • The terrain wireframe is a LineSegments over the surface's position buffer with each grid edge drawn once, instead of a wireframe: true mesh (which built its line index in a JS array and drew every interior edge twice); its opacity goes from 0.2 to 0.36 so the doubled-edge look is unchanged. On the same terrain the first-draw stall drops from ~1.05 s to ~0.57 s and the wireframe's per-frame GPU cost from ~24 ms to ~15 ms.
  • The terrain probe no longer raycasts the wireframe (only surface hits were ever used), halving the time a probe click takes (~0.83 s -> ~0.40 s).

6.2.0 - 2026-10-08

Added

  • Polylines: SimulationScene.create_polyline / model.polylines draw a world-space line from fixed points (a planned route) or from a per-frame frames blob with NaN padding (a replanned local path), with colour, dashed style and a "Show Polylines" toggle (polylinesVisible view-link flag and viewerDefaults.ui key).
  • Time-ranged visibility: bodies, static objects and polylines accept visible_ranges=[[t_from, t_to], ...] (visibleRanges in the JSON) and are hidden while the playhead is outside every range.
  • Live WebSocket states messages carry a frameOffset (absolute index of the first frame in the message), so a viewer that connects after the catch-up buffer has wrapped places episode boundaries correctly instead of shifting them by the dropped frames.
  • SimulationScene.reconcile_available_attributes() (previously an internal step of save()); show() and LiveViewer now call it too, so arrows and contact markers for attributes that were never declared with available_attributes appear in the live/show() viewer exactly as they do after save-then-view.
  • The Batch Legend reports when a rename could not be persisted (no file to write to under show()/LiveViewer/render, or the request failed) instead of silently keeping the new name.
  • /batch-names answers {"ok": true, "persisted": bool}; the sidecar is keyed by the batch selection (file.json#1) as well as the path.
  • create_terrain(compute_normals=False) stores no terrain normals (three floats per cell less in the file and over the wire); normals is now optional in the JSON format, SimulationScene.load and merge_simulation_files accept files without it, and the viewer shades from normals it computes itself (the "normals" arrows have nothing to draw then).
  • play=1 startup hash key (view_hash(play=True)): start playing once the scene is loaded, applied after t.
  • viewerDefaults.panels.recording: false and the hide=<panel,...> startup hash key (view_hash(hide=[...])) hide the REC / format / screenshot controls (and their R/S shortcuts) while keeping the rest of the playback bar; hide= takes any viewerDefaults.panels name.
  • simview static-viewer DIR copies the viewer page (index.html + static/) to DIR, so a site can host it as plain files next to save_static() bundles.

Changed

  • SimViewModel.from_dict no longer requires dt, collapse or staticObjects (matching the viewer and the format doc); a wrong-length batchNames is warned about and dropped instead of rejected.
  • create_terrain auto-computed normals use the real node spacing extent / (W - 1) (they were tilted by W / (W - 1)), and grid_res now means the distance between adjacent nodes, so the extent is (W - 1) * grid_res.
  • merge_simulation_files keeps each input's own batchNames for its selected batches (file stem only as a fallback) and carries the first file's viewerDefaults.
  • The terrain surface is shaded with the file's per-vertex normals (recomputed when absent) instead of flat (0, 0, 1) normals, so slopes get a lighting cue.
  • Video recording stops after exactly one loop of content at any playback speed.
  • A view-link hash change only pauses playback when it carries t.

Fixed

  • save() wrote availableAttributes in set order (hash-randomised per process), so identical scenes saved in two processes could fail to merge with "defines different bodies"; the order is now the attribute declaration order.
  • save_static externalised the per-frame __b64__ fields of a legacy-layout scene into /blob/ files the viewer never fetches, leaving every body at a NaN pose.
  • Every /static/* asset returned 500 when the package was installed through a symlinked path (HPC homes, macOS /tmp, symlinked venvs).
  • Live broadcast raised "Set changed size during iteration" and dropped the frame for the remaining clients whenever a tab connected or disconnected mid-send.
  • simview --host ::1 (any IPv6 host) failed with "No free port found".
  • The explicit simview view FILE form treated view as a file path.
  • Playback assumed the timeline starts at t = 0: a recording whose first timestamp is 100 s spent 100 s on frame 0 per loop and mapped the progress bar wrongly.
  • The scalar chart froze at the catch-up chunk in live mode and warned "Invalid end index" on every later frame.
  • Episode aggregates threw RangeError for an episode longer than ~125k frames.
  • A NaN cell in a terrain property (or diff) layer fired a blocking alert() from the vendored colour-map library and aborted the recolour; non-finite cells are now drawn neutral grey.
  • Split screen in focused render mode showed a blank half until the active batch was changed, because the split batches were never pinned visible.
  • Terrain-profile curves and CSV shifted left after a frame with a non-finite position; the gap is now plotted as a break.
  • add_state accepted per-batch rows that did not match batch_size (and a position/orientation count mismatch silently dropped rows); both now raise.
  • simview terrain --body sampled parented bodies in parent-local coordinates and could not find rigidly attached (local_transform) bodies at all.
  • A 1-D contacts tensor in a single-batch scene was written flat instead of per-batch, and merge then concatenated the index lists; an empty list raised.
  • simview diff reported 0° orientation error for a NaN pose and never tripped --fail-on-exceed on NaN; stats are NaN-aware and NaN counts as exceeding.
  • Merge: null property bounds crashed with TypeError, a singleton-vs-batched static object crashed with KeyError, a flat plain heightData list was rejected, and a non-monotonic time list resampled silently wrong (now a clear error).
  • simview info claimed a legacy file was columnar-repackable when its blob widths were wrong, crashed on a non-columnar states dict, and suggested gzip for an already-gzipped file; --save-merged into a missing directory now creates it.
  • An --area box lying inside a single terrain cell was rejected as non-overlapping.
  • view_hash now matches the viewer's serialiser byte for byte for non-finite and very large numbers and for !'()* in names, and the sync test parses the key order out of viewState.js so drift is caught.
  • Model validation: primitive shape parameters must be present and positive, terrain normal/property/embedding grids must match the heightmap, static objects loaded via from_dict are checked for duplicate names and shapes length, episode start_index past the last frame is rejected at save time, and unknown keys in a loaded state body are ignored with a warning instead of raising.
  • Server: blobs registered before a columnar bail-out were kept in memory for the server's lifetime; a ragged per-frame row crashed load instead of falling back to the legacy layout; a blob whose byte length is not a multiple of 4 raised struct.error instead of ValueError; bytes=5-3 answered 416 instead of ignoring the header; the batch-names fingerprint was taken at rename time instead of load time; extract_blobs rewrote metadata/batchNames strings starting with __b64__; ViewerHandle left its server thread running after a startup timeout; a stalled live broadcast was not cancelled on timeout; SimViewServer(sim_path=[a, b]) without data= silently served only a.
  • ssh://-oProxyCommand=... and [-o...]:path host forms reached ssh as options; a host starting with - is now rejected in every spec form.
  • Viewer: model.collapse is honoured (it was written by Python and read by nobody); a tcm= colour mode the scene lacks no longer throws inside the legend and aborts the rest of the view state; the legend and surface agree on a min == max range; per-batch nested embeddingData lists are accepted; a frame without contacts clears the previous frame's markers; contact markers, point arrays and trails are no longer drawn for hidden batches; probe/similarity clicks no longer hit hidden batches or fire through UI panels; Shift+Arrow no longer also rotates the camera; a NaN vector hides its arrow instead of reaching ArrowHelper; the REC button no longer sticks on STOP when recording cannot start; a negative or zero speed is rejected; a paused scrub into a not-yet-fetched field window re-renders when it lands; the live "waiting for first state" splash stays until the first frame; a missing terrain reports "Terrain data is missing" instead of a TypeError.
  • Viewer data checks: columnar blobs are validated against T * B * width and known field names, a partial trailing row throws like Python does, a window fetch requires a 206 response, non-monotonic times are warned about once, and interpolation keeps extra rows from whichever side is longer.
  • Error metrics with a NaN frame plot a gap and print "-" instead of NaN scales; recomputation is coalesced to one pass per animation frame while the panel is open.
  • SimView.destroy() now disposes the scene's resize/keyboard listeners and OrbitControls; terrain and static-object disposal release their GPU resources.

6.1.2 - 2026-10-08

Changed

  • The scalar plotter draws each series over the whole timeline, with a vertical line at the current playback time, instead of only the part played so far.

Fixed

  • A scalar series with NaN samples (gaps in its source) no longer plots as an empty chart: the y range is taken over finite values and the gaps are drawn as breaks.
  • Clicking a scalar chart (or re-selecting a batch) no longer throws the camera to the batch's grid origin and off a tracked body: changing batch now shifts the view by the offset between the two batches' cells, and body tracking re-centres on a batch switch.

6.1.1 - 2026-10-06

Added

  • The view link (and getViewState()/setViewState(), view_hash(...)) now also carries point-cloud visibility (pc), the tracked body (track), the terrain colour map (cmap) and the playback speed (speed), so a host page can hand a complete setup from one page load to the next. They are named keys, not flag bits: an absent key leaves the viewer's current setting alone, and old v=1 links decode unchanged.

Fixed

  • AnimationController.setSpeed() keeps the playback bar's speed dropdown in sync.
  • The scalar plotter no longer warns "Invalid end index" while the Analysis panel is opened before the states have loaded.

6.1.0 - 2026-10-06

Added

  • The viewer can be embedded in other applications -- see the new Embedding guide. The URL hash is now live: changing it after load (#v=1&t=2&cam=...) re-applies the view, so a host page can drive time, camera, batch and colour modes from outside. Two startup keys join it: #ui=0 hides every floating panel, and #data=<base url> loads a static bundle instead of the server API.
  • The viewer instance is exposed as window.simview (__debugSimView stays as an alias) with a supported surface: getViewState(), setViewState(), focusBody(name) and destroy(), plus simview:ready and simview:frame DOM events for a host page to react to load completion and frame changes.
  • simview.view_hash(...) builds view-link fragments (time, camera, batch, colour modes, toggle flags, data=, ui=False) from Python; stdlib-only, so it works on a base install. SimulationScene.show(view=..., height=...) opens the inline Jupyter viewer at a chosen view and iframe height; ViewerHandle.url_for(view) and LiveViewer.url return full viewer URLs.
  • SimulationScene.save_static(directory) writes a static bundle (model.json, states.json, blob/<id>) that any plain file server can host, via the stdlib-only simview.columnar.write_static_bundle.
  • create_body(color=..., opacity=..., visible=...) styles a body's mesh or primitive (RGB tint, translucency) and can start it hidden; the values are stored in the scene file.
  • SimulationScene(viewer_defaults=...) declares the viewer's initial state -- UI toggles such as pointCloudsVisible, open or closed options folders, the Body States window, and whole panels hidden -- stored as viewerDefaults in the model.

Fixed

  • Keyboard shortcuts (playback, visualization toggles, terrain probe) no longer fire while typing in a text input, textarea, select or contentEditable element, and the Body States window's Space-key handler is removed when the window is disposed.
  • The viewer page loads its stylesheets, scripts, import map and textures relative to the page, so it can be served from any path instead of only the origin root.

  • The JSON Format Specification page renders its field lists properly. Nested lists were indented too little for MkDocs, so everything under episodes, bodies, terrain and a state's bodies collapsed into one run-on paragraph with literal dashes. The Notes headings no longer run into their text, and code examples no longer overflow sideways.

  • The developer docs no longer describe code removed in 6.0.0. The architecture page dropped InteractionControls and gained remote.py, WindowedField and chartControls.js; einops is gone from the docstrings and the bug-report template.
  • The Live Streaming page no longer promises a late-joining viewer the full history: catch-up replays at most the most recent 10,000 frames.

6.0.1 - 2026-10-05

Fixed

  • The 3D view now resizes with the browser window. The canvas kept the size it had when the page loaded while the panels and charts reflowed, so after a resize the scene came out stretched or cropped.

6.0.0 - 2026-10-01

A cleanup release: about 1,200 fewer lines of application code, plus the vendored chroma-js and the unreachable colormap data. The scene JSON wire format is unchanged, so scenes load in both directions. It is a major release because some redundant Python authoring entry points were removed, listed below.

Removed

  • The duplicated authoring layer. SimulationScene now holds the authoring logic itself. Removed: SimViewModel.create_terrain, create_body, create_static_object_singleton, create_static_object_batched and add_terrain, and SimulationScene.add_terrain_object, add_body_object, add_static_object_instance, create_static_object_singleton and create_static_object_batched. Add static objects with SimViewModel.add_static_object.
  • SimViewBody.create_sphere / create_cylinder, which nothing called.
  • SimViewServer.run(debug=...) and python -m simview.server. Use simview <file>.
  • Module re-exports simview.terrain.load_scene / load_scene_model, simview.diff.load_scene and simview.remote.human_bytes. Import them from simview.utils. simview.model.BLOB_PREFIX is gone too; use simview.columnar.BLOB_PREFIX.
  • simview clear no longer sweeps simview_viz_*.json temp files or /tmp/.simview_cache. The launcher stopped writing them some time ago.
  • The vendored chroma-js. Batch palettes are interpolated in CIE LCh by a small built-in function whose output is identical.
  • The 71 matplotlib colormaps the viewer never offered from js-colormaps.js. Every map in the colormap dropdown is unchanged.

Changed

  • ViewerHandle is now the background server itself (simview.server.ViewerHandle). scene.show(), LiveViewer and simview render share it instead of three copies. from simview import ViewerHandle, url, notebook display and with-block use behave as before.
  • simview diff summaries gain min, and simview terrain --along-body summaries gain final, because both commands now share one stats helper.
  • A body missing bodyTransform now fails a merge with a clear ValueError instead of a KeyError.
  • The Body states, Analysis and Batches panels collapse with native <details>. Collapsed, Body states and Analysis now have the same height and centring.

Fixed

  • Keyboard shortcuts no longer cancel themselves out after a model reload. The controls' keydown listener was never removed, so each reload added another and every toggle fired twice.

Internal

  • Python: the blob codec, state-field tables, body-name helpers, bind-host logic and port warning each live in one place. uvicorn now chooses uvloop/httptools itself, and the code no longer falls back for a missing orjson, which is a hard dependency.
  • Frontend: the three uPlot panels share their resize, tooltip, marker and export-bar code. InteractionControls.js is gone. PlaybackControls removes its listeners with a single AbortController. View-state hashes are parsed with URLSearchParams, and existing share links still work.

5.1.1 - 2026-10-01

Fixed

  • SimView's button and select styles no longer leak into pages that embed the viewer. 5.1.0 styled every button and select on the page, including a 1px press offset (button:active { transform: translateY(1px) }) that replaced an embedder's own transform; the rules now apply only inside SimView's panels.
  • The Body states, Analysis and Controls panel headers share one size (13.2px); Body states used to grow with the window width.
  • The docs and demo deploys no longer fail when two pushes land close together.

5.1.0 - 2026-10-01

Added

  • C toggles Show Point Clouds in scenes that have point clouds but no contacts (contacts keep the key when present); the label reads "Show Point Clouds (C)" when the key applies.

Changed

  • Viewer UI restyled in Catppuccin Mocha. Every panel (Analysis, Body states, Batches, legends, playback bar, and now the lil-gui Controls panel, which floats inset from the corner instead of docking to the top edge) shares one translucent surface, with Mocha blue as the accent and the same chevron as each panel's collapse indicator. Chart axes, grids and series, body vector arrows, probe and wireframe use the palette too; the 3D background is Mocha mantle, so screenshots and recordings change as well. System fonts with tabular figures for numeric readouts, visible keyboard focus rings, a larger loading screen and a 3D-cube favicon. The playback time readout now reads 0.00 / 4.90 s.
  • Batch colors are the Mocha accents (blue, peach, green, mauve, …) instead of the old navy-to-cream diverging palette, whose first batch was nearly invisible on the dark background; past 14 batches the colors are interpolated between them.
  • The live demo shows a new scene: GT vs prediction on helhest sample 774.

Fixed

  • Stateless point clouds no longer show up as trackable bodies. A point cloud with no per-frame states (a static prop) is now left out of the Analysis tab's terrain-profile body picker (it could even be the default, giving an empty profile) and the Camera "Track Body" list, matching the Body State window. Body#hasPose is the shared rule.
  • The playback time readout shows two decimals and no longer wraps onto two lines in a narrow controls row.
  • Analysis scalar tabs size to their label instead of splitting the bar evenly, so with many scalars the names stay readable rather than truncating to "wh…"; the bar wraps as before.
  • The Analysis panel's scalar chart no longer overflows the panel by the height of the Export CSV bar.
  • The Analysis panel's Terrain chart shows its y-axis values again (a single tick step that didn't fit the short panel made uPlot drop the axis labels; the Scalars chart's coarser-step fallback is now shared) and gains time labels on its x-axis. The Error Metrics orientation label is no longer clipped, and both charts get more plot height from a slimmer x-axis strip.

5.0.0 - 2026-09-21

A cleanup release: no new features, roughly a thousand lines less code, two Python dependencies and two vendored JavaScript libraries gone. The scene JSON wire format and the Python authoring API (SimulationScene, SimViewBody, BodyTrajectory, SimViewModel) are unchanged — a scene written by 4.x loads in 5.0.0 and vice versa, and authoring code needs no edits.

Removed

  • The PNG-sequence recording format. The viewer's recording dropdown offered WEBM / MP4 / PNG; PNG captured each frame as a PNG blob and packed the run into a .tar, which was the only reason tar.js and download.js were vendored. Both libraries are gone with it. WEBM and MP4 are unaffected, as is the single-frame PNG screenshot button, which is a separate feature.
  • The jinja2 dependency. It rendered six url_for('static', ...) calls that all resolved to the constant /static/ prefix; index.html now hardcodes them.
  • einops from the authoring extra. Its four rearrange() calls are expressible in plain torch, which model.py already requires. If your own code imports einops while relying on simview[authoring] to install it, declare it yourself.

Changed

  • The CLI now uses real argparse subcommands. Every documented invocation is unchanged — simview info <file>, simview diff <file> --batches 0 1, simview terrain <file> --area --layer all --json — and output, CSV/JSON rendering and the 0 / 1 / 2 exit codes are byte-for-byte identical. Per-subcommand --help is now scoped, so flags no longer have to explain which subcommand they belong to. Breaking: a flag written before its subcommand (simview --json info x.json) is no longer accepted. Write it after the file, as the documentation has always shown: simview info x.json --json.
  • The grayscale, heatmap and terrain color maps now come from the bundled matplotlib maps (Greys_r, jet, terrain) instead of hand-rolled ramps. Scenes using those three names by name will look slightly different; every other map, including the default magma, is unchanged.
  • /blob Range handling now honours only bytes=start-end, the one form the viewer sends. Suffix (bytes=-N), open-ended (bytes=N-) and multi-range requests serve the whole blob with 200 instead of a partial response, which RFC 9110 permits for a range a server declines. A well-formed but out-of-range request still returns 416.

Internal

  • The stdlib-only inspection tools (info, diff, terrain) no longer hand-copy the same blob decoding, body-name resolution and scene loading; the shared helpers live in columnar.py and utils.py. terrain.py's cross-batch queries now call their single-batch counterparts instead of duplicating them.
  • 441 lines of CSS-in-JS moved from six UI panels into controls.css, and the three uPlot panels share one chart-setup helper.
  • Dead frontend config removed (GROUND_CONFIG, POINT_VECTOR_CONFIG, CONTACT_CONFIG, createArrows, and six CONTROLS_CONFIG keys that either restated three.js defaults or overwrote an OrbitControls method with a boolean).

4.2.1 - 2026-09-02

Added

  • "Show Point Clouds" toggle in Body Options, for scenes that contain point-cloud bodies. Point clouds are no longer governed by Body Visualization Mode (see below), so this is how they are hidden and shown.

Fixed

  • Mesh bodies no longer disappear once a run travels away from where it started. Bodies are drawn as instanced meshes positioned per instance, and THREE caches an instanced mesh's bounding sphere the first time it is frustum-tested and never invalidates it when the instances move — so every mesh body was culled wholesale as soon as its starting position left the view. Long recordings (a robot driving a few hundred metres) lost their bodies mid-playback while the terrain kept rendering.
  • Selecting the "points" visualization mode no longer blanks out every mesh body. A point-cloud body is the object itself rather than a way of drawing a body, so it no longer contributes "points" to the mode list, and Body Visualization Mode — none included — leaves point clouds alone. In a mixed scene (a robot plus a lidar cloud) the mode offered only points, which no mesh body could render.
  • Static point clouds are no longer listed in the Body states panel, where they showed a permanently-zero Position/Rotation. A point cloud that does carry per-frame data stays listed like any other body.
  • The Analysis panel's scalar chart now actually draws. It had been painting nothing at all: its x scale was never pinned (uPlot treats a scale's min/max as outputs — range is the pin — so the initial autoscale over empty data nulled it and no later update revisited it), the render loop redrew without rebuilding paths, so the chart kept redrawing whatever data it first saw, and the y axis offered a single tick increment that could not fit the panel's height, so uPlot drew no ticks or labels. An end-to-end test now asserts the chart paints its data and its axis rather than merely that the panel opens.
  • The camera's far plane and orbit limit now follow the terrain extent instead of being fixed at 500 m, so terrain and bodies on scenes hundreds of metres across no longer clip away, and the whole scene can be framed.

4.2.0 - 2026-08-31

Added

  • Merging a subset of a file's batches. Append #<batches> to any input file to contribute only some of its batches to the merged scene, e.g. simview gt.json method_a.json#1 method_b.json#1 — so several files that each carry their own copy of a shared ground truth can be compared without merging that ground truth once per file. The selector takes indices (#1), comma-separated lists (#0,2), inclusive ranges (#1-3), negative indices (#-1) and the file's own batchNames (#ours); merged batch names keep the source file's index (run[2]). Works with --save-merged, with remote inputs, and on a single file (to view only some of its batches). merge_simulation_files(paths, selections=...) is the Python equivalent. See CLI.
  • create_terrain(property_bounds=...) pins the color-scale range of a named terrain property explicitly, e.g. property_bounds={"friction": (0.0, 1.0)}, instead of always deriving it from that map's own min/max — so the same scale (and legend) stays comparable across scenes. Properties left out keep the data-range default, and cells outside an explicit range saturate at the end colors.
  • Each Error Metrics readout is prefixed with a swatch in its plot series color, so the readout doubles as a legend for the curves below it.

Fixed

  • Nested fields in the Scene Info panel now start collapsed. A scene with rich metadata used to expand every nested key at once, burying the top-level entries.

4.1.0 - 2026-08-19

Added

  • Remote scene files. Anywhere the CLI takes an input file (view, info, diff, terrain, render, and multi-file merges) it now also accepts an scp-style host:path, fetched over the system ssh and cached locally. Transfers are compressed (gzipped on the remote, or ssh -C when it has no gzip), and the cache entry carries the remote file's mtime, so freshness is a plain stat comparison. Adds --refresh/--offline, and simview clear knows about the new cache. See CLI.
  • The columnar ("v4") states layout is now an on-disk format, not just a server-side repack: SimulationScene.save() writes it by default, producing substantially smaller files that the viewer can load without any repacking. Pass columnar=False for the previous per-frame layout, or columnar=True to raise instead of falling back when a scene is too irregular to pack. See the JSON Format Specification.
  • simview info reports which states layout a file uses.
  • Episodes. An episodic (e.g. RL) recording can mark its resets with SimulationScene.mark_episode() / LiveViewer.mark_episode(), serialized as an optional model.episodes array. The viewer ticks episode boundaries on the playback bar, adds |◀ / ▶| navigation ([ / ]), and overlays per-episode aggregates (including the episode return) on the scalar plots. See Episodes.
  • Focused-batch rendering. Above 32 batches the viewer now draws only the focused batch by default, and builds each batch's per-batch scene objects (axes, arrows, point clouds, contact points) the first time it's rendered rather than all up front — so a scene with hundreds of parallel envs loads and runs like a single-env one. Toggle with "Render All Batches" in Body Options.
  • Windowed state loading. The /blob endpoint now supports HTTP Range requests, and the viewer uses them to stream the large current-frame-only fields (velocity, angularVelocity, force, torque) in ~1 MB windows around the playhead instead of materializing whole (T, B, k) runs. bodyTransform and the scalars stay fully resident, since trails, error metrics, the terrain profile and the scalar plots all walk the entire trajectory.

Changed

  • SimulationScene.save() writes columnar states by default. Files stay readable by load, merge_simulation_files, simview info/diff/terrain and the viewer either way, but a third-party tool that parses states as a JSON array will need to handle the object form (or be passed columnar=False).
  • A fully shared (singleton) terrain now ships exactly one copy of its height, normals, properties and embedding data instead of one identical copy per batch — a real saving for many-batch (e.g. RL-scale) scenes. Readers resolve the layout from the data length, so files written by older versions (which broadcast the singleton) keep working; a mixed terrain still broadcasts its shared fields.

Fixed

  • LiveViewer.push_state no longer blocks the simulation loop on a slow or hung viewer tab (up to 5 s per frame before). Frames go through a bounded queue drained by a sender thread, dropping the oldest pending frame when it fills so the live view keeps tracking the simulation; dropped frames still land in scene.states and the catch-up buffer, and stop() flushes the backlog.
  • The live frame buffer is now bounded (frame_buffer_size, 10k frames by default) instead of growing for the length of a run, and a viewer connecting mid-run replays the recent window in 500-frame slices rather than one json.dumps of the whole history. It is registered for live broadcasts only once the replay has caught up, so a new frame can no longer overtake the history it follows.
  • Trails are now appended to in place instead of being rebuilt per loaded chunk, which in live mode cost O(T²) geometry reallocation over a run.
  • simview diff resolves parent chains and compares world poses, like the viewer's Error Metrics panel does — the two used to report different numbers for the same scene. Rigidly-attached bodies (a constant localTransform, absent from states) are now diffable and addressable via --body.
  • Mesh bodies authored from tensors crashed the viewer on load: createGeometry called .flat() on vertices that always arrive blob-decoded as flat Float32Arrays. Meshes are also indexed with Uint32Array now, so more than 65535 vertices no longer wrap.
  • Serving an in-memory scene (show(), LiveViewer, SimViewLauncher) no longer rewrites the caller's model in place, which left a later save() writing dead /blob/... URLs and a second show() serving stale blob references.
  • merge_simulation_files carries terrain embeddingData (the features color mode was silently lost on merged scenes) and each model's metadata (namespaced under metadata.sources) through the merge, groups b64-decoded terrain normals back into per-vertex vec3s, and rejects inputs whose terrain x/y bounds differ instead of merging them into spatially misaligned batches.
  • add_state normalizes a 0-dim scalar tensor/array to the per-batch list every other frame stores, which merge_simulation_files used to raise on.
  • SimulationScene's internal-data cleanup frees the terrain's per-cell embedding_data too, potentially the largest of its arrays.
  • A blob fetch that returns an HTTP error now fails loudly on the load-error splash instead of being decoded as float32 garbage, and inline __b64__ state fields decode endian-safely like the standalone blob path already did.
  • setActiveBatch no longer forwards an out-of-range batch index to the body state window, scalar plotter and batch legend right after warning that it rejected it.
  • Dropped allow_credentials from the CORS config: combined with the any-localhost-port origin regex it let any other local dev server read scene data with the user's credentials. The API uses no cookies or auth headers, so nothing needed it.

Removed

  • The ctrl+drag box-selection code path, which never worked — it crashed on mouseup, raycast against an always-empty list, and checked event keys that don't exist. Click handling, the data probe tooltip and shift+arrow batch switching are unaffected.

4.0.0 - 2026-08-04

⚠ Breaking changes

  • Scene JSON files saved by older versions of simview will not load. Terrain's frictionData/stiffnessData fields and their bounds.minFriction/maxFriction/ minStiffness/maxStiffness entries are replaced by a generic terrain.properties object ({name: {data, min, max}} — see the JSON Format Specification). Re-save any existing scene file with the current version of simview (or SimulationScene.load() + save()) to pick up the new format; there is no automatic migration.
  • scene.create_terrain()/SimViewModel.create_terrain() no longer accept friction_map=/stiffness_map=; pass properties={"friction": ..., "stiffness": ...} instead (or any other named per-cell scalar map — see below).
  • The batch-names sidecar file (.<scene>.<hash>.batchnames.json) written before staleness-fingerprinting was added (pre-3.x) is no longer read; a fresh POST /batch-names regenerates it in the current format.
  • The model JSON's long-superseded batchSize field (renamed to simBatches several releases ago) is no longer read as a fallback.

Added

  • Terrain scalar properties (friction, stiffness, or any other per-cell field) are now a fully generic, arbitrarily-named mechanism end to end — Python (SimViewTerrain.properties), the CLI (simview terrain --layer <name>, simview info, simview merge), and the viewer (color mode dropdown, Legend, hover/probe tooltip, Terrain Profile tab) all support any property name supplied at authoring time, with zero code changes required to add a new one.
  • scene.create_pointcloud() now accepts optional color (static per-point RGB) and embedding (per-point feature vector) tensors; scene.create_terrain() gains a matching embedding_map (per-cell feature vector). When present, clicking a point or terrain cell recolors the whole body/grid by cosine similarity to the clicked location, computed client-side — a new "similarity" Point Color Mode for point clouds and "features" terrain color mode, both with a matching colormap legend.

Fixed

  • Clicking now only recolors a point cloud/terrain by similarity when the matching mode is already selected from its dropdown; otherwise it's an ordinary selection, and a click does nothing at all unless "Data Probe" or similarity mode is active.
  • The Analysis panel's "Terrain" tab no longer appears for bodies with no trajectory (e.g. a static point cloud), and now plots the whole trajectory up front instead of only revealing it progressively during playback.
  • "Scene Info" now shows full metadata keys/values instead of truncating them.

3.6 - 2026-07-29

Added

  • SimulationScene/SimViewModel now accept an optional free-form metadata dict (e.g. engine name, checkpoint path, git commit, CLI args) carried through to the saved JSON, simview info, and a read-only "Scene Info" panel in the browser, so a scene stays self-describing long after it was generated.
  • scene.create_terrain() now auto-computes normals from the heightmap gradients if normals is omitted.
  • scene.create_terrain() now accepts grid_res to auto-infer spatial x_lim/y_lim constraints instead of requiring manual definition.
  • simview terrain <file> --along-body BODY: sample terrain layer(s) bilinear-interpolated at a body's per-frame (x, y) position — "what terrain is under the robot's driven path". With --batches A B, both batches' terrains are sampled along batch A's (reference, typically ground-truth) trajectory and reported as value_a/value_b/delta per layer, so the delta reflects property differences under the path rather than trajectory divergence. Honors --layer/--every and the usual --json/--csv output modes.
  • "Terrain" tab in the browser Analysis panel: plots a terrain layer (height/friction/stiffness) sampled under a body's path over time, one series per batch, with layer/body pickers, a path picker ("own path" per batch, or every batch's terrain along one reference batch's path — e.g. ground truth's), playback-synced reveal, click-to-seek, and CSV export. The browser-side counterpart of simview terrain --along-body.
  • --fail-on-exceed flag for simview diff: exits with code 2 (after printing the normal report) when any diffed body's trajectory exceeds --pos-threshold/--rot-threshold-deg, and 0 when within them -- distinct from the usage/parse-error exit 1, so scripts and CI can use an exported scene as a regression tripwire.

Fixed

  • Focusing a batch while a Scalars plot was open threw s.stroke is not a function on the next redraw (uPlot expects series.stroke to stay a function; the focus handler was overwriting it with a color string).

3.5 - 2026-07-28

Added

  • simview render <file> --output frame.png: headless PNG screenshot via a real (headless) browser driving a real SimViewServer instance, with --view/--width/--height options. Ships as a new optional render extra rather than a hard dependency.
  • Terrain diff color overlay: a "diff" color mode (diverging colormap centered on zero) plus Diff Layer/Batch A/Batch B pickers in Terrain Options, with a matching diverging colorbar in the Legend.
  • The terrain data probe now shows every batch's height/friction/stiffness at the hovered cell, plus each one's delta from a reference batch, instead of just the hovered batch.
  • --per-axis flag for simview diff, reporting signed err_x/err_y/ err_z (batch A minus batch B) per frame in --json/--csv output.
  • Mean/min |delta| stats (alongside the existing max) in simview terrain --batches --area output.
  • Full documentation site (MkDocs + mkdocs-material + mkdocstrings) covering usage, the CLI, the JSON format specification, the API reference, and a developer guide, published to GitHub Pages. README.md is trimmed to a landing page that links to it.

Changed

  • The Error Metrics panel auto-selects a sensible Batch A/B default from batch names (e.g. ground-truth vs. post-adaptation) instead of always defaulting to indices 0/1, falling back to 0/1 when no batch name matches.

3.4 - 2026-07-27

Added

  • simview info <file>: a structural summary of a scene JSON (model/terrain/ body/state breakdown, columnar-repack eligibility, consistency warnings) in human-readable text or --json.
  • simview terrain <file> --point/--area: numeric height/friction/stiffness queries (bilinear-interpolated at a point, or a raw grid over an area), plus --batches A B to compare two batches (value_a/value_b/delta per layer).
  • simview diff <file> --batches A B: per-frame position/orientation divergence between two batches' trajectories, with --body/--every/ --pos-threshold/--rot-threshold-deg options.
  • --csv output for simview diff and simview terrain, alongside the existing --json.

Changed

  • Only emit uvicorn's access log when running in debug mode.

3.3 - 2026-07-17

Added

  • Expose the installed package version as simview.__version__.
  • Test against Python 3.14 in CI and advertise it in the package classifiers.
  • Dependabot configuration for GitHub Actions, npm, and Python dependencies.

Changed

  • Bump the PyPI development-status classifier to 5 - Production/Stable.
  • Use uvicorn's modern sansio websocket implementation for the live server when available, silencing the websockets.legacy deprecation warning.
  • Raise the CI coverage floor from 80% to 83%.

Removed

  • Unused collapsedMode/focusedMode placeholder flags from BatchManager.

3.2 - 2026-07-15

Added

  • Shareable view links: the current camera/playback state is encoded in the URL hash so a view can be restored or handed off.
  • Single-frame PNG screenshot export.

Changed

  • Prepare packaging for PyPI publishing (metadata, build, publish workflow).
  • Replace CCapture with the browser-native MediaRecorder for video recording, covered by an e2e test.

Fixed

  • Unblock CI: guard the optional numpy import and add --no-launch to example.py.
  • Plot visualization fixes.

3.1 - 2026-07-14

Added

  • Live streaming mode: LiveViewer pushes states to connected browser tabs over WebSocket as a simulation runs.
  • Non-blocking scene.show() with Jupyter iframe support (_repr_html_).
  • Smooth interpolated playback (position lerp + quaternion slerp) with a toggle.
  • Error-metric summary stats and CSV export in the analysis panel.

Changed

  • Vendor three.js and chroma-js locally so the viewer works fully offline.
  • Serve states as per-body whole-trajectory binary columns ("v4" columnar repack), backed by a Float32Array StateStore, for much cheaper playback of long recordings.

Fixed

  • Binary-search seek for non-uniform timelines, parallel blob fetches, and versioned immutable blob URLs.

Testing / infrastructure

  • Add vitest + Playwright frontend tests, pyright type checking, and a CI coverage floor.

3.0 - 2026-07-13

Baseline release. Highlights of the surface established by this version:

  • Authoring API — SimulationScene with incremental model building, add_state/add_trajectory (batched, binary-encoded), gzip support, and JSON save/load.
  • Wire format — HTTP-served model/states, binary-encoded numeric fields, parent-relative (rigid and articulated) body transforms, grouped body names.
  • Frontend — vanilla-JS/THREE.js viewer with batched split-screen comparison, camera tracking, trajectory trails, terrain data probe, a unified Analysis panel (Scalars + Error Metrics, plotted with uPlot), and synchronized timeline scrubbing.
  • Tooling — CLI (simview view / clear / --save-merged), multi-file merge pipeline, CORS-hardened server with cache headers, py.typed, and CI across Python 3.12/3.13 with a base-install-only check.