simview 3.6__tar.gz → 4.0.0__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 (197) hide show
  1. {simview-3.6/simview.egg-info → simview-4.0.0}/PKG-INFO +4 -4
  2. {simview-3.6 → simview-4.0.0}/README.md +3 -3
  3. {simview-3.6 → simview-4.0.0}/pyproject.toml +1 -1
  4. {simview-3.6 → simview-4.0.0}/simview/__main__.py +4 -3
  5. {simview-3.6 → simview-4.0.0}/simview/info.py +15 -23
  6. {simview-3.6 → simview-4.0.0}/simview/merge.py +51 -79
  7. {simview-3.6 → simview-4.0.0}/simview/model.py +138 -81
  8. {simview-3.6 → simview-4.0.0}/simview/scene.py +40 -8
  9. {simview-3.6 → simview-4.0.0}/simview/server.py +4 -8
  10. simview-4.0.0/simview/static/css/controls.css +104 -0
  11. {simview-3.6 → simview-4.0.0}/simview/static/js/SimView.js +56 -11
  12. {simview-3.6 → simview-4.0.0}/simview/static/js/components/BatchManager.js +5 -6
  13. {simview-3.6 → simview-4.0.0}/simview/static/js/components/InteractionController.js +123 -32
  14. {simview-3.6 → simview-4.0.0}/simview/static/js/config.js +9 -1
  15. {simview-3.6 → simview-4.0.0}/simview/static/js/objects/Body.js +98 -1
  16. {simview-3.6 → simview-4.0.0}/simview/static/js/objects/Terrain.js +184 -101
  17. simview-4.0.0/simview/static/js/objects/colormap.js +63 -0
  18. simview-4.0.0/simview/static/js/objects/similarity.js +38 -0
  19. {simview-3.6 → simview-4.0.0}/simview/static/js/objects/utils.js +25 -4
  20. {simview-3.6 → simview-4.0.0}/simview/static/js/ui/Controls.js +110 -9
  21. simview-4.0.0/simview/static/js/ui/Legend.js +154 -0
  22. {simview-3.6 → simview-4.0.0}/simview/static/js/ui/TerrainProfile.js +49 -52
  23. {simview-3.6 → simview-4.0.0}/simview/static/js/utils/terrainSample.js +16 -0
  24. {simview-3.6 → simview-4.0.0}/simview/terrain.py +20 -23
  25. {simview-3.6 → simview-4.0.0/simview.egg-info}/PKG-INFO +4 -4
  26. {simview-3.6 → simview-4.0.0}/simview.egg-info/SOURCES.txt +3 -0
  27. {simview-3.6 → simview-4.0.0}/tests/test_cli.py +0 -4
  28. {simview-3.6 → simview-4.0.0}/tests/test_info.py +1 -2
  29. simview-4.0.0/tests/test_pointcloud_features.py +135 -0
  30. {simview-3.6 → simview-4.0.0}/tests/test_roundtrip.py +18 -15
  31. {simview-3.6 → simview-4.0.0}/tests/test_scene.py +4 -5
  32. {simview-3.6 → simview-4.0.0}/tests/test_server.py +0 -18
  33. {simview-3.6 → simview-4.0.0}/tests/test_terrain_query.py +11 -11
  34. simview-3.6/simview/static/css/controls.css +0 -36
  35. simview-3.6/simview/static/js/ui/Legend.js +0 -103
  36. {simview-3.6 → simview-4.0.0}/LICENSE +0 -0
  37. {simview-3.6 → simview-4.0.0}/MANIFEST.in +0 -0
  38. {simview-3.6 → simview-4.0.0}/setup.cfg +0 -0
  39. {simview-3.6 → simview-4.0.0}/simview/__init__.py +0 -0
  40. {simview-3.6 → simview-4.0.0}/simview/diff.py +0 -0
  41. {simview-3.6 → simview-4.0.0}/simview/launcher.py +0 -0
  42. {simview-3.6 → simview-4.0.0}/simview/live.py +0 -0
  43. {simview-3.6 → simview-4.0.0}/simview/py.typed +0 -0
  44. {simview-3.6 → simview-4.0.0}/simview/render.py +0 -0
  45. {simview-3.6 → simview-4.0.0}/simview/state.py +0 -0
  46. {simview-3.6 → simview-4.0.0}/simview/static/js/components/AnimationController.js +0 -0
  47. {simview-3.6 → simview-4.0.0}/simview/static/js/components/InteractionControls.js +0 -0
  48. {simview-3.6 → simview-4.0.0}/simview/static/js/components/Scene.js +0 -0
  49. {simview-3.6 → simview-4.0.0}/simview/static/js/components/StateStore.js +0 -0
  50. {simview-3.6 → simview-4.0.0}/simview/static/js/main.js +0 -0
  51. {simview-3.6 → simview-4.0.0}/simview/static/js/objects/StaticObject.js +0 -0
  52. {simview-3.6 → simview-4.0.0}/simview/static/js/ui/AnalysisPanel.js +0 -0
  53. {simview-3.6 → simview-4.0.0}/simview/static/js/ui/BatchLegend.js +0 -0
  54. {simview-3.6 → simview-4.0.0}/simview/static/js/ui/BodyStateWindow.js +0 -0
  55. {simview-3.6 → simview-4.0.0}/simview/static/js/ui/ErrorMetrics.js +0 -0
  56. {simview-3.6 → simview-4.0.0}/simview/static/js/ui/PlaybackControls.js +0 -0
  57. {simview-3.6 → simview-4.0.0}/simview/static/js/ui/ScalarPlotter.js +0 -0
  58. {simview-3.6 → simview-4.0.0}/simview/static/js/utils/batchPresets.js +0 -0
  59. {simview-3.6 → simview-4.0.0}/simview/static/js/utils/blobCodec.js +0 -0
  60. {simview-3.6 → simview-4.0.0}/simview/static/js/utils/bodyTransforms.js +0 -0
  61. {simview-3.6 → simview-4.0.0}/simview/static/js/utils/csv.js +0 -0
  62. {simview-3.6 → simview-4.0.0}/simview/static/js/utils/errorMath.js +0 -0
  63. {simview-3.6 → simview-4.0.0}/simview/static/js/utils/injectStyles.js +0 -0
  64. {simview-3.6 → simview-4.0.0}/simview/static/js/utils/interpolate.js +0 -0
  65. {simview-3.6 → simview-4.0.0}/simview/static/js/utils/liveFollow.js +0 -0
  66. {simview-3.6 → simview-4.0.0}/simview/static/js/utils/loadRecordingLibs.js +0 -0
  67. {simview-3.6 → simview-4.0.0}/simview/static/js/utils/viewState.js +0 -0
  68. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/index.min.js +0 -0
  69. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/Color.js +0 -0
  70. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/chroma.js +0 -0
  71. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/colors/colorbrewer.js +0 -0
  72. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/colors/w3cx11.js +0 -0
  73. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/generator/average.js +0 -0
  74. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/generator/bezier.js +0 -0
  75. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/generator/blend.js +0 -0
  76. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/generator/cubehelix.js +0 -0
  77. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/generator/mix.js +0 -0
  78. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/generator/random.js +0 -0
  79. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/generator/scale.js +0 -0
  80. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/_hsx.js +0 -0
  81. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/hcg.js +0 -0
  82. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/hsi.js +0 -0
  83. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/hsl.js +0 -0
  84. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/hsv.js +0 -0
  85. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/index.js +0 -0
  86. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/lab.js +0 -0
  87. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/lch.js +0 -0
  88. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/lrgb.js +0 -0
  89. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/num.js +0 -0
  90. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/oklab.js +0 -0
  91. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/oklch.js +0 -0
  92. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/rgb.js +0 -0
  93. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/cmyk/cmyk2rgb.js +0 -0
  94. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/cmyk/index.js +0 -0
  95. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/cmyk/rgb2cmyk.js +0 -0
  96. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/css/css2rgb.js +0 -0
  97. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/css/hsl2css.js +0 -0
  98. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/css/index.js +0 -0
  99. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/css/lab2css.js +0 -0
  100. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/css/lch2css.js +0 -0
  101. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/css/oklab2css.js +0 -0
  102. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/css/oklch2css.js +0 -0
  103. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/css/rgb2css.js +0 -0
  104. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/gl/index.js +0 -0
  105. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/hcg/hcg2rgb.js +0 -0
  106. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/hcg/index.js +0 -0
  107. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/hcg/rgb2hcg.js +0 -0
  108. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/hex/hex2rgb.js +0 -0
  109. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/hex/index.js +0 -0
  110. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/hex/rgb2hex.js +0 -0
  111. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/hsi/hsi2rgb.js +0 -0
  112. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/hsi/index.js +0 -0
  113. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/hsi/rgb2hsi.js +0 -0
  114. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/hsl/hsl2rgb.js +0 -0
  115. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/hsl/index.js +0 -0
  116. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/hsl/rgb2hsl.js +0 -0
  117. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/hsv/hsv2rgb.js +0 -0
  118. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/hsv/index.js +0 -0
  119. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/hsv/rgb2hsv.js +0 -0
  120. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/input.js +0 -0
  121. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/lab/index.js +0 -0
  122. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/lab/lab-constants.js +0 -0
  123. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/lab/lab2rgb.js +0 -0
  124. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/lab/rgb2lab.js +0 -0
  125. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/lch/hcl2rgb.js +0 -0
  126. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/lch/index.js +0 -0
  127. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/lch/lab2lch.js +0 -0
  128. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/lch/lch2lab.js +0 -0
  129. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/lch/lch2rgb.js +0 -0
  130. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/lch/rgb2lch.js +0 -0
  131. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/named/index.js +0 -0
  132. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/num/index.js +0 -0
  133. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/num/num2rgb.js +0 -0
  134. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/num/rgb2num.js +0 -0
  135. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/oklab/index.js +0 -0
  136. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/oklab/oklab2rgb.js +0 -0
  137. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/oklab/rgb2oklab.js +0 -0
  138. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/oklch/index.js +0 -0
  139. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/oklch/oklch2rgb.js +0 -0
  140. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/oklch/rgb2oklch.js +0 -0
  141. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/rgb/index.js +0 -0
  142. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/temp/index.js +0 -0
  143. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/temp/rgb2temperature.js +0 -0
  144. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/io/temp/temperature2rgb.js +0 -0
  145. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/ops/alpha.js +0 -0
  146. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/ops/clipped.js +0 -0
  147. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/ops/darken.js +0 -0
  148. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/ops/get.js +0 -0
  149. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/ops/luminance.js +0 -0
  150. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/ops/mix.js +0 -0
  151. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/ops/premultiply.js +0 -0
  152. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/ops/saturate.js +0 -0
  153. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/ops/set.js +0 -0
  154. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/ops/shade.js +0 -0
  155. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/utils/analyze.js +0 -0
  156. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/utils/clip_rgb.js +0 -0
  157. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/utils/contrast.js +0 -0
  158. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/utils/contrastAPCA.js +0 -0
  159. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/utils/delta-e.js +0 -0
  160. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/utils/distance.js +0 -0
  161. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/utils/index.js +0 -0
  162. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/utils/last.js +0 -0
  163. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/utils/limit.js +0 -0
  164. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/utils/multiply-matrices.js +0 -0
  165. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/utils/scales.js +0 -0
  166. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/utils/type.js +0 -0
  167. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/utils/unpack.js +0 -0
  168. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/utils/valid.js +0 -0
  169. {simview-3.6 → simview-4.0.0}/simview/static/lib/chroma-js-3.1.2/src/version.js +0 -0
  170. {simview-3.6 → simview-4.0.0}/simview/static/lib/download.js +0 -0
  171. {simview-3.6 → simview-4.0.0}/simview/static/lib/js-colormaps.js +0 -0
  172. {simview-3.6 → simview-4.0.0}/simview/static/lib/tar.js +0 -0
  173. {simview-3.6 → simview-4.0.0}/simview/static/lib/three-0.174.0/addons/controls/OrbitControls.js +0 -0
  174. {simview-3.6 → simview-4.0.0}/simview/static/lib/three-0.174.0/addons/libs/lil-gui.module.min.js +0 -0
  175. {simview-3.6 → simview-4.0.0}/simview/static/lib/three-0.174.0/three.core.js +0 -0
  176. {simview-3.6 → simview-4.0.0}/simview/static/lib/three-0.174.0/three.module.js +0 -0
  177. {simview-3.6 → simview-4.0.0}/simview/static/lib/uPlot.esm.js +0 -0
  178. {simview-3.6 → simview-4.0.0}/simview/static/lib/uPlot.min.css +0 -0
  179. {simview-3.6 → simview-4.0.0}/simview/static/textures/contacts/red-cross0.png +0 -0
  180. {simview-3.6 → simview-4.0.0}/simview/static/textures/points/ball0.png +0 -0
  181. {simview-3.6 → simview-4.0.0}/simview/static/textures/points/ball1.png +0 -0
  182. {simview-3.6 → simview-4.0.0}/simview/templates/index.html +0 -0
  183. {simview-3.6 → simview-4.0.0}/simview/utils.py +0 -0
  184. {simview-3.6 → simview-4.0.0}/simview.egg-info/dependency_links.txt +0 -0
  185. {simview-3.6 → simview-4.0.0}/simview.egg-info/entry_points.txt +0 -0
  186. {simview-3.6 → simview-4.0.0}/simview.egg-info/requires.txt +0 -0
  187. {simview-3.6 → simview-4.0.0}/simview.egg-info/top_level.txt +0 -0
  188. {simview-3.6 → simview-4.0.0}/tests/test_columnar_states.py +0 -0
  189. {simview-3.6 → simview-4.0.0}/tests/test_contacts.py +0 -0
  190. {simview-3.6 → simview-4.0.0}/tests/test_diff.py +0 -0
  191. {simview-3.6 → simview-4.0.0}/tests/test_launcher.py +0 -0
  192. {simview-3.6 → simview-4.0.0}/tests/test_live.py +0 -0
  193. {simview-3.6 → simview-4.0.0}/tests/test_merge.py +0 -0
  194. {simview-3.6 → simview-4.0.0}/tests/test_show.py +0 -0
  195. {simview-3.6 → simview-4.0.0}/tests/test_terrain.py +0 -0
  196. {simview-3.6 → simview-4.0.0}/tests/test_trajectory.py +0 -0
  197. {simview-3.6 → simview-4.0.0}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simview
