@flighthq/picking 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ export * from './pickScene';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from './pickScene';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { Scene } from '@flighthq/scene';
2
+ import type { Camera, SceneHit } from '@flighthq/types';
3
+ export declare function pickScene(scene: Readonly<Scene>, camera: Readonly<Camera>, screenX: number, screenY: number, out: SceneHit): SceneHit | null;
4
+ //# sourceMappingURL=pickScene.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pickScene.d.ts","sourceRoot":"","sources":["../src/pickScene.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAE7C,OAAO,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAuC,MAAM,iBAAiB,CAAC;AAa7F,wBAAgB,SAAS,CACvB,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,EACtB,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,EACxB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,QAAQ,GACZ,QAAQ,GAAG,IAAI,CAkDjB"}
@@ -0,0 +1,115 @@
1
+ import { getCameraScreenToWorldRay } from '@flighthq/camera';
2
+ import { createAabb, createMatrix4, createRay3D, createVector3, getRay3DPointAt, intersectRay3DAabb, intersectRay3DTriangle, inverseMatrix4, } from '@flighthq/geometry';
3
+ import { getMeshGeometryTriangleCount, getMeshGeometryVertexPosition } from '@flighthq/mesh';
4
+ import { ensureNodeWorldTransformMatrix4, forEachNodeDescendant, getNodeWorldTransformMatrix4 } from '@flighthq/node';
5
+ import { getSceneNodeWorldBounds, isMesh } from '@flighthq/scene';
6
+ // Resolves the nearest Mesh hit by a camera pick ray through a scene, filling `out` and returning it,
7
+ // or `null` on a miss (sentinel, not a throw). `screenX`/`screenY` are normalized device coordinates
8
+ // in [-1, 1] (the viewport→NDC mapping is the caller's responsibility, mirroring the camera unproject).
9
+ //
10
+ // Bronze precision: per-node broad-phase against the world AABB, then a brute-force ray↔triangle test
11
+ // over the candidate `MeshGeometry` (a per-`MeshGeometry` BVH is a later additive optimization). The
12
+ // ray is transformed into each mesh's local space (one inverse matrix per mesh) rather than every
13
+ // vertex into world space; the local direction is intentionally left un-normalized so the parametric
14
+ // `t` stays in world-ray units and is comparable across meshes.
15
+ //
16
+ // Alias-safe: `out` is only written when a nearer hit is found; on a miss `out` is untouched.
17
+ export function pickScene(scene, camera, screenX, screenY, out) {
18
+ const aspect = camera.projection.kind === 'perspective' ? camera.projection.aspect : 1;
19
+ if (!getCameraScreenToWorldRay(_worldRay, camera, screenX, screenY, aspect))
20
+ return null;
21
+ let found = false;
22
+ let bestT = Infinity;
23
+ forEachNodeDescendant(scene, (node) => {
24
+ const mesh = node;
25
+ if (!isMesh(mesh))
26
+ return;
27
+ // Broad-phase: skip meshes whose world bounds the ray cannot reach.
28
+ getSceneNodeWorldBounds(_worldBounds, mesh);
29
+ if (intersectRay3DAabb(_worldRay, _worldBounds) < 0)
30
+ return;
31
+ // Narrow-phase: test in mesh-local space. Requires the world matrix to be invertible.
32
+ ensureNodeWorldTransformMatrix4(mesh);
33
+ if (!inverseMatrix4(_inverseWorld, getNodeWorldTransformMatrix4(mesh)))
34
+ return;
35
+ transformPointByMatrix4(_localRay.origin, _worldRay.origin, _inverseWorld.m);
36
+ transformDirectionByMatrix4(_localRay.direction, _worldRay.direction, _inverseWorld.m);
37
+ const geometry = mesh.geometry;
38
+ const indices = geometry.indices;
39
+ const triangleCount = getMeshGeometryTriangleCount(geometry);
40
+ for (let triangle = 0; triangle < triangleCount; triangle++) {
41
+ const base = triangle * 3;
42
+ const i0 = indices ? indices[base] : base;
43
+ const i1 = indices ? indices[base + 1] : base + 1;
44
+ const i2 = indices ? indices[base + 2] : base + 2;
45
+ getMeshGeometryVertexPosition(_a, geometry, i0);
46
+ getMeshGeometryVertexPosition(_b, geometry, i1);
47
+ getMeshGeometryVertexPosition(_c, geometry, i2);
48
+ const t = intersectRay3DTriangle(_localRay, _a, _b, _c);
49
+ if (t < 0 || t >= bestT)
50
+ continue;
51
+ bestT = t;
52
+ found = true;
53
+ out.node = mesh;
54
+ out.distance = t;
55
+ getRay3DPointAt(_worldPoint, _worldRay, t);
56
+ out.pointX = _worldPoint.x;
57
+ out.pointY = _worldPoint.y;
58
+ out.pointZ = _worldPoint.z;
59
+ getRay3DPointAt(_localPoint, _localRay, t);
60
+ writeBarycentric(out, _localPoint, _a, _b, _c);
61
+ }
62
+ });
63
+ return found ? out : null;
64
+ }
65
+ // Fills `out.u/v/w` with the barycentric weights of `p` within triangle `a,b,c` (weights of a, b, c
66
+ // respectively; `p = u*a + v*b + w*c`). Degenerate triangles collapse to `(1, 0, 0)`.
67
+ function writeBarycentric(out, p, a, b, c) {
68
+ const v0x = b.x - a.x, v0y = b.y - a.y, v0z = b.z - a.z;
69
+ const v1x = c.x - a.x, v1y = c.y - a.y, v1z = c.z - a.z;
70
+ const v2x = p.x - a.x, v2y = p.y - a.y, v2z = p.z - a.z;
71
+ const d00 = v0x * v0x + v0y * v0y + v0z * v0z;
72
+ const d01 = v0x * v1x + v0y * v1y + v0z * v1z;
73
+ const d11 = v1x * v1x + v1y * v1y + v1z * v1z;
74
+ const d20 = v2x * v0x + v2y * v0y + v2z * v0z;
75
+ const d21 = v2x * v1x + v2y * v1y + v2z * v1z;
76
+ const denom = d00 * d11 - d01 * d01;
77
+ if (denom === 0) {
78
+ out.u = 1;
79
+ out.v = 0;
80
+ out.w = 0;
81
+ return;
82
+ }
83
+ const inv = 1 / denom;
84
+ const v = (d11 * d20 - d01 * d21) * inv;
85
+ const w = (d00 * d21 - d01 * d20) * inv;
86
+ out.u = 1 - v - w;
87
+ out.v = v;
88
+ out.w = w;
89
+ }
90
+ // Transforms a point (w = 1) by a column-major 4x4 matrix array into `out`.
91
+ function transformPointByMatrix4(out, p, m) {
92
+ const x = p.x, y = p.y, z = p.z;
93
+ out.x = m[0] * x + m[4] * y + m[8] * z + m[12];
94
+ out.y = m[1] * x + m[5] * y + m[9] * z + m[13];
95
+ out.z = m[2] * x + m[6] * y + m[10] * z + m[14];
96
+ }
97
+ // Transforms a direction (w = 0, translation ignored) by a column-major 4x4 matrix array into `out`.
98
+ // The result is intentionally not normalized so a ray's parametric `t` is preserved across the
99
+ // world↔local transform.
100
+ function transformDirectionByMatrix4(out, d, m) {
101
+ const x = d.x, y = d.y, z = d.z;
102
+ out.x = m[0] * x + m[4] * y + m[8] * z;
103
+ out.y = m[1] * x + m[5] * y + m[9] * z;
104
+ out.z = m[2] * x + m[6] * y + m[10] * z;
105
+ }
106
+ const _worldRay = createRay3D();
107
+ const _localRay = createRay3D();
108
+ const _inverseWorld = createMatrix4();
109
+ const _worldBounds = createAabb();
110
+ const _a = createVector3();
111
+ const _b = createVector3();
112
+ const _c = createVector3();
113
+ const _localPoint = createVector3();
114
+ const _worldPoint = createVector3();
115
+ //# sourceMappingURL=pickScene.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pickScene.js","sourceRoot":"","sources":["../src/pickScene.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@flighthq/picking",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "src/**/*.test.ts",
16
+ "!dist/**/*.test.js",
17
+ "!dist/**/*.test.d.ts",
18
+ "!dist/**/*.test.js.map",
19
+ "!dist/**/*.test.d.ts.map"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc -b",
23
+ "clean": "tsc -b --clean",
24
+ "test": "vitest run --config vitest.config.ts",
25
+ "test:watch": "vitest --watch --config vitest.config.ts",
26
+ "prepack": "npm run clean && npm run clean:dist && npm run build",
27
+ "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
28
+ },
29
+ "dependencies": {
30
+ "@flighthq/camera": "0.1.0",
31
+ "@flighthq/geometry": "0.1.0",
32
+ "@flighthq/mesh": "0.1.0",
33
+ "@flighthq/node": "0.1.0",
34
+ "@flighthq/scene": "0.1.0",
35
+ "@flighthq/types": "0.1.0"
36
+ },
37
+ "devDependencies": {
38
+ "typescript": "^5.3.0"
39
+ },
40
+ "description": "3D picking: camera-ray scene selection (ray vs bounds + brute-force ray vs triangle)",
41
+ "sideEffects": false
42
+ }
@@ -0,0 +1,114 @@
1
+ import { createCamera, createPerspectiveProjection, setCameraViewMatrix4FromLookAt } from '@flighthq/camera';
2
+ import { createVector3 } from '@flighthq/geometry';
3
+ import { createBoxMeshGeometry } from '@flighthq/mesh';
4
+ import { addNodeChild } from '@flighthq/node';
5
+ import type { Scene } from '@flighthq/scene';
6
+ import { createMesh, createScene, createSceneNode, setSceneNodePosition } from '@flighthq/scene';
7
+ import type { Camera, Mesh, SceneHit } from '@flighthq/types';
8
+
9
+ import { pickScene } from './pickScene';
10
+
11
+ function makeCamera(): Camera {
12
+ const camera = createCamera({
13
+ far: 100,
14
+ near: 0.1,
15
+ projection: createPerspectiveProjection({ aspect: 1, fovY: Math.PI / 2 }),
16
+ });
17
+ // Look down -Z at the origin from z = 5.
18
+ setCameraViewMatrix4FromLookAt(camera, createVector3(0, 0, 5), createVector3(0, 0, 0), createVector3(0, 1, 0));
19
+ return camera;
20
+ }
21
+
22
+ function makeHit(): SceneHit {
23
+ return { distance: 0, node: null as unknown as Mesh, pointX: 0, pointY: 0, pointZ: 0, u: 0, v: 0, w: 0 };
24
+ }
25
+
26
+ function sceneWithCenteredBox(): { scene: Scene; mesh: Mesh } {
27
+ const scene = createScene();
28
+ const mesh = createMesh(createBoxMeshGeometry(2, 2, 2), []);
29
+ addNodeChild(scene, mesh);
30
+ return { scene, mesh };
31
+ }
32
+
33
+ describe('pickScene', () => {
34
+ it('fills barycentric weights that sum to one', () => {
35
+ const camera = makeCamera();
36
+ const { scene } = sceneWithCenteredBox();
37
+ const out = makeHit();
38
+
39
+ const hit = pickScene(scene, camera, 0, 0, out);
40
+
41
+ expect(hit).not.toBeNull();
42
+ expect((hit?.u ?? 0) + (hit?.v ?? 0) + (hit?.w ?? 0)).toBeCloseTo(1, 5);
43
+ });
44
+
45
+ it('returns null for a scene containing only non-mesh nodes', () => {
46
+ const camera = makeCamera();
47
+ const scene = createScene();
48
+ // A plain SceneNode is not a Mesh; isMesh() returns false for it.
49
+ const node = createSceneNode();
50
+ addNodeChild(scene, node);
51
+ const out = makeHit();
52
+
53
+ expect(pickScene(scene, camera, 0, 0, out)).toBeNull();
54
+ });
55
+
56
+ it('returns null when the ray misses every mesh', () => {
57
+ const camera = makeCamera();
58
+ const { scene } = sceneWithCenteredBox();
59
+ const out = makeHit();
60
+
61
+ // Far corner of the viewport points away from the small centred box.
62
+ expect(pickScene(scene, camera, 0.99, 0.99, out)).toBeNull();
63
+ });
64
+
65
+ it('returns null when the scene has no meshes', () => {
66
+ const camera = makeCamera();
67
+ const scene = createScene();
68
+ const out = makeHit();
69
+
70
+ expect(pickScene(scene, camera, 0, 0, out)).toBeNull();
71
+ });
72
+
73
+ it('returns the mesh hit by the center ray', () => {
74
+ const camera = makeCamera();
75
+ const { scene, mesh } = sceneWithCenteredBox();
76
+ const out = makeHit();
77
+
78
+ const hit = pickScene(scene, camera, 0, 0, out);
79
+
80
+ expect(hit).toBe(out);
81
+ expect(hit?.node).toBe(mesh);
82
+ // Front face of a 2x2x2 box centred at the origin is at z = 1; the ray travels -Z from z ~= 5.
83
+ expect(hit?.pointZ).toBeCloseTo(1, 3);
84
+ expect(hit?.pointX).toBeCloseTo(0, 3);
85
+ expect(hit?.pointY).toBeCloseTo(0, 3);
86
+ expect(hit?.distance).toBeGreaterThan(0);
87
+ });
88
+
89
+ it('returns the nearer of two overlapping meshes', () => {
90
+ // Camera is at z = 5 looking toward the origin in the -Z direction.
91
+ const camera = makeCamera();
92
+ const scene = createScene();
93
+
94
+ // meshFar is centred at the origin: front face at z = 1, back face at z = -1.
95
+ const meshFar = createMesh(createBoxMeshGeometry(2, 2, 2), []);
96
+ addNodeChild(scene, meshFar);
97
+
98
+ // meshNear is shifted to z = 2: front face at z = 3, back face at z = 1.
99
+ // The camera ray hits its front face first (distance ≈ 2 vs ≈ 4 for meshFar).
100
+ const meshNear = createMesh(createBoxMeshGeometry(2, 2, 2), []);
101
+ setSceneNodePosition(meshNear, 0, 0, 2);
102
+ addNodeChild(scene, meshNear);
103
+
104
+ const out = makeHit();
105
+ const hit = pickScene(scene, camera, 0, 0, out);
106
+
107
+ expect(hit).toBe(out);
108
+ expect(hit?.node).toBe(meshNear);
109
+ // Front face of meshNear is at z = 3.
110
+ expect(hit?.pointZ).toBeCloseTo(3, 3);
111
+ expect(hit?.distance).toBeGreaterThan(0);
112
+ expect(hit?.distance).toBeLessThan(3); // meshFar's front face would be at distance ≈ 4
113
+ });
114
+ });