fl-web-component 1.4.8 → 2.0.0-beta.1

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.
Files changed (37) hide show
  1. package/README.md +1 -30
  2. package/dist/fl-web-component.common.1.js +2 -2
  3. package/dist/fl-web-component.common.1.js.map +1 -1
  4. package/dist/fl-web-component.common.2.js.map +1 -1
  5. package/dist/fl-web-component.common.js +77408 -47295
  6. package/dist/fl-web-component.common.js.map +1 -1
  7. package/dist/fl-web-component.css +1 -1
  8. package/package.json +12 -4
  9. package/packages/components/com-graphics/box.json +77 -0
  10. package/packages/components/com-graphics/component/ann-tool.vue +465 -0
  11. package/packages/components/com-graphics/index copy.vue +1679 -0
  12. package/packages/components/com-graphics/index.vue +3890 -301
  13. package/packages/components/com-graphics/pid.vue +210 -44
  14. package/packages/components/com-graphics/test.html +127 -0
  15. package/packages/components/com-tiles/index.vue +187 -0
  16. package/packages/utils/StreamLoader.js +1498 -0
  17. package/packages/utils/StreamLoaderParser.worker.js +595 -0
  18. package/patches/camera-controls+2.9.0.patch +63 -63
  19. package/src/main.js +9 -1
  20. package/src/static/ann-img/mark_circle@2x.png +0 -0
  21. package/src/static/ann-img/mark_clear@2x.png +0 -0
  22. package/src/static/ann-img/mark_cloud@2x.png +0 -0
  23. package/src/static/ann-img/mark_color@2x.png +0 -0
  24. package/src/static/ann-img/mark_eraser@2x.png +0 -0
  25. package/src/static/ann-img/mark_exit@2x.png +0 -0
  26. package/src/static/ann-img/mark_finish@2x.png +0 -0
  27. package/src/static/ann-img/mark_font@2x.png +0 -0
  28. package/src/static/ann-img/mark_polyline@2x.png +0 -0
  29. package/src/static/ann-img/mark_rectangle@2x.png +0 -0
  30. package/src/static/ann-img/mark_zoomin@2x.png +0 -0
  31. package/src/static/ann-img/mark_zoomout@2x.png +0 -0
  32. package/src/utils/cloud.js +110 -0
  33. package/src/utils/cursor.js +10 -0
  34. package/src/utils/flgltf-parser.js +245 -193
  35. package/src/utils/instance-parser.js +718 -170
  36. package/dist/fl-web-component.common.3.js +0 -7740
  37. package/dist/fl-web-component.common.3.js.map +0 -1
@@ -20,13 +20,15 @@
20
20
  pointControls,
21
21
  threeMeasure,
22
22
  modelGroup,
23
- gui,
24
23
  animateId,
25
24
  scenePass,
26
25
  outlineComposer,
26
+ outlinePass,
27
27
  renderTarget,
28
28
  sceneClock,
29
- mat4,
29
+ bizToThreeMatrix,
30
+ threeToBizMatrix,
31
+ stats,
30
32
  ] = (function* (v) {
31
33
  while (true) yield v;
32
34
  })(null);
@@ -45,6 +47,7 @@
45
47
  moveLeft,
46
48
  moveRight,
47
49
  measureFlag,
50
+ // rotatedSceneFlag,
48
51
  ] = (function* (v) {
49
52
  while (true) yield v;
50
53
  })(false);
@@ -53,9 +56,41 @@
53
56
  while (true) yield v;
54
57
  })(true);
55
58
 
59
+ const renderedThisFrame = new Set();
60
+
61
+ function markRendered(mesh) {
62
+ mesh.onBeforeRender = function (renderer, scene, camera, geometry, material, group) {
63
+ renderedThisFrame.add(mesh);
64
+ };
65
+ }
66
+
67
+ // 交互期间丢弃当前帧渲染的标记
68
+ let skipNextRenderFrame = false;
69
+ // 增强的交互检测标记
70
+ let forceSkipRendering = false;
71
+ // let interactionFrameCount = 0; // 交互期间跳过的帧数计数
72
+
56
73
  var clippingMesh = [],
57
74
  modelActive = [],
58
- modelActions = [];
75
+ modelActions = [],
76
+ modelGroups = [], // 存储所有模型组的数组,支持增量渲染
77
+ userInteracting = false, // 用户交互标志位
78
+ centeringDebounceTimer = null, // 居中防抖定时器
79
+ hasExecutedCentering = false, // 是否已经执行过居中操作
80
+ needsCenteringAfterInteraction = false, // 用户交互结束后是否需要居中
81
+ // 用于区分点击和拖拽的变量
82
+ mouseDownPosition = { x: 0, y: 0 }, // 鼠标按下时的位置
83
+ mouseUpPosition = { x: 0, y: 0 }, // 鼠标抬起时的位置
84
+ isDragging = false, // 是否正在拖拽
85
+ dragThreshold = 5, // 拖拽阈值,像素单位
86
+ sceneBoundingBox = null, // 场景包围盒
87
+ // 包围盒显示相关变量
88
+ sceneBoundingBoxHelper = null, // 场景包围盒辅助线
89
+ boundingBoxVisible = false; // 包围盒是否可见
90
+
91
+ // 根据场景包围盒动态计算的最大dolly距离(对角线长度)
92
+ let maxDollyDistance = Infinity;
93
+
59
94
  var removeSpeed = 200,
60
95
  upSpeed = 200; //控制器移动速度 , //控制跳起时的速度
61
96
  var roamConfig = {
@@ -67,7 +102,24 @@
67
102
  x轴: 0,
68
103
  y轴: 0,
69
104
  z轴: 0,
105
+ '-x轴': 0,
106
+ '-y轴': 0,
107
+ '-z轴': 0,
70
108
  };
109
+ // let lodLevel = 0; // 模型初始等级
110
+ // 性能优化:记录上一次的formatMin和formatMax值
111
+ let lastFormatMin = null;
112
+ let lastFormatMax = null;
113
+ let firstDraw = true;
114
+ let highSSE = 3;
115
+ let sseValue = 80;
116
+ let gui = null;
117
+
118
+ let frameCounter = 0;
119
+ let perfLogFrameCount = 0;
120
+ let lastPerfLogTime = 0;
121
+ const LOG_INTERVAL = 30;
122
+
71
123
  // 绘制对象映射实例表
72
124
  import CameraControls from 'camera-controls';
73
125
  import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js';
@@ -79,13 +131,31 @@
79
131
  import MeasureDistance from '@/utils/threejs/measure-distance.js';
80
132
  import MeasureArea from '@/utils/threejs/measure-area.js';
81
133
  import MeasureAngle from '@/utils/threejs/measure-angle.js';
82
- import { parseData } from '@/utils/flgltf-parser';
83
- import { handleInstancedMeshModel } from '@/utils/instance-parser';
134
+ import { parseData, processMeshData, processNodeData } from '@/utils/flgltf-parser';
135
+ import {
136
+ handleInstancedMeshModel,
137
+ resetProcessingState,
138
+ PRIMITIVE_TYPE,
139
+ draw3Dmodel,
140
+ getDrawObjectInstance,
141
+ instanceToInstancedMeshMap,
142
+ } from '@/utils/instance-parser';
84
143
  import { RainShader } from '@/utils/threejs/rain-shader.js';
85
144
  import { SnowShader } from '@/utils/threejs/snow-shader.js';
86
145
  import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
87
146
  import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
88
147
  import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';
148
+ import { OutlinePass } from 'three/examples/jsm/postprocessing/OutlinePass.js';
149
+ import { OutputPass } from 'three/examples/jsm/postprocessing/OutputPass.js';
150
+ import Stats from 'three/examples/jsm/libs/stats.module.js';
151
+ import { OBB } from 'three/examples/jsm/math/OBB.js';
152
+ import boxJson from './box.json';
153
+ import { StreamLoader } from '../../utils/StreamLoader.js';
154
+ import StreamLoaderParserWorker from '../../utils/StreamLoaderParser.worker.js';
155
+
156
+ const isDebug = process.env.NODE_ENV !== 'production' || process.env.VUE_APP_IS_WATCH === true;
157
+ // const isDebug = false;
158
+
89
159
  export default {
90
160
  name: 'FlModel',
91
161
  props: {
@@ -97,12 +167,102 @@
97
167
  },
98
168
  },
99
169
  data() {
100
- return {};
170
+ return {
171
+ // modelStateManager 和 occlusionState 中的大部分属性无需 Vue 响应式监听
172
+ modelStateManager: {
173
+ frustumCheckEnabled: true,
174
+ // isloadedModelsIds 需要响应式,可能用于外部展示或 computed
175
+ isloadedModelsIds: [],
176
+ },
177
+ occlusionState: {
178
+ // enabled: false,
179
+ enabled: true,
180
+ // previewEnabled: false,
181
+ previewEnabled: isDebug,
182
+ bufferWidth: 1700,
183
+ sampleStride: 1,
184
+ minSampleCount: 5,
185
+ // asyncBuildEnabled: true,
186
+ },
187
+ // 分帧加载状态管理
188
+ batchLoadingState: {
189
+ isLoading: false,
190
+ currentBatch: 0,
191
+ totalBatches: 0,
192
+ loadedCount: 0,
193
+ totalCount: 0,
194
+ isPaused: false,
195
+ },
196
+ };
101
197
  },
102
198
  created() {
199
+ // 初始化非响应式的高频状态对象
200
+ this.noObserver = {
201
+ modelStateManager: {
202
+ // frustumBounds: null,
203
+ lastUpdateTime: 0,
204
+ debounceTimer: null,
205
+ debounceDelay: 0,
206
+ lastCullingTime: 0,
207
+ pendingRemovals: [],
208
+ pendingBoundingToFull: {},
209
+ cameraBoxFaceDistance: null,
210
+ bypassCullingModelIds: new Set(),
211
+ colorConfig: new Map(),
212
+ },
213
+ occlusionState: {
214
+ zBuffer: null,
215
+ screenWidth: 0,
216
+ screenHeight: 0,
217
+ scale: 1,
218
+ _colorRT: null,
219
+ _rtW: 0,
220
+ _rtH: 0,
221
+ _colorBuffer: null,
222
+ _occScene: null,
223
+ _occMat: null,
224
+ _sharedBoxGeom: null,
225
+ metrics: null,
226
+ _previewCanvas: null,
227
+ _previewCtx: null,
228
+ _previewBuffer: null,
229
+ _previewImageData: null,
230
+ },
231
+ batchLoadingState: {
232
+ batchSize: 5,
233
+ pendingData: [],
234
+ animationFrameId: null,
235
+ onProgress: null,
236
+ onComplete: null,
237
+ pauseReason: '',
238
+ pauseStartTime: 0,
239
+ resumeTimer: null,
240
+ resumeDelay: 0,
241
+ interactionState: {
242
+ isInteracting: false,
243
+ lastInteractionTime: 0,
244
+ interactionType: '',
245
+ wheelTimeout: null,
246
+ },
247
+ },
248
+ streamLoader: null,
249
+ // isPerformingInitialCentering: false, // 标记是否正在执行初始居中
250
+ isObserverEnabled: true, // 标记相机变更观察者是否启用
251
+ sceneBoxes: new Map(),
252
+ documentModelIds: new Map(),
253
+ outlineInstanceProxyMap: new Map(),
254
+ occlusionWorker: null,
255
+ occlusionWorkerRequestMap: new Map(),
256
+ occlusionWorkerRequestId: 0,
257
+ };
258
+
259
+ this.initOcclusionWorker();
260
+
103
261
  CameraControls.install({ THREE: this.THREE });
104
- mat4 = new this.THREE.Matrix4();
105
- mat4.makeRotationX(-Math.PI / 2);
262
+ bizToThreeMatrix = new this.THREE.Matrix4();
263
+ bizToThreeMatrix.makeRotationX(-Math.PI / 2);
264
+ threeToBizMatrix = new this.THREE.Matrix4();
265
+ threeToBizMatrix.makeRotationX(Math.PI / 2);
106
266
  fpsClock = new this.THREE.Clock();
107
267
  raycaster = new this.THREE.Raycaster();
108
268
  sceneClock = new this.THREE.Clock();
@@ -112,6 +272,7 @@
112
272
  magFilter: this.THREE.LinearFilter,
113
273
  format: this.THREE.RGBAFormat,
114
274
  stencilBuffer: true,
275
+ samples: 0
115
276
  });
116
277
  },
117
278
  mounted() {
@@ -120,9 +281,13 @@
120
281
  this.initScene();
121
282
  this.initCamera();
122
283
  this.initControl();
284
+ this.initPostProcessing();
285
+ // 初始化统一的相机事件监听
286
+ this.initCameraChangeObserver();
123
287
  this.initLight();
124
288
  this.initLabelRender();
125
289
  this.exportParmas();
290
+
126
291
  // 判断是设备是手机还是电脑
127
292
  let isMobileDevice = this.isMobileDevice();
128
293
  if (isMobileDevice) {
@@ -134,7 +299,73 @@
134
299
  }
135
300
  this.animate();
136
301
  },
302
+ beforeDestroy() {
303
+ // 组件销毁前清理资源
304
+ this.destroyScene();
305
+ },
137
306
  methods: {
307
+ getOutlineInstanceProxyKey(instancedMesh, instanceIndex) {
308
+ return `${instancedMesh.uuid}:${instanceIndex}`;
309
+ },
310
+ ensureOutlineInstanceProxy(instancedMesh, instanceIndex) {
311
+ if (!scene || !instancedMesh || !instancedMesh.isInstancedMesh) return null;
312
+ const state = this.noObserver;
313
+ if (!state || !state.outlineInstanceProxyMap) return null;
314
+
315
+ instancedMesh.updateMatrixWorld(true);
316
+ const key = this.getOutlineInstanceProxyKey(instancedMesh, instanceIndex);
317
+ const cached = state.outlineInstanceProxyMap.get(key);
318
+ if (cached) {
319
+ const instanceMatrix = new this.THREE.Matrix4();
320
+ instancedMesh.getMatrixAt(instanceIndex, instanceMatrix);
321
+ cached.matrix.copy(instancedMesh.matrixWorld).multiply(instanceMatrix);
322
+ cached.matrixWorldNeedsUpdate = true;
323
+ return cached;
324
+ }
325
+
326
+ const proxyMaterial = new this.THREE.MeshBasicMaterial({
327
+ color: 0xffffff,
328
+ transparent: true,
329
+ opacity: 0,
330
+ depthWrite: false,
331
+ });
332
+ proxyMaterial.colorWrite = false;
333
+
334
+ const proxy = new this.THREE.Mesh(instancedMesh.geometry, proxyMaterial);
335
+ proxy.matrixAutoUpdate = false;
336
+ proxy.frustumCulled = false;
337
+ proxy.layers.mask = instancedMesh.layers.mask;
338
+
339
+ const instanceMatrix = new this.THREE.Matrix4();
340
+ instancedMesh.getMatrixAt(instanceIndex, instanceMatrix);
341
+ proxy.matrix.copy(instancedMesh.matrixWorld).multiply(instanceMatrix);
342
+ proxy.matrixWorldNeedsUpdate = true;
343
+ proxy.userData = {
344
+ outlineProxy: true,
345
+ instancedMeshUuid: instancedMesh.uuid,
346
+ instanceIndex,
347
+ };
348
+
349
+ scene.add(proxy);
350
+ state.outlineInstanceProxyMap.set(key, proxy);
351
+ return proxy;
352
+ },
353
+ removeOutlineInstanceProxy(instancedMesh, instanceIndex) {
354
+ const state = this.noObserver;
355
+ if (!state || !state.outlineInstanceProxyMap || !instancedMesh) return null;
356
+ const key = this.getOutlineInstanceProxyKey(instancedMesh, instanceIndex);
357
+ const proxy = state.outlineInstanceProxyMap.get(key);
358
+ if (!proxy) return null;
359
+
360
+ state.outlineInstanceProxyMap.delete(key);
361
+ if (outlinePass) {
362
+ const idx = outlinePass.selectedObjects.indexOf(proxy);
363
+ if (idx !== -1) outlinePass.selectedObjects.splice(idx, 1);
364
+ }
365
+ if (scene) scene.remove(proxy);
366
+ if (proxy.material) proxy.material.dispose && proxy.material.dispose();
367
+ return proxy;
368
+ },
138
369
  // 判断是设备是手机还是电脑
139
370
  isMobileDevice() {
140
371
  const userAgent = navigator.userAgent || navigator.vendor || window.opera;
@@ -149,149 +380,2235 @@
149
380
  }
150
381
  return false;
151
382
  },
