258 lines
12 KiB
Vue
258 lines
12 KiB
Vue
<template>
|
|
<v-navigation-drawer width="133">
|
|
<v-btn-toggle variant="tonal" density="comfortable" class="ma-3" v-model="renderType" color="light-blue-darken-4">
|
|
<v-btn value="ST" icon="mdi-video-3d-variant"></v-btn>
|
|
<v-btn value="VR" icon="mdi-google-cardboard"></v-btn>
|
|
<v-btn value="AG" icon="mdi-glasses"></v-btn>
|
|
</v-btn-toggle>
|
|
<v-btn-toggle variant="tonal" density="comfortable" class="ma-3" v-model="mode" color="green-darken-4">
|
|
<v-btn class="text-none" value="translate" icon="mdi-cursor-move"></v-btn>
|
|
<v-btn class="text-none" value="rotate" icon="mdi-rotate-orbit"></v-btn>
|
|
<v-btn class="text-none" value="scale" icon="mdi-resize"></v-btn>
|
|
</v-btn-toggle>
|
|
<v-btn-toggle variant="tonal" v-model="cameraType" class="ma-3" density="comfortable" color="orange-darken-4">
|
|
<v-btn value="perspective" icon="mdi-cone"></v-btn>
|
|
<v-btn value="orthographic" icon="mdi-cone-off"></v-btn>
|
|
</v-btn-toggle>
|
|
<div v-if="currentObject" :version="version">
|
|
<v-card subtitle="Position" class="ma-1" color="light-blue-darken-4">
|
|
<v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="x" v-model="currentObject.position.x" :step="0.1"></v-number-input>
|
|
<v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="y" v-model="currentObject.position.y" :step="0.1"></v-number-input>
|
|
<v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="z" v-model="currentObject.position.z" :step="0.1"></v-number-input>
|
|
</v-card>
|
|
<v-card subtitle="Rotation" class="ma-1" color="green-darken-4">
|
|
<v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="x" v-model="currentObject.rotation.x" :step="0.1"></v-number-input>
|
|
<v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="y" v-model="currentObject.rotation.y" :step="0.1"></v-number-input>
|
|
<v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="z" v-model="currentObject.rotation.z" :step="0.1"></v-number-input>
|
|
</v-card>
|
|
<v-card subtitle="Scale" class="ma-1" color="pink-darken-4">
|
|
<template v-slot:append>
|
|
<v-btn :icon="uniformScale ? 'mdi-lock-outline' : 'mdi-lock-open-variant-outline'" size="x-small" variant="text" density="compact" @click="uniformScale = !uniformScale"></v-btn>
|
|
</template>
|
|
<v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="x" v-model="currentObject.scale.x" :step="0.01"></v-number-input>
|
|
<v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="y" v-model="currentObject.scale.y" :step="0.01"></v-number-input>
|
|
<v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="z" v-model="currentObject.scale.z" :step="0.01"></v-number-input>
|
|
</v-card>
|
|
<v-card subtitle="Info" v-if="objectInfo" class="ma-1" color="purple-darken-4">
|
|
<div class="d-flex flex-column text-caption px-3">
|
|
<div>Width: {{ (currentObject.scale.x * objectInfo.width).toFixed(2) }}</div>
|
|
<div>Height: {{ (currentObject.scale.y * objectInfo.height).toFixed(2) }}</div>
|
|
<div>Depth: {{ (currentObject.scale.z * objectInfo.depth).toFixed(2) }}</div>
|
|
</div>
|
|
</v-card>
|
|
</div>
|
|
</v-navigation-drawer>
|
|
<div class="container my-3 position-relative game-designer-canvas">
|
|
<div ref="target" @click="targetClick" @pointerdown="targetPointerDown"></div>
|
|
<div class="renderer-gizmo"></div>
|
|
</div>
|
|
<v-toolbar density="compact">
|
|
<v-slide-group show-arrows>
|
|
<v-slide-group-item v-for="(a, i) in objectAnimations" :key="i" v-slot="{ isSelected }">
|
|
<v-btn :color="isSelected ? 'primary' : undefined" class="ma-2"
|
|
:prepend-icon="'mdi-' + (a.playing ? 'stop' : 'play')" rounded @click="toggleAnimation(a)">
|
|
{{ a.name }}
|
|
</v-btn>
|
|
</v-slide-group-item>
|
|
</v-slide-group>
|
|
</v-toolbar>
|
|
<v-navigation-drawer location="right">
|
|
<v-list selectable color="primary" @update:selected="loadScene($event[0])" :items="scenes"></v-list>
|
|
<v-list selectable @update:selected="selectObject($event[0])" v-model:selected="selectedObject" color="secondary">
|
|
<v-list-item v-for="(v, k) in flatObjects" :title="v.value.__title" :subtitle="v.key" :value="k" :class="`ml-${v.value.__level * 3}`">
|
|
<template v-slot:prepend>
|
|
<v-btn variant="plain" density="comfortable" size="small" v-if="v.value.__o"
|
|
:icon="`mdi-eye${v.value.__o.visible ? '' : '-off'}`"
|
|
@click.stop="v.value.__o.visible = !v.value.__o.visible"></v-btn>
|
|
<!-- <v-icon :icon="components[item.name].icon" size="small"></v-icon> -->
|
|
</template>
|
|
<template v-slot:append>
|
|
<v-img :src="`/asset/thumb/${v.value.__type}.webp`" width="30"></v-img>
|
|
</template>
|
|
</v-list-item>
|
|
</v-list>
|
|
</v-navigation-drawer>
|
|
</template>
|
|
|
|
<script>
|
|
import { GameEngine } from '@/lib/GameEngine';
|
|
import { GameManager } from '@/lib/GameManager';
|
|
import { markRaw } from 'vue';
|
|
|
|
let engine = null, manager = null;
|
|
|
|
export default {
|
|
props:{
|
|
modelValue: Object,
|
|
},
|
|
data(){
|
|
return {
|
|
env: 'GameDesigner',
|
|
scenes: [],
|
|
currentScene: null,
|
|
sceneObjects: {},
|
|
flatObjects: [],
|
|
currentObject: null,
|
|
selectedObject: [],
|
|
objectAnimations: [],
|
|
mode: 'translate',
|
|
pointerDownTime: 0,
|
|
renderType: 'ST',
|
|
cameraType: 'perspective',
|
|
uniformScale: true,
|
|
version: 0
|
|
}
|
|
},
|
|
|
|
async mounted(){
|
|
engine = new GameEngine();
|
|
await engine.init(this.$refs.target, {
|
|
xr: true,
|
|
gizmo: true,
|
|
stats: true,
|
|
mode: 'GameDesigner'
|
|
});
|
|
manager = await new GameManager(engine, this.modelValue, null, {
|
|
onObjectLoad:(sceneObjects, data, object3d, source)=>{
|
|
['position', 'scale', 'rotation'].forEach(p=>{
|
|
sceneObjects[data.id][p] = object3d[p].clone();
|
|
})
|
|
sceneObjects[data.id].__o = markRaw(object3d);
|
|
sceneObjects[data.id].__animations = markRaw(source.animations || []);
|
|
sceneObjects[data.id].__title = data.title;
|
|
sceneObjects[data.id].__type = data.type;
|
|
sceneObjects[data.id].__level = data.level || 0;
|
|
object3d.__pn_id = this.flatObjects.length;
|
|
this.flatObjects.push({
|
|
key: data.id, value: sceneObjects[data.id]
|
|
});
|
|
}
|
|
});
|
|
this.scenes = manager.scenarioData.scenes.map(s=>({
|
|
title: s.data.title,
|
|
value: s.data.id
|
|
}))
|
|
window.addEventListener('resize', this.resize);
|
|
},
|
|
|
|
async unmounted(){
|
|
this.debug('Disposing scene')
|
|
window.removeEventListener('resize', this.resize);
|
|
engine.transformControls.removeEventListener('change', this.update)
|
|
engine.tm?.setGame(null);
|
|
engine.dispose();
|
|
this.debug('Disposed scene', JSON.stringify(engine.renderer.info.memory));
|
|
engine = null;
|
|
manager = null;
|
|
},
|
|
|
|
computed:{
|
|
objectInfo(){
|
|
if (this.currentObject.__o.userData?.bbox){
|
|
let b = this.currentObject.__o.userData.bbox;
|
|
return {
|
|
width: b.max.x - b.min.x,
|
|
height: b.max.y - b.min.y,
|
|
depth: b.max.z - b.min.z
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
watch:{
|
|
mode(n){
|
|
engine.transformControls.setMode(n)
|
|
},
|
|
renderType(v){
|
|
engine.renderType = v;
|
|
},
|
|
cameraType(v){
|
|
if (v == 'perspective'){
|
|
engine.setCameraPerspective();
|
|
}else{
|
|
engine.setCameraOrthographic();
|
|
}
|
|
},
|
|
'currentObject':{
|
|
deep: true,
|
|
handler(v){
|
|
this.handleScale(this.currentObject, this.currentObject.__o)
|
|
}
|
|
}
|
|
},
|
|
|
|
methods:{
|
|
async loadScene(sceneId){
|
|
this.flatObjects = [];
|
|
await manager.loadScene(sceneId)
|
|
this.currentScene = manager.scenarioData.scenes.find(sc=>sc.data.id == sceneId);
|
|
this.sceneObjects = this.modelValue.scenes?.[sceneId]?.objects
|
|
engine.transformControls.addEventListener('objectChange', this.handleTransformControlsChange)
|
|
},
|
|
|
|
handleScale(src, dst){
|
|
if (this.uniformScale){
|
|
for (let p of ['x', 'y', 'z']){
|
|
if (src.scale[p] != dst.scale[p]) {
|
|
let k = src.scale[p]/dst.scale[p];
|
|
['x', 'y', 'z'].filter(c=>src.scale[c] == dst.scale[c]).forEach(c=>{
|
|
src.scale[c] = dst.scale[c] * k;
|
|
engine.transformControls._scaleStart[c] = src.scale[c];
|
|
})
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
['position', 'scale', 'rotation'].forEach(p=>{
|
|
dst[p].copy(src[p]);
|
|
})
|
|
},
|
|
|
|
handleTransformControlsChange(){
|
|
this.handleScale(this.currentObject.__o, this.currentObject);
|
|
},
|
|
|
|
selectObject(oid){
|
|
this.currentObject = this.flatObjects[oid]?.value;
|
|
this.selectedObject = [oid];
|
|
engine.transformControls.attach(this.currentObject.__o);
|
|
engine.gizmo.target = this.currentObject.position;
|
|
engine.camera.updateProjectionMatrix();
|
|
this.objectAnimations = this.currentObject?.__animations?.map(a => ({
|
|
name: a.name, id: a.uuid, a
|
|
}));
|
|
},
|
|
|
|
targetClick(e){
|
|
if (performance.now() - this.pointerDownTime < 200){
|
|
let intersects = engine.intersect(e, this.$refs.target, this.flatObjects.map(o=>o.value.__o), true);
|
|
if (intersects.length){
|
|
this.selectObject(intersects[0].object.__pn_id)
|
|
}else{
|
|
engine.transformControls.detach();
|
|
}
|
|
}
|
|
},
|
|
|
|
targetPointerDown(){
|
|
this.pointerDownTime = performance.now();
|
|
},
|
|
|
|
targetPointer(e, t){
|
|
engine.onPointer(e, this.$refs.target, t);
|
|
},
|
|
|
|
async toggleAnimation(animation){
|
|
animation.playing = !animation.playing;
|
|
engine.playAnimation(engine.scene, animation.a, animation.playing);
|
|
},
|
|
|
|
resize(){
|
|
let r = this.$refs.target;
|
|
this.debug('resizing!!', r.clientWidth, r.clientHeight, r)
|
|
engine.resize(r.clientWidth, r.clientHeight);
|
|
//this.zoom = Math.min(r.clientWidth / this.viewBox.w, r.clientHeight / this.viewBox.h);
|
|
},
|
|
}
|
|
}
|
|
</script> |