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,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
|
+
}
|