freeblox/packages/editor/src/editor/core/workspace.ts

354 lines
10 KiB
TypeScript

import { EngineComponent, EventEmitter, Renderer } from '@freeblox/engine';
import {
AxesHelper,
BoxGeometry,
BoxHelper,
Color,
Euler,
GridHelper,
Material,
Mesh,
MeshPhongMaterial,
Object3D,
Vector3,
} from 'three';
import {
EditorEvents,
MouseButtonEvent,
MouseMoveEvent,
SelectEvent,
TransformModeEvent,
} from '../types/events';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { TransformControls } from 'three/examples/jsm/controls/TransformControls.js';
export class EditorWorkspace extends EngineComponent {
public background = new Object3D();
public world = new Object3D();
public helpers = new Object3D();
public orbitControls!: OrbitControls;
public transformControls!: TransformControls;
private grid!: GridHelper;
private box!: BoxHelper;
private axes!: AxesHelper;
public transformPosition?: Vector3;
public transformRotation?: Euler;
public transformScale?: Vector3;
public selection: Object3D[] = [];
public eventCleanUp?: Function;
constructor(
protected renderer: Renderer,
protected events: EventEmitter<EditorEvents>
) {
super(renderer, events);
}
initialize() {
this.addToScene(this.renderer.scene);
this.initializeHelpers();
this.initializeControls();
this.eventCleanUp = this.initializeSelector();
}
update(dt: number) {
this.orbitControls?.update();
this.box?.update();
}
cleanUp(): void {
this.removeFromScene(this.renderer.scene);
this.eventCleanUp?.call(this);
}
private addToScene(scene: Object3D) {
this.background.name = '_background';
this.world.name = '_world';
this.helpers.name = '_helper';
scene.add(this.background, this.world, this.helpers);
const test = new Mesh(new BoxGeometry(), new MeshPhongMaterial());
test.position.set(2, 2, 2);
this.world.add(test);
}
private removeFromScene(scene: Object3D) {
scene.remove(this.background, this.world, this.helpers);
}
private initializeSelector() {
let moved = false;
const mouseDownEventHandler = () => {
moved = false;
};
const mouseMoveEventHandler = () => {
moved = true;
};
const mouseUpEventHandler = (event: MouseButtonEvent) => {
if (moved) return;
if (!event.target?.object) {
if (!this.selection.length) return;
const oldSelection = this.selection;
this.selection = [];
oldSelection.forEach((selection) =>
this.events.emit('deselect', {
object: selection,
selection: [],
picker: true,
})
);
return;
}
const object = event.target!.object;
if (this.selection.includes(object)) {
if (event.control) {
const index = this.selection.indexOf(object);
this.selection.splice(index, 1);
this.events.emit('deselect', {
object,
selection: this.selection,
picker: true,
});
return;
}
}
if (event.control) {
this.selection.push(object);
this.events.emit('select', {
object,
selection: this.selection,
multi: true,
picker: true,
});
return;
}
const notObject = this.selection.filter(
(entry) => entry.id !== object.id
);
const wasEmpty = !this.selection.length;
this.selection = [object];
if (wasEmpty) {
this.events.emit('select', {
object: object,
selection: [object],
picker: true,
});
}
notObject.forEach((entry) =>
this.events.emit('deselect', {
object: entry,
selection: this.selection,
picker: true,
})
);
};
const selectHandler = (select: SelectEvent) => {
if (!select.picker) {
if (select.multi) {
this.selection.push(select.object);
return;
}
this.selection = [select.object];
}
const attachTo = this.selection[this.selection.length - 1];
this.transformControls?.attach(attachTo);
this.box.setFromObject(attachTo);
this.box.visible = true;
};
const deselectHandler = (select: SelectEvent) => {
if (!select.picker) {
const index = this.selection.indexOf(select.object);
this.selection.splice(index, 1);
}
if (this.selection.length) {
const attachTo = this.selection[this.selection.length - 1];
this.transformControls?.attach(attachTo);
this.box.setFromObject(attachTo);
this.box.visible = true;
return;
}
this.transformControls?.detach();
this.box.visible = false;
};
const transformMode = (mode: TransformModeEvent) => {
if (!this.transformControls) return;
if (!mode) {
this.transformControls.enabled = false;
return;
}
this.transformControls.setMode(mode);
};
const transformSnap = (value: number) => {
if (!this.transformControls) return;
this.transformControls.setTranslationSnap(value);
this.transformControls.setScaleSnap(value);
};
const transformRotationSnap = (value: number) => {
if (!this.transformControls) return;
this.transformControls.setRotationSnap(value);
};
this.events.addListener('mouseDown', mouseDownEventHandler);
this.events.addListener('mouseMove', mouseMoveEventHandler);
this.events.addListener('mouseUp', mouseUpEventHandler);
this.events.addListener('select', selectHandler);
this.events.addListener('deselect', deselectHandler);
this.events.addListener('transformMode', transformMode);
this.events.addListener('transformSnap', transformSnap);
this.events.addListener('transformRotationSnap', transformRotationSnap);
return () => {
this.events.removeEventListener('mouseDown', mouseDownEventHandler);
this.events.removeEventListener('mouseMove', mouseMoveEventHandler);
this.events.removeEventListener('mouseUp', mouseUpEventHandler);
this.events.removeEventListener('select', selectHandler);
this.events.removeEventListener('deselect', deselectHandler);
this.events.removeEventListener('transformMode', transformMode);
this.events.removeEventListener('transformSnap', transformSnap);
this.events.removeEventListener(
'transformRotationSnap',
transformRotationSnap
);
};
}
private initializeHelpers() {
this.grid = new GridHelper(100, 100);
this.background.add(this.grid);
this.box = new BoxHelper(this.world, 0x00a2ff);
(this.box.material as Material).depthTest = false;
(this.box.material as Material).transparent = true;
this.box.visible = false;
this.helpers.add(this.box);
this.axes = new AxesHelper(50);
this.helpers.add(this.axes);
}
private initializeControls() {
let translationSnap: number | null = 0.5;
let scaleSnap: number | null = 0.5;
let rotationSnap: number | null = null;
let mode: 'translate' | 'rotate' | 'scale' = 'translate';
if (this.orbitControls) this.orbitControls.dispose();
if (this.transformControls) {
translationSnap = this.transformControls.translationSnap;
scaleSnap = (this.transformControls as any).scaleSnap; // FIXME: typedef bug
rotationSnap = this.transformControls.rotationSnap;
mode = this.transformControls.getMode();
this.helpers.remove(this.transformControls);
this.transformControls.dispose();
}
this.orbitControls = new OrbitControls(
this.renderer.camera,
this.renderer.renderer.domElement
);
this.transformControls = new TransformControls(
this.renderer.camera,
this.renderer.renderer.domElement
);
this.transformControls.translationSnap = translationSnap;
(this.transformControls as any).scaleSnap = scaleSnap;
this.transformControls.rotationSnap = rotationSnap;
this.transformControls.setMode(mode);
this.helpers.add(this.transformControls);
this.transformControls.addEventListener('mouseDown', () => {
this.orbitControls.enabled = false;
const target = this.transformControls.object;
if (!target) return;
this.transformPosition = target.position.clone();
this.transformRotation = target.rotation.clone();
this.transformScale = target.scale.clone();
this.events.emit('transformStart', {
position: this.transformPosition!,
rotation: this.transformRotation!,
scale: this.transformScale!,
object: target,
});
});
this.transformControls.addEventListener('mouseUp', () => {
this.orbitControls.enabled = true;
const target = this.transformControls.object;
if (!target) return;
this.events.emit('transformEnd', {
position: target.position.clone(),
rotation: target.rotation.clone(),
scale: target.scale.clone(),
object: target,
});
});
this.transformControls.addEventListener('change', () => {
const target = this.transformControls.object;
if (!target) return;
this.events.emit('transformChange', {
lastPosition: this.transformPosition!,
lastRotation: this.transformRotation!,
lastScale: this.transformScale!,
position: target.position.clone(),
rotation: target.rotation.clone(),
scale: target.scale.clone(),
object: target,
});
if (
this.transformPosition &&
!this.transformPosition.equals(target.position)
) {
this.events.emit('change', {
object: target,
property: 'position',
value: target.position.clone(),
transformed: true,
});
}
if (
this.transformRotation &&
!this.transformRotation.equals(target.rotation)
) {
this.events.emit('change', {
object: target,
property: 'rotation',
value: target.rotation.clone(),
transformed: true,
});
}
if (this.transformScale && !this.transformScale.equals(target.scale)) {
this.events.emit('change', {
object: target,
property: 'scale',
value: target.scale.clone(),
transformed: true,
});
}
});
}
}