@fengyizhi/scene-lib 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/fengyizhi-scene-lib.mjs +999 -110
- package/fesm2022/fengyizhi-scene-lib.mjs.map +1 -1
- package/index.d.ts +143 -5
- package/package.json +1 -1
|
@@ -168,6 +168,7 @@ class SceneStore {
|
|
|
168
168
|
this.doc.environment = doc.environment;
|
|
169
169
|
this.doc.flyPaths = doc.flyPaths;
|
|
170
170
|
this.doc.dashboard = doc.dashboard;
|
|
171
|
+
this.doc.instanceGroups = doc.instanceGroups;
|
|
171
172
|
this._selection = [];
|
|
172
173
|
// 加载文档时清空撤销/重做历史
|
|
173
174
|
this.history.clear();
|
|
@@ -204,6 +205,44 @@ class AddNodeCommand {
|
|
|
204
205
|
}
|
|
205
206
|
}
|
|
206
207
|
}
|
|
208
|
+
/** 批量添加节点(用于复制操作,一次撤销全部) */
|
|
209
|
+
class BatchAddCommand {
|
|
210
|
+
label;
|
|
211
|
+
nodes;
|
|
212
|
+
parentIds;
|
|
213
|
+
constructor(items) {
|
|
214
|
+
this.nodes = items.map(i => i.node);
|
|
215
|
+
this.parentIds = items.map(i => i.parentId);
|
|
216
|
+
this.label = `批量添加 (${items.length} 个)`;
|
|
217
|
+
}
|
|
218
|
+
do(store) {
|
|
219
|
+
for (let i = 0; i < this.nodes.length; i++) {
|
|
220
|
+
const node = this.nodes[i];
|
|
221
|
+
const parentId = this.parentIds[i];
|
|
222
|
+
if (!store.node(node.id)) {
|
|
223
|
+
store.doc.nodes.push(clone(node));
|
|
224
|
+
}
|
|
225
|
+
if (parentId) {
|
|
226
|
+
const p = store.node(parentId);
|
|
227
|
+
if (p && !p.children.includes(node.id))
|
|
228
|
+
p.children.push(node.id);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
undo(store) {
|
|
233
|
+
const ids = new Set(this.nodes.map(n => n.id));
|
|
234
|
+
store.doc.nodes = store.doc.nodes.filter(n => !ids.has(n.id));
|
|
235
|
+
for (let i = 0; i < this.parentIds.length; i++) {
|
|
236
|
+
const parentId = this.parentIds[i];
|
|
237
|
+
const node = this.nodes[i];
|
|
238
|
+
if (parentId) {
|
|
239
|
+
const p = store.node(parentId);
|
|
240
|
+
if (p)
|
|
241
|
+
p.children = p.children.filter(c => c !== node.id);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
207
246
|
class RemoveNodeCommand {
|
|
208
247
|
id;
|
|
209
248
|
label = '删除节点';
|
|
@@ -265,6 +304,7 @@ class GroupCommand {
|
|
|
265
304
|
originalParents = new Map();
|
|
266
305
|
originalPositions = new Map();
|
|
267
306
|
removedEmptyGroups = [];
|
|
307
|
+
commonParentId = null;
|
|
268
308
|
constructor(selectedIds) {
|
|
269
309
|
this.selectedIds = selectedIds;
|
|
270
310
|
this.groupId = nextId('grp');
|
|
@@ -330,9 +370,27 @@ class GroupCommand {
|
|
|
330
370
|
];
|
|
331
371
|
}
|
|
332
372
|
}
|
|
373
|
+
// 找到共同父节点:如果所有选中节点都在同一个父节点下,组也应该在该父节点下
|
|
374
|
+
const parentIds = [...this.originalParents.values()];
|
|
375
|
+
const allSameParent = parentIds.length > 0 && parentIds.every(p => p === parentIds[0]);
|
|
376
|
+
this.commonParentId = allSameParent ? parentIds[0] : null;
|
|
377
|
+
if (this.commonParentId) {
|
|
378
|
+
// 添加到共同父节点下
|
|
379
|
+
const parentNode = store.node(this.commonParentId);
|
|
380
|
+
if (parentNode) {
|
|
381
|
+
parentNode.children.push(group.id);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
333
384
|
store.doc.nodes.push(clone(group));
|
|
334
385
|
}
|
|
335
386
|
undo(store) {
|
|
387
|
+
// 从共同父节点中移除 group(如果有的话)
|
|
388
|
+
if (this.commonParentId) {
|
|
389
|
+
const parentNode = store.node(this.commonParentId);
|
|
390
|
+
if (parentNode) {
|
|
391
|
+
parentNode.children = parentNode.children.filter(c => c !== this.groupId);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
336
394
|
// 删除新创建的 group
|
|
337
395
|
store.doc.nodes = store.doc.nodes.filter(n => n.id !== this.groupId);
|
|
338
396
|
// 恢复被删除的空组
|
|
@@ -355,6 +413,23 @@ class GroupCommand {
|
|
|
355
413
|
}
|
|
356
414
|
}
|
|
357
415
|
}
|
|
416
|
+
/** 计算节点的世界位置(只考虑平移,忽略旋转缩放) */
|
|
417
|
+
function computeWorldPosition(store, nodeId) {
|
|
418
|
+
const node = store.node(nodeId);
|
|
419
|
+
if (!node)
|
|
420
|
+
return [0, 0, 0];
|
|
421
|
+
const parent = store.parentOf(nodeId);
|
|
422
|
+
if (!parent) {
|
|
423
|
+
// 根节点,局部坐标就是世界坐标
|
|
424
|
+
return [...node.position];
|
|
425
|
+
}
|
|
426
|
+
const parentWorld = computeWorldPosition(store, parent.id);
|
|
427
|
+
return [
|
|
428
|
+
parentWorld[0] + node.position[0],
|
|
429
|
+
parentWorld[1] + node.position[1],
|
|
430
|
+
parentWorld[2] + node.position[2],
|
|
431
|
+
];
|
|
432
|
+
}
|
|
358
433
|
/** 解除合并:将 group 的子节点提升到 group 的父节点下,删除 group */
|
|
359
434
|
class UngroupCommand {
|
|
360
435
|
label = '解除合并';
|
|
@@ -362,6 +437,10 @@ class UngroupCommand {
|
|
|
362
437
|
children = [];
|
|
363
438
|
groupParentId = null;
|
|
364
439
|
groupName = '';
|
|
440
|
+
/** 记录 group 的变换(用于 undo 恢复) */
|
|
441
|
+
groupTransform = null;
|
|
442
|
+
/** 记录子节点在 group 内的原始位置(用于 undo 恢复) */
|
|
443
|
+
originalChildPositions = new Map();
|
|
365
444
|
constructor(groupId) {
|
|
366
445
|
this.groupId = groupId;
|
|
367
446
|
}
|
|
@@ -372,6 +451,39 @@ class UngroupCommand {
|
|
|
372
451
|
this.groupParentId = store.parentOf(this.groupId)?.id ?? null;
|
|
373
452
|
this.children = [...group.children];
|
|
374
453
|
this.groupName = group.name;
|
|
454
|
+
this.originalChildPositions.clear();
|
|
455
|
+
// 记录 group 的变换状态
|
|
456
|
+
this.groupTransform = {
|
|
457
|
+
position: [...group.position],
|
|
458
|
+
rotation: [...group.rotation],
|
|
459
|
+
scale: [...group.scale],
|
|
460
|
+
};
|
|
461
|
+
// 计算 group 的世界位置
|
|
462
|
+
const groupWorldPos = computeWorldPosition(store, this.groupId);
|
|
463
|
+
// 计算目标父节点的世界位置
|
|
464
|
+
const parentWorldPos = this.groupParentId
|
|
465
|
+
? computeWorldPosition(store, this.groupParentId)
|
|
466
|
+
: [0, 0, 0];
|
|
467
|
+
// 记录原始位置,并转换子节点坐标
|
|
468
|
+
for (const childId of this.children) {
|
|
469
|
+
const child = store.node(childId);
|
|
470
|
+
if (!child)
|
|
471
|
+
continue;
|
|
472
|
+
// 记录原始位置(group 的局部坐标)
|
|
473
|
+
this.originalChildPositions.set(childId, [...child.position]);
|
|
474
|
+
// 计算子节点的世界位置
|
|
475
|
+
const childWorldPos = [
|
|
476
|
+
groupWorldPos[0] + child.position[0],
|
|
477
|
+
groupWorldPos[1] + child.position[1],
|
|
478
|
+
groupWorldPos[2] + child.position[2],
|
|
479
|
+
];
|
|
480
|
+
// 转换为父节点的局部坐标
|
|
481
|
+
child.position = [
|
|
482
|
+
childWorldPos[0] - parentWorldPos[0],
|
|
483
|
+
childWorldPos[1] - parentWorldPos[1],
|
|
484
|
+
childWorldPos[2] - parentWorldPos[2],
|
|
485
|
+
];
|
|
486
|
+
}
|
|
375
487
|
// 将子节点移到 group 的父节点下
|
|
376
488
|
if (this.groupParentId) {
|
|
377
489
|
const parent = store.node(this.groupParentId);
|
|
@@ -393,6 +505,12 @@ class UngroupCommand {
|
|
|
393
505
|
const group = createNode('group', this.groupName || '组');
|
|
394
506
|
group.id = this.groupId;
|
|
395
507
|
group.children = [...this.children];
|
|
508
|
+
// 恢复 group 的变换状态
|
|
509
|
+
if (this.groupTransform) {
|
|
510
|
+
group.position = [...this.groupTransform.position];
|
|
511
|
+
group.rotation = [...this.groupTransform.rotation];
|
|
512
|
+
group.scale = [...this.groupTransform.scale];
|
|
513
|
+
}
|
|
396
514
|
store.doc.nodes.push(clone(group));
|
|
397
515
|
// 从当前父节点移除子节点
|
|
398
516
|
for (const id of this.children) {
|
|
@@ -411,6 +529,13 @@ class UngroupCommand {
|
|
|
411
529
|
}
|
|
412
530
|
}
|
|
413
531
|
}
|
|
532
|
+
// 恢复子节点的原始位置(group 的局部坐标)
|
|
533
|
+
for (const [id, pos] of this.originalChildPositions) {
|
|
534
|
+
const child = store.node(id);
|
|
535
|
+
if (child) {
|
|
536
|
+
child.position = [...pos];
|
|
537
|
+
}
|
|
538
|
+
}
|
|
414
539
|
}
|
|
415
540
|
}
|
|
416
541
|
/** 材质覆盖命令:记录 before/after,支持 undo/redo */
|
|
@@ -535,13 +660,17 @@ class SetAlarmCommand {
|
|
|
535
660
|
}
|
|
536
661
|
}
|
|
537
662
|
}
|
|
538
|
-
/**
|
|
663
|
+
/** 拖动节点改变父节点(保持世界坐标不变) */
|
|
539
664
|
class ReparentCommand {
|
|
540
665
|
nodeId;
|
|
541
666
|
newParentId;
|
|
542
667
|
label = '移动节点';
|
|
543
668
|
oldParentId = null;
|
|
544
669
|
oldIndex = 0;
|
|
670
|
+
/** 记录旧位置(用于 undo 恢复) */
|
|
671
|
+
oldPosition = null;
|
|
672
|
+
oldRotation = null;
|
|
673
|
+
oldScale = null;
|
|
545
674
|
constructor(nodeId, newParentId) {
|
|
546
675
|
this.nodeId = nodeId;
|
|
547
676
|
this.newParentId = newParentId;
|
|
@@ -558,6 +687,12 @@ class ReparentCommand {
|
|
|
558
687
|
this.oldIndex = oldParent.children.indexOf(this.nodeId);
|
|
559
688
|
}
|
|
560
689
|
}
|
|
690
|
+
// 记录原始变换(用于 undo 恢复)
|
|
691
|
+
this.oldPosition = [...node.position];
|
|
692
|
+
this.oldRotation = [...node.rotation];
|
|
693
|
+
this.oldScale = [...node.scale];
|
|
694
|
+
// 计算世界坐标(只考虑平移)
|
|
695
|
+
const worldPos = computeWorldPosition(store, this.nodeId);
|
|
561
696
|
// 从旧父节点移除
|
|
562
697
|
if (this.oldParentId) {
|
|
563
698
|
const oldParent = store.node(this.oldParentId);
|
|
@@ -572,8 +707,22 @@ class ReparentCommand {
|
|
|
572
707
|
newParent.children.push(this.nodeId);
|
|
573
708
|
}
|
|
574
709
|
}
|
|
710
|
+
// 转换坐标:世界坐标 → 新父节点的局部坐标
|
|
711
|
+
if (this.newParentId) {
|
|
712
|
+
const newParentWorld = computeWorldPosition(store, this.newParentId);
|
|
713
|
+
node.position = [
|
|
714
|
+
worldPos[0] - newParentWorld[0],
|
|
715
|
+
worldPos[1] - newParentWorld[1],
|
|
716
|
+
worldPos[2] - newParentWorld[2],
|
|
717
|
+
];
|
|
718
|
+
}
|
|
719
|
+
else {
|
|
720
|
+
// 移动到根级,直接使用世界坐标
|
|
721
|
+
node.position = worldPos;
|
|
722
|
+
}
|
|
575
723
|
}
|
|
576
724
|
undo(store) {
|
|
725
|
+
const node = store.node(this.nodeId);
|
|
577
726
|
// 从当前父节点移除
|
|
578
727
|
const currentParent = store.parentOf(this.nodeId);
|
|
579
728
|
if (currentParent) {
|
|
@@ -586,6 +735,14 @@ class ReparentCommand {
|
|
|
586
735
|
oldParent.children.splice(this.oldIndex, 0, this.nodeId);
|
|
587
736
|
}
|
|
588
737
|
}
|
|
738
|
+
// 恢复原始变换
|
|
739
|
+
if (node && this.oldPosition) {
|
|
740
|
+
node.position = [...this.oldPosition];
|
|
741
|
+
if (this.oldRotation)
|
|
742
|
+
node.rotation = [...this.oldRotation];
|
|
743
|
+
if (this.oldScale)
|
|
744
|
+
node.scale = [...this.oldScale];
|
|
745
|
+
}
|
|
589
746
|
}
|
|
590
747
|
}
|
|
591
748
|
/** 环境配置命令:背景色、半球光、平行光、HDRI、场景空间 */
|
|
@@ -598,15 +755,9 @@ class SetEnvironmentCommand {
|
|
|
598
755
|
this.after = after;
|
|
599
756
|
}
|
|
600
757
|
do(store) {
|
|
601
|
-
const floorColor = this.after.room?.floorColor || 'none';
|
|
602
|
-
const floorTexture = this.after.room?.floorTexture?.substring(0, 30) || 'no texture';
|
|
603
|
-
console.log('[SetEnvironmentCommand.do] applying - floorColor:', floorColor, 'floorTexture:', floorTexture);
|
|
604
758
|
store.doc.environment = clone(this.after);
|
|
605
759
|
}
|
|
606
760
|
undo(store) {
|
|
607
|
-
const floorColor = this.before?.room?.floorColor || 'none';
|
|
608
|
-
const floorTexture = this.before?.room?.floorTexture?.substring(0, 30) || 'no texture';
|
|
609
|
-
console.log('[SetEnvironmentCommand.undo] restoring - floorColor:', floorColor, 'floorTexture:', floorTexture);
|
|
610
761
|
if (this.before) {
|
|
611
762
|
store.doc.environment = clone(this.before);
|
|
612
763
|
}
|
|
@@ -1239,6 +1390,142 @@ class MovePathPointToEndCommand {
|
|
|
1239
1390
|
}
|
|
1240
1391
|
}
|
|
1241
1392
|
}
|
|
1393
|
+
/** 对齐命令:将多个节点对齐到指定轴向的指定位置 */
|
|
1394
|
+
class AlignCommand {
|
|
1395
|
+
nodeIds;
|
|
1396
|
+
axis;
|
|
1397
|
+
position;
|
|
1398
|
+
targetPositions;
|
|
1399
|
+
label;
|
|
1400
|
+
beforePositions = new Map();
|
|
1401
|
+
constructor(nodeIds, axis, position, targetPositions) {
|
|
1402
|
+
this.nodeIds = nodeIds;
|
|
1403
|
+
this.axis = axis;
|
|
1404
|
+
this.position = position;
|
|
1405
|
+
this.targetPositions = targetPositions;
|
|
1406
|
+
const axisNames = { x: '红轴(X)', y: '绿轴(Y)', z: '蓝轴(Z)' };
|
|
1407
|
+
const positionNames = { min: '负方向', center: '居中', max: '正方向' };
|
|
1408
|
+
this.label = `对齐 ${axisNames[axis]} ${positionNames[position]}`;
|
|
1409
|
+
}
|
|
1410
|
+
do(store) {
|
|
1411
|
+
// 记录原始位置(用于 undo)
|
|
1412
|
+
this.beforePositions.clear();
|
|
1413
|
+
for (const id of this.nodeIds) {
|
|
1414
|
+
const node = store.node(id);
|
|
1415
|
+
if (node) {
|
|
1416
|
+
this.beforePositions.set(id, [...node.position]);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
// 应用新位置
|
|
1420
|
+
for (const [id, pos] of this.targetPositions) {
|
|
1421
|
+
const node = store.node(id);
|
|
1422
|
+
if (node) {
|
|
1423
|
+
node.position = clone(pos);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
undo(store) {
|
|
1428
|
+
for (const [id, pos] of this.beforePositions) {
|
|
1429
|
+
const node = store.node(id);
|
|
1430
|
+
if (node) {
|
|
1431
|
+
node.position = clone(pos);
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
/** 启用实例化渲染 */
|
|
1437
|
+
class EnableInstancingCommand {
|
|
1438
|
+
nodeIds;
|
|
1439
|
+
label = '启用实例化渲染';
|
|
1440
|
+
groupId;
|
|
1441
|
+
constructor(nodeIds) {
|
|
1442
|
+
this.nodeIds = nodeIds;
|
|
1443
|
+
this.groupId = nextId('inst');
|
|
1444
|
+
}
|
|
1445
|
+
do(store) {
|
|
1446
|
+
if (!store.doc.instanceGroups)
|
|
1447
|
+
store.doc.instanceGroups = [];
|
|
1448
|
+
// 获取 assetId(从第一个节点)
|
|
1449
|
+
const firstNode = store.node(this.nodeIds[0]);
|
|
1450
|
+
if (!firstNode?.assetId)
|
|
1451
|
+
return;
|
|
1452
|
+
// 检查是否已存在同 assetId 的实例组
|
|
1453
|
+
const existing = store.doc.instanceGroups.find(g => g.assetId === firstNode.assetId);
|
|
1454
|
+
if (existing) {
|
|
1455
|
+
// 添加到现有组
|
|
1456
|
+
for (const id of this.nodeIds) {
|
|
1457
|
+
if (!existing.nodeIds.includes(id))
|
|
1458
|
+
existing.nodeIds.push(id);
|
|
1459
|
+
}
|
|
1460
|
+
this.groupId = existing.id;
|
|
1461
|
+
}
|
|
1462
|
+
else {
|
|
1463
|
+
// 创建新组
|
|
1464
|
+
store.doc.instanceGroups.push({
|
|
1465
|
+
id: this.groupId,
|
|
1466
|
+
assetId: firstNode.assetId,
|
|
1467
|
+
nodeIds: [...this.nodeIds],
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
undo(store) {
|
|
1472
|
+
if (!store.doc.instanceGroups)
|
|
1473
|
+
return;
|
|
1474
|
+
const group = store.doc.instanceGroups.find(g => g.id === this.groupId);
|
|
1475
|
+
if (group) {
|
|
1476
|
+
// 移除添加的节点
|
|
1477
|
+
group.nodeIds = group.nodeIds.filter(id => !this.nodeIds.includes(id));
|
|
1478
|
+
// 如果组为空,删除组
|
|
1479
|
+
if (group.nodeIds.length === 0) {
|
|
1480
|
+
store.doc.instanceGroups = store.doc.instanceGroups.filter(g => g.id !== this.groupId);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
/** 禁用实例化渲染 */
|
|
1486
|
+
class DisableInstancingCommand {
|
|
1487
|
+
nodeIds;
|
|
1488
|
+
label = '禁用实例化渲染';
|
|
1489
|
+
removedGroup = null;
|
|
1490
|
+
constructor(nodeIds) {
|
|
1491
|
+
this.nodeIds = nodeIds;
|
|
1492
|
+
}
|
|
1493
|
+
do(store) {
|
|
1494
|
+
if (!store.doc.instanceGroups)
|
|
1495
|
+
return;
|
|
1496
|
+
// 找到包含这些节点的实例组
|
|
1497
|
+
const group = store.doc.instanceGroups.find(g => this.nodeIds.some(id => g.nodeIds.includes(id)));
|
|
1498
|
+
if (group) {
|
|
1499
|
+
// 保存用于 undo
|
|
1500
|
+
this.removedGroup = { id: group.id, assetId: group.assetId, nodeIds: [...group.nodeIds] };
|
|
1501
|
+
// 移除这些节点
|
|
1502
|
+
group.nodeIds = group.nodeIds.filter(id => !this.nodeIds.includes(id));
|
|
1503
|
+
// 如果组为空,删除组
|
|
1504
|
+
if (group.nodeIds.length === 0) {
|
|
1505
|
+
store.doc.instanceGroups = store.doc.instanceGroups.filter(g => g.id !== group.id);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
undo(store) {
|
|
1510
|
+
if (!this.removedGroup)
|
|
1511
|
+
return;
|
|
1512
|
+
if (!store.doc.instanceGroups)
|
|
1513
|
+
store.doc.instanceGroups = [];
|
|
1514
|
+
// 恢复组
|
|
1515
|
+
const existing = store.doc.instanceGroups.find(g => g.id === this.removedGroup.id);
|
|
1516
|
+
if (existing) {
|
|
1517
|
+
// 恢复节点
|
|
1518
|
+
for (const id of this.removedGroup.nodeIds) {
|
|
1519
|
+
if (!existing.nodeIds.includes(id))
|
|
1520
|
+
existing.nodeIds.push(id);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
else {
|
|
1524
|
+
// 恢复整个组
|
|
1525
|
+
store.doc.instanceGroups.push({ ...this.removedGroup });
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1242
1529
|
|
|
1243
1530
|
/**
|
|
1244
1531
|
* 粒子系统运行时
|
|
@@ -2964,6 +3251,12 @@ class SceneViewer {
|
|
|
2964
3251
|
pendingAssetNodes = new Map();
|
|
2965
3252
|
/** 动画基础变换(nodeId → 数据模型原始变换) */
|
|
2966
3253
|
animBases = new Map();
|
|
3254
|
+
/** FPS 统计 */
|
|
3255
|
+
fpsFrames = 0;
|
|
3256
|
+
fpsTime = 0;
|
|
3257
|
+
fpsCurrent = 0;
|
|
3258
|
+
/** 场景统计缓存 */
|
|
3259
|
+
statsCache = { vertices: 0, faces: 0, drawCalls: 0, dirty: true };
|
|
2967
3260
|
/** 缩略图渲染器(离屏) */
|
|
2968
3261
|
thumbRenderer;
|
|
2969
3262
|
thumbScene = new THREE.Scene();
|
|
@@ -2974,6 +3267,10 @@ class SceneViewer {
|
|
|
2974
3267
|
lastFrameTime = 0;
|
|
2975
3268
|
/** 拖拽中冻结动画 */
|
|
2976
3269
|
animationFrozen = false;
|
|
3270
|
+
/** 性能模式:true 时使用 renderer(无后处理),false 时使用 composer(有后处理) */
|
|
3271
|
+
performanceMode = false;
|
|
3272
|
+
/** 顶层节点集合(没有父节点的节点,用于视锥剔除优化) */
|
|
3273
|
+
topLevelNodes = new Set();
|
|
2977
3274
|
/** 相机是否已从文档恢复(只在首次 sync 时恢复一次) */
|
|
2978
3275
|
cameraRestored = false;
|
|
2979
3276
|
/** 保存原始材质参数(覆盖前),用于撤销覆盖时恢复 */
|
|
@@ -2984,6 +3281,8 @@ class SceneViewer {
|
|
|
2984
3281
|
textureCache = new Map();
|
|
2985
3282
|
/** 标签对象缓存(nodeId → label mesh) */
|
|
2986
3283
|
labelObjects = new Map();
|
|
3284
|
+
/** 实例化网格缓存(groupId → InstancedMesh 数组) */
|
|
3285
|
+
instanceMeshes = new Map();
|
|
2987
3286
|
textureLoader = new TextureLoader();
|
|
2988
3287
|
/** HDRI 环境贴图缓存 */
|
|
2989
3288
|
hdriTexture = null;
|
|
@@ -3061,6 +3360,8 @@ class SceneViewer {
|
|
|
3061
3360
|
pathEditor;
|
|
3062
3361
|
/** 是否为编辑器模式(编辑器模式下动画默认播放,不依赖数据) */
|
|
3063
3362
|
isEditorMode;
|
|
3363
|
+
/** 相机视角变化回调 */
|
|
3364
|
+
onCameraChange;
|
|
3064
3365
|
constructor(host, store, cb, dashboardOverlayEl, isEditorMode = false) {
|
|
3065
3366
|
this.host = host;
|
|
3066
3367
|
this.store = store;
|
|
@@ -3081,6 +3382,22 @@ class SceneViewer {
|
|
|
3081
3382
|
this.scene.add(this.gridHelper);
|
|
3082
3383
|
this.orbit = new OrbitControls(this.camera, this.renderer.domElement);
|
|
3083
3384
|
this.orbit.enableDamping = true;
|
|
3385
|
+
// 相机视角变化时,保存相机并触发草稿缓存
|
|
3386
|
+
let cameraSaveTimer = null;
|
|
3387
|
+
/** 恢复相机时置 true,阻止触发 cameraChange */
|
|
3388
|
+
let restoringCamera = false;
|
|
3389
|
+
this.orbit.addEventListener('change', () => {
|
|
3390
|
+
if (restoringCamera)
|
|
3391
|
+
return;
|
|
3392
|
+
if (cameraSaveTimer)
|
|
3393
|
+
clearTimeout(cameraSaveTimer);
|
|
3394
|
+
cameraSaveTimer = setTimeout(() => {
|
|
3395
|
+
this.saveCamera();
|
|
3396
|
+
this.onCameraChange?.();
|
|
3397
|
+
}, 500);
|
|
3398
|
+
});
|
|
3399
|
+
// 暴露给 restoreCamera 使用
|
|
3400
|
+
this._setRestoringCamera = (v) => { restoringCamera = v; };
|
|
3084
3401
|
this.gizmo = new TransformControls(this.camera, this.renderer.domElement);
|
|
3085
3402
|
this.gizmo.setSize(0.9);
|
|
3086
3403
|
this.gizmo.addEventListener('dragging-changed', (e) => {
|
|
@@ -3165,6 +3482,12 @@ class SceneViewer {
|
|
|
3165
3482
|
obj.traverse(c => { if (c instanceof THREE.Mesh)
|
|
3166
3483
|
allMeshes.push(c); });
|
|
3167
3484
|
}
|
|
3485
|
+
// 添加 InstancedMesh 到拾取列表
|
|
3486
|
+
for (const meshes of this.instanceMeshes.values()) {
|
|
3487
|
+
for (const im of meshes) {
|
|
3488
|
+
allMeshes.push(im);
|
|
3489
|
+
}
|
|
3490
|
+
}
|
|
3168
3491
|
const rect = this.renderer.domElement.getBoundingClientRect();
|
|
3169
3492
|
const ndc = new THREE.Vector2(((e.clientX - rect.left) / rect.width) * 2 - 1, -((e.clientY - rect.top) / rect.height) * 2 + 1);
|
|
3170
3493
|
this.raycaster.setFromCamera(ndc, this.camera);
|
|
@@ -3250,6 +3573,9 @@ class SceneViewer {
|
|
|
3250
3573
|
}
|
|
3251
3574
|
return;
|
|
3252
3575
|
}
|
|
3576
|
+
// 按住了 Ctrl/Meta 时不记录 WASD 移动键,避免 Ctrl+S 等快捷键触发后退
|
|
3577
|
+
if (e.ctrlKey || e.metaKey)
|
|
3578
|
+
return;
|
|
3253
3579
|
this.keysPressed.add(e.key.toLowerCase());
|
|
3254
3580
|
};
|
|
3255
3581
|
this.onKeyUp = (e) => this.keysPressed.delete(e.key.toLowerCase());
|
|
@@ -3260,38 +3586,10 @@ class SceneViewer {
|
|
|
3260
3586
|
// 初始化后处理
|
|
3261
3587
|
this.initPostProcessing();
|
|
3262
3588
|
this.renderer.setAnimationLoop(() => {
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
continue;
|
|
3268
|
-
// 给包围盒加 3% 空隙,让框与模型之间有视觉间距
|
|
3269
|
-
const pos = h.geometry.attributes['position'].array;
|
|
3270
|
-
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
|
3271
|
-
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
3272
|
-
for (let i = 0; i < pos.length; i += 3) {
|
|
3273
|
-
if (pos[i] < minX)
|
|
3274
|
-
minX = pos[i];
|
|
3275
|
-
if (pos[i + 1] < minY)
|
|
3276
|
-
minY = pos[i + 1];
|
|
3277
|
-
if (pos[i + 2] < minZ)
|
|
3278
|
-
minZ = pos[i + 2];
|
|
3279
|
-
if (pos[i] > maxX)
|
|
3280
|
-
maxX = pos[i];
|
|
3281
|
-
if (pos[i + 1] > maxY)
|
|
3282
|
-
maxY = pos[i + 1];
|
|
3283
|
-
if (pos[i + 2] > maxZ)
|
|
3284
|
-
maxZ = pos[i + 2];
|
|
3285
|
-
}
|
|
3286
|
-
const cx = (minX + maxX) / 2, cy = (minY + maxY) / 2, cz = (minZ + maxZ) / 2;
|
|
3287
|
-
const pad = 0.03;
|
|
3288
|
-
for (let i = 0; i < pos.length; i += 3) {
|
|
3289
|
-
pos[i] = cx + (pos[i] - cx) + (pos[i] - cx >= 0 ? pad : -pad);
|
|
3290
|
-
pos[i + 1] = cy + (pos[i + 1] - cy) + (pos[i + 1] - cy >= 0 ? pad : -pad);
|
|
3291
|
-
pos[i + 2] = cz + (pos[i + 2] - cz) + (pos[i + 2] - cz >= 0 ? pad : -pad);
|
|
3292
|
-
}
|
|
3293
|
-
h.geometry.attributes['position'].needsUpdate = true;
|
|
3294
|
-
}
|
|
3589
|
+
// TODO: 包围盒按需更新,暂时注释测试性能
|
|
3590
|
+
// for (const h of this.helpers.values()) {
|
|
3591
|
+
// h.update();
|
|
3592
|
+
// }
|
|
3295
3593
|
// 更新灯光助手
|
|
3296
3594
|
for (const h of this.lightHelpers.values()) {
|
|
3297
3595
|
if (h instanceof THREE.DirectionalLightHelper || h instanceof THREE.SpotLightHelper) {
|
|
@@ -3309,6 +3607,14 @@ class SceneViewer {
|
|
|
3309
3607
|
const now = performance.now();
|
|
3310
3608
|
const dt = this.lastFrameTime ? (now - this.lastFrameTime) / 1000 : 0;
|
|
3311
3609
|
this.lastFrameTime = now;
|
|
3610
|
+
// FPS 统计
|
|
3611
|
+
this.fpsFrames++;
|
|
3612
|
+
this.fpsTime += dt;
|
|
3613
|
+
if (this.fpsTime >= 0.5) {
|
|
3614
|
+
this.fpsCurrent = Math.round(this.fpsFrames / this.fpsTime);
|
|
3615
|
+
this.fpsFrames = 0;
|
|
3616
|
+
this.fpsTime = 0;
|
|
3617
|
+
}
|
|
3312
3618
|
// 更新动画
|
|
3313
3619
|
this.updateAnimations(dt);
|
|
3314
3620
|
// 更新视觉效果(UV 动画、粒子系统)
|
|
@@ -3325,7 +3631,15 @@ class SceneViewer {
|
|
|
3325
3631
|
this.constrainCameraToRoom();
|
|
3326
3632
|
// 更新看板 world 模式
|
|
3327
3633
|
this.updateDashboard();
|
|
3328
|
-
|
|
3634
|
+
// 视锥剔除:隐藏不在相机视锥内的顶层节点
|
|
3635
|
+
this.updateFrustumCulling();
|
|
3636
|
+
// 根据性能模式选择渲染方式
|
|
3637
|
+
if (this.performanceMode) {
|
|
3638
|
+
this.renderer.render(this.scene, this.camera);
|
|
3639
|
+
}
|
|
3640
|
+
else {
|
|
3641
|
+
this.composer.render();
|
|
3642
|
+
}
|
|
3329
3643
|
});
|
|
3330
3644
|
// 初始化路径编辑器
|
|
3331
3645
|
const pathEditorCallbacks = {
|
|
@@ -3398,7 +3712,7 @@ class SceneViewer {
|
|
|
3398
3712
|
setPathHandleSize(size) {
|
|
3399
3713
|
this.pathEditor.setHandleSize(size);
|
|
3400
3714
|
}
|
|
3401
|
-
/** 将当前相机状态写入 SceneDoc
|
|
3715
|
+
/** 将当前相机状态写入 SceneDoc(保存前调用,不触发 notifyChanged) */
|
|
3402
3716
|
saveCamera() {
|
|
3403
3717
|
this.store.doc.camera = {
|
|
3404
3718
|
position: [
|
|
@@ -3418,6 +3732,8 @@ class SceneViewer {
|
|
|
3418
3732
|
const cam = this.store.doc.camera;
|
|
3419
3733
|
if (!cam)
|
|
3420
3734
|
return;
|
|
3735
|
+
// 设置标志,阻止 change 事件触发 cameraChange → saveDraft
|
|
3736
|
+
this._setRestoringCamera?.(true);
|
|
3421
3737
|
this.camera.position.set(...cam.position);
|
|
3422
3738
|
this.orbit.target.set(...cam.target);
|
|
3423
3739
|
// 重置 damping 状态,防止残留速度导致位置漂移
|
|
@@ -3426,6 +3742,7 @@ class SceneViewer {
|
|
|
3426
3742
|
this.orbit.enableDamping = true;
|
|
3427
3743
|
// 强制相机朝向 target
|
|
3428
3744
|
this.camera.lookAt(this.orbit.target);
|
|
3745
|
+
this._setRestoringCamera?.(false);
|
|
3429
3746
|
}
|
|
3430
3747
|
/** 导出场景为 GLB 文件 */
|
|
3431
3748
|
exportScene() {
|
|
@@ -3472,6 +3789,10 @@ class SceneViewer {
|
|
|
3472
3789
|
this.cameraRestored = false;
|
|
3473
3790
|
// 新文档加载:清空资产加载状态,确保资产重新加载
|
|
3474
3791
|
this.loadingAssetUrls.clear();
|
|
3792
|
+
// 清空原始材质缓存,避免旧场景的材质状态污染新场景
|
|
3793
|
+
this.originalMaterials.clear();
|
|
3794
|
+
// 重置房间配置缓存,确保 applyRoom 重新应用
|
|
3795
|
+
this.lastRoomKey = '';
|
|
3475
3796
|
}
|
|
3476
3797
|
}
|
|
3477
3798
|
const seen = new Set();
|
|
@@ -3600,6 +3921,7 @@ class SceneViewer {
|
|
|
3600
3921
|
for (const [id, obj] of [...this.objects]) {
|
|
3601
3922
|
if (!seen.has(id)) {
|
|
3602
3923
|
this.objects.delete(id);
|
|
3924
|
+
this.markStatsDirty();
|
|
3603
3925
|
obj.parent?.remove(obj);
|
|
3604
3926
|
if (obj instanceof THREE.Mesh) {
|
|
3605
3927
|
obj.geometry.dispose();
|
|
@@ -3641,6 +3963,31 @@ class SceneViewer {
|
|
|
3641
3963
|
}
|
|
3642
3964
|
}
|
|
3643
3965
|
}
|
|
3966
|
+
// 清理不再存在的实例组对应的 InstancedMesh
|
|
3967
|
+
const validGroupIds = new Set((this.store.doc.instanceGroups || []).map(g => g.id));
|
|
3968
|
+
for (const [groupId, meshes] of [...this.instanceMeshes]) {
|
|
3969
|
+
if (!validGroupIds.has(groupId)) {
|
|
3970
|
+
// 恢复原始节点的 visible 状态
|
|
3971
|
+
if (meshes.length > 0) {
|
|
3972
|
+
const nodeIds = meshes[0].userData['instanceNodeIds'];
|
|
3973
|
+
if (nodeIds) {
|
|
3974
|
+
for (const nodeId of nodeIds) {
|
|
3975
|
+
const obj = this.objects.get(nodeId);
|
|
3976
|
+
if (obj) {
|
|
3977
|
+
obj.visible = true;
|
|
3978
|
+
}
|
|
3979
|
+
}
|
|
3980
|
+
}
|
|
3981
|
+
}
|
|
3982
|
+
// 清理 InstancedMesh
|
|
3983
|
+
for (const mesh of meshes) {
|
|
3984
|
+
mesh.parent?.remove(mesh);
|
|
3985
|
+
mesh.geometry.dispose();
|
|
3986
|
+
mesh.material.dispose();
|
|
3987
|
+
}
|
|
3988
|
+
this.instanceMeshes.delete(groupId);
|
|
3989
|
+
}
|
|
3990
|
+
}
|
|
3644
3991
|
// 层级关系
|
|
3645
3992
|
for (const n of this.store.doc.nodes) {
|
|
3646
3993
|
const obj = this.objects.get(n.id);
|
|
@@ -3676,6 +4023,10 @@ class SceneViewer {
|
|
|
3676
4023
|
this.syncEffects();
|
|
3677
4024
|
// 同步路径编辑器
|
|
3678
4025
|
this.pathEditor.sync();
|
|
4026
|
+
// 同步实例化组
|
|
4027
|
+
this.syncInstanceGroups();
|
|
4028
|
+
// 更新顶层节点集合(用于视锥剔除优化)
|
|
4029
|
+
this.updateTopLevelNodes();
|
|
3679
4030
|
// 应用环境配置
|
|
3680
4031
|
this.applyEnvironment();
|
|
3681
4032
|
// 只在首次加载时恢复相机(后续 doc 变更不重置视角)
|
|
@@ -3691,6 +4042,203 @@ class SceneViewer {
|
|
|
3691
4042
|
this.restoreCamera();
|
|
3692
4043
|
this.cameraRestored = true;
|
|
3693
4044
|
}
|
|
4045
|
+
/** 同步实例化组:创建/更新 InstancedMesh */
|
|
4046
|
+
syncInstanceGroups() {
|
|
4047
|
+
const groups = this.store.doc.instanceGroups;
|
|
4048
|
+
if (!groups || groups.length === 0) {
|
|
4049
|
+
// 恢复所有被隐藏的原始节点
|
|
4050
|
+
for (const [groupId, meshes] of this.instanceMeshes) {
|
|
4051
|
+
if (meshes.length > 0) {
|
|
4052
|
+
const nodeIds = meshes[0].userData['instanceNodeIds'];
|
|
4053
|
+
if (nodeIds) {
|
|
4054
|
+
for (const nodeId of nodeIds) {
|
|
4055
|
+
const obj = this.objects.get(nodeId);
|
|
4056
|
+
if (obj) {
|
|
4057
|
+
obj.visible = true;
|
|
4058
|
+
obj.traverse(child => {
|
|
4059
|
+
if (child !== obj)
|
|
4060
|
+
child.visible = true;
|
|
4061
|
+
});
|
|
4062
|
+
}
|
|
4063
|
+
}
|
|
4064
|
+
}
|
|
4065
|
+
}
|
|
4066
|
+
// 清理 InstancedMesh
|
|
4067
|
+
for (const mesh of meshes) {
|
|
4068
|
+
mesh.parent?.remove(mesh);
|
|
4069
|
+
mesh.geometry.dispose();
|
|
4070
|
+
mesh.material.dispose();
|
|
4071
|
+
}
|
|
4072
|
+
}
|
|
4073
|
+
this.instanceMeshes.clear();
|
|
4074
|
+
this.markStatsDirty();
|
|
4075
|
+
return;
|
|
4076
|
+
}
|
|
4077
|
+
const seenGroups = new Set();
|
|
4078
|
+
for (const group of groups) {
|
|
4079
|
+
seenGroups.add(group.id);
|
|
4080
|
+
const nodeIds = group.nodeIds;
|
|
4081
|
+
if (nodeIds.length === 0)
|
|
4082
|
+
continue;
|
|
4083
|
+
// 获取第一个节点作为模板
|
|
4084
|
+
const templateNodeId = nodeIds[0];
|
|
4085
|
+
const templateObj = this.objects.get(templateNodeId);
|
|
4086
|
+
if (!templateObj)
|
|
4087
|
+
continue;
|
|
4088
|
+
// 收集模板对象中的所有 Mesh
|
|
4089
|
+
const templateMeshes = [];
|
|
4090
|
+
templateObj.traverse(child => {
|
|
4091
|
+
if (child instanceof THREE.Mesh) {
|
|
4092
|
+
templateMeshes.push(child);
|
|
4093
|
+
}
|
|
4094
|
+
});
|
|
4095
|
+
if (templateMeshes.length === 0)
|
|
4096
|
+
continue;
|
|
4097
|
+
// 获取或创建 InstancedMesh 数组
|
|
4098
|
+
let instancedMeshes = this.instanceMeshes.get(group.id);
|
|
4099
|
+
const needsRecreate = !instancedMeshes || instancedMeshes.length !== templateMeshes.length;
|
|
4100
|
+
if (needsRecreate) {
|
|
4101
|
+
// 清理旧的 InstancedMesh
|
|
4102
|
+
if (instancedMeshes) {
|
|
4103
|
+
for (const mesh of instancedMeshes) {
|
|
4104
|
+
mesh.parent?.remove(mesh);
|
|
4105
|
+
mesh.geometry.dispose();
|
|
4106
|
+
mesh.material.dispose();
|
|
4107
|
+
}
|
|
4108
|
+
}
|
|
4109
|
+
// 为每个模板 Mesh 创建 InstancedMesh
|
|
4110
|
+
instancedMeshes = [];
|
|
4111
|
+
for (let i = 0; i < templateMeshes.length; i++) {
|
|
4112
|
+
const templateMesh = templateMeshes[i];
|
|
4113
|
+
const geometry = templateMesh.geometry;
|
|
4114
|
+
const material = templateMesh.material.clone();
|
|
4115
|
+
const instancedMesh = new THREE.InstancedMesh(geometry, material, nodeIds.length);
|
|
4116
|
+
instancedMesh.userData['instanceGroupId'] = group.id;
|
|
4117
|
+
instancedMesh.userData['instanceNodeIds'] = nodeIds;
|
|
4118
|
+
instancedMesh.userData['meshIndex'] = i;
|
|
4119
|
+
this.scene.add(instancedMesh);
|
|
4120
|
+
instancedMeshes.push(instancedMesh);
|
|
4121
|
+
}
|
|
4122
|
+
this.instanceMeshes.set(group.id, instancedMeshes);
|
|
4123
|
+
// 隐藏参与实例化的节点及其子 Mesh
|
|
4124
|
+
for (const nodeId of nodeIds) {
|
|
4125
|
+
const obj = this.objects.get(nodeId);
|
|
4126
|
+
if (obj) {
|
|
4127
|
+
obj.visible = false;
|
|
4128
|
+
obj.traverse(child => {
|
|
4129
|
+
if (child !== obj)
|
|
4130
|
+
child.visible = false;
|
|
4131
|
+
});
|
|
4132
|
+
}
|
|
4133
|
+
}
|
|
4134
|
+
this.markStatsDirty();
|
|
4135
|
+
}
|
|
4136
|
+
// 此时 instancedMeshes 必定存在
|
|
4137
|
+
if (!instancedMeshes)
|
|
4138
|
+
continue;
|
|
4139
|
+
// 更新每个 InstancedMesh 的实例矩阵
|
|
4140
|
+
for (let meshIdx = 0; meshIdx < instancedMeshes.length; meshIdx++) {
|
|
4141
|
+
const instancedMesh = instancedMeshes[meshIdx];
|
|
4142
|
+
const matrix = new THREE.Matrix4();
|
|
4143
|
+
for (let i = 0; i < nodeIds.length; i++) {
|
|
4144
|
+
const nodeId = nodeIds[i];
|
|
4145
|
+
const node = this.store.node(nodeId);
|
|
4146
|
+
const obj = this.objects.get(nodeId);
|
|
4147
|
+
if (!node || !obj)
|
|
4148
|
+
continue;
|
|
4149
|
+
// 找到对应的 mesh
|
|
4150
|
+
const meshes = [];
|
|
4151
|
+
obj.traverse(child => {
|
|
4152
|
+
if (child instanceof THREE.Mesh) {
|
|
4153
|
+
meshes.push(child);
|
|
4154
|
+
}
|
|
4155
|
+
});
|
|
4156
|
+
if (meshIdx >= meshes.length)
|
|
4157
|
+
continue;
|
|
4158
|
+
const mesh = meshes[meshIdx];
|
|
4159
|
+
// 计算世界矩阵(确保父链更新)
|
|
4160
|
+
mesh.updateWorldMatrix(true, true);
|
|
4161
|
+
matrix.copy(mesh.matrixWorld);
|
|
4162
|
+
instancedMesh.setMatrixAt(i, matrix);
|
|
4163
|
+
}
|
|
4164
|
+
instancedMesh.instanceMatrix.needsUpdate = true;
|
|
4165
|
+
}
|
|
4166
|
+
}
|
|
4167
|
+
// 清理不再存在的实例组
|
|
4168
|
+
for (const [groupId, meshes] of this.instanceMeshes) {
|
|
4169
|
+
if (!seenGroups.has(groupId)) {
|
|
4170
|
+
for (const mesh of meshes) {
|
|
4171
|
+
mesh.parent?.remove(mesh);
|
|
4172
|
+
mesh.geometry.dispose();
|
|
4173
|
+
mesh.material.dispose();
|
|
4174
|
+
}
|
|
4175
|
+
this.instanceMeshes.delete(groupId);
|
|
4176
|
+
// 恢复原始 mesh 的可见性
|
|
4177
|
+
const nodeIds = meshes[0]?.userData['instanceNodeIds'];
|
|
4178
|
+
if (nodeIds) {
|
|
4179
|
+
for (const nodeId of nodeIds) {
|
|
4180
|
+
const obj = this.objects.get(nodeId);
|
|
4181
|
+
if (obj) {
|
|
4182
|
+
obj.visible = true;
|
|
4183
|
+
obj.traverse(child => {
|
|
4184
|
+
if (child !== obj)
|
|
4185
|
+
child.visible = true;
|
|
4186
|
+
});
|
|
4187
|
+
}
|
|
4188
|
+
}
|
|
4189
|
+
}
|
|
4190
|
+
}
|
|
4191
|
+
}
|
|
4192
|
+
}
|
|
4193
|
+
/** 设置性能模式 */
|
|
4194
|
+
setPerformanceMode(enabled) {
|
|
4195
|
+
if (this.performanceMode === enabled)
|
|
4196
|
+
return;
|
|
4197
|
+
this.performanceMode = enabled;
|
|
4198
|
+
// 性能模式下关闭阴影,否则保持原设置
|
|
4199
|
+
// 注意:shadowMap.enabled 需要在 renderer 创建后设置,不能动态切换
|
|
4200
|
+
// 所以这里只影响渲染通道,阴影由初始化时的环境设置决定
|
|
4201
|
+
}
|
|
4202
|
+
/** 更新顶层节点集合(没有父节点的节点) */
|
|
4203
|
+
updateTopLevelNodes() {
|
|
4204
|
+
this.topLevelNodes.clear();
|
|
4205
|
+
for (const obj of this.objects.values()) {
|
|
4206
|
+
if (obj.parent === this.scene || obj.parent === null) {
|
|
4207
|
+
this.topLevelNodes.add(obj);
|
|
4208
|
+
}
|
|
4209
|
+
}
|
|
4210
|
+
}
|
|
4211
|
+
/** 视锥剔除:隐藏不在相机视锥内的顶层节点,大幅减少 CPU 遍历开销 */
|
|
4212
|
+
updateFrustumCulling() {
|
|
4213
|
+
if (this.topLevelNodes.size === 0)
|
|
4214
|
+
return;
|
|
4215
|
+
// 创建相机视锥
|
|
4216
|
+
const frustum = new THREE.Frustum();
|
|
4217
|
+
const projScreenMatrix = new THREE.Matrix4();
|
|
4218
|
+
projScreenMatrix.multiplyMatrices(this.camera.projectionMatrix, this.camera.matrixWorldInverse);
|
|
4219
|
+
frustum.setFromProjectionMatrix(projScreenMatrix);
|
|
4220
|
+
let hiddenCount = 0;
|
|
4221
|
+
let visibleCount = 0;
|
|
4222
|
+
for (const obj of this.topLevelNodes) {
|
|
4223
|
+
// 计算包围盒(世界空间)
|
|
4224
|
+
obj.updateWorldMatrix(false, false);
|
|
4225
|
+
const bbox = new THREE.Box3().setFromObject(obj);
|
|
4226
|
+
// 判断是否与视锥相交
|
|
4227
|
+
const inFrustum = frustum.intersectsBox(bbox);
|
|
4228
|
+
// 只在可见性变化时才设置,避免不必要的状态更新
|
|
4229
|
+
if (obj.visible !== inFrustum) {
|
|
4230
|
+
obj.visible = inFrustum;
|
|
4231
|
+
}
|
|
4232
|
+
if (inFrustum)
|
|
4233
|
+
visibleCount++;
|
|
4234
|
+
else
|
|
4235
|
+
hiddenCount++;
|
|
4236
|
+
}
|
|
4237
|
+
// 每 60 帧输出一次调试信息
|
|
4238
|
+
if (this.fpsFrames % 60 === 0) {
|
|
4239
|
+
console.log(`[视锥剔除] 顶层节点:${this.topLevelNodes.size},可见:${visibleCount},隐藏:${hiddenCount}`);
|
|
4240
|
+
}
|
|
4241
|
+
}
|
|
3694
4242
|
/** 创建标签 mesh */
|
|
3695
4243
|
createLabel(text) {
|
|
3696
4244
|
const canvas = document.createElement('canvas');
|
|
@@ -3896,7 +4444,7 @@ class SceneViewer {
|
|
|
3896
4444
|
const d = room.depth ?? 30;
|
|
3897
4445
|
const floorColor = (room.floorUseBaseColor ?? true) ? (room.floorColor ?? '#808080') : '#ffffff';
|
|
3898
4446
|
const wallColor = (room.wallUseBaseColor ?? true) ? (room.wallColor ?? '#c8c0b0') : '#ffffff';
|
|
3899
|
-
const ceilingColor = (room.ceilingUseBaseColor ?? true) ? (room.ceilingColor ?? '#
|
|
4447
|
+
const ceilingColor = (room.ceilingUseBaseColor ?? true) ? (room.ceilingColor ?? '#c8c0b0') : '#ffffff';
|
|
3900
4448
|
this.disposeRoom();
|
|
3901
4449
|
this.scene.background = null;
|
|
3902
4450
|
// 构建 6 面材质:[+X墙, -X墙, +Y天花板, -Y地板, +Z墙, -Z墙]
|
|
@@ -4051,13 +4599,14 @@ class SceneViewer {
|
|
|
4051
4599
|
move.sub(right);
|
|
4052
4600
|
if (keys.has('d'))
|
|
4053
4601
|
move.add(right);
|
|
4054
|
-
if (keys.has('q')
|
|
4602
|
+
if (keys.has('q'))
|
|
4055
4603
|
move.y -= 1;
|
|
4056
4604
|
if (keys.has('e') || keys.has(' '))
|
|
4057
4605
|
move.y += 1;
|
|
4058
4606
|
if (move.lengthSq() === 0)
|
|
4059
4607
|
return;
|
|
4060
|
-
|
|
4608
|
+
const speed = keys.has('shift') ? this.moveSpeed * 3 : this.moveSpeed; // Shift 加速 3 倍
|
|
4609
|
+
move.normalize().multiplyScalar(speed * 0.016); // 假设 60fps,约 16ms/帧
|
|
4061
4610
|
// 同时移动 orbit target 和 camera
|
|
4062
4611
|
this.orbit.target.add(move);
|
|
4063
4612
|
this.camera.position.add(move);
|
|
@@ -4128,15 +4677,13 @@ class SceneViewer {
|
|
|
4128
4677
|
this.composer.addPass(renderPass);
|
|
4129
4678
|
// 2. SSAO(屏幕空间环境光遮蔽)—— 增强角落/缝隙的阴影深度
|
|
4130
4679
|
const ssaoPass = new SSAOPass(this.scene, this.camera, w, h);
|
|
4131
|
-
ssaoPass.kernelRadius = 16;
|
|
4132
|
-
ssaoPass.minDistance = 0.005;
|
|
4133
|
-
ssaoPass.maxDistance = 0.1;
|
|
4680
|
+
ssaoPass.kernelRadius = 16;
|
|
4681
|
+
ssaoPass.minDistance = 0.005;
|
|
4682
|
+
ssaoPass.maxDistance = 0.1;
|
|
4134
4683
|
ssaoPass.output = SSAOPass.OUTPUT.Default;
|
|
4135
4684
|
this.composer.addPass(ssaoPass);
|
|
4136
4685
|
// 3. Bloom(泛光)—— 高亮区域发光效果
|
|
4137
|
-
const bloomPass = new UnrealBloomPass(new THREE.Vector2(w, h), 0.3,
|
|
4138
|
-
0.4, // radius:泛光扩散范围
|
|
4139
|
-
0.85);
|
|
4686
|
+
const bloomPass = new UnrealBloomPass(new THREE.Vector2(w, h), 0.3, 0.4, 0.85);
|
|
4140
4687
|
this.composer.addPass(bloomPass);
|
|
4141
4688
|
// 4. 输出通道(色调映射 + 色彩空间转换)
|
|
4142
4689
|
const outputPass = new OutputPass();
|
|
@@ -4719,21 +5266,21 @@ class SceneViewer {
|
|
|
4719
5266
|
const rows = widget.dataPoints.map((dp, i) => {
|
|
4720
5267
|
const dpValue = this.dashboardValues.get(`${widget.id}_dp${i}`);
|
|
4721
5268
|
const dpDisplay = this.formatValue(dpValue, { decimals: widget.decimals });
|
|
4722
|
-
return `
|
|
4723
|
-
<div style="display: flex; justify-content: space-between; align-items: center; padding: 4px 0; border-bottom: 1px solid rgba(255,255,255,0.05);">
|
|
4724
|
-
<span style="font-size: 12px; opacity: 0.7;">${dp.label}</span>
|
|
4725
|
-
<span style="font-size: 16px; font-weight: 500;">${dpDisplay}${dp.unit ? `<small style="font-size: 11px; opacity: 0.6; margin-left: 2px;">${dp.unit}</small>` : ''}</span>
|
|
4726
|
-
</div>
|
|
5269
|
+
return `
|
|
5270
|
+
<div style="display: flex; justify-content: space-between; align-items: center; padding: 4px 0; border-bottom: 1px solid rgba(255,255,255,0.05);">
|
|
5271
|
+
<span style="font-size: 12px; opacity: 0.7;">${dp.label}</span>
|
|
5272
|
+
<span style="font-size: 16px; font-weight: 500;">${dpDisplay}${dp.unit ? `<small style="font-size: 11px; opacity: 0.6; margin-left: 2px;">${dp.unit}</small>` : ''}</span>
|
|
5273
|
+
</div>
|
|
4727
5274
|
`;
|
|
4728
5275
|
}).join('');
|
|
4729
5276
|
el.innerHTML = rows;
|
|
4730
5277
|
}
|
|
4731
5278
|
else {
|
|
4732
5279
|
// 兼容旧的单值模式
|
|
4733
|
-
el.innerHTML = `
|
|
4734
|
-
<div style="font-size: 12px; opacity: 0.7; margin-bottom: 4px;">${widget.label || ''}</div>
|
|
4735
|
-
<div style="font-size: 28px; font-weight: 600;">${displayValue}</div>
|
|
4736
|
-
${widget.unit ? `<div style="font-size: 12px; opacity: 0.6; margin-top: 2px;">${widget.unit}</div>` : ''}
|
|
5280
|
+
el.innerHTML = `
|
|
5281
|
+
<div style="font-size: 12px; opacity: 0.7; margin-bottom: 4px;">${widget.label || ''}</div>
|
|
5282
|
+
<div style="font-size: 28px; font-weight: 600;">${displayValue}</div>
|
|
5283
|
+
${widget.unit ? `<div style="font-size: 12px; opacity: 0.6; margin-top: 2px;">${widget.unit}</div>` : ''}
|
|
4737
5284
|
`;
|
|
4738
5285
|
}
|
|
4739
5286
|
break;
|
|
@@ -4742,39 +5289,39 @@ class SceneViewer {
|
|
|
4742
5289
|
break;
|
|
4743
5290
|
case 'status':
|
|
4744
5291
|
const statusColor = this.getStatusColor(value, widget);
|
|
4745
|
-
el.innerHTML = `
|
|
4746
|
-
<div style="display: flex; align-items: center; gap: 8px;">
|
|
4747
|
-
<div style="width: 12px; height: 12px; border-radius: 50%; background: ${statusColor};"></div>
|
|
4748
|
-
<div style="font-size: 14px;">${widget.label || ''}</div>
|
|
4749
|
-
</div>
|
|
5292
|
+
el.innerHTML = `
|
|
5293
|
+
<div style="display: flex; align-items: center; gap: 8px;">
|
|
5294
|
+
<div style="width: 12px; height: 12px; border-radius: 50%; background: ${statusColor};"></div>
|
|
5295
|
+
<div style="font-size: 14px;">${widget.label || ''}</div>
|
|
5296
|
+
</div>
|
|
4750
5297
|
`;
|
|
4751
5298
|
break;
|
|
4752
5299
|
case 'progress':
|
|
4753
5300
|
const progress = this.calcProgress(value, widget);
|
|
4754
|
-
el.innerHTML = `
|
|
4755
|
-
<div style="font-size: 12px; opacity: 0.7; margin-bottom: 8px;">${widget.label || ''}</div>
|
|
4756
|
-
<div style="height: 8px; background: rgba(255,255,255,0.1); border-radius: 4px; overflow: hidden;">
|
|
4757
|
-
<div style="height: 100%; width: ${progress}%; background: #4f83cc; transition: width 0.3s;"></div>
|
|
4758
|
-
</div>
|
|
4759
|
-
<div style="font-size: 12px; text-align: right; margin-top: 4px;">${displayValue}</div>
|
|
5301
|
+
el.innerHTML = `
|
|
5302
|
+
<div style="font-size: 12px; opacity: 0.7; margin-bottom: 8px;">${widget.label || ''}</div>
|
|
5303
|
+
<div style="height: 8px; background: rgba(255,255,255,0.1); border-radius: 4px; overflow: hidden;">
|
|
5304
|
+
<div style="height: 100%; width: ${progress}%; background: #4f83cc; transition: width 0.3s;"></div>
|
|
5305
|
+
</div>
|
|
5306
|
+
<div style="font-size: 12px; text-align: right; margin-top: 4px;">${displayValue}</div>
|
|
4760
5307
|
`;
|
|
4761
5308
|
break;
|
|
4762
5309
|
case 'gauge':
|
|
4763
5310
|
// 简化版仪表:只显示数值
|
|
4764
5311
|
const gaugeProgress = this.calcProgress(value, widget);
|
|
4765
|
-
el.innerHTML = `
|
|
4766
|
-
<div style="font-size: 12px; opacity: 0.7; margin-bottom: 4px;">${widget.label || ''}</div>
|
|
4767
|
-
<div style="font-size: 24px; font-weight: 600; text-align: center;">${displayValue}</div>
|
|
4768
|
-
<div style="height: 4px; background: rgba(255,255,255,0.1); border-radius: 2px; margin-top: 8px; overflow: hidden;">
|
|
4769
|
-
<div style="height: 100%; width: ${gaugeProgress}%; background: #4f83cc;"></div>
|
|
4770
|
-
</div>
|
|
5312
|
+
el.innerHTML = `
|
|
5313
|
+
<div style="font-size: 12px; opacity: 0.7; margin-bottom: 4px;">${widget.label || ''}</div>
|
|
5314
|
+
<div style="font-size: 24px; font-weight: 600; text-align: center;">${displayValue}</div>
|
|
5315
|
+
<div style="height: 4px; background: rgba(255,255,255,0.1); border-radius: 2px; margin-top: 8px; overflow: hidden;">
|
|
5316
|
+
<div style="height: 100%; width: ${gaugeProgress}%; background: #4f83cc;"></div>
|
|
5317
|
+
</div>
|
|
4771
5318
|
`;
|
|
4772
5319
|
break;
|
|
4773
5320
|
case 'chart':
|
|
4774
5321
|
// 简化版:显示当前值
|
|
4775
|
-
el.innerHTML = `
|
|
4776
|
-
<div style="font-size: 12px; opacity: 0.7; margin-bottom: 4px;">${widget.label || ''}</div>
|
|
4777
|
-
<div style="font-size: 20px; font-weight: 500;">${displayValue}</div>
|
|
5322
|
+
el.innerHTML = `
|
|
5323
|
+
<div style="font-size: 12px; opacity: 0.7; margin-bottom: 4px;">${widget.label || ''}</div>
|
|
5324
|
+
<div style="font-size: 20px; font-weight: 500;">${displayValue}</div>
|
|
4778
5325
|
`;
|
|
4779
5326
|
break;
|
|
4780
5327
|
}
|
|
@@ -5433,6 +5980,59 @@ class SceneViewer {
|
|
|
5433
5980
|
}
|
|
5434
5981
|
this.lightHelpers.clear();
|
|
5435
5982
|
this.lightTargets.clear();
|
|
5983
|
+
// 清理贴图缓存
|
|
5984
|
+
for (const tex of this.textureCache.values())
|
|
5985
|
+
tex.dispose();
|
|
5986
|
+
this.textureCache.clear();
|
|
5987
|
+
// 清理资产缓存(GLB 模型的几何体/材质/纹理)
|
|
5988
|
+
for (const group of this.assetCache.values()) {
|
|
5989
|
+
group.traverse(child => {
|
|
5990
|
+
if (child instanceof THREE.Mesh) {
|
|
5991
|
+
child.geometry.dispose();
|
|
5992
|
+
const mats = Array.isArray(child.material) ? child.material : [child.material];
|
|
5993
|
+
for (const m of mats) {
|
|
5994
|
+
if (m instanceof THREE.MeshStandardMaterial) {
|
|
5995
|
+
m.map?.dispose();
|
|
5996
|
+
m.normalMap?.dispose();
|
|
5997
|
+
m.roughnessMap?.dispose();
|
|
5998
|
+
m.metalnessMap?.dispose();
|
|
5999
|
+
m.emissiveMap?.dispose();
|
|
6000
|
+
}
|
|
6001
|
+
m.dispose();
|
|
6002
|
+
}
|
|
6003
|
+
}
|
|
6004
|
+
});
|
|
6005
|
+
}
|
|
6006
|
+
this.assetCache.clear();
|
|
6007
|
+
this.originalMaterials.clear();
|
|
6008
|
+
// 清理标签
|
|
6009
|
+
for (const label of this.labelObjects.values()) {
|
|
6010
|
+
label.geometry.dispose();
|
|
6011
|
+
label.material.map?.dispose();
|
|
6012
|
+
label.material.dispose();
|
|
6013
|
+
}
|
|
6014
|
+
this.labelObjects.clear();
|
|
6015
|
+
// 清理告警弹窗
|
|
6016
|
+
for (const popup of this.alarmPopups.values()) {
|
|
6017
|
+
popup.geometry.dispose();
|
|
6018
|
+
popup.material.map?.dispose();
|
|
6019
|
+
popup.material.dispose();
|
|
6020
|
+
}
|
|
6021
|
+
this.alarmPopups.clear();
|
|
6022
|
+
this.alarmCreationTime.clear();
|
|
6023
|
+
// 清理屏幕内容定时器
|
|
6024
|
+
for (const state of this.screenContentStates.values()) {
|
|
6025
|
+
clearInterval(state.intervalId);
|
|
6026
|
+
}
|
|
6027
|
+
this.screenContentStates.clear();
|
|
6028
|
+
// 清理灯光选择器
|
|
6029
|
+
for (const picker of this.lightPickers.values()) {
|
|
6030
|
+
picker.geometry.dispose();
|
|
6031
|
+
picker.material.dispose();
|
|
6032
|
+
}
|
|
6033
|
+
this.lightPickers.clear();
|
|
6034
|
+
// 清理缩略图渲染器
|
|
6035
|
+
this.thumbRenderer?.dispose();
|
|
5436
6036
|
// 清理看板 DOM 和定时器
|
|
5437
6037
|
for (const el of this.dashboardElements.values()) {
|
|
5438
6038
|
el.remove();
|
|
@@ -5466,6 +6066,41 @@ class SceneViewer {
|
|
|
5466
6066
|
mesh.receiveShadow = true;
|
|
5467
6067
|
return mesh;
|
|
5468
6068
|
}
|
|
6069
|
+
/**
|
|
6070
|
+
* 参考 JCISceneParser 的 GdeepCloneMaterials
|
|
6071
|
+
* 几何体共享(不 clone),材质独立克隆
|
|
6072
|
+
* - Object3D/Group:clone(false) 不递归,手动处理 children
|
|
6073
|
+
* - Mesh:共享 geometry,克隆 material
|
|
6074
|
+
* - Light:完整克隆
|
|
6075
|
+
*/
|
|
6076
|
+
gdeepCloneMaterials(obj, nodeId) {
|
|
6077
|
+
let clone;
|
|
6078
|
+
if (obj instanceof THREE.Mesh) {
|
|
6079
|
+
// Mesh:几何体共享,材质克隆(关键:保留 geometry 引用)
|
|
6080
|
+
const material = obj.material ? obj.material.clone() : undefined;
|
|
6081
|
+
clone = new THREE.Mesh(obj.geometry, material);
|
|
6082
|
+
clone.castShadow = true;
|
|
6083
|
+
clone.receiveShadow = true;
|
|
6084
|
+
// 启用视锥剔除:不在相机视锥体内的 Mesh 不参与渲染
|
|
6085
|
+
clone.frustumCulled = true;
|
|
6086
|
+
}
|
|
6087
|
+
else if (obj instanceof THREE.Light) {
|
|
6088
|
+
// 灯光:完整克隆
|
|
6089
|
+
clone = obj.clone();
|
|
6090
|
+
}
|
|
6091
|
+
else {
|
|
6092
|
+
// Group 或其他 Object3D:clone(false) 不递归克隆 children
|
|
6093
|
+
clone = obj.clone(false);
|
|
6094
|
+
}
|
|
6095
|
+
// 复制通用属性
|
|
6096
|
+
clone.name = obj.name;
|
|
6097
|
+
clone.userData = { ...obj.userData, 'id': nodeId };
|
|
6098
|
+
// 递归处理 children
|
|
6099
|
+
for (const child of obj.children) {
|
|
6100
|
+
clone.add(this.gdeepCloneMaterials(child, nodeId));
|
|
6101
|
+
}
|
|
6102
|
+
return clone;
|
|
6103
|
+
}
|
|
5469
6104
|
/** 判断对象是否为灯光 */
|
|
5470
6105
|
isLight(obj) {
|
|
5471
6106
|
return obj instanceof THREE.Light;
|
|
@@ -5767,21 +6402,8 @@ class SceneViewer {
|
|
|
5767
6402
|
const node = this.store.node(nodeId);
|
|
5768
6403
|
if (!node)
|
|
5769
6404
|
return;
|
|
5770
|
-
|
|
5771
|
-
clone
|
|
5772
|
-
// 彻底断开材质共享:clone(true) 不会 clone 材质(只是引用复制),
|
|
5773
|
-
// 需要给每个 mesh 都分配独立材质副本,避免跨节点/跨 mesh 共享
|
|
5774
|
-
clone.traverse(child => {
|
|
5775
|
-
if (child instanceof THREE.Mesh) {
|
|
5776
|
-
child.userData['id'] = nodeId;
|
|
5777
|
-
child.castShadow = true;
|
|
5778
|
-
child.receiveShadow = true;
|
|
5779
|
-
const mat = child.material;
|
|
5780
|
-
if (mat) {
|
|
5781
|
-
child.material = mat.clone();
|
|
5782
|
-
}
|
|
5783
|
-
}
|
|
5784
|
-
});
|
|
6405
|
+
// 几何体共享,材质独立克隆(参考 JCISceneParser 的 GdeepCloneMaterials)
|
|
6406
|
+
const clone = this.gdeepCloneMaterials(assetScene, nodeId);
|
|
5785
6407
|
// 将 pivot 校正到几何中心:把子对象整体反向偏移 -center
|
|
5786
6408
|
// 这样外层 clone 的 pivot 就落在几何中心,旋转/缩放/吸附都以此为基准
|
|
5787
6409
|
const box = new THREE.Box3().setFromObject(clone);
|
|
@@ -5834,6 +6456,7 @@ class SceneViewer {
|
|
|
5834
6456
|
clone.scale.set(...node.scale);
|
|
5835
6457
|
clone.visible = node.visible;
|
|
5836
6458
|
clone.userData['assetLoaded'] = true;
|
|
6459
|
+
this.markStatsDirty();
|
|
5837
6460
|
// 层级关系
|
|
5838
6461
|
const parent = this.store.parentOf(nodeId);
|
|
5839
6462
|
const target = (parent && this.objects.get(parent.id)) || this.scene;
|
|
@@ -6088,13 +6711,11 @@ class SceneViewer {
|
|
|
6088
6711
|
obj.traverse(child => {
|
|
6089
6712
|
if (!(child instanceof THREE.Mesh))
|
|
6090
6713
|
return;
|
|
6091
|
-
// 给没有 name 的 mesh 设置默认名称(与 getMeshNames 保持一致)
|
|
6092
6714
|
if (!child.name)
|
|
6093
6715
|
child.name = `mesh_${unnamedIndex++}`;
|
|
6094
6716
|
let mat = child.material;
|
|
6095
6717
|
if (!mat)
|
|
6096
6718
|
return;
|
|
6097
|
-
// OBJ 加载的可能是其他材质类型,转换为 MeshStandardMaterial
|
|
6098
6719
|
if (!(mat instanceof THREE.MeshStandardMaterial)) {
|
|
6099
6720
|
const oldMat = mat;
|
|
6100
6721
|
const newMat = new THREE.MeshStandardMaterial();
|
|
@@ -6111,7 +6732,6 @@ class SceneViewer {
|
|
|
6111
6732
|
child.material = newMat;
|
|
6112
6733
|
mat = newMat;
|
|
6113
6734
|
}
|
|
6114
|
-
// 如果材质被多个 mesh 共享,给当前 mesh 分配独立副本
|
|
6115
6735
|
if (seenMaterials.has(mat)) {
|
|
6116
6736
|
const independentMat = mat.clone();
|
|
6117
6737
|
child.material = independentMat;
|
|
@@ -6127,14 +6747,13 @@ class SceneViewer {
|
|
|
6127
6747
|
resetMaterialToOriginal(nodeId, meshName, mat) {
|
|
6128
6748
|
const orig = this.originalMaterials.get(nodeId)?.[meshName];
|
|
6129
6749
|
if (!orig)
|
|
6130
|
-
return;
|
|
6750
|
+
return;
|
|
6131
6751
|
mat.color.set(orig.color);
|
|
6132
6752
|
mat.opacity = orig.opacity;
|
|
6133
6753
|
mat.transparent = orig.transparent;
|
|
6134
6754
|
mat.roughness = orig.roughness;
|
|
6135
6755
|
mat.metalness = orig.metalness;
|
|
6136
6756
|
mat.emissive.set(orig.emissive);
|
|
6137
|
-
// 恢复原始贴图
|
|
6138
6757
|
mat.map = orig.map ?? null;
|
|
6139
6758
|
mat.normalMap = orig.normalMap ?? null;
|
|
6140
6759
|
mat.roughnessMap = orig.roughnessMap ?? null;
|
|
@@ -6245,9 +6864,31 @@ class SceneViewer {
|
|
|
6245
6864
|
if (obj && !this.helpers.has(id)) {
|
|
6246
6865
|
if (node.primitive === 'asset' && !obj.userData['assetLoaded'])
|
|
6247
6866
|
continue;
|
|
6248
|
-
|
|
6867
|
+
// 计算 obj 的局部空间包围盒(忽略自身 transform,只看子节点)
|
|
6868
|
+
obj.updateWorldMatrix(true, false);
|
|
6869
|
+
const invObjMatrix = obj.matrixWorld.clone().invert();
|
|
6870
|
+
const localBox = new THREE.Box3();
|
|
6871
|
+
obj.traverse((child) => {
|
|
6872
|
+
const mesh = child;
|
|
6873
|
+
if (!mesh.isMesh || !mesh.geometry?.attributes['position'])
|
|
6874
|
+
return;
|
|
6875
|
+
mesh.updateWorldMatrix(true, false);
|
|
6876
|
+
const pos = mesh.geometry.attributes['position'];
|
|
6877
|
+
const tempVec = new THREE.Vector3();
|
|
6878
|
+
// mesh 相对于 obj 的变换
|
|
6879
|
+
const relMatrix = mesh.matrixWorld.clone().premultiply(invObjMatrix);
|
|
6880
|
+
for (let i = 0; i < pos.count; i++) {
|
|
6881
|
+
tempVec.fromBufferAttribute(pos, i);
|
|
6882
|
+
tempVec.applyMatrix4(relMatrix);
|
|
6883
|
+
localBox.expandByPoint(tempVec);
|
|
6884
|
+
}
|
|
6885
|
+
});
|
|
6886
|
+
localBox.expandByScalar(0.03);
|
|
6887
|
+
const h = new THREE.BoxHelper(new THREE.Object3D(), 0xfadb14);
|
|
6888
|
+
h.geometry.dispose();
|
|
6889
|
+
h.geometry = this.createBoxGeometryFromMinMax(localBox.min, localBox.max);
|
|
6249
6890
|
this.helpers.set(id, h);
|
|
6250
|
-
|
|
6891
|
+
obj.add(h);
|
|
6251
6892
|
}
|
|
6252
6893
|
}
|
|
6253
6894
|
}
|
|
@@ -6294,6 +6935,20 @@ class SceneViewer {
|
|
|
6294
6935
|
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
|
6295
6936
|
return geometry;
|
|
6296
6937
|
}
|
|
6938
|
+
/** 用局部坐标 min/max 创建包围盒几何体 */
|
|
6939
|
+
createBoxGeometryFromMinMax(min, max) {
|
|
6940
|
+
const positions = new Float32Array([
|
|
6941
|
+
min.x, min.y, max.z, max.x, min.y, max.z, max.x, min.y, max.z, max.x, max.y, max.z, max.x, max.y, max.z,
|
|
6942
|
+
min.x, max.y, max.z, min.x, max.y, max.z, min.x, min.y, max.z,
|
|
6943
|
+
min.x, min.y, min.z, max.x, min.y, min.z, max.x, min.y, min.z, max.x, max.y, min.z, max.x, max.y, min.z,
|
|
6944
|
+
min.x, max.y, min.z, min.x, max.y, min.z, min.x, min.y, min.z,
|
|
6945
|
+
min.x, min.y, min.z, min.x, min.y, max.z, max.x, min.y, min.z, max.x, min.y, max.z, max.x, max.y, min.z,
|
|
6946
|
+
max.x, max.y, max.z, min.x, max.y, min.z, min.x, max.y, max.z,
|
|
6947
|
+
]);
|
|
6948
|
+
const geometry = new THREE.BufferGeometry();
|
|
6949
|
+
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
|
6950
|
+
return geometry;
|
|
6951
|
+
}
|
|
6297
6952
|
/** 计算 group 的所有后代的包围盒(本地空间,相对于组的 Object3D) */
|
|
6298
6953
|
computeGroupLocalBounds(groupId) {
|
|
6299
6954
|
const box = new THREE.Box3();
|
|
@@ -6371,8 +7026,30 @@ class SceneViewer {
|
|
|
6371
7026
|
});
|
|
6372
7027
|
}
|
|
6373
7028
|
}
|
|
7029
|
+
// 添加 InstancedMesh 到拾取列表
|
|
7030
|
+
for (const meshes of this.instanceMeshes.values()) {
|
|
7031
|
+
for (const im of meshes) {
|
|
7032
|
+
allMeshes.push(im);
|
|
7033
|
+
}
|
|
7034
|
+
}
|
|
6374
7035
|
const hits = this.raycaster.intersectObjects(allMeshes, false);
|
|
6375
|
-
|
|
7036
|
+
if (hits.length === 0) {
|
|
7037
|
+
this.cb.onSelect({ id: null, additive: e.ctrlKey || e.metaKey });
|
|
7038
|
+
return;
|
|
7039
|
+
}
|
|
7040
|
+
const hit = hits[0];
|
|
7041
|
+
const obj = hit.object;
|
|
7042
|
+
// 检查是否是 InstancedMesh
|
|
7043
|
+
if (obj instanceof THREE.InstancedMesh && hit.instanceId !== undefined) {
|
|
7044
|
+
const nodeIds = obj.userData['instanceNodeIds'];
|
|
7045
|
+
if (nodeIds && hit.instanceId < nodeIds.length) {
|
|
7046
|
+
const id = nodeIds[hit.instanceId];
|
|
7047
|
+
this.cb.onSelect({ id, additive: e.ctrlKey || e.metaKey });
|
|
7048
|
+
return;
|
|
7049
|
+
}
|
|
7050
|
+
}
|
|
7051
|
+
// 普通 Mesh
|
|
7052
|
+
const id = obj.userData['id'] ?? null;
|
|
6376
7053
|
this.cb.onSelect({ id, additive: e.ctrlKey || e.metaKey });
|
|
6377
7054
|
}
|
|
6378
7055
|
// ── 框选方法 ──
|
|
@@ -6428,6 +7105,9 @@ class SceneViewer {
|
|
|
6428
7105
|
const node = this.store.node(id);
|
|
6429
7106
|
if (!node)
|
|
6430
7107
|
continue;
|
|
7108
|
+
// 跳过子节点(只选顶层对象)
|
|
7109
|
+
if (this.store.parentOf(id))
|
|
7110
|
+
continue;
|
|
6431
7111
|
const center = new THREE.Vector3();
|
|
6432
7112
|
if (obj instanceof THREE.Mesh) {
|
|
6433
7113
|
obj.getWorldPosition(center);
|
|
@@ -6561,6 +7241,192 @@ class SceneViewer {
|
|
|
6561
7241
|
Math.round(hit.z * 100) / 100,
|
|
6562
7242
|
];
|
|
6563
7243
|
}
|
|
7244
|
+
/** 获取当前 FPS */
|
|
7245
|
+
getFps() {
|
|
7246
|
+
return this.fpsCurrent;
|
|
7247
|
+
}
|
|
7248
|
+
/** 标记场景统计过期(模型加载/删除后调用) */
|
|
7249
|
+
markStatsDirty() {
|
|
7250
|
+
this.statsCache.dirty = true;
|
|
7251
|
+
}
|
|
7252
|
+
/** 获取场景顶点数和三角面数(带缓存,只统计根节点避免重复) */
|
|
7253
|
+
getSceneStats() {
|
|
7254
|
+
if (!this.statsCache.dirty)
|
|
7255
|
+
return { vertices: this.statsCache.vertices, faces: this.statsCache.faces, drawCalls: this.statsCache.drawCalls };
|
|
7256
|
+
console.log('[getSceneStats] 开始统计场景数据');
|
|
7257
|
+
let vertices = 0;
|
|
7258
|
+
let faces = 0;
|
|
7259
|
+
let drawCalls = 0;
|
|
7260
|
+
// 收集所有被实例化的节点 ID
|
|
7261
|
+
const instancedNodeIds = new Set();
|
|
7262
|
+
for (const group of this.store.doc.instanceGroups || []) {
|
|
7263
|
+
for (const nodeId of group.nodeIds) {
|
|
7264
|
+
instancedNodeIds.add(nodeId);
|
|
7265
|
+
}
|
|
7266
|
+
}
|
|
7267
|
+
console.log('[getSceneStats] 被实例化的节点数:', instancedNodeIds.size);
|
|
7268
|
+
// 只遍历逻辑根节点(无父节点的),traverse 自然覆盖所有后代,避免子节点被重复计数
|
|
7269
|
+
for (const [id, obj] of this.objects) {
|
|
7270
|
+
if (this.store.parentOf(id))
|
|
7271
|
+
continue; // 有父节点,跳过(会被父 traverse 覆盖)
|
|
7272
|
+
// 如果这个节点被实例化了,跳过(由 InstancedMesh 统计)
|
|
7273
|
+
if (instancedNodeIds.has(id)) {
|
|
7274
|
+
console.log('[getSceneStats] 跳过被实例化的节点:', id);
|
|
7275
|
+
continue;
|
|
7276
|
+
}
|
|
7277
|
+
obj.traverse(child => {
|
|
7278
|
+
// 跳过不可见的对象(包括被实例化隐藏的)
|
|
7279
|
+
// 需要检查对象本身及其所有祖先的 visible 状态
|
|
7280
|
+
let current = child;
|
|
7281
|
+
const visibleChain = [];
|
|
7282
|
+
while (current) {
|
|
7283
|
+
visibleChain.push(`${current.name || 'unnamed'}(visible=${current.visible})`);
|
|
7284
|
+
if (!current.visible) {
|
|
7285
|
+
console.log('[getSceneStats] 跳过不可见对象:', child.name || 'unnamed', '祖先链:', visibleChain.reverse().join(' -> '));
|
|
7286
|
+
return;
|
|
7287
|
+
}
|
|
7288
|
+
current = current.parent;
|
|
7289
|
+
}
|
|
7290
|
+
if (!child.isMesh)
|
|
7291
|
+
return;
|
|
7292
|
+
const mesh = child;
|
|
7293
|
+
// 跳过 InstancedMesh(后面统一统计 instanceMeshes)
|
|
7294
|
+
if (mesh instanceof THREE.InstancedMesh) {
|
|
7295
|
+
console.log('[getSceneStats] 跳过 InstancedMesh(在 traverse 中):', mesh.name || 'unnamed');
|
|
7296
|
+
return;
|
|
7297
|
+
}
|
|
7298
|
+
console.log('[getSceneStats] 统计 mesh(在 traverse 中):', mesh.name || 'unnamed', '几何体顶点数:', mesh.geometry.attributes['position']?.count || 0, '祖先链:', visibleChain.reverse().join(' -> '));
|
|
7299
|
+
drawCalls += 1;
|
|
7300
|
+
const geo = mesh.geometry;
|
|
7301
|
+
if (!geo.index) {
|
|
7302
|
+
const pos = geo.attributes['position'];
|
|
7303
|
+
if (pos) {
|
|
7304
|
+
vertices += pos.count;
|
|
7305
|
+
faces += pos.count / 3;
|
|
7306
|
+
}
|
|
7307
|
+
}
|
|
7308
|
+
else {
|
|
7309
|
+
const pos = geo.attributes['position'];
|
|
7310
|
+
if (pos)
|
|
7311
|
+
vertices += pos.count;
|
|
7312
|
+
faces += geo.index.count / 3;
|
|
7313
|
+
}
|
|
7314
|
+
});
|
|
7315
|
+
}
|
|
7316
|
+
// 统计 InstancedMesh
|
|
7317
|
+
console.log('[getSceneStats] 统计独立的 InstancedMesh,组数:', this.instanceMeshes.size);
|
|
7318
|
+
for (const [groupId, instancedMeshArray] of this.instanceMeshes) {
|
|
7319
|
+
for (const instancedMesh of instancedMeshArray) {
|
|
7320
|
+
drawCalls += 1;
|
|
7321
|
+
const geo = instancedMesh.geometry;
|
|
7322
|
+
if (!geo.index) {
|
|
7323
|
+
const pos = geo.attributes['position'];
|
|
7324
|
+
if (pos) {
|
|
7325
|
+
vertices += pos.count;
|
|
7326
|
+
faces += pos.count / 3;
|
|
7327
|
+
}
|
|
7328
|
+
}
|
|
7329
|
+
else {
|
|
7330
|
+
const pos = geo.attributes['position'];
|
|
7331
|
+
if (pos)
|
|
7332
|
+
vertices += pos.count;
|
|
7333
|
+
faces += geo.index.count / 3;
|
|
7334
|
+
}
|
|
7335
|
+
}
|
|
7336
|
+
}
|
|
7337
|
+
console.log('[getSceneStats] 最终统计结果 - 顶点:', vertices, '面:', faces, 'DrawCalls:', drawCalls);
|
|
7338
|
+
this.statsCache.vertices = vertices;
|
|
7339
|
+
this.statsCache.faces = faces;
|
|
7340
|
+
this.statsCache.drawCalls = drawCalls;
|
|
7341
|
+
this.statsCache.dirty = false;
|
|
7342
|
+
return { vertices, faces, drawCalls };
|
|
7343
|
+
}
|
|
7344
|
+
/** 获取指定节点的顶点数、面数、DrawCalls */
|
|
7345
|
+
getNodeStats(nodeIds) {
|
|
7346
|
+
let vertices = 0;
|
|
7347
|
+
let faces = 0;
|
|
7348
|
+
let drawCalls = 0;
|
|
7349
|
+
// 收集所有被实例化的节点 ID
|
|
7350
|
+
const instancedNodeIds = new Set();
|
|
7351
|
+
for (const group of this.store.doc.instanceGroups || []) {
|
|
7352
|
+
for (const nodeId of group.nodeIds) {
|
|
7353
|
+
instancedNodeIds.add(nodeId);
|
|
7354
|
+
}
|
|
7355
|
+
}
|
|
7356
|
+
// 统计被选中的节点中有多少被实例化了
|
|
7357
|
+
const selectedInstancedCount = nodeIds.filter(id => instancedNodeIds.has(id)).length;
|
|
7358
|
+
// 如果有被实例化的节点,统计 InstancedMesh
|
|
7359
|
+
if (selectedInstancedCount > 0) {
|
|
7360
|
+
// 找到对应的实例组
|
|
7361
|
+
for (const group of this.store.doc.instanceGroups || []) {
|
|
7362
|
+
// 检查这个组是否包含选中的节点
|
|
7363
|
+
const hasSelectedNode = group.nodeIds.some(id => nodeIds.includes(id));
|
|
7364
|
+
if (!hasSelectedNode)
|
|
7365
|
+
continue;
|
|
7366
|
+
// 统计这个组的 InstancedMesh
|
|
7367
|
+
const instancedMeshArray = this.instanceMeshes.get(group.id);
|
|
7368
|
+
if (instancedMeshArray) {
|
|
7369
|
+
for (const instancedMesh of instancedMeshArray) {
|
|
7370
|
+
drawCalls += 1;
|
|
7371
|
+
const geo = instancedMesh.geometry;
|
|
7372
|
+
if (!geo.index) {
|
|
7373
|
+
const pos = geo.attributes['position'];
|
|
7374
|
+
if (pos) {
|
|
7375
|
+
vertices += pos.count;
|
|
7376
|
+
faces += pos.count / 3;
|
|
7377
|
+
}
|
|
7378
|
+
}
|
|
7379
|
+
else {
|
|
7380
|
+
const pos = geo.attributes['position'];
|
|
7381
|
+
if (pos)
|
|
7382
|
+
vertices += pos.count;
|
|
7383
|
+
faces += geo.index.count / 3;
|
|
7384
|
+
}
|
|
7385
|
+
}
|
|
7386
|
+
}
|
|
7387
|
+
}
|
|
7388
|
+
}
|
|
7389
|
+
// 统计未被实例化的选中节点
|
|
7390
|
+
for (const id of nodeIds) {
|
|
7391
|
+
if (instancedNodeIds.has(id))
|
|
7392
|
+
continue; // 跳过被实例化的节点
|
|
7393
|
+
const obj = this.objects.get(id);
|
|
7394
|
+
if (!obj)
|
|
7395
|
+
continue;
|
|
7396
|
+
obj.traverse(child => {
|
|
7397
|
+
if (!child.isMesh)
|
|
7398
|
+
return;
|
|
7399
|
+
drawCalls++;
|
|
7400
|
+
const mesh = child;
|
|
7401
|
+
const geo = mesh.geometry;
|
|
7402
|
+
if (!geo.index) {
|
|
7403
|
+
const pos = geo.attributes['position'];
|
|
7404
|
+
if (pos) {
|
|
7405
|
+
vertices += pos.count;
|
|
7406
|
+
faces += pos.count / 3;
|
|
7407
|
+
}
|
|
7408
|
+
}
|
|
7409
|
+
else {
|
|
7410
|
+
const pos = geo.attributes['position'];
|
|
7411
|
+
if (pos)
|
|
7412
|
+
vertices += pos.count;
|
|
7413
|
+
faces += geo.index.count / 3;
|
|
7414
|
+
}
|
|
7415
|
+
});
|
|
7416
|
+
}
|
|
7417
|
+
return { vertices, faces, drawCalls };
|
|
7418
|
+
}
|
|
7419
|
+
/** 获取节点的世界空间包围盒 */
|
|
7420
|
+
getObjectBoundingBox(nodeId) {
|
|
7421
|
+
const obj = this.objects.get(nodeId);
|
|
7422
|
+
if (!obj)
|
|
7423
|
+
return null;
|
|
7424
|
+
const box = new THREE.Box3().setFromObject(obj);
|
|
7425
|
+
return {
|
|
7426
|
+
min: [box.min.x, box.min.y, box.min.z],
|
|
7427
|
+
max: [box.max.x, box.max.y, box.max.z],
|
|
7428
|
+
};
|
|
7429
|
+
}
|
|
6564
7430
|
}
|
|
6565
7431
|
|
|
6566
7432
|
/**
|
|
@@ -6578,6 +7444,7 @@ class SceneViewComponent {
|
|
|
6578
7444
|
transformEnd = new EventEmitter();
|
|
6579
7445
|
dropAsset = new EventEmitter();
|
|
6580
7446
|
thumbnailGenerated = new EventEmitter();
|
|
7447
|
+
cameraChange = new EventEmitter();
|
|
6581
7448
|
pathPointMoved = new EventEmitter();
|
|
6582
7449
|
addPathPoint = new EventEmitter();
|
|
6583
7450
|
deletePathPoint = new EventEmitter();
|
|
@@ -6604,7 +7471,11 @@ class SceneViewComponent {
|
|
|
6604
7471
|
onSegmentRightClick: (nodeId, effectIndex, segmentIndex, position, event) => this.zone.run(() => this.segmentRightClick.emit({ nodeId, effectIndex, segmentIndex, position, event })),
|
|
6605
7472
|
onModelSurfaceClick: (nodeId, effectIndex, position, event) => this.zone.run(() => this.modelSurfaceClick.emit({ nodeId, effectIndex, position, event })),
|
|
6606
7473
|
onMovePathPointToEnd: (nodeId, effectIndex, pointIndex, toStart) => this.zone.run(() => this.movePathPointToEnd.emit({ nodeId, effectIndex, pointIndex, toStart })),
|
|
6607
|
-
}
|
|
7474
|
+
});
|
|
7475
|
+
// 相机视角变化回调
|
|
7476
|
+
this.viewer.onCameraChange = () => {
|
|
7477
|
+
this.zone.run(() => this.cameraChange.emit());
|
|
7478
|
+
};
|
|
6608
7479
|
this.viewer.setMode(this.mode);
|
|
6609
7480
|
});
|
|
6610
7481
|
}
|
|
@@ -6747,6 +7618,10 @@ class SceneViewComponent {
|
|
|
6747
7618
|
hideAlarm(nodeId) {
|
|
6748
7619
|
this.viewer?.hideAlarm(nodeId);
|
|
6749
7620
|
}
|
|
7621
|
+
/** 设置性能模式 */
|
|
7622
|
+
setPerformanceMode(enabled) {
|
|
7623
|
+
this.viewer?.setPerformanceMode(enabled);
|
|
7624
|
+
}
|
|
6750
7625
|
/** 拖拽悬停:允许放置 */
|
|
6751
7626
|
onDragOver(e) {
|
|
6752
7627
|
e.preventDefault();
|
|
@@ -6773,17 +7648,29 @@ class SceneViewComponent {
|
|
|
6773
7648
|
getViewer() {
|
|
6774
7649
|
return this.viewer;
|
|
6775
7650
|
}
|
|
7651
|
+
getFps() {
|
|
7652
|
+
return this.viewer?.getFps() ?? 0;
|
|
7653
|
+
}
|
|
7654
|
+
getSceneStats() {
|
|
7655
|
+
return this.viewer?.getSceneStats() ?? { vertices: 0, faces: 0, drawCalls: 0 };
|
|
7656
|
+
}
|
|
7657
|
+
getNodeStats(nodeIds) {
|
|
7658
|
+
return this.viewer?.getNodeStats(nodeIds) ?? { vertices: 0, faces: 0, drawCalls: 0 };
|
|
7659
|
+
}
|
|
7660
|
+
getObjectBoundingBox(nodeId) {
|
|
7661
|
+
return this.viewer?.getObjectBoundingBox(nodeId) ?? null;
|
|
7662
|
+
}
|
|
6776
7663
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.34", ngImport: i0, type: SceneViewComponent, deps: [{ token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Component });
|
|
6777
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.34", type: SceneViewComponent, isStandalone: true, selector: "twin-scene-view", inputs: { store: "store", mode: "mode", isEditorMode: "isEditorMode" }, outputs: { select: "select", transformEnd: "transformEnd", dropAsset: "dropAsset", thumbnailGenerated: "thumbnailGenerated", pathPointMoved: "pathPointMoved", addPathPoint: "addPathPoint", deletePathPoint: "deletePathPoint", pathEditingChanged: "pathEditingChanged", pathPointRightClick: "pathPointRightClick", segmentRightClick: "segmentRightClick", modelSurfaceClick: "modelSurfaceClick", movePathPointToEnd: "movePathPointToEnd" }, viewQueries: [{ propertyName: "host", first: true, predicate: ["host"], descendants: true, static: true }, { propertyName: "dashboardOverlay", first: true, predicate: ["dashboardOverlay"], descendants: true, static: true }], usesOnChanges: true, ngImport: i0, template: `
|
|
6778
|
-
<div #host class="scene-host" (dragover)="onDragOver($event)" (drop)="onDrop($event)"></div>
|
|
6779
|
-
<div #dashboardOverlay class="dashboard-overlay"></div>
|
|
7664
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.34", type: SceneViewComponent, isStandalone: true, selector: "twin-scene-view", inputs: { store: "store", mode: "mode", isEditorMode: "isEditorMode" }, outputs: { select: "select", transformEnd: "transformEnd", dropAsset: "dropAsset", thumbnailGenerated: "thumbnailGenerated", cameraChange: "cameraChange", pathPointMoved: "pathPointMoved", addPathPoint: "addPathPoint", deletePathPoint: "deletePathPoint", pathEditingChanged: "pathEditingChanged", pathPointRightClick: "pathPointRightClick", segmentRightClick: "segmentRightClick", modelSurfaceClick: "modelSurfaceClick", movePathPointToEnd: "movePathPointToEnd" }, viewQueries: [{ propertyName: "host", first: true, predicate: ["host"], descendants: true, static: true }, { propertyName: "dashboardOverlay", first: true, predicate: ["dashboardOverlay"], descendants: true, static: true }], usesOnChanges: true, ngImport: i0, template: `
|
|
7665
|
+
<div #host class="scene-host" (dragover)="onDragOver($event)" (drop)="onDrop($event)"></div>
|
|
7666
|
+
<div #dashboardOverlay class="dashboard-overlay"></div>
|
|
6780
7667
|
`, isInline: true, styles: [":host{display:block;width:100%;height:100%;position:relative}.scene-host{width:100%;height:100%;overflow:hidden}.scene-host canvas{display:block}.dashboard-overlay{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;overflow:hidden}.dashboard-overlay .dashboard-widget{pointer-events:auto}\n"] });
|
|
6781
7668
|
}
|
|
6782
7669
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.34", ngImport: i0, type: SceneViewComponent, decorators: [{
|
|
6783
7670
|
type: Component,
|
|
6784
|
-
args: [{ selector: 'twin-scene-view', standalone: true, template: `
|
|
6785
|
-
<div #host class="scene-host" (dragover)="onDragOver($event)" (drop)="onDrop($event)"></div>
|
|
6786
|
-
<div #dashboardOverlay class="dashboard-overlay"></div>
|
|
7671
|
+
args: [{ selector: 'twin-scene-view', standalone: true, template: `
|
|
7672
|
+
<div #host class="scene-host" (dragover)="onDragOver($event)" (drop)="onDrop($event)"></div>
|
|
7673
|
+
<div #dashboardOverlay class="dashboard-overlay"></div>
|
|
6787
7674
|
`, styles: [":host{display:block;width:100%;height:100%;position:relative}.scene-host{width:100%;height:100%;overflow:hidden}.scene-host canvas{display:block}.dashboard-overlay{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;overflow:hidden}.dashboard-overlay .dashboard-widget{pointer-events:auto}\n"] }]
|
|
6788
7675
|
}], ctorParameters: () => [{ type: i0.NgZone }], propDecorators: { host: [{
|
|
6789
7676
|
type: ViewChild,
|
|
@@ -6805,6 +7692,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.34", ngImpo
|
|
|
6805
7692
|
type: Output
|
|
6806
7693
|
}], thumbnailGenerated: [{
|
|
6807
7694
|
type: Output
|
|
7695
|
+
}], cameraChange: [{
|
|
7696
|
+
type: Output
|
|
6808
7697
|
}], pathPointMoved: [{
|
|
6809
7698
|
type: Output
|
|
6810
7699
|
}], addPathPoint: [{
|
|
@@ -6834,5 +7723,5 @@ const DRACO_ENCODER_PATH = 'assets/draco/encoder/';
|
|
|
6834
7723
|
* Generated bundle index. Do not edit.
|
|
6835
7724
|
*/
|
|
6836
7725
|
|
|
6837
|
-
export { AddAnimationCommand, AddEffectCommand, AddNodeCommand, BatchRemoveCommand, DRACO_DECODER_PATH, DRACO_ENCODER_PATH, FlyPathCommand, GroupCommand, History, InsertPathPointCommand, KeyframeCommand, MaterialOverrideCommand, MovePathPointToEndCommand, ParticleRuntime, PathEditor, PathParticleRuntime, RemoveAnimationCommand, RemoveEffectCommand, RemoveNodeCommand, RemovePathPointCommand, ReparentCommand, SceneStore, SceneViewComponent, SceneViewer, SegmentedPath, SetAlarmCommand, SetDashboardCommand, SetEnvironmentCommand, SetLightPropertyCommand, SetNodeNameCommand, SetNodeTextCommand, SyncMaterialCommand, ToggleSegmentTypeCommand, TransformCommand, UngroupCommand, UpdateAnimationCommand, UpdateEffectCommand, UpdatePathPointsCommand, clone, createNode, createSceneDoc, nextId };
|
|
7726
|
+
export { AddAnimationCommand, AddEffectCommand, AddNodeCommand, AlignCommand, BatchAddCommand, BatchRemoveCommand, DRACO_DECODER_PATH, DRACO_ENCODER_PATH, DisableInstancingCommand, EnableInstancingCommand, FlyPathCommand, GroupCommand, History, InsertPathPointCommand, KeyframeCommand, MaterialOverrideCommand, MovePathPointToEndCommand, ParticleRuntime, PathEditor, PathParticleRuntime, RemoveAnimationCommand, RemoveEffectCommand, RemoveNodeCommand, RemovePathPointCommand, ReparentCommand, SceneStore, SceneViewComponent, SceneViewer, SegmentedPath, SetAlarmCommand, SetDashboardCommand, SetEnvironmentCommand, SetLightPropertyCommand, SetNodeNameCommand, SetNodeTextCommand, SyncMaterialCommand, ToggleSegmentTypeCommand, TransformCommand, UngroupCommand, UpdateAnimationCommand, UpdateEffectCommand, UpdatePathPointsCommand, clone, createNode, createSceneDoc, nextId };
|
|
6838
7727
|
//# sourceMappingURL=fengyizhi-scene-lib.mjs.map
|