[
  {
    "path": ".gitattributes",
    "content": " # Normalize line endings for all files that Git considers text files.\n* text=auto eol=lf\n\n# Exclude irrelevant files when downloading from the Asset Library.\n/README.md                               export-ignore\n/LICENSE                                 export-ignore\n/.gitignore                              export-ignore\n/.gitattributes                          export-ignore\n/addons/goutte.animated_shape_2d/extras  export-ignore\n"
  },
  {
    "path": ".gitignore",
    "content": ""
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2023 Friends of Godette\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "Animated Shape 2D Addon for Godot\n---------------------------------\n\n[![MIT](https://img.shields.io/github/license/Goutte/godot-addon-animated-shape-2d.svg?style=for-the-badge)](https://github.com/Goutte/godot-addon-animated-shape-2d)\n[![Release](https://img.shields.io/github/release/Goutte/godot-addon-animated-shape-2d.svg?style=for-the-badge)](https://github.com/Goutte/godot-addon-animated-shape-2d/releases)\n[![FeedStarvingDev](https://img.shields.io/liberapay/patrons/Goutte.svg?style=for-the-badge&logo=liberapay)](https://liberapay.com/Goutte/)\n\n\nA [Godot](https://godotengine.org/) `4.x` addon that adds an `AnimatedShape2D` that can provide a custom shape for each frame of each animation of an `AnimatedSprite2D`.\n\nIt is useful to make custom hitboxes, hurtboxes, and hardboxes for each pose of your character, if you animated it using `AnimatedSprite2D`.\n\nIt comes with an Editor GUI to preview and edit your shapes, in the fashion of the `SpriteFrames` bottom panel.\n\n![A screenshot of the GUI showing a custom \"Animated Shape\" bottom panel in Godot](./addons/goutte.animated_shape_2d/extras/screenshot_01.png)\n\n\nFeatures\n--------\n\n- customize a shape for each frame of your animations\n- configurable fallbacks\n- editor GUI, updated in real time\n- supports undo & redo where it matters\n- extensible\n\n\nInstall\n-------\n\nThe installation is as usual, through the `Assets Library` within Godot, look for [_AnimatedShape2D_](https://godotengine.org/asset-library/asset/2484).\nYou can also simply copy the files of this project into yours, it should work.\n\nThen, **enable the plugin** in `Scene > Project Settings > Plugins`.\n\n\nUsage\n-----\n\nPlease see the [addons' README](./addons/goutte.animated_shape_2d/README.md).\n\n\n-----\n\n> 🦊 _Feedback and contributions are welcome!_\n\n\n"
  },
  {
    "path": "addons/goutte.animated_shape_2d/README.md",
    "content": "Animated Shape 2D Addon for Godot\n---------------------------------\n\n[![MIT](https://img.shields.io/github/license/Goutte/godot-addon-animated-shape-2d.svg?style=for-the-badge)](https://github.com/Goutte/godot-addon-animated-shape-2d)\n[![Release](https://img.shields.io/github/release/Goutte/godot-addon-animated-shape-2d.svg?style=for-the-badge)](https://github.com/Goutte/godot-addon-animated-shape-2d/releases)\n[![FeedStarvingDev](https://img.shields.io/liberapay/patrons/Goutte.svg?style=for-the-badge&logo=liberapay)](https://liberapay.com/Goutte/)\n\n\nA [Godot](https://godotengine.org/) `^4.2` addon that adds an `AnimatedShape2D` node that can customize a `CollisionShape2D` for each frame of each animation of an `AnimatedSprite2D`.\n\nIt is useful to make custom hitboxes, hurtboxes, and hardboxes for each pose of your character,\nif you animated it using `AnimatedSprite2D`.\n\nIt comes with an Editor GUI to preview your shapes, in the fashion of the `SpriteFrames` bottom panel.\n\nYou can also use it to \"tag\" specific animation frames with custom metadata.\n\n\nFeatures\n--------\n\n- customize a shape for each frame of your animations\n- store metadata for each frame of your animations\n- configurable fallbacks\n- editor GUI, updated in real time\n- copy & pasting, with either shallow of deep copies\n- supports undo & redo where it matters\n- dogfed\n- extensible\n\n\nInstall\n-------\n\nThe installation is as usual, through the `Assets Library` within Godot, look for [_AnimatedShape2D_](https://godotengine.org/asset-library/asset/2484).\nYou can also simply copy the files of this project into yours, it should work.\n\nThen, enable the plugin in `Scene > Project Settings > Plugins`.\n\n\nUsage\n-----\n\n1. Add a `AnimatedShape2D` anywhere in your scene and inspect it.\n2. Target a `AnimatedSprite2D` to read frames from.\n3. Target a `CollisionShape2D` to write to.\n4. Make a new empty `ShapeFrames2D` to store the customization data into.\n5. Add shape customizations to specific frames using the bottom panel.\n6. Star this repository if you are happy ; share the love!\n\n> You can only target one `CollisionShape2D` per `AnimatedShape2D`.\n> Make one `AnimatedShape2D` per type of box you want to customize. _(hitbox, hurtbox, etc.)_\n\n\nHow it Works\n------------\n\n`AnimatedShape2D` stores enough data in a `ShapeFrames2D` resource to fully configure a `CollisionShape2D` for each frame of each animation of an `AnimatedSprite2D`.\n\nIt listens to the `AnimatedSprite2D` frame|animation changes, and updates its target `CollisionShape2D` accordingly.\n\n_That's it._\n\n\n\n-----\n\n> 🦊 _Feedback and contributions are welcome!_\n> https://github.com/Goutte/godot-addon-animated-shape-2d\n\n"
  },
  {
    "path": "addons/goutte.animated_shape_2d/animated_shape_2d.gd",
    "content": "@tool\n@icon(\"./animated_shape_2d.svg\")\nextends Node\nclass_name AnimatedShape2D\n#class_name AnimatedCollisionShape2D\n#class_name AnimatedSprite2DCollisions\n#class_name CollisionShape2DFramer\n\n## Customizes a CollisionShape2D for each frame of an AnimatedSprite2D.\n\n# Usage:\n# 1. Add this node anywhere in your scene\n# 2. Target an input AnimatedSprite2D\n# 3. Target an output CollisionShape2D\n# 4. Load or Create a ShapeFrames2D (it's our database)\n# \n# Notes:\n# - You can put this pretty much anywhere you want in your scene.\n# - This _could_ be a script on a CollisionShape2D, but this way your are free\n#   to have your own script on your collision shape if you want to.\n# - This is quite experimental ; contributions are welcome.\n#   https://github.com/Goutte/godot-addon-animated-shape-2d\n\n## Animated sprite we're going to watch to figure out which shape we want.\n## We're reading the animation name and frame from it.\n@export var animated_sprite: AnimatedSprite2D\n\n## Target collision shape whose shape we're going to write to.\n## We're also going to configure this CollisionShape2D (position, disabled)\n## for each frame of the AnimatedSprite2D above.\n@export var collision_shape: CollisionShape2D\n\n## Shape data for each animation and frame of the animated sprite.\n## This holds enough data to configure the collision shape for each frame\n## of the animated sprite: shape, position, disabled…\n@export var shape_frames: ShapeFrames2D\n\n## If [code]true[/code], use the initial shape in the target CollisionShape2D\n## as fallback when the shape is not defined in the ShapeFrames2D.\n## If [code]false[/code], do not use fallback and therefore disable the shape.\n## This has lower priority than use_previous_as_fallback.\n@export var use_initial_as_fallback := true\n\n## If [code]true[/code], use the previous shape in the target CollisionShape2D\n## as fallback when the shape is not defined in the ShapeFrames2D.\n## If [code]false[/code], do not use fallback and therefore disable the shape.\n## This has higher priority than use_initial_as_fallback.\n## This is handy if for example all your frames use the same shape,\n## and shapes only change per animation.\n@export var use_previous_as_fallback := false\n\n## If [code]true[/code], use call_deferred() to set CollisionShape2D properties.\n@export var use_deferred_calls := true\n\n## Flip horizontally the collision shapes when the animated sprite is flipped,\n## by inverting the scale of their parent Area2D.  Only works on collision\n## shapes that are children of Area2D, to avoid weird behaviors with physics.\n@export var handle_flip_h := true\n\n## Maximum amount of shape size and position change per physics frame.\n## Only used in the [code]INTERPOLATE[/code] mode.\n@export var interpolation_step := 3.0\n\n\nenum SHAPE_UPDATE_MODE {\n\t## Update the existing shape resource properties in the CollisionShape2D,\n\t## but only if shape types are compatible.\n\tUPDATE,\n\t## Works like [code]UPDATE[/code], but interpolates values instead of setting them.\n\t## This helps when sudden, big changes in a collision shape make the physics\n\t## engine glitch and your character starts clipping through the environment.\n\t## Use with [code]interpolation_step[/code].\n\tINTERPOLATE,\n\t## Always replace the existing shape resource in the CollisionShape2D.\n\t## This may trigger additional [code]entered[/code] signals.\n\tREPLACE,\n}\n\n## How the Shape2D resource of the CollisionShape2D is updated between frames.\n## Weird things will happen if you change this at runtime.\n@export var update_shape_mode := SHAPE_UPDATE_MODE.UPDATE\n\n\nvar fallback_shape: Shape2D\nvar fallback_position: Vector2\nvar fallback_disabled: bool\nvar initial_scale: Vector2\nvar collision_shape_parent: Node2D\n\nvar is_tweening_collision_shape_position := false\nvar target_collision_shape_position := Vector2.ZERO\nvar is_tweening_collision_shape_shape := false\nvar target_collision_shape_shape: Shape2D\n\n\nfunc _ready():\n\tif not Engine.is_editor_hint():\n\t\tsetup()\n\t\tupdate_shape()\n\telse:\n\t\tset_physics_process(false)\n\n\nfunc _physics_process(_delta: float):\n\tif self.is_tweening_collision_shape_position:\n\t\ttween_collision_shape_position()\n\tif self.is_tweening_collision_shape_shape:\n\t\ttween_collision_shape_shape()\n\n\nfunc _get_configuration_warnings() -> PackedStringArray:\n\tvar warnings := PackedStringArray()\n\tif self.animated_sprite == null:\n\t\twarnings.append(\"This node requires a target AnimatedSprite2D to read frames from.\")\n\tif self.collision_shape == null:\n\t\twarnings.append(\"This node requires a target CollisionShape2D to write customizations to.\")\n\tif self.shape_frames == null:\n\t\twarnings.append(\"This node requires a ShapeFrames2D to store data.  Make a new one?\")\n\treturn warnings\n\n\nfunc setup():\n\tif self.collision_shape == null:\n\t\treturn\n\tif self.shape_frames == null:\n\t\treturn\n\t\n\t# We might update the original collision shape's shape, so we duplicate\n\tif self.collision_shape.shape:\n\t\tself.fallback_shape = self.collision_shape.shape.duplicate(true)\n\tself.fallback_position = self.collision_shape.position\n\tself.fallback_disabled = self.collision_shape.disabled\n\tself.collision_shape_parent = self.collision_shape.get_parent()\n\tif self.collision_shape_parent != null:\n\t\tself.initial_scale = self.collision_shape_parent.scale\n\t\n\tself.animated_sprite.animation_changed.connect(update_shape)\n\tself.animated_sprite.frame_changed.connect(update_shape)\n\t\n\tset_physics_process(self.update_shape_mode == SHAPE_UPDATE_MODE.INTERPOLATE)\n\n\nfunc get_current_shape_frame() -> ShapeFrame2D:\n\tvar animation_name := self.animated_sprite.get_animation()\n\tvar frame := self.animated_sprite.get_frame()\n\treturn self.shape_frames.get_shape_frame(animation_name, frame)\n\n\nfunc update_shape():\n\tif self.shape_frames == null:\n\t\treturn\n\tvar shape_frame := get_current_shape_frame()\n\t\n\tvar shape: Shape2D = null\n\tif shape_frame != null:\n\t\tshape = shape_frame.get_shape()\n\tvar position := Vector2.ZERO\n\tvar disabled := false\n\tif shape_frame != null:\n\t\tposition = shape_frame.position\n\t\tdisabled = shape_frame.disabled\n\tif shape == null and self.use_previous_as_fallback:\n\t\t# Improvement idea: allow flipping in this case as well\n\t\treturn\n\tif shape == null and self.use_initial_as_fallback:\n\t\tshape = self.fallback_shape\n\t\tposition = self.fallback_position\n\t\tdisabled = self.fallback_disabled\n\t\n\tupdate_collision_shape_shape(shape)\n\tupdate_collision_shape_position(position)\n\tupdate_collision_shape_disabled(disabled)\n\t\n\tif self.handle_flip_h and is_collision_shape_parent_flippable():\n\t\t# Improvement idea: flip the CollisionBody2D itself and mirror its x pos\n\t\tif self.animated_sprite.flip_h:\n\t\t\tself.collision_shape_parent.scale.x = -self.initial_scale.x\n\t\telse:\n\t\t\tself.collision_shape_parent.scale.x = self.initial_scale.x\n\n\nfunc update_collision_shape_disabled(disabled: bool):\n\tif self.use_deferred_calls:\n\t\tself.collision_shape.set_deferred(&\"disabled\", disabled)\n\telse:\n\t\tself.collision_shape.disabled = disabled\n\n\nfunc update_collision_shape_position(new_position: Vector2):\n\tif new_position == self.collision_shape.position:\n\t\treturn\n\t\n\tif self.update_shape_mode == SHAPE_UPDATE_MODE.INTERPOLATE:\n\t\tself.is_tweening_collision_shape_position = true\n\t\tself.target_collision_shape_position = new_position\n\telse:\n\t\tself.collision_shape.position = new_position\n\n\nfunc update_collision_shape_shape(new_shape: Shape2D):\n\tif new_shape == self.collision_shape.shape:\n\t\treturn\n\t\n\tif (\n\t\tself.update_shape_mode == SHAPE_UPDATE_MODE.INTERPOLATE\n\t\tand\n\t\tself.collision_shape.shape != null\n\t\tand\n\t\tnew_shape != null\n\t):\n\t\tif (\n\t\t\t(self.collision_shape.shape.get_class() == new_shape.get_class())\n\t\t):\n\t\t\tself.is_tweening_collision_shape_shape = true\n\t\t\tself.target_collision_shape_shape = new_shape\n\t\t\treturn\n\t\n\tif (\n\t\tself.update_shape_mode == SHAPE_UPDATE_MODE.UPDATE\n\t\tand\n\t\tself.collision_shape.shape != null\n\t\tand\n\t\tnew_shape != null\n\t):\n\t\tif (\n\t\t\t(self.collision_shape.shape is RectangleShape2D)\n\t\t\tand\n\t\t\t(new_shape is RectangleShape2D)\n\t\t):\n\t\t\tself.collision_shape.shape.size = new_shape.size\n\t\t\treturn\n\t\t\n\t\tif (\n\t\t\t(self.collision_shape.shape is CircleShape2D)\n\t\t\tand\n\t\t\t(new_shape is CircleShape2D)\n\t\t):\n\t\t\tself.collision_shape.shape.radius = new_shape.radius\n\t\t\treturn\n\t\t\n\t\tif (\n\t\t\t(self.collision_shape.shape is CapsuleShape2D)\n\t\t\tand\n\t\t\t(new_shape is CapsuleShape2D)\n\t\t):\n\t\t\tself.collision_shape.shape.height = new_shape.height\n\t\t\tself.collision_shape.shape.radius = new_shape.radius\n\t\t\treturn\n\t\t\n\t\tif (\n\t\t\t(self.collision_shape.shape is SegmentShape2D)\n\t\t\tand\n\t\t\t(new_shape is SegmentShape2D)\n\t\t):\n\t\t\tself.collision_shape.shape.a = new_shape.a\n\t\t\tself.collision_shape.shape.b = new_shape.b\n\t\t\treturn\n\t\t\n\t\tif (\n\t\t\t(self.collision_shape.shape is WorldBoundaryShape2D)\n\t\t\tand\n\t\t\t(new_shape is WorldBoundaryShape2D)\n\t\t):\n\t\t\tself.collision_shape.shape.distance = new_shape.distance\n\t\t\tself.collision_shape.shape.normal = new_shape.normal\n\t\t\treturn\n\t\t\n\t\t# If the update cannot be done, we want a duplicate of the shape\n\t\t# because we might update it later on.\n\t\tif use_deferred_calls:\n\t\t\tself.collision_shape.set_deferred(&\"shape\", new_shape.duplicate(true))\n\t\telse:\n\t\t\tself.collision_shape.shape = new_shape.duplicate(true)\n\t\treturn\n\t\n\t# Or perhaps just simply REPLACE the shape.\n\t# This triggers (possibly unwanted) extra area_entered signals.\n\tif use_deferred_calls:\n\t\tself.collision_shape.set_deferred(&\"shape\", new_shape)\n\telse:\n\t\tself.collision_shape.shape = new_shape\n\n\n# Make the shape properties go towards their target, but not by more than\n# the configured interpolation step, to keep things smooth.\n# This method is insanely verbose, but not very complicated.\n# I did not want to use reflection for shorter code but worse perfs.\nfunc tween_collision_shape_shape():\n\tif not self.is_tweening_collision_shape_shape:\n\t\treturn\n\t\n\tif (\n\t\tself.collision_shape.shape == null\n\t\tor\n\t\tself.target_collision_shape_shape == null\n\t):\n\t\treturn\n\t\n\tif (\n\t\t(self.collision_shape.shape is RectangleShape2D)\n\t\tand\n\t\t(self.target_collision_shape_shape is RectangleShape2D)\n\t):\n\t\tself.collision_shape.shape.size.x += clampf(\n\t\t\tself.target_collision_shape_shape.size.x\n\t\t\t-\n\t\t\tself.collision_shape.shape.size.x,\n\t\t\t-self.interpolation_step,\n\t\t\tself.interpolation_step,\n\t\t)\n\t\tself.collision_shape.shape.size.y += clampf(\n\t\t\tself.target_collision_shape_shape.size.y\n\t\t\t-\n\t\t\tself.collision_shape.shape.size.y,\n\t\t\t-self.interpolation_step,\n\t\t\tself.interpolation_step,\n\t\t)\n\t\t\n\t\tif self.collision_shape.shape.size == self.target_collision_shape_shape.size:\n\t\t\tself.is_tweening_collision_shape_shape = false\n\t\t\n\t\treturn\n\t\n\tif (\n\t\t(self.collision_shape.shape is CircleShape2D)\n\t\tand\n\t\t(self.target_collision_shape_shape is CircleShape2D)\n\t):\n\t\tself.collision_shape.shape.radius += clampf(\n\t\t\tself.target_collision_shape_shape.radius\n\t\t\t-\n\t\t\tself.collision_shape.shape.radius,\n\t\t\t-self.interpolation_step,\n\t\t\tself.interpolation_step,\n\t\t)\n\t\t\n\t\tif self.collision_shape.shape.radius == target_collision_shape_shape.radius:\n\t\t\tself.is_tweening_collision_shape_shape = false\n\t\t\n\t\treturn\n\t\n\tif (\n\t\t(self.collision_shape.shape is CapsuleShape2D)\n\t\tand\n\t\t(self.target_collision_shape_shape is CapsuleShape2D)\n\t):\n\t\tself.collision_shape.shape.height += clampf(\n\t\t\tself.target_collision_shape_shape.height\n\t\t\t-\n\t\t\tself.collision_shape.shape.height,\n\t\t\t-self.interpolation_step,\n\t\t\tself.interpolation_step,\n\t\t)\n\t\tself.collision_shape.shape.radius += clampf(\n\t\t\tself.target_collision_shape_shape.radius\n\t\t\t-\n\t\t\tself.collision_shape.shape.radius,\n\t\t\t-self.interpolation_step,\n\t\t\tself.interpolation_step,\n\t\t)\n\t\t\n\t\tif (\n\t\t\tself.collision_shape.shape.radius == target_collision_shape_shape.radius\n\t\t\tand\n\t\t\tself.collision_shape.shape.height == target_collision_shape_shape.height\n\t\t):\n\t\t\tself.is_tweening_collision_shape_shape = false\n\t\t\n\t\treturn\n\t\n\tif (\n\t\t(self.collision_shape.shape is SegmentShape2D)\n\t\tand\n\t\t(self.target_collision_shape_shape is SegmentShape2D)\n\t):\n\t\tself.collision_shape.shape.a.x += clampf(\n\t\t\tself.target_collision_shape_shape.a.x\n\t\t\t-\n\t\t\tself.collision_shape.shape.a.x,\n\t\t\t-self.interpolation_step,\n\t\t\tself.interpolation_step,\n\t\t)\n\t\tself.collision_shape.shape.a.y += clampf(\n\t\t\tself.target_collision_shape_shape.a.y\n\t\t\t-\n\t\t\tself.collision_shape.shape.a.y,\n\t\t\t-self.interpolation_step,\n\t\t\tself.interpolation_step,\n\t\t)\n\t\tself.collision_shape.shape.b.x += clampf(\n\t\t\tself.target_collision_shape_shape.b.x\n\t\t\t-\n\t\t\tself.collision_shape.shape.b.x,\n\t\t\t-self.interpolation_step,\n\t\t\tself.interpolation_step,\n\t\t)\n\t\tself.collision_shape.shape.b.y += clampf(\n\t\t\tself.target_collision_shape_shape.b.y\n\t\t\t-\n\t\t\tself.collision_shape.shape.b.y,\n\t\t\t-self.interpolation_step,\n\t\t\tself.interpolation_step,\n\t\t)\n\t\t\n\t\tif (\n\t\t\tself.collision_shape.shape.a == target_collision_shape_shape.a\n\t\t\tand\n\t\t\tself.collision_shape.shape.b == target_collision_shape_shape.b\n\t\t):\n\t\t\tself.is_tweening_collision_shape_shape = false\n\t\t\n\t\treturn\n\t\n\t# If shape types are incompatible or not supported, cancel the interpolation\n\t# and simply replace the shape, with a duplicate because we might update it.\n\tself.is_tweening_collision_shape_shape = false\n\tself.collision_shape.shape = target_collision_shape_shape.duplicate(true)\n\n\nfunc tween_collision_shape_position():\n\tif not self.is_tweening_collision_shape_position:\n\t\treturn\n\t\n\tself.collision_shape.position.x += clampf(\n\t\tself.target_collision_shape_position.x - self.collision_shape.position.x,\n\t\t-self.interpolation_step,\n\t\tself.interpolation_step,\n\t)\n\tself.collision_shape.position.y += clampf(\n\t\tself.target_collision_shape_position.y - self.collision_shape.position.y,\n\t\t-self.interpolation_step,\n\t\tself.interpolation_step,\n\t)\n\t\n\tif self.collision_shape.position == self.target_collision_shape_position:\n\t\tself.is_tweening_collision_shape_position = false\n\n\n## We don't want to flip PhysicsBodies because it creates odd behaviors.\n## Override this method if that's what you want for some reason.\nfunc is_collision_shape_parent_flippable() -> bool:\n\treturn (\n\t\tself.collision_shape_parent != null\n\t\tand\n\t\tnot (self.collision_shape_parent is PhysicsBody2D)\n\t)\n"
  },
  {
    "path": "addons/goutte.animated_shape_2d/animated_shape_2d.svg.import",
    "content": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://cyjyxgm3by1ae\"\npath=\"res://.godot/imported/animated_shape_2d.svg-8a2942c665c36f113bd6a54b476fbb9b.ctex\"\nmetadata={\n\"vram_texture\": false\n}\n\n[deps]\n\nsource_file=\"res://addons/goutte.animated_shape_2d/animated_shape_2d.svg\"\ndest_files=[\"res://.godot/imported/animated_shape_2d.svg-8a2942c665c36f113bd6a54b476fbb9b.ctex\"]\n\n[params]\n\ncompress/mode=0\ncompress/high_quality=false\ncompress/lossy_quality=0.7\ncompress/hdr_compression=1\ncompress/normal_map=0\ncompress/channel_pack=0\nmipmaps/generate=false\nmipmaps/limit=-1\nroughness/mode=0\nroughness/src_normal=\"\"\nprocess/fix_alpha_border=true\nprocess/premult_alpha=false\nprocess/normal_map_invert_y=false\nprocess/hdr_as_srgb=false\nprocess/hdr_clamp_exposure=false\nprocess/size_limit=0\ndetect_3d/compress_to=1\nsvg/scale=1.0\neditor/scale_with_editor_scale=false\neditor/convert_colors_with_editor_theme=false\n"
  },
  {
    "path": "addons/goutte.animated_shape_2d/editor/icons/copy.png.import",
    "content": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://085a76nwyjqf\"\npath=\"res://.godot/imported/copy.png-2a519d62cfcdeffa08a3d01a8cd47952.ctex\"\nmetadata={\n\"vram_texture\": false\n}\n\n[deps]\n\nsource_file=\"res://addons/goutte.animated_shape_2d/editor/icons/copy.png\"\ndest_files=[\"res://.godot/imported/copy.png-2a519d62cfcdeffa08a3d01a8cd47952.ctex\"]\n\n[params]\n\ncompress/mode=0\ncompress/high_quality=false\ncompress/lossy_quality=0.7\ncompress/hdr_compression=1\ncompress/normal_map=0\ncompress/channel_pack=0\nmipmaps/generate=false\nmipmaps/limit=-1\nroughness/mode=0\nroughness/src_normal=\"\"\nprocess/fix_alpha_border=true\nprocess/premult_alpha=false\nprocess/normal_map_invert_y=false\nprocess/hdr_as_srgb=false\nprocess/hdr_clamp_exposure=false\nprocess/size_limit=0\ndetect_3d/compress_to=1\n"
  },
  {
    "path": "addons/goutte.animated_shape_2d/editor/icons/edit.png.import",
    "content": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://be0cxufi44ytn\"\npath=\"res://.godot/imported/edit.png-96c9f593918026c095b8077c91767aa5.ctex\"\nmetadata={\n\"vram_texture\": false\n}\n\n[deps]\n\nsource_file=\"res://addons/goutte.animated_shape_2d/editor/icons/edit.png\"\ndest_files=[\"res://.godot/imported/edit.png-96c9f593918026c095b8077c91767aa5.ctex\"]\n\n[params]\n\ncompress/mode=0\ncompress/high_quality=false\ncompress/lossy_quality=0.7\ncompress/hdr_compression=1\ncompress/normal_map=0\ncompress/channel_pack=0\nmipmaps/generate=false\nmipmaps/limit=-1\nroughness/mode=0\nroughness/src_normal=\"\"\nprocess/fix_alpha_border=true\nprocess/premult_alpha=false\nprocess/normal_map_invert_y=false\nprocess/hdr_as_srgb=false\nprocess/hdr_clamp_exposure=false\nprocess/size_limit=0\ndetect_3d/compress_to=1\n"
  },
  {
    "path": "addons/goutte.animated_shape_2d/editor/icons/link.png.import",
    "content": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://chl5rhpr6ngqp\"\npath=\"res://.godot/imported/link.png-e080a774f28ccd8863f5e23555455903.ctex\"\nmetadata={\n\"vram_texture\": false\n}\n\n[deps]\n\nsource_file=\"res://addons/goutte.animated_shape_2d/editor/icons/link.png\"\ndest_files=[\"res://.godot/imported/link.png-e080a774f28ccd8863f5e23555455903.ctex\"]\n\n[params]\n\ncompress/mode=0\ncompress/high_quality=false\ncompress/lossy_quality=0.7\ncompress/hdr_compression=1\ncompress/normal_map=0\ncompress/channel_pack=0\nmipmaps/generate=false\nmipmaps/limit=-1\nroughness/mode=0\nroughness/src_normal=\"\"\nprocess/fix_alpha_border=true\nprocess/premult_alpha=false\nprocess/normal_map_invert_y=false\nprocess/hdr_as_srgb=false\nprocess/hdr_clamp_exposure=false\nprocess/size_limit=0\ndetect_3d/compress_to=1\n"
  },
  {
    "path": "addons/goutte.animated_shape_2d/editor/icons/new.png.import",
    "content": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://cbgoa2ilmautt\"\npath=\"res://.godot/imported/new.png-f21616bfd9c8d333fd8a398de8c672bc.ctex\"\nmetadata={\n\"vram_texture\": false\n}\n\n[deps]\n\nsource_file=\"res://addons/goutte.animated_shape_2d/editor/icons/new.png\"\ndest_files=[\"res://.godot/imported/new.png-f21616bfd9c8d333fd8a398de8c672bc.ctex\"]\n\n[params]\n\ncompress/mode=0\ncompress/high_quality=false\ncompress/lossy_quality=0.7\ncompress/hdr_compression=1\ncompress/normal_map=0\ncompress/channel_pack=0\nmipmaps/generate=false\nmipmaps/limit=-1\nroughness/mode=0\nroughness/src_normal=\"\"\nprocess/fix_alpha_border=true\nprocess/premult_alpha=false\nprocess/normal_map_invert_y=false\nprocess/hdr_as_srgb=false\nprocess/hdr_clamp_exposure=false\nprocess/size_limit=0\ndetect_3d/compress_to=1\n"
  },
  {
    "path": "addons/goutte.animated_shape_2d/editor/icons/paste.png.import",
    "content": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://c5mpguq347mtg\"\npath=\"res://.godot/imported/paste.png-02c243ab26b4f6897dc226741e89794a.ctex\"\nmetadata={\n\"vram_texture\": false\n}\n\n[deps]\n\nsource_file=\"res://addons/goutte.animated_shape_2d/editor/icons/paste.png\"\ndest_files=[\"res://.godot/imported/paste.png-02c243ab26b4f6897dc226741e89794a.ctex\"]\n\n[params]\n\ncompress/mode=0\ncompress/high_quality=false\ncompress/lossy_quality=0.7\ncompress/hdr_compression=1\ncompress/normal_map=0\ncompress/channel_pack=0\nmipmaps/generate=false\nmipmaps/limit=-1\nroughness/mode=0\nroughness/src_normal=\"\"\nprocess/fix_alpha_border=true\nprocess/premult_alpha=false\nprocess/normal_map_invert_y=false\nprocess/hdr_as_srgb=false\nprocess/hdr_clamp_exposure=false\nprocess/size_limit=0\ndetect_3d/compress_to=1\n"
  },
  {
    "path": "addons/goutte.animated_shape_2d/editor/icons/remove.png.import",
    "content": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://hsf5o8ys0vo3\"\npath=\"res://.godot/imported/remove.png-b830910b67fe242c64c7a538aae1b97e.ctex\"\nmetadata={\n\"vram_texture\": false\n}\n\n[deps]\n\nsource_file=\"res://addons/goutte.animated_shape_2d/editor/icons/remove.png\"\ndest_files=[\"res://.godot/imported/remove.png-b830910b67fe242c64c7a538aae1b97e.ctex\"]\n\n[params]\n\ncompress/mode=0\ncompress/high_quality=false\ncompress/lossy_quality=0.7\ncompress/hdr_compression=1\ncompress/normal_map=0\ncompress/channel_pack=0\nmipmaps/generate=false\nmipmaps/limit=-1\nroughness/mode=0\nroughness/src_normal=\"\"\nprocess/fix_alpha_border=true\nprocess/premult_alpha=false\nprocess/normal_map_invert_y=false\nprocess/hdr_as_srgb=false\nprocess/hdr_clamp_exposure=false\nprocess/size_limit=0\ndetect_3d/compress_to=1\n"
  },
  {
    "path": "addons/goutte.animated_shape_2d/editor/icons/shift_left.png.import",
    "content": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://btv6bx8bipqrm\"\npath=\"res://.godot/imported/shift_left.png-2904efcb7f02a4fa2b265a34ae875c4f.ctex\"\nmetadata={\n\"vram_texture\": false\n}\n\n[deps]\n\nsource_file=\"res://addons/goutte.animated_shape_2d/editor/icons/shift_left.png\"\ndest_files=[\"res://.godot/imported/shift_left.png-2904efcb7f02a4fa2b265a34ae875c4f.ctex\"]\n\n[params]\n\ncompress/mode=0\ncompress/high_quality=false\ncompress/lossy_quality=0.7\ncompress/hdr_compression=1\ncompress/normal_map=0\ncompress/channel_pack=0\nmipmaps/generate=false\nmipmaps/limit=-1\nroughness/mode=0\nroughness/src_normal=\"\"\nprocess/fix_alpha_border=true\nprocess/premult_alpha=false\nprocess/normal_map_invert_y=false\nprocess/hdr_as_srgb=false\nprocess/hdr_clamp_exposure=false\nprocess/size_limit=0\ndetect_3d/compress_to=1\n"
  },
  {
    "path": "addons/goutte.animated_shape_2d/editor/icons/shift_right.png.import",
    "content": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://q30fj4bowepc\"\npath=\"res://.godot/imported/shift_right.png-de4924e407dbbfc32773a9a30221b8a0.ctex\"\nmetadata={\n\"vram_texture\": false\n}\n\n[deps]\n\nsource_file=\"res://addons/goutte.animated_shape_2d/editor/icons/shift_right.png\"\ndest_files=[\"res://.godot/imported/shift_right.png-de4924e407dbbfc32773a9a30221b8a0.ctex\"]\n\n[params]\n\ncompress/mode=0\ncompress/high_quality=false\ncompress/lossy_quality=0.7\ncompress/hdr_compression=1\ncompress/normal_map=0\ncompress/channel_pack=0\nmipmaps/generate=false\nmipmaps/limit=-1\nroughness/mode=0\nroughness/src_normal=\"\"\nprocess/fix_alpha_border=true\nprocess/premult_alpha=false\nprocess/normal_map_invert_y=false\nprocess/hdr_as_srgb=false\nprocess/hdr_clamp_exposure=false\nprocess/size_limit=0\ndetect_3d/compress_to=1\n"
  },
  {
    "path": "addons/goutte.animated_shape_2d/editor/icons/zoom_less.png.import",
    "content": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://cwgak166wa6hw\"\npath=\"res://.godot/imported/zoom_less.png-ea172f3b8537da096481d713108fafea.ctex\"\nmetadata={\n\"vram_texture\": false\n}\n\n[deps]\n\nsource_file=\"res://addons/goutte.animated_shape_2d/editor/icons/zoom_less.png\"\ndest_files=[\"res://.godot/imported/zoom_less.png-ea172f3b8537da096481d713108fafea.ctex\"]\n\n[params]\n\ncompress/mode=0\ncompress/high_quality=false\ncompress/lossy_quality=0.7\ncompress/hdr_compression=1\ncompress/normal_map=0\ncompress/channel_pack=0\nmipmaps/generate=false\nmipmaps/limit=-1\nroughness/mode=0\nroughness/src_normal=\"\"\nprocess/fix_alpha_border=true\nprocess/premult_alpha=false\nprocess/normal_map_invert_y=false\nprocess/hdr_as_srgb=false\nprocess/hdr_clamp_exposure=false\nprocess/size_limit=0\ndetect_3d/compress_to=1\n"
  },
  {
    "path": "addons/goutte.animated_shape_2d/editor/icons/zoom_more.png.import",
    "content": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://b64sqrouwimqs\"\npath=\"res://.godot/imported/zoom_more.png-dcace68e63f154adcf3f1343a64aab3f.ctex\"\nmetadata={\n\"vram_texture\": false\n}\n\n[deps]\n\nsource_file=\"res://addons/goutte.animated_shape_2d/editor/icons/zoom_more.png\"\ndest_files=[\"res://.godot/imported/zoom_more.png-dcace68e63f154adcf3f1343a64aab3f.ctex\"]\n\n[params]\n\ncompress/mode=0\ncompress/high_quality=false\ncompress/lossy_quality=0.7\ncompress/hdr_compression=1\ncompress/normal_map=0\ncompress/channel_pack=0\nmipmaps/generate=false\nmipmaps/limit=-1\nroughness/mode=0\nroughness/src_normal=\"\"\nprocess/fix_alpha_border=true\nprocess/premult_alpha=false\nprocess/normal_map_invert_y=false\nprocess/hdr_as_srgb=false\nprocess/hdr_clamp_exposure=false\nprocess/size_limit=0\ndetect_3d/compress_to=1\n"
  },
  {
    "path": "addons/goutte.animated_shape_2d/editor/icons/zoom_reset.png.import",
    "content": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://2vmlyocy0aeu\"\npath=\"res://.godot/imported/zoom_reset.png-81fb038c23d931244978fb7781d3ccb4.ctex\"\nmetadata={\n\"vram_texture\": false\n}\n\n[deps]\n\nsource_file=\"res://addons/goutte.animated_shape_2d/editor/icons/zoom_reset.png\"\ndest_files=[\"res://.godot/imported/zoom_reset.png-81fb038c23d931244978fb7781d3ccb4.ctex\"]\n\n[params]\n\ncompress/mode=0\ncompress/high_quality=false\ncompress/lossy_quality=0.7\ncompress/hdr_compression=1\ncompress/normal_map=0\ncompress/channel_pack=0\nmipmaps/generate=false\nmipmaps/limit=-1\nroughness/mode=0\nroughness/src_normal=\"\"\nprocess/fix_alpha_border=true\nprocess/premult_alpha=false\nprocess/normal_map_invert_y=false\nprocess/hdr_as_srgb=false\nprocess/hdr_clamp_exposure=false\nprocess/size_limit=0\ndetect_3d/compress_to=1\n"
  },
  {
    "path": "addons/goutte.animated_shape_2d/editor/linked_frames_feedback.gd",
    "content": "@tool\nextends Node\n\n\n@export var editor: ShapeFramesBottomPanelControl\n\n\nfunc update_for(animation_name: StringName, frame_index: int):\n\tassert(editor.currently_selected_animation_name == animation_name)\n\t# 1. Grab the sprite frame resource of the selected frame\n\tvar frame_editor := editor.get_frame_at(frame_index)\n\tvar shape_frame := frame_editor.get_shape_frame()\n\t# 2. Iterate over all frames to find linked frames\n\tvar linked_frames_editors: Array[ShapeFrameEditor]= []\n\tfor some_frame_editor in editor.frames_list:\n\t\tif shape_frame == null:\n\t\t\tcontinue\n\t\tif some_frame_editor.get_shape_frame() != shape_frame:\n\t\t\tcontinue\n\t\tlinked_frames_editors.append(some_frame_editor)\n\t# 3. Hide the link marker everywhere\n\tfor some_frame_editor in editor.frames_list:\n\t\tsome_frame_editor.hide_link_marker()\n\t# 4. Show the link marker where appropriate\n\tif linked_frames_editors.size() > 1:\n\t\tfor linked_frame_editor in linked_frames_editors:\n\t\t\tlinked_frame_editor.show_link_marker()\n\n\nfunc _on_shape_frames_bottom_panel_control_frame_selected(animation_name: StringName, frame_index: int):\n\tupdate_for(animation_name, frame_index)\n\n\nfunc _on_shape_frames_bottom_panel_control_frame_changed(animation_name, frame_index):\n\tvar selected_frame_editor := editor.get_selected_frame()\n\tupdate_for(selected_frame_editor.animation_name, selected_frame_editor.frame_index)\n\n"
  },
  {
    "path": "addons/goutte.animated_shape_2d/editor/shape_frame_editor.gd",
    "content": "@tool\nextends Control\nclass_name ShapeFrameEditor\n\n## Editor GUI for a single ShapeFrame2D.\n## Shows a preview of the sprite and the shape, as well as action buttons.\n\n\nconst SHAPE_PREVIEW_SCRIPT := preload(\"./shape_preview.gd\")\n\n\n## Animated shape resource we are editing.\n## This holds enough data to configure a CollisionShape2D for a specific frame.\nvar animated_shape: AnimatedShape2D\n## Animation name of the AnimatedSprite2D we're targeting.\nvar animation_name: String\n## Frame of the above animation we are targeting.\nvar frame_index: int\n\n## Zoom level of the preview.  Only integers are supported in there for now.\nvar zoom_level := 1.0\n## Bakground color of the preview.\nvar background_color := Color.WEB_GRAY\n\nvar undo_redo: EditorUndoRedoManager\n\n\nsignal frame_selected\nsignal frame_deselected\nsignal changed\n\n\n## Mandatory dependency injection, since it's best to leave _init() alone.\nfunc configure(\n\tanimated_shape: AnimatedShape2D,\n\tanimation_name: String,\n\tframe_index: int,\n):\n\tself.animated_shape = animated_shape\n\tself.animation_name = animation_name\n\tself.frame_index = frame_index\n\n\n## Optional dependency injection\nfunc set_undo_redo(undo_redo: EditorUndoRedoManager):\n\tself.undo_redo = undo_redo\n\n\nfunc set_zoom_level(new_zoom_level: float):\n\tzoom_level = new_zoom_level\n\n\nfunc set_background_color(new_background_color: Color):\n\tbackground_color = new_background_color\n\n\nfunc _enter_tree():\n\tconnect_to_shape_frame()\n\n\nfunc _exit_tree():\n\tdisconnect_from_shape_frame()\n\tremove_preview_of_shape_frame()\n\tif is_selected():\n\t\tframe_deselected.emit()\n\n\nfunc build(button_group: ButtonGroup):\n\tupdate()\n\t# I had to set the ButtonGroup procedurally, a resource file won't work.\n\t%SpriteButton.button_group = button_group\n\n\nfunc get_shape_frame() -> ShapeFrame2D:\n\tif self.animated_shape == null:\n\t\treturn null\n\tif self.animated_shape.shape_frames == null:\n\t\treturn null\n\treturn self.animated_shape.shape_frames.get_shape_frame(\n\t\tself.animation_name, self.frame_index,\n\t)\n\n\nfunc set_shape_frame(value: ShapeFrame2D):\n\tif self.animated_shape == null:\n\t\treturn\n\tif self.animated_shape.shape_frames == null:\n\t\treturn\n\tdisconnect_from_shape_frame()\n\tself.animated_shape.shape_frames.set_shape_frame(\n\t\tself.animation_name, self.frame_index, value,\n\t)\n\tconnect_to_shape_frame()\n\tupdate()\n\temit_changed()\n\n\n## Connect to the edited Resource, in order to update the GUI in real time.\nfunc connect_to_shape_frame():\n\tvar shape_frame := get_shape_frame()\n\tif shape_frame != null:\n\t\tshape_frame.changed.connect(on_shape_frame_changed)\n\n\n## Disconnect from the edited Resource, to not leave connections hanging.\nfunc disconnect_from_shape_frame():\n\tvar shape_frame := get_shape_frame()\n\tif shape_frame != null:\n\t\tif shape_frame.changed.is_connected(on_shape_frame_changed):\n\t\t\tshape_frame.changed.disconnect(on_shape_frame_changed)\n\n\nfunc is_selected() -> bool:\n\treturn %SpriteButton.button_pressed\n\n\nfunc select():\n\t%SpriteButton.button_pressed = true\n\n\nfunc show_link_marker():\n\t%LinkMarker.show()\n\n\nfunc hide_link_marker():\n\t%LinkMarker.hide()\n\n\n## The crux of the matter ; update the scene according to the data.\nfunc update():\n\tif self.animated_shape == null:\n\t\treturn\n\tif self.animated_shape.animated_sprite == null:\n\t\treturn\n\tif self.animated_shape.animated_sprite.sprite_frames == null:\n\t\treturn\n\t\n\t# I. The actual sprite from the SpriteFrames, for this frame.\n\t%SpriteFrameTexture.texture = self.animated_shape.animated_sprite.sprite_frames.get_frame_texture(self.animation_name, self.frame_index)\n\t%SpriteFrameTexture.custom_minimum_size = self.zoom_level * %SpriteFrameTexture.texture.get_size()\n\t%SpriteFrameTexture.texture_filter = self.animated_shape.animated_sprite.texture_filter\n\tif %SpriteFrameTexture.texture_filter == TEXTURE_FILTER_PARENT_NODE:\n\t\t%SpriteFrameTexture.texture_filter = TEXTURE_FILTER_NEAREST\n\t%SpriteFrameTexture.texture_repeat = self.animated_shape.animated_sprite.texture_repeat\n\tif %SpriteFrameTexture.texture_repeat == TEXTURE_REPEAT_PARENT_NODE:\n\t\t%SpriteFrameTexture.texture_repeat = TEXTURE_REPEAT_DISABLED\n\t\n\t# II. Origin (0, 0) of the parent of the collision shape,\n\t# relative to the sprite, to help positioning our collision shape\n\t# at the right spot relative to the sprite in this preview.\n\tvar collision_shape_parent_transform := Transform2D()\n\tif self.animated_shape.collision_shape.get_parent() is Node2D:\n\t\tcollision_shape_parent_transform = self.animated_shape.collision_shape.get_parent().global_transform\n\t%Origin.transform = (\n\t\tself.animated_shape.animated_sprite.global_transform.affine_inverse()\n\t\t*\n\t\tcollision_shape_parent_transform\n\t)\n\tif self.animated_shape.animated_sprite.centered:\n\t\t%Origin.position += (\n\t\t\tself.animated_shape.animated_sprite.sprite_frames.get_frame_texture(\n\t\t\t\tself.animation_name, self.frame_index,\n\t\t\t).get_size()\n\t\t\t*\n\t\t\t0.5\n\t\t)\n\t%Origin.position -= (\n\t\tself.animated_shape.animated_sprite.offset\n\t)\n\t\n\t# III. Display the preview of the collision shape.\n\tif self.animated_shape.shape_frames == null:\n\t\treturn\n\tvar shape_frame := get_shape_frame()\n\tif shape_frame == null:\n\t\t%ShapeHolder.shape = null\n\telse:\n\t\t%ShapeHolder.shape = shape_frame.get_shape()\n\t\t%ShapeHolder.position = shape_frame.position\n\t\t%ShapeHolder.disabled = shape_frame.disabled\n\t\t%ShapeHolder.debug_color = self.animated_shape.collision_shape.debug_color\n\t\tif shape_frame.debug_color != Color.BLACK:\n\t\t\t%ShapeHolder.debug_color = shape_frame.debug_color\n\t\t\t\n\t\n\t# IV. Adjust the preview to the zoom level\n\t%ZoomAdjuster.scale = Vector2.ONE * self.zoom_level\n\t\n\t# V. Background clear color.\n\t%BackgroundColor.color = self.background_color\n\t\n\t# VI. Tooltip on the main sprite button\n\t%SpriteButton.tooltip_text = \"%s/%d\" % [self.animation_name, self.frame_index]\n\tif shape_frame != null:\n\t\t%SpriteButton.tooltip_text += \" %s\" % [shape_frame]\n\t\t%SpriteButton.tooltip_text += \"\\nClick to edit.\"\n\t\n\t# X. Action button: Create\n\tif shape_frame == null:\n\t\t%CreateButton.visible = true\n\t\t%CreateButton.disabled = false\n\telse:\n\t\t%CreateButton.visible = false\n\t\t%CreateButton.disabled = true\n\t\n\t# XI. Action button: Edit\n\t#if shape_frame == null:\n\t\t#%EditButton.visible = false\n\t\t#%EditButton.disabled = true\n\t#else:\n\t\t#%EditButton.visible = true\n\t\t#%EditButton.disabled = false\n\t\n\t# XII. Action button: Copy\n\tif shape_frame == null:\n\t\t%CopyButton.visible = false\n\t\t%CopyButton.disabled = true\n\telse:\n\t\t%CopyButton.visible = true\n\t\t%CopyButton.disabled = false\n\t\n\t# XIII. Action button: Delete\n\tif shape_frame == null:\n\t\t%DeleteButton.visible = false\n\t\t%DeleteButton.disabled = true\n\telse:\n\t\t%DeleteButton.visible = true\n\t\t%DeleteButton.disabled = false\n\t\n\t# L. 2D View Preview / Mouse GUI Editor\n\tif is_preview_showing():\n\t\tpreview_shape_frame()\n\n\nfunc inspect_shape_frame():\n\tvar shape_frame := get_shape_frame()\n\tif shape_frame == null:\n\t\tif self.animated_shape:\n\t\t\tEditorInterface.edit_node(self.animated_shape)\n\t\treturn\n\tEditorInterface.edit_resource(shape_frame)\n\n\n## The UndoRedo does not like when we use different objects, so we wrap this method here.\n#func set_shape_frame(animation_name: StringName, frame_index: int):\n\t#self.animated_shape.shape_frames.set_(animation_name, frame_index)\n\n\n## The UndoRedo does not like when we use different objects, so we wrap this method here.\nfunc remove_shape_frame():\n\tself.animated_shape.shape_frames.remove_shape_frame(self.animation_name, self.frame_index)\n\n\n#  _____                _\n# |  __ \\              (_)\n# | |__) | __ _____   ___  _____      __\n# |  ___/ '__/ _ \\ \\ / / |/ _ \\ \\ /\\ / /\n# | |   | | |  __/\\ V /| |  __/\\ V  V /\n# |_|   |_|  \\___| \\_/ |_|\\___| \\_/\\_/\n#\n# The big one shown in the 2D Editor when we select this shape frame.\n# This is actually more than a preview since we can *edit* the shape with it.\n\n\nvar sprite_preview: AnimatedSprite2D\nvar preview_background: ColorRect\nvar preview_shape: CollisionShape2D\n\n\nfunc is_preview_showing() -> bool:\n\treturn is_instance_valid(self.sprite_preview)\n\n\nfunc remove_preview_of_shape_frame():\n\tif is_instance_valid(preview_shape):\n\t\tpreview_shape.queue_free()\n\t\tpreview_shape = null\n\tif is_instance_valid(preview_background):\n\t\tpreview_background.queue_free()\n\t\tpreview_background = null\n\tif is_instance_valid(sprite_preview):\n\t\tsprite_preview.queue_free()\n\t\tsprite_preview = null\n\n\nfunc preview_shape_frame():\n\tif not is_instance_valid(sprite_preview):\n\t\tsprite_preview = animated_shape.animated_sprite.duplicate()\n\t\tsprite_preview.name = \"PreviewAnimatedSprite2D\"\n\t\tsprite_preview.owner = null\n\tsprite_preview.animation = self.animation_name\n\tsprite_preview.frame = self.frame_index\n\t\n\tif not is_instance_valid(preview_background):\n\t\tpreview_background = ColorRect.new()\n\t\tpreview_background.name = \"PreviewBackgroundColorRect\"\n\t\tpreview_background.owner = null\n\t\tpreview_background.show_behind_parent = true\n\t\tpreview_background.set_anchors_preset(PRESET_FULL_RECT)\n\t\tif sprite_preview.centered:\n\t\t\tvar s := sprite_preview.sprite_frames.get_frame_texture(sprite_preview.animation, sprite_preview.frame).get_size()\n\t\t\tpreview_background.offset_left -= s.x * 0.5\n\t\t\tpreview_background.offset_right -= s.x * 0.5\n\t\t\tpreview_background.offset_top -= s.y * 0.5\n\t\t\tpreview_background.offset_bottom -= s.y * 0.5\n\t\t# TODO: handle sprite offset too, probably\n\tpreview_background.color = self.background_color\n\t\n\tif preview_background.get_parent() != sprite_preview:\n\t\tif preview_background.get_parent() != null:\n\t\t\tpreview_background.get_parent().remove_child(preview_background)\n\t\tsprite_preview.add_child(preview_background)\n\t\n\tvar shape_frame := get_shape_frame()\n\tif shape_frame != null:\n\t\tif not is_instance_valid(preview_shape):\n\t\t\tpreview_shape = CollisionShape2D.new()\n\t\t\tpreview_shape.name = \"PreviewCollisionShape2D\"\n\t\t\tpreview_shape.set_script(SHAPE_PREVIEW_SCRIPT)\n\t\t\tpreview_shape.rectangle_changed.connect(on_preview_shape_rectangle_changed)\n\t\t\tpreview_shape.item_rect_changed.connect(on_preview_shape_rect_changed)\n\t\t\tself.animated_shape.collision_shape.add_sibling(preview_shape)\n\t\tpreview_shape.shape = shape_frame.shape\n\t\tpreview_shape.position = shape_frame.position\n\t\tpreview_shape.disabled = shape_frame.disabled\n\t\tpreview_shape.debug_color = self.animated_shape.collision_shape.debug_color\n\t\tif shape_frame.debug_color != Color.BLACK:\n\t\t\tpreview_shape.debug_color = shape_frame.debug_color\n\telse:\n\t\tif is_instance_valid(preview_shape):\n\t\t\tpreview_shape.queue_free()\n\t\t\tpreview_shape = null\n\t\n\tif sprite_preview.get_parent() == null:\n\t\tself.animated_shape.animated_sprite.add_sibling(sprite_preview)\n\t\n\tvar selection := EditorInterface.get_selection().get_selected_nodes()\n\tvar already_selected := not selection.is_empty()\n\tif already_selected:\n\t\talready_selected = (selection[0] == preview_shape)\n\tif is_instance_valid(preview_shape) and not already_selected:\n\t\tEditorInterface.get_selection().clear()\n\t\tEditorInterface.get_selection().add_node(preview_shape)\n\t\t# Whatever the doc says, the node already IS inspected.  Might change.\n\t\t# Anyway we don't even WANT to inspect this node, and we have to hack\n\t\t# around this unwanted inspection, see _on_sprite_button_toggled().\n\t\t#EditorInterface.edit_node(preview_shape)\n\t\t\n\t\t# This path was created using the infamous Editor Debugger with a tweak.\n\t\t# We are not using the raw index in parent, but index by class in parent\n\t\t# because it will be a little more resilient to changes in the tree.\n\t\t# This is a hack, and may not play nice with third party plugins.\n\t\t# If you know of another way to enable the mouse move mode, plz share!\n\t\t# We need to use the mouse move mode because the selection mode does\n\t\t# not like non-owned nodes, even if they are _already selected_.\n\t\tvar path := [  # [ class, index_by_class_in_parent ]\n\t\t\t[\"VBoxContainer\", 0], [\"HSplitContainer\", 0],\n\t\t\t[\"HSplitContainer\", 0], [\"HSplitContainer\", 0],\n\t\t\t[\"VBoxContainer\", 0], [\"VSplitContainer\", 0],\n\t\t\t[\"VSplitContainer\", 0], [\"VBoxContainer\", 0],\n\t\t\t[\"PanelContainer\", 0], [\"VBoxContainer\", 0],\n\t\t\t[\"CanvasItemEditor\", 0], [\"MarginContainer\", 0],\n\t\t\t[\"HFlowContainer\", 0], [\"HBoxContainer\", 0],\n\t\t\t[\"Button\", 1],\n\t\t]\n\t\tvar mouse_move_button := get_editor_node_from_path(path) as Button\n\t\tif mouse_move_button != null:\n\t\t\tmouse_move_button.pressed.emit()\n\t\telse:\n\t\t\t# Ouch, the hack above broke, as expected.  Best ignore this.\n\t\t\t# Just make sure you use the Mouse Move Mode when editing the 2D\n\t\t\t# preview, and not the Select Mode (we can't reposition with it).\n\t\t\tpush_warning(\"Mouse Move Button of 2D View was not found.\")\n\n\nfunc on_preview_shape_rectangle_changed():\n\tupdate_from_preview_shape()\n\n\nfunc on_preview_shape_rect_changed():\n\t# I wanna know when this starts working.\n\tprint(\"Oh, now item_rect_changed signal works.  Used to not.\")\n\t# Enable this when it works, and remove workaraound?\n\t#update_from_preview_shape()\n\n\nfunc update_from_preview_shape():\n\tvar shape_frame := get_shape_frame()\n\tif shape_frame == null:\n\t\treturn\n\tif not is_instance_valid(self.preview_shape):\n\t\treturn\n\tshape_frame.position = self.preview_shape.position\n\n\n## Tool (could be static) to fetch a node from a weird path of [type, index],\n## where the index is only amongst nodes of the specified type,\n## to be more resilient to changes in the tree that will break the path.\n## This path is for the Editor only and starts in the base editor control.\n## Use the (modded) \"editor_debug\" addon to get the path (copy typed path). F10\nfunc get_editor_node_from_path(path: Array) -> Node:\n\tvar node := EditorInterface.get_base_control()\n\tfor datum in path:\n\t\tvar node_class: String = datum[0]\n\t\tvar node_index: int = datum[1]\n\t\tvar current_index := 0\n\t\tvar found := false\n\t\tfor child in node.get_children():\n\t\t\tif child.get_class() != node_class:\n\t\t\t\tcontinue\n\t\t\tif current_index == node_index:\n\t\t\t\tnode = child\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\tcurrent_index += 1\n\t\tif not found:\n\t\t\treturn null\n\treturn node\n\n\n#  _      _     _\n# | |    (_)   | |\n# | |     _ ___| |_ ___ _ __   ___ _ __ ___\n# | |    | / __| __/ _ \\ '_ \\ / _ \\ '__/ __|\n# | |____| \\__ \\ ||  __/ | | |  __/ |  \\__ \\\n# |______|_|___/\\__\\___|_| |_|\\___|_|  |___/\n#\n\n## UndoRedo won't accept calling methods on signals, so we'll call this instead.\nfunc emit_changed():\n\tself.changed.emit()\n\nfunc on_shape_frame_changed():\n\tupdate()\n\temit_changed()\n\n\nfunc _on_sprite_button_toggled(toggled_on: bool):\n\tif toggled_on:\n\t\tself.frame_selected.emit()\n\t\tpreview_shape_frame()\n\t\t#inspect_shape_frame()  # nope, the preview has priority somehow\n\t\t#inspect_shape_frame.call_deferred()  # nope too\n\t\t# So, we use this horrendous await that will create bugs:\n\t\tget_tree().create_timer(0.064).timeout.connect(\n\t\t\tfunc():\n\t\t\t\tinspect_shape_frame()\n\t\t)\n\telse:\n\t\tself.frame_deselected.emit()\n\t\tremove_preview_of_shape_frame()\n\n\nfunc _on_create_button_pressed():\n\tvar shape_frame := get_shape_frame()\n\tif shape_frame != null:\n\t\treturn\n\t\n\t# We could also use the UndoRedo here, but…  Hassle > Gain\n\t\n\tshape_frame = ShapeFrame2D.new()\n\tshape_frame.disabled = self.animated_shape.collision_shape.disabled\n\tshape_frame.position = self.animated_shape.collision_shape.position\n\tif self.animated_shape.collision_shape.shape:\n\t\tshape_frame.shape = self.animated_shape.collision_shape.shape.duplicate(true)\n\telse:\n\t\tshape_frame.shape = RectangleShape2D.new()\n\tself.animated_shape.shape_frames.set_shape_frame(\n\t\tself.animation_name, self.frame_index, shape_frame,\n\t)\n\t\n\tupdate()\n\tconnect_to_shape_frame()\n\tinspect_shape_frame()\n\temit_changed()\n\n\nfunc _on_edit_button_pressed():\n\tinspect_shape_frame()\n\n\nfunc _on_copy_button_pressed():\n\tvar shape_frame := get_shape_frame()\n\tif shape_frame == null:\n\t\treturn\n\tDisplayServer.clipboard_set(var_to_str(shape_frame.get_instance_id()))\n\n\nfunc _on_paste_button_pressed():\n\tvar previous_shape_frame := get_shape_frame()\n\tvar copied_instance_id: int = str_to_var(DisplayServer.clipboard_get())\n\tif copied_instance_id == null:\n\t\treturn\n\tvar pasted_shape_frame: ShapeFrame2D = instance_from_id(copied_instance_id)\n\tif pasted_shape_frame == null:\n\t\treturn\n\tif not (pasted_shape_frame is ShapeFrame2D):\n\t\treturn\n\t\n\tif Input.is_key_pressed(KEY_CTRL) or Input.is_key_pressed(KEY_META):\n\t\tpasted_shape_frame = pasted_shape_frame.duplicate(true)\n\t\n\tif self.undo_redo != null:\n\t\tself.undo_redo.create_action(\n\t\t\ttr(\"Paste Shape Frame\"), UndoRedo.MERGE_DISABLE, self,\n\t\t)\n\t\tself.undo_redo.add_do_method(\n\t\t\tself, &\"disconnect_from_shape_frame\",\n\t\t)\n\t\tself.undo_redo.add_do_method(\n\t\t\tself, &\"set_shape_frame\",\n\t\t\tpasted_shape_frame,\n\t\t)\n\t\t#self.undo_redo.add_do_method(\n\t\t\t#self, &\"connect_to_shape_frame\",\n\t\t#)\n\t\t#self.undo_redo.add_do_method(\n\t\t\t#self, &\"update\",\n\t\t#)\n\t\t#self.undo_redo.add_do_method(\n\t\t\t#self, &\"inspect_shape_frame\",\n\t\t#)\n\t\t#self.undo_redo.add_do_method(\n\t\t\t#self, &\"emit_changed\",\n\t\t#)\n\t\tself.undo_redo.add_undo_method(\n\t\t\tself, &\"disconnect_from_shape_frame\",\n\t\t)\n\t\tself.undo_redo.add_undo_method(\n\t\t\tself, &\"set_shape_frame\",\n\t\t\tprevious_shape_frame,\n\t\t)\n\t\t#self.undo_redo.add_undo_method(\n\t\t\t#self, &\"connect_to_shape_frame\",\n\t\t#)\n\t\t#self.undo_redo.add_undo_method(\n\t\t\t#self, &\"update\",\n\t\t#)\n\t\t#self.undo_redo.add_undo_method(\n\t\t\t#self, &\"inspect_shape_frame\",\n\t\t#)\n\t\t#self.undo_redo.add_undo_method(\n\t\t\t#self, &\"emit_changed\",\n\t\t#)\n\t\tself.undo_redo.commit_action()\n\telse:\n\t\t# Same as above, without the UndoRedo shenanigans.\n\t\tdisconnect_from_shape_frame()\n\t\tself.animated_shape.shape_frames.set_shape_frame(\n\t\t\tself.animation_name, self.frame_index, pasted_shape_frame,\n\t\t)\n\t\tconnect_to_shape_frame()\n\t\tupdate()\n\t\temit_changed()\n\n\nfunc _on_delete_button_pressed():\n\tvar shape_frame := get_shape_frame()\n\tif shape_frame == null:\n\t\treturn\n\t\n\tif self.undo_redo != null:\n\t\tself.undo_redo.create_action(\n\t\t\ttr(\"Delete Shape Frame\"), UndoRedo.MERGE_DISABLE, self,\n\t\t)\n\t\tself.undo_redo.add_do_method(\n\t\t\tself, &\"disconnect_from_shape_frame\",\n\t\t)\n\t\tself.undo_redo.add_do_method(\n\t\t\tself, &\"remove_shape_frame\",\n\t\t)\n\t\tself.undo_redo.add_do_method(\n\t\t\tself, &\"update\",\n\t\t)\n\t\tself.undo_redo.add_do_method(\n\t\t\tself, &\"inspect_shape_frame\",\n\t\t)\n\t\tself.undo_redo.add_do_method(\n\t\t\tself, &\"emit_changed\",\n\t\t)\n\t\tself.undo_redo.add_undo_method(\n\t\t\tself, &\"set_shape_frame\",\n\t\t\tshape_frame,\n\t\t)\n\t\t#self.undo_redo.add_undo_method(\n\t\t\t#self, &\"connect_to_shape_frame\",\n\t\t#)\n\t\tself.undo_redo.add_undo_method(\n\t\t\tself, &\"update\",\n\t\t)\n\t\tself.undo_redo.add_undo_method(\n\t\t\tself, &\"inspect_shape_frame\",\n\t\t)\n\t\tself.undo_redo.add_undo_method(\n\t\t\tself, &\"emit_changed\",\n\t\t)\n\t\tself.undo_redo.commit_action()\n\telse:\n\t\t# Same as above, but without the UndoRedo shenanigans\n\t\tdisconnect_from_shape_frame()\n\t\tremove_shape_frame()\n\t\tupdate()\n\t\temit_changed()\n\n"
  },
  {
    "path": "addons/goutte.animated_shape_2d/editor/shape_frame_editor.tscn",
    "content": "[gd_scene load_steps=10 format=3 uid=\"uid://c6fdijn2r2lcs\"]\n\n[ext_resource type=\"Script\" path=\"res://addons/goutte.animated_shape_2d/editor/shape_frame_editor.gd\" id=\"1_pmp4u\"]\n[ext_resource type=\"Texture2D\" uid=\"uid://chl5rhpr6ngqp\" path=\"res://addons/goutte.animated_shape_2d/editor/icons/link.png\" id=\"2_3bsnw\"]\n[ext_resource type=\"Texture2D\" uid=\"uid://hsf5o8ys0vo3\" path=\"res://addons/goutte.animated_shape_2d/editor/icons/remove.png\" id=\"3_bm3kb\"]\n[ext_resource type=\"Texture2D\" uid=\"uid://cbgoa2ilmautt\" path=\"res://addons/goutte.animated_shape_2d/editor/icons/new.png\" id=\"3_j1q7m\"]\n[ext_resource type=\"Texture2D\" uid=\"uid://085a76nwyjqf\" path=\"res://addons/goutte.animated_shape_2d/editor/icons/copy.png\" id=\"4_tt4qt\"]\n[ext_resource type=\"Texture2D\" uid=\"uid://be0cxufi44ytn\" path=\"res://addons/goutte.animated_shape_2d/editor/icons/edit.png\" id=\"5_q0gfy\"]\n[ext_resource type=\"Texture2D\" uid=\"uid://c5mpguq347mtg\" path=\"res://addons/goutte.animated_shape_2d/editor/icons/paste.png\" id=\"5_tdv8b\"]\n\n[sub_resource type=\"StyleBoxFlat\" id=\"StyleBoxFlat_ahkyv\"]\nbg_color = Color(1, 1, 1, 1)\ncorner_radius_top_left = 4\ncorner_radius_top_right = 4\ncorner_radius_bottom_right = 4\ncorner_radius_bottom_left = 4\ncorner_detail = 3\n\n[sub_resource type=\"PlaceholderTexture2D\" id=\"PlaceholderTexture2D_qm6te\"]\nsize = Vector2(256, 256)\n\n[node name=\"ShapeFrameEditor\" type=\"MarginContainer\"]\nlight_mask = 0\noffset_right = 72.0\noffset_bottom = 72.0\nauto_translate = false\nlocalize_numeral_system = false\ntheme_override_constants/margin_left = 4\ntheme_override_constants/margin_top = 4\ntheme_override_constants/margin_right = 4\ntheme_override_constants/margin_bottom = 4\nscript = ExtResource(\"1_pmp4u\")\n\n[node name=\"VBoxContainer\" type=\"VBoxContainer\" parent=\".\"]\nlayout_mode = 2\n\n[node name=\"MarginContainer\" type=\"MarginContainer\" parent=\"VBoxContainer\"]\nlayout_mode = 2\n\n[node name=\"SpriteButton\" type=\"Button\" parent=\"VBoxContainer/MarginContainer\"]\nunique_name_in_owner = true\neditor_description = \"Button Group is set procedurally because setting it here did not work as expected.\"\nclip_contents = true\nlayout_mode = 2\nmouse_default_cursor_shape = 2\ntheme_override_styles/pressed = SubResource(\"StyleBoxFlat_ahkyv\")\ntoggle_mode = true\n\n[node name=\"MarginContainer\" type=\"MarginContainer\" parent=\"VBoxContainer/MarginContainer/SpriteButton\"]\nlayout_mode = 1\nanchors_preset = 15\nanchor_right = 1.0\nanchor_bottom = 1.0\ngrow_horizontal = 2\ngrow_vertical = 2\ntheme_override_constants/margin_left = 3\ntheme_override_constants/margin_top = 3\ntheme_override_constants/margin_right = 3\ntheme_override_constants/margin_bottom = 3\n\n[node name=\"BackgroundColor\" type=\"ColorRect\" parent=\"VBoxContainer/MarginContainer/SpriteButton/MarginContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\nmouse_filter = 2\ncolor = Color(0.341176, 0.341176, 0.341176, 1)\n\n[node name=\"SpriteFrameTexture\" type=\"TextureRect\" parent=\"VBoxContainer/MarginContainer\"]\nunique_name_in_owner = true\nlight_mask = 0\nlayout_mode = 2\nauto_translate = false\nlocalize_numeral_system = false\nmouse_filter = 2\ntexture = SubResource(\"PlaceholderTexture2D_qm6te\")\nstretch_mode = 5\n\n[node name=\"ZoomAdjuster\" type=\"Node2D\" parent=\"VBoxContainer/MarginContainer/SpriteFrameTexture\"]\nunique_name_in_owner = true\n\n[node name=\"Origin\" type=\"Marker2D\" parent=\"VBoxContainer/MarginContainer/SpriteFrameTexture/ZoomAdjuster\"]\nunique_name_in_owner = true\ngizmo_extents = 6.0\n\n[node name=\"ShapeHolder\" type=\"CollisionShape2D\" parent=\"VBoxContainer/MarginContainer/SpriteFrameTexture/ZoomAdjuster/Origin\"]\nunique_name_in_owner = true\n\n[node name=\"Control\" type=\"Control\" parent=\"VBoxContainer/MarginContainer\"]\nlayout_mode = 2\nmouse_filter = 2\n\n[node name=\"LinkMarker\" type=\"TextureRect\" parent=\"VBoxContainer/MarginContainer/Control\"]\nunique_name_in_owner = true\nvisible = false\nself_modulate = Color(1, 1, 0, 1)\nlayout_mode = 1\nanchors_preset = -1\nanchor_left = 1.0\nanchor_right = 1.0\noffset_top = 4.0\noffset_right = -4.0\ngrow_horizontal = 0\ntooltip_text = \"All the frames marked with this icon are linked together ;\nthat is, if you change one, they will all change.\nTo paste without linking, maintain CTRL when pressing the Paste button,\nand it will make a deep copy instead of a shallow (linked) one.\"\ntexture = ExtResource(\"2_3bsnw\")\nmetadata/_edit_lock_ = true\n\n[node name=\"ActionsContainer\" type=\"HBoxContainer\" parent=\"VBoxContainer\"]\nlayout_mode = 2\nalignment = 1\n\n[node name=\"CreateButton\" type=\"Button\" parent=\"VBoxContainer/ActionsContainer\"]\nunique_name_in_owner = true\ntexture_filter = 1\ntexture_repeat = 1\nlayout_mode = 2\ntooltip_text = \"Create a custom collision shape configuration (ShapeFrame2D) for this frame.  It will appear in the Inspector.\"\nfocus_mode = 0\nmouse_default_cursor_shape = 2\nicon = ExtResource(\"3_j1q7m\")\nflat = true\nicon_alignment = 1\n\n[node name=\"EditButton\" type=\"Button\" parent=\"VBoxContainer/ActionsContainer\"]\nunique_name_in_owner = true\nvisible = false\ntexture_filter = 1\ntexture_repeat = 1\nlayout_mode = 2\ntooltip_text = \"Edit the ShapeFrame2D in the inspector.\"\nfocus_mode = 0\nmouse_default_cursor_shape = 2\nicon = ExtResource(\"5_q0gfy\")\nflat = true\nicon_alignment = 1\n\n[node name=\"CopyButton\" type=\"Button\" parent=\"VBoxContainer/ActionsContainer\"]\nunique_name_in_owner = true\ntexture_filter = 1\ntexture_repeat = 1\nlayout_mode = 2\ntooltip_text = \"Copy the collision shape customization of this frame into the clipboard.\"\nfocus_mode = 0\nmouse_default_cursor_shape = 2\nicon = ExtResource(\"4_tt4qt\")\nflat = true\nicon_alignment = 1\n\n[node name=\"PasteButton\" type=\"Button\" parent=\"VBoxContainer/ActionsContainer\"]\nunique_name_in_owner = true\ntexture_filter = 1\ntexture_repeat = 1\nlayout_mode = 2\ntooltip_text = \"Paste the copied collision shape customization from the clipboard into this frame.\nBeware, this pastes a shallow copy and therefore the two frames will now be edited together.\nUse CTRL+Click here to paste a deep copy and decouple the frames.\"\nfocus_mode = 0\nmouse_default_cursor_shape = 2\nicon = ExtResource(\"5_tdv8b\")\nflat = true\nicon_alignment = 1\n\n[node name=\"DeleteButton\" type=\"Button\" parent=\"VBoxContainer/ActionsContainer\"]\nunique_name_in_owner = true\ntexture_filter = 1\ntexture_repeat = 1\nlayout_mode = 2\ntooltip_text = \"Delete the collision shape customization for this frame.\"\nfocus_mode = 0\nmouse_default_cursor_shape = 2\nicon = ExtResource(\"3_bm3kb\")\nflat = true\nicon_alignment = 1\n\n[connection signal=\"toggled\" from=\"VBoxContainer/MarginContainer/SpriteButton\" to=\".\" method=\"_on_sprite_button_toggled\"]\n[connection signal=\"pressed\" from=\"VBoxContainer/ActionsContainer/CreateButton\" to=\".\" method=\"_on_create_button_pressed\"]\n[connection signal=\"pressed\" from=\"VBoxContainer/ActionsContainer/EditButton\" to=\".\" method=\"_on_edit_button_pressed\"]\n[connection signal=\"pressed\" from=\"VBoxContainer/ActionsContainer/CopyButton\" to=\".\" method=\"_on_copy_button_pressed\"]\n[connection signal=\"pressed\" from=\"VBoxContainer/ActionsContainer/PasteButton\" to=\".\" method=\"_on_paste_button_pressed\"]\n[connection signal=\"pressed\" from=\"VBoxContainer/ActionsContainer/DeleteButton\" to=\".\" method=\"_on_delete_button_pressed\"]\n"
  },
  {
    "path": "addons/goutte.animated_shape_2d/editor/shape_frames_bottom_panel_control.gd",
    "content": "@tool\nextends Control\nclass_name ShapeFramesBottomPanelControl\n#class_name ShapeFramesEditor\n\n## Bottom panel for the Editor, shown along with Output, Debugger, etc.\n## Dedicated to editing a single AnimatedShape2D.\n## Will show a list of animation names, and frames for each animation.\n\nconst FRAME_SCENE := preload(\"./shape_frame_editor.tscn\")\n\n@onready var animation_names_item_list: ItemList = %AnimationNamesItemList\n@onready var frames_container := %FramesContainer\n\n## The thing we are previewing and editing.\nvar animated_shape: AnimatedShape2D\n\n## Used to access Editor things like UndoRedo.\n## Someday we will not need this anymore, hopefully.\nvar editor_plugin: EditorPlugin\n\n## Zoom level of the button previews ; only integers for now.  Contribs welcome.\nvar zoom_level := 1.0: set = set_zoom_level\n\n## Customizable background color of previews.\nvar background_color := Color.WEB_GRAY\n\n## Button Group for the various frames, so that only one is selected at a time.\n## We assign this procedurally because assigning it in the scene did not work.\nvar frames_button_group: ButtonGroup\n\nvar currently_selected_animation_name: StringName\n\n## Array of ShapeFrameEditor currently shown, for the selected animation.\n## These are the children of frames_container, except when config is missing.\nvar frames_list: Array[ShapeFrameEditor] = []  # of ShapeFrameEditor\n\n\nsignal frame_selected(animation_name: String, frame_index: int)\nsignal frame_deselected(animation_name: String, frame_index: int)\nsignal frame_changed(animation_name: String, frame_index: int)\n\n\nfunc configure(\n\teditor_plugin: EditorPlugin,\n):\n\tself.editor_plugin = editor_plugin\n\n\nfunc clear():\n\tself.animation_names_item_list.deselect_all()\n\tself.animation_names_item_list.clear()\n\tself.animated_shape = null\n\tclear_shape_frames()\n\n\nfunc clear_shape_frames():\n\t# We iterate the container node and not the frames_list to also remove the\n\t# initial label helpers that appear when the AnimatedShape2D lacks config.\n\tfor child in self.frames_container.get_children():\n\t\tchild.queue_free()\n\tself.frames_list.clear()\n\n\nfunc rebuild_gui(\n\tfor_animated_shape: AnimatedShape2D,\n\tforce := false,\n):\n\tif (self.animated_shape == for_animated_shape) and not force:\n\t\treturn\n\t\n\tclear()\n\tself.animated_shape = for_animated_shape\n\t\n\t#%BackgroundColorPicker.color = …  # from editor settings ?\n\tself.background_color = %BackgroundColorPicker.color\n\t\n\tvar missing_requirements := false\n\tif not is_instance_valid(self.animated_shape.animated_sprite):\n\t\tvar label := Label.new()\n\t\tlabel.text = tr(\"Please assign an input animated sprite to this AnimatedShape2D.\")\n\t\tself.frames_container.add_child(label)\n\t\tmissing_requirements = true\n\tif not is_instance_valid(self.animated_shape.collision_shape):\n\t\tvar label := Label.new()\n\t\tlabel.text = tr(\"Please assign a target collision shape to this AnimatedShape2D.\")\n\t\tself.frames_container.add_child(label)\n\t\tmissing_requirements = true\n\tif not is_instance_valid(self.animated_shape.shape_frames):\n\t\tvar label := Label.new()\n\t\tlabel.text = tr(\"Please create or load shape frames data for this AnimatedShape2D.\")\n\t\tself.frames_container.add_child(label)\n\t\tmissing_requirements = true\n\t\n\tvar inspector := EditorInterface.get_inspector()\n\tif inspector.property_edited.is_connected(on_change_do_reload):\n\t\tinspector.property_edited.disconnect(on_change_do_reload)\n\tif missing_requirements:\n\t\tinspector.property_edited.connect(on_change_do_reload)\n\t\treturn\n\t\n\tvar animation_name := &\"default\"\n\tif is_instance_valid(self.animated_shape.animated_sprite):\n\t\tanimation_name = self.animated_shape.animated_sprite.animation\n\trebuild_animation_names_item_list(self.animated_shape, animation_name)\n\n\nfunc rebuild_animation_names_item_list(\n\tanimated_shape: AnimatedShape2D,\n\tselected_animation_name: String,\n):\n\tif animated_shape == null:\n\t\tprint(\"AnimatedShape2D: no animated shape is configured.\")\n\t\treturn\n\tif animated_shape.animated_sprite == null:\n\t\tprint(\"AnimatedShape2D: no animated sprite is configured.\")\n\t\treturn\n\tif animated_shape.animated_sprite.sprite_frames == null:\n\t\tprint(\"AnimatedShape2D: no sprite frames is configured.\")\n\t\treturn\n\t\n\tvar index := 0\n\tvar selected_index := 0\n\tfor animation_name in animated_shape.animated_sprite.sprite_frames.get_animation_names():\n\t\tself.animation_names_item_list.add_item(animation_name)\n\t\tif animation_name == selected_animation_name:\n\t\t\tselected_index = index\n\t\tindex += 1\n\t\n\tself.animation_names_item_list.select(selected_index)\n\tself.animation_names_item_list.item_selected.emit(selected_index)\n\n\nfunc rebuild_view_of_animation(animation_name: String):\n\tclear_shape_frames()\n\tself.currently_selected_animation_name = animation_name\n\t\n\tself.frames_button_group = ButtonGroup.new()\n\tvar frames_count := self.animated_shape.animated_sprite.sprite_frames.get_frame_count(animation_name)\n\tfor frame_index in frames_count:\n\t\tvar frame_scene := FRAME_SCENE.instantiate() as ShapeFrameEditor\n\t\tframe_scene.configure(self.animated_shape, animation_name, frame_index)\n\t\tframe_scene.set_undo_redo(self.editor_plugin.get_undo_redo())\n\t\tframe_scene.set_zoom_level(self.zoom_level)\n\t\tframe_scene.set_background_color(self.background_color)\n\t\tframe_scene.build(self.frames_button_group)\n\t\tself.frames_container.add_child(frame_scene)\n\t\tself.frames_list.append(frame_scene)\n\t\n\tfor frame_scene: ShapeFrameEditor in self.frames_list:\n\t\tif frame_scene == null:\n\t\t\tcontinue\n\t\tframe_scene.frame_selected.connect(on_frame_selected.bind(frame_scene.animation_name, frame_scene.frame_index))\n\t\tframe_scene.frame_deselected.connect(on_frame_deselected.bind(frame_scene.animation_name, frame_scene.frame_index))\n\t\tframe_scene.changed.connect(on_frame_changed.bind(frame_scene.animation_name, frame_scene.frame_index))\n\n\nfunc rebuild_view_of_animation_by_index(item_index: int):\n\tvar animation_name := self.animation_names_item_list.get_item_text(item_index)\n\trebuild_view_of_animation(animation_name)\n\n\nfunc rebuild_view_of_selected_animation():\n\tvar selected_animations_indices := self.animation_names_item_list.get_selected_items()\n\tif not selected_animations_indices.is_empty():\n\t\trebuild_view_of_animation_by_index(selected_animations_indices[0])\n\n\nfunc set_zoom_level(new_zoom_level: float):\n\tif new_zoom_level == zoom_level:\n\t\treturn\n\tzoom_level = new_zoom_level\n\trebuild_view_of_selected_animation()\n\n\nfunc get_selected_frame() -> ShapeFrameEditor:\n\tfor frame_editor: ShapeFrameEditor in self.frames_list:\n\t\tif frame_editor == null:\n\t\t\tcontinue\n\t\tif frame_editor.is_selected():\n\t\t\treturn frame_editor\n\treturn null\n\n\nfunc get_frame_at(frame_index: int) -> ShapeFrameEditor:\n\tif frame_index < 0:\n\t\treturn null\n\tif frame_index > self.frames_list.size() - 1:\n\t\treturn null\n\treturn self.frames_list[frame_index]\n\n\nfunc shift_frames_from_selected(cursor_direction: int) -> Error:\n\tvar frame_editor := get_selected_frame()\n\tif not is_instance_valid(frame_editor):\n\t\treturn ERR_CANT_ACQUIRE_RESOURCE\n\tif frame_editor.get_shape_frame() == null:\n\t\treturn ERR_DOES_NOT_EXIST\n\tvar shifted := shift_frames(cursor_direction, frame_editor.frame_index)\n\tif shifted != OK:\n\t\treturn shifted\n\tvar new_selected := get_frame_at(frame_editor.frame_index + cursor_direction)\n\tif is_instance_valid(new_selected):\n\t\tnew_selected.select()\n\treturn OK\n\n\n## Shift the frames from frame index, one slot in the specified direction.\nfunc shift_frames(cursor_direction: int, from_frame_index: int) -> Error:\n\tif (cursor_direction != 1) and (cursor_direction != -1):\n\t\tpush_warning(\"AnimatedShape2D: unsupported value for cursor direction.\")\n\t\treturn ERR_INVALID_PARAMETER\n\t\n\t# Collect to frame(s) to shift\n\tvar frames_to_shift := Array()\n\tvar cursor_index := from_frame_index\n\tvar minimum_index := 0\n\tvar maximum_index := self.frames_list.size() - 1\n\tvar ok := false\n\twhile true:\n\t\tif cursor_index < minimum_index:\n\t\t\tbreak\n\t\tif cursor_index > maximum_index:\n\t\t\tbreak\n\t\tvar current_frame_editor: ShapeFrameEditor = self.frames_list[cursor_index]\n\t\tif current_frame_editor.get_shape_frame() == null:\n\t\t\tok = true\n\t\t\tbreak\n\t\tframes_to_shift.append(current_frame_editor)\n\t\tcursor_index += cursor_direction\n\t\n\tif not ok:\n\t\tprint(\"AnimatedShape2D: cancelling shift because there is no room.\")\n\t\treturn ERR_ALREADY_EXISTS\n\t\n\t# Now we can do the actual shifting\n\tfor i in frames_to_shift.size():\n\t\tvar j := cursor_index - i * cursor_direction\n\t\tself.frames_list[j].set_shape_frame(self.frames_list[j-cursor_direction].get_shape_frame())\n\tself.frames_list[cursor_index - frames_to_shift.size() * cursor_direction].set_shape_frame(null)\n\t\n\treturn OK\n\n\nfunc on_frame_selected(animation_name: String, frame_index: int):\n\tframe_selected.emit(animation_name, frame_index)\n\t# Below could be a subscriber to the above signal?\n\tvar shape_frame := animated_shape.shape_frames.get_shape_frame(\n\t\tanimation_name, frame_index,\n\t)\n\tif shape_frame != null:\n\t\t%ShiftLeftButton.disabled = false\n\t\t%ShiftRightButton.disabled = false\n\n\nfunc on_frame_deselected(animation_name: String, frame_index: int):\n\tframe_deselected.emit(animation_name, frame_index)\n\t# Below could be a subscriber to the above signal?\n\t%ShiftLeftButton.disabled = true\n\t%ShiftRightButton.disabled = true\n\n\nfunc on_frame_changed(animation_name: String, frame_index: int):\n\tframe_changed.emit(animation_name, frame_index)\n\n\n## Updates the bottom panel as the user fills the required properties.\n## This listener is only connected when something is missing.\nfunc on_change_do_reload(_property: String):\n\tvar inspector := EditorInterface.get_inspector()\n\tif not (inspector.get_edited_object() is AnimatedShape2D):\n\t\treturn\n\tif inspector.property_edited.is_connected(on_change_do_reload):\n\t\tinspector.property_edited.disconnect(on_change_do_reload)\n\trebuild_gui(self.animated_shape, true)\n\n\nfunc _on_animation_names_item_list_item_selected(index: int):\n\trebuild_view_of_animation_by_index(index)\n\n\nfunc _on_zoom_less_button_pressed():\n\tvar new_zoom_level := self.zoom_level - 1.0\n\t# Note: current zoom logic in TextureRect won't allow going below 1\n\t#if self.zoom_level <= 1.0:\n\t\t#new_zoom_level = self.zoom_level * 0.5\n\t#new_zoom_level = max(0.125, new_zoom_level)\n\tnew_zoom_level = max(1.0, new_zoom_level)\n\tset_zoom_level(new_zoom_level)\n\n\nfunc _on_zoom_reset_button_pressed():\n\tset_zoom_level(1.0)\n\n\nfunc _on_zoom_more_button_pressed():\n\tvar new_zoom_level := self.zoom_level + 1.0\n\t# Note: current zoom logic in TextureRect won't allow going below 1\n\t#if self.zoom_level <= 1.0:\n\t\t#new_zoom_level = self.zoom_level * 2.0\n\t#new_zoom_level = max(0.125, new_zoom_level)\n\tnew_zoom_level = max(1.0, new_zoom_level)\n\tnew_zoom_level = min(10.0, new_zoom_level)\n\tset_zoom_level(new_zoom_level)\n\n\nfunc _on_background_color_picker_color_changed(color: Color):\n\tself.background_color = color\n\trebuild_view_of_selected_animation()\n\n\nfunc _on_shift_left_button_pressed():\n\tshift_frames_from_selected(-1)\n\n\nfunc _on_shift_right_button_pressed():\n\tshift_frames_from_selected(1)\n\n"
  },
  {
    "path": "addons/goutte.animated_shape_2d/editor/shape_frames_bottom_panel_control.tscn",
    "content": "[gd_scene load_steps=8 format=3 uid=\"uid://fh5kcvadxlh3\"]\n\n[ext_resource type=\"Script\" path=\"res://addons/goutte.animated_shape_2d/editor/shape_frames_bottom_panel_control.gd\" id=\"1_5xwm0\"]\n[ext_resource type=\"Script\" path=\"res://addons/goutte.animated_shape_2d/editor/linked_frames_feedback.gd\" id=\"2_njk1o\"]\n[ext_resource type=\"Texture2D\" uid=\"uid://cwgak166wa6hw\" path=\"res://addons/goutte.animated_shape_2d/editor/icons/zoom_less.png\" id=\"2_rtykk\"]\n[ext_resource type=\"Texture2D\" uid=\"uid://b64sqrouwimqs\" path=\"res://addons/goutte.animated_shape_2d/editor/icons/zoom_more.png\" id=\"3_2217o\"]\n[ext_resource type=\"Texture2D\" uid=\"uid://2vmlyocy0aeu\" path=\"res://addons/goutte.animated_shape_2d/editor/icons/zoom_reset.png\" id=\"3_h377a\"]\n[ext_resource type=\"Texture2D\" uid=\"uid://btv6bx8bipqrm\" path=\"res://addons/goutte.animated_shape_2d/editor/icons/shift_left.png\" id=\"5_jj5t1\"]\n[ext_resource type=\"Texture2D\" uid=\"uid://q30fj4bowepc\" path=\"res://addons/goutte.animated_shape_2d/editor/icons/shift_right.png\" id=\"6_66ia2\"]\n\n[node name=\"ShapeFramesBottomPanelControl\" type=\"Control\"]\nlayout_mode = 3\nanchors_preset = 15\nanchor_right = 1.0\nanchor_bottom = 1.0\ngrow_horizontal = 2\ngrow_vertical = 2\nscript = ExtResource(\"1_5xwm0\")\n\n[node name=\"LinkedFramesFeedback\" type=\"Node\" parent=\".\" node_paths=PackedStringArray(\"editor\")]\nscript = ExtResource(\"2_njk1o\")\neditor = NodePath(\"..\")\n\n[node name=\"HSplitContainer\" type=\"HSplitContainer\" parent=\".\"]\nlayout_mode = 1\nanchors_preset = 15\nanchor_right = 1.0\nanchor_bottom = 1.0\ngrow_horizontal = 2\ngrow_vertical = 2\n\n[node name=\"AnimationNamesItemList\" type=\"ItemList\" parent=\"HSplitContainer\"]\nunique_name_in_owner = true\ncustom_minimum_size = Vector2(80, 0)\nlayout_mode = 2\nsize_flags_horizontal = 3\nallow_search = false\n\n[node name=\"MarginContainer\" type=\"VBoxContainer\" parent=\"HSplitContainer\"]\nlayout_mode = 2\nsize_flags_horizontal = 3\nsize_flags_stretch_ratio = 3.0\n\n[node name=\"ActionContainer\" type=\"HBoxContainer\" parent=\"HSplitContainer/MarginContainer\"]\nlayout_mode = 2\n\n[node name=\"ZoomLessButton\" type=\"Button\" parent=\"HSplitContainer/MarginContainer/ActionContainer\"]\nlayout_mode = 2\ntooltip_text = \"Zoom out the previews.\"\nmouse_default_cursor_shape = 2\nicon = ExtResource(\"2_rtykk\")\nflat = true\n\n[node name=\"ZoomResetButton\" type=\"Button\" parent=\"HSplitContainer/MarginContainer/ActionContainer\"]\nlayout_mode = 2\ntooltip_text = \"Reset the zoom level of the previews.\"\nmouse_default_cursor_shape = 2\nicon = ExtResource(\"3_h377a\")\nflat = true\n\n[node name=\"ZoomMoreButton\" type=\"Button\" parent=\"HSplitContainer/MarginContainer/ActionContainer\"]\nlayout_mode = 2\ntooltip_text = \"Zoom in the previews.\"\nmouse_default_cursor_shape = 2\nicon = ExtResource(\"3_2217o\")\nflat = true\n\n[node name=\"VSeparator1\" type=\"VSeparator\" parent=\"HSplitContainer/MarginContainer/ActionContainer\"]\nlayout_mode = 2\n\n[node name=\"BackgroundColorPicker\" type=\"ColorPickerButton\" parent=\"HSplitContainer/MarginContainer/ActionContainer\"]\nunique_name_in_owner = true\ncustom_minimum_size = Vector2(50, 31)\nlayout_mode = 2\ntooltip_text = \"Choose a color for the background of the previews.\"\nfocus_mode = 0\nmouse_default_cursor_shape = 2\ncolor = Color(0.282353, 0.282353, 0.282353, 1)\n\n[node name=\"VSeparator2\" type=\"VSeparator\" parent=\"HSplitContainer/MarginContainer/ActionContainer\"]\nlayout_mode = 2\n\n[node name=\"ShiftLeftButton\" type=\"Button\" parent=\"HSplitContainer/MarginContainer/ActionContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\ntooltip_text = \"Shift the selected shape frame one slot to the left, pushing adjacent shape frames if necessary.\nIf there is no free room to shift to, the shift is cancelled, because\nwe don't want to destroy any shape frame data by shifting over it.\"\nmouse_default_cursor_shape = 2\ndisabled = true\nicon = ExtResource(\"5_jj5t1\")\nflat = true\n\n[node name=\"ShiftRightButton\" type=\"Button\" parent=\"HSplitContainer/MarginContainer/ActionContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\ntooltip_text = \"Shift the selected shape frame one slot to the right, pushing adjacent shape frames if necessary.\nIf there is no free room to shift to, the shift is cancelled, because\nwe don't want to destroy any shape frame data by shifting over it.\"\nmouse_default_cursor_shape = 2\ndisabled = true\nicon = ExtResource(\"6_66ia2\")\nflat = true\n\n[node name=\"ScrollContainer\" type=\"ScrollContainer\" parent=\"HSplitContainer/MarginContainer\"]\nlayout_mode = 2\nsize_flags_vertical = 3\nfollow_focus = true\nhorizontal_scroll_mode = 0\n\n[node name=\"FramesContainer\" type=\"HFlowContainer\" parent=\"HSplitContainer/MarginContainer/ScrollContainer\"]\nunique_name_in_owner = true\nlayout_mode = 2\nsize_flags_horizontal = 3\nsize_flags_vertical = 3\n\n[connection signal=\"frame_changed\" from=\".\" to=\"LinkedFramesFeedback\" method=\"_on_shape_frames_bottom_panel_control_frame_changed\"]\n[connection signal=\"frame_selected\" from=\".\" to=\"LinkedFramesFeedback\" method=\"_on_shape_frames_bottom_panel_control_frame_selected\"]\n[connection signal=\"item_selected\" from=\"HSplitContainer/AnimationNamesItemList\" to=\".\" method=\"_on_animation_names_item_list_item_selected\"]\n[connection signal=\"pressed\" from=\"HSplitContainer/MarginContainer/ActionContainer/ZoomLessButton\" to=\".\" method=\"_on_zoom_less_button_pressed\"]\n[connection signal=\"pressed\" from=\"HSplitContainer/MarginContainer/ActionContainer/ZoomResetButton\" to=\".\" method=\"_on_zoom_reset_button_pressed\"]\n[connection signal=\"pressed\" from=\"HSplitContainer/MarginContainer/ActionContainer/ZoomMoreButton\" to=\".\" method=\"_on_zoom_more_button_pressed\"]\n[connection signal=\"color_changed\" from=\"HSplitContainer/MarginContainer/ActionContainer/BackgroundColorPicker\" to=\".\" method=\"_on_background_color_picker_color_changed\"]\n[connection signal=\"pressed\" from=\"HSplitContainer/MarginContainer/ActionContainer/ShiftLeftButton\" to=\".\" method=\"_on_shift_left_button_pressed\"]\n[connection signal=\"pressed\" from=\"HSplitContainer/MarginContainer/ActionContainer/ShiftRightButton\" to=\".\" method=\"_on_shift_right_button_pressed\"]\n"
  },
  {
    "path": "addons/goutte.animated_shape_2d/editor/shape_preview.gd",
    "content": "@tool\nextends CollisionShape2D\n\n## Added to the temporary CollisionShape2D created in the 2D view of the Editor.\n## Used to recover the position and size changes, since item_rect_changed NOPE.\n## Note that we don't need the size of the shape, it's already propagated.\n## This is only for the position, and perhaps later on rotation and scale ?\n\n\nsignal rectangle_changed\n\n\nfunc _ready():\n\tset_notify_local_transform(true)\n\n\nfunc _notification(what: int):\n\tif what == NOTIFICATION_LOCAL_TRANSFORM_CHANGED:\n\t\trectangle_changed.emit()\n\n"
  },
  {
    "path": "addons/goutte.animated_shape_2d/plugin.cfg",
    "content": "[plugin]\n\nname=\"Animated Shape 2D\"\ndescription=\"Helps making animated shapes to go with your animated sprites.  This allows you to fully configure a CollisionShape2D for each frame of each animation of an AnimatedSprite2D.  It also comes with an Editor, and supports linked frames that you can edit together, any type of shape, and various fallback mechanisms.\"\nauthor=\"Goutte\"\nversion=\"1.5.0\"\nscript=\"plugin.gd\"\n"
  },
  {
    "path": "addons/goutte.animated_shape_2d/plugin.gd",
    "content": "@tool\nextends EditorPlugin\n\n#\n# This plugin adds the following to the Editor:\n# - An AnimatedShape2D Node you can add to your scenes in order to configure\n#   a CollisionShape2D with dynamic values per frame of an AnimatedSprite2D.\n#   This is very handy for making dynamic Hurtboxes, Solidboxes, or Hitboxes.\n# - A GUI for editing a ShapeFrame2D, similar to the SpriteFrames GUI.\n#\n\nconst BOTTOM_PANEL_CONTROL_SCENE := preload(\"./editor/shape_frames_bottom_panel_control.tscn\")\n\nvar bottom_panel_button: Button\nvar bottom_panel_control: Control\n\n\nfunc _enter_tree():\n\t# I. Register the \"Animated Shape\" bottom panel.\n\tself.bottom_panel_control = BOTTOM_PANEL_CONTROL_SCENE.instantiate()\n\tself.bottom_panel_control.configure(self)\n\tself.bottom_panel_button = add_control_to_bottom_panel(\n\t\tself.bottom_panel_control,\n\t\ttr(\"Animated Shape\"),\n\t)\n\t\n\t# II. Show/Hide it depending on what's inspected in the Inspector.\n\t#     This could also work using what's selected in the Scene Tree Editor?\n\ton_inspector_edited_object_changed()\n\tget_editor_interface().get_inspector().edited_object_changed.connect(\n\t\ton_inspector_edited_object_changed,\n\t)\n\n\nfunc _exit_tree():\n\tremove_control_from_bottom_panel(self.bottom_panel_control)\n\t\n\tif get_editor_interface().get_inspector().edited_object_changed.is_connected(\n\t\ton_inspector_edited_object_changed,\n\t):\n\t\tget_editor_interface().get_inspector().edited_object_changed.disconnect(\n\t\t\ton_inspector_edited_object_changed,\n\t\t)\n\n\nfunc update_bottom_panel(animated_shape: AnimatedShape2D):\n\tif animated_shape == null:\n\t\tself.bottom_panel_button.button_pressed = false\n\t\tself.bottom_panel_button.visible = false\n\t\tself.bottom_panel_control.clear()\n\t\treturn\n\t\n\tself.bottom_panel_button.visible = true\n\tself.bottom_panel_button.button_pressed = true\n\tself.bottom_panel_control.rebuild_gui(animated_shape)\n\n\nfunc on_inspector_edited_object_changed():\n\tvar edited_object := get_editor_interface().get_inspector().get_edited_object()\n\tif edited_object is ShapeFrame2D:\n\t\treturn\n\tif edited_object is CollisionShape2D and (edited_object as CollisionShape2D).owner == null:\n\t\treturn  # we're editing the previews' shape\n\tif edited_object is Shape2D:\n\t\treturn  # same\n\tif edited_object == null:\n\t\treturn  # same\n\tvar animated_shape: AnimatedShape2D = null\n\tif edited_object is AnimatedShape2D:\n\t\tanimated_shape = edited_object as AnimatedShape2D\n\tupdate_bottom_panel(animated_shape)\n\n"
  },
  {
    "path": "addons/goutte.animated_shape_2d/shape_frame_2d.gd",
    "content": "@tool\nextends Resource\nclass_name ShapeFrame2D\n#class_name Shape2DFrame\n\n## Data object for a single shape frame.\n## Basically a configurator for a CollisionShape2D.\n## Each frame of each animation of an AnimatedSprite2D will be matched to one\n## of these, in a Dictionary in ShapeFrames2D.\n## You can also use the metadata of this Resource to store custom records per frame.\n\n\n## Position of the collision shape in its parent.\n@export var position := Vector2.ZERO:\n\tset(value):\n\t\tif value != position:\n\t\t\tposition = value\n\t\t\temit_changed()\n\n## Disable the collision shape when [code]true[/code].\n@export var disabled := false:\n\tset(value):\n\t\tif value != disabled:\n\t\t\tdisabled = value\n\t\t\temit_changed()\n\n## Shape of the collision shape.\n@export var shape: Shape2D = null: get = get_shape, set = set_shape\n\nfunc get_shape() -> Shape2D:\n\treturn shape\n\n\nfunc set_shape(value: Shape2D):\n\tshape = value\n\temit_changed()\n\n\n## Override the debug color of the shape.\n## Especially useful when adding metadata to the shape.\n## Black with full opacity disables this override.\n@export var debug_color := Color.BLACK\n\n\n## Used to make dummy ; perhaps keep as a procedural API ?\nstatic func make_rectangle(\n\tsize: Vector2,\n\tposition := Vector2.ZERO,\n\tdisabled := false,\n) -> ShapeFrame2D:\n\tvar sf := ShapeFrame2D.new()\n\tsf.shape = RectangleShape2D.new()\n\tsf.shape.size = size\n\tsf.position = position\n\tsf.disabled = disabled\n\treturn sf\n"
  },
  {
    "path": "addons/goutte.animated_shape_2d/shape_frames_2d.gd",
    "content": "@tool\nextends Resource\nclass_name ShapeFrames2D\n#class_name Shape2DFrames\n\n## Resource holding the configuration of a CollisionShape2D,\n## for each frame of each animation of a SpriteFrames.\n## This is basically a mapping of ShapeFrame2D for each (animation name, frame).\n\n\n## This helps avoiding infinite loops in case you pass INF as frame index.\n## If you hit that limit (what are you doing?!), it is safe to bump it up.\nconst MAXIMUM_FRAMES_AMOUNT := 666\n\n\n## Actual data of this resource.\n## This is a Dictionary of Arrays of ShapeFrame2D, indexed by animation name.\n## In each Array of ShapeFrame2D, there ought to be one ShapeFrame2D per frame\n## in the corresponding animation of the AnimatedSprite2D.\n## It means that the shape of this should follow the shape of the SpriteFrames.\n## [code]\n## {\n## \t'<animation name>': [ ShapeFrame2D, … ],\n## \t…\n## }\n## [/code]\n@export var data := Dictionary()\n\n\n## Returns the shape frame data for the provided animation and frame.\n## This should return null if no data was found.\nfunc get_shape_frame(animation_name: StringName, frame: int) -> ShapeFrame2D:\n\tif self.data.has(animation_name):\n\t\tvar animation_data: Array = self.data[animation_name] as Array\n\t\tif animation_data == null:\n\t\t\treturn null\n\t\tif animation_data.size() > frame:\n\t\t\treturn animation_data[frame] as ShapeFrame2D\n\treturn null\n\n\n## Sets a ShapeFrame2D for the provided animation and frame.\n## When the AnimatedSprite2D shows this frame, the CollisionShape2D will be\n## configured using the provided ShapeFrame2D.  See AnimatedShape2D.\nfunc set_shape_frame(\n\tanimation_name: StringName,\n\tframe: int,\n\tshape_frame: ShapeFrame2D,\n):\n\tif not self.data.has(animation_name):\n\t\tself.data[animation_name] = Array()\n\tvar animation_data: Array = self.data[animation_name] as Array\n\tframe = max(frame, 0)\n\tframe = min(frame, MAXIMUM_FRAMES_AMOUNT)\n\tif animation_data.size() <= frame:\n\t\tanimation_data.resize(frame + 1)\n\tanimation_data[frame] = shape_frame\n\n\n## Removes the ShapeFrame2D datum for the provided key tuple.\n## This will let the frame fall back to configured defaults.\nfunc remove_shape_frame(animation_name: StringName, frame: int):\n\tif not self.data.has(animation_name):\n\t\treturn\n\tvar animation_data: Array = self.data[animation_name] as Array\n\tif animation_data.size() > frame:\n\t\tanimation_data[frame] = null\n\n\n# Example of a tentative procedural API to make this.\n# Prefer using the Editor for now.\n#static func make_dummy() -> ShapeFrames2D:\n\t#var dummy := ShapeFrames2D.new()\n\t#dummy.data = {\n\t\t#&'jump_ascent': [\n\t\t\t#ShapeFrame2D.make_rectangle(Vector2(16, 32), Vector2(0, -17)),\n\t\t\t#ShapeFrame2D.make_rectangle(Vector2(16, 28), Vector2(0, -15)),\n\t\t\t#ShapeFrame2D.make_rectangle(Vector2(16, 22), Vector2(0, -12)),\n\t\t\t#ShapeFrame2D.make_rectangle(Vector2(16, 16), Vector2(0, -9)),\n\t\t\t#ShapeFrame2D.make_rectangle(Vector2(16, 16), Vector2(0, -9)),\n\t\t\t#ShapeFrame2D.make_rectangle(Vector2(16, 16), Vector2(0, -9)),\n\t\t\t#ShapeFrame2D.make_rectangle(Vector2(16, 16), Vector2(0, -9)),\n\t\t\t#ShapeFrame2D.make_rectangle(Vector2(16, 16), Vector2(0, -9)),\n\t\t\t#ShapeFrame2D.make_rectangle(Vector2(16, 16), Vector2(0, -9)),\n\t\t\t#ShapeFrame2D.make_rectangle(Vector2(16, 16), Vector2(0, -9)),\n\t\t\t#ShapeFrame2D.make_rectangle(Vector2(16, 16), Vector2(0, -9)),\n\t\t\t#ShapeFrame2D.make_rectangle(Vector2(16, 16), Vector2(0, -9)),\n\t\t\t#ShapeFrame2D.make_rectangle(Vector2(16, 16), Vector2(0, -9)),\n\t\t#],\n\t\t#&'jump_descent': [\n\t\t\t#ShapeFrame2D.make_rectangle(Vector2(16, 16), Vector2(0, -9)),\n\t\t\t#ShapeFrame2D.make_rectangle(Vector2(16, 16), Vector2(0, -9)),\n\t\t\t#ShapeFrame2D.make_rectangle(Vector2(16, 16), Vector2(0, -9)),\n\t\t\t#ShapeFrame2D.make_rectangle(Vector2(16, 16), Vector2(0, -9)),\n\t\t\t#ShapeFrame2D.make_rectangle(Vector2(16, 22), Vector2(0, -12)),\n\t\t\t#ShapeFrame2D.make_rectangle(Vector2(16, 28), Vector2(0, -15)),\n\t\t\t#ShapeFrame2D.make_rectangle(Vector2(16, 32), Vector2(0, -17)),\n\t\t#],\n\t\t#&'jump_flight': [\n\t\t\t#ShapeFrame2D.make_rectangle(Vector2(16, 16), Vector2(0, -9)),\n\t\t\t#ShapeFrame2D.make_rectangle(Vector2(16, 16), Vector2(0, -9)),\n\t\t\t#ShapeFrame2D.make_rectangle(Vector2(16, 16), Vector2(0, -9)),\n\t\t\t#ShapeFrame2D.make_rectangle(Vector2(16, 16), Vector2(0, -9)),\n\t\t#],\n\t#}\n\t#return dummy\n"
  }
]