@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.
- package/dist/viewer-three.js +723 -358
- package/dist/viewer-three.js.map +1 -1
- package/dist/viewer-three.min.js +3 -3
- package/dist/viewer-three.module.js +724 -359
- package/dist/viewer-three.module.js.map +1 -1
- package/lib/Viewer/draggers/CuttingPlaneDragger.d.ts +23 -5
- package/lib/Viewer/helpers/PlaneHelper2.d.ts +8 -4
- package/lib/Viewer/measurement/Snapper.d.ts +1 -1
- package/package.json +5 -5
- package/src/Viewer/components/SelectionComponent.ts +1 -1
- package/src/Viewer/controls/WalkControls.ts +10 -2
- package/src/Viewer/draggers/CuttingPlaneDragger.ts +191 -31
- package/src/Viewer/draggers/CuttingPlaneXAxis.ts +2 -3
- package/src/Viewer/draggers/CuttingPlaneYAxis.ts +2 -3
- package/src/Viewer/draggers/CuttingPlaneZAxis.ts +2 -3
- package/src/Viewer/draggers/index.ts +2 -0
- package/src/Viewer/helpers/PlaneHelper2.ts +30 -17
- package/src/Viewer/loaders/DynamicGltfLoader/DynamicGltfLoader.js +495 -182
- package/src/Viewer/loaders/DynamicGltfLoader/DynamicModelImpl.ts +44 -33
- package/src/Viewer/measurement/Snapper.ts +13 -5
- package/src/Viewer/models/ModelImpl.ts +13 -10
|
@@ -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
|
-
|
|
1183
|
-
|
|
1336
|
+
// 2. Inject matrix retrieval at start of main()
|
|
1337
|
+
shader.vertexShader = shader.vertexShader.replace(
|
|
1338
|
+
"void main() {",
|
|
1184
1339
|
`
|
|
1185
|
-
|
|
1186
|
-
|
|
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
|
-
"
|
|
1360
|
+
"#include <begin_vertex>",
|
|
1192
1361
|
`
|
|
1193
|
-
|
|
1194
|
-
|
|
1362
|
+
vec3 transformed = vec3( position );
|
|
1363
|
+
transformed = ( batchingMatrix * vec4( transformed, 1.0 ) ).xyz;
|
|
1195
1364
|
`
|
|
1196
1365
|
);
|
|
1197
1366
|
|
|
1198
|
-
|
|
1199
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
2242
|
-
|
|
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
|
-
|
|
2271
|
-
|
|
2272
|
-
? objects
|
|
2273
|
-
: Array.from(objects)
|
|
2274
|
-
: Array.from(this.originalObjects);
|
|
2513
|
+
// Store transform map directly
|
|
2514
|
+
this.objectTransforms = objectTransformMap;
|
|
2275
2515
|
|
|
2276
|
-
|
|
2277
|
-
|
|
2516
|
+
// Reset to identity first to ensure clean state
|
|
2517
|
+
this._resetTransformData(false);
|
|
2278
2518
|
|
|
2279
|
-
|
|
2519
|
+
// Cache references for tight loop
|
|
2520
|
+
const transformData = this.transformData;
|
|
2521
|
+
const objectIdToIndex = this.objectIdToIndex;
|
|
2280
2522
|
|
|
2281
|
-
|
|
2282
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2288
|
-
|
|
2534
|
+
const handle = userData.handle;
|
|
2535
|
+
if (handle === undefined) continue;
|
|
2289
2536
|
|
|
2290
|
-
|
|
2291
|
-
|
|
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
|
-
|
|
2294
|
-
|
|
2295
|
-
const offset = direction.multiplyScalar(distance * (explodeFactor - 1.0));
|
|
2552
|
+
const handle = userData.handle;
|
|
2553
|
+
if (handle === undefined) continue;
|
|
2296
2554
|
|
|
2297
|
-
const
|
|
2298
|
-
|
|
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
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
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
|
-
|
|
2323
|
-
|
|
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
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
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
|
-
|
|
2341
|
-
|
|
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
|
-
|
|
2344
|
-
const
|
|
2598
|
+
if (objectTransformMap instanceof Map) {
|
|
2599
|
+
for (const [object, matrix] of objectTransformMap.entries()) {
|
|
2600
|
+
if (!object.userData?.handle) continue;
|
|
2345
2601
|
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
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
|
-
|
|
2356
|
-
|
|
2607
|
+
// Extract translation from the transform matrix (structure-root-local space)
|
|
2608
|
+
_offset.setFromMatrixPosition(matrix);
|
|
2357
2609
|
|
|
2358
|
-
|
|
2359
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2634
|
+
object.position.add(_offset);
|
|
2364
2635
|
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
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
|
-
|
|
2372
|
-
|
|
2373
|
-
}
|
|
2644
|
+
createExplodeTransforms(objects = null, explodeCenter = null, explodeFactor = 1.5) {
|
|
2645
|
+
const transformMap = new Map();
|
|
2374
2646
|
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
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
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2655
|
+
const objectsArray = objects
|
|
2656
|
+
? Array.isArray(objects)
|
|
2657
|
+
? objects
|
|
2658
|
+
: Array.from(objects)
|
|
2659
|
+
: Array.from(this.originalObjects);
|
|
2383
2660
|
|
|
2384
|
-
|
|
2385
|
-
const
|
|
2386
|
-
const normalMatrix = new Matrix4();
|
|
2661
|
+
// Cache inverse matrices for structures
|
|
2662
|
+
const structureInverseMatrices = new Map();
|
|
2387
2663
|
|
|
2388
|
-
//
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
this.
|
|
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
|
|
2674
|
+
for (const obj of objectsArray) {
|
|
2675
|
+
if (!obj.geometry || !obj.geometry.attributes.position) continue;
|
|
2395
2676
|
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2677
|
+
// OPTIMIZATION: Use cached node extent if available
|
|
2678
|
+
if (!obj.userData.explodeVector) {
|
|
2679
|
+
let center = null;
|
|
2399
2680
|
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
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
|
-
|
|
2411
|
-
|
|
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
|
-
|
|
2414
|
-
|
|
2699
|
+
// Calculate vector from explode center to object center
|
|
2700
|
+
const explodeVector = center.sub(explodeCenter);
|
|
2415
2701
|
|
|
2416
|
-
|
|
2417
|
-
|
|
2702
|
+
// Cache it
|
|
2703
|
+
obj.userData.explodeVector = explodeVector;
|
|
2704
|
+
}
|
|
2705
|
+
const explodeVector = obj.userData.explodeVector;
|
|
2706
|
+
const distance = explodeVector.length();
|
|
2418
2707
|
|
|
2419
|
-
|
|
2420
|
-
|
|
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
|
-
|
|
2423
|
-
|
|
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
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
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
|
-
|
|
2749
|
+
return transformMap;
|
|
2433
2750
|
}
|
|
2434
2751
|
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
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
|
-
//
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
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
|
-
|
|
2457
|
-
|
|
2769
|
+
clearHandleTransforms() {
|
|
2770
|
+
this.clearTransforms();
|
|
2458
2771
|
}
|
|
2459
2772
|
|
|
2460
2773
|
syncHiddenObjects() {
|