fl-web-component 2.0.0-beta.9 → 2.0.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.
Files changed (33) hide show
  1. package/dist/fl-web-component.common.js +34592 -1519
  2. package/dist/fl-web-component.common.js.map +1 -1
  3. package/dist/fl-web-component.css +1 -1
  4. package/package.json +4 -15
  5. package/packages/components/com-card/index.vue +1 -1
  6. package/packages/components/com-flcanvas/components/bspline.js +31 -34
  7. package/packages/components/com-flcanvas/components/entityFormatting.js +810 -823
  8. package/packages/components/com-flcanvas/components/round10.js +17 -17
  9. package/packages/components/com-flcanvas/index.vue +314 -333
  10. package/packages/components/com-formDialog/index.vue +6 -4
  11. package/packages/components/com-graphics/component/ann-tool.vue +263 -208
  12. package/packages/components/com-graphics/index.vue +366 -773
  13. package/packages/components/com-graphics/pid.vue +304 -295
  14. package/packages/components/com-table/column-default.vue +2 -3
  15. package/packages/components/com-table/column-dynamic.vue +7 -4
  16. package/packages/components/com-table/column.vue +1 -2
  17. package/packages/components/com-table/index.vue +6 -5
  18. package/packages/components/com-tabs/index.vue +1 -2
  19. package/packages/components/com-tiles/index.vue +134 -136
  20. package/packages/components/com-treeDynamic/index.vue +1 -1
  21. package/packages/utils/StreamLoader.js +1548 -1489
  22. package/packages/utils/StreamLoaderParser.worker.js +9 -14
  23. package/src/main.js +2 -8
  24. package/src/utils/cloud.js +28 -28
  25. package/src/utils/cursor.js +11 -9
  26. package/src/utils/flgltf-parser.js +257 -245
  27. package/src/utils/instance-parser.js +20 -22
  28. package/src/utils/mini-devtool.js +94 -39
  29. package/src/utils/threejs/measure-angle.js +51 -13
  30. package/src/utils/threejs/measure-area.js +43 -12
  31. package/src/utils/threejs/measure-distance.js +43 -12
  32. package/src/utils/threejs/rain-shader.js +10 -10
  33. package/src/utils/threejs/snow-shader.js +9 -9
