import test from 'node:test'; import assert from 'node:assert/strict'; import { beginPalmAimSelection, computePalmAimRay, createPalmAimLatch, endPalmAimSelection, getPalmAimSelectionRay, recordStablePalmAimRay } from '../vr180player/xr/hand-aim.js'; const wrist = { x: 0, y: 0, z: 0 }; const middleMetacarpal = { x: 0, y: 0.1, z: 0 }; const ringMetacarpal = { x: 0.025, y: 0.1, z: 0 }; const straightAheadRay = { direction: { x: 0, y: 0, z: -1 }, origin: { x: 0, y: 1.4, z: -0.04 } }; const pinchedRay = { direction: { x: 0.4, y: -0.2, z: -0.8 }, origin: { x: 0.04, y: 1.32, z: -0.03 } }; test('computePalmAimRay tilts a right palm ray toward the finger-forward axis', () => { const ray = computePalmAimRay({ handedness: 'right', indexMetacarpal: { x: -0.045, y: 0.1, z: 0 }, middleMetacarpal: { x: -0.015, y: 0.1, z: 0 }, pinkyMetacarpal: { x: 0.045, y: 0.1, z: 0 }, ringMetacarpal: { x: 0.015, y: 0.1, z: 0 }, wrist }); assert.ok(ray); assert.equal(Math.round(ray.direction.x * 1000) / 1000, 0); assert.equal(Math.round(ray.direction.y * 1000) / 1000, 0.643); assert.equal(Math.round(ray.direction.z * 1000) / 1000, -0.766); assert.equal(Math.round(ray.origin.y * 1000) / 1000, 0.084); assert.equal(Math.round(ray.origin.z * 1000) / 1000, -0.027); }); test('computePalmAimRay flips the palm normal for a left hand while keeping forward tilt', () => { const ray = computePalmAimRay({ handedness: 'left', indexMetacarpal: { x: 0.045, y: 0.1, z: 0 }, middleMetacarpal: { x: 0.015, y: 0.1, z: 0 }, pinkyMetacarpal: { x: -0.045, y: 0.1, z: 0 }, ringMetacarpal: { x: -0.015, y: 0.1, z: 0 }, wrist }); assert.ok(ray); assert.equal(Math.round(ray.direction.y * 1000) / 1000, 0.643); assert.equal(Math.round(ray.direction.z * 1000) / 1000, -0.766); }); test('computePalmAimRay returns null when palm joints cannot define a stable ray', () => { assert.equal(computePalmAimRay({ handedness: 'right', wrist }), null); assert.equal(computePalmAimRay({ handedness: 'right', indexMetacarpal: { x: 0, y: 0.1, z: 0 }, pinkyMetacarpal: { x: 0, y: 0.1, z: 0 }, wrist }), null); }); test('palm aim latch returns the latest fresh stable ray', () => { const latch = createPalmAimLatch(); recordStablePalmAimRay(latch, straightAheadRay, 100); assert.deepEqual(getPalmAimSelectionRay(latch, 120), straightAheadRay); assert.equal(getPalmAimSelectionRay(latch, 500), null); }); test('palm aim latch freezes the pre-selection ray while selecting', () => { const latch = createPalmAimLatch(); recordStablePalmAimRay(latch, straightAheadRay, 100); assert.deepEqual(beginPalmAimSelection(latch, 120), straightAheadRay); recordStablePalmAimRay(latch, pinchedRay, 130); assert.deepEqual(getPalmAimSelectionRay(latch, 140), straightAheadRay); assert.deepEqual(getPalmAimSelectionRay(latch, 1000), straightAheadRay); endPalmAimSelection(latch); recordStablePalmAimRay(latch, pinchedRay, 150); assert.deepEqual(getPalmAimSelectionRay(latch, 160), pinchedRay); }); test('palm aim latch ignores stale rays when selection starts too late', () => { const latch = createPalmAimLatch(); recordStablePalmAimRay(latch, straightAheadRay, 100); assert.equal(beginPalmAimSelection(latch, 450), null); assert.equal(getPalmAimSelectionRay(latch, 450), null); });