fl-web-component 1.4.7 → 1.4.9-beta.0
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.
- package/README.md +1 -28
- package/dist/fl-web-component.common.1.js +2 -2
- package/dist/fl-web-component.common.1.js.map +1 -1
- package/dist/fl-web-component.common.2.js.map +1 -1
- package/dist/fl-web-component.common.js +77420 -47296
- package/dist/fl-web-component.common.js.map +1 -1
- package/dist/fl-web-component.css +1 -1
- package/package.json +12 -4
- package/packages/components/com-flcanvas/components/entityFormatting.js +9 -1
- package/packages/components/com-graphics/box.json +77 -0
- package/packages/components/com-graphics/component/ann-tool.vue +465 -0
- package/packages/components/com-graphics/index copy.vue +1679 -0
- package/packages/components/com-graphics/index.vue +3890 -301
- package/packages/components/com-graphics/pid.vue +210 -44
- package/packages/components/com-graphics/test.html +127 -0
- package/packages/components/com-tiles/index.vue +187 -0
- package/packages/utils/StreamLoader.js +1498 -0
- package/packages/utils/StreamLoaderParser.worker.js +595 -0
- package/patches/camera-controls+2.9.0.patch +63 -63
- package/src/main.js +9 -1
- package/src/static/ann-img/mark_circle@2x.png +0 -0
- package/src/static/ann-img/mark_clear@2x.png +0 -0
- package/src/static/ann-img/mark_cloud@2x.png +0 -0
- package/src/static/ann-img/mark_color@2x.png +0 -0
- package/src/static/ann-img/mark_eraser@2x.png +0 -0
- package/src/static/ann-img/mark_exit@2x.png +0 -0
- package/src/static/ann-img/mark_finish@2x.png +0 -0
- package/src/static/ann-img/mark_font@2x.png +0 -0
- package/src/static/ann-img/mark_polyline@2x.png +0 -0
- package/src/static/ann-img/mark_rectangle@2x.png +0 -0
- package/src/static/ann-img/mark_zoomin@2x.png +0 -0
- package/src/static/ann-img/mark_zoomout@2x.png +0 -0
- package/src/utils/cloud.js +110 -0
- package/src/utils/cursor.js +10 -0
- package/src/utils/flgltf-parser.js +245 -193
- package/src/utils/instance-parser.js +718 -170
- package/dist/fl-web-component.common.3.js +0 -7740
- 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
|
-
|
|
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 {
|
|
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
|
-
|
|
105
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
203
|
-
const
|
|
204
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
228
|
-
if (
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
center.
|
|
424
|
-
center.
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
|
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.
|
|
460
|
-
|
|
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.
|
|
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
|
|
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.
|
|
534
|
-
|
|
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
|
|
544
|
-
|
|
545
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
574
|
-
|
|
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
|
-
|
|
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
|
|
655
|
-
|
|
656
|
-
|
|
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 =
|
|
661
|
-
|
|
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
|
-
|
|
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
|
-
|
|
695
|
-
|
|
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 =
|
|
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(
|
|
965
|
-
|
|
966
|
-
|
|
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
|
-
|
|
979
|
-
|
|
980
|
-
|
|
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,
|
|
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.
|
|
1094
|
-
.min(
|
|
1095
|
-
.max(
|
|
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.
|
|
1103
|
-
.min(
|
|
1104
|
-
.max(
|
|
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.
|
|
1112
|
-
.min(
|
|
1113
|
-
.max(
|
|
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(
|
|
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
|
-
|
|
1155
|
-
|
|
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 =
|
|
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
|
-
|
|
1290
|
-
const
|
|
1291
|
-
const
|
|
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(
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
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
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4592
|
+
// 解析数据
|
|
4593
|
+
let parsedData = parseData(data, options);
|
|
4594
|
+
let instances = parsedData.instances;
|
|
4595
|
+
let drawObjs = parsedData.drawObjs;
|
|
1648
4596
|
|
|
1649
|
-
|
|
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>
|