diff --git a/2d/gizmos/.editorconfig b/2d/gizmos/.editorconfig new file mode 100644 index 00000000000..f28239ba528 --- /dev/null +++ b/2d/gizmos/.editorconfig @@ -0,0 +1,4 @@ +root = true + +[*] +charset = utf-8 diff --git a/2d/gizmos/.gitattributes b/2d/gizmos/.gitattributes new file mode 100644 index 00000000000..8ad74f78d9c --- /dev/null +++ b/2d/gizmos/.gitattributes @@ -0,0 +1,2 @@ +# Normalize EOL for all files that Git considers text files. +* text=auto eol=lf diff --git a/2d/gizmos/.gitignore b/2d/gizmos/.gitignore new file mode 100644 index 00000000000..0af181cfb54 --- /dev/null +++ b/2d/gizmos/.gitignore @@ -0,0 +1,3 @@ +# Godot 4+ specific ignores +.godot/ +/android/ diff --git a/2d/gizmos/README.md b/2d/gizmos/README.md new file mode 100644 index 00000000000..87a6fa2a72b --- /dev/null +++ b/2d/gizmos/README.md @@ -0,0 +1,8 @@ +# Gizmo Plugins + +This demonstrates how to create 2D gizmo plugins to provide a nicer editing experience for +custom nodes. + +Language: GDScript + +Renderer: Compatibility diff --git a/2d/gizmos/addons/gizmo_plugins/circle_gizmo_plugin.gd b/2d/gizmos/addons/gizmo_plugins/circle_gizmo_plugin.gd new file mode 100644 index 00000000000..c8481258310 --- /dev/null +++ b/2d/gizmos/addons/gizmo_plugins/circle_gizmo_plugin.gd @@ -0,0 +1,211 @@ +# This is the gizmo plugin that provides editor support for our Circle node. +# We deliberately do not use the class_name declaration here to avoid polluting the +# global namespace with editor-only classes. +@tool +extends EditorCanvasItemGizmoPlugin + + +## This is the entry point into the gizmo system. We first need to tell the editor +## whether or not this plugin will support a specific node. This plugin only supports +## circle nodes, so we only return true if the given node is a circle node. +func _has_gizmo(for_canvas_item: CanvasItem) -> bool: + return for_canvas_item is Circle + + +## This function should return the name of the gizmo. This is used in the gizmo menu in the +## 2D editor to allow the user to show and hide the gizmo(s) created by this plugin. +func _get_gizmo_name() -> String: + return "Circle" + +## This function tells the editor whether this node has a bounding rectangle. +## If this returns true, then the editor will draw a boundary around the node and also provide +## scaling handles. +func _edit_use_rect(_gizmo: EditorCanvasItemGizmo) -> bool: + return true + +## This function tells the editor what the bounding rectangle of the node is. +## This is only called if _edit_use_rect returns true. +func _edit_get_rect(gizmo: EditorCanvasItemGizmo) -> Rect2: + # First we get the node we're editing + var circle:Circle = gizmo.get_canvas_item() + + # This is a centered circle, so the rectangle only depends on the radius. + var radius:float = circle.radius + return Rect2(Vector2(-radius, -radius) - circle.pivot, Vector2(radius * 2, radius * 2)) + +## If we override _edit_get_rect, we also must override _edit_set_rect. This +## function will be called by the editor if the user modifies the bounding +## rectangle, and we need to apply the new rectangle to our node. +func _edit_set_rect(gizmo: EditorCanvasItemGizmo, boundary: Rect2) -> void: + # Most of the time, we want to do the same thing as the built-in nodes. + # We look at how the bounding rectangle changed and modify the transform + # of the node. Because we need to do this often, there is a built-in helper + # method for this. + var old_boundary:Rect2 = _edit_get_rect(gizmo) + + # We get back a transform that represents the change... + var new_transform:Transform2D = boundary_change_to_transform(old_boundary, boundary) + + var circle:Circle = gizmo.get_canvas_item() + # ..and we can multiply this with the existing transform to get + # the new position and scale of the node. + circle.transform *= new_transform + + # The editor handles undo and redo for these size changes so + # that is not something we need to care about. + +## This tells the editor whether our canvas item has a custom pivot. +## Enabling this, will draw the custom pivot and allow the user to change it. +func _has_pivot(_gizmo: EditorCanvasItemGizmo) -> bool: + return true + +## Returns the position of the pivot relative to the node's position. Note that +## this must return the position where pivot should be drawn. +func _get_pivot(_gizmo: EditorCanvasItemGizmo) -> Vector2: + # Since our circle implements the pivot by offsetting the drawing, the + # pivot point is always at the node position, so we return Vector2.ZERO here. + return Vector2.ZERO + +## Updates the position of the pivot. The given given pivot is relative to the +## node's position. +func _set_pivot(gizmo: EditorCanvasItemGizmo, pivot: Vector2) -> void: + var circle:Circle = gizmo.get_canvas_item() + # The new pivot we get here is relative to the node position. Since + # we offset the circle drawing by the pivot, our pivot position is always + # at the node position. This means that the pivot we get is relative to + # our old pivot (which visually was at the node position). Therefore we add + # it to the circle's pivot rather than overwriting it. If you implement + # pivots differently, you may need to do different calculations here. + circle.pivot = circle.pivot + pivot + +## When dragging the pivot around, the editor constantly takes snapshots of the editor +## state and restores them before applying a new pivot. It also uses these snapshots +## to provide undo/redo for pivot movement, so we don't have to take care of this. +func _edit_get_state(gizmo: EditorCanvasItemGizmo) -> Dictionary: + var circle:Circle = gizmo.get_canvas_item() + # the base state (transform, etc.) is automatically saved from the + # underlying node, so we only need to add what is custom to our node. In our case + # this is just the pivot field. + return {"pivot" : circle.pivot } + +## The editor calls this when a snapshot is to be restored. Note that this implementation +## is called before the underlying canvas item's implementation, so we can be sure we see +## the exact same state that we had right after creating the snapshot. +func _edit_set_state(gizmo: EditorCanvasItemGizmo, state: Dictionary) -> void: + var circle:Circle = gizmo.get_canvas_item() + # Again, the underlying CanvasItem will restore the transform, so we only need + # to take care about the pivot. + circle.pivot = state.pivot + +## We can override _redraw to add custom selection shapes and handles that makes working +## with our nodes nicer in the editor. +func _redraw(gizmo: EditorCanvasItemGizmo) -> void: + # By default, the Godot editor has no idea how the shape of our + # custom node is. So it treats it as a single point, which makes selecting it + # in the 2D view rather difficult. We can add collision shapes, so the editor + # can actually pick something. + + # A simple way would be to just use the boundary as a collision + # shape (remove the comment from the following line to test it). + # gizmo.add_collision_rect(_edit_get_rect(gizmo)) + + # But that has the problem that if we click in the corners + # of that rect, where no circle exists, it will still get selected. + # So we rather create a collision shape that is closer to a circle. + # We use a 16 segment approximation here as it is good enough for + # our purposes. + var circle:Circle = gizmo.get_canvas_item() + + var circle_polygon:PackedVector2Array = [] + for i:int in 16: + var angle:float = i * TAU / 16.0 + circle_polygon.append( + (Vector2(cos(angle), sin(angle)) * circle.radius) + - circle.pivot # drawing is offset by the pivot, so we need to take this into account + ) + gizmo.add_collision_polygon(circle_polygon) + + # Lets also add a custom handle so we can edit the radius nicely + # in the editor rather than having to do it in the inspector. + # For this we use the add_handles function on the gizmo. We need + # to give it the position of all the handles we want to have for + # our node. We just need one for now. + + # Handle positions are relative to the node. We put the radius handle + # at a 45 degree angle, so it doesn't overlap with the scaling handles + var handle_pos:Vector2 = \ + Vector2(sin(PI/4.0), cos(PI/4.0)) * circle.radius \ + - circle.pivot + gizmo.add_handles([handle_pos]) + +## If we add custom handles, we should override this method to give the editor +## the name of the handle. This is used to show the user what will change if +## they drag the handle. +func _get_handle_name(_gizmo: EditorCanvasItemGizmo, handle_id: int, _secondary: bool) -> String: + # The handle id by default is its position in the handles array that we gave it + # in the add_handles call (see above). + if handle_id == 0: + return "Radius" + + # Should not happen since we only have one handle, but defensive coding + # doesn't hurt. + return "Unknown handle" + +## Overriding this method will allow the editor to get the value that is associated with +## the handle. The editor calls this when the user drags on a handle to show the current +## value to the user. The value is also used later to commit or abort a handle drag. +func _get_handle_value(gizmo: EditorCanvasItemGizmo, handle_id: int, _secondary: bool) -> Variant: + # Our only handle represents the radius, so we give the radius of our + # associated circle node back. + var circle:Circle = gizmo.get_canvas_item() + + if handle_id == 0: + return circle.radius + + # Again, should not happen. + return "?" + +## While the user is dragging the handle, the editor will repeatedly call this function +## with the updated position. It is then up to us to decide what the position change actually +## means and apply it to the node. +func _set_handle(gizmo: EditorCanvasItemGizmo, handle_id: int, _secondary: bool, position: Vector2) -> void: + # shouldn't happen, we only have one handle + if handle_id != 0: + return + + var circle:Circle = gizmo.get_canvas_item() + + # The position that we get is relative to the node, so we can just + # look at how far away from the center the user has dragged the handle + # to set the new radius + + # The center is offset by the pivot + var center := -circle.pivot + var new_radius:float = (position - center).length() + circle.radius = new_radius + +## Once the user releases the handle or aborts the handle movement, the editor will call this method +## so we can apply the change to the node or revert back to the original value. Unlike the position and +## size change, the editor cannot do undo/redo for us because it doesn't know what the handles actually +## change. So we need to handle this ourselves. +func _commit_handle(gizmo: EditorCanvasItemGizmo, handle_id: int, _secondary: bool, restore: Variant, cancel: bool) -> void: + if handle_id != 0: + return + + var circle:Circle = gizmo.get_canvas_item() + + # The cancel parameter tells us whether we need to revert the change or + # commit it. When reverting, we can simply apply the original value which is + # given to us in the restore parameter: + if cancel: + circle.radius = restore + # Since nothing has effectively changed, we don't need to add any undo/redo code. + return + + # Otherwise, we need to create an undo/redo action for the change: + var undo_redo:EditorUndoRedoManager = EditorInterface.get_editor_undo_redo() + undo_redo.create_action("Set radius ") + undo_redo.add_do_property(circle, "radius", circle.radius) + undo_redo.add_undo_property(circle, "radius", restore) + undo_redo.commit_action() + diff --git a/2d/gizmos/addons/gizmo_plugins/circle_gizmo_plugin.gd.uid b/2d/gizmos/addons/gizmo_plugins/circle_gizmo_plugin.gd.uid new file mode 100644 index 00000000000..b04ca6ae857 --- /dev/null +++ b/2d/gizmos/addons/gizmo_plugins/circle_gizmo_plugin.gd.uid @@ -0,0 +1 @@ +uid://r0m4boojbs8t diff --git a/2d/gizmos/addons/gizmo_plugins/editor_plugin.gd b/2d/gizmos/addons/gizmo_plugins/editor_plugin.gd new file mode 100644 index 00000000000..f5cf9a3bec8 --- /dev/null +++ b/2d/gizmos/addons/gizmo_plugins/editor_plugin.gd @@ -0,0 +1,27 @@ +@tool +## This editor plugin registers the gizmo plugins we use for editing our +## custom nodes. +extends EditorPlugin + +# We don't use class_name for editor classes to avoid polluting the +# global namespace with editor-only classes. Therefore we use the +# "old fashioned" way to include a class. Note how preload can use +# relative paths, this makes it a nicer choice over load here. +const CircleGizmoPlugin = preload("circle_gizmo_plugin.gd") +const FlowerGizmoPlugin = preload("flower_gizmo_plugin.gd") + +var _circle_gizmo_plugin:CircleGizmoPlugin +var _flower_gizmo_plugin:FlowerGizmoPlugin + +## Registers the gizmo plugins with the editor. +func _enter_tree() -> void: + _circle_gizmo_plugin = CircleGizmoPlugin.new() + add_canvas_item_gizmo_plugin(_circle_gizmo_plugin) + + _flower_gizmo_plugin = FlowerGizmoPlugin.new() + add_canvas_item_gizmo_plugin(_flower_gizmo_plugin) + +## Unregisters the gizmo plugins from, the editor. +func _exit_tree() -> void: + remove_canvas_item_gizmo_plugin(_circle_gizmo_plugin) + remove_canvas_item_gizmo_plugin(_flower_gizmo_plugin) diff --git a/2d/gizmos/addons/gizmo_plugins/editor_plugin.gd.uid b/2d/gizmos/addons/gizmo_plugins/editor_plugin.gd.uid new file mode 100644 index 00000000000..a4cdec55d4c --- /dev/null +++ b/2d/gizmos/addons/gizmo_plugins/editor_plugin.gd.uid @@ -0,0 +1 @@ +uid://s417m7tefo4u diff --git a/2d/gizmos/addons/gizmo_plugins/flower_gizmo_plugin.gd b/2d/gizmos/addons/gizmo_plugins/flower_gizmo_plugin.gd new file mode 100644 index 00000000000..2801d713395 --- /dev/null +++ b/2d/gizmos/addons/gizmo_plugins/flower_gizmo_plugin.gd @@ -0,0 +1,217 @@ +# This is the gizmo plugin that provides editor support for our Flower node. +# It uses the same principles and functions that the Circle gizmo plugin uses +# so have a look at the Circle gizmo plugin first to get an overview on how +# these functions work. +# +# This plugin adds subgizmos. Subgizmos are similar to handles, but unlike handles, +# they can be selected and transformed - handles can only be moved but not selected. +# Usually you will use subgizmos for sub-structure parts of the node. In this example we +# use them for the flower petals. Other good use cases for subgizmos would be the +# vertices of a path or a polygon, while e.g. the tangent handles of a bezier curve would +# rather be done with handles. +@tool +extends EditorCanvasItemGizmoPlugin + +func _has_gizmo(for_canvas_item: CanvasItem) -> bool: + return for_canvas_item is Flower + +func _get_gizmo_name() -> String: + return "Flower" + +## The flower isn't supposed to be user scalable. We can only move the node and each +## individual petal. So this method returns false, and the editor will not show transform +## gizmos for this node. +func _edit_use_rect(_gizmo: EditorCanvasItemGizmo) -> bool: + return false + + +func _redraw(gizmo: EditorCanvasItemGizmo) -> void: + var flower:Flower = gizmo.get_canvas_item() + + # We add selection shapes for the flower disk and petals so the user can + # easily pick any part of the flower. + gizmo.add_collision_polygon(_calculate_collision_circle(flower.radius, Transform2D())) + + for i:int in flower._petals.size(): + var petal:Transform2D = flower._petals[i] + var polygon:PackedVector2Array = _calculate_collision_circle(flower.radius, petal) + gizmo.add_collision_polygon(polygon) + + # We also draw an overlay to show the currently selected + # petals. Petals are subgizmos. See the subgizmo selection + # functions below. + if gizmo.is_subgizmo_selected(i): + gizmo.add_polygon(polygon, Color(0.39215687, 0.58431375, 0.92941177, 0.8)) + + + # Like for the circle node, lets add a handle to conveniently set the + # radius of the flower disk. + var handle_pos:Vector2 = Vector2(sin(PI/4.0), cos(PI/4.0)) * flower.radius + gizmo.add_handles([handle_pos]) + + +## Since we need the collision circles also for our subgizmos (see below), we have a helper method +## for calculating these. +func _calculate_collision_circle(radius:float, transform:Transform2D) -> PackedVector2Array: + # Collision polygon is calculated very similar to how it's done for the circle + var circle_polygon:PackedVector2Array = [] + for i:int in 16: + var angle:float = i * TAU / 16.0 + var point:Vector2 = Vector2(cos(angle), sin(angle)) * radius + # except we multiply it with the transform to get the position and shape of the petals. + circle_polygon.append(transform * point) + + return circle_polygon + +## This function is called when the user clicks in the editor. We can override it to +## return the ID of a subgizmo that exists at the given location. For our flower, the +## petals are subgizmos. So when given point is over a petal, we return the index of +## that petal. +func _subgizmos_intersect_point(gizmo: EditorCanvasItemGizmo, point: Vector2, _distance: float) -> int: + var flower:Flower = gizmo.get_canvas_item() + + # We walk over the petals, create a collision polygon for each and check if the + # given point is inside. If this is the case we return the index of the petal as our + # subgizmo ID. The point we get is already in local coordinates, so we don't need + # to do any extra calculations + for i:int in flower._petals.size(): + var petal:Transform2D = flower._petals[i] + var collision_polygon:PackedVector2Array = _calculate_collision_circle(flower.radius, petal) + if Geometry2D.is_point_in_polygon(point, collision_polygon): + return i + + return -1 + +## This function is called when the user does a subgizmo rectangle selection (e.g. shift + drag). +## We can override this to return the IDs of all subgizmos that are inside of the rect. +func _subgizmos_intersect_rect(gizmo: EditorCanvasItemGizmo, rect: Rect2) -> PackedInt32Array: + var flower:Flower = gizmo.get_canvas_item() + var result:PackedInt32Array = [] + + # We can in principle use the same approach here as we did in _subgizmos_intersect_point. + # However the rectangle is given in canvas coordinates and not in local coordinates, because + # it represents a rectangular selection on the canvas. So we need to find all petals that + # have an overlap with this rectangle. To do this, we need the flower's global transform + # to calculate collision shapes in canvas space. + var global_transform:Transform2D = flower.global_transform + + # We also need a polygon representation of the selection rect to calculate the overlap later. + var rect_shape:PackedVector2Array = [] + + # Godot expects polygons to be clockwise, so that's what we do here. + rect_shape.append(rect.position) + rect_shape.append(rect.position + Vector2(0, rect.size.y)) + rect_shape.append(rect.position + rect.size) + rect_shape.append(rect.position + Vector2(rect.size.x, 0)) + + for i in flower._petals.size(): + # apply the global transform of the flower, so we get a collision shape in global + # coordinates. + var petal_global:Transform2D = global_transform * flower._petals[i] + var collision_polygon:PackedVector2Array = _calculate_collision_circle(flower.radius, petal_global) + + # Now we can perform an intersect operation between the rect shape and the collision polygon. + # If this is not empty, we have an overlap. + var overlap:Array[PackedVector2Array] = Geometry2D.intersect_polygons(rect_shape, collision_polygon) + if not overlap.is_empty(): + result.append(i) + + return result + +## This is called by the editor to get the transform behind a subgizmo. So this method is +## similar to _get_handle_value, that gets called for handles. Unlike handles, which can +## represent any value, subgizmos always represent transforms. In our case we return the +## transforms of our flower petals +func _get_subgizmo_transform(gizmo: EditorCanvasItemGizmo, subgizmo_id: int) -> Transform2D: + var flower:Flower = gizmo.get_canvas_item() + return flower._petals[subgizmo_id] + +## This is called by the editor got apply a new transform to a subgizmo after the user has +## edited it in the editor. This is similar to _set_handle that gets called for handles. +func _set_subgizmo_transform(gizmo: EditorCanvasItemGizmo, subgizmo_id: int, transform: Transform2D) -> void: + var flower:Flower = gizmo.get_canvas_item() + flower._petals[subgizmo_id] = transform + flower._repaint() + +## This is called by the editor when the user finishes or aborts subgizmo editing. It works very +## similar to _commit_handle. So we get the IDs of the modified subgizmos and a matching array with +## how their transforms were when the modification began. The cancel parameter tells us, if the user +## cancelled the action. Like with handles, we need to do the undo/redo part ourselves here as the +## editor cannot know how the transformed subgizmos are represented internally and what needs to be +## done to undo/redo this change. +func _commit_subgizmos(gizmo: EditorCanvasItemGizmo, ids: PackedInt32Array, restores: Array[Transform2D], cancel: bool) -> void: + var flower:Flower = gizmo.get_canvas_item() + if cancel: + # if the operation was cancelled, we simply undo the change and set back our + # initial transforms. + for i:int in ids.size(): + var subgizmo_id:int = ids[i] + var old_transform:Transform2D = restores[i] + flower._petals[subgizmo_id] = old_transform + + flower._repaint() + # Since nothing really changed, we don't need to do any undo/redo here. + return + + # For the undo part we need to build the undo array ourselves. So we + # take the current state, apply the restores and use that as undo state + var undo_petals:Array[Transform2D] = flower._petals.duplicate() + for i:int in ids.size(): + var subgizmo_id:int = ids[i] + var old_transform:Transform2D = restores[i] + undo_petals[subgizmo_id] = old_transform + + + var undo_redo:EditorUndoRedoManager = EditorInterface.get_editor_undo_redo() + undo_redo.create_action("Set petals") + undo_redo.add_do_property(flower, "_petals", flower._petals) + undo_redo.add_do_method(flower, "_repaint" ) + undo_redo.add_undo_property(flower, "_petals", undo_petals) + undo_redo.add_undo_method(flower, "_repaint" ) + undo_redo.commit_action() + + + +# The handle management for the radius is very similar to the Circle node. Have a look there +# for details on how handles work. We're leaving the details out here for brevity. + +func _get_handle_name(_gizmo: EditorCanvasItemGizmo, handle_id: int, _secondary: bool) -> String: + if handle_id == 0: + return "Radius" + + return "Unknown handle" + +func _get_handle_value(gizmo: EditorCanvasItemGizmo, handle_id: int, _secondary: bool) -> Variant: + var flower:Flower = gizmo.get_canvas_item() + + if handle_id == 0: + return flower.radius + + # Again, should not happen. + return "?" + +func _set_handle(gizmo: EditorCanvasItemGizmo, handle_id: int, _secondary: bool, position: Vector2) -> void: + if handle_id != 0: + return + + var flower:Flower = gizmo.get_canvas_item() + + var new_radius:float = position.length() + flower.radius = new_radius + +func _commit_handle(gizmo: EditorCanvasItemGizmo, handle_id: int, _secondary: bool, restore: Variant, cancel: bool) -> void: + if handle_id != 0: + return + + var flower:Flower = gizmo.get_canvas_item() + + if cancel: + flower.radius = restore + return + + var undo_redo:EditorUndoRedoManager = EditorInterface.get_editor_undo_redo() + undo_redo.create_action("Set radius ") + undo_redo.add_do_property(flower, "radius", flower.radius) + undo_redo.add_undo_property(flower, "radius", restore) + undo_redo.commit_action() + diff --git a/2d/gizmos/addons/gizmo_plugins/flower_gizmo_plugin.gd.uid b/2d/gizmos/addons/gizmo_plugins/flower_gizmo_plugin.gd.uid new file mode 100644 index 00000000000..301bc169f0e --- /dev/null +++ b/2d/gizmos/addons/gizmo_plugins/flower_gizmo_plugin.gd.uid @@ -0,0 +1 @@ +uid://ll6jcrl8o5tx diff --git a/2d/gizmos/addons/gizmo_plugins/plugin.cfg b/2d/gizmos/addons/gizmo_plugins/plugin.cfg new file mode 100644 index 00000000000..d29ec4ea15e --- /dev/null +++ b/2d/gizmos/addons/gizmo_plugins/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="Gizmo Plugins" +description="An example plugin showing how to write 2D gizmo plugins." +author="Jan Thomä" +version="1.0" +script="editor_plugin.gd" diff --git a/2d/gizmos/circle/circle.gd b/2d/gizmos/circle/circle.gd new file mode 100644 index 00000000000..9ca3d8e3c87 --- /dev/null +++ b/2d/gizmos/circle/circle.gd @@ -0,0 +1,35 @@ +@tool +class_name Circle +extends Node2D + +## The radius of the circle +@export var radius:float = 100: + set(value): + # don't allow negative values + radius = max(0, value) + queue_redraw() + + +## The color of the circle +@export var color:Color: + set(value): + color = value + queue_redraw() + +## The pivot point of the circle, relative to the center. +@export var pivot:Vector2: + set(value): + # since a pivot is basically a virtual Node2D inbetween, + # we shift our own position here, so the pivot position + # happens to be at local (0,0). + + # first undo any old displacement by the previous pivot + global_position = global_transform * (-pivot) + # then move to handle the displacement of the new pivot + global_position = global_transform * (value) + pivot = value + queue_redraw() + + +func _draw() -> void: + draw_circle(-pivot, radius, color) diff --git a/2d/gizmos/circle/circle.gd.uid b/2d/gizmos/circle/circle.gd.uid new file mode 100644 index 00000000000..5958e534791 --- /dev/null +++ b/2d/gizmos/circle/circle.gd.uid @@ -0,0 +1 @@ +uid://dn1ng5xs46v62 diff --git a/2d/gizmos/circle/circle.tscn b/2d/gizmos/circle/circle.tscn new file mode 100644 index 00000000000..2fd70375b99 --- /dev/null +++ b/2d/gizmos/circle/circle.tscn @@ -0,0 +1,11 @@ +[gd_scene format=3 uid="uid://cchcf6jfu0pi"] + +[ext_resource type="Script" uid="uid://dn1ng5xs46v62" path="res://circle/circle.gd" id="1_top1s"] + +[node name="CircleExample" type="Node2D" unique_id=1745448022] + +[node name="Circle" type="Node2D" parent="." unique_id=1666963409] +script = ExtResource("1_top1s") +radius = 204.09483 +color = Color(0.53184503, 0.52447194, 0.1520774, 1) +metadata/_custom_type_script = "uid://dn1ng5xs46v62" diff --git a/2d/gizmos/flower/flower.gd b/2d/gizmos/flower/flower.gd new file mode 100644 index 00000000000..c230330cf2a --- /dev/null +++ b/2d/gizmos/flower/flower.gd @@ -0,0 +1,124 @@ +@tool +class_name Flower +extends Node2D + +## This holds the amount and transforms of the petals +@export_storage var _petals:Array[Transform2D] = [] + +## The radius of the flower disk. +@export var radius:float = 100: + set(value): + radius = max(0, value) + _repaint() + +## The color of the flower disk. +@export var disk_color:Color = Color.BROWN: + set(value): + disk_color = value + _repaint() + +## The color of the petals. +@export var petal_color:Color = Color.YELLOW: + set(value): + petal_color = value + _repaint() + +## Tool button to add a new petal to the flower. +@export_tool_button("Add petal") var add_petal:Callable = _add_petal +## Tool button to remove a petal from the flower. +@export_tool_button("Remove petal") var remove_petal:Callable = _remove_petal +## Tool button to rearrange all petals to the default layout. +@export_tool_button("Arrange petals") var arrange_petals:Callable = _arrange_petals + +## The child circle node which we use to draw the core. +var _disk_circle:Circle +## The child circle nodes which we use to draw the petals. +var _petal_circles:Array[Circle] = [] + +func _ready() -> void: + _repaint() + +## Adds a new petal to the flower. +func _add_petal() -> void: + var new_petals:Array[Transform2D] = _petals.duplicate() + var angle:float = TAU / float(_petals.size() + 1) + + new_petals.append( + Transform2D() + .scaled(Vector2(0.5, 1)) + .translated(Vector2(0, 1.75 * radius)) + .rotated(_petals.size() * angle) + ) + # We handle undo/redo code in a single place, so we call that here. + _set_petals("Add petal", new_petals) + +## Removes a petal from the flower. If the flower has no petals +## does nothing. +func _remove_petal() -> void: + if _petals.size() > 0: + _set_petals("Remove petal", _petals.slice(0, -1)) + +## Arranges the petals of the flower in the default style. +func _arrange_petals() -> void: + if _petals.is_empty(): + return + + var angle:float = TAU / float(_petals.size()) + var new_petals:Array[Transform2D] = [] + + for i in _petals.size(): + new_petals.append( + Transform2D() + .scaled(Vector2(0.5, 1)) + .translated(Vector2(0, 1.75 * radius)) + .rotated(i * angle) + ) + + _set_petals("Arrange petals", new_petals) + +## Updates petal transforms and provides undo/redo support for this in the editor. +func _set_petals(description:String, new_petals:Array[Transform2D]) -> void: + if Engine.is_editor_hint(): + # When called inside the editor, we do proper undo/redo. + var undo_redo:EditorUndoRedoManager = EditorInterface.get_editor_undo_redo() + undo_redo.create_action(description) + undo_redo.add_do_property(self, "_petals", new_petals) + undo_redo.add_do_method(self, "_repaint") + undo_redo.add_undo_property(self, "_petals", _petals) + undo_redo.add_undo_method(self, "_repaint") + undo_redo.commit_action() + else: + # Outside the editor, just apply the new value and repaint. + _petals = new_petals + _repaint() + + +## Updates the circle nodes we use to draw the petals. +func _repaint() -> void: + # Ensure we have a disk circle child node + if not is_instance_valid(_disk_circle): + _disk_circle = Circle.new() + add_child(_disk_circle) + + # Update disk size and color + _disk_circle.color = disk_color + _disk_circle.radius = radius + _disk_circle.z_index = 1 + + for i in _petals.size(): + # Ensure we have a circle for that petal + if _petal_circles.size() <= i: + var circle:Circle = Circle.new() + add_child(circle) + _petal_circles.append(circle) + + # Update petal color, size and positioning + _petal_circles[i].color = petal_color + _petal_circles[i].radius = radius + _petal_circles[i].transform = _petals[i] + + # Remove any extra petal circles we may have + while _petal_circles.size() > _petals.size(): + var circle:Circle = _petal_circles.pop_back() + circle.queue_free() + diff --git a/2d/gizmos/flower/flower.gd.uid b/2d/gizmos/flower/flower.gd.uid new file mode 100644 index 00000000000..8c77d0c0084 --- /dev/null +++ b/2d/gizmos/flower/flower.gd.uid @@ -0,0 +1 @@ +uid://2c0renfiv8bd diff --git a/2d/gizmos/flower/flower.tscn b/2d/gizmos/flower/flower.tscn new file mode 100644 index 00000000000..b36df07cbd8 --- /dev/null +++ b/2d/gizmos/flower/flower.tscn @@ -0,0 +1,10 @@ +[gd_scene format=3 uid="uid://dg2jwwv2aokge"] + +[ext_resource type="Script" uid="uid://2c0renfiv8bd" path="res://flower/flower.gd" id="1_l8ytj"] + +[node name="Flower" type="Node2D" unique_id=2051816069] +script = ExtResource("1_l8ytj") +_petals = Array[Transform2D]([Transform2D(0.6801224, 0, 0, 1.3361742, -6.8711233e-07, 119.12494), Transform2D(0.35355338, 0.35355338, -0.70710677, 0.70710677, -63.041218, 63.041218), Transform2D(-2.9729094e-08, 0.6680871, -1.3602448, -5.840603e-08, -121.27092, -4.5659153e-06), Transform2D(-0.35355338, 0.35355338, -0.70710677, -0.70710677, -63.041218, -63.041218), Transform2D(-0.6801224, -5.840603e-08, 1.18916375e-07, -1.3361742, 9.914728e-06, -119.12494), Transform2D(-0.3535534, -0.35355335, 0.7071067, -0.7071068, 63.041214, -63.041225), Transform2D(8.110378e-09, -0.6680871, 1.3602448, 1.5933718e-08, 121.27092, 2.0617522e-06), Transform2D(0.35355335, -0.35355344, 0.7071069, 0.7071067, 63.04123, 63.041214)]) +radius = 72.28762 +disk_color = Color(0.76, 0.26523998, 0.053199995, 1) +petal_color = Color(0.9607843, 0.77254903, 0.09411765, 1) diff --git a/2d/gizmos/icon.svg b/2d/gizmos/icon.svg new file mode 100644 index 00000000000..c6bbb7d820d --- /dev/null +++ b/2d/gizmos/icon.svg @@ -0,0 +1 @@ + diff --git a/2d/gizmos/icon.svg.import b/2d/gizmos/icon.svg.import new file mode 100644 index 00000000000..034ed7b9885 --- /dev/null +++ b/2d/gizmos/icon.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dpbxdt3mf4eud" +path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://icon.svg" +dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/2d/gizmos/project.godot b/2d/gizmos/project.godot new file mode 100644 index 00000000000..1e42292abc2 --- /dev/null +++ b/2d/gizmos/project.godot @@ -0,0 +1,34 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="gizmos" +config/features=PackedStringArray("4.7", "Forward Plus") +config/icon="res://icon.svg" + +[debug] + +gdscript/warnings/directory_rules={} +gdscript/warnings/untyped_declaration=1 + +[editor_plugins] + +enabled=PackedStringArray("res://addons/gizmo_plugins/plugin.cfg") + +[physics] + +3d/physics_engine="Jolt Physics" + +[rendering] + +rendering_device/driver.windows="d3d12" +renderer/rendering_method="gl_compatibility" +renderer/rendering_method.mobile="gl_compatibility"