custom camera controls

This commit is contained in:
Evert Prants 2023-06-07 20:53:11 +03:00
parent 2e62620d1e
commit 4fe6c2ac6c
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
7 changed files with 255 additions and 11 deletions

View File

@ -75,6 +75,12 @@ const editMenu = computed(() => [
shortcut: 'CTRL+V',
onClick: () => props.editor.events.emit('paste', undefined),
},
{
id: 'duplicate',
label: 'Duplicate',
shortcut: 'CTRL+D',
onClick: () => props.editor.events.emit('duplicate'),
},
{
id: 'delete',
label: 'Delete',
@ -87,7 +93,7 @@ const addMenu = computed(() =>
Object.keys(instancableGameObjects).map((item) => ({
id: item,
label: item,
onClick: () => props.editor.events.emit('instance', { type: item }),
onClick: () => props.editor.events.emit('insert', item),
}))
);

View File

@ -0,0 +1,196 @@
import { Euler, EventDispatcher, Object3D, Vector3 } from 'three';
const _changeEvent = { type: 'change' };
const _PI_2 = Math.PI / 2;
class CameraControls extends EventDispatcher {
private cameraMoving = false;
// Set to constrain the pitch of the camera
// Range is 0 to Math.PI radians
public minPolarAngle = 0; // radians
public maxPolarAngle = Math.PI; // radians
public pointerSpeed = 1.5;
public movementSpeed = 0.025;
private rotation = new Euler(0, 0, 0, 'YXZ');
private look = new Vector3();
private boundOnMouseMove = this.onMouseMove.bind(this);
private boundOnMouseDown = this.onMouseDown.bind(this);
private boundOnMouseUp = this.onMouseUp.bind(this);
private boundOnKeyDown = this.onKeyDown.bind(this);
private boundOnKeyUp = this.onKeyUp.bind(this);
private movement = {
forward: 0,
backward: 0,
left: 0,
right: 0,
};
constructor(private camera: Object3D, private domElement: HTMLElement) {
super();
this.connect();
}
connect() {
this.domElement.ownerDocument.addEventListener(
'mousemove',
this.boundOnMouseMove
);
this.domElement.ownerDocument.addEventListener(
'mousedown',
this.boundOnMouseDown
);
this.domElement.ownerDocument.addEventListener(
'mouseup',
this.boundOnMouseUp
);
window.addEventListener('keydown', this.boundOnKeyDown);
window.addEventListener('keyup', this.boundOnKeyUp);
}
disconnect() {
this.domElement.ownerDocument.removeEventListener(
'mousemove',
this.boundOnMouseMove
);
this.domElement.ownerDocument.removeEventListener(
'mousedown',
this.boundOnMouseDown
);
this.domElement.ownerDocument.removeEventListener(
'mouseup',
this.boundOnMouseUp
);
window.removeEventListener('keydown', this.boundOnKeyDown);
window.removeEventListener('keyup', this.boundOnKeyUp);
}
dispose() {
this.disconnect();
}
update(dt: number) {
if (this.movement.forward !== 0) {
this.moveForward(this.movement.forward * dt);
}
if (this.movement.backward !== 0) {
this.moveForward(-this.movement.backward * dt);
}
if (this.movement.right !== 0) {
this.moveRight(this.movement.right * dt);
}
if (this.movement.left !== 0) {
this.moveRight(-this.movement.left * dt);
}
}
getDirection(v: Vector3) {
return v.set(0, 0, -1).applyQuaternion(this.camera.quaternion);
}
moveForward(distance: number) {
const camera = this.camera;
this.look.setFromMatrixColumn(camera.matrix, 0);
this.look.crossVectors(
camera.up.clone().applyEuler(this.rotation),
this.look
);
camera.position.addScaledVector(this.look, distance);
}
moveRight(distance: number) {
const camera = this.camera;
this.look.setFromMatrixColumn(camera.matrix, 0);
camera.position.addScaledVector(this.look, distance);
}
private onMouseDown(event: MouseEvent) {
if (event.button === 2) this.cameraMoving = true;
}
private onMouseUp(event: MouseEvent) {
if (event.button === 2) this.cameraMoving = false;
}
private onMouseMove(event: MouseEvent) {
if (this.cameraMoving === false) return;
const movementX = event.movementX || 0;
const movementY = event.movementY || 0;
const camera = this.camera;
this.rotation.setFromQuaternion(camera.quaternion);
this.rotation.y -= movementX * 0.002 * this.pointerSpeed;
this.rotation.x -= movementY * 0.002 * this.pointerSpeed;
this.rotation.x = Math.max(
_PI_2 - this.maxPolarAngle,
Math.min(_PI_2 - this.minPolarAngle, this.rotation.x)
);
camera.quaternion.setFromEuler(this.rotation);
this.dispatchEvent(_changeEvent);
}
private onKeyDown(event: KeyboardEvent) {
if (!event.target || (event.target as HTMLElement).tagName !== 'BODY')
return;
if (event.altKey || event.ctrlKey) return;
switch (event.code) {
case 'KeyW':
this.movement.forward = this.movementSpeed;
break;
case 'KeyS':
this.movement.backward = this.movementSpeed;
break;
case 'KeyA':
this.movement.left = this.movementSpeed;
break;
case 'KeyD':
this.movement.right = this.movementSpeed;
break;
}
}
private onKeyUp(event: KeyboardEvent) {
if (!event.target || (event.target as HTMLElement).tagName !== 'BODY')
return;
if (event.altKey || event.ctrlKey) return;
switch (event.code) {
case 'KeyW':
this.movement.forward = 0;
break;
case 'KeyS':
this.movement.backward = 0;
break;
case 'KeyA':
this.movement.left = 0;
break;
case 'KeyD':
this.movement.right = 0;
break;
}
}
}
export { CameraControls };

View File

@ -35,6 +35,7 @@ export class ShotcutsComponent extends EngineComponent {
}
if (!event.ctrlKey) return;
event.preventDefault();
switch (event.key) {
case 'c':
this.events.emit('copy');
@ -48,6 +49,9 @@ export class ShotcutsComponent extends EngineComponent {
case 'z':
this.events.emit('undo');
break;
case 'd':
this.events.emit('duplicate');
break;
case 'y':
this.events.emit('redo');
break;

View File

@ -30,9 +30,9 @@ import {
SelectionEvent,
TransformModeEvent,
} from '../types/events';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { TransformControls } from 'three/addons/controls/TransformControls.js';
import { ViewHelper } from 'three/addons/helpers/ViewHelper.js';
import { CameraControls } from './controls';
/**
* This component does most of the work related to editing the level.
@ -45,7 +45,6 @@ export class WorkspaceComponent extends EngineComponent {
public world = new World();
public helpers = new Object3D();
public orbitControls!: OrbitControls;
public transformControls!: TransformControls;
private grid!: GridHelper;
private box!: BoxHelper;
@ -62,6 +61,8 @@ export class WorkspaceComponent extends EngineComponent {
public clipboard: Object3D[] = [];
public cleanUpEvents?: Function;
public cameraControls!: CameraControls;
constructor(
protected renderer: Renderer,
protected events: EventEmitter<EditorEvents>
@ -77,9 +78,10 @@ export class WorkspaceComponent extends EngineComponent {
}
update(dt: number) {
this.orbitControls?.update();
this.box?.update();
this.cameraControls?.update(dt);
if (this.viewHelper.animating) {
this.viewHelper.update(dt);
}
@ -304,6 +306,28 @@ export class WorkspaceComponent extends EngineComponent {
});
};
const duplicateEvent = () => {
const filteredSelection = this.selection.filter(
(entry) => !(entry as GameObject).virtual
);
if (!filteredSelection.length) return;
this.selection.length = 0;
filteredSelection.forEach((entry) => {
const newObject = this.cutOperation ? entry : entry.clone();
this.world.add(newObject);
this.selection.push(newObject);
});
this.selection.forEach((entry) =>
this.events.emit('selected', {
object: entry,
selection: this.selection,
multi: this.selection.length > 1,
})
);
};
const pasteEvent = (target?: Object3D) => {
if (!this.clipboard.length) return;
this.selection.length = 0;
@ -387,6 +411,14 @@ export class WorkspaceComponent extends EngineComponent {
this.events.emit('resetHistory');
};
const insertEvent = (item: string) => {
this.events.once('sceneJoin', (object) => {
this.events.emit('select', { object });
});
this.events.emit('instance', { type: item });
};
this.events.addListener('mouseDown', mouseDownEventHandler);
this.events.addListener('mouseMove', mouseMoveEventHandler);
this.events.addListener('mouseUp', mouseUpEventHandler);
@ -403,6 +435,8 @@ export class WorkspaceComponent extends EngineComponent {
this.events.addListener('delete', deleteEvent);
this.events.addListener('undo', undoEvent);
this.events.addListener('reset', resetEvent);
this.events.addListener('duplicate', duplicateEvent);
this.events.addListener('insert', insertEvent);
return () => {
this.events.removeEventListener('mouseDown', mouseDownEventHandler);
@ -424,6 +458,8 @@ export class WorkspaceComponent extends EngineComponent {
this.events.removeEventListener('delete', deleteEvent);
this.events.removeEventListener('undo', undoEvent);
this.events.removeEventListener('reset', resetEvent);
this.events.removeEventListener('duplicate', duplicateEvent);
this.events.removeEventListener('insert', insertEvent);
};
}
@ -452,7 +488,7 @@ export class WorkspaceComponent extends EngineComponent {
let rotationSnap: number | null = MathUtils.degToRad(15);
let mode: 'translate' | 'rotate' | 'scale' = 'translate';
if (this.orbitControls) this.orbitControls.dispose();
if (this.cameraControls) this.cameraControls.dispose();
if (this.transformControls) {
translationSnap = this.transformControls.translationSnap;
scaleSnap = (this.transformControls as any).scaleSnap; // FIXME: typedef bug
@ -462,7 +498,7 @@ export class WorkspaceComponent extends EngineComponent {
this.transformControls.dispose();
}
this.orbitControls = new OrbitControls(
this.cameraControls = new CameraControls(
this.renderer.camera,
this.renderer.renderer.domElement
);
@ -476,10 +512,7 @@ export class WorkspaceComponent extends EngineComponent {
this.transformControls.setMode(mode);
this.helpers.add(this.transformControls);
this.viewHelper.center = this.orbitControls.target;
this.transformControls.addEventListener('mouseDown', () => {
this.orbitControls.enabled = false;
const target = this.transformControls.object;
if (!target) return;
this.transforming = true;
@ -495,7 +528,6 @@ export class WorkspaceComponent extends EngineComponent {
});
this.transformControls.addEventListener('mouseUp', () => {
this.orbitControls.enabled = true;
this.transforming = false;
const target = this.transformControls.object;
if (!target) return;

View File

@ -43,6 +43,8 @@ export type Events = {
cut: () => void;
copy: () => void;
delete: () => void;
duplicate: () => void;
insert: (type: string) => void;
paste: (event: Object3D | undefined) => void;
resetHistory: () => void;
};

View File

@ -50,6 +50,7 @@ export class Brick extends GameObject3D {
set transparency(value: number) {
this.material.transparent = value != 0;
this.material.opacity = 1 - value;
this.material.needsUpdate = true;
}
set texture(path: string | undefined) {

View File

@ -1,8 +1,11 @@
import { Material, Object3D } from 'three';
export class Property {
constructor(
public name: string,
public type: any,
public exposed = true,
public validators: Function[] = []
public validators: Function[] = [],
public postChange?: (obj: Object3D | Material) => void
) {}
}