@flighthq/picking 0.1.0 → 0.2.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.
@@ -1,4 +1,13 @@
1
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;
2
+ import type { Camera, Mesh, Ray3D, SceneHit } from '@flighthq/types';
3
+ export interface ScenePickOptions {
4
+ cullBackfaces?: boolean;
5
+ maxDistance?: number;
6
+ predicate?: (mesh: Readonly<Mesh>) => boolean;
7
+ }
8
+ export declare function createSceneHit(): SceneHit;
9
+ export declare function pickScene(scene: Readonly<Scene>, camera: Readonly<Camera>, screenX: number, screenY: number, out: SceneHit, options?: Readonly<ScenePickOptions>): SceneHit | null;
10
+ export declare function pickSceneAll(scene: Readonly<Scene>, camera: Readonly<Camera>, screenX: number, screenY: number, outArray: SceneHit[], options?: Readonly<ScenePickOptions>): SceneHit[];
11
+ export declare function pickSceneAllWithRay3D(scene: Readonly<Scene>, ray: Readonly<Ray3D>, outArray: SceneHit[], options?: Readonly<ScenePickOptions>): SceneHit[];
12
+ export declare function pickSceneWithRay3D(scene: Readonly<Scene>, ray: Readonly<Ray3D>, out: SceneHit, options?: Readonly<ScenePickOptions>): SceneHit | null;
4
13
  //# sourceMappingURL=pickScene.d.ts.map
@@ -1 +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"}
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,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAsB,MAAM,iBAAiB,CAAC;AASzF,MAAM,WAAW,gBAAgB;IAC/B,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC,KAAK,OAAO,CAAC;CAC/C;AAID,wBAAgB,cAAc,IAAI,QAAQ,CAezC;AAQD,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,EACb,OAAO,CAAC,EAAE,QAAQ,CAAC,gBAAgB,CAAC,GACnC,QAAQ,GAAG,IAAI,CAGjB;AAKD,wBAAgB,YAAY,CAC1B,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,EACtB,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,EACxB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,QAAQ,EAAE,EACpB,OAAO,CAAC,EAAE,QAAQ,CAAC,gBAAgB,CAAC,GACnC,QAAQ,EAAE,CAMZ;AAMD,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,EACtB,GAAG,EAAE,QAAQ,CAAC,KAAK,CAAC,EACpB,QAAQ,EAAE,QAAQ,EAAE,EACpB,OAAO,CAAC,EAAE,QAAQ,CAAC,gBAAgB,CAAC,GACnC,QAAQ,EAAE,CAcZ;AAcD,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,EACtB,GAAG,EAAE,QAAQ,CAAC,KAAK,CAAC,EACpB,GAAG,EAAE,QAAQ,EACb,OAAO,CAAC,EAAE,QAAQ,CAAC,gBAAgB,CAAC,GACnC,QAAQ,GAAG,IAAI,CAUjB"}
package/dist/pickScene.js CHANGED
@@ -1,11 +1,70 @@
1
1
  import { getCameraScreenToWorldRay } from '@flighthq/camera';
2
2
  import { createAabb, createMatrix4, createRay3D, createVector3, getRay3DPointAt, intersectRay3DAabb, intersectRay3DTriangle, inverseMatrix4, } from '@flighthq/geometry';
3
3
  import { getMeshGeometryTriangleCount, getMeshGeometryVertexPosition } from '@flighthq/mesh';
4
- import { ensureNodeWorldTransformMatrix4, forEachNodeDescendant, getNodeWorldTransformMatrix4 } from '@flighthq/node';
4
+ import { ensureNodeWorldTransformMatrix4, getNodeRuntime, getNodeWorldTransformMatrix4 } from '@flighthq/node';
5
5
  import { getSceneNodeWorldBounds, isMesh } from '@flighthq/scene';
6
+ // Allocates a zeroed SceneHit. Handy for the `out` of `pickScene`/`pickSceneWithRay3D` and for
7
+ // growing the `outArray` of the multi-hit queries. `node` is null until a pick fills it.
8
+ export function createSceneHit() {
9
+ return {
10
+ distance: 0,
11
+ node: null,
12
+ normalX: 0,
13
+ normalY: 0,
14
+ normalZ: 0,
15
+ pointX: 0,
16
+ pointY: 0,
17
+ pointZ: 0,
18
+ triangleIndex: -1,
19
+ u: 0,
20
+ v: 0,
21
+ w: 0,
22
+ };
23
+ }
6
24
  // Resolves the nearest Mesh hit by a camera pick ray through a scene, filling `out` and returning it,
