-
Notifications
You must be signed in to change notification settings - Fork 916
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Import several animations to a sprite object in one go (#6035)
- Sprite animations are automatically created when several image files are selected and they ends with an animation name and an optional frame index (most naming conversions should work).
- Loading branch information
Showing
7 changed files
with
930 additions
and
404 deletions.
There are no files selected for viewing
102 changes: 102 additions & 0 deletions
102
newIDE/app/src/ObjectEditor/Editors/SpriteEditor/AnimationImportHelper.js
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,102 @@ | ||
// @flow | ||
import path from 'path-browserify'; | ||
import groupBy from 'lodash/groupBy'; | ||
|
||
const findCommonPrefix = (values: Array<string>): string => { | ||
if (values.length === 0) { | ||
return ''; | ||
} | ||
if (values.length === 1) { | ||
return values[0]; | ||
} | ||
let hasFoundBiggerPrefix = true; | ||
let prefixLength = 0; | ||
for (let index = 0; hasFoundBiggerPrefix; index++) { | ||
const character = values[0].charAt(index); | ||
hasFoundBiggerPrefix = values.every( | ||
value => value.length > index && value.charAt(index) === character | ||
); | ||
if (!hasFoundBiggerPrefix) { | ||
prefixLength = index; | ||
} | ||
} | ||
return values[0].substring(0, prefixLength); | ||
}; | ||
|
||
const separators = [' ', '_', '-', '.']; | ||
|
||
const trimFromSeparators = (value: string) => { | ||
let lowerIndex = 0; | ||
for (let index = 0; index < value.length; index++) { | ||
if (!separators.includes(value.charAt(index))) { | ||
lowerIndex = index - 1; | ||
break; | ||
} | ||
} | ||
let upperIndex = value.length - 1; | ||
for (let index = value.length - 1; index >= lowerIndex; index--) { | ||
if (!separators.includes(value.charAt(index))) { | ||
upperIndex = index + 1; | ||
break; | ||
} | ||
} | ||
return value.substring(lowerIndex, upperIndex + 1); | ||
}; | ||
|
||
export const groupResourcesByAnimations = ( | ||
resources: Array<gdResource> | ||
): Map<string, Array<gdResource>> => { | ||
const resourcesByAnimation = new Map<string, Array<gdResource>>(); | ||
|
||
if (resources.length === 0) { | ||
return resourcesByAnimation; | ||
} | ||
|
||
// Extract the frame indexes from the file names. | ||
const namedResources = resources.map(resource => { | ||
// The resource name is used instead of the resource file path because | ||
// cloud projects are prefixing files names with a UID. | ||
const basename = path.basename( | ||
resource.getName(), | ||
path.extname(resource.getName()) | ||
); | ||
const indexMatches = basename.match(/\(\d+\)$|\d+$/g); | ||
const indexNumberMatches = | ||
indexMatches && indexMatches[0].match(/\(\d+\)$|\d+$/g); | ||
const index = indexNumberMatches ? parseInt(indexNumberMatches[0]) : null; | ||
let name = trimFromSeparators( | ||
indexMatches | ||
? basename.substring(0, basename.length - indexMatches[0].length) | ||
: basename | ||
); | ||
if (separators.some(separator => name.endsWith(separator))) { | ||
name = name.substring(0, name.length - 1); | ||
} | ||
return { | ||
resource, | ||
name, | ||
index: isNaN(index) ? null : index, | ||
}; | ||
}); | ||
|
||
const commonPrefix = findCommonPrefix( | ||
namedResources.map(resources => resources.name) | ||
); | ||
// Remove the common prefix as it's probably the object name. | ||
for (const namedResource of namedResources) { | ||
namedResource.name = namedResource.name.substring(commonPrefix.length); | ||
} | ||
|
||
// Index the resources by animation names and frame indexes. | ||
const resourcesByName = groupBy(namedResources, ({ name }) => name); | ||
for (const name in resourcesByName) { | ||
const enumeratedResources = resourcesByName[name]; | ||
resourcesByAnimation.set( | ||
name, | ||
enumeratedResources | ||
.sort((a, b) => (a.index || 0) - (b.index || 0)) | ||
.map(resource => resource.resource) | ||
); | ||
} | ||
return resourcesByAnimation; | ||
}; |
191 changes: 191 additions & 0 deletions
191
newIDE/app/src/ObjectEditor/Editors/SpriteEditor/AnimationImportHelper.spec.js
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,191 @@ | ||
// @flow | ||
import { groupResourcesByAnimations } from './AnimationImportHelper'; | ||
const gd = global.gd; | ||
|
||
describe('AnimationImportHelper', () => { | ||
const createResources = (filePaths: Array<string>): Array<gdResource> => | ||
filePaths.map(filePath => { | ||
const resource = new gd.ImageResource(); | ||
resource.setName(filePath); | ||
resource.setFile(filePath); | ||
return resource; | ||
}); | ||
|
||
const deleteResources = (resources: Array<gdResource>): void => { | ||
for (const resource of resources) { | ||
resource.delete(); | ||
} | ||
}; | ||
|
||
const getAnimationFramesCount = ( | ||
resourcesByAnimations: Map<string, Array<gdResource>>, | ||
animationName: string | ||
): number | null => { | ||
const frames = resourcesByAnimations.get(animationName); | ||
return frames ? frames.length : null; | ||
}; | ||
|
||
const getAnimationFramesPaths = ( | ||
resourcesByAnimations: Map<string, Array<gdResource>>, | ||
animationName: string | ||
): Array<string> | null => { | ||
const frames = resourcesByAnimations.get(animationName); | ||
return frames ? frames.map(resource => resource.getFile()) : null; | ||
}; | ||
|
||
it('can handle empty resource lists', () => { | ||
const resources = createResources([]); | ||
|
||
const resourcesByAnimations = groupResourcesByAnimations(resources); | ||
expect(resourcesByAnimations.size).toBe(0); | ||
|
||
deleteResources(resources); | ||
}); | ||
|
||
it('can handle 1 frame alone', () => { | ||
const resources = createResources(['Assets/Player Jump.png']); | ||
|
||
const resourcesByAnimations = groupResourcesByAnimations(resources); | ||
expect(resourcesByAnimations.size).toBe(1); | ||
expect(getAnimationFramesCount(resourcesByAnimations, '')).toBe(1); | ||
|
||
deleteResources(resources); | ||
}); | ||
|
||
it('can handle 1 animation alone', () => { | ||
const resources = createResources([ | ||
'Assets/Player Run 1.png', | ||
'Assets/Player Run 2.png', | ||
]); | ||
|
||
const resourcesByAnimations = groupResourcesByAnimations(resources); | ||
expect(resourcesByAnimations.size).toBe(1); | ||
expect(getAnimationFramesCount(resourcesByAnimations, '')).toBe(2); | ||
|
||
deleteResources(resources); | ||
}); | ||
|
||
it('can find animation names without frame indexes', () => { | ||
const resources = createResources([ | ||
'Assets/Player Jump.png', | ||
'Assets/Player Run 1.png', | ||
'Assets/Player Run 2.png', | ||
]); | ||
|
||
const resourcesByAnimations = groupResourcesByAnimations(resources); | ||
expect(resourcesByAnimations.size).toBe(2); | ||
expect(getAnimationFramesCount(resourcesByAnimations, 'Jump')).toBe(1); | ||
expect(getAnimationFramesCount(resourcesByAnimations, 'Run')).toBe(2); | ||
|
||
deleteResources(resources); | ||
}); | ||
|
||
it('can find animation names without object name', () => { | ||
const resources = createResources([ | ||
'Assets/Jump.png', | ||
'Assets/Run 1.png', | ||
'Assets/Run 2.png', | ||
]); | ||
|
||
const resourcesByAnimations = groupResourcesByAnimations(resources); | ||
expect(resourcesByAnimations.size).toBe(2); | ||
expect(getAnimationFramesCount(resourcesByAnimations, 'Jump')).toBe(1); | ||
expect(getAnimationFramesCount(resourcesByAnimations, 'Run')).toBe(2); | ||
|
||
deleteResources(resources); | ||
}); | ||
|
||
it('can find animation names without any separator', () => { | ||
const resources = createResources([ | ||
'Assets/PlayerJump.png', | ||
'Assets/PlayerRun1.png', | ||
'Assets/PlayerRun2.png', | ||
]); | ||
|
||
const resourcesByAnimations = groupResourcesByAnimations(resources); | ||
expect(resourcesByAnimations.size).toBe(2); | ||
expect(getAnimationFramesCount(resourcesByAnimations, 'Jump')).toBe(1); | ||
expect(getAnimationFramesCount(resourcesByAnimations, 'Run')).toBe(2); | ||
|
||
deleteResources(resources); | ||
}); | ||
|
||
it('can find animation names when separator contains several characters', () => { | ||
const resources = createResources([ | ||
'Assets/Player - Jump.png', | ||
'Assets/Player - Run - 1.png', | ||
'Assets/Player - Run - 2.png', | ||
]); | ||
|
||
const resourcesByAnimations = groupResourcesByAnimations(resources); | ||
expect(resourcesByAnimations.size).toBe(2); | ||
expect(getAnimationFramesCount(resourcesByAnimations, 'Jump')).toBe(1); | ||
expect(getAnimationFramesCount(resourcesByAnimations, 'Run')).toBe(2); | ||
|
||
deleteResources(resources); | ||
}); | ||
|
||
it('can sort frames by numerical order', () => { | ||
const resources = createResources([ | ||
'Assets/PlayerRun02.png', | ||
'Assets/PlayerJump.png', | ||
'Assets/PlayerRun1.png', | ||
]); | ||
|
||
const resourcesByAnimations = groupResourcesByAnimations(resources); | ||
expect(resourcesByAnimations.size).toBe(2); | ||
expect(getAnimationFramesCount(resourcesByAnimations, 'Jump')).toBe(1); | ||
expect(getAnimationFramesPaths(resourcesByAnimations, 'Run')).toStrictEqual( | ||
['Assets/PlayerRun1.png', 'Assets/PlayerRun02.png'] | ||
); | ||
|
||
deleteResources(resources); | ||
}); | ||
|
||
it('can sort frames by numerical order without taking "-" as a negative sign', () => { | ||
const resources = createResources([ | ||
'Assets/Jump.png', | ||
'Assets/Run-1.png', | ||
'Assets/Run-2.png', | ||
]); | ||
|
||
const resourcesByAnimations = groupResourcesByAnimations(resources); | ||
expect(resourcesByAnimations.size).toBe(2); | ||
expect(getAnimationFramesCount(resourcesByAnimations, 'Jump')).toBe(1); | ||
expect(getAnimationFramesPaths(resourcesByAnimations, 'Run')).toStrictEqual( | ||
['Assets/Run-1.png', 'Assets/Run-2.png'] | ||
); | ||
|
||
deleteResources(resources); | ||
}); | ||
|
||
it('can find frame index inside parenthesis', () => { | ||
const resources = createResources([ | ||
'Assets/Player Run (1).png', | ||
'Assets/Player Run (2).png', | ||
'Assets/Player Jump.png', | ||
]); | ||
|
||
const resourcesByAnimations = groupResourcesByAnimations(resources); | ||
expect(resourcesByAnimations.size).toBe(2); | ||
expect(getAnimationFramesCount(resourcesByAnimations, 'Jump')).toBe(1); | ||
expect(getAnimationFramesCount(resourcesByAnimations, 'Run')).toBe(2); | ||
|
||
deleteResources(resources); | ||
}); | ||
|
||
it('can find animation frames when indexes have holes', () => { | ||
const resources = createResources([ | ||
'Assets/Jump.png', | ||
'Assets/Run 0.png', | ||
'Assets/Run 567.png', | ||
]); | ||
|
||
const resourcesByAnimations = groupResourcesByAnimations(resources); | ||
expect(resourcesByAnimations.size).toBe(2); | ||
expect(getAnimationFramesCount(resourcesByAnimations, 'Jump')).toBe(1); | ||
expect(getAnimationFramesCount(resourcesByAnimations, 'Run')).toBe(2); | ||
|
||
deleteResources(resources); | ||
}); | ||
}); |
Oops, something went wrong.