#72 epic refactoring

This commit is contained in:
2026-03-21 17:45:27 +02:00
parent da326ddf96
commit 8dd88174af
25 changed files with 665 additions and 672 deletions
@@ -23,7 +23,7 @@
<script>
import { GameEngine } from '@/lib/GameEngine.js';
import { autoScale } from '@/lib/MeshUtils';
let gameEngine = null;
let engine = null;
export default{
props:{
@@ -45,8 +45,8 @@ export default{
}
},
beforeUnmount() {
gameEngine?.dispose();
gameEngine = null;
engine?.dispose();
engine = null;
},
watch:{
object(n){
@@ -54,8 +54,8 @@ export default{
},
async obj(){
if (!this.obj) return;
gameEngine = new GameEngine();
await gameEngine.init(this.$refs.target, {
engine = new GameEngine();
await engine.init(this.$refs.target, {
gizmo: true,
xr: true,
mode: 'ObjectPreview'
@@ -71,41 +71,45 @@ export default{
methods:{
async loadAsset() {
if (this.forRendering) {
gameEngine.resetScene();
engine.resetScene();
if (this.obj.type == 'panorama2d') {
await gameEngine.loadPanorama(this.obj.asset.name);
// let t = await gameEngine.loadTexture(`/asset/default/${this.obj.asset.name}`);
// t.mapping = gameEngine.$.EquirectangularReflectionMapping;
// gameEngine.scene.background = t;
// gameEngine.scene.environment = t;
// gameEngine.scene.add(gameEngine.camera);
await engine.loadPanorama(this.obj.asset.name);
// let t = await engine.loadTexture(`/asset/default/${this.obj.asset.name}`);
// t.mapping = engine.$.EquirectangularReflectionMapping;
// engine.scene.background = t;
// engine.scene.environment = t;
// engine.scene.add(engine.camera);
} else {
let gltf = await gameEngine.load(this.obj.asset.name);
let gltf = await engine.load(this.obj.asset.name);
//console.debug('GLTF', gltf);
this.loadedAsset = gltf;
this.animations = gltf.animations.map(a => ({
name: a.name, id: a.uuid
}));
autoScale(gltf.scene);
let bb = new gameEngine.$.Box3().setFromObject(gltf.scene);
let bb = new engine.$.Box3().setFromObject(gltf.scene);
//console.log(bb)
gameEngine.camera.position.set(bb.max.x, bb.max.y, bb.max.z);
gameEngine.orbitControls.target.set((bb.max.x + bb.min.x) / 2, (bb.max.y + bb.min.y) / 2, (bb.max.z + bb.min.z) / 2)
gameEngine.orbitControls.update();
gameEngine.activeObjects.add(gltf.scene);
//gameEngine.scene.add(gameEngine.light);
engine.camera.position.set(bb.max.x, bb.max.y, bb.max.z);
engine.orbitControls.target.set((bb.max.x + bb.min.x) / 2, (bb.max.y + bb.min.y) / 2, (bb.max.z + bb.min.z) / 2)
engine.orbitControls.update();
engine.activeObjects.add(gltf.scene);
//engine.scene.add(engine.light);
}
}
},
async toggleAnimation(animation){
animation.playing = !animation.playing;
gameEngine.playAnimation(
gameEngine.scene,
engine.playAnimation(
engine.scene,
this.loadedAsset.animations.find(a=>a.uuid == animation.id),
animation.playing
);
},
async captureScreenshot(){
return await engine.captureScreenshot();
}
}
}
</script>
+124 -24
View File
@@ -14,23 +14,23 @@
<v-btn value="perspective" icon="mdi-cone"></v-btn>
<v-btn value="orthographic" icon="mdi-cone-off"></v-btn>
</v-btn-toggle>
<template v-if="currentObject?.__o">
<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.__o.position.x"></v-number-input>
<v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="y" v-model="currentObject.__o.position.y"></v-number-input>
<v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="z" v-model="currentObject.__o.position.z"></v-number-input>
<v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="x" v-model="currentObject.position.x"></v-number-input>
<v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="y" v-model="currentObject.position.y"></v-number-input>
<v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="z" v-model="currentObject.position.z"></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.__o.rotation.x"></v-number-input>
<v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="y" v-model="currentObject.__o.rotation.y"></v-number-input>
<v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="z" v-model="currentObject.__o.rotation.z"></v-number-input>
<v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="x" v-model="currentObject.rotation.x"></v-number-input>
<v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="y" v-model="currentObject.rotation.y"></v-number-input>
<v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="z" v-model="currentObject.rotation.z"></v-number-input>
</v-card>
<v-card subtitle="Scale" class="ma-1" color="pink-darken-4">
<v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="x" v-model="currentObject.__o.scale.x"></v-number-input>
<v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="y" v-model="currentObject.__o.scale.y"></v-number-input>
<v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="z" v-model="currentObject.__o.scale.z"></v-number-input>
<v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="x" v-model="currentObject.scale.x"></v-number-input>
<v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="y" v-model="currentObject.scale.y"></v-number-input>
<v-number-input :precision="null" controlVariant="stacked" hide-details density="compact" label="z" v-model="currentObject.scale.z"></v-number-input>
</v-card>
</template>
</div>
</v-navigation-drawer>
<div class="container my-3 position-relative game-designer-canvas">
<div ref="target" @click="targetClick" @pointerdown="targetPointerDown"></div>
@@ -47,11 +47,9 @@
</v-slide-group>
</v-toolbar>
<v-navigation-drawer location="right">
<v-list v-model:selected="scenesList" selectable color="primary">
<v-list-item v-for="(s, i) in scenes" :key="i" :title="s.data.title" :value="s"></v-list-item>
</v-list>
<v-list selectable v-model:selected="objectsList" color="secondary">
<v-list-item v-for="(v, k) in sceneObjects" :title="v.__title" :subtitle="k" :value=v>
<v-list selectable color="primary" @update:selected="loadScene($event[0])" :items="scenes"></v-list>
<v-list selectable @update:selected="selectObject($event[0])" color="secondary">
<v-list-item v-for="(v, k) in sceneObjects" :title="v.__title" :subtitle="k" :value="k">
<template v-slot:prepend>
<v-btn variant="plain" density="comfortable" size="small" v-if="v.__o"
:icon="`mdi-eye${v.__o.visible ? '' : '-off'}`"
@@ -64,35 +62,137 @@
</template>
<script>
import { GameEngine } from '@/lib/GameEngine';
import { GameManager } from '@/lib/GameManager';
import { markRaw } from 'vue';
import GameEnvironmentMixin from '@/mixins/GameEnvironmentMixin';
let engine = null, manager = null;
export default {
mixins: [GameEnvironmentMixin],
props:{
modelValue: Object,
},
data(){
return {
env: 'GameDesigner',
scenesList: [],
objectsList: [],
scenes: [],
currentScene: null,
sceneObjects: {},
currentObject: null,
objectAnimations: [],
mode: 'translate',
pointerDownTime: 0,
scenario: null,
renderType: 'ST',
cameraType: 'perspective'
cameraType: 'perspective',
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', 'visible'].forEach(p=>{
sceneObjects[data.id][p] = object3d[p];
})
sceneObjects[data.id].__o = markRaw(object3d);
sceneObjects[data.id].__animations = markRaw(source.animations || []);
sceneObjects[data.id].__title = data.title;
}
});
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;
},
watch:{
async 'object.scenario'(n){
await this.loadScenario()
mode(n){
engine.transformControls.setMode(n)
},
renderType(v){
engine.renderType = v;
},
cameraType(v){
if (v == 'perspective'){
engine.setCameraPerspective();
}else{
engine.setCameraOrthographic();
}
}
},
methods:{
async loadScene(sceneId){
engine.transformControls.removeEventListener('change', this.update)
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('change', this.update)
},
selectObject(oid){
this.currentObject = this.sceneObjects[oid];
engine.transformControls.attach(this.currentObject.__o);
engine.gizmo.target = this.currentObject.__o.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, engine.activeObjects.children, 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);
},
update(){
this.version ++;
}
}
}
</script>
+44 -31
View File
@@ -6,44 +6,57 @@
<div ref="target" @click="targetClick" class="canvas-wrapper"
@mousedown="targetPointer($event, 'start')"
@mousemove="targetPointer($event, 'drag')"
@mouseup="targetPointer($event, 'end')"
@pointerdown="targetPointerDown"></div>
@mouseup="targetPointer($event, 'end')"></div>
</div>
<video class="d-none" src="" ref="videoPlayer"></video>
</template>
<script>
import { useAppStore } from '@/stores/app';
import GameEnvironmentMixin from '@/mixins/GameEnvironmentMixin';
const store = useAppStore();
import { GameEngine } from '@/lib/GameEngine';
import { GameManager } from '@/lib/GameManager';
let engine = null, manager = null;
export default {
mixins:[GameEnvironmentMixin],
props:{
modelValue: Object,
props:['id'],
async mounted(){
engine = new GameEngine();
await engine.init(this.$refs.target, {
xr: true,
mode: 'GamePlay'
});
manager = await new GameManager(engine, this.id);
window.addEventListener('resize', this.resize);
manager.loadScene(manager.scenarioData.scenes[0].data.id)
},
watch:{
scenario(n){
this.debug('Scenario changed', n);
if (n){
this.scenesList = [this.scenes?.[0]];
}
async unmounted(){
this.debug('Disposing scene')
window.removeEventListener('resize', this.resize);
engine.tm?.setGame(null);
engine.dispose();
this.debug('Disposed scene', JSON.stringify(engine.renderer.info.memory));
engine = null;
manager = null;
},
methods:{
targetClick(e){
engine.onClick(e, this.$refs.target);
},
targetPointer(e, t){
engine.onPointer(e, this.$refs.target, t);
},
resize(){
let r = this.$refs.target;
this.debug('resizing', r.clientWidth, r.clientHeight, r)
engine.resize(r.clientWidth, r.clientHeight);
},
async fullScreen(){
await engine.renderer.domElement.requestFullscreen()
}
},
data(){
return {
env: 'GamePlay',
scenesList: [],
objectsList: [],
pointerDownTime: 0,
scenario: null,
renderType: 'ST',
cameraType: 'perspective',
store
}
},
}
}
</script>
+80 -43
View File
@@ -1,61 +1,98 @@
<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>
<v-btn-toggle variant="tonal" v-model="store.prefs.xr.depthSense" class="ma-3" density="comfortable" color="green-darken-2">
<v-btn :value="true" icon="mdi-cube-outline"></v-btn>
</v-btn-toggle>
<v-divider class="my-2"></v-divider>
<v-btn variant="tonal" icon="mdi-walk" @click="control" v-tooltip="`Pointer lock controls mode`" class="ma-1" size="small"></v-btn>
<v-btn variant="tonal" icon="mdi-image-frame" @click="setGameHeader" v-tooltip="`Capture screenshot as game header image`" class="ma-1" size="small"></v-btn>
<v-btn icon="mdi-fullscreen" @click="fullScreen" variant="tonal" v-tooltip="l.fullScreen" size="small" class="ma-1" ></v-btn>
</v-navigation-drawer>
<div class="container my-3 position-relative game-designer-canvas">
<div ref="target" @click="targetClick" class="canvas-wrapper"
@mousedown="targetPointer($event, 'start')"
@mousemove="targetPointer($event, 'drag')"
@mouseup="targetPointer($event, 'end')"
@pointerdown="targetPointerDown"></div>
@mouseup="targetPointer($event, 'end')" ></div>
</div>
<v-navigation-drawer location="right">
<v-list v-model:selected="scenesList" selectable color="primary">
<v-list-item v-for="(s, i) in scenes" :key="i" :title="s.data.title" :value="s"></v-list-item>
</v-list>
<v-navigation-drawer width="133" rail location="right" class="mt-3">
<v-menu>
<template v-slot:activator="{ props }">
<v-btn icon="mdi-panorama-outline" color="primary" v-bind="props" v-tooltip="'Select scene'"></v-btn>
</template>
<v-list selectable color="primary" @update:selected="loadScene($event[0])" :items="scenes"></v-list>
</v-menu>
<v-divider class="my-2"></v-divider>
<v-btn variant="text" icon="mdi-walk" @click="control" v-tooltip="`Pointer lock controls mode`"></v-btn>
<v-btn variant="text" icon="mdi-image-frame" @click="setGameHeader" v-tooltip="`Capture screenshot as game header image`"></v-btn>
<v-btn icon="mdi-fullscreen" @click="fullScreen" variant="text" v-tooltip="l.fullScreen"></v-btn>
<v-btn-toggle variant="text" v-model="store.prefs.xr.depthSense" color="green-darken-2" v-tooltip="`Toggle XR depth sense`">
<v-btn :value="true" icon="mdi-cube-outline"></v-btn>
</v-btn-toggle>
</v-navigation-drawer>
<video class="d-none" src="" ref="videoPlayer"></video>
</template>
<script>
import GameEnvironmentMixin from '@/mixins/GameEnvironmentMixin';
import { GameEngine } from '@/lib/GameEngine';
import { GameManager } from '@/lib/GameManager';
let engine = null, manager = null;
export default {
mixins:[GameEnvironmentMixin],
props:{
modelValue: Object,
},
props:['id'],
data(){
return {
env: 'GamePreview',
scenesList: [],
objectsList: [],
mode: 'translate',
pointerDownTime: 0,
scenario: null,
renderType: 'ST',
cameraType: 'perspective',
scenes: []
}
},
async mounted(){
engine = new GameEngine();
await engine.init(this.$refs.target, {
xr: true,
gizmo: false,
stats: true,
depthSense: this.store.prefs.xr.depthSense,
mode: 'GamePreview'
});
manager = await new GameManager(engine, this.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.tm?.setGame(null);
engine.dispose();
this.debug('Disposed scene', JSON.stringify(engine.renderer.info.memory));
engine = null;
manager = null;
},
methods:{
loadScene(sceneId){
manager.loadScene(sceneId)
},
targetClick(e){
engine.onClick(e, this.$refs.target);
},
targetPointer(e, t){
engine.onPointer(e, this.$refs.target, t);
},
resize(){
let r = this.$refs.target;
this.debug('resizing', r.clientWidth, r.clientHeight, r)
engine.resize(r.clientWidth, r.clientHeight);
},
control(){
engine.pointerControls.lock(true);
},
async fullScreen(){
await engine.renderer.domElement.requestFullscreen()
},
async setGameHeader(){
let screenshot = await engine.captureScreenshot();
let fd = new FormData();
fd.append('file', screenshot);
await this.$api.game.setHeader(this.modelValue.id, fd);
}
}
}
</script>
@@ -1,6 +1,6 @@
import { Color, Group, DoubleSide, RepeatWrapping, MeshStandardMaterial, VideoTexture } from "three"
import { EventManager } from '@/lib/EventManager';
import { centerOrigin } from "@/lib/MeshUtils";
import { centerOrigin, clearMaterial, clearObject } from "@/lib/MeshUtils";
import Utils from "#/app/Utils";
class ClassicPuzzle extends EventManager {
@@ -20,7 +20,7 @@ class ClassicPuzzle extends EventManager {
vi.addEventListener('loadedmetadata', async ()=>{
map = new VideoTexture( vi );
resolve();
});
}, { once: true }); //'once' is needed in order to avoid memory leaks (the listener gets removed after called)
})
}else{
map = await engine.loadTexture(data.$go.asset.name);
@@ -101,6 +101,12 @@ class ClassicPuzzle extends EventManager {
engine.draggable.remove(done0)
container.add(dragZone);
this.object = centerOrigin(container);
this.dispose = () => {
//console.log('disposing!!!!!!!')
clearMaterial(defaultMaterial);
clearObject(gltf.scene);
super.dispose();
}
resolve(this);
})
}
@@ -92,23 +92,15 @@ class InteractiveObject extends EventManager{
if (obj.distance) {
engine.hideIfFar(this.object, obj.distance)
// const o = this.object;
// let dstm = obj.distance;
// let v = new Vector3();
// o.visible = false;
// engine.addEventListener('beforeRender', function () {
// o.getWorldPosition(v);
// var dst = engine.cameraWorld.position.distanceTo(v);
// if (dst <= dstm && !o.visible) {
// o.visible = true;
// }else if (dst > dstm && o.visible){
// o.visible = false;
// }
// });
}
this.object.userData._io = this;
resolve(this);
});
}
dispose(){
super.dispose();
this.io?.dispose?.();
}
}
class VisibilityActivator{
@@ -1,6 +1,6 @@
import { BoxGeometry, Mesh, MeshStandardMaterial, Group, Vector3, CatmullRomCurve3, Color } from 'three';
import { LineMaterial, LineGeometry, Line2 } from 'three/examples/jsm/Addons.js';
import { centerOrigin } from '@/lib/MeshUtils';
import { centerOrigin, clearMaterial } from '@/lib/MeshUtils';
import { EventManager } from '@/lib/EventManager';
import Utils from '#/app/Utils';
@@ -111,6 +111,12 @@ class PairMatchingGame extends EventManager {
this.object = centerOrigin(container);
this.dispose = () => {
clearMaterial(material);
bm.dispose();
super.dispose();
}
resolve(this);
})
}
@@ -36,7 +36,7 @@ class Particles {
gg.translate(position.x, position.y, position.z);
arr.push(gg);
var gg = geometry.clone();
gg = geometry.clone();
gg.rotateY(angle + Math.PI);
gg.translate(position.x, position.y, position.z);
arr.push(gg);
@@ -1,5 +1,5 @@
import { BoxGeometry, Mesh, MeshBasicMaterial, Group, VideoTexture } from 'three';
import { centerOrigin } from '@/lib/MeshUtils';
import { centerOrigin, clearMaterial } from '@/lib/MeshUtils';
import { EventManager } from '@/lib/EventManager';
class PuzzleGame1 extends EventManager {
@@ -24,7 +24,7 @@ class PuzzleGame1 extends EventManager {
vi.addEventListener('loadedmetadata', async ()=>{
map = new VideoTexture( vi );
resolve();
});
}, { once: true });
})
}else{
map = await engine.loadTexture(data.$go.asset.name);
@@ -73,9 +73,9 @@ class PuzzleGame1 extends EventManager {
container.add(mesh);
}
container.children[0].onBeforeRender = () => {
this.update();
};
// container.children[0].onBeforeRender = () => {
// this.update();
// };
var check = () => {
let i = 0;
@@ -121,8 +121,17 @@ class PuzzleGame1 extends EventManager {
}
};
engine.addEventListener('beforeRender', this.update)
this.object = centerOrigin(container);
this.dispose = () => {
//console.log('disposing PG1')
clearMaterial(material);
bm.dispose();
super.dispose();
}
resolve(this);
})
}
@@ -17,7 +17,7 @@ class PuzzleGame2 extends EventManager {
vi.addEventListener('loadedmetadata', async ()=>{
map = new VideoTexture( vi );
resolve();
});
}, { once: true });
})
}else{
map = await engine.loadTexture(data.$go.asset.name);
@@ -109,9 +109,9 @@ class PuzzleGame2 extends EventManager {
}
last = container.children[lidx];
container.children[0].onBeforeRender = () => {
this.update();
};
// container.children[0].onBeforeRender = () => {
// this.update();
// };
this.shuffle();
@@ -164,6 +164,8 @@ class PuzzleGame2 extends EventManager {
//engine.dashboard.addPoints(10);
}
};
engine.addEventListener('beforeRender', this.update)
this.object = centerOrigin(container)
resolve(this)
});
@@ -45,9 +45,10 @@ var PuzzleGame4 = function(context, gltf, w, h){
context.clickable.add(c, clickFn);
})
this.object.children[0].onBeforeRender = ()=>{
this.update();
}
// this.object.children[0].onBeforeRender = ()=>{
// this.update();
// }
engine.addEventListener('beforeRender', this.update)
});
var check = ()=>{
@@ -77,7 +77,7 @@ class VideoPlayer extends EventManager {
}
resolve(this);
})
}, { once: true })
vi.src = engine.assetPath + data.$go.asset.name;
})
}