scene designer
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item to="/game-objects/add">Нов игрови обект</v-list-item>
|
||||
<v-list-item>Нов сценарий</v-list-item>
|
||||
<v-list-item to="/scenarios/add">Нов сценарий</v-list-item>
|
||||
<v-list-item to="/games/add">Нова игра</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
@@ -21,7 +21,7 @@
|
||||
<v-divider></v-divider>
|
||||
<v-list nav>
|
||||
<v-list-item prepend-icon="mdi-database" to="/game-objects/list" :title="$l.gameObjects"></v-list-item>
|
||||
<v-list-item prepend-icon="mdi-receipt-text-edit-outline" :title="$l.gameScenarios"></v-list-item>
|
||||
<v-list-item prepend-icon="mdi-receipt-text-edit-outline" to="/scenarios/list" :title="$l.gameScenarios"></v-list-item>
|
||||
<v-list-item prepend-icon="mdi-cogs" :title="$l.gameRules"></v-list-item>
|
||||
<v-divider></v-divider>
|
||||
<v-list-item prepend-icon="mdi-controller" :title="$l.games" to="/games/list"></v-list-item>
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
<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>
|
||||
<div @wheel="onWheel" @mousedown="onMouseDown" @mouseup="onMouseUp" @mousemove="onDrag"
|
||||
:class="`svg-container ${mode}`" ref="svgContainer">
|
||||
<svg>
|
||||
|
||||
</svg>
|
||||
</div>
|
||||
<v-navigation-drawer location="right">
|
||||
<svg-scene></svg-scene>
|
||||
</v-navigation-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SvgScene from './SvgScene.vue';
|
||||
|
||||
const components = {
|
||||
SvgScene
|
||||
}
|
||||
export default {
|
||||
components: { SvgScene },
|
||||
props:{
|
||||
modelValue: Object
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
listMode: 'select'
|
||||
}
|
||||
},
|
||||
computed:{
|
||||
object:()=>this.modelValue,
|
||||
zoom:{
|
||||
get(){
|
||||
return 1 / this.scale;
|
||||
},
|
||||
set(v){
|
||||
this.rescale(1 / v);
|
||||
}
|
||||
},
|
||||
mode(){
|
||||
return this.listMode[0];
|
||||
},
|
||||
components(){
|
||||
return components;
|
||||
},
|
||||
},
|
||||
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){
|
||||
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.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);
|
||||
})
|
||||
}else if (this.target) {
|
||||
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.items.find(i=>i.id == id))
|
||||
this.items.push({
|
||||
name: this.mode,
|
||||
data: this.target.target,
|
||||
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]
|
||||
},
|
||||
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));
|
||||
},
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,28 @@
|
||||
<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>
|
||||
@@ -0,0 +1,25 @@
|
||||
<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>
|
||||
@@ -35,6 +35,7 @@ class GameEngine {
|
||||
// renderer.shadowMap.type = THREE.PCFSoftShadowMap; // default THREE.PCFShadowMap
|
||||
renderer.outputEncoding = THREE.sRGBEncoding;
|
||||
const controls = new OrbitControls( camera, renderer.domElement );
|
||||
//controls.enableZoom = true;
|
||||
//const controls = new MapControls( camera, renderer.domElement );
|
||||
this.transformControls = new TransformControls( camera, renderer.domElement );
|
||||
this.transformControls.addEventListener( 'dragging-changed', function ( event ) {
|
||||
@@ -64,6 +65,12 @@ class GameEngine {
|
||||
scene.background = bck; //new THREE.Color(0.7,0.7,0.7);
|
||||
scene.environment = texture;
|
||||
console.log('GameEngine started')
|
||||
renderer.domElement.addEventListener('wheel', (event)=>{
|
||||
camera.zoom -= event.deltaY / 1000;
|
||||
camera.zoom = Math.max(camera.zoom, .4);
|
||||
controls.rotateSpeed = 1 / camera.zoom;
|
||||
camera.updateProjectionMatrix();
|
||||
})
|
||||
}
|
||||
|
||||
$ = THREE;
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<v-card :title="id == 'add' ? $l.createScenario : $l.editScenario" class="container my-3">
|
||||
<v-form class="pa-4" v-model="valid">
|
||||
<v-text-field :label="$l.name" v-model="object.name" :rules="[rules.required]"></v-text-field>
|
||||
</v-form>
|
||||
<!-- <v-card-actions>
|
||||
<v-btn @click="saveAndPreview" :loading="loading" prepend-icon="mdi-content-save" color="primary"
|
||||
:disabled="!valid">
|
||||
{{ $l.saveAndPreview }}
|
||||
</v-btn>
|
||||
<v-btn @click="publish" prepend-icon="mdi-publish" color="success" v-if="false && object.id">{{ $l.publish
|
||||
}}</v-btn>
|
||||
</v-card-actions> -->
|
||||
</v-card>
|
||||
<SceneDesigner v-model="object"></SceneDesigner>
|
||||
|
||||
<div class="sceneDrawer" >
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SceneDesigner from '@/components/SceneDesigner/SceneDesigner.vue';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
object: {},
|
||||
valid: false,
|
||||
rules: {
|
||||
required: v => v ? true : this.$l.fieldRequired,
|
||||
requiredFile: v => (v?.length || this.id != 'add') ? true : this.$l.fieldRequired
|
||||
},
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
id() {
|
||||
return this.$route.params?.id;
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
|
||||
}
|
||||
</script>
|
||||
+28
-2
@@ -23,10 +23,36 @@ const lang = {
|
||||
darkMode: 'Тъмен режим',
|
||||
confirmDeletionOf: 'Потвърдете изтриването на',
|
||||
yes: 'Да',
|
||||
no: 'Не'
|
||||
no: 'Не',
|
||||
createScenario: 'Създаване на сценарий',
|
||||
editScenario: 'Редкатиране на сценарий',
|
||||
addScene: 'Добавяне на сцена'
|
||||
},
|
||||
en: {
|
||||
|
||||
createGameObject: 'Add game object',
|
||||
editGameObject: 'Edit game object',
|
||||
name: 'Name',
|
||||
fieldRequired: 'Field is required',
|
||||
objectType: 'Object type',
|
||||
objectFile: 'File',
|
||||
panorama2d: 'Panorama picture',
|
||||
environment3d: 'Environment',
|
||||
object3d: '3D object',
|
||||
object2d: '2D object (picture)',
|
||||
audio: 'Audio',
|
||||
player3d: 'Player',
|
||||
saveAndPreview: 'Save and preview',
|
||||
preview: 'Preview',
|
||||
captureThumbnail: 'Save thumbnail',
|
||||
publish: 'Publish',
|
||||
gameObjects: 'Objects',
|
||||
gameScenarios: 'Scenarios',
|
||||
gameRules: 'Rules',
|
||||
games: 'Games',
|
||||
darkMode: 'Dark mode',
|
||||
confirmDeletionOf: 'Confirm deletion of',
|
||||
yes: 'Yes',
|
||||
no: 'No'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user