@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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sequent-org/ifc-viewer",
3
3
  "private": false,
4
- "version": "1.2.4-ci.60.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
 
@@ -1473,12 +1513,21 @@ export class LabelPlacementController {
1473
1513
  this._tmpV.copy(m.localPoint);
1474
1514
  model.localToWorld(this._tmpV);
1475
1515
 
1476
- const ndc = this._tmpV.project(camera);
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 inFront = Number.isFinite(ndc.z) && ndc.z >= -1 && ndc.z <= 1;
1480
- if (!inFront) {
1481
- 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");
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.el.style.display = "none";
1538
+ this.#setMarkerVisibility(m, false, "invalidScreen");
1490
1539
  continue;
1491
1540
  }
1492
1541
 
1493
- m.el.style.display = "block";
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;
@@ -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);