toolbar, icons

This commit is contained in:
Evert Prants 2023-06-06 21:09:04 +03:00
parent 2f68699913
commit f081ecbfc7
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
15 changed files with 229 additions and 55 deletions

View File

@ -7,6 +7,7 @@
], ],
"main": "./dist/editor.umd.cjs", "main": "./dist/editor.umd.cjs",
"module": "./dist/editor.js", "module": "./dist/editor.js",
"types": "./dist/index.d.ts",
"exports": { "exports": {
".": { ".": {
"import": "./dist/editor.js", "import": "./dist/editor.js",

View File

@ -8,6 +8,7 @@
:selectionMap="selectionMap" :selectionMap="selectionMap"
:depth="0" :depth="0"
@select="selectItem" @select="selectItem"
@toggle="toggleVisibility"
/> />
</SidebarPanel> </SidebarPanel>
@ -62,6 +63,14 @@ const selectItem = (item: Object3D, ctrl: boolean) => {
}); });
}; };
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) => { const update = (item: GameObject, property: string, value: unknown) => {
props.editor.events.emit('change', { props.editor.events.emit('change', {
object: item, object: item,

View File

@ -1,26 +1,14 @@
<template> <template>
<div class="toolbar"> <div class="toolbar">
<template v-for="item of toolbarItems">
<Menu <Menu
id="file" :id="item.id"
:items="fileMenu" :items="item.items"
:open-sibling="currentlyOpen" :open-sibling="currentlyOpen"
@toggle="(state, reason) => toggleEvent('file', state, reason)" @toggle="(state, reason) => toggleEvent(item.id, state, reason)"
>File</Menu >{{ item.label }}</Menu
>
<Menu
id="edit"
:items="editMenu"
:open-sibling="currentlyOpen"
@toggle="(state, reason) => toggleEvent('edit', state, reason)"
>Edit</Menu
>
<Menu
id="add"
:items="addMenu"
:open-sibling="currentlyOpen"
@toggle="(state, reason) => toggleEvent('add', state, reason)"
>Add</Menu
> >
</template>
</div> </div>
</template> </template>
@ -29,6 +17,7 @@ import { computed, ref } from 'vue';
import { Editor } from '../editor'; import { Editor } from '../editor';
import Menu from './menu/Menu.vue'; import Menu from './menu/Menu.vue';
import { instancableGameObjects } from '@freeblox/engine'; import { instancableGameObjects } from '@freeblox/engine';
import { useEditorEvents } from '../composables/use-editor-events';
const currentlyOpen = ref<string | undefined>(undefined); const currentlyOpen = ref<string | undefined>(undefined);
@ -40,6 +29,8 @@ const emit = defineEmits<{
(e: 'update'): void; (e: 'update'): void;
}>(); }>();
// const { register } = useEditorEvents(props.editor);
const fileMenu = [ const fileMenu = [
{ {
id: 'new', id: 'new',
@ -100,6 +91,24 @@ const addMenu = computed(() =>
})) }))
); );
const toolbarItems = computed(() => [
{
id: 'file',
label: 'File',
items: fileMenu,
},
{
id: 'edit',
label: 'Edit',
items: editMenu.value,
},
{
id: 'add',
label: 'Add',
items: addMenu.value,
},
]);
const toggleEvent = (id: string, state: boolean, reason: 'user' | 'leave') => { const toggleEvent = (id: string, state: boolean, reason: 'user' | 'leave') => {
if (state) currentlyOpen.value = id; if (state) currentlyOpen.value = id;
else if (reason === 'user') currentlyOpen.value = undefined; else if (reason === 'user') currentlyOpen.value = undefined;

View File

@ -1,30 +1,50 @@
<template> <template>
<div class="button-bar-wrapper">
<div class="button-bar"> <div class="button-bar">
<button <button
v-for="mode of modes" v-for="mode of modes"
type="button" type="button"
@click="changeMode(mode.name)" @click="changeMode(mode.name)"
:class="{ active: mode.name === currentMode, 'mode-button': true }" :class="{ active: mode.name === currentMode, 'mode-button': true }"
:title="mode.title"
> >
{{ mode.text }} <component :is="mode.icon" />
</button> </button>
</div> </div>
<div class="snap-ctrl">
<span>Snap</span>
<input
type="text"
v-model="currentRotationSnap"
@change="changeRotationSnap"
v-if="currentMode === 'rotate'"
/>
<input type="text" v-model="currentSnap" @change="changeSnap" v-else />
</div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue'; import { ref } from 'vue';
import { Editor, TransformModeEvent } from '../editor'; import { Editor, TransformModeEvent } from '../editor';
import { useEditorEvents } from '../composables/use-editor-events'; import { useEditorEvents } from '../composables/use-editor-events';
import TranslateSvg from '../icons/translate.svg.vue';
import RotateSvg from '../icons/rotate.svg.vue';
import ScaleSvg from '../icons/scale.svg.vue';
const currentMode = ref<TransformModeEvent>('translate'); const currentMode = ref<TransformModeEvent>('translate');
const currentSnap = ref<number>(0.5);
const currentRotationSnap = ref<number>(15);
const modes: { const modes: {
name: TransformModeEvent; name: TransformModeEvent;
text: string; icon: any;
title: string;
}[] = [ }[] = [
{ name: 'translate', text: 'T' }, { name: 'translate', icon: TranslateSvg, title: 'Move' },
{ name: 'rotate', text: 'R' }, { name: 'rotate', icon: RotateSvg, title: 'Rotate' },
{ name: 'scale', text: 'S' }, { name: 'scale', icon: ScaleSvg, title: 'Scale' },
]; ];
const props = defineProps<{ const props = defineProps<{
@ -41,26 +61,49 @@ function handleTransformMode(mode: TransformModeEvent) {
currentMode.value = mode; currentMode.value = mode;
} }
function handleRotationSnap(snap: number) {
currentRotationSnap.value = snap;
}
function handleTransformSnap(snap: number) {
currentSnap.value = snap;
}
function changeMode(mode: TransformModeEvent) { function changeMode(mode: TransformModeEvent) {
props.editor.events.emit('transformMode', mode); props.editor.events.emit('transformMode', mode);
} }
function changeSnap() {
const value = Number(currentSnap.value);
if (isNaN(value)) return;
props.editor.events.emit('transformSnap', value);
}
function changeRotationSnap() {
const value = Number(currentRotationSnap.value);
if (isNaN(value)) return;
props.editor.events.emit('transformRotationSnap', value);
}
register('transformMode', handleTransformMode); register('transformMode', handleTransformMode);
register('transformSnap', handleTransformSnap);
register('transformRotationSnap', handleRotationSnap);
</script> </script>
<style lang="scss"> <style lang="scss">
.button-bar { .button-bar {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: absolute; align-items: center;
top: calc(32px + 36px);
left: 32px;
.mode-button { .mode-button {
display: flex;
appearance: none; appearance: none;
cursor: pointer; cursor: pointer;
border: 0; border: 0;
padding: 8px; padding: 6px;
width: 2rem;
height: 2rem;
background-color: #efefef; background-color: #efefef;
&.active { &.active {
@ -70,6 +113,42 @@ register('transformMode', handleTransformMode);
&:hover { &:hover {
background-color: #ffffff; background-color: #ffffff;
} }
&.active,
&:hover,
&:focus-visible {
svg {
fill: #000000;
}
}
svg {
width: 100%;
height: 100%;
fill: #727272;
}
}
&-wrapper {
position: absolute;
top: calc(32px + 36px);
left: 20px;
}
}
.snap-ctrl {
background-color: #ddd;
display: flex;
flex-direction: column;
text-align: center;
margin-top: 8px;
input {
margin-top: 4px;
border: 0;
width: 4rem;
font-size: 1rem;
text-align: center;
} }
} }
</style> </style>

View File

@ -32,21 +32,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, watch } from 'vue'; import { computed, onMounted, watch } from 'vue';
import { ref } from 'vue'; import { ref } from 'vue';
import { MenuItem } from '../../types/menu.interface';
type ToggleTrigger = 'user' | 'leave'; type ToggleTrigger = 'user' | 'leave';
const buttonRef = ref(); const buttonRef = ref();
const isOpen = ref(false); const isOpen = ref(false);
export interface MenuItem {
id: string;
label?: string;
shortcut?: string;
onClick?: () => void;
component?: any;
children?: MenuItem[];
}
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
id: string; id: string;

View File

@ -2,11 +2,23 @@
<div class="sidebar-row"> <div class="sidebar-row">
<button <button
type="button" type="button"
:class="{ selected: !!selected, 'sidebar-row-button': true }" :class="{
selected: !!selected,
'sidebar-row-button': true,
hidden: !item.visible,
}"
:style="{ paddingLeft: buttonPadding }" :style="{ paddingLeft: buttonPadding }"
@click="click($event)" @click="click($event)"
> >
{{ item.name }} <span>{{ item.name }}</span>
<button
v-if="!(item as GameObject).virtual"
@click.prevent="toggleVisibility"
class="sidebar-row-toggle-visible"
>
<VisibleSvg v-if="item.visible" />
<HiddenSvg v-else />
</button>
</button> </button>
<div class="sidebar-row-children"> <div class="sidebar-row-children">
@ -16,6 +28,7 @@
:selectionMap="selectionMap" :selectionMap="selectionMap"
:depth="depth + 1" :depth="depth + 1"
@select="(o, c) => emit('select', o, c)" @select="(o, c) => emit('select', o, c)"
@toggle="(o) => emit('toggle', o)"
></SidebarRow> ></SidebarRow>
</div> </div>
</div> </div>
@ -25,6 +38,8 @@
import { GameObject } from '@freeblox/engine'; import { GameObject } from '@freeblox/engine';
import { Object3D } from 'three'; import { Object3D } from 'three';
import { computed } from 'vue'; import { computed } from 'vue';
import VisibleSvg from '../../icons/visible.svg.vue';
import HiddenSvg from '../../icons/hidden.svg.vue';
const props = defineProps<{ const props = defineProps<{
item: Object3D; item: Object3D;
@ -35,6 +50,7 @@ const props = defineProps<{
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update'): void; (e: 'update'): void;
(e: 'select', item: Object3D, ctrl: boolean): void; (e: 'select', item: Object3D, ctrl: boolean): void;
(e: 'toggle', item: Object3D): void;
}>(); }>();
const selected = computed(() => props.selectionMap?.includes(props.item.uuid)); const selected = computed(() => props.selectionMap?.includes(props.item.uuid));
@ -45,6 +61,8 @@ const click = ($event: MouseEvent) => {
const filtered = (items: Object3D[]) => const filtered = (items: Object3D[]) =>
items.filter((item) => item instanceof GameObject); items.filter((item) => item instanceof GameObject);
const toggleVisibility = () => emit('toggle', props.item);
</script> </script>
<style lang="scss"> <style lang="scss">
@ -53,6 +71,10 @@ const filtered = (items: Object3D[]) =>
flex-direction: column; flex-direction: column;
&-button { &-button {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
background-color: transparent; background-color: transparent;
user-select: none; user-select: none;
appearance: none; appearance: none;
@ -61,9 +83,27 @@ const filtered = (items: Object3D[]) =>
text-align: left; text-align: left;
cursor: pointer; cursor: pointer;
&.hidden {
font-style: italic;
color: #4b4b4b;
}
&.selected { &.selected {
background-color: #bcefff; background-color: #bcefff;
} }
} }
&-toggle-visible {
appearance: none;
cursor: pointer;
border: 0;
background: transparent;
width: 1rem;
height: 1rem;
svg {
width: 100%;
height: 100%;
}
}
} }
</style> </style>

View File

@ -262,7 +262,7 @@ export class WorkspaceComponent extends EngineComponent {
const transformRotationSnap = (value: number) => { const transformRotationSnap = (value: number) => {
if (!this.transformControls) return; if (!this.transformControls) return;
this.transformControls.setRotationSnap(value); this.transformControls.setRotationSnap(MathUtils.degToRad(value));
}; };
const changeListener = (change: ChangeEvent) => { const changeListener = (change: ChangeEvent) => {

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960">
<path
d="m629-419-44-44q26-71-27-118t-115-24l-44-44q17-11 38-16t43-5q71 0 120.5 49.5T650-500q0 22-5.5 43.5T629-419Zm129 129-40-40q49-36 85.5-80.5T857-500q-50-111-150-175.5T490-740q-42 0-86 8t-69 19l-46-47q35-16 89.5-28T485-800q143 0 261.5 81.5T920-500q-26 64-67 117t-95 93Zm58 226L648-229q-35 14-79 21.5t-89 7.5q-146 0-265-81.5T40-500q20-52 55.5-101.5T182-696L56-822l42-43 757 757-39 44ZM223-654q-37 27-71.5 71T102-500q51 111 153.5 175.5T488-260q33 0 65-4t48-12l-64-64q-11 5-27 7.5t-30 2.5q-70 0-120-49t-50-121q0-15 2.5-30t7.5-27l-97-97Zm305 142Zm-116 58Z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960">
<path
d="M480-80q-140 0-248-86T93-389h61q30 111 120.5 180T480-140q98 0 180.5-50.5T788-328H657v-61h223v229h-59v-109q-57 88-147 138.5T480-80Zm1-299q-42 0-72-30t-30-72q0-42 30-72t72-30q42 0 72 30t30 72q0 42-30 72t-72 30ZM80-572v-228h60v107q57-88 146.5-137.5T480-880q140 0 248.5 85.5T868-572h-61q-30-111-121-180t-206-69q-97 0-179 51T173-633h131v61H80Z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960">
<path
d="M120-120v-300h60v198l558-558H540v-60h300v300h-60v-198L222-180h198v60H120Z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960">
<path
d="M480-80 317-243l44-44 89 89v-189h60v189l89-89 44 44L480-80ZM238-322 80-480l159-159 44 44-85 85h189v60H198l84 84-44 44Zm484 0-44-44 84-84H574v-60h188l-84-84 44-44 158 158-158 158ZM450-574v-188l-84 84-44-44 158-158 158 158-44 44-84-84v188h-60Z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960">
<path
d="M480.118-330Q551-330 600.5-379.618q49.5-49.617 49.5-120.5Q650-571 600.382-620.5q-49.617-49.5-120.5-49.5Q409-670 359.5-620.382q-49.5 49.617-49.5 120.5Q310-429 359.618-379.5q49.617 49.5 120.5 49.5Zm-.353-58Q433-388 400.5-420.735q-32.5-32.736-32.5-79.5Q368-547 400.735-579.5q32.736-32.5 79.5-32.5Q527-612 559.5-579.265q32.5 32.736 32.5 79.5Q592-453 559.265-420.5q-32.736 32.5-79.5 32.5ZM480-200q-146 0-264-83T40-500q58-134 176-217t264-83q146 0 264 83t176 217q-58 134-176 217t-264 83Zm0-300Zm-.169 240Q601-260 702.5-325.5 804-391 857-500q-53-109-154.331-174.5-101.332-65.5-222.5-65.5Q359-740 257.5-674.5 156-609 102-500q54 109 155.331 174.5 101.332 65.5 222.5 65.5Z"
/>
</svg>
</template>

View File

@ -0,0 +1,8 @@
export interface MenuItem {
id: string;
label?: string;
shortcut?: string;
onClick?: () => void;
component?: any;
children?: MenuItem[];
}

View File

@ -13,7 +13,8 @@
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "noEmit": true,
"jsx": "preserve", "allowJs": true,
"jsx": "preserve"
}, },
"include": [ "include": [
"src/**/*.ts", "src/**/*.ts",

View File

@ -1,6 +1,6 @@
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue'; import vue from '@vitejs/plugin-vue';
import dts from 'vite-plugin-dts' import dts from 'vite-plugin-dts';
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
@ -10,7 +10,7 @@ export default defineConfig({
entry: 'src/index.ts', entry: 'src/index.ts',
name: 'Editor', name: 'Editor',
fileName: 'editor', fileName: 'editor',
formats: ['es', 'cjs', 'umd'] formats: ['es', 'cjs', 'umd'],
}, },
rollupOptions: { rollupOptions: {
external: ['vue'], external: ['vue'],
@ -20,5 +20,5 @@ export default defineConfig({
}, },
}, },
}, },
} },
}); });