@sequent-org/ifc-viewer 1.2.4-ci.42.0 → 1.2.4-ci.44.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/viewer/SectionCapsPass.js +53 -0
- package/src/viewer/SectionCapsRenderer.js +224 -0
- package/src/viewer/Viewer.js +147 -69
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.44.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",
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Pass } from "three/examples/jsm/postprocessing/Pass.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pass для EffectComposer: рисует "cap" сечения поверх текущего буфера,
|
|
5
|
+
* используя depth+stencil текущего renderTarget.
|
|
6
|
+
*/
|
|
7
|
+
export class SectionCapsPass extends Pass {
|
|
8
|
+
/**
|
|
9
|
+
* @param {{
|
|
10
|
+
* capsRenderer: { render(args: any): void },
|
|
11
|
+
* getScene: () => any,
|
|
12
|
+
* getCamera: () => any,
|
|
13
|
+
* getSubject: () => any,
|
|
14
|
+
* getActivePlanes: () => any[],
|
|
15
|
+
* }} args
|
|
16
|
+
*/
|
|
17
|
+
constructor({ capsRenderer, getScene, getCamera, getSubject, getActivePlanes }) {
|
|
18
|
+
super();
|
|
19
|
+
this._caps = capsRenderer;
|
|
20
|
+
this._getScene = getScene;
|
|
21
|
+
this._getCamera = getCamera;
|
|
22
|
+
this._getSubject = getSubject;
|
|
23
|
+
this._getActivePlanes = getActivePlanes;
|
|
24
|
+
|
|
25
|
+
// Рисуем поверх readBuffer, swap не нужен
|
|
26
|
+
this.needsSwap = false;
|
|
27
|
+
this.clear = false;
|
|
28
|
+
this.enabled = true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// eslint-disable-next-line no-unused-vars
|
|
32
|
+
render(renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */) {
|
|
33
|
+
if (!this.enabled) return;
|
|
34
|
+
const scene = this._getScene?.();
|
|
35
|
+
const camera = this._getCamera?.();
|
|
36
|
+
const subject = this._getSubject?.();
|
|
37
|
+
const planes = this._getActivePlanes?.() || [];
|
|
38
|
+
const activePlanes = planes.filter((p) => p && isFinite(p.constant));
|
|
39
|
+
if (!scene || !camera || !subject || activePlanes.length === 0) return;
|
|
40
|
+
|
|
41
|
+
// Важно: рисуем в readBuffer (текущий буфер композера)
|
|
42
|
+
renderer.setRenderTarget(this.renderToScreen ? null : readBuffer);
|
|
43
|
+
this._caps.render({
|
|
44
|
+
renderer,
|
|
45
|
+
scene,
|
|
46
|
+
camera,
|
|
47
|
+
subject,
|
|
48
|
+
activePlanes,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Рисует "заглушку" (cap) для секущих плоскостей через stencil buffer.
|
|
5
|
+
*
|
|
6
|
+
* Идея: для каждой активной плоскости
|
|
7
|
+
* 1) два прохода по модели (BackSide/FrontSide) с инкрементом/декрементом stencil
|
|
8
|
+
* 2) рисуем большую плоскость на месте сечения с stencilFunc != 0
|
|
9
|
+
*
|
|
10
|
+
* Это закрывает "пустоту" в двухслойных стенах и убирает мерцание внутри.
|
|
11
|
+
*/
|
|
12
|
+
export class SectionCapsRenderer {
|
|
13
|
+
/**
|
|
14
|
+
* @param {{ color?: number }} options
|
|
15
|
+
*/
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
this.color = options.color ?? 0x212121;
|
|
18
|
+
|
|
19
|
+
/** @type {THREE.Scene} */
|
|
20
|
+
this._capScene = new THREE.Scene();
|
|
21
|
+
|
|
22
|
+
/** @type {THREE.Mesh<THREE.PlaneGeometry, THREE.MeshBasicMaterial>} */
|
|
23
|
+
this._capMesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1, 1, 1), this._createCapMaterial());
|
|
24
|
+
this._capMesh.frustumCulled = false;
|
|
25
|
+
this._capScene.add(this._capMesh);
|
|
26
|
+
|
|
27
|
+
this._stencilBack = this._createStencilMaterial({
|
|
28
|
+
side: THREE.BackSide,
|
|
29
|
+
zOp: THREE.IncrementWrapStencilOp,
|
|
30
|
+
});
|
|
31
|
+
this._stencilFront = this._createStencilMaterial({
|
|
32
|
+
side: THREE.FrontSide,
|
|
33
|
+
zOp: THREE.DecrementWrapStencilOp,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
this._tmp = {
|
|
37
|
+
v0: new THREE.Vector3(),
|
|
38
|
+
v1: new THREE.Vector3(),
|
|
39
|
+
box: new THREE.Box3(),
|
|
40
|
+
size: new THREE.Vector3(),
|
|
41
|
+
center: new THREE.Vector3(),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
this._warnedNoStencil = false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** @param {THREE.WebGLRenderer} renderer */
|
|
48
|
+
_hasStencil(renderer) {
|
|
49
|
+
try {
|
|
50
|
+
const gl = renderer.getContext();
|
|
51
|
+
const attrs = gl?.getContextAttributes?.();
|
|
52
|
+
// В WebGL2/рендер-таргетах stencil может быть и при attrs.stencil=false,
|
|
53
|
+
// но если attrs явно false — предупредим один раз, а дальше попытаемся всё равно.
|
|
54
|
+
if (attrs && attrs.stencil === false && !this._warnedNoStencil) {
|
|
55
|
+
this._warnedNoStencil = true;
|
|
56
|
+
// eslint-disable-next-line no-console
|
|
57
|
+
console.warn("[SectionCaps] WebGL context reports stencil=false; caps may not render. Consider enabling stencil in WebGLRenderer.");
|
|
58
|
+
}
|
|
59
|
+
} catch (_) {}
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
_createStencilMaterial({ side, zOp }) {
|
|
64
|
+
const m = new THREE.MeshBasicMaterial({
|
|
65
|
+
color: 0xffffff,
|
|
66
|
+
side,
|
|
67
|
+
// ничего в цвет не пишем — только stencil
|
|
68
|
+
colorWrite: false,
|
|
69
|
+
depthWrite: false,
|
|
70
|
+
depthTest: true,
|
|
71
|
+
});
|
|
72
|
+
m.stencilWrite = true;
|
|
73
|
+
m.stencilRef = 0;
|
|
74
|
+
m.stencilFunc = THREE.AlwaysStencilFunc;
|
|
75
|
+
m.stencilFail = THREE.KeepStencilOp;
|
|
76
|
+
m.stencilZFail = zOp;
|
|
77
|
+
m.stencilZPass = zOp;
|
|
78
|
+
// локальный клиппинг на материале
|
|
79
|
+
m.clippingPlanes = null;
|
|
80
|
+
m.clipIntersection = false;
|
|
81
|
+
m.clipShadows = false;
|
|
82
|
+
return m;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
_createCapMaterial() {
|
|
86
|
+
const m = new THREE.MeshBasicMaterial({
|
|
87
|
+
color: this.color,
|
|
88
|
+
side: THREE.DoubleSide,
|
|
89
|
+
depthTest: true,
|
|
90
|
+
depthWrite: false,
|
|
91
|
+
transparent: false,
|
|
92
|
+
polygonOffset: true,
|
|
93
|
+
polygonOffsetFactor: -1,
|
|
94
|
+
polygonOffsetUnits: -1,
|
|
95
|
+
});
|
|
96
|
+
m.stencilWrite = true;
|
|
97
|
+
m.stencilRef = 0;
|
|
98
|
+
m.stencilFunc = THREE.NotEqualStencilFunc;
|
|
99
|
+
m.stencilFail = THREE.KeepStencilOp;
|
|
100
|
+
m.stencilZFail = THREE.KeepStencilOp;
|
|
101
|
+
m.stencilZPass = THREE.KeepStencilOp;
|
|
102
|
+
m.stencilMask = 0xff;
|
|
103
|
+
m.clippingPlanes = null; // задаём на кадр
|
|
104
|
+
m.clipIntersection = false;
|
|
105
|
+
m.clipShadows = false;
|
|
106
|
+
return m;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @param {{ obj: THREE.Object3D, root: THREE.Object3D }} args
|
|
111
|
+
*/
|
|
112
|
+
_isInSubtree({ obj, root }) {
|
|
113
|
+
let n = obj;
|
|
114
|
+
while (n) {
|
|
115
|
+
if (n === root) return true;
|
|
116
|
+
n = n.parent;
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Обновляет положение/ориентацию "большой" плоскости cap.
|
|
123
|
+
* @param {THREE.Plane} plane
|
|
124
|
+
* @param {THREE.Object3D} subject
|
|
125
|
+
*/
|
|
126
|
+
_updateCapMeshTransform(plane, subject) {
|
|
127
|
+
const { box, size, v0, v1 } = this._tmp;
|
|
128
|
+
box.setFromObject(subject);
|
|
129
|
+
box.getSize(size);
|
|
130
|
+
const maxDim = Math.max(size.x, size.y, size.z);
|
|
131
|
+
const extent = Math.max(1e-3, maxDim * 2.2); // запас, stencil ограничит до контура среза
|
|
132
|
+
|
|
133
|
+
// PlaneGeometry лежит в XY и смотрит в +Z
|
|
134
|
+
v0.set(0, 0, 1);
|
|
135
|
+
v1.copy(plane.normal).normalize();
|
|
136
|
+
this._capMesh.quaternion.setFromUnitVectors(v0, v1);
|
|
137
|
+
|
|
138
|
+
// точка на плоскости: p0 = -constant * normal
|
|
139
|
+
this._capMesh.position.copy(plane.normal).multiplyScalar(-plane.constant);
|
|
140
|
+
this._capMesh.scale.set(extent, extent, 1);
|
|
141
|
+
this._capMesh.updateMatrixWorld(true);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Рисует cap'ы в текущий render target (screen или буфер композера).
|
|
146
|
+
*
|
|
147
|
+
* @param {{
|
|
148
|
+
* renderer: THREE.WebGLRenderer,
|
|
149
|
+
* scene: THREE.Scene,
|
|
150
|
+
* camera: THREE.Camera,
|
|
151
|
+
* subject: THREE.Object3D | null,
|
|
152
|
+
* activePlanes: THREE.Plane[],
|
|
153
|
+
* }} args
|
|
154
|
+
*/
|
|
155
|
+
render({ renderer, scene, camera, subject, activePlanes }) {
|
|
156
|
+
if (!renderer || !scene || !camera) return;
|
|
157
|
+
if (!subject) return;
|
|
158
|
+
if (!activePlanes || activePlanes.length === 0) return;
|
|
159
|
+
this._hasStencil(renderer);
|
|
160
|
+
|
|
161
|
+
// Спрячем все меши вне модели, чтобы stencil не "цеплял" землю/тень и прочее
|
|
162
|
+
const hidden = [];
|
|
163
|
+
try {
|
|
164
|
+
scene.traverse((node) => {
|
|
165
|
+
if (!node?.isMesh) return;
|
|
166
|
+
if (this._isInSubtree({ obj: node, root: subject })) return;
|
|
167
|
+
if (node.visible) {
|
|
168
|
+
node.visible = false;
|
|
169
|
+
hidden.push(node);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
} catch (_) {}
|
|
173
|
+
|
|
174
|
+
const prevOverride = scene.overrideMaterial;
|
|
175
|
+
const prevLocal = renderer.localClippingEnabled;
|
|
176
|
+
const prevGlobalPlanes = renderer.clippingPlanes;
|
|
177
|
+
|
|
178
|
+
// Важно: для материалов с local clipping нужно localClippingEnabled=true,
|
|
179
|
+
// а глобальные renderer.clippingPlanes отключаем, чтобы cap не резало "самой собой".
|
|
180
|
+
renderer.localClippingEnabled = true;
|
|
181
|
+
renderer.clippingPlanes = [];
|
|
182
|
+
|
|
183
|
+
const gl = renderer.getContext();
|
|
184
|
+
const prevAutoClear = renderer.autoClear;
|
|
185
|
+
renderer.autoClear = false;
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
for (const p of activePlanes) {
|
|
189
|
+
if (!p || !isFinite(p.constant)) continue;
|
|
190
|
+
|
|
191
|
+
// Чистим stencil перед обработкой каждой плоскости
|
|
192
|
+
try {
|
|
193
|
+
gl.clearStencil(0);
|
|
194
|
+
gl.clear(gl.STENCIL_BUFFER_BIT);
|
|
195
|
+
} catch (_) {}
|
|
196
|
+
|
|
197
|
+
// Stencil pass: back faces (+1)
|
|
198
|
+
this._stencilBack.clippingPlanes = activePlanes;
|
|
199
|
+
scene.overrideMaterial = this._stencilBack;
|
|
200
|
+
renderer.render(scene, camera);
|
|
201
|
+
|
|
202
|
+
// Stencil pass: front faces (-1)
|
|
203
|
+
this._stencilFront.clippingPlanes = activePlanes;
|
|
204
|
+
scene.overrideMaterial = this._stencilFront;
|
|
205
|
+
renderer.render(scene, camera);
|
|
206
|
+
|
|
207
|
+
// Cap plane: stencil != 0, и клиппинг только другими плоскостями (без текущей)
|
|
208
|
+
const capMat = this._capMesh.material;
|
|
209
|
+
capMat.color.setHex(this.color);
|
|
210
|
+
capMat.clippingPlanes = activePlanes.filter((q) => q !== p);
|
|
211
|
+
this._updateCapMeshTransform(p, subject);
|
|
212
|
+
renderer.render(this._capScene, camera);
|
|
213
|
+
}
|
|
214
|
+
} finally {
|
|
215
|
+
scene.overrideMaterial = prevOverride;
|
|
216
|
+
renderer.localClippingEnabled = prevLocal;
|
|
217
|
+
renderer.clippingPlanes = prevGlobalPlanes;
|
|
218
|
+
renderer.autoClear = prevAutoClear;
|
|
219
|
+
for (const n of hidden) n.visible = true;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
package/src/viewer/Viewer.js
CHANGED
|
@@ -13,6 +13,8 @@ import { FXAAShader } from "three/examples/jsm/shaders/FXAAShader.js";
|
|
|
13
13
|
import { RoomEnvironment } from "three/examples/jsm/environments/RoomEnvironment.js";
|
|
14
14
|
import { NavCube } from "./NavCube.js";
|
|
15
15
|
import { SectionManipulator } from "./SectionManipulator.js";
|
|
16
|
+
import { SectionCapsRenderer } from "./SectionCapsRenderer.js";
|
|
17
|
+
import { SectionCapsPass } from "./SectionCapsPass.js";
|
|
16
18
|
import { ZoomToCursorController } from "./ZoomToCursorController.js";
|
|
17
19
|
import { MiddleMousePanController } from "./MiddleMousePanController.js";
|
|
18
20
|
import { RightMouseModelMoveController } from "./RightMouseModelMoveController.js";
|
|
@@ -126,6 +128,9 @@ export class Viewer {
|
|
|
126
128
|
// Шаг 4: финальная постобработка (контраст/насыщенность) — должна быть последним pass'ом
|
|
127
129
|
this._step4Pass = null;
|
|
128
130
|
this._step4 = { enabled: false, saturation: 1.0, contrast: 1.0 };
|
|
131
|
+
// Сечение: заливка "внутренностей" (cap) через stencil buffer
|
|
132
|
+
this._sectionCaps = new SectionCapsRenderer({ color: 0x212121 });
|
|
133
|
+
this._sectionCapsPass = null;
|
|
129
134
|
this.clipping = {
|
|
130
135
|
enabled: false,
|
|
131
136
|
planes: [
|
|
@@ -148,10 +153,10 @@ export class Viewer {
|
|
|
148
153
|
this._sectionClippingActive = false;
|
|
149
154
|
/** @type {WeakMap<THREE.Mesh, any>} */
|
|
150
155
|
this._sectionOriginalMaterial = new WeakMap();
|
|
151
|
-
//
|
|
152
|
-
this.
|
|
153
|
-
//
|
|
154
|
-
this.
|
|
156
|
+
// Локальная подсветка "внутри": без теней, с малой дальностью (чтобы не пересвечивать фасад/внешнюю тень)
|
|
157
|
+
this._interiorAssist = { light: null, box: null, enabled: false, lastBoxAt: 0 };
|
|
158
|
+
// Пост-эффекты только "внутри": AO OFF (убирает "сетку"), чуть контраста (без глобального пересвета)
|
|
159
|
+
this._interiorPost = { snapshot: null, contrast: 0.12 };
|
|
155
160
|
|
|
156
161
|
// Snapshot начального состояния для Home
|
|
157
162
|
this._home = {
|
|
@@ -449,7 +454,8 @@ export class Viewer {
|
|
|
449
454
|
// Рендерер
|
|
450
455
|
// logarithmicDepthBuffer: уменьшает z-fighting на почти копланарных поверхностях (часто в IFC).
|
|
451
456
|
// Это заметно снижает "мигание" тонких накладных деталей на фасадах.
|
|
452
|
-
|
|
457
|
+
// stencil: нужен для отрисовки "cap" по контуру сечения
|
|
458
|
+
this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, logarithmicDepthBuffer: true, stencil: true });
|
|
453
459
|
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
|
454
460
|
this.renderer.autoClear = false; // управляем очисткой вручную для мульти-проходов
|
|
455
461
|
// Тени по умолчанию выключены (включаются только через setShadowsEnabled)
|
|
@@ -628,6 +634,9 @@ export class Viewer {
|
|
|
628
634
|
this.setSunHeight(5.9);
|
|
629
635
|
this.setShadowsEnabled(this.shadowsEnabled);
|
|
630
636
|
|
|
637
|
+
// Локальная подсветка "внутри" (по умолчанию скрыта)
|
|
638
|
+
this.#ensureInteriorAssistLight();
|
|
639
|
+
|
|
631
640
|
// Демонстрационный куб отключён для чистого прелоадера
|
|
632
641
|
// Оставим сцену пустой до загрузки модели
|
|
633
642
|
// Добавим метод фокусировки объекта
|
|
@@ -755,6 +764,8 @@ export class Viewer {
|
|
|
755
764
|
}
|
|
756
765
|
|
|
757
766
|
if (this.controls) this.controls.update();
|
|
767
|
+
// "Внутренняя" подсветка/пост-эффекты: включаются только когда камера внутри модели
|
|
768
|
+
this.#updateInteriorAssist();
|
|
758
769
|
this._notifyZoomIfChanged();
|
|
759
770
|
if (this.renderer && this.camera && this.scene) {
|
|
760
771
|
// Применим ТОЛЬКО активные (конечные) плоскости отсечения
|
|
@@ -770,6 +781,17 @@ export class Viewer {
|
|
|
770
781
|
this._composer.render();
|
|
771
782
|
} else {
|
|
772
783
|
this.renderer.render(this.scene, this.camera);
|
|
784
|
+
// Cap (закрытие среза) в режиме без композера
|
|
785
|
+
try {
|
|
786
|
+
const subject = this.activeModel || this.demoCube;
|
|
787
|
+
this._sectionCaps?.render?.({
|
|
788
|
+
renderer: this.renderer,
|
|
789
|
+
scene: this.scene,
|
|
790
|
+
camera: this.camera,
|
|
791
|
+
subject,
|
|
792
|
+
activePlanes,
|
|
793
|
+
});
|
|
794
|
+
} catch (_) {}
|
|
773
795
|
}
|
|
774
796
|
// Рендер оверлея манипуляторов без глобального клиппинга поверх
|
|
775
797
|
const prevLocal = this.renderer.localClippingEnabled;
|
|
@@ -1960,69 +1982,9 @@ export class Viewer {
|
|
|
1960
1982
|
if (next === this._sectionClippingActive) return;
|
|
1961
1983
|
this._sectionClippingActive = next;
|
|
1962
1984
|
|
|
1963
|
-
//
|
|
1964
|
-
|
|
1965
|
-
|
|
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 (_) {}
|
|
1985
|
+
// Важно: никаких глобальных усилений света/яркости на само включение сечения.
|
|
1986
|
+
// Подсветка делается локально и только когда камера "внутри".
|
|
1987
|
+
try { this.#updateInteriorAssist(true); } catch (_) {}
|
|
2026
1988
|
|
|
2027
1989
|
// 1) self-shadowing (комнаты/стены)
|
|
2028
1990
|
if (this.activeModel) {
|
|
@@ -2043,6 +2005,110 @@ export class Viewer {
|
|
|
2043
2005
|
try { if (this.renderer?.shadowMap) this.renderer.shadowMap.needsUpdate = true; } catch (_) {}
|
|
2044
2006
|
}
|
|
2045
2007
|
|
|
2008
|
+
#ensureInteriorAssistLight() {
|
|
2009
|
+
if (!this.scene) return;
|
|
2010
|
+
if (this._interiorAssist.light) return;
|
|
2011
|
+
try {
|
|
2012
|
+
// Небольшой "fill light" около камеры. Без теней, чтобы не ломать внешнюю тень.
|
|
2013
|
+
const light = new THREE.PointLight(0xffffff, 0.9, 6.5, 2.0);
|
|
2014
|
+
light.castShadow = false;
|
|
2015
|
+
light.visible = false;
|
|
2016
|
+
light.name = 'interior-assist-light';
|
|
2017
|
+
this.scene.add(light);
|
|
2018
|
+
this._interiorAssist.light = light;
|
|
2019
|
+
} catch (_) {
|
|
2020
|
+
this._interiorAssist.light = null;
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
#isCameraInsideModelBox() {
|
|
2025
|
+
const model = this.activeModel;
|
|
2026
|
+
if (!model || !this.camera) return false;
|
|
2027
|
+
const now = performance?.now?.() ?? Date.now();
|
|
2028
|
+
// Обновляем bbox не чаще ~4 раза/сек (достаточно; модель обычно статична)
|
|
2029
|
+
if (!this._interiorAssist.box || (now - (this._interiorAssist.lastBoxAt || 0)) > 250) {
|
|
2030
|
+
try {
|
|
2031
|
+
this._interiorAssist.box = new THREE.Box3().setFromObject(model);
|
|
2032
|
+
this._interiorAssist.lastBoxAt = now;
|
|
2033
|
+
} catch (_) {
|
|
2034
|
+
this._interiorAssist.box = null;
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
const box = this._interiorAssist.box;
|
|
2038
|
+
if (!box) return false;
|
|
2039
|
+
const p = this.camera.position;
|
|
2040
|
+
const eps = 0.05;
|
|
2041
|
+
return (
|
|
2042
|
+
p.x > (box.min.x + eps) && p.x < (box.max.x - eps) &&
|
|
2043
|
+
p.y > (box.min.y + eps) && p.y < (box.max.y - eps) &&
|
|
2044
|
+
p.z > (box.min.z + eps) && p.z < (box.max.z - eps)
|
|
2045
|
+
);
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
#updateInteriorAssist(force = false) {
|
|
2049
|
+
if (!this._sectionClippingActive) {
|
|
2050
|
+
if (this._interiorAssist.enabled || force) {
|
|
2051
|
+
this._interiorAssist.enabled = false;
|
|
2052
|
+
if (this._interiorAssist.light) this._interiorAssist.light.visible = false;
|
|
2053
|
+
this.#restoreInteriorPost();
|
|
2054
|
+
}
|
|
2055
|
+
return;
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
this.#ensureInteriorAssistLight();
|
|
2059
|
+
const inside = this.#isCameraInsideModelBox();
|
|
2060
|
+
if (inside !== this._interiorAssist.enabled || force) {
|
|
2061
|
+
this._interiorAssist.enabled = inside;
|
|
2062
|
+
if (this._interiorAssist.light) this._interiorAssist.light.visible = inside;
|
|
2063
|
+
if (inside) this.#applyInteriorPost();
|
|
2064
|
+
else this.#restoreInteriorPost();
|
|
2065
|
+
}
|
|
2066
|
+
// Следуем за камерой, если активны
|
|
2067
|
+
if (inside && this._interiorAssist.light) {
|
|
2068
|
+
try { this._interiorAssist.light.position.copy(this.camera.position); } catch (_) {}
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
#applyInteriorPost() {
|
|
2073
|
+
// AO OFF + лёгкий контраст только "внутри"
|
|
2074
|
+
if (!this._interiorPost.snapshot) {
|
|
2075
|
+
this._interiorPost.snapshot = {
|
|
2076
|
+
ao: { ...this.visual.ao },
|
|
2077
|
+
color: { ...this.visual.color },
|
|
2078
|
+
};
|
|
2079
|
+
}
|
|
2080
|
+
try { this.setAOEnabled(false); } catch (_) {}
|
|
2081
|
+
try {
|
|
2082
|
+
this.setColorCorrectionEnabled(true);
|
|
2083
|
+
this.setColorContrast(this._interiorPost.contrast);
|
|
2084
|
+
// brightness не трогаем, чтобы не пересвечивать
|
|
2085
|
+
} catch (_) {}
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
#restoreInteriorPost() {
|
|
2089
|
+
const snap = this._interiorPost.snapshot;
|
|
2090
|
+
if (!snap) return;
|
|
2091
|
+
try {
|
|
2092
|
+
this.setAOEnabled(!!snap.ao?.enabled);
|
|
2093
|
+
if (typeof snap.ao?.intensity === 'number') this.setAOIntensity(snap.ao.intensity);
|
|
2094
|
+
if (typeof snap.ao?.radius === 'number') this.setAORadius(snap.ao.radius);
|
|
2095
|
+
if (typeof snap.ao?.minDistance === 'number') this.visual.ao.minDistance = snap.ao.minDistance;
|
|
2096
|
+
if (typeof snap.ao?.maxDistance === 'number') this.visual.ao.maxDistance = snap.ao.maxDistance;
|
|
2097
|
+
if (this._ssaoPass) {
|
|
2098
|
+
this._ssaoPass.minDistance = this.visual.ao.minDistance;
|
|
2099
|
+
this._ssaoPass.maxDistance = this.visual.ao.maxDistance;
|
|
2100
|
+
}
|
|
2101
|
+
} catch (_) {}
|
|
2102
|
+
try {
|
|
2103
|
+
this.setColorCorrectionEnabled(!!snap.color?.enabled);
|
|
2104
|
+
if (typeof snap.color?.hue === 'number') this.setColorHue(snap.color.hue);
|
|
2105
|
+
if (typeof snap.color?.saturation === 'number') this.setColorSaturation(snap.color.saturation);
|
|
2106
|
+
if (typeof snap.color?.brightness === 'number') this.setColorBrightness(snap.color.brightness);
|
|
2107
|
+
if (typeof snap.color?.contrast === 'number') this.setColorContrast(snap.color.contrast);
|
|
2108
|
+
} catch (_) {}
|
|
2109
|
+
this._interiorPost.snapshot = null;
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2046
2112
|
/**
|
|
2047
2113
|
* Дамп текущих параметров тест-пресета в консоль.
|
|
2048
2114
|
*/
|
|
@@ -2835,7 +2901,9 @@ export class Viewer {
|
|
|
2835
2901
|
const { width, height } = this._getContainerSize();
|
|
2836
2902
|
const w = Math.max(1, Math.floor(width));
|
|
2837
2903
|
const h = Math.max(1, Math.floor(height));
|
|
2838
|
-
|
|
2904
|
+
// Нужен stencil buffer в render targets композера для "cap" сечения
|
|
2905
|
+
const rt = new THREE.WebGLRenderTarget(w, h, { depthBuffer: true, stencilBuffer: true });
|
|
2906
|
+
this._composer = new EffectComposer(this.renderer, rt);
|
|
2839
2907
|
this._renderPass = new RenderPass(this.scene, this.camera);
|
|
2840
2908
|
this._composer.addPass(this._renderPass);
|
|
2841
2909
|
this._ssaoPass = new SSAOPass(this.scene, this.camera, w, h);
|
|
@@ -2914,6 +2982,16 @@ export class Viewer {
|
|
|
2914
2982
|
this._composer.addPass(this._step4Pass);
|
|
2915
2983
|
this.#applyStep4Uniforms();
|
|
2916
2984
|
|
|
2985
|
+
// Cap (закрытие сечения) — перед FXAA, чтобы сгладить край заливки
|
|
2986
|
+
this._sectionCapsPass = new SectionCapsPass({
|
|
2987
|
+
capsRenderer: this._sectionCaps,
|
|
2988
|
+
getScene: () => this.scene,
|
|
2989
|
+
getCamera: () => this.camera,
|
|
2990
|
+
getSubject: () => (this.activeModel || this.demoCube),
|
|
2991
|
+
getActivePlanes: () => (this.clipping?.planes || []),
|
|
2992
|
+
});
|
|
2993
|
+
this._composer.addPass(this._sectionCapsPass);
|
|
2994
|
+
|
|
2917
2995
|
// FXAA pass для устранения "лесенки" на кривых линиях (aliasing)
|
|
2918
2996
|
this._fxaaPass = new ShaderPass(FXAAShader);
|
|
2919
2997
|
this._fxaaPass.material.uniforms['resolution'].value.x = 1 / w;
|