@sequent-org/ifc-viewer 1.0.2-ci.2.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.
@@ -0,0 +1,260 @@
1
+ import * as THREE from "three";
2
+
3
+ /**
4
+ * Класс SectionManipulator отвечает за визуализацию и интерактив
5
+ * одной секущей плоскости: квадрата-подсветки и стрелки, за
6
+ * которую можно тянуть, перемещая плоскость вдоль её нормали.
7
+ *
8
+ * ООП: класс инкапсулирует собственные объекты сцены и обработчики.
9
+ * Внешний мир управляет только включением/позиционированием через Plane
10
+ * и вызывает update().
11
+ */
12
+ export class SectionManipulator {
13
+ /**
14
+ * @param {Object} opts
15
+ * @param {THREE.Scene} opts.scene - сцена, куда добавлять гизмо
16
+ * @param {THREE.Camera} opts.camera - активная камера
17
+ * @param {import('three/examples/jsm/controls/OrbitControls').OrbitControls} opts.controls - OrbitControls
18
+ * @param {HTMLElement} opts.domElement - DOM-элемент канваса для событий
19
+ * @param {THREE.Plane} opts.plane - глобальная плоскость отсечения (shared)
20
+ * @param {'x'|'y'|'z'} opts.axis - ось секущей плоскости
21
+ */
22
+ constructor({ scene, camera, controls, domElement, plane, axis }) {
23
+ this.scene = scene;
24
+ this.camera = camera;
25
+ this.controls = controls;
26
+ this.domElement = domElement;
27
+ this.plane = plane;
28
+ this.axis = axis;
29
+
30
+ // Визуальные элементы
31
+ this.root = new THREE.Group();
32
+ this.root.name = `section-manipulator-${axis}`;
33
+ this.root.visible = false;
34
+ this.scene.add(this.root);
35
+
36
+ // Рамка (без заливки) визуализации плоскости
37
+ this.planeQuad = this.#createPlaneFrame();
38
+ this.root.add(this.planeQuad);
39
+
40
+ // Стрелка и хит-ручка
41
+ const { arrow, handle } = this.#createArrowWithHandle();
42
+ this.arrow = arrow;
43
+ this.handle = handle;
44
+ this.root.add(this.arrow);
45
+ this.root.add(this.handle);
46
+
47
+ // Луч и вспомогательные объекты для drag
48
+ this.raycaster = new THREE.Raycaster();
49
+ this.isDragging = false;
50
+ this.dragData = null; // { startDistance, startHitPoint: Vector3, dragPlane: THREE.Plane }
51
+
52
+ // Слушатели событий
53
+ this._onPointerDown = this._onPointerDown.bind(this);
54
+ this._onPointerMove = this._onPointerMove.bind(this);
55
+ this._onPointerUp = this._onPointerUp.bind(this);
56
+ this.domElement.addEventListener('pointerdown', this._onPointerDown);
57
+ }
58
+
59
+ /** Освобождение ресурсов и слушателей */
60
+ dispose() {
61
+ this.domElement.removeEventListener('pointerdown', this._onPointerDown);
62
+ window.removeEventListener('pointermove', this._onPointerMove);
63
+ window.removeEventListener('pointerup', this._onPointerUp);
64
+ // Удаляем из сцены и чистим геом/материалы
65
+ const cleanup = (obj) => {
66
+ obj.traverse?.((node) => {
67
+ if (node.geometry?.dispose) node.geometry.dispose();
68
+ if (node.material) {
69
+ if (Array.isArray(node.material)) node.material.forEach((m) => m?.dispose && m.dispose());
70
+ else if (node.material.dispose) node.material.dispose();
71
+ }
72
+ });
73
+ if (obj.parent) obj.parent.remove(obj);
74
+ };
75
+ cleanup(this.root);
76
+ }
77
+
78
+ /** Включает/выключает видимость манипулятора */
79
+ setEnabled(enabled) {
80
+ this.root.visible = !!enabled;
81
+ }
82
+
83
+ /**
84
+ * Обновляет позицию/ориентацию/масштаб в зависимости от плоскости и модели
85
+ * @param {THREE.Object3D|null} subject - активная модель для расчёта размеров и направления стрелки
86
+ */
87
+ update(subject) {
88
+ if (!this.root.visible || !isFinite(this.plane.constant)) return;
89
+
90
+ // Позиция центра плоскости вдоль нормали: d = -constant
91
+ const distance = -this.plane.constant;
92
+ const normal = this.plane.normal.clone().normalize();
93
+ this.root.position.copy(normal).multiplyScalar(distance);
94
+
95
+ // Ориентация рамки и стрелки: ось Z гизмо смотрит вдоль нормали плоскости
96
+ const q = new THREE.Quaternion();
97
+ q.setFromUnitVectors(new THREE.Vector3(0, 0, 1), normal);
98
+ this.root.setRotationFromQuaternion(q);
99
+
100
+ // Масштаб под габариты модели
101
+ let sceneScale = 1;
102
+ if (subject) {
103
+ const box = new THREE.Box3().setFromObject(subject);
104
+ const size = box.getSize(new THREE.Vector3());
105
+ sceneScale = Math.max(size.x, size.y, size.z, 1) * 1.2;
106
+ }
107
+ const quadScale = sceneScale / 100; // базовая геометрия 100x100
108
+ this.planeQuad.scale.setScalar(quadScale);
109
+
110
+ // Стрелка на стороне камеры (между камерой и плоскостью), направлена от объекта (наружу)
111
+ const arrowLength = Math.max(sceneScale * 0.12, 0.2);
112
+ const signedCam = this.plane.normal.dot(this.camera.position) + this.plane.constant;
113
+ const localSign = signedCam > 0 ? 1 : -1; // +Z если камера на положительной стороне, иначе -Z
114
+ const localDir = new THREE.Vector3(0, 0, localSign);
115
+ this.#setArrowDirectionAndLength(localDir, arrowLength);
116
+
117
+ // Позиция стрелки: ближе к камере, чуть отступив от плоскости
118
+ const offset = Math.max(arrowLength * 0.8, 0.1);
119
+ const arrowBase = localDir.clone().multiplyScalar(offset);
120
+ this.arrow.position.copy(arrowBase);
121
+
122
+ // Хит-цилиндр: центр по середине стрелки, длина покрывает всю стрелку
123
+ const hitRadius = Math.max(sceneScale * 0.03, 0.1);
124
+ const handleLen = arrowLength * 1.4; // чуть длиннее стрелки
125
+ const handleCenter = arrowBase.clone().add(localDir.clone().multiplyScalar(arrowLength * 0.5));
126
+ this.handle.position.copy(handleCenter);
127
+ this.#orientHandleToDirection(localDir, 1);
128
+ const baseRadius = 0.06;
129
+ const rScale = hitRadius / baseRadius;
130
+ this.handle.scale.set(rScale, handleLen, rScale);
131
+ }
132
+
133
+ // --- Внутренние методы визуальных элементов ---
134
+ #createPlaneFrame() {
135
+ const geom = new THREE.PlaneGeometry(100, 100);
136
+ const edgesGeom = new THREE.EdgesGeometry(geom, 1);
137
+ const lineMat = new THREE.LineBasicMaterial({
138
+ color: 0xff0055,
139
+ depthTest: false,
140
+ transparent: true,
141
+ opacity: 1,
142
+ clippingPlanes: [],
143
+ });
144
+ const lines = new THREE.LineSegments(edgesGeom, lineMat);
145
+ lines.name = `section-frame-${this.axis}`;
146
+ lines.renderOrder = 999;
147
+ return lines;
148
+ }
149
+
150
+ #createArrowWithHandle() {
151
+ // Стрелка из ArrowHelper
152
+ const dir = new THREE.Vector3(0, 0, 1);
153
+ const length = 1;
154
+ const color = 0xff0055;
155
+ const headLength = 0.3;
156
+ const headWidth = 0.15;
157
+ const arrow = new THREE.ArrowHelper(dir, new THREE.Vector3(0, 0, 0), length, color, headLength, headWidth);
158
+ arrow.line.material = new THREE.LineBasicMaterial({ color, depthTest: false, clippingPlanes: [] });
159
+ if (arrow.cone?.material) {
160
+ arrow.cone.material = new THREE.MeshBasicMaterial({ color, depthTest: false, clippingPlanes: [] });
161
+ }
162
+ arrow.renderOrder = 1000;
163
+ arrow.name = `section-arrow-${this.axis}`;
164
+
165
+ // Невидимый цилиндр для удобного попадания лучом
166
+ const cylGeom = new THREE.CylinderGeometry(0.06, 0.06, 1, 16);
167
+ const cylMat = new THREE.MeshBasicMaterial({ visible: false, depthTest: false });
168
+ const handle = new THREE.Mesh(cylGeom, cylMat);
169
+ handle.name = `section-handle-${this.axis}`;
170
+ handle.userData.__isSectionHandle = true;
171
+
172
+ return { arrow, handle };
173
+ }
174
+
175
+ #setArrowDirectionAndLength(direction, length) {
176
+ const origin = new THREE.Vector3(0, 0, 0);
177
+ const dirNorm = direction.clone().normalize();
178
+ this.arrow.setDirection(dirNorm);
179
+ this.arrow.setLength(length, Math.min(length * 0.5, 0.5 * length), Math.min(length * 0.25, 0.25 * length));
180
+ }
181
+
182
+ #orientHandleToDirection(direction, _lengthIgnored) {
183
+ // Ориентируем цилиндр вдоль direction с центром в позиции handle
184
+ const quat = new THREE.Quaternion();
185
+ quat.setFromUnitVectors(new THREE.Vector3(0, 1, 0), direction.clone().normalize());
186
+ this.handle.quaternion.copy(quat);
187
+ }
188
+
189
+ // --- Drag обработчики ---
190
+ _onPointerDown(e) {
191
+ if (!this.root.visible) return;
192
+ const hit = this.#intersectPointerWithHandle(e);
193
+ if (!hit) return;
194
+
195
+ // Блокируем дефолт и начинаем перетаскивание
196
+ e.preventDefault();
197
+ try { this.domElement.setPointerCapture?.(e.pointerId); } catch(_) {}
198
+
199
+ this.isDragging = true;
200
+ this.controls && (this.controls.enabled = false);
201
+ window.addEventListener('pointermove', this._onPointerMove);
202
+ window.addEventListener('pointerup', this._onPointerUp, { once: true });
203
+
204
+ const normal = this.plane.normal.clone().normalize();
205
+ const distance = -this.plane.constant; // текущее d вдоль нормали
206
+ const planePoint = normal.clone().multiplyScalar(distance);
207
+
208
+ const viewDir = new THREE.Vector3();
209
+ this.camera.getWorldDirection(viewDir).normalize();
210
+ // Вспомогательная плоскость перпендикулярна взгляду и проходит через planePoint
211
+ const dragPlane = new THREE.Plane().setFromNormalAndCoplanarPoint(viewDir, planePoint);
212
+
213
+ const startHitPoint = this.#raycastToPlane(e, dragPlane) || planePoint.clone();
214
+ this.dragData = { startDistance: distance, startHitPoint, dragPlane };
215
+ }
216
+
217
+ _onPointerMove(e) {
218
+ if (!this.isDragging || !this.dragData) return;
219
+ const { startDistance, startHitPoint, dragPlane } = this.dragData;
220
+ const hit = this.#raycastToPlane(e, dragPlane);
221
+ if (!hit) return;
222
+
223
+ const normal = this.plane.normal.clone().normalize();
224
+ const deltaVec = hit.clone().sub(startHitPoint);
225
+ const delta = deltaVec.dot(normal); // проекция смещения на нормаль
226
+ const newDistance = startDistance + delta;
227
+
228
+ // Обновляем константу плоскости и визуализацию
229
+ this.plane.constant = -newDistance;
230
+ }
231
+
232
+ _onPointerUp(e) {
233
+ this.isDragging = false;
234
+ this.dragData = null;
235
+ window.removeEventListener('pointermove', this._onPointerMove);
236
+ this.controls && (this.controls.enabled = true);
237
+ try { this.domElement.releasePointerCapture?.(e.pointerId); } catch(_) {}
238
+ }
239
+
240
+ // --- Лучевые помощники ---
241
+ #intersectPointerWithHandle(e) {
242
+ const rect = this.domElement.getBoundingClientRect();
243
+ const x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
244
+ const y = -(((e.clientY - rect.top) / rect.height) * 2 - 1);
245
+ this.raycaster.setFromCamera({ x, y }, this.camera);
246
+ const intersects = this.raycaster.intersectObject(this.handle, true);
247
+ return intersects && intersects.length > 0 ? intersects[0] : null;
248
+ }
249
+
250
+ #raycastToPlane(e, plane) {
251
+ const rect = this.domElement.getBoundingClientRect();
252
+ const x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
253
+ const y = -(((e.clientY - rect.top) / rect.height) * 2 - 1);
254
+ this.raycaster.setFromCamera({ x, y }, this.camera);
255
+ const hit = new THREE.Vector3();
256
+ return this.raycaster.ray.intersectPlane(plane, hit) ? hit : null;
257
+ }
258
+ }
259
+
260
+