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,586 @@
1
+ import { GUI } from "three/addons/libs/lil-gui.module.min.js";
2
+ import { SELECTION_CONFIG } from "../config.js";
3
+ import { colorMapOptions } from "../../lib/js-colormaps.js";
4
+ import { serializeViewState, toggleMapFromUiState } from "../utils/viewState.js";
5
+
6
+ export class UIControls {
7
+ constructor(app) {
8
+ this.app = app;
9
+ this.attributeAvailability = this.determineAttributeAvailability();
10
+ this.visualizationModes = this.determineAvailableVisualizationModes();
11
+ this.gui = this.createDatGUI();
12
+ this.keyboardControlsListener = null;
13
+ this.setupKeyboardControls(app);
14
+ }
15
+
16
+ // Determine which attributes are available across all bodies
17
+ determineAttributeAvailability() {
18
+ const attributeTypes = [
19
+ "contacts",
20
+ "velocity",
21
+ "angularVelocity",
22
+ "force",
23
+ "torque",
24
+ ];
25
+ const availability = {};
26
+
27
+ // Initialize all attributes as unavailable
28
+ attributeTypes.forEach((type) => {
29
+ availability[type] = false;
30
+ });
31
+
32
+ // Check each body for available attributes
33
+ this.app.bodies.forEach((body) => {
34
+ const availableAttributes = body.getAvailableAttributes();
35
+ attributeTypes.forEach((type) => {
36
+ if (availableAttributes.has(type)) {
37
+ availability[type] = true;
38
+ }
39
+ });
40
+ });
41
+
42
+ return availability;
43
+ }
44
+
45
+ // Union of visualization modes actually available across all bodies, plus a
46
+ // "none" option to hide bodies entirely (e.g. for terrain-only viewing).
47
+ determineAvailableVisualizationModes() {
48
+ const modes = new Set();
49
+ this.app.bodies.forEach((body) => {
50
+ body.getAvailableVisualizationModes().forEach((m) => modes.add(m));
51
+ });
52
+ modes.add("none");
53
+ return [...modes];
54
+ }
55
+
56
+ changeTargetBatch(key) {
57
+ const { scene, batchManager } = this.app;
58
+ const currentBatchTarget = batchManager.currentlyActiveBatch;
59
+ const { row, col } =
60
+ batchManager.getRowColFromBatchIndex(currentBatchTarget);
61
+ const azimuth = scene.controls.getAzimuthalAngle();
62
+ const cosAz = Math.cos(azimuth);
63
+ const sinAz = Math.sin(azimuth);
64
+ let dx, dy;
65
+ if (Math.abs(cosAz) > Math.abs(sinAz)) {
66
+ dx = cosAz > 0 ? 1 : -1;
67
+ dy = 0;
68
+ } else {
69
+ dx = 0;
70
+ dy = sinAz > 0 ? 1 : -1;
71
+ }
72
+ let newRow = row;
73
+ let newCol = col;
74
+ switch (key) {
75
+ case "arrowright":
76
+ newRow += dy;
77
+ newCol += dx;
78
+ break;
79
+ case "arrowleft":
80
+ newRow -= dy;
81
+ newCol -= dx;
82
+ break;
83
+ case "arrowdown":
84
+ newRow -= dx;
85
+ newCol += dy;
86
+ break;
87
+ case "arrowup":
88
+ newRow += dx;
89
+ newCol -= dy;
90
+ break;
91
+ }
92
+ batchManager.setActiveBatchByRowCol(newRow, newCol);
93
+ }
94
+
95
+ createDatGUI() {
96
+ this.gui = new GUI();
97
+
98
+ // Only show body-related controls when there's actually a body to control
99
+ if (this.app.bodies && this.app.bodies.size > 0) {
100
+ let defaultVisualizationMode = this.app.uiState.bodyVisualizationMode;
101
+ if (!this.visualizationModes.includes(defaultVisualizationMode)) {
102
+ defaultVisualizationMode =
103
+ this.visualizationModes.find((m) => m !== "none") || "none";
104
+ this.app.uiState.bodyVisualizationMode = defaultVisualizationMode;
105
+ }
106
+
107
+ const controls = {
108
+ bodyVisualizationMode: defaultVisualizationMode,
109
+ showAxes: this.app.uiState.axesVisible,
110
+ showTrails: this.app.uiState.trailsVisible,
111
+ smoothInterpolation: this.app.uiState.smoothInterpolation,
112
+ showContacts: this.app.uiState.attributeVisible.contacts,
113
+ showVelocity: this.app.uiState.attributeVisible.velocity,
114
+ showAngularVelocity: this.app.uiState.attributeVisible.angularVelocity,
115
+ showForce: this.app.uiState.attributeVisible.force,
116
+ showTorque: this.app.uiState.attributeVisible.torque,
117
+ };
118
+
119
+ this.bodyFolder = this.gui.addFolder("Body Options");
120
+
121
+ this.bodyFolder
122
+ .add(controls, "bodyVisualizationMode", this.visualizationModes)
123
+ .name("Body Visualization Mode (B)")
124
+ .onChange((value) => {
125
+ this.updateVisualizationMode(value);
126
+ });
127
+
128
+ this.bodyFolder
129
+ .add(controls, "showAxes")
130
+ .name("Show Axes (A)")
131
+ .onChange((value) => {
132
+ this.updateAxesVisibility(value);
133
+ });
134
+
135
+ this.bodyFolder
136
+ .add(controls, "showTrails")
137
+ .name("Show Trails (G)")
138
+ .onChange((value) => {
139
+ this.updateTrailsVisibility(value);
140
+ });
141
+
142
+ this.bodyFolder
143
+ .add(controls, "smoothInterpolation")
144
+ .name("Smooth Interpolation (I)")
145
+ .onChange((value) => {
146
+ this.updateSmoothInterpolation(value);
147
+ });
148
+
149
+ // Only create toggles for attributes actually present in the loaded data
150
+ const attributeControls = [
151
+ { property: "showContacts", name: "Show Contacts (C)", type: "contacts" },
152
+ {
153
+ property: "showVelocity",
154
+ name: "Show Linear Velocity (V)",
155
+ type: "velocity",
156
+ },
157
+ {
158
+ property: "showAngularVelocity",
159
+ name: "Show Angular Velocity (W)",
160
+ type: "angularVelocity",
161
+ },
162
+ {
163
+ property: "showForce",
164
+ name: "Show Linear Force (F)",
165
+ type: "force",
166
+ },
167
+ { property: "showTorque", name: "Show Torque (T)", type: "torque" },
168
+ ];
169
+
170
+ attributeControls
171
+ .filter((control) => this.attributeAvailability[control.type])
172
+ .forEach((control) => {
173
+ this.bodyFolder
174
+ .add(controls, control.property)
175
+ .name(control.name)
176
+ .onChange((value) => {
177
+ this.updateAttributeVisibility(control.type, value);
178
+ });
179
+ });
180
+
181
+ this.bodyFolder.open();
182
+ }
183
+
184
+ // Terrain controls (unchanged)
185
+ this.terrainFolder = this.gui.addFolder("Terrain Options");
186
+
187
+ const availableColorModes = this.app.terrain
188
+ ? this.app.terrain.getAvailableColorModes()
189
+ : ["height"];
190
+
191
+ let currentColorMode = this.app.uiState?.terrainColorMode || "height";
192
+ if (!availableColorModes.includes(currentColorMode)) {
193
+ currentColorMode = availableColorModes[0] || "height";
194
+ this.app.uiState.terrainColorMode = currentColorMode;
195
+ }
196
+
197
+ const terrainControls = {
198
+ showSurface: this.app.uiState.terrainVisualizationModes?.surface ?? true,
199
+ showWireframe:
200
+ this.app.uiState.terrainVisualizationModes?.wireframe ?? false,
201
+ showNormals: this.app.uiState.terrainVisualizationModes?.normals ?? false,
202
+ colorMap: this.app.uiState?.terrainColorMap || "viridis",
203
+ colorMode: currentColorMode,
204
+ terrainProbe: this.app.uiState?.terrainProbe ?? false,
205
+ };
206
+
207
+ this.terrainFolder
208
+ .add(terrainControls, "showSurface")
209
+ .name("Show Surface")
210
+ .onChange((value) => {
211
+ this.updateTerrainVisualization("surface", value);
212
+ });
213
+
214
+ this.terrainFolder
215
+ .add(terrainControls, "showWireframe")
216
+ .name("Show Wireframe")
217
+ .onChange((value) => {
218
+ this.updateTerrainVisualization("wireframe", value);
219
+ });
220
+
221
+ this.terrainFolder
222
+ .add(terrainControls, "showNormals")
223
+ .name("Show Normals")
224
+ .onChange((value) => {
225
+ this.updateTerrainVisualization("normals", value);
226
+ });
227
+
228
+ this.terrainFolder
229
+ .add(terrainControls, "colorMap", colorMapOptions)
230
+ .name("Color Map")
231
+ .onChange((value) => {
232
+ this.updateTerrainColorMap(value);
233
+ });
234
+
235
+ this.terrainFolder
236
+ .add(terrainControls, "colorMode", availableColorModes)
237
+ .name("Color Mode")
238
+ .onChange((value) => {
239
+ this.updateTerrainColorMode(value);
240
+ });
241
+
242
+ const terrainProbeCtrl = this.terrainFolder
243
+ .add(terrainControls, "terrainProbe")
244
+ .name("Data Probe (P)")
245
+ .onChange((value) => {
246
+ this.app.uiState.terrainProbe = value;
247
+ if (!value && this.app.interactionController) {
248
+ this.app.interactionController.hideTerrainTooltip();
249
+ }
250
+ });
251
+
252
+ this.handleKeydown = (e) => {
253
+ if (e.key.toLowerCase() === "p" &&
254
+ !e.ctrlKey && !e.metaKey && !e.altKey &&
255
+ document.activeElement.tagName !== "INPUT") {
256
+ terrainProbeCtrl.setValue(!terrainProbeCtrl.getValue());
257
+ }
258
+ };
259
+ window.addEventListener("keydown", this.handleKeydown);
260
+
261
+ this.terrainFolder.open();
262
+
263
+ const cameraFolder = this.gui.addFolder("Camera Options");
264
+ const cameraControls = {
265
+ fov: this.app.scene.camera.fov,
266
+ trackBody: "None",
267
+ splitScreen: false,
268
+ splitBatchA: 0,
269
+ splitBatchB: 1
270
+ };
271
+ cameraFolder
272
+ .add(cameraControls, "fov", 20, 120)
273
+ .name("Field of View")
274
+ .onChange((value) => {
275
+ this.app.scene.camera.fov = value;
276
+ this.app.scene.camera.updateProjectionMatrix();
277
+ });
278
+ if (this.app.bodies && this.app.bodies.size > 0) {
279
+ const bodyNames = ["None", ...Array.from(this.app.bodies.keys())];
280
+ cameraFolder
281
+ .add(cameraControls, "trackBody", bodyNames)
282
+ .name("Track Body")
283
+ .onChange((value) => {
284
+ this.app.uiState.trackBody = value;
285
+ });
286
+ }
287
+
288
+ if (this.app.batchManager && this.app.batchManager.simBatches >= 2) {
289
+ const batches = Array.from({length: this.app.batchManager.simBatches}, (_, i) => i);
290
+ const splitScreenCtrl = cameraFolder.add(cameraControls, "splitScreen").name("Split Screen");
291
+ const splitBatchACtrl = cameraFolder.add(cameraControls, "splitBatchA", batches).name("Split Batch A").onChange(v => this.app.uiState.splitBatchA = parseInt(v));
292
+ const splitBatchBCtrl = cameraFolder.add(cameraControls, "splitBatchB", batches).name("Split Batch B").onChange(v => this.app.uiState.splitBatchB = parseInt(v));
293
+
294
+ // Hide the batch selectors initially if splitScreen is off
295
+ splitBatchACtrl.hide();
296
+ splitBatchBCtrl.hide();
297
+
298
+ splitScreenCtrl.onChange(v => {
299
+ this.app.uiState.splitScreen = v;
300
+ if (v) {
301
+ splitBatchACtrl.show();
302
+ splitBatchBCtrl.show();
303
+ } else {
304
+ splitBatchACtrl.hide();
305
+ splitBatchBCtrl.hide();
306
+ }
307
+ });
308
+
309
+ this.app.uiState.splitScreen = cameraControls.splitScreen;
310
+ this.app.uiState.splitBatchA = cameraControls.splitBatchA;
311
+ this.app.uiState.splitBatchB = cameraControls.splitBatchB;
312
+ }
313
+
314
+ cameraControls.copyViewLink = () => this.copyViewLink(copyViewLinkCtrl);
315
+ const copyViewLinkCtrl = cameraFolder
316
+ .add(cameraControls, "copyViewLink")
317
+ .name("Copy view link");
318
+
319
+ this.cameraControls = cameraControls;
320
+
321
+ return this.gui;
322
+ }
323
+
324
+ // Builds the current view-state hash (see utils/viewState.js), writes it
325
+ // to location.hash (via replaceState, so it doesn't spam browser history),
326
+ // copies the full shareable URL to the clipboard, and gives transient
327
+ // feedback on the button itself (label flips to "Copied!" for a beat).
328
+ copyViewLink(controller) {
329
+ const { camera, controls } = this.app.scene;
330
+ const toggles = toggleMapFromUiState(this.app.uiState);
331
+ const hash = serializeViewState({
332
+ time: this.app.animationController ? this.app.animationController.getCurrentTime() : undefined,
333
+ camera: {
334
+ position: camera.position,
335
+ target: controls.target,
336
+ fov: camera.fov,
337
+ },
338
+ batchIndex: this.app.batchManager ? this.app.batchManager.currentlyActiveBatch : undefined,
339
+ bodyVisualizationMode: this.app.uiState.bodyVisualizationMode,
340
+ terrainColorMode: this.app.uiState.terrainColorMode,
341
+ toggles,
342
+ });
343
+
344
+ history.replaceState(null, "", hash);
345
+ const url = location.href;
346
+
347
+ const showFeedback = (text) => {
348
+ if (!controller) return;
349
+ const originalName = "Copy view link";
350
+ controller.name(text);
351
+ setTimeout(() => controller.name(originalName), 1500);
352
+ };
353
+
354
+ if (navigator.clipboard && navigator.clipboard.writeText) {
355
+ navigator.clipboard.writeText(url).then(
356
+ () => showFeedback("Copied!"),
357
+ () => this.#fallbackCopyToClipboard(url, showFeedback)
358
+ );
359
+ } else {
360
+ this.#fallbackCopyToClipboard(url, showFeedback);
361
+ }
362
+ }
363
+
364
+ // execCommand("copy") fallback for browsers/contexts without the async
365
+ // Clipboard API (e.g. insecure contexts) -- a throwaway offscreen
366
+ // textarea is the standard workaround.
367
+ #fallbackCopyToClipboard(text, showFeedback) {
368
+ try {
369
+ const textarea = document.createElement("textarea");
370
+ textarea.value = text;
371
+ textarea.style.position = "fixed";
372
+ textarea.style.opacity = "0";
373
+ document.body.appendChild(textarea);
374
+ textarea.focus();
375
+ textarea.select();
376
+ const ok = document.execCommand("copy");
377
+ document.body.removeChild(textarea);
378
+ showFeedback(ok ? "Copied!" : "Copy failed");
379
+ } catch (e) {
380
+ console.warn("Failed to copy view link to clipboard:", e);
381
+ showFeedback("Copy failed");
382
+ }
383
+ }
384
+
385
+ // Applies a decoded view-state's toggles/bodyVisualizationMode/
386
+ // terrainColorMode (see utils/viewState.js + SimView.js's apply-on-load
387
+ // hook) through this panel's own lil-gui controllers -- setValue() runs
388
+ // each control's existing onChange handler (so e.g. body meshes actually
389
+ // toggle their axes/trails/etc, exactly like a user clicking the
390
+ // checkbox would) and refreshes the widget's on-screen display, so
391
+ // lil-gui never drifts out of sync with uiState. Missing/unknown
392
+ // controllers (e.g. an attribute not present in this scene) are
393
+ // silently skipped -- this must tolerate a partial state.
394
+ applyViewState(state) {
395
+ if (!state || typeof state !== "object") return;
396
+
397
+ if (typeof state.bodyVisualizationMode === "string") {
398
+ const ctrl = this.findController("bodyVisualizationMode");
399
+ if (ctrl && this.visualizationModes.includes(state.bodyVisualizationMode)) {
400
+ ctrl.setValue(state.bodyVisualizationMode);
401
+ }
402
+ }
403
+
404
+ if (typeof state.terrainColorMode === "string") {
405
+ const ctrl = this.findController("colorMode");
406
+ if (ctrl) ctrl.setValue(state.terrainColorMode);
407
+ }
408
+
409
+ if (state.toggles && typeof state.toggles === "object") {
410
+ const propertyForKey = {
411
+ axesVisible: "showAxes",
412
+ trailsVisible: "showTrails",
413
+ smoothInterpolation: "smoothInterpolation",
414
+ terrainProbe: "terrainProbe",
415
+ "attributeVisible.contacts": "showContacts",
416
+ "attributeVisible.velocity": "showVelocity",
417
+ "attributeVisible.angularVelocity": "showAngularVelocity",
418
+ "attributeVisible.force": "showForce",
419
+ "attributeVisible.torque": "showTorque",
420
+ "terrainVisualizationModes.surface": "showSurface",
421
+ "terrainVisualizationModes.wireframe": "showWireframe",
422
+ "terrainVisualizationModes.normals": "showNormals",
423
+ };
424
+ Object.entries(propertyForKey).forEach(([stateKey, property]) => {
425
+ if (!(stateKey in state.toggles)) return;
426
+ const ctrl = this.findController(property);
427
+ if (ctrl) ctrl.setValue(!!state.toggles[stateKey]);
428
+ });
429
+ }
430
+ }
431
+
432
+ setupKeyboardControls(app) {
433
+ this.keyboardControlsListener = window.addEventListener(
434
+ "keydown",
435
+ (event) => {
436
+ switch (event.key.toLowerCase()) {
437
+ case "b":
438
+ const modes = this.visualizationModes;
439
+ const currentIndex = modes.indexOf(
440
+ this.app.uiState.bodyVisualizationMode
441
+ );
442
+ const nextIndex = (currentIndex + 1) % modes.length;
443
+ this.updateVisualizationMode(modes[nextIndex]);
444
+ const controller = this.findController("bodyVisualizationMode");
445
+ if (controller) controller.setValue(modes[nextIndex]);
446
+ break;
447
+ case "a":
448
+ this.toggleControl("showAxes");
449
+ break;
450
+ case "g":
451
+ this.toggleControl("showTrails");
452
+ break;
453
+ case "i":
454
+ this.toggleControl("smoothInterpolation");
455
+ break;
456
+ case "c":
457
+ if (this.attributeAvailability.contacts)
458
+ this.toggleControl("showContacts");
459
+ break;
460
+ case "v":
461
+ if (this.attributeAvailability.velocity)
462
+ this.toggleControl("showVelocity");
463
+ break;
464
+ case "w":
465
+ if (this.attributeAvailability.angularVelocity)
466
+ this.toggleControl("showAngularVelocity");
467
+ break;
468
+ case "f":
469
+ if (this.attributeAvailability.force)
470
+ this.toggleControl("showForce");
471
+ break;
472
+ case "t":
473
+ if (this.attributeAvailability.torque)
474
+ this.toggleControl("showTorque");
475
+ break;
476
+ case "arrowup":
477
+ case "arrowdown":
478
+ case "arrowleft":
479
+ case "arrowright":
480
+ if (event[SELECTION_CONFIG.BATCH.key]) {
481
+ this.changeTargetBatch(event.key.toLowerCase());
482
+ event.stopPropagation();
483
+ }
484
+ break;
485
+ }
486
+ }
487
+ );
488
+ }
489
+
490
+ // Searches every folder (Body/Terrain/Camera Options), not just
491
+ // bodyFolder, so callers (keyboard shortcuts, and applyViewState below)
492
+ // can find and setValue() any control -- setValue() both updates the
493
+ // underlying uiState (via each controller's onChange) and refreshes the
494
+ // widget's on-screen display, so this is the single path that keeps
495
+ // lil-gui and uiState in sync.
496
+ findController(property) {
497
+ if (!this.gui) return null;
498
+ for (const controller of this.gui.controllersRecursive()) {
499
+ if (controller.property === property) {
500
+ return controller;
501
+ }
502
+ }
503
+ return null;
504
+ }
505
+
506
+ toggleControl(property) {
507
+ const controller = this.findController(property);
508
+ if (controller) {
509
+ controller.setValue(!controller.getValue());
510
+ }
511
+ }
512
+
513
+ updateVisualizationMode(mode) {
514
+ this.app.bodies.forEach((body) => {
515
+ body.updateVisualizationMode(mode);
516
+ });
517
+ this.app.uiState.bodyVisualizationMode = mode;
518
+ }
519
+
520
+ updateAxesVisibility(show) {
521
+ this.app.bodies.forEach((body) => {
522
+ body.toggleAxes(show);
523
+ });
524
+ this.app.uiState.axesVisible = show;
525
+ }
526
+
527
+ updateTrailsVisibility(show) {
528
+ this.app.bodies.forEach((body) => {
529
+ body.toggleTrails(show);
530
+ });
531
+ this.app.uiState.trailsVisible = show;
532
+ }
533
+
534
+ updateSmoothInterpolation(enabled) {
535
+ this.app.uiState.smoothInterpolation = enabled;
536
+ }
537
+
538
+ updateAttributeVisibility(attrType, show) {
539
+ this.app.bodies.forEach((body) => {
540
+ if (attrType === "contacts") {
541
+ body.toggleContactPoints(show);
542
+ } else {
543
+ body.toggleBodyVector(attrType, show);
544
+ }
545
+ });
546
+ this.app.uiState.attributeVisible[attrType] = show;
547
+ }
548
+
549
+ updateTerrainVisualization(type, visible) {
550
+ if (this.app.terrain) {
551
+ this.app.terrain.toggleVisualization(type, visible);
552
+ if (!this.app.uiState.terrainVisualizationModes) {
553
+ this.app.uiState.terrainVisualizationModes = {};
554
+ }
555
+ this.app.uiState.terrainVisualizationModes[type] = visible;
556
+ }
557
+ }
558
+
559
+ updateTerrainColorMap(colorMap) {
560
+ if (this.app.terrain) {
561
+ this.app.uiState.terrainColorMap = colorMap;
562
+ this.app.terrain.setColorMap(colorMap);
563
+ if (this.app.legend) this.app.legend.update();
564
+ }
565
+ }
566
+
567
+ updateTerrainColorMode(mode) {
568
+ if (this.app.terrain) {
569
+ this.app.uiState.terrainColorMode = mode;
570
+ this.app.terrain.setColorMode(mode);
571
+ if (this.app.legend) this.app.legend.update();
572
+ }
573
+ }
574
+
575
+ dispose() {
576
+ if (this.handleKeydown) {
577
+ window.removeEventListener("keydown", this.handleKeydown);
578
+ }
579
+ this.gui.destroy();
580
+ this.gui = null;
581
+ if (this.keyboardControlsListener) {
582
+ window.removeEventListener("keydown", this.keyboardControlsListener);
583
+ this.keyboardControlsListener = null;
584
+ }
585
+ }
586
+ }