CAD/client/src/helpers/circularFrustum.ts

85 lines
3.5 KiB
TypeScript

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