@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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sequent-org/ifc-viewer",
3
3
  "private": false,
4
- "version": "1.2.4-ci.61.0",
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",
@@ -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.el.style.display = "none";
1503
+ this.#setMarkerVisibility(m, false, "labelsHidden");
1464
1504
  continue;
1465
1505
  }
1466
1506
 
1467
1507
  if (!model) {
1468
- m.el.style.display = "none";
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.el.style.display = "none";
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 inFront = Number.isFinite(ndc.z) && ndc.z >= -1 && ndc.z <= 1;
1485
- if (!inFront) {
1486
- m.el.style.display = "none";
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.el.style.display = "none";
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.el.style.display = "block";
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;
@@ -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 = () => { this.#hideRotationAxisLine(); };
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) this.controls.update();
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);