@sequent-org/ifc-viewer 1.2.4-ci.45.0 → 1.2.4-ci.47.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.45.0",
4
+ "version": "1.2.4-ci.47.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,6 +31,8 @@ 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] - Показывать ли боковую панель с деревом
@@ -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)
@@ -157,16 +175,22 @@ export class IfcViewer {
157
175
  * @returns {Promise<Object|null>} - Загруженная модель или null при ошибке
158
176
  */
159
177
  async loadModel(source) {
160
- if (!this.ifcService) {
178
+ if (!this.viewer) {
161
179
  throw new Error('IfcViewer: не инициализирован. Вызовите init() сначала');
162
180
  }
181
+ if (!this.modelLoaders) this._initModelLoaders();
163
182
 
164
183
  try {
165
- let model = null;
166
- const loadSource = source || this.options.ifcUrl || this.options.ifcFile;
184
+ let result = null;
185
+ const loadSource =
186
+ source ||
187
+ this.options.modelUrl ||
188
+ this.options.modelFile ||
189
+ this.options.ifcUrl ||
190
+ this.options.ifcFile;
167
191
 
168
192
  if (!loadSource) {
169
- throw new Error('Не указан источник IFC модели');
193
+ throw new Error('Не указан источник модели');
170
194
  }
171
195
 
172
196
  // Показываем прелоадер если есть
@@ -174,18 +198,42 @@ export class IfcViewer {
174
198
 
175
199
  // Загружаем модель в зависимости от типа источника
176
200
  if (typeof loadSource === 'string') {
177
- model = await this.ifcService.loadUrl(loadSource);
201
+ result = await this.modelLoaders.loadUrl(loadSource, {
202
+ viewer: this.viewer,
203
+ wasmUrl: this.options.wasmUrl,
204
+ logger: console,
205
+ });
206
+ } else if (Array.isArray(loadSource) || (typeof FileList !== 'undefined' && loadSource instanceof FileList)) {
207
+ const files = Array.from(loadSource).filter(Boolean);
208
+ result = (files.length > 1)
209
+ ? await this.modelLoaders.loadFiles(files, {
210
+ viewer: this.viewer,
211
+ wasmUrl: this.options.wasmUrl,
212
+ logger: console,
213
+ })
214
+ : await this.modelLoaders.loadFile(files[0], {
215
+ viewer: this.viewer,
216
+ wasmUrl: this.options.wasmUrl,
217
+ logger: console,
218
+ });
178
219
  } else if (loadSource instanceof File) {
179
- model = await this.ifcService.loadFile(loadSource);
220
+ result = await this.modelLoaders.loadFile(loadSource, {
221
+ viewer: this.viewer,
222
+ wasmUrl: this.options.wasmUrl,
223
+ logger: console,
224
+ });
180
225
  } else {
181
226
  throw new Error('Неподдерживаемый тип источника модели');
182
227
  }
183
228
 
184
- if (model) {
185
- this.currentModel = model;
229
+ if (result?.object3D) {
230
+ this.currentLoadResult = result;
231
+ this.currentCapabilities = result.capabilities || null;
232
+ this.currentModel = result.object3D;
233
+ this._syncIfcOnlyControls();
186
234
 
187
235
  // Обновляем дерево структуры
188
- await this._updateTreeView(model);
236
+ await this._updateTreeView(result.object3D);
189
237
 
190
238
  // Обновляем информационную панель
191
239
  this._updateInfoPanel();
@@ -196,11 +244,11 @@ export class IfcViewer {
196
244
  }
197
245
 
198
246
  // Диспетчируем событие загрузки модели
199
- this._dispatchEvent('model-loaded', { model, viewer: this });
247
+ this._dispatchEvent('model-loaded', { model: result.object3D, result, viewer: this });
200
248
  }
201
249
 
202
250
  this._hidePreloader();
203
- return model;
251
+ return result?.object3D || null;
204
252
 
205
253
  } catch (error) {
206
254
  console.error('IfcViewer: ошибка загрузки модели', error);
@@ -210,6 +258,21 @@ export class IfcViewer {
210
258
  }
211
259
  }
212
260
 
261
+ /**
262
+ * Включает/выключает IFC-специфичные контролы (изоляция/дерево).
263
+ * @private
264
+ */
265
+ _syncIfcOnlyControls() {
266
+ const isIfc = this.currentCapabilities?.kind === 'ifc' && !!this.currentCapabilities?.ifcService;
267
+ try {
268
+ const isolateToggle = this.containerElement.querySelector('#ifcIsolateToggle');
269
+ if (isolateToggle) {
270
+ isolateToggle.disabled = !isIfc;
271
+ if (!isIfc) isolateToggle.checked = false;
272
+ }
273
+ } catch (_) {}
274
+ }
275
+
213
276
  /**
214
277
  * Освобождает ресурсы и очищает интерфейс
215
278
  */
@@ -255,8 +318,10 @@ export class IfcViewer {
255
318
  * @returns {Object|null} Информация о модели или null
256
319
  */
257
320
  getModelInfo() {
258
- if (!this.ifcService) return null;
259
- return this.ifcService.getLastInfo();
321
+ const ifcSvc = (this.currentCapabilities?.kind === 'ifc') ? this.currentCapabilities?.ifcService : null;
322
+ if (ifcSvc) return ifcSvc.getLastInfo();
323
+ if (!this.currentLoadResult) return null;
324
+ return { name: this.currentLoadResult.name || '', modelID: '', format: this.currentLoadResult.format || '' };
260
325
  }
261
326
 
262
327
  /**
@@ -314,6 +379,8 @@ export class IfcViewer {
314
379
  <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
380
 
316
381
  <div class="navbar-end flex gap-2">
382
+ <!-- Загрузка модели -->
383
+ <button class="btn btn-sm" id="ifcUploadBtnTop" title="Загрузить модель">📁</button>
317
384
 
318
385
  <!-- Стили отображения -->
319
386
  <div class="join">
@@ -391,6 +458,9 @@ export class IfcViewer {
391
458
 
392
459
  <!-- Панель зума (будет создана Viewer'ом) -->
393
460
  <div id="ifcZoomPanel" class="absolute bottom-4 right-4 z-30"></div>
461
+
462
+ <!-- File input (скрыт): accept выставляется реестром загрузчиков -->
463
+ <input id="ifcFileInput" type="file" class="hidden" />
394
464
  </div>
395
465
  `;
396
466
 
@@ -440,6 +510,33 @@ export class IfcViewer {
440
510
  this.ifcService.init();
441
511
  }
442
512
 
513
+ /**
514
+ * Инициализирует реестр загрузчиков форматов (IFC/FBX/...)
515
+ * Добавляйте новые форматы через this.modelLoaders.register(new XxxModelLoader()).
516
+ * @private
517
+ */
518
+ _initModelLoaders() {
519
+ // Если по какой-то причине init вызывается повторно — не пересоздаём
520
+ if (this.modelLoaders) return;
521
+ this.modelLoaders = new ModelLoaderRegistry()
522
+ .register(new IfcModelLoader(this.ifcService))
523
+ .register(new FbxModelLoader())
524
+ .register(new GltfModelLoader())
525
+ .register(new ObjModelLoader())
526
+ .register(new TdsModelLoader())
527
+ .register(new StlModelLoader())
528
+ .register(new DaeModelLoader());
529
+
530
+ // Если в интерфейсе есть file input — настроим accept
531
+ try {
532
+ const input = this.containerElement.querySelector('#ifcFileInput');
533
+ if (input) {
534
+ input.accept = this.modelLoaders.getAcceptString();
535
+ input.multiple = true;
536
+ }
537
+ } catch (_) {}
538
+ }
539
+
443
540
  /**
444
541
  * Инициализирует компонент дерева IFC
445
542
  * @private
@@ -451,10 +548,10 @@ export class IfcViewer {
451
548
 
452
549
  // Настраиваем обработчик выбора узла
453
550
  this.ifcTreeView.onSelect(async (node) => {
454
- if (this.ifcService) {
455
- const ids = this.ifcService.collectElementIDsFromStructure(node);
456
- await this.ifcService.highlightByIds(ids);
457
- }
551
+ const ifcSvc = (this.currentCapabilities?.kind === 'ifc') ? this.currentCapabilities?.ifcService : null;
552
+ if (!ifcSvc) return;
553
+ const ids = ifcSvc.collectElementIDsFromStructure(node);
554
+ await ifcSvc.highlightByIds(ids);
458
555
  });
459
556
  }
460
557
  }
@@ -480,17 +577,20 @@ export class IfcViewer {
480
577
  });
481
578
 
482
579
  this._addEventListener('#ifcFileInput', 'change', async (e) => {
483
- const file = e.target.files?.[0];
484
- if (file) {
485
- await this.loadModel(file);
580
+ const files = e.target.files;
581
+ if (files && files.length) {
582
+ await this.loadModel(files); // FileList (multi-file supported)
486
583
  e.target.value = ''; // Очистка input
487
584
  }
488
585
  });
489
586
 
490
587
  // Переключатель изоляции
491
588
  this._addEventListener('#ifcIsolateToggle', 'change', (e) => {
492
- if (this.ifcService) {
493
- this.ifcService.setIsolateMode(e.target.checked);
589
+ const ifcSvc = (this.currentCapabilities?.kind === 'ifc') ? this.currentCapabilities?.ifcService : null;
590
+ if (ifcSvc) {
591
+ ifcSvc.setIsolateMode(e.target.checked);
592
+ } else {
593
+ e.target.checked = false;
494
594
  }
495
595
  });
496
596
 
@@ -564,10 +664,16 @@ export class IfcViewer {
564
664
  * @private
565
665
  */
566
666
  async _updateTreeView(model) {
567
- if (!this.ifcTreeView || !this.ifcService || !model) return;
667
+ if (!this.ifcTreeView) return;
668
+ const ifcSvc = (this.currentCapabilities?.kind === 'ifc') ? this.currentCapabilities?.ifcService : null;
669
+ if (!ifcSvc || !model) {
670
+ // Не-IFC: дерево структуры недоступно
671
+ try { this.ifcTreeView.render(null); } catch (_) {}
672
+ return;
673
+ }
568
674
 
569
675
  try {
570
- const structure = await this.ifcService.getSpatialStructure(model.modelID);
676
+ const structure = await ifcSvc.getSpatialStructure(model.modelID);
571
677
  if (structure) {
572
678
  this.ifcTreeView.render(structure);
573
679
  }
@@ -582,14 +688,34 @@ export class IfcViewer {
582
688
  */
583
689
  _updateInfoPanel() {
584
690
  const infoElement = this.containerElement.querySelector('#ifcInfo');
585
- if (!infoElement || !this.ifcService) return;
691
+ if (!infoElement) return;
692
+
693
+ const ifcSvc = (this.currentCapabilities?.kind === 'ifc') ? this.currentCapabilities?.ifcService : null;
694
+ if (ifcSvc) {
695
+ const info = ifcSvc.getLastInfo();
696
+ infoElement.innerHTML = `
697
+ <div class="flex items-center justify-between">
698
+ <div>
699
+ <div class="font-medium text-xs">${info.name || '—'}</div>
700
+ <div class="opacity-70">modelID: ${info.modelID || '—'}</div>
701
+ </div>
702
+ </div>
703
+ `;
704
+ return;
705
+ }
586
706
 
587
- const info = this.ifcService.getLastInfo();
707
+ const name = this.currentLoadResult?.name || '—';
708
+ const format = this.currentLoadResult?.format || '—';
709
+ const missing = Array.isArray(this.currentLoadResult?.capabilities?.missingAssets) ? this.currentLoadResult.capabilities.missingAssets : [];
710
+ const missingHtml = missing.length
711
+ ? `<div class="opacity-70 mt-1">missing: ${missing.slice(0, 10).map((x) => String(x)).join(', ')}${missing.length > 10 ? '…' : ''}</div>`
712
+ : '';
588
713
  infoElement.innerHTML = `
589
714
  <div class="flex items-center justify-between">
590
715
  <div>
591
- <div class="font-medium text-xs">${info.name || '—'}</div>
592
- <div class="opacity-70">modelID: ${info.modelID || '—'}</div>
716
+ <div class="font-medium text-xs">${name}</div>
717
+ <div class="opacity-70">format: ${format}</div>
718
+ ${missingHtml}
593
719
  </div>
594
720
  </div>
595
721
  `;
@@ -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();