@sequent-org/ifc-viewer 1.2.4-ci.60.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 +187 -9
- 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
|
|
|
@@ -1473,12 +1513,21 @@ export class LabelPlacementController {
|
|
|
1473
1513
|
this._tmpV.copy(m.localPoint);
|
|
1474
1514
|
model.localToWorld(this._tmpV);
|
|
1475
1515
|
|
|
1476
|
-
|
|
1516
|
+
if (this.#isPointClippedBySection(this._tmpV)) {
|
|
1517
|
+
this.#setMarkerVisibility(m, false, "clipped");
|
|
1518
|
+
continue;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
const ndc = this._tmpNdc.copy(this._tmpV).project(camera);
|
|
1477
1522
|
|
|
1478
|
-
// Если точка за камерой или
|
|
1479
|
-
const
|
|
1480
|
-
|
|
1481
|
-
|
|
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");
|
|
1482
1531
|
continue;
|
|
1483
1532
|
}
|
|
1484
1533
|
|
|
@@ -1486,15 +1535,78 @@ export class LabelPlacementController {
|
|
|
1486
1535
|
const y = (-ndc.y * 0.5 + 0.5) * h;
|
|
1487
1536
|
|
|
1488
1537
|
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
1489
|
-
m
|
|
1538
|
+
this.#setMarkerVisibility(m, false, "invalidScreen");
|
|
1490
1539
|
continue;
|
|
1491
1540
|
}
|
|
1492
1541
|
|
|
1493
|
-
|
|
1542
|
+
const occluded = this.#isPointOccludedByModel(this._tmpV, ndc, model, camera);
|
|
1543
|
+
if (occluded) {
|
|
1544
|
+
this.#setMarkerVisibility(m, false, "occluded");
|
|
1545
|
+
continue;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
this.#setMarkerVisibility(m, true, "visible");
|
|
1494
1549
|
m.el.style.transform = `translate3d(${x}px, ${y}px, 0) translate(-50%, -50%)`;
|
|
1495
1550
|
}
|
|
1496
1551
|
}
|
|
1497
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
|
+
|
|
1594
|
+
#isPointClippedBySection(pointWorld) {
|
|
1595
|
+
const planes = this.viewer?.clipping?.planes || [];
|
|
1596
|
+
for (const plane of planes) {
|
|
1597
|
+
if (!plane || !Number.isFinite(plane.constant)) continue;
|
|
1598
|
+
const signed = plane.distanceToPoint(pointWorld);
|
|
1599
|
+
if (signed < -1e-4) return true;
|
|
1600
|
+
}
|
|
1601
|
+
return false;
|
|
1602
|
+
}
|
|
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
|
+
|
|
1498
1610
|
#setLabelsHidden(hidden) {
|
|
1499
1611
|
const next = !!hidden;
|
|
1500
1612
|
if (this._labelsHidden === next) return;
|
|
@@ -1507,6 +1619,72 @@ export class LabelPlacementController {
|
|
|
1507
1619
|
this.#syncHideButton();
|
|
1508
1620
|
}
|
|
1509
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
|
+
|
|
1510
1688
|
#syncHideButton() {
|
|
1511
1689
|
const btn = this._ui?.hideBtn;
|
|
1512
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);
|