@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 +11 -1
- package/package.json +1 -1
- package/src/IfcViewer.js +154 -28
- package/src/ifc/IfcService.js +0 -2
- package/src/index.js +10 -0
- package/src/main.js +122 -33
- package/src/model-loading/ModelLoaderRegistry.js +252 -0
- package/src/model-loading/loaders/DaeModelLoader.js +275 -0
- package/src/model-loading/loaders/FbxModelLoader.js +68 -0
- package/src/model-loading/loaders/GltfModelLoader.js +316 -0
- package/src/model-loading/loaders/IfcModelLoader.js +77 -0
- package/src/model-loading/loaders/ObjModelLoader.js +310 -0
- package/src/model-loading/loaders/StlModelLoader.js +102 -0
- package/src/model-loading/loaders/TdsModelLoader.js +205 -0
- package/src/viewer/Viewer.js +281 -48
package/README.md
CHANGED
|
@@ -341,7 +341,7 @@ import '@sequent-org/ifc-viewer/style.css'
|
|
|
341
341
|
- Одновременно активна только одна плоскость
|
|
342
342
|
|
|
343
343
|
### Загрузка файлов
|
|
344
|
-
- **📁 Загрузить** - Кнопка выбора
|
|
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.
|
|
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.
|
|
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
|
|
166
|
-
const loadSource =
|
|
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('Не указан источник
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
185
|
-
this.
|
|
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(
|
|
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
|
|
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
|
-
|
|
259
|
-
return
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
|
484
|
-
if (
|
|
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
|
-
|
|
493
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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">${
|
|
592
|
-
<div class="opacity-70">
|
|
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
|
`;
|
package/src/ifc/IfcService.js
CHANGED
|
@@ -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
|
|
345
|
-
if (!
|
|
346
|
-
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
|
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
|
-
|
|
532
|
-
|
|
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
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
if (
|
|
551
|
-
|
|
552
|
-
const
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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();
|