diff --git a/client/src/App.scss b/client/src/App.scss index b033e62..713e48a 100644 --- a/client/src/App.scss +++ b/client/src/App.scss @@ -7,11 +7,11 @@ height: 600px; } -#hit-test { +.hit-test-info { position: absolute; - top: 600px; - left: 000px; - width: 600px; + top: 0px; + left: 0px; + font-size: 75%; pointer-events: none; color: white; diff --git a/client/src/App.tsx b/client/src/App.tsx index 6018f83..e66e7be 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -9,7 +9,7 @@ export const App = function () {
- +
diff --git a/client/src/components/HitTestView.tsx b/client/src/components/HitTestView.tsx index adb9a2a..9d186f4 100644 --- a/client/src/components/HitTestView.tsx +++ b/client/src/components/HitTestView.tsx @@ -1,25 +1,29 @@ import * as yaml from 'yaml'; import { observer } from "mobx-react-lite"; import { state } from "../state/root"; +import type { CSSProperties } from 'react'; +import { formatPoint } from '../helpers/stringFormat'; -export const HitTestView = observer(function () { +export const HitTestView = observer(function ({ float }: { float: boolean }) { - const left = state.mousePosition.x; - const top = state.mousePosition.y; + const style: CSSProperties = {}; + if (float) { + style.left = state.mousePosition.x; + style.top = state.mousePosition.y; + style.width = 'auto'; + style.height = 'auto'; + } return ( -
+
-                {
-                    `${top},${left}`
-                }
                 {
                     state.hitResults.hits.map((hit) =>
-                        
+
{yaml.stringify( { - hit: { ...hit, object: undefined, triangle: undefined }, - userData: hit.object.userData, + hit: { ...hit, intersection: { ...hit.intersection, point: formatPoint(hit.intersection.point), object: undefined, triangle: undefined } }, + // userData: hit.intersection.object.userData, }, undefined, 2, diff --git a/client/src/components/Viewport.tsx b/client/src/components/Viewport.tsx index ad655d5..8acded9 100644 --- a/client/src/components/Viewport.tsx +++ b/client/src/components/Viewport.tsx @@ -12,15 +12,15 @@ export const Viewport = function () { sceneHelper.clearHints(); if (e.hitResults.hits.length) { e.hitResults.hits.forEach((hit) => { - sceneHelper.showPointHint(hit.object.uuid, hit.intersection.point); - }) + sceneHelper.showPointHint(hit.intersection.object.uuid, hit.intersection.point); + }) // console.log(e.position); // console.log(e.hitTest.objects.map((o) => o)); // console.log(e.hitTest.objects.flatMap((o) => o.point.toArray())); } // raycaster.setFromCamera(new THREE.Vector2(e.x, e.y), camera); // const hits = raycaster.intersectObjects(sync.meshes); - const hoveredFaceIds = e.hitResults.hits.map((hit) => hit.object.userData.faceId); + const hoveredFaceIds = e.hitResults.hits.map((hit) => hit.intersection.object.userData.faceId); // if (hoveredFaceIds.length) // console.log(hoveredFaceIds); diff --git a/client/src/helpers/circularFrustumIntersect.ts b/client/src/helpers/circularFrustumIntersect.ts index 3b13341..b1fbbd8 100644 --- a/client/src/helpers/circularFrustumIntersect.ts +++ b/client/src/helpers/circularFrustumIntersect.ts @@ -3,33 +3,59 @@ import { CONTAINED, ExtendedTriangle, INTERSECTED, NOT_INTERSECTED } from 'three import { CircularFrustum } from './circularFrustum'; import type { Id } from '../types'; -export type HitResults = { - hits: HitResult[]; -} - export type TriangleVertexHitDetail = { kind: 'vertex', index: 0 | 1 | 2, - id: Id, + id?: Id, } export type TriangleEdgeHitDetail = { kind: 'edge', index: 0 | 1 | 2, // edge 0=AB, 1=BC, 2=CA - id: Id, + id?: Id, } export type TriangleFaceHitDetail = { kind: 'face', + id?: Id, } export type TriangleHitDetail = TriangleVertexHitDetail | TriangleEdgeHitDetail | TriangleFaceHitDetail; -export type HitResult = { - object: THREE.Object3D; - point: THREE.Vector3; // world-space closest hit point - depth: number; // depth along frustum axis - 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 Visibility = 'visible' | 'backface'; // | 'occluded' + +export type Intersection = { + object: THREE.Object3D, + point: THREE.Vector3, // world-space closest hit point + depth: number, // depth along frustum axis + triangle: { a: THREE.Vector3, b: THREE.Vector3, c: THREE.Vector3 }, + triHit?: TriangleHitDetail, + visibility: Visibility, +} + +export type BaseHitResult = { + intersection: Intersection, +} + +export type FaceHitResult = BaseHitResult & { + kind: 'face', + id?: Id, +} + +export type EdgeHitResult = BaseHitResult & { + kind: 'edge', + id?: Id, + faceId: Id, +} + +export type VertexHitResult = BaseHitResult & { + kind: 'vertex', + id?: Id, + faceId: Id, +} + +export type HitResult = FaceHitResult | EdgeHitResult | VertexHitResult; + +export type HitResults = { + hits: HitResult[]; } export type CircularFrustumIntersectionOptions = { @@ -55,7 +81,7 @@ const BARYCENTRIC_EPSILON = 1e-1; function classifyTriangleHit( point: THREE.Vector3, tri: ExtendedTriangle, - vertexIds: [Id, Id, Id], + vertexIds?: [Id, Id, Id], ): TriangleHitDetail { // Compute barycentric coords via areas const ab = tri.b.clone().sub(tri.a); @@ -78,9 +104,9 @@ function classifyTriangleHit( 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] }; + 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; @@ -180,7 +206,7 @@ export class CircularFrustumIntersection { public intersectMesh( mesh: THREE.Mesh, findAll: boolean, - ): HitResult[] { + ): Intersection[] { const geometry = mesh.geometry; if (!geometry) return []; @@ -201,14 +227,14 @@ export class CircularFrustumIntersection { return mesh.userData.vertexIds[vertexIndex]; } - function getGeometryVertextIds(triIndex: number): HitResult['vertexIds'] { + 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]), ] - : undefined; + : ['', '', '']; } const axisRay = new THREE.Ray(localFrustum.apex, localFrustum.axisNormalized); @@ -221,7 +247,7 @@ export class CircularFrustumIntersection { } }; - const results: HitResult[] = []; + const results: Intersection[] = []; if (!geometry.boundsTree) geometry.computeBoundsTree(); @@ -232,7 +258,13 @@ export class CircularFrustumIntersection { intersectsBounds: (box: THREE.Box3) => intersectionResultToBvh(CircularFrustumIntersection.intersectsBox(box, localFrustum)), intersectsTriangle: (tri: ExtendedTriangle, triIndex: number, contained: boolean) => { - const tiangleVertexIds = getGeometryVertextIds(triIndex) ?? ['','','']; + const tiangleVertexIds = getGeometryVertextIds(triIndex); + + const normal = new THREE.Vector3(); + tri.getNormal(normal); + 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) { const worldPoint = tri.a.clone().applyMatrix4(mesh.matrixWorld); @@ -243,7 +275,8 @@ export class CircularFrustumIntersection { depth, triangle: tri, triHit: classifyTriangleHit(tri.a, tri, tiangleVertexIds), - vertexIds: tiangleVertexIds, + visibility, + // vertexIds: tiangleVertexIds, }); return !findAll; } @@ -310,7 +343,7 @@ export class CircularFrustumIntersection { depth: worldDepth, triangle: { a, b, c }, triHit: classifyTriangleHit(bestPoint.local, tri, tiangleVertexIds), - vertexIds: tiangleVertexIds, + visibility, }); return !findAll; } @@ -334,11 +367,41 @@ export class CircularFrustumIntersection { if (!(object instanceof THREE.Mesh)) return; - results.push(...this.intersectMesh(object, !!options.findAll)); + results.push( + ...this.intersectMesh(object, !!options.findAll) + .flatMap((i) => this.intersectionToHitResult(i)), + ); }); // sort closest first - results.sort((a, b) => a.depth - b.depth); + results.sort((a, b) => a.intersection.depth - b.intersection.depth); + return results; + } + + private intersectionToHitResult(intersection: Intersection): HitResult[] { + const faceId = intersection.object.userData.faceId; + + const results: HitResult[] = [{ + kind: 'face', + intersection, + id: faceId, + }]; + if (intersection.triHit?.kind === 'edge') { + results.unshift({ + kind: 'edge', + intersection, + id: intersection.triHit?.id, + faceId, + }); + } + if (intersection.triHit?.kind === 'vertex') { + results.unshift({ + kind: 'vertex', + intersection, + id: intersection.triHit?.id, + faceId, + }); + } return results; } } \ No newline at end of file diff --git a/client/src/layers/sceneSync.ts b/client/src/layers/sceneSync.ts index 93a86c7..9b38830 100644 --- a/client/src/layers/sceneSync.ts +++ b/client/src/layers/sceneSync.ts @@ -38,7 +38,8 @@ export class SceneSync { const meshes = Geometry .tessellateSolid(id) .map(meshToDto); - this.addSolid(meshes[2]); + this.addSolid(meshes[0]); // bottom + this.addSolid(meshes[3]); // front // for (const mesh of meshes) // this.addSolid(mesh); }