3
- Version: 3.6
3
+ Version: 4.0.0
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
@@ -34,7 +34,7 @@ Dynamic: license-file
34
34
  # SimView Visualizer
35
35
 
36
36
  [![Documentation](https://img.shields.io/badge/docs-GitHub%20Pages-blue.svg)](https://vlk-jan.github.io/simview/)
37
- [![Live Demo](https://img.shields.io/badge/demo-live-brightgreen.svg)](https://vlk-jan.github.io/assets/demos/simview/)
37
+ [![Live Demo](https://img.shields.io/badge/demo-live-brightgreen.svg)](https://vlk-jan.github.io/simview/demo/)
38
38
 
39
39
  **SimView** is a powerful and interactive tool for visualizing 3D models and terrain data in simulations. It enables you to explore and analyze multiple simulation scenarios (batches) within a shared environment, all defined through an intuitive JSON format or a Python API.
40
40
 
@@ -50,8 +50,8 @@ the API reference, and developer/architecture docs — is available at:
50
50
  **[https://vlk-jan.github.io/simview/](https://vlk-jan.github.io/simview/)**
51
51
 
52
52
  Want to try it without installing anything? There's a
53
- [live browser demo](https://vlk-jan.github.io/assets/demos/simview/)
54
- (static snapshot, no backend needed).
53
+ [live browser demo](https://vlk-jan.github.io/simview/demo/)
54
+ (always up-to-date with the current code, no backend needed).
55
55
 
56
56
  ---
57
57
 
@@ -1,7 +1,7 @@
1
1
  # SimView Visualizer
2
2
 
3
3
  [![Documentation](https://img.shields.io/badge/docs-GitHub%20Pages-blue.svg)](https://vlk-jan.github.io/simview/)
4
- [![Live Demo](https://img.shields.io/badge/demo-live-brightgreen.svg)](https://vlk-jan.github.io/assets/demos/simview/)
4
+ [![Live Demo](https://img.shields.io/badge/demo-live-brightgreen.svg)](https://vlk-jan.github.io/simview/demo/)
5
5
 
6
6
  **SimView** is a powerful and interactive tool for visualizing 3D models and terrain data in simulations. It enables you to explore and analyze multiple simulation scenarios (batches) within a shared environment, all defined through an intuitive JSON format or a Python API.
7
7
 
@@ -17,8 +17,8 @@ the API reference, and developer/architecture docs — is available at:
17
17
  **[https://vlk-jan.github.io/simview/](https://vlk-jan.github.io/simview/)**
18
18
 
19
19
  Want to try it without installing anything? There's a
20
- [live browser demo](https://vlk-jan.github.io/assets/demos/simview/)
21
- (static snapshot, no backend needed).
20
+ [live browser demo](https://vlk-jan.github.io/simview/demo/)
21
+ (always up-to-date with the current code, no backend needed).
22
22
 
23
23
  ---
24
24
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "simview"
7
- version = "3.6"
7
+ version = "4.0.0"
8
8
  readme = "README.md"
9
9
  requires-python = ">=3.12"
10
10
  license = "BSD-3-Clause"
@@ -374,11 +374,12 @@ def build_parser() -> argparse.ArgumentParser:
374
374
  )
375
375
  parser.add_argument(
376
376
  "--layer",
377
- choices=["height", "friction", "stiffness", "all"],
378
377
  default="all",
379
378
  help=(
380
- "With 'simview terrain <file>', which terrain layer(s) to query "
381
- "(default: all present)."
379
+ "With 'simview terrain <file>', which terrain layer to query: "
380
+ "'height', 'all' (default: all present), or the name of any "
381
+ "arbitrary property the terrain was authored with (e.g. "
382
+ "'friction', 'stiffness', or a custom name)."
382
383
  ),
383
384
  )
384
385
  parser.add_argument(
@@ -69,8 +69,7 @@ def _summarize_terrain(terrain: dict) -> dict:
69
69
  bounds = terrain.get("bounds") or {}
70
70
  height = terrain.get("heightData")
71
71
  normals = terrain.get("normals")
72
- friction = terrain.get("frictionData")
73
- stiffness = terrain.get("stiffnessData")
72
+ properties = terrain.get("properties") or {}
74
73
  return {
75
74
  "extent": {"x": dims.get("sizeX"), "y": dims.get("sizeY")},
76
75
  "shape": {"x": dims.get("resolutionX"), "y": dims.get("resolutionY")},
@@ -83,28 +82,18 @@ def _summarize_terrain(terrain: dict) -> dict:
83
82
  "max_z": bounds.get("maxZ"),
84
83
  },
85
84
  "is_singleton": terrain.get("isSingleton"),
86
- "has_friction": friction is not None,
87
- "has_stiffness": stiffness is not None,
88
- "friction_bounds": (
89
- {"min": bounds.get("minFriction"), "max": bounds.get("maxFriction")}
90
- if friction is not None
91
- else None
92
- ),
93
- "stiffness_bounds": (
94
- {"min": bounds.get("minStiffness"), "max": bounds.get("maxStiffness")}
95
- if stiffness is not None
96
- else None
97
- ),
98
85
  "height_data_encoding": _encoding_of(height),
99
86
  "height_data_bytes": _blob_byte_length(height),
100
87
  "normals_encoding": _encoding_of(normals),
101
88
  "normals_bytes": _blob_byte_length(normals),
102
- "friction_data_encoding": _encoding_of(friction)
103
- if friction is not None
104
- else None,
105
- "stiffness_data_encoding": (
106
- _encoding_of(stiffness) if stiffness is not None else None
107
- ),
89
+ "properties": {
90
+ name: {
91
+ "bounds": {"min": prop.get("min"), "max": prop.get("max")},
92
+ "encoding": _encoding_of(prop.get("data")),
93
+ "bytes": _blob_byte_length(prop.get("data")),
94
+ }
95
+ for name, prop in properties.items()
96
+ },
108
97
  }
109
98
 
110
99
 
@@ -466,9 +455,12 @@ def format_text(summary: dict) -> str:
466
455
  f" Bounds: x=[{b['min_x']},{b['max_x']}] "
467
456
  f"y=[{b['min_y']},{b['max_y']}] z=[{b['min_z']},{b['max_z']}]"
468
457
  )
469
- friction = "yes" if terrain["has_friction"] else "no"
470
- stiffness = "yes" if terrain["has_stiffness"] else "no"
471
- lines.append(f" Friction data: {friction} Stiffness data: {stiffness}")
458
+ props = terrain["properties"]
459
+ if props:
460
+ names = ", ".join(sorted(props))
461
+ lines.append(f" Properties: {names}")
462
+ else:
463
+ lines.append(" Properties: (none)")
472
464
  h_bytes = terrain["height_data_bytes"]
473
465
  n_bytes = terrain["normals_bytes"]
474
466
  lines.append(
@@ -64,13 +64,13 @@ def _decode_state_field(value, width: int):
64
64
 
65
65
 
66
66
  def _decode_per_batch(value: list | str, batch_size: int) -> list:
67
- """Normalize a terrain data field (heightData/normals/frictionData/
68
- stiffnessData) to a plain list of length `batch_size`, one entry per
69
- batch, regardless of whether it's a binary ``__b64__`` blob (one flat
70
- buffer covering all batches) or an already-batched plain list. Used so
71
- that inputs mixing binary and plain-list encoding can still be
72
- concatenated -- each file's field is decoded independently rather than
73
- branching on a single file's encoding."""
67
+ """Normalize a terrain data field (heightData/normals/a named property's
68
+ data) to a plain list of length `batch_size`, one entry per batch,
69
+ regardless of whether it's a binary ``__b64__`` blob (one flat buffer
70
+ covering all batches) or an already-batched plain list. Used so that
71
+ inputs mixing binary and plain-list encoding can still be concatenated --
72
+ each file's field is decoded independently rather than branching on a
73
+ single file's encoding."""
74
74
  flat = _decode_b64_floats(value)
75
75
  if flat is None:
76
76
  # `_decode_b64_floats` only returns None for non-`__b64__` input, and
@@ -276,26 +276,28 @@ def _merge_terrain(
276
276
  f"'{labels[0]}' dimensions {dims}."
277
277
  )
278
278
 
279
- has_friction = all(
280
- model["terrain"].get("frictionData") is not None for model in models
281
- )
282
- has_stiffness = all(
283
- model["terrain"].get("stiffnessData") is not None for model in models
284
- )
285
- if not has_friction and any(
286
- model["terrain"].get("frictionData") is not None for model in models
287
- ):
288
- logger.warning(
289
- "Not all files provide terrain friction data; dropping "
290
- "frictionData from the merged terrain."
291
- )
292
- if not has_stiffness and any(
293
- model["terrain"].get("stiffnessData") is not None for model in models
294
- ):
295
- logger.warning(
296
- "Not all files provide terrain stiffness data; dropping "
297
- "stiffnessData from the merged terrain."
279
+ # Union of property names across all files, in first-seen order. A property
280
+ # present in every file is kept (concatenated + bounds merged); one present
281
+ # in only some is dropped with a warning, same all-or-nothing rule as
282
+ # heightData/normals require unconditionally.
283
+ property_names: list[str] = []
284
+ for model in models:
285
+ for name in model["terrain"].get("properties") or {}:
286
+ if name not in property_names:
287
+ property_names.append(name)
288
+ kept_properties = []
289
+ for name in property_names:
290
+ present_in_all = all(
291
+ name in (model["terrain"].get("properties") or {}) for model in models
298
292
  )
293
+ if present_in_all:
294
+ kept_properties.append(name)
295
+ else:
296
+ logger.warning(
297
+ "Not all files provide terrain property '%s'; dropping it "
298
+ "from the merged terrain.",
299
+ name,
300
+ )
299
301
 
300
302
  def _concat_lists_or_b64(items: list[tuple]) -> list | None:
301
303
  # Each item is decoded independently, keyed by its own batch_size (not
@@ -309,9 +311,12 @@ def _merge_terrain(
309
311
  merged.extend(_decode_per_batch(value, batch_size))
310
312
  return merged
311
313
 
312
- height_data, normals, friction_data, stiffness_data = [], [], [], []
314
+ height_data, normals = [], []
315
+ property_data: dict[str, list] = {name: [] for name in kept_properties}
313
316
  min_z = max_z = None
314
- min_friction = max_friction = min_stiffness = max_stiffness = None
317
+ property_min_max: dict[str, tuple[float | None, float | None]] = {
318
+ name: (None, None) for name in kept_properties
319
+ }
315
320
  for model, batch_size, label in zip(models, batch_sizes, labels):
316
321
  terrain = model["terrain"]
317
322
  singleton = terrain.get("isSingleton", False)
@@ -335,51 +340,18 @@ def _merge_terrain(
335
340
  bounds = terrain["bounds"]
336
341
  min_z = bounds["minZ"] if min_z is None else min(min_z, bounds["minZ"])
337
342
  max_z = bounds["maxZ"] if max_z is None else max(max_z, bounds["maxZ"])
338
- if has_friction:
339
- friction_data.append(
343
+ for name in kept_properties:
344
+ prop = terrain["properties"][name]
345
+ property_data[name].append(
340
346
  (
341
- _expand_batched(
342
- terrain["frictionData"],
343
- singleton,
344
- batch_size,
345
- "frictionData",
346
- label,
347
- ),
347
+ _expand_batched(prop["data"], singleton, batch_size, name, label),
348
348
  batch_size,
349
349
  )
350
350
  )
351
- min_friction = (
352
- bounds["minFriction"]
353
- if min_friction is None
354
- else min(min_friction, bounds["minFriction"])
355
- )
356
- max_friction = (
357
- bounds["maxFriction"]
358
- if max_friction is None
359
- else max(max_friction, bounds["maxFriction"])
360
- )
361
- if has_stiffness:
362
- stiffness_data.append(
363
- (
364
- _expand_batched(
365
- terrain["stiffnessData"],
366
- singleton,
367
- batch_size,
368
- "stiffnessData",
369
- label,
370
- ),
371
- batch_size,
372
- )
373
- )
374
- min_stiffness = (
375
- bounds["minStiffness"]
376
- if min_stiffness is None
377
- else min(min_stiffness, bounds["minStiffness"])
378
- )
379
- max_stiffness = (
380
- bounds["maxStiffness"]
381
- if max_stiffness is None
382
- else max(max_stiffness, bounds["maxStiffness"])
351
+ cur_min, cur_max = property_min_max[name]
352
+ property_min_max[name] = (
353
+ prop["min"] if cur_min is None else min(cur_min, prop["min"]),
354
+ prop["max"] if cur_max is None else max(cur_max, prop["max"]),
383
355
  )
384
356
 
385
357
  merged_bounds = {
@@ -390,12 +362,15 @@ def _merge_terrain(
390
362
  "minZ": min_z,
391
363
  "maxZ": max_z,
392
364
  }
393
- if has_friction:
394
- merged_bounds["minFriction"] = min_friction
395
- merged_bounds["maxFriction"] = max_friction
396
- if has_stiffness:
397
- merged_bounds["minStiffness"] = min_stiffness
398
- merged_bounds["maxStiffness"] = max_stiffness
365
+
366
+ merged_properties = {
367
+ name: {
368
+ "data": _concat_lists_or_b64(property_data[name]),
369
+ "min": property_min_max[name][0],
370
+ "max": property_min_max[name][1],
371
+ }
372
+ for name in kept_properties
373
+ }
399
374
 
400
375
  return {
401
376
  "dimensions": dims,
@@ -403,10 +378,7 @@ def _merge_terrain(
403
378
  "isSingleton": False,
404
379
  "heightData": _concat_lists_or_b64(height_data),
405
380
  "normals": _concat_lists_or_b64(normals),
406
- "frictionData": _concat_lists_or_b64(friction_data) if has_friction else None,
407
- "stiffnessData": _concat_lists_or_b64(stiffness_data)
408
- if has_stiffness
409
- else None,
381
+ "properties": merged_properties,
410
382
  }
411
383
 
412
384
 
@@ -49,6 +49,27 @@ class OptionalBodyStateAttribute(StrEnum):
49
49
  TORQUE = "torque"
50
50
 
51
51
 
52
+ @dataclass
53
+ class TerrainProperty:
54
+ """One arbitrary named per-cell scalar field over the terrain grid (e.g.
55
+ friction, stiffness, or any other user-defined property), stored the same
56
+ way as `SimViewTerrain.height_data` -- a plain nested list, or an opaque
57
+ `__b64__`-prefixed blob string for compactness."""
58
+
59
+ data: list[list[float]] | str
60
+ # Value range used by the viewer to normalize the color map (analogous to
61
+ # min_z/max_z for height). None when not computed.
62
+ min: float | None = None
63
+ max: float | None = None
64
+
65
+ def to_json(self):
66
+ return {"data": self.data, "min": self.min, "max": self.max}
67
+
68
+ @classmethod
69
+ def from_dict(cls, d: dict) -> "TerrainProperty":
70
+ return cls(data=d["data"], min=d.get("min"), max=d.get("max"))
71
+
72
+
52
73
  @dataclass
53
74
  class SimViewTerrain:
54
75
  extent_x: float
@@ -68,33 +89,22 @@ class SimViewTerrain:
68
89
  height_data: list[list[float]] | str
69
90
  normals: list[list[list[float]]] | str
70
91
  is_singleton: bool
71
- friction_data: list[list[float]] | str | None = None
72
- stiffness_data: list[list[float]] | str | None = None
73
- # Value ranges used by the viewer to normalize the color map (analogous to
74
- # min_z/max_z for height). None when the corresponding data is absent.
75
- min_friction: float | None = None
76
- max_friction: float | None = None
77
- min_stiffness: float | None = None
78
- max_stiffness: float | None = None
92
+ # Arbitrary named per-cell scalar fields (e.g. "friction", "stiffness", or
93
+ # any other user-defined property), keyed by name -- see `TerrainProperty`.
94
+ # Adding a new property never requires touching this class or the viewer:
95
+ # supply it by name in `create()`'s `properties` dict and it becomes
96
+ # selectable as a terrain color mode automatically.
97
+ properties: dict[str, TerrainProperty] = field(default_factory=dict)
98
+ # Per-cell K-wide feature vector (e.g. a reduced-dim PCA projection of a
99
+ # learned backbone's features), stored the same way as `normals` (width
100
+ # inferred client-side from flat length, not shipped explicitly). Used by
101
+ # the viewer's "features" color mode: cosine similarity to a clicked cell,
102
+ # computed entirely in-browser -- not a named scalar property, so it has
103
+ # no min/max bounds (similarity is always [-1, 1]) and isn't part of
104
+ # `properties`.
105
+ embedding_data: list[list[float]] | str | None = None
79
106
 
80
107
  def to_json(self):
81
- # min/max friction/stiffness are independent Optional fields (not tied to
82
- # friction_data/stiffness_data by the type system, only by convention in
83
- # create()/from_dict), so the dict's values are genuinely `float | None`.
84
- bounds: dict[str, float | None] = {
85
- "minX": self.min_x,
86
- "minY": self.min_y,
87
- "maxX": self.max_x,
88
- "maxY": self.max_y,
89
- "minZ": self.min_z,
90
- "maxZ": self.max_z,
91
- }
92
- if self.friction_data is not None:
93
- bounds["minFriction"] = self.min_friction
94
- bounds["maxFriction"] = self.max_friction
95
- if self.stiffness_data is not None:
96
- bounds["minStiffness"] = self.min_stiffness
97
- bounds["maxStiffness"] = self.max_stiffness
98
108
  return {
99
109
  "dimensions": {
100
110
  "sizeX": self.extent_x,
@@ -102,22 +112,31 @@ class SimViewTerrain:
102
112
  "resolutionX": self.shape_x,
103
113
  "resolutionY": self.shape_y,
104
114
  },
105
- "bounds": bounds,
115
+ "bounds": {
116
+ "minX": self.min_x,
117
+ "minY": self.min_y,
118
+ "maxX": self.max_x,
119
+ "maxY": self.max_y,
120
+ "minZ": self.min_z,
121
+ "maxZ": self.max_z,
122
+ },
106
123
  "heightData": self.height_data,
107
124
  "normals": self.normals,
108
125
  "isSingleton": self.is_singleton,
109
- "frictionData": self.friction_data,
110
- "stiffnessData": self.stiffness_data,
126
+ "properties": {
127
+ name: prop.to_json() for name, prop in self.properties.items()
128
+ },
129
+ "embeddingData": self.embedding_data,
111
130
  }
112
131
 
113
132
  @classmethod
114
133
  def from_dict(cls, d: dict) -> "SimViewTerrain":
115
134
  """Reconstruct a SimViewTerrain from the dict produced by `to_json`.
116
135
 
117
- `heightData`/`normals`/`frictionData`/`stiffnessData` are kept in
118
- whatever form they were serialized in (plain nested lists or a
119
- `__b64__` blob string) -- decode with `simview.model._decode_blob` if
120
- you need the flat float values back out.
136
+ `heightData`/`normals`/each property's `data` are kept in whatever
137
+ form they were serialized in (plain nested lists or a `__b64__` blob
138
+ string) -- decode with `simview.model._decode_blob` if you need the
139
+ flat float values back out.
121
140
  """
122
141
  try:
123
142
  dimensions = d["dimensions"]
@@ -128,6 +147,11 @@ class SimViewTerrain:
128
147
  except KeyError as e:
129
148
  raise ValueError(f"Terrain dict is missing required key: {e}") from e
130
149
 
150
+ properties = {
151
+ name: TerrainProperty.from_dict(p)
152
+ for name, p in (d.get("properties") or {}).items()
153
+ }
154
+
131
155
  return cls(
132
156
  extent_x=dimensions["sizeX"],
133
157
  extent_y=dimensions["sizeY"],
@@ -142,12 +166,8 @@ class SimViewTerrain:
142
166
  height_data=height_data,
143
167
  normals=normals,
144
168
  is_singleton=is_singleton,
145
- friction_data=d.get("frictionData"),
146
- stiffness_data=d.get("stiffnessData"),
147
- min_friction=bounds.get("minFriction"),
148
- max_friction=bounds.get("maxFriction"),
149
- min_stiffness=bounds.get("minStiffness"),
150
- max_stiffness=bounds.get("maxStiffness"),
169
+ properties=properties,
170
+ embedding_data=d.get("embeddingData"),
151
171
  )
152
172
 
153
173
  @staticmethod
@@ -157,8 +177,8 @@ class SimViewTerrain:
157
177
  x_lim: tuple[float, float],
158
178
  y_lim: tuple[float, float],
159
179
  is_singleton: bool,
160
- friction_map: torch.Tensor | None = None,
161
- stiffness_map: torch.Tensor | None = None,
180
+ properties: dict[str, torch.Tensor] | None = None,
181
+ embedding_map: torch.Tensor | None = None,
162
182
  ) -> "SimViewTerrain":
163
183
  if heightmap.ndim != 3:
164
184
  raise ValueError(
@@ -186,31 +206,30 @@ class SimViewTerrain:
186
206
  rearrange(normals, "b c d1 d2 -> b (d1 d2) c").cpu().numpy()
187
207
  )
188
208
 
189
- friction_data_list = None
190
- min_friction = max_friction = None
191
- if friction_map is not None:
192
- if friction_map.ndim != 3:
209
+ properties_out: dict[str, TerrainProperty] = {}
210
+ for name, prop_map in (properties or {}).items():
211
+ if prop_map.ndim != 3:
193
212
  raise ValueError(
194
- f"Friction map must include a batch dimension (ndim=3); got ndim={friction_map.ndim}."
213
+ f"Property '{name}' map must include a batch dimension "
214
+ f"(ndim=3); got ndim={prop_map.ndim}."
195
215
  )
196
- friction_data_list = _encode_blob(
197
- rearrange(friction_map, "b d1 d2 -> b (d1 d2)").cpu().numpy()
216
+ properties_out[name] = TerrainProperty(
217
+ data=_encode_blob(
218
+ rearrange(prop_map, "b d1 d2 -> b (d1 d2)").cpu().numpy()
219
+ ),
220
+ min=prop_map.min().item(),
221
+ max=prop_map.max().item(),
198
222
  )
199
- min_friction = friction_map.min().item()
200
- max_friction = friction_map.max().item()
201
223
 
202
- stiffness_data_list = None
203
- min_stiffness = max_stiffness = None
204
- if stiffness_map is not None:
205
- if stiffness_map.ndim != 3:
224
+ embedding_data_list = None
225
+ if embedding_map is not None:
226
+ if embedding_map.ndim != 4:
206
227
  raise ValueError(
207
- f"Stiffness map must include a batch dimension (ndim=3); got ndim={stiffness_map.ndim}."
228
+ f"Embedding map must include a batch dimension (ndim=4); got ndim={embedding_map.ndim}."
208
229
  )
209
- stiffness_data_list = _encode_blob(
210
- rearrange(stiffness_map, "b d1 d2 -> b (d1 d2)").cpu().numpy()
230
+ embedding_data_list = _encode_blob(
231
+ rearrange(embedding_map, "b k d1 d2 -> b (d1 d2) k").cpu().numpy()
211
232
  )
212
- min_stiffness = stiffness_map.min().item()
213
- max_stiffness = stiffness_map.max().item()
214
233
 
215
234
  return SimViewTerrain(
216
235
  extent_x=extent_x,
@@ -226,12 +245,8 @@ class SimViewTerrain:
226
245
  height_data=height_data_list,
227
246
  normals=normals_list,
228
247
  is_singleton=is_singleton,
229
- friction_data=friction_data_list,
230
- stiffness_data=stiffness_data_list,
231
- min_friction=min_friction,
232
- max_friction=max_friction,
233
- min_stiffness=min_stiffness,
234
- max_stiffness=max_stiffness,
248
+ properties=properties_out,
249
+ embedding_data=embedding_data_list,
235
250
  )
236
251
 
237
252
 
@@ -335,7 +350,37 @@ class SimViewBody:
335
350
  )
336
351
 
337
352
  @staticmethod
338
- def create_pointcloud(name: str, points: torch.Tensor, **kwargs) -> "SimViewBody":
353
+ def create_pointcloud(
354
+ name: str,
355
+ points: torch.Tensor,
356
+ color: torch.Tensor | None = None,
357
+ embedding: torch.Tensor | None = None,
358
+ **kwargs,
359
+ ) -> "SimViewBody":
360
+ """`color` (N, 3) in [0, 1] is an optional static per-point RGB color,
361
+ used by the viewer for vertex-colored rendering. `embedding` (N, K) is
362
+ an optional per-point K-wide feature vector (e.g. a reduced-dim PCA
363
+ projection of a learned backbone's features) enabling the viewer's
364
+ click-to-similarity color mode: cosine similarity to a clicked point,
365
+ computed entirely in-browser from this data. Both round-trip through
366
+ the existing generic `__b64__` blob mechanism -- no new wire format."""
367
+ if points.ndim != 2 or points.shape[1] != 3:
368
+ raise ValueError(
369
+ f"points must have shape (N, 3); got {tuple(points.shape)}."
370
+ )
371
+ N = points.shape[0]
372
+ if color is not None:
373
+ if tuple(color.shape) != (N, 3):
374
+ raise ValueError(
375
+ f"color must have shape ({N}, 3) matching points; got {tuple(color.shape)}."
376
+ )
377
+ kwargs["color"] = color
378
+ if embedding is not None:
379
+ if embedding.ndim != 2 or embedding.shape[0] != N:
380
+ raise ValueError(
381
+ f"embedding must have shape ({N}, K) matching points; got {tuple(embedding.shape)}."
382
+ )
383
+ kwargs["embedding"] = embedding
339
384
  return SimViewBody.create(
340
385
  name, BodyShapeType.POINTCLOUD, points=points, **kwargs
341
386
  )
@@ -532,8 +577,8 @@ class SimViewModel:
532
577
  x_lim: tuple[float, float] | None = None,
533
578
  y_lim: tuple[float, float] | None = None,
534
579
  grid_res: float | None = None,
535
- friction_map: torch.Tensor | None = None,
536
- stiffness_map: torch.Tensor | None = None,
580
+ properties: dict[str, torch.Tensor] | None = None,
581
+ embedding_map: torch.Tensor | None = None,
537
582
  ) -> None:
538
583
  """Adds terrain to the internal simulation model.
539
584
 
@@ -545,8 +590,14 @@ class SimViewModel:
545
590
  y_lim (tuple[float, float] | None): (min, max) coordinates for the Y axis.
546
591
  grid_res (float | None): Grid resolution. If x_lim and y_lim are omitted,
547
592
  they will be automatically inferred assuming the grid is centered at 0.
548
- friction_map (torch.Tensor | None): Optional friction coefficient map.
549
- stiffness_map (torch.Tensor | None): Optional stiffness coefficient map.
593
+ properties (dict[str, torch.Tensor] | None): Optional arbitrary named
594
+ per-cell scalar maps (2D or 3D, like `heightmap`), e.g.
595
+ `{"friction": friction_map, "stiffness": stiffness_map}`. Each becomes
596
+ selectable as a terrain color mode in the viewer automatically, with no
597
+ further code changes needed.
598
+ embedding_map (torch.Tensor | None): Optional per-cell K-wide feature map
599
+ (3D channels-first `(K, Dy, Dx)` or 4D `(B, K, Dy, Dx)`, like `normals`)
600
+ enabling the viewer's click-to-similarity "features" color mode.
550
601
  """
551
602
  if heightmap.ndim == 2:
552
603
  heightmap = heightmap.unsqueeze(0) # add batch dim
@@ -574,10 +625,12 @@ class SimViewModel:
574
625
 
575
626
  if normals.ndim == 3: # channels first
576
627
  normals = normals.unsqueeze(0) # add batch dim
577
- if friction_map is not None and friction_map.ndim == 2:
578
- friction_map = friction_map.unsqueeze(0)
579
- if stiffness_map is not None and stiffness_map.ndim == 2:
580
- stiffness_map = stiffness_map.unsqueeze(0)
628
+ properties = {
629
+ name: (prop.unsqueeze(0) if prop.ndim == 2 else prop)
630
+ for name, prop in (properties or {}).items()
631
+ }
632
+ if embedding_map is not None and embedding_map.ndim == 3: # channels first
633
+ embedding_map = embedding_map.unsqueeze(0)
581
634
 
582
635
  # Each field's batch dim must be either 1 (shared across all batches) or
583
636
  # exactly batch_size (per-batch). Anything else is a mistake, and the old
@@ -586,8 +639,8 @@ class SimViewModel:
586
639
  provided = {
587
640
  "heightmap": heightmap,
588
641
  "normals": normals,
589
- "friction_map": friction_map,
590
- "stiffness_map": stiffness_map,
642
+ "embedding_map": embedding_map,
643
+ **properties,
591
644
  }
592
645
  for name, tensor in provided.items():
593
646
  if tensor is not None and tensor.shape[0] not in (1, self.batch_size):
@@ -609,10 +662,14 @@ class SimViewModel:
609
662
  heightmap = heightmap.repeat(self.batch_size, 1, 1)
610
663
  if normals.shape[0] == 1:
611
664
  normals = normals.repeat(self.batch_size, 1, 1, 1)
612
- if friction_map is not None and friction_map.shape[0] == 1:
613
- friction_map = friction_map.repeat(self.batch_size, 1, 1)
614
- if stiffness_map is not None and stiffness_map.shape[0] == 1:
615
- stiffness_map = stiffness_map.repeat(self.batch_size, 1, 1)
665
+ properties = {
666
+ name: (
667
+ prop.repeat(self.batch_size, 1, 1) if prop.shape[0] == 1 else prop
668
+ )
669
+ for name, prop in properties.items()
670
+ }
671
+ if embedding_map is not None and embedding_map.shape[0] == 1:
672
+ embedding_map = embedding_map.repeat(self.batch_size, 1, 1, 1)
616
673
 
617
674
  self.terrain = SimViewTerrain.create(
618
675
  heightmap=heightmap,
@@ -620,8 +677,8 @@ class SimViewModel:
620
677
  x_lim=x_lim,
621
678
  y_lim=y_lim,
622
679
  is_singleton=is_singleton,
623
- friction_map=friction_map,
624
- stiffness_map=stiffness_map,
680
+ properties=properties,
681
+ embedding_map=embedding_map,
625
682
  )
626
683
 
627
684
  def create_body(