fl-web-component 2.1.1-beta.1 → 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 = 10;
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,
@@ -215,7 +221,11 @@ export default {
215
221
  isPaused: false,
216
222
  },
217
223
  isolateMode: false,
218
- collisionObbDebugEnabled: isDebug,
224
+ sceneBoundingBoxDebugEnabled: false,
225
+ // collisionObbDebugEnabled: isDebug,
226
+ collisionObbDebugEnabled: false,
227
+ // occlusionBoundsDebugEnabled: isDebug,
228
+ occlusionBoundsDebugEnabled: false,
219
229
  };
220
230
  },
221
231
  watch: {
@@ -224,6 +234,13 @@ export default {
224
234
  this.detachTransformControls();
225
235
  }
226
236
  },
237
+ sceneBoundingBoxDebugEnabled(val) {
238
+ if (val) {
239
+ this.refreshSceneBoundingBoxHelper(true);
240
+ } else {
241
+ this.hideSceneBoundingBox();
242
+ }
243
+ },
227
244
  },
228
245
  beforeCreate() {
229
246
  this.spaceUp = true;
@@ -258,6 +275,7 @@ export default {
258
275
  'sceneBoundingBox',
259
276
  'sceneBoundingBoxHelper',
260
277
  'collisionObbDebugGroup',
278
+ 'occlusionBoundsDebugGroup',
261
279
  ];
262
280
  arr.forEach(item => {
263
281
  this[item] = null;
@@ -279,6 +297,7 @@ export default {
279
297
  'isDragging',
280
298
  'boundingBoxVisible',
281
299
  'collisionObbDebugEnabled',
300
+ 'occlusionBoundsDebugEnabled',
282
301
  ].forEach(item => {
283
302
  this[item] = false;
284
303
  });
@@ -360,11 +379,16 @@ export default {
360
379
  wheelTimeout: null,
361
380
  },
362
381
  },
382
+ cameraControlMoveObserver: {
383
+ enabled: false,
384
+ interval: 500,
385
+ },
363
386
  streamLoader: null,
364
387
  // isPerformingInitialCentering: false, // 标记是否正在执行初始居中
365
388
  isObserverEnabled: true, // 标记相机变更观察者是否启用
366
389
  sceneBoxes: new Map(),
367
390
  documentModelIds: new Map(),
391
+ documentSceneBoundsIds: new Map(),
368
392
  outlineInstanceProxyMap: new Map(),
369
393
  occlusionWorker: null,
370
394
  occlusionWorkerRequestMap: new Map(),
@@ -1204,6 +1228,26 @@ export default {
1204
1228
  if (instancedMesh && instancedMesh.userData) return instancedMesh.userData.copyMatrix || null;
1205
1229
  return null;
1206
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
+ },
1207
1251
  isSourceHiddenInstance(instancedMesh, instanceId) {
1208
1252
  const instanceState = this.getInstanceState(instancedMesh, instanceId);
1209
1253
  if (instanceState && instanceState.sourceVisible !== undefined) {
@@ -1469,71 +1513,178 @@ export default {
1469
1513
  updateGlobalSceneBoundingBox() {
1470
1514
  const state = this.noObserver || {};
1471
1515
  const occScene = state.occlusionState && state.occlusionState._occScene;
1516
+ const mergedBox = new this.THREE.Box3();
1517
+ let hasValidBox = false;
1518
+ const mergeBox = box => {
1519
+ if (!box || !box.isBox3 || box.isEmpty()) return;
1520
+ if (!hasValidBox) {
1521
+ mergedBox.copy(box);
1522
+ hasValidBox = true;
1523
+ return;
1524
+ }
1525
+ mergedBox.union(box);
1526
+ };
1472
1527
 
1473
- // 优先使用由 boxIndex 构建出的离屏全场景,作为整体居中与 home 的统一范围
1528
+ // 离屏遮挡场景只包含参与遮挡的模型,需要继续合并场景范围索引中的二维图形范围。
1474
1529
  if (occScene && occScene.children && occScene.children.length > 0) {
1475
1530
  const occBox = new this.THREE.Box3().setFromObject(occScene);
1476
- if (!occBox.isEmpty()) {
1477
- this.sceneBoundingBox = occBox;
1478
- return this.sceneBoundingBox;
1479
- }
1531
+ mergeBox(occBox);
1480
1532
  }
1481
1533
 
1482
- // 离屏场景尚未完成时,回退到 boxIndex 联合包围盒,确保每次 add boxIndex 都能更新全场景范围
1483
- if (this._boxIndex && this._boxIndex.size > 0) {
1484
- let minX = Infinity,
1485
- minY = Infinity,
1486
- minZ = Infinity;
1487
- let maxX = -Infinity,
1488
- maxY = -Infinity,
1489
- maxZ = -Infinity;
1490
- this._boxIndex.forEach(box => {
1491
- if (!box || !box.isBox3 || box.isEmpty()) return;
1492
- const mn = box.min;
1493
- const mx = box.max;
1494
- if (mn.x < minX) minX = mn.x;
1495
- if (mn.y < minY) minY = mn.y;
1496
- if (mn.z < minZ) minZ = mn.z;
1497
- if (mx.x > maxX) maxX = mx.x;
1498
- if (mx.y > maxY) maxY = mx.y;
1499
- if (mx.z > maxZ) maxZ = mx.z;
1534
+ // sceneBoundsIndex 包含二维和三维图形,是全场景 dolly 距离的主要范围来源。
1535
+ if (this._sceneBoundsIndex && this._sceneBoundsIndex.size > 0) {
1536
+ this._sceneBoundsIndex.forEach(box => {
1537
+ mergeBox(box);
1500
1538
  });
1501
- if (
1502
- Number.isFinite(minX) &&
1503
- Number.isFinite(minY) &&
1504
- Number.isFinite(minZ) &&
1505
- Number.isFinite(maxX) &&
1506
- Number.isFinite(maxY) &&
1507
- Number.isFinite(maxZ)
1508
- ) {
1509
- this.sceneBoundingBox = new this.THREE.Box3(
1510
- new this.THREE.Vector3(minX, minY, minZ),
1511
- new this.THREE.Vector3(maxX, maxY, maxZ)
1512
- );
1513
- return this.sceneBoundingBox;
1514
- }
1515
1539
  }
1516
1540
 
1517
1541
  const boxes = Array.from((state.sceneBoxes && state.sceneBoxes.values()) || []);
1518
1542
  if (boxes.length > 0) {
1519
- const firstBox = boxes[0].clone();
1520
- for (let i = 1; i < boxes.length; i++) {
1521
- firstBox.union(boxes[i]);
1543
+ for (let i = 0; i < boxes.length; i++) {
1544
+ mergeBox(boxes[i]);
1522
1545
  }
1523
- this.sceneBoundingBox = firstBox;
1546
+ }
1547
+
1548
+ if (hasValidBox) {
1549
+ this.sceneBoundingBox = mergedBox;
1550
+ this.refreshSceneBoundingBoxHelper(this.sceneBoundingBoxDebugEnabled);
1524
1551
  return this.sceneBoundingBox;
1525
1552
  }
1526
1553
 
1527
1554
  this.sceneBoundingBox = new this.THREE.Box3();
1555
+ this.refreshSceneBoundingBoxHelper(this.sceneBoundingBoxDebugEnabled);
1528
1556
  return this.sceneBoundingBox;
1529
1557
  },
1558
+ upsertIndexedBox(indexMap, modelId, box) {
1559
+ if (!indexMap || !modelId || !box || !box.isBox3) return null;
1560
+ const key = String(modelId);
1561
+ const existingBox = indexMap.get(key);
1562
+ if (!existingBox) {
1563
+ indexMap.set(key, box);
1564
+ return box;
1565
+ }
1566
+ existingBox.union(box);
1567
+ return existingBox;
1568
+ },
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;
1579
+ if (!targetBox.userData) {
1580
+ targetBox.userData = sourceUserData;
1581
+ return;
1582
+ }
1583
+
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);
1591
+ targetUserData.visible = targetUserData.visible !== false || sourceUserData.visible !== false;
1592
+ targetUserData.sourceVisible =
1593
+ targetUserData.sourceVisible !== false || sourceUserData.sourceVisible !== false;
1594
+ targetUserData.transparent = !!targetUserData.transparent && !!sourceUserData.transparent;
1595
+ targetUserData.mergedBoxCount = (targetUserData.mergedBoxCount || 1) + 1;
1596
+ // 多图元模型分别参与遮挡测试,任一图元可见时都会保留整个模型。
1597
+ targetUserData.occlusionEnabled =
1598
+ !!targetUserData.occlusionEnabled && !!sourceUserData.occlusionEnabled;
1599
+ targetUserData.occlusionProxyBoxes = targetProxyBoxes.concat(sourceProxyBoxes);
1600
+
1601
+ const targetObbs = Array.isArray(targetUserData.collisionObbs)
1602
+ ? targetUserData.collisionObbs
1603
+ : [];
1604
+ const sourceObbs = Array.isArray(sourceUserData.collisionObbs)
1605
+ ? sourceUserData.collisionObbs
1606
+ : [];
1607
+ if (sourceObbs.length > 0) {
1608
+ targetUserData.collisionObbs = targetObbs.concat(sourceObbs);
1609
+ }
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
+ },
1665
+ upsertModelBox(modelId, box) {
1666
+ if (!this._boxIndex || !modelId || !box || !box.isBox3) return;
1667
+ const key = String(modelId);
1668
+ const existingBox = this._boxIndex.get(key);
1669
+ if (!existingBox) {
1670
+ this._boxIndex.set(key, box);
1671
+ return;
1672
+ }
1673
+ this.mergeModelBoxUserData(existingBox, box);
1674
+ existingBox.union(box);
1675
+ },
1530
1676
  setBoxIndex(boxJson, documentId, isAdd = true) {
1531
1677
  if (!this._boxIndex) this._boxIndex = new Map();
1678
+ if (!this._sceneBoundsIndex) this._sceneBoundsIndex = new Map();
1532
1679
  if (!documentId) {
1533
1680
  this._boxIndex.clear();
1681
+ this._sceneBoundsIndex.clear();
1534
1682
  if (this.noObserver && this.noObserver.documentModelIds) {
1535
1683
  this.noObserver.documentModelIds.clear();
1536
1684
  }
1685
+ if (this.noObserver && this.noObserver.documentSceneBoundsIds) {
1686
+ this.noObserver.documentSceneBoundsIds.clear();
1687
+ }
1537
1688
  this.hasExecutedCentering = false;
1538
1689
  this.buildOctreeFromBoxIndex();
1539
1690
  return;
@@ -1542,19 +1693,35 @@ export default {
1542
1693
  if (isAdd && boxJson) {
1543
1694
  const arr = boxJson ? boxJson : [];
1544
1695
  const modelIds = new Set();
1696
+ const sceneBoundIds = new Set();
1545
1697
  const validGeomTypes = new Set(Object.values(GEOM_TYPES));
1546
1698
  for (let i = 0; i < arr.length; i++) {
1547
1699
  const it = arr[i];
1548
1700
  if (!validGeomTypes.has(it.geomType)) {
1549
1701
  continue;
1550
1702
  }
1551
- if (it.obb.length == 0) {
1703
+ const minSource = it.min;
1704
+ const maxSource = it.max;
1705
+ const hasValidMinMax =
1706
+ minSource &&
1707
+ maxSource &&
1708
+ minSource.length >= 3 &&
1709
+ maxSource.length >= 3 &&
1710
+ Number.isFinite(minSource[0]) &&
1711
+ Number.isFinite(minSource[1]) &&
1712
+ Number.isFinite(minSource[2]) &&
1713
+ Number.isFinite(maxSource[0]) &&
1714
+ Number.isFinite(maxSource[1]) &&
1715
+ Number.isFinite(maxSource[2]);
1716
+ if (!hasValidMinMax) {
1552
1717
  continue;
1553
1718
  }
1719
+ const obbData = it.obb || [];
1720
+ const modelId = String(it.id);
1554
1721
 
1555
1722
  // 使用新的 min/max 数据结构
1556
- const min = new this.THREE.Vector3(it.min[0], it.min[1], it.min[2]);
1557
- const max = new this.THREE.Vector3(it.max[0], it.max[1], it.max[2]);
1723
+ const min = new this.THREE.Vector3(minSource[0], minSource[1], minSource[2]);
1724
+ const max = new this.THREE.Vector3(maxSource[0], maxSource[1], maxSource[2]);
1558
1725
 
1559
1726
  const sourceBox = new this.THREE.Box3(min.clone(), max.clone());
1560
1727
  // 八叉树仍使用 AABB 粗筛,最终碰撞使用 sourceBox 派生的 OBB 精筛
@@ -1588,10 +1755,18 @@ export default {
1588
1755
  boxThree.applyMatrix4(collisionMatrix);
1589
1756
  }
1590
1757
 
1758
+ this.upsertIndexedBox(this._sceneBoundsIndex, modelId, boxThree.clone());
1759
+ sceneBoundIds.add(modelId);
1760
+
1761
+ // 没有 OBB 数据的二维图形只参与全场景范围,不纳入碰撞和遮挡索引。
1762
+ if (obbData.length === 0) {
1763
+ continue;
1764
+ }
1765
+
1591
1766
  const userData = {
1592
1767
  flag: it.flag || 1, // 默认为 1
1593
1768
  geomType: it.geomType,
1594
- obbData: it.obb, // 存储原始 OBB 数据
1769
+ obbData, // 存储原始 OBB 数据
1595
1770
  transparent: it.transp > 0,
1596
1771
  sourceVisible: it.visible === false ? false : true,
1597
1772
  visible: it.visible === false ? false : true, // 默认显示
@@ -1605,8 +1780,7 @@ export default {
1605
1780
  // userData.matrix = new this.THREE.Matrix4().identity();
1606
1781
  }
1607
1782
  // 如果是 flag=3,解析并存储中心点、半轴长、旋转矩阵
1608
- else if (it.flag === 3 && it.obb && it.obb.length === 15) {
1609
- const obbData = it.obb;
1783
+ else if (it.flag === 3 && obbData.length === 15) {
1610
1784
  const center = new this.THREE.Vector3(obbData[0], obbData[1], obbData[2]);
1611
1785
  const halfSize = new this.THREE.Vector3(obbData[3], obbData[4], obbData[5]);
1612
1786
  const rotation = new this.THREE.Matrix3().fromArray(obbData.slice(6, 15));
@@ -1629,27 +1803,26 @@ export default {
1629
1803
  halfSize: halfSize,
1630
1804
  };
1631
1805
 
1632
- const collisionObb = new OBB(center.clone(), halfSize.clone(), rotation.clone());
1633
- if (collisionMatrix) {
1634
- collisionObb.applyMatrix4(collisionMatrix);
1635
- }
1806
+ const collisionObb = this._createCollisionObb(
1807
+ center,
1808
+ halfSize,
1809
+ rotation,
1810
+ collisionMatrix
1811
+ );
1636
1812
  userData.collisionObbs = [collisionObb];
1637
1813
  }
1638
1814
  // 如果是 flag=2,解析并存储多个子包围盒 (min, max)
1639
- else if (it.flag === 2 && it.obb && it.obb.length > 0) {
1815
+ else if (it.flag === 2 && obbData.length > 0) {
1640
1816
  const subBoxes = [];
1641
1817
  const collisionObbs = [];
1642
- for (let k = 0; k < it.obb.length; k += 6) {
1643
- if (k + 5 < it.obb.length) {
1644
- const min = new this.THREE.Vector3(it.obb[k], it.obb[k + 1], it.obb[k + 2]);
1645
- const max = new this.THREE.Vector3(it.obb[k + 3], it.obb[k + 4], it.obb[k + 5]);
1818
+ for (let k = 0; k < obbData.length; k += 6) {
1819
+ if (k + 5 < obbData.length) {
1820
+ const min = new this.THREE.Vector3(obbData[k], obbData[k + 1], obbData[k + 2]);
1821
+ const max = new this.THREE.Vector3(obbData[k + 3], obbData[k + 4], obbData[k + 5]);
1646
1822
  subBoxes.push({ min, max });
1647
1823
 
1648
1824
  const subBox = new this.THREE.Box3(min.clone(), max.clone());
1649
- const collisionObb = new OBB().fromBox3(subBox);
1650
- if (collisionMatrix) {
1651
- collisionObb.applyMatrix4(collisionMatrix);
1652
- }
1825
+ const collisionObb = this._createCollisionObbFromBox(subBox, collisionMatrix);
1653
1826
  collisionObbs.push(collisionObb);
1654
1827
  }
1655
1828
  }
@@ -1660,20 +1833,17 @@ export default {
1660
1833
  }
1661
1834
 
1662
1835
  if (!userData.collisionObbs || userData.collisionObbs.length === 0) {
1663
- const collisionObb = new OBB().fromBox3(sourceBox);
1664
- if (collisionMatrix) {
1665
- collisionObb.applyMatrix4(collisionMatrix);
1666
- }
1836
+ const collisionObb = this._createCollisionObbFromBox(sourceBox, collisionMatrix);
1667
1837
  userData.collisionObbs = [collisionObb];
1668
1838
  }
1669
1839
 
1670
1840
  boxThree.userData = userData;
1671
1841
 
1672
- const modelId = String(it.id);
1673
- this._boxIndex.set(modelId, boxThree);
1842
+ this.upsertModelBox(modelId, boxThree);
1674
1843
  modelIds.add(modelId);
1675
1844
  }
1676
1845
  this.noObserver.documentModelIds.set(documentId, modelIds);
1846
+ this.noObserver.documentSceneBoundsIds.set(documentId, sceneBoundIds);
1677
1847
  } else {
1678
1848
  const modelIds = this.noObserver.documentModelIds.get(documentId);
1679
1849
  if (modelIds) {
@@ -1687,14 +1857,38 @@ export default {
1687
1857
  this.hasExecutedCentering = false;
1688
1858
  }
1689
1859
  }
1860
+ const sceneBoundIds =
1861
+ this.noObserver.documentSceneBoundsIds &&
1862
+ this.noObserver.documentSceneBoundsIds.get(documentId);
1863
+ if (sceneBoundIds) {
1864
+ sceneBoundIds.forEach(id => {
1865
+ this._sceneBoundsIndex.delete(id);
1866
+ });
1867
+ this.noObserver.documentSceneBoundsIds.delete(documentId);
1868
+ }
1690
1869
  }
1691
1870
  this.buildOctreeFromBoxIndex();
1692
1871
  this.refreshCollisionObbDebug();
1872
+ this.refreshOcclusionBoundsDebug();
1693
1873
  console.log('time end', Date.now());
1694
1874
  },
1695
1875
  shouldApplyOcclusionByBox(box) {
1696
1876
  return !!(box && box.userData && box.userData.occlusionEnabled);
1697
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
+ },
1698
1892
  tryInitialCenterAfterBoundsReady() {
1699
1893
  if (this.hasExecutedCentering || this.userInteracting) return;
1700
1894
  if (
@@ -1703,7 +1897,9 @@ export default {
1703
1897
  this.sceneBoundingBox.isEmpty()
1704
1898
  )
1705
1899
  return;
1706
- if (!this._boxIndex || this._boxIndex.size === 0) return;
1900
+ const hasBoxIndex = this._boxIndex && this._boxIndex.size > 0;
1901
+ const hasSceneBoundsIndex = this._sceneBoundsIndex && this._sceneBoundsIndex.size > 0;
1902
+ if (!hasBoxIndex && !hasSceneBoundsIndex) return;
1707
1903
 
1708
1904
  this.smartModelCenter(this.sceneBoundingBox, 0);
1709
1905
  },
@@ -1805,6 +2001,53 @@ export default {
1805
2001
  }
1806
2002
  node.items.push({ id, box });
1807
2003
  },
2004
+ _buildCollisionOctreeFromEntries(entries) {
2005
+ if (!Array.isArray(entries) || entries.length === 0) {
2006
+ return null;
2007
+ }
2008
+
2009
+ let minX = Infinity;
2010
+ let minY = Infinity;
2011
+ let minZ = Infinity;
2012
+ let maxX = -Infinity;
2013
+ let maxY = -Infinity;
2014
+ let maxZ = -Infinity;
2015
+
2016
+ entries.forEach(item => {
2017
+ const box = item && item.box;
2018
+ if (!box || !box.isBox3) return;
2019
+ const mn = box.min;
2020
+ const mx = box.max;
2021
+ if (mn.x < minX) minX = mn.x;
2022
+ if (mn.y < minY) minY = mn.y;
2023
+ if (mn.z < minZ) minZ = mn.z;
2024
+ if (mx.x > maxX) maxX = mx.x;
2025
+ if (mx.y > maxY) maxY = mx.y;
2026
+ if (mx.z > maxZ) maxZ = mx.z;
2027
+ });
2028
+
2029
+ if (minX === Infinity) {
2030
+ return null;
2031
+ }
2032
+
2033
+ const rootBox = new this.THREE.Box3(
2034
+ new this.THREE.Vector3(minX, minY, minZ),
2035
+ new this.THREE.Vector3(maxX, maxY, maxZ)
2036
+ );
2037
+ const root = {
2038
+ box: rootBox,
2039
+ items: [],
2040
+ children: null,
2041
+ depth: 0,
2042
+ };
2043
+
2044
+ entries.forEach(item => {
2045
+ if (!item || !item.box) return;
2046
+ this._octreeInsert(root, String(item.id), item.box);
2047
+ });
2048
+
2049
+ return root;
2050
+ },
1808
2051
  _isCollisionBoxAvailable(box) {
1809
2052
  if (!box || !box.isBox3 || box.isEmpty()) return false;
1810
2053
  if (box.userData && box.userData.visible === false) return false;
@@ -1935,7 +2178,8 @@ export default {
1935
2178
  _buildCollisionPairMap() {
1936
2179
  const pairMap = new Map();
1937
2180
  const collisionPairs = this.getOctreeCollisionPairs();
1938
- const pairs = collisionPairs && Array.isArray(collisionPairs.pairs) ? collisionPairs.pairs : [];
2181
+ const pairs =
2182
+ collisionPairs && Array.isArray(collisionPairs.pairs) ? collisionPairs.pairs : [];
1939
2183
  for (let i = 0; i < pairs.length; i++) {
1940
2184
  const pair = pairs[i];
1941
2185
  if (!pair) continue;
@@ -1956,20 +2200,15 @@ export default {
1956
2200
 
1957
2201
  this._clearCollisionObbDebugGroup(false);
1958
2202
  const pairMap = this._buildCollisionPairMap();
1959
- if (pairMap.size === 0) return;
2203
+ if (!this._boxIndex || this._boxIndex.size === 0) return;
1960
2204
 
1961
- pairMap.forEach((relatedIds, modelId) => {
1962
- const box =
1963
- this._boxIndex && this._boxIndex.has(String(modelId))
1964
- ? this._boxIndex.get(String(modelId))
1965
- : this._boxIndex && this._boxIndex.has(modelId)
1966
- ? this._boxIndex.get(modelId)
1967
- : null;
2205
+ this._boxIndex.forEach((box, modelId) => {
1968
2206
  if (!this._isCollisionBoxAvailable(box)) return;
1969
2207
 
1970
2208
  const obbs = this._getCollisionObbs(box);
1971
2209
  if (!obbs.length) return;
1972
2210
 
2211
+ const relatedIds = pairMap.get(String(modelId));
1973
2212
  const isColliding = relatedIds && relatedIds.size > 0;
1974
2213
  const color = isColliding ? 0xff3b30 : 0x36cfc9;
1975
2214
  for (let i = 0; i < obbs.length; i++) {
@@ -1995,6 +2234,165 @@ export default {
1995
2234
  this.hideCollisionObbDebug();
1996
2235
  }
1997
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
+ },
1998
2396
  _getObbAxisInfo(obb) {
1999
2397
  const xAxis = new this.THREE.Vector3();
2000
2398
  const yAxis = new this.THREE.Vector3();
@@ -2064,16 +2462,15 @@ export default {
2064
2462
  const obbsB = this._getCollisionObbs(boxB);
2065
2463
  for (let i = 0; i < obbsA.length; i++) {
2066
2464
  for (let j = 0; j < obbsB.length; j++) {
2067
- if (this._getObbMinPenetration(obbsA[i], obbsB[j]) > COLLISION_PENETRATION_EPSILON) {
2465
+ if (this._getObbMinPenetration(obbsA[i], obbsB[j]) >= COLLISION_CONTACT_EPSILON) {
2068
2466
  return true;
2069
2467
  }
2070
2468
  }
2071
2469
  }
2072
2470
  return false;
2073
2471
  },
2074
- _queryOctreeByBox(box) {
2472
+ _queryOctreeByBoxWithRoot(root, box) {
2075
2473
  const results = [];
2076
- const root = this._octree;
2077
2474
  if (!root || !this._isCollisionBoxAvailable(box)) return results;
2078
2475
 
2079
2476
  const stack = [root];
@@ -2099,25 +2496,88 @@ export default {
2099
2496
 
2100
2497
  return results;
2101
2498
  },
2102
- getOctreeCollisionPairs() {
2499
+ _queryOctreeByBox(box) {
2500
+ return this._queryOctreeByBoxWithRoot(this._octree, box);
2501
+ },
2502
+ getOctreeCollisionPairs(options = {}) {
2103
2503
  const pairs = [];
2104
2504
  if (!this._boxIndex || this._boxIndex.size === 0) return { pairs };
2105
2505
 
2106
- if (!this._octree) {
2107
- this.buildOctreeFromBoxIndex();
2506
+ const normalizeIds = value =>
2507
+ Array.isArray(value)
2508
+ ? Array.from(
2509
+ new Set(
2510
+ value
2511
+ .map(item => (item === null || item === undefined ? '' : String(item)))
2512
+ .filter(item => item !== '')
2513
+ )
2514
+ )
2515
+ : [];
2516
+
2517
+ const getEntriesByIds = ids => {
2518
+ const entries = [];
2519
+ const idSet = new Set(ids);
2520
+ this._boxIndex.forEach((box, id) => {
2521
+ const key = String(id);
2522
+ if (idSet.has(key)) {
2523
+ entries.push({
2524
+ id: key,
2525
+ box,
2526
+ });
2527
+ }
2528
+ });
2529
+ return entries;
2530
+ };
2531
+
2532
+ const sourceIds = normalizeIds(options.sourceIds);
2533
+ const targetIds = normalizeIds(options.targetIds);
2534
+ const includeIds = Array.isArray(options.includeIds) ? normalizeIds(options.includeIds) : [];
2535
+
2536
+ let root = this._octree;
2537
+ let sourceEntries = [];
2538
+ let dedupePairKeySet = null;
2539
+
2540
+ if (sourceIds.length > 0 && targetIds.length > 0) {
2541
+ const targetEntries = getEntriesByIds(targetIds);
2542
+ sourceEntries = getEntriesByIds(sourceIds);
2543
+ if (sourceEntries.length === 0 || targetEntries.length === 0) return { pairs };
2544
+ root = this._buildCollisionOctreeFromEntries(targetEntries);
2545
+ if (!root) return { pairs };
2546
+ dedupePairKeySet = new Set();
2547
+ } else if (includeIds.length > 0) {
2548
+ sourceEntries = getEntriesByIds(includeIds);
2549
+ if (sourceEntries.length === 0) return { pairs };
2550
+ root = this._buildCollisionOctreeFromEntries(sourceEntries);
2551
+ if (!root) return { pairs };
2552
+ } else {
2553
+ if (!this._octree) {
2554
+ this.buildOctreeFromBoxIndex();
2555
+ }
2556
+ if (!this._octree) return { pairs };
2557
+ this._boxIndex.forEach((box, id) => {
2558
+ sourceEntries.push({
2559
+ id: String(id),
2560
+ box,
2561
+ });
2562
+ });
2108
2563
  }
2109
- if (!this._octree) return { pairs };
2110
2564
 
2111
- this._boxIndex.forEach((boxA, idA) => {
2565
+ sourceEntries.forEach(({ id: idA, box: boxA }) => {
2112
2566
  if (!this._isCollisionBoxAvailable(boxA)) return;
2113
2567
 
2114
- const hits = this._queryOctreeByBox(boxA);
2568
+ const hits = this._queryOctreeByBoxWithRoot(root, boxA);
2115
2569
  for (let i = 0; i < hits.length; i++) {
2116
2570
  const item = hits[i];
2117
2571
  if (!item || String(item.id) === String(idA)) continue;
2118
2572
  const boxB = item.box;
2119
2573
  if (!this._isCollisionBoxMatched(boxA, boxB)) continue;
2120
2574
 
2575
+ if (dedupePairKeySet) {
2576
+ const pairKey = `${String(idA)}__${String(item.id)}`;
2577
+ if (dedupePairKeySet.has(pairKey)) continue;
2578
+ dedupePairKeySet.add(pairKey);
2579
+ }
2580
+
2121
2581
  pairs.push({
2122
2582
  idA: this._formatCollisionPairId(idA),
2123
2583
  idB: this._formatCollisionPairId(item.id),
@@ -2571,7 +3031,8 @@ export default {
2571
3031
  /**
2572
3032
  * 执行视椎体裁切,清理视椎体外的InstancedMesh实例
2573
3033
  */
2574
- async performFrustumCulling() {
3034
+ async performFrustumCulling(options = {}) {
3035
+ const skipUnload = !!(options && options.skipUnload);
2575
3036
  const modelState = this.noObserver
2576
3037
  ? this.noObserver.modelStateManager
2577
3038
  : this.modelStateManager;
@@ -2613,7 +3074,12 @@ export default {
2613
3074
  visibleIdSet.add(this.formatModelId(hits[i].modelId));
2614
3075
  continue;
2615
3076
  }
2616
- candidates.push(hits[i]);
3077
+ this.appendOcclusionCandidates(
3078
+ candidates,
3079
+ hits[i].modelId,
3080
+ hits[i].box,
3081
+ globalFrustum
3082
+ );
2617
3083
  }
2618
3084
  } else if (this._boxIndex && this._boxIndex.size > 0) {
2619
3085
  this._boxIndex.forEach((box, modelId) => {
@@ -2624,7 +3090,7 @@ export default {
2624
3090
  visibleIdSet.add(this.formatModelId(modelId));
2625
3091
  return;
2626
3092
  }
2627
- candidates.push({ modelId, box });
3093
+ this.appendOcclusionCandidates(candidates, modelId, box, globalFrustum);
2628
3094
  }
2629
3095
  });
2630
3096
  }
@@ -2740,6 +3206,7 @@ export default {
2740
3206
  const tBuild0 = performance.now();
2741
3207
 
2742
3208
  const flag1Items = [];
3209
+ const flag2Items = [];
2743
3210
  const flag3Items = [];
2744
3211
 
2745
3212
  let globalIdx = 1;
@@ -2753,6 +3220,8 @@ export default {
2753
3220
  const flag = (c.box.userData && c.box.userData.flag) || 1;
2754
3221
  if (flag === 3 && c.box.userData && c.box.userData.obb) {
2755
3222
  flag3Items.push({ c, idx });
3223
+ } else if (flag === 2) {
3224
+ flag2Items.push({ c, idx });
2756
3225
  } else if (flag === 1) {
2757
3226
  flag1Items.push({ c, idx });
2758
3227
  }
@@ -2769,7 +3238,8 @@ export default {
2769
3238
  const _tempMatA = occObjs._tempMatA || (occObjs._tempMatA = new this.THREE.Matrix4());
2770
3239
 
2771
3240
  let boxes = occObjs.boxes;
2772
- const boxCount = flag3Items.length;
3241
+ const boxItems = flag3Items.concat(flag2Items);
3242
+ const boxCount = boxItems.length;
2773
3243
  if (boxCount > 0) {
2774
3244
  const needCapacity = boxCount;
2775
3245
  const capacity = boxes && typeof boxes.capacity === 'number' ? boxes.capacity : 0;
@@ -2799,19 +3269,26 @@ export default {
2799
3269
  }
2800
3270
  boxes.mesh.count = boxCount;
2801
3271
  for (let i = 0; i < boxCount; i++) {
2802
- const item = flag3Items[i];
2803
- const { matrix, halfSize } = item.c.box.userData.obb;
3272
+ const item = boxItems[i];
2804
3273
  const idx = item.idx;
2805
3274
  const r = (idx & 255) / 255;
2806
3275
  const g = ((idx >> 8) & 255) / 255;
2807
3276
  const b = ((idx >> 16) & 255) / 255;
2808
3277
  _tempColor.setRGB(r, g, b);
2809
3278
 
2810
- _tempScale.copy(halfSize).multiplyScalar(2);
2811
- _tempMatrix.copy(matrix);
2812
- _tempMatrix.scale(_tempScale);
2813
- if (typeof this.bizToThreeMatrix !== 'undefined' && this.bizToThreeMatrix) {
2814
- _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);
2815
3292
  }
2816
3293
 
2817
3294
  boxes.mesh.setMatrixAt(i, _tempMatrix);
@@ -3254,12 +3731,12 @@ export default {
3254
3731
  this.renderer.toneMapping = prevToneMapping;
3255
3732
  } catch (e) {
3256
3733
  for (let i = 0; i < candidates.length; i++) {
3257
- visibleIdSet.add(candidates[i].modelId);
3734
+ visibleIdSet.add(this.formatModelId(candidates[i].modelId));
3258
3735
  }
3259
3736
  }
3260
3737
  } else {
3261
3738
  for (let i = 0; i < candidates.length; i++) {
3262
- visibleIdSet.add(candidates[i].modelId);
3739
+ visibleIdSet.add(this.formatModelId(candidates[i].modelId));
3263
3740
  }
3264
3741
  }
3265
3742
  visibleIds.length = 0;
@@ -3268,37 +3745,43 @@ export default {
3268
3745
  const parentsToCheck = new Set();
3269
3746
  this.scene.traverse(child => {
3270
3747
  if (!child.isInstancedMesh) return;
3271
- // 如果复用数量等于大于2个,则跳过裁剪和剔除处理(始终保留)
3272
- if (child.count >= 2) {
3273
- let instancesMap =
3274
- child && child.userData && child.userData.instancesMap instanceof Map
3275
- ? child.userData.instancesMap
3276
- : child.parent &&
3277
- child.parent.userData &&
3278
- child.parent.userData.instancesMap instanceof Map
3279
- ? child.parent.userData.instancesMap
3280
- : 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;
3281
3756
 
3757
+ // 多实例复用同一个 InstancedMesh 时,仅在全部实例都不可见时整体卸载。
3758
+ if (child.count >= 2) {
3282
3759
  if (instancesMap) {
3283
3760
  let allInactive = true;
3284
3761
  const modelIds = Array.from(instancesMap.keys());
3762
+ let unloadModelId = null;
3285
3763
 
3286
3764
  // 第一遍遍历:检查是否有任何一个实例处于活跃状态(在视锥体内且可见)
3287
3765
  for (const modelId of modelIds) {
3288
3766
  // 检查已取消勾选模型进行卸载
3289
- const documentId = modelId.split(':')[1];
3290
- if (!this.noObserver.documentModelIds.get(documentId)) {
3291
- toUnload.push({ modelId, child });
3292
- 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;
3293
3772
  }
3294
3773
 
3295
- 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
+ }
3296
3784
 
3297
- const instanceInfo = instancesMap.get(modelId);
3298
- const instanceIndex =
3299
- instanceInfo && typeof instanceInfo.instanceIndex === 'number'
3300
- ? instanceInfo.instanceIndex
3301
- : null;
3302
3785
  const box =
3303
3786
  this._boxIndex && this._boxIndex.has(String(modelId))
3304
3787
  ? this._boxIndex.get(String(modelId))
@@ -3306,16 +3789,19 @@ export default {
3306
3789
  ? this._boxIndex.get(modelId)
3307
3790
  : null;
3308
3791
  if (!this.shouldApplyOcclusionByBox(box)) {
3792
+ if (!skipUnload) this.setInstanceCullingVisible(child, modelId, true);
3309
3793
  allInactive = false;
3794
+ toLoadSet.delete(formattedModelId);
3310
3795
  continue;
3311
3796
  }
3312
3797
 
3313
- const inFrustum = this.isModelInFrustum(child, instanceIndex, globalFrustum);
3314
- const modelInVisible = toLoadSet.has(modelId);
3315
- // console.log('modelId', modelId, 'inFrustum', inFrustum, 'modelInVisible', modelInVisible)
3798
+ const modelInVisible = visibleIdSet.has(formattedModelId);
3799
+ toLoadSet.delete(formattedModelId);
3316
3800
 
3317
- // 只要有一个实例在视锥体内且在可见列表中,就不整体卸载
3318
- if (inFrustum || modelInVisible) {
3801
+ // 可见集合已经聚合了同一模型的全部图元,任一图元可见时都保留该模型实例。
3802
+ const isActive = modelInVisible;
3803
+ if (!skipUnload) this.setInstanceCullingVisible(child, modelId, isActive);
3804
+ if (isActive) {
3319
3805
  allInactive = false;
3320
3806
  }
3321
3807
  }
@@ -3329,20 +3815,24 @@ export default {
3329
3815
  ? this._boxIndex.get(modelId)
3330
3816
  : null;
3331
3817
  if (!this.shouldApplyOcclusionByBox(box)) {
3332
- if (toLoadSet.has(modelId)) {
3333
- toLoadSet.delete(modelId);
3334
- }
3818
+ toLoadSet.delete(this.formatModelId(modelId));
3335
3819
  continue;
3336
3820
  }
3337
3821
  // 无论是否卸载,只要模型已在场景中,就从待加载集合中移除
3338
- if (toLoadSet.has(modelId)) {
3339
- toLoadSet.delete(modelId);
3340
- }
3822
+ toLoadSet.delete(this.formatModelId(modelId));
3341
3823
 
3342
- // 仅当所有实例都不活跃时,才执行卸载
3343
- if (!this.firstPerSign && !this.roaming && allInactive) {
3344
- toUnload.push({ modelId, child });
3345
- }
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 });
3346
3836
  }
3347
3837
  }
3348
3838
  // if (toLoadSet.has(modelId)) {
@@ -3351,10 +3841,18 @@ export default {
3351
3841
  return;
3352
3842
  }
3353
3843
  const modelId =
3354
- 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
3355
3847
  ? child.parent.userData.instanceId
3356
3848
  : child.uuid;
3357
- 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;
3358
3856
  const box =
3359
3857
  this._boxIndex && this._boxIndex.has(String(modelId))
3360
3858
  ? this._boxIndex.get(String(modelId))
@@ -3362,18 +3860,13 @@ export default {
3362
3860
  ? this._boxIndex.get(modelId)
3363
3861
  : null;
3364
3862
  if (!this.shouldApplyOcclusionByBox(box)) {
3365
- if (toLoadSet.has(modelId)) {
3366
- toLoadSet.delete(modelId);
3367
- }
3863
+ toLoadSet.delete(formattedModelId);
3368
3864
  return;
3369
3865
  }
3370
3866
 
3371
- const inFrustum = this.isModelInFrustum(child, null, globalFrustum);
3372
- let modelInVisible = toLoadSet.has(modelId);
3373
- if (modelInVisible) {
3374
- toLoadSet.delete(modelId);
3375
- }
3376
- if (!this.firstPerSign && !this.roaming && (!inFrustum || !modelInVisible)) {
3867
+ const modelInVisible = visibleIdSet.has(formattedModelId);
3868
+ toLoadSet.delete(formattedModelId);
3869
+ if (!skipUnload && !this.firstPerSign && !this.roaming && !modelInVisible) {
3377
3870
  toUnload.push({ modelId, child });
3378
3871
  }
3379
3872
  });
@@ -3491,9 +3984,15 @@ export default {
3491
3984
  * @param {Object} config - 配置项
3492
3985
  * @param {Object} config.modelApi - 模型API接口
3493
3986
  * @param {string} [config.projectId] - 项目ID
3987
+ * @param {Object} [config.cameraControlMoveObserver] - 普通视角运动中触发加载的配置
3988
+ * @param {boolean} [config.cameraControlMoveObserver.enabled=false] - 是否开启普通视角运动中加载
3989
+ * @param {number} [config.cameraControlMoveObserver.interval=500] - 触发间隔,单位毫秒
3990
+ * @param {boolean} [config.enableCameraControlMoveObserver=false] - 是否开启普通视角运动中加载
3991
+ * @param {number} [config.cameraControlMoveInterval=500] - 触发间隔,单位毫秒
3494
3992
  */
3495
3993
  init(config = {}) {
3496
3994
  console.log('init');
3995
+ this.setCameraControlMoveObserverConfig(config);
3497
3996
  this.initStreamLoader(config.modelApi, config);
3498
3997
  },
3499
3998
 
@@ -3920,6 +4419,124 @@ export default {
3920
4419
  this.noObserver.isObserverEnabled = enabled;
3921
4420
  },
3922
4421
 
4422
+ setCameraControlMoveObserverConfig(config = {}) {
4423
+ if (!this.noObserver) return;
4424
+ const nestedConfig = config.cameraControlMoveObserver || {};
4425
+ const enabled =
4426
+ typeof nestedConfig.enabled === 'boolean'
4427
+ ? nestedConfig.enabled
4428
+ : typeof config.enableCameraControlMoveObserver === 'boolean'
4429
+ ? config.enableCameraControlMoveObserver
4430
+ : false;
4431
+ const rawInterval =
4432
+ nestedConfig.interval !== undefined
4433
+ ? nestedConfig.interval
4434
+ : config.cameraControlMoveInterval;
4435
+ const interval = Number(rawInterval);
4436
+ const safeInterval = Number.isFinite(interval) && interval > 0 ? interval : 500;
4437
+
4438
+ this.noObserver.cameraControlMoveObserver = {
4439
+ enabled,
4440
+ interval: safeInterval,
4441
+ };
4442
+ this.resetCameraControlMoveSnapshot(false);
4443
+ },
4444
+
4445
+ isCameraControlMoveObserverEnabled() {
4446
+ const config = this.noObserver && this.noObserver.cameraControlMoveObserver;
4447
+ return !!(config && config.enabled);
4448
+ },
4449
+
4450
+ getCameraControlTarget(target) {
4451
+ if (!target || !this.cameraControls) return false;
4452
+ if (typeof this.cameraControls.getTarget === 'function') {
4453
+ this.cameraControls.getTarget(target);
4454
+ return true;
4455
+ }
4456
+ if (this.cameraControls._target) {
4457
+ target.copy(this.cameraControls._target);
4458
+ return true;
4459
+ }
4460
+ return false;
4461
+ },
4462
+
4463
+ resetCameraControlMoveSnapshot(syncCurrent = false) {
4464
+ if (!this.noObserver) return;
4465
+ if (!this.noObserver._cameraControlMoveState) {
4466
+ this.noObserver._cameraControlMoveState = {
4467
+ position: new this.THREE.Vector3(),
4468
+ quaternion: new this.THREE.Quaternion(),
4469
+ target: new this.THREE.Vector3(),
4470
+ currentTarget: new this.THREE.Vector3(),
4471
+ initialized: false,
4472
+ hasTarget: false,
4473
+ };
4474
+ }
4475
+ const state = this.noObserver._cameraControlMoveState;
4476
+ if (!syncCurrent || !this.camera) {
4477
+ state.initialized = false;
4478
+ state.hasTarget = false;
4479
+ return;
4480
+ }
4481
+
4482
+ this.camera.updateMatrixWorld(true);
4483
+ state.position.copy(this.camera.position);
4484
+ state.quaternion.copy(this.camera.quaternion);
4485
+ state.hasTarget = this.getCameraControlTarget(state.target);
4486
+ state.initialized = true;
4487
+ },
4488
+
4489
+ observeCameraControlMove() {
4490
+ if (
4491
+ !this.noObserver ||
4492
+ !this.noObserver.isObserverEnabled ||
4493
+ !this.isCameraControlMoveObserverEnabled() ||
4494
+ !this.camera ||
4495
+ !this.cameraControls ||
4496
+ typeof this._cameraChangeObserver !== 'function'
4497
+ ) {
4498
+ return;
4499
+ }
4500
+
4501
+ const loadingState = this.noObserver.batchLoadingState || {};
4502
+ const interactionState = loadingState.interactionState || {};
4503
+ const wheelActive = !!interactionState.wheelTimeout;
4504
+ const controlActive = !!this.noObserver.isControlActive;
4505
+ if ((!controlActive && !wheelActive) || this.firstPerSign) {
4506
+ this.resetCameraControlMoveSnapshot(false);
4507
+ return;
4508
+ }
4509
+
4510
+ if (!this.noObserver._cameraControlMoveState) {
4511
+ this.resetCameraControlMoveSnapshot(true);
4512
+ return;
4513
+ }
4514
+
4515
+ const state = this.noObserver._cameraControlMoveState;
4516
+ if (!state.initialized) {
4517
+ this.resetCameraControlMoveSnapshot(true);
4518
+ return;
4519
+ }
4520
+
4521
+ this.camera.updateMatrixWorld(true);
4522
+ const hasTarget = this.getCameraControlTarget(state.currentTarget);
4523
+ const positionMoved = state.position.distanceToSquared(this.camera.position) > 1e-8;
4524
+ const quaternionMoved = 1 - Math.abs(state.quaternion.dot(this.camera.quaternion)) > 1e-10;
4525
+ const targetMoved =
4526
+ hasTarget !== state.hasTarget ||
4527
+ (hasTarget && state.target.distanceToSquared(state.currentTarget) > 1e-8);
4528
+
4529
+ if (!positionMoved && !quaternionMoved && !targetMoved) return;
4530
+
4531
+ state.position.copy(this.camera.position);
4532
+ state.quaternion.copy(this.camera.quaternion);
4533
+ state.hasTarget = hasTarget;
4534
+ if (hasTarget) {
4535
+ state.target.copy(state.currentTarget);
4536
+ }
4537
+ this._cameraChangeObserver('controlMove');
4538
+ },
4539
+
3923
4540
  /**
3924
4541
  * 初始化相机变更观察者 (CameraChangeObserver)
3925
4542
  * 统一监听:用户交互(Wheel/Control)、程序化动画、API调用
@@ -3954,7 +4571,9 @@ export default {
3954
4571
  // 执行视锥体裁剪
3955
4572
  // 第一视角不裁切
3956
4573
  if (this.modelStateManager.frustumCheckEnabled) {
3957
- await this.performFrustumCulling();
4574
+ await this.performFrustumCulling({
4575
+ skipUnload: source === 'controlMove',
4576
+ });
3958
4577
  }
3959
4578
 
3960
4579
  // 执行流式加载请求
@@ -3966,11 +4585,19 @@ export default {
3966
4585
  }
3967
4586
  },
3968
4587
  source => {
3969
- // 漫游模式下,或者第一人称移动时,使用节流策略(每300ms触发一次)
4588
+ // 漫游、第一人称移动、普通视角拖拽/缩放中,使用节流策略避免持续运动被防抖吞掉。
3970
4589
  // 避免在持续运动中因防抖重置定时器而无法触发
3971
4590
  if (source === 'roam' || source === 'firstPersonMove') {
3972
4591
  return { type: 'throttle', limit: 1000 };
3973
4592
  }
4593
+ if (source === 'controlMove') {
4594
+ const config = this.noObserver && this.noObserver.cameraControlMoveObserver;
4595
+ const interval =
4596
+ config && Number.isFinite(config.interval) && config.interval > 0
4597
+ ? config.interval
4598
+ : 500;
4599
+ return { type: 'throttle', limit: interval };
4600
+ }
3974
4601
  return 300; // 默认防抖 300ms
3975
4602
  }
3976
4603
  );
@@ -3984,8 +4611,20 @@ export default {
3984
4611
  this._wheelHandler = res => {
3985
4612
  this.$emit('wheelStart', res);
3986
4613
 
3987
- // 统一交互开始处理
3988
- this.beginInteraction('wheel');
4614
+ const cameraControlMoveEnabled = this.isCameraControlMoveObserverEnabled();
4615
+ const loadingState = this.noObserver
4616
+ ? this.noObserver.batchLoadingState
4617
+ : this.batchLoadingState;
4618
+ const wheelAlreadyActive =
4619
+ loadingState &&
4620
+ loadingState.interactionState &&
4621
+ loadingState.interactionState.wheelTimeout;
4622
+ if (cameraControlMoveEnabled && !wheelAlreadyActive) {
4623
+ this.resetCameraControlMoveSnapshot(true);
4624
+ } else if (!cameraControlMoveEnabled) {
4625
+ // 未开启普通视角运动加载时,保持原有交互中断流程。
4626
+ this.beginInteraction('wheel');
4627
+ }
3989
4628
 
3990
4629
  // 设置滚轮交互的结束检测
3991
4630
  this.scheduleWheelInteractionEnd(res, 100);
@@ -4027,9 +4666,12 @@ export default {
4027
4666
  // 4. 监听器 - Controls (用户拖拽/操作)
4028
4667
  this._onControlStart = res => {
4029
4668
  this.noObserver.isControlActive = true;
4030
-
4031
- // 统一交互开始处理(相机)
4032
- this.beginInteraction('this.camera');
4669
+ if (this.isCameraControlMoveObserverEnabled()) {
4670
+ this.resetCameraControlMoveSnapshot(true);
4671
+ } else {
4672
+ // 未开启普通视角运动加载时,保持原有交互中断流程。
4673
+ this.beginInteraction('this.camera');
4674
+ }
4033
4675
  this.$emit('controlStart', res);
4034
4676
 
4035
4677
  // 记录鼠标按下时的位置
@@ -4046,8 +4688,10 @@ export default {
4046
4688
 
4047
4689
  this.$emit('controlEnd', res);
4048
4690
 
4049
- // 统一交互结束处理(相机)
4050
- this.endInteraction('this.camera', res, { immediateResume: false });
4691
+ if (!this.isCameraControlMoveObserverEnabled()) {
4692
+ // 未开启普通视角运动加载时,保持原有交互中断流程。
4693
+ this.endInteraction('this.camera', res, { immediateResume: false });
4694
+ }
4051
4695
 
4052
4696
  // 记录鼠标抬起时的位置并计算是否发生了拖拽
4053
4697
  const event = res.originalEvent || window.event;
@@ -4076,6 +4720,9 @@ export default {
4076
4720
  // 触发观察者
4077
4721
  this._cameraChangeObserver('control');
4078
4722
  }
4723
+ if (this.isCameraControlMoveObserverEnabled()) {
4724
+ this.resetCameraControlMoveSnapshot(false);
4725
+ }
4079
4726
  };
4080
4727
 
4081
4728
  this.cameraControls.addEventListener('controlstart', this._onControlStart);
@@ -4136,12 +4783,37 @@ export default {
4136
4783
  },
4137
4784
  // 动态设置视角滚轮的距离
4138
4785
  setCameraConfig() {
4786
+ if (
4787
+ !this.sceneBoundingBox ||
4788
+ !this.sceneBoundingBox.isBox3 ||
4789
+ this.sceneBoundingBox.isEmpty()
4790
+ ) {
4791
+ return;
4792
+ }
4139
4793
  // let box3 = new this.THREE.Box3().setFromObject(this.sceneBoundingBox);
4140
4794
  let size = new this.THREE.Vector3();
4141
4795
  this.sceneBoundingBox.getSize(size);
4142
4796
  const maxBorder = Math.max(size.x, size.y, size.z);
4797
+ if (isDebug) {
4798
+ const center = this.sceneBoundingBox.getCenter(new this.THREE.Vector3());
4799
+ console.log('sceneBoundingBox 调试信息', {
4800
+ size: {
4801
+ x: size.x,
4802
+ y: size.y,
4803
+ z: size.z,
4804
+ },
4805
+ center: {
4806
+ x: center.x,
4807
+ y: center.y,
4808
+ z: center.z,
4809
+ },
4810
+ maxBorder,
4811
+ });
4812
+ this.refreshSceneBoundingBoxHelper(this.sceneBoundingBoxDebugEnabled);
4813
+ }
4143
4814
  // this.cameraControls.this.camera.far = maxBorder * 10; // 设置相机的远裁剪面
4144
4815
  this.cameraControls.minDistance = maxBorder * 0.2; // 动态设置视角滚轮的距离
4816
+ // this.cameraControls.minDistance = 1000; // 动态设置视角滚轮的距离
4145
4817
  this.camera.updateProjectionMatrix();
4146
4818
  },
4147
4819
  // 获取mesh的中心点
@@ -4469,8 +5141,19 @@ export default {
4469
5141
  if (this.threeMeasure) {
4470
5142
  this.threeMeasure.updateParams(width, height);
4471
5143
  }
5144
+ this.updateMeshLineResolution(width, height);
4472
5145
  }
4473
5146
  },
5147
+ updateMeshLineResolution(width, height) {
5148
+ if (!this.modelGroup || !width || !height) return;
5149
+ this.modelGroup.traverse(child => {
5150
+ const material = child && child.material;
5151
+ if (material && material.type === 'MeshLineMaterial' && material.resolution) {
5152
+ material.resolution.set(width, height);
5153
+ material.needsUpdate = true;
5154
+ }
5155
+ });
5156
+ },
4474
5157
  setModelCenter(target, options = {}) {
4475
5158
  // return
4476
5159
  let box3;
@@ -4644,6 +5327,7 @@ export default {
4644
5327
  children.instanceMatrix.needsUpdate = true;
4645
5328
  });
4646
5329
  this.refreshCollisionObbDebug();
5330
+ this.refreshOcclusionBoundsDebug();
4647
5331
  this.notifyCameraChange(); // 触发场景更新
4648
5332
  break;
4649
5333
  }
@@ -4749,6 +5433,7 @@ export default {
4749
5433
  this.buildOctreeFromBoxIndex();
4750
5434
  }
4751
5435
  this.refreshCollisionObbDebug();
5436
+ this.refreshOcclusionBoundsDebug();
4752
5437
  },
4753
5438
  showOutlinePass(instanceId) {
4754
5439
  let targetObj = this.getObjectByName(instanceId);
@@ -4896,6 +5581,7 @@ export default {
4896
5581
  children.instanceMatrix.needsUpdate = true;
4897
5582
  });
4898
5583
  this.refreshCollisionObbDebug();
5584
+ this.refreshOcclusionBoundsDebug();
4899
5585
  break;
4900
5586
  }
4901
5587
  case 'opacity':
@@ -5317,6 +6003,7 @@ export default {
5317
6003
  }
5318
6004
  this.clearBypassCullingModelIds();
5319
6005
  this.hideCollisionObbDebug();
6006
+ this.hideOcclusionBoundsDebug();
5320
6007
 
5321
6008
  if (this.scene) {
5322
6009
  this.removeTraverse();
@@ -5582,6 +6269,7 @@ export default {
5582
6269
  if (nodeType === 'custom-root') return true;
5583
6270
  if (userData.transformControlHelper === true) return true;
5584
6271
  if (userData.collisionObbDebugHelper === true) return true;
6272
+ if (userData.occlusionBoundsDebugHelper === true) return true;
5585
6273
  if (object.isCamera || object.isLight) return true;
5586
6274
  if (typeof object.type === 'string' && /Helper$/i.test(object.type)) return true;
5587
6275
  return object.type === 'CSS2DObject' || object.type === 'TransformControlsRoot';
@@ -6545,6 +7233,7 @@ export default {
6545
7233
  });
6546
7234
  }
6547
7235
  this.cameraControls.enabled && this.cameraControls.update(this.timeStamp);
7236
+ this.observeCameraControlMove();
6548
7237
  // 计算相机近裁面到包围盒当前面的距离
6549
7238
  this.cameraTrack();
6550
7239
  this.firstPerspective();
@@ -6692,6 +7381,7 @@ export default {
6692
7381
  object.userData.instancesMap instanceof Map &&
6693
7382
  object.userData.instancesMap.get(instanceId);
6694
7383
  if (instanceInfo && instanceInfo.visible === false) return false;
7384
+ if (instanceInfo && instanceInfo.cullingVisible === false) return false;
6695
7385
 
6696
7386
  const box =
6697
7387
  this._boxIndex && this._boxIndex.has(String(instanceId))
@@ -6755,6 +7445,13 @@ export default {
6755
7445
  const loadingState = this.noObserver
6756
7446
  ? this.noObserver.batchLoadingState
6757
7447
  : this.batchLoadingState;
7448
+ const rect = this.renderer
7449
+ ? this.renderer.domElement.getBoundingClientRect()
7450
+ : { width: window.innerWidth, height: window.innerHeight };
7451
+ options.meshLineResolution = {
7452
+ width: rect.width || window.innerWidth || 1,
7453
+ height: rect.height || window.innerHeight || 1,
7454
+ };
6758
7455
  // 如果已经在加载中,先停止之前的加载
6759
7456
  if (loadingState.isLoading) {
6760
7457
  this.stopBatchLoading('restart');
@@ -7318,8 +8015,15 @@ export default {
7318
8015
 
7319
8016
  loadingState.interactionState.wheelTimeout = setTimeout(() => {
7320
8017
  this.$emit('wheelEnd', event);
7321
- this.endInteraction('wheel', event, { immediateResume: false });
7322
8018
  loadingState.interactionState.wheelTimeout = null;
8019
+ if (this.isCameraControlMoveObserverEnabled()) {
8020
+ this.resetCameraControlMoveSnapshot(false);
8021
+ if (typeof this._cameraChangeObserver === 'function') {
8022
+ this._cameraChangeObserver('wheel');
8023
+ }
8024
+ } else {
8025
+ this.endInteraction('wheel', event, { immediateResume: false });
8026
+ }
7323
8027
  }, delay);
7324
8028
  },
7325
8029
 
@@ -7347,9 +8051,87 @@ export default {
7347
8051
  /**
7348
8052
  * 隐藏场景包围盒
7349
8053
  */
8054
+ refreshSceneBoundingBoxHelper(forceVisible = this.boundingBoxVisible) {
8055
+ if (this.sceneBoundingBoxHelper && this.scene) {
8056
+ this.scene.remove(this.sceneBoundingBoxHelper);
8057
+ this.sceneBoundingBoxHelper.geometry && this.sceneBoundingBoxHelper.geometry.dispose();
8058
+ if (Array.isArray(this.sceneBoundingBoxHelper.material)) {
8059
+ this.sceneBoundingBoxHelper.material.forEach(material => {
8060
+ material && material.dispose && material.dispose();
8061
+ });
8062
+ } else if (
8063
+ this.sceneBoundingBoxHelper.material &&
8064
+ this.sceneBoundingBoxHelper.material.dispose
8065
+ ) {
8066
+ this.sceneBoundingBoxHelper.material.dispose();
8067
+ }
8068
+ this.sceneBoundingBoxHelper = null;
8069
+ }
8070
+
8071
+ if (
8072
+ !forceVisible ||
8073
+ !this.scene ||
8074
+ !this.sceneBoundingBox ||
8075
+ !this.sceneBoundingBox.isBox3 ||
8076
+ this.sceneBoundingBox.isEmpty()
8077
+ ) {
8078
+ this.boundingBoxVisible = false;
8079
+ return false;
8080
+ }
8081
+
8082
+ const helper = new this.THREE.Box3Helper(this.sceneBoundingBox, 0x00ff88);
8083
+ helper.name = 'sceneBoundingBoxHelper';
8084
+ helper.renderOrder = 999;
8085
+ if (helper.material) {
8086
+ helper.material.depthTest = false;
8087
+ helper.material.transparent = true;
8088
+ helper.material.opacity = 0.9;
8089
+ }
8090
+ this.scene.add(helper);
8091
+ this.sceneBoundingBoxHelper = helper;
8092
+ this.boundingBoxVisible = true;
8093
+ return true;
8094
+ },
8095
+
8096
+ showSceneBoundingBox() {
8097
+ this.sceneBoundingBoxDebugEnabled = true;
8098
+ const shown = this.refreshSceneBoundingBoxHelper(true);
8099
+ if (!shown) {
8100
+ console.warn('sceneBoundingBox 无有效范围,无法绘制调试线框');
8101
+ return;
8102
+ }
8103
+
8104
+ const size = this.sceneBoundingBox.getSize(new this.THREE.Vector3());
8105
+ const center = this.sceneBoundingBox.getCenter(new this.THREE.Vector3());
8106
+ console.log('sceneBoundingBox 线框已绘制', {
8107
+ size: {
8108
+ x: size.x,
8109
+ y: size.y,
8110
+ z: size.z,
8111
+ },
8112
+ center: {
8113
+ x: center.x,
8114
+ y: center.y,
8115
+ z: center.z,
8116
+ },
8117
+ });
8118
+ },
8119
+
7350
8120
  hideSceneBoundingBox() {
8121
+ this.sceneBoundingBoxDebugEnabled = false;
7351
8122
  if (this.sceneBoundingBoxHelper && this.scene) {
7352
8123
  this.scene.remove(this.sceneBoundingBoxHelper);
8124
+ this.sceneBoundingBoxHelper.geometry && this.sceneBoundingBoxHelper.geometry.dispose();
8125
+ if (Array.isArray(this.sceneBoundingBoxHelper.material)) {
8126
+ this.sceneBoundingBoxHelper.material.forEach(material => {
8127
+ material && material.dispose && material.dispose();
8128
+ });
8129
+ } else if (
8130
+ this.sceneBoundingBoxHelper.material &&
8131
+ this.sceneBoundingBoxHelper.material.dispose
8132
+ ) {
8133
+ this.sceneBoundingBoxHelper.material.dispose();
8134
+ }
7353
8135
  this.sceneBoundingBoxHelper = null;
7354
8136
  this.boundingBoxVisible = false;
7355
8137
  console.log('场景包围盒已隐藏');
@@ -7360,7 +8142,7 @@ export default {
7360
8142
  * 切换场景包围盒显示状态
7361
8143
  */
7362
8144
  toggleSceneBoundingBox() {
7363
- if (this.boundingBoxVisible) {
8145
+ if (this.sceneBoundingBoxDebugEnabled) {
7364
8146
  this.hideSceneBoundingBox();
7365
8147
  } else {
7366
8148
  this.showSceneBoundingBox();