basic hit testing
This commit is contained in:
parent
aa17ecd4c9
commit
bd5fa12a1e
|
|
@ -16,6 +16,7 @@
|
|||
"react-dom": "^19.2.6",
|
||||
"sass-embedded": "^1.99.0",
|
||||
"three": "^0.184.0",
|
||||
"three-mesh-bvh": "^0.9.10",
|
||||
"uuid": "^14.0.0",
|
||||
"verb-nurbs": "^3.0.3"
|
||||
},
|
||||
|
|
@ -3313,7 +3314,17 @@
|
|||
"version": "0.184.0",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.184.0.tgz",
|
||||
"integrity": "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/three-mesh-bvh": {
|
||||
"version": "0.9.10",
|
||||
"resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.9.10.tgz",
|
||||
"integrity": "sha512-UOlTgPIeqUURcwaG8knxvBaruwZlC4X3/WSHEFO7rYvMVv/YNUrkfFEszvfj36pXV88dCHoHNnIp0PifkirnTQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"three": ">= 0.159.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.16",
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
"react-dom": "^19.2.6",
|
||||
"sass-embedded": "^1.99.0",
|
||||
"three": "^0.184.0",
|
||||
"three-mesh-bvh": "^0.9.10",
|
||||
"uuid": "^14.0.0",
|
||||
"verb-nurbs": "^3.0.3"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,10 +1,18 @@
|
|||
#viewport {
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
background: white;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
}
|
||||
|
||||
#hit-test {
|
||||
position: absolute;
|
||||
top: 600px;
|
||||
left: 000px;
|
||||
width: 600px;
|
||||
font-size: 75%;
|
||||
}
|
||||
|
||||
#blob-view {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Viewport } from './components/Viewport'
|
||||
import { DbView } from './components/DbView'
|
||||
import { HitTestView } from './components/HitTestView'
|
||||
|
||||
import './App.scss'
|
||||
|
||||
|
|
@ -7,6 +8,7 @@ export const App = function () {
|
|||
return (
|
||||
<div>
|
||||
<Viewport />
|
||||
<HitTestView />
|
||||
<DbView />
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
export * from './mesh';
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type { Mesh, Solid, Surface } from "../types";
|
||||
import type { Mesh, Solid, Surface } from "../../types";
|
||||
|
||||
export type MeshDto = {
|
||||
vertices: Float32Array;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export type SolildDto = {
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import { observer } from "mobx-react-lite";
|
||||
import { state } from "../state/root";
|
||||
|
||||
export const HitTestView = observer(function () {
|
||||
|
||||
return (
|
||||
<div id="hit-test">
|
||||
<pre>
|
||||
{
|
||||
state.hitTest.objects.map((obj) =>
|
||||
<div key={obj.object.uuid}>
|
||||
{JSON.stringify(obj.point.toArray())}
|
||||
{JSON.stringify(obj.object.userData)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</pre>
|
||||
</div>
|
||||
)
|
||||
});
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
import { useEffect, useRef } from "react";
|
||||
import * as THREE from 'three';
|
||||
import { useInteraction, type InteractionMouseMoveEventArgs } from "../helpers/hooks/useInteration";
|
||||
import { db } from "../backend/db";
|
||||
import { HitTestFactory, type HitTest } from "../helpers/hitTest";
|
||||
import { model } from "../model/model";
|
||||
import { Point3dHelper } from "../helpers/point3dHelper";
|
||||
import { SceneHelper } from "../helpers/sceneHelper";
|
||||
|
||||
export type ThreeViewEventArgs = {
|
||||
camera: THREE.Camera,
|
||||
scene: THREE.Scene,
|
||||
}
|
||||
|
||||
export type ThreeViewTickEventArgs = ThreeViewEventArgs & {
|
||||
deltaTime: number,
|
||||
absoluteTime: number,
|
||||
}
|
||||
|
||||
export type ThreeViewMouseMoveEventArgs = ThreeViewEventArgs & {
|
||||
hitTest: HitTest,
|
||||
}
|
||||
|
||||
export type ThreeViewProps = {
|
||||
sceneHelper: SceneHelper,
|
||||
onTick?: (event: ThreeViewTickEventArgs) => void,
|
||||
onMouseMove?: (event: ThreeViewMouseMoveEventArgs) => void,
|
||||
onDispose: (event: ThreeViewEventArgs) => void,
|
||||
}
|
||||
|
||||
function setupScene(size: { w: number, h: number }): { scene: THREE.Scene, camera: THREE.PerspectiveCamera } {
|
||||
// --- 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));
|
||||
|
||||
const dir = new THREE.DirectionalLight(0xffffff, 1.2);
|
||||
dir.position.set(5, 8, 5);
|
||||
dir.castShadow = true;
|
||||
scene.add(dir);
|
||||
|
||||
const pt = new THREE.PointLight(0x5588ff, 1.5, 20);
|
||||
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;
|
||||
// scene.add(plane);
|
||||
// scene.add(new THREE.GridHelper(14, 14, 0x222233, 0x111122));
|
||||
|
||||
return { scene, camera };
|
||||
}
|
||||
|
||||
export const ThreeView = function (props: ThreeViewProps) {
|
||||
|
||||
const viewportRef = useRef<HTMLDivElement | null>(null);
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
const cameraRef = useRef<THREE.PerspectiveCamera | null>(null);
|
||||
|
||||
let handleHover: (e: InteractionMouseMoveEventArgs) => void;
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
model.initFromBlob(db.saveToBlob());
|
||||
|
||||
const container = viewportRef.current!;
|
||||
const W = container.clientWidth;
|
||||
const H = container.clientHeight;
|
||||
|
||||
// --- Renderer ---
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
renderer.setSize(W, H);
|
||||
renderer.shadowMap.enabled = true;
|
||||
container.appendChild(renderer.domElement);
|
||||
|
||||
canvasRef.current = renderer.domElement;
|
||||
|
||||
const { scene, camera } = setupScene({ w: W, h: H });
|
||||
cameraRef.current = camera;
|
||||
|
||||
props.sceneHelper.initialize(scene);
|
||||
|
||||
const handleWindowResize = () => {
|
||||
const w = container.clientWidth;
|
||||
const h = container.clientHeight;
|
||||
camera.aspect = w / h;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(w, h);
|
||||
};
|
||||
window.addEventListener("resize", handleWindowResize);
|
||||
|
||||
handleHover = (e: InteractionMouseMoveEventArgs) => {
|
||||
const hitTest = HitTestFactory.hitTest(
|
||||
props.sceneHelper,
|
||||
new THREE.Vector2(e.position.x, e.position.y),
|
||||
camera,
|
||||
{ tolerancePixels: 3, cameraPixelSize: e.pixelSize }
|
||||
);
|
||||
props.onMouseMove?.({
|
||||
camera,
|
||||
scene,
|
||||
hitTest,
|
||||
});
|
||||
};
|
||||
|
||||
// --- Animation loop ---
|
||||
let lastTime = performance.now();
|
||||
let animId: number;
|
||||
function animate(time: DOMHighResTimeStamp) {
|
||||
animId = requestAnimationFrame(animate);
|
||||
const deltaTime = lastTime ? time - lastTime : 0;
|
||||
lastTime = time;
|
||||
props.onTick?.({
|
||||
camera,
|
||||
scene,
|
||||
deltaTime,
|
||||
absoluteTime: time,
|
||||
});
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
animId = requestAnimationFrame(animate);
|
||||
|
||||
// --- Cleanup ---
|
||||
return () => {
|
||||
if (animId)
|
||||
cancelAnimationFrame(animId);
|
||||
|
||||
container.removeChild(renderer.domElement);
|
||||
|
||||
window.removeEventListener("resize", handleWindowResize);
|
||||
|
||||
renderer.dispose();
|
||||
|
||||
props.onDispose({
|
||||
camera,
|
||||
scene,
|
||||
});
|
||||
|
||||
props.sceneHelper.dispose();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useInteraction(canvasRef, cameraRef, {
|
||||
onMouseMove: (e) => handleHover?.(e),
|
||||
});
|
||||
|
||||
return (
|
||||
<div id="viewport" ref={viewportRef}>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,193 +1,43 @@
|
|||
import { useEffect, useRef } from "react";
|
||||
import * as THREE from 'three';
|
||||
import { useInteraction, type InteractionMouseMoveEventArgs } from "../helpers/useInteration";
|
||||
import { SceneSync } from "../layers/sceneSync";
|
||||
import { GeometryCache } from "../layers/geometryCache";
|
||||
import { meshToDto } from "../backend/dto";
|
||||
import { db } from "../backend/db";
|
||||
import { NURBSBuilder } from "../verb/NURBSBuilder";
|
||||
import { MeshService } from "../verb/meshService";
|
||||
import { Geometry } from "../backend/geometry/geometry";
|
||||
import { useState } from "react";
|
||||
import { useSceneHelper } from "../helpers/hooks/useSceneHelper";
|
||||
import { Point3dHelper } from "../helpers/point3dHelper";
|
||||
import { state } from "../state/root";
|
||||
import { ThreeView, type ThreeViewEventArgs, type ThreeViewMouseMoveEventArgs } from "./ThreeVIew";
|
||||
|
||||
export type ViewportProps = {
|
||||
onHover?: (faceIds: string[]) => void;
|
||||
}
|
||||
export const Viewport = function () {
|
||||
|
||||
function setupScene(size: { w: number, h: number }): { scene: THREE.Scene, camera: THREE.PerspectiveCamera } {
|
||||
// --- Scene & Camera ---
|
||||
const scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color(0x0a0a12);
|
||||
const sceneHelper = useSceneHelper();
|
||||
|
||||
const camera = new THREE.PerspectiveCamera(55, size.w / size.h, 0.1, 100);
|
||||
camera.position.set(4, 3, 6);
|
||||
function handleMouseMove(e: ThreeViewMouseMoveEventArgs) {
|
||||
state.setHitTest(e.hitTest);
|
||||
|
||||
// --- Lights ---
|
||||
scene.add(new THREE.AmbientLight(0xffffff, 0.3));
|
||||
|
||||
const dir = new THREE.DirectionalLight(0xffffff, 1.2);
|
||||
dir.position.set(5, 8, 5);
|
||||
dir.castShadow = true;
|
||||
scene.add(dir);
|
||||
|
||||
const pt = new THREE.PointLight(0x5588ff, 1.5, 20);
|
||||
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;
|
||||
// scene.add(plane);
|
||||
// scene.add(new THREE.GridHelper(14, 14, 0x222233, 0x111122));
|
||||
|
||||
return { scene, camera };
|
||||
}
|
||||
|
||||
function initializeSceneObjects(scene: THREE.Scene): { positions: THREE.Vector3[], meshes: THREE.Mesh[], geometries: THREE.BufferGeometry[], materials: THREE.Material[] } {
|
||||
const materials = [
|
||||
new THREE.MeshStandardMaterial({ color: 0x5f77dd, roughness: 0.3, metalness: 0.4 }),
|
||||
new THREE.MeshStandardMaterial({ color: 0xdd775f, roughness: 0.5, metalness: 0.1 }),
|
||||
new THREE.MeshStandardMaterial({ color: 0x5fdd99, roughness: 0.2, metalness: 0.6 }),
|
||||
new THREE.MeshStandardMaterial({ color: 0xddcc5f, roughness: 0.4, metalness: 0.2 }),
|
||||
];
|
||||
|
||||
const geometries = [
|
||||
new THREE.BoxGeometry(1, 1, 1),
|
||||
new THREE.SphereGeometry(0.6, 32, 32),
|
||||
new THREE.TorusGeometry(0.5, 0.2, 16, 48),
|
||||
new THREE.ConeGeometry(0.5, 1.2, 32),
|
||||
new THREE.CylinderGeometry(0.3, 0.5, 1.2, 32),
|
||||
new THREE.OctahedronGeometry(0.65),
|
||||
];
|
||||
|
||||
const positions: [number, number, number][] = [
|
||||
[0, 0.5, 0], [2.5, 0.6, 0], [-2.5, 0.6, 0],
|
||||
[0, 0.6, 2.5], [2.5, 0.6, -2.5], [-2.5, 0.6, 2.5],
|
||||
];
|
||||
|
||||
const meshes = geometries.map((geo, i) => {
|
||||
const mesh = new THREE.Mesh(geo, materials[i % materials.length]);
|
||||
mesh.position.set(...positions[i]);
|
||||
mesh.castShadow = true;
|
||||
mesh.receiveShadow = true;
|
||||
scene.add(mesh);
|
||||
return mesh;
|
||||
});
|
||||
|
||||
return { positions: positions.map(([x, y, z]) => new THREE.Vector3().set(x, y, z)), meshes, geometries, materials };
|
||||
}
|
||||
|
||||
export const Viewport = function (props: ViewportProps) {
|
||||
|
||||
const viewportRef = useRef<HTMLDivElement | null>(null);
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
const cameraRef = useRef<THREE.PerspectiveCamera | null>(null);
|
||||
|
||||
// let hoveredMesh: THREE.Object3D | null = null;
|
||||
|
||||
let handleHover: (e: InteractionMouseMoveEventArgs) => void;
|
||||
|
||||
useEffect(() => {
|
||||
const container = viewportRef.current!;
|
||||
const W = container.clientWidth;
|
||||
const H = container.clientHeight;
|
||||
|
||||
// --- Renderer ---
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
renderer.setSize(W, H);
|
||||
renderer.shadowMap.enabled = true;
|
||||
container.appendChild(renderer.domElement);
|
||||
|
||||
canvasRef.current = renderer.domElement;
|
||||
|
||||
const { scene, camera } = setupScene({ w: W, h: H });
|
||||
cameraRef.current = camera;
|
||||
|
||||
|
||||
const cache = new GeometryCache();
|
||||
const sync = new SceneSync(scene, cache);
|
||||
|
||||
const solid = db.solids[0];
|
||||
const meshes = Geometry
|
||||
.tessellateSolid(solid.id);
|
||||
meshes
|
||||
.forEach((mesh) => {
|
||||
console.log(meshes);
|
||||
const dto = meshToDto(mesh);
|
||||
sync.addSolid(dto);
|
||||
});
|
||||
|
||||
// sync.setSelected(['48']);
|
||||
|
||||
// const { positions, meshes, geometries, materials } = initializeSceneObjects(scene);
|
||||
|
||||
// --- Resize handler ---
|
||||
const onResize = () => {
|
||||
const w = container.clientWidth;
|
||||
const h = container.clientHeight;
|
||||
camera.aspect = w / h;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(w, h);
|
||||
};
|
||||
window.addEventListener("resize", onResize);
|
||||
|
||||
// --- Hover highlight ---
|
||||
const raycaster = new THREE.Raycaster();
|
||||
handleHover = (e: InteractionMouseMoveEventArgs) => {
|
||||
raycaster.setFromCamera(new THREE.Vector2(e.x, e.y), camera);
|
||||
const hits = raycaster.intersectObjects(sync.meshes);
|
||||
const hoveredFaceIds = hits.map(hit => hit.object.userData.faceId);
|
||||
if (hoveredFaceIds.length)
|
||||
console.log(hoveredFaceIds);
|
||||
props.onHover?.(hoveredFaceIds);
|
||||
sync.setSelected(hoveredFaceIds);
|
||||
};
|
||||
|
||||
// --- Animation loop ---
|
||||
let lastTime = performance.now();
|
||||
let animId: number;
|
||||
|
||||
function animate(time: DOMHighResTimeStamp) {
|
||||
animId = requestAnimationFrame(animate);
|
||||
// const deltaTime = lastTime ? time - lastTime : 0;
|
||||
lastTime = time;
|
||||
// meshes.forEach((m, i) => {
|
||||
// m.rotation.y += 0.006 + i * 0.001;
|
||||
// m.rotation.x += 0.003;
|
||||
// if (m !== hoveredMesh) {
|
||||
// m.position.y = positions[i].y + Math.sin(t + i * 1.1) * 0.15;
|
||||
// }
|
||||
// });
|
||||
renderer.render(scene, camera);
|
||||
sceneHelper.clearPoints();
|
||||
if (e.hitTest.objects.length) {
|
||||
e.hitTest.objects.forEach((obj) => {
|
||||
sceneHelper.setPoint(obj.object.uuid, obj.point);
|
||||
})
|
||||
// console.log(e.position);
|
||||
// console.log(e.hitTest.objects.map((o) => o));
|
||||
// console.log(e.hitTest.objects.flatMap((o) => o.point.toArray()));
|
||||
}
|
||||
animId = requestAnimationFrame(animate);
|
||||
// raycaster.setFromCamera(new THREE.Vector2(e.x, e.y), camera);
|
||||
// const hits = raycaster.intersectObjects(sync.meshes);
|
||||
const hoveredFaceIds = e.hitTest.objects.map(hit => hit.object.userData.faceId);
|
||||
// if (hoveredFaceIds.length)
|
||||
// console.log(hoveredFaceIds);
|
||||
|
||||
// --- Cleanup ---
|
||||
return () => {
|
||||
if (animId)
|
||||
cancelAnimationFrame(animId);
|
||||
sceneHelper.setSelection(hoveredFaceIds);
|
||||
}
|
||||
|
||||
container.removeChild(renderer.domElement);
|
||||
function handleDispose(e: ThreeViewEventArgs): void {
|
||||
// throw new Error("Function not implemented.");
|
||||
}
|
||||
|
||||
window.removeEventListener("resize", onResize);
|
||||
|
||||
renderer.dispose();
|
||||
|
||||
sync.dispose();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useInteraction(canvasRef, cameraRef, {
|
||||
onMouseMove: (e) => handleHover?.(e),
|
||||
});
|
||||
|
||||
return (
|
||||
<div id="viewport" ref={viewportRef}>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (<>
|
||||
<ThreeView
|
||||
sceneHelper={sceneHelper}
|
||||
onMouseMove={handleMouseMove}
|
||||
onDispose={handleDispose}
|
||||
/>
|
||||
</>);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
import * as THREE from 'three';
|
||||
import {
|
||||
computeBoundsTree, disposeBoundsTree,
|
||||
computeBatchedBoundsTree, disposeBatchedBoundsTree, acceleratedRaycast,
|
||||
} from 'three-mesh-bvh';
|
||||
import type { SceneSync } from '../layers/sceneSync';
|
||||
import type { SceneHelper } from './sceneHelper';
|
||||
|
||||
// Add the extension functions
|
||||
THREE.BufferGeometry.prototype.computeBoundsTree = computeBoundsTree;
|
||||
THREE.BufferGeometry.prototype.disposeBoundsTree = disposeBoundsTree;
|
||||
THREE.Mesh.prototype.raycast = acceleratedRaycast;
|
||||
|
||||
THREE.BatchedMesh.prototype.computeBoundsTree = computeBatchedBoundsTree;
|
||||
THREE.BatchedMesh.prototype.disposeBoundsTree = disposeBatchedBoundsTree;
|
||||
THREE.BatchedMesh.prototype.raycast = acceleratedRaycast;
|
||||
|
||||
export type HitTest = {
|
||||
objects: THREE.Intersection<THREE.Object3D>[];
|
||||
}
|
||||
|
||||
export type HitTestRaycasterOptions = {
|
||||
cameraPixelSize: THREE.Vector2Like;
|
||||
tolerancePixels: number;
|
||||
}
|
||||
|
||||
export type HitTestOptions = HitTestRaycasterOptions & {
|
||||
}
|
||||
|
||||
export class HitTestFactory {
|
||||
|
||||
private static raycasters: [THREE.Vector2, THREE.Raycaster][] = Array(9).fill(0).map(() => [new THREE.Vector2(), new THREE.Raycaster()]);
|
||||
|
||||
private static setupRaycasters(cursor: THREE.Vector2, camera: THREE.PerspectiveCamera, options: HitTestRaycasterOptions) {
|
||||
|
||||
this.raycasters[0][0].copy(cursor);
|
||||
this.raycasters[0][1].setFromCamera(cursor, camera);
|
||||
|
||||
const count = HitTestFactory.raycasters.length - 1;
|
||||
const step = Math.PI * 2 / count;
|
||||
|
||||
for (let angle = 0, idx = 0; idx < count; angle += step, idx++) {
|
||||
const pos = {
|
||||
x: Math.cos(angle) * options.tolerancePixels * options.cameraPixelSize.x,
|
||||
y: Math.sin(angle) * options.tolerancePixels * options.cameraPixelSize.y,
|
||||
};
|
||||
const v = HitTestFactory.raycasters[idx + 1][0];
|
||||
v.copy(cursor).add(pos);
|
||||
HitTestFactory.raycasters[idx + 1][1].setFromCamera(v, camera);
|
||||
}
|
||||
}
|
||||
|
||||
public static getRaycasterPosition(index: number): THREE.Vector2 {
|
||||
return HitTestFactory.raycasters[index][0];
|
||||
}
|
||||
|
||||
public static get raycasterCount(): number {
|
||||
return HitTestFactory.raycasters.length;
|
||||
}
|
||||
|
||||
public static hitTest(scene: SceneHelper, cursor: THREE.Vector2, camera: THREE.PerspectiveCamera, options: HitTestOptions): HitTest {
|
||||
|
||||
HitTestFactory.setupRaycasters(cursor, camera, options);
|
||||
|
||||
const objects: THREE.Object3D[] = scene.objects;
|
||||
|
||||
const hitTest: Record<string, THREE.Intersection<THREE.Object3D>> = {};
|
||||
|
||||
HitTestFactory.raycasters.forEach((raycaster) => {
|
||||
const hits = raycaster[1].intersectObjects(objects);
|
||||
for (const hit of hits) {
|
||||
hitTest[hit.object.uuid] = hit;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
objects: Object.values(hitTest),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,13 @@
|
|||
import { useEffect, type RefObject } from "react";
|
||||
import * as THREE from "three";
|
||||
|
||||
export type InteractionMouseMoveEventArgs = { x: number, y: number };
|
||||
export type InteractionMouseMoveEventArgs = {
|
||||
position: { x: number, y: number },
|
||||
pixelSize: { x: number, y: number },
|
||||
};
|
||||
|
||||
export type UseInteractionOptions = {
|
||||
onMouseMove?: (position: InteractionMouseMoveEventArgs) => void,
|
||||
onMouseMove?: (e: InteractionMouseMoveEventArgs) => void,
|
||||
}
|
||||
|
||||
export function useInteraction(
|
||||
|
|
@ -66,11 +69,11 @@ export function useInteraction(
|
|||
|
||||
const onHover = (e: MouseEvent) => {
|
||||
const rect = target.getBoundingClientRect();
|
||||
const pos = {
|
||||
const position = {
|
||||
x: ((e.clientX - rect.left) / rect.width) * 2 - 1,
|
||||
y: -((e.clientY - rect.top) / rect.height) * 2 + 1,
|
||||
};
|
||||
options.onMouseMove?.(pos);
|
||||
options.onMouseMove?.({ position, pixelSize: { x: 2 / rect.width, y: 2 / rect.height } });
|
||||
};
|
||||
|
||||
const onContextMenu = (e: Event) => e.preventDefault();
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { useState } from "react";
|
||||
import { SceneHelper } from "../sceneHelper";
|
||||
|
||||
export function useSceneHelper(): SceneHelper {
|
||||
|
||||
const [sceneHelper] = useState<SceneHelper>(new SceneHelper());
|
||||
|
||||
return sceneHelper;
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import * as THREE from "three";
|
||||
|
||||
export class Point2dHelper {
|
||||
|
||||
private vec = new THREE.Vector3(); // create once and reuse
|
||||
|
||||
private marker = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(0.01, 8, 8),
|
||||
new THREE.MeshBasicMaterial({ color: 0xff0000 })
|
||||
);
|
||||
private camera: THREE.PerspectiveCamera;
|
||||
|
||||
constructor(scene: THREE.Scene, camera: THREE.PerspectiveCamera) {
|
||||
this.camera = camera;
|
||||
scene.add(this.marker);
|
||||
}
|
||||
|
||||
public set(position: THREE.Vector2Like) {
|
||||
|
||||
const targetZ = -10;
|
||||
|
||||
this.vec.set(position.x, position.y, 0.5);
|
||||
|
||||
this.vec.unproject(this.camera);
|
||||
|
||||
this.vec.sub(this.camera.position).normalize();
|
||||
|
||||
// var distance = -this.camera.position.z / this.vec.z;
|
||||
var distance = (this.camera.near - this.camera.position.z) / this.vec.z;
|
||||
|
||||
this.marker.position.copy(this.camera.position).add(this.vec.multiplyScalar(distance));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
import * as THREE from "three";
|
||||
|
||||
export type Point3d = {
|
||||
position: THREE.Vector3,
|
||||
mesh: THREE.Mesh,
|
||||
}
|
||||
|
||||
export class Point3dHelper {
|
||||
|
||||
private scene: THREE.Scene;
|
||||
private readonly baseMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff });
|
||||
|
||||
private readonly markers: Record<string, Point3d> = {};
|
||||
|
||||
constructor(scene: THREE.Scene) {
|
||||
this.scene = scene;
|
||||
}
|
||||
|
||||
private ensure(id: string): Point3d {
|
||||
if (!this.markers[id]) {
|
||||
this.markers[id] = {
|
||||
position: new THREE.Vector3(),
|
||||
mesh: new THREE.Mesh(
|
||||
new THREE.SphereGeometry(0.1, 8, 8),
|
||||
this.baseMaterial,
|
||||
),
|
||||
};
|
||||
this.scene.add(this.markers[id].mesh);
|
||||
}
|
||||
|
||||
return this.markers[id];
|
||||
}
|
||||
|
||||
private disposePoint(id: string) {
|
||||
const point = this.markers[id];
|
||||
if (point) {
|
||||
this.scene.remove(point.mesh);
|
||||
point.mesh.geometry.dispose();
|
||||
delete (this.markers[id]);
|
||||
}
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
for (const id in this.markers)
|
||||
this.disposePoint(id);
|
||||
}
|
||||
|
||||
public set(id: string, position: THREE.Vector3Like) {
|
||||
const point = this.ensure(id);
|
||||
point.position.copy(position);
|
||||
point.mesh.position.copy(position);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import type { Object3D, Object3DEventMap, Scene, Vector3 } from "three";
|
||||
import { Point3dHelper } from "./point3dHelper";
|
||||
import { SceneSync } from "../layers/sceneSync";
|
||||
import { GeometryCache } from "../layers/geometryCache";
|
||||
import type { Id } from "../types";
|
||||
|
||||
export class SceneHelper {
|
||||
|
||||
private sync: SceneSync | undefined;
|
||||
|
||||
private pointHelper: Point3dHelper | undefined;
|
||||
|
||||
constructor() {
|
||||
|
||||
}
|
||||
|
||||
public initialize(scene: Scene) {
|
||||
this.pointHelper = new Point3dHelper(scene);
|
||||
|
||||
this.sync = new SceneSync(scene, new GeometryCache());
|
||||
this.sync.addWholeModel();
|
||||
}
|
||||
|
||||
public setSelection(faceIds: Id[]) {
|
||||
this.sync?.setSelected(faceIds);
|
||||
}
|
||||
|
||||
public get objects(): Object3D<Object3DEventMap>[] {
|
||||
|
||||
return this.sync?.meshes ?? [];
|
||||
}
|
||||
|
||||
public setPoint(id: string, point: Vector3) {
|
||||
this.pointHelper?.set(id, point);
|
||||
}
|
||||
|
||||
public clearPoints() {
|
||||
this.pointHelper?.dispose();
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.sync?.dispose();
|
||||
|
||||
this.clearPoints();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
import * as THREE from 'three';
|
||||
import type { MeshDto } from '../backend/dto';
|
||||
|
||||
export type Id = string;
|
||||
import type { Id } from '../types';
|
||||
|
||||
export class GeometryCache {
|
||||
private readonly _cache = new Map<string, THREE.BufferGeometry>();
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
import * as THREE from 'three';
|
||||
import type { GeometryCache, Id } from './geometryCache';
|
||||
import type { MeshDto } from '../backend/dto';
|
||||
import type { GeometryCache } from './geometryCache';
|
||||
import { meshToDto, type MeshDto } from '../backend/dto/mesh';
|
||||
import { model } from '../model/model';
|
||||
import { Geometry } from '../backend/geometry/geometry';
|
||||
import type { Id } from '../types';
|
||||
|
||||
export class SceneSync {
|
||||
private scene: THREE.Scene;
|
||||
private meshByFace: Map<Id, THREE.Mesh>;
|
||||
private meshByFace: Record<Id, THREE.Mesh> = {}; // faceId → THREE.Mesh
|
||||
private cache: GeometryCache;
|
||||
|
||||
private _selectedFaceIds: Id[] = [];
|
||||
|
|
@ -16,7 +19,6 @@ export class SceneSync {
|
|||
constructor(scene: THREE.Scene, cache: GeometryCache) {
|
||||
this.scene = scene;
|
||||
this.cache = cache;
|
||||
this.meshByFace = new Map(); // faceId → THREE.Mesh
|
||||
}
|
||||
|
||||
public get selectedFaceIds() {
|
||||
|
|
@ -24,7 +26,21 @@ export class SceneSync {
|
|||
}
|
||||
|
||||
public get meshes(): THREE.Mesh[] {
|
||||
return Array.from(this.meshByFace.values());
|
||||
return Object.values(this.meshByFace);
|
||||
}
|
||||
|
||||
public get items(): Record<Id, THREE.Mesh> {
|
||||
return this.meshByFace;
|
||||
}
|
||||
|
||||
addWholeModel() {
|
||||
for (const id of Object.keys(model.solids)) {
|
||||
const meshes = Geometry
|
||||
.tessellateSolid(id)
|
||||
.map(meshToDto);
|
||||
for (const mesh of meshes)
|
||||
this.addSolid(mesh);
|
||||
}
|
||||
}
|
||||
|
||||
// Called when FE scene graph syncs from BE
|
||||
|
|
@ -34,12 +50,14 @@ export class SceneSync {
|
|||
1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1
|
||||
]);
|
||||
|
||||
if (this.meshByFace.has(faceId))
|
||||
if (this.meshByFace[faceId])
|
||||
return;
|
||||
|
||||
const geo = this.cache.getOrCreate(faceId, 0, dto);
|
||||
const mesh = new THREE.Mesh(geo, this.baseMaterial.clone());
|
||||
mesh.userData.faceId = faceId;
|
||||
mesh.userData.surfaceId = dto.surfaceId;
|
||||
mesh.userData.solidId = dto.solidId;
|
||||
|
||||
// Apply transform (col-major mat4 from BE)
|
||||
const m = new THREE.Matrix4();
|
||||
|
|
@ -47,11 +65,11 @@ export class SceneSync {
|
|||
mesh.applyMatrix4(m);
|
||||
|
||||
this.scene.add(mesh);
|
||||
this.meshByFace.set(faceId, mesh);
|
||||
this.meshByFace[faceId] = mesh;
|
||||
}
|
||||
|
||||
updateTransform(faceId: Id, colMajorMat4: number[]) {
|
||||
const mesh = this.meshByFace.get(faceId);
|
||||
const mesh = this.meshByFace[faceId];
|
||||
if (!mesh) return;
|
||||
const m = new THREE.Matrix4().fromArray(colMajorMat4);
|
||||
mesh.matrix.copy(m);
|
||||
|
|
@ -61,7 +79,7 @@ export class SceneSync {
|
|||
setSelected(faceIds: Id[]) {
|
||||
this._selectedFaceIds = faceIds;
|
||||
|
||||
for (const [sid, mesh] of this.meshByFace) {
|
||||
for (const [sid, mesh] of Object.entries(this.meshByFace)) {
|
||||
const mat = mesh.material as THREE.MeshPhongMaterial;
|
||||
|
||||
if (faceIds.includes(sid)) {
|
||||
|
|
@ -75,8 +93,8 @@ export class SceneSync {
|
|||
}
|
||||
}
|
||||
|
||||
public disposeMesh(solidId: Id) {
|
||||
const mesh = this.meshByFace.get(solidId);
|
||||
public disposeMesh(faceId: Id) {
|
||||
const mesh = this.meshByFace[faceId];
|
||||
if (!mesh)
|
||||
return;
|
||||
|
||||
|
|
@ -91,13 +109,13 @@ export class SceneSync {
|
|||
else
|
||||
mesh.material.dispose();
|
||||
|
||||
this.cache.unset(solidId, 0);
|
||||
this.meshByFace.delete(solidId);
|
||||
this.cache.unset(faceId, 0);
|
||||
delete (this.meshByFace[faceId]);
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
for (const solidId of this.meshByFace.keys())
|
||||
this.disposeMesh(solidId);
|
||||
for (const faceId of Object.keys(this.meshByFace))
|
||||
this.disposeMesh(faceId);
|
||||
|
||||
this.baseMaterial.dispose();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
// import { v4 as uuid } from 'uuid';
|
||||
import type { Edge, Face, HalfEdge, Id, Loop, Primitive, Solid, Surface, Vertex } from "../types/brep";
|
||||
|
||||
export type DbBlob = {
|
||||
vertices: Vertex[],
|
||||
halfEdges: HalfEdge[],
|
||||
edges: Edge[],
|
||||
loops: Loop[],
|
||||
faces: Face[],
|
||||
surfaces: Surface[],
|
||||
solids: Solid[],
|
||||
}
|
||||
|
||||
export class Model {
|
||||
public vertices: Record<Id, Vertex> = {};
|
||||
public halfEdges: Record<Id, HalfEdge> = {};
|
||||
public edges: Record<Id, Edge> = {};
|
||||
public loops: Record<Id, Loop> = {};
|
||||
public faces: Record<Id, Face> = {};
|
||||
public surfaces: Record<Id, Surface> = {};
|
||||
public solids: Record<Id, Solid> = {};
|
||||
|
||||
public get primitives(): Record<Id, Primitive> {
|
||||
return {
|
||||
...this.vertices,
|
||||
...this.halfEdges,
|
||||
...this.edges,
|
||||
...this.loops,
|
||||
...this.faces,
|
||||
...this.surfaces,
|
||||
...this.solids,
|
||||
};
|
||||
}
|
||||
|
||||
public initFromBlob(value: DbBlob) {
|
||||
this.vertices = Object.fromEntries(value.vertices.map((v) => [v.id, v]));
|
||||
this.halfEdges = Object.fromEntries(value.halfEdges.map((v) => [v.id, v]));
|
||||
this.edges = Object.fromEntries(value.edges.map((v) => [v.id, v]));
|
||||
this.loops = Object.fromEntries(value.loops.map((v) => [v.id, v]));
|
||||
this.faces = Object.fromEntries(value.faces.map((v) => [v.id, v]));
|
||||
this.surfaces = Object.fromEntries(value.surfaces.map((v) => [v.id, v]));
|
||||
this.solids = Object.fromEntries(value.solids.map((v) => [v.id, v]));
|
||||
}
|
||||
|
||||
public primitiveById(id: Primitive['id']): Primitive | undefined {
|
||||
return this.primitives[id];
|
||||
}
|
||||
|
||||
public vertexById(id: Vertex['id']): Vertex | undefined {
|
||||
return this.vertices[id];
|
||||
}
|
||||
|
||||
public halfEdgeById(id: HalfEdge['id']): HalfEdge | undefined {
|
||||
return this.halfEdges[id];
|
||||
}
|
||||
|
||||
public halfEdgesByLoop(loopId: string): HalfEdge[] {
|
||||
const loop = this.loopById(loopId)!;
|
||||
const startHalfEdgeId = loop.start;
|
||||
|
||||
const halfEdges: HalfEdge[] = [];
|
||||
|
||||
const visited = new Set<string>();
|
||||
let halfEdgeId: string | undefined = startHalfEdgeId;
|
||||
while (halfEdgeId && !visited.has(halfEdgeId)) {
|
||||
const halfEdge = this.halfEdgeById(halfEdgeId)! as HalfEdge;
|
||||
halfEdges.push(halfEdge);
|
||||
visited.add(halfEdgeId);
|
||||
halfEdgeId = halfEdge.next;
|
||||
}
|
||||
|
||||
return halfEdges;
|
||||
}
|
||||
|
||||
public edgeById(id: Edge['id']): Edge | undefined {
|
||||
return this.edges[id];
|
||||
}
|
||||
|
||||
public loopById(id: Loop['id']): Loop | undefined {
|
||||
return this.loops[id];
|
||||
}
|
||||
|
||||
public faceById(id: Face['id']): Face | undefined {
|
||||
return this.faces[id];
|
||||
}
|
||||
|
||||
public surfaceById(id: Surface['id']): Surface | undefined {
|
||||
return this.surfaces[id];
|
||||
}
|
||||
|
||||
public solidById(id: Solid['id']): Solid | undefined {
|
||||
return this.solids[id];
|
||||
}
|
||||
}
|
||||
|
||||
export const model = new Model();
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
import { makeAutoObservable } from "mobx";
|
||||
import type { Id } from "../types";
|
||||
import type { HitTest } from "../helpers/hitTest";
|
||||
|
||||
export class Root {
|
||||
|
||||
public selectedPrimitiveIds: Id[] = [];
|
||||
public hitTest: HitTest = { objects: [] };
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
|
|
@ -12,6 +13,10 @@ export class Root {
|
|||
public setSelectedPrimitiveIds(value: Id[]) {
|
||||
this.selectedPrimitiveIds = value;
|
||||
}
|
||||
|
||||
public setHitTest(value: HitTest) {
|
||||
this.hitTest = value;
|
||||
}
|
||||
}
|
||||
|
||||
export const state = new Root();
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
export type Id = string;
|
||||
|
||||
export type Primitive = {
|
||||
id: Id,
|
||||
version?: number;
|
||||
}
|
||||
|
||||
export type Vertex = Primitive & {
|
||||
x: number,
|
||||
y: number,
|
||||
z: number,
|
||||
ownerHalfEdgeA?: HalfEdge['id'],
|
||||
ownerHalfEdgeB?: HalfEdge['id'],
|
||||
}
|
||||
|
||||
export type HalfEdge = Primitive & {
|
||||
origin: Vertex['id'],
|
||||
twin?: HalfEdge['id'],
|
||||
next: HalfEdge['id'], // next in loop
|
||||
prev: HalfEdge['id'], // prev in loop
|
||||
ownerLoop: Loop['id'],
|
||||
// ownerEdge: Edge['id'],
|
||||
}
|
||||
|
||||
// export type Edge = Primitive & {
|
||||
// a: HalfEdge['id'],
|
||||
// b: HalfEdge['id'],
|
||||
// // crease: boolean, // sharp vs smooth
|
||||
// ownerLoop: Loop['id'],
|
||||
// }
|
||||
|
||||
export type Loop = Primitive & {
|
||||
start: HalfEdge['id'],
|
||||
ownerFace: Face['id'],
|
||||
}
|
||||
|
||||
export type Face = Primitive & {
|
||||
outerLoop: Loop['id'],
|
||||
holes: Loop['id'][],
|
||||
ownerSurface: Surface['id'],
|
||||
}
|
||||
|
||||
export type Surface = Primitive & {
|
||||
faces: Face['id'][],
|
||||
}
|
||||
|
||||
export type Solid = Primitive & {
|
||||
outerSurface: Surface['id'][],
|
||||
// holes: Surface['id'][],
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "server",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "commonjs"
|
||||
}
|
||||
Loading…
Reference in New Issue