simview 6.1.0__tar.gz → 6.1.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.
Files changed (105) hide show
  1. {simview-6.1.0/simview.egg-info → simview-6.1.2}/PKG-INFO +1 -1
  2. {simview-6.1.0 → simview-6.1.2}/pyproject.toml +1 -1
  3. {simview-6.1.0 → simview-6.1.2}/simview/static/js/SimView.js +14 -2
  4. {simview-6.1.0 → simview-6.1.2}/simview/static/js/components/AnimationController.js +4 -0
  5. {simview-6.1.0 → simview-6.1.2}/simview/static/js/components/BatchManager.js +14 -6
  6. {simview-6.1.0 → simview-6.1.2}/simview/static/js/ui/Controls.js +15 -0
  7. {simview-6.1.0 → simview-6.1.2}/simview/static/js/ui/ScalarPlotter.js +21 -41
  8. {simview-6.1.0 → simview-6.1.2}/simview/static/js/ui/chartControls.js +15 -0
  9. {simview-6.1.0 → simview-6.1.2}/simview/static/js/utils/viewState.js +29 -5
  10. {simview-6.1.0 → simview-6.1.2}/simview/view.py +17 -0
  11. {simview-6.1.0 → simview-6.1.2/simview.egg-info}/PKG-INFO +1 -1
  12. {simview-6.1.0 → simview-6.1.2}/tests/test_view.py +7 -0
  13. {simview-6.1.0 → simview-6.1.2}/LICENSE +0 -0
  14. {simview-6.1.0 → simview-6.1.2}/MANIFEST.in +0 -0
  15. {simview-6.1.0 → simview-6.1.2}/README.md +0 -0
  16. {simview-6.1.0 → simview-6.1.2}/setup.cfg +0 -0
  17. {simview-6.1.0 → simview-6.1.2}/simview/__init__.py +0 -0
  18. {simview-6.1.0 → simview-6.1.2}/simview/__main__.py +0 -0
  19. {simview-6.1.0 → simview-6.1.2}/simview/columnar.py +0 -0
  20. {simview-6.1.0 → simview-6.1.2}/simview/diff.py +0 -0
  21. {simview-6.1.0 → simview-6.1.2}/simview/info.py +0 -0
  22. {simview-6.1.0 → simview-6.1.2}/simview/launcher.py +0 -0
  23. {simview-6.1.0 → simview-6.1.2}/simview/live.py +0 -0
  24. {simview-6.1.0 → simview-6.1.2}/simview/merge.py +0 -0
  25. {simview-6.1.0 → simview-6.1.2}/simview/model.py +0 -0
  26. {simview-6.1.0 → simview-6.1.2}/simview/py.typed +0 -0
  27. {simview-6.1.0 → simview-6.1.2}/simview/remote.py +0 -0
  28. {simview-6.1.0 → simview-6.1.2}/simview/render.py +0 -0
  29. {simview-6.1.0 → simview-6.1.2}/simview/scene.py +0 -0
  30. {simview-6.1.0 → simview-6.1.2}/simview/server.py +0 -0
  31. {simview-6.1.0 → simview-6.1.2}/simview/state.py +0 -0
  32. {simview-6.1.0 → simview-6.1.2}/simview/static/css/controls.css +0 -0
  33. {simview-6.1.0 → simview-6.1.2}/simview/static/js/components/InteractionController.js +0 -0
  34. {simview-6.1.0 → simview-6.1.2}/simview/static/js/components/Scene.js +0 -0
  35. {simview-6.1.0 → simview-6.1.2}/simview/static/js/components/StateStore.js +0 -0
  36. {simview-6.1.0 → simview-6.1.2}/simview/static/js/components/WindowedField.js +0 -0
  37. {simview-6.1.0 → simview-6.1.2}/simview/static/js/config.js +0 -0
  38. {simview-6.1.0 → simview-6.1.2}/simview/static/js/main.js +0 -0
  39. {simview-6.1.0 → simview-6.1.2}/simview/static/js/objects/Body.js +0 -0
  40. {simview-6.1.0 → simview-6.1.2}/simview/static/js/objects/StaticObject.js +0 -0
  41. {simview-6.1.0 → simview-6.1.2}/simview/static/js/objects/Terrain.js +0 -0
  42. {simview-6.1.0 → simview-6.1.2}/simview/static/js/objects/colormap.js +0 -0
  43. {simview-6.1.0 → simview-6.1.2}/simview/static/js/objects/similarity.js +0 -0
  44. {simview-6.1.0 → simview-6.1.2}/simview/static/js/objects/utils.js +0 -0
  45. {simview-6.1.0 → simview-6.1.2}/simview/static/js/ui/AnalysisPanel.js +0 -0
  46. {simview-6.1.0 → simview-6.1.2}/simview/static/js/ui/BatchLegend.js +0 -0
  47. {simview-6.1.0 → simview-6.1.2}/simview/static/js/ui/BodyStateWindow.js +0 -0
  48. {simview-6.1.0 → simview-6.1.2}/simview/static/js/ui/ErrorMetrics.js +0 -0
  49. {simview-6.1.0 → simview-6.1.2}/simview/static/js/ui/Legend.js +0 -0
  50. {simview-6.1.0 → simview-6.1.2}/simview/static/js/ui/PlaybackControls.js +0 -0
  51. {simview-6.1.0 → simview-6.1.2}/simview/static/js/ui/TerrainProfile.js +0 -0
  52. {simview-6.1.0 → simview-6.1.2}/simview/static/js/utils/batchPresets.js +0 -0
  53. {simview-6.1.0 → simview-6.1.2}/simview/static/js/utils/batchVisibility.js +0 -0
  54. {simview-6.1.0 → simview-6.1.2}/simview/static/js/utils/blobCodec.js +0 -0
  55. {simview-6.1.0 → simview-6.1.2}/simview/static/js/utils/blobWindow.js +0 -0
  56. {simview-6.1.0 → simview-6.1.2}/simview/static/js/utils/bodyTransforms.js +0 -0
  57. {simview-6.1.0 → simview-6.1.2}/simview/static/js/utils/cameraRange.js +0 -0
  58. {simview-6.1.0 → simview-6.1.2}/simview/static/js/utils/csv.js +0 -0
  59. {simview-6.1.0 → simview-6.1.2}/simview/static/js/utils/episodes.js +0 -0
  60. {simview-6.1.0 → simview-6.1.2}/simview/static/js/utils/errorMath.js +0 -0
  61. {simview-6.1.0 → simview-6.1.2}/simview/static/js/utils/interpolate.js +0 -0
  62. {simview-6.1.0 → simview-6.1.2}/simview/static/js/utils/keyboard.js +0 -0
  63. {simview-6.1.0 → simview-6.1.2}/simview/static/js/utils/liveFollow.js +0 -0
  64. {simview-6.1.0 → simview-6.1.2}/simview/static/js/utils/terrainSample.js +0 -0
  65. {simview-6.1.0 → simview-6.1.2}/simview/static/js/utils/uplot.js +0 -0
  66. {simview-6.1.0 → simview-6.1.2}/simview/static/js/utils/viewerDefaults.js +0 -0
  67. {simview-6.1.0 → simview-6.1.2}/simview/static/lib/js-colormaps.js +0 -0
  68. {simview-6.1.0 → simview-6.1.2}/simview/static/lib/three-0.174.0/addons/controls/OrbitControls.js +0 -0
  69. {simview-6.1.0 → simview-6.1.2}/simview/static/lib/three-0.174.0/addons/libs/lil-gui.module.min.js +0 -0
  70. {simview-6.1.0 → simview-6.1.2}/simview/static/lib/three-0.174.0/three.core.js +0 -0
  71. {simview-6.1.0 → simview-6.1.2}/simview/static/lib/three-0.174.0/three.module.js +0 -0
  72. {simview-6.1.0 → simview-6.1.2}/simview/static/lib/uPlot.esm.js +0 -0
  73. {simview-6.1.0 → simview-6.1.2}/simview/static/lib/uPlot.min.css +0 -0
  74. {simview-6.1.0 → simview-6.1.2}/simview/static/textures/contacts/red-cross0.png +0 -0
  75. {simview-6.1.0 → simview-6.1.2}/simview/static/textures/points/ball1.png +0 -0
  76. {simview-6.1.0 → simview-6.1.2}/simview/templates/index.html +0 -0
  77. {simview-6.1.0 → simview-6.1.2}/simview/terrain.py +0 -0
  78. {simview-6.1.0 → simview-6.1.2}/simview/utils.py +0 -0
  79. {simview-6.1.0 → simview-6.1.2}/simview.egg-info/SOURCES.txt +0 -0
  80. {simview-6.1.0 → simview-6.1.2}/simview.egg-info/dependency_links.txt +0 -0
  81. {simview-6.1.0 → simview-6.1.2}/simview.egg-info/entry_points.txt +0 -0
  82. {simview-6.1.0 → simview-6.1.2}/simview.egg-info/requires.txt +0 -0
  83. {simview-6.1.0 → simview-6.1.2}/simview.egg-info/top_level.txt +0 -0
  84. {simview-6.1.0 → simview-6.1.2}/tests/test_batch_selection.py +0 -0
  85. {simview-6.1.0 → simview-6.1.2}/tests/test_cli.py +0 -0
  86. {simview-6.1.0 → simview-6.1.2}/tests/test_columnar_ondisk.py +0 -0
  87. {simview-6.1.0 → simview-6.1.2}/tests/test_columnar_states.py +0 -0
  88. {simview-6.1.0 → simview-6.1.2}/tests/test_contacts.py +0 -0
  89. {simview-6.1.0 → simview-6.1.2}/tests/test_diff.py +0 -0
  90. {simview-6.1.0 → simview-6.1.2}/tests/test_embed_defaults.py +0 -0
  91. {simview-6.1.0 → simview-6.1.2}/tests/test_episodes.py +0 -0
  92. {simview-6.1.0 → simview-6.1.2}/tests/test_info.py +0 -0
  93. {simview-6.1.0 → simview-6.1.2}/tests/test_launcher.py +0 -0
  94. {simview-6.1.0 → simview-6.1.2}/tests/test_live.py +0 -0
  95. {simview-6.1.0 → simview-6.1.2}/tests/test_merge.py +0 -0
  96. {simview-6.1.0 → simview-6.1.2}/tests/test_pointcloud_features.py +0 -0
  97. {simview-6.1.0 → simview-6.1.2}/tests/test_remote.py +0 -0
  98. {simview-6.1.0 → simview-6.1.2}/tests/test_roundtrip.py +0 -0
  99. {simview-6.1.0 → simview-6.1.2}/tests/test_scene.py +0 -0
  100. {simview-6.1.0 → simview-6.1.2}/tests/test_server.py +0 -0
  101. {simview-6.1.0 → simview-6.1.2}/tests/test_show.py +0 -0
  102. {simview-6.1.0 → simview-6.1.2}/tests/test_terrain.py +0 -0
  103. {simview-6.1.0 → simview-6.1.2}/tests/test_terrain_query.py +0 -0
  104. {simview-6.1.0 → simview-6.1.2}/tests/test_trajectory.py +0 -0
  105. {simview-6.1.0 → simview-6.1.2}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simview
3
- Version: 6.1.0
3
+ Version: 6.1.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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "simview"
7
- version = "6.1.0"
7
+ version = "6.1.2"
8
8
  readme = "README.md"
9
9
  requires-python = ">=3.12"
10
10
  license = "BSD-3-Clause"
@@ -602,6 +602,9 @@ export class SimView {
602
602
  // actually flips body/terrain visuals) stay in sync instead of
603
603
  // uiState silently drifting out from under the displayed panel.
604
604
  if (this.uiControls) {
605
+ if (Number.isFinite(state.playbackSpeed) && this.animationController) {
606
+ this.animationController.setSpeed(state.playbackSpeed);
607
+ }
605
608
  this.uiControls.applyViewState(state);
606
609
  }
607
610
  } catch (e) {
@@ -623,6 +626,10 @@ export class SimView {
623
626
  bodyVisualizationMode: this.uiState.bodyVisualizationMode,
624
627
  terrainColorMode: this.uiState.terrainColorMode,
625
628
  toggles: toggleMapFromUiState(this.uiState),
629
+ pointCloudsVisible: this.uiState.pointCloudsVisible,
630
+ trackBody: this.uiState.trackBody,
631
+ terrainColorMap: this.uiState.terrainColorMap,
632
+ playbackSpeed: this.animationController ? this.animationController.playbackSpeed : undefined,
626
633
  };
627
634
  }
