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.
Files changed (163) hide show
  1. simview/__init__.py +69 -0
  2. simview/__main__.py +174 -0
  3. simview/launcher.py +109 -0
  4. simview/live.py +204 -0
  5. simview/merge.py +593 -0
  6. simview/model.py +690 -0
  7. simview/py.typed +0 -0
  8. simview/scene.py +603 -0
  9. simview/server.py +739 -0
  10. simview/state.py +190 -0
  11. simview/static/css/controls.css +36 -0
  12. simview/static/js/SimView.js +601 -0
  13. simview/static/js/components/AnimationController.js +654 -0
  14. simview/static/js/components/BatchManager.js +198 -0
  15. simview/static/js/components/InteractionController.js +446 -0
  16. simview/static/js/components/InteractionControls.js +116 -0
  17. simview/static/js/components/Scene.js +166 -0
  18. simview/static/js/components/StateStore.js +188 -0
  19. simview/static/js/config.js +233 -0
  20. simview/static/js/main.js +3 -0
  21. simview/static/js/objects/Body.js +616 -0
  22. simview/static/js/objects/StaticObject.js +181 -0
  23. simview/static/js/objects/Terrain.js +514 -0
  24. simview/static/js/objects/utils.js +378 -0
  25. simview/static/js/ui/AnalysisPanel.js +233 -0
  26. simview/static/js/ui/BatchLegend.js +207 -0
  27. simview/static/js/ui/BodyStateWindow.js +521 -0
  28. simview/static/js/ui/Controls.js +586 -0
  29. simview/static/js/ui/ErrorMetrics.js +664 -0
  30. simview/static/js/ui/Legend.js +87 -0
  31. simview/static/js/ui/PlaybackControls.js +314 -0
  32. simview/static/js/ui/ScalarPlotter.js +575 -0
  33. simview/static/js/utils/blobCodec.js +73 -0
  34. simview/static/js/utils/bodyTransforms.js +151 -0
  35. simview/static/js/utils/csv.js +46 -0
  36. simview/static/js/utils/errorMath.js +69 -0
  37. simview/static/js/utils/injectStyles.js +16 -0
  38. simview/static/js/utils/interpolate.js +65 -0
  39. simview/static/js/utils/liveFollow.js +17 -0
  40. simview/static/js/utils/loadRecordingLibs.js +34 -0
  41. simview/static/js/utils/viewState.js +226 -0
  42. simview/static/lib/chroma-js-3.1.2/index.min.js +8 -0
  43. simview/static/lib/chroma-js-3.1.2/src/Color.js +49 -0
  44. simview/static/lib/chroma-js-3.1.2/src/chroma.js +10 -0
  45. simview/static/lib/chroma-js-3.1.2/src/colors/colorbrewer.js +81 -0
  46. simview/static/lib/chroma-js-3.1.2/src/colors/w3cx11.js +164 -0
  47. simview/static/lib/chroma-js-3.1.2/src/generator/average.js +88 -0
  48. simview/static/lib/chroma-js-3.1.2/src/generator/bezier.js +86 -0
  49. simview/static/lib/chroma-js-3.1.2/src/generator/blend.js +57 -0
  50. simview/static/lib/chroma-js-3.1.2/src/generator/cubehelix.js +87 -0
  51. simview/static/lib/chroma-js-3.1.2/src/generator/mix.js +19 -0
  52. simview/static/lib/chroma-js-3.1.2/src/generator/random.js +12 -0
  53. simview/static/lib/chroma-js-3.1.2/src/generator/scale.js +394 -0
  54. simview/static/lib/chroma-js-3.1.2/src/interpolator/_hsx.js +59 -0
  55. simview/static/lib/chroma-js-3.1.2/src/interpolator/hcg.js +12 -0
  56. simview/static/lib/chroma-js-3.1.2/src/interpolator/hsi.js +12 -0
  57. simview/static/lib/chroma-js-3.1.2/src/interpolator/hsl.js +12 -0
  58. simview/static/lib/chroma-js-3.1.2/src/interpolator/hsv.js +12 -0
  59. simview/static/lib/chroma-js-3.1.2/src/interpolator/index.js +1 -0
  60. simview/static/lib/chroma-js-3.1.2/src/interpolator/lab.js +19 -0
  61. simview/static/lib/chroma-js-3.1.2/src/interpolator/lch.js +13 -0
  62. simview/static/lib/chroma-js-3.1.2/src/interpolator/lrgb.js +19 -0
  63. simview/static/lib/chroma-js-3.1.2/src/interpolator/num.js +15 -0
  64. simview/static/lib/chroma-js-3.1.2/src/interpolator/oklab.js +19 -0
  65. simview/static/lib/chroma-js-3.1.2/src/interpolator/oklch.js +12 -0
  66. simview/static/lib/chroma-js-3.1.2/src/interpolator/rgb.js +18 -0
  67. simview/static/lib/chroma-js-3.1.2/src/io/cmyk/cmyk2rgb.js +16 -0
  68. simview/static/lib/chroma-js-3.1.2/src/io/cmyk/index.js +27 -0
  69. simview/static/lib/chroma-js-3.1.2/src/io/cmyk/rgb2cmyk.js +17 -0
  70. simview/static/lib/chroma-js-3.1.2/src/io/css/css2rgb.js +238 -0
  71. simview/static/lib/chroma-js-3.1.2/src/io/css/hsl2css.js +26 -0
  72. simview/static/lib/chroma-js-3.1.2/src/io/css/index.js +27 -0
  73. simview/static/lib/chroma-js-3.1.2/src/io/css/lab2css.js +24 -0
  74. simview/static/lib/chroma-js-3.1.2/src/io/css/lch2css.js +24 -0
  75. simview/static/lib/chroma-js-3.1.2/src/io/css/oklab2css.js +16 -0
  76. simview/static/lib/chroma-js-3.1.2/src/io/css/oklch2css.js +16 -0
  77. simview/static/lib/chroma-js-3.1.2/src/io/css/rgb2css.js +61 -0
  78. simview/static/lib/chroma-js-3.1.2/src/io/gl/index.js +22 -0
  79. simview/static/lib/chroma-js-3.1.2/src/io/hcg/hcg2rgb.js +55 -0
  80. simview/static/lib/chroma-js-3.1.2/src/io/hcg/index.js +27 -0
  81. simview/static/lib/chroma-js-3.1.2/src/io/hcg/rgb2hcg.js +23 -0
  82. simview/static/lib/chroma-js-3.1.2/src/io/hex/hex2rgb.js +56 -0
  83. simview/static/lib/chroma-js-3.1.2/src/io/hex/index.js +29 -0
  84. simview/static/lib/chroma-js-3.1.2/src/io/hex/rgb2hex.js +29 -0
  85. simview/static/lib/chroma-js-3.1.2/src/io/hsi/hsi2rgb.js +45 -0
  86. simview/static/lib/chroma-js-3.1.2/src/io/hsi/index.js +27 -0
  87. simview/static/lib/chroma-js-3.1.2/src/io/hsi/rgb2hsi.js +31 -0
  88. simview/static/lib/chroma-js-3.1.2/src/io/hsl/hsl2rgb.js +35 -0
  89. simview/static/lib/chroma-js-3.1.2/src/io/hsl/index.js +27 -0
  90. simview/static/lib/chroma-js-3.1.2/src/io/hsl/rgb2hsl.js +45 -0
  91. simview/static/lib/chroma-js-3.1.2/src/io/hsv/hsv2rgb.js +47 -0
  92. simview/static/lib/chroma-js-3.1.2/src/io/hsv/index.js +27 -0
  93. simview/static/lib/chroma-js-3.1.2/src/io/hsv/rgb2hsv.js +32 -0
  94. simview/static/lib/chroma-js-3.1.2/src/io/input.js +4 -0
  95. simview/static/lib/chroma-js-3.1.2/src/io/lab/index.js +28 -0
  96. simview/static/lib/chroma-js-3.1.2/src/io/lab/lab-constants.js +120 -0
  97. simview/static/lib/chroma-js-3.1.2/src/io/lab/lab2rgb.js +101 -0
  98. simview/static/lib/chroma-js-3.1.2/src/io/lab/rgb2lab.js +67 -0
  99. simview/static/lib/chroma-js-3.1.2/src/io/lch/hcl2rgb.js +9 -0
  100. simview/static/lib/chroma-js-3.1.2/src/io/lch/index.js +35 -0
  101. simview/static/lib/chroma-js-3.1.2/src/io/lch/lab2lch.js +12 -0
  102. simview/static/lib/chroma-js-3.1.2/src/io/lch/lch2lab.js +18 -0
  103. simview/static/lib/chroma-js-3.1.2/src/io/lch/lch2rgb.js +13 -0
  104. simview/static/lib/chroma-js-3.1.2/src/io/lch/rgb2lch.js +12 -0
  105. simview/static/lib/chroma-js-3.1.2/src/io/named/index.js +30 -0
  106. simview/static/lib/chroma-js-3.1.2/src/io/num/index.js +32 -0
  107. simview/static/lib/chroma-js-3.1.2/src/io/num/num2rgb.js +13 -0
  108. simview/static/lib/chroma-js-3.1.2/src/io/num/rgb2num.js +8 -0
  109. simview/static/lib/chroma-js-3.1.2/src/io/oklab/index.js +27 -0
  110. simview/static/lib/chroma-js-3.1.2/src/io/oklab/oklab2rgb.js +34 -0
  111. simview/static/lib/chroma-js-3.1.2/src/io/oklab/rgb2oklab.js +37 -0
  112. simview/static/lib/chroma-js-3.1.2/src/io/oklch/index.js +27 -0
  113. simview/static/lib/chroma-js-3.1.2/src/io/oklch/oklch2rgb.js +13 -0
  114. simview/static/lib/chroma-js-3.1.2/src/io/oklch/rgb2oklch.js +12 -0
  115. simview/static/lib/chroma-js-3.1.2/src/io/rgb/index.js +44 -0
  116. simview/static/lib/chroma-js-3.1.2/src/io/temp/index.js +22 -0
  117. simview/static/lib/chroma-js-3.1.2/src/io/temp/rgb2temperature.js +30 -0
  118. simview/static/lib/chroma-js-3.1.2/src/io/temp/temperature2rgb.js +39 -0
  119. simview/static/lib/chroma-js-3.1.2/src/ops/alpha.js +13 -0
  120. simview/static/lib/chroma-js-3.1.2/src/ops/clipped.js +5 -0
  121. simview/static/lib/chroma-js-3.1.2/src/ops/darken.js +17 -0
  122. simview/static/lib/chroma-js-3.1.2/src/ops/get.js +13 -0
  123. simview/static/lib/chroma-js-3.1.2/src/ops/luminance.js +54 -0
  124. simview/static/lib/chroma-js-3.1.2/src/ops/mix.js +10 -0
  125. simview/static/lib/chroma-js-3.1.2/src/ops/premultiply.js +12 -0
  126. simview/static/lib/chroma-js-3.1.2/src/ops/saturate.js +15 -0
  127. simview/static/lib/chroma-js-3.1.2/src/ops/set.js +43 -0
  128. simview/static/lib/chroma-js-3.1.2/src/ops/shade.js +11 -0
  129. simview/static/lib/chroma-js-3.1.2/src/utils/analyze.js +191 -0
  130. simview/static/lib/chroma-js-3.1.2/src/utils/clip_rgb.js +15 -0
  131. simview/static/lib/chroma-js-3.1.2/src/utils/contrast.js +12 -0
  132. simview/static/lib/chroma-js-3.1.2/src/utils/contrastAPCA.js +68 -0
  133. simview/static/lib/chroma-js-3.1.2/src/utils/delta-e.js +62 -0
  134. simview/static/lib/chroma-js-3.1.2/src/utils/distance.js +17 -0
  135. simview/static/lib/chroma-js-3.1.2/src/utils/index.js +27 -0
  136. simview/static/lib/chroma-js-3.1.2/src/utils/last.js +8 -0
  137. simview/static/lib/chroma-js-3.1.2/src/utils/limit.js +5 -0
  138. simview/static/lib/chroma-js-3.1.2/src/utils/multiply-matrices.js +36 -0
  139. simview/static/lib/chroma-js-3.1.2/src/utils/scales.js +15 -0
  140. simview/static/lib/chroma-js-3.1.2/src/utils/type.js +18 -0
  141. simview/static/lib/chroma-js-3.1.2/src/utils/unpack.js +17 -0
  142. simview/static/lib/chroma-js-3.1.2/src/utils/valid.js +11 -0
  143. simview/static/lib/chroma-js-3.1.2/src/version.js +2 -0
  144. simview/static/lib/download.js +132 -0
  145. simview/static/lib/js-colormaps.js +19335 -0
  146. simview/static/lib/tar.js +334 -0
  147. simview/static/lib/three-0.174.0/addons/controls/OrbitControls.js +1556 -0
  148. simview/static/lib/three-0.174.0/addons/libs/lil-gui.module.min.js +8 -0
  149. simview/static/lib/three-0.174.0/three.core.js +48830 -0
  150. simview/static/lib/three-0.174.0/three.module.js +17313 -0
  151. simview/static/lib/uPlot.esm.js +6140 -0
  152. simview/static/lib/uPlot.min.css +2 -0
  153. simview/static/textures/contacts/red-cross0.png +0 -0
  154. simview/static/textures/points/ball0.png +0 -0
  155. simview/static/textures/points/ball1.png +0 -0
  156. simview/templates/index.html +81 -0
  157. simview/utils.py +41 -0
  158. simview-3.2.dist-info/METADATA +552 -0
  159. simview-3.2.dist-info/RECORD +163 -0
  160. simview-3.2.dist-info/WHEEL +5 -0
  161. simview-3.2.dist-info/entry_points.txt +2 -0
  162. simview-3.2.dist-info/licenses/LICENSE +28 -0
  163. simview-3.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,664 @@
