Skip to content

Commit edb1c8a

Browse files
authored
feat(spx-gui): automatically bind generated animation to appropriate sprite state (#2801)
* generated animation auto bind to default state * fix condition * use inferred animation bindings from aigc * fix type * use animation gen-id instead of name * fix type * fix type assertion
1 parent 029bc44 commit edb1c8a

File tree

4 files changed

+39
-3
lines changed

4 files changed

+39
-3
lines changed

spx-gui/src/apis/aigc.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
type UniversalUrl
1414
} from './common'
1515
import type { AssetExtraSettings, AssetType } from './asset'
16+
import type { State } from '@/models/sprite.ts'
1617

1718
export type ProjectSettings = {
1819
name: string
@@ -277,6 +278,7 @@ export async function enrichAssetSettings(
277278
export type SpriteContentSettings = {
278279
costumes: CostumeSettings[]
279280
animations: AnimationSettings[]
281+
animationBindings: Partial<Record<State, string>>
280282
}
281283

282284
export async function genSpriteContentSettings(

spx-gui/src/models/gen/aigc-mock.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
} from '@/apis/aigc'
1818
import { AnimationLoopMode, ArtStyle, BackdropCategory, Perspective, SpriteCategory } from '@/apis/common'
1919
import * as aigcApis from '@/apis/aigc'
20+
import { State } from '@/models/sprite.ts'
2021

2122
export function setupAigcMock() {
2223
vi.mock('@/apis/aigc', { spy: true })
@@ -167,7 +168,11 @@ export class MockAigcApis {
167168
loopMode: AnimationLoopMode.Loopable,
168169
referenceFrameUrl: null
169170
}
170-
]
171+
],
172+
animationBindings: {
173+
[State.Default]: 'walk',
174+
[State.Step]: 'jump'
175+
}
171176
}
172177
}
173178

spx-gui/src/models/gen/sprite-gen.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as fileHelpers from '@/models/common/file'
66
import { makeProject } from '../common/test'
77
import { setupAigcMock, MockAigcApis } from './aigc-mock'
88
import { SpriteGen } from './sprite-gen'
9+
import { State } from '../sprite'
910
import type { CostumeGen } from './costume-gen'
1011
import type { AnimationGen } from './animation-gen'
1112
import { RotationStyle } from '../sprite'
@@ -145,6 +146,8 @@ describe('SpriteGen', () => {
145146
expect(sprite.animations.length).toBe(2)
146147
expect(sprite.animations[0].name).toBe('walk')
147148
expect(sprite.animations[1].name).toBe('jump')
149+
expect(sprite.getAnimationBoundStates(sprite.animations[0].id)).toEqual([State.Default])
150+
expect(sprite.getAnimationBoundStates(sprite.animations[1].id)).toEqual([State.Step])
148151
})
149152

150153
it('should validate sprite name correctly', async () => {

spx-gui/src/models/gen/sprite-gen.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
adoptAsset
1414
} from '@/apis/aigc'
1515
import { Project } from '../project'
16-
import { RotationStyle, Sprite } from '../sprite'
16+
import { RotationStyle, Sprite, State } from '../sprite'
1717
import { Costume } from '../costume'
1818
import type { Animation } from '../animation'
1919
import { getProjectSettings, Phase, Task } from './common'
@@ -38,6 +38,7 @@ export class SpriteGen extends Disposable {
3838
private genImagesTask: Task<TaskType.GenerateCostume>
3939
private genImagesPhase: Phase<File[]>
4040
private prepareContentPhase: Phase<void>
41+
private animationGenIdBindings: Partial<Record<State, string>> = {}
4142

4243
constructor(i18n: I18n, project: Project, initialDescription = '') {
4344
super()
@@ -179,7 +180,18 @@ export class SpriteGen extends Disposable {
179180
() => this.animations.map((a) => a.result).filter((a): a is Animation => a != null),
180181
(generatedAnimations) => {
181182
generatedAnimations.forEach((a) => {
182-
if (!sprite.animations.includes(a)) sprite.addAnimation(a)
183+
if (!sprite.animations.includes(a)) {
184+
sprite.addAnimation(a)
185+
// Auto bind states based on recommended animation bindings
186+
const aGen = this.animations.find((gen) => gen.result?.id === a.id)
187+
if (aGen == null) return
188+
const statesToBind = (Object.entries(this.animationGenIdBindings) as Array<[State, string]>)
189+
.filter(([, genId]) => genId === aGen.id)
190+
.map(([state]) => state)
191+
if (statesToBind.length > 0) {
192+
sprite.setAnimationBoundStates(a.id, statesToBind, false)
193+
}
194+
}
183195
})
184196
sprite.animations.slice().forEach((a) => {
185197
if (!generatedAnimations.includes(a)) sprite.removeAnimation(a.id)
@@ -223,6 +235,20 @@ export class SpriteGen extends Disposable {
223235
this.animations = settings.animations.map(
224236
(s) => new AnimationGen(this, project, { ...s, referenceCostumeId: defaultCostume.id })
225237
)
238+
// Store recommended animation bindings. Replace animation names with animation-gen IDs in case of name changes.
239+
const recommendedBindings = settings.animationBindings || {}
240+
const animationNameToGenIdMap = new Map(this.animations.map((gen) => [gen.name, gen.id]))
241+
// Only support Default, Step, and Die states (turn and glide are not supported for now)
242+
const supportedStates = [State.Default, State.Step, State.Die] as const
243+
this.animationGenIdBindings = Object.fromEntries(
244+
supportedStates
245+
.map((state) => {
246+
const animationName = recommendedBindings[state]
247+
const genId = animationName ? animationNameToGenIdMap.get(animationName) : null
248+
return [state, genId]
249+
})
250+
.filter(([, genId]) => genId != null)
251+
)
226252
})
227253
}
228254

0 commit comments

Comments
 (0)