basic hit testing
This commit is contained in:
parent
aa17ecd4c9
commit
bd5fa12a1e
|
|
@ -16,6 +16,7 @@
|
||||||
"react-dom": "^19.2.6",
|
"react-dom": "^19.2.6",
|
||||||
"sass-embedded": "^1.99.0",
|
"sass-embedded": "^1.99.0",
|
||||||
"three": "^0.184.0",
|
"three": "^0.184.0",
|
||||||
|
"three-mesh-bvh": "^0.9.10",
|
||||||
"uuid": "^14.0.0",
|
"uuid": "^14.0.0",
|
||||||
"verb-nurbs": "^3.0.3"
|
"verb-nurbs": "^3.0.3"
|
||||||
},
|
},
|
||||||
|
|
@ -3313,7 +3314,17 @@
|
||||||
"version": "0.184.0",
|
"version": "0.184.0",
|
||||||
"resolved": "https://registry.npmjs.org/three/-/three-0.184.0.tgz",
|
"resolved": "https://registry.npmjs.org/three/-/three-0.184.0.tgz",
|
||||||
"integrity": "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==",
|
"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": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.16",
|
"version": "0.2.16",
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
"react-dom": "^19.2.6",
|
"react-dom": "^19.2.6",
|
||||||
"sass-embedded": "^1.99.0",
|
"sass-embedded": "^1.99.0",
|
||||||
"three": "^0.184.0",
|
"three": "^0.184.0",
|
||||||
|
"three-mesh-bvh": "^0.9.10",
|
||||||
"uuid": "^14.0.0",
|
"uuid": "^14.0.0",
|
||||||
"verb-nurbs": "^3.0.3"
|
"verb-nurbs": "^3.0.3"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,18 @@
|
||||||
#viewport {
|
#viewport {
|
||||||
width: 600px;
|
|
||||||
height: 600px;
|
|
||||||
background: white;
|
background: white;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
left: 0px;
|
left: 0px;
|
||||||
|
width: 600px;
|
||||||
|
height: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#hit-test {
|
||||||
|
position: absolute;
|
||||||
|
top: 600px;
|
||||||
|
left: 000px;
|
||||||
|
width: 600px;
|
||||||
|
font-size: 75%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#blob-view {
|
#blob-view {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Viewport } from './components/Viewport'
|
import { Viewport } from './components/Viewport'
|
||||||
import { DbView } from './components/DbView'
|
import { DbView } from './components/DbView'
|
||||||
|
import { HitTestView } from './components/HitTestView'
|
||||||
|
|
||||||
import './App.scss'
|
import './App.scss'
|
||||||
|
|
||||||
|
|
@ -7,6 +8,7 @@ export const App = function () {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Viewport />
|
<Viewport />
|
||||||
|
<HitTestView />
|
||||||
<DbView />
|
<DbView />
|
||||||
</div>
|
</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 = {
|
export type MeshDto = {
|
||||||
vertices: Float32Array;
|
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 { useState } from "react";
|
||||||
import * as THREE from 'three';
|
import { useSceneHelper } from "../helpers/hooks/useSceneHelper";
|
||||||
import { useInteraction, type InteractionMouseMoveEventArgs } from "../helpers/useInteration";
|
import { Point3dHelper } from "../helpers/point3dHelper";
|
||||||
import { SceneSync } from "../layers/sceneSync";
|
import { state } from "../state/root";
|
||||||
import { GeometryCache } from "../layers/geometryCache";
|
import { ThreeView, type ThreeViewEventArgs, type ThreeViewMouseMoveEventArgs } from "./ThreeVIew";
|
||||||
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";
|
|
||||||
|
|
||||||
export type ViewportProps = {
|
export const Viewport = function () {
|
||||||
onHover?: (faceIds: string[]) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupScene(size: { w: number, h: number }): { scene: THREE.Scene, camera: THREE.PerspectiveCamera } {
|
const sceneHelper = useSceneHelper();
|
||||||
// --- 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);
|
function handleMouseMove(e: ThreeViewMouseMoveEventArgs) {
|
||||||
camera.position.set(4, 3, 6);
|
state.setHitTest(e.hitTest);
|
||||||
|
|
||||||
// --- Lights ---
|
sceneHelper.clearPoints();
|
||||||
scene.add(new THREE.AmbientLight(0xffffff, 0.3));
|
if (e.hitTest.objects.length) {
|
||||||
|
e.hitTest.objects.forEach((obj) => {
|
||||||
const dir = new THREE.DirectionalLight(0xffffff, 1.2);
|
sceneHelper.setPoint(obj.object.uuid, obj.point);
|
||||||
dir.position.set(5, 8, 5);
|
})
|
||||||
dir.castShadow = true;
|
// console.log(e.position);
|
||||||
scene.add(dir);
|
// console.log(e.hitTest.objects.map((o) => o));
|
||||||
|
// console.log(e.hitTest.objects.flatMap((o) => o.point.toArray()));
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
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 ---
|
sceneHelper.setSelection(hoveredFaceIds);
|
||||||
return () => {
|
}
|
||||||
if (animId)
|
|
||||||
cancelAnimationFrame(animId);
|
|
||||||
|
|
||||||
container.removeChild(renderer.domElement);
|
function handleDispose(e: ThreeViewEventArgs): void {
|
||||||
|
// throw new Error("Function not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
window.removeEventListener("resize", onResize);
|
return (<>
|
||||||
|
<ThreeView
|
||||||
renderer.dispose();
|
sceneHelper={sceneHelper}
|
||||||
|
onMouseMove={handleMouseMove}
|
||||||
sync.dispose();
|
onDispose={handleDispose}
|
||||||
};
|
/>
|
||||||
}, []);
|
</>);
|
||||||
|
};
|
||||||
useInteraction(canvasRef, cameraRef, {
|
|
||||||
onMouseMove: (e) => handleHover?.(e),
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div id="viewport" ref={viewportRef}>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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 { useEffect, type RefObject } from "react";
|
||||||
import * as THREE from "three";
|
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 = {
|
export type UseInteractionOptions = {
|
||||||
onMouseMove?: (position: InteractionMouseMoveEventArgs) => void,
|
onMouseMove?: (e: InteractionMouseMoveEventArgs) => void,
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useInteraction(
|
export function useInteraction(
|
||||||
|
|
@ -66,11 +69,11 @@ export function useInteraction(
|
||||||
|
|
||||||
const onHover = (e: MouseEvent) => {
|
const onHover = (e: MouseEvent) => {
|
||||||
const rect = target.getBoundingClientRect();
|
const rect = target.getBoundingClientRect();
|
||||||
const pos = {
|
const position = {
|
||||||
x: ((e.clientX - rect.left) / rect.width) * 2 - 1,
|
x: ((e.clientX - rect.left) / rect.width) * 2 - 1,
|
||||||
y: -((e.clientY - rect.top) / rect.height) * 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();
|
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 * as THREE from 'three';
|
||||||
import type { MeshDto } from '../backend/dto';
|
import type { MeshDto } from '../backend/dto';
|
||||||
|
import type { Id } from '../types';
|
||||||
export type Id = string;
|
|
||||||
|
|
||||||
export class GeometryCache {
|
export class GeometryCache {
|
||||||
private readonly _cache = new Map<string, THREE.BufferGeometry>();
|
private readonly _cache = new Map<string, THREE.BufferGeometry>();
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import type { GeometryCache, Id } from './geometryCache';
|
import type { GeometryCache } from './geometryCache';
|
||||||
import type { MeshDto } from '../backend/dto';
|
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 {
|
export class SceneSync {
|
||||||
private scene: THREE.Scene;
|
private scene: THREE.Scene;
|
||||||
private meshByFace: Map<Id, THREE.Mesh>;
|
private meshByFace: Record<Id, THREE.Mesh> = {}; // faceId → THREE.Mesh
|
||||||
private cache: GeometryCache;
|
private cache: GeometryCache;
|
||||||
|
|
||||||
private _selectedFaceIds: Id[] = [];
|
private _selectedFaceIds: Id[] = [];
|
||||||
|
|
@ -16,7 +19,6 @@ export class SceneSync {
|
||||||
constructor(scene: THREE.Scene, cache: GeometryCache) {
|
constructor(scene: THREE.Scene, cache: GeometryCache) {
|
||||||
this.scene = scene;
|
this.scene = scene;
|
||||||
this.cache = cache;
|
this.cache = cache;
|
||||||
this.meshByFace = new Map(); // faceId → THREE.Mesh
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public get selectedFaceIds() {
|
public get selectedFaceIds() {
|
||||||
|
|
@ -24,7 +26,21 @@ export class SceneSync {
|
||||||
}
|
}
|
||||||
|
|
||||||
public get meshes(): THREE.Mesh[] {
|
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
|
// 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
|
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;
|
return;
|
||||||
|
|
||||||
const geo = this.cache.getOrCreate(faceId, 0, dto);
|
const geo = this.cache.getOrCreate(faceId, 0, dto);
|
||||||
const mesh = new THREE.Mesh(geo, this.baseMaterial.clone());
|
const mesh = new THREE.Mesh(geo, this.baseMaterial.clone());
|
||||||
mesh.userData.faceId = faceId;
|
mesh.userData.faceId = faceId;
|
||||||
|
mesh.userData.surfaceId = dto.surfaceId;
|
||||||
|
mesh.userData.solidId = dto.solidId;
|
||||||
|
|
||||||
// Apply transform (col-major mat4 from BE)
|
// Apply transform (col-major mat4 from BE)
|
||||||
const m = new THREE.Matrix4();
|
const m = new THREE.Matrix4();
|
||||||
|
|
@ -47,11 +65,11 @@ export class SceneSync {
|
||||||
mesh.applyMatrix4(m);
|
mesh.applyMatrix4(m);
|
||||||
|
|
||||||
this.scene.add(mesh);
|
this.scene.add(mesh);
|
||||||
this.meshByFace.set(faceId, mesh);
|
this.meshByFace[faceId] = mesh;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTransform(faceId: Id, colMajorMat4: number[]) {
|
updateTransform(faceId: Id, colMajorMat4: number[]) {
|
||||||
const mesh = this.meshByFace.get(faceId);
|
const mesh = this.meshByFace[faceId];
|
||||||
if (!mesh) return;
|
if (!mesh) return;
|
||||||
const m = new THREE.Matrix4().fromArray(colMajorMat4);
|
const m = new THREE.Matrix4().fromArray(colMajorMat4);
|
||||||
mesh.matrix.copy(m);
|
mesh.matrix.copy(m);
|
||||||
|
|
@ -61,7 +79,7 @@ export class SceneSync {
|
||||||
setSelected(faceIds: Id[]) {
|
setSelected(faceIds: Id[]) {
|
||||||
this._selectedFaceIds = faceIds;
|
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;
|
const mat = mesh.material as THREE.MeshPhongMaterial;
|
||||||
|
|
||||||
if (faceIds.includes(sid)) {
|
if (faceIds.includes(sid)) {
|
||||||
|
|
@ -75,8 +93,8 @@ export class SceneSync {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public disposeMesh(solidId: Id) {
|
public disposeMesh(faceId: Id) {
|
||||||
const mesh = this.meshByFace.get(solidId);
|
const mesh = this.meshByFace[faceId];
|
||||||
if (!mesh)
|
if (!mesh)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
@ -91,13 +109,13 @@ export class SceneSync {
|
||||||
else
|
else
|
||||||
mesh.material.dispose();
|
mesh.material.dispose();
|
||||||
|
|
||||||
this.cache.unset(solidId, 0);
|
this.cache.unset(faceId, 0);
|
||||||
this.meshByFace.delete(solidId);
|
delete (this.meshByFace[faceId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public dispose() {
|
public dispose() {
|
||||||
for (const solidId of this.meshByFace.keys())
|
for (const faceId of Object.keys(this.meshByFace))
|
||||||
this.disposeMesh(solidId);
|
this.disposeMesh(faceId);
|
||||||
|
|
||||||
this.baseMaterial.dispose();
|
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 { makeAutoObservable } from "mobx";
|
||||||
import type { Id } from "../types";
|
import type { Id } from "../types";
|
||||||
|
import type { HitTest } from "../helpers/hitTest";
|
||||||
|
|
||||||
export class Root {
|
export class Root {
|
||||||
|
|
||||||
public selectedPrimitiveIds: Id[] = [];
|
public selectedPrimitiveIds: Id[] = [];
|
||||||
|
public hitTest: HitTest = { objects: [] };
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
|
|
@ -12,6 +13,10 @@ export class Root {
|
||||||
public setSelectedPrimitiveIds(value: Id[]) {
|
public setSelectedPrimitiveIds(value: Id[]) {
|
||||||
this.selectedPrimitiveIds = value;
|
this.selectedPrimitiveIds = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setHitTest(value: HitTest) {
|
||||||
|
this.hitTest = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const state = new Root();
|
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