1
0
Fork 0
mirror of https://github.com/Orama-Interactive/Pixelorama.git synced 2025-03-12 22:35:18 +00:00

Implement rectangular grid and guide snapping

Snap to the rectangular grid and guides (excluding symmetry guides). Can be toggled from the View menu. Currently affects the following tools: Pencil, Eraser, Shading, Line, all shape tool and all selection tools. Although maybe this should not affect the Magic Wand and Select By Color? The snapping distance is currently unaffected by the zoom (should change), and it will be exposed as a parameter in the preferences at a later commit.
This commit is contained in:
Emmanouil Papadeas 2023-01-25 04:37:03 +02:00
parent d9e3c980ae
commit 42d6f12530
15 changed files with 142 additions and 10 deletions

View file

@ -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

View file

@ -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()

View file

@ -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:

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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:

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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