@threejs-addons/threejs-addons
Version:
free & opensource threejs addons to boost productivity
368 lines (254 loc) • 13.8 kB
text/typescript
import { Scene } from 'three/src/scenes/Scene';
import { PerspectiveCamera } from 'three/src/cameras/PerspectiveCamera';
import { Vector3 } from 'three/src/math/Vector3';
import { generateUUID } from 'three/src/math/MathUtils';
import { CameraControlType, CameraControllerType, ControlType, Index, configureControllerArgsType } from '../../types';
import { OrbitControlsWrapper, PointerLockControlsWrapper } from '../../wrappers';
import { PointerLockControls } from 'three/examples/jsm/controls/PointerLockControls';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
interface CameraControllerAPI {
/** controllers */
controllers: CameraControllerType[];
activeController: CameraControllerType | null;
/** configurations */
loopControllerIndex: boolean;
/** track controllers */
currentControllerIndex: Index;
previousControllerIndex: Index | null;
/** track controls */
currentControlTypeIndex: Index;
previousControlTypeIndex: Index | null;
loopControlTypeIndex: boolean;
/** scene */
scene?: Scene | undefined;
/** methods */
switchControllerNext(index: Index | null): Index | null;
switchControlTypeNext(index: Index | null): Index;
configureController(args: configureControllerArgsType): void;
setActiveController(index: Index, disableCurrentController: boolean): void;
getCurrentActiveControlType(): ControlType.ORBIT_CONTROLS | ControlType.POINTER_LOCK_CONTROLS | null;
onResize({ width, height }: { width: number, height: number }): void;
updateProjectionMatrix(): void;
}
/**
* @description CameraController class for managing camera and its respective controls.
*/
export class CameraController implements CameraControllerAPI {
controllers: CameraControllerType[] = [];
activeController: CameraControllerType | null = null;
/**confgurations */
loopControllerIndex: boolean = true;
/** track controllers */
currentControllerIndex: Index = 0;
previousControllerIndex: Index | null = 0;
/** track controls */
currentControlTypeIndex: Index = 0;
previousControlTypeIndex: Index | null = 0;
loopControlTypeIndex: boolean = true;
/** scene */
scene?: Scene | undefined = undefined;
constructor(args: { scene?: Scene }){
if(args.scene !== null){
this.scene = args.scene
}
}
/** switch controllers */
switchControllerNext(index: Index | null): Index | null {
this.previousControllerIndex = this.currentControllerIndex;
this.currentControllerIndex = index ? index : this.currentControllerIndex + 1;
const activeControlType = this.getCurrentActiveControlType();
if(!activeControlType) return null;
if(this.currentControllerIndex > this.controllers.length - 1){
if(index) throw new Error(`[CameraController] index ${index} is out of range`);
if(!this.loopControllerIndex) return this.currentControllerIndex;
this.currentControllerIndex = 0;
}
/** disable previous controlls */
this.disableControlsByControllerIndex(this.previousControllerIndex);
/** enable only controller on active index */
this.controllers.forEach((controller: CameraControllerType, index: Index) => controller.active = index === this.currentControllerIndex)
this.activeController = this.controllers[this.currentControllerIndex];
/** enable active controlls */
this.enableActiveControllerControl(activeControlType);
return this.currentControllerIndex;
}
/** toggle controls */
switchControlTypeNext(index: Index | null): Index {
this.previousControlTypeIndex = this.currentControlTypeIndex;
this.currentControlTypeIndex = index ? index : this.currentControlTypeIndex + 1;
if((this.currentControlTypeIndex) > this.controllers[this.currentControllerIndex].controls.length - 1){
if(index !== null)throw new Error(`[CameraController] index ${index} is out of range`);
if(!this.loopControlTypeIndex) return this.currentControlTypeIndex;
this.currentControlTypeIndex = 0;
}
this.toggleControlTypeByIndex(this.previousControlTypeIndex, this.currentControlTypeIndex);
return this.currentControlTypeIndex;
}
/** configure each controller to add to the controllers list */
configureController( args: configureControllerArgsType ): void {
if(!args) throw new Error('no options passed to configure');
const controls: (OrbitControlsWrapper | PointerLockControlsWrapper)[] = [];
const camera = args.camera as PerspectiveCamera;
/** add camera to scene */
if(this.scene !== undefined){
this.scene.add(camera);
}
/** configure camera */
camera.aspect = window.innerWidth/window.innerHeight;
camera.fov = 75;
camera.updateProjectionMatrix();
for(const control of args.controls){
if(control.type === ControlType.ORBIT_CONTROLS){
const orbitControls = new OrbitControlsWrapper( camera, args.domElement );
for(const option in control.options){
(orbitControls as any)[option] = control.options[option];
}
orbitControls.userData = { uuid: generateUUID(), type: ControlType.ORBIT_CONTROLS, active: false };
controls.push(orbitControls);
}
if(control.type === ControlType.POINTER_LOCK_CONTROLS){
const pointerControls = new PointerLockControlsWrapper( camera, args.domElement );
for(const option in control.options){
(pointerControls as any)[option] = control.options[option];
}
pointerControls.userData = { uuid: generateUUID(), type: ControlType.POINTER_LOCK_CONTROLS, active: false };
controls.push(pointerControls);
}
}
const controller: CameraControllerType = {
uuid: generateUUID(),
camera,
controls,
active: args.active,
disable: () => this.disableControlsByControllerIndex(this.currentControllerIndex),
enable: () => this.enableActiveControllerControl(this.getCurrentActiveControlType())
}
this.controllers.push(controller);
}
/** set the active controller from your list of controllers */
setActiveController(index: Index, disableCurrentController: boolean): void {
if(index > this.controllers.length - 1) throw new Error(`index ${index} is out of range`);
this.currentControllerIndex = index;
this.controllers.forEach((controller: CameraControllerType, index: Index) => controller.active = index === this.currentControllerIndex);
let previouslyActiveController = this.activeController;
if(!previouslyActiveController){
this.activeController = this.controllers[this.currentControllerIndex];
this.activeController.camera.aspect = window.innerWidth/window.innerHeight;
this.updateProjectionMatrix();
this.activeController.controls.forEach((control: OrbitControlsWrapper | PointerLockControlsWrapper) => {
if(!control.userData) return;
if('enabled' in control && control.userData.type === ControlType.ORBIT_CONTROLS){
control.userData.active = true;
control.enabled = true;
this.normalizeOrbitControlsTarget(control);
control.update();
}
})
return;
}
let activeControlType = this.getCurrentActiveControlType();
this.activeController = this.controllers[this.currentControllerIndex];
this.activeController.camera.aspect = window.innerWidth/window.innerHeight;
this.updateProjectionMatrix();
previouslyActiveController.controls.forEach((control: OrbitControlsWrapper | PointerLockControlsWrapper) => {
if(!control.userData) return;
control.userData.active = false
});
this.activeController?.controls.forEach((control: OrbitControlsWrapper | PointerLockControlsWrapper) => {
if(!control.userData) return;
control.userData.active = !activeControlType ? control.userData.type === ControlType.ORBIT_CONTROLS : control.userData.type === activeControlType
});
}
/** get active control type */
getCurrentActiveControlType(): ControlType.ORBIT_CONTROLS | ControlType.POINTER_LOCK_CONTROLS | null {
let activeControl = this.activeController?.controls.find((control: OrbitControlsWrapper | PointerLockControlsWrapper ) => control.userData && control.userData.active === true)
if(!activeControl?.userData) return null;
return activeControl?.userData.type;
}
/** reszie camera aspect ratio */
onResize({ width, height }: { width: number, height: number }): void{
if(!this.activeController)return;
this.activeController.camera.aspect = width/height;
this.updateProjectionMatrix();
}
/** calls updateProjectionMatrix on current active camera */
updateProjectionMatrix(): void{
if(!this.activeController)return;
let c = this.activeController.camera as PerspectiveCamera
c.updateProjectionMatrix();
}
/** disable previous controls */
private disableControlsByControllerIndex(index: Index): void {
if(index === null || index === undefined) return;
this.controllers[index].controls.forEach((control) => {
if(!control.userData) return;
control.userData.active = false
if('update' in control && control.userData.type === ControlType.ORBIT_CONTROLS){
control.enabled = false;
control.update();
}
if('disconnect' in control && control.userData.type === ControlType.POINTER_LOCK_CONTROLS){
control.unlock();
control.disconnect();
}
})
}
/** enable active controllers control by activeControlType */
private enableActiveControllerControl(activeControlType: CameraControlType | null ): void {
if(!this.activeController)return;
this.activeController.controls.forEach(control => {
if(!control.userData) return;
control.userData.active = control.userData.type === activeControlType;
if('update' in control && control.userData.type === ControlType.ORBIT_CONTROLS && activeControlType === ControlType.ORBIT_CONTROLS){
control.enabled = true;
this.normalizeOrbitControlsTarget(control);
control.update();
}
if( 'lock' in control && control.userData.type === ControlType.POINTER_LOCK_CONTROLS && activeControlType === ControlType.POINTER_LOCK_CONTROLS){
control.connect();
control.lock();
}
})
}
/** toggle Control By index */
private toggleControlTypeByIndex(previousControlTypeIndex: Index, currentControlTypeIndex: Index): void {
if(!this.activeController) return;
if(previousControlTypeIndex === null || previousControlTypeIndex === undefined) return;
let prevControl = this.activeController.controls[ previousControlTypeIndex ] as OrbitControlsWrapper & (PointerLockControlsWrapper & PointerLockControls);
if(!prevControl) return;
if(!prevControl.userData) return;
prevControl.userData.active = false;
if( 'update' in prevControl && prevControl?.userData.type === ControlType.ORBIT_CONTROLS){
prevControl.enabled = false;
prevControl.update();
}
if( 'unlock' in prevControl && prevControl?.userData.type === ControlType.POINTER_LOCK_CONTROLS){
prevControl.unlock();
prevControl.disconnect();
}
let currControl = this.activeController?.controls[ currentControlTypeIndex ]
if(!currControl) return;
if(!currControl.userData) return;
currControl.userData.active = true;
if('update' in currControl && currControl?.userData.type === ControlType.ORBIT_CONTROLS){
/** get previous controls look direction */
const lookdirection = new Vector3();
prevControl.getDirection(lookdirection);
const lookAtPoint = new Vector3().copy(this.activeController.camera.position).add(lookdirection);
currControl.target.copy(lookAtPoint);
currControl.enabled = true;
currControl.update();
}
if('lock' in currControl && currControl?.userData.type === ControlType.POINTER_LOCK_CONTROLS){
currControl.connect();
currControl.lock();
}
}
private normalizeOrbitControlsTarget(control: OrbitControls){
const direction = new Vector3(0, 0, -1);
direction.applyQuaternion(control.object.quaternion);
const distance = 0.2; // The distance in front of the camera
const target = new Vector3().copy(control.object.position).add(direction.multiplyScalar(distance));
control.target.copy(target);
}
}