@sequent-org/ifc-viewer 1.2.4-ci.46.0 → 1.2.4-ci.48.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 CHANGED
@@ -341,7 +341,7 @@ import '@sequent-org/ifc-viewer/style.css'
341
341
  - Одновременно активна только одна плоскость
342
342
 
343
343
  ### Загрузка файлов
344
- - **📁 Загрузить** - Кнопка выбора IFC файла пользователем
344
+ - **📁 Загрузить** - Кнопка выбора файла модели пользователем
345
345
 
346
346
  ## 🧪 Тестирование
347
347
 
@@ -359,8 +359,18 @@ npm run test:manual
359
359
  ## 📦 Поддерживаемые форматы
360
360
 
361
361
  - `.ifc` - стандартные IFC файлы
362
+ - `.ifs` - вариант IFC (если используется в вашем пайплайне)
362
363
  - `.ifczip` - архивы IFC
363
364
  - `.zip` - ZIP архивы с IFC файлами
365
+ - `.fbx` - FBX модели (базовая поддержка)
366
+ - `.glb` - glTF Binary (рекомендуется для загрузки из файла)
367
+ - `.gltf` - glTF JSON (часто требует внешние .bin/текстуры; надёжнее грузить по URL)
368
+ - `.obj` - Wavefront OBJ (можно загружать один OBJ или OBJ+MTL(+текстуры) через мультивыбор файлов)
369
+ - `.3ds` - 3D Studio (3DS) (рекомендуется загружать .3ds + текстуры через мультивыбор файлов)
370
+ - `.stl` - STL (ASCII/Binary). Обычно без материалов/цветов — отображается с дефолтным материалом.
371
+ - `.dae` - COLLADA (DAE) (можно загружать .dae + текстуры через мультивыбор файлов)
372
+
373
+ Добавление новых форматов делается через реестр загрузчиков (`ModelLoaderRegistry`): регистрируете новый `XxxModelLoader`, и UI/точки входа загрузки остаются без изменений.
364
374
 
365
375
  ## 🔧 Troubleshooting
366
376
 
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.46.0",
4
+ "version": "1.2.4-ci.48.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
@@ -13,6 +13,14 @@
13
13
  import { Viewer } from "./viewer/Viewer.js";
14
14
  import { IfcService } from "./ifc/IfcService.js";
15
15
  import { IfcTreeView } from "./ifc/IfcTreeView.js";
16
+ import { ModelLoaderRegistry } from "./model-loading/ModelLoaderRegistry.js";
17
+ import { IfcModelLoader } from "./model-loading/loaders/IfcModelLoader.js";
18
+ import { FbxModelLoader } from "./model-loading/loaders/FbxModelLoader.js";
19
+ import { GltfModelLoader } from "./model-loading/loaders/GltfModelLoader.js";
20
+ import { ObjModelLoader } from "./model-loading/loaders/ObjModelLoader.js";
21
+ import { TdsModelLoader } from "./model-loading/loaders/TdsModelLoader.js";
22
+ import { StlModelLoader } from "./model-loading/loaders/StlModelLoader.js";
23
+ import { DaeModelLoader } from "./model-loading/loaders/DaeModelLoader.js";
16
24
  import './style.css';
17
25
 
18
26
 
