diff --git a/client/src/helpers/circularFrustumIntersect.ts b/client/src/helpers/circularFrustumIntersect.ts index cde76ad..2981f85 100644 --- a/client/src/helpers/circularFrustumIntersect.ts +++ b/client/src/helpers/circularFrustumIntersect.ts @@ -85,74 +85,6 @@ export function intersectionResultToBvh(value: IntersectionResult): typeof NOT_I } } -const BARYCENTRIC_EPSILON = 1e-1; - -function triangleFaceEdgeVertexHit( - point: THREE.Vector3, - tri: THREE.Triangle, - // vertexIds?: [Id, Id, Id], -): TriangleHitDetail[] { - const results: TriangleHitDetail[] = [{ kind: 'face' }]; - - const bary = new THREE.Vector3(); - tri.getBarycoord(point, bary); - const [u, v, w] = bary.toArray(); // x = AB, y = AC, z = BC - - //TODO if AB is much longer than AC, epsilon has different world size. need to scale - const eps = 1 - BARYCENTRIC_EPSILON; - - const onAB = w < BARYCENTRIC_EPSILON; - const onBC = u < BARYCENTRIC_EPSILON; - const onCA = v < BARYCENTRIC_EPSILON; - - if (onAB) { - results.unshift({ - kind: 'edge', - index: 0, - ptIndexA: 0, - ptIndexB: 1, - ptA: tri.a, - ptB: tri.b, - // idA: vertexIds?.[0], - // idB: vertexIds?.[1], - }); - } - if (onBC) { - results.unshift({ - kind: 'edge', - index: 1, - ptIndexA: 1, - ptIndexB: 2, - ptA: tri.b, - ptB: tri.c, - // idA: vertexIds?.[1], - // idB: vertexIds?.[2], - }); - } - if (onCA) { - results.unshift({ - kind: 'edge', - index: 2, - ptIndexA: 2, - ptIndexB: 0, - ptA: tri.c, - ptB: tri.a, - // idA: vertexIds?.[2], - // idB: vertexIds?.[0], - }); - } - - const onA = u > eps; - const onB = v > eps; - const onC = w > eps; - - if (onA) results.unshift({ kind: 'vertex', index: 0, pt: tri.a }); - if (onB) results.unshift({ kind: 'vertex', index: 1, pt: tri.b }); - if (onC) results.unshift({ kind: 'vertex', index: 2, pt: tri.c }); - - return results; -} - function closestPointOnEdgeToRay( start: THREE.Vector3, end: THREE.Vector3, @@ -174,6 +106,64 @@ function closestPointOnEdgeToRay( 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 { public readonly frustum: CircularFrustum; @@ -353,7 +343,7 @@ export class CircularFrustumIntersection { const worldPoint = closestContained.clone().applyMatrix4(mesh.matrixWorld); const depth = worldFrustum.axisNormalized.dot(worldPoint.clone().sub(worldFrustum.apex)); results.push( - ...triangleFaceEdgeVertexHit(closestContained, tri) + ...triangleDetailsByFrustum(tri, localFrustum) .map((details) => { const closestPoint = getHitClosestPoint(tri, details, worldPoint); const radialDistanceAbsolute = CircularFrustumIntersection.distanceToPoint(closestPoint, worldFrustum); @@ -417,7 +407,7 @@ export class CircularFrustumIntersection { const worldPoint = bestPoint.local.clone().applyMatrix4(mesh.matrixWorld); const worldDepth = worldFrustum.axisNormalized.dot(worldPoint.clone().sub(worldFrustum.apex)); results.push( - ...triangleFaceEdgeVertexHit(bestPoint.local, tri) + ...triangleDetailsByFrustum(tri, localFrustum) .map((details) => { const closestPoint = getHitClosestPoint(tri, details, worldPoint); const radialDistanceAbsolute = CircularFrustumIntersection.distanceToPoint(closestPoint, worldFrustum);