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,181 @@
1
+ import * as THREE from "three";
2
+ import { STATIC_OBJECT_CONFIG } from "../config.js"; // Assuming this exists
3
+ import {
4
+ createGeometry,
5
+ createMesh,
6
+ createPoints,
7
+ createWireframe,
8
+ } from "./utils.js";
9
+
10
+ export class StaticObject {
11
+ constructor(objectData, app) {
12
+ this.app = app;
13
+ this.name = objectData.name;
14
+ this.isSingleton = objectData.isSingleton || false; // Default to false if not specified
15
+ this.batchSize = app.batchManager.simBatches;
16
+ this.representations = { mesh: [], wireframe: [], points: [] }; // For visualization modes
17
+ this.batchGroups = []; // Store batch groups
18
+
19
+ // Validate input data
20
+ if (this.isSingleton) {
21
+ if (!objectData.shape || !objectData.shape.type) {
22
+ throw new Error(
23
+ "Singleton static object requires a 'shape' definition."
24
+ );
25
+ }
26
+ this.type = objectData.shape.type;
27
+ } else {
28
+ if (
29
+ !Array.isArray(objectData.shapes) ||
30
+ objectData.shapes.length !== this.batchSize
31
+ ) {
32
+ throw new Error(
33
+ `Batched static object requires a 'shapes' array of length ${this.batchSize}.`
34
+ );
35
+ }
36
+ this.type = objectData.shapes[0].type; // Assume consistent types for simplicity
37
+ }
38
+
39
+ // Initialize and create visual objects
40
+ this.initializeGroup();
41
+ this.createBatchGroups(objectData);
42
+ }
43
+
44
+ /** Initialize the main Three.js group */
45
+ initializeGroup() {
46
+ this.group = new THREE.Group();
47
+ this.group.name = this.name;
48
+ }
49
+
50
+ createBatchGroups(objectData) {
51
+ const geometryConfig = STATIC_OBJECT_CONFIG.geometry || {};
52
+
53
+ if (this.isSingleton) {
54
+ const shape = objectData.shape;
55
+ const meshConfig = {
56
+ ...STATIC_OBJECT_CONFIG.mesh,
57
+ color: shape.color || STATIC_OBJECT_CONFIG.color || 0xffffff,
58
+ };
59
+ const pointsConfig = {
60
+ ...STATIC_OBJECT_CONFIG.points,
61
+ color: shape.color || STATIC_OBJECT_CONFIG.color || 0xffffff,
62
+ };
63
+
64
+ if (shape.type === "pointcloud") {
65
+ for (let i = 0; i < this.batchSize; i++) {
66
+ const points = createPoints(shape.points, pointsConfig);
67
+ if (points) {
68
+ points.visible = this.app.uiState.bodyVisualizationMode === "points";
69
+ const offset = this.app.batchManager.getBatchOffset(i);
70
+ points.position.set(offset.x, offset.y, offset.z);
71
+ this.group.add(points);
72
+ this.representations["points"].push(points);
73
+ }
74
+ }
75
+ } else {
76
+ const geometry = createGeometry(shape, geometryConfig);
77
+ const meshMaterial = createMesh(geometry, meshConfig).material;
78
+ const wireframeMaterial = new THREE.MeshBasicMaterial({
79
+ color: STATIC_OBJECT_CONFIG.wireframe?.color || 0x4080ff,
80
+ wireframe: true,
81
+ transparent: true,
82
+ opacity: 0.2
83
+ });
84
+
85
+ const instancedMesh = new THREE.InstancedMesh(geometry, meshMaterial, this.batchSize);
86
+ const instancedWireframe = new THREE.InstancedMesh(geometry, wireframeMaterial, this.batchSize);
87
+
88
+ for (let i = 0; i < this.batchSize; i++) {
89
+ const offset = this.app.batchManager.getBatchOffset(i);
90
+ const matrix = new THREE.Matrix4().makeTranslation(offset.x, offset.y, offset.z);
91
+ instancedMesh.setMatrixAt(i, matrix);
92
+ instancedWireframe.setMatrixAt(i, matrix);
93
+ }
94
+
95
+ instancedMesh.visible = this.app.uiState.bodyVisualizationMode === "mesh";
96
+ instancedWireframe.visible = this.app.uiState.bodyVisualizationMode === "wireframe";
97
+
98
+ this.group.add(instancedMesh);
99
+ this.group.add(instancedWireframe);
100
+
101
+ this.representations["mesh"] = instancedMesh;
102
+ this.representations["wireframe"] = instancedWireframe;
103
+ }
104
+ } else {
105
+ // Non-singleton: reuse geometry if possible (if all shapes are same type and same dimensions)
106
+ // For now, keep as is but optimize material/geometry creation if they are identical
107
+ for (let i = 0; i < this.batchSize; i++) {
108
+ const batchGroup = new THREE.Group();
109
+ batchGroup.name = `${this.name}_batch_${i}`;
110
+ this.group.add(batchGroup);
111
+ this.batchGroups.push(batchGroup);
112
+
113
+ const shape = objectData.shapes[i];
114
+ const meshConfig = {
115
+ ...STATIC_OBJECT_CONFIG.mesh,
116
+ color: shape.color || STATIC_OBJECT_CONFIG.color || 0xffffff,
117
+ };
118
+ const pointsConfig = {
119
+ ...STATIC_OBJECT_CONFIG.points,
120
+ color: shape.color || STATIC_OBJECT_CONFIG.color || 0xffffff,
121
+ };
122
+
123
+ if (shape.type === "pointcloud") {
124
+ const points = createPoints(shape.points, pointsConfig);
125
+ if (points) {
126
+ points.visible = this.app.uiState.bodyVisualizationMode === "points";
127
+ batchGroup.add(points);
128
+ this.representations["points"].push(points);
129
+ }
130
+ } else {
131
+ const geometry = createGeometry(shape, geometryConfig);
132
+ const mesh = createMesh(geometry, meshConfig);
133
+ mesh.visible = this.app.uiState.bodyVisualizationMode === "mesh";
134
+ batchGroup.add(mesh);
135
+ this.representations["mesh"].push(mesh);
136
+ const wireframe = createWireframe(geometry, STATIC_OBJECT_CONFIG.wireframe);
137
+ wireframe.visible = this.app.uiState.bodyVisualizationMode === "wireframe";
138
+ batchGroup.add(wireframe);
139
+ this.representations["wireframe"].push(wireframe);
140
+ }
141
+
142
+ const offset = this.app.batchManager.getBatchOffset(i);
143
+ batchGroup.position.set(offset.x, offset.y, offset.z);
144
+ }
145
+ }
146
+ }
147
+
148
+ /** Update visualization mode (mesh, wireframe, points) */
149
+ updateVisualizationMode(mode) {
150
+ for (const [type, obj] of Object.entries(this.representations)) {
151
+ if (obj instanceof THREE.InstancedMesh) {
152
+ obj.visible = type === mode;
153
+ } else if (Array.isArray(obj)) {
154
+ obj.forEach((o) => (o.visible = type === mode));
155
+ }
156
+ }
157
+ }
158
+
159
+ /** Return the Three.js group for scene integration */
160
+ getObject3D() {
161
+ return this.group;
162
+ }
163
+
164
+ /** Clean up resources */
165
+ dispose() {
166
+ if (this.group) {
167
+ if (
168
+ this.app.scene &&
169
+ typeof this.app.scene.removeObject3D === "function"
170
+ ) {
171
+ this.app.scene.removeObject3D(this.group);
172
+ } else if (this.group.parent) {
173
+ this.group.parent.remove(this.group);
174
+ }
175
+
176
+ this.group = null;
177
+ this.representations = { mesh: [], wireframe: [], points: [] };
178
+ this.batchGroups = [];
179
+ }
180
+ }
181
+ }
@@ -0,0 +1,514 @@
1
+ import * as THREE from "three";
2
+ import { TERRAIN_CONFIG } from "../config.js";
3
+ import { colorMapOptions, evaluate_cmap } from "../../lib/js-colormaps.js";
4
+
5
+ export class Terrain {
6
+ constructor(terrainData, app) {
7
+ this.app = app; // Store reference to app for accessing batch manager
8
+ this.bounds = terrainData.bounds;
9
+ this.dimensions = terrainData.dimensions;
10
+ this.group = null;
11
+
12
+ this.heightData = this.#normalizeScalarField(terrainData.heightData, true);
13
+ this.frictionData = this.#normalizeScalarField(terrainData.frictionData);
14
+ this.stiffnessData = this.#normalizeScalarField(terrainData.stiffnessData);
15
+ this.isSingleton = terrainData.isSingleton;
16
+
17
+ const normals = this.#normalizeNormals(terrainData.normals);
18
+
19
+ this.#createVisualRepresentations(this.heightData, normals);
20
+ }
21
+
22
+ // Reshapes a flat Float32Array of `batchSize` concatenated per-vertex
23
+ // records (`width` floats each) into one subarray view per batch.
24
+ #splitIntoBatches(flatArray, width) {
25
+ const batchSize = this.app.batchManager.simBatches;
26
+ const resolution = this.dimensions.resolutionX * this.dimensions.resolutionY;
27
+ const perBatch = resolution * width;
28
+ const batches = [];
29
+ for (let i = 0; i < batchSize; i++) {
30
+ batches.push(flatArray.subarray(i * perBatch, (i + 1) * perBatch));
31
+ }
32
+ return batches;
33
+ }
34
+
35
+ // Normalizes a per-vertex scalar terrain field (heightData/frictionData/
36
+ // stiffnessData) to an array of one entry per batch. Passes through
37
+ // unchanged if already batched, or if absent (friction/stiffness are
38
+ // optional).
39
+ #normalizeScalarField(data, logNormalization = false) {
40
+ if (data instanceof Float32Array) {
41
+ return this.#splitIntoBatches(data, 1);
42
+ }
43
+ if (Array.isArray(data) && data.length > 0 && typeof data[0] === "number") {
44
+ if (logNormalization) {
45
+ console.debug("Normalizing flat heightData to single batch");
46
+ }
47
+ return [data];
48
+ }
49
+ return data;
50
+ }
51
+
52
+ // Normalizes terrain normals (per-vertex 3-vectors) to an array of one
53
+ // batch of vectors.
54
+ #normalizeNormals(data) {
55
+ if (data instanceof Float32Array) {
56
+ return this.#splitIntoBatches(data, 3);
57
+ }
58
+ if (Array.isArray(data) && data.length > 0) {
59
+ if (typeof data[0] === "number") {
60
+ // Case: Flat array [x, y, z, x, y, z...]
61
+ console.debug("Normalizing flat normals array to single batch of vectors");
62
+ const vectors = [];
63
+ for (let i = 0; i < data.length; i += 3) {
64
+ vectors.push([data[i], data[i + 1], data[i + 2]]);
65
+ }
66
+ return [vectors];
67
+ }
68
+ if (
69
+ Array.isArray(data[0]) &&
70
+ data[0].length > 0 &&
71
+ typeof data[0][0] === "number"
72
+ ) {
73
+ // Case: Array of vectors [[x,y,z], ...] -> Wrap in batch
74
+ console.debug("Normalizing list of normal vectors to single batch");
75
+ return [data];
76
+ }
77
+ }
78
+ return data;
79
+ }
80
+
81
+ #createMaterials() {
82
+ // Create materials for the surface and wireframe
83
+ const surfaceMaterial = new THREE.MeshPhongMaterial({
84
+ vertexColors: true,
85
+ side: THREE.DoubleSide,
86
+ flatShading: TERRAIN_CONFIG.flatShading || false,
87
+ shininess: TERRAIN_CONFIG.shininess || 10,
88
+ });
89
+
90
+ const wireframeMaterial = new THREE.MeshBasicMaterial({
91
+ color: 0xffffff,
92
+ wireframe: true,
93
+ opacity: 0.2,
94
+ transparent: true,
95
+ });
96
+
97
+ return { surfaceMaterial, wireframeMaterial };
98
+ }
99
+
100
+ #createVisualRepresentations(heightData, normals) {
101
+ console.debug(
102
+ `Creating terrain geometry: ${this.dimensions.sizeX}x${this.dimensions.sizeY} m, resolution: ${this.dimensions.resolutionX}x${this.dimensions.resolutionY}`
103
+ );
104
+ const { surfaceMaterial, wireframeMaterial } = this.#createMaterials();
105
+
106
+ let singletonSurfaceGeometry = null;
107
+ let singletonNormals = null;
108
+
109
+ if (this.isSingleton) {
110
+ singletonSurfaceGeometry = this.#createSurfaceGeometryFromHeightData(
111
+ heightData[0],
112
+ 0
113
+ );
114
+ singletonNormals = this.#createNormalVectors(
115
+ heightData[0],
116
+ normals[0]
117
+ );
118
+ }
119
+
120
+ // Create geometry for each batch
121
+ this.group = new THREE.Group();
122
+ for (let i = 0; i < this.app.batchManager.simBatches; i++) {
123
+ const batchGroup = new THREE.Group();
124
+ batchGroup.name = `batch${i}`;
125
+
126
+ const surfaceGeometry = this.isSingleton
127
+ ? singletonSurfaceGeometry
128
+ : this.#createSurfaceGeometryFromHeightData(heightData[i], i);
129
+
130
+ const surfaceMesh = new THREE.Mesh(surfaceGeometry, surfaceMaterial);
131
+ surfaceMesh.name = "surface";
132
+ surfaceMesh.receiveShadow = true;
133
+ surfaceMesh.castShadow = true;
134
+ batchGroup.add(surfaceMesh);
135
+
136
+ const wireframeMesh = new THREE.Mesh(surfaceGeometry, wireframeMaterial);
137
+ wireframeMesh.name = "wireframe";
138
+ batchGroup.add(wireframeMesh);
139
+
140
+ const surfaceNormals = this.isSingleton
141
+ ? singletonNormals.clone()
142
+ : this.#createNormalVectors(heightData[i], normals[i]);
143
+
144
+ surfaceNormals.name = "normals";
145
+ batchGroup.add(surfaceNormals);
146
+
147
+ for (const [key, value] of Object.entries(
148
+ this.app.uiState.terrainVisualizationModes
149
+ )) {
150
+ const obj = batchGroup.getObjectByName(key);
151
+ if (obj) obj.visible = value;
152
+ }
153
+ // translate by the batch offset
154
+ const batch_offset = this.app.batchManager.getBatchOffset(i);
155
+ batchGroup.position.set(batch_offset.x, batch_offset.y, batch_offset.z);
156
+ this.group.add(batchGroup);
157
+ }
158
+ }
159
+
160
+ // Normalize a value into [0, 1] against an explicit [min, max] range.
161
+ // Falls back to [0, 1] clamping when the range is missing or degenerate.
162
+ #normalizeToRange(value, min, max) {
163
+ if (
164
+ typeof min !== "number" ||
165
+ typeof max !== "number" ||
166
+ max - min === 0
167
+ ) {
168
+ return Math.max(0, Math.min(1, value));
169
+ }
170
+ const normalized = (value - min) / (max - min);
171
+ return Math.max(0, Math.min(1, normalized));
172
+ }
173
+
174
+ calculateNormalizedHeight(height) {
175
+ var normalizedHeight =
176
+ (height - this.bounds.minZ) / (this.bounds.maxZ - this.bounds.minZ);
177
+ normalizedHeight = Number.isFinite(normalizedHeight) ? normalizedHeight : 0;
178
+ normalizedHeight = Math.max(0, Math.min(1, normalizedHeight));
179
+ return normalizedHeight;
180
+ }
181
+
182
+ /**
183
+ *
184
+ * @param {array} heightData - Height data for the terrain, a flattened array
185
+ * @returns
186
+ */
187
+ #createSurfaceGeometryFromHeightData(heightData, batchIndex) {
188
+ const { sizeX, sizeY, resolutionX, resolutionY } = this.dimensions;
189
+ const { minX, minY, maxX, maxY } = this.bounds;
190
+ // Create a plane geometry with the right number of segments
191
+ const geometry = new THREE.PlaneGeometry(
192
+ sizeX,
193
+ sizeY,
194
+ resolutionX - 1,
195
+ resolutionY - 1
196
+ );
197
+ // Center the geometry based on bounds
198
+ const centerX = (minX + maxX) / 2;
199
+ const centerY = (minY + maxY) / 2;
200
+ geometry.translate(centerX, centerY, 0);
201
+ // Get attributes for direct manipulation
202
+ const position = geometry.attributes.position;
203
+ // Create color buffer
204
+ const colorAttribute = new THREE.BufferAttribute(
205
+ new Float32Array(position.count * 3),
206
+ 3
207
+ );
208
+ geometry.setAttribute("color", colorAttribute);
209
+
210
+ // Apply height data to geometry
211
+ // NOTE: THREE.js PlaneGeometry vertices are arranged in rows from bottom to top (Y increases)
212
+ const callableColormap = this.getCallableFromColorMapName(
213
+ this.app.uiState.terrainColorMap
214
+ );
215
+
216
+ for (let i = 0; i < position.count; i++) {
217
+ // Convert vertex index to grid coordinates
218
+ const col = i % resolutionX;
219
+ const invertedRow = Math.floor(i / resolutionX);
220
+ const row = resolutionY - invertedRow - 1; // Invert row index
221
+ // Calculate index in the flattened height data array
222
+ const dataIndex = row * resolutionX + col;
223
+ // Set Z coordinate (height)
224
+ position.setZ(i, heightData[dataIndex]);
225
+ }
226
+
227
+ // Apply colors
228
+ this.#updateSurfaceColor(batchIndex, geometry, callableColormap);
229
+
230
+ // Make sure changes are applied
231
+ position.needsUpdate = true;
232
+ return geometry;
233
+ }
234
+
235
+ getCallableFromColorMapName(cmapName) {
236
+ let reversed = false;
237
+ // Check if this is a reversed colormap request
238
+ if (cmapName.endsWith("_r")) {
239
+ cmapName = cmapName.substring(0, cmapName.length - 2);
240
+ reversed = true;
241
+ }
242
+ if (colorMapOptions.includes(cmapName))
243
+ return (value) => {
244
+ const [r, g, b] = evaluate_cmap(value, cmapName, reversed);
245
+ return new THREE.Color(r / 255, g / 255, b / 255);
246
+ };
247
+ console.log(
248
+ `Colormap ${cmapName} not found in colorMapOptions. Using default colormap instead.`
249
+ );
250
+ // Fallback
251
+ switch (cmapName) {
252
+ case "grayscale":
253
+ return (value) => new THREE.Color(value, value, value);
254
+ case "heatmap":
255
+ // Simple heatmap: blue->cyan->green->yellow->red
256
+ return (value) => {
257
+ if (value < 0.25) {
258
+ return new THREE.Color(0, value * 4, 1);
259
+ } else if (value < 0.5) {
260
+ return new THREE.Color(0, 1, 1 - (value - 0.25) * 4);
261
+ } else if (value < 0.75) {
262
+ return new THREE.Color((value - 0.5) * 4, 1, 0);
263
+ } else {
264
+ return new THREE.Color(1, 1 - (value - 0.75) * 4, 0);
265
+ }
266
+ };
267
+ case "terrain":
268
+ // Terrain color map - blues for low areas, greens for middle, browns/whites for high
269
+ return (value) => {
270
+ if (value < 0.2) {
271
+ return new THREE.Color(0.0, 0.2, 0.5 + value); // Deep to shallow water
272
+ } else if (value < 0.4) {
273
+ const t = (value - 0.2) * 5; // 0-1 within this range
274
+ return new THREE.Color(0.2 * t, 0.5 + 0.2 * t, 0.7 - 0.2 * t); // Shore transition
275
+ } else if (value < 0.75) {
276
+ const t = (value - 0.4) / 0.35; // 0-1 within this range
277
+ return new THREE.Color(0.2 + 0.3 * t, 0.7 - 0.2 * t, 0.5 - 0.4 * t); // Green to brown
278
+ } else {
279
+ const t = (value - 0.75) * 4; // 0-1 within this range
280
+ return new THREE.Color(0.5 + 0.5 * t, 0.5 + 0.5 * t, 0.1 + 0.9 * t); // Brown to white (snow)
281
+ }
282
+ };
283
+ default:
284
+ // Default blue to red gradient
285
+ return (value) => new THREE.Color(value, 0.2, 1 - value);
286
+ }
287
+ }
288
+
289
+ /**
290
+ *
291
+ * @param {array} heightData - Height data for the terrain, a flattened array
292
+ * @param {array} normals - Normal data for the terrain, an array of 3D vectors
293
+ * @returns {THREE.Group} - Group containing normal vectors
294
+ */
295
+ #createNormalVectors(heightData, normals) {
296
+ const { sizeX, sizeY, resolutionX, resolutionY } = this.dimensions;
297
+ const { minX, minY } = this.bounds;
298
+
299
+ const normalVectors = new THREE.Group();
300
+ normalVectors.visible =
301
+ this.app.uiState.terrainVisualizationModes.normals || false;
302
+
303
+ // Create a helper arrow for each normal
304
+ const normalLength = TERRAIN_CONFIG.normalLength || 0.5;
305
+ const skipFactor = Math.max(
306
+ 1,
307
+ Math.floor(resolutionX / TERRAIN_CONFIG.skipNormalCells)
308
+ ); // Adaptive skip factor based on resolution
309
+
310
+ // Sample normals at regular intervals
311
+ for (let row = 0; row < resolutionY; row += skipFactor) {
312
+ for (let col = 0; col < resolutionX; col += skipFactor) {
313
+ const dataIndex = row * resolutionX + col;
314
+
315
+ if (dataIndex < heightData.length) {
316
+ // Calculate real-world coordinates
317
+ const x = minX + col * (sizeX / (resolutionX - 1));
318
+ const y = minY + row * (sizeY / (resolutionY - 1));
319
+ const z = heightData[dataIndex];
320
+
321
+ // Get normal data
322
+ let nx, ny, nz;
323
+ if (normals instanceof Float32Array) {
324
+ nx = normals[dataIndex * 3];
325
+ ny = normals[dataIndex * 3 + 1];
326
+ nz = normals[dataIndex * 3 + 2];
327
+ } else {
328
+ [nx, ny, nz] = normals[dataIndex];
329
+ }
330
+
331
+ const origin = new THREE.Vector3(x, y, z);
332
+ const direction = new THREE.Vector3(nx, ny, nz);
333
+
334
+ const arrowHelper = new THREE.ArrowHelper(
335
+ direction.normalize(),
336
+ origin,
337
+ normalLength,
338
+ 0xff0000
339
+ );
340
+
341
+ normalVectors.add(arrowHelper);
342
+ }
343
+ }
344
+ }
345
+ return normalVectors;
346
+ }
347
+
348
+ // Toggle methods for visualizations - now updated to support batched terrains
349
+ toggleVisualization(type, visible) {
350
+ // Toggle in the original terrain
351
+ for (const batchGroup of this.group.children) {
352
+ const object = batchGroup.getObjectByName(type);
353
+ if (object) {
354
+ object.visible = visible;
355
+ }
356
+ }
357
+ }
358
+
359
+ #updateSurfaceColor(batchIndex, geometry, callableColormap) {
360
+ if (!geometry) return;
361
+ const position = geometry.attributes.position;
362
+ const colorAttribute = geometry.attributes.color;
363
+ if (!position || !colorAttribute) return;
364
+
365
+ const { resolutionX, resolutionY } = this.dimensions;
366
+ const mode = this.app.uiState.terrainColorMode || "height";
367
+
368
+ // Update all colors based on the new colormap
369
+ for (let i = 0; i < position.count; i++) {
370
+ let value;
371
+ if (mode === "height") {
372
+ value = this.calculateNormalizedHeight(position.getZ(i));
373
+ } else {
374
+ const col = i % resolutionX;
375
+ const invertedRow = Math.floor(i / resolutionX);
376
+ const row = resolutionY - invertedRow - 1;
377
+ const dataIndex = row * resolutionX + col;
378
+
379
+ if (mode === "friction" && this.frictionData && this.frictionData[batchIndex]) {
380
+ // Normalize against the data range shipped in bounds (falls back to [0, 1]).
381
+ value = this.#normalizeToRange(
382
+ this.frictionData[batchIndex][dataIndex],
383
+ this.bounds.minFriction,
384
+ this.bounds.maxFriction
385
+ );
386
+ } else if (mode === "stiffness" && this.stiffnessData && this.stiffnessData[batchIndex]) {
387
+ // Normalize against the data range shipped in bounds (falls back to [0, 1]).
388
+ value = this.#normalizeToRange(
389
+ this.stiffnessData[batchIndex][dataIndex],
390
+ this.bounds.minStiffness,
391
+ this.bounds.maxStiffness
392
+ );
393
+ } else {
394
+ value = this.calculateNormalizedHeight(position.getZ(i));
395
+ }
396
+ }
397
+ const color = callableColormap(value);
398
+ colorAttribute.setXYZ(i, color.r, color.g, color.b);
399
+ }
400
+ // Update the buffer
401
+ colorAttribute.needsUpdate = true;
402
+ }
403
+
404
+ // Update terrain colors with current colormap
405
+ setColorMap(colormapName) {
406
+ // Update the main terrain surface
407
+ const callableColormap = this.getCallableFromColorMapName(colormapName);
408
+ const batchesToUpdate = this.isSingleton ? 1 : this.app.batchManager.simBatches;
409
+
410
+ for (let i = 0; i < batchesToUpdate; i++) {
411
+ const batchGroup = this.group.getObjectByName(`batch${i}`);
412
+ const surfaceMesh = batchGroup.getObjectByName("surface");
413
+ const geometry = surfaceMesh.geometry;
414
+ this.#updateSurfaceColor(i, geometry, callableColormap);
415
+ }
416
+ }
417
+
418
+ // Update terrain colors with current mode
419
+ setColorMode(mode) {
420
+ const callableColormap = this.getCallableFromColorMapName(this.app.uiState.terrainColorMap);
421
+ const batchesToUpdate = this.isSingleton ? 1 : this.app.batchManager.simBatches;
422
+
423
+ for (let i = 0; i < batchesToUpdate; i++) {
424
+ const batchGroup = this.group.getObjectByName(`batch${i}`);
425
+ const surfaceMesh = batchGroup.getObjectByName("surface");
426
+ const geometry = surfaceMesh.geometry;
427
+ this.#updateSurfaceColor(i, geometry, callableColormap);
428
+ }
429
+ }
430
+
431
+ getAvailableColorModes() {
432
+ const modes = [];
433
+ if (this.heightData && this.heightData.length > 0) {
434
+ modes.push("height");
435
+ }
436
+ if (this.frictionData && this.frictionData.length > 0) {
437
+ modes.push("friction");
438
+ }
439
+ if (this.stiffnessData && this.stiffnessData.length > 0) {
440
+ modes.push("stiffness");
441
+ }
442
+ return modes;
443
+ }
444
+
445
+ getPropertiesAt(x, y, batchIndex) {
446
+ const batchOffset = this.app.batchManager.getBatchOffset(batchIndex);
447
+ const localX = x - batchOffset.x;
448
+ const localY = y - batchOffset.y;
449
+
450
+ const { sizeX, sizeY, resolutionX, resolutionY } = this.dimensions;
451
+ const { minX, minY, maxX, maxY } = this.bounds;
452
+
453
+ if (localX < minX || localX > maxX || localY < minY || localY > maxY) {
454
+ return null;
455
+ }
456
+
457
+ const col = Math.round(((localX - minX) / sizeX) * (resolutionX - 1));
458
+ const row = Math.round(((localY - minY) / sizeY) * (resolutionY - 1));
459
+
460
+ if (col < 0 || col >= resolutionX || row < 0 || row >= resolutionY) return null;
461
+
462
+ const dataIndex = row * resolutionX + col;
463
+ const props = {};
464
+
465
+ const dataBatchIndex = this.isSingleton ? 0 : batchIndex;
466
+
467
+ if (this.heightData && this.heightData[dataBatchIndex]) {
468
+ props.height = this.heightData[dataBatchIndex][dataIndex];
469
+ } else {
470
+ return null;
471
+ }
472
+
473
+ if (this.frictionData && this.frictionData[dataBatchIndex]) {
474
+ props.friction = this.frictionData[dataBatchIndex][dataIndex];
475
+ }
476
+
477
+ if (this.stiffnessData && this.stiffnessData[dataBatchIndex]) {
478
+ props.stiffness = this.stiffnessData[dataBatchIndex][dataIndex];
479
+ }
480
+
481
+ return props;
482
+ }
483
+
484
+ // Get THREE.js group containing all visualizations
485
+ getObject3D() {
486
+ return this.group;
487
+ }
488
+
489
+ // Clean up resources when terrain is no longer needed
490
+ dispose() {
491
+ const geometries = new Set();
492
+ const materials = new Set();
493
+
494
+ for (const batchGroup of this.group.children) {
495
+ batchGroup.traverse((child) => {
496
+ if (child.geometry) geometries.add(child.geometry);
497
+ if (child.material) {
498
+ if (Array.isArray(child.material)) {
499
+ child.material.forEach((m) => materials.add(m));
500
+ } else {
501
+ materials.add(child.material);
502
+ }
503
+ }
504
+ });
505
+ if (this.app && this.app.scene) {
506
+ this.app.scene.removeObject3D(batchGroup);
507
+ }
508
+ }
509
+
510
+ geometries.forEach((g) => g.dispose());
511
+ materials.forEach((m) => m.dispose());
512
+ this.group = null;
513
+ }
514
+ }