Warning, /firebird/packages/firebird-ng/display/src/lib/services/picking.ts is written in an unsupported language. File is not indexed.
0001 /**
0002 * Picking helpers behind ThreeService's pointer handling: the clip test,
0003 * the visibility-aware raycast, the click gesture test and the picking BVH
0004 * builder. They hold no Angular or renderer state, so specs can pin them.
0005 */
0006
0007 import { BufferGeometry, Intersection, Mesh, Object3D, Plane, Raycaster, Vector3 } from 'three';
0008 import { MeshBVH, acceleratedRaycast } from 'three-mesh-bvh';
0009
0010 /** The clip settings of one `ClippingGroup` (or anything shaped like it). */
0011 export interface ClipGroupLike {
0012 enabled: boolean;
0013 clipIntersection: boolean;
0014 clippingPlanes: readonly Plane[];
0015 }
0016
0017 /**
0018 * The world-space planes that clip a point, split by how they combine. Built
0019 * the way three's ClippingContext accumulates a chain of nested clipping
0020 * groups: every enabled group adds its planes to one of the two sets.
0021 */
0022 export interface ClipPlaneSets {
0023 /** A point is clipped when it lies behind ANY of these planes. */
0024 union: Plane[];
0025 /** A point is clipped when it lies behind ALL of these planes (ignored when empty). */
0026 intersection: Plane[];
0027 }
0028
0029 const Z_AXIS = new Vector3(0, 0, 1);
0030
0031 /**
0032 * Points the two wedge planes at a pie slice around the beam (Z) axis:
0033 * plane A at `startAngleDeg`, plane B at `startAngleDeg + openingAngleDeg`,
0034 * both through the axis. Returns the clipping mode the slice needs:
0035 * intersection (true) below 180 degrees, where only the region behind BOTH
0036 * planes is removed, and union (false) from 180 degrees on.
0037 */
0038 export function setWedgePlanes(planeA: Plane, planeB: Plane, startAngleDeg: number, openingAngleDeg: number): boolean {
0039 const start = (startAngleDeg * Math.PI) / 180;
0040 const opening = (openingAngleDeg * Math.PI) / 180;
0041 planeA.normal.set(0, -1, 0).applyAxisAngle(Z_AXIS, start);
0042 planeA.constant = 0;
0043 planeB.normal.set(0, 1, 0).applyAxisAngle(Z_AXIS, start + opening);
0044 planeB.constant = 0;
0045 return openingAngleDeg < 180;
0046 }
0047
0048 /** Collects the plane sets of a chain of nested clipping groups, outermost first. */
0049 export function clipPlaneSetsOf(chain: readonly ClipGroupLike[]): ClipPlaneSets {
0050 const sets: ClipPlaneSets = { union: [], intersection: [] };
0051 for (const group of chain) {
0052 if (!group.enabled || group.clippingPlanes.length === 0) continue;
0053 (group.clipIntersection ? sets.intersection : sets.union).push(...group.clippingPlanes);
0054 }
0055 return sets;
0056 }
0057
0058 /**
0059 * True when the renderer discards a fragment at `point`. Mirrors three's
0060 * clipping shader: "behind" a plane means `plane.distanceToPoint(p) < 0`;
0061 * union planes clip on any, intersection planes only on all.
0062 */
0063 export function isPointClipped(point: Vector3, planes: ClipPlaneSets): boolean {
0064 for (const plane of planes.union) {
0065 if (plane.distanceToPoint(point) < 0) return true;
0066 }
0067 if (planes.intersection.length === 0) return false;
0068 for (const plane of planes.intersection) {
0069 if (plane.distanceToPoint(point) >= 0) return false;
0070 }
0071 return true;
0072 }
0073
0074 /**
0075 * Raycasts the given subtrees like `Raycaster.intersectObjects(roots, true)`,
0076 * but skips every object with an invisible ancestor (three's raycaster
0077 * ignores `visible`, so a hidden piece would block the visible objects
0078 * behind it). A root that is itself invisible yields no hits.
0079 */
0080 export function intersectVisible(raycaster: Raycaster, roots: readonly Object3D[]): Intersection[] {
0081 const hits: Intersection[] = [];
0082 const visit = (object: Object3D): void => {
0083 if (!object.visible) return;
0084 let propagate = true;
0085 if (object.layers.test(raycaster.layers)) {
0086 // Object3D.raycast may return false to stop descending, as in three.
0087 propagate = (object.raycast(raycaster, hits) as unknown) !== false;
0088 }
0089 if (!propagate) return;
0090 for (const child of object.children) visit(child);
0091 };
0092 for (const root of roots) visit(root);
0093 hits.sort((a, b) => a.distance - b.distance);
0094 return hits;
0095 }
0096
0097 /** Screen position of a pointer event, in CSS pixels. */
0098 export interface PointerPosition {
0099 clientX: number;
0100 clientY: number;
0101 }
0102
0103 /** Pointer travel, in CSS pixels, below which a press and release count as a click. */
0104 export const CLICK_MAX_TRAVEL_PX = 5;
0105
0106 /**
0107 * True when a press and release form a click rather than a drag (an orbit or
0108 * pan starts with a press too, and must not select).
0109 */
0110 export function isClickGesture(down: PointerPosition, up: PointerPosition, maxTravelPx = CLICK_MAX_TRAVEL_PX): boolean {
0111 return Math.hypot(up.clientX - down.clientX, up.clientY - down.clientY) <= maxTravelPx;
0112 }
0113
0114 /**
0115 * True for meshes whose triangles a MeshBVH can accelerate. Instanced,
0116 * batched and skinned meshes place triangles through per-instance or
0117 * per-bone transforms the BVH does not see; fat lines (Line2,
0118 * LineSegments2) are meshes of instanced quads with their own raycast.
0119 */
0120 export function isBvhCandidate(object: Object3D): object is Mesh {
0121 const flags = object as unknown as Record<string, unknown>;
0122 if (flags['isMesh'] !== true) return false;
0123 if (flags['isInstancedMesh'] || flags['isBatchedMesh'] || flags['isSkinnedMesh'] || flags['isLineSegments2']) {
0124 return false;
0125 }
0126 const geometry = (object as Mesh).geometry as BufferGeometry | undefined;
0127 return !!geometry?.attributes?.['position'];
0128 }
0129
0130 /** The BVH candidates of a subtree, in traversal order. */
0131 export function collectBvhCandidates(root: Object3D): Mesh[] {
0132 const meshes: Mesh[] = [];
0133 root.traverse(object => {
0134 if (isBvhCandidate(object)) meshes.push(object);
0135 });
0136 return meshes;
0137 }
0138
0139 /**
0140 * Routes a subtree's candidate meshes through three-mesh-bvh's raycast.
0141 * Per mesh, never on `Mesh.prototype`: other meshes in the host app keep
0142 * three's own raycast. A mesh without a BVH yet falls back to three's
0143 * raycast inside `acceleratedRaycast`, so this is safe before the build.
0144 */
0145 export function useAcceleratedRaycast(root: Object3D): void {
0146 root.traverse(object => {
0147 if (isBvhCandidate(object)) object.raycast = acceleratedRaycast;
0148 });
0149 }
0150
0151 /**
0152 * Builds the picking BVH of one mesh's geometry, once per geometry (copies
0153 * that share the geometry share the BVH). Indirect mode leaves the
0154 * geometry's index buffer untouched, so building never changes what or in
0155 * which order the GPU draws. Returns false when the geometry already had one.
0156 */
0157 export function buildPickingBvh(mesh: Mesh): boolean {
0158 const geometry = mesh.geometry as BufferGeometry & { boundsTree?: MeshBVH };
0159 mesh.raycast = acceleratedRaycast;
0160 if (geometry.boundsTree) return false;
0161 geometry.boundsTree = new MeshBVH(geometry, { indirect: true });
0162 return true;
0163 }