Compare commits
2 Commits
1f607e4a6c
...
df90158cb0
| Author | SHA1 | Date |
|---|---|---|
|
|
df90158cb0 | |
|
|
c6979f03fc |
|
|
@ -7,11 +7,11 @@
|
||||||
height: 600px;
|
height: 600px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#hit-test {
|
.hit-test-info {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 600px;
|
top: 0px;
|
||||||
left: 000px;
|
left: 0px;
|
||||||
width: 600px;
|
|
||||||
font-size: 75%;
|
font-size: 75%;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
color: white;
|
color: white;
|
||||||
|
|
@ -20,7 +20,6 @@
|
||||||
#blob-view {
|
#blob-view {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background: #eee;
|
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
|
||||||
|
|
@ -30,7 +29,7 @@
|
||||||
width: 400px;
|
width: 400px;
|
||||||
|
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-size: 10pt;
|
font-size: 75%;
|
||||||
|
|
||||||
& .primitive {
|
& .primitive {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ export const App = function () {
|
||||||
<div>
|
<div>
|
||||||
<Viewport />
|
<Viewport />
|
||||||
<div className="side-panel">
|
<div className="side-panel">
|
||||||
<HitTestView />
|
<HitTestView float/>
|
||||||
<DbView />
|
<DbView />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,29 @@
|
||||||
import * as yaml from 'yaml';
|
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";
|
||||||
|
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 style: CSSProperties = {};
|
||||||
const top = state.mousePosition.y;
|
if (float) {
|
||||||
|
style.left = state.mousePosition.x;
|
||||||
|
style.top = state.mousePosition.y;
|
||||||
|
style.width = 'auto';
|
||||||
|
style.height = 'auto';
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="hit-test" style={{ top, left }}>
|
<div className="hit-test-info" style={style}>
|
||||||
<pre style={{ textWrap: 'wrap' }}>
|
<pre style={{ textWrap: 'wrap' }}>
|
||||||
{
|
|
||||||
`${top},${left}`
|
|
||||||
}
|
|
||||||
{
|
{
|
||||||
state.hitResults.hits.map((hit) =>
|
state.hitResults.hits.map((hit) =>
|
||||||
<div key={hit.object.uuid}>
|
<div key={hit.id}>
|
||||||
<div>{yaml.stringify(
|
<div>{yaml.stringify(
|
||||||
{
|
{
|
||||||
hit: { ...hit, object: undefined, triangle: undefined },
|
hit: { ...hit, intersection: { ...hit.intersection, point: formatPoint(hit.intersection.point), object: undefined, triangle: undefined } },
|
||||||
userData: hit.object.userData,
|
// userData: hit.intersection.object.userData,
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
2,
|
2,
|
||||||
|
|
|
||||||
|
|
@ -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.object.uuid, hit.intersection.point);
|
sceneHelper.showPointHint(hit.intersection.object.uuid, hit.intersection.point);
|
||||||
})
|
})
|
||||||
// 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.object.userData.faceId);
|
const hoveredFaceIds = e.hitResults.hits.map((hit) => hit.intersection.object.userData.faceId);
|
||||||
// if (hoveredFaceIds.length)
|
// if (hoveredFaceIds.length)
|
||||||
// console.log(hoveredFaceIds);
|
// console.log(hoveredFaceIds);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,33 +3,59 @@ import { CONTAINED, ExtendedTriangle, INTERSECTED, NOT_INTERSECTED } from 'three
|
||||||
import { CircularFrustum } from './circularFrustum';
|
import { CircularFrustum } from './circularFrustum';
|
||||||
import type { Id } from '../types';
|
import type { Id } from '../types';
|
||||||
|
|
||||||
export type HitResults = {
|
|
||||||
hits: HitResult[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TriangleVertexHitDetail = {
|
export type TriangleVertexHitDetail = {
|
||||||
kind: 'vertex',
|
kind: 'vertex',
|
||||||
index: 0 | 1 | 2,
|
index: 0 | 1 | 2,
|
||||||
id: Id,
|
id?: Id,
|
||||||
}
|
}
|
||||||
export type TriangleEdgeHitDetail = {
|
export type TriangleEdgeHitDetail = {
|
||||||
kind: 'edge',
|
kind: 'edge',
|
||||||
index: 0 | 1 | 2, // edge 0=AB, 1=BC, 2=CA
|
index: 0 | 1 | 2, // edge 0=AB, 1=BC, 2=CA
|
||||||
id: Id,
|
id?: Id,
|
||||||
}
|
}
|
||||||
export type TriangleFaceHitDetail = {
|
export type TriangleFaceHitDetail = {
|
||||||
kind: 'face',
|
kind: 'face',
|
||||||
|
id?: Id,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TriangleHitDetail = TriangleVertexHitDetail | TriangleEdgeHitDetail | TriangleFaceHitDetail;
|
export type TriangleHitDetail = TriangleVertexHitDetail | TriangleEdgeHitDetail | TriangleFaceHitDetail;
|
||||||
|
|
||||||
export type HitResult = {
|
export type Visibility = 'visible' | 'backface'; // | 'occluded'
|
||||||
object: THREE.Object3D;
|
|
||||||
point: THREE.Vector3; // world-space closest hit point
|
export type Intersection = {
|
||||||
depth: number; // depth along frustum axis
|
object: THREE.Object3D,
|
||||||
triangle: { a: THREE.Vector3, b: THREE.Vector3, c: THREE.Vector3 };
|
point: THREE.Vector3, // world-space closest hit point
|
||||||
triHit?: TriangleHitDetail;
|
depth: number, // depth along frustum axis
|
||||||
vertexIds: [Id, Id, Id] | undefined; // undefined is when geometry does not have .index
|
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 = {
|
export type CircularFrustumIntersectionOptions = {
|
||||||
|
|
@ -55,7 +81,7 @@ const BARYCENTRIC_EPSILON = 1e-1;
|
||||||
function classifyTriangleHit(
|
function classifyTriangleHit(
|
||||||
point: THREE.Vector3,
|
point: THREE.Vector3,
|
||||||
tri: ExtendedTriangle,
|
tri: ExtendedTriangle,
|
||||||
vertexIds: [Id, Id, Id],
|
vertexIds?: [Id, Id, Id],
|
||||||
): TriangleHitDetail {
|
): TriangleHitDetail {
|
||||||
// Compute barycentric coords via areas
|
// Compute barycentric coords via areas
|
||||||
const ab = tri.b.clone().sub(tri.a);
|
const ab = tri.b.clone().sub(tri.a);
|
||||||
|
|
@ -78,9 +104,9 @@ function classifyTriangleHit(
|
||||||
const onB = v > eps;
|
const onB = v > eps;
|
||||||
const onC = w > eps;
|
const onC = w > eps;
|
||||||
|
|
||||||
if (onA) return { kind: 'vertex', index: 0, id: vertexIds[0] };
|
if (onA) return { kind: 'vertex', index: 0, id: vertexIds?.[0] };
|
||||||
if (onB) return { kind: 'vertex', index: 1, id: vertexIds[1] };
|
if (onB) return { kind: 'vertex', index: 1, id: vertexIds?.[1] };
|
||||||
if (onC) return { kind: 'vertex', index: 2, id: vertexIds[2] };
|
if (onC) return { kind: 'vertex', index: 2, id: vertexIds?.[2] };
|
||||||
|
|
||||||
const onAB = w < BARYCENTRIC_EPSILON; // u+v≈1, w≈0
|
const onAB = w < BARYCENTRIC_EPSILON; // u+v≈1, w≈0
|
||||||
const onBC = u < BARYCENTRIC_EPSILON;
|
const onBC = u < BARYCENTRIC_EPSILON;
|
||||||
|
|
@ -180,7 +206,7 @@ export class CircularFrustumIntersection {
|
||||||
public intersectMesh(
|
public intersectMesh(
|
||||||
mesh: THREE.Mesh,
|
mesh: THREE.Mesh,
|
||||||
findAll: boolean,
|
findAll: boolean,
|
||||||
): HitResult[] {
|
): Intersection[] {
|
||||||
const geometry = mesh.geometry;
|
const geometry = mesh.geometry;
|
||||||
if (!geometry)
|
if (!geometry)
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -201,14 +227,14 @@ export class CircularFrustumIntersection {
|
||||||
return mesh.userData.vertexIds[vertexIndex];
|
return mesh.userData.vertexIds[vertexIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGeometryVertextIds(triIndex: number): HitResult['vertexIds'] {
|
function getGeometryVertextIds(triIndex: number): [Id, Id, Id] {
|
||||||
return geometry.index
|
return geometry.index
|
||||||
? [
|
? [
|
||||||
getGeometryVertextIdByIndex(geometry.index.array[triIndex * 3]),
|
getGeometryVertextIdByIndex(geometry.index.array[triIndex * 3]),
|
||||||
getGeometryVertextIdByIndex(geometry.index.array[triIndex * 3 + 1]),
|
getGeometryVertextIdByIndex(geometry.index.array[triIndex * 3 + 1]),
|
||||||
getGeometryVertextIdByIndex(geometry.index.array[triIndex * 3 + 2]),
|
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);
|
||||||
|
|
@ -221,7 +247,7 @@ export class CircularFrustumIntersection {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const results: HitResult[] = [];
|
const results: Intersection[] = [];
|
||||||
|
|
||||||
if (!geometry.boundsTree)
|
if (!geometry.boundsTree)
|
||||||
geometry.computeBoundsTree();
|
geometry.computeBoundsTree();
|
||||||
|
|
@ -232,7 +258,13 @@ export class CircularFrustumIntersection {
|
||||||
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, 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) {
|
if (contained) {
|
||||||
const worldPoint = tri.a.clone().applyMatrix4(mesh.matrixWorld);
|
const worldPoint = tri.a.clone().applyMatrix4(mesh.matrixWorld);
|
||||||
|
|
@ -243,7 +275,8 @@ export class CircularFrustumIntersection {
|
||||||
depth,
|
depth,
|
||||||
triangle: tri,
|
triangle: tri,
|
||||||
triHit: classifyTriangleHit(tri.a, tri, tiangleVertexIds),
|
triHit: classifyTriangleHit(tri.a, tri, tiangleVertexIds),
|
||||||
vertexIds: tiangleVertexIds,
|
visibility,
|
||||||
|
// vertexIds: tiangleVertexIds,
|
||||||
});
|
});
|
||||||
return !findAll;
|
return !findAll;
|
||||||
}
|
}
|
||||||
|
|
@ -310,7 +343,7 @@ export class CircularFrustumIntersection {
|
||||||
depth: worldDepth,
|
depth: worldDepth,
|
||||||
triangle: { a, b, c },
|
triangle: { a, b, c },
|
||||||
triHit: classifyTriangleHit(bestPoint.local, tri, tiangleVertexIds),
|
triHit: classifyTriangleHit(bestPoint.local, tri, tiangleVertexIds),
|
||||||
vertexIds: tiangleVertexIds,
|
visibility,
|
||||||
});
|
});
|
||||||
return !findAll;
|
return !findAll;
|
||||||
}
|
}
|
||||||
|
|
@ -334,11 +367,41 @@ export class CircularFrustumIntersection {
|
||||||
if (!(object instanceof THREE.Mesh))
|
if (!(object instanceof THREE.Mesh))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
results.push(...this.intersectMesh(object, !!options.findAll));
|
results.push(
|
||||||
|
...this.intersectMesh(object, !!options.findAll)
|
||||||
|
.flatMap((i) => this.intersectionToHitResult(i)),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// sort closest first
|
// 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;
|
return results;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,3 +1,8 @@
|
||||||
|
:root {
|
||||||
|
background: #101020;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
/* :root {
|
/* :root {
|
||||||
--text: #6b6375;
|
--text: #6b6375;
|
||||||
--text-h: #08060d;
|
--text-h: #08060d;
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,8 @@ export class SceneSync {
|
||||||
const meshes = Geometry
|
const meshes = Geometry
|
||||||
.tessellateSolid(id)
|
.tessellateSolid(id)
|
||||||
.map(meshToDto);
|
.map(meshToDto);
|
||||||
this.addSolid(meshes[2]);
|
this.addSolid(meshes[0]); // bottom
|
||||||
|
this.addSolid(meshes[3]); // front
|
||||||
// for (const mesh of meshes)
|
// for (const mesh of meshes)
|
||||||
// this.addSolid(mesh);
|
// this.addSolid(mesh);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue