parent
e751d2c5dd
commit
5de79305b7
|
|
@ -10,6 +10,8 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/earcut": "^3.0.0",
|
"@types/earcut": "^3.0.0",
|
||||||
"earcut": "^3.0.2",
|
"earcut": "^3.0.2",
|
||||||
|
"js-yaml": "^4.1.1",
|
||||||
|
"json5": "^2.2.3",
|
||||||
"mobx": "^6.15.3",
|
"mobx": "^6.15.3",
|
||||||
"mobx-react-lite": "^4.1.1",
|
"mobx-react-lite": "^4.1.1",
|
||||||
"react": "^19.2.6",
|
"react": "^19.2.6",
|
||||||
|
|
@ -18,7 +20,8 @@
|
||||||
"three": "^0.184.0",
|
"three": "^0.184.0",
|
||||||
"three-mesh-bvh": "^0.9.10",
|
"three-mesh-bvh": "^0.9.10",
|
||||||
"uuid": "^14.0.0",
|
"uuid": "^14.0.0",
|
||||||
"verb-nurbs": "^3.0.3"
|
"verb-nurbs": "^3.0.3",
|
||||||
|
"yaml": "^2.9.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
|
|
@ -1551,6 +1554,12 @@
|
||||||
"url": "https://github.com/sponsors/epoberezkin"
|
"url": "https://github.com/sponsors/epoberezkin"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/argparse": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||||
|
"license": "Python-2.0"
|
||||||
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "4.0.4",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||||
|
|
@ -2185,6 +2194,18 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/js-yaml": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"argparse": "^2.0.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"js-yaml": "bin/js-yaml.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jsesc": {
|
"node_modules/jsesc": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
||||||
|
|
@ -2223,7 +2244,6 @@
|
||||||
"version": "2.2.3",
|
"version": "2.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||||
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"json5": "lib/cli.js"
|
"json5": "lib/cli.js"
|
||||||
|
|
@ -3617,6 +3637,22 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/yaml": {
|
||||||
|
"version": "2.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
|
||||||
|
"integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"peer": true,
|
||||||
|
"bin": {
|
||||||
|
"yaml": "bin.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/eemeli"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yocto-queue": {
|
"node_modules/yocto-queue": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/earcut": "^3.0.0",
|
"@types/earcut": "^3.0.0",
|
||||||
"earcut": "^3.0.2",
|
"earcut": "^3.0.2",
|
||||||
|
"js-yaml": "^4.1.1",
|
||||||
|
"json5": "^2.2.3",
|
||||||
"mobx": "^6.15.3",
|
"mobx": "^6.15.3",
|
||||||
"mobx-react-lite": "^4.1.1",
|
"mobx-react-lite": "^4.1.1",
|
||||||
"react": "^19.2.6",
|
"react": "^19.2.6",
|
||||||
|
|
@ -20,7 +22,8 @@
|
||||||
"three": "^0.184.0",
|
"three": "^0.184.0",
|
||||||
"three-mesh-bvh": "^0.9.10",
|
"three-mesh-bvh": "^0.9.10",
|
||||||
"uuid": "^14.0.0",
|
"uuid": "^14.0.0",
|
||||||
"verb-nurbs": "^3.0.3"
|
"verb-nurbs": "^3.0.3",
|
||||||
|
"yaml": "^2.9.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@
|
||||||
left: 000px;
|
left: 000px;
|
||||||
width: 600px;
|
width: 600px;
|
||||||
font-size: 75%;
|
font-size: 75%;
|
||||||
|
pointer-events: none;
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
#blob-view {
|
#blob-view {
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,10 @@ export const App = function () {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Viewport />
|
<Viewport />
|
||||||
<HitTestView />
|
<div className="side-panel">
|
||||||
<DbView />
|
<HitTestView />
|
||||||
|
<DbView />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import type { Mesh, Solid, Surface } from "../../types";
|
import type { Face, Mesh, Solid, Surface, Vertex } from "../../types";
|
||||||
|
|
||||||
export type MeshDto = {
|
export type MeshDto = {
|
||||||
vertices: Float32Array;
|
vertices: Float32Array;
|
||||||
normals: Float32Array
|
normals: Float32Array
|
||||||
indices: Uint16Array;
|
indices: Uint16Array;
|
||||||
|
|
||||||
faceId: Solid['id'];
|
vertexIds: Vertex['id'][];
|
||||||
|
faceId: Face['id'];
|
||||||
surfaceId: Surface['id'];
|
surfaceId: Surface['id'];
|
||||||
solidId: Solid['id'];
|
solidId: Solid['id'];
|
||||||
};
|
};
|
||||||
|
|
@ -15,6 +16,7 @@ export function meshToDto(mesh: Mesh): MeshDto {
|
||||||
vertices: new Float32Array(mesh.vertices.flat()),
|
vertices: new Float32Array(mesh.vertices.flat()),
|
||||||
normals: new Float32Array(mesh.normals.flat()),
|
normals: new Float32Array(mesh.normals.flat()),
|
||||||
indices: new Uint16Array(mesh.indices.flat()),
|
indices: new Uint16Array(mesh.indices.flat()),
|
||||||
|
vertexIds: mesh.vertexIds,
|
||||||
faceId: mesh.faceId,
|
faceId: mesh.faceId,
|
||||||
surfaceId: mesh.surfaceId,
|
surfaceId: mesh.surfaceId,
|
||||||
solidId: mesh.solidId,
|
solidId: mesh.solidId,
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ export class PlaneTessellator {
|
||||||
vertices: vertices3d.map((v) => [v.x, v.y, v.z]),
|
vertices: vertices3d.map((v) => [v.x, v.y, v.z]),
|
||||||
normals: vertices3d.map(() => basis.normal),
|
normals: vertices3d.map(() => basis.normal),
|
||||||
indices,
|
indices,
|
||||||
|
vertexIds: vertices3d.map((v) => v.id),
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
// import type { V3 } from "../types";
|
|
||||||
// import type { Solid } from "../types/brep";
|
|
||||||
// import { db } from "./db";
|
|
||||||
|
|
||||||
// export type Mesh {
|
|
||||||
// solidId: Solid['id'],
|
|
||||||
// positions: V3[],
|
|
||||||
// normals: new Float32Array(normals),
|
|
||||||
// indices: new Uint16Array(indices),
|
|
||||||
// faceIds, // parallel to triangle index pairs
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export class MeshService {
|
|
||||||
// public tesselateSolid(solidId: Solid['id']): Mesh {
|
|
||||||
|
|
||||||
// const positions: V3[] = [];
|
|
||||||
// const normals: V3[] = [];
|
|
||||||
// const indices: number[] = [];
|
|
||||||
// const faceIds: string[] = []; // per-triangle face ID for picking
|
|
||||||
|
|
||||||
// const solid = db.solids.find(s => s.id === solidId)!;
|
|
||||||
// const surface = db.surfaces.find(s => s.id === solid.outerSurface[0])!;
|
|
||||||
// const faces = db.faces.filter(f => surface.faces.includes(f.id));
|
|
||||||
|
|
||||||
// for (const face of faces) {
|
|
||||||
// const loop = db.loops.find(l => l.id === face.outerLoop)!;
|
|
||||||
// let startHalfEdgeId = loop.start;
|
|
||||||
|
|
||||||
// let halfEdgeId = startHalfEdgeId;
|
|
||||||
// for (let index = 0; ; index++) {
|
|
||||||
// const halfEdge = db.halfEdges.find(h => h.id === halfEdgeId)!;
|
|
||||||
|
|
||||||
// const
|
|
||||||
|
|
||||||
// if (!halfEdge.next || halfEdge.next === startHalfEdgeId)
|
|
||||||
// break;
|
|
||||||
// halfEdgeId = halfEdge.next;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Face vertex winding for each of 6 faces (two triangles per quad face)
|
|
||||||
// const faceQuads: Record<string, string[]> = {
|
|
||||||
// '0': ['v0', 'v3', 'v2', 'v1'], // -Z (flip winding for correct normal)
|
|
||||||
// '1': ['v4', 'v5', 'v6', 'v7'], // +Z
|
|
||||||
// '2': ['v0', 'v4', 'v7', 'v3'], // -X
|
|
||||||
// '3': ['v1', 'v2', 'v6', 'v5'], // +X
|
|
||||||
// '4': ['v0', 'v1', 'v5', 'v4'], // -Y
|
|
||||||
// '5': ['v3', 'v7', 'v6', 'v2'], // +Y
|
|
||||||
// };
|
|
||||||
|
|
||||||
// let base = 0;
|
|
||||||
// for (let faceIdx = 0; faceIdx < faces.length; faceIdx++) {
|
|
||||||
// const face = faces[faceIdx];
|
|
||||||
// const [a, b, c, d] = faceQuads[faceIdx.toString()].map(vid => verts[vid].pos);
|
|
||||||
// const n = face.normal;
|
|
||||||
// // 4 verts per face (no sharing — flat normals)
|
|
||||||
// positions.push(...a, ...b, ...c, ...d);
|
|
||||||
// normals.push(...n, ...n, ...n, ...n);
|
|
||||||
// // two triangles: 0-1-2 and 0-2-3
|
|
||||||
// indices.push(base, base + 1, base + 2, base, base + 2, base + 3);
|
|
||||||
// faceIds.push(face.id, face.id);
|
|
||||||
// base += 4;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return {
|
|
||||||
// positions,
|
|
||||||
// normals,
|
|
||||||
// indices,
|
|
||||||
// faceIds, // parallel to triangle index pairs
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
@ -1,155 +0,0 @@
|
||||||
// import verb from "verb-nurbs";
|
|
||||||
|
|
||||||
// import type { V3 } from "../types";
|
|
||||||
// import type { Loop, Solid, Surface } from "../types/brep";
|
|
||||||
// import { db } from "./db";
|
|
||||||
|
|
||||||
// const verbAny = verb as any;
|
|
||||||
|
|
||||||
|
|
||||||
// export class MeshService {
|
|
||||||
// public tessellateSolid(solidId: Solid['id']): Mesh {
|
|
||||||
// const solid = db.solidById(solidId)!;
|
|
||||||
|
|
||||||
// return {
|
|
||||||
// ...this.tessellateSurface(solid.outerSurface[0]),
|
|
||||||
// solidId,
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// public tessellateSurface(surfaceId: Surface['id']): Omit<Mesh, 'solidId'> {
|
|
||||||
// const surface = db.surfaceById(surfaceId)!;
|
|
||||||
|
|
||||||
// const vertices: V3[] = [];
|
|
||||||
// const normals: V3[] = [];
|
|
||||||
// const indices: number[] = [];
|
|
||||||
|
|
||||||
// for (const faceId of surface.faces) {
|
|
||||||
// const face = db.faceById(faceId)!;
|
|
||||||
// const loop = db.loopById(face.outerLoop)!;
|
|
||||||
|
|
||||||
// console.dir(loop);
|
|
||||||
|
|
||||||
// const boundary = this.getLoopVertices(loop);
|
|
||||||
// if (boundary.length < 3)
|
|
||||||
// throw new Error(`Face ${faceId} has boundary with unsupported vertex count: ${boundary.length}`);
|
|
||||||
|
|
||||||
// console.dir(boundary);
|
|
||||||
|
|
||||||
// const mesh = this.tessellateBoundary(boundary);
|
|
||||||
// const baseIndex = vertices.length;
|
|
||||||
|
|
||||||
// for (const point of mesh.points)
|
|
||||||
// vertices.push([point[0], point[1], point[2]]);
|
|
||||||
|
|
||||||
// for (const normal of mesh.normals)
|
|
||||||
// normals.push([normal[0], normal[1], normal[2]]);
|
|
||||||
|
|
||||||
// for (const face of mesh.faces)
|
|
||||||
// indices.push(baseIndex + face[0], baseIndex + face[1], baseIndex + face[2]);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return {
|
|
||||||
// vertices,
|
|
||||||
// normals,
|
|
||||||
// indices,
|
|
||||||
// surfaceId,
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|
||||||
// private getLoopVertices(loop: Loop): V3[] {
|
|
||||||
// const vertices: V3[] = [];
|
|
||||||
|
|
||||||
// let halfEdgeId = loop.start;
|
|
||||||
// while (halfEdgeId) {
|
|
||||||
// const halfEdge = db.halfEdgeById(halfEdgeId)!;
|
|
||||||
|
|
||||||
// const vertex = db.vertexById(halfEdge.origin)!;
|
|
||||||
// vertices.push([vertex.x, vertex.y, vertex.z]);
|
|
||||||
|
|
||||||
// halfEdgeId = halfEdge.next;
|
|
||||||
// if (halfEdgeId === loop.start)
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return vertices;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// private tessellateBoundary(boundary: V3[]): { points: number[][]; normals: number[][]; faces: number[][] } {
|
|
||||||
// if (boundary.length === 4) {
|
|
||||||
// return this.tessellateQuadBoundary(boundary);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return this.tessellatePlanarPolygon(boundary);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// private tessellateQuadBoundary(boundary: V3[]): { points: number[][]; normals: number[][]; faces: number[][] } {
|
|
||||||
// const surfaceData = this.buildQuadSurfaceData(boundary);
|
|
||||||
// const node = new verbAny.core.AdaptiveRefinementNode(surfaceData);
|
|
||||||
// node.divide({ minDepth: 1 });
|
|
||||||
// return node.triangulate();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// private buildQuadSurfaceData(boundary: V3[]): any {
|
|
||||||
// const [p0, p1, p2, p3] = boundary;
|
|
||||||
// const controlPoints = [
|
|
||||||
// [p0, p3],
|
|
||||||
// [p1, p2],
|
|
||||||
// ];
|
|
||||||
// const weights = [
|
|
||||||
// [1, 1],
|
|
||||||
// [1, 1],
|
|
||||||
// ];
|
|
||||||
|
|
||||||
// return {
|
|
||||||
// knotsU: [0, 0, 1, 1],
|
|
||||||
// knotsV: [0, 0, 1, 1],
|
|
||||||
// controlPoints: verbAny.eval.Eval.homogenize2d(controlPoints, weights),
|
|
||||||
// degreeU: 1,
|
|
||||||
// degreeV: 1,
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|
||||||
// private tessellatePlanarPolygon(boundary: V3[]): { points: number[][]; normals: number[][]; faces: number[][] } {
|
|
||||||
// const points = boundary.map((pt) => [pt[0], pt[1], pt[2]]);
|
|
||||||
// const normal = this.computePolygonNormal(points);
|
|
||||||
// const faces: number[][] = [];
|
|
||||||
|
|
||||||
// for (let i = 1; i < points.length - 1; i += 1) {
|
|
||||||
// faces.push([0, i, i + 1]);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return {
|
|
||||||
// points,
|
|
||||||
// normals: points.map(() => normal),
|
|
||||||
// faces,
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|
||||||
// private computePolygonNormal(points: number[][]): number[] {
|
|
||||||
// if (points.length < 3) {
|
|
||||||
// return [0, 0, 0];
|
|
||||||
// }
|
|
||||||
|
|
||||||
// let nx = 0;
|
|
||||||
// let ny = 0;
|
|
||||||
// let nz = 0;
|
|
||||||
|
|
||||||
// for (let i = 0; i < points.length; i += 1) {
|
|
||||||
// const [x1, y1, z1] = points[i];
|
|
||||||
// const [x2, y2, z2] = points[(i + 1) % points.length];
|
|
||||||
// nx += (y1 - y2) * (z1 + z2);
|
|
||||||
// ny += (z1 - z2) * (x1 + x2);
|
|
||||||
// nz += (x1 - x2) * (y1 + y2);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const length = Math.hypot(nx, ny, nz);
|
|
||||||
// if (length === 0) {
|
|
||||||
// return [0, 0, 1];
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return [nx / length, ny / length, nz / length];
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export const meshService = new MeshService();
|
|
||||||
|
|
@ -1,164 +0,0 @@
|
||||||
// import type { Vertex, HalfEdge, Edge, Loop, Face } from "../types/brep";
|
|
||||||
// import type { DbBlob } from "./db";
|
|
||||||
|
|
||||||
// export function generateEmptyBlob(): DbBlob {
|
|
||||||
// return {
|
|
||||||
// vertices: [],
|
|
||||||
// halfEdges: [],
|
|
||||||
// edges: [],
|
|
||||||
// loops: [],
|
|
||||||
// faces: [],
|
|
||||||
// surfaces: [],
|
|
||||||
// solids: [],
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export function generateCubeBlob(pos: { x: number; y: number; z: number; }, size: number, nextId: () => string): DbBlob {
|
|
||||||
|
|
||||||
// function makeVertex(x: number, y: number, z: number): Vertex {
|
|
||||||
// return {
|
|
||||||
// id: nextId(),
|
|
||||||
// x: pos.x + x,
|
|
||||||
// y: pos.y + y,
|
|
||||||
// z: pos.z + z,
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const vertices = [
|
|
||||||
// makeVertex(0, 0, 0),
|
|
||||||
// makeVertex(size, 0, 0),
|
|
||||||
// makeVertex(size, size, 0),
|
|
||||||
// makeVertex(0, size, 0),
|
|
||||||
// makeVertex(0, 0, size),
|
|
||||||
// makeVertex(size, 0, size),
|
|
||||||
// makeVertex(size, size, size),
|
|
||||||
// makeVertex(0, size, size),
|
|
||||||
// ];
|
|
||||||
|
|
||||||
// const halfEdges: HalfEdge[] = [];
|
|
||||||
// const edges: Edge[] = [];
|
|
||||||
// const loops: Loop[] = [];
|
|
||||||
// const faces: Face[] = [];
|
|
||||||
|
|
||||||
// const faceVertexIndices = [
|
|
||||||
// [0, 1, 2, 3], // bottom
|
|
||||||
// [4, 7, 6, 5], // top
|
|
||||||
// [0, 4, 5, 1], // front
|
|
||||||
// [1, 5, 6, 2], // right
|
|
||||||
// [2, 6, 7, 3], // back
|
|
||||||
// [3, 7, 4, 0], // left
|
|
||||||
// ];
|
|
||||||
|
|
||||||
// const edgeMap = new Map<string, { edgeId: string; halfEdgeIds: string[]; }>();
|
|
||||||
// const vertexOwnerHalfEdge = new Map<string, string>();
|
|
||||||
// const halfEdgeById = new Map<string, HalfEdge>();
|
|
||||||
|
|
||||||
// for (let faceIndex = 0; faceIndex < 1 /* faceVertexIndices.length */; faceIndex++) {
|
|
||||||
// const vertexIndices = faceVertexIndices[faceIndex];
|
|
||||||
// const loopId = nextId();
|
|
||||||
// const faceId = nextId();
|
|
||||||
// const faceHalfEdgeIds: string[] = [];
|
|
||||||
|
|
||||||
// for (const vertexIndex of vertexIndices) {
|
|
||||||
// const halfEdgeId = nextId();
|
|
||||||
// const origin = vertices[vertexIndex].id;
|
|
||||||
// const halfEdge: HalfEdge = {
|
|
||||||
// id: halfEdgeId,
|
|
||||||
// origin,
|
|
||||||
// twin: '',
|
|
||||||
// ownerEdge: '',
|
|
||||||
// };
|
|
||||||
|
|
||||||
// halfEdges.push(halfEdge);
|
|
||||||
// halfEdgeById.set(halfEdgeId, halfEdge);
|
|
||||||
// faceHalfEdgeIds.push(halfEdgeId);
|
|
||||||
|
|
||||||
// if (!vertexOwnerHalfEdge.has(origin)) {
|
|
||||||
// vertexOwnerHalfEdge.set(origin, halfEdgeId);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// for (let index = 0; index < vertexIndices.length; index++) {
|
|
||||||
// const currentVertexIndex = vertexIndices[index];
|
|
||||||
// const nextVertexIndex = vertexIndices[(index + 1) % vertexIndices.length];
|
|
||||||
// const currentHalfEdgeId = faceHalfEdgeIds[index];
|
|
||||||
// const currentHalfEdge = halfEdgeById.get(currentHalfEdgeId)!;
|
|
||||||
// const edgeKey = `${Math.min(currentVertexIndex, nextVertexIndex)}-${Math.max(currentVertexIndex, nextVertexIndex)}`;
|
|
||||||
|
|
||||||
// const existing = edgeMap.get(edgeKey);
|
|
||||||
// if (existing) {
|
|
||||||
// existing.halfEdgeIds.push(currentHalfEdgeId);
|
|
||||||
// const edge = edges.find((item) => item.id === existing.edgeId)!;
|
|
||||||
// edge.b = currentHalfEdgeId;
|
|
||||||
// currentHalfEdge.ownerEdge = existing.edgeId;
|
|
||||||
// } else {
|
|
||||||
// const edgeId = nextId();
|
|
||||||
// edges.push({
|
|
||||||
// id: edgeId,
|
|
||||||
// a: currentHalfEdgeId,
|
|
||||||
// b: '',
|
|
||||||
// ownerLoop: loopId,
|
|
||||||
// });
|
|
||||||
// edgeMap.set(edgeKey, { edgeId, halfEdgeIds: [currentHalfEdgeId] });
|
|
||||||
// currentHalfEdge.ownerEdge = edgeId;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// currentHalfEdge.next = faceHalfEdgeIds[(index + 1) % faceHalfEdgeIds.length];
|
|
||||||
// currentHalfEdge.prev = faceHalfEdgeIds[(index + faceHalfEdgeIds.length - 1) % faceHalfEdgeIds.length];
|
|
||||||
// }
|
|
||||||
|
|
||||||
// loops.push({
|
|
||||||
// id: loopId,
|
|
||||||
// start: faceHalfEdgeIds[0],
|
|
||||||
// ownerFace: faceId,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// faces.push({
|
|
||||||
// id: faceId,
|
|
||||||
// outerLoop: loopId,
|
|
||||||
// holes: [],
|
|
||||||
// ownerSurface: '',
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// for (const { halfEdgeIds } of edgeMap.values()) {
|
|
||||||
// if (halfEdgeIds.length !== 2) {
|
|
||||||
// throw new Error('Cube generation failed: expected exactly two halfedges per edge.');
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const [firstHalfEdgeId, secondHalfEdgeId] = halfEdgeIds;
|
|
||||||
// const firstHalfEdge = halfEdgeById.get(firstHalfEdgeId)!;
|
|
||||||
// const secondHalfEdge = halfEdgeById.get(secondHalfEdgeId)!;
|
|
||||||
// firstHalfEdge.twin = secondHalfEdgeId;
|
|
||||||
// secondHalfEdge.twin = firstHalfEdgeId;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const verticesWithOwner = vertices.map((vertex) => ({
|
|
||||||
// ...vertex,
|
|
||||||
// ownerHalfEdge: vertexOwnerHalfEdge.get(vertex.id),
|
|
||||||
// }));
|
|
||||||
|
|
||||||
// const surfaceId = nextId();
|
|
||||||
// const solidId = nextId();
|
|
||||||
|
|
||||||
// faces.forEach(face => face.ownerSurface = surfaceId);
|
|
||||||
|
|
||||||
// const surface = {
|
|
||||||
// id: surfaceId,
|
|
||||||
// faces: faces.map(f => f.id),
|
|
||||||
// };
|
|
||||||
|
|
||||||
// return {
|
|
||||||
// vertices: verticesWithOwner,
|
|
||||||
// halfEdges,
|
|
||||||
// edges,
|
|
||||||
// loops,
|
|
||||||
// faces,
|
|
||||||
// surfaces: [surface],
|
|
||||||
// solids: [{
|
|
||||||
// id: solidId,
|
|
||||||
// outerSurface: [surfaceId],
|
|
||||||
// }],
|
|
||||||
// };
|
|
||||||
|
|
||||||
// }
|
|
||||||
|
|
@ -1,16 +1,29 @@
|
||||||
|
import * as yaml from 'yaml';
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { state } from "../state/root";
|
import { state } from "../state/root";
|
||||||
|
|
||||||
export const HitTestView = observer(function () {
|
export const HitTestView = observer(function () {
|
||||||
|
|
||||||
|
const left = state.mousePosition.x;
|
||||||
|
const top = state.mousePosition.y;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="hit-test">
|
<div id="hit-test" style={{ top, left }}>
|
||||||
<pre>
|
<pre style={{ textWrap: 'wrap' }}>
|
||||||
{
|
{
|
||||||
state.hitTest.hits.map((hit) =>
|
`${top},${left}`
|
||||||
|
}
|
||||||
|
{
|
||||||
|
state.hitResults.hits.map((hit) =>
|
||||||
<div key={hit.object.uuid}>
|
<div key={hit.object.uuid}>
|
||||||
{JSON.stringify(hit.point.toArray())}
|
<div>{yaml.stringify(
|
||||||
{JSON.stringify(hit.object.userData)}
|
{
|
||||||
|
hit: { ...hit, object: undefined, triangle: undefined },
|
||||||
|
userData: hit.object.userData,
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
2,
|
||||||
|
)}</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ export type ThreeViewTickEventArgs = ThreeViewEventArgs & {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ThreeViewMouseEventArgs = ThreeViewEventArgs & {
|
export type ThreeViewMouseEventArgs = ThreeViewEventArgs & {
|
||||||
|
screenPosition: THREE.Vector2Like,
|
||||||
|
position: THREE.Vector2Like,
|
||||||
hitResults: HitResults,
|
hitResults: HitResults,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -107,6 +109,8 @@ export const ThreeView = function (props: ThreeViewProps) {
|
||||||
e.screenSize,
|
e.screenSize,
|
||||||
);
|
);
|
||||||
props.onMouseMove?.({
|
props.onMouseMove?.({
|
||||||
|
position: e.position,
|
||||||
|
screenPosition: e.screenPosition,
|
||||||
camera,
|
camera,
|
||||||
scene,
|
scene,
|
||||||
hitResults,
|
hitResults,
|
||||||
|
|
@ -119,6 +123,8 @@ export const ThreeView = function (props: ThreeViewProps) {
|
||||||
e.screenSize,
|
e.screenSize,
|
||||||
);
|
);
|
||||||
props.onClick?.({
|
props.onClick?.({
|
||||||
|
position: e.position,
|
||||||
|
screenPosition: e.screenPosition,
|
||||||
camera,
|
camera,
|
||||||
scene,
|
scene,
|
||||||
hitResults,
|
hitResults,
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ export const Viewport = function () {
|
||||||
const sceneHelper = useSceneHelper();
|
const sceneHelper = useSceneHelper();
|
||||||
|
|
||||||
function handleMouseMove(e: ThreeViewMouseEventArgs) {
|
function handleMouseMove(e: ThreeViewMouseEventArgs) {
|
||||||
state.setHitTest(e.hitResults);
|
state.setHitTest(e.screenPosition, e.hitResults);
|
||||||
|
|
||||||
sceneHelper.clear();
|
sceneHelper.clear();
|
||||||
if (e.hitResults.hits.length) {
|
if (e.hitResults.hits.length) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { Plane, Ray, Vector3, type Triangle, type Vector2Like, type Vector3Like } from "three";
|
||||||
|
|
||||||
|
export function dot2(a: Vector2Like, b: Vector2Like) { return a.x * b.x + a.y * b.y; }
|
||||||
|
export function sub2(a: Vector2Like, b: Vector2Like): Vector2Like { return { x: a.x - b.x, y: a.y - b.y }; }
|
||||||
|
export function len2(a: Vector2Like) { return Math.sqrt(dot2(a, a)); }
|
||||||
|
|
||||||
|
/** Closest point on 2D segment [a,b] to point p */
|
||||||
|
function closestPointOnSegment2D(p: Vector2Like, a: Vector2Like, b: Vector2Like): Vector2Like {
|
||||||
|
const ab = sub2(b, a);
|
||||||
|
const t = Math.max(0, Math.min(1, dot2(sub2(p, a), ab) / dot2(ab, ab)));
|
||||||
|
return { x: a.x + ab.x * t, y: a.y + ab.y * t };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Signed area — positive if a,b,c are CCW */
|
||||||
|
export function signedArea2(a: Vector2Like, b: Vector2Like, c: Vector2Like) {
|
||||||
|
return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pointInTriangle2D(p: Vector2Like, a: Vector2Like, b: Vector2Like, c: Vector2Like): boolean {
|
||||||
|
const d1 = signedArea2(p, a, b);
|
||||||
|
const d2 = signedArea2(p, b, c);
|
||||||
|
const d3 = signedArea2(p, c, a);
|
||||||
|
const hasNeg = d1 < 0 || d2 < 0 || d3 < 0;
|
||||||
|
const hasPos = d1 > 0 || d2 > 0 || d3 > 0;
|
||||||
|
return !(hasNeg && hasPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Minimum distance from point p to triangle (a,b,c) in 2D.
|
||||||
|
* Returns 0 if p is inside. */
|
||||||
|
export function distPointToTriangle2D(p: Vector2Like, a: Vector2Like, b: Vector2Like, c: Vector2Like): number {
|
||||||
|
if (pointInTriangle2D(p, a, b, c))
|
||||||
|
return 0;
|
||||||
|
const d1 = len2(sub2(p, closestPointOnSegment2D(p, a, b)));
|
||||||
|
const d2 = len2(sub2(p, closestPointOnSegment2D(p, b, c)));
|
||||||
|
const d3 = len2(sub2(p, closestPointOnSegment2D(p, c, a)));
|
||||||
|
return Math.min(d1, d2, d3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Plane utilities ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export type TrianglePlane = {
|
||||||
|
plane: Plane;
|
||||||
|
/** Two orthonormal vectors spanning the plane */
|
||||||
|
u: Vector3Like;
|
||||||
|
v: Vector3Like;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Build a local 2D coordinate frame for the triangle's plane */
|
||||||
|
export function buildTrianglePlane(tri: Triangle): TrianglePlane {
|
||||||
|
const normal = new Vector3();
|
||||||
|
tri.getNormal(normal);
|
||||||
|
const plane = new Plane().setFromNormalAndCoplanarPoint(normal, tri.a);
|
||||||
|
|
||||||
|
const u = tri.b.clone().sub(tri.a).normalize();
|
||||||
|
const v = normal.clone().cross(u); // already normalized since normal⊥u, both unit
|
||||||
|
|
||||||
|
return { plane, u, v };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Project a 3D point onto the plane's 2D coordinate frame */
|
||||||
|
export function projectOntoPlane(point: Vector3, origin: Vector3Like, frame: TrianglePlane): Vector2Like {
|
||||||
|
const d = point.clone().sub(origin);
|
||||||
|
return { x: d.dot(frame.u), y: d.dot(frame.v) };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Main intersection ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intersect the local-space frustum axis ray with the triangle plane.
|
||||||
|
* Returns the hit point and depth, or null if ray is parallel to the plane.
|
||||||
|
*/
|
||||||
|
export function rayPlaneHit(
|
||||||
|
ray: Ray,
|
||||||
|
plane: Plane,
|
||||||
|
): { point: Vector3Like; depth: number } | null {
|
||||||
|
const denom = plane.normal.dot(ray.direction);
|
||||||
|
if (Math.abs(denom) < 1e-10) return null;
|
||||||
|
const t = -(plane.distanceToPoint(ray.origin)) / denom;
|
||||||
|
if (t < 0) return null; // behind apex
|
||||||
|
return {
|
||||||
|
point: ray.origin.clone().addScaledVector(ray.direction, t),
|
||||||
|
depth: t,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,16 +1,35 @@
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import { CONTAINED, ExtendedTriangle, INTERSECTED, NOT_INTERSECTED } from 'three-mesh-bvh';
|
import { CONTAINED, ExtendedTriangle, INTERSECTED, NOT_INTERSECTED } from 'three-mesh-bvh';
|
||||||
import { CircularFrustum } from './circularFrustum';
|
import { CircularFrustum } from './circularFrustum';
|
||||||
|
import type { Id } from '../types';
|
||||||
|
|
||||||
export type HitResults = {
|
export type HitResults = {
|
||||||
hits: HitResult[];
|
hits: HitResult[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TriangleVertexHitDetail = {
|
||||||
|
kind: 'vertex',
|
||||||
|
index: 0 | 1 | 2,
|
||||||
|
id: Id,
|
||||||
|
}
|
||||||
|
export type TriangleEdgeHitDetail = {
|
||||||
|
kind: 'edge',
|
||||||
|
index: 0 | 1 | 2, // edge 0=AB, 1=BC, 2=CA
|
||||||
|
id: Id,
|
||||||
|
}
|
||||||
|
export type TriangleFaceHitDetail = {
|
||||||
|
kind: 'face',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TriangleHitDetail = TriangleVertexHitDetail | TriangleEdgeHitDetail | TriangleFaceHitDetail;
|
||||||
|
|
||||||
export type HitResult = {
|
export type HitResult = {
|
||||||
object: THREE.Object3D;
|
object: THREE.Object3D;
|
||||||
point: THREE.Vector3; // world-space closest hit point
|
point: THREE.Vector3; // world-space closest hit point
|
||||||
depth: number; // depth along frustum axis
|
depth: number; // depth along frustum axis
|
||||||
triangle?: ExtendedTriangle; // only present when BVH was used
|
triangle: { a: THREE.Vector3, b: THREE.Vector3, c: THREE.Vector3 };
|
||||||
|
triHit?: TriangleHitDetail;
|
||||||
|
vertexIds: [Id, Id, Id] | undefined; // undefined is when geometry does not have .index
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CircularFrustumIntersectionOptions = {
|
export type CircularFrustumIntersectionOptions = {
|
||||||
|
|
@ -31,6 +50,49 @@ export function intersectionResultToBvh(value: IntersectionResult): typeof NOT_I
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const BARYCENTRIC_EPSILON = 1e-1;
|
||||||
|
|
||||||
|
function classifyTriangleHit(
|
||||||
|
point: THREE.Vector3,
|
||||||
|
tri: ExtendedTriangle,
|
||||||
|
vertexIds: Id[],
|
||||||
|
): TriangleHitDetail {
|
||||||
|
// Compute barycentric coords via areas
|
||||||
|
const ab = tri.b.clone().sub(tri.a);
|
||||||
|
const ac = tri.c.clone().sub(tri.a);
|
||||||
|
const ap = point.clone().sub(tri.a);
|
||||||
|
|
||||||
|
const d00 = ab.dot(ab);
|
||||||
|
const d01 = ab.dot(ac);
|
||||||
|
const d11 = ac.dot(ac);
|
||||||
|
const d20 = ap.dot(ab);
|
||||||
|
const d21 = ap.dot(ac);
|
||||||
|
const denom = d00 * d11 - d01 * d01;
|
||||||
|
|
||||||
|
const v = (d11 * d20 - d01 * d21) / denom; // weight of b
|
||||||
|
const w = (d00 * d21 - d01 * d20) / denom; // weight of c
|
||||||
|
const u = 1 - v - w; // weight of a
|
||||||
|
|
||||||
|
const eps = 1 - BARYCENTRIC_EPSILON;
|
||||||
|
const onA = u > eps;
|
||||||
|
const onB = v > eps;
|
||||||
|
const onC = w > eps;
|
||||||
|
|
||||||
|
if (onA) return { kind: 'vertex', index: 0, id: vertexIds[0] };
|
||||||
|
if (onB) return { kind: 'vertex', index: 1, id: vertexIds[1] };
|
||||||
|
if (onC) return { kind: 'vertex', index: 2, id: vertexIds[2] };
|
||||||
|
|
||||||
|
const onAB = w < BARYCENTRIC_EPSILON; // u+v≈1, w≈0
|
||||||
|
const onBC = u < BARYCENTRIC_EPSILON;
|
||||||
|
const onCA = v < BARYCENTRIC_EPSILON;
|
||||||
|
|
||||||
|
if (onAB) return { kind: 'edge', index: 0, id: 'none' };
|
||||||
|
if (onBC) return { kind: 'edge', index: 1, id: 'none' };
|
||||||
|
if (onCA) return { kind: 'edge', index: 2, id: 'none' };
|
||||||
|
|
||||||
|
return { kind: 'face' };
|
||||||
|
}
|
||||||
|
|
||||||
export class CircularFrustumIntersection {
|
export class CircularFrustumIntersection {
|
||||||
public readonly frustum: CircularFrustum;
|
public readonly frustum: CircularFrustum;
|
||||||
|
|
||||||
|
|
@ -135,8 +197,30 @@ export class CircularFrustumIntersection {
|
||||||
if (this.insersectsSphere(boundingSphere) === 'NOT_INTERSECTED')
|
if (this.insersectsSphere(boundingSphere) === 'NOT_INTERSECTED')
|
||||||
return [];
|
return [];
|
||||||
|
|
||||||
|
function getGeometryVertextIdByIndex(vertexIndex: number): Id {
|
||||||
|
return mesh.userData.vertexIds[vertexIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGeometryVertextIds(triIndex: number): HitResult['vertexIds'] {
|
||||||
|
return geometry.index
|
||||||
|
? [
|
||||||
|
getGeometryVertextIdByIndex(geometry.index.array[triIndex * 3]),
|
||||||
|
getGeometryVertextIdByIndex(geometry.index.array[triIndex * 3 + 1]),
|
||||||
|
getGeometryVertextIdByIndex(geometry.index.array[triIndex * 3 + 2]),
|
||||||
|
]
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const axisRay = new THREE.Ray(localFrustum.apex, localFrustum.axisNormalized);
|
const axisRay = new THREE.Ray(localFrustum.apex, localFrustum.axisNormalized);
|
||||||
|
|
||||||
|
function tryBestPoint(v: THREE.Vector3, best: { depth: number, local?: THREE.Vector3 }) {
|
||||||
|
const d = CircularFrustumIntersection.pointAxialDepth(v, localFrustum);
|
||||||
|
if (d !== 'NOT_INTERSECTED' && d < best.depth) {
|
||||||
|
best.depth = d as number;
|
||||||
|
best.local = v.clone();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const results: HitResult[] = [];
|
const results: HitResult[] = [];
|
||||||
|
|
||||||
if (!geometry.boundsTree)
|
if (!geometry.boundsTree)
|
||||||
|
|
@ -147,29 +231,29 @@ export class CircularFrustumIntersection {
|
||||||
bvh.shapecast({
|
bvh.shapecast({
|
||||||
intersectsBounds: (box: THREE.Box3) => intersectionResultToBvh(CircularFrustumIntersection.intersectsBox(box, localFrustum)),
|
intersectsBounds: (box: THREE.Box3) => intersectionResultToBvh(CircularFrustumIntersection.intersectsBox(box, localFrustum)),
|
||||||
|
|
||||||
intersectsTriangle: (tri: ExtendedTriangle, _index: number, contained: boolean) => {
|
intersectsTriangle: (tri: ExtendedTriangle, triIndex: number, contained: boolean) => {
|
||||||
|
const tiangleVertexIds = getGeometryVertextIds(triIndex);
|
||||||
|
|
||||||
if (contained) {
|
if (contained) {
|
||||||
const worldPoint = tri.a.clone().applyMatrix4(mesh.matrixWorld);
|
const worldPoint = tri.a.clone().applyMatrix4(mesh.matrixWorld);
|
||||||
const depth = worldFrustum.axisNormalized.dot(worldPoint.clone().sub(worldFrustum.apex));
|
const depth = worldFrustum.axisNormalized.dot(worldPoint.clone().sub(worldFrustum.apex));
|
||||||
results.push({ object: mesh, point: worldPoint, depth, triangle: tri });
|
results.push({
|
||||||
|
object: mesh,
|
||||||
|
point: worldPoint,
|
||||||
|
depth,
|
||||||
|
triangle: tri,
|
||||||
|
triHit: classifyTriangleHit(tri.a, tri, tiangleVertexIds ?? []),
|
||||||
|
vertexIds: tiangleVertexIds,
|
||||||
|
});
|
||||||
return !findAll;
|
return !findAll;
|
||||||
}
|
}
|
||||||
|
|
||||||
let bestDepth = Infinity;
|
let bestPoint: { depth: number, local?: THREE.Vector3 } = { depth: Infinity, local: undefined };
|
||||||
let bestLocal: THREE.Vector3 | undefined = undefined;
|
|
||||||
|
|
||||||
const tryPoint = (v: THREE.Vector3) => {
|
|
||||||
const d = CircularFrustumIntersection.pointAxialDepth(v, localFrustum);
|
|
||||||
if (d !== 'NOT_INTERSECTED' && (d as number) < bestDepth) {
|
|
||||||
bestDepth = d as number;
|
|
||||||
bestLocal = v.clone();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// step 1: test vertices
|
// step 1: test vertices
|
||||||
tryPoint(tri.a);
|
tryBestPoint(tri.a, bestPoint);
|
||||||
tryPoint(tri.b);
|
tryBestPoint(tri.b, bestPoint);
|
||||||
tryPoint(tri.c);
|
tryBestPoint(tri.c, bestPoint);
|
||||||
|
|
||||||
// step 2: edges for a triangle that straddle the cone surface
|
// step 2: edges for a triangle that straddle the cone surface
|
||||||
// for each edge, find the point closest to the frustum axis ray, and also the point closest to the apex.
|
// for each edge, find the point closest to the frustum axis ray, and also the point closest to the apex.
|
||||||
|
|
@ -196,30 +280,38 @@ export class CircularFrustumIntersection {
|
||||||
const edgeLen = edge.length();
|
const edgeLen = edge.length();
|
||||||
const tClamped = Math.max(0, Math.min(edgeLen, t));
|
const tClamped = Math.max(0, Math.min(edgeLen, t));
|
||||||
const pointOnEdge = a.clone().addScaledVector(edgeDir, tClamped);
|
const pointOnEdge = a.clone().addScaledVector(edgeDir, tClamped);
|
||||||
tryPoint(pointOnEdge);
|
tryBestPoint(pointOnEdge, bestPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Closest point on edge to the apex itself
|
// Closest point on edge to the apex itself
|
||||||
const tApex = Math.max(0, Math.min(1, -toA.dot(edge) / edge.lengthSq()));
|
const tApex = Math.max(0, Math.min(1, -toA.dot(edge) / edge.lengthSq()));
|
||||||
tryPoint(a.clone().addScaledVector(edge, tApex));
|
tryBestPoint(a.clone().addScaledVector(edge, tApex), bestPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
// step 3: closest point on triangle face to the apex
|
// step 3: closest point on triangle face to the apex
|
||||||
const closestOnFace = new THREE.Vector3();
|
const closestOnFace = new THREE.Vector3();
|
||||||
tri.closestPointToPoint(localFrustum.apex, closestOnFace);
|
tri.closestPointToPoint(localFrustum.apex, closestOnFace);
|
||||||
if (!isNaN(closestOnFace.x))
|
if (!isNaN(closestOnFace.x))
|
||||||
tryPoint(closestOnFace);
|
tryBestPoint(closestOnFace, bestPoint);
|
||||||
|
|
||||||
// 4. Axis ray through the face — catches large faces the cone passes through
|
// step 3: large faces that frustum (its axis) passes through
|
||||||
const faceHit = new THREE.Vector3();
|
const faceHit = new THREE.Vector3();
|
||||||
if (axisRay.intersectTriangle(tri.a, tri.b, tri.c, false, faceHit)) {
|
if (axisRay.intersectTriangle(tri.a, tri.b, tri.c, false, faceHit)) {
|
||||||
tryPoint(faceHit);
|
tryBestPoint(faceHit, bestPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bestLocal !== undefined) {
|
if (bestPoint.local) {
|
||||||
const worldPoint = (bestLocal as THREE.Vector3).clone().applyMatrix4(mesh.matrixWorld);
|
const worldPoint = bestPoint.local.clone().applyMatrix4(mesh.matrixWorld);
|
||||||
const worldDepth = worldFrustum.axisNormalized.dot(worldPoint.clone().sub(worldFrustum.apex));
|
const worldDepth = worldFrustum.axisNormalized.dot(worldPoint.clone().sub(worldFrustum.apex));
|
||||||
results.push({ object: mesh, point: worldPoint, depth: worldDepth, triangle: tri });
|
const { a, b, c } = tri;
|
||||||
|
results.push({
|
||||||
|
object: mesh,
|
||||||
|
point: worldPoint,
|
||||||
|
depth: worldDepth,
|
||||||
|
triangle: { a, b, c },
|
||||||
|
triHit: classifyTriangleHit(bestPoint.local, tri, tiangleVertexIds),
|
||||||
|
vertexIds: tiangleVertexIds,
|
||||||
|
});
|
||||||
return !findAll;
|
return !findAll;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { normalizeScreenPosition } from "../normalizeScreenPosition";
|
||||||
const CLICK_THRESHOLD = 2; // px
|
const CLICK_THRESHOLD = 2; // px
|
||||||
|
|
||||||
export type InteractionMouseEventArgs = {
|
export type InteractionMouseEventArgs = {
|
||||||
|
screenPosition: THREE.Vector2Like,
|
||||||
position: THREE.Vector2Like,
|
position: THREE.Vector2Like,
|
||||||
screenSize: THREE.Vector2Like,
|
screenSize: THREE.Vector2Like,
|
||||||
pixelSize: THREE.Vector2Like,
|
pixelSize: THREE.Vector2Like,
|
||||||
|
|
@ -59,8 +60,14 @@ export function useInteraction(
|
||||||
|
|
||||||
const onMouseUp = (e: MouseEvent) => {
|
const onMouseUp = (e: MouseEvent) => {
|
||||||
isDragging = false;
|
isDragging = false;
|
||||||
if ((e.clientX - startX < CLICK_THRESHOLD) && (e.clientY - startY < CLICK_THRESHOLD))
|
if ((e.clientX - startX < CLICK_THRESHOLD) && (e.clientY - startY < CLICK_THRESHOLD)) {
|
||||||
options.onMouseClick?.(normalizeScreenPosition({ x: e.clientX, y: e.clientY }, target));
|
|
||||||
|
const screenPosition = { x: e.clientX, y: e.clientY };
|
||||||
|
options.onMouseClick?.({
|
||||||
|
...normalizeScreenPosition(screenPosition, target),
|
||||||
|
screenPosition,
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onMouseMove = (e: MouseEvent) => {
|
const onMouseMove = (e: MouseEvent) => {
|
||||||
|
|
@ -80,7 +87,11 @@ export function useInteraction(
|
||||||
};
|
};
|
||||||
|
|
||||||
const onHover = (e: MouseEvent) => {
|
const onHover = (e: MouseEvent) => {
|
||||||
options.onMouseMove?.(normalizeScreenPosition({ x: e.clientX, y: e.clientY }, target));
|
const screenPosition = { x: e.clientX, y: e.clientY };
|
||||||
|
options.onMouseMove?.({
|
||||||
|
...normalizeScreenPosition(screenPosition, target),
|
||||||
|
screenPosition,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onContextMenu = (e: Event) => e.preventDefault();
|
const onContextMenu = (e: Event) => e.preventDefault();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type { ColorRepresentation, Object3D, Object3DEventMap, OrthographicCamera, PerspectiveCamera, Scene, Vector2Like, Vector3, Vector3Like } from "three";
|
import type { ColorRepresentation, Object3D, Object3DEventMap, OrthographicCamera, PerspectiveCamera, Scene, Vector2Like, Vector3Like } from "three";
|
||||||
import { SceneSync } from "../layers/sceneSync";
|
import { SceneSync } from "../layers/sceneSync";
|
||||||
import { GeometryCache } from "../layers/geometryCache";
|
import { GeometryCache } from "../layers/geometryCache";
|
||||||
import type { Id } from "../types";
|
import type { Id } from "../types";
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ export class SceneSync {
|
||||||
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.vertexIds = dto.vertexIds;
|
||||||
mesh.userData.surfaceId = dto.surfaceId;
|
mesh.userData.surfaceId = dto.surfaceId;
|
||||||
mesh.userData.solidId = dto.solidId;
|
mesh.userData.solidId = dto.solidId;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
import { makeAutoObservable } from "mobx";
|
import { makeAutoObservable } from "mobx";
|
||||||
import type { Id } from "../types";
|
import type { Id } from "../types";
|
||||||
import type { HitResults } from "../helpers/circularFrustumIntersect";
|
import type { HitResults } from "../helpers/circularFrustumIntersect";
|
||||||
|
import type { Vector2Like } from "three";
|
||||||
|
|
||||||
export class Root {
|
export class Root {
|
||||||
public selectedPrimitiveIds: Id[] = [];
|
public selectedPrimitiveIds: Id[] = [];
|
||||||
public hitTest: HitResults = { hits: [] };
|
public hitResults: HitResults = { hits: [] };
|
||||||
|
public mousePosition: Vector2Like = { x: 0, y: 0 };
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
|
|
@ -14,8 +16,9 @@ export class Root {
|
||||||
this.selectedPrimitiveIds = value;
|
this.selectedPrimitiveIds = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setHitTest(value: HitResults) {
|
public setHitTest(mousePosition: Vector2Like, hitResults: HitResults) {
|
||||||
this.hitTest = value;
|
this.mousePosition = mousePosition;
|
||||||
|
this.hitResults = hitResults;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Face, Solid, Surface } from "./brep";
|
import type { Face, Solid, Surface, Vertex } from "./brep";
|
||||||
|
|
||||||
export type V2 = [x: number, y: number];
|
export type V2 = [x: number, y: number];
|
||||||
export type V3 = [x: number, y: number, z: number];
|
export type V3 = [x: number, y: number, z: number];
|
||||||
|
|
@ -9,6 +9,7 @@ export type Mesh = {
|
||||||
normals: V3[];
|
normals: V3[];
|
||||||
indices: number[];
|
indices: number[];
|
||||||
|
|
||||||
|
vertexIds: Vertex['id'][];
|
||||||
faceId: Face['id'];
|
faceId: Face['id'];
|
||||||
surfaceId: Surface['id'];
|
surfaceId: Surface['id'];
|
||||||
solidId: Solid['id'];
|
solidId: Solid['id'];
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue