1
0
Fork 0
mirror of https://github.com/Orama-Interactive/Pixelorama.git synced 2025-01-18 09:09:47 +00:00

Paint selection brush (#792)

* Added the PaintSelect tool

The shortcut keys are not configured

* Some Optimization attempts

* remove typo

* Re-Designed Tool Icon
This commit is contained in:
Variable 2022-12-14 21:34:13 +05:00 committed by GitHub
parent 6148791069
commit 1df003b2df
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 477 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 B

View file

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

View file

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

View file

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