mirror of
https://github.com/tonytins/CozyPixelStudio.git
synced 2025-05-05 16:24:49 -04:00
Rename SelectionRectangle to SelectionShape, make it have non-rectangular shape and multiple SelectionShapes can exist
- Create multiple selection rectangles - Merge them together if they intersect - Move the selections (without contents as of right now) - Gizmos are being drawn but they are not functional yet Code is very ugly.
This commit is contained in:
parent
2a086e3ea0
commit
f3fb98e068
10 changed files with 230 additions and 74 deletions
|
@ -84,6 +84,16 @@ _global_script_classes=[ {
|
|||
"language": "GDScript",
|
||||
"path": "res://src/Classes/Project.gd"
|
||||
}, {
|
||||
"base": "Reference",
|
||||
"class": "Selection",
|
||||
"language": "GDScript",
|
||||
"path": "res://src/Classes/Selection.gd"
|
||||
}, {
|
||||
"base": "Polygon2D",
|
||||
"class": "SelectionShape",
|
||||
"language": "GDScript",
|
||||
"path": "res://src/Tools/SelectionShape.gd"
|
||||
}, {
|
||||
"base": "Guide",
|
||||
"class": "SymmetryGuide",
|
||||
"language": "GDScript",
|
||||
|
@ -105,6 +115,8 @@ _global_script_class_icons={
|
|||
"PaletteColor": "",
|
||||
"Patterns": "",
|
||||
"Project": "",
|
||||
"Selection": "",
|
||||
"SelectionShape": "",
|
||||
"SymmetryGuide": ""
|
||||
}
|
||||
|
||||
|
|
|
@ -111,7 +111,6 @@ var small_preview_viewport : ViewportContainer
|
|||
var camera : Camera2D
|
||||
var camera2 : Camera2D
|
||||
var camera_preview : Camera2D
|
||||
var selection_rectangle : Polygon2D
|
||||
var horizontal_ruler : BaseButton
|
||||
var vertical_ruler : BaseButton
|
||||
var transparent_checker : ColorRect
|
||||
|
@ -215,7 +214,6 @@ func _ready() -> void:
|
|||
camera = find_node_by_name(main_viewport, "Camera2D")
|
||||
camera2 = find_node_by_name(root, "Camera2D2")
|
||||
camera_preview = find_node_by_name(root, "CameraPreview")
|
||||
selection_rectangle = find_node_by_name(root, "SelectionRectangle")
|
||||
horizontal_ruler = find_node_by_name(root, "HorizontalRuler")
|
||||
vertical_ruler = find_node_by_name(root, "VerticalRuler")
|
||||
transparent_checker = find_node_by_name(root, "TransparentChecker")
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
class_name Project extends Reference
|
||||
# A class for project properties.
|
||||
|
||||
|
||||
var name := "" setget name_changed
|
||||
var size : Vector2 setget size_changed
|
||||
var undo_redo : UndoRedo
|
||||
|
@ -24,7 +23,7 @@ var x_symmetry_axis : SymmetryGuide
|
|||
var y_symmetry_axis : SymmetryGuide
|
||||
|
||||
var selected_pixels := []
|
||||
var selected_rect := Rect2(0, 0, 0, 0) setget _set_selected_rect
|
||||
var selections := [] setget _set_selections # Array of SelectionShape(s)
|
||||
|
||||
# For every camera (currently there are 3)
|
||||
var cameras_zoom := [Vector2(0.15, 0.15), Vector2(0.15, 0.15), Vector2(0.15, 0.15)] # Array of Vector2
|
||||
|
@ -85,9 +84,9 @@ func clear_selection() -> void:
|
|||
selected_pixels.clear()
|
||||
|
||||
|
||||
func _set_selected_rect(value : Rect2) -> void:
|
||||
selected_rect = value
|
||||
Global.selection_rectangle.set_rect(value)
|
||||
func _set_selections(value : Array) -> void:
|
||||
selections = value
|
||||
# Global.selection_rectangl.set_rect(value)
|
||||
|
||||
|
||||
func change_project() -> void:
|
||||
|
@ -147,7 +146,7 @@ func change_project() -> void:
|
|||
self.animation_tags = animation_tags
|
||||
|
||||
# Change the selection rectangle
|
||||
Global.selection_rectangle.set_rect(selected_rect)
|
||||
# Global.selection_rectangl.set_rect(selected_rect)
|
||||
|
||||
# Change the guides
|
||||
for guide in Global.canvas.get_children():
|
||||
|
@ -364,7 +363,7 @@ func name_changed(value : String) -> void:
|
|||
func size_changed(value : Vector2) -> void:
|
||||
size = value
|
||||
update_tile_mode_rects()
|
||||
Global.selection_rectangle.set_rect(Global.selection_rectangle.get_rect())
|
||||
# Global.selection_rectangl.set_rect(Global.selection_rectangl.get_rect())
|
||||
|
||||
|
||||
func frames_changed(value : Array) -> void:
|
||||
|
@ -583,3 +582,16 @@ func update_tile_mode_rects() -> void:
|
|||
|
||||
func is_empty() -> bool:
|
||||
return frames.size() == 1 and layers.size() == 1 and frames[0].cels[0].image.is_invisible() and animation_tags.size() == 0
|
||||
|
||||
|
||||
func get_selection_image() -> Image:
|
||||
var image := Image.new()
|
||||
var cel_image : Image = frames[current_frame].cels[current_layer].image
|
||||
image.copy_from(cel_image)
|
||||
image.lock()
|
||||
image.fill(Color(0, 0, 0, 0))
|
||||
for pixel in selected_pixels:
|
||||
var color : Color = cel_image.get_pixelv(pixel)
|
||||
image.set_pixelv(pixel, color)
|
||||
image.unlock()
|
||||
return image
|
||||
|
|
11
src/Classes/Selection.gd
Normal file
11
src/Classes/Selection.gd
Normal file
|
@ -0,0 +1,11 @@
|
|||
class_name Selection extends Reference
|
||||
|
||||
|
||||
var selected_area := [] # Selected pixels for each selection
|
||||
var borders : PoolVector2Array
|
||||
var node : SelectionShape
|
||||
|
||||
|
||||
func _init(_node : SelectionShape) -> void:
|
||||
node = _node
|
||||
Global.canvas.add_child(node)
|
|
@ -9,12 +9,16 @@ func draw_start(position : Vector2) -> void:
|
|||
starting_pos = position
|
||||
offset = position
|
||||
if Global.current_project.selected_pixels:
|
||||
Global.selection_rectangle.move_start(true)
|
||||
pass
|
||||
# Global.selection_rectangl.move_start(true)
|
||||
|
||||
|
||||
func draw_move(position : Vector2) -> void:
|
||||
if Global.current_project.selected_pixels:
|
||||
Global.selection_rectangle.move_rect(position - offset)
|
||||
for selection in Global.current_project.selections:
|
||||
selection.move_polygon(position - offset)
|
||||
offset = position
|
||||
# Global.selection_rectangl.move_rect(position - offset)
|
||||
else:
|
||||
Global.canvas.move_preview_location = position - starting_pos
|
||||
offset = position
|
||||
|
@ -37,7 +41,8 @@ func draw_end(position : Vector2) -> void:
|
|||
|
||||
# print(pixels[3])
|
||||
if project.selected_pixels:
|
||||
Global.selection_rectangle.move_end()
|
||||
for selection in Global.current_project.selections:
|
||||
selection.select_rect()
|
||||
else:
|
||||
Global.canvas.move_preview_location = Vector2.ZERO
|
||||
var image_copy := Image.new()
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
extends BaseTool
|
||||
|
||||
|
||||
var current_selection_id := -1
|
||||
var start_position := Vector2.INF
|
||||
var _start := Rect2(0, 0, 0, 0)
|
||||
var _offset := Vector2.ZERO
|
||||
var _drag := false
|
||||
|
@ -8,49 +10,85 @@ var _move := false
|
|||
|
||||
|
||||
func draw_start(position : Vector2) -> void:
|
||||
if Global.selection_rectangle.has_point(position):
|
||||
var i := 0
|
||||
for selection in Global.current_project.selections:
|
||||
if selection.has_point(position):
|
||||
current_selection_id = i
|
||||
i += 1
|
||||
|
||||
if current_selection_id == -1:
|
||||
current_selection_id = Global.current_project.selections.size()
|
||||
var selection_shape := preload("res://src/Tools/SelectionShape.tscn").instance()
|
||||
Global.current_project.selections.append(selection_shape)
|
||||
Global.canvas.add_child(selection_shape)
|
||||
_start = Rect2(position, Vector2.ZERO)
|
||||
selection_shape.set_rect(_start)
|
||||
else:
|
||||
var selection : SelectionShape = Global.current_project.selections[current_selection_id]
|
||||
_move = true
|
||||
_offset = position
|
||||
Global.selection_rectangle.move_start(Tools.shift)
|
||||
_set_cursor_text(Global.selection_rectangle.get_rect())
|
||||
else:
|
||||
_drag = true
|
||||
_start = Rect2(position, Vector2.ZERO)
|
||||
Global.selection_rectangle.set_rect(_start)
|
||||
start_position = position
|
||||
_set_cursor_text(selection.get_rect())
|
||||
# if Global.selection_rectangle.has_point(position):
|
||||
# _move = true
|
||||
# _offset = position
|
||||
# Global.selection_rectangle.move_start(Tools.shift)
|
||||
# _set_cursor_text(Global.selection_rectangle.get_rect())
|
||||
# else:
|
||||
# _drag = true
|
||||
# _start = Rect2(position, Vector2.ZERO)
|
||||
# Global.selection_rectangle.set_rect(_start)
|
||||
|
||||
|
||||
func draw_move(position : Vector2) -> void:
|
||||
var selection : SelectionShape = Global.current_project.selections[current_selection_id]
|
||||
|
||||
if _move:
|
||||
Global.selection_rectangle.move_rect(position - _offset)
|
||||
for _selection in Global.current_project.selections:
|
||||
_selection.move_polygon(position - _offset)
|
||||
_offset = position
|
||||
_set_cursor_text(Global.selection_rectangle.get_rect())
|
||||
_set_cursor_text(selection.get_rect())
|
||||
else:
|
||||
var rect := _start.expand(position).abs()
|
||||
rect = rect.grow_individual(0, 0, 1, 1)
|
||||
Global.selection_rectangle.set_rect(rect)
|
||||
selection.set_rect(rect)
|
||||
_set_cursor_text(rect)
|
||||
# if _move:
|
||||
# Global.selection_rectangle.move_rect(position - _offset)
|
||||
# _offset = position
|
||||
# _set_cursor_text(Global.selection_rectangle.get_rect())
|
||||
# else:
|
||||
# var rect := _start.expand(position).abs()
|
||||
# rect = rect.grow_individual(0, 0, 1, 1)
|
||||
# Global.selection_rectangle.set_rect(rect)
|
||||
# _set_cursor_text(rect)
|
||||
|
||||
|
||||
func draw_end(_position : Vector2) -> void:
|
||||
func draw_end(position : Vector2) -> void:
|
||||
if _move:
|
||||
Global.selection_rectangle.move_end()
|
||||
for _selection in Global.current_project.selections:
|
||||
_selection.move_polygon_end(position, start_position)
|
||||
else:
|
||||
Global.selection_rectangle.select_rect()
|
||||
_drag = false
|
||||
var selection : SelectionShape = Global.current_project.selections[current_selection_id]
|
||||
selection.select_rect()
|
||||
# _drag = false
|
||||
_move = false
|
||||
cursor_text = ""
|
||||
start_position = Vector2.INF
|
||||
current_selection_id = -1
|
||||
|
||||
|
||||
func cursor_move(position : Vector2) -> void:
|
||||
if _drag:
|
||||
_cursor = Vector2.INF
|
||||
elif Global.selection_rectangle.has_point(position):
|
||||
_cursor = Vector2.INF
|
||||
Global.main_viewport.mouse_default_cursor_shape = Input.CURSOR_MOVE
|
||||
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
|
||||
else:
|
||||
_cursor = position
|
||||
Global.main_viewport.mouse_default_cursor_shape = Input.CURSOR_CROSS
|
||||
func cursor_move(_position : Vector2) -> void:
|
||||
pass
|
||||
# if _drag:
|
||||
# _cursor = Vector2.INF
|
||||
# elif Global.selection_rectangle.has_point(position):
|
||||
# _cursor = Vector2.INF
|
||||
# Global.main_viewport.mouse_default_cursor_shape = Input.CURSOR_MOVE
|
||||
# Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
|
||||
# else:
|
||||
# _cursor = position
|
||||
# Global.main_viewport.mouse_default_cursor_shape = Input.CURSOR_CROSS
|
||||
|
||||
|
||||
func _set_cursor_text(rect : Rect2) -> void:
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
extends Polygon2D
|
||||
class_name SelectionShape extends Polygon2D
|
||||
|
||||
|
||||
var line_offset := Vector2.ZERO setget _offset_changed
|
||||
var tween : Tween
|
||||
var local_selected_pixels := [] setget _local_selected_pixels_changed # Array of Vector2s
|
||||
var clear_selection_on_tree_exit := true
|
||||
var _selected_rect := Rect2(0, 0, 0, 0)
|
||||
var _clipped_rect := Rect2(0, 0, 0, 0)
|
||||
var _move_image := Image.new()
|
||||
|
@ -53,6 +55,20 @@ func _draw() -> void:
|
|||
var end := Vector2(end_x, end_y)
|
||||
draw_dashed_line(start, end, Color.white, Color.black, 1.0, 1.0, false)
|
||||
|
||||
if !local_selected_pixels:
|
||||
return
|
||||
var rect_pos := _selected_rect.position
|
||||
var rect_end := _selected_rect.end
|
||||
draw_circle(rect_pos, 1, Color.gray)
|
||||
draw_circle(Vector2((rect_end.x + rect_pos.x) / 2, rect_pos.y), 1, Color.gray)
|
||||
draw_circle(Vector2(rect_end.x, rect_pos.y), 1, Color.gray)
|
||||
draw_circle(Vector2(rect_end.x, (rect_end.y + rect_pos.y) / 2), 1, Color.gray)
|
||||
draw_circle(rect_end, 1, Color.gray)
|
||||
draw_circle(Vector2(rect_end.x, rect_end.y), 1, Color.gray)
|
||||
draw_circle(Vector2((rect_end.x + rect_pos.x) / 2, rect_end.y), 1, Color.gray)
|
||||
draw_circle(Vector2(rect_pos.x, rect_end.y), 1, Color.gray)
|
||||
draw_circle(Vector2(rect_pos.x, (rect_end.y + rect_pos.y) / 2), 1, Color.gray)
|
||||
|
||||
if _move_pixel:
|
||||
draw_texture(_move_texture, _clipped_rect.position, Color(1, 1, 1, 0.5))
|
||||
|
||||
|
@ -114,8 +130,22 @@ func draw_dashed_line(from : Vector2, to : Vector2, color : Color, color2 : Colo
|
|||
draw_line(segment_start, to, color, width, antialiased)
|
||||
|
||||
|
||||
func _local_selected_pixels_changed(value : Array) -> void:
|
||||
for pixel in local_selected_pixels:
|
||||
if pixel in Global.current_project.selected_pixels:
|
||||
Global.current_project.selected_pixels.erase(pixel)
|
||||
|
||||
local_selected_pixels = value
|
||||
|
||||
for pixel in local_selected_pixels:
|
||||
if pixel in Global.current_project.selected_pixels:
|
||||
continue
|
||||
else:
|
||||
Global.current_project.selected_pixels.append(pixel)
|
||||
|
||||
|
||||
func has_point(position : Vector2) -> bool:
|
||||
return _selected_rect.has_point(position)
|
||||
return Geometry.is_point_in_polygon(position, polygon)
|
||||
|
||||
|
||||
func get_rect() -> Rect2:
|
||||
|
@ -128,32 +158,69 @@ func set_rect(rect : Rect2) -> void:
|
|||
polygon[1] = Vector2(rect.end.x, rect.position.y)
|
||||
polygon[2] = rect.end
|
||||
polygon[3] = Vector2(rect.position.x, rect.end.y)
|
||||
visible = not rect.has_no_area()
|
||||
# visible = not rect.has_no_area()
|
||||
|
||||
|
||||
func move_polygon(move : Vector2) -> void:
|
||||
_selected_rect.position += move
|
||||
_clipped_rect.position += move
|
||||
for i in polygon.size():
|
||||
polygon[i] += move
|
||||
# set_rect(_selected_rect)
|
||||
|
||||
|
||||
func move_polygon_end(new_pos : Vector2, old_pos : Vector2) -> void:
|
||||
var diff := new_pos - old_pos
|
||||
var selected_pixels_copy = local_selected_pixels.duplicate()
|
||||
for i in selected_pixels_copy.size():
|
||||
selected_pixels_copy[i] += diff
|
||||
|
||||
self.local_selected_pixels = selected_pixels_copy
|
||||
|
||||
|
||||
func select_rect() -> void:
|
||||
var project : Project = Global.current_project
|
||||
if rect.has_no_area():
|
||||
project.selected_pixels = []
|
||||
else:
|
||||
project.clear_selection()
|
||||
for x in range(rect.position.x, rect.end.x):
|
||||
for y in range(rect.position.y, rect.end.y):
|
||||
self.local_selected_pixels = []
|
||||
var selected_pixels_copy = local_selected_pixels.duplicate()
|
||||
for x in range(_selected_rect.position.x, _selected_rect.end.x):
|
||||
for y in range(_selected_rect.position.y, _selected_rect.end.y):
|
||||
var pos := Vector2(x, y)
|
||||
# if polygon.size() > 4: # if it's not a rectangle
|
||||
# if !Geometry.is_point_in_polygon(pos, polygon):
|
||||
# continue
|
||||
if x < 0 or x >= project.size.x:
|
||||
continue
|
||||
if y < 0 or y >= project.size.y:
|
||||
continue
|
||||
project.selected_pixels.append(Vector2(x, y))
|
||||
selected_pixels_copy.append(pos)
|
||||
|
||||
self.local_selected_pixels = selected_pixels_copy
|
||||
if local_selected_pixels.size() == 0:
|
||||
queue_free()
|
||||
return
|
||||
merge_multiple_selections()
|
||||
# var undo_data = _get_undo_data(false)
|
||||
# Global.current_project.selected_rect = _selected_rect
|
||||
# commit_undo("Rectangle Select", undo_data)
|
||||
|
||||
|
||||
func move_rect(move : Vector2) -> void:
|
||||
_selected_rect.position += move
|
||||
_clipped_rect.position += move
|
||||
set_rect(_selected_rect)
|
||||
|
||||
|
||||
func select_rect() -> void:
|
||||
var undo_data = _get_undo_data(false)
|
||||
Global.current_project.selected_rect = _selected_rect
|
||||
commit_undo("Rectangle Select", undo_data)
|
||||
func merge_multiple_selections() -> void:
|
||||
if Global.current_project.selections.size() < 2:
|
||||
return
|
||||
for selection in Global.current_project.selections:
|
||||
if selection == self:
|
||||
continue
|
||||
var arr = Geometry.merge_polygons_2d(polygon, selection.polygon)
|
||||
# print(arr)
|
||||
if arr.size() == 1: # if the selections intersect
|
||||
set_polygon(arr[0])
|
||||
_selected_rect = _selected_rect.merge(selection._selected_rect)
|
||||
var selected_pixels_copy = local_selected_pixels.duplicate()
|
||||
for pixel in selection.local_selected_pixels:
|
||||
selected_pixels_copy.append(pixel)
|
||||
selection.clear_selection_on_tree_exit = false
|
||||
selection.queue_free()
|
||||
self.local_selected_pixels = selected_pixels_copy
|
||||
|
||||
|
||||
func move_start(move_pixel : bool) -> void:
|
||||
|
@ -283,3 +350,9 @@ func _get_undo_data(undo_image : bool) -> Dictionary:
|
|||
data["image_data"] = image.data
|
||||
image.lock()
|
||||
return data
|
||||
|
||||
|
||||
func _on_SelectionShape_tree_exiting() -> void:
|
||||
Global.current_project.selections.erase(self)
|
||||
if clear_selection_on_tree_exit:
|
||||
self.local_selected_pixels = []
|
12
src/Tools/SelectionShape.tscn
Normal file
12
src/Tools/SelectionShape.tscn
Normal file
|
@ -0,0 +1,12 @@
|
|||
[gd_scene load_steps=2 format=2]
|
||||
|
||||
[ext_resource path="res://src/Tools/SelectionShape.gd" type="Script" id=1]
|
||||
|
||||
[node name="SelectionShape" type="Polygon2D"]
|
||||
z_index = 1
|
||||
color = Color( 1, 1, 1, 0 )
|
||||
invert_enable = true
|
||||
invert_border = 0.5
|
||||
polygon = PoolVector2Array( 0, 0, 0, 0, 0, 0, 0, 0 )
|
||||
script = ExtResource( 1 )
|
||||
[connection signal="tree_exiting" from="." to="." method="_on_SelectionShape_tree_exiting"]
|
|
@ -257,16 +257,21 @@ func edit_menu_id_pressed(id : int) -> void:
|
|||
Global.current_project.undo_redo.redo()
|
||||
Global.control.redone = false
|
||||
EditMenuId.COPY:
|
||||
Global.selection_rectangle.copy()
|
||||
pass
|
||||
# Global.selection_rectangl.copy()
|
||||
EditMenuId.CUT:
|
||||
Global.selection_rectangle.cut()
|
||||
pass
|
||||
# Global.selection_rectangl.cut()
|
||||
EditMenuId.PASTE:
|
||||
Global.selection_rectangle.paste()
|
||||
pass
|
||||
# Global.selection_rectangl.paste()
|
||||
EditMenuId.DELETE:
|
||||
Global.selection_rectangle.delete()
|
||||
pass
|
||||
# Global.selection_rectangl.delete()
|
||||
EditMenuId.CLEAR_SELECTION:
|
||||
Global.selection_rectangle.set_rect(Rect2(0, 0, 0, 0))
|
||||
Global.selection_rectangle.select_rect()
|
||||
pass
|
||||
# Global.selection_rectangl.set_rect(Rect2(0, 0, 0, 0))
|
||||
# Global.selection_rectangl.select_rect()
|
||||
EditMenuId.PREFERENCES:
|
||||
Global.preferences_dialog.popup_centered(Vector2(400, 280))
|
||||
Global.dialog_open(true)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
[gd_scene load_steps=28 format=2]
|
||||
[gd_scene load_steps=27 format=2]
|
||||
|
||||
[ext_resource path="res://src/UI/ToolButtons.gd" type="Script" id=1]
|
||||
[ext_resource path="res://src/UI/Canvas/CanvasPreview.tscn" type="PackedScene" id=2]
|
||||
|
@ -7,7 +7,6 @@
|
|||
[ext_resource path="res://src/UI/TransparentChecker.tscn" type="PackedScene" id=5]
|
||||
[ext_resource path="res://src/UI/Canvas/Rulers/HorizontalRuler.gd" type="Script" id=6]
|
||||
[ext_resource path="res://src/UI/Canvas/CameraMovement.gd" type="Script" id=7]
|
||||
[ext_resource path="res://src/SelectionRectangle.gd" type="Script" id=8]
|
||||
[ext_resource path="res://src/Shaders/TransparentChecker.shader" type="Shader" id=9]
|
||||
[ext_resource path="res://assets/graphics/dark_themes/tools/bucket.png" type="Texture" id=10]
|
||||
[ext_resource path="res://assets/graphics/dark_themes/tools/colorpicker.png" type="Texture" id=11]
|
||||
|
@ -348,15 +347,6 @@ current = true
|
|||
zoom = Vector2( 0.15, 0.15 )
|
||||
script = ExtResource( 7 )
|
||||
|
||||
[node name="SelectionRectangle" type="Polygon2D" parent="CanvasAndTimeline/ViewportAndRulers/HSplitContainer/ViewportandVerticalRuler/ViewportContainer/Viewport"]
|
||||
visible = false
|
||||
z_index = 1
|
||||
color = Color( 1, 1, 1, 0 )
|
||||
invert_enable = true
|
||||
invert_border = 0.5
|
||||
polygon = PoolVector2Array( 0, 0, 0, 0, 0, 0, 0, 0 )
|
||||
script = ExtResource( 8 )
|
||||
|
||||
[node name="ViewportContainer2" type="ViewportContainer" parent="CanvasAndTimeline/ViewportAndRulers/HSplitContainer"]
|
||||
margin_left = 902.0
|
||||
margin_right = 902.0
|
||||
|
|
Loading…
Add table
Reference in a new issue