Skip to content

Commit 844d161

Browse files
authored
Fix export stall when StageViewer is hidden in map mode (goplus#2968)
* fix: skip stage screenshots when preview is hidden * refactor: tighten screenshot renderable guard
1 parent 084ac0b commit 844d161

File tree

2 files changed

+26
-3
lines changed

2 files changed

+26
-3
lines changed

spx-gui/src/components/editor/preview/stage-viewer/StageViewer.vue

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,10 +471,18 @@ function handleWheel(e: KonvaEventObject<WheelEvent>) {
471471
472472
// TODO: implement a standalone screenshot taker which does not depend on StageViewer
473473
// See details in https://github.com/goplus/builder/issues/1807 .
474+
const canTakeScreenshot = computed(() => stageConfig.value != null)
475+
476+
function ensureCanTakeScreenshot() {
477+
if (!canTakeScreenshot.value) throw new Error('stage viewer is not renderable for screenshot')
478+
}
479+
474480
async function takeScreenshot(name: string, signal?: AbortSignal) {
481+
ensureCanTakeScreenshot()
475482
const stage = await untilNotNull(stageRef, signal)
476483
const nodeTransformer = await untilNotNull(nodeTransformerRef, signal)
477484
await until(() => !loading.value, signal)
485+
ensureCanTakeScreenshot()
478486
// Omit transform control when taking screenshot
479487
const blob = await nodeTransformer.withHidden(
480488
() =>
@@ -488,6 +496,7 @@ async function takeScreenshot(name: string, signal?: AbortSignal) {
488496
}
489497
490498
watchEffect((onCleanup) => {
499+
if (!canTakeScreenshot.value) return
491500
const unbind = editorCtx.project.bindScreenshotTaker(takeScreenshot)
492501
onCleanup(unbind)
493502
})

spx-gui/src/models/spx/project.test.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ import { fromText, toConfig, type Files } from '../common/file'
77
import * as hashHelper from '../common/hash'
88
import { Backdrop } from './backdrop'
99
import { Monitor } from './widget/monitor'
10-
import { SpxProject, projectConfigFilePath, type RawProjectConfig } from './project'
10+
import { SpxProject, projectConfigFilePath, type RawProjectConfig, type ScreenshotTaker } from './project'
1111

1212
function mockFile(name = 'mocked') {
1313
return fromText(name, Math.random() + '')
1414
}
1515

16-
function makeProject() {
16+
function makeProject(screenshotTaker: ScreenshotTaker | null = async () => mockFile()) {
1717
const project = new SpxProject()
1818
const sound = new Sound('sound', mockFile())
1919
project.addSound(sound)
@@ -35,7 +35,7 @@ function makeProject() {
3535
const animation = Animation.create('default', animationCostumes)
3636
sprite.addAnimation(animation)
3737
project.addSprite(sprite)
38-
project.bindScreenshotTaker(async () => mockFile())
38+
if (screenshotTaker != null) project.bindScreenshotTaker(screenshotTaker)
3939
return project
4040
}
4141

@@ -99,6 +99,20 @@ describe('Project', () => {
9999
expect(hash).toBe(hash2)
100100
})
101101

102+
it('should export with existing thumbnail after screenshot taker unbound', async () => {
103+
const project = makeProject(null)
104+
const thumbnail = mockFile('thumbnail')
105+
const unbindScreenshotTaker = project.bindScreenshotTaker(async () => thumbnail)
106+
107+
// Schedule the debounced update first, then flush it immediately for a deterministic assertion.
108+
project['updateThumbnail']()
109+
await project['updateThumbnail'].flush()
110+
unbindScreenshotTaker()
111+
112+
const { metadata } = await project.export()
113+
expect(metadata.thumbnail).toBe(thumbnail)
114+
})
115+
102116
it('should move sprites correctly', async () => {
103117
const project = new SpxProject()
104118
const sprite1 = new Sprite('sprite1')

0 commit comments

Comments
 (0)