└── addons └── basic_shape_creation ├── basic_polygon2d ├── BasicPolygon2D.Enums.cs ├── basic_polygon2d.svg ├── basic_polygon2d.svg.import ├── BasicPolygon2D.cs └── basic_polygon2d.gd ├── basic_collision_polygon2d ├── basic_collision_polygon2d.svg ├── basic_collision_polygon2d.svg.import ├── BasicCollisionPolygon2D.cs └── basic_collision_polygon2d.gd ├── LICENSE.txt ├── plugin.cfg ├── gui_handlers ├── size_rotation_handler.gd ├── scale_size_handler.gd └── base_handler.gd ├── plugin.gd ├── BasicGeometry2D.cs └── basic_geometry2d.gd /addons/basic_shape_creation/basic_polygon2d/BasicPolygon2D.Enums.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BasicShapeCreation; 4 | 5 | public enum ClosingMethod 6 | { 7 | Slice = 0, Chord, Arc 8 | } 9 | 10 | [Flags] 11 | public enum ExportBehavior 12 | { 13 | Disabled = 0b_00, Editor= 0b_01, Runtime=0b_10 14 | } 15 | 16 | public enum ShapeType 17 | { 18 | Polygon = 0, Polyline, Multiline 19 | } 20 | -------------------------------------------------------------------------------- /addons/basic_shape_creation/basic_collision_polygon2d/basic_collision_polygon2d.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /addons/basic_shape_creation/basic_polygon2d/basic_polygon2d.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /addons/basic_shape_creation/basic_polygon2d/basic_polygon2d.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://p02py6a2e6d" 6 | path="res://.godot/imported/basic_polygon2d.svg-b6792c11045b9532db9bc4c159f67a6e.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/basic_shape_creation/basic_polygon2d/basic_polygon2d.svg" 14 | dest_files=["res://.godot/imported/basic_polygon2d.svg-b6792c11045b9532db9bc4c159f67a6e.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/basic_shape_creation/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Deon Liu (GitHub username: 9thAzure) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /addons/basic_shape_creation/basic_collision_polygon2d/basic_collision_polygon2d.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://y2xtpyb5bwji" 6 | path="res://.godot/imported/basic_collision_polygon2d.svg-de313fd862ba755cc959ae62969c268c.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/basic_shape_creation/basic_collision_polygon2d/basic_collision_polygon2d.svg" 14 | dest_files=["res://.godot/imported/basic_collision_polygon2d.svg-de313fd862ba755cc959ae62969c268c.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/basic_shape_creation/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="Basic Shape Creation" 4 | description="An addon for the Godot Engine which adds a few basic functions for creating and modifying shapes, 5 | and a few nodes that use those functions to create shapes and use them. 6 | 7 | These functions and nodes are written in GDScript to make them universally compatible. 8 | They are exposed to C# via wrapper classes in the `BasicShapeCreation` namespace. 9 | 10 | --- Nodes --- 11 | 12 | Currently, there are 2 nodes offered: 13 | - BasicPolygon2D - General purpose node for creating, drawing, and exporting shapes 14 | - BasicCollisionPolygon2D - A specialization of BasicPolygon2D that provides collision shapes to a parent, similar to CollisionPolygon2D. 15 | 16 | --- Functions --- 17 | 18 | Functions are provided under the BasicGeometry2D singleton. The provided functions include: 19 | 20 | - add_shape - Creates a shape and inserts it into the provided array at the provided index. 21 | - add_ring - Takes a shape and duplicates its points to be some proportional amount closer to the shape center. 22 | - add_rounded_corners - Takes a shape and rounds the corners. 23 | " 24 | author="Deon Liu" 25 | version="3.0.0" 26 | script="plugin.gd" 27 | -------------------------------------------------------------------------------- /addons/basic_shape_creation/gui_handlers/size_rotation_handler.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends "res://addons/basic_shape_creation/gui_handlers/base_handler.gd" 3 | 4 | var _size_index := -1 5 | var _old_size : PackedFloat64Array 6 | var _old_rotation : float 7 | 8 | func _init(plugin : EditorPlugin, undo_redo_manager : EditorUndoRedoManager, size_index : int = 0, handler_size := 9.0) -> void: 9 | super(plugin, undo_redo_manager, handler_size) 10 | _size_index = size_index 11 | 12 | func _from_parent_properties() -> void: 13 | var offset_rotation : float = shape().offset_rotation + get_rotation_offset() 14 | 15 | position = Vector2(sin(offset_rotation), -cos(offset_rotation)) * shape().sizes[_size_index] 16 | 17 | func _update_properties() -> void: 18 | shape().sizes[_size_index] = position.length() 19 | shape().offset_rotation = fmod(atan2(position.y, position.x) + PI / 2 - get_rotation_offset() + TAU, TAU) 20 | 21 | func _mouse_pressed() -> void: 22 | _old_size = shape().sizes.duplicate() 23 | _old_rotation = shape().offset_rotation 24 | 25 | func _mouse_released() -> void: 26 | _undo_redo_manager.create_action("Resizing and Rotating Shape") 27 | 28 | _undo_redo_manager.add_do_property(shape(), &"sizes", shape().sizes) 29 | _undo_redo_manager.add_do_property(shape(), &"offset_rotation", shape().offset_rotation) 30 | 31 | _undo_redo_manager.add_undo_property(shape(), &"sizes", _old_size) 32 | _undo_redo_manager.add_undo_property(shape(), &"offset_rotation", _old_rotation) 33 | 34 | _undo_redo_manager.commit_action(false) 35 | shape().notify_property_list_changed() 36 | 37 | func get_rotation_offset() -> float: 38 | var vertices_count : int = shape().vertices_count 39 | if vertices_count == 1: 40 | vertices_count = 32 41 | elif vertices_count == 2: 42 | vertices_count = maxi(2, shape().sizes.size()) 43 | return _size_index * TAU / vertices_count 44 | -------------------------------------------------------------------------------- /addons/basic_shape_creation/gui_handlers/scale_size_handler.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends "res://addons/basic_shape_creation/gui_handlers/base_handler.gd" 3 | 4 | var _old_sizes : PackedFloat64Array = [] 5 | var _old_max_size := -1.0 6 | var _old_rotation := 0.0 7 | var _was_flipped := false 8 | 9 | func _init(plugin : EditorPlugin, undo_redo_manager : EditorUndoRedoManager, handler_size := 9.0) -> void: 10 | super(plugin, undo_redo_manager, handler_size) 11 | _shift_clamps = [clamp_straight_line] 12 | always_clamp = true 13 | _old_rotation = shape().offset_rotation 14 | 15 | func get_max_size() -> float: 16 | var max_size := -0.0 17 | for size in shape().sizes: max_size = maxf(max_size, size) 18 | if shape().ring_ratio < 0: 19 | max_size = lerpf(max_size, 0, shape().ring_ratio) 20 | return max_size 21 | 22 | func _from_parent_properties() -> void: 23 | position = Vector2(1, -1) * get_max_size() 24 | 25 | func _update_properties() -> void: 26 | var new_size := clamp_straight_line().x 27 | shape().offset_rotation = _old_rotation + (0 if new_size >= 0 != _was_flipped else (-PI if _was_flipped else PI)) 28 | var scale := absf(new_size / _old_max_size) 29 | for i in _old_sizes.size(): 30 | shape().sizes[i] = _old_sizes[i] * scale 31 | 32 | func _mouse_pressed() -> void: 33 | _old_sizes = shape().sizes.duplicate() 34 | _old_max_size = get_max_size() 35 | _old_rotation = shape().offset_rotation 36 | _was_flipped = position.x < 0 37 | 38 | func _mouse_released() -> void: 39 | _undo_redo_manager.create_action("Scale sizes") 40 | 41 | _undo_redo_manager.add_do_property(shape(), &"sizes", shape().sizes) 42 | _undo_redo_manager.add_undo_property(shape(), &"sizes", _old_sizes) 43 | 44 | if not is_equal_approx(_old_rotation, shape().offset_rotation): 45 | _undo_redo_manager.add_do_method(shape(), &"offset_rotation", shape().offset_rotation) 46 | _undo_redo_manager.add_undo_property(shape(), &"offset_rotation", _old_rotation) 47 | 48 | _undo_redo_manager.commit_action(false) 49 | shape().notify_property_list_changed() 50 | -------------------------------------------------------------------------------- /addons/basic_shape_creation/gui_handlers/base_handler.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends RefCounted 3 | 4 | const Plugin := preload("res://addons/basic_shape_creation/plugin.gd") 5 | 6 | var always_clamp := false 7 | var _shift_clamps : Array[Callable] = [clamp_straight_line, clamp_circle_radius, clamp_compass_lines] 8 | 9 | var _plugin : Plugin 10 | var _undo_redo_manager : EditorUndoRedoManager 11 | var size := 1.0 12 | 13 | func shape() -> Node2D: 14 | return _plugin._current_object 15 | 16 | var _old_position := Vector2.ZERO 17 | 18 | var position := Vector2.ZERO: 19 | set(value): 20 | position = value 21 | _plugin.update_overlays() 22 | 23 | func get_global_transform() -> Transform2D: return shape().get_viewport_transform() * shape().global_transform * shape().offset_transform.rotated_local(-shape().offset_transform.get_rotation()) 24 | 25 | func to_local(point : Vector2) -> Vector2: 26 | var transform := get_global_transform() 27 | point -= transform.origin 28 | 29 | return transform.affine_inverse().basis_xform(point) 30 | 31 | func to_global(point : Vector2) -> Vector2: 32 | var transform := get_global_transform() 33 | return transform.basis_xform(point) + transform.origin 34 | 35 | func _init(plugin : Plugin, undo_redo_manager : EditorUndoRedoManager, handler_size := 9.0) -> void: 36 | _plugin = plugin 37 | _undo_redo_manager = undo_redo_manager 38 | size = handler_size 39 | 40 | func mouse_press(point : Vector2) -> bool: 41 | const extra_margin := 2.0 42 | if (point - to_global(position)).length_squared() <= (size + extra_margin) ** 2: 43 | _old_position = position 44 | if _old_position == Vector2.ZERO: 45 | _old_position = Vector2.RIGHT 46 | _mouse_pressed() 47 | return true 48 | return false 49 | 50 | var suppress_from_parent_call := false 51 | func mouse_release() -> bool: 52 | if _plugin._pressed_handler == self: 53 | suppress_from_parent_call = true 54 | _mouse_released() 55 | return true 56 | return false 57 | 58 | func mouse_dragged(mouse_position: Vector2) -> void: 59 | position = to_local(mouse_position) 60 | if always_clamp or Input.is_key_pressed(KEY_SHIFT): 61 | _clamp_position() 62 | _mouse_dragged(mouse_position) 63 | _update_properties() 64 | 65 | func version_change() -> void: 66 | maintain_position() 67 | 68 | func _from_parent_properties() -> void: 69 | printerr("'_from_parent_properties' is abstract") 70 | 71 | func _update_properties() -> void: 72 | printerr("'_update_properties' is abstract") 73 | 74 | func _mouse_pressed() -> void: 75 | printerr("'_mouse_pressed' is abstract") 76 | 77 | func _mouse_released() -> void: 78 | printerr("'_mouse_released' is abstract") 79 | 80 | func _mouse_dragged(position: Vector2) -> void: 81 | pass 82 | 83 | func maintain_position() -> void: 84 | if suppress_from_parent_call: 85 | suppress_from_parent_call = false 86 | return 87 | 88 | _from_parent_properties() 89 | 90 | func _clamp_position() -> void: 91 | if _shift_clamps.size() == 0: 92 | return 93 | 94 | var best_position := position 95 | var best_distance := INF 96 | for i in _shift_clamps.size(): 97 | var point = _shift_clamps[i].call() 98 | if typeof(point) != TYPE_VECTOR2: 99 | printerr("method %s did not returned a %s, not a vector2." % [_shift_clamps[i], typeof(point)]) 100 | continue 101 | 102 | var distance : float = (point - position).length_squared() 103 | if distance < best_distance: 104 | best_distance = distance 105 | best_position = point 106 | 107 | position = best_position 108 | 109 | func clamp_straight_line() -> Vector2: 110 | var allowed_line := _old_position 111 | var inverse_line := Vector2(-allowed_line.y, allowed_line.x) 112 | var a := BasicGeometry2D._find_intersection(position, inverse_line, Vector2.ZERO, allowed_line) 113 | return position + inverse_line * a 114 | 115 | func clamp_circle_radius() -> Vector2: 116 | var radius := _old_position.length() 117 | return position.normalized() * radius 118 | 119 | func clamp_compass_lines() -> Vector2: 120 | var functional_position := position 121 | var angle := atan2(functional_position.y, functional_position.x) 122 | var multiplier := floor((angle + TAU / 16) / (TAU / 8)) 123 | 124 | angle = multiplier * TAU / 8 125 | var slope := Vector2(cos(angle), sin(angle)) 126 | return Geometry2D.get_closest_point_to_segment_uncapped(position, Vector2.ZERO, slope) 127 | -------------------------------------------------------------------------------- /addons/basic_shape_creation/plugin.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | const BaseHandler := preload("res://addons/basic_shape_creation/gui_handlers/base_handler.gd") 5 | const SizeRotationHandler := preload("res://addons/basic_shape_creation/gui_handlers/size_rotation_handler.gd") 6 | const ScaleSizeHandler := preload("res://addons/basic_shape_creation/gui_handlers/scale_size_handler.gd") 7 | 8 | var _current_object : Node2D = null 9 | var _handlers : Array[BaseHandler] = [] 10 | var _pressed_handler : BaseHandler = null 11 | var _size_handler_count := 0 12 | var _has_sent_not_found_button_warning := false 13 | 14 | func _enable_plugin() -> void: 15 | var undoredo := get_undo_redo() 16 | undoredo.history_changed.connect(_on_version_change) 17 | undoredo.version_changed.connect(_on_version_change) 18 | 19 | func _disable_plugin() -> void: 20 | remove_handlers() 21 | var undoredo := get_undo_redo() 22 | if undoredo.version_changed.is_connected(_on_version_change): 23 | undoredo.history_changed.disconnect(_on_version_change) 24 | undoredo.version_changed.disconnect(_on_version_change) 25 | 26 | func _on_version_change() -> void: 27 | if _current_object == null: 28 | return 29 | 30 | if not _is_handled_node(_current_object): 31 | EditorInterface.edit_node(_current_object) 32 | return 33 | 34 | var _new_size_handler_count = _current_object.sizes.size() 35 | if _new_size_handler_count != _size_handler_count: 36 | _size_handler_count = _new_size_handler_count 37 | remove_handlers() 38 | create_handlers() 39 | 40 | for handler in _handlers: 41 | handler.version_change() 42 | update_overlays() 43 | 44 | func _handles(object : Object) -> bool: 45 | return _is_handled_node(object) 46 | 47 | func _is_handled_node(object : Object) -> bool: 48 | return ( 49 | object is BasicPolygon2D or 50 | object is BasicCollisionPolygon2D 51 | ) and object.get_class() != "EditorDebuggerRemoteObject" 52 | 53 | func _edit(object : Object) -> void: 54 | update_overlays() 55 | if object == null: 56 | remove_handlers() 57 | _current_object = null 58 | return 59 | 60 | if not is_same(object, _current_object): 61 | remove_handlers() 62 | _current_object = object 63 | create_handlers() 64 | 65 | # just in case it get disconnected somehow, typically due to file edit while plugin is active. 66 | if not get_undo_redo().version_changed.is_connected(_on_version_change): 67 | get_undo_redo().version_changed.connect(_on_version_change) 68 | get_undo_redo().history_changed.connect(_on_version_change) 69 | 70 | func create_handlers() -> void: 71 | assert(_current_object != null) 72 | 73 | _size_handler_count = _current_object.sizes.size() 74 | for i in _size_handler_count: 75 | _handlers.append(SizeRotationHandler.new(self, get_undo_redo(), i)) 76 | if _size_handler_count > 1: 77 | _handlers.append(ScaleSizeHandler.new(self, get_undo_redo())) 78 | 79 | for handler in _handlers: 80 | handler.maintain_position() 81 | 82 | func remove_handlers() -> void: 83 | _handlers.clear() 84 | 85 | func _forward_canvas_gui_input(event) -> bool: 86 | if event is InputEventMouseButton: 87 | if event.button_index != MOUSE_BUTTON_MASK_LEFT: 88 | return false 89 | 90 | if event.pressed: 91 | if not _select_mode_button_selected(): 92 | return false 93 | 94 | for handler in _handlers: 95 | var intercepts := handler.mouse_press(event.position) 96 | if intercepts: 97 | _pressed_handler = handler 98 | update_overlays() 99 | return true 100 | return false 101 | else: 102 | if _pressed_handler == null: 103 | return false 104 | var result := _pressed_handler.mouse_release() 105 | _pressed_handler = null 106 | update_overlays() 107 | return result 108 | 109 | elif event is InputEventMouseMotion: 110 | if _pressed_handler == null: 111 | return false 112 | 113 | _pressed_handler.mouse_dragged(event.position) 114 | for handler in _handlers: 115 | if handler != _pressed_handler: 116 | handler.maintain_position() 117 | update_overlays() 118 | 119 | 120 | return false 121 | 122 | func to_canvas(points : PackedVector2Array) -> PackedVector2Array: 123 | points = points.duplicate() 124 | var transform := _current_object.get_viewport_transform() * _current_object.get_global_transform() 125 | for i in points.size(): 126 | points[i] = transform.basis_xform(points[i]) + transform.origin 127 | return points 128 | 129 | func _forward_canvas_draw_over_viewport(viewport_control: Control) -> void: 130 | var instance : BasicPolygon2D = _current_object if _current_object is BasicPolygon2D else _current_object._basic_polygon_instance 131 | if instance._queue_status == BasicPolygon2D._QUEUE_REGENERATE: 132 | instance.regenerate() 133 | 134 | if _current_object.get_created_shape().size() > 0 and _current_object.get_created_shape_type() == BasicPolygon2D.ShapeType.POLYGON: 135 | var outline_color := Color(0.925, 0.38, 0.216) 136 | var line_width := 3.5 137 | match _current_object.get_created_shape_type(): 138 | BasicPolygon2D.ShapeType.POLYGON: 139 | var shape : PackedVector2Array = to_canvas(_current_object.get_created_shape()) 140 | viewport_control.draw_polyline(shape, outline_color, line_width) 141 | viewport_control.draw_line(shape[-1], shape[0], outline_color, line_width) 142 | 143 | BasicPolygon2D.ShapeType.POLYLINE: 144 | viewport_control.draw_polyline(to_canvas(_current_object.get_created_shape()), outline_color, line_width) 145 | 146 | BasicPolygon2D.ShapeType.MULTILINE: 147 | viewport_control.draw_multiline(to_canvas(_current_object.get_created_shape()), outline_color, line_width) 148 | 149 | for handler in _handlers: 150 | const margin := 1.0 151 | 152 | var shape := BasicGeometry2D.create_shape(5, [handler.size], Transform2D(0, handler.to_global(handler.position))) 153 | var color := Color.LIME_GREEN if _pressed_handler == handler else Color.WHITE 154 | viewport_control.draw_colored_polygon(shape, color) 155 | viewport_control.draw_polyline(shape, Color.BLACK, margin, true) 156 | viewport_control.draw_line(shape[-1], shape[0], Color.BLACK, margin, true) 157 | 158 | var _select_mode_button : Button = null 159 | func _select_mode_button_selected() -> bool: 160 | if _is_select_mode_button_invalid(_select_mode_button): 161 | _get_select_mode_button() 162 | if _is_select_mode_button_invalid(_select_mode_button): 163 | return true 164 | 165 | return _select_mode_button.button_pressed 166 | 167 | func _is_select_mode_button_invalid(button : Button) -> bool: 168 | return button == null or not is_instance_valid(button) or not button.toggle_mode or button.icon == null 169 | 170 | func _get_select_mode_button() -> void: 171 | var main_screen := EditorInterface.get_editor_main_screen() 172 | 173 | var found_node : Node = main_screen.get_node_or_null("@CanvasItemEditor@9465/@MarginContainer@9280/@HFlowContainer@9281/@HBoxContainer@9282/@Button@9329") 174 | if found_node != null and found_node is Button: 175 | _select_mode_button = found_node 176 | return 177 | 178 | found_node = main_screen 179 | for i in 5: 180 | if found_node == null: 181 | break 182 | found_node = found_node.get_child(0) 183 | 184 | if found_node != null and found_node is Button: 185 | _select_mode_button = found_node 186 | return 187 | 188 | if not _has_sent_not_found_button_warning: 189 | push_warning("(Simplified Shape Creation plugin) - Unable to find the select mode button. Handlers for the nodes provided by this plugin will always be selectable, even if other modes are selected") 190 | _has_sent_not_found_button_warning = true 191 | -------------------------------------------------------------------------------- /addons/basic_shape_creation/BasicGeometry2D.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Diagnostics.CodeAnalysis; 4 | using System.Threading; 5 | using Godot; 6 | 7 | namespace BasicShapeCreation; 8 | 9 | /// Holds methods for creating and modifying shapes. 10 | /// 11 | /// In order to interop with the gdscript equivalent methods, an instance of the has to be created, 12 | /// as well as be manually freed at the end of application lifetime via . 13 | ///

