@inweb/viewer-three 27.2.1 → 27.2.3

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();
@@ -126,6 +126,123 @@ export class DynamicGltfLoader {
126
126
  this.mergedGeometryVisibility = new Map(); // mergedObject -> visibility array
127
127
 
128
128
  this._webglInfoCache = null;
129
+
130
+ // Transform texture support
131
+ this.transformTextureSize = 1024;
132
+ this.transformTexture = this.createDummyTexture();
133
+ this.transformData = null;
134
+ this.identityTransformData = null;
135
+ this.visibilityMaterials = new Set(); // Keep track of materials to update uniforms
136
+ }
137
+
138
+ // layout (1 matrix = 4 pixels)
139
+ // RGBA RGBA RGBA RGBA (=> column1, column2, column3, column4)
140
+ // with 8x8 pixel texture max 16 matrices * 4 pixels = (8 * 8)
141
+ // 16x16 pixel texture max 64 matrices * 4 pixels = (16 * 16)
142
+ // 32x32 pixel texture max 256 matrices * 4 pixels = (32 * 32)
143
+ // 64x64 pixel texture max 1024 matrices * 4 pixels = (64 * 64)
144
+
145
+ createDummyTexture() {
146
+ // Create 1x1 dummy texture with identity matrix to prevent shader crash/black screen
147
+ const data = new Float32Array(16); // 4x4 matrix
148
+ const identity = new Matrix4();
149
+ identity.toArray(data);
150
+
151
+ // Correct dummy size: 4x1 to hold at least one matrix
152
+ const dummyData = new Float32Array(16);
153
+ identity.toArray(dummyData);
154
+ const dummyTexture = new DataTexture(dummyData, 4, 1, RGBAFormat, FloatType);
155
+
156
+ dummyTexture.minFilter = NearestFilter;
157
+ dummyTexture.magFilter = NearestFilter;
158
+ dummyTexture.needsUpdate = true;
159
+ return dummyTexture;
160
+ }
161
+
162
+ initTransformTexture() {
163
+ if (this.transformTexture) {
164
+ this.transformTexture.dispose();
165
+ }
166
+
167
+ // Logic from BatchedMesh.js _initMatricesTexture
168
+ // layout (1 matrix = 4 pixels)
169
+ // RGBA RGBA RGBA RGBA (=> column1, column2, column3, column4)
170
+ const maxInstanceCount = this.maxObjectId + 1;
171
+ let size = Math.sqrt(maxInstanceCount * 4); // 4 pixels needed for 1 matrix
172
+ size = Math.ceil(size / 4) * 4;
173
+ size = Math.max(size, 4);
174
+
175
+ this.transformTextureSize = size;
176
+ const arraySize = size * size * 4; // 4 floats per RGBA pixel
177
+ this.transformData = new Float32Array(arraySize);
178
+
179
+ // Create and cache identity matrices for fast reset
180
+ this.identityTransformData = new Float32Array(arraySize);
181
+ for (let i = 0; i <= this.maxObjectId; i++) {
182
+ const base = i * 16;
183
+ if (base + 15 < arraySize) {
184
+ this.identityTransformData[base + 0] = 1; // m00
185
+ this.identityTransformData[base + 5] = 1; // m11
186
+ this.identityTransformData[base + 10] = 1; // m22
187
+ this.identityTransformData[base + 15] = 1; // m33
188
+ }
189
+ }
190
+
191
+ // Initialize with identity matrices
192
+ this._resetTransformData(false);
193
+
194
+ this.transformTexture = new DataTexture(this.transformData, size, size, RGBAFormat, FloatType);
195
+
196
+ this.transformTexture.needsUpdate = true;
197
+ this.transformTexture.generateMipmaps = false;
198
+
199
+ console.log(`Initialized transform texture: ${size}x${size} for ${maxInstanceCount} objects`);
200
+
201
+ this.updateMaterialUniforms();
202
+
203
+ // Force all visibility materials to update
204
+ this.visibilityMaterials.forEach((material) => {
205
+ material.needsUpdate = true;
206
+ });
207
+ }
208
+
209
+ // Fast reset to Identity matrices without creating Matrix4 objects
210
+ _resetTransformData(updateTexture = true) {
211
+ if (!this.transformData || !this.identityTransformData) return;
212
+
213
+ // Fast copy from cached identity array
214
+ this.transformData.set(this.identityTransformData);
215
+
216
+ if (updateTexture) {
217
+ this.updateTransformTexture();
218
+ }
219
+ }
220
+
221
+ updateMaterialUniforms() {
222
+ // Only update if the texture actually changed
223
+ // In three.js, setting `.value = this.transformTexture` triggers uniformity checks
224
+ // We can avoid looping over all materials if the texture reference hasn't changed
225
+ if (
226
+ this._lastTransformTexture === this.transformTexture &&
227
+ this._lastTransformTextureSize === this.transformTextureSize
228
+ ) {
229
+ return;
230
+ }
231
+
232
+ this._lastTransformTexture = this.transformTexture;
233
+ this._lastTransformTextureSize = this.transformTextureSize;
234
+
235
+ this.visibilityMaterials.forEach((material) => {
236
+ if (material.userData && material.userData.visibilityUniforms) {
237
+ material.userData.visibilityUniforms.transformTexture.value = this.transformTexture;
238
+ material.userData.visibilityUniforms.transformTextureSize.value = this.transformTextureSize;
239
+ }
240
+ });
241
+ }
242
+
243
+ updateTransformTexture() {
244
+ if (!this.transformTexture) return;
245
+ this.transformTexture.needsUpdate = true;
129
246
  }
