proper vertex and edge hit tests
This commit is contained in:
parent
1753b897f8
commit
c05f0436e3
|
|
@ -11,21 +11,17 @@ export const HitTestView = observer(function ({ float }: { float: boolean }) {
|
||||||
const result = JSON.parse(JSON.stringify(hit)) as HitResult;
|
const result = JSON.parse(JSON.stringify(hit)) as HitResult;
|
||||||
const resultAny = result as any;
|
const resultAny = result as any;
|
||||||
|
|
||||||
delete (resultAny.intersection.object);
|
delete (resultAny.object);
|
||||||
delete (resultAny.intersection.triangle);
|
delete (resultAny.uuid);
|
||||||
delete (resultAny.intersection.details.kind);
|
delete (resultAny.id);
|
||||||
delete (resultAny.intersection.details.index);
|
delete (resultAny.faceId);
|
||||||
delete (resultAny.intersection.triangleVertexIds);
|
delete (resultAny.kind);
|
||||||
delete (resultAny.intersection.details);
|
delete (resultAny.visibility);
|
||||||
|
|
||||||
resultAny.intersection.point = formatPoint(resultAny.intersection.point)
|
resultAny.point = formatPoint(resultAny.point)
|
||||||
resultAny.intersection.depth = Number(Number(resultAny.intersection.depth).toFixed(3));
|
resultAny.depth = Number(Number(resultAny.depth).toFixed(10));
|
||||||
resultAny.intersection.radialDistanceAbsolute = Number(Number(resultAny.intersection.radialDistanceAbsolute).toFixed(3));
|
resultAny.radialDistanceAbsolute = Number(Number(result.radialDistanceAbsolute).toFixed(3));
|
||||||
resultAny.intersection.radialDistance = Number(Number(resultAny.intersection.radialDistance).toFixed(3));
|
resultAny.radialDistance = Number(Number(result.radialDistance).toFixed(3));
|
||||||
|
|
||||||
if (result.kind === 'edge') {
|
|
||||||
delete (resultAny.intersection.triangle);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
@ -41,11 +37,24 @@ export const HitTestView = observer(function ({ float }: { float: boolean }) {
|
||||||
return (
|
return (
|
||||||
<div className="hit-test-info" style={style}>
|
<div className="hit-test-info" style={style}>
|
||||||
{
|
{
|
||||||
state.hitResults.hits.map((hit) =>
|
state.hitResults.hits.map((hit) => {
|
||||||
<div key={hit.faceId + '-' + hit.id} style={{ padding: 2, borderBottom: '1px solid gray'}}>
|
let description = '';
|
||||||
|
switch (hit.kind) {
|
||||||
|
case 'face':
|
||||||
|
description = `face ${hit.faceId}`;
|
||||||
|
break;
|
||||||
|
case 'edge':
|
||||||
|
description = `edge ${hit.id} on face ${hit.faceId}`;
|
||||||
|
break;
|
||||||
|
case 'vertex':
|
||||||
|
description = `vertex ${hit.id} on face ${hit.faceId}`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return <div key={hit.uuid} style={{ padding: 2, borderBottom: '1px solid gray' }}>
|
||||||
|
<div style={{ fontWeight: 'bold' }}>{hit.visibility} {description}</div>
|
||||||
<div>{yaml.stringify(renderHitResult(hit), undefined, 4)}</div>
|
<div>{yaml.stringify(renderHitResult(hit), undefined, 4)}</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ export const Viewport = function () {
|
||||||
sceneHelper.clearHints();
|
sceneHelper.clearHints();
|
||||||
if (e.hitResults.hits.length) {
|
if (e.hitResults.hits.length) {
|
||||||
e.hitResults.hits.forEach((hit) => {
|
e.hitResults.hits.forEach((hit) => {
|
||||||
sceneHelper.showPointHint(hit.id!, hit.intersection.point);
|
sceneHelper.showPointHint(hit.uuid, hit.point, 5, hit.kind === 'vertex' ? 'yellow' : (hit.kind === 'edge' ? 'lime' : 'white'));
|
||||||
})
|
})
|
||||||
// console.log(e.position);
|
// console.log(e.position);
|
||||||
// console.log(e.hitTest.objects.map((o) => o));
|
// console.log(e.hitTest.objects.map((o) => o));
|
||||||
|
|
@ -20,7 +20,7 @@ export const Viewport = function () {
|
||||||
}
|
}
|
||||||
// raycaster.setFromCamera(new THREE.Vector2(e.x, e.y), camera);
|
// raycaster.setFromCamera(new THREE.Vector2(e.x, e.y), camera);
|
||||||
// const hits = raycaster.intersectObjects(sync.meshes);
|
// const hits = raycaster.intersectObjects(sync.meshes);
|
||||||
const hoveredFaceIds = e.hitResults.hits.map((hit) => hit.intersection.object.userData.faceId);
|
const hoveredFaceIds = e.hitResults.hits.map((hit) => hit.object.userData.faceId);
|
||||||
// if (hoveredFaceIds.length)
|
// if (hoveredFaceIds.length)
|
||||||
// console.log(hoveredFaceIds);
|
// console.log(hoveredFaceIds);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,51 +1,25 @@
|
||||||
import yaml from 'yaml';
|
|
||||||
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';
|
import type { Id } from '../types';
|
||||||
import type { MeshDto } from '../backend/dto';
|
import type { MeshDto } from '../backend/dto';
|
||||||
import { clamp } from '../utils/math';
|
import { clamp } from '../utils/math';
|
||||||
|
import { model } from '../model/model';
|
||||||
export type TriangleVertexHitDetail = {
|
|
||||||
kind: 'vertex',
|
|
||||||
index: 0 | 1 | 2,
|
|
||||||
pt: THREE.Vector3,
|
|
||||||
}
|
|
||||||
export type TriangleEdgeHitDetail = {
|
|
||||||
kind: 'edge',
|
|
||||||
index: 0 | 1 | 2, // edge 0=AB, 1=BC, 2=CA
|
|
||||||
ptIndexA: 0 | 1 | 2,
|
|
||||||
ptIndexB: 0 | 1 | 2,
|
|
||||||
ptA: THREE.Vector3,
|
|
||||||
ptB: THREE.Vector3,
|
|
||||||
}
|
|
||||||
export type TriangleFaceHitDetail = {
|
|
||||||
kind: 'face',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TriangleHitDetail = TriangleVertexHitDetail | TriangleEdgeHitDetail | TriangleFaceHitDetail;
|
|
||||||
|
|
||||||
export type Visibility = 'visible' | 'backface'; // | 'occluded'
|
export type Visibility = 'visible' | 'backface'; // | 'occluded'
|
||||||
|
|
||||||
export type Intersection = {
|
export type BaseHitResult = {
|
||||||
|
uuid: string,
|
||||||
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
|
||||||
radialDistanceAbsolute: number, // perpendicular distance from frustum axis
|
radialDistanceAbsolute: number, // perpendicular distance from frustum axis
|
||||||
radialDistance: number, // radialDistanceAbsolute normalized (0..1) to frustum radius at depth
|
radialDistance: number, // radialDistanceAbsolute normalized (0..1) to frustum radius at depth
|
||||||
triangle: { a: THREE.Vector3, b: THREE.Vector3, c: THREE.Vector3 },
|
|
||||||
details: TriangleHitDetail,
|
|
||||||
visibility: Visibility,
|
visibility: Visibility,
|
||||||
triangleVertexIds: [Id, Id, Id],
|
|
||||||
}
|
|
||||||
|
|
||||||
export type BaseHitResult = {
|
|
||||||
intersection: Intersection,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FaceHitResult = BaseHitResult & {
|
export type FaceHitResult = BaseHitResult & {
|
||||||
kind: 'face',
|
kind: 'face',
|
||||||
id?: Id,
|
|
||||||
faceId: Id,
|
faceId: Id,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -68,7 +42,7 @@ export type HitResults = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CircularFrustumIntersectionOptions = {
|
export type CircularFrustumIntersectionOptions = {
|
||||||
findAll?: boolean; // defaults to false
|
// findAll?: boolean; // defaults to false
|
||||||
filter?: (object: THREE.Object3D) => boolean; // defaults to every object
|
filter?: (object: THREE.Object3D) => boolean; // defaults to every object
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,6 +59,13 @@ export function intersectionResultToBvh(value: IntersectionResult): typeof NOT_I
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isTriangleVisible(tri: THREE.Triangle, frustum: CircularFrustum): boolean {
|
||||||
|
const normal = new THREE.Vector3();
|
||||||
|
tri.getNormal(normal);
|
||||||
|
const facingRatio = normal.dot(frustum.axisNormalized);
|
||||||
|
return facingRatio < 0;
|
||||||
|
}
|
||||||
|
|
||||||
function closestPointOnEdgeToRay(
|
function closestPointOnEdgeToRay(
|
||||||
start: THREE.Vector3,
|
start: THREE.Vector3,
|
||||||
end: THREE.Vector3,
|
end: THREE.Vector3,
|
||||||
|
|
@ -106,64 +87,6 @@ function closestPointOnEdgeToRay(
|
||||||
return start.clone().addScaledVector(edgeDir, clamp(t, 0, edgeLen) / edgeLen);
|
return start.clone().addScaledVector(edgeDir, clamp(t, 0, edgeLen) / edgeLen);
|
||||||
}
|
}
|
||||||
|
|
||||||
function triangleDetailsByFrustum(
|
|
||||||
tri: THREE.Triangle,
|
|
||||||
localFrustum: CircularFrustum,
|
|
||||||
): TriangleHitDetail[] {
|
|
||||||
const results: TriangleHitDetail[] = [{ kind: 'face' }];
|
|
||||||
|
|
||||||
const verts: [THREE.Vector3, 0 | 1 | 2][] = [
|
|
||||||
[tri.a, 0],
|
|
||||||
[tri.b, 1],
|
|
||||||
[tri.c, 2],
|
|
||||||
];
|
|
||||||
const edges: [THREE.Vector3, THREE.Vector3, 0 | 1 | 2, 0 | 1 | 2, 0 | 1 | 2][] = [
|
|
||||||
[tri.a, tri.b, 0, 0, 1],
|
|
||||||
[tri.b, tri.c, 1, 1, 2],
|
|
||||||
[tri.c, tri.a, 2, 2, 0],
|
|
||||||
];
|
|
||||||
|
|
||||||
// A vertex is "in the frustum" if it passes the cone test
|
|
||||||
const vertexInFrustum = verts.map(([v]) =>
|
|
||||||
CircularFrustumIntersection.pointAxialDepth(v, localFrustum) !== 'NOT_INTERSECTED'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Promote to vertex hits
|
|
||||||
for (const [v, idx] of verts) {
|
|
||||||
if (vertexInFrustum[idx]) {
|
|
||||||
results.unshift({ kind: 'vertex', index: idx, pt: v });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Promote to edge hits: an edge is hit if ANY point along it falls inside the frustum.
|
|
||||||
// We sample: the two endpoints, the closest point to the axis ray, and the closest to the apex.
|
|
||||||
for (const [a, b, edgeIdx, ptIndexA, ptIndexB] of edges) {
|
|
||||||
if (vertexInFrustum[ptIndexA] || vertexInFrustum[ptIndexB]) {
|
|
||||||
// At least one endpoint inside — edge is hit
|
|
||||||
results.unshift({ kind: 'edge', index: edgeIdx, ptIndexA, ptIndexB, ptA: a, ptB: b });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check closest point on edge to frustum axis
|
|
||||||
const closestToAxis = closestPointOnEdgeToRay(a, b, localFrustum.ray);
|
|
||||||
if (CircularFrustumIntersection.pointAxialDepth(closestToAxis, localFrustum) !== 'NOT_INTERSECTED') {
|
|
||||||
results.unshift({ kind: 'edge', index: edgeIdx, ptIndexA, ptIndexB, ptA: a, ptB: b });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check closest point on edge to apex
|
|
||||||
const edge = b.clone().sub(a);
|
|
||||||
const toA = a.clone().sub(localFrustum.apex);
|
|
||||||
const tApex = clamp(-toA.dot(edge) / edge.lengthSq(), 0, 1);
|
|
||||||
const closestToApex = a.clone().addScaledVector(edge, tApex);
|
|
||||||
if (CircularFrustumIntersection.pointAxialDepth(closestToApex, localFrustum) !== 'NOT_INTERSECTED') {
|
|
||||||
results.unshift({ kind: 'edge', index: edgeIdx, ptIndexA, ptIndexB, ptA: a, ptB: b });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CircularFrustumIntersection {
|
export class CircularFrustumIntersection {
|
||||||
public readonly frustum: CircularFrustum;
|
public readonly frustum: CircularFrustum;
|
||||||
|
|
||||||
|
|
@ -259,16 +182,18 @@ export class CircularFrustumIntersection {
|
||||||
return CircularFrustumIntersection.distanceToPoint(point, this.frustum);
|
return CircularFrustumIntersection.distanceToPoint(point, this.frustum);
|
||||||
}
|
}
|
||||||
|
|
||||||
public intersectMesh(
|
public intersectMeshFaces(
|
||||||
mesh: THREE.Mesh,
|
mesh: THREE.Mesh,
|
||||||
findAll: boolean,
|
): FaceHitResult[] {
|
||||||
): Intersection[] {
|
const findAll = true;
|
||||||
|
|
||||||
const geometry = mesh.geometry;
|
const geometry = mesh.geometry;
|
||||||
if (!geometry)
|
if (!geometry)
|
||||||
return [];
|
return [];
|
||||||
|
|
||||||
const matrix = mesh.matrixWorld;
|
const matrix = mesh.matrixWorld;
|
||||||
const matrixInverted = matrix.clone().invert(); // world -> local matrix
|
const matrixInverted = matrix.clone().invert(); // world -> local matrix
|
||||||
|
|
||||||
const worldFrustum = this.frustum;
|
const worldFrustum = this.frustum;
|
||||||
const localFrustum = this.toObjectLocalSpace(matrixInverted);
|
const localFrustum = this.toObjectLocalSpace(matrixInverted);
|
||||||
|
|
||||||
|
|
@ -279,47 +204,14 @@ export class CircularFrustumIntersection {
|
||||||
if (this.insersectsSphere(boundingSphere) === 'NOT_INTERSECTED')
|
if (this.insersectsSphere(boundingSphere) === 'NOT_INTERSECTED')
|
||||||
return [];
|
return [];
|
||||||
|
|
||||||
function getGeometryVertextIdByIndex(vertexIndex: number): Id {
|
const bestFaceHits: Record<Id, FaceHitResult> = {};
|
||||||
return (mesh.userData as MeshDto).loop[vertexIndex].vertex;
|
|
||||||
|
function tryBestTriangleHit(hit: FaceHitResult) {
|
||||||
|
const bestFaceHit = bestFaceHits[hit.faceId];
|
||||||
|
if (!bestFaceHit || bestFaceHit.radialDistance > hit.radialDistance)
|
||||||
|
bestFaceHits[hit.faceId] = hit;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGeometryVertextIds(triIndex: number): [Id, Id, Id] {
|
|
||||||
return geometry.index
|
|
||||||
? [
|
|
||||||
getGeometryVertextIdByIndex(geometry.index.array[triIndex * 3]),
|
|
||||||
getGeometryVertextIdByIndex(geometry.index.array[triIndex * 3 + 1]),
|
|
||||||
getGeometryVertextIdByIndex(geometry.index.array[triIndex * 3 + 2]),
|
|
||||||
]
|
|
||||||
: ['', '', ''];
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function getHitClosestPoint(
|
|
||||||
triangle: THREE.Triangle,
|
|
||||||
details: TriangleHitDetail,
|
|
||||||
faceClosestPoint: THREE.Vector3,
|
|
||||||
): THREE.Vector3 {
|
|
||||||
switch (details.kind) {
|
|
||||||
case 'face':
|
|
||||||
return faceClosestPoint;
|
|
||||||
case 'edge':
|
|
||||||
return closestPointOnEdgeToRay(details.ptA, details.ptB, worldFrustum.ray);
|
|
||||||
case 'vertex':
|
|
||||||
return [triangle.a, triangle.b, triangle.c][details.index];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const results: Intersection[] = [];
|
|
||||||
|
|
||||||
if (!geometry.boundsTree)
|
if (!geometry.boundsTree)
|
||||||
geometry.computeBoundsTree();
|
geometry.computeBoundsTree();
|
||||||
const bvh = geometry.boundsTree;
|
const bvh = geometry.boundsTree;
|
||||||
|
|
@ -328,171 +220,151 @@ 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, triIndex: number, contained: boolean) => {
|
intersectsTriangle: (tri: ExtendedTriangle) => {
|
||||||
const triangleVertexIds = getGeometryVertextIds(triIndex);
|
const visibility = isTriangleVisible(tri, localFrustum) ? 'visible' : 'backface';
|
||||||
|
|
||||||
const normal = new THREE.Vector3();
|
let bestPoint: THREE.Vector3 | undefined;
|
||||||
tri.getNormal(normal);
|
let bestRadialDistance = Infinity;
|
||||||
const facingRatio = normal.dot(localFrustum.axisNormalized);
|
|
||||||
// normal orientation is same as frusum axis means triangle is faced way from camera
|
|
||||||
const visibility: Visibility = facingRatio >= 0 ? 'backface' : 'visible';
|
|
||||||
|
|
||||||
if (contained) {
|
function tryBestPoint(facePoint: THREE.Vector3) {
|
||||||
const closestContained = new THREE.Vector3();
|
if (CircularFrustumIntersection.pointAxialDepth(facePoint, localFrustum) === 'NOT_INTERSECTED')
|
||||||
tri.closestPointToPoint(localFrustum.apex, closestContained);
|
return;
|
||||||
const worldPoint = closestContained.clone().applyMatrix4(mesh.matrixWorld);
|
const radialDistance = CircularFrustumIntersection.distanceToPoint(facePoint, localFrustum);
|
||||||
const depth = worldFrustum.axisNormalized.dot(worldPoint.clone().sub(worldFrustum.apex));
|
if (radialDistance < bestRadialDistance) {
|
||||||
results.push(
|
bestRadialDistance = radialDistance;
|
||||||
...triangleDetailsByFrustum(tri, localFrustum)
|
bestPoint = facePoint;
|
||||||
.map((details) => {
|
}
|
||||||
const closestPoint = getHitClosestPoint(tri, details, worldPoint);
|
|
||||||
const radialDistanceAbsolute = CircularFrustumIntersection.distanceToPoint(closestPoint, worldFrustum);
|
|
||||||
return {
|
|
||||||
object: mesh,
|
|
||||||
point: closestPoint,
|
|
||||||
depth,
|
|
||||||
radialDistanceAbsolute,
|
|
||||||
radialDistance: radialDistanceAbsolute / (depth * Math.tan(worldFrustum.halfAngle)),
|
|
||||||
triangle: tri,
|
|
||||||
details,
|
|
||||||
visibility,
|
|
||||||
triangleVertexIds,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return !findAll;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let bestPoint: { depth: number, local?: THREE.Vector3 } = { depth: Infinity, local: undefined };
|
// ray pierces triangle -> internal hit point
|
||||||
|
|
||||||
// step 1: test vertices
|
|
||||||
tryBestPoint(tri.a, bestPoint);
|
|
||||||
tryBestPoint(tri.b, bestPoint);
|
|
||||||
tryBestPoint(tri.c, bestPoint);
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
const edges: [THREE.Vector3, THREE.Vector3][] = [
|
|
||||||
[tri.a, tri.b],
|
|
||||||
[tri.b, tri.c],
|
|
||||||
[tri.c, tri.a],
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const [a, b] of edges) {
|
|
||||||
const pointOnEdge = closestPointOnEdgeToRay(a, b, localFrustum.ray);
|
|
||||||
|
|
||||||
// Closest point on edge segment to the axis ray
|
|
||||||
tryBestPoint(pointOnEdge, bestPoint);
|
|
||||||
|
|
||||||
// Closest point on edge to the apex itself
|
|
||||||
const toA = a.clone().sub(localFrustum.apex);
|
|
||||||
const edge = b.clone().sub(a);
|
|
||||||
const tApex = clamp(-toA.dot(edge) / edge.lengthSq(), 0, 1);
|
|
||||||
tryBestPoint(a.clone().addScaledVector(edge, tApex), bestPoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
// step 3: closest point on triangle face to the apex
|
|
||||||
const closestOnFace = new THREE.Vector3();
|
|
||||||
tri.closestPointToPoint(localFrustum.apex, closestOnFace);
|
|
||||||
if (!isNaN(closestOnFace.x))
|
|
||||||
tryBestPoint(closestOnFace, bestPoint);
|
|
||||||
|
|
||||||
// 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 (localFrustum.ray.intersectTriangle(tri.a, tri.b, tri.c, false, faceHit))
|
||||||
tryBestPoint(faceHit, bestPoint);
|
tryBestPoint(faceHit);
|
||||||
}
|
|
||||||
|
|
||||||
if (bestPoint.local) {
|
// ray misses triangle, but cone touches an edge -> external, but close enough, hit point
|
||||||
const worldPoint = bestPoint.local.clone().applyMatrix4(mesh.matrixWorld);
|
for (const [a, b] of [[tri.a, tri.b], [tri.b, tri.c], [tri.c, tri.a]] as [THREE.Vector3, THREE.Vector3][])
|
||||||
const worldDepth = worldFrustum.axisNormalized.dot(worldPoint.clone().sub(worldFrustum.apex));
|
tryBestPoint(closestPointOnEdgeToRay(a, b, localFrustum.ray));
|
||||||
results.push(
|
|
||||||
...triangleDetailsByFrustum(tri, localFrustum)
|
if (bestPoint) {
|
||||||
.map((details) => {
|
const bestPointWorld = bestPoint.clone().applyMatrix4(matrix);
|
||||||
const closestPoint = getHitClosestPoint(tri, details, worldPoint);
|
const depth = worldFrustum.axisNormalized.dot(bestPointWorld.clone().sub(worldFrustum.apex));
|
||||||
const radialDistanceAbsolute = CircularFrustumIntersection.distanceToPoint(closestPoint, worldFrustum);
|
const radialDistanceAbsolute = CircularFrustumIntersection.distanceToPoint(bestPointWorld, worldFrustum);
|
||||||
return {
|
const faceId = (mesh.userData as MeshDto).faceId;
|
||||||
object: mesh,
|
|
||||||
point: closestPoint,
|
tryBestTriangleHit({
|
||||||
depth: worldDepth,
|
kind: 'face',
|
||||||
radialDistanceAbsolute,
|
uuid: faceId,
|
||||||
radialDistance: radialDistanceAbsolute / (worldDepth * Math.tan(worldFrustum.halfAngle)),
|
faceId: faceId,
|
||||||
triangle: tri,
|
object: mesh,
|
||||||
details,
|
point: bestPointWorld,
|
||||||
visibility,
|
depth,
|
||||||
triangleVertexIds,
|
radialDistanceAbsolute,
|
||||||
};
|
radialDistance: radialDistanceAbsolute / (depth * Math.tan(worldFrustum.halfAngle)),
|
||||||
}),
|
visibility,
|
||||||
);
|
});
|
||||||
return !findAll;
|
return !findAll;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return Object.values(bestFaceHits);
|
||||||
|
}
|
||||||
|
|
||||||
|
public intersectMesh(
|
||||||
|
mesh: THREE.Mesh,
|
||||||
|
): HitResult[] {
|
||||||
|
const faceHits = this.intersectMeshFaces(mesh);
|
||||||
|
const results = [...faceHits] as HitResult[];
|
||||||
|
for (const faceHit of faceHits) {
|
||||||
|
const loop = model.loopByFaceId(faceHit.faceId);
|
||||||
|
if (!loop)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const halfEdges = model.halfEdgesByLoop(loop.id);
|
||||||
|
const vertices = halfEdges
|
||||||
|
.map((he) => model.vertexById(he.origin)!)
|
||||||
|
.map((v) => ({ id: v.id, point: new THREE.Vector3(v.x, v.y, v.z) }));
|
||||||
|
|
||||||
|
const edges = halfEdges
|
||||||
|
.map((he, idx) => ({ id: he.ownerEdge, a: vertices[idx].point, b: vertices[(idx + 1) % vertices.length].point }));
|
||||||
|
|
||||||
|
for (const vertex of vertices) {
|
||||||
|
const d = this.pointAxialDepth(vertex.point);
|
||||||
|
if (d === 'NOT_INTERSECTED')
|
||||||
|
continue;
|
||||||
|
const radialDistanceAbsolute = CircularFrustumIntersection.distanceToPoint(vertex.point, this.frustum);
|
||||||
|
results.push(
|
||||||
|
{
|
||||||
|
kind: 'vertex',
|
||||||
|
uuid: faceHit.faceId + '.' + vertex.id,
|
||||||
|
id: vertex.id,
|
||||||
|
faceId: faceHit.faceId,
|
||||||
|
object: faceHit.object,
|
||||||
|
visibility: faceHit.visibility,
|
||||||
|
point: vertex.point,
|
||||||
|
depth: d,
|
||||||
|
radialDistanceAbsolute,
|
||||||
|
radialDistance: radialDistanceAbsolute / (d * Math.tan(this.frustum.halfAngle)),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const edge of edges) {
|
||||||
|
const closestLocal = closestPointOnEdgeToRay(edge.a, edge.b, this.frustum.ray);
|
||||||
|
const d = this.pointAxialDepth(closestLocal);
|
||||||
|
if (d === 'NOT_INTERSECTED')
|
||||||
|
continue;
|
||||||
|
const radialDistanceAbsolute = CircularFrustumIntersection.distanceToPoint(closestLocal, this.frustum);
|
||||||
|
results.push(
|
||||||
|
{
|
||||||
|
kind: 'edge',
|
||||||
|
uuid: faceHit.faceId + '.' + edge.id,
|
||||||
|
id: edge.id,
|
||||||
|
faceId: faceHit.faceId,
|
||||||
|
object: faceHit.object,
|
||||||
|
visibility: faceHit.visibility,
|
||||||
|
point: closestLocal,
|
||||||
|
depth: d,
|
||||||
|
radialDistanceAbsolute,
|
||||||
|
radialDistance: radialDistanceAbsolute / (d * Math.tan(this.frustum.halfAngle)),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
public intersectObject(
|
public intersectObject(
|
||||||
obj: THREE.Object3D,
|
object: THREE.Object3D,
|
||||||
options: CircularFrustumIntersectionOptions = {},
|
options: CircularFrustumIntersectionOptions = {},
|
||||||
): HitResult[] {
|
): HitResult[] {
|
||||||
const results: HitResult[] = [];
|
const results: HitResult[] = [];
|
||||||
|
|
||||||
obj.traverseVisible((object) => {
|
object.traverseVisible((object) => {
|
||||||
if (options.filter && !options.filter(object))
|
if (options.filter && !options.filter(object))
|
||||||
return;
|
return;
|
||||||
if (!(object instanceof THREE.Mesh))
|
if (!(object instanceof THREE.Mesh))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
results.push(
|
results.push(...this.intersectMesh(object));
|
||||||
...this.intersectMesh(object, !!options.findAll)
|
|
||||||
.map((i) => this.intersectionToHitResult(i))
|
|
||||||
.filter((i) => !!i),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// sort closest first
|
results.sort((a, b) => a.depth - b.depth);
|
||||||
results.sort((a, b) => a.intersection.depth - b.intersection.depth);
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
private intersectionToHitResult(intersection: Intersection): HitResult | undefined {
|
public intersectObjects(
|
||||||
const userData = intersection.object.userData as MeshDto;
|
objects: THREE.Object3D[],
|
||||||
const faceId = userData.faceId;
|
options: CircularFrustumIntersectionOptions = {},
|
||||||
const loop = userData.loop;
|
): HitResult[] {
|
||||||
|
const results: HitResult[] = [];
|
||||||
|
|
||||||
function vertexId(index: number) {
|
for (const object of objects)
|
||||||
return intersection.triangleVertexIds[index];
|
results.push(...this.intersectObject(object, options));
|
||||||
}
|
|
||||||
|
|
||||||
switch (intersection.details.kind) {
|
results.sort((a, b) => a.depth - b.depth);
|
||||||
case 'face':
|
return results;
|
||||||
return {
|
|
||||||
kind: 'face',
|
|
||||||
id: faceId,
|
|
||||||
faceId,
|
|
||||||
intersection,
|
|
||||||
};
|
|
||||||
case 'edge':
|
|
||||||
const triHit = intersection.details;
|
|
||||||
const edge = loop.find((v) => (v.vertex === vertexId(triHit.ptIndexA)) && (v.vertex2 === vertexId(triHit.ptIndexB)))?.edge;
|
|
||||||
return edge // undefined for edges created by tesselation like diagonals, etc.
|
|
||||||
? {
|
|
||||||
kind: 'edge',
|
|
||||||
id: edge,
|
|
||||||
faceId,
|
|
||||||
intersection,
|
|
||||||
}
|
|
||||||
: undefined;
|
|
||||||
case 'vertex':
|
|
||||||
return {
|
|
||||||
kind: 'vertex',
|
|
||||||
id: vertexId(intersection.details.index),
|
|
||||||
faceId,
|
|
||||||
intersection,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -50,12 +50,8 @@ export class SceneHelper {
|
||||||
): HitResults {
|
): HitResults {
|
||||||
this.buildMouseFrustum(mouseNormalized, screenSize);
|
this.buildMouseFrustum(mouseNormalized, screenSize);
|
||||||
|
|
||||||
const hits: HitResult[] = [];
|
|
||||||
for (const object of this.objects)
|
|
||||||
hits.push(...this.mouseFrustum.intersectObject(object));
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hits,
|
hits: this.mouseFrustum.intersectObjects(this.objects),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,17 @@ export class Model {
|
||||||
return this.loops[id];
|
return this.loops[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public loopsByFilter(filter: (loop: Loop) => boolean): Loop[] {
|
||||||
|
return Object.values(this.loops).filter(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
public loopByFaceId(faceId: Face['id']): Loop | undefined {
|
||||||
|
const face = this.faceById(faceId);
|
||||||
|
if (!face)
|
||||||
|
return undefined
|
||||||
|
return this.loopById(face.outerLoop);
|
||||||
|
}
|
||||||
|
|
||||||
public faceById(id: Face['id']): Face | undefined {
|
public faceById(id: Face['id']): Face | undefined {
|
||||||
return this.faces[id];
|
return this.faces[id];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue