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:
parent
d9e3c980ae
commit
42d6f12530
15 changed files with 142 additions and 10 deletions
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue