diff --git a/src/Autoload/Global.gd b/src/Autoload/Global.gd index 3cd516c9f..260f9c292 100644 --- a/src/Autoload/Global.gd +++ b/src/Autoload/Global.gd @@ -137,6 +137,9 @@ var draw_grid := false var draw_pixel_grid := false var show_rulers := true var show_guides := true +var snapping_distance := 10.0 +var snap_to_rectangular_grid := false +var snap_to_guides := false # Onion skinning options var onion_skinning := false diff --git a/src/Tools/BaseTool.gd b/src/Tools/BaseTool.gd index 43bc38676..3f98ff7c4 100644 --- a/src/Tools/BaseTool.gd +++ b/src/Tools/BaseTool.gd @@ -80,6 +80,83 @@ func draw_preview() -> void: pass +func snap_position(position: Vector2) -> Vector2: + var snap_distance := Global.snapping_distance * Vector2.ONE + if Global.snap_to_rectangular_grid: + var grid_size := Vector2(Global.grid_width, Global.grid_height) + var grid_offset := Vector2(Global.grid_offset_x, Global.grid_offset_y) + var grid_pos := position.snapped(grid_size) + grid_pos += grid_offset + var closest_point_grid := _get_closest_point_to_grid(position, snap_distance, grid_pos) + if closest_point_grid != Vector2.INF: + position = closest_point_grid.floor() + + if Global.snap_to_guides: + var snap_to := Vector2.INF + for guide in Global.current_project.guides: + if guide is SymmetryGuide: + continue + var closest_point := _get_closest_point_to_segment( + position, snap_distance, guide.points[0], guide.points[1] + ) + if closest_point == Vector2.INF: # Is not close to a guide + continue + # Snap to the closest guide + if ( + snap_to == Vector2.INF + or (snap_to - position).length() > (closest_point - position).length() + ): + snap_to = closest_point + if snap_to != Vector2.INF: + position = snap_to.floor() + return position + + +func _get_closest_point_to_grid( + position: Vector2, snap_distance: Vector2, grid_pos: Vector2 +) -> Vector2: + # If the cursor is close to the start/origin of a grid cell, snap to that + var closest_point := Vector2.INF + var rect := Rect2() + rect.position = position - (snap_distance / 4.0) + rect.end = position + (snap_distance / 4.0) + if rect.has_point(grid_pos): + closest_point = grid_pos + return closest_point + # If the cursor is far from the grid cell origin but still close to a grid line + # Look for a point close to a horizontal grid line + var grid_start_hor := Vector2(0, grid_pos.y) + var grid_end_hor := Vector2(Global.current_project.size.x, grid_pos.y) + var closest_point_hor := _get_closest_point_to_segment( + position, snap_distance, grid_start_hor, grid_end_hor + ) + # Look for a point close to a vertical grid line + var grid_start_ver := Vector2(grid_pos.x, 0) + var grid_end_ver := Vector2(grid_pos.x, Global.current_project.size.y) + var closest_point_ver := _get_closest_point_to_segment( + position, snap_distance, grid_start_ver, grid_end_ver + ) + # Snap to the closest point to the closest grid line + var horizontal_distance := (closest_point_hor - position).length() + var vertical_distance := (closest_point_ver - position).length() + if horizontal_distance < vertical_distance: + closest_point = closest_point_hor + elif horizontal_distance > vertical_distance: + closest_point = closest_point_ver + elif horizontal_distance == vertical_distance and closest_point_hor != Vector2.INF: + closest_point = grid_pos + return closest_point + + +func _get_closest_point_to_segment( + position: Vector2, distance: Vector2, s1: Vector2, s2: Vector2 +) -> Vector2: + var closest_point := Vector2.INF + if Geometry.segment_intersects_segment_2d(position - distance, position + distance, s1, s2): + closest_point = Geometry.get_closest_point_to_segment_2d(position, s1, s2) + return closest_point + + func _get_draw_rect() -> Rect2: if Global.current_project.has_selection: return Global.current_project.selection_map.get_used_rect() diff --git a/src/Tools/Draw.gd b/src/Tools/Draw.gd index baf389064..cb7e0ce2a 100644 --- a/src/Tools/Draw.gd +++ b/src/Tools/Draw.gd @@ -370,13 +370,13 @@ func remove_unselected_parts_of_brush(brush: Image, dst: Vector2) -> Image: func draw_indicator(left: bool) -> void: var color := Global.left_tool_color if left else Global.right_tool_color - draw_indicator_at(_cursor, Vector2.ZERO, color) + draw_indicator_at(snap_position(_cursor), Vector2.ZERO, color) if Global.current_project.tiles.mode and Global.current_project.tiles.has_point(_cursor): var position := _line_start if _draw_line else _cursor var nearest_tile := Global.current_project.tiles.get_nearest_tile(position) if nearest_tile.position != Vector2.ZERO: var offset := nearest_tile.position - draw_indicator_at(_cursor, offset, Color.green) + draw_indicator_at(snap_position(_cursor), offset, Color.green) func draw_indicator_at(position: Vector2, offset: Vector2, color: Color) -> void: diff --git a/src/Tools/Eraser.gd b/src/Tools/Eraser.gd index 51e744dc3..1f92b6c43 100644 --- a/src/Tools/Eraser.gd +++ b/src/Tools/Eraser.gd @@ -35,6 +35,7 @@ func set_config(config: Dictionary) -> void: func draw_start(position: Vector2) -> void: + position = snap_position(position) .draw_start(position) if Input.is_action_pressed("draw_color_picker"): _picking_color = true @@ -63,6 +64,7 @@ func draw_start(position: Vector2) -> void: func draw_move(position: Vector2) -> void: + position = snap_position(position) .draw_move(position) if _picking_color: # Still return even if we released Alt if Input.is_action_pressed("draw_color_picker"): @@ -70,7 +72,7 @@ func draw_move(position: Vector2) -> void: return if _draw_line: - var d = _line_angle_constraint(_line_start, position) + var d := _line_angle_constraint(_line_start, position) _line_end = d.position cursor_text = d.text update_line_polylines(_line_start, _line_end) @@ -82,6 +84,7 @@ func draw_move(position: Vector2) -> void: func draw_end(position: Vector2) -> void: + position = snap_position(position) .draw_end(position) if _picking_color: return diff --git a/src/Tools/LineTool.gd b/src/Tools/LineTool.gd index eeded6dfb..81b335acf 100644 --- a/src/Tools/LineTool.gd +++ b/src/Tools/LineTool.gd @@ -67,6 +67,7 @@ func _input(event: InputEvent) -> void: func draw_start(position: Vector2) -> void: + position = snap_position(position) .draw_start(position) if Input.is_action_pressed("shape_displace"): _picking_color = true @@ -85,6 +86,7 @@ func draw_start(position: Vector2) -> void: func draw_move(position: Vector2) -> void: + position = snap_position(position) .draw_move(position) if _picking_color: # Still return even if we released Alt if Input.is_action_pressed("shape_displace"): @@ -94,7 +96,7 @@ func draw_move(position: Vector2) -> void: if _drawing: if _displace_origin: _original_pos += position - _offset - var d = _line_angle_constraint(_original_pos, position) + var d := _line_angle_constraint(_original_pos, position) _dest = d.position if Input.is_action_pressed("shape_center"): @@ -106,6 +108,7 @@ func draw_move(position: Vector2) -> void: func draw_end(position: Vector2) -> void: + position = snap_position(position) .draw_end(position) if _picking_color: return diff --git a/src/Tools/Pencil.gd b/src/Tools/Pencil.gd index b8e80c474..ef070bf6b 100644 --- a/src/Tools/Pencil.gd +++ b/src/Tools/Pencil.gd @@ -70,6 +70,7 @@ func update_config() -> void: func draw_start(position: Vector2) -> void: + position = snap_position(position) .draw_start(position) if Input.is_action_pressed("draw_color_picker"): _picking_color = true @@ -105,6 +106,7 @@ func draw_start(position: Vector2) -> void: func draw_move(position: Vector2) -> void: + position = snap_position(position) .draw_move(position) if _picking_color: # Still return even if we released Alt if Input.is_action_pressed("draw_color_picker"): @@ -112,7 +114,7 @@ func draw_move(position: Vector2) -> void: return if _draw_line: - var d = _line_angle_constraint(_line_start, position) + var d := _line_angle_constraint(_line_start, position) _line_end = d.position cursor_text = d.text update_line_polylines(_line_start, _line_end) @@ -126,6 +128,7 @@ func draw_move(position: Vector2) -> void: func draw_end(position: Vector2) -> void: + position = snap_position(position) .draw_end(position) if _picking_color: return diff --git a/src/Tools/SelectionTools/EllipseSelect.gd b/src/Tools/SelectionTools/EllipseSelect.gd index e4a3c7434..9c6869370 100644 --- a/src/Tools/SelectionTools/EllipseSelect.gd +++ b/src/Tools/SelectionTools/EllipseSelect.gd @@ -26,6 +26,7 @@ func _input(event: InputEvent) -> void: func draw_move(position: Vector2) -> void: if selection_node.arrow_key_move: return + position = snap_position(position) .draw_move(position) if !_move: if _displace_origin: @@ -38,6 +39,7 @@ func draw_move(position: Vector2) -> void: func draw_end(position: Vector2) -> void: if selection_node.arrow_key_move: return + position = snap_position(position) .draw_end(position) _rect = Rect2(0, 0, 0, 0) _square = false diff --git a/src/Tools/SelectionTools/Lasso.gd b/src/Tools/SelectionTools/Lasso.gd index 7c7c5cef6..e4146dc02 100644 --- a/src/Tools/SelectionTools/Lasso.gd +++ b/src/Tools/SelectionTools/Lasso.gd @@ -5,6 +5,7 @@ var _draw_points := [] func draw_start(position: Vector2) -> void: + position = snap_position(position) .draw_start(position) if !_move: _draw_points.append(position) @@ -14,6 +15,7 @@ func draw_start(position: Vector2) -> void: func draw_move(position: Vector2) -> void: if selection_node.arrow_key_move: return + position = snap_position(position) .draw_move(position) if !_move: append_gap(_last_position, position) @@ -25,6 +27,7 @@ func draw_move(position: Vector2) -> void: func draw_end(position: Vector2) -> void: if selection_node.arrow_key_move: return + position = snap_position(position) if !_move: _draw_points.append(position) .draw_end(position) diff --git a/src/Tools/SelectionTools/PaintSelect.gd b/src/Tools/SelectionTools/PaintSelect.gd index 7f5c7f9c2..52bb70a65 100644 --- a/src/Tools/SelectionTools/PaintSelect.gd +++ b/src/Tools/SelectionTools/PaintSelect.gd @@ -33,6 +33,7 @@ func update_config() -> void: func draw_start(position: Vector2) -> void: + position = snap_position(position) .draw_start(position) if !_move: _draw_points.append_array(draw_tool(position)) @@ -42,6 +43,7 @@ func draw_start(position: Vector2) -> void: func draw_move(position: Vector2) -> void: if selection_node.arrow_key_move: return + position = snap_position(position) .draw_move(position) if !_move: append_gap(_last_position, position) @@ -53,6 +55,7 @@ func draw_move(position: Vector2) -> void: func draw_end(position: Vector2) -> void: if selection_node.arrow_key_move: return + position = snap_position(position) if !_move: _draw_points.append_array(draw_tool(position)) .draw_end(position) diff --git a/src/Tools/SelectionTools/PolygonSelect.gd b/src/Tools/SelectionTools/PolygonSelect.gd index 82cdeaf3f..8cc5103e3 100644 --- a/src/Tools/SelectionTools/PolygonSelect.gd +++ b/src/Tools/SelectionTools/PolygonSelect.gd @@ -27,6 +27,7 @@ func _input(event: InputEvent) -> void: func draw_start(position: Vector2) -> void: if !$DoubleClickTimer.is_stopped(): return + position = snap_position(position) .draw_start(position) if !_move and !_draw_points: _ongoing_selection = true @@ -37,12 +38,14 @@ func draw_start(position: Vector2) -> void: func draw_move(position: Vector2) -> void: if selection_node.arrow_key_move: return + position = snap_position(position) .draw_move(position) func draw_end(position: Vector2) -> void: if selection_node.arrow_key_move: return + position = snap_position(position) if !_move and _draw_points: append_gap(_draw_points[-1], position, _draw_points) if position == _draw_points[0] and _draw_points.size() > 1: diff --git a/src/Tools/SelectionTools/RectSelect.gd b/src/Tools/SelectionTools/RectSelect.gd index a607f5003..f6206dc45 100644 --- a/src/Tools/SelectionTools/RectSelect.gd +++ b/src/Tools/SelectionTools/RectSelect.gd @@ -26,6 +26,7 @@ func _input(event: InputEvent) -> void: func draw_move(position: Vector2) -> void: if selection_node.arrow_key_move: return + position = snap_position(position) .draw_move(position) if !_move: if _displace_origin: @@ -38,6 +39,7 @@ func draw_move(position: Vector2) -> void: func draw_end(position: Vector2) -> void: if selection_node.arrow_key_move: return + position = snap_position(position) .draw_end(position) _rect = Rect2(0, 0, 0, 0) _square = false diff --git a/src/Tools/SelectionTools/SelectionTool.gd b/src/Tools/SelectionTools/SelectionTool.gd index 28050694d..b6962c610 100644 --- a/src/Tools/SelectionTools/SelectionTool.gd +++ b/src/Tools/SelectionTools/SelectionTool.gd @@ -73,6 +73,7 @@ func set_spinbox_values() -> void: func draw_start(position: Vector2) -> void: + position = snap_position(position) .draw_start(position) if selection_node.arrow_key_move: return @@ -147,6 +148,7 @@ func draw_start(position: Vector2) -> void: func draw_move(position: Vector2) -> void: + position = snap_position(position) .draw_move(position) if selection_node.arrow_key_move: return @@ -173,7 +175,7 @@ func draw_move(position: Vector2) -> void: - prev_pos ) position = position.snapped(grid_size) - var grid_offset = Vector2(Global.grid_offset_x, Global.grid_offset_y) + var grid_offset := Vector2(Global.grid_offset_x, Global.grid_offset_y) grid_offset = Vector2(fmod(grid_offset.x, grid_size.x), fmod(grid_offset.y, grid_size.y)) position += grid_offset @@ -187,6 +189,7 @@ func draw_move(position: Vector2) -> void: func draw_end(position: Vector2) -> void: + position = snap_position(position) .draw_end(position) if selection_node.arrow_key_move: return diff --git a/src/Tools/Shading.gd b/src/Tools/Shading.gd index 53edc153b..093113318 100644 --- a/src/Tools/Shading.gd +++ b/src/Tools/Shading.gd @@ -206,6 +206,7 @@ func update_strength() -> void: func draw_start(position: Vector2) -> void: + position = snap_position(position) .draw_start(position) if Input.is_action_pressed("draw_color_picker"): _picking_color = true @@ -234,6 +235,7 @@ func draw_start(position: Vector2) -> void: func draw_move(position: Vector2) -> void: + position = snap_position(position) .draw_move(position) if _picking_color: # Still return even if we released Alt if Input.is_action_pressed("draw_color_picker"): @@ -241,7 +243,7 @@ func draw_move(position: Vector2) -> void: return if _draw_line: - var d = _line_angle_constraint(_line_start, position) + var d := _line_angle_constraint(_line_start, position) _line_end = d.position cursor_text = d.text update_line_polylines(_line_start, _line_end) @@ -253,6 +255,7 @@ func draw_move(position: Vector2) -> void: func draw_end(position: Vector2) -> void: + position = snap_position(position) .draw_end(position) if _picking_color: return diff --git a/src/Tools/ShapeDrawer.gd b/src/Tools/ShapeDrawer.gd index 9533b2632..1a12f4552 100644 --- a/src/Tools/ShapeDrawer.gd +++ b/src/Tools/ShapeDrawer.gd @@ -82,6 +82,7 @@ func _input(event: InputEvent) -> void: func draw_start(position: Vector2) -> void: + position = snap_position(position) .draw_start(position) if Input.is_action_pressed("draw_color_picker"): _picking_color = true @@ -99,6 +100,7 @@ func draw_start(position: Vector2) -> void: func draw_move(position: Vector2) -> void: + position = snap_position(position) .draw_move(position) if _picking_color: # Still return even if we released draw_color_picker (Alt) if Input.is_action_pressed("draw_color_picker"): @@ -114,6 +116,7 @@ func draw_move(position: Vector2) -> void: func draw_end(position: Vector2) -> void: + position = snap_position(position) .draw_end(position) if _picking_color: return diff --git a/src/UI/TopMenuContainer/TopMenuContainer.gd b/src/UI/TopMenuContainer/TopMenuContainer.gd index 74c44877c..7007e5408 100644 --- a/src/UI/TopMenuContainer/TopMenuContainer.gd +++ b/src/UI/TopMenuContainer/TopMenuContainer.gd @@ -26,6 +26,7 @@ onready var greyscale_vision: ColorRect = ui.find_node("GreyscaleVision") onready var new_image_dialog: ConfirmationDialog = Global.control.find_node("CreateNewImage") onready var window_opacity_dialog: AcceptDialog = Global.control.find_node("WindowOpacityDialog") onready var tile_mode_submenu := PopupMenu.new() +onready var snap_to_submenu := PopupMenu.new() onready var panels_submenu := PopupMenu.new() onready var layouts_submenu := PopupMenu.new() onready var recent_projects_submenu := PopupMenu.new() @@ -122,17 +123,19 @@ func _setup_view_menu() -> void: "Show Pixel Grid", "Show Rulers", "Show Guides", + "Snap To", ] view_menu = view_menu_button.get_popup() - var i := 0 - for item in view_menu_items: + for i in view_menu_items.size(): + var item: String = view_menu_items[i] if item == "Tile Mode": _setup_tile_mode_submenu(item) + elif item == "Snap To": + _setup_snap_to_submenu(item) elif item == "Tile Mode Offsets": view_menu.add_item(item, i) else: view_menu.add_check_item(item, i) - i += 1 view_menu.set_item_checked(Global.ViewMenu.SHOW_RULERS, true) view_menu.set_item_checked(Global.ViewMenu.SHOW_GUIDES, true) view_menu.hide_on_checkable_item_selection = false @@ -175,6 +178,15 @@ func _setup_tile_mode_submenu(item: String) -> void: view_menu.add_submenu_item(item, tile_mode_submenu.get_name()) +func _setup_snap_to_submenu(item: String) -> void: + snap_to_submenu.set_name("snap_to_submenu") + snap_to_submenu.add_check_item("Snap to Rectangular Grid") + snap_to_submenu.add_check_item("Snap to Guides") + snap_to_submenu.connect("id_pressed", self, "_snap_to_submenu_id_pressed") + view_menu.add_child(snap_to_submenu) + view_menu.add_submenu_item(item, snap_to_submenu.get_name()) + + func _setup_window_menu() -> void: # Order as in Global.WindowMenu enum var window_menu_items := [ @@ -464,6 +476,15 @@ func _tile_mode_submenu_id_pressed(id: int) -> void: Global.canvas.grid.update() +func _snap_to_submenu_id_pressed(id: int) -> void: + if id == 0: + Global.snap_to_rectangular_grid = !Global.snap_to_rectangular_grid + snap_to_submenu.set_item_checked(id, Global.snap_to_rectangular_grid) + elif id == 1: + Global.snap_to_guides = !Global.snap_to_guides + snap_to_submenu.set_item_checked(id, Global.snap_to_guides) + + func window_menu_id_pressed(id: int) -> void: if not Global.can_draw: return