@sequent-org/ifc-viewer 1.2.4-ci.25.0 → 1.2.4-ci.27.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.25.0",
4
+ "version": "1.2.4-ci.27.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,18 +13,8 @@
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 './style.css';
16
17
 
17
- // Загружаем стили при импорте IfcViewer
18
- if (typeof window !== 'undefined' && !document.querySelector('style[data-ifc-viewer]')) {
19
- import('./style.css').then(() => {
20
- // Добавляем маркер, что стили загружены
21
- const style = document.createElement('style');
22
- style.setAttribute('data-ifc-viewer', 'loaded');
23
- document.head.appendChild(style);
24
- }).catch(error => {
25
- console.warn('IfcViewer: не удалось загрузить стили:', error.message);
26
- });
27
- }
28
18
 
29
19
  export class IfcViewer {
30
20
  /**
@@ -34,6 +24,7 @@ export class IfcViewer {
34
24
  * @param {string} [options.ifcUrl] - URL для загрузки IFC файла
35
25
  * @param {File} [options.ifcFile] - File объект для загрузки IFC файла
36
26
  * @param {string} [options.wasmUrl] - URL для загрузки WASM файла web-ifc
27
+ * @param {boolean} [options.useTestPreset=true] - Включать ли пресет "Тест" по умолчанию (рекомендованные тени/визуал)
37
28
  * @param {boolean} [options.showSidebar=false] - Показывать ли боковую панель с деревом
38
29
  * @param {boolean} [options.showControls=false] - Показывать ли панель управления (нижние кнопки)
39
30
  * @param {boolean} [options.showToolbar=true] - Показывать ли верхнюю панель инструментов
@@ -61,6 +52,8 @@ export class IfcViewer {
61
52
  ifcUrl: options.ifcUrl || null,
62
53
  ifcFile: options.ifcFile || null,
63
54
  wasmUrl: options.wasmUrl || null,
55
+ // По умолчанию включаем пресет "Тест" для корректного вида теней (как в демо-настройках)
56
+ useTestPreset: options.useTestPreset !== false,
64
57
  showSidebar: options.showSidebar === true, // по умолчанию false
65
58
  showControls: options.showControls === true, // по умолчанию false
66
59
  showToolbar: options.showToolbar !== false, // по умолчанию true
@@ -90,7 +83,7 @@ export class IfcViewer {
90
83
  // Внутренние состояния управления
91
84
  this.viewerState = {
92
85
  quality: 'medium', // 'low' | 'medium' | 'high'
93
- edgesVisible: true,
86
+ edgesVisible: false,
94
87
  flatShading: true,
95
88
  clipping: {
96
89
  x: false,
@@ -122,6 +115,12 @@ export class IfcViewer {
122
115
  this._initViewer();
123
116
  this._initIfcService();
124
117
  this._initTreeView();
118
+
119
+ // Применяем дефолтный пресет пакета (полностью независим от index.html)
120
+ // Важно: пресет должен примениться ДО загрузки модели, чтобы настройки подхватились при replaceWithModel()
121
+ if (this.options.useTestPreset && this.viewer?.setTestPresetEnabled) {
122
+ this.viewer.setTestPresetEnabled(true);
123
+ }
125
124
 
126
125
  // Настраиваем обработчики событий
127
126
  this._setupEventHandlers();
@@ -308,14 +307,14 @@ export class IfcViewer {
308
307
 
309
308
  <!-- Стили отображения -->
310
309
  <div class="join">
311
- <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>
312
311
  </div>
313
312
 
314
313
  <!-- Секущие плоскости -->
315
314
  <div class="join">
316
315
  <button class="btn btn-sm join-item" id="ifcClipX" style="margin-right:2px"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" class="MuiSvgIcon-root MuiSvgIcon-fontSizeLarge" focusable="false" viewBox="0 0 24 24" aria-hidden="true"><path d="M9.6 3v9.07l3.87-1.72a2 2 0 01.81-.17 2.08 2.08 0 011.77 3.09 1.09 1.09 0 01-.56.56l-4.36 1.94L21.6 21V9z"></path><path d="M4.74 15.33l9.14-4.07a1 1 0 011.32.51 1 1 0 01-.51 1.32l-9.14 4.07 4 1.52L9 20l-6.6-2.53 2.53-6.6 1.32.51z"></path></svg></button>
317
316
  <button class="btn btn-sm join-item" id="ifcClipZ" style="margin-right:2px"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" class="MuiSvgIcon-root MuiSvgIcon-fontSizeLarge" focusable="false" viewBox="0 0 24 24" aria-hidden="true"><path d="M8 13.82a1.09 1.09 0 01-.56-.56 2.08 2.08 0 011.78-3.09 2 2 0 01.81.17l3.87 1.72V3l-11 6v12l9.54-5.2z"></path><path d="M17.24 11.37l1.32-.51 2.53 6.6L14.5 20l-.5-1.32 4-1.52-9.18-4.07a1 1 0 01-.51-1.32 1 1 0 011.32-.51l9.14 4.07z"></path></svg></button>
318
- <button class="btn btn-sm join-item" id="ifcClipY" style="margin-right:2px"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" class="MuiSvgIcon-root MuiSvgIcon-fontSizeLarge" focusable="false" viewBox="0 0 24 24" aria-hidden="true"><path d="M14.25 11.18v3.52A1.87 1.87 0 0111 15.88a1 1 0 01-.32-.72V11.1l-9 4.5L12.45 21l9.9-5.4z"></path><path d="M8.85 8.4L8 7.5 12.45 3 17 7.5l-.9.9-2.7-2.7v9a.9.9 0 01-.9.9.9.9 0 01-.9-.9v-9z"></path></svg></button>
317
+ <button class="btn btn-sm join-item" id="ifcClipY" style="margin-right:0px"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" class="MuiSvgIcon-root MuiSvgIcon-fontSizeLarge" focusable="false" viewBox="0 0 24 24" aria-hidden="true"><path d="M14.25 11.18v3.52A1.87 1.87 0 0111 15.88a1 1 0 01-.32-.72V11.1l-9 4.5L12.45 21l9.9-5.4z"></path><path d="M8.85 8.4L8 7.5 12.45 3 17 7.5l-.9.9-2.7-2.7v9a.9.9 0 01-.9.9.9.9 0 01-.9-.9v-9z"></path></svg></button>
319
318
  </div>
320
319
 
321
320
  </div>
@@ -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();
@@ -69,7 +555,9 @@ if (app) {
69
555
  qualMed?.addEventListener("click", () => { viewer.setQuality('medium'); setActive(qualMed); });
70
556
  qualHigh?.addEventListener("click", () => { viewer.setQuality('high'); setActive(qualHigh); });
71
557
 
72
- let edgesOn = true;
558
+ // Рёбра по умолчанию выключены
559
+ let edgesOn = false;
560
+ viewer.setEdgesVisible(edgesOn);
73
561
  toggleEdges?.addEventListener("click", () => { edgesOn = !edgesOn; viewer.setEdgesVisible(edgesOn); });
74
562
  let flatOn = true;
75
563
  toggleShading?.addEventListener("click", () => { flatOn = !flatOn; viewer.setFlatShading(flatOn); });