@sequent-org/ifc-viewer 1.2.4-ci.30.0 → 1.2.4-ci.32.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/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.30.0",
4
+ "version": "1.2.4-ci.32.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
@@ -121,6 +121,15 @@ export class IfcViewer {
121
121
  // Важно: пресет должен примениться ДО загрузки модели, чтобы настройки подхватились при replaceWithModel()
122
122
  if (this.options.useTestPreset && this.viewer?.setTestPresetEnabled) {
123
123
  this.viewer.setTestPresetEnabled(true);
124
+ // Дефолты пакета (подобранные значения для Autodesk-like вида)
125
+ // В пакете применяются всегда при включённом useTestPreset (по умолчанию true).
126
+ try { this.viewer.setExposure?.(1.19); } catch (_) {}
127
+ try { this.viewer.setCoolLightingEnabled?.(true); } catch (_) {}
128
+ try { this.viewer.setCoolLightingHue?.(240); } catch (_) {}
129
+ try { this.viewer.setCoolLightingAmount?.(1.00); } catch (_) {}
130
+ try { this.viewer.setStep4Enabled?.(true); } catch (_) {}
131
+ try { this.viewer.setStep4Contrast?.(1.35); } catch (_) {}
132
+ try { this.viewer.setStep4Saturation?.(1.60); } catch (_) {}
124
133
  }
125
134
 
126
135
  // Настраиваем обработчики событий
@@ -317,17 +326,17 @@ export class IfcViewer {
317
326
  </button>
318
327
  <button class="btn btn-sm join-item" id="ifcToggleProjection" title="Перспектива / Ортогонально (переключение)">
319
328
  <!-- По умолчанию Ortho, поэтому показываем действие: включить Perspective -->
320
- <svg width="24" height="24" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
321
- <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"/>
322
- <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"/>
323
- <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"/>
324
- <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"/>
325
- <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"/>
326
- <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"/>
327
- <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"/>
328
- <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"/>
329
- <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"/>
330
- <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"/>
329
+ <svg width="24" height="24" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" fill="currentColor" style="color:#252A3F">
330
+ <path d="M 365.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"/>
331
+ <path 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"/>
332
+ <path 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"/>
333
+ <path 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"/>
334
+ <path 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"/>
335
+ <path 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"/>
336
+ <path 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"/>
337
+ <path 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"/>
338
+ <path 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"/>
339
+ <path 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"/>
331
340
  </svg>
332
341
  </button>
333
342
  </div>
@@ -730,13 +739,22 @@ export class IfcViewer {
730
739
 
731
740
  // Иконки: показываем альтернативный режим
732
741
  const ICON_PERSPECTIVE = `
733
- <svg width="24" height="24" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
734
- <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"/>
742
+ <svg width="24" height="24" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" fill="currentColor" style="color:#252A3F">
743
+ <path d="M 365.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"/>
744
+ <path 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"/>
745
+ <path 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"/>
746
+ <path 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"/>
747
+ <path 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"/>
748
+ <path 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"/>
749
+ <path 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"/>
750
+ <path 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"/>
751
+ <path 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"/>
752
+ <path 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"/>
735
753
  </svg>
736
754
  `;
737
755
  const ICON_ORTHO = `
738
- <svg width="24" height="24" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
739
- <path fill="#000000" d="M256.02 48.55 Q 257.33 48.55 258.06 48.94 Q 381.49 115.11 442.91 148.14 Q 445.24 149.39 445.26 152.25 Q 445.52 184.71 445.52 256.00 Q 445.52 327.29 445.26 359.75 Q 445.24 362.61 442.91 363.86 Q 381.49 396.89 258.06 463.06 Q 257.33 463.45 256.02 463.45 Q 254.71 463.45 253.98 463.06 Q 130.55 396.89 69.13 363.86 Q 66.80 362.61 66.78 359.75 Q 66.52 327.29 66.52 256.00 Q 66.52 184.71 66.78 152.25 Q 66.80 149.39 69.13 148.14 Q 130.55 115.11 253.98 48.94 Q 254.71 48.55 256.02 48.55 Z"/>
756
+ <svg width="24" height="24" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" fill="currentColor" style="color:#252A3F">
757
+ <path d="M 256.02 48.55 Q 257.33 48.55 258.06 48.94 Q 381.49 115.11 442.91 148.14 Q 445.24 149.39 445.26 152.25 Q 445.52 184.71 445.52 256.00 Q 445.52 327.29 445.26 359.75 Q 445.24 362.61 442.91 363.86 Q 381.49 396.89 258.06 463.06 Q 257.33 463.45 256.02 463.45 Q 254.71 463.45 253.98 463.06 Q 130.55 396.89 69.13 363.86 Q 66.80 362.61 66.78 359.75 Q 66.52 327.29 66.52 256.00 Q 66.52 184.71 66.78 152.25 Q 66.80 149.39 69.13 148.14 Q 130.55 115.11 253.98 48.94 Q 254.71 48.55 256.02 48.55 Z M 256.03 147.56 Q 257.36 147.56 258.05 147.94 Q 295.68 168.96 347.89 198.33 A 0.77 0.75 44.3 0 0 348.62 198.33 L 429.62 152.89 A 0.32 0.32 0.0 0 0 429.62 152.33 Q 332.33 100.05 256.30 59.37 Q 256.25 59.35 256.15 59.34 Q 256.09 59.33 256.02 59.33 Q 255.97 59.33 255.90 59.34 Q 255.81 59.35 255.76 59.37 Q 179.73 100.05 82.44 152.34 A 0.32 0.32 0.0 0 0 82.44 152.90 L 163.44 198.34 A 0.77 0.75 -44.3 0 0 164.17 198.34 Q 216.38 168.96 254.01 147.94 Q 254.70 147.56 256.03 147.56 Z M 255.82 250.17 A 0.38 0.38 0.0 0 0 256.20 250.17 L 337.45 204.58 A 0.38 0.38 0.0 0 0 337.45 203.92 L 256.20 158.33 A 0.38 0.38 0.0 0 0 255.82 158.33 L 174.57 203.92 A 0.38 0.38 0.0 0 0 174.57 204.58 L 255.82 250.17 Z M 76.99 161.29 A 0.33 0.33 0.0 0 0 76.50 161.58 L 76.50 246.92 A 0.33 0.33 0.0 0 0 76.99 247.21 L 153.06 204.54 A 0.33 0.33 0.0 0 0 153.06 203.96 L 76.99 161.29 Z M 434.97 247.14 A 0.35 0.35 0.0 0 0 435.49 246.83 L 435.49 161.67 A 0.35 0.35 0.0 0 0 434.97 161.36 L 359.05 203.94 A 0.35 0.35 0.0 0 0 359.05 204.56 L 434.97 247.14 Z M 245.33 256.28 A 0.32 0.32 0.0 0 0 245.33 255.72 L 163.96 210.07 A 0.32 0.32 0.0 0 0 163.64 210.07 L 82.27 255.72 A 0.32 0.32 0.0 0 0 82.27 256.28 L 163.64 301.93 A 0.32 0.32 0.0 0 0 163.96 301.93 L 245.33 256.28 Z M 429.83 256.28 A 0.32 0.32 0.0 0 0 429.83 255.72 L 348.46 210.07 A 0.32 0.32 0.0 0 0 348.14 210.07 L 266.77 255.72 A 0.32 0.32 0.0 0 0 266.77 256.28 L 348.14 301.93 A 0.32 0.32 0.0 0 0 348.46 301.93 L 429.83 256.28 Z M 337.56 308.04 A 0.33 0.33 0.0 0 0 337.56 307.46 L 256.20 261.82 A 0.33 0.33 0.0 0 0 255.88 261.82 L 174.51 307.46 A 0.33 0.33 0.0 0 0 174.51 308.04 L 255.87 353.68 A 0.33 0.33 0.0 0 0 256.19 353.68 L 337.56 308.04 Z M 76.96 264.77 A 0.31 0.31 0.0 0 0 76.50 265.04 L 76.50 350.46 A 0.31 0.31 0.0 0 0 76.96 350.73 L 153.09 308.02 A 0.31 0.31 0.0 0 0 153.09 307.48 L 76.96 264.77 Z M 434.97 350.63 A 0.35 0.35 0.0 0 0 435.49 350.33 L 435.49 265.17 A 0.35 0.35 0.0 0 0 434.97 264.87 L 359.05 307.44 A 0.35 0.35 0.0 0 0 359.05 308.06 L 434.97 350.63 Z M 256.02 364.45 Q 254.69 364.45 254.00 364.06 Q 216.37 343.04 164.17 313.67 A 0.77 0.75 44.3 0 0 163.44 313.67 L 82.44 359.10 A 0.32 0.32 0.0 0 0 82.44 359.66 Q 179.72 411.94 255.74 452.63 Q 255.79 452.65 255.89 452.66 Q 255.96 452.67 256.00 452.67 Q 256.07 452.67 256.14 452.66 Q 256.24 452.65 256.29 452.63 Q 332.31 411.95 429.60 359.67 A 0.32 0.32 0.0 0 0 429.60 359.11 L 348.60 313.68 A 0.77 0.75 -44.3 0 0 347.87 313.68 Q 295.66 343.04 258.03 364.06 Q 257.35 364.45 256.02 364.45 Z"/>
740
758
  </svg>
741
759
  `;
742
760
 
package/src/main.js CHANGED
@@ -9,496 +9,277 @@ if (app) {
9
9
  const viewer = new Viewer(app);
10
10
  viewer.init();
11
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
- };
12
+ // ===== Левая панель: временно оставляем только "Тест" =====
13
+ // Остальные контролы (тени/солнце/материалы/визуал/цветокор) будем добавлять пошагово позже.
14
+ // (Старый код управления панелью закомментирован ниже для последующего восстановления при необходимости.)
75
15
 
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);
16
+ const testPresetToggle = document.getElementById("testPresetToggle");
17
+ // Шаг 1 (Tone mapping): в текущей версии он входит в пресет "Тест", но exposure можно подстроить.
18
+ const step1ToneToggle = document.getElementById("step1ToneToggle");
19
+ const step1Exposure = document.getElementById("step1Exposure");
20
+ const step1ExposureValue = document.getElementById("step1ExposureValue");
21
+ const step1Dump = document.getElementById("step1Dump");
22
+ // Шаг 2 (свет): холодный оттенок
23
+ const step2CoolLighting = document.getElementById("step2CoolLighting");
24
+ const step2Hue = document.getElementById("step2Hue");
25
+ const step2HueValue = document.getElementById("step2HueValue");
26
+ const step2Blue = document.getElementById("step2Blue");
27
+ const step2BlueValue = document.getElementById("step2BlueValue");
28
+ const step2Dump = document.getElementById("step2Dump");
29
+ // Шаг 3 (фон)
30
+ const step3Background = document.getElementById("step3Background");
31
+ const step3Dump = document.getElementById("step3Dump");
32
+ // Шаг 4 (пост-обработка: контраст + насыщенность)
33
+ const step4Post = document.getElementById("step4Post");
34
+ const step4Contrast = document.getElementById("step4Contrast");
35
+ const step4ContrastValue = document.getElementById("step4ContrastValue");
36
+ const step4Saturation = document.getElementById("step4Saturation");
37
+ const step4SaturationValue = document.getElementById("step4SaturationValue");
38
+ const step4Dump = document.getElementById("step4Dump");
39
+
40
+ const setStep1UiEnabled = (enabled) => {
41
+ const on = !!enabled;
42
+ if (step1ToneToggle) step1ToneToggle.checked = on;
43
+ if (step1Exposure) step1Exposure.disabled = !on;
44
+ if (step1Dump) step1Dump.disabled = !on;
96
45
  };
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
- });
46
+ const setStep2UiEnabled = (enabled) => {
47
+ const on = !!enabled;
48
+ if (step2CoolLighting) step2CoolLighting.disabled = !on;
49
+ if (step2Hue) step2Hue.disabled = !on || !(step2CoolLighting && step2CoolLighting.checked);
50
+ if (step2Blue) step2Blue.disabled = !on || !(step2CoolLighting && step2CoolLighting.checked);
51
+ if (step2Dump) step2Dump.disabled = !on;
52
+ if (!on && step2CoolLighting) step2CoolLighting.checked = false;
126
53
  };
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;
54
+ const setStep3UiEnabled = (enabled) => {
55
+ const on = !!enabled;
56
+ if (step3Background) step3Background.disabled = !on;
57
+ if (step3Dump) step3Dump.disabled = !on;
58
+ if (!on && step3Background) step3Background.checked = false;
134
59
  };
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));
60
+ const setStep4UiEnabled = (enabled) => {
61
+ const on = !!enabled;
62
+ if (step4Post) step4Post.disabled = !on;
63
+ const step4On = !!(step4Post && step4Post.checked);
64
+ if (step4Contrast) step4Contrast.disabled = !on || !step4On;
65
+ if (step4Saturation) step4Saturation.disabled = !on || !step4On;
66
+ if (step4Dump) step4Dump.disabled = !on;
67
+ if (!on) {
68
+ if (step4Post) step4Post.checked = false;
69
+ if (step4Contrast) step4Contrast.disabled = true;
70
+ if (step4Saturation) step4Saturation.disabled = true;
71
+ }
152
72
  };
