Skip to content

Commit

Permalink
sv
Browse files Browse the repository at this point in the history
  • Loading branch information
nighca committed Nov 13, 2024
1 parent fd92b6d commit e489879
Show file tree
Hide file tree
Showing 20 changed files with 286 additions and 588 deletions.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
}
await gameApp.StopGame()
}
window.dispatchEvent(new Event('runnerReady'))
</script>
</body>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ const i18n = useI18n()
editorCtx = useEditorCtx()
const loaderConfig = {
// TODO:
// * `cross-origin-resource-policy: cross-origin` for `builder-static.gopluscdn.com`
// * ould not create web worker(s). Falling back to loading web worker code in main thread, which might cause UI freezes. Please see https://github.com/microsoft/monaco-editor#faq
paths: {
vs: 'https://builder-static.gopluscdn.com/libs/monaco-editor/0.45.0/min/vs'
}
Expand Down
34 changes: 17 additions & 17 deletions spx-gui/src/components/editor/preview/EditorPreview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
<div class="header">
{{ $t({ en: 'Preview', zh: '预览' }) }}
</div>
<UIButton class="run-button" type="primary" icon="play" @click="show = true">
<UIButton class="run-button" type="primary" icon="play" @click="running = true">
{{ $t({ en: 'Run', zh: '运行' }) }}
</UIButton>
</UICardHeader>

<UIFullScreenModal v-model:show="show" class="project-runner-modal">
<RunnerContainer :project="editorCtx.project" @close="show = false" />
</UIFullScreenModal>
<div class="project-runner-modal" :class="{ visible: running }">
<RunnerContainer :project="editorCtx.project" :visible="running" @close="running = false" />
</div>
<div class="stage-viewer-container">
<StageViewer />
</div>
Expand All @@ -21,11 +21,11 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import { useEditorCtx } from '@/components/editor/EditorContextProvider.vue'
import { UICard, UICardHeader, UIButton, UIFullScreenModal } from '@/components/ui'
import { UICard, UICardHeader, UIButton } from '@/components/ui'
import StageViewer from './stage-viewer/StageViewer.vue'
import RunnerContainer, { preload as preloadRunner } from './RunnerContainer.vue'
let show = ref(false)
let running = ref(false)
const editorCtx = useEditorCtx()
Expand Down Expand Up @@ -57,17 +57,17 @@ onMounted(() => {
}
.project-runner-modal {
margin-left: 32px;
margin-right: 32px;
height: 80vh;
display: flex;
justify-content: center;
align-items: center;
background: #fff;
border-radius: 20px;
.n-modal-content {
border-radius: 20px;
display: none;
position: fixed;
z-index: 100;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: white;
&.visible {
display: block;
}
padding: 16px;
}
</style>
30 changes: 16 additions & 14 deletions spx-gui/src/components/editor/preview/RunnerContainer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,13 @@
</template>

<script lang="ts">
import ProjectRunner, { preload } from '@/components/project/runner/ProjectRunner.vue'
import ProjectRunner, { preload } from '@/components/project/runner/v2/ProjectRunnerV2.vue'
import { untilNotNull } from '@/utils/utils'
export { preload }
</script>

<script setup lang="ts">
import { onMounted, ref, type CSSProperties, watch, nextTick } from 'vue'
import { ref, type CSSProperties, watch, nextTick } from 'vue'

Check warning on line 57 in spx-gui/src/components/editor/preview/RunnerContainer.vue

View workflow job for this annotation

GitHub Actions / spx-gui-lint

'nextTick' is defined but never used
import dayjs from 'dayjs'
import type { Project } from '@/models/project'
import { usePublishProject } from '@/components/project'
Expand All @@ -62,6 +63,7 @@ import { useMessageHandle } from '@/utils/exception'
const props = defineProps<{
project: Project
visible: boolean
}>()
const emit = defineEmits<{
Expand All @@ -87,18 +89,23 @@ const displayMode = ref<'landscape' | 'portrait'>('landscape')
const expanded = ref(false)
watch(
() => props.project,
async (newProject) => {
const mapSize = newProject.stage.getMapSize()
() => props.visible,
async (visible, _, onCleanup) => {
if (!visible) return
const mapSize = props.project.stage.getMapSize()
runnerAspectRatio.value.aspectRatio = `${mapSize.width}/${mapSize.height}`
displayMode.value = mapSize.width > mapSize.height ? 'landscape' : 'portrait'
consoleMessages.value = []
// Wait for the project to be injected into the component
await nextTick()
// // Wait for the project to be injected into the component
// await nextTick()
handleRerun()
const projectRunner = await untilNotNull(projectRunnerRef)
consoleMessages.value = []
projectRunner.run()
onCleanup(() => {
projectRunner.stop()
})
},
{ immediate: true }
)
Expand All @@ -109,11 +116,6 @@ const handleConsole = (type: 'log' | 'warn', args: any[]) => {
consoleMessages.value.unshift({ id: nextId.value++, time, message, type })
}
onMounted(() => {
if (!projectRunnerRef.value) throw new Error('projectRunnerRef is not ready')
projectRunnerRef.value.run()
})
const handleRerun = () => {
projectRunnerRef.value?.rerun()
consoleMessages.value = []
Expand Down
229 changes: 229 additions & 0 deletions spx-gui/src/components/project/runner/v2/ProjectRunnerV2.vue
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)

Check warning on line 183 in spx-gui/src/components/project/runner/v2/ProjectRunnerV2.vue

View workflow job for this annotation

GitHub Actions / spx-gui-lint

Unexpected console statement
// 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.
Loading

0 comments on commit e489879

Please sign in to comment.