140 lines
6.3 KiB
TypeScript
140 lines
6.3 KiB
TypeScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { PerspectiveCamera, Vector3 } from 'three';
|
|
import { createFpsCameraController } from '../src/controls/fpsCameraController.ts';
|
|
import { directionFromAngles } from '../src/astronomy/direction.ts';
|
|
import { SUMMIT_TERRAIN } from '../src/constants/summitTerrain.ts';
|
|
|
|
function withController(run: (camera: PerspectiveCamera, canvas: EventTarget, picks: number[][],
|
|
controller: ReturnType<typeof createFpsCameraController>) => void,
|
|
options: Parameters<typeof createFpsCameraController>[3] = {}) {
|
|
const oldWindow = Object.getOwnPropertyDescriptor(globalThis, 'window');
|
|
const oldDocument = Object.getOwnPropertyDescriptor(globalThis, 'document');
|
|
const doc = Object.assign(new EventTarget(), { querySelector: () => ({ textContent: '' }) });
|
|
const canvas = Object.assign(new EventTarget(), { ownerDocument: doc, focus() {}, blur() {}, tabIndex: 0,
|
|
setPointerCapture() {}, releasePointerCapture() {}, hasPointerCapture() { return true; } });
|
|
Object.defineProperty(globalThis, 'window', { value: new EventTarget(), configurable: true });
|
|
Object.defineProperty(globalThis, 'document', { value: doc, configurable: true });
|
|
const camera = new PerspectiveCamera();
|
|
const picks: number[][] = [];
|
|
const controller = createFpsCameraController(camera, canvas as unknown as HTMLCanvasElement,
|
|
(x, y) => { picks.push([x, y]); return new Vector3(x, 0, y); }, options);
|
|
try { run(camera, canvas, picks, controller); }
|
|
finally {
|
|
controller.dispose();
|
|
for (const [key, value] of [['window', oldWindow], ['document', oldDocument]] as const) {
|
|
if (value) Object.defineProperty(globalThis, key, value);
|
|
else Reflect.deleteProperty(globalThis, key);
|
|
}
|
|
}
|
|
}
|
|
function pointer(canvas: EventTarget, type: string, x: number, y: number, pointerType = 'touch', button = 0, pointerId = 1) {
|
|
canvas.dispatchEvent(Object.assign(new Event(type, { cancelable: true }),
|
|
{ pointerId, isPrimary: pointerId === 1, pointerType, button, clientX: x, clientY: y }));
|
|
}
|
|
|
|
test('direction mode stops travel and rotates only the character until done', () => {
|
|
let yaw = 0;
|
|
withController((camera, canvas, picks, controller) => {
|
|
pointer(canvas, 'pointerdown', 20, 20);
|
|
pointer(canvas, 'pointerup', 20, 20);
|
|
assert.ok(controller.destination);
|
|
controller.setTurning(true);
|
|
assert.equal(controller.destination, null);
|
|
const view = camera.quaternion.clone();
|
|
pointer(canvas, 'pointerdown', 100, 100);
|
|
pointer(canvas, 'pointermove', 150, 120);
|
|
pointer(canvas, 'pointerup', 150, 120);
|
|
assert.notEqual(yaw, 0);
|
|
assert.ok(camera.quaternion.equals(view));
|
|
assert.equal(picks.length, 1);
|
|
controller.setTurning(false);
|
|
const facing = yaw;
|
|
pointer(canvas, 'pointerdown', 100, 100);
|
|
pointer(canvas, 'pointermove', 150, 100);
|
|
pointer(canvas, 'pointerup', 150, 100);
|
|
assert.equal(yaw, facing);
|
|
assert.ok(!camera.quaternion.equals(view));
|
|
}, { onTurn: delta => { yaw += delta; } });
|
|
});
|
|
|
|
test('pinch zoom never rotates or becomes a destination when either finger lifts', () => {
|
|
let zoom = 1;
|
|
withController((camera, canvas, picks) => {
|
|
const view = camera.quaternion.clone();
|
|
pointer(canvas, 'pointerdown', 100, 100);
|
|
pointer(canvas, 'pointerdown', 200, 100, 'touch', 0, 2);
|
|
pointer(canvas, 'pointermove', 250, 100, 'touch', 0, 2);
|
|
assert.ok(zoom < 1);
|
|
pointer(canvas, 'pointerup', 100, 100);
|
|
pointer(canvas, 'pointermove', 280, 100, 'touch', 0, 2);
|
|
pointer(canvas, 'pointerup', 280, 100, 'touch', 0, 2);
|
|
assert.deepEqual(picks, []);
|
|
assert.ok(camera.quaternion.equals(view));
|
|
pointer(canvas, 'pointerdown', 50, 50);
|
|
pointer(canvas, 'pointerup', 50, 50);
|
|
assert.equal(picks.length, 1);
|
|
}, { onZoom: factor => { zoom *= factor; } });
|
|
});
|
|
|
|
test('selecting a character takes priority over tap movement and blur exits turning', () => {
|
|
withController((_camera, canvas, picks, controller) => {
|
|
pointer(canvas, 'pointerdown', 100, 100);
|
|
pointer(canvas, 'pointerup', 100, 100);
|
|
assert.deepEqual(picks, []);
|
|
controller.setTurning(true);
|
|
window.dispatchEvent(new Event('blur'));
|
|
assert.equal(controller.turning, false);
|
|
}, { onSelect: () => true });
|
|
});
|
|
|
|
test('opening a menu stops walking without losing the explicit direction mode', () => {
|
|
withController((_camera, canvas, _picks, controller) => {
|
|
pointer(canvas, 'pointerdown', 100, 100);
|
|
pointer(canvas, 'pointerup', 100, 100);
|
|
assert.ok(controller.destination);
|
|
canvas.dispatchEvent(new Event('blur'));
|
|
assert.equal(controller.destination, null);
|
|
controller.setTurning(true);
|
|
canvas.dispatchEvent(new Event('blur'));
|
|
assert.equal(controller.turning, true, 'Done button can receive focus before exiting');
|
|
});
|
|
});
|
|
|
|
test('touch tap and mouse right click select a destination, left click does not', () => {
|
|
withController((_camera, canvas, picks) => {
|
|
for (const [type, button] of [['touch', 0], ['mouse', 2], ['mouse', 0]] as const) {
|
|
pointer(canvas, 'pointerdown', 100, 120, type, button);
|
|
pointer(canvas, 'pointerup', 100, 120, type, button);
|
|
}
|
|
assert.deepEqual(picks, [[100, 120], [100, 120]]);
|
|
});
|
|
});
|
|
|
|
test('drag changes the view without selecting a destination, including when returning to its start', () => {
|
|
withController((camera, canvas, picks) => {
|
|
pointer(canvas, 'pointerdown', 100, 100);
|
|
pointer(canvas, 'pointermove', 130, 110);
|
|
assert.notEqual(camera.rotation.y, 0);
|
|
pointer(canvas, 'pointerup', 100, 100);
|
|
assert.deepEqual(picks, []);
|
|
pointer(canvas, 'pointerdown', 100, 100);
|
|
pointer(canvas, 'pointercancel', 100, 100);
|
|
pointer(canvas, 'pointerup', 100, 100);
|
|
assert.deepEqual(picks, []);
|
|
});
|
|
});
|
|
|
|
test('first horizontal drag after a southward reset preserves camera pitch', () => {
|
|
withController((camera, canvas) => {
|
|
camera.lookAt(new Vector3(...directionFromAngles(SUMMIT_TERRAIN.heading, SUMMIT_TERRAIN.pitch)));
|
|
const before = camera.getWorldDirection(new Vector3());
|
|
pointer(canvas, 'pointerdown', 100, 100, 'mouse');
|
|
pointer(canvas, 'pointermove', 110, 100, 'mouse');
|
|
const after = camera.getWorldDirection(new Vector3());
|
|
assert.ok(Math.abs(after.y - before.y) < 1e-8);
|
|
assert.ok(before.angleTo(after) > 0);
|
|
assert.ok(before.angleTo(after) < 0.04);
|
|
});
|
|
});
|