@sequent-org/ifc-viewer 1.2.4-ci.43.0 → 1.2.4-ci.45.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/IfcViewer.js +2 -2
- package/src/viewer/SectionCapsPass.js +53 -0
- package/src/viewer/SectionCapsRenderer.js +224 -0
- package/src/viewer/Viewer.js +41 -17
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.45.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
|
@@ -301,7 +301,7 @@ export class IfcViewer {
|
|
|
301
301
|
_createInterface() {
|
|
302
302
|
// Основная разметка просмотрщика
|
|
303
303
|
const html = `
|
|
304
|
-
<div class="ifc-viewer-container" style="width: 100%; height: 100%; position: relative; display: flex; flex-direction: column; border:0px red solid;">
|
|
304
|
+
<div class="ifc-viewer-container" style="width: 100%; height: 100%; position: relative; display: flex; flex-direction: column; background: #ffffff; border:0px red solid;">
|
|
305
305
|
<!-- Прелоадер -->
|
|
306
306
|
<div id="ifcPreloader" class="absolute inset-0 bg-base-100 flex items-center justify-center z-50">
|
|
307
307
|
<div class="text-center">
|
|
@@ -352,7 +352,7 @@ export class IfcViewer {
|
|
|
352
352
|
</div>
|
|
353
353
|
|
|
354
354
|
<!-- Основной контейнер просмотрщика -->
|
|
355
|
-
<div id="ifcViewerMain" class="w-full flex-1 relative"></div>
|
|
355
|
+
<div id="ifcViewerMain" class="w-full flex-1 relative bg-base-100" style="background: #ffffff;"></div>
|
|
356
356
|
|
|
357
357
|
<!-- Боковая панель (временно скрыта) -->
|
|
358
358
|
<div id="ifcSidebar" class="absolute left-0 top-0 h-full w-80 bg-base-200 shadow-lg transform -translate-x-full transition-transform duration-300 pointer-events-none z-40" style="display: none;">
|
|
@@ -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: [
|
|
@@ -449,9 +454,15 @@ export class Viewer {
|
|
|
449
454
|
// Рендерер
|
|
450
455
|
// logarithmicDepthBuffer: уменьшает z-fighting на почти копланарных поверхностях (часто в IFC).
|
|
451
456
|
// Это заметно снижает "мигание" тонких накладных деталей на фасадах.
|
|
452
|
-
|
|
457
|
+
// stencil: нужен для отрисовки "cap" по контуру сечения
|
|
458
|
+
// ВАЖНО: фон всегда должен быть чисто белым и не зависеть от CSS-окружения (модалки/страницы).
|
|
459
|
+
// Поэтому делаем рендер НЕпрозрачным (alpha: false) и задаём белый clearColor.
|
|
460
|
+
this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false, logarithmicDepthBuffer: true, stencil: true });
|
|
453
461
|
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
|
454
462
|
this.renderer.autoClear = false; // управляем очисткой вручную для мульти-проходов
|
|
463
|
+
try {
|
|
464
|
+
this.renderer.setClearColor(0xffffff, 1);
|
|
465
|
+
} catch (_) {}
|
|
455
466
|
// Тени по умолчанию выключены (включаются только через setShadowsEnabled)
|
|
456
467
|
this.renderer.shadowMap.enabled = false;
|
|
457
468
|
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
|
@@ -474,6 +485,8 @@ export class Viewer {
|
|
|
474
485
|
|
|
475
486
|
// Сцена
|
|
476
487
|
this.scene = new THREE.Scene();
|
|
488
|
+
// Гарантируем белый фон сцены (в том числе для composer/pass-ов).
|
|
489
|
+
try { this.scene.background = new THREE.Color(0xffffff); } catch (_) {}
|
|
477
490
|
// Оверлей-сцена для секущих манипуляторов (без клиппинга)
|
|
478
491
|
this.sectionOverlayScene = new THREE.Scene();
|
|
479
492
|
|
|
@@ -775,6 +788,17 @@ export class Viewer {
|
|
|
775
788
|
this._composer.render();
|
|
776
789
|
} else {
|
|
777
790
|
this.renderer.render(this.scene, this.camera);
|
|
791
|
+
// Cap (закрытие среза) в режиме без композера
|
|
792
|
+
try {
|
|
793
|
+
const subject = this.activeModel || this.demoCube;
|
|
794
|
+
this._sectionCaps?.render?.({
|
|
795
|
+
renderer: this.renderer,
|
|
796
|
+
scene: this.scene,
|
|
797
|
+
camera: this.camera,
|
|
798
|
+
subject,
|
|
799
|
+
activePlanes,
|
|
800
|
+
});
|
|
801
|
+
} catch (_) {}
|
|
778
802
|
}
|
|
779
803
|
// Рендер оверлея манипуляторов без глобального клиппинга поверх
|
|
780
804
|
const prevLocal = this.renderer.localClippingEnabled;
|
|
@@ -2650,24 +2674,12 @@ export class Viewer {
|
|
|
2650
2674
|
* @param {boolean} enabled
|
|
2651
2675
|
*/
|
|
2652
2676
|
setStep3BackgroundEnabled(enabled) {
|
|
2653
|
-
|
|
2654
|
-
|
|
2677
|
+
// По требованиям: фон ВСЕГДА чисто белый, и шаг 3 не должен менять фон сцены.
|
|
2678
|
+
// Поэтому игнорируем "включение" и принудительно удерживаем белый фон.
|
|
2655
2679
|
if (!this.scene) return;
|
|
2656
|
-
|
|
2657
|
-
if (next) {
|
|
2658
|
-
// Снимем, что было до (Color|Texture|null)
|
|
2659
|
-
const prev = this.scene.background ?? null;
|
|
2660
|
-
this._step3Background.snapshot = { background: prev };
|
|
2661
|
-
try { this.scene.background = new THREE.Color(this._step3Background.colorHex); } catch (_) {}
|
|
2662
|
-
this._step3Background.enabled = true;
|
|
2663
|
-
return;
|
|
2664
|
-
}
|
|
2665
|
-
|
|
2666
|
-
const snap = this._step3Background.snapshot;
|
|
2667
2680
|
this._step3Background.enabled = false;
|
|
2668
2681
|
this._step3Background.snapshot = null;
|
|
2669
|
-
|
|
2670
|
-
try { this.scene.background = snap.background ?? null; } catch (_) {}
|
|
2682
|
+
try { this.scene.background = new THREE.Color(0xffffff); } catch (_) {}
|
|
2671
2683
|
}
|
|
2672
2684
|
|
|
2673
2685
|
setAOEnabled(enabled) {
|
|
@@ -2884,7 +2896,9 @@ export class Viewer {
|
|
|
2884
2896
|
const { width, height } = this._getContainerSize();
|
|
2885
2897
|
const w = Math.max(1, Math.floor(width));
|
|
2886
2898
|
const h = Math.max(1, Math.floor(height));
|
|
2887
|
-
|
|
2899
|
+
// Нужен stencil buffer в render targets композера для "cap" сечения
|
|
2900
|
+
const rt = new THREE.WebGLRenderTarget(w, h, { depthBuffer: true, stencilBuffer: true });
|
|
2901
|
+
this._composer = new EffectComposer(this.renderer, rt);
|
|
2888
2902
|
this._renderPass = new RenderPass(this.scene, this.camera);
|
|
2889
2903
|
this._composer.addPass(this._renderPass);
|
|
2890
2904
|
this._ssaoPass = new SSAOPass(this.scene, this.camera, w, h);
|
|
@@ -2963,6 +2977,16 @@ export class Viewer {
|
|
|
2963
2977
|
this._composer.addPass(this._step4Pass);
|
|
2964
2978
|
this.#applyStep4Uniforms();
|
|
2965
2979
|
|
|
2980
|
+
// Cap (закрытие сечения) — перед FXAA, чтобы сгладить край заливки
|
|
2981
|
+
this._sectionCapsPass = new SectionCapsPass({
|
|
2982
|
+
capsRenderer: this._sectionCaps,
|
|
2983
|
+
getScene: () => this.scene,
|
|
2984
|
+
getCamera: () => this.camera,
|
|
2985
|
+
getSubject: () => (this.activeModel || this.demoCube),
|
|
2986
|
+
getActivePlanes: () => (this.clipping?.planes || []),
|
|
2987
|
+
});
|
|
2988
|
+
this._composer.addPass(this._sectionCapsPass);
|
|
2989
|
+
|
|
2966
2990
|
// FXAA pass для устранения "лесенки" на кривых линиях (aliasing)
|
|
2967
2991
|
this._fxaaPass = new ShaderPass(FXAAShader);
|
|
2968
2992
|
this._fxaaPass.material.uniforms['resolution'].value.x = 1 / w;
|