130
247
 
131
248
  setVisibleEdges(visible) {
@@ -1169,42 +1286,100 @@ export class DynamicGltfLoader {
1169
1286
  }
1170
1287
 
1171
1288
  createVisibilityMaterial(material) {
1289
+ this.visibilityMaterials.add(material);
1290
+
1291
+ const uniforms = {
1292
+ transformTexture: { value: this.transformTexture },
1293
+ transformTextureSize: { value: this.transformTextureSize },
1294
+ };
1295
+ material.userData.visibilityUniforms = uniforms;
1296
+
1172
1297
  material.onBeforeCompile = (shader) => {
1298
+ shader.uniforms.transformTexture = uniforms.transformTexture;
1299
+ shader.uniforms.transformTextureSize = uniforms.transformTextureSize;
1300
+
1301
+ // 1. Common Definitions
1173
1302
  shader.vertexShader = shader.vertexShader.replace(
1174
1303
  "#include <common>",
1175
1304
  `
1176
1305
  #include <common>
1306
+
1177
1307
  attribute float visibility;
1308
+ attribute float objectId;
1178
1309
  varying float vVisibility;
1310
+ uniform highp sampler2D transformTexture;
1311
+ uniform float transformTextureSize;
1312
+
1313
+ mat4 getTransformMatrix(float instanceId) {
1314
+ int size = int(transformTextureSize);
1315
+ int index = int(instanceId) * 4;
1316
+
1317
+ int x0 = index % size;
1318
+ int y0 = index / size;
1319
+
1320
+ vec4 row0 = texelFetch(transformTexture, ivec2(x0, y0), 0);
1321
+ vec4 row1 = texelFetch(transformTexture, ivec2(x0 + 1, y0), 0);
1322
+ vec4 row2 = texelFetch(transformTexture, ivec2(x0 + 2, y0), 0);
1323
+ vec4 row3 = texelFetch(transformTexture, ivec2(x0 + 3, y0), 0);
1324
+
1325
+ return mat4(row0, row1, row2, row3);
1326
+ }
1179
1327
  `
1180
1328
  );
1181
1329
 
1182
- shader.fragmentShader = shader.fragmentShader.replace(
1183
- "#include <common>",
1330
+ // 2. Inject matrix retrieval at start of main()
1331
+ shader.vertexShader = shader.vertexShader.replace(
1332
+ "void main() {",
1184
1333
  `
1185
- #include <common>
1186
- varying float vVisibility;
1334
+ void main() {
1335
+ mat4 batchingMatrix = getTransformMatrix(objectId);
1336
+ vVisibility = visibility;
1187
1337
  `
1188
1338
  );
1189
1339
 
1340
+ // 3. Transform Normal
1341
+ if (shader.vertexShader.includes("#include <beginnormal_vertex>")) {
1342
+ shader.vertexShader = shader.vertexShader.replace(
1343
+ "#include <beginnormal_vertex>",
1344
+ `
1345
+ vec3 objectNormal = vec3( normal );
1346
+ mat3 bm = mat3( batchingMatrix );
1347
+ objectNormal = bm * objectNormal;
1348
+ `
1349
+ );
1350
+ }
1351
+
1352
+ // 4. Transform Position
1190
1353
  shader.vertexShader = shader.vertexShader.replace(
1191
- "void main() {",
1354
+ "#include <begin_vertex>",
1192
1355
  `
1193
- void main() {
1194
- vVisibility = visibility;
1356
+ vec3 transformed = vec3( position );
1357
+ transformed = ( batchingMatrix * vec4( transformed, 1.0 ) ).xyz;
1195
1358
  `
1196
1359
  );
1197
1360
 
1198
- shader.fragmentShader = shader.fragmentShader.replace(
1199
- "void main() {",
1361
+ // 5. Fragment Shader
1362
+ shader.fragmentShader = shader.fragmentShader
1363
+ .replace(
1364
+ "#include <common>",
1365
+ `
1366
+ #include <common>
1367
+ varying float vVisibility;
1200
1368
  `
1369
+ )
1370
+ .replace(
1371
+ "void main() {",
1372
+ `
1201
1373
  void main() {
1202
1374
  if (vVisibility < 0.5) discard;
1203
1375
  `
1204
- );
1376
+ );
1377
+
1378
+ //console.log("!vertex", shader.vertexShader);
1205
1379
  };
1206
- material.needsUpdate = true;
1207
1380
 
1381
+ // Ensure the material recompiles to pick up changes
1382
+ material.needsUpdate = true;
1208
1383
  return material;
1209
1384
  }
1210
1385
 
@@ -1332,6 +1507,8 @@ export class DynamicGltfLoader {
1332
1507
  this.objectIdToIndex.clear();
1333
1508
  this.maxObjectId = 0;
1334
1509
  this.objectVisibility = new Float32Array();
1510
+ this.meshToNodeMap = null;
1511
+ this.visibilityMaterials.clear();
1335
1512
  }
1336
1513
 
1337
1514
  setStructureTransform(structureId, matrix) {
@@ -1466,6 +1643,10 @@ export class DynamicGltfLoader {
1466
1643
 
1467
1644
  this.originalObjects.clear();
1468
1645
  this.originalObjectsToSelection.clear();
1646
+ // Clear previous optimization data
1647
+ this.objectIdToIndex.clear();
1648
+ this.maxObjectId = 0;
1649
+
1469
1650
  const structureGroups = new Map();
1470
1651
 
1471
1652
  this.dispatchEvent("optimizationprogress", {
@@ -1474,6 +1655,8 @@ export class DynamicGltfLoader {
1474
1655
  message: "Collecting scene objects...",
1475
1656
  });
1476
1657
 
1658
+ let totalObjectsToMerge = 0;
1659
+
1477
1660
  this.scene.traverse((object) => {
1478
1661
  if (object.userData.structureId) {
1479
1662
  const structureId = object.userData.structureId;
@@ -1493,19 +1676,41 @@ export class DynamicGltfLoader {
1493
1676
  }
1494
1677
 
1495
1678
  const group = structureGroups.get(structureId);
1679
+ let added = false;
1496
1680
 
1497
1681
  if (object instanceof Mesh) {
1498
1682
  this.addToMaterialGroup(object, group.mapMeshes, group.meshes);
1683
+ added = true;
1499
1684
  } else if (object instanceof LineSegments) {
1500
1685
  this.addToMaterialGroup(object, group.mapLineSegments, group.lineSegments);
1686
+ added = true;
1501
1687
  } else if (object instanceof Line) {
1502
1688
  this.addToMaterialGroup(object, group.mapLines, group.lines);
1689
+ added = true;
1503
1690
  } else if (object instanceof Points) {
1504
1691
  this.addToMaterialGroup(object, group.mapPoints, group.points);
1692
+ added = true;
1693
+ }
1694
+
1695
+ if (added) {
1696
+ totalObjectsToMerge++;
1505
1697
  }
1506
1698
  }
1507
1699
  });
1508
1700
 
1701
+ // Initialize transform texture and visibility arrays BEFORE merging
1702
+ // This ensures that as we create merged objects, the texture is large enough
1703
+ // and populated with identity matrices, so objects don't disappear (scale 0).
1704
+ if (totalObjectsToMerge > 0) {
1705
+ console.log(`Pre-allocating transform texture for ${totalObjectsToMerge} objects`);
1706
+ this.maxObjectId = totalObjectsToMerge;
1707
+ this.initTransformTexture();
1708
+ this.initializeObjectVisibility();
1709
+
1710
+ // Reset counter so IDs are assigned from 0 during merge
1711
+ this.maxObjectId = 0;
1712
+ }
1713
+
1509
1714
  let processedGroups = 0;
1510
1715
  const totalGroups = structureGroups.size;
1511
1716
 
@@ -1563,7 +1768,7 @@ export class DynamicGltfLoader {
1563
1768
  }
1564
1769
  });
1565
1770
 
1566
- this.initializeObjectVisibility();
1771
+ // Texture and visibility initialized at start of optimization
1567
1772
 
1568
1773
  console.log(`Optimization complete. Total objects: ${this.maxObjectId}`);
1569
1774
 
@@ -1658,6 +1863,7 @@ export class DynamicGltfLoader {
1658
1863
  const visibilityMaterial = this.createVisibilityMaterial(group.material);
1659
1864
 
1660
1865
  const mergedMesh = new Mesh(mergedGeometry, visibilityMaterial);
1866
+ mergedMesh.frustumCulled = false; // Disable culling because vertex shader moves objects
1661
1867
  mergedMesh.userData.isOptimized = true;
1662
1868
  rootGroup.add(mergedMesh);
1663
1869
 
@@ -1804,6 +2010,7 @@ export class DynamicGltfLoader {
1804
2010
  const visibilityMaterial = this.createVisibilityMaterial(group.material);
1805
2011
 
1806
2012
  const mergedLine = new LineSegments(geometry, visibilityMaterial);
2013
+ mergedLine.frustumCulled = false;
1807
2014
  mergedLine.userData.isEdge = isEdge;
1808
2015
  mergedLine.userData.isOptimized = true;
1809
2016
 
@@ -1915,6 +2122,7 @@ export class DynamicGltfLoader {
1915
2122
  const visibilityMaterial = this.createVisibilityMaterial(group.material);
1916
2123
 
1917
2124
  const mergedLine = new LineSegments(mergedGeometry, visibilityMaterial);
2125
+ mergedLine.frustumCulled = false;
1918
2126
  mergedLine.userData.isEdge = isEdge;
1919
2127
  mergedLine.userData.isOptimized = true;
1920
2128
 
@@ -2001,7 +2209,33 @@ export class DynamicGltfLoader {
2001
2209
 
2002
2210
  if (geometries.length > 0) {
2003
2211
  const mergedGeometry = mergeGeometries(geometries, false);
2004
- const mergedPoints = new Points(mergedGeometry, group.material);
2212
+
2213
+ // Add objectId attribute
2214
+ const totalVertices = mergedGeometry.attributes.position.count;
2215
+ const objectIds = new Float32Array(totalVertices);
2216
+ let vertexOffset = 0;
2217
+
2218
+ group.objects.forEach((points) => {
2219
+ const handle = points.userData.handle;
2220
+ if (!this.objectIdToIndex.has(handle)) {
2221
+ this.objectIdToIndex.set(handle, this.maxObjectId++);
2222
+ }
2223
+ const objectId = this.objectIdToIndex.get(handle);
2224
+ const count = points.geometry.attributes.position.count;
2225
+ for (let i = 0; i < count; i++) {
2226
+ objectIds[vertexOffset++] = objectId;
2227
+ }
2228
+ });
2229
+ mergedGeometry.setAttribute("objectId", new BufferAttribute(objectIds, 1));
2230
+
2231
+ // Add visibility attribute
2232
+ const visibilityArray = new Float32Array(totalVertices);
2233
+ visibilityArray.fill(1.0);
2234
+ mergedGeometry.setAttribute("visibility", new BufferAttribute(visibilityArray, 1));
2235
+
2236
+ const visibilityMaterial = this.createVisibilityMaterial(group.material);
2237
+ const mergedPoints = new Points(mergedGeometry, visibilityMaterial);
2238
+ mergedPoints.frustumCulled = false;
2005
2239
  mergedPoints.userData.isOptimized = true;
2006
2240
 
2007
2241
  if (this.useVAO) {
@@ -2085,15 +2319,41 @@ export class DynamicGltfLoader {
2085
2319
  });
2086
2320
 
2087
2321
  const finalGeometry = mergeGeometries(geometriesWithIndex, false);
2322
+
2323
+ // Add objectId attribute
2324
+ const totalVertices = finalGeometry.attributes.position.count;
2325
+ const objectIds = new Float32Array(totalVertices);
2326
+ let vertexOffset = 0;
2327
+
2328
+ lineSegmentsArray.forEach((segment) => {
2329
+ const handle = segment.userData.handle;
2330
+ if (!this.objectIdToIndex.has(handle)) {
2331
+ this.objectIdToIndex.set(handle, this.maxObjectId++);
2332
+ }
2333
+ const objectId = this.objectIdToIndex.get(handle);
2334
+ const count = segment.geometry.attributes.position.count;
2335
+ for (let i = 0; i < count; i++) {
2336
+ objectIds[vertexOffset++] = objectId;
2337
+ }
2338
+ });
2339
+ finalGeometry.setAttribute("objectId", new BufferAttribute(objectIds, 1));
2340
+
2341
+ // Add visibility attribute
2342
+ const visibilityArray = new Float32Array(totalVertices);
2343
+ visibilityArray.fill(1.0);
2344
+ finalGeometry.setAttribute("visibility", new BufferAttribute(visibilityArray, 1));
2345
+
2088
2346
  const material = new LineBasicMaterial({
2089
2347
  vertexColors: true,
2090
2348
  });
2349
+ const visibilityMaterial = this.createVisibilityMaterial(material);
2091
2350
 
2092
2351
  if (this.useVAO) {
2093
2352
  this.createVAO(finalGeometry);
2094
2353
  }
2095
2354
 
2096
- const mergedLine = new LineSegments(finalGeometry, material);
2355
+ const mergedLine = new LineSegments(finalGeometry, visibilityMaterial);
2356
+ mergedLine.frustumCulled = false;
2097
2357
  mergedLine.userData.structureId = structureId;
2098
2358
  mergedLine.userData.isOptimized = true;
2099
2359
  rootGroup.add(mergedLine);
@@ -2238,21 +2498,69 @@ export class DynamicGltfLoader {
2238
2498
  return;
2239
2499
  }
2240
2500
 
2501
+ if (!this.transformData) {
2502
+ console.warn("Transform texture not initialized");
2503
+ return;
2504
+ }
2505
+
2241
2506
  // Store transform map directly
2242
- this.objectTransforms = new Map(objectTransformMap);
2507
+ this.objectTransforms = objectTransformMap;
2243
2508
 
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);
2509
+ // Reset to identity first to ensure clean state
2510
+ this._resetTransformData(false);
2511
+
2512
+ // Cache references for tight loop
2513
+ const transformData = this.transformData;
2514
+ const objectIdToIndex = this.objectIdToIndex;
2515
+
2516
+ // Fast track map iteration using an array of values if we can
2517
+ // While .entries() is fast, sometimes direct properties check is faster
2518
+ let textureNeedsUpdate = false;
2519
+
2520
+ // Process matrices directly into texture array
2521
+ if (objectTransformMap instanceof Map) {
2522
+ // Modern V8 engines optimize for...of on Maps better than Array.from or destructuring iterators
2523
+ for (const [object, matrix] of objectTransformMap.entries()) {
2524
+ const userData = object.userData;
2525
+ if (!userData) continue;
2526
+
2527
+ const handle = userData.handle;
2528
+ if (handle === undefined) continue;
2529
+
2530
+ const objectId = objectIdToIndex.get(handle);
2531
+ if (objectId !== undefined) {
2532
+ // TypedArray.set is highly optimized in modern JS engines
2533
+ transformData.set(matrix.elements, objectId * 16);
2534
+ textureNeedsUpdate = true;
2535
+ }
2536
+ }
2537
+ } else {
2538
+ // Fallback for arrays of [object, matrix] pairs
2539
+ const len = objectTransformMap.length;
2540
+ for (let i = 0; i < len; i++) {
2541
+ const pair = objectTransformMap[i];
2542
+ const userData = pair[0].userData;
2543
+ if (!userData) continue;
2544
+
2545
+ const handle = userData.handle;
2546
+ if (handle === undefined) continue;
2547
+
2548
+ const objectId = objectIdToIndex.get(handle);
2549
+ if (objectId !== undefined) {
2550
+ transformData.set(pair[1].elements, objectId * 16);
2551
+ textureNeedsUpdate = true;
2552
+ }
2553
+ }
2253
2554
  }
2254
- for (const point of this.mergedPoints) {
2255
- this._applyTransformToMergedObject(point);
2555
+
2556
+ if (textureNeedsUpdate) {
2557
+ this.updateTransformTexture();
2558
+ if (
2559
+ this._lastTransformTexture !== this.transformTexture ||
2560
+ this._lastTransformTextureSize !== this.transformTextureSize
2561
+ ) {
2562
+ this.updateMaterialUniforms();
2563
+ }
2256
2564
  }
2257
2565
  }
2258
2566
 
@@ -2273,28 +2581,90 @@ export class DynamicGltfLoader {
2273
2581
  : Array.from(objects)
2274
2582
  : Array.from(this.originalObjects);
2275
2583
 
2584
+ // Cache inverse matrices for structures
2585
+ const structureInverseMatrices = new Map();
2586
+
2587
+ // Map mesh -> node to access cached geometryExtents
2588
+ if (!this.meshToNodeMap) {
2589
+ this.meshToNodeMap = new Map();
2590
+ for (const node of this.nodes.values()) {
2591
+ if (node.object) {
2592
+ this.meshToNodeMap.set(node.object, node);
2593
+ }
2594
+ }
2595
+ }
2596
+
2276
2597
  for (const obj of objectsArray) {
2277
2598
  if (!obj.geometry || !obj.geometry.attributes.position) continue;
2278
2599
 
2279
- const boundingBox = new Box3().setFromBufferAttribute(obj.geometry.attributes.position);
2600
+ // OPTIMIZATION: Use cached node extent if available
2601
+ if (!obj.userData.explodeVector) {
2602
+ let center = null;
2280
2603
 
2281
- if (obj.matrixWorld) {
2282
- boundingBox.applyMatrix4(obj.matrixWorld);
2283
- }
2604
+ // 1. Try to get extent from node (fastest, pre-calculated)
2605
+ const node = this.meshToNodeMap.get(obj);
2606
+ if (node && node.geometryExtents) {
2607
+ const box = node.geometryExtents.clone();
2608
+ box.applyMatrix4(obj.matrixWorld);
2609
+ center = new Vector3();
2610
+ box.getCenter(center);
2611
+ }
2284
2612
 
2285
- if (boundingBox.isEmpty()) continue;
2613
+ // 2. Fallback to geometry bounding box
2614
+ if (!center) {
2615
+ if (!obj.geometry.boundingBox) obj.geometry.computeBoundingBox();
2616
+ const box = obj.geometry.boundingBox.clone();
2617
+ box.applyMatrix4(obj.matrixWorld);
2618
+ center = new Vector3();
2619
+ box.getCenter(center);
2620
+ }
2286
2621
 
2287
- const objectCenter = new Vector3();
2288
- boundingBox.getCenter(objectCenter);
2622
+ // Calculate vector from explode center to object center
2623
+ const explodeVector = center.sub(explodeCenter);
2289
2624
 
2290
- const direction = objectCenter.clone().sub(explodeCenter);
2291
- const distance = direction.length();
2625
+ // Cache it
2626
+ obj.userData.explodeVector = explodeVector;
2627
+ }
2628
+ const explodeVector = obj.userData.explodeVector;
2629
+ const distance = explodeVector.length();
2292
2630
 
2293
2631
  if (distance > 0) {
2294
- direction.normalize();
2295
- const offset = direction.multiplyScalar(distance * (explodeFactor - 1.0));
2632
+ // Calculate offset in World Space
2633
+ const offset = explodeVector.clone().multiplyScalar(explodeFactor - 1.0);
2634
+
2635
+ // Convert offset from World Space to Local Space of the merged mesh
2636
+ const localOffset = offset.clone();
2637
+
2638
+ if (obj.userData.structureId) {
2639
+ const structureId = obj.userData.structureId;
2640
+ let inverseMatrix = structureInverseMatrices.get(structureId);
2641
+
2642
+ if (!inverseMatrix) {
2643
+ const rootGroup = this.structureRoots.get(structureId);
2644
+ if (rootGroup) {
2645
+ // Reuse cached inverse matrix if possible
2646
+ if (!rootGroup.userData.inverseWorldMatrix) {
2647
+ // rootGroup.updateMatrixWorld(true); // Trust current state
2648
+ rootGroup.userData.inverseWorldMatrix = new Matrix4().copy(rootGroup.matrixWorld).invert();
2649
+ }
2650
+ inverseMatrix = rootGroup.userData.inverseWorldMatrix;
2651
+ structureInverseMatrices.set(structureId, inverseMatrix);
2652
+ }
2653
+ }
2654
+
2655
+ if (inverseMatrix) {
2656
+ const zero = new Vector3(0, 0, 0).applyMatrix4(inverseMatrix);
2657
+ const vec = offset.clone().applyMatrix4(inverseMatrix).sub(zero);
2658
+ localOffset.copy(vec);
2659
+ }
2660
+ }
2296
2661
 
2297
- const matrix = new Matrix4().makeTranslation(offset.x, offset.y, offset.z);
2662
+ let matrix = obj.userData.explodeMatrix;
2663
+ if (!matrix) {
2664
+ matrix = new Matrix4();
2665
+ obj.userData.explodeMatrix = matrix;
2666
+ }
2667
+ matrix.makeTranslation(localOffset.x, localOffset.y, localOffset.z);
2298
2668
  transformMap.set(obj, matrix);
2299
2669
  }
2300
2670
  }
@@ -2304,159 +2674,13 @@ export class DynamicGltfLoader {
2304
2674
 
2305
2675
  clearTransforms() {
2306
2676
  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);
2319
- }
2677
+ this._resetTransformData(true);
2320
2678
  }
2321
2679
 
2322
2680
  clearHandleTransforms() {
2323
2681
  this.clearTransforms();
2324
2682
  }
2325
2683
 
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));
2338
- }
2339
-
2340
- const originalPositions = this.transformedGeometries.get(mergedObject.uuid);
2341
- const tempVector = new Vector3();
2342
-
2343
- for (const [originalMesh, mappingData] of objectData.objectMapping) {
2344
- const transform = this.objectTransforms.get(originalMesh);
2345
-
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];
2351
- }
2352
- continue;
2353
- }
2354
-
2355
- const startVertex = mappingData.startVertexIndex;
2356
- const vertexCount = mappingData.vertexCount;
2357
-
2358
- for (let i = 0; i < vertexCount; i++) {
2359
- const idx = (startVertex + i) * 3;
2360
-
2361
- tempVector.set(originalPositions[idx], originalPositions[idx + 1], originalPositions[idx + 2]);
2362
-
2363
- tempVector.applyMatrix4(transform);
2364
-
2365
- positions[idx] = tempVector.x;
2366
- positions[idx + 1] = tempVector.y;
2367
- positions[idx + 2] = tempVector.z;
2368
- }
2369
- }
2370
-
2371
- if (geometry.attributes.normal) {
2372
- this._updateNormalsForTransform(geometry, objectData, originalPositions);
2373
- }
2374
-
2375
- positionAttr.needsUpdate = true;
2376
- geometry.computeBoundingSphere();
2377
- geometry.computeBoundingBox();
2378
- }
2379
-
2380
- _updateNormalsForTransform(geometry, objectData, originalPositions) {
2381
- const normalAttr = geometry.attributes.normal;
2382
- if (!normalAttr) return;
2383
-
2384
- const normals = normalAttr.array;
2385
- const tempVector = new Vector3();
2386
- const normalMatrix = new Matrix4();
2387
-
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));
2392
- }
2393
-
2394
- const originalNormals = this.transformedGeometries.get(normalsKey);
2395
-
2396
- for (const [originalMesh, mappingData] of objectData.objectMapping) {
2397
- // Direct lookup by object reference - NO HANDLE LOOKUP!
2398
- const transform = this.objectTransforms.get(originalMesh);
2399
-
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];
2406
- }
2407
- continue;
2408
- }
2409
-
2410
- // Create normal matrix (inverse transpose of transform)
2411
- normalMatrix.copy(transform).invert().transpose();
2412
-
2413
- const startVertex = mappingData.startVertexIndex;
2414
- const vertexCount = mappingData.vertexCount;
2415
-
2416
- for (let i = 0; i < vertexCount; i++) {
2417
- const idx = (startVertex + i) * 3;
2418
-
2419
- // Get original normal
2420
- tempVector.set(originalNormals[idx], originalNormals[idx + 1], originalNormals[idx + 2]);
2421
-
2422
- // Apply normal transformation
2423
- tempVector.applyMatrix4(normalMatrix).normalize();
2424
-
2425
- // Write back transformed normal
2426
- normals[idx] = tempVector.x;
2427
- normals[idx + 1] = tempVector.y;
2428
- normals[idx + 2] = tempVector.z;
2429
- }
2430
- }
2431
-
2432
- normalAttr.needsUpdate = true;
2433
- }
2434
-
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
- }
2446
-
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;
2454
- }
2455
-
2456
- geometry.computeBoundingSphere();
2457
- geometry.computeBoundingBox();
2458
- }
2459
-
2460
2684
  syncHiddenObjects() {
2461
2685
  if (this.mergedObjectMap.size === 0) {
2462
2686
  console.log("No merged objects to sync");