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,198 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { BATCH_PALETTE_GENERATION_CONFIG } from "../config.js";
|
|
3
|
+
import { generateDivergingPalette } from "../objects/utils.js";
|
|
4
|
+
|
|
5
|
+
export class BatchManager {
|
|
6
|
+
constructor(app, modelData) {
|
|
7
|
+
this.app = app;
|
|
8
|
+
this.simBatches = 1; // Default to single batch
|
|
9
|
+
this.currentlyActiveBatch = 0; // Default to the first batch
|
|
10
|
+
this.collapsedMode = false; // Flag for collapsed mode, TODO implement this
|
|
11
|
+
this.focusedMode = false; // Flag for focused mode, TODO implement this
|
|
12
|
+
|
|
13
|
+
// Batch offset configuration
|
|
14
|
+
this.spacing = 0.5; // Spacing between batches in meters
|
|
15
|
+
this.batchOffsets = []; // Array of {x, y, z} offsets for each batch
|
|
16
|
+
this.batchPalette = []; // Array of colors for each batch
|
|
17
|
+
this.batchNames = []; // Array of display names for each batch
|
|
18
|
+
this._initialize(modelData);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
_initialize(modelData) {
|
|
22
|
+
if (!modelData) return;
|
|
23
|
+
|
|
24
|
+
// Set batch count from model data
|
|
25
|
+
if (modelData.simBatches !== undefined) {
|
|
26
|
+
this.simBatches = Math.max(1, parseInt(modelData.simBatches));
|
|
27
|
+
console.log(
|
|
28
|
+
`Initializing with ${this.simBatches} simulation batches`
|
|
29
|
+
);
|
|
30
|
+
this.app.simBatches = this.simBatches;
|
|
31
|
+
} else if (modelData.batchSize !== undefined) {
|
|
32
|
+
// Fallback for backward compatibility if needed, though README says simBatches
|
|
33
|
+
this.simBatches = Math.max(1, parseInt(modelData.batchSize));
|
|
34
|
+
console.log(`Initializing with ${this.simBatches} simulation batches (fallback from batchSize)`);
|
|
35
|
+
this.app.simBatches = this.simBatches;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
this.sideLength = Math.ceil(Math.sqrt(this.simBatches));
|
|
39
|
+
const sideLength = this.sideLength;
|
|
40
|
+
// Use README-defined terrain dimension names: sizeX, sizeY
|
|
41
|
+
const { sizeX, sizeY } = modelData.terrain.dimensions;
|
|
42
|
+
|
|
43
|
+
// Initialize batch offsets
|
|
44
|
+
this.batchOffsets = [];
|
|
45
|
+
for (let i = 0; i < this.simBatches; i++) {
|
|
46
|
+
const rowIdx = Math.floor(i / sideLength);
|
|
47
|
+
const colIdx = i % sideLength;
|
|
48
|
+
this.batchOffsets.push({
|
|
49
|
+
x: colIdx * (sizeX + this.spacing),
|
|
50
|
+
y: rowIdx * (sizeY + this.spacing),
|
|
51
|
+
z: 0,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
console.debug("Batch offsets initialized:", this.batchOffsets);
|
|
55
|
+
|
|
56
|
+
this.batchPalette = generateDivergingPalette(
|
|
57
|
+
BATCH_PALETTE_GENERATION_CONFIG.colors,
|
|
58
|
+
this.simBatches + 1,
|
|
59
|
+
BATCH_PALETTE_GENERATION_CONFIG.correctLightness
|
|
60
|
+
);
|
|
61
|
+
console.debug("Batch palette initialized:", this.batchPalette);
|
|
62
|
+
|
|
63
|
+
const providedNames = Array.isArray(modelData.batchNames)
|
|
64
|
+
? modelData.batchNames
|
|
65
|
+
: null;
|
|
66
|
+
this.batchNames = Array.from({ length: this.simBatches }, (_, i) =>
|
|
67
|
+
providedNames && typeof providedNames[i] === "string" && providedNames[i]
|
|
68
|
+
? providedNames[i]
|
|
69
|
+
: `Batch ${i}`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
getBatchName(batchIndex) {
|
|
74
|
+
return this.batchNames[batchIndex] ?? `Batch ${batchIndex}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
setBatchName(batchIndex, name) {
|
|
78
|
+
if (batchIndex < 0 || batchIndex >= this.simBatches) return;
|
|
79
|
+
const trimmed = (name || "").trim() || `Batch ${batchIndex}`;
|
|
80
|
+
this.batchNames[batchIndex] = trimmed;
|
|
81
|
+
this._persistBatchNames();
|
|
82
|
+
if (this.app.batchLegend) this.app.batchLegend.refresh();
|
|
83
|
+
if (this.app.errorMetrics) this.app.errorMetrics.refreshBatchLabels();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async _persistBatchNames() {
|
|
87
|
+
try {
|
|
88
|
+
const response = await fetch("/batch-names", {
|
|
89
|
+
method: "POST",
|
|
90
|
+
headers: { "Content-Type": "application/json" },
|
|
91
|
+
body: JSON.stringify({ names: this.batchNames }),
|
|
92
|
+
});
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
console.warn("Failed to persist batch names:", response.status);
|
|
95
|
+
}
|
|
96
|
+
} catch (e) {
|
|
97
|
+
console.warn("Failed to persist batch names:", e);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
getSimBatches() {
|
|
102
|
+
return this.simBatches;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
getColorForBatch(batchIndex) {
|
|
106
|
+
if (batchIndex < 0 || batchIndex >= this.simBatches) {
|
|
107
|
+
return new THREE.Color(0x000000); // Default to black for invalid index
|
|
108
|
+
}
|
|
109
|
+
return this.batchPalette[batchIndex];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
getRowColFromBatchIndex(batchIndex) {
|
|
113
|
+
if (batchIndex < 0 || batchIndex >= this.simBatches) {
|
|
114
|
+
return { row: -1, col: -1 };
|
|
115
|
+
}
|
|
116
|
+
const sideLength = this.sideLength;
|
|
117
|
+
const rowIdx = Math.floor(batchIndex / sideLength);
|
|
118
|
+
const colIdx = batchIndex % sideLength;
|
|
119
|
+
return { row: rowIdx, col: colIdx };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
getBatchIndexFromRowCol(rowIdx, colIdx) {
|
|
123
|
+
const sideLength = this.sideLength;
|
|
124
|
+
if (
|
|
125
|
+
rowIdx < 0 ||
|
|
126
|
+
colIdx < 0 ||
|
|
127
|
+
rowIdx >= sideLength ||
|
|
128
|
+
colIdx >= sideLength
|
|
129
|
+
) {
|
|
130
|
+
return -1;
|
|
131
|
+
}
|
|
132
|
+
const batchIndex = rowIdx * sideLength + colIdx;
|
|
133
|
+
if (batchIndex < 0 || batchIndex >= this.simBatches) {
|
|
134
|
+
return -1;
|
|
135
|
+
}
|
|
136
|
+
return batchIndex;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
getOffsetByRowCol(rowIdx, colIdx) {
|
|
140
|
+
const sideLength = this.sideLength;
|
|
141
|
+
if (
|
|
142
|
+
rowIdx < 0 ||
|
|
143
|
+
colIdx < 0 ||
|
|
144
|
+
rowIdx >= sideLength ||
|
|
145
|
+
colIdx >= sideLength
|
|
146
|
+
) {
|
|
147
|
+
throw new Error("Invalid row or column index");
|
|
148
|
+
}
|
|
149
|
+
const batchIndex = rowIdx * sideLength + colIdx;
|
|
150
|
+
return this.getBatchOffset(batchIndex);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Get offset for a specific batch
|
|
154
|
+
getBatchOffset(batchIndex) {
|
|
155
|
+
if (batchIndex >= 0 && batchIndex < this.batchOffsets.length) {
|
|
156
|
+
return this.batchOffsets[batchIndex];
|
|
157
|
+
}
|
|
158
|
+
return { x: 0, y: 0, z: 0 };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
changeFocusOnBatchByRowCol(row, col) {
|
|
162
|
+
const { camera, controls } = this.app.scene;
|
|
163
|
+
const { x, y, z } = this.getOffsetByRowCol(row, col);
|
|
164
|
+
const newCameraTarget = new THREE.Vector3(x, y, z);
|
|
165
|
+
const oldCameraTarget = controls.target.clone();
|
|
166
|
+
controls.target = newCameraTarget;
|
|
167
|
+
camera.position.add(newCameraTarget.clone().sub(oldCameraTarget));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
changeFocusOnBatchByIndex(batchIndex) {
|
|
171
|
+
const { row, col } = this.getRowColFromBatchIndex(batchIndex);
|
|
172
|
+
this.changeFocusOnBatchByRowCol(row, col);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
setActiveBatch(batchIndex) {
|
|
176
|
+
if (batchIndex >= 0 && batchIndex < this.simBatches) {
|
|
177
|
+
this.currentlyActiveBatch = batchIndex;
|
|
178
|
+
this.changeFocusOnBatchByIndex(batchIndex);
|
|
179
|
+
} else {
|
|
180
|
+
console.warn("Invalid batch index:", batchIndex);
|
|
181
|
+
}
|
|
182
|
+
this.app.bodyStateWindow.setSelectedBatch(batchIndex);
|
|
183
|
+
if (this.app.scalarPlotter) {
|
|
184
|
+
this.app.scalarPlotter.setFocusedBatch(batchIndex);
|
|
185
|
+
}
|
|
186
|
+
if (this.app.batchLegend) {
|
|
187
|
+
this.app.batchLegend.highlightActive();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
setActiveBatchByRowCol(row, col) {
|
|
192
|
+
const batchIndex = this.getBatchIndexFromRowCol(row, col);
|
|
193
|
+
if (batchIndex === -1) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
this.setActiveBatch(batchIndex);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { SELECTION_CONFIG } from "../config.js";
|
|
3
|
+
|
|
4
|
+
export class InteractionController {
|
|
5
|
+
constructor(app) {
|
|
6
|
+
this.app = app;
|
|
7
|
+
this.raycaster = new THREE.Raycaster();
|
|
8
|
+
this.mouse = new THREE.Vector2();
|
|
9
|
+
this.hoveredObject = null;
|
|
10
|
+
this.selectedObject = null;
|
|
11
|
+
|
|
12
|
+
this.isDragging = false;
|
|
13
|
+
this.startPoint = { x: 0, y: 0 };
|
|
14
|
+
this.selectionBox = null;
|
|
15
|
+
|
|
16
|
+
this.currentSelectionMode = null;
|
|
17
|
+
|
|
18
|
+
// Cache for getIntersectableObjects — rebuilt only when selection mode changes
|
|
19
|
+
this._intersectableCache = null;
|
|
20
|
+
this._intersectableCacheMode = null;
|
|
21
|
+
// Reusable Vector3 for screen-projection in selection box
|
|
22
|
+
this._projPoint = new THREE.Vector3();
|
|
23
|
+
|
|
24
|
+
// Create the probe sphere
|
|
25
|
+
const sphereGeo = new THREE.SphereGeometry(0.05, 16, 16);
|
|
26
|
+
const sphereMat = new THREE.MeshBasicMaterial({ color: 0xff0000, depthTest: false });
|
|
27
|
+
this.probeSphere = new THREE.Mesh(sphereGeo, sphereMat);
|
|
28
|
+
this.probeSphere.renderOrder = 999; // Draw on top
|
|
29
|
+
this.probeSphere.visible = false;
|
|
30
|
+
if (this.app.scene) {
|
|
31
|
+
this.app.scene.addObject3D(this.probeSphere);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Bind methods to preserve context
|
|
35
|
+
this.onMouseMove = this.onMouseMove.bind(this);
|
|
36
|
+
this.onClick = this.onClick.bind(this);
|
|
37
|
+
this.handleHover = this.handleHover.bind(this);
|
|
38
|
+
this.onMouseDown = this.onMouseDown.bind(this);
|
|
39
|
+
this.onMouseDrag = this.onMouseDrag.bind(this);
|
|
40
|
+
this.onMouseUp = this.onMouseUp.bind(this);
|
|
41
|
+
this.onKeyUp = this.onKeyUp.bind(this);
|
|
42
|
+
|
|
43
|
+
// handleHover does a full raycast; mousemove can fire far more often
|
|
44
|
+
// than the display refreshes, so coalesce to at most one raycast per
|
|
45
|
+
// animation frame instead of one per event.
|
|
46
|
+
this._hoverRafId = null;
|
|
47
|
+
this._queueHoverUpdate = () => {
|
|
48
|
+
if (this._hoverRafId !== null) return;
|
|
49
|
+
this._hoverRafId = requestAnimationFrame(() => {
|
|
50
|
+
this._hoverRafId = null;
|
|
51
|
+
this.handleHover();
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
this.initializeEventListeners();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
initializeEventListeners() {
|
|
59
|
+
// Use passive event listeners where possible for better performance
|
|
60
|
+
const passiveOptions = { passive: true };
|
|
61
|
+
|
|
62
|
+
window.addEventListener("mousemove", this.onMouseMove, passiveOptions);
|
|
63
|
+
window.addEventListener("click", this.onClick);
|
|
64
|
+
window.addEventListener("keyup", this.onKeyUp);
|
|
65
|
+
|
|
66
|
+
const renderer = this.app.scene && this.app.scene.renderer;
|
|
67
|
+
if (!renderer) {
|
|
68
|
+
console.warn("InteractionController: renderer not found during initialization.");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const canvas = renderer.domElement;
|
|
72
|
+
if (!canvas) return;
|
|
73
|
+
|
|
74
|
+
canvas.addEventListener("mousemove", this._queueHoverUpdate, passiveOptions);
|
|
75
|
+
canvas.addEventListener("mousedown", this.onMouseDown);
|
|
76
|
+
canvas.addEventListener("mousemove", this.onMouseDrag, passiveOptions);
|
|
77
|
+
canvas.addEventListener("mouseup", this.onMouseUp);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
cleanup() {
|
|
81
|
+
// Remove event listeners when cleaning up
|
|
82
|
+
window.removeEventListener("mousemove", this.onMouseMove);
|
|
83
|
+
window.removeEventListener("click", this.onClick);
|
|
84
|
+
window.removeEventListener("keyup", this.onKeyUp);
|
|
85
|
+
|
|
86
|
+
const renderer = this.app.scene && this.app.scene.renderer;
|
|
87
|
+
const canvas = renderer ? renderer.domElement : null;
|
|
88
|
+
if (canvas) {
|
|
89
|
+
canvas.removeEventListener("mousemove", this._queueHoverUpdate);
|
|
90
|
+
canvas.removeEventListener("mousedown", this.onMouseDown);
|
|
91
|
+
canvas.removeEventListener("mousemove", this.onMouseDrag);
|
|
92
|
+
canvas.removeEventListener("mouseup", this.onMouseUp);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (this._hoverRafId !== null) {
|
|
96
|
+
cancelAnimationFrame(this._hoverRafId);
|
|
97
|
+
this._hoverRafId = null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
this.clearSelectionBox();
|
|
101
|
+
|
|
102
|
+
if (this.probeSphere && this.app.scene) {
|
|
103
|
+
this.app.scene.removeObject3D(this.probeSphere);
|
|
104
|
+
this.probeSphere.geometry.dispose();
|
|
105
|
+
this.probeSphere.material.dispose();
|
|
106
|
+
this.probeSphere = null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
getSelectionMode(event) {
|
|
111
|
+
for (const [mode, config] of Object.entries(SELECTION_CONFIG)) {
|
|
112
|
+
if (event[`${config.key}Key`]) {
|
|
113
|
+
return config;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
onMouseMove(e) {
|
|
120
|
+
// Calculate mouse position in normalized device coordinates (-1 to +1)
|
|
121
|
+
this.mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
|
|
122
|
+
this.mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
onClick(e) {
|
|
126
|
+
if (!this.app.scene || !this.app.scene.camera) return;
|
|
127
|
+
|
|
128
|
+
// Prevent click if mouse was dragged (e.g. rotating camera)
|
|
129
|
+
if (this.lastMouseDown) {
|
|
130
|
+
const dx = e.clientX - this.lastMouseDown.x;
|
|
131
|
+
const dy = e.clientY - this.lastMouseDown.y;
|
|
132
|
+
this.lastMouseDown = null; // Consume the mousedown
|
|
133
|
+
if (dx * dx + dy * dy > 25) {
|
|
134
|
+
return; // 5px movement threshold
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
this.raycaster.setFromCamera(this.mouse, this.app.scene.camera);
|
|
139
|
+
const intersects = this.raycaster.intersectObjects(
|
|
140
|
+
Object.values(this.app.bodies).flatMap((body) =>
|
|
141
|
+
body.children.filter((child) => child.isMesh)
|
|
142
|
+
), // Filter only meshes
|
|
143
|
+
true // Enable recursive raycasting
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
if (intersects.length > 0) {
|
|
147
|
+
this.selectedObject = intersects[0].object;
|
|
148
|
+
this.hideTerrainTooltip();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Raycast against terrain
|
|
153
|
+
if (this.app.uiState && this.app.uiState.terrainProbe && this.app.terrain && this.app.terrain.group) {
|
|
154
|
+
const terrainIntersects = this.raycaster.intersectObject(this.app.terrain.group, true);
|
|
155
|
+
const surfaceIntersect = terrainIntersects.find(i => i.object.name === "surface");
|
|
156
|
+
if (surfaceIntersect) {
|
|
157
|
+
this.showTerrainTooltip(e, surfaceIntersect);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
this.hideTerrainTooltip();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
showTerrainTooltip(e, intersect) {
|
|
166
|
+
const point = intersect.point;
|
|
167
|
+
let batchIndex = 0;
|
|
168
|
+
let current = intersect.object;
|
|
169
|
+
while (current) {
|
|
170
|
+
if (current.name && current.name.startsWith("batch")) {
|
|
171
|
+
batchIndex = parseInt(current.name.replace("batch", ""));
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
current = current.parent;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const props = this.app.terrain.getPropertiesAt(point.x, point.y, batchIndex);
|
|
178
|
+
if (!props) return;
|
|
179
|
+
|
|
180
|
+
let tooltip = document.getElementById("terrain-tooltip");
|
|
181
|
+
if (!tooltip) {
|
|
182
|
+
tooltip = document.createElement("div");
|
|
183
|
+
tooltip.id = "terrain-tooltip";
|
|
184
|
+
Object.assign(tooltip.style, {
|
|
185
|
+
position: "absolute",
|
|
186
|
+
background: "rgba(0, 0, 0, 0.8)",
|
|
187
|
+
color: "white",
|
|
188
|
+
padding: "8px",
|
|
189
|
+
borderRadius: "4px",
|
|
190
|
+
pointerEvents: "none",
|
|
191
|
+
zIndex: "1000",
|
|
192
|
+
fontSize: "12px",
|
|
193
|
+
fontFamily: "monospace",
|
|
194
|
+
whiteSpace: "pre"
|
|
195
|
+
});
|
|
196
|
+
document.body.appendChild(tooltip);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
tooltip.style.left = `${e.clientX + 10}px`;
|
|
200
|
+
tooltip.style.top = `${e.clientY + 10}px`;
|
|
201
|
+
tooltip.style.display = "block";
|
|
202
|
+
|
|
203
|
+
let text = `Batch: ${batchIndex}\nX: ${point.x.toFixed(3)}, Y: ${point.y.toFixed(3)}\nHeight: ${props.height.toFixed(3)}`;
|
|
204
|
+
if (props.friction !== undefined) text += `\nFriction: ${props.friction.toFixed(3)}`;
|
|
205
|
+
if (props.stiffness !== undefined) text += `\nStiffness: ${props.stiffness.toExponential(2)}`;
|
|
206
|
+
|
|
207
|
+
tooltip.innerText = text;
|
|
208
|
+
|
|
209
|
+
if (this.probeSphere) {
|
|
210
|
+
this.probeSphere.position.copy(point);
|
|
211
|
+
this.probeSphere.visible = true;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
hideTerrainTooltip() {
|
|
216
|
+
const tooltip = document.getElementById("terrain-tooltip");
|
|
217
|
+
if (tooltip) {
|
|
218
|
+
tooltip.style.display = "none";
|
|
219
|
+
}
|
|
220
|
+
if (this.probeSphere) {
|
|
221
|
+
this.probeSphere.visible = false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
onMouseDown(e) {
|
|
226
|
+
this.lastMouseDown = { x: e.clientX, y: e.clientY };
|
|
227
|
+
|
|
228
|
+
this.currentSelectionMode = this.getSelectionMode(e);
|
|
229
|
+
if (!this.currentSelectionMode) return;
|
|
230
|
+
|
|
231
|
+
e.preventDefault(); // Prevent default only when needed
|
|
232
|
+
this.isDragging = true;
|
|
233
|
+
this.startPoint = {
|
|
234
|
+
x: e.clientX,
|
|
235
|
+
y: e.clientY,
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
this.initSelectionBox(e.clientX, e.clientY);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
onMouseDrag(e) {
|
|
242
|
+
if (!this.isDragging || !this.currentSelectionMode) {
|
|
243
|
+
if (this.selectionBox) {
|
|
244
|
+
this.clearSelectionBox();
|
|
245
|
+
}
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
this.updateSelectionBox(e.clientX, e.clientY);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
onMouseUp(e) {
|
|
253
|
+
if (!this.isDragging) return;
|
|
254
|
+
|
|
255
|
+
this.isDragging = false;
|
|
256
|
+
if (e.ctrlKey) {
|
|
257
|
+
this.selectObjectsInBox();
|
|
258
|
+
}
|
|
259
|
+
this.clearSelectionBox();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
onKeyUp(e) {
|
|
263
|
+
for (const config of Object.values(SELECTION_CONFIG)) {
|
|
264
|
+
if (e.key === config.key) {
|
|
265
|
+
this.isDragging = false;
|
|
266
|
+
this.currentSelectionMode = null;
|
|
267
|
+
this.clearSelectionBox();
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
initSelectionBox(x, y) {
|
|
273
|
+
this.clearSelectionBox(); // Clear any existing selection box
|
|
274
|
+
|
|
275
|
+
const overlay = document.createElement("div");
|
|
276
|
+
Object.assign(overlay.style, {
|
|
277
|
+
position: "fixed", // Using fixed instead of absolute
|
|
278
|
+
border: "1px dashed #00ff00",
|
|
279
|
+
backgroundColor: "rgba(0, 255, 0, 0.1)",
|
|
280
|
+
pointerEvents: "none",
|
|
281
|
+
left: `${x}px`,
|
|
282
|
+
top: `${y}px`,
|
|
283
|
+
width: "0px",
|
|
284
|
+
height: "0px",
|
|
285
|
+
zIndex: "1000",
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
document.body.appendChild(overlay);
|
|
289
|
+
this.selectionBox = overlay;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
updateSelectionBox(currentX, currentY) {
|
|
293
|
+
if (!this.selectionBox) return;
|
|
294
|
+
|
|
295
|
+
const width = currentX - this.startPoint.x;
|
|
296
|
+
const height = currentY - this.startPoint.y;
|
|
297
|
+
|
|
298
|
+
const left = width < 0 ? currentX : this.startPoint.x;
|
|
299
|
+
const top = height < 0 ? currentY : this.startPoint.y;
|
|
300
|
+
const absWidth = Math.abs(width);
|
|
301
|
+
const absHeight = Math.abs(height);
|
|
302
|
+
|
|
303
|
+
Object.assign(this.selectionBox.style, {
|
|
304
|
+
left: `${left}px`,
|
|
305
|
+
top: `${top}px`,
|
|
306
|
+
width: `${absWidth}px`,
|
|
307
|
+
height: `${absHeight}px`,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
clearSelectionBox() {
|
|
312
|
+
if (this.selectionBox) {
|
|
313
|
+
this.selectionBox.remove();
|
|
314
|
+
this.selectionBox = null;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
handleHover() {
|
|
319
|
+
if (!this.app.scene || !this.app.scene.camera) return;
|
|
320
|
+
this.raycaster.setFromCamera(this.mouse, this.app.scene.camera);
|
|
321
|
+
const intersectables = this.getIntersectableObjects();
|
|
322
|
+
const intersects = this.raycaster.intersectObjects(intersectables, true);
|
|
323
|
+
|
|
324
|
+
// Handle hover state
|
|
325
|
+
const newHovered = intersects[0]?.object ?? null;
|
|
326
|
+
if (this.hoveredObject !== newHovered) {
|
|
327
|
+
if (this.hoveredObject) {
|
|
328
|
+
this.onObjectUnhover(this.hoveredObject);
|
|
329
|
+
}
|
|
330
|
+
if (newHovered) {
|
|
331
|
+
this.onObjectHover(newHovered);
|
|
332
|
+
}
|
|
333
|
+
this.hoveredObject = newHovered;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
onObjectHover(object) {
|
|
338
|
+
object.scale.setScalar(1.2);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
onObjectUnhover(object) {
|
|
342
|
+
object.scale.setScalar(1.0);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
getIntersectableObjects() {
|
|
346
|
+
if (!this.currentSelectionMode) return [];
|
|
347
|
+
|
|
348
|
+
const modeKey = this.currentSelectionMode.objects;
|
|
349
|
+
if (this._intersectableCache && this._intersectableCacheMode === modeKey) {
|
|
350
|
+
return this._intersectableCache;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const objects = this.app[modeKey];
|
|
354
|
+
let result;
|
|
355
|
+
if (modeKey === "bodies") {
|
|
356
|
+
result = Object.values(objects)
|
|
357
|
+
.flatMap((body) => body.children)
|
|
358
|
+
.filter((child) => child.isMesh);
|
|
359
|
+
} else {
|
|
360
|
+
result = Object.values(objects);
|
|
361
|
+
}
|
|
362
|
+
this._intersectableCache = result;
|
|
363
|
+
this._intersectableCacheMode = modeKey;
|
|
364
|
+
return result;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
invalidateIntersectableCache() {
|
|
368
|
+
this._intersectableCache = null;
|
|
369
|
+
this._intersectableCacheMode = null;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
selectObjectsInBox() {
|
|
373
|
+
if (!this.currentSelectionMode || !this.selectionBox) return;
|
|
374
|
+
|
|
375
|
+
const camera = this.app.scene && this.app.scene.camera;
|
|
376
|
+
const renderer = this.app.scene && this.app.scene.renderer;
|
|
377
|
+
if (!camera || !renderer || !renderer.domElement) return;
|
|
378
|
+
|
|
379
|
+
const rect = renderer.domElement.getBoundingClientRect();
|
|
380
|
+
|
|
381
|
+
// Get the selection box coordinates
|
|
382
|
+
const boxLeft = parseInt(this.selectionBox.style.left);
|
|
383
|
+
const boxTop = parseInt(this.selectionBox.style.top);
|
|
384
|
+
const boxWidth = parseInt(this.selectionBox.style.width);
|
|
385
|
+
const boxHeight = parseInt(this.selectionBox.style.height);
|
|
386
|
+
|
|
387
|
+
// Function to check if a point is inside the selection box
|
|
388
|
+
const isPointInSelectionBox = (point) => {
|
|
389
|
+
// Convert 3D point to screen coordinates (reuse _projPoint to avoid allocation)
|
|
390
|
+
this._projPoint.copy(point).project(camera);
|
|
391
|
+
|
|
392
|
+
// Convert to pixel coordinates
|
|
393
|
+
const x = ((this._projPoint.x + 1) * rect.width) / 2 + rect.left;
|
|
394
|
+
const y = ((-this._projPoint.y + 1) * rect.height) / 2 + rect.top;
|
|
395
|
+
|
|
396
|
+
return (
|
|
397
|
+
x >= boxLeft &&
|
|
398
|
+
x <= boxLeft + boxWidth &&
|
|
399
|
+
y >= boxTop &&
|
|
400
|
+
y <= boxTop + boxHeight
|
|
401
|
+
);
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
// Clear previous selection
|
|
405
|
+
const selectionSet = this.app[this.currentSelectionMode.set];
|
|
406
|
+
selectionSet.forEach((obj) => {
|
|
407
|
+
if (obj.material) {
|
|
408
|
+
obj.material.color.setHex(obj.originalColor || 0xffffff);
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
selectionSet.clear();
|
|
412
|
+
|
|
413
|
+
// Select objects (reuse bbox/center to avoid per-object allocations)
|
|
414
|
+
const selectableObjects = this.getIntersectableObjects();
|
|
415
|
+
const bbox = new THREE.Box3();
|
|
416
|
+
const center = new THREE.Vector3();
|
|
417
|
+
selectableObjects.forEach((object) => {
|
|
418
|
+
bbox.setFromObject(object);
|
|
419
|
+
bbox.getCenter(center);
|
|
420
|
+
|
|
421
|
+
if (isPointInSelectionBox(center)) {
|
|
422
|
+
if (!object.hasOwnProperty("originalColor")) {
|
|
423
|
+
object.originalColor = object.material.color.getHex();
|
|
424
|
+
}
|
|
425
|
+
object.material.color.setHex(0xff0000);
|
|
426
|
+
selectionSet.add(object);
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
console.log(
|
|
431
|
+
`Selected ${selectionSet.size} ${this.currentSelectionMode.objects}`
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
deselectAll() {
|
|
436
|
+
for (const config of Object.values(SELECTION_CONFIG)) {
|
|
437
|
+
const selectionSet = this.app[config.set];
|
|
438
|
+
selectionSet.forEach((obj) => {
|
|
439
|
+
if (obj.material && obj.hasOwnProperty("originalColor")) {
|
|
440
|
+
obj.material.color.setHex(obj.originalColor);
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
selectionSet.clear();
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|