7
25
  // or `null` on a miss (sentinel, not a throw). `screenX`/`screenY` are normalized device coordinates
8
26
  // in [-1, 1] (the viewport→NDC mapping is the caller's responsibility, mirroring the camera unproject).
27
+ // A thin wrapper: it unprojects the camera ray, then delegates to `pickSceneWithRay3D`.
28
+ //
29
+ // Alias-safe: `out` is only written when a hit is found; on a miss `out` is untouched.
30
+ export function pickScene(scene, camera, screenX, screenY, out, options) {
31
+ if (!buildCameraPickRay(_cameraRay, camera, screenX, screenY))
32
+ return null;
33
+ return pickSceneWithRay3D(scene, _cameraRay, out, options);
34
+ }
35
+ // Collects every Mesh hit by a camera pick ray into `outArray`, sorted by ascending distance, and
36
+ // returns `outArray`. A thin wrapper over `pickSceneAllWithRay3D` that first unprojects the camera
37
+ // ray; on an unprojectable camera the array is emptied and returned.
38
+ export function pickSceneAll(scene, camera, screenX, screenY, outArray, options) {
39
+ if (!buildCameraPickRay(_cameraRay, camera, screenX, screenY)) {
40
+ outArray.length = 0;
41
+ return outArray;
42
+ }
43
+ return pickSceneAllWithRay3D(scene, _cameraRay, outArray, options);
44
+ }
45
+ // Collects every Mesh hit by a world-space `ray` into `outArray`, sorted by ascending distance, and
46
+ // returns `outArray` (its `length` is set to the hit count; empty on a miss). Existing slots are
47
+ // reused and new ones allocated as needed, so the array can be kept and refilled across picks. The
48
+ // general primitive behind `pickSceneAll`; the ray need not be unit-length (see `ScenePickOptions`).
49
+ export function pickSceneAllWithRay3D(scene, ray, outArray, options) {
50
+ let count = 0;
51
+ forEachSceneRayHit(scene, ray, options, (hit) => {
52
+ let slot = outArray[count];
53
+ if (slot === undefined) {
54
+ slot = createSceneHit();
55
+ outArray[count] = slot;
56
+ }
57
+ copySceneHit(slot, hit);
58
+ count++;
59
+ });
60
+ outArray.length = count;
61
+ outArray.sort(compareSceneHitByDistance);
62
+ return outArray;
63
+ }
64
+ // Resolves the nearest Mesh hit by a world-space `ray` through a scene, filling `out` and returning
65
+ // it, or `null` on a miss. The general primitive behind `pickScene`: `pickScene` builds the ray from
66
+ // a camera and delegates here. The ray direction need not be unit-length; `t` (and `out.distance`)
67
+ // is in direction-length units.
9
68
  //
10
69
  // Bronze precision: per-node broad-phase against the world AABB, then a brute-force ray↔triangle test
11
70
  // over the candidate `MeshGeometry` (a per-`MeshGeometry` BVH is a later additive optimization). The
@@ -14,54 +73,148 @@ import { getSceneNodeWorldBounds, isMesh } from '@flighthq/scene';
14
73
  // `t` stays in world-ray units and is comparable across meshes.
15
74
  //
16
75
  // 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;
76
+ export function pickSceneWithRay3D(scene, ray, out, options) {
21
77
  let found = false;
22
78
  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)
79
+ forEachSceneRayHit(scene, ray, options, (hit) => {
80
+ if (hit.distance >= bestT)
30
81
  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
- }
82
+ bestT = hit.distance;
83
+ found = true;
84
+ copySceneHit(out, hit);
62
85
  });
63
86
  return found ? out : null;
64
87
  }