1
+ import uPlot from "../../lib/uPlot.esm.js";
2
+ import { FREQ_CONFIG } from "../config.js";
3
+ import { downloadCsv, rowsToCsv, sanitizeForFilename } from "../utils/csv.js";
4
+ import {
5
+ maxWithIndex,
6
+ positionAxisError,
7
+ positionError,
8
+ quaternionAngleError,
9
+ rmse,
10
+ } from "../utils/errorMath.js";
11
+ import { injectStyles } from "../utils/injectStyles.js";
12
+
13
+ // Compares two batches of the same body over the full timeline: Euclidean
14
+ // position error and quaternion angle (orientation) error. Useful for e.g.
15
+ // comparing a real-world recording batch against a simulated rerun batch.
16
+ export class ErrorMetrics {
17
+ static styleId = "error-metrics-styles";
18
+
19
+ constructor(app) {
20
+ this.app = app;
21
+ this.isExpanded = false;
22
+ this.selectedBody = app.bodies.keys().next().value || null;
23
+ this.batchA = 0;
24
+ this.batchB = Math.min(1, app.batchManager.simBatches - 1);
25
+ this.showAxes = false;
26
+ this.showStats = false;
27
+ this.posSeries = [];
28
+ this.rotSeries = [];
29
+ this.axisSeries = { x: [], y: [], z: [] };
30
+ this.minRenderDelay = 1000 / FREQ_CONFIG.errorMetrics;
31
+ this.lastRenderTime = Number.NEGATIVE_INFINITY;
32
+ this.chart = null;
33
+ this.resizeObserver = null;
34
+ this.markerTime = null;
35
+
36
+ this._injectStyles();
37
+ this._setupHTML();
38
+ this._setupEventListeners();
39
+ }
40
+
41
+ _injectStyles() {
42
+ const css = `
43
+ .error-metrics-content {
44
+ padding: 10px;
45
+ }
46
+ .error-metrics-controls {
47
+ display: flex;
48
+ align-items: center;
49
+ justify-content: space-between;
50
+ flex-wrap: wrap;
51
+ row-gap: 6px;
52
+ gap: 10px;
53
+ margin-top: 8px;
54
+ }
55
+ .error-metrics-control-group {
56
+ display: flex;
57
+ align-items: center;
58
+ gap: 5px;
59
+ }
60
+ .error-metrics-control-group label {
61
+ color: #ccc;
62
+ font-size: 0.9em;
63
+ white-space: nowrap;
64
+ }
65
+ .error-metrics-control-group select {
66
+ background-color: rgba(50, 50, 50, 0.8);
67
+ color: white;
68
+ border: 1px solid white;
69
+ padding: 0.1em 0.2em;
70
+ border-radius: 3px;
71
+ font-size: 0.9em;
72
+ max-width: 100px;
73
+ }
74
+ .error-metrics-control-group input[type="checkbox"] {
75
+ cursor: pointer;
76
+ }
77
+ .error-metrics-readout {
78
+ margin-top: 10px;
79
+ font-family: monospace;
80
+ }
81
+ .error-metrics-readout div {
82
+ display: flex;
83
+ justify-content: space-between;
84
+ padding: 2px 0;
85
+ }
86
+ .error-metrics-stats {
87
+ margin-top: 8px;
88
+ padding-top: 8px;
89
+ border-top: 1px solid rgba(255, 255, 255, 0.2);
90
+ font-family: monospace;
91
+ font-size: 0.85em;
92
+ }
93
+ .error-metrics-stats div {
94
+ display: flex;
95
+ justify-content: space-between;
96
+ padding: 1px 0;
97
+ color: #ccc;
98
+ }
99
+ .error-metrics-stats div span:last-child {
100
+ color: white;
101
+ }
102
+ .error-metrics-export {
103
+ margin-top: 8px;
104
+ display: flex;
105
+ justify-content: flex-end;
106
+ }
107
+ .error-metrics-export button {
108
+ background-color: rgba(50, 50, 50, 0.8);
109
+ color: white;
110
+ border: 1px solid white;
111
+ padding: 0.2em 0.6em;
112
+ border-radius: 3px;
113
+ font-size: 0.85em;
114
+ cursor: pointer;
115
+ }
116
+ .error-metrics-plot {
117
+ width: 100%;
118
+ height: 15vh;
119
+ margin-top: 10px;
120
+ position: relative;
121
+ background-color: rgba(0, 0, 0, 1);
122
+ cursor: pointer;
123
+ }
124
+ .error-metrics-plot .uplot,
125
+ .error-metrics-plot .u-wrap {
126
+ width: 100%;
127
+ height: 100%;
128
+ }
129
+ .error-metrics-plot .u-legend {
130
+ display: none;
131
+ }
132
+ `;
133
+ injectStyles(ErrorMetrics.styleId, css);
134
+ }
135
+
136
+ _setupHTML() {
137
+ this.content = document.createElement("div");
138
+ this.content.className = "error-metrics-content";
139
+
140
+ this.controlsContainer = document.createElement("div");
141
+ this.controlsContainer.className = "error-metrics-controls";
142
+ this.content.appendChild(this.controlsContainer);
143
+
144
+ this.bodySelect = this._addSelectGroup("Body:", [...this.app.bodies.keys()], this.selectedBody);
145
+ const batchOptions = [...Array(this.app.batchManager.simBatches).keys()];
146
+ this.batchASelect = this._addSelectGroup("Batch A:", batchOptions, this.batchA, true);
147
+ this.batchBSelect = this._addSelectGroup("Batch B:", batchOptions, this.batchB, true);
148
+ this.axesToggle = this._addCheckboxGroup("Per-axis:", this.showAxes);
149
+ this.statsToggle = this._addCheckboxGroup("Details:", this.showStats);
150
+
151
+ this.readout = document.createElement("div");
152
+ this.readout.className = "error-metrics-readout";
153
+ const posRow = this._makeReadoutRow("Position error:");
154
+ this.posReadout = posRow.row;
155
+ this.posReadoutValue = posRow.valueSpan;
156
+ const axisXRow = this._makeReadoutRow("X error:");
157
+ this.axisReadoutX = axisXRow.row;
158
+ this.axisReadoutXValue = axisXRow.valueSpan;
159
+ const axisYRow = this._makeReadoutRow("Y error:");
160
+ this.axisReadoutY = axisYRow.row;
161
+ this.axisReadoutYValue = axisYRow.valueSpan;
162
+ const axisZRow = this._makeReadoutRow("Z error:");
163
+ this.axisReadoutZ = axisZRow.row;
164
+ this.axisReadoutZValue = axisZRow.valueSpan;
165
+ const rotRow = this._makeReadoutRow("Orientation error:");
166
+ this.rotReadout = rotRow.row;
167
+ this.rotReadoutValue = rotRow.valueSpan;
168
+ this.readout.appendChild(this.posReadout);
169
+ this.readout.appendChild(this.axisReadoutX);
170
+ this.readout.appendChild(this.axisReadoutY);
171
+ this.readout.appendChild(this.axisReadoutZ);
172
+ this.readout.appendChild(this.rotReadout);
173
+ this.content.appendChild(this.readout);
174
+ this._applyAxesVisibility();
175
+
176
+ this.stats = document.createElement("div");
177
+ this.stats.className = "error-metrics-stats";
178
+ const posRmseRow = this._makeReadoutRow("Position RMSE:");
179
+ this.posRmseValue = posRmseRow.valueSpan;
180
+ const posMaxRow = this._makeReadoutRow("Max position error (t):");
181
+ this.posMaxValue = posMaxRow.valueSpan;
182
+ const driftRow = this._makeReadoutRow("Final drift:");
183
+ this.driftValue = driftRow.valueSpan;
184
+ const rotRmseRow = this._makeReadoutRow("Orientation RMSE:");
185
+ this.rotRmseValue = rotRmseRow.valueSpan;
186
+ const rotMaxRow = this._makeReadoutRow("Max angle error (t):");
187
+ this.rotMaxValue = rotMaxRow.valueSpan;
188
+ this.stats.appendChild(posRmseRow.row);
189
+ this.stats.appendChild(posMaxRow.row);
190
+ this.stats.appendChild(driftRow.row);
191
+ this.stats.appendChild(rotRmseRow.row);
192
+ this.stats.appendChild(rotMaxRow.row);
193
+ this.content.appendChild(this.stats);
194
+ this._applyStatsVisibility();
195
+
196
+ this.exportContainer = document.createElement("div");
197
+ this.exportContainer.className = "error-metrics-export";
198
+ this.exportButton = document.createElement("button");
199
+ this.exportButton.textContent = "Export CSV";
200
+ this.exportContainer.appendChild(this.exportButton);
201
+ this.content.appendChild(this.exportContainer);
202
+
203
+ this.plotDiv = document.createElement("div");
204
+ this.plotDiv.className = "error-metrics-plot";
205
+ this.content.appendChild(this.plotDiv);
206
+ }
207
+
208
+ _makeReadoutRow(labelText) {
209
+ const row = document.createElement("div");
210
+ const label = document.createElement("span");
211
+ label.textContent = labelText;
212
+ const valueSpan = document.createElement("span");
213
+ valueSpan.textContent = "-";
214
+ row.appendChild(label);
215
+ row.appendChild(valueSpan);
216
+ return { row, valueSpan };
217
+ }
218
+
219
+ _addSelectGroup(labelText, options, selected, isBatch = false) {
220
+ const group = document.createElement("div");
221
+ group.className = "error-metrics-control-group";
222
+ const label = document.createElement("label");
223
+ label.textContent = labelText;
224
+ const select = document.createElement("select");
225
+ options.forEach((opt) => {
226
+ const option = document.createElement("option");
227
+ option.value = opt;
228
+ option.textContent = isBatch
229
+ ? `${opt}: ${this.app.batchManager.getBatchName(opt)}`
230
+ : opt;
231
+ if (opt === selected) option.selected = true;
232
+ select.appendChild(option);
233
+ });
234
+ group.appendChild(label);
235
+ group.appendChild(select);
236
+ this.controlsContainer.appendChild(group);
237
+ return select;
238
+ }
239
+
240
+ _addCheckboxGroup(labelText, checked) {
241
+ const group = document.createElement("div");
242
+ group.className = "error-metrics-control-group";
243
+ const label = document.createElement("label");
244
+ label.textContent = labelText;
245
+ const checkbox = document.createElement("input");
246
+ checkbox.type = "checkbox";
247
+ checkbox.checked = checked;
248
+ group.appendChild(label);
249
+ group.appendChild(checkbox);
250
+ this.controlsContainer.appendChild(group);
251
+ return checkbox;
252
+ }
253
+
254
+ // Shows the combined-magnitude readout row or the per-axis rows,
255
+ // whichever matches the current toggle state.
256
+ _applyAxesVisibility() {
257
+ this.posReadout.style.display = this.showAxes ? "none" : "";
258
+ this.axisReadoutX.style.display = this.showAxes ? "" : "none";
259
+ this.axisReadoutY.style.display = this.showAxes ? "" : "none";
260
+ this.axisReadoutZ.style.display = this.showAxes ? "" : "none";
261
+ }
262
+
263
+ // Shows/hides the RMSE/max-error/drift summary block based on the
264
+ // "Details" toggle.
265
+ _applyStatsVisibility() {
266
+ this.stats.style.display = this.showStats ? "" : "none";
267
+ }
268
+
269
+ // Called after a batch is renamed elsewhere (e.g. the BatchLegend), so the
270
+ // Batch A/B dropdowns don't keep showing a stale name.
271
+ refreshBatchLabels() {
272
+ for (const select of [this.batchASelect, this.batchBSelect]) {
273
+ for (const option of select.options) {
274
+ const batchIndex = parseInt(option.value);
275
+ option.textContent = `${batchIndex}: ${this.app.batchManager.getBatchName(batchIndex)}`;
276
+ }
277
+ }
278
+ }
279
+
280
+ _setupEventListeners() {
281
+ this.bodySelect.addEventListener("change", (e) => {
282
+ this.selectedBody = e.target.value;
283
+ this._recompute();
284
+ });
285
+ this.batchASelect.addEventListener("change", (e) => {
286
+ this.batchA = parseInt(e.target.value);
287
+ this._recompute();
288
+ });
289
+ this.batchBSelect.addEventListener("change", (e) => {
290
+ this.batchB = parseInt(e.target.value);
291
+ this._recompute();
292
+ });
293
+ this.axesToggle.addEventListener("change", (e) => {
294
+ this.showAxes = e.target.checked;
295
+ this._applyAxesVisibility();
296
+ this._buildChart();
297
+ });
298
+ this.statsToggle.addEventListener("change", (e) => {
299
+ this.showStats = e.target.checked;
300
+ this._applyStatsVisibility();
301
+ });
302
+ this.exportButton.addEventListener("click", () => this._exportCsv());
303
+ }
304
+
305
+ // Called by AnalysisPanel when this panel becomes/stops being the visible section.
306
+ setVisible(visible) {
307
+ if (this.isExpanded === visible) return;
308
+ this.isExpanded = visible;
309
+ if (this.isExpanded) this._recompute();
310
+ }
311
+
312
+ // Called by SimView once body position/quaternion history has been (re)built.
313
+ onHistoryReady() {
314
+ if (!this.app.bodies.has(this.selectedBody)) {
315
+ this.selectedBody = this.app.bodies.keys().next().value || null;
316
+ }
317
+ if (this.isExpanded) this._recompute();
318
+ }
319
+
320
+ _computeSeries() {
321
+ const body = this.app.bodies.get(this.selectedBody);
322
+ if (!body || !body.validStates) {
323
+ this.posSeries = [];
324
+ this.rotSeries = [];
325
+ this.axisSeries = { x: [], y: [], z: [] };
326
+ return;
327
+ }
328
+ const store = this.app.animationController ? this.app.animationController.store : null;
329
+ if (!store) {
330
+ this.posSeries = [];
331
+ this.rotSeries = [];
332
+ this.axisSeries = { x: [], y: [], z: [] };
333
+ return;
334
+ }
335
+
336
+ const posA = body.positionHistory[this.batchA];
337
+ const posB = body.positionHistory[this.batchB];
338
+ const quatA = body.quaternionHistory[this.batchA];
339
+ const quatB = body.quaternionHistory[this.batchB];
340
+ if (!posA || !posB || !quatA || !quatB) {
341
+ this.posSeries = [];
342
+ this.rotSeries = [];
343
+ this.axisSeries = { x: [], y: [], z: [] };
344
+ return;
345
+ }
346
+
347
+ const n = body.validStates;
348
+ const posSeries = new Array(n);
349
+ const rotSeries = new Array(n);
350
+ const axisXSeries = new Array(n);
351
+ const axisYSeries = new Array(n);
352
+ const axisZSeries = new Array(n);
353
+ for (let s = 0; s < n; s++) {
354
+ const { dx, dy, dz } = positionAxisError(posA, posB, s);
355
+ const posErr = positionError(posA, posB, s);
356
+ const rotErrDeg = (quaternionAngleError(quatA, quatB, s) * 180) / Math.PI;
357
+
358
+ const t = s < store.length ? store.timeAt(s) : s;
359
+ posSeries[s] = { x: t, y: posErr };
360
+ rotSeries[s] = { x: t, y: rotErrDeg };
361
+ axisXSeries[s] = { x: t, y: dx };
362
+ axisYSeries[s] = { x: t, y: dy };
363
+ axisZSeries[s] = { x: t, y: dz };
364
+ }
365
+ this.posSeries = posSeries;
366
+ this.rotSeries = rotSeries;
367
+ this.axisSeries = { x: axisXSeries, y: axisYSeries, z: axisZSeries };
368
+ }
369
+
370
+ _recompute() {
371
+ this._computeSeries();
372
+ this._buildChart();
373
+ this._computeStats();
374
+ }
375
+
376
+ // Summary statistics over the full timeline for the current
377
+ // body/batch-pair selection: position RMSE, max position error (with the
378
+ // time it occurs at), final-frame drift, orientation RMSE, and max angle
379
+ // error. Displayed compactly below the live readout.
380
+ _computeStats() {
381
+ if (this.posSeries.length === 0) {
382
+ this.posRmseValue.textContent = "-";
383
+ this.posMaxValue.textContent = "-";
384
+ this.driftValue.textContent = "-";
385
+ this.rotRmseValue.textContent = "-";
386
+ this.rotMaxValue.textContent = "-";
387
+ return;
388
+ }
389
+
390
+ const posValues = this.posSeries.map((p) => p.y);
391
+ const rotValues = this.rotSeries.map((p) => p.y);
392
+
393
+ const posRmse = rmse(posValues);
394
+ const posMax = maxWithIndex(posValues);
395
+ const posMaxTime = this.posSeries[posMax.index].x;
396
+ const drift = posValues[posValues.length - 1];
397
+ const rotRmse = rmse(rotValues);
398
+ const rotMax = maxWithIndex(rotValues);
399
+
400
+ this.posRmseValue.textContent = `${posRmse.toFixed(3)} m`;
401
+ this.posMaxValue.textContent = `${posMax.value.toFixed(3)} m (t=${posMaxTime.toFixed(3)})`;
402
+ this.driftValue.textContent = `${drift.toFixed(3)} m`;
403
+ this.rotRmseValue.textContent = `${rotRmse.toFixed(2)}°`;
404
+ this.rotMaxValue.textContent = `${rotMax.value.toFixed(2)}°`;
405
+ }
406
+
407
+ // Downloads the current selection's per-frame series as CSV: time,
408
+ // combined position error, signed per-axis error, and orientation angle
409
+ // error (degrees, matching the chart/readout convention).
410
+ _exportCsv() {
411
+ if (this.posSeries.length === 0) return;
412
+ const header = ["time", "pos_error", "err_x", "err_y", "err_z", "angle_error_deg"];
413
+ const rows = this.posSeries.map((p, i) => [
414
+ p.x,
415
+ p.y,
416
+ this.axisSeries.x[i].y,
417
+ this.axisSeries.y[i].y,
418
+ this.axisSeries.z[i].y,
419
+ this.rotSeries[i].y,
420
+ ]);
421
+ const csv = rowsToCsv(header, rows);
422
+ const bodyPart = sanitizeForFilename(this.selectedBody);
423
+ const batchAPart = sanitizeForFilename(this.app.batchManager.getBatchName(this.batchA));
424
+ const batchBPart = sanitizeForFilename(this.app.batchManager.getBatchName(this.batchB));
425
+ const filename = `error_metrics_${bodyPart}_${batchAPart}_vs_${batchBPart}.csv`;
426
+ downloadCsv(filename, csv);
427
+ }
428
+
429
+ // Draws the current playback time as a vertical marker line over the
430
+ // finished plot.
431
+ _drawMarker(u) {
432
+ if (this.markerTime === null) return;
433
+ const x = u.valToPos(this.markerTime, "x", true);
434
+ if (x < u.bbox.left || x > u.bbox.left + u.bbox.width) return;
435
+ const ctx = u.ctx;
436
+ ctx.save();
437
+ ctx.strokeStyle = "#ffffff";
438
+ ctx.lineWidth = 1;
439
+ ctx.beginPath();
440
+ ctx.moveTo(x, u.bbox.top);
441
+ ctx.lineTo(x, u.bbox.top + u.bbox.height);
442
+ ctx.stroke();
443
+ ctx.restore();
444
+ }
445
+
446
+ _buildChart() {
447
+ if (this.resizeObserver) {
448
+ this.resizeObserver.disconnect();
449
+ this.resizeObserver = null;
450
+ }
451
+ if (this.chart) {
452
+ this.chart.destroy();
453
+ this.chart = null;
454
+ }
455
+ this.plotDiv.innerHTML = "";
456
+ if (this.posSeries.length === 0) {
457
+ return;
458
+ }
459
+
460
+ const xValues = this.posSeries.map((p) => p.x);
461
+ const rotValues = this.rotSeries.map((p) => p.y);
462
+
463
+ const seriesConfigs = [{}];
464
+ const dataArrays = [xValues];
465
+ if (this.showAxes) {
466
+ seriesConfigs.push(
467
+ { label: "X error", stroke: "#ff6b6b", width: 1, points: { show: false }, scale: "pos" },
468
+ { label: "Y error", stroke: "#51cf66", width: 1, points: { show: false }, scale: "pos" },
469
+ { label: "Z error", stroke: "#4c9aff", width: 1, points: { show: false }, scale: "pos" }
470
+ );
471
+ dataArrays.push(
472
+ this.axisSeries.x.map((p) => p.y),
473
+ this.axisSeries.y.map((p) => p.y),
474
+ this.axisSeries.z.map((p) => p.y)
475
+ );
476
+ } else {
477
+ seriesConfigs.push({
478
+ label: "Position error",
479
+ stroke: "#4c9aff",
480
+ width: 1,
481
+ points: { show: false },
482
+ scale: "pos",
483
+ });
484
+ dataArrays.push(this.posSeries.map((p) => p.y));
485
+ }
486
+ seriesConfigs.push({
487
+ label: "Orientation error",
488
+ stroke: "#ff9f4c",
489
+ width: 1,
490
+ points: { show: false },
491
+ scale: "rot",
492
+ });
493
+ dataArrays.push(rotValues);
494
+
495
+ const rect = this.plotDiv.getBoundingClientRect();
496
+ this.chart = new uPlot(
497
+ {
498
+ width: Math.max(rect.width, 1),
499
+ height: Math.max(rect.height, 1),
500
+ padding: [8, 8, 0, 0],
501
+ series: seriesConfigs,
502
+ scales: {
503
+ x: { time: false },
504
+ pos: this.showAxes
505
+ ? {
506
+ range: (u, min, max) => {
507
+ const m = Math.max(Math.abs(min), Math.abs(max), 1e-6);
508
+ return [-m * 1.05, m * 1.05];
509
+ },
510
+ }
511
+ : { range: (u, min, max) => [0, max * 1.01 || 1] },
512
+ rot: { range: (u, min, max) => [0, max * 1.01 || 1] },
513
+ },
514
+ axes: [
515
+ {
516
+ show: true,
517
+ stroke: "rgba(255, 255, 255, 0.3)",
518
+ grid: { show: false },
519
+ ticks: { show: false },
520
+ font: "12px Arial",
521
+ },
522
+ {
523
+ scale: "pos",
524
+ show: true,
525
+ side: 3,
526
+ label: "Position (m)",
527
+ labelFont: "12px Arial",
528
+ stroke: this.showAxes ? "rgba(255, 255, 255, 0.6)" : "#4c9aff",
529
+ grid: { stroke: "rgb(53, 53, 53)", width: 1 },
530
+ ticks: { stroke: "rgb(73, 73, 73)" },
531
+ font: "12px Arial",
532
+ },
533
+ {
534
+ scale: "rot",
535
+ show: true,
536
+ side: 1,
537
+ label: "Orientation (deg)",
538
+ labelFont: "12px Arial",
539
+ stroke: "#ff9f4c",
540
+ grid: { show: false },
541
+ ticks: { stroke: "rgb(73, 73, 73)" },
542
+ font: "12px Arial",
543
+ },
544
+ ],
545
+ legend: { show: false },
546
+ cursor: {
547
+ drag: { x: false, y: false },
548
+ points: { show: false },
549
+ },
550
+ hooks: {
551
+ draw: [(u) => this._drawMarker(u)],
552
+ setCursor: [(u) => this._updateTooltip(u)],
553
+ },
554
+ },
555
+ dataArrays,
556
+ this.plotDiv
557
+ );
558
+
559
+ this.chart.over.addEventListener("click", (e) => {
560
+ const idx = this.chart.cursor.idx;
561
+ if (idx === null || idx === undefined) return;
562
+ const xVal = this.chart.data[0][idx];
563
+ if (xVal !== undefined && xVal !== null && this.app.animationController) {
564
+ this.app.animationController.goToTime(xVal);
565
+ }
566
+ });
567
+
568
+ this._createTooltip();
569
+
570
+ this.resizeObserver = new ResizeObserver(() => {
571
+ const r = this.plotDiv.getBoundingClientRect();
572
+ if (r.width > 0 && r.height > 0 && this.chart) {
573
+ this.chart.setSize({ width: r.width, height: r.height });
574
+ }
575
+ });
576
+ this.resizeObserver.observe(this.plotDiv);
577
+ }
578
+
579
+ _createTooltip() {
580
+ const tooltip = document.createElement("div");
581
+ tooltip.style.cssText =
582
+ "position:absolute;pointer-events:none;display:none;" +
583
+ "background:rgba(20,20,20,0.9);border:1px solid rgba(255,255,255,0.3);" +
584
+ "border-radius:3px;padding:4px 6px;font-family:Arial;font-size:11px;" +
585
+ "color:white;white-space:nowrap;z-index:10;";
586
+ this.plotDiv.appendChild(tooltip);
587
+ this._tooltip = tooltip;
588
+ }
589
+
590
+ _updateTooltip(u) {
591
+ if (!this._tooltip) return;
592
+ const idx = u.cursor.idx;
593
+ if (idx === null || idx === undefined || u.cursor.left < 0) {
594
+ this._tooltip.style.display = "none";
595
+ return;
596
+ }
597
+ const time = u.data[0][idx];
598
+ let html = `Time: ${time.toFixed(3)}<br>`;
599
+ if (this.showAxes) {
600
+ const x = u.data[1][idx];
601
+ const y = u.data[2][idx];
602
+ const z = u.data[3][idx];
603
+ const rot = u.data[4][idx];
604
+ html +=
605
+ `<span style="color:#ff6b6b;">X: ${x.toFixed(3)} m</span><br>` +
606
+ `<span style="color:#51cf66;">Y: ${y.toFixed(3)} m</span><br>` +
607
+ `<span style="color:#4c9aff;">Z: ${z.toFixed(3)} m</span><br>` +
608
+ `<span style="color:#ff9f4c;">Orientation: ${rot.toFixed(2)}°</span>`;
609
+ } else {
610
+ const pos = u.data[1][idx];
611
+ const rot = u.data[2][idx];
612
+ html +=
613
+ `<span style="color:#4c9aff;">Position: ${pos.toFixed(3)} m</span><br>` +
614
+ `<span style="color:#ff9f4c;">Orientation: ${rot.toFixed(2)}°</span>`;
615
+ }
616
+ this._tooltip.innerHTML = html;
617
+ this._tooltip.style.left = `${u.cursor.left + 12}px`;
618
+ this._tooltip.style.top = `${u.cursor.top + 12}px`;
619
+ this._tooltip.style.display = "block";
620
+ }
621
+
622
+ _updateReadoutAndMarker() {
623
+ if (!this.app.animationController) return;
624
+ const idx = this.app.animationController.getCurrentStateIndex();
625
+ const pos = this.posSeries[idx];
626
+ const rot = this.rotSeries[idx];
627
+ if (this.showAxes) {
628
+ const ax = this.axisSeries.x[idx];
629
+ const ay = this.axisSeries.y[idx];
630
+ const az = this.axisSeries.z[idx];
631
+ this.axisReadoutXValue.textContent = ax ? ax.y.toFixed(3) + " m" : "-";
632
+ this.axisReadoutYValue.textContent = ay ? ay.y.toFixed(3) + " m" : "-";
633
+ this.axisReadoutZValue.textContent = az ? az.y.toFixed(3) + " m" : "-";
634
+ } else {
635
+ this.posReadoutValue.textContent = pos ? pos.y.toFixed(3) + " m" : "-";
636
+ }
637
+ this.rotReadoutValue.textContent = rot ? rot.y.toFixed(2) + "°" : "-";
638
+
639
+ if (this.chart && pos) {
640
+ if (this.markerTime !== pos.x) {
641
+ this.markerTime = pos.x;
642
+ this.chart.redraw(false, false);
643
+ }
644
+ }
645
+ }
646
+
647
+ animate(now) {
648
+ if (!this.isExpanded) return;
649
+ if (now - this.lastRenderTime < this.minRenderDelay) return;
650
+ this.lastRenderTime = now;
651
+ this._updateReadoutAndMarker();
652
+ }
653
+
654
+ dispose() {
655
+ if (this.resizeObserver) {
656
+ this.resizeObserver.disconnect();
657
+ this.resizeObserver = null;
658
+ }
659
+ if (this.chart) {
660
+ this.chart.destroy();
661
+ this.chart = null;
662
+ }
663
+ }
664
+ }