diff --git a/project.godot b/project.godot index ab20b2bdd0c5..5744dbfebb25 100644 --- a/project.godot +++ b/project.godot @@ -139,6 +139,16 @@ _global_script_classes=[ { "language": "GDScript", "path": "res://src/Classes/Project.gd" }, { +"base": "Sprite", +"class": "ReferenceImage", +"language": "GDScript", +"path": "res://src/UI/Canvas/ReferenceImage.gd" +}, { +"base": "VBoxContainer", +"class": "ReferencesPanel", +"language": "GDScript", +"path": "res://src/UI/ReferencesPanel.gd" +}, { "base": "Image", "class": "SelectionMap", "language": "GDScript", @@ -201,6 +211,8 @@ _global_script_class_icons={ "PixelCel": "", "PixelLayer": "", "Project": "", +"ReferenceImage": "", +"ReferencesPanel": "", "SelectionMap": "", "SelectionTool": "", "ShaderImageEffect": "", diff --git a/src/Autoload/Global.gd b/src/Autoload/Global.gd index afd048d6e464..9ee3b04f6730 100644 --- a/src/Autoload/Global.gd +++ b/src/Autoload/Global.gd @@ -172,6 +172,8 @@ onready var brushes_popup: Popup = control.find_node("BrushesPopup") onready var patterns_popup: Popup = control.find_node("PatternsPopup") onready var palette_panel: PalettePanel = control.find_node("Palettes") +onready var references_panel: ReferencesPanel = control.find_node("References") + onready var top_menu_container: Panel = control.find_node("TopMenuContainer") onready var rotation_level_button: Button = control.find_node("RotationLevel") onready var rotation_level_spinbox: SpinBox = control.find_node("RotationSpinbox") diff --git a/src/Autoload/OpenSave.gd b/src/Autoload/OpenSave.gd index 2ac04cde67da..071dcb0a7b4c 100644 --- a/src/Autoload/OpenSave.gd +++ b/src/Autoload/OpenSave.gd @@ -596,6 +596,15 @@ func open_image_as_new_layer(image: Image, file_name: String, frame_index := 0) project.undo_redo.commit_action() +func import_reference_image_from_path(path: String): + var project: Project = Global.current_project + var ri := ReferenceImage.new() + ri.project = project + ri.deserialize({"image_path": path}) + Global.canvas.add_child(ri) + project.change_project() + + func set_new_imported_tab(project: Project, path: String) -> void: var prev_project_empty: bool = Global.current_project.is_empty() var prev_project_pos: int = Global.current_project_index diff --git a/src/Classes/Project.gd b/src/Classes/Project.gd index 69ffdc3ca29f..8355b4dd70d9 100644 --- a/src/Classes/Project.gd +++ b/src/Classes/Project.gd @@ -23,6 +23,7 @@ var selected_cels := [[0, 0]] # Array of Arrays of 2 integers (frame & layer) var animation_tags := [] setget _animation_tags_changed # Array of AnimationTags var guides := [] # Array of Guides var brushes := [] # Array of Images +var reference_images := [] # Array of ReferenceImages var fps := 6.0 var x_symmetry_point @@ -88,6 +89,8 @@ func _init(_frames := [], _name := tr("untitled"), _size := Vector2(64, 64)) -> func remove() -> void: undo_redo.free() + for ri in reference_images: + ri.queue_free() for guide in guides: guide.queue_free() # Prevents memory leak (due to the layers' project reference stopping ref counting from freeing) @@ -179,6 +182,7 @@ func change_project() -> void: Global.animation_timeline.fps_spinbox.value = fps Global.horizontal_ruler.update() Global.vertical_ruler.update() + Global.references_panel.project_changed() Global.cursor_position_label.text = "[%s×%s]" % [size.x, size.y] Global.window_title = "%s - Pixelorama %s" % [name, Global.current_version] @@ -292,6 +296,10 @@ func serialize() -> Dictionary: for brush in brushes: brush_data.append({"size_x": brush.get_size().x, "size_y": brush.get_size().y}) + var reference_image_data := [] + for reference_image in reference_images: + reference_image_data.append(reference_image.serialize()) + var tile_mask_data := { "size_x": tiles.tile_mask.get_size().x, "size_y": tiles.tile_mask.get_size().y } @@ -316,6 +324,7 @@ func serialize() -> Dictionary: "symmetry_points": [x_symmetry_point, y_symmetry_point], "frames": frame_data, "brushes": brush_data, + "reference_images": reference_image_data, "export_directory_path": directory_path, "export_file_name": file_name, "export_file_format": file_format, @@ -397,6 +406,12 @@ func deserialize(dict: Dictionary) -> void: guide.has_focus = false guide.project = self Global.canvas.add_child(guide) + if dict.has("reference_images"): + for g in dict.reference_images: + var ri := ReferenceImage.new() + ri.project = self + ri.deserialize(g) + Global.canvas.add_child(ri) if dict.has("symmetry_points"): x_symmetry_point = dict.symmetry_points[0] y_symmetry_point = dict.symmetry_points[1] diff --git a/src/UI/Canvas/ReferenceImage.gd b/src/UI/Canvas/ReferenceImage.gd new file mode 100644 index 000000000000..e8854e15b6e9 --- /dev/null +++ b/src/UI/Canvas/ReferenceImage.gd @@ -0,0 +1,81 @@ +class_name ReferenceImage +extends Sprite +# A class describing a reference image + +signal properties_changed + +var project = Global.current_project + +var image_path: String = "" + + +func _ready() -> void: + project.reference_images.append(self) + + +func change_properties(): + emit_signal("properties_changed") + + +# Resets the position and scale of the reference image. +func position_reset(): + position = project.size / 2.0 + if texture != null: + scale = ( + Vector2.ONE + * min(project.size.x / texture.get_width(), project.size.y / texture.get_height()) + ) + else: + scale = Vector2.ONE + + +# Serialize details of the reference image. +func serialize(): + return { + "x": position.x, + "y": position.y, + "scale_x": scale.x, + "scale_y": scale.y, + "modulate_r": modulate.r, + "modulate_g": modulate.g, + "modulate_b": modulate.b, + "modulate_a": modulate.a, + "image_path": image_path + } + + +# Load details of the reference image from a dictionary. +# Be aware that new ReferenceImages are created via deserialization. +# This is because deserialization sets up some nice defaults. +func deserialize(d: Dictionary): + modulate = Color(1, 1, 1, 0.5) + if d.has("image_path"): + # Note that reference images are referred to by path. + # These images may be rather big. + # Also + image_path = d["image_path"] + var img = Image.new() + if img.load(image_path) == OK: + var itex = ImageTexture.new() + # don't do FLAG_REPEAT - it could cause visual issues + itex.create_from_image(img, Texture.FLAG_MIPMAPS | Texture.FLAG_FILTER) + texture = itex + # Now that the image may have been established... + position_reset() + if d.has("x"): + position.x = d["x"] + if d.has("y"): + position.y = d["y"] + if d.has("scale_x"): + scale.x = d["scale_x"] + if d.has("scale_y"): + scale.y = d["scale_y"] + if d.has("modulate_r"): + modulate.r = d["modulate_r"] + if d.has("modulate_g"): + modulate.g = d["modulate_g"] + if d.has("modulate_b"): + modulate.b = d["modulate_b"] + if d.has("modulate_a"): + modulate.a = d["modulate_a"] + change_properties() diff --git a/src/UI/Dialogs/PreviewDialog.gd b/src/UI/Dialogs/PreviewDialog.gd index d4a658e58b27..06d8614d6918 100644 --- a/src/UI/Dialogs/PreviewDialog.gd +++ b/src/UI/Dialogs/PreviewDialog.gd @@ -7,6 +7,7 @@ enum ImageImportOptions { NEW_FRAME, REPLACE_CEL, NEW_LAYER, + NEW_REFERENCE_IMAGE, PALETTE, BRUSH, PATTERN @@ -49,6 +50,7 @@ func _on_PreviewDialog_about_to_show() -> void: import_options.add_item("New frame") import_options.add_item("Replace cel") import_options.add_item("New layer") + import_options.add_item("New reference image") import_options.add_item("New palette") import_options.add_item("New brush") import_options.add_item("New pattern") @@ -141,6 +143,9 @@ func _on_PreviewDialog_confirmed() -> void: var frame_index: int = new_layer_options.get_node("AtFrameSpinbox").value - 1 OpenSave.open_image_as_new_layer(image, path.get_basename().get_file(), frame_index) + elif current_import_option == ImageImportOptions.NEW_REFERENCE_IMAGE: + OpenSave.import_reference_image_from_path(path) + elif current_import_option == ImageImportOptions.PALETTE: Palettes.import_palette_from_path(path) diff --git a/src/UI/ReferenceImageButton.gd b/src/UI/ReferenceImageButton.gd new file mode 100644 index 000000000000..94f38fc67f0c --- /dev/null +++ b/src/UI/ReferenceImageButton.gd @@ -0,0 +1,67 @@ +extends Container +# UI to handle reference image editing. + +var element: ReferenceImage +var _ignore_spinbox_changes = false + + +func _ready(): + $Interior/Path.text = element.image_path + element.connect("properties_changed", self, "_update_properties") + _update_properties() + + +func _update_properties(): + # This is because otherwise a little dance will occur. + # This also breaks non-uniform scales (not supported UI-wise, but...) + _ignore_spinbox_changes = true + $Interior/Options/Scale.value = element.scale.x * 100 + $Interior/Options/X.value = element.position.x + $Interior/Options/Y.value = element.position.y + $Interior/Options/X.max_value = element.project.size.x + $Interior/Options/Y.max_value = element.project.size.y + $Interior/Options2/Opacity.value = element.modulate.a * 100 + _ignore_spinbox_changes = false + + +func _on_Reset_pressed(): + element.position_reset() + element.change_properties() + + +func _on_Remove_pressed(): + var index = Global.current_project.reference_images.find(element) + if index != -1: + queue_free() + element.queue_free() + Global.current_project.reference_images.remove(index) + Global.current_project.change_project() + + +func _on_Scale_value_changed(value): + if _ignore_spinbox_changes: + return + element.scale.x = value / 100 + element.scale.y = value / 100 + element.change_properties() + + +func _on_X_value_changed(value): + if _ignore_spinbox_changes: + return + element.position.x = value + element.change_properties() + + +func _on_Y_value_changed(value): + if _ignore_spinbox_changes: + return + element.position.y = value + element.change_properties() + + +func _on_Opacity_value_changed(value): + if _ignore_spinbox_changes: + return + element.modulate.a = value / 100 + element.change_properties() diff --git a/src/UI/ReferenceImageButton.tscn b/src/UI/ReferenceImageButton.tscn new file mode 100644 index 000000000000..ff99f99758ce --- /dev/null +++ b/src/UI/ReferenceImageButton.tscn @@ -0,0 +1,94 @@ +[gd_scene load_steps=3 format=2] + +[ext_resource path="res://src/UI/ReferenceImageButton.gd" type="Script" id=1] +[ext_resource path="res://src/UI/Nodes/ValueSlider.tscn" type="PackedScene" id=2] + +[node name="ReferenceImageButton" type="PanelContainer"] +anchor_right = 1.0 +anchor_bottom = 1.0 +margin_right = -969.0 +margin_bottom = -581.0 +size_flags_horizontal = 3 +script = ExtResource( 1 ) + +[node name="Interior" type="VBoxContainer" parent="."] +margin_left = 7.0 +margin_top = 7.0 +margin_right = 304.0 +margin_bottom = 132.0 + +[node name="Path" type="Label" parent="Interior"] +margin_right = 297.0 +margin_bottom = 14.0 +size_flags_horizontal = 3 +autowrap = true + +[node name="Options" type="HBoxContainer" parent="Interior"] +margin_top = 18.0 +margin_right = 297.0 +margin_bottom = 42.0 + +[node name="Label2" type="Label" parent="Interior/Options"] +margin_top = 5.0 +margin_right = 56.0 +margin_bottom = 19.0 +text = "Position:" + +[node name="X" parent="Interior/Options" instance=ExtResource( 2 )] +margin_left = 60.0 +margin_right = 122.0 +allow_greater = true +allow_lesser = true + +[node name="Y" parent="Interior/Options" instance=ExtResource( 2 )] +margin_left = 126.0 +margin_right = 189.0 +allow_greater = true +allow_lesser = true + +[node name="Label" type="Label" parent="Interior/Options"] +margin_left = 193.0 +margin_top = 5.0 +margin_right = 230.0 +margin_bottom = 19.0 +text = "Scale:" + +[node name="Scale" parent="Interior/Options" instance=ExtResource( 2 )] +margin_left = 234.0 +margin_right = 297.0 +allow_greater = true +allow_lesser = true + +[node name="Options2" type="HBoxContainer" parent="Interior"] +margin_top = 46.0 +margin_right = 297.0 +margin_bottom = 70.0 + +[node name="Label" type="Label" parent="Interior/Options2"] +margin_top = 5.0 +margin_right = 53.0 +margin_bottom = 19.0 +text = "Opacity:" + +[node name="Opacity" parent="Interior/Options2" instance=ExtResource( 2 )] +margin_left = 57.0 +margin_right = 177.0 + +[node name="Reset" type="Button" parent="Interior/Options2"] +margin_left = 181.0 +margin_right = 229.0 +margin_bottom = 24.0 +text = "Reset" + +[node name="Remove" type="Button" parent="Interior/Options2"] +margin_left = 233.0 +margin_right = 297.0 +margin_bottom = 24.0 +text = "Remove" + +[connection signal="value_changed" from="Interior/Options/X" to="." method="_on_X_value_changed"] +[connection signal="value_changed" from="Interior/Options/Y" to="." method="_on_Y_value_changed"] +[connection signal="value_changed" from="Interior/Options/Scale" to="." method="_on_Scale_value_changed"] +[connection signal="value_changed" from="Interior/Options2/Opacity" to="." method="_on_Opacity_value_changed"] +[connection signal="pressed" from="Interior/Options2/Reset" to="." method="_on_Reset_pressed"] +[connection signal="pressed" from="Interior/Options2/Remove" to="." method="_on_Remove_pressed"] diff --git a/src/UI/ReferencesPanel.gd b/src/UI/ReferencesPanel.gd new file mode 100644 index 000000000000..52f0d7b829ff --- /dev/null +++ b/src/UI/ReferencesPanel.gd @@ -0,0 +1,21 @@ +class_name ReferencesPanel +extends VBoxContainer +# Panel for reference image management + +onready var list = $"Scroll/List" + + +func project_changed(): + for c in list.get_children(): + c.queue_free() + # Just do this here because I'm not sure where it's done. + # By all means, change this! + for ref in Global.canvas.get_children(): + if ref is ReferenceImage: + ref.visible = false + # And update. + for ref in Global.current_project.reference_images: + ref.visible = true + var l = preload("res://src/UI/ReferenceImageButton.tscn").instance() + l.element = ref + list.add_child(l) diff --git a/src/UI/ReferencesPanel.tscn b/src/UI/ReferencesPanel.tscn new file mode 100644 index 000000000000..990e57a3fc58 --- /dev/null +++ b/src/UI/ReferencesPanel.tscn @@ -0,0 +1,24 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://src/UI/ReferencesPanel.gd" type="Script" id=1] + +[node name="References" type="VBoxContainer"] +script = ExtResource( 1 ) + +[node name="Label" type="Label" parent="."] +margin_right = 1.0 +margin_bottom = 1000.0 +text = "When opening an image, it may be imported as a reference." +autowrap = true + +[node name="Scroll" type="ScrollContainer" parent="."] +margin_top = 1004.0 +margin_right = 1.0 +margin_bottom = 1004.0 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="List" type="VBoxContainer" parent="Scroll"] +margin_right = 1.0 +size_flags_horizontal = 3 +size_flags_vertical = 3 diff --git a/src/UI/UI.tscn b/src/UI/UI.tscn index 0cda4bd011a5..8728d9508718 100644 --- a/src/UI/UI.tscn +++ b/src/UI/UI.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=47 format=2] +[gd_scene load_steps=48 format=2] [ext_resource path="res://src/UI/Tools.tscn" type="PackedScene" id=1] [ext_resource path="res://src/UI/Canvas/CanvasPreview.tscn" type="PackedScene" id=2] @@ -10,6 +10,7 @@ [ext_resource path="res://src/Shaders/Greyscale.gdshader" type="Shader" id=8] [ext_resource path="res://src/Shaders/TransparentChecker.shader" type="Shader" id=9] [ext_resource path="res://src/UI/GlobalToolOptions.tscn" type="PackedScene" id=10] +[ext_resource path="res://src/UI/ReferencesPanel.tscn" type="PackedScene" id=11] [ext_resource path="res://addons/dockable_container/layout.gd" type="Script" id=14] [ext_resource path="res://src/UI/CanvasPreviewContainer.tscn" type="PackedScene" id=16] [ext_resource path="res://src/UI/ColorPickers.tscn" type="PackedScene" id=17] @@ -33,7 +34,7 @@ shader_param/size = Vector2( 100, 100 ) [sub_resource type="Resource" id=1] resource_name = "Tabs" script = ExtResource( 36 ) -names = PoolStringArray( "Tools" ) +names = PoolStringArray( "Tools", "References" ) current_tab = 0 [sub_resource type="Resource" id=8] @@ -223,20 +224,20 @@ margin_bottom = -4.0 [node name="Main Canvas" type="VBoxContainer" parent="DockableContainer"] margin_left = 60.0 margin_top = 8.0 -margin_right = 938.0 -margin_bottom = 532.0 +margin_right = 1063.0 +margin_bottom = 642.5 size_flags_horizontal = 3 size_flags_vertical = 3 custom_constants/separation = 0 [node name="TabsContainer" type="PanelContainer" parent="DockableContainer/Main Canvas"] -margin_right = 878.0 +margin_right = 1003.0 margin_bottom = 38.0 [node name="Tabs" type="Tabs" parent="DockableContainer/Main Canvas/TabsContainer"] margin_left = 7.0 margin_top = 7.0 -margin_right = 871.0 +margin_right = 996.0 margin_bottom = 31.0 tab_align = 0 tab_close_display_policy = 2 @@ -245,7 +246,7 @@ script = ExtResource( 3 ) [node name="HorizontalRuler" type="Button" parent="DockableContainer/Main Canvas"] margin_top = 38.0 -margin_right = 878.0 +margin_right = 1003.0 margin_bottom = 58.0 rect_min_size = Vector2( 0, 16 ) focus_mode = 0 @@ -256,15 +257,15 @@ script = ExtResource( 6 ) [node name="ViewportandVerticalRuler" type="HBoxContainer" parent="DockableContainer/Main Canvas"] margin_top = 58.0 -margin_right = 878.0 -margin_bottom = 524.0 +margin_right = 1003.0 +margin_bottom = 634.0 size_flags_horizontal = 3 size_flags_vertical = 3 custom_constants/separation = 0 [node name="VerticalRuler" type="Button" parent="DockableContainer/Main Canvas/ViewportandVerticalRuler"] margin_right = 16.0 -margin_bottom = 466.0 +margin_bottom = 576.0 rect_min_size = Vector2( 16, 0 ) focus_mode = 0 mouse_default_cursor_shape = 15 @@ -276,8 +277,8 @@ script = ExtResource( 4 ) [node name="ViewportContainer" type="ViewportContainer" parent="DockableContainer/Main Canvas/ViewportandVerticalRuler"] margin_left = 16.0 -margin_right = 878.0 -margin_bottom = 466.0 +margin_right = 1003.0 +margin_bottom = 576.0 focus_mode = 2 mouse_default_cursor_shape = 3 size_flags_horizontal = 3 @@ -286,7 +287,7 @@ stretch = true script = ExtResource( 23 ) [node name="Viewport" type="Viewport" parent="DockableContainer/Main Canvas/ViewportandVerticalRuler/ViewportContainer"] -size = Vector2( 862, 466 ) +size = Vector2( 987, 576 ) handle_input_locally = false usage = 0 render_target_update_mode = 3 @@ -339,30 +340,31 @@ script = ExtResource( 7 ) [node name="Animation Timeline" parent="DockableContainer" instance=ExtResource( 18 )] margin_left = 60.0 -margin_top = 556.0 -margin_right = 938.0 +margin_top = 666.5 +margin_right = 1063.0 margin_bottom = 716.0 [node name="Canvas Preview" parent="DockableContainer" instance=ExtResource( 16 )] -margin_left = 958.0 +margin_left = 1083.0 margin_top = 8.0 margin_right = 1276.0 margin_bottom = 98.0 [node name="Color Pickers" parent="DockableContainer" instance=ExtResource( 17 )] +margin_left = 1083.0 margin_top = 122.0 margin_bottom = 168.0 [node name="Global Tool Options" parent="DockableContainer" instance=ExtResource( 10 )] -margin_left = 958.0 +margin_left = 1083.0 margin_top = 192.0 margin_right = 1276.0 margin_bottom = 242.0 [node name="Left Tool Options" type="ScrollContainer" parent="DockableContainer"] -margin_left = 958.0 +margin_left = 1083.0 margin_top = 266.0 -margin_right = 1107.0 +margin_right = 1169.5 margin_bottom = 592.0 rect_min_size = Vector2( 72, 72 ) __meta__ = { @@ -370,32 +372,36 @@ __meta__ = { } [node name="LeftPanelContainer" type="PanelContainer" parent="DockableContainer/Left Tool Options"] -margin_right = 149.0 -margin_bottom = 326.0 +margin_right = 130.0 +margin_bottom = 314.0 rect_min_size = Vector2( 130, 0 ) size_flags_horizontal = 3 size_flags_vertical = 3 [node name="Right Tool Options" type="ScrollContainer" parent="DockableContainer"] -margin_left = 1127.0 +margin_left = 1189.5 margin_top = 266.0 margin_right = 1276.0 margin_bottom = 592.0 rect_min_size = Vector2( 72, 72 ) [node name="RightPanelContainer" type="PanelContainer" parent="DockableContainer/Right Tool Options"] -margin_right = 149.0 -margin_bottom = 326.0 +margin_right = 130.0 +margin_bottom = 314.0 rect_min_size = Vector2( 130, 0 ) size_flags_horizontal = 3 size_flags_vertical = 3 [node name="Palettes" parent="DockableContainer" instance=ExtResource( 20 )] -margin_left = 958.0 +margin_left = 1083.0 margin_top = 616.0 margin_right = 1276.0 margin_bottom = 716.0 +[node name="References" parent="DockableContainer" instance=ExtResource( 11 )] +margin_right = 33.0 +margin_bottom = 14.0 + [connection signal="item_rect_changed" from="DockableContainer/Main Canvas" to="." method="_on_main_canvas_item_rect_changed"] [connection signal="visibility_changed" from="DockableContainer/Main Canvas" to="." method="_on_main_canvas_visibility_changed"] [connection signal="reposition_active_tab_request" from="DockableContainer/Main Canvas/TabsContainer/Tabs" to="DockableContainer/Main Canvas/TabsContainer/Tabs" method="_on_Tabs_reposition_active_tab_request"]