@inweb/viewer-three 27.2.2 → 27.3.0

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.
@@ -23,6 +23,10 @@ import {
23
23
  NormalBlending,
24
24
  BufferAttribute,
25
25
  LineBasicMaterial,
26
+ DataTexture,
27
+ RGBAFormat,
28
+ FloatType,
29
+ NearestFilter,
26
30
  } from "three";
27
31
  import { GL_CONSTANTS } from "./GltfStructure.js";
28
32
  import { mergeGeometries } from "three/examples/jsm/utils/BufferGeometryUtils.js";
@@ -61,10 +65,6 @@ export class DynamicGltfLoader {
61
65
 
62
66
  this.memoryLimit = this.getAvailableMemory();
63
67
  this.optimizationMemoryMultiplier = 5;
64
- /**
65
- * Real memory ~1.7x raw geometry (Three.js objects, Map/Set, buffers overhead). Used for limit
66
- * checks and display.
67
- */
68
68
  this.memoryEstimationFactor = 1.7;
69
69
  this.loadedGeometrySize = 0;
70
70
  this.geometryCache = new Map();
@@ -110,6 +110,12 @@ export class DynamicGltfLoader {
110
110
  this.objectTransforms = new Map(); // originalObject -> Matrix4
111
111
  this.transformedGeometries = new Map(); // mergedObject.uuid -> original position data
112
112
 
113
+ // TODO: Remove when highlight is implemented via shader.
114
+ // Sync transforms to original (hidden) objects so GeometryHighlighter
115
+ // wireframes follow the exploded positions.
116
+ this.syncTransformsToOriginalObjects = true;
117
+ this._originalObjectMatrices = new Map(); // object -> original matrix (for restore)
118
+
113
119
  this.activeChunkLoads = 0;
114
120
  this.chunkQueue = [];
115
121
 
@@ -126,6 +132,123 @@ export class DynamicGltfLoader {
126
132
  this.mergedGeometryVisibility = new Map(); // mergedObject -> visibility array
127
133
 
128
134
  this._webglInfoCache = null;
135
+
136
+ // Transform texture support
137
+ this.transformTextureSize = 1024;
138
+ this.transformTexture = this.createDummyTexture();
139
+ this.transformData = null;
140
+ this.identityTransformData = null;
141
+ this.visibilityMaterials = new Set(); // Keep track of materials to update uniforms
142
+ }
143
+
144
+ // layout (1 matrix = 4 pixels)
145
+ // RGBA RGBA RGBA RGBA (=> column1, column2, column3, column4)
146
+ // with 8x8 pixel texture max 16 matrices * 4 pixels = (8 * 8)
147
+ // 16x16 pixel texture max 64 matrices * 4 pixels = (16 * 16)
148
+ // 32x32 pixel texture max 256 matrices * 4 pixels = (32 * 32)
149
+ // 64x64 pixel texture max 1024 matrices * 4 pixels = (64 * 64)
150
+
151
+ createDummyTexture() {
152
+ // Create 1x1 dummy texture with identity matrix to prevent shader crash/black screen
153
+ const data = new Float32Array(16); // 4x4 matrix
154
+ const identity = new Matrix4();
155
+ identity.toArray(data);
156
+
157
+ // Correct dummy size: 4x1 to hold at least one matrix
158
+ const dummyData = new Float32Array(16);
159
+ identity.toArray(dummyData);
160
+ const dummyTexture = new DataTexture(dummyData, 4, 1, RGBAFormat, FloatType);
161
+
162
+ dummyTexture.minFilter = NearestFilter;
163
+ dummyTexture.magFilter = NearestFilter;
164
+ dummyTexture.needsUpdate = true;
165
+ return dummyTexture;
166
+ }
167
+
168
+ initTransformTexture() {
169
+ if (this.transformTexture) {
170
+ this.transformTexture.dispose();
171
+ }
172
+
173
+ // Logic from BatchedMesh.js _initMatricesTexture
174
+ // layout (1 matrix = 4 pixels)
175
+ // RGBA RGBA RGBA RGBA (=> column1, column2, column3, column4)
176
+ const maxInstanceCount = this.maxObjectId + 1;
177
+ let size = Math.sqrt(maxInstanceCount * 4); // 4 pixels needed for 1 matrix
178
+ size = Math.ceil(size / 4) * 4;
179
+ size = Math.max(size, 4);
180
+
181
+ this.transformTextureSize = size;
182
+ const arraySize = size * size * 4; // 4 floats per RGBA pixel
183
+ this.transformData = new Float32Array(arraySize);
184
+
185
+ // Create and cache identity matrices for fast reset
186
+ this.identityTransformData = new Float32Array(arraySize);
187
+ for (let i = 0; i <= this.maxObjectId; i++) {
188
+ const base = i * 16;
189
+ if (base + 15 < arraySize) {
190
+ this.identityTransformData[base + 0] = 1; // m00
191
+ this.identityTransformData[base + 5] = 1; // m11
192
+ this.identityTransformData[base + 10] = 1; // m22
193
+ this.identityTransformData[base + 15] = 1; // m33
194
+ }
195
+ }
196
+
197
+ // Initialize with identity matrices
198
+ this._resetTransformData(false);
199
+
200
+ this.transformTexture = new DataTexture(this.transformData, size, size, RGBAFormat, FloatType);
201
+
202
+ this.transformTexture.needsUpdate = true;
203
+ this.transformTexture.generateMipmaps = false;
204
+
205
+ console.log(`Initialized transform texture: ${size}x${size} for ${maxInstanceCount} objects`);
206
+
207
+ this.updateMaterialUniforms();
208
+
209
+ // Force all visibility materials to update
210
+ this.visibilityMaterials.forEach((material) => {
211
+ material.needsUpdate = true;
212
+ });
213
+ }
214
+
215
+ // Fast reset to Identity matrices without creating Matrix4 objects
216
+ _resetTransformData(updateTexture = true) {
217
+ if (!this.transformData || !this.identityTransformData) return;
218
+
219
+ // Fast copy from cached identity array
220
+ this.transformData.set(this.identityTransformData);
221
+
222
+ if (updateTexture) {
223
+ this.updateTransformTexture();
224
+ }
225
+ }
226
+
227
+ updateMaterialUniforms() {
228
+ // Only update if the texture actually changed
229
+ // In three.js, setting `.value = this.transformTexture` triggers uniformity checks
230
+ // We can avoid looping over all materials if the texture reference hasn't changed
231
+ if (
232
+ this._lastTransformTexture === this.transformTexture &&
233
+ this._lastTransformTextureSize === this.transformTextureSize
234
+ ) {
235
+ return;
236
+ }
237
+
238
+ this._lastTransformTexture = this.transformTexture;
239
+ this._lastTransformTextureSize = this.transformTextureSize;
240
+
241
+ this.visibilityMaterials.forEach((material) => {
242
+ if (material.userData && material.userData.visibilityUniforms) {
243
+ material.userData.visibilityUniforms.transformTexture.value = this.transformTexture;
244
+ material.userData.visibilityUniforms.transformTextureSize.value = this.transformTextureSize;
245
+ }
246
+ });
247
+ }
248
+
249
+ updateTransformTexture() {
250
+ if (!this.transformTexture) return;
251
+ this.transformTexture.needsUpdate = true;
129
252
  }
130
253
 
131
254
  setVisibleEdges(visible) {
@@ -1169,42 +1292,100 @@ export class DynamicGltfLoader {
1169
1292
  }
1170
1293
 
1171
1294
  createVisibilityMaterial(material) {
1295
+ this.visibilityMaterials.add(material);
1296
+
1297
+ const uniforms = {
1298
+ transformTexture: { value: this.transformTexture },
1299
+ transformTextureSize: { value: this.transformTextureSize },
1300
+ };
1301
+ material.userData.visibilityUniforms = uniforms;
1302
+
1172
1303
  material.onBeforeCompile = (shader) => {
1304
+ shader.uniforms.transformTexture = uniforms.transformTexture;
1305
+ shader.uniforms.transformTextureSize = uniforms.transformTextureSize;
1306
+
1307
+ // 1. Common Definitions
1173
1308
  shader.vertexShader = shader.vertexShader.replace(
1174
1309
  "#include <common>",
1175
1310
  `
1176
1311
  #include <common>
1312
+
1177
1313
  attribute float visibility;
1314
+ attribute float objectId;
1178
1315
  varying float vVisibility;
1316
+ uniform highp sampler2D transformTexture;
1317
+ uniform float transformTextureSize;
1318
+
1319
+ mat4 getTransformMatrix(float instanceId) {
1320
+ int size = int(transformTextureSize);
1321
+ int index = int(instanceId) * 4;
1322
+
1323
+ int x0 = index % size;
1324
+ int y0 = index / size;
1325
+
1326
+ vec4 row0 = texelFetch(transformTexture, ivec2(x0, y0), 0);
1327
+ vec4 row1 = texelFetch(transformTexture, ivec2(x0 + 1, y0), 0);
1328
+ vec4 row2 = texelFetch(transformTexture, ivec2(x0 + 2, y0), 0);
1329
+ vec4 row3 = texelFetch(transformTexture, ivec2(x0 + 3, y0), 0);
1330
+
1331
+ return mat4(row0, row1, row2, row3);
1332
+ }
1179
1333
  `
1180
1334
  );
1181
1335
 
1182
- shader.fragmentShader = shader.fragmentShader.replace(
1183
- "#include <common>",
1336
+ // 2. Inject matrix retrieval at start of main()
1337
+ shader.vertexShader = shader.vertexShader.replace(
1338
+ "void main() {",
1184
1339
  `
1185
- #include <common>
1186
- varying float vVisibility;
1340
+ void main() {
1341
+ mat4 batchingMatrix = getTransformMatrix(objectId);
1342
+ vVisibility = visibility;
1187
1343
  `
1188
1344
  );
1189
1345
 
1346
+ // 3. Transform Normal
1347
+ if (shader.vertexShader.includes("#include <beginnormal_vertex>")) {
1348
+ shader.vertexShader = shader.vertexShader.replace(
1349
+ "#include <beginnormal_vertex>",
1350
+ `
1351
+ vec3 objectNormal = vec3( normal );
1352
+ mat3 bm = mat3( batchingMatrix );
1353
+ objectNormal = bm * objectNormal;
1354
+ `
1355
+ );
1356
+ }
1357
+
1358
+ // 4. Transform Position
1190
1359
  shader.vertexShader = shader.vertexShader.replace(
1191
- "void main() {",
1360
+ "#include <begin_vertex>",
1192
1361
  `
1193
- void main() {
1194
- vVisibility = visibility;
1362
+ vec3 transformed = vec3( position );
1363
+ transformed = ( batchingMatrix * vec4( transformed, 1.0 ) ).xyz;
1195
1364
  `
1196
1365
  );
1197
1366
 
1198
- shader.fragmentShader = shader.fragmentShader.replace(
1199
- "void main() {",
1367
+ // 5. Fragment Shader
1368
+ shader.fragmentShader = shader.fragmentShader
1369
+ .replace(
1370
+ "#include <common>",
1371
+ `
1372
+ #include <common>
1373
+ varying float vVisibility;
1200
1374
  `
1375
+ )
1376
+ .replace(
1377
+ "void main() {",
1378
+ `
1201
1379
  void main() {
1202
1380
  if (vVisibility < 0.5) discard;
1203
1381
  `
1204
- );
1382
+ );
1383
+
1384
+ //console.log("!vertex", shader.vertexShader);
1205
1385
  };
1206
- material.needsUpdate = true;
1207
1386
 
1387
+ // Ensure the material recompiles to pick up changes
1388
+ material.needsUpdate = true;
1208
1389
  return material;
1209
1390
  }
1210
1391
 
@@ -1320,6 +1501,7 @@ export class DynamicGltfLoader {
1320
1501
 
1321
1502
  this.objectTransforms.clear();
1322
1503
  this.transformedGeometries.clear();
1504
+ this._originalObjectMatrices.clear();
1323
1505
 
1324
1506
  this.totalLoadedObjects = 0;
1325
1507
  this.currentMemoryUsage = 0;
@@ -1332,6 +1514,8 @@ export class DynamicGltfLoader {
1332
1514
  this.objectIdToIndex.clear();
1333
1515
  this.maxObjectId = 0;
1334
1516
  this.objectVisibility = new Float32Array();
1517
+ this.meshToNodeMap = null;
1518
+ this.visibilityMaterials.clear();
1335
1519
  }
1336
1520
 
1337
1521
  setStructureTransform(structureId, matrix) {
@@ -1466,6 +1650,10 @@ export class DynamicGltfLoader {
1466
1650
 
1467
1651
  this.originalObjects.clear();
1468
1652
  this.originalObjectsToSelection.clear();
1653
+ // Clear previous optimization data
1654
+ this.objectIdToIndex.clear();
1655
+ this.maxObjectId = 0;
1656
+
1469
1657
  const structureGroups = new Map();
1470
1658
 
1471
1659
  this.dispatchEvent("optimizationprogress", {
@@ -1474,6 +1662,8 @@ export class DynamicGltfLoader {
1474
1662
  message: "Collecting scene objects...",
1475
1663
  });
1476
1664
 
1665
+ let totalObjectsToMerge = 0;
1666
+
1477
1667
  this.scene.traverse((object) => {
1478
1668
  if (object.userData.structureId) {
1479
1669
  const structureId = object.userData.structureId;
@@ -1493,19 +1683,41 @@ export class DynamicGltfLoader {
1493
1683
  }
1494
1684
 
1495
1685
  const group = structureGroups.get(structureId);
1686
+ let added = false;
1496
1687
 
1497
1688
  if (object instanceof Mesh) {
1498
1689
  this.addToMaterialGroup(object, group.mapMeshes, group.meshes);
1690
+ added = true;
1499
1691
  } else if (object instanceof LineSegments) {
1500
1692
  this.addToMaterialGroup(object, group.mapLineSegments, group.lineSegments);
1693
+ added = true;
1501
1694
  } else if (object instanceof Line) {
1502
1695
  this.addToMaterialGroup(object, group.mapLines, group.lines);
1696
+ added = true;
1503
1697
  } else if (object instanceof Points) {
1504
1698
  this.addToMaterialGroup(object, group.mapPoints, group.points);
1699
+ added = true;
1700
+ }
1701
+
1702
+ if (added) {
1703
+ totalObjectsToMerge++;
1505
1704
  }
1506
1705
  }
1507
1706
  });
1508
1707
 
1708
+ // Initialize transform texture and visibility arrays BEFORE merging
1709
+ // This ensures that as we create merged objects, the texture is large enough
1710
+ // and populated with identity matrices, so objects don't disappear (scale 0).
1711
+ if (totalObjectsToMerge > 0) {
1712
+ console.log(`Pre-allocating transform texture for ${totalObjectsToMerge} objects`);
1713
+ this.maxObjectId = totalObjectsToMerge;
1714
+ this.initTransformTexture();
1715
+ this.initializeObjectVisibility();
1716
+
1717
+ // Reset counter so IDs are assigned from 0 during merge
1718
+ this.maxObjectId = 0;
1719
+ }
1720
+
1509
1721
  let processedGroups = 0;
1510
1722
  const totalGroups = structureGroups.size;
1511
1723
 
@@ -1563,7 +1775,7 @@ export class DynamicGltfLoader {
1563
1775
  }
1564
1776
  });
1565
1777
 
1566
- this.initializeObjectVisibility();
1778
+ // Texture and visibility initialized at start of optimization
1567
1779
 
1568
1780
  console.log(`Optimization complete. Total objects: ${this.maxObjectId}`);
1569
1781
 
@@ -1658,6 +1870,7 @@ export class DynamicGltfLoader {
1658
1870
  const visibilityMaterial = this.createVisibilityMaterial(group.material);
1659
1871
 
1660
1872
  const mergedMesh = new Mesh(mergedGeometry, visibilityMaterial);
1873
+ mergedMesh.frustumCulled = false; // Disable culling because vertex shader moves objects
1661
1874
  mergedMesh.userData.isOptimized = true;
1662
1875
  rootGroup.add(mergedMesh);
1663
1876
 
@@ -1804,6 +2017,7 @@ export class DynamicGltfLoader {
1804
2017
  const visibilityMaterial = this.createVisibilityMaterial(group.material);
1805
2018
 
1806
2019
  const mergedLine = new LineSegments(geometry, visibilityMaterial);
2020
+ mergedLine.frustumCulled = false;
1807
2021
  mergedLine.userData.isEdge = isEdge;
1808
2022
  mergedLine.userData.isOptimized = true;
1809
2023
 
@@ -1915,6 +2129,7 @@ export class DynamicGltfLoader {
1915
2129
  const visibilityMaterial = this.createVisibilityMaterial(group.material);
1916
2130
 
1917
2131
  const mergedLine = new LineSegments(mergedGeometry, visibilityMaterial);
2132
+ mergedLine.frustumCulled = false;
1918
2133
  mergedLine.userData.isEdge = isEdge;
1919
2134
  mergedLine.userData.isOptimized = true;
1920
2135
 
@@ -2001,7 +2216,33 @@ export class DynamicGltfLoader {
2001
2216
 
2002
2217
  if (geometries.length > 0) {
2003
2218
  const mergedGeometry = mergeGeometries(geometries, false);
2004
- const mergedPoints = new Points(mergedGeometry, group.material);
2219
+
2220
+ // Add objectId attribute
2221
+ const totalVertices = mergedGeometry.attributes.position.count;
2222
+ const objectIds = new Float32Array(totalVertices);
2223
+ let vertexOffset = 0;
2224
+
2225
+ group.objects.forEach((points) => {
2226
+ const handle = points.userData.handle;
2227
+ if (!this.objectIdToIndex.has(handle)) {
2228
+ this.objectIdToIndex.set(handle, this.maxObjectId++);
2229
+ }
2230
+ const objectId = this.objectIdToIndex.get(handle);
2231
+ const count = points.geometry.attributes.position.count;
2232
+ for (let i = 0; i < count; i++) {
2233
+ objectIds[vertexOffset++] = objectId;
2234
+ }
2235
+ });
2236
+ mergedGeometry.setAttribute("objectId", new BufferAttribute(objectIds, 1));
2237
+
2238
+ // Add visibility attribute
2239
+ const visibilityArray = new Float32Array(totalVertices);
2240
+ visibilityArray.fill(1.0);
2241
+ mergedGeometry.setAttribute("visibility", new BufferAttribute(visibilityArray, 1));
2242
+
2243
+ const visibilityMaterial = this.createVisibilityMaterial(group.material);
2244
+ const mergedPoints = new Points(mergedGeometry, visibilityMaterial);
2245
+ mergedPoints.frustumCulled = false;
2005
2246
  mergedPoints.userData.isOptimized = true;
2006
2247
 
2007
2248
  if (this.useVAO) {
@@ -2085,15 +2326,41 @@ export class DynamicGltfLoader {
2085
2326
  });
2086
2327
 
2087
2328
  const finalGeometry = mergeGeometries(geometriesWithIndex, false);
2329
+
2330
+ // Add objectId attribute
2331
+ const totalVertices = finalGeometry.attributes.position.count;
2332
+ const objectIds = new Float32Array(totalVertices);
2333
+ let vertexOffset = 0;
2334
+
2335
+ lineSegmentsArray.forEach((segment) => {
2336
+ const handle = segment.userData.handle;
2337
+ if (!this.objectIdToIndex.has(handle)) {
2338
+ this.objectIdToIndex.set(handle, this.maxObjectId++);
2339
+ }
2340
+ const objectId = this.objectIdToIndex.get(handle);
2341
+ const count = segment.geometry.attributes.position.count;
2342
+ for (let i = 0; i < count; i++) {
2343
+ objectIds[vertexOffset++] = objectId;
2344
+ }
2345
+ });
2346
+ finalGeometry.setAttribute("objectId", new BufferAttribute(objectIds, 1));
2347
+
2348
+ // Add visibility attribute
2349
+ const visibilityArray = new Float32Array(totalVertices);
2350
+ visibilityArray.fill(1.0);
2351
+ finalGeometry.setAttribute("visibility", new BufferAttribute(visibilityArray, 1));
2352
+
2088
2353
  const material = new LineBasicMaterial({
2089
2354
  vertexColors: true,
2090
2355
  });
2356
+ const visibilityMaterial = this.createVisibilityMaterial(material);
2091
2357
 
2092
2358
  if (this.useVAO) {
2093
2359
  this.createVAO(finalGeometry);
2094
2360
  }
2095
2361
 
2096
- const mergedLine = new LineSegments(finalGeometry, material);
2362
+ const mergedLine = new LineSegments(finalGeometry, visibilityMaterial);
2363
+ mergedLine.frustumCulled = false;
2097
2364
  mergedLine.userData.structureId = structureId;
2098
2365
  mergedLine.userData.isOptimized = true;
2099
2366
  rootGroup.add(mergedLine);
@@ -2238,223 +2505,269 @@ export class DynamicGltfLoader {
2238
2505
  return;
2239
2506
  }
2240
2507
 
2241
- // Store transform map directly
2242
- this.objectTransforms = new Map(objectTransformMap);
2243
-
2244
- // Apply transforms to all merged meshes
2245
- for (const mesh of this.mergedMesh) {
2246
- this._applyTransformToMergedObject(mesh);
2247
- }
2248
- for (const line of this.mergedLines) {
2249
- this._applyTransformToMergedObject(line);
2250
- }
2251
- for (const lineSegment of this.mergedLineSegments) {
2252
- this._applyTransformToMergedObject(lineSegment);
2253
- }
2254
- for (const point of this.mergedPoints) {
2255
- this._applyTransformToMergedObject(point);
2256
- }
2257
- }
2258
-
2259
- createExplodeTransforms(objects = null, explodeCenter = null, explodeFactor = 1.5) {
2260
- const transformMap = new Map();
2261
-
2262
- if (!explodeCenter) {
2263
- explodeCenter = new Vector3();
2264
- const extent = this.getTotalGeometryExtent();
2265
- if (!extent.isEmpty()) {
2266
- extent.getCenter(explodeCenter);
2267
- }
2508
+ if (!this.transformData) {
2509
+ console.warn("Transform texture not initialized");
2510
+ return;
2268
2511
  }
2269
2512
 
2270
- const objectsArray = objects
2271
- ? Array.isArray(objects)
2272
- ? objects
2273
- : Array.from(objects)
2274
- : Array.from(this.originalObjects);
2513
+ // Store transform map directly
2514
+ this.objectTransforms = objectTransformMap;
2275
2515
 
2276
- for (const obj of objectsArray) {
2277
- if (!obj.geometry || !obj.geometry.attributes.position) continue;
2516
+ // Reset to identity first to ensure clean state
2517
+ this._resetTransformData(false);
2278
2518
 
2279
- const boundingBox = new Box3().setFromBufferAttribute(obj.geometry.attributes.position);
2519
+ // Cache references for tight loop
2520
+ const transformData = this.transformData;
2521
+ const objectIdToIndex = this.objectIdToIndex;
2280
2522
 
2281
- if (obj.matrixWorld) {
2282
- boundingBox.applyMatrix4(obj.matrixWorld);
2283
- }
2523
+ // Fast track map iteration using an array of values if we can
2524
+ // While .entries() is fast, sometimes direct properties check is faster
2525
+ let textureNeedsUpdate = false;
2284
2526
 
2285
- if (boundingBox.isEmpty()) continue;
2527
+ // Process matrices directly into texture array
2528
+ if (objectTransformMap instanceof Map) {
2529
+ // Modern V8 engines optimize for...of on Maps better than Array.from or destructuring iterators
2530
+ for (const [object, matrix] of objectTransformMap.entries()) {
2531
+ const userData = object.userData;
2532
+ if (!userData) continue;
2286
2533
 
2287
- const objectCenter = new Vector3();
2288
- boundingBox.getCenter(objectCenter);
2534
+ const handle = userData.handle;
2535
+ if (handle === undefined) continue;
2289
2536
 
2290
- const direction = objectCenter.clone().sub(explodeCenter);
2291
- const distance = direction.length();
2537
+ const objectId = objectIdToIndex.get(handle);
2538
+ if (objectId !== undefined) {
2539
+ // TypedArray.set is highly optimized in modern JS engines
2540
+ transformData.set(matrix.elements, objectId * 16);
2541
+ textureNeedsUpdate = true;
2542
+ }
2543
+ }
2544
+ } else {
2545
+ // Fallback for arrays of [object, matrix] pairs
2546
+ const len = objectTransformMap.length;
2547
+ for (let i = 0; i < len; i++) {
2548
+ const pair = objectTransformMap[i];
2549
+ const userData = pair[0].userData;
2550
+ if (!userData) continue;
2292
2551
 
2293
- if (distance > 0) {
2294
- direction.normalize();
2295
- const offset = direction.multiplyScalar(distance * (explodeFactor - 1.0));
2552
+ const handle = userData.handle;
2553
+ if (handle === undefined) continue;
2296
2554
 
2297
- const matrix = new Matrix4().makeTranslation(offset.x, offset.y, offset.z);
2298
- transformMap.set(obj, matrix);
2555
+ const objectId = objectIdToIndex.get(handle);
2556
+ if (objectId !== undefined) {
2557
+ transformData.set(pair[1].elements, objectId * 16);
2558
+ textureNeedsUpdate = true;
2559
+ }
2299
2560
  }
2300
2561
  }
2301
2562
 
2302
- return transformMap;
2303
- }
2304
-
2305
- clearTransforms() {
2306
- this.objectTransforms.clear();
2307
-
2308
- for (const mesh of this.mergedMesh) {
2309
- this._restoreOriginalGeometry(mesh);
2310
- }
2311
- for (const line of this.mergedLines) {
2312
- this._restoreOriginalGeometry(line);
2313
- }
2314
- for (const lineSegment of this.mergedLineSegments) {
2315
- this._restoreOriginalGeometry(lineSegment);
2316
- }
2317
- for (const point of this.mergedPoints) {
2318
- this._restoreOriginalGeometry(point);
2563
+ if (textureNeedsUpdate) {
2564
+ this.updateTransformTexture();
2565
+ if (
2566
+ this._lastTransformTexture !== this.transformTexture ||
2567
+ this._lastTransformTextureSize !== this.transformTextureSize
2568
+ ) {
2569
+ this.updateMaterialUniforms();
2570
+ }
2319
2571
  }
2320
- }
2321
2572
 
2322
- clearHandleTransforms() {
2323
- this.clearTransforms();
2573
+ // TODO: Remove this block when highlight is implemented via shader.
2574
+ // Sync transforms to original (hidden) objects so that
2575
+ // GeometryHighlighter wireframes follow exploded positions.
2576
+ if (this.syncTransformsToOriginalObjects) {
2577
+ this._syncOriginalObjectTransforms(objectTransformMap);
2578
+ }
2324
2579
  }
2325
2580
 
2326
- _applyTransformToMergedObject(mergedObject) {
2327
- const objectData = this.mergedObjectMap.get(mergedObject.uuid);
2328
- if (!objectData || !objectData.objectMapping) return;
2329
-
2330
- const geometry = mergedObject.geometry;
2331
- if (!geometry || !geometry.attributes.position) return;
2332
-
2333
- const positionAttr = geometry.attributes.position;
2334
- const positions = positionAttr.array;
2335
-
2336
- if (!this.transformedGeometries.has(mergedObject.uuid)) {
2337
- this.transformedGeometries.set(mergedObject.uuid, new Float32Array(positions));
2581
+ // TODO: Remove when highlight is implemented via shader.
2582
+ _syncOriginalObjectTransforms(objectTransformMap) {
2583
+ // First, restore any previously transformed objects back to their saved position
2584
+ for (const [obj, savedPos] of this._originalObjectMatrices) {
2585
+ obj.position.copy(savedPos);
2586
+ if (obj.userData.highlight) {
2587
+ obj.userData.highlight.position.copy(savedPos);
2588
+ }
2338
2589
  }
2590
+ this._originalObjectMatrices.clear();
2339
2591
 
2340
- const originalPositions = this.transformedGeometries.get(mergedObject.uuid);
2341
- const tempVector = new Vector3();
2592
+ // The transform matrix is in the structure root's local space.
2593
+ // Original objects may be nested inside groups with their own transforms,
2594
+ // so we must convert the offset from structure-root-local → parent-local space.
2595
+ const _offset = new Vector3();
2596
+ const _parentInverse = new Matrix4();
2342
2597
 
2343
- for (const [originalMesh, mappingData] of objectData.objectMapping) {
2344
- const transform = this.objectTransforms.get(originalMesh);
2598
+ if (objectTransformMap instanceof Map) {
2599
+ for (const [object, matrix] of objectTransformMap.entries()) {
2600
+ if (!object.userData?.handle) continue;
2345
2601
 
2346
- if (!transform) {
2347
- const startIdx = mappingData.startVertexIndex * 3;
2348
- const endIdx = (mappingData.startVertexIndex + mappingData.vertexCount) * 3;
2349
- for (let i = startIdx; i < endIdx; i++) {
2350
- positions[i] = originalPositions[i];
2602
+ // Save original position before modifying
2603
+ if (!this._originalObjectMatrices.has(object)) {
2604
+ this._originalObjectMatrices.set(object, object.position.clone());
2351
2605
  }
2352
- continue;
2353
- }
2354
2606
 
2355
- const startVertex = mappingData.startVertexIndex;
2356
- const vertexCount = mappingData.vertexCount;
2607
+ // Extract translation from the transform matrix (structure-root-local space)
2608
+ _offset.setFromMatrixPosition(matrix);
2357
2609
 
2358
- for (let i = 0; i < vertexCount; i++) {
2359
- const idx = (startVertex + i) * 3;
2610
+ // Convert offset from structure-root-local to world space,
2611
+ // then from world space to parent-local space.
2612
+ // The structure root's matrixWorld converts local→world.
2613
+ // The parent's inverse matrixWorld converts world→parent-local.
2614
+ // But we only need to transform a direction/offset (not a point),
2615
+ // so we transform two points and subtract.
2616
+ if (object.userData.structureId) {
2617
+ const rootGroup = this.structureRoots.get(object.userData.structureId);
2618
+ if (rootGroup && object.parent && object.parent !== rootGroup) {
2619
+ // Transform offset: structureRoot-local → world → parent-local
2620
+ // For a vector (not point): apply rotation/scale only
2621
+ const origin = new Vector3(0, 0, 0);
2622
+ origin.applyMatrix4(rootGroup.matrixWorld);
2623
+ _offset.applyMatrix4(rootGroup.matrixWorld);
2624
+ _offset.sub(origin);
2360
2625
 
2361
- tempVector.set(originalPositions[idx], originalPositions[idx + 1], originalPositions[idx + 2]);
2626
+ const parentOrigin = new Vector3(0, 0, 0);
2627
+ _parentInverse.copy(object.parent.matrixWorld).invert();
2628
+ parentOrigin.applyMatrix4(_parentInverse);
2629
+ _offset.applyMatrix4(_parentInverse);
2630
+ _offset.sub(parentOrigin);
2631
+ }
2632
+ }
2362
2633
 
2363
- tempVector.applyMatrix4(transform);
2634
+ object.position.add(_offset);
2364
2635
 
2365
- positions[idx] = tempVector.x;
2366
- positions[idx + 1] = tempVector.y;
2367
- positions[idx + 2] = tempVector.z;
2636
+ // Also update highlight wireframe if it exists right now
2637
+ if (object.userData.highlight) {
2638
+ object.userData.highlight.position.copy(object.position);
2639
+ }
2368
2640
  }
2369
2641
  }
2642
+ }
2370
2643
 
2371
- if (geometry.attributes.normal) {
2372
- this._updateNormalsForTransform(geometry, objectData, originalPositions);
2373
- }
2644
+ createExplodeTransforms(objects = null, explodeCenter = null, explodeFactor = 1.5) {
2645
+ const transformMap = new Map();
2374
2646
 
2375
- positionAttr.needsUpdate = true;
2376
- geometry.computeBoundingSphere();
2377
- geometry.computeBoundingBox();
2378
- }
2647
+ if (!explodeCenter) {
2648
+ explodeCenter = new Vector3();
2649
+ const extent = this.getTotalGeometryExtent();
2650
+ if (!extent.isEmpty()) {
2651
+ extent.getCenter(explodeCenter);
2652
+ }
2653
+ }
2379
2654
 
2380
- _updateNormalsForTransform(geometry, objectData, originalPositions) {
2381
- const normalAttr = geometry.attributes.normal;
2382
- if (!normalAttr) return;
2655
+ const objectsArray = objects
2656
+ ? Array.isArray(objects)
2657
+ ? objects
2658
+ : Array.from(objects)
2659
+ : Array.from(this.originalObjects);
2383
2660
 
2384
- const normals = normalAttr.array;
2385
- const tempVector = new Vector3();
2386
- const normalMatrix = new Matrix4();
2661
+ // Cache inverse matrices for structures
2662
+ const structureInverseMatrices = new Map();
2387
2663
 
2388
- // Store original normals if not already stored
2389
- const normalsKey = `${geometry.uuid}_normals`;
2390
- if (!this.transformedGeometries.has(normalsKey)) {
2391
- this.transformedGeometries.set(normalsKey, new Float32Array(normals));
2664
+ // Map mesh -> node to access cached geometryExtents
2665
+ if (!this.meshToNodeMap) {
2666
+ this.meshToNodeMap = new Map();
2667
+ for (const node of this.nodes.values()) {
2668
+ if (node.object) {
2669
+ this.meshToNodeMap.set(node.object, node);
2670
+ }
2671
+ }
2392
2672
  }
2393
2673
 
2394
- const originalNormals = this.transformedGeometries.get(normalsKey);
2674
+ for (const obj of objectsArray) {
2675
+ if (!obj.geometry || !obj.geometry.attributes.position) continue;
2395
2676
 
2396
- for (const [originalMesh, mappingData] of objectData.objectMapping) {
2397
- // Direct lookup by object reference - NO HANDLE LOOKUP!
2398
- const transform = this.objectTransforms.get(originalMesh);
2677
+ // OPTIMIZATION: Use cached node extent if available
2678
+ if (!obj.userData.explodeVector) {
2679
+ let center = null;
2399
2680
 
2400
- if (!transform) {
2401
- // Restore original normals
2402
- const startIdx = mappingData.startVertexIndex * 3;
2403
- const endIdx = (mappingData.startVertexIndex + mappingData.vertexCount) * 3;
2404
- for (let i = startIdx; i < endIdx; i++) {
2405
- normals[i] = originalNormals[i];
2681
+ // 1. Try to get extent from node (fastest, pre-calculated)
2682
+ const node = this.meshToNodeMap.get(obj);
2683
+ if (node && node.geometryExtents) {
2684
+ const box = node.geometryExtents.clone();
2685
+ box.applyMatrix4(obj.matrixWorld);
2686
+ center = new Vector3();
2687
+ box.getCenter(center);
2406
2688
  }
2407
- continue;
2408
- }
2409
2689
 
2410
- // Create normal matrix (inverse transpose of transform)
2411
- normalMatrix.copy(transform).invert().transpose();
2690
+ // 2. Fallback to geometry bounding box
2691
+ if (!center) {
2692
+ if (!obj.geometry.boundingBox) obj.geometry.computeBoundingBox();
2693
+ const box = obj.geometry.boundingBox.clone();
2694
+ box.applyMatrix4(obj.matrixWorld);
2695
+ center = new Vector3();
2696
+ box.getCenter(center);
2697
+ }
2412
2698
 
2413
- const startVertex = mappingData.startVertexIndex;
2414
- const vertexCount = mappingData.vertexCount;
2699
+ // Calculate vector from explode center to object center
2700
+ const explodeVector = center.sub(explodeCenter);
2415
2701
 
2416
- for (let i = 0; i < vertexCount; i++) {
2417
- const idx = (startVertex + i) * 3;
2702
+ // Cache it
2703
+ obj.userData.explodeVector = explodeVector;
2704
+ }
2705
+ const explodeVector = obj.userData.explodeVector;
2706
+ const distance = explodeVector.length();
2418
2707
 
2419
- // Get original normal
2420
- tempVector.set(originalNormals[idx], originalNormals[idx + 1], originalNormals[idx + 2]);
2708
+ if (distance > 0) {
2709
+ // Calculate offset in World Space
2710
+ const offset = explodeVector.clone().multiplyScalar(explodeFactor - 1.0);
2711
+
2712
+ // Convert offset from World Space to Local Space of the merged mesh
2713
+ const localOffset = offset.clone();
2714
+
2715
+ if (obj.userData.structureId) {
2716
+ const structureId = obj.userData.structureId;
2717
+ let inverseMatrix = structureInverseMatrices.get(structureId);
2718
+
2719
+ if (!inverseMatrix) {
2720
+ const rootGroup = this.structureRoots.get(structureId);
2721
+ if (rootGroup) {
2722
+ // Reuse cached inverse matrix if possible
2723
+ if (!rootGroup.userData.inverseWorldMatrix) {
2724
+ // rootGroup.updateMatrixWorld(true); // Trust current state
2725
+ rootGroup.userData.inverseWorldMatrix = new Matrix4().copy(rootGroup.matrixWorld).invert();
2726
+ }
2727
+ inverseMatrix = rootGroup.userData.inverseWorldMatrix;
2728
+ structureInverseMatrices.set(structureId, inverseMatrix);
2729
+ }
2730
+ }
2421
2731
 
2422
- // Apply normal transformation
2423
- tempVector.applyMatrix4(normalMatrix).normalize();
2732
+ if (inverseMatrix) {
2733
+ const zero = new Vector3(0, 0, 0).applyMatrix4(inverseMatrix);
2734
+ const vec = offset.clone().applyMatrix4(inverseMatrix).sub(zero);
2735
+ localOffset.copy(vec);
2736
+ }
2737
+ }
2424
2738
 
2425
- // Write back transformed normal
2426
- normals[idx] = tempVector.x;
2427
- normals[idx + 1] = tempVector.y;
2428
- normals[idx + 2] = tempVector.z;
2739
+ let matrix = obj.userData.explodeMatrix;
2740
+ if (!matrix) {
2741
+ matrix = new Matrix4();
2742
+ obj.userData.explodeMatrix = matrix;
2743
+ }
2744
+ matrix.makeTranslation(localOffset.x, localOffset.y, localOffset.z);
2745
+ transformMap.set(obj, matrix);
2429
2746
  }
2430
2747
  }
2431
2748
 
2432
- normalAttr.needsUpdate = true;
2749
+ return transformMap;
2433
2750
  }
2434
2751
 
2435
- _restoreOriginalGeometry(mergedObject) {
2436
- const geometry = mergedObject.geometry;
2437
- if (!geometry || !geometry.attributes.position) return;
2438
-
2439
- // Restore original positions
2440
- const originalPositions = this.transformedGeometries.get(mergedObject.uuid);
2441
- if (originalPositions) {
2442
- const positions = geometry.attributes.position.array;
2443
- positions.set(originalPositions);
2444
- geometry.attributes.position.needsUpdate = true;
2445
- }
2752
+ clearTransforms() {
2753
+ this.objectTransforms.clear();
2754
+ this._resetTransformData(true);
2446
2755
 
2447
- // Restore original normals
2448
- const normalsKey = `${geometry.uuid}_normals`;
2449
- const originalNormals = this.transformedGeometries.get(normalsKey);
2450
- if (originalNormals && geometry.attributes.normal) {
2451
- const normals = geometry.attributes.normal.array;
2452
- normals.set(originalNormals);
2453
- geometry.attributes.normal.needsUpdate = true;
2756
+ // TODO: Remove when highlight is implemented via shader.
2757
+ // Restore original object positions
2758
+ if (this.syncTransformsToOriginalObjects) {
2759
+ for (const [obj, savedPos] of this._originalObjectMatrices) {
2760
+ obj.position.copy(savedPos);
2761
+ if (obj.userData.highlight) {
2762
+ obj.userData.highlight.position.copy(savedPos);
2763
+ }
2764
+ }
2765
+ this._originalObjectMatrices.clear();
2454
2766
  }
2767
+ }
2455
2768
 
2456
- geometry.computeBoundingSphere();
2457
- geometry.computeBoundingBox();
2769
+ clearHandleTransforms() {
2770
+ this.clearTransforms();
2458
2771
  }
2459
2772
 
2460
2773
  syncHiddenObjects() {