@sequent-org/ifc-viewer 1.0.2-ci.2.0 → 1.0.2-ci.4.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/README.md +201 -0
- package/package.json +29 -2
- package/src/IfcViewer.js +782 -0
- package/src/index.js +13 -0
- package/.github/workflows/npm-publish.yml +0 -39
- package/console-log.txt +0 -1924
- package/fragments.html +0 -46
- package/index.html +0 -101
- package/postcss.config.cjs +0 -7
- package/tailwind.config.cjs +0 -5
- package/vite.config.js +0 -36
package/src/IfcViewer.js
ADDED
|
@@ -0,0 +1,782 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Класс IfcViewer - основная точка входа для просмотра IFC моделей
|
|
3
|
+
* Инкапсулирует всю логику создания и управления 3D просмотрщиком
|
|
4
|
+
*
|
|
5
|
+
* Пример использования:
|
|
6
|
+
* const viewer = new IfcViewer({
|
|
7
|
+
* container: document.getElementById('modal-content'),
|
|
8
|
+
* ifcUrl: '/path/to/model.ifc'
|
|
9
|
+
* })
|
|
10
|
+
* await viewer.init()
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { Viewer } from "./viewer/Viewer.js";
|
|
14
|
+
import { IfcService } from "./ifc/IfcService.js";
|
|
15
|
+
import { IfcTreeView } from "./ifc/IfcTreeView.js";
|
|
16
|
+
|
|
17
|
+
export class IfcViewer {
|
|
18
|
+
/**
|
|
19
|
+
* Создаёт новый экземпляр IfcViewer
|
|
20
|
+
* @param {Object} options - Параметры конфигурации
|
|
21
|
+
* @param {HTMLElement|string} options.container - Контейнер для рендера (элемент или селектор)
|
|
22
|
+
* @param {string} [options.ifcUrl] - URL для загрузки IFC файла
|
|
23
|
+
* @param {File} [options.ifcFile] - File объект для загрузки IFC файла
|
|
24
|
+
* @param {boolean} [options.showSidebar=false] - Показывать ли боковую панель с деревом
|
|
25
|
+
* @param {boolean} [options.showControls=false] - Показывать ли панель управления (нижние кнопки)
|
|
26
|
+
* @param {boolean} [options.showToolbar=true] - Показывать ли верхнюю панель инструментов
|
|
27
|
+
* @param {boolean} [options.autoLoad=true] - Автоматически загружать IFC файл при инициализации
|
|
28
|
+
* @param {string} [options.theme='light'] - Тема интерфейса ('light' | 'dark')
|
|
29
|
+
* @param {Object} [options.viewerOptions] - Дополнительные опции для Viewer
|
|
30
|
+
*/
|
|
31
|
+
constructor(options = {}) {
|
|
32
|
+
// Валидация параметров
|
|
33
|
+
if (!options.container) {
|
|
34
|
+
throw new Error('IfcViewer: параметр container обязателен');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Получение контейнера
|
|
38
|
+
this.containerElement = typeof options.container === 'string'
|
|
39
|
+
? document.querySelector(options.container)
|
|
40
|
+
: options.container;
|
|
41
|
+
|
|
42
|
+
if (!this.containerElement) {
|
|
43
|
+
throw new Error('IfcViewer: контейнер не найден');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Сохранение конфигурации
|
|
47
|
+
this.options = {
|
|
48
|
+
ifcUrl: options.ifcUrl || null,
|
|
49
|
+
ifcFile: options.ifcFile || null,
|
|
50
|
+
showSidebar: options.showSidebar === true, // по умолчанию false
|
|
51
|
+
showControls: options.showControls === true, // по умолчанию false
|
|
52
|
+
showToolbar: options.showToolbar !== false, // по умолчанию true
|
|
53
|
+
autoLoad: options.autoLoad !== false,
|
|
54
|
+
theme: options.theme || 'light',
|
|
55
|
+
viewerOptions: options.viewerOptions || {}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Внутренние компоненты
|
|
59
|
+
this.viewer = null;
|
|
60
|
+
this.ifcService = null;
|
|
61
|
+
this.ifcTreeView = null;
|
|
62
|
+
this.isInitialized = false;
|
|
63
|
+
this.currentModel = null;
|
|
64
|
+
|
|
65
|
+
// DOM элементы интерфейса
|
|
66
|
+
this.elements = {
|
|
67
|
+
viewerContainer: null,
|
|
68
|
+
sidebar: null,
|
|
69
|
+
controls: null,
|
|
70
|
+
uploadInput: null
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Слушатели событий для очистки
|
|
74
|
+
this.eventListeners = new Map();
|
|
75
|
+
|
|
76
|
+
// Внутренние состояния управления
|
|
77
|
+
this.viewerState = {
|
|
78
|
+
quality: 'medium', // 'low' | 'medium' | 'high'
|
|
79
|
+
edgesVisible: true,
|
|
80
|
+
flatShading: true,
|
|
81
|
+
clipping: {
|
|
82
|
+
x: false,
|
|
83
|
+
y: false,
|
|
84
|
+
z: false,
|
|
85
|
+
active: null // текущая активная ось
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Инициализирует просмотрщик и создаёт интерфейс
|
|
92
|
+
* @returns {Promise<void>}
|
|
93
|
+
*/
|
|
94
|
+
async init() {
|
|
95
|
+
if (this.isInitialized) {
|
|
96
|
+
console.warn('IfcViewer: уже инициализирован');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
// Создаём разметку интерфейса
|
|
102
|
+
this._createInterface();
|
|
103
|
+
|
|
104
|
+
// Применяем тему
|
|
105
|
+
this._applyTheme();
|
|
106
|
+
|
|
107
|
+
// Инициализируем компоненты
|
|
108
|
+
this._initViewer();
|
|
109
|
+
this._initIfcService();
|
|
110
|
+
this._initTreeView();
|
|
111
|
+
|
|
112
|
+
// Настраиваем обработчики событий
|
|
113
|
+
this._setupEventHandlers();
|
|
114
|
+
|
|
115
|
+
// Автозагрузка файла если указан
|
|
116
|
+
if (this.options.autoLoad && (this.options.ifcUrl || this.options.ifcFile)) {
|
|
117
|
+
await this.loadModel();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
this.isInitialized = true;
|
|
121
|
+
|
|
122
|
+
// Диспетчируем событие готовности
|
|
123
|
+
this._dispatchEvent('ready', { viewer: this });
|
|
124
|
+
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.error('IfcViewer: ошибка инициализации', error);
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Загружает IFC модель из URL или File
|
|
133
|
+
* @param {string|File} [source] - Источник модели (URL или File). Если не указан, использует из options
|
|
134
|
+
* @returns {Promise<Object|null>} - Загруженная модель или null при ошибке
|
|
135
|
+
*/
|
|
136
|
+
async loadModel(source) {
|
|
137
|
+
if (!this.ifcService) {
|
|
138
|
+
throw new Error('IfcViewer: не инициализирован. Вызовите init() сначала');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
let model = null;
|
|
143
|
+
const loadSource = source || this.options.ifcUrl || this.options.ifcFile;
|
|
144
|
+
|
|
145
|
+
if (!loadSource) {
|
|
146
|
+
throw new Error('Не указан источник IFC модели');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Показываем прелоадер если есть
|
|
150
|
+
this._showPreloader();
|
|
151
|
+
|
|
152
|
+
// Загружаем модель в зависимости от типа источника
|
|
153
|
+
if (typeof loadSource === 'string') {
|
|
154
|
+
model = await this.ifcService.loadUrl(loadSource);
|
|
155
|
+
} else if (loadSource instanceof File) {
|
|
156
|
+
model = await this.ifcService.loadFile(loadSource);
|
|
157
|
+
} else {
|
|
158
|
+
throw new Error('Неподдерживаемый тип источника модели');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (model) {
|
|
162
|
+
this.currentModel = model;
|
|
163
|
+
|
|
164
|
+
// Обновляем дерево структуры
|
|
165
|
+
await this._updateTreeView(model);
|
|
166
|
+
|
|
167
|
+
// Обновляем информационную панель
|
|
168
|
+
this._updateInfoPanel();
|
|
169
|
+
|
|
170
|
+
// Показываем сайдбар при успешной загрузке
|
|
171
|
+
if (this.options.showSidebar) {
|
|
172
|
+
this._setSidebarVisible(true);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Диспетчируем событие загрузки модели
|
|
176
|
+
this._dispatchEvent('model-loaded', { model, viewer: this });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
this._hidePreloader();
|
|
180
|
+
return model;
|
|
181
|
+
|
|
182
|
+
} catch (error) {
|
|
183
|
+
console.error('IfcViewer: ошибка загрузки модели', error);
|
|
184
|
+
this._hidePreloader();
|
|
185
|
+
this._dispatchEvent('error', { error, viewer: this });
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Освобождает ресурсы и очищает интерфейс
|
|
192
|
+
*/
|
|
193
|
+
dispose() {
|
|
194
|
+
if (!this.isInitialized) return;
|
|
195
|
+
|
|
196
|
+
// Очищаем слушатели событий
|
|
197
|
+
this.eventListeners.forEach((listener, key) => {
|
|
198
|
+
const [element, event] = key.split('.');
|
|
199
|
+
const el = element === 'document' ? document :
|
|
200
|
+
element === 'window' ? window : this.elements[element];
|
|
201
|
+
if (el && listener) {
|
|
202
|
+
el.removeEventListener(event, listener);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
this.eventListeners.clear();
|
|
206
|
+
|
|
207
|
+
// Освобождаем компоненты
|
|
208
|
+
if (this.ifcService) {
|
|
209
|
+
this.ifcService.dispose();
|
|
210
|
+
this.ifcService = null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (this.viewer) {
|
|
214
|
+
this.viewer.dispose();
|
|
215
|
+
this.viewer = null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Очищаем DOM
|
|
219
|
+
if (this.containerElement) {
|
|
220
|
+
this.containerElement.innerHTML = '';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
this.isInitialized = false;
|
|
224
|
+
this.currentModel = null;
|
|
225
|
+
|
|
226
|
+
// Диспетчируем событие освобождения ресурсов
|
|
227
|
+
this._dispatchEvent('disposed', { viewer: this });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Получает информацию о текущей модели
|
|
232
|
+
* @returns {Object|null} Информация о модели или null
|
|
233
|
+
*/
|
|
234
|
+
getModelInfo() {
|
|
235
|
+
if (!this.ifcService) return null;
|
|
236
|
+
return this.ifcService.getLastInfo();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Получает экземпляр Viewer для прямого доступа к функциям просмотра
|
|
241
|
+
* @returns {Viewer|null}
|
|
242
|
+
*/
|
|
243
|
+
getViewer() {
|
|
244
|
+
return this.viewer;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Получает экземпляр IfcService для работы с IFC данными
|
|
249
|
+
* @returns {IfcService|null}
|
|
250
|
+
*/
|
|
251
|
+
getIfcService() {
|
|
252
|
+
return this.ifcService;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Устанавливает видимость боковой панели
|
|
257
|
+
* @param {boolean} visible - Показать или скрыть
|
|
258
|
+
*/
|
|
259
|
+
setSidebarVisible(visible) {
|
|
260
|
+
this._setSidebarVisible(visible);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Переключает тему интерфейса
|
|
265
|
+
* @param {string} theme - Новая тема ('light' | 'dark')
|
|
266
|
+
*/
|
|
267
|
+
setTheme(theme) {
|
|
268
|
+
this.options.theme = theme;
|
|
269
|
+
this._applyTheme();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ==================== ПРИВАТНЫЕ МЕТОДЫ ====================
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Создаёт HTML разметку интерфейса
|
|
276
|
+
* @private
|
|
277
|
+
*/
|
|
278
|
+
_createInterface() {
|
|
279
|
+
// Основная разметка просмотрщика
|
|
280
|
+
const html = `
|
|
281
|
+
<div class="ifc-viewer-container" style="width: 100%; height: 100%; position: relative; display: flex; flex-direction: column;">
|
|
282
|
+
<!-- Верхняя панель управления -->
|
|
283
|
+
<div id="ifcToolbar" class="navbar bg-neutral text-neutral-content shrink-0 px-4" style="${this.options.showToolbar ? '' : 'display: none;'}">
|
|
284
|
+
<div class="navbar-start">
|
|
285
|
+
<span class="text-lg font-semibold">IFC Viewer</span>
|
|
286
|
+
</div>
|
|
287
|
+
<div class="navbar-end flex gap-2">
|
|
288
|
+
<!-- Качество рендеринга -->
|
|
289
|
+
<div class="join">
|
|
290
|
+
<button class="btn btn-sm join-item" id="ifcQualLow">Low</button>
|
|
291
|
+
<button class="btn btn-sm join-item btn-active" id="ifcQualMed">Med</button>
|
|
292
|
+
<button class="btn btn-sm join-item" id="ifcQualHigh">High</button>
|
|
293
|
+
</div>
|
|
294
|
+
|
|
295
|
+
<!-- Стили отображения -->
|
|
296
|
+
<div class="join">
|
|
297
|
+
<button class="btn btn-sm join-item btn-active" id="ifcToggleEdges">Edges</button>
|
|
298
|
+
<button class="btn btn-sm join-item btn-active" id="ifcToggleShading">Flat</button>
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
<!-- Секущие плоскости -->
|
|
302
|
+
<div class="join">
|
|
303
|
+
<button class="btn btn-sm join-item" id="ifcClipX">Clip X</button>
|
|
304
|
+
<button class="btn btn-sm join-item" id="ifcClipY">Clip Y</button>
|
|
305
|
+
<button class="btn btn-sm join-item" id="ifcClipZ">Clip Z</button>
|
|
306
|
+
</div>
|
|
307
|
+
|
|
308
|
+
<!-- Кнопка загрузки файла -->
|
|
309
|
+
<button id="ifcUploadBtnTop" class="btn btn-sm bg-white text-black">📁 Загрузить</button>
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
|
|
313
|
+
<!-- Слайдеры секущих плоскостей (изначально скрыты) -->
|
|
314
|
+
<div id="ifcClipControls" class="bg-base-200 px-4 py-2 border-b border-base-300" style="display: ${this.options.showToolbar ? 'none' : 'none'};">
|
|
315
|
+
<div class="flex items-center gap-4 text-sm">
|
|
316
|
+
<!-- Слайдер X -->
|
|
317
|
+
<div id="ifcClipXControl" class="flex items-center gap-2" style="display: none;">
|
|
318
|
+
<span class="w-12">Clip X:</span>
|
|
319
|
+
<input type="range" id="ifcClipXRange" class="range range-sm flex-1" min="0" max="1" step="0.01" value="0.5">
|
|
320
|
+
</div>
|
|
321
|
+
<!-- Слайдер Y -->
|
|
322
|
+
<div id="ifcClipYControl" class="flex items-center gap-2" style="display: none;">
|
|
323
|
+
<span class="w-12">Clip Y:</span>
|
|
324
|
+
<input type="range" id="ifcClipYRange" class="range range-sm flex-1" min="0" max="1" step="0.01" value="0.5">
|
|
325
|
+
</div>
|
|
326
|
+
<!-- Слайдер Z -->
|
|
327
|
+
<div id="ifcClipZControl" class="flex items-center gap-2" style="display: none;">
|
|
328
|
+
<span class="w-12">Clip Z:</span>
|
|
329
|
+
<input type="range" id="ifcClipZRange" class="range range-sm flex-1" min="0" max="1" step="0.01" value="0.5">
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
|
|
334
|
+
<!-- Прелоадер -->
|
|
335
|
+
<div id="ifcPreloader" class="absolute inset-0 bg-base-100 flex items-center justify-center z-50">
|
|
336
|
+
<div class="text-center">
|
|
337
|
+
<span class="loading loading-spinner loading-lg"></span>
|
|
338
|
+
<div class="mt-2 text-sm opacity-70">Загрузка модели...</div>
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
|
|
342
|
+
<!-- Основной контейнер просмотрщика -->
|
|
343
|
+
<div id="ifcViewerMain" class="w-full flex-1 relative"></div>
|
|
344
|
+
|
|
345
|
+
<!-- Боковая панель -->
|
|
346
|
+
<div id="ifcSidebar" class="absolute left-0 top-0 h-full w-80 bg-base-200 shadow-lg transform -translate-x-full transition-transform duration-300 pointer-events-none z-40">
|
|
347
|
+
<div class="flex flex-col h-full">
|
|
348
|
+
<!-- Заголовок панели -->
|
|
349
|
+
<div class="flex items-center justify-between p-4 border-b border-base-300">
|
|
350
|
+
<h3 class="font-medium">Структура модели</h3>
|
|
351
|
+
<button id="ifcSidebarClose" class="btn btn-ghost btn-sm">✕</button>
|
|
352
|
+
</div>
|
|
353
|
+
|
|
354
|
+
<!-- Информация о модели -->
|
|
355
|
+
<div id="ifcInfo" class="p-4 border-b border-base-300 bg-base-100">
|
|
356
|
+
<div class="text-sm opacity-70">Модель не загружена</div>
|
|
357
|
+
</div>
|
|
358
|
+
|
|
359
|
+
<!-- Дерево структуры -->
|
|
360
|
+
<div class="flex-1 overflow-auto">
|
|
361
|
+
<div id="ifcTree" class="p-2"></div>
|
|
362
|
+
</div>
|
|
363
|
+
|
|
364
|
+
<!-- Переключатель режима изоляции -->
|
|
365
|
+
<div class="p-4 border-t border-base-300">
|
|
366
|
+
<label class="label cursor-pointer">
|
|
367
|
+
<span class="label-text">Режим изоляции</span>
|
|
368
|
+
<input type="checkbox" id="ifcIsolateToggle" class="toggle toggle-primary" />
|
|
369
|
+
</label>
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
373
|
+
|
|
374
|
+
<!-- Панель управления -->
|
|
375
|
+
<div id="ifcControls" class="absolute top-4 left-4 z-30" style="${this.options.showControls ? '' : 'display: none;'}">
|
|
376
|
+
<!-- Кнопка панели (показываем если включен сайдбар) -->
|
|
377
|
+
<button id="ifcSidebarToggle" class="btn btn-primary btn-sm mb-2">☰</button>
|
|
378
|
+
|
|
379
|
+
<!-- Кнопка загрузки -->
|
|
380
|
+
<button id="ifcUploadBtn" class="btn btn-secondary btn-sm">📁</button>
|
|
381
|
+
<input type="file" id="ifcFileInput" accept=".ifc,.ifczip,.zip" style="display: none;">
|
|
382
|
+
</div>
|
|
383
|
+
|
|
384
|
+
<!-- Панель зума (будет создана Viewer'ом) -->
|
|
385
|
+
<div id="ifcZoomPanel" class="absolute bottom-4 right-4 z-30"></div>
|
|
386
|
+
</div>
|
|
387
|
+
`;
|
|
388
|
+
|
|
389
|
+
this.containerElement.innerHTML = html;
|
|
390
|
+
|
|
391
|
+
// Сохраняем ссылки на элементы
|
|
392
|
+
this.elements.viewerContainer = this.containerElement.querySelector('#ifcViewerMain');
|
|
393
|
+
this.elements.sidebar = this.containerElement.querySelector('#ifcSidebar');
|
|
394
|
+
this.elements.controls = this.containerElement.querySelector('#ifcControls');
|
|
395
|
+
this.elements.uploadInput = this.containerElement.querySelector('#ifcFileInput');
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Применяет тему интерфейса
|
|
400
|
+
* @private
|
|
401
|
+
*/
|
|
402
|
+
_applyTheme() {
|
|
403
|
+
const container = this.containerElement.querySelector('.ifc-viewer-container');
|
|
404
|
+
if (container) {
|
|
405
|
+
container.setAttribute('data-theme', this.options.theme);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Инициализирует компонент Viewer
|
|
411
|
+
* @private
|
|
412
|
+
*/
|
|
413
|
+
_initViewer() {
|
|
414
|
+
if (!this.elements.viewerContainer) {
|
|
415
|
+
throw new Error('Контейнер для viewer не найден');
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
this.viewer = new Viewer(this.elements.viewerContainer);
|
|
419
|
+
this.viewer.init();
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Инициализирует сервис IFC
|
|
424
|
+
* @private
|
|
425
|
+
*/
|
|
426
|
+
_initIfcService() {
|
|
427
|
+
if (!this.viewer) {
|
|
428
|
+
throw new Error('Viewer должен быть инициализирован перед IfcService');
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
this.ifcService = new IfcService(this.viewer);
|
|
432
|
+
this.ifcService.init();
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Инициализирует компонент дерева IFC
|
|
437
|
+
* @private
|
|
438
|
+
*/
|
|
439
|
+
_initTreeView() {
|
|
440
|
+
const treeElement = this.containerElement.querySelector('#ifcTree');
|
|
441
|
+
if (treeElement) {
|
|
442
|
+
this.ifcTreeView = new IfcTreeView(treeElement);
|
|
443
|
+
|
|
444
|
+
// Настраиваем обработчик выбора узла
|
|
445
|
+
this.ifcTreeView.onSelect(async (node) => {
|
|
446
|
+
if (this.ifcService) {
|
|
447
|
+
const ids = this.ifcService.collectElementIDsFromStructure(node);
|
|
448
|
+
await this.ifcService.highlightByIds(ids);
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Настраивает обработчики событий
|
|
456
|
+
* @private
|
|
457
|
+
*/
|
|
458
|
+
_setupEventHandlers() {
|
|
459
|
+
// Кнопка переключения сайдбара
|
|
460
|
+
this._addEventListener('#ifcSidebarToggle', 'click', () => {
|
|
461
|
+
this._setSidebarVisible(true);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// Кнопка закрытия сайдбара
|
|
465
|
+
this._addEventListener('#ifcSidebarClose', 'click', () => {
|
|
466
|
+
this._setSidebarVisible(false);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// Загрузка файла
|
|
470
|
+
this._addEventListener('#ifcUploadBtn', 'click', () => {
|
|
471
|
+
this.elements.uploadInput?.click();
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
this._addEventListener('#ifcFileInput', 'change', async (e) => {
|
|
475
|
+
const file = e.target.files?.[0];
|
|
476
|
+
if (file) {
|
|
477
|
+
await this.loadModel(file);
|
|
478
|
+
e.target.value = ''; // Очистка input
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
// Переключатель изоляции
|
|
483
|
+
this._addEventListener('#ifcIsolateToggle', 'change', (e) => {
|
|
484
|
+
if (this.ifcService) {
|
|
485
|
+
this.ifcService.setIsolateMode(e.target.checked);
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
// ==================== ОБРАБОТЧИКИ ВЕРХНЕЙ ПАНЕЛИ ====================
|
|
490
|
+
|
|
491
|
+
// Кнопки качества рендеринга
|
|
492
|
+
this._addEventListener('#ifcQualLow', 'click', () => {
|
|
493
|
+
this._setQuality('low');
|
|
494
|
+
});
|
|
495
|
+
this._addEventListener('#ifcQualMed', 'click', () => {
|
|
496
|
+
this._setQuality('medium');
|
|
497
|
+
});
|
|
498
|
+
this._addEventListener('#ifcQualHigh', 'click', () => {
|
|
499
|
+
this._setQuality('high');
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
// Переключатели отображения
|
|
503
|
+
this._addEventListener('#ifcToggleEdges', 'click', () => {
|
|
504
|
+
this._toggleEdges();
|
|
505
|
+
});
|
|
506
|
+
this._addEventListener('#ifcToggleShading', 'click', () => {
|
|
507
|
+
this._toggleShading();
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
// Секущие плоскости
|
|
511
|
+
this._addEventListener('#ifcClipX', 'click', () => {
|
|
512
|
+
this._toggleClipAxis('x');
|
|
513
|
+
});
|
|
514
|
+
this._addEventListener('#ifcClipY', 'click', () => {
|
|
515
|
+
this._toggleClipAxis('y');
|
|
516
|
+
});
|
|
517
|
+
this._addEventListener('#ifcClipZ', 'click', () => {
|
|
518
|
+
this._toggleClipAxis('z');
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
// Слайдеры позиции секущих плоскостей
|
|
522
|
+
this._addEventListener('#ifcClipXRange', 'input', (e) => {
|
|
523
|
+
if (this.viewer && this.viewerState.clipping.x) {
|
|
524
|
+
const t = parseFloat(e.target.value);
|
|
525
|
+
this.viewer.setSectionNormalized('x', true, t);
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
this._addEventListener('#ifcClipYRange', 'input', (e) => {
|
|
529
|
+
if (this.viewer && this.viewerState.clipping.y) {
|
|
530
|
+
const t = parseFloat(e.target.value);
|
|
531
|
+
this.viewer.setSectionNormalized('y', true, t);
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
this._addEventListener('#ifcClipZRange', 'input', (e) => {
|
|
535
|
+
if (this.viewer && this.viewerState.clipping.z) {
|
|
536
|
+
const t = parseFloat(e.target.value);
|
|
537
|
+
this.viewer.setSectionNormalized('z', true, t);
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
// Дополнительная кнопка загрузки в верхней панели
|
|
542
|
+
this._addEventListener('#ifcUploadBtnTop', 'click', () => {
|
|
543
|
+
this.elements.uploadInput?.click();
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Обновляет дерево структуры модели
|
|
549
|
+
* @param {Object} model - Загруженная модель
|
|
550
|
+
* @private
|
|
551
|
+
*/
|
|
552
|
+
async _updateTreeView(model) {
|
|
553
|
+
if (!this.ifcTreeView || !this.ifcService || !model) return;
|
|
554
|
+
|
|
555
|
+
try {
|
|
556
|
+
const structure = await this.ifcService.getSpatialStructure(model.modelID);
|
|
557
|
+
if (structure) {
|
|
558
|
+
this.ifcTreeView.render(structure);
|
|
559
|
+
}
|
|
560
|
+
} catch (error) {
|
|
561
|
+
console.error('Ошибка обновления дерева структуры:', error);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Обновляет информационную панель
|
|
567
|
+
* @private
|
|
568
|
+
*/
|
|
569
|
+
_updateInfoPanel() {
|
|
570
|
+
const infoElement = this.containerElement.querySelector('#ifcInfo');
|
|
571
|
+
if (!infoElement || !this.ifcService) return;
|
|
572
|
+
|
|
573
|
+
const info = this.ifcService.getLastInfo();
|
|
574
|
+
infoElement.innerHTML = `
|
|
575
|
+
<div class="flex items-center justify-between">
|
|
576
|
+
<div>
|
|
577
|
+
<div class="font-medium text-xs">${info.name || '—'}</div>
|
|
578
|
+
<div class="opacity-70">modelID: ${info.modelID || '—'}</div>
|
|
579
|
+
</div>
|
|
580
|
+
</div>
|
|
581
|
+
`;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Показывает/скрывает боковую панель
|
|
586
|
+
* @param {boolean} visible - Видимость панели
|
|
587
|
+
* @private
|
|
588
|
+
*/
|
|
589
|
+
_setSidebarVisible(visible) {
|
|
590
|
+
if (!this.elements.sidebar) return;
|
|
591
|
+
|
|
592
|
+
if (visible) {
|
|
593
|
+
this.elements.sidebar.classList.remove('-translate-x-full');
|
|
594
|
+
this.elements.sidebar.classList.add('translate-x-0');
|
|
595
|
+
this.elements.sidebar.classList.remove('pointer-events-none');
|
|
596
|
+
} else {
|
|
597
|
+
this.elements.sidebar.classList.add('-translate-x-full');
|
|
598
|
+
this.elements.sidebar.classList.remove('translate-x-0');
|
|
599
|
+
this.elements.sidebar.classList.add('pointer-events-none');
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Показывает прелоадер
|
|
605
|
+
* @private
|
|
606
|
+
*/
|
|
607
|
+
_showPreloader() {
|
|
608
|
+
const preloader = this.containerElement.querySelector('#ifcPreloader');
|
|
609
|
+
if (preloader) {
|
|
610
|
+
preloader.style.opacity = '1';
|
|
611
|
+
preloader.style.visibility = 'visible';
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Скрывает прелоадер
|
|
617
|
+
* @private
|
|
618
|
+
*/
|
|
619
|
+
_hidePreloader() {
|
|
620
|
+
const preloader = this.containerElement.querySelector('#ifcPreloader');
|
|
621
|
+
if (preloader) {
|
|
622
|
+
preloader.style.transition = 'opacity 400ms ease';
|
|
623
|
+
preloader.style.opacity = '0';
|
|
624
|
+
setTimeout(() => {
|
|
625
|
+
preloader.style.visibility = 'hidden';
|
|
626
|
+
}, 400);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Добавляет слушатель события с автоматической очисткой
|
|
632
|
+
* @param {string} selector - Селектор элемента
|
|
633
|
+
* @param {string} event - Тип события
|
|
634
|
+
* @param {Function} handler - Обработчик события
|
|
635
|
+
* @private
|
|
636
|
+
*/
|
|
637
|
+
_addEventListener(selector, event, handler) {
|
|
638
|
+
const element = this.containerElement.querySelector(selector);
|
|
639
|
+
if (element) {
|
|
640
|
+
element.addEventListener(event, handler);
|
|
641
|
+
this.eventListeners.set(`${selector}.${event}`, handler);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Отправляет пользовательское событие
|
|
647
|
+
* @param {string} eventName - Имя события
|
|
648
|
+
* @param {Object} detail - Детали события
|
|
649
|
+
* @private
|
|
650
|
+
*/
|
|
651
|
+
_dispatchEvent(eventName, detail = {}) {
|
|
652
|
+
try {
|
|
653
|
+
const event = new CustomEvent(`ifcviewer:${eventName}`, {
|
|
654
|
+
detail,
|
|
655
|
+
bubbles: true
|
|
656
|
+
});
|
|
657
|
+
this.containerElement.dispatchEvent(event);
|
|
658
|
+
} catch (error) {
|
|
659
|
+
console.error('Ошибка отправки события:', error);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// ==================== МЕТОДЫ УПРАВЛЕНИЯ ВЕРХНЕЙ ПАНЕЛИ ====================
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Устанавливает качество рендеринга
|
|
667
|
+
* @param {string} preset - Качество ('low' | 'medium' | 'high')
|
|
668
|
+
* @private
|
|
669
|
+
*/
|
|
670
|
+
_setQuality(preset) {
|
|
671
|
+
if (!this.viewer) return;
|
|
672
|
+
|
|
673
|
+
this.viewerState.quality = preset;
|
|
674
|
+
this.viewer.setQuality(preset);
|
|
675
|
+
|
|
676
|
+
// Обновляем активное состояние кнопок
|
|
677
|
+
const buttons = ['#ifcQualLow', '#ifcQualMed', '#ifcQualHigh'];
|
|
678
|
+
const activeButton = preset === 'low' ? '#ifcQualLow' :
|
|
679
|
+
preset === 'high' ? '#ifcQualHigh' : '#ifcQualMed';
|
|
680
|
+
|
|
681
|
+
buttons.forEach(selector => {
|
|
682
|
+
const btn = this.containerElement.querySelector(selector);
|
|
683
|
+
if (btn) {
|
|
684
|
+
btn.classList.toggle('btn-active', selector === activeButton);
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Переключает отображение граней
|
|
691
|
+
* @private
|
|
692
|
+
*/
|
|
693
|
+
_toggleEdges() {
|
|
694
|
+
if (!this.viewer) return;
|
|
695
|
+
|
|
696
|
+
this.viewerState.edgesVisible = !this.viewerState.edgesVisible;
|
|
697
|
+
this.viewer.setEdgesVisible(this.viewerState.edgesVisible);
|
|
698
|
+
|
|
699
|
+
// Обновляем состояние кнопки
|
|
700
|
+
const btn = this.containerElement.querySelector('#ifcToggleEdges');
|
|
701
|
+
if (btn) {
|
|
702
|
+
btn.classList.toggle('btn-active', this.viewerState.edgesVisible);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Переключает плоское затенение
|
|
708
|
+
* @private
|
|
709
|
+
*/
|
|
710
|
+
_toggleShading() {
|
|
711
|
+
if (!this.viewer) return;
|
|
712
|
+
|
|
713
|
+
this.viewerState.flatShading = !this.viewerState.flatShading;
|
|
714
|
+
this.viewer.setFlatShading(this.viewerState.flatShading);
|
|
715
|
+
|
|
716
|
+
// Обновляем состояние кнопки
|
|
717
|
+
const btn = this.containerElement.querySelector('#ifcToggleShading');
|
|
718
|
+
if (btn) {
|
|
719
|
+
btn.classList.toggle('btn-active', this.viewerState.flatShading);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Переключает секущую плоскость по оси
|
|
725
|
+
* @param {string} axis - Ось ('x' | 'y' | 'z')
|
|
726
|
+
* @private
|
|
727
|
+
*/
|
|
728
|
+
_toggleClipAxis(axis) {
|
|
729
|
+
if (!this.viewer) return;
|
|
730
|
+
|
|
731
|
+
const clipping = this.viewerState.clipping;
|
|
732
|
+
const currentState = clipping[axis];
|
|
733
|
+
const newState = !currentState;
|
|
734
|
+
|
|
735
|
+
// Если включаем новую ось, отключаем предыдущую активную
|
|
736
|
+
if (newState && clipping.active && clipping.active !== axis) {
|
|
737
|
+
const prevAxis = clipping.active;
|
|
738
|
+
clipping[prevAxis] = false;
|
|
739
|
+
this.viewer.setSection(prevAxis, false, 0);
|
|
740
|
+
|
|
741
|
+
// Обновляем кнопку предыдущей оси
|
|
742
|
+
const prevBtn = this.containerElement.querySelector(`#ifcClip${prevAxis.toUpperCase()}`);
|
|
743
|
+
if (prevBtn) prevBtn.classList.remove('btn-active');
|
|
744
|
+
|
|
745
|
+
// Скрываем слайдер предыдущей оси
|
|
746
|
+
const prevControl = this.containerElement.querySelector(`#ifcClip${prevAxis.toUpperCase()}Control`);
|
|
747
|
+
if (prevControl) prevControl.style.display = 'none';
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Устанавливаем новое состояние
|
|
751
|
+
clipping[axis] = newState;
|
|
752
|
+
clipping.active = newState ? axis : null;
|
|
753
|
+
this.viewer.setSection(axis, newState, 0);
|
|
754
|
+
|
|
755
|
+
// Обновляем состояние кнопки
|
|
756
|
+
const btn = this.containerElement.querySelector(`#ifcClip${axis.toUpperCase()}`);
|
|
757
|
+
if (btn) {
|
|
758
|
+
btn.classList.toggle('btn-active', newState);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// Показываем/скрываем панель слайдеров
|
|
762
|
+
this._updateClipControls();
|
|
763
|
+
|
|
764
|
+
// Показываем/скрываем слайдер текущей оси
|
|
765
|
+
const control = this.containerElement.querySelector(`#ifcClip${axis.toUpperCase()}Control`);
|
|
766
|
+
if (control) {
|
|
767
|
+
control.style.display = newState ? 'flex' : 'none';
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Обновляет видимость панели управления секущими плоскостями
|
|
773
|
+
* @private
|
|
774
|
+
*/
|
|
775
|
+
_updateClipControls() {
|
|
776
|
+
const panel = this.containerElement.querySelector('#ifcClipControls');
|
|
777
|
+
if (!panel) return;
|
|
778
|
+
|
|
779
|
+
const hasActiveClipping = this.viewerState.clipping.active !== null;
|
|
780
|
+
panel.style.display = hasActiveClipping ? 'block' : 'none';
|
|
781
|
+
}
|
|
782
|
+
}
|