@sequent-org/ifc-viewer 1.2.4-ci.62.0 → 1.2.4-ci.63.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.62.0",
4
+ "version": "1.2.4-ci.63.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/IfcViewer.js CHANGED
@@ -41,6 +41,7 @@ export class IfcViewer {
41
41
  * @param {boolean} [options.showSidebar=false] - Показывать ли боковую панель с деревом
42
42
  * @param {boolean} [options.showControls=false] - Показывать ли панель управления (нижние кнопки)
43
43
  * @param {boolean} [options.showToolbar=true] - Показывать ли верхнюю панель инструментов
44
+ * @param {boolean} [options.labelEditingEnabled=true] - Разрешить ли редактирование меток
44
45
  * @param {boolean} [options.autoLoad=true] - Автоматически загружать модель при инициализации (modelUrl/modelFile/ifcUrl/ifcFile)
45
46
  * @param {string} [options.theme='light'] - Тема интерфейса ('light' | 'dark')
46
47
  * @param {Object} [options.viewerOptions] - Дополнительные опции для Viewer
@@ -74,6 +75,7 @@ export class IfcViewer {
74
75
  showSidebar: options.showSidebar === true, // по умолчанию false
75
76
  showControls: options.showControls === true, // по умолчанию false
76
77
  showToolbar: options.showToolbar !== false, // по умолчанию true
78
+ labelEditingEnabled: options.labelEditingEnabled !== false,
77
79
  autoLoad: options.autoLoad !== false,
78
80
  theme: options.theme || 'light',
79
81
  viewerOptions: options.viewerOptions || {}
@@ -389,6 +391,24 @@ export class IfcViewer {
389
391
  this.labelPlacement.selectLabel(id);
390
392
  }
391
393
 
394
+ /**
395
+ * Включает/выключает режим редактирования меток.
396
+ * @param {boolean} enabled
397
+ */
398
+ setLabelEditingEnabled(enabled) {
399
+ if (!this.labelPlacement) return;
400
+ this.labelPlacement.setEditingEnabled(enabled);
401
+ }
402
+
403
+ /**
404
+ * Возвращает текущий режим редактирования меток.
405
+ * @returns {boolean}
406
+ */
407
+ getLabelEditingEnabled() {
408
+ if (!this.labelPlacement) return false;
409
+ return this.labelPlacement.getEditingEnabled();
410
+ }
411
+
392
412
  /**
393
413
  * @deprecated используйте setLabelMarkers
394
414
  */
@@ -564,7 +584,12 @@ export class IfcViewer {
564
584
  // В пакете включаем UI "меток" по умолчанию:
565
585
  // кнопка "+ Добавить метку" + режим постановки меток + сохранение/восстановление состояния.
566
586
  try {
567
- this.labelPlacement = new LabelPlacementController({ viewer: this.viewer, container: this.elements.viewerContainer, logger: console });
587
+ this.labelPlacement = new LabelPlacementController({
588
+ viewer: this.viewer,
589
+ container: this.elements.viewerContainer,
590
+ logger: console,
591
+ editingEnabled: this.options.labelEditingEnabled,
592
+ });
568
593
  this.cardPlacement = this.labelPlacement;
569
594
  } catch (e) {
570
595
  console.warn('IfcViewer: LabelPlacementController init failed', e);
package/src/main.js CHANGED
@@ -54,6 +54,16 @@ if (app) {
54
54
  // Остальные контролы (тени/солнце/материалы/визуал/цветокор) будем добавлять пошагово позже.
55
55
  // (Старый код управления панелью закомментирован ниже для последующего восстановления при необходимости.)
56
56
 
57
+ // Анимация (damping)
58
+ const dampingDynamic = document.getElementById("dampingDynamic");
59
+ const dampingDynamicValue = document.getElementById("dampingDynamicValue");
60
+ const dampingBase = document.getElementById("dampingBase");
61
+ const dampingBaseValue = document.getElementById("dampingBaseValue");
62
+ const dampingSettle = document.getElementById("dampingSettle");
63
+ const dampingSettleValue = document.getElementById("dampingSettleValue");
64
+ const dampingSettleMs = document.getElementById("dampingSettleMs");
65
+ const dampingSettleMsValue = document.getElementById("dampingSettleMsValue");
66
+
57
67
  const testPresetToggle = document.getElementById("testPresetToggle");
58
68
  // Шаг 1 (Tone mapping): в текущей версии он входит в пресет "Тест", но exposure можно подстроить.
59
69
  const step1ToneToggle = document.getElementById("step1ToneToggle");
@@ -78,6 +88,59 @@ if (app) {
78
88
  const step4SaturationValue = document.getElementById("step4SaturationValue");
79
89
  const step4Dump = document.getElementById("step4Dump");
80
90
 
91
+ const applyDampingDynamic = (v) => {
92
+ const on = Number(v) >= 1;
93
+ if (dampingDynamicValue) dampingDynamicValue.textContent = on ? "1" : "0";
94
+ try { viewer.setDampingConfig?.({ dynamic: on }); } catch (_) {}
95
+ };
96
+ const applyDampingBase = (v) => {
97
+ const value = Number(v);
98
+ if (!Number.isFinite(value)) return;
99
+ if (dampingBaseValue) dampingBaseValue.textContent = value.toFixed(2);
100
+ try { viewer.setDampingConfig?.({ base: value }); } catch (_) {}
101
+ };
102
+ const applyDampingSettle = (v) => {
103
+ const value = Number(v);
104
+ if (!Number.isFinite(value)) return;
105
+ if (dampingSettleValue) dampingSettleValue.textContent = value.toFixed(2);
106
+ try { viewer.setDampingConfig?.({ settle: value }); } catch (_) {}
107
+ };
108
+ const applyDampingSettleMs = (v) => {
109
+ const value = Math.max(0, Math.round(Number(v)));
110
+ if (!Number.isFinite(value)) return;
111
+ if (dampingSettleMsValue) dampingSettleMsValue.textContent = String(value);
112
+ try { viewer.setDampingConfig?.({ settleMs: value }); } catch (_) {}
113
+ };
114
+
115
+ // Инициализация damping UI от текущих параметров viewer
116
+ const initDampingUi = () => {
117
+ const cfg = viewer.getDampingConfig?.() || {};
118
+ if (dampingDynamic) {
119
+ dampingDynamic.value = cfg.dynamic ? "1" : "0";
120
+ applyDampingDynamic(dampingDynamic.value);
121
+ dampingDynamic.addEventListener("input", (e) => applyDampingDynamic(e.target.value));
122
+ }
123
+ if (dampingBase) {
124
+ const v = Number.isFinite(cfg.base) ? cfg.base : Number(dampingBase.value);
125
+ dampingBase.value = Number.isFinite(v) ? String(v) : "0.50";
126
+ applyDampingBase(dampingBase.value);
127
+ dampingBase.addEventListener("input", (e) => applyDampingBase(e.target.value));
128
+ }
129
+ if (dampingSettle) {
130
+ const v = Number.isFinite(cfg.settle) ? cfg.settle : Number(dampingSettle.value);
131
+ dampingSettle.value = Number.isFinite(v) ? String(v) : "0.00";
132
+ applyDampingSettle(dampingSettle.value);
133
+ dampingSettle.addEventListener("input", (e) => applyDampingSettle(e.target.value));
134
+ }
135
+ if (dampingSettleMs) {
136
+ const v = Number.isFinite(cfg.settleMs) ? cfg.settleMs : Number(dampingSettleMs.value);
137
+ dampingSettleMs.value = Number.isFinite(v) ? String(v) : "0";
138
+ applyDampingSettleMs(dampingSettleMs.value);
139
+ dampingSettleMs.addEventListener("input", (e) => applyDampingSettleMs(e.target.value));
140
+ }
141
+ };
142
+ initDampingUi();
143
+
81
144
  const setStep1UiEnabled = (enabled) => {
82
145
  const on = !!enabled;
83
146
  if (step1ToneToggle) step1ToneToggle.checked = on;
@@ -37,6 +37,7 @@ export class LabelPlacementController {
37
37
  this.container = deps.container;
38
38
  this.logger = deps.logger || null;
39
39
  this._visibilityLogEnabled = !!deps.visibilityLogEnabled;
40
+ this._editingEnabled = deps?.editingEnabled !== false;
40
41
 
41
42
  this._placing = false;
42
43
  this._nextId = 1;
@@ -117,6 +118,7 @@ export class LabelPlacementController {
117
118
 
118
119
  this._ui = this.#createUi();
119
120
  this.#attachUi();
121
+ this.#syncEditingUi();
120
122
  this.#bindEvents();
121
123
  this.#startRaf();
122
124
  }
@@ -181,7 +183,7 @@ export class LabelPlacementController {
181
183
  }
182
184
 
183
185
  startPlacement() {
184
- if (this._placing || this._labelsHidden) return;
186
+ if (!this._editingEnabled || this._placing || this._labelsHidden) return;
185
187
  this._placing = true;
186
188
 
187
189
  const controls = this.viewer?.controls;
@@ -736,6 +738,7 @@ export class LabelPlacementController {
736
738
  try { e.preventDefault(); } catch (_) {}
737
739
  try { e.stopPropagation(); } catch (_) {}
738
740
  try { e.stopImmediatePropagation?.(); } catch (_) {}
741
+ if (!this._editingEnabled) return;
739
742
  this.startPlacement();
740
743
  };
741
744
  this._ui.btn.addEventListener("click", this._onBtnClick, { passive: false });
@@ -781,6 +784,7 @@ export class LabelPlacementController {
781
784
  const target = e.target;
782
785
  const action = target?.getAttribute?.("data-action");
783
786
  if (action !== "add") return;
787
+ if (!this._editingEnabled) return;
784
788
  if (this._labelsHidden) return;
785
789
  try { e.preventDefault(); } catch (_) {}
786
790
  try { e.stopPropagation(); } catch (_) {}
@@ -796,6 +800,7 @@ export class LabelPlacementController {
796
800
  if (e.key === "Escape") this.cancelPlacement();
797
801
  return;
798
802
  }
803
+ if (!this._editingEnabled) return;
799
804
 
800
805
  const target = e.target;
801
806
  const tag = (target && target.tagName) ? String(target.tagName).toLowerCase() : "";
@@ -930,6 +935,7 @@ export class LabelPlacementController {
930
935
 
931
936
  this._onCanvasContextMenu = (e) => {
932
937
  // Контекстное меню добавления метки по ПКМ на модели (если нет метки под курсором).
938
+ if (!this._editingEnabled) return;
933
939
  try { e.preventDefault(); } catch (_) {}
934
940
  try { e.stopPropagation(); } catch (_) {}
935
941
  try { e.stopImmediatePropagation?.(); } catch (_) {}
@@ -1243,6 +1249,7 @@ export class LabelPlacementController {
1243
1249
  }
1244
1250
 
1245
1251
  #emitLabelAction(action, marker = null) {
1252
+ if (!this._editingEnabled) return;
1246
1253
  const target = marker || this.#getSelectedMarker();
1247
1254
  if (!target) return;
1248
1255
  const detail = this.#buildActionPayload(target);
@@ -1251,6 +1258,7 @@ export class LabelPlacementController {
1251
1258
  }
1252
1259
 
1253
1260
  #openContextMenu(marker, clientX, clientY) {
1261
+ if (!this._editingEnabled) return;
1254
1262
  const menu = this._ui?.menu;
1255
1263
  if (!menu || !marker) return;
1256
1264
 
@@ -1364,6 +1372,11 @@ export class LabelPlacementController {
1364
1372
  // если были в режиме постановки — выходим
1365
1373
  try { this.cancelPlacement(); } catch (_) {}
1366
1374
 
1375
+ if (!this._editingEnabled) {
1376
+ this.#handleMarkerClick(marker);
1377
+ return;
1378
+ }
1379
+
1367
1380
  if (e.button === 0) {
1368
1381
  this.#closeContextMenu();
1369
1382
  this.#setSelectedMarker(marker);
@@ -1395,6 +1408,7 @@ export class LabelPlacementController {
1395
1408
  try { marker.el.addEventListener("dragend", onMarkerDragEnd); } catch (_) {}
1396
1409
 
1397
1410
  const onMarkerContextMenu = (e) => {
1411
+ if (!this._editingEnabled) return;
1398
1412
  try { e.preventDefault(); } catch (_) {}
1399
1413
  try { e.stopPropagation(); } catch (_) {}
1400
1414
  try { e.stopImmediatePropagation?.(); } catch (_) {}
@@ -1699,7 +1713,7 @@ export class LabelPlacementController {
1699
1713
  }
1700
1714
 
1701
1715
  #syncAddAvailability() {
1702
- const disabled = !!this._labelsHidden;
1716
+ const disabled = !!this._labelsHidden || !this._editingEnabled;
1703
1717
  const btn = this._ui?.btn;
1704
1718
  if (btn) {
1705
1719
  btn.disabled = disabled;
@@ -1711,5 +1725,33 @@ export class LabelPlacementController {
1711
1725
  try { menuAdd.classList.toggle("ifc-label-menu-item--disabled", disabled); } catch (_) {}
1712
1726
  }
1713
1727
  }
1728
+
1729
+ #syncEditingUi() {
1730
+ const actions = this._ui?.actions;
1731
+ if (actions) actions.style.display = this._editingEnabled ? "" : "none";
1732
+ if (!this._editingEnabled) {
1733
+ try { this.cancelPlacement(); } catch (_) {}
1734
+ try { this.#closeContextMenu(); } catch (_) {}
1735
+ try { this.#closeCanvasMenu(); } catch (_) {}
1736
+ this._labelDrag.active = false;
1737
+ this._labelDrag.moved = false;
1738
+ this._labelDrag.pointerId = null;
1739
+ this._labelDrag.id = null;
1740
+ this._labelDrag.clickMarker = null;
1741
+ this.#setDragGhostVisible(false);
1742
+ }
1743
+ this.#syncAddAvailability();
1744
+ }
1745
+
1746
+ setEditingEnabled(enabled) {
1747
+ const next = !!enabled;
1748
+ if (this._editingEnabled === next) return;
1749
+ this._editingEnabled = next;
1750
+ this.#syncEditingUi();
1751
+ }
1752
+
1753
+ getEditingEnabled() {
1754
+ return !!this._editingEnabled;
1755
+ }
1714
1756
  }
1715
1757
 
@@ -192,9 +192,9 @@ export class Viewer {
192
192
 
193
193
  this._damping = {
194
194
  dynamic: true,
195
- base: 0.06,
196
- settle: 0.18,
197
- settleMs: 250,
195
+ base: 0.5,
196
+ settle: 0,
197
+ settleMs: 0,
198
198
  isSettling: false,
199
199
  lastEndTs: 0,
200
200
  };
@@ -258,6 +258,53 @@ export class Viewer {
258
258
  return { enabled: !!this._zoomToCursor.enabled, debug: !!this._zoomToCursor.debug };
259
259
  }
260
260
 
261
+ /**
262
+ * Возвращает параметры динамического damping (для UI).
263
+ */
264
+ getDampingConfig() {
265
+ const d = this._damping || {};
266
+ return {
267
+ dynamic: !!d.dynamic,
268
+ base: Number.isFinite(d.base) ? d.base : 0,
269
+ settle: Number.isFinite(d.settle) ? d.settle : 0,
270
+ settleMs: Number.isFinite(d.settleMs) ? d.settleMs : 0,
271
+ };
272
+ }
273
+
274
+ /**
275
+ * Обновляет параметры динамического damping.
276
+ * @param {{ dynamic?: boolean, base?: number, settle?: number, settleMs?: number }} next
277
+ */
278
+ setDampingConfig(next = {}) {
279
+ if (!this._damping) return;
280
+ const clamp01 = (v) => Math.min(1, Math.max(0, v));
281
+ if (Object.prototype.hasOwnProperty.call(next, 'dynamic')) {
282
+ this._damping.dynamic = !!next.dynamic;
283
+ }
284
+ if (Object.prototype.hasOwnProperty.call(next, 'base')) {
285
+ const v = Number(next.base);
286
+ if (Number.isFinite(v)) this._damping.base = clamp01(v);
287
+ }
288
+ if (Object.prototype.hasOwnProperty.call(next, 'settle')) {
289
+ const v = Number(next.settle);
290
+ if (Number.isFinite(v)) this._damping.settle = clamp01(v);
291
+ }
292
+ if (Object.prototype.hasOwnProperty.call(next, 'settleMs')) {
293
+ const v = Number(next.settleMs);
294
+ if (Number.isFinite(v)) this._damping.settleMs = Math.max(0, v);
295
+ }
296
+ if (this.controls) {
297
+ if (!this._damping.dynamic) {
298
+ this._damping.isSettling = false;
299
+ this.controls.enableDamping = false;
300
+ this.controls.dampingFactor = this._damping.base;
301
+ } else {
302
+ this.controls.enableDamping = true;
303
+ if (!this._damping.isSettling) this.controls.dampingFactor = this._damping.base;
304
+ }
305
+ }
306
+ }
307
+
261
308
  /**
262
309
  * Возвращает "домашнюю" точку вращения для текущей модели:
263
310
  * центр bbox (совпадает с кадрированием при первичной загрузке).