@sequent-org/ifc-viewer 1.2.4-ci.61.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/styles-local.css +7 -0
- package/src/ui/LabelPlacementController.js +217 -12
- package/src/viewer/Viewer.js +89 -2
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;
|
package/src/styles-local.css
CHANGED
|
@@ -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,8 @@ 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;
|
|
40
|
+
this._editingEnabled = deps?.editingEnabled !== false;
|
|
37
41
|
|
|
38
42
|
this._placing = false;
|
|
39
43
|
this._nextId = 1;
|
|
@@ -46,6 +50,8 @@ export class LabelPlacementController {
|
|
|
46
50
|
this._ndc = new THREE.Vector2();
|
|
47
51
|
this._tmpV = new THREE.Vector3();
|
|
48
52
|
this._tmpLocal = new THREE.Vector3();
|
|
53
|
+
this._tmpV2 = new THREE.Vector3();
|
|
54
|
+
this._tmpNdc = new THREE.Vector3();
|
|
49
55
|
|
|
50
56
|
this._lastPointer = { x: 0, y: 0 };
|
|
51
57
|
this._ghostPos = { x: 0, y: 0 };
|
|
@@ -97,8 +103,22 @@ export class LabelPlacementController {
|
|
|
97
103
|
v2: new THREE.Vector3(),
|
|
98
104
|
};
|
|
99
105
|
|
|
106
|
+
this._autoHide = {
|
|
107
|
+
active: false,
|
|
108
|
+
prevHidden: false,
|
|
109
|
+
};
|
|
110
|
+
this._showAfterStop = {
|
|
111
|
+
raf: 0,
|
|
112
|
+
active: false,
|
|
113
|
+
lastChangeTs: 0,
|
|
114
|
+
idleMs: 0,
|
|
115
|
+
eps: 5e-4,
|
|
116
|
+
lastCamMatrix: new Float32Array(16),
|
|
117
|
+
};
|
|
118
|
+
|
|
100
119
|
this._ui = this.#createUi();
|
|
101
120
|
this.#attachUi();
|
|
121
|
+
this.#syncEditingUi();
|
|
102
122
|
this.#bindEvents();
|
|
103
123
|
this.#startRaf();
|
|
104
124
|
}
|
|
@@ -140,6 +160,14 @@ export class LabelPlacementController {
|
|
|
140
160
|
try { dom?.removeEventListener("contextmenu", this._onCanvasContextMenu, { capture: true }); } catch (_) {
|
|
141
161
|
try { dom?.removeEventListener("contextmenu", this._onCanvasContextMenu); } catch (_) {}
|
|
142
162
|
}
|
|
163
|
+
try {
|
|
164
|
+
const controls = this.viewer?.controls;
|
|
165
|
+
if (controls && typeof controls.removeEventListener === "function") {
|
|
166
|
+
try { controls.removeEventListener("start", this._onControlsStart); } catch (_) {}
|
|
167
|
+
try { controls.removeEventListener("end", this._onControlsEnd); } catch (_) {}
|
|
168
|
+
}
|
|
169
|
+
} catch (_) {}
|
|
170
|
+
try { this.#cancelShowAfterStop(); } catch (_) {}
|
|
143
171
|
|
|
144
172
|
if (this._raf) cancelAnimationFrame(this._raf);
|
|
145
173
|
this._raf = 0;
|
|
@@ -155,7 +183,7 @@ export class LabelPlacementController {
|
|
|
155
183
|
}
|
|
156
184
|
|
|
157
185
|
startPlacement() {
|
|
158
|
-
if (this._placing || this._labelsHidden) return;
|
|
186
|
+
if (!this._editingEnabled || this._placing || this._labelsHidden) return;
|
|
159
187
|
this._placing = true;
|
|
160
188
|
|
|
161
189
|
const controls = this.viewer?.controls;
|
|
@@ -710,6 +738,7 @@ export class LabelPlacementController {
|
|
|
710
738
|
try { e.preventDefault(); } catch (_) {}
|
|
711
739
|
try { e.stopPropagation(); } catch (_) {}
|
|
712
740
|
try { e.stopImmediatePropagation?.(); } catch (_) {}
|
|
741
|
+
if (!this._editingEnabled) return;
|
|
713
742
|
this.startPlacement();
|
|
714
743
|
};
|
|
715
744
|
this._ui.btn.addEventListener("click", this._onBtnClick, { passive: false });
|
|
@@ -755,6 +784,7 @@ export class LabelPlacementController {
|
|
|
755
784
|
const target = e.target;
|
|
756
785
|
const action = target?.getAttribute?.("data-action");
|
|
757
786
|
if (action !== "add") return;
|
|
787
|
+
if (!this._editingEnabled) return;
|
|
758
788
|
if (this._labelsHidden) return;
|
|
759
789
|
try { e.preventDefault(); } catch (_) {}
|
|
760
790
|
try { e.stopPropagation(); } catch (_) {}
|
|
@@ -770,6 +800,7 @@ export class LabelPlacementController {
|
|
|
770
800
|
if (e.key === "Escape") this.cancelPlacement();
|
|
771
801
|
return;
|
|
772
802
|
}
|
|
803
|
+
if (!this._editingEnabled) return;
|
|
773
804
|
|
|
774
805
|
const target = e.target;
|
|
775
806
|
const tag = (target && target.tagName) ? String(target.tagName).toLowerCase() : "";
|
|
@@ -904,6 +935,7 @@ export class LabelPlacementController {
|
|
|
904
935
|
|
|
905
936
|
this._onCanvasContextMenu = (e) => {
|
|
906
937
|
// Контекстное меню добавления метки по ПКМ на модели (если нет метки под курсором).
|
|
938
|
+
if (!this._editingEnabled) return;
|
|
907
939
|
try { e.preventDefault(); } catch (_) {}
|
|
908
940
|
try { e.stopPropagation(); } catch (_) {}
|
|
909
941
|
try { e.stopImmediatePropagation?.(); } catch (_) {}
|
|
@@ -920,6 +952,19 @@ export class LabelPlacementController {
|
|
|
920
952
|
this.#openCanvasMenu(hit, e.clientX, e.clientY);
|
|
921
953
|
};
|
|
922
954
|
dom.addEventListener("contextmenu", this._onCanvasContextMenu, { capture: true, passive: false });
|
|
955
|
+
|
|
956
|
+
const controls = this.viewer?.controls;
|
|
957
|
+
if (controls && typeof controls.addEventListener === "function") {
|
|
958
|
+
this._onControlsStart = () => {
|
|
959
|
+
this.#beginAutoHideForControls();
|
|
960
|
+
};
|
|
961
|
+
this._onControlsEnd = () => {
|
|
962
|
+
this.#scheduleShowAfterStop();
|
|
963
|
+
};
|
|
964
|
+
try { controls.addEventListener("start", this._onControlsStart); } catch (_) {}
|
|
965
|
+
try { controls.addEventListener("end", this._onControlsEnd); } catch (_) {}
|
|
966
|
+
}
|
|
967
|
+
|
|
923
968
|
}
|
|
924
969
|
|
|
925
970
|
#setGhostVisible(visible) {
|
|
@@ -1108,6 +1153,7 @@ export class LabelPlacementController {
|
|
|
1108
1153
|
let best = null;
|
|
1109
1154
|
for (const h of hits) {
|
|
1110
1155
|
if (!h || !h.point) continue;
|
|
1156
|
+
if (this.#isPointClippedBySection(h.point)) continue;
|
|
1111
1157
|
const obj = h.object;
|
|
1112
1158
|
if (obj && obj.isMesh) { best = h; break; }
|
|
1113
1159
|
}
|
|
@@ -1203,6 +1249,7 @@ export class LabelPlacementController {
|
|
|
1203
1249
|
}
|
|
1204
1250
|
|
|
1205
1251
|
#emitLabelAction(action, marker = null) {
|
|
1252
|
+
if (!this._editingEnabled) return;
|
|
1206
1253
|
const target = marker || this.#getSelectedMarker();
|
|
1207
1254
|
if (!target) return;
|
|
1208
1255
|
const detail = this.#buildActionPayload(target);
|
|
@@ -1211,6 +1258,7 @@ export class LabelPlacementController {
|
|
|
1211
1258
|
}
|
|
1212
1259
|
|
|
1213
1260
|
#openContextMenu(marker, clientX, clientY) {
|
|
1261
|
+
if (!this._editingEnabled) return;
|
|
1214
1262
|
const menu = this._ui?.menu;
|
|
1215
1263
|
if (!menu || !marker) return;
|
|
1216
1264
|
|
|
@@ -1324,6 +1372,11 @@ export class LabelPlacementController {
|
|
|
1324
1372
|
// если были в режиме постановки — выходим
|
|
1325
1373
|
try { this.cancelPlacement(); } catch (_) {}
|
|
1326
1374
|
|
|
1375
|
+
if (!this._editingEnabled) {
|
|
1376
|
+
this.#handleMarkerClick(marker);
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1327
1380
|
if (e.button === 0) {
|
|
1328
1381
|
this.#closeContextMenu();
|
|
1329
1382
|
this.#setSelectedMarker(marker);
|
|
@@ -1355,6 +1408,7 @@ export class LabelPlacementController {
|
|
|
1355
1408
|
try { marker.el.addEventListener("dragend", onMarkerDragEnd); } catch (_) {}
|
|
1356
1409
|
|
|
1357
1410
|
const onMarkerContextMenu = (e) => {
|
|
1411
|
+
if (!this._editingEnabled) return;
|
|
1358
1412
|
try { e.preventDefault(); } catch (_) {}
|
|
1359
1413
|
try { e.stopPropagation(); } catch (_) {}
|
|
1360
1414
|
try { e.stopImmediatePropagation?.(); } catch (_) {}
|
|
@@ -1460,12 +1514,12 @@ export class LabelPlacementController {
|
|
|
1460
1514
|
if (!m || !m.el) continue;
|
|
1461
1515
|
|
|
1462
1516
|
if (this._labelsHidden) {
|
|
1463
|
-
m
|
|
1517
|
+
this.#setMarkerVisibility(m, false, "labelsHidden");
|
|
1464
1518
|
continue;
|
|
1465
1519
|
}
|
|
1466
1520
|
|
|
1467
1521
|
if (!model) {
|
|
1468
|
-
m
|
|
1522
|
+
this.#setMarkerVisibility(m, false, "noModel");
|
|
1469
1523
|
continue;
|
|
1470
1524
|
}
|
|
1471
1525
|
|
|
@@ -1474,16 +1528,20 @@ export class LabelPlacementController {
|
|
|
1474
1528
|
model.localToWorld(this._tmpV);
|
|
1475
1529
|
|
|
1476
1530
|
if (this.#isPointClippedBySection(this._tmpV)) {
|
|
1477
|
-
m
|
|
1531
|
+
this.#setMarkerVisibility(m, false, "clipped");
|
|
1478
1532
|
continue;
|
|
1479
1533
|
}
|
|
1480
1534
|
|
|
1481
|
-
const ndc = this._tmpV.project(camera);
|
|
1535
|
+
const ndc = this._tmpNdc.copy(this._tmpV).project(camera);
|
|
1482
1536
|
|
|
1483
|
-
// Если точка за камерой или
|
|
1484
|
-
const
|
|
1485
|
-
|
|
1486
|
-
|
|
1537
|
+
// Если точка за камерой или вне кадра — скрываем
|
|
1538
|
+
const ndcFinite = Number.isFinite(ndc.x) && Number.isFinite(ndc.y) && Number.isFinite(ndc.z);
|
|
1539
|
+
const inView = ndcFinite
|
|
1540
|
+
&& ndc.x >= -1 && ndc.x <= 1
|
|
1541
|
+
&& ndc.y >= -1 && ndc.y <= 1
|
|
1542
|
+
&& ndc.z >= -1 && ndc.z <= 1;
|
|
1543
|
+
if (!inView) {
|
|
1544
|
+
this.#setMarkerVisibility(m, false, "outOfView");
|
|
1487
1545
|
continue;
|
|
1488
1546
|
}
|
|
1489
1547
|
|
|
@@ -1491,15 +1549,62 @@ export class LabelPlacementController {
|
|
|
1491
1549
|
const y = (-ndc.y * 0.5 + 0.5) * h;
|
|
1492
1550
|
|
|
1493
1551
|
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
1494
|
-
m
|
|
1552
|
+
this.#setMarkerVisibility(m, false, "invalidScreen");
|
|
1495
1553
|
continue;
|
|
1496
1554
|
}
|
|
1497
1555
|
|
|
1498
|
-
|
|
1556
|
+
const occluded = this.#isPointOccludedByModel(this._tmpV, ndc, model, camera);
|
|
1557
|
+
if (occluded) {
|
|
1558
|
+
this.#setMarkerVisibility(m, false, "occluded");
|
|
1559
|
+
continue;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
this.#setMarkerVisibility(m, true, "visible");
|
|
1499
1563
|
m.el.style.transform = `translate3d(${x}px, ${y}px, 0) translate(-50%, -50%)`;
|
|
1500
1564
|
}
|
|
1501
1565
|
}
|
|
1502
1566
|
|
|
1567
|
+
#setMarkerVisibility(marker, visible, reason) {
|
|
1568
|
+
if (!marker || !marker.el) return;
|
|
1569
|
+
if (visible) {
|
|
1570
|
+
marker.el.style.display = "block";
|
|
1571
|
+
try { marker.el.classList.remove("ifc-label-marker--hidden"); } catch (_) {}
|
|
1572
|
+
} else {
|
|
1573
|
+
try { marker.el.classList.add("ifc-label-marker--hidden"); } catch (_) {}
|
|
1574
|
+
marker.el.style.display = "block";
|
|
1575
|
+
}
|
|
1576
|
+
if (!this._visibilityLogEnabled) return;
|
|
1577
|
+
if (marker.visible === visible && marker.hiddenReason === reason) return;
|
|
1578
|
+
marker.visible = visible;
|
|
1579
|
+
marker.hiddenReason = reason;
|
|
1580
|
+
this.logger?.log?.("[LabelVisibility]", {
|
|
1581
|
+
id: marker.id,
|
|
1582
|
+
visible,
|
|
1583
|
+
reason,
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
#isPointOccludedByModel(pointWorld, ndc, model, camera) {
|
|
1588
|
+
if (!pointWorld || !ndc || !model || !camera) return false;
|
|
1589
|
+
this._raycaster.setFromCamera(ndc, camera);
|
|
1590
|
+
const hits = this._raycaster.intersectObject(model, true);
|
|
1591
|
+
if (!hits || hits.length === 0) return false;
|
|
1592
|
+
let hit = null;
|
|
1593
|
+
for (const h of hits) {
|
|
1594
|
+
if (!h || !h.object || !h.object.isMesh) continue;
|
|
1595
|
+
if (this.#isPointClippedBySection(h.point)) continue;
|
|
1596
|
+
hit = h;
|
|
1597
|
+
break;
|
|
1598
|
+
}
|
|
1599
|
+
if (!hit || !Number.isFinite(hit.distance)) return false;
|
|
1600
|
+
const ray = this._raycaster.ray;
|
|
1601
|
+
this._tmpV2.copy(pointWorld).sub(ray.origin);
|
|
1602
|
+
const t = this._tmpV2.dot(ray.direction);
|
|
1603
|
+
if (!Number.isFinite(t) || t <= 0) return false;
|
|
1604
|
+
const epsilon = 1e-2;
|
|
1605
|
+
return hit.distance + epsilon < t;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1503
1608
|
#isPointClippedBySection(pointWorld) {
|
|
1504
1609
|
const planes = this.viewer?.clipping?.planes || [];
|
|
1505
1610
|
for (const plane of planes) {
|
|
@@ -1510,6 +1615,12 @@ export class LabelPlacementController {
|
|
|
1510
1615
|
return false;
|
|
1511
1616
|
}
|
|
1512
1617
|
|
|
1618
|
+
#copyMatrix(matrix, cache) {
|
|
1619
|
+
if (!matrix || !cache || cache.length !== 16) return;
|
|
1620
|
+
const e = matrix.elements;
|
|
1621
|
+
for (let i = 0; i < 16; i += 1) cache[i] = e[i] ?? 0;
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1513
1624
|
#setLabelsHidden(hidden) {
|
|
1514
1625
|
const next = !!hidden;
|
|
1515
1626
|
if (this._labelsHidden === next) return;
|
|
@@ -1522,6 +1633,72 @@ export class LabelPlacementController {
|
|
|
1522
1633
|
this.#syncHideButton();
|
|
1523
1634
|
}
|
|
1524
1635
|
|
|
1636
|
+
#beginAutoHideForControls() {
|
|
1637
|
+
if (!this._autoHide.active) {
|
|
1638
|
+
this._autoHide.prevHidden = this._labelsHidden;
|
|
1639
|
+
this._autoHide.active = true;
|
|
1640
|
+
}
|
|
1641
|
+
this._labelsHidden = true;
|
|
1642
|
+
this.#syncHideButton();
|
|
1643
|
+
this.#cancelShowAfterStop();
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
#scheduleShowAfterStop() {
|
|
1647
|
+
if (!this._autoHide.active) return;
|
|
1648
|
+
const cam = this.viewer?.camera;
|
|
1649
|
+
if (!cam) return;
|
|
1650
|
+
this._showAfterStop.active = true;
|
|
1651
|
+
this._showAfterStop.lastChangeTs = performance.now();
|
|
1652
|
+
this.#copyMatrix(cam.matrixWorld, this._showAfterStop.lastCamMatrix);
|
|
1653
|
+
if (!this._showAfterStop.raf) {
|
|
1654
|
+
this._showAfterStop.raf = requestAnimationFrame(() => this.#tickShowAfterStop());
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
#cancelShowAfterStop() {
|
|
1659
|
+
if (this._showAfterStop.raf) cancelAnimationFrame(this._showAfterStop.raf);
|
|
1660
|
+
this._showAfterStop.raf = 0;
|
|
1661
|
+
this._showAfterStop.active = false;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
#tickShowAfterStop() {
|
|
1665
|
+
if (!this._showAfterStop.active) {
|
|
1666
|
+
this._showAfterStop.raf = 0;
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1669
|
+
const cam = this.viewer?.camera;
|
|
1670
|
+
if (!cam) {
|
|
1671
|
+
this._showAfterStop.raf = 0;
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
const now = performance.now();
|
|
1675
|
+
if (!this.#isCameraStable(cam.matrixWorld, this._showAfterStop.lastCamMatrix, this._showAfterStop.eps)) {
|
|
1676
|
+
this._showAfterStop.lastChangeTs = now;
|
|
1677
|
+
}
|
|
1678
|
+
if (now - this._showAfterStop.lastChangeTs >= this._showAfterStop.idleMs) {
|
|
1679
|
+
this._labelsHidden = this._autoHide.prevHidden;
|
|
1680
|
+
this._autoHide.active = false;
|
|
1681
|
+
this.#syncHideButton();
|
|
1682
|
+
this._showAfterStop.active = false;
|
|
1683
|
+
this._showAfterStop.raf = 0;
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
this._showAfterStop.raf = requestAnimationFrame(() => this.#tickShowAfterStop());
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
#isCameraStable(matrix, cache, eps) {
|
|
1690
|
+
if (!matrix || !cache || cache.length !== 16) return true;
|
|
1691
|
+
const e = matrix.elements;
|
|
1692
|
+
let stable = true;
|
|
1693
|
+
for (let i = 0; i < 16; i += 1) {
|
|
1694
|
+
const v = e[i] ?? 0;
|
|
1695
|
+
const d = Math.abs(v - cache[i]);
|
|
1696
|
+
if (d > eps) stable = false;
|
|
1697
|
+
cache[i] = v;
|
|
1698
|
+
}
|
|
1699
|
+
return stable;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1525
1702
|
#syncHideButton() {
|
|
1526
1703
|
const btn = this._ui?.hideBtn;
|
|
1527
1704
|
if (!btn) return;
|
|
@@ -1536,7 +1713,7 @@ export class LabelPlacementController {
|
|
|
1536
1713
|
}
|
|
1537
1714
|
|
|
1538
1715
|
#syncAddAvailability() {
|
|
1539
|
-
const disabled = !!this._labelsHidden;
|
|
1716
|
+
const disabled = !!this._labelsHidden || !this._editingEnabled;
|
|
1540
1717
|
const btn = this._ui?.btn;
|
|
1541
1718
|
if (btn) {
|
|
1542
1719
|
btn.disabled = disabled;
|
|
@@ -1548,5 +1725,33 @@ export class LabelPlacementController {
|
|
|
1548
1725
|
try { menuAdd.classList.toggle("ifc-label-menu-item--disabled", disabled); } catch (_) {}
|
|
1549
1726
|
}
|
|
1550
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
|
+
}
|
|
1551
1756
|
}
|
|
1552
1757
|
|
package/src/viewer/Viewer.js
CHANGED
|
@@ -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.5,
|
|
196
|
+
settle: 0,
|
|
197
|
+
settleMs: 0,
|
|
198
|
+
isSettling: false,
|
|
199
|
+
lastEndTs: 0,
|
|
200
|
+
};
|
|
201
|
+
|
|
193
202
|
this._onPointerDown = null;
|
|
194
203
|
this._onPointerUp = null;
|
|
195
204
|
this._onPointerMove = null;
|
|
@@ -249,6 +258,53 @@ export class Viewer {
|
|
|
249
258
|
return { enabled: !!this._zoomToCursor.enabled, debug: !!this._zoomToCursor.debug };
|
|
250
259
|
}
|
|
251
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
|
+
|
|
252
308
|
/**
|
|
253
309
|
* Возвращает "домашнюю" точку вращения для текущей модели:
|
|
254
310
|
* центр bbox (совпадает с кадрированием при первичной загрузке).
|
|
@@ -701,13 +757,25 @@ export class Viewer {
|
|
|
701
757
|
const dir = this.camera.position.clone().sub(this.controls.target).normalize();
|
|
702
758
|
this._prevViewDir = dir;
|
|
703
759
|
this._smoothedAxis = null;
|
|
760
|
+
if (this._damping.dynamic) {
|
|
761
|
+
this.controls.dampingFactor = this._damping.base;
|
|
762
|
+
this._damping.isSettling = false;
|
|
763
|
+
this.controls.enableDamping = true;
|
|
764
|
+
}
|
|
704
765
|
};
|
|
705
766
|
this._onControlsChange = () => {
|
|
706
767
|
// Обновляем ось только при зажатой ЛКМ (вращение)
|
|
707
768
|
if (!this._isLmbDown) return;
|
|
708
769
|
this.#updateRotationAxisLine();
|
|
709
770
|
};
|
|
710
|
-
this._onControlsEnd = () => {
|
|
771
|
+
this._onControlsEnd = () => {
|
|
772
|
+
this.#hideRotationAxisLine();
|
|
773
|
+
if (this._damping.dynamic && this.controls) {
|
|
774
|
+
this._damping.isSettling = true;
|
|
775
|
+
this._damping.lastEndTs = performance.now();
|
|
776
|
+
this.controls.enableDamping = true;
|
|
777
|
+
}
|
|
778
|
+
};
|
|
711
779
|
this.controls.addEventListener('start', this._onControlsStart);
|
|
712
780
|
this.controls.addEventListener('change', this._onControlsChange);
|
|
713
781
|
this.controls.addEventListener('end', this._onControlsEnd);
|
|
@@ -768,7 +836,10 @@ export class Viewer {
|
|
|
768
836
|
this.demoCube.rotation.x += 0.005;
|
|
769
837
|
}
|
|
770
838
|
|
|
771
|
-
if (this.controls)
|
|
839
|
+
if (this.controls) {
|
|
840
|
+
this.#updateDynamicDamping();
|
|
841
|
+
this.controls.update();
|
|
842
|
+
}
|
|
772
843
|
// "Внутренняя" подсветка/пост-эффекты: включаются только когда камера внутри модели
|
|
773
844
|
this.#updateInteriorAssist();
|
|
774
845
|
this._notifyZoomIfChanged();
|
|
@@ -814,6 +885,22 @@ export class Viewer {
|
|
|
814
885
|
this.animationId = requestAnimationFrame(this.animate);
|
|
815
886
|
}
|
|
816
887
|
|
|
888
|
+
#updateDynamicDamping() {
|
|
889
|
+
if (!this.controls) return;
|
|
890
|
+
if (!this._damping.dynamic || !this.controls.enableDamping) return;
|
|
891
|
+
if (this._damping.isSettling) {
|
|
892
|
+
const now = performance.now();
|
|
893
|
+
if (now - this._damping.lastEndTs <= this._damping.settleMs) {
|
|
894
|
+
this.controls.dampingFactor = this._damping.settle;
|
|
895
|
+
} else {
|
|
896
|
+
this._damping.isSettling = false;
|
|
897
|
+
this.controls.dampingFactor = this._damping.base;
|
|
898
|
+
}
|
|
899
|
+
} else {
|
|
900
|
+
this.controls.dampingFactor = this._damping.base;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
817
904
|
dispose() {
|
|
818
905
|
if (this.animationId) cancelAnimationFrame(this.animationId);
|
|
819
906
|
window.removeEventListener("resize", this.handleResize);
|