@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 +11 -1
- package/package.json +1 -1
- package/src/IfcViewer.js +163 -31
- 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 +271 -37
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.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] - Автоматически загружать
|
|
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 (
|
|
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.
|
|
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
|
|
166
|
-
const loadSource =
|
|
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('Не указан источник
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
185
|
-
this.
|
|
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(
|
|
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
|
|
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
|
-
|
|
259
|
-
return
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
|
484
|
-
if (
|
|
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
|
-
|
|
493
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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">${
|
|
592
|
-
<div class="opacity-70">
|
|
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
|
`;
|
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();
|