sidebar
This commit is contained in:
parent
8e073effa8
commit
b90e43bf79
|
@ -29,6 +29,7 @@
|
|||
"devDependencies": {
|
||||
"@types/three": "^0.152.1",
|
||||
"@vitejs/plugin-vue": "^4.1.0",
|
||||
"sass": "^1.62.1",
|
||||
"tslib": "^2.5.3",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.3.9",
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
<template>
|
||||
<div class="sidebar">
|
||||
<SidebarPanel>
|
||||
<template #title>Explorer</template>
|
||||
<SidebarRow
|
||||
v-for="object of items"
|
||||
:item="object"
|
||||
:selectionMap="selectionMap"
|
||||
:depth="0"
|
||||
@select="selectItem"
|
||||
/>
|
||||
</SidebarPanel>
|
||||
|
||||
<SidebarPanel>
|
||||
<template #title>Properties</template>
|
||||
asd
|
||||
</SidebarPanel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onMounted, ref, shallowRef } from 'vue';
|
||||
import { useEditorEvents } from '../composables/use-editor-events';
|
||||
import { Editor, SelectionEvent } from '../editor';
|
||||
import SidebarPanel from './sidebar/SidebarPanel.vue';
|
||||
import { GameObject } from '@freeblox/engine';
|
||||
import { Object3D } from 'three';
|
||||
import SidebarRow from './sidebar/SidebarRow.vue';
|
||||
|
||||
const items = shallowRef<Object3D[]>([]);
|
||||
const selectionMap = ref<string[]>([]);
|
||||
|
||||
const props = defineProps<{
|
||||
editor: Editor;
|
||||
}>();
|
||||
|
||||
const { register } = useEditorEvents(props.editor);
|
||||
|
||||
const createSceneMap = () => {
|
||||
if (!props.editor?.running) return;
|
||||
const sceneTree = props.editor.getSceneTree();
|
||||
items.value = [sceneTree.world, sceneTree.environment];
|
||||
};
|
||||
|
||||
const updateSelectionMap = (event: SelectionEvent) => {
|
||||
selectionMap.value = event.selection.map((item) => item.uuid);
|
||||
};
|
||||
|
||||
const selectItem = (item: Object3D, ctrl: boolean) => {
|
||||
props.editor.events.emit('select', {
|
||||
object: item as GameObject,
|
||||
multi: ctrl,
|
||||
});
|
||||
};
|
||||
|
||||
register('initialized', () => createSceneMap());
|
||||
register('selected', (event) => updateSelectionMap(event));
|
||||
register('deselected', (event) => updateSelectionMap(event));
|
||||
|
||||
onMounted(() => createSceneMap());
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.sidebar {
|
||||
display: grid;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
width: 360px;
|
||||
}
|
||||
</style>
|
|
@ -1,23 +1,56 @@
|
|||
<template>
|
||||
<div class="editor-wrapper" ref="wrapperRef"></div>
|
||||
<div class="editor">
|
||||
<Toolbar :editor="editorRef" />
|
||||
<TransformControls :editor="editorRef" />
|
||||
<div class="editor-row">
|
||||
<div class="viewport">
|
||||
<div class="canvas-wrapper" ref="wrapperRef"></div>
|
||||
</div>
|
||||
<EditorSidebar :editor="editorRef" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, shallowRef } from 'vue';
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref, shallowRef } from 'vue';
|
||||
import { Editor } from '../editor';
|
||||
import Toolbar from './Toolbar.vue';
|
||||
import TransformControls from './TransformControls.vue';
|
||||
import EditorSidebar from './EditorSidebar.vue';
|
||||
|
||||
const wrapperRef = ref();
|
||||
const editorRef = shallowRef<Editor>(new Editor());
|
||||
|
||||
const resize = () => editorRef.value.viewport.setSizeFromWindow();
|
||||
const resize = () => editorRef.value.viewport.setSizeFromViewport();
|
||||
|
||||
onMounted(() => {
|
||||
editorRef.value.mount(wrapperRef.value);
|
||||
window.addEventListener('resize', resize);
|
||||
});
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', resize);
|
||||
editorRef.value?.stop();
|
||||
};
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', resize);
|
||||
editorRef.value?.stop();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.editor {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.viewport {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.editor-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
<template>
|
||||
<div class="toolbar"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Editor } from '../editor';
|
||||
|
||||
const props = defineProps<{
|
||||
editor: Editor;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update'): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.toolbar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 36px;
|
||||
background-color: #efefef;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,75 @@
|
|||
<template>
|
||||
<div class="button-bar">
|
||||
<button
|
||||
v-for="mode of modes"
|
||||
type="button"
|
||||
@click="changeMode(mode.name)"
|
||||
:class="{ active: mode.name === currentMode, 'mode-button': true }"
|
||||
>
|
||||
{{ mode.text }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { Editor, TransformModeEvent } from '../editor';
|
||||
import { useEditorEvents } from '../composables/use-editor-events';
|
||||
|
||||
const currentMode = ref<TransformModeEvent>('translate');
|
||||
|
||||
const modes: {
|
||||
name: TransformModeEvent;
|
||||
text: string;
|
||||
}[] = [
|
||||
{ name: 'translate', text: 'T' },
|
||||
{ name: 'rotate', text: 'R' },
|
||||
{ name: 'scale', text: 'S' },
|
||||
];
|
||||
|
||||
const props = defineProps<{
|
||||
editor: Editor;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update'): void;
|
||||
}>();
|
||||
|
||||
const { register } = useEditorEvents(props.editor);
|
||||
|
||||
function handleTransformMode(mode: TransformModeEvent) {
|
||||
currentMode.value = mode;
|
||||
}
|
||||
|
||||
function changeMode(mode: TransformModeEvent) {
|
||||
props.editor.events.emit('transformMode', mode);
|
||||
}
|
||||
|
||||
register('transformMode', handleTransformMode);
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.button-bar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: calc(32px + 36px);
|
||||
left: 32px;
|
||||
|
||||
.mode-button {
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
padding: 8px;
|
||||
background-color: #efefef;
|
||||
|
||||
&.active {
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,21 @@
|
|||
<template>
|
||||
<div class="sidebar-panel">
|
||||
<div class="sidebar-panel-title"><slot name="title" /></div>
|
||||
<div class="sidebar-panel-inner"><slot /></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.sidebar-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&-title {
|
||||
background-color: #e5e5e5;
|
||||
padding: 8px;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
color: #5a5a5a;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,66 @@
|
|||
<template>
|
||||
<div class="sidebar-row">
|
||||
<button
|
||||
type="button"
|
||||
:class="{ selected: !!selected, 'sidebar-row-button': true }"
|
||||
:style="{ paddingLeft: buttonPadding }"
|
||||
@click="click($event)"
|
||||
>
|
||||
{{ item.name }}
|
||||
</button>
|
||||
<div class="sidebar-row-children">
|
||||
<SidebarRow
|
||||
v-for="object of filtered(item.children)"
|
||||
:item="object"
|
||||
:selectionMap="selectionMap"
|
||||
:depth="depth + 1"
|
||||
@select="(o, c) => emit('select', o, c)"
|
||||
></SidebarRow>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { GameObject } from '@freeblox/engine';
|
||||
import { Object3D } from 'three';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
item: Object3D;
|
||||
selectionMap: string[];
|
||||
depth: number;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'update'): void;
|
||||
(e: 'select', item: Object3D, ctrl: boolean): void;
|
||||
}>();
|
||||
|
||||
const selected = computed(() => props.selectionMap?.includes(props.item.uuid));
|
||||
const buttonPadding = computed(() => props.depth * 8 + 8 + 'px');
|
||||
const click = ($event: MouseEvent) => {
|
||||
emit('select', props.item, $event.ctrlKey);
|
||||
};
|
||||
|
||||
const filtered = (items: Object3D[]) =>
|
||||
items.filter((item) => item instanceof GameObject);
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.sidebar-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&-button {
|
||||
background-color: #efefef;
|
||||
appearance: none;
|
||||
padding: 8px;
|
||||
border: 0;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
|
||||
&.selected {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,32 @@
|
|||
import { onBeforeUnmount, onMounted } from 'vue';
|
||||
import { Editor, EditorEvents } from '../editor';
|
||||
|
||||
export const useEditorEvents = (editor: Editor) => {
|
||||
const handlers: {
|
||||
[x: string]: (...args: any) => void;
|
||||
} = {};
|
||||
|
||||
const register = <E extends keyof EditorEvents>(
|
||||
event: E,
|
||||
fn: EditorEvents[E]
|
||||
) => {
|
||||
handlers[event as string] = fn;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
Object.keys(handlers).forEach((event) => {
|
||||
editor.events.addListener(event as keyof EditorEvents, handlers[event]);
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
Object.keys(handlers).forEach((event) => {
|
||||
editor.events.removeEventListener(
|
||||
event as keyof EditorEvents,
|
||||
handlers[event]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
return { register };
|
||||
};
|
|
@ -5,6 +5,7 @@ import {
|
|||
EventEmitter,
|
||||
GameRunner,
|
||||
Renderer,
|
||||
LevelComponent,
|
||||
} from '@freeblox/engine';
|
||||
import { EditorEvents } from '../types/events';
|
||||
import { WorkspaceComponent } from './workspace';
|
||||
|
@ -18,6 +19,7 @@ export class Editor extends GameRunner {
|
|||
public workspace!: WorkspaceComponent;
|
||||
public mouse!: MouseComponent;
|
||||
public environment!: EnvironmentComponent;
|
||||
public level!: LevelComponent;
|
||||
public running = false;
|
||||
|
||||
override mount(element: HTMLElement) {
|
||||
|
@ -36,6 +38,10 @@ export class Editor extends GameRunner {
|
|||
this.environment = new EnvironmentComponent(this.render, this.events);
|
||||
this.environment.initialize();
|
||||
|
||||
this.level = new LevelComponent(this.render, this.events);
|
||||
this.level.initialize();
|
||||
|
||||
this.viewport.setSizeFromViewport();
|
||||
this.start();
|
||||
}
|
||||
|
||||
|
@ -55,6 +61,7 @@ export class Editor extends GameRunner {
|
|||
override start() {
|
||||
this.running = true;
|
||||
this.loop(this.lastTick);
|
||||
this.events.emit('initialized');
|
||||
}
|
||||
|
||||
override stop() {
|
||||
|
@ -64,4 +71,12 @@ export class Editor extends GameRunner {
|
|||
this.mouse.cleanUp();
|
||||
this.render.cleanUp();
|
||||
}
|
||||
|
||||
public getSceneTree() {
|
||||
return this.level.getSceneTree();
|
||||
}
|
||||
|
||||
public export(name: string) {
|
||||
return this.level.serializeLevelSave(name);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import {
|
||||
Brick,
|
||||
ChangeEvent,
|
||||
Cylinder,
|
||||
EngineComponent,
|
||||
Environment,
|
||||
EventEmitter,
|
||||
GameObject,
|
||||
MouseButtonEvent,
|
||||
|
@ -20,13 +22,19 @@ import {
|
|||
Object3D,
|
||||
Vector3,
|
||||
} from 'three';
|
||||
import { EditorEvents, SelectEvent, TransformModeEvent } from '../types/events';
|
||||
import {
|
||||
EditorEvents,
|
||||
SelectEvent,
|
||||
SelectionEvent,
|
||||
TransformModeEvent,
|
||||
} from '../types/events';
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
||||
import { TransformControls } from 'three/examples/jsm/controls/TransformControls.js';
|
||||
import { World } from '@freeblox/engine/dist/gameobjects/world.object';
|
||||
|
||||
export class WorkspaceComponent extends EngineComponent {
|
||||
public background = new Object3D();
|
||||
public world = new Object3D();
|
||||
public world = new World();
|
||||
public helpers = new Object3D();
|
||||
|
||||
public orbitControls!: OrbitControls;
|
||||
|
@ -40,7 +48,7 @@ export class WorkspaceComponent extends EngineComponent {
|
|||
public transformScale?: Vector3;
|
||||
|
||||
public selection: Object3D[] = [];
|
||||
public eventCleanUp?: Function;
|
||||
public cleanUpEvents?: Function;
|
||||
|
||||
constructor(
|
||||
protected renderer: Renderer,
|
||||
|
@ -53,7 +61,7 @@ export class WorkspaceComponent extends EngineComponent {
|
|||
this.addToScene(this.renderer.scene);
|
||||
this.initializeHelpers();
|
||||
this.initializeControls();
|
||||
this.eventCleanUp = this.initializeSelector();
|
||||
this.cleanUpEvents = this.bindEvents();
|
||||
}
|
||||
|
||||
update(dt: number) {
|
||||
|
@ -63,12 +71,11 @@ export class WorkspaceComponent extends EngineComponent {
|
|||
|
||||
cleanUp(): void {
|
||||
this.removeFromScene(this.renderer.scene);
|
||||
this.eventCleanUp?.call(this);
|
||||
this.cleanUpEvents?.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);
|
||||
|
||||
|
@ -91,7 +98,7 @@ export class WorkspaceComponent extends EngineComponent {
|
|||
scene.remove(this.background, this.world, this.helpers);
|
||||
}
|
||||
|
||||
private initializeSelector() {
|
||||
private bindEvents() {
|
||||
let moved = false;
|
||||
let clicked = false;
|
||||
let casterDebounce: ReturnType<typeof setTimeout> | undefined;
|
||||
|
@ -114,12 +121,64 @@ export class WorkspaceComponent extends EngineComponent {
|
|||
casterDebounce = undefined;
|
||||
clicked = false;
|
||||
if (moved) return;
|
||||
if (!event.target?.object) {
|
||||
|
||||
let object = event.target?.object;
|
||||
if (object && !(object instanceof GameObject) && object.parent) {
|
||||
object = object.parent;
|
||||
}
|
||||
|
||||
this.events.emit('select', {
|
||||
object: object as GameObject,
|
||||
multi: event.control,
|
||||
});
|
||||
};
|
||||
|
||||
const selectedHandler = (select: SelectionEvent) => {
|
||||
if (!select.picker) {
|
||||
if (select.multi) {
|
||||
this.selection.push(select.object);
|
||||
return;
|
||||
}
|
||||
this.selection = [select.object];
|
||||
}
|
||||
|
||||
const attachTo = this.selection.find(
|
||||
(item) => !(item as GameObject).virtual
|
||||
);
|
||||
if (attachTo) {
|
||||
this.transformControls?.attach(attachTo);
|
||||
this.box.setFromObject(attachTo);
|
||||
this.box.visible = true;
|
||||
}
|
||||
};
|
||||
|
||||
const deselectedHandler = (select: SelectionEvent) => {
|
||||
if (!select.picker) {
|
||||
const index = this.selection.indexOf(select.object);
|
||||
this.selection.splice(index, 1);
|
||||
}
|
||||
|
||||
const attachTo = this.selection.find(
|
||||
(item) => !(item as GameObject).virtual
|
||||
);
|
||||
if (this.selection.length && attachTo) {
|
||||
this.transformControls?.attach(attachTo);
|
||||
this.box.setFromObject(attachTo);
|
||||
this.box.visible = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.transformControls?.detach();
|
||||
this.box.visible = false;
|
||||
};
|
||||
|
||||
const selectHandler = (event: SelectEvent) => {
|
||||
if (!event.object) {
|
||||
if (!this.selection.length) return;
|
||||
const oldSelection = this.selection;
|
||||
this.selection = [];
|
||||
oldSelection.forEach((selection) =>
|
||||
this.events.emit('deselect', {
|
||||
this.events.emit('deselected', {
|
||||
object: selection,
|
||||
selection: [],
|
||||
picker: true,
|
||||
|
@ -128,18 +187,12 @@ export class WorkspaceComponent extends EngineComponent {
|
|||
return;
|
||||
}
|
||||
|
||||
let object = event.target!.object;
|
||||
if (
|
||||
!(object instanceof GameObject) &&
|
||||
object.parent instanceof GameObject
|
||||
) {
|
||||
object = object.parent;
|
||||
}
|
||||
let object = event.object;
|
||||
if (this.selection.includes(object)) {
|
||||
if (event.control) {
|
||||
if (event.multi) {
|
||||
const index = this.selection.indexOf(object);
|
||||
this.selection.splice(index, 1);
|
||||
this.events.emit('deselect', {
|
||||
this.events.emit('deselected', {
|
||||
object,
|
||||
selection: this.selection,
|
||||
picker: true,
|
||||
|
@ -148,9 +201,9 @@ export class WorkspaceComponent extends EngineComponent {
|
|||
}
|
||||
}
|
||||
|
||||
if (event.control) {
|
||||
if (event.multi) {
|
||||
this.selection.push(object);
|
||||
this.events.emit('select', {
|
||||
this.events.emit('selected', {
|
||||
object,
|
||||
selection: this.selection,
|
||||
multi: true,
|
||||
|
@ -166,7 +219,7 @@ export class WorkspaceComponent extends EngineComponent {
|
|||
const wasEmpty = !this.selection.length;
|
||||
this.selection = [object];
|
||||
if (wasEmpty) {
|
||||
this.events.emit('select', {
|
||||
this.events.emit('selected', {
|
||||
object: object,
|
||||
selection: [object],
|
||||
picker: true,
|
||||
|
@ -174,7 +227,7 @@ export class WorkspaceComponent extends EngineComponent {
|
|||
}
|
||||
|
||||
notObject.forEach((entry) =>
|
||||
this.events.emit('deselect', {
|
||||
this.events.emit('deselected', {
|
||||
object: entry,
|
||||
selection: this.selection,
|
||||
picker: true,
|
||||
|
@ -182,39 +235,6 @@ export class WorkspaceComponent extends EngineComponent {
|
|||
);
|
||||
};
|
||||
|
||||
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) {
|
||||
|
@ -235,27 +255,39 @@ export class WorkspaceComponent extends EngineComponent {
|
|||
this.transformControls.setRotationSnap(value);
|
||||
};
|
||||
|
||||
const changeListener = (change: ChangeEvent) => {
|
||||
if (change.object instanceof Environment) {
|
||||
this.events.emit('setEnvironment', {
|
||||
[change.property]: change.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('selected', selectedHandler);
|
||||
this.events.addListener('deselected', deselectedHandler);
|
||||
this.events.addListener('transformMode', transformMode);
|
||||
this.events.addListener('transformSnap', transformSnap);
|
||||
this.events.addListener('transformRotationSnap', transformRotationSnap);
|
||||
this.events.addListener('change', changeListener);
|
||||
|
||||
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('selected', selectedHandler);
|
||||
this.events.removeEventListener('deselected', deselectedHandler);
|
||||
this.events.removeEventListener('transformMode', transformMode);
|
||||
this.events.removeEventListener('transformSnap', transformSnap);
|
||||
this.events.removeEventListener(
|
||||
'transformRotationSnap',
|
||||
transformRotationSnap
|
||||
);
|
||||
this.events.removeEventListener('change', changeListener);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
export * from './core/editor';
|
||||
export * from './types/events';
|
||||
|
|
|
@ -16,21 +16,18 @@ export interface TransformCompleteEvent extends TransformEvent {
|
|||
lastScale: Vector3;
|
||||
}
|
||||
|
||||
export interface ChangeEvent {
|
||||
object: Object3D;
|
||||
property: string;
|
||||
value: any;
|
||||
edited?: boolean;
|
||||
transformed?: boolean;
|
||||
}
|
||||
|
||||
export interface SelectEvent {
|
||||
export interface SelectionEvent {
|
||||
object: Object3D;
|
||||
selection: Object3D[];
|
||||
multi?: boolean;
|
||||
picker?: boolean;
|
||||
}
|
||||
|
||||
export interface SelectEvent {
|
||||
object: Object3D;
|
||||
multi?: boolean;
|
||||
}
|
||||
|
||||
export type Events = {
|
||||
transformStart: (event: TransformEvent) => void;
|
||||
transformChange: (event: TransformCompleteEvent) => void;
|
||||
|
@ -38,9 +35,9 @@ export type Events = {
|
|||
transformMode: (event: TransformModeEvent) => void;
|
||||
transformSnap: (event: number) => void;
|
||||
transformRotationSnap: (event: number) => void;
|
||||
change: (event: ChangeEvent) => void;
|
||||
select: (event: SelectEvent) => void;
|
||||
deselect: (event: SelectEvent) => void;
|
||||
selected: (event: SelectionEvent) => void;
|
||||
deselected: (event: SelectionEvent) => void;
|
||||
};
|
||||
|
||||
export type EditorEvents = Events & EngineEvents;
|
||||
|
|
|
@ -6,3 +6,7 @@
|
|||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export * from './manager';
|
|
@ -0,0 +1,134 @@
|
|||
import { CubeTexture, CubeTextureLoader, Texture, TextureLoader } from 'three';
|
||||
import { Asset } from '../types/asset';
|
||||
|
||||
export class AssetManagerFactory {
|
||||
public assets: Asset[] = [];
|
||||
public texureLoader = new TextureLoader();
|
||||
public cubeTextureLoader = new CubeTextureLoader();
|
||||
|
||||
/**
|
||||
* Get an asset by path, this is an unique identifier
|
||||
* @param path Asset path
|
||||
*/
|
||||
getAssetByPath(path: string) {
|
||||
return this.assets.find((entry) => entry.path === path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new texture asset
|
||||
* @param data Data URI
|
||||
* @param name Asset name
|
||||
* @param type Texture type
|
||||
*/
|
||||
async createAsset(
|
||||
data: string,
|
||||
name: string,
|
||||
type: 'Texture' | 'CubeTexture' = 'Texture'
|
||||
) {
|
||||
const asset: Asset = {
|
||||
name,
|
||||
type,
|
||||
data,
|
||||
};
|
||||
|
||||
await this.load(asset);
|
||||
|
||||
asset.path = `fblxassetid:${asset.texture!.uuid}`;
|
||||
this.assets.push(asset);
|
||||
|
||||
return asset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load asset into a Texture
|
||||
* @param asset Remote or local asset data
|
||||
*/
|
||||
async load(asset: Asset) {
|
||||
return (
|
||||
asset.type === 'Texture'
|
||||
? this.loadTextureData(
|
||||
asset.remote ? asset.path : asset.data,
|
||||
asset.name
|
||||
)
|
||||
: this.loadCubeTexture(
|
||||
asset.remote ? asset.path : asset.data,
|
||||
asset.name
|
||||
)
|
||||
).then((texture) => {
|
||||
asset.texture = texture;
|
||||
return asset;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load assets into Textures
|
||||
* @param assets Remote or local asset data
|
||||
*/
|
||||
async loadAll(assets: Asset[]) {
|
||||
const loaded = await Promise.allSettled(
|
||||
assets.map((item) => this.load(item))
|
||||
);
|
||||
|
||||
loaded
|
||||
.filter((entry) => entry.status === 'rejected')
|
||||
.forEach((error) => console.error('Failed loading asset', error));
|
||||
|
||||
this.assets.push(
|
||||
...loaded
|
||||
.filter((entry) => entry.status === 'fulfilled')
|
||||
.map((fulfilled) => (fulfilled as PromiseFulfilledResult<Asset>).value)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize for save file without Texture data
|
||||
*/
|
||||
serialize(): Asset[] {
|
||||
return this.assets.map((entry) => ({
|
||||
name: entry.name,
|
||||
path: entry.path,
|
||||
data: !entry.remote ? entry.data : undefined,
|
||||
type: entry.type,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Load texture
|
||||
* @param path Path
|
||||
* @param name Texture name
|
||||
*/
|
||||
private async loadTextureData(path: string, name?: string) {
|
||||
return new Promise<Texture>((resolve, reject) => {
|
||||
this.texureLoader.load(
|
||||
path,
|
||||
(texture) => {
|
||||
texture.name = name || 'Texture';
|
||||
resolve(texture);
|
||||
},
|
||||
undefined,
|
||||
reject
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load cube texture loader
|
||||
* @param path pos-x, neg-x, pos-y, neg-y, pos-z, neg-z
|
||||
* @param name Texture name
|
||||
*/
|
||||
private async loadCubeTexture(path: string[], name?: string) {
|
||||
return new Promise<CubeTexture>((resolve, reject) => {
|
||||
this.cubeTextureLoader.load(
|
||||
path,
|
||||
(texture) => {
|
||||
texture.name = name || 'Texture';
|
||||
resolve(texture);
|
||||
},
|
||||
undefined,
|
||||
reject
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const assetManager = new AssetManagerFactory();
|
|
@ -3,11 +3,13 @@ import { EngineEvents, EnvironmentEvent } from '../types/events';
|
|||
import { EngineComponent } from '../types/engine-component';
|
||||
import { Renderer } from '../core/renderer';
|
||||
import { EventEmitter } from '../utils/events';
|
||||
import { Environment } from '../gameobjects/environment.object';
|
||||
|
||||
export class EnvironmentComponent extends EngineComponent {
|
||||
public ambient!: AmbientLight;
|
||||
public directional!: DirectionalLight;
|
||||
private handlerCleanUp?: Function;
|
||||
public object = new Environment();
|
||||
private cleanUpEvents?: Function;
|
||||
|
||||
constructor(
|
||||
protected renderer: Renderer,
|
||||
|
@ -17,6 +19,7 @@ export class EnvironmentComponent extends EngineComponent {
|
|||
}
|
||||
|
||||
initialize(): void {
|
||||
this.renderer.scene.add(this.object);
|
||||
this.renderer.renderer.setClearColor(0x00aaff);
|
||||
|
||||
this.ambient = new AmbientLight(0x8a8a8a, 1.0);
|
||||
|
@ -25,7 +28,7 @@ export class EnvironmentComponent extends EngineComponent {
|
|||
|
||||
this.renderer.scene.add(this.ambient);
|
||||
this.renderer.scene.add(this.directional);
|
||||
this.handlerCleanUp = this.initializeEvents();
|
||||
this.cleanUpEvents = this.bindEvents();
|
||||
}
|
||||
|
||||
update(delta: number): void {}
|
||||
|
@ -33,10 +36,10 @@ export class EnvironmentComponent extends EngineComponent {
|
|||
cleanUp(): void {
|
||||
this.renderer.scene.remove(this.ambient);
|
||||
this.renderer.scene.remove(this.directional);
|
||||
this.handlerCleanUp?.call(this);
|
||||
this.cleanUpEvents?.call(this);
|
||||
}
|
||||
|
||||
private initializeEvents() {
|
||||
private bindEvents() {
|
||||
const setEnvironmentEvent = (event: EnvironmentEvent) => {
|
||||
if (event.sunColor) this.directional.color = new Color(event.sunColor);
|
||||
if (event.sunPosition) this.directional.position.copy(event.sunPosition);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export * from './environment';
|
||||
export * from './viewport';
|
||||
export * from './mouse';
|
||||
export * from './level';
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
import { Renderer } from '../core/renderer';
|
||||
import { EngineComponent } from '../types/engine-component';
|
||||
import { EngineEvents } from '../types/events';
|
||||
import { EventEmitter } from '../utils/events';
|
||||
import { Environment } from '../gameobjects/environment.object';
|
||||
import { assetManager } from '../assets/manager';
|
||||
import { WorldFile } from '../types/world-file';
|
||||
import { World } from '../gameobjects/world.object';
|
||||
|
||||
export class LevelComponent extends EngineComponent {
|
||||
private world!: World;
|
||||
private environment!: Environment;
|
||||
private cleanUpEvents?: Function;
|
||||
|
||||
constructor(
|
||||
protected renderer: Renderer,
|
||||
protected events: EventEmitter<EngineEvents>
|
||||
) {
|
||||
super(renderer, events);
|
||||
}
|
||||
|
||||
initialize(): void {
|
||||
this.world = this.renderer.scene.getObjectByName('World') as World;
|
||||
this.environment = this.renderer.scene.getObjectByName(
|
||||
'Environment'
|
||||
) as Environment;
|
||||
this.cleanUpEvents = this.bindEvents();
|
||||
}
|
||||
|
||||
update(delta: number): void {}
|
||||
|
||||
cleanUp(): void {
|
||||
this.cleanUpEvents?.call(this);
|
||||
}
|
||||
|
||||
public getSceneTree() {
|
||||
return {
|
||||
world: this.world,
|
||||
environment: this.environment,
|
||||
};
|
||||
}
|
||||
|
||||
public serializeLevelSave(name: string): WorldFile {
|
||||
const world = this.world.serialize();
|
||||
const environment = this.environment.serialize();
|
||||
const assets = assetManager.serialize();
|
||||
return {
|
||||
name,
|
||||
world,
|
||||
environment,
|
||||
assets,
|
||||
};
|
||||
}
|
||||
|
||||
private bindEvents() {
|
||||
return () => {};
|
||||
}
|
||||
}
|
|
@ -3,12 +3,12 @@ import { EngineComponent } from '../types/engine-component';
|
|||
import { Renderer } from '../core/renderer';
|
||||
import { EngineEvents } from '../types/events';
|
||||
import { EventEmitter } from '../utils/events';
|
||||
import { World } from '../gameobjects/world.object';
|
||||
|
||||
type MouseMap = [boolean, boolean, boolean];
|
||||
type EventMap = [string, Function];
|
||||
|
||||
export class MouseComponent extends EngineComponent {
|
||||
private world!: Object3D;
|
||||
private world!: World;
|
||||
|
||||
private mouseButtons: MouseMap = [false, false, false];
|
||||
private mouseButtonsLast: MouseMap = [false, false, false];
|
||||
|
@ -17,8 +17,9 @@ export class MouseComponent extends EngineComponent {
|
|||
private mousePositionGL = new Vector2(0, 0);
|
||||
private mousePositionLast = new Vector2(0, 0);
|
||||
private mousePositionGLLast = new Vector2(0, 0);
|
||||
private canvasOffset = new Vector2(0, 0);
|
||||
|
||||
private boundEvents: EventMap[] = [];
|
||||
private cleanUpEvents?: Function;
|
||||
private ray = new Raycaster();
|
||||
|
||||
constructor(
|
||||
|
@ -32,9 +33,21 @@ export class MouseComponent extends EngineComponent {
|
|||
return this.renderer.renderer.domElement;
|
||||
}
|
||||
|
||||
initialize(): void {
|
||||
this.world = this.renderer.scene.getObjectByName('_world')!;
|
||||
initialize() {
|
||||
this.world = this.renderer.scene.getObjectByName('World') as World;
|
||||
this.cleanUpEvents = this.bindEvents();
|
||||
}
|
||||
|
||||
update(delta: number): void {
|
||||
this.ray.setFromCamera(this.mousePositionGL, this.renderer.camera);
|
||||
this.mouseButtonsLast = [...this.mouseButtons];
|
||||
}
|
||||
|
||||
cleanUp(): void {
|
||||
this.cleanUpEvents?.call(this);
|
||||
}
|
||||
|
||||
private bindEvents() {
|
||||
const mouseDown = (ev: MouseEvent) => {
|
||||
const [object] = this.ray.intersectObjects(this.world.children, true);
|
||||
this.mouseButtons[ev.button] = true;
|
||||
|
@ -67,7 +80,7 @@ export class MouseComponent extends EngineComponent {
|
|||
this.mousePositionLast = this.mousePosition.clone();
|
||||
this.mousePositionGLLast = this.mousePositionGL.clone();
|
||||
|
||||
this.mousePosition.set(ev.clientX, ev.clientY);
|
||||
this.mousePosition.set(ev.clientX, ev.clientY).sub(this.canvasOffset);
|
||||
this.mousePositionGL.set(
|
||||
(this.mousePosition.x / this.renderer.resolution.x) * 2 - 1,
|
||||
-(this.mousePosition.y / this.renderer.resolution.y) * 2 + 1
|
||||
|
@ -83,27 +96,24 @@ export class MouseComponent extends EngineComponent {
|
|||
|
||||
const contextMenu = (ev: MouseEvent) => ev.preventDefault();
|
||||
|
||||
const onResize = () => {
|
||||
const calculate = this.canvas.getBoundingClientRect();
|
||||
this.canvasOffset.set(calculate.left, calculate.top);
|
||||
};
|
||||
|
||||
this.canvas.addEventListener('mousedown', mouseDown);
|
||||
this.canvas.addEventListener('mousemove', mouseMove);
|
||||
this.canvas.addEventListener('mouseup', mouseUp);
|
||||
this.canvas.addEventListener('contextmenu', contextMenu);
|
||||
this.events.addListener('resize', onResize);
|
||||
onResize();
|
||||
|
||||
this.boundEvents.push(
|
||||
['mousedown', mouseDown],
|
||||
['mousemove', mouseMove],
|
||||
['mouseup', mouseUp],
|
||||
['contextmenu', contextMenu]
|
||||
);
|
||||
}
|
||||
|
||||
update(delta: number): void {
|
||||
this.ray.setFromCamera(this.mousePositionGL, this.renderer.camera);
|
||||
this.mouseButtonsLast = [...this.mouseButtons];
|
||||
}
|
||||
|
||||
cleanUp(): void {
|
||||
for (const [event, handler] of this.boundEvents) {
|
||||
this.canvas.removeEventListener(event, handler as EventListener);
|
||||
}
|
||||
return () => {
|
||||
this.canvas.removeEventListener('mousedown', mouseDown);
|
||||
this.canvas.removeEventListener('mousemove', mouseMove);
|
||||
this.canvas.removeEventListener('mouseup', mouseUp);
|
||||
this.canvas.removeEventListener('contextmenu', contextMenu);
|
||||
this.events.removeEventListener('resize', onResize);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import { Object3D } from 'three';
|
||||
import { Object3D, Vector2 } from 'three';
|
||||
import { EngineEvents } from '../types/events';
|
||||
import { EngineComponent } from '../types/engine-component';
|
||||
import { Renderer } from '../core/renderer';
|
||||
import { EventEmitter } from '../utils/events';
|
||||
|
||||
export class ViewportComponent extends EngineComponent {
|
||||
private cleanUpEvents?: Function;
|
||||
|
||||
constructor(
|
||||
protected render: Renderer,
|
||||
protected events: EventEmitter<EngineEvents>
|
||||
|
@ -21,13 +23,15 @@ export class ViewportComponent extends EngineComponent {
|
|||
}
|
||||
|
||||
initialize() {
|
||||
this.setSizeFromWindow();
|
||||
this.cleanUpEvents = this.bindEvents();
|
||||
this.camera.position.set(16, 8, 16);
|
||||
}
|
||||
|
||||
update(dt: number) {}
|
||||
|
||||
cleanUp(): void {}
|
||||
cleanUp(): void {
|
||||
this.cleanUpEvents?.call(this);
|
||||
}
|
||||
|
||||
setSize(width: number, height: number) {
|
||||
this.render.viewport.style.width = `${width}px`;
|
||||
|
@ -36,6 +40,31 @@ export class ViewportComponent extends EngineComponent {
|
|||
}
|
||||
|
||||
setSizeFromWindow() {
|
||||
this.setSize(window.innerWidth, window.innerHeight);
|
||||
this.events.emit(
|
||||
'resize',
|
||||
new Vector2(window.innerWidth, window.innerHeight)
|
||||
);
|
||||
}
|
||||
|
||||
setSizeFromViewport() {
|
||||
this.events.emit(
|
||||
'resize',
|
||||
new Vector2(
|
||||
this.render.viewport.parentElement!.clientWidth,
|
||||
this.render.viewport.parentElement!.clientHeight
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private bindEvents() {
|
||||
const resizeEvent = (size: Vector2) => {
|
||||
this.setSize(size.x, size.y);
|
||||
};
|
||||
|
||||
this.events.addListener('resize', resizeEvent);
|
||||
|
||||
return () => {
|
||||
this.events.removeEventListener('resize', resizeEvent);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ import {
|
|||
BufferGeometry,
|
||||
Color,
|
||||
ColorRepresentation,
|
||||
Material,
|
||||
Mesh,
|
||||
MeshPhongMaterial,
|
||||
} from 'three';
|
||||
|
@ -12,24 +11,29 @@ import {
|
|||
gameObject3DEditorProperties,
|
||||
} from '../types/game-object';
|
||||
import { Property } from '../types/property';
|
||||
import { gameObjectFactory } from './factory';
|
||||
import { gameObjectGeometries } from './geometries';
|
||||
import { assetManager } from '../assets/manager';
|
||||
import { AssetInfo } from '../types/asset';
|
||||
|
||||
export const brickEditorProperties: EditorProperties = {
|
||||
...gameObject3DEditorProperties,
|
||||
color: new Property('color', Color, true, []),
|
||||
transparency: new Property('transparency', Number, true, []),
|
||||
texture: new Property('texture', AssetInfo, true, []),
|
||||
};
|
||||
|
||||
export class Brick extends GameObject3D {
|
||||
public objectType = Brick.name;
|
||||
private texturePath?: string;
|
||||
protected material = new MeshPhongMaterial();
|
||||
protected mesh: Mesh = new Mesh(this.geometry, this.material);
|
||||
|
||||
constructor(
|
||||
protected geometry: BufferGeometry = gameObjectFactory.boxGeometry,
|
||||
protected geometry: BufferGeometry = gameObjectGeometries.boxGeometry,
|
||||
public editorProperties: EditorProperties = brickEditorProperties
|
||||
) {
|
||||
super(editorProperties);
|
||||
this.name = this.objectType;
|
||||
this.add(this.mesh);
|
||||
}
|
||||
|
||||
|
@ -47,4 +51,24 @@ export class Brick extends GameObject3D {
|
|||
this.material.transparent = value != 0;
|
||||
this.material.opacity = 1 - value;
|
||||
}
|
||||
|
||||
set texture(path: string | undefined) {
|
||||
if (!path) {
|
||||
this.material.map = null;
|
||||
this.texturePath = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
const asset = assetManager.getAssetByPath(path);
|
||||
if (!asset || !asset.texture) {
|
||||
console.error(`Asset ${path} does not exist or is not loaded`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.texturePath = path;
|
||||
this.material.map = asset.texture;
|
||||
}
|
||||
get texture() {
|
||||
return this.texturePath;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { Mesh, Color } from 'three';
|
||||
import { Property } from '../types/property';
|
||||
import { Mesh } from 'three';
|
||||
import { Brick } from './brick.object';
|
||||
import { gameObjectFactory } from './factory';
|
||||
import { gameObjectGeometries } from './geometries';
|
||||
|
||||
export class Cylinder extends Brick {
|
||||
public objectType = Cylinder.name;
|
||||
protected mesh = new Mesh(this.geometry, this.material);
|
||||
|
||||
constructor() {
|
||||
super(gameObjectFactory.cylinderGeometry);
|
||||
super(gameObjectGeometries.cylinderGeometry);
|
||||
this.name = this.objectType;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import { Color, Vector3 } from 'three';
|
||||
import {
|
||||
EditorProperties,
|
||||
GameObject,
|
||||
SerializedObject,
|
||||
} from '../types/game-object';
|
||||
import { Property } from '../types/property';
|
||||
|
||||
export const environmentEditorProperties: EditorProperties = {
|
||||
sunColor: new Property('sunColor', Color, true, []),
|
||||
sunPosition: new Property('sunPosition', Vector3, true, []),
|
||||
sunStrength: new Property('sunStrength', Number, true, []),
|
||||
ambientColor: new Property('ambientColor', Color, true, []),
|
||||
ambientStrength: new Property('ambientStrength', Number, true, []),
|
||||
clearColor: new Property('clearColor', Color, true, []),
|
||||
};
|
||||
|
||||
export class Environment extends GameObject {
|
||||
public objectType = Environment.name;
|
||||
public name = Environment.name;
|
||||
public virtual = true;
|
||||
|
||||
sunColor = new Color(0xffffff);
|
||||
sunPosition = new Vector3(1, 1, 1);
|
||||
sunStrength = 1;
|
||||
ambientColor = new Color(0x8a8a8a);
|
||||
ambientStrength = 1;
|
||||
clearColor = new Color(0x00aaff);
|
||||
|
||||
constructor() {
|
||||
super(environmentEditorProperties);
|
||||
}
|
||||
|
||||
override serialize() {
|
||||
return super.serialize() as SerializedEnvironment;
|
||||
}
|
||||
}
|
||||
|
||||
export interface SerializedEnvironment extends SerializedObject {
|
||||
sunColor: Color;
|
||||
sunPosition: Vector3;
|
||||
sunStrength: number;
|
||||
ambientColor: Color;
|
||||
ambientStrength: number;
|
||||
clearColor: Color;
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import { BoxGeometry, CylinderGeometry, SphereGeometry } from 'three';
|
||||
|
||||
class GameObjectFactory {
|
||||
class GameObjectGeometryFactory {
|
||||
public boxGeometry = new BoxGeometry();
|
||||
public sphereGeometry = new SphereGeometry(0.5);
|
||||
public cylinderGeometry = new CylinderGeometry(0.5, 0.5);
|
||||
|
@ -47,5 +47,5 @@ class GameObjectFactory {
|
|||
}
|
||||
}
|
||||
|
||||
export const gameObjectFactory = new GameObjectFactory();
|
||||
Object.freeze(gameObjectFactory);
|
||||
export const gameObjectGeometries = new GameObjectGeometryFactory();
|
||||
Object.freeze(gameObjectGeometries);
|
|
@ -1,6 +1,20 @@
|
|||
export * from './brick.object';
|
||||
export * from './cylinder.object';
|
||||
export * from './sphere.object';
|
||||
export * from './wedge.object';
|
||||
export * from './wedge-corner.object';
|
||||
export * from './wedge-inner-corner.object';
|
||||
import { Cylinder } from './cylinder.object';
|
||||
import { Brick } from './brick.object';
|
||||
import { Sphere } from './sphere.object';
|
||||
import { Wedge } from './wedge.object';
|
||||
import { WedgeCorner } from './wedge-corner.object';
|
||||
import { WedgeInnerCorner } from './wedge-inner-corner.object';
|
||||
import { GameObject } from '../types/game-object';
|
||||
import { Instancable } from '../types/instancable';
|
||||
|
||||
export const instancableGameObjects: Record<string, Instancable<GameObject>> = {
|
||||
[Brick.name]: Brick,
|
||||
[Cylinder.name]: Cylinder,
|
||||
[Sphere.name]: Sphere,
|
||||
[Wedge.name]: Wedge,
|
||||
[WedgeCorner.name]: WedgeCorner,
|
||||
[WedgeInnerCorner.name]: WedgeInnerCorner,
|
||||
};
|
||||
|
||||
export * from './environment.object';
|
||||
export { Cylinder, Brick, Sphere, Wedge, WedgeCorner, WedgeInnerCorner };
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { Mesh, Color } from 'three';
|
||||
import { Property } from '../types/property';
|
||||
import { Mesh } from 'three';
|
||||
import { Brick } from './brick.object';
|
||||
import { gameObjectFactory } from './factory';
|
||||
import { gameObjectGeometries } from './geometries';
|
||||
|
||||
export class Sphere extends Brick {
|
||||
public objectType = Sphere.name;
|
||||
protected mesh = new Mesh(this.geometry, this.material);
|
||||
|
||||
constructor() {
|
||||
super(gameObjectFactory.sphereGeometry);
|
||||
super(gameObjectGeometries.sphereGeometry);
|
||||
this.name = this.objectType;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { Mesh, Color } from 'three';
|
||||
import { Property } from '../types/property';
|
||||
import { Mesh } from 'three';
|
||||
import { Brick } from './brick.object';
|
||||
import { gameObjectFactory } from './factory';
|
||||
import { gameObjectGeometries } from './geometries';
|
||||
|
||||
export class WedgeCorner extends Brick {
|
||||
public objectType = WedgeCorner.name;
|
||||
protected mesh = new Mesh(this.geometry, this.material);
|
||||
|
||||
constructor() {
|
||||
super(gameObjectFactory.wedgeCornerGeometry);
|
||||
super(gameObjectGeometries.wedgeCornerGeometry);
|
||||
this.name = this.objectType;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { Mesh, Color } from 'three';
|
||||
import { Property } from '../types/property';
|
||||
import { Mesh } from 'three';
|
||||
import { Brick } from './brick.object';
|
||||
import { gameObjectFactory } from './factory';
|
||||
import { gameObjectGeometries } from './geometries';
|
||||
|
||||
export class WedgeInnerCorner extends Brick {
|
||||
public objectType = WedgeInnerCorner.name;
|
||||
protected mesh = new Mesh(this.geometry, this.material);
|
||||
|
||||
constructor() {
|
||||
super(gameObjectFactory.wedgeInnerCornerGeometry);
|
||||
super(gameObjectGeometries.wedgeInnerCornerGeometry);
|
||||
this.name = this.objectType;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { Mesh, Color } from 'three';
|
||||
import { Property } from '../types/property';
|
||||
import { Mesh } from 'three';
|
||||
import { Brick } from './brick.object';
|
||||
import { gameObjectFactory } from './factory';
|
||||
import { gameObjectGeometries } from './geometries';
|
||||
|
||||
export class Wedge extends Brick {
|
||||
public objectType = Wedge.name;
|
||||
protected mesh = new Mesh(this.geometry, this.material);
|
||||
|
||||
constructor() {
|
||||
super(gameObjectFactory.wedgeGeometry);
|
||||
super(gameObjectGeometries.wedgeGeometry);
|
||||
this.name = this.objectType;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import { GameObject } from '../types/game-object';
|
||||
|
||||
export class World extends GameObject {
|
||||
public objectType = World.name;
|
||||
public name = 'World';
|
||||
public virtual = true;
|
||||
}
|
|
@ -3,3 +3,4 @@ export * from './utils';
|
|||
export * from './types';
|
||||
export * from './components';
|
||||
export * from './gameobjects';
|
||||
export * from './assets';
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
import { Texture } from 'three';
|
||||
|
||||
export interface Asset {
|
||||
name: string;
|
||||
path?: string;
|
||||
type: 'Texture' | 'CubeTexture';
|
||||
texture?: Texture;
|
||||
data: any;
|
||||
remote?: boolean;
|
||||
}
|
||||
|
||||
export class AssetInfo {
|
||||
constructor(public path: string, public name: string) {}
|
||||
}
|
|
@ -5,6 +5,7 @@ import {
|
|||
ColorRepresentation,
|
||||
Vector3,
|
||||
} from 'three';
|
||||
import { WorldFile } from './world-file';
|
||||
|
||||
export interface MousePositionEvent {
|
||||
position: Vector2;
|
||||
|
@ -35,10 +36,26 @@ export interface EnvironmentEvent {
|
|||
clearColor?: ColorRepresentation;
|
||||
}
|
||||
|
||||
export interface ChangeEvent {
|
||||
object: Object3D;
|
||||
property: string;
|
||||
value: any;
|
||||
edited?: boolean;
|
||||
transformed?: boolean;
|
||||
}
|
||||
|
||||
export interface SceneTreeEvent {
|
||||
world: Object3D;
|
||||
environment: EnvironmentEvent;
|
||||
}
|
||||
|
||||
export type EngineEvents = {
|
||||
error: (error: Error) => void;
|
||||
mouseDown: (event: MouseButtonEvent) => void;
|
||||
mouseUp: (event: MouseButtonEvent) => void;
|
||||
mouseMove: (event: MouseMoveEvent) => void;
|
||||
setEnvironment: (event: EnvironmentEvent) => void;
|
||||
change: (event: ChangeEvent) => void;
|
||||
resize: (event: Vector2) => void;
|
||||
initialized: () => void;
|
||||
};
|
||||
|
|
|
@ -16,10 +16,13 @@ export const gameObject3DEditorProperties: EditorProperties = {
|
|||
|
||||
export class GameObject extends Object3D {
|
||||
public objectType = 'GameObject';
|
||||
public virtual = false;
|
||||
|
||||
constructor(
|
||||
public editorProperties: EditorProperties = gameObjectEditorProperties
|
||||
) {
|
||||
super();
|
||||
this.name = this.objectType;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -50,6 +53,14 @@ export class GameObject extends Object3D {
|
|||
|
||||
return object;
|
||||
}
|
||||
|
||||
parse(input: SerializedObject) {
|
||||
Object.keys(input)
|
||||
.filter((key) => key !== 'children')
|
||||
.forEach((key) => {
|
||||
(this as Record<string, unknown>)[key] = input[key];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class GameObject3D extends GameObject {
|
||||
|
|
|
@ -2,3 +2,6 @@ export * from './game-runner';
|
|||
export * from './events';
|
||||
export * from './engine-component';
|
||||
export * from './game-object';
|
||||
export * from './instancable';
|
||||
export * from './world-file';
|
||||
export * from './asset';
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export type Instancable<T> = { new (): T } | Function;
|
|
@ -0,0 +1,10 @@
|
|||
import { SerializedEnvironment } from '../gameobjects/environment.object';
|
||||
import { Asset } from './asset';
|
||||
import { SerializedObject } from './game-object';
|
||||
|
||||
export interface WorldFile {
|
||||
name: string;
|
||||
world: SerializedObject;
|
||||
environment: SerializedEnvironment;
|
||||
assets: Asset[];
|
||||
}
|
Loading…
Reference in New Issue