fl-web-component 2.1.1-beta.10 → 2.1.1-beta.11

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.
@@ -165,9 +165,15 @@ import { onContextHandle } from './component/context';
165
165
 
166
166
  const isDebug = process.env.NODE_ENV !== 'production' || process.env.VUE_APP_IS_WATCH === true;
167
167
  // const isDebug = false;
168
- const COLLISION_PENETRATION_EPSILON = 1;
168
+ const COLLISION_CONTACT_EPSILON = 1;
169
169
  const COLLISION_OBB_AXIS_EPSILON = 1e-8;
170
170
  const COLLISION_DEBUG_GROUP_NAME = '__collision_obb_debug_group__';
171
+ const OCCLUSION_DEBUG_GROUP_NAME = '__occlusion_bounds_debug_group__';
172
+ const OCCLUSION_DEBUG_FLAG_COLORS = {
173
+ 1: 0xffc107,
174
+ 2: 0x409eff,
175
+ 3: 0xff4dff,
176
+ };
171
177
 
172
178
  export default {
173
179
  name: 'FlModel',
@@ -198,8 +204,8 @@ export default {
198
204
  occlusionState: {
199
205
  // enabled: false,
200
206
  enabled: true,
201
- // previewEnabled: false,
202
- previewEnabled: isDebug,
207
+ previewEnabled: false,
208
+ // previewEnabled: isDebug,
203
209
  bufferWidth: 1700,
204
210
  sampleStride: 1,
205
211
  minSampleCount: 5,
@@ -218,6 +224,8 @@ export default {
218
224
  sceneBoundingBoxDebugEnabled: false,
219
225
  // collisionObbDebugEnabled: isDebug,
220
226
  collisionObbDebugEnabled: false,
227
+ // occlusionBoundsDebugEnabled: isDebug,
228
+ occlusionBoundsDebugEnabled: false,
221
229
  };
222
230
  },
223
231
  watch: {
@@ -267,6 +275,7 @@ export default {
267
275
  'sceneBoundingBox',
268
276
  'sceneBoundingBoxHelper',
269
277
  'collisionObbDebugGroup',
278
+ 'occlusionBoundsDebugGroup',
270
279
  ];
271
280
  arr.forEach(item => {
272
281
  this[item] = null;
@@ -288,6 +297,7 @@ export default {
288
297
  'isDragging',
289
298
  'boundingBoxVisible',
290
299
  'collisionObbDebugEnabled',
300
+ 'occlusionBoundsDebugEnabled',
291
301
  ].forEach(item => {
292
302
  this[item] = false;
293
303
  });
@@ -1218,6 +1228,26 @@ export default {
1218
1228
  if (instancedMesh && instancedMesh.userData) return instancedMesh.userData.copyMatrix || null;
1219
1229
  return null;
1220
1230
  },
1231
+ setInstanceCullingVisible(instancedMesh, instanceId, visible) {
1232
+ const instanceState = this.getInstanceState(instancedMesh, instanceId);
1233
+ if (!instanceState || typeof instanceState.instanceIndex !== 'number') return;
1234
+
1235
+ const copyMatrix =
1236
+ instanceState.copyMatrix || this.getInstanceCopyMatrix(instancedMesh, instanceId);
1237
+ if (!copyMatrix) return;
1238
+
1239
+ const shouldShow =
1240
+ visible && instanceState.visible !== false && this.isSourceVisible(instanceId, instancedMesh);
1241
+ const matrix = new this.THREE.Matrix4().copy(copyMatrix);
1242
+ if (!shouldShow) {
1243
+ matrix.makeTranslation(9999999, 9999999, 9999999);
1244
+ this.removeOutlineObject(instancedMesh, instanceState.instanceIndex);
1245
+ }
1246
+
1247
+ instancedMesh.setMatrixAt(instanceState.instanceIndex, matrix);
1248
+ instancedMesh.instanceMatrix.needsUpdate = true;
1249
+ instanceState.cullingVisible = !!visible;
1250
+ },
1221
1251
  isSourceHiddenInstance(instancedMesh, instanceId) {
1222
1252
  const instanceState = this.getInstanceState(instancedMesh, instanceId);
1223
1253
  if (instanceState && instanceState.sourceVisible !== undefined) {
@@ -1536,21 +1566,37 @@ export default {
1536
1566
  existingBox.union(box);
1537
1567
  return existingBox;
1538
1568
  },
1539
- mergeModelBoxUserData(targetBox, sourceUserData) {
1540
- if (!targetBox || !sourceUserData) return;
1569
+ createOcclusionProxyBox(box) {
1570
+ if (!box || !box.isBox3) return null;
1571
+ const proxyBox = box.clone();
1572
+ proxyBox.userData = { ...(box.userData || {}) };
1573
+ delete proxyBox.userData.occlusionProxyBoxes;
1574
+ return proxyBox;
1575
+ },
1576
+ mergeModelBoxUserData(targetBox, sourceBox) {
1577
+ if (!targetBox || !sourceBox || !sourceBox.userData) return;
1578
+ const sourceUserData = sourceBox.userData;
1541
1579
  if (!targetBox.userData) {
1542
1580
  targetBox.userData = sourceUserData;
1543
1581
  return;
1544
1582
  }
1545
1583
 
1546
1584
  const targetUserData = targetBox.userData;
1585
+ const targetProxyBoxes = Array.isArray(targetUserData.occlusionProxyBoxes)
1586
+ ? targetUserData.occlusionProxyBoxes
1587
+ : [this.createOcclusionProxyBox(targetBox)].filter(Boolean);
1588
+ const sourceProxyBoxes = Array.isArray(sourceUserData.occlusionProxyBoxes)
1589
+ ? sourceUserData.occlusionProxyBoxes
1590
+ : [this.createOcclusionProxyBox(sourceBox)].filter(Boolean);
1547
1591
  targetUserData.visible = targetUserData.visible !== false || sourceUserData.visible !== false;
1548
1592
  targetUserData.sourceVisible =
1549
1593
  targetUserData.sourceVisible !== false || sourceUserData.sourceVisible !== false;
1550
1594
  targetUserData.transparent = !!targetUserData.transparent && !!sourceUserData.transparent;
1551
1595
  targetUserData.mergedBoxCount = (targetUserData.mergedBoxCount || 1) + 1;
1552
- // 同一模型包含多个图元时,单个图元的遮挡代理不再可靠,改由视锥体合并盒兜底加载。
1553
- targetUserData.occlusionEnabled = false;
1596
+ // 多图元模型分别参与遮挡测试,任一图元可见时都会保留整个模型。
1597
+ targetUserData.occlusionEnabled =
1598
+ !!targetUserData.occlusionEnabled && !!sourceUserData.occlusionEnabled;
1599
+ targetUserData.occlusionProxyBoxes = targetProxyBoxes.concat(sourceProxyBoxes);
1554
1600
 
1555
1601
  const targetObbs = Array.isArray(targetUserData.collisionObbs)
1556
1602
  ? targetUserData.collisionObbs
@@ -1562,6 +1608,60 @@ export default {
1562
1608
  targetUserData.collisionObbs = targetObbs.concat(sourceObbs);
1563
1609
  }
1564
1610
  },
1611
+ _createCollisionObb(center, halfSize, rotation, matrix) {
1612
+ const worldCenter = center.clone();
1613
+ const worldHalfSize = halfSize.clone();
1614
+ const worldRotation = new this.THREE.Matrix3();
1615
+
1616
+ if (!matrix) {
1617
+ return new OBB(worldCenter, worldHalfSize, rotation.clone());
1618
+ }
1619
+
1620
+ worldCenter.applyMatrix4(matrix);
1621
+
1622
+ const localAxes = [
1623
+ new this.THREE.Vector3(),
1624
+ new this.THREE.Vector3(),
1625
+ new this.THREE.Vector3(),
1626
+ ];
1627
+ rotation.extractBasis(localAxes[0], localAxes[1], localAxes[2]);
1628
+
1629
+ const linearMatrix = new this.THREE.Matrix3().setFromMatrix4(matrix);
1630
+ const worldAxes = localAxes.map(axis => axis.clone().applyMatrix3(linearMatrix));
1631
+ const halfSizes = [halfSize.x, halfSize.y, halfSize.z];
1632
+
1633
+ for (let i = 0; i < 3; i++) {
1634
+ const axisLength = worldAxes[i].length();
1635
+ if (axisLength <= COLLISION_OBB_AXIS_EPSILON) {
1636
+ worldAxes[i].copy(localAxes[i]);
1637
+ continue;
1638
+ }
1639
+
1640
+ worldAxes[i].multiplyScalar(1 / axisLength);
1641
+ halfSizes[i] *= axisLength;
1642
+ }
1643
+
1644
+ worldHalfSize.set(halfSizes[0], halfSizes[1], halfSizes[2]);
1645
+ worldRotation.set(
1646
+ worldAxes[0].x,
1647
+ worldAxes[1].x,
1648
+ worldAxes[2].x,
1649
+ worldAxes[0].y,
1650
+ worldAxes[1].y,
1651
+ worldAxes[2].y,
1652
+ worldAxes[0].z,
1653
+ worldAxes[1].z,
1654
+ worldAxes[2].z
1655
+ );
1656
+
1657
+ return new OBB(worldCenter, worldHalfSize, worldRotation);
1658
+ },
1659
+ _createCollisionObbFromBox(box, matrix) {
1660
+ const center = box.getCenter(new this.THREE.Vector3());
1661
+ const halfSize = box.getSize(new this.THREE.Vector3()).multiplyScalar(0.5);
1662
+ const rotation = new this.THREE.Matrix3().identity();
1663
+ return this._createCollisionObb(center, halfSize, rotation, matrix);
1664
+ },
1565
1665
  upsertModelBox(modelId, box) {
1566
1666
  if (!this._boxIndex || !modelId || !box || !box.isBox3) return;
1567
1667
  const key = String(modelId);
@@ -1570,8 +1670,8 @@ export default {
1570
1670
  this._boxIndex.set(key, box);
1571
1671
  return;
1572
1672
  }
1673
+ this.mergeModelBoxUserData(existingBox, box);
1573
1674
  existingBox.union(box);
1574
- this.mergeModelBoxUserData(existingBox, box.userData);
1575
1675
  },
1576
1676
  setBoxIndex(boxJson, documentId, isAdd = true) {
1577
1677
  if (!this._boxIndex) this._boxIndex = new Map();
@@ -1703,10 +1803,12 @@ export default {
1703
1803
  halfSize: halfSize,
1704
1804
  };
1705
1805
 
1706
- const collisionObb = new OBB(center.clone(), halfSize.clone(), rotation.clone());
1707
- if (collisionMatrix) {
1708
- collisionObb.applyMatrix4(collisionMatrix);
1709
- }
1806
+ const collisionObb = this._createCollisionObb(
1807
+ center,
1808
+ halfSize,
1809
+ rotation,
1810
+ collisionMatrix
1811
+ );
1710
1812
  userData.collisionObbs = [collisionObb];
1711
1813
  }
1712
1814
  // 如果是 flag=2,解析并存储多个子包围盒 (min, max)
@@ -1720,10 +1822,7 @@ export default {
1720
1822
  subBoxes.push({ min, max });
1721
1823
 
1722
1824
  const subBox = new this.THREE.Box3(min.clone(), max.clone());
1723
- const collisionObb = new OBB().fromBox3(subBox);
1724
- if (collisionMatrix) {
1725
- collisionObb.applyMatrix4(collisionMatrix);
1726
- }
1825
+ const collisionObb = this._createCollisionObbFromBox(subBox, collisionMatrix);
1727
1826
  collisionObbs.push(collisionObb);
1728
1827
  }
1729
1828
  }
@@ -1734,10 +1833,7 @@ export default {
1734
1833
  }
1735
1834
 
1736
1835
  if (!userData.collisionObbs || userData.collisionObbs.length === 0) {
1737
- const collisionObb = new OBB().fromBox3(sourceBox);
1738
- if (collisionMatrix) {
1739
- collisionObb.applyMatrix4(collisionMatrix);
1740
- }
1836
+ const collisionObb = this._createCollisionObbFromBox(sourceBox, collisionMatrix);
1741
1837
  userData.collisionObbs = [collisionObb];
1742
1838
  }
1743
1839
 
@@ -1773,11 +1869,26 @@ export default {
1773
1869
  }
1774
1870
  this.buildOctreeFromBoxIndex();
1775
1871
  this.refreshCollisionObbDebug();
1872
+ this.refreshOcclusionBoundsDebug();
1776
1873
  console.log('time end', Date.now());
1777
1874
  },
1778
1875
  shouldApplyOcclusionByBox(box) {
1779
1876
  return !!(box && box.userData && box.userData.occlusionEnabled);
1780
1877
  },
1878
+ appendOcclusionCandidates(candidates, modelId, box, frustum) {
1879
+ if (!box) return;
1880
+ const userData = box.userData || {};
1881
+ const proxyBoxes = Array.isArray(userData.occlusionProxyBoxes)
1882
+ ? userData.occlusionProxyBoxes
1883
+ : [box];
1884
+
1885
+ for (let i = 0; i < proxyBoxes.length; i++) {
1886
+ const proxyBox = proxyBoxes[i];
1887
+ if (!proxyBox || (proxyBox.userData && proxyBox.userData.visible === false)) continue;
1888
+ if (frustum && !frustum.intersectsBox(proxyBox)) continue;
1889
+ candidates.push({ modelId, box: proxyBox });
1890
+ }
1891
+ },
1781
1892
  tryInitialCenterAfterBoundsReady() {
1782
1893
  if (this.hasExecutedCentering || this.userInteracting) return;
1783
1894
  if (
@@ -2089,20 +2200,15 @@ export default {
2089
2200
 
2090
2201
  this._clearCollisionObbDebugGroup(false);
2091
2202
  const pairMap = this._buildCollisionPairMap();
2092
- if (pairMap.size === 0) return;
2203
+ if (!this._boxIndex || this._boxIndex.size === 0) return;
2093
2204
 
2094
- pairMap.forEach((relatedIds, modelId) => {
2095
- const box =
2096
- this._boxIndex && this._boxIndex.has(String(modelId))
2097
- ? this._boxIndex.get(String(modelId))
2098
- : this._boxIndex && this._boxIndex.has(modelId)
2099
- ? this._boxIndex.get(modelId)
2100
- : null;
2205
+ this._boxIndex.forEach((box, modelId) => {
2101
2206
  if (!this._isCollisionBoxAvailable(box)) return;
2102
2207
 
2103
2208
  const obbs = this._getCollisionObbs(box);
2104
2209
  if (!obbs.length) return;
2105
2210
 
2211
+ const relatedIds = pairMap.get(String(modelId));
2106
2212
  const isColliding = relatedIds && relatedIds.size > 0;
2107
2213
  const color = isColliding ? 0xff3b30 : 0x36cfc9;
2108
2214
  for (let i = 0; i < obbs.length; i++) {
@@ -2128,6 +2234,165 @@ export default {
2128
2234
  this.hideCollisionObbDebug();
2129
2235
  }
2130
2236
  },
2237
+ _getOcclusionDebugGroup() {
2238
+ if (!this.scene) return null;
2239
+ if (this.occlusionBoundsDebugGroup && this.occlusionBoundsDebugGroup.parent === this.scene) {
2240
+ return this.occlusionBoundsDebugGroup;
2241
+ }
2242
+
2243
+ const group = new this.THREE.Group();
2244
+ group.name = OCCLUSION_DEBUG_GROUP_NAME;
2245
+ group.userData.transformControlHelper = true;
2246
+ group.userData.occlusionBoundsDebugHelper = true;
2247
+ this.scene.add(group);
2248
+ this.occlusionBoundsDebugGroup = group;
2249
+ return group;
2250
+ },
2251
+ _createOcclusionFlag1LineSegments(box, color) {
2252
+ const userData = box && box.userData ? box.userData : {};
2253
+ const vertices = userData.obbData;
2254
+ if (!vertices || vertices.length < 9) return null;
2255
+
2256
+ const sourceGeometry = new this.THREE.BufferGeometry();
2257
+ sourceGeometry.setAttribute(
2258
+ 'position',
2259
+ new this.THREE.BufferAttribute(new Float32Array(vertices), 3)
2260
+ );
2261
+
2262
+ const indices = userData.indices;
2263
+ if (indices && indices.length >= 3) {
2264
+ if (indices.isBufferAttribute) {
2265
+ sourceGeometry.setIndex(indices);
2266
+ } else if (ArrayBuffer.isView(indices)) {
2267
+ sourceGeometry.setIndex(new this.THREE.BufferAttribute(indices, 1));
2268
+ } else {
2269
+ sourceGeometry.setIndex(indices);
2270
+ }
2271
+ }
2272
+
2273
+ let wireframeGeometry = null;
2274
+ try {
2275
+ // flag=1 对应真实遮挡网格,必须按顶点和三角形索引绘制,不能退化成 AABB/OBB。
2276
+ wireframeGeometry = new this.THREE.WireframeGeometry(sourceGeometry);
2277
+ } catch (error) {
2278
+ console.warn('flag=1 遮挡调试线框生成失败', error);
2279
+ }
2280
+ sourceGeometry.dispose();
2281
+ if (!wireframeGeometry || wireframeGeometry.getAttribute('position').count === 0) {
2282
+ if (wireframeGeometry) wireframeGeometry.dispose();
2283
+ return null;
2284
+ }
2285
+
2286
+ const material = new this.THREE.LineBasicMaterial({
2287
+ color,
2288
+ transparent: true,
2289
+ opacity: 0.95,
2290
+ depthTest: false,
2291
+ depthWrite: false,
2292
+ toneMapped: false,
2293
+ });
2294
+ const lineSegments = new this.THREE.LineSegments(wireframeGeometry, material);
2295
+ const transformMatrix = new this.THREE.Matrix4();
2296
+ const matrixArr = userData.matrix;
2297
+ if (matrixArr && matrixArr.length >= 16) {
2298
+ transformMatrix.fromArray(matrixArr);
2299
+ }
2300
+ if (this.bizToThreeMatrix) {
2301
+ transformMatrix.premultiply(this.bizToThreeMatrix);
2302
+ }
2303
+ lineSegments.matrixAutoUpdate = false;
2304
+ lineSegments.matrix.copy(transformMatrix);
2305
+ lineSegments.matrixWorldNeedsUpdate = true;
2306
+ lineSegments.renderOrder = 998;
2307
+ lineSegments.userData.transformControlHelper = true;
2308
+ lineSegments.userData.occlusionBoundsDebugHelper = true;
2309
+ lineSegments.userData.occlusionFlag = 1;
2310
+ return lineSegments;
2311
+ },
2312
+ _createOcclusionBoxLineSegments(obb, color, flag) {
2313
+ const lineSegments = this._createCollisionObbLineSegments(obb, color);
2314
+ if (!lineSegments) return null;
2315
+
2316
+ lineSegments.renderOrder = 998;
2317
+ lineSegments.userData.collisionObbDebugHelper = false;
2318
+ lineSegments.userData.occlusionBoundsDebugHelper = true;
2319
+ lineSegments.userData.occlusionFlag = flag;
2320
+ return lineSegments;
2321
+ },
2322
+ _clearOcclusionBoundsDebugGroup(removeGroup = false) {
2323
+ const group = this.occlusionBoundsDebugGroup;
2324
+ if (!group) return;
2325
+
2326
+ while (group.children.length > 0) {
2327
+ const child = group.children[group.children.length - 1];
2328
+ group.remove(child);
2329
+ if (child.geometry) child.geometry.dispose();
2330
+ if (Array.isArray(child.material)) {
2331
+ child.material.forEach(material => material && material.dispose());
2332
+ } else if (child.material) {
2333
+ child.material.dispose();
2334
+ }
2335
+ }
2336
+
2337
+ if (removeGroup && this.scene && group.parent === this.scene) {
2338
+ this.scene.remove(group);
2339
+ this.occlusionBoundsDebugGroup = null;
2340
+ } else if (!this.scene) {
2341
+ this.occlusionBoundsDebugGroup = null;
2342
+ }
2343
+ },
2344
+ hideOcclusionBoundsDebug() {
2345
+ this._clearOcclusionBoundsDebugGroup();
2346
+ this.occlusionBoundsDebugEnabled = false;
2347
+ },
2348
+ refreshOcclusionBoundsDebug() {
2349
+ if (!this.occlusionBoundsDebugEnabled) return;
2350
+ const group = this._getOcclusionDebugGroup();
2351
+ if (!group) return;
2352
+
2353
+ this._clearOcclusionBoundsDebugGroup(false);
2354
+ if (!this._boxIndex || this._boxIndex.size === 0) return;
2355
+
2356
+ this._boxIndex.forEach((box, modelId) => {
2357
+ if (!this._isCollisionBoxAvailable(box) || !this.shouldApplyOcclusionByBox(box)) return;
2358
+
2359
+ const userData = box.userData || {};
2360
+ const flag = Number(userData.flag) || 1;
2361
+ const color = OCCLUSION_DEBUG_FLAG_COLORS[flag];
2362
+ if (!color) return;
2363
+
2364
+ if (flag === 1) {
2365
+ const lineSegments = this._createOcclusionFlag1LineSegments(box, color);
2366
+ if (!lineSegments) return;
2367
+ lineSegments.name = `${OCCLUSION_DEBUG_GROUP_NAME}:${modelId}:flag-1`;
2368
+ lineSegments.userData.modelId = modelId;
2369
+ group.add(lineSegments);
2370
+ return;
2371
+ }
2372
+
2373
+ const obbs = this._getCollisionObbs(box);
2374
+ for (let i = 0; i < obbs.length; i++) {
2375
+ const lineSegments = this._createOcclusionBoxLineSegments(obbs[i], color, flag);
2376
+ if (!lineSegments) continue;
2377
+ lineSegments.name = `${OCCLUSION_DEBUG_GROUP_NAME}:${modelId}:flag-${flag}:${i}`;
2378
+ lineSegments.userData.modelId = modelId;
2379
+ group.add(lineSegments);
2380
+ }
2381
+ });
2382
+ },
2383
+ showOcclusionBoundsDebug() {
2384
+ this.occlusionBoundsDebugEnabled = true;
2385
+ this.refreshOcclusionBoundsDebug();
2386
+ },
2387
+ toggleOcclusionBoundsDebug(forceVisible) {
2388
+ const nextVisible =
2389
+ typeof forceVisible === 'boolean' ? forceVisible : !this.occlusionBoundsDebugEnabled;
2390
+ if (nextVisible) {
2391
+ this.showOcclusionBoundsDebug();
2392
+ } else {
2393
+ this.hideOcclusionBoundsDebug();
2394
+ }
2395
+ },
2131
2396
  _getObbAxisInfo(obb) {
2132
2397
  const xAxis = new this.THREE.Vector3();
2133
2398
  const yAxis = new this.THREE.Vector3();
@@ -2197,7 +2462,7 @@ export default {
2197
2462
  const obbsB = this._getCollisionObbs(boxB);
2198
2463
  for (let i = 0; i < obbsA.length; i++) {
2199
2464
  for (let j = 0; j < obbsB.length; j++) {
2200
- if (this._getObbMinPenetration(obbsA[i], obbsB[j]) > COLLISION_PENETRATION_EPSILON) {
2465
+ if (this._getObbMinPenetration(obbsA[i], obbsB[j]) >= COLLISION_CONTACT_EPSILON) {
2201
2466
  return true;
2202
2467
  }
2203
2468
  }
@@ -2809,7 +3074,12 @@ export default {
2809
3074
  visibleIdSet.add(this.formatModelId(hits[i].modelId));
2810
3075
  continue;
2811
3076
  }
2812
- candidates.push(hits[i]);
3077
+ this.appendOcclusionCandidates(
3078
+ candidates,
3079
+ hits[i].modelId,
3080
+ hits[i].box,
3081
+ globalFrustum
3082
+ );
2813
3083
  }
2814
3084
  } else if (this._boxIndex && this._boxIndex.size > 0) {
2815
3085
  this._boxIndex.forEach((box, modelId) => {
@@ -2820,7 +3090,7 @@ export default {
2820
3090
  visibleIdSet.add(this.formatModelId(modelId));
2821
3091
  return;
2822
3092
  }
2823
- candidates.push({ modelId, box });
3093
+ this.appendOcclusionCandidates(candidates, modelId, box, globalFrustum);
2824
3094
  }
2825
3095
  });
2826
3096
  }
@@ -2936,6 +3206,7 @@ export default {
2936
3206
  const tBuild0 = performance.now();
2937
3207
 
2938
3208
  const flag1Items = [];
3209
+ const flag2Items = [];
2939
3210
  const flag3Items = [];
2940
3211
 
2941
3212
  let globalIdx = 1;
@@ -2949,6 +3220,8 @@ export default {
2949
3220
  const flag = (c.box.userData && c.box.userData.flag) || 1;
2950
3221
  if (flag === 3 && c.box.userData && c.box.userData.obb) {
2951
3222
  flag3Items.push({ c, idx });
3223
+ } else if (flag === 2) {
3224
+ flag2Items.push({ c, idx });
2952
3225
  } else if (flag === 1) {
2953
3226
  flag1Items.push({ c, idx });
2954
3227
  }
@@ -2965,7 +3238,8 @@ export default {
2965
3238
  const _tempMatA = occObjs._tempMatA || (occObjs._tempMatA = new this.THREE.Matrix4());
2966
3239
 
2967
3240
  let boxes = occObjs.boxes;
2968
- const boxCount = flag3Items.length;
3241
+ const boxItems = flag3Items.concat(flag2Items);
3242
+ const boxCount = boxItems.length;
2969
3243
  if (boxCount > 0) {
2970
3244
  const needCapacity = boxCount;
2971
3245
  const capacity = boxes && typeof boxes.capacity === 'number' ? boxes.capacity : 0;
@@ -2995,19 +3269,26 @@ export default {
2995
3269
  }
2996
3270
  boxes.mesh.count = boxCount;
2997
3271
  for (let i = 0; i < boxCount; i++) {
2998
- const item = flag3Items[i];
2999
- const { matrix, halfSize } = item.c.box.userData.obb;
3272
+ const item = boxItems[i];
3000
3273
  const idx = item.idx;
3001
3274
  const r = (idx & 255) / 255;
3002
3275
  const g = ((idx >> 8) & 255) / 255;
3003
3276
  const b = ((idx >> 16) & 255) / 255;
3004
3277
  _tempColor.setRGB(r, g, b);
3005
3278
 
3006
- _tempScale.copy(halfSize).multiplyScalar(2);
3007
- _tempMatrix.copy(matrix);
3008
- _tempMatrix.scale(_tempScale);
3009
- if (typeof this.bizToThreeMatrix !== 'undefined' && this.bizToThreeMatrix) {
3010
- _tempMatrix.premultiply(this.bizToThreeMatrix);
3279
+ const obb = item.c.box.userData && item.c.box.userData.obb;
3280
+ if (obb) {
3281
+ _tempScale.copy(obb.halfSize).multiplyScalar(2);
3282
+ _tempMatrix.copy(obb.matrix);
3283
+ _tempMatrix.scale(_tempScale);
3284
+ if (typeof this.bizToThreeMatrix !== 'undefined' && this.bizToThreeMatrix) {
3285
+ _tempMatrix.premultiply(this.bizToThreeMatrix);
3286
+ }
3287
+ } else {
3288
+ item.c.box.getSize(_tempScale);
3289
+ item.c.box.getCenter(_tempVec3);
3290
+ _tempMatrix.makeTranslation(_tempVec3.x, _tempVec3.y, _tempVec3.z);
3291
+ _tempMatrix.scale(_tempScale);
3011
3292
  }
3012
3293
 
3013
3294
  boxes.mesh.setMatrixAt(i, _tempMatrix);
@@ -3450,12 +3731,12 @@ export default {
3450
3731
  this.renderer.toneMapping = prevToneMapping;
3451
3732
  } catch (e) {
3452
3733
  for (let i = 0; i < candidates.length; i++) {
3453
- visibleIdSet.add(candidates[i].modelId);
3734
+ visibleIdSet.add(this.formatModelId(candidates[i].modelId));
3454
3735
  }
3455
3736
  }
3456
3737
  } else {
3457
3738
  for (let i = 0; i < candidates.length; i++) {
3458
- visibleIdSet.add(candidates[i].modelId);
3739
+ visibleIdSet.add(this.formatModelId(candidates[i].modelId));
3459
3740
  }
3460
3741
  }
3461
3742
  visibleIds.length = 0;
@@ -3464,37 +3745,43 @@ export default {
3464
3745
  const parentsToCheck = new Set();
3465
3746
  this.scene.traverse(child => {
3466
3747
  if (!child.isInstancedMesh) return;
3467
- // 如果复用数量等于大于2个,则跳过裁剪和剔除处理(始终保留)
3468
- if (child.count >= 2) {
3469
- let instancesMap =
3470
- child && child.userData && child.userData.instancesMap instanceof Map
3471
- ? child.userData.instancesMap
3472
- : child.parent &&
3473
- child.parent.userData &&
3474
- child.parent.userData.instancesMap instanceof Map
3475
- ? child.parent.userData.instancesMap
3476
- : null;
3748
+ const instancesMap =
3749
+ child && child.userData && child.userData.instancesMap instanceof Map
3750
+ ? child.userData.instancesMap
3751
+ : child.parent &&
3752
+ child.parent.userData &&
3753
+ child.parent.userData.instancesMap instanceof Map
3754
+ ? child.parent.userData.instancesMap
3755
+ : null;
3477
3756
 
3757
+ // 多实例复用同一个 InstancedMesh 时,仅在全部实例都不可见时整体卸载。
3758
+ if (child.count >= 2) {
3478
3759
  if (instancesMap) {
3479
3760
  let allInactive = true;
3480
3761
  const modelIds = Array.from(instancesMap.keys());
3762
+ let unloadModelId = null;
3481
3763
 
3482
3764
  // 第一遍遍历:检查是否有任何一个实例处于活跃状态(在视锥体内且可见)
3483
3765
  for (const modelId of modelIds) {
3484
3766
  // 检查已取消勾选模型进行卸载
3485
- const documentId = modelId.split(':')[1];
3486
- if (!this.noObserver.documentModelIds.get(documentId)) {
3487
- toUnload.push({ modelId, child });
3488
- return;
3767
+ const documentId = String(modelId).split(':')[1];
3768
+ if (documentId && !this.noObserver.documentModelIds.get(documentId)) {
3769
+ if (!skipUnload) this.setInstanceCullingVisible(child, modelId, false);
3770
+ unloadModelId = modelId;
3771
+ continue;
3489
3772
  }
3490
3773
 
3491
- if (bypassList && bypassList.size > 0 && bypassList.has(modelId)) return;
3774
+ const formattedModelId = this.formatModelId(modelId);
3775
+ if (
3776
+ bypassList &&
3777
+ bypassList.size > 0 &&
3778
+ (bypassList.has(modelId) || bypassList.has(formattedModelId))
3779
+ ) {
3780
+ if (!skipUnload) this.setInstanceCullingVisible(child, modelId, true);
3781
+ allInactive = false;
3782
+ continue;
3783
+ }
3492
3784
 
3493
- const instanceInfo = instancesMap.get(modelId);
3494
- const instanceIndex =
3495
- instanceInfo && typeof instanceInfo.instanceIndex === 'number'
3496
- ? instanceInfo.instanceIndex
3497
- : null;
3498
3785
  const box =
3499
3786
  this._boxIndex && this._boxIndex.has(String(modelId))
3500
3787
  ? this._boxIndex.get(String(modelId))
@@ -3502,16 +3789,19 @@ export default {
3502
3789
  ? this._boxIndex.get(modelId)
3503
3790
  : null;
3504
3791
  if (!this.shouldApplyOcclusionByBox(box)) {
3792
+ if (!skipUnload) this.setInstanceCullingVisible(child, modelId, true);
3505
3793
  allInactive = false;
3794
+ toLoadSet.delete(formattedModelId);
3506
3795
  continue;
3507
3796
  }
3508
3797
 
3509
- const inFrustum = this.isModelInFrustum(child, instanceIndex, globalFrustum);
3510
- const modelInVisible = toLoadSet.has(modelId);
3511
- // console.log('modelId', modelId, 'inFrustum', inFrustum, 'modelInVisible', modelInVisible)
3798
+ const modelInVisible = visibleIdSet.has(formattedModelId);
3799
+ toLoadSet.delete(formattedModelId);
3512
3800
 
3513
- // 只要有一个实例在视锥体内且在可见列表中,就不整体卸载
3514
- if (inFrustum || modelInVisible) {
3801
+ // 可见集合已经聚合了同一模型的全部图元,任一图元可见时都保留该模型实例。
3802
+ const isActive = modelInVisible;
3803
+ if (!skipUnload) this.setInstanceCullingVisible(child, modelId, isActive);
3804
+ if (isActive) {
3515
3805
  allInactive = false;
3516
3806
  }
3517
3807
  }
@@ -3525,20 +3815,24 @@ export default {
3525
3815
  ? this._boxIndex.get(modelId)
3526
3816
  : null;
3527
3817
  if (!this.shouldApplyOcclusionByBox(box)) {
3528
- if (toLoadSet.has(modelId)) {
3529
- toLoadSet.delete(modelId);
3530
- }
3818
+ toLoadSet.delete(this.formatModelId(modelId));
3531
3819
  continue;
3532
3820
  }
3533
3821
  // 无论是否卸载,只要模型已在场景中,就从待加载集合中移除
3534
- if (toLoadSet.has(modelId)) {
3535
- toLoadSet.delete(modelId);
3536
- }
3822
+ toLoadSet.delete(this.formatModelId(modelId));
3537
3823
 
3538
- // 仅当所有实例都不活跃时,才执行卸载
3539
- if (!skipUnload && !this.firstPerSign && !this.roaming && allInactive) {
3540
- toUnload.push({ modelId, child });
3541
- }
3824
+ if (!unloadModelId) unloadModelId = modelId;
3825
+ }
3826
+
3827
+ // 仅当所有实例都不活跃时,才整体卸载一次,避免重复释放同一网格。
3828
+ if (
3829
+ !skipUnload &&
3830
+ !this.firstPerSign &&
3831
+ !this.roaming &&
3832
+ allInactive &&
3833
+ unloadModelId !== null
3834
+ ) {
3835
+ toUnload.push({ modelId: unloadModelId, child });
3542
3836
  }
3543
3837
  }
3544
3838
  // if (toLoadSet.has(modelId)) {
@@ -3547,10 +3841,18 @@ export default {
3547
3841
  return;
3548
3842
  }
3549
3843
  const modelId =
3550
- child.parent && child.parent.userData && child.parent.userData.instanceId
3844
+ instancesMap && instancesMap.size > 0
3845
+ ? instancesMap.keys().next().value
3846
+ : child.parent && child.parent.userData && child.parent.userData.instanceId
3551
3847
  ? child.parent.userData.instanceId
3552
3848
  : child.uuid;
3553
- if (bypassList && bypassList.size > 0 && bypassList.has(modelId)) return;
3849
+ const formattedModelId = this.formatModelId(modelId);
3850
+ if (
3851
+ bypassList &&
3852
+ bypassList.size > 0 &&
3853
+ (bypassList.has(modelId) || bypassList.has(formattedModelId))
3854
+ )
3855
+ return;
3554
3856
  const box =
3555
3857
  this._boxIndex && this._boxIndex.has(String(modelId))
3556
3858
  ? this._boxIndex.get(String(modelId))
@@ -3558,18 +3860,13 @@ export default {
3558
3860
  ? this._boxIndex.get(modelId)
3559
3861
  : null;
3560
3862
  if (!this.shouldApplyOcclusionByBox(box)) {
3561
- if (toLoadSet.has(modelId)) {
3562
- toLoadSet.delete(modelId);
3563
- }
3863
+ toLoadSet.delete(formattedModelId);
3564
3864
  return;
3565
3865
  }
3566
3866
 
3567
- const inFrustum = this.isModelInFrustum(child, null, globalFrustum);
3568
- let modelInVisible = toLoadSet.has(modelId);
3569
- if (modelInVisible) {
3570
- toLoadSet.delete(modelId);
3571
- }
3572
- if (!skipUnload && !this.firstPerSign && !this.roaming && (!inFrustum || !modelInVisible)) {
3867
+ const modelInVisible = visibleIdSet.has(formattedModelId);
3868
+ toLoadSet.delete(formattedModelId);
3869
+ if (!skipUnload && !this.firstPerSign && !this.roaming && !modelInVisible) {
3573
3870
  toUnload.push({ modelId, child });
3574
3871
  }
3575
3872
  });
@@ -5030,6 +5327,7 @@ export default {
5030
5327
  children.instanceMatrix.needsUpdate = true;
5031
5328
  });
5032
5329
  this.refreshCollisionObbDebug();
5330
+ this.refreshOcclusionBoundsDebug();
5033
5331
  this.notifyCameraChange(); // 触发场景更新
5034
5332
  break;
5035
5333
  }
@@ -5135,6 +5433,7 @@ export default {
5135
5433
  this.buildOctreeFromBoxIndex();
5136
5434
  }
5137
5435
  this.refreshCollisionObbDebug();
5436
+ this.refreshOcclusionBoundsDebug();
5138
5437
  },
5139
5438
  showOutlinePass(instanceId) {
5140
5439
  let targetObj = this.getObjectByName(instanceId);
@@ -5282,6 +5581,7 @@ export default {
5282
5581
  children.instanceMatrix.needsUpdate = true;
5283
5582
  });
5284
5583
  this.refreshCollisionObbDebug();
5584
+ this.refreshOcclusionBoundsDebug();
5285
5585
  break;
5286
5586
  }
5287
5587
  case 'opacity':
@@ -5703,6 +6003,7 @@ export default {
5703
6003
  }
5704
6004
  this.clearBypassCullingModelIds();
5705
6005
  this.hideCollisionObbDebug();
6006
+ this.hideOcclusionBoundsDebug();
5706
6007
 
5707
6008
  if (this.scene) {
5708
6009
  this.removeTraverse();
@@ -5968,6 +6269,7 @@ export default {
5968
6269
  if (nodeType === 'custom-root') return true;
5969
6270
  if (userData.transformControlHelper === true) return true;
5970
6271
  if (userData.collisionObbDebugHelper === true) return true;
6272
+ if (userData.occlusionBoundsDebugHelper === true) return true;
5971
6273
  if (object.isCamera || object.isLight) return true;
5972
6274
  if (typeof object.type === 'string' && /Helper$/i.test(object.type)) return true;
5973
6275
  return object.type === 'CSS2DObject' || object.type === 'TransformControlsRoot';
@@ -7079,6 +7381,7 @@ export default {
7079
7381
  object.userData.instancesMap instanceof Map &&
7080
7382
  object.userData.instancesMap.get(instanceId);
7081
7383
  if (instanceInfo && instanceInfo.visible === false) return false;
7384
+ if (instanceInfo && instanceInfo.cullingVisible === false) return false;
7082
7385
 
7083
7386
  const box =
7084
7387
  this._boxIndex && this._boxIndex.has(String(instanceId))