Compare commits

..

2 Commits

Author SHA1 Message Date
azykov@mail.ru df90158cb0
floating hit test info box 2026-05-24 11:56:06 +03:00
azykov@mail.ru c6979f03fc
dark theme 2026-05-24 11:49:22 +03:00
7 changed files with 119 additions and 47 deletions

View File

@ -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;
@ -20,7 +20,6 @@
#blob-view {
margin-top: 20px;
padding: 10px;
background: #eee;
font-family: monospace;
white-space: pre-wrap;
@ -30,7 +29,7 @@
width: 400px;
text-align: left;
font-size: 10pt;
font-size: 75%;
& .primitive {

View File

@ -9,7 +9,7 @@ export const App = function () {
<div>
<Viewport />
<div className="side-panel">
<HitTestView />
<HitTestView float/>
<DbView />
</div>
</div>

View File

@ -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 (
<div id="hit-test" style={{ top, left }}>
<div className="hit-test-info" style={style}>
<pre style={{ textWrap: 'wrap' }}>
{
`${top},${left}`
}
{
state.hitResults.hits.map((hit) =>
<div key={hit.object.uuid}>
<div key={hit.id}>
<div>{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,

View File

@ -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);

View File

@ -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;
}
}

View File

@ -1,3 +1,8 @@
:root {
background: #101020;
color: white;
}
/* :root {
--text: #6b6375;
--text-h: #08060d;

View File

@ -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);
}