@sequent-org/ifc-viewer 1.2.4-ci.40.0 → 1.2.4-ci.42.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.40.0",
4
+ "version": "1.2.4-ci.42.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",
@@ -113,7 +113,7 @@ export class IfcService {
113
113
  // Ранее используемые флаги вроде USE_FAST_BOOLS/SMALL_TRIANGLE_THRESHOLD библиотекой web-ifc не используются.
114
114
  const config = {
115
115
  COORDINATE_TO_ORIGIN: true,
116
- CIRCLE_SEGMENTS: 12,
116
+ CIRCLE_SEGMENTS: 32, // Увеличено с 12 до 32 для гладких кривых (уменьшает "лесенку")
117
117
  // Tolerances: см. web-ifc CreateSettings defaults
118
118
  TOLERANCE_PLANE_INTERSECTION: 1e-4,
119
119
  TOLERANCE_PLANE_DEVIATION: 1e-4,
@@ -9,6 +9,7 @@ import { SSAOPass } from "three/examples/jsm/postprocessing/SSAOPass.js";
9
9
  import { ShaderPass } from "three/examples/jsm/postprocessing/ShaderPass.js";
10
10
  import { HueSaturationShader } from "three/examples/jsm/shaders/HueSaturationShader.js";
11
11
  import { BrightnessContrastShader } from "three/examples/jsm/shaders/BrightnessContrastShader.js";
12
+ import { FXAAShader } from "three/examples/jsm/shaders/FXAAShader.js";
12
13
  import { RoomEnvironment } from "three/examples/jsm/environments/RoomEnvironment.js";
13
14
  import { NavCube } from "./NavCube.js";
14
15
  import { SectionManipulator } from "./SectionManipulator.js";
@@ -143,6 +144,14 @@ export class Viewer {
143
144
  z: null,
144
145
  },
145
146
  };
147
+ // Состояние "есть ли активное сечение" (нужно, чтобы сечение влияло на тени/освещение).
148
+ this._sectionClippingActive = false;
149
+ /** @type {WeakMap<THREE.Mesh, any>} */
150
+ this._sectionOriginalMaterial = new WeakMap();
151
+ // +50% яркости при активном сечении (комнаты светлее)
152
+ this._sectionLightBoost = { mul: 1.5, snapshot: null };
153
+ // При активном сечении: AO выключаем (убирает "сетку"), а контраст возвращаем лёгкой цветокоррекцией
154
+ this._sectionPostBoost = { snapshot: null, brightness: 0.03, contrast: 0.15 };
146
155
 
147
156
  // Snapshot начального состояния для Home
148
157
  this._home = {
@@ -1056,6 +1065,13 @@ export class Viewer {
1056
1065
  if (this._ssaoPass?.setSize) {
1057
1066
  try { this._ssaoPass.setSize(width, height); } catch (_) {}
1058
1067
  }
1068
+ // Обновляем FXAA resolution при изменении размера
1069
+ if (this._fxaaPass) {
1070
+ try {
1071
+ this._fxaaPass.material.uniforms['resolution'].value.x = 1 / Math.max(1, width);
1072
+ this._fxaaPass.material.uniforms['resolution'].value.y = 1 / Math.max(1, height);
1073
+ } catch (_) {}
1074
+ }
1059
1075
 
1060
1076
  // Если активен MMB-pan (viewOffset), нужно переустановить его под новый размер
1061
1077
  try { this._mmbPan?.controller?.applyCurrentOffset?.(width, height); } catch (_) {}
@@ -1184,8 +1200,8 @@ export class Viewer {
1184
1200
  if (node.isMesh) {
1185
1201
  // Тени управляются единообразно через setShadowsEnabled()
1186
1202
  node.castShadow = !!this.shadowsEnabled;
1187
- // Самозатенение включается только в пресете "Тест"
1188
- node.receiveShadow = !!this.shadowsEnabled && !!this._testPreset?.enabled;
1203
+ // Самозатенение: в тест-пресете ИЛИ при активном сечении
1204
+ node.receiveShadow = this.#getModelReceiveShadowEnabled();
1189
1205
  // Стекло/прозрачность: рендерим после непрозрачных (уменьшает мерцание сортировки)
1190
1206
  try {
1191
1207
  const mats = Array.isArray(node.material) ? node.material : [node.material];
@@ -1199,6 +1215,8 @@ export class Viewer {
1199
1215
 
1200
1216
  // Материальный пресет (если выбран не original)
1201
1217
  this.#applyMaterialStyleToModel(object3D);
1218
+ // Синхронизируем "сечение → shadow-pass" для материалов после назначения пресета
1219
+ this.#applyClipShadowsToModelMaterials();
1202
1220
 
1203
1221
  // Настроим пределы зума и сфокусируемся на новой модели
1204
1222
  this.applyAdaptiveZoomLimits(object3D, { padding: 1.2, slack: 2.5, minRatio: 0.05, recenter: true });
@@ -1679,10 +1697,12 @@ export class Viewer {
1679
1697
  this.activeModel.traverse?.((node) => {
1680
1698
  if (!node?.isMesh) return;
1681
1699
  node.castShadow = next;
1682
- // Самозатенение включается только в пресете "Тест"
1683
- node.receiveShadow = next && !!this._testPreset?.enabled;
1700
+ // Самозатенение: в тест-пресете ИЛИ при активном сечении
1701
+ node.receiveShadow = this.#getModelReceiveShadowEnabled();
1684
1702
  });
1685
1703
  }
1704
+ // Если тени переключили — синхронизируем shadow-pass клиппинга
1705
+ this.#applyClipShadowsToModelMaterials();
1686
1706
  }
1687
1707
 
1688
1708
  /**
@@ -1847,9 +1867,180 @@ export class Viewer {
1847
1867
  this.activeModel.traverse?.((node) => {
1848
1868
  if (!node?.isMesh) return;
1849
1869
  node.castShadow = !!this.shadowsEnabled;
1850
- node.receiveShadow = false;
1870
+ node.receiveShadow = this.#getModelReceiveShadowEnabled();
1851
1871
  });
1852
1872
  }
1873
+ this.#applyClipShadowsToModelMaterials();
1874
+ }
1875
+
1876
+ #getModelReceiveShadowEnabled() {
1877
+ // Самозатенение нужно для корректного освещения при сечении (видны комнаты внутри).
1878
+ return !!this.shadowsEnabled && (this._sectionClippingActive || !!this._testPreset?.enabled);
1879
+ }
1880
+
1881
+ #getClipShadowsEnabled() {
1882
+ // Чтобы внешняя тень менялась при движении сечения, клиппинг должен участвовать в shadow-pass.
1883
+ return !!this.shadowsEnabled && !!this._sectionClippingActive;
1884
+ }
1885
+
1886
+ #applyClipShadowsToModelMaterials() {
1887
+ const model = this.activeModel || this.demoCube;
1888
+ if (!model) return;
1889
+ const enabled = this.#getClipShadowsEnabled();
1890
+ const touched = new Set();
1891
+ // ВАЖНО: НЕ использовать renderer.clippingPlanes (там каждый кадр создаётся новый массив).
1892
+ // Для shadow-pass материал должен держать актуальный список активных плоскостей.
1893
+ const activePlanes = enabled
1894
+ ? (this.clipping?.planes || []).filter((p) => p && isFinite(p.constant))
1895
+ : null;
1896
+ model.traverse?.((node) => {
1897
+ if (!node?.isMesh) return;
1898
+ const mats = Array.isArray(node.material) ? node.material : [node.material];
1899
+ for (const m of mats) {
1900
+ if (!m || touched.has(m)) continue;
1901
+ touched.add(m);
1902
+ try {
1903
+ // Важно: для shadow-pass надёжнее использовать LOCAL clipping (material.clippingPlanes) + clipShadows.
1904
+ // Иначе часть сборок three может не применять global renderer.clippingPlanes к shadow map.
1905
+ if ('clippingPlanes' in m) m.clippingPlanes = activePlanes;
1906
+ if ('clipShadows' in m) m.clipShadows = enabled;
1907
+ // Тени “среза” и внутренняя отрисовка часто требуют double-side (в IFC нормали бывают проблемные).
1908
+ if (enabled && 'side' in m && m.side !== THREE.DoubleSide) m.side = THREE.DoubleSide;
1909
+ m.needsUpdate = true; // пересобрать шейдеры (включая depth/shadow варианты)
1910
+ } catch (_) {}
1911
+ }
1912
+ });
1913
+ }
1914
+
1915
+ #applySectionMaterialPolicy() {
1916
+ // При активном сечении переводим неосвещаемые материалы в MeshStandardMaterial,
1917
+ // чтобы внутри появились свет/тени. При выключении — возвращаем оригиналы.
1918
+ const model = this.activeModel || this.demoCube;
1919
+ if (!model) return;
1920
+
1921
+ if (!this._sectionClippingActive) {
1922
+ model.traverse?.((node) => {
1923
+ if (!node?.isMesh) return;
1924
+ const orig = this._sectionOriginalMaterial.get(node);
1925
+ if (orig) node.material = orig;
1926
+ });
1927
+ return;
1928
+ }
1929
+
1930
+ model.traverse?.((node) => {
1931
+ if (!node?.isMesh) return;
1932
+ const cur = node.material;
1933
+ if (!cur) return;
1934
+ // Трогаем только "неосвещаемые" материалы (они не показывают тени на стенах).
1935
+ const arr = Array.isArray(cur) ? cur : [cur];
1936
+ const hasBasic = arr.some((m) => !!m?.isMeshBasicMaterial);
1937
+ if (!hasBasic) return;
1938
+
1939
+ if (!this._sectionOriginalMaterial.has(node)) {
1940
+ this._sectionOriginalMaterial.set(node, cur);
1941
+ }
1942
+
1943
+ const convert = (m) => {
1944
+ if (!m?.isMeshBasicMaterial) return m;
1945
+ const cm = this.#getConvertedMaterial(m); // -> MeshStandardMaterial с сохранением color/map/alpha
1946
+ // Делаем "архитектурный" вид по умолчанию: матовый, без металла
1947
+ try { if ('roughness' in cm) cm.roughness = 0.9; } catch (_) {}
1948
+ try { if ('metalness' in cm) cm.metalness = 0.0; } catch (_) {}
1949
+ try { cm.needsUpdate = true; } catch (_) {}
1950
+ return cm;
1951
+ };
1952
+
1953
+ node.material = Array.isArray(cur) ? cur.map(convert) : convert(cur);
1954
+ });
1955
+ }
1956
+
1957
+ #syncSectionClippingState() {
1958
+ const active = this.clipping?.planes?.some((p) => p && isFinite(p.constant));
1959
+ const next = !!active;
1960
+ if (next === this._sectionClippingActive) return;
1961
+ this._sectionClippingActive = next;
1962
+
1963
+ // 0) Светлее при сечении (+50%)
1964
+ try {
1965
+ if (this._sectionClippingActive) {
1966
+ if (!this._sectionLightBoost.snapshot) {
1967
+ this._sectionLightBoost.snapshot = {
1968
+ sunIntensity: this.sunLight?.intensity ?? null,
1969
+ ambientIntensity: this.ambientLight?.intensity ?? null,
1970
+ };
1971
+ }
1972
+ if (this.sunLight && Number.isFinite(this._sectionLightBoost.snapshot.sunIntensity)) {
1973
+ this.sunLight.intensity = this._sectionLightBoost.snapshot.sunIntensity * this._sectionLightBoost.mul;
1974
+ }
1975
+ if (this.ambientLight && Number.isFinite(this._sectionLightBoost.snapshot.ambientIntensity)) {
1976
+ this.ambientLight.intensity = this._sectionLightBoost.snapshot.ambientIntensity * this._sectionLightBoost.mul;
1977
+ }
1978
+ } else {
1979
+ const snap = this._sectionLightBoost.snapshot;
1980
+ if (snap) {
1981
+ if (this.sunLight && Number.isFinite(snap.sunIntensity)) this.sunLight.intensity = snap.sunIntensity;
1982
+ if (this.ambientLight && Number.isFinite(snap.ambientIntensity)) this.ambientLight.intensity = snap.ambientIntensity;
1983
+ }
1984
+ this._sectionLightBoost.snapshot = null;
1985
+ }
1986
+ } catch (_) {}
1987
+
1988
+ // 0.5) Пост-эффекты: AO OFF + лёгкий контраст (только при сечении)
1989
+ try {
1990
+ if (this._sectionClippingActive) {
1991
+ if (!this._sectionPostBoost.snapshot) {
1992
+ this._sectionPostBoost.snapshot = {
1993
+ ao: { ...this.visual.ao },
1994
+ color: { ...this.visual.color },
1995
+ };
1996
+ }
1997
+ // AO OFF
1998
+ this.setAOEnabled(false);
1999
+ // Color correction ON (без изменения hue/saturation)
2000
+ this.setColorCorrectionEnabled(true);
2001
+ this.setColorBrightness(this._sectionPostBoost.brightness);
2002
+ this.setColorContrast(this._sectionPostBoost.contrast);
2003
+ } else {
2004
+ const snap = this._sectionPostBoost.snapshot;
2005
+ if (snap) {
2006
+ // Restore AO
2007
+ this.setAOEnabled(!!snap.ao?.enabled);
2008
+ if (typeof snap.ao?.intensity === 'number') this.setAOIntensity(snap.ao.intensity);
2009
+ if (typeof snap.ao?.radius === 'number') this.setAORadius(snap.ao.radius);
2010
+ if (typeof snap.ao?.minDistance === 'number') this.visual.ao.minDistance = snap.ao.minDistance;
2011
+ if (typeof snap.ao?.maxDistance === 'number') this.visual.ao.maxDistance = snap.ao.maxDistance;
2012
+ if (this._ssaoPass) {
2013
+ this._ssaoPass.minDistance = this.visual.ao.minDistance;
2014
+ this._ssaoPass.maxDistance = this.visual.ao.maxDistance;
2015
+ }
2016
+ // Restore color correction
2017
+ this.setColorCorrectionEnabled(!!snap.color?.enabled);
2018
+ if (typeof snap.color?.hue === 'number') this.setColorHue(snap.color.hue);
2019
+ if (typeof snap.color?.saturation === 'number') this.setColorSaturation(snap.color.saturation);
2020
+ if (typeof snap.color?.brightness === 'number') this.setColorBrightness(snap.color.brightness);
2021
+ if (typeof snap.color?.contrast === 'number') this.setColorContrast(snap.color.contrast);
2022
+ }
2023
+ this._sectionPostBoost.snapshot = null;
2024
+ }
2025
+ } catch (_) {}
2026
+
2027
+ // 1) self-shadowing (комнаты/стены)
2028
+ if (this.activeModel) {
2029
+ this.activeModel.traverse?.((node) => {
2030
+ if (!node?.isMesh) return;
2031
+ try { node.receiveShadow = this.#getModelReceiveShadowEnabled(); } catch (_) {}
2032
+ });
2033
+ }
2034
+
2035
+ // 1.5) materials: ensure they react to light/shadows when section is active
2036
+ this.#applySectionMaterialPolicy();
2037
+
2038
+ // 2) clipping in shadow pass (крыша перестаёт участвовать в тени)
2039
+ this.#applyClipShadowsToModelMaterials();
2040
+
2041
+ // 3) форсируем апдейт теней
2042
+ try { if (this.sunLight?.shadow) this.sunLight.shadow.needsUpdate = true; } catch (_) {}
2043
+ try { if (this.renderer?.shadowMap) this.renderer.shadowMap.needsUpdate = true; } catch (_) {}
1853
2044
  }
1854
2045
 
1855
2046
  /**
@@ -2722,6 +2913,14 @@ export class Viewer {
2722
2913
  this._step4Pass.enabled = !!this._step4?.enabled;
2723
2914
  this._composer.addPass(this._step4Pass);
2724
2915
  this.#applyStep4Uniforms();
2916
+
2917
+ // FXAA pass для устранения "лесенки" на кривых линиях (aliasing)
2918
+ this._fxaaPass = new ShaderPass(FXAAShader);
2919
+ this._fxaaPass.material.uniforms['resolution'].value.x = 1 / w;
2920
+ this._fxaaPass.material.uniforms['resolution'].value.y = 1 / h;
2921
+ this._fxaaPass.enabled = true; // Включен всегда для сглаживания
2922
+ this._composer.addPass(this._fxaaPass);
2923
+
2725
2924
  try { this._composer.setSize(w, h); } catch (_) {}
2726
2925
  }
2727
2926
 
@@ -3020,6 +3219,9 @@ export class Viewer {
3020
3219
  plane.constant = Infinity;
3021
3220
  this.#setGizmoVisible(axis, false);
3022
3221
  }
3222
+
3223
+ // Сечение → тени/освещение
3224
+ this.#syncSectionClippingState();
3023
3225
  }
3024
3226
 
3025
3227
  // Устанавливает позицию секущей плоскости по нормализованному значению [0..1] в пределах габаритов модели