-
Notifications
You must be signed in to change notification settings - Fork 29
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
20 changed files
with
286 additions
and
588 deletions.
There are no files selected for viewing
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -112,6 +112,7 @@ | |
} | ||
await gameApp.StopGame() | ||
} | ||
window.dispatchEvent(new Event('runnerReady')) | ||
</script> | ||
</body> | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
229 changes: 229 additions & 0 deletions
229
spx-gui/src/components/project/runner/v2/ProjectRunnerV2.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,229 @@ | ||
<script lang="ts"> | ||
export function preload() {} | ||
</script> | ||
|
||
<script setup lang="ts"> | ||
import { ref, shallowRef, watch } from 'vue' | ||
import JSZip from 'jszip' | ||
import { untilNotNull } from '@/utils/utils' | ||
import { getCleanupSignal } from '@/utils/disposable' | ||
import { useFileUrl } from '@/utils/file' | ||
import type { File, Files } from '@/models/common/file' | ||
import { hashFile } from '@/models/common/hash' | ||
import type { Project } from '@/models/project' | ||
import { UIImg, UILoading } from '@/components/ui' | ||
// import rawRunnerHtml from './assets/runner.html?raw' | ||
// import godotEditorScriptUrl from './assets/godot.editor.js?url' | ||
// import gameScriptUrl from './assets/game.js?url' | ||
// import godotEditorWasmUrl from './assets/godot.editor.wasm?url' | ||
const runnerVersion = '20241111_1621' | ||
const runnerBaseUrl = `/runner/${runnerVersion}/` | ||
const runnerUrl = `${runnerBaseUrl}runner.html` | ||
const props = defineProps<{ project: Project }>() | ||
const emit = defineEmits<{ | ||
console: [type: 'log' | 'warn', args: unknown[]] | ||
loaded: [] | ||
}>() | ||
type Paths = string[] | ||
interface IframeWindow extends Window { | ||
startProject(buffer: ArrayBuffer, projectName: string, showEditor: boolean): Promise<void> | ||
updateProject(buffer: ArrayBuffer, addInfos: Paths, deleteInfos: Paths, updateInfos: Paths): Promise<void> | ||
stopProject(): Promise<void> | ||
runGame(): Promise<void> | ||
stopGame(): Promise<void> | ||
console: typeof console | ||
} | ||
const loading = ref(true) | ||
const [thumbnailUrl, thumbnailUrlLoading] = useFileUrl(() => props.project.thumbnail) | ||
const iframeRef = ref<HTMLIFrameElement>() | ||
const iframeWindowRef = ref<IframeWindow>() | ||
const iframeWindowWithProjectStartedRef = ref<IframeWindow>() | ||
watch(iframeRef, (iframe) => { | ||
if (iframe == null) return | ||
const iframeWindow = iframe.contentWindow as IframeWindow | null | ||
if (iframeWindow == null) throw new Error('iframeWindow expected') | ||
// let runnerHtml = rawRunnerHtml | ||
// runnerHtml = runnerHtml.replace('godot.editor.js', godotEditorScriptUrl) | ||
// runnerHtml = runnerHtml.replace('game.js', gameScriptUrl) | ||
// iframeWindow.document.write(runnerHtml) | ||
// TODO: focus to canvas | ||
// TODO: uncomment me | ||
// iframeWindow.console.log = function (...args: unknown[]) { | ||
// // eslint-disable-next-line no-console | ||
// console.log(...args) | ||
// emit('console', 'log', args) | ||
// } | ||
// iframeWindow.console.warn = function (...args: unknown[]) { | ||
// console.warn(...args) | ||
// emit('console', 'warn', args) | ||
// } | ||
iframeWindow.addEventListener('runnerReady', () => { | ||
// eslint-disable-next-line no-console | ||
console.debug('[ProjectRunnerV2]', 'runnerReady') | ||
iframeWindowRef.value = iframeWindow | ||
emit('loaded') | ||
}) | ||
}) | ||
const lastFiles = shallowRef<Files>({}) | ||
async function getProjectData() { | ||
const zip = new JSZip() | ||
const [, files] = await props.project.export() | ||
Object.entries(files).forEach(([path, file]) => { | ||
if (file != null) zip.file(path, file.arrayBuffer()) | ||
}) | ||
const zipData = await zip.generateAsync({ type: 'arraybuffer' }) | ||
return [files, zipData] as const | ||
} | ||
async function getUpdateInfo(oldFiles: Files, newFiles: Files): Promise<[toAdd: Paths, toDelete: Paths, toUpdate: Paths]> { | ||
const fileHashs = new Map<File, string>() | ||
const oldEntries = Object.entries(oldFiles) | ||
const newEntries = Object.entries(newFiles) | ||
const files = new Set<File>() | ||
;[...oldEntries, ...newEntries].forEach(([, file]) => { | ||
if (file != null) files.add(file) | ||
}) | ||
await Promise.all([...files].map(async (file) => { | ||
const hash = await hashFile(file) | ||
fileHashs.set(file, hash) | ||
})) | ||
const toAdd: Paths = [] | ||
const toDelete: Paths = [] | ||
const toUpdate: Paths = [] | ||
oldEntries.forEach(([path, file]) => { | ||
if (file == null) return | ||
const newFile = newFiles[path] | ||
if (newFile == null) { | ||
toDelete.push(path) | ||
return | ||
} | ||
const oldHash = fileHashs.get(file) | ||
const newHash = fileHashs.get(newFile) | ||
if (oldHash !== newHash) toUpdate.push(path) | ||
}) | ||
newEntries.forEach(([path, file]) => { | ||
if (file == null) return | ||
const oldFile = oldFiles[path] | ||
if (oldFile == null) toAdd.push(path) | ||
}) | ||
return [toAdd, toDelete, toUpdate] | ||
} | ||
// @JiepengTan: only digits and en letters are accepted | ||
function encodeProjectNameComponent(comp: string) { | ||
return comp.split('').map(c => c.charCodeAt(0).toString(16)).join('') | ||
} | ||
// @JiepengTan: use `_` to separate owner and name to simplify cache management | ||
function encodeProjectName(project: Project) { | ||
const owner = project.owner ?? 'anonymous' | ||
const name = project.name ?? '' | ||
return `${encodeProjectNameComponent(owner)}_${encodeProjectNameComponent(name)}` | ||
} | ||
function withLog(methodName: string, promise: Promise<unknown>) { | ||
// eslint-disable-next-line no-console | ||
console.debug('[ProjectRunnerV2]', methodName) | ||
promise.then( | ||
() => { | ||
// eslint-disable-next-line no-console | ||
console.debug('[ProjectRunnerV2]', `${methodName} done`) | ||
}, | ||
(err) => { | ||
console.error('[ProjectRunnerV2]', `${methodName} failed`, err) | ||
} | ||
) | ||
return promise | ||
} | ||
watch( | ||
() => props.project, | ||
async (project, _prevProject, onCleanup) => { | ||
const signal = getCleanupSignal(onCleanup) | ||
const iframeWindow = await untilNotNull(iframeWindowRef) | ||
signal.throwIfAborted() | ||
const [files, zipData] = await getProjectData() | ||
signal.throwIfAborted() | ||
const projectName = encodeProjectName(project) | ||
withLog('startProject', iframeWindow.startProject(zipData, projectName, false)) | ||
lastFiles.value = files | ||
iframeWindowWithProjectStartedRef.value = iframeWindow | ||
signal.addEventListener('abort', () => { | ||
withLog('stopProject', iframeWindow.stopProject()) | ||
}) | ||
}, | ||
{ immediate: true } | ||
) | ||
defineExpose({ | ||
async run() { | ||
loading.value = true | ||
const iframeWindow = await untilNotNull(iframeWindowWithProjectStartedRef) | ||
const [files, zipData] = await getProjectData() | ||
// TODO: update project as soon as possible? dont wait for `run` | ||
const updateInfo = await getUpdateInfo(lastFiles.value, files) | ||
loading.value = false // TODO: move to other place | ||
if (updateInfo.some(info => info.length > 0)) { | ||
console.debug('[ProjectRunnerV2] updateProject with', ...updateInfo) | ||
// await withLog('updateProject', iframeWindow.updateProject(zipData, ...updateInfo)) | ||
withLog('updateProject', iframeWindow.updateProject(zipData, ...updateInfo)) | ||
} | ||
await withLog('runGame', iframeWindow.runGame()) | ||
}, | ||
async stop() { | ||
const iframeWindow = iframeWindowRef.value | ||
if (iframeWindow != null) { | ||
await withLog('stopGame', iframeWindow.stopGame()) | ||
} | ||
}, | ||
async rerun() { | ||
await this.stop() | ||
await this.run() | ||
} | ||
}) | ||
</script> | ||
|
||
<template> | ||
<div class="iframe-container"> | ||
<iframe ref="iframeRef" class="iframe" frameborder="0" :src="runnerUrl" /> | ||
<UIImg v-show="loading" class="thumbnail" :src="thumbnailUrl" :loading="thumbnailUrlLoading" /> | ||
<UILoading :visible="loading" cover /> | ||
</div> | ||
</template> | ||
|
||
<style lang="scss" scoped> | ||
.iframe-container { | ||
position: relative; | ||
aspect-ratio: 4 / 3; | ||
display: flex; | ||
justify-content: center; | ||
align-items: center; | ||
} | ||
.iframe { | ||
width: 100%; | ||
height: 100%; | ||
} | ||
.thumbnail { | ||
position: absolute; | ||
left: 0; | ||
right: 0; | ||
top: 0; | ||
bottom: 0; | ||
} | ||
</style> |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Oops, something went wrong.