@sequent-org/ifc-viewer 1.2.4-ci.26.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 +18 -0
- package/package.json +1 -1
- package/src/IfcViewer.js +11 -2
- package/src/ifc/IfcService.js +10 -6
- package/src/main.js +489 -1
- package/src/viewer/Viewer.js +1471 -16
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.
|
|
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
|
@@ -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:
|
|
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();
|
|
@@ -298,7 +307,7 @@ export class IfcViewer {
|
|
|
298
307
|
|
|
299
308
|
<!-- Стили отображения -->
|
|
300
309
|
<div class="join">
|
|
301
|
-
<button class="btn btn-sm join-item
|
|
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>
|
|
302
311
|
</div>
|
|
303
312
|
|
|
304
313
|
<!-- Секущие плоскости -->
|
package/src/ifc/IfcService.js
CHANGED
|
@@ -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
|
-
'/
|
|
88
|
-
'/
|
|
89
|
-
'/
|
|
90
|
-
'./
|
|
91
|
-
'
|
|
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
|
-
|
|
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); });
|