@sequent-org/ifc-viewer 1.2.4-ci.54.0 → 1.2.4-ci.55.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.54.0",
4
+ "version": "1.2.4-ci.55.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",
@@ -59,6 +59,10 @@ export class LabelPlacementController {
59
59
  open: false,
60
60
  marker: null,
61
61
  };
62
+ this._canvasMenu = {
63
+ open: false,
64
+ hit: null,
65
+ };
62
66
 
63
67
  this._fly = {
64
68
  raf: 0,
@@ -99,6 +103,11 @@ export class LabelPlacementController {
99
103
  try { this._ui?.btn?.removeEventListener("click", this._onBtnClick); } catch (_) {}
100
104
  try { this._ui?.menu?.removeEventListener("pointerdown", this._onMenuPointerDown); } catch (_) {}
101
105
  try { this._ui?.menu?.removeEventListener("click", this._onMenuClick); } catch (_) {}
106
+ try { this._ui?.canvasMenu?.removeEventListener("pointerdown", this._onCanvasMenuPointerDown); } catch (_) {}
107
+ try { this._ui?.canvasMenu?.removeEventListener("click", this._onCanvasMenuClick); } catch (_) {}
108
+ try { dom?.removeEventListener("contextmenu", this._onCanvasContextMenu, { capture: true }); } catch (_) {
109
+ try { dom?.removeEventListener("contextmenu", this._onCanvasContextMenu); } catch (_) {}
110
+ }
102
111
 
103
112
  if (this._raf) cancelAnimationFrame(this._raf);
104
113
  this._raf = 0;
@@ -109,6 +118,7 @@ export class LabelPlacementController {
109
118
  try { this._ui?.ghost?.remove?.(); } catch (_) {}
110
119
  try { this._ui?.btn?.remove?.(); } catch (_) {}
111
120
  try { this._ui?.menu?.remove?.(); } catch (_) {}
121
+ try { this._ui?.canvasMenu?.remove?.(); } catch (_) {}
112
122
  }
113
123
 
114
124
  startPlacement() {
@@ -560,7 +570,20 @@ export class LabelPlacementController {
560
570
  menu.appendChild(menuMove);
561
571
  menu.appendChild(menuDelete);
562
572
 
563
- return { btn, ghost, dot, num, menu };
573
+ const canvasMenu = document.createElement("div");
574
+ canvasMenu.className = "ifc-label-menu";
575
+ canvasMenu.style.display = "none";
576
+ canvasMenu.setAttribute("role", "menu");
577
+
578
+ const menuAdd = document.createElement("button");
579
+ menuAdd.type = "button";
580
+ menuAdd.className = "ifc-label-menu-item";
581
+ menuAdd.textContent = "Добавить метку";
582
+ menuAdd.setAttribute("data-action", "add");
583
+
584
+ canvasMenu.appendChild(menuAdd);
585
+
586
+ return { btn, ghost, dot, num, menu, canvasMenu };
564
587
  }
565
588
 
566
589
  #attachUi() {
@@ -568,6 +591,7 @@ export class LabelPlacementController {
568
591
  this.container.appendChild(this._ui.btn);
569
592
  this.container.appendChild(this._ui.ghost);
570
593
  this.container.appendChild(this._ui.menu);
594
+ this.container.appendChild(this._ui.canvasMenu);
571
595
  }
572
596
 
573
597
  #bindEvents() {
@@ -600,6 +624,27 @@ export class LabelPlacementController {
600
624
  };
601
625
  this._ui.menu.addEventListener("click", this._onMenuClick);
602
626
 
627
+ this._onCanvasMenuPointerDown = (e) => {
628
+ // Не даём клику меню попасть в canvas/OrbitControls
629
+ try { e.preventDefault(); } catch (_) {}
630
+ try { e.stopPropagation(); } catch (_) {}
631
+ try { e.stopImmediatePropagation?.(); } catch (_) {}
632
+ };
633
+ this._ui.canvasMenu.addEventListener("pointerdown", this._onCanvasMenuPointerDown, { passive: false });
634
+
635
+ this._onCanvasMenuClick = (e) => {
636
+ const target = e.target;
637
+ const action = target?.getAttribute?.("data-action");
638
+ if (action !== "add") return;
639
+ try { e.preventDefault(); } catch (_) {}
640
+ try { e.stopPropagation(); } catch (_) {}
641
+
642
+ const hit = this._canvasMenu?.hit || null;
643
+ if (hit) this.#createMarkerAtHit(hit);
644
+ this.#closeCanvasMenu();
645
+ };
646
+ this._ui.canvasMenu.addEventListener("click", this._onCanvasMenuClick);
647
+
603
648
  this._onKeyDown = (e) => {
604
649
  if (this._placing) {
605
650
  if (e.key === "Escape") this.cancelPlacement();
@@ -634,10 +679,13 @@ export class LabelPlacementController {
634
679
  window.addEventListener("keydown", this._onKeyDown);
635
680
 
636
681
  this._onWindowPointerDown = (e) => {
637
- if (!this._contextMenu.open) return;
682
+ if (!this._contextMenu.open && !this._canvasMenu.open) return;
638
683
  const menu = this._ui?.menu;
684
+ const canvasMenu = this._ui?.canvasMenu;
639
685
  if (menu && menu.contains(e.target)) return;
686
+ if (canvasMenu && canvasMenu.contains(e.target)) return;
640
687
  this.#closeContextMenu();
688
+ this.#closeCanvasMenu();
641
689
  };
642
690
  window.addEventListener("pointerdown", this._onWindowPointerDown, true);
643
691
 
@@ -692,6 +740,25 @@ export class LabelPlacementController {
692
740
  this.cancelPlacement(); // по ТЗ: “вторым кликом устанавливаем”
693
741
  };
694
742
  dom.addEventListener("pointerdown", this._onPointerDownCapture, { capture: true, passive: false });
743
+
744
+ this._onCanvasContextMenu = (e) => {
745
+ // Контекстное меню добавления метки по ПКМ на модели (если нет метки под курсором).
746
+ try { e.preventDefault(); } catch (_) {}
747
+ try { e.stopPropagation(); } catch (_) {}
748
+ try { e.stopImmediatePropagation?.(); } catch (_) {}
749
+ try { this.cancelPlacement(); } catch (_) {}
750
+
751
+ const el = document.elementFromPoint?.(e.clientX, e.clientY);
752
+ if (el && el.closest?.(".ifc-label-marker")) return;
753
+ if (el && el.closest?.(".ifc-label-menu")) return;
754
+
755
+ const hit = this.#pickModelPoint(e.clientX, e.clientY);
756
+ if (!hit) return;
757
+
758
+ this.#closeContextMenu();
759
+ this.#openCanvasMenu(hit, e.clientX, e.clientY);
760
+ };
761
+ dom.addEventListener("contextmenu", this._onCanvasContextMenu, { capture: true, passive: false });
695
762
  }
696
763
 
697
764
  #setGhostVisible(visible) {
@@ -892,6 +959,46 @@ export class LabelPlacementController {
892
959
  menu.style.display = "none";
893
960
  }
894
961
 
962
+ #openCanvasMenu(hit, clientX, clientY) {
963
+ const menu = this._ui?.canvasMenu;
964
+ if (!menu || !hit) return;
965
+
966
+ this.#closeCanvasMenu();
967
+
968
+ this._canvasMenu.open = true;
969
+ this._canvasMenu.hit = hit;
970
+
971
+ if (!this._containerOffsetValid) this.#refreshContainerOffset();
972
+ const x = clientX - this._containerOffset.left;
973
+ const y = clientY - this._containerOffset.top;
974
+
975
+ menu.style.display = "block";
976
+ menu.style.left = "0px";
977
+ menu.style.top = "0px";
978
+ menu.style.transform = `translate3d(${x}px, ${y}px, 0) translate(8px, 8px)`;
979
+
980
+ try {
981
+ const rect = this.container?.getBoundingClientRect?.();
982
+ const mw = menu.offsetWidth || 0;
983
+ const mh = menu.offsetHeight || 0;
984
+ if (rect && mw && mh) {
985
+ let px = x + 8;
986
+ let py = y + 8;
987
+ if (px + mw > rect.width) px = Math.max(0, rect.width - mw - 4);
988
+ if (py + mh > rect.height) py = Math.max(0, rect.height - mh - 4);
989
+ menu.style.transform = `translate3d(${px}px, ${py}px, 0)`;
990
+ }
991
+ } catch (_) {}
992
+ }
993
+
994
+ #closeCanvasMenu() {
995
+ const menu = this._ui?.canvasMenu;
996
+ if (!menu) return;
997
+ this._canvasMenu.open = false;
998
+ this._canvasMenu.hit = null;
999
+ menu.style.display = "none";
1000
+ }
1001
+
895
1002
  #createMarkerFromData(data, emitPlacedEvent) {
896
1003
  if (!data) return null;
897
1004
  const localPoint = data.localPoint || {};
@@ -213,7 +213,7 @@ export class Viewer {
213
213
 
214
214
  // RMB: перемещение модели относительно "оси" (pivot), pivot остаётся на месте
215
215
  this._rmbModelMove = {
216
- enabled: true,
216
+ enabled: false,
217
217
  debug: false,
218
218
  controller: null,
219
219
  pivotAnchor: null, // THREE.Vector3|null (фиксированная ось после ПКМ)