diff --git a/assets/graphics/tools/cursors/paintselect.png b/assets/graphics/tools/cursors/paintselect.png new file mode 100644 index 000000000..0c0aa794e Binary files /dev/null and b/assets/graphics/tools/cursors/paintselect.png differ diff --git a/assets/graphics/tools/paintselect.png b/assets/graphics/tools/paintselect.png new file mode 100644 index 000000000..9af7e1ef7 Binary files /dev/null and b/assets/graphics/tools/paintselect.png differ diff --git a/src/Autoload/Tools.gd b/src/Autoload/Tools.gd index ab82dc911..4a3d14d8f 100644 --- a/src/Autoload/Tools.gd +++ b/src/Autoload/Tools.gd @@ -51,6 +51,13 @@ var tools := { "lasso", preload("res://src/Tools/SelectionTools/Lasso.tscn") ), + "PaintSelect": + Tool.new( + "PaintSelect", + "Select by Drawing", + "paint_selection", + preload("res://src/Tools/SelectionTools/PaintSelect.tscn") + ), "Move": Tool.new("Move", "Move", "move", preload("res://src/Tools/Move.tscn")), "Zoom": Tool.new("Zoom", "Zoom", "zoom", preload("res://src/Tools/Zoom.tscn")), "Pan": Tool.new("Pan", "Pan", "pan", preload("res://src/Tools/Pan.tscn")), diff --git a/src/Tools/SelectionTools/PaintSelect.gd b/src/Tools/SelectionTools/PaintSelect.gd new file mode 100644 index 000000000..2f58cdefd --- /dev/null +++ b/src/Tools/SelectionTools/PaintSelect.gd @@ -0,0 +1,380 @@ +extends SelectionTool + +var _brush_size: int = 2 +var _brush := Brushes.get_default_brush() +var _indicator := BitMap.new() +var _polylines := [] +var _brush_image := Image.new() +var _brush_texture := ImageTexture.new() +var _circle_tool_shortcut: PoolVector2Array + +var _last_position := Vector2.INF +var _draw_points := [] + + +func get_config() -> Dictionary: + return { + "brush_type": _brush.type, + "brush_index": _brush.index, + "brush_size": _brush_size, + } + + +func set_config(config: Dictionary) -> void: + var type: int = config.get("brush_type", _brush.type) + var index: int = config.get("brush_index", _brush.index) + _brush = Global.brushes_popup.get_brush(type, index) + _brush_size = config.get("brush_size", _brush_size) + + +func update_config() -> void: + $Brush/BrushSize.value = _brush_size + update_brush() + + +func draw_start(position: Vector2) -> void: + .draw_start(position) + if !_move: + _draw_points.append_array(draw_tool(position)) + _last_position = position + + +func draw_move(position: Vector2) -> void: + if selection_node.arrow_key_move: + return + .draw_move(position) + if !_move: + append_gap(_last_position, position) + _last_position = position + _draw_points.append_array(draw_tool(position)) + _offset = position + + +func draw_end(position: Vector2) -> void: + if selection_node.arrow_key_move: + return + if !_move: + _draw_points.append_array(draw_tool(position)) + .draw_end(position) + + +func draw_preview() -> void: + if _last_position != Vector2.INF and !_move: + var canvas: Node2D = Global.canvas.previews + var position := canvas.position + var scale := canvas.scale + if Global.mirror_view: + position.x = position.x + Global.current_project.size.x + scale.x = -1 + canvas.draw_set_transform(position, canvas.rotation, scale) + var indicator := _fill_bitmap_with_points(_draw_points, Global.current_project.size) + + for line in _create_polylines(indicator): + canvas.draw_polyline(PoolVector2Array(line), Color.black) + + # Handle mirroring + if Tools.horizontal_mirror: + for line in _create_polylines( + _fill_bitmap_with_points( + mirror_array(_draw_points, true, false), Global.current_project.size + ) + ): + canvas.draw_polyline(PoolVector2Array(line), Color.black) + if Tools.vertical_mirror: + for line in _create_polylines( + _fill_bitmap_with_points( + mirror_array(_draw_points, true, true), Global.current_project.size + ) + ): + canvas.draw_polyline(PoolVector2Array(line), Color.black) + if Tools.vertical_mirror: + for line in _create_polylines( + _fill_bitmap_with_points( + mirror_array(_draw_points, false, true), Global.current_project.size + ) + ): + canvas.draw_polyline(PoolVector2Array(line), Color.black) + + canvas.draw_set_transform(canvas.position, canvas.rotation, canvas.scale) + + +func apply_selection(_position) -> void: + var project: Project = Global.current_project + var cleared := false + if !_add and !_subtract and !_intersect: + cleared = true + Global.canvas.selection.clear_selection() + if _draw_points.size() > 1: + var selection_map_copy := SelectionMap.new() + selection_map_copy.copy_from(project.selection_map) + if _intersect: + selection_map_copy.clear() + paint_selection(selection_map_copy, _draw_points) + + # Handle mirroring + if Tools.horizontal_mirror: + paint_selection(selection_map_copy, mirror_array(_draw_points, true, false)) + if Tools.vertical_mirror: + paint_selection(selection_map_copy, mirror_array(_draw_points, true, true)) + if Tools.vertical_mirror: + paint_selection(selection_map_copy, mirror_array(_draw_points, false, true)) + + project.selection_map = selection_map_copy + Global.canvas.selection.big_bounding_rectangle = project.selection_map.get_used_rect() + else: + if !cleared: + Global.canvas.selection.clear_selection() + + Global.canvas.selection.commit_undo("Select", undo_data) + _draw_points.clear() + _last_position = Vector2.INF + + +func paint_selection(selection_map: SelectionMap, points: PoolVector2Array) -> void: + var project: Project = Global.current_project + var size := selection_map.get_size() + for point in points: + if point.x < 0 or point.y < 0 or point.x >= size.x or point.y >= size.y: + continue + if _intersect: + if project.selection_map.is_pixel_selected(point): + selection_map.select_pixel(point, true) + else: + selection_map.select_pixel(point, !_subtract) + + +# Bresenham's Algorithm +# Thanks to https://godotengine.org/qa/35276/tile-based-line-drawing-algorithm-efficiency +func append_gap(start: Vector2, end: Vector2) -> void: + var dx := int(abs(end.x - start.x)) + var dy := int(-abs(end.y - start.y)) + var err := dx + dy + var e2 := err << 1 + var sx = 1 if start.x < end.x else -1 + var sy = 1 if start.y < end.y else -1 + var x = start.x + var y = start.y + while !(x == end.x && y == end.y): + e2 = err << 1 + if e2 >= dy: + err += dy + x += sx + if e2 <= dx: + err += dx + y += sy + _draw_points.append_array(draw_tool(Vector2(x, y))) + + +func mirror_array(array: Array, h: bool, v: bool) -> Array: + var new_array := [] + var project: Project = Global.current_project + for point in array: + if h and v: + new_array.append( + Vector2(project.x_symmetry_point - point.x, project.y_symmetry_point - point.y) + ) + elif h: + new_array.append(Vector2(project.x_symmetry_point - point.x, point.y)) + elif v: + new_array.append(Vector2(point.x, project.y_symmetry_point - point.y)) + + return new_array + + +func draw_tool(position: Vector2) -> PoolVector2Array: + _prepare_tool() + return _draw_tool(position) + + +func _prepare_tool() -> void: + match _brush.type: + Brushes.CIRCLE: + _prepare_circle_tool(false) + Brushes.FILLED_CIRCLE: + _prepare_circle_tool(true) + + +func _prepare_circle_tool(fill: bool) -> void: + var circle_tool_map := _create_circle_indicator(_brush_size, fill) + # Go through that BitMap and build an Array of the "displacement" from the center of the bits + # that are true. + var diameter := _brush_size * 2 + 1 + for n in range(0, diameter): + for m in range(0, diameter): + if circle_tool_map.get_bit(Vector2(m, n)): + _circle_tool_shortcut.append(Vector2(m - _brush_size, n - _brush_size)) + + +# Make sure to always have invoked _prepare_tool() before this. This computes the coordinates to be +# drawn if it can (except for the generic brush, when it's actually drawing them) +func _draw_tool(position: Vector2) -> PoolVector2Array: + match _brush.type: + Brushes.PIXEL: + return _compute_draw_tool_pixel(position) + Brushes.CIRCLE: + return _compute_draw_tool_circle(position, false) + Brushes.FILLED_CIRCLE: + return _compute_draw_tool_circle(position, true) + _: + return _compute_draw_tool_brush(position) + + +func _compute_draw_tool_pixel(position: Vector2) -> PoolVector2Array: + var result := PoolVector2Array() + var start := position - Vector2.ONE * (_brush_size >> 1) + var end := start + Vector2.ONE * _brush_size + for y in range(start.y, end.y): + for x in range(start.x, end.x): + if !_draw_points.has(Vector2(x, y)): + result.append(Vector2(x, y)) + return result + + +# Compute the array of coordinates that should be drawn +func _compute_draw_tool_circle(position: Vector2, fill := false) -> PoolVector2Array: + var size := Vector2(_brush_size, _brush_size) + var pos = position - (size / 2).floor() + if _circle_tool_shortcut: + return _draw_tool_circle_from_map(position) + + var result := PoolVector2Array() + if fill: + result = DrawingAlgos.get_ellipse_points_filled(pos, size) + else: + result = DrawingAlgos.get_ellipse_points(pos, size) + return result + + +func _draw_tool_circle_from_map(position: Vector2) -> PoolVector2Array: + var result := PoolVector2Array() + for displacement in _circle_tool_shortcut: + result.append(position + displacement) + return result + + +func _compute_draw_tool_brush(position: Vector2) -> PoolVector2Array: + var result := PoolVector2Array() + var brush_mask := BitMap.new() + var pos = position - (_indicator.get_size() / 2).floor() + brush_mask.create_from_image_alpha(_brush_image, 0.0) + for x in brush_mask.get_size().x: + for y in brush_mask.get_size().y: + if !_draw_points.has(Vector2(x, y)): + if brush_mask.get_bit(Vector2(x, y)): + result.append(pos + Vector2(x, y)) + + return result + + +func _on_BrushType_pressed() -> void: + if not Global.brushes_popup.is_connected("brush_selected", self, "_on_Brush_selected"): + Global.brushes_popup.connect( + "brush_selected", self, "_on_Brush_selected", [], CONNECT_ONESHOT + ) + # Now we set position and tab allignment considering certain conditions + var pop_size := Vector2(226, 72) + var pop_position: Vector2 = $Brush/Type.rect_global_position + var off_limit: float = Global.shrink * (pop_position.x + pop_size.x) - OS.get_window_size().x + if off_limit <= 72 and off_limit > 0: # Some space left "Leftward" + pop_position -= Vector2(pop_size.x / 2.0 - 48, -32) + Global.brushes_popup.get_node("TabContainer").tab_align = TabContainer.ALIGN_CENTER + elif off_limit >= 72: # No space left "Leftward" + pop_position -= Vector2(pop_size.x / 2.0 + 16, -32) + Global.brushes_popup.get_node("TabContainer").tab_align = TabContainer.ALIGN_RIGHT + else: + pop_position -= Vector2(0, -32) # Plenty of space left "Leftward" + Global.brushes_popup.get_node("TabContainer").tab_align = TabContainer.ALIGN_LEFT + Global.brushes_popup.popup(Rect2(pop_position, pop_size)) + + +func _on_Brush_selected(brush: Brushes.Brush) -> void: + _brush = brush + update_brush() + save_config() + + +func _on_BrushSize_value_changed(value: float) -> void: + if _brush_size != int(value): + _brush_size = int(value) + update_config() + save_config() + + +# The Blue Indicator code +func update_brush() -> void: + $Brush/BrushSize.suffix = "px" # Assume we are using default brushes + match _brush.type: + Brushes.PIXEL: + _brush_texture.create_from_image(load("res://assets/graphics/pixel_image.png"), 0) + Brushes.CIRCLE: + _brush_texture.create_from_image(load("res://assets/graphics/circle_9x9.png"), 0) + Brushes.FILLED_CIRCLE: + _brush_texture.create_from_image(load("res://assets/graphics/circle_filled_9x9.png"), 0) + Brushes.FILE, Brushes.RANDOM_FILE, Brushes.CUSTOM: + $Brush/BrushSize.suffix = "00 %" # Use a different size convention on images + if _brush.random.size() <= 1: + _brush_image = _create_blended_brush_image(_brush.image) + else: + var random := randi() % _brush.random.size() + _brush_image = _create_blended_brush_image(_brush.random[random]) + _brush_texture.create_from_image(_brush_image, 0) + _indicator = _create_brush_indicator() + _polylines = _create_polylines(_indicator) + + $Brush/Type/Texture.texture = _brush_texture + + +func _create_blended_brush_image(image: Image) -> Image: + var size := image.get_size() * _brush_size + var brush := Image.new() + brush.copy_from(image) + brush.resize(size.x, size.y, Image.INTERPOLATE_NEAREST) + return brush + + +func _create_brush_indicator() -> BitMap: + match _brush.type: + Brushes.PIXEL: + return _create_pixel_indicator(_brush_size) + Brushes.CIRCLE: + return _create_circle_indicator(_brush_size, false) + Brushes.FILLED_CIRCLE: + return _create_circle_indicator(_brush_size, true) + _: + return _create_image_indicator(_brush_image) + + +func _create_pixel_indicator(size: int) -> BitMap: + var bitmap := BitMap.new() + bitmap.create(Vector2.ONE * size) + bitmap.set_bit_rect(Rect2(Vector2.ZERO, Vector2.ONE * size), true) + return bitmap + + +func _create_circle_indicator(size: int, fill := false) -> BitMap: + _circle_tool_shortcut = PoolVector2Array() + var diameter := Vector2(size, size) * 2 + Vector2.ONE + return _fill_bitmap_with_points(_compute_draw_tool_circle(Vector2(size, size), fill), diameter) + + +func _create_image_indicator(image: Image) -> BitMap: + var bitmap := BitMap.new() + bitmap.create_from_image_alpha(image, 0.0) + return bitmap + + +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) + + +func draw_indicator_at(position: Vector2, offset: Vector2, color: Color) -> void: + var canvas = Global.canvas.indicators + position -= (_indicator.get_size() / 2).floor() + position -= offset + canvas.draw_set_transform(position, canvas.rotation, canvas.scale) + var polylines := _polylines + for line in polylines: + var pool := PoolVector2Array(line) + canvas.draw_polyline(pool, color) + canvas.draw_set_transform(canvas.position, canvas.rotation, canvas.scale) diff --git a/src/Tools/SelectionTools/PaintSelect.tscn b/src/Tools/SelectionTools/PaintSelect.tscn new file mode 100644 index 000000000..699f9ccd0 --- /dev/null +++ b/src/Tools/SelectionTools/PaintSelect.tscn @@ -0,0 +1,90 @@ +[gd_scene load_steps=6 format=2] + +[ext_resource path="res://src/Tools/SelectionTools/SelectionTool.tscn" type="PackedScene" id=1] +[ext_resource path="res://src/Tools/SelectionTools/PaintSelect.gd" type="Script" id=2] +[ext_resource path="res://src/UI/Nodes/ValueSlider.tscn" type="PackedScene" id=3] + +[sub_resource type="StyleBoxFlat" id=1] +bg_color = Color( 1, 1, 1, 1 ) +border_color = Color( 1, 1, 1, 1 ) +corner_radius_top_left = 5 +corner_radius_top_right = 5 +corner_radius_bottom_right = 5 +corner_radius_bottom_left = 5 +anti_aliasing = false + +[sub_resource type="StyleBoxFlat" id=2] +bg_color = Color( 1, 1, 1, 1 ) +border_color = Color( 1, 1, 1, 1 ) +corner_radius_top_left = 5 +corner_radius_top_right = 5 +corner_radius_bottom_right = 5 +corner_radius_bottom_left = 5 +anti_aliasing = false + +[node name="ToolOptions" instance=ExtResource( 1 )] +script = ExtResource( 2 ) + +[node name="Brush" type="HBoxContainer" parent="." index="2"] +margin_top = 26.0 +margin_right = 116.0 +margin_bottom = 58.0 +alignment = 1 + +[node name="Type" type="Button" parent="Brush" index="0"] +margin_right = 32.0 +margin_bottom = 32.0 +rect_min_size = Vector2( 32, 32 ) +hint_tooltip = "Select a brush" +mouse_default_cursor_shape = 2 +size_flags_horizontal = 0 +custom_styles/hover = SubResource( 1 ) +custom_styles/pressed = SubResource( 1 ) +custom_styles/focus = SubResource( 2 ) +custom_styles/disabled = SubResource( 2 ) +custom_styles/normal = SubResource( 1 ) + +[node name="Texture" type="TextureRect" parent="Brush/Type" index="0"] +margin_right = 32.0 +margin_bottom = 32.0 +expand = true +stretch_mode = 6 + +[node name="BrushSize" parent="Brush" index="1" instance=ExtResource( 3 )] +margin_left = 36.0 +margin_right = 116.0 +margin_bottom = 32.0 +min_value = 1.0 +max_value = 25.0 +value = 1.0 +allow_greater = true +prefix = "Size:" +suffix = "px" +is_global = true + +[node name="PositionLabel" parent="." index="3"] +margin_top = 62.0 +margin_bottom = 76.0 + +[node name="XSpinBox" parent="." index="4"] +margin_top = 80.0 +margin_bottom = 104.0 + +[node name="YSpinBox" parent="." index="5"] +margin_top = 108.0 +margin_bottom = 132.0 + +[node name="SizeLabel" parent="." index="6"] +margin_top = 136.0 +margin_bottom = 150.0 + +[node name="WSpinBox" parent="." index="7"] +margin_top = 154.0 +margin_bottom = 178.0 + +[node name="HSpinBox" parent="." index="8"] +margin_top = 182.0 +margin_bottom = 206.0 + +[connection signal="pressed" from="Brush/Type" to="." method="_on_BrushType_pressed"] +[connection signal="value_changed" from="Brush/BrushSize" to="." method="_on_BrushSize_value_changed"]