Skip to content

Commit

Permalink
Fix animation sound loss during undo/redo (#718)
Browse files Browse the repository at this point in the history
* fix warning

* fix animation sound during export & load
  • Loading branch information
nighca authored Aug 9, 2024
1 parent 7946138 commit a63333b
Show file tree
Hide file tree
Showing 11 changed files with 62 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { Disposable } from '@/models/common/disposable'
import { Disposable } from '@/utils/disposable'
import { useFileUrl } from '@/utils/file'
import type { File } from '@/models/common/file'
import type { Costume } from '@/models/costume'
Expand Down
2 changes: 1 addition & 1 deletion spx-gui/src/components/ui/block-items/UIBlockItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import type { Color } from '..'
const props = withDefaults(
defineProps<{
active?: boolean
color: Color
color?: Color
variant?: 'standard' | 'colorful'
size?: 'medium' | 'large'
}>(),
Expand Down
2 changes: 1 addition & 1 deletion spx-gui/src/models/animation.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { reactive } from 'vue'

import { Disposable } from '@/utils/disposable'
import {
ensureValidCostumeName,
getAnimationName,
validateAnimationName
} from './common/asset-name'
import { Disposable } from './common/disposable'
import type { Files } from './common/file'
import type { Costume, RawCostumeConfig } from './costume'
import type { Sprite } from './sprite'
Expand Down
2 changes: 1 addition & 1 deletion spx-gui/src/models/common/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import { markRaw } from 'vue'
import { getMimeFromExt } from '@/utils/file'
import { extname } from '@/utils/path'
import type { Disposer } from './disposable'
import type { Disposer } from '@/utils/disposable'
import { Cancelled } from '@/utils/exception'

export type Options = {
Expand Down
2 changes: 1 addition & 1 deletion spx-gui/src/models/costume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { reactive } from 'vue'

import { extname, resolve } from '@/utils/path'
import { adaptImg } from '@/utils/spx'
import { Disposable } from '@/utils/disposable'
import { File, type Files } from './common/file'
import { type Size } from './common'
import type { Sprite } from './sprite'
import { getCostumeName, validateCostumeName } from './common/asset-name'
import { Disposable } from './common/disposable'
import { Animation } from './animation'

export type CostumeInits = {
Expand Down
38 changes: 38 additions & 0 deletions spx-gui/src/models/project/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { describe, it, expect } from 'vitest'
import { Sprite } from '../sprite'
import { Animation } from '../animation'
import { Sound } from '../sound'
import { Costume } from '../costume'
import { fromText } from '../common/file'
import { Project } from '.'

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

function makeProject() {
const project = new Project()
const sprite = new Sprite('Sprite')
const costume = new Costume('default', mockFile())
sprite.addCostume(costume)
const animationCostumes = Array.from({ length: 3 }, (_, i) => new Costume(`a${i}`, mockFile()))
const animation = Animation.create('default', animationCostumes)
sprite.addAnimation(animation)
const sound = new Sound('sound', mockFile())
project.addSprite(sprite)
project.addSound(sound)
return project
}

describe('Project', () => {
it('should preserve animation sound with exportGameFiles & loadGameFiles', async () => {
const project = makeProject()
const sprite = project.sprites[0]
const animation = sprite.animations[0]
animation.setSound(project.sounds[0].name)

const files = project.exportGameFiles()
await project.loadGameFiles(files)
expect(project.sprites[0].animations[0].sound).toBe(project.sounds[0].name)
})
})
24 changes: 10 additions & 14 deletions spx-gui/src/models/project/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { reactive, watch } from 'vue'

import { join } from '@/utils/path'
import { debounce } from '@/utils/utils'
import { Disposable } from '@/utils/disposable'
import { IsPublic, type ProjectData } from '@/apis/project'
import { Disposable } from '../common/disposable'
import { toConfig, type Files, fromConfig } from '../common/file'
import { Stage, type RawStageConfig } from '../stage'
import { Sprite } from '../sprite'
Expand Down Expand Up @@ -101,6 +101,7 @@ export class Project extends Disposable {
removeSprite(name: string) {
const idx = this.sprites.findIndex((s) => s.name === name)
const [sprite] = this.sprites.splice(idx, 1)
this.zorder = this.zorder.filter((v) => v !== sprite.name)
sprite.dispose()
this.autoSelect()
}
Expand Down Expand Up @@ -129,9 +130,6 @@ export class Project extends Disposable {
}
)
)
sprite.addDisposer(() => {
this.zorder = this.zorder.filter((v) => v !== sprite.name)
})
}
private setSpriteZorderIdx(
name: string,
Expand Down Expand Up @@ -160,6 +158,14 @@ export class Project extends Disposable {
removeSound(name: string) {
const idx = this.sounds.findIndex((s) => s.name === name)
const [sound] = this.sounds.splice(idx, 1)
// TODO: it may be better to do `setSound(null)` in `Animation`, but for now it is difficult for `Animation` to know when sound is removed
for (const sprite of this.sprites) {
for (const animation of sprite.animations) {
if (animation.sound === sound.name) {
animation.setSound(null)
}
}
}
sound.dispose()
this.autoSelect()
}
Expand Down Expand Up @@ -192,16 +198,6 @@ export class Project extends Disposable {
}
)
)
sound.addDisposer(() => {
// TODO: it may be better to do `setSound(null)` in `Animation`, but for now it is difficult for `Animation` to know when sound is removed
for (const sprite of this.sprites) {
for (const animation of sprite.animations) {
if (animation.sound === sound.name) {
animation.setSound(null)
}
}
}
})
}

setPublic(isPublic: IsPublic) {
Expand Down
2 changes: 1 addition & 1 deletion spx-gui/src/models/sound.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { reactive } from 'vue'
import { extname, join, resolve } from '@/utils/path'
import { adaptAudio } from '@/utils/spx'
import { Disposable } from './common/disposable'
import { Disposable } from '@/utils/disposable'
import { File, fromConfig, type Files, listDirs, toConfig } from './common/file'
import { getSoundName, validateSoundName } from './common/asset-name'
import type { Project } from './project'
Expand Down
14 changes: 7 additions & 7 deletions spx-gui/src/models/sprite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
import { reactive, watch } from 'vue'
import { nomalizeDegree } from '@/utils/utils'
import { join } from '@/utils/path'
import { Disposable } from '@/utils/disposable'
import { fromText, type Files, fromConfig, toText, toConfig, listDirs } from './common/file'
import { Disposable } from './common/disposable'
import {
ensureValidAnimationName,
ensureValidCostumeName,
Expand Down Expand Up @@ -134,6 +134,9 @@ export class Sprite extends Disposable {
const idx = this.animations.findIndex((s) => s.name === name)
if (idx === -1) throw new Error(`animation ${name} not found`)
const [animation] = this.animations.splice(idx, 1)
Object.entries(this.animationBindings).forEach(([state, name]) => {
if (name === animation.name) this.animationBindings[state as State] = undefined
})
animation.setSprite(null)
animation.dispose()
}
Expand All @@ -145,7 +148,6 @@ export class Sprite extends Disposable {
const newAnimationName = ensureValidAnimationName(animation.name, this)
animation.setName(newAnimationName)
animation.setSprite(this)
animation.addDisposer(() => animation.setSprite(null))
this.animations.push(animation)
animation.addDisposer(
// update animationBindings when sprite renamed
Expand All @@ -158,11 +160,6 @@ export class Sprite extends Disposable {
}
)
)
animation.addDisposer(() => {
Object.entries(this.animationBindings).forEach(([state, name]) => {
if (name === animation.name) this.animationBindings[state as State] = undefined
})
})
}

private animationBindings: Record<State, string | undefined>
Expand Down Expand Up @@ -226,6 +223,9 @@ export class Sprite extends Disposable {
this.code = code
this.costumes = []
this.animations = []
this.addDisposer(() => {
this.animations.splice(0).forEach((a) => a.dispose())
})
this.animationBindings = {
[State.default]: inits?.defaultAnimation,
[State.die]: inits?.animBindings?.[State.die],
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion spx-gui/src/utils/img.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Disposable } from '@/models/common/disposable'
import { Disposable } from '@/utils/disposable'

/** Convert arbitrary-type (supported by current browser) image content to type-`image/jpeg` content. */
export async function toJpeg(blob: Blob) {
Expand Down

0 comments on commit a63333b

Please sign in to comment.