parent
cfd3cc1142
commit
e15f831abc
|
|
@ -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 };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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('; ');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue