diff --git a/addons/goutte.animated_shape_2d/animated_shape_2d.gd b/addons/goutte.animated_shape_2d/animated_shape_2d.gd index be44b61..79844d4 100644 --- a/addons/goutte.animated_shape_2d/animated_shape_2d.gd +++ b/addons/goutte.animated_shape_2d/animated_shape_2d.gd @@ -2,6 +2,7 @@ @icon("./animated_shape_2d.svg") extends Node class_name AnimatedShape2D +#class_name AnimatedCollisionShape2D #class_name AnimatedSprite2DCollisions #class_name CollisionShape2DFramer @@ -48,6 +49,9 @@ class_name AnimatedShape2D ## 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. @@ -118,6 +122,8 @@ func _get_configuration_warnings() -> PackedStringArray: 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: @@ -141,6 +147,8 @@ func get_current_shape_frame() -> ShapeFrame2D: func update_shape(): + if self.shape_frames == null: + return var shape_frame := get_current_shape_frame() var shape: Shape2D = null @@ -161,8 +169,7 @@ func update_shape(): update_collision_shape_shape(shape) update_collision_shape_position(position) - #self.collision_shape.disabled = disabled - self.collision_shape.set_deferred(&"disabled", disabled) + 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 @@ -172,6 +179,13 @@ func update_shape(): 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 @@ -253,12 +267,18 @@ func update_collision_shape_shape(new_shape: Shape2D): # If the update cannot be done, we want a duplicate of the shape # because we might update it later on. - self.collision_shape.shape = new_shape.duplicate(true) + 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. - self.collision_shape.shape = new_shape + 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 diff --git a/addons/goutte.animated_shape_2d/editor/icons/link.png b/addons/goutte.animated_shape_2d/editor/icons/link.png new file mode 100644 index 0000000..1c492bd Binary files /dev/null and b/addons/goutte.animated_shape_2d/editor/icons/link.png differ diff --git a/addons/goutte.animated_shape_2d/editor/icons/link.png.import b/addons/goutte.animated_shape_2d/editor/icons/link.png.import new file mode 100644 index 0000000..5c2440d --- /dev/null +++ b/addons/goutte.animated_shape_2d/editor/icons/link.png.import @@ -0,0 +1,34 @@ +[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 diff --git a/addons/goutte.animated_shape_2d/editor/linked_frames_feedback.gd b/addons/goutte.animated_shape_2d/editor/linked_frames_feedback.gd new file mode 100644 index 0000000..9781156 --- /dev/null +++ b/addons/goutte.animated_shape_2d/editor/linked_frames_feedback.gd @@ -0,0 +1,37 @@ +@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) + diff --git a/addons/goutte.animated_shape_2d/editor/shape_frame_editor.gd b/addons/goutte.animated_shape_2d/editor/shape_frame_editor.gd index 844c835..2f1f396 100644 --- a/addons/goutte.animated_shape_2d/editor/shape_frame_editor.gd +++ b/addons/goutte.animated_shape_2d/editor/shape_frame_editor.gd @@ -27,6 +27,7 @@ var undo_redo: EditorUndoRedoManager signal frame_selected signal frame_deselected +signal changed ## Mandatory dependency injection, since it's best to leave _init() alone. @@ -91,6 +92,7 @@ func set_shape_frame(value: ShapeFrame2D): ) connect_to_shape_frame() update() + emit_changed() ## Connect to the edited Resource, in order to update the GUI in real time. @@ -116,6 +118,14 @@ 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: @@ -231,6 +241,16 @@ func inspect_shape_frame(): 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) + + # _____ _ # | __ \ (_) # | |__) | __ _____ ___ _____ __ @@ -406,14 +426,18 @@ func get_editor_node_from_path(path: Array) -> 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: - frame_selected.emit() + self.frame_selected.emit() preview_shape_frame() #inspect_shape_frame() # nope, the preview has priority somehow #inspect_shape_frame.call_deferred() # nope too @@ -423,7 +447,7 @@ func _on_sprite_button_toggled(toggled_on: bool): inspect_shape_frame() ) else: - frame_deselected.emit() + self.frame_deselected.emit() remove_preview_of_shape_frame() @@ -448,6 +472,7 @@ func _on_create_button_pressed(): update() connect_to_shape_frame() inspect_shape_frame() + emit_changed() func _on_edit_button_pressed(): @@ -483,34 +508,40 @@ func _on_paste_button_pressed(): self, &"disconnect_from_shape_frame", ) self.undo_redo.add_do_method( - self.animated_shape.shape_frames, &"set_shape_frame", - self.animation_name, self.frame_index, 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, &"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.animated_shape.shape_frames, &"set_shape_frame", - self.animation_name, self.frame_index, 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, &"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. @@ -520,6 +551,7 @@ func _on_paste_button_pressed(): ) connect_to_shape_frame() update() + emit_changed() func _on_delete_button_pressed(): @@ -535,8 +567,7 @@ func _on_delete_button_pressed(): self, &"disconnect_from_shape_frame", ) self.undo_redo.add_do_method( - self.animated_shape.shape_frames, &"remove_shape_frame", - self.animation_name, self.frame_index, + self, &"remove_shape_frame", ) self.undo_redo.add_do_method( self, &"update", @@ -544,25 +575,30 @@ func _on_delete_button_pressed(): self.undo_redo.add_do_method( self, &"inspect_shape_frame", ) - self.undo_redo.add_undo_method( - self.animated_shape.shape_frames, &"set_shape_frame", - self.animation_name, self.frame_index, shape_frame, + self.undo_redo.add_do_method( + self, &"emit_changed", ) self.undo_redo.add_undo_method( - self, &"connect_to_shape_frame", + 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() - self.animated_shape.shape_frames.remove_shape_frame( - self.animation_name, self.frame_index, - ) + remove_shape_frame() update() + emit_changed() diff --git a/addons/goutte.animated_shape_2d/editor/shape_frame_editor.tscn b/addons/goutte.animated_shape_2d/editor/shape_frame_editor.tscn index b9e8bcd..ecd8661 100644 --- a/addons/goutte.animated_shape_2d/editor/shape_frame_editor.tscn +++ b/addons/goutte.animated_shape_2d/editor/shape_frame_editor.tscn @@ -1,6 +1,7 @@ -[gd_scene load_steps=8 format=3 uid="uid://c6fdijn2r2lcs"] +[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"] @@ -15,6 +16,9 @@ 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 @@ -67,6 +71,7 @@ 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"] @@ -79,6 +84,28 @@ 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 diff --git a/addons/goutte.animated_shape_2d/editor/shape_frames_bottom_panel_control.gd b/addons/goutte.animated_shape_2d/editor/shape_frames_bottom_panel_control.gd index d7f316b..478e222 100644 --- a/addons/goutte.animated_shape_2d/editor/shape_frames_bottom_panel_control.gd +++ b/addons/goutte.animated_shape_2d/editor/shape_frames_bottom_panel_control.gd @@ -1,5 +1,7 @@ @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. @@ -27,13 +29,16 @@ var background_color := Color.WEB_GRAY ## 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() # of ShapeFrameEditor +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( @@ -128,6 +133,7 @@ func rebuild_animation_names_item_list( 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) @@ -146,6 +152,7 @@ func rebuild_view_of_animation(animation_name: String): 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): @@ -253,6 +260,10 @@ func on_frame_deselected(animation_name: String, frame_index: int): %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): diff --git a/addons/goutte.animated_shape_2d/editor/shape_frames_bottom_panel_control.tscn b/addons/goutte.animated_shape_2d/editor/shape_frames_bottom_panel_control.tscn index 46aee3c..7f8aaf3 100644 --- a/addons/goutte.animated_shape_2d/editor/shape_frames_bottom_panel_control.tscn +++ b/addons/goutte.animated_shape_2d/editor/shape_frames_bottom_panel_control.tscn @@ -1,6 +1,7 @@ -[gd_scene load_steps=7 format=3 uid="uid://fh5kcvadxlh3"] +[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"] @@ -16,6 +17,10 @@ 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 @@ -109,6 +114,8 @@ 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"]