Back to home page

EIC code displayed by LXR

 
 

    


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 }