diff --git a/packages/core/src/3d/entities/controllers/input/free-camera.controller.ts b/packages/core/src/3d/entities/controllers/input/free-camera.controller.ts index 6e4d48e..8e54f8b 100644 --- a/packages/core/src/3d/entities/controllers/input/free-camera.controller.ts +++ b/packages/core/src/3d/entities/controllers/input/free-camera.controller.ts @@ -1,4 +1,4 @@ -import { BehaviorSubject, combineLatest, filter, takeUntil } from 'rxjs'; +import { BehaviorSubject, combineLatest, filter, Subject, takeUntil } from 'rxjs'; import { DirectionKeyboardInput, DirectionKeyboardKeymap, @@ -91,6 +91,21 @@ export class FreeCameraController extends IEntity { */ public readonly directionsInput: DirectionKeyboardInput; + protected spherical: MutableSpherical = { phi: 0, radius: 1, theta: 0 }; + + get active(): boolean { + return super.active; + } + + set active(value: boolean) { + if (!super.active && value) { + this.reset(); + } + super.active = value; + } + + protected resetMotion$: Subject = new Subject(); + /** * Creates a new FreeCameraInput instance. * @param keyboard The keyboard input controller to use for camera movement. @@ -117,8 +132,14 @@ export class FreeCameraController extends IEntity { this.directionsInput = new DirectionKeyboardInput(keyboard, this.options.keymap); } + public reset(): void { + this.spherical = Pnt3.toSpherical(Pnt3.rot({ x: 0, y: 0, z: -1 }, this.camera.rotation)); + this.resetMotion$.next(); + } + async onSpawned(world: GgWorld): Promise { await super.onSpawned(world); + this.spherical = Pnt3.toSpherical(Pnt3.rot({ x: 0, y: 0, z: -1 }, this.camera.rotation)); // Subscribe to keyboard input for movement controls const keys = ['KeyE', 'KeyQ']; if (this.camera.camera.supportsFov) { @@ -166,14 +187,17 @@ export class FreeCameraController extends IEntity { }); // Subscribe to mouse input for camera rotation - const spherical: MutableSpherical = Pnt3.toSpherical(Pnt3.rot({ x: 0, y: 0, z: -1 }, this.camera.rotation)); let isTouchScreen = MouseInput.isTouchDevice(); let mouseDelta$ = this.mouseInput.delta$.pipe( takeUntil(this._onRemoved$), - filter(() => isTouchScreen || !this.options.ignoreMouseUnlessPointerLocked || this.mouseInput.isPointerLocked), + filter( + () => + this.active && + (isTouchScreen || !this.options.ignoreMouseUnlessPointerLocked || this.mouseInput.isPointerLocked), + ), ); if (this.options.cameraRotationElasticity > 0) { - const s$: BehaviorSubject = new BehaviorSubject(spherical); + const s$: BehaviorSubject = new BehaviorSubject(this.spherical); mouseDelta$.subscribe(delta => { const s = s$.getValue(); s$.next({ @@ -185,41 +209,54 @@ export class FreeCameraController extends IEntity { radius: 1, }); }); - s$.pipe( - takeUntil(this._onRemoved$), - ggElastic( - this.tick$, - this.options.cameraRotationElasticity, - (a, b, f) => ({ phi: a.phi + f * (b.phi - a.phi), theta: a.theta + f * (b.theta - a.theta), radius: 1 }), - (a, b) => Pnt2.dist({ x: a.phi, y: a.theta }, { x: b.phi, y: b.theta }) < 0.0001, - ), - ).subscribe(s => { - spherical.theta = s.theta; - spherical.phi = s.phi; + const startElasticMotion = () => { + s$.pipe( + takeUntil(this._onRemoved$), + ggElastic( + this.tick$, + this.options.cameraRotationElasticity, + (a, b, f) => ({ phi: a.phi + f * (b.phi - a.phi), theta: a.theta + f * (b.theta - a.theta), radius: 1 }), + (a, b) => Pnt2.dist({ x: a.phi, y: a.theta }, { x: b.phi, y: b.theta }) < 0.0001, + ), + takeUntil(this.resetMotion$), + ).subscribe(s => { + this.spherical.theta = s.theta; + this.spherical.phi = s.phi; + }); + }; + this.resetMotion$.pipe(takeUntil(this._onRemoved$)).subscribe(() => { + s$.next(this.spherical); + startElasticMotion(); }); + startElasticMotion(); } else { mouseDelta$.subscribe(delta => { - spherical.theta -= (delta.x * this.options.cameraRotationSensitivity) / 1000; - spherical.phi += (delta.y * this.options.cameraRotationSensitivity) / 1000; - spherical.phi = Math.max(0.000001, Math.min(Math.PI - 0.000001, spherical.phi)); + this.spherical.theta -= (delta.x * this.options.cameraRotationSensitivity) / 1000; + this.spherical.phi += (delta.y * this.options.cameraRotationSensitivity) / 1000; + this.spherical.phi = Math.max(0.000001, Math.min(Math.PI - 0.000001, this.spherical.phi)); }); } // Setup updating camera position and rotation based on input - this.camera.tick$.pipe(takeUntil(this._onRemoved$)).subscribe(([_, delta]) => { - this.camera.camera.fov += cameraFovInc; - this.camera.position = Pnt3.add( - this.camera.position, - Pnt3.rot( - Pnt3.scalarMult(translateVector, (this.options.cameraLinearSpeed * delta) / 1000), - this.camera.rotation, - ), - ); - this.camera.rotation = Qtrn.lookAt( - this.camera.position, - Pnt3.add(this.camera.position, Pnt3.fromSpherical(spherical)), - ); - }); + this.camera.tick$ + .pipe( + takeUntil(this._onRemoved$), + filter(() => this.active), + ) + .subscribe(([_, delta]) => { + this.camera.camera.fov += cameraFovInc; + this.camera.position = Pnt3.add( + this.camera.position, + Pnt3.rot( + Pnt3.scalarMult(translateVector, (this.options.cameraLinearSpeed * delta) / 1000), + this.camera.rotation, + ), + ); + this.camera.rotation = Qtrn.lookAt( + this.camera.position, + Pnt3.add(this.camera.position, Pnt3.fromSpherical(this.spherical)), + ); + }); // start input this.mouseInput.start(); diff --git a/packages/core/src/3d/entities/controllers/input/orbit-camera.controller.ts b/packages/core/src/3d/entities/controllers/input/orbit-camera.controller.ts index 515b0c5..dd0af86 100644 --- a/packages/core/src/3d/entities/controllers/input/orbit-camera.controller.ts +++ b/packages/core/src/3d/entities/controllers/input/orbit-camera.controller.ts @@ -14,7 +14,7 @@ import { Spherical, TickOrder, } from '../../../../base'; -import { BehaviorSubject, filter, takeUntil } from 'rxjs'; +import { BehaviorSubject, filter, Subject, takeUntil } from 'rxjs'; import { map } from 'rxjs/operators'; import { Renderer3dEntity } from '../../renderer-3d.entity'; @@ -63,25 +63,44 @@ export class OrbitCameraController extends IEntity { protected spherical: MutableSpherical = { phi: 0, radius: 10, theta: 0 }; public target: Point3 = Pnt3.O; + + get active(): boolean { + return super.active; + } + + set active(value: boolean) { + if (!super.active && value) { + this.reset(); + } + super.active = value; + } + public get radius(): number { return this.spherical.radius; } + public set radius(value: number) { this.spherical.radius = value; } + public get phi(): number { return this.spherical.phi; } + public set phi(value: number) { this.spherical.phi = Math.max(0.000001, Math.min(Math.PI - 0.000001, value)); } + public get theta(): number { return this.spherical.theta; } + public set theta(value: number) { this.spherical.theta = value; } + protected resetMotion$: Subject = new Subject(); + constructor( protected readonly camera: Renderer3dEntity, options: Partial = {}, @@ -91,13 +110,20 @@ export class OrbitCameraController extends IEntity { this.mouseInput = new MouseInput(this.options.mouseOptions); } + public reset(): void { + let targetDistance = Pnt3.dist(this.target, this.camera.position); + this.target = Pnt3.add(this.camera.position, Pnt3.rot({ x: 0, y: 0, z: -targetDistance }, this.camera.rotation)); + this.spherical = Pnt3.toSpherical(Pnt3.sub(this.camera.position, this.target)); + this.resetMotion$.next(); + } + async onSpawned(world: GgWorld): Promise { await super.onSpawned(world); this.spherical = Pnt3.toSpherical(Pnt3.sub(this.camera.position, this.target)); if (this.options.orbiting) { let mouseDelta$ = this.mouseInput.delta$.pipe( takeUntil(this._onRemoved$), - filter(() => this.mouseInput.state == MouseInputState.DRAG), + filter(() => this.active && this.mouseInput.state == MouseInputState.DRAG), ); if (this.options.orbitingElasticity > 0) { const s$: BehaviorSubject = new BehaviorSubject(this.spherical); @@ -112,18 +138,26 @@ export class OrbitCameraController extends IEntity { radius: 1, }); }); - s$.pipe( - takeUntil(this._onRemoved$), - ggElastic( - this.tick$, - this.options.orbitingElasticity, - (a, b, f) => ({ phi: a.phi + f * (b.phi - a.phi), theta: a.theta + f * (b.theta - a.theta), radius: 1 }), - (a, b) => Pnt2.dist({ x: a.phi, y: a.theta }, { x: b.phi, y: b.theta }) < 0.0001, - ), - ).subscribe(s => { - this.spherical.theta = s.theta; - this.spherical.phi = s.phi; + const startElasticMotion = () => { + s$.pipe( + takeUntil(this._onRemoved$), + ggElastic( + this.tick$, + this.options.orbitingElasticity, + (a, b, f) => ({ phi: a.phi + f * (b.phi - a.phi), theta: a.theta + f * (b.theta - a.theta), radius: 1 }), + (a, b) => Pnt2.dist({ x: a.phi, y: a.theta }, { x: b.phi, y: b.theta }) < 0.0001, + ), + takeUntil(this.resetMotion$), + ).subscribe(s => { + this.spherical.theta = s.theta; + this.spherical.phi = s.phi; + }); + }; + this.resetMotion$.pipe(takeUntil(this._onRemoved$)).subscribe(() => { + s$.next(this.spherical); + startElasticMotion(); }); + startElasticMotion(); } else { mouseDelta$.subscribe(delta => { this.spherical.theta -= (delta.x * (this.options.orbiting as any).sensitivityX) / 1000; @@ -200,6 +234,7 @@ export class OrbitCameraController extends IEntity { this.camera.tick$ .pipe( takeUntil(this._onRemoved$), + filter(() => this.active), map(() => this.spherical), ) .subscribe(spherical => { diff --git a/packages/core/src/base/inputs/mouse.input.ts b/packages/core/src/base/inputs/mouse.input.ts index c1fa244..8a00b0a 100644 --- a/packages/core/src/base/inputs/mouse.input.ts +++ b/packages/core/src/base/inputs/mouse.input.ts @@ -221,7 +221,7 @@ export class MouseInput extends IInput<[], [unlockPointer?: boolean]> { if (this.options.canvas) { this.options.canvas.releasePointerCapture(event.pointerId); } - this._element.removeEventListener('pointerup', onPointerUp as any); + window.removeEventListener('pointerup', onPointerUp as any); this._element.removeEventListener('pointercancel', onPointerUp as any); } this._state$.next(pointerLengthsStateMap[Math.min(pointers.length, 2)]); @@ -235,7 +235,8 @@ export class MouseInput extends IInput<[], [unlockPointer?: boolean]> { if (this.options.canvas) { this.options.canvas.setPointerCapture(event.pointerId); } - this._element.addEventListener('pointerup', onPointerUp as any); + // use window instead of this._element to handle case when mouse was released over other element + window.addEventListener('pointerup', onPointerUp as any); this._element.addEventListener('pointercancel', onPointerUp as any); } catch (err) { console.error(err);