Compare commits

...

2 Commits

Author SHA1 Message Date
azykov@mail.ru e15f831abc
better mouse controls
scene cleanup
2026-05-24 11:27:49 +03:00
azykov@mail.ru cfd3cc1142
minor code fix 2026-05-23 20:42:44 +03:00
7 changed files with 58 additions and 49 deletions

View File

@ -31,12 +31,13 @@ export type ThreeViewProps = {
} }
function setupScene(size: { w: number, h: number }): { scene: THREE.Scene, camera: THREE.PerspectiveCamera } { function setupScene(size: { w: number, h: number }): { scene: THREE.Scene, camera: THREE.PerspectiveCamera } {
THREE.Object3D.DEFAULT_UP.set(0, 0, 1);
// --- Scene & Camera --- // --- Scene & Camera ---
const scene = new THREE.Scene(); const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0a12); scene.background = new THREE.Color(0x0a0a12);
const camera = new THREE.PerspectiveCamera(55, size.w / size.h, 0.1, 100); const camera = new THREE.PerspectiveCamera(55, size.w / size.h, 0.1, 100);
camera.position.set(4, 3, 6);
// --- Lights --- // --- Lights ---
scene.add(new THREE.AmbientLight(0xffffff, 0.3)); 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); pt.position.set(-3, 2, -3);
scene.add(pt); scene.add(pt);
// --- Floor & Grid --- // const plane = new THREE.Mesh(
const plane = new THREE.Mesh( // new THREE.PlaneGeometry(14, 14),
new THREE.PlaneGeometry(14, 14), // new THREE.MeshStandardMaterial({ color: 0x080810 })
new THREE.MeshStandardMaterial({ color: 0x080810 }) // );
); // plane.rotation.x = -Math.PI / 2;
plane.rotation.x = -Math.PI / 2; // plane.receiveShadow = true;
plane.receiveShadow = true;
// scene.add(plane); // 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 }; return { scene, camera };
} }

View File