@@ -1,1489 +1,1548 @@
1
- import { gunzipSync } from 'fflate';
2
- import StreamLoaderParserWorker from './StreamLoaderParser.worker.js';
3
-
4
- /**
5
- * StreamLoader 类
6
- * 封装流式加载、解析与渲染调度的核心逻辑
7
- */
8
- export class StreamLoader {
9
- constructor(config = {}) {
10
- // 依赖注入
11
- this.modelApi = config.modelApi;
12
- this.projectId = config.projectId || '';
13
-
14
- this.createStreamRequestId =
15
- config.createStreamRequestId ||
16
- (() => `stream_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`);
17
- this.onCancelRequestId = config.onCancelRequestId;
18
- this.batchSize = config.batchSize || 20;
19
-
20
- // 钩子函数
21
- this.renderModelData = config.renderModelData || (async () => {});
22
- // 外部提供的交互状态检查函数(可选)
23
- this.externalEnsureNotInteracting = config.ensureNotInteracting;
24
- this.prefixIdKey = config.prefixIdKey || 'documentId';
25
- this.debug = config.debug || false;
26
-
27
- // 内部状态管理
28
- this.activeRequests = new Map();
29
- this.requestCounter = 0;
30
-
31
- this.currentAbortController = null;
32
- this.currentStreamReader = null;
33
- this.activeStreamReaders = new Set();
34
- this.activeStreamControllers = new Set();
35
- this.activeRequestIds = new Set();
36
- this.currentRequestId = null;
37
-
38
- this.isUserInteracting = false;
39
- this.parsePausePromise = null;
40
- this.parsePauseResolver = null;
41
-
42
- this.controlPressStartAt = 0;
43
- this.longPressAbortThresholdMs = 250;
44
-
45
- this.sceneBox = null;
46
- this.boxIndex = null;
47
- this.modelRegistry = new Map();
48
-
49
- // Initialize worker
50
- try {
51
- this.worker = new StreamLoaderParserWorker();
52
- // this.worker = null; // TODO
53
- } catch (e) {
54
- this.worker = null;
55
- }
56
- this.workerRequestMap = new Map();
57
- this.workerMessageHandler = null;
58
- if (this.worker) {
59
- this.workerMessageHandler = e => {
60
- const payload = e.data || {};
61
- const pending = this.workerRequestMap.get(payload.id);
62
- if (!pending) return;
63
- this.workerRequestMap.delete(payload.id);
64
- if (payload.type === 'success') {
65
- pending.resolve(payload.result);
66
- } else {
67
- pending.reject(new Error(payload.error));
68
- }
69
- };
70
- this.worker.addEventListener('message', this.workerMessageHandler);
71
- }
72
- }
73
-
74
- /**
75
- * 为对象或对象数组应用prefixId前缀
76
- * @param {Object|Array<Object>} target - 单个对象或对象数组
77
- * @param {string} propKey - 需要应用prefixId前缀的属性键名
78
- * @param {string} customPrefixId - 自定义prefixId,可选
79
- */
80
- _applyPrefixId(target, propKey, customPrefixId) {
81
- if (!this.prefixIdKey || !target) return;
82
-
83
- if (Array.isArray(target)) {
84
- target.forEach(item => {
85
- const prefixId = customPrefixId || item[this.prefixIdKey];
86
- if (item && prefixId && item[propKey] != null) {
87
- item[propKey] = `${item[propKey]}:${prefixId}`;
88
- }
89
- });
90
- } else if ((customPrefixId || target[this.prefixIdKey]) && target[propKey] != null) {
91
- const prefixId = customPrefixId || target[this.prefixIdKey];
92
- target[propKey] = `${target[propKey]}:${prefixId}`;
93
- }
94
- }
95
-
96
- workerRequest(type, data, transferable = []) {
97
- if (!this.worker) {
98
- return Promise.reject(new Error('Worker is not initialized'));
99
- }
100
- return new Promise((resolve, reject) => {
101
- const id = this.requestCounter++;
102
- this.workerRequestMap.set(id, { resolve, reject });
103
- this.worker.postMessage({ id, type, data }, transferable);
104
- });
105
- }
106
-
107
- // ----------------------------------------------------------------
108
- // 核心解析逻辑
109
- // ----------------------------------------------------------------
110
-
111
- async parseStreamImmediate(reader, list, range, abortSignal = null, streamId = null) {
112
- const batchMeshes = [];
113
- const batchPrimitives = [];
114
- const batchSize = this.batchSize;
115
- let batchStart = 0;
116
-
117
- const ensureNotAborted = () => {
118
- if (abortSignal && abortSignal.aborted) {
119
- return reader.cancel().catch(() => {});
120
- // throw new DOMException('Request was aborted', 'AbortError');
121
- }
122
- return null;
123
- };
124
-
125
- if (this.worker) {
126
- const localStreamId = streamId || this.createStreamRequestId();
127
- const ensureNotInteracting = () => {
128
- if (!this.externalEnsureNotInteracting && !this.isUserInteracting) return null;
129
- return this.ensureNotInteracting(abortSignal);
130
- };
131
-
132
- await this.workerRequest('streamInit', {
133
- streamId: localStreamId,
134
- prefixIdKey: this.prefixIdKey,
135
- });
136
-
137
- try {
138
- if (this.debug) performance.mark('while-start');
139
- while (true) {
140
- const abortPromise = ensureNotAborted();
141
- if (abortPromise) await abortPromise;
142
- const interactionPromise = ensureNotInteracting();
143
- if (interactionPromise) await interactionPromise;
144
-
145
- let content;
146
- try {
147
- content = await reader.read();
148
- } catch (e) {
149
- if (e && e.name === 'AbortError') {
150
- throw e;
151
- }
152
- throw e;
153
- }
154
-
155
- const { done, value } = content;
156
- if (done) {
157
- const abortPromiseOnDone = ensureNotAborted();
158
- if (abortPromiseOnDone) await abortPromiseOnDone;
159
- if (this.debug) {
160
- performance.mark('while-end');
161
- performance.measure('while', 'while-start', 'while-end');
162
- }
163
-
164
- const flushed = await this.workerRequest('streamFlush', { streamId: localStreamId });
165
- if (flushed?.meshes?.length) {
166
- batchMeshes.push(...flushed.meshes);
167
- batchPrimitives.push(...flushed.primitives);
168
- }
169
-
170
- while (batchMeshes.length - batchStart >= batchSize) {
171
- const abortPromiseInBatch = ensureNotAborted();
172
- if (abortPromiseInBatch) await abortPromiseInBatch;
173
- const interactionPromiseInBatch = ensureNotInteracting();
174
- if (interactionPromiseInBatch) await interactionPromiseInBatch;
175
- const meshesToProcess = batchMeshes.slice(batchStart, batchStart + batchSize);
176
- const primitivesToProcess = batchPrimitives.slice(batchStart, batchStart + batchSize);
177
- batchStart += batchSize;
178
- this.processBatchData(
179
- meshesToProcess,
180
- primitivesToProcess,
181
- list,
182
- range,
183
- abortSignal
184
- );
185
- }
186
-
187
- if (batchMeshes.length - batchStart > 0) {
188
- const abortPromiseInBatch = ensureNotAborted();
189
- if (abortPromiseInBatch) await abortPromiseInBatch;
190
- const interactionPromiseInBatch = ensureNotInteracting();
191
- if (interactionPromiseInBatch) await interactionPromiseInBatch;
192
- const meshesToProcess = batchMeshes.slice(batchStart);
193
- const primitivesToProcess = batchPrimitives.slice(batchStart);
194
- batchStart = batchMeshes.length;
195
- this.processBatchData(
196
- meshesToProcess,
197
- primitivesToProcess,
198
- list,
199
- range,
200
- abortSignal
201
- );
202
- }
203
- break;
204
- }
205
-
206
- const transferable = value?.buffer instanceof ArrayBuffer ? [value.buffer] : [];
207
- const parsed = await this.workerRequest(
208
- 'streamPush',
209
- {
210
- streamId: localStreamId,
211
- chunk: value.buffer,
212
- byteOffset: value.byteOffset,
213
- byteLength: value.byteLength,
214
- },
215
- transferable
216
- );
217
-
218
- if (parsed?.meshes?.length) {
219
- batchMeshes.push(...parsed.meshes);
220
- batchPrimitives.push(...parsed.primitives);
221
- }
222
-
223
- while (batchMeshes.length - batchStart >= batchSize) {
224
- const abortPromiseInBatch = ensureNotAborted();
225
- if (abortPromiseInBatch) await abortPromiseInBatch;
226
- const interactionPromiseInBatch = ensureNotInteracting();
227
- if (interactionPromiseInBatch) await interactionPromiseInBatch;
228
- const meshesToProcess = batchMeshes.slice(batchStart, batchStart + batchSize);
229
- const primitivesToProcess = batchPrimitives.slice(batchStart, batchStart + batchSize);
230
- batchStart += batchSize;
231
- this.processBatchData(
232
- meshesToProcess,
233
- primitivesToProcess,
234
- list,
235
- range,
236
- abortSignal
237
- );
238
- }
239
-
240
- if (batchStart >= batchSize * 4) {
241
- batchMeshes.splice(0, batchStart);
242
- batchPrimitives.splice(0, batchStart);
243
- batchStart = 0;
244
- }
245
- }
246
- } finally {
247
- try {
248
- await this.workerRequest('streamDispose', { streamId: localStreamId });
249
- } catch (e) {}
250
- }
251
- return;
252
- }
253
-
254
- const decoder = new TextDecoder('utf-8');
255
- let buffer = new Uint8Array();
256
-
257
- const pendingPrimitives = new Map();
258
- let expectingPrimitive = true;
259
- let isFullProps = true;
260
-
261
-
262
- while (true) {
263
- await ensureNotAborted();
264
-
265
- await this.ensureNotInteracting(abortSignal);
266
-
267
- let content;
268
- try {
269
- content = await reader.read();
270
- } catch (e) {
271
- if (e && e.name === 'AbortError') {
272
- throw e;
273
- }
274
- throw e;
275
- }
276
-
277
- const { done, value } = content;
278
- if (done) {
279
- await ensureNotAborted();
280
-
281
- if (batchMeshes.length > 0) {
282
- await ensureNotAborted();
283
- await this.ensureNotInteracting(abortSignal);
284
- await this.processBatchData(batchMeshes, batchPrimitives, list, range, abortSignal);
285
- }
286
- break;
287
- }
288
-
289
- const newBuf = new Uint8Array(buffer.length + value.length);
290
- newBuf.set(buffer);
291
- newBuf.set(value, buffer.length);
292
- buffer = newBuf;
293
-
294
- while (buffer.length > 0) {
295
- await ensureNotAborted();
296
- if (expectingPrimitive) {
297
- if (buffer.length < 12) break;
298
-
299
- try {
300
- const dataView = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
301
- const primitiveResult = this.parsePrimitive(dataView, buffer, 0, isFullProps);
302
- let primitiveData = primitiveResult.primitive;
303
-
304
- this._applyPrefixId(primitiveData, 'id');
305
- this._applyPrefixId(primitiveData, 'material');
306
- const consumedBytes = primitiveResult.offset;
307
-
308
- if (isFullProps) {
309
- const { position, normal, posindex, nolindex, indices } =
310
- this.parsePrimitiveData(primitiveData);
311
- const formatted = this.formatPrimitiveData({ position, normal, posindex, nolindex });
312
-
313
- delete primitiveData.nolindex;
314
- delete primitiveData.posindex;
315
-
316
- primitiveData.position = formatted.position;
317
- primitiveData.normal = formatted.normal;
318
- primitiveData.indices = indices;
319
- }
320
- const docId = primitiveData.id;
321
-
322
- pendingPrimitives.set(docId, primitiveData);
323
-
324
- expectingPrimitive = false;
325
- buffer = buffer.slice(consumedBytes);
326
- } catch (e) {
327
- break;
328
- }
329
- } else {
330
- if (buffer.length < 4) break;
331
-
332
- const length = new DataView(buffer.buffer, buffer.byteOffset).getUint32(0, false);
333
-
334
- if (length > 10 * 1024 * 1024) {
335
- expectingPrimitive = true;
336
- continue;
337
- }
338
-
339
- const totalLen = 4 + length;
340
-
341
- if (buffer.length < totalLen) break;
342
-
343
- const data = buffer.slice(4, totalLen);
344
- let mesh = null;
345
-
346
- try {
347
- const jsonStr = decoder.decode(data);
348
- mesh = JSON.parse(jsonStr);
349
- } catch (e) {
350
- expectingPrimitive = true;
351
- continue;
352
- }
353
-
354
- if (mesh) {
355
- isFullProps = true;
356
-
357
- this._applyPrefixId(mesh, 'id');
358
- if (Array.isArray(mesh)) {
359
- mesh.forEach(item => {
360
- this._applyPrefixId(item.primitives, 'prmid', item.documentId);
361
- });
362
- } else {
363
- this._applyPrefixId(mesh.primitives, 'prmid', mesh.documentId);
364
- }
365
-
366
- let docId = mesh.id;
367
- const meshToPrimId = mesh.primitives.map(item => item.prmid);
368
- const hasPendingPrimitive = meshToPrimId.some(item => pendingPrimitives.has(item));
369
- const hasPendingMesh = pendingPrimitives.has(docId);
370
-
371
- if (hasPendingMesh || hasPendingPrimitive) {
372
- let primitiveData = pendingPrimitives.get(docId);
373
- if (!hasPendingMesh && hasPendingPrimitive) {
374
- const primId = meshToPrimId[0];
375
- primitiveData = pendingPrimitives.get(primId);
376
- }
377
-
378
- batchMeshes.push(mesh);
379
- batchPrimitives.push(primitiveData);
380
-
381
- if (batchMeshes.length >= this.batchSize) {
382
- await ensureNotAborted();
383
- await this.ensureNotInteracting(abortSignal);
384
- await this.processBatchData(batchMeshes, batchPrimitives, list, range, abortSignal);
385
-
386
- batchMeshes.length = 0;
387
- batchPrimitives.length = 0;
388
- }
389
- }
390
-
391
- expectingPrimitive = false;
392
- buffer = buffer.slice(totalLen);
393
- }
394
- }
395
- }
396
- }
397
- }
398
-
399
- // ----------------------------------------------------------------
400
- // 数据处理与渲染
401
- // ----------------------------------------------------------------
402
-
403
- processBatchData(meshes, primitives, list, range, abortSignal = null) {
404
- try {
405
- if (abortSignal && abortSignal.aborted) {
406
- throw new DOMException('Request was aborted', 'AbortError');
407
- }
408
- this.renderModelData(meshes, primitives, list, range); // TODO
409
- } catch (error) {
410
- console.error('Failed to render batch data:', error);
411
- }
412
- }
413
-
414
- // ----------------------------------------------------------------
415
- // 请求管理
416
- // ----------------------------------------------------------------
417
-
418
- async getPrimitivesByRangeStream(params, list, range, abortSignal = null) {
419
- await this.abortAllStreamRequests();
420
-
421
- const requestId = this.createStreamRequestId();
422
- let internalController = null;
423
- let reader = null;
424
-
425
- try {
426
- internalController = new AbortController();
427
- this.currentAbortController = internalController;
428
- this.activeStreamControllers.add(internalController);
429
- this.activeRequestIds.add(requestId);
430
-
431
- if (abortSignal) {
432
- if (abortSignal.aborted) {
433
- // internalController.abort(); // TODO
434
- } else {
435
- abortSignal.addEventListener('abort', () => internalController.abort(), { once: true });
436
- }
437
- }
438
-
439
- if (internalController.signal.aborted) {
440
- throw new DOMException('Request was aborted', 'AbortError');
441
- }
442
- params.requestId = requestId;
443
- params.projectId = this.projectId
444
- let res;
445
- if (this.modelApi && typeof this.modelApi.getPrimitivesByRangeStream === 'function') {
446
- res = await this.modelApi.getPrimitivesByRangeStream(params, internalController.signal);
447
- } else {
448
- throw new Error('modelApi.getPrimitivesByRangeStream is not available');
449
- }
450
-
451
- reader = res.getReader();
452
- this.currentStreamReader = reader;
453
- this.activeStreamReaders.add(reader);
454
-
455
- await this.parseStreamImmediate(reader, list, range, internalController.signal, requestId);
456
-
457
- if (res) {
458
- return res;
459
- }
460
- return null;
461
- } catch (error) {
462
- if (error.name === 'AbortError') {
463
- throw error;
464
- }
465
- console.error(error);
466
- throw error;
467
- } finally {
468
- if (reader) this.activeStreamReaders.delete(reader);
469
- if (internalController) this.activeStreamControllers.delete(internalController);
470
- if (requestId) this.activeRequestIds.delete(requestId);
471
- this.currentStreamReader = null;
472
- this.currentAbortController = null;
473
- }
474
- }
475
-
476
- async getPrimitivesByRange(params, abortSignal = null) {
477
- try {
478
- if (abortSignal && abortSignal.aborted) {
479
- throw new DOMException('Request was aborted', 'AbortError');
480
- }
481
-
482
- const res = await this.modelApi.getPrimitivesByRange(params, abortSignal);
483
-
484
- if (abortSignal && abortSignal.aborted) {
485
- throw new DOMException('Request was aborted', 'AbortError');
486
- }
487
-
488
- if (res) {
489
- return res;
490
- } else {
491
- throw new Error('Failed to get primitives');
492
- }
493
- } catch (error) {
494
- if (error.name === 'AbortError') {
495
- throw error;
496
- }
497
- throw error;
498
- }
499
- }
500
-
501
- async getPrimitivesByMaterial(params) {
502
- try {
503
- const res = await this.modelApi.getPrimitivesByMaterial(params);
504
- if (res) {
505
- return res;
506
- }
507
- throw new Error('Failed to get Material');
508
- } catch (error) {
509
- throw error;
510
- }
511
- }
512
-
513
- createAbortableRequest(requestKey = null) {
514
- const requestId = requestKey || `req_${++this.requestCounter}`;
515
-
516
- if (this.activeRequests.has(requestId)) {
517
- const existingController = this.activeRequests.get(requestId);
518
- existingController.abort();
519
- }
520
-
521
- const abortController = new AbortController();
522
-
523
- this.activeRequests.set(requestId, abortController);
524
- return {
525
- requestId,
526
- signal: abortController.signal,
527
- abort: () => {
528
- abortController.abort();
529
- this.activeRequests.delete(requestId);
530
- },
531
- };
532
- }
533
-
534
- cleanupRequest(requestId) {
535
- if (this.activeRequests.has(requestId)) {
536
- this.activeRequests.delete(requestId);
537
- }
538
- }
539
-
540
- async abortAllStreamRequests() {
541
- try {
542
- const requestIds = Array.from(this.activeRequestIds);
543
- for (const requestId of requestIds) {
544
- if (typeof this.onCancelRequestId === 'function') {
545
- try {
546
- await this.onCancelRequestId(requestId);
547
- } catch (e) {}
548
- }
549
- }
550
- this.activeRequestIds.clear();
551
-
552
- this.activeStreamControllers.forEach(controller => {
553
- try {
554
- controller.abort();
555
- } catch (e) {}
556
- });
557
- this.activeStreamControllers.clear();
558
-
559
- const readers = Array.from(this.activeStreamReaders);
560
- this.activeStreamReaders.clear();
561
- for (const reader of readers) {
562
- try {
563
- await reader.cancel();
564
- } catch (e) {}
565
- }
566
- } finally {
567
- if (this.currentAbortController) {
568
- try {
569
- this.currentAbortController.abort();
570
- } catch (e) {}
571
- this.currentAbortController = null;
572
- }
573
- if (this.currentStreamReader) {
574
- try {
575
- await this.currentStreamReader.cancel();
576
- } catch (e) {}
577
- this.currentStreamReader = null;
578
- }
579
- }
580
- }
581
-
582
- // ----------------------------------------------------------------
583
- // 辅助加载方法
584
- // ----------------------------------------------------------------
585
-
586
- async fetchPrimitiveBufferByStream(range, list, abortSignal = null, requestId = null) {
587
- try {
588
- await this.abortAllStreamRequests();
589
- const request = this.createAbortableRequest(requestId); // Use requestId if provided as key? Logic slightly different in utils
590
-
591
- // Adaptation: utils used getPrimitivesByRangeStreamWithAutoAbort which calls getPrimitivesByRangeStream
592
- // Here I simplify by calling getPrimitivesByRangeStream directly but managing request key
593
-
594
- const buffer = await this.getPrimitivesByRangeStream(
595
- range,
596
- list,
597
- range,
598
- abortSignal || request.signal
599
- );
600
-
601
- this.cleanupRequest(request.requestId);
602
- if (!buffer) return null;
603
- return buffer;
604
- } catch (error) {
605
- if (error.name === 'AbortError') {
606
- // throw error;
607
- }
608
- if (requestId && this.currentRequestId !== requestId) {
609
- return null;
610
- }
611
- // throw error;
612
- }
613
- }
614
-
615
- async fetchPrimitiveBuffer(range, abortSignal = null, requestId = null) {
616
- try {
617
- const request = this.createAbortableRequest(requestId || 'default');
618
- const buffer = await this.getPrimitivesByRange(range, abortSignal || request.signal);
619
- this.cleanupRequest(request.requestId);
620
-
621
- if (!buffer || buffer.byteLength === 0) return null;
622
- if (buffer.byteLength < 4) return null;
623
- return buffer;
624
- } catch (error) {
625
- if (error.name === 'AbortError') {
626
- throw error;
627
- }
628
- if (requestId && this.currentRequestId !== requestId) {
629
- return null;
630
- }
631
- throw error;
632
- }
633
- }
634
-
635
- async parseBufferData(buffer) {
636
- if (this.worker) {
637
- let transferable = [];
638
- if (buffer instanceof ArrayBuffer) {
639
- transferable.push(buffer);
640
- } else if (buffer.buffer instanceof ArrayBuffer) {
641
- transferable.push(buffer.buffer);
642
- }
643
-
644
- return this.workerRequest(
645
- 'parseBufferData',
646
- { buffer, prefixIdKey: this.prefixIdKey },
647
- transferable
648
- );
649
- } else {
650
- return this._parseBufferDataSync(buffer);
651
- }
652
- }
653
-
654
- async _parseBufferDataSync(buffer) {
655
- let uint8Array = null;
656
- let dataView = null;
657
- let meshJsonBytes = null;
658
- let primitives = [];
659
-
660
- try {
661
- uint8Array = new Uint8Array(buffer);
662
- dataView = new DataView(buffer);
663
-
664
- let offset = 0;
665
- const meshJsonLength = dataView.getInt32(offset, false);
666
- offset += 4;
667
-
668
- meshJsonBytes = new Uint8Array(uint8Array.buffer.slice(offset, offset + meshJsonLength));
669
- offset += meshJsonLength;
670
-
671
- const decoder = new TextDecoder('utf-8');
672
- const meshJsonStr = decoder.decode(meshJsonBytes);
673
- meshJsonBytes = null;
674
-
675
- let mesh;
676
- try {
677
- mesh = JSON.parse(meshJsonStr);
678
- this._applyPrefixId(mesh, 'id');
679
- if (Array.isArray(mesh)) {
680
- mesh.forEach(item => {
681
- this._applyPrefixId(item.primitives, 'prmid', item.documentId);
682
- });
683
- } else {
684
- this._applyPrefixId(mesh.primitives, 'prmid', mesh.documentId);
685
- }
686
- } catch (e) {
687
- console.error('JSON 解析失败:', e, meshJsonStr);
688
- throw new Error('JSON解析失败');
689
- }
690
-
691
- while (offset < uint8Array.length) {
692
- try {
693
- const result = this.parsePrimitive(dataView, uint8Array, offset);
694
- console.log('result', result);
695
- if (!result) break;
696
- primitives.push(result.primitive);
697
- offset = result.offset;
698
- } catch (e) {
699
- console.error('解析失败,偏移:', offset, e);
700
- break;
701
- }
702
- }
703
-
704
- for (let i = 0; i < primitives.length; i++) {
705
- const primitive = primitives[i];
706
- this._applyPrefixId(primitive, 'id');
707
- this._applyPrefixId(primitive, 'material');
708
- const { position, normal, posindex, nolindex, indices } =
709
- this.parsePrimitiveData(primitive);
710
- const formatted = this.formatPrimitiveData({ position, normal, posindex, nolindex });
711
- delete primitive.nolindex;
712
- delete primitive.posindex;
713
- primitive.position = formatted.position;
714
- primitive.normal = formatted.normal;
715
- primitive.indices = indices;
716
- }
717
-
718
- return { mesh, primitives };
719
- } catch (error) {
720
- throw error;
721
- } finally {
722
- uint8Array = null;
723
- dataView = null;
724
- meshJsonBytes = null;
725
- }
726
- }
727
-
728
- async fetchJsonSync(list, range, abortSignal = null, requestId = null) {
729
- let buffer = null;
730
- let primitives = [];
731
- try {
732
- const loadStartTime = Date.now();
733
- buffer = await this.fetchPrimitiveBuffer(range, abortSignal, requestId);
734
- if (!buffer) {
735
- return { primitives: [] };
736
- }
737
- const { mesh, primitives: parsed } = await this.parseBufferData(buffer);
738
- primitives = parsed;
739
- if (typeof this.renderModelData === 'function') {
740
- await this.renderModelData(mesh, primitives, list, range);
741
- }
742
- return { primitives, mesh };
743
- } catch (err) {
744
- if (err.name === 'AbortError') {
745
- throw err;
746
- }
747
- if (requestId && this.currentRequestId !== requestId) {
748
- return;
749
- }
750
- throw err;
751
- } finally {
752
- try {
753
- buffer = null;
754
- primitives = null;
755
- if (typeof window !== 'undefined' && window.gc) {
756
- window.gc();
757
- }
758
- } catch (cleanupErr) {
759
- console.warn('内存清理时发生错误:', cleanupErr);
760
- }
761
- }
762
- }
763
-
764
- async fetchJsonStream(list, range, abortSignal = null, requestId = null) {
765
- try {
766
- const loadStartTime = Date.now();
767
- await this.fetchPrimitiveBufferByStream(range, list, abortSignal, requestId);
768
- } catch (err) {
769
- if (err.name === 'AbortError') {
770
- throw err;
771
- }
772
- if (requestId && this.currentRequestId !== requestId) {
773
- return;
774
- }
775
- throw err;
776
- }
777
- }
778
-
779
- // ----------------------------------------------------------------
780
- // 数据格式化与解析工具
781
- // ----------------------------------------------------------------
782
-
783
- formatPrimitiveData(primitive) {
784
- const { position, normal, posindex, nolindex } = primitive;
785
- const restoredPosition = new Array(posindex.length);
786
- const restoredNormal = new Array(nolindex.length);
787
- for (let i = 0; i < posindex.length; i++) {
788
- const posIndex = posindex[i];
789
- restoredPosition[i] = position[posIndex]; // 索引和顶点一对一
790
- }
791
- for (let i = 0; i < nolindex.length; i++) {
792
- const normalIndex = nolindex[i];
793
- restoredNormal[i] = normal[normalIndex]; // 索引和法向量一对一
794
- }
795
- primitive.position = null;
796
- primitive.normal = null;
797
- primitive.posindex = null;
798
- primitive.nolindex = null;
799
- return { position: restoredPosition, normal: restoredNormal };
800
- }
801
-
802
- parsePrimitiveData(primitive) {
803
- const keyOrder = ['position', 'normal', 'indices', 'posindex', 'nolindex'];
804
- const typeMap = {
805
- 0: { bytes: 4, getter: 'getFloat32' },
806
- 1: { bytes: 4, getter: 'getInt32' },
807
- 2: { bytes: 2, getter: 'getInt16' },
808
- };
809
-
810
- const parsedResult = {};
811
- const dataTypeString = primitive.dataType;
812
-
813
- for (let i = 0; i < keyOrder.length; i++) {
814
- const key = keyOrder[i];
815
- const typeCode = dataTypeString[i];
816
- const typeInfo = typeMap[typeCode];
817
- const uint8Array = primitive.isCompressed === 1 || primitive.isCompressed === undefined ? gunzipSync(primitive[key]) : primitive[key];
818
- if (!typeInfo || !uint8Array) {
819
- console.warn(`无法找到键 "${key}" 或其类型定义,已跳过。`);
820
- parsedResult[key] = [];
821
- continue;
822
- }
823
- const result_array = [];
824
- const dataView = new DataView(
825
- uint8Array.buffer,
826
- uint8Array.byteOffset,
827
- uint8Array.byteLength
828
- );
829
- const littleEndian = true;
830
- for (let byteOffset = 0; byteOffset < dataView.byteLength; byteOffset += typeInfo.bytes) {
831
- if (byteOffset + typeInfo.bytes > dataView.byteLength) {
832
- console.warn(
833
- `键 "${key}" 的数据长度 (${dataView.byteLength}) 不是其类型 (${typeInfo.bytes}字节) 的整数倍,末尾数据可能不完整。`
834
- );
835
- break;
836
- }
837
- const value = dataView[typeInfo.getter](byteOffset, littleEndian);
838
- result_array.push(value);
839
- }
840
- parsedResult[key] = result_array;
841
- }
842
- return parsedResult;
843
- }
844
-
845
- parsePrimitive(dataView, uint8Array, offset, isFullProps = true) {
846
- const primitive = {};
847
- if (dataView.byteLength < offset + 12) {
848
- throw new Error('Insufficient data for primitive header');
849
- }
850
- primitive.id = dataView.getInt32(offset, false);
851
- offset += 4;
852
-
853
- if (dataView.byteLength < offset + 4) {
854
- throw new Error('Insufficient data for GeomText length');
855
- }
856
- const documentTextLen = dataView.getUint32(offset, false);
857
- offset += 4;
858
- if (documentTextLen === 0xffffffff) {
859
- primitive.documentId = null;
860
- } else if (documentTextLen > 0) {
861
- if (dataView.byteLength < offset + documentTextLen) {
862
- throw new Error('Insufficient data for GeomText content');
863
- }
864
- const textBytes = uint8Array.subarray(offset, offset + documentTextLen);
865
- primitive.documentId = new TextDecoder('utf-8').decode(textBytes);
866
- offset += documentTextLen;
867
- } else {
868
- primitive.documentId = '';
869
- }
870
-
871
- primitive.material = dataView.getInt32(offset, false);
872
- offset += 4;
873
-
874
- if (dataView.byteLength < offset + 4) {
875
- throw new Error('Insufficient data for GeomText length');
876
- }
877
- const geomTextLen = dataView.getUint32(offset, false);
878
- offset += 4;
879
- if (geomTextLen === 0xffffffff) {
880
- primitive.geomText = null;
881
- } else if (geomTextLen > 0) {
882
- if (dataView.byteLength < offset + geomTextLen) {
883
- throw new Error('Insufficient data for GeomText content');
884
- }
885
- const textBytes = uint8Array.subarray(offset, offset + geomTextLen);
886
- primitive.geomText = new TextDecoder('utf-8').decode(textBytes);
887
- offset += geomTextLen;
888
- } else {
889
- primitive.geomText = '';
890
- }
891
-
892
- // console.log('window.plat', window.plat)
893
- // if(window.plat != 1){
894
- // primitive.isCompressed = dataView.getInt32(offset, false);
895
- // offset += 4;
896
- // }
897
- primitive.isCompressed = dataView.getInt32(offset, false);
898
- offset += 4;
899
-
900
- if (isFullProps) {
901
- if (dataView.byteLength < offset + 4)
902
- throw new Error('Insufficient data for Position length');
903
- const positionLen = dataView.getUint32(offset, false);
904
- offset += 4;
905
- if (positionLen === 0xffffffff) {
906
- primitive.position = null;
907
- } else {
908
- if (dataView.byteLength < offset + positionLen)
909
- throw new Error('Insufficient data for Position content');
910
- primitive.position = uint8Array.subarray(offset, offset + positionLen);
911
- offset += positionLen;
912
- }
913
- }
914
-
915
- if (isFullProps) {
916
- if (dataView.byteLength < offset + 4) throw new Error('Insufficient data for Normal length');
917
- const normalLen = dataView.getUint32(offset, false);
918
- offset += 4;
919
- if (normalLen === 0xffffffff) {
920
- primitive.normal = null;
921
- } else {
922
- if (dataView.byteLength < offset + normalLen)
923
- throw new Error('Insufficient data for Normal content');
924
- primitive.normal = uint8Array.subarray(offset, offset + normalLen);
925
- offset += normalLen;
926
- }
927
- }
928
-
929
- if (isFullProps) {
930
- if (dataView.byteLength < offset + 4) throw new Error('Insufficient data for Indices length');
931
- const indicesLen = dataView.getUint32(offset, false);
932
- offset += 4;
933
- if (indicesLen === 0xffffffff) {
934
- primitive.indices = null;
935
- } else {
936
- if (dataView.byteLength < offset + indicesLen)
937
- throw new Error('Insufficient data for Indices content');
938
- primitive.indices = uint8Array.subarray(offset, offset + indicesLen);
939
- offset += indicesLen;
940
- }
941
- }
942
-
943
- if (isFullProps) {
944
- if (dataView.byteLength < offset + 4)
945
- throw new Error('Insufficient data for Posindex length');
946
- const posindexLen = dataView.getUint32(offset, false);
947
- offset += 4;
948
- if (posindexLen === 0xffffffff) {
949
- primitive.posindex = null;
950
- } else {
951
- if (dataView.byteLength < offset + posindexLen)
952
- throw new Error('Insufficient data for Posindex content');
953
- primitive.posindex = uint8Array.subarray(offset, offset + posindexLen);
954
- offset += posindexLen;
955
- }
956
- }
957
-
958
- if (isFullProps) {
959
- if (dataView.byteLength < offset + 4)
960
- throw new Error('Insufficient data for Nolindex length');
961
- const nolindexLen = dataView.getUint32(offset, false);
962
- offset += 4;
963
- if (nolindexLen === 0xffffffff) {
964
- primitive.nolindex = null;
965
- } else {
966
- if (dataView.byteLength < offset + nolindexLen)
967
- throw new Error('Insufficient data for Nolindex content');
968
- primitive.nolindex = uint8Array.subarray(offset, offset + nolindexLen);
969
- offset += nolindexLen;
970
- }
971
- }
972
-
973
- if (dataView.byteLength < offset + 4) throw new Error('Insufficient data for DataType length');
974
- const dataTypeLen = dataView.getUint32(offset, false);
975
- offset += 4;
976
- if (dataTypeLen === 0xffffffff) {
977
- primitive.dataType = null;
978
- } else if (dataTypeLen > 0) {
979
- if (dataView.byteLength < offset + dataTypeLen)
980
- throw new Error('Insufficient data for DataType content');
981
- const textBytes = uint8Array.subarray(offset, offset + dataTypeLen);
982
- primitive.dataType = new TextDecoder('utf-8').decode(textBytes);
983
- offset += dataTypeLen;
984
- } else {
985
- primitive.dataType = '';
986
- }
987
-
988
- // console.log('primitive.dataType', primitive.dataType)
989
-
990
- if (dataView.byteLength < offset + 4) throw new Error('Insufficient data for Min count');
991
- const minCount = dataView.getUint32(offset, false);
992
- offset += 4;
993
- if (minCount === 0xffffffff) {
994
- primitive.min = null;
995
- } else {
996
- if (dataView.byteLength < offset + minCount * 8)
997
- throw new Error('Insufficient data for Min values');
998
- primitive.min = [];
999
- for (let i = 0; i < minCount; i++) {
1000
- primitive.min.push(dataView.getFloat64(offset, false));
1001
- offset += 8;
1002
- }
1003
- }
1004
-
1005
- if (dataView.byteLength < offset + 4) throw new Error('Insufficient data for Max count');
1006
- const maxCount = dataView.getUint32(offset, false);
1007
- offset += 4;
1008
- if (maxCount === 0xffffffff) {
1009
- primitive.max = null;
1010
- } else {
1011
- if (dataView.byteLength < offset + maxCount * 8)
1012
- throw new Error('Insufficient data for Max values');
1013
- primitive.max = [];
1014
- for (let i = 0; i < maxCount; i++) {
1015
- primitive.max.push(dataView.getFloat64(offset, false));
1016
- offset += 8;
1017
- }
1018
- }
1019
-
1020
- if (dataView.byteLength < offset + 4) throw new Error('Insufficient data for GeomType');
1021
- primitive.geomType = dataView.getInt32(offset, false);
1022
- offset += 4;
1023
-
1024
- // console.log('primitive offset', primitive, offset)
1025
- return { primitive, offset };
1026
- }
1027
-
1028
- // ----------------------------------------------------------------
1029
- // 交互控制
1030
- // ----------------------------------------------------------------
1031
-
1032
- async ensureNotInteracting(abortSignal) {
1033
- if (abortSignal && abortSignal.aborted) return;
1034
-
1035
- // 如果外部提供了检查函数,优先使用
1036
- if (this.externalEnsureNotInteracting) {
1037
- await this.externalEnsureNotInteracting(abortSignal);
1038
- return;
1039
- }
1040
-
1041
- if (!this.isUserInteracting) return;
1042
- if (!this.parsePausePromise) {
1043
- this.parsePausePromise = new Promise(resolve => {
1044
- this.parsePauseResolver = resolve;
1045
- });
1046
- }
1047
- await this.parsePausePromise;
1048
- }
1049
-
1050
- resumeParsing() {
1051
- if (this.parsePauseResolver) {
1052
- this.parsePauseResolver();
1053
- this.parsePauseResolver = null;
1054
- this.parsePausePromise = null;
1055
- }
1056
- }
1057
-
1058
- handleControlStart() {
1059
- this.controlPressStartAt = Date.now();
1060
- this.isUserInteracting = true;
1061
- }
1062
-
1063
- async handleControlEnd() {
1064
- const pressDuration = Date.now() - (this.controlPressStartAt || 0);
1065
- if (pressDuration >= this.longPressAbortThresholdMs) {
1066
- try {
1067
- // await this.abortAllStreamRequests();
1068
- } catch (_) {}
1069
- }
1070
- this.controlPressStartAt = 0;
1071
- this.isUserInteracting = false;
1072
- this.resumeParsing();
1073
- }
1074
-
1075
- async handleWheelStart() {
1076
- try {
1077
- // await this.abortAllStreamRequests();
1078
- } catch (_) {}
1079
- this.isUserInteracting = true;
1080
- }
1081
-
1082
- handleWheelEnd() {
1083
- this.isUserInteracting = false;
1084
- this.resumeParsing();
1085
- }
1086
-
1087
- // ----------------------------------------------------------------
1088
- // 批量加载与详情获取
1089
- // ----------------------------------------------------------------
1090
-
1091
- async batchLoadRegions(item, divideData) {
1092
- if (!divideData || divideData.length === 0) {
1093
- return;
1094
- }
1095
- try {
1096
- const priorityRegions = [];
1097
- const level0Regions = divideData.filter(
1098
- region => region.lodLevel && region.lodLevel.includes(0)
1099
- );
1100
- const level1Regions = divideData.filter(
1101
- region => region.lodLevel && region.lodLevel.includes(1)
1102
- );
1103
- const level2Regions = divideData.filter(
1104
- region => region.lodLevel && region.lodLevel.includes(2)
1105
- );
1106
- const level3Regions = divideData.filter(
1107
- region => region.lodLevel && region.lodLevel.includes(3)
1108
- );
1109
- const level4Regions = divideData.filter(
1110
- region => region.lodLevel && region.lodLevel.includes(4)
1111
- );
1112
- priorityRegions.push(
1113
- ...level0Regions,
1114
- ...level1Regions,
1115
- ...level2Regions,
1116
- ...level3Regions,
1117
- ...level4Regions
1118
- );
1119
- if (priorityRegions.length === 0) {
1120
- return;
1121
- }
1122
- for (let i = 0; i < priorityRegions.length && i < 1; i++) {
1123
- const regionData = priorityRegions[i];
1124
- const range = { regionIndex: regionData.index, documentId: item.id };
1125
- try {
1126
- await this.fetchJsonSync(item, range);
1127
- } catch (error) {}
1128
- }
1129
- } catch (error) {
1130
- throw error;
1131
- }
1132
- }
1133
-
1134
- async loadModelByIds(options) {
1135
- let { params, onComplete } = options;
1136
- let buffer = null;
1137
- let primitives = [];
1138
- try {
1139
- const loadStartTime = Date.now();
1140
-
1141
- // 构建 documentIdToIds 映射 多个documentid->modelid
1142
- const { ids } = options.params || {};
1143
- const documentIdToIds = this.parseModelDocumentMappings(ids);
1144
- delete params.ids;
1145
- params.documentIdToIds = documentIdToIds;
1146
- params.projectId = this.projectId;
1147
-
1148
- buffer = await this.getPrimitivesByRangeDetail(params);
1149
- if (!buffer) {
1150
- return { primitives: [] };
1151
- }
1152
- const { mesh, primitives: parsed } = await this.parseBufferData(buffer);
1153
- primitives = parsed;
1154
-
1155
- await this.renderModelData(mesh, primitives, params.folderInfo, null, onComplete, params.immediateUpdate ?? true);
1156
-
1157
- return { primitives, mesh };
1158
- } catch (err) {
1159
- if (err.name === 'AbortError') {
1160
- throw err;
1161
- }
1162
- throw err;
1163
- } finally {
1164
- try {
1165
- buffer = null;
1166
- if (primitives && primitives.length > 0) {
1167
- primitives.forEach(primitive => {
1168
- if (primitive && typeof primitive === 'object') {
1169
- Object.keys(primitive).forEach(key => {
1170
- if (Array.isArray(primitive[key]) && primitive[key].length > 100) {
1171
- primitive[key] = null;
1172
- }
1173
- });
1174
- }
1175
- });
1176
- }
1177
- primitives = null;
1178
- if (typeof window !== 'undefined' && window.gc) {
1179
- window.gc();
1180
- }
1181
- } catch (cleanupErr) {}
1182
- }
1183
- }
1184
-
1185
- // ----------------------------------------------------------------
1186
- // API 包装
1187
- // ----------------------------------------------------------------
1188
-
1189
- async getPrimitivesByDivide(params) {
1190
- const res = await this.modelApi.getPrimitivesByDivide(params);
1191
- if (res) return res;
1192
- throw new Error('Failed to get primitives');
1193
- }
1194
-
1195
- async getPrimitivesByRangeDetail(params) {
1196
- const res = await this.modelApi.getPrimitivesByRangeDetail(params);
1197
- if (res) return res;
1198
- throw new Error('Failed to get batch model');
1199
- }
1200
-
1201
- // ----------------------------------------------------------------
1202
- // 其他
1203
- // ----------------------------------------------------------------
1204
-
1205
- async getSceneBox({id, projectId = this.projectId}) {
1206
- const res = await this.modelApi.getSceneBox({ id, projectId });
1207
- if (res && res[0]) {
1208
- this.sceneBox = res[0];
1209
- return res[0];
1210
- } else {
1211
- return null;
1212
- }
1213
- }
1214
-
1215
- async getBox({id, projectId = this.projectId || 0}) {
1216
- // 1. 尝试从 IndexedDB 读取
1217
- const cached = await this._getFromDB(id, projectId);
1218
- if (cached) {
1219
- return cached;
1220
- }
1221
-
1222
- // 2. 正常请求
1223
- const res = await this.modelApi.getBox({ id, projectId });
1224
- if (res) {
1225
- // 3. 存入 IndexedDB (存储原始数据)
1226
- this._applyPrefixId(res, 'id', id);
1227
-
1228
- this._saveToDB(id, res, projectId);
1229
- return res;
1230
- } else {
1231
- return null;
1232
- }
1233
- }
1234
-
1235
- async handleCameraControlForStream(args) {
1236
- // 性能优化注释:禁用 range 被注释字段的解构声明与初始化
1237
- const {
1238
- chainRequest,
1239
- loadedModels,
1240
- // max,
1241
- // min,
1242
- // viewportHeight,
1243
- // viewportWidth,
1244
- // formula,
1245
- // cameraWorldPosition,
1246
- // lodLevel,
1247
- // fovY,
1248
- // vertices,
1249
- // distance,
1250
- // highSSE,
1251
- // sseValue,
1252
- // isAllType,
1253
- // queryIds,
1254
- // sceneBox,
1255
- } = args;
1256
-
1257
- // 构建 documentIdToIds 映射 多个documentid->modelid
1258
- // 性能优化:提前构建映射,避免后续重复遍历 loadedModels
1259
- const documentIdToIds = this.parseModelDocumentMappings(loadedModels);
1260
-
1261
- // 获取当前模型资源(通常由外部注入或设置)
1262
- const activeItems = [];
1263
- // const activeMaterialData = [];
1264
- // const loadedDocIds = new Set(); // 性能优化:不再需要单独的 Set
1265
-
1266
- // 从注册表中收集数据
1267
- // 性能优化:直接使用 documentIdToIds 的 keys,避免重复解析 loadedModels
1268
- Object.keys(documentIdToIds).forEach(docId => {
1269
- const record = this.modelRegistry.get(docId);
1270
- if (record) {
1271
- activeItems.push(record.item);
1272
- // if (record.materialData) {
1273
- // if (Array.isArray(record.materialData)) {
1274
- // activeMaterialData.push(...record.materialData);
1275
- // } else {
1276
- // activeMaterialData.push(record.materialData);
1277
- // }
1278
- // }
1279
- }
1280
- });
1281
-
1282
- if (activeItems.length === 0) {
1283
- return;
1284
- }
1285
-
1286
- const primaryItem = activeItems[0]; // 使用第一个item作为主上下文
1287
-
1288
- const requestId = Date.now() + '_' + Math.random().toString(36).substr(2, 9);
1289
-
1290
- if (!chainRequest && this.currentAbortController) {
1291
- this.currentAbortController.abort();
1292
- this.currentAbortController = null;
1293
- }
1294
-
1295
- this.currentAbortController = new AbortController();
1296
- this.currentRequestId = requestId;
1297
-
1298
- // 性能优化注释:禁用 range 被注释字段的坐标拆解计算
1299
- // const { x: coordinateX, y: coordinateY, z: coordinateZ } = cameraWorldPosition;
1300
- // const documentIdToIds = {
1301
- // "a8d4e49c-874c-4a11-9d72-13e8d0a6354e": [614]
1302
- // }
1303
- // 性能优化注释:禁用 range 中除 documentIdToIds 外的所有字段
1304
- const range = {
1305
- // minX: min.x,
1306
- // maxX: max.x,
1307
- // minY: min.y,
1308
- // maxY: max.y,
1309
- // maxZ: max.z,
1310
- // minZ: min.z,
1311
- // viewportHeight,
1312
- // formula,
1313
- // coordinateX,
1314
- // coordinateY,
1315
- // coordinateZ,
1316
- // fovY,
1317
- // viewportWidth,
1318
- documentIdToIds,
1319
- // distance,
1320
- // highSSE,
1321
- // sseValue,
1322
- // isAllType,
1323
- // queryIds,
1324
- // sceneBox,
1325
- // ...vertices,
1326
- };
1327
-
1328
- // 性能优化注释:禁用基于 sceneBox 的 range 二次裁剪逻辑
1329
- // const boxToUse = sceneBox || this.sceneBox;
1330
- // if (boxToUse) {
1331
- // range.minX = Math.max(range.minX, boxToUse.min_x);
1332
- // range.maxX = Math.min(range.maxX, boxToUse.max_x);
1333
- // range.minY = Math.max(range.minY, boxToUse.min_y);
1334
- // range.maxY = Math.min(range.maxY, boxToUse.max_y);
1335
- // range.minZ = Math.max(range.minZ, boxToUse.min_z);
1336
- // range.maxZ = Math.min(range.maxZ, boxToUse.max_z);
1337
- // }
1338
-
1339
- try {
1340
- await this.fetchJsonStream(primaryItem, range, this.currentAbortController.signal, requestId);
1341
- if (this.currentRequestId === requestId) {
1342
- this.currentAbortController = null;
1343
- this.currentRequestId = null;
1344
- } else {
1345
- }
1346
- } catch (error) {
1347
- if (error.name === 'AbortError') {
1348
- return;
1349
- }
1350
- if (this.currentRequestId === requestId) {
1351
- this.currentAbortController = null;
1352
- this.currentRequestId = null;
1353
- }
1354
- }
1355
- }
1356
-
1357
- updateModelRegistry(item, materialData, sceneBox, boxIndex) {
1358
- if (item && item.id) {
1359
- this.modelRegistry.set(item.id, {
1360
- item,
1361
- materialData,
1362
- sceneBox,
1363
- boxIndex,
1364
- });
1365
- }
1366
- }
1367
-
1368
- /**
1369
- * 将复合 ID 数组解析为 documentId 到 modelId 列表的映射
1370
- * @param {Array<string>} loadedModels - 复合 ID 数组 (如 ["288:docId"])
1371
- * @returns {Object} - 映射对象 (如 {"docId": [288]})
1372
- */
1373
- parseModelDocumentMappings(loadedModels) {
1374
- const acc = {};
1375
- if (!loadedModels || !Array.isArray(loadedModels)) return acc;
1376
-
1377
- // 性能优化:使用 for 循环代替 reduce,并优化 parseInt 调用
1378
- for (let i = 0, len = loadedModels.length; i < len; i++) {
1379
- const str = loadedModels[i];
1380
- if (typeof str !== 'string') continue;
1381
-
1382
- const firstIndex = str.indexOf(':');
1383
- if (firstIndex !== -1) {
1384
- // 优化:直接解析整数,避免 substring 创建临时字符串
1385
- const modelId = parseInt(str, 10);
1386
- if (!isNaN(modelId)) {
1387
- const docId = str.substring(firstIndex + 1);
1388
- if (docId) {
1389
- if (!acc[docId]) {
1390
- acc[docId] = [];
1391
- }
1392
- // if(modelId == 613 || modelId == 614){
1393
- // acc[docId].push(modelId);
1394
- // } // TODO 608 603 模型单独处理
1395
- acc[docId].push(modelId);
1396
- }
1397
- }
1398
- }
1399
- }
1400
- return acc;
1401
- }
1402
-
1403
- async processModelItem(item, options = {}) {
1404
- try {
1405
- if(typeof item.id !== 'string'){
1406
- item.id = "" + item.id;
1407
- }
1408
-
1409
- // 获取材质数据
1410
- const materialData = await this.getPrimitivesByMaterial({ id: item.id, projectId: this.projectId });
1411
- this._applyPrefixId(materialData, 'id');
1412
-
1413
- // 获取场景包围盒
1414
- const sceneBox = await this.getSceneBox({ id: item.id, projectId: this.projectId });
1415
- this.sceneBox = sceneBox;
1416
-
1417
- // 获取BoxIndex
1418
- const boxIndex = await this.getBox({ id: item.id, projectId: this.projectId });
1419
- this.boxIndex = boxIndex;
1420
-
1421
- const modelResourceMapObj = { materialData, item };
1422
-
1423
- // 设置当前模型到注册表
1424
- this.updateModelRegistry(item, materialData, sceneBox, boxIndex);
1425
- console.log('modelRegistry', this.modelRegistry)
1426
-
1427
- return { modelResourceMap: modelResourceMapObj, sceneBox, boxIndex, item };
1428
- } catch (error) {
1429
- return null;
1430
- }
1431
- }
1432
-
1433
- // ----------------------------------------------------------------
1434
- // IndexedDB 缓存支持
1435
- // ----------------------------------------------------------------
1436
-
1437
- _initDB() {
1438
- if (this._dbReady) return this._dbReady;
1439
- this._dbReady = new Promise((resolve, reject) => {
1440
- if (typeof indexedDB === 'undefined') {
1441
- resolve(null);
1442
- return;
1443
- }
1444
- // 打开数据库
1445
- const request = indexedDB.open('StreamLoaderDB', 1);
1446
- request.onerror = () => {
1447
- console.warn('IndexedDB open failed');
1448
- resolve(null);
1449
- };
1450
- request.onsuccess = () => resolve(request.result);
1451
- request.onupgradeneeded = (event) => {
1452
- const db = event.target.result;
1453
- if (!db.objectStoreNames.contains('boxCache')) {
1454
- db.createObjectStore('boxCache');
1455
- }
1456
- };
1457
- });
1458
- return this._dbReady;
1459
- }
1460
-
1461
- async _getFromDB(id, projectId = this.projectId || 0) {
1462
- try {
1463
- const db = await this._initDB();
1464
- if (!db) return null;
1465
- return new Promise((resolve) => {
1466
- const transaction = db.transaction(['boxCache'], 'readonly');
1467
- const store = transaction.objectStore('boxCache');
1468
- const request = store.get(`${projectId}:${id}`);
1469
- request.onsuccess = () => resolve(request.result);
1470
- request.onerror = () => resolve(null);
1471
- });
1472
- } catch (e) {
1473
- console.warn('Error reading from IndexedDB:', e);
1474
- return null;
1475
- }
1476
- }
1477
-
1478
- async _saveToDB(id, data, projectId = this.projectId || 0) {
1479
- try {
1480
- const db = await this._initDB();
1481
- if (!db) return;
1482
- const transaction = db.transaction(['boxCache'], 'readwrite');
1483
- const store = transaction.objectStore('boxCache');
1484
- store.put(data, `${projectId}:${id}`);
1485
- } catch (e) {
1486
- console.warn('Error saving to IndexedDB:', e);
1487
- }
1488
- }
1489
- }
1
+ import { gunzipSync } from 'fflate';
2
+ import StreamLoaderParserWorker from './StreamLoaderParser.worker.js';
3
+
4
+ /**
5
+ * StreamLoader 类
6
+ * 封装流式加载、解析与渲染调度的核心逻辑
7
+ */
8
+ export class StreamLoader {
9
+ constructor(config = {}) {
10
+ // 依赖注入
11
+ this.modelApi = config.modelApi;
12
+ this.projectId = config.projectId || '';
13
+
14
+ this.createStreamRequestId =
15
+ config.createStreamRequestId ||
16
+ (() => `stream_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`);
17
+ this.onCancelRequestId = config.onCancelRequestId;
18
+ this.batchSize = config.batchSize || 20;
19
+
20
+ // 钩子函数
21
+ this.renderModelData = config.renderModelData || (async () => {});
22
+ // 外部提供的交互状态检查函数(可选)
23
+ this.externalEnsureNotInteracting = config.ensureNotInteracting;
24
+ this.prefixIdKey = config.prefixIdKey || 'documentId';
25
+ this.debug = config.debug || false;
26
+
27
+ // 内部状态管理
28
+ this.activeRequests = new Map();
29
+ this.requestCounter = 0;
30
+
31
+ this.currentAbortController = null;
32
+ this.currentStreamReader = null;
33
+ this.activeStreamReaders = new Set();
34
+ this.activeStreamControllers = new Set();
35
+ this.activeRequestIds = new Set();
36
+ this.currentRequestId = null;
37
+
38
+ this.isUserInteracting = false;
39
+ this.parsePausePromise = null;
40
+ this.parsePauseResolver = null;
41
+
42
+ this.controlPressStartAt = 0;
43
+ this.longPressAbortThresholdMs = 250;
44
+
45
+ this.sceneBox = null;
46
+ this.boxIndex = null;
47
+ this.modelRegistry = new Map();
48
+
49
+ // Initialize worker
50
+ try {
51
+ this.worker = new StreamLoaderParserWorker();
52
+ // this.worker = null; // TODO
53
+ } catch (e) {
54
+ this.worker = null;
55
+ }
56
+ this.workerRequestMap = new Map();
57
+ this.workerMessageHandler = null;
58
+ if (this.worker) {
59
+ this.workerMessageHandler = e => {
60
+ const payload = e.data || {};
61
+ const pending = this.workerRequestMap.get(payload.id);
62
+ if (!pending) return;
63
+ this.workerRequestMap.delete(payload.id);
64
+ if (payload.type === 'success') {
65
+ pending.resolve(payload.result);
66
+ } else {
67
+ pending.reject(new Error(payload.error));
68
+ }
69
+ };
70
+ this.worker.addEventListener('message', this.workerMessageHandler);
71
+ }
72
+ }
73
+
74
+ /**
75
+ * 为对象或对象数组应用prefixId前缀
76
+ * @param {Object|Array<Object>} target - 单个对象或对象数组
77
+ * @param {string} propKey - 需要应用prefixId前缀的属性键名
78
+ * @param {string} customPrefixId - 自定义prefixId,可选
79
+ */
80
+ _applyPrefixId(target, propKey, customPrefixId) {
81
+ if (!this.prefixIdKey || !target) return;
82
+
83
+ if (Array.isArray(target)) {
84
+ target.forEach(item => {
85
+ const prefixId = customPrefixId || item[this.prefixIdKey];
86
+ if (item && prefixId && item[propKey] != null) {
87
+ item[propKey] = `${item[propKey]}:${prefixId}`;
88
+ }
89
+ });
90
+ } else if ((customPrefixId || target[this.prefixIdKey]) && target[propKey] != null) {
91
+ const prefixId = customPrefixId || target[this.prefixIdKey];
92
+ target[propKey] = `${target[propKey]}:${prefixId}`;
93
+ }
94
+ }
95
+
96
+ /**
97
+ * 将合并材质数据覆盖到原材质数据中(仅覆盖 merge 中明确提供的字段)
98
+ * 规则:mergeMaterialData id materialData 出现过的,覆盖其 color/transp
99
+ * 注意:应在 _applyPrefixId(materialData, 'id') 之前调用(使用原始 id 对比)
100
+ * @param {Array<Object>|Object|null} materialData
101
+ * @param {Array<Object>|Object|null} mergeMaterialData
102
+ * @returns {Array<Object>|Object|null} materialData(原地修改后返回)
103
+ */
104
+ _mergeMaterialDataOverrides(materialData, mergeMaterialData) {
105
+ if (!materialData || !mergeMaterialData) return materialData;
106
+
107
+ const materials = Array.isArray(materialData) ? materialData : [materialData];
108
+ const merges = Array.isArray(mergeMaterialData) ? mergeMaterialData : [mergeMaterialData];
109
+ if (materials.length === 0 || merges.length === 0) return materialData;
110
+
111
+ const mergeById = new Map();
112
+ for (let i = 0; i < merges.length; i++) {
113
+ const m = merges[i];
114
+ if (!m || m.id == null) continue;
115
+ mergeById.set(String(m.id), m);
116
+ }
117
+ if (mergeById.size === 0) return materialData;
118
+
119
+ for (let i = 0; i < materials.length; i++) {
120
+ const mat = materials[i];
121
+ if (!mat || mat.id == null) continue;
122
+ const merge = mergeById.get(String(mat.id));
123
+ if (!merge) continue;
124
+
125
+ if (merge.color !== undefined) {
126
+ mat.color = merge.color;
127
+ }
128
+ if (merge.transp !== undefined) {
129
+ mat.transp = merge.transp;
130
+ }
131
+ }
132
+
133
+ return materialData;
134
+ }
135
+
136
+ workerRequest(type, data, transferable = []) {
137
+ if (!this.worker) {
138
+ return Promise.reject(new Error('Worker is not initialized'));
139
+ }
140
+ return new Promise((resolve, reject) => {
141
+ const id = this.requestCounter++;
142
+ this.workerRequestMap.set(id, { resolve, reject });
143
+ this.worker.postMessage({ id, type, data }, transferable);
144
+ });
145
+ }
146
+
147
+ // ----------------------------------------------------------------
148
+ // 核心解析逻辑
149
+ // ----------------------------------------------------------------
150
+
151
+ async parseStreamImmediate(reader, list, range, abortSignal = null, streamId = null) {
152
+ const batchMeshes = [];
153
+ const batchPrimitives = [];
154
+ const batchSize = this.batchSize;
155
+ let batchStart = 0;
156
+
157
+ const ensureNotAborted = () => {
158
+ if (abortSignal && abortSignal.aborted) {
159
+ return reader.cancel().catch(() => {});
160
+ // throw new DOMException('Request was aborted', 'AbortError');
161
+ }
162
+ return null;
163
+ };
164
+
165
+ if (this.worker) {
166
+ const localStreamId = streamId || this.createStreamRequestId();
167
+ const ensureNotInteracting = () => {
168
+ if (!this.externalEnsureNotInteracting && !this.isUserInteracting) return null;
169
+ return this.ensureNotInteracting(abortSignal);
170
+ };
171
+
172
+ await this.workerRequest('streamInit', {
173
+ streamId: localStreamId,
174
+ prefixIdKey: this.prefixIdKey,
175
+ });
176
+
177
+ try {
178
+ if (this.debug) performance.mark('while-start');
179
+ while (true) {
180
+ const abortPromise = ensureNotAborted();
181
+ if (abortPromise) await abortPromise;
182
+ const interactionPromise = ensureNotInteracting();
183
+ if (interactionPromise) await interactionPromise;
184
+
185
+ let content;
186
+ try {
187
+ content = await reader.read();
188
+ } catch (e) {
189
+ if (e && e.name === 'AbortError') {
190
+ throw e;
191
+ }
192
+ throw e;
193
+ }
194
+
195
+ const { done, value } = content;
196
+ if (done) {
197
+ const abortPromiseOnDone = ensureNotAborted();
198
+ if (abortPromiseOnDone) await abortPromiseOnDone;
199
+ if (this.debug) {
200
+ performance.mark('while-end');
201
+ performance.measure('while', 'while-start', 'while-end');
202
+ }
203
+
204
+ const flushed = await this.workerRequest('streamFlush', { streamId: localStreamId });
205
+ if (flushed?.meshes?.length) {
206
+ batchMeshes.push(...flushed.meshes);
207
+ batchPrimitives.push(...flushed.primitives);
208
+ }
209
+
210
+ while (batchMeshes.length - batchStart >= batchSize) {
211
+ const abortPromiseInBatch = ensureNotAborted();
212
+ if (abortPromiseInBatch) await abortPromiseInBatch;
213
+ const interactionPromiseInBatch = ensureNotInteracting();
214
+ if (interactionPromiseInBatch) await interactionPromiseInBatch;
215
+ const meshesToProcess = batchMeshes.slice(batchStart, batchStart + batchSize);
216
+ const primitivesToProcess = batchPrimitives.slice(batchStart, batchStart + batchSize);
217
+ batchStart += batchSize;
218
+ this.processBatchData(meshesToProcess, primitivesToProcess, list, range, abortSignal);
219
+ }
220
+
221
+ if (batchMeshes.length - batchStart > 0) {
222
+ const abortPromiseInBatch = ensureNotAborted();
223
+ if (abortPromiseInBatch) await abortPromiseInBatch;
224
+ const interactionPromiseInBatch = ensureNotInteracting();
225
+ if (interactionPromiseInBatch) await interactionPromiseInBatch;
226
+ const meshesToProcess = batchMeshes.slice(batchStart);
227
+ const primitivesToProcess = batchPrimitives.slice(batchStart);
228
+ batchStart = batchMeshes.length;
229
+ this.processBatchData(meshesToProcess, primitivesToProcess, list, range, abortSignal);
230
+ }
231
+ break;
232
+ }
233
+
234
+ const transferable = value?.buffer instanceof ArrayBuffer ? [value.buffer] : [];
235
+ const parsed = await this.workerRequest(
236
+ 'streamPush',
237
+ {
238
+ streamId: localStreamId,
239
+ chunk: value.buffer,
240
+ byteOffset: value.byteOffset,
241
+ byteLength: value.byteLength,
242
+ },
243
+ transferable
244
+ );
245
+
246
+ if (parsed?.meshes?.length) {
247
+ batchMeshes.push(...parsed.meshes);
248
+ batchPrimitives.push(...parsed.primitives);
249
+ }
250
+
251
+ while (batchMeshes.length - batchStart >= batchSize) {
252
+ const abortPromiseInBatch = ensureNotAborted();
253
+ if (abortPromiseInBatch) await abortPromiseInBatch;
254
+ const interactionPromiseInBatch = ensureNotInteracting();
255
+ if (interactionPromiseInBatch) await interactionPromiseInBatch;
256
+ const meshesToProcess = batchMeshes.slice(batchStart, batchStart + batchSize);
257
+ const primitivesToProcess = batchPrimitives.slice(batchStart, batchStart + batchSize);
258
+ batchStart += batchSize;
259
+ this.processBatchData(meshesToProcess, primitivesToProcess, list, range, abortSignal);
260
+ }
261
+
262
+ if (batchStart >= batchSize * 4) {
263
+ batchMeshes.splice(0, batchStart);
264
+ batchPrimitives.splice(0, batchStart);
265
+ batchStart = 0;
266
+ }
267
+ }
268
+ } finally {
269
+ try {
270
+ await this.workerRequest('streamDispose', { streamId: localStreamId });
271
+ } catch (e) {}
272
+ }
273
+ return;
274
+ }
275
+
276
+ const decoder = new TextDecoder('utf-8');
277
+ let buffer = new Uint8Array();
278
+
279
+ const pendingPrimitives = new Map();
280
+ let expectingPrimitive = true;
281
+ let isFullProps = true;
282
+
283
+ while (true) {
284
+ await ensureNotAborted();
285
+
286
+ await this.ensureNotInteracting(abortSignal);
287
+
288
+ let content;
289
+ try {
290
+ content = await reader.read();
291
+ } catch (e) {
292
+ if (e && e.name === 'AbortError') {
293
+ throw e;
294
+ }
295
+ throw e;
296
+ }
297
+
298
+ const { done, value } = content;
299
+ if (done) {
300
+ await ensureNotAborted();
301
+
302
+ if (batchMeshes.length > 0) {
303
+ await ensureNotAborted();
304
+ await this.ensureNotInteracting(abortSignal);
305
+ await this.processBatchData(batchMeshes, batchPrimitives, list, range, abortSignal);
306
+ }
307
+ break;
308
+ }
309
+
310
+ const newBuf = new Uint8Array(buffer.length + value.length);
311
+ newBuf.set(buffer);
312
+ newBuf.set(value, buffer.length);
313
+ buffer = newBuf;
314
+
315
+ while (buffer.length > 0) {
316
+ await ensureNotAborted();
317
+ if (expectingPrimitive) {
318
+ if (buffer.length < 12) break;
319
+
320
+ try {
321
+ const dataView = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
322
+ const primitiveResult = this.parsePrimitive(dataView, buffer, 0, isFullProps);
323
+ let primitiveData = primitiveResult.primitive;
324
+
325
+ this._applyPrefixId(primitiveData, 'id');
326
+ this._applyPrefixId(primitiveData, 'material');
327
+ const consumedBytes = primitiveResult.offset;
328
+
329
+ if (isFullProps) {
330
+ const { position, normal, posindex, nolindex, indices } =
331
+ this.parsePrimitiveData(primitiveData);
332
+ const formatted = this.formatPrimitiveData({ position, normal, posindex, nolindex });
333
+
334
+ delete primitiveData.nolindex;
335
+ delete primitiveData.posindex;
336
+
337
+ primitiveData.position = formatted.position;
338
+ primitiveData.normal = formatted.normal;
339
+ primitiveData.indices = indices;
340
+ }
341
+ const docId = primitiveData.id;
342
+
343
+ pendingPrimitives.set(docId, primitiveData);
344
+
345
+ expectingPrimitive = false;
346
+ buffer = buffer.slice(consumedBytes);
347
+ } catch (e) {
348
+ break;
349
+ }
350
+ } else {
351
+ if (buffer.length < 4) break;
352
+
353
+ const length = new DataView(buffer.buffer, buffer.byteOffset).getUint32(0, false);
354
+
355
+ if (length > 10 * 1024 * 1024) {
356
+ expectingPrimitive = true;
357
+ continue;
358
+ }
359
+
360
+ const totalLen = 4 + length;
361
+
362
+ if (buffer.length < totalLen) break;
363
+
364
+ const data = buffer.slice(4, totalLen);
365
+ let mesh = null;
366
+
367
+ try {
368
+ const jsonStr = decoder.decode(data);
369
+ mesh = JSON.parse(jsonStr);
370
+ } catch (e) {
371
+ expectingPrimitive = true;
372
+ continue;
373
+ }
374
+
375
+ if (mesh) {
376
+ isFullProps = true;
377
+
378
+ this._applyPrefixId(mesh, 'id');
379
+ if (Array.isArray(mesh)) {
380
+ mesh.forEach(item => {
381
+ this._applyPrefixId(item.primitives, 'prmid', item.documentId);
382
+ });
383
+ } else {
384
+ this._applyPrefixId(mesh.primitives, 'prmid', mesh.documentId);
385
+ }
386
+
387
+ let docId = mesh.id;
388
+ const meshToPrimId = mesh.primitives.map(item => item.prmid);
389
+ const hasPendingPrimitive = meshToPrimId.some(item => pendingPrimitives.has(item));
390
+ const hasPendingMesh = pendingPrimitives.has(docId);
391
+
392
+ if (hasPendingMesh || hasPendingPrimitive) {
393
+ let primitiveData = pendingPrimitives.get(docId);
394
+ if (!hasPendingMesh && hasPendingPrimitive) {
395
+ const primId = meshToPrimId[0];
396
+ primitiveData = pendingPrimitives.get(primId);
397
+ }
398
+
399
+ batchMeshes.push(mesh);
400
+ batchPrimitives.push(primitiveData);
401
+
402
+ if (batchMeshes.length >= this.batchSize) {
403
+ await ensureNotAborted();
404
+ await this.ensureNotInteracting(abortSignal);
405
+ await this.processBatchData(batchMeshes, batchPrimitives, list, range, abortSignal);
406
+
407
+ batchMeshes.length = 0;
408
+ batchPrimitives.length = 0;
409
+ }
410
+ }
411
+
412
+ expectingPrimitive = false;
413
+ buffer = buffer.slice(totalLen);
414
+ }
415
+ }
416
+ }
417
+ }
418
+ }
419
+
420
+ // ----------------------------------------------------------------
421
+ // 数据处理与渲染
422
+ // ----------------------------------------------------------------
423
+
424
+ processBatchData(meshes, primitives, list, range, abortSignal = null) {
425
+ try {
426
+ if (abortSignal && abortSignal.aborted) {
427
+ throw new DOMException('Request was aborted', 'AbortError');
428
+ }
429
+ this.renderModelData(meshes, primitives, list, range); // TODO
430
+ } catch (error) {
431
+ console.error('Failed to render batch data:', error);
432
+ }
433
+ }
434
+
435
+ // ----------------------------------------------------------------
436
+ // 请求管理
437
+ // ----------------------------------------------------------------
438
+
439
+ async getPrimitivesByRangeStream(params, list, range, abortSignal = null) {
440
+ await this.abortAllStreamRequests();
441
+
442
+ const requestId = this.createStreamRequestId();
443
+ let internalController = null;
444
+ let reader = null;
445
+
446
+ try {
447
+ internalController = new AbortController();
448
+ this.currentAbortController = internalController;
449
+ this.activeStreamControllers.add(internalController);
450
+ this.activeRequestIds.add(requestId);
451
+
452
+ if (abortSignal) {
453
+ if (abortSignal.aborted) {
454
+ // internalController.abort(); // TODO
455
+ } else {
456
+ abortSignal.addEventListener('abort', () => internalController.abort(), { once: true });
457
+ }
458
+ }
459
+
460
+ if (internalController.signal.aborted) {
461
+ throw new DOMException('Request was aborted', 'AbortError');
462
+ }
463
+ params.requestId = requestId;
464
+ params.projectId = this.projectId;
465
+ let res;
466
+ if (this.modelApi && typeof this.modelApi.getPrimitivesByRangeStream === 'function') {
467
+ res = await this.modelApi.getPrimitivesByRangeStream(params, internalController.signal);
468
+ } else {
469
+ throw new Error('modelApi.getPrimitivesByRangeStream is not available');
470
+ }
471
+
472
+ reader = res.getReader();
473
+ this.currentStreamReader = reader;
474
+ this.activeStreamReaders.add(reader);
475
+
476
+ await this.parseStreamImmediate(reader, list, range, internalController.signal, requestId);
477
+
478
+ if (res) {
479
+ return res;
480
+ }
481
+ return null;
482
+ } catch (error) {
483
+ if (error.name === 'AbortError') {
484
+ throw error;
485
+ }
486
+ console.error(error);
487
+ throw error;
488
+ } finally {
489
+ if (reader) this.activeStreamReaders.delete(reader);
490
+ if (internalController) this.activeStreamControllers.delete(internalController);
491
+ if (requestId) this.activeRequestIds.delete(requestId);
492
+ this.currentStreamReader = null;
493
+ this.currentAbortController = null;
494
+ }
495
+ }
496
+
497
+ async getPrimitivesByRange(params, abortSignal = null) {
498
+ try {
499
+ if (abortSignal && abortSignal.aborted) {
500
+ throw new DOMException('Request was aborted', 'AbortError');
501
+ }
502
+
503
+ const res = await this.modelApi.getPrimitivesByRange(params, abortSignal);
504
+
505
+ if (abortSignal && abortSignal.aborted) {
506
+ throw new DOMException('Request was aborted', 'AbortError');
507
+ }
508
+
509
+ if (res) {
510
+ return res;
511
+ } else {
512
+ throw new Error('Failed to get primitives');
513
+ }
514
+ } catch (error) {
515
+ if (error.name === 'AbortError') {
516
+ throw error;
517
+ }
518
+ throw error;
519
+ }
520
+ }
521
+
522
+ async getPrimitivesByMaterial(params) {
523
+ try {
524
+ const res = await this.modelApi.getPrimitivesByMaterial(params);
525
+ if (res) {
526
+ return res;
527
+ }
528
+ throw new Error('Failed to get Material');
529
+ } catch (error) {
530
+ throw error;
531
+ }
532
+ }
533
+
534
+ async getPrimitivesByMergeMaterial(params) {
535
+ try {
536
+ if (!this.modelApi || typeof this.modelApi.getPrimitivesByMergeMaterial !== 'function') {
537
+ return null;
538
+ }
539
+
540
+ const res = await this.modelApi.getPrimitivesByMergeMaterial(params);
541
+ return res || null;
542
+ } catch (error) {
543
+ // mergeMaterial 是可选能力:接口异常时降级为不合并
544
+ return null;
545
+ }
546
+ }
547
+
548
+ createAbortableRequest(requestKey = null) {
549
+ const requestId = requestKey || `req_${++this.requestCounter}`;
550
+
551
+ if (this.activeRequests.has(requestId)) {
552
+ const existingController = this.activeRequests.get(requestId);
553
+ existingController.abort();
554
+ }
555
+
556
+ const abortController = new AbortController();
557
+
558
+ this.activeRequests.set(requestId, abortController);
559
+ return {
560
+ requestId,
561
+ signal: abortController.signal,
562
+ abort: () => {
563
+ abortController.abort();
564
+ this.activeRequests.delete(requestId);
565
+ },
566
+ };
567
+ }
568
+
569
+ cleanupRequest(requestId) {
570
+ if (this.activeRequests.has(requestId)) {
571
+ this.activeRequests.delete(requestId);
572
+ }
573
+ }
574
+
575
+ async abortAllStreamRequests() {
576
+ try {
577
+ const requestIds = Array.from(this.activeRequestIds);
578
+ for (const requestId of requestIds) {
579
+ if (typeof this.onCancelRequestId === 'function') {
580
+ try {
581
+ await this.onCancelRequestId(requestId);
582
+ } catch (e) {}
583
+ }
584
+ }
585
+ this.activeRequestIds.clear();
586
+
587
+ this.activeStreamControllers.forEach(controller => {
588
+ try {
589
+ controller.abort();
590
+ } catch (e) {}
591
+ });
592
+ this.activeStreamControllers.clear();
593
+
594
+ const readers = Array.from(this.activeStreamReaders);
595
+ this.activeStreamReaders.clear();
596
+ for (const reader of readers) {
597
+ try {
598
+ await reader.cancel();
599
+ } catch (e) {}
600
+ }
601
+ } finally {
602
+ if (this.currentAbortController) {
603
+ try {
604
+ this.currentAbortController.abort();
605
+ } catch (e) {}
606
+ this.currentAbortController = null;
607
+ }
608
+ if (this.currentStreamReader) {
609
+ try {
610
+ await this.currentStreamReader.cancel();
611
+ } catch (e) {}
612
+ this.currentStreamReader = null;
613
+ }
614
+ }
615
+ }
616
+
617
+ // ----------------------------------------------------------------
618
+ // 辅助加载方法
619
+ // ----------------------------------------------------------------
620
+
621
+ async fetchPrimitiveBufferByStream(range, list, abortSignal = null, requestId = null) {
622
+ try {
623
+ await this.abortAllStreamRequests();
624
+ const request = this.createAbortableRequest(requestId); // Use requestId if provided as key? Logic slightly different in utils
625
+
626
+ // Adaptation: utils used getPrimitivesByRangeStreamWithAutoAbort which calls getPrimitivesByRangeStream
627
+ // Here I simplify by calling getPrimitivesByRangeStream directly but managing request key
628
+
629
+ const buffer = await this.getPrimitivesByRangeStream(
630
+ range,
631
+ list,
632
+ range,
633
+ abortSignal || request.signal
634
+ );
635
+
636
+ this.cleanupRequest(request.requestId);
637
+ if (!buffer) return null;
638
+ return buffer;
639
+ } catch (error) {
640
+ if (error.name === 'AbortError') {
641
+ // throw error;
642
+ }
643
+ if (requestId && this.currentRequestId !== requestId) {
644
+ return null;
645
+ }
646
+ // throw error;
647
+ }
648
+ }
649
+
650
+ async fetchPrimitiveBuffer(range, abortSignal = null, requestId = null) {
651
+ try {
652
+ const request = this.createAbortableRequest(requestId || 'default');
653
+ const buffer = await this.getPrimitivesByRange(range, abortSignal || request.signal);
654
+ this.cleanupRequest(request.requestId);
655
+
656
+ if (!buffer || buffer.byteLength === 0) return null;
657
+ if (buffer.byteLength < 4) return null;
658
+ return buffer;
659
+ } catch (error) {
660
+ if (error.name === 'AbortError') {
661
+ throw error;
662
+ }
663
+ if (requestId && this.currentRequestId !== requestId) {
664
+ return null;
665
+ }
666
+ throw error;
667
+ }
668
+ }
669
+
670
+ async parseBufferData(buffer) {
671
+ if (this.worker) {
672
+ let transferable = [];
673
+ if (buffer instanceof ArrayBuffer) {
674
+ transferable.push(buffer);
675
+ } else if (buffer.buffer instanceof ArrayBuffer) {
676
+ transferable.push(buffer.buffer);
677
+ }
678
+
679
+ return this.workerRequest(
680
+ 'parseBufferData',
681
+ { buffer, prefixIdKey: this.prefixIdKey },
682
+ transferable
683
+ );
684
+ } else {
685
+ return this._parseBufferDataSync(buffer);
686
+ }
687
+ }
688
+
689
+ async _parseBufferDataSync(buffer) {
690
+ let uint8Array = null;
691
+ let dataView = null;
692
+ let meshJsonBytes = null;
693
+ let primitives = [];
694
+
695
+ try {
696
+ uint8Array = new Uint8Array(buffer);
697
+ dataView = new DataView(buffer);
698
+
699
+ let offset = 0;
700
+ const meshJsonLength = dataView.getInt32(offset, false);
701
+ offset += 4;
702
+
703
+ meshJsonBytes = new Uint8Array(uint8Array.buffer.slice(offset, offset + meshJsonLength));
704
+ offset += meshJsonLength;
705
+
706
+ const decoder = new TextDecoder('utf-8');
707
+ const meshJsonStr = decoder.decode(meshJsonBytes);
708
+ meshJsonBytes = null;
709
+
710
+ let mesh;
711
+ try {
712
+ mesh = JSON.parse(meshJsonStr);
713
+ this._applyPrefixId(mesh, 'id');
714
+ if (Array.isArray(mesh)) {
715
+ mesh.forEach(item => {
716
+ this._applyPrefixId(item.primitives, 'prmid', item.documentId);
717
+ });
718
+ } else {
719
+ this._applyPrefixId(mesh.primitives, 'prmid', mesh.documentId);
720
+ }
721
+ } catch (e) {
722
+ console.error('JSON 解析失败:', e, meshJsonStr);
723
+ throw new Error('JSON解析失败');
724
+ }
725
+
726
+ while (offset < uint8Array.length) {
727
+ try {
728
+ const result = this.parsePrimitive(dataView, uint8Array, offset);
729
+ console.log('result', result);
730
+ if (!result) break;
731
+ primitives.push(result.primitive);
732
+ offset = result.offset;
733
+ } catch (e) {
734
+ console.error('解析失败,偏移:', offset, e);
735
+ break;
736
+ }
737
+ }
738
+
739
+ for (let i = 0; i < primitives.length; i++) {
740
+ const primitive = primitives[i];
741
+ this._applyPrefixId(primitive, 'id');
742
+ this._applyPrefixId(primitive, 'material');
743
+ const { position, normal, posindex, nolindex, indices } =
744
+ this.parsePrimitiveData(primitive);
745
+ const formatted = this.formatPrimitiveData({ position, normal, posindex, nolindex });
746
+ delete primitive.nolindex;
747
+ delete primitive.posindex;
748
+ primitive.position = formatted.position;
749
+ primitive.normal = formatted.normal;
750
+ primitive.indices = indices;
751
+ }
752
+
753
+ return { mesh, primitives };
754
+ } catch (error) {
755
+ throw error;
756
+ } finally {
757
+ uint8Array = null;
758
+ dataView = null;
759
+ meshJsonBytes = null;
760
+ }
761
+ }
762
+
763
+ async fetchJsonSync(list, range, abortSignal = null, requestId = null) {
764
+ let buffer = null;
765
+ let primitives = [];
766
+ try {
767
+ const loadStartTime = Date.now();
768
+ buffer = await this.fetchPrimitiveBuffer(range, abortSignal, requestId);
769
+ if (!buffer) {
770
+ return { primitives: [] };
771
+ }
772
+ const { mesh, primitives: parsed } = await this.parseBufferData(buffer);
773
+ primitives = parsed;
774
+ if (typeof this.renderModelData === 'function') {
775
+ await this.renderModelData(mesh, primitives, list, range);
776
+ }
777
+ return { primitives, mesh };
778
+ } catch (err) {
779
+ if (err.name === 'AbortError') {
780
+ throw err;
781
+ }
782
+ if (requestId && this.currentRequestId !== requestId) {
783
+ return;
784
+ }
785
+ throw err;
786
+ } finally {
787
+ try {
788
+ buffer = null;
789
+ primitives = null;
790
+ if (typeof window !== 'undefined' && window.gc) {
791
+ window.gc();
792
+ }
793
+ } catch (cleanupErr) {
794
+ console.warn('内存清理时发生错误:', cleanupErr);
795
+ }
796
+ }
797
+ }
798
+
799
+ async fetchJsonStream(list, range, abortSignal = null, requestId = null) {
800
+ try {
801
+ const loadStartTime = Date.now();
802
+ await this.fetchPrimitiveBufferByStream(range, list, abortSignal, requestId);
803
+ } catch (err) {
804
+ if (err.name === 'AbortError') {
805
+ throw err;
806
+ }
807
+ if (requestId && this.currentRequestId !== requestId) {
808
+ return;
809
+ }
810
+ throw err;
811
+ }
812
+ }
813
+
814
+ // ----------------------------------------------------------------
815
+ // 数据格式化与解析工具
816
+ // ----------------------------------------------------------------
817
+
818
+ formatPrimitiveData(primitive) {
819
+ const { position, normal, posindex, nolindex } = primitive;
820
+ const restoredPosition = new Array(posindex.length);
821
+ const restoredNormal = new Array(nolindex.length);
822
+ for (let i = 0; i < posindex.length; i++) {
823
+ const posIndex = posindex[i];
824
+ restoredPosition[i] = position[posIndex]; // 索引和顶点一对一
825
+ }
826
+ for (let i = 0; i < nolindex.length; i++) {
827
+ const normalIndex = nolindex[i];
828
+ restoredNormal[i] = normal[normalIndex]; // 索引和法向量一对一
829
+ }
830
+ primitive.position = null;
831
+ primitive.normal = null;
832
+ primitive.posindex = null;
833
+ primitive.nolindex = null;
834
+ return { position: restoredPosition, normal: restoredNormal };
835
+ }
836
+
837
+ parsePrimitiveData(primitive) {
838
+ const keyOrder = ['position', 'normal', 'indices', 'posindex', 'nolindex'];
839
+ const typeMap = {
840
+ 0: { bytes: 4, getter: 'getFloat32' },
841
+ 1: { bytes: 4, getter: 'getInt32' },
842
+ 2: { bytes: 2, getter: 'getInt16' },
843
+ };
844
+
845
+ const parsedResult = {};
846
+ const dataTypeString = primitive.dataType;
847
+
848
+ for (let i = 0; i < keyOrder.length; i++) {
849
+ const key = keyOrder[i];
850
+ const typeCode = dataTypeString[i];
851
+ const typeInfo = typeMap[typeCode];
852
+ const uint8Array =
853
+ primitive.isCompressed === 1 || primitive.isCompressed === undefined
854
+ ? gunzipSync(primitive[key])
855
+ : primitive[key];
856
+ if (!typeInfo || !uint8Array) {
857
+ console.warn(`无法找到键 "${key}" 或其类型定义,已跳过。`);
858
+ parsedResult[key] = [];
859
+ continue;
860
+ }
861
+ const result_array = [];
862
+ const dataView = new DataView(
863
+ uint8Array.buffer,
864
+ uint8Array.byteOffset,
865
+ uint8Array.byteLength
866
+ );
867
+ const littleEndian = true;
868
+ for (let byteOffset = 0; byteOffset < dataView.byteLength; byteOffset += typeInfo.bytes) {
869
+ if (byteOffset + typeInfo.bytes > dataView.byteLength) {
870
+ console.warn(
871
+ `键 "${key}" 的数据长度 (${dataView.byteLength}) 不是其类型 (${typeInfo.bytes}字节) 的整数倍,末尾数据可能不完整。`
872
+ );
873
+ break;
874
+ }
875
+ const value = dataView[typeInfo.getter](byteOffset, littleEndian);
876
+ result_array.push(value);
877
+ }
878
+ parsedResult[key] = result_array;
879
+ }
880
+ return parsedResult;
881
+ }
882
+
883
+ parsePrimitive(dataView, uint8Array, offset, isFullProps = true) {
884
+ const primitive = {};
885
+ if (dataView.byteLength < offset + 12) {
886
+ throw new Error('Insufficient data for primitive header');
887
+ }
888
+ primitive.id = dataView.getInt32(offset, false);
889
+ offset += 4;
890
+
891
+ if (dataView.byteLength < offset + 4) {
892
+ throw new Error('Insufficient data for GeomText length');
893
+ }
894
+ const documentTextLen = dataView.getUint32(offset, false);
895
+ offset += 4;
896
+ if (documentTextLen === 0xffffffff) {
897
+ primitive.documentId = null;
898
+ } else if (documentTextLen > 0) {
899
+ if (dataView.byteLength < offset + documentTextLen) {
900
+ throw new Error('Insufficient data for GeomText content');
901
+ }
902
+ const textBytes = uint8Array.subarray(offset, offset + documentTextLen);
903
+ primitive.documentId = new TextDecoder('utf-8').decode(textBytes);
904
+ offset += documentTextLen;
905
+ } else {
906
+ primitive.documentId = '';
907
+ }
908
+
909
+ primitive.material = dataView.getInt32(offset, false);
910
+ offset += 4;
911
+
912
+ if (dataView.byteLength < offset + 4) {
913
+ throw new Error('Insufficient data for GeomText length');
914
+ }
915
+ const geomTextLen = dataView.getUint32(offset, false);
916
+ offset += 4;
917
+ if (geomTextLen === 0xffffffff) {
918
+ primitive.geomText = null;
919
+ } else if (geomTextLen > 0) {
920
+ if (dataView.byteLength < offset + geomTextLen) {
921
+ throw new Error('Insufficient data for GeomText content');
922
+ }
923
+ const textBytes = uint8Array.subarray(offset, offset + geomTextLen);
924
+ primitive.geomText = new TextDecoder('utf-8').decode(textBytes);
925
+ offset += geomTextLen;
926
+ } else {
927
+ primitive.geomText = '';
928
+ }
929
+
930
+ // console.log('window.plat', window.plat)
931
+ // if(window.plat != 1){
932
+ // primitive.isCompressed = dataView.getInt32(offset, false);
933
+ // offset += 4;
934
+ // }
935
+ primitive.isCompressed = dataView.getInt32(offset, false);
936
+ offset += 4;
937
+
938
+ if (isFullProps) {
939
+ if (dataView.byteLength < offset + 4)
940
+ throw new Error('Insufficient data for Position length');
941
+ const positionLen = dataView.getUint32(offset, false);
942
+ offset += 4;
943
+ if (positionLen === 0xffffffff) {
944
+ primitive.position = null;
945
+ } else {
946
+ if (dataView.byteLength < offset + positionLen)
947
+ throw new Error('Insufficient data for Position content');
948
+ primitive.position = uint8Array.subarray(offset, offset + positionLen);
949
+ offset += positionLen;
950
+ }
951
+ }
952
+
953
+ if (isFullProps) {
954
+ if (dataView.byteLength < offset + 4) throw new Error('Insufficient data for Normal length');
955
+ const normalLen = dataView.getUint32(offset, false);
956
+ offset += 4;
957
+ if (normalLen === 0xffffffff) {
958
+ primitive.normal = null;
959
+ } else {
960
+ if (dataView.byteLength < offset + normalLen)
961
+ throw new Error('Insufficient data for Normal content');
962
+ primitive.normal = uint8Array.subarray(offset, offset + normalLen);
963
+ offset += normalLen;
964
+ }
965
+ }
966
+
967
+ if (isFullProps) {
968
+ if (dataView.byteLength < offset + 4) throw new Error('Insufficient data for Indices length');
969
+ const indicesLen = dataView.getUint32(offset, false);
970
+ offset += 4;
971
+ if (indicesLen === 0xffffffff) {
972
+ primitive.indices = null;
973
+ } else {
974
+ if (dataView.byteLength < offset + indicesLen)
975
+ throw new Error('Insufficient data for Indices content');
976
+ primitive.indices = uint8Array.subarray(offset, offset + indicesLen);
977
+ offset += indicesLen;
978
+ }
979
+ }
980
+
981
+ if (isFullProps) {
982
+ if (dataView.byteLength < offset + 4)
983
+ throw new Error('Insufficient data for Posindex length');
984
+ const posindexLen = dataView.getUint32(offset, false);
985
+ offset += 4;
986
+ if (posindexLen === 0xffffffff) {
987
+ primitive.posindex = null;
988
+ } else {
989
+ if (dataView.byteLength < offset + posindexLen)
990
+ throw new Error('Insufficient data for Posindex content');
991
+ primitive.posindex = uint8Array.subarray(offset, offset + posindexLen);
992
+ offset += posindexLen;
993
+ }
994
+ }
995
+
996
+ if (isFullProps) {
997
+ if (dataView.byteLength < offset + 4)
998
+ throw new Error('Insufficient data for Nolindex length');
999
+ const nolindexLen = dataView.getUint32(offset, false);
1000
+ offset += 4;
1001
+ if (nolindexLen === 0xffffffff) {
1002
+ primitive.nolindex = null;
1003
+ } else {
1004
+ if (dataView.byteLength < offset + nolindexLen)
1005
+ throw new Error('Insufficient data for Nolindex content');
1006
+ primitive.nolindex = uint8Array.subarray(offset, offset + nolindexLen);
1007
+ offset += nolindexLen;
1008
+ }
1009
+ }
1010
+
1011
+ if (dataView.byteLength < offset + 4) throw new Error('Insufficient data for DataType length');
1012
+ const dataTypeLen = dataView.getUint32(offset, false);
1013
+ offset += 4;
1014
+ if (dataTypeLen === 0xffffffff) {
1015
+ primitive.dataType = null;
1016
+ } else if (dataTypeLen > 0) {
1017
+ if (dataView.byteLength < offset + dataTypeLen)
1018
+ throw new Error('Insufficient data for DataType content');
1019
+ const textBytes = uint8Array.subarray(offset, offset + dataTypeLen);
1020
+ primitive.dataType = new TextDecoder('utf-8').decode(textBytes);
1021
+ offset += dataTypeLen;
1022
+ } else {
1023
+ primitive.dataType = '';
1024
+ }
1025
+
1026
+ // console.log('primitive.dataType', primitive.dataType)
1027
+
1028
+ if (dataView.byteLength < offset + 4) throw new Error('Insufficient data for Min count');
1029
+ const minCount = dataView.getUint32(offset, false);
1030
+ offset += 4;
1031
+ if (minCount === 0xffffffff) {
1032
+ primitive.min = null;
1033
+ } else {
1034
+ if (dataView.byteLength < offset + minCount * 8)
1035
+ throw new Error('Insufficient data for Min values');
1036
+ primitive.min = [];
1037
+ for (let i = 0; i < minCount; i++) {
1038
+ primitive.min.push(dataView.getFloat64(offset, false));
1039
+ offset += 8;
1040
+ }
1041
+ }
1042
+
1043
+ if (dataView.byteLength < offset + 4) throw new Error('Insufficient data for Max count');
1044
+ const maxCount = dataView.getUint32(offset, false);
1045
+ offset += 4;
1046
+ if (maxCount === 0xffffffff) {
1047
+ primitive.max = null;
1048
+ } else {
1049
+ if (dataView.byteLength < offset + maxCount * 8)
1050
+ throw new Error('Insufficient data for Max values');
1051
+ primitive.max = [];
1052
+ for (let i = 0; i < maxCount; i++) {
1053
+ primitive.max.push(dataView.getFloat64(offset, false));
1054
+ offset += 8;
1055
+ }
1056
+ }
1057
+
1058
+ if (dataView.byteLength < offset + 4) throw new Error('Insufficient data for GeomType');
1059
+ primitive.geomType = dataView.getInt32(offset, false);
1060
+ offset += 4;
1061
+
1062
+ // console.log('primitive offset', primitive, offset)
1063
+ return { primitive, offset };
1064
+ }
1065
+
1066
+ // ----------------------------------------------------------------
1067
+ // 交互控制
1068
+ // ----------------------------------------------------------------
1069
+
1070
+ async ensureNotInteracting(abortSignal) {
1071
+ if (abortSignal && abortSignal.aborted) return;
1072
+
1073
+ // 如果外部提供了检查函数,优先使用
1074
+ if (this.externalEnsureNotInteracting) {
1075
+ await this.externalEnsureNotInteracting(abortSignal);
1076
+ return;
1077
+ }
1078
+
1079
+ if (!this.isUserInteracting) return;
1080
+ if (!this.parsePausePromise) {
1081
+ this.parsePausePromise = new Promise(resolve => {
1082
+ this.parsePauseResolver = resolve;
1083
+ });
1084
+ }
1085
+ await this.parsePausePromise;
1086
+ }
1087
+
1088
+ resumeParsing() {
1089
+ if (this.parsePauseResolver) {
1090
+ this.parsePauseResolver();
1091
+ this.parsePauseResolver = null;
1092
+ this.parsePausePromise = null;
1093
+ }
1094
+ }
1095
+
1096
+ handleControlStart() {
1097
+ this.controlPressStartAt = Date.now();
1098
+ this.isUserInteracting = true;
1099
+ }
1100
+
1101
+ async handleControlEnd() {
1102
+ const pressDuration = Date.now() - (this.controlPressStartAt || 0);
1103
+ if (pressDuration >= this.longPressAbortThresholdMs) {
1104
+ try {
1105
+ // await this.abortAllStreamRequests();
1106
+ } catch (_) {}
1107
+ }
1108
+ this.controlPressStartAt = 0;
1109
+ this.isUserInteracting = false;
1110
+ this.resumeParsing();
1111
+ }
1112
+
1113
+ async handleWheelStart() {
1114
+ try {
1115
+ // await this.abortAllStreamRequests();
1116
+ } catch (_) {}
1117
+ this.isUserInteracting = true;
1118
+ }
1119
+
1120
+ handleWheelEnd() {
1121
+ this.isUserInteracting = false;
1122
+ this.resumeParsing();
1123
+ }
1124
+
1125
+ // ----------------------------------------------------------------
1126
+ // 批量加载与详情获取
1127
+ // ----------------------------------------------------------------
1128
+
1129
+ async batchLoadRegions(item, divideData) {
1130
+ if (!divideData || divideData.length === 0) {
1131
+ return;
1132
+ }
1133
+ try {
1134
+ const priorityRegions = [];
1135
+ const level0Regions = divideData.filter(
1136
+ region => region.lodLevel && region.lodLevel.includes(0)
1137
+ );
1138
+ const level1Regions = divideData.filter(
1139
+ region => region.lodLevel && region.lodLevel.includes(1)
1140
+ );
1141
+ const level2Regions = divideData.filter(
1142
+ region => region.lodLevel && region.lodLevel.includes(2)
1143
+ );
1144
+ const level3Regions = divideData.filter(
1145
+ region => region.lodLevel && region.lodLevel.includes(3)
1146
+ );
1147
+ const level4Regions = divideData.filter(
1148
+ region => region.lodLevel && region.lodLevel.includes(4)
1149
+ );
1150
+ priorityRegions.push(
1151
+ ...level0Regions,
1152
+ ...level1Regions,
1153
+ ...level2Regions,
1154
+ ...level3Regions,
1155
+ ...level4Regions
1156
+ );
1157
+ if (priorityRegions.length === 0) {
1158
+ return;
1159
+ }
1160
+ for (let i = 0; i < priorityRegions.length && i < 1; i++) {
1161
+ const regionData = priorityRegions[i];
1162
+ const range = { regionIndex: regionData.index, documentId: item.id };
1163
+ try {
1164
+ await this.fetchJsonSync(item, range);
1165
+ } catch (error) {}
1166
+ }
1167
+ } catch (error) {
1168
+ throw error;
1169
+ }
1170
+ }
1171
+
1172
+ async loadModelByIds(options) {
1173
+ let { params, onComplete } = options;
1174
+ let buffer = null;
1175
+ let primitives = [];
1176
+ try {
1177
+ const loadStartTime = Date.now();
1178
+
1179
+ // 构建 documentIdToIds 映射 多个documentid->modelid
1180
+ const { ids } = options.params || {};
1181
+ const documentIdToIds = this.parseModelDocumentMappings(ids);
1182
+ delete params.ids;
1183
+ params.documentIdToIds = documentIdToIds;
1184
+ params.projectId = this.projectId;
1185
+
1186
+ buffer = await this.getPrimitivesByRangeDetail(params);
1187
+ if (!buffer) {
1188
+ return { primitives: [] };
1189
+ }
1190
+ const { mesh, primitives: parsed } = await this.parseBufferData(buffer);
1191
+ primitives = parsed;
1192
+
1193
+ await this.renderModelData(
1194
+ mesh,
1195
+ primitives,
1196
+ params.folderInfo,
1197
+ null,
1198
+ onComplete,
1199
+ params.immediateUpdate ?? true
1200
+ );
1201
+
1202
+ return { primitives, mesh };
1203
+ } catch (err) {
1204
+ if (err.name === 'AbortError') {
1205
+ throw err;
1206
+ }
1207
+ throw err;
1208
+ } finally {
1209
+ try {
1210
+ buffer = null;
1211
+ if (primitives && primitives.length > 0) {
1212
+ primitives.forEach(primitive => {
1213
+ if (primitive && typeof primitive === 'object') {
1214
+ Object.keys(primitive).forEach(key => {
1215
+ if (Array.isArray(primitive[key]) && primitive[key].length > 100) {
1216
+ primitive[key] = null;
1217
+ }
1218
+ });
1219
+ }
1220
+ });
1221
+ }
1222
+ primitives = null;
1223
+ if (typeof window !== 'undefined' && window.gc) {
1224
+ window.gc();
1225
+ }
1226
+ } catch (cleanupErr) {}
1227
+ }
1228
+ }
1229
+
1230
+ // ----------------------------------------------------------------
1231
+ // API 包装
1232
+ // ----------------------------------------------------------------
1233
+
1234
+ async getPrimitivesByDivide(params) {
1235
+ const res = await this.modelApi.getPrimitivesByDivide(params);
1236
+ if (res) return res;
1237
+ throw new Error('Failed to get primitives');
1238
+ }
1239
+
1240
+ async getPrimitivesByRangeDetail(params) {
1241
+ const res = await this.modelApi.getPrimitivesByRangeDetail(params);
1242
+ if (res) return res;
1243
+ throw new Error('Failed to get batch model');
1244
+ }
1245
+
1246
+ // ----------------------------------------------------------------
1247
+ // 其他
1248
+ // ----------------------------------------------------------------
1249
+
1250
+ async getSceneBox({ id, projectId = this.projectId }) {
1251
+ const res = await this.modelApi.getSceneBox({ id, projectId });
1252
+ if (res && res[0]) {
1253
+ this.sceneBox = res[0];
1254
+ return res[0];
1255
+ } else {
1256
+ return null;
1257
+ }
1258
+ }
1259
+
1260
+ async getBox({ id, projectId = this.projectId || 0 }) {
1261
+ // 1. 尝试从 IndexedDB 读取
1262
+ const cached = await this._getFromDB(id, projectId);
1263
+ if (cached) {
1264
+ return cached;
1265
+ }
1266
+
1267
+ // 2. 正常请求
1268
+ const res = await this.modelApi.getBox({ id, projectId });
1269
+ if (res) {
1270
+ // 3. 存入 IndexedDB (存储原始数据)
1271
+ this._applyPrefixId(res, 'id', id);
1272
+
1273
+ this._saveToDB(id, res, projectId);
1274
+ return res;
1275
+ } else {
1276
+ return null;
1277
+ }
1278
+ }
1279
+
1280
+ async handleCameraControlForStream(args) {
1281
+ // 性能优化注释:禁用 range 被注释字段的解构声明与初始化
1282
+ const {
1283
+ chainRequest,
1284
+ loadedModels,
1285
+ // max,
1286
+ // min,
1287
+ // viewportHeight,
1288
+ // viewportWidth,
1289
+ // formula,
1290
+ // cameraWorldPosition,
1291
+ // lodLevel,
1292
+ // fovY,
1293
+ // vertices,
1294
+ // distance,
1295
+ // highSSE,
1296
+ // sseValue,
1297
+ // isAllType,
1298
+ // queryIds,
1299
+ // sceneBox,
1300
+ } = args;
1301
+
1302
+ // 构建 documentIdToIds 映射 多个documentid->modelid
1303
+ // 性能优化:提前构建映射,避免后续重复遍历 loadedModels
1304
+ const documentIdToIds = this.parseModelDocumentMappings(loadedModels);
1305
+
1306
+ // 获取当前模型资源(通常由外部注入或设置)
1307
+ const activeItems = [];
1308
+ // const activeMaterialData = [];
1309
+ // const loadedDocIds = new Set(); // 性能优化:不再需要单独的 Set
1310
+
1311
+ // 从注册表中收集数据
1312
+ // 性能优化:直接使用 documentIdToIds 的 keys,避免重复解析 loadedModels
1313
+ Object.keys(documentIdToIds).forEach(docId => {
1314
+ const record = this.modelRegistry.get(docId);
1315
+ if (record) {
1316
+ activeItems.push(record.item);
1317
+ // if (record.materialData) {
1318
+ // if (Array.isArray(record.materialData)) {
1319
+ // activeMaterialData.push(...record.materialData);
1320
+ // } else {
1321
+ // activeMaterialData.push(record.materialData);
1322
+ // }
1323
+ // }
1324
+ }
1325
+ });
1326
+
1327
+ if (activeItems.length === 0) {
1328
+ return;
1329
+ }
1330
+
1331
+ const primaryItem = activeItems[0]; // 使用第一个item作为主上下文
1332
+
1333
+ const requestId = Date.now() + '_' + Math.random().toString(36).substr(2, 9);
1334
+
1335
+ if (!chainRequest && this.currentAbortController) {
1336
+ this.currentAbortController.abort();
1337
+ this.currentAbortController = null;
1338
+ }
1339
+
1340
+ this.currentAbortController = new AbortController();
1341
+ this.currentRequestId = requestId;
1342
+
1343
+ // 性能优化注释:禁用 range 被注释字段的坐标拆解计算
1344
+ // const { x: coordinateX, y: coordinateY, z: coordinateZ } = cameraWorldPosition;
1345
+ // const documentIdToIds = {
1346
+ // "a8d4e49c-874c-4a11-9d72-13e8d0a6354e": [614]
1347
+ // }
1348
+ // 性能优化注释:禁用 range 中除 documentIdToIds 外的所有字段
1349
+ const range = {
1350
+ // minX: min.x,
1351
+ // maxX: max.x,
1352
+ // minY: min.y,
1353
+ // maxY: max.y,
1354
+ // maxZ: max.z,
1355
+ // minZ: min.z,
1356
+ // viewportHeight,
1357
+ // formula,
1358
+ // coordinateX,
1359
+ // coordinateY,
1360
+ // coordinateZ,
1361
+ // fovY,
1362
+ // viewportWidth,
1363
+ documentIdToIds,
1364
+ // distance,
1365
+ // highSSE,
1366
+ // sseValue,
1367
+ // isAllType,
1368
+ // queryIds,
1369
+ // sceneBox,
1370
+ // ...vertices,
1371
+ };
1372
+
1373
+ // 性能优化注释:禁用基于 sceneBox 的 range 二次裁剪逻辑
1374
+ // const boxToUse = sceneBox || this.sceneBox;
1375
+ // if (boxToUse) {
1376
+ // range.minX = Math.max(range.minX, boxToUse.min_x);
1377
+ // range.maxX = Math.min(range.maxX, boxToUse.max_x);
1378
+ // range.minY = Math.max(range.minY, boxToUse.min_y);
1379
+ // range.maxY = Math.min(range.maxY, boxToUse.max_y);
1380
+ // range.minZ = Math.max(range.minZ, boxToUse.min_z);
1381
+ // range.maxZ = Math.min(range.maxZ, boxToUse.max_z);
1382
+ // }
1383
+
1384
+ try {
1385
+ await this.fetchJsonStream(primaryItem, range, this.currentAbortController.signal, requestId);
1386
+ if (this.currentRequestId === requestId) {
1387
+ this.currentAbortController = null;
1388
+ this.currentRequestId = null;
1389
+ } else {
1390
+ }
1391
+ } catch (error) {
1392
+ if (error.name === 'AbortError') {
1393
+ return;
1394
+ }
1395
+ if (this.currentRequestId === requestId) {
1396
+ this.currentAbortController = null;
1397
+ this.currentRequestId = null;
1398
+ }
1399
+ }
1400
+ }
1401
+
1402
+ updateModelRegistry(item, materialData, mergeMaterialData, sceneBox, boxIndex) {
1403
+ if (item && item.id) {
1404
+ this.modelRegistry.set(item.id, {
1405
+ item,
1406
+ materialData,
1407
+ mergeMaterialData,
1408
+ sceneBox,
1409
+ boxIndex,
1410
+ });
1411
+ }
1412
+ }
1413
+
1414
+ /**
1415
+ * 将复合 ID 数组解析为 documentId 到 modelId 列表的映射
1416
+ * @param {Array<string>} loadedModels - 复合 ID 数组 (如 ["288:docId"])
1417
+ * @returns {Object} - 映射对象 (如 {"docId": [288]})
1418
+ */
1419
+ parseModelDocumentMappings(loadedModels) {
1420
+ const acc = {};
1421
+ if (!loadedModels || !Array.isArray(loadedModels)) return acc;
1422
+
1423
+ // 性能优化:使用 for 循环代替 reduce,并优化 parseInt 调用
1424
+ for (let i = 0, len = loadedModels.length; i < len; i++) {
1425
+ const str = loadedModels[i];
1426
+ if (typeof str !== 'string') continue;
1427
+
1428
+ const firstIndex = str.indexOf(':');
1429
+ if (firstIndex !== -1) {
1430
+ // 优化:直接解析整数,避免 substring 创建临时字符串
1431
+ const modelId = parseInt(str, 10);
1432
+ if (!isNaN(modelId)) {
1433
+ const docId = str.substring(firstIndex + 1);
1434
+ if (docId) {
1435
+ if (!acc[docId]) {
1436
+ acc[docId] = [];
1437
+ }
1438
+ // if(modelId == 613 || modelId == 614){
1439
+ // acc[docId].push(modelId);
1440
+ // } // TODO 608 603 模型单独处理
1441
+ acc[docId].push(modelId);
1442
+ }
1443
+ }
1444
+ }
1445
+ }
1446
+ return acc;
1447
+ }
1448
+
1449
+ async processModelItem(item, options = {}) {
1450
+ try {
1451
+ if (typeof item.id !== 'string') {
1452
+ item.id = '' + item.id;
1453
+ }
1454
+
1455
+ // 获取材质数据
1456
+ const materialData = await this.getPrimitivesByMaterial({
1457
+ id: item.id,
1458
+ projectId: this.projectId,
1459
+ });
1460
+
1461
+ // 获取合并材质数据
1462
+ const mergeMaterialData = await this.getPrimitivesByMergeMaterial({
1463
+ id: item.id,
1464
+ projectId: this.projectId,
1465
+ });
1466
+ this._applyPrefixId(mergeMaterialData, 'id');
1467
+ this._applyPrefixId(materialData, 'id');
1468
+
1469
+ // 获取场景包围盒
1470
+ const sceneBox = await this.getSceneBox({ id: item.id, projectId: this.projectId });
1471
+ this.sceneBox = sceneBox;
1472
+
1473
+ // 获取BoxIndex
1474
+ const boxIndex = await this.getBox({ id: item.id, projectId: this.projectId });
1475
+ this.boxIndex = boxIndex;
1476
+
1477
+ // 将 mergeMaterialData 覆盖到 materialData(按 id 覆盖 color/transp)
1478
+ this._mergeMaterialDataOverrides(boxIndex, mergeMaterialData);
1479
+
1480
+ const modelResourceMapObj = { materialData, mergeMaterialData, item };
1481
+
1482
+ // 设置当前模型到注册表
1483
+ this.updateModelRegistry(item, materialData, mergeMaterialData, sceneBox, boxIndex);
1484
+ console.log('modelRegistry', this.modelRegistry);
1485
+
1486
+ return { modelResourceMap: modelResourceMapObj, sceneBox, boxIndex, item };
1487
+ } catch (error) {
1488
+ return null;
1489
+ }
1490
+ }
1491
+
1492
+ // ----------------------------------------------------------------
1493
+ // IndexedDB 缓存支持
1494
+ // ----------------------------------------------------------------
1495
+
1496
+ _initDB() {
1497
+ if (this._dbReady) return this._dbReady;
1498
+ this._dbReady = new Promise((resolve, reject) => {
1499
+ if (typeof indexedDB === 'undefined') {
1500
+ resolve(null);
1501
+ return;
1502
+ }
1503
+ // 打开数据库
1504
+ const request = indexedDB.open('StreamLoaderDB', 1);
1505
+ request.onerror = () => {
1506
+ console.warn('IndexedDB open failed');
1507
+ resolve(null);
1508
+ };
1509
+ request.onsuccess = () => resolve(request.result);
1510
+ request.onupgradeneeded = event => {
1511
+ const db = event.target.result;
1512
+ if (!db.objectStoreNames.contains('boxCache')) {
1513
+ db.createObjectStore('boxCache');
1514
+ }
1515
+ };
1516
+ });
1517
+ return this._dbReady;
1518
+ }
1519
+
1520
+ async _getFromDB(id, projectId = this.projectId || 0) {
1521
+ try {
1522
+ const db = await this._initDB();
1523
+ if (!db) return null;
1524
+ return new Promise(resolve => {
1525
+ const transaction = db.transaction(['boxCache'], 'readonly');
1526
+ const store = transaction.objectStore('boxCache');
1527
+ const request = store.get(`${projectId}:${id}`);
1528
+ request.onsuccess = () => resolve(request.result);
1529
+ request.onerror = () => resolve(null);
1530
+ });
1531
+ } catch (e) {
1532
+ console.warn('Error reading from IndexedDB:', e);
1533
+ return null;
1534
+ }
1535
+ }
1536
+
1537
+ async _saveToDB(id, data, projectId = this.projectId || 0) {
1538
+ try {
1539
+ const db = await this._initDB();
1540
+ if (!db) return;
1541
+ const transaction = db.transaction(['boxCache'], 'readwrite');
1542
+ const store = transaction.objectStore('boxCache');
1543
+ store.put(data, `${projectId}:${id}`);
1544
+ } catch (e) {
1545
+ console.warn('Error saving to IndexedDB:', e);
1546
+ }
1547
+ }
1548
+ }