Add tests and update navigation controller deactivation logic

This commit is contained in:
2026-04-11 05:16:53 +02:00
parent 5dbd68e0ce
commit 75d8723565
3 changed files with 97 additions and 2 deletions

View File

@@ -3,6 +3,7 @@ import { Vector3 } from "three";
import type { Vec3 } from "../core/vector";
import type {
NavigationControllerDeactivateOptions,
NavigationController,
RuntimeControllerContext
} from "./navigation-controller";
@@ -83,7 +84,11 @@ export class OrbitVisitorNavigationController implements NavigationController {
this.updateCameraTransform();
}
deactivate(ctx: RuntimeControllerContext): void {
deactivate(
ctx: RuntimeControllerContext,
_options: NavigationControllerDeactivateOptions = {}
): void {
void _options;
ctx.domElement.removeEventListener("pointerdown", this.handlePointerDown);
ctx.domElement.removeEventListener("wheel", this.handleWheel);
ctx.domElement.removeEventListener("contextmenu", this.handleContextMenu);

View File

@@ -342,11 +342,17 @@ export class RuntimeHost {
loadScene(runtimeScene: RuntimeSceneDefinition) {
const requestId = ++this.collisionWorldRequestId;
const preservePointerLockDuringLoad =
this.activeController === this.firstPersonController &&
this.desiredNavigationMode === "firstPerson" &&
document.pointerLockElement === this.domElement;
this.sceneReady = false;
this.runtimeScene = runtimeScene;
this.currentWorld = runtimeScene.world;
this.activeController?.deactivate(this.controllerContext);
this.activeController?.deactivate(this.controllerContext, {
releasePointerLock: !preservePointerLockDuringLoad
});
this.activeController = null;
this.firstPersonController.resetSceneState();
this.orbitVisitorController.resetSceneState();

View File

@@ -0,0 +1,84 @@
import { PerspectiveCamera } from "three";
import { afterEach, describe, expect, it, vi } from "vitest";
import { createEmptySceneDocument } from "../../src/document/scene-document";
import { createPlayerStartEntity } from "../../src/entities/entity-instances";
import { buildRuntimeSceneFromDocument } from "../../src/runtime-three/runtime-scene-build";
import { FirstPersonNavigationController } from "../../src/runtime-three/first-person-navigation-controller";
function createRuntimeControllerContext() {
const runtimeScene = buildRuntimeSceneFromDocument(
{
...createEmptySceneDocument({ name: "Pointer Lock Scene" }),
entities: {
"entity-player-start-main": createPlayerStartEntity({
id: "entity-player-start-main"
})
}
},
{
navigationMode: "firstPerson"
}
);
const domElement = document.createElement("canvas");
return {
domElement,
context: {
camera: new PerspectiveCamera(70, 1, 0.05, 1000),
domElement,
getRuntimeScene: () => runtimeScene,
resolveFirstPersonMotion: () => null,
resolvePlayerVolumeState: () => ({
inWater: false,
inFog: false
}),
setRuntimeMessage: vi.fn(),
setFirstPersonTelemetry: vi.fn()
}
};
}
describe("FirstPersonNavigationController", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("can deactivate during a scene transition without releasing pointer lock", () => {
const { context, domElement } = createRuntimeControllerContext();
const controller = new FirstPersonNavigationController();
const exitPointerLockSpy = vi
.spyOn(document, "exitPointerLock")
.mockImplementation(() => undefined);
Object.defineProperty(document, "pointerLockElement", {
configurable: true,
get: () => domElement
});
controller.activate(context);
controller.deactivate(context, {
releasePointerLock: false
});
expect(exitPointerLockSpy).not.toHaveBeenCalled();
});
it("still releases pointer lock for a normal deactivation", () => {
const { context, domElement } = createRuntimeControllerContext();
const controller = new FirstPersonNavigationController();
const exitPointerLockSpy = vi
.spyOn(document, "exitPointerLock")
.mockImplementation(() => undefined);
Object.defineProperty(document, "pointerLockElement", {
configurable: true,
get: () => domElement
});
controller.activate(context);
controller.deactivate(context);
expect(exitPointerLockSpy).toHaveBeenCalledTimes(1);
});
});