game scenarios

This commit is contained in:
2025-03-14 19:13:52 +02:00
parent 96869a62e4
commit 6aad752ce3
13 changed files with 396 additions and 127 deletions
@@ -0,0 +1,12 @@
<template>
<line :x1="x" :y1="y" :x2="x" :y2="y" class="annotation point"></line>
</template>
<script>
export default {
props:{
x: Number,
y: Number
}
}
</script>
@@ -0,0 +1,46 @@
<template>
<teleport to=".scene-designer" v-if="active">
<g @mousedown="$emit('target', {target:vd, attrs:['x1', 'y1'], delta: true})" :class="{gameObject: true, selected}">
<line :x1="vd.x1" :y1="vd.y1" :x2="parent.vd.x1" :y2="parent.vd.y1"></line>
<svg-icon :src="`/asset/thumb/${modelValue.id}.webp`" :x="vd.x1" :y="vd.y1" :size="37"></svg-icon>
</g>
</teleport>
<v-list density="compact" nav v-if="selected">
<v-list-item prepend-icon="mdi-panorama-outline" :title="$l.addScene" value="scene"></v-list-item>
</v-list>
</template>
<script>
import SvgIcon from './SvgIcon.vue';
import Utils from '@/lib/utils';
export default {
emits:['target'],
components: { SvgIcon },
data(){
return {
active: false
}
},
mounted(){
this.active = true;
},
props:{
modelValue: Object,
vd: Object,
selected: Boolean,
cid:String,
visible: Boolean,
parent: Object
},
steps: [['x1', 'y1']],
icon: 'mdi-vector-line',
name: 'svg-game-object',
modifiers: ['x1', 'y1'],
methods:{
intersect(v){
return Utils.intersectLineRect(this.vd, v);
}
}
}
</script>
+48
View File
@@ -0,0 +1,48 @@
<template>
<teleport to=".scene-designer" v-if="active">
<g @mousedown="$emit('target', {target:vd, attrs:['x1', 'y1'], delta: true})" :class="{scene: true, selected}">
<svg-icon :src="`/asset/thumb/${modelValue.environment}.webp`" :x="vd.x1" :y="vd.y1" :size="65"></svg-icon>
</g>
</teleport>
<v-card title="Scene" v-if="selected">
<v-form class="pa-4">
<v-text-field density="compact" :label="$l.name" v-model="modelValue.name"></v-text-field>
</v-form>
<v-btn prepend-icon="mdi-panorama-outline" ></v-btn>
</v-card>
</template>
<script>
import SvgIcon from './SvgIcon.vue';
import Utils from '@/lib/utils';
export default {
emits:['target'],
components: { SvgIcon },
data(){
return {
active: false
}
},
mounted(){
this.active = true;
},
props:{
modelValue: Object,
vd: Object,
selected: Boolean,
cid:String,
visible: Boolean,
parent: Object
},
steps: [['x1', 'y1']],
icon: 'mdi-vector-line',
name: 'svg-scene',
modifiers: ['x1', 'y1'],
methods:{
intersect(v){
return Utils.intersectLineRect(this.vd, v);
}
}
}
</script>
+122 -61
View File
@@ -1,40 +1,93 @@
<template>
<div class="container my-3">
<v-btn-toggle variant="tonal" density="compact" class="mx-auto" v-model="listMode" color="blue">
<v-btn class="text-none" value="scene"><v-icon>mdi-panorama-outline</v-icon><span>{{ $l.addScene }}</span></v-btn>
<v-btn class="text-none" value="object"><v-icon>mdi-bird</v-icon><span>{{ $l.addScene }}</span></v-btn>
<v-btn class="text-none" value="task"><v-icon>mdi-checkbox-marked-circle-plus-outline</v-icon><span>{{ $l.addScene }}</span></v-btn>
<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="object" prepend-icon="mdi-bird">{{ $l.addScene }}</v-btn>
<v-btn size="small" class="text-none" value="task" prepend-icon="mdi-checkbox-marked-circle-plus-outline">{{ $l.addScene }}</v-btn>
</v-btn-toggle>
<div @wheel="onWheel" @mousedown="onMouseDown" @mouseup="onMouseUp" @mousemove="onDrag"
:class="`svg-container ${mode}`" ref="svgContainer">
<svg>
<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">
<svg-scene></svg-scene>
<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 SvgScene from './SvgScene.vue';
import GameObject from './GameObject.vue';
import Scene from './Scene.vue';
import SvgRectangle from './SvgRectangle.vue';
import Utils from '@/lib/utils';
const components = {
SvgScene
Scene, GameObject
}
export default {
components: { SvgScene },
props:{
modelValue: Object
},
data(){
return {
listMode: 'select'
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,
}
},
mounted(){
window.addEventListener('resize', this.resize);
this.resize();
},
unmounted(){
window,removeEventListener('resize', this.resize);
},
computed:{
object:()=>this.modelValue,
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;
@@ -43,12 +96,22 @@ export default {
this.rescale(1 / v);
}
},
mode(){
return this.listMode[0];
},
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){
@@ -111,6 +174,7 @@ export default {
})
},
onWheel(e){
e.preventDefault();
this.rescale(null, e);
},
onDrag(e){
@@ -121,11 +185,11 @@ export default {
y: Utils.round(e.movementY*this.scale, 0)
}
this.selectedItem.forEach(i=>{
let mf = components[i.name].modifiers;
mf.filter(m=>m.match(/^x[0-9]+$/)).forEach(x=>i.data[x]+= p.x);
mf.filter(m=>m.match(/^y[0-9]+$/)).forEach(y=>i.data[y]+= p.y);
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) {
}else if (this.target && this.mode != 'pan') {
this.retarget(e)
}
}
@@ -164,7 +228,7 @@ export default {
let id, nid = 1;
do {
id = `${this.components[this.mode].name}-${nid++}`
}while (this.items.find(i=>i.id == id))
}while (this.flatItems.find(i=>i.id == id))
this.items.push({
name: this.mode,
data: this.target.target,
@@ -211,59 +275,56 @@ export default {
this.target = t;
this.selectedItem = [item]
},
async save(){
let imageData = await Utils.blobToBase64(await (await fetch(this.imageTarget)).blob());
//console.log(imageData)
let jsonData = {
items: this.items,
viewBox: this.viewBox,
offset: this.offset,
zoom: this.zoom,
rotation: this.imgRotation,
page: this.page,
data: imageData
};
const link = document.createElement('a')
const blob = new Blob([JSON.stringify(jsonData)], {type: 'application/json'});
link.href = URL.createObjectURL(blob)
link.download = "document.json"
link.click();
},
select(){
let r = Utils.adjustMinMax(this.selector);
this.selectedItem = this.items.filter(i=>this.$refs['svg-'+i.id][0].intersect(r));
this.selectedItem = this.flatItems.filter(i=>this.$refs['svg-'+i.id][0].intersect(r));
},
async processImage(){
this.processingImage = true;
await this.$nextTick();
let processor = new ImageProcessor(this.$refs.imageCanvas, this.img)
let lines = processor.identifyLines(), iw = this.img.naturalWidth;
lines.forEach((l, i)=>{
this.items.push({
name: 'SvgHorizontalLine', id: `aline-${i}`, visible: true, title: `ALine-${i}`,
data: { x1:iw*.1, y1: l, x2: iw*.9}
})
})
this.processingImage = false;
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);
}
}
}
</script>
<style lang="scss">
:root {
--svg-scale: 1;
}
.svg-container{
svg{
width: 100%;
min-height: 100vh;
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: #19c;
stroke-width: calc( 2px * var(--svg-scale) );
}
g.selector {
line {
stroke-dasharray: 0 calc(8 * var(--svg-scale)) 0;
}
}
}
image{
clip-path: circle(50% at 50% 50%);
}
circle {
stroke: rgb(var(--v-theme-primary));
fill:rgba(255,255,255,.5);
stroke-width: 2px;
overflow: hidden;
max-width: 100vw;
max-height: 95vh;
&.pan {
cursor: grab;
}
}
</style>
@@ -1,28 +0,0 @@
<template>
<circle :cx="65 + x" :cy="65 + y" :r="70"></circle>
<image :href="src" :x="x" :y="y" height="130" width="130" preserveAspectRatio="xMidYMid slice"></image>
</template>
<script>
export default {
props:['src', 'x', 'y'],
data(){
return {
target: null,
img: null,
size:{}
}
},
created(){
// this.img = new Image();
// this.img.onload = ()=>{
// this.size = {
// w: this.img.naturalWidth,
// h: this.img.naturalHeight,
// a: this.img.naturalWidth / this.img.naturalHeight
// }
// }
// this.img.src= this.target;
}
}
</script>
+16
View File
@@ -0,0 +1,16 @@
<template>
<circle :cx="x" :cy="y" :r="size+5"></circle>
<image :href="src" :x="x-size" :y="y-size" :height="size*2" :width="size*2" preserveAspectRatio="xMidYMid slice"></image>
</template>
<script>
export default {
props:['src', 'x', 'y', 'size'],
data(){
return {
}
},
created(){
}
}
</script>
@@ -0,0 +1,35 @@
<template>
<g :class="{ selected }">
<line :x1="modelValue.x1" :y1="modelValue.y1" :x2="modelValue.x2" :y2="modelValue.y1" @mousedown="$emit('target', { target:modelValue, attrs:[, 'y1'] })"></line>
<line :x1="modelValue.x2" :y1="modelValue.y1" :x2="modelValue.x2" :y2="modelValue.y2" @mousedown="$emit('target', { target:modelValue, attrs:['x2'] })"></line>
<line :x1="modelValue.x2" :y1="modelValue.y2" :x2="modelValue.x1" :y2="modelValue.y2" @mousedown="$emit('target', { target:modelValue, attrs:[, 'y2'] })"></line>
<line :x1="modelValue.x1" :y1="modelValue.y2" :x2="modelValue.x1" :y2="modelValue.y1" @mousedown="$emit('target', { target:modelValue, attrs:['x1'] })"></line>
<AnnotationPoint :x="modelValue.x1" :y="modelValue.y1" class="movable" @mousedown="$emit('target', {target:modelValue, attrs:['x1', 'y1']})"></AnnotationPoint>
<AnnotationPoint :x="modelValue.x2" :y="modelValue.y2" class="movable" @mousedown="$emit('target', {target:modelValue, attrs:['x2', 'y2']})"></AnnotationPoint>
<AnnotationPoint :x="modelValue.x1" :y="modelValue.y2" class="movable" @mousedown="$emit('target', {target:modelValue, attrs:['x1', 'y2']})"></AnnotationPoint>
<AnnotationPoint :x="modelValue.x2" :y="modelValue.y1" class="movable" @mousedown="$emit('target', {target:modelValue, attrs:['x2', 'y1']})"></AnnotationPoint>
</g>
</template>
<script>
import AnnotationPoint from './AnnotationPoint.vue';
import Utils from '@/lib/utils';
export default {
components: { AnnotationPoint },
props:{
modelValue: Object,
selected: Boolean
},
steps: [['x1', 'y1'], ['x2', 'y2']],
icon: 'mdi-vector-rectangle',
name: 'rectangle',
modifiers: ['x1', 'y1', 'x2', 'y2'],
methods:{
intersect(v){
let r = Utils.adjustMinMax(this.modelValue);
return Utils.intersectRectRect(r, v)
}
}
}
</script>
-25
View File
@@ -1,25 +0,0 @@
<template>
<teleport to="svg" defer>
<g>
<svg-avatar src="/asset/thumb/6.webp" :x="50" :y="50"></svg-avatar>
</g>
</teleport>
<v-list density="compact" nav>
<v-list-item prepend-icon="mdi-panorama-outline" :title="$l.addScene" value="scene"></v-list-item>
</v-list>
</template>
<script>
import SvgAvatar from './SvgAvatar.vue';
export default {
components: { SvgAvatar },
data(){
return {
active: false
}
},
mounted(){
this.active = true;
}
}
</script>