@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 +1 -1
- package/src/ifc/IfcService.js +1 -1
- package/src/viewer/Viewer.js +207 -5
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sequent-org/ifc-viewer",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "1.2.4-ci.
|
|
4
|
+
"version": "1.2.4-ci.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",
|
package/src/ifc/IfcService.js
CHANGED
|
@@ -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,
|
package/src/viewer/Viewer.js
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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 =
|
|
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] в пределах габаритов модели
|