88
+ // Unprojects a camera pick ray into `out`. Orthographic projections ignore `aspect` (their extents
89
+ // are the half-width/half-height on the descriptor), so the passed value only matters for
90
+ // perspective, where it must be the projection's own aspect to avoid a horizontal ray skew.
91
+ function buildCameraPickRay(out, camera, screenX, screenY) {
92
+ const aspect = camera.projection.kind === 'perspective' ? camera.projection.aspect : 1;
93
+ return getCameraScreenToWorldRay(out, camera, screenX, screenY, aspect);
94
+ }
95
+ // Field-copies `src` into `out`. SceneHit is a plain result struct (no runtime identity), so a shallow
96
+ // field copy is a full copy.
97
+ function copySceneHit(out, src) {
98
+ out.node = src.node;
99
+ out.distance = src.distance;
100
+ out.triangleIndex = src.triangleIndex;
101
+ out.u = src.u;
102
+ out.v = src.v;
103
+ out.w = src.w;
104
+ out.pointX = src.pointX;
105
+ out.pointY = src.pointY;
106
+ out.pointZ = src.pointZ;
107
+ out.normalX = src.normalX;
108
+ out.normalY = src.normalY;
109
+ out.normalZ = src.normalZ;
110
+ }
111
+ // Ascending-distance comparator for `Array.sort` over SceneHit results.
112
+ function compareSceneHitByDistance(a, b) {
113
+ return a.distance - b.distance;
114
+ }
115
+ // Walks every enabled Mesh in the scene, tests `ray` against its triangles under `options`, and calls
116
+ // `onHit` once per passing triangle with the fully-populated shared `_hit` scratch. Disabled nodes
117
+ // (`enabled === false`) prune their whole subtree, mirroring scene culling; the `predicate` option
118
+ // excludes an individual mesh without pruning its descendants. `onHit` must consume `_hit` before it
119
+ // returns — the scratch is overwritten on the next triangle.
120
+ //
121
+ // Non-reentrant (C/C++ port note): this pass and its narrow-phase run on module-level scratch
122
+ // (`_hit`, `_localRay`, the vertex/point/normal temporaries, and the shared `_cameraRay` filled by the
123
+ // camera wrappers). It must not be called re-entrantly, and `onHit` must not itself start another
124
+ // pick. A native port replaces this shared scratch with per-call stack storage.
125
+ function forEachSceneRayHit(scene, ray, options, onHit) {
126
+ const predicate = options?.predicate;
127
+ const maxDistance = options?.maxDistance ?? Infinity;
128
+ const cullBackfaces = options?.cullBackfaces ?? false;
129
+ pickNode(scene, ray, predicate, maxDistance, cullBackfaces, onHit);
130
+ }
131
+ // Depth-first pruning walk: a disabled node contributes nothing and skips its subtree; an enabled
132
+ // Mesh is narrow-phase tested; every enabled node's children are descended.
133
+ function pickNode(node, ray, predicate, maxDistance, cullBackfaces, onHit) {
134
+ if (!node.enabled)
135
+ return;
136
+ if (isMesh(node) && (predicate === undefined || predicate(node))) {
137
+ intersectMeshTriangles(node, ray, maxDistance, cullBackfaces, onHit);
138
+ }
139
+ const children = getNodeRuntime(node).children;
140
+ if (children !== null) {
141
+ for (let i = 0; i < children.length; i++) {
142
+ pickNode(children[i], ray, predicate, maxDistance, cullBackfaces, onHit);
143
+ }
144
+ }
145
+ }
146
+ // Broad-phase (world AABB) then brute-force ray↔triangle narrow-phase for one mesh, reporting each
147
+ // passing triangle through `onHit`.
148
+ function intersectMeshTriangles(mesh, ray, maxDistance, cullBackfaces, onHit) {
149
+ getSceneNodeWorldBounds(_worldBounds, mesh);
150
+ if (intersectRay3DAabb(ray, _worldBounds) < 0)
151
+ return;
152
+ ensureNodeWorldTransformMatrix4(mesh);
153
+ const worldMatrix = getNodeWorldTransformMatrix4(mesh);
154
+ if (!inverseMatrix4(_inverseWorld, worldMatrix))
155
+ return;
156
+ transformPointByMatrix4(_localRay.origin, ray.origin, _inverseWorld.m);
157
+ transformDirectionByMatrix4(_localRay.direction, ray.direction, _inverseWorld.m);
158
+ const geometry = mesh.geometry;
159
+ const indices = geometry.indices;
160
+ const triangleCount = getMeshGeometryTriangleCount(geometry);
161
+ for (let triangle = 0; triangle < triangleCount; triangle++) {
162
+ const base = triangle * 3;
163
+ const i0 = indices ? indices[base] : base;
164
+ const i1 = indices ? indices[base + 1] : base + 1;
165
+ const i2 = indices ? indices[base + 2] : base + 2;
166
+ getMeshGeometryVertexPosition(_a, geometry, i0);
167
+ getMeshGeometryVertexPosition(_b, geometry, i1);
168
+ getMeshGeometryVertexPosition(_c, geometry, i2);
169
+ const t = intersectRay3DTriangle(_localRay, _a, _b, _c);
170
+ if (t < 0 || t > maxDistance)
171
+ continue;
172
+ // World-space geometric face normal (normalized cross of two world edges). Transforming the
173
+ // three verts by the world matrix — rather than the local normal by the inverse-transpose —
174
+ // keeps the sign correct under mirroring/negative scale and yields the world normal directly.
175
+ transformPointByMatrix4(_wa, _a, worldMatrix.m);
176
+ transformPointByMatrix4(_wb, _b, worldMatrix.m);
177
+ transformPointByMatrix4(_wc, _c, worldMatrix.m);
178
+ if (!writeFaceNormal(_worldNormal, _wa, _wb, _wc))
179
+ continue;
180
+ // Back-facing when the ray runs along the same side as the normal (dot > 0). Only the sign
181
+ // matters, so the un-normalized ray direction is fine here.
182
+ if (cullBackfaces &&
183
+ ray.direction.x * _worldNormal.x + ray.direction.y * _worldNormal.y + ray.direction.z * _worldNormal.z > 0) {
184
+ continue;
185
+ }
186
+ _hit.node = mesh;
187
+ _hit.distance = t;
188
+ _hit.triangleIndex = triangle;
189
+ _hit.normalX = _worldNormal.x;
190
+ _hit.normalY = _worldNormal.y;
191
+ _hit.normalZ = _worldNormal.z;
192
+ getRay3DPointAt(_worldPoint, ray, t);
193
+ _hit.pointX = _worldPoint.x;
194
+ _hit.pointY = _worldPoint.y;
195
+ _hit.pointZ = _worldPoint.z;
196
+ getRay3DPointAt(_localPoint, _localRay, t);
197
+ writeBarycentric(_hit, _localPoint, _a, _b, _c);
198
+ onHit(_hit);
199
+ }
200
+ }
201
+ // Writes the unit geometric face normal of triangle `a,b,c` (normalized `(b-a) × (c-a)`) into `out`,
202
+ // returning `false` for a degenerate (zero-area) triangle, in which case `out` is left untouched.
203
+ function writeFaceNormal(out, a, b, c) {
204
+ const e1x = b.x - a.x, e1y = b.y - a.y, e1z = b.z - a.z;
205
+ const e2x = c.x - a.x, e2y = c.y - a.y, e2z = c.z - a.z;
206
+ const nx = e1y * e2z - e1z * e2y;
207
+ const ny = e1z * e2x - e1x * e2z;
208
+ const nz = e1x * e2y - e1y * e2x;
209
+ const lengthSquared = nx * nx + ny * ny + nz * nz;
210
+ if (lengthSquared === 0)
211
+ return false;
212
+ const inv = 1 / Math.sqrt(lengthSquared);
213
+ out.x = nx * inv;
214
+ out.y = ny * inv;
215
+ out.z = nz * inv;
216
+ return true;
217
+ }
65
218
  // Fills `out.u/v/w` with the barycentric weights of `p` within triangle `a,b,c` (weights of a, b, c
