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",
"module": "./dist/editor.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/editor.js",

View File

@ -8,6 +8,7 @@
:selectionMap="selectionMap"
:depth="0"
@select="selectItem"
@toggle="toggleVisibility"
/>
</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) => {
props.editor.events.emit('change', {
object: item,

View File

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

View File

@ -1,30 +1,50 @@
<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 class="button-bar-wrapper">
<div class="button-bar">
<button
v-for="mode of modes"
type="button"
@click="changeMode(mode.name)"
:class="{ active: mode.name === currentMode, 'mode-button': true }"
:title="mode.title"
>
<component :is="mode.icon" />
</button>
</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>
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue';
import { ref } from 'vue';
import { Editor, TransformModeEvent } from '../editor';
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 currentSnap = ref<number>(0.5);
const currentRotationSnap = ref<number>(15);
const modes: {
name: TransformModeEvent;
text: string;
icon: any;
title: string;
}[] = [
{ name: 'translate', text: 'T' },
{ name: 'rotate', text: 'R' },
{ name: 'scale', text: 'S' },
{ name: 'translate', icon: TranslateSvg, title: 'Move' },
{ name: 'rotate', icon: RotateSvg, title: 'Rotate' },
{ name: 'scale', icon: ScaleSvg, title: 'Scale' },
];
const props = defineProps<{
@ -41,26 +61,49 @@ function handleTransformMode(mode: TransformModeEvent) {
currentMode.value = mode;
}
function handleRotationSnap(snap: number) {
currentRotationSnap.value = snap;
}
function handleTransformSnap(snap: number) {
currentSnap.value = snap;
}
function changeMode(mode: TransformModeEvent) {
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('transformSnap', handleTransformSnap);
register('transformRotationSnap', handleRotationSnap);
</script>
<style lang="scss">
.button-bar {
display: flex;
flex-direction: column;
position: absolute;
top: calc(32px + 36px);
left: 32px;
align-items: center;
.mode-button {
display: flex;
appearance: none;
cursor: pointer;
border: 0;
padding: 8px;
padding: 6px;
width: 2rem;
height: 2rem;
background-color: #efefef;
&.active {
@ -70,6 +113,42 @@ register('transformMode', handleTransformMode);
&:hover {
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>

View File

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

View File

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

View File

@ -262,7 +262,7 @@ export class WorkspaceComponent extends EngineComponent {
const transformRotationSnap = (value: number) => {
if (!this.transformControls) return;
this.transformControls.setRotationSnap(value);
this.transformControls.setRotationSnap(MathUtils.degToRad(value));
};
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,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"allowJs": true,
"jsx": "preserve"
},
"include": [
"src/**/*.ts",

View File

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