diff --git a/project.godot b/project.godot index 42c9bc0d6..e19ae7308 100644 --- a/project.godot +++ b/project.godot @@ -159,6 +159,11 @@ _global_script_classes=[ { "language": "GDScript", "path": "res://src/UI/Buttons/PatternsPopup.gd" }, { +"base": "Line2D", +"class": "PerspectiveLine", +"language": "GDScript", +"path": "res://src/UI/PerspectiveEditor/PerspectiveLine.gd" +}, { "base": "BaseCel", "class": "PixelCel", "language": "GDScript", @@ -250,6 +255,7 @@ _global_script_class_icons={ "PalettePanel": "", "PaletteSwatch": "", "Patterns": "", +"PerspectiveLine": "", "PixelCel": "", "PixelLayer": "", "Project": "", diff --git a/src/Autoload/Global.gd b/src/Autoload/Global.gd index 0b1b0d690..238248e7c 100644 --- a/src/Autoload/Global.gd +++ b/src/Autoload/Global.gd @@ -20,6 +20,7 @@ enum ViewMenu { SHOW_PIXEL_GRID, SHOW_RULERS, SHOW_GUIDES, + SHOW_MOUSE_GUIDES, SNAP_TO, } enum WindowMenu { WINDOW_OPACITY, PANELS, LAYOUTS, MOVABLE_PANELS, ZEN_MODE, FULLSCREEN_MODE } @@ -138,6 +139,7 @@ var draw_grid := false var draw_pixel_grid := false var show_rulers := true var show_guides := true +var show_mouse_guides := false var snapping_distance := 10.0 var snap_to_rectangular_grid := false var snap_to_guides := false @@ -181,6 +183,7 @@ 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("Reference Images") +onready var perspective_editor := control.find_node("Perspective Editor") onready var top_menu_container: Panel = control.find_node("TopMenuContainer") onready var rotation_level_button: Button = control.find_node("RotationLevel") diff --git a/src/Classes/Project.gd b/src/Classes/Project.gd index 44be487f9..92b6f031e 100644 --- a/src/Classes/Project.gd +++ b/src/Classes/Project.gd @@ -24,6 +24,7 @@ var animation_tags := [] setget _animation_tags_changed # Array of AnimationTag var guides := [] # Array of Guides var brushes := [] # Array of Images var reference_images := [] # Array of ReferenceImages +var vanishing_points := [] # Array of Vanishing Points var fps := 6.0 var x_symmetry_point @@ -91,6 +92,12 @@ func remove() -> void: undo_redo.free() for ri in reference_images: ri.queue_free() + if self == Global.current_project: + # If the project is not current_project then the points need not be removed + for point_idx in vanishing_points.size(): + var editor = Global.perspective_editor + for c in editor.vanishing_point_container.get_children(): + c.queue_free() for guide in guides: guide.queue_free() # Prevents memory leak (due to the layers' project reference stopping ref counting from freeing) @@ -186,6 +193,7 @@ func change_project() -> void: Global.horizontal_ruler.update() Global.vertical_ruler.update() Global.references_panel.project_changed() + Global.perspective_editor.update() Global.cursor_position_label.text = "[%s×%s]" % [size.x, size.y] Global.window_title = "%s - Pixelorama %s" % [name, Global.current_version] @@ -325,6 +333,7 @@ func serialize() -> Dictionary: "frames": frame_data, "brushes": brush_data, "reference_images": reference_image_data, + "vanishing_points": vanishing_points, "export_directory_path": directory_path, "export_file_name": file_name, "export_file_format": file_format, @@ -412,6 +421,9 @@ func deserialize(dict: Dictionary) -> void: ri.project = self ri.deserialize(g) Global.canvas.add_child(ri) + if dict.has("vanishing_points"): + vanishing_points = dict.vanishing_points + Global.perspective_editor.update() if dict.has("symmetry_points"): x_symmetry_point = dict.symmetry_points[0] y_symmetry_point = dict.symmetry_points[1] diff --git a/src/Main.gd b/src/Main.gd index e066649be..26b8a74d2 100644 --- a/src/Main.gd +++ b/src/Main.gd @@ -416,6 +416,7 @@ func _exit_tree() -> void: Global.config_cache.set_value("view_menu", "draw_pixel_grid", Global.draw_pixel_grid) Global.config_cache.set_value("view_menu", "show_rulers", Global.show_rulers) Global.config_cache.set_value("view_menu", "show_guides", Global.show_guides) + Global.config_cache.set_value("view_menu", "show_mouse_guides", Global.show_mouse_guides) Global.config_cache.save("user://cache.ini") var i := 0 diff --git a/src/UI/Canvas/Canvas.gd b/src/UI/Canvas/Canvas.gd index 58457256b..b46db3bb0 100644 --- a/src/UI/Canvas/Canvas.gd +++ b/src/UI/Canvas/Canvas.gd @@ -16,6 +16,7 @@ onready var grid = $Grid onready var selection = $Selection onready var indicators = $Indicators onready var previews = $Previews +onready var mouse_guide_container = $MouseGuideContainer func _ready() -> void: diff --git a/src/UI/Canvas/Canvas.tscn b/src/UI/Canvas/Canvas.tscn index 37a2127fd..905f96776 100644 --- a/src/UI/Canvas/Canvas.tscn +++ b/src/UI/Canvas/Canvas.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=15 format=2] +[gd_scene load_steps=16 format=2] [ext_resource path="res://src/UI/Canvas/Canvas.gd" type="Script" id=1] [ext_resource path="res://src/UI/Canvas/Grid.gd" type="Script" id=2] @@ -10,6 +10,7 @@ [ext_resource path="res://src/UI/Canvas/Selection.gd" type="Script" id=8] [ext_resource path="res://src/Shaders/MarchingAntsOutline.shader" type="Shader" id=9] [ext_resource path="res://src/Shaders/AutoInvertColors.shader" type="Shader" id=10] +[ext_resource path="res://src/UI/Canvas/MouseGuideContainer.tscn" type="PackedScene" id=11] [ext_resource path="res://src/UI/Canvas/OnionSkinning.gd" type="Script" id=12] [sub_resource type="CanvasItemMaterial" id=1] @@ -72,3 +73,5 @@ script = ExtResource( 12 ) [node name="OnionFuture" type="Node2D" parent="."] script = ExtResource( 12 ) + +[node name="MouseGuideContainer" parent="." instance=ExtResource( 11 )] diff --git a/src/UI/Canvas/MouseGuide.gd b/src/UI/Canvas/MouseGuide.gd new file mode 100644 index 000000000..8e288cf25 --- /dev/null +++ b/src/UI/Canvas/MouseGuide.gd @@ -0,0 +1,77 @@ +extends Line2D + +enum Types { VERTICAL, HORIZONTAL } +const INPUT_WIDTH := 4 +export var type := 0 +var track_mouse := true + + +func _ready() -> void: + # Add a subtle difference to the normal guide color by mixing in some green + default_color = Global.guide_color.linear_interpolate(Color(0.2, 0.92, 0.2), .6) + width = Global.camera.zoom.x * 2 + draw_guide_line() + + +func draw_guide_line(): + if type == Types.HORIZONTAL: + points[0] = Vector2(-19999, 0) + points[1] = Vector2(19999, 0) + else: + points[0] = Vector2(0, 19999) + points[1] = Vector2(0, -19999) + + +func _input(event: InputEvent) -> void: + if !Global.show_mouse_guides or !Global.can_draw or !Global.has_focus: + visible = false + return + visible = true + if event is InputEventMouseMotion: + var tmp_transform = get_canvas_transform().affine_inverse() + var tmp_position = Global.main_viewport.get_local_mouse_position() + var mouse_point = (tmp_transform.basis_xform(tmp_position) + tmp_transform.origin).snapped( + Vector2(0.5, 0.5) + ) + + var project_size = Global.current_project.size + if Rect2(Vector2.ZERO, project_size).has_point(mouse_point): + visible = true + else: + visible = false + return + if type == Types.HORIZONTAL: + points[0].y = mouse_point.y + points[1].y = mouse_point.y + else: + points[0].x = mouse_point.x + points[1].x = mouse_point.x + update() + + +func _draw() -> void: + width = Global.camera.zoom.x * 2 + var viewport_size: Vector2 = Global.main_viewport.rect_size + var zoom: Vector2 = Global.camera.zoom + + # viewport_poly is an array of the points that make up the corners of the viewport + var viewport_poly := [ + Vector2.ZERO, Vector2(viewport_size.x, 0), viewport_size, Vector2(0, viewport_size.y) + ] + # Adjusting viewport_poly to take into account the camera offset, zoom, and rotation + for p in range(viewport_poly.size()): + viewport_poly[p] = ( + viewport_poly[p].rotated(Global.camera.rotation) * zoom + + Vector2( + ( + Global.camera.offset.x + - (viewport_size.rotated(Global.camera.rotation).x / 2) * zoom.x + ), + ( + Global.camera.offset.y + - (viewport_size.rotated(Global.camera.rotation).y / 2) * zoom.y + ) + ) + ) + + draw_set_transform(viewport_poly[0], Global.camera.rotation, zoom * 2) diff --git a/src/UI/Canvas/MouseGuideContainer.tscn b/src/UI/Canvas/MouseGuideContainer.tscn new file mode 100644 index 000000000..f1f467d85 --- /dev/null +++ b/src/UI/Canvas/MouseGuideContainer.tscn @@ -0,0 +1,31 @@ +[gd_scene load_steps=4 format=2] + +[ext_resource path="res://assets/graphics/dotted_line.png" type="Texture" id=1] +[ext_resource path="res://src/UI/Canvas/MouseGuide.gd" type="Script" id=2] + +[sub_resource type="AtlasTexture" id=1] +flags = 2 +atlas = ExtResource( 1 ) +region = Rect2( 0, 0, 8, 1 ) + +[node name="MouseGuideContainer" type="Node2D"] + +[node name="Vertical" type="Line2D" parent="."] +visible = false +points = PoolVector2Array( 0, 19999, 0, -19999 ) +default_color = Color( 1, 1, 1, 1 ) +texture = SubResource( 1 ) +texture_mode = 1 +script = ExtResource( 2 ) + +[node name="Horizontal" type="Line2D" parent="."] +visible = false +points = PoolVector2Array( -19999, 0, 19999, 0 ) +default_color = Color( 1, 1, 1, 1 ) +texture = SubResource( 1 ) +texture_mode = 1 +script = ExtResource( 2 ) +__meta__ = { +"_editor_description_": "" +} +type = 1 diff --git a/src/UI/PerspectiveEditor/LineButton.gd b/src/UI/PerspectiveEditor/LineButton.gd new file mode 100644 index 000000000..a6288dad6 --- /dev/null +++ b/src/UI/PerspectiveEditor/LineButton.gd @@ -0,0 +1,9 @@ +extends Button + +onready var length_slider = $"%LengthSlider" + + +func _ready(): + var p_size = Global.current_project.size + var suitable_length = sqrt(pow(p_size.x, 2) + pow(p_size.y, 2)) + length_slider.max_value = suitable_length diff --git a/src/UI/PerspectiveEditor/LineButton.tscn b/src/UI/PerspectiveEditor/LineButton.tscn new file mode 100644 index 000000000..51ef1a4da --- /dev/null +++ b/src/UI/PerspectiveEditor/LineButton.tscn @@ -0,0 +1,61 @@ +[gd_scene load_steps=3 format=2] + +[ext_resource path="res://src/UI/Nodes/ValueSlider.tscn" type="PackedScene" id=1] +[ext_resource path="res://src/UI/Nodes/CollapsibleContainer.gd" type="Script" id=3] + +[node name="LineButton" type="VBoxContainer"] +margin_right = 159.0 +margin_bottom = 20.0 +theme_type_variation = "CollapsibleContainer" +script = ExtResource( 3 ) + +[node name="HBoxContainer" type="HBoxContainer" parent="."] +visible = false +margin_top = 24.0 +margin_right = 159.0 +margin_bottom = 76.0 + +[node name="Spacer" type="Control" parent="HBoxContainer"] +margin_right = 20.0 +margin_bottom = 52.0 +rect_min_size = Vector2( 20, 0 ) + +[node name="VSeparator" type="VSeparator" parent="HBoxContainer"] +margin_left = 24.0 +margin_right = 28.0 +margin_bottom = 52.0 + +[node name="Properties" type="VBoxContainer" parent="HBoxContainer"] +margin_left = 32.0 +margin_right = 83.0 +margin_bottom = 52.0 +size_flags_horizontal = 3 + +[node name="AngleSlider" parent="HBoxContainer/Properties" instance=ExtResource( 1 )] +margin_right = 51.0 +max_value = 359.999 +step = 0.001 +prefix = "Angle:" +suffix = "°" + +[node name="LengthSlider" parent="HBoxContainer/Properties" instance=ExtResource( 1 )] +unique_name_in_owner = true +margin_top = 28.0 +margin_right = 51.0 +margin_bottom = 52.0 +max_value = 19999.0 +value = 19999.0 +allow_greater = true +prefix = "Length:" +suffix = "px" + +[node name="Delete" type="Button" parent="HBoxContainer"] +margin_left = 87.0 +margin_right = 151.0 +margin_bottom = 52.0 +text = "Remove" + +[node name="VSeparator2" type="VSeparator" parent="HBoxContainer"] +margin_left = 155.0 +margin_right = 159.0 +margin_bottom = 52.0 diff --git a/src/UI/PerspectiveEditor/PerspectiveEditor.gd b/src/UI/PerspectiveEditor/PerspectiveEditor.gd new file mode 100644 index 000000000..9fa8367e3 --- /dev/null +++ b/src/UI/PerspectiveEditor/PerspectiveEditor.gd @@ -0,0 +1,67 @@ +extends Control + +var axes: Node2D +var do_pool = [] # A pool that stores data of points removed by undo +var delete_pool = [] # A pool that containg deleted data and their index +var vanishing_point_res := preload("res://src/UI/PerspectiveEditor/VanishingPoint.tscn") + +onready var vanishing_point_container = $"%VanishingPointContainer" + + +func _on_AddPoint_pressed() -> void: + do_pool.clear() # Reset + var project = Global.current_project + project.undos += 1 + project.undo_redo.create_action("Add Vanishing Point") + project.undo_redo.add_do_method(self, "add_vanishing_point", true) + project.undo_redo.add_undo_method(self, "undo_add_vanishing_point") + project.undo_redo.commit_action() + + +func update(): + for c in vanishing_point_container.get_children(): + c.queue_free() + for idx in Global.current_project.vanishing_points.size(): + var point_data = Global.current_project.vanishing_points[idx] + var vanishing_point := vanishing_point_res.instance() + vanishing_point_container.add_child(vanishing_point) + vanishing_point.initiate(point_data, idx) + + +func add_vanishing_point(is_redo := false): + var vanishing_point := vanishing_point_res.instance() + vanishing_point_container.add_child(vanishing_point) + if is_redo and !do_pool.empty(): + vanishing_point.initiate(do_pool.pop_back()) + vanishing_point.update_data_to_project() + else: + vanishing_point.initiate() + + +func undo_add_vanishing_point(): + var point = vanishing_point_container.get_child(vanishing_point_container.get_child_count() - 1) + point.queue_free() + do_pool.append(point.data.duplicate()) + point.update_data_to_project(true) + + +func delete_point(idx): + var project = Global.current_project + project.undos += 1 + project.undo_redo.create_action("Delete Vanishing Point") + project.undo_redo.add_do_method(self, "do_delete_point", idx) + project.undo_redo.add_undo_method(self, "undo_delete_point", idx) + project.undo_redo.commit_action() + + +func do_delete_point(idx): + var point = vanishing_point_container.get_child(idx) + delete_pool.append(point.data) + point.queue_free() + point.update_data_to_project(true) + + +func undo_delete_point(idx): + var point = delete_pool.pop_back() + Global.current_project.vanishing_points.insert(idx, point) + update() diff --git a/src/UI/PerspectiveEditor/PerspectiveEditor.tscn b/src/UI/PerspectiveEditor/PerspectiveEditor.tscn new file mode 100644 index 000000000..ccf31ac7b --- /dev/null +++ b/src/UI/PerspectiveEditor/PerspectiveEditor.tscn @@ -0,0 +1,72 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://src/UI/PerspectiveEditor/PerspectiveEditor.gd" type="Script" id=1] + +[node name="PerspectiveEditor" type="Control"] +anchor_right = 1.0 +anchor_bottom = 1.0 +margin_right = -1013.0 +margin_bottom = -510.0 +rect_min_size = Vector2( 270, 10 ) +script = ExtResource( 1 ) + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +self_modulate = Color( 0.698039, 0.698039, 0.698039, 0.698039 ) +anchor_right = 1.0 +anchor_bottom = 1.0 +margin_left = 4.0 +margin_right = -4.0 +rect_clip_content = true +size_flags_vertical = 3 +__meta__ = { +"_editor_description_": "" +} + +[node name="Header" type="HBoxContainer" parent="VBoxContainer"] +margin_right = 262.0 +margin_bottom = 30.0 +custom_constants/separation = 0 + +[node name="Label" type="Label" parent="VBoxContainer/Header"] +margin_top = 8.0 +margin_right = 232.0 +margin_bottom = 22.0 +size_flags_horizontal = 3 +theme_type_variation = "Header" +text = "Perspective Editor" +align = 1 +valign = 1 + +[node name="AddPoint" type="Button" parent="VBoxContainer/Header"] +margin_left = 232.0 +margin_right = 262.0 +margin_bottom = 30.0 +rect_min_size = Vector2( 30, 30 ) +text = "+" +clip_text = true + +[node name="HSeparator" type="HSeparator" parent="VBoxContainer"] +margin_top = 34.0 +margin_right = 262.0 +margin_bottom = 38.0 + +[node name="HSeparator2" type="HSeparator" parent="VBoxContainer"] +margin_top = 42.0 +margin_right = 262.0 +margin_bottom = 46.0 + +[node name="Content" type="ScrollContainer" parent="VBoxContainer"] +margin_top = 50.0 +margin_right = 262.0 +margin_bottom = 210.0 +size_flags_vertical = 3 + +[node name="VanishingPointContainer" type="VBoxContainer" parent="VBoxContainer/Content"] +unique_name_in_owner = true +margin_right = 262.0 +margin_bottom = 160.0 +size_flags_horizontal = 3 +size_flags_vertical = 3 +custom_constants/separation = 5 + +[connection signal="pressed" from="VBoxContainer/Header/AddPoint" to="." method="_on_AddPoint_pressed"] diff --git a/src/UI/PerspectiveEditor/PerspectiveLine.gd b/src/UI/PerspectiveEditor/PerspectiveLine.gd new file mode 100644 index 000000000..dafb807fa --- /dev/null +++ b/src/UI/PerspectiveEditor/PerspectiveLine.gd @@ -0,0 +1,101 @@ +class_name PerspectiveLine +extends Line2D + +const INPUT_WIDTH := 4 +var hidden = false +var track_mouse := false +var _data = {"start": Vector2.ZERO, "angle": 0, "length": 19999, "color": Color.black} + + +func initiate(data: Dictionary): + width = Global.camera.zoom.x * 2 + Global.canvas.add_child(self) + refresh(data) + + +func refresh(data: Dictionary): + _data = data + default_color = data.color + draw_perspective_line() + + +func draw_perspective_line(): + var angle = -_data.angle + points[0] = _data.start + if hidden: + points[1] = _data.start + else: + points[1] = ( + _data.start + + Vector2(_data.length * cos(deg2rad(angle)), _data.length * sin(deg2rad(angle))) + ) + + +func hide_perspective_line(): + points[1] = _data.start + hidden = true + + +func _input(event: InputEvent) -> void: + if event is InputEventMouse: + if track_mouse: + if !Global.can_draw or !Global.has_focus: + hide_perspective_line() + return + default_color.a = 0.5 + var tmp_transform = get_canvas_transform().affine_inverse() + var tmp_position = Global.main_viewport.get_local_mouse_position() + var mouse_point = tmp_transform.basis_xform(tmp_position) + tmp_transform.origin + var project_size = Global.current_project.size + if Rect2(Vector2.ZERO, project_size).has_point(mouse_point): + hidden = false + draw_perspective_line() + var rel_vector = mouse_point - _data.start + var test_vector = Vector2(_data.start.x, 0) + if sign(test_vector.x) == 0: + test_vector.x += 0.5 + + _data.angle = rad2deg(test_vector.angle_to(rel_vector)) + if sign(test_vector.x) == -1: + _data.angle += 180 + + points[1] = ( + _data.start + + Vector2( + _data.length * cos(deg2rad(_data.angle)), + _data.length * sin(deg2rad(_data.angle)) + ) + ) + else: + hide_perspective_line() + update() + + +func _draw() -> void: + draw_circle(_data.start, Global.camera.zoom.x * 5, default_color) + width = Global.camera.zoom.x * 2 + if hidden: # Hidden line + return + var viewport_size: Vector2 = Global.main_viewport.rect_size + var zoom: Vector2 = Global.camera.zoom + # viewport_poly is an array of the points that make up the corners of the viewport + var viewport_poly := [ + Vector2.ZERO, Vector2(viewport_size.x, 0), viewport_size, Vector2(0, viewport_size.y) + ] + # Adjusting viewport_poly to take into account the camera offset, zoom, and rotation + for p in range(viewport_poly.size()): + viewport_poly[p] = ( + viewport_poly[p].rotated(Global.camera.rotation) * zoom + + Vector2( + ( + Global.camera.offset.x + - (viewport_size.rotated(Global.camera.rotation).x / 2) * zoom.x + ), + ( + Global.camera.offset.y + - (viewport_size.rotated(Global.camera.rotation).y / 2) * zoom.y + ) + ) + ) + # If there's no intersection with a viewport edge, show string in top left corner + draw_set_transform(viewport_poly[0], Global.camera.rotation, zoom * 2) diff --git a/src/UI/PerspectiveEditor/PerspectiveLine.tscn b/src/UI/PerspectiveEditor/PerspectiveLine.tscn new file mode 100644 index 000000000..3cf1a1c3b --- /dev/null +++ b/src/UI/PerspectiveEditor/PerspectiveLine.tscn @@ -0,0 +1,7 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://src/UI/PerspectiveEditor/PerspectiveLine.gd" type="Script" id=1] + +[node name="PerspectiveLine" type="Line2D"] +points = PoolVector2Array( 0, 0, 64, 0 ) +script = ExtResource( 1 ) diff --git a/src/UI/PerspectiveEditor/PointCollapseContainer.gd b/src/UI/PerspectiveEditor/PointCollapseContainer.gd new file mode 100644 index 000000000..0e6642340 --- /dev/null +++ b/src/UI/PerspectiveEditor/PointCollapseContainer.gd @@ -0,0 +1,41 @@ +extends Button + +# This is NOT related to the CollapsibleContainer class (though it behaves similarly) +# i did it like this because the "Content" is part of a different node +export var point_text := "" setget _set_text +export var visible_content := false setget _set_visible_content +onready var content = $"%Content" + + +func _ready() -> void: + _set_visible(pressed) + content.connect("visibility_changed", self, "_child_visibility_changed") + + +func _set_text(value: String) -> void: + $Label.text = value + rect_min_size = $Label.rect_size + + +func _set_visible_content(value: bool) -> void: + visible_content = value + pressed = value + + +func _on_Button_toggled(button_pressed: bool) -> void: + _set_visible(button_pressed) + + +func _set_visible(pressed: bool) -> void: + if pressed: + $TextureRect.rect_rotation = 0 + else: + $TextureRect.rect_rotation = -90 + content.visible = pressed + + +# Checks if a child becomes visible from another source and ensures +# it remains invisible if the button is not pressed +func _child_visibility_changed() -> void: + if not pressed: + content.visible = false diff --git a/src/UI/PerspectiveEditor/VanishingPoint.gd b/src/UI/PerspectiveEditor/VanishingPoint.gd new file mode 100644 index 000000000..b5b58fa06 --- /dev/null +++ b/src/UI/PerspectiveEditor/VanishingPoint.gd @@ -0,0 +1,228 @@ +extends VBoxContainer + +var perspective_lines = [] +var tracker_line: PerspectiveLine +var data = { + "position_x": 0, + "position_y": 0, + "angles": [], + "lengths": [], + "color": Color(randf(), randf(), randf(), 1).to_html(), +} + +onready var color_picker_button = $"%ColorPickerButton" +onready var title = $"%PointCollapseContainer" +onready var pos_x = $"%X" +onready var pos_y = $"%Y" +onready var line_buttons_container = $"%LinesContainer" +onready var boundary_l = $Content/BoundaryL +onready var boundary_r = $Content/BoundaryR +onready var boundary_b = $Content/VBoxContainer/BoundaryB + + +func initiate(start_data = null, idx = -1) -> void: + if start_data: + data = start_data.duplicate() + add_line(null, true) + for i in data.angles.size(): + var loaded_line_data = { + "start": Vector2(data.position_x, data.position_y), + "angle": data.angles[i], + "length": data.lengths[i], + "color": Color(data.color) + } + add_line(loaded_line_data) + if idx != -1: + title.point_text = str("Point: ", idx + 1) + else: + title.point_text = str("Point: ", get_parent().get_child_count()) + pos_x.value = data.position_x + pos_y.value = data.position_y + color_picker_button.color = Color(data.color) + update_boundary_color(color_picker_button.color) + color_picker_button.connect("color_changed", self, "_on_color_changed") + pos_x.connect("value_changed", self, "_on_X_value_changed") + pos_y.connect("value_changed", self, "_on_Y_value_changed") + if !start_data: + update_data_to_project() + + +func update_boundary_color(color: Color): + var luminance = (0.2126 * color.r) + (0.7152 * color.g) + (0.0722 * color.b) + color.a = 0.9 - luminance * 0.4 # Interpolates between 0.5 to 0.9 + boundary_l.color = color + boundary_r.color = color + boundary_b.color = color + + +# Signals +func _on_AddLine_pressed() -> void: + add_line() + update_data_to_project() + + +func _on_Delete_pressed() -> void: + Global.perspective_editor.delete_point(get_index()) + + +func _on_color_changed(_color: Color): + update_boundary_color(_color) + data.color = _color.to_html() + refresh(-1) + update_data_to_project() + + +func _on_X_value_changed(value: float) -> void: + data.position_x = value + refresh(-1) + update_data_to_project() + + +func _on_Y_value_changed(value): + data.position_y = value + refresh(-1) + update_data_to_project() + + +func _angle_changed(value: float, line_button): + var line_index = line_button.get_index() + data.angles[line_index] = value + refresh(line_index) + update_data_to_project() + + +func _length_changed(value: float, line_button): + var line_index = line_button.get_index() + data.lengths[line_index] = value + refresh(line_index) + update_data_to_project() + + +func _remove_line_pressed(line_button): + var index = line_button.get_index() + remove_line(index) + line_button.queue_free() + update_data_to_project() + + +# Methods +func add_line(loaded_line_data = null, is_tracker := false): + var p_size = Global.current_project.size + var default_line_data = { + "start": Vector2(data.position_x, data.position_y), + "angle": 0, + "length": 19999, + "color": Color(data.color) + } + if default_line_data.start.x > p_size.x / 2: + # If new line is created ahed of half project distance then + # reverse it's angle (for beautification) + default_line_data.angle = 180 + + # Check if we have loading data instead of creating line + # Then THAT should be our data + if loaded_line_data: + default_line_data = loaded_line_data + + if is_tracker: # if we are creating tracker line then length adjustment is not required + if tracker_line != null: # Also if the tracker line already exists then cancel creation + return + else: # If we are not creating a perspective line then adjust it's length + var suitable_length = sqrt(pow(p_size.x, 2) + pow(p_size.y, 2)) + default_line_data.length = suitable_length + + # Create the visual line + var line = preload("res://src/UI/PerspectiveEditor/PerspectiveLine.tscn").instance() + line.initiate(default_line_data) + + # Set it's mode accordingly + if is_tracker: # Settings for Tracker mode + line.track_mouse = true + tracker_line = line + tracker_line.hide_perspective_line() + else: # Settings for Normal mode + var line_button = preload("res://src/UI/PerspectiveEditor/LineButton.tscn").instance() + line_buttons_container.add_child(line_button) + var index = line_button.get_parent().get_child_count() - 2 + line_button.get_parent().move_child(line_button, index) + + var line_name = str( + "Line", line_button.get_index() + 1, " (", int(default_line_data.angle), "°)" + ) + line_button.text = line_name + + var remove_button = line_button.find_node("Delete") + var angle_slider = line_button.find_node("AngleSlider") + var length_slider = line_button.find_node("LengthSlider") + + angle_slider.value = default_line_data.angle + length_slider.value = default_line_data.length + if !loaded_line_data: + data.angles.append(angle_slider.value) + data.lengths.append(length_slider.value) + + angle_slider.connect("value_changed", self, "_angle_changed", [line_button]) + length_slider.connect("value_changed", self, "_length_changed", [line_button]) + remove_button.connect("pressed", self, "_remove_line_pressed", [line_button]) + perspective_lines.append(line) + + +func remove_line(line_index): + var line_to_remove = perspective_lines[line_index] + perspective_lines.remove(line_index) + data.angles.remove(line_index) + data.lengths.remove(line_index) + line_to_remove.queue_free() + + +func update_data_to_project(removal := false): + var project = Global.current_project + var idx = get_index() + if removal: + project.vanishing_points.remove(idx) + return + if idx < project.vanishing_points.size(): + project.vanishing_points[idx] = data + else: + project.vanishing_points.append(data) + Global.current_project.has_changed = true + + +func refresh(index: int): + if index == -1: # means all lines should be refreshed + refresh_tracker() + for i in data.angles.size(): + refresh_line(i) + else: + refresh_line(index) + + +func refresh_line(index: int): + var line_data = { + "start": Vector2(data.position_x, data.position_y), + "angle": data.angles[index], + "length": data.lengths[index], + "color": Color(data.color) + } + var line_button = line_buttons_container.get_child(index) + var line_name = str("Line", line_button.get_index() + 1, " (", int(line_data.angle), "°)") + line_button.text = line_name + perspective_lines[index].refresh(line_data) + + +func refresh_tracker(): + var line_data = { + "start": Vector2(data.position_x, data.position_y), + "angle": tracker_line._data.angle, + "length": tracker_line._data.length, + "color": Color(data.color) + } + tracker_line.refresh(line_data) + + +func _exit_tree() -> void: + if tracker_line: + tracker_line.queue_free() + tracker_line = null + for idx in perspective_lines.size(): + perspective_lines[idx].queue_free() diff --git a/src/UI/PerspectiveEditor/VanishingPoint.tscn b/src/UI/PerspectiveEditor/VanishingPoint.tscn new file mode 100644 index 000000000..4bede9cbb --- /dev/null +++ b/src/UI/PerspectiveEditor/VanishingPoint.tscn @@ -0,0 +1,171 @@ +[gd_scene load_steps=6 format=2] + +[ext_resource path="res://src/UI/Nodes/ValueSlider.tscn" type="PackedScene" id=1] +[ext_resource path="res://src/UI/PerspectiveEditor/VanishingPoint.gd" type="Script" id=2] +[ext_resource path="res://src/UI/Nodes/CollapsibleContainer.tscn" type="PackedScene" id=3] +[ext_resource path="res://assets/graphics/misc/value_arrow.svg" type="Texture" id=4] +[ext_resource path="res://src/UI/PerspectiveEditor/PointCollapseContainer.gd" type="Script" id=5] + +[node name="VanishingPoint" type="VBoxContainer" groups=["Entry"]] +margin_right = 261.0 +margin_bottom = 145.0 +script = ExtResource( 2 ) + +[node name="TitleContainer" type="HBoxContainer" parent="."] +margin_right = 261.0 +margin_bottom = 20.0 + +[node name="PointCollapseContainer" type="Button" parent="TitleContainer"] +unique_name_in_owner = true +margin_right = 207.0 +margin_bottom = 20.0 +mouse_default_cursor_shape = 2 +size_flags_horizontal = 3 +theme_type_variation = "CollapsibleCheckBox" +toggle_mode = true +script = ExtResource( 5 ) + +[node name="TextureRect" type="TextureRect" parent="TitleContainer/PointCollapseContainer" groups=["UIButtons"]] +anchor_top = 0.5 +anchor_bottom = 0.5 +margin_left = 2.0 +margin_top = -6.0 +margin_right = 14.0 +margin_bottom = 6.0 +rect_rotation = -90.0 +rect_pivot_offset = Vector2( 6, 6 ) +texture = ExtResource( 4 ) + +[node name="Label" type="Label" parent="TitleContainer/PointCollapseContainer"] +anchor_right = 1.0 +anchor_bottom = 1.0 +margin_left = 14.0 +theme_type_variation = "Header" +valign = 1 + +[node name="ColorPickerButton" type="ColorPickerButton" parent="TitleContainer"] +unique_name_in_owner = true +margin_left = 211.0 +margin_right = 261.0 +margin_bottom = 20.0 +rect_min_size = Vector2( 50, 0 ) + +[node name="Content" type="HBoxContainer" parent="."] +unique_name_in_owner = true +margin_top = 24.0 +margin_right = 261.0 +margin_bottom = 130.0 + +[node name="Spacer" type="Control" parent="Content"] +margin_right = 5.0 +margin_bottom = 106.0 +rect_min_size = Vector2( 5, 0 ) + +[node name="BoundaryL" type="ColorRect" parent="Content"] +margin_left = 9.0 +margin_right = 11.0 +margin_bottom = 106.0 +rect_min_size = Vector2( 2, 0 ) + +[node name="VBoxContainer" type="VBoxContainer" parent="Content"] +margin_left = 15.0 +margin_right = 255.0 +margin_bottom = 106.0 +size_flags_horizontal = 3 + +[node name="PointInfo" type="HBoxContainer" parent="Content/VBoxContainer"] +margin_right = 240.0 +margin_bottom = 52.0 + +[node name="Label" type="Label" parent="Content/VBoxContainer/PointInfo"] +margin_right = 56.0 +margin_bottom = 52.0 +size_flags_vertical = 5 +text = "Position:" + +[node name="Position" type="VBoxContainer" parent="Content/VBoxContainer/PointInfo"] +margin_left = 60.0 +margin_right = 240.0 +margin_bottom = 52.0 +size_flags_horizontal = 3 + +[node name="X" parent="Content/VBoxContainer/PointInfo/Position" instance=ExtResource( 1 )] +unique_name_in_owner = true +margin_right = 180.0 +step = 0.5 +allow_greater = true +allow_lesser = true +prefix = "X :" + +[node name="Y" parent="Content/VBoxContainer/PointInfo/Position" instance=ExtResource( 1 )] +unique_name_in_owner = true +margin_top = 28.0 +margin_right = 180.0 +margin_bottom = 52.0 +step = 0.5 +allow_greater = true +allow_lesser = true +prefix = "Y :" + +[node name="CollapsibleContainer" parent="Content/VBoxContainer" instance=ExtResource( 3 )] +margin_top = 56.0 +margin_right = 240.0 +margin_bottom = 76.0 +text = "Lines" + +[node name="HBoxContainer" type="HBoxContainer" parent="Content/VBoxContainer/CollapsibleContainer"] +visible = false +margin_top = 24.0 +margin_right = 261.0 +margin_bottom = 44.0 + +[node name="Spacer" type="Control" parent="Content/VBoxContainer/CollapsibleContainer/HBoxContainer"] +margin_right = 20.0 +margin_bottom = 20.0 +rect_min_size = Vector2( 20, 0 ) + +[node name="VSeparator" type="VSeparator" parent="Content/VBoxContainer/CollapsibleContainer/HBoxContainer"] +margin_right = 4.0 +margin_bottom = 40.0 + +[node name="LinesContainer" type="VBoxContainer" parent="Content/VBoxContainer/CollapsibleContainer/HBoxContainer"] +unique_name_in_owner = true +margin_left = 24.0 +margin_right = 261.0 +margin_bottom = 20.0 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="AddLine" type="Button" parent="Content/VBoxContainer/CollapsibleContainer/HBoxContainer/LinesContainer"] +margin_right = 237.0 +margin_bottom = 20.0 +text = "Add Line" + +[node name="Delete" type="Button" parent="Content/VBoxContainer"] +margin_top = 80.0 +margin_right = 240.0 +margin_bottom = 100.0 +rect_min_size = Vector2( 40, 0 ) +text = "Delete Point" + +[node name="BoundaryB" type="ColorRect" parent="Content/VBoxContainer"] +margin_top = 104.0 +margin_right = 240.0 +margin_bottom = 106.0 +rect_min_size = Vector2( 0, 2 ) + +[node name="BoundaryR" type="ColorRect" parent="Content"] +margin_left = 259.0 +margin_right = 261.0 +margin_bottom = 106.0 +rect_min_size = Vector2( 2, 0 ) + +[node name="HSeparator" type="HSeparator" parent="."] +margin_top = 134.0 +margin_right = 261.0 +margin_bottom = 144.0 +custom_constants/separation = 10 + +[connection signal="toggled" from="TitleContainer/PointCollapseContainer" to="TitleContainer/PointCollapseContainer" method="_on_Button_toggled"] +[connection signal="pressed" from="Content/VBoxContainer/CollapsibleContainer/HBoxContainer/LinesContainer/AddLine" to="." method="_on_AddLine_pressed"] +[connection signal="pressed" from="Content/VBoxContainer/Delete" to="." method="_on_Delete_pressed"] diff --git a/src/UI/TopMenuContainer/TopMenuContainer.gd b/src/UI/TopMenuContainer/TopMenuContainer.gd index 7007e5408..e90aa7e22 100644 --- a/src/UI/TopMenuContainer/TopMenuContainer.gd +++ b/src/UI/TopMenuContainer/TopMenuContainer.gd @@ -123,6 +123,7 @@ func _setup_view_menu() -> void: "Show Pixel Grid", "Show Rulers", "Show Guides", + "Show Mouse Guides", "Snap To", ] view_menu = view_menu_button.get_popup() @@ -160,8 +161,13 @@ func _setup_view_menu() -> void: var show_guides: bool = Global.config_cache.get_value( "view_menu", "show_guides", Global.show_guides ) + var show_mouse_guides: bool = Global.config_cache.get_value( + "view_menu", "show_mouse_guides", Global.show_mouse_guides + ) if show_guides != Global.show_guides: _toggle_show_guides() + if show_mouse_guides != Global.show_mouse_guides: + _toggle_show_mouse_guides() func _setup_tile_mode_submenu(item: String) -> void: @@ -460,6 +466,8 @@ func view_menu_id_pressed(id: int) -> void: _toggle_show_rulers() Global.ViewMenu.SHOW_GUIDES: _toggle_show_guides() + Global.ViewMenu.SHOW_MOUSE_GUIDES: + _toggle_show_mouse_guides() _: _handle_metadata(id, view_menu_button) @@ -574,8 +582,6 @@ func _toggle_show_grid() -> void: func _toggle_show_pixel_grid() -> void: Global.draw_pixel_grid = !Global.draw_pixel_grid view_menu.set_item_checked(Global.ViewMenu.SHOW_PIXEL_GRID, Global.draw_pixel_grid) - if Global.canvas.pixel_grid: - Global.canvas.pixel_grid.update() func _toggle_show_rulers() -> void: @@ -598,6 +604,15 @@ func _toggle_show_guides() -> void: guide.visible = Global.show_y_symmetry_axis and Global.show_guides +func _toggle_show_mouse_guides() -> void: + Global.show_mouse_guides = !Global.show_mouse_guides + view_menu.set_item_checked(Global.ViewMenu.SHOW_MOUSE_GUIDES, Global.show_mouse_guides) + if Global.show_mouse_guides: + if Global.canvas.mouse_guide_container: + Global.canvas.mouse_guide_container.get_child(0).update() + Global.canvas.mouse_guide_container.get_child(1).update() + + func _toggle_zen_mode() -> void: for i in ui_elements.size(): if ui_elements[i].name == "Main Canvas": diff --git a/src/UI/UI.tscn b/src/UI/UI.tscn index db710e6f7..416026929 100644 --- a/src/UI/UI.tscn +++ b/src/UI/UI.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=48 format=2] +[gd_scene load_steps=49 format=2] [ext_resource path="res://src/UI/Tools/Tools.tscn" type="PackedScene" id=1] [ext_resource path="res://src/UI/Canvas/CanvasPreview.tscn" type="PackedScene" id=2] @@ -11,6 +11,7 @@ [ext_resource path="res://src/Shaders/TransparentChecker.shader" type="Shader" id=9] [ext_resource path="res://src/UI/GlobalToolOptions/GlobalToolOptions.tscn" type="PackedScene" id=10] [ext_resource path="res://src/UI/ReferenceImages/ReferencesPanel.tscn" type="PackedScene" id=11] +[ext_resource path="res://src/UI/PerspectiveEditor/PerspectiveEditor.tscn" type="PackedScene" id=12] [ext_resource path="res://addons/dockable_container/layout.gd" type="Script" id=14] [ext_resource path="res://src/UI/CanvasPreviewContainer/CanvasPreviewContainer.tscn" type="PackedScene" id=16] [ext_resource path="res://src/UI/ColorPickers/ColorPickers.tscn" type="PackedScene" id=17] @@ -34,7 +35,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", "Perspective Editor" ) current_tab = 0 [sub_resource type="Resource" id=8] @@ -404,6 +405,11 @@ margin_left = 1079.0 margin_right = 1080.0 margin_bottom = 1004.0 +[node name="Perspective Editor" parent="DockableContainer" instance=ExtResource( 12 )] +visible = false +margin_right = -1010.0 +margin_bottom = -710.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"]