diff --git a/Core/GDCore/Project/InitialInstance.cpp b/Core/GDCore/Project/InitialInstance.cpp index 720ad5e3f4af..31c45b28fdda 100644 --- a/Core/GDCore/Project/InitialInstance.cpp +++ b/Core/GDCore/Project/InitialInstance.cpp @@ -27,7 +27,11 @@ InitialInstance::InitialInstance() rotationX(0), rotationY(0), zOrder(0), + opacity(255), layer(""), + flippedX(false), + flippedY(false), + flippedZ(false), customSize(false), customDepth(false), width(0), @@ -57,7 +61,11 @@ void InitialInstance::UnserializeFrom(const SerializerElement& element) { SetHasCustomDepth(false); } SetZOrder(element.GetIntAttribute("zOrder", 0, "plan")); + SetOpacity(element.GetIntAttribute("opacity", 255)); SetLayer(element.GetStringAttribute("layer")); + SetFlippedX(element.GetBoolAttribute("flippedX", false)); + SetFlippedY(element.GetBoolAttribute("flippedY", false)); + SetFlippedZ(element.GetBoolAttribute("flippedZ", false)); SetLocked(element.GetBoolAttribute("locked", false)); SetSealed(element.GetBoolAttribute("sealed", false)); SetShouldKeepRatio(element.GetBoolAttribute("keepRatio", false)); @@ -113,6 +121,10 @@ void InitialInstance::SerializeTo(SerializerElement& element) const { element.SetAttribute("y", GetY()); if (GetZ() != 0) element.SetAttribute("z", GetZ()); element.SetAttribute("zOrder", GetZOrder()); + if (GetOpacity() != 255) element.SetAttribute("opacity", GetOpacity()); + if (IsFlippedX()) element.SetAttribute("flippedX", IsFlippedX()); + if (IsFlippedY()) element.SetAttribute("flippedY", IsFlippedY()); + if (IsFlippedZ()) element.SetAttribute("flippedZ", IsFlippedZ()); element.SetAttribute("layer", GetLayer()); element.SetAttribute("angle", GetAngle()); if (GetRotationX() != 0) element.SetAttribute("rotationX", GetRotationX()); @@ -155,8 +167,8 @@ InitialInstance& InitialInstance::ResetPersistentUuid() { std::map InitialInstance::GetCustomProperties( - gd::ObjectsContainer &globalObjectsContainer, - gd::ObjectsContainer &objectsContainer) { + gd::ObjectsContainer& globalObjectsContainer, + gd::ObjectsContainer& objectsContainer) { // Find an object if (objectsContainer.HasObjectNamed(GetObjectName())) return objectsContainer.GetObject(GetObjectName()) @@ -172,9 +184,10 @@ InitialInstance::GetCustomProperties( } bool InitialInstance::UpdateCustomProperty( - const gd::String &name, const gd::String &value, - gd::ObjectsContainer &globalObjectsContainer, - gd::ObjectsContainer &objectsContainer) { + const gd::String& name, + const gd::String& value, + gd::ObjectsContainer& globalObjectsContainer, + gd::ObjectsContainer& objectsContainer) { if (objectsContainer.HasObjectNamed(GetObjectName())) return objectsContainer.GetObject(GetObjectName()) .GetConfiguration() diff --git a/Core/GDCore/Project/InitialInstance.h b/Core/GDCore/Project/InitialInstance.h index bb8a423d4381..e521d3dcba1c 100644 --- a/Core/GDCore/Project/InitialInstance.h +++ b/Core/GDCore/Project/InitialInstance.h @@ -29,7 +29,7 @@ class GD_CORE_API InitialInstance { * \brief Create an initial instance pointing to no object, at position (0,0). */ InitialInstance(); - virtual ~InitialInstance(){}; + virtual ~InitialInstance() {}; /** * Must return a pointer to a copy of the object. A such method is needed to @@ -123,6 +123,46 @@ class GD_CORE_API InitialInstance { */ void SetZOrder(int zOrder_) { zOrder = zOrder_; } + /** + * \brief Get Opacity. + */ + int GetOpacity() const { return opacity; } + + /** + * \brief Set the opacity of the instance. + */ + void SetOpacity(int opacity_) { opacity = opacity_; } + + /** + * \brief Return true if the instance is flipped on X axis. + */ + bool IsFlippedX() const { return flippedX; } + + /** + * \brief Set whether the instance is flipped on X axis. + */ + void SetFlippedX(bool flippedX_) { flippedX = flippedX_; } + + /** + * \brief Return true if the instance is flipped on Y axis. + */ + bool IsFlippedY() const { return flippedY; } + + /** + * \brief Set whether the instance is flipped on Y axis. + */ + void SetFlippedY(bool flippedY_) { flippedY = flippedY_; } + + /** + * \brief Return true if the instance is flipped on Z axis. + */ + bool IsFlippedZ() const { return flippedZ; } + + /** + * \brief Set whether the instance is flipped on Z axis. + */ + void SetFlippedZ(bool flippedZ_) { flippedZ = flippedZ_; } + /** * \brief Get the layer the instance belongs to. */ @@ -134,8 +174,9 @@ class GD_CORE_API InitialInstance { void SetLayer(const gd::String& layer_) { layer = layer_; } /** - * \brief Return true if the instance has a width/height which is different from its - * object default width/height. This is independent from `HasCustomDepth`. + * \brief Return true if the instance has a width/height which is different + * from its object default width/height. This is independent from + * `HasCustomDepth`. * * \see gd::Object */ @@ -150,15 +191,13 @@ class GD_CORE_API InitialInstance { bool HasCustomDepth() const { return customDepth; } /** - * \brief Set whether the instance has a width/height which is different from its - * object default width/height or not. - * This is independent from `SetHasCustomDepth`. + * \brief Set whether the instance has a width/height which is different from + * its object default width/height or not. This is independent from + * `SetHasCustomDepth`. * * \see gd::Object */ - void SetHasCustomSize(bool hasCustomSize_) { - customSize = hasCustomSize_; - } + void SetHasCustomSize(bool hasCustomSize_) { customSize = hasCustomSize_; } /** * \brief Set whether the instance has a depth which is different from its @@ -264,18 +303,19 @@ class GD_CORE_API InitialInstance { * \note Common properties ( name, position... ) do not need to be * inserted in this map */ - std::map - GetCustomProperties(gd::ObjectsContainer &globalObjectsContainer, - gd::ObjectsContainer &objectsContainer); + std::map GetCustomProperties( + gd::ObjectsContainer& globalObjectsContainer, + gd::ObjectsContainer& objectsContainer); /** * \brief Update the property called \a name with the new \a value. * * \return false if the property could not be updated. */ - bool UpdateCustomProperty(const gd::String &name, const gd::String &value, - gd::ObjectsContainer &globalObjectsContainer, - gd::ObjectsContainer &objectsContainer); + bool UpdateCustomProperty(const gd::String& name, + const gd::String& value, + gd::ObjectsContainer& globalObjectsContainer, + gd::ObjectsContainer& objectsContainer); /** * \brief Get the value of a double property stored in the instance. @@ -343,6 +383,10 @@ class GD_CORE_API InitialInstance { double rotationX; ///< Instance angle on X axis (for a 3D object) double rotationY; ///< Instance angle on Y axis (for a 3D object) int zOrder; ///< Instance Z order (for a 2D object) + int opacity; ///< Instance opacity + bool flippedX; ///< True if the instance is flipped on X axis + bool flippedY; ///< True if the instance is flipped on Y axis + bool flippedZ; ///< True if the instance is flipped on Z axis gd::String layer; ///< Instance layer bool customSize; ///< True if object has a custom width and height bool customDepth; ///< True if object has a custom depth @@ -352,13 +396,13 @@ class GD_CORE_API InitialInstance { gd::VariablesContainer initialVariables; ///< Instance specific variables bool locked; ///< True if the instance is locked bool sealed; ///< True if the instance is sealed - bool keepRatio; ///< True if the instance's dimensions - /// should keep the same ratio. + bool keepRatio; ///< True if the instance's dimensions + /// should keep the same ratio. mutable gd::String persistentUuid; ///< A persistent random version 4 UUID, /// useful for hot reloading. - static gd::String* - badStringPropertyValue; ///< Empty string returned by GetRawStringProperty + static gd::String* badStringPropertyValue; ///< Empty string returned by + ///< GetRawStringProperty }; } // namespace gd diff --git a/Extensions/3D/A_RuntimeObject3D.ts b/Extensions/3D/A_RuntimeObject3D.ts index 4e6b4198099a..f57067cb5aad 100644 --- a/Extensions/3D/A_RuntimeObject3D.ts +++ b/Extensions/3D/A_RuntimeObject3D.ts @@ -164,8 +164,18 @@ namespace gdjs { this.setWidth(initialInstanceData.width); this.setHeight(initialInstanceData.height); } - if (initialInstanceData.depth !== undefined) + if (initialInstanceData.depth !== undefined) { this.setDepth(initialInstanceData.depth); + } + if (initialInstanceData.flippedX) { + this.flipX(initialInstanceData.flippedX); + } + if (initialInstanceData.flippedY) { + this.flipY(initialInstanceData.flippedY); + } + if (initialInstanceData.flippedZ) { + this.flipZ(initialInstanceData.flippedZ); + } } setX(x: float): void { diff --git a/Extensions/3D/CustomRuntimeObject3D.ts b/Extensions/3D/CustomRuntimeObject3D.ts index 1bdf7936c994..906cd55750fe 100644 --- a/Extensions/3D/CustomRuntimeObject3D.ts +++ b/Extensions/3D/CustomRuntimeObject3D.ts @@ -72,8 +72,18 @@ namespace gdjs { extraInitializationFromInitialInstance(initialInstanceData: InstanceData) { super.extraInitializationFromInitialInstance(initialInstanceData); - if (initialInstanceData.depth !== undefined) + if (initialInstanceData.depth !== undefined) { this.setDepth(initialInstanceData.depth); + } + if (initialInstanceData.flippedX) { + this.flipX(initialInstanceData.flippedX); + } + if (initialInstanceData.flippedY) { + this.flipY(initialInstanceData.flippedY); + } + if (initialInstanceData.flippedZ) { + this.flipZ(initialInstanceData.flippedZ); + } } /** diff --git a/Extensions/3D/JsExtension.js b/Extensions/3D/JsExtension.js index 9234593e7fd8..6034c28b57fa 100644 --- a/Extensions/3D/JsExtension.js +++ b/Extensions/3D/JsExtension.js @@ -2216,8 +2216,14 @@ module.exports = { this._centerY / objectTextureFrame.height; this._pixiTexturedObject.angle = this._instance.getAngle(); - this._pixiTexturedObject.scale.x = width / objectTextureFrame.width; - this._pixiTexturedObject.scale.y = height / objectTextureFrame.height; + const scaleX = + (width / objectTextureFrame.width) * + (this._instance.isFlippedX() ? -1 : 1); + const scaleY = + (height / objectTextureFrame.height) * + (this._instance.isFlippedY() ? -1 : 1); + this._pixiTexturedObject.scale.x = scaleX; + this._pixiTexturedObject.scale.y = scaleY; this._pixiTexturedObject.position.x = this._instance.getX() + @@ -2244,6 +2250,9 @@ module.exports = { this._pixiFallbackObject.position.y = this._instance.getY() + height / 2; this._pixiFallbackObject.angle = this._instance.getAngle(); + + if (this._instance.isFlippedX()) this._pixiFallbackObject.scale.x = -1; + if (this._instance.isFlippedY()) this._pixiFallbackObject.scale.y = -1; } update() { @@ -2393,12 +2402,16 @@ module.exports = { RenderedInstance.toRad(this._instance.getAngle()) ); + 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 ( - width !== this._threeObject.scale.width || - height !== this._threeObject.scale.height || - depth !== this._threeObject.scale.depth + scaleX !== this._threeObject.scale.width || + scaleY !== this._threeObject.scale.height || + scaleZ !== this._threeObject.scale.depth ) { - this._threeObject.scale.set(width, height, depth); + this._threeObject.scale.set(scaleX, scaleY, scaleZ); this.updateTextureUvMapping(); } } @@ -3186,12 +3199,16 @@ module.exports = { RenderedInstance.toRad(this._instance.getAngle()) ); + 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 ( - width !== this._threeObject.scale.width || - height !== this._threeObject.scale.height || - depth !== this._threeObject.scale.depth + scaleX !== this._threeObject.scale.width || + scaleY !== this._threeObject.scale.height || + scaleZ !== this._threeObject.scale.depth ) { - this._threeObject.scale.set(width, height, depth); + this._threeObject.scale.set(scaleX, scaleY, scaleZ); } } diff --git a/Extensions/BBText/JsExtension.js b/Extensions/BBText/JsExtension.js index 7c3ffa2aa93e..a534614e07bd 100644 --- a/Extensions/BBText/JsExtension.js +++ b/Extensions/BBText/JsExtension.js @@ -66,13 +66,6 @@ module.exports = { .setLabel(_('Base color')) .setGroup(_('Appearance')); - objectProperties - .getOrCreate('opacity') - .setValue(objectContent.opacity.toString()) - .setType('number') - .setLabel(_('Opacity (0-255)')) - .setGroup(_('Appearance')); - objectProperties .getOrCreate('fontSize') .setValue(objectContent.fontSize.toString()) @@ -545,9 +538,6 @@ module.exports = { this._pixiObject.text = rawText; } - const opacity = +properties.get('opacity').getValue(); - this._pixiObject.alpha = opacity / 255; - const color = properties.get('color').getValue(); this._pixiObject.textStyles.default.fill = objectsRenderingService.rgbOrHexToHexNumber( color @@ -607,6 +597,13 @@ module.exports = { this._pixiObject.dirty = true; } } + + // Do not hide completely an object so it can still be manipulated + const alphaForDisplay = Math.max( + this._instance.getOpacity() / 255, + 0.5 + ); + this._pixiObject.alpha = alphaForDisplay; } /** diff --git a/Extensions/BBText/bbtextruntimeobject.ts b/Extensions/BBText/bbtextruntimeobject.ts index b95229e7ce84..b87869c55171 100644 --- a/Extensions/BBText/bbtextruntimeobject.ts +++ b/Extensions/BBText/bbtextruntimeobject.ts @@ -192,6 +192,9 @@ namespace gdjs { 250 ); } + if (initialInstanceData.opacity !== undefined) { + this.setOpacity(initialInstanceData.opacity); + } } onDestroyed(): void { diff --git a/Extensions/BitmapText/JsExtension.js b/Extensions/BitmapText/JsExtension.js index badb02cd9593..d06142f6ed2d 100644 --- a/Extensions/BitmapText/JsExtension.js +++ b/Extensions/BitmapText/JsExtension.js @@ -59,13 +59,6 @@ module.exports = { .setType('textarea') .setLabel(_('Text')); - objectProperties - .getOrCreate('opacity') - .setValue(objectContent.opacity.toString()) - .setType('number') - .setLabel(_('Opacity (0-255)')) - .setGroup(_('Appearance')); - objectProperties .getOrCreate('align') .setValue(objectContent.align) @@ -673,9 +666,6 @@ module.exports = { const rawText = properties.get('text').getValue(); this._pixiObject.text = rawText; - const opacity = +properties.get('opacity').getValue(); - this._pixiObject.alpha = opacity / 255; - const align = properties.get('align').getValue(); this._pixiObject.align = align; @@ -739,6 +729,13 @@ module.exports = { this._pixiObject.rotation = RenderedInstance.toRad( this._instance.getAngle() ); + + // Do not hide completely an object so it can still be manipulated + const alphaForDisplay = Math.max( + this._instance.getOpacity() / 255, + 0.5 + ); + this._pixiObject.alpha = alphaForDisplay; } onRemovedFromScene() { diff --git a/Extensions/BitmapText/bitmaptextruntimeobject.ts b/Extensions/BitmapText/bitmaptextruntimeobject.ts index b9ff8043e30b..3804cd91459d 100644 --- a/Extensions/BitmapText/bitmaptextruntimeobject.ts +++ b/Extensions/BitmapText/bitmaptextruntimeobject.ts @@ -203,6 +203,9 @@ namespace gdjs { if (initialInstanceData.customSize) { this.setWrappingWidth(initialInstanceData.width); } + if (initialInstanceData.opacity !== undefined) { + this.setOpacity(initialInstanceData.opacity); + } } onDestroyed(): void { diff --git a/Extensions/PanelSpriteObject/panelspriteruntimeobject.ts b/Extensions/PanelSpriteObject/panelspriteruntimeobject.ts index 3cbeec2519ff..74696391b514 100644 --- a/Extensions/PanelSpriteObject/panelspriteruntimeobject.ts +++ b/Extensions/PanelSpriteObject/panelspriteruntimeobject.ts @@ -172,6 +172,9 @@ namespace gdjs { this.setWidth(initialInstanceData.width); this.setHeight(initialInstanceData.height); } + if (initialInstanceData.opacity !== undefined) { + this.setOpacity(initialInstanceData.opacity); + } } /** diff --git a/Extensions/PrimitiveDrawing/shapepainterruntimeobject.ts b/Extensions/PrimitiveDrawing/shapepainterruntimeobject.ts index 766a33e0a7aa..dc4eba40f7e3 100644 --- a/Extensions/PrimitiveDrawing/shapepainterruntimeobject.ts +++ b/Extensions/PrimitiveDrawing/shapepainterruntimeobject.ts @@ -166,6 +166,19 @@ namespace gdjs { return true; } + /** + * Initialize the extra parameters that could be set for an instance. + * @param initialInstanceData The extra parameters + */ + extraInitializationFromInitialInstance(initialInstanceData: InstanceData) { + if (initialInstanceData.flippedX) { + this.flipX(initialInstanceData.flippedX); + } + if (initialInstanceData.flippedY) { + this.flipY(initialInstanceData.flippedY); + } + } + stepBehaviorsPreEvents(instanceContainer: gdjs.RuntimeInstanceContainer) { //We redefine stepBehaviorsPreEvents just to clear the graphics before running events. if (this._clearBetweenFrames) { diff --git a/Extensions/Spine/JsExtension.js b/Extensions/Spine/JsExtension.js index 787b0b01fba8..326622790a27 100644 --- a/Extensions/Spine/JsExtension.js +++ b/Extensions/Spine/JsExtension.js @@ -182,6 +182,20 @@ module.exports = { this._instance.getAngle() ); + // Do not hide completely an object so it can still be manipulated + const alphaForDisplay = Math.max( + this._instance.getOpacity() / 255, + 0.5 + ); + this._pixiObject.alpha = alphaForDisplay; + // Scale is already handled below, so we just apply the flip here. + this._pixiObject.scale.x = + Math.abs(this._pixiObject.scale.x) * + (this._instance.isFlippedX() ? -1 : 1); + this._pixiObject.scale.y = + Math.abs(this._pixiObject.scale.y) * + (this._instance.isFlippedY() ? -1 : 1); + this.setAnimation(this._instance.getRawDoubleProperty('animation')); const width = this.getWidth(); diff --git a/Extensions/Spine/spineruntimeobject.ts b/Extensions/Spine/spineruntimeobject.ts index 09d37f1c993a..3c07188eb533 100644 --- a/Extensions/Spine/spineruntimeobject.ts +++ b/Extensions/Spine/spineruntimeobject.ts @@ -208,6 +208,15 @@ namespace gdjs { this.setSize(initialInstanceData.width, initialInstanceData.height); this.invalidateHitboxes(); } + if (initialInstanceData.opacity !== undefined) { + this.setOpacity(initialInstanceData.opacity); + } + if (initialInstanceData.flippedX) { + this.flipX(initialInstanceData.flippedX); + } + if (initialInstanceData.flippedY) { + this.flipY(initialInstanceData.flippedY); + } } getDrawableX(): number { diff --git a/Extensions/TextInput/JsExtension.js b/Extensions/TextInput/JsExtension.js index 5ba27af39092..7aa37e7b2d68 100644 --- a/Extensions/TextInput/JsExtension.js +++ b/Extensions/TextInput/JsExtension.js @@ -789,6 +789,13 @@ module.exports = { ); this._pixiGraphics.drawRect(0, 0, width, height); this._pixiGraphics.endFill(); + + // Do not hide completely an object so it can still be manipulated + const alphaForDisplay = Math.max( + this._instance.getOpacity() / 255, + 0.5 + ); + this._pixiObject.alpha = alphaForDisplay; } getDefaultWidth() { diff --git a/Extensions/TextInput/textinputruntimeobject.ts b/Extensions/TextInput/textinputruntimeobject.ts index f27fa2fbe15e..9e3a54c79b8e 100644 --- a/Extensions/TextInput/textinputruntimeobject.ts +++ b/Extensions/TextInput/textinputruntimeobject.ts @@ -254,6 +254,9 @@ namespace gdjs { this.setWidth(initialInstanceData.width); this.setHeight(initialInstanceData.height); } + if (initialInstanceData.opacity !== undefined) { + this.setOpacity(initialInstanceData.opacity); + } } onScenePaused(runtimeScene: gdjs.RuntimeScene): void { diff --git a/Extensions/TextObject/textruntimeobject.ts b/Extensions/TextObject/textruntimeobject.ts index 9fcd5897db0d..01a63207f617 100644 --- a/Extensions/TextObject/textruntimeobject.ts +++ b/Extensions/TextObject/textruntimeobject.ts @@ -328,6 +328,9 @@ namespace gdjs { } else { this.setWrapping(false); } + if (initialInstanceData.opacity !== undefined) { + this.setOpacity(initialInstanceData.opacity); + } } /** diff --git a/Extensions/TileMap/JsExtension.js b/Extensions/TileMap/JsExtension.js index 362fc4e2133d..e80702f63363 100644 --- a/Extensions/TileMap/JsExtension.js +++ b/Extensions/TileMap/JsExtension.js @@ -1611,6 +1611,7 @@ module.exports = { this.tileMapPixiObject = new Tilemap.CompositeTilemap(); this._pixiObject = this.tileMapPixiObject; + this._editableTileMap = null; // Implement `containsPoint` so that we can set `interactive` to true and // the Tilemap will properly emit events when hovered/clicked. @@ -1686,35 +1687,27 @@ module.exports = { * This is used to reload the Tilemap */ updateTileMap() { + const tilemapObjectProperties = this._associatedObjectConfiguration.getProperties(); + // Get the tileset resource to use - const tilemapAtlasImage = this._associatedObjectConfiguration - .getProperties() + const tilemapAtlasImage = tilemapObjectProperties .get('tilemapAtlasImage') .getValue(); - const tilemapJsonFile = this._associatedObjectConfiguration - .getProperties() + const tilemapJsonFile = tilemapObjectProperties .get('tilemapJsonFile') .getValue(); - const tilesetJsonFile = this._associatedObjectConfiguration - .getProperties() + const tilesetJsonFile = tilemapObjectProperties .get('tilesetJsonFile') .getValue(); const layerIndex = parseInt( - this._associatedObjectConfiguration - .getProperties() - .get('layerIndex') - .getValue(), + tilemapObjectProperties.get('layerIndex').getValue(), 10 ); const levelIndex = parseInt( - this._associatedObjectConfiguration - .getProperties() - .get('levelIndex') - .getValue(), + tilemapObjectProperties.get('levelIndex').getValue(), 10 ); - const displayMode = this._associatedObjectConfiguration - .getProperties() + const displayMode = tilemapObjectProperties .get('displayMode') .getValue(); @@ -1755,6 +1748,8 @@ module.exports = { return; } + this._editableTileMap = tileMap; + /** @type {TileMapHelper.TileTextureCache} */ manager.getOrLoadTextureCache( this._loadTileMapWithCallback.bind(this), @@ -1774,12 +1769,13 @@ module.exports = { return; } this._onLoadingSuccess(); + if (!this._editableTileMap) return; - this.width = tileMap.getWidth(); - this.height = tileMap.getHeight(); + this.width = this._editableTileMap.getWidth(); + this.height = this._editableTileMap.getHeight(); TilemapHelper.PixiTileMapHelper.updatePixiTileMap( this.tileMapPixiObject, - tileMap, + this._editableTileMap, textureCache, displayMode, layerIndex @@ -1800,6 +1796,85 @@ module.exports = { } } + /** + * This is called to update the PIXI object on the scene editor, without reloading the tilemap. + */ + updatePixiTileMap() { + const tilemapObjectProperties = this._associatedObjectConfiguration.getProperties(); + + // 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 tilemapResource = this._project + .getResourcesManager() + .getResource(tilemapJsonFile); + + let metadata = {}; + try { + const tilemapMetadataAsString = tilemapResource.getMetadata(); + if (tilemapMetadataAsString) + metadata = JSON.parse(tilemapMetadataAsString); + } catch (error) { + console.warn('Malformed metadata in a tilemap object:', error); + } + const mapping = metadata.embeddedResourcesMapping || {}; + + /** @type {TileMapHelper.TileMapManager} */ + const manager = TilemapHelper.TileMapManager.getManager(this._project); + + /** @type {TileMapHelper.TileTextureCache} */ + manager.getOrLoadTextureCache( + this._loadTileMapWithCallback.bind(this), + (textureName) => + this._pixiResourcesLoader.getPIXITexture( + this._project, + mapping[textureName] || textureName + ), + tilemapAtlasImage, + tilemapJsonFile, + tilesetJsonFile, + levelIndex, + (textureCache) => { + if (!textureCache) { + this._onLoadingError(); + // getOrLoadTextureCache already log warns and errors. + return; + } + this._onLoadingSuccess(); + if (!this._editableTileMap) return; + + this.width = this._editableTileMap.getWidth(); + this.height = this._editableTileMap.getHeight(); + TilemapHelper.PixiTileMapHelper.updatePixiTileMap( + this.tileMapPixiObject, + this._editableTileMap, + textureCache, + displayMode, + layerIndex + ); + } + ); + } + // GDJS doesn't use Promise to avoid allocation. _loadTileMapWithCallback(tilemapJsonFile, tilesetJsonFile, callback) { this._loadTileMap(tilemapJsonFile, tilesetJsonFile).then(callback); @@ -1870,6 +1945,30 @@ module.exports = { this._pixiObject.rotation = RenderedInstance.toRad( this._instance.getAngle() ); + + // Update the opacity, if needed. + // Do not hide completely an object so it can still be manipulated + const alphaForDisplay = Math.max( + this._instance.getOpacity() / 255, + 0.5 + ); + + if ( + this._editableTileMap && + this._pixiObject.alpha !== alphaForDisplay + ) { + this._pixiObject.alpha = alphaForDisplay; + for (const layer of this._editableTileMap.getLayers()) { + // Only update layers that are of type TileMapHelper.EditableTileMapLayer. + // @ts-ignore - only this type of layer has setAlpha. + if (layer.setAlpha) { + const editableLayer = /** @type {TileMapHelper.EditableTileMapLayer} */ (layer); + editableLayer.setAlpha(alphaForDisplay); + } + } + // Only update the tilemap if the alpha has changed. + this.updatePixiTileMap(); + } } /** @@ -2148,6 +2247,9 @@ module.exports = { } } + /** + * This is called to update the PIXI object on the scene editor, without reloading the tilemap. + */ updatePixiTileMap() { const atlasImageResourceName = this._associatedObjectConfiguration .getProperties() @@ -2266,6 +2368,26 @@ module.exports = { objectToChange.rotation = RenderedInstance.toRad( this._instance.getAngle() ); + + // Update the opacity, if needed. + // Do not hide completely an object so it can still be manipulated + const alphaForDisplay = Math.max( + this._instance.getOpacity() / 255, + 0.5 + ); + + if (this._editableTileMap && objectToChange.alpha !== alphaForDisplay) { + objectToChange.alpha = alphaForDisplay; + for (const layer of this._editableTileMap.getLayers()) { + // Only update layers that are of type TileMapHelper.EditableTileMapLayer. + // @ts-ignore - only this type of layer has setAlpha. + if (layer.setAlpha) { + const editableLayer = /** @type {TileMapHelper.EditableTileMapLayer} */ (layer); + editableLayer.setAlpha(alphaForDisplay); + } + } + this.updatePixiTileMap(); + } } /** diff --git a/Extensions/TileMap/simpletilemapruntimeobject.ts b/Extensions/TileMap/simpletilemapruntimeobject.ts index 128e9990d8d1..9ac1f68ceab8 100644 --- a/Extensions/TileMap/simpletilemapruntimeobject.ts +++ b/Extensions/TileMap/simpletilemapruntimeobject.ts @@ -186,11 +186,14 @@ namespace gdjs { // 2. Update the renderer so that it updates the tilemap object // (used for width and position calculations). this._loadInitialTileMap((tileMap: TileMapHelper.EditableTileMap) => { - // 3. Set custom dimensions if applicable. + // 3. Set custom dimensions & opacity if applicable. if (initialInstanceData.customSize) { this.setWidth(initialInstanceData.width); this.setHeight(initialInstanceData.height); } + if (initialInstanceData.opacity !== undefined) { + this.setOpacity(initialInstanceData.opacity); + } // 4. Update position (calculations based on renderer's dimensions). this._renderer.updatePosition(); diff --git a/Extensions/TileMap/tilemapruntimeobject.ts b/Extensions/TileMap/tilemapruntimeobject.ts index 23568ec77991..95a21f1c5571 100644 --- a/Extensions/TileMap/tilemapruntimeobject.ts +++ b/Extensions/TileMap/tilemapruntimeobject.ts @@ -203,6 +203,9 @@ namespace gdjs { this.setWidth(initialInstanceData.width); this.setHeight(initialInstanceData.height); } + if (initialInstanceData.opacity !== undefined) { + this.setOpacity(initialInstanceData.opacity); + } } private _updateTileMap(): void { diff --git a/Extensions/TiledSpriteObject/tiledspriteruntimeobject.ts b/Extensions/TiledSpriteObject/tiledspriteruntimeobject.ts index 272e2e1804d9..01c076c9eee6 100644 --- a/Extensions/TiledSpriteObject/tiledspriteruntimeobject.ts +++ b/Extensions/TiledSpriteObject/tiledspriteruntimeobject.ts @@ -135,6 +135,9 @@ namespace gdjs { this.setWidth(initialInstanceData.width); this.setHeight(initialInstanceData.height); } + if (initialInstanceData.opacity !== undefined) { + this.setOpacity(initialInstanceData.opacity); + } } /** diff --git a/Extensions/Video/JsExtension.js b/Extensions/Video/JsExtension.js index 83fa6fb64071..4c1ae80a484c 100644 --- a/Extensions/Video/JsExtension.js +++ b/Extensions/Video/JsExtension.js @@ -62,12 +62,6 @@ module.exports = { videoObject.getProperties = function (objectContent) { var objectProperties = new gd.MapStringPropertyDescriptor(); - objectProperties - .getOrCreate('Opacity') - .setValue(objectContent.opacity.toString()) - .setType('number') - .setLabel(_('Video opacity (0-255)')) - .setGroup(_('Appearance')); objectProperties .getOrCreate('Looped') .setValue(objectContent.loop ? 'true' : 'false') @@ -625,13 +619,6 @@ module.exports = { } } - // Update opacity - const opacity = +this._associatedObjectConfiguration - .getProperties() - .get('Opacity') - .getValue(); - this._pixiObject.alpha = opacity / 255; - // Read position and angle from the instance this._pixiObject.position.x = this._instance.getX() + this._pixiObject.width / 2; @@ -645,6 +632,13 @@ module.exports = { this._pixiObject.width = this.getCustomWidth(); this._pixiObject.height = this.getCustomHeight(); } + + // Do not hide completely an object so it can still be manipulated + const alphaForDisplay = Math.max( + this._instance.getOpacity() / 255, + 0.5 + ); + this._pixiObject.alpha = alphaForDisplay; } /** diff --git a/Extensions/Video/videoruntimeobject.ts b/Extensions/Video/videoruntimeobject.ts index fd290e82c720..5041967b2293 100644 --- a/Extensions/Video/videoruntimeobject.ts +++ b/Extensions/Video/videoruntimeobject.ts @@ -155,6 +155,9 @@ namespace gdjs { this.setWidth(initialInstanceData.width); this.setHeight(initialInstanceData.height); } + if (initialInstanceData.opacity !== undefined) { + this.setOpacity(initialInstanceData.opacity); + } } onDestroyed(): void { diff --git a/GDJS/Runtime/CustomRuntimeObject.ts b/GDJS/Runtime/CustomRuntimeObject.ts index 17ac9726ac31..0b3daaa8e385 100644 --- a/GDJS/Runtime/CustomRuntimeObject.ts +++ b/GDJS/Runtime/CustomRuntimeObject.ts @@ -166,6 +166,15 @@ namespace gdjs { this.setWidth(initialInstanceData.width); this.setHeight(initialInstanceData.height); } + if (initialInstanceData.opacity !== undefined) { + this.setOpacity(initialInstanceData.opacity); + } + if (initialInstanceData.flippedX) { + this.flipX(initialInstanceData.flippedX); + } + if (initialInstanceData.flippedY) { + this.flipY(initialInstanceData.flippedY); + } } onDeletedFromScene(parent: gdjs.RuntimeInstanceContainer): void { diff --git a/GDJS/Runtime/pixi-renderers/CustomRuntimeObject2DPixiRenderer.ts b/GDJS/Runtime/pixi-renderers/CustomRuntimeObject2DPixiRenderer.ts index ad164ac5f771..ef916467b4f7 100644 --- a/GDJS/Runtime/pixi-renderers/CustomRuntimeObject2DPixiRenderer.ts +++ b/GDJS/Runtime/pixi-renderers/CustomRuntimeObject2DPixiRenderer.ts @@ -80,8 +80,10 @@ namespace gdjs { this._object.getY() + this._pixiContainer.pivot.y * Math.abs(scaleY); this._pixiContainer.rotation = gdjs.toRad(this._object.angle); - this._pixiContainer.scale.x = scaleX; - this._pixiContainer.scale.y = scaleY; + this._pixiContainer.scale.x = + scaleX * (this._object.isFlippedX() ? -1 : 1); + this._pixiContainer.scale.y = + scaleY * (this._object.isFlippedY() ? -1 : 1); this._pixiContainer.visible = !this._object.hidden; this._pixiContainer.alpha = opacity / 255; diff --git a/GDJS/Runtime/spriteruntimeobject.ts b/GDJS/Runtime/spriteruntimeobject.ts index 7e468982ea51..7dd4b90e9e8e 100644 --- a/GDJS/Runtime/spriteruntimeobject.ts +++ b/GDJS/Runtime/spriteruntimeobject.ts @@ -184,6 +184,15 @@ namespace gdjs { this.setWidth(initialInstanceData.width); this.setHeight(initialInstanceData.height); } + if (initialInstanceData.opacity !== undefined) { + this.setOpacity(initialInstanceData.opacity); + } + if (initialInstanceData.flippedX) { + this.flipX(initialInstanceData.flippedX); + } + if (initialInstanceData.flippedY) { + this.flipY(initialInstanceData.flippedY); + } } /** diff --git a/GDJS/Runtime/types/project-data.d.ts b/GDJS/Runtime/types/project-data.d.ts index bc607ea279bd..c6b23d620230 100644 --- a/GDJS/Runtime/types/project-data.d.ts +++ b/GDJS/Runtime/types/project-data.d.ts @@ -254,6 +254,11 @@ declare interface InstanceData { rotationY?: number; zOrder: number; + opacity?: number; + + flippedX?: boolean; + flippedY?: boolean; + flippedZ?: boolean; customSize: boolean; width: number; diff --git a/GDevelop.js/Bindings/Bindings.idl b/GDevelop.js/Bindings/Bindings.idl index 7858d4584783..57d0d7728918 100644 --- a/GDevelop.js/Bindings/Bindings.idl +++ b/GDevelop.js/Bindings/Bindings.idl @@ -1303,9 +1303,18 @@ interface InitialInstance { void SetShouldKeepRatio(boolean keepRatio); long GetZOrder(); void SetZOrder(long zOrder); + long GetOpacity(); + void SetOpacity(long opacity); [Const, Ref] DOMString GetLayer(); void SetLayer([Const] DOMString layer); + boolean IsFlippedX(); + void SetFlippedX(boolean flippedX); + boolean IsFlippedY(); + void SetFlippedY(boolean flippedY); + boolean IsFlippedZ(); + void SetFlippedZ(boolean flippedZ); + void SetHasCustomSize(boolean enable); boolean HasCustomSize(); void SetHasCustomDepth(boolean enable); diff --git a/GDevelop.js/types.d.ts b/GDevelop.js/types.d.ts index 7705f4a3c8f1..d83b607f317f 100644 --- a/GDevelop.js/types.d.ts +++ b/GDevelop.js/types.d.ts @@ -1088,8 +1088,16 @@ export class InitialInstance extends EmscriptenObject { setShouldKeepRatio(keepRatio: boolean): void; getZOrder(): number; setZOrder(zOrder: number): void; + getOpacity(): number; + setOpacity(opacity: number): void; getLayer(): string; setLayer(layer: string): void; + isFlippedX(): boolean; + setFlippedX(flippedX: boolean): void; + isFlippedY(): boolean; + setFlippedY(flippedY: boolean): void; + isFlippedZ(): boolean; + setFlippedZ(flippedZ: boolean): void; setHasCustomSize(enable: boolean): void; hasCustomSize(): boolean; setHasCustomDepth(enable: boolean): void; diff --git a/GDevelop.js/types/gdinitialinstance.js b/GDevelop.js/types/gdinitialinstance.js index 8908de31ed87..e2f540f2ca9b 100644 --- a/GDevelop.js/types/gdinitialinstance.js +++ b/GDevelop.js/types/gdinitialinstance.js @@ -23,8 +23,16 @@ declare class gdInitialInstance { setShouldKeepRatio(keepRatio: boolean): void; getZOrder(): number; setZOrder(zOrder: number): void; + getOpacity(): number; + setOpacity(opacity: number): void; getLayer(): string; setLayer(layer: string): void; + isFlippedX(): boolean; + setFlippedX(flippedX: boolean): void; + isFlippedY(): boolean; + setFlippedY(flippedY: boolean): void; + isFlippedZ(): boolean; + setFlippedZ(flippedZ: boolean): void; setHasCustomSize(enable: boolean): void; hasCustomSize(): boolean; setHasCustomDepth(enable: boolean): void; diff --git a/newIDE/app/src/CompactPropertiesEditor/index.js b/newIDE/app/src/CompactPropertiesEditor/index.js index 8b967b07b6d4..94472cee3e61 100644 --- a/newIDE/app/src/CompactPropertiesEditor/index.js +++ b/newIDE/app/src/CompactPropertiesEditor/index.js @@ -27,6 +27,7 @@ import VerticallyCenterWithBar from '../UI/VerticallyCenterWithBar'; import GDevelopThemeContext from '../UI/Theme/GDevelopThemeContext'; import { textEllipsisStyle } from '../UI/TextEllipsis'; import CompactPropertiesEditorRowField from './CompactPropertiesEditorRowField'; +import CompactToggleButtons from '../UI/CompactToggleButtons'; // An "instance" here is the objects for which properties are shown export type Instance = Object; // This could be improved using generics. @@ -42,6 +43,8 @@ export type ValueFieldCommonProperties = {| disabled?: (instances: Array) => boolean, onEditButtonBuildMenuTemplate?: (i18n: I18nType) => Array, onEditButtonClick?: () => void, + getValueFromDisplayedValue?: string => string, + getDisplayedValueFromValue?: string => string, |}; // "Primitive" value fields are "simple" fields. @@ -144,6 +147,18 @@ type ActionButton = {| onClick: (instance: Instance) => void, |}; +type ToggleButtons = {| + name: string, + nonFieldType: 'toggleButtons', + buttons: Array<{| + name: string, + renderIcon: (className?: string) => React.Node, + tooltip: React.Node, + getValue: Instance => boolean, + setValue: (instance: Instance, newValue: boolean) => void, + |}>, +|}; + // A value field is a primitive or a resource. export type ValueField = PrimitiveValueField | ResourceField; @@ -154,6 +169,7 @@ export type Field = | SectionTitle | Title | ActionButton + | ToggleButtons | VerticalCenterWithBar | {| name: string, @@ -418,6 +434,8 @@ const CompactPropertiesEditor = ({ instances.forEach(i => onClickEndAdornment(i)); _onInstancesModified(instances); }, + getValueFromDisplayedValue: field.getValueFromDisplayedValue, + getDisplayedValueFromValue: field.getDisplayedValueFromValue, }; if (field.renderLeftIcon || field.hideLabel) { return ( @@ -676,6 +694,36 @@ const CompactPropertiesEditor = ({ [instances] ); + const renderToggleButtons = React.useCallback( + (field: ToggleButtons) => { + const buttons = field.buttons.map(button => { + // Button is toggled if all instances have a truthy value for it. + const isToggled = + instances.filter(instance => button.getValue(instance)).length === + instances.length; + return { + id: button.name, + renderIcon: button.renderIcon, + tooltip: button.tooltip, + isActive: isToggled, + onClick: () => { + instances.forEach(instance => + button.setValue(instance, !isToggled) + ); + _onInstancesModified(instances); + }, + }; + }); + + return ( + + + + ); + }, + [instances, _onInstancesModified] + ); + const renderResourceField = (field: ResourceField) => { if (!project || !resourceManagementProps) { console.error( @@ -823,6 +871,8 @@ const CompactPropertiesEditor = ({ return renderSectionTitle(field); } else if (field.nonFieldType === 'button') { return renderButton(field); + } else if (field.nonFieldType === 'toggleButtons') { + return renderToggleButtons(field); } else if (field.nonFieldType === 'verticalCenterWithBar') { return renderVerticalCenterWithBar(field); } diff --git a/newIDE/app/src/InstancesEditor/CompactInstancePropertiesEditor/CompactPropertiesSchema.js b/newIDE/app/src/InstancesEditor/CompactInstancePropertiesEditor/CompactPropertiesSchema.js index 0754ba07c6c4..321c499a65fd 100644 --- a/newIDE/app/src/InstancesEditor/CompactInstancePropertiesEditor/CompactPropertiesSchema.js +++ b/newIDE/app/src/InstancesEditor/CompactInstancePropertiesEditor/CompactPropertiesSchema.js @@ -15,6 +15,9 @@ import LetterH from '../../UI/CustomSvgIcons/LetterH'; import LetterW from '../../UI/CustomSvgIcons/LetterW'; import LetterD from '../../UI/CustomSvgIcons/LetterD'; import LetterZ from '../../UI/CustomSvgIcons/LetterZ'; +import Opacity from '../../UI/CustomSvgIcons/Opacity'; +import FlipHorizontal from '../../UI/CustomSvgIcons/FlipHorizontal'; +import FlipVertical from '../../UI/CustomSvgIcons/FlipVertical'; import Instance from '../../UI/CustomSvgIcons/Instance'; import Link from '../../UI/CustomSvgIcons/Link'; import Unlink from '../../UI/CustomSvgIcons/Unlink'; @@ -27,6 +30,7 @@ import Object2d from '../../UI/CustomSvgIcons/Object2d'; import RotateX from '../../UI/CustomSvgIcons/RotateX'; import RotateY from '../../UI/CustomSvgIcons/RotateY'; import RotateZ from '../../UI/CustomSvgIcons/RotateZ'; +import FlipZ from '../../UI/CustomSvgIcons/FlipZ'; /** * Applies ratio to value without intermediary value to avoid precision issues. @@ -414,8 +418,73 @@ const getKeepRatioField = ({ getNextValue: (currentValue: boolean) => !currentValue, }); +const getOpacityField = ({ i18n }: {| i18n: I18nType |}) => ({ + name: 'Opacity', + getLabel: () => i18n._(t`Opacity`), + valueType: 'number', + getValue: (instance: gdInitialInstance) => { + const opacity = instance.getOpacity(); + return Math.round((opacity / 255) * 100); + }, + setValue: (instance: gdInitialInstance, newValue: number) => { + const newOpacity = Math.round((newValue / 100) * 255); + const opacity = Math.max(0, Math.min(255, newOpacity)); + instance.setOpacity(opacity); + }, + renderLeftIcon: className => , + getDisplayedValueFromValue: (value: string): string => { + return `${value}%`; + }, + getValueFromDisplayedValue: (displayedValue: string): string => { + return displayedValue.replace('%', ''); + }, +}); + +const getFlippableButtons = ({ + i18n, + canFlipZ, +}: {| + i18n: I18nType, + canFlipZ: boolean, +|}) => ({ + name: 'Flip', + nonFieldType: 'toggleButtons', + buttons: [ + { + name: 'Flip horizontal', + renderIcon: className => , + tooltip: i18n._(t`Flip horizontally`), + getValue: (instance: gdInitialInstance): boolean => instance.isFlippedX(), + setValue: (instance: gdInitialInstance, newValue: boolean) => + instance.setFlippedX(newValue), + }, + { + name: 'Flip vertical', + tooltip: i18n._(t`Flip vertically`), + renderIcon: className => , + getValue: (instance: gdInitialInstance): boolean => instance.isFlippedY(), + setValue: (instance: gdInitialInstance, newValue: boolean) => + instance.setFlippedY(newValue), + }, + canFlipZ + ? { + name: 'Flip Z', + tooltip: i18n._(t` Flip along Z axis`), + renderIcon: className => , + getValue: (instance: gdInitialInstance): boolean => + instance.isFlippedZ(), + setValue: (instance: gdInitialInstance, newValue: boolean) => + instance.setFlippedZ(newValue), + } + : null, + ].filter(Boolean), +}); + export const makeSchema = ({ is3DInstance, + hasOpacity, + canBeFlippedXY, + canBeFlippedZ, i18n, forceUpdate, onEditObjectByName, @@ -423,6 +492,9 @@ export const makeSchema = ({ layersContainer, }: {| is3DInstance: boolean, + hasOpacity: boolean, + canBeFlippedXY: boolean, + canBeFlippedZ: boolean, i18n: I18nType, forceUpdate: () => void, onEditObjectByName: (name: string) => void, @@ -500,6 +572,15 @@ export const makeSchema = ({ }, ], }, + hasOpacity + ? { + name: 'Opacity', + type: 'row', + preventWrap: true, + removeSpacers: true, + children: [getOpacityField({ i18n })], + } + : null, getLayerField({ i18n, layersContainer }), { name: 'Rotation', @@ -507,10 +588,21 @@ export const makeSchema = ({ title: i18n._(t`Rotation`), preventWrap: true, removeSpacers: true, - children: getRotationXAndRotationYFields({ i18n }), + children: canBeFlippedXY + ? [getFlippableButtons({ i18n, canFlipZ: canBeFlippedZ })] + : [], }, - getRotationZField({ i18n }), - ]; + { + name: 'Rotation X and Y', + type: 'row', + preventWrap: true, + removeSpacers: true, + children: [ + ...getRotationXAndRotationYFields({ i18n }), + getRotationZField({ i18n }), + ], + }, + ].filter(Boolean); } return [ @@ -562,6 +654,15 @@ export const makeSchema = ({ }, ], }, + hasOpacity + ? { + name: 'Opacity', + type: 'row', + preventWrap: true, + removeSpacers: true, + children: [getOpacityField({ i18n })], + } + : null, getLayerField({ i18n, layersContainer }), { name: 'Rotation', @@ -569,9 +670,18 @@ export const makeSchema = ({ title: i18n._(t`Rotation`), preventWrap: true, removeSpacers: true, + children: canBeFlippedXY + ? [getFlippableButtons({ i18n, canFlipZ: canBeFlippedZ })] + : [], + }, + { + name: 'Rotation Z', + type: 'row', + preventWrap: true, + removeSpacers: true, children: [getRotationZField({ i18n })], }, - ]; + ].filter(Boolean); }; export const reorderInstanceSchemaForCustomProperties = ( diff --git a/newIDE/app/src/InstancesEditor/CompactInstancePropertiesEditor/index.js b/newIDE/app/src/InstancesEditor/CompactInstancePropertiesEditor/index.js index 3d8a000dcafd..3ff609be8368 100644 --- a/newIDE/app/src/InstancesEditor/CompactInstancePropertiesEditor/index.js +++ b/newIDE/app/src/InstancesEditor/CompactInstancePropertiesEditor/index.js @@ -90,32 +90,6 @@ const CompactInstancePropertiesEditor = ({ }: Props) => { const forceUpdate = useForceUpdate(); - const schemaFor2D: Schema = React.useMemo( - () => - makeSchema({ - i18n, - is3DInstance: false, - onGetInstanceSize, - onEditObjectByName, - layersContainer, - forceUpdate, - }), - [i18n, onGetInstanceSize, onEditObjectByName, layersContainer, forceUpdate] - ); - - const schemaFor3D: Schema = React.useMemo( - () => - makeSchema({ - i18n, - is3DInstance: true, - onGetInstanceSize, - onEditObjectByName, - layersContainer, - forceUpdate, - }), - [i18n, onGetInstanceSize, onEditObjectByName, layersContainer, forceUpdate] - ); - const instance = instances[0]; /** * TODO: multiple instances support for variables list. Expected behavior should be: @@ -144,10 +118,20 @@ const CompactInstancePropertiesEditor = ({ ); if (!object) return { object: undefined, instanceSchema: undefined }; - const is3DInstance = gd.MetadataProvider.getObjectMetadata( + const objectMetadata = gd.MetadataProvider.getObjectMetadata( project.getCurrentPlatform(), object.getType() - ).isRenderedIn3D(); + ); + const is3DInstance = objectMetadata.isRenderedIn3D(); + const hasOpacity = objectMetadata.hasDefaultBehavior( + 'OpacityCapability::OpacityBehavior' + ); + const canBeFlippedXY = objectMetadata.hasDefaultBehavior( + 'FlippableCapability::FlippableBehavior' + ); + const canBeFlippedZ = objectMetadata.hasDefaultBehavior( + 'Scene3D::Base3DBehavior' + ); const instanceSchemaForCustomProperties = propertiesMapToSchema( properties, (instance: gdInitialInstance) => @@ -168,11 +152,20 @@ const CompactInstancePropertiesEditor = ({ instanceSchemaForCustomProperties, i18n ); + const instanceSchema = makeSchema({ + i18n, + is3DInstance, + hasOpacity, + canBeFlippedXY, + canBeFlippedZ, + onGetInstanceSize, + onEditObjectByName, + layersContainer, + forceUpdate, + }).concat(reorderedInstanceSchemaForCustomProperties); return { object, - instanceSchema: is3DInstance - ? schemaFor3D.concat(reorderedInstanceSchemaForCustomProperties) - : schemaFor2D.concat(reorderedInstanceSchemaForCustomProperties), + instanceSchema, }; }, [ @@ -181,8 +174,10 @@ const CompactInstancePropertiesEditor = ({ objectsContainer, project, i18n, - schemaFor3D, - schemaFor2D, + forceUpdate, + layersContainer, + onGetInstanceSize, + onEditObjectByName, ] ); diff --git a/newIDE/app/src/ObjectsRendering/Renderers/CustomObjectLayoutingModel.js b/newIDE/app/src/ObjectsRendering/Renderers/CustomObjectLayoutingModel.js index 8fd2caf061eb..e5626e418414 100644 --- a/newIDE/app/src/ObjectsRendering/Renderers/CustomObjectLayoutingModel.js +++ b/newIDE/app/src/ObjectsRendering/Renderers/CustomObjectLayoutingModel.js @@ -387,6 +387,7 @@ export class ChildInstance { x: number; y: number; z: number; + opacity: number; _hasCustomSize: boolean; _hasCustomDepth: boolean; _customWidth: number; @@ -397,6 +398,7 @@ export class ChildInstance { this.x = 0; this.y = 0; this.z = 0; + this.opacity = 255; this._customWidth = 0; this._customHeight = 0; this._customDepth = 0; @@ -464,6 +466,26 @@ export class ChildInstance { setZOrder(zOrder: number) {} + getOpacity() { + return this.opacity; + } + + setOpacity(opacity: number) { + this.opacity = opacity; + } + + isFlippedX() { + return false; + } + + setFlippedX(flippedX: boolean) {} + + isFlippedY() { + return false; + } + + setFlippedY(flippedY: boolean) {} + getLayer() { return ''; } diff --git a/newIDE/app/src/ObjectsRendering/Renderers/RenderedCustomObjectInstance.js b/newIDE/app/src/ObjectsRendering/Renderers/RenderedCustomObjectInstance.js index 8800c1d34ff1..8df0fc4edd30 100644 --- a/newIDE/app/src/ObjectsRendering/Renderers/RenderedCustomObjectInstance.js +++ b/newIDE/app/src/ObjectsRendering/Renderers/RenderedCustomObjectInstance.js @@ -524,6 +524,13 @@ export default class RenderedCustomObjectInstance extends Rendered3DInstance this._pixiObject.scale.x = 1; this._pixiObject.scale.y = 1; } + + // Do not hide completely an object so it can still be manipulated + const alphaForDisplay = Math.max(this._instance.getOpacity() / 255, 0.5); + this._pixiObject.alpha = alphaForDisplay; + + if (this._instance.isFlippedX()) this._pixiObject.scale.x *= -1; + if (this._instance.isFlippedY()) this._pixiObject.scale.y *= -1; } getDefaultWidth() { diff --git a/newIDE/app/src/ObjectsRendering/Renderers/RenderedIconInstance.js b/newIDE/app/src/ObjectsRendering/Renderers/RenderedIconInstance.js index 247e8787ae11..fc985e100f74 100644 --- a/newIDE/app/src/ObjectsRendering/Renderers/RenderedIconInstance.js +++ b/newIDE/app/src/ObjectsRendering/Renderers/RenderedIconInstance.js @@ -37,6 +37,10 @@ export default function makeRenderer(iconPath: string) { this._pixiObject.position.x = this._instance.getX(); this._pixiObject.position.y = this._instance.getY(); this._pixiObject.angle = this._instance.getAngle(); + + // Do not hide completely an object so it can still be manipulated + const alphaForDisplay = Math.max(this._instance.getOpacity() / 255, 0.5); + this._pixiObject.alpha = alphaForDisplay; } static getThumbnail( diff --git a/newIDE/app/src/ObjectsRendering/Renderers/RenderedPanelSpriteInstance.js b/newIDE/app/src/ObjectsRendering/Renderers/RenderedPanelSpriteInstance.js index f8dd915c9b31..f83b8c7d2398 100644 --- a/newIDE/app/src/ObjectsRendering/Renderers/RenderedPanelSpriteInstance.js +++ b/newIDE/app/src/ObjectsRendering/Renderers/RenderedPanelSpriteInstance.js @@ -76,6 +76,10 @@ export default class RenderedPanelSpriteInstance extends RenderedInstance { if (this._width !== oldWidth || this._height !== oldHeight) { this.updateWidthHeight(); } + + // Do not hide completely an object so it can still be manipulated + const alphaForDisplay = Math.max(this._instance.getOpacity() / 255, 0.5); + this._pixiObject.alpha = alphaForDisplay; } makeObjectsAndUpdateTextures() { diff --git a/newIDE/app/src/ObjectsRendering/Renderers/RenderedParticleEmitterInstance.js b/newIDE/app/src/ObjectsRendering/Renderers/RenderedParticleEmitterInstance.js index 8d12e346dec3..4878a641c713 100644 --- a/newIDE/app/src/ObjectsRendering/Renderers/RenderedParticleEmitterInstance.js +++ b/newIDE/app/src/ObjectsRendering/Renderers/RenderedParticleEmitterInstance.js @@ -44,6 +44,10 @@ export default class RenderedParticleEmitterInstance extends RenderedInstance { update() { this._pixiObject.position.x = this._instance.getX(); this._pixiObject.position.y = this._instance.getY(); + // Do not hide completely an object so it can still be manipulated + const alphaForDisplay = Math.max(this._instance.getOpacity() / 255, 0.5); + this._pixiObject.alpha = alphaForDisplay; + this.updateGraphics(); } diff --git a/newIDE/app/src/ObjectsRendering/Renderers/RenderedSprite3DInstance.js b/newIDE/app/src/ObjectsRendering/Renderers/RenderedSprite3DInstance.js index 2ad05e2f116e..b5c93ae5c12a 100644 --- a/newIDE/app/src/ObjectsRendering/Renderers/RenderedSprite3DInstance.js +++ b/newIDE/app/src/ObjectsRendering/Renderers/RenderedSprite3DInstance.js @@ -133,7 +133,11 @@ export default class RenderedSprite3DInstance extends Rendered3DInstance { threeObject.position.y += this._instance.getY(); threeObject.position.z += this._instance.getZ(); - threeObject.scale.set(width, height, 1); + const scaleX = width * (this._instance.isFlippedX() ? -1 : 1); + const scaleY = height * (this._instance.isFlippedY() ? -1 : 1); + const scaleZ = 1 * (this._instance.isFlippedZ() ? -1 : 1); + + threeObject.scale.set(scaleX, scaleY, scaleZ); } updateSprite(): boolean { diff --git a/newIDE/app/src/ObjectsRendering/Renderers/RenderedSpriteInstance.js b/newIDE/app/src/ObjectsRendering/Renderers/RenderedSpriteInstance.js index 2076f86c7cef..802cb7287f8b 100644 --- a/newIDE/app/src/ObjectsRendering/Renderers/RenderedSpriteInstance.js +++ b/newIDE/app/src/ObjectsRendering/Renderers/RenderedSpriteInstance.js @@ -118,6 +118,13 @@ export default class RenderedSpriteInstance extends RenderedInstance { this._pixiObject.position.y = this._instance.getY() + (this._centerY - this._originY) * Math.abs(this._pixiObject.scale.y); + + // Do not hide completely an object so it can still be manipulated + const alphaForDisplay = Math.max(this._instance.getOpacity() / 255, 0.5); + this._pixiObject.alpha = alphaForDisplay; + + if (this._instance.isFlippedX()) this._pixiObject.scale.x *= -1; + if (this._instance.isFlippedY()) this._pixiObject.scale.y *= -1; } updateSprite(): boolean { @@ -235,11 +242,15 @@ export default class RenderedSpriteInstance extends RenderedInstance { getCenterX(): number { if (!this._sprite || !this._pixiObject) return 0; - return this._centerX * this._pixiObject.scale.x; // This is equivalent to `this._animationFrame.center.x * Math.abs(this._scaleX)` in the runtime. + return ( + this._centerX * Math.abs(this._pixiObject.scale.x) // This is equivalent to `this._animationFrame.center.x * Math.abs(this._scaleX)` in the runtime. + ); } getCenterY(): number { if (!this._sprite || !this._pixiObject) return 0; - return this._centerY * this._pixiObject.scale.y; // This is equivalent to `this._animationFrame.center.y * Math.abs(this._scaleY)` in the runtime. + return ( + this._centerY * Math.abs(this._pixiObject.scale.y) // This is equivalent to `this._animationFrame.center.y * Math.abs(this._scaleY)` in the runtime. + ); } } diff --git a/newIDE/app/src/ObjectsRendering/Renderers/RenderedTextInstance.js b/newIDE/app/src/ObjectsRendering/Renderers/RenderedTextInstance.js index 4bc14287a7ea..bf182a0e6a20 100644 --- a/newIDE/app/src/ObjectsRendering/Renderers/RenderedTextInstance.js +++ b/newIDE/app/src/ObjectsRendering/Renderers/RenderedTextInstance.js @@ -209,6 +209,10 @@ export default class RenderedTextInstance extends RenderedInstance { this._pixiObject.rotation = RenderedInstance.toRad( this._instance.getAngle() ); + + // Do not hide completely an object so it can still be manipulated + const alphaForDisplay = Math.max(this._instance.getOpacity() / 255, 0.5); + this._pixiObject.alpha = alphaForDisplay; } getDefaultWidth() { diff --git a/newIDE/app/src/ObjectsRendering/Renderers/RenderedTiledSpriteInstance.js b/newIDE/app/src/ObjectsRendering/Renderers/RenderedTiledSpriteInstance.js index de8478e8f8fc..5d1f7b0fe65e 100644 --- a/newIDE/app/src/ObjectsRendering/Renderers/RenderedTiledSpriteInstance.js +++ b/newIDE/app/src/ObjectsRendering/Renderers/RenderedTiledSpriteInstance.js @@ -89,6 +89,10 @@ export default class RenderedTiledSpriteInstance extends RenderedInstance { this._pixiObject.rotation = RenderedInstance.toRad( this._instance.getAngle() ); + + // Do not hide completely an object so it can still be manipulated + const alphaForDisplay = Math.max(this._instance.getOpacity() / 255, 0.5); + this._pixiObject.alpha = alphaForDisplay; } getDefaultWidth() { diff --git a/newIDE/app/src/UI/CompactSemiControlledNumberField/index.js b/newIDE/app/src/UI/CompactSemiControlledNumberField/index.js index 0de1a3217c1a..c113929759f9 100644 --- a/newIDE/app/src/UI/CompactSemiControlledNumberField/index.js +++ b/newIDE/app/src/UI/CompactSemiControlledNumberField/index.js @@ -48,6 +48,8 @@ type Props = {| useLeftIconAsNumberControl?: boolean, renderEndAdornmentOnHover?: (className: string) => React.Node, onClickEndAdornment?: () => void, + getValueFromDisplayedValue?: string => string, + getDisplayedValueFromValue?: string => string, errorText?: React.Node, |}; @@ -57,6 +59,8 @@ const CompactSemiControlledNumberField = ({ onChange, errorText, commitOnBlur, + getValueFromDisplayedValue, + getDisplayedValueFromValue, ...otherProps }: Props) => { const textFieldRef = React.useRef(null); @@ -137,16 +141,23 @@ const CompactSemiControlledNumberField = ({ [commitOnBlur, onChange] ); + const stringValue = getDisplayedValueFromValue + ? getDisplayedValueFromValue(value.toString()) + : value.toString(); + return (
{ setFocused(true); - setTemporaryValue(value.toString()); + const originalStringValue = getValueFromDisplayedValue + ? getValueFromDisplayedValue(stringValue) + : stringValue; + setTemporaryValue(originalStringValue); }} onKeyDown={event => { if (shouldValidate(event)) { @@ -204,7 +215,10 @@ const CompactSemiControlledNumberField = ({ } }} onBlur={event => { - if (!cancelEditionRef.current) onChangeValue(temporaryValue, 'blur'); + const newValue = getDisplayedValueFromValue + ? getDisplayedValueFromValue(temporaryValue) + : temporaryValue; + if (!cancelEditionRef.current) onChangeValue(newValue, 'blur'); setFocused(false); setTemporaryValue(''); cancelEditionRef.current = false; diff --git a/newIDE/app/src/UI/CompactToggleButtons/CompactToggleButtons.module.css b/newIDE/app/src/UI/CompactToggleButtons/CompactToggleButtons.module.css new file mode 100644 index 000000000000..7e7895b18e4a --- /dev/null +++ b/newIDE/app/src/UI/CompactToggleButtons/CompactToggleButtons.module.css @@ -0,0 +1,42 @@ +.container { + display: flex; + align-items: center; + background-color: var(--theme-text-field-default-background-color); + flex: 1; +} + +.compactToggleButton { + 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; + align-items: center; + justify-content: center; + flex: 1; + min-width: 0px; + margin: 3px; + border: 0; +} + +.separator { + background-color: var(--theme-text-field-disabled-color); + height: 15px; + width: 1px; + margin: 0 1px; +} + +/* svg tag is needed to be first priority compared to Material UI Custom SVG icon classes*/ +svg.icon { + font-size: 20px; + color: var(--theme-text-default-color); + transition: color 0.1s linear; +} + +.compactToggleButton.active { + background-color: var(--theme-icon-button-selected-background-color); +} +.compactToggleButton.active svg.icon { + color: var(--theme-icon-button-selected-color); +} \ No newline at end of file diff --git a/newIDE/app/src/UI/CompactToggleButtons/index.js b/newIDE/app/src/UI/CompactToggleButtons/index.js new file mode 100644 index 000000000000..a0a3ff101814 --- /dev/null +++ b/newIDE/app/src/UI/CompactToggleButtons/index.js @@ -0,0 +1,60 @@ +// @flow + +import * as React from 'react'; +import Tooltip from '@material-ui/core/Tooltip'; +import classNames from 'classnames'; +import classes from './CompactToggleButtons.module.css'; +import { tooltipEnterDelay } from '../Tooltip'; + +type CompactToggleButton = {| + id: string, + renderIcon: (className?: string) => React.Node, + tooltip: React.Node, + onClick: () => void, + isActive: boolean, +|}; +export type CompactToggleButtonsProps = {| + id: string, + buttons: CompactToggleButton[], +|}; + +const CompactToggleButtons = ({ id, buttons }: CompactToggleButtonsProps) => { + return ( +
+ {buttons.map((button, index) => ( + + + + + {index < buttons.length - 1 && ( +
+ )} + + ))} +
+ ); +}; + +export default CompactToggleButtons; diff --git a/newIDE/app/src/UI/CustomSvgIcons/FlipZ.js b/newIDE/app/src/UI/CustomSvgIcons/FlipZ.js new file mode 100644 index 000000000000..c414529bc3b1 --- /dev/null +++ b/newIDE/app/src/UI/CustomSvgIcons/FlipZ.js @@ -0,0 +1,38 @@ +import React from 'react'; +import SvgIcon from '@material-ui/core/SvgIcon'; + +export default React.memo(props => ( + + + + + + +)); diff --git a/newIDE/app/src/UI/CustomSvgIcons/Opacity.js b/newIDE/app/src/UI/CustomSvgIcons/Opacity.js new file mode 100644 index 000000000000..bb850d015301 --- /dev/null +++ b/newIDE/app/src/UI/CustomSvgIcons/Opacity.js @@ -0,0 +1,41 @@ +import React from 'react'; +import SvgIcon from '@material-ui/core/SvgIcon'; + +export default React.memo(props => ( + + + + + + +)); diff --git a/newIDE/app/src/stories/componentStories/UI/CompactToggleButtons.stories.js b/newIDE/app/src/stories/componentStories/UI/CompactToggleButtons.stories.js new file mode 100644 index 000000000000..2b6e767d36ad --- /dev/null +++ b/newIDE/app/src/stories/componentStories/UI/CompactToggleButtons.stories.js @@ -0,0 +1,97 @@ +// @flow +import * as React from 'react'; + +import muiDecorator from '../../ThemeDecorator'; +import paperDecorator from '../../PaperDecorator'; + +import CompactToggleButtons from '../../../UI/CompactToggleButtons'; +import { ColumnStackLayout } from '../../../UI/Layout'; +import Layers from '../../../UI/CustomSvgIcons/Layers'; + +export default { + title: 'UI Building Blocks/CompactToggleButtons', + component: CompactToggleButtons, + decorators: [paperDecorator, muiDecorator], +}; + +export const Default = () => { + const [value, setValue] = React.useState(false); + const [value1, setValue1] = React.useState(true); + const [value2, setValue2] = React.useState(false); + const [value3, setValue3] = React.useState(true); + const [value4, setValue4] = React.useState(false); + const [value5, setValue5] = React.useState(false); + return ( + + , + tooltip: 'Layer', + onClick: () => { + setValue(!value); + }, + isActive: value, + }, + ]} + /> + , + tooltip: 'Layer', + onClick: () => { + setValue1(!value1); + }, + isActive: value1, + }, + { + id: 'button2', + renderIcon: className => , + tooltip: 'Layer', + onClick: () => { + setValue2(!value2); + }, + isActive: value2, + }, + ]} + /> + , + tooltip: 'Layer', + onClick: () => { + setValue3(!value3); + }, + isActive: value3, + }, + { + id: 'button2', + renderIcon: className => , + tooltip: 'Layer', + onClick: () => { + setValue4(!value4); + }, + isActive: value4, + }, + { + id: 'button3', + renderIcon: className => , + tooltip: 'Layer', + onClick: () => { + setValue5(!value5); + }, + isActive: value5, + }, + ]} + /> + + ); +};