freeblox/packages/editor/src/components/EditorSidebar.vue

188 lines
4.9 KiB
Vue

<template>
<div class="sidebar">
<SidebarPanel>
<template #title>Explorer</template>
<SidebarRow
v-for="object of items"
:item="object"
:selectionMap="selectionMap"
:depth="0"
@select="selectItem"
@toggle="toggleVisibility"
@itemDragStart="(o) => (dragging = o)"
@itemDragOver="(o) => (dragTarget = o)"
@itemDragEnd="dragCompleted"
@operation="explorerOperation"
/>
</SidebarPanel>
<SidebarPanel>
<template #title>Properties</template>
<SidebarForm :selection="selection" @update="update" />
</SidebarPanel>
</div>
</template>
<script setup lang="ts">
import { nextTick, onMounted, ref, shallowRef } from 'vue';
import { useEditorEvents } from '../composables/use-editor-events';
import { Editor, EditorEvents, SelectionEvent } from '../editor';
import SidebarPanel from './sidebar/SidebarPanel.vue';
import { GameObject } from '@freeblox/engine';
import { Object3D } from 'three';
import SidebarRow from './sidebar/SidebarRow.vue';
import SidebarForm from './sidebar/SidebarForm.vue';
import { exportToFile } from '../utils/export-file';
const items = shallowRef<GameObject[]>([]);
const selection = shallowRef<GameObject[]>([]);
const selectionMap = ref<string[]>([]);
const dragging = shallowRef<Object3D | undefined>();
const dragTarget = shallowRef<Object3D | undefined>();
const props = defineProps<{
editor: Editor;
}>();
const { register } = useEditorEvents(props.editor);
const createSceneMap = () => {
if (!props.editor?.running) return;
const sceneTree = props.editor.getSceneTree();
items.value = [];
nextTick(() => {
items.value = [sceneTree.world, sceneTree.environment];
});
};
const updateSelectionMap = (event: SelectionEvent) => {
selection.value = [];
selectionMap.value = event.selection.map((item) => item.uuid);
nextTick(
() => (selection.value = props.editor.getSelection() as GameObject[])
);
};
const selectItem = (item: Object3D, ctrl: boolean) => {
props.editor.events.emit('select', {
object: item as GameObject,
multi: ctrl,
});
};
const toggleVisibility = (item: Object3D) =>
props.editor.events.emit('change', {
object: item,
property: 'visible',
value: !item.visible,
edited: true,
});
const update = (item: GameObject, property: string, value: unknown) => {
props.editor.events.emit('change', {
object: item,
property,
value,
edited: true,
});
};
const rerenderSelection = () => {
selection.value = [];
nextTick(
() => (selection.value = props.editor.getSelection() as GameObject[])
);
};
const clearDrag = () => {
dragging.value = undefined;
dragTarget.value = undefined;
};
const dragCompleted = (object: Object3D) => {
// If the end event doesn't come from the same source
if (object !== dragging.value) return clearDrag();
// If the target was not set
if (!dragTarget.value) return clearDrag();
// If the target is the same object as the source
if (dragTarget.value.id === dragging.value.id) return clearDrag();
// Cannot move a virtual object
if (dragging.value instanceof GameObject && dragging.value.virtual)
return clearDrag();
// Cannot move to a virtual object unless it is world
if (
dragTarget.value instanceof GameObject &&
dragTarget.value.virtual &&
dragTarget.value.objectType !== 'World'
)
return clearDrag();
// Reparent the object
props.editor.events.emit('reparent', {
object: dragging.value,
parent: dragTarget.value,
});
clearDrag();
};
const explorerOperation = (
object: Object3D,
operation: string,
param?: string
) => {
if (['cut', 'copy', 'delete', 'duplicate'].includes(operation)) {
props.editor.events.emit(operation as keyof EditorEvents);
return;
}
if (operation === 'paste') {
props.editor.events.emit('paste', object);
return;
}
if (operation === 'add' && param) {
props.editor.events.emit('instance', {
type: param,
parent: object,
});
return;
}
if (operation === 'export' && object instanceof GameObject) {
const data = JSON.stringify(object.serialize());
exportToFile(data, `${object.name}.json`, 'application/json');
}
};
register('initialized', () => createSceneMap());
register('loadComplete', () => createSceneMap());
register('sceneJoin', () => createSceneMap());
register('sceneLeave', () => createSceneMap());
register('selected', (event) => updateSelectionMap(event));
register('deselected', (event) => updateSelectionMap(event));
register('change', (event) => {
if (['name', 'visible', 'parent'].includes(event.property)) createSceneMap();
rerenderSelection();
});
register('reset', () => {
createSceneMap();
rerenderSelection();
});
onMounted(() => createSceneMap());
</script>
<style lang="scss">
.sidebar {
--sidebar-width: 320px;
display: grid;
grid-template-rows: 1fr 1fr;
width: var(--sidebar-width);
flex-shrink: 0;
}
</style>