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

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,7 +165,7 @@ 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_PENETRATION_EPSILON = 1;
169
169
  const COLLISION_OBB_AXIS_EPSILON = 1e-8;
170
170
  const COLLISION_DEBUG_GROUP_NAME = '__collision_obb_debug_group__';
171
171
 
@@ -215,7 +215,9 @@ export default {
215
215
  isPaused: false,
216
216
  },
217
217
  isolateMode: false,
218
- collisionObbDebugEnabled: true,
218
+ sceneBoundingBoxDebugEnabled: false,
219
+ // collisionObbDebugEnabled: isDebug,
220
+ collisionObbDebugEnabled: false,
219
221
  };
220
222
  },
221
223
  watch: {
@@ -224,6 +226,13 @@ export default {
224
226
  this.detachTransformControls();
225
227
  }
226
228
  },
229
+ sceneBoundingBoxDebugEnabled(val) {
230
+ if (val) {
231
+ this.refreshSceneBoundingBoxHelper(true);
232
+ } else {
233
+ this.hideSceneBoundingBox();
234
+ }
235
+ },
227
236
  },
228
237
  beforeCreate() {
229
238
  this.spaceUp = true;
@@ -360,11 +369,16 @@ export default {
360
369
  wheelTimeout: null,
361
370
  },
362
371
  },
372
+ cameraControlMoveObserver: {
373
+ enabled: false,
374
+ interval: 500,
375
+ },
363
376
  streamLoader: null,
364
377
  // isPerformingInitialCentering: false, // 标记是否正在执行初始居中
365
378
  isObserverEnabled: true, // 标记相机变更观察者是否启用
366
379
  sceneBoxes: new Map(),
367
380
  documentModelIds: new Map(),
381
+ documentSceneBoundsIds: new Map(),
368
382
  outlineInstanceProxyMap: new Map(),
369
383
  occlusionWorker: null,
370
384
  occlusionWorkerRequestMap: new Map(),
