mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Add support for Spine objects (2D skeletal animation) (#5927)
* This allows to use animatable 2D objects created with Spine (purchase a licence on [their website](https://esotericsoftware.com/) if you're interested). * 2D skeletal animation allows for very smooth animations, and keep resource usage low compared to animations made of frames like Sprite objects. It's perfect for 2D games and can be used for animated characters, avatars, UI elements. * Many thanks to @f0nar and @LousyMolars for their work on this new feature and associated game examples! --------- Co-authored-by: Vladyslav Pohorielov <vpohorielov@playtika.com> Co-authored-by: Gleb Volkov <glebusheg@gmail.com> Co-authored-by: Florian Rival <Florian.rival@gmail.com> Co-authored-by: Davy Hélard <davy.helard@gmail.com>
This commit is contained in:

committed by
GitHub

parent
f623b352ee
commit
d0005ba2cb
@@ -714,6 +714,8 @@ gd::String EventsCodeGenerator::GenerateParameterCodes(
|
||||
metadata.GetType() == "tilesetResource" ||
|
||||
metadata.GetType() == "videoResource" ||
|
||||
metadata.GetType() == "model3DResource" ||
|
||||
metadata.GetType() == "atlasResource" ||
|
||||
metadata.GetType() == "spineResource" ||
|
||||
// Deprecated, old parameter names:
|
||||
metadata.GetType() == "password" || metadata.GetType() == "musicfile" ||
|
||||
metadata.GetType() == "soundfile" || metadata.GetType() == "police") {
|
||||
|
@@ -217,7 +217,9 @@ class GD_CORE_API ValueTypeMetadata {
|
||||
parameterType == "jsonResource" ||
|
||||
parameterType == "tilemapResource" ||
|
||||
parameterType == "tilesetResource" ||
|
||||
parameterType == "model3DResource";
|
||||
parameterType == "model3DResource" ||
|
||||
parameterType == "atlasResource" ||
|
||||
parameterType == "spineResource";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@@ -52,6 +52,16 @@ void ArbitraryResourceWorker::ExposeModel3D(gd::String& resourceName){
|
||||
// do.
|
||||
};
|
||||
|
||||
void ArbitraryResourceWorker::ExposeAtlas(gd::String& resourceName){
|
||||
// Nothing to do by default - each child class can define here the action to
|
||||
// do.
|
||||
};
|
||||
|
||||
void ArbitraryResourceWorker::ExposeSpine(gd::String& resourceName){
|
||||
// Nothing to do by default - each child class can define here the action to
|
||||
// do.
|
||||
};
|
||||
|
||||
void ArbitraryResourceWorker::ExposeVideo(gd::String& videoName){
|
||||
// Nothing to do by default - each child class can define here the action to
|
||||
// do.
|
||||
@@ -120,6 +130,7 @@ void ArbitraryResourceWorker::ExposeEmbeddeds(gd::String& resourceName) {
|
||||
|
||||
gd::String potentiallyUpdatedTargetResourceName = targetResourceName;
|
||||
ExposeResourceWithType(targetResource.GetKind(), potentiallyUpdatedTargetResourceName);
|
||||
ExposeEmbeddeds(potentiallyUpdatedTargetResourceName);
|
||||
|
||||
if (potentiallyUpdatedTargetResourceName != targetResourceName) {
|
||||
// The resource name was renamed. Also update the mapping.
|
||||
@@ -176,6 +187,14 @@ void ArbitraryResourceWorker::ExposeResourceWithType(
|
||||
ExposeVideo(resourceName);
|
||||
return;
|
||||
}
|
||||
if (resourceType == "atlas") {
|
||||
ExposeAtlas(resourceName);
|
||||
return;
|
||||
}
|
||||
if (resourceType == "spine") {
|
||||
ExposeSpine(resourceName);
|
||||
return;
|
||||
}
|
||||
gd::LogError("Unexpected resource type: " + resourceType + " for: " + resourceName);
|
||||
return;
|
||||
}
|
||||
@@ -244,6 +263,14 @@ bool ResourceWorkerInEventsWorker::DoVisitInstruction(gd::Instruction& instructi
|
||||
gd::String updatedParameterValue = parameterValue;
|
||||
worker.ExposeModel3D(updatedParameterValue);
|
||||
instruction.SetParameter(parameterIndex, updatedParameterValue);
|
||||
} else if (parameterMetadata.GetType() == "atlasResource") {
|
||||
gd::String updatedParameterValue = parameterValue;
|
||||
worker.ExposeAtlas(updatedParameterValue);
|
||||
instruction.SetParameter(parameterIndex, updatedParameterValue);
|
||||
} else if (parameterMetadata.GetType() == "spineResource") {
|
||||
gd::String updatedParameterValue = parameterValue;
|
||||
worker.ExposeSpine(updatedParameterValue);
|
||||
instruction.SetParameter(parameterIndex, updatedParameterValue);
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -96,6 +96,16 @@ public:
|
||||
* \brief Expose a 3D model, which is always a reference to a "model3D" resource.
|
||||
*/
|
||||
virtual void ExposeModel3D(gd::String &resourceName);
|
||||
|
||||
/**
|
||||
* \brief Expose an atlas, which is always a reference to a "atlas" resource.
|
||||
*/
|
||||
virtual void ExposeAtlas(gd::String &resourceName);
|
||||
|
||||
/**
|
||||
* \brief Expose an spine, which is always a reference to a "spine" resource.
|
||||
*/
|
||||
virtual void ExposeSpine(gd::String &resourceName);
|
||||
|
||||
/**
|
||||
* \brief Expose a video, which is always a reference to a "video" resource.
|
||||
|
@@ -79,6 +79,12 @@ public:
|
||||
virtual void ExposeModel3D(gd::String& otherResourceName) override {
|
||||
MatchResourceName(otherResourceName);
|
||||
};
|
||||
virtual void ExposeAtlas(gd::String& otherResourceName) override {
|
||||
MatchResourceName(otherResourceName);
|
||||
};
|
||||
virtual void ExposeSpine(gd::String& otherResourceName) override {
|
||||
MatchResourceName(otherResourceName);
|
||||
};
|
||||
|
||||
void MatchResourceName(gd::String& otherResourceName) {
|
||||
if (otherResourceName == resourceName) matchesResourceName = true;
|
||||
|
@@ -46,6 +46,8 @@ public:
|
||||
std::set<gd::String>& GetAllVideos() { return GetAll("video"); };
|
||||
std::set<gd::String>& GetAllBitmapFonts() { return GetAll("bitmapFont"); };
|
||||
std::set<gd::String>& GetAll3DModels() { return GetAll("model3D"); };
|
||||
std::set<gd::String>& GetAllAtlases() { return GetAll("atlas"); };
|
||||
std::set<gd::String>& GetAllSpines() { return GetAll("spine"); };
|
||||
std::set<gd::String>& GetAll(const gd::String& resourceType) {
|
||||
if (resourceType == "image") return allImages;
|
||||
if (resourceType == "audio") return allAudios;
|
||||
@@ -56,6 +58,8 @@ public:
|
||||
if (resourceType == "video") return allVideos;
|
||||
if (resourceType == "bitmapFont") return allBitmapFonts;
|
||||
if (resourceType == "model3D") return allModel3Ds;
|
||||
if (resourceType == "atlas") return allAtlases;
|
||||
if (resourceType == "spine") return allSpines;
|
||||
|
||||
return emptyResources;
|
||||
};
|
||||
@@ -90,6 +94,12 @@ public:
|
||||
virtual void ExposeModel3D(gd::String& resourceName) override {
|
||||
allModel3Ds.insert(resourceName);
|
||||
};
|
||||
virtual void ExposeAtlas(gd::String& resourceName) override {
|
||||
allAtlases.insert(resourceName);
|
||||
};
|
||||
virtual void ExposeSpine(gd::String& resourceName) override {
|
||||
allSpines.insert(resourceName);
|
||||
};
|
||||
|
||||
protected:
|
||||
std::vector<gd::String> allResources;
|
||||
@@ -102,6 +112,8 @@ public:
|
||||
std::set<gd::String> allVideos;
|
||||
std::set<gd::String> allBitmapFonts;
|
||||
std::set<gd::String> allModel3Ds;
|
||||
std::set<gd::String> allAtlases;
|
||||
std::set<gd::String> allSpines;
|
||||
std::set<gd::String> emptyResources;
|
||||
|
||||
static const std::vector<gd::String> resourceTypes;
|
||||
|
@@ -65,6 +65,12 @@ class ResourcesRenamer : public gd::ArbitraryResourceWorker {
|
||||
virtual void ExposeModel3D(gd::String& resourceName) override {
|
||||
RenameIfNeeded(resourceName);
|
||||
};
|
||||
virtual void ExposeAtlas(gd::String& resourceName) override {
|
||||
RenameIfNeeded(resourceName);
|
||||
};
|
||||
virtual void ExposeSpine(gd::String& resourceName) override {
|
||||
RenameIfNeeded(resourceName);
|
||||
};
|
||||
|
||||
private:
|
||||
void RenameIfNeeded(gd::String& resourceName) {
|
||||
|
@@ -80,6 +80,12 @@ private:
|
||||
void ExposeModel3D(gd::String &resourceName) override {
|
||||
AddUsedResource(resourceName);
|
||||
};
|
||||
void ExposeAtlas(gd::String &resourceName) override {
|
||||
AddUsedResource(resourceName);
|
||||
};
|
||||
void ExposeSpine(gd::String &resourceName) override {
|
||||
AddUsedResource(resourceName);
|
||||
};
|
||||
|
||||
std::set<gd::String> resourceNames;
|
||||
};
|
||||
|
@@ -155,6 +155,10 @@ void CustomObjectConfiguration::ExposeResources(gd::ArbitraryResourceWorker& wor
|
||||
worker.ExposeBitmapFont(newPropertyValue);
|
||||
} else if (resourceType == "model3D") {
|
||||
worker.ExposeModel3D(newPropertyValue);
|
||||
} else if (resourceType == "atlas") {
|
||||
worker.ExposeAtlas(newPropertyValue);
|
||||
} else if (resourceType == "spine") {
|
||||
worker.ExposeSpine(newPropertyValue);
|
||||
}
|
||||
|
||||
if (newPropertyValue != oldPropertyValue) {
|
||||
|
@@ -93,6 +93,10 @@ std::shared_ptr<Resource> ResourcesManager::CreateResource(
|
||||
return std::make_shared<BitmapFontResource>();
|
||||
else if (kind == "model3D")
|
||||
return std::make_shared<Model3DResource>();
|
||||
else if (kind == "atlas")
|
||||
return std::make_shared<AtlasResource>();
|
||||
else if (kind == "spine")
|
||||
return std::make_shared<SpineResource>();
|
||||
|
||||
std::cout << "Bad resource created (type: " << kind << ")" << std::endl;
|
||||
return std::make_shared<Resource>();
|
||||
@@ -756,6 +760,20 @@ void Model3DResource::SerializeTo(SerializerElement& element) const {
|
||||
element.SetAttribute("file", GetFile());
|
||||
}
|
||||
|
||||
void AtlasResource::SetFile(const gd::String& newFile) {
|
||||
file = NormalizePathSeparator(newFile);
|
||||
}
|
||||
|
||||
void AtlasResource::UnserializeFrom(const SerializerElement& element) {
|
||||
SetUserAdded(element.GetBoolAttribute("userAdded"));
|
||||
SetFile(element.GetStringAttribute("file"));
|
||||
}
|
||||
|
||||
void AtlasResource::SerializeTo(SerializerElement& element) const {
|
||||
element.SetAttribute("userAdded", IsUserAdded());
|
||||
element.SetAttribute("file", GetFile());
|
||||
}
|
||||
|
||||
ResourceFolder::ResourceFolder(const ResourceFolder& other) { Init(other); }
|
||||
|
||||
ResourceFolder& ResourceFolder::operator=(const ResourceFolder& other) {
|
||||
|
@@ -373,6 +373,21 @@ class GD_CORE_API JsonResource : public Resource {
|
||||
gd::String file;
|
||||
};
|
||||
|
||||
/**
|
||||
* \brief Describe a spine json file used by a project.
|
||||
*
|
||||
* \see Resource
|
||||
* \ingroup ResourcesManagement
|
||||
*/
|
||||
class GD_CORE_API SpineResource : public JsonResource {
|
||||
public:
|
||||
SpineResource() : JsonResource() { SetKind("spine"); };
|
||||
virtual ~SpineResource(){};
|
||||
virtual SpineResource* Clone() const override {
|
||||
return new SpineResource(*this);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* \brief Describe a tilemap file used by a project.
|
||||
*
|
||||
@@ -507,6 +522,32 @@ class GD_CORE_API Model3DResource : public Resource {
|
||||
gd::String file;
|
||||
};
|
||||
|
||||
/**
|
||||
* \brief Describe an atlas file used by a project.
|
||||
*
|
||||
* \see Resource
|
||||
* \ingroup ResourcesManagement
|
||||
*/
|
||||
class GD_CORE_API AtlasResource : public Resource {
|
||||
public:
|
||||
AtlasResource() : Resource() { SetKind("atlas"); };
|
||||
virtual ~AtlasResource(){};
|
||||
virtual AtlasResource* Clone() const override {
|
||||
return new AtlasResource(*this);
|
||||
}
|
||||
|
||||
virtual const gd::String& GetFile() const override { return file; };
|
||||
virtual void SetFile(const gd::String& newFile) override;
|
||||
|
||||
virtual bool UseFile() const override { return true; }
|
||||
void SerializeTo(SerializerElement& element) const override;
|
||||
|
||||
void UnserializeFrom(const SerializerElement& element) override;
|
||||
|
||||
private:
|
||||
gd::String file;
|
||||
};
|
||||
|
||||
/**
|
||||
* \brief Inventory all resources used by a project
|
||||
*
|
||||
|
1
Extensions/.gitignore
vendored
Normal file
1
Extensions/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Spine/pixi-spine
|
@@ -26,6 +26,7 @@ set(
|
||||
TextEntryObject
|
||||
TextObject
|
||||
TiledSpriteObject
|
||||
Spine
|
||||
TopDownMovementBehavior)
|
||||
|
||||
# Automatically add all listed extensions
|
||||
|
20
Extensions/Spine/CMakeLists.txt
Normal file
20
Extensions/Spine/CMakeLists.txt
Normal file
@@ -0,0 +1,20 @@
|
||||
cmake_minimum_required(VERSION 2.6)
|
||||
cmake_policy(SET CMP0015 NEW)
|
||||
|
||||
project(SpineObject)
|
||||
gd_add_extension_includes()
|
||||
|
||||
#Defines
|
||||
###
|
||||
gd_add_extension_definitions(SpineObject)
|
||||
|
||||
#The targets
|
||||
###
|
||||
include_directories(.)
|
||||
file(GLOB source_files *.cpp *.h)
|
||||
gd_add_clang_utils(SpineObject "${source_files}")
|
||||
gd_add_extension_target(SpineObject "${source_files}")
|
||||
|
||||
#Linker files for the IDE extension
|
||||
###
|
||||
gd_extension_link_libraries(SpineObject)
|
323
Extensions/Spine/JsExtension.js
Normal file
323
Extensions/Spine/JsExtension.js
Normal file
@@ -0,0 +1,323 @@
|
||||
// @flow
|
||||
/**
|
||||
* This is a declaration of an extension for GDevelop 5.
|
||||
*
|
||||
* ℹ️ Changes in this file are watched and automatically imported if the editor
|
||||
* is running. You can also manually run `node import-GDJS-Runtime.js` (in newIDE/app/scripts).
|
||||
*
|
||||
* The file must be named "JsExtension.js", otherwise GDevelop won't load it.
|
||||
* ⚠️ If you make a change and the extension is not loaded, open the developer console
|
||||
* and search for any errors.
|
||||
*
|
||||
* More information on https://github.com/4ian/GDevelop/blob/master/newIDE/README-extensions.md
|
||||
*/
|
||||
|
||||
/*::
|
||||
// Import types to allow Flow to do static type checking on this file.
|
||||
// Extensions declaration are typed using Flow (like the editor), but the files
|
||||
// for the game engine are checked with TypeScript annotations.
|
||||
import { type ObjectsRenderingService, type ObjectsEditorService } from '../JsExtensionTypes.flow.js'
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
createExtension: function (
|
||||
_ /*: (string) => string */,
|
||||
gd /*: libGDevelop */
|
||||
) {
|
||||
const extension = new gd.PlatformExtension();
|
||||
|
||||
extension
|
||||
.setExtensionInformation(
|
||||
'SpineObject',
|
||||
_('Spine (experimental)'),
|
||||
_('Displays a Spine animation.'),
|
||||
'Vladyslav Pohorielov',
|
||||
'Open source (MIT License)'
|
||||
)
|
||||
.setExtensionHelpPath('/objects/spine')
|
||||
.setCategory('Advanced');
|
||||
|
||||
extension
|
||||
.addInstructionOrExpressionGroupMetadata(_('Spine'))
|
||||
.setIcon('JsPlatform/Extensions/spine.svg');
|
||||
|
||||
const object = extension
|
||||
.addObject(
|
||||
'SpineObject',
|
||||
_('Spine (experimental)'),
|
||||
_(
|
||||
'Display and smoothly animate a 2D object with skeletal animations made with Spine. Use files exported from Spine (json, atlas and image).'
|
||||
),
|
||||
'JsPlatform/Extensions/spine.svg',
|
||||
new gd.SpineObjectConfiguration()
|
||||
)
|
||||
.addDefaultBehavior('EffectCapability::EffectBehavior')
|
||||
.addDefaultBehavior('ResizableCapability::ResizableBehavior')
|
||||
.addDefaultBehavior('ScalableCapability::ScalableBehavior')
|
||||
.addDefaultBehavior('FlippableCapability::FlippableBehavior')
|
||||
.addDefaultBehavior('OpacityCapability::OpacityBehavior')
|
||||
.addDefaultBehavior('AnimatableCapability::AnimatableBehavior')
|
||||
.setIncludeFile('Extensions/Spine/spineruntimeobject.js')
|
||||
.addIncludeFile('Extensions/Spine/spineruntimeobject-pixi-renderer.js')
|
||||
.addIncludeFile('Extensions/Spine/pixi-spine/pixi-spine.js')
|
||||
.addIncludeFile('Extensions/Spine/managers/pixi-spine-atlas-manager.js')
|
||||
.addIncludeFile('Extensions/Spine/managers/pixi-spine-manager.js')
|
||||
.setCategoryFullName(_('Advanced'));
|
||||
|
||||
object
|
||||
.addExpressionAndConditionAndAction(
|
||||
'number',
|
||||
'Animation',
|
||||
_('Animation mixing duration'),
|
||||
_(
|
||||
'the duration of the smooth transition between 2 animations (in second)'
|
||||
),
|
||||
_('the animation mixing duration'),
|
||||
_('Animations and images'),
|
||||
'JsPlatform/Extensions/spine.svg'
|
||||
)
|
||||
.addParameter('object', _('Spine'), 'SpineObject')
|
||||
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
|
||||
.setFunctionName('setAnimationMixingDuration')
|
||||
.setGetter('getAnimationMixingDuration');
|
||||
|
||||
return extension;
|
||||
},
|
||||
|
||||
/**
|
||||
* You can optionally add sanity tests that will check the basic working
|
||||
* of your extension behaviors/objects by instantiating behaviors/objects
|
||||
* and setting the property to a given value.
|
||||
*
|
||||
* If you don't have any tests, you can simply return an empty array.
|
||||
*
|
||||
* But it is recommended to create tests for the behaviors/objects properties you created
|
||||
* to avoid mistakes.
|
||||
*/
|
||||
runExtensionSanityTests: function (
|
||||
gd /*: libGDevelop */,
|
||||
extension /*: gdPlatformExtension*/
|
||||
) {
|
||||
return [];
|
||||
},
|
||||
/**
|
||||
* Register editors for objects.
|
||||
*
|
||||
* ℹ️ Run `node import-GDJS-Runtime.js` (in newIDE/app/scripts) if you make any change.
|
||||
*/
|
||||
registerEditorConfigurations: function (
|
||||
objectsEditorService /*: ObjectsEditorService */
|
||||
) {},
|
||||
/**
|
||||
* Register renderers for instance of objects on the scene editor.
|
||||
*
|
||||
* ℹ️ Run `node import-GDJS-Runtime.js` (in newIDE/app/scripts) if you make any change.
|
||||
*/
|
||||
registerInstanceRenderers: function (
|
||||
objectsRenderingService /*: ObjectsRenderingService */
|
||||
) {
|
||||
const { PIXI, RenderedInstance, gd } = objectsRenderingService;
|
||||
|
||||
class RenderedSpineInstance extends RenderedInstance {
|
||||
_spine = null;
|
||||
_rect = new PIXI.Graphics();
|
||||
_initialWidth = null;
|
||||
_initialHeight = null;
|
||||
_animationIndex = -1;
|
||||
_spineOriginOffsetX = 0;
|
||||
_spineOriginOffsetY = 0;
|
||||
|
||||
constructor(
|
||||
project,
|
||||
layout,
|
||||
instance,
|
||||
associatedObjectConfiguration,
|
||||
pixiContainer,
|
||||
pixiResourcesLoader
|
||||
) {
|
||||
super(
|
||||
project,
|
||||
layout,
|
||||
instance,
|
||||
associatedObjectConfiguration,
|
||||
pixiContainer,
|
||||
pixiResourcesLoader
|
||||
);
|
||||
|
||||
// there is issue with spine selection. mouse events are not triggering during interaction.
|
||||
// create the invisible background rectangle to fill spine range.
|
||||
this._rect.alpha = 0;
|
||||
this._pixiObject = new PIXI.Container();
|
||||
this._pixiObject.addChild(this._rect);
|
||||
this._pixiContainer.addChild(this._pixiObject);
|
||||
|
||||
this._loadSpine();
|
||||
}
|
||||
|
||||
static getThumbnail(project, resourcesLoader, objectConfiguration) {
|
||||
return 'JsPlatform/Extensions/spine.svg';
|
||||
}
|
||||
|
||||
update() {
|
||||
this._pixiObject.position.set(
|
||||
this._instance.getX(),
|
||||
this._instance.getY()
|
||||
);
|
||||
|
||||
this.setAnimation(this._instance.getRawDoubleProperty('animation'));
|
||||
|
||||
const width = this.getWidth();
|
||||
const height = this.getHeight();
|
||||
const { _spine: spine } = this;
|
||||
|
||||
if (spine) {
|
||||
spine.width = width;
|
||||
spine.height = height;
|
||||
const localBounds = spine.getLocalBounds(undefined, true);
|
||||
|
||||
this._spineOriginOffsetX = localBounds.x * spine.scale.x;
|
||||
this._spineOriginOffsetY = localBounds.y * spine.scale.y;
|
||||
this._rect.position.set(
|
||||
this._spineOriginOffsetX,
|
||||
this._spineOriginOffsetY
|
||||
);
|
||||
}
|
||||
|
||||
this._rect.clear();
|
||||
this._rect.beginFill(0xffffff);
|
||||
this._rect.lineStyle(1, 0xff0000);
|
||||
this._rect.drawRect(0, 0, width, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns x coordinate of this spine origin offset
|
||||
*/
|
||||
getOriginX() {
|
||||
return -this._spineOriginOffsetX;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns y coordinate of this spine origin offset
|
||||
*/
|
||||
getOriginY() {
|
||||
return -this._spineOriginOffsetY;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} index - animation index
|
||||
*/
|
||||
setAnimation(index) {
|
||||
const { _spine: spine } = this;
|
||||
const configuration = this._getConfiguration();
|
||||
|
||||
if (
|
||||
!spine ||
|
||||
configuration.hasNoAnimations() ||
|
||||
index === this._animationIndex
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Number.isInteger(index) || index < 0) {
|
||||
index = 0;
|
||||
} else if (configuration.getAnimationsCount() <= index) {
|
||||
index = configuration.getAnimationsCount() - 1;
|
||||
}
|
||||
|
||||
this._animationIndex = index;
|
||||
const animation = configuration.getAnimation(index);
|
||||
const source = animation.getSource();
|
||||
const shouldLoop = animation.shouldLoop();
|
||||
const scale = this.getScale();
|
||||
|
||||
// reset scale to track new animation range
|
||||
// if custom size is set it will be reinitialized in update method
|
||||
spine.scale.set(1, 1);
|
||||
spine.state.setAnimation(0, source, shouldLoop);
|
||||
spine.state.tracks[0].trackTime = 0;
|
||||
spine.update(0);
|
||||
spine.autoUpdate = false;
|
||||
this._initialWidth = spine.width * this.getScale();
|
||||
this._initialHeight = spine.height * this.getScale();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number} default width
|
||||
*/
|
||||
getDefaultWidth() {
|
||||
return this._initialWidth !== null ? this._initialWidth : 256;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number} default height
|
||||
*/
|
||||
getDefaultHeight() {
|
||||
return this._initialHeight !== null ? this._initialHeight : 256;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number} defined scale
|
||||
*/
|
||||
getScale() {
|
||||
return Number(this._getProperties().get('scale').getValue()) || 1;
|
||||
}
|
||||
|
||||
onRemovedFromScene() {
|
||||
super.onRemovedFromScene();
|
||||
this._pixiObject.destroy({ children: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns this spine object configuration
|
||||
*/
|
||||
_getConfiguration() {
|
||||
return gd.asSpineConfiguration(this._associatedObjectConfiguration);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns this object properties container
|
||||
*/
|
||||
_getProperties() {
|
||||
return this._associatedObjectConfiguration.getProperties();
|
||||
}
|
||||
|
||||
_loadSpine() {
|
||||
const properties = this._getProperties();
|
||||
const spineResourceName = properties
|
||||
.get('spineResourceName')
|
||||
.getValue();
|
||||
|
||||
this._pixiResourcesLoader
|
||||
.getSpineData(this._project, spineResourceName)
|
||||
.then((spineDataOrLoadingError) => {
|
||||
if (!spineDataOrLoadingError.skeleton) {
|
||||
console.error(
|
||||
'Unable to load Spine (' +
|
||||
(spineDataOrLoadingError.loadingErrorReason ||
|
||||
'Unknown reason') +
|
||||
')',
|
||||
spineDataOrLoadingError.loadingError
|
||||
);
|
||||
this._spine = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this._spine = new PIXI.Spine(spineDataOrLoadingError.skeleton);
|
||||
} catch (error) {
|
||||
console.error('Exception while loading Spine.', error);
|
||||
this._spine = null;
|
||||
return;
|
||||
}
|
||||
this._pixiObject.addChild(this._spine);
|
||||
this.update();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
objectsRenderingService.registerInstanceRenderer(
|
||||
'SpineObject::SpineObject',
|
||||
RenderedSpineInstance
|
||||
);
|
||||
},
|
||||
};
|
165
Extensions/Spine/SpineObjectConfiguration.cpp
Normal file
165
Extensions/Spine/SpineObjectConfiguration.cpp
Normal file
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
GDevelop - Spine Extension
|
||||
Copyright (c) 2010-2016 Florian Rival (Florian.Rival@gmail.com)
|
||||
This project is released under the MIT License.
|
||||
*/
|
||||
|
||||
#include "SpineObjectConfiguration.h"
|
||||
|
||||
#include "GDCore/CommonTools.h"
|
||||
#include "GDCore/IDE/Project/ArbitraryResourceWorker.h"
|
||||
#include "GDCore/Project/InitialInstance.h"
|
||||
#include "GDCore/Project/MeasurementUnit.h"
|
||||
#include "GDCore/Project/Object.h"
|
||||
#include "GDCore/Project/Project.h"
|
||||
#include "GDCore/Project/PropertyDescriptor.h"
|
||||
#include "GDCore/Serialization/SerializerElement.h"
|
||||
#include "GDCore/Tools/Localization.h"
|
||||
|
||||
using namespace std;
|
||||
|
||||
SpineAnimation SpineObjectConfiguration::badAnimation;
|
||||
|
||||
SpineObjectConfiguration::SpineObjectConfiguration()
|
||||
: scale(1), spineResourceName("") {};
|
||||
|
||||
bool SpineObjectConfiguration::UpdateProperty(const gd::String &propertyName, const gd::String &newValue) {
|
||||
if (propertyName == "scale") {
|
||||
scale = newValue.To<double>();
|
||||
return true;
|
||||
}
|
||||
if (propertyName == "spineResourceName") {
|
||||
spineResourceName = newValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
std::map<gd::String, gd::PropertyDescriptor>
|
||||
SpineObjectConfiguration::GetProperties() const {
|
||||
std::map<gd::String, gd::PropertyDescriptor> objectProperties;
|
||||
|
||||
objectProperties["scale"]
|
||||
.SetValue(gd::String::From(scale))
|
||||
.SetType("number")
|
||||
.SetLabel(_("Scale"))
|
||||
.SetGroup(_("Default size"));
|
||||
|
||||
objectProperties["spineResourceName"]
|
||||
.SetValue(spineResourceName)
|
||||
.SetType("resource")
|
||||
.AddExtraInfo("spine")
|
||||
.SetLabel(_("Spine json"));
|
||||
|
||||
return objectProperties;
|
||||
}
|
||||
|
||||
bool SpineObjectConfiguration::UpdateInitialInstanceProperty(
|
||||
gd::InitialInstance &instance, const gd::String &propertyName,
|
||||
const gd::String &newValue, gd::Project &project, gd::Layout &layout
|
||||
) {
|
||||
if (propertyName == "animation") {
|
||||
instance.SetRawDoubleProperty("animation", std::max(0, newValue.empty() ? 0 : newValue.To<int>()));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::map<gd::String, gd::PropertyDescriptor>
|
||||
SpineObjectConfiguration::GetInitialInstanceProperties(const gd::InitialInstance &instance, gd::Project &project, gd::Layout &layout) {
|
||||
std::map<gd::String, gd::PropertyDescriptor> properties;
|
||||
properties["animation"] =
|
||||
gd::PropertyDescriptor(gd::String::From(instance.GetRawDoubleProperty("animation")))
|
||||
.SetLabel(_("Animation"))
|
||||
.SetType("number");
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
void SpineObjectConfiguration::DoUnserializeFrom(gd::Project &project, const gd::SerializerElement &element) {
|
||||
auto &content = element.GetChild("content");
|
||||
|
||||
scale = content.GetDoubleAttribute("scale");
|
||||
spineResourceName = content.GetStringAttribute("spineResourceName");
|
||||
|
||||
RemoveAllAnimations();
|
||||
auto &animationsElement = content.GetChild("animations");
|
||||
animationsElement.ConsiderAsArrayOf("animation");
|
||||
for (std::size_t i = 0; i < animationsElement.GetChildrenCount(); ++i) {
|
||||
auto &animationElement = animationsElement.GetChild(i);
|
||||
SpineAnimation animation;
|
||||
animation.SetName(animationElement.GetStringAttribute("name", ""));
|
||||
animation.SetSource(animationElement.GetStringAttribute("source", ""));
|
||||
animation.SetShouldLoop(animationElement.GetBoolAttribute("loop", false));
|
||||
AddAnimation(animation);
|
||||
}
|
||||
}
|
||||
|
||||
void SpineObjectConfiguration::DoSerializeTo(gd::SerializerElement &element) const {
|
||||
auto &content = element.AddChild("content");
|
||||
content.SetAttribute("scale", scale);
|
||||
content.SetAttribute("spineResourceName", spineResourceName);
|
||||
|
||||
auto &animationsElement = content.AddChild("animations");
|
||||
animationsElement.ConsiderAsArrayOf("animation");
|
||||
for (auto &animation : animations) {
|
||||
auto &animationElement = animationsElement.AddChild("animation");
|
||||
animationElement.SetAttribute("name", animation.GetName());
|
||||
animationElement.SetAttribute("source", animation.GetSource());
|
||||
animationElement.SetAttribute("loop", animation.ShouldLoop());
|
||||
}
|
||||
}
|
||||
|
||||
void SpineObjectConfiguration::ExposeResources(gd::ArbitraryResourceWorker &worker) {
|
||||
worker.ExposeSpine(spineResourceName);
|
||||
worker.ExposeEmbeddeds(spineResourceName);
|
||||
}
|
||||
|
||||
const SpineAnimation &
|
||||
SpineObjectConfiguration::GetAnimation(std::size_t nb) const {
|
||||
if (nb >= animations.size()) return badAnimation;
|
||||
|
||||
return animations[nb];
|
||||
}
|
||||
|
||||
SpineAnimation &SpineObjectConfiguration::GetAnimation(std::size_t nb) {
|
||||
if (nb >= animations.size()) return badAnimation;
|
||||
|
||||
return animations[nb];
|
||||
}
|
||||
|
||||
bool SpineObjectConfiguration::HasAnimationNamed(const gd::String &name) const {
|
||||
return !name.empty() && (find_if(animations.begin(), animations.end(),
|
||||
[&name](const SpineAnimation &animation) {
|
||||
return animation.GetName() == name;
|
||||
}) != animations.end());
|
||||
}
|
||||
|
||||
void SpineObjectConfiguration::AddAnimation(const SpineAnimation &animation) {
|
||||
animations.push_back(animation);
|
||||
}
|
||||
|
||||
bool SpineObjectConfiguration::RemoveAnimation(std::size_t nb) {
|
||||
if (nb >= GetAnimationsCount())
|
||||
return false;
|
||||
|
||||
animations.erase(animations.begin() + nb);
|
||||
return true;
|
||||
}
|
||||
|
||||
void SpineObjectConfiguration::SwapAnimations(std::size_t firstIndex, std::size_t secondIndex) {
|
||||
if (firstIndex < animations.size() && secondIndex < animations.size() && firstIndex != secondIndex) {
|
||||
std::swap(animations[firstIndex], animations[secondIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
void SpineObjectConfiguration::MoveAnimation(std::size_t oldIndex, std::size_t newIndex) {
|
||||
if (oldIndex >= animations.size() || newIndex >= animations.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto animation = animations[oldIndex];
|
||||
animations.erase(animations.begin() + oldIndex);
|
||||
animations.insert(animations.begin() + newIndex, animation);
|
||||
}
|
162
Extensions/Spine/SpineObjectConfiguration.h
Normal file
162
Extensions/Spine/SpineObjectConfiguration.h
Normal file
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
GDevelop - Spine Extension
|
||||
Copyright (c) 2010-2016 Florian Rival (Florian.Rival@gmail.com)
|
||||
This project is released under the MIT License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "GDCore/Project/ObjectConfiguration.h"
|
||||
namespace gd {
|
||||
class InitialInstance;
|
||||
class Project;
|
||||
} // namespace gd
|
||||
|
||||
class GD_EXTENSION_API SpineAnimation {
|
||||
public:
|
||||
SpineAnimation() : shouldLoop(false) {};
|
||||
virtual ~SpineAnimation(){};
|
||||
|
||||
/**
|
||||
* \brief Return the name of the animation
|
||||
*/
|
||||
const gd::String &GetName() const { return name; }
|
||||
|
||||
/**
|
||||
* \brief Change the name of the animation
|
||||
*/
|
||||
void SetName(const gd::String &name_) { name = name_; }
|
||||
|
||||
/**
|
||||
* \brief Return the name of the animation from the spine file.
|
||||
*/
|
||||
const gd::String &GetSource() const { return source; }
|
||||
|
||||
/**
|
||||
* \brief Change the name of the animation from the spine file.
|
||||
*/
|
||||
void SetSource(const gd::String &source_) { source = source_; }
|
||||
|
||||
/**
|
||||
* \brief Return true if the animation should loop.
|
||||
*/
|
||||
const bool ShouldLoop() const { return shouldLoop; }
|
||||
|
||||
/**
|
||||
* \brief Change whether the animation should loop or not.
|
||||
*/
|
||||
void SetShouldLoop(bool shouldLoop_) { shouldLoop = shouldLoop_; }
|
||||
|
||||
private:
|
||||
gd::String name;
|
||||
gd::String source;
|
||||
bool shouldLoop;
|
||||
};
|
||||
|
||||
/**
|
||||
* \brief Spine object configuration is used for storage and for the IDE.
|
||||
*/
|
||||
class GD_EXTENSION_API SpineObjectConfiguration : public gd::ObjectConfiguration {
|
||||
public:
|
||||
SpineObjectConfiguration();
|
||||
virtual ~SpineObjectConfiguration(){};
|
||||
virtual std::unique_ptr<gd::ObjectConfiguration> Clone() const override {
|
||||
return gd::make_unique<SpineObjectConfiguration>(*this);
|
||||
}
|
||||
|
||||
virtual void ExposeResources(gd::ArbitraryResourceWorker &worker) override;
|
||||
|
||||
virtual std::map<gd::String, gd::PropertyDescriptor>GetProperties() const override;
|
||||
|
||||
virtual bool UpdateProperty(const gd::String &name, const gd::String &value) override;
|
||||
|
||||
virtual std::map<gd::String, gd::PropertyDescriptor>
|
||||
GetInitialInstanceProperties(const gd::InitialInstance &instance,
|
||||
gd::Project &project,
|
||||
gd::Layout &layout) override;
|
||||
|
||||
virtual bool UpdateInitialInstanceProperty(gd::InitialInstance &instance,
|
||||
const gd::String &name,
|
||||
const gd::String &value,
|
||||
gd::Project &project,
|
||||
gd::Layout &layout) override;
|
||||
|
||||
/** \name Animations
|
||||
* Methods related to animations management
|
||||
*/
|
||||
///@{
|
||||
/**
|
||||
* \brief Return the animation at the specified index.
|
||||
* If the index is out of bound, a "bad animation" object is returned.
|
||||
*/
|
||||
const SpineAnimation &GetAnimation(std::size_t nb) const;
|
||||
|
||||
/**
|
||||
* \brief Return the animation at the specified index.
|
||||
* If the index is out of bound, a "bad animation" object is returned.
|
||||
*/
|
||||
SpineAnimation &GetAnimation(std::size_t nb);
|
||||
|
||||
/**
|
||||
* \brief Return the number of animations this object has.
|
||||
*/
|
||||
std::size_t GetAnimationsCount() const { return animations.size(); };
|
||||
|
||||
/**
|
||||
* \brief Return true if the animation called "name" exists.
|
||||
*/
|
||||
bool HasAnimationNamed(const gd::String& name) const;
|
||||
|
||||
/**
|
||||
* \brief Add an animation at the end of the existing ones.
|
||||
*/
|
||||
void AddAnimation(const SpineAnimation &animation);
|
||||
|
||||
/**
|
||||
* \brief Remove an animation.
|
||||
*/
|
||||
bool RemoveAnimation(std::size_t nb);
|
||||
|
||||
/**
|
||||
* \brief Remove all animations.
|
||||
*/
|
||||
void RemoveAllAnimations() { animations.clear(); }
|
||||
|
||||
/**
|
||||
* \brief Return true if the object hasn't any animation.
|
||||
*/
|
||||
bool HasNoAnimations() const { return animations.empty(); }
|
||||
|
||||
/**
|
||||
* \brief Swap the position of two animations
|
||||
*/
|
||||
void SwapAnimations(std::size_t firstIndex, std::size_t secondIndex);
|
||||
|
||||
/**
|
||||
* \brief Change the position of the specified animation
|
||||
*/
|
||||
void MoveAnimation(std::size_t oldIndex, std::size_t newIndex);
|
||||
|
||||
/**
|
||||
* \brief Return a read-only reference to the vector containing all the
|
||||
* animation of the object.
|
||||
*/
|
||||
const std::vector<SpineAnimation> &GetAllAnimations() const {
|
||||
return animations;
|
||||
}
|
||||
|
||||
///@}
|
||||
|
||||
protected:
|
||||
virtual void DoUnserializeFrom(gd::Project &project, const gd::SerializerElement &element) override;
|
||||
virtual void DoSerializeTo(gd::SerializerElement &element) const override;
|
||||
|
||||
private:
|
||||
double scale;
|
||||
|
||||
gd::String spineResourceName;
|
||||
|
||||
std::vector<SpineAnimation> animations;
|
||||
|
||||
static SpineAnimation badAnimation;
|
||||
};
|
199
Extensions/Spine/managers/pixi-spine-atlas-manager.ts
Normal file
199
Extensions/Spine/managers/pixi-spine-atlas-manager.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
* GDevelop JS Platform
|
||||
* Copyright 2013-present Florian Rival (Florian.Rival@gmail.com). All rights reserved.
|
||||
* This project is released under the MIT License.
|
||||
*/
|
||||
namespace gdjs {
|
||||
/** The callback called when a text that was requested is loaded (or an error occurred). */
|
||||
export type SpineAtlasManagerRequestCallback = (
|
||||
error: Error | null,
|
||||
content?: pixi_spine.TextureAtlas
|
||||
) => void;
|
||||
|
||||
const atlasKinds: ResourceKind[] = ['atlas'];
|
||||
|
||||
/**
|
||||
* AtlasManager loads atlas files with pixi loader, using the "atlas" resources
|
||||
* registered in the game resources and process them to Pixi TextureAtlas.
|
||||
*
|
||||
* Contrary to audio/fonts, text files are loaded asynchronously, when requested.
|
||||
* You should properly handle errors, and give the developer/player a way to know
|
||||
* that loading failed.
|
||||
*/
|
||||
export class SpineAtlasManager implements gdjs.ResourceManager {
|
||||
private _imageManager: ImageManager;
|
||||
private _resourceLoader: ResourceLoader;
|
||||
private _loadedSpineAtlases = new gdjs.ResourceCache<
|
||||
pixi_spine.TextureAtlas
|
||||
>();
|
||||
private _loadingSpineAtlases = new gdjs.ResourceCache<
|
||||
Promise<pixi_spine.TextureAtlas>
|
||||
>();
|
||||
|
||||
/**
|
||||
* @param resources The resources data of the game.
|
||||
* @param resourcesLoader The resources loader of the game.
|
||||
*/
|
||||
constructor(
|
||||
resourceLoader: gdjs.ResourceLoader,
|
||||
imageManager: ImageManager
|
||||
) {
|
||||
this._resourceLoader = resourceLoader;
|
||||
this._imageManager = imageManager;
|
||||
}
|
||||
|
||||
getResourceKinds(): ResourceKind[] {
|
||||
return atlasKinds;
|
||||
}
|
||||
|
||||
async processResource(resourceName: string): Promise<void> {
|
||||
// Do nothing because pixi-spine parses resources by itself.
|
||||
}
|
||||
|
||||
async loadResource(resourceName: string): Promise<void> {
|
||||
await this.getOrLoad(resourceName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns promisified loaded atlas resource if it is availble, loads it otherwise.
|
||||
*
|
||||
* @param resources The data of resource to load.
|
||||
*/
|
||||
getOrLoad(resourceName: string): Promise<pixi_spine.TextureAtlas> {
|
||||
const resource = this._getAtlasResource(resourceName);
|
||||
|
||||
if (!resource) {
|
||||
return Promise.reject(
|
||||
`Unable to find atlas for resource '${resourceName}'.`
|
||||
);
|
||||
}
|
||||
|
||||
let loadingPromise = this._loadingSpineAtlases.get(resource);
|
||||
|
||||
if (!loadingPromise) {
|
||||
loadingPromise = new Promise<pixi_spine.TextureAtlas>(
|
||||
(resolve, reject) => {
|
||||
const onLoad: SpineAtlasManagerRequestCallback = (
|
||||
error,
|
||||
content
|
||||
) => {
|
||||
if (error) {
|
||||
return reject(
|
||||
`Error while preloading a spine atlas resource: ${error}`
|
||||
);
|
||||
}
|
||||
if (!content) {
|
||||
return reject(
|
||||
`Cannot reach texture atlas for resource '${resourceName}'.`
|
||||
);
|
||||
}
|
||||
|
||||
resolve(content);
|
||||
};
|
||||
|
||||
this.load(resource, onLoad);
|
||||
}
|
||||
);
|
||||
|
||||
this._loadingSpineAtlases.set(resource, loadingPromise);
|
||||
}
|
||||
|
||||
return loadingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load specified atlas resource and pass it to callback once it is loaded.
|
||||
*
|
||||
* @param resources The data of resource to load.
|
||||
* @param callback The callback to pass atlas to it once it is loaded.
|
||||
*/
|
||||
load(
|
||||
resource: ResourceData,
|
||||
callback: SpineAtlasManagerRequestCallback
|
||||
): void {
|
||||
const game = this._resourceLoader.getRuntimeGame();
|
||||
const embeddedResourcesNames = game.getEmbeddedResourcesNames(
|
||||
resource.name
|
||||
);
|
||||
|
||||
if (!embeddedResourcesNames.length)
|
||||
return callback(
|
||||
new Error(`${resource.name} do not have image metadata!`)
|
||||
);
|
||||
|
||||
const images = embeddedResourcesNames.reduce<{
|
||||
[key: string]: PIXI.Texture;
|
||||
}>((imagesMap, embeddedResourceName) => {
|
||||
const mappedResourceName = game.resolveEmbeddedResource(
|
||||
resource.name,
|
||||
embeddedResourceName
|
||||
);
|
||||
imagesMap[
|
||||
embeddedResourceName
|
||||
] = this._imageManager.getOrLoadPIXITexture(mappedResourceName);
|
||||
|
||||
return imagesMap;
|
||||
}, {});
|
||||
const onLoad = (atlas: pixi_spine.TextureAtlas) => {
|
||||
this._loadedSpineAtlases.set(resource, atlas);
|
||||
callback(null, atlas);
|
||||
};
|
||||
|
||||
PIXI.Assets.setPreferences({
|
||||
preferWorkers: false,
|
||||
crossOrigin: this._resourceLoader.checkIfCredentialsRequired(
|
||||
resource.file
|
||||
)
|
||||
? 'use-credentials'
|
||||
: 'anonymous',
|
||||
});
|
||||
PIXI.Assets.add(resource.name, resource.file, { images });
|
||||
PIXI.Assets.load<pixi_spine.TextureAtlas | string>(resource.name).then(
|
||||
(atlas) => {
|
||||
/**
|
||||
* Ideally atlas of TextureAtlas should be passed here
|
||||
* but there is known issue in case of preloaded images (see https://github.com/pixijs/spine/issues/537)
|
||||
*
|
||||
* Here covered all possible ways to make it work fine if issue is fixed in pixi-spine or after migration to spine-pixi
|
||||
*/
|
||||
if (typeof atlas === 'string') {
|
||||
new pixi_spine.TextureAtlas(
|
||||
atlas,
|
||||
(textureName, textureCb) =>
|
||||
textureCb(images[textureName].baseTexture),
|
||||
onLoad
|
||||
);
|
||||
} else {
|
||||
onLoad(atlas);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given atlas resource was loaded (preloaded or loaded with `load`).
|
||||
* @param resourceName The name of the atlas resource.
|
||||
* @returns true if the content of the atlas resource is loaded, false otherwise.
|
||||
*/
|
||||
isLoaded(resourceName: string): boolean {
|
||||
return !!this._loadedSpineAtlases.getFromName(resourceName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Pixi TextureAtlas for the given resource that is already loaded (preloaded or loaded with `load`).
|
||||
* If the resource is not loaded, `null` will be returned.
|
||||
* @param resourceName The name of the atlas resource.
|
||||
* @returns the TextureAtlas of the atlas if loaded, `null` otherwise.
|
||||
*/
|
||||
getAtlasTexture(resourceName: string): pixi_spine.TextureAtlas | null {
|
||||
return this._loadedSpineAtlases.getFromName(resourceName);
|
||||
}
|
||||
|
||||
private _getAtlasResource(resourceName: string): ResourceData | null {
|
||||
const resource = this._resourceLoader.getResource(resourceName);
|
||||
return resource && this.getResourceKinds().includes(resource.kind)
|
||||
? resource
|
||||
: null;
|
||||
}
|
||||
}
|
||||
}
|
120
Extensions/Spine/managers/pixi-spine-manager.ts
Normal file
120
Extensions/Spine/managers/pixi-spine-manager.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* GDevelop JS Platform
|
||||
* Copyright 2013-present Florian Rival (Florian.Rival@gmail.com). All rights reserved.
|
||||
* This project is released under the MIT License.
|
||||
*/
|
||||
namespace gdjs {
|
||||
const logger = new gdjs.Logger('Spine Manager');
|
||||
|
||||
const resourceKinds: ResourceKind[] = ['spine'];
|
||||
|
||||
/**
|
||||
* SpineManager manages pixi spine skeleton data.
|
||||
*/
|
||||
export class SpineManager implements gdjs.ResourceManager {
|
||||
private _spineAtlasManager: SpineAtlasManager;
|
||||
private _resourceLoader: ResourceLoader;
|
||||
private _loadedSpines = new gdjs.ResourceCache<pixi_spine.ISkeletonData>();
|
||||
|
||||
/**
|
||||
* @param resourceDataArray The resources data of the game.
|
||||
* @param resourcesLoader The resources loader of the game.
|
||||
*/
|
||||
constructor(
|
||||
resourceLoader: gdjs.ResourceLoader,
|
||||
spineAtlasManager: SpineAtlasManager
|
||||
) {
|
||||
this._resourceLoader = resourceLoader;
|
||||
this._spineAtlasManager = spineAtlasManager;
|
||||
}
|
||||
|
||||
getResourceKinds(): ResourceKind[] {
|
||||
return resourceKinds;
|
||||
}
|
||||
|
||||
async processResource(resourceName: string): Promise<void> {
|
||||
// Do nothing because pixi-spine parses resources by itself.
|
||||
}
|
||||
|
||||
async loadResource(resourceName: string): Promise<void> {
|
||||
const resource = this._getSpineResource(resourceName);
|
||||
|
||||
if (!resource) {
|
||||
return logger.error(
|
||||
`Unable to find spine json for resource ${resourceName}.`
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const game = this._resourceLoader.getRuntimeGame();
|
||||
const embeddedResourcesNames = game.getEmbeddedResourcesNames(
|
||||
resource.name
|
||||
);
|
||||
|
||||
// there should be exactly one file which is pointing to atlas
|
||||
if (embeddedResourcesNames.length !== 1) {
|
||||
return logger.error(
|
||||
`Unable to find atlas metadata for resource spine json ${resourceName}.`
|
||||
);
|
||||
}
|
||||
|
||||
const atlasResourceName = game.resolveEmbeddedResource(
|
||||
resource.name,
|
||||
embeddedResourcesNames[0]
|
||||
);
|
||||
const spineAtlas = await this._spineAtlasManager.getOrLoad(
|
||||
atlasResourceName
|
||||
);
|
||||
PIXI.Assets.setPreferences({
|
||||
preferWorkers: false,
|
||||
crossOrigin: this._resourceLoader.checkIfCredentialsRequired(
|
||||
resource.file
|
||||
)
|
||||
? 'use-credentials'
|
||||
: 'anonymous',
|
||||
});
|
||||
PIXI.Assets.add(resource.name, resource.file, { spineAtlas });
|
||||
const loadedJson = await PIXI.Assets.load(resource.name);
|
||||
|
||||
if (loadedJson.spineData) {
|
||||
this._loadedSpines.set(resource, loadedJson.spineData);
|
||||
} else {
|
||||
logger.error(
|
||||
`Loader cannot process spine resource ${resource.name} correctly.`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error while preloading spine resource ${resource.name}: ${error}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the object for the given resource that is already loaded (preloaded or loaded with `loadJson`).
|
||||
* If the resource is not loaded, `null` will be returned.
|
||||
*
|
||||
* @param resourceName The name of the spine skeleton.
|
||||
* @returns the spine skeleton if loaded, `null` otherwise.
|
||||
*/
|
||||
getSpine(resourceName: string): pixi_spine.ISkeletonData | null {
|
||||
return this._loadedSpines.getFromName(resourceName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given spine skeleton was loaded.
|
||||
* @param resourceName The name of the spine skeleton.
|
||||
* @returns true if the content of the spine skeleton is loaded, false otherwise.
|
||||
*/
|
||||
isSpineLoaded(resourceName: string): boolean {
|
||||
return !!this._loadedSpines.getFromName(resourceName);
|
||||
}
|
||||
|
||||
private _getSpineResource(resourceName: string): ResourceData | null {
|
||||
const resource = this._resourceLoader.getResource(resourceName);
|
||||
return resource && this.getResourceKinds().includes(resource.kind)
|
||||
? resource
|
||||
: null;
|
||||
}
|
||||
}
|
||||
}
|
202
Extensions/Spine/spineruntimeobject-pixi-renderer.ts
Normal file
202
Extensions/Spine/spineruntimeobject-pixi-renderer.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
namespace gdjs {
|
||||
const isSpine = (obj: any): obj is pixi_spine.Spine =>
|
||||
obj instanceof pixi_spine.Spine;
|
||||
|
||||
export class SpineRuntimeObjectPixiRenderer {
|
||||
private _object: gdjs.SpineRuntimeObject;
|
||||
private _rendererObject: pixi_spine.Spine | PIXI.Container;
|
||||
private _isAnimationComplete = true;
|
||||
|
||||
/**
|
||||
* @param runtimeObject The object to render
|
||||
* @param instanceContainer The container in which the object is
|
||||
*/
|
||||
constructor(
|
||||
runtimeObject: gdjs.SpineRuntimeObject,
|
||||
private instanceContainer: gdjs.RuntimeInstanceContainer
|
||||
) {
|
||||
this._object = runtimeObject;
|
||||
this._rendererObject = this.constructRendererObject();
|
||||
if (isSpine(this._rendererObject)) {
|
||||
this._rendererObject.autoUpdate = false;
|
||||
}
|
||||
|
||||
this.updatePosition();
|
||||
this.updateAngle();
|
||||
this.updateOpacity();
|
||||
this.updateScale();
|
||||
|
||||
instanceContainer
|
||||
.getLayer('')
|
||||
.getRenderer()
|
||||
.addRendererObject(this._rendererObject, runtimeObject.getZOrder());
|
||||
}
|
||||
|
||||
updateAnimation(timeDelta: float) {
|
||||
if (!isSpine(this._rendererObject)) {
|
||||
return;
|
||||
}
|
||||
this._rendererObject.update(timeDelta);
|
||||
}
|
||||
|
||||
getRendererObject(): pixi_spine.Spine | PIXI.Container {
|
||||
return this._rendererObject;
|
||||
}
|
||||
|
||||
getOriginOffset(): PIXI.Point {
|
||||
if (!isSpine(this._rendererObject)) return new PIXI.Point(0, 0);
|
||||
|
||||
const localBounds = this._rendererObject.getLocalBounds(undefined, true);
|
||||
|
||||
return new PIXI.Point(
|
||||
localBounds.x * this._rendererObject.scale.x,
|
||||
localBounds.y * this._rendererObject.scale.y
|
||||
);
|
||||
}
|
||||
|
||||
onDestroy(): void {
|
||||
this._rendererObject.destroy();
|
||||
}
|
||||
|
||||
updateScale(): void {
|
||||
const scaleX = Math.max(
|
||||
this._object._originalScale * this._object.getScaleX(),
|
||||
0
|
||||
);
|
||||
const scaleY = Math.max(
|
||||
this._object._originalScale * this._object.getScaleY(),
|
||||
0
|
||||
);
|
||||
this._rendererObject.scale.x = this._object.isFlippedX()
|
||||
? -scaleX
|
||||
: scaleX;
|
||||
this._rendererObject.scale.y = this._object.isFlippedY()
|
||||
? -scaleY
|
||||
: scaleY;
|
||||
}
|
||||
|
||||
updatePosition(): void {
|
||||
this._rendererObject.position.x = this._object.x;
|
||||
this._rendererObject.position.y = this._object.y;
|
||||
}
|
||||
|
||||
updateAngle(): void {
|
||||
this._rendererObject.rotation = gdjs.toRad(this._object.angle);
|
||||
}
|
||||
|
||||
updateOpacity(): void {
|
||||
this._rendererObject.alpha = this._object.getOpacity() / 255;
|
||||
}
|
||||
|
||||
getWidth(): float {
|
||||
return this._rendererObject.width;
|
||||
}
|
||||
|
||||
getHeight(): float {
|
||||
return this._rendererObject.height;
|
||||
}
|
||||
|
||||
setWidth(width: float): void {
|
||||
this._rendererObject.width = width;
|
||||
}
|
||||
|
||||
setHeight(height: float): void {
|
||||
this._rendererObject.height = height;
|
||||
}
|
||||
|
||||
getUnscaledWidth(): float {
|
||||
return Math.abs(
|
||||
(this._rendererObject.width * this._object._originalScale) /
|
||||
this._rendererObject.scale.x
|
||||
);
|
||||
}
|
||||
|
||||
getUnscaledHeight(): float {
|
||||
return Math.abs(
|
||||
(this._rendererObject.height * this._object._originalScale) /
|
||||
this._rendererObject.scale.y
|
||||
);
|
||||
}
|
||||
|
||||
setMixing(from: string, to: string, duration: number): void {
|
||||
if (!isSpine(this._rendererObject)) return;
|
||||
|
||||
this._rendererObject.stateData.setMix(from, to, duration);
|
||||
}
|
||||
|
||||
setAnimation(animation: string, loop: boolean): void {
|
||||
if (isSpine(this._rendererObject)) {
|
||||
const onCompleteListener: pixi_spine.IAnimationStateListener = {
|
||||
complete: () => {
|
||||
this._isAnimationComplete = true;
|
||||
(this._rendererObject as pixi_spine.Spine).state.removeListener(
|
||||
onCompleteListener
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
this._isAnimationComplete = false;
|
||||
this._rendererObject.state.addListener(onCompleteListener);
|
||||
this._rendererObject.state.setAnimation(0, animation, loop);
|
||||
this._rendererObject.update(0);
|
||||
}
|
||||
}
|
||||
|
||||
getAnimationDuration(sourceAnimationName: string) {
|
||||
if (!isSpine(this._rendererObject)) {
|
||||
return 0;
|
||||
}
|
||||
const animation = this._rendererObject.spineData.findAnimation(
|
||||
sourceAnimationName
|
||||
);
|
||||
return animation ? animation.duration : 0;
|
||||
}
|
||||
|
||||
getAnimationElapsedTime(): number {
|
||||
if (!isSpine(this._rendererObject)) {
|
||||
return 0;
|
||||
}
|
||||
const tracks = this._rendererObject.state.tracks;
|
||||
if (tracks.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
// This should be fine because only 1 track is used.
|
||||
const track = tracks[0];
|
||||
// @ts-ignore TrackEntry.getAnimationTime is not exposed.
|
||||
return track.getAnimationTime();
|
||||
}
|
||||
|
||||
setAnimationElapsedTime(time: number): void {
|
||||
if (!isSpine(this._rendererObject)) {
|
||||
return;
|
||||
}
|
||||
const tracks = this._rendererObject.state.tracks;
|
||||
if (tracks.length === 0) {
|
||||
return;
|
||||
}
|
||||
const track = tracks[0];
|
||||
track.trackTime = time;
|
||||
}
|
||||
|
||||
isAnimationComplete(): boolean {
|
||||
return this._isAnimationComplete;
|
||||
}
|
||||
|
||||
private constructRendererObject(): pixi_spine.Spine | PIXI.Container {
|
||||
const game = this.instanceContainer.getGame();
|
||||
const spineManager = game.getSpineManager();
|
||||
|
||||
if (
|
||||
!spineManager ||
|
||||
!spineManager.isSpineLoaded(this._object.spineResourceName)
|
||||
) {
|
||||
return new PIXI.Container();
|
||||
}
|
||||
|
||||
return new pixi_spine.Spine(
|
||||
spineManager.getSpine(this._object.spineResourceName)!
|
||||
);
|
||||
}
|
||||
}
|
||||
export const SpineRuntimeObjectRenderer = SpineRuntimeObjectPixiRenderer;
|
||||
}
|
406
Extensions/Spine/spineruntimeobject.ts
Normal file
406
Extensions/Spine/spineruntimeobject.ts
Normal file
@@ -0,0 +1,406 @@
|
||||
namespace gdjs {
|
||||
type SpineAnimation = { name: string; source: string; loop: boolean };
|
||||
|
||||
export type SpineObjectDataType = {
|
||||
content: {
|
||||
opacity: float;
|
||||
scale: float;
|
||||
timeScale: float;
|
||||
spineResourceName: string;
|
||||
animations: SpineAnimation[];
|
||||
};
|
||||
};
|
||||
export type SpineObjectData = ObjectData & SpineObjectDataType;
|
||||
|
||||
export class SpineRuntimeObject
|
||||
extends gdjs.RuntimeObject
|
||||
implements
|
||||
gdjs.Resizable,
|
||||
gdjs.Scalable,
|
||||
gdjs.Animatable,
|
||||
gdjs.OpacityHandler {
|
||||
private _opacity: float = 255;
|
||||
private _scaleX: number = 1;
|
||||
private _scaleY: number = 1;
|
||||
_originalScale: number;
|
||||
private _flippedX: boolean = false;
|
||||
private _flippedY: boolean = false;
|
||||
private _animations: SpineAnimation[];
|
||||
private _currentAnimationIndex = -1;
|
||||
private _animationSpeedScale: float = 1;
|
||||
private _animationPaused: boolean = false;
|
||||
private _isPausedFrameDirty = false;
|
||||
/** The duration in second for the smooth transition between 2 animations */
|
||||
private _animationMixingDuration: number;
|
||||
private _renderer: gdjs.SpineRuntimeObjectPixiRenderer;
|
||||
|
||||
readonly spineResourceName: string;
|
||||
|
||||
/**
|
||||
* @param instanceContainer The container the object belongs to.
|
||||
* @param objectData The object data used to initialize the object
|
||||
*/
|
||||
constructor(
|
||||
instanceContainer: gdjs.RuntimeInstanceContainer,
|
||||
objectData: SpineObjectData
|
||||
) {
|
||||
super(instanceContainer, objectData);
|
||||
|
||||
this._animations = objectData.content.animations;
|
||||
this._originalScale = objectData.content.scale;
|
||||
this.spineResourceName = objectData.content.spineResourceName;
|
||||
this._animationMixingDuration = 0.1;
|
||||
this._renderer = new gdjs.SpineRuntimeObjectRenderer(
|
||||
this,
|
||||
instanceContainer
|
||||
);
|
||||
this.setAnimationIndex(0);
|
||||
this._renderer.updateAnimation(0);
|
||||
|
||||
// *ALWAYS* call `this.onCreated()` at the very end of your object constructor.
|
||||
this.onCreated();
|
||||
}
|
||||
|
||||
update(instanceContainer: gdjs.RuntimeInstanceContainer): void {
|
||||
if (this._animationPaused) {
|
||||
if (this._isPausedFrameDirty) {
|
||||
this._renderer.updateAnimation(0);
|
||||
this.invalidateHitboxes();
|
||||
this._isPausedFrameDirty = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const elapsedTime = this.getElapsedTime() / 1000;
|
||||
this._renderer.updateAnimation(elapsedTime * this._animationSpeedScale);
|
||||
this.invalidateHitboxes();
|
||||
}
|
||||
|
||||
getRendererObject(): pixi_spine.Spine | PIXI.Container {
|
||||
return this._renderer.getRendererObject();
|
||||
}
|
||||
|
||||
updateFromObjectData(
|
||||
oldObjectData: SpineObjectData,
|
||||
newObjectData: SpineObjectData
|
||||
): boolean {
|
||||
super.updateFromObjectData(oldObjectData, newObjectData);
|
||||
|
||||
if (oldObjectData.content.scale !== newObjectData.content.scale) {
|
||||
this._originalScale = newObjectData.content.scale;
|
||||
this._renderer.updateScale();
|
||||
this.invalidateHitboxes();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
extraInitializationFromInitialInstance(
|
||||
initialInstanceData: InstanceData
|
||||
): void {
|
||||
const animationData = initialInstanceData.numberProperties.find(
|
||||
(data) => data.name === 'animation'
|
||||
);
|
||||
const animationIndex = animationData
|
||||
? animationData.value
|
||||
: this._currentAnimationIndex;
|
||||
|
||||
this.setAnimationIndexWithMixing(animationIndex, 0);
|
||||
|
||||
if (initialInstanceData.customSize) {
|
||||
this.setSize(initialInstanceData.width, initialInstanceData.height);
|
||||
this.invalidateHitboxes();
|
||||
}
|
||||
}
|
||||
|
||||
getDrawableX(): number {
|
||||
const originOffset = this._renderer.getOriginOffset();
|
||||
|
||||
return this.getX() + originOffset.x;
|
||||
}
|
||||
|
||||
getDrawableY(): number {
|
||||
const originOffset = this._renderer.getOriginOffset();
|
||||
|
||||
return this.getY() + originOffset.y;
|
||||
}
|
||||
|
||||
onDestroyed(): void {
|
||||
super.onDestroyed();
|
||||
this._renderer.onDestroy();
|
||||
}
|
||||
|
||||
setX(x: float): void {
|
||||
super.setX(x);
|
||||
this._renderer.updatePosition();
|
||||
}
|
||||
|
||||
setY(y: float): void {
|
||||
super.setY(y);
|
||||
this._renderer.updatePosition();
|
||||
}
|
||||
|
||||
setAngle(angle: float): void {
|
||||
super.setAngle(angle);
|
||||
this._renderer.updateAngle();
|
||||
}
|
||||
|
||||
setOpacity(opacity: float): void {
|
||||
this._opacity = Math.max(0, Math.min(255, opacity));
|
||||
this._renderer.updateOpacity();
|
||||
}
|
||||
|
||||
getOpacity(): float {
|
||||
return this._opacity;
|
||||
}
|
||||
|
||||
getWidth(): float {
|
||||
return this._renderer.getWidth();
|
||||
}
|
||||
|
||||
getHeight(): float {
|
||||
return this._renderer.getHeight();
|
||||
}
|
||||
|
||||
setWidth(newWidth: float): void {
|
||||
const unscaledWidth = this._renderer.getUnscaledWidth();
|
||||
if (unscaledWidth !== 0) {
|
||||
this.setScaleX(newWidth / unscaledWidth);
|
||||
}
|
||||
}
|
||||
|
||||
setHeight(newHeight: float): void {
|
||||
const unscaledHeight = this._renderer.getUnscaledHeight();
|
||||
if (unscaledHeight !== 0) {
|
||||
this.setScaleY(newHeight / unscaledHeight);
|
||||
}
|
||||
}
|
||||
|
||||
setSize(newWidth: number, newHeight: number): void {
|
||||
this.setWidth(newWidth);
|
||||
this.setHeight(newHeight);
|
||||
}
|
||||
|
||||
setScale(newScale: float): void {
|
||||
if (newScale < 0) {
|
||||
newScale = 0;
|
||||
}
|
||||
if (
|
||||
newScale === Math.abs(this._scaleX) &&
|
||||
newScale === Math.abs(this._scaleY)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this._scaleX = newScale * (this._flippedX ? -1 : 1);
|
||||
this._scaleY = newScale * (this._flippedY ? -1 : 1);
|
||||
this._renderer.updateScale();
|
||||
this.invalidateHitboxes();
|
||||
}
|
||||
|
||||
setScaleX(newScale: float): void {
|
||||
if (newScale < 0) {
|
||||
newScale = 0;
|
||||
}
|
||||
if (newScale === Math.abs(this._scaleX)) {
|
||||
return;
|
||||
}
|
||||
this._scaleX = newScale * (this._flippedX ? -1 : 1);
|
||||
this._renderer.updateScale();
|
||||
this.invalidateHitboxes();
|
||||
}
|
||||
|
||||
setScaleY(newScale: float): void {
|
||||
if (newScale < 0) {
|
||||
newScale = 0;
|
||||
}
|
||||
if (newScale === Math.abs(this._scaleY)) {
|
||||
return;
|
||||
}
|
||||
this._scaleY = newScale * (this._flippedY ? -1 : 1);
|
||||
this._renderer.updateScale();
|
||||
this.invalidateHitboxes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the scale of the object (or the geometric mean of the X and Y scale in case they are different).
|
||||
*
|
||||
* @return the scale of the object (or the geometric mean of the X and Y scale in case they are different).
|
||||
*/
|
||||
getScale(): float {
|
||||
const scaleX = Math.abs(this._scaleX);
|
||||
const scaleY = Math.abs(this._scaleY);
|
||||
return scaleX === scaleY ? scaleX : Math.sqrt(scaleX * scaleY);
|
||||
}
|
||||
|
||||
getScaleY(): float {
|
||||
return Math.abs(this._scaleY);
|
||||
}
|
||||
|
||||
getScaleX(): float {
|
||||
return Math.abs(this._scaleX);
|
||||
}
|
||||
|
||||
isFlippedX(): boolean {
|
||||
return this._flippedX;
|
||||
}
|
||||
|
||||
isFlippedY(): boolean {
|
||||
return this._flippedY;
|
||||
}
|
||||
|
||||
flipX(enable: boolean) {
|
||||
if (enable !== this._flippedX) {
|
||||
this._scaleX *= -1;
|
||||
this._flippedX = enable;
|
||||
this.invalidateHitboxes();
|
||||
this._renderer.updateScale();
|
||||
}
|
||||
}
|
||||
|
||||
flipY(enable: boolean) {
|
||||
if (enable !== this._flippedY) {
|
||||
this._scaleY *= -1;
|
||||
this._flippedY = enable;
|
||||
this.invalidateHitboxes();
|
||||
this._renderer.updateScale();
|
||||
}
|
||||
}
|
||||
|
||||
setAnimationIndex(animationIndex: number): void {
|
||||
this.setAnimationIndexWithMixing(
|
||||
animationIndex,
|
||||
this._animationMixingDuration
|
||||
);
|
||||
}
|
||||
|
||||
setAnimationIndexWithMixing(
|
||||
animationIndex: number,
|
||||
mixingDuration: number
|
||||
): void {
|
||||
if (
|
||||
this._animations.length === 0 ||
|
||||
this._currentAnimationIndex === animationIndex ||
|
||||
!this.isAnimationIndex(animationIndex)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const previousAnimation = this._animations[this._currentAnimationIndex];
|
||||
const newAnimation = this._animations[animationIndex];
|
||||
this._currentAnimationIndex = animationIndex;
|
||||
|
||||
if (previousAnimation) {
|
||||
this._renderer.setMixing(
|
||||
previousAnimation.source,
|
||||
newAnimation.source,
|
||||
mixingDuration
|
||||
);
|
||||
}
|
||||
this._renderer.setAnimation(newAnimation.source, newAnimation.loop);
|
||||
this._isPausedFrameDirty = true;
|
||||
}
|
||||
|
||||
setAnimationName(animationName: string): void {
|
||||
this.setAnimationNameWithMixing(
|
||||
animationName,
|
||||
this._animationMixingDuration
|
||||
);
|
||||
}
|
||||
|
||||
setAnimationNameWithMixing(
|
||||
animationName: string,
|
||||
mixingDuration: number
|
||||
): void {
|
||||
this.setAnimationIndexWithMixing(
|
||||
this.getAnimationIndexFor(animationName),
|
||||
mixingDuration
|
||||
);
|
||||
}
|
||||
|
||||
getAnimationIndexFor(animationName: string): number {
|
||||
return this._animations.findIndex(
|
||||
(animation) => animation.name === animationName
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the duration in second for the smooth transition between 2 animations.
|
||||
*/
|
||||
getAnimationMixingDuration(): number {
|
||||
return this._animationMixingDuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the duration in second for the smooth transition between 2 animations.
|
||||
*/
|
||||
setAnimationMixingDuration(animationMixingDuration: number): void {
|
||||
this._animationMixingDuration = animationMixingDuration;
|
||||
}
|
||||
|
||||
getAnimationIndex(): number {
|
||||
return this._currentAnimationIndex;
|
||||
}
|
||||
|
||||
getAnimationName(): string {
|
||||
return this.isAnimationIndex(this._currentAnimationIndex)
|
||||
? this._animations[this._currentAnimationIndex].name
|
||||
: '';
|
||||
}
|
||||
|
||||
isAnimationIndex(animationIndex: number): boolean {
|
||||
return (
|
||||
Number.isInteger(animationIndex) &&
|
||||
animationIndex >= 0 &&
|
||||
animationIndex < this._animations.length
|
||||
);
|
||||
}
|
||||
|
||||
hasAnimationEnded(): boolean {
|
||||
return this._renderer.isAnimationComplete();
|
||||
}
|
||||
|
||||
isAnimationPaused() {
|
||||
return this._animationPaused;
|
||||
}
|
||||
|
||||
pauseAnimation() {
|
||||
this._animationPaused = true;
|
||||
}
|
||||
|
||||
resumeAnimation() {
|
||||
this._animationPaused = false;
|
||||
}
|
||||
|
||||
getAnimationSpeedScale() {
|
||||
return this._animationSpeedScale;
|
||||
}
|
||||
|
||||
setAnimationSpeedScale(ratio: float): void {
|
||||
this._animationSpeedScale = ratio;
|
||||
}
|
||||
|
||||
getAnimationElapsedTime(): number {
|
||||
if (this._animations.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
return this._renderer.getAnimationElapsedTime();
|
||||
}
|
||||
|
||||
setAnimationElapsedTime(time: number): void {
|
||||
if (this._animations.length === 0) {
|
||||
return;
|
||||
}
|
||||
this._renderer.setAnimationElapsedTime(time);
|
||||
this._isPausedFrameDirty = true;
|
||||
}
|
||||
|
||||
getAnimationDuration(): number {
|
||||
if (this._animations.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
return this._renderer.getAnimationDuration(
|
||||
this._animations[this._currentAnimationIndex].source
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
gdjs.registerObject('SpineObject::SpineObject', gdjs.SpineRuntimeObject);
|
||||
}
|
2
GDJS/.gitignore
vendored
2
GDJS/.gitignore
vendored
@@ -1 +1 @@
|
||||
/node_modules
|
||||
/node_modules
|
@@ -124,6 +124,8 @@ namespace gdjs {
|
||||
private _jsonManager: JsonManager;
|
||||
private _model3DManager: Model3DManager;
|
||||
private _bitmapFontManager: BitmapFontManager;
|
||||
private _spineAtlasManager: SpineAtlasManager | null = null;
|
||||
private _spineManager: SpineManager | null = null;
|
||||
|
||||
/**
|
||||
* Only used by events.
|
||||
@@ -172,6 +174,18 @@ namespace gdjs {
|
||||
);
|
||||
this._model3DManager = new gdjs.Model3DManager(this);
|
||||
|
||||
// add spine related managers only if spine extension is used
|
||||
if (gdjs.SpineAtlasManager && gdjs.SpineManager) {
|
||||
this._spineAtlasManager = new gdjs.SpineAtlasManager(
|
||||
this,
|
||||
this._imageManager
|
||||
);
|
||||
this._spineManager = new gdjs.SpineManager(
|
||||
this,
|
||||
this._spineAtlasManager
|
||||
);
|
||||
}
|
||||
|
||||
const resourceManagers: Array<ResourceManager> = [
|
||||
this._imageManager,
|
||||
this._soundManager,
|
||||
@@ -180,6 +194,11 @@ namespace gdjs {
|
||||
this._bitmapFontManager,
|
||||
this._model3DManager,
|
||||
];
|
||||
|
||||
if (this._spineAtlasManager)
|
||||
resourceManagers.push(this._spineAtlasManager);
|
||||
if (this._spineManager) resourceManagers.push(this._spineManager);
|
||||
|
||||
this._resourceManagersMap = new Map<ResourceKind, ResourceManager>();
|
||||
for (const resourceManager of resourceManagers) {
|
||||
for (const resourceKind of resourceManager.getResourceKinds()) {
|
||||
@@ -188,6 +207,13 @@ namespace gdjs {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns the runtime game instance.
|
||||
*/
|
||||
getRuntimeGame(): RuntimeGame {
|
||||
return this._runtimeGame;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the resources data of the game. Useful for hot-reloading, should
|
||||
* not be used otherwise.
|
||||
@@ -576,6 +602,24 @@ namespace gdjs {
|
||||
getModel3DManager(): gdjs.Model3DManager {
|
||||
return this._model3DManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Spine manager of the game, used to load and construct spine skeletons from game
|
||||
* resources.
|
||||
* @return The Spine manager for the game
|
||||
*/
|
||||
getSpineManager(): gdjs.SpineManager | null {
|
||||
return this._spineManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Spine Atlas manager of the game, used to load atlases from game
|
||||
* resources.
|
||||
* @return The Spine Atlas manager for the game
|
||||
*/
|
||||
getSpineAtlasManager(): gdjs.SpineAtlasManager | null {
|
||||
return this._spineAtlasManager;
|
||||
}
|
||||
}
|
||||
|
||||
type PromiseError<T> = { item: T; error: Error };
|
||||
|
@@ -29,8 +29,6 @@ namespace gdjs {
|
||||
*/
|
||||
setAnimationName(newAnimationName: string): void;
|
||||
|
||||
isCurrentAnimationName(name: string): boolean;
|
||||
|
||||
/**
|
||||
* Return true if animation has ended.
|
||||
* The animation had ended if:
|
||||
@@ -117,10 +115,6 @@ namespace gdjs {
|
||||
this.object.setAnimationName(newAnimationName);
|
||||
}
|
||||
|
||||
isCurrentAnimationName(name: string): boolean {
|
||||
return this.object.isCurrentAnimationName(name);
|
||||
}
|
||||
|
||||
hasAnimationEnded(): boolean {
|
||||
return this.object.hasAnimationEnded();
|
||||
}
|
||||
|
@@ -152,6 +152,7 @@ namespace gdjs {
|
||||
getGlobalResourceNames(data),
|
||||
data.layouts
|
||||
);
|
||||
|
||||
this._effectsManager = new gdjs.EffectsManager();
|
||||
this._maxFPS = this._data.properties.maxFPS;
|
||||
this._minFPS = this._data.properties.minFPS;
|
||||
@@ -306,6 +307,24 @@ namespace gdjs {
|
||||
return this._resourcesLoader.getModel3DManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Spine manager of the game, used to load and construct spine skeletons from game
|
||||
* resources.
|
||||
* @return The Spine manager for the game
|
||||
*/
|
||||
getSpineManager(): gdjs.SpineManager | null {
|
||||
return this._resourcesLoader.getSpineManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Spine Atlas manager of the game, used to load atlases from game
|
||||
* resources.
|
||||
* @return The Spine Atlas manager for the game
|
||||
*/
|
||||
getSpineAtlasManager(): gdjs.SpineAtlasManager | null {
|
||||
return this._resourcesLoader.getSpineAtlasManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the input manager of the game, storing mouse, keyboard
|
||||
* and touches states.
|
||||
@@ -1152,5 +1171,16 @@ namespace gdjs {
|
||||
? mapping[embeddedResourceName]
|
||||
: embeddedResourceName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array of resources that are embedded to passed one.
|
||||
* @param resourceName The name of resource to find embedded resources of.
|
||||
* @returns The array of related resources names.
|
||||
*/
|
||||
getEmbeddedResourcesNames(resourceName: string): string[] {
|
||||
return this._embeddedResourcesMappings.has(resourceName)
|
||||
? Object.keys(this._embeddedResourcesMappings.get(resourceName)!)
|
||||
: [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
4
GDJS/Runtime/types/global-pixi-spine.d.ts
vendored
Normal file
4
GDJS/Runtime/types/global-pixi-spine.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
import * as pixi_spine from 'pixi-spine';
|
||||
|
||||
export = pixi_spine;
|
||||
export as namespace pixi_spine;
|
4
GDJS/Runtime/types/project-data.d.ts
vendored
4
GDJS/Runtime/types/project-data.d.ts
vendored
@@ -280,4 +280,6 @@ declare type ResourceKind =
|
||||
| 'tilemap'
|
||||
| 'tileset'
|
||||
| 'bitmapFont'
|
||||
| 'model3D';
|
||||
| 'model3D'
|
||||
| 'atlas'
|
||||
| 'spine';
|
||||
|
174
GDJS/package-lock.json
generated
174
GDJS/package-lock.json
generated
@@ -19,6 +19,7 @@
|
||||
"lebab": "^3.1.0",
|
||||
"minimist": "^1.2.5",
|
||||
"patch-package": "^6.4.7",
|
||||
"pixi-spine": "4.0.4",
|
||||
"pixi.js": "7.3.0",
|
||||
"prettier": "2.1.2",
|
||||
"recursive-readdir": "^2.2.2",
|
||||
@@ -28,6 +29,91 @@
|
||||
"typescript": "4.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixi-spine/base": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@pixi-spine/base/-/base-4.0.3.tgz",
|
||||
"integrity": "sha512-0bunaWebaDswLFtYZ6whV+ZvgLQ7oANcvbPmIOoVpS/1pOY3Y/GAnWOFbgp3qt9Q/ntLYqNjGve6xq0IqpsTAA==",
|
||||
"dev": true,
|
||||
"peerDependencies": {
|
||||
"@pixi/core": "^7.0.0",
|
||||
"@pixi/display": "^7.0.0",
|
||||
"@pixi/graphics": "^7.0.0",
|
||||
"@pixi/mesh": "^7.0.0",
|
||||
"@pixi/mesh-extras": "^7.0.0",
|
||||
"@pixi/sprite": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixi-spine/loader-base": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@pixi-spine/loader-base/-/loader-base-4.0.4.tgz",
|
||||
"integrity": "sha512-Grgu+PxiUpgYWpuMRr3h5jrN3ZTnwyXfu3HuYdFb6mbJTTMub4xBPALeui+O+tw0k9RNRAr99pUzu9Rc9XTbAw==",
|
||||
"dev": true,
|
||||
"peerDependencies": {
|
||||
"@pixi-spine/base": "^4.0.0",
|
||||
"@pixi/assets": " ^7.0.0",
|
||||
"@pixi/core": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixi-spine/loader-uni": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@pixi-spine/loader-uni/-/loader-uni-4.0.3.tgz",
|
||||
"integrity": "sha512-tfhTJrnuog8ObKbbiSG1wV/nIUc3O98WfwS6lCmewaupoMIKF0ujg21MCqXUXJvljQJzU9tbURI+DWu4w9dnnA==",
|
||||
"dev": true,
|
||||
"peerDependencies": {
|
||||
"@pixi-spine/base": "^4.0.0",
|
||||
"@pixi-spine/loader-base": "^4.0.0",
|
||||
"@pixi-spine/runtime-3.7": "^4.0.0",
|
||||
"@pixi-spine/runtime-3.8": "^4.0.0",
|
||||
"@pixi-spine/runtime-4.1": "^4.0.0",
|
||||
"@pixi/assets": " ^7.0.0",
|
||||
"@pixi/core": "^7.0.0",
|
||||
"@pixi/display": "^7.0.0",
|
||||
"@pixi/graphics": "^7.0.0",
|
||||
"@pixi/mesh": "^7.0.0",
|
||||
"@pixi/mesh-extras": "^7.0.0",
|
||||
"@pixi/sprite": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixi-spine/runtime-3.7": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@pixi-spine/runtime-3.7/-/runtime-3.7-4.0.3.tgz",
|
||||
"integrity": "sha512-zuopKtSqjRc37wjW5xJ64j9DbiBB7rkPMFeldeWBPCbfZiCcFcwSZwZnrcgC+f4HIGo0NeviAvJGM8Hcf3AyeA==",
|
||||
"dev": true,
|
||||
"peerDependencies": {
|
||||
"@pixi-spine/base": "^4.0.0",
|
||||
"@pixi/core": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixi-spine/runtime-3.8": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@pixi-spine/runtime-3.8/-/runtime-3.8-4.0.3.tgz",
|
||||
"integrity": "sha512-lIhb4jOTon+FVYLO9AIgcB6jf9hC+RLEn8PesaDRibDocQ1htVCkEIhCIU3Mc00fuqIby7lMBsINeS/Th0q3bw==",
|
||||
"dev": true,
|
||||
"peerDependencies": {
|
||||
"@pixi-spine/base": "^4.0.0",
|
||||
"@pixi/core": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixi-spine/runtime-4.0": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@pixi-spine/runtime-4.0/-/runtime-4.0-4.0.3.tgz",
|
||||
"integrity": "sha512-2Y8qhxRkg/yH/9VylGsRVAd5W+dXVPhHTjFk0RR9wEUzTCkdZ17pE+56s2nESi2X3sYNKkz8FowfaqIvXnVGxw==",
|
||||
"dev": true,
|
||||
"peerDependencies": {
|
||||
"@pixi-spine/base": "^4.0.0",
|
||||
"@pixi/core": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixi-spine/runtime-4.1": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@pixi-spine/runtime-4.1/-/runtime-4.1-4.0.3.tgz",
|
||||
"integrity": "sha512-jK433snCQMC4FUPiDgyIcxhiatvRNSxqgs0CgHjjQ0l8GlY6gPpkkdThQ6GsFNme1SUZ4uvnWwawXFIGjW1IpQ==",
|
||||
"dev": true,
|
||||
"peerDependencies": {
|
||||
"@pixi-spine/base": "^4.0.0",
|
||||
"@pixi/core": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixi/accessibility": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@pixi/accessibility/-/accessibility-7.3.0.tgz",
|
||||
@@ -1599,6 +1685,30 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/pixi-spine": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/pixi-spine/-/pixi-spine-4.0.4.tgz",
|
||||
"integrity": "sha512-XRq1yARVoi4av7RXnd9+P37SWI9+e4/f5yTScZPJGB+sY5VcRYN6BYkBQ+y8nUKI1aJIjlms9z+pGxqikm+eFQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@pixi-spine/base": "^4.0.3",
|
||||
"@pixi-spine/loader-base": "^4.0.4",
|
||||
"@pixi-spine/loader-uni": "^4.0.3",
|
||||
"@pixi-spine/runtime-3.7": "^4.0.3",
|
||||
"@pixi-spine/runtime-3.8": "^4.0.3",
|
||||
"@pixi-spine/runtime-4.0": "^4.0.3",
|
||||
"@pixi-spine/runtime-4.1": "^4.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@pixi/assets": "^7.0.0",
|
||||
"@pixi/core": "^7.0.0",
|
||||
"@pixi/display": "^7.0.0",
|
||||
"@pixi/graphics": "^7.0.0",
|
||||
"@pixi/mesh": "^7.0.0",
|
||||
"@pixi/mesh-extras": "^7.0.0",
|
||||
"@pixi/sprite": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pixi.js": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-7.3.0.tgz",
|
||||
@@ -1981,6 +2091,55 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@pixi-spine/base": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@pixi-spine/base/-/base-4.0.3.tgz",
|
||||
"integrity": "sha512-0bunaWebaDswLFtYZ6whV+ZvgLQ7oANcvbPmIOoVpS/1pOY3Y/GAnWOFbgp3qt9Q/ntLYqNjGve6xq0IqpsTAA==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"@pixi-spine/loader-base": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@pixi-spine/loader-base/-/loader-base-4.0.4.tgz",
|
||||
"integrity": "sha512-Grgu+PxiUpgYWpuMRr3h5jrN3ZTnwyXfu3HuYdFb6mbJTTMub4xBPALeui+O+tw0k9RNRAr99pUzu9Rc9XTbAw==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"@pixi-spine/loader-uni": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@pixi-spine/loader-uni/-/loader-uni-4.0.3.tgz",
|
||||
"integrity": "sha512-tfhTJrnuog8ObKbbiSG1wV/nIUc3O98WfwS6lCmewaupoMIKF0ujg21MCqXUXJvljQJzU9tbURI+DWu4w9dnnA==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"@pixi-spine/runtime-3.7": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@pixi-spine/runtime-3.7/-/runtime-3.7-4.0.3.tgz",
|
||||
"integrity": "sha512-zuopKtSqjRc37wjW5xJ64j9DbiBB7rkPMFeldeWBPCbfZiCcFcwSZwZnrcgC+f4HIGo0NeviAvJGM8Hcf3AyeA==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"@pixi-spine/runtime-3.8": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@pixi-spine/runtime-3.8/-/runtime-3.8-4.0.3.tgz",
|
||||
"integrity": "sha512-lIhb4jOTon+FVYLO9AIgcB6jf9hC+RLEn8PesaDRibDocQ1htVCkEIhCIU3Mc00fuqIby7lMBsINeS/Th0q3bw==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"@pixi-spine/runtime-4.0": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@pixi-spine/runtime-4.0/-/runtime-4.0-4.0.3.tgz",
|
||||
"integrity": "sha512-2Y8qhxRkg/yH/9VylGsRVAd5W+dXVPhHTjFk0RR9wEUzTCkdZ17pE+56s2nESi2X3sYNKkz8FowfaqIvXnVGxw==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"@pixi-spine/runtime-4.1": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@pixi-spine/runtime-4.1/-/runtime-4.1-4.0.3.tgz",
|
||||
"integrity": "sha512-jK433snCQMC4FUPiDgyIcxhiatvRNSxqgs0CgHjjQ0l8GlY6gPpkkdThQ6GsFNme1SUZ4uvnWwawXFIGjW1IpQ==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"@pixi/accessibility": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@pixi/accessibility/-/accessibility-7.3.0.tgz",
|
||||
@@ -3203,6 +3362,21 @@
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true
|
||||
},
|
||||
"pixi-spine": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/pixi-spine/-/pixi-spine-4.0.4.tgz",
|
||||
"integrity": "sha512-XRq1yARVoi4av7RXnd9+P37SWI9+e4/f5yTScZPJGB+sY5VcRYN6BYkBQ+y8nUKI1aJIjlms9z+pGxqikm+eFQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@pixi-spine/base": "^4.0.3",
|
||||
"@pixi-spine/loader-base": "^4.0.4",
|
||||
"@pixi-spine/loader-uni": "^4.0.3",
|
||||
"@pixi-spine/runtime-3.7": "^4.0.3",
|
||||
"@pixi-spine/runtime-3.8": "^4.0.3",
|
||||
"@pixi-spine/runtime-4.0": "^4.0.3",
|
||||
"@pixi-spine/runtime-4.1": "^4.0.3"
|
||||
}
|
||||
},
|
||||
"pixi.js": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-7.3.0.tgz",
|
||||
|
@@ -16,6 +16,7 @@
|
||||
"minimist": "^1.2.5",
|
||||
"patch-package": "^6.4.7",
|
||||
"pixi.js": "7.3.0",
|
||||
"pixi-spine": "4.0.4",
|
||||
"prettier": "2.1.2",
|
||||
"recursive-readdir": "^2.2.2",
|
||||
"shelljs": "^0.8.4",
|
||||
@@ -23,8 +24,19 @@
|
||||
"typedoc-plugin-reference-excluder": "^1.0.0",
|
||||
"typescript": "4.3.2"
|
||||
},
|
||||
"overrides": {
|
||||
"pixi-spine": {
|
||||
"@pixi/assets": "7.3.0",
|
||||
"@pixi/core": "7.3.0",
|
||||
"@pixi/display": "7.3.0",
|
||||
"@pixi/graphics": "7.3.0",
|
||||
"@pixi/mesh": "7.3.0",
|
||||
"@pixi/mesh-extras": "7.3.0",
|
||||
"@pixi/sprite": "7.3.0"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "patch-package",
|
||||
"postinstall": "patch-package && node scripts/install-spine.js",
|
||||
"check-types": "tsc",
|
||||
"build": "node scripts/build.js",
|
||||
"test": "cd tests && npm run test-benchmark",
|
||||
|
36
GDJS/scripts/install-spine.js
Normal file
36
GDJS/scripts/install-spine.js
Normal file
@@ -0,0 +1,36 @@
|
||||
const path = require('path');
|
||||
const shell = require('shelljs');
|
||||
|
||||
const readContent = (path, testErrorMessage) => {
|
||||
if (!shell.test('-f', path)) throw new Error(`${testErrorMessage} Should exist by ${path}.`);
|
||||
|
||||
const readingResult = shell.cat(path);
|
||||
|
||||
if (readingResult.stderr) throw new Error(readingResult.stderr);
|
||||
|
||||
return readingResult.toString();
|
||||
};
|
||||
|
||||
try {
|
||||
shell.echo(`Start pixi-spine.js copying...`);
|
||||
|
||||
const originalSpineDir = path.resolve('node_modules/pixi-spine');
|
||||
const originalSpinePackage = JSON.parse(readContent(path.join(originalSpineDir, 'package.json'), 'Cannot find pixi-spine package.json file.'));
|
||||
const originalSpineContent = readContent(path.join(originalSpineDir, originalSpinePackage.extensionConfig.bundle), 'Cannot find pixi-spine.js.');
|
||||
|
||||
const varSpineExport = '\nvar pixi_spine = this.PIXI.spine;\n';
|
||||
const runtimeSpineDir = '../Extensions/Spine/pixi-spine';
|
||||
|
||||
if (!shell.test('-d', runtimeSpineDir)) {
|
||||
shell.echo(`Creating directory for pixi-spine.js ${runtimeSpineDir}.`);
|
||||
shell.mkdir(runtimeSpineDir);
|
||||
}
|
||||
|
||||
const runtimeSpinePath = path.join(runtimeSpineDir, 'pixi-spine.js');
|
||||
new shell.ShellString(originalSpineContent + varSpineExport).to(runtimeSpinePath);
|
||||
|
||||
shell.echo(`✅ Properly copied pixi-spine.js from node_modules to ${runtimeSpinePath}.`);
|
||||
} catch(error) {
|
||||
shell.echo(`❌ Unable to copy pixi-spine.js from node_modules. Error is: ${error}`)
|
||||
shell.exit(1);
|
||||
}
|
@@ -23,6 +23,7 @@ const transformExcludedExtensions = ['.min.js', '.d.ts'];
|
||||
const untransformedPaths = [
|
||||
// GDJS prebuilt files:
|
||||
'GDJS/Runtime/pixi-renderers/pixi.js',
|
||||
'GDJS/Runtime/pixi-renderers/pixi-spine.js',
|
||||
'GDJS/Runtime/pixi-renderers/three.js',
|
||||
'GDJS/Runtime/pixi-renderers/ThreeAddons.js',
|
||||
'GDJS/Runtime/pixi-renderers/draco/gltf/draco_wasm_wrapper.js',
|
||||
|
101
GDJS/tests/games/capabilities/assets/spineboy.atlas
Normal file
101
GDJS/tests/games/capabilities/assets/spineboy.atlas
Normal file
@@ -0,0 +1,101 @@
|
||||
spineboy.png
|
||||
size: 1024, 256
|
||||
filter: Linear, Linear
|
||||
scale: 0.5
|
||||
crosshair
|
||||
bounds: 813, 160, 45, 45
|
||||
eye-indifferent
|
||||
bounds: 569, 2, 47, 45
|
||||
eye-surprised
|
||||
bounds: 643, 7, 47, 45
|
||||
rotate: 90
|
||||
front-bracer
|
||||
bounds: 811, 51, 29, 40
|
||||
front-fist-closed
|
||||
bounds: 807, 93, 38, 41
|
||||
front-fist-open
|
||||
bounds: 815, 210, 43, 44
|
||||
front-foot
|
||||
bounds: 706, 64, 63, 35
|
||||
rotate: 90
|
||||
front-shin
|
||||
bounds: 80, 11, 41, 92
|
||||
front-thigh
|
||||
bounds: 754, 12, 23, 56
|
||||
front-upper-arm
|
||||
bounds: 618, 5, 23, 49
|
||||
goggles
|
||||
bounds: 214, 20, 131, 83
|
||||
gun
|
||||
bounds: 347, 14, 105, 102
|
||||
rotate: 90
|
||||
head
|
||||
bounds: 80, 105, 136, 149
|
||||
hoverboard-board
|
||||
bounds: 2, 8, 246, 76
|
||||
rotate: 90
|
||||
hoverboard-thruster
|
||||
bounds: 478, 2, 30, 32
|
||||
hoverglow-small
|
||||
bounds: 218, 117, 137, 38
|
||||
rotate: 90
|
||||
mouth-grind
|
||||
bounds: 775, 80, 47, 30
|
||||
rotate: 90
|
||||
mouth-oooo
|
||||
bounds: 779, 31, 47, 30
|
||||
rotate: 90
|
||||
mouth-smile
|
||||
bounds: 783, 207, 47, 30
|
||||
rotate: 90
|
||||
muzzle-glow
|
||||
bounds: 779, 4, 25, 25
|
||||
muzzle-ring
|
||||
bounds: 451, 14, 25, 105
|
||||
muzzle01
|
||||
bounds: 664, 60, 67, 40
|
||||
rotate: 90
|
||||
muzzle02
|
||||
bounds: 580, 56, 68, 42
|
||||
rotate: 90
|
||||
muzzle03
|
||||
bounds: 478, 36, 83, 53
|
||||
rotate: 90
|
||||
muzzle04
|
||||
bounds: 533, 49, 75, 45
|
||||
rotate: 90
|
||||
muzzle05
|
||||
bounds: 624, 56, 68, 38
|
||||
rotate: 90
|
||||
neck
|
||||
bounds: 806, 8, 18, 21
|
||||
portal-bg
|
||||
bounds: 258, 121, 133, 133
|
||||
portal-flare1
|
||||
bounds: 690, 2, 56, 30
|
||||
rotate: 90
|
||||
portal-flare2
|
||||
bounds: 510, 3, 57, 31
|
||||
portal-flare3
|
||||
bounds: 722, 4, 58, 30
|
||||
rotate: 90
|
||||
portal-shade
|
||||
bounds: 393, 121, 133, 133
|
||||
portal-streaks1
|
||||
bounds: 528, 126, 126, 128
|
||||
portal-streaks2
|
||||
bounds: 656, 129, 125, 125
|
||||
rear-bracer
|
||||
bounds: 826, 13, 28, 36
|
||||
rear-foot
|
||||
bounds: 743, 70, 57, 30
|
||||
rotate: 90
|
||||
rear-shin
|
||||
bounds: 174, 14, 38, 89
|
||||
rear-thigh
|
||||
bounds: 783, 158, 28, 47
|
||||
rear-upper-arm
|
||||
bounds: 783, 136, 20, 44
|
||||
rotate: 90
|
||||
torso
|
||||
bounds: 123, 13, 49, 90
|
3672
GDJS/tests/games/capabilities/assets/spineboy.json
Normal file
3672
GDJS/tests/games/capabilities/assets/spineboy.json
Normal file
File diff suppressed because it is too large
Load Diff
BIN
GDJS/tests/games/capabilities/assets/spineboy.png
Normal file
BIN
GDJS/tests/games/capabilities/assets/spineboy.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 239 KiB |
File diff suppressed because it is too large
Load Diff
@@ -41,14 +41,15 @@ module.exports = function (config) {
|
||||
'./newIDE/app/resources/GDJS/Runtime/AsyncTasksManager.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/libs/rbush.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/pixi-renderers/pixi.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/pixi-renderers/pixi-spine.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/pixi-renderers/three.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/pixi-renderers/*.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/howler-sound-manager/howler.min.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/howler-sound-manager/howler-sound-manager.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/fontfaceobserver-font-manager/fontfaceobserver.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/fontfaceobserver-font-manager/fontfaceobserver-font-manager.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/jsonmanager.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/Model3DManager.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/jsonmanager.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/ResourceLoader.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/ResourceCache.js',
|
||||
'./newIDE/app/resources/GDJS/Runtime/timemanager.js',
|
||||
|
@@ -1121,6 +1121,11 @@ interface JsonResource {
|
||||
};
|
||||
JsonResource implements Resource;
|
||||
|
||||
interface SpineResource {
|
||||
void SpineResource();
|
||||
};
|
||||
SpineResource implements JsonResource;
|
||||
|
||||
interface TilemapResource {
|
||||
void TilemapResource();
|
||||
};
|
||||
@@ -1136,6 +1141,11 @@ interface Model3DResource {
|
||||
};
|
||||
Model3DResource implements Resource;
|
||||
|
||||
interface AtlasResource {
|
||||
void AtlasResource();
|
||||
};
|
||||
AtlasResource implements Resource;
|
||||
|
||||
interface InitialInstance {
|
||||
void InitialInstance();
|
||||
|
||||
@@ -3210,6 +3220,35 @@ interface Model3DObjectConfiguration {
|
||||
};
|
||||
Model3DObjectConfiguration implements ObjectConfiguration;
|
||||
|
||||
|
||||
interface SpineAnimation {
|
||||
void SpineAnimation();
|
||||
|
||||
void SetName([Const] DOMString name);
|
||||
[Const, Ref] DOMString GetName();
|
||||
|
||||
void SetSource([Const] DOMString name);
|
||||
[Const, Ref] DOMString GetSource();
|
||||
|
||||
void SetShouldLoop(boolean shouldLoop);
|
||||
boolean ShouldLoop();
|
||||
};
|
||||
|
||||
interface SpineObjectConfiguration {
|
||||
void SpineObjectConfiguration();
|
||||
|
||||
void AddAnimation([Const, Ref] SpineAnimation animation);
|
||||
[Ref] SpineAnimation GetAnimation(unsigned long index);
|
||||
boolean HasAnimationNamed([Const] DOMString name);
|
||||
unsigned long GetAnimationsCount();
|
||||
void RemoveAnimation(unsigned long index);
|
||||
void RemoveAllAnimations();
|
||||
boolean HasNoAnimations();
|
||||
void SwapAnimations(unsigned long first, unsigned long second);
|
||||
void MoveAnimation(unsigned long oldIndex, unsigned long newIndex);
|
||||
};
|
||||
SpineObjectConfiguration implements ObjectConfiguration;
|
||||
|
||||
interface Vector2f {
|
||||
void Vector2f();
|
||||
|
||||
|
@@ -195,6 +195,10 @@ void ObjectJsImplementation::ExposeResources(gd::ArbitraryResourceWorker& worker
|
||||
worker.ExposeBitmapFont(newPropertyValue);
|
||||
} else if (resourceType == "model3D") {
|
||||
worker.ExposeModel3D(newPropertyValue);
|
||||
} else if (resourceType == "atlas") {
|
||||
worker.ExposeAtlas(newPropertyValue);
|
||||
} else if (resourceType == "spine") {
|
||||
worker.ExposeSpine(newPropertyValue);
|
||||
}
|
||||
|
||||
if (newPropertyValue != oldPropertyValue) {
|
||||
|
@@ -110,6 +110,8 @@
|
||||
#include "../../Extensions/TextEntryObject/TextEntryObject.h"
|
||||
#include "../../Extensions/TextObject/TextObject.h"
|
||||
#include "../../Extensions/TiledSpriteObject/TiledSpriteObject.h"
|
||||
#include "../../Extensions/3D/Model3DObjectConfiguration.h"
|
||||
#include "../../Extensions/Spine/SpineObjectConfiguration.h"
|
||||
#include "BehaviorJsImplementation.h"
|
||||
#include "BehaviorSharedDataJsImplementation.h"
|
||||
#include "ObjectJsImplementation.h"
|
||||
|
@@ -150,6 +150,9 @@ var adaptNamingConventions = function (gd) {
|
||||
gd.asModel3DConfiguration = function (evt) {
|
||||
return gd.castObject(evt, gd.Model3DObjectConfiguration);
|
||||
};
|
||||
gd.asSpineConfiguration = function (evt) {
|
||||
return gd.castObject(evt, gd.SpineObjectConfiguration);
|
||||
};
|
||||
|
||||
gd.asImageResource = function (evt) {
|
||||
return gd.castObject(evt, gd.ImageResource);
|
||||
|
@@ -119,3 +119,4 @@ target_link_libraries(GD PathfindingBehavior)
|
||||
target_link_libraries(GD PhysicsBehavior)
|
||||
target_link_libraries(GD ParticleSystem)
|
||||
target_link_libraries(GD Scene3D)
|
||||
target_link_libraries(GD SpineObject)
|
||||
|
@@ -13,7 +13,7 @@ module.exports = function (grunt) {
|
||||
let cmakeBinary = 'emcmake cmake';
|
||||
let cmakeGeneratorArgs = [];
|
||||
let makeBinary = 'emmake make';
|
||||
let makeArgs = ['-j 4'];
|
||||
let makeArgs = ['-j 8'];
|
||||
|
||||
// Use more specific paths on Windows
|
||||
if (isWin) {
|
||||
|
@@ -1,5 +1,4 @@
|
||||
const shell = require('shelljs');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const sourcePath = path.join(__dirname, '../../Binaries/embuild/GDevelop.js');
|
||||
|
@@ -199,6 +199,7 @@ type ParticleEmitterObject_RendererType = 0 | 1 | 2`
|
||||
` asObjectJsImplementation(gdObjectConfiguration): gdObjectJsImplementation;`,
|
||||
` asCustomObjectConfiguration(gdObjectConfiguration): gdCustomObjectConfiguration;`,
|
||||
` asModel3DConfiguration(gdObjectConfiguration): gdModel3DObjectConfiguration;`,
|
||||
` asSpineConfiguration(gdObjectConfiguration): gdSpineObjectConfiguration;`,
|
||||
'',
|
||||
` asImageResource(gdResource): gdImageResource;`,
|
||||
'',
|
||||
@@ -344,13 +345,13 @@ type ParticleEmitterObject_RendererType = 0 | 1 | 2`
|
||||
shell.sed(
|
||||
'-i',
|
||||
/setKind\(kind: string\): void/,
|
||||
"setKind(kind: 'image' | 'audio' | 'font' | 'video' | 'json' | 'tilemap' | 'tileset' | 'model3D'): void",
|
||||
"setKind(kind: 'image' | 'audio' | 'font' | 'video' | 'json' | 'tilemap' | 'tileset' | 'model3D' | 'atlas' | 'spine'): void",
|
||||
'types/gdresource.js'
|
||||
);
|
||||
shell.sed(
|
||||
'-i',
|
||||
/getKind\(\): string/,
|
||||
"getKind(): 'image' | 'audio' | 'font' | 'video' | 'json' | 'tilemap' | 'tileset' | 'model3D'",
|
||||
"getKind(): 'image' | 'audio' | 'font' | 'video' | 'json' | 'tilemap' | 'tileset' | 'model3D' | 'atlas' | 'spine'",
|
||||
'types/gdresource.js'
|
||||
);
|
||||
|
||||
|
31
GDevelop.js/types.d.ts
vendored
31
GDevelop.js/types.d.ts
vendored
@@ -978,6 +978,10 @@ export class JsonResource extends EmscriptenObject {
|
||||
constructor(): void;
|
||||
}
|
||||
|
||||
export class SpineResource extends EmscriptenObject {
|
||||
constructor(): void;
|
||||
}
|
||||
|
||||
export class TilemapResource extends EmscriptenObject {
|
||||
constructor(): void;
|
||||
}
|
||||
@@ -986,6 +990,10 @@ export class TilesetResource extends EmscriptenObject {
|
||||
constructor(): void;
|
||||
}
|
||||
|
||||
export class AtlasResource extends EmscriptenObject {
|
||||
constructor(): void;
|
||||
}
|
||||
|
||||
export class InitialInstance extends EmscriptenObject {
|
||||
constructor(): void;
|
||||
setObjectName(name: string): void;
|
||||
@@ -2431,6 +2439,29 @@ export class SpriteObject extends EmscriptenObject {
|
||||
setAdaptCollisionMaskAutomatically(adaptCollisionMaskAutomatically: boolean): void;
|
||||
}
|
||||
|
||||
export class SpineAnimation extends EmscriptenObject {
|
||||
constructor(): void;
|
||||
setName(name: string): void;
|
||||
getName(): string;
|
||||
setSource(name: string): void;
|
||||
getSource(): string;
|
||||
setShouldLoop(shouldLoop: boolean): void;
|
||||
shouldLoop(): boolean;
|
||||
}
|
||||
|
||||
export class SpineObjectConfiguration extends EmscriptenObject {
|
||||
constructor(): void;
|
||||
addAnimation(animation: SpineAnimation): void;
|
||||
getAnimation(index: number): SpineAnimation;
|
||||
hasAnimationNamed(name: string): boolean;
|
||||
getAnimationsCount(): number;
|
||||
removeAnimation(index: number): void;
|
||||
removeAllAnimations(): void;
|
||||
hasNoAnimations(): boolean;
|
||||
swapAnimations(first: number, second: number): void;
|
||||
moveAnimation(oldIndex: number, newIndex: number): void;
|
||||
}
|
||||
|
||||
export class TextObject extends EmscriptenObject {
|
||||
constructor(): void;
|
||||
setString(string: string): void;
|
||||
|
6
GDevelop.js/types/gdatlasresource.js
Normal file
6
GDevelop.js/types/gdatlasresource.js
Normal file
@@ -0,0 +1,6 @@
|
||||
// Automatically generated by GDevelop.js/scripts/generate-types.js
|
||||
declare class gdAtlasResource extends gdResource {
|
||||
constructor(): void;
|
||||
delete(): void;
|
||||
ptr: number;
|
||||
};
|
@@ -4,8 +4,8 @@ declare class gdResource {
|
||||
clone(): gdResource;
|
||||
setName(name: string): void;
|
||||
getName(): string;
|
||||
setKind(kind: 'image' | 'audio' | 'font' | 'video' | 'json' | 'tilemap' | 'tileset' | 'model3D'): void;
|
||||
getKind(): 'image' | 'audio' | 'font' | 'video' | 'json' | 'tilemap' | 'tileset' | 'model3D';
|
||||
setKind(kind: 'image' | 'audio' | 'font' | 'video' | 'json' | 'tilemap' | 'tileset' | 'model3D' | 'atlas' | 'spine'): void;
|
||||
getKind(): 'image' | 'audio' | 'font' | 'video' | 'json' | 'tilemap' | 'tileset' | 'model3D' | 'atlas' | 'spine';
|
||||
isUserAdded(): boolean;
|
||||
setUserAdded(yes: boolean): void;
|
||||
useFile(): boolean;
|
||||
|
12
GDevelop.js/types/gdspineanimation.js
Normal file
12
GDevelop.js/types/gdspineanimation.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// Automatically generated by GDevelop.js/scripts/generate-types.js
|
||||
declare class gdSpineAnimation {
|
||||
constructor(): void;
|
||||
setName(name: string): void;
|
||||
getName(): string;
|
||||
setSource(name: string): void;
|
||||
getSource(): string;
|
||||
setShouldLoop(shouldLoop: boolean): void;
|
||||
shouldLoop(): boolean;
|
||||
delete(): void;
|
||||
ptr: number;
|
||||
};
|
15
GDevelop.js/types/gdspineobjectconfiguration.js
Normal file
15
GDevelop.js/types/gdspineobjectconfiguration.js
Normal file
@@ -0,0 +1,15 @@
|
||||
// Automatically generated by GDevelop.js/scripts/generate-types.js
|
||||
declare class gdSpineObjectConfiguration extends gdObjectConfiguration {
|
||||
constructor(): void;
|
||||
addAnimation(animation: gdSpineAnimation): void;
|
||||
getAnimation(index: number): gdSpineAnimation;
|
||||
hasAnimationNamed(name: string): boolean;
|
||||
getAnimationsCount(): number;
|
||||
removeAnimation(index: number): void;
|
||||
removeAllAnimations(): void;
|
||||
hasNoAnimations(): boolean;
|
||||
swapAnimations(first: number, second: number): void;
|
||||
moveAnimation(oldIndex: number, newIndex: number): void;
|
||||
delete(): void;
|
||||
ptr: number;
|
||||
};
|
6
GDevelop.js/types/gdspineresource.js
Normal file
6
GDevelop.js/types/gdspineresource.js
Normal file
@@ -0,0 +1,6 @@
|
||||
// Automatically generated by GDevelop.js/scripts/generate-types.js
|
||||
declare class gdSpineResource extends gdJsonResource {
|
||||
constructor(): void;
|
||||
delete(): void;
|
||||
ptr: number;
|
||||
};
|
@@ -36,6 +36,7 @@ declare class libGDevelop {
|
||||
asObjectJsImplementation(gdObjectConfiguration): gdObjectJsImplementation;
|
||||
asCustomObjectConfiguration(gdObjectConfiguration): gdCustomObjectConfiguration;
|
||||
asModel3DConfiguration(gdObjectConfiguration): gdModel3DObjectConfiguration;
|
||||
asSpineConfiguration(gdObjectConfiguration): gdSpineObjectConfiguration;
|
||||
|
||||
asImageResource(gdResource): gdImageResource;
|
||||
|
||||
@@ -109,9 +110,11 @@ declare class libGDevelop {
|
||||
BitmapFontResource: Class<gdBitmapFontResource>;
|
||||
VideoResource: Class<gdVideoResource>;
|
||||
JsonResource: Class<gdJsonResource>;
|
||||
SpineResource: Class<gdSpineResource>;
|
||||
TilemapResource: Class<gdTilemapResource>;
|
||||
TilesetResource: Class<gdTilesetResource>;
|
||||
Model3DResource: Class<gdModel3DResource>;
|
||||
AtlasResource: Class<gdAtlasResource>;
|
||||
InitialInstance: Class<gdInitialInstance>;
|
||||
InitialInstancesContainer: Class<gdInitialInstancesContainer>;
|
||||
HighestZOrderFinder: Class<gdHighestZOrderFinder>;
|
||||
@@ -231,6 +234,8 @@ declare class libGDevelop {
|
||||
SpriteObject: Class<gdSpriteObject>;
|
||||
Model3DAnimation: Class<gdModel3DAnimation>;
|
||||
Model3DObjectConfiguration: Class<gdModel3DObjectConfiguration>;
|
||||
SpineAnimation: Class<gdSpineAnimation>;
|
||||
SpineObjectConfiguration: Class<gdSpineObjectConfiguration>;
|
||||
Vector2f: Class<gdVector2f>;
|
||||
VectorVector2f: Class<gdVectorVector2f>;
|
||||
TextObject: Class<gdTextObject>;
|
||||
|
30295
newIDE/app/package-lock.json
generated
30295
newIDE/app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -61,6 +61,7 @@
|
||||
"lodash": "4.17.4",
|
||||
"node-require-function": "^1.2.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"pixi-spine": "4.0.4",
|
||||
"pixi.js-legacy": "7.3.0",
|
||||
"posthog-js": "^1.57.2",
|
||||
"prop-types": "^15.5.10",
|
||||
@@ -95,6 +96,17 @@
|
||||
"url-search-params": "^1.0.2",
|
||||
"xxhashjs": "^0.2.2"
|
||||
},
|
||||
"overrides": {
|
||||
"pixi-spine": {
|
||||
"@pixi/assets": "7.3.0",
|
||||
"@pixi/core": "7.3.0",
|
||||
"@pixi/display": "7.3.0",
|
||||
"@pixi/graphics": "7.3.0",
|
||||
"@pixi/mesh": "7.3.0",
|
||||
"@pixi/mesh-extras": "7.3.0",
|
||||
"@pixi/sprite": "7.3.0"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "patch-package && cd ../../GDJS && npm install && cd ../newIDE/app && npm run import-resources && npm run make-version-metadata",
|
||||
"import-resources": "npm run import-zipped-external-editors && npm run build-theme-resources && cd scripts && node import-libGD.js && node import-GDJS-Runtime.js && node import-monaco-editor.js && node import-zipped-external-libs.js",
|
||||
|
9
newIDE/app/public/JsPlatform/Extensions/spine.svg
Normal file
9
newIDE/app/public/JsPlatform/Extensions/spine.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<svg width="32" height="32" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.17 3L7 9L0 8.5V11.5L9.5 13.96L9.52223 13.9426C9.67532 13.9801 9.83534 14 10 14C11.1046 14 12 13.1046 12 12C12 10.8 11.46 5 10.96 1.5L10.9586 1.50037C10.8629 0.929434 10.6059 0.413168 10.2361 0H5.76389C5.28885 0.530756 5 1.23165 5 2C5 2.35057 5.05949 2.68727 5.17 3Z" fill="#3DB3E4"/>
|
||||
<circle cx="8" cy="2" r="1" fill="#404D9B"/>
|
||||
<path d="M7.22677 4.76781C7.10153 4.08024 7.64253 3.40581 8.43514 3.26143C9.22775 3.11705 9.97181 3.55739 10.0971 4.24496C10.3238 5.48992 10.5884 10.5868 9.79581 10.7312C9.00321 10.8756 7.45355 6.01277 7.22677 4.76781Z" fill="#404D9B"/>
|
||||
<path d="M6.12523 6.13193L7 9.00002L0 8.50002V11.5L9.5 13.96L9.52223 13.9426C9.67532 13.9801 9.83534 14 10 14C11.1046 14 12 13.1046 12 12L6.12523 6.13193Z" fill="#2399CA"/>
|
||||
<path d="M10.1383 10.1467C10.056 10.4862 9.94166 10.7026 9.78711 10.7308C9.39621 10.802 8.82118 9.65528 8.31151 8.32533C8.31151 8.32533 10.1352 10.1471 10.1383 10.1467Z" fill="#273382"/>
|
||||
<circle cx="10" cy="12" r="1" fill="#273382"/>
|
||||
<path d="M7.59932 10.3794C8.39844 10.5318 8.96462 11.0834 8.86392 11.6116C8.76322 12.1397 8.03378 12.4443 7.23466 12.292C5.78773 12.0161 0.0816328 10.4844 0.182332 9.95628C0.283031 9.42813 6.15239 10.1035 7.59932 10.3794Z" fill="#273382"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
@@ -123,6 +123,10 @@ export const installResource = (
|
||||
newResource = new gd.JsonResource();
|
||||
} else if (serializedResource.kind === 'model3D') {
|
||||
newResource = new gd.Model3DResource();
|
||||
} else if (serializedResource.kind === 'atlas') {
|
||||
newResource = new gd.AtlasResource();
|
||||
} else if (serializedResource.kind === 'spine') {
|
||||
newResource = new gd.SpineResource();
|
||||
} else {
|
||||
throw new Error(
|
||||
`Resource of kind "${serializedResource.kind}" is not supported.`
|
||||
|
@@ -178,6 +178,7 @@ export const ResourceCard = ({ resource, onChoose, size }: Props) => {
|
||||
case 'json':
|
||||
case 'tilemap':
|
||||
case 'tileset':
|
||||
case 'spine':
|
||||
return (
|
||||
<GenericCard onChoose={onChoose} resource={resource} size={size}>
|
||||
<Line justifyContent="center">
|
||||
@@ -221,6 +222,17 @@ export const ResourceCard = ({ resource, onChoose, size }: Props) => {
|
||||
</Line>
|
||||
</GenericCard>
|
||||
);
|
||||
case 'atlas':
|
||||
return (
|
||||
<GenericCard onChoose={onChoose} resource={resource} size={size}>
|
||||
<Line justifyContent="center">
|
||||
<FontDownload style={styles.icon} />
|
||||
</Line>
|
||||
<Line justifyContent="center">
|
||||
<RaisedButton onClick={onChoose} label={<Trans>Choose</Trans>} />
|
||||
</Line>
|
||||
</GenericCard>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
@@ -0,0 +1,49 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import * as React from 'react';
|
||||
import ResourceSelector, {
|
||||
type ResourceSelectorInterface,
|
||||
} from '../../ResourcesList/ResourceSelector';
|
||||
import ResourcesLoader from '../../ResourcesLoader';
|
||||
import {
|
||||
type ParameterFieldProps,
|
||||
type ParameterFieldInterface,
|
||||
type FieldFocusFunction,
|
||||
} from './ParameterFieldCommons';
|
||||
|
||||
export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
|
||||
function AtlasResourceField(props: ParameterFieldProps, ref) {
|
||||
const field = React.useRef<?ResourceSelectorInterface>(null);
|
||||
const focus: FieldFocusFunction = options => {
|
||||
if (field.current) field.current.focus(options);
|
||||
};
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
focus,
|
||||
}));
|
||||
|
||||
if (!props.resourceManagementProps || !props.project) {
|
||||
console.error(
|
||||
'Missing project or resourceManagementProps for AtlasResourceField'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ResourceSelector
|
||||
margin={props.isInline ? 'none' : 'dense'}
|
||||
project={props.project}
|
||||
resourceManagementProps={props.resourceManagementProps}
|
||||
resourcesLoader={ResourcesLoader}
|
||||
resourceKind="atlas"
|
||||
fullWidth
|
||||
initialResourceName={props.value}
|
||||
onChange={props.onChange}
|
||||
floatingLabelText={<Trans>Choose the atlas file (.atlas) to use</Trans>}
|
||||
onRequestClose={props.onRequestClose}
|
||||
onApply={props.onApply}
|
||||
ref={field}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
@@ -87,6 +87,19 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort();
|
||||
} else if (object.getType() === 'SpineObject::SpineObject') {
|
||||
const spineConfiguration = gd.asSpineConfiguration(
|
||||
object.getConfiguration()
|
||||
);
|
||||
|
||||
return mapFor(0, spineConfiguration.getAnimationsCount(), index => {
|
||||
const animationName = spineConfiguration
|
||||
.getAnimation(index)
|
||||
.getName();
|
||||
return animationName.length > 0 ? animationName : null;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort();
|
||||
}
|
||||
|
||||
return [];
|
||||
|
@@ -0,0 +1,48 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import ResourceSelector, {
|
||||
type ResourceSelectorInterface,
|
||||
} from '../../ResourcesList/ResourceSelector';
|
||||
import ResourcesLoader from '../../ResourcesLoader';
|
||||
import {
|
||||
type ParameterFieldProps,
|
||||
type ParameterFieldInterface,
|
||||
type FieldFocusFunction,
|
||||
} from './ParameterFieldCommons';
|
||||
|
||||
export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
|
||||
function SpineResourceField(props, ref) {
|
||||
const field = React.useRef<?ResourceSelectorInterface>(null);
|
||||
const focus: FieldFocusFunction = options => {
|
||||
if (field.current) field.current.focus(options);
|
||||
};
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
focus,
|
||||
}));
|
||||
|
||||
if (!props.resourceManagementProps || !props.project) {
|
||||
console.error(
|
||||
'Missing project or resourceManagementProps for SpineResourceField'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ResourceSelector
|
||||
margin={props.isInline ? 'none' : 'dense'}
|
||||
project={props.project}
|
||||
resourceManagementProps={props.resourceManagementProps}
|
||||
resourcesLoader={ResourcesLoader}
|
||||
resourceKind="spine"
|
||||
fullWidth
|
||||
initialResourceName={props.value}
|
||||
onChange={props.onChange}
|
||||
floatingLabelText={<Trans>Choose the spine json file to use</Trans>}
|
||||
onRequestClose={props.onRequestClose}
|
||||
onApply={props.onApply}
|
||||
ref={field}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
@@ -41,6 +41,7 @@ import ImageResourceField from './ParameterFields/ImageResourceField';
|
||||
import AudioResourceField from './ParameterFields/AudioResourceField';
|
||||
import VideoResourceField from './ParameterFields/VideoResourceField';
|
||||
import JsonResourceField from './ParameterFields/JsonResourceField';
|
||||
import SpineResourceField from './ParameterFields/SpineResourceField';
|
||||
import BitmapFontResourceField from './ParameterFields/BitmapFontResourceField';
|
||||
import FontResourceField from './ParameterFields/FontResourceField';
|
||||
import ColorExpressionField from './ParameterFields/ColorExpressionField';
|
||||
@@ -64,6 +65,7 @@ import IdentifierField from './ParameterFields/IdentifierField';
|
||||
import TilemapResourceField from './ParameterFields/TilemapResourceField';
|
||||
import TilesetResourceField from './ParameterFields/TilesetResourceField';
|
||||
import Model3DResourceField from './ParameterFields/Model3DResourceField';
|
||||
import AtlasResourceField from './ParameterFields/AtlasResourceField';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
@@ -94,6 +96,8 @@ const components = {
|
||||
bitmapFontResource: BitmapFontResourceField,
|
||||
fontResource: FontResourceField,
|
||||
model3DResource: Model3DResourceField,
|
||||
atlasResource: AtlasResourceField,
|
||||
spineResource: SpineResourceField,
|
||||
color: ColorExpressionField,
|
||||
police: DefaultField, //TODO
|
||||
forceMultiplier: ForceMultiplierField,
|
||||
@@ -151,7 +155,8 @@ const userFriendlyTypeName: { [string]: MessageDescriptor } = {
|
||||
fontResource: t`Font resource`,
|
||||
jsonResource: t`JSON resource`,
|
||||
tilemapResource: t`Tile map resource`,
|
||||
model3DResource: t`3D model resource`,
|
||||
atlasResource: t`Atlas resource`,
|
||||
spineResource: t`Spine json resource`,
|
||||
color: t`Color`,
|
||||
forceMultiplier: t`Instant or permanent force`,
|
||||
sceneName: t`Scene name`,
|
||||
|
@@ -171,6 +171,12 @@ const jsExtensions = [
|
||||
extensionModule: require('GDJS-for-web-app-only/Runtime/Extensions/3D/JsExtension.js'),
|
||||
objectsRenderingServiceModules: {},
|
||||
},
|
||||
{
|
||||
name: 'SpineObject',
|
||||
// $FlowExpectedError - this path is ignored for Flow.
|
||||
extensionModule: require('GDJS-for-web-app-only/Runtime/Extensions/Spine/JsExtension.js'),
|
||||
objectsRenderingServiceModules: {},
|
||||
},
|
||||
];
|
||||
|
||||
type MakeExtensionsLoaderArguments = {|
|
||||
|
@@ -8,20 +8,11 @@ import { ColumnStackLayout, ResponsiveLineStackLayout } from '../../UI/Layout';
|
||||
import Text from '../../UI/Text';
|
||||
import SemiControlledTextField from '../../UI/SemiControlledTextField';
|
||||
import useForceUpdate from '../../Utils/UseForceUpdate';
|
||||
import ResourceSelector from '../../ResourcesList/ResourceSelector';
|
||||
import Checkbox from '../../UI/Checkbox';
|
||||
import { Column, Line, Spacer } from '../../UI/Grid';
|
||||
import FormHelperText from '@material-ui/core/FormHelperText';
|
||||
import InputAdornment from '@material-ui/core/InputAdornment';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import { MarkdownText } from '../../UI/MarkdownText';
|
||||
import SelectField from '../../UI/SelectField';
|
||||
import SelectOption from '../../UI/SelectOption';
|
||||
import MeasurementUnitDocumentation from '../../PropertiesEditor/MeasurementUnitDocumentation';
|
||||
import { getMeasurementUnitShortLabel } from '../../PropertiesEditor/PropertiesMapToSchema';
|
||||
import AlertMessage from '../../UI/AlertMessage';
|
||||
import { type ResourceManagementProps } from '../../ResourcesList/ResourceSource';
|
||||
import ResourcesLoader from '../../ResourcesLoader';
|
||||
import IconButton from '../../UI/IconButton';
|
||||
import RaisedButton from '../../UI/RaisedButton';
|
||||
import FlatButton from '../../UI/FlatButton';
|
||||
@@ -39,6 +30,11 @@ import useAlertDialog from '../../UI/Alert/useAlertDialog';
|
||||
import { type GLTF } from 'three/examples/jsm/loaders/GLTFLoader';
|
||||
import * as SkeletonUtils from 'three/examples/jsm/utils/SkeletonUtils';
|
||||
import * as THREE from 'three';
|
||||
import {
|
||||
PropertyCheckbox,
|
||||
PropertyField,
|
||||
PropertyResourceSelector,
|
||||
} from './PropertyFields';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
@@ -100,147 +96,6 @@ export const hasLight = (layout: ?gd.Layout) => {
|
||||
return false;
|
||||
};
|
||||
|
||||
type PropertyFieldProps = {|
|
||||
objectConfiguration: gdObjectConfiguration,
|
||||
propertyName: string,
|
||||
onChange?: () => void,
|
||||
|};
|
||||
|
||||
const PropertyField = ({
|
||||
objectConfiguration,
|
||||
propertyName,
|
||||
onChange,
|
||||
}: PropertyFieldProps) => {
|
||||
const forceUpdate = useForceUpdate();
|
||||
const properties = objectConfiguration.getProperties();
|
||||
|
||||
const updateProperty = React.useCallback(
|
||||
(value: string) => {
|
||||
const oldValue = objectConfiguration
|
||||
.getProperties()
|
||||
.get(propertyName)
|
||||
.getValue();
|
||||
objectConfiguration.updateProperty(propertyName, value);
|
||||
const newValue = objectConfiguration
|
||||
.getProperties()
|
||||
.get(propertyName)
|
||||
.getValue();
|
||||
if (onChange && newValue !== oldValue) {
|
||||
onChange();
|
||||
}
|
||||
forceUpdate();
|
||||
},
|
||||
[objectConfiguration, propertyName, onChange, forceUpdate]
|
||||
);
|
||||
|
||||
const property = properties.get(propertyName);
|
||||
const measurementUnit = property.getMeasurementUnit();
|
||||
const endAdornment = {
|
||||
label: getMeasurementUnitShortLabel(measurementUnit),
|
||||
tooltipContent: (
|
||||
<MeasurementUnitDocumentation
|
||||
label={measurementUnit.getLabel()}
|
||||
description={measurementUnit.getDescription()}
|
||||
elementsWithWords={measurementUnit.getElementsWithWords()}
|
||||
/>
|
||||
),
|
||||
};
|
||||
return (
|
||||
<Column noMargin expand key={propertyName}>
|
||||
<SemiControlledTextField
|
||||
floatingLabelFixed
|
||||
floatingLabelText={property.getLabel()}
|
||||
onChange={updateProperty}
|
||||
value={property.getValue()}
|
||||
endAdornment={
|
||||
<Tooltip title={endAdornment.tooltipContent}>
|
||||
<InputAdornment position="end">{endAdornment.label}</InputAdornment>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
const PropertyCheckbox = ({
|
||||
objectConfiguration,
|
||||
propertyName,
|
||||
}: PropertyFieldProps) => {
|
||||
const forceUpdate = useForceUpdate();
|
||||
const properties = objectConfiguration.getProperties();
|
||||
|
||||
const onChangeProperty = React.useCallback(
|
||||
(property: string, value: string) => {
|
||||
objectConfiguration.updateProperty(property, value);
|
||||
forceUpdate();
|
||||
},
|
||||
[objectConfiguration, forceUpdate]
|
||||
);
|
||||
|
||||
const property = properties.get(propertyName);
|
||||
return (
|
||||
<Checkbox
|
||||
checked={property.getValue() === 'true'}
|
||||
label={
|
||||
<React.Fragment>
|
||||
<Line noMargin>{property.getLabel()}</Line>
|
||||
<FormHelperText style={{ display: 'inline' }}>
|
||||
<MarkdownText source={property.getDescription()} />
|
||||
</FormHelperText>
|
||||
</React.Fragment>
|
||||
}
|
||||
onCheck={(_, value) => {
|
||||
onChangeProperty(propertyName, value ? '1' : '0');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type PropertyResourceSelectorProps = {|
|
||||
objectConfiguration: gdObjectConfiguration,
|
||||
propertyName: string,
|
||||
project: gd.Project,
|
||||
resourceManagementProps: ResourceManagementProps,
|
||||
onChange: (value: string) => void,
|
||||
|};
|
||||
|
||||
const PropertyResourceSelector = ({
|
||||
objectConfiguration,
|
||||
propertyName,
|
||||
project,
|
||||
resourceManagementProps,
|
||||
onChange,
|
||||
}: PropertyResourceSelectorProps) => {
|
||||
const forceUpdate = useForceUpdate();
|
||||
const { current: resourcesLoader } = React.useRef(ResourcesLoader);
|
||||
const properties = objectConfiguration.getProperties();
|
||||
|
||||
const onChangeProperty = React.useCallback(
|
||||
(property: string, value: string) => {
|
||||
objectConfiguration.updateProperty(property, value);
|
||||
onChange(value);
|
||||
forceUpdate();
|
||||
},
|
||||
[objectConfiguration, onChange, forceUpdate]
|
||||
);
|
||||
|
||||
const property = properties.get(propertyName);
|
||||
const extraInfos = property.getExtraInfo();
|
||||
return (
|
||||
<ResourceSelector
|
||||
project={project}
|
||||
// $FlowExpectedError
|
||||
resourceKind={extraInfos.size() > 0 ? extraInfos.at(0) : ''}
|
||||
floatingLabelText={property.getLabel()}
|
||||
resourceManagementProps={resourceManagementProps}
|
||||
initialResourceName={property.getValue()}
|
||||
onChange={value => onChangeProperty(propertyName, value)}
|
||||
resourcesLoader={resourcesLoader}
|
||||
fullWidth
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Model3DEditor = ({
|
||||
objectConfiguration,
|
||||
project,
|
||||
|
164
newIDE/app/src/ObjectEditor/Editors/PropertyFields.js
Normal file
164
newIDE/app/src/ObjectEditor/Editors/PropertyFields.js
Normal file
@@ -0,0 +1,164 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import SemiControlledTextField from '../../UI/SemiControlledTextField';
|
||||
import useForceUpdate from '../../Utils/UseForceUpdate';
|
||||
import Checkbox from '../../UI/Checkbox';
|
||||
import ResourceSelector from '../../ResourcesList/ResourceSelector';
|
||||
import FormHelperText from '@material-ui/core/FormHelperText';
|
||||
import InputAdornment from '@material-ui/core/InputAdornment';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import { MarkdownText } from '../../UI/MarkdownText';
|
||||
import MeasurementUnitDocumentation from '../../PropertiesEditor/MeasurementUnitDocumentation';
|
||||
import { getMeasurementUnitShortLabel } from '../../PropertiesEditor/PropertiesMapToSchema';
|
||||
import { type ResourceManagementProps } from '../../ResourcesList/ResourceSource';
|
||||
import ResourcesLoader from '../../ResourcesLoader';
|
||||
import { Column, Line } from '../../UI/Grid';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
type PropertyFieldProps = {|
|
||||
objectConfiguration: gdObjectConfiguration,
|
||||
propertyName: string,
|
||||
onChange?: () => void,
|
||||
|};
|
||||
|
||||
export const PropertyField = ({
|
||||
objectConfiguration,
|
||||
propertyName,
|
||||
onChange,
|
||||
}: PropertyFieldProps) => {
|
||||
const forceUpdate = useForceUpdate();
|
||||
const properties = objectConfiguration.getProperties();
|
||||
|
||||
const updateProperty = React.useCallback(
|
||||
(value: string) => {
|
||||
const oldValue = objectConfiguration
|
||||
.getProperties()
|
||||
.get(propertyName)
|
||||
.getValue();
|
||||
objectConfiguration.updateProperty(propertyName, value);
|
||||
const newValue = objectConfiguration
|
||||
.getProperties()
|
||||
.get(propertyName)
|
||||
.getValue();
|
||||
if (onChange && newValue !== oldValue) {
|
||||
onChange();
|
||||
}
|
||||
forceUpdate();
|
||||
},
|
||||
[objectConfiguration, propertyName, onChange, forceUpdate]
|
||||
);
|
||||
|
||||
const property = properties.get(propertyName);
|
||||
const measurementUnit = property.getMeasurementUnit();
|
||||
const endAdornment = {
|
||||
label: getMeasurementUnitShortLabel(measurementUnit),
|
||||
tooltipContent: (
|
||||
<MeasurementUnitDocumentation
|
||||
label={measurementUnit.getLabel()}
|
||||
description={measurementUnit.getDescription()}
|
||||
elementsWithWords={measurementUnit.getElementsWithWords()}
|
||||
/>
|
||||
),
|
||||
};
|
||||
return (
|
||||
<Column noMargin expand key={propertyName}>
|
||||
<SemiControlledTextField
|
||||
floatingLabelFixed
|
||||
floatingLabelText={property.getLabel()}
|
||||
onChange={updateProperty}
|
||||
value={property.getValue()}
|
||||
endAdornment={
|
||||
<Tooltip title={endAdornment.tooltipContent}>
|
||||
<InputAdornment position="end">{endAdornment.label}</InputAdornment>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export const PropertyCheckbox = ({
|
||||
objectConfiguration,
|
||||
propertyName,
|
||||
}: PropertyFieldProps) => {
|
||||
const forceUpdate = useForceUpdate();
|
||||
const properties = objectConfiguration.getProperties();
|
||||
|
||||
const onChangeProperty = React.useCallback(
|
||||
(property: string, value: string) => {
|
||||
objectConfiguration.updateProperty(property, value);
|
||||
forceUpdate();
|
||||
},
|
||||
[objectConfiguration, forceUpdate]
|
||||
);
|
||||
|
||||
const property = properties.get(propertyName);
|
||||
return (
|
||||
<Checkbox
|
||||
checked={property.getValue() === 'true'}
|
||||
label={
|
||||
<React.Fragment>
|
||||
<Line noMargin>{property.getLabel()}</Line>
|
||||
<FormHelperText style={{ display: 'inline' }}>
|
||||
<MarkdownText source={property.getDescription()} />
|
||||
</FormHelperText>
|
||||
</React.Fragment>
|
||||
}
|
||||
onCheck={(_, value) => {
|
||||
onChangeProperty(propertyName, value ? '1' : '0');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type PropertyResourceSelectorProps = {|
|
||||
objectConfiguration: gdObjectConfiguration,
|
||||
propertyName: string,
|
||||
project: gd.Project,
|
||||
resourceManagementProps: ResourceManagementProps,
|
||||
onChange: (value: string) => void,
|
||||
|};
|
||||
|
||||
export const PropertyResourceSelector = ({
|
||||
objectConfiguration,
|
||||
propertyName,
|
||||
project,
|
||||
resourceManagementProps,
|
||||
onChange,
|
||||
}: PropertyResourceSelectorProps) => {
|
||||
const forceUpdate = useForceUpdate();
|
||||
const { current: resourcesLoader } = React.useRef(ResourcesLoader);
|
||||
const properties = objectConfiguration.getProperties();
|
||||
|
||||
const onChangeProperty = React.useCallback(
|
||||
(propertyName: string, newValue: string) => {
|
||||
objectConfiguration.updateProperty(propertyName, newValue);
|
||||
onChange(newValue);
|
||||
forceUpdate();
|
||||
},
|
||||
[objectConfiguration, onChange, forceUpdate]
|
||||
);
|
||||
|
||||
// Note that property is a temporary - don't access it in callbacks.
|
||||
const property = properties.get(propertyName);
|
||||
const extraInfos = property.getExtraInfo();
|
||||
const value = property.getValue();
|
||||
const label = property.getLabel();
|
||||
|
||||
return (
|
||||
<ResourceSelector
|
||||
project={project}
|
||||
// $FlowExpectedError
|
||||
resourceKind={extraInfos.size() > 0 ? extraInfos.at(0) : ''}
|
||||
floatingLabelText={label}
|
||||
resourceManagementProps={resourceManagementProps}
|
||||
initialResourceName={value}
|
||||
onChange={newValue => {
|
||||
if (newValue !== value) onChangeProperty(propertyName, newValue);
|
||||
}}
|
||||
resourcesLoader={resourcesLoader}
|
||||
fullWidth
|
||||
/>
|
||||
);
|
||||
};
|
534
newIDE/app/src/ObjectEditor/Editors/SpineEditor.js
Normal file
534
newIDE/app/src/ObjectEditor/Editors/SpineEditor.js
Normal file
@@ -0,0 +1,534 @@
|
||||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { t } from '@lingui/macro';
|
||||
import { type EditorProps } from './EditorProps.flow';
|
||||
import { ColumnStackLayout, ResponsiveLineStackLayout } from '../../UI/Layout';
|
||||
import Text from '../../UI/Text';
|
||||
import SemiControlledTextField from '../../UI/SemiControlledTextField';
|
||||
import useForceUpdate from '../../Utils/UseForceUpdate';
|
||||
import Checkbox from '../../UI/Checkbox';
|
||||
import { Column, Line, Spacer } from '../../UI/Grid';
|
||||
import SelectField from '../../UI/SelectField';
|
||||
import SelectOption from '../../UI/SelectOption';
|
||||
import IconButton from '../../UI/IconButton';
|
||||
import RaisedButton from '../../UI/RaisedButton';
|
||||
import FlatButton from '../../UI/FlatButton';
|
||||
import { mapFor } from '../../Utils/MapFor';
|
||||
import ScrollView, { type ScrollViewInterface } from '../../UI/ScrollView';
|
||||
import { EmptyPlaceholder } from '../../UI/EmptyPlaceholder';
|
||||
import Add from '../../UI/CustomSvgIcons/Add';
|
||||
import Trash from '../../UI/CustomSvgIcons/Trash';
|
||||
import { makeDragSourceAndDropTarget } from '../../UI/DragAndDrop/DragSourceAndDropTarget';
|
||||
import { DragHandleIcon } from '../../UI/DragHandle';
|
||||
import DropIndicator from '../../UI/SortableVirtualizedItemList/DropIndicator';
|
||||
import GDevelopThemeContext from '../../UI/Theme/GDevelopThemeContext';
|
||||
import PixiResourcesLoader, {
|
||||
type SpineDataOrLoadingError,
|
||||
} from '../../ObjectsRendering/PixiResourcesLoader';
|
||||
import useAlertDialog from '../../UI/Alert/useAlertDialog';
|
||||
import { PropertyResourceSelector, PropertyField } from './PropertyFields';
|
||||
import AlertMessage from '../../UI/AlertMessage';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
const DragSourceAndDropTarget = makeDragSourceAndDropTarget(
|
||||
'spine-animations-list'
|
||||
);
|
||||
|
||||
const styles = {
|
||||
rowContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
marginTop: 5,
|
||||
},
|
||||
rowContent: {
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
},
|
||||
};
|
||||
|
||||
const SpineEditor = ({
|
||||
objectConfiguration,
|
||||
project,
|
||||
layout,
|
||||
object,
|
||||
onSizeUpdated,
|
||||
onObjectUpdated,
|
||||
resourceManagementProps,
|
||||
}: EditorProps) => {
|
||||
const scrollView = React.useRef<?ScrollViewInterface>(null);
|
||||
const [
|
||||
justAddedAnimationName,
|
||||
setJustAddedAnimationName,
|
||||
] = React.useState<?string>(null);
|
||||
const justAddedAnimationElement = React.useRef<?any>(null);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (
|
||||
scrollView.current &&
|
||||
justAddedAnimationElement.current &&
|
||||
justAddedAnimationName
|
||||
) {
|
||||
scrollView.current.scrollTo(justAddedAnimationElement.current);
|
||||
setJustAddedAnimationName(null);
|
||||
justAddedAnimationElement.current = null;
|
||||
}
|
||||
},
|
||||
[justAddedAnimationName]
|
||||
);
|
||||
const { showAlert } = useAlertDialog();
|
||||
|
||||
const draggedAnimationIndex = React.useRef<number | null>(null);
|
||||
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
const forceUpdate = useForceUpdate();
|
||||
|
||||
const spineConfiguration = gd.asSpineConfiguration(objectConfiguration);
|
||||
const properties = objectConfiguration.getProperties();
|
||||
|
||||
const [nameErrors, setNameErrors] = React.useState<{ [number]: React.Node }>(
|
||||
{}
|
||||
);
|
||||
|
||||
const [spineData, setSpineData] = React.useState<SpineDataOrLoadingError>({
|
||||
skeleton: null,
|
||||
loadingError: null,
|
||||
loadingErrorReason: null,
|
||||
});
|
||||
|
||||
const [sourceSelectOptions, setSourceSelectOptions] = React.useState<
|
||||
Array<Object>
|
||||
>([]);
|
||||
const spineResourceName = properties.get('spineResourceName').getValue();
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
(async () => {
|
||||
const spineData = await PixiResourcesLoader.getSpineData(
|
||||
project,
|
||||
spineResourceName
|
||||
);
|
||||
|
||||
setSpineData(spineData);
|
||||
|
||||
if (spineData.skeleton) {
|
||||
setSourceSelectOptions(
|
||||
spineData.skeleton.animations.map(animation => (
|
||||
<SelectOption
|
||||
key={animation.name}
|
||||
value={animation.name}
|
||||
label={animation.name}
|
||||
shouldNotTranslate
|
||||
/>
|
||||
))
|
||||
);
|
||||
}
|
||||
})();
|
||||
},
|
||||
[project, spineResourceName, setSourceSelectOptions]
|
||||
);
|
||||
|
||||
const onChangeSpineResourceName = React.useCallback(
|
||||
() => {
|
||||
spineConfiguration.removeAllAnimations();
|
||||
forceUpdate();
|
||||
},
|
||||
[forceUpdate, spineConfiguration]
|
||||
);
|
||||
|
||||
const scanNewAnimations = React.useCallback(
|
||||
() => {
|
||||
const { skeleton } = spineData;
|
||||
if (!skeleton) return;
|
||||
|
||||
setNameErrors({});
|
||||
|
||||
const animationSources = mapFor(
|
||||
0,
|
||||
spineConfiguration.getAnimationsCount(),
|
||||
animationIndex =>
|
||||
spineConfiguration.getAnimation(animationIndex).getSource()
|
||||
);
|
||||
|
||||
let hasAddedAnimation = false;
|
||||
for (const resourceAnimation of skeleton.animations) {
|
||||
if (animationSources.includes(resourceAnimation.name)) {
|
||||
continue;
|
||||
}
|
||||
const newAnimationName = spineConfiguration.hasAnimationNamed(
|
||||
resourceAnimation.name
|
||||
)
|
||||
? ''
|
||||
: resourceAnimation.name;
|
||||
|
||||
const newAnimation = new gd.SpineAnimation();
|
||||
newAnimation.setName(newAnimationName);
|
||||
newAnimation.setSource(resourceAnimation.name);
|
||||
spineConfiguration.addAnimation(newAnimation);
|
||||
newAnimation.delete();
|
||||
hasAddedAnimation = true;
|
||||
}
|
||||
if (hasAddedAnimation) {
|
||||
forceUpdate();
|
||||
onSizeUpdated();
|
||||
if (onObjectUpdated) onObjectUpdated();
|
||||
|
||||
// Scroll to the bottom of the list.
|
||||
// Ideally, we'd wait for the list to be updated to scroll, but
|
||||
// to simplify the code, we just wait a few ms for a new render
|
||||
// to be done.
|
||||
setTimeout(() => {
|
||||
if (scrollView.current) {
|
||||
scrollView.current.scrollToBottom();
|
||||
}
|
||||
}, 100); // A few ms is enough for a new render to be done.
|
||||
} else {
|
||||
showAlert({
|
||||
title: t`No new animation`,
|
||||
message: t`Every animation from the Spine file is already in the list.`,
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
forceUpdate,
|
||||
spineData,
|
||||
spineConfiguration,
|
||||
onObjectUpdated,
|
||||
onSizeUpdated,
|
||||
showAlert,
|
||||
]
|
||||
);
|
||||
|
||||
const addAnimation = React.useCallback(
|
||||
() => {
|
||||
setNameErrors({});
|
||||
|
||||
const emptyAnimation = new gd.SpineAnimation();
|
||||
spineConfiguration.addAnimation(emptyAnimation);
|
||||
emptyAnimation.delete();
|
||||
forceUpdate();
|
||||
onSizeUpdated();
|
||||
if (onObjectUpdated) onObjectUpdated();
|
||||
|
||||
// Scroll to the bottom of the list.
|
||||
// Ideally, we'd wait for the list to be updated to scroll, but
|
||||
// to simplify the code, we just wait a few ms for a new render
|
||||
// to be done.
|
||||
setTimeout(() => {
|
||||
if (scrollView.current) {
|
||||
scrollView.current.scrollToBottom();
|
||||
}
|
||||
}, 100); // A few ms is enough for a new render to be done.
|
||||
},
|
||||
[forceUpdate, onObjectUpdated, onSizeUpdated, spineConfiguration]
|
||||
);
|
||||
|
||||
const removeAnimation = React.useCallback(
|
||||
animationIndex => {
|
||||
setNameErrors({});
|
||||
|
||||
spineConfiguration.removeAnimation(animationIndex);
|
||||
forceUpdate();
|
||||
onSizeUpdated();
|
||||
if (onObjectUpdated) onObjectUpdated();
|
||||
},
|
||||
[forceUpdate, onObjectUpdated, onSizeUpdated, spineConfiguration]
|
||||
);
|
||||
|
||||
const moveAnimation = React.useCallback(
|
||||
(targetIndex: number) => {
|
||||
const draggedIndex = draggedAnimationIndex.current;
|
||||
if (draggedIndex === null) return;
|
||||
|
||||
setNameErrors({});
|
||||
|
||||
spineConfiguration.moveAnimation(
|
||||
draggedIndex,
|
||||
targetIndex > draggedIndex ? targetIndex - 1 : targetIndex
|
||||
);
|
||||
forceUpdate();
|
||||
},
|
||||
[spineConfiguration, forceUpdate]
|
||||
);
|
||||
|
||||
const changeAnimationName = React.useCallback(
|
||||
(animationIndex, newName) => {
|
||||
const currentName = spineConfiguration
|
||||
.getAnimation(animationIndex)
|
||||
.getName();
|
||||
if (currentName === newName) return;
|
||||
const animation = spineConfiguration.getAnimation(animationIndex);
|
||||
|
||||
setNameErrors({});
|
||||
|
||||
if (newName !== '' && spineConfiguration.hasAnimationNamed(newName)) {
|
||||
// The indexes can be used as a key because errors are cleared when
|
||||
// animations are moved.
|
||||
setNameErrors({
|
||||
...nameErrors,
|
||||
[animationIndex]: (
|
||||
<Trans>The animation name {newName} is already taken</Trans>
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
animation.setName(newName);
|
||||
if (layout && object) {
|
||||
gd.WholeProjectRefactorer.renameObjectAnimation(
|
||||
project,
|
||||
layout,
|
||||
object,
|
||||
currentName,
|
||||
newName
|
||||
);
|
||||
}
|
||||
forceUpdate();
|
||||
if (onObjectUpdated) onObjectUpdated();
|
||||
},
|
||||
[
|
||||
spineConfiguration,
|
||||
layout,
|
||||
object,
|
||||
forceUpdate,
|
||||
onObjectUpdated,
|
||||
nameErrors,
|
||||
project,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScrollView ref={scrollView}>
|
||||
<ColumnStackLayout noMargin>
|
||||
<AlertMessage kind="warning">
|
||||
<Trans>
|
||||
You need to own a license of Spine to publish a game with a Spine
|
||||
object.
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
<PropertyResourceSelector
|
||||
objectConfiguration={objectConfiguration}
|
||||
propertyName="spineResourceName"
|
||||
project={project}
|
||||
resourceManagementProps={resourceManagementProps}
|
||||
onChange={onChangeSpineResourceName}
|
||||
/>
|
||||
{!spineData.skeleton && spineData.loadingErrorReason ? (
|
||||
<AlertMessage kind="error">
|
||||
{spineData.loadingErrorReason === 'invalid-spine-resource' ? (
|
||||
<Trans>
|
||||
The selected resource is not a proper Spine resource.
|
||||
</Trans>
|
||||
) : spineData.loadingErrorReason ===
|
||||
'missing-texture-atlas-name' ? (
|
||||
<Trans>Missing texture atlas name in the Spine file.</Trans>
|
||||
) : spineData.loadingErrorReason ===
|
||||
'spine-resource-loading-error' ? (
|
||||
<Trans>
|
||||
Error while loading the Spine resource (
|
||||
{spineData.loadingError
|
||||
? spineData.loadingError.message
|
||||
: 'Unknown error'}
|
||||
).
|
||||
</Trans>
|
||||
) : spineData.loadingErrorReason === 'invalid-atlas-resource' ? (
|
||||
<Trans>
|
||||
The Atlas embedded in the Spine fine can't be located.
|
||||
</Trans>
|
||||
) : spineData.loadingErrorReason ===
|
||||
'missing-texture-resources' ? (
|
||||
<Trans>Missing texture for an atlas in the Spine file.</Trans>
|
||||
) : spineData.loadingErrorReason ===
|
||||
'atlas-resource-loading-error' ? (
|
||||
<Trans>
|
||||
Error while loading the Spine Texture Atlas resource (
|
||||
{spineData.loadingError
|
||||
? spineData.loadingError.message
|
||||
: 'Unknown error'}
|
||||
).
|
||||
</Trans>
|
||||
) : null}
|
||||
</AlertMessage>
|
||||
) : null}
|
||||
<Text size="block-title" noMargin>
|
||||
<Trans>Default size</Trans>
|
||||
</Text>
|
||||
<PropertyField
|
||||
objectConfiguration={objectConfiguration}
|
||||
propertyName="scale"
|
||||
/>
|
||||
{sourceSelectOptions.length && (
|
||||
<>
|
||||
<Text size="block-title">Animations</Text>
|
||||
<Column noMargin expand useFullHeight>
|
||||
{spineConfiguration.getAnimationsCount() === 0 ? (
|
||||
<Column noMargin expand justifyContent="center">
|
||||
<EmptyPlaceholder
|
||||
title={<Trans>Add your first animation</Trans>}
|
||||
description={
|
||||
<Trans>
|
||||
Import one or more animations that are available in
|
||||
this Spine file.
|
||||
</Trans>
|
||||
}
|
||||
actionLabel={<Trans>Add an animation</Trans>}
|
||||
onAction={addAnimation}
|
||||
/>
|
||||
</Column>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
{mapFor(
|
||||
0,
|
||||
spineConfiguration.getAnimationsCount(),
|
||||
animationIndex => {
|
||||
const animation = spineConfiguration.getAnimation(
|
||||
animationIndex
|
||||
);
|
||||
|
||||
const animationRef =
|
||||
justAddedAnimationName === animation.getName()
|
||||
? justAddedAnimationElement
|
||||
: null;
|
||||
|
||||
return (
|
||||
<DragSourceAndDropTarget
|
||||
key={animationIndex}
|
||||
beginDrag={() => {
|
||||
draggedAnimationIndex.current = animationIndex;
|
||||
return {};
|
||||
}}
|
||||
canDrag={() => true}
|
||||
canDrop={() => true}
|
||||
drop={() => {
|
||||
moveAnimation(animationIndex);
|
||||
}}
|
||||
>
|
||||
{({
|
||||
connectDragSource,
|
||||
connectDropTarget,
|
||||
isOver,
|
||||
canDrop,
|
||||
}) =>
|
||||
connectDropTarget(
|
||||
<div
|
||||
key={animationIndex}
|
||||
style={styles.rowContainer}
|
||||
>
|
||||
{isOver && (
|
||||
<DropIndicator canDrop={canDrop} />
|
||||
)}
|
||||
<div
|
||||
ref={animationRef}
|
||||
style={{
|
||||
...styles.rowContent,
|
||||
backgroundColor:
|
||||
gdevelopTheme.list.itemsBackgroundColor,
|
||||
}}
|
||||
>
|
||||
<Line noMargin expand alignItems="center">
|
||||
{connectDragSource(
|
||||
<span>
|
||||
<Column>
|
||||
<DragHandleIcon />
|
||||
</Column>
|
||||
</span>
|
||||
)}
|
||||
<Text noMargin noShrink>
|
||||
<Trans>
|
||||
Animation #{animationIndex}
|
||||
</Trans>
|
||||
</Text>
|
||||
<Spacer />
|
||||
<SemiControlledTextField
|
||||
margin="none"
|
||||
commitOnBlur
|
||||
errorText={nameErrors[animationIndex]}
|
||||
translatableHintText={t`Optional animation name`}
|
||||
value={animation.getName()}
|
||||
onChange={text =>
|
||||
changeAnimationName(
|
||||
animationIndex,
|
||||
text
|
||||
)
|
||||
}
|
||||
fullWidth
|
||||
/>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() =>
|
||||
removeAnimation(animationIndex)
|
||||
}
|
||||
>
|
||||
<Trash />
|
||||
</IconButton>
|
||||
</Line>
|
||||
<Spacer />
|
||||
</div>
|
||||
<Spacer />
|
||||
<ColumnStackLayout expand>
|
||||
<SelectField
|
||||
id="animation-source-field"
|
||||
value={animation.getSource()}
|
||||
onChange={(event, value) => {
|
||||
animation.setSource(event.target.value);
|
||||
forceUpdate();
|
||||
}}
|
||||
margin="dense"
|
||||
fullWidth
|
||||
floatingLabelText={
|
||||
<Trans>Spine animation name</Trans>
|
||||
}
|
||||
translatableHintText={t`Choose an animation`}
|
||||
>
|
||||
{sourceSelectOptions}
|
||||
</SelectField>
|
||||
<Checkbox
|
||||
label={<Trans>Loop</Trans>}
|
||||
checked={animation.shouldLoop()}
|
||||
onCheck={(e, checked) => {
|
||||
animation.setShouldLoop(checked);
|
||||
forceUpdate();
|
||||
}}
|
||||
/>
|
||||
</ColumnStackLayout>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</DragSourceAndDropTarget>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</Column>
|
||||
<Column noMargin>
|
||||
<ResponsiveLineStackLayout
|
||||
justifyContent="space-between"
|
||||
noColumnMargin
|
||||
>
|
||||
<FlatButton
|
||||
label={<Trans>Scan missing animations</Trans>}
|
||||
onClick={scanNewAnimations}
|
||||
/>
|
||||
<RaisedButton
|
||||
label={<Trans>Add an animation</Trans>}
|
||||
primary
|
||||
onClick={addAnimation}
|
||||
icon={<Add />}
|
||||
/>
|
||||
</ResponsiveLineStackLayout>
|
||||
</Column>
|
||||
</>
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
</ScrollView>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SpineEditor;
|
@@ -10,6 +10,7 @@ import ObjectPropertiesEditor from './Editors/ObjectPropertiesEditor';
|
||||
import CustomObjectPropertiesEditor from './Editors/CustomObjectPropertiesEditor';
|
||||
import Cube3DEditor from './Editors/Cube3DEditor';
|
||||
import Model3DEditor from './Editors/Model3DEditor';
|
||||
import SpineEditor from './Editors/SpineEditor';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
@@ -165,6 +166,21 @@ const ObjectsEditorService = {
|
||||
gd.asObjectJsImplementation(objectConfiguration),
|
||||
helpPagePath: '/objects/3d-model',
|
||||
},
|
||||
'SpineObject::SpineObject': {
|
||||
component: SpineEditor,
|
||||
createNewObject: (
|
||||
objectConfiguration: gdObjectConfiguration
|
||||
): gdObjectConfiguration =>
|
||||
gd
|
||||
.asObjectJsImplementation(objectConfiguration)
|
||||
.clone()
|
||||
.release(),
|
||||
castToObjectType: (
|
||||
objectConfiguration: gdObjectConfiguration
|
||||
): gdObjectJsImplementation =>
|
||||
gd.asObjectJsImplementation(objectConfiguration),
|
||||
helpPagePath: '/objects/spine',
|
||||
},
|
||||
'TiledSpriteObject::TiledSprite': {
|
||||
component: TiledSpriteEditor,
|
||||
createNewObject: (): gdTiledSpriteObject => new gd.TiledSpriteObject(),
|
||||
|
@@ -111,6 +111,7 @@ const objectTypeToDefaultName = {
|
||||
'TextInput::TextInputObject': 'NewTextInput',
|
||||
'Scene3D::Model3DObject': 'New3DModel',
|
||||
'Scene3D::Cube3DObject': 'New3DBox',
|
||||
'SpineObject::SpineObject': 'NewSpine',
|
||||
'Video::VideoObject': 'NewVideo',
|
||||
};
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
// @flow
|
||||
import 'pixi-spine';
|
||||
import RenderedUnknownInstance from './Renderers/RenderedUnknownInstance';
|
||||
import RenderedSpriteInstance from './Renderers/RenderedSpriteInstance';
|
||||
import RenderedTiledSpriteInstance from './Renderers/RenderedTiledSpriteInstance';
|
||||
@@ -12,7 +13,8 @@ import PixiResourcesLoader from './PixiResourcesLoader';
|
||||
import ResourcesLoader from '../ResourcesLoader';
|
||||
import RenderedInstance from './Renderers/RenderedInstance';
|
||||
import Rendered3DInstance from './Renderers/Rendered3DInstance';
|
||||
import * as PIXI from 'pixi.js-legacy';
|
||||
import * as PIXI_LEGACY from 'pixi.js-legacy';
|
||||
import * as PIXI_SPINE from 'pixi-spine';
|
||||
import * as THREE from 'three';
|
||||
import * as SkeletonUtils from 'three/examples/jsm/utils/SkeletonUtils';
|
||||
import optionalRequire from '../Utils/OptionalRequire';
|
||||
@@ -20,6 +22,7 @@ import { rgbOrHexToHexNumber } from '../Utils/ColorTransformer';
|
||||
const path = optionalRequire('path');
|
||||
const electron = optionalRequire('electron');
|
||||
const gd: libGDevelop = global.gd;
|
||||
const PIXI = { ...PIXI_LEGACY, ...PIXI_SPINE };
|
||||
|
||||
// Some PixiJS plugins like pixi-tilemap are not distributed as UMD modules,
|
||||
// or still require a global PIXI object to be accessible, so we expose PIXI here.
|
||||
|
@@ -1,24 +1,54 @@
|
||||
// @flow
|
||||
import 'pixi-spine';
|
||||
import slugs from 'slugs';
|
||||
import axios from 'axios';
|
||||
import * as PIXI from 'pixi.js-legacy';
|
||||
import * as PIXI_SPINE from 'pixi-spine';
|
||||
import { ISkeleton, TextureAtlas } from 'pixi-spine';
|
||||
import * as THREE from 'three';
|
||||
import { GLTFLoader, GLTF } from 'three/examples/jsm/loaders/GLTFLoader';
|
||||
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader';
|
||||
import ResourcesLoader from '../ResourcesLoader';
|
||||
import { loadFontFace } from '../Utils/FontFaceLoader';
|
||||
import { checkIfCredentialsRequired } from '../Utils/CrossOrigin';
|
||||
import { type ResourceKind } from '../ResourcesList/ResourceSource';
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
type SpineTextureAtlasOrLoadingError = {|
|
||||
textureAtlas: ?TextureAtlas,
|
||||
loadingError: ?Error,
|
||||
loadingErrorReason:
|
||||
| null
|
||||
| 'invalid-atlas-resource'
|
||||
| 'missing-texture-resources'
|
||||
| 'atlas-resource-loading-error',
|
||||
|};
|
||||
|
||||
export type SpineDataOrLoadingError = {|
|
||||
skeleton: ?ISkeleton,
|
||||
loadingError: ?Error,
|
||||
loadingErrorReason:
|
||||
| null
|
||||
| 'invalid-spine-resource'
|
||||
| 'missing-texture-atlas-name'
|
||||
| 'spine-resource-loading-error'
|
||||
// Atlas loading error reasons:
|
||||
| 'invalid-atlas-resource'
|
||||
| 'missing-texture-resources'
|
||||
| 'atlas-resource-loading-error',
|
||||
|};
|
||||
|
||||
type ResourcePromise<T> = { [resourceName: string]: Promise<T> };
|
||||
|
||||
let loadedBitmapFonts = {};
|
||||
let loadedFontFamilies = {};
|
||||
let loadedTextures = {};
|
||||
const invalidTexture = PIXI.Texture.from('res/error48.png');
|
||||
let loadedThreeTextures = {};
|
||||
let loadedThreeMaterials = {};
|
||||
let loadedOrLoading3DModelPromises: {
|
||||
[resourceName: string]: Promise<THREE.THREE_ADDONS.GLTF>,
|
||||
} = {};
|
||||
let loadedOrLoading3DModelPromises: ResourcePromise<THREE.THREE_ADDONS.GLTF> = {};
|
||||
let spineAtlasPromises: ResourcePromise<SpineTextureAtlasOrLoadingError> = {};
|
||||
let spineDataPromises: ResourcePromise<SpineDataOrLoadingError> = {};
|
||||
|
||||
const createInvalidModel = (): GLTF => {
|
||||
/**
|
||||
@@ -143,6 +173,61 @@ const traverseToRemoveMetalnessFromMeshes = (
|
||||
node: THREE.Object3D<THREE.Event>
|
||||
) => node.traverse(removeMetalnessFromMesh);
|
||||
|
||||
export const readEmbeddedResourcesMapping = (
|
||||
resource: gdResource
|
||||
): {} | null => {
|
||||
const metadataString = resource.getMetadata();
|
||||
try {
|
||||
const metadata = JSON.parse(metadataString);
|
||||
if (
|
||||
!metadata.embeddedResourcesMapping ||
|
||||
typeof metadata.embeddedResourcesMapping !== 'object'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return metadata.embeddedResourcesMapping;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getEmbedderResources = (
|
||||
project: gdProject,
|
||||
embeddedResourceName: string,
|
||||
embedderResourceKind: ResourceKind
|
||||
): Array<gdResource> => {
|
||||
const resourcesManager = project.getResourcesManager();
|
||||
const embedderResources: Array<gdResource> = [];
|
||||
|
||||
for (const resourceName of resourcesManager
|
||||
.getAllResourceNames()
|
||||
.toJSArray()) {
|
||||
if (embeddedResourceName === resourceName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const possibleEmbedderResource = resourcesManager.getResource(resourceName);
|
||||
if (possibleEmbedderResource.getKind() !== embedderResourceKind) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const embeddedResourcesMapping = readEmbeddedResourcesMapping(
|
||||
possibleEmbedderResource
|
||||
);
|
||||
if (!embeddedResourcesMapping) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const mappedResources = Object.values(embeddedResourcesMapping);
|
||||
if (mappedResources.includes(embeddedResourceName)) {
|
||||
embedderResources.push(possibleEmbedderResource);
|
||||
}
|
||||
}
|
||||
|
||||
return embedderResources;
|
||||
};
|
||||
|
||||
/**
|
||||
* Expose functions to load PIXI textures or fonts, given the names of
|
||||
* resources and a gd.Project.
|
||||
@@ -157,12 +242,28 @@ export default class PixiResourcesLoader {
|
||||
loadedThreeTextures = {};
|
||||
loadedThreeMaterials = {};
|
||||
loadedOrLoading3DModelPromises = {};
|
||||
spineAtlasPromises = {};
|
||||
spineDataPromises = {};
|
||||
}
|
||||
|
||||
static async reloadTextureForResource(
|
||||
static async _reloadEmbedderResources(
|
||||
project: gdProject,
|
||||
resourceName: string
|
||||
embeddedResourceName: string,
|
||||
embedderResourceKind: ResourceKind
|
||||
) {
|
||||
const embeddedResources = getEmbedderResources(
|
||||
project,
|
||||
embeddedResourceName,
|
||||
embedderResourceKind
|
||||
);
|
||||
await Promise.all(
|
||||
embeddedResources.map(embeddedResource =>
|
||||
this.reloadResource(project, embeddedResource.getName())
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
static async reloadResource(project: gdProject, resourceName: string) {
|
||||
const loadedTexture = loadedTextures[resourceName];
|
||||
if (loadedTexture && loadedTexture.textureCacheIds) {
|
||||
// The property textureCacheIds indicates that the PIXI.Texture object has some
|
||||
@@ -174,6 +275,9 @@ export default class PixiResourcesLoader {
|
||||
// been added and detected by file watcher. When reloading the texture, the cache must
|
||||
// be cleaned too.
|
||||
delete loadedTextures[resourceName];
|
||||
|
||||
// Also reload any resource embedding this resource:
|
||||
await this._reloadEmbedderResources(project, resourceName, 'atlas');
|
||||
}
|
||||
|
||||
await PixiResourcesLoader.loadTextures(project, [resourceName]);
|
||||
@@ -191,6 +295,34 @@ export default class PixiResourcesLoader {
|
||||
loadedThreeTextures[resourceName].dispose();
|
||||
delete loadedThreeTextures[resourceName];
|
||||
}
|
||||
if (spineAtlasPromises[resourceName]) {
|
||||
await PIXI.Assets.unload(resourceName).catch(async () => {
|
||||
// Workaround:
|
||||
// This is an expected error due to https://github.com/pixijs/spine/issues/537 issue (read comments
|
||||
// and search the other mentions to this issue in the codebase):
|
||||
// A string, instead of a TextureAtlas, is stored as the loaded atlas resource (which is the root cause of this exception).
|
||||
// pixi-spine considers it acts on a TextureAtlas and tries to call dispose on it that causes a TypeError.
|
||||
const { textureAtlas } = await spineAtlasPromises[resourceName];
|
||||
if (textureAtlas) {
|
||||
textureAtlas.dispose(); // Workaround by doing `dispose` ourselves.
|
||||
}
|
||||
});
|
||||
delete spineAtlasPromises[resourceName];
|
||||
|
||||
// Also reload any resource embedding this resource:
|
||||
await this._reloadEmbedderResources(project, resourceName, 'spine');
|
||||
}
|
||||
if (spineDataPromises[resourceName]) {
|
||||
await PIXI.Assets.unload(resourceName);
|
||||
delete spineDataPromises[resourceName];
|
||||
|
||||
// This line allows us to avoid issue https://github.com/pixijs/pixijs/issues/10069.
|
||||
// PIXI.Assets.resolver caches data that was passed to `PIXI.Assets.add`, even if resource was unloaded.
|
||||
// So every time we unload spine resources, we need to call it to clean the resolver cache
|
||||
// and pick up fresh data next time we call `getSpineData`.
|
||||
PIXI.Assets.resolver.prefer();
|
||||
}
|
||||
|
||||
const matchingMaterials = Object.keys(loadedThreeMaterials).filter(key =>
|
||||
key.startsWith(resourceName)
|
||||
);
|
||||
@@ -447,6 +579,232 @@ export default class PixiResourcesLoader {
|
||||
return loadingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Pixi spine texture atlas of the specified resource names.
|
||||
* @param project The project
|
||||
* @param spineTextureAtlasName The name of the atlas texture resource.
|
||||
* @returns The requested texture atlas, or null if it could not be loaded.
|
||||
*/
|
||||
static async _getSpineTextureAtlas(
|
||||
project: gdProject,
|
||||
spineTextureAtlasName: string
|
||||
): Promise<SpineTextureAtlasOrLoadingError> {
|
||||
const promise = spineAtlasPromises[spineTextureAtlasName];
|
||||
if (promise) return promise;
|
||||
|
||||
if (!spineTextureAtlasName) {
|
||||
return {
|
||||
textureAtlas: null,
|
||||
loadingError: null,
|
||||
loadingErrorReason: 'invalid-atlas-resource',
|
||||
};
|
||||
}
|
||||
|
||||
const resourceManager = project.getResourcesManager();
|
||||
if (!resourceManager.hasResource(spineTextureAtlasName)) {
|
||||
return {
|
||||
textureAtlas: null,
|
||||
loadingError: null,
|
||||
loadingErrorReason: 'invalid-atlas-resource',
|
||||
};
|
||||
}
|
||||
|
||||
const resource = resourceManager.getResource(spineTextureAtlasName);
|
||||
if (resource.getKind() !== 'atlas') {
|
||||
return {
|
||||
textureAtlas: null,
|
||||
loadingError: null,
|
||||
loadingErrorReason: 'invalid-atlas-resource',
|
||||
};
|
||||
}
|
||||
|
||||
const embeddedResourcesMapping = readEmbeddedResourcesMapping(resource);
|
||||
const textureAtlasMappingEntries = embeddedResourcesMapping
|
||||
? Object.entries(embeddedResourcesMapping)
|
||||
: [];
|
||||
if (!textureAtlasMappingEntries.length) {
|
||||
return {
|
||||
textureAtlas: null,
|
||||
loadingError: null,
|
||||
loadingErrorReason: 'missing-texture-resources',
|
||||
};
|
||||
}
|
||||
|
||||
const images = textureAtlasMappingEntries.reduce(
|
||||
(imagesMapping, [relatedPath, resourceName]) => {
|
||||
// flow check
|
||||
if (typeof resourceName === 'string') {
|
||||
imagesMapping[relatedPath] = this.getPIXITexture(
|
||||
project,
|
||||
resourceName
|
||||
);
|
||||
}
|
||||
|
||||
return imagesMapping;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
return (spineAtlasPromises[spineTextureAtlasName] = new Promise(resolve => {
|
||||
const atlasUrl = ResourcesLoader.getResourceFullUrl(
|
||||
project,
|
||||
spineTextureAtlasName,
|
||||
{
|
||||
isResourceForPixi: true,
|
||||
}
|
||||
);
|
||||
PIXI.Assets.setPreferences({
|
||||
preferWorkers: false,
|
||||
crossOrigin: checkIfCredentialsRequired(atlasUrl)
|
||||
? 'use-credentials'
|
||||
: 'anonymous',
|
||||
});
|
||||
PIXI.Assets.add(spineTextureAtlasName, atlasUrl, { images });
|
||||
PIXI.Assets.load(spineTextureAtlasName).then(
|
||||
atlas => {
|
||||
// Ideally atlas of type `TextureAtlas` should be passed here.
|
||||
// But there is a known issue in case of preloaded images (see https://github.com/pixijs/spine/issues/537
|
||||
// and search the other mentions to this issue in the codebase).
|
||||
//
|
||||
// This branching covers all possible ways to make it work fine,
|
||||
// if issue is fixed in pixi-spine or after migration to spine-pixi.
|
||||
if (typeof atlas === 'string') {
|
||||
new PIXI_SPINE.TextureAtlas(
|
||||
atlas,
|
||||
(textureName, textureCb) =>
|
||||
textureCb(images[textureName].baseTexture),
|
||||
textureAtlas =>
|
||||
resolve({
|
||||
textureAtlas,
|
||||
loadingError: null,
|
||||
loadingErrorReason: null,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
resolve({
|
||||
textureAtlas: atlas,
|
||||
loadingError: null,
|
||||
loadingErrorReason: null,
|
||||
});
|
||||
}
|
||||
},
|
||||
err => {
|
||||
console.error(
|
||||
`Error while loading Spine atlas "${spineTextureAtlasName}": ${err}.\nCheck if you selected the correct pair of atlas and image files.`
|
||||
);
|
||||
resolve({
|
||||
textureAtlas: null,
|
||||
loadingError: err,
|
||||
loadingErrorReason: 'atlas-resource-loading-error',
|
||||
});
|
||||
}
|
||||
);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Pixi spine data for the specified resource name.
|
||||
* @param project The project
|
||||
* @param spineName The name of the spine json resource
|
||||
* @returns The requested spine skeleton.
|
||||
*/
|
||||
static async getSpineData(
|
||||
project: gdProject,
|
||||
spineName: string
|
||||
): Promise<SpineDataOrLoadingError> {
|
||||
const promise = spineDataPromises[spineName];
|
||||
if (promise) return promise;
|
||||
|
||||
if (!spineName) {
|
||||
// Nothing is even tried to be loaded.
|
||||
return {
|
||||
skeleton: null,
|
||||
loadingError: null,
|
||||
loadingErrorReason: null,
|
||||
};
|
||||
}
|
||||
|
||||
const resourceManager = project.getResourcesManager();
|
||||
if (!resourceManager.hasResource(spineName)) {
|
||||
return {
|
||||
skeleton: null,
|
||||
loadingError: null,
|
||||
loadingErrorReason: 'invalid-spine-resource',
|
||||
};
|
||||
}
|
||||
|
||||
const resource = resourceManager.getResource(spineName);
|
||||
if (resource.getKind() !== 'spine') {
|
||||
return {
|
||||
skeleton: null,
|
||||
loadingError: null,
|
||||
loadingErrorReason: 'invalid-spine-resource',
|
||||
};
|
||||
}
|
||||
|
||||
const embeddedResourcesMapping = readEmbeddedResourcesMapping(resource);
|
||||
const spineTextureAtlasName = embeddedResourcesMapping
|
||||
? Object.values(embeddedResourcesMapping)[0]
|
||||
: null;
|
||||
if (typeof spineTextureAtlasName !== 'string') {
|
||||
return {
|
||||
skeleton: null,
|
||||
loadingError: null,
|
||||
loadingErrorReason: 'missing-texture-atlas-name',
|
||||
};
|
||||
}
|
||||
|
||||
return (spineDataPromises[spineName] = new Promise(resolve => {
|
||||
this._getSpineTextureAtlas(project, spineTextureAtlasName).then(
|
||||
textureAtlasOrLoadingError => {
|
||||
if (!textureAtlasOrLoadingError.textureAtlas) {
|
||||
return resolve({
|
||||
skeleton: null,
|
||||
loadingError: textureAtlasOrLoadingError.loadingError,
|
||||
loadingErrorReason: textureAtlasOrLoadingError.loadingErrorReason,
|
||||
});
|
||||
}
|
||||
|
||||
const spineUrl = ResourcesLoader.getResourceFullUrl(
|
||||
project,
|
||||
spineName,
|
||||
{
|
||||
isResourceForPixi: true,
|
||||
}
|
||||
);
|
||||
PIXI.Assets.setPreferences({
|
||||
preferWorkers: false,
|
||||
crossOrigin: checkIfCredentialsRequired(spineUrl)
|
||||
? 'use-credentials'
|
||||
: 'anonymous',
|
||||
});
|
||||
PIXI.Assets.add(spineName, spineUrl, {
|
||||
spineAtlas: textureAtlasOrLoadingError.textureAtlas,
|
||||
});
|
||||
PIXI.Assets.load(spineName).then(
|
||||
jsonData => {
|
||||
resolve({
|
||||
skeleton: jsonData.spineData,
|
||||
loadingError: null,
|
||||
loadingErrorReason: null,
|
||||
});
|
||||
},
|
||||
err => {
|
||||
console.error(
|
||||
`Error while loading Spine data "${spineName}": ${err}.\nCheck if you selected correct files.`
|
||||
);
|
||||
resolve({
|
||||
skeleton: null,
|
||||
loadingError: err,
|
||||
loadingErrorReason: 'spine-resource-loading-error',
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the PIXI video texture represented by the given resource.
|
||||
* If not loaded, it will load it.
|
||||
|
@@ -0,0 +1,67 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableHeader,
|
||||
TableHeaderColumn,
|
||||
TableRow,
|
||||
TableRowColumn,
|
||||
} from '../../UI/Table';
|
||||
import { readEmbeddedResourcesMapping } from '../../ObjectsRendering/PixiResourcesLoader';
|
||||
|
||||
type Props = {|
|
||||
resources: Array<gdResource>,
|
||||
|};
|
||||
|
||||
const styles = {
|
||||
tableCell: {
|
||||
// Avoid long filenames breaking the design.
|
||||
wordBreak: 'break-word',
|
||||
},
|
||||
};
|
||||
|
||||
export const EmbeddedResourcesMappingTable = ({ resources }: Props) => {
|
||||
if (resources.length !== 1) return null;
|
||||
|
||||
const resource = resources[0];
|
||||
const embeddedResourcesMapping = readEmbeddedResourcesMapping(resource);
|
||||
if (!embeddedResourcesMapping) return null;
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderColumn>
|
||||
<Trans>Embedded file name</Trans>
|
||||
</TableHeaderColumn>
|
||||
<TableHeaderColumn>
|
||||
<Trans>Associated resource name</Trans>
|
||||
</TableHeaderColumn>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{Object.entries(embeddedResourcesMapping).map(
|
||||
([embeddedFilePath, associatedResourceNameRaw]) => {
|
||||
const associatedResourceName =
|
||||
typeof associatedResourceNameRaw === 'string'
|
||||
? associatedResourceNameRaw
|
||||
: 'Unrecognized value.';
|
||||
|
||||
return (
|
||||
<TableRow key={embeddedFilePath}>
|
||||
<TableRowColumn style={styles.tableCell}>
|
||||
{embeddedFilePath}
|
||||
</TableRowColumn>
|
||||
<TableRowColumn style={styles.tableCell}>
|
||||
{associatedResourceName}
|
||||
</TableRowColumn>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
};
|
@@ -16,16 +16,10 @@ import {
|
||||
type ResourceManagementProps,
|
||||
} from '../../ResourcesList/ResourceSource';
|
||||
import useForceUpdate from '../../Utils/UseForceUpdate';
|
||||
|
||||
const styles = {
|
||||
propertiesContainer: {
|
||||
padding: 8,
|
||||
overflowY: 'scroll',
|
||||
scrollbarWidth: 'thin', // For Firefox, to avoid having a very large scrollbar.
|
||||
overflowX: 'hidden',
|
||||
flex: 1,
|
||||
},
|
||||
};
|
||||
import { EmbeddedResourcesMappingTable } from './EmbeddedResourcesMappingTable';
|
||||
import { Spacer } from '../../UI/Grid';
|
||||
import ScrollView from '../../UI/ScrollView';
|
||||
import { ColumnStackLayout } from '../../UI/Layout';
|
||||
|
||||
type Props = {|
|
||||
project: gdProject,
|
||||
@@ -152,15 +146,10 @@ const ResourcePropertiesEditor = React.forwardRef<
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={styles.propertiesContainer}
|
||||
key={resources.map(resource => '' + resource.ptr).join(';')}
|
||||
>
|
||||
<PropertiesEditor
|
||||
schema={schema.concat(resourceSchema)}
|
||||
instances={resources}
|
||||
/>
|
||||
</div>
|
||||
<PropertiesEditor
|
||||
schema={schema.concat(resourceSchema)}
|
||||
instances={resources}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[resources, schema, forceUpdate]
|
||||
@@ -181,9 +170,18 @@ const ResourcePropertiesEditor = React.forwardRef<
|
||||
return (
|
||||
<Background maxWidth>
|
||||
{renderPreview()}
|
||||
{!resources || !resources.length
|
||||
? renderEmpty()
|
||||
: renderResourcesProperties()}
|
||||
<Spacer />
|
||||
<ScrollView>
|
||||
<ColumnStackLayout
|
||||
expand
|
||||
key={resources.map(resource => '' + resource.ptr).join(';')}
|
||||
>
|
||||
{!resources || !resources.length
|
||||
? renderEmpty()
|
||||
: renderResourcesProperties()}
|
||||
<EmbeddedResourcesMappingTable resources={resources} />
|
||||
</ColumnStackLayout>
|
||||
</ScrollView>
|
||||
</Background>
|
||||
);
|
||||
}
|
||||
|
@@ -46,6 +46,8 @@ const resourceKindToInputAcceptedMimes = {
|
||||
// after the files have been picked.
|
||||
// 'model/gltf-binary'
|
||||
],
|
||||
atlas: [],
|
||||
spine: ['application/json'],
|
||||
};
|
||||
|
||||
const getAcceptedExtensions = (
|
||||
|
@@ -31,5 +31,11 @@ describe('FileToCloudProjectResourceUploader', () => {
|
||||
expect(getInputAcceptedMimesAndExtensions('model3D')).toMatchInlineSnapshot(
|
||||
`"file,.glb"`
|
||||
);
|
||||
expect(getInputAcceptedMimesAndExtensions('atlas')).toMatchInlineSnapshot(
|
||||
`".atlas"`
|
||||
);
|
||||
expect(getInputAcceptedMimesAndExtensions('spine')).toMatchInlineSnapshot(
|
||||
`"application/json,.json"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@@ -117,6 +117,21 @@ export function createAndMapEmbeddedResources(
|
||||
|
||||
mapping[relPath] = resourceName;
|
||||
|
||||
// embedded resources can have mappings too
|
||||
if (filesWithMappedResources.has(fullPath)) {
|
||||
const mappedResources = filesWithMappedResources.get(fullPath);
|
||||
|
||||
if (mappedResources && mappedResources.mapping) {
|
||||
theEmbeddedResource.setMetadata(
|
||||
JSON.stringify({
|
||||
embeddedResourcesMapping: mappedResources.mapping,
|
||||
})
|
||||
);
|
||||
|
||||
filesWithMappedResources.delete(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
project.getResourcesManager().addResource(theEmbeddedResource);
|
||||
}
|
||||
}
|
||||
@@ -212,7 +227,96 @@ export async function listTileMapEmbeddedResources(
|
||||
}
|
||||
}
|
||||
|
||||
export async function listSpineEmbeddedResources(
|
||||
project: gdProject,
|
||||
filePath: string
|
||||
): Promise<?EmbeddedResources> {
|
||||
if (!fs || !path) return null;
|
||||
|
||||
const atlasPath = filePath.replace('.json', '.atlas');
|
||||
const hasAtlasWithSameBasename = await new Promise<boolean>(resolve => {
|
||||
fs.promises
|
||||
.access(atlasPath, fs.constants.F_OK)
|
||||
.then(() => resolve(true))
|
||||
.catch(() => resolve(false));
|
||||
});
|
||||
|
||||
// Spine resources usually have the same base names:
|
||||
// e.g. skeleton.json, skeleton.atlas and skeleton.png.
|
||||
if (!hasAtlasWithSameBasename) {
|
||||
console.error(`Could not find an atlas file for Spine file ${filePath}.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const atlasFileName = path.basename(atlasPath);
|
||||
const embeddedResources = new Map<string, EmbeddedResource>();
|
||||
const isOutsideProjectFolder = !isPathInProjectFolder(project, atlasPath);
|
||||
const resource: EmbeddedResource = {
|
||||
resourceKind: 'atlas',
|
||||
relPath: atlasFileName,
|
||||
fullPath: atlasPath,
|
||||
isOutsideProjectFolder,
|
||||
};
|
||||
|
||||
embeddedResources.set(atlasFileName, resource);
|
||||
|
||||
return {
|
||||
embeddedResources,
|
||||
hasAnyEmbeddedResourceOutsideProjectFolder: isOutsideProjectFolder,
|
||||
};
|
||||
}
|
||||
|
||||
export async function listSpineTextureAtlasEmbeddedResources(
|
||||
project: gdProject,
|
||||
filePath: string
|
||||
): Promise<?EmbeddedResources> {
|
||||
if (!fs || !path) return null;
|
||||
|
||||
let atlasContent: ?string = null;
|
||||
try {
|
||||
atlasContent = await fs.promises.readFile(filePath, 'utf8');
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Unable to read Spine Atlas file at path ${filePath}:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
if (!atlasContent) return null;
|
||||
|
||||
const atlasImageRegex = /.*\.(png|jpeg|jpg)$/gm;
|
||||
const imageDependencies = atlasContent.match(atlasImageRegex);
|
||||
if (!imageDependencies) return null;
|
||||
|
||||
const dir = path.dirname(filePath);
|
||||
const embeddedResources = new Map<string, EmbeddedResource>();
|
||||
let hasAnyEmbeddedResourceOutsideProjectFolder = false;
|
||||
|
||||
for (const relatedImagePath of imageDependencies) {
|
||||
const fullPath = path.resolve(dir, relatedImagePath);
|
||||
const isOutsideProjectFolder = !isPathInProjectFolder(project, fullPath);
|
||||
const resource: EmbeddedResource = {
|
||||
resourceKind: 'image',
|
||||
relPath: relatedImagePath,
|
||||
fullPath,
|
||||
isOutsideProjectFolder,
|
||||
};
|
||||
|
||||
embeddedResources.set(relatedImagePath, resource);
|
||||
|
||||
if (isOutsideProjectFolder)
|
||||
hasAnyEmbeddedResourceOutsideProjectFolder = true;
|
||||
}
|
||||
|
||||
return {
|
||||
embeddedResources,
|
||||
hasAnyEmbeddedResourceOutsideProjectFolder,
|
||||
};
|
||||
}
|
||||
|
||||
export const embeddedResourcesParsers: { [string]: ParseEmbeddedFiles } = {
|
||||
tilemap: listTileMapEmbeddedResources,
|
||||
json: listTileMapEmbeddedResources,
|
||||
spine: listSpineEmbeddedResources,
|
||||
atlas: listSpineTextureAtlasEmbeddedResources,
|
||||
};
|
||||
|
@@ -98,6 +98,28 @@ const localResourceSources: Array<ResourceSource> = [
|
||||
// as written inside the tilemap to the name of the resource that is representing this file.
|
||||
const filesWithEmbeddedResources = new Map<string, EmbeddedResources>();
|
||||
const parseEmbeddedResources = embeddedResourcesParsers[kind];
|
||||
const recursivelyParseEmbeddedResources = async (
|
||||
initialEmbeddedResources: EmbeddedResources
|
||||
) => {
|
||||
for (const initialEmbeddedResource of initialEmbeddedResources.embeddedResources.values()) {
|
||||
const embeddedResourseParser =
|
||||
embeddedResourcesParsers[initialEmbeddedResource.resourceKind];
|
||||
|
||||
if (!embeddedResourseParser) continue;
|
||||
|
||||
const { fullPath } = initialEmbeddedResource;
|
||||
const newDependentResources = await embeddedResourseParser(
|
||||
project,
|
||||
fullPath
|
||||
);
|
||||
|
||||
if (newDependentResources) {
|
||||
filesWithEmbeddedResources.set(fullPath, newDependentResources);
|
||||
|
||||
await recursivelyParseEmbeddedResources(newDependentResources);
|
||||
}
|
||||
}
|
||||
};
|
||||
if (parseEmbeddedResources) {
|
||||
for (const filePath of filePaths) {
|
||||
const embeddedResources = await parseEmbeddedResources(
|
||||
@@ -106,6 +128,8 @@ const localResourceSources: Array<ResourceSource> = [
|
||||
);
|
||||
|
||||
if (embeddedResources) {
|
||||
await recursivelyParseEmbeddedResources(embeddedResources);
|
||||
|
||||
filesWithEmbeddedResources.set(filePath, embeddedResources);
|
||||
|
||||
if (embeddedResources.hasAnyEmbeddedResourceOutsideProjectFolder)
|
||||
|
@@ -48,6 +48,8 @@ const ResourcePreview = (props: Props) => {
|
||||
case 'tilemap':
|
||||
case 'tileset':
|
||||
case 'model3D':
|
||||
case 'atlas':
|
||||
case 'spine':
|
||||
return <GenericIconPreview renderIcon={props => <File {...props} />} />;
|
||||
case 'video':
|
||||
return <GenericIconPreview renderIcon={props => <Video {...props} />} />;
|
||||
|
@@ -24,7 +24,9 @@ export type ResourceKind =
|
||||
| 'tilemap'
|
||||
| 'tileset'
|
||||
| 'bitmapFont'
|
||||
| 'model3D';
|
||||
| 'model3D'
|
||||
| 'atlas'
|
||||
| 'spine';
|
||||
|
||||
export const allResourceKindsAndMetadata = [
|
||||
{
|
||||
@@ -55,7 +57,9 @@ export const allResourceKindsAndMetadata = [
|
||||
kind: 'json',
|
||||
displayName: t`Json`,
|
||||
fileExtensions: ['json'],
|
||||
createNewResource: () => new gd.JsonResource(),
|
||||
createNewResource: () => {
|
||||
return new gd.JsonResource();
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'tilemap',
|
||||
@@ -81,6 +85,20 @@ export const allResourceKindsAndMetadata = [
|
||||
fileExtensions: ['glb'],
|
||||
createNewResource: () => new gd.Model3DResource(),
|
||||
},
|
||||
{
|
||||
kind: 'atlas',
|
||||
displayName: t`Atlas`,
|
||||
fileExtensions: ['atlas'],
|
||||
createNewResource: () => new gd.AtlasResource(),
|
||||
},
|
||||
{
|
||||
kind: 'spine',
|
||||
displayName: t`Spine Json`,
|
||||
fileExtensions: ['json'],
|
||||
createNewResource: () => {
|
||||
return new gd.SpineResource();
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const constructors = {};
|
||||
|
@@ -116,10 +116,9 @@ export default class ResourcesList extends React.Component<Props, State> {
|
||||
case 'audio':
|
||||
return 'res/actions/music24.png';
|
||||
case 'json':
|
||||
return 'res/actions/fichier24.png';
|
||||
case 'tilemap':
|
||||
return 'res/actions/fichier24.png';
|
||||
case 'tileset':
|
||||
case 'spine':
|
||||
return 'res/actions/fichier24.png';
|
||||
case 'video':
|
||||
return 'JsPlatform/Extensions/videoicon24.png';
|
||||
|
@@ -236,10 +236,7 @@ export default class SceneEditor extends React.Component<Props, State> {
|
||||
// through the RenderedInstance's, triggering crashes. So the scene rendering
|
||||
// is paused during this period.
|
||||
editorDisplay.startSceneRendering(false);
|
||||
await PixiResourcesLoader.reloadTextureForResource(
|
||||
project,
|
||||
resourceName
|
||||
);
|
||||
await PixiResourcesLoader.reloadResource(project, resourceName);
|
||||
|
||||
editorDisplay.forceUpdateObjectsList();
|
||||
|
||||
|
@@ -53,3 +53,11 @@ export const Model3DResource = () => (
|
||||
</ResourceStoreStateProvider>
|
||||
</FixedHeightFlexContainer>
|
||||
);
|
||||
|
||||
export const AtlasResource = () => (
|
||||
<FixedHeightFlexContainer height={400}>
|
||||
<ResourceStoreStateProvider>
|
||||
<ResourceStore onChoose={action('onChoose')} resourceKind="atlas" />
|
||||
</ResourceStoreStateProvider>
|
||||
</FixedHeightFlexContainer>
|
||||
);
|
||||
|
@@ -18,6 +18,8 @@ import FontResourceField from '../../../EventsSheet/ParameterFields/FontResource
|
||||
import JsonResourceField from '../../../EventsSheet/ParameterFields/JsonResourceField';
|
||||
import TilemapResourceField from '../../../EventsSheet/ParameterFields/TilemapResourceField';
|
||||
import Model3DResourceField from '../../../EventsSheet/ParameterFields/Model3DResourceField';
|
||||
import AtlasResourceField from '../../../EventsSheet/ParameterFields/AtlasResourceField';
|
||||
import SpineResourceField from '../../../EventsSheet/ParameterFields/SpineResourceField';
|
||||
|
||||
export const AllResourceFields = () => (
|
||||
<Column expand>
|
||||
@@ -181,6 +183,46 @@ export const AllResourceFields = () => (
|
||||
)}
|
||||
/>
|
||||
</Line>
|
||||
<Line expand>
|
||||
<ValueStateHolder
|
||||
initialValue={''}
|
||||
render={(value, onChange) => (
|
||||
<AtlasResourceField
|
||||
project={testProject.project}
|
||||
scope={{
|
||||
project: testProject.project,
|
||||
layout: testProject.testLayout,
|
||||
}}
|
||||
globalObjectsContainer={testProject.project}
|
||||
objectsContainer={testProject.testLayout}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
parameterRenderingService={ParameterRenderingService}
|
||||
resourceManagementProps={fakeResourceManagementProps}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Line>
|
||||
<Line expand>
|
||||
<ValueStateHolder
|
||||
initialValue={''}
|
||||
render={(value, onChange) => (
|
||||
<SpineResourceField
|
||||
project={testProject.project}
|
||||
scope={{
|
||||
project: testProject.project,
|
||||
layout: testProject.testLayout,
|
||||
}}
|
||||
globalObjectsContainer={testProject.project}
|
||||
objectsContainer={testProject.testLayout}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
parameterRenderingService={ParameterRenderingService}
|
||||
resourceManagementProps={fakeResourceManagementProps}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Line>
|
||||
</Column>
|
||||
);
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"skipLibCheck": true,
|
||||
// Not used (as we don't compile with tsc), but necessary for iterating on Map/Set.
|
||||
"downlevelIteration": true,
|
||||
// Required by PixiJS, for importing 3rd-party dependencies like EventEmitter3.
|
||||
@@ -40,6 +41,7 @@
|
||||
// Exclude pre-built js code
|
||||
"exclude": [
|
||||
"GDJS/Runtime/pixi-renderers/pixi.js",
|
||||
"GDJS/Runtime/pixi-renderers/pixi-spine.js",
|
||||
"GDJS/Runtime/Cordova",
|
||||
"GDJS/Runtime/Electron",
|
||||
"GDJS/Runtime/FacebookInstantGames",
|
||||
|
Reference in New Issue
Block a user