Back to home page

EIC code displayed by LXR

 
 

    


Warning, /firebird/packages/firebird-ng/display/src/lib/services/picking.spec.ts is written in an unsupported language. File is not indexed.

0001 import {
0002   BoxGeometry,
0003   BufferGeometry,
0004   DoubleSide,
0005   Group,
0006   InstancedMesh,
0007   Mesh,
0008   MeshBasicMaterial,
0009   Plane,
0010   Raycaster,
0011   SphereGeometry,
0012   Vector3,
0013 } from 'three';
0014 import { acceleratedRaycast } from 'three-mesh-bvh';
0015 import {
0016   buildPickingBvh,
0017   clipPlaneSetsOf,
0018   collectBvhCandidates,
0019   intersectVisible,
0020   isBvhCandidate,
0021   isClickGesture,
0022   isPointClipped,
0023   setWedgePlanes,
0024   useAcceleratedRaycast,
0025 } from './picking';
0026 
0027 const material = new MeshBasicMaterial();
0028 
0029 /** A unit box centered on the Z axis at `z`. */
0030 function boxAt(z: number, name: string): Mesh {
0031   const mesh = new Mesh(new BoxGeometry(1, 1, 1), material);
0032   mesh.position.set(0, 0, z);
0033   mesh.name = name;
0034   mesh.updateMatrixWorld(true);
0035   return mesh;
0036 }
0037 
0038 /** A ray from z = 100 looking down -Z through the origin. */
0039 function rayDownZ(): Raycaster {
0040   return new Raycaster(new Vector3(0, 0, 100), new Vector3(0, 0, -1));
0041 }
0042 
0043 describe('clip test (picking matches what the renderer clips)', () => {
0044   // The wedge as ThreeService builds it: start 90, opening 90 removes the
0045   // quadrant x < 0, y > 0 (intersection mode: behind BOTH planes).
0046   function quarterWedge() {
0047     const a = new Plane();
0048     const b = new Plane();
0049     const clipIntersection = setWedgePlanes(a, b, 90, 90);
0050     return { a, b, clipIntersection };
0051   }
0052 
0053   it('a wedge under 180 degrees clips only the region behind both planes', () => {
0054     const { a, b, clipIntersection } = quarterWedge();
0055     expect(clipIntersection).toBe(true);
0056     const planes = clipPlaneSetsOf([{ enabled: true, clipIntersection, clippingPlanes: [a, b] }]);
0057 
0058     expect(isPointClipped(new Vector3(-5, 5, 0), planes)).toBe(true);   // removed quadrant
0059     expect(isPointClipped(new Vector3(5, 5, 0), planes)).toBe(false);   // visible
0060     expect(isPointClipped(new Vector3(-5, -5, 0), planes)).toBe(false); // visible
0061     expect(isPointClipped(new Vector3(5, -5, 0), planes)).toBe(false);  // visible
0062   });
0063 
0064   it('a wedge of 180 degrees or more clips on either plane (union mode)', () => {
0065     const a = new Plane();
0066     const b = new Plane();
0067     const clipIntersection = setWedgePlanes(a, b, 0, 180);
0068     expect(clipIntersection).toBe(false);
0069     const planes = clipPlaneSetsOf([{ enabled: true, clipIntersection, clippingPlanes: [a, b] }]);
0070     // Start 0, opening 180: plane A keeps y <= 0, plane B keeps y <= 0 too.
0071     expect(isPointClipped(new Vector3(3, 5, 0), planes)).toBe(true);
0072     expect(isPointClipped(new Vector3(3, -5, 0), planes)).toBe(false);
0073   });
0074 
0075   it('nested groups combine: the Z plane (union) clips on its own, disabled groups never clip', () => {
0076     const { a, b, clipIntersection } = quarterWedge();
0077     const zPlane = new Plane(new Vector3(0, 0, 1), 0); // keeps z >= 0
0078     const planes = clipPlaneSetsOf([
0079       { enabled: true, clipIntersection: false, clippingPlanes: [zPlane] },
0080       { enabled: true, clipIntersection, clippingPlanes: [a, b] },
0081     ]);
0082     expect(planes.union).toEqual([zPlane]);
0083     expect(planes.intersection).toEqual([a, b]);
0084     expect(isPointClipped(new Vector3(5, 5, -1), planes)).toBe(true);  // behind the Z plane
0085     expect(isPointClipped(new Vector3(5, 5, 1), planes)).toBe(false);
0086 
0087     const disabled = clipPlaneSetsOf([
0088       { enabled: false, clipIntersection: false, clippingPlanes: [zPlane] },
0089       { enabled: false, clipIntersection, clippingPlanes: [a, b] },
0090     ]);
0091     expect(isPointClipped(new Vector3(-5, 5, -1), disabled)).toBe(false);
0092   });
0093 
0094   it('a single intersection-mode plane (the per-view slice cut) clips like one plane', () => {
0095     const cut = new Plane(new Vector3(1, 0, 0), 0); // keeps x >= 0
0096     const planes = clipPlaneSetsOf([{ enabled: true, clipIntersection: true, clippingPlanes: [cut] }]);
0097     expect(isPointClipped(new Vector3(-1, 0, 0), planes)).toBe(true);
0098     expect(isPointClipped(new Vector3(1, 0, 0), planes)).toBe(false);
0099   });
0100 });
0101 
0102 describe('intersectVisible', () => {
0103   it('skips objects under an invisible ancestor, so hidden pieces do not block visible ones', () => {
0104     const root = new Group();
0105     const hiddenPiece = new Group();
0106     hiddenPiece.visible = false;
0107     hiddenPiece.add(boxAt(10, 'hidden-front'));
0108     root.add(hiddenPiece);
0109     root.add(boxAt(0, 'visible-back'));
0110     root.updateMatrixWorld(true);
0111 
0112     const hits = intersectVisible(rayDownZ(), [root]);
0113     expect(hits.length).toBeGreaterThan(0);
0114     expect(new Set(hits.map(hit => hit.object.name))).toEqual(new Set(['visible-back']));
0115   });
0116 
0117   it('returns hits sorted by distance across roots, and nothing from an invisible root', () => {
0118     const near = new Group();
0119     near.add(boxAt(20, 'near'));
0120     const far = new Group();
0121     far.add(boxAt(-20, 'far'));
0122     const hidden = new Group();
0123     hidden.visible = false;
0124     hidden.add(boxAt(50, 'hidden'));
0125     for (const group of [near, far, hidden]) group.updateMatrixWorld(true);
0126 
0127     const names = intersectVisible(rayDownZ(), [far, hidden, near]).map(hit => hit.object.name);
0128     expect(names[0]).toBe('near');
0129     expect(names).not.toContain('hidden');
0130     expect(names.indexOf('far')).toBeGreaterThan(names.lastIndexOf('near'));
0131   });
0132 });
0133 
0134 describe('isClickGesture', () => {
0135   it('accepts a release within the travel threshold and rejects a drag', () => {
0136     const down = { clientX: 100, clientY: 100 };
0137     expect(isClickGesture(down, { clientX: 100, clientY: 100 })).toBe(true);
0138     expect(isClickGesture(down, { clientX: 103, clientY: 104 })).toBe(true);  // 5 px
0139     expect(isClickGesture(down, { clientX: 104, clientY: 104 })).toBe(false); // 5.7 px
0140     expect(isClickGesture(down, { clientX: 160, clientY: 100 })).toBe(false); // an orbit
0141   });
0142 });
0143 
0144 describe('picking BVH', () => {
0145   it('accepts plain meshes and rejects instanced meshes, fat lines and position-less geometry', () => {
0146     expect(isBvhCandidate(new Mesh(new BoxGeometry(), material))).toBe(true);
0147     expect(isBvhCandidate(new InstancedMesh(new BoxGeometry(), material, 4))).toBe(false);
0148     const fatLine = new Mesh(new BoxGeometry(), material) as Mesh & { isLineSegments2?: boolean };
0149     fatLine.isLineSegments2 = true;
0150     expect(isBvhCandidate(fatLine)).toBe(false);
0151     expect(isBvhCandidate(new Mesh(new BufferGeometry(), material))).toBe(false);
0152     expect(isBvhCandidate(new Group())).toBe(false);
0153   });
0154 
0155   it('collects candidates of a subtree and routes them, and only them, through the accelerated raycast', () => {
0156     const root = new Group();
0157     const mesh = new Mesh(new BoxGeometry(), material);
0158     const instanced = new InstancedMesh(new BoxGeometry(), material, 2);
0159     root.add(mesh, instanced);
0160     expect(collectBvhCandidates(root)).toEqual([mesh]);
0161 
0162     useAcceleratedRaycast(root);
0163     expect(mesh.raycast).toBe(acceleratedRaycast);
0164     expect(instanced.raycast).toBe(InstancedMesh.prototype.raycast);
0165     // Never a prototype patch: other meshes keep three's raycast.
0166     expect(Mesh.prototype.raycast).not.toBe(acceleratedRaycast);
0167   });
0168 
0169   it('builds once per geometry, leaves the index buffer untouched and finds the same hits', () => {
0170     const geometry = new SphereGeometry(5, 32, 16);
0171     const indexBefore = geometry.index!.array.slice();
0172     const doubleSided = new MeshBasicMaterial({ side: DoubleSide });
0173     const plain = new Mesh(geometry.clone(), doubleSided);
0174     const accelerated = new Mesh(geometry, doubleSided);
0175     const copy = accelerated.clone(); // shares the geometry, as slice copies do
0176     for (const mesh of [plain, accelerated, copy]) mesh.updateMatrixWorld(true);
0177 
0178     expect(buildPickingBvh(accelerated)).toBe(true);
0179     expect(buildPickingBvh(copy)).toBe(false); // shared geometry: one BVH
0180     expect(copy.geometry.boundsTree).toBe(accelerated.geometry.boundsTree);
0181     expect([...geometry.index!.array]).toEqual([...indexBefore]);
0182 
0183     const ray = new Raycaster(new Vector3(0.3, 0.2, 20), new Vector3(0, 0, -1));
0184     const expected = ray.intersectObject(plain).map(hit => hit.distance.toFixed(6));
0185     const actual = ray.intersectObject(accelerated).map(hit => hit.distance.toFixed(6));
0186     expect(actual).toEqual(expected);
0187     expect(actual.length).toBe(2); // in and out through the double-sided sphere
0188   });
0189 
0190   it('leaves non-indexed geometry non-indexed', () => {
0191     const geometry = new BoxGeometry().toNonIndexed();
0192     const mesh = new Mesh(geometry, material);
0193     buildPickingBvh(mesh);
0194     expect(geometry.index).toBeNull();
0195     expect(geometry.boundsTree).toBeDefined();
0196   });
0197 });