toolbar, icons
This commit is contained in:
parent
2f68699913
commit
f081ecbfc7
@ -7,6 +7,7 @@
|
||||
],
|
||||
"main": "./dist/editor.umd.cjs",
|
||||
"module": "./dist/editor.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/editor.js",
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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) => {
|
||||
|
7
packages/editor/src/icons/hidden.svg.vue
Normal file
7
packages/editor/src/icons/hidden.svg.vue
Normal 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>
|
7
packages/editor/src/icons/rotate.svg.vue
Normal file
7
packages/editor/src/icons/rotate.svg.vue
Normal 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>
|
7
packages/editor/src/icons/scale.svg.vue
Normal file
7
packages/editor/src/icons/scale.svg.vue
Normal 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>
|
7
packages/editor/src/icons/translate.svg.vue
Normal file
7
packages/editor/src/icons/translate.svg.vue
Normal 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>
|
7
packages/editor/src/icons/visible.svg.vue
Normal file
7
packages/editor/src/icons/visible.svg.vue
Normal 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>
|
8
packages/editor/src/types/menu.interface.ts
Normal file
8
packages/editor/src/types/menu.interface.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export interface MenuItem {
|
||||
id: string;
|
||||
label?: string;
|
||||
shortcut?: string;
|
||||
onClick?: () => void;
|
||||
component?: any;
|
||||
children?: MenuItem[];
|
||||
}
|
@ -13,7 +13,8 @@
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
"allowJs": true,
|
||||
"jsx": "preserve"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
|
@ -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({
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user