628
635
 
@@ -750,8 +757,11 @@ export class SimView {
750
757
  const currentBodyPos = new THREE.Vector3(pos.x + offset.x, pos.y + offset.y, pos.z + offset.z);
751
758
 
752
759
  try {
753
- if (this._lastTrackedBody !== this.uiState.trackBody) {
754
- // On first tracking or switch, center the target on the body, preserving the viewing angle
760
+ if (
761
+ this._lastTrackedBody !== this.uiState.trackBody ||
762
+ this._lastTrackedBatch !== activeBatch
763
+ ) {
764
+ // On first tracking or a body/batch switch, center the target on the body, preserving the viewing angle
755
765
  const delta = currentBodyPos.clone().sub(this.scene.controls.target);
756
766
  this.scene.camera.position.add(delta);
757
767
  this.scene.controls.target.copy(currentBodyPos);
@@ -772,11 +782,13 @@ export class SimView {
772
782
  }
773
783
 
774
784
  this._lastTrackedBody = this.uiState.trackBody;
785
+ this._lastTrackedBatch = activeBatch;
775
786
  this._lastTrackedPosition = currentBodyPos.clone();
776
787
  }
777
788
  }
778
789
  } else {
779
790
  this._lastTrackedBody = null;
791
+ this._lastTrackedBatch = null;
780
792
  this._lastTrackedPosition = null;
781
793
  }
782
794
  this.scene.animate(now);
@@ -126,6 +126,10 @@ export class AnimationController {
126
126
 
127
127
  setSpeed(speed) {
128
128
  this.playbackSpeed = speed;
129
+ // Keep the playback bar's dropdown in step with programmatic callers.
130
+ if (this.playbackControls && this.playbackControls.speedSelect) {
131
+ this.playbackControls.speedSelect.value = String(speed);
132
+ }
129
133
  }
130
134
 
131
135
  setRecordingFormat(format) {
@@ -152,12 +152,19 @@ export class BatchManager {
152
152
  return { x: 0, y: 0, z: 0 };
153
153
  }
154
154
 
155
- changeFocusOnBatchByIndex(batchIndex) {
155
+ // Moves camera and target by the offset between the two batches' grid
156
+ // cells, so the view keeps its place relative to the scene. Snapping the
157
+ // target to the cell origin instead lost the view on every chart click
158
+ // (which re-selects the closest batch) and broke body tracking.
159
+ changeFocusOnBatchByIndex(batchIndex, fromBatchIndex) {
156
160
  const { camera, controls } = this.app.scene;
157
- const { x, y, z } = this.getBatchOffset(batchIndex);
158
- const newCameraTarget = new THREE.Vector3(x, y, z);
159
- camera.position.add(newCameraTarget.clone().sub(controls.target));
160
- controls.target = newCameraTarget;
161
+ const to = this.getBatchOffset(batchIndex);
162
+ const from = this.getBatchOffset(fromBatchIndex);
163
+ const shift = new THREE.Vector3(to.x - from.x, to.y - from.y, to.z - from.z);
164
+ if (shift.lengthSq() === 0) return;
165
+ camera.position.add(shift);
166
+ controls.target.add(shift);
167
+ controls.update();
161
168
  }
162
169
 
163
170
  setActiveBatch(batchIndex) {
@@ -167,8 +174,9 @@ export class BatchManager {
167
174
  console.warn("Invalid batch index:", batchIndex);
168
175
  return;
169
176
  }
177
+ const previousBatch = this.currentlyActiveBatch;
170
178
  this.currentlyActiveBatch = batchIndex;
171
- this.changeFocusOnBatchByIndex(batchIndex);
179
+ this.changeFocusOnBatchByIndex(batchIndex, previousBatch);
172
180
  // In focused mode the newly-active batch is (probably) not the one
173
181
  // currently built/drawn, so refresh before the panels read from it.
174
182
  this._recomputeVisibleBatches();
@@ -607,6 +607,21 @@ export class UIControls {
607
607
  if (ctrl) ctrl.setValue(state.terrainColorMode);
608
608
  }
609
609
 
610
+ if (typeof state.terrainColorMap === "string") {
611
+ const known = Array.isArray(colorMapOptions) ? colorMapOptions : Object.keys(colorMapOptions);
612
+ const ctrl = this.findController("colorMap");
613
+ if (ctrl && known.includes(state.terrainColorMap)) ctrl.setValue(state.terrainColorMap);
614
+ }
615
+
616
+ if (typeof state.pointCloudsVisible === "boolean") {
617
+ const ctrl = this.findController("showPointClouds");
618
+ if (ctrl) ctrl.setValue(state.pointCloudsVisible);
619
+ }
620
+
621
+ if (typeof state.trackBody === "string" && this.trackBodyCtrl) {
622
+ if (this.trackableBodyNames().includes(state.trackBody)) this.trackBodyCtrl.setValue(state.trackBody);
623
+ }
624
+
610
625
  if (state.toggles && typeof state.toggles === "object") {
611
626
  const propertyForKey = {
612
627
  axesVisible: "showAxes",
@@ -7,7 +7,7 @@ import {
7
7
  normalizeEpisodes,
8
8
  } from "../utils/episodes.js";
9
9
  import { makeChart, yIncrements } from "../utils/uplot.js";
10
- import { batchColumnsCsv, closestSeries, exportBar } from "./chartControls.js";
10
+ import { batchColumnsCsv, closestSeries, exportBar, finiteBounds } from "./chartControls.js";
11
11
 
12
12
  export class ScalarPlotter {
13
13
  constructor(app, scalarNames) {
@@ -35,7 +35,7 @@ export class ScalarPlotter {
35
35
  // {scalarName -> per-batch array of per-episode aggregates}, rebuilt
36
36
  // only when the episodes or the underlying series change.
37
37
  this.episodeAggregateCache = new Map();
38
- this.seriesRenderCallback = null;
38
+ this.markerDirty = false;
39
39
  this.opacityRenderCallback = null;
40
40
  this.minRenderDelay = 1000 / FREQ_CONFIG.scalarPlotter;
41
41
  this.lastRenderTime = Number.NEGATIVE_INFINITY;
@@ -140,16 +140,7 @@ export class ScalarPlotter {
140
140
  // scalar as one whole-trajectory Float32Array.
141
141
  const series = store.getScalarSeries(scalarName, batchSize);
142
142
  this.scalarSeries.set(scalarName, series);
143
-
144
- let min = Number.MAX_VALUE;
145
- let max = Number.MIN_VALUE;
146
- for (const batchSeries of series) {
147
- for (const { y } of batchSeries) {
148
- min = Math.min(min, y);
149
- max = Math.max(max, y);
150
- }
151
- }
152
- this.scalarBounds.set(scalarName, [min, max]);
143
+ this.scalarBounds.set(scalarName, finiteBounds(series));
153
144
  }
154
145
 
155
146
  this._initializePlots();
@@ -314,6 +305,7 @@ export class ScalarPlotter {
314
305
  },
315
306
  ],
316
307
  tooltip: (u, idx) => this._tooltipHtml(u, idx, name),
308
+ markerTime: () => this.times[this.currentEndIndex] ?? null,
317
309
  hooks: {
318
310
  draw: [
319
311
  (u) => {
@@ -331,7 +323,16 @@ export class ScalarPlotter {
331
323
  }
332
324
  },
333
325
  },
334
- [[], ...new Array(this.app.batchManager.simBatches).fill([])],
326
+ // The whole timeline, plotted once; playback only moves the
327
+ // marker. NaN (a gap in the source) becomes null, uPlot's gap.
328
+ [
329
+ this.times,
330
+ ...this.scalarSeries
331
+ .get(name)
332
+ .map((batchSeries) =>
333
+ batchSeries.map(({ y }) => (Number.isFinite(y) ? y : null))
334
+ ),
335
+ ],
335
336
  this.app
336
337
  );
337
338
 
@@ -354,6 +355,7 @@ export class ScalarPlotter {
354
355
  }
355
356
 
356
357
  setEndIndex(newEndIndex, force = false) {
358
+ if (this.times.length === 0) return; // store not loaded yet (initFromStore pending)
357
359
  if (newEndIndex < 0 || newEndIndex >= this.times.length) {
358
360
  console.warn(
359
361
  "Invalid end index. Must be within the range of time values."
@@ -364,27 +366,7 @@ export class ScalarPlotter {
364
366
  return;
365
367
  }
366
368
  this.currentEndIndex = newEndIndex;
367
- const activeChart = this.charts.get(this.activeScalar);
368
- if (!activeChart) {
369
- console.warn(`No chart found for scalar "${this.activeScalar}".`);
370
- return;
371
- }
372
-
373
- const scalarData = this.scalarSeries.get(this.activeScalar);
374
- if (!scalarData) {
375
- console.warn(`No data points found for scalar "${this.activeScalar}".`);
376
- return;
377
- }
378
-
379
- this.seriesRenderCallback = () => {
380
- const numPoints = this.currentEndIndex + 1;
381
- const xValues = this.times.slice(0, numPoints);
382
- const data = [xValues];
383
- for (let i = 0; i < this.app.batchManager.simBatches; i++) {
384
- data.push(scalarData[i].slice(0, numPoints).map((p) => p.y));
385
- }
386
- activeChart.setData(data, false);
387
- };
369
+ this.markerDirty = true;
388
370
  }
389
371
 
390
372
  setFocusedBatch(batchIndex, force = false) {
@@ -436,9 +418,8 @@ export class ScalarPlotter {
436
418
  }
437
419
 
438
420
  var needRender = false;
439
- if (this.seriesRenderCallback) {
440
- this.seriesRenderCallback();
441
- this.seriesRenderCallback = null;
421
+ if (this.markerDirty) {
422
+ this.markerDirty = false;
442
423
  needRender = true;
443
424
  }
444
425
  if (this.opacityRenderCallback) {
@@ -448,10 +429,9 @@ export class ScalarPlotter {
448
429
  }
449
430
 
450
431
  if (!needRender) return;
451
- // rebuildPaths must be true: the series data changes on every frame of
452
- // playback, and redrawing without rebuilding just repaints the paths
453
- // built for whatever data the chart first saw.
454
- activeChart.redraw(true, true);
432
+ // The data never changes after init, so the cached paths stay valid;
433
+ // strokes (focus opacity) are re-read on every draw anyway.
434
+ activeChart.redraw(false);
455
435
  }
456
436
 
457
437
  animate(now) {
@@ -20,6 +20,21 @@ export function closestSeries(u, dataIdx, yVal) {
20
20
  return best;
21
21
  }
22
22
 
23
+ // [min, max] over the finite y of every batch's {x, y} points; [0, 1] when
24
+ // there are none, so a chart never gets NaN/Infinity bounds.
25
+ export function finiteBounds(seriesPerBatch) {
26
+ let min = Infinity;
27
+ let max = -Infinity;
28
+ for (const batchSeries of seriesPerBatch) {
29
+ for (const { y } of batchSeries) {
30
+ if (!Number.isFinite(y)) continue;
31
+ if (y < min) min = y;
32
+ if (y > max) max = y;
33
+ }
34
+ }
35
+ return min <= max ? [min, max] : [0, 1];
36
+ }
37
+
23
38
  // CSV with a time column, then one column per batch (named after the batch's
24
39
  // display name); `seriesPerBatch[b][i]` is an {x, y} point or missing.
25
40
  export function batchColumnsCsv(batchManager, times, seriesPerBatch) {
@@ -30,11 +30,9 @@ const BOOLEAN_FLAG_KEYS = [
30
30
  "terrainVisualizationModes.normals",
31
31
  ];
32
32
 
33
- // Deliberately NOT in the list above: `pointCloudsVisible`. Appending only
34
- // works for flags that default off -- a link made before the flag existed has
35
- // no bit for it, which decodes as false, so a default-on flag would arrive
36
- // switched off and hide point clouds on every old link. Adding it needs a
37
- // format version that can tell "absent" from "false".
33
+ // Deliberately NOT in the list above: `pointCloudsVisible`. It defaults on,
34
+ // so a bit would decode old links (bit absent) as off; it travels as the
35
+ // named `pc=0|1` key instead, where absent means "leave alone".
38
36
 
39
37
  function getPath(obj, path) {
40
38
  return path.split(".").reduce((o, k) => (o && typeof o === "object" ? o[k] : undefined), obj);
@@ -87,6 +85,11 @@ function decodeFlags(mask) {
87
85
  // bodyVisualizationMode: string,
88
86
  // terrainColorMode: string,
89
87
  // toggles: { [key in BOOLEAN_FLAG_KEYS]?: boolean },
88
+ // pointCloudsVisible: boolean, // `pc=0|1` -- a named key, not a flag bit,
89
+ // // so "absent" (leave alone) != false
90
+ // trackBody: string, // `track=`
91
+ // terrainColorMap: string, // `cmap=`
92
+ // playbackSpeed: number, // `speed=`
90
93
  // }
91
94
  export function serializeViewState(state) {
92
95
  if (!state || typeof state !== "object") return "";
@@ -116,6 +119,18 @@ export function serializeViewState(state) {
116
119
  if (state.toggles && typeof state.toggles === "object") {
117
120
  params.push(`flags=${encodeFlags(state.toggles)}`);
118
121
  }
122
+ if (typeof state.pointCloudsVisible === "boolean") {
123
+ params.push(`pc=${state.pointCloudsVisible ? 1 : 0}`);
124
+ }
125
+ if (typeof state.trackBody === "string" && state.trackBody) {
126
+ params.push(`track=${encodeURIComponent(state.trackBody)}`);
127
+ }
128
+ if (typeof state.terrainColorMap === "string" && state.terrainColorMap) {
129
+ params.push(`cmap=${encodeURIComponent(state.terrainColorMap)}`);
130
+ }
131
+ if (Number.isFinite(state.playbackSpeed) && state.playbackSpeed > 0) {
132
+ params.push(`speed=${fmtNum(state.playbackSpeed)}`);
133
+ }
119
134
 
120
135
  return `#${params.join("&")}`;
121
136
  }
@@ -155,6 +170,15 @@ export function parseViewState(hash) {
155
170
  const mask = parseInt(params.get("flags"), 10);
156
171
  if (Number.isInteger(mask)) state.toggles = decodeFlags(mask);
157
172
 
173
+ const pc = params.get("pc");
174
+ if (pc === "0" || pc === "1") state.pointCloudsVisible = pc === "1";
175
+ const track = params.get("track");
176
+ if (track) state.trackBody = track;
177
+ const cmap = params.get("cmap");
178
+ if (cmap) state.terrainColorMap = cmap;
179
+ const speed = num("speed");
180
+ if (Number.isFinite(speed) && speed > 0) state.playbackSpeed = speed;
181
+
158
182
  return state;
159
183
  }
160
184
 
@@ -47,6 +47,10 @@ def view_hash(
47
47
  body_mode: str | None = None,
48
48
  terrain_color_mode: str | None = None,
49
49
  flags: Mapping[str, bool] | None = None,
50
+ point_clouds: bool | None = None,
51
+ track: str | None = None,
52
+ color_map: str | None = None,
53
+ speed: float | None = None,
50
54
  data: str | None = None,
51
55
  ui: bool | None = None,
52
56
  ) -> str:
@@ -55,6 +59,9 @@ def view_hash(
55
59
  `t` is the playback time in seconds, `cam`/`tgt` the camera position and
56
60
  orbit target, `batch` the focused batch index, `flags` a map of the dotted
57
61
  toggle names in `BOOLEAN_FLAG_KEYS` (unlisted ones default to off).
62
+ `point_clouds`, `track` (a body name, or ``"None"``), `color_map` and
63
+ `speed` are carried as named keys; leaving one ``None`` leaves the
64
+ viewer's current setting alone.
58
65
  `data` (base URL of a static bundle, see `SimulationScene.save_static`) and
59
66
  `ui=False` (hide all panels) are startup options, not view state.
60
67
  """
@@ -81,6 +88,16 @@ def view_hash(
81
88
  )
82
89
  mask = sum(1 << i for i, k in enumerate(BOOLEAN_FLAG_KEYS) if flags.get(k))
83
90
  params.append(f"flags={mask}")
91
+ if point_clouds is not None:
92
+ params.append(f"pc={int(bool(point_clouds))}")
93
+ if track:
94
+ params.append(f"track={quote(track, safe='')}")
95
+ if color_map:
96
+ params.append(f"cmap={quote(color_map, safe='')}")
97
+ if speed is not None:
98
+ if speed <= 0:
99
+ raise ValueError(f"speed must be positive; got {speed!r}")
100
+ params.append(f"speed={_num(speed)}")
84
101
  if params:
85
102
  params.insert(0, "v=1")
86
103
  if data is not None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simview
3
- Version: 6.1.0
3
+ Version: 6.1.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
@@ -35,6 +35,13 @@ def test_view_hash():
35
35
  assert view_hash(ui=False, data="http://h/x") == "#data=http%3A%2F%2Fh%2Fx&ui=0"
36
36
  with pytest.raises(ValueError):
37
37
  view_hash(flags={"nope": True})
38
+ # Named carry-over keys, in the JS serializer's order; absent means "leave alone".
39
+ assert (
40
+ view_hash(point_clouds=False, track="robot 1", color_map="viridis", speed=0.25)
41
+ == "#v=1&pc=0&track=robot%201&cmap=viridis&speed=0.25"
42
+ )
43
+ with pytest.raises(ValueError):
44
+ view_hash(speed=0)
38
45
 
39
46
 
40
47
  def _urls(o):
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes