diff --git a/src/Autoload/Tools.gd b/src/Autoload/Tools.gd index 1bde704bc..2b7b13455 100644 --- a/src/Autoload/Tools.gd +++ b/src/Autoload/Tools.gd @@ -583,6 +583,87 @@ func calculate_mirror_x_minus_y(pos: Vector2i, project: Project) -> Vector2i: ) +func is_placing_tiles() -> bool: + if Global.current_project.frames.size() == 0 or Global.current_project.layers.size() == 0: + return false + return Global.current_project.get_current_cel() is CelTileMap and TileSetPanel.placing_tiles + + +func _get_closest_point_to_grid(pos: Vector2, distance: float, grid_pos: Vector2) -> Vector2: + # If the cursor is close to the start/origin of a grid cell, snap to that + var snap_distance := distance * Vector2.ONE + var closest_point := Vector2.INF + var rect := Rect2() + rect.position = pos - (snap_distance / 4.0) + rect.end = pos + (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( + pos, 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( + pos, distance, grid_start_ver, grid_end_ver + ) + # Snap to the closest point to the closest grid line + var horizontal_distance := (closest_point_hor - pos).length() + var vertical_distance := (closest_point_ver - pos).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( + pos: Vector2, distance: float, s1: Vector2, s2: Vector2 +) -> Vector2: + var test_line := (s2 - s1).rotated(deg_to_rad(90)).normalized() + var from_a := pos - test_line * distance + var from_b := pos + test_line * distance + var closest_point := Vector2.INF + if Geometry2D.segment_intersects_segment(from_a, from_b, s1, s2): + closest_point = Geometry2D.get_closest_point_to_segment(pos, s1, s2) + return closest_point + + +func snap_to_rectangular_grid_boundary( + pos: Vector2, grid_size: Vector2i, grid_offset := Vector2i.ZERO, snapping_distance := 9999.0 +) -> Vector2: + var grid_pos := pos.snapped(grid_size) + grid_pos += Vector2(grid_offset) + # keeping grid_pos as is would have been fine but this adds extra accuracy as to + # which snap point (from the list below) is closest to mouse and occupy THAT point + # t_l is for "top left" and so on + var t_l := grid_pos + Vector2(-grid_size.x, -grid_size.y) + var t_c := grid_pos + Vector2(0, -grid_size.y) + var t_r := grid_pos + Vector2(grid_size.x, -grid_size.y) + var m_l := grid_pos + Vector2(-grid_size.x, 0) + var m_c := grid_pos + var m_r := grid_pos + Vector2(grid_size.x, 0) + var b_l := grid_pos + Vector2(-grid_size.x, grid_size.y) + var b_c := grid_pos + Vector2(0, grid_size.y) + var b_r := grid_pos + Vector2(grid_size) + var vec_arr: PackedVector2Array = [t_l, t_c, t_r, m_l, m_c, m_r, b_l, b_c, b_r] + for vec in vec_arr: + if vec.distance_to(pos) < grid_pos.distance_to(pos): + grid_pos = vec + + var grid_point := _get_closest_point_to_grid(pos, snapping_distance, grid_pos) + if grid_point != Vector2.INF: + pos = grid_point.floor() + return pos + + func set_button_size(button_size: int) -> void: var size := Vector2(24, 24) if button_size == Global.ButtonSize.SMALL else Vector2(32, 32) if not is_instance_valid(_tool_buttons): diff --git a/src/Tools/BaseDraw.gd b/src/Tools/BaseDraw.gd index 9b2ddf0f2..e01088a0a 100644 --- a/src/Tools/BaseDraw.gd +++ b/src/Tools/BaseDraw.gd @@ -163,7 +163,7 @@ func update_config() -> void: func update_brush() -> void: $Brush/BrushSize.suffix = "px" # Assume we are using default brushes - if is_placing_tiles(): + if Tools.is_placing_tiles(): var tilemap_cel := Global.current_project.get_current_cel() as CelTileMap var tileset := tilemap_cel.tileset var tile_index := clampi(TileSetPanel.selected_tile_index, 0, tileset.tiles.size() - 1) @@ -517,7 +517,7 @@ func remove_unselected_parts_of_brush(brush: Image, dst: Vector2i) -> Image: func draw_indicator(left: bool) -> void: var color := Global.left_tool_color if left else Global.right_tool_color var snapped_position := snap_position(_cursor) - if is_placing_tiles(): + if Tools.is_placing_tiles(): var tileset := (Global.current_project.get_current_cel() as CelTileMap).tileset var grid_size := tileset.tile_size snapped_position = _snap_to_rectangular_grid_center( @@ -545,7 +545,7 @@ func draw_indicator(left: bool) -> void: func draw_indicator_at(pos: Vector2i, offset: Vector2i, color: Color) -> void: var canvas: Node2D = Global.canvas.indicators - if _brush.type in IMAGE_BRUSHES and not _draw_line or is_placing_tiles(): + if _brush.type in IMAGE_BRUSHES and not _draw_line or Tools.is_placing_tiles(): pos -= _brush_image.get_size() / 2 pos -= offset canvas.draw_texture(_brush_texture, pos) @@ -580,7 +580,7 @@ func _set_pixel_no_cache(pos: Vector2i, ignore_mirroring := false) -> void: pos = _stroke_project.tiles.get_canon_position(pos) if Global.current_project.has_selection: pos = Global.current_project.selection_map.get_canon_position(pos) - if is_placing_tiles(): + if Tools.is_placing_tiles(): draw_tile(pos) return if !_stroke_project.can_pixel_get_drawn(pos): diff --git a/src/Tools/BaseSelectionTool.gd b/src/Tools/BaseSelectionTool.gd index 8ed365050..ce30f0b2f 100644 --- a/src/Tools/BaseSelectionTool.gd +++ b/src/Tools/BaseSelectionTool.gd @@ -152,6 +152,10 @@ func draw_move(pos: Vector2i) -> void: if not _move: return + if Tools.is_placing_tiles(): + var tileset := (Global.current_project.get_current_cel() as CelTileMap).tileset + var grid_size := tileset.tile_size + pos = Tools.snap_to_rectangular_grid_boundary(pos, grid_size) if Input.is_action_pressed("transform_snap_axis"): # Snap to axis var angle := Vector2(pos).angle_to_point(_start_pos) if absf(angle) <= PI / 4 or absf(angle) >= 3 * PI / 4: diff --git a/src/Tools/BaseShapeDrawer.gd b/src/Tools/BaseShapeDrawer.gd index 3050d7be0..5111b626f 100644 --- a/src/Tools/BaseShapeDrawer.gd +++ b/src/Tools/BaseShapeDrawer.gd @@ -189,7 +189,7 @@ func _draw_shape(origin: Vector2i, dest: Vector2i) -> void: _drawer.reset() # Draw each point offsetted based on the shape's thickness var draw_pos := point + thickness_vector - if is_placing_tiles(): + if Tools.is_placing_tiles(): draw_tile(draw_pos) else: if Global.current_project.can_pixel_get_drawn(draw_pos): diff --git a/src/Tools/BaseTool.gd b/src/Tools/BaseTool.gd index 51fa3221d..cdc14345d 100644 --- a/src/Tools/BaseTool.gd +++ b/src/Tools/BaseTool.gd @@ -79,12 +79,6 @@ func draw_end(_pos: Vector2i) -> void: project.can_undo = true -func is_placing_tiles() -> bool: - if Global.current_project.frames.size() == 0 or Global.current_project.layers.size() == 0: - return false - return Global.current_project.get_current_cel() is CelTileMap and TileSetPanel.placing_tiles - - func get_cell_position(pos: Vector2i) -> int: var tile_pos := 0 if Global.current_project.get_current_cel() is not CelTileMap: @@ -145,7 +139,7 @@ func draw_preview() -> void: func snap_position(pos: Vector2) -> Vector2: var snapping_distance := Global.snapping_distance / Global.camera.zoom.x if Global.snap_to_rectangular_grid_boundary: - pos = _snap_to_rectangular_grid_boundary( + pos = Tools.snap_to_rectangular_grid_boundary( pos, Global.grids[0].grid_size, Global.grids[0].grid_offset, snapping_distance ) @@ -218,81 +212,6 @@ func mirror_array(array: Array[Vector2i], callable := func(_array): pass) -> Arr return new_array -func _get_closest_point_to_grid(pos: Vector2, distance: float, grid_pos: Vector2) -> Vector2: - # If the cursor is close to the start/origin of a grid cell, snap to that - var snap_distance := distance * Vector2.ONE - var closest_point := Vector2.INF - var rect := Rect2() - rect.position = pos - (snap_distance / 4.0) - rect.end = pos + (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( - pos, 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( - pos, distance, grid_start_ver, grid_end_ver - ) - # Snap to the closest point to the closest grid line - var horizontal_distance := (closest_point_hor - pos).length() - var vertical_distance := (closest_point_ver - pos).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( - pos: Vector2, distance: float, s1: Vector2, s2: Vector2 -) -> Vector2: - var test_line := (s2 - s1).rotated(deg_to_rad(90)).normalized() - var from_a := pos - test_line * distance - var from_b := pos + test_line * distance - var closest_point := Vector2.INF - if Geometry2D.segment_intersects_segment(from_a, from_b, s1, s2): - closest_point = Geometry2D.get_closest_point_to_segment(pos, s1, s2) - return closest_point - - -func _snap_to_rectangular_grid_boundary( - pos: Vector2, grid_size: Vector2i, grid_offset: Vector2i, snapping_distance: float -) -> Vector2: - var grid_pos := pos.snapped(grid_size) - grid_pos += Vector2(grid_offset) - # keeping grid_pos as is would have been fine but this adds extra accuracy as to - # which snap point (from the list below) is closest to mouse and occupy THAT point - # t_l is for "top left" and so on - var t_l := grid_pos + Vector2(-grid_size.x, -grid_size.y) - var t_c := grid_pos + Vector2(0, -grid_size.y) - var t_r := grid_pos + Vector2(grid_size.x, -grid_size.y) - var m_l := grid_pos + Vector2(-grid_size.x, 0) - var m_c := grid_pos - var m_r := grid_pos + Vector2(grid_size.x, 0) - var b_l := grid_pos + Vector2(-grid_size.x, grid_size.y) - var b_c := grid_pos + Vector2(0, grid_size.y) - var b_r := grid_pos + Vector2(grid_size) - var vec_arr: PackedVector2Array = [t_l, t_c, t_r, m_l, m_c, m_r, b_l, b_c, b_r] - for vec in vec_arr: - if vec.distance_to(pos) < grid_pos.distance_to(pos): - grid_pos = vec - - var grid_point := _get_closest_point_to_grid(pos, snapping_distance, grid_pos) - if grid_point != Vector2.INF: - pos = grid_point.floor() - return pos - - func _snap_to_rectangular_grid_center( pos: Vector2, grid_size: Vector2i, grid_offset: Vector2i, snapping_distance: float ) -> Vector2: @@ -325,7 +244,7 @@ func _snap_to_rectangular_grid_center( func _snap_to_guide( snap_to: Vector2, pos: Vector2, distance: float, s1: Vector2, s2: Vector2 ) -> Vector2: - var closest_point := _get_closest_point_to_segment(pos, distance, s1, s2) + var closest_point := Tools.get_closest_point_to_segment(pos, distance, s1, s2) if closest_point == Vector2.INF: # Is not close to a guide return Vector2.INF # Snap to the closest guide @@ -386,7 +305,7 @@ func _pick_color(pos: Vector2i) -> void: if pos.x < 0 or pos.y < 0: return - if is_placing_tiles(): + if Tools.is_placing_tiles(): var cel := Global.current_project.get_current_cel() as CelTileMap Tools.selected_tile_index_changed.emit(cel.get_cell_index_at_coords(pos)) return diff --git a/src/Tools/DesignTools/Bucket.gd b/src/Tools/DesignTools/Bucket.gd index c54a91d35..e46149ae3 100644 --- a/src/Tools/DesignTools/Bucket.gd +++ b/src/Tools/DesignTools/Bucket.gd @@ -204,7 +204,7 @@ func fill(pos: Vector2i) -> void: func fill_in_color(pos: Vector2i) -> void: var project := Global.current_project - if is_placing_tiles(): + if Tools.is_placing_tiles(): for cel in _get_selected_draw_cels(): if cel is not CelTileMap: continue @@ -331,7 +331,7 @@ func _flood_fill(pos: Vector2i) -> void: # implements the floodfill routine by Shawn Hargreaves # from https://www1.udel.edu/CIS/software/dist/allegro-4.2.1/src/flood.c var project := Global.current_project - if is_placing_tiles(): + if Tools.is_placing_tiles(): for cel in _get_selected_draw_cels(): if cel is not CelTileMap: continue diff --git a/src/Tools/DesignTools/CurveTool.gd b/src/Tools/DesignTools/CurveTool.gd index 2ebcc875e..c14684534 100644 --- a/src/Tools/DesignTools/CurveTool.gd +++ b/src/Tools/DesignTools/CurveTool.gd @@ -195,7 +195,7 @@ func _draw_shape() -> void: func _draw_pixel(point: Vector2i, images: Array[ImageExtended]) -> void: - if is_placing_tiles(): + if Tools.is_placing_tiles(): draw_tile(point) else: if Global.current_project.can_pixel_get_drawn(point): diff --git a/src/Tools/DesignTools/LineTool.gd b/src/Tools/DesignTools/LineTool.gd index 668bfcc21..5e8917f0d 100644 --- a/src/Tools/DesignTools/LineTool.gd +++ b/src/Tools/DesignTools/LineTool.gd @@ -174,7 +174,7 @@ func _draw_shape() -> void: for point in points: # Reset drawer every time because pixel perfect sometimes breaks the tool _drawer.reset() - if is_placing_tiles(): + if Tools.is_placing_tiles(): draw_tile(point) else: # Draw each point offsetted based on the shape's thickness diff --git a/src/Tools/SelectionTools/RectSelect.gd b/src/Tools/SelectionTools/RectSelect.gd index 1e3cb7cf1..690379cb7 100644 --- a/src/Tools/SelectionTools/RectSelect.gd +++ b/src/Tools/SelectionTools/RectSelect.gd @@ -101,6 +101,11 @@ func apply_selection(pos: Vector2i) -> void: ## Given an origin point and destination point, returns a rect representing ## where the shape will be drawn and what is its size func _get_result_rect(origin: Vector2i, dest: Vector2i) -> Rect2i: + if Tools.is_placing_tiles(): + var tileset := (Global.current_project.get_current_cel() as CelTileMap).tileset + var grid_size := tileset.tile_size + origin = Tools.snap_to_rectangular_grid_boundary(origin, grid_size) + dest = Tools.snap_to_rectangular_grid_boundary(dest, grid_size) var rect := Rect2i() # Center the rect on the mouse @@ -125,6 +130,7 @@ func _get_result_rect(origin: Vector2i, dest: Vector2i) -> Rect2i: rect.position = Vector2i(mini(origin.x, dest.x), mini(origin.y, dest.y)) rect.size = (origin - dest).abs() - rect.size += Vector2i.ONE + if not Tools.is_placing_tiles(): + rect.size += Vector2i.ONE return rect diff --git a/src/Tools/UtilityTools/ColorPicker.gd b/src/Tools/UtilityTools/ColorPicker.gd index a5bb3df99..8e207edc7 100644 --- a/src/Tools/UtilityTools/ColorPicker.gd +++ b/src/Tools/UtilityTools/ColorPicker.gd @@ -65,7 +65,7 @@ func _pick_color(pos: Vector2i) -> void: pos = project.tiles.get_canon_position(pos) if pos.x < 0 or pos.y < 0: return - if is_placing_tiles(): + if Tools.is_placing_tiles(): var cel := Global.current_project.get_current_cel() as CelTileMap Tools.selected_tile_index_changed.emit(cel.get_cell_index_at_coords(pos)) return diff --git a/src/UI/Canvas/Selection.gd b/src/UI/Canvas/Selection.gd index df0ea3c7d..fb4967081 100644 --- a/src/UI/Canvas/Selection.gd +++ b/src/UI/Canvas/Selection.gd @@ -224,6 +224,10 @@ func _move_with_arrow_keys(event: InputEvent) -> void: if is_zero_approx(absf(move.y)): move.y = 0 var final_direction := (move * step).round() + if Tools.is_placing_tiles(): + var tileset := (Global.current_project.get_current_cel() as CelTileMap).tileset + var grid_size := tileset.tile_size + final_direction *= Vector2(grid_size) move_content(final_direction) @@ -313,6 +317,8 @@ func _update_on_zoom() -> void: func _gizmo_resize() -> void: + if Tools.is_placing_tiles(): + return var dir := dragged_gizmo.direction if Input.is_action_pressed("shape_center"): # Code inspired from https://github.com/GDQuest/godot-open-rpg @@ -379,10 +385,11 @@ func resize_selection() -> void: else: Global.current_project.selection_map.copy_from(original_bitmap) if is_moving_content: - content_pivot = original_big_bounding_rectangle.size / 2.0 preview_image.copy_from(original_preview_image) - DrawingAlgos.nn_rotate(preview_image, angle, content_pivot) - preview_image.resize(size.x, size.y, Image.INTERPOLATE_NEAREST) + if not Tools.is_placing_tiles(): + content_pivot = original_big_bounding_rectangle.size / 2.0 + DrawingAlgos.nn_rotate(preview_image, angle, content_pivot) + preview_image.resize(size.x, size.y, Image.INTERPOLATE_NEAREST) if temp_rect.size.x < 0: preview_image.flip_x() if temp_rect.size.y < 0: @@ -456,6 +463,15 @@ func move_borders(move: Vector2i) -> void: return marching_ants_outline.offset += Vector2(move) big_bounding_rectangle.position += move + if Tools.is_placing_tiles(): + var tileset := (Global.current_project.get_current_cel() as CelTileMap).tileset + var grid_size := tileset.tile_size + marching_ants_outline.offset = Tools.snap_to_rectangular_grid_boundary( + marching_ants_outline.offset, grid_size + ) + big_bounding_rectangle.position = Vector2i( + Tools.snap_to_rectangular_grid_boundary(big_bounding_rectangle.position, grid_size) + ) queue_redraw()