diff --git a/.import/polygonal_button.svg-2d658a9fda99f155f6d8b7d14df32b3b.md5 b/.import/polygonal_button.svg-2d658a9fda99f155f6d8b7d14df32b3b.md5 new file mode 100644 index 00000000..460d6b11 --- /dev/null +++ b/.import/polygonal_button.svg-2d658a9fda99f155f6d8b7d14df32b3b.md5 @@ -0,0 +1,3 @@ +source_md5="fea3be65d5f88dc9a57ed5d9b0236798" +dest_md5="52b501c75401e79f4200af6f4b93386c" + diff --git a/.import/polygonal_button.svg-2d658a9fda99f155f6d8b7d14df32b3b.stex b/.import/polygonal_button.svg-2d658a9fda99f155f6d8b7d14df32b3b.stex new file mode 100644 index 00000000..7bb2d29f Binary files /dev/null and b/.import/polygonal_button.svg-2d658a9fda99f155f6d8b7d14df32b3b.stex differ diff --git a/global/geometry/convex_polygon_2d.gd b/global/geometry/convex_polygon_2d.gd new file mode 100644 index 00000000..a2cd5324 --- /dev/null +++ b/global/geometry/convex_polygon_2d.gd @@ -0,0 +1,74 @@ +tool +class_name ConvexPolygon2D +extends Polygon + +export(bool) var is_oriented = false setget set_is_oriented +# this is used when checking if the point is inside. If this is oriented then the inside is the part left to the segements +export(bool) var are_border_inside = true setget set_are_border_inside + + +func _init(new_points = PoolVector2Array(), is_oriented_p = false, are_border_inside_p = true): + ._init() + is_oriented = is_oriented_p + are_border_inside = are_border_inside_p + # normaly signals cannot be connected at this point + _set_point_test(new_points) + + +func set_points(new_points): + if not _set_point_test(new_points): + printerr("array is not convex or has intersection, cannot assign") + + +func set_is_oriented(oriented): + is_oriented = oriented + emit_signal("changed") + + +func set_are_border_inside(border_inside): + are_border_inside = border_inside + emit_signal("changed") + + +func is_inside_convex(point): + # use it only if are_border_inside = false or is_oriented = true is important + # otherwise use is_inside + return _is_array_inside_convex(point, points, are_border_inside, is_oriented) + + +func get_orientation(): + # optimized for convex polygones + return _get_array_first_orientation(points) + + +func is_valid(): + return .is_valid() and is_convex() + + +func _set_point_test(new_points): + if _is_array_convex(new_points) and not _has_intersection_array(new_points): + points = new_points + emit_signal("changed") + return true + return false + + +static func _is_array_inside_convex(point, points_array, are_border_inside = true, is_oriented = false): + # only work for convex poly + # if not convex te output is unprevisible + var orientation = _get_array_first_orientation(points_array) + # bool that determine which region we need to check (bounded vs unbounded) + var unbounded_check = is_oriented and orientation == Polygon.Orientation.ANTI_TRIGONOMETRIC + for i in range(points_array.size()): + var edge = points_array[(i + 1) % points_array.size()] - points_array[i % points_array.size()] + var vector_to_point = point - points_array[i % points_array.size()] + var cross = edge.cross(vector_to_point) + var cross_oriented = (cross * orientation) if orientation != Polygon.Orientation.NOT_DEFINED else cross + if cross_oriented < 0.0 or (cross_oriented == 0.0 and not Utils.xor(unbounded_check, are_border_inside)): + # the first check determine if the point is to the side of the unbounded area + # the second condition determine if se are colinear to the edge + # the thrid one with the xor determine the behaviour depending if we consider boder inside + # as the border are considered inside event when we check for the unbounded area + # so in that case we check that teh point is not in the bounded area with inverse value of are_border_inside (achived by the xor) + return unbounded_check + return not unbounded_check diff --git a/global/geometry/polygon.gd b/global/geometry/polygon.gd new file mode 100644 index 00000000..14a5ebc0 --- /dev/null +++ b/global/geometry/polygon.gd @@ -0,0 +1,224 @@ +tool +class_name Polygon +extends Resource + +enum Orientation{ + ANTI_TRIGONOMETRIC = -1, + NOT_DEFINED = 0, + TRIGONOMETRIC = 1, +} + +export(PoolVector2Array) var points = PoolVector2Array() setget set_points + + +func _init(new_points = PoolVector2Array()): + ._init() + _set_point_test(new_points) + + +func set_points(new_points): + _set_point_test(new_points) + + +func reverse_orientation(): + points.invert() + emit_signal("changed") + + +func is_inside(point): + return Geometry.is_point_in_polygon(point, points) + + +func is_convex(): + return _is_array_convex(points) and not _has_intersection_array(points) + + +func push_back_point(point): + var new_points = points + new_points.push_back(point) + return _set_point_test(new_points) + + +func insert_point(point, index): + if index >= points.size(): + return + var new_points = points + new_points.insert(index, point) + return _set_point_test(new_points) + + +func pop_back_point(): + return pop_point(points.size() - 1) + + +func pop_point(index): + if index >= points.size(): + return null + var new_points = points + var point_back = points[index] + new_points.remove(index) + if _set_point_test(new_points): + return point_back + return null + + +func get_bounds(): + return _get_bounds_array(points) + + +func _set_point_test(new_points): + points = new_points + emit_signal("changed") + return true + + +func is_valid(): + return points != null and points.size() >= 3 + + +func get_center_of_mass(): + return _get_center_of_mass_array(points) + + +func set_orientation(orientation): + if orientation != Orientation.TRIGONOMETRIC and orientation != Orientation.ANTI_TRIGONOMETRIC: + return + var poly_orientation = get_orientation() + if poly_orientation != Orientation.NOT_DEFINED and poly_orientation != orientation: + reverse_orientation() + + +func get_shadow_points(shadow_size = 1, shadow_size_offset = Vector2.ZERO): + return _get_shadow_points_array(points, shadow_size, shadow_size_offset) + + +func get_orientation(): + return _get_poly_orientation(points) + + +func get_border_of_poly(edge_index, border_width): + return _get_border_of_array(points, edge_index, border_width) + + +static func _get_center_of_mass_array(points_array): + var center = Vector2.ZERO + if points_array.size() == 0: + return center + for i in points_array: + center += points_array + return center / points_array.size() + + +static func _get_bounds_array(points_array): + if points_array.size() == 0: + return Rect2(Vector2.ZERO, Vector2.ZERO) + var min_x = points_array[0].x + var min_y = points_array[0].y + var max_x = points_array[0].x + var max_y = points_array[0].y + for i in range(1, points_array.size()): + min_x = min(min_x, points_array[i].x) + min_y = min(min_y, points_array[i].y) + max_x = max(max_x, points_array[i].x) + max_y = max(max_y, points_array[i].y) + return Rect2(Vector2(min_x, min_y), Vector2(max_x - min_x, max_y - min_y)) + + +static func _has_intersection_array(points_array): + if points_array.size() < 3: + return false + for i in range(points_array.size() - 2): + for j in range(i + 2, points_array.size()): + var segment_1 = points_array[(i + 1) % points_array.size()] - points_array[i % points_array.size()] + var segment_2 = points_array[(j + 1) % points_array.size()] - points_array[j % points_array.size()] + var intersection = Geometry.line_intersects_line_2d(points_array[i % points_array.size()], segment_1, points_array[j % points_array.size()], segment_2) + var intersetion_segment = intersection - points_array[i % points_array.size()] + if _has_intersection_segent(points_array[i % points_array.size()], points_array[(i + 1) % points_array.size()], points_array[j % points_array.size()], points_array[(j + 1) % points_array.size()]): + return true + return false + + +static func _has_intersection_segent(point_1_start, point_1_end, point_2_start, point_2_end): + var segment_1 = point_1_end - point_1_start + var segment_2 = point_2_end - point_2_start + var intersection = Geometry.line_intersects_line_2d(point_1_start, segment_1, point_2_start, segment_2) + var intersetion_segment = intersection - point_1_start + # segemnt 1 and intersetion_segment are colinear + # the goal is to determine wehter of not intersection is inside the segment + # juste checking the length is not enought as it can be on the other side (dot is < 0) + return intersection != null and intersetion_segment.length() <= segment_1.length() and intersetion_segment.dot(segment_1) >= 0.0 + + +static func _is_array_convex(points_array): + if points_array.size() < 3: + return false + var orientation_head = _get_array_head_orientation(points_array) + for i in range(1, points_array.size()): + var segment_array = PoolVector2Array([points_array[i % points_array.size()] , points_array[(i + 1) % points_array.size()], points_array[(i + 2) % points_array.size() ]]) + var orientation = _get_array_head_orientation(segment_array) + if orientation_head == Orientation.NOT_DEFINED: + orientation_head = orientation + elif orientation != orientation_head and orientation != Orientation.NOT_DEFINED: + return false + return orientation_head != Orientation.NOT_DEFINED + + +static func _get_array_head_orientation(points_array): + if points_array.size() < 3: + return Orientation.NOT_DEFINED + var edge_1 = points_array[1] - points_array[0] + var edge_2 = points_array[2] - points_array[1] + var angle = fposmod(edge_2.angle_to(edge_1), 2.0 * PI) + if angle == PI or angle == 0.0: + return Orientation.NOT_DEFINED + return Orientation.TRIGONOMETRIC if angle < PI else Orientation.ANTI_TRIGONOMETRIC + + +static func _get_array_first_orientation(points_array): + # return the first not undefined orientation + # for convec poly this is the orientation of the poly + if points_array.size() < 3 or not _is_array_convex(points_array): + return Orientation.NOT_DEFINED + else: + for i in range(1, points_array.size()): + var segment_array = PoolVector2Array([points_array[i % points_array.size()], points_array[(i + 1) % points_array.size()], points_array[(i + 2) % points_array.size()]]) + var orientation = _get_array_head_orientation(segment_array) + if orientation != Orientation.NOT_DEFINED: + return orientation + return Orientation.NOT_DEFINED + + +static func _get_poly_orientation(points_array): + if points_array.size() < 3: + return Orientation.NOT_DEFINED + var total_angle = 0 + for i in range(points_array.size()): + var edge_1 = points_array[(i + 1) % points_array.size()] - points_array[i % points_array.size()] + var edge_2 = points_array[(i + 2) % points_array.size()] - points_array[(i + 1) % points_array.size()] + total_angle += - fposmod(edge_2.angle_to(edge_1), 2.0 * PI) + PI + if total_angle == 2.0 * PI or total_angle == 0.0: + # if total_angle this is jute that all point are colinear and therfore does not have an orientation + return Orientation.NOT_DEFINED + return Orientation.TRIGONOMETRIC if total_angle > 0.0 else Orientation.ANTI_TRIGONOMETRIC + + +static func _get_shadow_points_array(points_array, shadow_size = 1, shadow_size_offset = Vector2.ZERO): + # todo improve + var center_of_mass = _get_center_of_mass_array(points_array) + var array = PoolVector2Array() + for vector in points_array: + array.push_back((vector - center_of_mass).normalized() * shadow_size + vector + shadow_size_offset) + return array + + +static func _get_border_of_array(points_array, edge_index, border_width): + if edge_index > points_array.size() or border_width == 0: + return PoolVector2Array() + var orientation = _get_array_first_orientation(points_array) + var edge: Vector2 = points_array[(edge_index + 1) % points_array.size()] - points_array[edge_index % points_array.size()] + var edge_previous: Vector2 = points_array[edge_index % points_array.size()] - points_array[(edge_index - 1) % points_array.size()] + var edge_next: Vector2 = points_array[(edge_index + 2) % points_array.size()] - points_array[(edge_index + 1) % points_array.size()] + var array = [points_array[(edge_index) % points_array.size()], points_array[edge_index % points_array.size()]] + array.push_back((edge + edge_next).tangent().normalized() * orientation * border_width ) + array.push_back((edge + edge_previous).tangent().normalized() * orientation * border_width ) + return PoolVector2Array(array) diff --git a/global/utils.gd b/global/utils.gd index a8e51205..52f37e85 100644 --- a/global/utils.gd +++ b/global/utils.gd @@ -119,3 +119,7 @@ static func int_max(a: int, b: int) -> int: return a else: return b + + +static func xor(bool_1, bool_2): + return (bool_1 and not bool_2) or (not bool_1 and bool_2) diff --git a/gui/circular_button.gd b/gui/circular_button.gd index 43f88fdd..320dff59 100644 --- a/gui/circular_button.gd +++ b/gui/circular_button.gd @@ -143,6 +143,7 @@ func update(): static func get_vector_radial(angle, radius = 1.0): return Vector2(cos(angle), sin(angle)) * radius + func set_radius_out(new_radius): if new_radius < radius_in: return diff --git a/gui/polygonal_button.gd b/gui/polygonal_button.gd new file mode 100644 index 00000000..740eab6e --- /dev/null +++ b/gui/polygonal_button.gd @@ -0,0 +1,150 @@ +tool +class_name PolygonalButton, "res://resources/editor/polygonal_button.svg" +extends CustomShapeButton + +export(StyleBoxFlat) var base_style setget set_base_style +export(StyleBoxFlat) var hover_style setget set_hover_style +export(StyleBoxFlat) var selected_style setget set_selected_style + +export(Texture) var texture setget set_texture +export(Vector2) var texture_size setget set_texture_size + +var polygon: Polygon = null setget set_polygon + + +func _ready(): + ._ready() + if polygon != null: + polygon.connect("changed", self, "_on_changed") + if base_style != null and not base_style.is_connected("changed",self,"_on_changed"): + base_style.connect("changed",self,"_on_changed") + if selected_style != null and not selected_style.is_connected("changed",self,"_on_changed"): + selected_style.connect("changed",self,"_on_changed") + if hover_style != null and not hover_style.is_connected("changed",self,"_on_changed"): + hover_style.connect("changed",self,"_on_changed") + if texture != null and not texture.is_connected("changed",self,"_on_changed"): + texture.connect("changed",self,"_on_changed") + + +func _draw(): + # todo add theme + if rect_size.x == 0.0 or rect_size.y == 0.0: + return + # style setup + var colors_bg + var color_border + var border_width + var corner_detail = 8 + var shadow_color = Color(0.0, 0.0, 0.0, 0.0) + var shadow_size = 0 + var shadow_offseet = Vector2(0,0) + var anti_alaising = true + var draw_center = true + var style_active + if selected: + style_active = selected_style + elif _hover: + style_active = hover_style + else: + style_active = base_style + if style_active == null or base_style is StyleBoxEmpty: + colors_bg = Color(0.5,0.5,0.5) if (_hover or selected) else Color(0.7,0.7,0.7) + color_border = Color(0.0, 0.0, 0.0) + border_width = [1,1,1,1] + else: + colors_bg = style_active.bg_color + color_border = style_active.border_color + border_width = [style_active.border_width_left, style_active.border_width_top, style_active.border_width_right, style_active.border_width_bottom] + corner_detail = style_active.corner_detail + shadow_color = style_active.shadow_color + shadow_size = style_active.shadow_size + shadow_offseet = style_active.shadow_offset + anti_alaising = style_active.anti_aliasing + draw_center = style_active.draw_center + # draw + var center = rect_size / 2.0 + var points = polygon.points + # draw shadow (todo improve) + if shadow_size > 0: + var point_shadow = polygon.get_shadow_points(shadow_size, shadow_offseet) + draw_polygon(point_shadow, PoolColorArray([ shadow_color ]), PoolVector2Array(), null, null, anti_alaising) + # darw inner + if draw_center: + draw_polygon(points, PoolColorArray([ colors_bg ]), PoolVector2Array(), null, null, anti_alaising) + # draw border + for i in range(points.size()): + if border_width[i % 4] > 0: + # todo add way to override border_width to add more egdes + draw_polygon(polygon.get_border_of_poly(i ,border_width[i % 4]), PoolColorArray([color_border]), PoolVector2Array(), null, null, anti_alaising) + # draw texture + if texture != null: + _draw_texture() + # add foccus + # add text + + +func _draw_texture(): + var center = polygon.get_center_of_mass() + var rect_rexture = Rect2(center - texture_size / 2.0, texture_size) + draw_texture_rect(texture,rect_rexture,false) + + +func _on_changed(): + update() + + +func _is_inside(position): + return polygon.is_inside(position) + + +func set_polygon(new_polygon): + if new_polygon == null or not new_polygon.is_valid(): + return + if polygon != null: + polygon.disconnect("changed", self, "_on_changed") + polygon = new_polygon + polygon.connect("changed", self, "_on_changed") + _on_changed() + + +func set_base_style(style): + if base_style != null: + base_style.disconnect("changed",self,"_on_changed") + base_style = style + if base_style != null: + base_style.connect("changed",self,"_on_changed") + update() + + +func set_hover_style(style): + if hover_style != null: + hover_style.disconnect("changed",self,"_on_changed") + hover_style = style + if hover_style != null: + hover_style.connect("changed",self,"_on_changed") + update() + + +func set_texture(new_texture): + if texture != null: + texture.disconnect("changed",self,"_on_changed") + texture = new_texture + if texture != null: + texture.connect("changed",self,"_on_changed") + update() + + +func set_selected_style(style): + if selected_style != null: + selected_style.disconnect("changed",self,"_on_changed") + selected_style = style + if selected_style != null: + selected_style.connect("changed",self,"_on_changed") + update() + + +func set_texture_size(new_size): + if new_size.x < 0.0 or new_size.y < 0.0: + return + texture_size = new_size + update() diff --git a/project.godot b/project.godot index c990c737..b7a86c56 100644 --- a/project.godot +++ b/project.godot @@ -79,6 +79,11 @@ _global_script_classes=[ { "language": "GDScript", "path": "res://hud/system/buildings/contruction/construction_building_item.gd" }, { +"base": "Polygon", +"class": "ConvexPolygon2D", +"language": "GDScript", +"path": "res://global/geometry/convex_polygon_2d.gd" +}, { "base": "Sprite", "class": "CrownSprite", "language": "GDScript", @@ -209,6 +214,16 @@ _global_script_classes=[ { "language": "GDScript", "path": "res://matchmaking/player/player_info_faction_column.gd" }, { +"base": "Resource", +"class": "Polygon", +"language": "GDScript", +"path": "res://global/geometry/polygon.gd" +}, { +"base": "CustomShapeButton", +"class": "PolygonalButton", +"language": "GDScript", +"path": "res://gui/polygonal_button.gd" +}, { "base": "DictResource", "class": "RemoteDictResource", "language": "GDScript", @@ -309,6 +324,7 @@ _global_script_class_icons={ "CircularContainer": "res://resources/editor/circular_container.svg", "ConstanteRemoteResource": "", "ConstructionBuildingItem": "", +"ConvexPolygon2D": "", "CrownSprite": "res://resources/assets/2d/map/kalankar/crown_bottom.png", "CustomShapeButton": "res://resources/editor/custom_shape_button.svg", "DictResource": "", @@ -335,6 +351,8 @@ _global_script_class_icons={ "OptionKeyBinding": "", "Player": "", "PlayerInfoFactionColumn": "", +"Polygon": "", +"PolygonalButton": "res://resources/editor/polygonal_button.svg", "RemoteDictResource": "", "RoundedSelectablePanelLeft": "", "RoundedSelectablePanelRight": "", diff --git a/resources/editor/polygonal_button.svg b/resources/editor/polygonal_button.svg new file mode 100644 index 00000000..e5ecd0f4 --- /dev/null +++ b/resources/editor/polygonal_button.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/resources/editor/polygonal_button.svg.import b/resources/editor/polygonal_button.svg.import new file mode 100644 index 00000000..5ac9e5e9 --- /dev/null +++ b/resources/editor/polygonal_button.svg.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/polygonal_button.svg-2d658a9fda99f155f6d8b7d14df32b3b.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://resources/editor/polygonal_button.svg" +dest_files=[ "res://.import/polygonal_button.svg-2d658a9fda99f155f6d8b7d14df32b3b.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0