@@ -23,12 +31,14 @@ export class IfcViewer {
23
31
  * @param {HTMLElement|string} options.container - Контейнер для рендера (элемент или селектор)
24
32
  * @param {string} [options.ifcUrl] - URL для загрузки IFC файла
25
33
  * @param {File} [options.ifcFile] - File объект для загрузки IFC файла
34
+ * @param {string} [options.modelUrl] - URL для загрузки модели (любой поддерживаемый формат)
35
+ * @param {File} [options.modelFile] - File объект модели (любой поддерживаемый формат)
26
36
  * @param {string} [options.wasmUrl] - URL для загрузки WASM файла web-ifc
27
37
  * @param {boolean} [options.useTestPreset=true] - Включать ли пресет "Тест" по умолчанию (рекомендованные тени/визуал)
28
38
  * @param {boolean} [options.showSidebar=false] - Показывать ли боковую панель с деревом
29
39
  * @param {boolean} [options.showControls=false] - Показывать ли панель управления (нижние кнопки)
30
40
  * @param {boolean} [options.showToolbar=true] - Показывать ли верхнюю панель инструментов
31
- * @param {boolean} [options.autoLoad=true] - Автоматически загружать IFC файл при инициализации
41
+ * @param {boolean} [options.autoLoad=true] - Автоматически загружать модель при инициализации (modelUrl/modelFile/ifcUrl/ifcFile)
32
42
  * @param {string} [options.theme='light'] - Тема интерфейса ('light' | 'dark')
33
43
  * @param {Object} [options.viewerOptions] - Дополнительные опции для Viewer
34
44
  */
@@ -51,6 +61,9 @@ export class IfcViewer {
51
61
  this.options = {
52
62
  ifcUrl: options.ifcUrl || null,
53
63
  ifcFile: options.ifcFile || null,
64
+ // Универсальные поля для будущих форматов (не ломают обратную совместимость)
65
+ modelUrl: options.modelUrl || null,
66
+ modelFile: options.modelFile || null,
54
67
  wasmUrl: options.wasmUrl || null,
55
68
  // По умолчанию включаем пресет "Тест" для корректного вида теней (как в демо-настройках)
56
69
  useTestPreset: options.useTestPreset !== false,
@@ -66,8 +79,12 @@ export class IfcViewer {
66
79
  this.viewer = null;
67
80
  this.ifcService = null;
68
81
  this.ifcTreeView = null;
82
+ /** @type {ModelLoaderRegistry|null} */
83
+ this.modelLoaders = null;
69
84
  this.isInitialized = false;
70
85
  this.currentModel = null;
86
+ this.currentLoadResult = null;
87
+ this.currentCapabilities = null;
71
88
 
72
89
  // DOM элементы интерфейса
73
90
  this.elements = {
@@ -115,6 +132,7 @@ export class IfcViewer {
115
132
  // Инициализируем компоненты
116
133
  this._initViewer();
117
134
  this._initIfcService();
135
+ this._initModelLoaders();
118
136
  this._initTreeView();
119
137
 
120
138
  // Применяем дефолтный пресет пакета (полностью независим от index.html)
@@ -135,8 +153,14 @@ export class IfcViewer {
135
153
  // Настраиваем обработчики событий
136
154
  this._setupEventHandlers();
137
155
 
138
- // Автозагрузка файла если указан
139
- if (this.options.autoLoad && (this.options.ifcUrl || this.options.ifcFile)) {
156
+ // Автозагрузка модели (в режиме пакета) если указан источник
157
+ if (
158
+ this.options.autoLoad &&
159
+ (this.options.modelUrl ||
160
+ this.options.modelFile ||
161
+ this.options.ifcUrl ||
162
+ this.options.ifcFile)
163
+ ) {
140
164
  await this.loadModel();
141
165
  }
142
166
 
@@ -157,16 +181,22 @@ export class IfcViewer {
157
181
  * @returns {Promise<Object|null>} - Загруженная модель или null при ошибке
158
182
  */
159
183
  async loadModel(source) {
160
- if (!this.ifcService) {
184
+ if (!this.viewer) {
161
185
  throw new Error('IfcViewer: не инициализирован. Вызовите init() сначала');
162
186
  }
187
+ if (!this.modelLoaders) this._initModelLoaders();
163
188
 
164
189
  try {
165
- let model = null;
166
- const loadSource = source || this.options.ifcUrl || this.options.ifcFile;
190
+ let result = null;
191
+ const loadSource =
192
+ source ||
193
+ this.options.modelUrl ||
194
+ this.options.modelFile ||
195
+ this.options.ifcUrl ||
196
+ this.options.ifcFile;
167
197
 
168
198
  if (!loadSource) {
169
- throw new Error('Не указан источник IFC модели');
199
+ throw new Error('Не указан источник модели');
170
200
  }
171
201
 
172
202
  // Показываем прелоадер если есть
@@ -174,18 +204,42 @@ export class IfcViewer {
174
204
 
175
205
  // Загружаем модель в зависимости от типа источника
176
206
  if (typeof loadSource === 'string') {
177
- model = await this.ifcService.loadUrl(loadSource);
207
+ result = await this.modelLoaders.loadUrl(loadSource, {
208
+ viewer: this.viewer,
209
+ wasmUrl: this.options.wasmUrl,
210
+ logger: console,
211
+ });
212
+ } else if (Array.isArray(loadSource) || (typeof FileList !== 'undefined' && loadSource instanceof FileList)) {
213
+ const files = Array.from(loadSource).filter(Boolean);
214
+ result = (files.length > 1)
215
+ ? await this.modelLoaders.loadFiles(files, {
216
+ viewer: this.viewer,
217
+ wasmUrl: this.options.wasmUrl,
218
+ logger: console,
219
+ })
220
+ : await this.modelLoaders.loadFile(files[0], {
221
+ viewer: this.viewer,
222
+ wasmUrl: this.options.wasmUrl,
223
+ logger: console,
224
+ });
178
225
  } else if (loadSource instanceof File) {
179
- model = await this.ifcService.loadFile(loadSource);
226
+ result = await this.modelLoaders.loadFile(loadSource, {
227
+ viewer: this.viewer,
228
+ wasmUrl: this.options.wasmUrl,
229
+ logger: console,
230
+ });
180
231
  } else {
181
232
  throw new Error('Неподдерживаемый тип источника модели');
182
233
  }
183
234
 
184
- if (model) {
185
- this.currentModel = model;
235
+ if (result?.object3D) {
236
+ this.currentLoadResult = result;
237
+ this.currentCapabilities = result.capabilities || null;
238
+ this.currentModel = result.object3D;
239
+ this._syncIfcOnlyControls();
186
240
 
187
241
  // Обновляем дерево структуры
188
- await this._updateTreeView(model);
242
+ await this._updateTreeView(result.object3D);
189
243
 
190
244
  // Обновляем информационную панель
191
245
  this._updateInfoPanel();
@@ -196,11 +250,11 @@ export class IfcViewer {
196
250
  }
197
251
 
198
252
  // Диспетчируем событие загрузки модели
199
- this._dispatchEvent('model-loaded', { model, viewer: this });
253
+ this._dispatchEvent('model-loaded', { model: result.object3D, result, viewer: this });
200
254
  }
201
255
 
202
256
  this._hidePreloader();
203
- return model;
257
+ return result?.object3D || null;
204
258
 
205
259
  } catch (error) {
206
260
  console.error('IfcViewer: ошибка загрузки модели', error);
@@ -210,6 +264,21 @@ export class IfcViewer {
210
264
  }
211
265
  }
212
266
 
267
+ /**
268
+ * Включает/выключает IFC-специфичные контролы (изоляция/дерево).
269
+ * @private
270
+ */
271
+ _syncIfcOnlyControls() {
272
+ const isIfc = this.currentCapabilities?.kind === 'ifc' && !!this.currentCapabilities?.ifcService;
273
+ try {
274
+ const isolateToggle = this.containerElement.querySelector('#ifcIsolateToggle');
275
+ if (isolateToggle) {
276
+ isolateToggle.disabled = !isIfc;
277
+ if (!isIfc) isolateToggle.checked = false;
278
+ }
279
+ } catch (_) {}
280
+ }
281
+
213
282
  /**
214
283
  * Освобождает ресурсы и очищает интерфейс
215
284
  */
@@ -255,8 +324,10 @@ export class IfcViewer {
255
324
  * @returns {Object|null} Информация о модели или null
256
325
  */
257
326
  getModelInfo() {
258
- if (!this.ifcService) return null;
259
- return this.ifcService.getLastInfo();
327
+ const ifcSvc = (this.currentCapabilities?.kind === 'ifc') ? this.currentCapabilities?.ifcService : null;
328
+ if (ifcSvc) return ifcSvc.getLastInfo();
329
+ if (!this.currentLoadResult) return null;
330
+ return { name: this.currentLoadResult.name || '', modelID: '', format: this.currentLoadResult.format || '' };
260
331
  }
261
332
 
262
333
  /**
@@ -314,6 +385,8 @@ export class IfcViewer {
314
385
  <div id="ifcToolbar" class="d-flex px-4" style="border:0px red solid; width: 350px; position: absolute; z-index: 60; justify-content:space-between; bottom: 10px; left: calc(50% - 175px); ">
315
386
 
316
387
  <div class="navbar-end flex gap-2">
388
+ <!-- Загрузка модели -->
389
+ <button class="btn btn-sm" id="ifcUploadBtnTop" title="Загрузить модель">📁</button>
317
390
 
318
391
  <!-- Стили отображения -->
319
392
  <div class="join">
@@ -391,6 +464,9 @@ export class IfcViewer {
391
464
 
392
465
  <!-- Панель зума (будет создана Viewer'ом) -->
393
466
  <div id="ifcZoomPanel" class="absolute bottom-4 right-4 z-30"></div>
467
+
468
+ <!-- File input (скрыт): accept выставляется реестром загрузчиков -->
469
+ <input id="ifcFileInput" type="file" class="hidden" />
394
470
  </div>
395
471
  `;
396
472
 
@@ -440,6 +516,33 @@ export class IfcViewer {
440
516
  this.ifcService.init();
441
517
  }
442
518
 
519
+ /**
520
+ * Инициализирует реестр загрузчиков форматов (IFC/FBX/...)
521
+ * Добавляйте новые форматы через this.modelLoaders.register(new XxxModelLoader()).
522
+ * @private
523
+ */
524
+ _initModelLoaders() {
525
+ // Если по какой-то причине init вызывается повторно — не пересоздаём
526
+ if (this.modelLoaders) return;
527
+ this.modelLoaders = new ModelLoaderRegistry()
528
+ .register(new IfcModelLoader(this.ifcService))
529
+ .register(new FbxModelLoader())
530
+ .register(new GltfModelLoader())
531
+ .register(new ObjModelLoader())
532
+ .register(new TdsModelLoader())
533
+ .register(new StlModelLoader())
534
+ .register(new DaeModelLoader());
535
+
536
+ // Если в интерфейсе есть file input — настроим accept
537
+ try {
538
+ const input = this.containerElement.querySelector('#ifcFileInput');
539
+ if (input) {
540
+ input.accept = this.modelLoaders.getAcceptString();
541
+ input.multiple = true;
542
+ }
543
+ } catch (_) {}
544
+ }
545
+
443
546
  /**
444
547
  * Инициализирует компонент дерева IFC
445
548
  * @private
@@ -451,10 +554,10 @@ export class IfcViewer {
451
554
 
452
555
  // Настраиваем обработчик выбора узла
453
556
  this.ifcTreeView.onSelect(async (node) => {
454
- if (this.ifcService) {
455
- const ids = this.ifcService.collectElementIDsFromStructure(node);
456
- await this.ifcService.highlightByIds(ids);
457
- }
557
+ const ifcSvc = (this.currentCapabilities?.kind === 'ifc') ? this.currentCapabilities?.ifcService : null;
558
+ if (!ifcSvc) return;
559
+ const ids = ifcSvc.collectElementIDsFromStructure(node);
560
+ await ifcSvc.highlightByIds(ids);
458
561
  });
459
562
  }
460
563
  }
@@ -480,17 +583,20 @@ export class IfcViewer {
480
583
  });
481
584
 
482
585
  this._addEventListener('#ifcFileInput', 'change', async (e) => {
483
- const file = e.target.files?.[0];
484
- if (file) {
485
- await this.loadModel(file);
586
+ const files = e.target.files;
587
+ if (files && files.length) {
588
+ await this.loadModel(files); // FileList (multi-file supported)
486
589
  e.target.value = ''; // Очистка input
487
590
  }
488
591
  });
489
592
 
490
593
  // Переключатель изоляции
491
594
  this._addEventListener('#ifcIsolateToggle', 'change', (e) => {
492
- if (this.ifcService) {
493
- this.ifcService.setIsolateMode(e.target.checked);
595
+ const ifcSvc = (this.currentCapabilities?.kind === 'ifc') ? this.currentCapabilities?.ifcService : null;
596
+ if (ifcSvc) {
597
+ ifcSvc.setIsolateMode(e.target.checked);
598
+ } else {
599
+ e.target.checked = false;
494
600
  }
495
601
  });
496
602
 
@@ -564,10 +670,16 @@ export class IfcViewer {
564
670
  * @private
565
671
  */
566
672
  async _updateTreeView(model) {
567
- if (!this.ifcTreeView || !this.ifcService || !model) return;
673
+ if (!this.ifcTreeView) return;
674
+ const ifcSvc = (this.currentCapabilities?.kind === 'ifc') ? this.currentCapabilities?.ifcService : null;
675
+ if (!ifcSvc || !model) {
676
+ // Не-IFC: дерево структуры недоступно
677
+ try { this.ifcTreeView.render(null); } catch (_) {}
678
+ return;
679
+ }
568
680
 
569
681
  try {
570
- const structure = await this.ifcService.getSpatialStructure(model.modelID);
682
+ const structure = await ifcSvc.getSpatialStructure(model.modelID);
571
683
  if (structure) {
572
684
  this.ifcTreeView.render(structure);
573
685
  }
@@ -582,14 +694,34 @@ export class IfcViewer {
582
694
  */
583
695
  _updateInfoPanel() {
584
696
  const infoElement = this.containerElement.querySelector('#ifcInfo');
585
- if (!infoElement || !this.ifcService) return;
697
+ if (!infoElement) return;
698
+
699
+ const ifcSvc = (this.currentCapabilities?.kind === 'ifc') ? this.currentCapabilities?.ifcService : null;
700
+ if (ifcSvc) {
701
+ const info = ifcSvc.getLastInfo();
702
+ infoElement.innerHTML = `
703
+ <div class="flex items-center justify-between">
704
+ <div>
705
+ <div class="font-medium text-xs">${info.name || '—'}</div>
706
+ <div class="opacity-70">modelID: ${info.modelID || '—'}</div>
707
+ </div>
708
+ </div>
709
+ `;
710
+ return;
711
+ }
586
712
 
587
- const info = this.ifcService.getLastInfo();
713
+ const name = this.currentLoadResult?.name || '—';
714
+ const format = this.currentLoadResult?.format || '—';
715
+ const missing = Array.isArray(this.currentLoadResult?.capabilities?.missingAssets) ? this.currentLoadResult.capabilities.missingAssets : [];
716
+ const missingHtml = missing.length
717
+ ? `<div class="opacity-70 mt-1">missing: ${missing.slice(0, 10).map((x) => String(x)).join(', ')}${missing.length > 10 ? '…' : ''}</div>`
718
+ : '';
588
719
  infoElement.innerHTML = `
589
720
  <div class="flex items-center justify-between">
590
721
  <div>
591
- <div class="font-medium text-xs">${info.name || '—'}</div>
592
- <div class="opacity-70">modelID: ${info.modelID || '—'}</div>
722
+ <div class="font-medium text-xs">${name}</div>
723
+ <div class="opacity-70">format: ${format}</div>
724
+ ${missingHtml}
593
725
  </div>
594
726
  </div>
595
727
  `;
@@ -289,7 +289,6 @@ export class IfcService {
289
289
  const model = await this._loadModelWithFallback(url);
290
290
  // Показать модель вместо демо-куба
291
291
  if (this.viewer.replaceWithModel) this.viewer.replaceWithModel(model);
292
- if (this.viewer.focusObject) this.viewer.focusObject(model);
293
292
  this.lastModel = model;
294
293
  this.lastFileName = file?.name || null;
295
294
  // Сообщим, что модель загружена
@@ -318,7 +317,6 @@ export class IfcService {
318
317
  if (!model || !model.geometry) throw new Error('IFC model returned without geometry');
319
318
 
320
319
  if (this.viewer.replaceWithModel) this.viewer.replaceWithModel(model);
321
- if (this.viewer.focusObject) this.viewer.focusObject(model);
322
320
  this.lastModel = model;
323
321
 
324
322
  try {
package/src/index.js CHANGED
@@ -12,3 +12,13 @@ export { IfcViewer } from "./IfcViewer.js";
12
12
  export { Viewer } from "./viewer/Viewer.js";
13
13
  export { IfcService } from "./ifc/IfcService.js";
14
14
  export { IfcTreeView } from "./ifc/IfcTreeView.js";
15
+
16
+ // Расширяемая архитектура загрузчиков форматов
17
+ export { ModelLoaderRegistry } from "./model-loading/ModelLoaderRegistry.js";
18
+ export { IfcModelLoader } from "./model-loading/loaders/IfcModelLoader.js";
19
+ export { FbxModelLoader } from "./model-loading/loaders/FbxModelLoader.js";
20
+ export { GltfModelLoader } from "./model-loading/loaders/GltfModelLoader.js";
21
+ export { ObjModelLoader } from "./model-loading/loaders/ObjModelLoader.js";
22
+ export { TdsModelLoader } from "./model-loading/loaders/TdsModelLoader.js";
23
+ export { StlModelLoader } from "./model-loading/loaders/StlModelLoader.js";
24
+ export { DaeModelLoader } from "./model-loading/loaders/DaeModelLoader.js";
package/src/main.js CHANGED
@@ -2,6 +2,14 @@ import "./style.css";
2
2
  import { Viewer } from "./viewer/Viewer.js";
3
3
  import { IfcService } from "./ifc/IfcService.js";
4
4
  import { IfcTreeView } from "./ifc/IfcTreeView.js";
5
+ import { ModelLoaderRegistry } from "./model-loading/ModelLoaderRegistry.js";
6
+ import { IfcModelLoader } from "./model-loading/loaders/IfcModelLoader.js";
7
+ import { FbxModelLoader } from "./model-loading/loaders/FbxModelLoader.js";
8
+ import { GltfModelLoader } from "./model-loading/loaders/GltfModelLoader.js";
9
+ import { ObjModelLoader } from "./model-loading/loaders/ObjModelLoader.js";
10
+ import { TdsModelLoader } from "./model-loading/loaders/TdsModelLoader.js";
11
+ import { StlModelLoader } from "./model-loading/loaders/StlModelLoader.js";
12
+ import { DaeModelLoader } from "./model-loading/loaders/DaeModelLoader.js";
5
13
 
6
14
  // Инициализация three.js Viewer в контейнере #app
7
15
  const app = document.getElementById("app");
@@ -335,30 +343,82 @@ if (app) {
335
343
  const ifcInfoEl = document.getElementById("ifcInfo");
336
344
  const ifcTree = ifcTreeEl ? new IfcTreeView(ifcTreeEl) : null;
337
345
  const ifcIsolateToggle = document.getElementById("ifcIsolateToggle");
346
+ /** @type {any|null} */
347
+ let activeCapabilities = null;
348
+
349
+ // Реестр загрузчиков: добавляйте новые форматы через register(new XxxModelLoader())
350
+ const modelLoaders = new ModelLoaderRegistry()
351
+ .register(new IfcModelLoader(ifc))
352
+ .register(new FbxModelLoader())
353
+ .register(new GltfModelLoader())
354
+ .register(new ObjModelLoader())
355
+ .register(new TdsModelLoader())
356
+ .register(new StlModelLoader())
357
+ .register(new DaeModelLoader());
338
358
 
339
359
  const uploadBtn = document.getElementById("uploadBtn");
340
360
  const ifcInput = document.getElementById("ifcInput");
341
361
  if (uploadBtn && ifcInput) {
362
+ try { ifcInput.accept = modelLoaders.getAcceptString(); } catch (_) {}
363
+ try { ifcInput.multiple = true; } catch (_) {}
342
364
  uploadBtn.addEventListener("click", () => ifcInput.click());
343
365
  ifcInput.addEventListener("change", async (e) => {
344
- const file = e.target.files?.[0];
345
- if (!file) return;
346
- await ifc.loadFile(file);
366
+ const files = Array.from(e.target.files || []).filter(Boolean);
367
+ if (!files.length) return;
368
+ let result = null;
369
+ try {
370
+ // Multi-file: e.g. OBJ+MTL (+textures)
371
+ result = (files.length > 1)
372
+ ? await modelLoaders.loadFiles(files, { viewer, wasmUrl: wasmOverride, logger: console })
373
+ : await modelLoaders.loadFile(files[0], { viewer, wasmUrl: wasmOverride, logger: console });
374
+ activeCapabilities = result?.capabilities || null;
375
+ } catch (err) {
376
+ console.error('Model load error', err);
377
+ activeCapabilities = null;
378
+ }
347
379
  ifcInput.value = "";
348
- // Обновим дерево IFC и инфо
349
- const last = ifc.getLastInfo();
350
- const struct = await ifc.getSpatialStructure(last.modelID ? Number(last.modelID) : undefined);
351
- if (!struct) console.warn('IFC spatial structure not available for modelID', last?.modelID);
352
- if (ifcTree) ifcTree.render(struct);
353
- if (ifcInfoEl) {
354
- const info = ifc.getLastInfo();
355
- ifcInfoEl.innerHTML = `
356
- <div class="flex items-center justify-between">
357
- <div>
358
- <div class="font-medium text-xs">${info.name || '—'}</div>
359
- <div class="opacity-70">modelID: ${info.modelID || '—'}</div>
360
- </div>
361
- </div>`;
380
+
381
+ // Обновим панель: IFC дерево/инфо только для IFC
382
+ if (activeCapabilities?.kind === 'ifc' && activeCapabilities?.ifcService) {
383
+ const ifcSvc = activeCapabilities.ifcService;
384
+ const last = ifcSvc.getLastInfo();
385
+ const struct = await ifcSvc.getSpatialStructure(last.modelID ? Number(last.modelID) : undefined);
386
+ if (!struct) console.warn('IFC spatial structure not available for modelID', last?.modelID);
387
+ if (ifcTree) ifcTree.render(struct);
388
+ if (ifcInfoEl) {
389
+ const info = ifcSvc.getLastInfo();
390
+ ifcInfoEl.innerHTML = `
391
+ <div class="flex items-center justify-between">
392
+ <div>
393
+ <div class="font-medium text-xs">${info.name || '—'}</div>
394
+ <div class="opacity-70">modelID: ${info.modelID || '—'}</div>
395
+ </div>
396
+ </div>`;
397
+ }
398
+ if (ifcIsolateToggle) ifcIsolateToggle.disabled = false;
399
+ } else {
400
+ // Не-IFC: очищаем дерево, показываем базовую инфу
401
+ if (ifcTree) ifcTree.render(null);
402
+ if (ifcInfoEl) {
403
+ const name = result?.name || files[0]?.name || '—';
404
+ const format = result?.format || '—';
405
+ const missing = Array.isArray(result?.capabilities?.missingAssets) ? result.capabilities.missingAssets : [];
406
+ const missingHtml = missing.length
407
+ ? `<div class="mt-1 opacity-70">missing: ${missing.slice(0, 10).map((x) => String(x)).join(', ')}${missing.length > 10 ? '…' : ''}</div>`
408
+ : '';
409
+ ifcInfoEl.innerHTML = `
410
+ <div class="flex items-center justify-between">
411
+ <div>
412
+ <div class="font-medium text-xs">${name}</div>
413
+ <div class="opacity-70">format: ${format}</div>
414
+ ${missingHtml}
415
+ </div>
416
+ </div>`;
417
+ }
418
+ if (ifcIsolateToggle) {
419
+ ifcIsolateToggle.checked = false;
420
+ ifcIsolateToggle.disabled = true;
421
+ }
362
422
  }
363
423
  // Авто-открытие панели при ручной загрузке
364
424
  setSidebarVisible(true);
@@ -522,14 +582,21 @@ if (app) {
522
582
  // Переключатель изоляции
523
583
  ifcIsolateToggle?.addEventListener("change", (e) => {
524
584
  const enabled = e.target.checked;
525
- ifc.setIsolateMode(enabled);
585
+ if (activeCapabilities?.kind === 'ifc' && activeCapabilities?.ifcService) {
586
+ activeCapabilities.ifcService.setIsolateMode(enabled);
587
+ } else {
588
+ // для не-IFC изоляция недоступна
589
+ e.target.checked = false;
590
+ }
526
591
  });
527
592
 
528
593
  // Выбор узла в дереве → подсветка/изоляция
529
594
  if (ifcTree) {
530
595
  ifcTree.onSelect(async (node) => {
531
- const ids = ifc.collectElementIDsFromStructure(node);
532
- await ifc.highlightByIds(ids);
596
+ if (activeCapabilities?.kind !== 'ifc' || !activeCapabilities?.ifcService) return;
597
+ const ifcSvc = activeCapabilities.ifcService;
598
+ const ids = ifcSvc.collectElementIDsFromStructure(node);
599
+ await ifcSvc.highlightByIds(ids);
533
600
  });
534
601
  }
535
602
 
@@ -544,19 +611,41 @@ if (app) {
544
611
  const params = new URLSearchParams(location.search);
545
612
  const ifcUrlParam = params.get('ifc');
546
613
  const ifcUrl = ifcUrlParam || DEFAULT_IFC_URL;
547
- const model = await ifc.loadUrl(encodeURI(ifcUrl));
548
- if (model) {
549
- const struct = await ifc.getSpatialStructure();
550
- if (ifcTree) ifcTree.render(struct);
551
- if (ifcInfoEl) {
552
- const info = ifc.getLastInfo();
553
- ifcInfoEl.innerHTML = `
554
- <div class="flex items-center justify-between">
555
- <div>
556
- <div class="font-medium text-xs">${info.name || '—'}</div>
557
- <div class="opacity-70">modelID: ${info.modelID || '—'}</div>
558
- </div>
559
- </div>`;
614
+ const result = await modelLoaders.loadUrl(encodeURI(ifcUrl), { viewer, wasmUrl: wasmOverride, logger: console });
615
+ activeCapabilities = result?.capabilities || null;
616
+ if (result?.object3D) {
617
+ if (activeCapabilities?.kind === 'ifc' && activeCapabilities?.ifcService) {
618
+ const ifcSvc = activeCapabilities.ifcService;
619
+ const struct = await ifcSvc.getSpatialStructure();
620
+ if (ifcTree) ifcTree.render(struct);
621
+ if (ifcInfoEl) {
622
+ const info = ifcSvc.getLastInfo();
623
+ ifcInfoEl.innerHTML = `
624
+ <div class="flex items-center justify-between">
625
+ <div>
626
+ <div class="font-medium text-xs">${info.name || '—'}</div>
627
+ <div class="opacity-70">modelID: ${info.modelID || '—'}</div>
628
+ </div>
629
+ </div>`;
630
+ }
631
+ if (ifcIsolateToggle) ifcIsolateToggle.disabled = false;
632
+ } else {
633
+ if (ifcTree) ifcTree.render(null);
634
+ if (ifcInfoEl) {
635
+ const name = result?.name || '—';
636
+ const format = result?.format || '—';
637
+ ifcInfoEl.innerHTML = `
638
+ <div class="flex items-center justify-between">
639
+ <div>
640
+ <div class="font-medium text-xs">${name}</div>
641
+ <div class="opacity-70">format: ${format}</div>
642
+ </div>
643
+ </div>`;
644
+ }
645
+ if (ifcIsolateToggle) {
646
+ ifcIsolateToggle.checked = false;
647
+ ifcIsolateToggle.disabled = true;
648
+ }
560
649
  }
561
650
  // Не открываем панель автоматически при автозагрузке
562
651
  hidePreloader();