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,616 @@
1
+ import * as THREE from "three";
2
+ import { BODY_CONFIG, BODY_VECTOR_CONFIG, TRAIL_CONFIG } from "../config.js";
3
+ import {
4
+ createGeometry,
5
+ createMesh,
6
+ createPoints,
7
+ createContactPoints,
8
+ createArrow,
9
+ } from "./utils.js";
10
+
11
+ export class Body {
12
+ constructor(bodyData, app) {
13
+ this.app = app;
14
+ this.name = bodyData.name;
15
+ this.simBatches = app.batchManager.simBatches;
16
+
17
+ // Data Normalization: Handle root-level bodyPoints
18
+ if (bodyData.bodyPoints && !bodyData.shape.points) {
19
+ bodyData.shape.points = bodyData.bodyPoints;
20
+ }
21
+
22
+ // Mandatory attributes
23
+ this.positions = Array(this.simBatches)
24
+ .fill()
25
+ .map(() => new THREE.Vector3());
26
+ this.quaternions = Array(this.simBatches)
27
+ .fill()
28
+ .map(() => new THREE.Quaternion());
29
+ this.rotations = Array(this.simBatches)
30
+ .fill()
31
+ .map(() => new THREE.Euler()); // For debugging/display
32
+
33
+ // Initialize pose from bodyTransform if available
34
+ if (bodyData.bodyTransform) {
35
+ const transforms = Array.isArray(bodyData.bodyTransform[0]) ? bodyData.bodyTransform : [bodyData.bodyTransform];
36
+ for (let i = 0; i < Math.min(this.simBatches, transforms.length); i++) {
37
+ const t = transforms[i];
38
+ if (t.length >= 7) {
39
+ this.positions[i].set(t[0], t[1], t[2]);
40
+ // JSON quaternion order in bodyTransform is [w, x, y, z] based on observation
41
+ // Three.js set() takes (x, y, z, w)
42
+ this.quaternions[i].set(t[4], t[5], t[6], t[3]);
43
+ this.rotations[i].setFromQuaternion(this.quaternions[i]);
44
+ }
45
+ }
46
+ }
47
+
48
+ // Optional attributes management: only what the JSON actually declares as
49
+ // available is exposed to the UI, so options for missing data stay hidden.
50
+ this.availableAttributes = new Set();
51
+ if (bodyData.availableAttributes) {
52
+ bodyData.availableAttributes.forEach(attr => this.availableAttributes.add(attr));
53
+ }
54
+
55
+ // Storage/updaters are always set up so state updates can be applied if
56
+ // data arrives, independent of whether the attribute is UI-visible.
57
+ this.attributeStorage = new Map();
58
+ this.attributeUpdaters = new Map();
59
+
60
+ const vectorAttrs = ["velocity", "angularVelocity", "force", "torque"];
61
+ vectorAttrs.forEach(attr => {
62
+ this.attributeStorage.set(attr, Array(this.simBatches).fill().map(() => new THREE.Vector3()));
63
+ this.attributeUpdaters.set(attr, (data, batchIndex) => this.updateBodyVector(attr, data, batchIndex));
64
+ });
65
+
66
+ this.attributeStorage.set("contacts", Array(this.simBatches).fill().map(() => []));
67
+ this.attributeUpdaters.set("contacts", (data, batchIndex) => this.updateContactPointsVisibility(data, batchIndex));
68
+
69
+ this.hasContacts = this.availableAttributes.has("contacts");
70
+
71
+ // Reusable temporaries for hot-path matrix updates (avoid per-frame allocations)
72
+ this._worldPos = new THREE.Vector3();
73
+ this._unitScale = new THREE.Vector3(1, 1, 1);
74
+ this._matrix = new THREE.Matrix4();
75
+ this._normalizedVec = new THREE.Vector3();
76
+
77
+ // Scene setup
78
+ this.group = new THREE.Group();
79
+ this.group.name = this.name;
80
+ this.batchGroups = [];
81
+ this.bodyVectors = [];
82
+ this.contactPoints = [];
83
+ this.contactPointSizes = [];
84
+ this.representations = { mesh: [], wireframe: [], points: [] };
85
+ this.createBatchGroups(bodyData);
86
+
87
+ // Position/orientation history across all loaded states, per batch. Local
88
+ // (un-offset) coordinates, shared by trail rendering and error metrics.
89
+ this.validStates = 0;
90
+ this.positionHistory = [];
91
+ this.quaternionHistory = [];
92
+ this.trails = [];
93
+ this._trailStateIndex = -1;
94
+ }
95
+
96
+ // --- History (position/orientation over all states) ---
97
+
98
+ allocateHistory(numStates) {
99
+ this.allocatedStates = numStates;
100
+ this.validStates = numStates;
101
+ this.positionHistory = Array(this.simBatches)
102
+ .fill()
103
+ .map(() => new Float32Array(numStates * 3));
104
+ this.quaternionHistory = Array(this.simBatches)
105
+ .fill()
106
+ .map(() => new Float32Array(numStates * 4));
107
+ }
108
+
109
+ setHistoryPointAt(stateIndex, bodyState) {
110
+ if (!bodyState.bodyTransform) return;
111
+ const transformData = bodyState.bodyTransform;
112
+ const transforms = Array.isArray(transformData[0])
113
+ ? transformData
114
+ : [transformData];
115
+ for (let i = 0; i < Math.min(this.simBatches, transforms.length); i++) {
116
+ const t = transforms[i];
117
+ if (t.length < 7) continue;
118
+ const pBase = stateIndex * 3;
119
+ this.positionHistory[i][pBase] = t[0];
120
+ this.positionHistory[i][pBase + 1] = t[1];
121
+ this.positionHistory[i][pBase + 2] = t[2];
122
+ const qBase = stateIndex * 4;
123
+ // Stored as (x, y, z, w) to match THREE.Quaternion's convention.
124
+ this.quaternionHistory[i][qBase] = t[4];
125
+ this.quaternionHistory[i][qBase + 1] = t[5];
126
+ this.quaternionHistory[i][qBase + 2] = t[6];
127
+ this.quaternionHistory[i][qBase + 3] = t[3];
128
+ }
129
+ }
130
+
131
+ appendHistoryPointAt(stateIndex, bodyState) {
132
+ if (!this.positionHistory || this.positionHistory.length === 0) {
133
+ this.allocateHistory(100);
134
+ this.validStates = 0;
135
+ }
136
+ if (stateIndex >= this.allocatedStates) {
137
+ const newNumStates = Math.max(stateIndex + 1, this.allocatedStates * 2 || 100);
138
+ for(let i=0; i<this.simBatches; i++) {
139
+ const newPos = new Float32Array(newNumStates * 3);
140
+ newPos.set(this.positionHistory[i]);
141
+ this.positionHistory[i] = newPos;
142
+ const newQuat = new Float32Array(newNumStates * 4);
143
+ newQuat.set(this.quaternionHistory[i]);
144
+ this.quaternionHistory[i] = newQuat;
145
+ }
146
+ this.allocatedStates = newNumStates;
147
+ }
148
+ this.setHistoryPointAt(stateIndex, bodyState);
149
+ this.validStates = Math.max(this.validStates || 0, stateIndex + 1);
150
+ }
151
+
152
+ // --- Trails ---
153
+
154
+ finalizeTrails() {
155
+ this.disposeTrails();
156
+ const count = this.validStates || 0;
157
+ if (count < 2) return;
158
+ for (let i = 0; i < this.simBatches; i++) {
159
+ const offset = this.app.batchManager
160
+ ? this.app.batchManager.getBatchOffset(i)
161
+ : { x: 0, y: 0, z: 0 };
162
+ const localPositions = this.positionHistory[i];
163
+ const worldPositions = new Float32Array(count * 3);
164
+ for (let s = 0; s < count; s++) {
165
+ const base = s * 3;
166
+ worldPositions[base] = localPositions[base] + offset.x;
167
+ worldPositions[base + 1] = localPositions[base + 1] + offset.y;
168
+ worldPositions[base + 2] = localPositions[base + 2] + offset.z;
169
+ }
170
+ const geometry = new THREE.BufferGeometry();
171
+ geometry.setAttribute(
172
+ "position",
173
+ new THREE.BufferAttribute(worldPositions, 3)
174
+ );
175
+ geometry.setDrawRange(0, 1);
176
+ const color = this.app.batchManager
177
+ ? this.app.batchManager.getColorForBatch(i)
178
+ : 0xffffff;
179
+ const material = new THREE.LineBasicMaterial({
180
+ color,
181
+ transparent: true,
182
+ opacity: TRAIL_CONFIG.opacity,
183
+ });
184
+ const line = new THREE.Line(geometry, material);
185
+ line.frustumCulled = false;
186
+ line.visible = this.app.uiState.trailsVisible || false;
187
+ this.group.add(line);
188
+ this.trails[i] = line;
189
+ }
190
+ this._trailStateIndex = -1;
191
+ const currentIndex = this.app.animationController
192
+ ? this.app.animationController.getCurrentStateIndex()
193
+ : 0;
194
+ this.updateTrails(currentIndex);
195
+ }
196
+
197
+ updateTrails(stateIndex) {
198
+ if (this._trailStateIndex === stateIndex || this.trails.length === 0) return;
199
+ this._trailStateIndex = stateIndex;
200
+ const count = Math.max(1, Math.min(stateIndex + 1, this.validStates || 1));
201
+ for (const line of this.trails) {
202
+ if (line) line.geometry.setDrawRange(0, count);
203
+ }
204
+ }
205
+
206
+ toggleTrails(visible) {
207
+ this.trails.forEach((line) => line && (line.visible = visible));
208
+ }
209
+
210
+ disposeTrails() {
211
+ for (const line of this.trails) {
212
+ if (line) {
213
+ this.group.remove(line);
214
+ line.geometry.dispose();
215
+ line.material.dispose();
216
+ }
217
+ }
218
+ this.trails = [];
219
+ }
220
+
221
+ createBatchGroups(bodyData) {
222
+ const shape = bodyData.shape;
223
+
224
+ // Initialize instanced representations
225
+ if (shape.points && shape.points.length > 0) {
226
+ this.representations["points"] = this.createInstancedRepresentation(
227
+ "points",
228
+ shape.points,
229
+ BODY_CONFIG.points,
230
+ bodyData
231
+ );
232
+ }
233
+
234
+ if (shape.type !== "pointcloud") {
235
+ const geometry = createGeometry(shape, BODY_CONFIG.geometry);
236
+ if (geometry) {
237
+ this.representations["mesh"] = this.createInstancedRepresentation(
238
+ "mesh",
239
+ geometry,
240
+ BODY_CONFIG.mesh,
241
+ bodyData
242
+ );
243
+ this.representations["wireframe"] = this.createInstancedRepresentation(
244
+ "wireframe",
245
+ geometry,
246
+ BODY_CONFIG.wireframe,
247
+ bodyData
248
+ );
249
+ }
250
+ }
251
+
252
+ // Initialize contact points
253
+ if (
254
+ this.hasContacts &&
255
+ (shape.type === "pointcloud" || shape.type === "mesh" || shape.points || shape.vertices)
256
+ ) {
257
+ const points = shape.points || shape.vertices;
258
+ this.initializeInstancedContactPoints(points);
259
+ }
260
+
261
+ // We still need batch groups for individual axes or other non-instanced elements
262
+ for (let i = 0; i < this.simBatches; i++) {
263
+ const batchGroup = new THREE.Group();
264
+ batchGroup.name = `${this.name}_batch_${i}`;
265
+ this.group.add(batchGroup);
266
+ this.batchGroups.push(batchGroup);
267
+
268
+ // Axes helper is not instanced for now as it's for debugging
269
+ const axes = new THREE.AxesHelper(1);
270
+ axes.visible = this.app.uiState.axesVisible;
271
+ batchGroup.add(axes);
272
+
273
+ // Body vectors (arrows) are also not instanced yet
274
+ this.initializeBodyVectors(batchGroup, BODY_VECTOR_CONFIG, i);
275
+ }
276
+
277
+ // Initial update of instances
278
+ this.updateAllInstances();
279
+ }
280
+
281
+ createInstancedRepresentation(type, source, config, bodyData) {
282
+ if (type === "points") {
283
+ const pointsList = [];
284
+ for (let i = 0; i < this.simBatches; i++) {
285
+ const points = createPoints(source, config);
286
+ if (points) {
287
+ points.visible = this.app.uiState.bodyVisualizationMode === "points";
288
+ this.group.add(points);
289
+ pointsList.push(points);
290
+ }
291
+ }
292
+ return pointsList;
293
+ } else {
294
+ let material;
295
+ if (type === "mesh") {
296
+ material = createMesh(source, config).material;
297
+ } else if (type === "wireframe") {
298
+ // InstancedMesh only works with Mesh primitives, so we use a Mesh material with wireframe: true
299
+ material = new THREE.MeshBasicMaterial({
300
+ color: config.color || 0x4080ff,
301
+ wireframe: true,
302
+ transparent: config.transparent || false,
303
+ opacity: config.opacity || 1.0
304
+ });
305
+ }
306
+
307
+ const instancedMesh = new THREE.InstancedMesh(source, material, this.simBatches);
308
+ instancedMesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
309
+ instancedMesh.visible = this.app.uiState.bodyVisualizationMode === type;
310
+ instancedMesh.castShadow = (type === "mesh");
311
+ instancedMesh.receiveShadow = (type === "mesh");
312
+ this.group.add(instancedMesh);
313
+ return instancedMesh;
314
+ }
315
+ }
316
+
317
+ initializeInstancedContactPoints(points) {
318
+ if (!points?.length) return;
319
+ for (let i = 0; i < this.simBatches; i++) {
320
+ const contactPoints = createContactPoints(points, BODY_CONFIG.contactPoints);
321
+ if (contactPoints) {
322
+ const pointCount = points.length;
323
+ this.contactPointSizes[i] = new Float32Array(pointCount).fill(0);
324
+ contactPoints.geometry.setAttribute(
325
+ "size",
326
+ new THREE.Float32BufferAttribute(this.contactPointSizes[i], 1)
327
+ );
328
+ contactPoints.visible = this.app.uiState.attributeVisible.contacts || false;
329
+ this.group.add(contactPoints);
330
+ this.contactPoints[i] = contactPoints;
331
+ }
332
+ }
333
+ }
334
+
335
+ updateAllInstances() {
336
+ for (let i = 0; i < this.simBatches; i++) {
337
+ this.updateInstanceMatrix(i);
338
+ }
339
+ // Set needsUpdate only once after all matrices are updated
340
+ if (this.representations["mesh"] instanceof THREE.InstancedMesh) {
341
+ this.representations["mesh"].instanceMatrix.needsUpdate = true;
342
+ }
343
+ if (this.representations["wireframe"] instanceof THREE.InstancedMesh) {
344
+ this.representations["wireframe"].instanceMatrix.needsUpdate = true;
345
+ }
346
+ }
347
+
348
+ updateInstanceMatrix(batchIndex) {
349
+ if (batchIndex >= this.simBatches) return;
350
+
351
+ const position = this.positions[batchIndex];
352
+ const quaternion = this.quaternions[batchIndex];
353
+
354
+ if (!position || !quaternion) {
355
+ console.warn(`Position or quaternion missing for batch ${batchIndex}`);
356
+ return;
357
+ }
358
+
359
+ const offset = this.app.batchManager
360
+ ? this.app.batchManager.getBatchOffset(batchIndex)
361
+ : { x: 0, y: 0, z: 0 };
362
+
363
+ this._worldPos.set(
364
+ position.x + offset.x,
365
+ position.y + offset.y,
366
+ position.z + offset.z
367
+ );
368
+
369
+ this._matrix.compose(this._worldPos, quaternion, this._unitScale);
370
+
371
+ // Update InstancedMesh instances without setting needsUpdate yet
372
+ if (this.representations["mesh"] instanceof THREE.InstancedMesh) {
373
+ this.representations["mesh"].setMatrixAt(batchIndex, this._matrix);
374
+ }
375
+ if (this.representations["wireframe"] instanceof THREE.InstancedMesh) {
376
+ this.representations["wireframe"].setMatrixAt(batchIndex, this._matrix);
377
+ }
378
+
379
+ // Update non-instanced representations with strict index checks
380
+ const pointsRep = this.representations["points"];
381
+ if (Array.isArray(pointsRep) && pointsRep[batchIndex]) {
382
+ pointsRep[batchIndex].position.copy(this._worldPos);
383
+ pointsRep[batchIndex].quaternion.copy(quaternion);
384
+ }
385
+
386
+ if (Array.isArray(this.contactPoints) && this.contactPoints[batchIndex]) {
387
+ this.contactPoints[batchIndex].position.copy(this._worldPos);
388
+ this.contactPoints[batchIndex].quaternion.copy(quaternion);
389
+ }
390
+
391
+ if (Array.isArray(this.batchGroups) && this.batchGroups[batchIndex]) {
392
+ this.batchGroups[batchIndex].position.copy(this._worldPos);
393
+ this.batchGroups[batchIndex].quaternion.copy(quaternion);
394
+ }
395
+ }
396
+
397
+ updateAttribute(attr, data, batchIndex) {
398
+ if (!this.attributeStorage.has(attr) || batchIndex >= this.simBatches)
399
+ return;
400
+ const storage = this.attributeStorage.get(attr);
401
+ if (attr === "contacts") {
402
+ storage[batchIndex] = Array.isArray(data) ? data : [];
403
+ } else {
404
+ storage[batchIndex].fromArray(data.length >= 3 ? data : [0, 0, 0]);
405
+ }
406
+ const updater = this.attributeUpdaters.get(attr);
407
+ if (updater) updater(storage[batchIndex], batchIndex);
408
+ }
409
+
410
+ updateState(bodyState) {
411
+ // Update from bodyTransform (combined position + orientation [w,x,y,z])
412
+ if (bodyState.bodyTransform) {
413
+ const transformData = bodyState.bodyTransform;
414
+ const applyTransform = (t, i) => {
415
+ if (t.length >= 7) {
416
+ this.positions[i].set(t[0], t[1], t[2]);
417
+ // Three.js set() takes (x, y, z, w)
418
+ this.quaternions[i].set(t[4], t[5], t[6], t[3]);
419
+ this.rotations[i].setFromQuaternion(this.quaternions[i]);
420
+ }
421
+ };
422
+ if (Array.isArray(transformData[0])) {
423
+ for (let i = 0; i < Math.min(this.simBatches, transformData.length); i++) {
424
+ applyTransform(transformData[i], i);
425
+ }
426
+ } else {
427
+ applyTransform(transformData, 0);
428
+ }
429
+ }
430
+
431
+ // Update vector attributes (velocity, angularVelocity, force, torque). Each
432
+ // is provided directly under its own key, batched or single like bodyTransform.
433
+ const vectorAttrs = ["velocity", "angularVelocity", "force", "torque"];
434
+ vectorAttrs.forEach((attr) => {
435
+ const data = bodyState[attr];
436
+ if (!data) return;
437
+ if (Array.isArray(data[0])) {
438
+ for (let i = 0; i < Math.min(this.simBatches, data.length); i++) {
439
+ this.updateAttribute(attr, data[i], i);
440
+ }
441
+ } else {
442
+ this.updateAttribute(attr, data, 0);
443
+ }
444
+ });
445
+
446
+ if (bodyState.contacts) {
447
+ const contactsData = bodyState.contacts;
448
+ if (Array.isArray(contactsData)) {
449
+ for (let i = 0; i < Math.min(this.simBatches, contactsData.length); i++) {
450
+ this.updateAttribute("contacts", contactsData[i], i);
451
+ }
452
+ }
453
+ }
454
+
455
+ // Finally update all instance matrices in one go
456
+ this.updateAllInstances();
457
+
458
+ if (this.trails.length > 0 && this.app.animationController) {
459
+ this.updateTrails(this.app.animationController.getCurrentStateIndex());
460
+ }
461
+ }
462
+
463
+ setPosition(positionData, batchIndex = 0) {
464
+ if (
465
+ !positionData ||
466
+ positionData.length < 3 ||
467
+ batchIndex >= this.simBatches
468
+ )
469
+ return;
470
+ const [x, y, z] = positionData;
471
+ this.positions[batchIndex].set(x, y, z);
472
+ this.updateInstanceMatrix(batchIndex);
473
+ }
474
+
475
+ setOrientation(orientationData, batchIndex = 0) {
476
+ if (
477
+ !orientationData ||
478
+ orientationData.length < 4 ||
479
+ batchIndex >= this.simBatches
480
+ )
481
+ return;
482
+ const [qw, qx, qy, qz] = orientationData;
483
+ this.quaternions[batchIndex].set(qx, qy, qz, qw);
484
+ this.rotations[batchIndex].setFromQuaternion(this.quaternions[batchIndex]);
485
+ this.updateInstanceMatrix(batchIndex);
486
+ }
487
+
488
+ initializeBodyVectors(batchGroup, vectorConfigs, batchIndex) {
489
+ if (!this.bodyVectors[batchIndex]) {
490
+ this.bodyVectors[batchIndex] = new Map();
491
+ }
492
+ for (const attr of this.availableAttributes) {
493
+ if (attr !== "contacts" && vectorConfigs[attr]) {
494
+ const config = vectorConfigs[attr];
495
+ const vector = createArrow(
496
+ new THREE.Vector3(),
497
+ new THREE.Vector3(0, 1, 0),
498
+ config
499
+ );
500
+ vector.visible = this.app.uiState.attributeVisible[attr] || false;
501
+ vector.userData = { scale: config.scale };
502
+ batchGroup.add(vector);
503
+ this.bodyVectors[batchIndex].set(attr, vector);
504
+ }
505
+ }
506
+ }
507
+
508
+ updateBodyVector(type, vector, batchIndex) {
509
+ const arrow = this.bodyVectors[batchIndex]?.get(type);
510
+ if (!arrow) return;
511
+ const scale = arrow.userData.scale || 1.0;
512
+ const length = vector.length() * scale;
513
+ if (length < 1e-6) {
514
+ arrow.setLength(0);
515
+ return;
516
+ }
517
+ this._normalizedVec.copy(vector).normalize();
518
+ arrow.setDirection(this._normalizedVec);
519
+ const headLength = Math.min(length * 0.2, 0.5);
520
+ const headWidth = Math.min(length * 0.1, 0.25);
521
+ arrow.setLength(length, headLength, headWidth);
522
+ }
523
+
524
+ updateContactPointsVisibility(contactIndices, batchIndex) {
525
+ if (!this.contactPoints[batchIndex] || !this.contactPointSizes[batchIndex])
526
+ return;
527
+ this.contactPointSizes[batchIndex].fill(0);
528
+ if (Array.isArray(contactIndices)) {
529
+ contactIndices.forEach((index) => {
530
+ if (index >= 0 && index < this.contactPointSizes[batchIndex].length) {
531
+ this.contactPointSizes[batchIndex][index] =
532
+ BODY_CONFIG.contactPoints.size;
533
+ }
534
+ });
535
+ }
536
+ const sizeAttribute =
537
+ this.contactPoints[batchIndex].geometry.getAttribute("size");
538
+ sizeAttribute.array = this.contactPointSizes[batchIndex];
539
+ sizeAttribute.needsUpdate = true;
540
+ }
541
+
542
+ updateVisualizationMode(mode) {
543
+ for (const [type, obj] of Object.entries(this.representations)) {
544
+ if (obj instanceof THREE.InstancedMesh) {
545
+ obj.visible = type === mode;
546
+ } else if (Array.isArray(obj)) {
547
+ obj.forEach((o) => (o.visible = type === mode));
548
+ }
549
+ }
550
+ }
551
+
552
+ toggleContactPoints(visible) {
553
+ this.contactPoints.forEach((cp) => cp && (cp.visible = visible));
554
+ }
555
+
556
+ toggleAxes(visible) {
557
+ this.batchGroups.forEach((group) => {
558
+ const axes = group.children.find(
559
+ (child) => child instanceof THREE.AxesHelper
560
+ );
561
+ if (axes) axes.visible = visible;
562
+ });
563
+ }
564
+
565
+ toggleBodyVector(type, visible) {
566
+ if (!this.availableAttributes.has(type)) return;
567
+ this.bodyVectors.forEach((map) => {
568
+ const vector = map.get(type);
569
+ if (vector) vector.visible = visible;
570
+ });
571
+ }
572
+
573
+ getObject3D() {
574
+ return this.group;
575
+ }
576
+
577
+ getAvailableAttributes() {
578
+ return this.availableAttributes;
579
+ }
580
+
581
+ // Which of the mesh/wireframe/points representations this body actually has,
582
+ // so the UI can only offer visualization modes that will show something.
583
+ getAvailableVisualizationModes() {
584
+ const modes = [];
585
+ for (const type of ["mesh", "wireframe", "points"]) {
586
+ const rep = this.representations[type];
587
+ if (
588
+ rep instanceof THREE.InstancedMesh ||
589
+ (Array.isArray(rep) && rep.length > 0)
590
+ ) {
591
+ modes.push(type);
592
+ }
593
+ }
594
+ return modes;
595
+ }
596
+
597
+ dispose() {
598
+ if (this.group && this.group.parent) {
599
+ this.group.parent.remove(this.group);
600
+ this.group.traverse((child) => {
601
+ if (child.geometry) child.geometry.dispose();
602
+ if (child.material) {
603
+ Array.isArray(child.material)
604
+ ? child.material.forEach((m) => m.dispose())
605
+ : child.material.dispose();
606
+ }
607
+ });
608
+ }
609
+ this.group = null;
610
+ this.batchGroups = [];
611
+ this.bodyVectors = [];
612
+ this.contactPoints = [];
613
+ this.contactPointSizes = [];
614
+ this.trails = [];
615
+ }
616
+ }