@sequent-org/ifc-viewer 1.2.4-ci.58.0 → 1.2.4-ci.60.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.58.0",
4
+ "version": "1.2.4-ci.60.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",
@@ -111,7 +111,18 @@
111
111
  .pointer-events-none { pointer-events: none; }
112
112
 
113
113
  /* === IFC LABEL MARKERS (UI overlay) === */
114
+ .ifc-label-actions {
115
+ position: absolute;
116
+ left: 12px;
117
+ top: 12px;
118
+ z-index: 9999;
119
+ display: flex;
120
+ gap: 6px;
121
+ pointer-events: auto;
122
+ }
123
+
114
124
  .ifc-label-add-btn,
125
+ .ifc-label-hide-btn,
115
126
  .ifc-card-add-btn {
116
127
  position: absolute;
117
128
  left: 12px;
@@ -132,10 +143,35 @@
132
143
  }
133
144
 
134
145
  .ifc-label-add-btn:hover,
146
+ .ifc-label-hide-btn:hover,
135
147
  .ifc-card-add-btn:hover {
136
148
  background: rgba(255, 255, 255, 0.98);
137
149
  }
138
150
 
151
+ .ifc-label-actions .ifc-label-add-btn,
152
+ .ifc-label-actions .ifc-label-hide-btn {
153
+ position: static;
154
+ left: auto;
155
+ top: auto;
156
+ }
157
+
158
+ .ifc-label-hide-btn--active {
159
+ background: rgba(37, 99, 235, 0.95);
160
+ border-color: rgba(37, 99, 235, 0.95);
161
+ color: #ffffff;
162
+ }
163
+
164
+ .ifc-label-hide-btn--active:hover {
165
+ background: rgba(29, 78, 216, 0.98);
166
+ border-color: rgba(29, 78, 216, 0.98);
167
+ }
168
+
169
+ .ifc-label-add-btn:disabled,
170
+ .ifc-label-add-btn--disabled {
171
+ opacity: 0.5;
172
+ cursor: not-allowed;
173
+ }
174
+
139
175
  .ifc-label-ghost,
140
176
  .ifc-label-marker,
141
177
  .ifc-card-ghost,
@@ -232,6 +268,13 @@
232
268
  background: rgba(37, 99, 235, 0.12);
233
269
  }
234
270
 
271
+ .ifc-label-menu-item--disabled,
272
+ .ifc-label-menu-item--disabled:hover {
273
+ opacity: 0.5;
274
+ cursor: not-allowed;
275
+ background: transparent;
276
+ }
277
+
235
278
  /* === NAVBAR COMPONENT === */
