@sequent-org/ifc-viewer 1.2.4-ci.61.0 → 1.2.4-ci.62.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/styles-local.css +7 -0
- package/src/ui/LabelPlacementController.js +173 -10
- package/src/viewer/Viewer.js +42 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sequent-org/ifc-viewer",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "1.2.4-ci.
|
|
4
|
+
"version": "1.2.4-ci.62.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "IFC 3D model viewer component for web applications - fully self-contained with local IFCLoader",
|
|
7
7
|
"main": "src/index.js",
|
package/src/styles-local.css
CHANGED
|
@@ -182,6 +182,7 @@
|
|
|
182
182
|
display: block;
|
|
183
183
|
transform: translate(-50%, -50%);
|
|
184
184
|
will-change: transform;
|
|
185
|
+
transition: opacity 160ms ease;
|
|
185
186
|
}
|
|
186
187
|
|
|
187
188
|
.ifc-label-ghost,
|
|
@@ -223,6 +224,12 @@
|
|
|
223
224
|
cursor: pointer;
|
|
224
225
|
}
|
|
225
226
|
|
|
227
|
+
.ifc-label-marker--hidden,
|
|
228
|
+
.ifc-card-marker--hidden {
|
|
229
|
+
opacity: 0;
|
|
230
|
+
pointer-events: none;
|
|
231
|
+
}
|
|
232
|
+
|
|
226
233
|
/* Клик ловим на контейнере метки, дочерние элементы пусть не перехватывают */
|
|
227
234
|
.ifc-label-marker .ifc-label-dot,
|
|
228
235
|
.ifc-label-marker .ifc-label-num,
|
|
@@ -13,6 +13,8 @@ class LabelMarker {
|
|
|
13
13
|
this.localPoint = deps.localPoint;
|
|
14
14
|
this.el = deps.el;
|
|
15
15
|
this.sceneState = deps.sceneState || null;
|
|
16
|
+
this.visible = null;
|
|
17
|
+
this.hiddenReason = null;
|
|
16
18
|
}
|
|
17
19
|
}
|
|
18
20
|
|
|
@@ -34,6 +36,7 @@ export class LabelPlacementController {
|
|
|
34
36
|
this.viewer = deps.viewer;
|
|
35
37
|
this.container = deps.container;
|
|
36
38
|
this.logger = deps.logger || null;
|
|
39
|
+
this._visibilityLogEnabled = !!deps.visibilityLogEnabled;
|
|
37
40
|
|
|
38
41
|
this._placing = false;
|
|
39
42
|
this._nextId = 1;
|
|
@@ -46,6 +49,8 @@ export class LabelPlacementController {
|
|
|
46
49
|
this._ndc = new THREE.Vector2();
|
|
47
50
|
this._tmpV = new THREE.Vector3();
|
|
48
51
|
this._tmpLocal = new THREE.Vector3();
|
|
52
|
+
this._tmpV2 = new THREE.Vector3();
|
|
53
|
+
this._tmpNdc = new THREE.Vector3();
|
|
49
54
|
|
|
50
55
|
this._lastPointer = { x: 0, y: 0 };
|
|
51
56
|
this._ghostPos = { x: 0, y: 0 };
|
|
@@ -97,6 +102,19 @@ export class LabelPlacementController {
|
|
|
97
102
|
v2: new THREE.Vector3(),
|
|
98
103
|
};
|
|
99
104
|
|
|
105
|
+
this._autoHide = {
|
|
106
|
+
active: false,
|
|
107
|
+
prevHidden: false,
|
|
108
|
+
};
|
|
109
|
+
this._showAfterStop = {
|
|
110
|
+
raf: 0,
|
|
111
|
+
active: false,
|
|
112
|
+
lastChangeTs: 0,
|
|
113
|
+
idleMs: 0,
|
|
114
|
+
eps: 5e-4,
|
|
115
|
+
lastCamMatrix: new Float32Array(16),
|
|
116
|
+
};
|
|
117
|
+
|
|
100
118
|
this._ui = this.#createUi();
|
|
101
119
|
this.#attachUi();
|
|
102
120
|
this.#bindEvents();
|
|
@@ -140,6 +158,14 @@ export class LabelPlacementController {
|
|
|
140
158
|
try { dom?.removeEventListener("contextmenu", this._onCanvasContextMenu, { capture: true }); } catch (_) {
|
|
141
159
|
try { dom?.removeEventListener("contextmenu", this._onCanvasContextMenu); } catch (_) {}
|
|
142
160
|
}
|
|
161
|
+
try {
|
|
162
|
+
const controls = this.viewer?.controls;
|
|
163
|
+
if (controls && typeof controls.removeEventListener === "function") {
|
|
164
|
+
try { controls.removeEventListener("start", this._onControlsStart); } catch (_) {}
|
|
165
|
+
try { controls.removeEventListener("end", this._onControlsEnd); } catch (_) {}
|
|
166
|
+
}
|
|
167
|
+
} catch (_) {}
|
|
168
|
+
try { this.#cancelShowAfterStop(); } catch (_) {}
|
|
143
169
|
|
|
144
170
|
if (this._raf) cancelAnimationFrame(this._raf);
|
|
145
171
|
this._raf = 0;
|
|
@@ -920,6 +946,19 @@ export class LabelPlacementController {
|
|
|
920
946
|
this.#openCanvasMenu(hit, e.clientX, e.clientY);
|
|
921
947
|
};
|
|
922
948
|
dom.addEventListener("contextmenu", this._onCanvasContextMenu, { capture: true, passive: false });
|
|
949
|
+
|
|
950
|
+
const controls = this.viewer?.controls;
|
|
951
|
+
if (controls && typeof controls.addEventListener === "function") {
|
|
952
|
+
this._onControlsStart = () => {
|
|
953
|
+
this.#beginAutoHideForControls();
|
|
954
|
+
};
|
|
955
|
+
this._onControlsEnd = () => {
|
|
956
|
+
this.#scheduleShowAfterStop();
|
|
957
|
+
};
|
|
958
|
+
try { controls.addEventListener("start", this._onControlsStart); } catch (_) {}
|
|
959
|
+
try { controls.addEventListener("end", this._onControlsEnd); } catch (_) {}
|
|
960
|
+
}
|
|
961
|
+
|
|
923
962
|
}
|
|
924
963
|
|
|
925
964
|
#setGhostVisible(visible) {
|
|
@@ -1108,6 +1147,7 @@ export class LabelPlacementController {
|
|
|
1108
1147
|
let best = null;
|
|
1109
1148
|
for (const h of hits) {
|
|
1110
1149
|
if (!h || !h.point) continue;
|
|
1150
|
+
if (this.#isPointClippedBySection(h.point)) continue;
|
|
1111
1151
|
const obj = h.object;
|
|
1112
1152
|
if (obj && obj.isMesh) { best = h; break; }
|
|
1113
1153
|
}
|
|
@@ -1460,12 +1500,12 @@ export class LabelPlacementController {
|
|
|
1460
1500
|
if (!m || !m.el) continue;
|
|
1461
1501
|
|
|
1462
1502
|
if (this._labelsHidden) {
|
|
1463
|
-
m
|
|
1503
|
+
this.#setMarkerVisibility(m, false, "labelsHidden");
|
|
1464
1504
|
continue;
|
|
1465
1505
|
}
|
|
1466
1506
|
|
|
1467
1507
|
if (!model) {
|
|
1468
|
-
m
|
|
1508
|
+
this.#setMarkerVisibility(m, false, "noModel");
|
|
1469
1509
|
continue;
|
|
1470
1510
|
}
|
|
1471
1511
|
|
|
@@ -1474,16 +1514,20 @@ export class LabelPlacementController {
|
|
|
1474
1514
|
model.localToWorld(this._tmpV);
|
|
1475
1515
|
|
|
1476
1516
|
if (this.#isPointClippedBySection(this._tmpV)) {
|
|
1477
|
-
m
|
|
1517
|
+
this.#setMarkerVisibility(m, false, "clipped");
|
|
1478
1518
|
continue;
|
|
1479
1519
|
}
|
|
1480
1520
|
|
|
1481
|
-
const ndc = this._tmpV.project(camera);
|
|
1521
|
+
const ndc = this._tmpNdc.copy(this._tmpV).project(camera);
|
|
1482
1522
|
|
|
1483
|
-
// Если точка за камерой или
|
|
1484
|
-
const
|
|
1485
|
-
|
|
1486
|
-
|
|
1523
|
+
// Если точка за камерой или вне кадра — скрываем
|
|
1524
|
+
const ndcFinite = Number.isFinite(ndc.x) && Number.isFinite(ndc.y) && Number.isFinite(ndc.z);
|
|
1525
|
+
const inView = ndcFinite
|
|
1526
|
+
&& ndc.x >= -1 && ndc.x <= 1
|
|
1527
|
+
&& ndc.y >= -1 && ndc.y <= 1
|
|
1528
|
+
&& ndc.z >= -1 && ndc.z <= 1;
|
|
1529
|
+
if (!inView) {
|
|
1530
|
+
this.#setMarkerVisibility(m, false, "outOfView");
|
|
1487
1531
|
continue;
|
|
1488
1532
|
}
|
|
1489
1533
|
|
|
@@ -1491,15 +1535,62 @@ export class LabelPlacementController {
|
|
|
1491
1535
|
const y = (-ndc.y * 0.5 + 0.5) * h;
|
|
1492
1536
|
|
|
1493
1537
|
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
1494
|
-
m
|
|
1538
|
+
this.#setMarkerVisibility(m, false, "invalidScreen");
|
|
1539
|
+
continue;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
const occluded = this.#isPointOccludedByModel(this._tmpV, ndc, model, camera);
|
|
1543
|
+
if (occluded) {
|
|
1544
|
+
this.#setMarkerVisibility(m, false, "occluded");
|
|
1495
1545
|
continue;
|
|
1496
1546
|
}
|
|
1497
1547
|
|
|
1498
|
-
m
|
|
1548
|
+
this.#setMarkerVisibility(m, true, "visible");
|
|
1499
1549
|
m.el.style.transform = `translate3d(${x}px, ${y}px, 0) translate(-50%, -50%)`;
|
|
1500
1550
|
}
|
|
1501
1551
|
}
|
|
1502
1552
|
|
|
1553
|
+
#setMarkerVisibility(marker, visible, reason) {
|
|
1554
|
+
if (!marker || !marker.el) return;
|
|
1555
|
+
if (visible) {
|
|
1556
|
+
marker.el.style.display = "block";
|
|
1557
|
+
try { marker.el.classList.remove("ifc-label-marker--hidden"); } catch (_) {}
|
|
1558
|
+
} else {
|
|
1559
|
+
try { marker.el.classList.add("ifc-label-marker--hidden"); } catch (_) {}
|
|
1560
|
+
marker.el.style.display = "block";
|
|
1561
|
+
}
|
|
1562
|
+
if (!this._visibilityLogEnabled) return;
|
|
1563
|
+
if (marker.visible === visible && marker.hiddenReason === reason) return;
|
|
1564
|
+
marker.visible = visible;
|
|
1565
|
+
marker.hiddenReason = reason;
|
|
1566
|
+
this.logger?.log?.("[LabelVisibility]", {
|
|
1567
|
+
id: marker.id,
|
|
1568
|
+
visible,
|
|
1569
|
+
reason,
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
#isPointOccludedByModel(pointWorld, ndc, model, camera) {
|
|
1574
|
+
if (!pointWorld || !ndc || !model || !camera) return false;
|
|
1575
|
+
this._raycaster.setFromCamera(ndc, camera);
|
|
1576
|
+
const hits = this._raycaster.intersectObject(model, true);
|
|
1577
|
+
if (!hits || hits.length === 0) return false;
|
|
1578
|
+
let hit = null;
|
|
1579
|
+
for (const h of hits) {
|
|
1580
|
+
if (!h || !h.object || !h.object.isMesh) continue;
|
|
1581
|
+
if (this.#isPointClippedBySection(h.point)) continue;
|
|
1582
|
+
hit = h;
|
|
1583
|
+
break;
|
|
1584
|
+
}
|
|
1585
|
+
if (!hit || !Number.isFinite(hit.distance)) return false;
|
|
1586
|
+
const ray = this._raycaster.ray;
|
|
1587
|
+
this._tmpV2.copy(pointWorld).sub(ray.origin);
|
|
1588
|
+
const t = this._tmpV2.dot(ray.direction);
|
|
1589
|
+
if (!Number.isFinite(t) || t <= 0) return false;
|
|
1590
|
+
const epsilon = 1e-2;
|
|
1591
|
+
return hit.distance + epsilon < t;
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1503
1594
|
#isPointClippedBySection(pointWorld) {
|
|
1504
1595
|
const planes = this.viewer?.clipping?.planes || [];
|
|
1505
1596
|
for (const plane of planes) {
|
|
@@ -1510,6 +1601,12 @@ export class LabelPlacementController {
|
|
|
1510
1601
|
return false;
|
|
1511
1602
|
}
|
|
1512
1603
|
|
|
1604
|
+
#copyMatrix(matrix, cache) {
|
|
1605
|
+
if (!matrix || !cache || cache.length !== 16) return;
|
|
1606
|
+
const e = matrix.elements;
|
|
1607
|
+
for (let i = 0; i < 16; i += 1) cache[i] = e[i] ?? 0;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1513
1610
|
#setLabelsHidden(hidden) {
|
|
1514
1611
|
const next = !!hidden;
|
|
1515
1612
|
if (this._labelsHidden === next) return;
|
|
@@ -1522,6 +1619,72 @@ export class LabelPlacementController {
|
|
|
1522
1619
|
this.#syncHideButton();
|
|
1523
1620
|
}
|
|
1524
1621
|
|
|
1622
|
+
#beginAutoHideForControls() {
|
|
1623
|
+
if (!this._autoHide.active) {
|
|
1624
|
+
this._autoHide.prevHidden = this._labelsHidden;
|
|
1625
|
+
this._autoHide.active = true;
|
|
1626
|
+
}
|
|
1627
|
+
this._labelsHidden = true;
|
|
1628
|
+
this.#syncHideButton();
|
|
1629
|
+
this.#cancelShowAfterStop();
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
#scheduleShowAfterStop() {
|
|
1633
|
+
if (!this._autoHide.active) return;
|
|
1634
|
+
const cam = this.viewer?.camera;
|
|
1635
|
+
if (!cam) return;
|
|
1636
|
+
this._showAfterStop.active = true;
|
|
1637
|
+
this._showAfterStop.lastChangeTs = performance.now();
|
|
1638
|
+
this.#copyMatrix(cam.matrixWorld, this._showAfterStop.lastCamMatrix);
|
|
1639
|
+
if (!this._showAfterStop.raf) {
|
|
1640
|
+
this._showAfterStop.raf = requestAnimationFrame(() => this.#tickShowAfterStop());
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
#cancelShowAfterStop() {
|
|
1645
|
+
if (this._showAfterStop.raf) cancelAnimationFrame(this._showAfterStop.raf);
|
|
1646
|
+
this._showAfterStop.raf = 0;
|
|
1647
|
+
this._showAfterStop.active = false;
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
#tickShowAfterStop() {
|
|
1651
|
+
if (!this._showAfterStop.active) {
|
|
1652
|
+
this._showAfterStop.raf = 0;
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
const cam = this.viewer?.camera;
|
|
1656
|
+
if (!cam) {
|
|
1657
|
+
this._showAfterStop.raf = 0;
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
const now = performance.now();
|
|
1661
|
+
if (!this.#isCameraStable(cam.matrixWorld, this._showAfterStop.lastCamMatrix, this._showAfterStop.eps)) {
|
|
1662
|
+
this._showAfterStop.lastChangeTs = now;
|
|
1663
|
+
}
|
|
1664
|
+
if (now - this._showAfterStop.lastChangeTs >= this._showAfterStop.idleMs) {
|
|
1665
|
+
this._labelsHidden = this._autoHide.prevHidden;
|
|
1666
|
+
this._autoHide.active = false;
|
|
1667
|
+
this.#syncHideButton();
|
|
1668
|
+
this._showAfterStop.active = false;
|
|
1669
|
+
this._showAfterStop.raf = 0;
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
this._showAfterStop.raf = requestAnimationFrame(() => this.#tickShowAfterStop());
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
#isCameraStable(matrix, cache, eps) {
|
|
1676
|
+
if (!matrix || !cache || cache.length !== 16) return true;
|
|
1677
|
+
const e = matrix.elements;
|
|
1678
|
+
let stable = true;
|
|
1679
|
+
for (let i = 0; i < 16; i += 1) {
|
|
1680
|
+
const v = e[i] ?? 0;
|
|
1681
|
+
const d = Math.abs(v - cache[i]);
|
|
1682
|
+
if (d > eps) stable = false;
|
|
1683
|
+
cache[i] = v;
|
|
1684
|
+
}
|
|
1685
|
+
return stable;
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1525
1688
|
#syncHideButton() {
|
|
1526
1689
|
const btn = this._ui?.hideBtn;
|
|
1527
1690
|
if (!btn) return;
|
package/src/viewer/Viewer.js
CHANGED
|
@@ -190,6 +190,15 @@ export class Viewer {
|
|
|
190
190
|
this._rotAngleEps = 0.01; // ~0.57° минимальный угловой сдвиг
|
|
191
191
|
this._axisEmaAlpha = 0.15; // коэффициент сглаживания оси
|
|
192
192
|
|
|
193
|
+
this._damping = {
|
|
194
|
+
dynamic: true,
|
|
195
|
+
base: 0.06,
|
|
196
|
+
settle: 0.18,
|
|
197
|
+
settleMs: 250,
|
|
198
|
+
isSettling: false,
|
|
199
|
+
lastEndTs: 0,
|
|
200
|
+
};
|
|
201
|
+
|
|
193
202
|
this._onPointerDown = null;
|
|
194
203
|
this._onPointerUp = null;
|
|
195
204
|
this._onPointerMove = null;
|
|
@@ -701,13 +710,25 @@ export class Viewer {
|
|
|
701
710
|
const dir = this.camera.position.clone().sub(this.controls.target).normalize();
|
|
702
711
|
this._prevViewDir = dir;
|
|
703
712
|
this._smoothedAxis = null;
|
|
713
|
+
if (this._damping.dynamic) {
|
|
714
|
+
this.controls.dampingFactor = this._damping.base;
|
|
715
|
+
this._damping.isSettling = false;
|
|
716
|
+
this.controls.enableDamping = true;
|
|
717
|
+
}
|
|
704
718
|
};
|
|
705
719
|
this._onControlsChange = () => {
|
|
706
720
|
// Обновляем ось только при зажатой ЛКМ (вращение)
|
|
707
721
|
if (!this._isLmbDown) return;
|
|
708
722
|
this.#updateRotationAxisLine();
|
|
709
723
|
};
|
|
710
|
-
this._onControlsEnd = () => {
|
|
724
|
+
this._onControlsEnd = () => {
|
|
725
|
+
this.#hideRotationAxisLine();
|
|
726
|
+
if (this._damping.dynamic && this.controls) {
|
|
727
|
+
this._damping.isSettling = true;
|
|
728
|
+
this._damping.lastEndTs = performance.now();
|
|
729
|
+
this.controls.enableDamping = true;
|
|
730
|
+
}
|
|
731
|
+
};
|
|
711
732
|
this.controls.addEventListener('start', this._onControlsStart);
|
|
712
733
|
this.controls.addEventListener('change', this._onControlsChange);
|
|
713
734
|
this.controls.addEventListener('end', this._onControlsEnd);
|
|
@@ -768,7 +789,10 @@ export class Viewer {
|
|
|
768
789
|
this.demoCube.rotation.x += 0.005;
|
|
769
790
|
}
|
|
770
791
|
|
|
771
|
-
if (this.controls)
|
|
792
|
+
if (this.controls) {
|
|
793
|
+
this.#updateDynamicDamping();
|
|
794
|
+
this.controls.update();
|
|
795
|
+
}
|
|
772
796
|
// "Внутренняя" подсветка/пост-эффекты: включаются только когда камера внутри модели
|
|
773
797
|
this.#updateInteriorAssist();
|
|
774
798
|
this._notifyZoomIfChanged();
|
|
@@ -814,6 +838,22 @@ export class Viewer {
|
|
|
814
838
|
this.animationId = requestAnimationFrame(this.animate);
|
|
815
839
|
}
|
|
816
840
|
|
|
841
|
+
#updateDynamicDamping() {
|
|
842
|
+
if (!this.controls) return;
|
|
843
|
+
if (!this._damping.dynamic || !this.controls.enableDamping) return;
|
|
844
|
+
if (this._damping.isSettling) {
|
|
845
|
+
const now = performance.now();
|
|
846
|
+
if (now - this._damping.lastEndTs <= this._damping.settleMs) {
|
|
847
|
+
this.controls.dampingFactor = this._damping.settle;
|
|
848
|
+
} else {
|
|
849
|
+
this._damping.isSettling = false;
|
|
850
|
+
this.controls.dampingFactor = this._damping.base;
|
|
851
|
+
}
|
|
852
|
+
} else {
|
|
853
|
+
this.controls.dampingFactor = this._damping.base;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
817
857
|
dispose() {
|
|
818
858
|
if (this.animationId) cancelAnimationFrame(this.animationId);
|
|
819
859
|
window.removeEventListener("resize", this.handleResize);
|