Compare commits
No commits in common. "cf45752848d12bd8bd158a8fd511f4225ea19c9d" and "0a750606ac6b2a6fea304f4dd56b22cfaef536ff" have entirely different histories.
cf45752848
...
0a750606ac
|
|
@ -7,10 +7,10 @@ export const HitTestView = observer(function () {
|
||||||
<div id="hit-test">
|
<div id="hit-test">
|
||||||
<pre>
|
<pre>
|
||||||
{
|
{
|
||||||
state.hitTest.hits.map((hit) =>
|
state.hitTest.objects.map((obj) =>
|
||||||
<div key={hit.object.uuid}>
|
<div key={obj.object.uuid}>
|
||||||
{JSON.stringify(hit.point.toArray())}
|
{JSON.stringify(obj.point.toArray())}
|
||||||
{JSON.stringify(hit.object.userData)}
|
{JSON.stringify(obj.object.userData)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@ import { useEffect, useRef } from "react";
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import { useInteraction, type InteractionMouseEventArgs } from "../helpers/hooks/useInteration";
|
import { useInteraction, type InteractionMouseEventArgs } from "../helpers/hooks/useInteration";
|
||||||
import { db } from "../backend/db";
|
import { db } from "../backend/db";
|
||||||
|
import { HitTestFactory, type HitTest } from "../helpers/hitTest";
|
||||||
import { model } from "../model/model";
|
import { model } from "../model/model";
|
||||||
import { SceneHelper } from "../helpers/sceneHelper";
|
import { SceneHelper } from "../helpers/sceneHelper";
|
||||||
import type { HitResults } from "../helpers/circularFrustumIntersect";
|
|
||||||
|
|
||||||
export type ThreeViewEventArgs = {
|
export type ThreeViewEventArgs = {
|
||||||
camera: THREE.Camera,
|
camera: THREE.Camera,
|
||||||
|
|
@ -17,7 +17,7 @@ export type ThreeViewTickEventArgs = ThreeViewEventArgs & {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ThreeViewMouseEventArgs = ThreeViewEventArgs & {
|
export type ThreeViewMouseEventArgs = ThreeViewEventArgs & {
|
||||||
hitResults: HitResults,
|
hitTest: HitTest,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ThreeViewProps = {
|
export type ThreeViewProps = {
|
||||||
|
|
@ -90,7 +90,7 @@ export const ThreeView = function (props: ThreeViewProps) {
|
||||||
const { scene, camera } = setupScene({ w: W, h: H });
|
const { scene, camera } = setupScene({ w: W, h: H });
|
||||||
cameraRef.current = camera;
|
cameraRef.current = camera;
|
||||||
|
|
||||||
props.sceneHelper.initialize(scene, camera);
|
props.sceneHelper.initialize(scene);
|
||||||
|
|
||||||
const handleWindowResize = () => {
|
const handleWindowResize = () => {
|
||||||
const w = container.clientWidth;
|
const w = container.clientWidth;
|
||||||
|
|
@ -102,27 +102,31 @@ export const ThreeView = function (props: ThreeViewProps) {
|
||||||
window.addEventListener("resize", handleWindowResize);
|
window.addEventListener("resize", handleWindowResize);
|
||||||
|
|
||||||
handleHover = (e: InteractionMouseEventArgs) => {
|
handleHover = (e: InteractionMouseEventArgs) => {
|
||||||
const hitResults = props.sceneHelper.hitTest(
|
const hitTest = HitTestFactory.hitTest(
|
||||||
e.position,
|
props.sceneHelper,
|
||||||
e.screenSize,
|
new THREE.Vector2(e.position.x, e.position.y),
|
||||||
|
camera,
|
||||||
|
{ tolerancePixels: 3, cameraPixelSize: e.pixelSize }
|
||||||
);
|
);
|
||||||
props.onMouseMove?.({
|
props.onMouseMove?.({
|
||||||
camera,
|
camera,
|
||||||
scene,
|
scene,
|
||||||
hitResults,
|
hitTest,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
handleClick = (e: InteractionMouseEventArgs) => {
|
handleClick = (e: InteractionMouseEventArgs) => {
|
||||||
const hitResults = props.sceneHelper.hitTest(
|
const hitTest = HitTestFactory.hitTest(
|
||||||
e.position,
|
props.sceneHelper,
|
||||||
e.screenSize,
|
new THREE.Vector2(e.position.x, e.position.y),
|
||||||
|
camera,
|
||||||
|
{ tolerancePixels: 3, cameraPixelSize: e.pixelSize }
|
||||||
);
|
);
|
||||||
props.onClick?.({
|
props.onClick?.({
|
||||||
camera,
|
camera,
|
||||||
scene,
|
scene,
|
||||||
hitResults,
|
hitTest,
|
||||||
});
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Animation loop ---
|
// --- Animation loop ---
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
import { useSceneHelper } from "../helpers/hooks/useSceneHelper";
|
import { useSceneHelper } from "../helpers/hooks/useSceneHelper";
|
||||||
import { state } from "../state/root";
|
import { state } from "../state/root";
|
||||||
import { ThreeView, type ThreeViewEventArgs, type ThreeViewMouseEventArgs } from "./ThreeView";
|
import { ThreeView, type ThreeViewEventArgs, type ThreeViewMouseEventArgs } from "./ThreeVIew";
|
||||||
|
|
||||||
export const Viewport = function () {
|
export const Viewport = function () {
|
||||||
|
|
||||||
const sceneHelper = useSceneHelper();
|
const sceneHelper = useSceneHelper();
|
||||||
|
|
||||||
function handleMouseMove(e: ThreeViewMouseEventArgs) {
|
function handleMouseMove(e: ThreeViewMouseEventArgs) {
|
||||||
state.setHitTest(e.hitResults);
|
state.setHitTest(e.hitTest);
|
||||||
|
|
||||||
sceneHelper.clear();
|
sceneHelper.clearPoints();
|
||||||
if (e.hitResults.hits.length) {
|
if (e.hitTest.objects.length) {
|
||||||
e.hitResults.hits.forEach((hit) => {
|
e.hitTest.objects.forEach((obj) => {
|
||||||
sceneHelper.showPoint(hit.object.uuid, hit.point);
|
sceneHelper.setPoint(obj.object.uuid, obj.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,13 +20,11 @@ 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.hitTest.objects.map(hit => hit.object.userData.faceId);
|
||||||
// if (hoveredFaceIds.length)
|
// if (hoveredFaceIds.length)
|
||||||
// console.log(hoveredFaceIds);
|
// console.log(hoveredFaceIds);
|
||||||
|
|
||||||
sceneHelper.setSelection(hoveredFaceIds);
|
sceneHelper.setSelection(hoveredFaceIds);
|
||||||
|
|
||||||
sceneHelper.showMouseFrustum();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDispose(e: ThreeViewEventArgs): void {
|
function handleDispose(e: ThreeViewEventArgs): void {
|
||||||
|
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import * as THREE from 'three';
|
|
||||||
import {
|
|
||||||
computeBoundsTree, disposeBoundsTree,
|
|
||||||
computeBatchedBoundsTree, disposeBatchedBoundsTree, acceleratedRaycast,
|
|
||||||
} from 'three-mesh-bvh';
|
|
||||||
|
|
||||||
// Add the extension functions
|
|
||||||
THREE.BufferGeometry.prototype.computeBoundsTree = computeBoundsTree;
|
|
||||||
THREE.BufferGeometry.prototype.disposeBoundsTree = disposeBoundsTree;
|
|
||||||
THREE.Mesh.prototype.raycast = acceleratedRaycast;
|
|
||||||
|
|
||||||
THREE.BatchedMesh.prototype.computeBoundsTree = computeBatchedBoundsTree;
|
|
||||||
THREE.BatchedMesh.prototype.disposeBoundsTree = disposeBatchedBoundsTree;
|
|
||||||
THREE.BatchedMesh.prototype.raycast = acceleratedRaycast;
|
|
||||||
|
|
||||||
export const dummy = {};
|
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
import * as THREE from 'three';
|
|
||||||
|
|
||||||
export class CircularFrustum {
|
|
||||||
public readonly apex = new THREE.Vector3(); // Cone apex (camera position)
|
|
||||||
public readonly axisNormalized = new THREE.Vector3(); // normalized (unit) axis direction (camera → screen point)
|
|
||||||
|
|
||||||
public halfAngle: number = 0; // Half-angle of the cone in radians
|
|
||||||
public cosHalfAngle: number = 0; // cos(halfAngle) — cached
|
|
||||||
public sinHalfAngle: number = 0; // sin(halfAngle) — cached
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build a CircularFrustum from a screen-space point and pixel threshold.
|
|
||||||
*
|
|
||||||
* @param screenPointNormalized point in normalised device coordinates (NDC) [-1, 1]
|
|
||||||
* @param camera PerspectiveCamera or OrthographicCamera
|
|
||||||
* @param thresholdPx Screen-space radius in pixels
|
|
||||||
* @param screenSize renderer viewport size in pixels (e.g. from viewport.getBoundingClientRect())
|
|
||||||
*/
|
|
||||||
public setFromScreenPoint(
|
|
||||||
screenPointNormalized: THREE.Vector2Like, // in [-1, 1] space
|
|
||||||
screenSize: THREE.Vector2Like,
|
|
||||||
camera: THREE.PerspectiveCamera | THREE.OrthographicCamera,
|
|
||||||
thresholdPx: number, // in screen pixels
|
|
||||||
): void {
|
|
||||||
// cone apex
|
|
||||||
camera.getWorldPosition(this.apex);
|
|
||||||
|
|
||||||
// cone central ray (axis)
|
|
||||||
const centerNDC = new THREE.Vector3(screenPointNormalized.x, screenPointNormalized.y, 0.5); // center
|
|
||||||
const centerWorld = centerNDC.clone().unproject(camera);
|
|
||||||
this.axisNormalized.copy(centerWorld.sub(this.apex)).normalize();
|
|
||||||
|
|
||||||
// frustum near circle bottommost point
|
|
||||||
const bottomNDC = centerNDC.clone().add({ x: 0, y: (thresholdPx / screenSize.y) * 2, z: 0 });
|
|
||||||
const bottomWorld = bottomNDC.clone().unproject(camera);
|
|
||||||
const downAxis = bottomWorld.sub(this.apex).normalize();
|
|
||||||
|
|
||||||
// cone half-angle
|
|
||||||
const halfAngle = Math.acos(THREE.MathUtils.clamp(this.axisNormalized.dot(downAxis), -1, 1));
|
|
||||||
this.set(this.apex, this.axisNormalized, halfAngle);
|
|
||||||
|
|
||||||
// console.log({
|
|
||||||
// screenPointNormalized,
|
|
||||||
// screenSize,
|
|
||||||
// thresholdPx,
|
|
||||||
// centerNDC: centerNDC.toArray(),
|
|
||||||
// centerWorld: centerWorld.toArray(),
|
|
||||||
// bottomNDC: bottomNDC.toArray(),
|
|
||||||
// bottomWorld: bottomWorld.toArray(),
|
|
||||||
// downAxis: downAxis.toArray(),
|
|
||||||
// apex: this.apex.toArray(),
|
|
||||||
// axisNormalized: this.axisNormalized,
|
|
||||||
// halfAngle,
|
|
||||||
// });
|
|
||||||
// console.log(this.apex.toArray());
|
|
||||||
}
|
|
||||||
public getCircleAtDepth(depth: number): { center: THREE.Vector3Like, radius: number } {
|
|
||||||
const center = this.apex.clone().addScaledVector(this.axisNormalized, depth);
|
|
||||||
const radius = Math.tan(this.halfAngle) * depth;
|
|
||||||
|
|
||||||
return {
|
|
||||||
center,
|
|
||||||
radius,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private set(apex: THREE.Vector3Like, axisNormalized: THREE.Vector3Like, halfAngle: number) {
|
|
||||||
this.apex.copy(apex);
|
|
||||||
this.axisNormalized.copy(axisNormalized);
|
|
||||||
this.halfAngle = halfAngle;
|
|
||||||
this.cosHalfAngle = Math.cos(this.halfAngle);
|
|
||||||
this.sinHalfAngle = Math.sin(this.halfAngle);
|
|
||||||
}
|
|
||||||
|
|
||||||
public transform(matrix: THREE.Matrix4): CircularFrustum {
|
|
||||||
const transformed = new CircularFrustum();
|
|
||||||
|
|
||||||
const localApex = this.apex.clone().applyMatrix4(matrix);
|
|
||||||
const localAxis = this.axisNormalized.clone().transformDirection(matrix).normalize();
|
|
||||||
transformed.set(localApex, localAxis, this.halfAngle);
|
|
||||||
|
|
||||||
return transformed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,263 +0,0 @@
|
||||||
import * as THREE from 'three';
|
|
||||||
import { CONTAINED, ExtendedTriangle, INTERSECTED, NOT_INTERSECTED } from 'three-mesh-bvh';
|
|
||||||
import { CircularFrustum } from './circularFrustum';
|
|
||||||
|
|
||||||
export type HitResults = {
|
|
||||||
hits: HitResult[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type HitResult = {
|
|
||||||
object: THREE.Object3D;
|
|
||||||
point: THREE.Vector3; // world-space closest hit point
|
|
||||||
depth: number; // depth along frustum axis
|
|
||||||
triangle?: ExtendedTriangle; // only present when BVH was used
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CircularFrustumIntersectionOptions = {
|
|
||||||
findAll?: boolean; // defaults to false
|
|
||||||
filter?: (object: THREE.Object3D) => boolean; // defaults to every object
|
|
||||||
}
|
|
||||||
|
|
||||||
export type IntersectionResult = 'NOT_INTERSECTED' | 'INTERSECTED' | 'CONTAINED';
|
|
||||||
|
|
||||||
export function intersectionResultToBvh(value: IntersectionResult): typeof NOT_INTERSECTED | typeof INTERSECTED | typeof CONTAINED {
|
|
||||||
switch (value) {
|
|
||||||
case 'NOT_INTERSECTED':
|
|
||||||
return NOT_INTERSECTED;
|
|
||||||
case 'INTERSECTED':
|
|
||||||
return INTERSECTED;
|
|
||||||
case 'CONTAINED':
|
|
||||||
return CONTAINED;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CircularFrustumIntersection {
|
|
||||||
public readonly frustum: CircularFrustum;
|
|
||||||
|
|
||||||
constructor(frustum: CircularFrustum) {
|
|
||||||
this.frustum = frustum;
|
|
||||||
}
|
|
||||||
|
|
||||||
public insersectsSphere(sphere: THREE.Sphere): 'NOT_INTERSECTED' | number {
|
|
||||||
return CircularFrustumIntersection.insersectsSphere(sphere, this.frustum);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* sphere and frustum both should be in the same coordinate space (local or world)
|
|
||||||
*
|
|
||||||
* Uses the Barros / van den Bergen separating-axis approach:
|
|
||||||
* - Check whether the sphere centre is inside the cone (fast path)
|
|
||||||
* - Otherwise check the distance from the sphere centre to the
|
|
||||||
* nearest cone surface (lateral face + apex cap)
|
|
||||||
*
|
|
||||||
* @returns axial depth of sphere center or NOT_INTERSECTED
|
|
||||||
*/
|
|
||||||
public static insersectsSphere(sphere: THREE.Sphere, frustum: CircularFrustum): 'NOT_INTERSECTED' | number {
|
|
||||||
const toCenter = sphere.center.clone().sub(frustum.apex);
|
|
||||||
const axialDist = toCenter.dot(frustum.axisNormalized);
|
|
||||||
|
|
||||||
if (axialDist + sphere.radius < 0) // behind the apex entirely
|
|
||||||
return 'NOT_INTERSECTED';
|
|
||||||
|
|
||||||
const lateralDist = toCenter.clone().addScaledVector(frustum.axisNormalized, -axialDist).length();
|
|
||||||
const distToConeEdge = lateralDist * frustum.cosHalfAngle - axialDist * frustum.sinHalfAngle;
|
|
||||||
|
|
||||||
if (distToConeEdge > sphere.radius) // fully outside lateral surface
|
|
||||||
return 'NOT_INTERSECTED';
|
|
||||||
|
|
||||||
return axialDist;
|
|
||||||
}
|
|
||||||
|
|
||||||
public intersectsBox(box: THREE.Box3): IntersectionResult {
|
|
||||||
return CircularFrustumIntersection.intersectsBox(box, this.frustum);
|
|
||||||
}
|
|
||||||
|
|
||||||
// box and this.frustum both should be in the same coordinate space (local or world)
|
|
||||||
public static intersectsBox(box: THREE.Box3, frustum: CircularFrustum): IntersectionResult {
|
|
||||||
const sphere = new THREE.Sphere();
|
|
||||||
box.getBoundingSphere(sphere);
|
|
||||||
if (CircularFrustumIntersection.insersectsSphere(sphere, frustum) === 'NOT_INTERSECTED')
|
|
||||||
return 'NOT_INTERSECTED';
|
|
||||||
|
|
||||||
// Check if all 8 corners are inside — if so, CONTAINED
|
|
||||||
const corners = Array(8)
|
|
||||||
.fill(0)
|
|
||||||
.map((_, i) => new THREE.Vector3(
|
|
||||||
i & 1 ? box.max.x : box.min.x,
|
|
||||||
i & 2 ? box.max.y : box.min.y,
|
|
||||||
i & 4 ? box.max.z : box.min.z,
|
|
||||||
));
|
|
||||||
const allInside = corners.every((c) => CircularFrustumIntersection.pointAxialDepth(c, frustum) !== 'NOT_INTERSECTED');
|
|
||||||
return allInside
|
|
||||||
? 'CONTAINED'
|
|
||||||
: 'INTERSECTED';
|
|
||||||
}
|
|
||||||
|
|
||||||
public pointAxialDepth(point: THREE.Vector3): 'NOT_INTERSECTED' | number {
|
|
||||||
return CircularFrustumIntersection.pointAxialDepth(point, this.frustum);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static pointAxialDepth(point: THREE.Vector3, frustum: CircularFrustum): 'NOT_INTERSECTED' | number {
|
|
||||||
const toPoint = point.clone().sub(frustum.apex);
|
|
||||||
const dist = toPoint.length();
|
|
||||||
if (dist === 0)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
const axialDist = toPoint.dot(frustum.axisNormalized);
|
|
||||||
const cosAngle = axialDist / dist;
|
|
||||||
return cosAngle >= frustum.cosHalfAngle
|
|
||||||
? axialDist
|
|
||||||
: 'NOT_INTERSECTED';
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Local frustum construction ──────────────────────────────────────────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transform a world-space CircularFrustum into an object's local space.
|
|
||||||
* Note: halfAngle is only preserved exactly under uniform scale.
|
|
||||||
*/
|
|
||||||
private toObjectLocalSpace(invWorldMatrix: THREE.Matrix4): CircularFrustum {
|
|
||||||
return this.frustum.transform(invWorldMatrix);
|
|
||||||
}
|
|
||||||
|
|
||||||
public intersectMesh(
|
|
||||||
mesh: THREE.Mesh,
|
|
||||||
findAll: boolean,
|
|
||||||
): HitResult[] {
|
|
||||||
const geometry = mesh.geometry;
|
|
||||||
if (!geometry)
|
|
||||||
return [];
|
|
||||||
|
|
||||||
const matrix = mesh.matrixWorld;
|
|
||||||
const matrixInverted = matrix.clone().invert(); // world -> local matrix
|
|
||||||
const worldFrustum = this.frustum;
|
|
||||||
const localFrustum = this.toObjectLocalSpace(matrixInverted);
|
|
||||||
|
|
||||||
// quick check for bounding sphere
|
|
||||||
if (!geometry.boundingSphere)
|
|
||||||
geometry.computeBoundingSphere();
|
|
||||||
const boundingSphere = geometry.boundingSphere!.clone().applyMatrix4(matrix);
|
|
||||||
if (this.insersectsSphere(boundingSphere) === 'NOT_INTERSECTED')
|
|
||||||
return [];
|
|
||||||
|
|
||||||
const results: HitResult[] = [];
|
|
||||||
|
|
||||||
if (!geometry.boundsTree)
|
|
||||||
geometry.computeBoundsTree();
|
|
||||||
const bvh = geometry.boundsTree;
|
|
||||||
if (!bvh)
|
|
||||||
throw new Error('No BVH found for a mesh');
|
|
||||||
bvh.shapecast({
|
|
||||||
intersectsBounds: (box: THREE.Box3) => intersectionResultToBvh(CircularFrustumIntersection.intersectsBox(box, localFrustum)),
|
|
||||||
|
|
||||||
intersectsTriangle: (tri: ExtendedTriangle, _index: number, contained: boolean) => {
|
|
||||||
if (contained) {
|
|
||||||
const worldPoint = tri.a.clone().applyMatrix4(mesh.matrixWorld);
|
|
||||||
const depth = worldFrustum.axisNormalized.dot(worldPoint.clone().sub(worldFrustum.apex));
|
|
||||||
results.push({ object: mesh, point: worldPoint, depth, triangle: tri });
|
|
||||||
return !findAll;
|
|
||||||
}
|
|
||||||
|
|
||||||
let bestDepth = Infinity;
|
|
||||||
let bestLocal: THREE.Vector3 | undefined = undefined;
|
|
||||||
|
|
||||||
const tryPoint = (v: THREE.Vector3) => {
|
|
||||||
const d = CircularFrustumIntersection.pointAxialDepth(v, localFrustum);
|
|
||||||
if (d !== 'NOT_INTERSECTED' && (d as number) < bestDepth) {
|
|
||||||
bestDepth = d as number;
|
|
||||||
bestLocal = v.clone();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 1. Test vertices
|
|
||||||
tryPoint(tri.a);
|
|
||||||
tryPoint(tri.b);
|
|
||||||
tryPoint(tri.c);
|
|
||||||
|
|
||||||
// 2. For each edge, find the point closest to the frustum axis ray,
|
|
||||||
// and also the point closest to the apex.
|
|
||||||
// This catches triangles that straddle the cone surface.
|
|
||||||
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 edge = b.clone().sub(a);
|
|
||||||
const toA = a.clone().sub(localFrustum.apex);
|
|
||||||
|
|
||||||
// Closest point on edge segment to the axis ray
|
|
||||||
const edgeDir = edge.clone().normalize();
|
|
||||||
const axisDotEdge = localFrustum.axisNormalized.dot(edgeDir);
|
|
||||||
const denom = 1 - axisDotEdge * axisDotEdge;
|
|
||||||
|
|
||||||
if (Math.abs(denom) > 1e-10) {
|
|
||||||
const t = (
|
|
||||||
localFrustum.axisNormalized.dot(toA) * axisDotEdge
|
|
||||||
- toA.dot(edgeDir)
|
|
||||||
) / denom;
|
|
||||||
const edgeLen = edge.length();
|
|
||||||
const tClamped = Math.max(0, Math.min(edgeLen, t));
|
|
||||||
const pointOnEdge = a.clone().addScaledVector(edgeDir, tClamped);
|
|
||||||
tryPoint(pointOnEdge);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Closest point on edge to the apex itself
|
|
||||||
const tApex = Math.max(0, Math.min(1, -toA.dot(edge) / edge.lengthSq()));
|
|
||||||
tryPoint(a.clone().addScaledVector(edge, tApex));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Closest point on the triangle face to the apex
|
|
||||||
const closestOnFace = new THREE.Vector3();
|
|
||||||
tri.closestPointToPoint(localFrustum.apex, closestOnFace);
|
|
||||||
if (!isNaN(closestOnFace.x))
|
|
||||||
tryPoint(closestOnFace);
|
|
||||||
|
|
||||||
if (bestLocal !== undefined) {
|
|
||||||
const worldPoint = (bestLocal as THREE.Vector3).clone().applyMatrix4(mesh.matrixWorld);
|
|
||||||
const worldDepth = worldFrustum.axisNormalized.dot(worldPoint.clone().sub(worldFrustum.apex));
|
|
||||||
results.push({ object: mesh, point: worldPoint, depth: worldDepth, triangle: tri });
|
|
||||||
return !findAll;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
// } else {
|
|
||||||
// // ── Fallback: bounding box only ───────────────────────────────────
|
|
||||||
// if (!geometry.boundingBox)
|
|
||||||
// geometry.computeBoundingBox();
|
|
||||||
// const worldBox = geometry.boundingBox!.clone().applyMatrix4(mesh.matrixWorld);
|
|
||||||
// const boxResult = this.intersectsBox(worldBox.clone());
|
|
||||||
// if (boxResult !== NOT_INTERSECTED) {
|
|
||||||
// const center = new THREE.Vector3();
|
|
||||||
// worldBox.getCenter(center);
|
|
||||||
// const depth = this.frustum.axis.dot(center.clone().sub(this.frustum.apex));
|
|
||||||
// results.push({ object: mesh, point: center, depth });
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
public intersectObject(
|
|
||||||
obj: THREE.Object3D,
|
|
||||||
options: CircularFrustumIntersectionOptions = {},
|
|
||||||
): HitResult[] {
|
|
||||||
const results: HitResult[] = [];
|
|
||||||
|
|
||||||
obj.traverseVisible((object) => {
|
|
||||||
if (options.filter && !options.filter(object))
|
|
||||||
return;
|
|
||||||
if (!(object instanceof THREE.Mesh))
|
|
||||||
return;
|
|
||||||
|
|
||||||
results.push(...this.intersectMesh(object, !!options.findAll));
|
|
||||||
});
|
|
||||||
|
|
||||||
// sort closest first
|
|
||||||
results.sort((a, b) => a.depth - b.depth);
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import {
|
||||||
|
computeBoundsTree, disposeBoundsTree,
|
||||||
|
computeBatchedBoundsTree, disposeBatchedBoundsTree, acceleratedRaycast,
|
||||||
|
} from 'three-mesh-bvh';
|
||||||
|
import type { SceneSync } from '../layers/sceneSync';
|
||||||
|
import type { SceneHelper } from './sceneHelper';
|
||||||
|
|
||||||
|
// Add the extension functions
|
||||||
|
THREE.BufferGeometry.prototype.computeBoundsTree = computeBoundsTree;
|
||||||
|
THREE.BufferGeometry.prototype.disposeBoundsTree = disposeBoundsTree;
|
||||||
|
THREE.Mesh.prototype.raycast = acceleratedRaycast;
|
||||||
|
|
||||||
|
THREE.BatchedMesh.prototype.computeBoundsTree = computeBatchedBoundsTree;
|
||||||
|
THREE.BatchedMesh.prototype.disposeBoundsTree = disposeBatchedBoundsTree;
|
||||||
|
THREE.BatchedMesh.prototype.raycast = acceleratedRaycast;
|
||||||
|
|
||||||
|
export type HitTest = {
|
||||||
|
objects: THREE.Intersection<THREE.Object3D>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HitTestRaycasterOptions = {
|
||||||
|
cameraPixelSize: THREE.Vector2Like;
|
||||||
|
tolerancePixels: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HitTestOptions = HitTestRaycasterOptions & {
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HitTestFactory {
|
||||||
|
|
||||||
|
private static raycasters: [THREE.Vector2, THREE.Raycaster][] = Array(9).fill(0).map(() => [new THREE.Vector2(), new THREE.Raycaster()]);
|
||||||
|
|
||||||
|
private static setupRaycasters(cursor: THREE.Vector2, camera: THREE.PerspectiveCamera, options: HitTestRaycasterOptions) {
|
||||||
|
|
||||||
|
this.raycasters[0][0].copy(cursor);
|
||||||
|
this.raycasters[0][1].setFromCamera(cursor, camera);
|
||||||
|
|
||||||
|
const count = HitTestFactory.raycasters.length - 1;
|
||||||
|
const step = Math.PI * 2 / count;
|
||||||
|
|
||||||
|
for (let angle = 0, idx = 0; idx < count; angle += step, idx++) {
|
||||||
|
const pos = {
|
||||||
|
x: Math.cos(angle) * options.tolerancePixels * options.cameraPixelSize.x,
|
||||||
|
y: Math.sin(angle) * options.tolerancePixels * options.cameraPixelSize.y,
|
||||||
|
};
|
||||||
|
const v = HitTestFactory.raycasters[idx + 1][0];
|
||||||
|
v.copy(cursor).add(pos);
|
||||||
|
HitTestFactory.raycasters[idx + 1][1].setFromCamera(v, camera);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getRaycasterPosition(index: number): THREE.Vector2 {
|
||||||
|
return HitTestFactory.raycasters[index][0];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static get raycasterCount(): number {
|
||||||
|
return HitTestFactory.raycasters.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static hitTest(scene: SceneHelper, cursor: THREE.Vector2, camera: THREE.PerspectiveCamera, options: HitTestOptions): HitTest {
|
||||||
|
|
||||||
|
HitTestFactory.setupRaycasters(cursor, camera, options);
|
||||||
|
|
||||||
|
const objects: THREE.Object3D[] = scene.objects;
|
||||||
|
|
||||||
|
const hitTest: Record<string, THREE.Intersection<THREE.Object3D>> = {};
|
||||||
|
|
||||||
|
HitTestFactory.raycasters.forEach((raycaster) => {
|
||||||
|
const hits = raycaster[1].intersectObjects(objects);
|
||||||
|
for (const hit of hits) {
|
||||||
|
hitTest[hit.object.uuid] = hit;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
objects: Object.values(hitTest),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
import { useEffect, type RefObject } from "react";
|
import { useEffect, type RefObject } from "react";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { normalizeScreenPosition } from "../normalizeScreenPosition";
|
|
||||||
|
|
||||||
const CLICK_THRESHOLD = 2; // px
|
const CLICK_THRESHOLD = 2; // px
|
||||||
|
|
||||||
export type InteractionMouseEventArgs = {
|
export type InteractionMouseEventArgs = {
|
||||||
position: THREE.Vector2Like,
|
position: { x: number, y: number },
|
||||||
screenSize: THREE.Vector2Like,
|
pixelSize: { x: number, y: number },
|
||||||
pixelSize: THREE.Vector2Like,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UseInteractionOptions = {
|
export type UseInteractionOptions = {
|
||||||
|
|
@ -59,8 +57,14 @@ export function useInteraction(
|
||||||
|
|
||||||
const onMouseUp = (e: MouseEvent) => {
|
const onMouseUp = (e: MouseEvent) => {
|
||||||
isDragging = false;
|
isDragging = false;
|
||||||
if ((e.clientX - startX < CLICK_THRESHOLD) && (e.clientY - startY < CLICK_THRESHOLD))
|
if ((e.clientX - startX < CLICK_THRESHOLD) && (e.clientY - startY < CLICK_THRESHOLD)) {
|
||||||
options.onMouseClick?.(normalizeScreenPosition({ x: e.clientX, y: e.clientY }, target));
|
const rect = target.getBoundingClientRect();
|
||||||
|
const position = {
|
||||||
|
x: ((e.clientX - rect.left) / rect.width) * 2 - 1,
|
||||||
|
y: -((e.clientY - rect.top) / rect.height) * 2 + 1,
|
||||||
|
};
|
||||||
|
options.onMouseClick?.({ position, pixelSize: { x: 2 / rect.width, y: 2 / rect.height } });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onMouseMove = (e: MouseEvent) => {
|
const onMouseMove = (e: MouseEvent) => {
|
||||||
|
|
@ -80,7 +84,12 @@ export function useInteraction(
|
||||||
};
|
};
|
||||||
|
|
||||||
const onHover = (e: MouseEvent) => {
|
const onHover = (e: MouseEvent) => {
|
||||||
options.onMouseMove?.(normalizeScreenPosition({ x: e.clientX, y: e.clientY }, target));
|
const rect = target.getBoundingClientRect();
|
||||||
|
const position = {
|
||||||
|
x: ((e.clientX - rect.left) / rect.width) * 2 - 1,
|
||||||
|
y: -((e.clientY - rect.top) / rect.height) * 2 + 1,
|
||||||
|
};
|
||||||
|
options.onMouseMove?.({ position, pixelSize: { x: 2 / rect.width, y: 2 / rect.height } });
|
||||||
};
|
};
|
||||||
|
|
||||||
const onContextMenu = (e: Event) => e.preventDefault();
|
const onContextMenu = (e: Event) => e.preventDefault();
|
||||||
|
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
import type { Vector2Like } from "three";
|
|
||||||
|
|
||||||
export type NormalizedScreenPosition = {
|
|
||||||
position: Vector2Like,
|
|
||||||
screenSize: Vector2Like,
|
|
||||||
pixelSize: Vector2Like,
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizeScreenPosition(pos: Vector2Like, viewport: DOMRect | HTMLElement): NormalizedScreenPosition {
|
|
||||||
let rect: DOMRect;
|
|
||||||
if (viewport instanceof DOMRect)
|
|
||||||
rect = viewport;
|
|
||||||
else
|
|
||||||
rect = viewport.getBoundingClientRect();
|
|
||||||
|
|
||||||
return {
|
|
||||||
position: {
|
|
||||||
x: ((pos.x - rect.left) / rect.width) * 2 - 1,
|
|
||||||
y: -((pos.y - rect.top) / rect.height) * 2 + 1,
|
|
||||||
},
|
|
||||||
screenSize: { x: rect.width, y: rect.height },
|
|
||||||
pixelSize: {
|
|
||||||
x: 2 / rect.width,
|
|
||||||
y: 2 / rect.height,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
import * as THREE from "three";
|
||||||
|
|
||||||
|
export type Point3d = {
|
||||||
|
position: THREE.Vector3,
|
||||||
|
mesh: THREE.Mesh,
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Point3dHelper {
|
||||||
|
|
||||||
|
private scene: THREE.Scene;
|
||||||
|
private readonly baseMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff });
|
||||||
|
|
||||||
|
private readonly markers: Record<string, Point3d> = {};
|
||||||
|
|
||||||
|
constructor(scene: THREE.Scene) {
|
||||||
|
this.scene = scene;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ensure(id: string): Point3d {
|
||||||
|
if (!this.markers[id]) {
|
||||||
|
this.markers[id] = {
|
||||||
|
position: new THREE.Vector3(),
|
||||||
|
mesh: new THREE.Mesh(
|
||||||
|
new THREE.SphereGeometry(0.1, 8, 8),
|
||||||
|
this.baseMaterial,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
this.scene.add(this.markers[id].mesh);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.markers[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
private disposePoint(id: string) {
|
||||||
|
const point = this.markers[id];
|
||||||
|
if (point) {
|
||||||
|
this.scene.remove(point.mesh);
|
||||||
|
point.mesh.geometry.dispose();
|
||||||
|
delete (this.markers[id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispose() {
|
||||||
|
for (const id in this.markers)
|
||||||
|
this.disposePoint(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public set(id: string, position: THREE.Vector3Like) {
|
||||||
|
const point = this.ensure(id);
|
||||||
|
point.position.copy(position);
|
||||||
|
point.mesh.position.copy(position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,63 +1,26 @@
|
||||||
import type { ColorRepresentation, Object3D, Object3DEventMap, OrthographicCamera, PerspectiveCamera, Scene, Vector2Like, Vector3, Vector3Like } from "three";
|
import type { Object3D, Object3DEventMap, Scene, Vector3 } from "three";
|
||||||
|
import { Point3dHelper } from "./point3dHelper";
|
||||||
import { SceneSync } from "../layers/sceneSync";
|
import { SceneSync } from "../layers/sceneSync";
|
||||||
import { GeometryCache } from "../layers/geometryCache";
|
import { GeometryCache } from "../layers/geometryCache";
|
||||||
import type { Id } from "../types";
|
import type { Id } from "../types";
|
||||||
import { CircularFrustumIntersection, type HitResult, type HitResults } from "./circularFrustumIntersect";
|
|
||||||
import { CircularFrustum } from "./circularFrustum";
|
|
||||||
import './bvh';
|
|
||||||
import { VolatileGeometryHelper, type VolatileGeometryOptions } from "./volatileGeometryHelper";
|
|
||||||
|
|
||||||
export class SceneHelper {
|
export class SceneHelper {
|
||||||
|
|
||||||
private sync: SceneSync | undefined;
|
private sync: SceneSync | undefined;
|
||||||
|
|
||||||
private hints: VolatileGeometryHelper | undefined;
|
private pointHelper: Point3dHelper | undefined;
|
||||||
private camera: PerspectiveCamera | OrthographicCamera | undefined;
|
|
||||||
|
|
||||||
private mouseFrustum = new CircularFrustumIntersection(new CircularFrustum());
|
constructor() {
|
||||||
|
|
||||||
public initialize(
|
}
|
||||||
scene: Scene,
|
|
||||||
camera: PerspectiveCamera | OrthographicCamera,
|
public initialize(scene: Scene) {
|
||||||
) {
|
this.pointHelper = new Point3dHelper(scene);
|
||||||
this.hints = new VolatileGeometryHelper(scene, camera);
|
|
||||||
this.camera = camera;
|
|
||||||
|
|
||||||
this.sync = new SceneSync(scene, new GeometryCache());
|
this.sync = new SceneSync(scene, new GeometryCache());
|
||||||
this.sync.addWholeModel();
|
this.sync.addWholeModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
public buildMouseFrustum(
|
|
||||||
mouseNormalized: Vector2Like,
|
|
||||||
screenSize: Vector2Like,
|
|
||||||
radius: number = 5,
|
|
||||||
): void {
|
|
||||||
if (!this.camera)
|
|
||||||
throw new Error('Camera is not initialized');
|
|
||||||
|
|
||||||
this.mouseFrustum.frustum.setFromScreenPoint(
|
|
||||||
mouseNormalized,
|
|
||||||
screenSize,
|
|
||||||
this.camera,
|
|
||||||
radius,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public hitTest(
|
|
||||||
mouseNormalized: Vector2Like,
|
|
||||||
screenSize: Vector2Like,
|
|
||||||
): HitResults {
|
|
||||||
this.buildMouseFrustum(mouseNormalized, screenSize);
|
|
||||||
|
|
||||||
const hits: HitResult[] = [];
|
|
||||||
for (const object of this.objects)
|
|
||||||
hits.push(...this.mouseFrustum.intersectObject(object));
|
|
||||||
|
|
||||||
return {
|
|
||||||
hits,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public setSelection(faceIds: Id[]) {
|
public setSelection(faceIds: Id[]) {
|
||||||
this.sync?.setSelected(faceIds);
|
this.sync?.setSelected(faceIds);
|
||||||
}
|
}
|
||||||
|
|
@ -67,40 +30,17 @@ export class SceneHelper {
|
||||||
return this.sync?.meshes ?? [];
|
return this.sync?.meshes ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public showHint(id: string, position: Vector3Like, options: VolatileGeometryOptions) {
|
public setPoint(id: string, point: Vector3) {
|
||||||
this.hints?.set(id, position, options);
|
this.pointHelper?.set(id, point);
|
||||||
}
|
}
|
||||||
|
|
||||||
public showPoint(id: string, position: Vector3Like, size: number = 0.05, color: ColorRepresentation = 0xffffff) {
|
public clearPoints() {
|
||||||
this.hints?.set(id, position, { kind: "point", size, color });
|
this.pointHelper?.dispose();
|
||||||
}
|
|
||||||
|
|
||||||
public showCircle(id: string, position: Vector3Like, radius: number, thickness: number = 0.1, color: ColorRepresentation = 0xffffff) {
|
|
||||||
this.hints?.set(id, position, { kind: "circle", radius, thickness, color });
|
|
||||||
}
|
|
||||||
|
|
||||||
public showMouseFrustum() {
|
|
||||||
if (!this.camera)
|
|
||||||
throw new Error('Camera is not initialized');
|
|
||||||
|
|
||||||
const frustum = this.mouseFrustum.frustum;
|
|
||||||
const cameraDepth = this.camera.far - this.camera.near;
|
|
||||||
const nearDepth = this.camera.near + cameraDepth * 0.01;
|
|
||||||
const farDepth = this.camera.far - cameraDepth * 0.01;
|
|
||||||
const near = frustum.getCircleAtDepth(nearDepth);
|
|
||||||
const far = frustum.getCircleAtDepth(farDepth);
|
|
||||||
|
|
||||||
this.showCircle('hittest_near', near.center, near.radius, 0.05);
|
|
||||||
this.showCircle('hittest_far', far.center, far.radius, 0.1, 'red');
|
|
||||||
}
|
|
||||||
|
|
||||||
public clear() {
|
|
||||||
this.hints?.dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public dispose() {
|
public dispose() {
|
||||||
this.sync?.dispose();
|
this.sync?.dispose();
|
||||||
|
|
||||||
this.clear();
|
this.clearPoints();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
import * as THREE from "three";
|
|
||||||
|
|
||||||
export type VolatileGeometry = {
|
|
||||||
mesh: THREE.Mesh,
|
|
||||||
}
|
|
||||||
|
|
||||||
export type VolatileGeometryBaseOptions = {
|
|
||||||
color: THREE.ColorRepresentation,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export type VolatileGeometryPointOptions = VolatileGeometryBaseOptions & {
|
|
||||||
kind: 'point',
|
|
||||||
size: number,
|
|
||||||
}
|
|
||||||
|
|
||||||
export type VolatileGeometryCicleOptions = VolatileGeometryBaseOptions & {
|
|
||||||
kind: 'circle',
|
|
||||||
radius: number,
|
|
||||||
thickness: number,
|
|
||||||
}
|
|
||||||
|
|
||||||
export type VolatileGeometryOptions = VolatileGeometryPointOptions | VolatileGeometryCicleOptions;
|
|
||||||
|
|
||||||
export class VolatileGeometryHelper {
|
|
||||||
|
|
||||||
private scene: THREE.Scene;
|
|
||||||
private camera: THREE.PerspectiveCamera | THREE.OrthographicCamera;
|
|
||||||
|
|
||||||
private readonly baseMaterial = new THREE.MeshBasicMaterial({ color: 'red' });
|
|
||||||
|
|
||||||
private readonly markers: Record<string, VolatileGeometry> = {};
|
|
||||||
|
|
||||||
constructor(scene: THREE.Scene, camera: THREE.PerspectiveCamera | THREE.OrthographicCamera) {
|
|
||||||
this.scene = scene;
|
|
||||||
this.camera = camera;
|
|
||||||
}
|
|
||||||
|
|
||||||
private createGeometry(options: VolatileGeometryOptions): THREE.BufferGeometry {
|
|
||||||
switch (options.kind) {
|
|
||||||
case 'point':
|
|
||||||
return new THREE.SphereGeometry(options.size);
|
|
||||||
case 'circle':
|
|
||||||
return new THREE.TorusGeometry(options.radius, options.thickness * options.radius);
|
|
||||||
default:
|
|
||||||
throw new Error('Unknown volatile geometry type');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private ensure(id: string, options: VolatileGeometryOptions): VolatileGeometry {
|
|
||||||
if (!this.markers[id]) {
|
|
||||||
const material = this.baseMaterial.clone();
|
|
||||||
material.color.set(options.color);
|
|
||||||
this.markers[id] = {
|
|
||||||
mesh: new THREE.Mesh(
|
|
||||||
this.createGeometry(options),
|
|
||||||
material,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
this.scene.add(this.markers[id].mesh);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.markers[id];
|
|
||||||
}
|
|
||||||
|
|
||||||
private disposeGeometry(id: string) {
|
|
||||||
const point = this.markers[id];
|
|
||||||
if (point) {
|
|
||||||
this.scene.remove(point.mesh);
|
|
||||||
point.mesh.geometry.dispose();
|
|
||||||
delete (this.markers[id]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public dispose() {
|
|
||||||
for (const id in this.markers)
|
|
||||||
this.disposeGeometry(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public set(id: string, position: THREE.Vector3Like, options: VolatileGeometryOptions) {
|
|
||||||
const point = this.ensure(id, options);
|
|
||||||
|
|
||||||
// additional actions
|
|
||||||
switch (options.kind) {
|
|
||||||
case 'circle':
|
|
||||||
point.mesh.lookAt(this.camera.position);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
point.mesh.position.copy(position);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -38,9 +38,8 @@ export class SceneSync {
|
||||||
const meshes = Geometry
|
const meshes = Geometry
|
||||||
.tessellateSolid(id)
|
.tessellateSolid(id)
|
||||||
.map(meshToDto);
|
.map(meshToDto);
|
||||||
this.addSolid(meshes[2]);
|
for (const mesh of meshes)
|
||||||
// for (const mesh of meshes)
|
this.addSolid(mesh);
|
||||||
// this.addSolid(mesh);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { makeAutoObservable } from "mobx";
|
import { makeAutoObservable } from "mobx";
|
||||||
import type { Id } from "../types";
|
import type { Id } from "../types";
|
||||||
import type { HitResults } from "../helpers/circularFrustumIntersect";
|
import type { HitTest } from "../helpers/hitTest";
|
||||||
|
|
||||||
export class Root {
|
export class Root {
|
||||||
public selectedPrimitiveIds: Id[] = [];
|
public selectedPrimitiveIds: Id[] = [];
|
||||||
public hitTest: HitResults = { hits: [] };
|
public hitTest: HitTest = { objects: [] };
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
|
|
@ -14,7 +14,7 @@ export class Root {
|
||||||
this.selectedPrimitiveIds = value;
|
this.selectedPrimitiveIds = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setHitTest(value: HitResults) {
|
public setHitTest(value: HitTest) {
|
||||||
this.hitTest = value;
|
this.hitTest = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue