@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 +1 -1
- package/src/IfcViewer.js +26 -1
- package/src/main.js +63 -0
- package/src/ui/LabelPlacementController.js +44 -2
- package/src/viewer/Viewer.js +50 -3
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.
|
|
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({
|
|
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
|
|
package/src/viewer/Viewer.js
CHANGED
|
@@ -192,9 +192,9 @@ export class Viewer {
|
|
|
192
192
|
|
|
193
193
|
this._damping = {
|
|
194
194
|
dynamic: true,
|
|
195
|
-
base: 0.
|
|
196
|
-
settle: 0
|
|
197
|
-
settleMs:
|
|
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 (совпадает с кадрированием при первичной загрузке).
|