@@ -1469,71 +1483,108 @@ export default {
1469
1483
  updateGlobalSceneBoundingBox() {
1470
1484
  const state = this.noObserver || {};
1471
1485
  const occScene = state.occlusionState && state.occlusionState._occScene;
1486
+ const mergedBox = new this.THREE.Box3();
1487
+ let hasValidBox = false;
1488
+ const mergeBox = box => {
1489
+ if (!box || !box.isBox3 || box.isEmpty()) return;
1490
+ if (!hasValidBox) {
1491
+ mergedBox.copy(box);
1492
+ hasValidBox = true;
1493
+ return;
1494
+ }
1495
+ mergedBox.union(box);
1496
+ };
1472
1497
 
1473
- // 优先使用由 boxIndex 构建出的离屏全场景,作为整体居中与 home 的统一范围
1498
+ // 离屏遮挡场景只包含参与遮挡的模型,需要继续合并场景范围索引中的二维图形范围。
1474
1499
  if (occScene && occScene.children && occScene.children.length > 0) {
1475
1500
  const occBox = new this.THREE.Box3().setFromObject(occScene);
1476
- if (!occBox.isEmpty()) {
1477
- this.sceneBoundingBox = occBox;
1478
- return this.sceneBoundingBox;
1479
- }
1501
+ mergeBox(occBox);
1480
1502
  }
1481
1503
 
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;
1504
+ // sceneBoundsIndex 包含二维和三维图形,是全场景 dolly 距离的主要范围来源。
1505
+ if (this._sceneBoundsIndex && this._sceneBoundsIndex.size > 0) {
1506
+ this._sceneBoundsIndex.forEach(box => {
1507
+ mergeBox(box);
1500
1508
  });
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
1509
  }
1516
1510
 
1517
1511
  const boxes = Array.from((state.sceneBoxes && state.sceneBoxes.values()) || []);
1518
1512
  if (boxes.length > 0) {
1519
- const firstBox = boxes[0].clone();
1520
- for (let i = 1; i < boxes.length; i++) {
1521
- firstBox.union(boxes[i]);
1513
+ for (let i = 0; i < boxes.length; i++) {
1514
+ mergeBox(boxes[i]);
1522
1515
  }
1523
- this.sceneBoundingBox = firstBox;
1516
+ }
1517
+
1518
+ if (hasValidBox) {
1519
+ this.sceneBoundingBox = mergedBox;
1520
+ this.refreshSceneBoundingBoxHelper(this.sceneBoundingBoxDebugEnabled);
1524
1521
  return this.sceneBoundingBox;
1525
1522
  }
1526
1523
 
1527
1524
  this.sceneBoundingBox = new this.THREE.Box3();
1525
+ this.refreshSceneBoundingBoxHelper(this.sceneBoundingBoxDebugEnabled);
1528
1526
  return this.sceneBoundingBox;
1529
1527
  },
1528
+ upsertIndexedBox(indexMap, modelId, box) {
1529
+ if (!indexMap || !modelId || !box || !box.isBox3) return null;
1530
+ const key = String(modelId);
1531
+ const existingBox = indexMap.get(key);
1532
+ if (!existingBox) {
1533
+ indexMap.set(key, box);
1534
+ return box;
1535
+ }
1536
+ existingBox.union(box);
1537
+ return existingBox;
1538
+ },
1539
+ mergeModelBoxUserData(targetBox, sourceUserData) {
1540
+ if (!targetBox || !sourceUserData) return;
1541
+ if (!targetBox.userData) {
1542
+ targetBox.userData = sourceUserData;
1543
+ return;
1544
+ }
1545
+
1546
+ const targetUserData = targetBox.userData;
1547
+ targetUserData.visible = targetUserData.visible !== false || sourceUserData.visible !== false;
1548
+ targetUserData.sourceVisible =
1549
+ targetUserData.sourceVisible !== false || sourceUserData.sourceVisible !== false;
1550
+ targetUserData.transparent = !!targetUserData.transparent && !!sourceUserData.transparent;
1551
+ targetUserData.mergedBoxCount = (targetUserData.mergedBoxCount || 1) + 1;
1552
+ // 同一模型包含多个图元时,单个图元的遮挡代理不再可靠,改由视锥体合并盒兜底加载。
1553
+ targetUserData.occlusionEnabled = false;
1554
+
1555
+ const targetObbs = Array.isArray(targetUserData.collisionObbs)
1556
+ ? targetUserData.collisionObbs
1557
+ : [];
1558
+ const sourceObbs = Array.isArray(sourceUserData.collisionObbs)
1559
+ ? sourceUserData.collisionObbs
1560
+ : [];
1561
+ if (sourceObbs.length > 0) {
1562
+ targetUserData.collisionObbs = targetObbs.concat(sourceObbs);
1563
+ }
1564
+ },
1565
+ upsertModelBox(modelId, box) {
1566
+ if (!this._boxIndex || !modelId || !box || !box.isBox3) return;
1567
+ const key = String(modelId);
1568
+ const existingBox = this._boxIndex.get(key);
1569
+ if (!existingBox) {
1570
+ this._boxIndex.set(key, box);
1571
+ return;
1572
+ }
1573
+ existingBox.union(box);
1574
+ this.mergeModelBoxUserData(existingBox, box.userData);
1575
+ },
1530
1576
  setBoxIndex(boxJson, documentId, isAdd = true) {
1531
1577
  if (!this._boxIndex) this._boxIndex = new Map();
1578
+ if (!this._sceneBoundsIndex) this._sceneBoundsIndex = new Map();
1532
1579
  if (!documentId) {
1533
1580
  this._boxIndex.clear();
1581
+ this._sceneBoundsIndex.clear();
1534
1582
  if (this.noObserver && this.noObserver.documentModelIds) {
1535
1583
  this.noObserver.documentModelIds.clear();
1536
1584
  }
1585
+ if (this.noObserver && this.noObserver.documentSceneBoundsIds) {
1586
+ this.noObserver.documentSceneBoundsIds.clear();
1587
+ }
1537
1588
  this.hasExecutedCentering = false;
1538
1589
  this.buildOctreeFromBoxIndex();
1539
1590
  return;
@@ -1542,19 +1593,35 @@ export default {
1542
1593
  if (isAdd && boxJson) {
1543
1594
  const arr = boxJson ? boxJson : [];
1544
1595
  const modelIds = new Set();
1596
+ const sceneBoundIds = new Set();
1545
1597
  const validGeomTypes = new Set(Object.values(GEOM_TYPES));
1546
1598
  for (let i = 0; i < arr.length; i++) {
1547
1599
  const it = arr[i];
1548
1600
  if (!validGeomTypes.has(it.geomType)) {
1549
1601
  continue;
1550
1602
  }
1551
- if (it.obb.length == 0) {
1603
+ const minSource = it.min;
1604
+ const maxSource = it.max;
1605
+ const hasValidMinMax =
1606
+ minSource &&
1607
+ maxSource &&
1608
+ minSource.length >= 3 &&
1609
+ maxSource.length >= 3 &&
1610
+ Number.isFinite(minSource[0]) &&
1611
+ Number.isFinite(minSource[1]) &&
1612
+ Number.isFinite(minSource[2]) &&
1613
+ Number.isFinite(maxSource[0]) &&
1614
+ Number.isFinite(maxSource[1]) &&
1615
+ Number.isFinite(maxSource[2]);
1616
+ if (!hasValidMinMax) {
1552
1617
  continue;
1553
1618
  }
1619
+ const obbData = it.obb || [];
1620
+ const modelId = String(it.id);
1554
1621
 
1555
1622
  // 使用新的 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]);
1623
+ const min = new this.THREE.Vector3(minSource[0], minSource[1], minSource[2]);
1624
+ const max = new this.THREE.Vector3(maxSource[0], maxSource[1], maxSource[2]);
1558
1625
 
1559
1626
  const sourceBox = new this.THREE.Box3(min.clone(), max.clone());
1560
1627
  // 八叉树仍使用 AABB 粗筛,最终碰撞使用 sourceBox 派生的 OBB 精筛
@@ -1588,10 +1655,18 @@ export default {
1588
1655
  boxThree.applyMatrix4(collisionMatrix);
1589
1656
  }
1590
1657
 
1658
+ this.upsertIndexedBox(this._sceneBoundsIndex, modelId, boxThree.clone());
1659
+ sceneBoundIds.add(modelId);
1660
+
1661
+ // 没有 OBB 数据的二维图形只参与全场景范围,不纳入碰撞和遮挡索引。
1662
+ if (obbData.length === 0) {
1663
+ continue;
1664
+ }
1665
+
1591
1666
  const userData = {
1592
1667
  flag: it.flag || 1, // 默认为 1
1593
1668
  geomType: it.geomType,
1594
- obbData: it.obb, // 存储原始 OBB 数据
1669
+ obbData, // 存储原始 OBB 数据
1595
1670
  transparent: it.transp > 0,
1596
1671
  sourceVisible: it.visible === false ? false : true,
1597
1672
  visible: it.visible === false ? false : true, // 默认显示
@@ -1605,8 +1680,7 @@ export default {
1605
1680
  // userData.matrix = new this.THREE.Matrix4().identity();
1606
1681
  }
1607
1682
  // 如果是 flag=3,解析并存储中心点、半轴长、旋转矩阵
1608
- else if (it.flag === 3 && it.obb && it.obb.length === 15) {
1609
- const obbData = it.obb;
1683
+ else if (it.flag === 3 && obbData.length === 15) {
1610
1684
  const center = new this.THREE.Vector3(obbData[0], obbData[1], obbData[2]);
1611
1685
  const halfSize = new this.THREE.Vector3(obbData[3], obbData[4], obbData[5]);
1612
1686
  const rotation = new this.THREE.Matrix3().fromArray(obbData.slice(6, 15));
@@ -1636,13 +1710,13 @@ export default {
1636
1710
  userData.collisionObbs = [collisionObb];
1637
1711
  }
1638
1712
  // 如果是 flag=2,解析并存储多个子包围盒 (min, max)
1639
- else if (it.flag === 2 && it.obb && it.obb.length > 0) {
1713
+ else if (it.flag === 2 && obbData.length > 0) {
1640
1714
  const subBoxes = [];
1641
1715
  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]);
1716
+ for (let k = 0; k < obbData.length; k += 6) {
1717
+ if (k + 5 < obbData.length) {
1718
+ const min = new this.THREE.Vector3(obbData[k], obbData[k + 1], obbData[k + 2]);
1719
+ const max = new this.THREE.Vector3(obbData[k + 3], obbData[k + 4], obbData[k + 5]);
1646
1720
  subBoxes.push({ min, max });
1647
1721
 
1648
1722
  const subBox = new this.THREE.Box3(min.clone(), max.clone());
@@ -1669,11 +1743,11 @@ export default {
1669
1743
 
1670
1744
  boxThree.userData = userData;
1671
1745
 
1672
- const modelId = String(it.id);
1673
- this._boxIndex.set(modelId, boxThree);
1746
+ this.upsertModelBox(modelId, boxThree);
1674
1747
  modelIds.add(modelId);
1675
1748
  }
1676
1749
  this.noObserver.documentModelIds.set(documentId, modelIds);
1750
+ this.noObserver.documentSceneBoundsIds.set(documentId, sceneBoundIds);
1677
1751
  } else {
1678
1752
  const modelIds = this.noObserver.documentModelIds.get(documentId);
1679
1753
  if (modelIds) {
@@ -1687,6 +1761,15 @@ export default {
1687
1761
  this.hasExecutedCentering = false;
1688
1762
  }
1689
1763
  }
1764
+ const sceneBoundIds =
1765
+ this.noObserver.documentSceneBoundsIds &&
1766
+ this.noObserver.documentSceneBoundsIds.get(documentId);
1767
+ if (sceneBoundIds) {
1768
+ sceneBoundIds.forEach(id => {
1769
+ this._sceneBoundsIndex.delete(id);
1770
+ });
1771
+ this.noObserver.documentSceneBoundsIds.delete(documentId);
1772
+ }
1690
1773
  }
1691
1774
  this.buildOctreeFromBoxIndex();
1692
1775
  this.refreshCollisionObbDebug();
@@ -1703,7 +1786,9 @@ export default {
1703
1786
  this.sceneBoundingBox.isEmpty()
1704
1787
  )
1705
1788
  return;
1706
- if (!this._boxIndex || this._boxIndex.size === 0) return;
1789
+ const hasBoxIndex = this._boxIndex && this._boxIndex.size > 0;
1790
+ const hasSceneBoundsIndex = this._sceneBoundsIndex && this._sceneBoundsIndex.size > 0;
1791
+ if (!hasBoxIndex && !hasSceneBoundsIndex) return;
1707
1792
 
1708
1793
  this.smartModelCenter(this.sceneBoundingBox, 0);
1709
1794
  },
@@ -1805,6 +1890,53 @@ export default {
1805
1890
  }
1806
1891
  node.items.push({ id, box });
1807
1892
  },
1893
+ _buildCollisionOctreeFromEntries(entries) {
1894
+ if (!Array.isArray(entries) || entries.length === 0) {
1895
+ return null;
1896
+ }
1897
+
1898
+ let minX = Infinity;
1899
+ let minY = Infinity;
1900
+ let minZ = Infinity;
1901
+ let maxX = -Infinity;
1902
+ let maxY = -Infinity;
1903
+ let maxZ = -Infinity;
1904
+
1905
+ entries.forEach(item => {
1906
+ const box = item && item.box;
1907
+ if (!box || !box.isBox3) return;
1908
+ const mn = box.min;
1909
+ const mx = box.max;
1910
+ if (mn.x < minX) minX = mn.x;
1911
+ if (mn.y < minY) minY = mn.y;
1912
+ if (mn.z < minZ) minZ = mn.z;
1913
+ if (mx.x > maxX) maxX = mx.x;
1914
+ if (mx.y > maxY) maxY = mx.y;
1915
+ if (mx.z > maxZ) maxZ = mx.z;
1916
+ });
1917
+
1918
+ if (minX === Infinity) {
1919
+ return null;
1920
+ }
1921
+
1922
+ const rootBox = new this.THREE.Box3(
1923
+ new this.THREE.Vector3(minX, minY, minZ),
1924
+ new this.THREE.Vector3(maxX, maxY, maxZ)
1925
+ );
1926
+ const root = {
1927
+ box: rootBox,
1928
+ items: [],
1929
+ children: null,
1930
+ depth: 0,
1931
+ };
1932
+
1933
+ entries.forEach(item => {
1934
+ if (!item || !item.box) return;
1935
+ this._octreeInsert(root, String(item.id), item.box);
1936
+ });
1937
+
1938
+ return root;
1939
+ },
1808
1940
  _isCollisionBoxAvailable(box) {
1809
1941
  if (!box || !box.isBox3 || box.isEmpty()) return false;
1810
1942
  if (box.userData && box.userData.visible === false) return false;
@@ -1935,7 +2067,8 @@ export default {
1935
2067
  _buildCollisionPairMap() {
1936
2068
  const pairMap = new Map();
1937
2069
  const collisionPairs = this.getOctreeCollisionPairs();
1938
- const pairs = collisionPairs && Array.isArray(collisionPairs.pairs) ? collisionPairs.pairs : [];
2070
+ const pairs =
2071
+ collisionPairs && Array.isArray(collisionPairs.pairs) ? collisionPairs.pairs : [];
1939
2072
  for (let i = 0; i < pairs.length; i++) {
1940
2073
  const pair = pairs[i];
1941
2074
  if (!pair) continue;
@@ -2071,9 +2204,8 @@ export default {
2071
2204
  }
2072
2205
  return false;
2073
2206
  },
2074
- _queryOctreeByBox(box) {
2207
+ _queryOctreeByBoxWithRoot(root, box) {
2075
2208
  const results = [];
2076
- const root = this._octree;
2077
2209
  if (!root || !this._isCollisionBoxAvailable(box)) return results;
2078
2210
 
2079
2211
  const stack = [root];
@@ -2099,25 +2231,88 @@ export default {
2099
2231
 
2100
2232
  return results;
2101
2233
  },
2102
- getOctreeCollisionPairs() {
2234
+ _queryOctreeByBox(box) {
2235
+ return this._queryOctreeByBoxWithRoot(this._octree, box);
2236
+ },
2237
+ getOctreeCollisionPairs(options = {}) {
2103
2238
  const pairs = [];
2104
2239
  if (!this._boxIndex || this._boxIndex.size === 0) return { pairs };
2105
2240
 
2106
- if (!this._octree) {
2107
- this.buildOctreeFromBoxIndex();
2241
+ const normalizeIds = value =>
2242
+ Array.isArray(value)
2243
+ ? Array.from(
2244
+ new Set(
2245
+ value
2246
+ .map(item => (item === null || item === undefined ? '' : String(item)))
2247
+ .filter(item => item !== '')
2248
+ )
2249
+ )
2250
+ : [];
2251
+
2252
+ const getEntriesByIds = ids => {
2253
+ const entries = [];
2254
+ const idSet = new Set(ids);
2255
+ this._boxIndex.forEach((box, id) => {
2256
+ const key = String(id);
2257
+ if (idSet.has(key)) {
2258
+ entries.push({
2259
+ id: key,
2260
+ box,
2261
+ });
2262
+ }
2263
+ });
2264
+ return entries;
2265
+ };
2266
+
2267
+ const sourceIds = normalizeIds(options.sourceIds);
2268
+ const targetIds = normalizeIds(options.targetIds);
2269
+ const includeIds = Array.isArray(options.includeIds) ? normalizeIds(options.includeIds) : [];
2270
+
2271
+ let root = this._octree;
2272
+ let sourceEntries = [];
2273
+ let dedupePairKeySet = null;
2274
+
2275
+ if (sourceIds.length > 0 && targetIds.length > 0) {
2276
+ const targetEntries = getEntriesByIds(targetIds);
2277
+ sourceEntries = getEntriesByIds(sourceIds);
2278
+ if (sourceEntries.length === 0 || targetEntries.length === 0) return { pairs };
2279
+ root = this._buildCollisionOctreeFromEntries(targetEntries);
2280
+ if (!root) return { pairs };
2281
+ dedupePairKeySet = new Set();
2282
+ } else if (includeIds.length > 0) {
2283
+ sourceEntries = getEntriesByIds(includeIds);
2284
+ if (sourceEntries.length === 0) return { pairs };
2285
+ root = this._buildCollisionOctreeFromEntries(sourceEntries);
2286
+ if (!root) return { pairs };
2287
+ } else {
2288
+ if (!this._octree) {
2289
+ this.buildOctreeFromBoxIndex();
2290
+ }
2291
+ if (!this._octree) return { pairs };
2292
+ this._boxIndex.forEach((box, id) => {
2293
+ sourceEntries.push({
2294
+ id: String(id),
2295
+ box,
2296
+ });
2297
+ });
2108
2298
  }
2109
- if (!this._octree) return { pairs };
2110
2299
 
2111
- this._boxIndex.forEach((boxA, idA) => {
2300
+ sourceEntries.forEach(({ id: idA, box: boxA }) => {
2112
2301
  if (!this._isCollisionBoxAvailable(boxA)) return;
2113
2302
 
2114
- const hits = this._queryOctreeByBox(boxA);
2303
+ const hits = this._queryOctreeByBoxWithRoot(root, boxA);
2115
2304
  for (let i = 0; i < hits.length; i++) {
2116
2305
  const item = hits[i];
2117
2306
  if (!item || String(item.id) === String(idA)) continue;
2118
2307
  const boxB = item.box;
2119
2308
  if (!this._isCollisionBoxMatched(boxA, boxB)) continue;
2120
2309
 
2310
+ if (dedupePairKeySet) {
2311
+ const pairKey = `${String(idA)}__${String(item.id)}`;
2312
+ if (dedupePairKeySet.has(pairKey)) continue;
2313
+ dedupePairKeySet.add(pairKey);
2314
+ }
2315
+
2121
2316
  pairs.push({
2122
2317
  idA: this._formatCollisionPairId(idA),
2123
2318
  idB: this._formatCollisionPairId(item.id),
@@ -2571,7 +2766,8 @@ export default {
2571
2766
  /**
2572
2767
  * 执行视椎体裁切,清理视椎体外的InstancedMesh实例
2573
2768
  */
2574
- async performFrustumCulling() {
2769
+ async performFrustumCulling(options = {}) {
2770
+ const skipUnload = !!(options && options.skipUnload);
2575
2771
  const modelState = this.noObserver
2576
2772
  ? this.noObserver.modelStateManager
2577
2773
  : this.modelStateManager;
@@ -3340,7 +3536,7 @@ export default {
3340
3536
  }
3341
3537
 
3342
3538
  // 仅当所有实例都不活跃时,才执行卸载
3343
- if (!this.firstPerSign && !this.roaming && allInactive) {
3539
+ if (!skipUnload && !this.firstPerSign && !this.roaming && allInactive) {
3344
3540
  toUnload.push({ modelId, child });
3345
3541
  }
3346
3542
  }
@@ -3373,7 +3569,7 @@ export default {
3373
3569
  if (modelInVisible) {
3374
3570
  toLoadSet.delete(modelId);
3375
3571
  }
3376
- if (!this.firstPerSign && !this.roaming && (!inFrustum || !modelInVisible)) {
3572
+ if (!skipUnload && !this.firstPerSign && !this.roaming && (!inFrustum || !modelInVisible)) {
3377
3573
  toUnload.push({ modelId, child });
3378
3574
  }
3379
3575
  });
@@ -3491,9 +3687,15 @@ export default {
3491
3687
  * @param {Object} config - 配置项
3492
3688
  * @param {Object} config.modelApi - 模型API接口
3493
3689
  * @param {string} [config.projectId] - 项目ID
3690
+ * @param {Object} [config.cameraControlMoveObserver] - 普通视角运动中触发加载的配置
3691
+ * @param {boolean} [config.cameraControlMoveObserver.enabled=false] - 是否开启普通视角运动中加载
3692
+ * @param {number} [config.cameraControlMoveObserver.interval=500] - 触发间隔,单位毫秒
3693
+ * @param {boolean} [config.enableCameraControlMoveObserver=false] - 是否开启普通视角运动中加载
3694
+ * @param {number} [config.cameraControlMoveInterval=500] - 触发间隔,单位毫秒
3494
3695
  */
3495
3696
  init(config = {}) {
3496
3697
  console.log('init');
3698
+ this.setCameraControlMoveObserverConfig(config);
3497
3699
  this.initStreamLoader(config.modelApi, config);
3498
3700
  },
3499
3701
 
@@ -3920,6 +4122,124 @@ export default {
3920
4122
  this.noObserver.isObserverEnabled = enabled;
3921
4123
  },
3922
4124
 
4125
+ setCameraControlMoveObserverConfig(config = {}) {
4126
+ if (!this.noObserver) return;
4127
+ const nestedConfig = config.cameraControlMoveObserver || {};
4128
+ const enabled =
4129
+ typeof nestedConfig.enabled === 'boolean'
4130
+ ? nestedConfig.enabled
4131
+ : typeof config.enableCameraControlMoveObserver === 'boolean'
4132
+ ? config.enableCameraControlMoveObserver
4133
+ : false;
4134
+ const rawInterval =
4135
+ nestedConfig.interval !== undefined
4136
+ ? nestedConfig.interval
4137
+ : config.cameraControlMoveInterval;
4138
+ const interval = Number(rawInterval);
4139
+ const safeInterval = Number.isFinite(interval) && interval > 0 ? interval : 500;
4140
+
4141
+ this.noObserver.cameraControlMoveObserver = {
4142
+ enabled,
4143
+ interval: safeInterval,
4144
+ };
4145
+ this.resetCameraControlMoveSnapshot(false);
4146
+ },
4147
+
4148
+ isCameraControlMoveObserverEnabled() {
4149
+ const config = this.noObserver && this.noObserver.cameraControlMoveObserver;
4150
+ return !!(config && config.enabled);
4151
+ },
4152
+
4153
+ getCameraControlTarget(target) {
4154
+ if (!target || !this.cameraControls) return false;
4155
+ if (typeof this.cameraControls.getTarget === 'function') {
4156
+ this.cameraControls.getTarget(target);
4157
+ return true;
4158
+ }
4159
+ if (this.cameraControls._target) {
4160
+ target.copy(this.cameraControls._target);
4161
+ return true;
4162
+ }
4163
+ return false;
4164
+ },
4165
+
4166
+ resetCameraControlMoveSnapshot(syncCurrent = false) {
4167
+ if (!this.noObserver) return;
4168
+ if (!this.noObserver._cameraControlMoveState) {
4169
+ this.noObserver._cameraControlMoveState = {
4170
+ position: new this.THREE.Vector3(),
4171
+ quaternion: new this.THREE.Quaternion(),
4172
+ target: new this.THREE.Vector3(),
4173
+ currentTarget: new this.THREE.Vector3(),
4174
+ initialized: false,
4175
+ hasTarget: false,
4176
+ };
4177
+ }
4178
+ const state = this.noObserver._cameraControlMoveState;
4179
+ if (!syncCurrent || !this.camera) {
4180
+ state.initialized = false;
4181
+ state.hasTarget = false;
4182
+ return;
4183
+ }
4184
+
4185
+ this.camera.updateMatrixWorld(true);
4186
+ state.position.copy(this.camera.position);
4187
+ state.quaternion.copy(this.camera.quaternion);
4188
+ state.hasTarget = this.getCameraControlTarget(state.target);
4189
+ state.initialized = true;
4190
+ },
4191
+
4192
+ observeCameraControlMove() {
4193
+ if (
4194
+ !this.noObserver ||
4195
+ !this.noObserver.isObserverEnabled ||
4196
+ !this.isCameraControlMoveObserverEnabled() ||
4197
+ !this.camera ||
4198
+ !this.cameraControls ||
4199
+ typeof this._cameraChangeObserver !== 'function'
4200
+ ) {
4201
+ return;
4202
+ }
4203
+
4204
+ const loadingState = this.noObserver.batchLoadingState || {};
4205
+ const interactionState = loadingState.interactionState || {};
4206
+ const wheelActive = !!interactionState.wheelTimeout;
4207
+ const controlActive = !!this.noObserver.isControlActive;
4208
+ if ((!controlActive && !wheelActive) || this.firstPerSign) {
4209
+ this.resetCameraControlMoveSnapshot(false);
4210
+ return;
4211
+ }
4212
+
4213
+ if (!this.noObserver._cameraControlMoveState) {
4214
+ this.resetCameraControlMoveSnapshot(true);
4215
+ return;
4216
+ }
4217
+
4218
+ const state = this.noObserver._cameraControlMoveState;
4219
+ if (!state.initialized) {
4220
+ this.resetCameraControlMoveSnapshot(true);
4221
+ return;
4222
+ }
4223
+
4224
+ this.camera.updateMatrixWorld(true);
4225
+ const hasTarget = this.getCameraControlTarget(state.currentTarget);
4226
+ const positionMoved = state.position.distanceToSquared(this.camera.position) > 1e-8;
4227
+ const quaternionMoved = 1 - Math.abs(state.quaternion.dot(this.camera.quaternion)) > 1e-10;
4228
+ const targetMoved =
4229
+ hasTarget !== state.hasTarget ||
4230
+ (hasTarget && state.target.distanceToSquared(state.currentTarget) > 1e-8);
4231
+
4232
+ if (!positionMoved && !quaternionMoved && !targetMoved) return;
4233
+
4234
+ state.position.copy(this.camera.position);
4235
+ state.quaternion.copy(this.camera.quaternion);
4236
+ state.hasTarget = hasTarget;
4237
+ if (hasTarget) {
4238
+ state.target.copy(state.currentTarget);
4239
+ }
4240
+ this._cameraChangeObserver('controlMove');
4241
+ },
4242
+
3923
4243
  /**
3924
4244
  * 初始化相机变更观察者 (CameraChangeObserver)
3925
4245
  * 统一监听:用户交互(Wheel/Control)、程序化动画、API调用
@@ -3954,7 +4274,9 @@ export default {
3954
4274
  // 执行视锥体裁剪
3955
4275
  // 第一视角不裁切
3956
4276
  if (this.modelStateManager.frustumCheckEnabled) {
3957
- await this.performFrustumCulling();
4277
+ await this.performFrustumCulling({
4278
+ skipUnload: source === 'controlMove',
4279
+ });
3958
4280
  }
3959
4281
 
3960
4282
  // 执行流式加载请求
@@ -3966,11 +4288,19 @@ export default {
3966
4288
  }
3967
4289
  },
3968
4290
  source => {
3969
- // 漫游模式下,或者第一人称移动时,使用节流策略(每300ms触发一次)
4291
+ // 漫游、第一人称移动、普通视角拖拽/缩放中,使用节流策略避免持续运动被防抖吞掉。
3970
4292
  // 避免在持续运动中因防抖重置定时器而无法触发
3971
4293
  if (source === 'roam' || source === 'firstPersonMove') {
3972
4294
  return { type: 'throttle', limit: 1000 };
3973
4295
  }
4296
+ if (source === 'controlMove') {
4297
+ const config = this.noObserver && this.noObserver.cameraControlMoveObserver;
4298
+ const interval =
4299
+ config && Number.isFinite(config.interval) && config.interval > 0
4300
+ ? config.interval
4301
+ : 500;
4302
+ return { type: 'throttle', limit: interval };
4303
+ }
3974
4304
  return 300; // 默认防抖 300ms
3975
4305
  }
3976
4306
  );
@@ -3984,8 +4314,20 @@ export default {
3984
4314
  this._wheelHandler = res => {
3985
4315
  this.$emit('wheelStart', res);
3986
4316
 
3987
- // 统一交互开始处理
3988
- this.beginInteraction('wheel');
4317
+ const cameraControlMoveEnabled = this.isCameraControlMoveObserverEnabled();
4318
+ const loadingState = this.noObserver
4319
+ ? this.noObserver.batchLoadingState
4320
+ : this.batchLoadingState;
4321
+ const wheelAlreadyActive =
4322
+ loadingState &&
4323
+ loadingState.interactionState &&
4324
+ loadingState.interactionState.wheelTimeout;
4325
+ if (cameraControlMoveEnabled && !wheelAlreadyActive) {
4326
+ this.resetCameraControlMoveSnapshot(true);
4327
+ } else if (!cameraControlMoveEnabled) {
4328
+ // 未开启普通视角运动加载时,保持原有交互中断流程。
4329
+ this.beginInteraction('wheel');
4330
+ }
3989
4331
 
3990
4332
  // 设置滚轮交互的结束检测
3991
4333
  this.scheduleWheelInteractionEnd(res, 100);
@@ -4027,9 +4369,12 @@ export default {
4027
4369
  // 4. 监听器 - Controls (用户拖拽/操作)
4028
4370
  this._onControlStart = res => {
4029
4371
  this.noObserver.isControlActive = true;
4030
-
4031
- // 统一交互开始处理(相机)
4032
- this.beginInteraction('this.camera');
4372
+ if (this.isCameraControlMoveObserverEnabled()) {
4373
+ this.resetCameraControlMoveSnapshot(true);
4374
+ } else {
4375
+ // 未开启普通视角运动加载时,保持原有交互中断流程。
4376
+ this.beginInteraction('this.camera');
4377
+ }
4033
4378
  this.$emit('controlStart', res);
4034
4379
 
4035
4380
  // 记录鼠标按下时的位置
@@ -4046,8 +4391,10 @@ export default {
4046
4391
 
4047
4392
  this.$emit('controlEnd', res);
4048
4393
 
4049
- // 统一交互结束处理(相机)
4050
- this.endInteraction('this.camera', res, { immediateResume: false });
4394
+ if (!this.isCameraControlMoveObserverEnabled()) {
4395
+ // 未开启普通视角运动加载时,保持原有交互中断流程。
4396
+ this.endInteraction('this.camera', res, { immediateResume: false });
4397
+ }
4051
4398
 
4052
4399
  // 记录鼠标抬起时的位置并计算是否发生了拖拽
4053
4400
  const event = res.originalEvent || window.event;
@@ -4076,6 +4423,9 @@ export default {
4076
4423
  // 触发观察者
4077
4424
  this._cameraChangeObserver('control');
4078
4425
  }
4426
+ if (this.isCameraControlMoveObserverEnabled()) {
4427
+ this.resetCameraControlMoveSnapshot(false);
4428
+ }
4079
4429
  };
4080
4430
 
4081
4431
  this.cameraControls.addEventListener('controlstart', this._onControlStart);
@@ -4136,12 +4486,37 @@ export default {
4136
4486
  },
4137
4487
  // 动态设置视角滚轮的距离
4138
4488
  setCameraConfig() {
4489
+ if (
4490
+ !this.sceneBoundingBox ||
4491
+ !this.sceneBoundingBox.isBox3 ||
4492
+ this.sceneBoundingBox.isEmpty()
4493
+ ) {
4494
+ return;
4495
+ }
4139
4496
  // let box3 = new this.THREE.Box3().setFromObject(this.sceneBoundingBox);
4140
4497
  let size = new this.THREE.Vector3();
4141
4498
  this.sceneBoundingBox.getSize(size);
4142
4499
  const maxBorder = Math.max(size.x, size.y, size.z);
4500
+ if (isDebug) {
4501
+ const center = this.sceneBoundingBox.getCenter(new this.THREE.Vector3());
4502
+ console.log('sceneBoundingBox 调试信息', {
4503
+ size: {
4504
+ x: size.x,
4505
+ y: size.y,
4506
+ z: size.z,
4507
+ },
4508
+ center: {
4509
+ x: center.x,
4510
+ y: center.y,
4511
+ z: center.z,
4512
+ },
4513
+ maxBorder,
4514
+ });
4515
+ this.refreshSceneBoundingBoxHelper(this.sceneBoundingBoxDebugEnabled);
4516
+ }
4143
4517
  // this.cameraControls.this.camera.far = maxBorder * 10; // 设置相机的远裁剪面
4144
4518
  this.cameraControls.minDistance = maxBorder * 0.2; // 动态设置视角滚轮的距离
4519
+ // this.cameraControls.minDistance = 1000; // 动态设置视角滚轮的距离
4145
4520
  this.camera.updateProjectionMatrix();
4146
4521
  },
4147
4522
  // 获取mesh的中心点
@@ -4469,8 +4844,19 @@ export default {
4469
4844
  if (this.threeMeasure) {
4470
4845
  this.threeMeasure.updateParams(width, height);
4471
4846
  }
4847
+ this.updateMeshLineResolution(width, height);
4472
4848
  }
4473
4849
  },
4850
+ updateMeshLineResolution(width, height) {
4851
+ if (!this.modelGroup || !width || !height) return;
4852
+ this.modelGroup.traverse(child => {
4853
+ const material = child && child.material;
4854
+ if (material && material.type === 'MeshLineMaterial' && material.resolution) {
4855
+ material.resolution.set(width, height);
4856
+ material.needsUpdate = true;
4857
+ }
4858
+ });
4859
+ },
4474
4860
  setModelCenter(target, options = {}) {
4475
4861
  // return
4476
4862
  let box3;
@@ -6545,6 +6931,7 @@ export default {
6545
6931
  });
6546
6932
  }
6547
6933
  this.cameraControls.enabled && this.cameraControls.update(this.timeStamp);
6934
+ this.observeCameraControlMove();
6548
6935
  // 计算相机近裁面到包围盒当前面的距离
6549
6936
  this.cameraTrack();
6550
6937
  this.firstPerspective();
@@ -6755,6 +7142,13 @@ export default {
6755
7142
  const loadingState = this.noObserver
6756
7143
  ? this.noObserver.batchLoadingState
6757
7144
  : this.batchLoadingState;
7145
+ const rect = this.renderer
7146
+ ? this.renderer.domElement.getBoundingClientRect()
7147
+ : { width: window.innerWidth, height: window.innerHeight };
7148
+ options.meshLineResolution = {
7149
+ width: rect.width || window.innerWidth || 1,
7150
+ height: rect.height || window.innerHeight || 1,
7151
+ };
6758
7152
  // 如果已经在加载中,先停止之前的加载
6759
7153
  if (loadingState.isLoading) {
6760
7154
  this.stopBatchLoading('restart');
@@ -7318,8 +7712,15 @@ export default {
7318
7712
 
7319
7713
  loadingState.interactionState.wheelTimeout = setTimeout(() => {
7320
7714
  this.$emit('wheelEnd', event);
7321
- this.endInteraction('wheel', event, { immediateResume: false });
7322
7715
  loadingState.interactionState.wheelTimeout = null;
7716
+ if (this.isCameraControlMoveObserverEnabled()) {
7717
+ this.resetCameraControlMoveSnapshot(false);
7718
+ if (typeof this._cameraChangeObserver === 'function') {
7719
+ this._cameraChangeObserver('wheel');
7720
+ }
7721
+ } else {
7722
+ this.endInteraction('wheel', event, { immediateResume: false });
7723
+ }
7323
7724
  }, delay);
7324
7725
  },
7325
7726
 
@@ -7347,9 +7748,87 @@ export default {
7347
7748
  /**
7348
7749
  * 隐藏场景包围盒
7349
7750
  */
7751
+ refreshSceneBoundingBoxHelper(forceVisible = this.boundingBoxVisible) {
7752
+ if (this.sceneBoundingBoxHelper && this.scene) {
7753
+ this.scene.remove(this.sceneBoundingBoxHelper);
7754
+ this.sceneBoundingBoxHelper.geometry && this.sceneBoundingBoxHelper.geometry.dispose();
7755
+ if (Array.isArray(this.sceneBoundingBoxHelper.material)) {
7756
+ this.sceneBoundingBoxHelper.material.forEach(material => {
7757
+ material && material.dispose && material.dispose();
7758
+ });
7759
+ } else if (
7760
+ this.sceneBoundingBoxHelper.material &&
7761
+ this.sceneBoundingBoxHelper.material.dispose
7762
+ ) {
7763
+ this.sceneBoundingBoxHelper.material.dispose();
7764
+ }
7765
+ this.sceneBoundingBoxHelper = null;
7766
+ }
7767
+
7768
+ if (
7769
+ !forceVisible ||
7770
+ !this.scene ||
7771
+ !this.sceneBoundingBox ||
7772
+ !this.sceneBoundingBox.isBox3 ||
7773
+ this.sceneBoundingBox.isEmpty()
7774
+ ) {
7775
+ this.boundingBoxVisible = false;
7776
+ return false;
7777
+ }
7778
+
7779
+ const helper = new this.THREE.Box3Helper(this.sceneBoundingBox, 0x00ff88);
7780
+ helper.name = 'sceneBoundingBoxHelper';
7781
+ helper.renderOrder = 999;
7782
+ if (helper.material) {
7783
+ helper.material.depthTest = false;
7784
+ helper.material.transparent = true;
7785
+ helper.material.opacity = 0.9;
7786
+ }
7787
+ this.scene.add(helper);
7788
+ this.sceneBoundingBoxHelper = helper;
7789
+ this.boundingBoxVisible = true;
7790
+ return true;
7791
+ },
7792
+
7793
+ showSceneBoundingBox() {
7794
+ this.sceneBoundingBoxDebugEnabled = true;
7795
+ const shown = this.refreshSceneBoundingBoxHelper(true);
7796
+ if (!shown) {
7797
+ console.warn('sceneBoundingBox 无有效范围,无法绘制调试线框');
7798
+ return;
7799
+ }
7800
+
7801
+ const size = this.sceneBoundingBox.getSize(new this.THREE.Vector3());
7802
+ const center = this.sceneBoundingBox.getCenter(new this.THREE.Vector3());
7803
+ console.log('sceneBoundingBox 线框已绘制', {
7804
+ size: {
7805
+ x: size.x,
7806
+ y: size.y,
7807
+ z: size.z,
7808
+ },
7809
+ center: {
7810
+ x: center.x,
7811
+ y: center.y,
7812
+ z: center.z,
7813
+ },
7814
+ });
7815
+ },
7816
+
7350
7817
  hideSceneBoundingBox() {
7818
+ this.sceneBoundingBoxDebugEnabled = false;
7351
7819
  if (this.sceneBoundingBoxHelper && this.scene) {
7352
7820
  this.scene.remove(this.sceneBoundingBoxHelper);
7821
+ this.sceneBoundingBoxHelper.geometry && this.sceneBoundingBoxHelper.geometry.dispose();
7822
+ if (Array.isArray(this.sceneBoundingBoxHelper.material)) {
7823
+ this.sceneBoundingBoxHelper.material.forEach(material => {
7824
+ material && material.dispose && material.dispose();
7825
+ });
7826
+ } else if (
7827
+ this.sceneBoundingBoxHelper.material &&
7828
+ this.sceneBoundingBoxHelper.material.dispose
7829
+ ) {
7830
+ this.sceneBoundingBoxHelper.material.dispose();
7831
+ }
7353
7832
  this.sceneBoundingBoxHelper = null;
7354
7833
  this.boundingBoxVisible = false;
7355
7834
  console.log('场景包围盒已隐藏');
@@ -7360,7 +7839,7 @@ export default {
7360
7839
  * 切换场景包围盒显示状态
7361
7840
  */
7362
7841
  toggleSceneBoundingBox() {
7363
- if (this.boundingBoxVisible) {
7842
+ if (this.sceneBoundingBoxDebugEnabled) {
7364
7843
  this.hideSceneBoundingBox();
7365
7844
  } else {
7366
7845
  this.showSceneBoundingBox();