@sequent-org/ifc-viewer 1.2.4-ci.26.0 → 1.2.4-ci.28.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
@@ -27,6 +27,24 @@ IFC 3D model viewer component for web applications. Основан на Three.js
27
27
 
28
28
  **Готово!** Пакет полностью автоматический - никаких дополнительных настроек не требуется.
29
29
 
30
+ ### Дефолтный пресет визуала (важно)
31
+
32
+ По умолчанию пакет включает **пресет "Тест"** — это рекомендованные настройки визуала (тени + самозатенение + ACES/sRGB + SSAO + Environment), чтобы модель выглядела корректно сразу после интеграции.
33
+
34
+ Если нужно отключить и вернуться к базовым настройкам:
35
+
36
+ ```javascript
37
+ import { IfcViewer } from '@sequent-org/ifc-viewer'
38
+
39
+ const viewer = new IfcViewer({
40
+ container: '#viewer-container',
41
+ ifcUrl: '/path/to/model.ifc',
42
+ useTestPreset: false,
43
+ })
44
+
45
+ await viewer.init()
46
+ ```
47
+
30
48
  ## 🚀 Установка
31
49
 
32
50
  ```bash
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.26.0",
4
+ "version": "1.2.4-ci.28.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
@@ -24,6 +24,7 @@ export class IfcViewer {
24
24
  * @param {string} [options.ifcUrl] - URL для загрузки IFC файла
25
25
  * @param {File} [options.ifcFile] - File объект для загрузки IFC файла
26
26
  * @param {string} [options.wasmUrl] - URL для загрузки WASM файла web-ifc
27
+ * @param {boolean} [options.useTestPreset=true] - Включать ли пресет "Тест" по умолчанию (рекомендованные тени/визуал)
27
28
  * @param {boolean} [options.showSidebar=false] - Показывать ли боковую панель с деревом
28
29
  * @param {boolean} [options.showControls=false] - Показывать ли панель управления (нижние кнопки)
29
30
  * @param {boolean} [options.showToolbar=true] - Показывать ли верхнюю панель инструментов
@@ -51,6 +52,8 @@ export class IfcViewer {
51
52
  ifcUrl: options.ifcUrl || null,
52
53
  ifcFile: options.ifcFile || null,
53
54
  wasmUrl: options.wasmUrl || null,
55
+ // По умолчанию включаем пресет "Тест" для корректного вида теней (как в демо-настройках)
56
+ useTestPreset: options.useTestPreset !== false,
54
57
  showSidebar: options.showSidebar === true, // по умолчанию false
55
58
  showControls: options.showControls === true, // по умолчанию false
56
59
  showToolbar: options.showToolbar !== false, // по умолчанию true
@@ -80,7 +83,7 @@ export class IfcViewer {
80
83
  // Внутренние состояния управления
81
84
  this.viewerState = {
82
85
  quality: 'medium', // 'low' | 'medium' | 'high'
83
- edgesVisible: true,
86
+ edgesVisible: false,
84
87
  flatShading: true,
85
88
  clipping: {
86
89
  x: false,
@@ -112,6 +115,12 @@ export class IfcViewer {
112
115
  this._initViewer();
113
116
  this._initIfcService();
114
117
  this._initTreeView();
118
+
119
+ // Применяем дефолтный пресет пакета (полностью независим от index.html)
120
+ // Важно: пресет должен примениться ДО загрузки модели, чтобы настройки подхватились при replaceWithModel()
121
+ if (this.options.useTestPreset && this.viewer?.setTestPresetEnabled) {
122
+ this.viewer.setTestPresetEnabled(true);
123
+ }
115
124
 
116
125
  // Настраиваем обработчики событий
117
126
  this._setupEventHandlers();
@@ -292,13 +301,34 @@ export class IfcViewer {
292
301
  </div>
293
302
 
294
303
  <!-- Верхняя панель управления -->
295
- <div id="ifcToolbar" class="d-flex px-4" style="border:0px red solid; width: 250px; position: absolute; z-index: 60; justify-content:space-between; bottom: 10px; left: calc(50% - 125px); ">
304
+ <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); ">
296
305
 
297
306
  <div class="navbar-end flex gap-2">
298
307
 
299
308
  <!-- Стили отображения -->
300
309
  <div class="join">
301
- <button class="btn btn-sm join-item btn-active" id="ifcToggleEdges"><svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" class="c-tree__icon c-tree__icon--3d"><g fill="#252A3F" fill-rule="nonzero"><path d="M12.5 5L6.005 8.75v7.5L12.5 20l6.495-3.75v-7.5L12.5 5zm0-1.155l7.495 4.328v8.654L12.5 21.155l-7.495-4.328V8.173L12.5 3.845z"></path><path d="M12 12v8.059h1V12z"></path><path d="M5.641 9.157l7.045 4.025.496-.868-7.045-4.026z"></path><path d="M18.863 8.288l-7.045 4.026.496.868 7.045-4.025z"></path></g></svg></button>
310
+ <button class="btn btn-sm join-item" id="ifcToggleEdges"><svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" class="c-tree__icon c-tree__icon--3d"><g fill="#252A3F" fill-rule="nonzero"><path d="M12.5 5L6.005 8.75v7.5L12.5 20l6.495-3.75v-7.5L12.5 5zm0-1.155l7.495 4.328v8.654L12.5 21.155l-7.495-4.328V8.173L12.5 3.845z"></path><path d="M12 12v8.059h1V12z"></path><path d="M5.641 9.157l7.045 4.025.496-.868-7.045-4.026z"></path><path d="M18.863 8.288l-7.045 4.026.496.868 7.045-4.025z"></path></g></svg></button>
311
+ <button class="btn btn-sm join-item btn-active" id="ifcToggleShadows" title="Тени">
312
+ <svg width="24" height="24" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
313
+ <path fill="#000000" d="M207.39 0.00 C 212.56 0.44 217.91 0.57 222.85 1.13 C 262.70 5.63 300.04 25.42 325.92 56.02 Q 361.61 98.22 364.04 153.58 A 0.23 0.22 11.8 0 1 363.71 153.79 L 349.62 146.87 A 0.90 0.90 0.0 0 1 349.13 146.22 C 347.61 137.34 346.80 130.78 344.73 123.45 C 332.00 78.38 299.83 39.42 256.32 20.94 C 235.21 11.97 211.55 8.21 189.00 11.72 Q 153.18 17.30 128.20 42.72 Q 110.60 60.63 102.05 85.78 C 88.10 126.83 95.83 172.91 118.73 209.60 Q 122.30 215.32 127.44 222.49 A 1.70 1.70 0.0 0 1 127.76 223.45 L 128.09 298.58 A 0.23 0.23 0.0 0 1 127.75 298.78 C 82.71 273.41 52.38 228.77 46.61 177.37 C 41.26 129.73 57.69 81.88 91.39 47.83 Q 129.76 9.07 184.30 1.42 C 189.99 0.63 196.32 0.47 202.38 0.00 L 207.39 0.00 Z"/>
314
+ <path fill="#000000" d="M312.50 512.00 L 311.56 512.00 Q 309.05 511.43 307.15 509.96 A 2.09 2.07 -19.8 0 0 306.27 509.55 Q 304.17 509.09 303.52 508.75 Q 206.21 457.82 153.89 430.39 Q 152.01 429.41 151.14 427.04 Q 150.58 425.52 150.56 422.64 Q 150.42 393.12 149.69 237.76 Q 149.69 236.82 150.36 233.09 Q 150.86 230.31 153.63 228.87 Q 250.38 178.34 303.97 150.48 Q 307.28 148.76 310.46 150.29 Q 339.28 164.17 462.71 224.01 Q 466.51 225.85 466.51 230.76 Q 466.50 321.20 466.49 424.75 C 466.49 428.40 464.08 430.42 460.80 432.20 Q 425.93 451.10 315.52 510.97 A 0.87 0.80 -65.8 0 1 315.31 511.06 L 312.50 512.00 Z M 444.21 230.96 A 0.32 0.32 0.0 0 0 444.19 230.39 L 307.84 163.41 A 0.32 0.32 0.0 0 0 307.55 163.42 L 171.77 234.43 A 0.32 0.32 0.0 0 0 171.78 235.00 L 311.71 304.85 A 0.32 0.32 0.0 0 0 312.01 304.85 L 444.21 230.96 Z M 318.55 493.80 A 0.34 0.34 0.0 0 0 319.05 494.10 L 453.17 421.35 A 0.34 0.34 0.0 0 0 453.35 421.05 L 453.35 241.55 A 0.34 0.34 0.0 0 0 452.84 241.25 L 318.72 316.20 A 0.34 0.34 0.0 0 0 318.55 316.50 L 318.55 493.80 Z"/>
315
+ </svg>
316
+ </button>
317
+ <button class="btn btn-sm join-item" id="ifcToggleProjection" title="Перспектива / Ортогонально (переключение)">
318
+ <!-- По умолчанию Ortho, поэтому показываем действие: включить Perspective -->
319
+ <svg width="24" height="24" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
320
+ <path fill="#000000" d="M365.50 333.29 A 0.30 0.30 0.0 0 0 365.95 333.55 L 492.36 259.80 A 0.47 0.47 0.0 0 0 492.51 259.12 Q 489.74 255.31 492.90 252.78 A 0.30 0.30 0.0 0 0 492.83 252.27 C 489.14 250.57 490.13 245.43 493.90 244.50 C 496.33 243.90 501.93 247.88 504.97 249.79 A 1.50 1.48 -85.3 0 1 505.54 250.47 L 505.97 251.53 A 0.72 0.71 76.6 0 0 506.67 251.97 C 509.70 251.84 512.28 254.84 511.15 257.67 Q 510.77 258.62 508.18 260.14 C 355.38 349.68 251.70 410.06 149.28 469.74 A 3.94 3.93 -44.9 0 1 145.31 469.74 Q 7.70 389.45 2.96 386.69 C 0.09 385.02 0.50 382.93 0.50 379.49 Q 0.50 259.79 0.50 128.77 C 0.50 127.21 1.85 125.96 3.27 125.13 Q 68.02 87.24 145.61 41.87 C 146.90 41.11 148.92 41.81 150.33 42.63 Q 219.34 82.64 289.83 124.16 C 291.25 125.00 292.80 126.11 294.76 127.15 Q 299.89 129.89 301.84 131.37 C 305.49 134.15 301.99 140.40 297.26 138.18 Q 295.67 137.42 294.41 136.58 A 0.26 0.26 0.0 0 0 294.00 136.80 L 294.00 209.83 A 0.44 0.44 0.0 0 0 294.36 210.26 Q 340.50 219.23 361.26 223.22 C 366.12 224.15 365.53 227.44 365.51 232.03 Q 365.50 234.52 365.49 251.11 A 0.73 0.73 0.0 0 0 366.22 251.84 L 370.02 251.84 A 3.64 3.64 0.0 0 1 373.66 255.48 L 373.66 256.72 A 3.45 3.44 0.0 0 1 370.21 260.16 L 366.15 260.16 A 0.65 0.65 0.0 0 0 365.50 260.81 L 365.50 333.29 Z M 9.05 131.40 A 0.30 0.30 0.0 0 0 8.90 131.66 L 8.90 380.18 A 0.30 0.30 0.0 0 0 9.05 380.44 L 142.74 458.43 A 0.30 0.30 0.0 0 0 143.19 458.17 L 143.19 53.67 A 0.30 0.30 0.0 0 0 142.74 53.41 L 9.05 131.40 Z M 285.68 380.52 A 0.32 0.32 0.0 0 0 285.84 380.25 L 285.84 131.66 A 0.32 0.32 0.0 0 0 285.68 131.39 L 151.98 53.39 A 0.32 0.32 0.0 0 0 151.50 53.67 L 151.50 458.24 A 0.32 0.32 0.0 0 0 151.98 458.52 L 285.68 380.52 Z M 294.62 218.77 A 0.36 0.36 0.0 0 0 294.19 219.13 L 294.19 374.90 A 0.36 0.36 0.0 0 0 294.73 375.21 L 357.13 338.81 A 0.36 0.36 0.0 0 0 357.31 338.50 L 357.31 231.30 A 0.36 0.36 0.0 0 0 357.02 230.94 L 294.62 218.77 Z"/>
321
+ <path fill="#000000" d="M 331.8028 153.6467 A 4.00 4.00 0.0 0 1 326.3286 155.0726 L 318.9110 150.7207 A 4.00 4.00 0.0 0 1 317.4851 145.2465 L 317.6572 144.9533 A 4.00 4.00 0.0 0 1 323.1314 143.5274 L 330.5490 147.8793 A 4.00 4.00 0.0 0 1 331.9749 153.3535 L 331.8028 153.6467 Z"/>
322
+ <path fill="#000000" d="M 360.6890 170.5463 A 4.00 4.00 0.0 0 1 355.2099 171.9531 L 347.8247 167.5855 A 4.00 4.00 0.0 0 1 346.4179 162.1064 L 346.5910 161.8137 A 4.00 4.00 0.0 0 1 352.0701 160.4069 L 359.4553 164.7745 A 4.00 4.00 0.0 0 1 360.8621 170.2536 L 360.6890 170.5463 Z"/>
323
+ <path fill="#000000" d="M 389.5811 187.4643 A 3.99 3.99 0.0 0 1 384.1181 188.8771 L 376.8287 184.5833 A 3.99 3.99 0.0 0 1 375.4159 179.1204 L 375.6189 178.7757 A 3.99 3.99 0.0 0 1 381.0819 177.3629 L 388.3713 181.6567 A 3.99 3.99 0.0 0 1 389.7841 187.1196 L 389.5811 187.4643 Z"/>
324
+ <path fill="#000000" d="M 418.5914 204.3586 A 3.99 3.99 0.0 0 1 413.1235 205.7523 L 405.7288 201.3617 A 3.99 3.99 0.0 0 1 404.3350 195.8938 L 404.5086 195.6014 A 3.99 3.99 0.0 0 1 409.9765 194.2077 L 417.3712 198.5983 A 3.99 3.99 0.0 0 1 418.7650 204.0662 L 418.5914 204.3586 Z"/>
325
+ <path fill="#000000" d="M 447.6480 221.1624 A 3.99 3.99 0.0 0 1 442.2027 222.6419 L 434.7225 218.3579 A 3.99 3.99 0.0 0 1 433.2431 212.9126 L 433.4120 212.6176 A 3.99 3.99 0.0 0 1 438.8573 211.1381 L 446.3375 215.4221 A 3.99 3.99 0.0 0 1 447.8169 220.8674 L 447.6480 221.1624 Z"/>
326
+ <path fill="#000000" d="M 476.5002 238.1477 A 3.99 3.99 0.0 0 1 471.0372 239.5605 L 463.6099 235.1855 A 3.99 3.99 0.0 0 1 462.1971 229.7225 L 462.3798 229.4123 A 3.99 3.99 0.0 0 1 467.8428 227.9995 L 475.2701 232.3745 A 3.99 3.99 0.0 0 1 476.6829 237.8375 L 476.5002 238.1477 Z"/>
327
+ <path fill="#000000" d="M 407.4604 256.3255 A 3.98 3.98 0.0 0 1 403.4873 260.3125 L 394.8874 260.3275 A 3.98 3.98 0.0 0 1 390.9004 256.3545 L 390.8996 255.8945 A 3.98 3.98 0.0 0 1 394.8727 251.9075 L 403.4726 251.8925 A 3.98 3.98 0.0 0 1 407.4596 255.8655 L 407.4604 256.3255 Z"/>
328
+ <path fill="#000000" d="M 440.9596 256.3545 A 3.98 3.98 0.0 0 1 436.9726 260.3275 L 428.3727 260.3125 A 3.98 3.98 0.0 0 1 424.3996 256.3255 L 424.4004 255.8655 A 3.98 3.98 0.0 0 1 428.3874 251.8925 L 436.9873 251.9075 A 3.98 3.98 0.0 0 1 440.9604 255.8945 L 440.9596 256.3545 Z"/>
329
+ <path fill="#000000" d="M 474.4604 256.3255 A 3.98 3.98 0.0 0 1 470.4873 260.3125 L 461.8874 260.3275 A 3.98 3.98 0.0 0 1 457.9004 256.3545 L 457.8996 255.8945 A 3.98 3.98 0.0 0 1 461.8727 251.9075 L 470.4726 251.8925 A 3.98 3.98 0.0 0 1 474.4596 255.8655 L 474.4604 256.3255 Z"/>
330
+ </svg>
331
+ </button>
302
332
  </div>
303
333
 
304
334
  <!-- Секущие плоскости -->
@@ -71,7 +71,9 @@ export class IfcService {
71
71
  }
72
72
 
73
73
  /**
74
- * Получает список путей к WASM файлу в порядке приоритета
74
+ * Получает список путей к WASM в порядке приоритета.
75
+ * ВАЖНО: web-ifc сам добавляет имя файла web-ifc.wasm к переданному пути,
76
+ * поэтому здесь указываем директории, а не полный путь до файла.
75
77
  * @private
76
78
  */
77
79
  _getWasmPaths() {
@@ -83,12 +85,14 @@ export class IfcService {
83
85
  }
84
86
 
85
87
  // 2. Популярные пути по умолчанию (в порядке приоритета)
88
+ // Ожидаемый итоговый URL после SetWasmPath(path):
89
+ // path + 'web-ifc.wasm'
86
90
  paths.push(
87
- '/node_modules/web-ifc/web-ifc.wasm', // Прямо из node_modules (самый надежный)
88
- '/wasm/web-ifc.wasm', // Стандартный путь в public/wasm/
89
- '/web-ifc.wasm', // Корневой путь
90
- './web-ifc.wasm', // Относительный путь
91
- 'web-ifc.wasm' // Просто имя файла
91
+ '/wasm/', // Стандартный путь: public/wasm/web-ifc.wasm
92
+ '/node_modules/web-ifc/', // Прямо из node_modules
93
+ '/', // Корень dev-сервера
94
+ './', // Относительный путь
95
+ '' // Пусть библиотека сама определит путь
92
96
  );
93
97
 
94
98
  return paths;
package/src/main.js CHANGED
@@ -8,6 +8,492 @@ const app = document.getElementById("app");
8
8
  if (app) {
9
9
  const viewer = new Viewer(app);
10
10
  viewer.init();
11
+
12
+ // Панель свойств: тени
13
+ const shadowToggle = document.getElementById("shadowToggle");
14
+ const shadowGradToggle = document.getElementById("shadowGradToggle");
15
+ const shadowGradLen = document.getElementById("shadowGradLen");
16
+ const shadowGradLenValue = document.getElementById("shadowGradLenValue");
17
+ const shadowGradStr = document.getElementById("shadowGradStr");
18
+ const shadowGradStrValue = document.getElementById("shadowGradStrValue");
19
+ const shadowGradCurve = document.getElementById("shadowGradCurve");
20
+ const shadowGradCurveValue = document.getElementById("shadowGradCurveValue");
21
+ const shadowOpacity = document.getElementById("shadowOpacity");
22
+ const shadowOpacityValue = document.getElementById("shadowOpacityValue");
23
+ const shadowSoft = document.getElementById("shadowSoft");
24
+ const shadowSoftValue = document.getElementById("shadowSoftValue");
25
+ // Материалы
26
+ const matPreset = document.getElementById("matPreset");
27
+ const matRough = document.getElementById("matRough");
28
+ const matRoughValue = document.getElementById("matRoughValue");
29
+ const matMetal = document.getElementById("matMetal");
30
+ const matMetalValue = document.getElementById("matMetalValue");
31
+ // Визуал (диагностика)
32
+ const testPresetToggle = document.getElementById("testPresetToggle");
33
+ const rtQualityToggle = document.getElementById("rtQualityToggle");
34
+ const envToggle = document.getElementById("envToggle");
35
+ const envInt = document.getElementById("envInt");
36
+ const envIntValue = document.getElementById("envIntValue");
37
+ const toneToggle = document.getElementById("toneToggle");
38
+ const exposure = document.getElementById("exposure");
39
+ const exposureValue = document.getElementById("exposureValue");
40
+ const aoToggle = document.getElementById("aoToggle");
41
+ const aoInt = document.getElementById("aoInt");
42
+ const aoIntValue = document.getElementById("aoIntValue");
43
+ const aoRad = document.getElementById("aoRad");
44
+ const aoRadValue = document.getElementById("aoRadValue");
45
+ const dumpVisual = document.getElementById("dumpVisual");
46
+ // Цветокор
47
+ const ccToggle = document.getElementById("ccToggle");
48
+ const ccHue = document.getElementById("ccHue");
49
+ const ccHueValue = document.getElementById("ccHueValue");
50
+ const ccSat = document.getElementById("ccSat");
51
+ const ccSatValue = document.getElementById("ccSatValue");
52
+ const ccBri = document.getElementById("ccBri");
53
+ const ccBriValue = document.getElementById("ccBriValue");
54
+ const ccCon = document.getElementById("ccCon");
55
+ const ccConValue = document.getElementById("ccConValue");
56
+
57
+ // ===== Test preset ("Тест") - полностью изолированная настройка =====
58
+ const _testSnapshot = new Map();
59
+ const testSnapshotEl = (el) => {
60
+ if (!el) return;
61
+ _testSnapshot.set(el, {
62
+ checked: "checked" in el ? el.checked : undefined,
63
+ value: "value" in el ? el.value : undefined,
64
+ disabled: "disabled" in el ? el.disabled : undefined,
65
+ });
66
+ };
67
+ const testRestoreEl = (el) => {
68
+ if (!el) return;
69
+ const s = _testSnapshot.get(el);
70
+ if (!s) return;
71
+ if ("checked" in el && typeof s.checked === "boolean") el.checked = s.checked;
72
+ if ("value" in el && typeof s.value === "string") el.value = s.value;
73
+ if ("disabled" in el && typeof s.disabled === "boolean") el.disabled = s.disabled;
74
+ };
75
+
76
+ const getAllNonTestControls = () => ([
77
+ // Test preset toggle must stay enabled to allow turning it off
78
+ // Shadows + sun
79
+ shadowToggle, shadowGradToggle, shadowGradLen, shadowGradStr, shadowGradCurve, shadowOpacity, shadowSoft,
80
+ sunToggle, sunHeight,
81
+ // Materials
82
+ matPreset, matRough, matMetal,
83
+ // Visual
84
+ rtQualityToggle, envToggle, envInt, toneToggle, exposure, aoToggle, aoInt, aoRad,
85
+ dumpVisual,
86
+ // Color correction
87
+ ccToggle, ccHue, ccSat, ccBri, ccCon,
88
+ ].filter(Boolean));
89
+
90
+ const setDisabled = (el, disabled) => { if (el && "disabled" in el) el.disabled = !!disabled; };
91
+
92
+ const applyTestUiLock = (enabled) => {
93
+ getAllNonTestControls().forEach((el) => setDisabled(el, enabled));
94
+ // сам тест-переключатель не блокируем
95
+ if (testPresetToggle) setDisabled(testPresetToggle, false);
96
+ };
97
+
98
+ if (testPresetToggle) {
99
+ testPresetToggle.checked = false;
100
+ testPresetToggle.addEventListener("change", (e) => {
101
+ const on = !!e.target.checked;
102
+ if (on) {
103
+ _testSnapshot.clear();
104
+ // снимем снапшот со всех контролов, включая сам test (чтобы вернуть checked), но блокировать его не будем
105
+ [testPresetToggle, ...getAllNonTestControls()].forEach(testSnapshotEl);
106
+ viewer.setTestPresetEnabled?.(true);
107
+ applyTestUiLock(true);
108
+ } else {
109
+ viewer.setTestPresetEnabled?.(false);
110
+ // вернём UI
111
+ [testPresetToggle, ...getAllNonTestControls()].forEach(testRestoreEl);
112
+ applyTestUiLock(false);
113
+ }
114
+ });
115
+ }
116
+
117
+ // ===== Realtime-quality preset (UI master toggle) =====
118
+ const _rtSnapshot = new Map();
119
+ const snapshotEl = (el) => {
120
+ if (!el) return;
121
+ _rtSnapshot.set(el, {
122
+ checked: "checked" in el ? el.checked : undefined,
123
+ value: "value" in el ? el.value : undefined,
124
+ disabled: "disabled" in el ? el.disabled : undefined,
125
+ });
126
+ };
127
+ const restoreEl = (el) => {
128
+ if (!el) return;
129
+ const s = _rtSnapshot.get(el);
130
+ if (!s) return;
131
+ if ("checked" in el && typeof s.checked === "boolean") el.checked = s.checked;
132
+ if ("value" in el && typeof s.value === "string") el.value = s.value;
133
+ if ("disabled" in el && typeof s.disabled === "boolean") el.disabled = s.disabled;
134
+ };
135
+ // Важно: делаем это функцией, чтобы не попасть в TDZ для переменных,
136
+ // которые объявляются ниже (например, sunToggle/sunHeight).
137
+ const getRtManagedControls = () => ([
138
+ // Shadows + sun
139
+ shadowToggle, shadowGradToggle, shadowGradLen, shadowGradStr, shadowGradCurve, shadowOpacity, shadowSoft,
140
+ sunToggle, sunHeight,
141
+ // Materials
142
+ matPreset, matRough, matMetal,
143
+ // Visual
144
+ envToggle, envInt, toneToggle, exposure, aoToggle, aoInt, aoRad,
145
+ // Color correction
146
+ ccToggle, ccHue, ccSat, ccBri, ccCon,
147
+ ].filter(Boolean));
148
+
149
+ const applyRtQualityUiLock = (enabled) => {
150
+ // Блокируем все ручные контролы, кроме самого переключателя и кнопки dump
151
+ getRtManagedControls().forEach((el) => setDisabled(el, enabled));
152
+ };
153
+
154
+ if (rtQualityToggle) {
155
+ rtQualityToggle.checked = false;
156
+ rtQualityToggle.addEventListener("change", (e) => {
157
+ const on = !!e.target.checked;
158
+ if (on) {
159
+ // Снимем UI-снапшот, чтобы вернуть всё как было (включая disabled-состояния)
160
+ _rtSnapshot.clear();
161
+ getRtManagedControls().forEach(snapshotEl);
162
+ snapshotEl(dumpVisual); // dump не блокируем, но состояние тоже сохраним на всякий
163
+
164
+ viewer.setRealtimeQualityEnabled(true);
165
+ applyRtQualityUiLock(true);
166
+ } else {
167
+ viewer.setRealtimeQualityEnabled(false);
168
+ // Вернём UI
169
+ getRtManagedControls().forEach(restoreEl);
170
+ restoreEl(dumpVisual);
171
+ applyRtQualityUiLock(false);
172
+ }
173
+ });
174
+ }
175
+ if (shadowToggle) {
176
+ // Дефолт (из текущих подобранных значений)
177
+ shadowToggle.checked = true;
178
+ viewer.setShadowsEnabled(true);
179
+ shadowToggle.addEventListener("change", (e) => {
180
+ const on = !!e.target.checked;
181
+ viewer.setShadowsEnabled(on);
182
+ // UI градиента имеет смысл только когда тени включены
183
+ if (shadowGradToggle) shadowGradToggle.disabled = !on;
184
+ if (shadowGradLen) shadowGradLen.disabled = !on;
185
+ if (shadowGradStr) shadowGradStr.disabled = !on;
186
+ if (shadowGradCurve) shadowGradCurve.disabled = !on;
187
+ if (shadowOpacity) shadowOpacity.disabled = !on;
188
+ if (shadowSoft) shadowSoft.disabled = !on;
189
+ });
190
+ }
191
+ // Градиент тени: по умолчанию включён, но элементы блокируем пока тени выключены
192
+ const syncGradUiEnabled = (enabled) => {
193
+ if (shadowGradToggle) shadowGradToggle.disabled = !enabled;
194
+ if (shadowGradLen) shadowGradLen.disabled = !enabled;
195
+ if (shadowGradStr) shadowGradStr.disabled = !enabled;
196
+ if (shadowGradCurve) shadowGradCurve.disabled = !enabled;
197
+ if (shadowOpacity) shadowOpacity.disabled = !enabled;
198
+ if (shadowSoft) shadowSoft.disabled = !enabled;
199
+ };
200
+ syncGradUiEnabled(true);
201
+
202
+ if (shadowGradToggle) {
203
+ shadowGradToggle.checked = true;
204
+ viewer.setShadowGradientEnabled(true);
205
+ shadowGradToggle.addEventListener("change", (e) => {
206
+ viewer.setShadowGradientEnabled(!!e.target.checked);
207
+ });
208
+ }
209
+ if (shadowGradLen) {
210
+ shadowGradLen.value = "14.4";
211
+ if (shadowGradLenValue) shadowGradLenValue.textContent = "14.4";
212
+ viewer.setShadowGradientLength(14.4);
213
+ shadowGradLen.addEventListener("input", (e) => {
214
+ const v = Number(e.target.value);
215
+ if (shadowGradLenValue) shadowGradLenValue.textContent = v.toFixed(1);
216
+ viewer.setShadowGradientLength(v);
217
+ });
218
+ }
219
+ if (shadowGradStr) {
220
+ shadowGradStr.value = "1.00";
221
+ if (shadowGradStrValue) shadowGradStrValue.textContent = "1.00";
222
+ viewer.setShadowGradientStrength(1.0);
223
+ shadowGradStr.addEventListener("input", (e) => {
224
+ const v = Number(e.target.value);
225
+ if (shadowGradStrValue) shadowGradStrValue.textContent = v.toFixed(2);
226
+ viewer.setShadowGradientStrength(v);
227
+ });
228
+ }
229
+
230
+ if (shadowGradCurve) {
231
+ shadowGradCurve.value = "0.50";
232
+ if (shadowGradCurveValue) shadowGradCurveValue.textContent = "0.50";
233
+ viewer.setShadowGradientCurve(0.5);
234
+ shadowGradCurve.addEventListener("input", (e) => {
235
+ const v = Number(e.target.value);
236
+ if (shadowGradCurveValue) shadowGradCurveValue.textContent = v.toFixed(2);
237
+ viewer.setShadowGradientCurve(v);
238
+ });
239
+ }
240
+
241
+ // Полупрозрачность тени на земле
242
+ if (shadowOpacity) {
243
+ shadowOpacity.value = "0.14";
244
+ if (shadowOpacityValue) shadowOpacityValue.textContent = "0.14";
245
+ viewer.setShadowOpacity(0.14);
246
+ shadowOpacity.addEventListener("input", (e) => {
247
+ const v = Number(e.target.value);
248
+ if (shadowOpacityValue) shadowOpacityValue.textContent = v.toFixed(2);
249
+ viewer.setShadowOpacity(v);
250
+ });
251
+ }
252
+
253
+ // Мягкость края тени
254
+ if (shadowSoft) {
255
+ shadowSoft.value = "0.0";
256
+ if (shadowSoftValue) shadowSoftValue.textContent = "0.0";
257
+ viewer.setShadowSoftness(0.0);
258
+ shadowSoft.addEventListener("input", (e) => {
259
+ const v = Number(e.target.value);
260
+ if (shadowSoftValue) shadowSoftValue.textContent = v.toFixed(1);
261
+ viewer.setShadowSoftness(v);
262
+ });
263
+ }
264
+
265
+ // Панель свойств: солнце (глобальное освещение)
266
+ const sunToggle = document.getElementById("sunToggle");
267
+ const sunHeight = document.getElementById("sunHeight");
268
+ const sunHeightValue = document.getElementById("sunHeightValue");
269
+ if (sunToggle) {
270
+ // По умолчанию включено
271
+ sunToggle.checked = true;
272
+ viewer.setSunEnabled(true);
273
+ sunToggle.addEventListener("change", (e) => {
274
+ const on = !!e.target.checked;
275
+ viewer.setSunEnabled(on);
276
+ if (sunHeight) sunHeight.disabled = !on;
277
+ });
278
+ }
279
+ if (sunHeight) {
280
+ // Дефолт (из текущих подобранных значений)
281
+ sunHeight.value = "5.9";
282
+ if (sunHeightValue) sunHeightValue.textContent = "5.9";
283
+ viewer.setSunHeight(5.9);
284
+ sunHeight.disabled = !(sunToggle ? !!sunToggle.checked : true);
285
+ sunHeight.addEventListener("input", (e) => {
286
+ const v = Number(e.target.value);
287
+ if (sunHeightValue) sunHeightValue.textContent = v.toFixed(1);
288
+ viewer.setSunHeight(v);
289
+ });
290
+ }
291
+
292
+ // ===== Материалы =====
293
+ const MAT_DEFAULTS = {
294
+ original: { roughness: 0.90, metalness: 0.00, slidersEnabled: false },
295
+ matte: { roughness: 0.90, metalness: 0.00, slidersEnabled: true },
296
+ glossy: { roughness: 0.05, metalness: 0.00, slidersEnabled: true },
297
+ // Важно: "пластик" не должен быть металлом, иначе появятся резкие блики и "дёрганая" картинка при вращении
298
+ plastic: { roughness: 0.65, metalness: 0.00, slidersEnabled: true },
299
+ concrete: { roughness: 0.95, metalness: 0.00, slidersEnabled: true },
300
+ };
301
+
302
+ const setMatUiEnabled = (enabled) => {
303
+ if (matRough) matRough.disabled = !enabled;
304
+ if (matMetal) matMetal.disabled = !enabled;
305
+ };
306
+
307
+ const applyMatPresetUi = (preset) => {
308
+ const d = MAT_DEFAULTS[preset] || MAT_DEFAULTS.original;
309
+ if (matRough) matRough.value = String(d.roughness.toFixed(2));
310
+ if (matRoughValue) matRoughValue.textContent = d.roughness.toFixed(2);
311
+ if (matMetal) matMetal.value = String(d.metalness.toFixed(2));
312
+ if (matMetalValue) matMetalValue.textContent = d.metalness.toFixed(2);
313
+ setMatUiEnabled(d.slidersEnabled);
314
+ };
315
+
316
+ if (matPreset) {
317
+ // Дефолт: Пластик (как на скрине)
318
+ matPreset.value = "plastic";
319
+ applyMatPresetUi("plastic");
320
+ viewer.setMaterialPreset("plastic");
321
+ viewer.setMaterialRoughness(MAT_DEFAULTS.plastic.roughness);
322
+ viewer.setMaterialMetalness(MAT_DEFAULTS.plastic.metalness);
323
+ matPreset.addEventListener("change", (e) => {
324
+ const preset = e.target.value;
325
+ viewer.setMaterialPreset(preset);
326
+ applyMatPresetUi(preset);
327
+ // применяем дефолтные параметры пресета как override
328
+ const d = MAT_DEFAULTS[preset] || MAT_DEFAULTS.original;
329
+ if (d.slidersEnabled) {
330
+ viewer.setMaterialRoughness(d.roughness);
331
+ viewer.setMaterialMetalness(d.metalness);
332
+ } else {
333
+ viewer.setMaterialRoughness(null);
334
+ viewer.setMaterialMetalness(null);
335
+ }
336
+ });
337
+ }
338
+
339
+ if (matRough) {
340
+ matRough.addEventListener("input", (e) => {
341
+ const v = Number(e.target.value);
342
+ if (matRoughValue) matRoughValue.textContent = v.toFixed(2);
343
+ viewer.setMaterialRoughness(v);
344
+ });
345
+ }
346
+ if (matMetal) {
347
+ matMetal.addEventListener("input", (e) => {
348
+ const v = Number(e.target.value);
349
+ if (matMetalValue) matMetalValue.textContent = v.toFixed(2);
350
+ viewer.setMaterialMetalness(v);
351
+ });
352
+ }
353
+
354
+ // ===== Визуал (диагностика) =====
355
+ const syncVisualUiEnabled = () => {
356
+ const envOn = !!(envToggle && envToggle.checked);
357
+ const toneOn = !!(toneToggle && toneToggle.checked);
358
+ const aoOn = !!(aoToggle && aoToggle.checked);
359
+ if (envInt) envInt.disabled = !envOn;
360
+ if (exposure) exposure.disabled = !toneOn;
361
+ if (aoInt) aoInt.disabled = !aoOn;
362
+ if (aoRad) aoRad.disabled = !aoOn;
363
+ };
364
+
365
+ const syncCcUiEnabled = () => {
366
+ const on = !!(ccToggle && ccToggle.checked);
367
+ if (ccHue) ccHue.disabled = !on;
368
+ if (ccSat) ccSat.disabled = !on;
369
+ if (ccBri) ccBri.disabled = !on;
370
+ if (ccCon) ccCon.disabled = !on;
371
+ };
372
+
373
+ // Дефолты (как на скрине)
374
+ if (envToggle) {
375
+ envToggle.checked = true;
376
+ viewer.setEnvironmentEnabled(true);
377
+ envToggle.addEventListener("change", (e) => {
378
+ viewer.setEnvironmentEnabled(!!e.target.checked);
379
+ syncVisualUiEnabled();
380
+ });
381
+ }
382
+ if (envInt) {
383
+ envInt.value = "0.65";
384
+ if (envIntValue) envIntValue.textContent = "0.65";
385
+ viewer.setEnvironmentIntensity(0.65);
386
+ envInt.addEventListener("input", (e) => {
387
+ const v = Number(e.target.value);
388
+ if (envIntValue) envIntValue.textContent = v.toFixed(2);
389
+ viewer.setEnvironmentIntensity(v);
390
+ });
391
+ }
392
+
393
+ if (toneToggle) {
394
+ toneToggle.checked = true;
395
+ viewer.setToneMappingEnabled(true);
396
+ toneToggle.addEventListener("change", (e) => {
397
+ viewer.setToneMappingEnabled(!!e.target.checked);
398
+ syncVisualUiEnabled();
399
+ });
400
+ }
401
+ if (exposure) {
402
+ exposure.value = "1.11";
403
+ if (exposureValue) exposureValue.textContent = "1.11";
404
+ viewer.setExposure(1.11);
405
+ exposure.addEventListener("input", (e) => {
406
+ const v = Number(e.target.value);
407
+ if (exposureValue) exposureValue.textContent = v.toFixed(2);
408
+ viewer.setExposure(v);
409
+ });
410
+ }
411
+
412
+ if (aoToggle) {
413
+ aoToggle.checked = true;
414
+ viewer.setAOEnabled(true);
415
+ aoToggle.addEventListener("change", (e) => {
416
+ viewer.setAOEnabled(!!e.target.checked);
417
+ syncVisualUiEnabled();
418
+ });
419
+ }
420
+ if (aoInt) {
421
+ aoInt.value = "0.52";
422
+ if (aoIntValue) aoIntValue.textContent = "0.52";
423
+ viewer.setAOIntensity(0.52);
424
+ aoInt.addEventListener("input", (e) => {
425
+ const v = Number(e.target.value);
426
+ if (aoIntValue) aoIntValue.textContent = v.toFixed(2);
427
+ viewer.setAOIntensity(v);
428
+ });
429
+ }
430
+ if (aoRad) {
431
+ aoRad.value = "8";
432
+ if (aoRadValue) aoRadValue.textContent = "8";
433
+ viewer.setAORadius(8);
434
+ aoRad.addEventListener("input", (e) => {
435
+ const v = Number(e.target.value);
436
+ if (aoRadValue) aoRadValue.textContent = String(Math.round(v));
437
+ viewer.setAORadius(v);
438
+ });
439
+ }
440
+
441
+ syncVisualUiEnabled();
442
+
443
+ if (dumpVisual) {
444
+ dumpVisual.addEventListener("click", () => viewer.dumpVisualDebug());
445
+ }
446
+
447
+ // ===== Цветокор =====
448
+ if (ccToggle) {
449
+ ccToggle.checked = false;
450
+ viewer.setColorCorrectionEnabled(false);
451
+ ccToggle.addEventListener("change", (e) => {
452
+ viewer.setColorCorrectionEnabled(!!e.target.checked);
453
+ syncCcUiEnabled();
454
+ });
455
+ }
456
+ if (ccHue) {
457
+ ccHue.value = "0.00";
458
+ if (ccHueValue) ccHueValue.textContent = "0.00";
459
+ viewer.setColorHue(0.0);
460
+ ccHue.addEventListener("input", (e) => {
461
+ const v = Number(e.target.value);
462
+ if (ccHueValue) ccHueValue.textContent = v.toFixed(2);
463
+ viewer.setColorHue(v);
464
+ });
465
+ }
466
+ if (ccSat) {
467
+ ccSat.value = "0.00";
468
+ if (ccSatValue) ccSatValue.textContent = "0.00";
469
+ viewer.setColorSaturation(0.0);
470
+ ccSat.addEventListener("input", (e) => {
471
+ const v = Number(e.target.value);
472
+ if (ccSatValue) ccSatValue.textContent = v.toFixed(2);
473
+ viewer.setColorSaturation(v);
474
+ });
475
+ }
476
+ if (ccBri) {
477
+ ccBri.value = "0.00";
478
+ if (ccBriValue) ccBriValue.textContent = "0.00";
479
+ viewer.setColorBrightness(0.0);
480
+ ccBri.addEventListener("input", (e) => {
481
+ const v = Number(e.target.value);
482
+ if (ccBriValue) ccBriValue.textContent = v.toFixed(2);
483
+ viewer.setColorBrightness(v);
484
+ });
485
+ }
486
+ if (ccCon) {
487
+ ccCon.value = "0.00";
488
+ if (ccConValue) ccConValue.textContent = "0.00";
489
+ viewer.setColorContrast(0.0);
490
+ ccCon.addEventListener("input", (e) => {
491
+ const v = Number(e.target.value);
492
+ if (ccConValue) ccConValue.textContent = v.toFixed(2);
493
+ viewer.setColorContrast(v);
494
+ });
495
+ }
496
+ syncCcUiEnabled();
11
497
  // IFC загрузка
12
498
  const ifc = new IfcService(viewer);
13
499
  ifc.init();
@@ -52,11 +538,13 @@ if (app) {
52
538
  const qualLow = document.getElementById("qualLow");
53
539
  const qualMed = document.getElementById("qualMed");
54
540
  const qualHigh = document.getElementById("qualHigh");
55
- const toggleEdges = document.getElementById("toggleEdges");
541
+ // Нижний тулбар пакета (index.html): Edges
542
+ const toggleEdges = document.getElementById("ifcToggleEdges");
56
543
  const toggleShading = document.getElementById("toggleShading");
57
- const clipXBtn = document.getElementById("clipX");
58
- const clipYBtn = document.getElementById("clipY");
59
- const clipZBtn = document.getElementById("clipZ");
544
+ // Нижний тулбар пакета (index.html): секущие плоскости
545
+ const clipXBtn = document.getElementById("ifcClipX");
546
+ const clipYBtn = document.getElementById("ifcClipY");
547
+ const clipZBtn = document.getElementById("ifcClipZ");
60
548
  const clipXRange = document.getElementById("clipXRange");
61
549
  const clipYRange = document.getElementById("clipYRange");
62
550
  const clipZRange = document.getElementById("clipZRange");
@@ -69,7 +557,9 @@ if (app) {
69
557
  qualMed?.addEventListener("click", () => { viewer.setQuality('medium'); setActive(qualMed); });
70
558
  qualHigh?.addEventListener("click", () => { viewer.setQuality('high'); setActive(qualHigh); });
71
559
 
72
- let edgesOn = true;
560
+ // Рёбра по умолчанию выключены
561
+ let edgesOn = false;
562
+ viewer.setEdgesVisible(edgesOn);
73
563
  toggleEdges?.addEventListener("click", () => { edgesOn = !edgesOn; viewer.setEdgesVisible(edgesOn); });
74
564
  let flatOn = true;
75
565
  toggleShading?.addEventListener("click", () => { flatOn = !flatOn; viewer.setFlatShading(flatOn); });