fl-web-component 2.1.1-beta.2 → 2.1.1-beta.21

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',
@@ -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,28 @@ 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 &&
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
+ },
1207
1253
  isSourceHiddenInstance(instancedMesh, instanceId) {
1208
1254
  const instanceState = this.getInstanceState(instancedMesh, instanceId);
1209
1255
  if (instanceState && instanceState.sourceVisible !== undefined) {
@@ -1469,71 +1515,178 @@ export default {
1469
1515
  updateGlobalSceneBoundingBox() {
1470
1516
  const state = this.noObserver || {};
1471
1517
  const occScene = state.occlusionState && state.occlusionState._occScene;
1518
+ const mergedBox = new this.THREE.Box3();
1519
+ let hasValidBox = false;
1520
+ const mergeBox = box => {
1521
+ if (!box || !box.isBox3 || box.isEmpty()) return;
1522
+ if (!hasValidBox) {
1523
+ mergedBox.copy(box);
1524
+ hasValidBox = true;
1525
+ return;
1526
+ }
1527
+ mergedBox.union(box);
1528
+ };
1472
1529
 
1473
- // 优先使用由 boxIndex 构建出的离屏全场景,作为整体居中与 home 的统一范围
1530
+ // 离屏遮挡场景只包含参与遮挡的模型,需要继续合并场景范围索引中的二维图形范围。
1474
1531
  if (occScene && occScene.children && occScene.children.length > 0) {
1475
1532
  const occBox = new this.THREE.Box3().setFromObject(occScene);
1476
- if (!occBox.isEmpty()) {
1477
- this.sceneBoundingBox = occBox;
1478
- return this.sceneBoundingBox;
1479
- }
1533
+ mergeBox(occBox);
1480
1534
  }
1481
1535
 
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;
1536
+ // sceneBoundsIndex 包含二维和三维图形,是全场景 dolly 距离的主要范围来源。
1537
+ if (this._sceneBoundsIndex && this._sceneBoundsIndex.size > 0) {
1538
+ this._sceneBoundsIndex.forEach(box => {
1539
+ mergeBox(box);
1500
1540
  });
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
1541
  }
1516
1542
 
1517
1543
  const boxes = Array.from((state.sceneBoxes && state.sceneBoxes.values()) || []);
1518
1544
  if (boxes.length > 0) {
1519
- const firstBox = boxes[0].clone();
1520
- for (let i = 1; i < boxes.length; i++) {
1521
- firstBox.union(boxes[i]);
1545
+ for (let i = 0; i < boxes.length; i++) {
1546
+ mergeBox(boxes[i]);
1522
1547
  }
1523
- this.sceneBoundingBox = firstBox;
1548
+ }
1549
+
1550
+ if (hasValidBox) {
1551
+ this.sceneBoundingBox = mergedBox;
1552
+ this.refreshSceneBoundingBoxHelper(this.sceneBoundingBoxDebugEnabled);
1524
1553
  return this.sceneBoundingBox;
1525
1554
  }
1526
1555
 
1527
1556
  this.sceneBoundingBox = new this.THREE.Box3();
1557
+ this.refreshSceneBoundingBoxHelper(this.sceneBoundingBoxDebugEnabled);
1528
1558
  return this.sceneBoundingBox;
1529
1559
  },
1560
+ upsertIndexedBox(indexMap, modelId, box) {
1561
+ if (!indexMap || !modelId || !box || !box.isBox3) return null;
1562
+ const key = String(modelId);
1563
+ const existingBox = indexMap.get(key);
1564
+ if (!existingBox) {
1565
+ indexMap.set(key, box);
1566
+ return box;
1567
+ }
1568
+ existingBox.union(box);
1569
+ return existingBox;
1570
+ },
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;
1581
+ if (!targetBox.userData) {
1582
+ targetBox.userData = sourceUserData;
1583
+ return;
1584
+ }
1585
+
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);
1593
+ targetUserData.visible = targetUserData.visible !== false || sourceUserData.visible !== false;
1594
+ targetUserData.sourceVisible =
1595
+ targetUserData.sourceVisible !== false || sourceUserData.sourceVisible !== false;
1596
+ targetUserData.transparent = !!targetUserData.transparent && !!sourceUserData.transparent;
1597
+ targetUserData.mergedBoxCount = (targetUserData.mergedBoxCount || 1) + 1;
1598
+ // 多图元模型分别参与遮挡测试,任一图元可见时都会保留整个模型。
1599
+ targetUserData.occlusionEnabled =
1600
+ !!targetUserData.occlusionEnabled && !!sourceUserData.occlusionEnabled;
1601
+ targetUserData.occlusionProxyBoxes = targetProxyBoxes.concat(sourceProxyBoxes);
1602
+
1603
+ const targetObbs = Array.isArray(targetUserData.collisionObbs)
1604
+ ? targetUserData.collisionObbs
1605
+ : [];
1606
+ const sourceObbs = Array.isArray(sourceUserData.collisionObbs)
1607
+ ? sourceUserData.collisionObbs
1608
+ : [];
1609
+ if (sourceObbs.length > 0) {
1610
+ targetUserData.collisionObbs = targetObbs.concat(sourceObbs);
1611
+ }
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
+ },
1667
+ upsertModelBox(modelId, box) {
1668
+ if (!this._boxIndex || !modelId || !box || !box.isBox3) return;
1669
+ const key = String(modelId);
1670
+ const existingBox = this._boxIndex.get(key);
1671
+ if (!existingBox) {
1672
+ this._boxIndex.set(key, box);
1673
+ return;
1674
+ }
1675
+ this.mergeModelBoxUserData(existingBox, box);
1676
+ existingBox.union(box);
1677
+ },
1530
1678
  setBoxIndex(boxJson, documentId, isAdd = true) {
1531
1679
  if (!this._boxIndex) this._boxIndex = new Map();
1680
+ if (!this._sceneBoundsIndex) this._sceneBoundsIndex = new Map();
1532
1681
  if (!documentId) {
1533
1682
  this._boxIndex.clear();
1683
+ this._sceneBoundsIndex.clear();
1534
1684
  if (this.noObserver && this.noObserver.documentModelIds) {
1535
1685
  this.noObserver.documentModelIds.clear();
1536
1686
  }
1687
+ if (this.noObserver && this.noObserver.documentSceneBoundsIds) {
1688
+ this.noObserver.documentSceneBoundsIds.clear();
1689
+ }
1537
1690
  this.hasExecutedCentering = false;
1538
1691
  this.buildOctreeFromBoxIndex();
1539
1692
  return;
@@ -1542,32 +1695,50 @@ export default {
1542
1695
  if (isAdd && boxJson) {
1543
1696
  const arr = boxJson ? boxJson : [];
1544
1697
  const modelIds = new Set();
1698
+ const sceneBoundIds = new Set();
1545
1699
  const validGeomTypes = new Set(Object.values(GEOM_TYPES));
1546
1700
  for (let i = 0; i < arr.length; i++) {
1547
1701
  const it = arr[i];
1548
- if (!validGeomTypes.has(it.geomType)) {
1702
+ const tempGeomType = it.geomType || it.geom_type;
1703
+ if (!validGeomTypes.has(tempGeomType)) {
1549
1704
  continue;
1550
1705
  }
1551
- if (it.obb.length == 0) {
1706
+ const minSource = it.min;
1707
+ const maxSource = it.max;
1708
+ const hasValidMinMax =
1709
+ minSource &&
1710
+ maxSource &&
1711
+ minSource.length >= 3 &&
1712
+ maxSource.length >= 3 &&
1713
+ Number.isFinite(minSource[0]) &&
1714
+ Number.isFinite(minSource[1]) &&
1715
+ Number.isFinite(minSource[2]) &&
1716
+ Number.isFinite(maxSource[0]) &&
1717
+ Number.isFinite(maxSource[1]) &&
1718
+ Number.isFinite(maxSource[2]);
1719
+ if (!hasValidMinMax) {
1552
1720
  continue;
1553
1721
  }
1722
+ const obbData = it.obb || [];
1723
+ const modelId = String(it.id);
1554
1724
 
1555
1725
  // 使用新的 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]);
1726
+ const min = new this.THREE.Vector3(minSource[0], minSource[1], minSource[2]);
1727
+ const max = new this.THREE.Vector3(maxSource[0], maxSource[1], maxSource[2]);
1558
1728
 
1559
1729
  const sourceBox = new this.THREE.Box3(min.clone(), max.clone());
1560
1730
  // 八叉树仍使用 AABB 粗筛,最终碰撞使用 sourceBox 派生的 OBB 精筛
1561
1731
  const boxThree = sourceBox.clone();
1562
1732
 
1733
+ const tempMeshMatrix = it.meshMatrix || it.mesh_matrix;
1563
1734
  const hasMatrix = Array.isArray(it.matrix) && it.matrix.length >= 16;
1564
- const hasMeshMatrix = Array.isArray(it.meshMatrix) && it.meshMatrix.length >= 16;
1735
+ const hasMeshMatrix = Array.isArray(tempMeshMatrix) && tempMeshMatrix.length >= 16;
1565
1736
  let localMatrix = null;
1566
1737
  let collisionMatrix = null;
1567
1738
  if (hasMatrix || hasMeshMatrix) {
1568
1739
  localMatrix = new this.THREE.Matrix4();
1569
1740
  if (hasMeshMatrix) {
1570
- localMatrix.fromArray(it.meshMatrix);
1741
+ localMatrix.fromArray(tempMeshMatrix);
1571
1742
  } else {
1572
1743
  localMatrix.identity();
1573
1744
  }
@@ -1588,25 +1759,32 @@ export default {
1588
1759
  boxThree.applyMatrix4(collisionMatrix);
1589
1760
  }
1590
1761
 
1762
+ this.upsertIndexedBox(this._sceneBoundsIndex, modelId, boxThree.clone());
1763
+ sceneBoundIds.add(modelId);
1764
+
1765
+ // 没有 OBB 数据的二维图形只参与全场景范围,不纳入碰撞和遮挡索引。
1766
+ if (obbData.length === 0) {
1767
+ continue;
1768
+ }
1769
+
1591
1770
  const userData = {
1592
1771
  flag: it.flag || 1, // 默认为 1
1593
- geomType: it.geomType,
1594
- obbData: it.obb, // 存储原始 OBB 数据
1772
+ geomType: tempGeomType,
1773
+ obbData, // 存储原始 OBB 数据
1595
1774
  transparent: it.transp > 0,
1596
1775
  sourceVisible: it.visible === false ? false : true,
1597
1776
  visible: it.visible === false ? false : true, // 默认显示
1598
1777
  occlusionEnabled:
1599
- it.geomType === GEOM_TYPES.geom_3d || it.geomType === GEOM_TYPES.geom_3d_obj,
1778
+ tempGeomType === GEOM_TYPES.geom_3d || tempGeomType === GEOM_TYPES.geom_3d_obj,
1600
1779
  };
1601
1780
  if (it.flag === 1) {
1602
1781
  userData.indices = it.indices;
1603
1782
  userData.matrix = localMatrix ? localMatrix.toArray() : it.matrix;
1604
- userData.meshMatrix = it.meshMatrix;
1783
+ userData.meshMatrix = tempMeshMatrix;
1605
1784
  // userData.matrix = new this.THREE.Matrix4().identity();
1606
1785
  }
1607
1786
  // 如果是 flag=3,解析并存储中心点、半轴长、旋转矩阵
1608
- else if (it.flag === 3 && it.obb && it.obb.length === 15) {
1609
- const obbData = it.obb;
1787
+ else if (it.flag === 3 && obbData.length === 15) {
1610
1788
  const center = new this.THREE.Vector3(obbData[0], obbData[1], obbData[2]);
1611
1789
  const halfSize = new this.THREE.Vector3(obbData[3], obbData[4], obbData[5]);
1612
1790
  const rotation = new this.THREE.Matrix3().fromArray(obbData.slice(6, 15));
@@ -1629,27 +1807,26 @@ export default {
1629
1807
  halfSize: halfSize,
1630
1808
  };
1631
1809
 
1632
- const collisionObb = new OBB(center.clone(), halfSize.clone(), rotation.clone());
1633
- if (collisionMatrix) {
1634
- collisionObb.applyMatrix4(collisionMatrix);
1635
- }
1810
+ const collisionObb = this._createCollisionObb(
1811
+ center,
1812
+ halfSize,
1813
+ rotation,
1814
+ collisionMatrix
1815
+ );
1636
1816
  userData.collisionObbs = [collisionObb];
1637
1817
  }
1638
1818
  // 如果是 flag=2,解析并存储多个子包围盒 (min, max)
1639
- else if (it.flag === 2 && it.obb && it.obb.length > 0) {
1819
+ else if (it.flag === 2 && obbData.length > 0) {
1640
1820
  const subBoxes = [];
1641
1821
  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]);
1822
+ for (let k = 0; k < obbData.length; k += 6) {
1823
+ if (k + 5 < obbData.length) {
1824
+ const min = new this.THREE.Vector3(obbData[k], obbData[k + 1], obbData[k + 2]);
1825
+ const max = new this.THREE.Vector3(obbData[k + 3], obbData[k + 4], obbData[k + 5]);
1646
1826
  subBoxes.push({ min, max });
1647
1827
 
1648
1828
  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
- }
1829
+ const collisionObb = this._createCollisionObbFromBox(subBox, collisionMatrix);
1653
1830
  collisionObbs.push(collisionObb);
1654
1831
  }
1655
1832
  }
@@ -1660,20 +1837,17 @@ export default {
1660
1837
  }
1661
1838
 
1662
1839
  if (!userData.collisionObbs || userData.collisionObbs.length === 0) {
1663
- const collisionObb = new OBB().fromBox3(sourceBox);
1664
- if (collisionMatrix) {
1665
- collisionObb.applyMatrix4(collisionMatrix);
1666
- }
1840
+ const collisionObb = this._createCollisionObbFromBox(sourceBox, collisionMatrix);
1667
1841
  userData.collisionObbs = [collisionObb];
1668
1842
  }
1669
1843
 
1670
1844
  boxThree.userData = userData;
1671
1845
 
1672
- const modelId = String(it.id);
1673
- this._boxIndex.set(modelId, boxThree);
1846
+ this.upsertModelBox(modelId, boxThree);
1674
1847
  modelIds.add(modelId);
1675
1848
  }
1676
1849
  this.noObserver.documentModelIds.set(documentId, modelIds);
1850
+ this.noObserver.documentSceneBoundsIds.set(documentId, sceneBoundIds);
1677
1851
  } else {
1678
1852
  const modelIds = this.noObserver.documentModelIds.get(documentId);
1679
1853
  if (modelIds) {
@@ -1687,14 +1861,38 @@ export default {
1687
1861
  this.hasExecutedCentering = false;
1688
1862
  }
1689
1863
  }
1864
+ const sceneBoundIds =
1865
+ this.noObserver.documentSceneBoundsIds &&
1866
+ this.noObserver.documentSceneBoundsIds.get(documentId);
1867
+ if (sceneBoundIds) {
1868
+ sceneBoundIds.forEach(id => {
1869
+ this._sceneBoundsIndex.delete(id);
1870
+ });
1871
+ this.noObserver.documentSceneBoundsIds.delete(documentId);
1872
+ }
1690
1873
  }
1691
1874
  this.buildOctreeFromBoxIndex();
1692
1875
  this.refreshCollisionObbDebug();
1876
+ this.refreshOcclusionBoundsDebug();
1693
1877
  console.log('time end', Date.now());
1694
1878
  },
1695
1879
  shouldApplyOcclusionByBox(box) {
1696
1880
  return !!(box && box.userData && box.userData.occlusionEnabled);
1697
1881
  },
1882
+ appendOcclusionCandidates(candidates, modelId, box, frustum) {
1883
+ if (!box) return;
1884
+ const userData = box.userData || {};
1885
+ const proxyBoxes = Array.isArray(userData.occlusionProxyBoxes)
1886
+ ? userData.occlusionProxyBoxes
1887
+ : [box];
1888
+
1889
+ for (let i = 0; i < proxyBoxes.length; i++) {
1890
+ const proxyBox = proxyBoxes[i];
1891
+ if (!proxyBox || (proxyBox.userData && proxyBox.userData.visible === false)) continue;
1892
+ if (frustum && !frustum.intersectsBox(proxyBox)) continue;
1893
+ candidates.push({ modelId, box: proxyBox });
1894
+ }
1895
+ },
1698
1896
  tryInitialCenterAfterBoundsReady() {
1699
1897
  if (this.hasExecutedCentering || this.userInteracting) return;
1700
1898
  if (
@@ -1703,7 +1901,9 @@ export default {
1703
1901
  this.sceneBoundingBox.isEmpty()
1704
1902
  )
1705
1903
  return;
1706
- if (!this._boxIndex || this._boxIndex.size === 0) return;
1904
+ const hasBoxIndex = this._boxIndex && this._boxIndex.size > 0;
1905
+ const hasSceneBoundsIndex = this._sceneBoundsIndex && this._sceneBoundsIndex.size > 0;
1906
+ if (!hasBoxIndex && !hasSceneBoundsIndex) return;
1707
1907
 
1708
1908
  this.smartModelCenter(this.sceneBoundingBox, 0);
1709
1909
  },
@@ -1805,6 +2005,53 @@ export default {
1805
2005
  }
1806
2006
  node.items.push({ id, box });
1807
2007
  },
2008
+ _buildCollisionOctreeFromEntries(entries) {
2009
+ if (!Array.isArray(entries) || entries.length === 0) {
2010
+ return null;
2011
+ }
2012
+
2013
+ let minX = Infinity;
2014
+ let minY = Infinity;
2015
+ let minZ = Infinity;
2016
+ let maxX = -Infinity;
2017
+ let maxY = -Infinity;
2018
+ let maxZ = -Infinity;
2019
+
2020
+ entries.forEach(item => {
2021
+ const box = item && item.box;
2022
+ if (!box || !box.isBox3) return;
2023
+ const mn = box.min;
2024
+ const mx = box.max;
2025
+ if (mn.x < minX) minX = mn.x;
2026
+ if (mn.y < minY) minY = mn.y;
2027
+ if (mn.z < minZ) minZ = mn.z;
2028
+ if (mx.x > maxX) maxX = mx.x;
2029
+ if (mx.y > maxY) maxY = mx.y;
2030
+ if (mx.z > maxZ) maxZ = mx.z;
2031
+ });
2032
+
2033
+ if (minX === Infinity) {
2034
+ return null;
2035
+ }
2036
+
2037
+ const rootBox = new this.THREE.Box3(
2038
+ new this.THREE.Vector3(minX, minY, minZ),
2039
+ new this.THREE.Vector3(maxX, maxY, maxZ)
2040
+ );
2041
+ const root = {
2042
+ box: rootBox,
2043
+ items: [],
2044
+ children: null,
2045
+ depth: 0,
2046
+ };
2047
+
2048
+ entries.forEach(item => {
2049
+ if (!item || !item.box) return;
2050
+ this._octreeInsert(root, String(item.id), item.box);
2051
+ });
2052
+
2053
+ return root;
2054
+ },
1808
2055
  _isCollisionBoxAvailable(box) {
1809
2056
  if (!box || !box.isBox3 || box.isEmpty()) return false;
1810
2057
  if (box.userData && box.userData.visible === false) return false;
@@ -1935,7 +2182,8 @@ export default {
1935
2182
  _buildCollisionPairMap() {
1936
2183
  const pairMap = new Map();
1937
2184
  const collisionPairs = this.getOctreeCollisionPairs();
1938
- const pairs = collisionPairs && Array.isArray(collisionPairs.pairs) ? collisionPairs.pairs : [];
2185
+ const pairs =
2186
+ collisionPairs && Array.isArray(collisionPairs.pairs) ? collisionPairs.pairs : [];
1939
2187
  for (let i = 0; i < pairs.length; i++) {
1940
2188
  const pair = pairs[i];
1941
2189
  if (!pair) continue;
@@ -1956,20 +2204,15 @@ export default {
1956
2204
 
1957
2205
  this._clearCollisionObbDebugGroup(false);
1958
2206
  const pairMap = this._buildCollisionPairMap();
1959
- if (pairMap.size === 0) return;
2207
+ if (!this._boxIndex || this._boxIndex.size === 0) return;
1960
2208
 
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;
2209
+ this._boxIndex.forEach((box, modelId) => {
1968
2210
  if (!this._isCollisionBoxAvailable(box)) return;
1969
2211
 
1970
2212
  const obbs = this._getCollisionObbs(box);
1971
2213
  if (!obbs.length) return;
1972
2214
 
2215
+ const relatedIds = pairMap.get(String(modelId));
1973
2216
  const isColliding = relatedIds && relatedIds.size > 0;
1974
2217
  const color = isColliding ? 0xff3b30 : 0x36cfc9;
1975
2218
  for (let i = 0; i < obbs.length; i++) {
@@ -1995,6 +2238,165 @@ export default {
1995
2238
  this.hideCollisionObbDebug();
1996
2239
  }
1997
2240
  },
2241
+ _getOcclusionDebugGroup() {
2242
+ if (!this.scene) return null;
2243
+ if (this.occlusionBoundsDebugGroup && this.occlusionBoundsDebugGroup.parent === this.scene) {
2244
+ return this.occlusionBoundsDebugGroup;
2245
+ }
2246
+
2247
+ const group = new this.THREE.Group();
2248
+ group.name = OCCLUSION_DEBUG_GROUP_NAME;
2249
+ group.userData.transformControlHelper = true;
2250
+ group.userData.occlusionBoundsDebugHelper = true;
2251
+ this.scene.add(group);
2252
+ this.occlusionBoundsDebugGroup = group;
2253
+ return group;
2254
+ },
2255
+ _createOcclusionFlag1LineSegments(box, color) {
2256
+ const userData = box && box.userData ? box.userData : {};
2257
+ const vertices = userData.obbData;
2258
+ if (!vertices || vertices.length < 9) return null;
2259
+
2260
+ const sourceGeometry = new this.THREE.BufferGeometry();
2261
+ sourceGeometry.setAttribute(
2262
+ 'position',
2263
+ new this.THREE.BufferAttribute(new Float32Array(vertices), 3)
2264
+ );
2265
+
2266
+ const indices = userData.indices;
2267
+ if (indices && indices.length >= 3) {
2268
+ if (indices.isBufferAttribute) {
2269
+ sourceGeometry.setIndex(indices);
2270
+ } else if (ArrayBuffer.isView(indices)) {
2271
+ sourceGeometry.setIndex(new this.THREE.BufferAttribute(indices, 1));
2272
+ } else {
2273
+ sourceGeometry.setIndex(indices);
2274
+ }
2275
+ }
2276
+
2277
+ let wireframeGeometry = null;
2278
+ try {
2279
+ // flag=1 对应真实遮挡网格,必须按顶点和三角形索引绘制,不能退化成 AABB/OBB。
2280
+ wireframeGeometry = new this.THREE.WireframeGeometry(sourceGeometry);
2281
+ } catch (error) {
2282
+ console.warn('flag=1 遮挡调试线框生成失败', error);
2283
+ }
2284
+ sourceGeometry.dispose();
2285
+ if (!wireframeGeometry || wireframeGeometry.getAttribute('position').count === 0) {
2286
+ if (wireframeGeometry) wireframeGeometry.dispose();
2287
+ return null;
2288
+ }
2289
+
2290
+ const material = new this.THREE.LineBasicMaterial({
2291
+ color,
2292
+ transparent: true,
2293
+ opacity: 0.95,
2294
+ depthTest: false,
2295
+ depthWrite: false,
2296
+ toneMapped: false,
2297
+ });
2298
+ const lineSegments = new this.THREE.LineSegments(wireframeGeometry, material);
2299
+ const transformMatrix = new this.THREE.Matrix4();
2300
+ const matrixArr = userData.matrix;
2301
+ if (matrixArr && matrixArr.length >= 16) {
2302
+ transformMatrix.fromArray(matrixArr);
2303
+ }
2304
+ if (this.bizToThreeMatrix) {
2305
+ transformMatrix.premultiply(this.bizToThreeMatrix);
2306
+ }
2307
+ lineSegments.matrixAutoUpdate = false;
2308
+ lineSegments.matrix.copy(transformMatrix);
2309
+ lineSegments.matrixWorldNeedsUpdate = true;
2310
+ lineSegments.renderOrder = 998;
2311
+ lineSegments.userData.transformControlHelper = true;
2312
+ lineSegments.userData.occlusionBoundsDebugHelper = true;
2313
+ lineSegments.userData.occlusionFlag = 1;
2314
+ return lineSegments;
2315
+ },
2316
+ _createOcclusionBoxLineSegments(obb, color, flag) {
2317
+ const lineSegments = this._createCollisionObbLineSegments(obb, color);
2318
+ if (!lineSegments) return null;
2319
+
2320
+ lineSegments.renderOrder = 998;
2321
+ lineSegments.userData.collisionObbDebugHelper = false;
2322
+ lineSegments.userData.occlusionBoundsDebugHelper = true;
2323
+ lineSegments.userData.occlusionFlag = flag;
2324
+ return lineSegments;
2325
+ },
2326
+ _clearOcclusionBoundsDebugGroup(removeGroup = false) {
2327
+ const group = this.occlusionBoundsDebugGroup;
2328
+ if (!group) return;
2329
+
2330
+ while (group.children.length > 0) {
2331
+ const child = group.children[group.children.length - 1];
2332
+ group.remove(child);
2333
+ if (child.geometry) child.geometry.dispose();
2334
+ if (Array.isArray(child.material)) {
2335
+ child.material.forEach(material => material && material.dispose());
2336
+ } else if (child.material) {
2337
+ child.material.dispose();
2338
+ }
2339
+ }
2340
+
2341
+ if (removeGroup && this.scene && group.parent === this.scene) {
2342
+ this.scene.remove(group);
2343
+ this.occlusionBoundsDebugGroup = null;
2344
+ } else if (!this.scene) {
2345
+ this.occlusionBoundsDebugGroup = null;
2346
+ }
2347
+ },
2348
+ hideOcclusionBoundsDebug() {
2349
+ this._clearOcclusionBoundsDebugGroup();
2350
+ this.occlusionBoundsDebugEnabled = false;
2351
+ },
2352
+ refreshOcclusionBoundsDebug() {
2353
+ if (!this.occlusionBoundsDebugEnabled) return;
2354
+ const group = this._getOcclusionDebugGroup();
2355
+ if (!group) return;
2356
+
2357
+ this._clearOcclusionBoundsDebugGroup(false);
2358
+ if (!this._boxIndex || this._boxIndex.size === 0) return;
2359
+
2360
+ this._boxIndex.forEach((box, modelId) => {
2361
+ if (!this._isCollisionBoxAvailable(box) || !this.shouldApplyOcclusionByBox(box)) return;
2362
+
2363
+ const userData = box.userData || {};
2364
+ const flag = Number(userData.flag) || 1;
2365
+ const color = OCCLUSION_DEBUG_FLAG_COLORS[flag];
2366
+ if (!color) return;
2367
+
2368
+ if (flag === 1) {
2369
+ const lineSegments = this._createOcclusionFlag1LineSegments(box, color);
2370
+ if (!lineSegments) return;
2371
+ lineSegments.name = `${OCCLUSION_DEBUG_GROUP_NAME}:${modelId}:flag-1`;
2372
+ lineSegments.userData.modelId = modelId;
2373
+ group.add(lineSegments);
2374
+ return;
2375
+ }
2376
+
2377
+ const obbs = this._getCollisionObbs(box);
2378
+ for (let i = 0; i < obbs.length; i++) {
2379
+ const lineSegments = this._createOcclusionBoxLineSegments(obbs[i], color, flag);
2380
+ if (!lineSegments) continue;
2381
+ lineSegments.name = `${OCCLUSION_DEBUG_GROUP_NAME}:${modelId}:flag-${flag}:${i}`;
2382
+ lineSegments.userData.modelId = modelId;
2383
+ group.add(lineSegments);
2384
+ }
2385
+ });
2386
+ },
2387
+ showOcclusionBoundsDebug() {
2388
+ this.occlusionBoundsDebugEnabled = true;
2389
+ this.refreshOcclusionBoundsDebug();
2390
+ },
2391
+ toggleOcclusionBoundsDebug(forceVisible) {
2392
+ const nextVisible =
2393
+ typeof forceVisible === 'boolean' ? forceVisible : !this.occlusionBoundsDebugEnabled;
2394
+ if (nextVisible) {
2395
+ this.showOcclusionBoundsDebug();
2396
+ } else {
2397
+ this.hideOcclusionBoundsDebug();
2398
+ }
2399
+ },
1998
2400
  _getObbAxisInfo(obb) {
1999
2401
  const xAxis = new this.THREE.Vector3();
2000
2402
  const yAxis = new this.THREE.Vector3();
@@ -2064,16 +2466,15 @@ export default {
2064
2466
  const obbsB = this._getCollisionObbs(boxB);
2065
2467
  for (let i = 0; i < obbsA.length; i++) {
2066
2468
  for (let j = 0; j < obbsB.length; j++) {
2067
- if (this._getObbMinPenetration(obbsA[i], obbsB[j]) > COLLISION_PENETRATION_EPSILON) {
2469
+ if (this._getObbMinPenetration(obbsA[i], obbsB[j]) >= COLLISION_CONTACT_EPSILON) {
2068
2470
  return true;
2069
2471
  }
2070
2472
  }
2071
2473
  }
2072
2474
  return false;
2073
2475
  },
2074
- _queryOctreeByBox(box) {
2476
+ _queryOctreeByBoxWithRoot(root, box) {
2075
2477
  const results = [];
2076
- const root = this._octree;
2077
2478
  if (!root || !this._isCollisionBoxAvailable(box)) return results;
2078
2479
 
2079
2480
  const stack = [root];
@@ -2099,25 +2500,88 @@ export default {
2099
2500
 
2100
2501
  return results;
2101
2502
  },
2102
- getOctreeCollisionPairs() {
2503
+ _queryOctreeByBox(box) {
2504
+ return this._queryOctreeByBoxWithRoot(this._octree, box);
2505
+ },
2506
+ getOctreeCollisionPairs(options = {}) {
2103
2507
  const pairs = [];
2104
2508
  if (!this._boxIndex || this._boxIndex.size === 0) return { pairs };
2105
2509
 
2106
- if (!this._octree) {
2107
- this.buildOctreeFromBoxIndex();
2510
+ const normalizeIds = value =>
2511
+ Array.isArray(value)
2512
+ ? Array.from(
2513
+ new Set(
2514
+ value
2515
+ .map(item => (item === null || item === undefined ? '' : String(item)))
2516
+ .filter(item => item !== '')
2517
+ )
2518
+ )
2519
+ : [];
2520
+
2521
+ const getEntriesByIds = ids => {
2522
+ const entries = [];
2523
+ const idSet = new Set(ids);
2524
+ this._boxIndex.forEach((box, id) => {
2525
+ const key = String(id);
2526
+ if (idSet.has(key)) {
2527
+ entries.push({
2528
+ id: key,
2529
+ box,
2530
+ });
2531
+ }
2532
+ });
2533
+ return entries;
2534
+ };
2535
+
2536
+ const sourceIds = normalizeIds(options.sourceIds);
2537
+ const targetIds = normalizeIds(options.targetIds);
2538
+ const includeIds = Array.isArray(options.includeIds) ? normalizeIds(options.includeIds) : [];
2539
+
2540
+ let root = this._octree;
2541
+ let sourceEntries = [];
2542
+ let dedupePairKeySet = null;
2543
+
2544
+ if (sourceIds.length > 0 && targetIds.length > 0) {
2545
+ const targetEntries = getEntriesByIds(targetIds);
2546
+ sourceEntries = getEntriesByIds(sourceIds);
2547
+ if (sourceEntries.length === 0 || targetEntries.length === 0) return { pairs };
2548
+ root = this._buildCollisionOctreeFromEntries(targetEntries);
2549
+ if (!root) return { pairs };
2550
+ dedupePairKeySet = new Set();
2551
+ } else if (includeIds.length > 0) {
2552
+ sourceEntries = getEntriesByIds(includeIds);
2553
+ if (sourceEntries.length === 0) return { pairs };
2554
+ root = this._buildCollisionOctreeFromEntries(sourceEntries);
2555
+ if (!root) return { pairs };
2556
+ } else {
2557
+ if (!this._octree) {
2558
+ this.buildOctreeFromBoxIndex();
2559
+ }
2560
+ if (!this._octree) return { pairs };
2561
+ this._boxIndex.forEach((box, id) => {
2562
+ sourceEntries.push({
2563
+ id: String(id),
2564
+ box,
2565
+ });
2566
+ });
2108
2567
  }
2109
- if (!this._octree) return { pairs };
2110
2568
 
2111
- this._boxIndex.forEach((boxA, idA) => {
2569
+ sourceEntries.forEach(({ id: idA, box: boxA }) => {
2112
2570
  if (!this._isCollisionBoxAvailable(boxA)) return;
2113
2571
 
2114
- const hits = this._queryOctreeByBox(boxA);
2572
+ const hits = this._queryOctreeByBoxWithRoot(root, boxA);
2115
2573
  for (let i = 0; i < hits.length; i++) {
2116
2574
  const item = hits[i];
2117
2575
  if (!item || String(item.id) === String(idA)) continue;
2118
2576
  const boxB = item.box;
2119
2577
  if (!this._isCollisionBoxMatched(boxA, boxB)) continue;
2120
2578
 
2579
+ if (dedupePairKeySet) {
2580
+ const pairKey = `${String(idA)}__${String(item.id)}`;
2581
+ if (dedupePairKeySet.has(pairKey)) continue;
2582
+ dedupePairKeySet.add(pairKey);
2583
+ }
2584
+
2121
2585
  pairs.push({
2122
2586
  idA: this._formatCollisionPairId(idA),
2123
2587
  idB: this._formatCollisionPairId(item.id),
@@ -2571,7 +3035,8 @@ export default {
2571
3035
  /**
2572
3036
  * 执行视椎体裁切,清理视椎体外的InstancedMesh实例
2573
3037
  */
2574
- async performFrustumCulling() {
3038
+ async performFrustumCulling(options = {}) {
3039
+ const skipUnload = !!(options && options.skipUnload);
2575
3040
  const modelState = this.noObserver
2576
3041
  ? this.noObserver.modelStateManager
2577
3042
  : this.modelStateManager;
@@ -2613,7 +3078,7 @@ export default {
2613
3078
  visibleIdSet.add(this.formatModelId(hits[i].modelId));
2614
3079
  continue;
2615
3080
  }
2616
- candidates.push(hits[i]);
3081
+ this.appendOcclusionCandidates(candidates, hits[i].modelId, hits[i].box, globalFrustum);
2617
3082
  }
2618
3083
  } else if (this._boxIndex && this._boxIndex.size > 0) {
2619
3084
  this._boxIndex.forEach((box, modelId) => {
@@ -2624,7 +3089,7 @@ export default {
2624
3089
  visibleIdSet.add(this.formatModelId(modelId));
2625
3090
  return;
2626
3091
  }
2627
- candidates.push({ modelId, box });
3092
+ this.appendOcclusionCandidates(candidates, modelId, box, globalFrustum);
2628
3093
  }
2629
3094
  });
2630
3095
  }
@@ -2740,6 +3205,7 @@ export default {
2740
3205
  const tBuild0 = performance.now();
2741
3206
 
2742
3207
  const flag1Items = [];
3208
+ const flag2Items = [];
2743
3209
  const flag3Items = [];
2744
3210
 
2745
3211
  let globalIdx = 1;
@@ -2753,6 +3219,8 @@ export default {
2753
3219
  const flag = (c.box.userData && c.box.userData.flag) || 1;
2754
3220
  if (flag === 3 && c.box.userData && c.box.userData.obb) {
2755
3221
  flag3Items.push({ c, idx });
3222
+ } else if (flag === 2) {
3223
+ flag2Items.push({ c, idx });
2756
3224
  } else if (flag === 1) {
2757
3225
  flag1Items.push({ c, idx });
2758
3226
  }
@@ -2769,7 +3237,8 @@ export default {
2769
3237
  const _tempMatA = occObjs._tempMatA || (occObjs._tempMatA = new this.THREE.Matrix4());
2770
3238
 
2771
3239
  let boxes = occObjs.boxes;
2772
- const boxCount = flag3Items.length;
3240
+ const boxItems = flag3Items.concat(flag2Items);
3241
+ const boxCount = boxItems.length;
2773
3242
  if (boxCount > 0) {
2774
3243
  const needCapacity = boxCount;
2775
3244
  const capacity = boxes && typeof boxes.capacity === 'number' ? boxes.capacity : 0;
@@ -2799,19 +3268,26 @@ export default {
2799
3268
  }
2800
3269
  boxes.mesh.count = boxCount;
2801
3270
  for (let i = 0; i < boxCount; i++) {
2802
- const item = flag3Items[i];
2803
- const { matrix, halfSize } = item.c.box.userData.obb;
3271
+ const item = boxItems[i];
2804
3272
  const idx = item.idx;
2805
3273
  const r = (idx & 255) / 255;
2806
3274
  const g = ((idx >> 8) & 255) / 255;
2807
3275
  const b = ((idx >> 16) & 255) / 255;
2808
3276
  _tempColor.setRGB(r, g, b);
2809
3277
 
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);
3278
+ const obb = item.c.box.userData && item.c.box.userData.obb;
3279
+ if (obb) {
3280
+ _tempScale.copy(obb.halfSize).multiplyScalar(2);
3281
+ _tempMatrix.copy(obb.matrix);
3282
+ _tempMatrix.scale(_tempScale);
3283
+ if (typeof this.bizToThreeMatrix !== 'undefined' && this.bizToThreeMatrix) {
3284
+ _tempMatrix.premultiply(this.bizToThreeMatrix);
3285
+ }
3286
+ } else {
3287
+ item.c.box.getSize(_tempScale);
3288
+ item.c.box.getCenter(_tempVec3);
3289
+ _tempMatrix.makeTranslation(_tempVec3.x, _tempVec3.y, _tempVec3.z);
3290
+ _tempMatrix.scale(_tempScale);
2815
3291
  }
2816
3292
 
2817
3293
  boxes.mesh.setMatrixAt(i, _tempMatrix);
@@ -3254,12 +3730,12 @@ export default {
3254
3730
  this.renderer.toneMapping = prevToneMapping;
3255
3731
  } catch (e) {
3256
3732
  for (let i = 0; i < candidates.length; i++) {
3257
- visibleIdSet.add(candidates[i].modelId);
3733
+ visibleIdSet.add(this.formatModelId(candidates[i].modelId));
3258
3734
  }
3259
3735
  }
3260
3736
  } else {
3261
3737
  for (let i = 0; i < candidates.length; i++) {
3262
- visibleIdSet.add(candidates[i].modelId);
3738
+ visibleIdSet.add(this.formatModelId(candidates[i].modelId));
3263
3739
  }
3264
3740
  }
3265
3741
  visibleIds.length = 0;
@@ -3268,37 +3744,43 @@ export default {
3268
3744
  const parentsToCheck = new Set();
3269
3745
  this.scene.traverse(child => {
3270
3746
  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;
3747
+ const instancesMap =
3748
+ child && child.userData && child.userData.instancesMap instanceof Map
3749
+ ? child.userData.instancesMap
3750
+ : child.parent &&
3751
+ child.parent.userData &&
3752
+ child.parent.userData.instancesMap instanceof Map
3753
+ ? child.parent.userData.instancesMap
3754
+ : null;
3281
3755
 
3756
+ // 多实例复用同一个 InstancedMesh 时,仅在全部实例都不可见时整体卸载。
3757
+ if (child.count >= 2) {
3282
3758
  if (instancesMap) {
3283
3759
  let allInactive = true;
3284
3760
  const modelIds = Array.from(instancesMap.keys());
3761
+ let unloadModelId = null;
3285
3762
 
3286
3763
  // 第一遍遍历:检查是否有任何一个实例处于活跃状态(在视锥体内且可见)
3287
3764
  for (const modelId of modelIds) {
3288
3765
  // 检查已取消勾选模型进行卸载
3289
- const documentId = modelId.split(':')[1];
3290
- if (!this.noObserver.documentModelIds.get(documentId)) {
3291
- toUnload.push({ modelId, child });
3292
- return;
3766
+ const documentId = String(modelId).split(':')[1];
3767
+ if (documentId && !this.noObserver.documentModelIds.get(documentId)) {
3768
+ if (!skipUnload) this.setInstanceCullingVisible(child, modelId, false);
3769
+ unloadModelId = modelId;
3770
+ continue;
3293
3771
  }
3294
3772
 
3295
- if (bypassList && bypassList.size > 0 && bypassList.has(modelId)) return;
3773
+ const formattedModelId = this.formatModelId(modelId);
3774
+ if (
3775
+ bypassList &&
3776
+ bypassList.size > 0 &&
3777
+ (bypassList.has(modelId) || bypassList.has(formattedModelId))
3778
+ ) {
3779
+ if (!skipUnload) this.setInstanceCullingVisible(child, modelId, true);
3780
+ allInactive = false;
3781
+ continue;
3782
+ }
3296
3783
 
3297
- const instanceInfo = instancesMap.get(modelId);
3298
- const instanceIndex =
3299
- instanceInfo && typeof instanceInfo.instanceIndex === 'number'
3300
- ? instanceInfo.instanceIndex
3301
- : null;
3302
3784
  const box =
3303
3785
  this._boxIndex && this._boxIndex.has(String(modelId))
3304
3786
  ? this._boxIndex.get(String(modelId))
@@ -3306,16 +3788,19 @@ export default {
3306
3788
  ? this._boxIndex.get(modelId)
3307
3789
  : null;
3308
3790
  if (!this.shouldApplyOcclusionByBox(box)) {
3791
+ if (!skipUnload) this.setInstanceCullingVisible(child, modelId, true);
3309
3792
  allInactive = false;
3793
+ toLoadSet.delete(formattedModelId);
3310
3794
  continue;
3311
3795
  }
3312
3796
 
3313
- const inFrustum = this.isModelInFrustum(child, instanceIndex, globalFrustum);
3314
- const modelInVisible = toLoadSet.has(modelId);
3315
- // console.log('modelId', modelId, 'inFrustum', inFrustum, 'modelInVisible', modelInVisible)
3797
+ const modelInVisible = visibleIdSet.has(formattedModelId);
3798
+ toLoadSet.delete(formattedModelId);
3316
3799
 
3317
- // 只要有一个实例在视锥体内且在可见列表中,就不整体卸载
3318
- if (inFrustum || modelInVisible) {
3800
+ // 可见集合已经聚合了同一模型的全部图元,任一图元可见时都保留该模型实例。
3801
+ const isActive = modelInVisible;
3802
+ if (!skipUnload) this.setInstanceCullingVisible(child, modelId, isActive);
3803
+ if (isActive) {
3319
3804
  allInactive = false;
3320
3805
  }
3321
3806
  }
@@ -3329,20 +3814,24 @@ export default {
3329
3814
  ? this._boxIndex.get(modelId)
3330
3815
  : null;
3331
3816
  if (!this.shouldApplyOcclusionByBox(box)) {
3332
- if (toLoadSet.has(modelId)) {
3333
- toLoadSet.delete(modelId);
3334
- }
3817
+ toLoadSet.delete(this.formatModelId(modelId));
3335
3818
  continue;
3336
3819
  }
3337
3820
  // 无论是否卸载,只要模型已在场景中,就从待加载集合中移除
3338
- if (toLoadSet.has(modelId)) {
3339
- toLoadSet.delete(modelId);
3340
- }
3821
+ toLoadSet.delete(this.formatModelId(modelId));
3341
3822
 
3342
- // 仅当所有实例都不活跃时,才执行卸载
3343
- if (!this.firstPerSign && !this.roaming && allInactive) {
3344
- toUnload.push({ modelId, child });
3345
- }
3823
+ if (!unloadModelId) unloadModelId = modelId;
3824
+ }
3825
+
3826
+ // 仅当所有实例都不活跃时,才整体卸载一次,避免重复释放同一网格。
3827
+ if (
3828
+ !skipUnload &&
3829
+ !this.firstPerSign &&
3830
+ !this.roaming &&
3831
+ allInactive &&
3832
+ unloadModelId !== null
3833
+ ) {
3834
+ toUnload.push({ modelId: unloadModelId, child });
3346
3835
  }
3347
3836
  }
3348
3837
  // if (toLoadSet.has(modelId)) {
@@ -3351,10 +3840,18 @@ export default {
3351
3840
  return;
3352
3841
  }
3353
3842
  const modelId =
3354
- child.parent && child.parent.userData && child.parent.userData.instanceId
3843
+ instancesMap && instancesMap.size > 0
3844
+ ? instancesMap.keys().next().value
3845
+ : child.parent && child.parent.userData && child.parent.userData.instanceId
3355
3846
  ? child.parent.userData.instanceId
3356
3847
  : child.uuid;
3357
- if (bypassList && bypassList.size > 0 && bypassList.has(modelId)) return;
3848
+ const formattedModelId = this.formatModelId(modelId);
3849
+ if (
3850
+ bypassList &&
3851
+ bypassList.size > 0 &&
3852
+ (bypassList.has(modelId) || bypassList.has(formattedModelId))
3853
+ )
3854
+ return;
3358
3855
  const box =
3359
3856
  this._boxIndex && this._boxIndex.has(String(modelId))
3360
3857
  ? this._boxIndex.get(String(modelId))
@@ -3362,18 +3859,13 @@ export default {
3362
3859
  ? this._boxIndex.get(modelId)
3363
3860
  : null;
3364
3861
  if (!this.shouldApplyOcclusionByBox(box)) {
3365
- if (toLoadSet.has(modelId)) {
3366
- toLoadSet.delete(modelId);
3367
- }
3862
+ toLoadSet.delete(formattedModelId);
3368
3863
  return;
3369
3864
  }
3370
3865
 
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)) {
3866
+ const modelInVisible = visibleIdSet.has(formattedModelId);
3867
+ toLoadSet.delete(formattedModelId);
3868
+ if (!skipUnload && !this.firstPerSign && !this.roaming && !modelInVisible) {
3377
3869
  toUnload.push({ modelId, child });
3378
3870
  }
3379
3871
  });
@@ -3491,9 +3983,15 @@ export default {
3491
3983
  * @param {Object} config - 配置项
3492
3984
  * @param {Object} config.modelApi - 模型API接口
3493
3985
  * @param {string} [config.projectId] - 项目ID
3986
+ * @param {Object} [config.cameraControlMoveObserver] - 普通视角运动中触发加载的配置
3987
+ * @param {boolean} [config.cameraControlMoveObserver.enabled=false] - 是否开启普通视角运动中加载
3988
+ * @param {number} [config.cameraControlMoveObserver.interval=500] - 触发间隔,单位毫秒
3989
+ * @param {boolean} [config.enableCameraControlMoveObserver=false] - 是否开启普通视角运动中加载
3990
+ * @param {number} [config.cameraControlMoveInterval=500] - 触发间隔,单位毫秒
3494
3991
  */
3495
3992
  init(config = {}) {
3496
3993
  console.log('init');
3994
+ this.setCameraControlMoveObserverConfig(config);
3497
3995
  this.initStreamLoader(config.modelApi, config);
3498
3996
  },
3499
3997
 
@@ -3920,6 +4418,124 @@ export default {
3920
4418
  this.noObserver.isObserverEnabled = enabled;
3921
4419
  },
3922
4420
 
4421
+ setCameraControlMoveObserverConfig(config = {}) {
4422
+ if (!this.noObserver) return;
4423
+ const nestedConfig = config.cameraControlMoveObserver || {};
4424
+ const enabled =
4425
+ typeof nestedConfig.enabled === 'boolean'
4426
+ ? nestedConfig.enabled
4427
+ : typeof config.enableCameraControlMoveObserver === 'boolean'
4428
+ ? config.enableCameraControlMoveObserver
4429
+ : false;
4430
+ const rawInterval =
4431
+ nestedConfig.interval !== undefined
4432
+ ? nestedConfig.interval
4433
+ : config.cameraControlMoveInterval;
4434
+ const interval = Number(rawInterval);
4435
+ const safeInterval = Number.isFinite(interval) && interval > 0 ? interval : 500;
4436
+
4437
+ this.noObserver.cameraControlMoveObserver = {
4438
+ enabled,
4439
+ interval: safeInterval,
4440
+ };
4441
+ this.resetCameraControlMoveSnapshot(false);
4442
+ },
4443
+
4444
+ isCameraControlMoveObserverEnabled() {
4445
+ const config = this.noObserver && this.noObserver.cameraControlMoveObserver;
4446
+ return !!(config && config.enabled);
4447
+ },
4448
+
4449
+ getCameraControlTarget(target) {
4450
+ if (!target || !this.cameraControls) return false;
4451
+ if (typeof this.cameraControls.getTarget === 'function') {
4452
+ this.cameraControls.getTarget(target);
4453
+ return true;
4454
+ }
4455
+ if (this.cameraControls._target) {
4456
+ target.copy(this.cameraControls._target);
4457
+ return true;
4458
+ }
4459
+ return false;
4460
+ },
4461
+
4462
+ resetCameraControlMoveSnapshot(syncCurrent = false) {
4463
+ if (!this.noObserver) return;
4464
+ if (!this.noObserver._cameraControlMoveState) {
4465
+ this.noObserver._cameraControlMoveState = {
4466
+ position: new this.THREE.Vector3(),
4467
+ quaternion: new this.THREE.Quaternion(),
4468
+ target: new this.THREE.Vector3(),
4469
+ currentTarget: new this.THREE.Vector3(),
4470
+ initialized: false,
4471
+ hasTarget: false,
4472
+ };
4473
+ }
4474
+ const state = this.noObserver._cameraControlMoveState;
4475
+ if (!syncCurrent || !this.camera) {
4476
+ state.initialized = false;
4477
+ state.hasTarget = false;
4478
+ return;
4479
+ }
4480
+
4481
+ this.camera.updateMatrixWorld(true);
4482
+ state.position.copy(this.camera.position);
4483
+ state.quaternion.copy(this.camera.quaternion);
4484
+ state.hasTarget = this.getCameraControlTarget(state.target);
4485
+ state.initialized = true;
4486
+ },
4487
+
4488
+ observeCameraControlMove() {
4489
+ if (
4490
+ !this.noObserver ||
4491
+ !this.noObserver.isObserverEnabled ||
4492
+ !this.isCameraControlMoveObserverEnabled() ||
4493
+ !this.camera ||
4494
+ !this.cameraControls ||
4495
+ typeof this._cameraChangeObserver !== 'function'
4496
+ ) {
4497
+ return;
4498
+ }
4499
+
4500
+ const loadingState = this.noObserver.batchLoadingState || {};
4501
+ const interactionState = loadingState.interactionState || {};
4502
+ const wheelActive = !!interactionState.wheelTimeout;
4503
+ const controlActive = !!this.noObserver.isControlActive;
4504
+ if ((!controlActive && !wheelActive) || this.firstPerSign) {
4505
+ this.resetCameraControlMoveSnapshot(false);
4506
+ return;
4507
+ }
4508
+
4509
+ if (!this.noObserver._cameraControlMoveState) {
4510
+ this.resetCameraControlMoveSnapshot(true);
4511
+ return;
4512
+ }
4513
+
4514
+ const state = this.noObserver._cameraControlMoveState;
4515
+ if (!state.initialized) {
4516
+ this.resetCameraControlMoveSnapshot(true);
4517
+ return;
4518
+ }
4519
+
4520
+ this.camera.updateMatrixWorld(true);
4521
+ const hasTarget = this.getCameraControlTarget(state.currentTarget);
4522
+ const positionMoved = state.position.distanceToSquared(this.camera.position) > 1e-8;
4523
+ const quaternionMoved = 1 - Math.abs(state.quaternion.dot(this.camera.quaternion)) > 1e-10;
4524
+ const targetMoved =
4525
+ hasTarget !== state.hasTarget ||
4526
+ (hasTarget && state.target.distanceToSquared(state.currentTarget) > 1e-8);
4527
+
4528
+ if (!positionMoved && !quaternionMoved && !targetMoved) return;
4529
+
4530
+ state.position.copy(this.camera.position);
4531
+ state.quaternion.copy(this.camera.quaternion);
4532
+ state.hasTarget = hasTarget;
4533
+ if (hasTarget) {
4534
+ state.target.copy(state.currentTarget);
4535
+ }
4536
+ this._cameraChangeObserver('controlMove');
4537
+ },
4538
+
3923
4539
  /**
3924
4540
  * 初始化相机变更观察者 (CameraChangeObserver)
3925
4541
  * 统一监听:用户交互(Wheel/Control)、程序化动画、API调用
@@ -3954,7 +4570,9 @@ export default {
3954
4570
  // 执行视锥体裁剪
3955
4571
  // 第一视角不裁切
3956
4572
  if (this.modelStateManager.frustumCheckEnabled) {
3957
- await this.performFrustumCulling();
4573
+ await this.performFrustumCulling({
4574
+ skipUnload: source === 'controlMove',
4575
+ });
3958
4576
  }
3959
4577
 
3960
4578
  // 执行流式加载请求
@@ -3966,11 +4584,19 @@ export default {
3966
4584
  }
3967
4585
  },
3968
4586
  source => {
3969
- // 漫游模式下,或者第一人称移动时,使用节流策略(每300ms触发一次)
4587
+ // 漫游、第一人称移动、普通视角拖拽/缩放中,使用节流策略避免持续运动被防抖吞掉。
3970
4588
  // 避免在持续运动中因防抖重置定时器而无法触发
3971
4589
  if (source === 'roam' || source === 'firstPersonMove') {
3972
4590
  return { type: 'throttle', limit: 1000 };
3973
4591
  }
4592
+ if (source === 'controlMove') {
4593
+ const config = this.noObserver && this.noObserver.cameraControlMoveObserver;
4594
+ const interval =
4595
+ config && Number.isFinite(config.interval) && config.interval > 0
4596
+ ? config.interval
4597
+ : 500;
4598
+ return { type: 'throttle', limit: interval };
4599
+ }
3974
4600
  return 300; // 默认防抖 300ms
3975
4601
  }
3976
4602
  );
@@ -3984,8 +4610,20 @@ export default {
3984
4610
  this._wheelHandler = res => {
3985
4611
  this.$emit('wheelStart', res);
3986
4612
 
3987
- // 统一交互开始处理
3988
- this.beginInteraction('wheel');
4613
+ const cameraControlMoveEnabled = this.isCameraControlMoveObserverEnabled();
4614
+ const loadingState = this.noObserver
4615
+ ? this.noObserver.batchLoadingState
4616
+ : this.batchLoadingState;
4617
+ const wheelAlreadyActive =
4618
+ loadingState &&
4619
+ loadingState.interactionState &&
4620
+ loadingState.interactionState.wheelTimeout;
4621
+ if (cameraControlMoveEnabled && !wheelAlreadyActive) {
4622
+ this.resetCameraControlMoveSnapshot(true);
4623
+ } else if (!cameraControlMoveEnabled) {
4624
+ // 未开启普通视角运动加载时,保持原有交互中断流程。
4625
+ this.beginInteraction('wheel');
4626
+ }
3989
4627
 
3990
4628
  // 设置滚轮交互的结束检测
3991
4629
  this.scheduleWheelInteractionEnd(res, 100);
@@ -4027,9 +4665,12 @@ export default {
4027
4665
  // 4. 监听器 - Controls (用户拖拽/操作)
4028
4666
  this._onControlStart = res => {
4029
4667
  this.noObserver.isControlActive = true;
4030
-
4031
- // 统一交互开始处理(相机)
4032
- this.beginInteraction('this.camera');
4668
+ if (this.isCameraControlMoveObserverEnabled()) {
4669
+ this.resetCameraControlMoveSnapshot(true);
4670
+ } else {
4671
+ // 未开启普通视角运动加载时,保持原有交互中断流程。
4672
+ this.beginInteraction('this.camera');
4673
+ }
4033
4674
  this.$emit('controlStart', res);
4034
4675
 
4035
4676
  // 记录鼠标按下时的位置
@@ -4046,8 +4687,10 @@ export default {
4046
4687
 
4047
4688
  this.$emit('controlEnd', res);
4048
4689
 
4049
- // 统一交互结束处理(相机)
4050
- this.endInteraction('this.camera', res, { immediateResume: false });
4690
+ if (!this.isCameraControlMoveObserverEnabled()) {
4691
+ // 未开启普通视角运动加载时,保持原有交互中断流程。
4692
+ this.endInteraction('this.camera', res, { immediateResume: false });
4693
+ }
4051
4694
 
4052
4695
  // 记录鼠标抬起时的位置并计算是否发生了拖拽
4053
4696
  const event = res.originalEvent || window.event;
@@ -4076,6 +4719,9 @@ export default {
4076
4719
  // 触发观察者
4077
4720
  this._cameraChangeObserver('control');
4078
4721
  }
4722
+ if (this.isCameraControlMoveObserverEnabled()) {
4723
+ this.resetCameraControlMoveSnapshot(false);
4724
+ }
4079
4725
  };
4080
4726
 
4081
4727
  this.cameraControls.addEventListener('controlstart', this._onControlStart);
@@ -4136,12 +4782,37 @@ export default {
4136
4782
  },
4137
4783
  // 动态设置视角滚轮的距离
4138
4784
  setCameraConfig() {
4785
+ if (
4786
+ !this.sceneBoundingBox ||
4787
+ !this.sceneBoundingBox.isBox3 ||
4788
+ this.sceneBoundingBox.isEmpty()
4789
+ ) {
4790
+ return;
4791
+ }
4139
4792
  // let box3 = new this.THREE.Box3().setFromObject(this.sceneBoundingBox);
4140
4793
  let size = new this.THREE.Vector3();
4141
4794
  this.sceneBoundingBox.getSize(size);
4142
4795
  const maxBorder = Math.max(size.x, size.y, size.z);
4796
+ if (isDebug) {
4797
+ const center = this.sceneBoundingBox.getCenter(new this.THREE.Vector3());
4798
+ console.log('sceneBoundingBox 调试信息', {
4799
+ size: {
4800
+ x: size.x,
4801
+ y: size.y,
4802
+ z: size.z,
4803
+ },
4804
+ center: {
4805
+ x: center.x,
4806
+ y: center.y,
4807
+ z: center.z,
4808
+ },
4809
+ maxBorder,
4810
+ });
4811
+ this.refreshSceneBoundingBoxHelper(this.sceneBoundingBoxDebugEnabled);
4812
+ }
4143
4813
  // this.cameraControls.this.camera.far = maxBorder * 10; // 设置相机的远裁剪面
4144
4814
  this.cameraControls.minDistance = maxBorder * 0.2; // 动态设置视角滚轮的距离
4815
+ // this.cameraControls.minDistance = 1000; // 动态设置视角滚轮的距离
4145
4816
  this.camera.updateProjectionMatrix();
4146
4817
  },
4147
4818
  // 获取mesh的中心点
@@ -4469,8 +5140,19 @@ export default {
4469
5140
  if (this.threeMeasure) {
4470
5141
  this.threeMeasure.updateParams(width, height);
4471
5142
  }
5143
+ this.updateMeshLineResolution(width, height);
4472
5144
  }
4473
5145
  },
5146
+ updateMeshLineResolution(width, height) {
5147
+ if (!this.modelGroup || !width || !height) return;
5148
+ this.modelGroup.traverse(child => {
5149
+ const material = child && child.material;
5150
+ if (material && material.type === 'MeshLineMaterial' && material.resolution) {
5151
+ material.resolution.set(width, height);
5152
+ material.needsUpdate = true;
5153
+ }
5154
+ });
5155
+ },
4474
5156
  setModelCenter(target, options = {}) {
4475
5157
  // return
4476
5158
  let box3;
@@ -4644,7 +5326,9 @@ export default {
4644
5326
  children.instanceMatrix.needsUpdate = true;
4645
5327
  });
4646
5328
  this.refreshCollisionObbDebug();
4647
- this.notifyCameraChange(); // 触发场景更新
5329
+ this.refreshOcclusionBoundsDebug();
5330
+ // 调用此方法后 模型隐藏后再执行显示操作后无法正常显示,故在此先注释
5331
+ // this.notifyCameraChange(); // 触发场景更新
4648
5332
  break;
4649
5333
  }
4650
5334
  case 'opacity':
@@ -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();