153
73
 
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
- // синхронизируем тулбар-кнопку, если есть
180
- const _btn = document.getElementById("ifcToggleShadows");
181
- if (_btn) _btn.classList.toggle('btn-active', true);
182
- shadowToggle.addEventListener("change", (e) => {
183
- const on = !!e.target.checked;
184
- viewer.setShadowsEnabled(on);
185
- const btn = document.getElementById("ifcToggleShadows");
186
- if (btn) btn.classList.toggle('btn-active', on);
187
- // UI градиента имеет смысл только когда тени включены
188
- if (shadowGradToggle) shadowGradToggle.disabled = !on;
189
- if (shadowGradLen) shadowGradLen.disabled = !on;
190
- if (shadowGradStr) shadowGradStr.disabled = !on;
191
- if (shadowGradCurve) shadowGradCurve.disabled = !on;
192
- if (shadowOpacity) shadowOpacity.disabled = !on;
193
- if (shadowSoft) shadowSoft.disabled = !on;
194
- });
195
- }
196
- // Градиент тени: по умолчанию включён, но элементы блокируем пока тени выключены
197
- const syncGradUiEnabled = (enabled) => {
198
- if (shadowGradToggle) shadowGradToggle.disabled = !enabled;
199
- if (shadowGradLen) shadowGradLen.disabled = !enabled;
200
- if (shadowGradStr) shadowGradStr.disabled = !enabled;
201
- if (shadowGradCurve) shadowGradCurve.disabled = !enabled;
202
- if (shadowOpacity) shadowOpacity.disabled = !enabled;
203
- if (shadowSoft) shadowSoft.disabled = !enabled;
74
+ const applyStep2Hue = (deg) => {
75
+ const v = Math.round(Number(deg));
76
+ if (!Number.isFinite(v)) return;
77
+ if (step2HueValue) step2HueValue.textContent = String(v);
78
+ try { viewer.setCoolLightingHue?.(v); } catch (_) {}
204
79
  };
205
- syncGradUiEnabled(true);
206
-
207
- if (shadowGradToggle) {
208
- shadowGradToggle.checked = true;
209
- viewer.setShadowGradientEnabled(true);
210
- shadowGradToggle.addEventListener("change", (e) => {
211
- viewer.setShadowGradientEnabled(!!e.target.checked);
212
- });
213
- }
214
- if (shadowGradLen) {
215
- shadowGradLen.value = "14.4";
216
- if (shadowGradLenValue) shadowGradLenValue.textContent = "14.4";
217
- viewer.setShadowGradientLength(14.4);
218
- shadowGradLen.addEventListener("input", (e) => {
219
- const v = Number(e.target.value);
220
- if (shadowGradLenValue) shadowGradLenValue.textContent = v.toFixed(1);
221
- viewer.setShadowGradientLength(v);
222
- });
223
- }
224
- if (shadowGradStr) {
225
- shadowGradStr.value = "1.00";
226
- if (shadowGradStrValue) shadowGradStrValue.textContent = "1.00";
227
- viewer.setShadowGradientStrength(1.0);
228
- shadowGradStr.addEventListener("input", (e) => {
229
- const v = Number(e.target.value);
230
- if (shadowGradStrValue) shadowGradStrValue.textContent = v.toFixed(2);
231
- viewer.setShadowGradientStrength(v);
232
- });
233
- }
234
-
235
- if (shadowGradCurve) {
236
- shadowGradCurve.value = "0.50";
237
- if (shadowGradCurveValue) shadowGradCurveValue.textContent = "0.50";
238
- viewer.setShadowGradientCurve(0.5);
239
- shadowGradCurve.addEventListener("input", (e) => {
240
- const v = Number(e.target.value);
241
- if (shadowGradCurveValue) shadowGradCurveValue.textContent = v.toFixed(2);
242
- viewer.setShadowGradientCurve(v);
243
- });
244
- }
245
-
246
- // Полупрозрачность тени на земле
247
- if (shadowOpacity) {
248
- shadowOpacity.value = "0.14";
249
- if (shadowOpacityValue) shadowOpacityValue.textContent = "0.14";
250
- viewer.setShadowOpacity(0.14);
251
- shadowOpacity.addEventListener("input", (e) => {
252
- const v = Number(e.target.value);
253
- if (shadowOpacityValue) shadowOpacityValue.textContent = v.toFixed(2);
254
- viewer.setShadowOpacity(v);
255
- });
256
- }
257
-
258
- // Мягкость края тени
259
- if (shadowSoft) {
260
- shadowSoft.value = "0.0";
261
- if (shadowSoftValue) shadowSoftValue.textContent = "0.0";
262
- viewer.setShadowSoftness(0.0);
263
- shadowSoft.addEventListener("input", (e) => {
264
- const v = Number(e.target.value);
265
- if (shadowSoftValue) shadowSoftValue.textContent = v.toFixed(1);
266
- viewer.setShadowSoftness(v);
267
- });
268
- }
269
-
270
- // Панель свойств: солнце (глобальное освещение)
271
- const sunToggle = document.getElementById("sunToggle");
272
- const sunHeight = document.getElementById("sunHeight");
273
- const sunHeightValue = document.getElementById("sunHeightValue");
274
- if (sunToggle) {
275
- // По умолчанию включено
276
- sunToggle.checked = true;
277
- viewer.setSunEnabled(true);
278
- sunToggle.addEventListener("change", (e) => {
279
- const on = !!e.target.checked;
280
- viewer.setSunEnabled(on);
281
- if (sunHeight) sunHeight.disabled = !on;
282
- });
283
- }
284
- if (sunHeight) {
285
- // Дефолт (из текущих подобранных значений)
286
- sunHeight.value = "5.9";
287
- if (sunHeightValue) sunHeightValue.textContent = "5.9";
288
- viewer.setSunHeight(5.9);
289
- sunHeight.disabled = !(sunToggle ? !!sunToggle.checked : true);
290
- sunHeight.addEventListener("input", (e) => {
291
- const v = Number(e.target.value);
292
- if (sunHeightValue) sunHeightValue.textContent = v.toFixed(1);
293
- viewer.setSunHeight(v);
294
- });
295
- }
296
-
297
- // ===== Материалы =====
298
- const MAT_DEFAULTS = {
299
- original: { roughness: 0.90, metalness: 0.00, slidersEnabled: false },
300
- matte: { roughness: 0.90, metalness: 0.00, slidersEnabled: true },
301
- glossy: { roughness: 0.05, metalness: 0.00, slidersEnabled: true },
302
- // Важно: "пластик" не должен быть металлом, иначе появятся резкие блики и "дёрганая" картинка при вращении
303
- plastic: { roughness: 0.65, metalness: 0.00, slidersEnabled: true },
304
- concrete: { roughness: 0.95, metalness: 0.00, slidersEnabled: true },
80
+ const applyStep2Blue = (amount) => {
81
+ const v = Number(amount);
82
+ if (!Number.isFinite(v)) return;
83
+ if (step2BlueValue) step2BlueValue.textContent = v.toFixed(2);
84
+ try { viewer.setCoolLightingAmount?.(v); } catch (_) {}
305
85
  };
306
86
 
307
- const setMatUiEnabled = (enabled) => {
308
- if (matRough) matRough.disabled = !enabled;
309
- if (matMetal) matMetal.disabled = !enabled;
87
+ const applyStep1Exposure = (v) => {
88
+ const value = Number(v);
89
+ if (!Number.isFinite(value)) return;
90
+ if (step1ExposureValue) step1ExposureValue.textContent = value.toFixed(2);
91
+ try { viewer.setExposure(value); } catch (_) {}
310
92
  };
311
93
 
312
- const applyMatPresetUi = (preset) => {
313
- const d = MAT_DEFAULTS[preset] || MAT_DEFAULTS.original;
314
- if (matRough) matRough.value = String(d.roughness.toFixed(2));
315
- if (matRoughValue) matRoughValue.textContent = d.roughness.toFixed(2);
316
- if (matMetal) matMetal.value = String(d.metalness.toFixed(2));
317
- if (matMetalValue) matMetalValue.textContent = d.metalness.toFixed(2);
318
- setMatUiEnabled(d.slidersEnabled);
94
+ const applyStep4Contrast = (v) => {
95
+ const value = Number(v);
96
+ if (!Number.isFinite(value)) return;
97
+ if (step4ContrastValue) step4ContrastValue.textContent = value.toFixed(2);
98
+ try { viewer.setStep4Contrast?.(value); } catch (_) {}
319
99
  };
320
-
321
- if (matPreset) {
322
- // Дефолт: Пластик (как на скрине)
323
- matPreset.value = "plastic";
324
- applyMatPresetUi("plastic");
325
- viewer.setMaterialPreset("plastic");
326
- viewer.setMaterialRoughness(MAT_DEFAULTS.plastic.roughness);
327
- viewer.setMaterialMetalness(MAT_DEFAULTS.plastic.metalness);
328
- matPreset.addEventListener("change", (e) => {
329
- const preset = e.target.value;
330
- viewer.setMaterialPreset(preset);
331
- applyMatPresetUi(preset);
332
- // применяем дефолтные параметры пресета как override
333
- const d = MAT_DEFAULTS[preset] || MAT_DEFAULTS.original;
334
- if (d.slidersEnabled) {
335
- viewer.setMaterialRoughness(d.roughness);
336
- viewer.setMaterialMetalness(d.metalness);
337
- } else {
338
- viewer.setMaterialRoughness(null);
339
- viewer.setMaterialMetalness(null);
340
- }
341
- });
342
- }
343
-
344
- if (matRough) {
345
- matRough.addEventListener("input", (e) => {
346
- const v = Number(e.target.value);
347
- if (matRoughValue) matRoughValue.textContent = v.toFixed(2);
348
- viewer.setMaterialRoughness(v);
349
- });
350
- }
351
- if (matMetal) {
352
- matMetal.addEventListener("input", (e) => {
353
- const v = Number(e.target.value);
354
- if (matMetalValue) matMetalValue.textContent = v.toFixed(2);
355
- viewer.setMaterialMetalness(v);
356
- });
357
- }
358
-
359
- // ===== Визуал (диагностика) =====
360
- const syncVisualUiEnabled = () => {
361
- const envOn = !!(envToggle && envToggle.checked);
362
- const toneOn = !!(toneToggle && toneToggle.checked);
363
- const aoOn = !!(aoToggle && aoToggle.checked);
364
- if (envInt) envInt.disabled = !envOn;
365
- if (exposure) exposure.disabled = !toneOn;
366
- if (aoInt) aoInt.disabled = !aoOn;
367
- if (aoRad) aoRad.disabled = !aoOn;
100
+ const applyStep4Saturation = (v) => {
101
+ const value = Number(v);
102
+ if (!Number.isFinite(value)) return;
103
+ if (step4SaturationValue) step4SaturationValue.textContent = value.toFixed(2);
104
+ try { viewer.setStep4Saturation?.(value); } catch (_) {}
368
105
  };
369
106
 
370
- const syncCcUiEnabled = () => {
371
- const on = !!(ccToggle && ccToggle.checked);
372
- if (ccHue) ccHue.disabled = !on;
373
- if (ccSat) ccSat.disabled = !on;
374
- if (ccBri) ccBri.disabled = !on;
375
- if (ccCon) ccCon.disabled = !on;
107
+ const setTestPresetEnabled = (on) => {
108
+ const enabled = !!on;
109
+ try { viewer.setTestPresetEnabled?.(enabled); } catch (_) {}
110
+ if (testPresetToggle) testPresetToggle.checked = enabled;
111
+ // Шаг 1 сейчас привязан к "Тест"
112
+ setStep1UiEnabled(enabled);
113
+ if (enabled && step1Exposure) applyStep1Exposure(step1Exposure.value);
114
+ // Шаг 2 сейчас тоже привязан к "Тест"
115
+ setStep2UiEnabled(enabled);
116
+ if (!enabled) {
117
+ try { viewer.setCoolLightingEnabled?.(false); } catch (_) {}
118
+ }
119
+ // Шаг 3 сейчас тоже привязан к "Тест"
120
+ setStep3UiEnabled(enabled);
121
+ if (!enabled) {
122
+ try { viewer.setStep3BackgroundEnabled?.(false); } catch (_) {}
123
+ }
124
+ // Шаг 4 сейчас тоже привязан к "Тест"
125
+ setStep4UiEnabled(enabled);
126
+ if (!enabled) {
127
+ try { viewer.setStep4Enabled?.(false); } catch (_) {}
128
+ }
376
129
  };
377
130
 
378
- // Дефолты (как на скрине)
379
- if (envToggle) {
380
- envToggle.checked = true;
381
- viewer.setEnvironmentEnabled(true);
382
- envToggle.addEventListener("change", (e) => {
383
- viewer.setEnvironmentEnabled(!!e.target.checked);
384
- syncVisualUiEnabled();
131
+ // По умолчанию "Тест" включён и применяется сразу (до загрузки IFC)
132
+ setTestPresetEnabled(true);
133
+ testPresetToggle?.addEventListener("change", (e) => setTestPresetEnabled(!!e.target.checked));
134
+
135
+ // Шаг 1: exposure
136
+ if (step1Exposure) {
137
+ // дефолтное положение для подбора
138
+ step1Exposure.value = "1.19";
139
+ applyStep1Exposure(step1Exposure.value);
140
+ step1Exposure.addEventListener("input", (e) => applyStep1Exposure(e.target.value));
141
+ }
142
+ // Шаг 1: dump
143
+ step1Dump?.addEventListener("click", () => {
144
+ const r = viewer?.renderer;
145
+ // eslint-disable-next-line no-console
146
+ console.log('[Step1] tone', {
147
+ visualTone: viewer?.visual?.tone,
148
+ renderer: {
149
+ outputColorSpace: r?.outputColorSpace,
150
+ outputEncoding: r?.outputEncoding,
151
+ toneMapping: r?.toneMapping,
152
+ toneMappingExposure: r?.toneMappingExposure,
153
+ },
385
154
  });
386
- }
387
- if (envInt) {
388
- envInt.value = "0.65";
389
- if (envIntValue) envIntValue.textContent = "0.65";
390
- viewer.setEnvironmentIntensity(0.65);
391
- envInt.addEventListener("input", (e) => {
392
- const v = Number(e.target.value);
393
- if (envIntValue) envIntValue.textContent = v.toFixed(2);
394
- viewer.setEnvironmentIntensity(v);
395
- });
396
- }
155
+ });
397
156
 
398
- if (toneToggle) {
399
- toneToggle.checked = true;
400
- viewer.setToneMappingEnabled(true);
401
- toneToggle.addEventListener("change", (e) => {
402
- viewer.setToneMappingEnabled(!!e.target.checked);
403
- syncVisualUiEnabled();
157
+ // Шаг 2: холодный свет
158
+ if (step2CoolLighting) {
159
+ step2CoolLighting.checked = false;
160
+ step2CoolLighting.addEventListener("change", (e) => {
161
+ const on = !!e.target.checked;
162
+ try { viewer.setCoolLightingEnabled?.(on); } catch (_) {}
163
+ // при включении применим текущие значения ручек
164
+ if (on) {
165
+ if (step2Hue) applyStep2Hue(step2Hue.value);
166
+ if (step2Blue) applyStep2Blue(step2Blue.value);
167
+ }
168
+ // синхронизируем доступность ручек
169
+ if (step2Hue) step2Hue.disabled = !on;
170
+ if (step2Blue) step2Blue.disabled = !on;
404
171
  });
405
172
  }
406
- if (exposure) {
407
- exposure.value = "1.11";
408
- if (exposureValue) exposureValue.textContent = "1.11";
409
- viewer.setExposure(1.11);
410
- exposure.addEventListener("input", (e) => {
411
- const v = Number(e.target.value);
412
- if (exposureValue) exposureValue.textContent = v.toFixed(2);
413
- viewer.setExposure(v);
173
+ if (step2Hue) {
174
+ // дефолтное положение для подбора
175
+ step2Hue.value = "240";
176
+ applyStep2Hue(step2Hue.value);
177
+ step2Hue.disabled = true; // пока step2 выключен
178
+ step2Hue.addEventListener("input", (e) => applyStep2Hue(e.target.value));
179
+ }
180
+ if (step2Blue) {
181
+ // дефолтное положение для подбора
182
+ step2Blue.value = "1.00";
183
+ applyStep2Blue(step2Blue.value);
184
+ step2Blue.disabled = true; // пока step2 выключен
185
+ step2Blue.addEventListener("input", (e) => applyStep2Blue(e.target.value));
186
+ }
187
+ step2Dump?.addEventListener("click", () => {
188
+ const amb = viewer?.ambientLight;
189
+ const sun = viewer?.sunLight;
190
+ const hemi = viewer?.hemiLight;
191
+ const recv = viewer?.shadowReceiver;
192
+ const recvMat = recv?.material;
193
+ // eslint-disable-next-line no-console
194
+ console.log('[Step2] cool lighting', {
195
+ coolEnabled: viewer?._coolLighting?.enabled,
196
+ params: viewer?._coolLighting?.params,
197
+ ambient: amb ? { visible: amb.visible, intensity: amb.intensity, color: amb.color?.getHexString?.() } : null,
198
+ hemi: hemi ? { visible: hemi.visible, intensity: hemi.intensity, color: hemi.color?.getHexString?.(), ground: hemi.groundColor?.getHexString?.() } : null,
199
+ sun: sun ? { visible: sun.visible, intensity: sun.intensity, color: sun.color?.getHexString?.() } : null,
200
+ shadowReceiver: recv ? {
201
+ visible: !!recv.visible,
202
+ receiveShadow: !!recv.receiveShadow,
203
+ opacity: ('opacity' in (recvMat || {})) ? recvMat.opacity : undefined,
204
+ color: recvMat?.color?.getHexString?.(),
205
+ } : null,
414
206
  });
415
- }
207
+ });
416
208
 
417
- if (aoToggle) {
418
- aoToggle.checked = true;
419
- viewer.setAOEnabled(true);
420
- aoToggle.addEventListener("change", (e) => {
421
- viewer.setAOEnabled(!!e.target.checked);
422
- syncVisualUiEnabled();
423
- });
424
- }
425
- if (aoInt) {
426
- aoInt.value = "0.52";
427
- if (aoIntValue) aoIntValue.textContent = "0.52";
428
- viewer.setAOIntensity(0.52);
429
- aoInt.addEventListener("input", (e) => {
430
- const v = Number(e.target.value);
431
- if (aoIntValue) aoIntValue.textContent = v.toFixed(2);
432
- viewer.setAOIntensity(v);
209
+ // Шаг 3: фон сцены
210
+ if (step3Background) {
211
+ step3Background.checked = false;
212
+ step3Background.addEventListener("change", (e) => {
213
+ const on = !!e.target.checked;
214
+ try { viewer.setStep3BackgroundEnabled?.(on); } catch (_) {}
433
215
  });
434
216
  }
435
- if (aoRad) {
436
- aoRad.value = "8";
437
- if (aoRadValue) aoRadValue.textContent = "8";
438
- viewer.setAORadius(8);
439
- aoRad.addEventListener("input", (e) => {
440
- const v = Number(e.target.value);
441
- if (aoRadValue) aoRadValue.textContent = String(Math.round(v));
442
- viewer.setAORadius(v);
217
+ step3Dump?.addEventListener("click", () => {
218
+ const bg = viewer?.scene?.background;
219
+ // eslint-disable-next-line no-console
220
+ console.log('[Step3] background', {
221
+ enabled: viewer?._step3Background?.enabled,
222
+ type: bg ? (bg.isColor ? 'Color' : bg.isTexture ? 'Texture' : typeof bg) : 'null',
223
+ value: bg?.isColor ? bg.getHexString?.() : null,
443
224
  });
444
- }
445
-
446
- syncVisualUiEnabled();
447
-
448
- if (dumpVisual) {
449
- dumpVisual.addEventListener("click", () => viewer.dumpVisualDebug());
450
- }
225
+ });
451
226
 
452
- // ===== Цветокор =====
453
- if (ccToggle) {
454
- ccToggle.checked = false;
455
- viewer.setColorCorrectionEnabled(false);
456
- ccToggle.addEventListener("change", (e) => {
457
- viewer.setColorCorrectionEnabled(!!e.target.checked);
458
- syncCcUiEnabled();
459
- });
460
- }
461
- if (ccHue) {
462
- ccHue.value = "0.00";
463
- if (ccHueValue) ccHueValue.textContent = "0.00";
464
- viewer.setColorHue(0.0);
465
- ccHue.addEventListener("input", (e) => {
466
- const v = Number(e.target.value);
467
- if (ccHueValue) ccHueValue.textContent = v.toFixed(2);
468
- viewer.setColorHue(v);
469
- });
470
- }
471
- if (ccSat) {
472
- ccSat.value = "0.00";
473
- if (ccSatValue) ccSatValue.textContent = "0.00";
474
- viewer.setColorSaturation(0.0);
475
- ccSat.addEventListener("input", (e) => {
476
- const v = Number(e.target.value);
477
- if (ccSatValue) ccSatValue.textContent = v.toFixed(2);
478
- viewer.setColorSaturation(v);
479
- });
480
- }
481
- if (ccBri) {
482
- ccBri.value = "0.00";
483
- if (ccBriValue) ccBriValue.textContent = "0.00";
484
- viewer.setColorBrightness(0.0);
485
- ccBri.addEventListener("input", (e) => {
486
- const v = Number(e.target.value);
487
- if (ccBriValue) ccBriValue.textContent = v.toFixed(2);
488
- viewer.setColorBrightness(v);
227
+ // Шаг 4: пост-обработка (финальный pass)
228
+ if (step4Post) {
229
+ step4Post.checked = false;
230
+ step4Post.addEventListener("change", (e) => {
231
+ const on = !!e.target.checked;
232
+ try { viewer.setStep4Enabled?.(on); } catch (_) {}
233
+ // синхронизируем доступность ручек
234
+ if (step4Contrast) step4Contrast.disabled = !on;
235
+ if (step4Saturation) step4Saturation.disabled = !on;
236
+ if (on) {
237
+ if (step4Contrast) applyStep4Contrast(step4Contrast.value);
238
+ if (step4Saturation) applyStep4Saturation(step4Saturation.value);
239
+ }
489
240
  });
490
241
  }
491
- if (ccCon) {
492
- ccCon.value = "0.00";
493
- if (ccConValue) ccConValue.textContent = "0.00";
494
- viewer.setColorContrast(0.0);
495
- ccCon.addEventListener("input", (e) => {
496
- const v = Number(e.target.value);
497
- if (ccConValue) ccConValue.textContent = v.toFixed(2);
498
- viewer.setColorContrast(v);
242
+ if (step4Contrast) {
243
+ // дефолтное положение для подбора
244
+ step4Contrast.value = step4Contrast.value || "1.35";
245
+ applyStep4Contrast(step4Contrast.value);
246
+ step4Contrast.disabled = true; // пока step4 выключен
247
+ step4Contrast.addEventListener("input", (e) => applyStep4Contrast(e.target.value));
248
+ }
249
+ if (step4Saturation) {
250
+ // дефолтное положение для подбора
251
+ step4Saturation.value = step4Saturation.value || "1.60";
252
+ applyStep4Saturation(step4Saturation.value);
253
+ step4Saturation.disabled = true; // пока step4 выключен
254
+ step4Saturation.addEventListener("input", (e) => applyStep4Saturation(e.target.value));
255
+ }
256
+ step4Dump?.addEventListener("click", () => {
257
+ const recv = viewer?.shadowReceiver;
258
+ const recvMat = recv?.material;
259
+ // eslint-disable-next-line no-console
260
+ console.log('[Step4] post', {
261
+ enabled: viewer?._step4?.enabled,
262
+ params: viewer?._step4 ? { contrast: viewer._step4.contrast, saturation: viewer._step4.saturation } : null,
263
+ composer: viewer?._composer ? { passes: viewer._composer.passes?.length } : null,
264
+ shadowReceiver: recv ? {
265
+ visible: !!recv.visible,
266
+ receiveShadow: !!recv.receiveShadow,
267
+ opacity: ('opacity' in (recvMat || {})) ? recvMat.opacity : undefined,
268
+ color: recvMat?.color?.getHexString?.(),
269
+ } : null,
499
270
  });
500
- }
501
- syncCcUiEnabled();
271
+ });
272
+
273
+ /*
274
+ * ===== LEGACY: старые настройки левой панели (временно не используем) =====
275
+ * Здесь был код управления:
276
+ * - shadowToggle/shadowGrad/shadowOpacity/shadowSoft
277
+ * - sunToggle/sunHeight
278
+ * - material preset + rough/metal
279
+ * - env/tone/AO + dumpVisual
280
+ * - color correction
281
+ * - realtime-quality UI lock + test UI lock snapshots
282
+ */
502
283
  // IFC загрузка
503
284
  const ifc = new IfcService(viewer);
504
285
  ifc.init();
@@ -574,13 +355,22 @@ if (app) {
574
355
  // Переключение вида: "без перспективы" (Ortho) ↔ Perspective
575
356
  // Вариант 2: на кнопке показываем действие (альтернативный режим)
576
357
  const PROJ_ICON_PERSPECTIVE = `
577
- <svg width="24" height="24" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
578
- <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"/>
358
+ <svg width="24" height="24" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" fill="currentColor" style="color:#252A3F">
359
+ <path d="M 365.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"/>
360
+ <path 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"/>
361
+ <path 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"/>
362
+ <path 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"/>
363
+ <path 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"/>
364
+ <path 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"/>
365
+ <path 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"/>
366
+ <path 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"/>
367
+ <path 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"/>
368
+ <path 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"/>
579
369
  </svg>
580
370
  `;
581
371
  const PROJ_ICON_ORTHO = `
582
- <svg width="24" height="24" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
583
- <path fill="#000000" d="M256.02 48.55 Q 257.33 48.55 258.06 48.94 Q 381.49 115.11 442.91 148.14 Q 445.24 149.39 445.26 152.25 Q 445.52 184.71 445.52 256.00 Q 445.52 327.29 445.26 359.75 Q 445.24 362.61 442.91 363.86 Q 381.49 396.89 258.06 463.06 Q 257.33 463.45 256.02 463.45 Q 254.71 463.45 253.98 463.06 Q 130.55 396.89 69.13 363.86 Q 66.80 362.61 66.78 359.75 Q 66.52 327.29 66.52 256.00 Q 66.52 184.71 66.78 152.25 Q 66.80 149.39 69.13 148.14 Q 130.55 115.11 253.98 48.94 Q 254.71 48.55 256.02 48.55 Z"/>
372
+ <svg width="24" height="24" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" fill="currentColor" style="color:#252A3F">
373
+ <path d="M 256.02 48.55 Q 257.33 48.55 258.06 48.94 Q 381.49 115.11 442.91 148.14 Q 445.24 149.39 445.26 152.25 Q 445.52 184.71 445.52 256.00 Q 445.52 327.29 445.26 359.75 Q 445.24 362.61 442.91 363.86 Q 381.49 396.89 258.06 463.06 Q 257.33 463.45 256.02 463.45 Q 254.71 463.45 253.98 463.06 Q 130.55 396.89 69.13 363.86 Q 66.80 362.61 66.78 359.75 Q 66.52 327.29 66.52 256.00 Q 66.52 184.71 66.78 152.25 Q 66.80 149.39 69.13 148.14 Q 130.55 115.11 253.98 48.94 Q 254.71 48.55 256.02 48.55 Z M 256.03 147.56 Q 257.36 147.56 258.05 147.94 Q 295.68 168.96 347.89 198.33 A 0.77 0.75 44.3 0 0 348.62 198.33 L 429.62 152.89 A 0.32 0.32 0.0 0 0 429.62 152.33 Q 332.33 100.05 256.30 59.37 Q 256.25 59.35 256.15 59.34 Q 256.09 59.33 256.02 59.33 Q 255.97 59.33 255.90 59.34 Q 255.81 59.35 255.76 59.37 Q 179.73 100.05 82.44 152.34 A 0.32 0.32 0.0 0 0 82.44 152.90 L 163.44 198.34 A 0.77 0.75 -44.3 0 0 164.17 198.34 Q 216.38 168.96 254.01 147.94 Q 254.70 147.56 256.03 147.56 Z M 255.82 250.17 A 0.38 0.38 0.0 0 0 256.20 250.17 L 337.45 204.58 A 0.38 0.38 0.0 0 0 337.45 203.92 L 256.20 158.33 A 0.38 0.38 0.0 0 0 255.82 158.33 L 174.57 203.92 A 0.38 0.38 0.0 0 0 174.57 204.58 L 255.82 250.17 Z M 76.99 161.29 A 0.33 0.33 0.0 0 0 76.50 161.58 L 76.50 246.92 A 0.33 0.33 0.0 0 0 76.99 247.21 L 153.06 204.54 A 0.33 0.33 0.0 0 0 153.06 203.96 L 76.99 161.29 Z M 434.97 247.14 A 0.35 0.35 0.0 0 0 435.49 246.83 L 435.49 161.67 A 0.35 0.35 0.0 0 0 434.97 161.36 L 359.05 203.94 A 0.35 0.35 0.0 0 0 359.05 204.56 L 434.97 247.14 Z M 245.33 256.28 A 0.32 0.32 0.0 0 0 245.33 255.72 L 163.96 210.07 A 0.32 0.32 0.0 0 0 163.64 210.07 L 82.27 255.72 A 0.32 0.32 0.0 0 0 82.27 256.28 L 163.64 301.93 A 0.32 0.32 0.0 0 0 163.96 301.93 L 245.33 256.28 Z M 429.83 256.28 A 0.32 0.32 0.0 0 0 429.83 255.72 L 348.46 210.07 A 0.32 0.32 0.0 0 0 348.14 210.07 L 266.77 255.72 A 0.32 0.32 0.0 0 0 266.77 256.28 L 348.14 301.93 A 0.32 0.32 0.0 0 0 348.46 301.93 L 429.83 256.28 Z M 337.56 308.04 A 0.33 0.33 0.0 0 0 337.56 307.46 L 256.20 261.82 A 0.33 0.33 0.0 0 0 255.88 261.82 L 174.51 307.46 A 0.33 0.33 0.0 0 0 174.51 308.04 L 255.87 353.68 A 0.33 0.33 0.0 0 0 256.19 353.68 L 337.56 308.04 Z M 76.96 264.77 A 0.31 0.31 0.0 0 0 76.50 265.04 L 76.50 350.46 A 0.31 0.31 0.0 0 0 76.96 350.73 L 153.09 308.02 A 0.31 0.31 0.0 0 0 153.09 307.48 L 76.96 264.77 Z M 434.97 350.63 A 0.35 0.35 0.0 0 0 435.49 350.33 L 435.49 265.17 A 0.35 0.35 0.0 0 0 434.97 264.87 L 359.05 307.44 A 0.35 0.35 0.0 0 0 359.05 308.06 L 434.97 350.63 Z M 256.02 364.45 Q 254.69 364.45 254.00 364.06 Q 216.37 343.04 164.17 313.67 A 0.77 0.75 44.3 0 0 163.44 313.67 L 82.44 359.10 A 0.32 0.32 0.0 0 0 82.44 359.66 Q 179.72 411.94 255.74 452.63 Q 255.79 452.65 255.89 452.66 Q 255.96 452.67 256.00 452.67 Q 256.07 452.67 256.14 452.66 Q 256.24 452.65 256.29 452.63 Q 332.31 411.95 429.60 359.67 A 0.32 0.32 0.0 0 0 429.60 359.11 L 348.60 313.68 A 0.77 0.75 -44.3 0 0 347.87 313.68 Q 295.66 343.04 258.03 364.06 Q 257.35 364.45 256.02 364.45 Z"/>
584
374
  </svg>
585
375
  `;
586
376
  const syncProjectionIcon = () => {
@@ -598,14 +388,14 @@ if (app) {
598
388
  const setToolbarShadowsActive = (on) => {
599
389
  if (toggleShadowsBtn) toggleShadowsBtn.classList.toggle('btn-active', !!on);
600
390
  };
601
- setToolbarShadowsActive(true);
391
+ let toolbarShadowsOn = true;
392
+ try { viewer.setShadowsEnabled(toolbarShadowsOn); } catch (_) {}
393
+ setToolbarShadowsActive(toolbarShadowsOn);
602
394
  toggleShadowsBtn?.addEventListener("click", () => {
603
- const next = !(shadowToggle ? !!shadowToggle.checked : true);
395
+ toolbarShadowsOn = !toolbarShadowsOn;
604
396
  // Меняем состояние у Viewer
605
- viewer.setShadowsEnabled(next);
606
- // Синхронизируем UI слева, если он есть
607
- if (shadowToggle) shadowToggle.checked = next;
608
- setToolbarShadowsActive(next);
397
+ try { viewer.setShadowsEnabled(toolbarShadowsOn); } catch (_) {}
398
+ setToolbarShadowsActive(toolbarShadowsOn);
609
399
  });
610
400
 
611
401
  let flatOn = true;
@@ -56,6 +56,8 @@ export class Viewer {
56
56
  this.sunLight = null;
57
57
  /** @type {THREE.AmbientLight|null} */
58
58
  this.ambientLight = null;
59
+ /** @type {THREE.HemisphereLight|null} */
60
+ this.hemiLight = null;
59
61
  // Базовые координаты солнца (чтобы менять только высоту по Y)
60
62
  this._sunBaseXZ = { x: 5, z: 5 };
61
63
  // Параметры градиента тени на земле (модифицирует только ShadowMaterial приёмника)
@@ -78,6 +80,8 @@ export class Viewer {
78
80
  opacity: 0.14, // прозрачность тени на земле (ShadowMaterial.opacity)
79
81
  softness: 0.0, // мягкость края (DirectionalLight.shadow.radius)
80
82
  };
83
+ // Базовый цвет тени на земле (по умолчанию нейтрально-серый, чтобы можно было сравнивать с "синей" тенью в Шаге 2)
84
+ this._shadowReceiverBaseColor = new THREE.Color(0x2a2a2a);
81
85
 
82
86
  // Материалы (пресеты)
83
87
  this.materialStyle = {
@@ -101,6 +105,12 @@ export class Viewer {
101
105
  this._rtQuality = { enabled: false, snapshot: null };
102
106
  // Пресет "Тест": полностью изолированная настройка (тени+самозатенение+визуал из рекомендаций)
103
107
  this._testPreset = { enabled: false, snapshot: null };
108
+
109
+ // Шаг 2: холодное освещение (отдельно от пресета "Тест", но обычно используется вместе с ним)
110
+ this._coolLighting = { enabled: false, snapshot: null, params: { hueDeg: 210, amount: 1.0 } };
111
+
112
+ // Шаг 3: фон сцены (как в Autodesk)
113
+ this._step3Background = { enabled: false, snapshot: null, colorHex: 0xe8eef4 };
104
114
  this._baselineRenderer = null;
105
115
  this._pmrem = null;
106
116
  this._roomEnvTex = null;
@@ -109,6 +119,9 @@ export class Viewer {
109
119
  this._ssaoPass = null;
110
120
  this._hueSatPass = null;
111
121
  this._bcPass = null;
122
+ // Шаг 4: финальная постобработка (контраст/насыщенность) — должна быть последним pass'ом
123
+ this._step4Pass = null;
124
+ this._step4 = { enabled: false, saturation: 1.0, contrast: 1.0 };
112
125
  this.clipping = {
113
126
  enabled: false,
114
127
  planes: [
@@ -467,7 +480,7 @@ export class Viewer {
467
480
  this.#updateClippingGizmos();
468
481
  // Рендер основной сцены
469
482
  this.renderer.clear(true, true, true);
470
- const useComposer = !!(this._composer && (this.visual?.ao?.enabled || this.visual?.color?.enabled));
483
+ const useComposer = !!(this._composer && (this.visual?.ao?.enabled || this.visual?.color?.enabled || this._step4?.enabled));
471
484
  if (useComposer) {
472
485
  this._composer.render();
473
486
  } else {
@@ -534,6 +547,7 @@ export class Viewer {
534
547
  this._ssaoPass = null;
535
548
  this._hueSatPass = null;
536
549
  this._bcPass = null;
550
+ this._step4Pass = null;
537
551
  }
538
552
  if (this.resizeObserver) {
539
553
  this.resizeObserver.disconnect();
@@ -992,7 +1006,7 @@ export class Viewer {
992
1006
  if (!this.scene || this.shadowReceiver) return;
993
1007
  // ShadowMaterial рисует только тени, сама плоскость прозрачная.
994
1008
  // Если тени отключены — визуально ничего не изменится.
995
- const mat = new THREE.ShadowMaterial({ opacity: this.shadowStyle.opacity });
1009
+ const mat = new THREE.ShadowMaterial({ opacity: this.shadowStyle.opacity, color: this._shadowReceiverBaseColor.clone() });
996
1010
  // Градиент тени: модифицируем шейдер только приёмника (не влияет на остальные материалы)
997
1011
  mat.onBeforeCompile = (shader) => {
998
1012
  // uniforms
@@ -1083,6 +1097,8 @@ export class Viewer {
1083
1097
  plane.position.set(0, -9999, 0); // спрячем до первого апдейта по модели
1084
1098
  this.scene.add(plane);
1085
1099
  this.shadowReceiver = plane;
1100
+ // Если "холодный свет" (Шаг 2) уже включён — сразу подкрасим тень тем же оттенком
1101
+ try { this.#applyShadowTintFromCoolLighting(); } catch (_) {}
1086
1102
  }
1087
1103
 
1088
1104
  #updateShadowReceiverFromModel(model) {
@@ -1890,10 +1906,212 @@ export class Viewer {
1890
1906
  setExposure(exposure) {
1891
1907
  const v = Number(exposure);
1892
1908
  if (!Number.isFinite(v)) return;
1893
- this.visual.tone.exposure = Math.min(2.5, Math.max(0.1, v));
1909
+ this.visual.tone.exposure = Math.min(3.0, Math.max(0.1, v));
1894
1910
  this.#applyToneSettings();
1895
1911
  }
1896
1912
 
1913
+ /**
1914
+ * Шаг 2: включает/выключает холодное освещение (HemisphereLight + холодный AmbientLight),
1915
+ * сохраняя и восстанавливая исходные параметры.
1916
+ * @param {boolean} enabled
1917
+ */
1918
+ setCoolLightingEnabled(enabled) {
1919
+ const next = !!enabled;
1920
+ if (next === this._coolLighting.enabled) return;
1921
+
1922
+ if (next) {
1923
+ // Снимем снапшот, чтобы можно было восстановить
1924
+ this._coolLighting.snapshot = {
1925
+ ambient: this.ambientLight ? {
1926
+ visible: this.ambientLight.visible,
1927
+ intensity: this.ambientLight.intensity,
1928
+ color: this.ambientLight.color?.clone?.() || null,
1929
+ } : null,
1930
+ hemi: this.hemiLight ? {
1931
+ existed: true,
1932
+ visible: this.hemiLight.visible,
1933
+ intensity: this.hemiLight.intensity,
1934
+ color: this.hemiLight.color?.clone?.() || null,
1935
+ groundColor: this.hemiLight.groundColor?.clone?.() || null,
1936
+ } : { existed: false },
1937
+ shadowReceiverColor: (this.shadowReceiver?.material?.color?.clone?.() || null),
1938
+ };
1939
+
1940
+ // 1) Ambient: холодный общий
1941
+ if (this.ambientLight) {
1942
+ try { this.ambientLight.visible = true; } catch (_) {}
1943
+ try { this.ambientLight.color?.setHex?.(0xd0e0f0); } catch (_) {}
1944
+ try { this.ambientLight.intensity = 0.4; } catch (_) {}
1945
+ }
1946
+
1947
+ // 2) Hemisphere: добавляем/включаем
1948
+ if (!this.hemiLight && this.scene) {
1949
+ try {
1950
+ const hemi = new THREE.HemisphereLight(0xc0d8f0, 0x444444, 0.6);
1951
+ this.scene.add(hemi);
1952
+ this.hemiLight = hemi;
1953
+ } catch (_) {}
1954
+ }
1955
+ if (this.hemiLight) {
1956
+ try { this.hemiLight.visible = true; } catch (_) {}
1957
+ try { this.hemiLight.color?.setHex?.(0xc0d8f0); } catch (_) {}
1958
+ try { this.hemiLight.groundColor?.setHex?.(0x444444); } catch (_) {}
1959
+ try { this.hemiLight.intensity = 0.6; } catch (_) {}
1960
+ }
1961
+
1962
+ this._coolLighting.enabled = true;
1963
+ // Применим текущие параметры (hue/amount)
1964
+ try { this.#applyCoolLightingParams(); } catch (_) {}
1965
+ // Подкрасим тень на земле тем же холодным оттенком
1966
+ try { this.#applyShadowTintFromCoolLighting(); } catch (_) {}
1967
+ return;
1968
+ }
1969
+
1970
+ // Выключение: восстановление
1971
+ const snap = this._coolLighting.snapshot;
1972
+ this._coolLighting.enabled = false;
1973
+ this._coolLighting.snapshot = null;
1974
+ if (!snap) return;
1975
+
1976
+ // Ambient restore
1977
+ if (this.ambientLight && snap.ambient) {
1978
+ try { this.ambientLight.visible = !!snap.ambient.visible; } catch (_) {}
1979
+ try { if (snap.ambient.color) this.ambientLight.color.copy(snap.ambient.color); } catch (_) {}
1980
+ try { this.ambientLight.intensity = snap.ambient.intensity; } catch (_) {}
1981
+ }
1982
+
1983
+ // Hemisphere restore / dispose if created by us
1984
+ if (snap.hemi?.existed) {
1985
+ // Был — вернём параметры
1986
+ if (this.hemiLight) {
1987
+ try { this.hemiLight.visible = !!snap.hemi.visible; } catch (_) {}
1988
+ try { if (snap.hemi.color) this.hemiLight.color.copy(snap.hemi.color); } catch (_) {}
1989
+ try { if (snap.hemi.groundColor) this.hemiLight.groundColor.copy(snap.hemi.groundColor); } catch (_) {}
1990
+ try { this.hemiLight.intensity = snap.hemi.intensity; } catch (_) {}
1991
+ }
1992
+ } else {
1993
+ // Не было — удалим созданный
1994
+ if (this.hemiLight && this.scene) {
1995
+ try { this.scene.remove(this.hemiLight); } catch (_) {}
1996
+ try { this.hemiLight.dispose?.(); } catch (_) {}
1997
+ }
1998
+ this.hemiLight = null;
1999
+ }
2000
+
2001
+ // Shadow receiver color restore
2002
+ if (this.shadowReceiver?.material?.color) {
2003
+ try {
2004
+ if (snap.shadowReceiverColor) this.shadowReceiver.material.color.copy(snap.shadowReceiverColor);
2005
+ else this.shadowReceiver.material.color.copy(this._shadowReceiverBaseColor);
2006
+ this.shadowReceiver.material.needsUpdate = true;
2007
+ } catch (_) {}
2008
+ }
2009
+ }
2010
+
2011
+ /**
2012
+ * Оттенок "холодного" цвета (hue в градусах). Рекомендуемый диапазон: 190..240.
2013
+ * @param {number} hueDeg
2014
+ */
2015
+ setCoolLightingHue(hueDeg) {
2016
+ const v = Number(hueDeg);
2017
+ if (!Number.isFinite(v)) return;
2018
+ // Разрешим шире, но UI ограничивает "холодным" диапазоном
2019
+ this._coolLighting.params.hueDeg = Math.round(Math.min(360, Math.max(0, v)));
2020
+ this.#applyCoolLightingParams();
2021
+ }
2022
+
2023
+ /**
2024
+ * "Сколько синего добавить" (0..1). Это не экспозиция — влияет только на оттенок (смешивание базового и холодного).
2025
+ * @param {number} amount
2026
+ */
2027
+ setCoolLightingAmount(amount) {
2028
+ const v = Number(amount);
2029
+ if (!Number.isFinite(v)) return;
2030
+ this._coolLighting.params.amount = Math.min(1, Math.max(0, v));
2031
+ this.#applyCoolLightingParams();
2032
+ }
2033
+
2034
+ #applyCoolLightingParams() {
2035
+ if (!this._coolLighting?.enabled) return;
2036
+ const snap = this._coolLighting.snapshot;
2037
+ if (!snap) return;
2038
+
2039
+ const hue = (this._coolLighting.params?.hueDeg ?? 210) / 360;
2040
+ const amount = this._coolLighting.params?.amount ?? 1.0;
2041
+
2042
+ // Целевой "холодный" цвет: светлый, с небольшим насыщением, но с регулируемым hue
2043
+ const target = new THREE.Color().setHSL(hue, 0.22, 0.88);
2044
+
2045
+ // Ambient: смешиваем исходный цвет с target
2046
+ if (this.ambientLight && snap.ambient?.color) {
2047
+ try {
2048
+ const base = snap.ambient.color.clone();
2049
+ this.ambientLight.color.copy(base.lerp(target, amount));
2050
+ } catch (_) {}
2051
+ }
2052
+
2053
+ // Hemisphere sky: смешиваем исходный цвет с target, ground оставляем как есть
2054
+ if (this.hemiLight && snap.hemi?.color) {
2055
+ try {
2056
+ const base = snap.hemi.color.clone();
2057
+ this.hemiLight.color.copy(base.lerp(target, amount));
2058
+ } catch (_) {}
2059
+ }
2060
+
2061
+ // Тень на земле: подмешиваем тот же холодный оттенок
2062
+ try { this.#applyShadowTintFromCoolLighting(); } catch (_) {}
2063
+ }
2064
+
2065
+ /**
2066
+ * Подкрашивает тень на "земле" (ShadowMaterial приёмника) в холодный оттенок по параметрам Шага 2.
2067
+ * Это НЕ меняет самозатенение на модели — только цвет плоскости-приёмника.
2068
+ */
2069
+ #applyShadowTintFromCoolLighting() {
2070
+ if (!this._coolLighting?.enabled) return;
2071
+ const receiver = this.shadowReceiver;
2072
+ const mat = receiver?.material;
2073
+ if (!receiver || !mat || !mat.color) return;
2074
+
2075
+ const amount = this._coolLighting.params?.amount ?? 1.0;
2076
+
2077
+ // Базовый цвет: нейтрально-серый (или снапшот, если приёмник существовал до включения шага)
2078
+ const base = (this._coolLighting.snapshot?.shadowReceiverColor?.clone?.() || this._shadowReceiverBaseColor.clone());
2079
+ // Целевой "холодный" цвет для тени (фиксируем для визуального подбора): #386fa4
2080
+ const target = new THREE.Color('#386fa4');
2081
+ // Amount из Шага 2 напрямую управляет подмешиванием (0..1)
2082
+ const t = Math.min(1, Math.max(0, amount));
2083
+
2084
+ try {
2085
+ mat.color.copy(base.lerp(target, t));
2086
+ mat.needsUpdate = true;
2087
+ } catch (_) {}
2088
+ }
2089
+
2090
+ /**
2091
+ * Шаг 3: включает/выключает фон сцены (scene.background) и восстанавливает предыдущее значение.
2092
+ * @param {boolean} enabled
2093
+ */
2094
+ setStep3BackgroundEnabled(enabled) {
2095
+ const next = !!enabled;
2096
+ if (next === this._step3Background.enabled) return;
2097
+ if (!this.scene) return;
2098
+
2099
+ if (next) {
2100
+ // Снимем, что было до (Color|Texture|null)
2101
+ const prev = this.scene.background ?? null;
2102
+ this._step3Background.snapshot = { background: prev };
2103
+ try { this.scene.background = new THREE.Color(this._step3Background.colorHex); } catch (_) {}
2104
+ this._step3Background.enabled = true;
2105
+ return;
2106
+ }
2107
+
2108
+ const snap = this._step3Background.snapshot;
2109
+ this._step3Background.enabled = false;
2110
+ this._step3Background.snapshot = null;
2111
+ if (!snap) return;
2112
+ try { this.scene.background = snap.background ?? null; } catch (_) {}
2113
+ }
2114
+
1897
2115
  setAOEnabled(enabled) {
1898
2116
  const next = !!enabled;
1899
2117
  this.visual.ao.enabled = next;
@@ -1939,6 +2157,37 @@ export class Viewer {
1939
2157
  this.#applyColorCorrectionUniforms();
1940
2158
  }
1941
2159
 
2160
+ // ===== Шаг 4: финальная постобработка (контраст/насыщенность) =====
2161
+ setStep4Enabled(enabled) {
2162
+ const next = !!enabled;
2163
+ this._step4.enabled = next;
2164
+ if (next) this.#ensureComposer();
2165
+ if (this._step4Pass) this._step4Pass.enabled = next;
2166
+ this.#applyStep4Uniforms();
2167
+ }
2168
+
2169
+ setStep4Saturation(value) {
2170
+ const v = Number(value);
2171
+ if (!Number.isFinite(v)) return;
2172
+ // 1.0 = без изменений, <1.0 = менее насыщенно, >1.0 = более насыщенно
2173
+ this._step4.saturation = Math.min(3.0, Math.max(0.0, v));
2174
+ this.#applyStep4Uniforms();
2175
+ }
2176
+
2177
+ setStep4Contrast(value) {
2178
+ const v = Number(value);
2179
+ if (!Number.isFinite(v)) return;
2180
+ // 1.0 = без изменений, <1.0 = ниже контраст, >1.0 = выше контраст
2181
+ this._step4.contrast = Math.min(3.0, Math.max(0.0, v));
2182
+ this.#applyStep4Uniforms();
2183
+ }
2184
+
2185
+ #applyStep4Uniforms() {
2186
+ if (!this._step4Pass?.uniforms) return;
2187
+ if (this._step4Pass.uniforms.saturation) this._step4Pass.uniforms.saturation.value = this._step4.saturation ?? 1.0;
2188
+ if (this._step4Pass.uniforms.contrast) this._step4Pass.uniforms.contrast.value = this._step4.contrast ?? 1.0;
2189
+ }
2190
+
1942
2191
  #applyColorCorrectionUniforms() {
1943
2192
  if (this._hueSatPass?.uniforms) {
1944
2193
  this._hueSatPass.uniforms.hue.value = this.visual.color.hue ?? 0.0;
@@ -2098,6 +2347,63 @@ export class Viewer {
2098
2347
  this._composer.addPass(this._bcPass);
2099
2348
 
2100
2349
  this.#applyColorCorrectionUniforms();
2350
+
2351
+ // Шаг 4: финальный цветовой pass (должен применяться в самом конце конвейера)
2352
+ const step4Shader = {
2353
+ uniforms: {
2354
+ tDiffuse: { value: null },
2355
+ saturation: { value: this._step4?.saturation ?? 1.0 },
2356
+ contrast: { value: this._step4?.contrast ?? 1.0 },
2357
+ },
2358
+ vertexShader: `
2359
+ varying vec2 vUv;
2360
+ void main() {
2361
+ vUv = uv;
2362
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
2363
+ }
2364
+ `,
2365
+ fragmentShader: `
2366
+ uniform sampler2D tDiffuse;
2367
+ uniform float saturation; // 1.0 = no change
2368
+ uniform float contrast; // 1.0 = no change
2369
+ varying vec2 vUv;
2370
+
2371
+ vec3 rgb2hsv(vec3 c) {
2372
+ vec4 K = vec4(0.0, -1.0/3.0, 2.0/3.0, -1.0);
2373
+ vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
2374
+ vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
2375
+ float d = q.x - min(q.w, q.y);
2376
+ float e = 1.0e-10;
2377
+ return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
2378
+ }
2379
+
2380
+ vec3 hsv2rgb(vec3 c) {
2381
+ vec4 K = vec4(1.0, 2.0/3.0, 1.0/3.0, 3.0);
2382
+ vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
2383
+ return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
2384
+ }
2385
+
2386
+ void main() {
2387
+ vec4 color = texture2D(tDiffuse, vUv);
2388
+
2389
+ // Contrast first (pivot at 0.5), then clamp to keep values stable
2390
+ vec3 rgb = (color.rgb - 0.5) * contrast + 0.5;
2391
+ rgb = clamp(rgb, 0.0, 1.0);
2392
+
2393
+ // Saturation in HSV: лучше сохраняет оттенок в тёмных тонах (например, у синеватой тени)
2394
+ vec3 hsv = rgb2hsv(rgb);
2395
+ hsv.y = clamp(hsv.y * saturation, 0.0, 1.0);
2396
+ rgb = hsv2rgb(hsv);
2397
+
2398
+ color.rgb = clamp(rgb, 0.0, 1.0);
2399
+ gl_FragColor = color;
2400
+ }
2401
+ `,
2402
+ };
2403
+ this._step4Pass = new ShaderPass(step4Shader);
2404
+ this._step4Pass.enabled = !!this._step4?.enabled;
2405
+ this._composer.addPass(this._step4Pass);
2406
+ this.#applyStep4Uniforms();
2101
2407
  try { this._composer.setSize(w, h); } catch (_) {}
2102
2408
  }
2103
2409