custom camera controls
This commit is contained in:
parent
2e62620d1e
commit
4fe6c2ac6c
|
@ -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),
|
||||
}))
|
||||
);
|
||||
|
||||
|
|
|
@ -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 };
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
) {}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue