382 lines
11 KiB
TypeScript
382 lines
11 KiB
TypeScript
import {
|
|
Brick,
|
|
Cylinder,
|
|
EngineComponent,
|
|
EventEmitter,
|
|
GameObject,
|
|
MouseButtonEvent,
|
|
Renderer,
|
|
Sphere,
|
|
Wedge,
|
|
WedgeCorner,
|
|
WedgeInnerCorner,
|
|
} from '@freeblox/engine';
|
|
import {
|
|
AxesHelper,
|
|
BoxHelper,
|
|
Euler,
|
|
GridHelper,
|
|
Material,
|
|
Object3D,
|
|
Vector3,
|
|
} from 'three';
|
|
import { EditorEvents, 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 WorkspaceComponent 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 Brick();
|
|
test.position.set(2, 0, 2);
|
|
const test2 = new Cylinder();
|
|
test2.position.set(0, 0, 2);
|
|
const test3 = new Sphere();
|
|
test3.position.set(0, 0, 0);
|
|
const test4 = new Wedge();
|
|
test4.position.set(2, 0, 0);
|
|
const test5 = new WedgeCorner();
|
|
test5.position.set(4, 0, 0);
|
|
const test6 = new WedgeInnerCorner();
|
|
test6.position.set(4, 0, 2);
|
|
this.world.add(test, test2, test3, test4, test5, test6);
|
|
}
|
|
|
|
private removeFromScene(scene: Object3D) {
|
|
scene.remove(this.background, this.world, this.helpers);
|
|
}
|
|
|
|
private initializeSelector() {
|
|
let moved = false;
|
|
let clicked = false;
|
|
let casterDebounce: ReturnType<typeof setTimeout> | undefined;
|
|
|
|
const mouseDownEventHandler = () => {
|
|
moved = false;
|
|
clicked = true;
|
|
};
|
|
|
|
const mouseMoveEventHandler = () => {
|
|
if (casterDebounce || !clicked) return;
|
|
// Prevent miniscule mouse movement when clicking from preventing a selection
|
|
casterDebounce = setTimeout(() => {
|
|
moved = true;
|
|
}, 100);
|
|
};
|
|
|
|
const mouseUpEventHandler = (event: MouseButtonEvent) => {
|
|
clearTimeout(casterDebounce);
|
|
casterDebounce = undefined;
|
|
clicked = false;
|
|
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;
|
|
}
|
|
|
|
let object = event.target!.object;
|
|
if (
|
|
!(object instanceof GameObject) &&
|
|
object.parent instanceof GameObject
|
|
) {
|
|
object = object.parent;
|
|
}
|
|
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,
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|