Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reference Images #771

Merged
merged 5 commits into from Nov 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 12 additions & 0 deletions project.godot
Expand Up @@ -124,6 +124,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",
Expand Down Expand Up @@ -183,6 +193,8 @@ _global_script_class_icons={
"PixelCel": "",
"PixelLayer": "",
"Project": "",
"ReferenceImage": "",
"ReferencesPanel": "",
"SelectionMap": "",
"SelectionTool": "",
"ShaderImageEffect": "",
Expand Down
2 changes: 2 additions & 0 deletions src/Autoload/Global.gd
Expand Up @@ -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")
Expand Down
9 changes: 9 additions & 0 deletions src/Autoload/OpenSave.gd
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions src/Classes/Project.gd
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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
}
Expand All @@ -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,
Expand Down Expand Up @@ -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]
Expand Down
81 changes: 81 additions & 0 deletions 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()
5 changes: 5 additions & 0 deletions src/UI/Dialogs/PreviewDialog.gd
Expand Up @@ -7,6 +7,7 @@ enum ImageImportOptions {
NEW_FRAME,
REPLACE_CEL,
NEW_LAYER,
NEW_REFERENCE_IMAGE,
PALETTE,
BRUSH,
PATTERN
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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)

Expand Down
67 changes: 67 additions & 0 deletions 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()
94 changes: 94 additions & 0 deletions 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"]