66
219
  // respectively; `p = u*a + v*b + w*c`). Degenerate triangles collapse to `(1, 0, 0)`.
67
220
  function writeBarycentric(out, p, a, b, c) {
@@ -103,13 +256,20 @@ function transformDirectionByMatrix4(out, d, m) {
103
256
  out.y = m[1] * x + m[5] * y + m[9] * z;
104
257
  out.z = m[2] * x + m[6] * y + m[10] * z;
105
258
  }
106
- const _worldRay = createRay3D();
259
+ // Module-level scratch. This makes the pick pass non-reentrant (see forEachSceneRayHit) — a C/C++
260
+ // port moves these to per-call stack storage.
261
+ const _cameraRay = createRay3D();
107
262
  const _localRay = createRay3D();
108
263
  const _inverseWorld = createMatrix4();
109
264
  const _worldBounds = createAabb();
110
265
  const _a = createVector3();
111
266
  const _b = createVector3();
112
267
  const _c = createVector3();
268
+ const _wa = createVector3();
269
+ const _wb = createVector3();
270
+ const _wc = createVector3();
271
+ const _worldNormal = createVector3();
113
272
  const _localPoint = createVector3();
114
273
  const _worldPoint = createVector3();
274
+ const _hit = createSceneHit();
115
275
  //# sourceMappingURL=pickScene.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"pickScene.js","sourceRoot":"","sources":["../src/pickScene.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"pickScene.js","sourceRoot":"","sources":["../src/pickScene.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,11 @@
1
1
  {
2
2
  "name": "@flighthq/picking",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "https://github.com/flighthq/flight.git",
7
+ "directory": "packages/picking"
8
+ },
4
9
  "type": "module",
5
10
  "main": "dist/index.js",
6
11
  "types": "dist/index.d.ts",
@@ -27,12 +32,12 @@
27
32
  "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
28
33
  },
29
34
  "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"
