└── 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 |
--------------------------------------------------------------------------------