188 lines
4.9 KiB
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>
|