If the is implemented as a , will 14 | /// automatically be called when the root node is exiting the tree. This behaviour can be disabled 15 | /// with . 16 | ///
17 | public static class BasicGeometry2D 18 | { 19 | private static readonly ScriptLoader _loader = new(); 20 | private class ScriptLoader : Lazy, IDisposable 21 | { 22 | public ScriptLoader() : base(Factory, LazyThreadSafetyMode.ExecutionAndPublication) 23 | { 24 | } 25 | 26 | private static GodotObject Factory() 27 | { 28 | if (Engine.GetMainLoop() is SceneTree tree) 29 | { 30 | tree.Root.TreeExiting += BasicGeometry2D.Dispose; 31 | } 32 | 33 | var gdScript = GD.Load("res://addons/basic_shape_creation/basic_geometry2d.gd"); 34 | return gdScript.New().AsGodotObject(); 35 | } 36 | 37 | ~ScriptLoader() 38 | { 39 | Dispose(); 40 | } 41 | 42 | [SuppressMessage("ReSharper", "MemberHidesStaticFromOuterClass")] 43 | public void Dispose() 44 | { 45 | if (BasicGeometry2D.FreeOnWindowExit && GodotObject.IsInstanceValid(Value)) 46 | { 47 | Value.Free(); 48 | } 49 | } 50 | } 51 | 52 | /// 53 | /// Toggles whether to automatically free the gdscript when it is detected that the root 54 | /// is exiting the . 55 | /// 56 | /// 57 | /// Set this property to if you need to provide your own system to free the instance and prevent 58 | /// the auto freeing. Note that the auto freeing does not occur if the isn't a anyways, 59 | /// so setting this property to is not necessary. 60 | /// 61 | public static bool FreeOnWindowExit { get; set; } = true; 62 | 63 | /// 64 | /// Gets the instance used by this class to access the gdscript methods. 65 | /// 66 | public static GodotObject Instance => _loader.Value; 67 | 68 | /// 69 | /// s the gdscript instance this class interops with to call 70 | /// the other methods, if it hasn't been already. 71 | /// 72 | public static void Dispose() 73 | { 74 | _loader.Dispose(); 75 | } 76 | 77 | /// 78 | public class MethodName : GodotObject.MethodName 79 | { 80 | public static readonly StringName CreateShape = new("create_shape"); 81 | public static readonly StringName AddShape = new("add_shape"); 82 | public static readonly StringName AddRing = new("add_ring"); 83 | public static readonly StringName AddRoundedCorners = new("add_rounded_corners"); 84 | } 85 | 86 | /// 87 | public class PropertyName : GodotObject.PropertyName {} 88 | 89 | /// 90 | public class Signalname : GodotObject.SignalName {} 91 | 92 | /// 93 | /// Creates and returns a describing the shape specified by the parameters. 94 | /// 95 | /// The number of points on the base shape. If it is a value of 1, a value of 32 is used instead. 96 | /// Determines the length of each point from the center of the base shape, being repeatedly iterated through to get the length for each corner. 97 | /// The offset transform of the created shape. 98 | /// The starting angle of the arc out of the base shape that is cut out and returned, in radians. 99 | /// The ending angle of the arc out of the base shape that is cut out and returned, in radians. 100 | /// If true, adds a center point to the shape. It is automatically false if the arc of the shape is a complete circle. 101 | /// A describing the shape specified by the parameters. 102 | public static Vector2[] CreateShape(int verticesCount, double[] sizes, Transform2D? offsetTransform = null, 103 | double arcStart = 0d, double arcEnd = Math.Tau, bool addCentralPoint = true) 104 | { 105 | Debug.Assert(GodotObject.IsInstanceValid(_loader.Value)); 106 | Debug.Assert(_loader.Value.HasMethod(MethodName.CreateShape)); 107 | return _loader.Value.Call(MethodName.CreateShape, verticesCount, sizes, offsetTransform ?? Transform2D.Identity, 108 | arcStart, arcEnd, addCentralPoint).AsVector2Array(); 109 | } 110 | 111 | /// 112 | /// 113 | /// Creates and inserts the shape specified by the parameters into a copy of at index. 114 | /// 115 | /// The initial array to clone and insert a shape into. 116 | /// The index to insert the shape at. 117 | public static Vector2[] AddShape(Vector2[] points, int start, int verticesCount, double[] sizes, Transform2D? offsetTransform = null, 118 | double arcStart = 0d, double arcEnd = Math.Tau, bool addCentralPoint = true) 119 | { 120 | Debug.Assert(GodotObject.IsInstanceValid(_loader.Value)); 121 | Debug.Assert(_loader.Value.HasMethod(MethodName.AddShape)); 122 | return _loader.Value.Call(MethodName.AddShape, points, start, verticesCount, sizes, offsetTransform ?? Transform2D.Identity, 123 | arcStart, arcEnd, addCentralPoint).AsVector2Array(); 124 | } 125 | 126 | /// 127 | /// Returns a modified copy of , adding a duplicate ring of points. 128 | /// 129 | /// The base shape. 130 | /// The proportion of the distance of the original points which the new points are placed at, relative to the . 131 | /// The center of the shape. 132 | /// If true, the first point is also appended to the end before adding the ring. 133 | /// A , representing the shape of with an added ring. 134 | public static Vector2[] AddRing(Vector2[] shape, double lengthProportion, Vector2 shapeCenter = default, bool closeRing = true) 135 | { 136 | Debug.Assert(GodotObject.IsInstanceValid(_loader.Value)); 137 | Debug.Assert(_loader.Value.HasMethod(MethodName.AddRing)); 138 | return _loader.Value.Call(MethodName.AddRing, shape, lengthProportion, shapeCenter, closeRing).AsVector2Array(); 139 | } 140 | 141 | /// 142 | /// Returns a modified copy of with rounded corners. 143 | /// 144 | /// The method uses quadratic Bézier curves to place the points on the rounded corner. 145 | /// The base shape. 146 | /// The distance along the edge where the smoothed corner will start from. 147 | /// How many lines are in each corner. 148 | /// The initial point to round. 149 | /// The number of points to round. 150 | /// Whether the first and last corner should be limited to half the side distance or not. No effect if the entire shape is being rounded. 151 | /// A , representing the shape of with rounded corners. 152 | public static Vector2[] AddRoundedCorners(Vector2[] shape, double cornerSize, long cornerDetail, long startIndex = 0, long length = -1, bool limitEndingSlopes = true) 153 | { 154 | Debug.Assert(GodotObject.IsInstanceValid(_loader.Value)); 155 | Debug.Assert(_loader.Value.HasMethod(MethodName.AddRoundedCorners)); 156 | return _loader.Value.Call(MethodName.AddRoundedCorners, shape, cornerSize, cornerDetail, startIndex, length, limitEndingSlopes).AsVector2Array(); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /addons/basic_shape_creation/basic_geometry2d.gd: -------------------------------------------------------------------------------- 1 | extends Object 2 | class_name BasicGeometry2D 3 | 4 | ## Holds methods for creating and modifying shapes. 5 | 6 | 7 | 8 | # gets the point on a unit circle for the specified rotation. 9 | static func _circle_point(rotation : float) -> Vector2: 10 | return Vector2(sin(rotation), -cos(rotation)) 11 | 12 | # finds the intersection between 2 points and their slopes. The value returned is not the point itself, but a scaler 13 | # where the point of intersection is [c] point1 + return_value * slope1 [/c] 14 | static func _find_intersection(point1 : Vector2, slope1 : Vector2, point2: Vector2, slope2: Vector2) -> float: 15 | var numerator := slope2.y * (point2.x - point1.x) - slope2.x * (point2.y - point1.y) 16 | var devisor := (slope1.x * slope2.y) - (slope1.y * slope2.x) 17 | assert(devisor != 0, "one or both slopes are 0, or are parallel") 18 | return numerator / devisor 19 | 20 | ## Creates and returns a [PackedVector2Array] describing the shape specified by the parameters. 21 | ## [br][br] 22 | ## [param vertices_count] determines the number of points on the base shape. If a value of [code]1[/code] is used, 23 | ## A value of [code]32[/code] is used instead. 24 | ## [param sizes] determines the length of each point from the center of the base shape, being repeatedly iterated through 25 | ## to get the length for each vertex. 26 | ## [param offset_transform] is the transform applied after creation. 27 | ## [param arc_start] and [param arc_end] determine the arc out of that base shape that is cut out and returned, in radians. 28 | ## [param add_central_point] determines whether a central point is added to the shape. It is automatically set to [code]false[/code] 29 | ## if the arc of the shape is a complete circle. 30 | static func create_shape(vertices_count: int, sizes: PackedFloat64Array, offset_transform = Transform2D.IDENTITY, 31 | arc_start := 0.0, arc_end := TAU, add_central_point := true) -> PackedVector2Array: 32 | return add_shape([], 0, vertices_count, sizes, offset_transform, arc_start, arc_end, add_central_point) 33 | 34 | ## Creates and inserts the shape specified by the parameters into [param points] at [param start] index. 35 | ## [br][br] 36 | ## [param vertices_count] determines the number of points on the base shape. If a value of [code]1[/code] is used, 37 | ## A value of [code]32[/code] is used instead. 38 | ## [param sizes] determines the length of each point from the center of the base shape, being repeatedly iterated through 39 | ## to get the length for each vertex. 40 | ## [param offset_transform] is the transform applied after creation. 41 | ## [param arc_start] and [param arc_end] determine the arc out of that base shape that is cut out and returned, in radians. 42 | ## [param add_central_point] determines whether a central point is added to the shape. It is automatically set to [code]false[/code] 43 | ## if the arc of the shape is a complete circle. 44 | static func add_shape(points : PackedVector2Array, start : int, vertices_count: int, sizes: PackedFloat64Array, offset_transform = Transform2D.IDENTITY, 45 | arc_start := 0.0, arc_end := TAU, add_central_point := true) -> PackedVector2Array: 46 | assert(vertices_count >= 1, "param 'vertices_count' must be 1 or greater.") 47 | assert(sizes.size() != 0, "param 'sizes' must have at least one element") 48 | assert(arc_end > arc_start, "param 'arc_end' must be larger than 'arc_start'") 49 | 50 | if vertices_count == 1: 51 | vertices_count = 32 52 | 53 | var arc_angle := TAU / vertices_count 54 | 55 | var is_full_arc := false 56 | # checks if it is approximately a multiple of TAU. 57 | if is_zero_approx(sin((arc_end - arc_start) * PI / TAU)): 58 | is_full_arc = true 59 | add_central_point = false 60 | 61 | var starting_vertex_index : int = floorf(arc_start / arc_angle) 62 | var ending_vertex_index : int = ceilf(arc_end / arc_angle) 63 | var true_vertices_count := ending_vertex_index - starting_vertex_index + (1 if not is_full_arc else 0) 64 | var size_increase := true_vertices_count + (1 if add_central_point else 0) 65 | var original_size := points.size() 66 | points.resize(points.size() + size_increase) 67 | for i in original_size - start: 68 | var index := original_size - i - 1 69 | points[index + size_increase] = points[index] 70 | 71 | for i in true_vertices_count: 72 | var index := i + starting_vertex_index 73 | points[start + i] = _circle_point(index * arc_angle) * sizes[index % sizes.size()] 74 | 75 | if not is_equal_approx(starting_vertex_index, arc_start / arc_angle): 76 | var slope1 := _circle_point(arc_start) 77 | var scaler := _find_intersection(Vector2.ZERO, slope1, points[start], points[start + 1] - points[start]) 78 | points[start] = slope1 * scaler 79 | 80 | if not is_equal_approx(ending_vertex_index, arc_end / arc_angle) and not is_full_arc: 81 | var last_i := start + true_vertices_count - 1 82 | var slope1 := _circle_point(arc_end) 83 | var scaler := _find_intersection(Vector2.ZERO, slope1, points[last_i], points[last_i - 1] - points[last_i]) 84 | points[last_i] = slope1 * scaler 85 | 86 | if add_central_point: 87 | points[start + size_increase - 1] = Vector2.ZERO 88 | 89 | for i in points.size(): 90 | points[i] = offset_transform.basis_xform(points[i]) + offset_transform.get_origin() 91 | 92 | return points 93 | 94 | ## Modifies and returns [param shape], adding a duplicate ring of points 95 | ## at a distance that is [param length_proportion] percent of the original point's distance to [param shape_center]. 96 | ## [br][br] 97 | ## if [param close_ring] is true, the first point is also appended to the end before adding the ring. 98 | static func add_ring(shape: PackedVector2Array, length_proportion: float, shape_center := Vector2.ZERO, close_ring := true) -> PackedVector2Array: 99 | var original_size := shape.size() + (1 if close_ring else 0) 100 | 101 | shape.resize(original_size * 2) 102 | if close_ring: 103 | shape[original_size - 1] = shape[0] 104 | 105 | for i in original_size: 106 | shape[-i - 1] = shape[i].lerp(shape_center, length_proportion) 107 | 108 | return shape 109 | 110 | ## Modifies [param points] so that the shape it represents has rounded corners. The method uses quadratic Bézier curves for the corners. 111 | ## [br][br][param corner_size] determines how long each corner is, from the original point to at most half the side length. 112 | ## [param corner_detail] determines how many [b]lines[/b] are in each corner. 113 | ## [br][br][param start_index] & [param length] can be used to specify only part of the shape should be rounded. 114 | ## [param limit_ending_slopes] determines whether the first and last corner should be limited to half the side distance or not. No effect if the entire shape is being rounded. 115 | ## [param original_array_size], when used, indicates that the array has already been resized, so the method should add points into the empty space. 116 | ## This parameter specifies the part of the array that is currently used. 117 | static func add_rounded_corners(points : PackedVector2Array, corner_size : float, corner_detail : int, 118 | start_index := 0, length := -1, limit_ending_slopes := true, original_array_size := 0) -> PackedVector2Array: 119 | # argument prep 120 | var corner_size_squared := corner_size ** 2 121 | var resize_array := false 122 | if original_array_size <= 0: 123 | resize_array = true 124 | original_array_size = points.size() 125 | if length < 0: 126 | length = original_array_size - start_index 127 | if corner_detail == 0: 128 | corner_detail = 32 / points.size() 129 | var points_per_corner := corner_detail + 1 130 | 131 | if not limit_ending_slopes and length == original_array_size: 132 | limit_ending_slopes = true 133 | 134 | assert(points.size() >= 3, "param 'points' must have at least 3 points.") 135 | assert(corner_size >= 0, "param 'corner_size' must be 0 or greater.") 136 | assert(corner_detail >= 0, "param 'corner_detail' must be 0 or greater.") 137 | assert(start_index >= 0, "param 'start_index' must be 0 or greater.") 138 | assert(start_index + length <= original_array_size, "sum of param 'start_index' & param 'length' must not be greater than the original size of the array (param 'original_array_size', or if 0, size of param 'points').") 139 | assert(limit_ending_slopes || length != original_array_size, "param 'limit_ending_slopes' was set to false, but the entire shape is being rounded so there are no \"ending\" slopes.") 140 | 141 | # resizing and spacing 142 | var size_increase := length * (corner_detail + 1) - length 143 | if resize_array: 144 | points.resize(original_array_size + size_increase) 145 | for i in (original_array_size - start_index - length): 146 | points[-i - 1] = points[-i - 1 - size_increase] 147 | else: 148 | assert(original_array_size + size_increase <= points.size(), "The function is set to use the empty space in param 'points' but it is too small.") 149 | for i in (original_array_size - start_index - length): 150 | points[original_array_size - i - 1 + size_increase]= points[original_array_size - i - 1] 151 | 152 | for i in length: 153 | var index := length - i - 1 154 | points[start_index + index * points_per_corner] = points[index + start_index] 155 | 156 | # pre-loop prep and looping 157 | var current_point := points[start_index] 158 | var next_point : Vector2 159 | var point_after_final : Vector2 160 | var previous_point : Vector2 161 | if start_index == 0: 162 | if length == original_array_size: 163 | previous_point = points[original_array_size + size_increase - points_per_corner] 164 | else: 165 | previous_point = points[original_array_size + size_increase - 1] 166 | else: 167 | previous_point = points[start_index - 1] 168 | 169 | if start_index + length == original_array_size: 170 | point_after_final = points[0] 171 | else: 172 | point_after_final = points[start_index + length * points_per_corner - points.size()] 173 | 174 | for i in length: 175 | if i + 1 == length: 176 | next_point = point_after_final 177 | else: 178 | next_point = points[start_index + (i + 1) * points_per_corner] 179 | 180 | # creating corner 181 | var starting_slope := (current_point - previous_point) 182 | var ending_slope := (current_point - next_point) 183 | var starting_point : Vector2 184 | var ending_point : Vector2 185 | 186 | var slope_limit_value := 1 if not limit_ending_slopes and i == 0 else 2 187 | if starting_slope.length_squared() / (slope_limit_value * slope_limit_value) < corner_size_squared: 188 | starting_point = current_point - starting_slope / (slope_limit_value + 0.001) 189 | else: 190 | starting_point = current_point - starting_slope.normalized() * corner_size 191 | 192 | slope_limit_value = 1 if not limit_ending_slopes and i + 1 == length else 2 193 | if ending_slope.length_squared() / (slope_limit_value * slope_limit_value) < corner_size_squared: 194 | ending_point = current_point - ending_slope / (slope_limit_value + 0.001) 195 | else: 196 | ending_point = current_point - ending_slope.normalized() * corner_size 197 | 198 | points[start_index + i * points_per_corner] = starting_point 199 | points[start_index + i * points_per_corner + points_per_corner - 1] = ending_point 200 | # sub_i is initialized with a value of 1 as a corner_detail of 1 has no in-between points. 201 | var sub_i := 1 202 | while sub_i < corner_detail: 203 | var t_value := sub_i / (corner_detail as float) 204 | points[start_index + i * points_per_corner + sub_i] = _quadratic_bezier_interpolate(starting_point, current_point, ending_point, t_value) 205 | sub_i += 1 206 | 207 | # end, prep for next loop. 208 | previous_point = current_point 209 | current_point = next_point 210 | 211 | return points 212 | 213 | static func _quadratic_bezier_interpolate(start : Vector2, control : Vector2, end : Vector2, t : float) -> Vector2: 214 | return control + (t - 1) ** 2 * (start - control) + t ** 2 * (end - control) 215 | -------------------------------------------------------------------------------- /addons/basic_shape_creation/basic_collision_polygon2d/BasicCollisionPolygon2D.cs: -------------------------------------------------------------------------------- 1 | using Godot; 2 | using System; 3 | 4 | namespace BasicShapeCreation; 5 | 6 | /// 7 | /// A node that provides a shape generated by a to a parent and allows edits to it. 8 | /// The shape may be convex or concave, and may be series of lines instead, which may not be connected to each other. 9 | /// This can give a detection shape to an or turn a into a solid object. 10 | /// 11 | /// 12 | /// This class is a wrapper around an instance of a s with the at 13 | /// "res://addons/basic_shape_creation/basic_collision_polygon2d/basic_collision_polygon2d.gd" attached. 14 | /// The instance can be accessed with . 15 | /// 16 | public class BasicCollisionPolygon2D 17 | { 18 | /// The string path to the script this class wraps around. 19 | public const string GDScriptEquivalentPath = "res://addons/basic_shape_creation/basic_collision_polygon2d/basic_collision_polygon2d.gd"; 20 | /// The loaded of . 21 | public static readonly GDScript GDScriptEquivalent = GD.Load(GDScriptEquivalentPath); 22 | 23 | /// The instance this class wraps around. 24 | public Node2D Instance { get; } 25 | 26 | /// Toggles whether this shape is disabled and unable to detect collisions. 27 | public bool Disabled 28 | { 29 | get => Instance.Get(PropertyName.Disabled).AsBool(); 30 | set => Instance.Set(PropertyName.Disabled, value); 31 | } 32 | 33 | /// 34 | /// Toggles whether this shape only detects collisions with edges that face up, relative to 's rotation. 35 | /// 36 | /// This property has no effect if this is a child of an node. 37 | public bool OneWayCollision 38 | { 39 | get => Instance.Get(PropertyName.OneWayCollision).AsBool(); 40 | set => Instance.Set(PropertyName.OneWayCollision, value); 41 | } 42 | 43 | /// 44 | /// The margin used for one-way collision (in pixels). Higher values will make the shape thicker, 45 | /// and work better for colliders that enter the polygon at a high velocity. 46 | /// 47 | public int OneWayCollisionMargin 48 | { 49 | get => Instance.Get(PropertyName.OneWayCollisionMargin).AsInt32(); 50 | set => Instance.Set(PropertyName.OneWayCollisionMargin, value); 51 | } 52 | 53 | /// 54 | public int VerticesCount 55 | { 56 | get => (int)Instance.Get(PropertyName.VerticesCount); 57 | set => Instance.Set(PropertyName.VerticesCount, value); 58 | } 59 | 60 | /// 61 | public double[] Sizes 62 | { 63 | get => Instance.Get(PropertyName.Sizes).AsFloat64Array(); 64 | set => Instance.Set(PropertyName.Sizes, value); 65 | } 66 | 67 | /// 68 | public float RingRatio 69 | { 70 | get => Instance.Get(PropertyName.RingRatio).AsSingle(); 71 | set => Instance.Set(PropertyName.RingRatio, value); 72 | } 73 | 74 | /// 75 | public float CornerSize 76 | { 77 | get => Instance.Get(PropertyName.CornerSize).AsSingle(); 78 | set => Instance.Set(PropertyName.CornerSize, value); 79 | } 80 | 81 | /// 82 | public int CornerDetail 83 | { 84 | get => Instance.Get(PropertyName.CornerDetail).AsInt32(); 85 | set => Instance.Set(PropertyName.CornerDetail, value); 86 | } 87 | 88 | /// 89 | public float ArcStart 90 | { 91 | get => Instance.Get(PropertyName.ArcStart).AsSingle(); 92 | set => Instance.Set(PropertyName.ArcStart, value); 93 | } 94 | 95 | /// 96 | public float ArcAngle 97 | { 98 | get => Instance.Get(PropertyName.ArcAngle).AsSingle(); 99 | set => Instance.Set(PropertyName.ArcAngle, value); 100 | } 101 | 102 | /// 103 | public float ArcEnd 104 | { 105 | get => Instance.Get(PropertyName.ArcEnd).AsSingle(); 106 | set => Instance.Set(PropertyName.ArcEnd, value); 107 | } 108 | 109 | /// 110 | public float ArcStartDegrees 111 | { 112 | get => Instance.Get(PropertyName.ArcStartDegrees).AsSingle(); 113 | set => Instance.Set(PropertyName.ArcStartDegrees, value); 114 | } 115 | 116 | /// 117 | public float ArcAngleDegrees 118 | { 119 | get => Instance.Get(PropertyName.ArcAngleDegrees).AsSingle(); 120 | set => Instance.Set(PropertyName.ArcAngleDegrees, value); 121 | } 122 | 123 | /// 124 | public float ArcEndDegrees 125 | { 126 | get => Instance.Get(PropertyName.ArcEndDegrees).AsSingle(); 127 | set => Instance.Set(PropertyName.ArcEndDegrees, value); 128 | } 129 | 130 | /// 131 | public ClosingMethod ClosingMethod 132 | { 133 | get => Instance.Get(PropertyName.ClosingMethod).As(); 134 | set => Instance.Set(PropertyName.ClosingMethod, (int)value); 135 | } 136 | 137 | /// 138 | public bool RoundArcEnds 139 | { 140 | get => Instance.Get(PropertyName.RoundArcEnds).AsBool(); 141 | set => Instance.Set(PropertyName.RoundArcEnds, value); 142 | } 143 | 144 | /// 145 | public Vector2 OffsetPosition 146 | { 147 | get => Instance.Get(PropertyName.OffsetPosition).AsVector2(); 148 | set => Instance.Set(PropertyName.OffsetPosition, value); 149 | } 150 | 151 | /// 152 | public float OffsetRotationDegrees 153 | { 154 | get => Instance.Get(PropertyName.OffsetRotationDegrees).AsSingle(); 155 | set => Instance.Set(PropertyName.OffsetRotationDegrees, value); 156 | } 157 | 158 | /// 159 | public float OffsetRotation 160 | { 161 | get => Instance.Get(PropertyName.OffsetRotation).AsSingle(); 162 | set => Instance.Set(PropertyName.OffsetRotation, value); 163 | } 164 | 165 | /// 166 | public Vector2 OffsetScale 167 | { 168 | get => Instance.Get(PropertyName.OffsetScale).AsVector2(); 169 | set => Instance.Set(PropertyName.OffsetScale, value); 170 | } 171 | 172 | /// 173 | public float OffsetSkew 174 | { 175 | get => Instance.Get(PropertyName.OffsetSkew).AsSingle(); 176 | set => Instance.Set(PropertyName.OffsetSkew, value); 177 | } 178 | 179 | /// 180 | public Transform2D OffsetTransform 181 | { 182 | get => Instance.Get(PropertyName.OffsetTransform).AsTransform2D(); 183 | set => Instance.Set(PropertyName.OffsetTransform, value); 184 | } 185 | 186 | /// 187 | public Vector2 Position 188 | { 189 | get => Instance.Position; 190 | set => Instance.Position = value; 191 | } 192 | /// 193 | public float Rotation 194 | { 195 | get => Instance.Rotation; 196 | set => Instance.Rotation = value; 197 | } 198 | /// 199 | public float RotationDegrees 200 | { 201 | get => Instance.RotationDegrees; 202 | set => Instance.RotationDegrees = value; 203 | } 204 | /// 205 | public Vector2 Scale 206 | { 207 | get => Instance.Scale; 208 | set => Instance.Scale = value; 209 | } 210 | 211 | /// 212 | /// Gets the underlying wrapper around the instance that generates the 213 | /// shapes this provides. 214 | /// 215 | public BasicPolygon2D BasicPolygon { get; } 216 | 217 | /// 218 | public Vector2[] CreatedShape => Instance.Call(MethodName.GetCreatedShape).AsVector2Array(); 219 | 220 | /// 221 | public Godot.Collections.Array CreatedShapeDecomposed => Instance.Call(MethodName.GetCreatedShapeDecomposed).AsGodotArray(); 222 | 223 | /// 224 | /// Gets the type of shape created by this . 225 | public ShapeType CreatedShapeType => Instance.Call(MethodName.GetCreatedShapeType).As(); 226 | 227 | 228 | /// Gets the number of s that this is providing. 229 | public int ShapeCount => Instance.Call(MethodName.ShapeCount).AsInt32(); 230 | 231 | /// 232 | /// Gets the at the given index. The number of s is provided by . 233 | /// 234 | public Shape2D GetShape(int index) => Instance.Call(MethodName.GetShape, index).As(); 235 | 236 | /// Creates and wraps a around . 237 | /// The node with the attached to wrap. 238 | /// is . 239 | /// does not have the attached. 240 | public BasicCollisionPolygon2D(Node2D instance) 241 | { 242 | ArgumentNullException.ThrowIfNull(instance); 243 | if (!GDScriptEquivalent.InstanceHas(instance)) 244 | throw new ArgumentException($"must have attached script '{GDScriptEquivalentPath}'.", nameof(instance)); 245 | 246 | Instance = instance; 247 | BasicPolygon = new BasicPolygon2D(instance.Call(MethodName.GetBasicPolygon).As()); 248 | } 249 | 250 | /// Creates an instance of wrapped by a new . 251 | public BasicCollisionPolygon2D() : this(GDScriptEquivalent.New().As()) 252 | { 253 | } 254 | 255 | public static implicit operator Node2D(BasicCollisionPolygon2D node) => node.Instance; 256 | public static explicit operator BasicCollisionPolygon2D(Node2D node) => new(node); 257 | 258 | /// Cached s for the properties and fields contained in this class, for fast lookup. 259 | public class PropertyName : Node2D.PropertyName 260 | { 261 | public static readonly StringName Disabled = new("disabled"); 262 | public static readonly StringName OneWayCollision = new("one_way_collision"); 263 | public static readonly StringName OneWayCollisionMargin = new("one_way_collision_margin"); 264 | public static readonly StringName VerticesCount = new("vertices_count"); 265 | public static readonly StringName Sizes = new("sizes"); 266 | public static readonly StringName OffsetRotationDegrees = new("offset_rotation_degrees"); 267 | public static readonly StringName OffsetRotation = new("offset_rotation"); 268 | public static readonly StringName OffsetScale = new("offset_scale"); 269 | public static readonly StringName OffsetSkew = new("offset_skew"); 270 | public static readonly StringName OffsetTransform = new("offset_transform"); 271 | public static readonly StringName OffsetPosition = new("offset_position"); 272 | public static readonly StringName RingRatio = new("ring_ratio"); 273 | public static readonly StringName ArcStart = new("arc_start"); 274 | public static readonly StringName ArcAngle = new("arc_angle"); 275 | public static readonly StringName ArcEnd = new("arc_end"); 276 | public static readonly StringName ArcStartDegrees = new("arc_start_degrees"); 277 | public static readonly StringName ArcAngleDegrees = new("arc_angle_degrees"); 278 | public static readonly StringName ArcEndDegrees = new("arc_end_degrees"); 279 | public static readonly StringName CornerSize = new("corner_size"); 280 | public static readonly StringName CornerDetail = new("corner_detail"); 281 | public static readonly StringName ClosingMethod = new("closing_method"); 282 | public static readonly StringName RoundArcEnds = new("round_arc_ends"); 283 | } 284 | 285 | /// Cached s for the methods contained in this class, for fast lookup. 286 | public class MethodName : Node2D.MethodName 287 | { 288 | public static readonly StringName GetCreatedShape = new("get_created_shape"); 289 | public static readonly StringName GetCreatedShapeDecomposed = new("get_created_shape_decomposed"); 290 | public static readonly StringName GetCreatedShapeType = new("get_created_shape_type"); 291 | public static readonly StringName GetBasicPolygon = new("get_basic_polygon"); 292 | public static readonly StringName GetShape = new("get_shape"); 293 | public static readonly StringName ShapeCount = new("shape_count"); 294 | } 295 | 296 | /// Cached s for the signals contained in this class, for fast lookup. 297 | public class SignalName : Node2D.SignalName {} 298 | } 299 | -------------------------------------------------------------------------------- /addons/basic_shape_creation/basic_collision_polygon2d/basic_collision_polygon2d.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | @icon("res://addons/basic_shape_creation/basic_collision_polygon2d/basic_collision_polygon2d.svg") 3 | extends Node2D 4 | class_name BasicCollisionPolygon2D 5 | 6 | ## A node that provides a basic shape to a [CollisionObject2D] parent. 7 | ## 8 | ## A node that provides a shape generated by a [BasicPolygon2D] to a [CollisionObject2D] parent and allows edits to it. 9 | ## The shape may be convex or concave, and may be series of lines instead, which may not be connected to each other. 10 | ## This can give a detection shape to an [Area2D] or turn a [PhysicsBody2D] into a solid object. 11 | ## [br][br][b][color=red]Warning[/color][/b]: Removing this [Script] from a node with a [CollisionObject2D] parent will cause it to fail to remove 12 | ## the provided shape to the [CollisionObject2D]. Adding this [Script] to a node which already has a [CollisionObject2D] parent 13 | ## will cause it to fail to provide it with a shape until it is reparented. 14 | 15 | ## Toggles whether this shape is disabled and unable to detect collisions. 16 | @export 17 | var disabled := false: 18 | set(value): 19 | disabled = value 20 | queue_redraw() 21 | if _collision_object_parent != null: 22 | _collision_object_parent.shape_owner_set_disabled(_owner_id, value) 23 | 24 | ## Toggles whether this shape only detects collisions with edges that face up, relative to [BasicCollisionPolygon2D]'s rotation. 25 | ## [br][br][b]Note[/b]: This property has no effect if this [BasicCollisionPolygon2D] is a child of an [Area2D] node. 26 | @export 27 | var one_way_collision := false: 28 | set(value): 29 | one_way_collision = value 30 | queue_redraw() 31 | update_configuration_warnings() 32 | if _collision_object_parent != null: 33 | _collision_object_parent.shape_owner_set_one_way_collision(_owner_id, value) 34 | 35 | ## The margin used for one-way collision (in pixels). Higher values will make the shape thicker, 36 | ## and work better for colliders that enter the polygon at a high velocity. 37 | @export_range(0, 128, 0.1, "suffix:px") 38 | var one_way_collision_margin := 1.0: 39 | set(value): 40 | one_way_collision_margin = value 41 | if _collision_object_parent != null: 42 | _collision_object_parent.shape_owner_set_one_way_collision_margin(_owner_id, value) 43 | 44 | @export_group("Generation") 45 | 46 | ## The number of vertices in the base shape. 47 | ## A value of [code]1[/code] creates a 32 vertices shape. 48 | ## A value of [code]2[/code] creates multiple equidistantly spaced lines from the center, one for each value in [member sizes]. 49 | @export_range(1, 1000) 50 | var vertices_count : int = 1: 51 | get: return _basic_polygon_instance.vertices_count 52 | set(value): _basic_polygon_instance.vertices_count = value 53 | 54 | ## The distance from the center to each vertex, cycling through if there are multiple values. 55 | ## [br][br][b]Note[/b]: The default value is a [PackedFloat64Array] of [code][10.0][/code]. The [code][/code] 56 | ## documented here is a bug with Godot. 57 | @export 58 | var sizes : PackedFloat64Array = PackedFloat64Array([10]): 59 | get: return _basic_polygon_instance.sizes 60 | set(value): _basic_polygon_instance.sizes = value 61 | 62 | ## The size of the ring, in proportion from the outer edge to the center. A value of [code]1[/code] creates a normal shape, 63 | ## a value of [code]0[/code] creates a [enum ShapeType].Polyline outline, and a negative value extends the ring outwards proportionally. 64 | @export_range(0, 1, 0.001, "or_less") 65 | var ring_ratio : float = 1.0: 66 | get: return _basic_polygon_instance.ring_ratio 67 | set(value): _basic_polygon_instance.ring_ratio = value 68 | 69 | ## The size of each corner, as the distance along both edges, from the original vertex, to the point where the corner starts and ends. 70 | @export_range(0.0, 10, 0.001, "or_greater", "hide_slider") 71 | var corner_size : float = 0.0: 72 | get: return _basic_polygon_instance.corner_size 73 | set(value): _basic_polygon_instance.corner_size = value 74 | 75 | ## How many lines make up each corner. A value of [code]0[/code] will use a value of [code]32[/code] divided by [member vertices_count]. 76 | @export_range(0, 50) 77 | var corner_detail : int = 0: 78 | get: return _basic_polygon_instance.corner_detail 79 | set(value): _basic_polygon_instance.corner_detail = value 80 | 81 | ## The starting angle of the arc of the shape that is created, in radians. 82 | @export_range(-360, 360, 0.1, "or_greater", "or_less", "radians") 83 | var arc_start : float = 0.0: 84 | get: return _basic_polygon_instance.arc_start 85 | set(value): _basic_polygon_instance.arc_start = value 86 | 87 | ## The angle of the arc of the shape that is created, in radians. 88 | @export_range(0, 360, 0.1, "or_greater", "or_less", "radians") 89 | var arc_angle : float = TAU: 90 | get: return _basic_polygon_instance.arc_angle 91 | set(value): _basic_polygon_instance.arc_angle = value 92 | 93 | ## The ending angle of the arc of the shape that is created, in radians. 94 | ## [br][br][b]Note[/b]: This property's value depends on [member arc_start] and [member arc_angle], 95 | ## and setting this property will affect [member arc_angle]. 96 | var arc_end : float = TAU: 97 | get: return arc_start + arc_angle 98 | set(value): arc_angle = value - arc_start 99 | 100 | ## The starting angle of the arc of the shape that is created, in degrees. 101 | var arc_start_degrees : float: 102 | get: return rad_to_deg(arc_start) 103 | set(value): arc_start = deg_to_rad(value) 104 | 105 | ## The angle of the arc of the shape that is created, in degrees. 106 | var arc_angle_degrees : float: 107 | get: return rad_to_deg(arc_angle) 108 | set(value): arc_angle = deg_to_rad(value) 109 | 110 | ## The ending angle of the arc of the shape that is created, in degrees. 111 | ## [br][br][b]Note[/b]: This property's value depends on [member arc_start_degrees] and [member arc_angle_degrees], 112 | ## and setting this property will affect [member arc_angle_degrees]. 113 | var arc_end_degrees : float: 114 | get: return rad_to_deg(arc_end) 115 | set(value): arc_end = deg_to_rad(value) 116 | 117 | ## The method for closing an open shape. See [enum Polygon2D.ClosingMethod] 118 | @export 119 | var closing_method : BasicPolygon2D.ClosingMethod = BasicPolygon2D.ClosingMethod.SLICE: 120 | get: return _basic_polygon_instance.closing_method 121 | set(value): _basic_polygon_instance.closing_method = value 122 | 123 | ## Toggles rounding the corners cut out by [member arc_angle]. 124 | @export 125 | var round_arc_ends : bool = false: 126 | get: return _basic_polygon_instance.round_arc_ends 127 | set(value): _basic_polygon_instance.round_arc_ends = value 128 | 129 | @export_subgroup("Offset Transform", "offset") 130 | 131 | ## The offset postition of the shape 132 | @export 133 | var offset_position := Vector2.ZERO: 134 | get: return _basic_polygon_instance.offset_position 135 | set(value): _basic_polygon_instance.offset_position = value 136 | 137 | ## The offset rotation of the shape, in radians. 138 | @export_range(-360, 360, 0.1, "or_greater", "or_less", "radians") 139 | var offset_rotation : float = 0: 140 | get: return _basic_polygon_instance.offset_rotation 141 | set(value): _basic_polygon_instance.offset_rotation = value 142 | 143 | ## The offset rotation of the shape, in degrees. 144 | var offset_rotation_degrees : float = 0: 145 | set(value): 146 | offset_rotation = deg_to_rad(value) 147 | get: 148 | return rad_to_deg(offset_rotation) 149 | 150 | ## The offset scale of the shape. 151 | @export 152 | var offset_scale := Vector2.ONE: 153 | get: return _basic_polygon_instance.offset_scale 154 | set(value): _basic_polygon_instance.offset_scale = value 155 | 156 | ## The offset skew of the shape 157 | @export_range(-89.9, 89.9, 0.1, "radians") 158 | var offset_skew := 0.0: 159 | get: return _basic_polygon_instance.offset_skew 160 | set(value): _basic_polygon_instance.offset_skew = value 161 | 162 | ## The offset [Transform2D] of the shape. 163 | var offset_transform := Transform2D.IDENTITY: 164 | get: return _basic_polygon_instance.offset_transform 165 | set(value): _basic_polygon_instance.offset_transform = value 166 | 167 | var _created_shape : PackedVector2Array: 168 | get: return _basic_polygon_instance._created_shape 169 | set(value): _basic_polygon_instance._created_shape = value 170 | 171 | var _decomposed_created_shape : Array[PackedVector2Array]: 172 | get: return _basic_polygon_instance._decomposed_created_shape 173 | set(value): _basic_polygon_instance._decomposed_created_shape = value 174 | 175 | var _collision_shapes : Array[Shape2D] = []: 176 | set(value): 177 | assert(value != null) 178 | 179 | for shape in _collision_shapes: 180 | shape.changed.disconnect(queue_redraw) 181 | 182 | _collision_shapes = value 183 | _basic_polygon_instance._queue_status = BasicPolygon2D._UNQUEUED 184 | 185 | queue_redraw() 186 | for shape in _collision_shapes: 187 | shape.changed.connect(queue_redraw) 188 | 189 | if _collision_object_parent == null: 190 | return 191 | 192 | _collision_object_parent.shape_owner_clear_shapes(_owner_id) 193 | for shape in value: 194 | _collision_object_parent.shape_owner_add_shape(_owner_id, shape) 195 | 196 | _update_shape_owner() 197 | 198 | # PackedFloat64Arrays don't play well with reverts when exported in Godot 4.2, so this is required 199 | func _property_can_revert(property: StringName) -> bool: return property == &"sizes" 200 | func _property_get_revert(_property: StringName) -> Variant: return PackedFloat64Array([10.0]) 201 | 202 | func _get_property_list() -> Array[Dictionary]: 203 | return [{ 204 | name = "_decomposed_created_shape", 205 | type = TYPE_ARRAY, 206 | usage = PROPERTY_USAGE_STORAGE 207 | }, 208 | { 209 | name = "_created_shape", 210 | type = TYPE_PACKED_VECTOR2_ARRAY, 211 | usage = PROPERTY_USAGE_STORAGE 212 | }, 213 | { 214 | name = "_collision_shapes", 215 | type = TYPE_ARRAY, 216 | usage = PROPERTY_USAGE_STORAGE 217 | }] 218 | 219 | var _collision_object_parent : CollisionObject2D = null 220 | var _owner_id := -1 221 | 222 | var _basic_polygon_instance : BasicPolygon2D 223 | 224 | ## Gets the created shape. 225 | func get_created_shape() -> PackedVector2Array: return _created_shape 226 | ## Gets the created shape, decomposed into convex hulls. 227 | func get_created_shape_decomposed() -> Array[PackedVector2Array]: return _decomposed_created_shape 228 | ## Gets the type of shape provided by this [BasicCollisionPolygon2D]. See [enum BasicPolygon2D.ShapeType]. 229 | func get_created_shape_type() -> BasicPolygon2D.ShapeType: return _basic_polygon_instance.get_created_shape_type() 230 | 231 | func _init() -> void: 232 | _basic_polygon_instance = BasicPolygon2D.new() 233 | _basic_polygon_instance.draw_shape = false 234 | _basic_polygon_instance.export_behavior = BasicPolygon2D.ExportBehavior.EDITOR | BasicPolygon2D.ExportBehavior.RUN_TIME 235 | _basic_polygon_instance.shape_exported.connect(_on_shape_exported) 236 | add_child(_basic_polygon_instance, false, INTERNAL_MODE_FRONT) 237 | 238 | func _on_shape_exported(shape : PackedVector2Array, decomposed : Array[PackedVector2Array], type : BasicPolygon2D.ShapeType) -> void: 239 | if absf(arc_angle) <= PI and ring_ratio < 1 and ring_ratio > 0 and closing_method == BasicPolygon2D.ClosingMethod.CHORD: 240 | _collision_shapes = [] 241 | return 242 | 243 | match type: 244 | BasicPolygon2D.ShapeType.POLYGON: 245 | var shapes : Array[Shape2D] = [] 246 | shapes.resize(decomposed.size()) 247 | for i in decomposed.size(): 248 | var convex_shape := ConvexPolygonShape2D.new() 249 | convex_shape.points = decomposed[i] 250 | shapes[i] = convex_shape 251 | _collision_shapes = shapes 252 | 253 | BasicPolygon2D.ShapeType.POLYLINE: 254 | var polyline := shape.duplicate() 255 | polyline.resize(polyline.size() * 2 - 2) 256 | for i in shape.size() - 1: 257 | var index := shape.size() - i - 1 258 | polyline[-i * 2 - 1] = polyline[index] 259 | polyline[-i * 2 - 2] = polyline[index - 1] 260 | 261 | var concave_shape := ConcavePolygonShape2D.new() 262 | concave_shape.segments = polyline 263 | _collision_shapes = [concave_shape] 264 | 265 | BasicPolygon2D.ShapeType.MULTILINE: 266 | var concave_shape := ConcavePolygonShape2D.new() 267 | concave_shape.segments = shape 268 | _collision_shapes = [concave_shape] 269 | 270 | 271 | func _update_shape_owner() -> void: 272 | assert(_collision_object_parent != null) 273 | 274 | _collision_object_parent.shape_owner_set_transform(_owner_id, transform) 275 | _collision_object_parent.shape_owner_set_disabled(_owner_id, disabled) 276 | _collision_object_parent.shape_owner_set_one_way_collision(_owner_id, one_way_collision) 277 | _collision_object_parent.shape_owner_set_one_way_collision_margin(_owner_id, one_way_collision_margin) 278 | 279 | func _enter_tree() -> void: 280 | if _collision_object_parent != null: 281 | _update_shape_owner() 282 | 283 | func _notification(what: int) -> void: 284 | match(what): 285 | NOTIFICATION_PARENTED: 286 | _collision_object_parent = get_parent() as CollisionObject2D 287 | if _collision_object_parent == null: 288 | return 289 | 290 | _owner_id = _collision_object_parent.create_shape_owner(self) 291 | for shape in _collision_shapes: 292 | _collision_object_parent.shape_owner_add_shape(_owner_id, shape) 293 | _update_shape_owner() 294 | NOTIFICATION_LOCAL_TRANSFORM_CHANGED: 295 | if _collision_object_parent != null: 296 | _collision_object_parent.shape_owner_set_transform(_owner_id, transform) 297 | NOTIFICATION_UNPARENTED: 298 | if _collision_object_parent == null: 299 | return 300 | 301 | _collision_object_parent.remove_shape_owner(_owner_id) 302 | _collision_object_parent = null 303 | _owner_id = -1 304 | 305 | func _draw() -> void: 306 | if not Engine.is_editor_hint() and (not is_inside_tree() or not get_tree().debug_collisions_hint): 307 | return 308 | 309 | if _collision_shapes.is_empty(): 310 | return 311 | 312 | var debug_color : Color = ProjectSettings.get_setting("debug/shapes/collision/shape_color", Color("0099b36b")) 313 | var rid := get_canvas_item() 314 | var color := debug_color 315 | for shape in _collision_shapes: 316 | var color_actual := color 317 | if disabled: 318 | var gray := color.v 319 | color_actual = Color(gray, gray, gray, 0.25) 320 | shape.draw(rid, color_actual) 321 | color.h = fmod(color.h + 0.738, 1) 322 | 323 | if not one_way_collision: 324 | return 325 | 326 | color = debug_color.inverted() 327 | if disabled: 328 | color = color.darkened(0.25) 329 | 330 | var target := Vector2(0, 20) 331 | var size := 8 332 | var offset := Vector2(0.7071 * size, 0) 333 | draw_line(Vector2.ZERO, target, color, 2) 334 | draw_colored_polygon([target + Vector2(0, size), target + offset, target - offset], color) 335 | 336 | func _get_configuration_warnings() -> PackedStringArray: 337 | var warnings := _basic_polygon_instance._get_configuration_warnings() 338 | 339 | if _collision_object_parent == null: 340 | warnings.push_back("BasicCollisionPolygon2D only serves to provide a collision shape to a CollisionObject2D derived node.\nPlease only use it as a child of Area2D, StaticBody2D, RigidBody2D, CharacterBody2D, etc. to give them a shape.") 341 | if one_way_collision and _collision_object_parent is Area2D: 342 | warnings.push_back("The One Way Collision property will be ignored when the collision object is an Area2D.") 343 | 344 | return warnings 345 | 346 | ## Gets the number of [Shape2D]s that this [BasicCollisionPolygon2D] is providing. 347 | func shape_count() -> int: return _collision_shapes.size() 348 | 349 | ## Gets the [Shape2D] at the given index. The number of [Shape2D]s is provided by [method shape_count]. 350 | func get_shape(index : int) -> Shape2D: return _collision_shapes[index] 351 | 352 | ## Gets the underlying [BasicPolygon2D] instance that generates the shapes this [BasicCollisionPolygon2D] provides. 353 | func get_basic_polygon() -> BasicPolygon2D: return _basic_polygon_instance 354 | -------------------------------------------------------------------------------- /addons/basic_shape_creation/basic_polygon2d/BasicPolygon2D.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Godot; 3 | 4 | namespace BasicShapeCreation; 5 | 6 | /// 7 | /// A node for creating and drawing basic shapes, acting as a simplified wrapper around . 8 | /// The created shape can be accessed by , connecting the signal, 9 | /// or by using the 's export system with . 10 | ///

The shape is regenerated and exported whenever any of the shape properties are changed, and exported whenever any 11 | /// of the export properties are changed and returns . 12 | ///
13 | /// 14 | /// This class is a wrapper around an instance of a s with the at 15 | /// "res://addons/basic_shape_creation/basic_polygon2d/basic_polygon2d.gd" attached. 16 | /// The instance can be accessed with . 17 | /// 18 | public class BasicPolygon2D 19 | { 20 | /// The string path to the script this class wraps around. 21 | public const string GDScriptEquivalentPath = "res://addons/basic_shape_creation/basic_polygon2d/basic_polygon2d.gd"; 22 | /// The loaded of . 23 | public static readonly GDScript GDScriptEquivalent = GD.Load(GDScriptEquivalentPath); 24 | 25 | /// The instance this class wraps around. 26 | public Node2D Instance { get; } 27 | 28 | /// Emitted when a shape is exported. 29 | public delegate void ShapeExportedEventHandler(Vector2[] shape, Godot.Collections.Array shapeDecomposed, ShapeType shapeType); 30 | /// 31 | public event ShapeExportedEventHandler ShapeExported; 32 | 33 | /// 34 | /// The number of vertices in the base shape. 35 | /// 36 | /// 37 | /// A value of 1 creates a 32 vertices shape. 38 | /// A value of 2 creates multiple equidistantly spaced lines from the center, one for each value in . 39 | /// 40 | public int VerticesCount 41 | { 42 | get => (int)Instance.Get(PropertyName.VerticesCount); 43 | set => Instance.Set(PropertyName.VerticesCount, value); 44 | } 45 | 46 | /// 47 | /// The distance from the center to each vertex, cycling through if there are multiple values. 48 | /// 49 | public double[] Sizes 50 | { 51 | get => Instance.Get(PropertyName.Sizes).AsFloat64Array(); 52 | set => Instance.Set(PropertyName.Sizes, value); 53 | } 54 | 55 | /// The size of the ring, in proportion from the outer edge to the center. 56 | /// 57 | /// A value of 1 creates a normal shape, 58 | /// a value of 0 creates a outline, and a negative value extends the ring outwards proportionally. 59 | /// 60 | public float RingRatio 61 | { 62 | get => Instance.Get(PropertyName.RingRatio).AsSingle(); 63 | set => Instance.Set(PropertyName.RingRatio, value); 64 | } 65 | 66 | /// The size of each corner, as the distance along both edges, from the original vertex, to the point where the corner starts and ends. 67 | public float CornerSize 68 | { 69 | get => Instance.Get(PropertyName.CornerSize).AsSingle(); 70 | set => Instance.Set(PropertyName.CornerSize, value); 71 | } 72 | 73 | /// 74 | /// How many lines make up each corner. A value of 0 will use a value of 32 divided by . 75 | /// 76 | public int CornerDetail 77 | { 78 | get => Instance.Get(PropertyName.CornerDetail).AsInt32(); 79 | set => Instance.Set(PropertyName.CornerDetail, value); 80 | } 81 | 82 | /// The starting angle of the arc of the shape that is created, in radians. 83 | public float ArcStart 84 | { 85 | get => Instance.Get(PropertyName.ArcStart).AsSingle(); 86 | set => Instance.Set(PropertyName.ArcStart, value); 87 | } 88 | 89 | /// The angle of the arc of the shape that is created, in radians. 90 | public float ArcAngle 91 | { 92 | get => Instance.Get(PropertyName.ArcAngle).AsSingle(); 93 | set => Instance.Set(PropertyName.ArcAngle, value); 94 | } 95 | 96 | /// The ending angle of the arc of the shape that is created, in radians. 97 | /// 98 | /// This property's value depends on and , 99 | /// and setting this property will affect . 100 | /// 101 | public float ArcEnd 102 | { 103 | get => Instance.Get(PropertyName.ArcEnd).AsSingle(); 104 | set => Instance.Set(PropertyName.ArcEnd, value); 105 | } 106 | 107 | 108 | /// The starting angle of the arc of the shape that is created, in degrees. 109 | public float ArcStartDegrees 110 | { 111 | get => Instance.Get(PropertyName.ArcStartDegrees).AsSingle(); 112 | set => Instance.Set(PropertyName.ArcStartDegrees, value); 113 | } 114 | 115 | /// The angle of the arc of the shape that is created, in degrees. 116 | public float ArcAngleDegrees 117 | { 118 | get => Instance.Get(PropertyName.ArcAngleDegrees).AsSingle(); 119 | set => Instance.Set(PropertyName.ArcAngleDegrees, value); 120 | } 121 | 122 | /// The ending angle of the arc of the shape that is created, in degrees. 123 | /// 124 | /// This property's value depends on and , 125 | /// and setting this property will affect . 126 | /// 127 | public float ArcEndDegrees 128 | { 129 | get => Instance.Get(PropertyName.ArcEndDegrees).AsSingle(); 130 | set => Instance.Set(PropertyName.ArcEndDegrees, value); 131 | } 132 | 133 | /// The method for closing an open shape. 134 | /// 135 | public ClosingMethod ClosingMethod 136 | { 137 | get => Instance.Get(PropertyName.ClosingMethod).As(); 138 | set => Instance.Set(PropertyName.ClosingMethod, (int)value); 139 | } 140 | 141 | /// Toggles rounding the corners cut out by . 142 | public bool RoundArcEnds 143 | { 144 | get => Instance.Get(PropertyName.RoundArcEnds).AsBool(); 145 | set => Instance.Set(PropertyName.RoundArcEnds, value); 146 | } 147 | 148 | /// The offset position of the shape. 149 | public Vector2 OffsetPosition 150 | { 151 | get => Instance.Get(PropertyName.OffsetPosition).AsVector2(); 152 | set => Instance.Set(PropertyName.OffsetPosition, value); 153 | } 154 | /// The offset rotation of the shape, in degrees. 155 | public float OffsetRotationDegrees 156 | { 157 | get => Instance.Get(PropertyName.OffsetRotationDegrees).AsSingle(); 158 | set => Instance.Set(PropertyName.OffsetRotationDegrees, value); 159 | } 160 | /// The offset rotation of the shape, in radians. 161 | public float OffsetRotation 162 | { 163 | get => Instance.Get(PropertyName.OffsetRotation).AsSingle(); 164 | set => Instance.Set(PropertyName.OffsetRotation, value); 165 | } 166 | 167 | /// The offset scale of the shape. 168 | public Vector2 OffsetScale 169 | { 170 | get => Instance.Get(PropertyName.OffsetScale).AsVector2(); 171 | set => Instance.Set(PropertyName.OffsetScale, value); 172 | } 173 | 174 | /// The offset skew of the shape 175 | public float OffsetSkew 176 | { 177 | get => Instance.Get(PropertyName.OffsetSkew).AsSingle(); 178 | set => Instance.Set(PropertyName.OffsetSkew, value); 179 | } 180 | 181 | /// The offset of the shape. 182 | public Transform2D OffsetTransform 183 | { 184 | get => Instance.Get(PropertyName.OffsetTransform).AsTransform2D(); 185 | set => Instance.Set(PropertyName.OffsetTransform, value); 186 | } 187 | 188 | /// Toggles drawing the created shape. 189 | public bool DrawShape 190 | { 191 | get => Instance.Get(PropertyName.DrawShape).AsBool(); 192 | set => Instance.Set(PropertyName.DrawShape, value); 193 | } 194 | 195 | /// Toggles drawing a border around a . 196 | /// 197 | /// If the shape is a line, this property changes which color property is used; if , 198 | /// if . 199 | /// 200 | public bool DrawBorder 201 | { 202 | get => Instance.Get(PropertyName.DrawBorder).AsBool(); 203 | set => Instance.Set(PropertyName.DrawBorder, value); 204 | } 205 | 206 | /// 207 | /// The width of the drawn border, if the shape is a and is . 208 | /// The width of the drawn shape, if the shape is a line. If set to a value of 0, two-point thin lines are drawn. 209 | /// 210 | public float BorderWidth 211 | { 212 | get => Instance.Get(PropertyName.BorderWidth).AsSingle(); 213 | set => Instance.Set(PropertyName.BorderWidth, value); 214 | } 215 | 216 | /// The color of the shape. 217 | public Color Color 218 | { 219 | get => Instance.Get(PropertyName.Color).AsColor(); 220 | set => Instance.Set(PropertyName.Color, value); 221 | } 222 | 223 | 224 | /// The color of the border of the shape, and for a line shape if is . 225 | public Color BorderColor 226 | { 227 | get => Instance.Get(PropertyName.BorderColor).AsColor(); 228 | set => Instance.Set(PropertyName.BorderColor, value); 229 | } 230 | 231 | /// 232 | /// Toggles the setting of when exporting the shape with , 233 | /// and whether to do so in editor and/or at runtime. 234 | /// 235 | /// 236 | public ExportBehavior ExportBehavior 237 | { 238 | get => Instance.Get(PropertyName.ExportBehavior).As(); 239 | set => Instance.Set(PropertyName.ExportBehavior, (int)value); 240 | } 241 | 242 | /// 243 | /// Toggles setting the with the decomposed convex hulls of the created shape, instead of the shape itself. 244 | /// If , the set value will be of type , and type otherwise. 245 | /// 246 | public bool ExportAsDecomposedHulls 247 | { 248 | get => Instance.Get(PropertyName.ExportAsDecomposedHulls).AsBool(); 249 | set => Instance.Set(PropertyName.ExportAsDecomposedHulls, value); 250 | } 251 | 252 | /// Toggles automatically freeing itself after exporting for the first time at runtime. 253 | public bool AutoFree 254 | { 255 | get => Instance.Get(PropertyName.AutoFree).AsBool(); 256 | set => Instance.Set(PropertyName.AutoFree, value); 257 | } 258 | 259 | /// 260 | /// The properties to set with the created shape when exporting. See description of [NodePath] for how to reference a (sub) property. 261 | /// 262 | /// 263 | /// Requires to return for these properties to be set. 264 | /// The type of the set value depends on . 265 | /// 266 | public Godot.Collections.Array ExportTargets 267 | { 268 | get => Instance.Get(PropertyName.ExportTargets).AsGodotArray(); 269 | set => Instance.Call(MethodName.SetExportTargets, value); 270 | } 271 | 272 | /// 273 | public Vector2 Position 274 | { 275 | get => Instance.Position; 276 | set => Instance.Position = value; 277 | } 278 | /// 279 | public float Rotation 280 | { 281 | get => Instance.Rotation; 282 | set => Instance.Rotation = value; 283 | } 284 | /// 285 | public float RotationDegrees 286 | { 287 | get => Instance.RotationDegrees; 288 | set => Instance.RotationDegrees = value; 289 | } 290 | /// 291 | public Vector2 Scale 292 | { 293 | get => Instance.Scale; 294 | set => Instance.Scale = value; 295 | } 296 | 297 | /// Gets the created shape. 298 | /// A copy of the created shape 299 | public Vector2[] CreatedShape => Instance.Call(MethodName.GetCreatedShape).AsVector2Array(); 300 | 301 | /// Gets the created shape, decomposed into convex hulls. 302 | /// A copy of the created shape, decomposed into convex hulls. 303 | public Godot.Collections.Array CreatedShapeDecomposed => Instance.Call(MethodName.GetCreatedShapeDecomposed).AsGodotArray(); 304 | 305 | /// Gets the type of shape created by this . 306 | /// The created. 307 | public ShapeType CreatedShapeType => Instance.Call(MethodName.GetCreatedShapeType).As(); 308 | 309 | /// Determines whether will be set on . 310 | /// 311 | /// This is the case when has the flag of 312 | /// set which corresponds to whether this is running in editor or at runtime. 313 | ///

is emiited on regardless of this method's return value. 314 | ///
315 | /// 316 | /// Returns will be set on . 317 | /// 318 | public bool CanExport() => Instance.Call(MethodName.CanExport).AsBool(); 319 | 320 | /// 321 | /// Queues the to and the shape. 322 | /// Called when the Generation properties are modified. Multiple calls will be converted to a single call. 323 | /// 324 | /// 325 | /// Removes queued calls. 326 | /// 327 | public void QueueRegenerate() => Instance.Call(MethodName.QueueRegenerate); 328 | 329 | /// Instantly regenerates the shape, then s it. 330 | /// Removes queued and calls. 331 | public void Regenerate() => Instance.Call(MethodName.Regenerate); 332 | 333 | /// 334 | /// Queue the to the shape. Multiple calls will be converted to a single call. 335 | /// 336 | public void QueueExport() => Instance.Call(MethodName.QueueExport); 337 | 338 | /// 339 | /// Instantly exports the previously created shape, emitting , 340 | /// as well as setting the if returns . 341 | /// 342 | /// Removes queued and calls. 343 | public void Export() => Instance.Call(MethodName.Export); 344 | 345 | // /// 346 | // /// Transforms , rotating it by radians and scaling it by a factor of . 347 | // /// 348 | // /// Unlike other methods, this simply affects and , regenerating the shape 349 | // /// The amount to rotate the shape in radians. 350 | // /// The factor to scale the shape. 351 | // public void ApplyTransformation(float rotation, float scale, bool scale_width = true, bool scale_corner_size = true) => Instance.Call(MethodName.ApplyTransformation, rotation, scale, scale_width, scale_corner_size); 352 | // public void ApplyTransformation(float rotation, float scale) => Instance.Call(MethodName.ApplyTransformation, rotation, scale); 353 | 354 | /// 355 | public void QueueRedraw() => Instance.QueueRedraw(); 356 | 357 | 358 | /// Creates and wraps a around . 359 | /// The node with the attached to wrap. 360 | /// is . 361 | /// does not have the attached. 362 | public BasicPolygon2D(Node2D instance) 363 | { 364 | if (instance is null) 365 | throw new ArgumentNullException(nameof(instance)); 366 | if (GDScriptEquivalent != instance.GetScript().As()) 367 | throw new ArgumentException($"must have attached script '{GDScriptEquivalentPath}'.", nameof(instance)); 368 | 369 | Instance = instance; 370 | instance.Connect(SignalName.ShapeExported, Callable.From, ShapeType>((shape, decomposed, type) => ShapeExported?.Invoke(shape, decomposed, type))); 371 | } 372 | /// Creates an instance of wrapped by a new . 373 | public BasicPolygon2D() 374 | : this(GDScriptEquivalent.New().As()) 375 | { 376 | } 377 | 378 | // /// Returns an array of s with the points for the shape with the specified . 379 | // /// The number of vertices in the shape. If it is 1, a value of 32 is used. 380 | // /// The distance each corner vertices is from the center. 381 | // /// The rotation applied to the shape. 382 | // /// The center of the shape. 383 | // public static Vector2[] GetShapeVertices(int verticesCount, float size = 1, float offsetRotation = 0, Vector2 offsetPosition = default) 384 | // => _shared.Value.Call(MethodName.GetShapeVertices, verticesCount, size, offsetRotation, offsetPosition).As(); 385 | 386 | public static implicit operator Node2D(BasicPolygon2D instance) => instance.Instance; 387 | public static explicit operator BasicPolygon2D(Node2D instance) => new(instance); 388 | 389 | /// Cached s for the properties and fields contained in this class, for fast lookup. 390 | public class PropertyName : Node2D.PropertyName 391 | { 392 | public static readonly StringName VerticesCount = new("vertices_count"); 393 | public static readonly StringName Sizes = new("sizes"); 394 | public static readonly StringName OffsetRotationDegrees = new("offset_rotation_degrees"); 395 | public static readonly StringName OffsetRotation = new("offset_rotation"); 396 | public static readonly StringName OffsetScale = new("offset_scale"); 397 | public static readonly StringName OffsetSkew = new("offset_skew"); 398 | public static readonly StringName OffsetTransform = new("offset_transform"); 399 | public static readonly StringName Color = new("color"); 400 | public static readonly StringName OffsetPosition = new("offset_position"); 401 | public static readonly StringName RingRatio = new("ring_ratio"); 402 | public static readonly StringName ArcStart = new("arc_start"); 403 | public static readonly StringName ArcAngle = new("arc_angle"); 404 | public static readonly StringName ArcEnd = new("arc_end"); 405 | public static readonly StringName ArcStartDegrees = new("arc_start_degrees"); 406 | public static readonly StringName ArcAngleDegrees = new("arc_angle_degrees"); 407 | public static readonly StringName ArcEndDegrees = new("arc_end_degrees"); 408 | public static readonly StringName CornerSize = new("corner_size"); 409 | public static readonly StringName CornerDetail = new("corner_detail"); 410 | public static readonly StringName ClosingMethod = new("closing_method"); 411 | public static readonly StringName RoundArcEnds = new("round_arc_ends"); 412 | public static readonly StringName DrawShape = new("draw_shape"); 413 | public static readonly StringName DrawBorder = new("draw_border"); 414 | public static readonly StringName BorderWidth = new("border_width"); 415 | public static readonly StringName BorderColor = new("border_color"); 416 | public static readonly StringName ExportBehavior = new("export_behavior"); 417 | public static readonly StringName ExportAsDecomposedHulls = new("export_as_decomposed_hulls"); 418 | public static readonly StringName AutoFree = new("auto_free"); 419 | public static readonly StringName ExportTargets = new("export_targets"); 420 | } 421 | 422 | /// Cached s for the methods contained in this class, for fast lookup. 423 | public class MethodName : Node2D.MethodName 424 | { 425 | public static readonly StringName QueueRegenerate = new("queue_regenerate"); 426 | public static readonly StringName Regenerate = new("regenerate"); 427 | public static readonly StringName QueueExport = new("queue_export"); 428 | public static readonly StringName Export = new("export"); 429 | public static readonly StringName CanExport = new("can_export"); 430 | public static readonly StringName SetExportTargets = new("_set_export_targets"); 431 | public static readonly StringName GetCreatedShape = new("get_created_shape"); 432 | public static readonly StringName GetCreatedShapeDecomposed = new("get_created_shape_decomposed"); 433 | public static readonly StringName GetCreatedShapeType = new("get_created_shape_type"); 434 | } 435 | 436 | /// Cached s for the signals contained in this class, for fast lookup. 437 | public class SignalName : Node2D.SignalName 438 | { 439 | public static readonly StringName ShapeExported = new("shape_exported"); 440 | } 441 | } 442 | -------------------------------------------------------------------------------- /addons/basic_shape_creation/basic_polygon2d/basic_polygon2d.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | @icon("res://addons/basic_shape_creation/basic_polygon2d/basic_polygon2d.svg") 3 | class_name BasicPolygon2D 4 | extends Node2D 5 | 6 | ## A basic shape creater. 7 | ## 8 | ## A node for creating and drawing basic shapes, acting as a simplified wrapper around [BasicGeometry2D]. 9 | ## The created shape can be accessed by [method get_created_shape], connecting the [signal shape_exported] signal, 10 | ## or by using the [BasicPolygon2D]'s export system with [member export_targets]. 11 | ## [br][br]The shape is regenerated and exported whenever any of the shape properties are changed, and exported whenever any 12 | ## of the export properties are changed and [method can_export] returns [code]true[/code]. 13 | ## [br][br][b][color=red]Warning[/color][/b]: The method [method queue_regenerate], which the [BasicPolygon2D] uses to regenerate the shape, 14 | ## as well as [method queue_export], relies upon the main loop being a [SceneTree] to function properly. 15 | 16 | @export_group("Generation") 17 | ## The number of vertices in the base shape. 18 | ## A value of [code]1[/code] creates a 32 vertices shape. 19 | ## A value of [code]2[/code] creates multiple equidistantly spaced lines from the center, one for each value in [member sizes]. 20 | @export_range(1, 1000) 21 | var vertices_count : int = 1: 22 | set(value): 23 | assert(value > 0, "property 'vertices_count' must be greater than 0") 24 | vertices_count = value 25 | update_configuration_warnings() 26 | queue_regenerate() 27 | 28 | ## The distance from the center to each vertex, cycling through if there are multiple values. 29 | ## [br][br][b]Note[/b]: The default value is a [PackedFloat64Array] of [code][10.0][/code]. The [code][/code] 30 | ## documented here is a bug with Godot. 31 | @export 32 | var sizes : PackedFloat64Array = PackedFloat64Array([10.0]): 33 | set(value): 34 | if value.size() == 0: 35 | return 36 | 37 | for i in value.size(): 38 | if value[i] < 0.001: 39 | value[i] = 10 if i >= sizes.size() else sizes[i] 40 | 41 | 42 | sizes = value 43 | queue_regenerate() 44 | 45 | ## The size of the ring, in proportion from the outer edge to the center. A value of [code]1[/code] creates a normal shape, 46 | ## a value of [code]0[/code] creates a [enum ShapeType].Polyline outline, and a negative value extends the ring outwards proportionally. 47 | @export_range(0, 1, 0.001, "or_less") 48 | var ring_ratio : float = 1.0: 49 | set(value): 50 | assert(ring_ratio <= 1.0, "property 'ring_ratio' must be 1 or less") 51 | ring_ratio = value 52 | update_configuration_warnings() 53 | queue_regenerate() 54 | 55 | ## The size of each corner, as the distance along both edges, from the original vertex, to the point where the corner starts and ends. 56 | @export_range(0.0, 10, 0.001, "or_greater", "hide_slider") 57 | var corner_size : float = 0.0: 58 | set(value): 59 | assert(value >= 0, "property 'corner_size' must be greater than or equal to 0") 60 | corner_size = value 61 | queue_regenerate() 62 | 63 | ## How many lines make up each corner. A value of [code]0[/code] will use a value of [code]32[/code] divided by [member vertices_count]. 64 | @export_range(0, 50) 65 | var corner_detail : int = 0: 66 | set(value): 67 | assert(value >= 0, "property 'corner_detail' must be greater than or equal to 0") 68 | corner_detail = value 69 | queue_regenerate() 70 | 71 | ## The starting angle of the arc of the shape that is created, in radians. 72 | @export_range(-360, 360, 0.1, "or_greater", "or_less", "radians") 73 | var arc_start : float = 0.0: 74 | set(value): 75 | arc_start = value 76 | update_configuration_warnings() 77 | queue_regenerate() 78 | 79 | ## The angle of the arc of the shape that is created, in radians. 80 | @export_range(0, 360, 0.1, "or_greater", "or_less", "radians") 81 | var arc_angle : float = TAU: 82 | set(value): 83 | arc_angle = value 84 | update_configuration_warnings() 85 | queue_regenerate() 86 | 87 | ## The ending angle of the arc of the shape that is created, in radians. 88 | ## [br][br][b]Note[/b]: This property's value depends on [member arc_start] and [member arc_angle], 89 | ## and setting this property will affect [member arc_angle]. 90 | var arc_end : float = TAU: 91 | get: return arc_start + arc_angle 92 | set(value): arc_angle = value - arc_start 93 | 94 | ## The starting angle of the arc of the shape that is created, in degrees. 95 | var arc_start_degrees : float: 96 | get: return rad_to_deg(arc_start) 97 | set(value): arc_start = deg_to_rad(value) 98 | 99 | ## The angle of the arc of the shape that is created, in degrees. 100 | var arc_angle_degrees : float: 101 | get: return rad_to_deg(arc_angle) 102 | set(value): arc_angle = deg_to_rad(value) 103 | 104 | ## The ending angle of the arc of the shape that is created, in degrees. 105 | ## [br][br][b]Note[/b]: This property's value depends on [member arc_start_degrees] and [member arc_angle_degrees], 106 | ## and setting this property will affect [member arc_angle_degrees]. 107 | var arc_end_degrees : float: 108 | get: return rad_to_deg(arc_end) 109 | set(value): arc_end = deg_to_rad(value) 110 | 111 | ## Methods for closing an open shape. 112 | enum ClosingMethod { 113 | ## Shape is closed with two lines between the ends and the center of the shape. 114 | SLICE = 0, 115 | ## Shape is closed by connected the 2 ends together directly. 116 | CHORD, 117 | ## Shape is left open. This only has an effect for ring shapes, and is otherwise equivalent to [enum ClosingStrategy].SLICE. 118 | ARC, 119 | } 120 | 121 | ## The method for closing an open shape. See [enum ClosingMethod]. 122 | @export 123 | var closing_method : ClosingMethod = ClosingMethod.SLICE: 124 | set(value): 125 | closing_method = value 126 | update_configuration_warnings() 127 | queue_regenerate() 128 | 129 | ## Toggles rounding the corners cut out by [member arc_angle]. 130 | @export 131 | var round_arc_ends : bool = false: 132 | set(value): 133 | round_arc_ends = value 134 | queue_regenerate() 135 | 136 | @export_subgroup("Offset tranform", "offset") 137 | 138 | ## The offset postition of the shape 139 | @export 140 | var offset_position := Vector2.ZERO: 141 | set(value): 142 | offset_position = value 143 | queue_regenerate() 144 | 145 | ## The offset rotation of the shape, in radians. 146 | @export_range(-360, 360, 0.1, "or_greater", "or_less", "radians") 147 | var offset_rotation : float = 0: 148 | set(value): 149 | offset_rotation = value 150 | queue_regenerate() 151 | 152 | ## The offset rotation of the shape, in degrees. 153 | var offset_rotation_degrees : float = 0: 154 | set(value): 155 | offset_rotation = deg_to_rad(value) 156 | get: 157 | return rad_to_deg(offset_rotation) 158 | 159 | ## The offset scale of the shape. 160 | @export 161 | var offset_scale := Vector2.ONE: 162 | set(value): 163 | offset_scale = value 164 | queue_regenerate() 165 | 166 | ## The offset skew of the shape 167 | @export_range(-89.9, 89.9, 0.1, "radians") 168 | var offset_skew := 0.0: 169 | set(value): 170 | offset_skew = value 171 | queue_regenerate() 172 | 173 | ## The offset [Transform2D] of the shape. 174 | var offset_transform := Transform2D.IDENTITY: 175 | get: return Transform2D(offset_rotation, offset_scale, offset_skew, offset_position) 176 | set(value): 177 | offset_rotation = value.get_rotation() 178 | offset_position = value.get_origin() 179 | offset_skew = value.get_skew() 180 | offset_scale = value.get_scale() 181 | 182 | @export_group("Drawing") 183 | 184 | ## Toggles drawing the created shape. 185 | @export 186 | var draw_shape := true: 187 | set(value): 188 | draw_shape = value 189 | update_configuration_warnings() 190 | queue_redraw() 191 | 192 | ## Toggles drawing a border around a [enum ShapeType].POLYGON. 193 | ## [br][br]If the shape is a line, this property changes which color property is used; [member color] if [code]false[/code], [member border_color] if [code]true[/code]. 194 | @export 195 | var draw_border := false: 196 | set(value): 197 | draw_border = value 198 | queue_redraw() 199 | 200 | ## The width of the drawn border, if the shape is a [enum ShapeType].POLYGON and [member draw_border] is [code]true[/code]. 201 | ## The width of the drawn shape, if the shape is a line. If set to a value of [code]0[/code], two-point thin lines are drawn. 202 | @export_range(0, 10, 0.001, "or_greater", "hide_slider") 203 | var border_width : float = 0.0: 204 | set(value): 205 | assert(value >= 0, "property 'border_width' must be at least 0.") 206 | border_width = value 207 | queue_redraw() 208 | 209 | ## The color of the drawn shape. 210 | @export 211 | var color : Color = Color.WHITE: 212 | set(value): 213 | color = value 214 | queue_redraw() 215 | 216 | ## The color of the border of the shape, and for a line shape if [member draw_border] is [code]true[/code]. 217 | @export 218 | var border_color := Color.BLACK: 219 | set(value): 220 | border_color = value 221 | queue_redraw() 222 | 223 | @export_group("Exporting") 224 | 225 | ## Toggles the setting of [member export_targets] when exporting the shape with [method export], 226 | ## and whether to do so in editor and/or at runtime. See [enum ExportBehavior] for exact values to use. 227 | @export_flags("Editor:1", "Runtime:2") 228 | var export_behavior : int = ExportBehavior.DISABLED: 229 | set(value): 230 | var was_exporting := can_export() 231 | export_behavior = value 232 | if not was_exporting and can_export(): 233 | queue_export() 234 | 235 | ## When the [BasicPolygon2D] should set the [member export_targets]. 236 | enum ExportBehavior { 237 | ## Never export the shape. 238 | DISABLED = 0, 239 | ## Export while in the editor. Useful to preview the shape in other nodes, or if the set properties are serialized. 240 | EDITOR = 1, 241 | ## Export while the game is running. 242 | RUN_TIME = 2, 243 | } 244 | 245 | ## Toggles setting the [member export_targets] with the decomposed convex hulls of the created shape, instead of the shape itself. 246 | ## If [code]true[/code], the set value will be of type [Array][lb][PackedVector2Array][rb], and type [PackedVector2Array] otherwise. 247 | @export 248 | var export_as_decomposed_hulls := false 249 | 250 | ## Toggles automatically freeing itself after exporting for the first time at runtime. 251 | @export 252 | var auto_free := false 253 | 254 | @export_group("Exporting", "export") 255 | 256 | ## The properties to set with the created shape when exporting. See description of [NodePath] for how to reference a (sub) property. 257 | ## [br][br]Requires [method can_export] to return [code]true[/code] for these properties to be set. 258 | ## The type of the set value depends on [member export_as_decomposed_hulls]. 259 | @export 260 | var export_targets : Array[NodePath] = []: 261 | set(value): 262 | if value == null: 263 | return 264 | export_targets = value 265 | update_configuration_warnings() 266 | 267 | # for the purposes of c# interop, which cannot set typed arrays as of Godot v4.2 as the type isn't stored when interopping. 268 | func _set_export_targets(array : Array) -> void: 269 | if array.is_same_typed(export_targets): 270 | export_targets = array 271 | if array.is_empty(): 272 | export_targets = [] 273 | 274 | var targets : Array[NodePath] = [] 275 | for value in array: 276 | assert(typeof(value) == TYPE_NODE_PATH or typeof(value) == TYPE_STRING, "cannot convert %s from %s into a NodePath" % [value, array]) 277 | targets.push_back(value as NodePath) 278 | 279 | export_targets = targets 280 | 281 | ## Emitted when a shape is exported. 282 | signal shape_exported(shape : PackedVector2Array, decomposed_shape : Array[PackedVector2Array], shape_type : ShapeType) 283 | 284 | var _created_shape : PackedVector2Array = []: 285 | set(value): 286 | _created_shape = value 287 | if _queue_status != _QUEUE_DISPERSE: 288 | _queue_status = _UNQUEUED 289 | queue_export() 290 | queue_redraw() 291 | 292 | var _decomposed_created_shape : Array[PackedVector2Array] = []: 293 | set(value): 294 | _decomposed_created_shape = value 295 | if _queue_status != _QUEUE_DISPERSE: 296 | _queue_status = _UNQUEUED 297 | queue_export() 298 | queue_redraw() 299 | 300 | # PackedFloat64Arrays don't play well with reverts when exported in Godot 4.2, so this is required 301 | func _property_can_revert(property: StringName) -> bool: return property == &"sizes" 302 | func _property_get_revert(_property: StringName) -> Variant: return PackedFloat64Array([10.0]) 303 | 304 | ## Returns [code]true[/code] when [member export_targets] will be set on [method export]. This is the case when 305 | ## [member export_behavior] has the flag of [enum ExportBehavior] set which corrosponds to where this [BasicPolygon2D] is running, in editor or at runtime. 306 | ## [br][br][b]Note:[/b] [signal shape_exported] is emitted on [method export] regardless of this methods return value. 307 | func can_export() -> bool: 308 | var in_editor := Engine.is_editor_hint() 309 | return in_editor and (export_behavior & ExportBehavior.EDITOR) > 0 or not in_editor and (export_behavior & ExportBehavior.RUN_TIME) > 0 310 | 311 | ## Gets the created shape. 312 | ## [br][br][b]Note[/b]: The returned value is [b]Not[/b] a copy, and modifications to it will persist for all future consumers until the shape is regenerated. 313 | func get_created_shape() -> PackedVector2Array: return _created_shape 314 | ## Gets the created shape, decomposed into convex hulls. 315 | ## [br][br][b]Note[/b]: The returned value is [b]Not[/b] a copy, and modifications to it will persist for all future consumers until the shape is regenerated. 316 | func get_created_shape_decomposed() -> Array[PackedVector2Array]: return _decomposed_created_shape 317 | 318 | ## The type of shape created. 319 | enum ShapeType { 320 | ## The shape is a polygon. 321 | POLYGON = 0, 322 | ## The shape is a line, where each point is connected to the previous and next points, leading to interconnected lines. 323 | ## The first and last points are not connected. 324 | POLYLINE, 325 | ## The shape is a line, where points come in pairs representing individual, potentially unconnected lines. 326 | MULTILINE, 327 | } 328 | 329 | ## Gets the type of shape created by this [BasicPolygon2D]. See [enum ShapeType]. 330 | func get_created_shape_type() -> ShapeType: 331 | if vertices_count == 2: return ShapeType.MULTILINE 332 | if is_zero_approx(ring_ratio) or _created_shape.size() == 2: return ShapeType.POLYLINE 333 | return ShapeType.POLYGON 334 | 335 | const _UNQUEUED := 0 336 | const _QUEUE_DISPERSE := 1 337 | const _QUEUE_REGENERATE := 2 338 | 339 | var _queue_status : int = _UNQUEUED 340 | 341 | func _init() -> void: 342 | queue_regenerate() 343 | 344 | func _find_tree() -> SceneTree: 345 | if is_inside_tree(): 346 | return get_tree() 347 | assert(Engine.get_main_loop() is SceneTree, "'queue_regenerate' and 'queue_export' functions only work if the current main loop implementation of the engine is a SceneTree") 348 | return Engine.get_main_loop() as SceneTree 349 | 350 | ## Queue the [BasicPolygon2D] to regenerate and export the shape. Called when the Generation properties are modified. 351 | ## Multiple calls will be converted to a single call. See [method regenerate]. 352 | ## Removes queued [method queue_export] calls. 353 | func queue_regenerate() -> void: 354 | if _queue_status >= _QUEUE_REGENERATE: 355 | return 356 | 357 | _queue_status = _QUEUE_REGENERATE 358 | 359 | await _find_tree().process_frame 360 | if _queue_status != _QUEUE_REGENERATE: 361 | return 362 | 363 | regenerate() 364 | 365 | ## Instantly regenerates the shape, than [method export]s it. 366 | ## Removes queued [method queue_regenerate] and [method queue_export] calls. 367 | func regenerate() -> void: 368 | _queue_status = _UNQUEUED 369 | 370 | var shape : PackedVector2Array 371 | var decomposed_shape : Array[PackedVector2Array] 372 | var is_outline := is_zero_approx(ring_ratio) 373 | var is_ring_shape := not is_outline and ring_ratio < 1 374 | var uses_arc := not is_equal_approx(arc_angle, TAU) 375 | var rounded_corners := not is_zero_approx(corner_size) 376 | var true_corner_detail := corner_detail if corner_detail != 0 else maxi(1, 32 / vertices_count if vertices_count > 1 else 1) 377 | 378 | if is_zero_approx(arc_angle): 379 | _queue_status = _QUEUE_DISPERSE 380 | _created_shape = [] 381 | _decomposed_created_shape = [] 382 | export() 383 | return 384 | 385 | if vertices_count == 2: 386 | var line_count := sizes.size() 387 | var side_chord_arc_angle := TAU / line_count 388 | var line_arc_start := ceilf(arc_start / side_chord_arc_angle) * side_chord_arc_angle 389 | var line_arc_end := floorf(arc_end / side_chord_arc_angle) * side_chord_arc_angle 390 | 391 | if line_arc_start > line_arc_end: 392 | _queue_status = _QUEUE_DISPERSE 393 | _created_shape = [] 394 | _decomposed_created_shape = [] 395 | export() 396 | return 397 | 398 | if sizes.size() == 1: 399 | shape = PackedVector2Array([BasicGeometry2D._circle_point(line_arc_start + offset_rotation) * sizes[0], offset_position]) 400 | elif is_equal_approx(line_arc_start, line_arc_end): 401 | shape = PackedVector2Array([BasicGeometry2D._circle_point(line_arc_start + offset_rotation) * sizes[((line_arc_start / side_chord_arc_angle) as int) % line_count], offset_position]) 402 | else: 403 | shape = BasicGeometry2D.create_shape(line_count, sizes, offset_transform, line_arc_start, line_arc_end, false) 404 | shape.resize(shape.size() * 2) 405 | 406 | for i in shape.size() / 2: 407 | var index := shape.size() / 2 - i - 1 408 | var point := shape[index] 409 | shape[index * 2] = point 410 | shape[index * 2 + 1] = point.lerp(offset_position, ring_ratio) 411 | 412 | _queue_status = _QUEUE_DISPERSE 413 | _created_shape = shape 414 | _decomposed_created_shape = [shape] 415 | export() 416 | return 417 | 418 | var add_central_point := closing_method == ClosingMethod.SLICE or closing_method == ClosingMethod.ARC and is_equal_approx(ring_ratio, 1) 419 | shape = BasicGeometry2D.create_shape(vertices_count, sizes, offset_transform, arc_start, arc_end, add_central_point) 420 | 421 | if rounded_corners and shape.size() >= 3: 422 | if not uses_arc: 423 | BasicGeometry2D.add_rounded_corners(shape, corner_size, true_corner_detail) 424 | elif not round_arc_ends or round_arc_ends and closing_method == ClosingMethod.ARC and is_outline: 425 | BasicGeometry2D.add_rounded_corners(shape, corner_size, true_corner_detail, 1, shape.size() - (3 if add_central_point else 2)) 426 | elif closing_method == ClosingMethod.SLICE: 427 | BasicGeometry2D.add_rounded_corners(shape, corner_size, true_corner_detail, 0, shape.size() - 1) 428 | elif closing_method == ClosingMethod.CHORD: 429 | BasicGeometry2D.add_rounded_corners(shape, corner_size, true_corner_detail) 430 | elif closing_method == ClosingMethod.ARC and is_equal_approx(ring_ratio, 1): 431 | BasicGeometry2D.add_rounded_corners(shape, corner_size, true_corner_detail, 0, shape.size() - 1) 432 | 433 | if is_ring_shape: 434 | if not uses_arc or closing_method != ClosingMethod.SLICE: 435 | BasicGeometry2D.add_ring(shape, ring_ratio, offset_position, not uses_arc or closing_method == ClosingMethod.CHORD) 436 | else: # uses_arc and closing_strategy == ClosingStrategy.SLICE 437 | var arc_change := minf(TAU - arc_angle, -TAU * ring_ratio * (1 - arc_angle / TAU) / 4) 438 | var inner_arc_start := arc_start - arc_change / 2 439 | var inner_arc_end := arc_end + arc_change / 2 440 | if inner_arc_start < inner_arc_end: 441 | var inner_ring := BasicGeometry2D.create_shape(vertices_count, sizes, offset_transform, inner_arc_start, inner_arc_end, false) 442 | if is_equal_approx(inner_arc_end - inner_arc_start, TAU): 443 | inner_ring.push_back(inner_ring[0]) 444 | 445 | shape.resize(shape.size() + inner_ring.size() + 1) 446 | shape[-1] = offset_position 447 | for i in inner_ring.size(): 448 | shape[-i - 2] = inner_ring[i].lerp(offset_position, ring_ratio) 449 | 450 | if rounded_corners: 451 | var inner_corner_size := lerpf(corner_size, 0, ring_ratio) 452 | var inner_start := shape.size() - inner_ring.size() 453 | var inner_length := inner_ring.size() - 1 454 | if not round_arc_ends: 455 | inner_start += 1 456 | inner_length -= 2 457 | 458 | BasicGeometry2D.add_rounded_corners(shape, inner_corner_size, true_corner_detail, inner_start, inner_length, false) 459 | 460 | if rounded_corners and uses_arc and closing_method == ClosingMethod.ARC and round_arc_ends and is_ring_shape: 461 | var inner_corner_size := lerpf(corner_size, 0, ring_ratio) 462 | var original_size := shape.size() 463 | 464 | BasicGeometry2D.add_rounded_corners(shape, inner_corner_size, true_corner_detail, original_size / 2, original_size / 2) 465 | BasicGeometry2D.add_rounded_corners(shape, corner_size, true_corner_detail, 0, original_size / 2, false) 466 | 467 | if is_outline or shape.size() == 2: 468 | if (not uses_arc or closing_method != ClosingMethod.ARC) and shape.size() != 2: 469 | shape.push_back(shape[0]) 470 | 471 | decomposed_shape = [shape] 472 | 473 | _queue_status = _QUEUE_DISPERSE 474 | _created_shape = shape 475 | _decomposed_created_shape = decomposed_shape 476 | export() 477 | return 478 | 479 | if absf(arc_angle) <= PI and ring_ratio < 1 and ring_ratio > 0 and closing_method == ClosingMethod.CHORD: 480 | decomposed_shape = [shape] 481 | else: 482 | decomposed_shape = Geometry2D.decompose_polygon_in_convex(shape) 483 | 484 | # block _create_shape from queueing 'disperse' call. 485 | _queue_status = _QUEUE_DISPERSE 486 | _created_shape = shape 487 | _decomposed_created_shape = decomposed_shape 488 | export() 489 | 490 | func _get_property_list() -> Array[Dictionary]: 491 | var properties : Array[Dictionary] = [] 492 | properties.append({ 493 | name = "_created_shape", 494 | type = TYPE_PACKED_VECTOR2_ARRAY, 495 | usage = PROPERTY_USAGE_STORAGE 496 | }) 497 | properties.append({ 498 | name = "_decomposed_created_shape", 499 | type = TYPE_ARRAY, 500 | usage = PROPERTY_USAGE_STORAGE 501 | }) 502 | 503 | return properties 504 | 505 | ## Queue the [BasicPolygon2D] to export the shape. Multiple calls will be converted to a single call. 506 | func queue_export() -> void: 507 | if _queue_status >= _QUEUE_DISPERSE: 508 | return 509 | 510 | _queue_status = _QUEUE_DISPERSE 511 | 512 | await _find_tree().process_frame 513 | if _queue_status != _QUEUE_DISPERSE: 514 | return 515 | 516 | export() 517 | 518 | ## Instantly exports the previously created shape, emitting [signal shape_exported], 519 | ## as well as setting the export properties if [method can_export] returns [code]true[/code]. 520 | ## Removes queued [method queue_regenerate] and [method queue_export] calls. 521 | func export() -> void: 522 | _queue_status = _UNQUEUED 523 | 524 | shape_exported.emit(_created_shape, _decomposed_created_shape, get_created_shape_type()) 525 | if can_export(): 526 | var exported_objects : Variant = _decomposed_created_shape if export_as_decomposed_hulls else _created_shape 527 | for path in export_targets: 528 | var node := self if path.get_name_count() == 0 else get_node(NodePath(String(path.get_concatenated_names()))) 529 | assert(node != null) 530 | node.set_indexed(NodePath(String(path.get_concatenated_subnames())), exported_objects) 531 | 532 | if not Engine.is_editor_hint() and auto_free: 533 | queue_free() 534 | 535 | func _get_configuration_warnings() -> PackedStringArray: 536 | var warnings := PackedStringArray() 537 | if is_equal_approx(arc_start, arc_end): 538 | warnings.push_back("The arc of the shape is 0º, so nothing will be created") 539 | 540 | if absf(arc_angle) <= PI and ring_ratio < 1 and ring_ratio > 0 and closing_method == ClosingMethod.CHORD: 541 | warnings.push_back("A ring shape polygon that is closed as a chord with an arc angle less than or equal to 180º will not be a valid shape for the purposes of drawing and the like.") 542 | 543 | if vertices_count == 2 and not is_zero_approx(arc_angle): 544 | var line_count := maxi(sizes.size(), 2) 545 | var side_chord_arc_angle := TAU / line_count 546 | var line_arc_start := ceilf(arc_start / side_chord_arc_angle) * side_chord_arc_angle 547 | var line_arc_end := floorf(arc_end / side_chord_arc_angle) * side_chord_arc_angle 548 | if line_arc_start > line_arc_end: 549 | warnings.push_back("The arc of the shape covers an area where no lines are, so nothing will be created") 550 | 551 | for i in export_targets.size(): 552 | var path := export_targets[i] 553 | if path.is_empty(): 554 | warnings.push_back("The export path at index %s is unassigned." % i) 555 | continue 556 | 557 | var node_path := NodePath(String(path.get_concatenated_names())) 558 | var node := get_node_or_null(node_path) 559 | if node == null: 560 | warnings.push_back("The export path at index %s points to a non existant node" % i) 561 | continue 562 | 563 | if path.get_subname_count() == 0: 564 | warnings.push_back("The export path at index %s does not reference a property" % i) 565 | continue 566 | 567 | var previous_object : Variant = node 568 | var failure := false 569 | for i2 in path.get_subname_count() - 1: 570 | var property := path.get_subname(i2) 571 | if not (property in previous_object): 572 | warnings.push_back("The export path at index %s has a non-existant property reference at subname #%s (%s)" % [i, i2, property]) 573 | failure = true 574 | break 575 | 576 | previous_object = previous_object.get(property) 577 | if typeof(previous_object) != TYPE_OBJECT or previous_object == null: 578 | warnings.push_back("The export path at index %s has a property reference which is null or isn't of type Object at subname #%s (type: %s)" % [i, i2, "null" if previous_object == null else type_string(typeof(previous_object))]) 579 | failure = true 580 | break 581 | 582 | if failure: 583 | continue 584 | 585 | var last_i := path.get_subname_count() - 1 586 | var property := path.get_subname(last_i) 587 | if not (property in previous_object): 588 | warnings.push_back("The export path at index %s points to a non-existant property (%s)" % [i, path.get_concatenated_subnames()]) 589 | continue 590 | 591 | var type := typeof(previous_object.get(property)) 592 | if type != TYPE_PACKED_VECTOR2_ARRAY and type != TYPE_ARRAY: 593 | warnings.push_back("The export path at index %s points to a property that is either currently null or not an Array or PackedVector2Array (type: %s)" % [i, type_string(type)]) 594 | continue 595 | 596 | return warnings 597 | 598 | func _draw() -> void: 599 | if not draw_shape: 600 | return 601 | 602 | if is_zero_approx(arc_angle) or is_zero_approx(_created_shape.size()): 603 | return 604 | 605 | if absf(arc_angle) <= PI and ring_ratio < 1 and ring_ratio > 0 and closing_method == ClosingMethod.CHORD: 606 | return 607 | 608 | match get_created_shape_type(): 609 | ShapeType.POLYGON: 610 | if draw_border: 611 | if ring_ratio < 1 and (is_equal_approx(arc_angle, TAU) or closing_method == ClosingMethod.CHORD): 612 | assert(_created_shape.size() % 2 == 0) 613 | var border_line := _created_shape.slice(0, _created_shape.size() / 2) 614 | draw_polyline(border_line, border_color, border_width) 615 | 616 | for i in border_line.size(): 617 | border_line[i] = _created_shape[-i - 1] 618 | draw_polyline(border_line, border_color, border_width) 619 | elif is_zero_approx(border_width): 620 | draw_polyline(_created_shape, border_color) 621 | draw_line(_created_shape[-1], _created_shape[0], border_color) 622 | elif ring_ratio < 1 and arc_angle < TAU and closing_method == ClosingMethod.SLICE and _created_shape.size() > 3: 623 | var split := _created_shape.find(offset_position) 624 | assert(split != -1) 625 | 626 | var border_line := _created_shape.slice(0, split + 1) 627 | border_line.push_back(border_line[0]) 628 | draw_polyline(border_line, border_color, border_width) 629 | border_line = _created_shape.slice(split) 630 | draw_polyline(border_line, border_color, border_width) 631 | 632 | else: 633 | var border_line := _created_shape.duplicate() 634 | border_line.push_back(_created_shape[0]) 635 | draw_polyline(border_line, border_color, border_width) 636 | 637 | for hull in _decomposed_created_shape: 638 | draw_colored_polygon(hull, color) 639 | ShapeType.POLYLINE: 640 | draw_polyline(_created_shape, border_color if draw_border else color, border_width if border_width > 0 else -1) 641 | ShapeType.MULTILINE: 642 | draw_multiline(_created_shape, border_color if draw_border else color, border_width if border_width > 0 else -1) 643 | _: 644 | assert(false, "unexpected match case: %s" % get_created_shape_type()) 645 | --------------------------------------------------------------------------------