356 lines
13 KiB
Vue
356 lines
13 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">Add scene</v-btn>
|
|
<v-btn size="small" class="text-none" value="GameObject"
|
|
v-if="selectedItem.length == 1 && selectedItem[0].__type == 'Scene'" prepend-icon="mdi-bird">Add game
|
|
object</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">Add task</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">
|
|
<SvgRectangle v-model="selector" class="selector"></SvgRectangle>
|
|
</svg>
|
|
</div>
|
|
<v-navigation-drawer location="right">
|
|
<template v-for="(item, i) in flatItems" :key="i">
|
|
<component :is="components[item.__type]" :ref="'svg-'+item.id" :vd="item.vd" v-model="item.data"
|
|
@target="setTarget($event, item)" :visible="item.visible" :cid="item.id" :parent="item.__parent"
|
|
:selected="selectedItem.includes(item)">
|
|
</component>
|
|
</template>
|
|
</v-navigation-drawer>
|
|
</div>
|
|
</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 './AssetSelector.vue';
|
|
|
|
const components = {
|
|
Scene, GameObject
|
|
}
|
|
|
|
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'
|
|
}
|
|
}
|
|
},
|
|
mounted(){
|
|
window.addEventListener('resize', this.resize);
|
|
this.resize();
|
|
},
|
|
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=>{
|
|
i.__type = 'Scene';
|
|
fi.push(i);
|
|
i.data?.gameObjects?.forEach(go=>{
|
|
fi.push(go);
|
|
go.__parent = i;
|
|
go.__type = 'GameObject';
|
|
})
|
|
})
|
|
return fi.reverse();
|
|
}
|
|
},
|
|
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.id == id));
|
|
let targetArray = this.items;
|
|
if (this.mode == 'GameObject'){
|
|
if (this.selectedItem[0].data && !this.selectedItem[0].data.gameObjects){
|
|
this.selectedItem[0].data.gameObjects = [];
|
|
}
|
|
targetArray = this.selectedItem[0].data.gameObjects;
|
|
}
|
|
targetArray.push({
|
|
//__type: this.mode,
|
|
vd: this.target.target,
|
|
data: {},
|
|
visible: true,
|
|
id, title: id
|
|
})
|
|
}
|
|
}
|
|
|
|
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]
|
|
},
|
|
select(){
|
|
let r = Utils.adjustMinMax(this.selector);
|
|
this.selectedItem = this.flatItems.filter(i=>this.$refs['svg-'+i.id][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);
|
|
},
|
|
assetSelected(e, v){
|
|
console.log(e, v)
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss">
|
|
|
|
:root {
|
|
--svg-scale: 1;
|
|
}
|
|
|
|
.svg-container{
|
|
svg{
|
|
image{
|
|
clip-path: circle(50% at 50% 50%);
|
|
}
|
|
circle {
|
|
stroke: rgb(var(--v-theme-primary));
|
|
fill:rgba(255,255,255,.5);
|
|
stroke-width: 2px;
|
|
}
|
|
g{
|
|
&.selected circle{
|
|
fill: rgba(var(--v-theme-secondary), .9);
|
|
}
|
|
}
|
|
line, path{
|
|
stroke: rgb(213, 226, 231);
|
|
stroke-width: calc( 2px * var(--svg-scale) );
|
|
}
|
|
g.selector {
|
|
line {
|
|
stroke-dasharray: 0 calc(8 * var(--svg-scale)) 0;
|
|
}
|
|
}
|
|
}
|
|
overflow: hidden;
|
|
max-width: 100vw;
|
|
max-height: 95vh;
|
|
&.pan {
|
|
cursor: grab;
|
|
}
|
|
&.Scene, &.GameObject {
|
|
cursor: grabbing;
|
|
}
|
|
}
|
|
</style> |