@sequent-org/ifc-viewer 1.0.3-ci.6.0 → 1.0.4-ci.8.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
@@ -43,7 +43,8 @@ function showIfcModal(ifcUrl) {
43
43
 
44
44
  const viewer = new IfcViewer({
45
45
  container: container,
46
- ifcUrl: ifcUrl
46
+ ifcUrl: ifcUrl,
47
+ wasmUrl: '/storage/web-ifc.wasm' // Путь к WASM в Laravel storage
47
48
  // Минимальный режим по умолчанию - только просмотрщик с верхней панелью
48
49
  })
49
50
 
@@ -53,6 +54,28 @@ function showIfcModal(ifcUrl) {
53
54
  }
54
55
  ```
55
56
 
57
+ **Настройка WASM файла в Laravel:**
58
+
59
+ 1. Скопируйте `web-ifc.wasm` в папку `public/storage/`:
60
+ ```bash
61
+ cp node_modules/web-ifc/web-ifc.wasm public/storage/
62
+ ```
63
+
64
+ 2. Или используйте символическую ссылку:
65
+ ```bash
66
+ php artisan storage:link
67
+ # Затем скопируйте файл в storage/app/public/
68
+ ```
69
+
70
+ 3. Альтернативно, укажите путь к файлу в `node_modules`:
71
+ ```javascript
72
+ const viewer = new IfcViewer({
73
+ container: container,
74
+ ifcUrl: ifcUrl,
75
+ wasmUrl: '/node_modules/web-ifc/web-ifc.wasm'
76
+ })
77
+ ```
78
+
56
79
  ### Загрузка пользовательского файла
57
80
 
58
81
  ```javascript
@@ -74,6 +97,18 @@ fileInput.addEventListener('change', async (e) => {
74
97
  })
75
98
  ```
76
99
 
100
+ ### Кастомный путь к WASM файлу
101
+
102
+ ```javascript
103
+ const viewer = new IfcViewer({
104
+ container: '#viewer-container',
105
+ ifcUrl: '/models/building.ifc',
106
+ wasmUrl: '/custom-path/web-ifc.wasm' // Указываем свой путь к WASM
107
+ })
108
+
109
+ await viewer.init()
110
+ ```
111
+
77
112
  ## ⚙️ Опции конфигурации
78
113
 
79
114
  | Опция | Тип | По умолчанию | Описание |
@@ -81,12 +116,52 @@ fileInput.addEventListener('change', async (e) => {
81
116
  | `container` | `HTMLElement \| string` | **обязательно** | DOM элемент или селектор для контейнера |
82
117
  | `ifcUrl` | `string` | `null` | URL для загрузки IFC файла |
83
118
  | `ifcFile` | `File` | `null` | File объект для загрузки |
119
+ | `wasmUrl` | `string` | `null` | URL для загрузки WASM файла web-ifc |
84
120
  | `showSidebar` | `boolean` | `false` | Показывать боковую панель с деревом |
85
121
  | `showControls` | `boolean` | `false` | Показывать нижние кнопки управления |
86
122
  | `showToolbar` | `boolean` | `true` | Показывать верхнюю панель инструментов |
87
123
  | `autoLoad` | `boolean` | `true` | Автоматически загружать при инициализации |
88
124
  | `theme` | `string` | `'light'` | Тема интерфейса (`'light'` \| `'dark'`) |
89
125
 
126
+ ### 🔧 Подробное описание параметров
127
+
128
+ #### `wasmUrl` - Настройка пути к WASM файлу
129
+
130
+ Параметр `wasmUrl` позволяет указать кастомный путь к WASM файлу библиотеки `web-ifc`. Это особенно полезно при интеграции пакета в проекты с нестандартной структурой ресурсов.
131
+
132
+ **Особенности:**
133
+ - **По умолчанию**: Если не указан, используется автоматически определяемый путь из папки `public/wasm/`
134
+ - **Поддержка форматов**: Полные URL (`https://example.com/web-ifc.wasm`) и относительные пути (`/assets/web-ifc.wasm`)
135
+ - **Обратная совместимость**: При ошибке загрузки кастомного пути автоматически переключается на дефолтный
136
+ - **Обработка ошибок**: В консоль выводится предупреждение при неудачной настройке кастомного пути
137
+
138
+ **Примеры использования:**
139
+
140
+ ```javascript
141
+ // Относительный путь
142
+ const viewer1 = new IfcViewer({
143
+ container: '#viewer',
144
+ wasmUrl: '/assets/web-ifc.wasm'
145
+ })
146
+
147
+ // Полный URL
148
+ const viewer2 = new IfcViewer({
149
+ container: '#viewer',
150
+ wasmUrl: 'https://cdn.example.com/web-ifc.wasm'
151
+ })
152
+
153
+ // Путь с подпапкой
154
+ const viewer3 = new IfcViewer({
155
+ container: '#viewer',
156
+ wasmUrl: '/static/libs/web-ifc.wasm'
157
+ })
158
+ ```
159
+
160
+ **Когда использовать:**
161
+ - При размещении WASM файла в нестандартной папке
162
+ - При использовании CDN для статических ресурсов
163
+ - При интеграции в фреймворки с особой структурой ресурсов (Laravel, Next.js и т.д.)
164
+
90
165
  ## 🎯 API методы
91
166
 
92
167
  ### Основные методы
@@ -196,11 +271,56 @@ npm run test:manual
196
271
  - `.ifczip` - архивы IFC
197
272
  - `.zip` - ZIP архивы с IFC файлами
198
273
 
274
+ ## 🔧 Troubleshooting
275
+
276
+ ### Проблемы с WASM файлом
277
+
278
+ **Ошибка загрузки WASM:**
279
+ ```
280
+ Failed to load web-ifc.wasm
281
+ ```
282
+
283
+ **Решения:**
284
+ 1. **Проверьте путь к файлу:**
285
+ ```javascript
286
+ // Убедитесь, что файл доступен по указанному пути
287
+ const viewer = new IfcViewer({
288
+ container: '#viewer',
289
+ wasmUrl: '/correct-path/web-ifc.wasm'
290
+ })
291
+ ```
292
+
293
+ 2. **Проверьте CORS настройки:**
294
+ - Для локальной разработки используйте относительные пути
295
+ - Для продакшена настройте CORS для WASM файлов
296
+
297
+ 3. **Проверьте MIME-тип:**
298
+ ```nginx
299
+ # В nginx.conf
300
+ location ~* \.wasm$ {
301
+ add_header Content-Type application/wasm;
302
+ }
303
+ ```
304
+
305
+ 4. **Альтернативные пути:**
306
+ ```javascript
307
+ // Попробуйте разные варианты
308
+ wasmUrl: '/web-ifc.wasm' // корень
309
+ wasmUrl: '/assets/web-ifc.wasm' // папка assets
310
+ wasmUrl: '/static/web-ifc.wasm' // папка static
311
+ ```
312
+
313
+ **Отладка:**
314
+ - Откройте DevTools → Network и проверьте загрузку WASM файла
315
+ - Проверьте консоль на предупреждения о `wasmUrl`
316
+ - Убедитесь, что файл `web-ifc.wasm` существует и доступен
317
+
199
318
  ## 🔧 Системные требования
200
319
 
201
320
  - Node.js >= 16
202
- - Современный браузер с поддержкой WebGL
321
+ - Современный браузер с поддержкой WebGL и WebAssembly
203
322
  - Для работы требуются файлы `web-ifc.wasm` в публичной папке проекта
323
+ - Поддержка ES6 модулей в браузере
204
324
 
205
325
  ## 📄 Лицензия
206
326
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sequent-org/ifc-viewer",
3
3
  "private": false,
4
- "version": "1.0.3-ci.6.0",
4
+ "version": "1.0.4-ci.8.0",
5
5
  "type": "module",
6
6
  "description": "IFC 3D model viewer component for web applications",
7
7
  "main": "src/index.js",
package/src/IfcViewer.js CHANGED
@@ -21,6 +21,7 @@ export class IfcViewer {
21
21
  * @param {HTMLElement|string} options.container - Контейнер для рендера (элемент или селектор)
22
22
  * @param {string} [options.ifcUrl] - URL для загрузки IFC файла
23
23
  * @param {File} [options.ifcFile] - File объект для загрузки IFC файла
24
+ * @param {string} [options.wasmUrl] - URL для загрузки WASM файла web-ifc
24
25
  * @param {boolean} [options.showSidebar=false] - Показывать ли боковую панель с деревом
25
26
  * @param {boolean} [options.showControls=false] - Показывать ли панель управления (нижние кнопки)
26
27
  * @param {boolean} [options.showToolbar=true] - Показывать ли верхнюю панель инструментов
@@ -47,8 +48,9 @@ export class IfcViewer {
47
48
  this.options = {
48
49
  ifcUrl: options.ifcUrl || null,
49
50
  ifcFile: options.ifcFile || null,
51
+ wasmUrl: options.wasmUrl || null,
50
52
  showSidebar: options.showSidebar === true, // по умолчанию false
51
- showControls: options.showControls === true, // по умолчанию false
53
+ showControls: options.showControls === true, // по умолчанию false
52
54
  showToolbar: options.showToolbar !== false, // по умолчанию true
53
55
  autoLoad: options.autoLoad !== false,
54
56
  theme: options.theme || 'light',
@@ -430,7 +432,7 @@ export class IfcViewer {
430
432
  throw new Error('Viewer должен быть инициализирован перед IfcService');
431
433
  }
432
434
 
433
- this.ifcService = new IfcService(this.viewer);
435
+ this.ifcService = new IfcService(this.viewer, this.options.wasmUrl);
434
436
  this.ifcService.init();
435
437
  }
436
438
 
@@ -12,9 +12,11 @@ import IFCWorkerUrl from 'web-ifc-three/IFCWorker.js?url';
12
12
  export class IfcService {
13
13
  /**
14
14
  * @param {import('../viewer/Viewer').Viewer} viewer
15
+ * @param {string} [wasmUrl] - URL для загрузки WASM файла web-ifc
15
16
  */
16
- constructor(viewer) {
17
+ constructor(viewer, wasmUrl = null) {
17
18
  this.viewer = viewer;
19
+ this.wasmUrl = wasmUrl;
18
20
  this.loader = null;
19
21
  this.lastModel = null; // THREE.Object3D модели IFC
20
22
  this.lastFileName = null;
@@ -24,30 +26,115 @@ export class IfcService {
24
26
  }
25
27
 
26
28
  init() {
27
- this.loader = new IFCLoader();
28
- // Отключаем Web Worker: временно парсим в главном потоке для стабильности
29
- try { this.loader.ifcManager.useWebWorkers?.(false); } catch(_) {}
30
- // Путь к wasm файлу (скопируйте web-ifc.wasm в public/wasm)
31
29
  try {
32
- // Преобразуем URL файла wasm в URL каталога и передадим в воркер
33
- const wasmDir = new URL('.', WEBIFC_WASM_URL).href;
34
- this.loader.ifcManager.setWasmPath(wasmDir);
35
- // Дополнительно подстрахуемся передачей полного файла, если версия это поддерживает
36
- try { this.loader.ifcManager.setWasmPath(WEBIFC_WASM_URL); } catch(_) {}
37
- } catch (_) {
38
- this.loader.ifcManager.setWasmPath('/wasm/');
30
+ this.loader = new IFCLoader();
31
+ // Отключаем Web Worker: временно парсим в главном потоке для стабильности
32
+ try { this.loader.ifcManager.useWebWorkers?.(false); } catch(_) {}
33
+
34
+ // Настройка пути к WASM файлу с улучшенной обработкой ошибок
35
+ this._setupWasmPath();
36
+
37
+ // Настройка конфигурации web-ifc
38
+ this._setupWebIfcConfig();
39
+
40
+ } catch (error) {
41
+ console.error('IfcService: критическая ошибка инициализации:', error);
42
+ this._handleCriticalError(error);
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Настройка пути к WASM файлу с fallback
48
+ * @private
49
+ */
50
+ _setupWasmPath() {
51
+ const wasmPaths = this._getWasmPaths();
52
+
53
+ for (let i = 0; i < wasmPaths.length; i++) {
54
+ try {
55
+ this.loader.ifcManager.setWasmPath(wasmPaths[i]);
56
+ console.log(`IfcService: WASM путь установлен: ${wasmPaths[i]}`);
57
+ return; // Успешно установлен
58
+ } catch (error) {
59
+ console.warn(`IfcService: не удалось установить WASM путь ${wasmPaths[i]}:`, error.message);
60
+ if (i === wasmPaths.length - 1) {
61
+ // Последний путь тоже не сработал
62
+ throw new Error('Все пути к WASM файлу недоступны');
63
+ }
64
+ }
39
65
  }
66
+ }
67
+
68
+ /**
69
+ * Получает список путей к WASM файлу в порядке приоритета
70
+ * @private
71
+ */
72
+ _getWasmPaths() {
73
+ const paths = [];
74
+
75
+ // 1. Пользовательский путь (если указан)
76
+ if (this.wasmUrl) {
77
+ paths.push(this.wasmUrl);
78
+ }
79
+
80
+ // 2. Пути по умолчанию
81
+ try {
82
+ const wasmDir = new URL('.', WEBIFC_WASM_URL).href;
83
+ paths.push(wasmDir);
84
+ } catch (_) {}
85
+
86
+ try {
87
+ paths.push(WEBIFC_WASM_URL);
88
+ } catch (_) {}
89
+
90
+ // 3. Резервные пути
91
+ paths.push('/wasm/', '/wasm/web-ifc.wasm', '/web-ifc.wasm');
92
+
93
+ return paths;
94
+ }
95
+
96
+ /**
97
+ * Настройка конфигурации web-ifc
98
+ * @private
99
+ */
100
+ _setupWebIfcConfig() {
40
101
  try {
41
102
  this.loader.ifcManager.applyWebIfcConfig?.({
42
103
  COORDINATE_TO_ORIGIN: true,
43
104
  USE_FAST_BOOLS: true,
44
105
  // Порог игнорирования очень мелких полигонов (уменьшаем шум)
45
- // Некоторые сборки поддерживают SMALL_TRIANGLE_THRESHOLD
46
106
  SMALL_TRIANGLE_THRESHOLD: 1e-9,
47
107
  });
48
- } catch(_) {}
108
+ } catch (error) {
109
+ console.warn('IfcService: не удалось применить конфигурацию web-ifc:', error.message);
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Обработка критических ошибок инициализации
115
+ * @private
116
+ */
117
+ _handleCriticalError(error) {
118
+ // Создаем заглушку для loader, чтобы избежать падения
119
+ this.loader = {
120
+ ifcManager: {
121
+ setWasmPath: () => {},
122
+ applyWebIfcConfig: () => {},
123
+ useWebWorkers: () => {},
124
+ load: () => Promise.reject(new Error('WASM не инициализирован'))
125
+ }
126
+ };
127
+
128
+ // Уведомляем о критической ошибке
129
+ this.viewer?.container?.dispatchEvent(new CustomEvent('ifcviewer:error', {
130
+ detail: {
131
+ error: new Error('Критическая ошибка инициализации WASM: ' + error.message),
132
+ type: 'wasm_init_error'
133
+ }
134
+ }));
49
135
  }
50
136
 
137
+
51
138
  /**
52
139
  * Возвращает пространственную структуру IFC (иерархия) для активной модели
53
140
  * Структура: { expressID, type, children: [...] }
@@ -119,33 +206,36 @@ export class IfcService {
119
206
  * @param {File} file
120
207
  */
121
208
  async loadFile(file) {
122
- if (!this.loader) this.init();
123
- // Проверка расширения: поддерживаются .ifc и .ifczip
124
- const name = (file?.name || "").toLowerCase();
125
- const isIFC = name.endsWith(".ifc");
126
- const isIFS = name.endsWith(".ifs");
127
- const isZIP = name.endsWith(".ifczip") || name.endsWith(".zip");
128
- if (!isIFC && !isIFS && !isZIP) {
129
- alert("Формат не поддерживается. Используйте .ifc, .ifs или .ifczip");
130
- return null;
131
- }
132
- const url = URL.createObjectURL(file);
133
209
  try {
134
- const model = await this.loader.loadAsync(url);
135
- // Показать модель вместо демо-куба
136
- if (this.viewer.replaceWithModel) this.viewer.replaceWithModel(model);
137
- if (this.viewer.focusObject) this.viewer.focusObject(model);
138
- this.lastModel = model;
139
- this.lastFileName = file?.name || null;
140
- // Сообщим, что модель загружена
141
- try { document.dispatchEvent(new CustomEvent('ifc:model-loaded', { detail: { modelID: model.modelID } })); } catch(_) {}
142
- return model;
210
+ if (!this.loader) this.init();
211
+
212
+ // Проверка расширения: поддерживаются .ifc и .ifczip
213
+ const name = (file?.name || "").toLowerCase();
214
+ const isIFC = name.endsWith(".ifc");
215
+ const isIFS = name.endsWith(".ifs");
216
+ const isZIP = name.endsWith(".ifczip") || name.endsWith(".zip");
217
+ if (!isIFC && !isIFS && !isZIP) {
218
+ throw new Error("Формат не поддерживается. Используйте .ifc, .ifs или .ifczip");
219
+ }
220
+
221
+ const url = URL.createObjectURL(file);
222
+ try {
223
+ const model = await this._loadModelWithFallback(url);
224
+ // Показать модель вместо демо-куба
225
+ if (this.viewer.replaceWithModel) this.viewer.replaceWithModel(model);
226
+ if (this.viewer.focusObject) this.viewer.focusObject(model);
227
+ this.lastModel = model;
228
+ this.lastFileName = file?.name || null;
229
+ // Сообщим, что модель загружена
230
+ this._dispatchModelLoaded(model);
231
+ return model;
232
+ } finally {
233
+ URL.revokeObjectURL(url);
234
+ }
143
235
  } catch (err) {
144
236
  console.error("IFC load error:", err);
145
- alert("Ошибка загрузки IFC: " + (err?.message || err));
237
+ this._handleLoadError(err, 'loadFile');
146
238
  return null;
147
- } finally {
148
- URL.revokeObjectURL(url);
149
239
  }
150
240
  }
151
241
 
@@ -154,15 +244,17 @@ export class IfcService {
154
244
  * @param {string} url
155
245
  */
156
246
  async loadUrl(url) {
157
- if (!this.loader) this.init();
158
- if (!url) return null;
159
247
  try {
160
- // Защитим загрузку: перехватим возможные исключения на уровне воркера
161
- const model = await this.loader.loadAsync(url);
248
+ if (!this.loader) this.init();
249
+ if (!url) return null;
250
+
251
+ const model = await this._loadModelWithFallback(url);
162
252
  if (!model || !model.geometry) throw new Error('IFC model returned without geometry');
253
+
163
254
  if (this.viewer.replaceWithModel) this.viewer.replaceWithModel(model);
164
255
  if (this.viewer.focusObject) this.viewer.focusObject(model);
165
256
  this.lastModel = model;
257
+
166
258
  try {
167
259
  // Показать имя файла из URL
168
260
  const u = new URL(url, window.location.origin);
@@ -170,12 +262,13 @@ export class IfcService {
170
262
  } catch (_) {
171
263
  this.lastFileName = url;
172
264
  }
265
+
173
266
  // Сообщим, что модель загружена
174
- try { document.dispatchEvent(new CustomEvent('ifc:model-loaded', { detail: { modelID: model.modelID } })); } catch(_) {}
267
+ this._dispatchModelLoaded(model);
175
268
  return model;
176
269
  } catch (err) {
177
270
  console.error("IFC loadUrl error:", err);
178
- alert("Ошибка загрузки IFC по URL: " + (err?.message || err));
271
+ this._handleLoadError(err, 'loadUrl');
179
272
  return null;
180
273
  }
181
274
  }
@@ -263,6 +356,108 @@ export class IfcService {
263
356
  this.lastModel.visible = true;
264
357
  }
265
358
  }
359
+
360
+ /**
361
+ * Загружает модель с fallback обработкой ошибок WASM
362
+ * @private
363
+ */
364
+ async _loadModelWithFallback(url) {
365
+ try {
366
+ return await this.loader.loadAsync(url);
367
+ } catch (error) {
368
+ // Проверяем, связана ли ошибка с WASM
369
+ if (this._isWasmError(error)) {
370
+ console.warn('IfcService: обнаружена ошибка WASM, пытаемся переинициализировать...');
371
+ await this._reinitializeWithFallback();
372
+ // Повторная попытка загрузки
373
+ return await this.loader.loadAsync(url);
374
+ }
375
+ throw error;
376
+ }
377
+ }
378
+
379
+ /**
380
+ * Проверяет, связана ли ошибка с WASM
381
+ * @private
382
+ */
383
+ _isWasmError(error) {
384
+ const message = error?.message?.toLowerCase() || '';
385
+ return message.includes('wasm') ||
386
+ message.includes('webassembly') ||
387
+ message.includes('module') ||
388
+ message.includes('instantiate');
389
+ }
390
+
391
+ /**
392
+ * Переинициализирует loader с fallback путями
393
+ * @private
394
+ */
395
+ async _reinitializeWithFallback() {
396
+ try {
397
+ // Попробуем переинициализировать с другими путями
398
+ this.loader = new IFCLoader();
399
+ this._setupWasmPath();
400
+ this._setupWebIfcConfig();
401
+ } catch (error) {
402
+ console.error('IfcService: не удалось переинициализировать:', error);
403
+ throw new Error('Критическая ошибка WASM: невозможно загрузить модель');
404
+ }
405
+ }
406
+
407
+ /**
408
+ * Отправляет событие о загрузке модели
409
+ * @private
410
+ */
411
+ _dispatchModelLoaded(model) {
412
+ try {
413
+ document.dispatchEvent(new CustomEvent('ifc:model-loaded', {
414
+ detail: { modelID: model.modelID }
415
+ }));
416
+ } catch (_) {}
417
+ }
418
+
419
+ /**
420
+ * Обрабатывает ошибки загрузки
421
+ * @private
422
+ */
423
+ _handleLoadError(error, method) {
424
+ const errorMessage = `Ошибка загрузки IFC (${method}): ${error?.message || error}`;
425
+
426
+ // Отправляем событие об ошибке
427
+ this.viewer?.container?.dispatchEvent(new CustomEvent('ifcviewer:error', {
428
+ detail: {
429
+ error: new Error(errorMessage),
430
+ type: 'load_error',
431
+ method: method
432
+ }
433
+ }));
434
+
435
+ // Показываем пользователю понятное сообщение
436
+ const userMessage = this._getUserFriendlyMessage(error);
437
+ alert(userMessage);
438
+ }
439
+
440
+ /**
441
+ * Возвращает понятное пользователю сообщение об ошибке
442
+ * @private
443
+ */
444
+ _getUserFriendlyMessage(error) {
445
+ const message = error?.message?.toLowerCase() || '';
446
+
447
+ if (message.includes('wasm') || message.includes('webassembly')) {
448
+ return 'Ошибка загрузки WASM модуля. Проверьте доступность файла web-ifc.wasm';
449
+ }
450
+
451
+ if (message.includes('network') || message.includes('fetch')) {
452
+ return 'Ошибка сети при загрузке файла. Проверьте подключение к интернету';
453
+ }
454
+
455
+ if (message.includes('format') || message.includes('parse')) {
456
+ return 'Ошибка формата файла. Убедитесь, что файл является корректным IFC файлом';
457
+ }
458
+
459
+ return `Ошибка загрузки модели: ${error?.message || 'Неизвестная ошибка'}`;
460
+ }
266
461
  }
267
462
 
268
463