152
- initRender() {
153
- renderer = new this.THREE.WebGLRenderer({
154
- antialias: true,
155
- alpha: true,
156
- logarithmicDepthBuffer: true,
157
- powerPreference: 'high-performance',
158
- preserveDrawingBuffer: true, //保留图形缓冲区 TODO 临时截图使用
159
- });
160
- renderer.setPixelRatio(window.devicePixelRatio);
161
- const rect = instructions.getBoundingClientRect();
162
- renderer.setSize(rect.width, rect.height);
163
- renderer.domElement.id = 'three-model';
164
- renderer.shadowMap.enabled = true;
165
- // 暂时注释这句 还有worker里面的 这跟渲染出的模型浅或暗有关系
166
- renderer.outputEncoding = this.THREE.sRGBEncoding;
167
- instructions.appendChild(renderer.domElement);
168
- renderer.setClearAlpha(0);
383
+ // 节流工具方法
384
+ throttle(func, limit) {
385
+ let lastFunc;
386
+ let lastRan;
387
+ return function (...args) {
388
+ if (!lastRan) {
389
+ func.apply(this, args);
390
+ lastRan = Date.now();
391
+ } else {
392
+ clearTimeout(lastFunc);
393
+ lastFunc = setTimeout(function () {
394
+ if (Date.now() - lastRan >= limit) {
395
+ func.apply(this, args);
396
+ lastRan = Date.now();
397
+ }
398
+ }, limit - (Date.now() - lastRan));
399
+ }
400
+ };
401
+ },
402
+ // 防抖工具方法,支持动态延迟
403
+ debounce(func, delay) {
404
+ let timeoutId;
405
+ // 增加 throttle 支持
406
+ let throttleLastRan = 0;
407
+ let throttleTimer = null;
169
408
 
170
- // 与校审截图功能冲突,暂时先注释掉
171
- // -----------
172
- // renderer.autoClear = false;
173
- // renderer.autoClearColor = false;
174
- // renderer.autoClearDepth = false;
175
- // renderer.autoClearStencil = false;
176
- // -----------
409
+ return function (...args) {
410
+ const currentDelay = typeof delay === 'function' ? delay(...args) : delay;
411
+
412
+ // 如果返回 'throttle' 策略,则执行节流逻辑 (默认间隔 300ms,或可扩展)
413
+ if (
414
+ currentDelay === 'throttle' ||
415
+ (typeof currentDelay === 'object' && currentDelay.type === 'throttle')
416
+ ) {
417
+ const limit =
418
+ typeof currentDelay === 'object' && currentDelay.limit ? currentDelay.limit : 300;
419
+ const now = Date.now();
420
+
421
+ // 清除之前的防抖定时器,避免冲突
422
+ if (timeoutId) {
423
+ clearTimeout(timeoutId);
424
+ timeoutId = null;
425
+ }
426
+
427
+ if (now - throttleLastRan >= limit) {
428
+ func.apply(this, args);
429
+ throttleLastRan = now;
430
+ } else {
431
+ // 确保最后一次触发也能执行
432
+ clearTimeout(throttleTimer);
433
+ throttleTimer = setTimeout(() => {
434
+ if (Date.now() - throttleLastRan >= limit) {
435
+ func.apply(this, args);
436
+ throttleLastRan = Date.now();
437
+ }
438
+ }, limit - (now - throttleLastRan));
439
+ }
440
+ return;
441
+ }
442
+
443
+ // 常规防抖逻辑
444
+ clearTimeout(timeoutId);
445
+ // 切换回防抖模式时,清除节流定时器
446
+ if (throttleTimer) {
447
+ clearTimeout(throttleTimer);
448
+ throttleTimer = null;
449
+ }
450
+
451
+ if (currentDelay <= 0) {
452
+ func.apply(this, args);
453
+ } else {
454
+ timeoutId = setTimeout(() => func.apply(this, args), currentDelay);
455
+ }
456
+ };
177
457
  },
178
- initScene() {
179
- modelGroup = new this.THREE.Group();
180
- scene = new this.THREE.Scene();
458
+ initOcclusionWorker() {
459
+ if (!this.noObserver) return;
460
+ let worker = null;
461
+ try {
462
+ worker = new StreamLoaderParserWorker();
463
+ } catch (e) {
464
+ worker = null;
465
+ }
466
+ this.noObserver.occlusionWorker = worker;
467
+ this.noObserver.occlusionWorkerRequestId = 0;
468
+ this.noObserver.occlusionWorkerRequestMap = new Map();
469
+ if (worker) {
470
+ worker.addEventListener('message', e => {
471
+ const payload = e.data || {};
472
+ const pending = this.noObserver.occlusionWorkerRequestMap.get(payload.id);
473
+ if (!pending) return;
474
+ this.noObserver.occlusionWorkerRequestMap.delete(payload.id);
475
+ pending.resolve(payload);
476
+ });
477
+ }
181
478
  },
182
- initCamera() {
183
- camera = new this.THREE.PerspectiveCamera(
184
- 45,
185
- window.innerWidth / window.innerHeight,
186
- 0.1,
187
- 1000000000
188
- );
189
- camera.position.set(0, 100, 150);
479
+ occlusionWorkerRequest(type, data, transferable = []) {
480
+ const state = this.noObserver;
481
+ if (!state || !state.occlusionWorker) {
482
+ return Promise.resolve({ type: 'error', error: 'Worker is not initialized' });
483
+ }
484
+ return new Promise(resolve => {
485
+ const id = state.occlusionWorkerRequestId++;
486
+ state.occlusionWorkerRequestMap.set(id, { resolve });
487
+ state.occlusionWorker.postMessage({ id, type, data }, transferable);
488
+ });
190
489
  },
191
- initControl() {
192
- // 初始化控件
193
- cameraControls = new CameraControls(camera, renderer.domElement);
194
- cameraControls.dollyToCursor = true;
195
- cameraControls.smoothTime = 0.1;
196
- cameraControls.draggingSmoothTime = 0.05;
197
- cameraControls.truckSpeed = 2.0;
198
- cameraControls.infinityDolly = true;
199
- cameraControls.minDistance = 4;
490
+ scanOcclusionBufferSync(buffer, sw, sh, stride, maxIdx, minSampleCount) {
491
+ const indices = [];
492
+ if (!buffer || !sw || !sh || !maxIdx) return { indices };
493
+ const view = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
494
+ const step = Math.max(1, stride || 1);
495
+ const minCount = Math.max(1, minSampleCount || 1);
496
+ const counts = new Uint32Array(maxIdx + 1);
497
+ for (let y = 0; y < sh; y += step) {
498
+ const row = y * sw * 4;
499
+ for (let x = 0; x < sw; x += step) {
500
+ const p = row + x * 4;
501
+ const idxColor = view[p] + (view[p + 1] << 8) + (view[p + 2] << 16);
502
+ if (idxColor > 0 && idxColor <= maxIdx) {
503
+ const next = counts[idxColor] + 1;
504
+ counts[idxColor] = next;
505
+ if (next === minCount) {
506
+ indices.push(idxColor);
507
+ }
508
+ }
509
+ }
510
+ }
511
+ return { indices };
200
512
  },
201
- // 初始化光源
202
- initLight() {
203
- const pmremGenerator = new this.THREE.PMREMGenerator(renderer);
204
- scene.environment = pmremGenerator.fromScene(new RoomEnvironment(), 0.04).texture;
513
+ async scanOcclusionIndices(buffer, sw, sh, stride, maxIdx, minSampleCount) {
514
+ const state = this.noObserver;
515
+ const canWorker = state && state.occlusionWorker && buffer && buffer.buffer;
516
+ if (canWorker) {
517
+ const response = await this.occlusionWorkerRequest(
518
+ 'occlusionScan',
519
+ {
520
+ buffer: buffer.buffer,
521
+ sw,
522
+ sh,
523
+ stride,
524
+ maxIdx,
525
+ minSampleCount,
526
+ },
527
+ [buffer.buffer]
528
+ );
529
+ const restored = response && response.buffer ? new Uint8Array(response.buffer) : buffer;
530
+ if (response && response.type === 'success' && response.result) {
531
+ return { indices: response.result.indices || [], buffer: restored.buffer };
532
+ }
533
+ const syncRes = this.scanOcclusionBufferSync(
534
+ restored,
535
+ sw,
536
+ sh,
537
+ stride,
538
+ maxIdx,
539
+ minSampleCount
540
+ );
541
+ return { indices: syncRes.indices, buffer: restored.buffer };
542
+ }
543
+ const syncRes = this.scanOcclusionBufferSync(
544
+ buffer,
545
+ sw,
546
+ sh,
547
+ stride,
548
+ maxIdx,
549
+ minSampleCount
550
+ );
551
+ return { indices: syncRes.indices, buffer: buffer && buffer.buffer ? buffer.buffer : null };
205
552
  },
206
- // 初始化文字画布
207
- initLabelRender() {
208
- labelRenderer = new CSS2DRenderer();
209
- const rect = instructions.getBoundingClientRect();
210
- labelRenderer.setSize(rect.width, rect.height);
211
- labelRenderer.domElement.style.position = 'absolute';
553
+ setSceneBox(boundingBox, documentId, isAdd = true) {
554
+ if (!documentId) {
555
+ if (this.noObserver && this.noObserver.sceneBoxes) {
556
+ this.noObserver.sceneBoxes.clear();
557
+ }
558
+ sceneBoundingBox = new this.THREE.Box3();
559
+ return;
560
+ }
212
561
 
213
- labelRenderer.domElement.style.top = `${rect.top}px`;
214
- labelRenderer.domElement.style.pointerEvents = 'none';
215
- instructions.appendChild(labelRenderer.domElement);
562
+ if (isAdd && boundingBox) {
563
+ const {
564
+ max_x: maxX,
565
+ max_z: maxZ,
566
+ max_y: maxY,
567
+ min_x: minX,
568
+ min_z: minZ,
569
+ min_y: minY,
570
+ } = boundingBox;
571
+
572
+ const box = new this.THREE.Box3(
573
+ new this.THREE.Vector3(minX, minY, minZ),
574
+ new this.THREE.Vector3(maxX, maxY, maxZ)
575
+ ).applyMatrix4(bizToThreeMatrix);
576
+ this.noObserver.sceneBoxes.set(documentId, box);
577
+ } else {
578
+ this.noObserver.sceneBoxes.delete(documentId);
579
+ }
580
+
581
+ const boxes = Array.from(this.noObserver.sceneBoxes.values());
582
+ if (boxes.length > 0) {
583
+ const firstBox = boxes[0].clone();
584
+ for (let i = 1; i < boxes.length; i++) {
585
+ firstBox.union(boxes[i]);
586
+ }
587
+ sceneBoundingBox = firstBox;
588
+ } else {
589
+ sceneBoundingBox = new this.THREE.Box3();
590
+ }
216
591
  },
217
- // 根据模型数据绘制模型实体 业务平台可调用此方法加载模型
218
- /*
219
- 参数:data 模型数据
220
- color: '' 初始化模型的颜色 在业务方 有这个需求
221
- meshNameConfig: {}
222
- */
223
- drawModel(data, color = '', meshNameConfig = {}, options = {}) {
224
- if (Object.keys(data).length === 0) {
592
+ setBoxIndex(boxJson, documentId, isAdd = true) {
593
+ if (!this._boxIndex) this._boxIndex = new Map();
594
+ if (!documentId) {
595
+ this._boxIndex.clear();
596
+ if (this.noObserver && this.noObserver.documentModelIds) {
597
+ this.noObserver.documentModelIds.clear();
598
+ }
599
+ hasExecutedCentering = false;
600
+ this.buildOctreeFromBoxIndex();
225
601
  return;
226
602
  }
227
- const { instances, drawObjs } = parseData(data);
228
- if (instances.length > 0) {
229
- handleInstancedMeshModel(
230
- modelGroup,
231
- instances,
232
- drawObjs,
233
- '',
234
- scene,
235
- color,
236
- meshNameConfig
237
- );
238
- let modelBox3 = new this.THREE.Box3();
239
- modelBox3.expandByObject(modelGroup);
240
- let modelWorldPs = new this.THREE.Vector3()
241
- .addVectors(modelBox3.max, modelBox3.min)
242
- .multiplyScalar(0.5);
243
- scene.add(modelGroup);
244
- // 适配客户端的坐标系,threejs坐标需绕x轴旋转90度
245
- modelGroup.applyMatrix4(mat4);
246
- modelGroup.updateMatrixWorld();
247
- modelGroup.traverse(child => {
248
- if (child.isMesh) {
249
- const json = this.getMeshCenterAndVolume(child);
250
- let meshBox3 = new this.THREE.Box3();
251
- meshBox3.setFromObject(child);
252
- // 获取每个mesh的中心点,爆炸方向为爆炸中心点指向mesh中心点
253
- let worldPs = new this.THREE.Vector3()
254
- .addVectors(meshBox3.max, meshBox3.min)
255
- .multiplyScalar(0.5);
256
- if (isNaN(worldPs.x)) return;
257
- // 计算爆炸方向
258
- child.worldDir = new this.THREE.Vector3()
259
- .subVectors(worldPs, modelWorldPs)
260
- .normalize();
261
- // 保存初始坐标
262
- child.userData.center = json.center;
263
- child.userData.worldPs = worldPs;
264
- child.userData.oldPs = child.getWorldPosition(new this.THREE.Vector3());
265
- child.userData.box = json.box;
266
- child.userData.position = new this.THREE.Vector3().copy(child.position);
267
- child.userData.translate = {
268
- x: 0,
269
- y: 0,
270
- z: 0,
271
- };
272
- child.userData.rotate = {
273
- x: 0,
274
- y: 0,
275
- z: 0,
603
+
604
+ if (isAdd && boxJson) {
605
+ const arr = boxJson ? boxJson : [];
606
+ const modelIds = new Set();
607
+ for (let i = 0; i < arr.length; i++) {
608
+ const it = arr[i];
609
+
610
+ // 使用新的 min/max 数据结构
611
+ const min = new this.THREE.Vector3(it.min[0], it.min[1], it.min[2]);
612
+ const max = new this.THREE.Vector3(it.max[0], it.max[1], it.max[2]);
613
+
614
+ // 构造 AABB
615
+ const boxThree = new this.THREE.Box3(min, max);
616
+
617
+ // 应用坐标系转换 (Biz -> Three)
618
+ // 注意:applyMatrix4 会重新计算 AABB
619
+ boxThree.applyMatrix4(bizToThreeMatrix);
620
+
621
+ const userData = {
622
+ flag: it.flag || 1, // 默认为 1
623
+ obbData: it.obb, // 存储原始 OBB 数据
624
+ transparent: it.transp > 0,
625
+ };
626
+ if(it.flag === 1) {
627
+ userData.indices = it.indices;
628
+ userData.matrix = it.matrix;
629
+ // userData.matrix = new this.THREE.Matrix4().identity();
630
+ }
631
+ // 如果是 flag=3,解析并存储中心点、半轴长、旋转矩阵
632
+ else if (it.flag === 3 && it.obb && it.obb.length === 15) {
633
+ const obbData = it.obb;
634
+ const center = new this.THREE.Vector3(obbData[0], obbData[1], obbData[2]);
635
+ const halfSize = new this.THREE.Vector3(obbData[3], obbData[4], obbData[5]);
636
+ const rotation = new this.THREE.Matrix3().fromArray(obbData.slice(6, 15));
637
+
638
+ // 预计算局部到世界的变换矩阵 (不含 bizToThreeMatrix,后续渲染时统一处理)
639
+ const rotationMatrix4 = new this.THREE.Matrix4().setFromMatrix3(rotation);
640
+ const obbMatrix = new this.THREE.Matrix4().makeTranslation(
641
+ center.x,
642
+ center.y,
643
+ center.z
644
+ );
645
+ obbMatrix.multiply(rotationMatrix4);
646
+
647
+ userData.obb = {
648
+ matrix: obbMatrix,
649
+ center: center,
650
+ halfSize: halfSize,
276
651
  };
277
- child.userData.modelWorldPs = modelWorldPs;
278
652
  }
653
+ // 如果是 flag=2,解析并存储多个子包围盒 (min, max)
654
+ else if (it.flag === 2 && it.obb && it.obb.length > 0) {
655
+ const subBoxes = [];
656
+ for (let k = 0; k < it.obb.length; k += 6) {
657
+ if (k + 5 < it.obb.length) {
658
+ const min = new this.THREE.Vector3(it.obb[k], it.obb[k + 1], it.obb[k + 2]);
659
+ const max = new this.THREE.Vector3(it.obb[k + 3], it.obb[k + 4], it.obb[k + 5]);
660
+ subBoxes.push({ min, max });
661
+ }
662
+ }
663
+ userData.subBoxes = subBoxes;
664
+ }
665
+
666
+ boxThree.userData = userData;
667
+
668
+ const modelId = String(it.id);
669
+ this._boxIndex.set(modelId, boxThree);
670
+ modelIds.add(modelId);
671
+ }
672
+ this.noObserver.documentModelIds.set(documentId, modelIds);
673
+ } else {
674
+ const modelIds = this.noObserver.documentModelIds.get(documentId);
675
+ if (modelIds) {
676
+ modelIds.forEach(id => {
677
+ this._boxIndex.delete(id);
678
+ });
679
+ this.noObserver.documentModelIds.delete(documentId);
680
+
681
+ // 当前无模型了,重置相机中心状态
682
+ if(this.noObserver.documentModelIds.size == 0){
683
+ hasExecutedCentering = false
684
+ }
685
+ }
686
+ }
687
+ this.buildOctreeFromBoxIndex();
688
+ console.log('time end', Date.now());
689
+ },
690
+ buildOctreeFromBoxIndex() {
691
+ if (!this._boxIndex || this._boxIndex.size === 0) {
692
+ this._octree = null;
693
+ return;
694
+ }
695
+ let minX = Infinity,
696
+ minY = Infinity,
697
+ minZ = Infinity;
698
+ let maxX = -Infinity,
699
+ maxY = -Infinity,
700
+ maxZ = -Infinity;
701
+ this._boxIndex.forEach(box => {
702
+ if (!box || !box.isBox3) return;
703
+ const mn = box.min;
704
+ const mx = box.max;
705
+ if (mn.x < minX) minX = mn.x;
706
+ if (mn.y < minY) minY = mn.y;
707
+ if (mn.z < minZ) minZ = mn.z;
708
+ if (mx.x > maxX) maxX = mx.x;
709
+ if (mx.y > maxY) maxY = mx.y;
710
+ if (mx.z > maxZ) maxZ = mx.z;
711
+ });
712
+ const rootBox = new this.THREE.Box3(
713
+ new this.THREE.Vector3(minX, minY, minZ),
714
+ new this.THREE.Vector3(maxX, maxY, maxZ)
715
+ );
716
+ this._octreeMaxItems = 64;
717
+ this._octreeMaxDepth = 12;
718
+ this._octree = { box: rootBox, items: [], children: null, depth: 0 };
719
+ this._boxIndex.forEach((box, id) => {
720
+ this._octreeInsert(this._octree, String(id), box);
721
+ });
722
+ },
723
+ _octreeSubdivide(node) {
724
+ const min = node.box.min;
725
+ const max = node.box.max;
726
+ const mid = new this.THREE.Vector3(
727
+ (min.x + max.x) * 0.5,
728
+ (min.y + max.y) * 0.5,
729
+ (min.z + max.z) * 0.5
730
+ );
731
+ const children = [];
732
+ for (let i = 0; i < 8; i++) {
733
+ const cx0 = i & 1 ? mid.x : min.x;
734
+ const cy0 = i & 2 ? mid.y : min.y;
735
+ const cz0 = i & 4 ? mid.z : min.z;
736
+ const cx1 = i & 1 ? max.x : mid.x;
737
+ const cy1 = i & 2 ? max.y : mid.y;
738
+ const cz1 = i & 4 ? max.z : mid.z;
739
+ const cmin = new this.THREE.Vector3(cx0, cy0, cz0);
740
+ const cmax = new this.THREE.Vector3(cx1, cy1, cz1);
741
+ const cbox = new this.THREE.Box3(cmin, cmax);
742
+ children.push({ box: cbox, items: [], children: null, depth: node.depth + 1 });
743
+ }
744
+ node.children = children;
745
+ if (node.items && node.items.length) {
746
+ const keep = [];
747
+ for (let k = 0; k < node.items.length; k++) {
748
+ const it = node.items[k];
749
+ let placed = false;
750
+ for (let c = 0; c < 8; c++) {
751
+ const child = node.children[c];
752
+ if (child.box.containsBox(it.box)) {
753
+ child.items.push(it);
754
+ placed = true;
755
+ break;
756
+ }
757
+ }
758
+ if (!placed) keep.push(it);
759
+ }
760
+ node.items = keep;
761
+ }
762
+ },
763
+ _octreeInsert(node, id, box) {
764
+ if (!node.children) {
765
+ node.items.push({ id, box });
766
+ if (node.items.length > this._octreeMaxItems && node.depth < this._octreeMaxDepth) {
767
+ this._octreeSubdivide(node);
768
+ }
769
+ return;
770
+ }
771
+ for (let i = 0; i < 8; i++) {
772
+ const child = node.children[i];
773
+ if (child.box.containsBox(box)) {
774
+ this._octreeInsert(child, id, box);
775
+ return;
776
+ }
777
+ }
778
+ node.items.push({ id, box });
779
+ },
780
+ _getCurrentFrustum() {
781
+ // 确保相机矩阵是最新的
782
+ if (camera) {
783
+ camera.updateMatrixWorld();
784
+ camera.matrixWorldInverse.copy(camera.matrixWorld).invert();
785
+ }
786
+ const frustum = new this.THREE.Frustum();
787
+ const vpMatrix = new this.THREE.Matrix4().multiplyMatrices(
788
+ camera.projectionMatrix,
789
+ camera.matrixWorldInverse
790
+ );
791
+ frustum.setFromProjectionMatrix(vpMatrix);
792
+ return frustum;
793
+ },
794
+ queryOctreeByFrustum(frustum, excludeSet) {
795
+ const results = [];
796
+ const stack = [];
797
+ const root = this._octree;
798
+ if (!root) return results;
799
+ stack.push(root);
800
+
801
+ // 预分配临时对象以减少GC
802
+ const inverseMatrix = new this.THREE.Matrix4();
803
+ const localFrustum = new this.THREE.Frustum();
804
+ const localBox = new this.THREE.Box3();
805
+
806
+ while (stack.length) {
807
+ const node = stack.pop();
808
+ if (!frustum.intersectsBox(node.box)) continue;
809
+ if (node.children) {
810
+ for (let i = 0; i < 8; i++) {
811
+ stack.push(node.children[i]);
812
+ }
813
+ }
814
+ if (node.items && node.items.length) {
815
+ for (let k = 0; k < node.items.length; k++) {
816
+ const it = node.items[k];
817
+ if (excludeSet && excludeSet.size > 0 && excludeSet.has(it.id)) continue;
818
+
819
+ // 1. 粗测:World AABB
820
+ if (frustum.intersectsBox(it.box)) {
821
+ // let isIntersect = true;
822
+ // if (it.box.userData && it.box.userData.obb) {
823
+ // const obb = it.box.userData.obb;
824
+ // // 计算从 World 到 Local 的变换矩阵
825
+ // // inverseMatrix = (BizToThree * LocalMatrix)^-1 = LocalMatrix^-1 * ThreeToBiz
826
+ // inverseMatrix.copy(obb.matrix).invert();
827
+
828
+ // // 将 Frustum 变换到 Local Space
829
+ // // 注意:这里需要深拷贝 Planes,因为 applyMatrix4 会修改 Plane
830
+ // for (let i = 0; i < 6; i++) {
831
+ // localFrustum.planes[i].copy(frustum.planes[i]).applyMatrix4(inverseMatrix);
832
+ // }
833
+
834
+ // // 构造 Local AABB
835
+ // localBox.min.copy(obb.localMin);
836
+ // localBox.max.copy(obb.localMax);
837
+
838
+ // if (!localFrustum.intersectsBox(localBox)) {
839
+ // isIntersect = false;
840
+ // }
841
+ // }
842
+
843
+ // if (isIntersect) {
844
+ // results.push({ modelId: it.id, box: it.box });
845
+ // }
846
+ results.push({ modelId: it.id, box: it.box });
847
+ }
848
+ }
849
+ }
850
+ }
851
+ return results;
852
+ },
853
+ setCameraFar() {
854
+ const bbox = sceneBoundingBox;
855
+ const center = bbox.getCenter(new this.THREE.Vector3());
856
+ const size = bbox.getSize(new this.THREE.Vector3());
857
+ const maxDist = size.length();
858
+ // const distance = camera.position.distanceTo(center);
859
+ camera.far = maxDist * 2;
860
+ // camera.far = 100;
861
+ camera.updateProjectionMatrix();
862
+ },
863
+ // 新增:当相机位于场景包围盒内时,动态调整相机远裁剪面
864
+ adjustCameraFarPlaneForSceneBox() {
865
+ try {
866
+ if (!camera || !sceneBoundingBox || !sceneBoundingBox.isBox3) return;
867
+
868
+ // 获取最新相机位置
869
+ camera.updateMatrixWorld(true);
870
+ const camPos = new this.THREE.Vector3().setFromMatrixPosition(camera.matrixWorld);
871
+
872
+ const min = sceneBoundingBox.min;
873
+ const max = sceneBoundingBox.max;
874
+
875
+ // 包围盒八个顶点
876
+ const vertices = [
877
+ new this.THREE.Vector3(min.x, min.y, min.z),
878
+ new this.THREE.Vector3(min.x, min.y, max.z),
879
+ new this.THREE.Vector3(min.x, max.y, min.z),
880
+ new this.THREE.Vector3(min.x, max.y, max.z),
881
+ new this.THREE.Vector3(max.x, min.y, min.z),
882
+ new this.THREE.Vector3(max.x, min.y, max.z),
883
+ new this.THREE.Vector3(max.x, max.y, min.z),
884
+ new this.THREE.Vector3(max.x, max.y, max.z),
885
+ ];
886
+
887
+ // 计算到最远顶点的距离
888
+ let maxDistance = 0;
889
+ for (const v of vertices) {
890
+ const dist = camPos.distanceTo(v);
891
+ if (dist > maxDistance) maxDistance = dist;
892
+ }
893
+
894
+ // 确保 newFar > near,且更新投影矩阵
895
+ const newFar = Math.max(maxDistance, camera.near + 0.1);
896
+ if (Math.abs(camera.far - newFar) > 1e-6) {
897
+ camera.far = newFar;
898
+ camera.updateProjectionMatrix();
899
+ }
900
+
901
+ // console.log('camera.far', camera.far)
902
+ } catch (e) {
903
+ // 防御式处理,避免交互中断
904
+ }
905
+ },
906
+ /**
907
+ * 计算当前相机视口在指定平面上的投影范围,并返回视椎体高度(min/max)。
908
+ * frustumY.min 会被限制:不小于平面在视椎体 x/z 范围内的最低 y 值(如果可计算)。
909
+ *
910
+ * @param {THREE.Camera} camera
911
+ * @param {number} width
912
+ * @param {number} height
913
+ * @param {THREE.Plane} plane - three.js 的 Plane,方程为: normal.dot(p) + constant = 0
914
+ * @param {Object} [opts]
915
+ * @param {boolean} [opts.includeFrustumY=false]
916
+ * @returns {Object|null} { planeBox: THREE.Box3|null, frustumBox: THREE.Box3, frustumY: {min, max} } 或 null(当没有平面交点且未请求 frustumY)
917
+ */
918
+ getScreenPlaneBounds(camera, width, height, plane, opts = {}) {
919
+ const { includeFrustumY = false } = opts;
920
+
921
+ camera.updateMatrixWorld();
922
+ if (camera.projectionMatrixNeedsUpdate) camera.updateProjectionMatrix();
923
+
924
+ const screenCorners = [
925
+ [0, 0], // 左下
926
+ [width, 0], // 右下
927
+ [0, height], // 左上
928
+ [width, height], // 右上
929
+ ];
930
+
931
+ // 计算视椎体 8 个角点(near/far 的四角)
932
+ const frustumPoints = [];
933
+ for (const [x, y] of screenCorners) {
934
+ const ndcBase = new this.THREE.Vector3(
935
+ (x / width) * 2 - 1,
936
+ -(y / height) * 2 + 1,
937
+ undefined
938
+ );
939
+
940
+ const ndcNear = ndcBase.clone();
941
+ ndcNear.z = -1;
942
+ frustumPoints.push(ndcNear.clone().unproject(camera));
943
+
944
+ const ndcFar = ndcBase.clone();
945
+ ndcFar.z = 1;
946
+ frustumPoints.push(ndcFar.clone().unproject(camera));
947
+ }
948
+
949
+ const frustumBox = new this.THREE.Box3().setFromPoints(frustumPoints);
950
+
951
+ // 顶视将视椎体投影到 XOZ 平面(沿 Y 方向投影),用其范围替代原来的屏幕到平面的交点范围
952
+ const a = plane.normal.x;
953
+ const b = plane.normal.y;
954
+ const c = plane.normal.z;
955
+ const d = plane.constant;
956
+ const EPS = 1e-8;
957
+
958
+ let planeBox = null;
959
+ if (Math.abs(b) >= EPS) {
960
+ // 使用“近平面 + 远平面”构成的体积进行投影
961
+ const nearCorners = [
962
+ frustumPoints[0],
963
+ frustumPoints[2],
964
+ frustumPoints[4],
965
+ frustumPoints[6],
966
+ ];
967
+ const farCorners = [
968
+ frustumPoints[1],
969
+ frustumPoints[3],
970
+ frustumPoints[5],
971
+ frustumPoints[7],
972
+ ];
973
+ const volumeCorners = nearCorners.concat(farCorners);
974
+ const projectedPoints = volumeCorners.map(p => {
975
+ const yProj = -(a * p.x + c * p.z + d) / b;
976
+ return new this.THREE.Vector3(p.x, yProj, p.z);
279
977
  });
280
- // cameraControls.fitToSphere(scene, true); // TODO 待处理,先用 setModelCenter 进行定位
281
- // this.setModelCenter(modelGroup);
282
- this.home();
283
- this.$emit('modelLoaded');
284
- // cameraControls.saveState();
978
+ planeBox = new this.THREE.Box3().setFromPoints(projectedPoints);
979
+ }
980
+
981
+ if (!includeFrustumY && !planeBox) return null;
982
+
983
+ // helper: 计算视椎体远近裁剪面的 Y 轴投影范围(min/max)
984
+ // 基于 frustumPoints 中的 8 个角点:近面 [0,2,4,6],远面 [1,3,5,7]
985
+ function computePlaneMinYOverBox(plane, box) {
986
+ const nearIndices = [0, 2, 4, 6];
987
+ const farIndices = [1, 3, 5, 7];
988
+ const nearYs = nearIndices.map(i => frustumPoints[i].y);
989
+ const farYs = farIndices.map(i => frustumPoints[i].y);
990
+ const minY = Math.min(...nearYs, ...farYs);
991
+ const maxY = Math.max(...nearYs, ...farYs);
992
+ return { min: minY, max: maxY };
285
993
  }
286
994
 
287
- // 动态设置视角滚轮的距离
288
- this.setCameraConfig();
995
+ // 基于远/近裁剪面角点计算在 Y 轴的投影范围(min/max)
996
+ const nearFarY = computePlaneMinYOverBox(plane, frustumBox);
997
+
998
+ const target = this.getCameraTargetOnPlane(camera, plane);
999
+
1000
+ // 将最终范围限制在 sceneBoundingBox 内(若已存在)
1001
+ let finalMinY = nearFarY.min;
1002
+ let finalMaxY = nearFarY.max;
1003
+ if (sceneBoundingBox && sceneBoundingBox.isBox3) {
1004
+ finalMinY = Math.max(finalMinY, sceneBoundingBox.min.y);
1005
+ finalMaxY = Math.min(finalMaxY, sceneBoundingBox.max.y);
1006
+ }
1007
+
1008
+ return {
1009
+ planeBox, // 顶视投影后的范围包围盒(可能为 null)
1010
+ frustumBox, // 视椎体在世界空间的包围盒
1011
+ frustumY: { min: finalMinY, max: finalMaxY },
1012
+ ...target,
1013
+ };
1014
+ },
1015
+
1016
+ /**
1017
+ * 计算相机射线与场景包围盒(sceneBoundingBox)的交点,以及该点到视椎体近裁剪面。
1018
+ * 优先使用相机的目标点(cameraControls._target)确定射线方向;若不可用则回退为相机世界前向。
1019
+ * 若包围盒为空,回退为根据当前模型组/场景计算一次包围盒。
1020
+ * 若与包围盒无交点且提供了平面参数,则回退为与该平面的交点;仍无交点则将相机位置投影到该平面。
1021
+ *
1022
+ * @param {THREE.Camera} camera - 相机对象
1023
+ * @param {THREE.Plane} [plane] - 可选,用于无包围盒交点时的回退平面
1024
+ * @returns {Object} { targetPoint: THREE.Vector3, distanceToNearPlane: number, cameraPosition: THREE.Vector3, nearPlanePoint: THREE.Vector3 }
1025
+ */
1026
+ getCameraTargetOnPlane(camera, plane) {
1027
+ camera.updateMatrixWorld();
1028
+ if (camera.projectionMatrixNeedsUpdate) camera.updateProjectionMatrix();
1029
+
1030
+ // 相机世界位置
1031
+ const cameraPosition = new this.THREE.Vector3().setFromMatrixPosition(camera.matrixWorld);
1032
+
1033
+ // 从相机指向目标点的射线方向(优先使用 cameraControls._target)
1034
+ const rayDirection = new this.THREE.Vector3();
1035
+ if (
1036
+ typeof cameraControls !== 'undefined' &&
1037
+ cameraControls &&
1038
+ cameraControls.enabled &&
1039
+ cameraControls._target
1040
+ ) {
1041
+ rayDirection.copy(cameraControls._target).sub(cameraPosition).normalize();
1042
+ } else {
1043
+ camera.getWorldDirection(rayDirection); // -Z 方向(世界坐标)
1044
+ }
1045
+
1046
+ const ray = new this.THREE.Ray(cameraPosition.clone(), rayDirection.clone());
1047
+
1048
+ // 准备/计算场景包围盒
1049
+ // if (!sceneBoundingBox) {
1050
+ // const box3 = new this.THREE.Box3();
1051
+ // if (typeof modelGroup !== 'undefined' && modelGroup) {
1052
+ // box3.expandByObject(modelGroup);
1053
+ // } else if (scene) {
1054
+ // box3.expandByObject(scene);
1055
+ // }
1056
+ // sceneBoundingBox = box3;
1057
+ // }
1058
+
1059
+ // 与场景包围盒的交点(若无交点则进行回退)
1060
+ // 计算与场景包围盒的交点(优先)
1061
+ let boxHitPoint = null;
1062
+ // 若包围盒不存在或为空,兜底从模型组/场景计算一次
1063
+ if (
1064
+ !sceneBoundingBox ||
1065
+ (sceneBoundingBox.isBox3 && sceneBoundingBox.isEmpty && sceneBoundingBox.isEmpty())
1066
+ ) {
1067
+ const obj =
1068
+ typeof modelGroup !== 'undefined' &&
1069
+ modelGroup &&
1070
+ modelGroup.children &&
1071
+ modelGroup.children.length
1072
+ ? modelGroup
1073
+ : scene;
1074
+ if (obj) {
1075
+ sceneBoundingBox = new this.THREE.Box3().setFromObject(obj);
1076
+ }
1077
+ }
1078
+ if (sceneBoundingBox && sceneBoundingBox.isBox3) {
1079
+ boxHitPoint = ray.intersectBox(sceneBoundingBox, new this.THREE.Vector3());
1080
+ }
1081
+
1082
+ // targetPoint 保留原平面回退逻辑(用于其他需要点位的场景)
1083
+ let targetPoint = boxHitPoint;
1084
+ if (!targetPoint) {
1085
+ if (plane && plane.isPlane) {
1086
+ targetPoint = ray.intersectPlane(plane, new this.THREE.Vector3());
1087
+ if (!targetPoint) {
1088
+ targetPoint = new this.THREE.Vector3();
1089
+ plane.projectPoint(cameraPosition, targetPoint);
1090
+ }
1091
+ } else {
1092
+ // 无平面参数时,给出一个前向的占位点
1093
+ targetPoint = new this.THREE.Vector3();
1094
+ }
1095
+ }
1096
+
1097
+ // 以相机为原点,到包围盒交点的直线距离(若无交点则为 null)
1098
+ const distanceToBox = boxHitPoint ? cameraPosition.distanceTo(boxHitPoint) : null;
1099
+
1100
+ return {
1101
+ targetPoint,
1102
+ distanceToNearPlane: distanceToBox, // 兼容旧字段名
1103
+ distanceToBox,
1104
+ cameraPosition,
1105
+ hitPoint: boxHitPoint,
1106
+ nearPlanePoint: null,
1107
+ face: null,
1108
+ };
1109
+ },
1110
+
1111
+ /**
1112
+ * 检测模型是否在当前视椎体内
1113
+ * @param {THREE.Object3D} model - 要检测的模型对象
1114
+ * @param {number} [instanceId] - InstancedMesh 的实例索引
1115
+ * @param {THREE.Frustum} [frustum] - 预计算的视锥体对象
1116
+ * @returns {boolean} 是否在视椎体内
1117
+ */
1118
+ isModelInFrustum(model, instanceId = null, frustum = null) {
1119
+ if (!model || !camera) return true;
1120
+
1121
+ // 优先使用传入的 frustum,避免重复创建
1122
+ if (!frustum) {
1123
+ frustum = new this.THREE.Frustum();
1124
+ const matrix = new this.THREE.Matrix4().multiplyMatrices(
1125
+ camera.projectionMatrix,
1126
+ camera.matrixWorldInverse
1127
+ );
1128
+ frustum.setFromProjectionMatrix(matrix);
1129
+ }
1130
+
1131
+ let box;
1132
+ // 针对 InstancedMesh 的特定实例进行检测
1133
+ if (instanceId !== null && model.isInstancedMesh) {
1134
+ const instanceMatrix = new this.THREE.Matrix4();
1135
+ model.getMatrixAt(instanceId, instanceMatrix);
1136
+ // 计算实例的世界变换矩阵: World = MeshWorld * InstanceLocal
1137
+ const worldMatrix = instanceMatrix.premultiply(model.matrixWorld);
1138
+
1139
+ if (!model.geometry.boundingBox) {
1140
+ model.geometry.computeBoundingBox();
1141
+ }
1142
+ box = model.geometry.boundingBox.clone();
1143
+ box.applyMatrix4(worldMatrix);
1144
+ } else {
1145
+ // 获取模型的包围盒
1146
+ box = new this.THREE.Box3().setFromObject(model);
1147
+ }
1148
+
1149
+ // 检测包围盒是否与视椎体相交
1150
+ return frustum.intersectsBox(box);
1151
+ },
1152
+
1153
+ /**
1154
+ * 计算模型的屏幕空间误差(SSE)
1155
+ * SSE = (物体包围盒大小 * 视口高度) / (2 * 相机到物体包围盒中心距离 * tan(fov/2))
1156
+ * @param {THREE.Object3D} model - 要计算SSE的模型对象
1157
+ * @returns {number} SSE值
1158
+ */
1159
+ calculateSSE(model) {
1160
+ if (!model || !camera || !renderer) return 0;
1161
+
1162
+ // 获取模型的包围盒
1163
+ const box = new this.THREE.Box3().setFromObject(model);
1164
+
1165
+ // 计算包围盒的大小(使用对角线长度作为物体大小)
1166
+ const size = box.getSize(new this.THREE.Vector3());
1167
+ const objectSize = size.length();
1168
+
1169
+ // 获取包围盒中心点
1170
+ const center = box.getCenter(new this.THREE.Vector3());
1171
+
1172
+ // 获取相机世界位置
1173
+ const cameraPosition = new this.THREE.Vector3();
1174
+ camera.getWorldPosition(cameraPosition);
1175
+
1176
+ // 计算相机到物体包围盒中心的距离
1177
+ const distance = cameraPosition.distanceTo(center);
1178
+
1179
+ // 防止除零错误
1180
+ if (distance === 0) return Infinity;
1181
+
1182
+ // 获取视口高度
1183
+ const viewportHeight = renderer.domElement.clientHeight;
1184
+
1185
+ // 计算tan(fov/2),fov是以度为单位
1186
+ const halfFovRad = this.THREE.MathUtils.degToRad(camera.fov / 2);
1187
+ const tanHalfFov = Math.tan(halfFovRad);
1188
+
1189
+ // 计算SSE
1190
+ const sse = (objectSize * viewportHeight) / (2 * distance * tanHalfFov);
1191
+
1192
+ return { sse, distance };
1193
+ },
1194
+ // 针对已卸载实例的信息进行视锥体检测
1195
+ isInstanceInfoInFrustum(instanceInfo) {
1196
+ if (!instanceInfo || !camera) return true;
1197
+ const { geometry, originalMatrix, parent } = instanceInfo;
1198
+ if (!geometry || !originalMatrix) return false;
1199
+
1200
+ const frustum = new this.THREE.Frustum();
1201
+ const vpMatrix = new this.THREE.Matrix4().multiplyMatrices(
1202
+ camera.projectionMatrix,
1203
+ camera.matrixWorldInverse
1204
+ );
1205
+ frustum.setFromProjectionMatrix(vpMatrix);
1206
+
1207
+ const parentWorld =
1208
+ parent && parent.matrixWorld
1209
+ ? parent.matrixWorld
1210
+ : instanceInfo.parentWorldMatrix || new this.THREE.Matrix4();
1211
+ const worldMatrix = parentWorld.clone().multiply(originalMatrix);
1212
+
1213
+ if (!geometry.boundingBox) {
1214
+ geometry.computeBoundingBox();
1215
+ }
1216
+ const box = geometry.boundingBox.clone();
1217
+ box.applyMatrix4(worldMatrix);
1218
+
1219
+ return frustum.intersectsBox(box);
1220
+ },
1221
+
1222
+ /**
1223
+ * 执行视椎体裁切,清理视椎体外的InstancedMesh实例
1224
+ */
1225
+ async performFrustumCulling() {
1226
+ const modelState = this.noObserver
1227
+ ? this.noObserver.modelStateManager
1228
+ : this.modelStateManager;
1229
+ if (!this.modelStateManager.frustumCheckEnabled || !scene) return;
1230
+ const now = Date.now();
1231
+ if (now - modelState.lastCullingTime < 100) {
1232
+ return;
1233
+ }
1234
+ modelState.lastCullingTime = now;
1235
+
1236
+ // 预先创建视椎体,供后续遍历复用
1237
+ if (!this._frustum) this._frustum = new this.THREE.Frustum();
1238
+ if (!this._vpMatrix) this._vpMatrix = new this.THREE.Matrix4();
1239
+
1240
+ const globalFrustum = this._frustum;
1241
+ const globalVpMatrix = this._vpMatrix.multiplyMatrices(
1242
+ camera.projectionMatrix,
1243
+ camera.matrixWorldInverse
1244
+ );
1245
+ globalFrustum.setFromProjectionMatrix(globalVpMatrix);
1246
+
1247
+ // 使用局部变量收集 ID,最后一次性赋值
1248
+ const toUnload = [];
1249
+ const bypassList = modelState.bypassCullingModelIds;
1250
+ const visibleIds = [];
1251
+ const candidates = [];
1252
+ if (bypassList && bypassList.size > 0) {
1253
+ bypassList.forEach(id => visibleIds.push(id));
1254
+ }
1255
+ const visibleIdSet = new Set(visibleIds);
1256
+ if (this._octree) {
1257
+ const frustum = this._getCurrentFrustum();
1258
+ const exclude = bypassList || new Set();
1259
+ const hits = this.queryOctreeByFrustum(frustum, exclude);
1260
+ for (let i = 0; i < hits.length; i++) {
1261
+ candidates.push(hits[i]);
1262
+ }
1263
+ } else if (this._boxIndex && this._boxIndex.size > 0) {
1264
+ this._boxIndex.forEach((box, modelId) => {
1265
+ if (bypassList && bypassList.size > 0 && bypassList.has(modelId)) return;
1266
+ if (this.isBoxInFrustum(box)) {
1267
+ candidates.push({ modelId, box });
1268
+ }
1269
+ });
1270
+ }
1271
+ const occlusionState = this.noObserver
1272
+ ? this.noObserver.occlusionState
1273
+ : this.occlusionState;
1274
+ // 从响应式对象获取开关状态
1275
+ const occlusionEnabled = this.occlusionState && this.occlusionState.enabled;
1276
+
1277
+ if (occlusionEnabled) {
1278
+ const state = occlusionState;
1279
+ const w =
1280
+ renderer && renderer.domElement
1281
+ ? renderer.domElement.clientWidth || renderer.domElement.width || 0
1282
+ : 0;
1283
+ const h =
1284
+ renderer && renderer.domElement
1285
+ ? renderer.domElement.clientHeight || renderer.domElement.height || 0
1286
+ : 0;
1287
+ // bufferWidth/Height 仍从响应式对象读取以支持动态修改
1288
+ // 性能优化:使用降采样缓冲区进行遮挡剔除,大幅减少 readPixels 耗时 (从全屏降至约 256px 宽)
1289
+ const aspectRatio = h > 0 ? w / h : 1;
1290
+ let sw = this.occlusionState.bufferWidth || 256;
1291
+ let sh = Math.floor(sw / aspectRatio);
1292
+ sw = Math.max(1, sw);
1293
+ sh = Math.max(1, sh);
1294
+ let rt = state._colorRT;
1295
+ const t0 = performance.now();
1296
+ try {
1297
+ if (!rt || state._rtW !== sw || state._rtH !== sh) {
1298
+ if (rt && typeof rt.dispose === 'function') rt.dispose();
1299
+ rt = new this.THREE.WebGLRenderTarget(sw, sh, {
1300
+ depthBuffer: true,
1301
+ stencilBuffer: false,
1302
+ samples: 0
1303
+ });
1304
+ rt.texture.minFilter = this.THREE.NearestFilter;
1305
+ rt.texture.magFilter = this.THREE.NearestFilter;
1306
+ rt.texture.generateMipmaps = false;
1307
+ rt.texture.type = this.THREE.UnsignedByteType;
1308
+ rt.texture.format = this.THREE.RGBAFormat;
1309
+ rt.samples = 0;
1310
+ state._colorRT = rt;
1311
+ state._rtW = sw;
1312
+ state._rtH = sh;
1313
+ state._colorBuffer = new Uint8Array(sw * sh * 4);
1314
+ } else if (!state._colorBuffer || state._colorBuffer.length !== sw * sh * 4) {
1315
+ state._colorBuffer = new Uint8Array(sw * sh * 4);
1316
+ }
1317
+
1318
+ if (!state._occScene) state._occScene = new this.THREE.Scene();
1319
+ // if (!state._occPerfStats) state._occPerfStats = Object.create(null);
1320
+ // const _occRecordPerf = (name, ms, meta) => {
1321
+ // const stats = state._occPerfStats;
1322
+ // const prev = stats[name];
1323
+ // const next = prev || { count: 0, total: 0, max: 0, last: 0, meta: null };
1324
+ // next.count++;
1325
+ // next.total += ms;
1326
+ // next.last = ms;
1327
+ // if (ms > next.max) next.max = ms;
1328
+ // if (meta) next.meta = meta;
1329
+ // stats[name] = next;
1330
+ // };
1331
+
1332
+ // 过滤掉透明物体
1333
+ const opaqueCandidates = [];
1334
+ const transparentCandidates = [];
1335
+ for (let i = 0; i < candidates.length; i++) {
1336
+ if (candidates[i].box.userData && candidates[i].box.userData.transparent) {
1337
+ transparentCandidates.push(candidates[i]);
1338
+ } else {
1339
+ opaqueCandidates.push(candidates[i]);
1340
+ }
1341
+ }
1342
+
1343
+ const totalInstances = opaqueCandidates.length;
1344
+ const transparentTotal = transparentCandidates.length;
1345
+ const activeIdIndexArr =
1346
+ state._occIdIndexArr && Array.isArray(state._occIdIndexArr)
1347
+ ? state._occIdIndexArr
1348
+ : (state._occIdIndexArr = []);
1349
+ const transparentIdIndexArr =
1350
+ state._occTransparentIdIndexArr && Array.isArray(state._occTransparentIdIndexArr)
1351
+ ? state._occTransparentIdIndexArr
1352
+ : (state._occTransparentIdIndexArr = []);
1353
+
1354
+ if (!state._occRenderObjects) state._occRenderObjects = Object.create(null);
1355
+ const occObjs = state._occRenderObjects;
1356
+ if (!occObjs._initialized) {
1357
+ state._occScene.clear();
1358
+ occObjs._initialized = true;
1359
+ }
1360
+
1361
+ // const children = state._occScene.children;
1362
+ // while (children.length > 0) {
1363
+ // const child = children.pop(); // O(1) 删除末尾
1364
+
1365
+ // if (child.geometry) child.geometry.dispose();
1366
+ // if (child.material) {
1367
+ // if (Array.isArray(child.material)) child.material.forEach(m => m.dispose());
1368
+ // else child.material.dispose();
1369
+ // }
1370
+
1371
+ // // 手动断开引用
1372
+ // child.parent = null;
1373
+ // // child.dispatchEvent({ type: 'removed' }); // 保持轻量,暂不触发事件
1374
+ // }
1375
+
1376
+ const asyncBuildEnabled =
1377
+ this.occlusionState && this.occlusionState.asyncBuildEnabled && !!window.requestAnimationFrame;
1378
+
1379
+ const _occBuild = opaqueList => {
1380
+ const tBuild0 = performance.now();
1381
+
1382
+ const flag1Items = [];
1383
+ const flag3Items = [];
1384
+
1385
+ let globalIdx = 1;
1386
+ for (let i = 0; i < opaqueList.length; i++) {
1387
+ const c = opaqueList[i];
1388
+ const obbData = c.box && c.box.userData ? c.box.userData.obbData : null;
1389
+ if (!obbData || !obbData.length) continue;
1390
+ const idx = globalIdx++;
1391
+ activeIdIndexArr[idx] = this.formatModelId(c.modelId);
1392
+
1393
+ const flag = (c.box.userData && c.box.userData.flag) || 1;
1394
+ if (flag === 3 && c.box.userData && c.box.userData.obb) {
1395
+ flag3Items.push({ c, idx });
1396
+ } else if (flag === 1) {
1397
+ flag1Items.push({ c, idx });
1398
+ }
1399
+ }
1400
+ const maxIdx = globalIdx - 1;
1401
+ activeIdIndexArr.length = maxIdx + 1;
1402
+
1403
+ const _tempMatrix = occObjs._tempMatrix || (occObjs._tempMatrix = new this.THREE.Matrix4());
1404
+ const _tempColor = occObjs._tempColor || (occObjs._tempColor = new this.THREE.Color());
1405
+ const _tempScale = occObjs._tempScale || (occObjs._tempScale = new this.THREE.Vector3());
1406
+ const _tempVec3 = occObjs._tempVec3 || (occObjs._tempVec3 = new this.THREE.Vector3());
1407
+ const _tempMatA = occObjs._tempMatA || (occObjs._tempMatA = new this.THREE.Matrix4());
1408
+
1409
+ let boxes = occObjs.boxes;
1410
+ const boxCount = flag3Items.length;
1411
+ if (boxCount > 0) {
1412
+ const needCapacity = boxCount;
1413
+ const capacity = boxes && typeof boxes.capacity === 'number' ? boxes.capacity : 0;
1414
+ if (!boxes || capacity < needCapacity) {
1415
+ if (boxes && boxes.mesh) {
1416
+ state._occScene.remove(boxes.mesh);
1417
+ if (boxes.mesh.geometry) boxes.mesh.geometry.dispose();
1418
+ if (boxes.mesh.material) boxes.mesh.material.dispose();
1419
+ }
1420
+ const nextCap = Math.max(needCapacity, capacity > 0 ? capacity * 2 : 256);
1421
+ const geometry = new this.THREE.BoxGeometry(1, 1, 1);
1422
+ const mat = new this.THREE.MeshBasicMaterial({
1423
+ color: 0xffffff,
1424
+ side: this.THREE.DoubleSide,
1425
+ blending: this.THREE.NoBlending,
1426
+ depthTest: true,
1427
+ depthWrite: true,
1428
+ toneMapped: false,
1429
+ });
1430
+ const mesh = new this.THREE.InstancedMesh(geometry, mat, nextCap);
1431
+ mesh.frustumCulled = false;
1432
+ mesh.matrixAutoUpdate = false;
1433
+ state._occScene.add(mesh);
1434
+ boxes = occObjs.boxes = { mesh, capacity: nextCap };
1435
+ } else if (boxes.mesh && boxes.mesh.parent !== state._occScene) {
1436
+ state._occScene.add(boxes.mesh);
1437
+ }
1438
+ boxes.mesh.count = boxCount;
1439
+ for (let i = 0; i < boxCount; i++) {
1440
+ const item = flag3Items[i];
1441
+ const { matrix, halfSize } = item.c.box.userData.obb;
1442
+ const idx = item.idx;
1443
+ const r = (idx & 255) / 255;
1444
+ const g = ((idx >> 8) & 255) / 255;
1445
+ const b = ((idx >> 16) & 255) / 255;
1446
+ _tempColor.setRGB(r, g, b);
1447
+
1448
+ _tempScale.copy(halfSize).multiplyScalar(2);
1449
+ _tempMatrix.copy(matrix);
1450
+ _tempMatrix.scale(_tempScale);
1451
+ if (typeof bizToThreeMatrix !== 'undefined' && bizToThreeMatrix) {
1452
+ _tempMatrix.premultiply(bizToThreeMatrix);
1453
+ }
1454
+
1455
+ boxes.mesh.setMatrixAt(i, _tempMatrix);
1456
+ boxes.mesh.setColorAt(i, _tempColor);
1457
+ }
1458
+ boxes.mesh.instanceMatrix.needsUpdate = true;
1459
+ if (boxes.mesh.instanceColor) boxes.mesh.instanceColor.needsUpdate = true;
1460
+ boxes.mesh.visible = true;
1461
+ } else if (boxes && boxes.mesh) {
1462
+ boxes.mesh.count = 0;
1463
+ boxes.mesh.visible = false;
1464
+ }
1465
+
1466
+ let batch1 = occObjs.batch1;
1467
+ const flag1Count = flag1Items.length;
1468
+ if (flag1Count > 0) {
1469
+ let totalVerts = 0;
1470
+ let totalIdx = 0;
1471
+ for (let i = 0; i < flag1Count; i++) {
1472
+ const c = flag1Items[i].c;
1473
+ const obbData = c.box.userData.obbData;
1474
+ const v = (obbData.length / 3) | 0;
1475
+ totalVerts += v;
1476
+ const indices = c.box.userData.indices || [];
1477
+ totalIdx += indices && indices.length ? indices.length : v;
1478
+ }
1479
+
1480
+ const needPos = totalVerts * 3;
1481
+ const needIdx = totalIdx;
1482
+
1483
+ const ensureBatch1 = () => {
1484
+ if (batch1 && batch1.mesh && batch1.geometry && batch1.material) return;
1485
+ const geometry = new this.THREE.BufferGeometry();
1486
+ const material = new this.THREE.MeshBasicMaterial({
1487
+ vertexColors: true,
1488
+ side: this.THREE.DoubleSide,
1489
+ blending: this.THREE.NoBlending,
1490
+ depthTest: true,
1491
+ depthWrite: true,
1492
+ toneMapped: false,
1493
+ });
1494
+ const mesh = new this.THREE.Mesh(geometry, material);
1495
+ mesh.frustumCulled = false;
1496
+ batch1 = occObjs.batch1 = { mesh, geometry, material, posCap: 0, idxCap: 0 };
1497
+ state._occScene.add(mesh);
1498
+ };
1499
+ ensureBatch1();
1500
+
1501
+ if (batch1.mesh.parent !== state._occScene) {
1502
+ state._occScene.add(batch1.mesh);
1503
+ }
1504
+
1505
+ if (batch1.posCap < needPos || batch1.idxCap < needIdx) {
1506
+ batch1.posCap = Math.max(needPos, batch1.posCap > 0 ? batch1.posCap * 2 : needPos);
1507
+ batch1.idxCap = Math.max(needIdx, batch1.idxCap > 0 ? batch1.idxCap * 2 : needIdx);
1508
+
1509
+ const positions = new Float32Array(batch1.posCap);
1510
+ const colors = new Float32Array(batch1.posCap);
1511
+ const indices = new Uint32Array(batch1.idxCap);
1512
+
1513
+ batch1.positions = positions;
1514
+ batch1.colors = colors;
1515
+ batch1.indices = indices;
1516
+
1517
+ const posAttr = new this.THREE.BufferAttribute(positions, 3);
1518
+ const colAttr = new this.THREE.BufferAttribute(colors, 3);
1519
+ posAttr.setUsage(this.THREE.DynamicDrawUsage);
1520
+ colAttr.setUsage(this.THREE.DynamicDrawUsage);
1521
+ batch1.geometry.setAttribute('position', posAttr);
1522
+ batch1.geometry.setAttribute('color', colAttr);
1523
+
1524
+ const idxAttr = new this.THREE.BufferAttribute(indices, 1);
1525
+ idxAttr.setUsage(this.THREE.DynamicDrawUsage);
1526
+ batch1.geometry.setIndex(idxAttr);
1527
+ }
1528
+
1529
+ const positions = batch1.positions;
1530
+ const colors = batch1.colors;
1531
+ const indices = batch1.indices;
1532
+
1533
+ let posPtr = 0;
1534
+ let idxPtr = 0;
1535
+ let vertBase = 0;
1536
+
1537
+ for (let i = 0; i < flag1Count; i++) {
1538
+ const { c, idx } = flag1Items[i];
1539
+ const obbData = c.box.userData.obbData;
1540
+ const indicesIn = c.box.userData.indices || [];
1541
+ const matrixArr = c.box.userData.matrix || [];
1542
+
1543
+ const r = (idx & 255) / 255;
1544
+ const g = ((idx >> 8) & 255) / 255;
1545
+ const b = ((idx >> 16) & 255) / 255;
1546
+
1547
+ const vertexCount = (obbData.length / 3) | 0;
1548
+ const needTransform =
1549
+ (matrixArr && matrixArr.length >= 16) ||
1550
+ (typeof bizToThreeMatrix !== 'undefined' && bizToThreeMatrix);
1551
+
1552
+ if (needTransform) {
1553
+ if (matrixArr && matrixArr.length >= 16) {
1554
+ _tempMatA.fromArray(matrixArr);
1555
+ } else {
1556
+ _tempMatA.identity();
1557
+ }
1558
+ if (typeof bizToThreeMatrix !== 'undefined' && bizToThreeMatrix) {
1559
+ _tempMatA.premultiply(bizToThreeMatrix);
1560
+ }
1561
+ }
1562
+
1563
+ if (!needTransform && ArrayBuffer.isView(obbData)) {
1564
+ positions.set(obbData, posPtr);
1565
+ for (let v = 0; v < vertexCount; v++) {
1566
+ const c0 = posPtr + v * 3;
1567
+ colors[c0] = r;
1568
+ colors[c0 + 1] = g;
1569
+ colors[c0 + 2] = b;
1570
+ }
1571
+ posPtr += vertexCount * 3;
1572
+ } else {
1573
+ let srcPtr = 0;
1574
+ for (let v = 0; v < vertexCount; v++) {
1575
+ const x = obbData[srcPtr++];
1576
+ const y = obbData[srcPtr++];
1577
+ const z = obbData[srcPtr++];
1578
+ if (needTransform) {
1579
+ _tempVec3.set(x, y, z).applyMatrix4(_tempMatA);
1580
+ positions[posPtr] = _tempVec3.x;
1581
+ positions[posPtr + 1] = _tempVec3.y;
1582
+ positions[posPtr + 2] = _tempVec3.z;
1583
+ } else {
1584
+ positions[posPtr] = x;
1585
+ positions[posPtr + 1] = y;
1586
+ positions[posPtr + 2] = z;
1587
+ }
1588
+ colors[posPtr] = r;
1589
+ colors[posPtr + 1] = g;
1590
+ colors[posPtr + 2] = b;
1591
+ posPtr += 3;
1592
+ }
1593
+ }
1594
+
1595
+ if (indicesIn && indicesIn.length) {
1596
+ for (let k = 0; k < indicesIn.length; k++) {
1597
+ indices[idxPtr++] = (indicesIn[k] | 0) + vertBase;
1598
+ }
1599
+ } else {
1600
+ for (let k = 0; k < vertexCount; k++) {
1601
+ indices[idxPtr++] = vertBase + k;
1602
+ }
1603
+ }
1604
+ vertBase += vertexCount;
1605
+ }
1606
+
1607
+ batch1.geometry.setDrawRange(0, idxPtr);
1608
+ const posAttr = batch1.geometry.getAttribute('position');
1609
+ const colAttr = batch1.geometry.getAttribute('color');
1610
+ const idxAttr = batch1.geometry.getIndex();
1611
+ posAttr.needsUpdate = true;
1612
+ colAttr.needsUpdate = true;
1613
+ if (idxAttr) idxAttr.needsUpdate = true;
1614
+ batch1.mesh.visible = true;
1615
+ } else if (batch1 && batch1.mesh) {
1616
+ batch1.mesh.visible = false;
1617
+ if (batch1.geometry) batch1.geometry.setDrawRange(0, 0);
1618
+ }
1619
+
1620
+ // const tBuild1 = performance.now();
1621
+ // _occRecordPerf('occ_build_ms', tBuild1 - tBuild0, {
1622
+ // totalInstances,
1623
+ // flag1: flag1Count,
1624
+ // flag3: boxCount,
1625
+ // });
1626
+ state._occMaxIdx = maxIdx;
1627
+ state._occHasBuiltOnce = true;
1628
+ };
1629
+
1630
+ if (totalInstances > 0) {
1631
+ if (asyncBuildEnabled && state._occHasBuiltOnce) {
1632
+ if (!state._occAsyncBuild) state._occAsyncBuild = { pending: false, token: 0 };
1633
+ const asyncState = state._occAsyncBuild;
1634
+ if (!asyncState.pending) {
1635
+ asyncState.pending = true;
1636
+ const token = (asyncState.token + 1) | 0;
1637
+ asyncState.token = token;
1638
+ const opaqueSnapshot = opaqueCandidates.slice();
1639
+ window.requestAnimationFrame(() => {
1640
+ const cur = state._occAsyncBuild;
1641
+ if (!cur || cur.token !== token) return;
1642
+ try {
1643
+ _occBuild(opaqueSnapshot);
1644
+ } finally {
1645
+ if (state._occAsyncBuild && state._occAsyncBuild.token === token) {
1646
+ state._occAsyncBuild.pending = false;
1647
+ }
1648
+ }
1649
+ });
1650
+ }
1651
+ } else {
1652
+ _occBuild(opaqueCandidates);
1653
+ }
1654
+ } else {
1655
+ if (occObjs.boxes && occObjs.boxes.mesh) occObjs.boxes.mesh.visible = false;
1656
+ if (occObjs.batch1 && occObjs.batch1.mesh) occObjs.batch1.mesh.visible = false;
1657
+ state._occMaxIdx = 0;
1658
+ activeIdIndexArr.length = 0;
1659
+ }
1660
+
1661
+ if (!state._occTransparentScene) state._occTransparentScene = new this.THREE.Scene();
1662
+ let transparentMesh = state._occTransparentMesh;
1663
+ const transparentCapacity =
1664
+ transparentMesh && transparentMesh.userData && typeof transparentMesh.userData.capacity === 'number'
1665
+ ? transparentMesh.userData.capacity
1666
+ : 0;
1667
+ if (!transparentMesh || transparentCapacity < transparentTotal) {
1668
+ if (transparentMesh) {
1669
+ if (transparentMesh.geometry) transparentMesh.geometry.dispose();
1670
+ if (transparentMesh.material) transparentMesh.material.dispose();
1671
+ state._occTransparentScene.remove(transparentMesh);
1672
+ }
1673
+ const nextCap = Math.max(transparentTotal, transparentCapacity > 0 ? transparentCapacity * 2 : 256);
1674
+ const geometry = new this.THREE.BoxGeometry(1, 1, 1);
1675
+ const material = new this.THREE.MeshBasicMaterial({
1676
+ color: 0xffffff,
1677
+ side: this.THREE.DoubleSide,
1678
+ blending: this.THREE.NoBlending,
1679
+ depthTest: true,
1680
+ depthWrite: false,
1681
+ toneMapped: false,
1682
+ });
1683
+ transparentMesh = new this.THREE.InstancedMesh(geometry, material, nextCap);
1684
+ transparentMesh.frustumCulled = false;
1685
+ transparentMesh.matrixAutoUpdate = false;
1686
+ transparentMesh.userData.capacity = nextCap;
1687
+ state._occTransparentMesh = transparentMesh;
1688
+ state._occTransparentScene.add(transparentMesh);
1689
+ } else if (transparentMesh.parent !== state._occTransparentScene) {
1690
+ state._occTransparentScene.add(transparentMesh);
1691
+ }
1692
+ if (transparentTotal > 0) {
1693
+ const tempMatrix = new this.THREE.Matrix4();
1694
+ const tempScale = new this.THREE.Vector3();
1695
+ const tempObj = new this.THREE.Object3D();
1696
+ const tempColor = new this.THREE.Color();
1697
+ let tIdx = 1;
1698
+ for (let i = 0; i < transparentTotal; i++) {
1699
+ const c = transparentCandidates[i];
1700
+ const idx = tIdx++;
1701
+ transparentIdIndexArr[idx] = this.formatModelId(c.modelId);
1702
+ const r = (idx & 255) / 255;
1703
+ const g = ((idx >> 8) & 255) / 255;
1704
+ const b = ((idx >> 16) & 255) / 255;
1705
+ tempColor.setRGB(r, g, b);
1706
+ if (c.box.userData && c.box.userData.obb) {
1707
+ const { matrix, halfSize } = c.box.userData.obb;
1708
+ tempMatrix.copy(matrix);
1709
+ tempScale.copy(halfSize).multiplyScalar(2);
1710
+ tempMatrix.scale(tempScale);
1711
+ if (typeof bizToThreeMatrix !== 'undefined' && bizToThreeMatrix) {
1712
+ tempMatrix.premultiply(bizToThreeMatrix);
1713
+ }
1714
+ } else {
1715
+ c.box.getSize(tempScale);
1716
+ c.box.getCenter(tempObj.position);
1717
+ tempObj.scale.copy(tempScale);
1718
+ tempObj.rotation.set(0, 0, 0);
1719
+ tempObj.updateMatrix();
1720
+ tempMatrix.copy(tempObj.matrix);
1721
+ }
1722
+ transparentMesh.setMatrixAt(i, tempMatrix);
1723
+ transparentMesh.setColorAt(i, tempColor);
1724
+ }
1725
+ transparentMesh.count = transparentTotal;
1726
+ transparentMesh.visible = true;
1727
+ transparentMesh.instanceMatrix.needsUpdate = true;
1728
+ if (transparentMesh.instanceColor) transparentMesh.instanceColor.needsUpdate = true;
1729
+ const tMaxIdx = tIdx - 1;
1730
+ transparentIdIndexArr.length = tMaxIdx + 1;
1731
+ state._occTransparentMaxIdx = tMaxIdx;
1732
+ } else if (transparentMesh) {
1733
+ transparentMesh.count = 0;
1734
+ transparentMesh.visible = false;
1735
+ transparentIdIndexArr.length = 0;
1736
+ state._occTransparentMaxIdx = 0;
1737
+ }
1738
+
1739
+ // console.log('renderer', renderer)
1740
+ const prevToneMapping = renderer.toneMapping;
1741
+ const prevTarget = renderer.getRenderTarget ? renderer.getRenderTarget() : null;
1742
+ let prevClearColorHex = 0x000000;
1743
+ let prevClearAlpha = 0;
1744
+ try {
1745
+ const c = renderer.getClearColor ? renderer.getClearColor() : null;
1746
+ if (c && c.isColor) prevClearColorHex = c.getHex();
1747
+ prevClearAlpha =
1748
+ typeof renderer.getClearAlpha === 'function' ? renderer.getClearAlpha() : 0;
1749
+ } catch (_) {}
1750
+ renderer.toneMapping = this.THREE.NoToneMapping;
1751
+ renderer.setRenderTarget(rt);
1752
+ renderer.setClearColor(new this.THREE.Color(0, 0, 0), 0);
1753
+ renderer.clear(true, true, false);
1754
+ camera.updateMatrixWorld(true);
1755
+ renderer.render(state._occScene, camera);
1756
+ sceneBoundingBox = new this.THREE.Box3().setFromObject(state._occScene);
1757
+ // const t1 = performance.now();
1758
+ // 从响应式对象读取 previewEnabled
1759
+
1760
+ // const tPreview0 = performance.now();
1761
+ if (this.occlusionState.previewEnabled) {
1762
+ if (!state._previewCanvas) {
1763
+ const cvs = document.createElement('canvas');
1764
+ cvs.style.position = 'fixed';
1765
+ cvs.style.right = '8px';
1766
+ cvs.style.bottom = '8px';
1767
+ cvs.style.pointerEvents = 'none';
1768
+ cvs.style.zIndex = '9999';
1769
+ cvs.style.border = '1px solid #000';
1770
+ document.body.appendChild(cvs);
1771
+ state._previewCanvas = cvs;
1772
+ state._previewCtx = cvs.getContext('2d');
1773
+ }
1774
+ if (!state._previewBuffer || state._previewBuffer.length !== sw * sh * 4) {
1775
+ state._previewBuffer = new Uint8Array(sw * sh * 4);
1776
+ }
1777
+ state._previewCanvas.width = sw;
1778
+ state._previewCanvas.height = sh;
1779
+ const cssW = Math.min(renderer.domElement.width / 5);
1780
+ const cssH = Math.min(renderer.domElement.height / 5);
1781
+ state._previewCanvas.style.width = cssW + 'px';
1782
+ state._previewCanvas.style.height = cssH + 'px';
1783
+ renderer.readRenderTargetPixels(rt, 0, 0, sw, sh, state._previewBuffer);
1784
+ if (
1785
+ !state._previewImageData ||
1786
+ state._previewImageData.width !== sw ||
1787
+ state._previewImageData.height !== sh
1788
+ ) {
1789
+ state._previewImageData = state._previewCtx.createImageData(sw, sh);
1790
+ }
1791
+ const dst = state._previewImageData.data;
1792
+ const src = state._previewBuffer;
1793
+ for (let row = 0; row < sh; row++) {
1794
+ const srcRow = (sh - 1 - row) * sw * 4;
1795
+ const dstRow = row * sw * 4;
1796
+ dst.set(src.subarray(srcRow, srcRow + sw * 4), dstRow);
1797
+ }
1798
+ state._previewCtx.putImageData(state._previewImageData, 0, 0);
1799
+ } else {
1800
+ if (state._previewCanvas) {
1801
+ if (state._previewCanvas.parentNode)
1802
+ state._previewCanvas.parentNode.removeChild(state._previewCanvas);
1803
+ state._previewCanvas = null;
1804
+ state._previewCtx = null;
1805
+ state._previewBuffer = null;
1806
+ state._previewImageData = null;
1807
+ }
1808
+ }
1809
+ // const tPreview1 = performance.now();
1810
+ // _occRecordPerf('occ_preview_ms', tPreview1 - tPreview0, { sw, sh });
1811
+ renderer.readRenderTargetPixels(rt, 0, 0, sw, sh, state._colorBuffer);
1812
+
1813
+ const idIndexArr = activeIdIndexArr;
1814
+ const maxIdx = typeof state._occMaxIdx === 'number' ? state._occMaxIdx : 0;
1815
+ const totalPixels = sw * sh;
1816
+ const targetSamples = totalPixels;
1817
+ const baseStride = Math.max(
1818
+ 1,
1819
+ Math.floor(Math.sqrt(totalPixels / Math.max(1, targetSamples)))
1820
+ );
1821
+ const extraStride =
1822
+ this.occlusionState && this.occlusionState.sampleStride
1823
+ ? this.occlusionState.sampleStride
1824
+ : 1;
1825
+ const stride = Math.max(1, baseStride * Math.max(1, extraStride));
1826
+ const minSampleCount =
1827
+ this.occlusionState && this.occlusionState.minSampleCount
1828
+ ? this.occlusionState.minSampleCount
1829
+ : 1;
1830
+ const scanResult = await this.scanOcclusionIndices(
1831
+ state._colorBuffer,
1832
+ sw,
1833
+ sh,
1834
+ stride,
1835
+ maxIdx,
1836
+ minSampleCount
1837
+ );
1838
+ if (scanResult && scanResult.buffer) {
1839
+ state._colorBuffer = new Uint8Array(scanResult.buffer);
1840
+ }
1841
+ const indices = scanResult && scanResult.indices ? scanResult.indices : [];
1842
+ for (let i = 0; i < indices.length; i++) {
1843
+ const id = idIndexArr[indices[i]];
1844
+ if (typeof id !== 'undefined') {
1845
+ visibleIdSet.add(id);
1846
+ }
1847
+ }
1848
+
1849
+ const transparentMaxIdx =
1850
+ typeof state._occTransparentMaxIdx === 'number' ? state._occTransparentMaxIdx : 0;
1851
+ if (transparentMaxIdx > 0 && transparentMesh && transparentMesh.visible) {
1852
+ renderer.setClearColor(new this.THREE.Color(0, 0, 0), 0);
1853
+ renderer.clear(true, false, false);
1854
+ renderer.render(state._occTransparentScene, camera);
1855
+ renderer.readRenderTargetPixels(rt, 0, 0, sw, sh, state._colorBuffer);
1856
+
1857
+ const tIdIndexArr = transparentIdIndexArr;
1858
+ const tScanResult = await this.scanOcclusionIndices(
1859
+ state._colorBuffer,
1860
+ sw,
1861
+ sh,
1862
+ stride,
1863
+ transparentMaxIdx,
1864
+ minSampleCount
1865
+ );
1866
+ if (tScanResult && tScanResult.buffer) {
1867
+ state._colorBuffer = new Uint8Array(tScanResult.buffer);
1868
+ }
1869
+ const tIndices = tScanResult && tScanResult.indices ? tScanResult.indices : [];
1870
+ for (let i = 0; i < tIndices.length; i++) {
1871
+ const id = tIdIndexArr[tIndices[i]];
1872
+ if (typeof id !== 'undefined') {
1873
+ visibleIdSet.add(id);
1874
+ }
1875
+ }
1876
+ }
1877
+
1878
+ renderer.setRenderTarget(prevTarget);
1879
+ renderer.setClearColor(prevClearColorHex, prevClearAlpha);
1880
+ renderer.toneMapping = prevToneMapping;
1881
+ } catch (e) {
1882
+ for (let i = 0; i < candidates.length; i++) {
1883
+ visibleIdSet.add(candidates[i].modelId);
1884
+ }
1885
+ }
1886
+ } else {
1887
+ for (let i = 0; i < candidates.length; i++) {
1888
+ visibleIdSet.add(candidates[i].modelId);
1889
+ }
1890
+ }
1891
+ visibleIds.length = 0;
1892
+ visibleIdSet.forEach(id => visibleIds.push(id));
1893
+ const toLoadSet = new Set(visibleIdSet);
1894
+ const parentsToCheck = new Set();
1895
+ scene.traverse(child => {
1896
+ if (!child.isInstancedMesh) return;
1897
+ // 如果复用数量等于大于2个,则跳过裁剪和剔除处理(始终保留)
1898
+ if (child.count >= 2) {
1899
+ let instancesMap =
1900
+ child && child.userData && child.userData.instancesMap instanceof Map
1901
+ ? child.userData.instancesMap
1902
+ : child.parent &&
1903
+ child.parent.userData &&
1904
+ child.parent.userData.instancesMap instanceof Map
1905
+ ? child.parent.userData.instancesMap
1906
+ : null;
1907
+
1908
+ if (instancesMap) {
1909
+ let allInactive = true;
1910
+ const modelIds = Array.from(instancesMap.keys());
1911
+
1912
+ // 第一遍遍历:检查是否有任何一个实例处于活跃状态(在视锥体内且可见)
1913
+ for (const modelId of modelIds) {
1914
+ // 检查已取消勾选模型进行卸载
1915
+ const documentId = modelId.split(':')[1];
1916
+ if(!this.noObserver.documentModelIds.get(documentId)){
1917
+ toUnload.push({ modelId, child });
1918
+ return;
1919
+ }
1920
+
1921
+ if (bypassList && bypassList.size > 0 && bypassList.has(modelId)) return;
1922
+
1923
+ const instanceInfo = instancesMap.get(modelId);
1924
+ const instanceIndex =
1925
+ instanceInfo && typeof instanceInfo.instanceIndex === 'number'
1926
+ ? instanceInfo.instanceIndex
1927
+ : null;
1928
+
1929
+ const inFrustum = this.isModelInFrustum(child, instanceIndex, globalFrustum);
1930
+ const modelInVisible = toLoadSet.has(modelId);
1931
+ // console.log('modelId', modelId, 'inFrustum', inFrustum, 'modelInVisible', modelInVisible)
1932
+
1933
+ // 只要有一个实例在视锥体内且在可见列表中,就不整体卸载
1934
+ if (inFrustum || modelInVisible) {
1935
+ allInactive = false;
1936
+ }
1937
+ }
1938
+
1939
+ // 第二遍遍历:同步状态并决定是否卸载
1940
+ for (const modelId of modelIds) {
1941
+ // 无论是否卸载,只要模型已在场景中,就从待加载集合中移除
1942
+ if (toLoadSet.has(modelId)) {
1943
+ toLoadSet.delete(modelId);
1944
+ }
1945
+
1946
+ // 仅当所有实例都不活跃时,才执行卸载
1947
+ if (!firstPerSign && !roaming && allInactive) {
1948
+ toUnload.push({ modelId, child });
1949
+ }
1950
+ }
1951
+ }
1952
+ // if (toLoadSet.has(modelId)) {
1953
+ // toLoadSet.delete(modelId);
1954
+ // }
1955
+ return;
1956
+ }
1957
+ const modelId =
1958
+ child.parent && child.parent.userData && child.parent.userData.instanceId
1959
+ ? child.parent.userData.instanceId
1960
+ : child.uuid;
1961
+ if (bypassList && bypassList.size > 0 && bypassList.has(modelId)) return;
1962
+
1963
+ const inFrustum = this.isModelInFrustum(child, null, globalFrustum);
1964
+ let modelInVisible = toLoadSet.has(modelId);
1965
+ if (modelInVisible) {
1966
+ toLoadSet.delete(modelId);
1967
+ }
1968
+ if (!firstPerSign && !roaming && (!inFrustum || !modelInVisible)) {
1969
+ toUnload.push({ modelId, child });
1970
+ }
1971
+ });
1972
+ for (const { modelId, child } of toUnload) {
1973
+ if (child && child.parent) parentsToCheck.add(child.parent);
1974
+ this.unloadInstancedModel(modelId, child);
1975
+ }
1976
+ if (scene) {
1977
+ parentsToCheck.forEach(group => {
1978
+ if (group && group.children && group.children.length === 0) {
1979
+ group.removeFromParent();
1980
+ }
1981
+ });
1982
+ }
1983
+ this.modelStateManager.isloadedModelsIds = Object.freeze(Array.from(toLoadSet));
1984
+ },
1985
+ isBoxInFrustum(box) {
1986
+ if (!camera) return true;
1987
+ const frustum = new this.THREE.Frustum();
1988
+ const vpMatrix = new this.THREE.Matrix4().multiplyMatrices(
1989
+ camera.projectionMatrix,
1990
+ camera.matrixWorldInverse
1991
+ );
1992
+ frustum.setFromProjectionMatrix(vpMatrix);
1993
+ return frustum.intersectsBox(box);
1994
+ },
1995
+ formatModelId(modelId) {
1996
+ return Number.isNaN(+modelId) ? modelId : +modelId;
1997
+ },
1998
+ /**
1999
+ * 卸载InstancedMesh实例以释放内存
2000
+ * @param {string} modelId - 模型ID
2001
+ * @param {THREE.InstancedMesh} instancedMesh - InstancedMesh对象
2002
+ */
2003
+ unloadInstancedModel(modelId, instancedMesh) {
2004
+ if (!instancedMesh || !instancedMesh.userData) return;
2005
+
2006
+ // 卸载前内存快照
2007
+ // this.logRendererMemory(`before unload modelId=${modelId}`);
2008
+ const { instanceIndex } = instancedMesh.userData.instancesMap.get(modelId) || 0;
2009
+ // const instanceIndex = instancedMesh.userData.instanceIndex || 0;
2010
+
2011
+ this.removeOutlineInstanceProxy(instancedMesh, instanceIndex);
2012
+
2013
+ // 保存InstancedMesh实例信息用于后续重新加载
2014
+ const instanceInfo = {
2015
+ geometry: instancedMesh.geometry,
2016
+ material: instancedMesh.material,
2017
+ instancedMesh: instancedMesh,
2018
+ instanceIndex: instanceIndex,
2019
+ userData: { ...instancedMesh.userData },
2020
+ // 保存当前实例的变换矩阵
2021
+ originalMatrix: instancedMesh.userData.copyMatrix
2022
+ ? instancedMesh.userData.copyMatrix.clone()
2023
+ : new this.THREE.Matrix4(),
2024
+ // 保存当前实例的颜色
2025
+ originalColor: instancedMesh.material.userData.nColor
2026
+ ? instancedMesh.material.userData.nColor.clone()
2027
+ : new this.THREE.Color(1, 1, 1),
2028
+ };
2029
+
2030
+ // 完全删除实例
2031
+ instanceInfo.parent = instancedMesh.parent || null;
2032
+ instanceInfo.parentWorldMatrix =
2033
+ instancedMesh.parent && instancedMesh.parent.matrixWorld
2034
+ ? instancedMesh.parent.matrixWorld.clone()
2035
+ : new this.THREE.Matrix4();
2036
+ instanceInfo.count = typeof instancedMesh.count === 'number' ? instancedMesh.count : 1;
2037
+
2038
+ const parentGroup = instancedMesh.parent;
2039
+ if (parentGroup) {
2040
+ parentGroup.remove(instancedMesh);
2041
+ // 延迟移除空父组:避免在遍历期间改变树结构,这里不再立即移除 parentGroup
2042
+ }
2043
+
2044
+ // 触发实例相关资源释放(instanceMatrix / instanceColor / morphTexture 等)
2045
+ instancedMesh.dispose();
2046
+
2047
+ // 条件释放基础几何与材质(不共享时)
2048
+ const geometry = instanceInfo.geometry;
2049
+ const material = instanceInfo.material;
2050
+
2051
+ // if (geometry && !this.isGeometryShared(geometry)) {
2052
+ if (geometry) {
2053
+ geometry.dispose();
2054
+ }
2055
+
2056
+ if (material) {
2057
+ if (Array.isArray(material)) {
2058
+ material.forEach(m => {
2059
+ // if (m && !this.isMaterialShared(m)) m.dispose();
2060
+ if (m) m.dispose();
2061
+ });
2062
+ } else {
2063
+ // if (!this.isMaterialShared(material)) material.dispose();
2064
+ material.dispose();
2065
+ }
2066
+ }
2067
+
2068
+ // 置空实例引用(几何/材质对象仍保留以支持后续复用,GPU资源已释放)
2069
+ instanceInfo.instancedMesh = null;
2070
+ },
2071
+
2072
+ /**
2073
+ * 重新加载InstancedMesh实例
2074
+ * @param {string} modelId - 模型ID
2075
+ */
2076
+ reloadInstancedModel(modelId) {
2077
+ // 移除重新加载逻辑实现
2078
+ return;
2079
+ },
2080
+
2081
+ /**
2082
+ * 初始化流式加载器
2083
+ * @param {Object} modelApi - 模型API接口
2084
+ * @param {Object} config - 其他配置
2085
+ */
2086
+ initStreamLoader(modelApi, config = {}) {
2087
+ const { projectId, pbsId, version, ...restConfig } = config || {};
2088
+ const streamLoader = new StreamLoader({
2089
+ modelApi,
2090
+ projectId,
2091
+ debug: isDebug,
2092
+ renderModelData: this.renderModelData.bind(this),
2093
+ ensureNotInteracting: async abortSignal => {
2094
+ if (
2095
+ userInteracting ||
2096
+ this.noObserver.batchLoadingState.interactionState.isInteracting
2097
+ ) {
2098
+ await new Promise(resolve => setTimeout(resolve, 0));
2099
+ }
2100
+ },
2101
+ batchSize: this.noObserver.batchLoadingState.batchSize,
2102
+ onCancelRequestId: async requestId => {
2103
+ if (modelApi && typeof modelApi.postCanceledRequest === 'function') {
2104
+ return await modelApi.postCanceledRequest(requestId);
2105
+ }
2106
+ return null;
2107
+ },
2108
+ ...restConfig,
2109
+ });
2110
+ this.noObserver.streamLoader = streamLoader;
2111
+ },
2112
+
2113
+ /**
2114
+ * 设置当前流式加载的模型数据
2115
+ * @param {Object} item - 模型项信息
2116
+ * @param {Object} materialData - 材质数据
2117
+ */
2118
+ setStreamModel(item, materialData) {
2119
+ if (this.noObserver.streamLoader) {
2120
+ this.noObserver.streamLoader.setCurrentModel(item, materialData);
2121
+ // 设置完模型后,尝试触发一次范围加载以渲染初始视图
2122
+ // 使用 setTimeout 确保在下一帧执行,避免当前栈还在初始化中
2123
+ setTimeout(() => {
2124
+ this.getRangeStream();
2125
+ }, 0);
2126
+ } else {
2127
+ }
2128
+ },
2129
+
2130
+ /**
2131
+ * 批量加载区域(供外部调用)
2132
+ */
2133
+ async batchLoadRegions(item, divideData, materialData) {
2134
+ if (this.noObserver.streamLoader) {
2135
+ await this.noObserver.streamLoader.batchLoadRegions(item, divideData, materialData);
2136
+ }
2137
+ },
2138
+
2139
+ /**
2140
+ * 获取批量加载详情(供外部调用)
2141
+ */
2142
+ async loadModelByIds(options) {
2143
+ // 绕过剔除模型
2144
+ const { ids } = options.params || {};
2145
+ this.addBypassCullingModelIds(ids);
2146
+
2147
+ if (this.noObserver.streamLoader) {
2148
+ return await this.noObserver.streamLoader.loadModelByIds(options);
2149
+ }
2150
+ return null;
2151
+ },
2152
+
2153
+ /**
2154
+ * 处理单个模型项加载(供外部调用)
2155
+ */
2156
+ async processModelItem(item, todoFunc) {
2157
+ if (this.noObserver.streamLoader) {
2158
+ const res = await this.noObserver.streamLoader.processModelItem(item, todoFunc);
2159
+ // item.id is the documentId
2160
+ if (item && item.id) {
2161
+ if (res && res.sceneBox) {
2162
+ this.setSceneBox(res.sceneBox, item.id, true);
2163
+ }
2164
+ if (res && res.boxIndex) {
2165
+ this.setBoxIndex(res.boxIndex, item.id, true);
2166
+ }
2167
+ // this.setBoxIndex(boxJson.data, item.id, true);
2168
+ }
2169
+ return res;
2170
+ } else {
2171
+ return null;
2172
+ }
2173
+ },
2174
+
2175
+ removeModelByDocumentId(documentId) {
2176
+ this.setSceneBox(null, documentId, false);
2177
+ this.setBoxIndex(null, documentId, false);
2178
+ },
2179
+
2180
+ /**
2181
+ * 内部渲染流式数据方法
2182
+ */
2183
+ renderModelData(meshes, primitives, list, range, onComplete, immediateUpdate) {
2184
+ // 构造 drawModel 需要的数据格式
2185
+ const modelRegistry = this.noObserver.streamLoader.modelRegistry;
2186
+ const modelRecords = Array.from(modelRegistry.values());
2187
+ const material = [];
2188
+ modelRecords.forEach(record => {
2189
+ if (record.materialData) {
2190
+ if (Array.isArray(record.materialData)) {
2191
+ material.push(...record.materialData);
2192
+ } else {
2193
+ material.push(record.materialData);
2194
+ }
2195
+ }
2196
+ });
2197
+
2198
+ const regionModelData = {
2199
+ material,
2200
+ primitive: primitives,
2201
+ mesh: meshes, // 注意这里 meshes 对应 drawModel 的 data.mesh (如果它是单个) 或处理逻辑
2202
+ };
2203
+
2204
+ const options = {
2205
+ flatNode: true,
2206
+ colorConfig: this.getColorConfig()
2207
+ };
2208
+ // 如果有 documentId,则设置分组前缀名
2209
+ // if(options.userData.documentId){
2210
+ // options.defineGroupPreName = options.userData.documentId;
2211
+ // options.defineGroupPreNameKey = 'documentId';
2212
+ // }
2213
+
2214
+ // 这里的 list 包含 pbsId 和 version
2215
+ const meshNameConfig = {
2216
+ pbsId: list ? list.folderId : '',
2217
+ version: list ? list.version : '',
2218
+ };
2219
+
2220
+ options.onComplete = onComplete;
2221
+ options.immediateUpdate = immediateUpdate;
2222
+
2223
+ this.drawModel(regionModelData, '', meshNameConfig, options);
2224
+ },
2225
+
2226
+ getRangeStream(options) {
2227
+ // 性能优化注释:禁用 range 被注释字段在 controlArgs 中的传递,仅保留 loadedModels
2228
+ const controlArgs = {
2229
+ loadedModels: this.modelStateManager.isloadedModelsIds,
2230
+ };
2231
+
2232
+ if (this.noObserver && this.noObserver.streamLoader) {
2233
+ // 如果已初始化内部流式加载器,则直接调用
2234
+ this.noObserver.streamLoader.handleCameraControlForStream(controlArgs);
2235
+ } else {
2236
+ // 否则保持原有行为,抛出事件由外部处理
2237
+ this.$emit('control', controlArgs);
2238
+ }
2239
+
2240
+ // 性能优化注释:禁用 range 被注释字段相关的范围缓存更新
2241
+ // lastFormatMin = { ...formatMin };
2242
+ // lastFormatMax = { ...formatMax };
2243
+ },
2244
+ computeFrustumAABB() {
2245
+ // 确保相机矩阵最新
2246
+ camera.updateMatrixWorld(true);
2247
+
2248
+ // NDC 八个角
2249
+ const ndcCorners = {
2250
+ nearBottomLeft: [-1, -1, -1], // near-left-bottom
2251
+ nearBottomRight: [1, -1, -1], // near-right-bottom
2252
+ nearTopRight: [1, 1, -1], // near-right-top
2253
+ nearTopLeft: [-1, 1, -1], // near-left-top
2254
+ farBottomLeft: [-1, -1, 1], // far-left-bottom
2255
+ farBottomRight: [1, -1, 1], // far-right-bottom
2256
+ farTopRight: [1, 1, 1], // far-right-top
2257
+ farTopLeft: [-1, 1, 1], // far-left-top
2258
+ };
2259
+
2260
+ for (let key in ndcCorners) {
2261
+ const v = new this.THREE.Vector3(...ndcCorners[key]);
2262
+ v.unproject(camera); // NDC → world
2263
+ v.applyMatrix4(threeToBizMatrix); // world → biz
2264
+ ndcCorners[key] = v;
2265
+ }
2266
+
2267
+ // 返回展开的坐标值(业务坐标)
2268
+ return {
2269
+ // 近平面(Near Plane)
2270
+ nearTopLeftX: ndcCorners.nearTopLeft.x,
2271
+ nearTopLeftY: ndcCorners.nearTopLeft.y,
2272
+ nearTopLeftZ: ndcCorners.nearTopLeft.z,
2273
+ nearTopRightX: ndcCorners.nearTopRight.x,
2274
+ nearTopRightY: ndcCorners.nearTopRight.y,
2275
+ nearTopRightZ: ndcCorners.nearTopRight.z,
2276
+ nearBottomLeftX: ndcCorners.nearBottomLeft.x,
2277
+ nearBottomLeftY: ndcCorners.nearBottomLeft.y,
2278
+ nearBottomLeftZ: ndcCorners.nearBottomLeft.z,
2279
+ nearBottomRightX: ndcCorners.nearBottomRight.x,
2280
+ nearBottomRightY: ndcCorners.nearBottomRight.y,
2281
+ nearBottomRightZ: ndcCorners.nearBottomRight.z,
2282
+
2283
+ // 远平面(Far Plane)
2284
+ farTopLeftX: ndcCorners.farTopLeft.x,
2285
+ farTopLeftY: ndcCorners.farTopLeft.y,
2286
+ farTopLeftZ: ndcCorners.farTopLeft.z,
2287
+ farTopRightX: ndcCorners.farTopRight.x,
2288
+ farTopRightY: ndcCorners.farTopRight.y,
2289
+ farTopRightZ: ndcCorners.farTopRight.z,
2290
+ farBottomLeftX: ndcCorners.farBottomLeft.x,
2291
+ farBottomLeftY: ndcCorners.farBottomLeft.y,
2292
+ farBottomLeftZ: ndcCorners.farBottomLeft.z,
2293
+ farBottomRightX: ndcCorners.farBottomRight.x,
2294
+ farBottomRightY: ndcCorners.farBottomRight.y,
2295
+ farBottomRightZ: ndcCorners.farBottomRight.z,
2296
+ };
2297
+ },
2298
+ initRender() {
2299
+ renderer = new this.THREE.WebGLRenderer({
2300
+ antialias: true,
2301
+ alpha: true,
2302
+ logarithmicDepthBuffer: true,
2303
+ powerPreference: 'high-performance',
2304
+ preserveDrawingBuffer: false, //保留图形缓冲区 TODO 临时截图使用
2305
+ });
2306
+ renderer.debug.checkShaderErrors = false;
2307
+ renderer.info.autoReset = false;
2308
+ renderer.setPixelRatio(window.devicePixelRatio);
2309
+ const rect = instructions.getBoundingClientRect();
2310
+ renderer.setSize(rect.width, rect.height);
2311
+ renderer.domElement.id = 'three-model';
2312
+ renderer.shadowMap.enabled = true;
2313
+ renderer.outputEncoding = this.THREE.sRGBEncoding;
2314
+ instructions.appendChild(renderer.domElement);
2315
+ renderer.setClearAlpha(0);
2316
+
2317
+ // 监听体系已重构至 initCameraChangeObserver,此处仅保留必要的初始化
2318
+ // 原始的 wheel 监听逻辑已迁移
2319
+ // renderer.domElement.addEventListener('wheel', this._wheelHandler);
2320
+
2321
+ // 与校审截图功能冲突,暂时先注释掉
2322
+ // -----------
2323
+ // renderer.autoClear = false;
2324
+ // renderer.autoClearColor = false;
2325
+ // renderer.autoClearDepth = false;
2326
+ // renderer.autoClearStencil = false;
2327
+ // -----------
2328
+ },
2329
+ initPostProcessing() {
2330
+ outlineComposer = new EffectComposer(renderer, renderTarget);
2331
+
2332
+ const renderPass = new RenderPass(scene, camera);
2333
+ outlineComposer.addPass(renderPass);
2334
+
2335
+ outlinePass = new OutlinePass(
2336
+ new this.THREE.Vector2(window.innerWidth, window.innerHeight),
2337
+ scene,
2338
+ camera
2339
+ );
2340
+ outlinePass.edgeStrength = 3;
2341
+ outlinePass.edgeGlow = 0.5; // 边缘模糊度
2342
+ outlinePass.edgeThickness = 2; // 轮廓线宽度
2343
+ outlinePass.visibleEdgeColor.set('#ffffff'); // 默认白色,后续可动态调整
2344
+ outlinePass.hiddenEdgeColor.set('#ffffff');
2345
+ outlineComposer.addPass(outlinePass);
2346
+
2347
+ const outputPass = new OutputPass();
2348
+ outlineComposer.addPass(outputPass);
2349
+ },
2350
+ initScene() {
2351
+ modelGroup = new this.THREE.Group();
2352
+ scene = new this.THREE.Scene();
2353
+ if (isDebug) {
2354
+ stats = new Stats();
2355
+ document.body.appendChild(stats.dom);
2356
+ }
2357
+ },
2358
+ initCamera() {
2359
+ camera = new this.THREE.PerspectiveCamera(
2360
+ 45,
2361
+ window.innerWidth / window.innerHeight,
2362
+ 0.1,
2363
+ 1000000
2364
+ );
2365
+ // camera.position.set(0, 100, 150);
2366
+ },
2367
+ initControl() {
2368
+ // 初始化控件
2369
+ cameraControls = new CameraControls(camera, renderer.domElement);
2370
+ cameraControls.dollyToCursor = true;
2371
+ cameraControls.smoothTime = 0.1;
2372
+ cameraControls.draggingSmoothTime = 0.05;
2373
+ cameraControls.truckSpeed = 2.0;
2374
+ cameraControls.infinityDolly = true;
2375
+ cameraControls.minDistance = 10;
2376
+ // 若已存在场景包围盒,初始化时设置最大dolly距离为其对角线长度
2377
+
2378
+
2379
+ cameraControls.dollySpeed = 0.15; // 鼠标滚轮每次移动速度倍率
2380
+ },
2381
+
2382
+ setCameraObserverEnabled(enabled) {
2383
+ this.noObserver.isObserverEnabled = enabled;
2384
+ },
2385
+
2386
+ /**
2387
+ * 初始化相机变更观察者 (CameraChangeObserver)
2388
+ * 统一监听:用户交互(Wheel/Control)、程序化动画、API调用
2389
+ */
2390
+ initCameraChangeObserver() {
2391
+ // 初始化 control 状态标志
2392
+ this.noObserver.isControlActive = false;
2393
+
2394
+ // 1. 创建统一的响应触发器 (Debounced)
2395
+ this._cameraChangeObserver = this.debounce(
2396
+ async source => {
2397
+ // 如果观察者被禁用,则不执行后续逻辑
2398
+ if (!this.noObserver.isObserverEnabled) {
2399
+ return;
2400
+ }
2401
+
2402
+ // 特殊处理:如果正在执行初始居中操作,忽略此次变更
2403
+ // if (this.noObserver.isPerformingInitialCentering) {
2404
+ // if (source === 'rest') {
2405
+ // this.noObserver.isPerformingInitialCentering = false;
2406
+ // }
2407
+ // return;
2408
+ // }
2409
+
2410
+ if (source === 'rest' && this.noObserver.isControlActive) {
2411
+ return;
2412
+ }
2413
+
2414
+ // 在执行裁剪和范围流之前,先调整相机远面 (如果需要)
2415
+ // this.adjustCameraFarPlaneForSceneBox();
2416
+
2417
+ // 执行视锥体裁剪
2418
+ // 第一视角不裁切
2419
+ if (this.modelStateManager.frustumCheckEnabled) {
2420
+ await this.performFrustumCulling();
2421
+ }
2422
+
2423
+ // 执行流式加载请求
2424
+ if (
2425
+ this.modelStateManager.isloadedModelsIds &&
2426
+ this.modelStateManager.isloadedModelsIds.length > 0
2427
+ ) {
2428
+ this.getRangeStream(false);
2429
+ }
2430
+ },
2431
+ source => {
2432
+ // 漫游模式下,或者第一人称移动时,使用节流策略(每300ms触发一次)
2433
+ // 避免在持续运动中因防抖重置定时器而无法触发
2434
+ if (source === 'roam' || source === 'firstPersonMove') {
2435
+ return { type: 'throttle', limit: 1000 };
2436
+ }
2437
+ return 300; // 默认防抖 300ms
2438
+ }
2439
+ );
2440
+
2441
+ // 2. 公开API:允许外部或业务逻辑手动触发更新
2442
+ this.notifyCameraChange = (source = 'api') => {
2443
+ this._cameraChangeObserver(source);
2444
+ };
2445
+
2446
+ // 3. 监听器 - Wheel (用户滚轮)
2447
+ this._wheelHandler = res => {
2448
+ this.$emit('wheelStart', res);
2449
+
2450
+ // 统一交互开始处理
2451
+ this.beginInteraction('wheel');
2452
+
2453
+ // 设置滚轮交互的结束检测
2454
+ this.scheduleWheelInteractionEnd(res, 100);
2455
+
2456
+ // 滚轮逻辑:infinityDolly 与 maxDollyDistance 控制
2457
+ const event = res;
2458
+ const deltaY = event && typeof event.deltaY === 'number' ? event.deltaY : 0;
2459
+ const isZoomOut = deltaY > 0; // 远离
2460
+ const isZoomIn = deltaY < 0; // 靠近
2461
+
2462
+ // if (isZoomOut) {
2463
+ // cameraControls.infinityDolly = false; // 向外遵守 maxDistance
2464
+ // } else if (isZoomIn) {
2465
+ // cameraControls.infinityDolly = true; // 向前允许推进(超越 minDistance)
2466
+ // }
2467
+
2468
+ // dolly最大距离限制
2469
+ try {
2470
+ if (sceneBoundingBox && isFinite(maxDollyDistance)) {
2471
+ if (isZoomOut) {
2472
+ camera.updateMatrixWorld(true);
2473
+ const currentDistance = camera.position.distanceTo(cameraControls._target);
2474
+ if (currentDistance >= maxDollyDistance) {
2475
+ event.preventDefault && event.preventDefault();
2476
+ event.stopImmediatePropagation && event.stopImmediatePropagation();
2477
+ return;
2478
+ }
2479
+ }
2480
+ }
2481
+ } catch (e) {
2482
+ // 防御:任何异常不影响交互流程
2483
+ }
2484
+
2485
+ // 触发观察者
2486
+ this._cameraChangeObserver('wheel');
2487
+ };
2488
+ renderer.domElement.addEventListener('wheel', this._wheelHandler);
2489
+
2490
+ // 4. 监听器 - Controls (用户拖拽/操作)
2491
+ this._onControlStart = res => {
2492
+ this.noObserver.isControlActive = true;
2493
+
2494
+ // 统一交互开始处理(相机)
2495
+ this.beginInteraction('camera');
2496
+ this.$emit('controlStart', res);
2497
+
2498
+ // 记录鼠标按下时的位置
2499
+ const event = res.originalEvent || window.event;
2500
+ if (event) {
2501
+ mouseDownPosition.x = event.clientX || 0;
2502
+ mouseDownPosition.y = event.clientY || 0;
2503
+ isDragging = false;
2504
+ }
2505
+ };
2506
+
2507
+ this._onControlEnd = res => {
2508
+ this.noObserver.isControlActive = false;
2509
+
2510
+ this.$emit('controlEnd', res);
2511
+
2512
+ // 统一交互结束处理(相机)
2513
+ this.endInteraction('camera', res, { immediateResume: false });
2514
+
2515
+ // 记录鼠标抬起时的位置并计算是否发生了拖拽
2516
+ const event = res.originalEvent || window.event;
2517
+ if (event) {
2518
+ mouseUpPosition.x = event.clientX || 0;
2519
+ mouseUpPosition.y = event.clientY || 0;
2520
+ const deltaX = Math.abs(mouseUpPosition.x - mouseDownPosition.x);
2521
+ const deltaY = Math.abs(mouseUpPosition.y - mouseDownPosition.y);
2522
+ const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
2523
+ isDragging = distance > dragThreshold;
2524
+ }
2525
+
2526
+ // 只有在实际发生拖拽时才执行相关操作
2527
+ if (isDragging) {
2528
+ // 居中逻辑
2529
+ // if (needsCenteringAfterInteraction && !hasExecutedCentering) {
2530
+ // setTimeout(() => {
2531
+ // if (modelGroups.length > 0) {
2532
+ // this.setModelCenter(modelGroups[modelGroups.length - 1]);
2533
+ // hasExecutedCentering = true;
2534
+ // needsCenteringAfterInteraction = false;
2535
+ // }
2536
+ // }, 300);
2537
+ // }
2538
+
2539
+ // 触发观察者
2540
+ this._cameraChangeObserver('control');
2541
+ }
2542
+ };
2543
+
2544
+ cameraControls.addEventListener('controlstart', this._onControlStart);
2545
+ cameraControls.addEventListener('controlend', this._onControlEnd);
2546
+
2547
+ // 5. 监听器 - Programmatic (程序化动画/过渡结束)
2548
+ // 监听 rest 事件,捕获 cameraControls.setLookAt(..., true) 等动画结束(以及阻尼停止)
2549
+ // 注意:rest 事件在用户交互完全静止时也会触发,与 controlEnd 有部分重叠,但 Observer 有防抖,影响不大
2550
+ this._onRest = () => {
2551
+ this._cameraChangeObserver('rest');
2552
+ };
2553
+ cameraControls.addEventListener('rest', this._onRest);
2554
+ },
2555
+ // 初始化光源
2556
+ initLight() {
2557
+ const pmremGenerator = new this.THREE.PMREMGenerator(renderer);
2558
+ scene.environment = pmremGenerator.fromScene(new RoomEnvironment(), 0.04).texture;
2559
+ },
2560
+ // 初始化文字画布
2561
+ initLabelRender() {
2562
+ labelRenderer = new CSS2DRenderer();
2563
+ const rect = instructions.getBoundingClientRect();
2564
+ labelRenderer.setSize(rect.width, rect.height);
2565
+ labelRenderer.domElement.style.position = 'absolute';
2566
+
2567
+ labelRenderer.domElement.style.top = `${rect.top}px`;
2568
+ labelRenderer.domElement.style.pointerEvents = 'none';
2569
+ instructions.appendChild(labelRenderer.domElement);
2570
+ },
2571
+ // 根据模型数据绘制模型实体 业务平台可调用此方法加载模型
2572
+ /*
2573
+ 参数:data 模型数据
2574
+ color: '' 初始化模型的颜色 在业务方 有这个需求
2575
+ meshNameConfig: {}
2576
+ */
2577
+ drawModel(data, color = '', meshNameConfig = {}, options = {}) {
2578
+ // data = require('./mock.json');
2579
+ // 使用新的分帧加载方法,提供进度回调
2580
+ return this.drawModelWithBatchLoading(
2581
+ data,
2582
+ color,
2583
+ meshNameConfig,
2584
+ options,
2585
+ progress => {
2586
+ // 触发原有的进度事件
2587
+ this.$emit('loadProgress', progress);
2588
+ },
2589
+ complete => {
2590
+ options?.onComplete?.(complete);
2591
+ console.log('加载完成')
2592
+ // 触发原有的完成事件
2593
+ this.$emit('loadComplete');
2594
+ }
2595
+ );
2596
+ },
2597
+ // 动态设置视角滚轮的距离
2598
+ setCameraConfig() {
2599
+ // let box3 = new this.THREE.Box3().setFromObject(sceneBoundingBox);
2600
+ let size = new this.THREE.Vector3();
2601
+ sceneBoundingBox.getSize(size);
2602
+ const maxBorder = Math.max(size.x, size.y, size.z);
2603
+ // cameraControls.camera.far = maxBorder * 10; // 设置相机的远裁剪面
2604
+ cameraControls.minDistance = maxBorder * 0.2; // 动态设置视角滚轮的距离
2605
+ camera.updateProjectionMatrix();
289
2606
  },
290
2607
  // 获取mesh的中心点
291
2608
  getMeshCenterAndVolume(mesh) {
292
2609
  const box = new this.THREE.Box3().setFromObject(mesh);
293
2610
  const volume = box.getSize(new this.THREE.Vector3());
294
- const geometry = mesh.geometry;
2611
+ const geometry = mesh.isInstancedMesh ? mesh : mesh.geometry; // 适配instance
295
2612
  geometry.computeBoundingBox();
296
2613
  const center = geometry.boundingBox.getCenter(new this.THREE.Vector3());
297
2614
  return {
@@ -300,6 +2617,10 @@
300
2617
  };
301
2618
  },
302
2619
  mouseDown(event) {
2620
+ // 在鼠标按下时启用渲染中断以提升响应性
2621
+ forceSkipRendering = true;
2622
+ skipNextRenderFrame = true;
2623
+
303
2624
  const intersects = this.getRaycasterObjects(event);
304
2625
  firstTime = new Date().getTime();
305
2626
  let params = {
@@ -315,6 +2636,8 @@
315
2636
  this.$emit('leftMouseDown', params);
316
2637
  },
317
2638
  getRaycasterObjects(event) {
2639
+ // 保护空值,避免在未初始化或销毁后触发错误
2640
+ if (!renderer || !renderer.domElement || !camera || !scene) return [];
318
2641
  // 获取元素在页面中的偏移位置
319
2642
  const rect = renderer.domElement.getBoundingClientRect();
320
2643
  const x = event.clientX - rect.left;
@@ -325,9 +2648,28 @@
325
2648
  mouse.x = (x / rect.width) * 2 - 1;
326
2649
  mouse.y = -(y / rect.height) * 2 + 1;
327
2650
  raycaster.setFromCamera(mouse, camera);
328
- return raycaster.intersectObjects(scene.children, true);
2651
+ return scene && scene.children ? raycaster.intersectObjects(scene.children, true) : [];
2652
+ },
2653
+ getInstanceId(instancedMesh, instanceIndex, options) {
2654
+ if (!instancedMesh || instanceIndex === undefined || instanceIndex === null) {
2655
+ return null;
2656
+ }
2657
+ const meshName = instancedMesh.name;
2658
+ for (const props of instancedMesh.userData.instancesMap.values()) {
2659
+ if (props.instancedMeshId === meshName && props.instanceIndex === instanceIndex) {
2660
+ return props.instanceId;
2661
+ }
2662
+ }
329
2663
  },
330
2664
  mouseClick(event) {
2665
+ // 鼠标抬起时重置渲染中断标记
2666
+ setTimeout(() => {
2667
+ forceSkipRendering = false;
2668
+ // interactionFrameCount = 0;
2669
+ // 恢复批次加载操作
2670
+ this.resumeBatchLoading();
2671
+ }, 50); // 短暂延迟确保交互完成
2672
+
331
2673
  // 在测量模式下,不进行事件暴露
332
2674
  if (!measureFlag) {
333
2675
  lastTime = new Date().getTime();
@@ -347,7 +2689,8 @@
347
2689
  },
348
2690
  };
349
2691
  if (intersects.length > 0) {
350
- intersects[0].object.userData.currentInstanceIndex = intersects[0].instanceId;
2692
+ this.clearBypassCullingModelIds();
2693
+ const instanceId = this.getInstanceId(intersects[0].object, intersects[0].instanceId);
351
2694
  params = {
352
2695
  objects: [intersects[0].object],
353
2696
  mousePosition: { x: event.clientX, y: event.clientY },
@@ -357,6 +2700,8 @@
357
2700
  y: intersects[0].point.y,
358
2701
  z: intersects[0].point.z,
359
2702
  },
2703
+ // inHighPriorityRegion: inHighPriority,
2704
+ instanceId,
360
2705
  };
361
2706
  } else {
362
2707
  params = {
@@ -368,12 +2713,11 @@
368
2713
  y: -1,
369
2714
  z: -1,
370
2715
  },
2716
+ // inHighPriorityRegion: false,
371
2717
  };
372
2718
  }
373
2719
  if (event.button === 0) {
374
2720
  this.$emit('leftClick', params);
375
- } else if (event.button === 2) {
376
- this.$emit('rightClick', params);
377
2721
  } else if (event.button === 1) {
378
2722
  if (lastTime - lastMiddleClickTime < 2000) {
379
2723
  this.$emit('middleDblClick', params);
@@ -391,6 +2735,9 @@
391
2735
  camera.aspect = width / height;
392
2736
  camera.updateProjectionMatrix();
393
2737
  renderer.setSize(width, height, true);
2738
+ if (outlineComposer) {
2739
+ outlineComposer.setSize(width, height);
2740
+ }
394
2741
  labelRenderer.setSize(width, height);
395
2742
  // this.timeRender()
396
2743
  // 这里也要更新测量
@@ -399,34 +2746,82 @@
399
2746
  }
400
2747
  }
401
2748
  },
402
- setModelCenter(mesh) {
403
- const box3 = new this.THREE.Box3();
404
- mesh.traverseVisible(function (object) {
405
- // 3. 只处理有几何体的网格 (Mesh)
406
- if (object.isMesh) {
407
- // 4. 使用 expandByObject 扩展包围盒
408
- // 这个方法会计算 object 的世界坐标包围盒,并将其合并到 correctBoundingBox 中
409
- box3.expandByObject(object);
410
- }
411
- });
412
- // box3.setFromObject(mesh);
413
- const center = new this.THREE.Vector3();
414
- box3.getCenter(center);
2749
+ setModelCenter(target, options = {}) {
2750
+ // return
2751
+ let box3;
2752
+
2753
+ if (target.isBox3) {
2754
+ box3 = target;
2755
+ } else {
2756
+ box3 = new this.THREE.Box3();
2757
+ target.traverseVisible(function (object) {
2758
+ // 3. 只处理有几何体的网格 (Mesh)
2759
+ if (object.isMesh) {
2760
+ // 4. 使用 expandByObject 扩展包围盒
2761
+ // 这个方法会计算 object 的世界坐标包围盒,并将其合并到 correctBoundingBox 中
2762
+ box3.expandByObject(object);
2763
+ }
2764
+ });
2765
+ // box3.setFromObject(mesh);
2766
+ }
2767
+
2768
+ const center = box3.getCenter(new this.THREE.Vector3());
415
2769
  const size = box3.getSize(new this.THREE.Vector3());
416
- camera.position.set(center.x, center.y + size.y, center.z + size.z / 2);
417
- camera.lookAt(new this.THREE.Vector3(center.x, center.y, center.z));
418
- cameraControls.setLookAt(
419
- center.x,
420
- center.y + size.y,
421
- center.z + size.z / 2,
422
- center.x,
423
- center.y,
424
- center.z,
425
- true
426
- );
427
- cameraControls.update(0);
428
- camera.updateProjectionMatrix();
429
- cameraControls.saveState();
2770
+ const maxDim = Math.max(size.x, size.y, size.z);
2771
+
2772
+ // 标记开始初始居中动画,抑制 CameraChangeObserver
2773
+ // if (this.noObserver) {
2774
+ // this.noObserver.isPerformingInitialCentering = true;
2775
+ // }
2776
+ this.cameraLocation({
2777
+ x: center.x,
2778
+ y: center.y + maxDim,
2779
+ z: center.z + maxDim,
2780
+ heading: center.x,
2781
+ pitch: center.y,
2782
+ roll: center.z,
2783
+ enableTransition: options.enableTransition,
2784
+ });
2785
+ // cameraControls.setLookAt(
2786
+ // center.x,
2787
+ // center.y + size.y,
2788
+ // center.z + size.z / 2,
2789
+ // center.x,
2790
+ // center.y,
2791
+ // center.z,
2792
+ // true
2793
+ // );
2794
+ // cameraControls.update(0);
2795
+ // camera.updateProjectionMatrix();
2796
+ // cameraControls.saveState();
2797
+ },
2798
+ // 带防抖和用户交互检测的智能居中方法
2799
+ smartModelCenter(mesh, debounceDelay = 100) {
2800
+ // if (hasExecutedCentering) {
2801
+ // return;
2802
+ // }
2803
+
2804
+ // 如果用户正在交互,标记需要在交互结束后居中
2805
+ if (userInteracting) {
2806
+ needsCenteringAfterInteraction = true;
2807
+ return;
2808
+ }
2809
+
2810
+ // 清除之前的防抖定时器
2811
+ if (centeringDebounceTimer) {
2812
+ clearTimeout(centeringDebounceTimer);
2813
+ }
2814
+
2815
+ // 设置新的防抖定时器
2816
+ centeringDebounceTimer = setTimeout(() => {
2817
+ // 如果已经执行过居中操作,则不再执行
2818
+ if (!userInteracting && !hasExecutedCentering && this.noObserver.documentModelIds.size > 0) {
2819
+ this.setModelCenter(mesh, { enableTransition: false });
2820
+ hasExecutedCentering = true;
2821
+ // 触发场景更新
2822
+ this.notifyCameraChange();
2823
+ }
2824
+ }, debounceDelay);
430
2825
  },
431
2826
  // 修改指定模型实体属性
432
2827
  /**
@@ -449,44 +2844,70 @@
449
2844
  updateProperty(list) {
450
2845
  for (let index = 0; index < list.length; index++) {
451
2846
  let ele = list[index];
452
- let targetObj = this.getObjectByName(ele.name);
2847
+ let type = ele.type || 'mesh';
2848
+ let instanceId = ele.name;
2849
+ let targetObj = this.getObjectByName(instanceId);
453
2850
  for (const key in ele.attr) {
454
2851
  switch (key) {
455
2852
  case 'color':
456
2853
  targetObj.forEach(children => {
457
- const instanceIndex = children.userData.instanceMaps[ele.name]['instanceIndex'];
458
2854
  if (children.isMesh) {
459
- children.setColorAt(
460
- instanceIndex,
461
- new this.THREE.Color(ele.attr[key])
462
- );
2855
+ const { instanceIndex } = children.userData.instancesMap.get(instanceId);
2856
+ children.setColorAt(instanceIndex, new this.THREE.Color(ele.attr[key]));
463
2857
  children.instanceColor.needsUpdate = true;
2858
+ if (outlinePass) {
2859
+ if (children.isInstancedMesh) {
2860
+ const proxy = this.ensureOutlineInstanceProxy(children, instanceIndex);
2861
+ if (proxy && !outlinePass.selectedObjects.includes(proxy)) {
2862
+ outlinePass.selectedObjects.push(proxy);
2863
+ }
2864
+ } else if (!outlinePass.selectedObjects.includes(children)) {
2865
+ outlinePass.selectedObjects.push(children);
2866
+ }
2867
+ }
464
2868
  }
465
2869
  });
466
2870
  break;
467
2871
  case 'visible':
468
2872
  targetObj.forEach(children => {
469
- const instanceIndex = children.userData.instanceMaps[ele.name]['instanceIndex'];
470
- const copyMatrix = children.userData.instanceMaps[ele.name]['copyMatrix'];
2873
+ const { instanceIndex } = children.userData.instancesMap.get(instanceId);
471
2874
  if (ele.attr[key]) {
472
2875
  const restoreMatrix = new this.THREE.Matrix4().copy(
473
- copyMatrix
2876
+ children.userData.copyMatrix
474
2877
  );
475
2878
  children.setMatrixAt(instanceIndex, restoreMatrix);
2879
+ // if (outlinePass) {
2880
+ // if (children.isInstancedMesh) {
2881
+ // const proxy = this.ensureOutlineInstanceProxy(children, instanceIndex);
2882
+ // if (proxy && !outlinePass.selectedObjects.includes(proxy)) {
2883
+ // outlinePass.selectedObjects.push(proxy);
2884
+ // }
2885
+ // } else if (!outlinePass.selectedObjects.includes(children)) {
2886
+ // outlinePass.selectedObjects.push(children);
2887
+ // }
2888
+ // }
476
2889
  } else {
477
2890
  const offsetMatrix = new this.THREE.Matrix4()
478
- .copy(copyMatrix)
2891
+ .copy(children.userData.copyMatrix)
479
2892
  .makeTranslation(9999999, 9999999, 9999999);
480
2893
  children.setMatrixAt(instanceIndex, offsetMatrix);
2894
+ if (outlinePass) {
2895
+ if (children.isInstancedMesh) {
2896
+ this.removeOutlineInstanceProxy(children, instanceIndex);
2897
+ } else {
2898
+ const idx = outlinePass.selectedObjects.indexOf(children);
2899
+ if (idx !== -1) outlinePass.selectedObjects.splice(idx, 1);
2900
+ }
2901
+ }
481
2902
  }
482
2903
  children.instanceMatrix.needsUpdate = true;
483
2904
  });
484
2905
  break;
485
2906
  case 'opacity':
486
- const instanceIndex = children.userData.instanceMaps[ele.name]['instanceIndex'];
487
2907
  targetObj.forEach(children => {
488
2908
  if (children.isMesh) {
489
2909
  const opacity = children.geometry.attributes.opacity.array;
2910
+ const { instanceIndex } = children.userData.instancesMap.get(instanceId);
490
2911
  opacity[instanceIndex] = ele.attr[key];
491
2912
  children.geometry.attributes.opacity.needsUpdate = true;
492
2913
  }
@@ -502,6 +2923,40 @@
502
2923
  // obj.material.needsUpdate = true;
503
2924
  }
504
2925
  },
2926
+ showOutlinePass(instanceId) {
2927
+ let targetObj = this.getObjectByName(instanceId)
2928
+ targetObj.forEach(children => {
2929
+ if (children.isMesh) {
2930
+ const { instanceIndex } = children.userData.instancesMap.get(instanceId);
2931
+ if (outlinePass) {
2932
+ if (children.isInstancedMesh) {
2933
+ const proxy = this.ensureOutlineInstanceProxy(children, instanceIndex);
2934
+ if (proxy && !outlinePass.selectedObjects.includes(proxy)) {
2935
+ outlinePass.selectedObjects.push(proxy);
2936
+ }
2937
+ } else if (!outlinePass.selectedObjects.includes(children)) {
2938
+ outlinePass.selectedObjects.push(children);
2939
+ }
2940
+ }
2941
+ }
2942
+ });
2943
+ },
2944
+ hideOutlinePass(instanceId) {
2945
+ let targetObj = this.getObjectByName(instanceId)
2946
+ targetObj.forEach(children => {
2947
+ if (children.isMesh) {
2948
+ const { instanceIndex } = children.userData.instancesMap.get(instanceId);
2949
+ if (outlinePass) {
2950
+ if (children.isInstancedMesh) {
2951
+ this.removeOutlineInstanceProxy(children, instanceIndex);
2952
+ } else {
2953
+ const idx = outlinePass.selectedObjects.indexOf(children);
2954
+ if (idx !== -1) outlinePass.selectedObjects.splice(idx, 1);
2955
+ }
2956
+ }
2957
+ }
2958
+ });
2959
+ },
505
2960
  // 恢复模型原来的状态
506
2961
  updateWholeProperty() {
507
2962
  throw new Error('该方法已暂停使用,请使用restore方法。');
@@ -519,41 +2974,90 @@
519
2974
  // });
520
2975
  // }
521
2976
  },
2977
+ // 修改材质的自定义数据
2978
+ updateMaterialUserData(list) {
2979
+ for (let index = 0; index < list.length; index++) {
2980
+ let ele = list[index];
2981
+ let instanceId = ele.name;
2982
+ let targetObj = this.getObjectByName(instanceId);
2983
+ for (const key in ele.attr) {
2984
+ switch (key) {
2985
+ case 'nColor':
2986
+ targetObj.forEach(children => {
2987
+ if (children.isMesh) {
2988
+ children.material.userData[key] = new this.THREE.Color(ele.attr[key])
2989
+ }
2990
+ });
2991
+ break;
2992
+ }
2993
+
2994
+ }
2995
+ }
2996
+ },
2997
+ // 重置
2998
+ resetMaterialUserData(list) {
2999
+ console.log('resetMaterialUserData')
3000
+ for (let index = 0; index < list.length; index++) {
3001
+ let ele = list[index];
3002
+ let instanceId = ele.name;
3003
+ let targetObj = this.getObjectByName(instanceId);
3004
+ for (const key in ele.attr) {
3005
+ switch (key) {
3006
+ case 'nColor':
3007
+ targetObj.forEach(children => {
3008
+ if (children.isMesh) {
3009
+ const { instanceIndex } = children.userData.instancesMap.get(instanceId);
3010
+ children.setColorAt(instanceIndex, children.material.userData['oColor']);
3011
+ children.instanceColor.needsUpdate = true;
3012
+ children.material.userData[key] = children.material.userData['oColor']
3013
+ }
3014
+ });
3015
+ break;
3016
+ }
3017
+ }
3018
+ }
3019
+ },
522
3020
  // 清除上一次的属性修改操作 为了方便业务平台参数跟updateProperty方法的参数一样
523
3021
  resetProperty(list) {
524
3022
  for (let index = 0; index < list.length; index++) {
525
3023
  let ele = list[index];
526
- let obj = this.getObjectByName(ele.name);
3024
+ let instanceId = ele.name;
3025
+ let obj = this.getObjectByName(instanceId);
527
3026
  if (obj) {
528
3027
  for (const key in ele.attr) {
529
3028
  switch (key) {
530
3029
  case 'color':
531
3030
  obj.forEach(children => {
532
3031
  if (children.isMesh) {
533
- children.setColorAt(
534
- children.userData.instanceIndex,
535
- children.material.userData.nColor
536
- );
3032
+ const { instanceIndex } = children.userData.instancesMap.get(instanceId);
3033
+ children.setColorAt(instanceIndex, children.material.userData.nColor);
537
3034
  children.instanceColor.needsUpdate = true;
3035
+ if (outlinePass) {
3036
+ if (children.isInstancedMesh) {
3037
+ this.removeOutlineInstanceProxy(children, instanceIndex);
3038
+ } else {
3039
+ const idx = outlinePass.selectedObjects.indexOf(children);
3040
+ if (idx !== -1) outlinePass.selectedObjects.splice(idx, 1);
3041
+ }
3042
+ }
538
3043
  }
539
3044
  });
540
3045
  break;
541
3046
  case 'visible':
542
3047
  obj.forEach(children => {
543
- const index = children.userData.instanceIndex;
544
- const restoreMatrix = new this.THREE.Matrix4().copy(
545
- children.userData.copyMatrix
546
- );
547
- children.setMatrixAt(index, restoreMatrix);
3048
+ const { instanceIndex, copyMatrix } =
3049
+ children.userData.instancesMap.get(instanceId);
3050
+ const restoreMatrix = new this.THREE.Matrix4().copy(copyMatrix);
3051
+ children.setMatrixAt(instanceIndex, restoreMatrix);
548
3052
  children.instanceMatrix.needsUpdate = true;
549
3053
  });
550
3054
  break;
551
3055
  case 'opacity':
552
3056
  obj.forEach(children => {
553
3057
  if (children.isMesh) {
3058
+ const { instanceIndex } = children.userData.instancesMap.get(instanceId);
554
3059
  const opacity = children.geometry.attributes.opacity.array;
555
- opacity[children.userData.instanceIndex] =
556
- children.material.userData.nOpacity;
3060
+ opacity[instanceIndex] = children.material.userData.nOpacity;
557
3061
  children.geometry.attributes.opacity.needsUpdate = true;
558
3062
  }
559
3063
  });
@@ -563,26 +3067,32 @@
563
3067
  }
564
3068
  },
565
3069
  // 定位到模型
3070
+ // name 模型名称 可以是数组
566
3071
  locateModel(name) {
567
- let obj = scene.getObjectByName(name) || this.getObjectByName(name)?.[0];
3072
+ if (!scene) return;
3073
+ if (Array.isArray(name)) {
3074
+ const box3 = new this.THREE.Box3();
3075
+ name.forEach(n => {
3076
+ const arr = this.getObjectByName(n);
3077
+ arr.forEach(o => {
3078
+ box3.expandByObject(o);
3079
+ });
3080
+ });
3081
+ if (!box3.isEmpty()) {
3082
+ const center = box3.getCenter(new this.THREE.Vector3());
3083
+ const size = box3.getSize(new this.THREE.Vector3());
3084
+ this.locateByCenterBox(center, size, { viewAll: true });
3085
+ }
3086
+ return;
3087
+ }
3088
+ let obj = this.getObjectByName(name)[0];
568
3089
  if (obj) {
569
3090
  // cameraControls.fitToSphere(obj.parent, true); // TODO 待处理,先用 setModelCenter 进行定位
570
3091
  if (obj.isGroup) {
571
3092
  this.setModelCenter(obj);
572
3093
  } else if (obj.isMesh) {
573
- const instanceIndex = obj.userData.instanceMaps[name]['instanceIndex'];
574
- const tempMatrix = new this.THREE.Matrix4();
575
- // const instancePos = new this.THREE.Vector3();
576
- obj.getMatrixAt(instanceIndex, tempMatrix);
577
-
578
- let center = this.getCenter(obj, tempMatrix);
579
- // const instanceLocalCenter = center.applyMatrix4(tempMatrix);
580
- // center.applyMatrix4(tempMatrix); // 先应用实例矩阵
581
- // obj.localToWorld(center);
582
- // instancePos.setFromMatrixPosition(tempMatrix); // 提取局部位置
583
- // obj.localToWorld(instancePos);
584
-
585
- let size = this.getSize(obj, tempMatrix);
3094
+ let center = this.getCenter(obj);
3095
+ let size = this.getSize(obj);
586
3096
  this.locateByCenterBox(center, size);
587
3097
  }
588
3098
  }
@@ -601,6 +3111,7 @@
601
3111
  }
602
3112
  */
603
3113
  updatePropertyByCustom(params) {
3114
+ if (!scene) return;
604
3115
  scene.traverse(child => {
605
3116
  if (child.isMesh && child.userData[params.customName] === params.customValue) {
606
3117
  for (const key in params.attr) {
@@ -632,6 +3143,7 @@
632
3143
  视点定位可直接使用此方法
633
3144
  */
634
3145
  cameraLocation(params) {
3146
+ const { enableTransition = true } = params;
635
3147
  cameraControls.setLookAt(
636
3148
  params.x,
637
3149
  params.y,
@@ -639,7 +3151,7 @@
639
3151
  params.heading,
640
3152
  params.pitch,
641
3153
  params.roll,
642
- true
3154
+ enableTransition
643
3155
  );
644
3156
  cameraControls.update(0);
645
3157
  },
@@ -647,20 +3159,27 @@
647
3159
  /*
648
3160
  center: {x: 0, y: 0, z: 0},
649
3161
  box: {x: 0, y: 0, z: 0}
3162
+ options: {
3163
+ viewAll: true/false
3164
+ }
650
3165
  */
651
- locateByCenterBox(center, size) {
3166
+ locateByCenterBox(center, size, options) {
3167
+ let viewAll = (options && options.viewAll) || false;
3168
+
652
3169
  let maxDim = Math.max(size.x, size.y, size.z);
3170
+ let minDim = Math.min(size.x, size.y, size.z);
653
3171
  let fov = camera.fov * (Math.PI / 180);
654
- let distance = Math.min(
655
- maxDim * 100, // 最大距离限制
656
- Math.abs((maxDim * 1.0) / Math.sin(fov / 2))
657
- );
3172
+ // let baseDistance = viewAll
3173
+ // ? Math.abs((minDim * 1.0) / Math.sin(fov / 2))
3174
+ // : Math.abs((maxDim * 1.0) / Math.sin(fov / 2));
3175
+ let baseDistance = Math.abs((maxDim * 1.0) / Math.sin(fov / 2));
3176
+ let distance = Math.min(maxDim * 100, baseDistance);
658
3177
  let direction = new this.THREE.Vector3(1, 1, 1).normalize();
3178
+
659
3179
  let p = new this.THREE.Vector3().copy(center).add(direction.multiplyScalar(distance));
660
- let cameraCenter = new this.THREE.Vector3(p.x, p.y, p.z)
661
- .addScalar(
662
- Math.max(size.x, size.y, size.z)
663
- );
3180
+ let cameraCenter = viewAll
3181
+ ? new this.THREE.Vector3(p.x, p.y, p.z)
3182
+ : new this.THREE.Vector3(p.x, p.y, p.z).addScalar(Math.max(size.x, size.y, size.z));
664
3183
  cameraControls.setLookAt(
665
3184
  cameraCenter.x,
666
3185
  cameraCenter.y,
@@ -681,6 +3200,7 @@
681
3200
  }
682
3201
  */
683
3202
  billboard(data) {
3203
+ if (!scene) return null;
684
3204
  const divLabel = new CSS2DObject(data.billboard);
685
3205
  divLabel.name = data.labelClass; // 这个是用来清除广告牌用的
686
3206
  divLabel.position.set(data.x, data.y, data.z);
@@ -688,12 +3208,17 @@
688
3208
  return divLabel;
689
3209
  },
690
3210
  // 通过名字获取实体对象, 返回数组
691
- getObjectByName(name) {
3211
+ /*
3212
+ name: 实体的名字
3213
+ passType: 要过滤的类型,默认是group,不返回group类型的实体
3214
+ */
3215
+ getObjectByName(name, passType = 'group') {
3216
+ if (!scene) return [];
692
3217
  let object = [];
3218
+ const instancedMeshProps = instanceToInstancedMeshMap.get(name);
693
3219
  scene.traverse(item => {
694
- if (item.name === name
695
- || (item.userData?.instanceMaps?.[name] !== undefined) // 或者找出对应的实例对象
696
- ) {
3220
+ const tempName = instancedMeshProps ? instancedMeshProps.drawObjectId : name;
3221
+ if (item.name == tempName && item.type.toLowerCase() != passType.toLowerCase()) {
697
3222
  object.push(item);
698
3223
  }
699
3224
  });
@@ -701,10 +3226,60 @@
701
3226
  },
702
3227
  // 通过id获取实体对象, 返回查找到的对象
703
3228
  getObjectById(id) {
3229
+ if (!scene) return null;
704
3230
  return scene.getObjectById(id);
705
3231
  },
3232
+ getColorConfig() {
3233
+ const modelState = this.noObserver
3234
+ ? this.noObserver.modelStateManager
3235
+ : this.modelStateManager;
3236
+ return modelState.colorConfig;
3237
+ },
3238
+ addBypassCullingModelIds(modelIds) {
3239
+ const modelState = this.noObserver
3240
+ ? this.noObserver.modelStateManager
3241
+ : this.modelStateManager;
3242
+ if (!modelIds) return;
3243
+ if (Array.isArray(modelIds)) {
3244
+ modelIds.forEach(id => {
3245
+ const modelId = this.formatModelId(id);
3246
+ modelState.bypassCullingModelIds.add(modelId);
3247
+ });
3248
+ } else {
3249
+ const modelId = this.formatModelId(modelIds);
3250
+ modelState.bypassCullingModelIds.add(modelId);
3251
+ }
3252
+ },
3253
+ getBypassCullingModelIds() {
3254
+ const modelState = this.noObserver
3255
+ ? this.noObserver.modelStateManager
3256
+ : this.modelStateManager;
3257
+ return [...modelState.bypassCullingModelIds];
3258
+ },
3259
+ removeBypassCullingModelIds(modelIds) {
3260
+ const modelState = this.noObserver
3261
+ ? this.noObserver.modelStateManager
3262
+ : this.modelStateManager;
3263
+ if (!modelIds) return;
3264
+ if (Array.isArray(modelIds)) {
3265
+ modelIds.forEach(id => {
3266
+ const modelId = this.formatModelId(id);
3267
+ modelState.bypassCullingModelIds.delete(modelId);
3268
+ });
3269
+ } else {
3270
+ const modelId = this.formatModelId(modelIds);
3271
+ modelState.bypassCullingModelIds.delete(modelId);
3272
+ }
3273
+ },
3274
+ clearBypassCullingModelIds() {
3275
+ const modelState = this.noObserver
3276
+ ? this.noObserver.modelStateManager
3277
+ : this.modelStateManager;
3278
+ modelState.bypassCullingModelIds.clear();
3279
+ },
706
3280
  // 通过id删除对象
707
3281
  removeObjectById(id) {
3282
+ if (!scene) return;
708
3283
  let array = this.getObjectByName(id);
709
3284
  array.forEach(item => {
710
3285
  if (item.name === id) {
@@ -719,6 +3294,7 @@
719
3294
  },
720
3295
  // 通过名称删除对象
721
3296
  removeObjectByName(name) {
3297
+ if (!scene) return;
722
3298
  let array = this.getObjectByName(name);
723
3299
  array.forEach(item => {
724
3300
  item.material && item.material.dispose();
@@ -726,7 +3302,7 @@
726
3302
  if (item.isMesh) {
727
3303
  item.clear();
728
3304
  }
729
- scene.remove(item);
3305
+ if (scene) scene.remove(item);
730
3306
  });
731
3307
  },
732
3308
  // 删除场景中所有的实体
@@ -734,6 +3310,7 @@
734
3310
  return new Promise(resolve => {
735
3311
  if (scene) {
736
3312
  this.removeTraverse();
3313
+ this.removeModelByDocumentId();
737
3314
  resolve();
738
3315
  } else {
739
3316
  resolve();
@@ -741,21 +3318,23 @@
741
3318
  });
742
3319
  },
743
3320
  removeTraverse() {
3321
+ if (!modelGroup) return;
744
3322
  let length = modelGroup.children.length;
745
3323
  if (length > 0) {
746
3324
  let list = modelGroup.children[0];
3325
+ if (!list) return;
747
3326
  list.traverse(item => {
748
3327
  if (item.isMesh) {
749
- item.material.dispose();
750
- item.geometry.dispose();
3328
+ item.material && item.material.dispose();
3329
+ item.geometry && item.geometry.dispose();
751
3330
  item.clear();
752
3331
  item.material = null;
753
3332
  item.geometry = null;
754
- modelGroup.remove(item);
3333
+ modelGroup && modelGroup.remove(item);
755
3334
  item = null;
756
3335
  }
757
3336
  });
758
- modelGroup.remove(list);
3337
+ modelGroup && modelGroup.remove(list);
759
3338
  this.removeTraverse();
760
3339
  } else {
761
3340
  // 在这里清除一些标记之类的
@@ -769,15 +3348,125 @@
769
3348
  // 销毁场景 释放内存
770
3349
  destroyScene() {
771
3350
  cancelAnimationFrame(animateId);
3351
+
3352
+ if (this.noObserver && this.noObserver.outlineInstanceProxyMap) {
3353
+ this.noObserver.outlineInstanceProxyMap.forEach(proxy => {
3354
+ if (outlinePass) {
3355
+ const idx = outlinePass.selectedObjects.indexOf(proxy);
3356
+ if (idx !== -1) outlinePass.selectedObjects.splice(idx, 1);
3357
+ }
3358
+ if (scene) scene.remove(proxy);
3359
+ if (proxy && proxy.material && proxy.material.dispose) proxy.material.dispose();
3360
+ });
3361
+ this.noObserver.outlineInstanceProxyMap.clear();
3362
+ }
3363
+
3364
+ this.batchLoadingState = this.noObserver
3365
+ ? this.noObserver.batchLoadingState
3366
+ : this.batchLoadingState;
3367
+ // 清理防抖定时器和重置状态
3368
+ if (centeringDebounceTimer) {
3369
+ clearTimeout(centeringDebounceTimer);
3370
+ centeringDebounceTimer = null;
3371
+ }
3372
+
3373
+ // 清理渲染暂停/恢复相关的定时器
3374
+ if (this.batchLoadingState.resumeTimer) {
3375
+ clearTimeout(this.batchLoadingState.resumeTimer);
3376
+ this.batchLoadingState.resumeTimer = null;
3377
+ }
3378
+
3379
+ // 清理新增的交互状态定时器
3380
+ if (this.batchLoadingState.interactionState.wheelTimeout) {
3381
+ clearTimeout(this.batchLoadingState.interactionState.wheelTimeout);
3382
+ this.batchLoadingState.interactionState.wheelTimeout = null;
3383
+ }
3384
+
3385
+ // 停止批量加载
3386
+ this.stopBatchLoading();
3387
+
3388
+ hasExecutedCentering = false;
3389
+ needsCenteringAfterInteraction = false;
3390
+ userInteracting = false;
3391
+
3392
+ // 关闭视椎体裁切并清理防抖定时器
3393
+ this.modelStateManager.frustumCheckEnabled = false;
3394
+ if (this.modelStateManager.debounceTimer) {
3395
+ clearTimeout(this.modelStateManager.debounceTimer);
3396
+ this.modelStateManager.debounceTimer = null;
3397
+ }
3398
+ this.clearBypassCullingModelIds();
3399
+
772
3400
  if (scene) {
773
3401
  this.removeTraverse();
774
3402
  scene.clear();
775
3403
  scene = null;
776
3404
  }
3405
+
3406
+ // 移除滚轮事件监听器
3407
+ if (renderer && renderer.domElement && this._wheelHandler) {
3408
+ renderer.domElement.removeEventListener('wheel', this._wheelHandler);
3409
+ this._wheelHandler = null;
3410
+ }
3411
+
3412
+ // 移除鼠标点击/按下事件监听器
3413
+ if (renderer && renderer.domElement) {
3414
+ renderer.domElement.removeEventListener('mouseup', this.mouseClick, false);
3415
+ renderer.domElement.removeEventListener('mousedown', this.mouseDown, false);
3416
+ renderer.domElement.removeEventListener('pointerup', this.mouseClick, false);
3417
+ renderer.domElement.removeEventListener('pointerdown', this.mouseDown, false);
3418
+ }
3419
+
3420
+ // 取消 pointer lock 并移除相关键盘事件
3421
+ if (pointControls) {
3422
+ if (this._onFirstPersonChange) {
3423
+ try {
3424
+ pointControls.removeEventListener('change', this._onFirstPersonChange);
3425
+ } catch (e) {}
3426
+ this._onFirstPersonChange = null;
3427
+ }
3428
+ try {
3429
+ pointControls.unlock && pointControls.unlock();
3430
+ } catch (e) {}
3431
+ }
3432
+ if (this.noObserver) {
3433
+ this.noObserver._firstPersonLastPos = null;
3434
+ }
3435
+ window.removeEventListener('keydown', this.onKeyDown);
3436
+ window.removeEventListener('keyup', this.onKeyUp);
3437
+ document.removeEventListener('keydown', this.handleMeasureKeyDown, false);
3438
+
3439
+ // 关闭测量工具
3440
+ if (threeMeasure) {
3441
+ try {
3442
+ threeMeasure.close && threeMeasure.close();
3443
+ } catch (e) {}
3444
+ threeMeasure = null;
3445
+ }
3446
+
3447
+ // 移除 cameraControls 事件监听
3448
+ if (cameraControls) {
3449
+ if (this._onControlStart)
3450
+ cameraControls.removeEventListener('controlstart', this._onControlStart);
3451
+ if (this._onControlEnd)
3452
+ cameraControls.removeEventListener('controlend', this._onControlEnd);
3453
+ this._onControlStart = null;
3454
+ this._onControlEnd = null;
3455
+ }
3456
+
3457
+ // 清理 Label 渲染器 DOM
3458
+ if (labelRenderer && labelRenderer.domElement && instructions) {
3459
+ try {
3460
+ instructions.removeChild(labelRenderer.domElement);
3461
+ } catch (e) {}
3462
+ }
3463
+ labelRenderer = null;
3464
+
777
3465
  renderer.forceContextLoss();
778
3466
  renderer.dispose();
779
3467
  camera = null;
780
3468
  cameraControls = null;
3469
+ pointControls = null;
781
3470
  renderer.domElement = null;
782
3471
  renderer = null;
783
3472
  },
@@ -806,7 +3495,7 @@
806
3495
  });
807
3496
  let line = new this.THREE.Line(geometryLine, materialLine);
808
3497
  line.name = params.name;
809
- scene.add(line);
3498
+ if (scene) scene.add(line);
810
3499
  }
811
3500
  },
812
3501
  // 绘制贴图曲线
@@ -842,7 +3531,7 @@
842
3531
  });
843
3532
  let line = new this.THREE.Line(geometry, material);
844
3533
  line.name = params.name;
845
- scene.add(line);
3534
+ if (scene) scene.add(line);
846
3535
  clock = new this.THREE.Clock();
847
3536
  }
848
3537
  },
@@ -869,7 +3558,7 @@
869
3558
  },
870
3559
  // 更新漫游的配置
871
3560
  /*
872
- 参数为Object
3561
+ 参数为Object
873
3562
  */
874
3563
  updateRoamConfig(params) {
875
3564
  for (let key in params) {
@@ -906,6 +3595,10 @@
906
3595
  cameraControls.setPosition(point.x, point.y + 5, point.z, false);
907
3596
  cameraControls.setTarget(pointBox.x, pointBox.y + 5, pointBox.z, false);
908
3597
  progress += roamConfig.speed / 300;
3598
+
3599
+ if (typeof this._cameraChangeObserver === 'function') {
3600
+ this._cameraChangeObserver('roam');
3601
+ }
909
3602
  } else {
910
3603
  // 循环漫游
911
3604
  if (roamConfig.loop) {
@@ -924,6 +3617,7 @@
924
3617
  globalBomb(val) {
925
3618
  if (scene.children.length === 0) return;
926
3619
  for (let i = 0; i < scene.children.length; i++) {
3620
+ console.log('scene', scene)
927
3621
  scene.children[i].traverse(item => {
928
3622
  if (!item.isMesh || !item.worldDir) return;
929
3623
  // 爆炸公式
@@ -939,7 +3633,7 @@
939
3633
  value: 0 - 100 整数
940
3634
  */
941
3635
  localBomb(name, value) {
942
- let target = scene.getObjectByName(name);
3636
+ let target = this.getObjectByName(name)[0];
943
3637
  if (target) {
944
3638
  this.computedBomb(target, value);
945
3639
  }
@@ -951,6 +3645,16 @@
951
3645
  .add(new this.THREE.Vector3().copy(object.userData.position));
952
3646
  object.position.copy(bombPosition);
953
3647
  },
3648
+ setClippingPlanesConstants(clippingPlanesConstants) {
3649
+ this.clippingPlanesConstants = clippingPlanesConstants;
3650
+ },
3651
+ getClippingPlanes() {
3652
+ let clippingPlanesConstant = [];
3653
+ renderer.clippingPlanes.forEach(item => {
3654
+ clippingPlanesConstant.push(item.constant);
3655
+ });
3656
+ return clippingPlanesConstant;
3657
+ },
954
3658
  // 设置全局整体剖切
955
3659
  /*
956
3660
  先开启模型全局剖切模式, 会返回剖切值的最大最小值
@@ -961,23 +3665,56 @@
961
3665
  let max = box3.max;
962
3666
  let min = box3.min;
963
3667
  const clippingPlanes = [
964
- new this.THREE.Plane(new this.THREE.Vector3(-1, 0, 0), max.x),
965
- new this.THREE.Plane(new this.THREE.Vector3(0, -1, 0), max.y),
966
- new this.THREE.Plane(new this.THREE.Vector3(0, 0, -1), max.z),
3668
+ new this.THREE.Plane(
3669
+ new this.THREE.Vector3(-1, 0, 0),
3670
+ this.clippingPlanesConstants ? this.clippingPlanesConstants[0] : Math.ceil(max.x)
3671
+ ),
3672
+ new this.THREE.Plane(
3673
+ new this.THREE.Vector3(0, -1, 0),
3674
+ this.clippingPlanesConstants ? this.clippingPlanesConstants[1] : Math.ceil(max.y)
3675
+ ),
3676
+ new this.THREE.Plane(
3677
+ new this.THREE.Vector3(0, 0, -1),
3678
+ this.clippingPlanesConstants ? this.clippingPlanesConstants[2] : Math.ceil(max.z)
3679
+ ),
3680
+ new this.THREE.Plane(
3681
+ new this.THREE.Vector3(1, 0, 0),
3682
+ this.clippingPlanesConstants ? this.clippingPlanesConstants[3] : Math.ceil(-min.x)
3683
+ ),
3684
+ new this.THREE.Plane(
3685
+ new this.THREE.Vector3(0, 1, 0),
3686
+ this.clippingPlanesConstants ? this.clippingPlanesConstants[4] : Math.ceil(-min.y)
3687
+ ),
3688
+ new this.THREE.Plane(
3689
+ new this.THREE.Vector3(0, 0, 1),
3690
+ this.clippingPlanesConstants ? this.clippingPlanesConstants[5] : Math.ceil(-min.z)
3691
+ ),
967
3692
  ];
3693
+
968
3694
  renderer.clippingPlanes = clippingPlanes;
969
3695
  if (flag) {
3696
+ // 全局剖切的初始值
970
3697
  guiParams = {
971
3698
  x轴: Math.ceil(clippingPlanes[0].constant),
972
3699
  y轴: Math.ceil(clippingPlanes[2].constant),
973
3700
  z轴: Math.ceil(clippingPlanes[1].constant),
3701
+ '-x轴': -Math.ceil(clippingPlanes[3].constant),
3702
+ '-y轴': -Math.ceil(clippingPlanes[5].constant),
3703
+ '-z轴': -Math.ceil(clippingPlanes[4].constant),
974
3704
  };
975
3705
  this.addClippingGui(
976
3706
  '全局剖切',
977
3707
  {
978
- x: min.x,
979
- y: min.z,
980
- z: min.y,
3708
+ max: {
3709
+ x: max.x,
3710
+ y: max.z,
3711
+ z: max.y,
3712
+ },
3713
+ min: {
3714
+ x: min.x,
3715
+ y: min.z,
3716
+ z: min.y,
3717
+ },
981
3718
  },
982
3719
  clippingPlanes
983
3720
  );
@@ -1009,7 +3746,11 @@
1009
3746
  x轴: 0,
1010
3747
  y轴: 0,
1011
3748
  z轴: 0,
3749
+ '-x轴': 0,
3750
+ '-y轴': 0,
3751
+ '-z轴': 0,
1012
3752
  };
3753
+ this.clippingPlanesConstants = null;
1013
3754
  renderer.clippingPlanes = Object.freeze([]);
1014
3755
  gui && gui.destroy();
1015
3756
  },
@@ -1025,6 +3766,9 @@
1025
3766
  new this.THREE.Plane(new this.THREE.Vector3(-1, 0, 0), Math.ceil(boundingBox.max.x)),
1026
3767
  new this.THREE.Plane(new this.THREE.Vector3(0, -1, 0), Math.ceil(boundingBox.max.y)),
1027
3768
  new this.THREE.Plane(new this.THREE.Vector3(0, 0, -1), Math.ceil(boundingBox.max.z)),
3769
+ new this.THREE.Plane(new this.THREE.Vector3(1, 0, 0), -Math.floor(boundingBox.min.x)),
3770
+ new this.THREE.Plane(new this.THREE.Vector3(0, 1, 0), -Math.floor(boundingBox.min.y)),
3771
+ new this.THREE.Plane(new this.THREE.Vector3(0, 0, 1), -Math.floor(boundingBox.min.z)),
1028
3772
  ];
1029
3773
  obj.material.clippingPlanes = clippingPlanes;
1030
3774
  obj.material.needsUpdate = true;
@@ -1034,6 +3778,9 @@
1034
3778
  x轴: clippingPlanes[0].constant,
1035
3779
  y轴: clippingPlanes[2].constant,
1036
3780
  z轴: clippingPlanes[1].constant,
3781
+ '-x轴': clippingPlanes[3].constant,
3782
+ '-y轴': clippingPlanes[5].constant,
3783
+ '-z轴': clippingPlanes[4].constant,
1037
3784
  };
1038
3785
  this.addClippingGui(
1039
3786
  '局部剖切',
@@ -1075,6 +3822,9 @@
1075
3822
  x轴: 0,
1076
3823
  y轴: 0,
1077
3824
  z轴: 0,
3825
+ '-x轴': 0,
3826
+ '-y轴': 0,
3827
+ '-z轴': 0,
1078
3828
  };
1079
3829
  gui && gui.destroy();
1080
3830
  clippingMesh.forEach(item => {
@@ -1084,39 +3834,68 @@
1084
3834
  clippingMesh.splice(0);
1085
3835
  },
1086
3836
  // 添加剖切轴工具
1087
- addClippingGui(title, minValue, objClipp1, objClipp2) {
3837
+ addClippingGui(title, boudingValue, objClipp1, objClipp2) {
1088
3838
  gui = new GUI({
1089
3839
  title: title,
1090
3840
  });
1091
3841
  gui
1092
3842
  .add(guiParams, 'x轴')
1093
- .step(0.01)
1094
- .min(minValue.x)
1095
- .max(guiParams['x轴'])
3843
+ .step(0.001)
3844
+ .min(boudingValue.min.x)
3845
+ .max(boudingValue.max.x)
1096
3846
  .onChange(d => {
1097
3847
  objClipp1[0].constant = d;
1098
3848
  objClipp2 && (objClipp2[0].constant = d);
1099
3849
  });
3850
+ gui
3851
+ .add(guiParams, '-x轴')
3852
+ .step(0.001)
3853
+ .min(boudingValue.min.x)
3854
+ .max(boudingValue.max.x)
3855
+ .onChange(d => {
3856
+ objClipp1[3].constant = -d;
3857
+ objClipp2 && (objClipp2[3].constant = -d);
3858
+ });
1100
3859
  gui
1101
3860
  .add(guiParams, 'y轴')
1102
- .step(0.01)
1103
- .min(minValue.y)
1104
- .max(guiParams['y轴'])
3861
+ .step(0.001)
3862
+ .min(boudingValue.min.y)
3863
+ .max(boudingValue.max.y)
1105
3864
  .onChange(d => {
1106
3865
  objClipp1[2].constant = d;
1107
3866
  objClipp2 && (objClipp2[2].constant = d);
1108
3867
  });
3868
+ gui
3869
+ .add(guiParams, '-y轴')
3870
+ .step(0.001)
3871
+ .min(boudingValue.min.y)
3872
+ .max(boudingValue.max.y)
3873
+ .onChange(d => {
3874
+ objClipp1[5].constant = -d;
3875
+ objClipp2 && (objClipp2[5].constant = -d);
3876
+ });
1109
3877
  gui
1110
3878
  .add(guiParams, 'z轴')
1111
- .step(0.01)
1112
- .min(minValue.z)
1113
- .max(guiParams['z轴'])
3879
+ .step(0.001)
3880
+ .min(boudingValue.min.z)
3881
+ .max(boudingValue.max.z)
1114
3882
  .onChange(d => {
1115
3883
  objClipp1[1].constant = d;
3884
+ objClipp2 && (objClipp2[1].constant = d);
3885
+ });
3886
+ gui
3887
+ .add(guiParams, '-z轴')
3888
+ .step(0.001)
3889
+ .min(boudingValue.min.z)
3890
+ .max(boudingValue.max.z)
3891
+ .onChange(d => {
3892
+ objClipp1[4].constant = -d;
3893
+ objClipp2 && (objClipp2[4].constant = -d);
1116
3894
  });
1117
3895
  },
1118
3896
  // 开启第一视角
1119
- startFirstPer({ moveSpeed = 200, jumpSpeed = 200 }) {
3897
+ startFirstPer(options) {
3898
+ let { moveSpeed = 200, jumpSpeed = 200 } = options || {};
1120
3899
  removeSpeed = moveSpeed;
1121
3900
  upSpeed = jumpSpeed;
1122
3901
 
@@ -1136,8 +3915,22 @@
1136
3915
  initPointerLock() {
1137
3916
  this.home();
1138
3917
  setTimeout(() => {
3918
+ // 开启第一视角时,将相机与水平面保持水平
3919
+ camera.rotation.x = 0;
3920
+ camera.rotation.z = 0;
3921
+
1139
3922
  pointControls = new PointerLockControls(camera, renderer.domElement);
1140
3923
  pointControls.lock();
3924
+ if (!this._onFirstPersonChange) {
3925
+ this._onFirstPersonChange = () => {
3926
+ if (typeof this._cameraChangeObserver === 'function') {
3927
+ this._cameraChangeObserver('firstPerson');
3928
+ }
3929
+ };
3930
+ }
3931
+ try {
3932
+ pointControls.addEventListener('change', this._onFirstPersonChange);
3933
+ } catch (e) {}
1141
3934
  // 锁定
1142
3935
  pointControls.addEventListener('lock', () => {
1143
3936
  cameraControls.enabled = false;
@@ -1145,14 +3938,24 @@
1145
3938
  window.addEventListener('keyup', this.onKeyUp, false);
1146
3939
  renderer.domElement.removeEventListener('mouseup', this.mouseClick, false);
1147
3940
  renderer.domElement.removeEventListener('mousedown', this.mouseDown, false);
3941
+ if (typeof this._cameraChangeObserver === 'function') {
3942
+ this._cameraChangeObserver('firstPersonLock');
3943
+ }
1148
3944
  });
1149
3945
  // 解锁
1150
3946
  pointControls.addEventListener('unlock', () => {
1151
3947
  firstPerSign = false;
1152
3948
  cameraControls.enabled = true;
1153
3949
  // 返回初始视角
1154
- cameraControls.reset(true);
1155
- cameraControls.update(0);
3950
+ this.home();
3951
+ try {
3952
+ if (this._onFirstPersonChange && pointControls) {
3953
+ pointControls.removeEventListener('change', this._onFirstPersonChange);
3954
+ }
3955
+ } catch (e) {}
3956
+ if (this.noObserver) {
3957
+ this.noObserver._firstPersonLastPos = null;
3958
+ }
1156
3959
  setTimeout(() => {
1157
3960
  window.removeEventListener('keydown', this.onKeyDown);
1158
3961
  window.removeEventListener('keyup', this.onKeyUp);
@@ -1160,8 +3963,11 @@
1160
3963
  renderer.domElement.addEventListener('mousedown', this.mouseDown, false);
1161
3964
  // this.timeRender()
1162
3965
  }, 0);
3966
+ if (typeof this._cameraChangeObserver === 'function') {
3967
+ this._cameraChangeObserver('firstPersonUnlock');
3968
+ }
1163
3969
  });
1164
- scene.add(pointControls.object);
3970
+ if (scene) scene.add(pointControls.object);
1165
3971
  }, 10);
1166
3972
  },
1167
3973
  // 第一视角运动
@@ -1188,7 +3994,8 @@
1188
3994
  // 获取相机靠下5的位置
1189
3995
  downRaycaster.ray.origin.y += 5;
1190
3996
  // 判断是否停留在了立方体上面
1191
- let intersections = downRaycaster.intersectObjects(scene.children, true);
3997
+ let intersections =
3998
+ scene && scene.children ? downRaycaster.intersectObjects(scene.children, true) : [];
1192
3999
  var onObject = intersections.length > 0;
1193
4000
  if (onObject === true) {
1194
4001
  velocity.y = Math.max(0, velocity.y);
@@ -1204,6 +4011,21 @@
1204
4011
  control.position.y = 3 - 0 / 10;
1205
4012
  canJump = true;
1206
4013
  }
4014
+ if (this.noObserver) {
4015
+ if (!this.noObserver._firstPersonLastPos) {
4016
+ this.noObserver._firstPersonLastPos = new this.THREE.Vector3().copy(control.position);
4017
+ }
4018
+ const moved =
4019
+ this.noObserver._firstPersonLastPos.distanceToSquared(control.position) > 1e-8;
4020
+ if (moved) {
4021
+ this.noObserver._firstPersonLastPos.copy(control.position);
4022
+ if (typeof this._cameraChangeObserver === 'function') {
4023
+ this._cameraChangeObserver('firstPersonMove');
4024
+ }
4025
+ }
4026
+ } else if (typeof this._cameraChangeObserver === 'function') {
4027
+ this._cameraChangeObserver('firstPersonMove');
4028
+ }
1207
4029
  }
1208
4030
  },
1209
4031
  // 键盘监听事件
@@ -1270,26 +4092,15 @@
1270
4092
  },
1271
4093
  // 返回主视角/恢复相机初始状态
1272
4094
  home() {
4095
+ hasExecutedCentering = true;
4096
+
1273
4097
  if (roaming) {
1274
4098
  this.endRoam();
1275
4099
  }
1276
- // cameraControls.reset(true);
1277
- // cameraControls.update(0);
1278
- // this.timeRender()
1279
- const box = new this.THREE.Box3();
1280
- scene.traverseVisible(function (object) {
1281
- // 3. 只处理有几何体的网格 (Mesh)
1282
- if (object.isMesh) {
1283
- // 4. 使用 expandByObject 扩展包围盒
1284
- // 这个方法会计算 object 的世界坐标包围盒,并将其合并到 correctBoundingBox 中
1285
- box.expandByObject(object);
1286
- }
1287
- });
1288
4100
 
1289
- // const box = new this.THREE.Box3().setFromObject(scene);
1290
- const center = box.getCenter(new this.THREE.Vector3());
1291
- const size = box.getSize(new this.THREE.Vector3());
1292
- const maxDim = Math.max(size.x, size.y, size.z);
4101
+ const center = sceneBoundingBox.getCenter(new this.THREE.Vector3());
4102
+ const size = sceneBoundingBox.getSize(new this.THREE.Vector3());
4103
+ const maxDim = Math.max(size.x, size.y, size.z) * 0.4;
1293
4104
 
1294
4105
  this.cameraLocation({
1295
4106
  x: center.x,
@@ -1299,6 +4110,10 @@
1299
4110
  pitch: center.y,
1300
4111
  roll: center.z,
1301
4112
  });
4113
+ this.setCameraConfig();
4114
+ // cameraControls.reset(true);
4115
+ // cameraControls.update(0);
4116
+ // this.timeRender()
1302
4117
  },
1303
4118
  // 测量
1304
4119
  /*
@@ -1425,6 +4240,7 @@
1425
4240
  参数: object, 目标实体,
1426
4241
  */
1427
4242
  isolate(object) {
4243
+ if (!scene) return;
1428
4244
  // 隔离 将目标实体以外的实体隐藏掉
1429
4245
  scene.traverse(item => {
1430
4246
  if (item.isMesh && item.name !== object.name) {
@@ -1438,6 +4254,7 @@
1438
4254
  },
1439
4255
  // 还原操作 将修改过的实体进行恢复
1440
4256
  restore() {
4257
+ if (!scene) return;
1441
4258
  scene.traverse(item => {
1442
4259
  if (item.isMesh) {
1443
4260
  item.setColorAt(item.userData.instanceIndex, item.material.userData.nColor);
@@ -1485,7 +4302,7 @@
1485
4302
  // 不参与裁剪
1486
4303
  locationModel.userData.cull = false;
1487
4304
  locationModel.name = name;
1488
- scene.add(locationModel);
4305
+ if (scene) scene.add(locationModel);
1489
4306
  locationModel.position.copy(new this.THREE.Vector3(position.x, position.y, position.z));
1490
4307
  if (gltf.animations.length > 0) {
1491
4308
  let actionMixer = new this.THREE.AnimationMixer(gltf.scene);
@@ -1512,7 +4329,7 @@
1512
4329
  child.material.dispose();
1513
4330
  }
1514
4331
  });
1515
- scene.remove(item);
4332
+ if (scene) scene.remove(item);
1516
4333
  });
1517
4334
  modelActions.splice(0);
1518
4335
  modelActive.splice(0);
@@ -1555,45 +4372,48 @@
1555
4372
  scenePass = null;
1556
4373
  },
1557
4374
  // 获取中心点
1558
- getCenter(instancedMesh, tempMatrix) {
1559
- // 1. 获取原始几何体的中心(局部坐标,相对于单个 mesh 原点)
1560
- const geometry = instancedMesh.geometry;
1561
- if (!geometry.boundingBox) {
1562
- geometry.computeBoundingBox();
1563
- }
1564
- const localCenter = geometry.boundingBox.getCenter(new this.THREE.Vector3());
1565
-
1566
- // 2. 将中心应用实例的局部变换(tempMatrix)
1567
- const instanceLocalCenter = localCenter.clone().applyMatrix4(tempMatrix);
1568
-
1569
- // 3. 转换到世界坐标
1570
- const worldCenter = instancedMesh.localToWorld(instanceLocalCenter.clone());
1571
-
1572
- return worldCenter;
1573
- let center = new this.THREE.Vector3();
1574
- if(tempMatrix){
1575
- obj.applyMatrix4(tempMatrix)
1576
- obj.updateMatrixWorld();
4375
+ getCenter(obj, isBox3Info) {
4376
+ let box3;
4377
+ if (isBox3Info) {
4378
+ box3 = new this.THREE.Box3(
4379
+ new this.THREE.Vector3(obj.min[0], obj.min[1], obj.min[2]),
4380
+ new this.THREE.Vector3(obj.max[0], obj.max[1], obj.max[2])
4381
+ );
1577
4382
  }
1578
- obj.boundingBox.getCenter(center);
1579
- if (obj.userData.is3D) {
1580
- center.applyMatrix4(mat4);
1581
- obj.updateMatrixWorld();
4383
+ let center = new this.THREE.Vector3();
4384
+ (box3 && box3.getCenter(center)) || obj.boundingBox.getCenter(center);
4385
+ if (isBox3Info || obj.userData.is3D) {
4386
+ center.applyMatrix4(bizToThreeMatrix);
1582
4387
  }
1583
4388
  return center;
1584
4389
  },
1585
- getSize(obj, tempMatrix) {
4390
+ getSize(obj, isBox3Info) {
4391
+ let box3;
4392
+ if (isBox3Info) {
4393
+ box3 = new this.THREE.Box3(
4394
+ new this.THREE.Vector3(obj.min[0], obj.min[1], obj.min[2]),
4395
+ new this.THREE.Vector3(obj.max[0], obj.max[1], obj.max[2])
4396
+ );
4397
+ }
1586
4398
  let size = new this.THREE.Vector3();
1587
- obj.boundingBox.getSize(size);
1588
- if (obj.userData.is3D) {
1589
- size.applyMatrix4(mat4);
4399
+ (box3 && box3.getSize(size)) || obj.boundingBox.getSize(size);
4400
+ if (isBox3Info || obj.userData.is3D) {
4401
+ size.applyMatrix4(bizToThreeMatrix);
1590
4402
  }
1591
4403
  return size;
1592
4404
  },
1593
4405
  animate() {
4406
+ if (isDebug) {
4407
+ stats && stats.begin(); // 开始帧率统计
4408
+ }
4409
+
1594
4410
  const delta = fpsClock.getDelta();
1595
4411
  timeStamp += delta;
1596
4412
  animateId = requestAnimationFrame(this.animate);
4413
+
4414
+ // 1. 先重置计数器(关键!)
4415
+ renderer.info.reset(); // 重置上一帧的统计数据
4416
+
1597
4417
  if (timeStamp > singleFrameTime) {
1598
4418
  if (modelActions.length > 0) {
1599
4419
  modelActions.forEach(item => {
@@ -1601,16 +4421,112 @@
1601
4421
  });
1602
4422
  }
1603
4423
  cameraControls.enabled && cameraControls.update(timeStamp);
4424
+ // 计算相机近裁面到包围盒当前面的距离
1604
4425
  this.cameraTrack();
1605
4426
  this.firstPerspective();
1606
4427
  if (scenePass) {
1607
4428
  let d = sceneClock.getDelta();
1608
4429
  scenePass.uniforms['iTime'].value += d;
1609
4430
  }
1610
- labelRenderer.render(scene, camera);
1611
- renderer.render(scene, camera);
4431
+ labelRenderer.render(scene, camera);
4432
+ renderedThisFrame.clear();
4433
+
4434
+ // 增强的渲染中断逻辑:在用户交互期间强制跳过渲染
4435
+ const loadingState = this.noObserver
4436
+ ? this.noObserver.batchLoadingState
4437
+ : this.batchLoadingState;
4438
+ const shouldSkipRendering =
4439
+ skipNextRenderFrame ||
4440
+ forceSkipRendering ||
4441
+ loadingState.interactionState.isInteracting;
4442
+
4443
+ if (shouldSkipRendering) {
4444
+ // 重置单次跳过标记
4445
+ if (skipNextRenderFrame) {
4446
+ skipNextRenderFrame = false;
4447
+ }
4448
+
4449
+ // 统计跳过的帧数
4450
+ // interactionFrameCount++;
4451
+ // 在交互期间,每隔几帧强制渲染一次以保持基本的视觉反馈
4452
+ // if (interactionFrameCount % 2 === 0 && this.batchLoadingState.interactionState.isInteracting) {
4453
+ renderer.clear(true, true, true);
4454
+ renderer.render(scene, camera);
4455
+ // }
4456
+ // 可选:添加调试信息(生产环境可注释掉)
4457
+ // console.log(`跳过渲染帧 #${interactionFrameCount}, 交互类型: ${this.batchLoadingState.interactionState.interactionType}`);
4458
+ } else {
4459
+ // 正常渲染
4460
+ if (outlineComposer) {
4461
+ // 使用后处理管线渲染,避免重复渲染
4462
+ outlineComposer.render();
4463
+ } else {
4464
+ renderer.clear(true, true, true);
4465
+ renderer.render(scene, camera);
4466
+ }
4467
+ // 重置交互帧计数
4468
+ // if (interactionFrameCount > 0) {
4469
+ // // console.log(`交互结束,总共跳过 ${interactionFrameCount} 帧`);
4470
+ // interactionFrameCount = 0;
4471
+ // }
4472
+ }
4473
+
4474
+ // 3. 按频率输出(避免每帧都log)
4475
+ // if (frameCounter++ % LOG_INTERVAL === 0) {
4476
+ // // 统计当前视椎体中的模型数量(按组/子节点统计)以及 InstancedMesh 数量
4477
+ // let modelsInFrustum = 0;
4478
+ // let instancedMeshesInFrustum = 0;
4479
+ // try {
4480
+ // const frustum = new this.THREE.Frustum();
4481
+ // const vpMatrix = new this.THREE.Matrix4().multiplyMatrices(
4482
+ // camera.projectionMatrix,
4483
+ // camera.matrixWorldInverse
4484
+ // );
4485
+ // frustum.setFromProjectionMatrix(vpMatrix);
4486
+
4487
+ // // 优先使用 modelGroup 的子节点作为“模型”统计单位;否则回退到 scene.children
4488
+ // const children = (typeof modelGroup !== 'undefined' && modelGroup && modelGroup.children && modelGroup.children.length)
4489
+ // ? modelGroup.children
4490
+ // : scene.children;
4491
+
4492
+ // for (let i = 0; i < children.length; i++) {
4493
+ // const obj = children[i];
4494
+ // if (!obj || !obj.visible) continue;
4495
+ // // 以对象的包围盒为准,判断是否与视椎体相交
4496
+ // const box = new this.THREE.Box3().setFromObject(obj);
4497
+ // if (box.isEmpty()) continue;
4498
+ // if (frustum.intersectsBox(box)) {
4499
+ // modelsInFrustum++;
4500
+ // // 统计组内的 InstancedMesh 数量(处于视椎体内)
4501
+ // obj.traverse(child => {
4502
+ // if (!child || !child.visible || !child.isInstancedMesh) return;
4503
+ // const childBox = new this.THREE.Box3().setFromObject(child);
4504
+ // if (!childBox.isEmpty() && frustum.intersectsBox(childBox)) {
4505
+ // instancedMeshesInFrustum++;
4506
+ // }
4507
+ // });
4508
+ // }
4509
+ // }
4510
+ // } catch (e) {
4511
+ // // 统计过程不影响主流程,出现异常时仅忽略
4512
+ // }
4513
+
4514
+ // if ((perfLogFrameCount++ % 120 === 0) || (Date.now() - lastPerfLogTime >= 3000)) {
4515
+ // lastPerfLogTime = Date.now();
4516
+ // console.log(`frame info`, {
4517
+ // drawCalls: renderer.info.render.calls,
4518
+ // triangles: renderer.info.render.triangles,
4519
+ // textures: renderer.info.memory.textures,
4520
+ // });
4521
+ // }
4522
+ // }
4523
+
4524
+ // renderer.setRenderTarget(null)
1612
4525
  timeStamp = timeStamp % singleFrameTime;
1613
- outlineComposer && outlineComposer.render();
4526
+ }
4527
+
4528
+ if (isDebug) {
4529
+ stats && stats.end(); // 结束帧率统计
1614
4530
  }
1615
4531
  },
1616
4532
  // 暴露个别参数让业务自己做特殊业务。
@@ -1623,6 +4539,14 @@
1623
4539
  scene,
1624
4540
  };
1625
4541
  },
4542
+
4543
+ /**
4544
+ * 重置性能统计
4545
+ */
4546
+ // resetPerformanceStats() {
4547
+ // // 性能统计已移除,保留方法以兼容现有调用
4548
+ // interactionFrameCount = 0;
4549
+ // },
1626
4550
  setMouseAction(btn) {
1627
4551
  const ACTION_ENUM = {
1628
4552
  none: 0,
@@ -1635,18 +4559,603 @@
1635
4559
  right && (cameraControls.mouseButtons.right = ACTION_ENUM[right]);
1636
4560
  middle && (cameraControls.mouseButtons.middle = ACTION_ENUM[middle]);
1637
4561
  },
1638
- // 动态设置视角滚轮的距离
1639
- setCameraConfig() {
1640
- let box3 = new this.THREE.Box3().setFromObject(scene);
1641
- let size = new this.THREE.Vector3();
1642
- box3.getSize(size);
1643
- const maxBorder = Math.max(size.x, size.y, size.z);
1644
4562
 
1645
- // cameraControls.camera.far = maxBorder * 50; // 设置相机的远裁剪面
4563
+ // 分帧加载相关方法
4564
+ /**
4565
+ * 启动分帧批量加载
4566
+ * @param {Object} data - 模型数据
4567
+ * @param {string} color - 初始化模型的颜色
4568
+ * @param {Object} meshNameConfig - 网格名称配置
4569
+ * @param {Object} options - 选项
4570
+ * @param {Function} onProgress - 进度回调函数
4571
+ * @param {Function} onComplete - 完成回调函数
4572
+ */
4573
+ startBatchLoading(
4574
+ data,
4575
+ color = '',
4576
+ meshNameConfig = {},
4577
+ options = {},
4578
+ onProgress = null,
4579
+ onComplete = null
4580
+ ) {
4581
+ const loadingState = this.noObserver
4582
+ ? this.noObserver.batchLoadingState
4583
+ : this.batchLoadingState;
4584
+ // 如果已经在加载中,先停止之前的加载
4585
+ if (loadingState.isLoading) {
4586
+ this.stopBatchLoading();
4587
+ }
4588
+
4589
+ // 重置instance-parser的处理状态
4590
+ resetProcessingState();
1646
4591
 
1647
- cameraControls.minDistance = maxBorder * 0.05; // 动态设置视角滚轮的距离
4592
+ // 解析数据
4593
+ let parsedData = parseData(data, options);
4594
+ let instances = parsedData.instances;
4595
+ let drawObjs = parsedData.drawObjs;
1648
4596
 
1649
- camera.updateProjectionMatrix();
4597
+ if (instances.length === 0) {
4598
+ onComplete && onComplete();
4599
+ return;
4600
+ }
4601
+
4602
+ // 去重处理
4603
+ // const existingDrawObjectIds = new Set();
4604
+ // if (modelGroup && modelGroup.children && modelGroup.children.length) {
4605
+ // modelGroup.children.forEach(child => {
4606
+ // if (child && child.userData && child.userData.isInstancedMeshGroup && child.name) {
4607
+ // existingDrawObjectIds.add(child.name);
4608
+ // }
4609
+ // });
4610
+ // }
4611
+
4612
+ const filteredDrawObjs = drawObjs;
4613
+ const filteredInstances = instances;
4614
+
4615
+ // 初始化加载状态
4616
+ // 此时直接修改 nonReactiveState,不需要展开 ...this.batchLoadingState (因为响应式对象里只有基本字段)
4617
+ loadingState.isLoading = true;
4618
+ loadingState.currentBatch = 0;
4619
+ loadingState.totalCount = filteredInstances.length;
4620
+ loadingState.loadedCount = 0;
4621
+ loadingState.pendingData = this.createBatches(filteredInstances, filteredDrawObjs);
4622
+ loadingState.onProgress = onProgress;
4623
+ loadingState.onComplete = onComplete;
4624
+
4625
+ loadingState.totalBatches = loadingState.pendingData.length;
4626
+
4627
+ // 存储其他参数
4628
+ loadingState.color = color;
4629
+ loadingState.meshNameConfig = meshNameConfig;
4630
+ loadingState.options = options;
4631
+
4632
+ // 同步更新响应式状态中的关键字段,以便UI显示
4633
+ this.batchLoadingState.isLoading = true;
4634
+ this.batchLoadingState.currentBatch = 0;
4635
+ this.batchLoadingState.totalBatches = loadingState.totalBatches;
4636
+ this.batchLoadingState.loadedCount = 0;
4637
+ this.batchLoadingState.totalCount = loadingState.totalCount;
4638
+
4639
+ // 开始加载第一批
4640
+ this.loadNextBatch();
4641
+ },
4642
+
4643
+ /**
4644
+ * 创建批次数据
4645
+ * @param {Array} instances - 实例数组
4646
+ * @param {Array} drawObjs - 绘制对象数组
4647
+ * @returns {Array} 批次数组
4648
+ */
4649
+ createBatches(instances, drawObjs) {
4650
+ const batches = [];
4651
+ const loadingState = this.noObserver
4652
+ ? this.noObserver.batchLoadingState
4653
+ : this.batchLoadingState;
4654
+ const batchSize = loadingState.batchSize;
4655
+
4656
+ // 按drawObject分组
4657
+ const drawObjMap = new Map();
4658
+ drawObjs.forEach(obj => {
4659
+ drawObjMap.set(obj.drawObjId, obj);
4660
+ });
4661
+
4662
+ // 按drawObject ID分组instances
4663
+ const instancesByDrawObj = new Map();
4664
+ instances.forEach(inst => {
4665
+ if (!instancesByDrawObj.has(inst.drawObject)) {
4666
+ instancesByDrawObj.set(inst.drawObject, []);
4667
+ }
4668
+ instancesByDrawObj.get(inst.drawObject).push(inst);
4669
+ });
4670
+
4671
+ // 创建批次
4672
+ let currentBatch = [];
4673
+ let currentBatchSize = 0;
4674
+
4675
+ for (const [drawObjId, objInstances] of instancesByDrawObj) {
4676
+ let drawObj = drawObjMap.get(drawObjId);
4677
+
4678
+ // 如果通过 ID 直接找不到,或者需要验证 prmid 与 drawObjId 的对应关系
4679
+ if (!drawObj) {
4680
+ for (const value of drawObjMap.values()) {
4681
+ if (value && Array.isArray(value.geoms)) {
4682
+ const found = value.geoms.some(g => g.prmid === drawObjId);
4683
+ if (found) {
4684
+ drawObj = value;
4685
+ break;
4686
+ }
4687
+ }
4688
+ }
4689
+ }
4690
+
4691
+ if (!drawObj) continue;
4692
+
4693
+ // 如果当前批次加上这个drawObject会超过限制,先保存当前批次
4694
+ if (currentBatchSize + objInstances.length > batchSize && currentBatch.length > 0) {
4695
+ batches.push({
4696
+ instances: currentBatch.flatMap(item => item.instances),
4697
+ drawObjs: currentBatch.map(item => item.drawObj),
4698
+ });
4699
+ currentBatch = [];
4700
+ currentBatchSize = 0;
4701
+ }
4702
+
4703
+ currentBatch.push({
4704
+ drawObj,
4705
+ instances: objInstances,
4706
+ });
4707
+ currentBatchSize += objInstances.length;
4708
+
4709
+ // 如果当前批次已满,保存并开始新批次
4710
+ if (currentBatchSize >= batchSize) {
4711
+ batches.push({
4712
+ instances: currentBatch.flatMap(item => item.instances),
4713
+ drawObjs: currentBatch.map(item => item.drawObj),
4714
+ });
4715
+ currentBatch = [];
4716
+ currentBatchSize = 0;
4717
+ }
4718
+ }
4719
+
4720
+ // 保存最后一个批次
4721
+ if (currentBatch.length > 0) {
4722
+ batches.push({
4723
+ instances: currentBatch.flatMap(item => item.instances),
4724
+ drawObjs: currentBatch.map(item => item.drawObj),
4725
+ });
4726
+ }
4727
+
4728
+ return batches;
4729
+ },
4730
+
4731
+ /**
4732
+ * 加载下一批数据
4733
+ */
4734
+ async loadNextBatch() {
4735
+ const loadingState = this.noObserver
4736
+ ? this.noObserver.batchLoadingState
4737
+ : this.batchLoadingState;
4738
+ if (
4739
+ !loadingState.isLoading ||
4740
+ loadingState.currentBatch >= loadingState.pendingData.length
4741
+ ) {
4742
+ this.completeBatchLoading();
4743
+ return;
4744
+ }
4745
+
4746
+ // 检查是否需要暂停渲染
4747
+ if (loadingState.isPaused) {
4748
+ // 如果暂停,延迟一段时间后再次检查
4749
+ loadingState.animationFrameId = requestAnimationFrame(() => {
4750
+ this.loadNextBatch();
4751
+ });
4752
+ return;
4753
+ }
4754
+
4755
+ // 检查是否有用户交互正在进行,如果有则暂停批次加载
4756
+ if (loadingState.interactionState.isInteracting) {
4757
+ // 如果正在交互,延迟一段时间后再次检查
4758
+ loadingState.animationFrameId = requestAnimationFrame(() => {
4759
+ this.loadNextBatch();
4760
+ });
4761
+ return;
4762
+ }
4763
+
4764
+ const batch = loadingState.pendingData[loadingState.currentBatch];
4765
+
4766
+ try {
4767
+ console.log('加载批次:', loadingState.currentBatch);
4768
+ await this.processWithMainThread(batch);
4769
+
4770
+ // 更新进度
4771
+ loadingState.loadedCount += batch.instances.length;
4772
+ loadingState.currentBatch++;
4773
+
4774
+ // 同步到响应式状态
4775
+ this.batchLoadingState.loadedCount = loadingState.loadedCount;
4776
+ this.batchLoadingState.currentBatch = loadingState.currentBatch;
4777
+
4778
+ // 调用进度回调
4779
+ if (loadingState.onProgress) {
4780
+ loadingState.onProgress({
4781
+ loaded: loadingState.loadedCount,
4782
+ total: loadingState.totalCount,
4783
+ currentBatch: loadingState.currentBatch,
4784
+ totalBatches: loadingState.totalBatches,
4785
+ });
4786
+ }
4787
+
4788
+ // 使用 requestAnimationFrame 在下一帧继续加载
4789
+ loadingState.animationFrameId = requestAnimationFrame(() => {
4790
+ this.loadNextBatch();
4791
+ });
4792
+ } catch (error) {
4793
+ console.error('批次处理失败:', error);
4794
+
4795
+ // 继续下一批次
4796
+ loadingState.animationFrameId = requestAnimationFrame(() => {
4797
+ this.loadNextBatch();
4798
+ });
4799
+ }
4800
+ },
4801
+
4802
+ /**
4803
+ * 使用主线程处理批次数据
4804
+ */
4805
+ async processWithMainThread(batch) {
4806
+ const loadingState = this.noObserver
4807
+ ? this.noObserver.batchLoadingState
4808
+ : this.batchLoadingState;
4809
+ loadingState.options.batchSize = loadingState.batchSize;
4810
+ loadingState.options.resetState = loadingState.currentBatch === 0;
4811
+ // 使用原始的handleInstancedMeshModel方法
4812
+
4813
+ isDebug && performance.mark('handleInstancedMeshModel-start');
4814
+ await handleInstancedMeshModel(
4815
+ modelGroup,
4816
+ batch.instances,
4817
+ batch.drawObjs,
4818
+ '',
4819
+ scene,
4820
+ loadingState.color,
4821
+ loadingState.meshNameConfig,
4822
+ '',
4823
+ loadingState.options
4824
+ );
4825
+ isDebug && performance.mark('handleInstancedMeshModel-end');
4826
+ isDebug && performance.measure('handleInstancedMeshModel', 'handleInstancedMeshModel-start', 'handleInstancedMeshModel-end');
4827
+ },
4828
+
4829
+ /**
4830
+ * 完成批量加载
4831
+ */
4832
+ completeBatchLoading() {
4833
+ const loadingState = this.noObserver
4834
+ ? this.noObserver.batchLoadingState
4835
+ : this.batchLoadingState;
4836
+ if (!loadingState.isLoading) return;
4837
+
4838
+ // if (modelGroup) {
4839
+ // if (firstDraw) {
4840
+ // if (scene) scene.add(modelGroup);
4841
+ // firstDraw = false;
4842
+ // }
4843
+ // if (!rotatedSceneFlag) {
4844
+ // rotatedSceneFlag = true;
4845
+ // }
4846
+ if (modelGroup) {
4847
+ if (!modelGroup.userData.initDone) {
4848
+ scene.add(modelGroup);
4849
+ modelGroup.applyMatrix4(bizToThreeMatrix);
4850
+ modelGroup.updateMatrixWorld();
4851
+ this.smartModelCenter(sceneBoundingBox);
4852
+ this.setCameraConfig();
4853
+ modelGroup.userData.initDone = true;
4854
+ }
4855
+ modelGroup.updateMatrixWorld();
4856
+ let modelBox3 = new this.THREE.Box3();
4857
+ modelBox3.expandByObject(modelGroup);
4858
+ let modelWorldPs = new this.THREE.Vector3()
4859
+ .addVectors(modelBox3.max, modelBox3.min)
4860
+ .multiplyScalar(0.5);
4861
+ modelGroup.userData.modelWorldPs = modelWorldPs;
4862
+ modelGroup.traverse(child => {
4863
+ if (child.isMesh && !child.userData.batchInitDone) {
4864
+ markRendered(child);
4865
+ const json = this.getMeshCenterAndVolume(child);
4866
+ let meshBox3 = new this.THREE.Box3();
4867
+ meshBox3.setFromObject(child);
4868
+ let worldPs = new this.THREE.Vector3()
4869
+ .addVectors(meshBox3.max, meshBox3.min)
4870
+ .multiplyScalar(0.5);
4871
+ if (isNaN(worldPs.x)) return;
4872
+ child.worldDir = new this.THREE.Vector3().subVectors(worldPs, modelWorldPs).normalize();
4873
+ child.userData.center = json.center;
4874
+ child.userData.worldPs = worldPs;
4875
+ child.userData.oldPs = child.getWorldPosition(new this.THREE.Vector3());
4876
+ child.userData.box = json.box;
4877
+ child.userData.position = new this.THREE.Vector3().copy(child.position);
4878
+ child.userData.translate = { x: 0, y: 0, z: 0 };
4879
+ child.userData.rotate = { x: 0, y: 0, z: 0 };
4880
+ child.userData.modelWorldPs = modelWorldPs;
4881
+ if (loadingState.options && loadingState.options.userData) {
4882
+ for (const key in loadingState.options.userData) {
4883
+ child.userData[key] = loadingState.options.userData[key];
4884
+ }
4885
+ }
4886
+ child.userData.batchInitDone = true;
4887
+ }
4888
+ });
4889
+ }
4890
+ // }
4891
+
4892
+ // 重置加载状态
4893
+ loadingState.isLoading = false;
4894
+ loadingState.animationFrameId = null;
4895
+
4896
+ this.batchLoadingState.isLoading = false;
4897
+
4898
+ // 调用完成回调
4899
+ if (loadingState.onComplete) {
4900
+ loadingState.onComplete({
4901
+ totalLoaded: loadingState.loadedCount,
4902
+ totalBatches: loadingState.totalBatches,
4903
+ });
4904
+
4905
+ var axesHelper = new this.THREE.AxesHelper(10000);
4906
+ scene.add(axesHelper);
4907
+ // this.showSceneBoundingBox();
4908
+ }
4909
+
4910
+ // 触发事件
4911
+ // this.$emit('modelLoaded');
4912
+ },
4913
+
4914
+ /**
4915
+ * 停止批量加载
4916
+ */
4917
+ stopBatchLoading() {
4918
+ const loadingState = this.noObserver
4919
+ ? this.noObserver.batchLoadingState
4920
+ : this.batchLoadingState;
4921
+ if (loadingState.animationFrameId) {
4922
+ cancelAnimationFrame(loadingState.animationFrameId);
4923
+ loadingState.animationFrameId = null;
4924
+ }
4925
+ loadingState.isLoading = false;
4926
+ this.batchLoadingState.isLoading = false;
4927
+ },
4928
+
4929
+ /**
4930
+ * 修改后的drawModel方法,支持分帧加载
4931
+ */
4932
+ drawModelWithBatchLoading(
4933
+ data,
4934
+ color = '',
4935
+ meshNameConfig = {},
4936
+ options = {},
4937
+ onProgress = null,
4938
+ onComplete = null
4939
+ ) {
4940
+ if (Object.keys(data).length === 0) {
4941
+ onComplete && onComplete();
4942
+ return;
4943
+ }
4944
+
4945
+ // 如果是第一次调用drawModel且用户正在交互,标记需要在交互结束后居中
4946
+ if (modelGroups.length === 0 && userInteracting) {
4947
+ needsCenteringAfterInteraction = true;
4948
+ }
4949
+
4950
+ // 启动分帧加载
4951
+ this.startBatchLoading(data, color, meshNameConfig, options, onProgress, onComplete);
4952
+ },
4953
+
4954
+ /**
4955
+ * 统一的中断控制中心 - 设置中断状态
4956
+ * @param {boolean} active - 是否激活中断
4957
+ * @param {string} reason - 中断原因 ('wheel', 'camera', 'user_interaction' 等)
4958
+ * @param {Object} options - 配置项 { immediate: boolean }
4959
+ */
4960
+ setSystemInterruption(active, reason = 'user_interaction', options = {}) {
4961
+ const loadingState = this.noObserver
4962
+ ? this.noObserver.batchLoadingState
4963
+ : this.batchLoadingState;
4964
+
4965
+ if (active) {
4966
+ // --- 激活中断/暂停 ---
4967
+
4968
+ // 1. 更新交互状态标记
4969
+ // 如果是相机操作,更新全局标记
4970
+ if (reason === 'camera') {
4971
+ userInteracting = true;
4972
+ }
4973
+
4974
+ // 更新 batchLoadingState 中的交互状态
4975
+ loadingState.interactionState.isInteracting = true;
4976
+ loadingState.interactionState.interactionType = reason;
4977
+ loadingState.interactionState.lastInteractionTime = Date.now();
4978
+
4979
+ // 2. 暂停渲染相关
4980
+ // 立即标记暂停
4981
+ if (!loadingState.isPaused) {
4982
+ loadingState.isPaused = true;
4983
+ loadingState.pauseStartTime = Date.now();
4984
+ }
4985
+ loadingState.pauseReason = reason;
4986
+
4987
+ // 交互期间强制跳过渲染帧,提升响应速度
4988
+ forceSkipRendering = true;
4989
+ skipNextRenderFrame = true;
4990
+
4991
+ // 3. 清理之前的恢复定时器(防止在交互中途意外恢复)
4992
+ if (loadingState.resumeTimer) {
4993
+ clearTimeout(loadingState.resumeTimer);
4994
+ loadingState.resumeTimer = null;
4995
+ }
4996
+
4997
+ // 4. 中断当前的本地批量加载循环
4998
+ if (loadingState.animationFrameId) {
4999
+ cancelAnimationFrame(loadingState.animationFrameId);
5000
+ loadingState.animationFrameId = null;
5001
+ }
5002
+
5003
+ // 5. 联动 StreamLoader (如果存在实例)
5004
+ // 确保网络流式加载也同步暂停
5005
+ const streamLoader = this.noObserver.streamLoader;
5006
+ if (streamLoader && typeof streamLoader.handleControlStart === 'function') {
5007
+ streamLoader.handleControlStart();
5008
+ }
5009
+ } else {
5010
+ // --- 解除中断/恢复 ---
5011
+ const { immediate = false } = options;
5012
+
5013
+ // 关键修复:立即更新交互状态,不依赖 doResume 的执行时机
5014
+ // 这样可以避免当一种交互(如 camera)结束但另一种(如 wheel)仍活跃时,状态无法正确复位的问题
5015
+ if (reason === 'camera') {
5016
+ userInteracting = false;
5017
+ }
5018
+
5019
+ // 定义恢复执行逻辑
5020
+ const doResume = () => {
5021
+ // 双重检查:
5022
+ // 1. 如果有滚轮定时器未结束,说明还在连续滚动中,不恢复
5023
+ if (loadingState.interactionState.wheelTimeout) return;
5024
+
5025
+ // 2. 如果是 wheel 结束,但 userInteracting (camera) 还在进行,则不恢复
5026
+ if (reason === 'wheel' && userInteracting) return;
5027
+
5028
+ // 3. 检查是否有强制跳过标记 (可能由其他逻辑触发)
5029
+ // if (forceSkipRendering && reason !== 'user_interaction') return; // 视情况而定
5030
+
5031
+ // --- 执行恢复 ---
5032
+
5033
+ loadingState.isPaused = false;
5034
+ loadingState.pauseReason = '';
5035
+
5036
+ loadingState.interactionState.isInteracting = false;
5037
+ loadingState.interactionState.interactionType = '';
5038
+
5039
+ forceSkipRendering = false;
5040
+
5041
+ // 恢复本地批量加载 (如果未完成)
5042
+ if (loadingState.isLoading && loadingState.currentBatch < loadingState.totalBatches) {
5043
+ this.loadNextBatch();
5044
+ }
5045
+
5046
+ // 恢复 StreamLoader
5047
+ const streamLoader = this.noObserver.streamLoader;
5048
+ if (streamLoader && typeof streamLoader.handleControlEnd === 'function') {
5049
+ // StreamLoader 内部通常有防抖或延迟,这里直接通知结束即可
5050
+ streamLoader.handleControlEnd();
5051
+ }
5052
+ };
5053
+
5054
+ // 清理旧定时器
5055
+ if (loadingState.resumeTimer) {
5056
+ clearTimeout(loadingState.resumeTimer);
5057
+ loadingState.resumeTimer = null;
5058
+ }
5059
+
5060
+ if (immediate) {
5061
+ doResume();
5062
+ } else {
5063
+ // 默认延迟恢复,避免频繁抖动
5064
+ const delay = loadingState.resumeDelay || 100;
5065
+ loadingState.resumeTimer = setTimeout(() => {
5066
+ doResume();
5067
+ loadingState.resumeTimer = null;
5068
+ }, delay);
5069
+ }
5070
+ }
5071
+ },
5072
+
5073
+ /**
5074
+ * 暂停模型渲染 (兼容旧接口)
5075
+ */
5076
+ pauseModelRendering(reason = 'user_interaction') {
5077
+ this.setSystemInterruption(true, reason);
5078
+ },
5079
+
5080
+ /**
5081
+ * 交互开始
5082
+ */
5083
+ beginInteraction(type = 'user') {
5084
+ const reason =
5085
+ type === 'wheel' ? 'wheel' : type === 'camera' ? 'camera' : 'user_interaction';
5086
+ this.setSystemInterruption(true, reason);
5087
+ },
5088
+
5089
+ /**
5090
+ * 交互结束
5091
+ */
5092
+ endInteraction(type = 'user', event = null, opts = {}) {
5093
+ // 保持事件参数签名,虽然这里没用到 event
5094
+ this.setSystemInterruption(false, type, opts);
5095
+ },
5096
+
5097
+ /**
5098
+ * 滚轮交互结束的定时检测
5099
+ */
5100
+ scheduleWheelInteractionEnd(event, delay = 100) {
5101
+ const loadingState = this.noObserver
5102
+ ? this.noObserver.batchLoadingState
5103
+ : this.batchLoadingState;
5104
+
5105
+ if (loadingState.interactionState.wheelTimeout) {
5106
+ clearTimeout(loadingState.interactionState.wheelTimeout);
5107
+ loadingState.interactionState.wheelTimeout = null;
5108
+ }
5109
+
5110
+ loadingState.interactionState.wheelTimeout = setTimeout(() => {
5111
+ this.$emit('wheelEnd', event);
5112
+ this.endInteraction('wheel', event, { immediateResume: false });
5113
+ loadingState.interactionState.wheelTimeout = null;
5114
+ }, delay);
5115
+ },
5116
+
5117
+ /**
5118
+ * 恢复模型渲染 (兼容旧接口)
5119
+ */
5120
+ resumeModelRendering(immediate = true) {
5121
+ this.setSystemInterruption(false, 'user_interaction', { immediate });
5122
+ },
5123
+
5124
+ /**
5125
+ * 暂停批次加载 (兼容旧接口)
5126
+ */
5127
+ pauseBatchLoading(reason = 'user_interaction') {
5128
+ this.setSystemInterruption(true, reason);
5129
+ },
5130
+
5131
+ /**
5132
+ * 恢复批次加载 (兼容旧接口)
5133
+ */
5134
+ resumeBatchLoading() {
5135
+ this.setSystemInterruption(false, 'user_interaction');
5136
+ },
5137
+
5138
+ /**
5139
+ * 隐藏场景包围盒
5140
+ */
5141
+ hideSceneBoundingBox() {
5142
+ if (sceneBoundingBoxHelper && scene) {
5143
+ scene.remove(sceneBoundingBoxHelper);
5144
+ sceneBoundingBoxHelper = null;
5145
+ boundingBoxVisible = false;
5146
+ console.log('场景包围盒已隐藏');
5147
+ }
5148
+ },
5149
+
5150
+ /**
5151
+ * 切换场景包围盒显示状态
5152
+ */
5153
+ toggleSceneBoundingBox() {
5154
+ if (boundingBoxVisible) {
5155
+ this.hideSceneBoundingBox();
5156
+ } else {
5157
+ this.showSceneBoundingBox();
5158
+ }
1650
5159
  },
1651
5160
  },
1652
5161
  };
@@ -1695,6 +5204,86 @@
1695
5204
  width: 20px;
1696
5205
  height: 20px;
1697
5206
  }
5207
+
5208
+ /* 加载指示器样式 */
5209
+ .loading-overlay {
5210
+ position: absolute;
5211
+ top: 0;
5212
+ left: 0;
5213
+ right: 0;
5214
+ bottom: 0;
5215
+ background-color: rgba(0, 0, 0, 0.7);
5216
+ display: flex;
5217
+ align-items: center;
5218
+ justify-content: center;
5219
+ z-index: 1000;
5220
+ opacity: 0;
5221
+ visibility: hidden;
5222
+ transition: opacity 0.3s ease, visibility 0.3s ease;
5223
+ }
5224
+
5225
+ .loading-overlay--visible {
5226
+ opacity: 1;
5227
+ visibility: visible;
5228
+ }
5229
+
5230
+ .loading-content {
5231
+ background: white;
5232
+ border-radius: 12px;
5233
+ padding: 30px;
5234
+ text-align: center;
5235
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
5236
+ min-width: 300px;
5237
+ max-width: 400px;
5238
+ }
5239
+
5240
+ .loading-spinner {
5241
+ width: 40px;
5242
+ height: 40px;
5243
+ border: 4px solid #f3f3f3;
5244
+ border-top: 4px solid #409eff;
5245
+ border-radius: 50%;
5246
+ animation: spin 1s linear infinite;
5247
+ margin: 0 auto 20px;
5248
+ }
5249
+
5250
+ @keyframes spin {
5251
+ 0% {
5252
+ transform: rotate(0deg);
5253
+ }
5254
+ 100% {
5255
+ transform: rotate(360deg);
5256
+ }
5257
+ }
5258
+
5259
+ .loading-text {
5260
+ font-size: 16px;
5261
+ font-weight: 500;
5262
+ color: #333;
5263
+ margin-bottom: 20px;
5264
+ }
5265
+
5266
+ .loading-progress-bar {
5267
+ width: 100%;
5268
+ height: 8px;
5269
+ background-color: #f0f0f0;
5270
+ border-radius: 4px;
5271
+ overflow: hidden;
5272
+ margin-bottom: 15px;
5273
+ }
5274
+
5275
+ .loading-progress-fill {
5276
+ height: 100%;
5277
+ background: linear-gradient(90deg, #409eff 0%, #67c23a 100%);
5278
+ border-radius: 4px;
5279
+ transition: width 0.3s ease;
5280
+ }
5281
+
5282
+ .loading-details {
5283
+ font-size: 12px;
5284
+ color: #666;
5285
+ line-height: 1.5;
5286
+ }
1698
5287
  </style>
1699
5288
  <style>
1700
5289
  /* 自定义lil-gui样式 - 浅色主题 */
@@ -1853,4 +5442,4 @@
1853
5442
  .lil-gui .folder.closed > .children {
1854
5443
  display: none !important;
1855
5444
  }
1856
- </style>
5445
+ </style>