diff --git a/client/package-lock.json b/client/package-lock.json
index 84d7341..8590727 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -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",
diff --git a/client/package.json b/client/package.json
index 930d355..adef8d9 100644
--- a/client/package.json
+++ b/client/package.json
@@ -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"
},
diff --git a/client/src/App.scss b/client/src/App.scss
index c0c7a1f..1e3303b 100644
--- a/client/src/App.scss
+++ b/client/src/App.scss
@@ -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 {
diff --git a/client/src/App.tsx b/client/src/App.tsx
index b92b3e8..3413d57 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -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 (
+
)
diff --git a/client/src/backend/dto/index.ts b/client/src/backend/dto/index.ts
new file mode 100644
index 0000000..918b977
--- /dev/null
+++ b/client/src/backend/dto/index.ts
@@ -0,0 +1 @@
+export * from './mesh';
diff --git a/client/src/backend/dto.ts b/client/src/backend/dto/mesh.ts
similarity index 90%
rename from client/src/backend/dto.ts
rename to client/src/backend/dto/mesh.ts
index fb74925..0a1e1bb 100644
--- a/client/src/backend/dto.ts
+++ b/client/src/backend/dto/mesh.ts
@@ -1,4 +1,4 @@
-import type { Mesh, Solid, Surface } from "../types";
+import type { Mesh, Solid, Surface } from "../../types";
export type MeshDto = {
vertices: Float32Array;
diff --git a/client/src/backend/dto/solid.ts b/client/src/backend/dto/solid.ts
new file mode 100644
index 0000000..e135aa7
--- /dev/null
+++ b/client/src/backend/dto/solid.ts
@@ -0,0 +1,3 @@
+export type SolildDto = {
+
+}
diff --git a/client/src/components/HitTestView.tsx b/client/src/components/HitTestView.tsx
new file mode 100644
index 0000000..ff328de
--- /dev/null
+++ b/client/src/components/HitTestView.tsx
@@ -0,0 +1,20 @@
+import { observer } from "mobx-react-lite";
+import { state } from "../state/root";
+
+export const HitTestView = observer(function () {
+
+ return (
+
+
+ {
+ state.hitTest.objects.map((obj) =>
+
+ {JSON.stringify(obj.point.toArray())}
+ {JSON.stringify(obj.object.userData)}
+
+ )
+ }
+
+
+ )
+});
diff --git a/client/src/components/ThreeVIew.tsx b/client/src/components/ThreeVIew.tsx
new file mode 100644
index 0000000..a8b2531
--- /dev/null
+++ b/client/src/components/ThreeVIew.tsx
@@ -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(null);
+ const canvasRef = useRef(null);
+ const cameraRef = useRef(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 (
+
+
+
+ )
+}
diff --git a/client/src/components/Viewport.tsx b/client/src/components/Viewport.tsx
index 5f418fb..32548bb 100644
--- a/client/src/components/Viewport.tsx
+++ b/client/src/components/Viewport.tsx
@@ -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(null);
- const canvasRef = useRef(null);
- const cameraRef = useRef(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 (
-
-
-
- )
-}
+ return (<>
+
+ >);
+};
diff --git a/client/src/helpers/hitTest.ts b/client/src/helpers/hitTest.ts
new file mode 100644
index 0000000..f0e3bb7
--- /dev/null
+++ b/client/src/helpers/hitTest.ts
@@ -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[];
+}
+
+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> = {};
+
+ 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),
+ }
+ }
+}
diff --git a/client/src/helpers/useInteration.ts b/client/src/helpers/hooks/useInteration.ts
similarity index 91%
rename from client/src/helpers/useInteration.ts
rename to client/src/helpers/hooks/useInteration.ts
index 0a9d51d..3c1b44f 100644
--- a/client/src/helpers/useInteration.ts
+++ b/client/src/helpers/hooks/useInteration.ts
@@ -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();
diff --git a/client/src/helpers/hooks/useSceneHelper.ts b/client/src/helpers/hooks/useSceneHelper.ts
new file mode 100644
index 0000000..1db622e
--- /dev/null
+++ b/client/src/helpers/hooks/useSceneHelper.ts
@@ -0,0 +1,9 @@
+import { useState } from "react";
+import { SceneHelper } from "../sceneHelper";
+
+export function useSceneHelper(): SceneHelper {
+
+ const [sceneHelper] = useState(new SceneHelper());
+
+ return sceneHelper;
+}
diff --git a/client/src/helpers/point2dHelper.ts b/client/src/helpers/point2dHelper.ts
new file mode 100644
index 0000000..795fc2f
--- /dev/null
+++ b/client/src/helpers/point2dHelper.ts
@@ -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));
+ }
+}
\ No newline at end of file
diff --git a/client/src/helpers/point3dHelper.ts b/client/src/helpers/point3dHelper.ts
new file mode 100644
index 0000000..6e7dd95
--- /dev/null
+++ b/client/src/helpers/point3dHelper.ts
@@ -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 = {};
+
+ 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);
+ }
+}
\ No newline at end of file
diff --git a/client/src/helpers/sceneHelper.ts b/client/src/helpers/sceneHelper.ts
new file mode 100644
index 0000000..6dc1eaa
--- /dev/null
+++ b/client/src/helpers/sceneHelper.ts
@@ -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[] {
+
+ 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();
+ }
+}
diff --git a/client/src/layers/geometryCache.ts b/client/src/layers/geometryCache.ts
index 82df0d3..fc38ce7 100644
--- a/client/src/layers/geometryCache.ts
+++ b/client/src/layers/geometryCache.ts
@@ -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();
diff --git a/client/src/layers/sceneSync.ts b/client/src/layers/sceneSync.ts
index a45e8be..749bd9e 100644
--- a/client/src/layers/sceneSync.ts
+++ b/client/src/layers/sceneSync.ts
@@ -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;
+ private meshByFace: Record = {}; // 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 {
+ 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();
}
diff --git a/client/src/model/model.ts b/client/src/model/model.ts
new file mode 100644
index 0000000..2023f5a
--- /dev/null
+++ b/client/src/model/model.ts
@@ -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 = {};
+ public halfEdges: Record = {};
+ public edges: Record = {};
+ public loops: Record = {};
+ public faces: Record = {};
+ public surfaces: Record = {};
+ public solids: Record = {};
+
+ public get primitives(): Record {
+ 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();
+ 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();
diff --git a/client/src/state/root.ts b/client/src/state/root.ts
index a68c918..cc8697d 100644
--- a/client/src/state/root.ts
+++ b/client/src/state/root.ts
@@ -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();
diff --git a/client/src/types/brepdto.ts b/client/src/types/brepdto.ts
deleted file mode 100644
index 073954e..0000000
--- a/client/src/types/brepdto.ts
+++ /dev/null
@@ -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'][],
-}
diff --git a/server/package.json b/server/package.json
new file mode 100644
index 0000000..56c4b9a
--- /dev/null
+++ b/server/package.json
@@ -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"
+}