35
+ "@flighthq/camera": "0.2.0",
36
+ "@flighthq/geometry": "0.2.0",
37
+ "@flighthq/mesh": "0.2.0",
38
+ "@flighthq/node": "0.2.0",
39
+ "@flighthq/scene": "0.2.0",
40
+ "@flighthq/types": "0.2.0"
36
41
  },
37
42
  "devDependencies": {
38
43
  "typescript": "^5.3.0"
@@ -1,12 +1,24 @@
1
- import { createCamera, createPerspectiveProjection, setCameraViewMatrix4FromLookAt } from '@flighthq/camera';
2
- import { createVector3 } from '@flighthq/geometry';
3
- import { createBoxMeshGeometry } from '@flighthq/mesh';
1
+ import {
2
+ createCamera,
3
+ createOrthographicProjection,
4
+ createPerspectiveProjection,
5
+ setCameraViewMatrix4FromLookAt,
6
+ } from '@flighthq/camera';
7
+ import { createQuaternion, createRay3D, createVector3, setQuaternionFromAxisAngle, setRay3D } from '@flighthq/geometry';
8
+ import { createBoxMeshGeometry, createMeshGeometryFromAttributes } from '@flighthq/mesh';
4
9
  import { addNodeChild } from '@flighthq/node';
5
10
  import type { Scene } from '@flighthq/scene';
6
- import { createMesh, createScene, createSceneNode, setSceneNodePosition } from '@flighthq/scene';
7
- import type { Camera, Mesh, SceneHit } from '@flighthq/types';
11
+ import {
12
+ createMesh,
13
+ createScene,
14
+ createSceneNode,
15
+ setSceneNodePosition,
16
+ setSceneNodeRotationQuaternion,
17
+ setSceneNodeScale,
18
+ } from '@flighthq/scene';
19
+ import type { Camera, Mesh, Ray3D, SceneHit } from '@flighthq/types';
8
20
 
9
- import { pickScene } from './pickScene';
21
+ import { createSceneHit, pickScene, pickSceneAll, pickSceneAllWithRay3D, pickSceneWithRay3D } from './pickScene';
10
22
 
11
23
  function makeCamera(): Camera {
12
24
  const camera = createCamera({
@@ -19,8 +31,33 @@ function makeCamera(): Camera {
19
31
  return camera;
20
32
  }
21
33
 
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 };
34
+ // Orthographic camera with a deliberately non-square view volume (halfWidth != halfHeight) to guard
35
+ // against a horizontal/vertical mismap. Looks down -Z from z = 5.
36
+ function makeOrthoCamera(halfWidth: number, halfHeight: number): Camera {
37
+ const camera = createCamera({
38
+ far: 100,
39
+ near: 0.1,
40
+ projection: createOrthographicProjection({ halfHeight, halfWidth }),
41
+ });
42
+ setCameraViewMatrix4FromLookAt(camera, createVector3(0, 0, 5), createVector3(0, 0, 0), createVector3(0, 1, 0));
43
+ return camera;
44
+ }
45
+
46
+ // A world-space ray from z = 5 pointing toward the origin along -Z (mirrors makeCamera's center ray).
47
+ function makeCenterRay(): Ray3D {
48
+ const ray = createRay3D();
49
+ setRay3D(ray, createVector3(0, 0, 5), createVector3(0, 0, -1));
50
+ return ray;
51
+ }
52
+
53
+ // A single triangle in the z = 0 plane. `frontFacing` winds it so its geometric normal points toward
54
+ // +Z (toward a camera at +Z); otherwise it faces -Z (away).
55
+ function triangleMesh(frontFacing: boolean): Mesh {
56
+ const a = [-1, -1, 0];
57
+ const b = frontFacing ? [1, -1, 0] : [0, 1, 0];
58
+ const c = frontFacing ? [0, 1, 0] : [1, -1, 0];
59
+ const geometry = createMeshGeometryFromAttributes({ positions: [...a, ...b, ...c] });
60
+ return createMesh(geometry, []);
24
61
  }
25
62
 
26
63
  function sceneWithCenteredBox(): { scene: Scene; mesh: Mesh } {
@@ -30,11 +67,23 @@ function sceneWithCenteredBox(): { scene: Scene; mesh: Mesh } {
30
67
  return { scene, mesh };
31
68
  }
32
69
 
70
+ describe('createSceneHit', () => {
71
+ it('allocates a zeroed hit with a sentinel triangle index', () => {
72
+ const hit = createSceneHit();
73
+
74
+ expect(hit.triangleIndex).toBe(-1);
75
+ expect(hit.distance).toBe(0);
76
+ expect(hit.normalX).toBe(0);
77
+ expect(hit.normalY).toBe(0);
78
+ expect(hit.normalZ).toBe(0);
79
+ });
80
+ });
81
+
33
82
  describe('pickScene', () => {
34
83
  it('fills barycentric weights that sum to one', () => {
35
84
  const camera = makeCamera();
36
85
  const { scene } = sceneWithCenteredBox();
37
- const out = makeHit();
86
+ const out = createSceneHit();
38
87
 
39
88
  const hit = pickScene(scene, camera, 0, 0, out);
40
89
 
@@ -42,13 +91,27 @@ describe('pickScene', () => {
42
91
  expect((hit?.u ?? 0) + (hit?.v ?? 0) + (hit?.w ?? 0)).toBeCloseTo(1, 5);
43
92
  });
44
93
 
94
+ it('fills the world face normal and triangle index of the hit', () => {
95
+ const camera = makeCamera();
96
+ const { scene } = sceneWithCenteredBox();
97
+ const out = createSceneHit();
98
+
99
+ const hit = pickScene(scene, camera, 0, 0, out);
100
+
101
+ // Front face of the box points at +Z (toward the camera at z = 5).
102
+ expect(hit?.normalZ).toBeCloseTo(1, 5);
103
+ expect(hit?.normalX).toBeCloseTo(0, 5);
104
+ expect(hit?.normalY).toBeCloseTo(0, 5);
105
+ expect(hit?.triangleIndex).toBeGreaterThanOrEqual(0);
106
+ });
107
+
45
108
  it('returns null for a scene containing only non-mesh nodes', () => {
46
109
  const camera = makeCamera();
47
110
  const scene = createScene();
48
111
  // A plain SceneNode is not a Mesh; isMesh() returns false for it.
49
112
  const node = createSceneNode();
50
113
  addNodeChild(scene, node);
51
- const out = makeHit();
114
+ const out = createSceneHit();
52
115
 
53
116
  expect(pickScene(scene, camera, 0, 0, out)).toBeNull();
54
117
  });
@@ -56,7 +119,7 @@ describe('pickScene', () => {
56
119
  it('returns null when the ray misses every mesh', () => {
57
120
  const camera = makeCamera();
58
121
  const { scene } = sceneWithCenteredBox();
59
- const out = makeHit();
122
+ const out = createSceneHit();
60
123
 
61
124
  // Far corner of the viewport points away from the small centred box.
62
125
  expect(pickScene(scene, camera, 0.99, 0.99, out)).toBeNull();
@@ -65,7 +128,7 @@ describe('pickScene', () => {
65
128
  it('returns null when the scene has no meshes', () => {
66
129
  const camera = makeCamera();
67
130
  const scene = createScene();
68
- const out = makeHit();
131
+ const out = createSceneHit();
69
132
 
70
133
  expect(pickScene(scene, camera, 0, 0, out)).toBeNull();
71
134
  });
@@ -73,7 +136,7 @@ describe('pickScene', () => {
73
136
  it('returns the mesh hit by the center ray', () => {
74
137
  const camera = makeCamera();
75
138
  const { scene, mesh } = sceneWithCenteredBox();
76
- const out = makeHit();
139
+ const out = createSceneHit();
77
140
 
78
141
  const hit = pickScene(scene, camera, 0, 0, out);
79
142
 
@@ -101,7 +164,7 @@ describe('pickScene', () => {
101
164
  setSceneNodePosition(meshNear, 0, 0, 2);
102
165
  addNodeChild(scene, meshNear);
103
166
 
104
- const out = makeHit();
167
+ const out = createSceneHit();
105
168
  const hit = pickScene(scene, camera, 0, 0, out);
106
169
 
107
170
  expect(hit).toBe(out);
@@ -111,4 +174,191 @@ describe('pickScene', () => {
111
174
  expect(hit?.distance).toBeGreaterThan(0);
112
175
  expect(hit?.distance).toBeLessThan(3); // meshFar's front face would be at distance ≈ 4
113
176
  });
177
+
178
+ it('skips a disabled mesh so it is not pickable', () => {
179
+ const camera = makeCamera();
180
+ const { scene, mesh } = sceneWithCenteredBox();
181
+ mesh.enabled = false;
182
+ const out = createSceneHit();
183
+
184
+ expect(pickScene(scene, camera, 0, 0, out)).toBeNull();
185
+ });
186
+
187
+ it('prunes the subtree of a disabled group node', () => {
188
+ const camera = makeCamera();
189
+ const scene = createScene();
190
+ const group = createSceneNode();
191
+ group.enabled = false;
192
+ const mesh = createMesh(createBoxMeshGeometry(2, 2, 2), []);
193
+ addNodeChild(group, mesh);
194
+ addNodeChild(scene, group);
195
+ const out = createSceneHit();
196
+
197
+ expect(pickScene(scene, camera, 0, 0, out)).toBeNull();
198
+ });
199
+
200
+ it('excludes a mesh rejected by the predicate option', () => {
201
+ const camera = makeCamera();
202
+ const scene = createScene();
203
+ const meshFar = createMesh(createBoxMeshGeometry(2, 2, 2), []);
204
+ addNodeChild(scene, meshFar);
205
+ const meshNear = createMesh(createBoxMeshGeometry(2, 2, 2), []);
206
+ setSceneNodePosition(meshNear, 0, 0, 2);
207
+ addNodeChild(scene, meshNear);
208
+ const out = createSceneHit();
209
+
210
+ const hit = pickScene(scene, camera, 0, 0, out, { predicate: (m) => m === meshFar });
211
+
212
+ expect(hit?.node).toBe(meshFar);
213
+ });
214
+
215
+ it('rejects hits beyond maxDistance', () => {
216
+ const camera = makeCamera();
217
+ const { scene } = sceneWithCenteredBox();
218
+ const out = createSceneHit();
219
+
220
+ // Front face is ~4 world units from the camera at z = 5.
221
+ expect(pickScene(scene, camera, 0, 0, out, { maxDistance: 3 })).toBeNull();
222
+ expect(pickScene(scene, camera, 0, 0, out, { maxDistance: 4.5 })).not.toBeNull();
223
+ });
224
+
225
+ it('maps a non-square orthographic viewport without an aspect mismap', () => {
226
+ // View volume is 8 wide (halfWidth 4) but only 2 tall (halfHeight 1).
227
+ const camera = makeOrthoCamera(4, 1);
228
+ const scene = createScene();
229
+ // A 2x2x2 box centred at x = 3 spans x in [2, 4], within the horizontal extent but well outside
230
+ // the vertical one — so a square (aspect = 1) mismap of screenX would miss it.
231
+ const mesh = createMesh(createBoxMeshGeometry(2, 2, 2), []);
232
+ setSceneNodePosition(mesh, 3, 0, 0);
233
+ addNodeChild(scene, mesh);
234
+ const out = createSceneHit();
235
+
236
+ // screenX = 0.75 → world x = 0.75 * halfWidth(4) = 3.
237
+ const hit = pickScene(scene, camera, 0.75, 0, out);
238
+
239
+ expect(hit?.node).toBe(mesh);
240
+ expect(hit?.pointX).toBeCloseTo(3, 3);
241
+ expect(hit?.pointZ).toBeCloseTo(1, 3);
242
+ // A pick past the horizontal extent misses.
243
+ expect(pickScene(scene, camera, 1.1, 0, out)).toBeNull();
244
+ });
245
+
246
+ it('hits a rotated and scaled mesh through the local-space path', () => {
247
+ const camera = makeCamera();
248
+ const scene = createScene();
249
+ const mesh = createMesh(createBoxMeshGeometry(2, 2, 2), []);
250
+ // Non-uniform scale (z doubled) plus a rotation about the Z axis. The +Z face stays on-axis, so
251
+ // its world position is z = 1 * 2 = 2 while the surrounding geometry is rotated — exercising the
252
+ // inverse-transform narrow phase.
253
+ setSceneNodeScale(mesh, 1.5, 1.5, 2);
254
+ const q = createQuaternion();
255
+ setQuaternionFromAxisAngle(q, createVector3(0, 0, 1), Math.PI / 4);
256
+ setSceneNodeRotationQuaternion(mesh, q);
257
+ addNodeChild(scene, mesh);
258
+ const out = createSceneHit();
259
+
260
+ const hit = pickScene(scene, camera, 0, 0, out);
261
+
262
+ expect(hit?.node).toBe(mesh);
263
+ expect(hit?.pointZ).toBeCloseTo(2, 3);
264
+ expect(hit?.pointX).toBeCloseTo(0, 3);
265
+ expect(hit?.pointY).toBeCloseTo(0, 3);
266
+ expect(hit?.normalZ).toBeCloseTo(1, 3);
267
+ });
268
+ });
269
+
270
+ describe('pickSceneAll', () => {
271
+ it('collects both overlapping meshes sorted by ascending distance', () => {
272
+ const camera = makeCamera();
273
+ const scene = createScene();
274
+ const meshFar = createMesh(createBoxMeshGeometry(2, 2, 2), []);
275
+ addNodeChild(scene, meshFar);
276
+ const meshNear = createMesh(createBoxMeshGeometry(2, 2, 2), []);
277
+ setSceneNodePosition(meshNear, 0, 0, 2);
278
+ addNodeChild(scene, meshNear);
279
+ const outArray: SceneHit[] = [];
280
+
281
+ const hits = pickSceneAll(scene, camera, 0, 0, outArray);
282
+
283
+ expect(hits).toBe(outArray);
284
+ // Each box contributes a front-face hit (a 2x2x2 box's front quad is two triangles, but the
285
+ // center ray crosses one triangle per box). Nearest first.
286
+ expect(hits.length).toBeGreaterThanOrEqual(2);
287
+ for (let i = 1; i < hits.length; i++) {
288
+ expect(hits[i].distance).toBeGreaterThanOrEqual(hits[i - 1].distance);
289
+ }
290
+ expect(hits[0].node).toBe(meshNear);
291
+ });
292
+
293
+ it('empties the array on a miss', () => {
294
+ const camera = makeCamera();
295
+ const { scene } = sceneWithCenteredBox();
296
+ const outArray: SceneHit[] = [createSceneHit(), createSceneHit()];
297
+
298
+ const hits = pickSceneAll(scene, camera, 0.99, 0.99, outArray);
299
+
300
+ expect(hits).toBe(outArray);
301
+ expect(hits.length).toBe(0);
302
+ });
303
+ });
304
+
305
+ describe('pickSceneAllWithRay3D', () => {
306
+ it('collects every triangle a world ray crosses, front and back faces', () => {
307
+ const { scene } = sceneWithCenteredBox();
308
+ const ray = makeCenterRay();
309
+ const outArray: SceneHit[] = [];
310
+
311
+ const hits = pickSceneAllWithRay3D(scene, ray, outArray);
312
+
313
+ // The center ray runs along the shared diagonal of each quad, so it grazes both triangles of the
314
+ // front face (z = 1) and both of the back face (z = -1): four hits, sorted near-to-far.
315
+ expect(hits.length).toBe(4);
316
+ for (let i = 1; i < hits.length; i++) {
317
+ expect(hits[i].distance).toBeGreaterThanOrEqual(hits[i - 1].distance);
318
+ }
319
+ expect(hits[0].pointZ).toBeCloseTo(1, 3);
320
+ expect(hits[hits.length - 1].pointZ).toBeCloseTo(-1, 3);
321
+ });
322
+
323
+ it('keeps only front faces when cullBackfaces is set', () => {
324
+ const { scene } = sceneWithCenteredBox();
325
+ const ray = makeCenterRay();
326
+ const outArray: SceneHit[] = [];
327
+
328
+ const hits = pickSceneAllWithRay3D(scene, ray, outArray, { cullBackfaces: true });
329
+
330
+ // Only the two front-face triangles survive; both point at +Z (toward the ray origin).
331
+ expect(hits.length).toBe(2);
332
+ for (const hit of hits) {
333
+ expect(hit.pointZ).toBeCloseTo(1, 3);
334
+ expect(hit.normalZ).toBeCloseTo(1, 3);
335
+ }
336
+ });
337
+ });
338
+
339
+ describe('pickSceneWithRay3D', () => {
340
+ it('resolves the nearest hit for a world-space ray', () => {
341
+ const { scene, mesh } = sceneWithCenteredBox();
342
+ const ray = makeCenterRay();
343
+ const out = createSceneHit();
344
+
345
+ const hit = pickSceneWithRay3D(scene, ray, out, {});
346
+
347
+ expect(hit).toBe(out);
348
+ expect(hit?.node).toBe(mesh);
349
+ expect(hit?.pointZ).toBeCloseTo(1, 3);
350
+ });
351
+
352
+ it('culls a back-facing triangle but keeps a front-facing one', () => {
353
+ const front = createScene();
354
+ addNodeChild(front, triangleMesh(true));
355
+ const back = createScene();
356
+ addNodeChild(back, triangleMesh(false));
357
+ const out = createSceneHit();
358
+
359
+ expect(pickSceneWithRay3D(front, makeCenterRay(), out, { cullBackfaces: true })).not.toBeNull();
360
+ expect(pickSceneWithRay3D(back, makeCenterRay(), out, { cullBackfaces: true })).toBeNull();
361
+ // Double-sided (default) hits either winding.
362
+ expect(pickSceneWithRay3D(back, makeCenterRay(), out)).not.toBeNull();
363
+ });
114
364
  });