Files
pronature-platform/src/components/SceneDesigner/SceneDesigner.vue
T
2025-03-15 11:23:55 +02:00

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>