main 4ab90a80b815 cached
28 files
80.6 KB
22.2k tokens
1 requests
Download .txt
Repository: Goutte/godot-addon-animated-shape-2d
Branch: main
Commit: 4ab90a80b815
Files: 28
Total size: 80.6 KB

Directory structure:
gitextract_dzyrmz1o/

├── .gitattributes
├── .gitignore
├── LICENSE
├── README.md
└── addons/
    └── goutte.animated_shape_2d/
        ├── README.md
        ├── animated_shape_2d.gd
        ├── animated_shape_2d.svg.import
        ├── editor/
        │   ├── icons/
        │   │   ├── copy.png.import
        │   │   ├── edit.png.import
        │   │   ├── link.png.import
        │   │   ├── new.png.import
        │   │   ├── paste.png.import
        │   │   ├── remove.png.import
        │   │   ├── shift_left.png.import
        │   │   ├── shift_right.png.import
        │   │   ├── zoom_less.png.import
        │   │   ├── zoom_more.png.import
        │   │   └── zoom_reset.png.import
        │   ├── linked_frames_feedback.gd
        │   ├── shape_frame_editor.gd
        │   ├── shape_frame_editor.tscn
        │   ├── shape_frames_bottom_panel_control.gd
        │   ├── shape_frames_bottom_panel_control.tscn
        │   └── shape_preview.gd
        ├── plugin.cfg
        ├── plugin.gd
        ├── shape_frame_2d.gd
        └── shape_frames_2d.gd

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitattributes
================================================
 # Normalize line endings for all files that Git considers text files.
* text=auto eol=lf

# Exclude irrelevant files when downloading from the Asset Library.
/README.md                               export-ignore
/LICENSE                                 export-ignore
/.gitignore                              export-ignore
/.gitattributes                          export-ignore
/addons/goutte.animated_shape_2d/extras  export-ignore


================================================
FILE: .gitignore
================================================


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2023 Friends of Godette

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
Animated Shape 2D Addon for Godot
---------------------------------

