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
---------------------------------
[](https://github.com/Goutte/godot-addon-animated-shape-2d)
[](https://github.com/Goutte/godot-addon-animated-shape-2d/releases)
[](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.

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
---------------------------------
[](https://github.com/Goutte/godot-addon-animated-shape-2d)
[](https://github.com/Goutte/godot-addon-animated-shape-2d/releases)
[](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
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[\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.