@sequent-org/ifc-viewer 1.2.4-ci.43.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sequent-org/ifc-viewer",
3
3
  "private": false,
4
- "version": "1.2.4-ci.43.0",
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
+
@@ -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,7 +454,8 @@ export class Viewer {
449
454
  // Рендерер
450
455
  // logarithmicDepthBuffer: уменьшает z-fighting на почти копланарных поверхностях (часто в IFC).
451
456
  // Это заметно снижает "мигание" тонких накладных деталей на фасадах.
452
- this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, logarithmicDepthBuffer: true });
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)
@@ -775,6 +781,17 @@ export class Viewer {
775
781
  this._composer.render();
776
782
  } else {
777
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 (_) {}
778
795
  }
779
796
  // Рендер оверлея манипуляторов без глобального клиппинга поверх
780
797
  const prevLocal = this.renderer.localClippingEnabled;
@@ -2884,7 +2901,9 @@ export class Viewer {
2884
2901
  const { width, height } = this._getContainerSize();
2885
2902
  const w = Math.max(1, Math.floor(width));
2886
2903
  const h = Math.max(1, Math.floor(height));
2887
- this._composer = new EffectComposer(this.renderer);
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);
2888
2907
  this._renderPass = new RenderPass(this.scene, this.camera);
2889
2908
  this._composer.addPass(this._renderPass);
2890
2909
  this._ssaoPass = new SSAOPass(this.scene, this.camera, w, h);
@@ -2963,6 +2982,16 @@ export class Viewer {
2963
2982
  this._composer.addPass(this._step4Pass);
2964
2983
  this.#applyStep4Uniforms();
2965
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
+
2966
2995
  // FXAA pass для устранения "лесенки" на кривых линиях (aliasing)
2967
2996
  this._fxaaPass = new ShaderPass(FXAAShader);
2968
2997
  this._fxaaPass.material.uniforms['resolution'].value.x = 1 / w;