@ -9,10 +9,10 @@ export const Viewport = function () {
function handleMouseMove(e: ThreeViewMouseEventArgs) { function handleMouseMove(e: ThreeViewMouseEventArgs) {
state.setHitTest(e.screenPosition, e.hitResults); state.setHitTest(e.screenPosition, e.hitResults);
sceneHelper.clear(); sceneHelper.clearHints();
if (e.hitResults.hits.length) { if (e.hitResults.hits.length) {
e.hitResults.hits.forEach((hit) => { 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.position);
// console.log(e.hitTest.objects.map((o) => o)); // console.log(e.hitTest.objects.map((o) => o));
@ -26,7 +26,7 @@ export const Viewport = function () {
sceneHelper.setSelection(hoveredFaceIds); sceneHelper.setSelection(hoveredFaceIds);
sceneHelper.showMouseFrustum(); sceneHelper.showMouseFrustumHint();
} }
function handleDispose(e: ThreeViewEventArgs): void { function handleDispose(e: ThreeViewEventArgs): void {

View File

@ -55,7 +55,7 @@ const BARYCENTRIC_EPSILON = 1e-1;
function classifyTriangleHit( function classifyTriangleHit(
point: THREE.Vector3, point: THREE.Vector3,
tri: ExtendedTriangle, tri: ExtendedTriangle,
vertexIds: Id[], vertexIds: [Id, Id, Id],
): TriangleHitDetail { ): TriangleHitDetail {
// Compute barycentric coords via areas // Compute barycentric coords via areas
const ab = tri.b.clone().sub(tri.a); const ab = tri.b.clone().sub(tri.a);
@ -232,7 +232,7 @@ export class CircularFrustumIntersection {
intersectsBounds: (box: THREE.Box3) => intersectionResultToBvh(CircularFrustumIntersection.intersectsBox(box, localFrustum)), intersectsBounds: (box: THREE.Box3) => intersectionResultToBvh(CircularFrustumIntersection.intersectsBox(box, localFrustum)),
intersectsTriangle: (tri: ExtendedTriangle, triIndex: number, contained: boolean) => { intersectsTriangle: (tri: ExtendedTriangle, triIndex: number, contained: boolean) => {
const tiangleVertexIds = getGeometryVertextIds(triIndex); const tiangleVertexIds = getGeometryVertextIds(triIndex) ?? ['','',''];
if (contained) { if (contained) {
const worldPoint = tri.a.clone().applyMatrix4(mesh.matrixWorld); const worldPoint = tri.a.clone().applyMatrix4(mesh.matrixWorld);
@ -242,7 +242,7 @@ export class CircularFrustumIntersection {
point: worldPoint, point: worldPoint,
depth, depth,
triangle: tri, triangle: tri,
triHit: classifyTriangleHit(tri.a, tri, tiangleVertexIds ?? []), triHit: classifyTriangleHit(tri.a, tri, tiangleVertexIds),
vertexIds: tiangleVertexIds, vertexIds: tiangleVertexIds,
}); });
return !findAll; return !findAll;

View File

@ -1,9 +1,13 @@
import { useEffect, type RefObject } from "react"; import { useEffect, type RefObject } from "react";
import * as THREE from "three"; import * as THREE from "three";
import { normalizeScreenPosition } from "../normalizeScreenPosition"; import { normalizeScreenPosition } from "../normalizeScreenPosition";
import { formatPoint } from "../stringFormat";
const CLICK_THRESHOLD = 2; // px 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 = { export type InteractionMouseEventArgs = {
screenPosition: THREE.Vector2Like, screenPosition: THREE.Vector2Like,
position: THREE.Vector2Like, position: THREE.Vector2Like,
@ -37,14 +41,22 @@ export function useInteraction(
let isRightDrag = false; let isRightDrag = false;
let startX = 0, startY = 0; let startX = 0, startY = 0;
let lastX = 0, lastY = 0; let lastX = 0, lastY = 0;
let theta = 0.8, phi = 0.9, radius = 8; let azimuth = 0, elevation = 1, radius = 8;
let targetX = 0, targetY = 0; let targetPoint = new THREE.Vector3();
let rotationSpeed = 0.005;
// prevent flip at poles
const epsilon = 0.0001;
function updateCamera() { function updateCamera() {
camera.position.x = targetX + radius * Math.sin(phi) * Math.sin(theta); const cosEl = Math.cos(elevation);
camera.position.y = radius * Math.cos(phi); const x = targetPoint.x + radius * cosEl * Math.cos(azimuth);
camera.position.z = targetY + radius * Math.sin(phi) * Math.cos(theta); const y = targetPoint.y + radius * cosEl * Math.sin(azimuth);
camera.lookAt(targetX, 0, targetY); 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(); updateCamera();
@ -77,11 +89,12 @@ export function useInteraction(
lastX = e.clientX; lastX = e.clientX;
lastY = e.clientY; lastY = e.clientY;
if (isRightDrag) { if (isRightDrag) {
targetX -= dx * 0.01; targetPoint.x -= dx * 0.01;
targetY += dy * 0.01; targetPoint.y += dy * 0.01;
} else { } else {
theta -= dx * 0.005; azimuth = azimuth - dx * rotationSpeed;
phi = Math.max(0.15, Math.min(Math.PI - 0.15, phi - dy * 0.005)); elevation = elevation + dy * rotationSpeed;
elevation = Math.max(-Math.PI / 2 + epsilon, Math.min(Math.PI / 2 - epsilon, elevation + dy * rotationSpeed));
} }
updateCamera(); updateCamera();
}; };

View File

@ -2,7 +2,7 @@ import type { ColorRepresentation, Object3D, Object3DEventMap, OrthographicCamer
import { SceneSync } from "../layers/sceneSync"; import { SceneSync } from "../layers/sceneSync";
import { GeometryCache } from "../layers/geometryCache"; import { GeometryCache } from "../layers/geometryCache";
import type { Id } from "../types"; 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 { CircularFrustum } from "./circularFrustum";
import './bvh'; import './bvh';
import { VolatileGeometryHelper, type VolatileGeometryOptions } from "./volatileGeometryHelper"; import { VolatileGeometryHelper, type VolatileGeometryOptions } from "./volatileGeometryHelper";
@ -71,15 +71,15 @@ export class SceneHelper {
this.hints?.set(id, position, options); 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 }); 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 }); this.hints?.set(id, position, { kind: "circle", radius, thickness, color });
} }
public showMouseFrustum() { public showMouseFrustumHint() {
if (!this.camera) if (!this.camera)
throw new Error('Camera is not initialized'); throw new Error('Camera is not initialized');
@ -90,17 +90,17 @@ export class SceneHelper {
const near = frustum.getCircleAtDepth(nearDepth); const near = frustum.getCircleAtDepth(nearDepth);
const far = frustum.getCircleAtDepth(farDepth); const far = frustum.getCircleAtDepth(farDepth);
this.showCircle('hittest_near', near.center, near.radius, 0.05); this.showCircleHint('hittest_near', near.center, near.radius, 0.05);
this.showCircle('hittest_far', far.center, far.radius, 0.1, 'red'); this.showCircleHint('hittest_far', far.center, far.radius, 0.1, 'red');
} }
public clear() { public clearHints() {
this.hints?.dispose(); this.hints?.dispose();
} }
public dispose() { public dispose() {
this.sync?.dispose(); this.sync?.dispose();
this.clear(); this.clearHints();
} }
} }

View File

@ -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('; ');
}

View File

@ -47,9 +47,6 @@ export class SceneSync {
// Called when FE scene graph syncs from BE // Called when FE scene graph syncs from BE
addSolid(dto: MeshDto) { addSolid(dto: MeshDto) {
const faceId = dto.faceId; 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]) if (this.meshByFace[faceId])
return; return;
@ -61,23 +58,10 @@ export class SceneSync {
mesh.userData.surfaceId = dto.surfaceId; mesh.userData.surfaceId = dto.surfaceId;
mesh.userData.solidId = dto.solidId; 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.scene.add(mesh);
this.meshByFace[faceId] = 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[]) { setSelected(faceIds: Id[]) {
this._selectedFaceIds = faceIds; this._selectedFaceIds = faceIds;