377 lines
15 KiB
Vue
377 lines
15 KiB
Vue
<template>
|
|
<div class="container my-3">
|
|
<v-btn-toggle variant="tonal" density="compact" class="mx-auto" v-model="mode" color="blue">
|
|
<v-btn size="small" class="text-none" value="default"
|
|
prepend-icon="mdi-cursor-default-click">Pointer</v-btn>
|
|
<v-btn size="small" class="text-none" value="select" prepend-icon="mdi-select-multiple">Select</v-btn>
|
|
<v-btn size="small" class="text-none" value="move" prepend-icon="mdi-cursor-move">Move</v-btn>
|
|
<v-btn size="small" class="text-none" value="pan" prepend-icon="mdi-hand-back-right-outline">Pan</v-btn>
|
|
|
|
<v-btn size="small" class="text-none" value="Scene" prepend-icon="mdi-panorama-outline">{{ l.addScene }}</v-btn>
|
|
<v-btn size="small" class="text-none" value="GameObject"
|
|
v-if="selectedItem.length == 1 && selectedItem[0].__type == 'Scene'" prepend-icon="mdi-bird">{{ l.createGameObject }}</v-btn>
|
|
<v-btn size="small" class="text-none" value="Task"
|
|
v-if="selectedItem.length == 1 && selectedItem[0].__type == 'GameObject'"
|
|
prepend-icon="mdi-checkbox-marked-circle-plus-outline">{{ l.addTask }}</v-btn>
|
|
</v-btn-toggle>
|
|
<div @wheel="onWheel" @mousedown="onMouseDown" @mouseup="onMouseUp" @mousemove="onDrag"
|
|
:class="`svg-container ${mode}`" ref="svgContainer">
|
|
<svg class="scene-designer" @resize="resize" :width="viewBox.w" :height="viewBox.h"
|
|
:viewBox="`${vb.x} ${vb.y} ${vb.w} ${vb.h}`" x="0" y="0" xmlns="http://www.w3.org/2000/svg"
|
|
xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
<defs>
|
|
<marker id="arrow" viewBox="0 0 10 10" refX="5" refY="5"
|
|
markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
|
<path d="M 0 0 L 10 5 L 0 10 z" />
|
|
</marker>
|
|
</defs>
|
|
<SvgRectangle v-model="selector" class="selector"></SvgRectangle>
|
|
<g class="lines"></g>
|
|
<g class="tasks"></g>
|
|
<g class="game-objects"></g>
|
|
<g class="scenes"></g>
|
|
</svg>
|
|
</div>
|
|
<v-navigation-drawer location="right" :width="expandDrawer ? 800 : 400">
|
|
<v-btn :icon="expandDrawer ? 'mdi-arrow-expand-right' : 'mdi-arrow-expand-left'" variant="plain" size="x-small" density="compact"
|
|
@click="expandDrawer = !expandDrawer" v-if="selectedItem.length" class="position-absolute z-100 ma-1"></v-btn>
|
|
<template v-for="(item, i) in flatItems" :key="i">
|
|
<component :is="components[item.__type]" :ref="`sc-${item.__path}`" :vd="item.vd" v-model="item.data"
|
|
@target="setTarget($event, item)" @preview="preview"
|
|
:visible="item.visible" :cid="item.id" :parent="item.__parent"
|
|
:selected="selectedItem.includes(item)">
|
|
</component>
|
|
</template>
|
|
</v-navigation-drawer>
|
|
<v-navigation-drawer>
|
|
<v-list density="compact" nav v-model:selected="selectedItem"
|
|
:select-strategy="mode == 'select' ? 'leaf' : 'single-leaf'">
|
|
<v-list-item v-for="(item, i) in flatItems.toSorted((a,b)=>a.__path.localeCompare(b.__path))"
|
|
:key="i" :title="item.data.title" :value="item" :style="`padding-left:${item.__level}rem`"
|
|
v-show="!item.__parent || item.__parent.vd.expanded" :base-color="item.vd.__showInView ? '' : 'grey'"
|
|
:color="[0,'secondary', 'primary', 'success'][item.__level]">
|
|
<template v-slot:prepend>
|
|
<v-btn variant="plain" density="comfortable" size="small"
|
|
:icon="`mdi-eye${item.visible ? '' : '-off'}`"
|
|
@click.stop="item.visible = !item.visible"></v-btn>
|
|
<!-- <v-icon :icon="components[item.name].icon" size="small"></v-icon> -->
|
|
</template>
|
|
<template v-slot:append>
|
|
<v-btn variant="plain" density="compact" size="small" color="red-darken-4" icon="mdi-delete"
|
|
@click="remove(item)"></v-btn>
|
|
<v-btn v-if="item.__type != 'Task'" :icon="item.vd.expanded ? 'mdi-chevron-up' : 'mdi-chevron-down'"
|
|
variant="plain" size="small" @click="item.vd.expanded = !item.vd.expanded"></v-btn>
|
|
</template>
|
|
</v-list-item>
|
|
</v-list>
|
|
</v-navigation-drawer>
|
|
</div>
|
|
<v-dialog v-model="previewDialog" width="auto" max-width="1200">
|
|
<AssetPreview :objectId="previewObject" autoplay></AssetPreview>
|
|
</v-dialog>
|
|
</template>
|
|
|
|
<script>
|
|
import GameObject from './GameObject.vue';
|
|
import Scene from './Scene.vue';
|
|
import SvgRectangle from './SvgRectangle.vue';
|
|
import Utils from '@/lib/Utils';
|
|
import AssetSelector from '../AssetsManagement/AssetSelector.vue';
|
|
import Task from './Task.vue';
|
|
|
|
const components = {
|
|
Scene, GameObject, Task
|
|
}
|
|
|
|
export default {
|
|
components: { AssetSelector },
|
|
props:{
|
|
modelValue: Object
|
|
},
|
|
data(){
|
|
return {
|
|
context: this,
|
|
mode: 'default',
|
|
selectedItem: [],
|
|
viewBox: {
|
|
x: 0,
|
|
y: 0,
|
|
w: 1000,
|
|
h: 800
|
|
},
|
|
scale: 1,
|
|
offset:{
|
|
x: 0,
|
|
y: 0
|
|
},
|
|
selector:{
|
|
x1:0, x2:0, y1:0, y2:0
|
|
},
|
|
modeStep: 0,
|
|
mousedown: false,
|
|
target: null,
|
|
assetSelector: {
|
|
active: false,
|
|
type: ['Scene']
|
|
},
|
|
dialog: false,
|
|
expandDrawer: false,
|
|
previewObject: null,
|
|
previewDialog: false
|
|
}
|
|
},
|
|
mounted(){
|
|
window.addEventListener('resize', this.resize);
|
|
this.resize();
|
|
this.object.scenes = this.object.scenes || [];
|
|
},
|
|
unmounted(){
|
|
window.removeEventListener('resize', this.resize);
|
|
},
|
|
computed:{
|
|
vb(){
|
|
return {
|
|
x: (1-this.scale)*100 + this.offset.x,
|
|
y: (1-this.scale)*100 + this.offset.y,
|
|
w: this.viewBox.w * this.scale,
|
|
h: this.viewBox.h * this.scale
|
|
}
|
|
},
|
|
object(){
|
|
return this.modelValue;
|
|
},
|
|
items(){
|
|
return this.object.scenes;
|
|
},
|
|
zoom:{
|
|
get(){
|
|
return 1 / this.scale;
|
|
},
|
|
set(v){
|
|
this.rescale(1 / v);
|
|
}
|
|
},
|
|
components(){
|
|
return components;
|
|
},
|
|
flatItems(){
|
|
let fi = [];
|
|
this.items?.forEach((i, ii)=>{
|
|
i.__type = 'Scene';
|
|
i.__path = `scene-${ii.toString().padStart(4, '0')}`;
|
|
i.__level = 1;
|
|
i.data?.items?.forEach((go, igo)=>{
|
|
go.__parent = i;
|
|
go.__type = 'GameObject';
|
|
go.__path = `${i.__path}.go-${igo.toString().padStart(4, '0')}`
|
|
go.__level = 2;
|
|
go.data.items?.forEach((t, it)=>{
|
|
fi.push(t);
|
|
t.__parent = go;
|
|
t.__type = 'Task';
|
|
t.__path = `${go.__path}.task-${it.toString().padStart(4, '0')}`
|
|
t.__level = 3;
|
|
})
|
|
fi.push(go);
|
|
})
|
|
fi.push(i);
|
|
})
|
|
return fi;
|
|
}
|
|
},
|
|
methods:{
|
|
rescale(scale, e){
|
|
let oldScale = this.scale;
|
|
if (!e){
|
|
this.scale = scale;
|
|
this.offset.x += (oldScale - this.scale) * (this.$refs.svgContainer.offsetWidth/2);
|
|
this.offset.y += (oldScale - this.scale) * (this.$refs.svgContainer.offsetHeight/2);
|
|
}else{
|
|
this.scale *= (1 + Math.sign(e.deltaY) / 10);
|
|
let oo = {
|
|
x: (oldScale - this.scale) * e.offsetX,
|
|
y: (oldScale - this.scale) * e.offsetY
|
|
};
|
|
if (this.target){
|
|
this.retarget(e)
|
|
}
|
|
this.offset.x += oo.x;
|
|
this.offset.y += oo.y
|
|
}
|
|
document.documentElement.style.setProperty('--svg-scale', this.scale);
|
|
},
|
|
retarget(e){
|
|
if (this.target.delta){
|
|
this.retargetDelta(e, this.target)
|
|
}else{
|
|
this.retargetAbsolute(e, this.target)
|
|
}
|
|
},
|
|
retargetDelta(e, target){
|
|
let p = {
|
|
x: Utils.round(e.movementX*this.scale, 0),
|
|
y: Utils.round(e.movementY*this.scale, 0)
|
|
}
|
|
target.attrs.forEach(a=>{
|
|
if (Array.isArray(a)){
|
|
a[0] = p.x;
|
|
a[1] = p.y;
|
|
}else if (a.startsWith('x')){
|
|
this.target.target[a] += p.x;
|
|
}else if (a.startsWith('y')){
|
|
this.target.target[a] += p.y;
|
|
}
|
|
})
|
|
},
|
|
retargetAbsolute(e, target){
|
|
let p = {
|
|
x: Utils.round(this.vb.x + e.offsetX * this.scale),
|
|
y: Utils.round(this.vb.y + e.offsetY * this.scale)
|
|
}
|
|
target.attrs.forEach(a=>{
|
|
if (Array.isArray(a)){
|
|
a[0] = p.x;
|
|
a[1] = p.y;
|
|
}else if (a.startsWith('x')){
|
|
this.target.target[a] = p.x;
|
|
}else if (a.startsWith('y')){
|
|
this.target.target[a] = p.y;
|
|
}
|
|
})
|
|
},
|
|
onWheel(e){
|
|
e.preventDefault();
|
|
this.rescale(null, e);
|
|
},
|
|
onDrag(e){
|
|
if (this.mousedown?.button == 0 || this.modeStep > 0) {
|
|
if (this.mode == 'move'){
|
|
let p = {
|
|
x: Utils.round(e.movementX*this.scale, 0),
|
|
y: Utils.round(e.movementY*this.scale, 0)
|
|
}
|
|
this.selectedItem.forEach(i=>{
|
|
let mf = components[i.__type].modifiers;
|
|
mf.filter(m=>m.match(/^x[0-9]+$/)).forEach(x=>i.vd[x]+= p.x);
|
|
mf.filter(m=>m.match(/^y[0-9]+$/)).forEach(y=>i.vd[y]+= p.y);
|
|
})
|
|
}else if (this.target && this.mode != 'pan') {
|
|
this.retarget(e)
|
|
}
|
|
}
|
|
if (e.shiftKey ||
|
|
this.mousedown?.button == 1 ||
|
|
(this.mode == 'pan' && this.mousedown?.button == 0) ||
|
|
(this.mode == 'default' && this.mousedown?.button == 0 && !this.target)
|
|
){
|
|
this.offset.x -= e.movementX*this.scale;
|
|
this.offset.y -= e.movementY*this.scale;
|
|
}
|
|
//console.log(e);
|
|
},
|
|
onMouseDown(e){
|
|
this.mousedown = { button: e.button };
|
|
//console.log(e, this.mode, this.modeStep)
|
|
if (e.button == 0 && !['default', 'move', 'pan'].includes( this.mode )){
|
|
let cs;
|
|
if (this.mode == 'select'){
|
|
//console.log('selecting')
|
|
cs = [['x1', 'y1'], ['x2', 'y2']];
|
|
if (this.modeStep == 0){
|
|
this.target = {
|
|
target: this.selector,
|
|
attrs: []
|
|
}
|
|
}
|
|
}else{
|
|
cs = this.components[this.mode].steps;
|
|
//console.log(cs);
|
|
if (this.modeStep == 0){
|
|
this.target = {
|
|
target: {},
|
|
attrs: []
|
|
}
|
|
let id, nid = 1;
|
|
do {
|
|
id = `${this.components[this.mode].name}-${nid++}`
|
|
}while (this.flatItems.find(i=>i.data.id == id));
|
|
|
|
let targetContainer = this.items;
|
|
if (this.mode != 'Scene'){
|
|
targetContainer = this.selectedItem[0]?.data; //this.items;
|
|
targetContainer.items = targetContainer.items || [];
|
|
targetContainer = targetContainer.items;
|
|
}
|
|
targetContainer.push({
|
|
//__type: this.mode,
|
|
vd: this.target.target,
|
|
data: { title: id, id },
|
|
visible: true
|
|
})
|
|
}
|
|
}
|
|
|
|
let p = {
|
|
x: Utils.round(this.vb.x + e.offsetX * this.scale),
|
|
y: Utils.round(this.vb.y + e.offsetY * this.scale)
|
|
}
|
|
|
|
for (let i = this.modeStep + 1; i <= cs.length; i++){
|
|
this.target.target[cs[i-1][0]] = p.x;
|
|
this.target.target[cs[i-1][1]] = p.y;
|
|
}
|
|
|
|
this.modeStep++;
|
|
if (this.modeStep >= cs.length){
|
|
this.modeStep = 0;
|
|
if (this.mode == 'select'){
|
|
this.select();
|
|
this.selector.x1 = this.selector.y1 = this.selector.x2 = this.selector.y2 = 0;
|
|
}
|
|
}
|
|
if (this.modeStep ){
|
|
this.target.attrs[0] = cs[this.modeStep][0];
|
|
this.target.attrs[1] = cs[this.modeStep][1];
|
|
}
|
|
}
|
|
},
|
|
onMouseUp(){
|
|
this.mousedown = false;
|
|
if (this.mode == 'default' && !this.target){
|
|
this.selectedItem = [];
|
|
}
|
|
if (this.modeStep == 0){
|
|
this.target = null;
|
|
}
|
|
},
|
|
setTarget(t, item){
|
|
this.target = t;
|
|
this.selectedItem = [item]
|
|
let i = item;
|
|
while (i){
|
|
i.vd.expanded = true;
|
|
i = i.__parent;
|
|
}
|
|
},
|
|
select(){
|
|
let r = Utils.adjustMinMax(this.selector);
|
|
this.selectedItem = this.flatItems.filter(i=>this.$refs['sc-'+i.__path][0].intersect(r));
|
|
},
|
|
resize(){
|
|
let r = this.$refs.svgContainer;
|
|
this.viewBox.w = r.clientWidth;
|
|
this.viewBox.h = r.clientHeight;
|
|
//this.zoom = Math.min(r.clientWidth / this.viewBox.w, r.clientHeight / this.viewBox.h);
|
|
},
|
|
remove(item){
|
|
//console.log(item);
|
|
let p = item.__parent?.data?.items || this.items;
|
|
p.splice(p.indexOf(item), 1);
|
|
},
|
|
preview(v){
|
|
this.previewObject = v;
|
|
this.previewDialog = true;
|
|
}
|
|
}
|
|
}
|
|
</script> |