Compare commits

...

51 Commits

Author SHA1 Message Date
Florian Rival
14a9ede540 Add infinite button for particle tank 2024-09-25 18:59:12 +02:00
Florian Rival
6d9fb72de5 Switch back to toggles with label on the right 2024-09-25 18:04:03 +02:00
Florian Rival
a6f359c788 Display error border when invalid resource in compact fields + other small improvements 2024-09-25 17:55:29 +02:00
Florian Rival
9d9ef7a305 Add red border when a resource thumbnail is invalid 2024-09-25 12:41:11 +02:00
Florian Rival
e34b3bab98 Fix properties display and reduce some labels 2024-09-25 12:03:33 +02:00
Florian Rival
fc054de5b3 Fix separators in properties panels 2024-09-25 11:46:45 +02:00
Florian Rival
6533586fd5 Fix formatting 2024-09-24 21:52:45 +02:00
Florian Rival
5c78760bd2 Fix wrong merge and save folding state of behaviors 2024-09-24 21:51:39 +02:00
Florian Rival
57447a85a0 Merge branch 'master' into feat/object-panel 2024-09-24 17:37:46 +02:00
Florian Rival
43262b80e9 Fix libGD.js compilation 2024-09-23 19:01:28 +02:00
Florian Rival
73571dc250 Add basic profiler to the scene editor 2024-09-23 18:12:41 +02:00
Florian Rival
faa506cee7 Improve JsObjectImplementation serialization 2024-09-22 19:24:22 +02:00
Florian Rival
058de8d89e Fix performance of 3D model/Spine instance rendering by using direct getters instead of properties 2024-09-22 18:28:27 +02:00
Florian Rival
b45acc7232 Rework all objects based on ObjectJsImplementation to avoid reading properties at render time for instances 2024-09-22 16:58:02 +02:00
Florian Rival
1fdb1b4639 Fix loading of 3D cube textures 2024-09-21 16:27:50 +02:00
Florian Rival
d06490d224 Fix 3D cube tiling not updated when changed when face visibility is hidden 2024-09-21 14:31:08 +02:00
Florian Rival
108aa83b5a Add no variables text in properties panel 2024-09-20 15:22:21 +02:00
Florian Rival
36994146a5 Use a compact search bar in properties panel 2024-09-20 14:27:00 +02:00
Florian Rival
07c58970d9 Add button to open full editor for physics engine and regroup properties 2024-09-19 17:07:09 +02:00
Florian Rival
b65c5ecbc0 Hide single choice required behaviors in compact properties 2024-09-19 09:49:11 +02:00
Florian Rival
1f73056ab6 Add collapsible sections in the objects panel 2024-09-18 23:00:34 +02:00
Florian Rival
a3a87b3b09 Add properties for Shape Painter object and refactor colors 2024-09-18 19:28:42 +02:00
Florian Rival
2680536448 Allow to fold children object and add text for empty sections 2024-09-18 17:38:48 +02:00
Florian Rival
cc080cd39c Improve object properties display 2024-09-18 16:05:58 +02:00
Florian Rival
a90fe9ae82 Merge branch 'master' into feat/object-panel 2024-09-18 14:32:13 +02:00
Florian Rival
f742b393e1 Add buttons to edit animations for Sprite, Spine, tilemap 2024-09-17 10:53:26 +02:00
Florian Rival
4a7ab83f0e Updated Spine instance renderer to make it (partially) dynamic 2024-09-15 21:23:13 +02:00
Florian Rival
0cfc8eccf8 Fix sprite animation lists and selection checkbox
Don't show in changelog
2024-09-15 17:40:04 +02:00
Florian Rival
5b4dfd5c19 Make 3D model instance renderer dynamic and fix warnings 2024-09-15 15:21:48 +02:00
Florian Rival
5158d68157 Make 3D cube instance renderer dynamic 2024-09-13 19:11:33 +02:00
Florian Rival
d69657f533 Fix formatting and light instance rendering 2024-09-13 18:34:34 +02:00
Florian Rival
6e4e5f6499 Merge branch 'master' into feat/object-panel 2024-09-13 18:10:26 +02:00
Florian Rival
d214c504b5 Improve wording 2024-09-12 21:15:48 +02:00
Florian Rival
639626d361 Allow to properly change the particle emitter type in object panel 2024-09-12 21:15:48 +02:00
Florian Rival
3374823695 Update Particle Emitter to use a single string as color and add properties for the object panel 2024-09-12 21:15:48 +02:00
Florian Rival
3c8ee887b0 Show a loading texture for Panel Sprite instead of a error texture 2024-09-12 21:15:48 +02:00
Florian Rival
2551778b21 Make Light instance renderer dynamic 2024-09-12 21:15:48 +02:00
Florian Rival
0e586038a3 Make PanelSprite instance renderer fully dynamic 2024-09-12 21:15:48 +02:00
Florian Rival
ac1adaba6c Fix crash because of invalid texture when resetting Panel Sprite instance renderer 2024-09-12 21:15:48 +02:00
Florian Rival
6863360d3f Fix warning 2024-09-12 21:15:48 +02:00
Florian Rival
d56785eb5f Add events based object children in object panel 2024-09-12 21:15:48 +02:00
Florian Rival
0b5feabaab Add more properties 2024-09-12 21:15:48 +02:00
Florian Rival
be74dc35e9 WIP 2024-09-12 21:15:48 +02:00
Florian Rival
74e8b4e544 Add support for effects folding 2024-09-12 21:15:48 +02:00
Florian Rival
f8be8f7e14 Add back wrongly removed code 2024-09-12 21:15:48 +02:00
Florian Rival
b59f30cd41 Simplify property editors duplicate typing 2024-09-12 21:15:48 +02:00
Florian Rival
581fa8d828 Fix new behavior dialog opening 2024-09-12 21:15:48 +02:00
Florian Rival
1b4e5cdd4c WIP 2024-09-12 21:15:48 +02:00
Florian Rival
bdc4f443f8 WIP 2024-09-12 21:15:48 +02:00
Florian Rival
a479c43096 WIP 2024-09-12 21:15:48 +02:00
Florian Rival
969cde7bc3 WIP 2024-09-12 21:15:48 +02:00
116 changed files with 5898 additions and 3586 deletions

View File

@@ -33,6 +33,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsSpriteExtension(
"most elements of a game."),
"CppPlatform/Extensions/spriteicon.png")
.SetCategoryFullName(_("General"))
.SetOpenFullEditorLabel(_("Edit animations"))
.AddDefaultBehavior("EffectCapability::EffectBehavior")
.AddDefaultBehavior("ResizableCapability::ResizableBehavior")
.AddDefaultBehavior("ScalableCapability::ScalableBehavior")

View File

@@ -47,19 +47,11 @@ void SpriteObject::DoSerializeTo(gd::SerializerElement& element) const {
std::map<gd::String, gd::PropertyDescriptor> SpriteObject::GetProperties()
const {
std::map<gd::String, gd::PropertyDescriptor> properties;
properties[_("Animate even if hidden or far from the screen")]
.SetValue(updateIfNotVisible ? "true" : "false")
.SetType("Boolean");
properties["PLEASE_ALSO_SHOW_EDIT_BUTTON_THANKS"].SetValue("");
return properties;
}
bool SpriteObject::UpdateProperty(const gd::String& name,
const gd::String& value) {
if (name == _("Animate even if hidden or far from the screen"))
updateIfNotVisible = value == "1";
return true;
}

View File

@@ -307,6 +307,15 @@ class GD_CORE_API BehaviorMetadata : public InstructionOrExpressionContainerMeta
return *this;
}
BehaviorMetadata &SetOpenFullEditorLabel(const gd::String& label) {
openFullEditorLabel = label;
return *this;
}
const gd::String& GetOpenFullEditorLabel() const {
return openFullEditorLabel;
}
/**
* \brief Return the associated gd::Behavior, handling behavior contents.
*
@@ -384,6 +393,7 @@ class GD_CORE_API BehaviorMetadata : public InstructionOrExpressionContainerMeta
mutable std::vector<gd::String> requiredBehaviors;
bool isPrivate = false;
bool isHidden = false;
gd::String openFullEditorLabel;
QuickCustomization::Visibility quickCustomizationVisibility;
// TODO: Nitpicking: convert these to std::unique_ptr to clarify ownership.

View File

@@ -323,6 +323,15 @@ class GD_CORE_API ObjectMetadata : public InstructionOrExpressionContainerMetada
*/
bool IsRenderedIn3D() const { return isRenderedIn3D; }
ObjectMetadata &SetOpenFullEditorLabel(const gd::String& label) {
openFullEditorLabel = label;
return *this;
}
const gd::String& GetOpenFullEditorLabel() const {
return openFullEditorLabel;
}
std::map<gd::String, gd::InstructionMetadata> conditionsInfos;
std::map<gd::String, gd::InstructionMetadata> actionsInfos;
std::map<gd::String, gd::ExpressionMetadata> expressionsInfos;
@@ -344,6 +353,7 @@ class GD_CORE_API ObjectMetadata : public InstructionOrExpressionContainerMetada
std::set<gd::String> defaultBehaviorTypes;
bool hidden = false;
bool isRenderedIn3D = false;
gd::String openFullEditorLabel;
std::shared_ptr<gd::ObjectConfiguration>
blueprintObject; ///< The "blueprint" object to be copied when a new

View File

@@ -9,6 +9,7 @@
#include <map>
#include <memory>
#include <unordered_set>
#include "GDCore/Project/Object.h"
#include "GDCore/Project/Project.h"
#include "GDCore/Project/EventsBasedObject.h"
@@ -109,6 +110,27 @@ class CustomObjectConfiguration : public gd::ObjectConfiguration {
static const gd::CustomObjectConfiguration::EdgeAnchor
GetEdgeAnchorFromString(const gd::String &value);
/**
* Check if a child object properties must be displayed as folded in the editor.
* This is only useful when the object can override its children configuration (which
* is something being deprecated).
*/
bool IsChildObjectFolded(const gd::String& childName) const {
return unfoldedChildren.find(childName) == unfoldedChildren.end();
}
/**
* Set if a child object properties must be displayed as folded in the editor.
* This is only useful when the object can override its children configuration (which
* is something being deprecated).
*/
void SetChildObjectFolded(const gd::String& childName, bool folded) {
if (!folded)
unfoldedChildren.insert(childName);
else
unfoldedChildren.erase(childName);
}
protected:
void DoSerializeTo(SerializerElement& element) const override;
void DoUnserializeFrom(Project& project, const SerializerElement& element) override;
@@ -121,7 +143,8 @@ protected:
const Project* project; ///< The project is used to get the
///< EventBasedObject from the fullType.
gd::SerializerElement objectContent;
std::unordered_set<gd::String> unfoldedChildren;
bool isMarkedAsOverridingEventsBasedObjectChildrenConfiguration;
mutable std::map<gd::String, std::unique_ptr<gd::ObjectConfiguration>> childObjectConfigurations;

View File

@@ -12,6 +12,7 @@ namespace gd {
void Effect::SerializeTo(SerializerElement& element) const {
element.SetAttribute("name", GetName());
element.SetAttribute("effectType", GetEffectType());
if (IsFolded()) element.SetBoolAttribute("folded", true);
SerializerElement& doubleParametersElement =
element.AddChild("doubleParameters");
@@ -41,6 +42,7 @@ void Effect::UnserializeFrom(const SerializerElement& element) {
"effectName"
// end of compatibility code
));
SetFolded(element.GetBoolAttribute("folded", false));
doubleParameters.clear();
const SerializerElement& doubleParametersElement =

View File

@@ -194,6 +194,9 @@ void Object::UnserializeFrom(gd::Project& project,
behavior->UnserializeFrom(behaviorElement);
}
bool isFolded = behaviorElement.GetBoolAttribute("isFolded", false);
behavior->SetFolded(isFolded);
// Handle Quick Customization info.
if (behaviorElement.HasChild(
"propertiesQuickCustomizationVisibilities")) {
@@ -239,8 +242,10 @@ void Object::SerializeTo(SerializerElement& element) const {
behaviorElement.RemoveChild("type"); // The content can contain type or
// name properties, remove them.
behaviorElement.RemoveChild("name");
behaviorElement.RemoveChild("isFolded");
behaviorElement.SetAttribute("type", behavior.GetTypeName());
behaviorElement.SetAttribute("name", behavior.GetName());
if (behavior.IsFolded()) behaviorElement.SetAttribute("isFolded", true);
// Handle Quick Customization info.
behaviorElement.RemoveChild("propertiesQuickCustomizationVisibilities");

View File

@@ -803,11 +803,8 @@ module.exports = {
}
const Cube3DObject = new gd.ObjectJsImplementation();
Cube3DObject.updateProperty = function (
objectContent,
propertyName,
newValue
) {
Cube3DObject.updateProperty = function (propertyName, newValue) {
const objectContent = this.content;
if (
propertyName === 'width' ||
propertyName === 'height' ||
@@ -851,8 +848,9 @@ module.exports = {
return false;
};
Cube3DObject.getProperties = function (objectContent) {
Cube3DObject.getProperties = function () {
const objectProperties = new gd.MapStringPropertyDescriptor();
const objectContent = this.content;
objectProperties
.getOrCreate('enableTextureTransparency')
@@ -878,7 +876,8 @@ module.exports = {
'The top of each image can touch the **top face** (Y) or the **front face** (Z).'
)
)
.setGroup(_('Face orientation'));
.setGroup(_('Face orientation'))
.setAdvanced(true);
objectProperties
.getOrCreate('width')
@@ -909,7 +908,7 @@ module.exports = {
.setValue(objectContent.frontFaceResourceName || '')
.setType('resource')
.addExtraInfo('image')
.setLabel(_('Front face image'))
.setLabel(_('Front face'))
.setGroup(_('Textures'));
objectProperties
@@ -917,7 +916,7 @@ module.exports = {
.setValue(objectContent.backFaceResourceName || '')
.setType('resource')
.addExtraInfo('image')
.setLabel(_('Back face image'))
.setLabel(_('Back face'))
.setGroup(_('Textures'));
objectProperties
@@ -932,14 +931,15 @@ module.exports = {
'The top of the image can touch the **top face** (Y) or the **bottom face** (X).'
)
)
.setGroup(_('Textures'));
.setGroup(_('Face orientation'))
.setAdvanced(true);
objectProperties
.getOrCreate('leftFaceResourceName')
.setValue(objectContent.leftFaceResourceName || '')
.setType('resource')
.addExtraInfo('image')
.setLabel(_('Left face image'))
.setLabel(_('Left face'))
.setGroup(_('Textures'));
objectProperties
@@ -947,7 +947,7 @@ module.exports = {
.setValue(objectContent.rightFaceResourceName || '')
.setType('resource')
.addExtraInfo('image')
.setLabel(_('Right face image'))
.setLabel(_('Right face'))
.setGroup(_('Textures'));
objectProperties
@@ -955,7 +955,7 @@ module.exports = {
.setValue(objectContent.topFaceResourceName || '')
.setType('resource')
.addExtraInfo('image')
.setLabel(_('Top face image'))
.setLabel(_('Top face'))
.setGroup(_('Textures'));
objectProperties
@@ -963,92 +963,98 @@ module.exports = {
.setValue(objectContent.bottomFaceResourceName || '')
.setType('resource')
.addExtraInfo('image')
.setLabel(_('Bottom face image'))
.setLabel(_('Bottom face'))
.setGroup(_('Textures'));
objectProperties
.getOrCreate('frontFaceResourceRepeat')
.setValue(objectContent.frontFaceResourceRepeat ? 'true' : 'false')
.setType('boolean')
.setLabel(_('Tile front face image'))
.setLabel(_('Tile'))
.setGroup(_('Textures'));
objectProperties
.getOrCreate('backFaceResourceRepeat')
.setValue(objectContent.backFaceResourceRepeat ? 'true' : 'false')
.setType('boolean')
.setLabel(_('Tile back face image'))
.setLabel(_('Tile'))
.setGroup(_('Textures'));
objectProperties
.getOrCreate('leftFaceResourceRepeat')
.setValue(objectContent.leftFaceResourceRepeat ? 'true' : 'false')
.setType('boolean')
.setLabel(_('Tile left face image'))
.setLabel(_('Tile'))
.setGroup(_('Textures'));
objectProperties
.getOrCreate('rightFaceResourceRepeat')
.setValue(objectContent.rightFaceResourceRepeat ? 'true' : 'false')
.setType('boolean')
.setLabel(_('Tile right face image'))
.setLabel(_('Tile'))
.setGroup(_('Textures'));
objectProperties
.getOrCreate('topFaceResourceRepeat')
.setValue(objectContent.topFaceResourceRepeat ? 'true' : 'false')
.setType('boolean')
.setLabel(_('Tile top face image'))
.setLabel(_('Tile'))
.setGroup(_('Textures'));
objectProperties
.getOrCreate('bottomFaceResourceRepeat')
.setValue(objectContent.bottomFaceResourceRepeat ? 'true' : 'false')
.setType('boolean')
.setLabel(_('Tile bottom face image'))
.setLabel(_('Tile'))
.setGroup(_('Textures'));
objectProperties
.getOrCreate('frontFaceVisible')
.setValue(objectContent.frontFaceVisible ? 'true' : 'false')
.setType('boolean')
.setLabel(_('Show front face'))
.setGroup(_('Face visibility'));
.setLabel(_('Front face'))
.setGroup(_('Face visibility'))
.setAdvanced(true);
objectProperties
.getOrCreate('backFaceVisible')
.setValue(objectContent.backFaceVisible ? 'true' : 'false')
.setType('boolean')
.setLabel(_('Show back face'))
.setGroup(_('Face visibility'));
.setLabel(_('Back face'))
.setGroup(_('Face visibility'))
.setAdvanced(true);
objectProperties
.getOrCreate('leftFaceVisible')
.setValue(objectContent.leftFaceVisible ? 'true' : 'false')
.setType('boolean')
.setLabel(_('Show left face'))
.setGroup(_('Face visibility'));
.setLabel(_('Left face'))
.setGroup(_('Face visibility'))
.setAdvanced(true);
objectProperties
.getOrCreate('rightFaceVisible')
.setValue(objectContent.rightFaceVisible ? 'true' : 'false')
.setType('boolean')
.setLabel(_('Show right face'))
.setGroup(_('Face visibility'));
.setLabel(_('Right face'))
.setGroup(_('Face visibility'))
.setAdvanced(true);
objectProperties
.getOrCreate('topFaceVisible')
.setValue(objectContent.topFaceVisible ? 'true' : 'false')
.setType('boolean')
.setLabel(_('Show top face'))
.setGroup(_('Face visibility'));
.setLabel(_('Top face'))
.setGroup(_('Face visibility'))
.setAdvanced(true);
objectProperties
.getOrCreate('bottomFaceVisible')
.setValue(objectContent.bottomFaceVisible ? 'true' : 'false')
.setType('boolean')
.setLabel(_('Show bottom face'))
.setGroup(_('Face visibility'));
.setLabel(_('Bottom face'))
.setGroup(_('Face visibility'))
.setAdvanced(true);
objectProperties
.getOrCreate('materialType')
@@ -1060,38 +1066,35 @@ module.exports = {
return objectProperties;
};
Cube3DObject.setRawJSONContent(
JSON.stringify({
width: 100,
height: 100,
depth: 100,
enableTextureTransparency: false,
facesOrientation: 'Y',
frontFaceResourceName: '',
backFaceResourceName: '',
backFaceUpThroughWhichAxisRotation: 'X',
leftFaceResourceName: '',
rightFaceResourceName: '',
topFaceResourceName: '',
bottomFaceResourceName: '',
frontFaceVisible: true,
backFaceVisible: false,
leftFaceVisible: true,
rightFaceVisible: true,
topFaceVisible: true,
bottomFaceVisible: true,
frontFaceResourceRepeat: false,
backFaceResourceRepeat: false,
leftFaceResourceRepeat: false,
rightFaceResourceRepeat: false,
topFaceResourceRepeat: false,
bottomFaceResourceRepeat: false,
materialType: 'Basic',
})
);
Cube3DObject.content = {
width: 100,
height: 100,
depth: 100,
enableTextureTransparency: false,
facesOrientation: 'Y',
frontFaceResourceName: '',
backFaceResourceName: '',
backFaceUpThroughWhichAxisRotation: 'X',
leftFaceResourceName: '',
rightFaceResourceName: '',
topFaceResourceName: '',
bottomFaceResourceName: '',
frontFaceVisible: true,
backFaceVisible: false,
leftFaceVisible: true,
rightFaceVisible: true,
topFaceVisible: true,
bottomFaceVisible: true,
frontFaceResourceRepeat: false,
backFaceResourceRepeat: false,
leftFaceResourceRepeat: false,
rightFaceResourceRepeat: false,
topFaceResourceRepeat: false,
bottomFaceResourceRepeat: false,
materialType: 'Basic',
};
Cube3DObject.updateInitialInstanceProperty = function (
objectContent,
instance,
propertyName,
newValue
@@ -1099,7 +1102,7 @@ module.exports = {
return false;
};
Cube3DObject.getInitialInstanceProperties = function (content, instance) {
Cube3DObject.getInitialInstanceProperties = function (instance) {
const instanceProperties = new gd.MapStringPropertyDescriptor();
return instanceProperties;
};
@@ -2060,7 +2063,10 @@ module.exports = {
};
const getFirstVisibleFaceResourceName = (objectConfiguration) => {
const properties = objectConfiguration.getProperties();
const object = gd.castObject(
objectConfiguration,
gd.ObjectJsImplementation
);
const orderedFaces = [
['frontFaceVisible', 'frontFaceResourceName'],
@@ -2075,10 +2081,8 @@ module.exports = {
faceVisibleProperty,
faceResourceNameProperty,
] of orderedFaces) {
if (properties.get(faceVisibleProperty).getValue() === 'true') {
const textureResource = properties
.get(faceResourceNameProperty)
.getValue();
if (object.content[faceVisibleProperty]) {
const textureResource = object.content[faceResourceNameProperty];
if (textureResource) return textureResource;
}
}
@@ -2124,10 +2128,14 @@ module.exports = {
// Name of the resource that is rendered.
// If no face is visible, this will be null.
this._renderedResourceName = undefined;
const properties = associatedObjectConfiguration.getProperties();
this._defaultWidth = parseFloat(properties.get('width').getValue());
this._defaultHeight = parseFloat(properties.get('height').getValue());
this._defaultDepth = parseFloat(properties.get('depth').getValue());
const object = gd.castObject(
this._associatedObjectConfiguration,
gd.ObjectJsImplementation
);
this._defaultWidth = object.content.width;
this._defaultHeight = object.content.height;
this._defaultDepth = object.content.depth;
this._pixiObject = new PIXI.Container();
this._pixiFallbackObject = new PIXI.Graphics();
@@ -2315,70 +2323,67 @@ module.exports = {
pixiResourcesLoader
);
const properties = associatedObjectConfiguration.getProperties();
this._defaultWidth = parseFloat(properties.get('width').getValue());
this._defaultHeight = parseFloat(properties.get('height').getValue());
this._defaultDepth = parseFloat(properties.get('depth').getValue());
this._defaultWidth = 1;
this._defaultHeight = 1;
this._defaultDepth = 1;
this._pixiObject = new PIXI.Graphics();
this._pixiContainer.addChild(this._pixiObject);
this._faceResourceNames = [
properties.get('frontFaceResourceName').getValue(),
properties.get('backFaceResourceName').getValue(),
properties.get('leftFaceResourceName').getValue(),
properties.get('rightFaceResourceName').getValue(),
properties.get('topFaceResourceName').getValue(),
properties.get('bottomFaceResourceName').getValue(),
];
this._faceVisibilities = [
properties.get('frontFaceVisible').getValue() === 'true',
properties.get('backFaceVisible').getValue() === 'true',
properties.get('leftFaceVisible').getValue() === 'true',
properties.get('rightFaceVisible').getValue() === 'true',
properties.get('topFaceVisible').getValue() === 'true',
properties.get('bottomFaceVisible').getValue() === 'true',
];
this._shouldRepeatTextureOnFace = [
properties.get('frontFaceResourceRepeat').getValue() === 'true',
properties.get('backFaceResourceRepeat').getValue() === 'true',
properties.get('leftFaceResourceRepeat').getValue() === 'true',
properties.get('rightFaceResourceRepeat').getValue() === 'true',
properties.get('topFaceResourceRepeat').getValue() === 'true',
properties.get('bottomFaceResourceRepeat').getValue() === 'true',
];
this._facesOrientation = properties.get('facesOrientation').getValue();
this._backFaceUpThroughWhichAxisRotation = properties
.get('backFaceUpThroughWhichAxisRotation')
.getValue();
this._shouldUseTransparentTexture =
properties.get('enableTextureTransparency').getValue() === 'true';
this._faceResourceNames = ['', '', '', '', '', ''];
this._faceVisibilities = [true, true, true, true, true, true];
this._shouldRepeatTextureOnFace = [true, true, true, true, true, true];
this._facesOrientation = 'Y';
this._backFaceUpThroughWhichAxisRotation = 'X';
this._shouldUseTransparentTexture = false;
const geometry = new THREE.BoxGeometry(1, 1, 1);
const materials = [
this._getFaceMaterial(project, materialIndexToFaceIndex[0]),
this._getFaceMaterial(project, materialIndexToFaceIndex[1]),
this._getFaceMaterial(project, materialIndexToFaceIndex[2]),
this._getFaceMaterial(project, materialIndexToFaceIndex[3]),
this._getFaceMaterial(project, materialIndexToFaceIndex[4]),
this._getFaceMaterial(project, materialIndexToFaceIndex[5]),
getTransparentMaterial(),
getTransparentMaterial(),
getTransparentMaterial(),
getTransparentMaterial(),
getTransparentMaterial(),
getTransparentMaterial(),
];
this._threeObject = new THREE.Mesh(geometry, materials);
this._threeObject.rotation.order = 'ZYX';
this._threeGroup.add(this._threeObject);
this.updateThreeObject();
}
_getFaceMaterial(project, faceIndex) {
if (!this._faceVisibilities[faceIndex]) return getTransparentMaterial();
async _updateThreeObjectMaterials() {
const getFaceMaterial = async (project, faceIndex) => {
if (!this._faceVisibilities[faceIndex])
return getTransparentMaterial();
return this._pixiResourcesLoader.getThreeMaterial(
project,
this._faceResourceNames[faceIndex],
{
useTransparentTexture: this._shouldUseTransparentTexture,
}
);
return await this._pixiResourcesLoader.getThreeMaterial(
project,
this._faceResourceNames[faceIndex],
{
useTransparentTexture: this._shouldUseTransparentTexture,
}
);
};
const materials = await Promise.all([
getFaceMaterial(this._project, materialIndexToFaceIndex[0]),
getFaceMaterial(this._project, materialIndexToFaceIndex[1]),
getFaceMaterial(this._project, materialIndexToFaceIndex[2]),
getFaceMaterial(this._project, materialIndexToFaceIndex[3]),
getFaceMaterial(this._project, materialIndexToFaceIndex[4]),
getFaceMaterial(this._project, materialIndexToFaceIndex[5]),
]);
this._threeObject.material[0] = materials[0];
this._threeObject.material[1] = materials[1];
this._threeObject.material[2] = materials[2];
this._threeObject.material[3] = materials[3];
this._threeObject.material[4] = materials[4];
this._threeObject.material[5] = materials[5];
this._updateTextureUvMapping();
}
static _getResourceNameToDisplay(objectConfiguration) {
@@ -2386,6 +2391,15 @@ module.exports = {
}
updateThreeObject() {
const object = gd.castObject(
this._associatedObjectConfiguration,
gd.ObjectJsImplementation
);
this._defaultWidth = object.content.width;
this._defaultHeight = object.content.height;
this._defaultDepth = object.content.depth;
const width = this.getWidth();
const height = this.getHeight();
const depth = this.getDepth();
@@ -2402,18 +2416,107 @@ module.exports = {
RenderedInstance.toRad(this._instance.getAngle())
);
let materialsDirty = false;
let uvMappingDirty = false;
const shouldUseTransparentTexture =
object.content.enableTextureTransparency;
if (this._shouldUseTransparentTexture !== shouldUseTransparentTexture) {
this._shouldUseTransparentTexture = shouldUseTransparentTexture;
materialsDirty = true;
}
const faceResourceNames = [
object.content.frontFaceResourceName,
object.content.backFaceResourceName,
object.content.leftFaceResourceName,
object.content.rightFaceResourceName,
object.content.topFaceResourceName,
object.content.bottomFaceResourceName,
];
if (
this._faceResourceNames[0] !== faceResourceNames[0] ||
this._faceResourceNames[1] !== faceResourceNames[1] ||
this._faceResourceNames[2] !== faceResourceNames[2] ||
this._faceResourceNames[3] !== faceResourceNames[3] ||
this._faceResourceNames[4] !== faceResourceNames[4] ||
this._faceResourceNames[5] !== faceResourceNames[5]
) {
this._faceResourceNames = faceResourceNames;
materialsDirty = true;
}
const faceVisibilities = [
object.content.frontFaceVisible,
object.content.backFaceVisible,
object.content.leftFaceVisible,
object.content.rightFaceVisible,
object.content.topFaceVisible,
object.content.bottomFaceVisible,
];
if (
this._faceVisibilities[0] !== faceVisibilities[0] ||
this._faceVisibilities[1] !== faceVisibilities[1] ||
this._faceVisibilities[2] !== faceVisibilities[2] ||
this._faceVisibilities[3] !== faceVisibilities[3] ||
this._faceVisibilities[4] !== faceVisibilities[4] ||
this._faceVisibilities[5] !== faceVisibilities[5]
) {
this._faceVisibilities = faceVisibilities;
materialsDirty = true;
uvMappingDirty = true;
}
const shouldRepeatTextureOnFace = [
object.content.frontFaceResourceRepeat,
object.content.backFaceResourceRepeat,
object.content.leftFaceResourceRepeat,
object.content.rightFaceResourceRepeat,
object.content.topFaceResourceRepeat,
object.content.bottomFaceResourceRepeat,
];
if (
this._shouldRepeatTextureOnFace[0] !== shouldRepeatTextureOnFace[0] ||
this._shouldRepeatTextureOnFace[1] !== shouldRepeatTextureOnFace[1] ||
this._shouldRepeatTextureOnFace[2] !== shouldRepeatTextureOnFace[2] ||
this._shouldRepeatTextureOnFace[3] !== shouldRepeatTextureOnFace[3] ||
this._shouldRepeatTextureOnFace[4] !== shouldRepeatTextureOnFace[4] ||
this._shouldRepeatTextureOnFace[5] !== shouldRepeatTextureOnFace[5]
) {
this._shouldRepeatTextureOnFace = shouldRepeatTextureOnFace;
uvMappingDirty = true;
}
const backFaceUpThroughWhichAxisRotation =
object.content.backFaceUpThroughWhichAxisRotation;
if (
backFaceUpThroughWhichAxisRotation !==
this._backFaceUpThroughWhichAxisRotation
) {
this._backFaceUpThroughWhichAxisRotation = backFaceUpThroughWhichAxisRotation;
uvMappingDirty = true;
}
const facesOrientation = object.content.facesOrientation;
if (facesOrientation !== this._facesOrientation) {
this._facesOrientation = facesOrientation;
uvMappingDirty = true;
}
const scaleX = width * (this._instance.isFlippedX() ? -1 : 1);
const scaleY = height * (this._instance.isFlippedY() ? -1 : 1);
const scaleZ = depth * (this._instance.isFlippedZ() ? -1 : 1);
if (
scaleX !== this._threeObject.scale.width ||
scaleY !== this._threeObject.scale.height ||
scaleZ !== this._threeObject.scale.depth
scaleX !== this._threeObject.scale.x ||
scaleY !== this._threeObject.scale.y ||
scaleZ !== this._threeObject.scale.z
) {
this._threeObject.scale.set(scaleX, scaleY, scaleZ);
this.updateTextureUvMapping();
uvMappingDirty = true;
}
if (materialsDirty) this._updateThreeObjectMaterials();
if (uvMappingDirty) this._updateTextureUvMapping();
}
/**
@@ -2422,7 +2525,7 @@ module.exports = {
* The mesh must be configured with a list of materials in order
* for the method to work.
*/
updateTextureUvMapping() {
_updateTextureUvMapping() {
// @ts-ignore - position is stored as a Float32BufferAttribute
/** @type {THREE.BufferAttribute} */
const pos = this._threeObject.geometry.getAttribute('position');
@@ -2696,25 +2799,23 @@ module.exports = {
pixiContainer,
pixiResourcesLoader
);
const properties = associatedObjectConfiguration.getProperties();
this._defaultWidth = parseFloat(properties.get('width').getValue());
this._defaultHeight = parseFloat(properties.get('height').getValue());
this._defaultDepth = parseFloat(properties.get('depth').getValue());
const rotationX = parseFloat(properties.get('rotationX').getValue());
const rotationY = parseFloat(properties.get('rotationY').getValue());
const rotationZ = parseFloat(properties.get('rotationZ').getValue());
const keepAspectRatio =
properties.get('keepAspectRatio').getValue() === 'true';
const modelResourceName = properties
.get('modelResourceName')
.getValue();
this._originPoint = getPointForLocation(
properties.get('originLocation').getValue()
);
this._centerPoint = getPointForLocation(
properties.get('centerLocation').getValue()
const object = gd.castObject(
this._associatedObjectConfiguration,
gd.Model3DObjectConfiguration
);
this._defaultWidth = object.getWidth();
this._defaultHeight = object.getHeight();
this._defaultDepth = object.getDepth();
const rotationX = object.getRotationX();
const rotationY = object.getRotationY();
const rotationZ = object.getRotationZ();
const keepAspectRatio = object.shouldKeepAspectRatio();
const modelResourceName = object.getModelResourceName();
this._originPoint = getPointForLocation(object.getOriginLocation());
this._centerPoint = getPointForLocation(object.getCenterLocation());
// This renderer shows a placeholder for the object:
this._pixiObject = new PIXI.Graphics();
this._pixiContainer.addChild(this._pixiObject);
@@ -2923,6 +3024,17 @@ module.exports = {
}
}
const isSamePoint = (point1, point2) => {
if (!point1 && !point2) return true;
if (point1 && !point2) return false;
if (!point1 && point2) return false;
return (
point1[0] === point2[0] &&
point1[1] === point2[1] &&
point1[2] === point2[2]
);
};
const getPointForLocation = (location) => {
switch (location) {
case 'ModelOrigin':
@@ -2959,24 +3071,20 @@ module.exports = {
threeGroup,
pixiResourcesLoader
);
const properties = associatedObjectConfiguration.getProperties();
this._defaultWidth = parseFloat(properties.get('width').getValue());
this._defaultHeight = parseFloat(properties.get('height').getValue());
this._defaultDepth = parseFloat(properties.get('depth').getValue());
const rotationX = parseFloat(properties.get('rotationX').getValue());
const rotationY = parseFloat(properties.get('rotationY').getValue());
const rotationZ = parseFloat(properties.get('rotationZ').getValue());
const keepAspectRatio =
properties.get('keepAspectRatio').getValue() === 'true';
const modelResourceName = properties
.get('modelResourceName')
.getValue();
this._originPoint = getPointForLocation(
properties.get('originLocation').getValue()
);
this._centerPoint = getPointForLocation(
properties.get('centerLocation').getValue()
);
this._defaultWidth = 1;
this._defaultHeight = 1;
this._defaultDepth = 1;
this._originalWidth = 1;
this._originalHeight = 1;
this._originalDepth = 1;
this._rotationX = 0;
this._rotationY = 0;
this._rotationZ = 0;
this._keepAspectRatio = false;
this._originPoint = null;
this._centerPoint = null;
this._pixiObject = new PIXI.Graphics();
this._pixiContainer.addChild(this._pixiObject);
@@ -2985,28 +3093,8 @@ module.exports = {
this._threeObject.rotation.order = 'ZYX';
this._threeGroup.add(this._threeObject);
this._pixiResourcesLoader
.get3DModel(project, modelResourceName)
.then((model3d) => {
const clonedModel3D = THREE_ADDONS.SkeletonUtils.clone(
model3d.scene
);
// This group hold the rotation defined by properties.
const threeObject = new THREE.Group();
threeObject.rotation.order = 'ZYX';
threeObject.add(clonedModel3D);
this._updateDefaultTransformation(
threeObject,
rotationX,
rotationY,
rotationZ,
this._defaultWidth,
this._defaultHeight,
this._defaultDepth,
keepAspectRatio
);
this._threeObject.add(threeObject);
});
this._threeModelGroup = null;
this._clonedModel3D = null;
}
getOriginX() {
@@ -3047,23 +3135,28 @@ module.exports = {
return this._centerPoint || this._modelOriginPoint;
}
_updateDefaultTransformation(
threeObject,
rotationX,
rotationY,
rotationZ,
originalWidth,
originalHeight,
originalDepth,
keepAspectRatio
) {
threeObject.rotation.set(
(rotationX * Math.PI) / 180,
(rotationY * Math.PI) / 180,
(rotationZ * Math.PI) / 180
_updateDefaultTransformation() {
if (!this._clonedModel3D) return; // Model is not ready - nothing to do.
if (this._threeModelGroup) {
// Remove any previous container as we will recreate it just below
this._threeObject.clear();
}
// This group hold the rotation defined by properties.
// Always restart from a new group to avoid miscomputing bounding boxes/sizes.
this._threeModelGroup = new THREE.Group();
this._threeModelGroup.rotation.order = 'ZYX';
this._threeModelGroup.add(this._clonedModel3D);
this._threeModelGroup.rotation.set(
(this._rotationX * Math.PI) / 180,
(this._rotationY * Math.PI) / 180,
(this._rotationZ * Math.PI) / 180
);
this._threeModelGroup.updateMatrixWorld(true);
const boundingBox = new THREE.Box3().setFromObject(
this._threeModelGroup
);
threeObject.updateMatrixWorld(true);
const boundingBox = new THREE.Box3().setFromObject(threeObject);
const shouldKeepModelOrigin = !this._originPoint;
if (shouldKeepModelOrigin) {
@@ -3090,7 +3183,7 @@ module.exports = {
// Center the model.
const centerPoint = this._centerPoint;
if (centerPoint) {
threeObject.position.set(
this._threeModelGroup.position.set(
-(boundingBox.min.x + modelWidth * centerPoint[0]),
// The model is flipped on Y axis.
-(boundingBox.min.y + modelHeight * (1 - centerPoint[1])),
@@ -3099,11 +3192,11 @@ module.exports = {
}
// Rotate the model.
threeObject.scale.set(1, 1, 1);
threeObject.rotation.set(
(rotationX * Math.PI) / 180,
(rotationY * Math.PI) / 180,
(rotationZ * Math.PI) / 180
this._threeModelGroup.scale.set(1, 1, 1);
this._threeModelGroup.rotation.set(
(this._rotationX * Math.PI) / 180,
(this._rotationY * Math.PI) / 180,
(this._rotationZ * Math.PI) / 180
);
// Stretch the model in a 1x1x1 cube.
@@ -3115,23 +3208,23 @@ module.exports = {
// Flip on Y because the Y axis is on the opposite side of direct basis.
// It avoids models to be like a mirror refection.
scaleMatrix.makeScale(scaleX, -scaleY, scaleZ);
threeObject.updateMatrix();
threeObject.applyMatrix4(scaleMatrix);
this._threeModelGroup.updateMatrix();
this._threeModelGroup.applyMatrix4(scaleMatrix);
if (keepAspectRatio) {
if (this._keepAspectRatio) {
// Reduce the object dimensions to keep aspect ratio.
const widthRatio =
modelWidth < epsilon
? Number.POSITIVE_INFINITY
: originalWidth / modelWidth;
: this._originalWidth / modelWidth;
const heightRatio =
modelHeight < epsilon
? Number.POSITIVE_INFINITY
: originalHeight / modelHeight;
: this._originalHeight / modelHeight;
const depthRatio =
modelDepth < epsilon
? Number.POSITIVE_INFINITY
: originalDepth / modelDepth;
: this._originalDepth / modelDepth;
const minScaleRatio = Math.min(widthRatio, heightRatio, depthRatio);
if (!Number.isFinite(minScaleRatio)) {
this._defaultWidth = modelWidth;
@@ -3139,48 +3232,124 @@ module.exports = {
this._defaultDepth = modelDepth;
} else {
if (widthRatio === minScaleRatio) {
this._defaultWidth = originalWidth;
this._defaultWidth = this._originalWidth;
this._defaultHeight = Rendered3DInstance.applyRatio({
oldReferenceValue: modelWidth,
newReferenceValue: originalWidth,
newReferenceValue: this._originalWidth,
valueToApplyTo: modelHeight,
});
this._defaultDepth = Rendered3DInstance.applyRatio({
oldReferenceValue: modelWidth,
newReferenceValue: originalWidth,
newReferenceValue: this._originalWidth,
valueToApplyTo: modelDepth,
});
} else if (heightRatio === minScaleRatio) {
this._defaultWidth = Rendered3DInstance.applyRatio({
oldReferenceValue: modelHeight,
newReferenceValue: originalHeight,
newReferenceValue: this._originalHeight,
valueToApplyTo: modelWidth,
});
this._defaultHeight = originalHeight;
this._defaultHeight = this._originalHeight;
this._defaultDepth = Rendered3DInstance.applyRatio({
oldReferenceValue: modelHeight,
newReferenceValue: originalHeight,
newReferenceValue: this._originalHeight,
valueToApplyTo: modelDepth,
});
} else {
this._defaultWidth = Rendered3DInstance.applyRatio({
oldReferenceValue: modelDepth,
newReferenceValue: originalDepth,
newReferenceValue: this._originalDepth,
valueToApplyTo: modelWidth,
});
this._defaultHeight = Rendered3DInstance.applyRatio({
oldReferenceValue: modelDepth,
newReferenceValue: originalDepth,
newReferenceValue: this._originalDepth,
valueToApplyTo: modelHeight,
});
this._defaultDepth = originalDepth;
this._defaultDepth = this._originalDepth;
}
}
}
this._threeObject.add(this._threeModelGroup);
}
updateThreeObject() {
const object = gd.castObject(
this._associatedObjectConfiguration,
gd.Model3DObjectConfiguration
);
let defaultTransformationDirty = false;
const originalWidth = object.getWidth();
const originalHeight = object.getHeight();
const originalDepth = object.getDepth();
if (
this._originalWidth !== originalWidth ||
this._originalHeight !== originalHeight ||
this._originalDepth !== originalDepth
) {
this._originalWidth = originalWidth;
this._originalHeight = originalHeight;
this._originalDepth = originalDepth;
defaultTransformationDirty = true;
}
const rotationX = object.getRotationX();
const rotationY = object.getRotationY();
const rotationZ = object.getRotationZ();
if (
this._rotationX !== rotationX ||
this._rotationY !== rotationY ||
this._rotationZ !== rotationZ
) {
this._rotationX = rotationX;
this._rotationY = rotationY;
this._rotationZ = rotationZ;
defaultTransformationDirty = true;
}
const keepAspectRatio = object.shouldKeepAspectRatio();
if (this._keepAspectRatio !== keepAspectRatio) {
this._keepAspectRatio = keepAspectRatio;
defaultTransformationDirty = true;
}
const originPoint = getPointForLocation(object.getOriginLocation());
if (!isSamePoint(originPoint, this._originPoint)) {
this._originPoint = originPoint;
defaultTransformationDirty = true;
}
const centerPoint = getPointForLocation(object.getCenterLocation());
if (!isSamePoint(centerPoint, this._centerPoint)) {
this._centerPoint = centerPoint;
defaultTransformationDirty = true;
}
if (defaultTransformationDirty) this._updateDefaultTransformation();
const modelResourceName = object.getModelResourceName();
if (this._modelResourceName !== modelResourceName) {
this._modelResourceName = modelResourceName;
this._pixiResourcesLoader
.get3DModel(this._project, modelResourceName)
.then((model3d) => {
this._clonedModel3D = THREE_ADDONS.SkeletonUtils.clone(
model3d.scene
);
this._updateDefaultTransformation();
});
}
this._updateThreeObjectPosition();
}
_updateThreeObjectPosition() {
const width = this.getWidth();
const height = this.getHeight();
const depth = this.getDepth();
@@ -3204,9 +3373,9 @@ module.exports = {
const scaleZ = depth * (this._instance.isFlippedZ() ? -1 : 1);
if (
scaleX !== this._threeObject.scale.width ||
scaleY !== this._threeObject.scale.height ||
scaleZ !== this._threeObject.scale.depth
scaleX !== this._threeObject.scale.x ||
scaleY !== this._threeObject.scale.y ||
scaleZ !== this._threeObject.scale.z
) {
this._threeObject.scale.set(scaleX, scaleY, scaleZ);
}

View File

@@ -109,23 +109,26 @@ Model3DObjectConfiguration::GetProperties() const {
objectProperties["rotationX"]
.SetValue(gd::String::From(rotationX))
.SetType("number")
.SetLabel(_("Rotation around X axis"))
.SetLabel(_("X"))
.SetDescription(_("Rotation around X axis"))
.SetMeasurementUnit(gd::MeasurementUnit::GetDegreeAngle())
.SetGroup(_("Default orientation"));
.SetGroup(_("Default rotation"));
objectProperties["rotationY"]
.SetValue(gd::String::From(rotationY))
.SetType("number")
.SetLabel(_("Rotation around Y axis"))
.SetLabel(_("Y"))
.SetDescription(_("Rotation around Y axis"))
.SetMeasurementUnit(gd::MeasurementUnit::GetDegreeAngle())
.SetGroup(_("Default orientation"));
.SetGroup(_("Default rotation"));
objectProperties["rotationZ"]
.SetValue(gd::String::From(rotationZ))
.SetType("number")
.SetLabel(_("Rotation around Z axis"))
.SetLabel(_("Z"))
.SetDescription(_("Rotation around Z axis"))
.SetMeasurementUnit(gd::MeasurementUnit::GetDegreeAngle())
.SetGroup(_("Default orientation"));
.SetGroup(_("Default rotation"));
objectProperties["modelResourceName"]
.SetValue(modelResourceName)
@@ -139,7 +142,7 @@ Model3DObjectConfiguration::GetProperties() const {
.AddExtraInfo("Basic")
.AddExtraInfo("StandardWithoutMetalness")
.AddExtraInfo("KeepOriginal")
.SetLabel(_("Material modifier"));
.SetLabel(_("Material"));
objectProperties["originLocation"]
.SetValue(originLocation.empty() ? "TopLeft" : originLocation)
@@ -149,7 +152,9 @@ Model3DObjectConfiguration::GetProperties() const {
.AddExtraInfo("ObjectCenter")
.AddExtraInfo("BottomCenterZ")
.AddExtraInfo("BottomCenterY")
.SetLabel(_("Origin point"));
.SetLabel(_("Origin point"))
.SetGroup(_("Points"))
.SetAdvanced(true);
objectProperties["centerLocation"]
.SetValue(centerLocation.empty() ? "ObjectCenter" : centerLocation)
@@ -158,7 +163,9 @@ Model3DObjectConfiguration::GetProperties() const {
.AddExtraInfo("ObjectCenter")
.AddExtraInfo("BottomCenterZ")
.AddExtraInfo("BottomCenterY")
.SetLabel(_("Center point"));
.SetLabel(_("Center point"))
.SetGroup(_("Points"))
.SetAdvanced(true);
return objectProperties;
}

View File

@@ -140,7 +140,25 @@ public:
const std::vector<Model3DAnimation> &GetAllAnimations() const {
return animations;
}
///@}
/** \name Getters
* Fast access for rendering instances.
*/
///@{
double GetWidth() const { return width; };
double GetHeight() const { return height; };
double GetDepth() const { return depth; };
double GetRotationX() const { return rotationX; };
double GetRotationY() const { return rotationY; };
double GetRotationZ() const { return rotationZ; };
const gd::String& GetModelResourceName() const { return modelResourceName; };
const gd::String& GetMaterialType() const { return materialType; };
const gd::String& GetOriginLocation() const { return originLocation; };
const gd::String& GetCenterLocation() const { return centerLocation; };
bool shouldKeepAspectRatio() const { return keepAspectRatio; };
///@}
protected:

View File

@@ -75,7 +75,7 @@ std::map<gd::String, gd::PropertyDescriptor> AnchorBehavior::GetProperties(
.AddExtraInfo(_("Window center"))
.AddExtraInfo(_("Window right"))
.AddExtraInfo(_("Proportional"))
.SetLabel(_("Left edge anchor"))
.SetLabel(_("Left edge"))
.SetDescription(_("Anchor the left edge of the object on X axis."));
properties["rightEdgeAnchor"]
@@ -87,7 +87,7 @@ std::map<gd::String, gd::PropertyDescriptor> AnchorBehavior::GetProperties(
.AddExtraInfo(_("Window center"))
.AddExtraInfo(_("Window right"))
.AddExtraInfo(_("Proportional"))
.SetLabel(_("Right edge anchor"))
.SetLabel(_("Right edge"))
.SetDescription(_("Anchor the right edge of the object on X axis."));
properties["topEdgeAnchor"]
@@ -99,7 +99,7 @@ std::map<gd::String, gd::PropertyDescriptor> AnchorBehavior::GetProperties(
.AddExtraInfo(_("Window center"))
.AddExtraInfo(_("Window bottom"))
.AddExtraInfo(_("Proportional"))
.SetLabel(_("Top edge anchor"))
.SetLabel(_("Top edge"))
.SetDescription(_("Anchor the top edge of the object on Y axis."));
properties["bottomEdgeAnchor"]
@@ -111,7 +111,7 @@ std::map<gd::String, gd::PropertyDescriptor> AnchorBehavior::GetProperties(
.AddExtraInfo(_("Window center"))
.AddExtraInfo(_("Window bottom"))
.AddExtraInfo(_("Proportional"))
.SetLabel(_("Bottom edge anchor"))
.SetLabel(_("Bottom edge"))
.SetDescription(_("Anchor the bottom edge of the object on Y axis."));
properties["useLegacyBottomAndRightAnchors"]
@@ -119,12 +119,12 @@ std::map<gd::String, gd::PropertyDescriptor> AnchorBehavior::GetProperties(
"Stretch object when anchoring right or bottom edge (deprecated, "
"it's recommended to leave this unchecked and anchor both sides if "
"you want Sprite to stretch instead.)"))
.SetGroup(_("Deprecated options (advanced)"))
.SetValue(behaviorContent.GetBoolAttribute(
"useLegacyBottomAndRightAnchors", true)
? "true"
: "false")
.SetType("Boolean");
.SetType("Boolean")
.SetDeprecated(true);
return properties;
}

View File

@@ -34,11 +34,8 @@ module.exports = {
.setIcon('JsPlatform/Extensions/bbcode32.png');
var objectBBText = new gd.ObjectJsImplementation();
objectBBText.updateProperty = function (
objectContent,
propertyName,
newValue
) {
objectBBText.updateProperty = function (propertyName, newValue) {
const objectContent = this.content;
if (propertyName in objectContent) {
if (typeof objectContent[propertyName] === 'boolean')
objectContent[propertyName] = newValue === '1';
@@ -50,8 +47,9 @@ module.exports = {
return false;
};
objectBBText.getProperties = function (objectContent) {
objectBBText.getProperties = function () {
const objectProperties = new gd.MapStringPropertyDescriptor();
const objectContent = this.content;
objectProperties
.getOrCreate('text')
@@ -107,29 +105,26 @@ module.exports = {
return objectProperties;
};
objectBBText.setRawJSONContent(
JSON.stringify({
text:
'[b]bold[/b] [i]italic[/i] [size=15]smaller[/size] [font=times]times[/font] font\n[spacing=12]spaced out[/spacing]\n[outline=yellow]outlined[/outline] [shadow=red]DropShadow[/shadow] ',
opacity: 255,
fontSize: 20,
visible: true,
color: '0;0;0',
fontFamily: 'Arial',
align: 'left',
wordWrap: true,
})
);
objectBBText.content = {
text:
'[b]bold[/b] [i]italic[/i] [size=15]smaller[/size] [font=times]times[/font] font\n[spacing=12]spaced out[/spacing]\n[outline=yellow]outlined[/outline] [shadow=red]DropShadow[/shadow] ',
opacity: 255,
fontSize: 20,
visible: true,
color: '0;0;0',
fontFamily: 'Arial',
align: 'left',
wordWrap: true,
};
objectBBText.updateInitialInstanceProperty = function (
objectContent,
instance,
propertyName,
newValue
) {
return false;
};
objectBBText.getInitialInstanceProperties = function (content, instance) {
objectBBText.getInitialInstanceProperties = function (instance) {
var instanceProperties = new gd.MapStringPropertyDescriptor();
return instanceProperties;
};
@@ -531,22 +526,33 @@ module.exports = {
* This is called to update the PIXI object on the scene editor
*/
update() {
const properties = this._associatedObjectConfiguration.getProperties();
const object = gd.castObject(
this._associatedObjectConfiguration,
gd.ObjectJsImplementation
);
const rawText = properties.get('text').getValue();
const rawText = object.content.text;
if (rawText !== this._pixiObject.text) {
this._pixiObject.text = rawText;
}
const color = properties.get('color').getValue();
this._pixiObject.textStyles.default.fill = objectsRenderingService.rgbOrHexToHexNumber(
color
);
const color = object.content.color;
const newColor = objectsRenderingService.rgbOrHexToHexNumber(color);
if (newColor !== this._pixiObject.textStyles.default.fill) {
this._pixiObject.textStyles.default.fill = newColor;
this._pixiObject.dirty = true;
}
const fontSize = properties.get('fontSize').getValue();
this._pixiObject.textStyles.default.fontSize = `${fontSize}px`;
const fontSize = object.content.fontSize;
const newDefaultFontsize = `${fontSize}px`;
if (
newDefaultFontsize !== this._pixiObject.textStyles.default.fontSize
) {
this._pixiObject.textStyles.default.fontSize = `${fontSize}px`;
this._pixiObject.dirty = true;
}
const fontResourceName = properties.get('fontFamily').getValue();
const fontResourceName = object.content.fontFamily;
if (this._fontResourceName !== fontResourceName) {
this._fontResourceName = fontResourceName;
@@ -567,13 +573,13 @@ module.exports = {
});
}
const wordWrap = properties.get('wordWrap').getValue() === 'true';
const wordWrap = object.content.wordWrap;
if (wordWrap !== this._pixiObject._style.wordWrap) {
this._pixiObject._style.wordWrap = wordWrap;
this._pixiObject.dirty = true;
}
const align = properties.get('align').getValue();
const align = object.content.align;
if (align !== this._pixiObject._style.align) {
this._pixiObject._style.align = align;
this._pixiObject.dirty = true;

View File

@@ -34,11 +34,8 @@ module.exports = {
.setIcon('JsPlatform/Extensions/bitmapfont32.png');
const bitmapTextObject = new gd.ObjectJsImplementation();
bitmapTextObject.updateProperty = function (
objectContent,
propertyName,
newValue
) {
bitmapTextObject.updateProperty = function (propertyName, newValue) {
const objectContent = this.content;
if (propertyName in objectContent) {
if (typeof objectContent[propertyName] === 'boolean')
objectContent[propertyName] = newValue === '1';
@@ -50,8 +47,9 @@ module.exports = {
return false;
};
bitmapTextObject.getProperties = function (objectContent) {
bitmapTextObject.getProperties = function () {
const objectProperties = new gd.MapStringPropertyDescriptor();
const objectContent = this.content;
objectProperties
.getOrCreate('text')
@@ -66,7 +64,7 @@ module.exports = {
.addExtraInfo('left')
.addExtraInfo('center')
.addExtraInfo('right')
.setLabel(_('Alignment, when multiple lines are displayed'))
.setLabel(_('Alignment'))
.setGroup(_('Appearance'));
objectProperties
@@ -82,7 +80,7 @@ module.exports = {
.setValue(objectContent.textureAtlasResourceName)
.setType('resource')
.addExtraInfo('image')
.setLabel(_('Bitmap atlas image'))
.setLabel(_('Bitmap Atlas'))
.setGroup(_('Font'));
objectProperties
@@ -108,33 +106,27 @@ module.exports = {
return objectProperties;
};
bitmapTextObject.setRawJSONContent(
JSON.stringify({
text:
'This text use the default bitmap font.\nUse a custom Bitmap Font to create your own texts.',
opacity: 255,
scale: 1,
fontSize: 20,
tint: '255;255;255',
bitmapFontResourceName: '',
textureAtlasResourceName: '',
align: 'left',
wordWrap: true,
})
);
bitmapTextObject.content = {
text:
'This text use the default bitmap font.\nUse a custom Bitmap Font to create your own texts.',
opacity: 255,
scale: 1,
fontSize: 20,
tint: '255;255;255',
bitmapFontResourceName: '',
textureAtlasResourceName: '',
align: 'left',
wordWrap: true,
};
bitmapTextObject.updateInitialInstanceProperty = function (
objectContent,
instance,
propertyName,
newValue
) {
return false;
};
bitmapTextObject.getInitialInstanceProperties = function (
content,
instance
) {
bitmapTextObject.getInitialInstanceProperties = function (instance) {
var instanceProperties = new gd.MapStringPropertyDescriptor();
return instanceProperties;
};
@@ -659,31 +651,31 @@ module.exports = {
}
update() {
const properties = this._associatedObjectConfiguration.getProperties();
const object = gd.castObject(
this._associatedObjectConfiguration,
gd.ObjectJsImplementation
);
// Update the rendered text properties (note: Pixi is only
// applying changes if there were changed).
const rawText = properties.get('text').getValue();
const rawText = object.content.text;
this._pixiObject.text = rawText;
const align = properties.get('align').getValue();
const align = object.content.align;
this._pixiObject.align = align;
const color = properties.get('tint').getValue();
const color = object.content.tint;
this._pixiObject.tint = objectsRenderingService.rgbOrHexToHexNumber(
color
);
const scale = +(properties.get('scale').getValue() || 1);
const scale = object.content.scale;
this._pixiObject.scale.set(scale);
// Track the changes in font to load the new requested font.
const bitmapFontResourceName = properties
.get('bitmapFontResourceName')
.getValue();
const textureAtlasResourceName = properties
.get('textureAtlasResourceName')
.getValue();
const bitmapFontResourceName = object.content.bitmapFontResourceName;
const textureAtlasResourceName =
object.content.textureAtlasResourceName;
if (
this._currentBitmapFontResourceName !== bitmapFontResourceName ||
@@ -712,7 +704,7 @@ module.exports = {
}
// Set up the wrapping width if enabled.
const wordWrap = properties.get('wordWrap').getValue() === 'true';
const wordWrap = object.content.wordWrap;
if (wordWrap && this._instance.hasCustomSize()) {
this._pixiObject.maxWidth =
this.getCustomWidth() / this._pixiObject.scale.x;

View File

@@ -26,7 +26,8 @@ DestroyOutsideBehavior::GetProperties(
behaviorContent.GetDoubleAttribute("extraBorder", 0)))
.SetType("Number")
.SetMeasurementUnit(gd::MeasurementUnit::GetPixel())
.SetLabel(_("Margin before deleting the object, in pixels"));
.SetLabel(_("Deletion margin"))
.SetDescription(_("Margin before deleting the object, in pixels"));
return properties;
}

View File

@@ -26,7 +26,7 @@ std::map<gd::String, gd::PropertyDescriptor> DraggableBehavior::GetProperties(
? "true"
: "false")
.SetType("Boolean")
.SetLabel(_("Do a precision check against the object's collision mask"))
.SetLabel(_("Precise check"))
.SetDescription(
_("Use the object (custom) collision mask instead of the bounding "
"box, making the behavior more precise at the cost of "

View File

@@ -287,11 +287,9 @@ module.exports = {
// Everything that is stored inside the object is in "content" and is automatically
// saved/loaded to JSON.
var dummyObject = new gd.ObjectJsImplementation();
dummyObject.updateProperty = function (
objectContent,
propertyName,
newValue
) {
dummyObject.updateProperty = function (propertyName, newValue) {
const objectContent = this.content;
if (propertyName === 'My first property') {
objectContent.property1 = newValue;
return true;
@@ -311,8 +309,9 @@ module.exports = {
return false;
};
dummyObject.getProperties = function (objectContent) {
dummyObject.getProperties = function () {
var objectProperties = new gd.MapStringPropertyDescriptor();
const objectContent = this.content;
objectProperties
.getOrCreate('My first property')
@@ -336,17 +335,14 @@ module.exports = {
return objectProperties;
};
dummyObject.setRawJSONContent(
JSON.stringify({
property1: 'Hello world',
property2: true,
property3: 123,
myImage: '',
})
);
dummyObject.content = {
property1: 'Hello world',
property2: true,
property3: 123,
myImage: '',
};
dummyObject.updateInitialInstanceProperty = function (
objectContent,
instance,
propertyName,
newValue
@@ -362,7 +358,7 @@ module.exports = {
return false;
};
dummyObject.getInitialInstanceProperties = function (content, instance) {
dummyObject.getInitialInstanceProperties = function (instance) {
var instanceProperties = new gd.MapStringPropertyDescriptor();
instanceProperties
@@ -507,12 +503,13 @@ module.exports = {
* This is called to update the PIXI object on the scene editor
*/
update() {
const object = gd.castObject(
this._associatedObjectConfiguration,
gd.ObjectJsImplementation
);
// Read a property from the object
const property1Value = this._associatedObjectConfiguration
.getProperties()
.get('My first property')
.getValue();
this._pixiObject.text = property1Value;
this._pixiObject.text = object.content.property1;
// Read position and angle from the instance
this._pixiObject.position.x =

View File

@@ -68,11 +68,8 @@ module.exports = {
const lightObject = new gd.ObjectJsImplementation();
lightObject.updateProperty = function (
objectContent,
propertyName,
newValue
) {
lightObject.updateProperty = function (propertyName, newValue) {
const objectContent = this.content;
if (propertyName === 'radius') {
objectContent.radius = parseFloat(newValue);
return true;
@@ -96,8 +93,9 @@ module.exports = {
return false;
};
lightObject.getProperties = function (objectContent) {
lightObject.getProperties = function () {
const objectProperties = new gd.MapStringPropertyDescriptor();
const objectContent = this.content;
objectProperties.set(
'radius',
@@ -140,17 +138,14 @@ module.exports = {
return objectProperties;
};
lightObject.setRawJSONContent(
JSON.stringify({
radius: 50,
color: '255;255;255',
debugMode: false,
texture: '',
})
);
lightObject.content = {
radius: 50,
color: '255;255;255',
debugMode: false,
texture: '',
};
lightObject.updateInitialInstanceProperty = function (
objectContent,
instance,
propertyName,
newValue
@@ -158,7 +153,7 @@ module.exports = {
return false;
};
lightObject.getInitialInstanceProperties = function (content, instance) {
lightObject.getInitialInstanceProperties = function (instance) {
const instanceProperties = new gd.MapStringPropertyDescriptor();
return instanceProperties;
@@ -238,6 +233,10 @@ module.exports = {
* Renderer for instances of LightObject inside the IDE.
*/
class RenderedLightObjectInstance extends RenderedInstance {
_radius = 0;
_color = 0;
_radiusGraphics = null;
constructor(
project,
instance,
@@ -252,19 +251,6 @@ module.exports = {
pixiContainer,
pixiResourcesLoader
);
this._radius = parseFloat(
this._associatedObjectConfiguration
.getProperties()
.get('radius')
.getValue()
);
if (this._radius <= 0) this._radius = 1;
const color = objectsRenderingService.rgbOrHexToHexNumber(
this._associatedObjectConfiguration
.getProperties()
.get('color')
.getValue()
);
// The icon in the middle.
const lightIconSprite = new PIXI.Sprite(
@@ -274,18 +260,11 @@ module.exports = {
lightIconSprite.anchor.y = 0.5;
// The circle to show the radius of the light.
const radiusBorderWidth = 2;
const radiusGraphics = new PIXI.Graphics();
radiusGraphics.lineStyle(radiusBorderWidth, color, 0.8);
radiusGraphics.drawCircle(
0,
0,
Math.max(1, this._radius - radiusBorderWidth)
);
this._radiusGraphics = new PIXI.Graphics();
this._pixiObject = new PIXI.Container();
this._pixiObject.addChild(lightIconSprite);
this._pixiObject.addChild(radiusGraphics);
this._pixiObject.addChild(this._radiusGraphics);
this._pixiContainer.addChild(this._pixiObject);
this.update();
}
@@ -307,8 +286,41 @@ module.exports = {
* This is called to update the PIXI object on the scene editor
*/
update() {
const object = gd.castObject(
this._associatedObjectConfiguration,
gd.ObjectJsImplementation
);
this._pixiObject.position.x = this._instance.getX();
this._pixiObject.position.y = this._instance.getY();
let radiusGraphicsDirty = false;
let radius = object.content.radius;
if (radius <= 0) radius = 1;
if (radius !== this._radius) {
this._radius = radius;
radiusGraphicsDirty = true;
}
const color = objectsRenderingService.rgbOrHexToHexNumber(
object.content.color
);
if (color !== this._color) {
this._color = color;
radiusGraphicsDirty = true;
}
if (radiusGraphicsDirty) {
const radiusBorderWidth = 2;
this._radiusGraphics.clear();
this._radiusGraphics.lineStyle(radiusBorderWidth, color, 0.8);
this._radiusGraphics.drawCircle(
0,
0,
Math.max(1, this._radius - radiusBorderWidth)
);
}
}
/**

View File

@@ -33,12 +33,8 @@ ParticleEmitterBase::ParticleEmitterBase()
particleGravityY(0.0f),
particleLifeTimeMin(0.5f),
particleLifeTimeMax(2.5f),
particleRed1(255.0f),
particleRed2(255.0f),
particleGreen1(51),
particleGreen2(255),
particleBlue1(51),
particleBlue2(0.0f),
particleColor1("255;51;51"),
particleColor2("255;255;0"),
particleAlpha1(204),
particleAlpha2(0.0f),
particleSize1(100.0f),
@@ -57,6 +53,341 @@ ParticleEmitterBase::ParticleEmitterBase()
ParticleEmitterObject::ParticleEmitterObject() {}
bool ParticleEmitterObject::UpdateProperty(const gd::String& propertyName,
const gd::String& newValue) {
if (propertyName == "textureParticleName") {
SetParticleTexture(newValue);
return true;
}
if (propertyName == "rendererType") {
auto newRendererType = newValue == "Circle" ? Point
: newValue == "Line" ? Line
: Quad;
SetRendererType(newRendererType);
if (newRendererType != Quad) {
SetParticleTexture("");
}
return true;
}
if (propertyName == "particlesWidth") {
SetRendererParam1(newValue.To<double>());
return true;
}
if (propertyName == "particlesHeight") {
SetRendererParam2(newValue.To<double>());
return true;
}
if (propertyName == "lineLength") {
SetRendererParam1(newValue.To<double>());
return true;
}
if (propertyName == "lineThickness") {
SetRendererParam2(newValue.To<double>());
return true;
}
if (propertyName == "particlesSize") {
SetRendererParam1(newValue.To<double>());
return true;
}
if (propertyName == "particlesStartSize") {
SetParticleSize1(newValue.To<double>());
return true;
}
if (propertyName == "particlesEndSize") {
SetParticleSize2(newValue.To<double>());
return true;
}
if (propertyName == "particlesStartColor") {
SetParticleColor1(newValue);
return true;
}
if (propertyName == "particlesEndColor") {
SetParticleColor2(newValue);
return true;
}
if (propertyName == "particlesStartOpacity") {
SetParticleAlpha1(newValue.To<double>());
return true;
}
if (propertyName == "particlesEndOpacity") {
SetParticleAlpha2(newValue.To<double>());
return true;
}
if (propertyName == "additiveRendering") {
if (newValue == "1")
SetRenderingAdditive();
else
SetRenderingAlpha();
return true;
}
if (propertyName == "deleteWhenOutOfParticles") {
SetDestroyWhenNoParticles(newValue == "1");
return true;
}
if (propertyName == "maxParticlesCount") {
SetMaxParticleNb(newValue.To<double>());
return true;
}
if (propertyName == "tank") {
SetTank(newValue.To<double>());
return true;
}
if (propertyName == "flow") {
SetFlow(newValue.To<double>());
return true;
}
if (propertyName == "emitterForceMin") {
SetEmitterForceMin(newValue.To<double>());
return true;
}
if (propertyName == "emitterForceMax") {
SetEmitterForceMax(newValue.To<double>());
return true;
}
if (propertyName == "particleRotationSpeedMin") {
SetParticleAngle1(newValue.To<double>());
return true;
}
if (propertyName == "particleRotationSpeedMax") {
SetParticleAngle2(newValue.To<double>());
return true;
}
if (propertyName == "coneSprayAngle") {
SetConeSprayAngle(newValue.To<double>());
return true;
}
if (propertyName == "zoneRadius") {
SetZoneRadius(newValue.To<double>());
return true;
}
if (propertyName == "particleGravityX") {
SetParticleGravityX(newValue.To<double>());
return true;
}
if (propertyName == "particleGravityY") {
SetParticleGravityY(newValue.To<double>());
return true;
}
if (propertyName == "particleLifeTimeMin") {
SetParticleLifeTimeMin(newValue.To<double>());
return true;
}
if (propertyName == "particleLifeTimeMax") {
SetParticleLifeTimeMax(newValue.To<double>());
return true;
}
if (propertyName == "jumpForwardInTimeOnCreation") {
SetJumpForwardInTimeOnCreation(newValue.To<double>());
return true;
}
return false;
}
std::map<gd::String, gd::PropertyDescriptor>
ParticleEmitterObject::GetProperties() const {
std::map<gd::String, gd::PropertyDescriptor> objectProperties;
objectProperties["rendererType"]
.SetValue(GetRendererType() == Point ? "Circle"
: GetRendererType() == Line ? "Line"
: "Image")
.SetType("choice")
.AddExtraInfo("Circle")
.AddExtraInfo("Line")
.AddExtraInfo("Image")
.SetLabel(_("Particle type"))
.SetHasImpactOnOtherProperties(true);
if (GetRendererType() == Quad) {
objectProperties["textureParticleName"]
.SetValue(GetParticleTexture())
.SetType("resource")
.AddExtraInfo("image")
.SetLabel(_("Texture"));
objectProperties["particlesWidth"]
.SetValue(gd::String::From(GetRendererParam1()))
.SetType("number")
.SetLabel(_("Width"))
.SetMeasurementUnit(gd::MeasurementUnit::GetPixel())
.SetGroup(_("Particles size"));
objectProperties["particlesHeight"]
.SetValue(gd::String::From(GetRendererParam2()))
.SetType("number")
.SetLabel(_("Height"))
.SetMeasurementUnit(gd::MeasurementUnit::GetPixel())
.SetGroup(_("Particles size"));
} else if (GetRendererType() == Line) {
objectProperties["lineLength"]
.SetValue(gd::String::From(GetRendererParam1()))
.SetType("number")
.SetLabel(_("Lines length"))
.SetMeasurementUnit(gd::MeasurementUnit::GetPixel())
.SetGroup(_("Particles size"));
objectProperties["lineThickness"]
.SetValue(gd::String::From(GetRendererParam2()))
.SetType("number")
.SetLabel(_("Lines thickness"))
.SetMeasurementUnit(gd::MeasurementUnit::GetPixel())
.SetGroup(_("Particles size"));
} else {
objectProperties["particlesSize"]
.SetValue(gd::String::From(GetRendererParam1()))
.SetType("number")
.SetLabel(_("Size"))
.SetMeasurementUnit(gd::MeasurementUnit::GetPixel())
.SetGroup(_("Particles size"));
}
objectProperties["particlesStartSize"]
.SetValue(gd::String::From(GetParticleSize1()))
.SetType("number")
.SetLabel(_("Start size (in percents)"))
.SetGroup(_("Particles size"));
objectProperties["particlesEndSize"]
.SetValue(gd::String::From(GetParticleSize2()))
.SetType("number")
.SetLabel(_("End size (in percents)"))
.SetGroup(_("Particles size"));
objectProperties["particlesStartColor"]
.SetValue(GetParticleColor1())
.SetType("color")
.SetLabel(_("Start color"))
.SetGroup(_("Particles color"));
objectProperties["particlesEndColor"]
.SetValue(GetParticleColor2())
.SetType("color")
.SetLabel(_("End color"))
.SetGroup(_("Particles color"));
objectProperties["particlesStartOpacity"]
.SetValue(gd::String::From(GetParticleAlpha1()))
.SetType("number")
.SetLabel(_("Start opacity (0-255)"))
.SetGroup(_("Particles color"));
objectProperties["particlesEndOpacity"]
.SetValue(gd::String::From(GetParticleAlpha2()))
.SetType("number")
.SetLabel(_("End opacity (0-255)"))
.SetGroup(_("Particles color"));
objectProperties["additiveRendering"]
.SetValue(IsRenderingAdditive() ? "true" : "false")
.SetType("boolean")
.SetLabel(_("Additive rendering"))
.SetGroup(_("Particles color"));
objectProperties["deleteWhenOutOfParticles"]
.SetValue(GetDestroyWhenNoParticles() ? "true" : "false")
.SetType("boolean")
.SetLabel(_("Delete when out of particles"))
.SetGroup(_("Particles flow"));
objectProperties["maxParticlesCount"]
.SetValue(gd::String::From(GetMaxParticleNb()))
.SetType("number")
.SetLabel(_("Max particles count"))
.SetGroup(_("Particles flow"));
objectProperties["tank"]
.SetValue(gd::String::From(GetTank()))
.SetType("number")
.SetLabel(_("Tank"))
.SetGroup(_("Particles flow"))
.AddExtraInfo("canBeUnlimitedUsingMinus1");
objectProperties["flow"]
.SetValue(gd::String::From(GetFlow()))
.SetType("number")
.SetLabel(_("Flow"))
.SetGroup(_("Particles flow (particles/seconds)"));
objectProperties["emitterForceMin"]
.SetValue(gd::String::From(GetEmitterForceMin()))
.SetType("number")
.SetLabel(_("Emitter force min"))
.SetGroup(_("Particles movement"));
objectProperties["emitterForceMax"]
.SetValue(gd::String::From(GetEmitterForceMax()))
.SetType("number")
.SetLabel(_("Emitter force max"))
.SetGroup(_("Particles movement"));
objectProperties["particleRotationSpeedMin"]
.SetValue(gd::String::From(GetParticleAngle1()))
.SetType("number")
.SetLabel(_("Minimum rotation speed"))
.SetMeasurementUnit(gd::MeasurementUnit::GetDegreeAngle())
.SetGroup(_("Particles movement"));
objectProperties["particleRotationSpeedMax"]
.SetValue(gd::String::From(GetParticleAngle2()))
.SetType("number")
.SetLabel(_("Maximum rotation speed"))
.SetMeasurementUnit(gd::MeasurementUnit::GetDegreeAngle())
.SetGroup(_("Particles movement"));
objectProperties["coneSprayAngle"]
.SetValue(gd::String::From(GetConeSprayAngle()))
.SetType("number")
.SetLabel(_("Cone spray angle"))
.SetMeasurementUnit(gd::MeasurementUnit::GetDegreeAngle())
.SetGroup(_("Particles movement"));
objectProperties["zoneRadius"]
.SetValue(gd::String::From(GetZoneRadius()))
.SetType("number")
.SetLabel(_("Emitter radius"))
.SetMeasurementUnit(gd::MeasurementUnit::GetPixel())
.SetGroup(_("Particles movement"));
objectProperties["particleGravityX"]
.SetValue(gd::String::From(GetParticleGravityX()))
.SetType("number")
.SetLabel(_("Gravity X"))
.SetMeasurementUnit(gd::MeasurementUnit::GetPixel())
.SetGroup(_("Particles gravity"));
objectProperties["particleGravityY"]
.SetValue(gd::String::From(GetParticleGravityY()))
.SetType("number")
.SetLabel(_("Gravity Y"))
.SetMeasurementUnit(gd::MeasurementUnit::GetPixel())
.SetGroup(_("Particles gravity"));
objectProperties["particleLifeTimeMin"]
.SetValue(gd::String::From(GetParticleLifeTimeMin()))
.SetType("number")
.SetLabel(_("Minimum lifetime"))
.SetMeasurementUnit(gd::MeasurementUnit::GetSecond())
.SetGroup(_("Particles life time"));
objectProperties["particleLifeTimeMax"]
.SetValue(gd::String::From(GetParticleLifeTimeMax()))
.SetType("number")
.SetLabel(_("Maximum lifetime"))
.SetMeasurementUnit(gd::MeasurementUnit::GetSecond())
.SetGroup(_("Particles life time"));
objectProperties["jumpForwardInTimeOnCreation"]
.SetValue(gd::String::From(GetJumpForwardInTimeOnCreation()))
.SetType("number")
.SetLabel(_("Jump forward in time on creation"))
.SetMeasurementUnit(gd::MeasurementUnit::GetSecond())
.SetGroup(_("Particles life time"));
return objectProperties;
}
void ParticleEmitterObject::DoUnserializeFrom(
gd::Project& project, const gd::SerializerElement& element) {
ParticleEmitterBase::UnserializeParticleEmitterBaseFrom(element);
@@ -75,12 +406,27 @@ void ParticleEmitterBase::UnserializeParticleEmitterBaseFrom(
particleGravityY = element.GetDoubleAttribute("particleGravityY");
particleLifeTimeMin = element.GetDoubleAttribute("particleLifeTimeMin");
particleLifeTimeMax = element.GetDoubleAttribute("particleLifeTimeMax");
particleRed1 = element.GetDoubleAttribute("particleRed1");
particleRed2 = element.GetDoubleAttribute("particleRed2");
particleGreen1 = element.GetDoubleAttribute("particleGreen1");
particleGreen2 = element.GetDoubleAttribute("particleGreen2");
particleBlue1 = element.GetDoubleAttribute("particleBlue1");
particleBlue2 = element.GetDoubleAttribute("particleBlue2");
particleColor1 = element.GetStringAttribute("particleColor1");
// Compatibility with GD <= 5.4.210
if (element.HasChild("particleRed1") && !element.HasChild("particleColor1")) {
particleColor1 =
element.GetChild("particleRed1").GetValue().GetString() + ";" +
element.GetChild("particleGreen1").GetValue().GetString() + ";" +
element.GetChild("particleBlue1").GetValue().GetString();
}
// end of compatibility code
particleColor2 = element.GetStringAttribute("particleColor2");
// Compatibility with GD <= 5.4.210
if (element.HasChild("particleRed2") && !element.HasChild("particleColor2")) {
particleColor2 =
element.GetChild("particleRed2").GetValue().GetString() + ";" +
element.GetChild("particleGreen2").GetValue().GetString() + ";" +
element.GetChild("particleBlue2").GetValue().GetString();
}
// end of compatibility code
particleAlpha1 = element.GetDoubleAttribute("particleAlpha1");
particleAlpha2 = element.GetDoubleAttribute("particleAlpha2");
rendererParam1 = element.GetDoubleAttribute("rendererParam1");
@@ -106,7 +452,8 @@ void ParticleEmitterBase::UnserializeParticleEmitterBaseFrom(
element.GetBoolAttribute("destroyWhenNoParticles", false);
textureParticleName = element.GetStringAttribute("textureParticleName");
maxParticleNb = element.GetIntAttribute("maxParticleNb", 5000);
jumpForwardInTimeOnCreation = element.GetDoubleAttribute("jumpForwardInTimeOnCreation");
jumpForwardInTimeOnCreation =
element.GetDoubleAttribute("jumpForwardInTimeOnCreation");
{
gd::String result = element.GetStringAttribute("rendererType");
@@ -137,12 +484,8 @@ void ParticleEmitterBase::SerializeParticleEmitterBaseTo(
element.SetAttribute("particleGravityY", particleGravityY);
element.SetAttribute("particleLifeTimeMin", particleLifeTimeMin);
element.SetAttribute("particleLifeTimeMax", particleLifeTimeMax);
element.SetAttribute("particleRed1", particleRed1);
element.SetAttribute("particleRed2", particleRed2);
element.SetAttribute("particleGreen1", particleGreen1);
element.SetAttribute("particleGreen2", particleGreen2);
element.SetAttribute("particleBlue1", particleBlue1);
element.SetAttribute("particleBlue2", particleBlue2);
element.SetAttribute("particleColor1", particleColor1);
element.SetAttribute("particleColor2", particleColor2);
element.SetAttribute("particleAlpha1", particleAlpha1);
element.SetAttribute("particleAlpha2", particleAlpha2);
element.SetAttribute("particleSize1", particleSize1);
@@ -161,7 +504,8 @@ void ParticleEmitterBase::SerializeParticleEmitterBaseTo(
element.SetAttribute("destroyWhenNoParticles", destroyWhenNoParticles);
element.SetAttribute("textureParticleName", textureParticleName);
element.SetAttribute("maxParticleNb", (int)maxParticleNb);
element.SetAttribute("jumpForwardInTimeOnCreation", jumpForwardInTimeOnCreation);
element.SetAttribute("jumpForwardInTimeOnCreation",
jumpForwardInTimeOnCreation);
gd::String rendererTypeStr = "Point";
if (rendererType == Line)
@@ -169,6 +513,26 @@ void ParticleEmitterBase::SerializeParticleEmitterBaseTo(
else if (rendererType == Quad)
rendererTypeStr = "Quad";
element.SetAttribute("rendererType", rendererTypeStr);
// Still serialize the old particle color components for compatibility with GDevelop <= 5.4.210.
// Remove this in a few releases (or when hex strings are accepted for the color).
{
auto rgb = particleColor1.Split(';');
if (rgb.size() == 3) {
element.SetAttribute("particleRed1", rgb[0].To<double>());
element.SetAttribute("particleGreen1", rgb[1].To<double>());
element.SetAttribute("particleBlue1", rgb[2].To<double>());
}
}
{
auto rgb = particleColor2.Split(';');
if (rgb.size() == 3) {
element.SetAttribute("particleRed2", rgb[0].To<double>());
element.SetAttribute("particleGreen2", rgb[1].To<double>());
element.SetAttribute("particleBlue2", rgb[2].To<double>());
}
}
// end of compatibility code
}
ParticleEmitterBase::~ParticleEmitterBase() {}
@@ -227,26 +591,6 @@ double ParticleEmitterBase::GetParticleGravityLength() const {
GetParticleGravityX() * GetParticleGravityX());
}
void ParticleEmitterBase::SetParticleColor1(const gd::String& color) {
std::vector<gd::String> colors = color.Split(U';');
if (colors.size() < 3) return; // Color is incorrect
SetParticleRed1(colors[0].To<int>());
SetParticleGreen1(colors[1].To<int>());
SetParticleBlue1(colors[2].To<int>());
}
void ParticleEmitterBase::SetParticleColor2(const gd::String& color) {
std::vector<gd::String> colors = color.Split(U';');
if (colors.size() < 3) return; // Color is incorrect
SetParticleRed2(colors[0].To<int>());
SetParticleGreen2(colors[1].To<int>());
SetParticleBlue2(colors[2].To<int>());
}
/**
* Used by copy constructor and assignment operator.
* \warning Do not forget to update me if members were changed!
@@ -269,12 +613,8 @@ void ParticleEmitterBase::Init(const ParticleEmitterBase& other) {
particleGravityY = other.particleGravityY;
particleLifeTimeMin = other.particleLifeTimeMin;
particleLifeTimeMax = other.particleLifeTimeMax;
particleRed1 = other.particleRed1;
particleRed2 = other.particleRed2;
particleGreen1 = other.particleGreen1;
particleGreen2 = other.particleGreen2;
particleBlue1 = other.particleBlue1;
particleBlue2 = other.particleBlue2;
particleColor1 = other.particleColor1;
particleColor2 = other.particleColor2;
particleAlpha1 = other.particleAlpha1;
particleAlpha2 = other.particleAlpha2;
particleSize1 = other.particleSize1;

View File

@@ -48,15 +48,9 @@ class GD_EXTENSION_API ParticleEmitterBase {
void SetParticleGravityAngle(double newAngleInDegree);
void SetParticleGravityLength(double newLength);
void SetParticleColor1(const gd::String& color);
void SetParticleColor2(const gd::String& color);
void SetParticleColor1(const gd::String& color) { particleColor1 = color; };
void SetParticleColor2(const gd::String& color) { particleColor2 = color; };
void SetParticleRed1(double newValue) { particleRed1 = newValue; };
void SetParticleRed2(double newValue) { particleRed2 = newValue; };
void SetParticleGreen1(double newValue) { particleGreen1 = newValue; };
void SetParticleGreen2(double newValue) { particleGreen2 = newValue; };
void SetParticleBlue1(double newValue) { particleBlue1 = newValue; };
void SetParticleBlue2(double newValue) { particleBlue2 = newValue; };
void SetParticleAlpha1(double newValue) { particleAlpha1 = newValue; };
void SetParticleAlpha2(double newValue) { particleAlpha2 = newValue; };
void SetParticleSize1(double newValue) { particleSize1 = newValue; };
@@ -91,7 +85,7 @@ class GD_EXTENSION_API ParticleEmitterBase {
void SetDestroyWhenNoParticles(bool enable = true) {
destroyWhenNoParticles = enable;
};
void SetJumpForwardInTimeOnCreation(double newValue) {
void SetJumpForwardInTimeOnCreation(double newValue) {
jumpForwardInTimeOnCreation = newValue;
};
@@ -114,12 +108,8 @@ class GD_EXTENSION_API ParticleEmitterBase {
std::size_t GetMaxParticleNb() const { return maxParticleNb; };
bool GetDestroyWhenNoParticles() const { return destroyWhenNoParticles; };
double GetParticleRed1() const { return particleRed1; };
double GetParticleRed2() const { return particleRed2; };
double GetParticleGreen1() const { return particleGreen1; };
double GetParticleGreen2() const { return particleGreen2; };
double GetParticleBlue1() const { return particleBlue1; };
double GetParticleBlue2() const { return particleBlue2; };
const gd::String& GetParticleColor1() const { return particleColor1; };
const gd::String& GetParticleColor2() const { return particleColor2; };
double GetParticleAlpha1() const { return particleAlpha1; };
double GetParticleAlpha2() const { return particleAlpha2; };
double GetParticleSize1() const { return particleSize1; };
@@ -146,7 +136,7 @@ class GD_EXTENSION_API ParticleEmitterBase {
void SetRendererType(RendererType type) { rendererType = type; };
RendererType GetRendererType() const { return rendererType; };
bool IsRenderingAdditive() { return additive; };
bool IsRenderingAdditive() const { return additive; };
void SetRenderingAdditive() { additive = true; };
void SetRenderingAlpha() { additive = false; };
@@ -173,8 +163,9 @@ class GD_EXTENSION_API ParticleEmitterBase {
double zoneRadius;
double particleGravityX, particleGravityY;
double particleLifeTimeMin, particleLifeTimeMax;
double particleRed1, particleRed2, particleGreen1, particleGreen2,
particleBlue1, particleBlue2, particleAlpha1, particleAlpha2;
gd::String particleColor1;
gd::String particleColor2;
double particleAlpha1, particleAlpha2;
double particleSize1, particleSize2, particleAngle1, particleAngle2;
double particleAlphaRandomness1, particleAlphaRandomness2;
double particleSizeRandomness1, particleSizeRandomness2,
@@ -194,16 +185,22 @@ class GD_EXTENSION_API ParticleEmitterObject : public gd::ObjectConfiguration,
public:
ParticleEmitterObject();
virtual ~ParticleEmitterObject(){};
virtual std::unique_ptr<gd::ObjectConfiguration> Clone() const {
virtual std::unique_ptr<gd::ObjectConfiguration> Clone() const override {
return gd::make_unique<ParticleEmitterObject>(*this);
}
virtual void ExposeResources(gd::ArbitraryResourceWorker& worker);
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;
private:
virtual void DoUnserializeFrom(gd::Project& project,
const gd::SerializerElement& element);
virtual void DoSerializeTo(gd::SerializerElement& element) const;
const gd::SerializerElement& element) override;
virtual void DoSerializeTo(gd::SerializerElement& element) const override;
};
#endif // PARTICLEEMITTEROBJECT_H

View File

@@ -146,17 +146,13 @@ namespace gdjs {
{
time: 0,
value: gdjs.rgbToHex(
objectData.particleRed1,
objectData.particleGreen1,
objectData.particleBlue1
...gdjs.rgbOrHexToRGBColor(objectData.particleColor1)
),
},
{
time: 1,
value: gdjs.rgbToHex(
objectData.particleRed2,
objectData.particleGreen2,
objectData.particleBlue2
...gdjs.rgbOrHexToRGBColor(objectData.particleColor2)
),
},
],
@@ -292,31 +288,30 @@ namespace gdjs {
moveAcceleration.maxStart < 0);
}
setColor(
r1: number,
g1: number,
b1: number,
r2: number,
g2: number,
b2: number
): void {
setColor(color1: number, color2: number): void {
// console.log({color1,color2})
// debugger;
// Access private members of the behavior to apply changes right away.
const behavior: any = this.emitter.getBehavior('color');
const first = behavior.list.first;
const startColor = first.value;
startColor.r = r1;
startColor.g = g1;
startColor.b = b1;
{
const [r, g, b] = gdjs.hexNumberToRGBArray(color1);
first.value.r = r;
first.value.g = g;
first.value.b = b;
}
first.next = first.next || {
time: 1,
value: {},
};
const endColor = first.next.value;
endColor.r = r2;
endColor.g = g2;
endColor.b = b2;
{
const [r, g, b] = gdjs.hexNumberToRGBArray(color2);
first.next.value.r = r;
first.next.value.g = g;
first.next.value.b = b;
}
}
setSize(size1: float, size2: float): void {

View File

@@ -21,12 +21,8 @@ namespace gdjs {
particleLifeTimeMin: number;
particleGravityY: number;
particleGravityX: number;
particleRed2: number;
particleRed1: number;
particleGreen2: number;
particleGreen1: number;
particleBlue2: number;
particleBlue1: number;
particleColor2: string;
particleColor1: string;
particleSize2: number;
particleSize1: number;
/**
@@ -83,12 +79,8 @@ namespace gdjs {
gravx: number;
gravy: number;
// Color
colr1: number;
colr2: number;
colg1: number;
colg2: number;
colb1: number;
colb2: number;
color1: number;
color2: number;
// Size
size1: number;
size2: number;
@@ -124,12 +116,8 @@ namespace gdjs {
lifeTimeMax: float;
gravityX: number;
gravityY: number;
colorR1: number;
colorR2: number;
colorG1: number;
colorG2: number;
colorB1: number;
colorB2: number;
color1: number;
color2: number;
size1: number;
size2: number;
alpha1: number;
@@ -195,12 +183,12 @@ namespace gdjs {
this.lifeTimeMax = particleObjectData.particleLifeTimeMax;
this.gravityX = particleObjectData.particleGravityX;
this.gravityY = particleObjectData.particleGravityY;
this.colorR1 = particleObjectData.particleRed1;
this.colorR2 = particleObjectData.particleRed2;
this.colorG1 = particleObjectData.particleGreen1;
this.colorG2 = particleObjectData.particleGreen2;
this.colorB1 = particleObjectData.particleBlue1;
this.colorB2 = particleObjectData.particleBlue2;
this.color1 = gdjs.rgbOrHexStringToNumber(
particleObjectData.particleColor1
);
this.color2 = gdjs.rgbOrHexStringToNumber(
particleObjectData.particleColor2
);
this.size1 = particleObjectData.particleSize1;
this.size2 = particleObjectData.particleSize2;
this.alpha1 = particleObjectData.particleAlpha1;
@@ -296,23 +284,11 @@ namespace gdjs {
if (oldObjectData.particleGravityY !== newObjectData.particleGravityY) {
this.setParticleGravityY(newObjectData.particleGravityY);
}
if (oldObjectData.particleRed1 !== newObjectData.particleRed1) {
this.setParticleRed1(newObjectData.particleRed1);
if (oldObjectData.particleColor1 !== newObjectData.particleColor1) {
this.setParticleColor1(newObjectData.particleColor1);
}
if (oldObjectData.particleRed2 !== newObjectData.particleRed2) {
this.setParticleRed2(newObjectData.particleRed2);
}
if (oldObjectData.particleGreen1 !== newObjectData.particleGreen1) {
this.setParticleGreen1(newObjectData.particleGreen1);
}
if (oldObjectData.particleGreen2 !== newObjectData.particleGreen2) {
this.setParticleGreen2(newObjectData.particleGreen2);
}
if (oldObjectData.particleBlue1 !== newObjectData.particleBlue1) {
this.setParticleBlue1(newObjectData.particleBlue1);
}
if (oldObjectData.particleBlue2 !== newObjectData.particleBlue2) {
this.setParticleBlue2(newObjectData.particleBlue2);
if (oldObjectData.particleColor2 !== newObjectData.particleColor2) {
this.setParticleColor2(newObjectData.particleColor2);
}
if (oldObjectData.particleSize1 !== newObjectData.particleSize1) {
this.setParticleSize1(newObjectData.particleSize1);
@@ -397,12 +373,8 @@ namespace gdjs {
ltmax: this.lifeTimeMax,
gravx: this.gravityX,
gravy: this.gravityY,
colr1: this.colorR1,
colr2: this.colorR2,
colg1: this.colorG1,
colg2: this.colorG2,
colb1: this.colorB1,
colb2: this.colorB2,
color1: this.color1,
color2: this.color2,
size1: this.size1,
size2: this.size2,
alp1: this.alpha1,
@@ -463,23 +435,11 @@ namespace gdjs {
if (syncData.gravy !== undefined) {
this.setParticleGravityY(syncData.gravy);
}
if (syncData.colr1 !== undefined) {
this.setParticleRed1(syncData.colr1);
if (syncData.color1 !== undefined) {
this.setParticleColor1AsNumber(syncData.color1);
}
if (syncData.colr2 !== undefined) {
this.setParticleRed2(syncData.colr2);
}
if (syncData.colg1 !== undefined) {
this.setParticleGreen1(syncData.colg1);
}
if (syncData.colg2 !== undefined) {
this.setParticleGreen2(syncData.colg2);
}
if (syncData.colb1 !== undefined) {
this.setParticleBlue1(syncData.colb1);
}
if (syncData.colb2 !== undefined) {
this.setParticleBlue2(syncData.colb2);
if (syncData.color2 !== undefined) {
this.setParticleColor2AsNumber(syncData.color2);
}
if (syncData.size1 !== undefined) {
this.setParticleSize1(syncData.size1);
@@ -547,14 +507,7 @@ namespace gdjs {
this._renderer.setGravity(this.gravityX, this.gravityY);
}
if (this._colorDirty) {
this._renderer.setColor(
this.colorR1,
this.colorG1,
this.colorB1,
this.colorR2,
this.colorG2,
this.colorB2
);
this._renderer.setColor(this.color1, this.color2);
}
if (this._sizeDirty) {
this._renderer.setSize(this.size1, this.size2);
@@ -820,7 +773,7 @@ namespace gdjs {
}
getParticleRed1(): number {
return this.colorR1;
return gdjs.hexNumberToRGBArray(this.color1)[0];
}
setParticleRed1(red: number): void {
@@ -830,14 +783,14 @@ namespace gdjs {
if (red > 255) {
red = 255;
}
if (this.colorR1 !== red) {
this._colorDirty = true;
this.colorR1 = red;
}
const existingColor = gdjs.hexNumberToRGBArray(this.color1);
this.setParticleColor1AsNumber(
gdjs.rgbToHexNumber(red, existingColor[1], existingColor[2])
);
}
getParticleRed2(): number {
return this.colorR2;
return gdjs.hexNumberToRGBArray(this.color2)[0];
}
setParticleRed2(red: number): void {
@@ -847,14 +800,14 @@ namespace gdjs {
if (red > 255) {
red = 255;
}
if (this.colorR2 !== red) {
this._colorDirty = true;
this.colorR2 = red;
}
const existingColor = gdjs.hexNumberToRGBArray(this.color2);
this.setParticleColor2AsNumber(
gdjs.rgbToHexNumber(red, existingColor[1], existingColor[2])
);
}
getParticleGreen1(): number {
return this.colorG1;
return gdjs.hexNumberToRGBArray(this.color1)[1];
}
setParticleGreen1(green: number): void {
@@ -864,14 +817,14 @@ namespace gdjs {
if (green > 255) {
green = 255;
}
if (this.colorG1 !== green) {
this._colorDirty = true;
this.colorG1 = green;
}
const existingColor = gdjs.hexNumberToRGBArray(this.color1);
this.setParticleColor1AsNumber(
gdjs.rgbToHexNumber(existingColor[0], green, existingColor[2])
);
}
getParticleGreen2(): number {
return this.colorG2;
return gdjs.hexNumberToRGBArray(this.color2)[1];
}
setParticleGreen2(green: number): void {
@@ -881,14 +834,14 @@ namespace gdjs {
if (green > 255) {
green = 255;
}
if (this.colorG2 !== green) {
this._colorDirty = true;
this.colorG2 = green;
}
const existingColor = gdjs.hexNumberToRGBArray(this.color2);
this.setParticleColor2AsNumber(
gdjs.rgbToHexNumber(existingColor[0], green, existingColor[2])
);
}
getParticleBlue1(): number {
return this.colorB1;
return gdjs.hexNumberToRGBArray(this.color1)[2];
}
setParticleBlue1(blue: number): void {
@@ -898,14 +851,14 @@ namespace gdjs {
if (blue > 255) {
blue = 255;
}
if (this.colorB1 !== blue) {
this._colorDirty = true;
this.colorB1 = blue;
}
const existingColor = gdjs.hexNumberToRGBArray(this.color1);
this.setParticleColor1AsNumber(
gdjs.rgbToHexNumber(existingColor[0], existingColor[1], blue)
);
}
getParticleBlue2(): number {
return this.colorB2;
return gdjs.hexNumberToRGBArray(this.color2)[2];
}
setParticleBlue2(blue: number): void {
@@ -915,24 +868,32 @@ namespace gdjs {
if (blue > 255) {
blue = 255;
}
if (this.colorB2 !== blue) {
this._colorDirty = true;
this.colorB2 = blue;
}
const existingColor = gdjs.hexNumberToRGBArray(this.color2);
this.setParticleColor2AsNumber(
gdjs.rgbToHexNumber(existingColor[0], existingColor[1], blue)
);
}
setParticleColor1(rgbColor: string): void {
const colors = gdjs.rgbOrHexToRGBColor(rgbColor);
this.setParticleRed1(colors[0]);
this.setParticleGreen1(colors[1]);
this.setParticleBlue1(colors[2]);
setParticleColor1AsNumber(color: number): void {
this.color1 = color;
this._colorDirty = true;
}
setParticleColor2(rgbColor: string): void {
const colors = gdjs.rgbOrHexToRGBColor(rgbColor);
this.setParticleRed2(colors[0]);
this.setParticleGreen2(colors[1]);
this.setParticleBlue2(colors[2]);
setParticleColor1(rgbOrHexColor: string): void {
this.setParticleColor1AsNumber(
gdjs.rgbOrHexStringToNumber(rgbOrHexColor)
);
}
setParticleColor2AsNumber(color: number): void {
this.color2 = color;
this._colorDirty = true;
}
setParticleColor2(rgbOrHexColor: string): void {
this.setParticleColor2AsNumber(
gdjs.rgbOrHexStringToNumber(rgbOrHexColor)
);
}
getParticleSize1(): float {

View File

@@ -70,28 +70,28 @@ std::map<gd::String, gd::PropertyDescriptor> PathfindingBehavior::GetProperties(
.SetValue(
gd::String::From(behaviorContent.GetDoubleAttribute("angleOffset")));
properties["CellWidth"]
.SetLabel(_("Virtual cell width"))
.SetLabel(_("Cell width"))
.SetGroup(_("Virtual Grid"))
.SetType("Number")
.SetMeasurementUnit(gd::MeasurementUnit::GetPixel())
.SetValue(
gd::String::From(behaviorContent.GetDoubleAttribute("cellWidth", 0)));
properties["CellHeight"]
.SetLabel(_("Virtual cell height"))
.SetLabel(_("Cell height"))
.SetGroup(_("Virtual Grid"))
.SetType("Number")
.SetMeasurementUnit(gd::MeasurementUnit::GetPixel())
.SetValue(gd::String::From(
behaviorContent.GetDoubleAttribute("cellHeight", 0)));
properties["GridOffsetX"]
.SetLabel(_("Virtual grid X offset"))
.SetLabel(_("X offset"))
.SetGroup(_("Virtual Grid"))
.SetType("Number")
.SetMeasurementUnit(gd::MeasurementUnit::GetPixel())
.SetValue(gd::String::From(
behaviorContent.GetDoubleAttribute("gridOffsetX", 0)));
properties["GridOffsetY"]
.SetLabel(_("Virtual grid Y offset"))
.SetLabel(_("Y offset"))
.SetGroup(_("Virtual Grid"))
.SetType("Number")
.SetMeasurementUnit(gd::MeasurementUnit::GetPixel())

View File

@@ -185,7 +185,12 @@ module.exports = {
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
.addExtraInfo('Static')
.addExtraInfo('Dynamic')
.addExtraInfo('Kinematic');
.addExtraInfo('Kinematic')
.setDescription(
_(
"A static object won't move (perfect for obstacles). Dynamic objects can move. Kinematic will move according to forces applied to it only (useful for characters or specific mechanisms)."
)
);
behaviorProperties
.getOrCreate('bullet')
.setValue(
@@ -193,7 +198,14 @@ module.exports = {
)
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
.setType('Boolean')
.setLabel('Bullet');
.setLabel(_('Considered as a bullet'))
.setDescription(
_(
'Useful for fast moving objects which requires a more accurate collision detection.'
)
)
.setGroup(_('Physics body advanced settings'))
.setAdvanced(true);
behaviorProperties
.getOrCreate('fixedRotation')
.setValue(
@@ -203,7 +215,13 @@ module.exports = {
)
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
.setType('Boolean')
.setLabel('Fixed Rotation');
.setLabel('Fixed Rotation')
.setDescription(
_(
"If enabled, the object won't rotate and will stay at the same angle. Useful for characters for example."
)
)
.setGroup(_('Movement'));
behaviorProperties
.getOrCreate('canSleep')
.setValue(
@@ -211,7 +229,14 @@ module.exports = {
)
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
.setType('Boolean')
.setLabel('Can Sleep');
.setLabel(_('Can be put to sleep by the engine'))
.setDescription(
_(
"Allows the physics engine to stop computing interaction with the object when it's not touched. It's recommended to keep this on."
)
)
.setGroup(_('Physics body advanced settings'))
.setAdvanced(true);
behaviorProperties
.getOrCreate('shape')
.setValue(behaviorContent.getChild('shape').getStringValue())
@@ -233,7 +258,8 @@ module.exports = {
.setType('Number')
.setMeasurementUnit(gd.MeasurementUnit.getPixel())
.setLabel('Shape Dimension A')
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden);
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
.setHidden(true); // Hidden as required to be changed in the full editor.
behaviorProperties
.getOrCreate('shapeDimensionB')
.setValue(
@@ -245,7 +271,8 @@ module.exports = {
.setType('Number')
.setMeasurementUnit(gd.MeasurementUnit.getPixel())
.setLabel('Shape Dimension B')
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden);
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
.setHidden(true); // Hidden as required to be changed in the full editor.
behaviorProperties
.getOrCreate('shapeOffsetX')
.setValue(
@@ -254,7 +281,8 @@ module.exports = {
.setType('Number')
.setMeasurementUnit(gd.MeasurementUnit.getPixel())
.setLabel('Shape Offset X')
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden);
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
.setHidden(true); // Hidden as required to be changed in the full editor.
behaviorProperties
.getOrCreate('shapeOffsetY')
.setValue(
@@ -263,7 +291,8 @@ module.exports = {
.setType('Number')
.setMeasurementUnit(gd.MeasurementUnit.getPixel())
.setLabel('Shape Offset Y')
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden);
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
.setHidden(true); // Hidden as required to be changed in the full editor.
behaviorProperties
.getOrCreate('polygonOrigin')
.setValue(
@@ -276,7 +305,8 @@ module.exports = {
.addExtraInfo('Center')
.addExtraInfo('Origin')
.addExtraInfo('TopLeft')
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden);
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
.setHidden(true); // Hidden as required to be changed in the full editor.
behaviorProperties
.getOrCreate('vertices')
.setValue(
@@ -285,28 +315,44 @@ module.exports = {
: '[]'
)
.setLabel('Vertices')
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden);
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
.setHidden(true); // Hidden as required to be changed in the full editor.
behaviorProperties
.getOrCreate('density')
.setValue(
behaviorContent.getChild('density').getDoubleValue().toString(10)
)
.setType('Number')
.setLabel('Density');
.setLabel(_('Density'))
.setDescription(
_(
'Define the weight of the object, according to its size. The biggeer the density, the heavier the object.'
)
);
behaviorProperties
.getOrCreate('friction')
.setValue(
behaviorContent.getChild('friction').getDoubleValue().toString(10)
)
.setType('Number')
.setLabel('Friction');
.setLabel(_('Friction'))
.setDescription(
_(
'The friction applied when touching other objects. The higher the value, the more friction.'
)
);
behaviorProperties
.getOrCreate('restitution')
.setValue(
behaviorContent.getChild('restitution').getDoubleValue().toString(10)
)
.setType('Number')
.setLabel('Restitution');
.setLabel(_('Restitution'))
.setDescription(
_(
'The "bounciness" of the object. The higher the value, the more other objects will bounce against it.'
)
);
behaviorProperties
.getOrCreate('linearDamping')
.setValue(
@@ -316,7 +362,9 @@ module.exports = {
.toString(10)
)
.setType('Number')
.setLabel('Linear Damping');
.setLabel(_('Linear Damping'))
.setGroup(_('Movement'));
behaviorProperties
.getOrCreate('angularDamping')
.setValue(
@@ -326,8 +374,9 @@ module.exports = {
.toString(10)
)
.setType('Number')
.setLabel('Angular Damping')
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden);
.setLabel(_('Angular Damping'))
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
.setGroup(_('Movement'));
behaviorProperties
.getOrCreate('gravityScale')
.setValue(
@@ -335,19 +384,23 @@ module.exports = {
)
.setType('Number')
.setLabel('Gravity Scale')
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden);
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
.setGroup(_('Gravity'))
.setAdvanced(true);
behaviorProperties
.getOrCreate('layers')
.setValue(behaviorContent.getChild('layers').getIntValue().toString(10))
.setType('Number')
.setLabel('Layers')
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden);
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
.setHidden(true); // Hidden as required to be changed in the full editor.
behaviorProperties
.getOrCreate('masks')
.setValue(behaviorContent.getChild('masks').getIntValue().toString(10))
.setType('Number')
.setLabel('Masks')
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden);
.setQuickCustomizationVisibility(gd.QuickCustomization.Hidden)
.setHidden(true); // Hidden as required to be changed in the full editor.
return behaviorProperties;
};
@@ -474,9 +527,8 @@ module.exports = {
)
.setIncludeFile('Extensions/Physics2Behavior/physics2runtimebehavior.js')
.addIncludeFile('Extensions/Physics2Behavior/Box2D_v2.3.1_min.wasm.js')
.addRequiredFile(
'Extensions/Physics2Behavior/Box2D_v2.3.1_min.wasm.wasm'
);
.addRequiredFile('Extensions/Physics2Behavior/Box2D_v2.3.1_min.wasm.wasm')
.setOpenFullEditorLabel(_('Edit shape and avanced settings'));
// Global
aut

View File

@@ -13,18 +13,13 @@ This project is released under the MIT License.
#include "GDCore/Project/Project.h"
#include "GDCore/Serialization/SerializerElement.h"
#include "GDCore/Tools/Localization.h"
#include "GDCore/Project/PropertyDescriptor.h"
using namespace std;
ShapePainterObjectBase::ShapePainterObjectBase()
: fillColorR(255),
fillColorG(255),
fillColorB(255),
fillOpacity(255),
: fillOpacity(255),
outlineSize(1),
outlineColorR(0),
outlineColorG(0),
outlineColorB(0),
outlineOpacity(255),
clearBetweenFrames(true),
absoluteCoordinates(false),
@@ -42,19 +37,30 @@ void ShapePainterObjectBase::DoUnserializeFrom(
.GetValue()
.GetInt();
fillColorR =
element.GetChild("fillColor", 0, "FillColor").GetIntAttribute("r");
fillColorG =
element.GetChild("fillColor", 0, "FillColor").GetIntAttribute("g");
fillColorB =
element.GetChild("fillColor", 0, "FillColor").GetIntAttribute("b");
outlineColorR =
element.GetChild("outlineColor", 0, "OutlineColor").GetIntAttribute("r");
outlineColorG =
element.GetChild("outlineColor", 0, "OutlineColor").GetIntAttribute("g");
outlineColorB =
element.GetChild("outlineColor", 0, "OutlineColor").GetIntAttribute("b");
const auto& fillColorElement = element.GetChild("fillColor", 0, "FillColor");
if (fillColorElement.GetValue().IsString()) {
fillColor = fillColorElement.GetStringValue();
} else {
// Compatibility with GD <= 5.4.212
int fillColorR = fillColorElement.GetIntAttribute("r");
int fillColorG = fillColorElement.GetIntAttribute("g");
int fillColorB = fillColorElement.GetIntAttribute("b");
fillColor = gd::String::From(fillColorR) + ";" + gd::String::From(fillColorG) + ";" + gd::String::From(fillColorB);
// end of compatibility code
}
const auto& outlineColorElement = element.GetChild("outlineColor", 0, "OutlineColor");
if (outlineColorElement.GetValue().IsString()) {
outlineColor = outlineColorElement.GetStringValue();
} else {
// Compatibility with GD <= 5.4.212
int outlineColorR = outlineColorElement.GetIntAttribute("r");
int outlineColorG = outlineColorElement.GetIntAttribute("g");
int outlineColorB = outlineColorElement.GetIntAttribute("b");
outlineColor = gd::String::From(outlineColorR) + ";" + gd::String::From(outlineColorG) + ";" + gd::String::From(outlineColorB);
// end of compatibility code
}
absoluteCoordinates =
element.GetChild("absoluteCoordinates", 0, "AbsoluteCoordinates")
@@ -80,34 +86,44 @@ void ShapePainterObjectBase::DoSerializeTo(
element.AddChild("fillOpacity").SetValue(fillOpacity);
element.AddChild("outlineSize").SetValue(outlineSize);
element.AddChild("outlineOpacity").SetValue(outlineOpacity);
element.AddChild("fillColor")
.SetAttribute("r", (int)fillColorR)
.SetAttribute("g", (int)fillColorG)
.SetAttribute("b", (int)fillColorB);
element.AddChild("outlineColor")
.SetAttribute("r", (int)outlineColorR)
.SetAttribute("g", (int)outlineColorG)
.SetAttribute("b", (int)outlineColorB);
element.AddChild("absoluteCoordinates").SetValue(absoluteCoordinates);
element.AddChild("clearBetweenFrames").SetValue(clearBetweenFrames);
element.AddChild("antialiasing").SetValue(antialiasing);
{
auto rgb = fillColor.Split(';');
auto& fillColorElement = element.AddChild("fillColor");
if (rgb.size() == 3) {
// Still serialize the old particle color components for compatibility with GDevelop <= 5.4.212.
// Remove this in a few releases (or when hex strings are accepted for the color).
fillColorElement.AddChild("r").SetValue(rgb[0].To<double>());
fillColorElement.AddChild("g").SetValue(rgb[1].To<double>());
fillColorElement.AddChild("b").SetValue(rgb[2].To<double>());
// end of compatibility code
} else {
fillColorElement.SetValue(fillColor);
}
}
{
auto rgb = outlineColor.Split(';');
auto& outlineColorElement = element.AddChild("outlineColor");
if (rgb.size() == 3) {
// Still serialize the old particle color components for compatibility with GDevelop <= 5.4.212.
// Remove this in a few releases (or when hex strings are accepted for the color).
outlineColorElement.AddChild("r").SetValue(rgb[0].To<double>());
outlineColorElement.AddChild("g").SetValue(rgb[1].To<double>());
outlineColorElement.AddChild("b").SetValue(rgb[2].To<double>());
// end of compatibility code
} else {
outlineColorElement.SetValue(outlineColor);
}
}
}
void ShapePainterObject::DoSerializeTo(gd::SerializerElement& element) const {
ShapePainterObjectBase::DoSerializeTo(element);
}
/**
* Change the color filter of the sprite object
*/
void ShapePainterObjectBase::SetFillColor(unsigned int r,
unsigned int g,
unsigned int b) {
fillColorR = r;
fillColorG = g;
fillColorB = b;
}
void ShapePainterObjectBase::SetFillOpacity(double val) {
if (val > 255)
val = 255;
@@ -117,17 +133,6 @@ void ShapePainterObjectBase::SetFillOpacity(double val) {
fillOpacity = val;
}
/**
* Change the color filter of the sprite object
*/
void ShapePainterObjectBase::SetOutlineColor(unsigned int r,
unsigned int g,
unsigned int b) {
outlineColorR = r;
outlineColorG = g;
outlineColorB = b;
}
void ShapePainterObjectBase::SetOutlineOpacity(double val) {
if (val > 255)
val = 255;
@@ -137,26 +142,107 @@ void ShapePainterObjectBase::SetOutlineOpacity(double val) {
outlineOpacity = val;
}
/**
* Change the fill color
*/
void ShapePainterObjectBase::SetFillColor(const gd::String& color) {
std::vector<gd::String> colors = color.Split(U';');
if (colors.size() < 3) return;
bool ShapePainterObject::UpdateProperty(const gd::String& propertyName,
const gd::String& newValue) {
if (propertyName == "fillOpacity") {
SetFillOpacity(newValue.To<double>());
return true;
}
if (propertyName == "fillColor") {
SetFillColor(newValue);
return true;
}
if (propertyName == "outlineColor") {
SetOutlineColor(newValue);
return true;
}
if (propertyName == "outlineOpacity") {
SetOutlineOpacity(newValue.To<double>());
return true;
}
if (propertyName == "outlineSize") {
SetOutlineSize(newValue.To<double>());
return true;
}
fillColorR = colors[0].To<int>();
fillColorG = colors[1].To<int>();
fillColorB = colors[2].To<int>();
if (propertyName == "absoluteCoordinates") {
if (newValue == "1")
SetCoordinatesAbsolute();
else
SetCoordinatesRelative();
return true;
}
if (propertyName == "clearBetweenFrames") {
SetClearBetweenFrames(newValue == "1");
return true;
}
if (propertyName == "antialiasing") {
SetAntialiasing(newValue);
return true;
}
return false;
}
/**
* Change the color of the outline
*/
void ShapePainterObjectBase::SetOutlineColor(const gd::String& color) {
std::vector<gd::String> colors = color.Split(U';');
if (colors.size() < 3) return;
std::map<gd::String, gd::PropertyDescriptor>
ShapePainterObject::GetProperties() const {
std::map<gd::String, gd::PropertyDescriptor> objectProperties;
outlineColorR = colors[0].To<int>();
outlineColorG = colors[1].To<int>();
outlineColorB = colors[2].To<int>();
objectProperties["fillColor"]
.SetValue(GetFillColor())
.SetType("color")
.SetLabel(_("Fill color"))
.SetGroup(_("Fill"));
objectProperties["fillOpacity"]
.SetValue(gd::String::From(GetFillOpacity()))
.SetType("number")
.SetLabel(_("Fill opacity"))
.SetGroup(_("Fill"));
objectProperties["outlineColor"]
.SetValue(GetOutlineColor())
.SetType("color")
.SetLabel(_("Outline color"))
.SetGroup(_("Outline"));
objectProperties["outlineOpacity"]
.SetValue(gd::String::From(GetOutlineOpacity()))
.SetType("number")
.SetLabel(_("Outline opacity"))
.SetGroup(_("Outline"));
objectProperties["outlineSize"]
.SetValue(gd::String::From(GetOutlineSize()))
.SetType("number")
.SetLabel(_("Outline size"))
.SetGroup(_("Outline"));
objectProperties["absoluteCoordinates"]
.SetValue(AreCoordinatesAbsolute() ? "true" : "false")
.SetType("boolean")
.SetLabel(_("Use absolute coordinates"))
.SetGroup(_("Drawing"));
objectProperties["clearBetweenFrames"]
.SetValue(IsClearedBetweenFrames() ? "true" : "false")
.SetType("boolean")
.SetLabel(_("Clear drawing at each frame"))
.SetGroup(_("Drawing"))
.SetDescription(_("When activated, clear the previous render at each frame. Otherwise, shapes are staying on the screen until you clear manually the object in events."));
objectProperties["antialiasing"]
.SetValue(GetAntialiasing())
.SetType("choice")
.AddExtraInfo("none")
.AddExtraInfo("low")
.AddExtraInfo("medium")
.AddExtraInfo("high")
.SetGroup(_("Drawing"))
.SetLabel(_("Antialiasing"))
.SetDescription(_("Antialiasing mode"));
return objectProperties;
}

View File

@@ -32,35 +32,23 @@ class GD_EXTENSION_API ShapePainterObjectBase {
void SetOutlineOpacity(double val);
inline double GetOutlineOpacity() const { return outlineOpacity; };
void SetOutlineColor(unsigned int r, unsigned int v, unsigned int b);
inline unsigned int GetOutlineColorR() const { return outlineColorR; };
inline unsigned int GetOutlineColorG() const { return outlineColorG; };
inline unsigned int GetOutlineColorB() const { return outlineColorB; };
/** Used by GD events generated code : Prefer using original SetOutlineColor
*/
void SetOutlineColor(const gd::String& color);
const gd::String& GetOutlineColor() const { return outlineColor; };
void SetFillOpacity(double val);
inline double GetFillOpacity() const { return fillOpacity; };
void SetFillColor(unsigned int r, unsigned int v, unsigned int b);
inline unsigned int GetFillColorR() const { return fillColorR; };
inline unsigned int GetFillColorG() const { return fillColorG; };
inline unsigned int GetFillColorB() const { return fillColorB; };
/** Used by GD events generated code : Prefer using original SetFillColor
*/
void SetFillColor(const gd::String& color);
const gd::String& GetFillColor() const { return fillColor; };
inline void SetCoordinatesAbsolute() { absoluteCoordinates = true; }
inline void SetCoordinatesRelative() { absoluteCoordinates = false; }
inline bool AreCoordinatesAbsolute() { return absoluteCoordinates; }
inline bool AreCoordinatesAbsolute() const { return absoluteCoordinates; }
inline void SetClearBetweenFrames(bool value) { clearBetweenFrames = value; }
inline bool IsClearedBetweenFrames() { return clearBetweenFrames; }
inline bool IsClearedBetweenFrames() const { return clearBetweenFrames; }
inline gd::String GetAntialiasing() { return antialiasing; }
inline const gd::String& GetAntialiasing() const { return antialiasing; }
inline void SetAntialiasing(const gd::String& value) { antialiasing = value; }
protected:
@@ -70,16 +58,12 @@ class GD_EXTENSION_API ShapePainterObjectBase {
private:
// Fill color
unsigned int fillColorR;
unsigned int fillColorG;
unsigned int fillColorB;
gd::String fillColor;
float fillOpacity;
// Outline
int outlineSize;
unsigned int outlineColorR;
unsigned int outlineColorG;
unsigned int outlineColorB;
gd::String outlineColor;
float outlineOpacity;
bool absoluteCoordinates;
@@ -97,14 +81,20 @@ class GD_EXTENSION_API ShapePainterObject : public gd::ObjectConfiguration,
public:
ShapePainterObject();
virtual ~ShapePainterObject(){};
virtual std::unique_ptr<gd::ObjectConfiguration> Clone() const {
virtual std::unique_ptr<gd::ObjectConfiguration> Clone() const override {
return gd::make_unique<ShapePainterObject>(*this);
}
virtual std::map<gd::String, gd::PropertyDescriptor>
GetProperties() const override;
virtual bool UpdateProperty(const gd::String &name,
const gd::String &value) override;
private:
virtual void DoUnserializeFrom(gd::Project& project,
const gd::SerializerElement& element);
virtual void DoSerializeTo(gd::SerializerElement& element) const;
const gd::SerializerElement& element) override;
virtual void DoSerializeTo(gd::SerializerElement& element) const override;
};
#endif // SHAPEPAINTEROBJECT_H

View File

@@ -17,10 +17,10 @@ namespace gdjs {
/** Initial properties for a for {@link gdjs.ShapePainterRuntimeObject}. */
export type ShapePainterObjectDataType = {
/** The color (in RGB format) of the inner part of the painted shape */
fillColor: RGBColor;
/** The color (in RGB format) of the outline of the painted shape */
outlineColor: RGBColor;
/** The color of the inner part of the painted shape */
fillColor: RGBColor | string;
/** The color of the outline of the painted shape */
outlineColor: RGBColor | string;
/** The opacity of the inner part of the painted shape, from 0 to 255 */
fillOpacity: float;
/** The opacity of the outline of the painted shape, from 0 to 255 */
@@ -72,22 +72,28 @@ namespace gdjs {
shapePainterObjectData: ShapePainterObjectData
) {
super(instanceContainer, shapePainterObjectData);
this._fillColor = parseInt(
gdjs.rgbToHex(
shapePainterObjectData.fillColor.r,
shapePainterObjectData.fillColor.g,
shapePainterObjectData.fillColor.b
),
16
);
this._outlineColor = parseInt(
gdjs.rgbToHex(
shapePainterObjectData.outlineColor.r,
shapePainterObjectData.outlineColor.g,
shapePainterObjectData.outlineColor.b
),
16
);
this._fillColor =
typeof shapePainterObjectData.fillColor === 'string'
? gdjs.rgbOrHexStringToNumber(shapePainterObjectData.fillColor)
: parseInt(
gdjs.rgbToHex(
shapePainterObjectData.fillColor.r,
shapePainterObjectData.fillColor.g,
shapePainterObjectData.fillColor.b
),
16
);
this._outlineColor =
typeof shapePainterObjectData.outlineColor === 'string'
? gdjs.rgbOrHexStringToNumber(shapePainterObjectData.outlineColor)
: parseInt(
gdjs.rgbToHex(
shapePainterObjectData.outlineColor.r,
shapePainterObjectData.outlineColor.g,
shapePainterObjectData.outlineColor.b
),
16
);
this._fillOpacity = shapePainterObjectData.fillOpacity;
this._outlineOpacity = shapePainterObjectData.outlineOpacity;
this._outlineSize = shapePainterObjectData.outlineSize;
@@ -111,10 +117,17 @@ namespace gdjs {
oldObjectData: ShapePainterObjectData,
newObjectData: ShapePainterObjectData
): boolean {
if (typeof newObjectData.fillColor === 'string') {
if (oldObjectData.fillColor !== newObjectData.fillColor) {
this.setFillColor(newObjectData.fillColor);
}
}
if (
oldObjectData.fillColor.r !== newObjectData.fillColor.r ||
oldObjectData.fillColor.g !== newObjectData.fillColor.g ||
oldObjectData.fillColor.b !== newObjectData.fillColor.b
typeof oldObjectData.fillColor !== 'string' &&
typeof newObjectData.fillColor !== 'string' &&
(oldObjectData.fillColor.r !== newObjectData.fillColor.r ||
oldObjectData.fillColor.g !== newObjectData.fillColor.g ||
oldObjectData.fillColor.b !== newObjectData.fillColor.b)
) {
this.setFillColor(
'' +
@@ -125,10 +138,17 @@ namespace gdjs {
newObjectData.fillColor.b
);
}
if (typeof newObjectData.outlineColor === 'string') {
if (oldObjectData.outlineColor !== newObjectData.outlineColor) {
this.setOutlineColor(newObjectData.outlineColor);
}
}
if (
oldObjectData.outlineColor.r !== newObjectData.outlineColor.r ||
oldObjectData.outlineColor.g !== newObjectData.outlineColor.g ||
oldObjectData.outlineColor.b !== newObjectData.outlineColor.b
typeof oldObjectData.outlineColor !== 'string' &&
typeof newObjectData.outlineColor !== 'string' &&
(oldObjectData.outlineColor.r !== newObjectData.outlineColor.r ||
oldObjectData.outlineColor.g !== newObjectData.outlineColor.g ||
oldObjectData.outlineColor.b !== newObjectData.outlineColor.b)
) {
this.setOutlineColor(
'' +
@@ -163,6 +183,10 @@ namespace gdjs {
) {
this._clearBetweenFrames = newObjectData.clearBetweenFrames;
}
if (oldObjectData.antialiasing !== newObjectData.antialiasing) {
this.setAntialiasing(newObjectData.antialiasing);
}
return true;
}
@@ -450,23 +474,8 @@ namespace gdjs {
return !this._useAbsoluteCoordinates;
}
/**
*
* @param rgbColor semicolon separated decimal values
*/
setFillColor(rgbColor: string): void {
const colors = rgbColor.split(';');
if (colors.length < 3) {
return;
}
this._fillColor = parseInt(
gdjs.rgbToHex(
parseInt(colors[0], 10),
parseInt(colors[1], 10),
parseInt(colors[2], 10)
),
16
);
setFillColor(color: string): void {
this._fillColor = gdjs.rgbOrHexStringToNumber(color);
}
getFillColorR(): integer {
@@ -479,23 +488,8 @@ namespace gdjs {
return gdjs.hexNumberToRGB(this._fillColor).b;
}
/**
*
* @param rgbColor semicolon separated decimal values
*/
setOutlineColor(rgbColor: string): void {
const colors = rgbColor.split(';');
if (colors.length < 3) {
return;
}
this._outlineColor = parseInt(
gdjs.rgbToHex(
parseInt(colors[0], 10),
parseInt(colors[1], 10),
parseInt(colors[2], 10)
),
16
);
this._outlineColor = gdjs.rgbOrHexStringToNumber(rgbColor);
this._renderer.updateOutline();
}

View File

@@ -54,7 +54,8 @@ module.exports = {
.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'));
.setCategoryFullName(_('Advanced'))
.setOpenFullEditorLabel(_('Edit animations'));
object
.addExpressionAndConditionAndAction(
@@ -166,6 +167,7 @@ module.exports = {
this._pixiObject.addChild(this._rect);
this._pixiContainer.addChild(this._pixiObject);
this._spineResourceName = '';
this._loadSpine();
}
@@ -174,6 +176,17 @@ module.exports = {
}
update() {
const object = gd.castObject(
this._associatedObjectConfiguration,
gd.SpineObjectConfiguration
);
const spineResourceName = object.getSpineResourceName();
if (this._spineResourceName !== spineResourceName) {
this._spineResourceName = spineResourceName;
this._loadSpine();
}
this._pixiObject.position.set(
this._instance.getX(),
this._instance.getY()
@@ -198,10 +211,17 @@ module.exports = {
this.setAnimation(this._instance.getRawDoubleProperty('animation'));
const scale = object.getScale() || 1;
const spine = this._spine;
if (spine) {
const localBounds = spine.getLocalBounds(undefined, true);
this._initialWidth = localBounds.width * scale;
this._initialHeight = localBounds.height * scale;
}
const width = this.getWidth();
const height = this.getHeight();
const { _spine: spine } = this;
if (spine) {
spine.width = width;
spine.height = height;
@@ -248,7 +268,9 @@ module.exports = {
*/
setAnimation(index) {
const { _spine: spine } = this;
const configuration = this._getConfiguration();
const configuration = gd.asSpineConfiguration(
this._associatedObjectConfiguration
);
if (
!spine ||
@@ -276,8 +298,6 @@ module.exports = {
spine.state.tracks[0].trackTime = 0;
spine.update(0);
spine.autoUpdate = false;
this._initialWidth = spine.width * this.getScale();
this._initialHeight = spine.height * this.getScale();
}
/**
@@ -294,41 +314,17 @@ module.exports = {
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)
.getSpineData(this._project, this._spineResourceName)
.then((spineDataOrLoadingError) => {
if (this._spine) this._pixiObject.removeChild(this._spine);
if (!spineDataOrLoadingError.skeleton) {
console.error(
'Unable to load Spine (' +
@@ -343,13 +339,11 @@ module.exports = {
try {
this._spine = new PIXI.Spine(spineDataOrLoadingError.skeleton);
this._pixiObject.addChild(this._spine);
} catch (error) {
console.error('Exception while loading Spine.', error);
this._spine = null;
return;
}
this._pixiObject.addChild(this._spine);
this.update();
});
}
}

View File

@@ -65,9 +65,9 @@ public:
}
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>
@@ -139,6 +139,15 @@ public:
///@}
/** \name Getters
* Fast access for rendering instances.
*/
///@{
double GetScale() const { return scale; };
const gd::String& GetSpineResourceName() const { return spineResourceName; };
///@}
protected:
virtual void DoUnserializeFrom(gd::Project &project, const gd::SerializerElement &element) override;
virtual void DoSerializeTo(gd::SerializerElement &element) const override;

View File

@@ -31,11 +31,8 @@ module.exports = {
.setIcon('JsPlatform/Extensions/text_input.svg');
const textInputObject = new gd.ObjectJsImplementation();
textInputObject.updateProperty = function (
objectContent,
propertyName,
newValue
) {
textInputObject.updateProperty = function (propertyName, newValue) {
const objectContent = this.content;
if (propertyName === 'initialValue') {
objectContent.initialValue = newValue;
return true;
@@ -85,8 +82,9 @@ module.exports = {
return false;
};
textInputObject.getProperties = function (objectContent) {
textInputObject.getProperties = function () {
const objectProperties = new gd.MapStringPropertyDescriptor();
const objectContent = this.content;
objectProperties
.getOrCreate('initialValue')
@@ -141,21 +139,21 @@ module.exports = {
.setValue(objectContent.readOnly ? 'true' : 'false')
.setType('boolean')
.setLabel(_('Read only'))
.setGroup(_('Field appearance'));
.setGroup(_('Field'));
objectProperties
.getOrCreate('disabled')
.setValue(objectContent.disabled ? 'true' : 'false')
.setType('boolean')
.setLabel(_('Disabled'))
.setGroup(_('Field appearance'));
.setGroup(_('Field'));
objectProperties
.getOrCreate('textColor')
.setValue(objectContent.textColor || '0;0;0')
.setType('color')
.setLabel(_('Text color'))
.setGroup(_('Field appearance'));
.setGroup(_('Font'));
objectProperties
.getOrCreate('fillColor')
@@ -180,8 +178,8 @@ module.exports = {
.getOrCreate('borderColor')
.setValue(objectContent.borderColor || '0;0;0')
.setType('color')
.setLabel(_('Border color'))
.setGroup(_('Field appearance'));
.setLabel(_('Color'))
.setGroup(_('Border appearance'));
objectProperties
.getOrCreate('borderOpacity')
@@ -192,38 +190,35 @@ module.exports = {
).toString()
)
.setType('number')
.setLabel(_('Border opacity'))
.setGroup(_('Field appearance'));
.setLabel(_('Opacity'))
.setGroup(_('Border appearance'));
objectProperties
.getOrCreate('borderWidth')
.setValue((objectContent.borderWidth || 0).toString())
.setType('number')
.setLabel(_('Border width'))
.setGroup(_('Field appearance'));
.setLabel(_('Width'))
.setGroup(_('Border appearance'));
return objectProperties;
};
textInputObject.setRawJSONContent(
JSON.stringify({
initialValue: '',
placeholder: 'Touch to start typing',
fontResourceName: '',
fontSize: 20,
inputType: 'text',
textColor: '0;0;0',
fillColor: '255;255;255',
fillOpacity: 255,
borderColor: '0;0;0',
borderOpacity: 255,
borderWidth: 1,
readOnly: false,
disabled: false,
})
);
textInputObject.content = {
initialValue: '',
placeholder: 'Touch to start typing',
fontResourceName: '',
fontSize: 20,
inputType: 'text',
textColor: '0;0;0',
fillColor: '255;255;255',
fillOpacity: 255,
borderColor: '0;0;0',
borderOpacity: 255,
borderWidth: 1,
readOnly: false,
disabled: false,
};
textInputObject.updateInitialInstanceProperty = function (
objectContent,
instance,
propertyName,
newValue
@@ -238,10 +233,7 @@ module.exports = {
return false;
};
textInputObject.getInitialInstanceProperties = function (
content,
instance
) {
textInputObject.getInitialInstanceProperties = function (instance) {
const instanceProperties = new gd.MapStringPropertyDescriptor();
instanceProperties
@@ -682,18 +674,21 @@ module.exports = {
update() {
const instance = this._instance;
const properties = this._associatedObjectConfiguration.getProperties();
const object = gd.castObject(
this._associatedObjectConfiguration,
gd.ObjectJsImplementation
);
const placeholder =
instance.getRawStringProperty('placeholder') ||
properties.get('placeholder').getValue();
object.content.placeholder;
const initialValue =
instance.getRawStringProperty('initialValue') ||
properties.get('initialValue').getValue();
object.content.initialValue;
const hasInitialValue = initialValue !== '';
this._pixiText.text = hasInitialValue ? initialValue : placeholder;
const textColor = properties.get('textColor').getValue();
const textColor = object.content.textColor;
const finalTextColor = hasInitialValue
? objectsRenderingService.rgbOrHexToHexNumber(textColor)
: 0x888888;
@@ -703,13 +698,13 @@ module.exports = {
this._pixiText.dirty = true;
}
const fontSize = parseFloat(properties.get('fontSize').getValue());
const fontSize = object.content.fontSize;
if (this._pixiText.style.fontSize !== fontSize) {
this._pixiText.style.fontSize = fontSize;
this._pixiText.dirty = true;
}
const fontResourceName = properties.get('fontResourceName').getValue();
const fontResourceName = object.content.fontResourceName;
if (this._fontResourceName !== fontResourceName) {
this._fontResourceName = fontResourceName;
@@ -744,8 +739,7 @@ module.exports = {
this._instance.getAngle()
);
const borderWidth =
parseFloat(properties.get('borderWidth').getValue()) || 0;
const borderWidth = object.content.borderWidth || 0;
// Draw the mask for the text.
const textOffset = borderWidth + TEXT_MASK_PADDING;
@@ -759,8 +753,7 @@ module.exports = {
);
this._pixiTextMask.endFill();
const isTextArea =
properties.get('inputType').getValue() === 'text area';
const isTextArea = object.content.inputType === 'text area';
this._pixiText.position.x = textOffset;
this._pixiText.position.y = isTextArea
@@ -768,14 +761,10 @@ module.exports = {
: height / 2 - this._pixiText.height / 2;
// Draw the background and border.
const fillColor = properties.get('fillColor').getValue();
const fillOpacity = parseFloat(
properties.get('fillOpacity').getValue()
);
const borderColor = properties.get('borderColor').getValue();
const borderOpacity = parseFloat(
properties.get('borderOpacity').getValue()
);
const fillColor = object.content.fillColor;
const fillOpacity = object.content.fillOpacity;
const borderColor = object.content.borderColor;
const borderOpacity = object.content.borderOpacity;
this._pixiGraphics.clear();
this._pixiGraphics.lineStyle(

View File

@@ -158,76 +158,86 @@ std::map<gd::String, gd::PropertyDescriptor> TextObject::GetProperties() const {
.AddExtraInfo("left")
.AddExtraInfo("center")
.AddExtraInfo("right")
.SetLabel(_("Alignment, when multiple lines are displayed"))
.SetLabel(_("Alignment"))
.SetDescription(_("Alignment of the text when multiple lines are displayed"))
.SetGroup(_("Font"))
.SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden);
objectProperties["isOutlineEnabled"]
.SetValue(isOutlineEnabled ? "true" : "false")
.SetType("boolean")
.SetLabel(_("Outline"))
.SetLabel(_("Show outline"))
.SetGroup(_("Outline"))
.SetAdvanced()
.SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden);
objectProperties["outlineColor"]
.SetValue(outlineColor)
.SetType("color")
.SetLabel(_("Outline color"))
.SetLabel(_("Color"))
.SetGroup(_("Outline"))
.SetAdvanced()
.SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden);
objectProperties["outlineThickness"]
.SetValue(gd::String::From(outlineThickness))
.SetType("number")
.SetLabel(_("Outline thickness"))
.SetLabel(_("Thickness"))
.SetMeasurementUnit(gd::MeasurementUnit::GetPixel())
.SetGroup(_("Outline"))
.SetAdvanced()
.SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden);
objectProperties["isShadowEnabled"]
.SetValue(isShadowEnabled ? "true" : "false")
.SetType("boolean")
.SetLabel(_("Shadow"))
.SetLabel(_("Show shadow"))
.SetGroup(_("Shadow"))
.SetAdvanced()
.SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden);
objectProperties["shadowColor"]
.SetValue(shadowColor)
.SetType("color")
.SetLabel(_("Shadow color"))
.SetLabel(_("Color"))
.SetGroup(_("Shadow"))
.SetAdvanced()
.SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden);
objectProperties["shadowOpacity"]
.SetValue(gd::String::From(shadowOpacity))
.SetType("number")
.SetLabel(_("Shadow opacity"))
.SetLabel(_("Opacity"))
.SetMeasurementUnit(gd::MeasurementUnit::GetPixel())
.SetGroup(_("Shadow"))
.SetAdvanced()
.SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden);
objectProperties["shadowAngle"]
.SetValue(gd::String::From(shadowAngle))
.SetType("number")
.SetLabel(_("Shadow angle"))
.SetLabel(_("Angle"))
.SetMeasurementUnit(gd::MeasurementUnit::GetDegreeAngle())
.SetGroup(_("Shadow"))
.SetAdvanced()
.SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden);
objectProperties["shadowDistance"]
.SetValue(gd::String::From(shadowDistance))
.SetType("number")
.SetLabel(_("Shadow distance"))
.SetLabel(_("Distance"))
.SetMeasurementUnit(gd::MeasurementUnit::GetPixel())
.SetGroup(_("Shadow"))
.SetAdvanced()
.SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden);
objectProperties["shadowBlurRadius"]
.SetValue(gd::String::From(shadowBlurRadius))
.SetType("number")
.SetLabel(_("Shadow blur radius"))
.SetLabel(_("Blur radius"))
.SetMeasurementUnit(gd::MeasurementUnit::GetPixel())
.SetGroup(_("Shadow"))
.SetAdvanced()
.SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden);
return objectProperties;

View File

@@ -22,11 +22,8 @@
*/
const defineTileMap = function (extension, _, gd) {
var objectTileMap = new gd.ObjectJsImplementation();
objectTileMap.updateProperty = function (
objectContent,
propertyName,
newValue
) {
objectTileMap.updateProperty = function (propertyName, newValue) {
const objectContent = this.content;
if (propertyName === 'tilemapJsonFile') {
objectContent.tilemapJsonFile = newValue;
return true;
@@ -62,8 +59,9 @@ const defineTileMap = function (extension, _, gd) {
return false;
};
objectTileMap.getProperties = function (objectContent) {
objectTileMap.getProperties = function () {
var objectProperties = new gd.MapStringPropertyDescriptor();
const objectContent = this.content;
objectProperties.set(
'tilemapJsonFile',
@@ -147,29 +145,26 @@ const defineTileMap = function (extension, _, gd) {
return objectProperties;
};
objectTileMap.setRawJSONContent(
JSON.stringify({
tilemapJsonFile: '',
tilesetJsonFile: '',
tilemapAtlasImage: '',
displayMode: 'visible',
layerIndex: 0,
levelIndex: 0,
animationSpeedScale: 1,
animationFps: 4,
})
);
objectTileMap.content = {
tilemapJsonFile: '',
tilesetJsonFile: '',
tilemapAtlasImage: '',
displayMode: 'visible',
layerIndex: 0,
levelIndex: 0,
animationSpeedScale: 1,
animationFps: 4,
};
objectTileMap.updateInitialInstanceProperty = function (
objectContent,
instance,
propertyName,
newValue
) {
return false;
};
objectTileMap.getInitialInstanceProperties = function (content, instance) {
var instanceProperties = new gd.MapStringPropertyDescriptor();
objectTileMap.getInitialInstanceProperties = function (instance) {
const instanceProperties = new gd.MapStringPropertyDescriptor();
return instanceProperties;
};
@@ -597,11 +592,8 @@ const defineTileMap = function (extension, _, gd) {
*/
const defineSimpleTileMap = function (extension, _, gd) {
var objectSimpleTileMap = new gd.ObjectJsImplementation();
objectSimpleTileMap.updateProperty = function (
objectContent,
propertyName,
newValue
) {
objectSimpleTileMap.updateProperty = function (propertyName, newValue) {
const objectContent = this.content;
if (propertyName === 'atlasImage') {
objectContent.atlasImage = newValue;
return true;
@@ -625,8 +617,9 @@ const defineSimpleTileMap = function (extension, _, gd) {
return false;
};
objectSimpleTileMap.getProperties = function (objectContent) {
objectSimpleTileMap.getProperties = function () {
var objectProperties = new gd.MapStringPropertyDescriptor();
const objectContent = this.content;
objectProperties.set(
'columnCount',
@@ -665,6 +658,7 @@ const defineSimpleTileMap = function (extension, _, gd) {
.setType('number')
.setLabel(_('Tile size'))
.setDescription(_('Tile size in pixels.'))
.setHidden(true) // Hidden because a full editor is needed to recompute column/row counts
);
objectProperties.set(
'tilesWithHitBox',
@@ -684,19 +678,18 @@ const defineSimpleTileMap = function (extension, _, gd) {
.addExtraInfo('image')
.setLabel(_('Atlas image'))
.setDescription(_('The Atlas image containing the tileset.'))
.setHidden(true) // Hidden because a full editor is needed to recompute column/row counts
);
return objectProperties;
};
objectSimpleTileMap.setRawJSONContent(
JSON.stringify({
atlasImage: '',
rowCount: 1,
columnCount: 1,
tileSize: 8,
tilesWithHitBox: '',
})
);
objectSimpleTileMap.content = {
atlasImage: '',
rowCount: 1,
columnCount: 1,
tileSize: 8,
tilesWithHitBox: '',
};
objectSimpleTileMap.updateInitialInstanceProperty = function (
instance,
@@ -710,10 +703,7 @@ const defineSimpleTileMap = function (extension, _, gd) {
return false;
};
objectSimpleTileMap.getInitialInstanceProperties = function (
objectContent,
instance
) {
objectSimpleTileMap.getInitialInstanceProperties = function (instance) {
var instanceProperties = new gd.MapStringPropertyDescriptor();
instanceProperties
@@ -735,6 +725,7 @@ const defineSimpleTileMap = function (extension, _, gd) {
objectSimpleTileMap
)
.setCategoryFullName(_('General'))
.setOpenFullEditorLabel(_('Edit tileset and collisions'))
.addDefaultBehavior('ResizableCapability::ResizableBehavior')
.addDefaultBehavior('ScalableCapability::ScalableBehavior')
.addDefaultBehavior('OpacityCapability::OpacityBehavior')
@@ -1076,11 +1067,8 @@ const defineSimpleTileMap = function (extension, _, gd) {
*/
const defineCollisionMask = function (extension, _, gd) {
var collisionMaskObject = new gd.ObjectJsImplementation();
collisionMaskObject.updateProperty = function (
objectContent,
propertyName,
newValue
) {
collisionMaskObject.updateProperty = function (propertyName, newValue) {
const objectContent = this.content;
if (propertyName === 'tilemapJsonFile') {
objectContent.tilemapJsonFile = newValue;
return true;
@@ -1120,8 +1108,9 @@ const defineCollisionMask = function (extension, _, gd) {
return false;
};
collisionMaskObject.getProperties = function (objectContent) {
var objectProperties = new gd.MapStringPropertyDescriptor();
collisionMaskObject.getProperties = function () {
const objectProperties = new gd.MapStringPropertyDescriptor();
const objectContent = this.content;
objectProperties.set(
'tilemapJsonFile',
@@ -1214,32 +1203,26 @@ const defineCollisionMask = function (extension, _, gd) {
return objectProperties;
};
collisionMaskObject.setRawJSONContent(
JSON.stringify({
tilemapJsonFile: '',
tilesetJsonFile: '',
collisionMaskTag: '',
debugMode: false,
fillColor: '255;255;255',
outlineColor: '255;255;255',
fillOpacity: 64,
outlineOpacity: 128,
outlineSize: 1,
})
);
collisionMaskObject.content = {
tilemapJsonFile: '',
tilesetJsonFile: '',
collisionMaskTag: '',
debugMode: false,
fillColor: '255;255;255',
outlineColor: '255;255;255',
fillOpacity: 64,
outlineOpacity: 128,
outlineSize: 1,
};
collisionMaskObject.updateInitialInstanceProperty = function (
objectContent,
instance,
propertyName,
newValue
) {
return false;
};
collisionMaskObject.getInitialInstanceProperties = function (
content,
instance
) {
collisionMaskObject.getInitialInstanceProperties = function (instance) {
var instanceProperties = new gd.MapStringPropertyDescriptor();
return instanceProperties;
};
@@ -1702,29 +1685,18 @@ module.exports = {
* This is used to reload the Tilemap
*/
updateTileMap() {
const tilemapObjectProperties = this._associatedObjectConfiguration.getProperties();
const object = gd.castObject(
this._associatedObjectConfiguration,
gd.ObjectJsImplementation
);
// Get the tileset resource to use
const tilemapAtlasImage = tilemapObjectProperties
.get('tilemapAtlasImage')
.getValue();
const tilemapJsonFile = tilemapObjectProperties
.get('tilemapJsonFile')
.getValue();
const tilesetJsonFile = tilemapObjectProperties
.get('tilesetJsonFile')
.getValue();
const layerIndex = parseInt(
tilemapObjectProperties.get('layerIndex').getValue(),
10
);
const levelIndex = parseInt(
tilemapObjectProperties.get('levelIndex').getValue(),
10
);
const displayMode = tilemapObjectProperties
.get('displayMode')
.getValue();
const tilemapAtlasImage = object.content.tilemapAtlasImage;
const tilemapJsonFile = object.content.tilemapJsonFile;
const tilesetJsonFile = object.content.tilesetJsonFile;
const layerIndex = object.content.layerIndex;
const levelIndex = object.content.levelIndex;
const displayMode = object.content.displayMode;
const tilemapResource = this._project
.getResourcesManager()
@@ -1815,29 +1787,18 @@ module.exports = {
* This is called to update the PIXI object on the scene editor, without reloading the tilemap.
*/
updatePixiTileMap() {
const tilemapObjectProperties = this._associatedObjectConfiguration.getProperties();
const object = gd.castObject(
this._associatedObjectConfiguration,
gd.ObjectJsImplementation
);
// Get the tileset resource to use
const tilemapAtlasImage = tilemapObjectProperties
.get('tilemapAtlasImage')
.getValue();
const tilemapJsonFile = tilemapObjectProperties
.get('tilemapJsonFile')
.getValue();
const tilesetJsonFile = tilemapObjectProperties
.get('tilesetJsonFile')
.getValue();
const layerIndex = parseInt(
tilemapObjectProperties.get('layerIndex').getValue(),
10
);
const levelIndex = parseInt(
tilemapObjectProperties.get('levelIndex').getValue(),
10
);
const displayMode = tilemapObjectProperties
.get('displayMode')
.getValue();
const tilemapAtlasImage = object.content.tilemapAtlasImage;
const tilemapJsonFile = object.content.tilemapJsonFile;
const tilesetJsonFile = object.content.tilesetJsonFile;
const layerIndex = object.content.layerIndex;
const levelIndex = object.content.levelIndex;
const displayMode = object.content.displayMode;
const tilemapResource = this._project
.getResourcesManager()
@@ -2139,10 +2100,12 @@ module.exports = {
* Return the path to the thumbnail of the specified object.
*/
static getThumbnail(project, resourcesLoader, objectConfiguration) {
const atlasImageResourceName = objectConfiguration
.getProperties()
.get('atlasImage')
.getValue();
const object = gd.castObject(
objectConfiguration,
gd.ObjectJsImplementation
);
const atlasImageResourceName = object.content.atlasImage || '';
return resourcesLoader.getResourceFullUrl(
project,
atlasImageResourceName,
@@ -2158,37 +2121,20 @@ module.exports = {
* This is used to reload the Tilemap
*/
updateTileMap() {
const atlasImageResourceName = this._associatedObjectConfiguration
.getProperties()
.get('atlasImage')
.getValue();
const object = gd.castObject(
this._associatedObjectConfiguration,
gd.ObjectJsImplementation
);
const atlasImageResourceName = object.content.atlasImage;
if (!atlasImageResourceName) return;
const tilemapAsJSObject = JSON.parse(
this._instance.getRawStringProperty('tilemap') || '{}'
);
const tileSize = parseInt(
this._associatedObjectConfiguration
.getProperties()
.get('tileSize')
.getValue(),
10
);
const columnCount = parseInt(
this._associatedObjectConfiguration
.getProperties()
.get('columnCount')
.getValue(),
10
);
const rowCount = parseInt(
this._associatedObjectConfiguration
.getProperties()
.get('rowCount')
.getValue(),
10
);
const tileSize = object.content.tileSize;
const columnCount = object.content.columnCount;
const rowCount = object.content.rowCount;
const atlasTexture = this._pixiResourcesLoader.getPIXITexture(
this._project,
@@ -2266,32 +2212,16 @@ module.exports = {
* This is called to update the PIXI object on the scene editor, without reloading the tilemap.
*/
updatePixiTileMap() {
const atlasImageResourceName = this._associatedObjectConfiguration
.getProperties()
.get('atlasImage')
.getValue();
const object = gd.castObject(
this._associatedObjectConfiguration,
gd.ObjectJsImplementation
);
const tileSize = parseInt(
this._associatedObjectConfiguration
.getProperties()
.get('tileSize')
.getValue(),
10
);
const columnCount = parseInt(
this._associatedObjectConfiguration
.getProperties()
.get('columnCount')
.getValue(),
10
);
const rowCount = parseInt(
this._associatedObjectConfiguration
.getProperties()
.get('rowCount')
.getValue(),
10
);
const atlasImageResourceName = object.content.atlasImage;
const tileSize = object.content.tileSize;
const columnCount = object.content.columnCount;
const rowCount = object.content.rowCount;
/** @type {TileMapHelper.TileMapManager} */
const manager = TilemapHelper.TileMapManager.getManager(this._project);
@@ -2329,10 +2259,11 @@ module.exports = {
* This is called to update the PIXI object on the scene editor
*/
update() {
const atlasImageResourceName = this._associatedObjectConfiguration
.getProperties()
.get('atlasImage')
.getValue();
const object = gd.castObject(
this._associatedObjectConfiguration,
gd.ObjectJsImplementation
);
const atlasImageResourceName = object.content.atlasImage;
const isTileMapEmpty = this._editableTileMap
? this._editableTileMap.isEmpty()
@@ -2524,47 +2455,22 @@ module.exports = {
* This is used to reload the Tilemap
*/
updateTileMap() {
// This might become useful in the future
/*
const tilemapAtlasImage = this._associatedObjectConfiguration
.getProperties(this.project)
.get('tilemapAtlasImage')
.getValue();
*/
const tilemapJsonFile = this._associatedObjectConfiguration
.getProperties()
.get('tilemapJsonFile')
.getValue();
const tilesetJsonFile = this._associatedObjectConfiguration
.getProperties()
.get('tilesetJsonFile')
.getValue();
const collisionMaskTag = this._associatedObjectConfiguration
.getProperties()
.get('collisionMaskTag')
.getValue();
const object = gd.castObject(
this._associatedObjectConfiguration,
gd.ObjectJsImplementation
);
const tilemapJsonFile = object.content.tilemapJsonFile;
const tilesetJsonFile = object.content.tilesetJsonFile;
const collisionMaskTag = object.content.collisionMaskTag;
const outlineColor = objectsRenderingService.rgbOrHexToHexNumber(
this._associatedObjectConfiguration
.getProperties()
.get('outlineColor')
.getValue()
object.content.outlineColor
);
const fillColor = objectsRenderingService.rgbOrHexToHexNumber(
this._associatedObjectConfiguration
.getProperties()
.get('fillColor')
.getValue()
object.content.fillColor
);
const outlineOpacity =
+this._associatedObjectConfiguration
.getProperties()
.get('outlineOpacity')
.getValue() / 255;
const fillOpacity =
+this._associatedObjectConfiguration
.getProperties()
.get('fillOpacity')
.getValue() / 255;
const outlineOpacity = object.content.outlineOpacity / 255;
const fillOpacity = object.content.fillOpacity / 255;
const outlineSize = 1;
/** @type {TileMapHelper.TileMapManager} */

View File

@@ -35,72 +35,67 @@ module.exports = {
.setIcon('JsPlatform/Extensions/videoicon16.png');
var videoObject = new gd.ObjectJsImplementation();
videoObject.updateProperty = function (
objectContent,
propertyName,
newValue
) {
videoObject.updateProperty = function (propertyName, newValue) {
console.log('update', this.content);
if (propertyName === 'Opacity') {
objectContent.opacity = parseFloat(newValue);
this.content.opacity = parseFloat(newValue);
return true;
}
if (propertyName === 'Looped') {
objectContent.loop = newValue === '1';
this.content.loop = newValue === '1';
return true;
}
if (propertyName === 'Volume') {
objectContent.volume = parseFloat(newValue);
this.content.volume = parseFloat(newValue);
return true;
}
if (propertyName === 'videoResource') {
objectContent.videoResource = newValue;
this.content.videoResource = newValue;
return true;
}
return false;
};
videoObject.getProperties = function (objectContent) {
videoObject.getProperties = function () {
var objectProperties = new gd.MapStringPropertyDescriptor();
console.log('getProperties', this.content);
objectProperties
.getOrCreate('Looped')
.setValue(objectContent.loop ? 'true' : 'false')
.setValue(this.content.loop ? 'true' : 'false')
.setType('boolean')
.setLabel(_('Loop the video'))
.setGroup(_('Playback settings'));
objectProperties
.getOrCreate('Volume')
.setValue(objectContent.volume.toString())
.setValue(this.content.volume.toString())
.setType('number')
.setLabel(_('Video volume (0-100)'))
.setGroup(_('Playback settings'));
objectProperties
.getOrCreate('videoResource')
.setValue(objectContent.videoResource)
.setValue(this.content.videoResource)
.setType('resource')
.addExtraInfo('video')
.setLabel(_('Video resource'));
return objectProperties;
};
videoObject.setRawJSONContent(
JSON.stringify({
opacity: 255,
loop: false,
volume: 100,
videoResource: '',
})
);
videoObject.content = {
opacity: 255,
loop: false,
volume: 100,
videoResource: '',
};
videoObject.updateInitialInstanceProperty = function (
objectContent,
instance,
propertyName,
newValue
) {
return false;
};
videoObject.getInitialInstanceProperties = function (content, instance) {
videoObject.getInitialInstanceProperties = function (instance) {
var instanceProperties = new gd.MapStringPropertyDescriptor();
return instanceProperties;
};
@@ -583,10 +578,11 @@ module.exports = {
_getVideoTexture() {
// Get the video resource to use
const videoResource = this._associatedObjectConfiguration
.getProperties()
.get('videoResource')
.getValue();
const object = gd.castObject(
this._associatedObjectConfiguration,
gd.ObjectJsImplementation
);
const videoResource = object.content.videoResource;
// This returns a VideoTexture with autoPlay set to false
return this._pixiResourcesLoader.getPIXIVideoTexture(
@@ -600,10 +596,12 @@ module.exports = {
*/
update() {
// Check if the video resource has changed
const videoResource = this._associatedObjectConfiguration
.getProperties()
.get('videoResource')
.getValue();
const object = gd.castObject(
this._associatedObjectConfiguration,
gd.ObjectJsImplementation
);
const videoResource = object.content.videoResource;
if (videoResource !== this._videoResource) {
this._videoResource = videoResource;
this._pixiObject.texture = this._getVideoTexture();

View File

@@ -867,9 +867,6 @@ interface ObjectJsImplementation {
[Const] DOMString name,
[Const] DOMString value);
[Const, Ref] DOMString GetRawJSONContent();
[Ref] ObjectJsImplementation SetRawJSONContent([Const] DOMString newContent);
void SerializeTo([Ref] SerializerElement element);
void UnserializeFrom([Ref] Project project, [Const, Ref] SerializerElement element);
@@ -909,6 +906,9 @@ interface CustomObjectConfiguration {
[Ref] SpriteAnimationList GetAnimations();
boolean IsChildObjectFolded([Const] DOMString childName);
void SetChildObjectFolded([Const] DOMString childName, boolean folded);
CustomObjectConfiguration_EdgeAnchor STATIC_GetEdgeAnchorFromString([Const] DOMString value);
};
CustomObjectConfiguration implements ObjectConfiguration;
@@ -1907,6 +1907,9 @@ interface ObjectMetadata {
[Ref] ObjectMetadata MarkAsRenderedIn3D();
boolean IsRenderedIn3D();
[Ref] ObjectMetadata SetOpenFullEditorLabel([Const] DOMString label);
[Const, Ref] DOMString GetOpenFullEditorLabel();
};
enum QuickCustomization_Visibility {
@@ -2032,6 +2035,9 @@ interface BehaviorMetadata {
QuickCustomization_Visibility GetQuickCustomizationVisibility();
[Ref] BehaviorMetadata SetQuickCustomizationVisibility(QuickCustomization_Visibility visibility);
[Ref] BehaviorMetadata SetOpenFullEditorLabel([Const] DOMString label);
[Const, Ref] DOMString GetOpenFullEditorLabel();
[Ref] Behavior Get();
BehaviorsSharedData GetSharedDataInstance();
@@ -3429,6 +3435,20 @@ interface Model3DObjectConfiguration {
boolean HasNoAnimations();
void SwapAnimations(unsigned long first, unsigned long second);
void MoveAnimation(unsigned long oldIndex, unsigned long newIndex);
double GetWidth();
double GetHeight();
double GetDepth();
double GetRotationX();
double GetRotationY();
double GetRotationZ();
[Const, Ref] DOMString GetModelResourceName();
[Const, Ref] DOMString GetMaterialType();
[Const, Ref] DOMString GetOriginLocation();
[Const, Ref] DOMString GetCenterLocation();
boolean shouldKeepAspectRatio();
};
Model3DObjectConfiguration implements ObjectConfiguration;
@@ -3458,6 +3478,9 @@ interface SpineObjectConfiguration {
boolean HasNoAnimations();
void SwapAnimations(unsigned long first, unsigned long second);
void MoveAnimation(unsigned long oldIndex, unsigned long newIndex);
double GetScale();
[Const, Ref] DOMString GetSpineResourceName();
};
SpineObjectConfiguration implements ObjectConfiguration;
@@ -3577,21 +3600,18 @@ interface ShapePainterObject {
void SetOutlineSize(double size);
double GetOutlineSize();
void SetOutlineColor([Const] DOMString color);
[Const, Ref] DOMString GetOutlineColor();
void SetOutlineOpacity(double val);
double GetOutlineOpacity();
void SetOutlineColor(unsigned long r, unsigned long g, unsigned long b);
unsigned long GetOutlineColorR();
unsigned long GetOutlineColorG();
unsigned long GetOutlineColorB();
void SetFillColor([Const] DOMString color);
[Const, Ref] DOMString GetFillColor();
void SetFillOpacity(double val);
double GetFillOpacity();
void SetFillColor(unsigned long r,unsigned long g, unsigned long b);
unsigned long GetFillColorR();
unsigned long GetFillColorG();
unsigned long GetFillColorB();
[Const, Value] DOMString GetAntialiasing();
void SetAntialiasing([Const] DOMString value);
@@ -3660,18 +3680,10 @@ interface ParticleEmitterObject {
void SetParticleLifeTimeMax(double newValue);
double GetParticleLifeTimeMax();
void SetParticleRed1(double newValue);
double GetParticleRed1();
void SetParticleRed2(double newValue);
double GetParticleRed2();
void SetParticleGreen1(double newValue);
double GetParticleGreen1();
void SetParticleGreen2(double newValue);
double GetParticleGreen2();
void SetParticleBlue1(double newValue);
double GetParticleBlue1();
void SetParticleBlue2(double newValue);
double GetParticleBlue2();
void SetParticleColor1([Const] DOMString newValue);
[Const, Ref] DOMString GetParticleColor1();
void SetParticleColor2([Const] DOMString newValue);
[Const, Ref] DOMString GetParticleColor2();
void SetParticleAlpha1(double newValue);
double GetParticleAlpha1();
void SetParticleAlpha2(double newValue);

View File

@@ -27,6 +27,10 @@ std::unique_ptr<gd::ObjectConfiguration> ObjectJsImplementation::Clone() const {
self['getInitialInstanceProperties'];
clone['updateInitialInstanceProperty'] =
self['updateInitialInstanceProperty'];
// Make a clone of the JavaScript object containing the data. If we don't do that, the
// content of the object would be shared between the original and the clone.
clone['content'] = Module['_deepCloneForObjectJsImplementationContent'](self['content']);
},
(int)clone,
(int)this);
@@ -45,15 +49,13 @@ ObjectJsImplementation::GetProperties() const {
if (!self.hasOwnProperty('getProperties'))
throw 'getProperties is not defined on a ObjectJsImplementation.';
var objectContent = JSON.parse(UTF8ToString($1));
var newProperties = self['getProperties'](objectContent);
var newProperties = self['getProperties']();
if (!newProperties)
throw 'getProperties returned nothing in a gd::ObjectJsImplementation.';
return getPointer(newProperties);
},
(int)this,
jsonContent.c_str());
(int)this);
copiedProperties = *jsCreatedProperties;
delete jsCreatedProperties;
@@ -61,18 +63,15 @@ ObjectJsImplementation::GetProperties() const {
}
bool ObjectJsImplementation::UpdateProperty(const gd::String& arg0,
const gd::String& arg1) {
jsonContent = (const char*)EM_ASM_INT(
EM_ASM_INT(
{
var self = Module['getCache'](Module['ObjectJsImplementation'])[$0];
if (!self.hasOwnProperty('updateProperty'))
throw 'updateProperty is not defined on a ObjectJsImplementation.';
var objectContent = JSON.parse(UTF8ToString($1));
self['updateProperty'](
objectContent, UTF8ToString($2), UTF8ToString($3));
return ensureString(JSON.stringify(objectContent));
self['updateProperty'](UTF8ToString($1), UTF8ToString($2));
},
(int)this,
jsonContent.c_str(),
arg0.c_str(),
arg1.c_str());
@@ -91,17 +90,14 @@ ObjectJsImplementation::GetInitialInstanceProperties(
if (!self.hasOwnProperty('getInitialInstanceProperties'))
throw 'getInitialInstanceProperties is not defined on a ObjectJsImplementation.';
var objectContent = JSON.parse(UTF8ToString($1));
var newProperties = self['getInitialInstanceProperties'](
objectContent,
wrapPointer($2, Module['InitialInstance']));
wrapPointer($1, Module['InitialInstance']));
if (!newProperties)
throw 'getInitialInstanceProperties returned nothing in a gd::ObjectJsImplementation.';
return getPointer(newProperties);
},
(int)this,
jsonContent.c_str(),
(int)&instance);
copiedProperties = *jsCreatedProperties;
@@ -118,26 +114,53 @@ bool ObjectJsImplementation::UpdateInitialInstanceProperty(
var self = Module['getCache'](Module['ObjectJsImplementation'])[$0];
if (!self.hasOwnProperty('updateInitialInstanceProperty'))
throw 'updateInitialInstanceProperty is not defined on a ObjectJsImplementation.';
var objectContent = JSON.parse(UTF8ToString($1));
return self['updateInitialInstanceProperty'](
objectContent,
wrapPointer($2, Module['InitialInstance']),
UTF8ToString($3),
UTF8ToString($4));
wrapPointer($1, Module['InitialInstance']),
UTF8ToString($2),
UTF8ToString($3));
},
(int)this,
jsonContent.c_str(),
(int)&instance,
name.c_str(),
value.c_str());
}
void ObjectJsImplementation::DoSerializeTo(SerializerElement& element) const {
element.AddChild("content") = gd::Serializer::FromJSON(jsonContent);
SerializerElement* jsCreatedElement = (SerializerElement*)EM_ASM_INT(
{
var self = Module['getCache'](Module['ObjectJsImplementation'])[$0];
if (!self.content)
throw '`content` is not defined on a ObjectJsImplementation.';
var serializerElement = gd.Serializer.fromJSObject(self.content);
return getPointer(serializerElement);
},
(int)this);
// We could avoid a copy by using making a function on gd.Serializer that manipulates
// directly the SerializerElement passed to it.
element.AddChild("content") = *jsCreatedElement;
delete jsCreatedElement;
}
void ObjectJsImplementation::DoUnserializeFrom(Project& project,
const SerializerElement& element) {
jsonContent = gd::Serializer::ToJSON(element.GetChild("content"));
EM_ASM_INT(
{
var self = Module['getCache'](Module['ObjectJsImplementation'])[$0];
if (!self.content)
throw '`content` is not defined on a ObjectJsImplementation.';
var serializerElement = wrapPointer($1, Module['SerializerElement']);
if (!serializerElement.isValueUndefined() || serializerElement.consideredAsArray()) {
throw new Error('The element passed to ObjectJsImplementation::DoUnserializeFrom is not an object.');
}
// JSON.parse + toJSON is 30% faster than gd.Serializer.toJSObject.
self.content = JSON.parse(gd.Serializer.toJSON(serializerElement));
},
(int)this,
(int)&element.GetChild("content"));
}
void ObjectJsImplementation::__destroy__() { // Useless?

View File

@@ -9,15 +9,17 @@
using namespace gd;
/**
* \brief A gd::Object that stores its content in JSON and forward the
* properties related functions to Javascript with Emscripten.
* \brief A gd::ObjectConfiguration that wraps a Javascript object:
* the content of the object is stored in JavaScript in a "content" property,
* allowing both fast access in JavaScript and still ability to access properties
* via the usual methods.
*
* It also implements "ExposeResources" to expose the properties of type
* "resource".
*/
class ObjectJsImplementation : public gd::ObjectConfiguration {
public:
ObjectJsImplementation() : jsonContent("{}") {}
ObjectJsImplementation() {}
std::unique_ptr<gd::ObjectConfiguration> Clone() const override;
std::map<gd::String, gd::PropertyDescriptor> GetProperties() const override;
@@ -31,16 +33,9 @@ class ObjectJsImplementation : public gd::ObjectConfiguration {
void __destroy__();
const gd::String& GetRawJSONContent() const { return jsonContent; };
ObjectJsImplementation& SetRawJSONContent(const gd::String& newContent) {
jsonContent = newContent;
return *this;
};
void ExposeResources(gd::ArbitraryResourceWorker& worker) override;
protected:
void DoSerializeTo(SerializerElement& arg0) const override;
void DoUnserializeFrom(Project& arg0, const SerializerElement& arg1) override;
gd::String jsonContent;
};

View File

@@ -277,6 +277,42 @@ var adaptNamingConventions = function (gd) {
this.insert(e, this.size() - 1);
};
// A deep clone function for the `content` of `ObjectJsImplementation`.
function deepClone(obj) {
// Handle null, undefined, and non-object values
if (obj === null || typeof obj !== 'object') {
return obj;
}
// Handle Date
if (obj instanceof Date) {
return new Date(obj.getTime());
}
// Handle Array
if (Array.isArray(obj)) {
const clonedArr = [];
for (let i = 0; i < obj.length; i++) {
clonedArr.push(deepClone(obj[i]));
}
return clonedArr;
}
// Handle Objects
const clonedObj = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) { // Ensure key is directly on obj
clonedObj[key] = deepClone(obj[key]);
}
}
return clonedObj;
}
gd._deepCloneForObjectJsImplementationContent = function (obj) {
return deepClone(obj);
}
return gd;
};

View File

@@ -84,15 +84,15 @@ module.exports = {
const fakeObject = new gd.ObjectJsImplementation();
fakeObject.updateProperty = function (
objectContent,
propertyName,
newValue
) {
return false;
};
fakeObject.getProperties = function (objectContent) {
fakeObject.getProperties = function () {
const objectProperties = new gd.MapStringPropertyDescriptor();
const objectContent = this.content;
objectProperties
.getOrCreate('text')
@@ -102,14 +102,11 @@ module.exports = {
return objectProperties;
};
fakeObject.setRawJSONContent(
JSON.stringify({
text: 'Some text.',
})
);
fakeObject.content = {
text: 'Some text.',
};
fakeObject.updateInitialInstanceProperty = function (
objectContent,
instance,
propertyName,
newValue
@@ -118,7 +115,6 @@ module.exports = {
};
fakeObject.getInitialInstanceProperties = function (
content,
instance
) {
var instanceProperties = new gd.MapStringPropertyDescriptor();
@@ -142,21 +138,19 @@ module.exports = {
const fakeObject = new gd.ObjectJsImplementation();
fakeObject.updateProperty = function (
objectContent,
propertyName,
newValue
) {
return false;
};
fakeObject.getProperties = function (objectContent) {
fakeObject.getProperties = function () {
const objectProperties = new gd.MapStringPropertyDescriptor();
return objectProperties;
};
fakeObject.setRawJSONContent(JSON.stringify({}));
fakeObject.content = {};
fakeObject.updateInitialInstanceProperty = function (
objectContent,
instance,
propertyName,
newValue
@@ -165,7 +159,6 @@ module.exports = {
};
fakeObject.getInitialInstanceProperties = function (
content,
instance
) {
var instanceProperties = new gd.MapStringPropertyDescriptor();

View File

@@ -2105,38 +2105,35 @@ describe('libGD.js', function () {
describe('gd.ObjectJsImplementation', function () {
const createSampleObjectJsImplementation = () => {
let myObject = new gd.ObjectJsImplementation();
myObject.updateProperty = function (content, propertyName, newValue) {
myObject.updateProperty = function (propertyName, newValue) {
if (propertyName === 'My first property') {
content.property1 = newValue;
this.content.property1 = newValue;
return true;
}
if (propertyName === 'My other property') {
content.property2 = newValue === '1';
this.content.property2 = newValue === '1';
return true;
}
return false;
};
myObject.getProperties = function (content) {
myObject.getProperties = function () {
let properties = new gd.MapStringPropertyDescriptor();
properties.getOrCreate('My first property').setValue(content.property1);
properties.getOrCreate('My first property').setValue(this.content.property1);
properties
.getOrCreate('My other property')
.setValue(content.property2 ? '1' : '0')
.setValue(this.content.property2 ? '1' : '0')
.setType('Boolean');
return properties;
};
myObject.setRawJSONContent(
JSON.stringify({
property1: 'Initial value 1',
property2: true,
})
);
myObject.content = {
property1: 'Initial value 1',
property2: true,
};
myObject.updateInitialInstanceProperty = function (
content,
instance,
propertyName,
newValue
@@ -2153,7 +2150,6 @@ describe('libGD.js', function () {
return false;
};
myObject.getInitialInstanceProperties = function (
content,
instance
) {
let properties = new gd.MapStringPropertyDescriptor();
@@ -2169,11 +2165,8 @@ describe('libGD.js', function () {
return properties;
};
// TODO: Workaround a bad design of ObjectJsImplementation. When getProperties
// and associated methods are redefined in JS, they have different arguments (
// see ObjectJsImplementation C++ implementation). If called directly here from JS,
// the arguments will be mismatched. To workaround this, always case the object to
// a base gdObject to ensure C++ methods are called.
// Cast the object to a base gdObjectConfiguration to ensure C++ methods are called,
// and verify they properly call their JS equivalents.
return gd.castObject(myObject, gd.ObjectConfiguration);
};
@@ -2229,7 +2222,12 @@ describe('libGD.js', function () {
const object2 = object1.clone().release();
const object3 = object1.clone().release();
const object1jsImplementation = gd.castObject(object1, gd.ObjectJsImplementation);
const object2jsImplementation = gd.castObject(object2, gd.ObjectJsImplementation);
const object3jsImplementation = gd.castObject(object3, gd.ObjectJsImplementation);
{
// Check properties can be accessed.
const propertiesObject1 = object1.getProperties();
expect(propertiesObject1.has('My first property'));
expect(
@@ -2240,9 +2238,24 @@ describe('libGD.js', function () {
expect(
propertiesObject2.get('My first property').getValue() == 'test1'
);
// Check the JavaScript objects are unchanged for now.
expect(object1jsImplementation.content).toEqual({
property1: 'test1',
property2: true,
});
expect(object2jsImplementation.content).toEqual({
property1: 'test1',
property2: true,
});
expect(object3jsImplementation.content).toEqual({
property1: 'test1',
property2: true,
});
}
{
// Check a property can be updated.
object1.updateProperty('My first property', 'updated value');
const propertiesObject1 = object1.getProperties();
expect(propertiesObject1.has('My first property'));
@@ -2255,9 +2268,24 @@ describe('libGD.js', function () {
expect(
propertiesObject2.get('My first property').getValue() == 'test1'
);
// Check the JavaScript objects are updated.
expect(object1jsImplementation.content).toEqual({
property1: 'updated value',
property2: true,
});
expect(object2jsImplementation.content).toEqual({
property1: 'test1',
property2: true,
});
expect(object3jsImplementation.content).toEqual({
property1: 'test1',
property2: true,
});
}
{
// Check a property from another object can be updated.
object2.updateProperty('My first property', 'updated value object 2');
const propertiesObject1 = object1.getProperties();
expect(propertiesObject1.has('My first property'));
@@ -2276,6 +2304,20 @@ describe('libGD.js', function () {
expect(
propertiesObject3.get('My first property').getValue() == 'test1'
);
// Check the JavaScript objects are updated.
expect(object1jsImplementation.content).toEqual({
property1: 'updated value',
property2: true,
});
expect(object2jsImplementation.content).toEqual({
property1: 'updated value object 2',
property2: true,
});
expect(object3jsImplementation.content).toEqual({
property1: 'test1',
property2: true,
});
}
});
});

View File

@@ -77,8 +77,9 @@ const extraClassAttributes = {
'static Default = 0;',
'static Visible = 1;',
'static Hidden = 2;',
]
}
],
ObjectJsImplementation: ['content: Record<string, any>;'],
};
const PrimitiveTypes = new Map([
['DOMString', 'string'],
@@ -340,7 +341,9 @@ for (const [
} {${methods.length ? '\n ' + methods.join('\n ') : ''}${
attributes.length ? '\n ' + attributes.join('\n ') : ''
}${
(extraClassAttributes[interfaceName] || []).join('\n ')
extraClassAttributes[interfaceName]
? '\n ' + extraClassAttributes[interfaceName].join('\n ')
: ''
}
}`
);

View File

@@ -733,10 +733,9 @@ export class ObjectJsImplementation extends ObjectConfiguration {
updateProperty(name: string, value: string): boolean;
getInitialInstanceProperties(instance: InitialInstance): MapStringPropertyDescriptor;
updateInitialInstanceProperty(instance: InitialInstance, name: string, value: string): boolean;
getRawJSONContent(): string;
setRawJSONContent(newContent: string): ObjectJsImplementation;
serializeTo(element: SerializerElement): void;
unserializeFrom(project: Project, element: SerializerElement): void;
content: Record<string, any>;
}
export class CustomObjectConfiguration extends ObjectConfiguration {
@@ -751,6 +750,8 @@ export class CustomObjectConfiguration extends ObjectConfiguration {
getInitialInstanceProperties(instance: InitialInstance): MapStringPropertyDescriptor;
updateInitialInstanceProperty(instance: InitialInstance, name: string, value: string): boolean;
getAnimations(): SpriteAnimationList;
isChildObjectFolded(childName: string): boolean;
setChildObjectFolded(childName: string, folded: boolean): void;
static getEdgeAnchorFromString(value: string): CustomObjectConfiguration_EdgeAnchor;
}
@@ -1525,9 +1526,12 @@ export class ObjectMetadata extends EmscriptenObject {
isHidden(): boolean;
markAsRenderedIn3D(): ObjectMetadata;
isRenderedIn3D(): boolean;
setOpenFullEditorLabel(label: string): ObjectMetadata;
getOpenFullEditorLabel(): string;
}
export class QuickCustomization extends EmscriptenObject {static Default = 0;
export class QuickCustomization extends EmscriptenObject {
static Default = 0;
static Visible = 1;
static Hidden = 2;
}
@@ -1573,6 +1577,8 @@ export class BehaviorMetadata extends EmscriptenObject {
setHidden(): BehaviorMetadata;
getQuickCustomizationVisibility(): QuickCustomization_Visibility;
setQuickCustomizationVisibility(visibility: QuickCustomization_Visibility): BehaviorMetadata;
setOpenFullEditorLabel(label: string): BehaviorMetadata;
getOpenFullEditorLabel(): string;
get(): Behavior;
getSharedDataInstance(): BehaviorsSharedData;
getProperties(): MapStringPropertyDescriptor;
@@ -2544,6 +2550,17 @@ export class Model3DObjectConfiguration extends ObjectConfiguration {
hasNoAnimations(): boolean;
swapAnimations(first: number, second: number): void;
moveAnimation(oldIndex: number, newIndex: number): void;
getWidth(): number;
getHeight(): number;
getDepth(): number;
getRotationX(): number;
getRotationY(): number;
getRotationZ(): number;
getModelResourceName(): string;
getMaterialType(): string;
getOriginLocation(): string;
getCenterLocation(): string;
shouldKeepAspectRatio(): boolean;
}
export class SpineAnimation extends EmscriptenObject {
@@ -2567,6 +2584,8 @@ export class SpineObjectConfiguration extends ObjectConfiguration {
hasNoAnimations(): boolean;
swapAnimations(first: number, second: number): void;
moveAnimation(oldIndex: number, newIndex: number): void;
getScale(): number;
getSpineResourceName(): string;
}
export class Vector2f extends EmscriptenObject {
@@ -2663,18 +2682,14 @@ export class ShapePainterObject extends ObjectConfiguration {
isClearedBetweenFrames(): boolean;
setOutlineSize(size: number): void;
getOutlineSize(): number;
setOutlineColor(color: string): void;
getOutlineColor(): string;
setOutlineOpacity(val: number): void;
getOutlineOpacity(): number;
setOutlineColor(r: number, g: number, b: number): void;
getOutlineColorR(): number;
getOutlineColorG(): number;
getOutlineColorB(): number;
setFillColor(color: string): void;
getFillColor(): string;
setFillOpacity(val: number): void;
getFillOpacity(): number;
setFillColor(r: number, g: number, b: number): void;
getFillColorR(): number;
getFillColorG(): number;
getFillColorB(): number;
getAntialiasing(): string;
setAntialiasing(value: string): void;
}
@@ -2724,18 +2739,10 @@ export class ParticleEmitterObject extends ObjectConfiguration {
getParticleLifeTimeMin(): number;
setParticleLifeTimeMax(newValue: number): void;
getParticleLifeTimeMax(): number;
setParticleRed1(newValue: number): void;
getParticleRed1(): number;
setParticleRed2(newValue: number): void;
getParticleRed2(): number;
setParticleGreen1(newValue: number): void;
getParticleGreen1(): number;
setParticleGreen2(newValue: number): void;
getParticleGreen2(): number;
setParticleBlue1(newValue: number): void;
getParticleBlue1(): number;
setParticleBlue2(newValue: number): void;
getParticleBlue2(): number;
setParticleColor1(newValue: string): void;
getParticleColor1(): string;
setParticleColor2(newValue: string): void;
getParticleColor2(): string;
setParticleAlpha1(newValue: number): void;
getParticleAlpha1(): number;
setParticleAlpha2(newValue: number): void;

View File

@@ -35,6 +35,8 @@ declare class gdBehaviorMetadata {
setHidden(): gdBehaviorMetadata;
getQuickCustomizationVisibility(): QuickCustomization_Visibility;
setQuickCustomizationVisibility(visibility: QuickCustomization_Visibility): gdBehaviorMetadata;
setOpenFullEditorLabel(label: string): gdBehaviorMetadata;
getOpenFullEditorLabel(): string;
get(): gdBehavior;
getSharedDataInstance(): gdBehaviorsSharedData;
getProperties(): gdMapStringPropertyDescriptor;

View File

@@ -16,6 +16,8 @@ declare class gdCustomObjectConfiguration extends gdObjectConfiguration {
getInitialInstanceProperties(instance: gdInitialInstance): gdMapStringPropertyDescriptor;
updateInitialInstanceProperty(instance: gdInitialInstance, name: string, value: string): boolean;
getAnimations(): gdSpriteAnimationList;
isChildObjectFolded(childName: string): boolean;
setChildObjectFolded(childName: string, folded: boolean): void;
static getEdgeAnchorFromString(value: string): CustomObjectConfiguration_EdgeAnchor;
delete(): void;
ptr: number;

View File

@@ -10,6 +10,17 @@ declare class gdModel3DObjectConfiguration extends gdObjectConfiguration {
hasNoAnimations(): boolean;
swapAnimations(first: number, second: number): void;
moveAnimation(oldIndex: number, newIndex: number): void;
getWidth(): number;
getHeight(): number;
getDepth(): number;
getRotationX(): number;
getRotationY(): number;
getRotationZ(): number;
getModelResourceName(): string;
getMaterialType(): string;
getOriginLocation(): string;
getCenterLocation(): string;
shouldKeepAspectRatio(): boolean;
delete(): void;
ptr: number;
};

View File

@@ -6,8 +6,6 @@ declare class gdObjectJsImplementation extends gdObjectConfiguration {
updateProperty(name: string, value: string): boolean;
getInitialInstanceProperties(instance: gdInitialInstance): gdMapStringPropertyDescriptor;
updateInitialInstanceProperty(instance: gdInitialInstance, name: string, value: string): boolean;
getRawJSONContent(): string;
setRawJSONContent(newContent: string): gdObjectJsImplementation;
serializeTo(element: gdSerializerElement): void;
unserializeFrom(project: gdProject, element: gdSerializerElement): void;
delete(): void;

View File

@@ -28,6 +28,8 @@ declare class gdObjectMetadata {
isHidden(): boolean;
markAsRenderedIn3D(): gdObjectMetadata;
isRenderedIn3D(): boolean;
setOpenFullEditorLabel(label: string): gdObjectMetadata;
getOpenFullEditorLabel(): string;
delete(): void;
ptr: number;
};

View File

@@ -40,18 +40,10 @@ declare class gdParticleEmitterObject extends gdObjectConfiguration {
getParticleLifeTimeMin(): number;
setParticleLifeTimeMax(newValue: number): void;
getParticleLifeTimeMax(): number;
setParticleRed1(newValue: number): void;
getParticleRed1(): number;
setParticleRed2(newValue: number): void;
getParticleRed2(): number;
setParticleGreen1(newValue: number): void;
getParticleGreen1(): number;
setParticleGreen2(newValue: number): void;
getParticleGreen2(): number;
setParticleBlue1(newValue: number): void;
getParticleBlue1(): number;
setParticleBlue2(newValue: number): void;
getParticleBlue2(): number;
setParticleColor1(newValue: string): void;
getParticleColor1(): string;
setParticleColor2(newValue: string): void;
getParticleColor2(): string;
setParticleAlpha1(newValue: number): void;
getParticleAlpha1(): number;
setParticleAlpha2(newValue: number): void;

View File

@@ -8,18 +8,14 @@ declare class gdShapePainterObject extends gdObjectConfiguration {
isClearedBetweenFrames(): boolean;
setOutlineSize(size: number): void;
getOutlineSize(): number;
setOutlineColor(color: string): void;
getOutlineColor(): string;
setOutlineOpacity(val: number): void;
getOutlineOpacity(): number;
setOutlineColor(r: number, g: number, b: number): void;
getOutlineColorR(): number;
getOutlineColorG(): number;
getOutlineColorB(): number;
setFillColor(color: string): void;
getFillColor(): string;
setFillOpacity(val: number): void;
getFillOpacity(): number;
setFillColor(r: number, g: number, b: number): void;
getFillColorR(): number;
getFillColorG(): number;
getFillColorB(): number;
getAntialiasing(): string;
setAntialiasing(value: string): void;
delete(): void;

View File

@@ -10,6 +10,8 @@ declare class gdSpineObjectConfiguration extends gdObjectConfiguration {
hasNoAnimations(): boolean;
swapAnimations(first: number, second: number): void;
moveAnimation(oldIndex: number, newIndex: number): void;
getScale(): number;
getSpineResourceName(): string;
delete(): void;
ptr: number;
};

View File

@@ -284,74 +284,55 @@ const BehaviorConfigurationEditor = React.forwardRef<
}
);
type Props = {|
project: gdProject,
eventsFunctionsExtension: gdEventsFunctionsExtension | null,
object: gdObject,
onUpdateBehaviorsSharedData: () => void,
onSizeUpdated?: ?() => void,
resourceManagementProps: ResourceManagementProps,
onBehaviorsUpdated: () => void,
openBehaviorEvents: (
extensionName: string,
behaviorName: string
) => Promise<void>,
type UseManageBehaviorsState = {|
// Operations:
changeBehaviorName: (behavior: gdBehavior, newName: string) => void,
removeBehavior: (behaviorName: string) => void,
copyBehavior: (behaviorName: string) => void,
copyAllBehaviors: () => void,
pasteBehaviors: () => Promise<void>,
openNewBehaviorDialog: () => void,
resetJustAddedBehaviorName: () => void,
// Visual state:
newBehaviorDialog: React.Node,
justAddedBehaviorName: ?string,
|};
const BehaviorsEditor = (props: Props) => {
const { isMobile } = useResponsiveWindowSize();
const scrollView = React.useRef<?ScrollViewInterface>(null);
/**
* A hook allowing to add/remove/modify behaviors of an object.
*/
export const useManageObjectBehaviors = ({
project,
object,
eventsFunctionsExtension,
onUpdate,
onSizeUpdated,
onBehaviorsUpdated,
onUpdateBehaviorsSharedData,
}: {
project: gdProject,
object: gdObject,
eventsFunctionsExtension: gdEventsFunctionsExtension | null,
onUpdate: () => void,
onSizeUpdated?: ?() => void,
onBehaviorsUpdated?: ?() => void,
onUpdateBehaviorsSharedData: () => void,
}): UseManageBehaviorsState => {
const [
justAddedBehaviorName,
setJustAddedBehaviorName,
] = React.useState<?string>(null);
const justAddedBehaviorAccordionElement = React.useRef<?BehaviorConfigurationEditorInterface>(
null
);
React.useEffect(
() => {
if (
scrollView.current &&
justAddedBehaviorAccordionElement.current &&
justAddedBehaviorName
) {
scrollView.current.scrollTo(justAddedBehaviorAccordionElement.current);
setJustAddedBehaviorName(null);
justAddedBehaviorAccordionElement.current = null;
}
},
[justAddedBehaviorName]
);
const [newBehaviorDialogOpen, setNewBehaviorDialogOpen] = React.useState(
false
);
const [
selectedQuickCustomizationPropertiesBehavior,
setSelectedQuickCustomizationPropertiesBehavior,
] = React.useState<?gdBehavior>(null);
const openNewBehaviorDialog = React.useCallback(() => {
setNewBehaviorDialogOpen(true);
}, []);
const showBehaviorOverridingConfirmation = useBehaviorOverridingAlertDialog();
const {
object,
project,
eventsFunctionsExtension,
onSizeUpdated,
onBehaviorsUpdated,
onUpdateBehaviorsSharedData,
openBehaviorEvents,
} = props;
// As for now, any default behavior is hidden,
// it avoids to get behavior metadata to check the "hidden" flag.
const allVisibleBehaviors = object
.getAllBehaviorNames()
.toJSArray()
.map(behaviorName => object.getBehavior(behaviorName))
.filter(behavior => !behavior.isDefaultBehavior());
const forceUpdate = useForceUpdate();
const addBehavior = React.useCallback(
(type: string, defaultName: string) => {
const wasBehaviorAdded = addBehaviorToObject(
@@ -370,13 +351,13 @@ const BehaviorsEditor = (props: Props) => {
setJustAddedBehaviorName(defaultName);
}
forceUpdate();
onUpdate();
if (onSizeUpdated) onSizeUpdated();
onUpdateBehaviorsSharedData();
if (onBehaviorsUpdated) onBehaviorsUpdated();
},
[
forceUpdate,
onUpdate,
object,
onBehaviorsUpdated,
onSizeUpdated,
@@ -385,7 +366,7 @@ const BehaviorsEditor = (props: Props) => {
]
);
const onChangeBehaviorName = React.useCallback(
const changeBehaviorName = React.useCallback(
(behavior: gdBehavior, newName: string) => {
// TODO: This is disabled for now as there is no proper refactoring
// of events after a behavior renaming. Once refactoring is available,
@@ -395,13 +376,13 @@ const BehaviorsEditor = (props: Props) => {
if (object.hasBehaviorNamed(newName)) return;
object.renameBehavior(behavior.getName(), newName);
forceUpdate();
onUpdate();
if (onBehaviorsUpdated) onBehaviorsUpdated();
},
[forceUpdate, object, onBehaviorsUpdated]
[onUpdate, object, onBehaviorsUpdated]
);
const onRemoveBehavior = React.useCallback(
const removeBehavior = React.useCallback(
(behaviorName: string) => {
let message =
"Are you sure you want to remove this behavior? This can't be undone.";
@@ -437,9 +418,9 @@ const BehaviorsEditor = (props: Props) => {
serializedBehavior: serializeToJSObject(behavior),
},
]);
forceUpdate();
onUpdate();
},
[forceUpdate, object]
[onUpdate, object]
);
const copyAllBehaviors = React.useCallback(
@@ -458,9 +439,9 @@ const BehaviorsEditor = (props: Props) => {
};
}).filter(Boolean)
);
forceUpdate();
onUpdate();
},
[forceUpdate, object]
[onUpdate, object]
);
const pasteBehaviors = React.useCallback(
@@ -560,7 +541,7 @@ const BehaviorsEditor = (props: Props) => {
}
}
forceUpdate();
onUpdate();
if (firstAddedBehaviorName) {
setJustAddedBehaviorName(firstAddedBehaviorName);
if (onSizeUpdated) onSizeUpdated();
@@ -573,7 +554,7 @@ const BehaviorsEditor = (props: Props) => {
}
},
[
forceUpdate,
onUpdate,
object,
onBehaviorsUpdated,
onSizeUpdated,
@@ -583,6 +564,115 @@ const BehaviorsEditor = (props: Props) => {
]
);
const newBehaviorDialog = newBehaviorDialogOpen && (
<NewBehaviorDialog
open
objectType={object.getType()}
objectBehaviorsTypes={listObjectBehaviorsTypes(object)}
onClose={() => setNewBehaviorDialogOpen(false)}
onChoose={addBehavior}
project={project}
eventsFunctionsExtension={eventsFunctionsExtension}
/>
);
const resetJustAddedBehaviorName = React.useCallback(() => {
setJustAddedBehaviorName(null);
}, []);
return {
changeBehaviorName,
removeBehavior,
copyBehavior,
copyAllBehaviors,
pasteBehaviors,
newBehaviorDialog,
openNewBehaviorDialog,
justAddedBehaviorName,
resetJustAddedBehaviorName,
};
};
type Props = {|
project: gdProject,
eventsFunctionsExtension: gdEventsFunctionsExtension | null,
object: gdObject,
onUpdateBehaviorsSharedData: () => void,
onSizeUpdated?: ?() => void,
resourceManagementProps: ResourceManagementProps,
onBehaviorsUpdated: () => void,
openBehaviorEvents: (
extensionName: string,
behaviorName: string
) => Promise<void>,
|};
const BehaviorsEditor = (props: Props) => {
const { isMobile } = useResponsiveWindowSize();
const scrollView = React.useRef<?ScrollViewInterface>(null);
const justAddedBehaviorAccordionElement = React.useRef<?BehaviorConfigurationEditorInterface>(
null
);
const {
object,
project,
eventsFunctionsExtension,
onSizeUpdated,
onBehaviorsUpdated,
onUpdateBehaviorsSharedData,
openBehaviorEvents,
} = props;
const forceUpdate = useForceUpdate();
const [
selectedQuickCustomizationPropertiesBehavior,
setSelectedQuickCustomizationPropertiesBehavior,
] = React.useState<?gdBehavior>(null);
const {
changeBehaviorName,
removeBehavior,
copyBehavior,
copyAllBehaviors,
pasteBehaviors,
newBehaviorDialog,
openNewBehaviorDialog,
justAddedBehaviorName,
resetJustAddedBehaviorName,
} = useManageObjectBehaviors({
project,
object,
eventsFunctionsExtension,
onUpdate: forceUpdate,
onSizeUpdated,
onBehaviorsUpdated,
onUpdateBehaviorsSharedData,
});
React.useEffect(
() => {
if (
scrollView.current &&
justAddedBehaviorAccordionElement.current &&
justAddedBehaviorName
) {
scrollView.current.scrollTo(justAddedBehaviorAccordionElement.current);
resetJustAddedBehaviorName();
justAddedBehaviorAccordionElement.current = null;
}
},
[justAddedBehaviorName, resetJustAddedBehaviorName]
);
// As for now, any default behavior is hidden,
// it avoids to get behavior metadata to check the "hidden" flag.
const allVisibleBehaviors = object
.getAllBehaviorNames()
.toJSArray()
.map(behaviorName => object.getBehavior(behaviorName))
.filter(behavior => !behavior.isDefaultBehavior());
const openExtension = React.useCallback(
(behaviorType: string) => {
const elements = behaviorType.split('::');
@@ -634,7 +724,7 @@ const BehaviorsEditor = (props: Props) => {
actionLabel={
isMobile ? <Trans>Add</Trans> : <Trans>Add a behavior</Trans>
}
onAction={() => setNewBehaviorDialogOpen(true)}
onAction={openNewBehaviorDialog}
secondaryActionIcon={<PasteIcon />}
secondaryActionLabel={
isClipboardContainingBehaviors ? <Trans>Paste</Trans> : null
@@ -663,9 +753,9 @@ const BehaviorsEditor = (props: Props) => {
object={object}
behavior={behavior}
copyBehavior={copyBehavior}
onRemoveBehavior={onRemoveBehavior}
onRemoveBehavior={removeBehavior}
onBehaviorsUpdated={onBehaviorsUpdated}
onChangeBehaviorName={onChangeBehaviorName}
onChangeBehaviorName={changeBehaviorName}
openExtension={openExtension}
openBehaviorPropertiesQuickCustomizationDialog={
openBehaviorPropertiesQuickCustomizationDialog
@@ -715,7 +805,7 @@ const BehaviorsEditor = (props: Props) => {
)
}
primary
onClick={() => setNewBehaviorDialogOpen(true)}
onClick={openNewBehaviorDialog}
icon={<Add />}
id="add-behavior-button"
/>
@@ -724,19 +814,7 @@ const BehaviorsEditor = (props: Props) => {
</Column>
</React.Fragment>
)}
{newBehaviorDialogOpen && (
<NewBehaviorDialog
open={newBehaviorDialogOpen}
objectType={object.getType()}
objectBehaviorsTypes={listObjectBehaviorsTypes(object)}
onClose={() => setNewBehaviorDialogOpen(false)}
onChoose={addBehavior}
project={project}
eventsFunctionsExtension={eventsFunctionsExtension}
/>
)}
{newBehaviorDialog}
{!!selectedQuickCustomizationPropertiesBehavior && (
<QuickCustomizationPropertiesVisibilityDialog
open={!!selectedQuickCustomizationPropertiesBehavior}

View File

@@ -9,7 +9,7 @@ import { tooltipEnterDelay } from '../UI/Tooltip';
const styles = {
leftColumn: { flex: 2, minWidth: 0, maxWidth: 150 },
rightColumn: { flex: 3, minWidth: 75 },
rightColumn: { flex: 3, minWidth: 25 },
label: {
overflow: 'hidden',
textOverflow: 'ellipsis',

View File

@@ -69,6 +69,10 @@ const createField = (
getLabel,
getDescription,
hasImpactOnAllOtherFields: property.hasImpactOnOtherProperties(),
canBeUnlimitedUsingMinus1: property
.getExtraInfo()
.toJSArray()
.includes('canBeUnlimitedUsingMinus1'),
getEndAdornment,
};
} else if (valueType === 'string' || valueType === '') {
@@ -132,6 +136,7 @@ const createField = (
property.getExtraInfo().size() > 0 ? property.getExtraInfo().at(0) : '';
return {
name,
isHiddenWhenOnlyOneChoice: true,
valueType: 'string',
getChoices: () => {
return !object || behaviorType === ''
@@ -331,7 +336,7 @@ const propertiesMapToSchema = ({
object,
visibility = 'All',
quickCustomizationVisibilities,
}: {
}: {|
properties: gdMapStringPropertyDescriptor,
getProperties: (instance: Instance) => any,
onUpdateProperty: (
@@ -342,7 +347,7 @@ const propertiesMapToSchema = ({
object?: gdObject,
visibility?: 'All' | 'Basic' | 'Advanced' | 'Deprecated' | 'Basic-Quick',
quickCustomizationVisibilities?: gdQuickCustomizationVisibilitiesContainer,
}): Schema => {
|}): Schema => {
const propertyNames = properties.keys();
// Aggregate field by groups to be able to build field groups with a title.
const fieldsByGroups = new Map<string, Array<Field>>();

View File

@@ -45,6 +45,7 @@ export type ValueFieldCommonProperties = {|
hideLabel?: boolean,
getExtraDescription?: Instance => string,
hasImpactOnAllOtherFields?: boolean,
canBeUnlimitedUsingMinus1?: boolean,
disabled?: (instances: Array<gdInitialInstance>) => boolean,
onEditButtonBuildMenuTemplate?: (i18n: I18nType) => Array<MenuItemTemplate>,
onEditButtonClick?: () => void,
@@ -150,6 +151,7 @@ export type ActionButton = {|
getValue: Instance => string,
nonFieldType: 'button',
getIcon?: ({| fontSize: string |}) => React.Node,
showRightIcon?: boolean,
onClick: (instance: Instance) => void,
|};
@@ -231,6 +233,10 @@ const styles = {
marginTop: marginsSize,
borderTop: '1px solid black', // Border color is changed in the component.
},
level2Separator: {
flex: 1,
borderTop: '1px solid black', // Border color is changed in the component.
},
};
export const Separator = () => {
@@ -245,6 +251,18 @@ export const Separator = () => {
);
};
export const Level2Separator = () => {
const gdevelopTheme = React.useContext(GDevelopThemeContext);
return (
<div
style={{
...styles.level2Separator,
borderColor: gdevelopTheme.listItem.separatorColor,
}}
/>
);
};
const getDisabled = ({
instances,
field,
@@ -398,26 +416,21 @@ const CompactPropertiesEditor = ({
const { setValue } = field;
return (
<CompactPropertiesEditorRowField
<CompactToggleField
key={field.name}
label={getFieldLabel({ instances, field })}
markdownDescription={getFieldDescription(field)}
field={
<CompactToggleField
key={field.name}
id={field.name}
checked={getFieldValue({ instances, field })}
onCheck={newValue => {
instances.forEach(i => setValue(i, newValue));
onFieldChanged({
instances,
hasImpactOnAllOtherFields: field.hasImpactOnAllOtherFields,
});
}}
disabled={getDisabled({ instances, field })}
fullWidth
/>
}
id={field.name}
checked={getFieldValue({ instances, field })}
onCheck={newValue => {
instances.forEach(i => setValue(i, newValue));
onFieldChanged({
instances,
hasImpactOnAllOtherFields: field.hasImpactOnAllOtherFields,
});
}}
disabled={getDisabled({ instances, field })}
fullWidth
/>
);
} else if (field.valueType === 'number') {
@@ -458,6 +471,7 @@ const CompactPropertiesEditor = ({
return (
<CompactSemiControlledNumberField
{...commonProps}
canBeUnlimitedUsingMinus1={field.canBeUnlimitedUsingMinus1}
useLeftIconAsNumberControl
renderLeftIcon={field.renderLeftIcon}
leftIconTooltip={getFieldLabel({ instances, field })}
@@ -470,7 +484,12 @@ const CompactPropertiesEditor = ({
key={key}
label={getFieldLabel({ instances, field })}
markdownDescription={getFieldDescription(field)}
field={<CompactSemiControlledNumberField {...otherCommonProps} />}
field={
<CompactSemiControlledNumberField
canBeUnlimitedUsingMinus1={field.canBeUnlimitedUsingMinus1}
{...otherCommonProps}
/>
}
/>
);
}
@@ -603,24 +622,27 @@ const CompactPropertiesEditor = ({
(field: ValueField) => {
if (!field.getChoices || !field.getValue) return;
const children = field
.getChoices()
.map(({ value, label, labelIsUserDefined }) => (
<SelectOption
key={value}
value={value}
label={label}
shouldNotTranslate={labelIsUserDefined}
/>
));
const choices = field.getChoices();
if (choices.length < 2 && field.isHiddenWhenOnlyOneChoice) {
return;
}
const children = choices.map(({ value, label, labelIsUserDefined }) => (
<SelectOption
key={value}
value={value}
label={label}
shouldNotTranslate={labelIsUserDefined}
/>
));
let compactSelectField;
if (field.valueType === 'number') {
const { setValue } = field;
compactSelectField = (
<CompactSelectField
value={getFieldValue({ instances, field })}
key={field.name}
value={getFieldValue({ instances, field })}
id={field.name}
onChange={(newValue: string) => {
instances.forEach(i => setValue(i, parseFloat(newValue) || 0));
@@ -638,12 +660,12 @@ const CompactPropertiesEditor = ({
const { setValue } = field;
compactSelectField = (
<CompactSelectField
key={field.name}
value={getFieldValue({
instances,
field,
defaultValue: '(Multiple values)',
})}
key={field.name}
id={field.name}
onChange={(newValue: string) => {
instances.forEach(i => setValue(i, newValue || ''));
@@ -689,26 +711,31 @@ const CompactPropertiesEditor = ({
}) === DIFFERENT_VALUES;
}
return (
<React.Fragment key={`button-${field.label}`}>
<FlatButton
fullWidth
primary
leftIcon={
field.getIcon ? (
field.getIcon({ fontSize: 'small' })
) : (
<Edit fontSize="small" />
)
}
disabled={disabled}
label={field.label}
onClick={() => {
if (!instances[0]) return;
field.onClick(instances[0]);
}}
/>
<Spacer />
</React.Fragment>
<FlatButton
key={`button-${field.label}`}
fullWidth
primary
leftIcon={
field.showRightIcon ? null : field.getIcon ? (
field.getIcon({ fontSize: 'small' })
) : (
<Edit fontSize="small" />
)
}
rightIcon={
!field.showRightIcon ? null : field.getIcon ? (
field.getIcon({ fontSize: 'small' })
) : (
<Edit fontSize="small" />
)
}
disabled={disabled}
label={field.label}
onClick={() => {
if (!instances[0]) return;
field.onClick(instances[0]);
}}
/>
);
},
[instances]
@@ -880,16 +907,36 @@ const CompactPropertiesEditor = ({
},
[instances]
);
const renderSectionTitle = React.useCallback((field: SectionTitle) => {
return [
<Separator key={field.name + '-separator'} />,
<Line key={`section-title-${field.name}`} noMargin>
<Text displayInlineAsSpan size="sub-title" noMargin>
{field.title}
</Text>
</Line>,
];
}, []);
const renderSectionTitle = React.useCallback(
(field: { name: string, title: string }) => {
return [
<Separator key={field.name + '-separator'} />,
<Line key={`section-title-${field.name}`} noMargin>
<Text displayInlineAsSpan size="sub-title" noMargin>
{field.title}
</Text>
</Line>,
];
},
[]
);
const renderSectionLevel2Title = React.useCallback(
(field: { name: string, title: string }) => {
return [
<Column expand noMargin key={field.name + '-title'}>
<Spacer />
<LineStackLayout expand noMargin alignItems="center">
<Text size="sub-title" noMargin>
{field.title}
</Text>
<Level2Separator key={field.name + '-separator'} />
</LineStackLayout>
</Column>,
];
},
[]
);
return renderContainer(
schema.map(field => {
@@ -941,10 +988,10 @@ const CompactPropertiesEditor = ({
if (field.title) {
return [
<Separator key={field.name + '-separator'} />,
<Text key={field.name + '-title'} size="sub-title" noMargin>
{field.title}
</Text>,
...renderSectionLevel2Title({
title: field.title,
name: field.name,
}),
contentView,
];
}

View File

@@ -0,0 +1,36 @@
// @flow
import * as React from 'react';
import { type EnumeratedEffectMetadata } from './EnumerateEffects';
import { type ResourceManagementProps } from '../ResourcesList/ResourceSource';
import CompactPropertiesEditor from '../CompactPropertiesEditor';
const noRefreshOfAllFields = () => {
console.warn(
"An effect tried to refresh all fields, but the editor doesn't support it."
);
};
export const CompactEffectPropertiesEditor = ({
project,
effect,
effectMetadata,
resourceManagementProps,
}: {|
project: gdProject,
effect: gdEffect,
effectMetadata: ?EnumeratedEffectMetadata,
resourceManagementProps: ResourceManagementProps,
|}) => {
if (!effectMetadata) return null;
return (
<CompactPropertiesEditor
project={project}
schema={effectMetadata.parametersSchema}
instances={[effect]}
resourceManagementProps={resourceManagementProps}
onRefreshAllFields={noRefreshOfAllFields}
/>
);
};

View File

@@ -1,6 +1,6 @@
// @flow
import { mapFor } from '../Utils/MapFor';
import { type Schema } from '../PropertiesEditor';
import { type Schema } from '../CompactPropertiesEditor';
import { type ResourceKind } from '../ResourcesList/ResourceSource';
import flatten from 'lodash/flatten';

View File

@@ -333,7 +333,7 @@ type Props = {|
layerRenderingType: string,
|};
const getEnumeratedEffectMetadata = (
export const getEnumeratedEffectMetadata = (
allEffectDescriptions: Array<EnumeratedEffectMetadata>,
effectType: string
): ?EnumeratedEffectMetadata => {
@@ -380,50 +380,47 @@ export const getEffects3DCount = (
return effect3DCount;
};
/**
* Display a list of effects and allow to add/remove/edit them.
*
* All available effects are fetched from the project's platform.
*/
export default function EffectsList(props: Props) {
const {
effectsContainer,
onEffectsUpdated,
onEffectsRenamed,
project,
target,
} = props;
const scrollView = React.useRef<?ScrollViewInterface>(null);
type UseManageEffectsState = {|
allEffectMetadata: Array<EnumeratedEffectMetadata>,
all2DEffectMetadata: Array<EnumeratedEffectMetadata>,
all3DEffectMetadata: Array<EnumeratedEffectMetadata>,
draggedEffect: {| current: ?gdEffect |},
addEffect: boolean => void,
chooseEffectType: (effect: gdEffect, newEffectType: string) => void,
copyAllEffects: () => void,
copyEffect: (effect: gdEffect) => void,
duplicatedUniqueEffectMetadata: ?EnumeratedEffectMetadata,
isClipboardContainingEffects: boolean,
moveEffect: (targetEffect: gdEffect) => void,
pasteEffectsAtTheEnd: () => Promise<void>,
pasteEffectsBefore: (effect: gdEffect) => Promise<void>,
removeEffect: (effect: gdEffect) => void,
resetJustAddedEffectName: () => void,
justAddedEffectName: ?string,
|};
export const useManageEffects = ({
effectsContainer,
project,
onEffectsUpdated,
onUpdate,
target,
}: {|
effectsContainer: gdEffectsContainer,
project: gdProject,
onEffectsUpdated: () => void,
onUpdate: () => void,
target: 'object' | 'layer',
|}): UseManageEffectsState => {
const authenticatedUser = React.useContext(AuthenticatedUserContext);
const [justAddedEffectName, setJustAddedEffectName] = React.useState<?string>(
null
);
const justAddedEffectElement = React.useRef<?any>(null);
React.useEffect(
() => {
if (
scrollView.current &&
justAddedEffectElement.current &&
justAddedEffectName
) {
scrollView.current.scrollTo(justAddedEffectElement.current);
setJustAddedEffectName(null);
justAddedEffectElement.current = null;
}
},
[justAddedEffectName]
);
const draggedEffect = React.useRef<?gdEffect>(null);
const authenticatedUser = React.useContext(AuthenticatedUserContext);
const [nameErrors, setNameErrors] = React.useState<{ [number]: React.Node }>(
{}
);
const allEffectMetadata = React.useMemo(
() => enumerateEffectsMetadata(props.project),
[props.project]
() => enumerateEffectsMetadata(project),
[project]
);
const all3DEffectMetadata = React.useMemo(
@@ -452,15 +449,13 @@ export default function EffectsList(props: Props) {
[allEffectMetadata]
);
const all2DEffectMetadata = React.useMemo(
const all2DEffectMetadata: Array<EnumeratedEffectMetadata> = React.useMemo(
() => allEffectMetadata.filter(effect => effect.isMarkedAsOnlyWorkingFor2D),
[allEffectMetadata]
);
const showEffectOverridingConfirmation = useEffectOverridingAlertDialog();
const forceUpdate = useForceUpdate();
const chooseEffectType = React.useCallback(
(effect: gdEffect, newEffectType: string) => {
effect.setEffectType(newEffectType);
@@ -473,10 +468,10 @@ export default function EffectsList(props: Props) {
setEffectDefaultParameters(effect, effectMetadata.effectMetadata);
}
forceUpdate();
onUpdate();
onEffectsUpdated();
},
[allEffectMetadata, forceUpdate, onEffectsUpdated]
[allEffectMetadata, onUpdate, onEffectsUpdated]
);
const _addEffect = React.useCallback(
@@ -495,11 +490,11 @@ export default function EffectsList(props: Props) {
chooseEffectType(effect, 'Outline');
}
forceUpdate();
onUpdate();
onEffectsUpdated();
setJustAddedEffectName(newName);
},
[chooseEffectType, effectsContainer, forceUpdate, onEffectsUpdated]
[chooseEffectType, effectsContainer, onUpdate, onEffectsUpdated]
);
const addEffect = addCreateBadgePreHookIfNotClaimed(
@@ -511,10 +506,10 @@ export default function EffectsList(props: Props) {
const removeEffect = React.useCallback(
(effect: gdEffect) => {
effectsContainer.removeEffect(effect.getName());
forceUpdate();
onUpdate();
onEffectsUpdated();
},
[effectsContainer, forceUpdate, onEffectsUpdated]
[effectsContainer, onUpdate, onEffectsUpdated]
);
const copyEffect = React.useCallback(
@@ -526,9 +521,9 @@ export default function EffectsList(props: Props) {
serializedEffect: serializeToJSObject(effect),
},
]);
forceUpdate();
onUpdate();
},
[forceUpdate]
[onUpdate]
);
const copyAllEffects = React.useCallback(
@@ -544,9 +539,9 @@ export default function EffectsList(props: Props) {
};
})
);
forceUpdate();
onUpdate();
},
[forceUpdate, effectsContainer]
[onUpdate, effectsContainer]
);
const pasteEffects = React.useCallback(
@@ -624,7 +619,7 @@ export default function EffectsList(props: Props) {
}
}
forceUpdate();
onUpdate();
if (firstAddedEffectName) {
setJustAddedEffectName(firstAddedEffectName);
} else if (existingNamedEffects.length === 1) {
@@ -635,7 +630,7 @@ export default function EffectsList(props: Props) {
}
},
[
forceUpdate,
onUpdate,
project,
target,
effectsContainer,
@@ -674,10 +669,10 @@ export default function EffectsList(props: Props) {
draggedIndex,
targetIndex > draggedIndex ? targetIndex - 1 : targetIndex
);
forceUpdate();
onUpdate();
onEffectsUpdated();
},
[effectsContainer, forceUpdate, onEffectsUpdated]
[effectsContainer, onUpdate, onEffectsUpdated]
);
const isClipboardContainingEffects = Clipboard.has(EFFECTS_CLIPBOARD_KIND);
@@ -716,6 +711,91 @@ export default function EffectsList(props: Props) {
const duplicatedUniqueEffectMetadata = getDuplicatedUniqueEffectMetadata();
const resetJustAddedEffectName = React.useCallback(() => {
setJustAddedEffectName(null);
}, []);
return {
allEffectMetadata,
all2DEffectMetadata,
all3DEffectMetadata,
draggedEffect,
addEffect,
chooseEffectType,
copyAllEffects,
copyEffect,
duplicatedUniqueEffectMetadata,
isClipboardContainingEffects,
moveEffect,
pasteEffectsAtTheEnd,
pasteEffectsBefore,
removeEffect,
resetJustAddedEffectName,
justAddedEffectName,
};
};
/**
* Display a list of effects and allow to add/remove/edit them.
*
* All available effects are fetched from the project's platform.
*/
export default function EffectsList(props: Props) {
const {
effectsContainer,
onEffectsUpdated,
onEffectsRenamed,
project,
target,
} = props;
const scrollView = React.useRef<?ScrollViewInterface>(null);
const justAddedEffectElement = React.useRef<?any>(null);
const forceUpdate = useForceUpdate();
const {
allEffectMetadata,
all2DEffectMetadata,
all3DEffectMetadata,
addEffect,
chooseEffectType,
copyAllEffects,
copyEffect,
duplicatedUniqueEffectMetadata,
isClipboardContainingEffects,
moveEffect,
pasteEffectsAtTheEnd,
pasteEffectsBefore,
removeEffect,
resetJustAddedEffectName,
justAddedEffectName,
draggedEffect,
} = useManageEffects({
effectsContainer,
project,
onEffectsUpdated,
onUpdate: forceUpdate,
target,
});
React.useEffect(
() => {
if (
scrollView.current &&
justAddedEffectElement.current &&
justAddedEffectName
) {
scrollView.current.scrollTo(justAddedEffectElement.current);
resetJustAddedEffectName();
justAddedEffectElement.current = null;
}
},
[justAddedEffectName, resetJustAddedEffectName]
);
const [nameErrors, setNameErrors] = React.useState<{ [number]: React.Node }>(
{}
);
// Count the number of effects to hide titles of empty sections.
const platform = project.getCurrentPlatform();
const effects2DCount = getEffects2DCount(platform, effectsContainer);

View File

@@ -49,11 +49,11 @@ const applyRatio = ({
const getEditObjectButton = ({
i18n,
onEditObjectByName,
onEditObject,
is3DInstance,
}: {|
i18n: I18nType,
onEditObjectByName: (name: string) => void,
onEditObject: (name: string) => void,
is3DInstance: boolean,
|}) => ({
label: i18n._(t`Edit object`),
@@ -64,7 +64,7 @@ const getEditObjectButton = ({
: props => <Object2d {...props} />,
getValue: (instance: gdInitialInstance) => instance.getObjectName(),
onClick: (instance: gdInitialInstance) =>
onEditObjectByName(instance.getObjectName()),
onEditObject(instance.getObjectName()),
});
const getRotationXAndRotationYFields = ({ i18n }: {| i18n: I18nType |}) => [
@@ -487,7 +487,7 @@ export const makeSchema = ({
canBeFlippedZ,
i18n,
forceUpdate,
onEditObjectByName,
onEditObject,
onGetInstanceSize,
layersContainer,
}: {|
@@ -497,7 +497,7 @@ export const makeSchema = ({
canBeFlippedZ: boolean,
i18n: I18nType,
forceUpdate: () => void,
onEditObjectByName: (name: string) => void,
onEditObject: (name: string) => void,
onGetInstanceSize: gdInitialInstance => [number, number, number],
layersContainer: gdLayersContainer,
|}): Schema => {
@@ -519,7 +519,7 @@ export const makeSchema = ({
if (is3DInstance) {
return [
getTitleRow({ i18n }),
getEditObjectButton({ i18n, onEditObjectByName, is3DInstance }),
getEditObjectButton({ i18n, onEditObject, is3DInstance }),
{
name: 'Position',
type: 'row',
@@ -607,7 +607,7 @@ export const makeSchema = ({
return [
getTitleRow({ i18n }),
getEditObjectButton({ i18n, onEditObjectByName, is3DInstance }),
getEditObjectButton({ i18n, onEditObject, is3DInstance }),
{
name: 'Position',
type: 'row',

View File

@@ -3,8 +3,6 @@ import * as React from 'react';
import { Trans } from '@lingui/macro';
import { type I18n as I18nType } from '@lingui/core';
import Paper from '../../UI/Paper';
import EmptyMessage from '../../UI/EmptyMessage';
import CompactPropertiesEditor, {
Separator,
} from '../../CompactPropertiesEditor';
@@ -19,6 +17,7 @@ import ScrollView, { type ScrollViewInterface } from '../../UI/ScrollView';
import EventsRootVariablesFinder from '../../Utils/EventsRootVariablesFinder';
import VariablesList, {
type HistoryHandler,
type VariablesListInterface,
} from '../../VariablesList/VariablesList';
import ShareExternal from '../../UI/CustomSvgIcons/ShareExternal';
import useForceUpdate from '../../Utils/UseForceUpdate';
@@ -26,19 +25,16 @@ import ErrorBoundary from '../../UI/ErrorBoundary';
import {
makeSchema,
reorderInstanceSchemaForCustomProperties,
} from './CompactPropertiesSchema';
} from './CompactInstancePropertiesSchema';
import { ProjectScopedContainersAccessor } from '../../InstructionOrExpression/EventsScope';
import TileSetVisualizer, {
type TileMapTileSelection,
} from '../TileSetVisualizer';
import Add from '../../UI/CustomSvgIcons/Add';
const gd: libGDevelop = global.gd;
export const styles = {
paper: {
display: 'flex',
flex: 1,
minWidth: 0,
flexDirection: 'column',
},
icon: {
fontSize: 18,
},
@@ -51,8 +47,6 @@ const noRefreshOfAllFields = () => {
);
};
const gd: libGDevelop = global.gd;
type Props = {|
project: gdProject,
layout?: ?gdLayout,
@@ -61,7 +55,7 @@ type Props = {|
layersContainer: gdLayersContainer,
projectScopedContainersAccessor: ProjectScopedContainersAccessor,
instances: Array<gdInitialInstance>,
onEditObjectByName: string => void,
editObjectInPropertiesPanel: string => void,
onInstancesModified?: (Array<gdInitialInstance>) => void,
onGetInstanceSize: gdInitialInstance => [number, number, number],
editInstanceVariables: gdInitialInstance => void,
@@ -72,11 +66,7 @@ type Props = {|
onSelectTileMapTile: (?TileMapTileSelection) => void,
|};
export type CompactInstancePropertiesEditorInterface = {|
forceUpdate: () => void,
|};
const CompactInstancePropertiesEditor = ({
export const CompactInstancePropertiesEditor = ({
instances,
i18n,
project,
@@ -86,7 +76,7 @@ const CompactInstancePropertiesEditor = ({
layersContainer,
unsavedChanges,
historyHandler,
onEditObjectByName,
editObjectInPropertiesPanel,
onGetInstanceSize,
editInstanceVariables,
onInstancesModified,
@@ -95,6 +85,8 @@ const CompactInstancePropertiesEditor = ({
onSelectTileMapTile,
}: Props) => {
const forceUpdate = useForceUpdate();
const variablesListRef = React.useRef<?VariablesListInterface>(null);
const scrollViewRef = React.useRef<?ScrollViewInterface>(null);
const instance = instances[0];
/**
@@ -171,7 +163,7 @@ const CompactInstancePropertiesEditor = ({
canBeFlippedXY,
canBeFlippedZ,
onGetInstanceSize,
onEditObjectByName,
onEditObject: editObjectInPropertiesPanel,
layersContainer,
forceUpdate,
}).concat(reorderedInstanceSchemaForCustomProperties);
@@ -189,7 +181,7 @@ const CompactInstancePropertiesEditor = ({
forceUpdate,
layersContainer,
onGetInstanceSize,
onEditObjectByName,
editObjectInPropertiesPanel,
]
);
@@ -246,8 +238,8 @@ const CompactInstancePropertiesEditor = ({
</Column>
{shouldDisplayTileSetVisualizer && (
<>
<Separator />
<Column>
<Separator />
<Line alignItems="center" justifyContent="space-between">
<Text size="sub-title" noMargin>
<Trans>Tilemap painter</Trans>
@@ -269,23 +261,36 @@ const CompactInstancePropertiesEditor = ({
)}
{object && shouldDisplayVariablesList ? (
<>
<Separator />
<Column>
<Separator />
<Line alignItems="center" justifyContent="space-between">
<Text size="sub-title" noMargin>
<Trans>Instance Variables</Trans>
</Text>
<IconButton
size="small"
onClick={() => {
editInstanceVariables(instance);
}}
>
<ShareExternal style={styles.icon} />
</IconButton>
<Line alignItems="center" noMargin>
<IconButton
size="small"
onClick={() => {
editInstanceVariables(instance);
}}
>
<ShareExternal style={styles.icon} />
</IconButton>
<IconButton
size="small"
onClick={
variablesListRef.current
? variablesListRef.current.addVariable
: undefined
}
>
<Add style={styles.icon} />
</IconButton>
</Line>
</Line>
</Column>
<VariablesList
ref={variablesListRef}
projectScopedContainersAccessor={
projectScopedContainersAccessor
}
@@ -293,7 +298,7 @@ const CompactInstancePropertiesEditor = ({
inheritedVariablesContainer={object.getVariables()}
variablesContainer={instance.getVariables()}
areObjectVariables
size="small"
size="compact"
onComputeAllVariableNames={() =>
object && layout
? EventsRootVariablesFinder.findAllObjectVariables(
@@ -306,6 +311,9 @@ const CompactInstancePropertiesEditor = ({
}
historyHandler={historyHandler}
toolbarIconStyle={styles.icon}
compactEmptyPlaceholderText={
<Trans>There are no variables on this instance.</Trans>
}
/>
</>
) : null}
@@ -314,29 +322,3 @@ const CompactInstancePropertiesEditor = ({
</ErrorBoundary>
);
};
const CompactInstancePropertiesEditorContainer = React.forwardRef<
Props,
CompactInstancePropertiesEditorInterface
>((props, ref) => {
const forceUpdate = useForceUpdate();
React.useImperativeHandle(ref, () => ({
forceUpdate,
}));
return (
<Paper background="dark" square style={styles.paper}>
{!props.instances || !props.instances.length ? (
<EmptyMessage>
<Trans>
Click on an instance in the scene to display its properties
</Trans>
</EmptyMessage>
) : (
<CompactInstancePropertiesEditor {...props} />
)}
</Paper>
);
});
export default CompactInstancePropertiesEditorContainer;

View File

@@ -433,6 +433,7 @@ const FullSizeInstancesEditorWithScrollbars = (props: Props) => {
}
}}
showObjectInstancesIn3D={values.use3DEditor}
showBasicProfilingCounters={values.showBasicProfilingCounters}
{...otherProps}
/>
</ErrorBoundary>

View File

@@ -1,456 +0,0 @@
// @flow
import { Trans } from '@lingui/macro';
import { type I18n as I18nType } from '@lingui/core';
import { t } from '@lingui/macro';
import * as React from 'react';
import Background from '../../UI/Background';
import enumerateLayers from '../../LayersList/EnumerateLayers';
import EmptyMessage from '../../UI/EmptyMessage';
import PropertiesEditor from '../../PropertiesEditor';
import propertiesMapToSchema from '../../PropertiesEditor/PropertiesMapToSchema';
import { type Schema } from '../../PropertiesEditor';
import getObjectByName from '../../Utils/GetObjectByName';
import IconButton from '../../UI/IconButton';
import { Line, Column } from '../../UI/Grid';
import Text from '../../UI/Text';
import { type UnsavedChanges } from '../../MainFrame/UnsavedChangesContext';
import ScrollView from '../../UI/ScrollView';
import EventsRootVariablesFinder from '../../Utils/EventsRootVariablesFinder';
import VariablesList, {
type HistoryHandler,
} from '../../VariablesList/VariablesList';
import ShareExternal from '../../UI/CustomSvgIcons/ShareExternal';
import useForceUpdate from '../../Utils/UseForceUpdate';
import ErrorBoundary from '../../UI/ErrorBoundary';
import { ProjectScopedContainersAccessor } from '../../InstructionOrExpression/EventsScope';
const gd: libGDevelop = global.gd;
type Props = {|
project: gdProject,
layout: gdLayout,
projectScopedContainersAccessor: ProjectScopedContainersAccessor,
instances: Array<gdInitialInstance>,
onEditObjectByName: string => void,
onInstancesModified?: (Array<gdInitialInstance>) => void,
onGetInstanceSize: gdInitialInstance => [number, number, number],
editInstanceVariables: gdInitialInstance => void,
unsavedChanges?: ?UnsavedChanges,
i18n: I18nType,
historyHandler?: HistoryHandler,
|};
export type InstancePropertiesEditorInterface = {| forceUpdate: () => void |};
const makeSchema = ({
is3DInstance,
i18n,
forceUpdate,
onEditObjectByName,
onGetInstanceSize,
layout,
}) => {
const getInstanceWidth = (instance: gdInitialInstance) =>
instance.hasCustomSize()
? instance.getCustomWidth()
: onGetInstanceSize(instance)[0];
const getInstanceHeight = (instance: gdInitialInstance) =>
instance.hasCustomSize()
? instance.getCustomHeight()
: onGetInstanceSize(instance)[1];
const getInstanceDepth = (instance: gdInitialInstance) =>
instance.hasCustomDepth()
? instance.getCustomDepth()
: onGetInstanceSize(instance)[2];
return [
{
name: i18n._(t`Object`),
getValue: (instance: gdInitialInstance) => instance.getObjectName(),
nonFieldType: 'sectionTitle',
defaultValue: i18n._(t`Different objects`),
},
{
label: i18n._(t`Edit object`),
disabled: 'onValuesDifferent',
nonFieldType: 'button',
getValue: (instance: gdInitialInstance) => instance.getObjectName(),
onClick: (instance: gdInitialInstance) =>
onEditObjectByName(instance.getObjectName()),
},
{
name: i18n._(t`Instance`),
nonFieldType: 'sectionTitle',
},
{
name: 'Position',
type: 'row',
children: [
{
name: 'X',
getLabel: () => i18n._(t`X`),
valueType: 'number',
getValue: (instance: gdInitialInstance) => instance.getX(),
setValue: (instance: gdInitialInstance, newValue: number) =>
instance.setX(newValue),
},
{
name: 'Y',
getLabel: () => i18n._(t`Y`),
valueType: 'number',
getValue: (instance: gdInitialInstance) => instance.getY(),
setValue: (instance: gdInitialInstance, newValue: number) =>
instance.setY(newValue),
},
is3DInstance
? {
name: 'Z',
getLabel: () => i18n._(t`Z`),
valueType: 'number',
getValue: (instance: gdInitialInstance) => instance.getZ(),
setValue: (instance: gdInitialInstance, newValue: number) =>
instance.setZ(newValue),
}
: null,
].filter(Boolean),
},
{
name: 'Angles',
type: 'row',
children: [
is3DInstance
? {
name: 'RotationX',
getLabel: () => i18n._(t`Rotation (X)`),
valueType: 'number',
getValue: (instance: gdInitialInstance) =>
instance.getRotationX(),
setValue: (instance: gdInitialInstance, newValue: number) =>
instance.setRotationX(newValue),
}
: null,
is3DInstance
? {
name: 'RotationY',
getLabel: () => i18n._(t`Rotation (Y)`),
valueType: 'number',
getValue: (instance: gdInitialInstance) =>
instance.getRotationY(),
setValue: (instance: gdInitialInstance, newValue: number) =>
instance.setRotationY(newValue),
}
: null,
{
name: 'Angle',
getLabel: () =>
is3DInstance ? i18n._(t`Rotation (Z)`) : i18n._(t`Angle`),
valueType: 'number',
getValue: (instance: gdInitialInstance) => instance.getAngle(),
setValue: (instance: gdInitialInstance, newValue: number) =>
instance.setAngle(newValue),
},
].filter(Boolean),
},
{
name: 'Lock instance position angle',
getLabel: () => i18n._(t`Lock position/angle in the editor`),
valueType: 'boolean',
getValue: (instance: gdInitialInstance) => instance.isLocked(),
setValue: (instance: gdInitialInstance, newValue: boolean) => {
instance.setLocked(newValue);
if (!newValue) {
instance.setSealed(newValue);
}
},
},
{
name: 'Prevent instance selection',
getLabel: () => i18n._(t`Prevent selection in the editor`),
valueType: 'boolean',
disabled: (instances: gdInitialInstance[]) => {
return instances.some(instance => !instance.isLocked());
},
getValue: (instance: gdInitialInstance) => instance.isSealed(),
setValue: (instance: gdInitialInstance, newValue: boolean) =>
instance.setSealed(newValue),
},
!is3DInstance
? {
name: 'Z Order',
getLabel: () => i18n._(t`Z Order`),
valueType: 'number',
getValue: (instance: gdInitialInstance) => instance.getZOrder(),
setValue: (instance: gdInitialInstance, newValue: number) =>
instance.setZOrder(newValue),
}
: null,
{
name: 'Layer',
getLabel: () => i18n._(t`Layer`),
valueType: 'string',
getChoices: () => enumerateLayers(layout),
getValue: (instance: gdInitialInstance) => instance.getLayer(),
setValue: (instance: gdInitialInstance, newValue: string) =>
instance.setLayer(newValue),
},
{
name: 'Custom size',
getLabel: () => i18n._(t`Custom size`),
valueType: 'boolean',
getValue: (instance: gdInitialInstance) => instance.hasCustomSize(),
setValue: (instance: gdInitialInstance, newValue: boolean) => {
if (
instance.getCustomHeight() === 0 &&
instance.getCustomWidth() === 0 &&
instance.getCustomDepth() === 0
) {
// The instance custom dimensions have never been set before.
// To avoid setting setting all the dimensions to 0 when enabling
// the instance custom size flag, the current instance dimensions are used.
instance.setCustomWidth(getInstanceWidth(instance));
instance.setCustomHeight(getInstanceHeight(instance));
instance.setCustomDepth(getInstanceDepth(instance));
}
instance.setHasCustomSize(newValue);
instance.setHasCustomDepth(newValue);
forceUpdate();
},
},
{
name: 'custom-size-row',
type: 'row',
children: [
{
name: 'Width',
getLabel: () => i18n._(t`Width`),
valueType: 'number',
getValue: getInstanceWidth,
setValue: (instance: gdInitialInstance, newValue: number) => {
instance.setCustomWidth(Math.max(newValue, 0));
instance.setCustomHeight(getInstanceHeight(instance));
instance.setCustomDepth(getInstanceDepth(instance));
// This must be done after reading the size.
instance.setHasCustomSize(true);
instance.setHasCustomDepth(true);
forceUpdate();
},
},
{
name: 'Height',
getLabel: () => i18n._(t`Height`),
valueType: 'number',
getValue: getInstanceHeight,
setValue: (instance: gdInitialInstance, newValue: number) => {
instance.setCustomWidth(getInstanceWidth(instance));
instance.setCustomHeight(Math.max(newValue, 0));
instance.setCustomDepth(getInstanceDepth(instance));
// This must be done after reading the size.
instance.setHasCustomSize(true);
instance.setHasCustomDepth(true);
forceUpdate();
},
},
is3DInstance
? {
name: 'Depth',
getLabel: () => i18n._(t`Depth`),
valueType: 'number',
getValue: getInstanceDepth,
setValue: (instance: gdInitialInstance, newValue: number) => {
instance.setCustomWidth(getInstanceWidth(instance));
instance.setCustomHeight(getInstanceHeight(instance));
instance.setCustomDepth(Math.max(newValue, 0));
// This must be done after reading the size.
instance.setHasCustomSize(true);
instance.setHasCustomDepth(true);
forceUpdate();
},
}
: null,
].filter(Boolean),
},
].filter(Boolean);
};
const InstancePropertiesEditor = ({
instances,
i18n,
project,
layout,
projectScopedContainersAccessor,
unsavedChanges,
historyHandler,
onEditObjectByName,
onGetInstanceSize,
editInstanceVariables,
onInstancesModified,
}: Props) => {
const forceUpdate = useForceUpdate();
const schemaFor2D: Schema = React.useMemo(
() =>
makeSchema({
i18n,
is3DInstance: false,
onGetInstanceSize,
onEditObjectByName,
layout,
forceUpdate,
}),
[i18n, onGetInstanceSize, onEditObjectByName, layout, forceUpdate]
);
const schemaFor3D: Schema = React.useMemo(
() =>
makeSchema({
i18n,
is3DInstance: true,
onGetInstanceSize,
onEditObjectByName,
layout,
forceUpdate,
}),
[i18n, onGetInstanceSize, onEditObjectByName, layout, forceUpdate]
);
// TODO: multiple instances support.
const instance = instances[0];
const { object, instanceSchema } = React.useMemo(
() => {
if (!instance) return {};
const associatedObjectName = instance.getObjectName();
const object = getObjectByName(
project.getObjects(),
layout.getObjects(),
associatedObjectName
);
const properties = instance.getCustomProperties(
project.getObjects(),
layout.getObjects()
);
if (!object) return {};
const is3DInstance = gd.MetadataProvider.getObjectMetadata(
project.getCurrentPlatform(),
object.getType()
).isRenderedIn3D();
const instanceSchemaForCustomProperties = propertiesMapToSchema(
properties,
(instance: gdInitialInstance) =>
instance.getCustomProperties(
project.getObjects(),
layout.getObjects()
),
(instance: gdInitialInstance, name, value) =>
instance.updateCustomProperty(
name,
value,
project.getObjects(),
layout.getObjects()
)
);
return {
object,
instanceSchema: is3DInstance
? schemaFor3D.concat(instanceSchemaForCustomProperties)
: schemaFor2D.concat(instanceSchemaForCustomProperties),
};
},
[project, layout, instance, schemaFor2D, schemaFor3D]
);
if (!object || !instance || !instanceSchema) return null;
return (
<ErrorBoundary
componentTitle={<Trans>Instance properties</Trans>}
scope="scene-editor-instance-properties"
>
<ScrollView
autoHideScrollbar
key={instances
.map((instance: gdInitialInstance) => '' + instance.ptr)
.join(';')}
>
<Column expand noMargin id="instance-properties-editor">
<Column>
<PropertiesEditor
unsavedChanges={unsavedChanges}
schema={instanceSchema}
instances={instances}
onInstancesModified={onInstancesModified}
/>
<Line alignItems="center" justifyContent="space-between">
<Text>
<Trans>Instance Variables</Trans>
</Text>
<IconButton
size="small"
onClick={() => {
editInstanceVariables(instance);
}}
>
<ShareExternal />
</IconButton>
</Line>
</Column>
{object ? (
<VariablesList
projectScopedContainersAccessor={projectScopedContainersAccessor}
directlyStoreValueChangesWhileEditing
inheritedVariablesContainer={object.getVariables()}
variablesContainer={instance.getVariables()}
areObjectVariables
size="small"
onComputeAllVariableNames={() =>
object
? EventsRootVariablesFinder.findAllObjectVariables(
project.getCurrentPlatform(),
project,
layout,
object.getName()
)
: []
}
historyHandler={historyHandler}
/>
) : null}
</Column>
</ScrollView>
</ErrorBoundary>
);
};
const InstancePropertiesEditorContainer = React.forwardRef<
Props,
InstancePropertiesEditorInterface
>((props, ref) => {
const forceUpdate = useForceUpdate();
React.useImperativeHandle(ref, () => ({
forceUpdate,
}));
return (
<Background>
{!props.instances || !props.instances.length ? (
<EmptyMessage>
<Trans>
Click on an instance in the scene to display its properties
</Trans>
</EmptyMessage>
) : (
<InstancePropertiesEditor {...props} />
)}
</Background>
);
});
export default InstancePropertiesEditorContainer;

View File

@@ -0,0 +1,150 @@
// @flow
type InstanceCounter = {
updateCount: number,
totalUpdateTime: number,
};
export type BasicProfilingCounters = {
instanceCounters: { [string]: InstanceCounter },
totalInstancesUpdateCount: number,
totalInstancesUpdateTime: number,
totalPixiRenderingTime: number,
totalPixiUiRenderingTime: number,
totalThreeRenderingTime: number,
};
export const makeBasicProfilingCounters = (): BasicProfilingCounters => {
return {
instanceCounters: {},
totalInstancesUpdateCount: 0,
totalInstancesUpdateTime: 0,
totalPixiRenderingTime: 0,
totalPixiUiRenderingTime: 0,
totalThreeRenderingTime: 0,
};
};
export const resetBasicProfilingCounters = (
basicProfilingCounters: BasicProfilingCounters
): BasicProfilingCounters => {
basicProfilingCounters.instanceCounters = {};
basicProfilingCounters.totalInstancesUpdateCount = 0;
basicProfilingCounters.totalInstancesUpdateTime = 0;
basicProfilingCounters.totalPixiRenderingTime = 0;
basicProfilingCounters.totalPixiUiRenderingTime = 0;
basicProfilingCounters.totalThreeRenderingTime = 0;
return basicProfilingCounters;
};
export const increaseInstanceUpdate = (
basicProfilingCounters: BasicProfilingCounters,
objectName: string,
updateDuration: number
) => {
let instanceCounter = basicProfilingCounters.instanceCounters[objectName];
if (!instanceCounter) {
basicProfilingCounters.instanceCounters[objectName] = {
updateCount: 1,
totalUpdateTime: updateDuration,
};
} else {
instanceCounter.updateCount++;
instanceCounter.totalUpdateTime += updateDuration;
}
basicProfilingCounters.totalInstancesUpdateCount++;
basicProfilingCounters.totalInstancesUpdateTime += updateDuration;
};
export const increasePixiRenderingTime = (
basicProfilingCounters: BasicProfilingCounters,
pixiRenderingTime: number
) => {
basicProfilingCounters.totalPixiRenderingTime += pixiRenderingTime;
};
export const increasePixiUiRenderingTime = (
basicProfilingCounters: BasicProfilingCounters,
pixiUiRenderingTime: number
) => {
basicProfilingCounters.totalPixiUiRenderingTime += pixiUiRenderingTime;
};
export const increaseThreeRenderingTime = (
basicProfilingCounters: BasicProfilingCounters,
threeRenderingTime: number
) => {
basicProfilingCounters.totalThreeRenderingTime += threeRenderingTime;
};
export const mergeBasicProfilingCounters = (
destination: BasicProfilingCounters,
source: BasicProfilingCounters
): BasicProfilingCounters => {
for (const objectName in source.instanceCounters) {
if (source.instanceCounters.hasOwnProperty(objectName)) {
const instanceCounter = source.instanceCounters[objectName];
let destinationInstanceCounter = destination.instanceCounters[objectName];
if (!destinationInstanceCounter) {
destinationInstanceCounter = destination.instanceCounters[
objectName
] = {
updateCount: 0,
totalUpdateTime: 0,
};
}
destinationInstanceCounter.updateCount += instanceCounter.updateCount;
destinationInstanceCounter.totalUpdateTime +=
instanceCounter.totalUpdateTime;
}
}
destination.totalInstancesUpdateCount += source.totalInstancesUpdateCount;
destination.totalInstancesUpdateTime += source.totalInstancesUpdateTime;
destination.totalPixiRenderingTime += source.totalPixiRenderingTime;
destination.totalPixiUiRenderingTime += source.totalPixiUiRenderingTime;
destination.totalThreeRenderingTime += source.totalThreeRenderingTime;
return destination;
};
export const getBasicProfilingCountersText = (
basicProfilingCounters: BasicProfilingCounters
): string => {
const texts = [];
texts.push(
`Instances update count: ${
basicProfilingCounters.totalInstancesUpdateCount
}`
);
texts.push(
`Instances update time: ${basicProfilingCounters.totalInstancesUpdateTime.toFixed(
2
)}ms`
);
texts.push(
`Pixi rendering time: ${basicProfilingCounters.totalPixiRenderingTime.toFixed(
2
)}ms`
);
texts.push(
`Three rendering time: ${basicProfilingCounters.totalThreeRenderingTime.toFixed(
2
)}ms`
);
texts.push(
`Pixi UI rendering time: ${basicProfilingCounters.totalPixiUiRenderingTime.toFixed(
2
)}ms`
);
texts.push(' ');
for (const objectName in basicProfilingCounters.instanceCounters) {
const instanceCounters =
basicProfilingCounters.instanceCounters[objectName];
texts.push(
`${objectName}: ${
instanceCounters.updateCount
} updates, ${instanceCounters.totalUpdateTime.toFixed(2)}ms`
);
}
return texts.join('\n');
};

View File

@@ -16,6 +16,12 @@ import {
type Polygon,
} from '../../Utils/PolygonHelper';
import Rendered3DInstance from '../../ObjectsRendering/Renderers/Rendered3DInstance';
import {
type BasicProfilingCounters,
increaseInstanceUpdate,
makeBasicProfilingCounters,
resetBasicProfilingCounters,
} from './BasicProfilingCounters';
const gd: libGDevelop = global.gd;
export default class LayerRenderer {
@@ -83,6 +89,8 @@ export default class LayerRenderer {
_showObjectInstancesIn3D: boolean;
_basicProfilingCounters = makeBasicProfilingCounters();
constructor({
project,
globalObjectsContainer,
@@ -186,7 +194,18 @@ export default class LayerRenderer {
? 'auto'
: 'static';
}
if (isVisible) renderedInstance.update();
if (isVisible) {
const objectName = instance.getObjectName();
const time = performance.now();
renderedInstance.update();
const duration = performance.now() - time;
increaseInstanceUpdate(
this._basicProfilingCounters,
objectName,
duration
);
}
if (renderedInstance instanceof Rendered3DInstance) {
const threeObject = renderedInstance.getThreeObject();
@@ -555,6 +574,8 @@ export default class LayerRenderer {
}
render() {
resetBasicProfilingCounters(this._basicProfilingCounters);
this._computeViewBounds();
this.instances.iterateOverInstancesWithZOrdering(
// $FlowFixMe - gd.castObject is not supporting typings.
@@ -566,6 +587,10 @@ export default class LayerRenderer {
this._destroyUnusedInstanceRenderers();
}
getBasicProfilingCounters(): BasicProfilingCounters {
return this._basicProfilingCounters;
}
/**
* Create Three.js objects for 3D rendering of this layer.
*/

View File

@@ -5,6 +5,15 @@ import * as PIXI from 'pixi.js-legacy';
import * as THREE from 'three';
import { rgbToHexNumber } from '../../Utils/ColorTransformer';
import Rectangle from '../../Utils/Rectangle';
import {
type BasicProfilingCounters,
makeBasicProfilingCounters,
mergeBasicProfilingCounters,
resetBasicProfilingCounters,
increasePixiRenderingTime,
increaseThreeRenderingTime,
increasePixiUiRenderingTime,
} from './BasicProfilingCounters';
export type InstanceMeasurer = {|
getInstanceAABB: (gdInitialInstance, Rectangle) => Rectangle,
@@ -49,6 +58,8 @@ export default class InstancesRenderer {
temporaryRectangle: Rectangle;
instanceMeasurer: InstanceMeasurer;
_basicProfilingCounters = makeBasicProfilingCounters();
constructor({
project,
layersContainer,
@@ -176,6 +187,10 @@ export default class InstancesRenderer {
return this.instanceMeasurer;
}
getBasicProfilingCounters(): BasicProfilingCounters {
return this._basicProfilingCounters;
}
render(
pixiRenderer: PIXI.Renderer,
threeRenderer: THREE.WebGLRenderer | null,
@@ -183,6 +198,8 @@ export default class InstancesRenderer {
uiPixiContainer: PIXI.Container,
backgroundPixiContainer: PIXI.Container
) {
resetBasicProfilingCounters(this._basicProfilingCounters);
// Even if no rendering at all has been made already, setting up the Three.js/PixiJS renderers
// might have changed some WebGL states already. Reset the state for the very first frame.
// And, out of caution, keep doing it for every frame.
@@ -244,6 +261,11 @@ export default class InstancesRenderer {
layerRenderer.wasUsed = true;
layerRenderer.getPixiContainer().zOrder = i;
layerRenderer.render();
mergeBasicProfilingCounters(
this._basicProfilingCounters,
layerRenderer.getBasicProfilingCounters()
);
const layerContainer = layerRenderer.getPixiContainer();
viewPosition.applyTransformationToPixi(layerContainer);
@@ -256,7 +278,12 @@ export default class InstancesRenderer {
if (!threeRenderer) {
// Render a layer with 2D rendering (PixiJS) only.
const time = performance.now();
pixiRenderer.render(layerContainer, { clear: false });
increasePixiRenderingTime(
this._basicProfilingCounters,
performance.now() - time
);
} else {
// Render a layer with 3D rendering, and possibly some 2D rendering too.
const threeScene = layerRenderer.getThreeScene();
@@ -272,12 +299,17 @@ export default class InstancesRenderer {
// Do the rendering of the PixiJS objects of the layer on the render texture.
// Then, update the texture of the plane showing the PixiJS rendering,
// so that the 2D rendering made by PixiJS can be shown in the 3D world.
const pixiStartTime = performance.now();
layerRenderer.renderOnPixiRenderTexture(pixiRenderer);
layerRenderer.updateThreePlaneTextureFromPixiRenderTexture(
// The renderers are needed to find the internal WebGL texture.
threeRenderer,
pixiRenderer
);
increasePixiRenderingTime(
this._basicProfilingCounters,
performance.now() - pixiStartTime
);
// It's important to reset the internal WebGL state of PixiJS, then Three.js
// to ensure the 3D rendering is made properly by Three.js
@@ -287,7 +319,13 @@ export default class InstancesRenderer {
// Clear the depth as each layer is independent and display on top of the previous one,
// even 3D objects.
threeRenderer.clearDepth();
const threeStartTime = performance.now();
threeRenderer.render(threeScene, threeCamera);
increaseThreeRenderingTime(
this._basicProfilingCounters,
performance.now() - threeStartTime
);
}
}
}
@@ -300,7 +338,12 @@ export default class InstancesRenderer {
pixiRenderer.reset();
}
const time = performance.now();
pixiRenderer.render(uiPixiContainer);
increasePixiUiRenderingTime(
this._basicProfilingCounters,
performance.now() - time
);
if (threeRenderer) {
// It's important to reset the internal WebGL state of PixiJS, then Three.js

View File

@@ -0,0 +1,72 @@
// @flow
import * as PIXI from 'pixi.js-legacy';
import {
getBasicProfilingCountersText,
type BasicProfilingCounters,
} from './InstancesRenderer/BasicProfilingCounters';
export default class ProfilerBar {
_profilerBarContainer: PIXI.Container;
_profilerBarBackground: PIXI.Graphics;
_profilerBarText: PIXI.Text;
constructor() {
this._profilerBarContainer = new PIXI.Container();
this._profilerBarContainer.alpha = 0.8;
this._profilerBarContainer.hitArea = new PIXI.Rectangle(0, 0, 0, 0);
this._profilerBarBackground = new PIXI.Graphics();
this._profilerBarText = new PIXI.Text('', {
fontSize: 12,
fill: 0xffffff,
align: 'left',
});
this._profilerBarContainer.addChild(this._profilerBarBackground);
this._profilerBarContainer.addChild(this._profilerBarText);
}
getPixiObject(): PIXI.Container {
return this._profilerBarContainer;
}
render({
basicProfilingCounters,
display,
}: {|
basicProfilingCounters: BasicProfilingCounters,
display: boolean,
|}) {
if (!display) {
this._profilerBarContainer.visible = false;
return;
}
this._profilerBarContainer.visible = true;
const textPadding = 5;
const profilerBarPadding = 15;
const borderRadius = 6;
const textXPosition = profilerBarPadding + textPadding;
const textYPosition = profilerBarPadding + textPadding;
this._profilerBarText.text = getBasicProfilingCountersText(
basicProfilingCounters
);
this._profilerBarText.position.x = textXPosition;
this._profilerBarText.position.y = textYPosition;
const profilerBarXPosition = profilerBarPadding;
const profilerBarYPosition = profilerBarPadding;
const profilerBarWidth = this._profilerBarText.width + textPadding * 2;
const profilerBarHeight = this._profilerBarText.height + textPadding * 2;
this._profilerBarBackground.clear();
this._profilerBarBackground.beginFill(0x000000, 0.8);
this._profilerBarBackground.drawRoundedRect(
profilerBarXPosition,
profilerBarYPosition,
profilerBarWidth,
profilerBarHeight,
borderRadius
);
this._profilerBarBackground.endFill();
}
}

View File

@@ -23,6 +23,7 @@ import * as THREE from 'three';
import FpsLimiter from './FpsLimiter';
import { startPIXITicker, stopPIXITicker } from '../Utils/PIXITicker';
import StatusBar from './StatusBar';
import ProfilerBar from './ProfilerBar';
import CanvasCursor from './CanvasCursor';
import InstancesAdder from './InstancesAdder';
import { makeDropTarget } from '../UI/DragAndDrop/DropTarget';
@@ -128,6 +129,7 @@ type Props = {|
onMouseLeave?: MouseEvent => void,
screenType: ScreenType,
showObjectInstancesIn3D: boolean,
showBasicProfilingCounters: boolean,
|};
type State = {|
@@ -161,6 +163,7 @@ export default class InstancesEditor extends Component<Props, State> {
windowBorder: WindowBorder;
windowMask: WindowMask;
statusBar: StatusBar;
profilerBar: ProfilerBar;
uiPixiContainer: PIXI.Container;
backgroundPixiContainer: PIXI.Container;
backgroundArea: PIXI.Container;
@@ -480,6 +483,9 @@ export default class InstancesEditor extends Component<Props, State> {
if (this.background) {
this.backgroundPixiContainer.removeChild(this.background.getPixiObject());
}
if (this.profilerBar) {
this.uiPixiContainer.removeChild(this.profilerBar.getPixiObject());
}
this.instancesRenderer = new InstancesRenderer({
project: props.project,
@@ -570,6 +576,7 @@ export default class InstancesEditor extends Component<Props, State> {
height: this.props.height,
getLastCursorSceneCoordinates: this.getLastCursorSceneCoordinates,
});
this.profilerBar = new ProfilerBar();
this.uiPixiContainer.addChild(this.selectionRectangle.getPixiObject());
this.uiPixiContainer.addChild(this.instancesRenderer.getPixiContainer());
@@ -578,6 +585,7 @@ export default class InstancesEditor extends Component<Props, State> {
this.uiPixiContainer.addChild(this.selectedInstances.getPixiContainer());
this.uiPixiContainer.addChild(this.highlightedInstance.getPixiObject());
this.uiPixiContainer.addChild(this.statusBar.getPixiObject());
this.uiPixiContainer.addChild(this.profilerBar.getPixiObject());
this.uiPixiContainer.addChild(this.tileMapPaintingPreview.getPixiObject());
this.uiPixiContainer.addChild(this.clickInterceptor.getPixiObject());
@@ -1576,6 +1584,10 @@ export default class InstancesEditor extends Component<Props, State> {
this.windowBorder.render();
this.windowMask.render();
this.statusBar.render();
this.profilerBar.render({
basicProfilingCounters: this.instancesRenderer.getBasicProfilingCounters(),
display: this.props.showBasicProfilingCounters,
});
this.background.render();
this.instancesRenderer.render(

View File

@@ -216,6 +216,7 @@ export type PreferencesValues = {|
showDeprecatedInstructionWarning: boolean,
openDiagnosticReportAutomatically: boolean,
use3DEditor: boolean,
showBasicProfilingCounters: boolean,
inAppTutorialsProgress: InAppTutorialProgressDatabase,
newProjectsDefaultFolder: string,
newProjectsDefaultStorageProviderName: string,
@@ -299,6 +300,7 @@ export type Preferences = {|
getShowDeprecatedInstructionWarning: () => boolean,
setUse3DEditor: (enabled: boolean) => void,
getUse3DEditor: () => boolean,
setShowBasicProfilingCounters: (enabled: boolean) => void,
setNewProjectsDefaultStorageProviderName: (name: string) => void,
saveTutorialProgress: ({|
tutorialId: string,
@@ -369,6 +371,7 @@ export const initialPreferences = {
openDiagnosticReportAutomatically: true,
showDeprecatedInstructionWarning: false,
use3DEditor: isWebGLSupported(),
showBasicProfilingCounters: false,
inAppTutorialsProgress: {},
newProjectsDefaultFolder: app ? findDefaultFolder(app) : '',
newProjectsDefaultStorageProviderName: 'Cloud',
@@ -436,6 +439,7 @@ export const initialPreferences = {
getShowDeprecatedInstructionWarning: () => false,
setUse3DEditor: (enabled: boolean) => {},
getUse3DEditor: () => false,
setShowBasicProfilingCounters: (enabled: boolean) => {},
saveTutorialProgress: () => {},
getTutorialProgress: () => {},
setNewProjectsDefaultFolder: () => {},

View File

@@ -75,6 +75,7 @@ const PreferencesDialog = ({
setOpenDiagnosticReportAutomatically,
setShowDeprecatedInstructionWarning,
setUse3DEditor,
setShowBasicProfilingCounters,
setNewProjectsDefaultFolder,
setUseShortcutToClosePreviewWindow,
setWatchProjectFolderFilesForLocalProjects,
@@ -448,6 +449,12 @@ const PreferencesDialog = ({
<Trans>Show a warning on deprecated actions and conditions</Trans>
}
/>
<Toggle
onToggle={(e, check) => setShowBasicProfilingCounters(check)}
toggled={values.showBasicProfilingCounters}
labelPosition="right"
label={<Trans>Display profiling information in scene editor</Trans>}
/>
<Toggle
onToggle={(e, check) => setUse3DEditor(check)}
toggled={values.use3DEditor}

View File

@@ -169,6 +169,9 @@ export default class PreferencesProvider extends React.Component<Props, State> {
),
setUse3DEditor: this._setUse3DEditor.bind(this),
getUse3DEditor: this._getUse3DEditor.bind(this),
setShowBasicProfilingCounters: this._setShowBasicProfilingCounters.bind(
this
),
saveTutorialProgress: this._saveTutorialProgress.bind(this),
getTutorialProgress: this._getTutorialProgress.bind(this),
setNewProjectsDefaultFolder: this._setNewProjectsDefaultFolder.bind(this),
@@ -511,6 +514,18 @@ export default class PreferencesProvider extends React.Component<Props, State> {
return this.state.values.use3DEditor;
}
_setShowBasicProfilingCounters(showBasicProfilingCounters: boolean) {
this.setState(
state => ({
values: {
...state.values,
showBasicProfilingCounters,
},
}),
() => this._persistValuesToLocalStorage(this.state)
);
}
_checkUpdates(forceDownload?: boolean) {
// Checking for updates is only done on Electron.
// Note: This could be abstracted away later if other updates mechanisms

View File

@@ -1114,6 +1114,7 @@ const MainFrame = (props: Props) => {
if (error.name === 'CloudProjectReadingError') {
setCloudProjectFileMetadataToRecover(fileMetadata);
} else {
console.error('Failed to open the project:', error);
const errorMessage = getOpenErrorMessage
? getOpenErrorMessage(error)
: t`Ensure that you are connected to internet and that the URL used is correct, then try again.`;

View File

@@ -0,0 +1,153 @@
// @flow
import * as React from 'react';
import CompactPropertiesEditor from '../../CompactPropertiesEditor';
import propertiesMapToSchema from '../../CompactPropertiesEditor/PropertiesMapToCompactSchema';
import { type ResourceManagementProps } from '../../ResourcesList/ResourceSource';
import { type UnsavedChanges } from '../../MainFrame/UnsavedChangesContext';
import { ColumnStackLayout } from '../../UI/Layout';
import ChevronArrowRight from '../../UI/CustomSvgIcons/ChevronArrowRight';
import { Trans } from '@lingui/macro';
import FlatButton from '../../UI/FlatButton';
import ChevronArrowTop from '../../UI/CustomSvgIcons/ChevronArrowTop';
import Text from '../../UI/Text';
const gd: libGDevelop = global.gd;
type Props = {|
project: gdProject,
onRefreshAllFields: () => void,
resourceManagementProps: ResourceManagementProps,
unsavedChanges?: ?UnsavedChanges,
eventsBasedObject: gdEventsBasedObject,
customObjectConfiguration: gdCustomObjectConfiguration,
childObject: gdObject,
|};
export const ChildObjectPropertiesEditor = ({
project,
onRefreshAllFields,
resourceManagementProps,
unsavedChanges,
eventsBasedObject,
customObjectConfiguration,
childObject,
}: Props) => {
const [
showObjectAdvancedOptions,
setShowObjectAdvancedOptions,
] = React.useState(false);
const childObjectConfiguration = customObjectConfiguration.getChildObjectConfiguration(
childObject.getName()
);
const childObjectConfigurationAsGd = gd.castObject(
childObjectConfiguration,
gd.ObjectConfiguration
);
// Properties:
const objectBasicPropertiesSchema = React.useMemo(
() => {
const properties = childObjectConfigurationAsGd.getProperties();
const schema = propertiesMapToSchema({
properties,
getProperties: ({ object, objectConfiguration }) =>
objectConfiguration.getProperties(),
onUpdateProperty: ({ object, objectConfiguration }, name, value) =>
objectConfiguration.updateProperty(name, value),
visibility: 'Basic',
});
return schema;
},
[childObjectConfigurationAsGd]
);
const objectAdvancedPropertiesSchema = React.useMemo(
() => {
const properties = childObjectConfigurationAsGd.getProperties();
const schema = propertiesMapToSchema({
properties,
getProperties: ({ object, objectConfiguration }) =>
objectConfiguration.getProperties(),
onUpdateProperty: ({ object, objectConfiguration }, name, value) =>
objectConfiguration.updateProperty(name, value),
visibility: 'Advanced',
});
return schema;
},
[childObjectConfigurationAsGd]
);
const hasObjectAdvancedProperties = objectAdvancedPropertiesSchema.length > 0;
const hasSomeObjectProperties =
objectBasicPropertiesSchema.length > 0 || hasObjectAdvancedProperties;
return (
<ColumnStackLayout noMargin noOverflowParent>
{!hasSomeObjectProperties && (
<Text size="body2" align="center" color="secondary">
<Trans>This object has no properties.</Trans>
</Text>
)}
{hasSomeObjectProperties && (
<CompactPropertiesEditor
project={project}
resourceManagementProps={resourceManagementProps}
unsavedChanges={unsavedChanges}
schema={objectBasicPropertiesSchema}
instances={[
{
object: childObject,
objectConfiguration: childObjectConfigurationAsGd,
},
]}
onInstancesModified={() => {
// TODO: undo/redo?
}}
onRefreshAllFields={onRefreshAllFields}
/>
)}
{!showObjectAdvancedOptions && hasObjectAdvancedProperties && (
<FlatButton
fullWidth
primary
leftIcon={<ChevronArrowRight />}
label={<Trans>Show more</Trans>}
onClick={() => {
setShowObjectAdvancedOptions(true);
}}
/>
)}
{showObjectAdvancedOptions && hasObjectAdvancedProperties && (
<CompactPropertiesEditor
project={project}
resourceManagementProps={resourceManagementProps}
unsavedChanges={unsavedChanges}
schema={objectAdvancedPropertiesSchema}
instances={[
{
object: childObject,
objectConfiguration: childObjectConfigurationAsGd,
},
]}
onInstancesModified={() => {
// TODO: undo/redo?
}}
onRefreshAllFields={onRefreshAllFields}
/>
)}
{showObjectAdvancedOptions && hasObjectAdvancedProperties && (
<FlatButton
fullWidth
primary
leftIcon={<ChevronArrowTop />}
label={<Trans>Show less</Trans>}
onClick={() => {
setShowObjectAdvancedOptions(false);
}}
/>
)}
</ColumnStackLayout>
);
};

View File

@@ -0,0 +1,182 @@
// @flow
import * as React from 'react';
import { type ResourceManagementProps } from '../../ResourcesList/ResourceSource';
import propertiesMapToSchema from '../../CompactPropertiesEditor/PropertiesMapToCompactSchema';
import CompactPropertiesEditor from '../../CompactPropertiesEditor';
import FlatButton from '../../UI/FlatButton';
import { ColumnStackLayout } from '../../UI/Layout';
import { Trans } from '@lingui/macro';
import ChevronArrowTop from '../../UI/CustomSvgIcons/ChevronArrowTop';
import ChevronArrowRight from '../../UI/CustomSvgIcons/ChevronArrowRight';
import Text from '../../UI/Text';
import { Line } from '../../UI/Grid';
import { useForceRecompute } from '../../Utils/UseForceUpdate';
import { type Schema, type ActionButton } from '../../CompactPropertiesEditor';
import ShareExternal from '../../UI/CustomSvgIcons/ShareExternal';
export const getSchemaWithOpenFullEditorButton = ({
schema,
fullEditorLabel,
behavior,
onOpenFullEditor,
}: {|
schema: Schema,
fullEditorLabel: ?string,
behavior: gdBehavior,
onOpenFullEditor: () => void,
|}): Schema => {
if (!fullEditorLabel) return schema;
const actionButton: ActionButton = {
label: fullEditorLabel,
disabled: 'onValuesDifferent',
nonFieldType: 'button',
showRightIcon: true,
getIcon: style => <ShareExternal style={style} />,
getValue: behavior => behavior.getName(),
onClick: behavior => onOpenFullEditor(),
};
let added = false;
schema.forEach(field => {
if (field.children && field.name === '') {
field.children.push(actionButton);
added = true;
}
});
if (!added) schema.push(actionButton);
return schema;
};
export const CompactBehaviorPropertiesEditor = ({
project,
behaviorMetadata,
behavior,
object,
onOpenFullEditor,
onBehaviorUpdated,
resourceManagementProps,
}: {|
project: gdProject,
behaviorMetadata: gdBehaviorMetadata,
behavior: gdBehavior,
object: gdObject,
onOpenFullEditor: () => void,
onBehaviorUpdated: () => void,
resourceManagementProps: ResourceManagementProps,
|}) => {
const [showAdvancedOptions, setShowAdvancedOptions] = React.useState(false);
const [schemaRecomputeTrigger, forceRecomputeSchema] = useForceRecompute();
const fullEditorLabel = behaviorMetadata.getOpenFullEditorLabel();
const basicPropertiesSchema = React.useMemo(
() => {
if (schemaRecomputeTrigger) {
// schemaRecomputeTrigger allows to invalidate the schema when required.
}
const schema = propertiesMapToSchema({
properties: behavior.getProperties(),
getProperties: behavior => behavior.getProperties(),
onUpdateProperty: (behavior, name, value) => {
behavior.updateProperty(name, value);
},
object,
visibility: 'Basic',
});
return getSchemaWithOpenFullEditorButton({
schema,
fullEditorLabel,
behavior,
onOpenFullEditor,
});
},
[
behavior,
object,
schemaRecomputeTrigger,
fullEditorLabel,
onOpenFullEditor,
]
);
const advancedPropertiesSchema = React.useMemo(
() => {
if (schemaRecomputeTrigger) {
// schemaRecomputeTrigger allows to invalidate the schema when required.
}
return propertiesMapToSchema({
properties: behavior.getProperties(),
getProperties: behavior => behavior.getProperties(),
onUpdateProperty: (behavior, name, value) => {
behavior.updateProperty(name, value);
},
object,
visibility: 'Advanced',
});
},
[behavior, object, schemaRecomputeTrigger]
);
const hasAdvancedProperties = advancedPropertiesSchema.length > 0;
const hasSomeProperties =
basicPropertiesSchema.length > 0 || hasAdvancedProperties;
return (
<ColumnStackLayout expand noMargin noOverflowParent>
{!hasSomeProperties && (
<Line justifyContent="center" expand>
<Text size="body2" color="secondary" align="center" noMargin>
<Trans>Nothing to configure for this behavior.</Trans>
</Text>
</Line>
)}
{hasSomeProperties && (
<CompactPropertiesEditor
project={project}
schema={basicPropertiesSchema}
instances={[behavior]}
onInstancesModified={onBehaviorUpdated}
resourceManagementProps={resourceManagementProps}
onRefreshAllFields={forceRecomputeSchema}
/>
)}
{!showAdvancedOptions && hasAdvancedProperties && (
<FlatButton
fullWidth
primary
leftIcon={<ChevronArrowRight />}
label={<Trans>Show more</Trans>}
onClick={() => {
setShowAdvancedOptions(true);
}}
/>
)}
{showAdvancedOptions && hasAdvancedProperties && (
<CompactPropertiesEditor
project={project}
schema={advancedPropertiesSchema}
instances={[behavior]}
onInstancesModified={onBehaviorUpdated}
resourceManagementProps={resourceManagementProps}
onRefreshAllFields={forceRecomputeSchema}
/>
)}
{showAdvancedOptions && hasAdvancedProperties && (
<FlatButton
fullWidth
primary
leftIcon={<ChevronArrowTop />}
label={<Trans>Show less</Trans>}
onClick={() => {
setShowAdvancedOptions(false);
}}
/>
)}
</ColumnStackLayout>
);
};

View File

@@ -0,0 +1,40 @@
// @flow
import * as React from 'react';
import { type Schema, type ActionButton } from '../../CompactPropertiesEditor';
import ShareExternal from '../../UI/CustomSvgIcons/ShareExternal';
export const getSchemaWithOpenFullEditorButton = ({
schema,
fullEditorLabel,
object,
onEditObject,
}: {|
schema: Schema,
fullEditorLabel: ?string,
object: gdObject,
onEditObject: (object: gdObject) => void,
|}): Schema => {
if (!fullEditorLabel) return schema;
const actionButton: ActionButton = {
label: fullEditorLabel,
disabled: 'onValuesDifferent',
nonFieldType: 'button',
showRightIcon: true,
getIcon: style => <ShareExternal style={style} />,
getValue: ({ object }) => object.getName(),
onClick: ({ object }) => onEditObject(object),
};
let added = false;
schema.forEach(field => {
if (field.children && field.name === '') {
field.children.push(actionButton);
added = true;
}
});
if (!added) schema.push(actionButton);
return schema;
};

View File

@@ -0,0 +1,703 @@
// @flow
import { type I18n as I18nType } from '@lingui/core';
import * as React from 'react';
import { type UnsavedChanges } from '../../MainFrame/UnsavedChangesContext';
import VariablesList, {
type HistoryHandler,
type VariablesListInterface,
} from '../../VariablesList/VariablesList';
import { type ProjectScopedContainersAccessor } from '../../InstructionOrExpression/EventsScope';
import ErrorBoundary from '../../UI/ErrorBoundary';
import ScrollView from '../../UI/ScrollView';
import { Column, Line, Spacer, marginsSize } from '../../UI/Grid';
import CompactPropertiesEditor, {
Separator,
} from '../../CompactPropertiesEditor';
import Text from '../../UI/Text';
import { Trans, t } from '@lingui/macro';
import IconButton from '../../UI/IconButton';
import ShareExternal from '../../UI/CustomSvgIcons/ShareExternal';
import EventsRootVariablesFinder from '../../Utils/EventsRootVariablesFinder';
import propertiesMapToSchema from '../../CompactPropertiesEditor/PropertiesMapToCompactSchema';
import { type ObjectEditorTab } from '../../ObjectEditor/ObjectEditorDialog';
import { CompactBehaviorPropertiesEditor } from './CompactBehaviorPropertiesEditor';
import { type ResourceManagementProps } from '../../ResourcesList/ResourceSource';
import Paper from '../../UI/Paper';
import { ColumnStackLayout, LineStackLayout } from '../../UI/Layout';
import { IconContainer } from '../../UI/IconContainer';
import Remove from '../../UI/CustomSvgIcons/Remove';
import useForceUpdate, { useForceRecompute } from '../../Utils/UseForceUpdate';
import ChevronArrowRight from '../../UI/CustomSvgIcons/ChevronArrowRight';
import ChevronArrowBottom from '../../UI/CustomSvgIcons/ChevronArrowBottom';
import Add from '../../UI/CustomSvgIcons/Add';
import { useManageObjectBehaviors } from '../../BehaviorsEditor';
import Object3d from '../../UI/CustomSvgIcons/Object3d';
import Object2d from '../../UI/CustomSvgIcons/Object2d';
import { CompactEffectPropertiesEditor } from '../../EffectsList/CompactEffectPropertiesEditor';
import { mapFor } from '../../Utils/MapFor';
import {
getEnumeratedEffectMetadata,
useManageEffects,
} from '../../EffectsList';
import CompactSelectField from '../../UI/CompactSelectField';
import SelectOption from '../../UI/SelectOption';
import { ChildObjectPropertiesEditor } from './ChildObjectPropertiesEditor';
import { getSchemaWithOpenFullEditorButton } from './CompactObjectPropertiesSchema';
import FlatButton from '../../UI/FlatButton';
import ChevronArrowTop from '../../UI/CustomSvgIcons/ChevronArrowTop';
import Help from '../../UI/CustomSvgIcons/Help';
import { getHelpLink } from '../../Utils/HelpLink';
import Window from '../../Utils/Window';
import CompactTextField from '../../UI/CompactTextField';
import SquaredDoubleChevronArrowDown from '../../UI/CustomSvgIcons/SquaredDoubleChevronArrowDown';
import SquaredDoubleChevronArrowUp from '../../UI/CustomSvgIcons/SquaredDoubleChevronArrowUp';
import { textEllipsisStyle } from '../../UI/TextEllipsis';
const gd: libGDevelop = global.gd;
export const styles = {
icon: {
fontSize: 18,
},
scrollView: {
paddingTop: marginsSize,
// In theory, should not be needed (the children should be responsible for not
// overflowing the parent). In practice, even when no horizontal scroll is shown
// on Chrome, it might happen on Safari. Prevent any scroll to be 100% sure no
// scrollbar will be shown.
overflowX: 'hidden',
},
};
const CollapsibleSubPanel = ({
renderContent,
isFolded,
toggleFolded,
title,
titleIcon,
onRemove,
}: {|
renderContent: () => React.Node,
isFolded: boolean,
toggleFolded: () => void,
titleIcon?: ?React.Node,
title: string,
onRemove?: () => void,
|}) => (
<Paper background="medium">
<Line expand>
<ColumnStackLayout expand noOverflowParent>
<LineStackLayout noMargin justifyContent="space-between">
<Line noMargin alignItems="center">
<IconButton onClick={toggleFolded} size="small">
{isFolded ? (
<ChevronArrowRight style={styles.icon} />
) : (
<ChevronArrowBottom style={styles.icon} />
)}
</IconButton>
{titleIcon}
{titleIcon && <Spacer />}
<Text noMargin size="body" style={textEllipsisStyle}>
{title}
</Text>
</Line>
{onRemove ? (
<IconButton
tooltip={t`Remove behavior`}
onClick={onRemove}
size="small"
>
<Remove style={styles.icon} />
</IconButton>
) : null}
</LineStackLayout>
{isFolded ? null : renderContent()}
</ColumnStackLayout>
</Line>
</Paper>
);
const TopLevelCollapsibleSection = ({
title,
isFolded,
toggleFolded,
renderContent,
renderContentAsHiddenWhenFolded,
noContentMargin,
onOpenFullEditor,
onAdd,
}: {|
title: React.Node,
isFolded: boolean,
toggleFolded: () => void,
renderContent: () => React.Node,
renderContentAsHiddenWhenFolded?: boolean,
noContentMargin?: boolean,
onOpenFullEditor: () => void,
onAdd?: () => void,
|}) => (
<>
<Separator />
<Column noOverflowParent>
<LineStackLayout alignItems="center" justifyContent="space-between">
<LineStackLayout noMargin alignItems="center">
<IconButton size="small" onClick={toggleFolded}>
{isFolded ? (
<SquaredDoubleChevronArrowUp style={styles.icon} />
) : (
<SquaredDoubleChevronArrowDown style={styles.icon} />
)}
</IconButton>
<Text size="sub-title" noMargin style={textEllipsisStyle}>
{title}
</Text>
</LineStackLayout>
<Line alignItems="center" noMargin>
<IconButton size="small" onClick={onOpenFullEditor}>
<ShareExternal style={styles.icon} />
</IconButton>
{onAdd && (
<IconButton size="small" onClick={onAdd}>
<Add style={styles.icon} />
</IconButton>
)}
</Line>
</LineStackLayout>
</Column>
<Column noMargin={noContentMargin}>
{isFolded ? (
renderContentAsHiddenWhenFolded ? (
<div style={{ display: 'none' }}>{renderContent()}</div>
) : null
) : (
renderContent()
)}
</Column>
</>
);
type Props = {|
project: gdProject,
resourceManagementProps: ResourceManagementProps,
layout?: ?gdLayout,
eventsFunctionsExtension: gdEventsFunctionsExtension | null,
onUpdateBehaviorsSharedData: () => void,
objectsContainer: gdObjectsContainer,
globalObjectsContainer: gdObjectsContainer | null,
layersContainer: gdLayersContainer,
projectScopedContainersAccessor: ProjectScopedContainersAccessor,
unsavedChanges?: ?UnsavedChanges,
i18n: I18nType,
historyHandler?: HistoryHandler,
objects: Array<gdObject>,
onEditObject: (object: gdObject, initialTab: ?ObjectEditorTab) => void,
|};
export const CompactObjectPropertiesEditor = ({
project,
resourceManagementProps,
layout,
eventsFunctionsExtension,
onUpdateBehaviorsSharedData,
objectsContainer,
globalObjectsContainer,
layersContainer,
projectScopedContainersAccessor,
unsavedChanges,
i18n,
historyHandler,
objects,
onEditObject,
}: Props) => {
const forceUpdate = useForceUpdate();
const [
showObjectAdvancedOptions,
setShowObjectAdvancedOptions,
] = React.useState(false);
const [isPropertiesFolded, setIsPropertiesFolded] = React.useState(false);
const [isBehaviorsFolded, setIsBehaviorsFolded] = React.useState(false);
const [isVariablesFolded, setIsVariablesFolded] = React.useState(false);
const [isEffectsFolded, setIsEffectsFolded] = React.useState(false);
const [schemaRecomputeTrigger, forceRecomputeSchema] = useForceRecompute();
const variablesListRef = React.useRef<?VariablesListInterface>(null);
const object = objects[0];
const objectConfiguration = object.getConfiguration();
// Don't use a memo for this because metadata from custom objects are built
// from event-based object when extensions are refreshed after an extension
// installation.
const objectMetadata = gd.MetadataProvider.getObjectMetadata(
project.getCurrentPlatform(),
object.getType()
);
const is3DObject = !!objectMetadata && objectMetadata.isRenderedIn3D();
const fullEditorLabel = objectMetadata
? objectMetadata.getOpenFullEditorLabel()
: null;
// TODO: Workaround a bad design of ObjectJsImplementation. When getProperties
// and associated methods are redefined in JS, they have different arguments (
// see ObjectJsImplementation C++ implementation). If called directly here from JS,
// the arguments will be mismatched. To workaround this, always cast the object to
// a base gdObject to ensure C++ methods are called.
const objectConfigurationAsGd = gd.castObject(
objectConfiguration,
gd.ObjectConfiguration
);
// Properties:
const objectBasicPropertiesSchema = React.useMemo(
() => {
if (schemaRecomputeTrigger) {
// schemaRecomputeTrigger allows to invalidate the schema when required.
}
const properties = objectConfigurationAsGd.getProperties();
const objectBasicPropertiesSchema = propertiesMapToSchema({
properties,
getProperties: ({ object, objectConfiguration }) =>
objectConfiguration.getProperties(),
onUpdateProperty: ({ object, objectConfiguration }, name, value) =>
objectConfiguration.updateProperty(name, value),
visibility: 'Basic',
});
return getSchemaWithOpenFullEditorButton({
schema: objectBasicPropertiesSchema,
fullEditorLabel,
object,
onEditObject,
});
},
[
objectConfigurationAsGd,
schemaRecomputeTrigger,
fullEditorLabel,
object,
onEditObject,
]
);
const objectAdvancedPropertiesSchema = React.useMemo(
() => {
if (schemaRecomputeTrigger) {
// schemaRecomputeTrigger allows to invalidate the schema when required.
}
const properties = objectConfigurationAsGd.getProperties();
return propertiesMapToSchema({
properties,
getProperties: ({ object, objectConfiguration }) =>
objectConfiguration.getProperties(),
onUpdateProperty: ({ object, objectConfiguration }, name, value) =>
objectConfiguration.updateProperty(name, value),
visibility: 'Advanced',
});
},
[objectConfigurationAsGd, schemaRecomputeTrigger]
);
const hasObjectAdvancedProperties = objectAdvancedPropertiesSchema.length > 0;
const hasSomeObjectProperties =
objectBasicPropertiesSchema.length > 0 || hasObjectAdvancedProperties;
// Behaviors:
const {
openNewBehaviorDialog,
newBehaviorDialog,
removeBehavior,
} = useManageObjectBehaviors({
project,
object,
eventsFunctionsExtension,
onUpdate: forceUpdate,
onBehaviorsUpdated: forceUpdate,
onUpdateBehaviorsSharedData,
});
const allVisibleBehaviors = object
.getAllBehaviorNames()
.toJSArray()
.map(behaviorName => object.getBehavior(behaviorName))
.filter(behavior => !behavior.isDefaultBehavior());
// Effects:
const effectsContainer = object.getEffects();
const {
allEffectMetadata,
all2DEffectMetadata,
addEffect,
removeEffect,
chooseEffectType,
} = useManageEffects({
effectsContainer,
project,
onEffectsUpdated: forceUpdate,
onUpdate: forceUpdate,
target: 'object',
});
// Events based object children:
const eventsBasedObject = project.hasEventsBasedObject(
objectConfiguration.getType()
)
? project.getEventsBasedObject(objectConfiguration.getType())
: null;
const customObjectConfiguration = eventsBasedObject
? gd.asCustomObjectConfiguration(objectConfiguration)
: null;
const shouldDisplayEventsBasedObjectChildren =
customObjectConfiguration &&
(customObjectConfiguration.isForcedToOverrideEventsBasedObjectChildrenConfiguration() ||
customObjectConfiguration.isMarkedAsOverridingEventsBasedObjectChildrenConfiguration());
const helpLink = getHelpLink(objectMetadata.getHelpPath());
return (
<ErrorBoundary
componentTitle={<Trans>Object properties</Trans>}
scope="scene-editor-object-properties"
>
<ScrollView
autoHideScrollbar
style={styles.scrollView}
key={objects.map((instance: gdObject) => '' + instance.ptr).join(';')}
>
<Column expand noMargin id="object-properties-editor" noOverflowParent>
<ColumnStackLayout expand noOverflowParent>
<LineStackLayout
noMargin
alignItems="center"
justifyContent="space-between"
>
<LineStackLayout noMargin alignItems="center">
{is3DObject ? (
<Object3d style={styles.icon} />
) : (
<Object2d style={styles.icon} />
)}
<Text size="body" noMargin>
<Trans>{objectMetadata.getFullName()}</Trans>
</Text>
{helpLink && (
<IconButton
size="small"
onClick={() => {
Window.openExternalURL(helpLink);
}}
>
<Help style={styles.icon} />
</IconButton>
)}
</LineStackLayout>
</LineStackLayout>
<CompactTextField
value={object.getName()}
onChange={() => {}}
disabled
/>
</ColumnStackLayout>
<TopLevelCollapsibleSection
title={<Trans>Properties</Trans>}
isFolded={isPropertiesFolded}
toggleFolded={() => setIsPropertiesFolded(!isPropertiesFolded)}
onOpenFullEditor={() => onEditObject(object, 'properties')}
renderContent={() => (
<ColumnStackLayout noMargin noOverflowParent>
{!hasSomeObjectProperties && (
<Text size="body2" align="center" color="secondary">
<Trans>This object has no properties.</Trans>
</Text>
)}
{hasSomeObjectProperties && (
<CompactPropertiesEditor
project={project}
resourceManagementProps={resourceManagementProps}
unsavedChanges={unsavedChanges}
schema={objectBasicPropertiesSchema}
instances={[
{ object, objectConfiguration: objectConfigurationAsGd },
]}
onInstancesModified={() => {
// TODO: undo/redo?
}}
onRefreshAllFields={forceRecomputeSchema}
/>
)}
{!showObjectAdvancedOptions && hasObjectAdvancedProperties && (
<FlatButton
fullWidth
primary
leftIcon={<ChevronArrowRight />}
label={<Trans>Show more</Trans>}
onClick={() => {
setShowObjectAdvancedOptions(true);
}}
/>
)}
{showObjectAdvancedOptions && hasObjectAdvancedProperties && (
<CompactPropertiesEditor
project={project}
resourceManagementProps={resourceManagementProps}
unsavedChanges={unsavedChanges}
schema={objectAdvancedPropertiesSchema}
instances={[
{ object, objectConfiguration: objectConfigurationAsGd },
]}
onInstancesModified={() => {
// TODO: undo/redo?
}}
onRefreshAllFields={forceRecomputeSchema}
/>
)}
{showObjectAdvancedOptions && hasObjectAdvancedProperties && (
<FlatButton
fullWidth
primary
leftIcon={<ChevronArrowTop />}
label={<Trans>Show less</Trans>}
onClick={() => {
setShowObjectAdvancedOptions(false);
}}
/>
)}
{eventsBasedObject &&
customObjectConfiguration &&
shouldDisplayEventsBasedObjectChildren &&
mapFor(
0,
eventsBasedObject.getObjects().getObjectsCount(),
i => {
const childObject = eventsBasedObject
.getObjects()
.getObjectAt(i);
const childObjectName = childObject.getName();
const isFolded = customObjectConfiguration.isChildObjectFolded(
childObjectName
);
return (
<CollapsibleSubPanel
key={i}
renderContent={() => (
<ChildObjectPropertiesEditor
key={i}
project={project}
resourceManagementProps={resourceManagementProps}
unsavedChanges={unsavedChanges}
eventsBasedObject={eventsBasedObject}
customObjectConfiguration={
customObjectConfiguration
}
childObject={childObject}
onRefreshAllFields={forceRecomputeSchema}
/>
)}
isFolded={isFolded}
toggleFolded={() => {
customObjectConfiguration.setChildObjectFolded(
childObjectName,
!isFolded
);
forceUpdate();
}}
title={childObjectName}
/>
);
}
)}
</ColumnStackLayout>
)}
/>
<TopLevelCollapsibleSection
title={<Trans>Behaviors</Trans>}
isFolded={isBehaviorsFolded}
toggleFolded={() => setIsBehaviorsFolded(!isBehaviorsFolded)}
onOpenFullEditor={() => onEditObject(object, 'behaviors')}
onAdd={openNewBehaviorDialog}
renderContent={() => (
<ColumnStackLayout noMargin>
{!allVisibleBehaviors.length && (
<Text size="body2" align="center" color="secondary">
<Trans>There are no behaviors on this object.</Trans>
</Text>
)}
{allVisibleBehaviors.map(behavior => {
const behaviorTypeName = behavior.getTypeName();
const behaviorMetadata = gd.MetadataProvider.getBehaviorMetadata(
gd.JsPlatform.get(),
behaviorTypeName
);
const iconUrl = behaviorMetadata.getIconFilename();
return (
<CollapsibleSubPanel
key={behavior.ptr}
renderContent={() => (
<CompactBehaviorPropertiesEditor
project={project}
behaviorMetadata={behaviorMetadata}
behavior={behavior}
object={object}
onBehaviorUpdated={() => {}}
resourceManagementProps={resourceManagementProps}
onOpenFullEditor={() =>
onEditObject(object, 'behaviors')
}
/>
)}
isFolded={behavior.isFolded()}
toggleFolded={() => {
behavior.setFolded(!behavior.isFolded());
forceUpdate();
}}
titleIcon={
iconUrl ? (
<IconContainer
src={iconUrl}
alt={behaviorMetadata.getFullName()}
size={16}
/>
) : null
}
title={behavior.getName()}
onRemove={() => {
removeBehavior(behavior.getName());
}}
/>
);
})}
</ColumnStackLayout>
)}
/>
<TopLevelCollapsibleSection
title={<Trans>Object Variables</Trans>}
isFolded={isVariablesFolded}
toggleFolded={() => setIsVariablesFolded(!isVariablesFolded)}
onOpenFullEditor={() => onEditObject(object, 'variables')}
onAdd={() => {
if (variablesListRef.current) {
variablesListRef.current.addVariable();
}
setIsVariablesFolded(false);
}}
renderContentAsHiddenWhenFolded={
true /* Allows to keep a ref to the variables list for add button to work. */
}
noContentMargin
renderContent={() => (
<VariablesList
ref={variablesListRef}
projectScopedContainersAccessor={
projectScopedContainersAccessor
}
directlyStoreValueChangesWhileEditing
variablesContainer={object.getVariables()}
areObjectVariables
size="compact"
onComputeAllVariableNames={() =>
object && layout
? EventsRootVariablesFinder.findAllObjectVariables(
project.getCurrentPlatform(),
project,
layout,
object.getName()
)
: []
}
historyHandler={historyHandler}
toolbarIconStyle={styles.icon}
compactEmptyPlaceholderText={
<Trans>There are no variables on this object.</Trans>
}
/>
)}
/>
{objectMetadata &&
objectMetadata.hasDefaultBehavior(
'EffectCapability::EffectBehavior'
) && (
<TopLevelCollapsibleSection
title={<Trans>Effects</Trans>}
isFolded={isEffectsFolded}
toggleFolded={() => setIsEffectsFolded(!isEffectsFolded)}
onOpenFullEditor={() => onEditObject(object, 'effects')}
onAdd={() => addEffect(false)}
renderContent={() => (
<ColumnStackLayout>
{effectsContainer.getEffectsCount() === 0 && (
<Text size="body2" align="center" color="secondary">
<Trans>There are no effects on this object.</Trans>
</Text>
)}
{mapFor(
0,
effectsContainer.getEffectsCount(),
(index: number) => {
const effect: gdEffect = effectsContainer.getEffectAt(
index
);
const effectType = effect.getEffectType();
const effectMetadata = getEnumeratedEffectMetadata(
allEffectMetadata,
effectType
);
return (
<CollapsibleSubPanel
key={effect.ptr}
renderContent={() => (
<ColumnStackLayout expand noOverflowParent>
<CompactSelectField
value={effectType}
onChange={type =>
chooseEffectType(effect, type)
}
>
{all2DEffectMetadata.map(effectMetadata => (
<SelectOption
key={effectMetadata.type}
value={effectMetadata.type}
label={effectMetadata.fullName}
disabled={
effectMetadata.isMarkedAsNotWorkingForObjects
}
/>
))}
</CompactSelectField>
<CompactEffectPropertiesEditor
project={project}
effect={effect}
effectMetadata={effectMetadata}
resourceManagementProps={
resourceManagementProps
}
/>
</ColumnStackLayout>
)}
isFolded={effect.isFolded()}
toggleFolded={() => {
effect.setFolded(!effect.isFolded());
forceUpdate();
}}
title={effect.getName()}
onRemove={() => {
removeEffect(effect);
}}
/>
);
}
)}
</ColumnStackLayout>
)}
/>
)}
</Column>
</ScrollView>
{newBehaviorDialog}
</ErrorBoundary>
);
};

View File

@@ -164,18 +164,13 @@ export default class ParticleEmitterEditor extends React.Component<
floatingLabelText={<Trans>Particles start color</Trans>}
disableAlpha
fullWidth
color={rgbColorToRGBString({
r: particleEmitterConfiguration.getParticleRed1(),
g: particleEmitterConfiguration.getParticleGreen1(),
b: particleEmitterConfiguration.getParticleBlue1(),
})}
color={particleEmitterConfiguration.getParticleColor1()}
onChange={color => {
const rgbColor = rgbStringAndAlphaToRGBColor(color);
if (rgbColor) {
particleEmitterConfiguration.setParticleRed1(rgbColor.r);
particleEmitterConfiguration.setParticleGreen1(rgbColor.g);
particleEmitterConfiguration.setParticleBlue1(rgbColor.b);
particleEmitterConfiguration.setParticleColor1(
rgbColorToRGBString(rgbColor)
);
this.forceUpdate();
}
}}
@@ -199,18 +194,13 @@ export default class ParticleEmitterEditor extends React.Component<
floatingLabelText={<Trans>Particles end color</Trans>}
disableAlpha
fullWidth
color={rgbColorToRGBString({
r: particleEmitterConfiguration.getParticleRed2(),
g: particleEmitterConfiguration.getParticleGreen2(),
b: particleEmitterConfiguration.getParticleBlue2(),
})}
color={particleEmitterConfiguration.getParticleColor2()}
onChange={color => {
const rgbColor = rgbStringAndAlphaToRGBColor(color);
if (rgbColor) {
particleEmitterConfiguration.setParticleRed2(rgbColor.r);
particleEmitterConfiguration.setParticleGreen2(rgbColor.g);
particleEmitterConfiguration.setParticleBlue2(rgbColor.b);
particleEmitterConfiguration.setParticleColor2(
rgbColorToRGBString(rgbColor)
);
this.forceUpdate();
}
}}

View File

@@ -54,18 +54,12 @@ export default class PanelSpriteEditor extends React.Component<
floatingLabelText={<Trans>Outline color</Trans>}
disableAlpha
fullWidth
color={rgbColorToRGBString({
r: shapePainterConfiguration.getOutlineColorR(),
g: shapePainterConfiguration.getOutlineColorG(),
b: shapePainterConfiguration.getOutlineColorB(),
})}
color={shapePainterConfiguration.getOutlineColor()}
onChange={color => {
const rgbColor = rgbStringAndAlphaToRGBColor(color);
if (rgbColor) {
shapePainterConfiguration.setOutlineColor(
rgbColor.r,
rgbColor.g,
rgbColor.b
rgbColorToRGBString(rgbColor)
);
this.forceUpdate();
@@ -104,18 +98,12 @@ export default class PanelSpriteEditor extends React.Component<
floatingLabelText={<Trans>Fill color</Trans>}
disableAlpha
fullWidth
color={rgbColorToRGBString({
r: shapePainterConfiguration.getFillColorR(),
g: shapePainterConfiguration.getFillColorG(),
b: shapePainterConfiguration.getFillColorB(),
})}
color={shapePainterConfiguration.getFillColor()}
onChange={color => {
const rgbColor = rgbStringAndAlphaToRGBColor(color);
if (rgbColor) {
shapePainterConfiguration.setFillColor(
rgbColor.r,
rgbColor.g,
rgbColor.b
rgbColorToRGBString(rgbColor)
);
this.forceUpdate();

View File

@@ -95,7 +95,6 @@ const SpineEditor = ({
const forceUpdate = useForceUpdate();
const spineConfiguration = gd.asSpineConfiguration(objectConfiguration);
const properties = objectConfiguration.getProperties();
const [nameErrors, setNameErrors] = React.useState<{ [number]: React.Node }>(
{}
@@ -110,7 +109,7 @@ const SpineEditor = ({
const [sourceSelectOptions, setSourceSelectOptions] = React.useState<
Array<Object>
>([]);
const spineResourceName = properties.get('spineResourceName').getValue();
const spineResourceName = spineConfiguration.getSpineResourceName();
React.useEffect(
() => {

View File

@@ -47,8 +47,8 @@ const invalidTexture = PIXI.Texture.from('res/error48.png');
const loadingTexture = PIXI.Texture.from(
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAAA1BMVEXX19f5cgrAAAAAAXRSTlMz/za5cAAAAApJREFUCNdjQAMAABAAAbSqgB8AAAAASUVORK5CYII='
);
let loadedThreeTextures = {};
let loadedThreeMaterials = {};
let loadedOrLoadingThreeTextures: ResourcePromise<THREE.Texture> = {};
let loadedOrLoadingThreeMaterials: ResourcePromise<THREE.Material> = {};
let loadedOrLoading3DModelPromises: ResourcePromise<THREE.THREE_ADDONS.GLTF> = {};
let spineAtlasPromises: ResourcePromise<SpineTextureAtlasOrLoadingError> = {};
let spineDataPromises: ResourcePromise<SpineDataOrLoadingError> = {};
@@ -244,8 +244,8 @@ export default class PixiResourcesLoader {
loadedBitmapFonts = {};
loadedFontFamilies = {};
loadedTextures = {};
loadedThreeTextures = {};
loadedThreeMaterials = {};
loadedOrLoadingThreeTextures = {};
loadedOrLoadingThreeMaterials = {};
loadedOrLoading3DModelPromises = {};
spineAtlasPromises = {};
spineDataPromises = {};
@@ -296,9 +296,10 @@ export default class PixiResourcesLoader {
if (loadedBitmapFonts[resourceName]) {
delete loadedBitmapFonts[resourceName];
}
if (loadedThreeTextures[resourceName]) {
loadedThreeTextures[resourceName].dispose();
delete loadedThreeTextures[resourceName];
if (loadedOrLoadingThreeTextures[resourceName]) {
const threeTexture = await loadedOrLoadingThreeTextures[resourceName];
threeTexture.dispose();
delete loadedOrLoadingThreeTextures[resourceName];
}
if (spineAtlasPromises[resourceName]) {
await PIXI.Assets.unload(resourceName).catch(async () => {
@@ -328,14 +329,17 @@ export default class PixiResourcesLoader {
PIXI.Assets.resolver.prefer();
}
const matchingMaterials = Object.keys(loadedThreeMaterials).filter(key =>
key.startsWith(resourceName)
);
if (matchingMaterials.length > 0) {
matchingMaterials.forEach(key => {
loadedThreeMaterials[key].dispose();
delete loadedThreeMaterials[key];
});
const matchingMaterialCacheKeys = Object.keys(
loadedOrLoadingThreeMaterials
).filter(key => key.startsWith(resourceName));
if (matchingMaterialCacheKeys.length > 0) {
await Promise.all(
matchingMaterialCacheKeys.map(async key => {
const material = await loadedOrLoadingThreeMaterials[key];
material.dispose();
delete loadedOrLoadingThreeMaterials[key];
})
);
}
}
/**
@@ -503,12 +507,12 @@ export default class PixiResourcesLoader {
* @param resourceName The name of the resource
* @returns The requested texture, or a placeholder if not found.
*/
static getThreeTexture(
static async getThreeTexture(
project: gdProject,
resourceName: string
): THREE.Texture {
const loadedThreeTexture = loadedThreeTextures[resourceName];
if (loadedThreeTexture) return loadedThreeTexture;
): Promise<THREE.Texture> {
const loadedOrLoadingPromise = loadedOrLoadingThreeTextures[resourceName];
if (loadedOrLoadingPromise) return loadedOrLoadingPromise;
// Texture is not loaded, load it now from the PixiJS texture.
// TODO (3D) - optimization: don't load the PixiJS Texture if not used by PixiJS.
@@ -518,6 +522,15 @@ export default class PixiResourcesLoader {
resourceName
);
if (!pixiTexture.baseTexture.valid) {
// Post pone texture update if texture is not loaded.
return new Promise(resolve => {
pixiTexture.once('update', () =>
resolve(this.getThreeTexture(project, resourceName))
);
});
}
// @ts-ignore - source does exist on resource.
const image = pixiTexture.baseTexture.resource.source;
if (!(image instanceof HTMLImageElement)) {
@@ -537,9 +550,9 @@ export default class PixiResourcesLoader {
const resource = project.getResourcesManager().getResource(resourceName);
applyThreeTextureSettings(resource, threeTexture);
loadedThreeTextures[resourceName] = threeTexture;
return threeTexture;
return (loadedOrLoadingThreeTextures[resourceName] = Promise.resolve(
threeTexture
));
}
/**
@@ -549,23 +562,31 @@ export default class PixiResourcesLoader {
* @param options Set if the material should be transparent or not.
* @returns The requested material.
*/
static getThreeMaterial(
static async getThreeMaterial(
project: gdProject,
resourceName: string,
{ useTransparentTexture }: {| useTransparentTexture: boolean |}
) {
{
useTransparentTexture,
}: {|
useTransparentTexture: boolean,
|}
): Promise<THREE.Material> {
const cacheKey = `${resourceName}|transparent:${useTransparentTexture.toString()}`;
const loadedThreeMaterial = loadedThreeMaterials[cacheKey];
if (loadedThreeMaterial) return loadedThreeMaterial;
const loadedOrLoadingPromise = loadedOrLoadingThreeMaterials[cacheKey];
if (loadedOrLoadingPromise) return loadedOrLoadingPromise;
const material = new THREE.MeshBasicMaterial({
map: this.getThreeTexture(project, resourceName),
side: useTransparentTexture ? THREE.DoubleSide : THREE.FrontSide,
transparent: useTransparentTexture,
});
return (loadedOrLoadingThreeMaterials[cacheKey] = this.getThreeTexture(
project,
resourceName
).then(texture => {
const material = new THREE.MeshBasicMaterial({
map: texture,
side: useTransparentTexture ? THREE.DoubleSide : THREE.FrontSide,
transparent: useTransparentTexture,
});
loadedThreeMaterials[cacheKey] = material;
return material;
return material;
}));
}
/**

View File

@@ -11,16 +11,21 @@ type StretchedSprite = PIXI.Sprite | PIXI.TilingSprite;
* Renderer for gd.PanelSpriteObject
*
* Heavily inspired from the GDJS PIXI renderer for PanelSprite objects.
* TODO: Find a way to factor GDJS objects and IDE instances renderers.
*/
export default class RenderedPanelSpriteInstance extends RenderedInstance {
_centerSprite: StretchedSprite;
_borderSprites: StretchedSprite[];
// Cache of the values of the properties of the object, to detect
// changes
_textureName: string;
_width: number;
_height: number;
_tiled: boolean;
_wasRendered: boolean;
_leftMargin: number;
_topMargin: number;
_rightMargin: number;
_bottomMargin: number;
constructor(
project: gdProject,
@@ -41,25 +46,31 @@ export default class RenderedPanelSpriteInstance extends RenderedInstance {
}
update() {
//TODO
// if (this._pixiObject.visible && this._wasRendered) {
// this._pixiObject.cacheAsBitmap = true;
// }
// this._wasRendered = true;
const panelSprite = gd.asPanelSpriteConfiguration(
this._associatedObjectConfiguration
);
// Change in tiling needs PIXI objects to be recreated.
if (panelSprite.isTiled() !== this._tiled) {
this.makeObjectsAndUpdateTextures();
}
if (panelSprite.getTexture() !== this._textureName) {
// Change in texture or margins needs textures to be recreated.
if (
panelSprite.getTexture() !== this._textureName ||
panelSprite.getLeftMargin() !== this._leftMargin ||
panelSprite.getTopMargin() !== this._topMargin ||
panelSprite.getRightMargin() !== this._rightMargin ||
panelSprite.getBottomMargin() !== this._bottomMargin
) {
this.updateTextures();
}
// Change in position/angle is always applied.
this.updateAngle();
this.updatePosition();
// Handle change in size.
const oldWidth = this._width;
const oldHeight = this._height;
if (this._instance.hasCustomSize()) {
@@ -232,7 +243,6 @@ export default class RenderedPanelSpriteInstance extends RenderedInstance {
0
);
this._wasRendered = true;
this._pixiObject.cacheAsBitmap = false;
}
@@ -240,12 +250,19 @@ export default class RenderedPanelSpriteInstance extends RenderedInstance {
const panelSprite = gd.asPanelSpriteConfiguration(
this._associatedObjectConfiguration
);
// Store the values used for rendering, to detect changes
// that would need later to update the texture again.
this._textureName = panelSprite.getTexture();
this._leftMargin = panelSprite.getLeftMargin();
this._topMargin = panelSprite.getTopMargin();
this._rightMargin = panelSprite.getRightMargin();
this._bottomMargin = panelSprite.getBottomMargin();
const texture = PixiResourcesLoader.getPIXITexture(
this._project,
this._textureName
);
if (!texture.baseTexture.valid) {
// Post pone texture update if texture is not loaded.
texture.once('update', () => this.updateTextures());

View File

@@ -3,7 +3,7 @@ import RenderedInstance from './RenderedInstance';
import PixiResourcesLoader from '../../ObjectsRendering/PixiResourcesLoader';
import ResourcesLoader from '../../ResourcesLoader';
import * as PIXI from 'pixi.js-legacy';
import { rgbToHexNumber } from '../../Utils/ColorTransformer';
import { rgbOrHexToHexNumber } from '../../Utils/ColorTransformer';
const gd: libGDevelop = global.gd;
/**
@@ -70,11 +70,7 @@ export default class RenderedParticleEmitterInstance extends RenderedInstance {
this._pixiObject.beginFill(0, 0);
this._pixiObject.lineStyle(
3,
rgbToHexNumber(
particleEmitterConfiguration.getParticleRed2(),
particleEmitterConfiguration.getParticleGreen2(),
particleEmitterConfiguration.getParticleBlue2()
),
rgbOrHexToHexNumber(particleEmitterConfiguration.getParticleColor2()),
1
);
this._pixiObject.moveTo(0, 0);
@@ -91,11 +87,7 @@ export default class RenderedParticleEmitterInstance extends RenderedInstance {
this._pixiObject.lineStyle(0, 0x000000, 1);
this._pixiObject.beginFill(
rgbToHexNumber(
particleEmitterConfiguration.getParticleRed1(),
particleEmitterConfiguration.getParticleGreen1(),
particleEmitterConfiguration.getParticleBlue1()
)
rgbOrHexToHexNumber(particleEmitterConfiguration.getParticleColor1())
);
this._pixiObject.drawCircle(0, 0, 8);
this._pixiObject.endFill();

View File

@@ -7,6 +7,20 @@ import * as THREE from 'three';
const gd: libGDevelop = global.gd;
let transparentMaterial = null;
const getTransparentMaterial = () => {
if (!transparentMaterial)
transparentMaterial = new THREE.MeshBasicMaterial({
transparent: true,
opacity: 0,
// Set the alpha test to to ensure the faces behind are rendered
// (no "back face culling" that would still be done if alphaTest is not set).
alphaTest: 1,
});
return transparentMaterial;
};
/**
* Renderer for gd.SpriteObject
*/
@@ -50,15 +64,8 @@ export default class RenderedSprite3DInstance extends Rendered3DInstance {
this._pixiContainer.addChild(this._pixiObject);
this.updateSprite();
const material = this._pixiResourcesLoader.getThreeMaterial(
project,
this._sprite ? this._sprite.getImageName() : '',
{
useTransparentTexture: true,
}
);
const geometry = new THREE.PlaneGeometry(1, -1);
const threeObject = new THREE.Mesh(geometry, material);
const threeObject = new THREE.Mesh(geometry, getTransparentMaterial());
threeObject.rotation.order = 'ZYX';
this._threeGroup.add(threeObject);
this._threeObject = threeObject;
@@ -177,11 +184,13 @@ export default class RenderedSprite3DInstance extends Rendered3DInstance {
return true;
}
updateTextureAndSprite(): void {
async updateTextureAndSprite(): Promise<void> {
this.updateSprite();
const sprite = this._sprite;
if (!sprite) return;
// Note that `getPIXITexture` could be refactored to return a promise
// to make it nicer to use (no need to use "once('update')" pattern).
const texture = this._pixiResourcesLoader.getPIXITexture(
this._project,
sprite.getImageName()
@@ -196,7 +205,7 @@ export default class RenderedSprite3DInstance extends Rendered3DInstance {
this._textureWidth = texture.width;
this._textureHeight = texture.height;
const material = this._pixiResourcesLoader.getThreeMaterial(
const material = await this._pixiResourcesLoader.getThreeMaterial(
this._project,
sprite.getImageName(),
{

View File

@@ -158,13 +158,11 @@ const createField = (
const extraInfos = property.getExtraInfo().toJSArray();
// $FlowFixMe - assume the passed resource kind is always valid.
const kind: ResourceKind = extraInfos[0] || '';
// $FlowFixMe - assume the passed resource kind is always valid.
const fallbackKind: ResourceKind = extraInfos[1] || '';
return {
name,
valueType: 'resource',
resourceKind: kind,
fallbackResourceKind: fallbackKind,
getValue: (instance: Instance): string => {
return getProperties(instance)
.get(name)

View File

@@ -1,6 +1,5 @@
// @flow
import { Trans } from '@lingui/macro';
import { type I18n as I18nType } from '@lingui/core';
import * as React from 'react';
import SemiControlledTextField from '../UI/SemiControlledTextField';
import InlineCheckbox from '../UI/InlineCheckbox';
@@ -13,11 +12,7 @@ import { MarkdownText } from '../UI/MarkdownText';
import { rgbOrHexToRGBString } from '../Utils/ColorTransformer';
import FormHelperText from '@material-ui/core/FormHelperText';
import InputAdornment from '@material-ui/core/InputAdornment';
import { type MenuItemTemplate } from '../UI/Menu/Menu.flow';
import {
type ResourceKind,
type ResourceManagementProps,
} from '../ResourcesList/ResourceSource';
import { type ResourceManagementProps } from '../ResourcesList/ResourceSource';
import {
TextFieldWithButtonLayout,
ResponsiveLineStackLayout,
@@ -27,115 +22,34 @@ import RaisedButton from '../UI/RaisedButton';
import UnsavedChangesContext, {
type UnsavedChanges,
} from '../MainFrame/UnsavedChangesContext';
import { Column, Line, Spacer } from '../UI/Grid';
import { Column, Line } from '../UI/Grid';
import Text from '../UI/Text';
import useForceUpdate from '../Utils/UseForceUpdate';
import RaisedButtonWithSplitMenu from '../UI/RaisedButtonWithSplitMenu';
import Tooltip from '@material-ui/core/Tooltip';
import Edit from '../UI/CustomSvgIcons/Edit';
import {
type Schema,
type ValueField,
type ActionButton,
type SectionTitle,
type ResourceField,
} from '../CompactPropertiesEditor';
// Re-export the types.
export type {
Schema,
ValueField,
ActionButton,
SectionTitle,
ResourceField,
Field,
} from '../CompactPropertiesEditor';
// An "instance" here is the objects for which properties are shown
export type Instance = Object; // This could be improved using generics.
export type Instances = Array<Instance>;
// "Value" fields are fields displayed in the properties.
export type ValueFieldCommonProperties = {|
name: string,
getLabel?: Instance => string,
getDescription?: Instance => string,
getExtraDescription?: Instance => string,
disabled?: boolean | ((instances: Array<gdInitialInstance>) => boolean),
onEditButtonBuildMenuTemplate?: (i18n: I18nType) => Array<MenuItemTemplate>,
onEditButtonClick?: () => void,
|};
// "Primitive" value fields are "simple" fields.
export type PrimitiveValueField =
| {|
valueType: 'number',
getValue: Instance => number,
setValue: (instance: Instance, newValue: number) => void,
getEndAdornment?: Instance => {|
label: string,
tooltipContent: React.Node,
|},
...ValueFieldCommonProperties,
|}
| {|
valueType: 'string',
getValue: Instance => string,
setValue: (instance: Instance, newValue: string) => void,
getChoices?: ?() => Array<{|
value: string,
label: string,
labelIsUserDefined?: boolean,
|}>,
isHiddenWhenOnlyOneChoice?: boolean,
...ValueFieldCommonProperties,
|}
| {|
valueType: 'boolean',
getValue: Instance => boolean,
setValue: (instance: Instance, newValue: boolean) => void,
...ValueFieldCommonProperties,
|}
| {|
valueType: 'color',
getValue: Instance => string,
setValue: (instance: Instance, newValue: string) => void,
...ValueFieldCommonProperties,
|}
| {|
valueType: 'textarea',
getValue: Instance => string,
setValue: (instance: Instance, newValue: string) => void,
...ValueFieldCommonProperties,
|};
// "Resource" fields are showing a resource selector.
type ResourceField = {|
valueType: 'resource',
resourceKind: ResourceKind,
fallbackResourceKind?: ResourceKind,
getValue: Instance => string,
setValue: (instance: Instance, newValue: string) => void,
...ValueFieldCommonProperties,
|};
type SectionTitle = {|
name: string,
getValue?: Instance => string,
nonFieldType: 'sectionTitle',
defaultValue?: string,
|};
type ActionButton = {|
label: string,
disabled: 'onValuesDifferent',
getValue: Instance => string,
nonFieldType: 'button',
onClick: (instance: Instance) => void,
|};
// A value field is a primitive or a resource.
export type ValueField = PrimitiveValueField | ResourceField;
// A field can be a primitive, a resource or a list of fields
export type Field =
| PrimitiveValueField
| ResourceField
| SectionTitle
| ActionButton
| {|
name: string,
type: 'row' | 'column',
title?: ?string,
children: Array<Field>,
|};
// The schema is the tree of all fields.
export type Schema = Array<Field>;
type Props = {|
onInstancesModified?: Instances => void,
instances: Instances,
@@ -556,7 +470,6 @@ const PropertiesEditor = ({
project={project}
resourceManagementProps={resourceManagementProps}
resourceKind={field.resourceKind}
fallbackResourceKind={field.fallbackResourceKind}
resourceName={getFieldValue({
instances,
field,
@@ -583,43 +496,13 @@ const PropertiesEditor = ({
<ColumnStackLayout noMargin>{fields}</ColumnStackLayout>
);
const renderSectionTitle = React.useCallback(
(field: SectionTitle) => {
const { getValue } = field;
let additionalText = null;
if (getValue) {
let selectedInstancesValue = getFieldValue({
instances,
field,
defaultValue: field.defaultValue || 'Multiple Values',
});
if (!!selectedInstancesValue) additionalText = selectedInstancesValue;
}
if (!!additionalText) {
return (
<Line alignItems="baseline" key={`section-title-${field.name}`}>
<Text displayInlineAsSpan>{field.name}</Text>
<Spacer />
<Text
allowSelection
displayInlineAsSpan
size="body2"
>{`- ${additionalText}`}</Text>
</Line>
);
}
return (
<Line key={`section-title-${field.name}`}>
<Text displayInlineAsSpan>{field.name}</Text>
</Line>
);
},
[instances]
);
const renderSectionTitle = React.useCallback((field: SectionTitle) => {
return (
<Line key={`section-title-${field.name}`}>
<Text displayInlineAsSpan>{field.name}</Text>
</Line>
);
}, []);
return renderContainer(
schema.map(field => {

View File

@@ -89,7 +89,7 @@ const ResourcePropertiesEditor = React.forwardRef<
{
name: 'Resource name',
valueType: 'string',
disabled: true,
disabled: () => true,
getValue: (resource: gdResource) => resource.getName(),
setValue: (resource: gdResource, newValue: string) =>
resource.setName(newValue),

View File

@@ -222,6 +222,9 @@ export const CompactResourceSelectorWithThumbnail = ({
onlyForStorageProvider === storageProvider.internalName
);
const isResourceSetButInvalid =
resourceName && !project.getResourcesManager().hasResource(resourceName);
return (
<LineStackLayout noMargin expand id={idToUse.current}>
{displayThumbnail && (
@@ -237,7 +240,7 @@ export const CompactResourceSelectorWithThumbnail = ({
className={classNames({
[classes.container]: true,
[classes.disabled]: false,
[classes.errored]: false,
[classes.errored]: isResourceSetButInvalid,
})}
>
<div
@@ -245,7 +248,12 @@ export const CompactResourceSelectorWithThumbnail = ({
[classes.compactResourceSelector]: true,
})}
>
<input type="text" value={resourceName} />
<input
type="text"
spellCheck={false}
value={resourceName}
onChange={e => onChange(e.currentTarget.value)}
/>
</div>
</div>
<ElementWithMenu

View File

@@ -46,6 +46,7 @@ type Props = {|
const ImageThumbnail = (props: Props) => {
const { onContextMenu, resourcesLoader, resourceName, project } = props;
const theme = React.useContext(GDevelopThemeContext);
const [error, setError] = React.useState(false);
// Allow a long press to show the context menu
const longTouchForContextMenuProps = useLongTouch(
@@ -60,6 +61,8 @@ const ImageThumbnail = (props: Props) => {
const normalBorderColor = theme.imagePreview.borderColor;
const borderColor = props.selected
? theme.palette.secondary
: !!error
? theme.message.error
: normalBorderColor;
const containerStyle = {
@@ -87,9 +90,16 @@ const ImageThumbnail = (props: Props) => {
...styles.spriteThumbnailImage,
maxWidth: props.size || 100,
maxHeight: props.size || 100,
display: error ? 'none' : undefined,
}}
alt={resourceName}
src={resourcesLoader.getResourceFullUrl(project, resourceName, {})}
onError={error => {
setError(error);
}}
onLoad={() => {
setError(false);
}}
/>
{props.selectable && (
<div style={styles.checkboxContainer}>

View File

@@ -30,6 +30,7 @@ export type SceneEditorsDisplayProps = {|
objectsContainer: gdObjectsContainer,
projectScopedContainersAccessor: ProjectScopedContainersAccessor,
initialInstances: gdInitialInstancesContainer,
lastSelectionType: 'instance' | 'object',
instancesSelection: InstancesSelection,
selectedLayer: string,
onSelectInstances: (
@@ -39,7 +40,8 @@ export type SceneEditorsDisplayProps = {|
) => void,
editInstanceVariables: (instance: ?gdInitialInstance) => void,
editObjectByName: (objectName: string, initialTab?: ObjectEditorTab) => void,
onEditObject: gdObject => void,
editObjectInPropertiesPanel: (objectName: string) => void,
onEditObject: (object: gdObject, initialTab: ?ObjectEditorTab) => void,
selectedObjectFolderOrObjectsWithContext: ObjectFolderOrObjectWithContext[],
onSelectLayer: (layerName: string) => void,
editLayerEffects: (layer: ?gdLayer) => void,
@@ -122,7 +124,7 @@ export type SceneEditorsDisplayProps = {|
export type SceneEditorsDisplayInterface = {|
getName: () => 'mosaic' | 'swipeableDrawer',
forceUpdateInstancesList: () => void,
forceUpdateInstancesPropertiesEditor: () => void,
forceUpdatePropertiesEditor: () => void,
forceUpdateObjectsList: () => void,
forceUpdateObjectGroupsList: () => void,
scrollObjectGroupsListToObjectGroup: (objectGroup: gdObjectGroup) => void,

View File

@@ -0,0 +1,121 @@
// @flow
import * as React from 'react';
import { type I18n as I18nType } from '@lingui/core';
import Paper from '../UI/Paper';
import EmptyMessage from '../UI/EmptyMessage';
import useForceUpdate from '../Utils/UseForceUpdate';
import { CompactInstancePropertiesEditor } from '../InstancesEditor/CompactInstancePropertiesEditor';
import { Trans } from '@lingui/macro';
import { ProjectScopedContainersAccessor } from '../InstructionOrExpression/EventsScope';
import { type UnsavedChanges } from '../MainFrame/UnsavedChangesContext';
import { type HistoryHandler } from '../VariablesList/VariablesList';
import { type TileMapTileSelection } from '../InstancesEditor/TileSetVisualizer';
import { CompactObjectPropertiesEditor } from '../ObjectEditor/CompactObjectPropertiesEditor';
import { type ObjectEditorTab } from '../ObjectEditor/ObjectEditorDialog';
import { type ResourceManagementProps } from '../ResourcesList/ResourceSource';
export const styles = {
paper: {
display: 'flex',
flex: 1,
minWidth: 0,
flexDirection: 'column',
},
};
type Props = {|
project: gdProject,
resourceManagementProps: ResourceManagementProps,
layout?: ?gdLayout,
eventsFunctionsExtension: gdEventsFunctionsExtension | null,
objectsContainer: gdObjectsContainer,
globalObjectsContainer: gdObjectsContainer | null,
layersContainer: gdLayersContainer,
projectScopedContainersAccessor: ProjectScopedContainersAccessor,
unsavedChanges?: ?UnsavedChanges,
i18n: I18nType,
historyHandler?: HistoryHandler,
lastSelectionType: 'instance' | 'object',
// For objects:
objects: Array<gdObject>,
onEditObject: (object: gdObject, initialTab: ?ObjectEditorTab) => void,
onUpdateBehaviorsSharedData: () => void,
// For instances:
instances: Array<gdInitialInstance>,
editObjectInPropertiesPanel: (objectName: string) => void,
onInstancesModified?: (Array<gdInitialInstance>) => void,
onGetInstanceSize: gdInitialInstance => [number, number, number],
editInstanceVariables: gdInitialInstance => void,
tileMapTileSelection: ?TileMapTileSelection,
onSelectTileMapTile: (?TileMapTileSelection) => void,
|};
export type InstanceOrObjectPropertiesEditorInterface = {|
forceUpdate: () => void,
|};
export const InstanceOrObjectPropertiesEditorContainer = React.forwardRef<
Props,
InstanceOrObjectPropertiesEditorInterface
>((props, ref) => {
const forceUpdate = useForceUpdate();
React.useImperativeHandle(ref, () => ({
forceUpdate,
}));
const {
lastSelectionType,
// For objects:
objects,
onEditObject,
resourceManagementProps,
eventsFunctionsExtension,
onUpdateBehaviorsSharedData,
// For instances:
instances,
editObjectInPropertiesPanel,
onInstancesModified,
onGetInstanceSize,
editInstanceVariables,
tileMapTileSelection,
onSelectTileMapTile,
...commonProps
} = props;
return (
<Paper background="dark" square style={styles.paper}>
{!!instances.length && lastSelectionType === 'instance' ? (
<CompactInstancePropertiesEditor
instances={instances}
editObjectInPropertiesPanel={editObjectInPropertiesPanel}
onInstancesModified={onInstancesModified}
onGetInstanceSize={onGetInstanceSize}
editInstanceVariables={editInstanceVariables}
tileMapTileSelection={tileMapTileSelection}
onSelectTileMapTile={onSelectTileMapTile}
{...commonProps}
/>
) : !!objects.length && lastSelectionType === 'object' ? (
<CompactObjectPropertiesEditor
objects={objects}
onEditObject={onEditObject}
resourceManagementProps={resourceManagementProps}
eventsFunctionsExtension={eventsFunctionsExtension}
onUpdateBehaviorsSharedData={onUpdateBehaviorsSharedData}
{...commonProps}
/>
) : (
<EmptyMessage>
<Trans>
Click on an instance on the canvas or an object in the list to
display their properties.
</Trans>
</EmptyMessage>
)}
</Paper>
);
});

View File

@@ -28,9 +28,10 @@ import {
type SceneEditorsDisplayProps,
type SceneEditorsDisplayInterface,
} from '../EditorsDisplay.flow';
import CompactInstancePropertiesEditorContainer, {
type CompactInstancePropertiesEditorInterface,
} from '../../InstancesEditor/CompactInstancePropertiesEditor';
import {
InstanceOrObjectPropertiesEditorContainer,
type InstanceOrObjectPropertiesEditorInterface,
} from '../../SceneEditor/InstanceOrObjectPropertiesEditorContainer';
const initialMosaicEditorNodes = {
direction: 'row',
@@ -81,9 +82,11 @@ const MosaicEditorsDisplay = React.forwardRef<
>((props, ref) => {
const {
project,
resourceManagementProps,
layout,
eventsFunctionsExtension,
eventsBasedObject,
updateBehaviorsSharedData,
layersContainer,
globalObjectsContainer,
objectsContainer,
@@ -99,7 +102,7 @@ const MosaicEditorsDisplay = React.forwardRef<
} = React.useContext(PreferencesContext);
const selectedInstances = props.instancesSelection.getSelectedInstances();
const instancesPropertiesEditorRef = React.useRef<?CompactInstancePropertiesEditorInterface>(
const instanceOrObjectPropertiesEditorRef = React.useRef<?InstanceOrObjectPropertiesEditorInterface>(
null
);
const layersListRef = React.useRef<?LayersListInterface>(null);
@@ -109,9 +112,10 @@ const MosaicEditorsDisplay = React.forwardRef<
const editorMosaicRef = React.useRef<?EditorMosaicInterface>(null);
const objectGroupsListRef = React.useRef<?ObjectGroupsListInterface>(null);
const forceUpdateInstancesPropertiesEditor = React.useCallback(() => {
if (instancesPropertiesEditorRef.current)
instancesPropertiesEditorRef.current.forceUpdate();
// TODO: rename
const forceUpdatePropertiesEditor = React.useCallback(() => {
if (instanceOrObjectPropertiesEditorRef.current)
instanceOrObjectPropertiesEditorRef.current.forceUpdate();
}, []);
const forceUpdateInstancesList = React.useCallback(() => {
if (instancesListRef.current) instancesListRef.current.forceUpdate();
@@ -170,7 +174,7 @@ const MosaicEditorsDisplay = React.forwardRef<
return {
getName: () => 'mosaic',
forceUpdateInstancesList,
forceUpdateInstancesPropertiesEditor,
forceUpdatePropertiesEditor,
forceUpdateObjectsList,
forceUpdateObjectGroupsList,
scrollObjectGroupsListToObjectGroup,
@@ -220,26 +224,22 @@ const MosaicEditorsDisplay = React.forwardRef<
(instances: Array<gdInitialInstance>, multiSelect: boolean) => {
onSelectInstances(instances, multiSelect);
forceUpdateInstancesList();
forceUpdateInstancesPropertiesEditor();
forceUpdatePropertiesEditor();
},
[
forceUpdateInstancesList,
forceUpdateInstancesPropertiesEditor,
onSelectInstances,
]
[forceUpdateInstancesList, forceUpdatePropertiesEditor, onSelectInstances]
);
const selectedObjectNames = props.selectedObjectFolderOrObjectsWithContext
const selectedObjects = props.selectedObjectFolderOrObjectsWithContext
.map(objectFolderOrObjectWithContext => {
const { objectFolderOrObject } = objectFolderOrObjectWithContext;
if (!objectFolderOrObject) return null; // Protect ourselves from an unexpected null value.
if (objectFolderOrObject.isFolder()) return null;
return objectFolderOrObject.getObject().getName();
return objectFolderOrObject.getObject();
})
.filter(Boolean);
const selectedObjectNames = selectedObjects.map(object => object.getName());
const editors = {
properties: {
type: 'secondary',
@@ -247,24 +247,30 @@ const MosaicEditorsDisplay = React.forwardRef<
renderEditor: () => (
<I18n>
{({ i18n }) => (
<CompactInstancePropertiesEditorContainer
<InstanceOrObjectPropertiesEditorContainer
i18n={i18n}
project={project}
resourceManagementProps={resourceManagementProps}
layout={layout}
eventsFunctionsExtension={eventsFunctionsExtension}
onUpdateBehaviorsSharedData={updateBehaviorsSharedData}
objectsContainer={objectsContainer}
globalObjectsContainer={globalObjectsContainer}
layersContainer={layersContainer}
projectScopedContainersAccessor={projectScopedContainersAccessor}
instances={selectedInstances}
objects={selectedObjects}
editInstanceVariables={props.editInstanceVariables}
onEditObjectByName={props.editObjectByName}
editObjectInPropertiesPanel={props.editObjectInPropertiesPanel}
onEditObject={props.onEditObject}
onInstancesModified={forceUpdateInstancesList}
onGetInstanceSize={getInstanceSize}
ref={instancesPropertiesEditorRef}
ref={instanceOrObjectPropertiesEditorRef}
unsavedChanges={props.unsavedChanges}
historyHandler={props.historyHandler}
tileMapTileSelection={props.tileMapTileSelection}
onSelectTileMapTile={props.onSelectTileMapTile}
lastSelectionType={props.lastSelectionType}
/>
)}
</I18n>
@@ -285,7 +291,7 @@ const MosaicEditorsDisplay = React.forwardRef<
onEditLayer={props.editLayer}
onRemoveLayer={props.onRemoveLayer}
onLayerRenamed={props.onLayerRenamed}
onCreateLayer={forceUpdateInstancesPropertiesEditor}
onCreateLayer={forceUpdatePropertiesEditor}
layersContainer={layersContainer}
unsavedChanges={props.unsavedChanges}
ref={layersListRef}

View File

@@ -31,3 +31,43 @@ export const cleanNonExistingObjectFolderOrObjectWithContexts = (
)
);
};
export const getObjectFolderOrObjectWithContextFromObjectName = (
globalObjectsContainer: ?gdObjectsContainer,
objectsContainer: gdObjectsContainer,
objectName: string
): ObjectFolderOrObjectWithContext | null => {
let foundObjectFolderObjectWithContext = null;
if (globalObjectsContainer)
mapVector(
globalObjectsContainer.getAllObjectFolderOrObjects(),
objectFolderOrObject => {
if (
!objectFolderOrObject.isFolder() &&
objectFolderOrObject.getObject().getName() === objectName
) {
foundObjectFolderObjectWithContext = {
objectFolderOrObject,
global: true,
};
}
}
);
if (objectsContainer)
mapVector(
objectsContainer.getAllObjectFolderOrObjects(),
objectFolderOrObject => {
if (
!objectFolderOrObject.isFolder() &&
objectFolderOrObject.getObject().getName() === objectName
) {
foundObjectFolderObjectWithContext = {
objectFolderOrObject,
global: false,
};
}
}
);
return foundObjectFolderObjectWithContext;
};

View File

@@ -28,9 +28,10 @@ import {
type SceneEditorsDisplayProps,
} from '../EditorsDisplay.flow';
import ErrorBoundary from '../../UI/ErrorBoundary';
import CompactInstancePropertiesEditorContainer, {
type CompactInstancePropertiesEditorInterface,
} from '../../InstancesEditor/CompactInstancePropertiesEditor';
import {
InstanceOrObjectPropertiesEditorContainer,
type InstanceOrObjectPropertiesEditorInterface,
} from '../InstanceOrObjectPropertiesEditorContainer';
export const swipeableDrawerContainerId = 'swipeable-drawer-container';
@@ -57,9 +58,11 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef<
>((props, ref) => {
const {
project,
resourceManagementProps,
layout,
eventsFunctionsExtension,
eventsBasedObject,
updateBehaviorsSharedData,
layersContainer,
globalObjectsContainer,
objectsContainer,
@@ -72,7 +75,7 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef<
const { values } = React.useContext(PreferencesContext);
const screenType = useScreenType();
const instancesPropertiesEditorRef = React.useRef<?CompactInstancePropertiesEditorInterface>(
const instanceOrObjectPropertiesEditorRef = React.useRef<?InstanceOrObjectPropertiesEditorInterface>(
null
);
const layersListRef = React.useRef<?LayersListInterface>(null);
@@ -105,9 +108,9 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef<
[selectedEditorId, drawerOpeningState]
);
const forceUpdateInstancesPropertiesEditor = React.useCallback(() => {
if (instancesPropertiesEditorRef.current)
instancesPropertiesEditorRef.current.forceUpdate();
const forceUpdatePropertiesEditor = React.useCallback(() => {
if (instanceOrObjectPropertiesEditorRef.current)
instanceOrObjectPropertiesEditorRef.current.forceUpdate();
}, []);
const forceUpdateInstancesList = React.useCallback(() => {
if (instancesListRef.current) instancesListRef.current.forceUpdate();
@@ -159,7 +162,7 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef<
return {
getName: () => 'swipeableDrawer',
forceUpdateInstancesList,
forceUpdateInstancesPropertiesEditor,
forceUpdatePropertiesEditor,
forceUpdateObjectsList,
forceUpdateObjectGroupsList,
scrollObjectGroupsListToObjectGroup,
@@ -209,26 +212,22 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef<
(instances: Array<gdInitialInstance>, multiSelect: boolean) => {
onSelectInstances(instances, multiSelect, 'upperCenter');
forceUpdateInstancesList();
forceUpdateInstancesPropertiesEditor();
forceUpdatePropertiesEditor();
},
[
forceUpdateInstancesList,
forceUpdateInstancesPropertiesEditor,
onSelectInstances,
]
[forceUpdateInstancesList, forceUpdatePropertiesEditor, onSelectInstances]
);
const selectedObjectNames = props.selectedObjectFolderOrObjectsWithContext
const selectedObjects = props.selectedObjectFolderOrObjectsWithContext
.map(objectFolderOrObjectWithContext => {
const { objectFolderOrObject } = objectFolderOrObjectWithContext;
if (!objectFolderOrObject) return null; // Protect ourselves from an unexpected null value.
if (objectFolderOrObject.isFolder()) return null;
return objectFolderOrObject.getObject().getName();
return objectFolderOrObject.getObject();
})
.filter(Boolean);
const selectedObjectNames = selectedObjects.map(object => object.getName());
return (
<FullSizeMeasurer>
{({ width, height }) => (
@@ -269,6 +268,7 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef<
}
pauseRendering={!props.isActive}
showObjectInstancesIn3D={values.use3DEditor}
showBasicProfilingCounters={values.showBasicProfilingCounters}
tileMapTileSelection={props.tileMapTileSelection}
onSelectTileMapTile={props.onSelectTileMapTile}
/>
@@ -341,26 +341,34 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef<
{selectedEditorId === 'properties' && (
<I18n>
{({ i18n }) => (
<CompactInstancePropertiesEditorContainer
<InstanceOrObjectPropertiesEditorContainer
i18n={i18n}
project={project}
resourceManagementProps={resourceManagementProps}
layout={layout}
eventsFunctionsExtension={eventsFunctionsExtension}
onUpdateBehaviorsSharedData={updateBehaviorsSharedData}
objectsContainer={objectsContainer}
globalObjectsContainer={globalObjectsContainer}
layersContainer={layersContainer}
projectScopedContainersAccessor={
projectScopedContainersAccessor
}
objects={selectedObjects}
instances={selectedInstances}
editInstanceVariables={props.editInstanceVariables}
onEditObjectByName={props.editObjectByName}
editObjectInPropertiesPanel={
props.editObjectInPropertiesPanel
}
onEditObject={props.onEditObject}
onInstancesModified={forceUpdateInstancesList}
onGetInstanceSize={getInstanceSize}
ref={instancesPropertiesEditorRef}
ref={instanceOrObjectPropertiesEditorRef}
unsavedChanges={props.unsavedChanges}
historyHandler={props.historyHandler}
tileMapTileSelection={props.tileMapTileSelection}
onSelectTileMapTile={props.onSelectTileMapTile}
lastSelectionType={props.lastSelectionType}
/>
)}
</I18n>
@@ -420,7 +428,7 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef<
onEditLayer={props.editLayer}
onRemoveLayer={props.onRemoveLayer}
onLayerRenamed={props.onLayerRenamed}
onCreateLayer={forceUpdateInstancesPropertiesEditor}
onCreateLayer={forceUpdatePropertiesEditor}
layersContainer={layersContainer}
unsavedChanges={props.unsavedChanges}
ref={layersListRef}

View File

@@ -67,7 +67,10 @@ import {
type ObjectFolderOrObjectWithContext,
} from '../ObjectsList/EnumerateObjectFolderOrObject';
import uniq from 'lodash/uniq';
import { cleanNonExistingObjectFolderOrObjectWithContexts } from './ObjectFolderOrObjectsSelection';
import {
cleanNonExistingObjectFolderOrObjectWithContexts,
getObjectFolderOrObjectWithContextFromObjectName,
} from './ObjectFolderOrObjectsSelection';
import {
registerOnResourceExternallyChangedCallback,
unregisterOnResourceExternallyChangedCallback,
@@ -111,7 +114,6 @@ type Props = {|
getInitialInstancesEditorSettings: () => InstancesEditorSettings,
onEditObject?: ?(object: gdObject) => void,
onOpenMoreSettings?: ?() => void,
onOpenEvents: (sceneName: string) => void,
onObjectEdited: (objectWithContext: ObjectWithContext) => void,
@@ -161,6 +163,8 @@ type State = {|
selectedLayer: string,
tileMapTileSelection: ?TileMapTileSelection,
lastSelectionType: 'instance' | 'object',
|};
type CopyCutPasteOptions = {|
@@ -215,6 +219,8 @@ export default class SceneEditor extends React.Component<Props, State> {
selectedObjectFolderOrObjectsWithContext: [],
selectedLayer: BASE_LAYER_NAME,
invisibleLayerOnWhichInstancesHaveJustBeenAdded: null,
lastSelectionType: 'instance',
};
}
@@ -499,6 +505,22 @@ export default class SceneEditor extends React.Component<Props, State> {
this.editObject(globalObjectsContainer.getObject(objectName), initialTab);
};
editObjectInPropertiesPanel = (objectName: string) => {
const objectFolderOrObjectWithContext = getObjectFolderOrObjectWithContextFromObjectName(
this.props.globalObjectsContainer,
this.props.objectsContainer,
objectName
);
if (!objectFolderOrObjectWithContext) return;
this.setState({
selectedObjectFolderOrObjectsWithContext: [
objectFolderOrObjectWithContext,
],
lastSelectionType: 'object',
});
};
_editObjectGroup = (group: ?gdObjectGroup) => {
this.setState({ editedGroup: group, isCreatingNewGroup: false });
};
@@ -583,6 +605,7 @@ export default class SceneEditor extends React.Component<Props, State> {
this.setState(
{
lastSelectionType: 'object',
selectedObjectFolderOrObjectsWithContext,
},
() => {
@@ -686,7 +709,10 @@ export default class SceneEditor extends React.Component<Props, State> {
_onInstancesSelected = (instances: Array<gdInitialInstance>) => {
if (instances.length === 0) {
this.setState(
{ selectedObjectFolderOrObjectsWithContext: [] },
{
lastSelectionType: 'instance',
selectedObjectFolderOrObjectsWithContext: [],
},
this.updateToolbar
);
return;
@@ -702,6 +728,7 @@ export default class SceneEditor extends React.Component<Props, State> {
) {
this.setState(
{
lastSelectionType: 'instance',
selectedObjectFolderOrObjectsWithContext: [
{
objectFolderOrObject: globalObjectsContainer
@@ -716,6 +743,7 @@ export default class SceneEditor extends React.Component<Props, State> {
} else if (objectsContainer.hasObjectNamed(objectName)) {
this.setState(
{
lastSelectionType: 'instance',
selectedObjectFolderOrObjectsWithContext: [
{
objectFolderOrObject: objectsContainer
@@ -803,6 +831,7 @@ export default class SceneEditor extends React.Component<Props, State> {
viewControls.centerViewOnLastInstance(instances, offset);
}
this.setState({ lastSelectionType: 'instance' });
this.updateToolbar();
};
@@ -1701,8 +1730,7 @@ export default class SceneEditor extends React.Component<Props, State> {
};
forceUpdatePropertiesEditor = () => {
if (this.editorDisplay)
this.editorDisplay.forceUpdateInstancesPropertiesEditor();
if (this.editorDisplay) this.editorDisplay.forceUpdatePropertiesEditor();
};
forceUpdateCustomObjectRenderedInstances = () => {
@@ -1843,7 +1871,7 @@ export default class SceneEditor extends React.Component<Props, State> {
layersContainer={this.props.layersContainer}
globalObjectsContainer={this.props.globalObjectsContainer}
objectsContainer={this.props.objectsContainer}
onEditObject={this.props.onEditObject || this.editObject}
onEditObject={this.editObject}
onEditObjectVariables={object => {
this.editObject(object, 'variables');
}}
@@ -1873,6 +1901,7 @@ export default class SceneEditor extends React.Component<Props, State> {
editLayerEffects={this.editLayerEffects}
editInstanceVariables={this.editInstanceVariables}
editObjectByName={this.editObjectByName}
editObjectInPropertiesPanel={this.editObjectInPropertiesPanel}
selectedObjectFolderOrObjectsWithContext={
selectedObjectFolderOrObjectsWithContext
}
@@ -1894,7 +1923,7 @@ export default class SceneEditor extends React.Component<Props, State> {
onRenameObjectGroup={this._onRenameObjectGroup}
canObjectOrGroupBeGlobal={this.canObjectOrGroupBeGlobal}
updateBehaviorsSharedData={this.updateBehaviorsSharedData}
onEditObject={this.props.onEditObject || this.editObject}
onEditObject={this.editObject}
onRenameObjectFolderOrObjectWithContextFinish={
this._onRenameObjectFolderOrObjectWithContextFinish
}
@@ -1954,6 +1983,7 @@ export default class SceneEditor extends React.Component<Props, State> {
}
isActive={isActive}
onOpenedEditorsChanged={this.updateToolbar}
lastSelectionType={this.state.lastSelectionType}
/>
<I18n>
{({ i18n }) => (

View File

@@ -0,0 +1,92 @@
.container {
display: flex;
align-items: center;
}
.compactSearchBar {
border-radius: 4px;
color: var(--theme-text-default-color);
background-color: var(--theme-text-field-default-background-color);
transition: box-shadow 0.1s;
position: relative;
display: flex;
flex: 1;
min-width: 0px;
}
.container.disabled .compactSearchBar {
color: var(--theme-text-field-disabled-color);
}
.container.errored:not(.disabled) .compactSearchBar {
border: none;
outline: 1px solid var(--theme-text-field-default-error);
}
.container.errored:not(.disabled):hover .compactSearchBar {
outline: 1px solid var(--theme-text-field-active-error);
}
.container.errored:not(.disabled):focus-within .compactSearchBar {
outline: 1px solid var(--theme-text-field-active-error);
}
.compactSearchBar::before {
position: absolute;
left: 0;
right: 0;
bottom: 0;
top: 0;
content: '';
border-radius: 4px;
pointer-events: none;
}
.container:not(.disabled):not(.errored):hover .compactSearchBar::before {
border-bottom: 1px solid var(--theme-text-field-hover-border-color);
}
.container:not(.disabled):not(.errored):focus-within .compactSearchBar::before {
border-bottom: 1px solid var(--theme-text-field-active-border-color);
}
.compactSearchBar input {
outline: none;
border: none;
padding: 2px 8px;
padding-left: 22px;
background-image: none;
background-color: transparent;
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
font-size: 14px;
line-height: 20px;
font-family: var(--gdevelop-modern-font-family);
color: inherit;
flex: 1;
caret-color: var(--theme-text-field-active-caret-color);
min-width: 0px;
border-radius: inherit; /* Needed for InAppTutorialElementHighlighter to adapt its border-radius to the input container */
}
.compactSearchBar input::placeholder {
color: var(--theme-text-field-placeholder-color);
}
.searchIconContainer {
outline: 0;
border: 0;
position: absolute;
left: 3px;
top: 3px;
width: 17px;
height: 17px;
display: flex;
padding: 1px;
border-radius: 4px;
background-color: unset;
}
/* svg tag is needed to be first priority compared to Material UI Custom SVG icon classes*/
svg.searchIcon {
font-size: 15px;
color: var(--theme-text-field-placeholder-color);
transition: color 0.1s linear;
}

View File

@@ -0,0 +1,64 @@
// @flow
import * as React from 'react';
import { I18n } from '@lingui/react';
import classNames from 'classnames';
import classes from './CompactSearchBar.module.css';
import { makeTimestampedId } from '../../Utils/TimestampedId';
import Search from '../CustomSvgIcons/Search';
import { type MessageDescriptor } from '../../Utils/i18n/MessageDescriptor.flow';
import { t } from '@lingui/macro';
export type CompactSearchBarInterface = {|
blur: () => void,
|};
export type CompactSearchBarProps = {|
value: string,
onChange: (newValue: string) => void,
id?: string,
disabled?: boolean,
errored?: boolean,
placeholder?: MessageDescriptor,
|};
const CompactSearchBar = React.forwardRef<
CompactSearchBarProps,
CompactSearchBarInterface
>(({ value, onChange, id, disabled, errored, placeholder }, ref) => {
const idToUse = React.useRef<string>(id || makeTimestampedId());
return (
<I18n>
{({ i18n }) => (
<div
className={classNames({
[classes.container]: true,
[classes.disabled]: disabled,
[classes.errored]: errored,
})}
>
<div
className={classNames({
[classes.compactSearchBar]: true,
})}
>
<div className={classes.searchIconContainer}>
<Search className={classes.searchIcon} />
</div>
<input
id={idToUse.current}
// Type cannot be set to number in order to benefit from the mathematical parsing.
type={'text'}
disabled={disabled}
value={value}
onChange={e => onChange(e.currentTarget.value)}
placeholder={i18n._(placeholder || t`Search`)}
/>
</div>
</div>
)}
</I18n>
);
});
export default CompactSearchBar;

View File

@@ -61,6 +61,7 @@
outline: none;
border: none;
padding: 2px 8px;
padding-right: 22px;
background-image: none;
background-color: transparent;
-webkit-box-shadow: none;

View File

@@ -1,14 +1,7 @@
.container {
display: flex;
flex-direction: column;
flex-direction: row;
flex: 1;
min-width: 0px;
}
.container .error {
font-size: 14px;
line-height: 20px;
font-family: var(--gdevelop-modern-font-family);
color: var(--theme-error-color);
padding: 2px 0px;
gap: 4px;
}

View File

@@ -10,6 +10,9 @@ import {
shouldValidate,
} from '../KeyboardShortcuts/InteractionKeys';
import { calculate } from '../../Utils/MathExpressionParser';
import IconButton from '../IconButton';
import Infinite from '../CustomSvgIcons/Infinite';
import { t } from '@lingui/macro';
const getValueAsFloatIfValid = (valueAsString: string): number | null => {
const valueAsFloat = parseFloat(valueAsString);
@@ -18,6 +21,12 @@ const getValueAsFloatIfValid = (valueAsString: string): number | null => {
return isValueAsFloatValid ? valueAsFloat : null;
};
const styles = {
icon: {
fontSize: 18,
},
};
const updateAndReturnValueAsFloatIfValid = (
valueAsString: string,
valueToAddOrSubtract: number
@@ -41,6 +50,7 @@ type Props = {|
onChange: number => void,
commitOnBlur?: boolean,
disabled?: boolean,
canBeUnlimitedUsingMinus1?: boolean,
errored?: boolean,
placeholder?: string,
renderLeftIcon?: (className: string) => React.Node,
@@ -50,14 +60,13 @@ type Props = {|
onClickEndAdornment?: () => void,
getValueFromDisplayedValue?: string => string,
getDisplayedValueFromValue?: string => string,
errorText?: React.Node,
|};
const CompactSemiControlledNumberField = ({
value,
onChange,
errorText,
placeholder,
canBeUnlimitedUsingMinus1,
commitOnBlur,
getValueFromDisplayedValue,
getDisplayedValueFromValue,
@@ -145,12 +154,16 @@ const CompactSemiControlledNumberField = ({
? getDisplayedValueFromValue(value.toString())
: value.toString();
const isUnlimited = canBeUnlimitedUsingMinus1 && value === -1;
return (
<div className={classes.container}>
<CompactTextField
type="text"
ref={textFieldRef}
value={focused ? temporaryValue : stringValue}
disabled={isUnlimited}
placeholder={isUnlimited ? 'Unlimited' : placeholder}
value={isUnlimited ? '' : focused ? temporaryValue : stringValue}
onChange={onChangeValue}
onFocus={event => {
setFocused(true);
@@ -225,7 +238,23 @@ const CompactSemiControlledNumberField = ({
}}
{...otherProps}
/>
{errorText && <div className={classes.error}>{errorText}</div>}
{canBeUnlimitedUsingMinus1 && (
<IconButton
size="small"
onClick={() => {
if (isUnlimited) {
onChange(0);
if (textFieldRef.current) textFieldRef.current.focus();
} else {
onChange(-1);
}
}}
selected={isUnlimited}
tooltip={isUnlimited ? t`Remove unlimited` : t`Set to unlimited`}
>
<Infinite style={styles.icon} />
</IconButton>
)}
</div>
);
};

View File

@@ -1,6 +1,8 @@
.container {
display: flex;
align-items: center;
flex: 1;
min-width: 0;
}
.compactTextField {

View File

@@ -37,6 +37,7 @@ type OtherProps = {|
export type CompactTextFieldInterface = {|
blur: () => void,
focus: () => void,
|};
export type CompactTextFieldProps = {|
@@ -106,6 +107,9 @@ const CompactTextField = React.forwardRef<
blur: () => {
if (inputRef.current) inputRef.current.blur();
},
focus: () => {
if (inputRef.current) inputRef.current.focus();
},
}));
const onWheelIfFocused = React.useCallback(

View File

@@ -1,8 +1,8 @@
.container {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.fullWidth {

View File

@@ -1,9 +1,24 @@
// @flow
import * as React from 'react';
import Tooltip from '@material-ui/core/Tooltip';
import Text from '../../UI/Text';
import { MarkdownText } from '../../UI/MarkdownText';
import { tooltipEnterDelay } from '../../UI/Tooltip';
import classes from './CompactToggleField.module.css';
import classNames from 'classnames';
const styles = {
label: {
overflow: 'hidden',
textOverflow: 'ellipsis',
lineHeight: '17px',
maxHeight: 34, // 2 * lineHeight to limit to 2 lines.
opacity: 0.7,
},
};
type Props = {|
label: string,
markdownDescription?: ?string,
id?: string,
checked: boolean,
onCheck: (newValue: boolean) => void,
@@ -12,6 +27,9 @@ type Props = {|
|};
export const CompactToggleField = (props: Props) => {
const title = !props.markdownDescription
? props.label
: [props.label, ' - ', <MarkdownText source={props.markdownDescription} />];
return (
<label
className={classNames({
@@ -20,16 +38,10 @@ export const CompactToggleField = (props: Props) => {
})}
id={props.id}
>
<div
className={classNames({
[classes.toggleSwitch]: true,
})}
>
<div className={classes.toggleSwitch}>
<input
type="checkbox"
className={classNames({
[classes.checkbox]: true,
})}
className={classes.checkbox}
onChange={() => props.onCheck(!props.checked)}
disabled={props.disabled}
/>
@@ -57,6 +69,27 @@ export const CompactToggleField = (props: Props) => {
</span>
</span>
</div>
<Tooltip
title={title}
enterDelay={tooltipEnterDelay}
placement="bottom"
PopperProps={{
modifiers: {
offset: {
enabled: true,
/**
* It does not seem possible to get the tooltip closer to the anchor
* when positioned on top. So it is positioned on bottom with a negative offset.
*/
offset: '0,-20',
},
},
}}
>
<Text noMargin style={styles.label}>
{props.label}
</Text>
</Tooltip>
</label>
);
};

View File

@@ -0,0 +1,11 @@
import React from 'react';
import SvgIcon from '@material-ui/core/SvgIcon';
export default React.memo(props => (
<SvgIcon {...props} width="17" height="16" viewBox="0 0 17 16" fill="none">
<path
d="M4.79997 4.8335C3.05107 4.8335 1.6333 6.25126 1.6333 8.00016C1.6333 9.74905 3.05107 11.1668 4.79997 11.1668C5.83632 11.1668 6.75639 10.6684 7.33329 9.90047C7.33892 9.89296 7.34435 9.8853 7.34955 9.87749L7.52731 9.61083C7.68048 9.38106 7.61838 9.07062 7.3886 8.91746C7.15883 8.76429 6.8484 8.82639 6.69524 9.05617L6.52569 9.31051C6.12908 9.83181 5.50371 10.1668 4.79997 10.1668C3.60335 10.1668 2.6333 9.19677 2.6333 8.00016C2.6333 6.80355 3.60335 5.8335 4.79997 5.8335C5.50372 5.8335 6.12903 6.1686 6.52564 6.69003L8.6504 9.87749C8.65561 9.8853 8.66103 9.89296 8.66666 9.90046C9.24354 10.6684 10.1637 11.1668 11.2 11.1668C12.9489 11.1668 14.3667 9.74905 14.3667 8.00016C14.3667 6.25126 12.9489 4.8335 11.2 4.8335C10.1637 4.8335 9.24354 5.33185 8.66666 6.09988C8.50081 6.32067 8.54536 6.6341 8.76615 6.79995C8.98695 6.96579 9.30038 6.92124 9.46623 6.70045C9.86235 6.17307 10.4915 5.8335 11.2 5.8335C12.3966 5.8335 13.3667 6.80355 13.3667 8.00016C13.3667 9.19677 12.3966 10.1668 11.2 10.1668C10.4962 10.1668 9.87085 9.83181 9.47427 9.31052L7.34936 6.12283C7.34288 6.1131 7.33611 6.10368 7.32908 6.09455C6.75198 5.32965 5.83393 4.8335 4.79997 4.8335Z"
fill="currentColor"
/>
</SvgIcon>
));

Some files were not shown because too many files have changed in this diff Show More