236
279
  .navbar {
237
280
  display: flex;
@@ -40,6 +40,7 @@ export class LabelPlacementController {
40
40
  /** @type {LabelMarker[]} */
41
41
  this._markers = [];
42
42
  this._selectedId = null;
43
+ this._labelsHidden = false;
43
44
 
44
45
  this._raycaster = new THREE.Raycaster();
45
46
  this._ndc = new THREE.Vector2();
@@ -53,6 +54,8 @@ export class LabelPlacementController {
53
54
  this._labelDrag = {
54
55
  active: false,
55
56
  moved: false,
57
+ // true => призрак живёт в document.body и позиционируется по viewport
58
+ ghostInBody: false,
56
59
  pointerId: null,
57
60
  id: null,
58
61
  start: { x: 0, y: 0 },
@@ -129,6 +132,7 @@ export class LabelPlacementController {
129
132
  try { document.removeEventListener("pointerup", this._onLabelDragPointerUp); } catch (_) {}
130
133
  try { document.removeEventListener("pointercancel", this._onLabelDragPointerCancel); } catch (_) {}
131
134
  try { this._ui?.btn?.removeEventListener("click", this._onBtnClick); } catch (_) {}
135
+ try { this._ui?.hideBtn?.removeEventListener("click", this._onHideBtnClick); } catch (_) {}
132
136
  try { this._ui?.menu?.removeEventListener("pointerdown", this._onMenuPointerDown); } catch (_) {}
133
137
  try { this._ui?.menu?.removeEventListener("click", this._onMenuClick); } catch (_) {}
134
138
  try { this._ui?.canvasMenu?.removeEventListener("pointerdown", this._onCanvasMenuPointerDown); } catch (_) {}
@@ -145,13 +149,13 @@ export class LabelPlacementController {
145
149
 
146
150
  try { this._ui?.ghost?.remove?.(); } catch (_) {}
147
151
  try { this._ui?.dragGhost?.remove?.(); } catch (_) {}
148
- try { this._ui?.btn?.remove?.(); } catch (_) {}
152
+ try { this._ui?.actions?.remove?.(); } catch (_) {}
149
153
  try { this._ui?.menu?.remove?.(); } catch (_) {}
150
154
  try { this._ui?.canvasMenu?.remove?.(); } catch (_) {}
151
155
  }
152
156
 
153
157
  startPlacement() {
154
- if (this._placing) return;
158
+ if (this._placing || this._labelsHidden) return;
155
159
  this._placing = true;
156
160
 
157
161
  const controls = this.viewer?.controls;
@@ -591,11 +595,24 @@ export class LabelPlacementController {
591
595
  }
592
596
 
593
597
  #createUi() {
598
+ const actions = document.createElement("div");
599
+ actions.className = "ifc-label-actions";
600
+
594
601
  const btn = document.createElement("button");
595
602
  btn.type = "button";
596
603
  btn.className = "ifc-label-add-btn";
597
604
  btn.textContent = "+ Добавить метку";
598
605
 
606
+ const hideBtn = document.createElement("button");
607
+ hideBtn.type = "button";
608
+ hideBtn.className = "ifc-label-hide-btn";
609
+ hideBtn.textContent = "Скрыть метки";
610
+ hideBtn.setAttribute("aria-pressed", "false");
611
+ hideBtn.style.display = "none";
612
+
613
+ actions.appendChild(btn);
614
+ actions.appendChild(hideBtn);
615
+
599
616
  const ghost = document.createElement("div");
600
617
  ghost.className = "ifc-label-ghost";
601
618
  ghost.setAttribute("aria-hidden", "true");
@@ -615,6 +632,8 @@ export class LabelPlacementController {
615
632
  const dragGhost = document.createElement("div");
616
633
  dragGhost.className = "ifc-label-ghost ifc-label-ghost--drag";
617
634
  dragGhost.setAttribute("aria-hidden", "true");
635
+ // Фиксируем позицию, чтобы призрак не обрезался контейнером viewer
636
+ dragGhost.style.position = "fixed";
618
637
  dragGhost.style.display = "none";
619
638
  dragGhost.style.left = "0px";
620
639
  dragGhost.style.top = "0px";
@@ -666,14 +685,21 @@ export class LabelPlacementController {
666
685
 
667
686
  canvasMenu.appendChild(menuAdd);
668
687
 
669
- return { btn, ghost, dot, num, dragGhost, dragNum, menu, canvasMenu };
688
+ return { actions, btn, hideBtn, ghost, dot, num, dragGhost, dragNum, menu, canvasMenu, menuAdd };
670
689
  }
671
690
 
672
691
  #attachUi() {
673
692
  // Важно: container должен быть position:relative (в index.html уже так).
674
- this.container.appendChild(this._ui.btn);
693
+ this.container.appendChild(this._ui.actions);
675
694
  this.container.appendChild(this._ui.ghost);
676
- this.container.appendChild(this._ui.dragGhost);
695
+ // Призрак перетаскивания добавляем в body, чтобы не обрезался overflow контейнера
696
+ if (document?.body) {
697
+ document.body.appendChild(this._ui.dragGhost);
698
+ this._labelDrag.ghostInBody = true;
699
+ } else {
700
+ this.container.appendChild(this._ui.dragGhost);
701
+ this._labelDrag.ghostInBody = false;
702
+ }
677
703
  this.container.appendChild(this._ui.menu);
678
704
  this.container.appendChild(this._ui.canvasMenu);
679
705
  }
@@ -688,6 +714,15 @@ export class LabelPlacementController {
688
714
  };
689
715
  this._ui.btn.addEventListener("click", this._onBtnClick, { passive: false });
690
716
 
717
+ this._onHideBtnClick = (e) => {
718
+ try { e.preventDefault(); } catch (_) {}
719
+ try { e.stopPropagation(); } catch (_) {}
720
+ try { e.stopImmediatePropagation?.(); } catch (_) {}
721
+ if (!this._markers.length) return;
722
+ this.#setLabelsHidden(!this._labelsHidden);
723
+ };
724
+ this._ui.hideBtn.addEventListener("click", this._onHideBtnClick, { passive: false });
725
+
691
726
  this._onMenuPointerDown = (e) => {
692
727
  // Не даём клику меню попасть в canvas/OrbitControls
693
728
  try { e.preventDefault(); } catch (_) {}
@@ -720,6 +755,7 @@ export class LabelPlacementController {
720
755
  const target = e.target;
721
756
  const action = target?.getAttribute?.("data-action");
722
757
  if (action !== "add") return;
758
+ if (this._labelsHidden) return;
723
759
  try { e.preventDefault(); } catch (_) {}
724
760
  try { e.stopPropagation(); } catch (_) {}
725
761
 
@@ -942,9 +978,13 @@ export class LabelPlacementController {
942
978
 
943
979
  #updateDragGhostFromClient(clientX, clientY) {
944
980
  this._labelDrag.last = { x: clientX, y: clientY };
945
- if (!this._containerOffsetValid) this.#refreshContainerOffset();
946
- const x = (clientX - this._containerOffset.left);
947
- const y = (clientY - this._containerOffset.top);
981
+ let x = clientX;
982
+ let y = clientY;
983
+ if (!this._labelDrag.ghostInBody) {
984
+ if (!this._containerOffsetValid) this.#refreshContainerOffset();
985
+ x = (clientX - this._containerOffset.left);
986
+ y = (clientY - this._containerOffset.top);
987
+ }
948
988
  this._labelDrag.ghostPos.x = x;
949
989
  this._labelDrag.ghostPos.y = y;
950
990
  this.#applyDragGhostTransform();
@@ -1008,6 +1048,11 @@ export class LabelPlacementController {
1008
1048
  const moved = !!this._labelDrag.moved;
1009
1049
  const clientX = e?.clientX ?? this._labelDrag.last.x;
1010
1050
  const clientY = e?.clientY ?? this._labelDrag.last.y;
1051
+ this.logger?.log?.("[LabelClickDbg]", {
1052
+ phase: "finish",
1053
+ moved: !!moved,
1054
+ id: marker?.id ?? this._labelDrag.id,
1055
+ });
1011
1056
 
1012
1057
  this._labelDrag.active = false;
1013
1058
  this._labelDrag.moved = false;
@@ -1261,8 +1306,17 @@ export class LabelPlacementController {
1261
1306
  sceneState: data.sceneState || null,
1262
1307
  });
1263
1308
  this._markers.push(marker);
1309
+ this.#syncHideButton();
1264
1310
 
1265
1311
  const onMarkerPointerDown = (e) => {
1312
+ this.logger?.log?.("[LabelClickDbg]", {
1313
+ phase: "down",
1314
+ button: e?.button,
1315
+ buttons: e?.buttons,
1316
+ clientX: e?.clientX,
1317
+ clientY: e?.clientY,
1318
+ pointerId: e?.pointerId,
1319
+ });
1266
1320
  // Важно: не даём клику попасть в canvas/OrbitControls
1267
1321
  try { e.preventDefault(); } catch (_) {}
1268
1322
  try { e.stopPropagation(); } catch (_) {}
@@ -1281,6 +1335,14 @@ export class LabelPlacementController {
1281
1335
  try { marker.el.addEventListener("pointerdown", onMarkerPointerDown); } catch (_) {}
1282
1336
  }
1283
1337
  const onMarkerPointerUp = (e) => {
1338
+ this.logger?.log?.("[LabelClickDbg]", {
1339
+ phase: "up",
1340
+ button: e?.button,
1341
+ buttons: e?.buttons,
1342
+ clientX: e?.clientX,
1343
+ clientY: e?.clientY,
1344
+ pointerId: e?.pointerId,
1345
+ });
1284
1346
  };
1285
1347
  try { marker.el.addEventListener("pointerup", onMarkerPointerUp, { capture: true, passive: true }); } catch (_) {
1286
1348
  try { marker.el.addEventListener("pointerup", onMarkerPointerUp); } catch (_) {}
@@ -1336,6 +1398,7 @@ export class LabelPlacementController {
1336
1398
  }
1337
1399
  if (maxNumericId != null) this._nextId = Math.max(1, Math.floor(maxNumericId) + 1);
1338
1400
  if (prevSelectedId != null) this.selectLabel(prevSelectedId);
1401
+ this.#syncHideButton();
1339
1402
  }
1340
1403
 
1341
1404
  getLabelMarkers() {
@@ -1396,6 +1459,11 @@ export class LabelPlacementController {
1396
1459
  for (const m of this._markers) {
1397
1460
  if (!m || !m.el) continue;
1398
1461
 
1462
+ if (this._labelsHidden) {
1463
+ m.el.style.display = "none";
1464
+ continue;
1465
+ }
1466
+
1399
1467
  if (!model) {
1400
1468
  m.el.style.display = "none";
1401
1469
  continue;
@@ -1426,5 +1494,44 @@ export class LabelPlacementController {
1426
1494
  m.el.style.transform = `translate3d(${x}px, ${y}px, 0) translate(-50%, -50%)`;
1427
1495
  }
1428
1496
  }
1497
+
1498
+ #setLabelsHidden(hidden) {
1499
+ const next = !!hidden;
1500
+ if (this._labelsHidden === next) return;
1501
+ this._labelsHidden = next;
1502
+ if (this._labelsHidden) {
1503
+ try { this.cancelPlacement(); } catch (_) {}
1504
+ try { this.#closeContextMenu(); } catch (_) {}
1505
+ try { this.#closeCanvasMenu(); } catch (_) {}
1506
+ }
1507
+ this.#syncHideButton();
1508
+ }
1509
+
1510
+ #syncHideButton() {
1511
+ const btn = this._ui?.hideBtn;
1512
+ if (!btn) return;
1513
+ const hasMarkers = this._markers.length > 0;
1514
+ btn.style.display = hasMarkers ? "block" : "none";
1515
+ if (!hasMarkers && this._labelsHidden) {
1516
+ this._labelsHidden = false;
1517
+ }
1518
+ btn.setAttribute("aria-pressed", this._labelsHidden ? "true" : "false");
1519
+ try { btn.classList.toggle("ifc-label-hide-btn--active", this._labelsHidden); } catch (_) {}
1520
+ this.#syncAddAvailability();
1521
+ }
1522
+
1523
+ #syncAddAvailability() {
1524
+ const disabled = !!this._labelsHidden;
1525
+ const btn = this._ui?.btn;
1526
+ if (btn) {
1527
+ btn.disabled = disabled;
1528
+ try { btn.classList.toggle("ifc-label-add-btn--disabled", disabled); } catch (_) {}
1529
+ }
1530
+ const menuAdd = this._ui?.menuAdd;
1531
+ if (menuAdd) {
1532
+ menuAdd.disabled = disabled;
1533
+ try { menuAdd.classList.toggle("ifc-label-menu-item--disabled", disabled); } catch (_) {}
1534
+ }
1535
+ }
1429
1536
  }
1430
1537