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

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