simview 3.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- simview/__init__.py +69 -0
- simview/__main__.py +174 -0
- simview/launcher.py +109 -0
- simview/live.py +204 -0
- simview/merge.py +593 -0
- simview/model.py +690 -0
- simview/py.typed +0 -0
- simview/scene.py +603 -0
- simview/server.py +739 -0
- simview/state.py +190 -0
- simview/static/css/controls.css +36 -0
- simview/static/js/SimView.js +601 -0
- simview/static/js/components/AnimationController.js +654 -0
- simview/static/js/components/BatchManager.js +198 -0
- simview/static/js/components/InteractionController.js +446 -0
- simview/static/js/components/InteractionControls.js +116 -0
- simview/static/js/components/Scene.js +166 -0
- simview/static/js/components/StateStore.js +188 -0
- simview/static/js/config.js +233 -0
- simview/static/js/main.js +3 -0
- simview/static/js/objects/Body.js +616 -0
- simview/static/js/objects/StaticObject.js +181 -0
- simview/static/js/objects/Terrain.js +514 -0
- simview/static/js/objects/utils.js +378 -0
- simview/static/js/ui/AnalysisPanel.js +233 -0
- simview/static/js/ui/BatchLegend.js +207 -0
- simview/static/js/ui/BodyStateWindow.js +521 -0
- simview/static/js/ui/Controls.js +586 -0
- simview/static/js/ui/ErrorMetrics.js +664 -0
- simview/static/js/ui/Legend.js +87 -0
- simview/static/js/ui/PlaybackControls.js +314 -0
- simview/static/js/ui/ScalarPlotter.js +575 -0
- simview/static/js/utils/blobCodec.js +73 -0
- simview/static/js/utils/bodyTransforms.js +151 -0
- simview/static/js/utils/csv.js +46 -0
- simview/static/js/utils/errorMath.js +69 -0
- simview/static/js/utils/injectStyles.js +16 -0
- simview/static/js/utils/interpolate.js +65 -0
- simview/static/js/utils/liveFollow.js +17 -0
- simview/static/js/utils/loadRecordingLibs.js +34 -0
- simview/static/js/utils/viewState.js +226 -0
- simview/static/lib/chroma-js-3.1.2/index.min.js +8 -0
- simview/static/lib/chroma-js-3.1.2/src/Color.js +49 -0
- simview/static/lib/chroma-js-3.1.2/src/chroma.js +10 -0
- simview/static/lib/chroma-js-3.1.2/src/colors/colorbrewer.js +81 -0
- simview/static/lib/chroma-js-3.1.2/src/colors/w3cx11.js +164 -0
- simview/static/lib/chroma-js-3.1.2/src/generator/average.js +88 -0
- simview/static/lib/chroma-js-3.1.2/src/generator/bezier.js +86 -0
- simview/static/lib/chroma-js-3.1.2/src/generator/blend.js +57 -0
- simview/static/lib/chroma-js-3.1.2/src/generator/cubehelix.js +87 -0
- simview/static/lib/chroma-js-3.1.2/src/generator/mix.js +19 -0
- simview/static/lib/chroma-js-3.1.2/src/generator/random.js +12 -0
- simview/static/lib/chroma-js-3.1.2/src/generator/scale.js +394 -0
- simview/static/lib/chroma-js-3.1.2/src/interpolator/_hsx.js +59 -0
- simview/static/lib/chroma-js-3.1.2/src/interpolator/hcg.js +12 -0
- simview/static/lib/chroma-js-3.1.2/src/interpolator/hsi.js +12 -0
- simview/static/lib/chroma-js-3.1.2/src/interpolator/hsl.js +12 -0
- simview/static/lib/chroma-js-3.1.2/src/interpolator/hsv.js +12 -0
- simview/static/lib/chroma-js-3.1.2/src/interpolator/index.js +1 -0
- simview/static/lib/chroma-js-3.1.2/src/interpolator/lab.js +19 -0
- simview/static/lib/chroma-js-3.1.2/src/interpolator/lch.js +13 -0
- simview/static/lib/chroma-js-3.1.2/src/interpolator/lrgb.js +19 -0
- simview/static/lib/chroma-js-3.1.2/src/interpolator/num.js +15 -0
- simview/static/lib/chroma-js-3.1.2/src/interpolator/oklab.js +19 -0
- simview/static/lib/chroma-js-3.1.2/src/interpolator/oklch.js +12 -0
- simview/static/lib/chroma-js-3.1.2/src/interpolator/rgb.js +18 -0
- simview/static/lib/chroma-js-3.1.2/src/io/cmyk/cmyk2rgb.js +16 -0
- simview/static/lib/chroma-js-3.1.2/src/io/cmyk/index.js +27 -0
- simview/static/lib/chroma-js-3.1.2/src/io/cmyk/rgb2cmyk.js +17 -0
- simview/static/lib/chroma-js-3.1.2/src/io/css/css2rgb.js +238 -0
- simview/static/lib/chroma-js-3.1.2/src/io/css/hsl2css.js +26 -0
- simview/static/lib/chroma-js-3.1.2/src/io/css/index.js +27 -0
- simview/static/lib/chroma-js-3.1.2/src/io/css/lab2css.js +24 -0
- simview/static/lib/chroma-js-3.1.2/src/io/css/lch2css.js +24 -0
- simview/static/lib/chroma-js-3.1.2/src/io/css/oklab2css.js +16 -0
- simview/static/lib/chroma-js-3.1.2/src/io/css/oklch2css.js +16 -0
- simview/static/lib/chroma-js-3.1.2/src/io/css/rgb2css.js +61 -0
- simview/static/lib/chroma-js-3.1.2/src/io/gl/index.js +22 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hcg/hcg2rgb.js +55 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hcg/index.js +27 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hcg/rgb2hcg.js +23 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hex/hex2rgb.js +56 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hex/index.js +29 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hex/rgb2hex.js +29 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hsi/hsi2rgb.js +45 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hsi/index.js +27 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hsi/rgb2hsi.js +31 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hsl/hsl2rgb.js +35 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hsl/index.js +27 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hsl/rgb2hsl.js +45 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hsv/hsv2rgb.js +47 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hsv/index.js +27 -0
- simview/static/lib/chroma-js-3.1.2/src/io/hsv/rgb2hsv.js +32 -0
- simview/static/lib/chroma-js-3.1.2/src/io/input.js +4 -0
- simview/static/lib/chroma-js-3.1.2/src/io/lab/index.js +28 -0
- simview/static/lib/chroma-js-3.1.2/src/io/lab/lab-constants.js +120 -0
- simview/static/lib/chroma-js-3.1.2/src/io/lab/lab2rgb.js +101 -0
- simview/static/lib/chroma-js-3.1.2/src/io/lab/rgb2lab.js +67 -0
- simview/static/lib/chroma-js-3.1.2/src/io/lch/hcl2rgb.js +9 -0
- simview/static/lib/chroma-js-3.1.2/src/io/lch/index.js +35 -0
- simview/static/lib/chroma-js-3.1.2/src/io/lch/lab2lch.js +12 -0
- simview/static/lib/chroma-js-3.1.2/src/io/lch/lch2lab.js +18 -0
- simview/static/lib/chroma-js-3.1.2/src/io/lch/lch2rgb.js +13 -0
- simview/static/lib/chroma-js-3.1.2/src/io/lch/rgb2lch.js +12 -0
- simview/static/lib/chroma-js-3.1.2/src/io/named/index.js +30 -0
- simview/static/lib/chroma-js-3.1.2/src/io/num/index.js +32 -0
- simview/static/lib/chroma-js-3.1.2/src/io/num/num2rgb.js +13 -0
- simview/static/lib/chroma-js-3.1.2/src/io/num/rgb2num.js +8 -0
- simview/static/lib/chroma-js-3.1.2/src/io/oklab/index.js +27 -0
- simview/static/lib/chroma-js-3.1.2/src/io/oklab/oklab2rgb.js +34 -0
- simview/static/lib/chroma-js-3.1.2/src/io/oklab/rgb2oklab.js +37 -0
- simview/static/lib/chroma-js-3.1.2/src/io/oklch/index.js +27 -0
- simview/static/lib/chroma-js-3.1.2/src/io/oklch/oklch2rgb.js +13 -0
- simview/static/lib/chroma-js-3.1.2/src/io/oklch/rgb2oklch.js +12 -0
- simview/static/lib/chroma-js-3.1.2/src/io/rgb/index.js +44 -0
- simview/static/lib/chroma-js-3.1.2/src/io/temp/index.js +22 -0
- simview/static/lib/chroma-js-3.1.2/src/io/temp/rgb2temperature.js +30 -0
- simview/static/lib/chroma-js-3.1.2/src/io/temp/temperature2rgb.js +39 -0
- simview/static/lib/chroma-js-3.1.2/src/ops/alpha.js +13 -0
- simview/static/lib/chroma-js-3.1.2/src/ops/clipped.js +5 -0
- simview/static/lib/chroma-js-3.1.2/src/ops/darken.js +17 -0
- simview/static/lib/chroma-js-3.1.2/src/ops/get.js +13 -0
- simview/static/lib/chroma-js-3.1.2/src/ops/luminance.js +54 -0
- simview/static/lib/chroma-js-3.1.2/src/ops/mix.js +10 -0
- simview/static/lib/chroma-js-3.1.2/src/ops/premultiply.js +12 -0
- simview/static/lib/chroma-js-3.1.2/src/ops/saturate.js +15 -0
- simview/static/lib/chroma-js-3.1.2/src/ops/set.js +43 -0
- simview/static/lib/chroma-js-3.1.2/src/ops/shade.js +11 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/analyze.js +191 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/clip_rgb.js +15 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/contrast.js +12 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/contrastAPCA.js +68 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/delta-e.js +62 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/distance.js +17 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/index.js +27 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/last.js +8 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/limit.js +5 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/multiply-matrices.js +36 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/scales.js +15 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/type.js +18 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/unpack.js +17 -0
- simview/static/lib/chroma-js-3.1.2/src/utils/valid.js +11 -0
- simview/static/lib/chroma-js-3.1.2/src/version.js +2 -0
- simview/static/lib/download.js +132 -0
- simview/static/lib/js-colormaps.js +19335 -0
- simview/static/lib/tar.js +334 -0
- simview/static/lib/three-0.174.0/addons/controls/OrbitControls.js +1556 -0
- simview/static/lib/three-0.174.0/addons/libs/lil-gui.module.min.js +8 -0
- simview/static/lib/three-0.174.0/three.core.js +48830 -0
- simview/static/lib/three-0.174.0/three.module.js +17313 -0
- simview/static/lib/uPlot.esm.js +6140 -0
- simview/static/lib/uPlot.min.css +2 -0
- simview/static/textures/contacts/red-cross0.png +0 -0
- simview/static/textures/points/ball0.png +0 -0
- simview/static/textures/points/ball1.png +0 -0
- simview/templates/index.html +81 -0
- simview/utils.py +41 -0
- simview-3.2.dist-info/METADATA +552 -0
- simview-3.2.dist-info/RECORD +163 -0
- simview-3.2.dist-info/WHEEL +5 -0
- simview-3.2.dist-info/entry_points.txt +2 -0
- simview-3.2.dist-info/licenses/LICENSE +28 -0
- simview-3.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
import uPlot from "../../lib/uPlot.esm.js";
|
|
2
|
+
import { FREQ_CONFIG, SCALAR_PLOTTER_CONFIG } from "../config.js";
|
|
3
|
+
import { downloadCsv, rowsToCsv, sanitizeForFilename } from "../utils/csv.js";
|
|
4
|
+
import { injectStyles } from "../utils/injectStyles.js";
|
|
5
|
+
|
|
6
|
+
export class ScalarPlotter {
|
|
7
|
+
constructor(app, scalarNames) {
|
|
8
|
+
this.app = app;
|
|
9
|
+
this.scalarNames = scalarNames || [];
|
|
10
|
+
if (this.scalarNames.length === 0) {
|
|
11
|
+
console.warn("No scalar names provided.");
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
this.isExpanded = false;
|
|
15
|
+
this.activeScalar = this.scalarNames[0];
|
|
16
|
+
this.currentEndIndex = 0;
|
|
17
|
+
this.currentFocusedBatch = 0;
|
|
18
|
+
this.plotElements = {};
|
|
19
|
+
this.tabElements = {};
|
|
20
|
+
this.charts = new Map();
|
|
21
|
+
this.resizeObservers = new Map();
|
|
22
|
+
this.scalarSeries = new Map();
|
|
23
|
+
this.scalarBounds = new Map();
|
|
24
|
+
this.fullDataPoints = new Map();
|
|
25
|
+
this.times = [];
|
|
26
|
+
this.indices = [];
|
|
27
|
+
this.seriesRenderCallback = null;
|
|
28
|
+
this.opacityRenderCallback = null;
|
|
29
|
+
this.minRenderDelay = 1000 / FREQ_CONFIG.scalarPlotter;
|
|
30
|
+
this.lastRenderTime = Number.NEGATIVE_INFINITY;
|
|
31
|
+
this.renderTimeout = null; // To manage delayed rendering
|
|
32
|
+
|
|
33
|
+
this._injectStyles();
|
|
34
|
+
this._setupHTML();
|
|
35
|
+
this._setupEventListeners();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_injectStyles() {
|
|
39
|
+
const styleId = "scalar-styles";
|
|
40
|
+
const chartHeightPercentage = 15;
|
|
41
|
+
const css = `
|
|
42
|
+
/* Tab bar */
|
|
43
|
+
.scalar-tab-bar {
|
|
44
|
+
display: flex;
|
|
45
|
+
width: 100%;
|
|
46
|
+
flex-wrap: wrap;
|
|
47
|
+
padding: 0;
|
|
48
|
+
position: sticky;
|
|
49
|
+
top: 0;
|
|
50
|
+
z-index: 1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* Tabs */
|
|
54
|
+
.scalar-tab {
|
|
55
|
+
flex: 1;
|
|
56
|
+
text-align: center;
|
|
57
|
+
padding: 5px 10px; /* Adjusted padding */
|
|
58
|
+
margin-right: 2px; /* Space between tabs */
|
|
59
|
+
cursor: pointer;
|
|
60
|
+
background-color: rgba(0, 0, 0, 1);
|
|
61
|
+
border-radius: 2px 2px 0 0;
|
|
62
|
+
font-size: 1.0em;
|
|
63
|
+
color: white;
|
|
64
|
+
transition: background-color 0.2s ease, color 0.2s ease;
|
|
65
|
+
position: relative;
|
|
66
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
67
|
+
border-bottom: 1px solid white;
|
|
68
|
+
margin-bottom: -1px;
|
|
69
|
+
font-weight: bold;
|
|
70
|
+
white-space: nowrap; /* Prevent wrapping */
|
|
71
|
+
overflow: hidden; /* Hide overflow */
|
|
72
|
+
text-overflow: ellipsis; /* Add ellipsis if text too long */
|
|
73
|
+
}
|
|
74
|
+
.scalar-tab:first-child {
|
|
75
|
+
margin-left: 0;
|
|
76
|
+
border-left: none;
|
|
77
|
+
}
|
|
78
|
+
.scalar-tab:last-child {
|
|
79
|
+
margin-right: 0;
|
|
80
|
+
border-right: none;
|
|
81
|
+
}
|
|
82
|
+
.scalar-tab.active {
|
|
83
|
+
border: 1px solid white;
|
|
84
|
+
border-bottom: 1px solid rgba(0, 0, 0, 1);
|
|
85
|
+
}
|
|
86
|
+
.scalar-tab.active:hover {
|
|
87
|
+
background-color: rgba(0, 0, 0, 1);
|
|
88
|
+
}
|
|
89
|
+
.scalar-tab.active:first-child {
|
|
90
|
+
border-left: none;
|
|
91
|
+
}
|
|
92
|
+
.scalar-tab.active:last-child {
|
|
93
|
+
border-right: none;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* Export toolbar */
|
|
97
|
+
.scalar-export-bar {
|
|
98
|
+
display: flex;
|
|
99
|
+
justify-content: flex-end;
|
|
100
|
+
padding: 6px 0 0;
|
|
101
|
+
}
|
|
102
|
+
.scalar-export-bar button {
|
|
103
|
+
background-color: rgba(50, 50, 50, 0.8);
|
|
104
|
+
color: white;
|
|
105
|
+
border: 1px solid white;
|
|
106
|
+
padding: 0.2em 0.6em;
|
|
107
|
+
border-radius: 3px;
|
|
108
|
+
font-size: 0.85em;
|
|
109
|
+
cursor: pointer;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* Plot area */
|
|
113
|
+
.scalar-plot-area {
|
|
114
|
+
width: 100%;
|
|
115
|
+
height: ${chartHeightPercentage}vh;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.uplot-plot-div {
|
|
119
|
+
border-top: 1px solid white;
|
|
120
|
+
width: 100%;
|
|
121
|
+
height: 99%;
|
|
122
|
+
display: none;
|
|
123
|
+
background-color: rgba(0, 0, 0, 1);
|
|
124
|
+
cursor: pointer;
|
|
125
|
+
}
|
|
126
|
+
.uplot-plot-div.visible {
|
|
127
|
+
display: block;
|
|
128
|
+
}
|
|
129
|
+
.uplot-plot-div .uplot,
|
|
130
|
+
.uplot-plot-div .u-wrap {
|
|
131
|
+
width: 100%;
|
|
132
|
+
height: 100%;
|
|
133
|
+
}
|
|
134
|
+
.uplot-plot-div .u-legend {
|
|
135
|
+
display: none;
|
|
136
|
+
}
|
|
137
|
+
`;
|
|
138
|
+
injectStyles(styleId, css);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
_setupHTML() {
|
|
142
|
+
this.tabBar = document.createElement("div");
|
|
143
|
+
this.tabBar.className = "scalar-tab-bar";
|
|
144
|
+
|
|
145
|
+
this.plotArea = document.createElement("div");
|
|
146
|
+
this.plotArea.className = "scalar-plot-area";
|
|
147
|
+
|
|
148
|
+
this.exportBar = document.createElement("div");
|
|
149
|
+
this.exportBar.className = "scalar-export-bar";
|
|
150
|
+
this.exportButton = document.createElement("button");
|
|
151
|
+
this.exportButton.textContent = "Export CSV";
|
|
152
|
+
this.exportBar.appendChild(this.exportButton);
|
|
153
|
+
this.plotArea.appendChild(this.exportBar);
|
|
154
|
+
|
|
155
|
+
this.scalarNames.forEach((name, index) => {
|
|
156
|
+
const tabButton = document.createElement("button");
|
|
157
|
+
tabButton.className = "scalar-tab";
|
|
158
|
+
tabButton.textContent = name;
|
|
159
|
+
tabButton.dataset.scalarName = name;
|
|
160
|
+
if (index === 0) tabButton.classList.add("active");
|
|
161
|
+
this.tabBar.appendChild(tabButton);
|
|
162
|
+
this.tabElements[name] = tabButton;
|
|
163
|
+
|
|
164
|
+
const plotDiv = document.createElement("div");
|
|
165
|
+
plotDiv.id = `plot-${name}`;
|
|
166
|
+
plotDiv.className = "uplot-plot-div";
|
|
167
|
+
plotDiv.dataset.scalarName = name;
|
|
168
|
+
if (index === 0) plotDiv.classList.add("visible");
|
|
169
|
+
this.plotArea.appendChild(plotDiv);
|
|
170
|
+
this.plotElements[name] = plotDiv;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
_setupEventListeners() {
|
|
175
|
+
this.tabBar.addEventListener("click", (event) => {
|
|
176
|
+
const target = event.target;
|
|
177
|
+
if (target.classList.contains("scalar-tab")) {
|
|
178
|
+
const scalarName = target.dataset.scalarName;
|
|
179
|
+
if (scalarName && scalarName !== this.activeScalar) {
|
|
180
|
+
this._switchTab(scalarName);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
event.target.blur();
|
|
184
|
+
});
|
|
185
|
+
this.exportButton.addEventListener("click", () => this._exportCsv());
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Downloads the active scalar tab's full series as CSV: time, then one
|
|
189
|
+
// column per batch (named after the batch's current display name).
|
|
190
|
+
_exportCsv() {
|
|
191
|
+
const series = this.scalarSeries.get(this.activeScalar);
|
|
192
|
+
if (!series || this.times.length === 0) return;
|
|
193
|
+
|
|
194
|
+
const batchCount = this.app.batchManager.simBatches;
|
|
195
|
+
const header = ["time"];
|
|
196
|
+
for (let i = 0; i < batchCount; i++) {
|
|
197
|
+
header.push(this.app.batchManager.getBatchName(i) || `batch_${i}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const rows = this.times.map((t, idx) => {
|
|
201
|
+
const row = [t];
|
|
202
|
+
for (let i = 0; i < batchCount; i++) {
|
|
203
|
+
const point = series[i][idx];
|
|
204
|
+
row.push(point ? point.y : "");
|
|
205
|
+
}
|
|
206
|
+
return row;
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const csv = rowsToCsv(header, rows);
|
|
210
|
+
const scalarPart = sanitizeForFilename(this.activeScalar);
|
|
211
|
+
downloadCsv(`scalar_${scalarPart}.csv`, csv);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Called by AnalysisPanel when this panel becomes/stops being the visible section.
|
|
215
|
+
setVisible(visible) {
|
|
216
|
+
if (this.isExpanded === visible) return;
|
|
217
|
+
this.isExpanded = visible;
|
|
218
|
+
|
|
219
|
+
if (this.isExpanded && this.activeScalar) {
|
|
220
|
+
this._resizeChart(this.activeScalar);
|
|
221
|
+
this.setEndIndex(this.currentEndIndex, true);
|
|
222
|
+
this.setFocusedBatch(this.currentFocusedBatch, true);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
_switchTab(newScalarName) {
|
|
227
|
+
if (
|
|
228
|
+
!this.scalarNames.includes(newScalarName) ||
|
|
229
|
+
newScalarName === this.activeScalar
|
|
230
|
+
) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const oldTab = this.tabElements[this.activeScalar];
|
|
235
|
+
const oldPlot = this.plotElements[this.activeScalar];
|
|
236
|
+
if (oldTab) oldTab.classList.remove("active");
|
|
237
|
+
if (oldPlot) oldPlot.classList.remove("visible");
|
|
238
|
+
|
|
239
|
+
const newTab = this.tabElements[newScalarName];
|
|
240
|
+
const newPlot = this.plotElements[newScalarName];
|
|
241
|
+
if (newTab) newTab.classList.add("active");
|
|
242
|
+
if (newPlot) newPlot.classList.add("visible");
|
|
243
|
+
|
|
244
|
+
this.activeScalar = newScalarName;
|
|
245
|
+
this._resizeChart(this.activeScalar);
|
|
246
|
+
this.setEndIndex(this.currentEndIndex, true);
|
|
247
|
+
this.setFocusedBatch(this.currentFocusedBatch, true);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
initFromStore(store) {
|
|
251
|
+
const batchSize = this.app.batchManager.simBatches;
|
|
252
|
+
this.times = store.times;
|
|
253
|
+
this.indices = [...Array(batchSize).keys()];
|
|
254
|
+
for (const scalarName of this.scalarNames) {
|
|
255
|
+
// Per-batch series as plain {x, y} points (sliced into uPlot's
|
|
256
|
+
// columnar format on render), pulled from the store rather than
|
|
257
|
+
// walked per-frame here -- the columnar store already holds each
|
|
258
|
+
// scalar as one whole-trajectory Float32Array.
|
|
259
|
+
const series = store.getScalarSeries(scalarName, batchSize);
|
|
260
|
+
this.scalarSeries.set(scalarName, series);
|
|
261
|
+
|
|
262
|
+
let min = Number.MAX_VALUE;
|
|
263
|
+
let max = Number.MIN_VALUE;
|
|
264
|
+
for (const batchSeries of series) {
|
|
265
|
+
for (const { y } of batchSeries) {
|
|
266
|
+
min = Math.min(min, y);
|
|
267
|
+
max = Math.max(max, y);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
this.scalarBounds.set(scalarName, [min, max]);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
for (const scalarName of this.scalarNames) {
|
|
274
|
+
console.log(
|
|
275
|
+
"Scalar",
|
|
276
|
+
scalarName,
|
|
277
|
+
" has bounds",
|
|
278
|
+
this.scalarBounds.get(scalarName)
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
this._initializePlots();
|
|
283
|
+
if (this.isExpanded) {
|
|
284
|
+
this.setEndIndex(this.currentEndIndex, true);
|
|
285
|
+
this.setFocusedBatch(this.currentFocusedBatch, true);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
getChartInterval(min, max) {
|
|
290
|
+
const diff = max - min;
|
|
291
|
+
if (diff === 0)
|
|
292
|
+
return Math.max(
|
|
293
|
+
Math.abs(max) / SCALAR_PLOTTER_CONFIG.stepsPerYAxis,
|
|
294
|
+
1e-3
|
|
295
|
+
);
|
|
296
|
+
return diff / SCALAR_PLOTTER_CONFIG.stepsPerYAxis;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Finds the batch series whose y-value at the clicked x-index is closest
|
|
300
|
+
// to the clicked y-pixel, so a click near a particular line focuses that
|
|
301
|
+
// batch.
|
|
302
|
+
_closestSeriesAtIndex(u, dataIdx, yVal) {
|
|
303
|
+
let bestBatch = -1;
|
|
304
|
+
let bestDist = Infinity;
|
|
305
|
+
for (let i = 0; i < this.app.batchManager.simBatches; i++) {
|
|
306
|
+
const y = u.data[i + 1][dataIdx];
|
|
307
|
+
if (y === null || y === undefined) continue;
|
|
308
|
+
const dist = Math.abs(y - yVal);
|
|
309
|
+
if (dist < bestDist) {
|
|
310
|
+
bestDist = dist;
|
|
311
|
+
bestBatch = i;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return bestBatch;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
_initializePlots() {
|
|
318
|
+
const limOffset = 1e-2;
|
|
319
|
+
this.scalarNames.forEach((name) => {
|
|
320
|
+
const plotDiv = this.plotElements[name];
|
|
321
|
+
var [min, max] = this.scalarBounds.get(name);
|
|
322
|
+
min = min - limOffset;
|
|
323
|
+
max = max + limOffset;
|
|
324
|
+
|
|
325
|
+
const series = [{}];
|
|
326
|
+
for (let i = 0; i < this.app.batchManager.simBatches; i++) {
|
|
327
|
+
series.push({
|
|
328
|
+
label: `${name} ${i}`,
|
|
329
|
+
stroke: this.app.batchManager.getColorForBatch(i),
|
|
330
|
+
width: 1,
|
|
331
|
+
points: { show: false },
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const rect = plotDiv.getBoundingClientRect();
|
|
336
|
+
const chart = new uPlot(
|
|
337
|
+
{
|
|
338
|
+
width: Math.max(rect.width, 1),
|
|
339
|
+
height: Math.max(rect.height, 1),
|
|
340
|
+
padding: [8, 8, 0, 8],
|
|
341
|
+
series,
|
|
342
|
+
scales: {
|
|
343
|
+
x: { time: false, min: this.times[0], max: this.times[this.times.length - 1] },
|
|
344
|
+
y: { min, max },
|
|
345
|
+
},
|
|
346
|
+
axes: [
|
|
347
|
+
{
|
|
348
|
+
show: true,
|
|
349
|
+
stroke: "transparent",
|
|
350
|
+
grid: { show: false },
|
|
351
|
+
ticks: { show: false },
|
|
352
|
+
values: () => [],
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
show: true,
|
|
356
|
+
stroke: "white",
|
|
357
|
+
grid: { stroke: "rgb(53, 53, 53)", width: 1 },
|
|
358
|
+
ticks: { stroke: "rgb(73, 73, 73)" },
|
|
359
|
+
font: "12px Arial",
|
|
360
|
+
space: 30,
|
|
361
|
+
incrs: [this.getChartInterval(min, max)],
|
|
362
|
+
},
|
|
363
|
+
],
|
|
364
|
+
legend: { show: false },
|
|
365
|
+
cursor: {
|
|
366
|
+
drag: { x: false, y: false },
|
|
367
|
+
points: { show: false },
|
|
368
|
+
},
|
|
369
|
+
hooks: {
|
|
370
|
+
setCursor: [
|
|
371
|
+
(u) => {
|
|
372
|
+
this._updateTooltip(u, name);
|
|
373
|
+
},
|
|
374
|
+
],
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
[[], ...new Array(this.app.batchManager.simBatches).fill([])],
|
|
378
|
+
plotDiv
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
chart.over.addEventListener("click", (e) => {
|
|
382
|
+
const idx = chart.cursor.idx;
|
|
383
|
+
if (idx === null || idx === undefined) return;
|
|
384
|
+
const xVal = chart.data[0][idx];
|
|
385
|
+
const yVal = chart.posToVal(e.offsetY, "y");
|
|
386
|
+
const batchIndex = this._closestSeriesAtIndex(chart, idx, yVal);
|
|
387
|
+
if (batchIndex >= 0) {
|
|
388
|
+
this.app.batchManager.setActiveBatch(batchIndex);
|
|
389
|
+
}
|
|
390
|
+
if (xVal !== undefined && xVal !== null && this.app.animationController) {
|
|
391
|
+
this.app.animationController.goToTime(xVal);
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
this._createTooltip(plotDiv);
|
|
396
|
+
|
|
397
|
+
this.charts.set(name, chart);
|
|
398
|
+
|
|
399
|
+
const resizeObserver = new ResizeObserver(() => this._resizeChart(name));
|
|
400
|
+
resizeObserver.observe(plotDiv);
|
|
401
|
+
this.resizeObservers.set(name, resizeObserver);
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
_createTooltip(plotDiv) {
|
|
406
|
+
const tooltip = document.createElement("div");
|
|
407
|
+
tooltip.style.cssText =
|
|
408
|
+
"position:absolute;pointer-events:none;display:none;" +
|
|
409
|
+
"background:rgba(20,20,20,0.9);border:1px solid rgba(255,255,255,0.3);" +
|
|
410
|
+
"border-radius:3px;padding:4px 6px;font-family:Arial;font-size:11px;" +
|
|
411
|
+
"color:white;white-space:nowrap;z-index:10;";
|
|
412
|
+
plotDiv.style.position = "relative";
|
|
413
|
+
plotDiv.appendChild(tooltip);
|
|
414
|
+
plotDiv._tooltip = tooltip;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
_updateTooltip(u, name) {
|
|
418
|
+
const plotDiv = this.plotElements[name];
|
|
419
|
+
const tooltip = plotDiv && plotDiv._tooltip;
|
|
420
|
+
if (!tooltip) return;
|
|
421
|
+
|
|
422
|
+
const idx = u.cursor.idx;
|
|
423
|
+
if (idx === null || idx === undefined || u.cursor.left < 0) {
|
|
424
|
+
tooltip.style.display = "none";
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const yVal = u.posToVal(u.cursor.top, "y");
|
|
429
|
+
const batchIndex = this._closestSeriesAtIndex(u, idx, yVal);
|
|
430
|
+
if (batchIndex < 0) {
|
|
431
|
+
tooltip.style.display = "none";
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const time = u.data[0][idx];
|
|
436
|
+
const value = u.data[batchIndex + 1][idx];
|
|
437
|
+
if (value === null || value === undefined) {
|
|
438
|
+
tooltip.style.display = "none";
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const batchLabel = this.app.batchManager.getBatchName(batchIndex);
|
|
442
|
+
const color = this.app.batchManager.getColorForBatch(batchIndex);
|
|
443
|
+
|
|
444
|
+
tooltip.style.color = color;
|
|
445
|
+
tooltip.innerHTML = `Batch: ${batchLabel}<br>Time: ${time.toFixed(3)}<br>Value: ${value.toFixed(3)}`;
|
|
446
|
+
tooltip.style.display = "block";
|
|
447
|
+
|
|
448
|
+
const left = u.cursor.left + 12;
|
|
449
|
+
const top = u.cursor.top + 12;
|
|
450
|
+
tooltip.style.left = `${left}px`;
|
|
451
|
+
tooltip.style.top = `${top}px`;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
_resizeChart(name) {
|
|
455
|
+
const chart = this.charts.get(name);
|
|
456
|
+
const plotDiv = this.plotElements[name];
|
|
457
|
+
if (!chart || !plotDiv) return;
|
|
458
|
+
const rect = plotDiv.getBoundingClientRect();
|
|
459
|
+
if (rect.width <= 0 || rect.height <= 0) return;
|
|
460
|
+
chart.setSize({ width: rect.width, height: rect.height });
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
setEndIndex(newEndIndex, force = false) {
|
|
464
|
+
if (newEndIndex < 0 || newEndIndex >= this.times.length) {
|
|
465
|
+
console.warn(
|
|
466
|
+
"Invalid end index. Must be within the range of time values."
|
|
467
|
+
);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (this.currentEndIndex === newEndIndex && !force) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
this.currentEndIndex = newEndIndex;
|
|
474
|
+
const activeChart = this.charts.get(this.activeScalar);
|
|
475
|
+
if (!activeChart) {
|
|
476
|
+
console.warn(`No chart found for scalar "${this.activeScalar}".`);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const scalarData = this.scalarSeries.get(this.activeScalar);
|
|
481
|
+
if (!scalarData) {
|
|
482
|
+
console.warn(`No data points found for scalar "${this.activeScalar}".`);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
this.seriesRenderCallback = () => {
|
|
487
|
+
const numPoints = this.currentEndIndex + 1;
|
|
488
|
+
const xValues = this.times.slice(0, numPoints);
|
|
489
|
+
const data = [xValues];
|
|
490
|
+
for (let i = 0; i < this.app.batchManager.simBatches; i++) {
|
|
491
|
+
data.push(scalarData[i].slice(0, numPoints).map((p) => p.y));
|
|
492
|
+
}
|
|
493
|
+
activeChart.setData(data, false);
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
setFocusedBatch(batchIndex, force = false) {
|
|
498
|
+
if (batchIndex < 0 || batchIndex >= this.app.batchManager.simBatches) {
|
|
499
|
+
console.warn("Invalid batch index.");
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
if (this.currentFocusedBatch === batchIndex && !force) {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
this.currentFocusedBatch = batchIndex;
|
|
506
|
+
|
|
507
|
+
const activeChart = this.charts.get(this.activeScalar);
|
|
508
|
+
if (!activeChart) {
|
|
509
|
+
console.warn(`No chart found for scalar "${this.activeScalar}".`);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
this.opacityRenderCallback = () => {
|
|
513
|
+
for (let i = 0; i < this.app.batchManager.simBatches; i++) {
|
|
514
|
+
const opacity =
|
|
515
|
+
i === batchIndex ? 1 : SCALAR_PLOTTER_CONFIG.inactiveBatchOpacity;
|
|
516
|
+
const baseColor = this.app.batchManager.getColorForBatch(i);
|
|
517
|
+
const rgbaColor = this._hexToRgba(baseColor, opacity);
|
|
518
|
+
activeChart.series[i + 1].stroke = rgbaColor;
|
|
519
|
+
activeChart.series[i + 1]._stroke = rgbaColor;
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
_hexToRgba(hex, opacity) {
|
|
525
|
+
hex = hex.replace("#", "");
|
|
526
|
+
const r = parseInt(hex.substring(0, 2), 16);
|
|
527
|
+
const g = parseInt(hex.substring(2, 4), 16);
|
|
528
|
+
const b = parseInt(hex.substring(4, 6), 16);
|
|
529
|
+
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
_renderChart() {
|
|
533
|
+
const activeChart = this.charts.get(this.activeScalar);
|
|
534
|
+
if (!activeChart) return;
|
|
535
|
+
|
|
536
|
+
const plotDiv = this.plotElements[this.activeScalar];
|
|
537
|
+
if (!this.isExpanded || !plotDiv.classList.contains("visible")) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
var needRender = false;
|
|
542
|
+
if (this.seriesRenderCallback) {
|
|
543
|
+
this.seriesRenderCallback();
|
|
544
|
+
this.seriesRenderCallback = null;
|
|
545
|
+
needRender = true;
|
|
546
|
+
}
|
|
547
|
+
if (this.opacityRenderCallback) {
|
|
548
|
+
this.opacityRenderCallback();
|
|
549
|
+
this.opacityRenderCallback = null;
|
|
550
|
+
needRender = true;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (!needRender) return;
|
|
554
|
+
activeChart.redraw(false, true);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
animate(now) {
|
|
558
|
+
if (now - this.lastRenderTime < this.minRenderDelay) return;
|
|
559
|
+
this._renderChart();
|
|
560
|
+
this.lastRenderTime = now;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
dispose() {
|
|
564
|
+
for (const chart of this.charts.values()) {
|
|
565
|
+
chart.destroy();
|
|
566
|
+
}
|
|
567
|
+
for (const observer of this.resizeObservers.values()) {
|
|
568
|
+
observer.disconnect();
|
|
569
|
+
}
|
|
570
|
+
this.charts.clear();
|
|
571
|
+
this.resizeObservers.clear();
|
|
572
|
+
this.scalarSeries.clear();
|
|
573
|
+
this.scalarBounds.clear();
|
|
574
|
+
}
|
|
575
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Decodes the binary state/model encodings the server uses to avoid shipping
|
|
2
|
+
// huge floating-point arrays as JSON text (see server.py / state.py):
|
|
3
|
+
// standalone `/blob/...` float32 buffers, and inline `__b64__`-prefixed
|
|
4
|
+
// base64 float32 strings embedded in per-body state fields.
|
|
5
|
+
|
|
6
|
+
// Detected once: true if the platform is little-endian, which lets blob decoding
|
|
7
|
+
// skip the per-element DataView conversion (blobs are always little-endian
|
|
8
|
+
// float32 by contract, matching Python's "<f4").
|
|
9
|
+
export const IS_LITTLE_ENDIAN = (() => {
|
|
10
|
+
const buf = new ArrayBuffer(2);
|
|
11
|
+
new Uint16Array(buf)[0] = 0x0102;
|
|
12
|
+
return new Uint8Array(buf)[0] === 0x02;
|
|
13
|
+
})();
|
|
14
|
+
|
|
15
|
+
// Blobs are little-endian float32 by contract (Python "<f4"). On
|
|
16
|
+
// little-endian platforms (the overwhelming majority) the buffer can be
|
|
17
|
+
// reinterpreted directly with no per-element conversion; big-endian
|
|
18
|
+
// platforms fall back to a DataView-based byte swap.
|
|
19
|
+
export function decodeFloat32Blob(arrayBuffer) {
|
|
20
|
+
if (IS_LITTLE_ENDIAN) {
|
|
21
|
+
return new Float32Array(arrayBuffer);
|
|
22
|
+
}
|
|
23
|
+
const dataView = new DataView(arrayBuffer);
|
|
24
|
+
const floatArray = new Float32Array(arrayBuffer.byteLength / 4);
|
|
25
|
+
for (let i = 0; i < floatArray.length; i++) {
|
|
26
|
+
floatArray[i] = dataView.getFloat32(i * 4, true); // true = little-endian
|
|
27
|
+
}
|
|
28
|
+
return floatArray;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Per-body state fields that add_trajectory(binary=True) packs as float32
|
|
32
|
+
// `__b64__` blobs, with the trailing width used to reshape into per-batch rows.
|
|
33
|
+
export const STATE_FIELD_WIDTHS = {
|
|
34
|
+
bodyTransform: 7,
|
|
35
|
+
velocity: 3,
|
|
36
|
+
angularVelocity: 3,
|
|
37
|
+
force: 3,
|
|
38
|
+
torque: 3,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Decode a base64 float32 state field (little-endian, matching Python's "<f4")
|
|
42
|
+
// into an array of per-batch rows, e.g. [[x,y,z,w,qx,qy,qz], ...].
|
|
43
|
+
export function decodeStateField(str, width) {
|
|
44
|
+
const bin = atob(str.slice(7)); // strip "__b64__"
|
|
45
|
+
const bytes = new Uint8Array(bin.length);
|
|
46
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
47
|
+
const floats = new Float32Array(bytes.buffer);
|
|
48
|
+
const rows = new Array(floats.length / width);
|
|
49
|
+
for (let r = 0; r < rows.length; r++) {
|
|
50
|
+
const row = new Array(width);
|
|
51
|
+
const base = r * width;
|
|
52
|
+
for (let c = 0; c < width; c++) row[c] = floats[base + c];
|
|
53
|
+
rows[r] = row;
|
|
54
|
+
}
|
|
55
|
+
return rows;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Expand any binary-encoded per-body fields in a states chunk in place, so all
|
|
59
|
+
// downstream consumers see the same nested-array shape as legacy JSON states.
|
|
60
|
+
export function decodeStatesChunk(chunk) {
|
|
61
|
+
const widths = STATE_FIELD_WIDTHS;
|
|
62
|
+
for (const state of chunk) {
|
|
63
|
+
if (!state.bodies) continue;
|
|
64
|
+
for (const bodyState of state.bodies) {
|
|
65
|
+
for (const field in widths) {
|
|
66
|
+
const v = bodyState[field];
|
|
67
|
+
if (typeof v === "string" && v.startsWith("__b64__")) {
|
|
68
|
+
bodyState[field] = decodeStateField(v, widths[field]);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|