[![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)
[![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)
[![FeedStarvingDev](https://img.shields.io/liberapay/patrons/Goutte.svg?style=for-the-badge&logo=liberapay)](https://liberapay.com/Goutte/)


A [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`.

It is useful to make custom hitboxes, hurtboxes, and hardboxes for each pose of your character, if you animated it using `AnimatedSprite2D`.

It comes with an Editor GUI to preview and edit your shapes, in the fashion of the `SpriteFrames` bottom panel.

![A screenshot of the GUI showing a custom "Animated Shape" bottom panel in Godot](./addons/goutte.animated_shape_2d/extras/screenshot_01.png)


Features
--------

- customize a shape for each frame of your animations
- configurable fallbacks
- editor GUI, updated in real time
- supports undo & redo where it matters
- extensible


Install
-------

The installation is as usual, through the `Assets Library` within Godot, look for [_AnimatedShape2D_](https://godotengine.org/asset-library/asset/2484).
You can also simply copy the files of this project into yours, it should work.

Then, **enable the plugin** in `Scene > Project Settings > Plugins`.


Usage
-----

Please see the [addons' README](./addons/goutte.animated_shape_2d/README.md).


-----

> 🦊 _Feedback and contributions are welcome!_




================================================
FILE: addons/goutte.animated_shape_2d/README.md
================================================
Animated Shape 2D Addon for Godot
---------------------------------

[![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)
[![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)
[![FeedStarvingDev](https://img.shields.io/liberapay/patrons/Goutte.svg?style=for-the-badge&logo=liberapay)](https://liberapay.com/Goutte/)


A [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`.

It is useful to make custom hitboxes, hurtboxes, and hardboxes for each pose of your character,
if you animated it using `AnimatedSprite2D`.

It comes with an Editor GUI to preview your shapes, in the fashion of the `SpriteFrames` bottom panel.

You can also use it to "tag" specific animation frames with custom metadata.


Features
--------

- customize a shape for each frame of your animations
- store metadata for each frame of your animations
- configurable fallbacks
- editor GUI, updated in real time
- copy & pasting, with either shallow of deep copies
- supports undo & redo where it matters
- dogfed
- extensible


Install
-------

The installation is as usual, through the `Assets Library` within Godot, look for [_AnimatedShape2D_](https://godotengine.org/asset-library/asset/2484).
You can also simply copy the files of this project into yours, it should work.

Then, enable the plugin in `Scene > Project Settings > Plugins`.


Usage
-----

1. Add a `AnimatedShape2D` anywhere in your scene and inspect it.
2. Target a `AnimatedSprite2D` to read frames from.
3. Target a `CollisionShape2D` to write to.
4. Make a new empty `ShapeFrames2D` to store the customization data into.
5. Add shape customizations to specific frames using the bottom panel.
6. Star this repository if you are happy ; share the love!

> You can only target one `CollisionShape2D` per `AnimatedShape2D`.
> Make one `AnimatedShape2D` per type of box you want to customize. _(hitbox, hurtbox, etc.)_


How it Works
------------

`AnimatedShape2D` stores enough data in a `ShapeFrames2D` resource to fully configure a `CollisionShape2D` for each frame of each animation of an `AnimatedSprite2D`.

It listens to the `AnimatedSprite2D` frame|animation changes, and updates its target `CollisionShape2D` accordingly.

_That's it._



-----

> 🦊 _Feedback and contributions are welcome!_
> https://github.com/Goutte/godot-addon-animated-shape-2d



================================================
FILE: addons/goutte.animated_shape_2d/animated_shape_2d.gd
================================================
@tool
@icon("./animated_shape_2d.svg")
extends Node
class_name AnimatedShape2D
#class_name AnimatedCollisionShape2D
#class_name AnimatedSprite2DCollisions
#class_name CollisionShape2DFramer

## Customizes a CollisionShape2D for each frame of an AnimatedSprite2D.

# Usage:
# 1. Add this node anywhere in your scene
# 2. Target an input AnimatedSprite2D
# 3. Target an output CollisionShape2D
# 4. Load or Create a ShapeFrames2D (it's our database)
# 
# Notes:
# - You can put this pretty much anywhere you want in your scene.
# - This _could_ be a script on a CollisionShape2D, but this way your are free
#   to have your own script on your collision shape if you want to.
# - This is quite experimental ; contributions are welcome.
#   https://github.com/Goutte/godot-addon-animated-shape-2d

## Animated sprite we're going to watch to figure out which shape we want.
## We're reading the animation name and frame from it.
@export var animated_sprite: AnimatedSprite2D

## Target collision shape whose shape we're going to write to.
## We're also going to configure this CollisionShape2D (position, disabled)
## for each frame of the AnimatedSprite2D above.
@export var collision_shape: CollisionShape2D

## Shape data for each animation and frame of the animated sprite.
## This holds enough data to configure the collision shape for each frame
## of the animated sprite: shape, position, disabled…
@export var shape_frames: ShapeFrames2D

## If [code]true[/code], use the initial shape in the target CollisionShape2D
## as fallback when the shape is not defined in the ShapeFrames2D.
## If [code]false[/code], do not use fallback and therefore disable the shape.
## This has lower priority than use_previous_as_fallback.
@export var use_initial_as_fallback := true

## If [code]true[/code], use the previous shape in the target CollisionShape2D
## as fallback when the shape is not defined in the ShapeFrames2D.
## If [code]false[/code], do not use fallback and therefore disable the shape.
## This has higher priority than use_initial_as_fallback.
## This is handy if for example all your frames use the same shape,
## and shapes only change per animation.
@export var use_previous_as_fallback := false

## If [code]true[/code], use call_deferred() to set CollisionShape2D properties.
@export var use_deferred_calls := true

## Flip horizontally the collision shapes when the animated sprite is flipped,
## by inverting the scale of their parent Area2D.  Only works on collision
## shapes that are children of Area2D, to avoid weird behaviors with physics.
@export var handle_flip_h := true

## Maximum amount of shape size and position change per physics frame.
## Only used in the [code]INTERPOLATE[/code] mode.
@export var interpolation_step := 3.0


enum SHAPE_UPDATE_MODE {
	## Update the existing shape resource properties in the CollisionShape2D,
	## but only if shape types are compatible.
	UPDATE,
	## Works like [code]UPDATE[/code], but interpolates values instead of setting them.
	## This helps when sudden, big changes in a collision shape make the physics
	## engine glitch and your character starts clipping through the environment.
	## Use with [code]interpolation_step[/code].
	INTERPOLATE,
	## Always replace the existing shape resource in the CollisionShape2D.
	## This may trigger additional [code]entered[/code] signals.
	REPLACE,
}

## How the Shape2D resource of the CollisionShape2D is updated between frames.
## Weird things will happen if you change this at runtime.
@export var update_shape_mode := SHAPE_UPDATE_MODE.UPDATE


var fallback_shape: Shape2D
var fallback_position: Vector2
var fallback_disabled: bool
var initial_scale: Vector2
var collision_shape_parent: Node2D

var is_tweening_collision_shape_position := false
var target_collision_shape_position := Vector2.ZERO
var is_tweening_collision_shape_shape := false
var target_collision_shape_shape: Shape2D


func _ready():
	if not Engine.is_editor_hint():
		setup()
		update_shape()
	else:
		set_physics_process(false)


func _physics_process(_delta: float):
	if self.is_tweening_collision_shape_position:
		tween_collision_shape_position()
	if self.is_tweening_collision_shape_shape:
		tween_collision_shape_shape()


func _get_configuration_warnings() -> PackedStringArray:
	var warnings := PackedStringArray()
	if self.animated_sprite == null:
		warnings.append("This node requires a target AnimatedSprite2D to read frames from.")
	if self.collision_shape == null:
		warnings.append("This node requires a target CollisionShape2D to write customizations to.")
	if self.shape_frames == null:
		warnings.append("This node requires a ShapeFrames2D to store data.  Make a new one?")
	return warnings


func setup():
	if self.collision_shape == null:
		return
	if self.shape_frames == null:
		return
	
	# We might update the original collision shape's shape, so we duplicate
	if self.collision_shape.shape:
		self.fallback_shape = self.collision_shape.shape.duplicate(true)
	self.fallback_position = self.collision_shape.position
	self.fallback_disabled = self.collision_shape.disabled
	self.collision_shape_parent = self.collision_shape.get_parent()
	if self.collision_shape_parent != null:
		self.initial_scale = self.collision_shape_parent.scale
	
	self.animated_sprite.animation_changed.connect(update_shape)
	self.animated_sprite.frame_changed.connect(update_shape)
	
	set_physics_process(self.update_shape_mode == SHAPE_UPDATE_MODE.INTERPOLATE)


func get_current_shape_frame() -> ShapeFrame2D:
	var animation_name := self.animated_sprite.get_animation()
	var frame := self.animated_sprite.get_frame()
	return self.shape_frames.get_shape_frame(animation_name, frame)


func update_shape():
	if self.shape_frames == null:
		return
	var shape_frame := get_current_shape_frame()
	
	var shape: Shape2D = null
	if shape_frame != null:
		shape = shape_frame.get_shape()
	var position := Vector2.ZERO
	var disabled := false
	if shape_frame != null:
		position = shape_frame.position
		disabled = shape_frame.disabled
	if shape == null and self.use_previous_as_fallback:
		# Improvement idea: allow flipping in this case as well
		return
	if shape == null and self.use_initial_as_fallback:
		shape = self.fallback_shape
		position = self.fallback_position
		disabled = self.fallback_disabled
	
	update_collision_shape_shape(shape)
	update_collision_shape_position(position)
	update_collision_shape_disabled(disabled)
	
	if self.handle_flip_h and is_collision_shape_parent_flippable():
		# Improvement idea: flip the CollisionBody2D itself and mirror its x pos
		if self.animated_sprite.flip_h:
			self.collision_shape_parent.scale.x = -self.initial_scale.x
		else:
			self.collision_shape_parent.scale.x = self.initial_scale.x


func update_collision_shape_disabled(disabled: bool):
	if self.use_deferred_calls:
		self.collision_shape.set_deferred(&"disabled", disabled)
	else:
		self.collision_shape.disabled = disabled


func update_collision_shape_position(new_position: Vector2):
	if new_position == self.collision_shape.position:
		return
	
	if self.update_shape_mode == SHAPE_UPDATE_MODE.INTERPOLATE:
		self.is_tweening_collision_shape_position = true
		self.target_collision_shape_position = new_position
	else:
		self.collision_shape.position = new_position


func update_collision_shape_shape(new_shape: Shape2D):
	if new_shape == self.collision_shape.shape:
		return
	
	if (
		self.update_shape_mode == SHAPE_UPDATE_MODE.INTERPOLATE
		and
		self.collision_shape.shape != null
		and
		new_shape != null
	):
		if (
			(self.collision_shape.shape.get_class() == new_shape.get_class())
		):
			self.is_tweening_collision_shape_shape = true
			self.target_collision_shape_shape = new_shape
			return
	
	if (
		self.update_shape_mode == SHAPE_UPDATE_MODE.UPDATE
		and
		self.collision_shape.shape != null
		and
		new_shape != null
	):
		if (
			(self.collision_shape.shape is RectangleShape2D)
			and
			(new_shape is RectangleShape2D)
		):
			self.collision_shape.shape.size = new_shape.size
			return
		
		if (
			(self.collision_shape.shape is CircleShape2D)
			and
			(new_shape is CircleShape2D)
		):
			self.collision_shape.shape.radius = new_shape.radius
			return
		
		if (
			(self.collision_shape.shape is CapsuleShape2D)
			and
			(new_shape is CapsuleShape2D)
		):
			self.collision_shape.shape.height = new_shape.height
			self.collision_shape.shape.radius = new_shape.radius
			return
		
		if (
			(self.collision_shape.shape is SegmentShape2D)
			and
			(new_shape is SegmentShape2D)
		):
			self.collision_shape.shape.a = new_shape.a
			self.collision_shape.shape.b = new_shape.b
			return
		
		if (
			(self.collision_shape.shape is WorldBoundaryShape2D)
			and
			(new_shape is WorldBoundaryShape2D)
		):
			self.collision_shape.shape.distance = new_shape.distance
			self.collision_shape.shape.normal = new_shape.normal
			return
		
		# If the update cannot be done, we want a duplicate of the shape
		# because we might update it later on.
		if use_deferred_calls:
			self.collision_shape.set_deferred(&"shape", new_shape.duplicate(true))
		else:
			self.collision_shape.shape = new_shape.duplicate(true)
		return
	
	# Or perhaps just simply REPLACE the shape.
	# This triggers (possibly unwanted) extra area_entered signals.
	if use_deferred_calls:
		self.collision_shape.set_deferred(&"shape", new_shape)
	else:
		self.collision_shape.shape = new_shape


# Make the shape properties go towards their target, but not by more than
# the configured interpolation step, to keep things smooth.
# This method is insanely verbose, but not very complicated.
# I did not want to use reflection for shorter code but worse perfs.
func tween_collision_shape_shape():
	if not self.is_tweening_collision_shape_shape:
		return
	
	if (
		self.collision_shape.shape == null
		or
		self.target_collision_shape_shape == null
	):
		return
	
	if (
		(self.collision_shape.shape is RectangleShape2D)
		and
		(self.target_collision_shape_shape is RectangleShape2D)
	):
		self.collision_shape.shape.size.x += clampf(
			self.target_collision_shape_shape.size.x
			-
			self.collision_shape.shape.size.x,
			-self.interpolation_step,
			self.interpolation_step,
		)
		self.collision_shape.shape.size.y += clampf(
			self.target_collision_shape_shape.size.y
			-
			self.collision_shape.shape.size.y,
			-self.interpolation_step,
			self.interpolation_step,
		)
		
		if self.collision_shape.shape.size == self.target_collision_shape_shape.size:
			self.is_tweening_collision_shape_shape = false
		
		return
	
	if (
		(self.collision_shape.shape is CircleShape2D)
		and
		(self.target_collision_shape_shape is CircleShape2D)
	):
		self.collision_shape.shape.radius += clampf(
			self.target_collision_shape_shape.radius
			-
			self.collision_shape.shape.radius,
			-self.interpolation_step,
			self.interpolation_step,
		)
		
		if self.collision_shape.shape.radius == target_collision_shape_shape.radius:
			self.is_tweening_collision_shape_shape = false
		
		return
	
	if (
		(self.collision_shape.shape is CapsuleShape2D)
		and
		(self.target_collision_shape_shape is CapsuleShape2D)
	):
		self.collision_shape.shape.height += clampf(
			self.target_collision_shape_shape.height
			-
			self.collision_shape.shape.height,
			-self.interpolation_step,
			self.interpolation_step,
		)
		self.collision_shape.shape.radius += clampf(
			self.target_collision_shape_shape.radius
			-
			self.collision_shape.shape.radius,
			-self.interpolation_step,
			self.interpolation_step,
		)
		
		if (
			self.collision_shape.shape.radius == target_collision_shape_shape.radius
			and
			self.collision_shape.shape.height == target_collision_shape_shape.height
		):
			self.is_tweening_collision_shape_shape = false
		
		return
	
	if (
		(self.collision_shape.shape is SegmentShape2D)
		and
		(self.target_collision_shape_shape is SegmentShape2D)
	):
		self.collision_shape.shape.a.x += clampf(
			self.target_collision_shape_shape.a.x
			-
			self.collision_shape.shape.a.x,
			-self.interpolation_step,
			self.interpolation_step,
		)
		self.collision_shape.shape.a.y += clampf(
			self.target_collision_shape_shape.a.y
			-
			self.collision_shape.shape.a.y,
			-self.interpolation_step,
			self.interpolation_step,
		)
		self.collision_shape.shape.b.x += clampf(
			self.target_collision_shape_shape.b.x
			-
			self.collision_shape.shape.b.x,
			-self.interpolation_step,
			self.interpolation_step,
		)
		self.collision_shape.shape.b.y += clampf(
			self.target_collision_shape_shape.b.y
			-
			self.collision_shape.shape.b.y,
			-self.interpolation_step,
			self.interpolation_step,
		)
		
		if (
			self.collision_shape.shape.a == target_collision_shape_shape.a
			and
			self.collision_shape.shape.b == target_collision_shape_shape.b
		):
			self.is_tweening_collision_shape_shape = false
		
		return
	
	# If shape types are incompatible or not supported, cancel the interpolation
	# and simply replace the shape, with a duplicate because we might update it.
	self.is_tweening_collision_shape_shape = false
	self.collision_shape.shape = target_collision_shape_shape.duplicate(true)


func tween_collision_shape_position():
	if not self.is_tweening_collision_shape_position:
		return
	
	self.collision_shape.position.x += clampf(
		self.target_collision_shape_position.x - self.collision_shape.position.x,
		-self.interpolation_step,
		self.interpolation_step,
	)
	self.collision_shape.position.y += clampf(
		self.target_collision_shape_position.y - self.collision_shape.position.y,
		-self.interpolation_step,
		self.interpolation_step,
	)
	
	if self.collision_shape.position == self.target_collision_shape_position:
		self.is_tweening_collision_shape_position = false


## We don't want to flip PhysicsBodies because it creates odd behaviors.
## Override this method if that's what you want for some reason.
func is_collision_shape_parent_flippable() -> bool:
	return (
		self.collision_shape_parent != null
		and
		not (self.collision_shape_parent is PhysicsBody2D)
	)


================================================
FILE: addons/goutte.animated_shape_2d/animated_shape_2d.svg.import
================================================
[remap]

importer="texture"
type="CompressedTexture2D"
uid="uid://cyjyxgm3by1ae"
path="res://.godot/imported/animated_shape_2d.svg-8a2942c665c36f113bd6a54b476fbb9b.ctex"
metadata={
"vram_texture": false
}

[deps]

source_file="res://addons/goutte.animated_shape_2d/animated_shape_2d.svg"
dest_files=["res://.godot/imported/animated_shape_2d.svg-8a2942c665c36f113bd6a54b476fbb9b.ctex"]

[params]

compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false


================================================
FILE: addons/goutte.animated_shape_2d/editor/icons/copy.png.import
================================================
[remap]

importer="texture"
type="CompressedTexture2D"
uid="uid://085a76nwyjqf"
path="res://.godot/imported/copy.png-2a519d62cfcdeffa08a3d01a8cd47952.ctex"
metadata={
"vram_texture": false
}

[deps]

source_file="res://addons/goutte.animated_shape_2d/editor/icons/copy.png"
dest_files=["res://.godot/imported/copy.png-2a519d62cfcdeffa08a3d01a8cd47952.ctex"]

[params]

compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1


================================================
FILE: addons/goutte.animated_shape_2d/editor/icons/edit.png.import
================================================
[remap]

importer="texture"
type="CompressedTexture2D"
uid="uid://be0cxufi44ytn"
path="res://.godot/imported/edit.png-96c9f593918026c095b8077c91767aa5.ctex"
metadata={
"vram_texture": false
}

[deps]

source_file="res://addons/goutte.animated_shape_2d/editor/icons/edit.png"
dest_files=["res://.godot/imported/edit.png-96c9f593918026c095b8077c91767aa5.ctex"]

[params]

compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1


================================================
FILE: addons/goutte.animated_shape_2d/editor/icons/link.png.import
================================================
[remap]

importer="texture"
type="CompressedTexture2D"
uid="uid://chl5rhpr6ngqp"
path="res://.godot/imported/link.png-e080a774f28ccd8863f5e23555455903.ctex"
metadata={
"vram_texture": false
}

[deps]

source_file="res://addons/goutte.animated_shape_2d/editor/icons/link.png"
dest_files=["res://.godot/imported/link.png-e080a774f28ccd8863f5e23555455903.ctex"]

[params]

compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1


================================================
FILE: addons/goutte.animated_shape_2d/editor/icons/new.png.import
================================================
[remap]

importer="texture"
type="CompressedTexture2D"
uid="uid://cbgoa2ilmautt"
path="res://.godot/imported/new.png-f21616bfd9c8d333fd8a398de8c672bc.ctex"
metadata={
"vram_texture": false
}

[deps]

source_file="res://addons/goutte.animated_shape_2d/editor/icons/new.png"
dest_files=["res://.godot/imported/new.png-f21616bfd9c8d333fd8a398de8c672bc.ctex"]

[params]

compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1


================================================
FILE: addons/goutte.animated_shape_2d/editor/icons/paste.png.import
================================================
[remap]

importer="texture"
type="CompressedTexture2D"
uid="uid://c5mpguq347mtg"
path="res://.godot/imported/paste.png-02c243ab26b4f6897dc226741e89794a.ctex"
metadata={
"vram_texture": false
}

[deps]

source_file="res://addons/goutte.animated_shape_2d/editor/icons/paste.png"
dest_files=["res://.godot/imported/paste.png-02c243ab26b4f6897dc226741e89794a.ctex"]

[params]

compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1


================================================
FILE: addons/goutte.animated_shape_2d/editor/icons/remove.png.import
================================================
[remap]

importer="texture"
type="CompressedTexture2D"
uid="uid://hsf5o8ys0vo3"
path="res://.godot/imported/remove.png-b830910b67fe242c64c7a538aae1b97e.ctex"
metadata={
"vram_texture": false
}

[deps]

source_file="res://addons/goutte.animated_shape_2d/editor/icons/remove.png"
dest_files=["res://.godot/imported/remove.png-b830910b67fe242c64c7a538aae1b97e.ctex"]

[params]

compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1


================================================
FILE: addons/goutte.animated_shape_2d/editor/icons/shift_left.png.import
================================================
[remap]

importer="texture"
type="CompressedTexture2D"
uid="uid://btv6bx8bipqrm"
path="res://.godot/imported/shift_left.png-2904efcb7f02a4fa2b265a34ae875c4f.ctex"
metadata={
"vram_texture": false
}

[deps]

source_file="res://addons/goutte.animated_shape_2d/editor/icons/shift_left.png"
dest_files=["res://.godot/imported/shift_left.png-2904efcb7f02a4fa2b265a34ae875c4f.ctex"]

[params]

compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1


================================================
FILE: addons/goutte.animated_shape_2d/editor/icons/shift_right.png.import
================================================
[remap]

importer="texture"
type="CompressedTexture2D"
uid="uid://q30fj4bowepc"
path="res://.godot/imported/shift_right.png-de4924e407dbbfc32773a9a30221b8a0.ctex"
metadata={
"vram_texture": false
}

[deps]

source_file="res://addons/goutte.animated_shape_2d/editor/icons/shift_right.png"
dest_files=["res://.godot/imported/shift_right.png-de4924e407dbbfc32773a9a30221b8a0.ctex"]

[params]

compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1


================================================
FILE: addons/goutte.animated_shape_2d/editor/icons/zoom_less.png.import
================================================
[remap]

importer="texture"
type="CompressedTexture2D"
uid="uid://cwgak166wa6hw"
path="res://.godot/imported/zoom_less.png-ea172f3b8537da096481d713108fafea.ctex"
metadata={
"vram_texture": false
}

[deps]

source_file="res://addons/goutte.animated_shape_2d/editor/icons/zoom_less.png"
dest_files=["res://.godot/imported/zoom_less.png-ea172f3b8537da096481d713108fafea.ctex"]

[params]

compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1


================================================
FILE: addons/goutte.animated_shape_2d/editor/icons/zoom_more.png.import
================================================
[remap]

importer="texture"
type="CompressedTexture2D"
uid="uid://b64sqrouwimqs"
path="res://.godot/imported/zoom_more.png-dcace68e63f154adcf3f1343a64aab3f.ctex"
metadata={
"vram_texture": false
}

[deps]

source_file="res://addons/goutte.animated_shape_2d/editor/icons/zoom_more.png"
dest_files=["res://.godot/imported/zoom_more.png-dcace68e63f154adcf3f1343a64aab3f.ctex"]

[params]

compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1


================================================
FILE: addons/goutte.animated_shape_2d/editor/icons/zoom_reset.png.import
================================================
[remap]

importer="texture"
type="CompressedTexture2D"
uid="uid://2vmlyocy0aeu"
path="res://.godot/imported/zoom_reset.png-81fb038c23d931244978fb7781d3ccb4.ctex"
metadata={
"vram_texture": false
}

[deps]

source_file="res://addons/goutte.animated_shape_2d/editor/icons/zoom_reset.png"
dest_files=["res://.godot/imported/zoom_reset.png-81fb038c23d931244978fb7781d3ccb4.ctex"]

[params]

compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1


================================================
FILE: addons/goutte.animated_shape_2d/editor/linked_frames_feedback.gd
================================================
@tool
extends Node


@export var editor: ShapeFramesBottomPanelControl


func update_for(animation_name: StringName, frame_index: int):
	assert(editor.currently_selected_animation_name == animation_name)
	# 1. Grab the sprite frame resource of the selected frame
	var frame_editor := editor.get_frame_at(frame_index)
	var shape_frame := frame_editor.get_shape_frame()
	# 2. Iterate over all frames to find linked frames
	var linked_frames_editors: Array[ShapeFrameEditor]= []
	for some_frame_editor in editor.frames_list:
		if shape_frame == null:
			continue
		if some_frame_editor.get_shape_frame() != shape_frame:
			continue
		linked_frames_editors.append(some_frame_editor)
	# 3. Hide the link marker everywhere
	for some_frame_editor in editor.frames_list:
		some_frame_editor.hide_link_marker()
	# 4. Show the link marker where appropriate
	if linked_frames_editors.size() > 1:
		for linked_frame_editor in linked_frames_editors:
			linked_frame_editor.show_link_marker()


func _on_shape_frames_bottom_panel_control_frame_selected(animation_name: StringName, frame_index: int):
	update_for(animation_name, frame_index)


func _on_shape_frames_bottom_panel_control_frame_changed(animation_name, frame_index):
	var selected_frame_editor := editor.get_selected_frame()
	update_for(selected_frame_editor.animation_name, selected_frame_editor.frame_index)



================================================
FILE: addons/goutte.animated_shape_2d/editor/shape_frame_editor.gd
================================================
@tool
extends Control
class_name ShapeFrameEditor

## Editor GUI for a single ShapeFrame2D.
## Shows a preview of the sprite and the shape, as well as action buttons.


const SHAPE_PREVIEW_SCRIPT := preload("./shape_preview.gd")


## Animated shape resource we are editing.
## This holds enough data to configure a CollisionShape2D for a specific frame.
var animated_shape: AnimatedShape2D
## Animation name of the AnimatedSprite2D we're targeting.
var animation_name: String
## Frame of the above animation we are targeting.
var frame_index: int

## Zoom level of the preview.  Only integers are supported in there for now.
var zoom_level := 1.0
## Bakground color of the preview.
var background_color := Color.WEB_GRAY

var undo_redo: EditorUndoRedoManager


signal frame_selected
signal frame_deselected
signal changed


## Mandatory dependency injection, since it's best to leave _init() alone.
func configure(
	animated_shape: AnimatedShape2D,
	animation_name: String,
	frame_index: int,
):
	self.animated_shape = animated_shape
	self.animation_name = animation_name
	self.frame_index = frame_index


## Optional dependency injection
func set_undo_redo(undo_redo: EditorUndoRedoManager):
	self.undo_redo = undo_redo


func set_zoom_level(new_zoom_level: float):
	zoom_level = new_zoom_level


func set_background_color(new_background_color: Color):
	background_color = new_background_color


func _enter_tree():
	connect_to_shape_frame()


func _exit_tree():
	disconnect_from_shape_frame()
	remove_preview_of_shape_frame()
	if is_selected():
		frame_deselected.emit()


func build(button_group: ButtonGroup):
	update()
	# I had to set the ButtonGroup procedurally, a resource file won't work.
	%SpriteButton.button_group = button_group


func get_shape_frame() -> ShapeFrame2D:
	if self.animated_shape == null:
		return null
	if self.animated_shape.shape_frames == null:
		return null
	return self.animated_shape.shape_frames.get_shape_frame(
		self.animation_name, self.frame_index,
	)


func set_shape_frame(value: ShapeFrame2D):
	if self.animated_shape == null:
		return
	if self.animated_shape.shape_frames == null:
		return
	disconnect_from_shape_frame()
	self.animated_shape.shape_frames.set_shape_frame(
		self.animation_name, self.frame_index, value,
	)
	connect_to_shape_frame()
	update()
	emit_changed()


## Connect to the edited Resource, in order to update the GUI in real time.
func connect_to_shape_frame():
	var shape_frame := get_shape_frame()
	if shape_frame != null:
		shape_frame.changed.connect(on_shape_frame_changed)


## Disconnect from the edited Resource, to not leave connections hanging.
func disconnect_from_shape_frame():
	var shape_frame := get_shape_frame()
	if shape_frame != null:
		if shape_frame.changed.is_connected(on_shape_frame_changed):
			shape_frame.changed.disconnect(on_shape_frame_changed)


func is_selected() -> bool:
	return %SpriteButton.button_pressed


func select():
	%SpriteButton.button_pressed = true


func show_link_marker():
	%LinkMarker.show()


func hide_link_marker():
	%LinkMarker.hide()


## The crux of the matter ; update the scene according to the data.
func update():
	if self.animated_shape == null:
		return
	if self.animated_shape.animated_sprite == null:
		return
	if self.animated_shape.animated_sprite.sprite_frames == null:
		return
	
	# I. The actual sprite from the SpriteFrames, for this frame.
	%SpriteFrameTexture.texture = self.animated_shape.animated_sprite.sprite_frames.get_frame_texture(self.animation_name, self.frame_index)
	%SpriteFrameTexture.custom_minimum_size = self.zoom_level * %SpriteFrameTexture.texture.get_size()
	%SpriteFrameTexture.texture_filter = self.animated_shape.animated_sprite.texture_filter
	if %SpriteFrameTexture.texture_filter == TEXTURE_FILTER_PARENT_NODE:
		%SpriteFrameTexture.texture_filter = TEXTURE_FILTER_NEAREST
	%SpriteFrameTexture.texture_repeat = self.animated_shape.animated_sprite.texture_repeat
	if %SpriteFrameTexture.texture_repeat == TEXTURE_REPEAT_PARENT_NODE:
		%SpriteFrameTexture.texture_repeat = TEXTURE_REPEAT_DISABLED
	
	# II. Origin (0, 0) of the parent of the collision shape,
	# relative to the sprite, to help positioning our collision shape
	# at the right spot relative to the sprite in this preview.
	var collision_shape_parent_transform := Transform2D()
	if self.animated_shape.collision_shape.get_parent() is Node2D:
		collision_shape_parent_transform = self.animated_shape.collision_shape.get_parent().global_transform
	%Origin.transform = (
		self.animated_shape.animated_sprite.global_transform.affine_inverse()
		*
		collision_shape_parent_transform
	)
	if self.animated_shape.animated_sprite.centered:
		%Origin.position += (
			self.animated_shape.animated_sprite.sprite_frames.get_frame_texture(
				self.animation_name, self.frame_index,
			).get_size()
			*
			0.5
		)
	%Origin.position -= (
		self.animated_shape.animated_sprite.offset
	)
	
	# III. Display the preview of the collision shape.
	if self.animated_shape.shape_frames == null:
		return
	var shape_frame := get_shape_frame()
	if shape_frame == null:
		%ShapeHolder.shape = null
	else:
		%ShapeHolder.shape = shape_frame.get_shape()
		%ShapeHolder.position = shape_frame.position
		%ShapeHolder.disabled = shape_frame.disabled
		%ShapeHolder.debug_color = self.animated_shape.collision_shape.debug_color
		if shape_frame.debug_color != Color.BLACK:
			%ShapeHolder.debug_color = shape_frame.debug_color
			
	
	# IV. Adjust the preview to the zoom level
	%ZoomAdjuster.scale = Vector2.ONE * self.zoom_level
	
	# V. Background clear color.
	%BackgroundColor.color = self.background_color
	
	# VI. Tooltip on the main sprite button
	%SpriteButton.tooltip_text = "%s/%d" % [self.animation_name, self.frame_index]
	if shape_frame != null:
		%SpriteButton.tooltip_text += " %s" % [shape_frame]
		%SpriteButton.tooltip_text += "\nClick to edit."
	
	# X. Action button: Create
	if shape_frame == null:
		%CreateButton.visible = true
		%CreateButton.disabled = false
	else:
		%CreateButton.visible = false
		%CreateButton.disabled = true
	
	# XI. Action button: Edit
	#if shape_frame == null:
		#%EditButton.visible = false
		#%EditButton.disabled = true
	#else:
		#%EditButton.visible = true
		#%EditButton.disabled = false
	
	# XII. Action button: Copy
	if shape_frame == null:
		%CopyButton.visible = false
		%CopyButton.disabled = true
	else:
		%CopyButton.visible = true
		%CopyButton.disabled = false
	
	# XIII. Action button: Delete
	if shape_frame == null:
		%DeleteButton.visible = false
		%DeleteButton.disabled = true
	else:
		%DeleteButton.visible = true
		%DeleteButton.disabled = false
	
	# L. 2D View Preview / Mouse GUI Editor
	if is_preview_showing():
		preview_shape_frame()


func inspect_shape_frame():
	var shape_frame := get_shape_frame()
	if shape_frame == null:
		if self.animated_shape:
			EditorInterface.edit_node(self.animated_shape)
		return
	EditorInterface.edit_resource(shape_frame)


## The UndoRedo does not like when we use different objects, so we wrap this method here.
#func set_shape_frame(animation_name: StringName, frame_index: int):
	#self.animated_shape.shape_frames.set_(animation_name, frame_index)


## The UndoRedo does not like when we use different objects, so we wrap this method here.
func remove_shape_frame():
	self.animated_shape.shape_frames.remove_shape_frame(self.animation_name, self.frame_index)


#  _____                _
# |  __ \              (_)
# | |__) | __ _____   ___  _____      __
# |  ___/ '__/ _ \ \ / / |/ _ \ \ /\ / /
# | |   | | |  __/\ V /| |  __/\ V  V /
# |_|   |_|  \___| \_/ |_|\___| \_/\_/
#
# The big one shown in the 2D Editor when we select this shape frame.
# This is actually more than a preview since we can *edit* the shape with it.


var sprite_preview: AnimatedSprite2D
var preview_background: ColorRect
var preview_shape: CollisionShape2D


func is_preview_showing() -> bool:
	return is_instance_valid(self.sprite_preview)


func remove_preview_of_shape_frame():
	if is_instance_valid(preview_shape):
		preview_shape.queue_free()
		preview_shape = null
	if is_instance_valid(preview_background):
		preview_background.queue_free()
		preview_background = null
	if is_instance_valid(sprite_preview):
		sprite_preview.queue_free()
		sprite_preview = null


func preview_shape_frame():
	if not is_instance_valid(sprite_preview):
		sprite_preview = animated_shape.animated_sprite.duplicate()
		sprite_preview.name = "PreviewAnimatedSprite2D"
		sprite_preview.owner = null
	sprite_preview.animation = self.animation_name
	sprite_preview.frame = self.frame_index
	
	if not is_instance_valid(preview_background):
		preview_background = ColorRect.new()
		preview_background.name = "PreviewBackgroundColorRect"
		preview_background.owner = null
		preview_background.show_behind_parent = true
		preview_background.set_anchors_preset(PRESET_FULL_RECT)
		if sprite_preview.centered:
			var s := sprite_preview.sprite_frames.get_frame_texture(sprite_preview.animation, sprite_preview.frame).get_size()
			preview_background.offset_left -= s.x * 0.5
			preview_background.offset_right -= s.x * 0.5
			preview_background.offset_top -= s.y * 0.5
			preview_background.offset_bottom -= s.y * 0.5
		# TODO: handle sprite offset too, probably
	preview_background.color = self.background_color
	
	if preview_background.get_parent() != sprite_preview:
		if preview_background.get_parent() != null:
			preview_background.get_parent().remove_child(preview_background)
		sprite_preview.add_child(preview_background)
	
	var shape_frame := get_shape_frame()
	if shape_frame != null:
		if not is_instance_valid(preview_shape):
			preview_shape = CollisionShape2D.new()
			preview_shape.name = "PreviewCollisionShape2D"
			preview_shape.set_script(SHAPE_PREVIEW_SCRIPT)
			preview_shape.rectangle_changed.connect(on_preview_shape_rectangle_changed)
			preview_shape.item_rect_changed.connect(on_preview_shape_rect_changed)
			self.animated_shape.collision_shape.add_sibling(preview_shape)
		preview_shape.shape = shape_frame.shape
		preview_shape.position = shape_frame.position
		preview_shape.disabled = shape_frame.disabled
		preview_shape.debug_color = self.animated_shape.collision_shape.debug_color
		if shape_frame.debug_color != Color.BLACK:
			preview_shape.debug_color = shape_frame.debug_color
	else:
		if is_instance_valid(preview_shape):
			preview_shape.queue_free()
			preview_shape = null
	
	if sprite_preview.get_parent() == null:
		self.animated_shape.animated_sprite.add_sibling(sprite_preview)
	
	var selection := EditorInterface.get_selection().get_selected_nodes()
	var already_selected := not selection.is_empty()
	if already_selected:
		already_selected = (selection[0] == preview_shape)
	if is_instance_valid(preview_shape) and not already_selected:
		EditorInterface.get_selection().clear()
		EditorInterface.get_selection().add_node(preview_shape)
		# Whatever the doc says, the node already IS inspected.  Might change.
		# Anyway we don't even WANT to inspect this node, and we have to hack
		# around this unwanted inspection, see _on_sprite_button_toggled().
		#EditorInterface.edit_node(preview_shape)
		
		# This path was created using the infamous Editor Debugger with a tweak.
		# We are not using the raw index in parent, but index by class in parent
		# because it will be a little more resilient to changes in the tree.
		# This is a hack, and may not play nice with third party plugins.
		# If you know of another way to enable the mouse move mode, plz share!
		# We need to use the mouse move mode because the selection mode does
		# not like non-owned nodes, even if they are _already selected_.
		var path := [  # [ class, index_by_class_in_parent ]
			["VBoxContainer", 0], ["HSplitContainer", 0],
			["HSplitContainer", 0], ["HSplitContainer", 0],
			["VBoxContainer", 0], ["VSplitContainer", 0],
			["VSplitContainer", 0], ["VBoxContainer", 0],
			["PanelContainer", 0], ["VBoxContainer", 0],
			["CanvasItemEditor", 0], ["MarginContainer", 0],
			["HFlowContainer", 0], ["HBoxContainer", 0],
			["Button", 1],
		]
		var mouse_move_button := get_editor_node_from_path(path) as Button
		if mouse_move_button != null:
			mouse_move_button.pressed.emit()
		else:
			# Ouch, the hack above broke, as expected.  Best ignore this.
			# Just make sure you use the Mouse Move Mode when editing the 2D
			# preview, and not the Select Mode (we can't reposition with it).
			push_warning("Mouse Move Button of 2D View was not found.")


func on_preview_shape_rectangle_changed():
	update_from_preview_shape()


func on_preview_shape_rect_changed():
	# I wanna know when this starts working.
	print("Oh, now item_rect_changed signal works.  Used to not.")
	# Enable this when it works, and remove workaraound?
	#update_from_preview_shape()


func update_from_preview_shape():
	var shape_frame := get_shape_frame()
	if shape_frame == null:
		return
	if not is_instance_valid(self.preview_shape):
		return
	shape_frame.position = self.preview_shape.position


## Tool (could be static) to fetch a node from a weird path of [type, index],
## where the index is only amongst nodes of the specified type,
## to be more resilient to changes in the tree that will break the path.
## This path is for the Editor only and starts in the base editor control.
## Use the (modded) "editor_debug" addon to get the path (copy typed path). F10
func get_editor_node_from_path(path: Array) -> Node:
	var node := EditorInterface.get_base_control()
	for datum in path:
		var node_class: String = datum[0]
		var node_index: int = datum[1]
		var current_index := 0
		var found := false
		for child in node.get_children():
			if child.get_class() != node_class:
				continue
			if current_index == node_index:
				node = child
				found = true
				break
			current_index += 1
		if not found:
			return null
	return node


#  _      _     _
# | |    (_)   | |
# | |     _ ___| |_ ___ _ __   ___ _ __ ___
# | |    | / __| __/ _ \ '_ \ / _ \ '__/ __|
# | |____| \__ \ ||  __/ | | |  __/ |  \__ \
# |______|_|___/\__\___|_| |_|\___|_|  |___/
#

## UndoRedo won't accept calling methods on signals, so we'll call this instead.
func emit_changed():
	self.changed.emit()

func on_shape_frame_changed():
	update()
	emit_changed()


func _on_sprite_button_toggled(toggled_on: bool):
	if toggled_on:
		self.frame_selected.emit()
		preview_shape_frame()
		#inspect_shape_frame()  # nope, the preview has priority somehow
		#inspect_shape_frame.call_deferred()  # nope too
		# So, we use this horrendous await that will create bugs:
		get_tree().create_timer(0.064).timeout.connect(
			func():
				inspect_shape_frame()
		)
	else:
		self.frame_deselected.emit()
		remove_preview_of_shape_frame()


func _on_create_button_pressed():
	var shape_frame := get_shape_frame()
	if shape_frame != null:
		return
	
	# We could also use the UndoRedo here, but…  Hassle > Gain
	
	shape_frame = ShapeFrame2D.new()
	shape_frame.disabled = self.animated_shape.collision_shape.disabled
	shape_frame.position = self.animated_shape.collision_shape.position
	if self.animated_shape.collision_shape.shape:
		shape_frame.shape = self.animated_shape.collision_shape.shape.duplicate(true)
	else:
		shape_frame.shape = RectangleShape2D.new()
	self.animated_shape.shape_frames.set_shape_frame(
		self.animation_name, self.frame_index, shape_frame,
	)
	
	update()
	connect_to_shape_frame()
	inspect_shape_frame()
	emit_changed()


func _on_edit_button_pressed():
	inspect_shape_frame()


func _on_copy_button_pressed():
	var shape_frame := get_shape_frame()
	if shape_frame == null:
		return
	DisplayServer.clipboard_set(var_to_str(shape_frame.get_instance_id()))


func _on_paste_button_pressed():
	var previous_shape_frame := get_shape_frame()
	var copied_instance_id: int = str_to_var(DisplayServer.clipboard_get())
	if copied_instance_id == null:
		return
	var pasted_shape_frame: ShapeFrame2D = instance_from_id(copied_instance_id)
	if pasted_shape_frame == null:
		return
	if not (pasted_shape_frame is ShapeFrame2D):
		return
	
	if Input.is_key_pressed(KEY_CTRL) or Input.is_key_pressed(KEY_META):
		pasted_shape_frame = pasted_shape_frame.duplicate(true)
	
	if self.undo_redo != null:
		self.undo_redo.create_action(
			tr("Paste Shape Frame"), UndoRedo.MERGE_DISABLE, self,
		)
		self.undo_redo.add_do_method(
			self, &"disconnect_from_shape_frame",
		)
		self.undo_redo.add_do_method(
			self, &"set_shape_frame",
			pasted_shape_frame,
		)
		#self.undo_redo.add_do_method(
			#self, &"connect_to_shape_frame",
		#)
		#self.undo_redo.add_do_method(
			#self, &"update",
		#)
		#self.undo_redo.add_do_method(
			#self, &"inspect_shape_frame",
		#)
		#self.undo_redo.add_do_method(
			#self, &"emit_changed",
		#)
		self.undo_redo.add_undo_method(
			self, &"disconnect_from_shape_frame",
		)
		self.undo_redo.add_undo_method(
			self, &"set_shape_frame",
			previous_shape_frame,
		)
		#self.undo_redo.add_undo_method(
			#self, &"connect_to_shape_frame",
		#)
		#self.undo_redo.add_undo_method(
			#self, &"update",
		#)
		#self.undo_redo.add_undo_method(
			#self, &"inspect_shape_frame",
		#)
		#self.undo_redo.add_undo_method(
			#self, &"emit_changed",
		#)
		self.undo_redo.commit_action()
	else:
		# Same as above, without the UndoRedo shenanigans.
		disconnect_from_shape_frame()
		self.animated_shape.shape_frames.set_shape_frame(
			self.animation_name, self.frame_index, pasted_shape_frame,
		)
		connect_to_shape_frame()
		update()
		emit_changed()


func _on_delete_button_pressed():
	var shape_frame := get_shape_frame()
	if shape_frame == null:
		return
	
	if self.undo_redo != null:
		self.undo_redo.create_action(
			tr("Delete Shape Frame"), UndoRedo.MERGE_DISABLE, self,
		)
		self.undo_redo.add_do_method(
			self, &"disconnect_from_shape_frame",
		)
		self.undo_redo.add_do_method(
			self, &"remove_shape_frame",
		)
		self.undo_redo.add_do_method(
			self, &"update",
		)
		self.undo_redo.add_do_method(
			self, &"inspect_shape_frame",
		)
		self.undo_redo.add_do_method(
			self, &"emit_changed",
		)
		self.undo_redo.add_undo_method(
			self, &"set_shape_frame",
			shape_frame,
		)
		#self.undo_redo.add_undo_method(
			#self, &"connect_to_shape_frame",
		#)
		self.undo_redo.add_undo_method(
			self, &"update",
		)
		self.undo_redo.add_undo_method(
			self, &"inspect_shape_frame",
		)
		self.undo_redo.add_undo_method(
			self, &"emit_changed",
		)
		self.undo_redo.commit_action()
	else:
		# Same as above, but without the UndoRedo shenanigans
		disconnect_from_shape_frame()
		remove_shape_frame()
		update()
		emit_changed()



================================================
FILE: addons/goutte.animated_shape_2d/editor/shape_frame_editor.tscn
================================================
[gd_scene load_steps=10 format=3 uid="uid://c6fdijn2r2lcs"]

[ext_resource type="Script" path="res://addons/goutte.animated_shape_2d/editor/shape_frame_editor.gd" id="1_pmp4u"]
[ext_resource type="Texture2D" uid="uid://chl5rhpr6ngqp" path="res://addons/goutte.animated_shape_2d/editor/icons/link.png" id="2_3bsnw"]
[ext_resource type="Texture2D" uid="uid://hsf5o8ys0vo3" path="res://addons/goutte.animated_shape_2d/editor/icons/remove.png" id="3_bm3kb"]
[ext_resource type="Texture2D" uid="uid://cbgoa2ilmautt" path="res://addons/goutte.animated_shape_2d/editor/icons/new.png" id="3_j1q7m"]
[ext_resource type="Texture2D" uid="uid://085a76nwyjqf" path="res://addons/goutte.animated_shape_2d/editor/icons/copy.png" id="4_tt4qt"]
[ext_resource type="Texture2D" uid="uid://be0cxufi44ytn" path="res://addons/goutte.animated_shape_2d/editor/icons/edit.png" id="5_q0gfy"]
[ext_resource type="Texture2D" uid="uid://c5mpguq347mtg" path="res://addons/goutte.animated_shape_2d/editor/icons/paste.png" id="5_tdv8b"]

[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ahkyv"]
bg_color = Color(1, 1, 1, 1)
corner_radius_top_left = 4
corner_radius_top_right = 4
corner_radius_bottom_right = 4
corner_radius_bottom_left = 4
corner_detail = 3

[sub_resource type="PlaceholderTexture2D" id="PlaceholderTexture2D_qm6te"]
size = Vector2(256, 256)

[node name="ShapeFrameEditor" type="MarginContainer"]
light_mask = 0
offset_right = 72.0
offset_bottom = 72.0
auto_translate = false
localize_numeral_system = false
theme_override_constants/margin_left = 4
theme_override_constants/margin_top = 4
theme_override_constants/margin_right = 4
theme_override_constants/margin_bottom = 4
script = ExtResource("1_pmp4u")

[node name="VBoxContainer" type="VBoxContainer" parent="."]
layout_mode = 2

[node name="MarginContainer" type="MarginContainer" parent="VBoxContainer"]
layout_mode = 2

[node name="SpriteButton" type="Button" parent="VBoxContainer/MarginContainer"]
unique_name_in_owner = true
editor_description = "Button Group is set procedurally because setting it here did not work as expected."
clip_contents = true
layout_mode = 2
mouse_default_cursor_shape = 2
theme_override_styles/pressed = SubResource("StyleBoxFlat_ahkyv")
toggle_mode = true

[node name="MarginContainer" type="MarginContainer" parent="VBoxContainer/MarginContainer/SpriteButton"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_constants/margin_left = 3
theme_override_constants/margin_top = 3
theme_override_constants/margin_right = 3
theme_override_constants/margin_bottom = 3

[node name="BackgroundColor" type="ColorRect" parent="VBoxContainer/MarginContainer/SpriteButton/MarginContainer"]
unique_name_in_owner = true
layout_mode = 2
mouse_filter = 2
color = Color(0.341176, 0.341176, 0.341176, 1)

[node name="SpriteFrameTexture" type="TextureRect" parent="VBoxContainer/MarginContainer"]
unique_name_in_owner = true
light_mask = 0
layout_mode = 2
auto_translate = false
localize_numeral_system = false
mouse_filter = 2
texture = SubResource("PlaceholderTexture2D_qm6te")
stretch_mode = 5

[node name="ZoomAdjuster" type="Node2D" parent="VBoxContainer/MarginContainer/SpriteFrameTexture"]
unique_name_in_owner = true

[node name="Origin" type="Marker2D" parent="VBoxContainer/MarginContainer/SpriteFrameTexture/ZoomAdjuster"]
unique_name_in_owner = true
gizmo_extents = 6.0

[node name="ShapeHolder" type="CollisionShape2D" parent="VBoxContainer/MarginContainer/SpriteFrameTexture/ZoomAdjuster/Origin"]
unique_name_in_owner = true

[node name="Control" type="Control" parent="VBoxContainer/MarginContainer"]
layout_mode = 2
mouse_filter = 2

[node name="LinkMarker" type="TextureRect" parent="VBoxContainer/MarginContainer/Control"]
unique_name_in_owner = true
visible = false
self_modulate = Color(1, 1, 0, 1)
layout_mode = 1
anchors_preset = -1
anchor_left = 1.0
anchor_right = 1.0
offset_top = 4.0
offset_right = -4.0
grow_horizontal = 0
tooltip_text = "All the frames marked with this icon are linked together ;
that is, if you change one, they will all change.
To paste without linking, maintain CTRL when pressing the Paste button,
and it will make a deep copy instead of a shallow (linked) one."
texture = ExtResource("2_3bsnw")
metadata/_edit_lock_ = true

[node name="ActionsContainer" type="HBoxContainer" parent="VBoxContainer"]
layout_mode = 2
alignment = 1

[node name="CreateButton" type="Button" parent="VBoxContainer/ActionsContainer"]
unique_name_in_owner = true
texture_filter = 1
texture_repeat = 1
layout_mode = 2
tooltip_text = "Create a custom collision shape configuration (ShapeFrame2D) for this frame.  It will appear in the Inspector."
focus_mode = 0
mouse_default_cursor_shape = 2
icon = ExtResource("3_j1q7m")
flat = true
icon_alignment = 1

[node name="EditButton" type="Button" parent="VBoxContainer/ActionsContainer"]
unique_name_in_owner = true
visible = false
texture_filter = 1
texture_repeat = 1
layout_mode = 2
tooltip_text = "Edit the ShapeFrame2D in the inspector."
focus_mode = 0
mouse_default_cursor_shape = 2
icon = ExtResource("5_q0gfy")
flat = true
icon_alignment = 1

[node name="CopyButton" type="Button" parent="VBoxContainer/ActionsContainer"]
unique_name_in_owner = true
texture_filter = 1
texture_repeat = 1
layout_mode = 2
tooltip_text = "Copy the collision shape customization of this frame into the clipboard."
focus_mode = 0
mouse_default_cursor_shape = 2
icon = ExtResource("4_tt4qt")
flat = true
icon_alignment = 1

[node name="PasteButton" type="Button" parent="VBoxContainer/ActionsContainer"]
unique_name_in_owner = true
texture_filter = 1
texture_repeat = 1
layout_mode = 2
tooltip_text = "Paste the copied collision shape customization from the clipboard into this frame.
Beware, this pastes a shallow copy and therefore the two frames will now be edited together.
Use CTRL+Click here to paste a deep copy and decouple the frames."
focus_mode = 0
mouse_default_cursor_shape = 2
icon = ExtResource("5_tdv8b")
flat = true
icon_alignment = 1

[node name="DeleteButton" type="Button" parent="VBoxContainer/ActionsContainer"]
unique_name_in_owner = true
texture_filter = 1
texture_repeat = 1
layout_mode = 2
tooltip_text = "Delete the collision shape customization for this frame."
focus_mode = 0
mouse_default_cursor_shape = 2
icon = ExtResource("3_bm3kb")
flat = true
icon_alignment = 1

[connection signal="toggled" from="VBoxContainer/MarginContainer/SpriteButton" to="." method="_on_sprite_button_toggled"]
[connection signal="pressed" from="VBoxContainer/ActionsContainer/CreateButton" to="." method="_on_create_button_pressed"]
[connection signal="pressed" from="VBoxContainer/ActionsContainer/EditButton" to="." method="_on_edit_button_pressed"]
[connection signal="pressed" from="VBoxContainer/ActionsContainer/CopyButton" to="." method="_on_copy_button_pressed"]
[connection signal="pressed" from="VBoxContainer/ActionsContainer/PasteButton" to="." method="_on_paste_button_pressed"]
[connection signal="pressed" from="VBoxContainer/ActionsContainer/DeleteButton" to="." method="_on_delete_button_pressed"]


================================================
FILE: addons/goutte.animated_shape_2d/editor/shape_frames_bottom_panel_control.gd
================================================
@tool
extends Control
class_name ShapeFramesBottomPanelControl
#class_name ShapeFramesEditor

## Bottom panel for the Editor, shown along with Output, Debugger, etc.
## Dedicated to editing a single AnimatedShape2D.
## Will show a list of animation names, and frames for each animation.

const FRAME_SCENE := preload("./shape_frame_editor.tscn")

@onready var animation_names_item_list: ItemList = %AnimationNamesItemList
@onready var frames_container := %FramesContainer

## The thing we are previewing and editing.
var animated_shape: AnimatedShape2D

## Used to access Editor things like UndoRedo.
## Someday we will not need this anymore, hopefully.
var editor_plugin: EditorPlugin

## Zoom level of the button previews ; only integers for now.  Contribs welcome.
var zoom_level := 1.0: set = set_zoom_level

## Customizable background color of previews.
var background_color := Color.WEB_GRAY

## Button Group for the various frames, so that only one is selected at a time.
## We assign this procedurally because assigning it in the scene did not work.
var frames_button_group: ButtonGroup

var currently_selected_animation_name: StringName

## Array of ShapeFrameEditor currently shown, for the selected animation.
## These are the children of frames_container, except when config is missing.
var frames_list: Array[ShapeFrameEditor] = []  # of ShapeFrameEditor


signal frame_selected(animation_name: String, frame_index: int)
signal frame_deselected(animation_name: String, frame_index: int)
signal frame_changed(animation_name: String, frame_index: int)


func configure(
	editor_plugin: EditorPlugin,
):
	self.editor_plugin = editor_plugin


func clear():
	self.animation_names_item_list.deselect_all()
	self.animation_names_item_list.clear()
	self.animated_shape = null
	clear_shape_frames()


func clear_shape_frames():
	# We iterate the container node and not the frames_list to also remove the
	# initial label helpers that appear when the AnimatedShape2D lacks config.
	for child in self.frames_container.get_children():
		child.queue_free()
	self.frames_list.clear()


func rebuild_gui(
	for_animated_shape: AnimatedShape2D,
	force := false,
):
	if (self.animated_shape == for_animated_shape) and not force:
		return
	
	clear()
	self.animated_shape = for_animated_shape
	
	#%BackgroundColorPicker.color = …  # from editor settings ?
	self.background_color = %BackgroundColorPicker.color
	
	var missing_requirements := false
	if not is_instance_valid(self.animated_shape.animated_sprite):
		var label := Label.new()
		label.text = tr("Please assign an input animated sprite to this AnimatedShape2D.")
		self.frames_container.add_child(label)
		missing_requirements = true
	if not is_instance_valid(self.animated_shape.collision_shape):
		var label := Label.new()
		label.text = tr("Please assign a target collision shape to this AnimatedShape2D.")
		self.frames_container.add_child(label)
		missing_requirements = true
	if not is_instance_valid(self.animated_shape.shape_frames):
		var label := Label.new()
		label.text = tr("Please create or load shape frames data for this AnimatedShape2D.")
		self.frames_container.add_child(label)
		missing_requirements = true
	
	var inspector := EditorInterface.get_inspector()
	if inspector.property_edited.is_connected(on_change_do_reload):
		inspector.property_edited.disconnect(on_change_do_reload)
	if missing_requirements:
		inspector.property_edited.connect(on_change_do_reload)
		return
	
	var animation_name := &"default"
	if is_instance_valid(self.animated_shape.animated_sprite):
		animation_name = self.animated_shape.animated_sprite.animation
	rebuild_animation_names_item_list(self.animated_shape, animation_name)


func rebuild_animation_names_item_list(
	animated_shape: AnimatedShape2D,
	selected_animation_name: String,
):
	if animated_shape == null:
		print("AnimatedShape2D: no animated shape is configured.")
		return
	if animated_shape.animated_sprite == null:
		print("AnimatedShape2D: no animated sprite is configured.")
		return
	if animated_shape.animated_sprite.sprite_frames == null:
		print("AnimatedShape2D: no sprite frames is configured.")
		return
	
	var index := 0
	var selected_index := 0
	for animation_name in animated_shape.animated_sprite.sprite_frames.get_animation_names():
		self.animation_names_item_list.add_item(animation_name)
		if animation_name == selected_animation_name:
			selected_index = index
		index += 1
	
	self.animation_names_item_list.select(selected_index)
	self.animation_names_item_list.item_selected.emit(selected_index)


func rebuild_view_of_animation(animation_name: String):
	clear_shape_frames()
	self.currently_selected_animation_name = animation_name
	
	self.frames_button_group = ButtonGroup.new()
	var frames_count := self.animated_shape.animated_sprite.sprite_frames.get_frame_count(animation_name)
	for frame_index in frames_count:
		var frame_scene := FRAME_SCENE.instantiate() as ShapeFrameEditor
		frame_scene.configure(self.animated_shape, animation_name, frame_index)
		frame_scene.set_undo_redo(self.editor_plugin.get_undo_redo())
		frame_scene.set_zoom_level(self.zoom_level)
		frame_scene.set_background_color(self.background_color)
		frame_scene.build(self.frames_button_group)
		self.frames_container.add_child(frame_scene)
		self.frames_list.append(frame_scene)
	
	for frame_scene: ShapeFrameEditor in self.frames_list:
		if frame_scene == null:
			continue
		frame_scene.frame_selected.connect(on_frame_selected.bind(frame_scene.animation_name, frame_scene.frame_index))
		frame_scene.frame_deselected.connect(on_frame_deselected.bind(frame_scene.animation_name, frame_scene.frame_index))
		frame_scene.changed.connect(on_frame_changed.bind(frame_scene.animation_name, frame_scene.frame_index))


func rebuild_view_of_animation_by_index(item_index: int):
	var animation_name := self.animation_names_item_list.get_item_text(item_index)
	rebuild_view_of_animation(animation_name)


func rebuild_view_of_selected_animation():
	var selected_animations_indices := self.animation_names_item_list.get_selected_items()
	if not selected_animations_indices.is_empty():
		rebuild_view_of_animation_by_index(selected_animations_indices[0])


func set_zoom_level(new_zoom_level: float):
	if new_zoom_level == zoom_level:
		return
	zoom_level = new_zoom_level
	rebuild_view_of_selected_animation()


func get_selected_frame() -> ShapeFrameEditor:
	for frame_editor: ShapeFrameEditor in self.frames_list:
		if frame_editor == null:
			continue
		if frame_editor.is_selected():
			return frame_editor
	return null


func get_frame_at(frame_index: int) -> ShapeFrameEditor:
	if frame_index < 0:
		return null
	if frame_index > self.frames_list.size() - 1:
		return null
	return self.frames_list[frame_index]


func shift_frames_from_selected(cursor_direction: int) -> Error:
	var frame_editor := get_selected_frame()
	if not is_instance_valid(frame_editor):
		return ERR_CANT_ACQUIRE_RESOURCE
	if frame_editor.get_shape_frame() == null:
		return ERR_DOES_NOT_EXIST
	var shifted := shift_frames(cursor_direction, frame_editor.frame_index)
	if shifted != OK:
		return shifted
	var new_selected := get_frame_at(frame_editor.frame_index + cursor_direction)
	if is_instance_valid(new_selected):
		new_selected.select()
	return OK


## Shift the frames from frame index, one slot in the specified direction.
func shift_frames(cursor_direction: int, from_frame_index: int) -> Error:
	if (cursor_direction != 1) and (cursor_direction != -1):
		push_warning("AnimatedShape2D: unsupported value for cursor direction.")
		return ERR_INVALID_PARAMETER
	
	# Collect to frame(s) to shift
	var frames_to_shift := Array()
	var cursor_index := from_frame_index
	var minimum_index := 0
	var maximum_index := self.frames_list.size() - 1
	var ok := false
	while true:
		if cursor_index < minimum_index:
			break
		if cursor_index > maximum_index:
			break
		var current_frame_editor: ShapeFrameEditor = self.frames_list[cursor_index]
		if current_frame_editor.get_shape_frame() == null:
			ok = true
			break
		frames_to_shift.append(current_frame_editor)
		cursor_index += cursor_direction
	
	if not ok:
		print("AnimatedShape2D: cancelling shift because there is no room.")
		return ERR_ALREADY_EXISTS
	
	# Now we can do the actual shifting
	for i in frames_to_shift.size():
		var j := cursor_index - i * cursor_direction
		self.frames_list[j].set_shape_frame(self.frames_list[j-cursor_direction].get_shape_frame())
	self.frames_list[cursor_index - frames_to_shift.size() * cursor_direction].set_shape_frame(null)
	
	return OK


func on_frame_selected(animation_name: String, frame_index: int):
	frame_selected.emit(animation_name, frame_index)
	# Below could be a subscriber to the above signal?
	var shape_frame := animated_shape.shape_frames.get_shape_frame(
		animation_name, frame_index,
	)
	if shape_frame != null:
		%ShiftLeftButton.disabled = false
		%ShiftRightButton.disabled = false


func on_frame_deselected(animation_name: String, frame_index: int):
	frame_deselected.emit(animation_name, frame_index)
	# Below could be a subscriber to the above signal?
	%ShiftLeftButton.disabled = true
	%ShiftRightButton.disabled = true


func on_frame_changed(animation_name: String, frame_index: int):
	frame_changed.emit(animation_name, frame_index)


## Updates the bottom panel as the user fills the required properties.
## This listener is only connected when something is missing.
func on_change_do_reload(_property: String):
	var inspector := EditorInterface.get_inspector()
	if not (inspector.get_edited_object() is AnimatedShape2D):
		return
	if inspector.property_edited.is_connected(on_change_do_reload):
		inspector.property_edited.disconnect(on_change_do_reload)
	rebuild_gui(self.animated_shape, true)


func _on_animation_names_item_list_item_selected(index: int):
	rebuild_view_of_animation_by_index(index)


func _on_zoom_less_button_pressed():
	var new_zoom_level := self.zoom_level - 1.0
	# Note: current zoom logic in TextureRect won't allow going below 1
	#if self.zoom_level <= 1.0:
		#new_zoom_level = self.zoom_level * 0.5
	#new_zoom_level = max(0.125, new_zoom_level)
	new_zoom_level = max(1.0, new_zoom_level)
	set_zoom_level(new_zoom_level)


func _on_zoom_reset_button_pressed():
	set_zoom_level(1.0)


func _on_zoom_more_button_pressed():
	var new_zoom_level := self.zoom_level + 1.0
	# Note: current zoom logic in TextureRect won't allow going below 1
	#if self.zoom_level <= 1.0:
		#new_zoom_level = self.zoom_level * 2.0
	#new_zoom_level = max(0.125, new_zoom_level)
	new_zoom_level = max(1.0, new_zoom_level)
	new_zoom_level = min(10.0, new_zoom_level)
	set_zoom_level(new_zoom_level)


func _on_background_color_picker_color_changed(color: Color):
	self.background_color = color
	rebuild_view_of_selected_animation()


func _on_shift_left_button_pressed():
	shift_frames_from_selected(-1)


func _on_shift_right_button_pressed():
	shift_frames_from_selected(1)



================================================
FILE: addons/goutte.animated_shape_2d/editor/shape_frames_bottom_panel_control.tscn
================================================
[gd_scene load_steps=8 format=3 uid="uid://fh5kcvadxlh3"]

[ext_resource type="Script" path="res://addons/goutte.animated_shape_2d/editor/shape_frames_bottom_panel_control.gd" id="1_5xwm0"]
[ext_resource type="Script" path="res://addons/goutte.animated_shape_2d/editor/linked_frames_feedback.gd" id="2_njk1o"]
[ext_resource type="Texture2D" uid="uid://cwgak166wa6hw" path="res://addons/goutte.animated_shape_2d/editor/icons/zoom_less.png" id="2_rtykk"]
[ext_resource type="Texture2D" uid="uid://b64sqrouwimqs" path="res://addons/goutte.animated_shape_2d/editor/icons/zoom_more.png" id="3_2217o"]
[ext_resource type="Texture2D" uid="uid://2vmlyocy0aeu" path="res://addons/goutte.animated_shape_2d/editor/icons/zoom_reset.png" id="3_h377a"]
[ext_resource type="Texture2D" uid="uid://btv6bx8bipqrm" path="res://addons/goutte.animated_shape_2d/editor/icons/shift_left.png" id="5_jj5t1"]
[ext_resource type="Texture2D" uid="uid://q30fj4bowepc" path="res://addons/goutte.animated_shape_2d/editor/icons/shift_right.png" id="6_66ia2"]

[node name="ShapeFramesBottomPanelControl" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_5xwm0")

[node name="LinkedFramesFeedback" type="Node" parent="." node_paths=PackedStringArray("editor")]
script = ExtResource("2_njk1o")
editor = NodePath("..")

[node name="HSplitContainer" type="HSplitContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2

[node name="AnimationNamesItemList" type="ItemList" parent="HSplitContainer"]
unique_name_in_owner = true
custom_minimum_size = Vector2(80, 0)
layout_mode = 2
size_flags_horizontal = 3
allow_search = false

[node name="MarginContainer" type="VBoxContainer" parent="HSplitContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_stretch_ratio = 3.0

[node name="ActionContainer" type="HBoxContainer" parent="HSplitContainer/MarginContainer"]
layout_mode = 2

[node name="ZoomLessButton" type="Button" parent="HSplitContainer/MarginContainer/ActionContainer"]
layout_mode = 2
tooltip_text = "Zoom out the previews."
mouse_default_cursor_shape = 2
icon = ExtResource("2_rtykk")
flat = true

[node name="ZoomResetButton" type="Button" parent="HSplitContainer/MarginContainer/ActionContainer"]
layout_mode = 2
tooltip_text = "Reset the zoom level of the previews."
mouse_default_cursor_shape = 2
icon = ExtResource("3_h377a")
flat = true

[node name="ZoomMoreButton" type="Button" parent="HSplitContainer/MarginContainer/ActionContainer"]
layout_mode = 2
tooltip_text = "Zoom in the previews."
mouse_default_cursor_shape = 2
icon = ExtResource("3_2217o")
flat = true

[node name="VSeparator1" type="VSeparator" parent="HSplitContainer/MarginContainer/ActionContainer"]
layout_mode = 2

[node name="BackgroundColorPicker" type="ColorPickerButton" parent="HSplitContainer/MarginContainer/ActionContainer"]
unique_name_in_owner = true
custom_minimum_size = Vector2(50, 31)
layout_mode = 2
tooltip_text = "Choose a color for the background of the previews."
focus_mode = 0
mouse_default_cursor_shape = 2
color = Color(0.282353, 0.282353, 0.282353, 1)

[node name="VSeparator2" type="VSeparator" parent="HSplitContainer/MarginContainer/ActionContainer"]
layout_mode = 2

[node name="ShiftLeftButton" type="Button" parent="HSplitContainer/MarginContainer/ActionContainer"]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Shift the selected shape frame one slot to the left, pushing adjacent shape frames if necessary.
If there is no free room to shift to, the shift is cancelled, because
we don't want to destroy any shape frame data by shifting over it."
mouse_default_cursor_shape = 2
disabled = true
icon = ExtResource("5_jj5t1")
flat = true

[node name="ShiftRightButton" type="Button" parent="HSplitContainer/MarginContainer/ActionContainer"]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Shift the selected shape frame one slot to the right, pushing adjacent shape frames if necessary.
If there is no free room to shift to, the shift is cancelled, because
we don't want to destroy any shape frame data by shifting over it."
mouse_default_cursor_shape = 2
disabled = true
icon = ExtResource("6_66ia2")
flat = true

[node name="ScrollContainer" type="ScrollContainer" parent="HSplitContainer/MarginContainer"]
layout_mode = 2
size_flags_vertical = 3
follow_focus = true
horizontal_scroll_mode = 0

[node name="FramesContainer" type="HFlowContainer" parent="HSplitContainer/MarginContainer/ScrollContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3

[connection signal="frame_changed" from="." to="LinkedFramesFeedback" method="_on_shape_frames_bottom_panel_control_frame_changed"]
[connection signal="frame_selected" from="." to="LinkedFramesFeedback" method="_on_shape_frames_bottom_panel_control_frame_selected"]
[connection signal="item_selected" from="HSplitContainer/AnimationNamesItemList" to="." method="_on_animation_names_item_list_item_selected"]
[connection signal="pressed" from="HSplitContainer/MarginContainer/ActionContainer/ZoomLessButton" to="." method="_on_zoom_less_button_pressed"]
[connection signal="pressed" from="HSplitContainer/MarginContainer/ActionContainer/ZoomResetButton" to="." method="_on_zoom_reset_button_pressed"]
[connection signal="pressed" from="HSplitContainer/MarginContainer/ActionContainer/ZoomMoreButton" to="." method="_on_zoom_more_button_pressed"]
[connection signal="color_changed" from="HSplitContainer/MarginContainer/ActionContainer/BackgroundColorPicker" to="." method="_on_background_color_picker_color_changed"]
[connection signal="pressed" from="HSplitContainer/MarginContainer/ActionContainer/ShiftLeftButton" to="." method="_on_shift_left_button_pressed"]
[connection signal="pressed" from="HSplitContainer/MarginContainer/ActionContainer/ShiftRightButton" to="." method="_on_shift_right_button_pressed"]


================================================
FILE: addons/goutte.animated_shape_2d/editor/shape_preview.gd
================================================
@tool
extends CollisionShape2D

## Added to the temporary CollisionShape2D created in the 2D view of the Editor.
## Used to recover the position and size changes, since item_rect_changed NOPE.
## Note that we don't need the size of the shape, it's already propagated.
## This is only for the position, and perhaps later on rotation and scale ?


signal rectangle_changed


func _ready():
	set_notify_local_transform(true)


func _notification(what: int):
	if what == NOTIFICATION_LOCAL_TRANSFORM_CHANGED:
		rectangle_changed.emit()



================================================
FILE: addons/goutte.animated_shape_2d/plugin.cfg
================================================
[plugin]

name="Animated Shape 2D"
description="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."
author="Goutte"
version="1.5.0"
script="plugin.gd"


================================================
FILE: addons/goutte.animated_shape_2d/plugin.gd
================================================
@tool
extends EditorPlugin

#
# This plugin adds the following to the Editor:
# - An AnimatedShape2D Node you can add to your scenes in order to configure
#   a CollisionShape2D with dynamic values per frame of an AnimatedSprite2D.
#   This is very handy for making dynamic Hurtboxes, Solidboxes, or Hitboxes.
# - A GUI for editing a ShapeFrame2D, similar to the SpriteFrames GUI.
#

const BOTTOM_PANEL_CONTROL_SCENE := preload("./editor/shape_frames_bottom_panel_control.tscn")

var bottom_panel_button: Button
var bottom_panel_control: Control


func _enter_tree():
	# I. Register the "Animated Shape" bottom panel.
	self.bottom_panel_control = BOTTOM_PANEL_CONTROL_SCENE.instantiate()
	self.bottom_panel_control.configure(self)
	self.bottom_panel_button = add_control_to_bottom_panel(
		self.bottom_panel_control,
		tr("Animated Shape"),
	)
	
	# II. Show/Hide it depending on what's inspected in the Inspector.
	#     This could also work using what's selected in the Scene Tree Editor?
	on_inspector_edited_object_changed()
	get_editor_interface().get_inspector().edited_object_changed.connect(
		on_inspector_edited_object_changed,
	)


func _exit_tree():
	remove_control_from_bottom_panel(self.bottom_panel_control)
	
	if get_editor_interface().get_inspector().edited_object_changed.is_connected(
		on_inspector_edited_object_changed,
	):
		get_editor_interface().get_inspector().edited_object_changed.disconnect(
			on_inspector_edited_object_changed,
		)


func update_bottom_panel(animated_shape: AnimatedShape2D):
	if animated_shape == null:
		self.bottom_panel_button.button_pressed = false
		self.bottom_panel_button.visible = false
		self.bottom_panel_control.clear()
		return
	
	self.bottom_panel_button.visible = true
	self.bottom_panel_button.button_pressed = true
	self.bottom_panel_control.rebuild_gui(animated_shape)


func on_inspector_edited_object_changed():
	var edited_object := get_editor_interface().get_inspector().get_edited_object()
	if edited_object is ShapeFrame2D:
		return
	if edited_object is CollisionShape2D and (edited_object as CollisionShape2D).owner == null:
		return  # we're editing the previews' shape
	if edited_object is Shape2D:
		return  # same
	if edited_object == null:
		return  # same
	var animated_shape: AnimatedShape2D = null
	if edited_object is AnimatedShape2D:
		animated_shape = edited_object as AnimatedShape2D
	update_bottom_panel(animated_shape)



================================================
FILE: addons/goutte.animated_shape_2d/shape_frame_2d.gd
================================================
@tool
extends Resource
class_name ShapeFrame2D
#class_name Shape2DFrame

## Data object for a single shape frame.
## Basically a configurator for a CollisionShape2D.
## Each frame of each animation of an AnimatedSprite2D will be matched to one
## of these, in a Dictionary in ShapeFrames2D.
## You can also use the metadata of this Resource to store custom records per frame.


## Position of the collision shape in its parent.
@export var position := Vector2.ZERO:
	set(value):
		if value != position:
			position = value
			emit_changed()

## Disable the collision shape when [code]true[/code].
@export var disabled := false:
	set(value):
		if value != disabled:
			disabled = value
			emit_changed()

## Shape of the collision shape.
@export var shape: Shape2D = null: get = get_shape, set = set_shape

func get_shape() -> Shape2D:
	return shape


func set_shape(value: Shape2D):
	shape = value
	emit_changed()


## Override the debug color of the shape.
## Especially useful when adding metadata to the shape.
## Black with full opacity disables this override.
@export var debug_color := Color.BLACK


## Used to make dummy ; perhaps keep as a procedural API ?
static func make_rectangle(
	size: Vector2,
	position := Vector2.ZERO,
	disabled := false,
) -> ShapeFrame2D:
	var sf := ShapeFrame2D.new()
	sf.shape = RectangleShape2D.new()
	sf.shape.size = size
	sf.position = position
	sf.disabled = disabled
	return sf


================================================
FILE: addons/goutte.animated_shape_2d/shape_frames_2d.gd
================================================
@tool
extends Resource
class_name ShapeFrames2D
#class_name Shape2DFrames

## Resource holding the configuration of a CollisionShape2D,
## for each frame of each animation of a SpriteFrames.
## This is basically a mapping of ShapeFrame2D for each (animation name, frame).


## This helps avoiding infinite loops in case you pass INF as frame index.
## If you hit that limit (what are you doing?!), it is safe to bump it up.
const MAXIMUM_FRAMES_AMOUNT := 666


## Actual data of this resource.
## This is a Dictionary of Arrays of ShapeFrame2D, indexed by animation name.
## In each Array of ShapeFrame2D, there ought to be one ShapeFrame2D per frame
## in the corresponding animation of the AnimatedSprite2D.
## It means that the shape of this should follow the shape of the SpriteFrames.
## [code]
## {
## 	'<animation name>': [ ShapeFrame2D, … ],
## 	…
## }
## [/code]
@export var data := Dictionary()


## Returns the shape frame data for the provided animation and frame.
## This should return null if no data was found.
func get_shape_frame(animation_name: StringName, frame: int) -> ShapeFrame2D:
	if self.data.has(animation_name):
		var animation_data: Array = self.data[animation_name] as Array
		if animation_data == null:
			return null
		if animation_data.size() > frame:
			return animation_data[frame] as ShapeFrame2D
	return null


## Sets a ShapeFrame2D for the provided animation and frame.
## When the AnimatedSprite2D shows this frame, the CollisionShape2D will be
## configured using the provided ShapeFrame2D.  See AnimatedShape2D.
func set_shape_frame(
	animation_name: StringName,
	frame: int,
	shape_frame: ShapeFrame2D,
):
	if not self.data.has(animation_name):
		self.data[animation_name] = Array()
	var animation_data: Array = self.data[animation_name] as Array
	frame = max(frame, 0)
	frame = min(frame, MAXIMUM_FRAMES_AMOUNT)
	if animation_data.size() <= frame:
		animation_data.resize(frame + 1)
	animation_data[frame] = shape_frame


## Removes the ShapeFrame2D datum for the provided key tuple.
## This will let the frame fall back to configured defaults.
func remove_shape_frame(animation_name: StringName, frame: int):
	if not self.data.has(animation_name):
		return
	var animation_data: Array = self.data[animation_name] as Array
	if animation_data.size() > frame:
		animation_data[frame] = null


# Example of a tentative procedural API to make this.
# Prefer using the Editor for now.
#static func make_dummy() -> ShapeFrames2D:
	#var dummy := ShapeFrames2D.new()
	#dummy.data = {
		#&'jump_ascent': [
			#ShapeFrame2D.make_rectangle(Vector2(16, 32), Vector2(0, -17)),
			#ShapeFrame2D.make_rectangle(Vector2(16, 28), Vector2(0, -15)),
			#ShapeFrame2D.make_rectangle(Vector2(16, 22), Vector2(0, -12)),
			#ShapeFrame2D.make_rectangle(Vector2(16, 16), Vector2(0, -9)),
			#ShapeFrame2D.make_rectangle(Vector2(16, 16), Vector2(0, -9)),
			#ShapeFrame2D.make_rectangle(Vector2(16, 16), Vector2(0, -9)),
			#ShapeFrame2D.make_rectangle(Vector2(16, 16), Vector2(0, -9)),
			#ShapeFrame2D.make_rectangle(Vector2(16, 16), Vector2(0, -9)),
			#ShapeFrame2D.make_rectangle(Vector2(16, 16), Vector2(0, -9)),
			#ShapeFrame2D.make_rectangle(Vector2(16, 16), Vector2(0, -9)),
			#ShapeFrame2D.make_rectangle(Vector2(16, 16), Vector2(0, -9)),
			#ShapeFrame2D.make_rectangle(Vector2(16, 16), Vector2(0, -9)),
			#ShapeFrame2D.make_rectangle(Vector2(16, 16), Vector2(0, -9)),
		#],
		#&'jump_descent': [
			#ShapeFrame2D.make_rectangle(Vector2(16, 16), Vector2(0, -9)),
			#ShapeFrame2D.make_rectangle(Vector2(16, 16), Vector2(0, -9)),
			#ShapeFrame2D.make_rectangle(Vector2(16, 16), Vector2(0, -9)),
			#ShapeFrame2D.make_rectangle(Vector2(16, 16), Vector2(0, -9)),
			#ShapeFrame2D.make_rectangle(Vector2(16, 22), Vector2(0, -12)),
			#ShapeFrame2D.make_rectangle(Vector2(16, 28), Vector2(0, -15)),
			#ShapeFrame2D.make_rectangle(Vector2(16, 32), Vector2(0, -17)),
		#],
		#&'jump_flight': [
			#ShapeFrame2D.make_rectangle(Vector2(16, 16), Vector2(0, -9)),
			#ShapeFrame2D.make_rectangle(Vector2(16, 16), Vector2(0, -9)),
			#ShapeFrame2D.make_rectangle(Vector2(16, 16), Vector2(0, -9)),
			#ShapeFrame2D.make_rectangle(Vector2(16, 16), Vector2(0, -9)),
		#],
	#}
	#return dummy
Download .txt
gitextract_dzyrmz1o/

├── .gitattributes
├── .gitignore
├── LICENSE
├── README.md
└── addons/
    └── goutte.animated_shape_2d/
        ├── README.md
        ├── animated_shape_2d.gd
        ├── animated_shape_2d.svg.import
        ├── editor/
        │   ├── icons/
        │   │   ├── copy.png.import
        │   │   ├── edit.png.import
        │   │   ├── link.png.import
        │   │   ├── new.png.import
        │   │   ├── paste.png.import
        │   │   ├── remove.png.import
        │   │   ├── shift_left.png.import
        │   │   ├── shift_right.png.import
        │   │   ├── zoom_less.png.import
        │   │   ├── zoom_more.png.import
        │   │   └── zoom_reset.png.import
        │   ├── linked_frames_feedback.gd
        │   ├── shape_frame_editor.gd
        │   ├── shape_frame_editor.tscn
        │   ├── shape_frames_bottom_panel_control.gd
        │   ├── shape_frames_bottom_panel_control.tscn
        │   └── shape_preview.gd
        ├── plugin.cfg
        ├── plugin.gd
        ├── shape_frame_2d.gd
        └── shape_frames_2d.gd
Condensed preview — 28 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (90K chars).
[
  {
    "path": ".gitattributes",
    "chars": 434,
    "preview": " # Normalize line endings for all files that Git considers text files.\n* text=auto eol=lf\n\n# Exclude irrelevant files wh"
  },
  {
    "path": ".gitignore",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "LICENSE",
    "chars": 1075,
    "preview": "MIT License\n\nCopyright (c) 2023 Friends of Godette\n\nPermission is hereby granted, free of charge, to any person obtainin"
  },
  {
    "path": "README.md",
    "chars": 1784,
    "preview": "Animated Shape 2D Addon for Godot\n---------------------------------\n\n[![MIT](https://img.shields.io/github/license/Goutt"
  },
  {
    "path": "addons/goutte.animated_shape_2d/README.md",
    "chars": 2668,
    "preview": "Animated Shape 2D Addon for Godot\n---------------------------------\n\n[![MIT](https://img.shields.io/github/license/Goutt"
  },
  {
    "path": "addons/goutte.animated_shape_2d/animated_shape_2d.gd",
    "chars": 13957,
    "preview": "@tool\n@icon(\"./animated_shape_2d.svg\")\nextends Node\nclass_name AnimatedShape2D\n#class_name AnimatedCollisionShape2D\n#cla"
  },
  {
    "path": "addons/goutte.animated_shape_2d/animated_shape_2d.svg.import",
    "chars": 914,
    "preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://cyjyxgm3by1ae\"\npath=\"res://.godot/imported/animated_sh"
  },
  {
    "path": "addons/goutte.animated_shape_2d/editor/icons/copy.png.import",
    "chars": 790,
    "preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://085a76nwyjqf\"\npath=\"res://.godot/imported/copy.png-2a5"
  },
  {
    "path": "addons/goutte.animated_shape_2d/editor/icons/edit.png.import",
    "chars": 791,
    "preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://be0cxufi44ytn\"\npath=\"res://.godot/imported/edit.png-96"
  },
  {
    "path": "addons/goutte.animated_shape_2d/editor/icons/link.png.import",
    "chars": 791,
    "preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://chl5rhpr6ngqp\"\npath=\"res://.godot/imported/link.png-e0"
  },
  {
    "path": "addons/goutte.animated_shape_2d/editor/icons/new.png.import",
    "chars": 788,
    "preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://cbgoa2ilmautt\"\npath=\"res://.godot/imported/new.png-f21"
  },
  {
    "path": "addons/goutte.animated_shape_2d/editor/icons/paste.png.import",
    "chars": 794,
    "preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://c5mpguq347mtg\"\npath=\"res://.godot/imported/paste.png-0"
  },
  {
    "path": "addons/goutte.animated_shape_2d/editor/icons/remove.png.import",
    "chars": 796,
    "preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://hsf5o8ys0vo3\"\npath=\"res://.godot/imported/remove.png-b"
  },
  {
    "path": "addons/goutte.animated_shape_2d/editor/icons/shift_left.png.import",
    "chars": 809,
    "preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://btv6bx8bipqrm\"\npath=\"res://.godot/imported/shift_left."
  },
  {
    "path": "addons/goutte.animated_shape_2d/editor/icons/shift_right.png.import",
    "chars": 811,
    "preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://q30fj4bowepc\"\npath=\"res://.godot/imported/shift_right."
  },
  {
    "path": "addons/goutte.animated_shape_2d/editor/icons/zoom_less.png.import",
    "chars": 806,
    "preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://cwgak166wa6hw\"\npath=\"res://.godot/imported/zoom_less.p"
  },
  {
    "path": "addons/goutte.animated_shape_2d/editor/icons/zoom_more.png.import",
    "chars": 806,
    "preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://b64sqrouwimqs\"\npath=\"res://.godot/imported/zoom_more.p"
  },
  {
    "path": "addons/goutte.animated_shape_2d/editor/icons/zoom_reset.png.import",
    "chars": 808,
    "preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://2vmlyocy0aeu\"\npath=\"res://.godot/imported/zoom_reset.p"
  },
  {
    "path": "addons/goutte.animated_shape_2d/editor/linked_frames_feedback.gd",
    "chars": 1360,
    "preview": "@tool\nextends Node\n\n\n@export var editor: ShapeFramesBottomPanelControl\n\n\nfunc update_for(animation_name: StringName, fra"
  },
  {
    "path": "addons/goutte.animated_shape_2d/editor/shape_frame_editor.gd",
    "chars": 18516,
    "preview": "@tool\nextends Control\nclass_name ShapeFrameEditor\n\n## Editor GUI for a single ShapeFrame2D.\n## Shows a preview of the sp"
  },
  {
    "path": "addons/goutte.animated_shape_2d/editor/shape_frame_editor.tscn",
    "chars": 7115,
    "preview": "[gd_scene load_steps=10 format=3 uid=\"uid://c6fdijn2r2lcs\"]\n\n[ext_resource type=\"Script\" path=\"res://addons/goutte.anima"
  },
  {
    "path": "addons/goutte.animated_shape_2d/editor/shape_frames_bottom_panel_control.gd",
    "chars": 10902,
    "preview": "@tool\nextends Control\nclass_name ShapeFramesBottomPanelControl\n#class_name ShapeFramesEditor\n\n## Bottom panel for the Ed"
  },
  {
    "path": "addons/goutte.animated_shape_2d/editor/shape_frames_bottom_panel_control.tscn",
    "chars": 6006,
    "preview": "[gd_scene load_steps=8 format=3 uid=\"uid://fh5kcvadxlh3\"]\n\n[ext_resource type=\"Script\" path=\"res://addons/goutte.animate"
  },
  {
    "path": "addons/goutte.animated_shape_2d/editor/shape_preview.gd",
    "chars": 533,
    "preview": "@tool\nextends CollisionShape2D\n\n## Added to the temporary CollisionShape2D created in the 2D view of the Editor.\n## Used"
  },
  {
    "path": "addons/goutte.animated_shape_2d/plugin.cfg",
    "chars": 413,
    "preview": "[plugin]\n\nname=\"Animated Shape 2D\"\ndescription=\"Helps making animated shapes to go with your animated sprites.  This all"
  },
  {
    "path": "addons/goutte.animated_shape_2d/plugin.gd",
    "chars": 2408,
    "preview": "@tool\nextends EditorPlugin\n\n#\n# This plugin adds the following to the Editor:\n# - An AnimatedShape2D Node you can add to"
  },
  {
    "path": "addons/goutte.animated_shape_2d/shape_frame_2d.gd",
    "chars": 1421,
    "preview": "@tool\nextends Resource\nclass_name ShapeFrame2D\n#class_name Shape2DFrame\n\n## Data object for a single shape frame.\n## Bas"
  },
  {
    "path": "addons/goutte.animated_shape_2d/shape_frames_2d.gd",
    "chars": 4209,
    "preview": "@tool\nextends Resource\nclass_name ShapeFrames2D\n#class_name Shape2DFrames\n\n## Resource holding the configuration of a Co"
  }
]

About this extraction

This page contains the full source code of the Goutte/godot-addon-animated-shape-2d GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 28 files (80.6 KB), approximately 22.2k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!