From e15f831abcbb28419e7f94756be394be26eede22 Mon Sep 17 00:00:00 2001 From: "azykov@mail.ru" Date: Sun, 24 May 2026 11:27:49 +0300 Subject: [PATCH] better mouse controls scene cleanup --- client/src/components/ThreeVIew.tsx | 24 ++++++++++------- client/src/components/Viewport.tsx | 6 ++--- client/src/helpers/hooks/useInteration.ts | 33 ++++++++++++++++------- client/src/helpers/sceneHelper.ts | 16 +++++------ client/src/helpers/stringFormat.ts | 6 +++++ client/src/layers/sceneSync.ts | 16 ----------- 6 files changed, 55 insertions(+), 46 deletions(-) create mode 100644 client/src/helpers/stringFormat.ts diff --git a/client/src/components/ThreeVIew.tsx b/client/src/components/ThreeVIew.tsx index fefe069..1c66fe5 100644 --- a/client/src/components/ThreeVIew.tsx +++ b/client/src/components/ThreeVIew.tsx @@ -31,12 +31,13 @@ export type ThreeViewProps = { } function setupScene(size: { w: number, h: number }): { scene: THREE.Scene, camera: THREE.PerspectiveCamera } { + THREE.Object3D.DEFAULT_UP.set(0, 0, 1); + // --- Scene & Camera --- const scene = new THREE.Scene(); scene.background = new THREE.Color(0x0a0a12); const camera = new THREE.PerspectiveCamera(55, size.w / size.h, 0.1, 100); - camera.position.set(4, 3, 6); // --- Lights --- scene.add(new THREE.AmbientLight(0xffffff, 0.3)); @@ -50,15 +51,20 @@ function setupScene(size: { w: number, h: number }): { scene: THREE.Scene, camer pt.position.set(-3, 2, -3); scene.add(pt); - // --- Floor & Grid --- - const plane = new THREE.Mesh( - new THREE.PlaneGeometry(14, 14), - new THREE.MeshStandardMaterial({ color: 0x080810 }) - ); - plane.rotation.x = -Math.PI / 2; - plane.receiveShadow = true; + // const plane = new THREE.Mesh( + // new THREE.PlaneGeometry(14, 14), + // new THREE.MeshStandardMaterial({ color: 0x080810 }) + // ); + // plane.rotation.x = -Math.PI / 2; + // plane.receiveShadow = true; // scene.add(plane); - // scene.add(new THREE.GridHelper(14, 14, 0x222233, 0x111122)); + + const xyGrid = new THREE.GridHelper(14, 14, 0x222233, 0x111122); + xyGrid.rotation.x = Math.PI / 2 + scene.add(xyGrid); + + const axesHelper = new THREE.AxesHelper(2).setColors('red', 'green', 'blue'); + scene.add(axesHelper); return { scene, camera }; } diff --git a/client/src/components/Viewport.tsx b/client/src/components/Viewport.tsx index 6e1d09e..ad655d5 100644 --- a/client/src/components/Viewport.tsx +++ b/client/src/components/Viewport.tsx @@ -9,10 +9,10 @@ export const Viewport = function () { function handleMouseMove(e: ThreeViewMouseEventArgs) { state.setHitTest(e.screenPosition, e.hitResults); - sceneHelper.clear(); + sceneHelper.clearHints(); if (e.hitResults.hits.length) { e.hitResults.hits.forEach((hit) => { - sceneHelper.showPoint(hit.object.uuid, hit.point); + sceneHelper.showPointHint(hit.object.uuid, hit.intersection.point); }) // console.log(e.position); // console.log(e.hitTest.objects.map((o) => o)); @@ -26,7 +26,7 @@ export const Viewport = function () { sceneHelper.setSelection(hoveredFaceIds); - sceneHelper.showMouseFrustum(); + sceneHelper.showMouseFrustumHint(); } function handleDispose(e: ThreeViewEventArgs): void { diff --git a/client/src/helpers/hooks/useInteration.ts b/client/src/helpers/hooks/useInteration.ts index f1af11a..91d0289 100644 --- a/client/src/helpers/hooks/useInteration.ts +++ b/client/src/helpers/hooks/useInteration.ts @@ -1,9 +1,13 @@ import { useEffect, type RefObject } from "react"; import * as THREE from "three"; import { normalizeScreenPosition } from "../normalizeScreenPosition"; +import { formatPoint } from "../stringFormat"; const CLICK_THRESHOLD = 2; // px +function clamp(v: number, min: number, max: number): number { + return Math.max(min, Math.min(max, v)) +} export type InteractionMouseEventArgs = { screenPosition: THREE.Vector2Like, position: THREE.Vector2Like, @@ -37,14 +41,22 @@ export function useInteraction( let isRightDrag = false; let startX = 0, startY = 0; let lastX = 0, lastY = 0; - let theta = 0.8, phi = 0.9, radius = 8; - let targetX = 0, targetY = 0; + let azimuth = 0, elevation = 1, radius = 8; + let targetPoint = new THREE.Vector3(); + + let rotationSpeed = 0.005; + + // prevent flip at poles + const epsilon = 0.0001; function updateCamera() { - camera.position.x = targetX + radius * Math.sin(phi) * Math.sin(theta); - camera.position.y = radius * Math.cos(phi); - camera.position.z = targetY + radius * Math.sin(phi) * Math.cos(theta); - camera.lookAt(targetX, 0, targetY); + const cosEl = Math.cos(elevation); + const x = targetPoint.x + radius * cosEl * Math.cos(azimuth); + const y = targetPoint.y + radius * cosEl * Math.sin(azimuth); + const z = targetPoint.z + radius * Math.sin(elevation); + camera.position.set(x, y, z) + camera.lookAt(targetPoint); + console.log(`${formatPoint(camera.position)} ${azimuth} ${elevation}`); } updateCamera(); @@ -77,11 +89,12 @@ export function useInteraction( lastX = e.clientX; lastY = e.clientY; if (isRightDrag) { - targetX -= dx * 0.01; - targetY += dy * 0.01; + targetPoint.x -= dx * 0.01; + targetPoint.y += dy * 0.01; } else { - theta -= dx * 0.005; - phi = Math.max(0.15, Math.min(Math.PI - 0.15, phi - dy * 0.005)); + azimuth = azimuth - dx * rotationSpeed; + elevation = elevation + dy * rotationSpeed; + elevation = Math.max(-Math.PI / 2 + epsilon, Math.min(Math.PI / 2 - epsilon, elevation + dy * rotationSpeed)); } updateCamera(); }; diff --git a/client/src/helpers/sceneHelper.ts b/client/src/helpers/sceneHelper.ts index 0b76667..c509df2 100644 --- a/client/src/helpers/sceneHelper.ts +++ b/client/src/helpers/sceneHelper.ts @@ -2,7 +2,7 @@ import type { ColorRepresentation, Object3D, Object3DEventMap, OrthographicCamer import { SceneSync } from "../layers/sceneSync"; import { GeometryCache } from "../layers/geometryCache"; import type { Id } from "../types"; -import { CircularFrustumIntersection, type HitResult, type HitResults } from "./circularFrustumIntersect"; +import { CircularFrustumIntersection, type Intersection, type HitResults, type HitResult } from "./circularFrustumIntersect"; import { CircularFrustum } from "./circularFrustum"; import './bvh'; import { VolatileGeometryHelper, type VolatileGeometryOptions } from "./volatileGeometryHelper"; @@ -71,15 +71,15 @@ export class SceneHelper { this.hints?.set(id, position, options); } - public showPoint(id: string, position: Vector3Like, size: number = 0.05, color: ColorRepresentation = 0xffffff) { + public showPointHint(id: string, position: Vector3Like, size: number = 0.05, color: ColorRepresentation = 0xffffff) { this.hints?.set(id, position, { kind: "point", size, color }); } - public showCircle(id: string, position: Vector3Like, radius: number, thickness: number = 0.1, color: ColorRepresentation = 0xffffff) { + public showCircleHint(id: string, position: Vector3Like, radius: number, thickness: number = 0.1, color: ColorRepresentation = 0xffffff) { this.hints?.set(id, position, { kind: "circle", radius, thickness, color }); } - public showMouseFrustum() { + public showMouseFrustumHint() { if (!this.camera) throw new Error('Camera is not initialized'); @@ -90,17 +90,17 @@ export class SceneHelper { const near = frustum.getCircleAtDepth(nearDepth); const far = frustum.getCircleAtDepth(farDepth); - this.showCircle('hittest_near', near.center, near.radius, 0.05); - this.showCircle('hittest_far', far.center, far.radius, 0.1, 'red'); + this.showCircleHint('hittest_near', near.center, near.radius, 0.05); + this.showCircleHint('hittest_far', far.center, far.radius, 0.1, 'red'); } - public clear() { + public clearHints() { this.hints?.dispose(); } public dispose() { this.sync?.dispose(); - this.clear(); + this.clearHints(); } } diff --git a/client/src/helpers/stringFormat.ts b/client/src/helpers/stringFormat.ts new file mode 100644 index 0000000..f701f2e --- /dev/null +++ b/client/src/helpers/stringFormat.ts @@ -0,0 +1,6 @@ +import type { Vector3Like } from "three"; + +export function formatPoint(point: Vector3Like): string { + return [point.x, point.y, point.z].map((v) => v.toFixed(3)).join('; '); +} + diff --git a/client/src/layers/sceneSync.ts b/client/src/layers/sceneSync.ts index 48d25aa..93a86c7 100644 --- a/client/src/layers/sceneSync.ts +++ b/client/src/layers/sceneSync.ts @@ -47,9 +47,6 @@ export class SceneSync { // Called when FE scene graph syncs from BE addSolid(dto: MeshDto) { const faceId = dto.faceId; - const transform = new Float32Array([ - 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 - ]); if (this.meshByFace[faceId]) return; @@ -61,23 +58,10 @@ export class SceneSync { mesh.userData.surfaceId = dto.surfaceId; mesh.userData.solidId = dto.solidId; - // Apply transform (col-major mat4 from BE) - const m = new THREE.Matrix4(); - m.fromArray(transform); // THREE expects col-major - mesh.applyMatrix4(m); - this.scene.add(mesh); this.meshByFace[faceId] = mesh; } - updateTransform(faceId: Id, colMajorMat4: number[]) { - const mesh = this.meshByFace[faceId]; - if (!mesh) return; - const m = new THREE.Matrix4().fromArray(colMajorMat4); - mesh.matrix.copy(m); - mesh.matrix.decompose(mesh.position, mesh.quaternion, mesh.scale); - } - setSelected(faceIds: Id[]) { this._selectedFaceIds = faceIds;