diff --git a/doc/classes/CanvasItem.xml b/doc/classes/CanvasItem.xml
index 9f125f385a4f..b756410260b4 100644
--- a/doc/classes/CanvasItem.xml
+++ b/doc/classes/CanvasItem.xml
@@ -23,6 +23,26 @@
Corresponds to the [constant NOTIFICATION_DRAW] notification in [method Object._notification].
+
+
+
+
+ Attaches the given [param gizmo] to this node. Only works in the editor.
+ [b]Note:[/b] [param gizmo] should be an [EditorCanvasItemGizmo]. The argument type is [CanvasItemGizmo] to avoid depending on editor classes in [CanvasItem].
+
+
+
+
+
+ Clears all [EditorCanvasItemGizmo] objects attached to this node. Only works in the editor.
+
+
+
+
+
+ Deselects all subgizmos for this node. Useful to call when the selected subgizmo may no longer exist after a property change. Only works in the editor.
+
+
@@ -485,6 +505,12 @@
Returns the transform of this node, converted from its registered canvas's coordinate system to its viewport's coordinate system. See also [method Node.get_viewport].
+
+
+
+ Returns all the [CanvasItemGizmo] objects attached to this node. Only works in the editor.
+
+
@@ -638,6 +664,16 @@
[b]Note:[/b] Many canvas items such as [Camera2D] or [Light2D] automatically enable this in order to function correctly.
+
+
+
+
+
+
+ Selects the [param gizmo]'s subgizmo with the given [param id] and sets its transform. Only works in the editor.
+ [b]Note:[/b] The gizmo object would typically be an instance of [EditorCanvasItemGizmo], but the argument type is kept generic to avoid creating a dependency on editor classes in [CanvasItem].
+
+
@@ -653,6 +689,12 @@
[b]Note:[/b] For controls that inherit [Popup], the correct way to make them visible is to call one of the multiple [code]popup*()[/code] functions instead.
+
+
+
+ Updates all the [CanvasItemGizmo] objects attached to this node. Only works in the editor.
+
+
diff --git a/doc/classes/CanvasItemGizmo.xml b/doc/classes/CanvasItemGizmo.xml
new file mode 100644
index 000000000000..24410682c18b
--- /dev/null
+++ b/doc/classes/CanvasItemGizmo.xml
@@ -0,0 +1,12 @@
+
+
+
+ Abstract class to expose editor gizmos for [CanvasItem].
+
+
+ This abstract class helps connect the [CanvasItem] scene with the editor-specific [EditorCanvasItemGizmo] class.
+ [CanvasItemGizmo] by itself has no exposed API, refer to [method CanvasItem.add_gizmo] and pass it an [EditorCanvasItemGizmo] instance.
+
+
+
+
diff --git a/doc/classes/EditorCanvasItemGizmo.xml b/doc/classes/EditorCanvasItemGizmo.xml
new file mode 100644
index 000000000000..f987866c13aa
--- /dev/null
+++ b/doc/classes/EditorCanvasItemGizmo.xml
@@ -0,0 +1,283 @@
+
+
+
+ Gizmo for editing [CanvasItem] objects.
+
+
+ Gizmo that is used for providing custom visualization and editing (handles and subgizmos) for [CanvasItem] objects. Can be overridden to create custom gizmos, but for simple gizmos creating an [EditorCanvasItemGizmoPlugin] is usually recommended.
+
+
+
+
+
+
+
+
+
+ Override this method to run code when the user starts dragging a handle (handles must have been previously added by [method add_handles]).
+
+
+
+
+
+
+
+
+
+ Override this method to commit a handle being edited (handles must have been previously added by [method add_handles]). This usually means creating an [UndoRedo] action for the change, using the current handle value as "do" and the [param restore] argument as "undo".
+ If the [param cancel] argument is [code]true[/code], the [param restore] value should be directly set, without any [UndoRedo] action.
+ The [param secondary] argument is [code]true[/code] when the committed handle is secondary (see [method add_handles] for more information).
+
+
+
+
+
+
+
+
+ Override this method to commit a group of subgizmos being edited (see [method _subgizmos_intersect_point] and [method _subgizmos_intersect_rect]). This usually means creating an [UndoRedo] action for the change, using the current transforms as "do" and the [param restores] transforms as "undo".
+ If the [param cancel] argument is [code]true[/code], the [param restores] transforms should be directly set, without any [UndoRedo] action.
+
+
+
+
+
+ Override this method to return the bounding rectangle of the CanvasItem. The returned value is used to draw the selection rectangle
+ and provide handles to change the bounding rectangle.
+
+
+
+
+
+ Override this method to return the current state of the gizmo. This is used to restore the gizmo to its previous state when the user cancels an edit.
+ [b]Note:[/b] The underlying CanvasItem may have its own state saved. To preserve editor functionality, the built-in state has precedence over state saved by the Gizmo.
+
+
+
+
+
+
+ Override this method to apply a new boundary to the CanvasItem. It is called by the editor when the user changes the bounding rectangle.
+ [b]Note:[/b] The editor fully handles undo/redo for this method calling [method _edit_set_state] as necessary. You only need to apply the change to the node's transform.
+
+
+
+
+
+
+ Override this method to restore the gizmo to its previous state when the user cancels an edit.
+ [b]Note:[/b] The underlying CanvasItem may have its own state saved. To preserve editor functionality, the built-in state has precedence over state saved by the Gizmo. The dictionary given to this method may contain additional items to what was saved in [method _edit_get_state].
+
+
+
+
+
+ Override this method to return [code]true[/code] if the gizmo should be drawn with a bounding rectangle. This is used to draw the selection rectangle
+ and provide handles to change the bounding rectangle.
+
+
+
+
+
+
+
+ Override this method to return the name of an edited handle (handles must have been previously added by [method add_handles]). Handles can be named for reference to the user when editing.
+ The [param secondary] argument is [code]true[/code] when the requested handle is secondary (see [method add_handles] for more information).
+
+
+
+
+
+
+
+ Override this method to return the current value of a handle. This value will be requested at the start of an edit and used as the [code]restore[/code] argument in [method _commit_handle].
+ The [param secondary] argument is [code]true[/code] when the requested handle is secondary (see [method add_handles] for more information).
+
+
+
+
+
+ Override this method to return the pivot point of the CanvasItem. This is used to draw the pivot point and provide handles to change the pivot point.
+
+
+
+
+
+
+ Override this method to return the current transform of a subgizmo. This transform will be requested at the start of an edit and used as the [code]restore[/code] argument in [method _commit_subgizmos].
+
+
+
+
+
+ Override this method to return [code]true[/code] if the CanvasItem has a pivot point. This is used to draw the pivot point and provide handles to change the pivot point.
+
+
+
+
+
+
+
+ Override this method to return [code]true[/code] whenever the given handle should be highlighted in the editor.
+ The [param secondary] argument is [code]true[/code] when the requested handle is secondary (see [method add_handles] for more information).
+
+
+
+
+
+ Override this method to add all the gizmo elements whenever a gizmo update is requested. It's common to call [method clear] at the beginning of this method and then add visual elements depending on the node's properties.
+
+
+
+
+
+
+
+
+ Override this method to update the node properties when the user drags a gizmo handle (previously added with [method add_handles]). The provided [param point] is the mouse position in screen coordinates.
+ The [param secondary] argument is [code]true[/code] when the edited handle is secondary (see [method add_handles] for more information).
+
+
+
+
+
+
+ Override this method to set the pivot point of the CanvasItem when the user drags the pivot handle.
+
+
+
+
+
+
+
+ Override this method to update the node properties during subgizmo editing (see [method _subgizmos_intersect_point] and [method _subgizmos_intersect_rect]). The [param transform] is given in the [CanvasItem]'s local coordinate system.
+
+
+
+
+
+
+
+ Override this method to allow selecting subgizmos using mouse clicks. Given a [param point] and a [param distance] in local coordinates (relative to the CanvasItem), this method should return which subgizmo should be selected. The returned value should be a unique subgizmo identifier, which can have any non-negative value and will be used in other virtual methods like [method _get_subgizmo_transform] or [method _commit_subgizmos].
+
+
+
+
+
+
+ Override this method to allow selecting subgizmos using mouse clicks. Given a [param rect] in global coordinates (canvas coordinates), this method should return which subgizmos should be selected. The returned value should be an array of unique subgizmo identifiers, which can have any non-negative value and will be used in other virtual methods like [method _get_subgizmo_transform] or [method _commit_subgizmos].
+
+
+
+
+
+
+
+
+ Draw a circle at the given [param position] with the given [param radius] and [param color]. Call this method during [method _redraw].
+
+
+
+
+
+
+ Adds the specified [param polygon] to the gizmo's collision shape for picking. Call this method during [method _redraw].
+
+
+
+
+
+
+ Adds the specified [param rect] to the gizmo's collision shape for picking. Call this method during [method _redraw].
+
+
+
+
+
+
+ Adds the specified [param segments] to the gizmo's collision shape for picking. Call this method during [method _redraw].
+
+
+
+
+
+
+
+
+
+ Adds a list of handles (points) which can be used to edit the properties of the gizmo's [CanvasItem]. The [param ids] argument can be used to specify a custom identifier for each handle, if an empty array is passed, the ids will be assigned automatically from the [param handles] argument order.
+ The [param secondary] argument marks the added handles as secondary, meaning they will normally have lower selection priority than regular handles. When the user is holding the shift key secondary handles will switch to have higher priority than regular handles. This change in priority can be used to place multiple handles at the same point while still giving the user control on their selection.
+ There are virtual methods which will be called upon editing of these handles. Call this method during [method _redraw].
+
+
+
+
+
+
+
+ Draws a polygon with the given [param points] and [param color]. Call this method during [method _redraw].
+
+
+
+
+
+
+
+ Draws a polyline with the given [param points] and [param color]. Call this method during [method _redraw].
+
+
+
+
+
+
+
+ Draws a rectangle with the given [param rect] and [param color]. Call this method during [method _redraw].
+
+
+
+
+
+ Removes everything in the gizmo including meshes, collisions and handles.
+
+
+
+
+
+ Returns the [CanvasItem] node associated with this gizmo.
+
+
+
+
+
+ Returns the [EditorCanvasItemGizmoPlugin] that owns this gizmo.
+
+
+
+
+
+ Returns a list of the currently selected subgizmos. Can be used to highlight selected elements during [method _redraw].
+
+
+
+
+
+
+ Returns [code]true[/code] if the given subgizmo is currently selected. Can be used to highlight selected elements during [method _redraw].
+
+
+
+
+
+
+ Sets the reference [CanvasItem] node for the gizmo. [param canvas_item] must inherit from [CanvasItem].
+
+
+
+
+
+
+ Sets the gizmo's visibility. If [code]true[/code], the gizmo will be shown. If [code]false[/code], it will be hidden.
+
+
+
+
diff --git a/doc/classes/EditorCanvasItemGizmoPlugin.xml b/doc/classes/EditorCanvasItemGizmoPlugin.xml
new file mode 100644
index 000000000000..4ef3895c5664
--- /dev/null
+++ b/doc/classes/EditorCanvasItemGizmoPlugin.xml
@@ -0,0 +1,239 @@
+
+
+
+ A class used by the editor to define CanvasItem gizmo types.
+
+
+ [EditorCanvasItemGizmoPlugin] allows you to define a new type of Gizmo. There are two main ways to do so: extending [EditorCanvasItemGizmoPlugin] for the simpler gizmos, or creating a new [EditorCanvasItemGizmo] type.
+ To use [EditorCanvasItemGizmoPlugin], register it using the [method EditorPlugin.add_canvas_item_gizmo_plugin] method first.
+
+
+
+
+
+
+
+
+
+
+ Override this method to run code when the user starts dragging a handle (handles must have been previously added by [method EditorCanvasItemGizmo.add_handles]).
+
+
+
+
+
+ Override this method to define whether the gizmos handled by this plugin can be hidden or not. Returns [code]true[/code] if not overridden.
+
+
+
+
+
+
+
+
+
+
+ Override this method to commit a handle being edited (handles must have been previously added by [method EditorCanvasItemGizmo.add_handles] during [method _redraw]). This usually means creating an [UndoRedo] action for the change, using the current handle value as "do" and the [param restore] argument as "undo".
+ If the [param cancel] argument is [code]true[/code], the [param restore] value should be directly set, without any [UndoRedo] action.
+ The [param secondary] argument is [code]true[/code] when the committed handle is secondary (see [method EditorCanvasItemGizmo.add_handles] for more information).
+ Called for this plugin's active gizmos.
+
+
+
+
+
+
+
+
+
+ Override this method to commit a group of subgizmos being edited (see [method _subgizmos_intersect_point] and [method _subgizmos_intersect_rect]). This usually means creating an [UndoRedo] action for the change, using the current transforms as "do" and the [param restores] transforms as "undo".
+ If the [param cancel] argument is [code]true[/code], the [param restores] transforms should be directly set, without any [UndoRedo] action. As with all subgizmo methods, transforms are given in local space respect to the gizmo's CanvasItem. Called for this plugin's active gizmos.
+
+
+
+
+
+
+ Override this method to return a custom [EditorCanvasItemGizmo] for the 2D nodes of your choice, return [code]null[/code] for the rest of nodes. See also [method _has_gizmo].
+
+
+
+
+
+
+ Override this method to return the bounding rectangle of the CanvasItem. The returned value is used to draw the selection rectangle and provide handles to change the bounding rectangle.
+
+
+
+
+
+
+ Override this method to return the current state of the gizmo. This is used to restore the gizmo to its previous state when the user cancels an edit.
+ [b]Note:[/b] The underlying CanvasItem may have its own state saved. To preserve editor functionality, the built-in state has precedence over state saved by the Gizmo. The Gizmo does not get access to the underlying CanvasItem's state.
+
+
+
+
+
+
+
+ Override this method to apply a new boundary to the CanvasItem. It is called by the editor when the user changes the bounding rectangle.
+ [b]Note:[/b] The editor fully handles undo/redo for this method calling [method _edit_set_state] as necessary. You only need to apply the change to the node's transform.
+
+
+
+
+
+
+
+ Override this method to restore the gizmo to its previous state when the user cancels an edit.
+ [b]Note:[/b] The underlying CanvasItem may have its own state saved. To preserve editor functionality, the built-in state has precedence over state saved by the Gizmo. The dictionary given to this method may contain additional items to what was saved in [method _edit_get_state].
+
+
+
+
+
+
+ Override this method to return [code]true[/code] if the gizmo should be drawn with a bounding rectangle. This is used to draw the selection rectangle and provide handles to change the bounding rectangle.
+
+
+
+
+
+ Override this method to provide the name that will appear in the gizmo visibility menu.
+
+
+
+
+
+
+
+
+ Override this method to provide gizmo's handle names. The [param secondary] argument is [code]true[/code] when the requested handle is secondary (see [method EditorCanvasItemGizmo.add_handles] for more information). Called for this plugin's active gizmos.
+
+
+
+
+
+
+
+
+ Override this method to return the current value of a handle. This value will be requested at the start of an edit and used as the [code]restore[/code] argument in [method _commit_handle].
+ The [param secondary] argument is [code]true[/code] when the requested handle is secondary (see [method EditorCanvasItemGizmo.add_handles] for more information).
+ Called for this plugin's active gizmos.
+
+
+
+
+
+
+ Override this method to return the pivot point of the CanvasItem. This is used to draw the pivot point and provide handles to change the pivot point.
+
+
+
+
+
+ Override this method to set the gizmo's priority. Gizmos with higher priority will have precedence when processing inputs like handles or subgizmos selection.
+ All built-in editor gizmos return a priority of [code]-1[/code]. If not overridden, this method will return [code]0[/code], which means custom gizmos will automatically get higher priority than built-in gizmos.
+
+
+
+
+
+
+
+ Override this method to return the current transform of a subgizmo. As with all subgizmo methods, the transform should be in local space respect to the gizmo's CanvasItem. This transform will be requested at the start of an edit and used in the [code]restore[/code] argument in [method _commit_subgizmos]. Called for this plugin's active gizmos.
+
+
+
+
+
+
+ Override this method to define which CanvasItem nodes have a gizmo from this plugin. Whenever a [CanvasItem] node is added to a scene this method is called, if it returns [code]true[/code] the node gets a generic [EditorCanvasItemGizmo] assigned and is added to this plugin's list of active gizmos.
+
+
+
+
+
+
+ Override this method to return [code]true[/code] if the CanvasItem has a pivot point. This is used to draw the pivot point and provide handles to change the pivot point.
+
+
+
+
+
+
+
+
+ Override this method to return [code]true[/code] whenever to given handle should be highlighted in the editor. The [param secondary] argument is [code]true[/code] when the requested handle is secondary (see [method EditorCanvasItemGizmo.add_handles] for more information). Called for this plugin's active gizmos.
+
+
+
+
+
+ Override this method to define whether CanvasItem with this gizmo should be selectable even when the gizmo is hidden.
+
+
+
+
+
+
+ Override this method to add all the gizmo elements whenever a gizmo update is requested. It's common to call [method EditorCanvasItemGizmo.clear] at the beginning of this method and then add visual elements depending on the node's properties.
+
+
+
+
+
+
+
+
+
+ Override this method to update the node's properties when the user drags a gizmo handle (previously added with [method EditorCanvasItemGizmo.add_handles]). The provided [param position] is the mouse position in screen coordinates.
+ The [param secondary] argument is [code]true[/code] when the edited handle is secondary (see [method EditorCanvasItemGizmo.add_handles] for more information).
+ Called for this plugin's active gizmos.
+
+
+
+
+
+
+
+ Override this method to set the pivot point of the CanvasItem when the user drags the pivot handle.
+
+
+
+
+
+
+
+
+ Override this method to update the node properties during subgizmo editing (see [method _subgizmos_intersect_point] and [method _subgizmos_intersect_rect]). The [param transform] is given in the CanvasItem's local coordinate system. Called for this plugin's active gizmos.
+
+
+
+
+
+
+
+
+ Override this method to allow selecting subgizmos using mouse clicks. Given a [param point] and a [param distance] in local coordinates (relative to the CanvasItem), this method should return which subgizmo should be selected. The returned value should be a unique subgizmo identifier, which can have any non-negative value and will be used in other virtual methods like [method _get_subgizmo_transform] or [method _commit_subgizmos]. Called for this plugin's active gizmos.
+
+
+
+
+
+
+
+ Override this method to allow selecting subgizmos using mouse drag box selection. Given a [param rect] in global coordinates (canvas coordinates), this method should return which subgizmos should be selected. The returned value should be an array of unique subgizmo identifiers, which can have any non-negative value and will be used in other virtual methods like [method _get_subgizmo_transform] or [method _commit_subgizmos]. Called for this plugin's active gizmos.
+
+
+
+
+
+
+
+ Transforms a boundary change from a [param before] to a [param after] rectangle into a [Transform2D] that represents the transformation applied to the rectangle. Use this to compute the new CanvasItem's transform after a boundary change.
+
+
+
+
diff --git a/doc/classes/EditorPlugin.xml b/doc/classes/EditorPlugin.xml
index f0ea1b120324..0e1c710ff525 100644
--- a/doc/classes/EditorPlugin.xml
+++ b/doc/classes/EditorPlugin.xml
@@ -415,6 +415,14 @@
Adds a script at [param path] to the Autoload list as [param name].
+
+
+
+
+ Registers a new [EditorCanvasItemGizmoPlugin]. Gizmo plugins are used to add custom gizmos to the 2D preview viewport for a [CanvasItem].
+ See [method add_inspector_plugin] for an example of how to register a plugin.
+
+
@@ -656,6 +664,13 @@
Removes an Autoload [param name] from the list.
+
+
+
+
+ Removes a gizmo plugin registered by [method add_canvas_item_gizmo_plugin].
+
+
diff --git a/editor/plugins/editor_plugin.cpp b/editor/plugins/editor_plugin.cpp
index 70b30b35e293..c558d224cedf 100644
--- a/editor/plugins/editor_plugin.cpp
+++ b/editor/plugins/editor_plugin.cpp
@@ -475,6 +475,16 @@ void EditorPlugin::remove_export_platform(const Ref &p_pla
EditorExport::get_singleton()->remove_export_platform(p_platform);
}
+void EditorPlugin::add_canvas_item_gizmo_plugin(const Ref &p_gizmo_plugin) {
+ ERR_FAIL_COND(p_gizmo_plugin.is_null());
+ CanvasItemEditor::get_singleton()->add_gizmo_plugin(p_gizmo_plugin);
+}
+
+void EditorPlugin::remove_canvas_item_gizmo_plugin(const Ref &p_gizmo_plugin) {
+ ERR_FAIL_COND(p_gizmo_plugin.is_null());
+ CanvasItemEditor::get_singleton()->remove_gizmo_plugin(p_gizmo_plugin);
+}
+
void EditorPlugin::add_node_3d_gizmo_plugin(const Ref &p_gizmo_plugin) {
ERR_FAIL_COND(p_gizmo_plugin.is_null());
Node3DEditor::get_singleton()->add_gizmo_plugin(p_gizmo_plugin);
@@ -664,6 +674,8 @@ void EditorPlugin::_bind_methods() {
ClassDB::bind_method(D_METHOD("remove_export_plugin", "plugin"), &EditorPlugin::remove_export_plugin);
ClassDB::bind_method(D_METHOD("add_export_platform", "platform"), &EditorPlugin::add_export_platform);
ClassDB::bind_method(D_METHOD("remove_export_platform", "platform"), &EditorPlugin::remove_export_platform);
+ ClassDB::bind_method(D_METHOD("add_canvas_item_gizmo_plugin", "plugin"), &EditorPlugin::add_canvas_item_gizmo_plugin);
+ ClassDB::bind_method(D_METHOD("remove_canvas_item_gizmo_plugin", "plugin"), &EditorPlugin::remove_canvas_item_gizmo_plugin);
ClassDB::bind_method(D_METHOD("add_node_3d_gizmo_plugin", "plugin"), &EditorPlugin::add_node_3d_gizmo_plugin);
ClassDB::bind_method(D_METHOD("remove_node_3d_gizmo_plugin", "plugin"), &EditorPlugin::remove_node_3d_gizmo_plugin);
ClassDB::bind_method(D_METHOD("add_inspector_plugin", "plugin"), &EditorPlugin::add_inspector_plugin);
diff --git a/editor/plugins/editor_plugin.h b/editor/plugins/editor_plugin.h
index 381f6503e36f..b2c954616791 100644
--- a/editor/plugins/editor_plugin.h
+++ b/editor/plugins/editor_plugin.h
@@ -47,6 +47,7 @@ class EditorExportPlatform;
class EditorImportPlugin;
class EditorInspectorPlugin;
class EditorInterface;
+class EditorCanvasItemGizmoPlugin;
class EditorNode3DGizmoPlugin;
class EditorResourceConversionPlugin;
class EditorSceneFormatImporter;
@@ -242,6 +243,9 @@ class EditorPlugin : public Node {
void add_export_platform(const Ref &p_platform);
void remove_export_platform(const Ref &p_platform);
+ void add_canvas_item_gizmo_plugin(const Ref &p_gizmo_plugin);
+ void remove_canvas_item_gizmo_plugin(const Ref &p_gizmo_plugin);
+
void add_node_3d_gizmo_plugin(const Ref &p_gizmo_plugin);
void remove_node_3d_gizmo_plugin(const Ref &p_gizmo_plugin);
diff --git a/editor/register_editor_types.cpp b/editor/register_editor_types.cpp
index ddd9e49512cc..6f3ae785d218 100644
--- a/editor/register_editor_types.cpp
+++ b/editor/register_editor_types.cpp
@@ -78,6 +78,7 @@
#include "editor/inspector/sub_viewport_preview_editor_plugin.h"
#include "editor/inspector/tool_button_editor_plugin.h"
#include "editor/scene/2d/camera_2d_editor_plugin.h"
+#include "editor/scene/2d/canvas_item_editor_gizmos.h"
#include "editor/scene/2d/light_occluder_2d_editor_plugin.h"
#include "editor/scene/2d/line_2d_editor_plugin.h"
#include "editor/scene/2d/particles_2d_editor_plugin.h"
@@ -162,6 +163,8 @@ void register_editor_types() {
GDREGISTER_ABSTRACT_CLASS(EditorToaster);
GDREGISTER_CLASS(EditorNode3DGizmo);
GDREGISTER_CLASS(EditorNode3DGizmoPlugin);
+ GDREGISTER_CLASS(EditorCanvasItemGizmo);
+ GDREGISTER_CLASS(EditorCanvasItemGizmoPlugin);
GDREGISTER_ABSTRACT_CLASS(EditorResourcePreview);
GDREGISTER_CLASS(EditorResourcePreviewGenerator);
GDREGISTER_CLASS(EditorResourceTooltipPlugin);
diff --git a/editor/scene/2d/canvas_item_editor_gizmos.cpp b/editor/scene/2d/canvas_item_editor_gizmos.cpp
new file mode 100644
index 000000000000..77da9c714a41
--- /dev/null
+++ b/editor/scene/2d/canvas_item_editor_gizmos.cpp
@@ -0,0 +1,1011 @@
+/**************************************************************************/
+/* canvas_item_editor_gizmos.cpp */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#include "canvas_item_editor_gizmos.h"
+
+#include "core/math/geometry_2d.h"
+#include "core/object/class_db.h"
+#include "editor/editor_node.h"
+#include "editor/editor_undo_redo_manager.h"
+#include "editor/scene/canvas_item_editor_plugin.h"
+#include "scene/main/scene_tree.h"
+#include "scene/resources/mesh.h"
+#include "servers/rendering/rendering_server.h"
+
+bool EditorCanvasItemGizmo::is_editable() const {
+ ERR_FAIL_NULL_V(canvas_item, false);
+
+ const Node *edited_root = canvas_item->get_tree()->get_edited_scene_root();
+ if (canvas_item == edited_root) {
+ return true;
+ }
+ if (canvas_item->get_owner() == edited_root) {
+ return true;
+ }
+ if (edited_root->is_editable_instance(canvas_item->get_owner())) {
+ return true;
+ }
+
+ return false;
+}
+
+void EditorCanvasItemGizmo::clear() {
+ ERR_FAIL_NULL(RenderingServer::get_singleton());
+
+ for (Instance &instance : instances) {
+ if (instance.instance.is_valid()) {
+ RS::get_singleton()->free_rid(instance.instance);
+ instance.instance = RID();
+ }
+ }
+
+ collision_segments.clear();
+ collision_rects.clear();
+ collision_polygons.clear();
+ instances.clear();
+ handles.clear();
+ handle_ids.clear();
+ secondary_handles.clear();
+ secondary_handle_ids.clear();
+ use_boundary_handle = false;
+ use_pivot_handle = false;
+}
+
+void EditorCanvasItemGizmo::redraw() {
+ ERR_FAIL_NULL(gizmo_plugin);
+ clear();
+
+ if (!GDVIRTUAL_CALL(_redraw)) {
+ gizmo_plugin->redraw(this);
+ }
+
+ if (CanvasItemEditor::get_singleton()->is_current_selected_gizmo(this)) {
+ CanvasItemEditor::get_singleton()->update_transform_gizmo();
+ }
+}
+bool EditorCanvasItemGizmo::_edit_use_rect() const {
+ ERR_FAIL_NULL_V(gizmo_plugin, false);
+ bool ret = false;
+ if (GDVIRTUAL_CALL(_edit_use_rect, ret)) {
+ return ret;
+ }
+
+ return gizmo_plugin->_edit_use_rect(this);
+}
+
+Rect2 EditorCanvasItemGizmo::_edit_get_rect() const {
+ ERR_FAIL_NULL_V(gizmo_plugin, Rect2());
+ Rect2 ret;
+ if (GDVIRTUAL_CALL(_edit_get_rect, ret)) {
+ return ret;
+ }
+ return gizmo_plugin->_edit_get_rect(this);
+}
+
+void EditorCanvasItemGizmo::_edit_set_rect(const Rect2 &p_rect) {
+ ERR_FAIL_NULL(gizmo_plugin);
+ if (GDVIRTUAL_CALL(_edit_set_rect, p_rect)) {
+ return;
+ }
+ gizmo_plugin->_edit_set_rect(this, p_rect);
+}
+
+bool EditorCanvasItemGizmo::_has_pivot() const {
+ ERR_FAIL_NULL_V(gizmo_plugin, false);
+ bool ret = false;
+ if (GDVIRTUAL_CALL(_has_pivot, ret)) {
+ return ret;
+ }
+ return gizmo_plugin->_has_pivot(this);
+}
+
+Vector2 EditorCanvasItemGizmo::_get_pivot() const {
+ ERR_FAIL_NULL_V(gizmo_plugin, Vector2());
+ Vector2 ret;
+ if (GDVIRTUAL_CALL(_get_pivot, ret)) {
+ return ret;
+ }
+
+ return gizmo_plugin->_get_pivot(this);
+}
+
+void EditorCanvasItemGizmo::_set_pivot(const Vector2 &p_point) {
+ ERR_FAIL_NULL(gizmo_plugin);
+ if (GDVIRTUAL_CALL(_set_pivot, p_point)) {
+ return;
+ }
+
+ gizmo_plugin->_set_pivot(this, p_point);
+}
+
+Dictionary EditorCanvasItemGizmo::_edit_get_state() const {
+ ERR_FAIL_NULL_V(gizmo_plugin, Dictionary());
+
+ // because the gdscript method has no way of calling super back into c++ code, we
+ // implicitly merge what the gdscript implementation has returned with the returns from
+ // the c++ implementation, letting the c++ implementation win in case of conflicts.
+ Dictionary ret;
+ if (GDVIRTUAL_IS_OVERRIDDEN(_edit_get_state)) {
+ GDVIRTUAL_CALL(_edit_get_state, ret);
+ }
+
+ Dictionary base = gizmo_plugin->_edit_get_state(this);
+ if (ret.is_empty()) {
+ // skip merge logic, no point in wasting the CPU cycles for that
+ return base;
+ }
+
+ // make a copy (otherwise gdscript code might be surprised if the dictionary is magically changed)
+ return ret.merged(base, true);
+}
+
+void EditorCanvasItemGizmo::_edit_set_state(const Dictionary &p_state) {
+ ERR_FAIL_NULL(gizmo_plugin);
+
+ // this is a bit different, because gdscript methods cannot call super into c++ code, so
+ // when the method is overridden, we still call the gizmo plugin afterward (which in turn calls the CanvasItem)
+ // to ensure the canvas item also gets its state restored.
+ if (GDVIRTUAL_IS_OVERRIDDEN(_edit_set_state)) {
+ GDVIRTUAL_CALL(_edit_set_state, p_state);
+ }
+
+ gizmo_plugin->_edit_set_state(this, p_state);
+}
+
+String EditorCanvasItemGizmo::_get_handle_name(int p_id, bool p_secondary) const {
+ ERR_FAIL_NULL_V(gizmo_plugin, "");
+ String ret;
+ if (GDVIRTUAL_CALL(_get_handle_name, p_id, p_secondary, ret)) {
+ return ret;
+ }
+
+ return gizmo_plugin->_get_handle_name(this, p_id, p_secondary);
+}
+
+bool EditorCanvasItemGizmo::_is_handle_highlighted(int p_id, bool p_secondary) const {
+ ERR_FAIL_NULL_V(gizmo_plugin, false);
+ bool success = false;
+ if (GDVIRTUAL_CALL(_is_handle_highlighted, p_id, p_secondary, success)) {
+ return success;
+ }
+
+ return gizmo_plugin->_is_handle_highlighted(this, p_id, p_secondary);
+}
+
+Variant EditorCanvasItemGizmo::_get_handle_value(int p_id, bool p_secondary) const {
+ ERR_FAIL_NULL_V(gizmo_plugin, Variant());
+ Variant value;
+ if (GDVIRTUAL_CALL(_get_handle_value, p_id, p_secondary, value)) {
+ return value;
+ }
+
+ return gizmo_plugin->_get_handle_value(this, p_id, p_secondary);
+}
+
+void EditorCanvasItemGizmo::_begin_handle_action(int p_id, bool p_secondary) {
+ ERR_FAIL_NULL(gizmo_plugin);
+ if (GDVIRTUAL_CALL(_begin_handle_action, p_id, p_secondary)) {
+ return;
+ }
+
+ gizmo_plugin->_begin_handle_action(this, p_id, p_secondary);
+}
+
+void EditorCanvasItemGizmo::_set_handle(int p_id, bool p_secondary, const Point2 &p_point) {
+ ERR_FAIL_NULL(gizmo_plugin);
+ if (GDVIRTUAL_CALL(_set_handle, p_id, p_secondary, p_point)) {
+ return;
+ }
+
+ gizmo_plugin->_set_handle(this, p_id, p_secondary, p_point);
+}
+
+void EditorCanvasItemGizmo::_commit_handle(int p_id, bool p_secondary, const Variant &p_restore, bool p_cancel) {
+ ERR_FAIL_NULL(gizmo_plugin);
+ if (GDVIRTUAL_CALL(_commit_handle, p_id, p_secondary, p_restore, p_cancel)) {
+ return;
+ }
+
+ gizmo_plugin->_commit_handle(this, p_id, p_secondary, p_restore, p_cancel);
+}
+
+int EditorCanvasItemGizmo::_subgizmos_intersect_point(const Point2 &p_point, real_t p_max_distance) const {
+ ERR_FAIL_NULL_V(gizmo_plugin, -1);
+ int id = -1;
+ if (GDVIRTUAL_CALL(_subgizmos_intersect_point, p_point, p_max_distance, id)) {
+ return id;
+ }
+
+ return gizmo_plugin->_subgizmos_intersect_point(this, p_point, p_max_distance);
+}
+
+Vector EditorCanvasItemGizmo::_subgizmos_intersect_rect(const Rect2 &p_rect) const {
+ ERR_FAIL_NULL_V(gizmo_plugin, Vector());
+ Vector ret;
+ if (GDVIRTUAL_CALL(_subgizmos_intersect_rect, p_rect, ret)) {
+ return ret;
+ }
+
+ return gizmo_plugin->_subgizmos_intersect_rect(this, p_rect);
+}
+
+Transform2D EditorCanvasItemGizmo::_get_subgizmo_transform(int p_id) const {
+ ERR_FAIL_NULL_V(gizmo_plugin, Transform2D());
+ Transform2D ret;
+ if (GDVIRTUAL_CALL(_get_subgizmo_transform, p_id, ret)) {
+ return ret;
+ }
+
+ return gizmo_plugin->_get_subgizmo_transform(this, p_id);
+}
+
+void EditorCanvasItemGizmo::_set_subgizmo_transform(int p_id, const Transform2D &p_transform) {
+ ERR_FAIL_NULL(gizmo_plugin);
+ if (GDVIRTUAL_CALL(_set_subgizmo_transform, p_id, p_transform)) {
+ return;
+ }
+
+ gizmo_plugin->_set_subgizmo_transform(this, p_id, p_transform);
+}
+
+void EditorCanvasItemGizmo::_commit_subgizmos(const Vector &p_ids, const Vector &p_restore, bool p_cancel) {
+ ERR_FAIL_NULL(gizmo_plugin);
+ if (GDVIRTUAL_IS_OVERRIDDEN(_commit_subgizmos)) {
+ TypedArray restore;
+ restore.resize(p_restore.size());
+ for (int i = 0; i < p_restore.size(); i++) {
+ restore[i] = p_restore[i];
+ }
+
+ if (GDVIRTUAL_CALL(_commit_subgizmos, p_ids, restore, p_cancel)) {
+ return;
+ }
+ }
+
+ gizmo_plugin->_commit_subgizmos(this, p_ids, p_restore, p_cancel);
+}
+
+void EditorCanvasItemGizmo::set_canvas_item(CanvasItem *p_canvas_item) {
+ ERR_FAIL_NULL(p_canvas_item);
+ canvas_item = p_canvas_item;
+}
+
+void EditorCanvasItemGizmo::Instance::create_instance(CanvasItem *p_base, bool p_visible) {
+ ERR_FAIL_NULL(p_base);
+
+ instance = RS::get_singleton()->canvas_item_create();
+ RS::get_singleton()->canvas_item_set_parent(instance, p_base->get_canvas_item());
+ RS::get_singleton()->canvas_item_set_z_index(instance, 1);
+ RS::get_singleton()->canvas_item_set_z_as_relative_to_parent(instance, true);
+ RS::get_singleton()->canvas_item_set_visible(instance, p_visible);
+}
+
+void EditorCanvasItemGizmo::add_circle(const Vector2 &p_pos, float p_radius, const Color &p_color) {
+ ERR_FAIL_NULL(canvas_item);
+
+ Instance ins;
+ ins.create_instance(canvas_item, visible);
+ RS::get_singleton()->canvas_item_add_circle(ins.instance, p_pos, p_radius, p_color);
+ instances.push_back(ins);
+}
+
+void EditorCanvasItemGizmo::add_polygon(const Vector &p_polygon, const Color &p_color) {
+ ERR_FAIL_NULL(canvas_item);
+
+ Vector colors;
+ colors.reserve(p_polygon.size());
+ for (int i = 0; i < p_polygon.size(); i++) {
+ colors.append(p_color);
+ }
+
+ Instance ins;
+ ins.create_instance(canvas_item, visible);
+ RS::get_singleton()->canvas_item_add_polygon(ins.instance, p_polygon, colors);
+ instances.push_back(ins);
+}
+
+void EditorCanvasItemGizmo::add_polyline(const Vector &p_points, const Color &p_color) {
+ ERR_FAIL_NULL(canvas_item);
+
+ Vector colors;
+ colors.reserve(p_points.size());
+ for (int i = 0; i < p_points.size(); i++) {
+ colors.append(p_color);
+ }
+
+ Instance ins;
+ ins.create_instance(canvas_item, visible);
+ RS::get_singleton()->canvas_item_add_polyline(ins.instance, p_points, colors);
+ instances.push_back(ins);
+}
+
+void EditorCanvasItemGizmo::add_rect(const Rect2 &p_rect, const Color &p_color) {
+ ERR_FAIL_NULL(canvas_item);
+
+ Instance ins;
+ ins.create_instance(canvas_item, visible);
+ RS::get_singleton()->canvas_item_add_rect(ins.instance, p_rect, p_color);
+ instances.push_back(ins);
+}
+
+void EditorCanvasItemGizmo::add_collision_segments(const Vector &p_lines) {
+ ERR_FAIL_COND_MSG(p_lines.size() % 2 != 0, "Collision segments must be a list of even length.");
+ int from = collision_segments.size();
+ collision_segments.resize(from + p_lines.size());
+ for (int i = 0; i < p_lines.size(); i++) {
+ collision_segments.write[from + i] = p_lines[i];
+ }
+}
+
+void EditorCanvasItemGizmo::add_collision_rect(const Rect2 &p_rect) {
+ collision_rects.push_back(p_rect);
+}
+
+void EditorCanvasItemGizmo::add_collision_polygon(const Vector &p_polygon) {
+ collision_polygons.push_back(p_polygon);
+}
+
+void EditorCanvasItemGizmo::add_handles(const Vector &p_handles, Ref p_texture, const Vector &p_ids, bool p_secondary) {
+ if (!_is_selected() || !is_editable()) {
+ return;
+ }
+
+ ERR_FAIL_NULL(canvas_item);
+
+ Vector &handle_list = p_secondary ? secondary_handles : handles;
+ Vector &id_list = p_secondary ? secondary_handle_ids : handle_ids;
+
+ if (p_ids.is_empty()) {
+ ERR_FAIL_COND_MSG(!id_list.is_empty(), "IDs must be provided for all handles, as handles with IDs already exist.");
+ } else {
+ ERR_FAIL_COND_MSG(p_handles.size() != p_ids.size(), "The number of IDs should be the same as the number of handles.");
+ }
+
+ bool is_current_hover_gizmo = CanvasItemEditor::get_singleton()->get_current_hover_gizmo() == this;
+ bool current_hover_handle_secondary;
+ int current_hover_handle = CanvasItemEditor::get_singleton()->get_current_hover_gizmo_handle(current_hover_handle_secondary);
+
+ Ref texture = p_texture;
+ if (texture.is_null()) {
+ texture = EditorNode::get_singleton()->get_editor_theme()->get_icon(SNAME("EditorHandle"), SNAME("EditorIcons"));
+ }
+ // shouldn't happen but better be safe
+ ERR_FAIL_COND(texture.is_null());
+ Size2 texture_size = texture->get_size();
+
+ Control *viewport = CanvasItemEditor::get_singleton()->get_viewport_control();
+
+ // we draw handles in viewport space so they will change position with zoom/pan but not scale
+ Transform2D xform = CanvasItemEditor::get_singleton()->get_canvas_transform() * canvas_item->get_screen_transform();
+
+ int64_t handle_count = p_handles.size();
+ for (int i = 0; i < handle_count; i++) {
+ Vector2 position = xform.xform(p_handles[i]);
+ int id = p_ids.is_empty() ? i : p_ids[i];
+
+ Instance ins;
+ ins.create_instance(viewport, visible);
+ instances.push_back(ins);
+
+ Color modulate = Color(1, 1, 1, 1.0);
+ if (_is_handle_highlighted(id, p_secondary)) {
+ modulate = Color(0, 0, 1, 0.9);
+ }
+
+ if (!is_current_hover_gizmo || current_hover_handle != id || p_secondary != current_hover_handle_secondary) {
+ modulate.a = 0.8;
+ }
+
+ RS::get_singleton()->canvas_item_add_texture_rect(ins.instance, Rect2(position - texture_size / 2, texture_size), texture->get_rid(), false, modulate);
+ }
+
+ // update internal handle lists
+ int current_size = handle_list.size();
+ handle_list.resize(current_size + handle_count);
+ for (int i = 0; i < handle_count; i++) {
+ handle_list.write[current_size + i] = p_handles[i];
+ }
+
+ if (!p_ids.is_empty()) {
+ current_size = id_list.size();
+ id_list.resize(current_size + p_ids.size());
+ for (int i = 0; i < p_ids.size(); i++) {
+ id_list.write[current_size + i] = p_ids[i];
+ }
+ }
+}
+
+bool EditorCanvasItemGizmo::intersect_rect(const Rect2 &p_rect) const {
+ ERR_FAIL_NULL_V(canvas_item, false);
+ ERR_FAIL_COND_V(!valid, false);
+
+ if (!visible && !gizmo_plugin->is_selectable_when_hidden()) {
+ return false;
+ }
+
+ Transform2D transform = canvas_item->get_global_transform();
+
+ // for collision segments it is enough if at least one point
+ // of a segment is inside the rectangle
+ for (const Vector2 &pos : collision_segments) {
+ Vector2 global_position = transform.xform(pos);
+ if (p_rect.has_point(global_position)) {
+ return true;
+ }
+ }
+
+ // same for collision polygons
+ for (const Vector &collision_polygon : collision_polygons) {
+ for (const Vector2 &collision_point : collision_polygon) {
+ Vector2 global_position = transform.xform(collision_point);
+ if (p_rect.has_point(global_position)) {
+ return true;
+ }
+ }
+ }
+
+ // for rectangles we check if they overlap
+ Transform2D inverse_transform = transform.affine_inverse();
+ for (const Rect2 &collision_rect : collision_rects) {
+ if (collision_rect.intersects_transformed(inverse_transform, p_rect)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void EditorCanvasItemGizmo::handles_intersect_point(const Point2 &p_point, real_t p_max_distance, bool p_shift_pressed, int &r_id, bool &r_secondary) {
+ r_id = -1;
+ r_secondary = false;
+
+ ERR_FAIL_NULL(canvas_item);
+ ERR_FAIL_COND(!valid);
+
+ if (!visible) {
+ return;
+ }
+
+ real_t min_d = 1e20;
+ for (int i = 0; i < secondary_handles.size(); i++) {
+ real_t distance = secondary_handles[i].distance_to(p_point);
+ if (distance < p_max_distance && distance < min_d) {
+ min_d = distance;
+ if (secondary_handle_ids.is_empty()) {
+ r_id = i;
+ } else {
+ r_id = secondary_handle_ids[i];
+ }
+ r_secondary = true;
+ }
+ }
+
+ if (r_id != -1 && p_shift_pressed) {
+ return;
+ }
+
+ min_d = 1e20;
+ for (int i = 0; i < handles.size(); i++) {
+ real_t distance = handles[i].distance_to(p_point);
+ if (distance < p_max_distance && distance < min_d) {
+ min_d = distance;
+ if (handle_ids.is_empty()) {
+ r_id = i;
+ } else {
+ r_id = handle_ids[i];
+ }
+ r_secondary = false;
+ }
+ }
+}
+
+bool EditorCanvasItemGizmo::intersect_point(const Point2 &p_point, const real_t p_max_distance) const {
+ ERR_FAIL_NULL_V(canvas_item, false);
+ ERR_FAIL_COND_V(!valid, false);
+
+ if (!visible && !gizmo_plugin->is_selectable_when_hidden()) {
+ return false;
+ }
+
+ for (int i = 0; i < collision_segments.size(); i += 2) {
+ Vector2 a = collision_segments[i];
+ Vector2 b = collision_segments[i + 1];
+ Vector2 closest = Geometry2D::get_closest_point_to_segment(p_point, a, b);
+ if (closest.distance_to(p_point) < p_max_distance) {
+ return true;
+ }
+ }
+
+ for (const Rect2 &collision_rect : collision_rects) {
+ if (collision_rect.has_point(p_point)) {
+ return true;
+ }
+ }
+
+ for (const Vector &collision_polygon : collision_polygons) {
+ if (Geometry2D::is_point_in_polygon(p_point, collision_polygon)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool EditorCanvasItemGizmo::is_subgizmo_selected(int p_id) const {
+ CanvasItemEditor *ed = CanvasItemEditor::get_singleton();
+ ERR_FAIL_NULL_V(ed, false);
+ return ed->is_current_selected_gizmo(this) && ed->is_subgizmo_selected(p_id);
+}
+
+Vector EditorCanvasItemGizmo::get_subgizmo_selection() const {
+ Vector ret;
+
+ CanvasItemEditor *ed = CanvasItemEditor::get_singleton();
+ ERR_FAIL_NULL_V(ed, ret);
+
+ if (ed->is_current_selected_gizmo(this)) {
+ ret = ed->get_subgizmo_selection();
+ }
+
+ return ret;
+}
+
+void EditorCanvasItemGizmo::create() {
+ ERR_FAIL_NULL(canvas_item);
+ ERR_FAIL_COND(valid);
+ valid = true;
+
+ for (Instance &instance : instances) {
+ instance.create_instance(canvas_item, visible);
+ }
+
+ transform();
+}
+
+void EditorCanvasItemGizmo::transform() {
+ ERR_FAIL_NULL(canvas_item);
+ ERR_FAIL_COND(!valid);
+ for (const Instance &instance : instances) {
+ RS::get_singleton()->canvas_item_set_transform(instance.instance, canvas_item->get_global_transform());
+ }
+}
+
+void EditorCanvasItemGizmo::free() {
+ ERR_FAIL_NULL(RS::get_singleton());
+ ERR_FAIL_NULL(canvas_item);
+ ERR_FAIL_COND(!valid);
+
+ clear();
+ valid = false;
+}
+
+void EditorCanvasItemGizmo::set_visible(bool p_visible) {
+ visible = p_visible;
+ for (const Instance &instance : instances) {
+ RS::get_singleton()->canvas_item_set_visible(instance.instance, p_visible);
+ }
+}
+
+void EditorCanvasItemGizmo::set_plugin(EditorCanvasItemGizmoPlugin *p_plugin) {
+ gizmo_plugin = p_plugin;
+}
+
+void EditorCanvasItemGizmo::_bind_methods() {
+ ClassDB::bind_method(D_METHOD("add_circle", "position", "radius", "color"), &EditorCanvasItemGizmo::add_circle, DEFVAL(Color(1, 1, 1)));
+ ClassDB::bind_method(D_METHOD("add_polygon", "points", "color"), &EditorCanvasItemGizmo::add_polygon, DEFVAL(Color(1, 1, 1)));
+ ClassDB::bind_method(D_METHOD("add_polyline", "points", "color"), &EditorCanvasItemGizmo::add_polyline, DEFVAL(Color(1, 1, 1)));
+ ClassDB::bind_method(D_METHOD("add_rect", "rect", "color"), &EditorCanvasItemGizmo::add_rect, DEFVAL(Color(1, 1, 1)));
+ ClassDB::bind_method(D_METHOD("add_collision_segments", "segments"), &EditorCanvasItemGizmo::add_collision_segments);
+ ClassDB::bind_method(D_METHOD("add_collision_rect", "rect"), &EditorCanvasItemGizmo::add_collision_rect);
+ ClassDB::bind_method(D_METHOD("add_collision_polygon", "polygon"), &EditorCanvasItemGizmo::add_collision_polygon);
+ ClassDB::bind_method(D_METHOD("add_handles", "handles", "texture", "ids", "secondary"), &EditorCanvasItemGizmo::add_handles, DEFVAL(Ref()), DEFVAL(PackedInt32Array()), DEFVAL(false));
+ ClassDB::bind_method(D_METHOD("set_canvas_item", "canvas_item"), &EditorCanvasItemGizmo::_set_canvas_item);
+ ClassDB::bind_method(D_METHOD("get_canvas_item"), &EditorCanvasItemGizmo::get_canvas_item);
+ ClassDB::bind_method(D_METHOD("get_plugin"), &EditorCanvasItemGizmo::get_plugin);
+ ClassDB::bind_method(D_METHOD("clear"), &EditorCanvasItemGizmo::clear);
+ ClassDB::bind_method(D_METHOD("set_visible", "visible"), &EditorCanvasItemGizmo::set_visible);
+ ClassDB::bind_method(D_METHOD("is_subgizmo_selected", "id"), &EditorCanvasItemGizmo::is_subgizmo_selected);
+ ClassDB::bind_method(D_METHOD("get_subgizmo_selection"), &EditorCanvasItemGizmo::get_subgizmo_selection);
+ ClassDB::bind_method(D_METHOD("_edit_set_state", "state"), &EditorCanvasItemGizmo::_edit_set_state);
+
+ GDVIRTUAL_BIND(_redraw);
+ GDVIRTUAL_BIND(_get_handle_name, "id", "secondary");
+ GDVIRTUAL_BIND(_is_handle_highlighted, "id", "secondary");
+
+ GDVIRTUAL_BIND(_edit_use_rect);
+ GDVIRTUAL_BIND(_edit_get_rect);
+ GDVIRTUAL_BIND(_edit_set_rect, "boundary");
+
+ GDVIRTUAL_BIND(_has_pivot);
+ GDVIRTUAL_BIND(_get_pivot);
+ GDVIRTUAL_BIND(_set_pivot, "pivot");
+
+ GDVIRTUAL_BIND(_edit_get_state);
+ GDVIRTUAL_BIND(_edit_set_state, "state");
+
+ GDVIRTUAL_BIND(_get_handle_value, "id", "secondary");
+ GDVIRTUAL_BIND(_begin_handle_action, "id", "secondary");
+ GDVIRTUAL_BIND(_set_handle, "id", "secondary", "point");
+ GDVIRTUAL_BIND(_commit_handle, "id", "secondary", "restore", "cancel");
+
+ GDVIRTUAL_BIND(_subgizmos_intersect_point, "point", "distance");
+ GDVIRTUAL_BIND(_subgizmos_intersect_rect, "rect");
+ GDVIRTUAL_BIND(_get_subgizmo_transform, "id");
+ GDVIRTUAL_BIND(_set_subgizmo_transform, "id", "transform");
+ GDVIRTUAL_BIND(_commit_subgizmos, "ids", "restores", "cancel");
+}
+
+EditorCanvasItemGizmo::EditorCanvasItemGizmo() {
+ valid = false;
+ visible = false;
+ selected = false;
+ canvas_item = nullptr;
+ gizmo_plugin = nullptr;
+ use_boundary_handle = false;
+ use_pivot_handle = false;
+}
+
+EditorCanvasItemGizmo::~EditorCanvasItemGizmo() {
+ if (gizmo_plugin != nullptr) {
+ gizmo_plugin->unregister_gizmo(this);
+ }
+ clear();
+}
+
+/////
+
+String EditorCanvasItemGizmoPlugin::get_gizmo_name() const {
+ String ret;
+ if (GDVIRTUAL_CALL(_get_gizmo_name, ret)) {
+ return ret;
+ }
+ WARN_PRINT_ONCE("A CanvasItem editor gizmo has no name defined (it will appear as \"Unnamed Gizmo\" in the \"View > Gizmos\" menu). To resolve this, override the `_get_gizmo_name()` function to return a String in the script that extends EditorCanvasItemGizmoPlugin.");
+ return "Unnamed Gizmo";
+}
+
+int EditorCanvasItemGizmoPlugin::get_priority() const {
+ int ret = 0;
+ if (GDVIRTUAL_CALL(_get_priority, ret)) {
+ return ret;
+ }
+ return 0;
+}
+
+Transform2D EditorCanvasItemGizmoPlugin::boundary_change_to_transform(const Rect2 &p_before, const Rect2 &p_after) {
+ Vector2 zero_offset;
+ Size2 new_scale(1, 1);
+
+ if (p_before.size.x != 0) {
+ zero_offset.x = -p_before.position.x / p_before.size.x;
+ new_scale.x = p_after.size.x / p_before.size.x;
+ }
+
+ if (p_before.size.y != 0) {
+ zero_offset.y = -p_before.position.y / p_before.size.y;
+ new_scale.y = p_after.size.y / p_before.size.y;
+ }
+
+ Point2 new_pos = p_after.position + p_after.size * zero_offset;
+ return Transform2D().scaled(new_scale).translated(new_pos);
+}
+
+Ref EditorCanvasItemGizmoPlugin::get_gizmo(CanvasItem *p_canvas_item) {
+ if (get_script_instance() && get_script_instance()->has_method("_get_gizmo")) {
+ return get_script_instance()->call("_get_gizmo", p_canvas_item);
+ }
+
+ Ref ref = create_gizmo(p_canvas_item);
+ if (ref.is_null()) {
+ return ref;
+ }
+
+ ref->set_plugin(this);
+ ref->set_canvas_item(p_canvas_item);
+ ref->set_visible(gizmos_visible);
+
+ current_gizmos.insert(ref.ptr());
+ return ref;
+}
+
+void EditorCanvasItemGizmoPlugin::_bind_methods() {
+ ClassDB::bind_static_method("EditorCanvasItemGizmoPlugin", D_METHOD("boundary_change_to_transform", "before", "after"), &EditorCanvasItemGizmoPlugin::boundary_change_to_transform);
+
+ GDVIRTUAL_BIND(_has_gizmo, "for_canvas_item");
+ GDVIRTUAL_BIND(_create_gizmo, "for_canvas_item");
+
+ GDVIRTUAL_BIND(_get_gizmo_name);
+ GDVIRTUAL_BIND(_get_priority);
+ GDVIRTUAL_BIND(_can_be_hidden);
+ GDVIRTUAL_BIND(_is_selectable_when_hidden);
+
+ GDVIRTUAL_BIND(_redraw, "gizmo");
+
+ GDVIRTUAL_BIND(_edit_use_rect, "gizmo");
+ GDVIRTUAL_BIND(_edit_set_rect, "gizmo", "boundary");
+ GDVIRTUAL_BIND(_edit_get_rect, "gizmo");
+
+ GDVIRTUAL_BIND(_has_pivot, "gizmo");
+ GDVIRTUAL_BIND(_set_pivot, "gizmo", "pivot");
+ GDVIRTUAL_BIND(_get_pivot, "gizmo");
+
+ GDVIRTUAL_BIND(_edit_get_state, "gizmo");
+ GDVIRTUAL_BIND(_edit_set_state, "gizmo", "state");
+
+ GDVIRTUAL_BIND(_get_handle_name, "gizmo", "handle_id", "secondary");
+ GDVIRTUAL_BIND(_is_handle_highlighted, "gizmo", "handle_id", "secondary");
+ GDVIRTUAL_BIND(_get_handle_value, "gizmo", "handle_id", "secondary");
+ GDVIRTUAL_BIND(_begin_handle_action, "gizmo", "handle_id", "secondary");
+ GDVIRTUAL_BIND(_set_handle, "gizmo", "handle_id", "secondary", "position");
+ GDVIRTUAL_BIND(_commit_handle, "gizmo", "handle_id", "secondary", "restore", "cancel");
+
+ GDVIRTUAL_BIND(_subgizmos_intersect_point, "gizmo", "point", "distance");
+ GDVIRTUAL_BIND(_subgizmos_intersect_rect, "gizmo", "rect");
+ GDVIRTUAL_BIND(_get_subgizmo_transform, "gizmo", "subgizmo_id");
+ GDVIRTUAL_BIND(_set_subgizmo_transform, "gizmo", "subgizmo_id", "transform");
+ GDVIRTUAL_BIND(_commit_subgizmos, "gizmo", "ids", "restores", "cancel");
+}
+
+bool EditorCanvasItemGizmoPlugin::has_gizmo(CanvasItem *p_canvas_item) {
+ bool success = false;
+ GDVIRTUAL_CALL(_has_gizmo, p_canvas_item, success);
+ return success;
+}
+
+Ref EditorCanvasItemGizmoPlugin::create_gizmo(CanvasItem *p_canvas_item) {
+ Ref ret;
+ if (GDVIRTUAL_CALL(_create_gizmo, p_canvas_item, ret)) {
+ return ret;
+ }
+
+ Ref ref;
+ if (has_gizmo(p_canvas_item)) {
+ ref.instantiate();
+ }
+
+ return ref;
+}
+
+bool EditorCanvasItemGizmoPlugin::can_be_hidden() const {
+ bool ret = true;
+ GDVIRTUAL_CALL(_can_be_hidden, ret);
+ return ret;
+}
+
+bool EditorCanvasItemGizmoPlugin::is_selectable_when_hidden() const {
+ bool ret = false;
+ GDVIRTUAL_CALL(_is_selectable_when_hidden, ret);
+ return ret;
+}
+
+bool EditorCanvasItemGizmoPlugin::can_commit_handle_on_click() const {
+ return false;
+}
+
+void EditorCanvasItemGizmoPlugin::redraw(EditorCanvasItemGizmo *p_gizmo) {
+ GDVIRTUAL_CALL(_redraw, p_gizmo);
+}
+
+bool EditorCanvasItemGizmoPlugin::_edit_use_rect(const EditorCanvasItemGizmo *p_gizmo) const {
+ ERR_FAIL_NULL_V(p_gizmo, false);
+ bool ret = false;
+ if (GDVIRTUAL_CALL(_edit_use_rect, Ref(p_gizmo), ret)) {
+ return ret;
+ }
+ CanvasItem *canvas_item = p_gizmo->get_canvas_item();
+ ERR_FAIL_NULL_V(canvas_item, false);
+ return canvas_item->_edit_use_rect();
+}
+
+void EditorCanvasItemGizmoPlugin::_edit_set_rect(const EditorCanvasItemGizmo *p_gizmo, Rect2 p_boundary) {
+ ERR_FAIL_NULL(p_gizmo);
+ if (GDVIRTUAL_IS_OVERRIDDEN(_edit_set_rect)) {
+ GDVIRTUAL_CALL(_edit_set_rect, Ref(p_gizmo), p_boundary);
+ return;
+ }
+ CanvasItem *ci = p_gizmo->get_canvas_item();
+ ERR_FAIL_NULL(ci);
+ ci->_edit_set_rect(p_boundary);
+}
+
+Rect2 EditorCanvasItemGizmoPlugin::_edit_get_rect(const EditorCanvasItemGizmo *p_gizmo) const {
+ ERR_FAIL_NULL_V(p_gizmo, Rect2());
+ if (GDVIRTUAL_IS_OVERRIDDEN(_edit_get_rect)) {
+ Rect2 ret;
+ GDVIRTUAL_CALL(_edit_get_rect, Ref(p_gizmo), ret);
+ return ret;
+ }
+ CanvasItem *ci = p_gizmo->get_canvas_item();
+ ERR_FAIL_NULL_V(ci, Rect2());
+ return ci->_edit_get_rect();
+}
+
+bool EditorCanvasItemGizmoPlugin::_has_pivot(const EditorCanvasItemGizmo *p_gizmo) const {
+ ERR_FAIL_NULL_V(p_gizmo, false);
+ if (GDVIRTUAL_IS_OVERRIDDEN(_has_pivot)) {
+ bool ret = false;
+ GDVIRTUAL_CALL(_has_pivot, Ref(p_gizmo), ret);
+ return ret;
+ }
+
+ CanvasItem *canvas_item = p_gizmo->get_canvas_item();
+ ERR_FAIL_NULL_V(canvas_item, false);
+ return canvas_item->_edit_use_pivot();
+}
+
+void EditorCanvasItemGizmoPlugin::_set_pivot(const EditorCanvasItemGizmo *p_gizmo, const Vector2 &p_pivot) {
+ ERR_FAIL_NULL(p_gizmo);
+ if (GDVIRTUAL_IS_OVERRIDDEN(_set_pivot)) {
+ GDVIRTUAL_CALL(_set_pivot, Ref(p_gizmo), p_pivot);
+ return;
+ }
+ CanvasItem *ci = p_gizmo->get_canvas_item();
+ ERR_FAIL_NULL(ci);
+ ci->_edit_set_pivot(p_pivot);
+}
+
+Point2 EditorCanvasItemGizmoPlugin::_get_pivot(const EditorCanvasItemGizmo *p_gizmo) const {
+ ERR_FAIL_NULL_V(p_gizmo, Point2());
+ if (GDVIRTUAL_IS_OVERRIDDEN(_get_pivot)) {
+ Point2 ret;
+ GDVIRTUAL_CALL(_get_pivot, Ref(p_gizmo), ret);
+ return ret;
+ }
+ CanvasItem *ci = p_gizmo->get_canvas_item();
+ ERR_FAIL_NULL_V(ci, Point2());
+ return ci->_edit_get_pivot();
+}
+
+Dictionary EditorCanvasItemGizmoPlugin::_edit_get_state(const EditorCanvasItemGizmo *p_gizmo) const {
+ ERR_FAIL_NULL_V(p_gizmo, Dictionary());
+ CanvasItem *ci = p_gizmo->get_canvas_item();
+ ERR_FAIL_NULL_V(ci, Dictionary());
+
+ Dictionary ret;
+ if (GDVIRTUAL_IS_OVERRIDDEN(_edit_get_state)) {
+ GDVIRTUAL_CALL(_edit_get_state, Ref(p_gizmo), ret);
+ }
+
+ // similar to EditorCanvasItemGizmo::_edit_get_state, we merge the results
+ Dictionary base = ci->_edit_get_state();
+
+ if (ret.is_empty()) {
+ return base;
+ }
+
+ return ret.merged(base, true);
+}
+
+void EditorCanvasItemGizmoPlugin::_edit_set_state(const EditorCanvasItemGizmo *p_gizmo, const Dictionary &p_state) {
+ ERR_FAIL_NULL(p_gizmo);
+
+ // allow GDScript code to do their own restores on a known good state
+ // we do this before the underlying canvas item, so we can be sure the GDScript code
+ // sees the same state it had when it created the state dictionary
+ if (GDVIRTUAL_IS_OVERRIDDEN(_edit_set_state)) {
+ GDVIRTUAL_CALL(_edit_set_state, Ref(p_gizmo), p_state);
+ }
+
+ // then restore underlying canvas item state
+ CanvasItem *ci = p_gizmo->get_canvas_item();
+ ERR_FAIL_NULL(ci);
+ ci->_edit_set_state(p_state);
+}
+
+bool EditorCanvasItemGizmoPlugin::_is_handle_highlighted(const EditorCanvasItemGizmo *p_gizmo, int p_id, bool p_secondary) const {
+ bool ret = false;
+ GDVIRTUAL_CALL(_is_handle_highlighted, Ref(p_gizmo), p_id, p_secondary, ret);
+ return ret;
+}
+
+String EditorCanvasItemGizmoPlugin::_get_handle_name(const EditorCanvasItemGizmo *p_gizmo, int p_id, bool p_secondary) const {
+ String ret;
+ GDVIRTUAL_CALL(_get_handle_name, Ref(p_gizmo), p_id, p_secondary, ret);
+ return ret;
+}
+
+Variant EditorCanvasItemGizmoPlugin::_get_handle_value(const EditorCanvasItemGizmo *p_gizmo, int p_id, bool p_secondary) const {
+ Variant ret;
+ GDVIRTUAL_CALL(_get_handle_value, Ref(p_gizmo), p_id, p_secondary, ret);
+ return ret;
+}
+
+void EditorCanvasItemGizmoPlugin::_begin_handle_action(const EditorCanvasItemGizmo *p_gizmo, int p_id, bool p_secondary) {
+ GDVIRTUAL_CALL(_begin_handle_action, Ref(p_gizmo), p_id, p_secondary);
+}
+
+void EditorCanvasItemGizmoPlugin::_set_handle(const EditorCanvasItemGizmo *p_gizmo, int p_id, bool p_secondary, const Point2 &p_point) {
+ GDVIRTUAL_CALL(_set_handle, Ref(p_gizmo), p_id, p_secondary, p_point);
+}
+
+void EditorCanvasItemGizmoPlugin::_commit_handle(const EditorCanvasItemGizmo *p_gizmo, int p_id, bool p_secondary, const Variant &p_restore, bool p_cancel) {
+ GDVIRTUAL_CALL(_commit_handle, Ref(p_gizmo), p_id, p_secondary, p_restore, p_cancel);
+}
+
+int EditorCanvasItemGizmoPlugin::_subgizmos_intersect_point(const EditorCanvasItemGizmo *p_gizmo, const Vector2 &p_point, real_t p_max_distance) const {
+ int ret = -1;
+ GDVIRTUAL_CALL(_subgizmos_intersect_point, Ref(p_gizmo), p_point, p_max_distance, ret);
+ return ret;
+}
+
+Vector EditorCanvasItemGizmoPlugin::_subgizmos_intersect_rect(const EditorCanvasItemGizmo *p_gizmo, const Rect2 &p_rect) const {
+ Vector ret;
+ GDVIRTUAL_CALL(_subgizmos_intersect_rect, Ref(p_gizmo), p_rect, ret);
+ return ret;
+}
+
+Transform2D EditorCanvasItemGizmoPlugin::_get_subgizmo_transform(const EditorCanvasItemGizmo *p_gizmo, int p_id) const {
+ Transform2D ret;
+ GDVIRTUAL_CALL(_get_subgizmo_transform, Ref(p_gizmo), p_id, ret);
+ return ret;
+}
+
+void EditorCanvasItemGizmoPlugin::_set_subgizmo_transform(const EditorCanvasItemGizmo *p_gizmo, int p_id, const Transform2D &p_xform) {
+ GDVIRTUAL_CALL(_set_subgizmo_transform, Ref(p_gizmo), p_id, p_xform);
+}
+
+void EditorCanvasItemGizmoPlugin::_commit_subgizmos(const EditorCanvasItemGizmo *p_gizmo, const Vector &p_ids, const Vector &p_transforms, bool p_cancel) {
+ TypedArray transforms;
+ transforms.reserve(p_transforms.size());
+ for (int i = 0; i < p_transforms.size(); i++) {
+ transforms.append(p_transforms[i]);
+ }
+
+ GDVIRTUAL_CALL(_commit_subgizmos, Ref(p_gizmo), p_ids, transforms, p_cancel);
+}
+
+void EditorCanvasItemGizmoPlugin::set_gizmos_visible(bool p_visible) {
+ gizmos_visible = p_visible;
+ for (EditorCanvasItemGizmo *gizmo : current_gizmos) {
+ gizmo->set_visible(p_visible);
+ }
+}
+
+bool EditorCanvasItemGizmoPlugin::is_gizmos_visible() const {
+ return gizmos_visible;
+}
+
+void EditorCanvasItemGizmoPlugin::unregister_gizmo(EditorCanvasItemGizmo *p_gizmo) {
+ current_gizmos.erase(p_gizmo);
+}
+
+EditorCanvasItemGizmoPlugin::EditorCanvasItemGizmoPlugin() {
+ gizmos_visible = true;
+}
+
+EditorCanvasItemGizmoPlugin::~EditorCanvasItemGizmoPlugin() {
+ for (EditorCanvasItemGizmo *gizmo : current_gizmos) {
+ gizmo->set_plugin(nullptr);
+ gizmo->get_canvas_item()->remove_gizmo(gizmo);
+ }
+
+ if (CanvasItemEditor::get_singleton()) {
+ CanvasItemEditor::get_singleton()->update_all_gizmos();
+ }
+}
diff --git a/editor/scene/2d/canvas_item_editor_gizmos.h b/editor/scene/2d/canvas_item_editor_gizmos.h
new file mode 100644
index 000000000000..5044e89ae196
--- /dev/null
+++ b/editor/scene/2d/canvas_item_editor_gizmos.h
@@ -0,0 +1,256 @@
+/**************************************************************************/
+/* canvas_item_editor_gizmos.h */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#pragma once
+
+#include "scene/main/canvas_item.h"
+
+class EditorCanvasItemGizmoPlugin;
+
+class EditorCanvasItemGizmo : public CanvasItemGizmo {
+ GDCLASS(EditorCanvasItemGizmo, CanvasItemGizmo);
+
+ struct Instance {
+ RID instance;
+ Transform2D xform;
+
+ void create_instance(CanvasItem *p_base, bool p_visible = false);
+ };
+
+ bool selected;
+
+ Vector collision_segments;
+ Vector collision_rects;
+ Vector> collision_polygons;
+ // TODO: GIZMOS: i think we're going to need exclusion shapes, so plugins can properly model
+ // selection of 2D shapes with holes that cannot be modeled with simple polygons (e.g. a torus shape)
+
+ Vector handles;
+ Vector handle_ids;
+ Vector secondary_handles;
+ Vector secondary_handle_ids;
+
+ bool use_boundary_handle;
+ bool use_pivot_handle;
+
+ bool valid;
+ bool visible;
+ Vector instances;
+ CanvasItem *canvas_item = nullptr;
+
+ void _set_canvas_item(CanvasItem *p_canvas_item) { set_canvas_item(Object::cast_to(p_canvas_item)); }
+
+protected:
+ static void _bind_methods();
+
+ EditorCanvasItemGizmoPlugin *gizmo_plugin = nullptr;
+
+ GDVIRTUAL0(_redraw)
+
+ GDVIRTUAL0RC(bool, _edit_use_rect)
+ GDVIRTUAL0RC(Rect2, _edit_get_rect)
+ GDVIRTUAL1(_edit_set_rect, Rect2)
+
+ GDVIRTUAL0RC(bool, _has_pivot)
+ GDVIRTUAL0RC(Vector2, _get_pivot)
+ GDVIRTUAL1(_set_pivot, Vector2)
+
+ GDVIRTUAL0RC(Dictionary, _edit_get_state)
+ GDVIRTUAL1(_edit_set_state, Dictionary)
+
+ GDVIRTUAL2RC(String, _get_handle_name, int, bool)
+ GDVIRTUAL2RC(bool, _is_handle_highlighted, int, bool)
+ GDVIRTUAL2RC(Variant, _get_handle_value, int, bool)
+ GDVIRTUAL2(_begin_handle_action, int, bool)
+ GDVIRTUAL3(_set_handle, int, bool, Vector2)
+ GDVIRTUAL4(_commit_handle, int, bool, Variant, bool)
+
+ GDVIRTUAL2RC(int, _subgizmos_intersect_point, Vector2, real_t)
+ GDVIRTUAL1RC(Vector, _subgizmos_intersect_rect, Rect2)
+ GDVIRTUAL1RC(Transform2D, _get_subgizmo_transform, int)
+ GDVIRTUAL2(_set_subgizmo_transform, int, Transform2D)
+ GDVIRTUAL3(_commit_subgizmos, Vector, TypedArray, bool)
+
+public:
+ void add_circle(const Vector2 &p_pos, float p_radius, const Color &p_color = Color(1, 1, 1));
+ void add_polygon(const Vector &p_polygon, const Color &p_color = Color(1, 1, 1));
+ void add_polyline(const Vector &p_points, const Color &p_color = Color(1, 1, 1));
+ void add_rect(const Rect2 &p_rect, const Color &p_color = Color(1, 1, 1));
+
+ void add_collision_segments(const Vector &p_lines);
+ void add_collision_rect(const Rect2 &p_rect);
+ void add_collision_polygon(const Vector &p_polygon);
+
+ void add_handles(const Vector &p_handles, Ref p_texture, const Vector &p_ids = Vector(), bool p_secondary = false);
+
+ virtual bool _edit_use_rect() const;
+ virtual Rect2 _edit_get_rect() const;
+ virtual void _edit_set_rect(const Rect2 &p_rect);
+
+ virtual bool _has_pivot() const;
+ virtual Vector2 _get_pivot() const;
+ virtual void _set_pivot(const Vector2 &p_pivot);
+
+ virtual Dictionary _edit_get_state() const;
+ virtual void _edit_set_state(const Dictionary &p_state);
+
+ virtual bool _is_handle_highlighted(int p_id, bool p_secondary) const;
+ virtual String _get_handle_name(int p_id, bool p_secondary) const;
+ virtual Variant _get_handle_value(int p_id, bool p_secondary) const;
+ virtual void _begin_handle_action(int p_id, bool p_secondary);
+ virtual void _set_handle(int p_id, bool p_secondary, const Point2 &p_point);
+ virtual void _commit_handle(int p_id, bool p_secondary, const Variant &p_restore, bool p_cancel = false);
+
+ virtual int _subgizmos_intersect_point(const Point2 &p_point, real_t p_max_distance) const;
+ virtual Vector _subgizmos_intersect_rect(const Rect2 &p_rect) const;
+ virtual Transform2D _get_subgizmo_transform(int p_id) const;
+ virtual void _set_subgizmo_transform(int p_id, const Transform2D &p_xform);
+ virtual void _commit_subgizmos(const Vector &p_ids, const Vector &p_transforms, bool p_cancel = false);
+
+ void _set_selected(bool p_selected) { selected = p_selected; }
+ bool _is_selected() const { return selected; }
+
+ void set_canvas_item(CanvasItem *p_canvas_item);
+ CanvasItem *get_canvas_item() const { return canvas_item; }
+
+ Ref get_plugin() const { return gizmo_plugin; }
+
+ bool intersect_rect(const Rect2 &p_rect) const;
+ void handles_intersect_point(const Point2 &p_point, real_t p_max_distance, bool p_shift_pressed, int &r_id, bool &r_secondary);
+ bool intersect_point(const Point2 &p_point, real_t p_max_distance) const;
+ bool is_subgizmo_selected(int p_id) const;
+ Vector get_subgizmo_selection() const;
+
+ virtual void clear() override;
+ virtual void create() override;
+ virtual void transform() override;
+ virtual void redraw() override;
+ virtual void free() override;
+
+ virtual bool is_editable() const;
+
+ void set_visible(bool p_visible);
+ void set_plugin(EditorCanvasItemGizmoPlugin *p_plugin);
+
+ EditorCanvasItemGizmo();
+ ~EditorCanvasItemGizmo();
+};
+
+class EditorCanvasItemGizmoPlugin : public Resource {
+ GDCLASS(EditorCanvasItemGizmoPlugin, Resource);
+
+protected:
+ bool gizmos_visible = true;
+ HashSet current_gizmos;
+
+ static void _bind_methods();
+ virtual bool has_gizmo(CanvasItem *p_canvas_item);
+ virtual Ref create_gizmo(CanvasItem *p_canvas_item);
+
+ GDVIRTUAL1RC(bool, _has_gizmo, CanvasItem *)
+ GDVIRTUAL1RC(Ref, _create_gizmo, CanvasItem *)
+
+ GDVIRTUAL0RC(String, _get_gizmo_name)
+ GDVIRTUAL0RC(int, _get_priority)
+ GDVIRTUAL0RC(bool, _can_be_hidden)
+ GDVIRTUAL0RC(bool, _is_selectable_when_hidden)
+
+ GDVIRTUAL1(_redraw, Ref)
+
+ GDVIRTUAL1RC(bool, _edit_use_rect, Ref)
+ GDVIRTUAL2(_edit_set_rect, Ref, Rect2)
+ GDVIRTUAL1RC(Rect2, _edit_get_rect, Ref)
+
+ GDVIRTUAL1RC(bool, _has_pivot, Ref)
+ GDVIRTUAL2(_set_pivot, Ref, Vector2)
+ GDVIRTUAL1RC(Vector2, _get_pivot, Ref)
+
+ GDVIRTUAL1RC(Dictionary, _edit_get_state, Ref)
+ GDVIRTUAL2C(_edit_set_state, Ref, Dictionary)
+
+ GDVIRTUAL3RC(String, _get_handle_name, Ref, int, bool)
+ GDVIRTUAL3RC(bool, _is_handle_highlighted, Ref, int, bool)
+ GDVIRTUAL3RC(Variant, _get_handle_value, Ref, int, bool)
+ GDVIRTUAL3(_begin_handle_action, Ref, int, bool)
+ GDVIRTUAL4(_set_handle, Ref, int, bool, Vector2)
+ GDVIRTUAL5(_commit_handle, Ref, int, bool, Variant, bool)
+
+ GDVIRTUAL3RC(int, _subgizmos_intersect_point, Ref, Vector2, real_t)
+ GDVIRTUAL2RC(Vector, _subgizmos_intersect_rect, Ref, Rect2)
+ GDVIRTUAL2RC(Transform2D, _get_subgizmo_transform, Ref, int)
+ GDVIRTUAL3(_set_subgizmo_transform, Ref, int, Transform2D)
+ GDVIRTUAL4(_commit_subgizmos, Ref, Vector, TypedArray, bool)
+
+public:
+ static Transform2D boundary_change_to_transform(const Rect2 &p_before, const Rect2 &p_after);
+
+ virtual String get_gizmo_name() const;
+ virtual int get_priority() const;
+ virtual bool can_be_hidden() const;
+ virtual bool is_selectable_when_hidden() const;
+ virtual bool can_commit_handle_on_click() const;
+
+ virtual void redraw(EditorCanvasItemGizmo *p_gizmo);
+ virtual bool _edit_use_rect(const EditorCanvasItemGizmo *p_gizmo) const;
+ virtual void _edit_set_rect(const EditorCanvasItemGizmo *p_gizmo, Rect2 p_rect);
+ virtual Rect2 _edit_get_rect(const EditorCanvasItemGizmo *p_gizmo) const;
+
+ // pivot handle
+ virtual bool _has_pivot(const EditorCanvasItemGizmo *p_gizmo) const;
+ virtual Vector2 _get_pivot(const EditorCanvasItemGizmo *p_gizmo) const;
+ virtual void _set_pivot(const EditorCanvasItemGizmo *p_gizmo, const Vector2 &p_point);
+
+ // edit state
+ virtual Dictionary _edit_get_state(const EditorCanvasItemGizmo *p_gizmo) const;
+ virtual void _edit_set_state(const EditorCanvasItemGizmo *p_gizmo, const Dictionary &p_state);
+
+ // extra handles
+ virtual bool _is_handle_highlighted(const EditorCanvasItemGizmo *p_gizmo, int p_id, bool p_secondary) const;
+ virtual String _get_handle_name(const EditorCanvasItemGizmo *p_gizmo, int p_id, bool p_secondary) const;
+ virtual Variant _get_handle_value(const EditorCanvasItemGizmo *p_gizmo, int p_id, bool p_secondary) const;
+ virtual void _begin_handle_action(const EditorCanvasItemGizmo *p_gizmo, int p_id, bool p_secondary);
+ virtual void _set_handle(const EditorCanvasItemGizmo *p_gizmo, int p_id, bool p_secondary, const Point2 &p_point);
+ virtual void _commit_handle(const EditorCanvasItemGizmo *p_gizmo, int p_id, bool p_secondary, const Variant &p_restore, bool p_cancel = false);
+
+ // subgizmos
+ virtual int _subgizmos_intersect_point(const EditorCanvasItemGizmo *p_gizmo, const Vector2 &p_point, real_t p_max_distance) const;
+ virtual Vector _subgizmos_intersect_rect(const EditorCanvasItemGizmo *p_gizmo, const Rect2 &p_rect) const;
+ virtual Transform2D _get_subgizmo_transform(const EditorCanvasItemGizmo *p_gizmo, int p_id) const;
+ virtual void _set_subgizmo_transform(const EditorCanvasItemGizmo *p_gizmo, int p_id, const Transform2D &p_xform);
+ virtual void _commit_subgizmos(const EditorCanvasItemGizmo *p_gizmo, const Vector &p_ids, const Vector &p_transforms, bool p_cancel = false);
+
+ Ref get_gizmo(CanvasItem *p_canvas_item);
+ void set_gizmos_visible(bool p_visible);
+ bool is_gizmos_visible() const;
+ void unregister_gizmo(EditorCanvasItemGizmo *p_gizmo);
+
+ EditorCanvasItemGizmoPlugin();
+ virtual ~EditorCanvasItemGizmoPlugin();
+};
diff --git a/editor/scene/canvas_item_editor_plugin.cpp b/editor/scene/canvas_item_editor_plugin.cpp
index 241d8ba54d6a..d084e9eb47a1 100644
--- a/editor/scene/canvas_item_editor_plugin.cpp
+++ b/editor/scene/canvas_item_editor_plugin.cpp
@@ -49,7 +49,6 @@
#include "editor/inspector/editor_context_menu_plugin.h"
#include "editor/plugins/editor_plugin_list.h"
#include "editor/run/editor_run_bar.h"
-#include "editor/script/script_editor_plugin.h"
#include "editor/settings/editor_settings.h"
#include "editor/themes/editor_scale.h"
#include "editor/themes/editor_theme_manager.h"
@@ -274,6 +273,90 @@ class SnapDialog : public ConfirmationDialog {
}
};
+// Helpers for bridging legacy editing system with gizmos.
+Rect2 _get_edit_rect(const CanvasItem *p_canvas_item) {
+ ERR_FAIL_NULL_V(p_canvas_item, Rect2());
+ for (Ref gizmo : p_canvas_item->get_gizmos()) {
+ if (gizmo.is_valid()) {
+ return gizmo->_edit_get_rect();
+ }
+ }
+ return p_canvas_item->_edit_get_rect();
+}
+
+bool _use_edit_rect(const CanvasItem *p_canvas_item) {
+ ERR_FAIL_NULL_V(p_canvas_item, false);
+ for (Ref gizmo : p_canvas_item->get_gizmos()) {
+ if (gizmo.is_valid()) {
+ return gizmo->_edit_use_rect();
+ }
+ }
+ return p_canvas_item->_edit_use_rect();
+}
+
+void _set_edit_rect(CanvasItem *p_canvas_item, const Rect2 &p_rect) {
+ ERR_FAIL_NULL(p_canvas_item);
+ for (Ref gizmo : p_canvas_item->get_gizmos()) {
+ if (gizmo.is_valid()) {
+ gizmo->_edit_set_rect(p_rect);
+ return;
+ }
+ }
+ p_canvas_item->_edit_set_rect(p_rect);
+}
+
+Vector2 _get_edit_pivot(const CanvasItem *p_canvas_item) {
+ ERR_FAIL_NULL_V(p_canvas_item, Vector2());
+ for (Ref gizmo : p_canvas_item->get_gizmos()) {
+ if (gizmo.is_valid() && gizmo->_has_pivot()) {
+ return gizmo->_get_pivot();
+ }
+ }
+ return p_canvas_item->_edit_get_pivot();
+}
+
+void _set_edit_pivot(CanvasItem *p_canvas_item, const Vector2 &p_pivot) {
+ ERR_FAIL_NULL(p_canvas_item);
+ for (Ref gizmo : p_canvas_item->get_gizmos()) {
+ if (gizmo.is_valid() && gizmo->_has_pivot()) {
+ gizmo->_set_pivot(p_pivot);
+ return;
+ }
+ }
+ p_canvas_item->_edit_set_pivot(p_pivot);
+}
+
+bool _use_edit_pivot(const CanvasItem *p_canvas_item) {
+ ERR_FAIL_NULL_V(p_canvas_item, false);
+ for (Ref gizmo : p_canvas_item->get_gizmos()) {
+ if (gizmo.is_valid() && gizmo->_has_pivot()) {
+ return true;
+ }
+ }
+ return p_canvas_item->_edit_use_pivot();
+}
+
+Dictionary _get_edit_state(const CanvasItem *p_canvas_item) {
+ ERR_FAIL_NULL_V(p_canvas_item, Dictionary());
+ for (Ref gizmo : p_canvas_item->get_gizmos()) {
+ if (gizmo.is_valid()) {
+ return gizmo->_edit_get_state();
+ }
+ }
+ return p_canvas_item->_edit_get_state();
+}
+
+void _set_edit_state(CanvasItem *p_canvas_item, const Dictionary &p_state) {
+ ERR_FAIL_NULL(p_canvas_item);
+ for (Ref gizmo : p_canvas_item->get_gizmos()) {
+ if (gizmo.is_valid()) {
+ gizmo->_edit_set_state(p_state);
+ return;
+ }
+ }
+ p_canvas_item->_edit_set_state(p_state);
+}
+
bool CanvasItemEditor::_is_node_locked(const Node *p_node) const {
return p_node->get_meta("_edit_lock_", false);
}
@@ -354,9 +437,10 @@ void CanvasItemEditor::_snap_other_nodes(
if (ci && !exception) {
Transform2D ci_transform = ci->get_screen_transform();
if (std::fmod(ci_transform.get_rotation() - p_transform_to_snap.get_rotation(), (real_t)360.0) == 0.0) {
- if (ci->_edit_use_rect()) {
- Point2 begin = ci_transform.xform(ci->_edit_get_rect().get_position());
- Point2 end = ci_transform.xform(ci->_edit_get_rect().get_position() + ci->_edit_get_rect().get_size());
+ if (_use_edit_rect(ci)) {
+ Rect2 rect = _get_edit_rect(ci);
+ Point2 begin = ci_transform.xform(rect.get_position());
+ Point2 end = ci_transform.xform(rect.get_position() + rect.get_size());
_snap_if_closer_point(p_value, r_current_snap, r_current_snap_target, begin, p_snap_target, ci_transform.get_rotation());
_snap_if_closer_point(p_value, r_current_snap, r_current_snap_target, end, p_snap_target, ci_transform.get_rotation());
@@ -393,9 +477,10 @@ Point2 CanvasItemEditor::snap_point(Point2 p_target, unsigned int p_modes, unsig
_snap_if_closer_point(p_target, output, snap_target, (begin + end) / 2.0, SNAP_TARGET_PARENT, rotation);
_snap_if_closer_point(p_target, output, snap_target, end, SNAP_TARGET_PARENT, rotation);
} else if (const CanvasItem *parent_ci = Object::cast_to(p_self_canvas_item->get_parent())) {
- if (parent_ci->_edit_use_rect()) {
- Point2 begin = p_self_canvas_item->get_transform().affine_inverse().xform(parent_ci->_edit_get_rect().get_position());
- Point2 end = p_self_canvas_item->get_transform().affine_inverse().xform(parent_ci->_edit_get_rect().get_position() + parent_ci->_edit_get_rect().get_size());
+ if (_use_edit_rect(parent_ci)) {
+ Rect2 rect = _get_edit_rect(parent_ci);
+ Point2 begin = p_self_canvas_item->get_transform().affine_inverse().xform(rect.get_position());
+ Point2 end = p_self_canvas_item->get_transform().affine_inverse().xform(rect.get_position() + rect.get_size());
_snap_if_closer_point(p_target, output, snap_target, begin, SNAP_TARGET_PARENT, rotation);
_snap_if_closer_point(p_target, output, snap_target, (begin + end) / 2.0, SNAP_TARGET_PARENT, rotation);
_snap_if_closer_point(p_target, output, snap_target, end, SNAP_TARGET_PARENT, rotation);
@@ -418,9 +503,10 @@ Point2 CanvasItemEditor::snap_point(Point2 p_target, unsigned int p_modes, unsig
// Self sides
if ((is_snap_active && snap_node_sides && (p_modes & SNAP_NODE_SIDES)) || (p_forced_modes & SNAP_NODE_SIDES)) {
- if (p_self_canvas_item->_edit_use_rect()) {
- Point2 begin = p_self_canvas_item->get_screen_transform().xform(p_self_canvas_item->_edit_get_rect().get_position());
- Point2 end = p_self_canvas_item->get_screen_transform().xform(p_self_canvas_item->_edit_get_rect().get_position() + p_self_canvas_item->_edit_get_rect().get_size());
+ if (_use_edit_rect(p_self_canvas_item)) {
+ Rect2 rect = _get_edit_rect(p_self_canvas_item);
+ Point2 begin = p_self_canvas_item->get_screen_transform().xform(rect.get_position());
+ Point2 end = p_self_canvas_item->get_screen_transform().xform(rect.get_position() + rect.get_size());
_snap_if_closer_point(p_target, output, snap_target, begin, SNAP_TARGET_SELF, rotation);
_snap_if_closer_point(p_target, output, snap_target, end, SNAP_TARGET_SELF, rotation);
}
@@ -428,8 +514,9 @@ Point2 CanvasItemEditor::snap_point(Point2 p_target, unsigned int p_modes, unsig
// Self center
if ((is_snap_active && snap_node_center && (p_modes & SNAP_NODE_CENTER)) || (p_forced_modes & SNAP_NODE_CENTER)) {
- if (p_self_canvas_item->_edit_use_rect()) {
- Point2 center = p_self_canvas_item->get_screen_transform().xform(p_self_canvas_item->_edit_get_rect().get_center());
+ if (_use_edit_rect(p_self_canvas_item)) {
+ Rect2 rect = _get_edit_rect(p_self_canvas_item);
+ Point2 center = p_self_canvas_item->get_screen_transform().xform(rect.get_center());
_snap_if_closer_point(p_target, output, snap_target, center, SNAP_TARGET_SELF, rotation);
} else {
Point2 position = p_self_canvas_item->get_screen_transform().xform(Point2());
@@ -578,13 +665,13 @@ Rect2 CanvasItemEditor::_get_encompassing_rect_from_list(const Listget();
- Rect2 rect = Rect2(ci->get_global_transform_with_canvas().xform(ci->_edit_get_rect().get_center()), Size2());
+ Rect2 rect = Rect2(ci->get_global_transform_with_canvas().xform(_get_edit_rect(ci).get_center()), Size2());
// Expand with the other ones
for (CanvasItem *ci2 : p_list) {
Transform2D xform = ci2->get_global_transform_with_canvas();
- Rect2 current_rect = ci2->_edit_get_rect();
+ Rect2 current_rect = _get_edit_rect(ci2);
rect.expand_to(xform.xform(current_rect.position));
rect.expand_to(xform.xform(current_rect.position + Vector2(current_rect.size.x, 0)));
rect.expand_to(xform.xform(current_rect.position + current_rect.size));
@@ -619,7 +706,7 @@ void CanvasItemEditor::_expand_encompassing_rect_using_children(Rect2 &r_rect, c
xform *= p_parent_xform;
}
xform *= ci->get_transform();
- Rect2 rect = ci->_edit_get_rect();
+ Rect2 rect = _get_edit_rect(ci);
if (r_first) {
r_rect = Rect2(xform.xform(rect.get_center()), Size2());
r_first = false;
@@ -676,8 +763,30 @@ void CanvasItemEditor::find_canvas_items_at_pos(const Point2 &p_pos, Node *p_nod
xform *= p_parent_xform;
}
xform = (xform * ci->get_transform()).affine_inverse();
+ const Vector2 point = xform.xform(p_pos);
const real_t local_grab_distance = xform.basis_xform(Vector2(grab_distance, 0)).length() / zoom;
- if (ci->_edit_is_selected_on_click(xform.xform(p_pos), local_grab_distance)) {
+
+ // gizmos way
+ // note: this may produce duplicate entries but the calling
+ // function is filtering for duplicates anyways, so we're not spending
+ // any extra effort here.
+ Vector[> gizmos = ci->get_gizmos();
+ for (Ref gizmo : gizmos) {
+ if (gizmo.is_null()) {
+ continue;
+ }
+ if (gizmo->intersect_point(point, local_grab_distance)) {
+ Node2D *node = Object::cast_to(ci);
+ SelectResult res;
+ res.item = ci;
+ res.z_index = node ? node->get_z_index() : 0;
+ res.has_z = true;
+ r_items.push_back(res);
+ }
+ }
+
+ // legacy way
+ if (ci->_edit_is_selected_on_click(point, local_grab_distance)) {
Node2D *node = Object::cast_to(ci);
SelectResult res;
@@ -783,6 +892,8 @@ void CanvasItemEditor::_find_canvas_items_in_rect(const Rect2 &p_rect, Node *p_n
}
xform *= ci->get_transform();
+ // legacy way - this deliberately does not use the _get_bounding_rect function, as with gizmos we use
+ // gizmo collision shapes rather than bounding boxes for hit detection
if (ci->_edit_use_rect()) {
Rect2 rect = ci->_edit_get_rect();
if (p_rect.has_point(xform.xform(rect.position)) &&
@@ -796,10 +907,89 @@ void CanvasItemEditor::_find_canvas_items_in_rect(const Rect2 &p_rect, Node *p_n
r_items->push_back(ci);
}
}
+
+ // gizmos way
+ Vector][> gizmos = ci->get_gizmos();
+ for (Ref gizmo : gizmos) {
+ if (gizmo.is_null()) {
+ continue;
+ }
+ if (gizmo->intersect_rect(p_rect)) {
+ r_items->push_back(ci);
+ }
+ }
+ }
+}
+
+bool CanvasItemEditor::_select_subgizmos(Point2 p_click_pos, bool p_append) {
+ if (selected_canvas_item) {
+ CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data(selected_canvas_item);
+ if (se) {
+ bool selection_changed = false;
+
+ for (Ref gizmo : selected_canvas_item->get_gizmos()) {
+ if (gizmo.is_null()) {
+ continue;
+ }
+
+ Transform2D xform = selected_canvas_item->get_global_transform().affine_inverse();
+ Point2 local_pos = xform.xform(p_click_pos);
+ const real_t local_grab_distance = xform.basis_xform(Vector2(grab_distance, 0)).length() / zoom;
+
+ int subgizmo_id = gizmo->_subgizmos_intersect_point(local_pos, local_grab_distance);
+ if (subgizmo_id >= 0) {
+ if (p_append) {
+ if (se->subgizmos.has(subgizmo_id)) {
+ se->subgizmos.erase(subgizmo_id);
+ } else {
+ se->subgizmos.insert(subgizmo_id, gizmo->_get_subgizmo_transform(subgizmo_id));
+ }
+ selection_changed = true;
+ } else {
+ // replacement selection
+ // this works like the Node selection - if you click on an already
+ // selected subgizmo, nothing happens, even if multiple other
+ // subgizmos of the same gizmo are selected
+ // to deselect, you need to click in a free space, otherwise we cannot tell
+ // reselect from a drag attempt.
+ selection_changed = se->gizmo != gizmo || !se->subgizmos.has(subgizmo_id);
+ if (selection_changed) {
+ se->subgizmos.clear();
+ se->subgizmos.insert(subgizmo_id, gizmo->_get_subgizmo_transform(subgizmo_id));
+ }
+ }
+
+ if (se->subgizmos.is_empty()) {
+ se->gizmo = Ref();
+ } else {
+ se->gizmo = gizmo;
+ }
+
+ if (selection_changed) {
+ gizmo->redraw();
+ }
+ update_transform_gizmo();
+
+ // first hit wins
+ return selection_changed;
+ }
+ }
+
+ // no hit, at all. if we're not appending, clear the subgizmo selection
+ if (!p_append) {
+ if (se->gizmo.is_valid()) {
+ se->subgizmos.clear();
+ se->gizmo->redraw();
+ se->gizmo = Ref();
+ return true;
+ }
+ }
+ }
}
+ return false;
}
-bool CanvasItemEditor::_select_click_on_item(CanvasItem *item, Point2 p_click_pos, bool p_append) {
+bool CanvasItemEditor::_select_click_on_item(CanvasItem *item, bool p_append) {
bool still_selected = true;
const List &top_node_list = editor_selection->get_top_selected_node_list();
if (p_append && !top_node_list.is_empty()) {
@@ -896,11 +1086,40 @@ Vector2 CanvasItemEditor::_position_to_anchor(const Control *p_control, Vector2
return output;
}
-void CanvasItemEditor::_save_canvas_item_state(const List &p_canvas_items, bool save_bones) {
+void CanvasItemEditor::_update_gizmos_menu() {
+ gizmos_menu->clear();
+
+ // built-in 2D gizmos
+ gizmos_menu->add_check_shortcut(ED_SHORTCUT("canvas_item_editor/show_position_gizmos", TTRC("Position")), SHOW_POSITION_GIZMOS);
+ gizmos_menu->set_item_checked(0, show_position_gizmos);
+ gizmos_menu->add_check_shortcut(ED_SHORTCUT("canvas_item_editor/show_lock_gizmos", TTRC("Lock")), SHOW_LOCK_GIZMOS);
+ gizmos_menu->set_item_checked(1, show_lock_gizmos);
+ gizmos_menu->add_check_shortcut(ED_SHORTCUT("canvas_item_editor/show_group_gizmos", TTRC("Group")), SHOW_GROUP_GIZMOS);
+ gizmos_menu->set_item_checked(2, show_group_gizmos);
+ gizmos_menu->add_check_shortcut(ED_SHORTCUT("canvas_item_editor/show_transformation_gizmos", TTRC("Transformation")), SHOW_TRANSFORMATION_GIZMOS);
+ gizmos_menu->set_item_checked(3, show_transformation_gizmos);
+
+ for (int i = 0; i < gizmo_plugins_by_name.size(); i++) {
+ Ref plugin = gizmo_plugins_by_name[i];
+ if (!plugin->can_be_hidden()) {
+ continue;
+ }
+
+ String plugin_name = plugin->get_gizmo_name();
+ int plugin_id = SHOW_USER_DEFINED_GIZMO + i;
+ bool gizmos_visible = plugin->is_gizmos_visible();
+
+ gizmos_menu->add_check_item(plugin_name, plugin_id);
+ int index = gizmos_menu->get_item_index(plugin_id);
+ gizmos_menu->set_item_checked(index, gizmos_visible);
+ }
+}
+
+void CanvasItemEditor::_save_drag_selection_state() {
original_transform = Transform2D();
bool transform_stored = false;
- for (CanvasItem *ci : p_canvas_items) {
+ for (CanvasItem *ci : drag_selection) {
CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data(ci);
if (se) {
if (!transform_stored) {
@@ -908,10 +1127,11 @@ void CanvasItemEditor::_save_canvas_item_state(const List &p_canva
transform_stored = true;
}
- se->undo_state = ci->_edit_get_state();
+ se->undo_state = _get_edit_state(ci);
se->pre_drag_xform = ci->get_screen_transform();
- if (ci->_edit_use_rect()) {
- se->pre_drag_rect = ci->_edit_get_rect();
+ Rect2 rect = _get_edit_rect(ci);
+ if (rect != Rect2()) {
+ se->pre_drag_rect = rect;
} else {
se->pre_drag_rect = Rect2();
}
@@ -919,18 +1139,76 @@ void CanvasItemEditor::_save_canvas_item_state(const List &p_canva
}
}
-void CanvasItemEditor::_restore_canvas_item_state(const List &p_canvas_items, bool restore_bones) {
+void CanvasItemEditor::_restore_drag_selection_state() {
+ // restore any subgizmo state, if we have a gizmo selected
+ for (CanvasItem *ci : drag_selection) {
+ CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data(ci);
+ if (se->gizmo.is_valid()) {
+ // if the gizmo is valid, we have transformed subgizmos, not the canvas item(s) themselves, so
+ // we restore the subgizmos, only and end it here.
+ Vector ids;
+ Vector xforms;
+ for (const KeyValue &entry : se->subgizmos) {
+ ids.push_back(entry.key);
+ xforms.push_back(entry.value);
+ }
+ // rollback subgizmo changes
+ se->gizmo->_commit_subgizmos(ids, xforms, true);
+
+ // since only one gizmo can ever be selected at the same time, we stop it here.
+ return;
+ }
+ }
+
+ // if we're here, no subgizmo was selected, and we do the regular canvas item restore
for (CanvasItem *ci : drag_selection) {
CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data(ci);
- ci->_edit_set_state(se->undo_state);
+ _set_edit_state(ci, se->undo_state);
+ }
+}
+
+CanvasItemEditorSelectedItem *CanvasItemEditor::_get_selected_subgizmo() const {
+ if (selected_canvas_item) {
+ CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data(selected_canvas_item);
+ if (se && se->gizmo.is_valid()) {
+ return se;
+ }
}
+ return nullptr;
}
-void CanvasItemEditor::_commit_canvas_item_state(const List &p_canvas_items, const String &action_name, bool commit_bones) {
+void CanvasItemEditor::_commit_drag_selection_state(const String &action_name) {
List modified_canvas_items;
- for (CanvasItem *ci : p_canvas_items) {
- Dictionary old_state = editor_selection->get_node_editor_data(ci)->undo_state;
- Dictionary new_state = ci->_edit_get_state();
+ for (CanvasItem *ci : drag_selection) {
+ CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data(ci);
+ if (se->gizmo.is_valid()) {
+ // if the gizmo is valid, we have transformed subgizmos, not the canvas item(s) themselves, so
+ // we commit the subgizmos, only and end it here.
+ Vector ids;
+ Vector xforms;
+ for (const KeyValue &entry : se->subgizmos) {
+ ids.push_back(entry.key);
+ xforms.push_back(entry.value);
+ }
+ // commit subgizmo changes (gizmos do their own redo/undo)
+ se->gizmo->_commit_subgizmos(ids, xforms, false);
+
+ // we also need to store the new starting transforms of our subgizmos
+ for (const KeyValue &entry : se->subgizmos) {
+ se->subgizmos[entry.key] = se->gizmo->_get_subgizmo_transform(entry.key);
+ }
+
+ // since only one gizmo can ever be selected at the same time, we stop it here.
+ return;
+ }
+ }
+
+ // if we are here, no subgizmo was selected, and we do the regular canvas item commit
+ for (CanvasItem *ci : drag_selection) {
+ CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data(ci);
+
+ Dictionary old_state = se->undo_state;
+ Dictionary new_state = _get_edit_state(ci);
if (old_state.hash() != new_state.hash()) {
modified_canvas_items.push_back(ci);
@@ -946,15 +1224,22 @@ void CanvasItemEditor::_commit_canvas_item_state(const List &p_can
for (CanvasItem *ci : modified_canvas_items) {
CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data(ci);
if (se) {
- undo_redo->add_do_method(ci, "_edit_set_state", ci->_edit_get_state());
- undo_redo->add_undo_method(ci, "_edit_set_state", se->undo_state);
- if (commit_bones) {
- for (const Dictionary &F : se->pre_drag_bones_undo_state) {
- ci = Object::cast_to(ci->get_parent());
- undo_redo->add_do_method(ci, "_edit_set_state", ci->_edit_get_state());
- undo_redo->add_undo_method(ci, "_edit_set_state", F);
+ // if the object has gizmos, the state save/restore goes through them
+ bool gizmos_handle_state = false;
+ for (Ref gizmo : ci->get_gizmos()) {
+ if (gizmo.is_valid()) {
+ undo_redo->add_do_method(gizmo.ptr(), "_edit_set_state", _get_edit_state(ci));
+ undo_redo->add_undo_method(gizmo.ptr(), "_edit_set_state", se->undo_state);
+ gizmos_handle_state = true;
+ // highest priority gizmo wins
+ break;
}
}
+ if (!gizmos_handle_state) {
+ // fall back to legacy implementation.
+ undo_redo->add_do_method(ci, "_edit_set_state", ci->_edit_get_state());
+ undo_redo->add_undo_method(ci, "_edit_set_state", se->undo_state);
+ }
}
}
undo_redo->add_do_method(viewport, "queue_redraw");
@@ -985,7 +1270,7 @@ void CanvasItemEditor::_selection_result_pressed(int p_result) {
CanvasItem *item = selection_results_menu[p_result].item;
if (item) {
- _select_click_on_item(item, Point2(), selection_menu_additive_selection);
+ _select_click_on_item(item, selection_menu_additive_selection);
}
selection_results_menu.clear();
}
@@ -1481,7 +1766,7 @@ bool CanvasItemEditor::_gui_input_pivot(const Ref &p_event) {
// Filters the selection with nodes that allow setting the pivot
drag_selection = List();
for (CanvasItem *ci : selection) {
- if (ci->_edit_use_pivot() || move_temp_pivot) {
+ if (_use_edit_pivot(ci) || move_temp_pivot) {
drag_selection.push_back(ci);
}
}
@@ -1497,7 +1782,7 @@ bool CanvasItemEditor::_gui_input_pivot(const Ref &p_event) {
return true;
}
- _save_canvas_item_state(drag_selection);
+ _save_drag_selection_state();
drag_from = transform.affine_inverse().xform(event_pos);
Vector2 new_pos;
if (drag_selection.size() == 1) {
@@ -1506,7 +1791,7 @@ bool CanvasItemEditor::_gui_input_pivot(const Ref &p_event) {
new_pos = snap_point(drag_from, SNAP_OTHER_NODES | SNAP_GRID | SNAP_PIXEL, 0, nullptr, drag_selection);
}
for (CanvasItem *ci : drag_selection) {
- ci->_edit_set_pivot(ci->get_screen_transform().affine_inverse().xform(new_pos));
+ _set_edit_pivot(ci, ci->get_screen_transform().affine_inverse().xform(new_pos));
}
drag_type = DRAG_PIVOT;
@@ -1519,7 +1804,7 @@ bool CanvasItemEditor::_gui_input_pivot(const Ref &p_event) {
// Move the pivot
if (m.is_valid()) {
drag_to = transform.affine_inverse().xform(m->get_position());
- _restore_canvas_item_state(drag_selection);
+ _restore_drag_selection_state();
Vector2 new_pos;
if (drag_selection.size() == 1) {
new_pos = snap_point(drag_to, SNAP_NODE_SIDES | SNAP_NODE_CENTER | SNAP_NODE_ANCHORS | SNAP_OTHER_NODES | SNAP_GRID | SNAP_PIXEL, 0, drag_selection.front()->get());
@@ -1527,7 +1812,7 @@ bool CanvasItemEditor::_gui_input_pivot(const Ref &p_event) {
new_pos = snap_point(drag_to, SNAP_OTHER_NODES | SNAP_GRID | SNAP_PIXEL);
}
for (CanvasItem *ci : drag_selection) {
- ci->_edit_set_pivot(ci->get_screen_transform().affine_inverse().xform(new_pos));
+ _set_edit_pivot(ci, ci->get_screen_transform().affine_inverse().xform(new_pos));
}
return true;
}
@@ -1544,7 +1829,7 @@ bool CanvasItemEditor::_gui_input_pivot(const Ref &p_event) {
// Cancel a drag
if (ED_IS_SHORTCUT("canvas_item_editor/cancel_transform", p_event) || (b.is_valid() && b->get_button_index() == MouseButton::RIGHT && b->is_pressed())) {
- _restore_canvas_item_state(drag_selection);
+ _restore_drag_selection_state();
snap_target[0] = SNAP_TARGET_NONE;
snap_target[1] = SNAP_TARGET_NONE;
_reset_drag();
@@ -1583,7 +1868,15 @@ bool CanvasItemEditor::_gui_input_rotate(const Ref &p_event) {
// Remove not movable nodes
for (List::Element *E = selection.front(); E;) {
List::Element *N = E->next();
- if (!_is_node_movable(E->get(), true)) {
+ CanvasItem *ci = E->get();
+
+ if (!_is_node_movable(ci, true)) {
+ // but if the node has currently subgizmos selected, we can still rotate these
+ CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data(ci);
+ if (se->gizmo.is_valid()) {
+ continue;
+ }
+ // otherwise it is out
selection.erase(E);
}
E = N;
@@ -1594,14 +1887,15 @@ bool CanvasItemEditor::_gui_input_rotate(const Ref &p_event) {
drag_type = DRAG_ROTATE;
drag_from = transform.affine_inverse().xform(b->get_position());
CanvasItem *ci = drag_selection.front()->get();
+ CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data(ci);
if (!Math::is_inf(temp_pivot.x) || !Math::is_inf(temp_pivot.y)) {
drag_rotation_center = temp_pivot;
- } else if (ci->_edit_use_pivot()) {
- drag_rotation_center = ci->get_screen_transform().xform(ci->_edit_get_pivot());
+ } else if (_use_edit_pivot(ci)) {
+ drag_rotation_center = ci->get_screen_transform().xform(_get_edit_pivot(ci));
} else {
drag_rotation_center = ci->get_screen_transform().get_origin();
}
- _save_canvas_item_state(drag_selection);
+ _save_drag_selection_state();
return true;
} else {
if (has_locked_items) {
@@ -1616,9 +1910,39 @@ bool CanvasItemEditor::_gui_input_rotate(const Ref &p_event) {
if (drag_type == DRAG_ROTATE) {
// Rotate the node
if (m.is_valid()) {
- _restore_canvas_item_state(drag_selection);
+ drag_to = transform.affine_inverse().xform(m->get_position());
+
+ // subgizmo handling
+ {
+ CanvasItemEditorSelectedItem *se = _get_selected_subgizmo();
+ if (se) {
+ bool opposite = (selected_canvas_item->get_global_transform().get_scale().sign().dot(selected_canvas_item->get_transform().get_scale().sign()) == 0);
+ real_t angle = (opposite ? -1 : 1) * snap_angle((drag_from - drag_rotation_center).angle_to(drag_to - drag_rotation_center));
+
+ for (KeyValue &entry : se->subgizmos) {
+ Transform2D new_xform;
+ if (!temp_pivot.is_finite()) {
+ // if we have no temp pivot, just rotate around their individual centers, similar to how it is done
+ // for multiple canvas item selections.
+ Vector2 origin = entry.value.get_origin();
+ new_xform = entry.value.translated(-origin).rotated(angle).translated(origin);
+ } else {
+ // Rotate around the temp pivot. The temp pivot is in global space, so we need to transform into local space as
+ // gizmo transforms are local.
+ Vector2 local_temp_pivot = selected_canvas_item->get_global_transform().affine_inverse().xform(temp_pivot);
+ new_xform = entry.value.translated(-local_temp_pivot).rotated(angle).translated(local_temp_pivot);
+ }
+ se->gizmo->_set_subgizmo_transform(entry.key, new_xform);
+ }
+ viewport->queue_redraw();
+ return true;
+ }
+ }
+
+ // normal node handling
+ _restore_drag_selection_state();
for (CanvasItem *ci : drag_selection) {
- drag_to = transform.affine_inverse().xform(m->get_position());
+ CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data(ci);
//Rotate the opposite way if the canvas item's compounded scale has an uneven number of negative elements
bool opposite = (ci->get_global_transform().get_scale().sign().dot(ci->get_transform().get_scale().sign()) == 0);
real_t prev_rotation = ci->_edit_get_rotation();
@@ -1644,7 +1968,7 @@ bool CanvasItemEditor::_gui_input_rotate(const Ref &p_event) {
// Cancel a drag
if (ED_IS_SHORTCUT("canvas_item_editor/cancel_transform", p_event) || (b.is_valid() && b->get_button_index() == MouseButton::RIGHT && b->is_pressed())) {
- _restore_canvas_item_state(drag_selection);
+ _restore_drag_selection_state();
_reset_drag();
viewport->queue_redraw();
return true;
@@ -1670,6 +1994,94 @@ bool CanvasItemEditor::_gui_input_open_scene_on_double_click(const Ref &p_event) {
+ Ref m = p_event;
+ Ref b = p_event;
+
+ if (drag_type == DRAG_NONE && selected_canvas_item) {
+ Vector][> gizmos = selected_canvas_item->get_gizmos();
+
+ // mouse clicks
+ if (b.is_valid() && b->get_button_index() == MouseButton::LEFT && b->is_pressed()) {
+ Point2 click = transform.affine_inverse().xform(b->get_position());
+ for (Ref gizmo : gizmos) {
+ if (gizmo.is_null()) {
+ continue;
+ }
+ int index;
+ bool secondary;
+ Transform2D xform = selected_canvas_item->get_global_transform().affine_inverse();
+ const real_t local_grab_distance = xform.basis_xform(Vector2(grab_distance, 0)).length() / zoom;
+ const Point2 local_pos = xform.xform(click);
+ gizmo->handles_intersect_point(local_pos, local_grab_distance, b->is_shift_pressed(), index, secondary);
+ if (index > -1) {
+ drag_selection = List();
+ drag_selection.push_back(selected_canvas_item);
+ drag_type = DRAG_GIZMO_HANDLE;
+ current_gizmo = gizmo;
+ current_gizmo_handle = index;
+ current_gizmo_handle_secondary = secondary;
+ current_gizmo_initial_value = gizmo->_get_handle_value(index, secondary);
+ gizmo->_begin_handle_action(index, secondary);
+ return true;
+ }
+ }
+ }
+
+ // hover support
+ if (m.is_valid()) {
+ Point2 mouse_pos = transform.affine_inverse().xform(m->get_position());
+
+ for (Ref gizmo : gizmos) {
+ int index;
+ bool secondary;
+ Transform2D xform = selected_canvas_item->get_global_transform().affine_inverse();
+ const real_t local_grab_distance = xform.basis_xform(Vector2(grab_distance, 0)).length() / zoom;
+ const Point2 local_pos = xform.xform(mouse_pos);
+ gizmo->handles_intersect_point(local_pos, local_grab_distance, false, index, secondary);
+ if (index > -1) {
+ bool changed = current_hover_gizmo != gizmo || current_hover_gizmo_handle != index || current_hover_gizmo_handle_secondary != secondary;
+ if (changed) {
+ current_hover_gizmo = gizmo;
+ current_hover_gizmo_handle = index;
+ current_hover_gizmo_handle_secondary = secondary;
+ viewport->queue_redraw();
+ }
+ return false; // don't consume the mouse motion, might be important for others
+ }
+ }
+
+ // no gizmo found, reset.
+ bool changed = current_hover_gizmo != nullptr || current_hover_gizmo_handle != -1 || current_hover_gizmo_handle_secondary != false;
+ if (changed) {
+ current_hover_gizmo = nullptr;
+ current_hover_gizmo_handle = -1;
+ current_hover_gizmo_handle_secondary = false;
+ viewport->queue_redraw();
+ }
+ return false;
+ }
+ }
+
+ // mouse drags
+ if (m.is_valid() && drag_type == DRAG_GIZMO_HANDLE) {
+ // handles are supplied in local space around the canvas item, so we need to convert the mouse
+ // position back into local coordinates.
+ Transform2D xform = (get_canvas_transform() * selected_canvas_item->get_screen_transform()).affine_inverse();
+ current_gizmo->_set_handle(current_gizmo_handle, current_gizmo_handle_secondary, xform.xform(m->get_position()));
+ viewport->queue_redraw();
+ return true;
+ }
+
+ // mouse releases
+ if (drag_type == DRAG_GIZMO_HANDLE && b.is_valid() && b->get_button_index() == MouseButton::LEFT && !b->is_pressed()) {
+ _commit_drag();
+ return true;
+ }
+
+ return false;
+}
+
bool CanvasItemEditor::_gui_input_anchors(const Ref &p_event) {
Ref b = p_event;
Ref m = p_event;
@@ -1715,7 +2127,7 @@ bool CanvasItemEditor::_gui_input_anchors(const Ref &p_event) {
drag_from = transform.affine_inverse().xform(b->get_position());
drag_selection = List();
drag_selection.push_back(control);
- _save_canvas_item_state(drag_selection);
+ _save_drag_selection_state();
return true;
}
}
@@ -1727,7 +2139,7 @@ bool CanvasItemEditor::_gui_input_anchors(const Ref &p_event) {
if (drag_type == DRAG_ANCHOR_TOP_LEFT || drag_type == DRAG_ANCHOR_TOP_RIGHT || drag_type == DRAG_ANCHOR_BOTTOM_RIGHT || drag_type == DRAG_ANCHOR_BOTTOM_LEFT || drag_type == DRAG_ANCHOR_ALL) {
// Drag the anchor
if (m.is_valid()) {
- _restore_canvas_item_state(drag_selection);
+ _restore_drag_selection_state();
Control *control = Object::cast_to(drag_selection.front()->get());
drag_to = transform.affine_inverse().xform(m->get_position());
@@ -1803,7 +2215,7 @@ bool CanvasItemEditor::_gui_input_anchors(const Ref &p_event) {
// Cancel a drag
if (ED_IS_SHORTCUT("canvas_item_editor/cancel_transform", p_event) || (b.is_valid() && b->get_button_index() == MouseButton::RIGHT && b->is_pressed())) {
- _restore_canvas_item_state(drag_selection);
+ _restore_drag_selection_state();
snap_target[0] = SNAP_TARGET_NONE;
snap_target[1] = SNAP_TARGET_NONE;
_reset_drag();
@@ -1824,8 +2236,8 @@ bool CanvasItemEditor::_gui_input_resize(const Ref &p_event) {
List selection = _get_edited_canvas_items();
if (selection.size() == 1) {
CanvasItem *ci = selection.front()->get();
- if (ci->_edit_use_rect() && _is_node_movable(ci)) {
- Rect2 rect = ci->_edit_get_rect();
+ if (_use_edit_rect(ci) && _is_node_movable(ci)) {
+ Rect2 rect = _get_edit_rect(ci);
Transform2D xform = transform * ci->get_screen_transform();
const Vector2 endpoints[4] = {
@@ -1872,7 +2284,7 @@ bool CanvasItemEditor::_gui_input_resize(const Ref &p_event) {
drag_from = transform.affine_inverse().xform(b->get_position());
drag_selection = List();
drag_selection.push_back(ci);
- _save_canvas_item_state(drag_selection);
+ _save_drag_selection_state();
return true;
}
}
@@ -1887,12 +2299,12 @@ bool CanvasItemEditor::_gui_input_resize(const Ref &p_event) {
CanvasItem *ci = drag_selection.front()->get();
CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data(ci);
//Reset state
- ci->_edit_set_state(se->undo_state);
+ _set_edit_state(ci, se->undo_state);
bool uniform = m->is_shift_pressed();
bool symmetric = m->is_alt_pressed();
- Rect2 local_rect = ci->_edit_get_rect();
+ Rect2 local_rect = _get_edit_rect(ci);
real_t aspect = local_rect.has_area() ? (local_rect.get_size().y / local_rect.get_size().x) : (local_rect.get_size().y + 1.0) / (local_rect.get_size().x + 1.0);
Point2 current_begin = local_rect.get_position();
Point2 current_end = local_rect.get_position() + local_rect.get_size();
@@ -1963,7 +2375,7 @@ bool CanvasItemEditor::_gui_input_resize(const Ref &p_event) {
current_begin.y = 2.0 * center.y - current_end.y;
}
}
- ci->_edit_set_rect(Rect2(current_begin, current_end - current_begin));
+ _set_edit_rect(ci, Rect2(current_begin, current_end - current_begin));
return true;
}
@@ -1975,7 +2387,7 @@ bool CanvasItemEditor::_gui_input_resize(const Ref &p_event) {
// Cancel a drag
if (ED_IS_SHORTCUT("canvas_item_editor/cancel_transform", p_event) || (b.is_valid() && b->get_button_index() == MouseButton::RIGHT && b->is_pressed())) {
- _restore_canvas_item_state(drag_selection);
+ _restore_drag_selection_state();
snap_target[0] = SNAP_TARGET_NONE;
snap_target[1] = SNAP_TARGET_NONE;
_reset_drag();
@@ -2000,6 +2412,12 @@ bool CanvasItemEditor::_gui_input_scale(const Ref &p_event) {
// Remove non-movable nodes.
for (CanvasItem *ci : selection) {
if (!_is_node_movable(ci, true)) {
+ // if the node has subgizmos selected, we can still scale those
+ CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data(ci);
+ if (se->gizmo.is_valid()) {
+ continue;
+ }
+ // otherwise
selection.erase(ci);
}
}
@@ -2040,7 +2458,7 @@ bool CanvasItemEditor::_gui_input_scale(const Ref &p_event) {
drag_from = transform.affine_inverse().xform(b->get_position());
drag_selection = selection;
- _save_canvas_item_state(drag_selection);
+ _save_drag_selection_state();
return true;
} else {
if (has_locked_items) {
@@ -2052,10 +2470,73 @@ bool CanvasItemEditor::_gui_input_scale(const Ref &p_event) {
} else if (drag_type == DRAG_SCALE_BOTH || drag_type == DRAG_SCALE_X || drag_type == DRAG_SCALE_Y) {
// Resize the node
if (m.is_valid()) {
- _restore_canvas_item_state(drag_selection);
-
drag_to = transform.affine_inverse().xform(m->get_position());
+ // subgizmo handling
+ {
+ CanvasItemEditorSelectedItem *se = _get_selected_subgizmo();
+ if (se) {
+ Transform2D edit_transform;
+ bool uniform = m->is_shift_pressed();
+ bool is_ctrl = m->is_command_or_control_pressed();
+
+ if (temp_pivot.is_finite()) {
+ edit_transform = Transform2D(selected_canvas_item->_edit_get_rotation(), temp_pivot);
+ } else {
+ edit_transform = selected_canvas_item->_edit_get_transform();
+ }
+ Transform2D parent_xform = selected_canvas_item->get_screen_transform() * selected_canvas_item->get_transform().affine_inverse();
+ Transform2D unscaled_transform = (transform * parent_xform * edit_transform).orthonormalized();
+ Transform2D simple_xform;
+
+ if (drag_type == DRAG_SCALE_BOTH) {
+ simple_xform = (viewport->get_transform() * unscaled_transform).affine_inverse() * transform;
+ } else {
+ Transform2D translation = Transform2D(0.0f, unscaled_transform.get_origin());
+ simple_xform = (viewport->get_transform() * translation).affine_inverse() * transform;
+ }
+
+ Point2 drag_from_local = simple_xform.xform(drag_from);
+ Point2 drag_to_local = simple_xform.xform(drag_to);
+ Size2 scale_factor = Size2(1.0f, 1.0f);
+ scale_factor = drag_to_local / drag_from_local;
+
+ if (drag_type == DRAG_SCALE_X) {
+ scale_factor.y = 1.0f;
+ }
+ if (drag_type == DRAG_SCALE_Y) {
+ scale_factor.x = 1.0f;
+ }
+
+ if (uniform) {
+ scale_factor.x = (scale_factor.x + scale_factor.y) / 2.0f;
+ scale_factor.y = scale_factor.x;
+ }
+
+ for (KeyValue &entry : se->subgizmos) {
+ Transform2D gizmo_xform = entry.value;
+ Vector2 gizmo_pos = gizmo_xform.get_origin();
+ // scale in place, move to origin, scale, move back
+ Transform2D new_gizmo_xform = gizmo_xform.translated(-gizmo_pos).scaled(scale_factor).translated(gizmo_pos);
+
+ // if we have a temp pivot, move each gizmo relative to the temp pivot, otherwise use the center of
+ // the selection as pivot
+ Vector2 pivot_local_pos = selected_subgizmos_center;
+ if (temp_pivot.is_finite()) {
+ // temp pivot is in canvas coordinates so we need to convert to to local
+ pivot_local_pos = selected_canvas_item->get_global_transform().affine_inverse().xform(temp_pivot);
+ }
+
+ Vector2 move_towards = (pivot_local_pos - gizmo_xform.get_origin()) * (Vector2(1.0, 1.0) - scale_factor);
+ new_gizmo_xform = new_gizmo_xform.translated(move_towards);
+
+ se->gizmo->_set_subgizmo_transform(entry.key, new_gizmo_xform);
+ }
+ return true;
+ }
+ }
+
+ _restore_drag_selection_state();
Size2 scale_max;
if (drag_type != DRAG_SCALE_BOTH) {
for (CanvasItem *ci : drag_selection) {
@@ -2178,7 +2659,7 @@ bool CanvasItemEditor::_gui_input_scale(const Ref &p_event) {
// Cancel a drag
if (ED_IS_SHORTCUT("canvas_item_editor/cancel_transform", p_event) || (b.is_valid() && b->get_button_index() == MouseButton::RIGHT && b->is_pressed())) {
- _restore_canvas_item_state(drag_selection);
+ _restore_drag_selection_state();
_reset_drag();
viewport->queue_redraw();
return true;
@@ -2233,7 +2714,7 @@ bool CanvasItemEditor::_gui_input_move(const Ref &p_event) {
}
drag_from = transform.affine_inverse().xform(b->get_position());
- _save_canvas_item_state(drag_selection);
+ _save_drag_selection_state();
return true;
} else {
@@ -2249,7 +2730,7 @@ bool CanvasItemEditor::_gui_input_move(const Ref &p_event) {
if (drag_type == DRAG_MOVE || drag_type == DRAG_MOVE_X || drag_type == DRAG_MOVE_Y) {
// Move the nodes
if (m.is_valid() && !drag_selection.is_empty()) {
- _restore_canvas_item_state(drag_selection, true);
+ _restore_drag_selection_state();
drag_to = transform.affine_inverse().xform(m->get_position());
Point2 previous_pos;
@@ -2291,7 +2772,32 @@ bool CanvasItemEditor::_gui_input_move(const Ref &p_event) {
}
}
+ // subgizmo handling
+ {
+ CanvasItemEditorSelectedItem *se = _get_selected_subgizmo();
+ if (se) {
+ // we use the delta for the snapped position, this way we get snapping for subgizmos
+ // positions are in canvas space, so we need to convert it to local space for the gizmo and then calculate
+ // the delta
+ Transform2D parent_xform_inf = selected_canvas_item->get_global_transform().affine_inverse();
+ Vector2 delta = parent_xform_inf.xform(new_pos) - parent_xform_inf.xform(previous_pos);
+ // delta is now in local space of the canvas item.
+
+ for (KeyValue &entry : se->subgizmos) {
+ Transform2D new_xform = entry.value.translated(delta);
+ se->gizmo->_set_subgizmo_transform(entry.key, new_xform);
+ }
+ // need a redraw here because moving subgizmos needs to re-draw the canvas item
+ // we modified to view effects.
+ viewport->queue_redraw();
+ // since only one subgizmo can ever be selected we can move out here.
+ return true;
+ }
+ }
+
+ // normal node handling
for (CanvasItem *ci : drag_selection) {
+ // no subgizmos selected - drag the canvas item
Transform2D parent_xform_inv = ci->get_transform() * ci->get_screen_transform().affine_inverse();
ci->_edit_set_position(ci->_edit_get_position() + parent_xform_inv.basis_xform(new_pos - previous_pos));
}
@@ -2306,7 +2812,7 @@ bool CanvasItemEditor::_gui_input_move(const Ref &p_event) {
// Cancel a drag
if (ED_IS_SHORTCUT("canvas_item_editor/cancel_transform", p_event) || (b.is_valid() && b->get_button_index() == MouseButton::RIGHT && b->is_pressed())) {
- _restore_canvas_item_state(drag_selection, true);
+ _restore_drag_selection_state();
snap_target[0] = SNAP_TARGET_NONE;
snap_target[1] = SNAP_TARGET_NONE;
_reset_drag();
@@ -2332,11 +2838,11 @@ bool CanvasItemEditor::_gui_input_move(const Ref &p_event) {
drag_type = DRAG_KEY_MOVE;
drag_from = Vector2();
drag_to = Vector2();
- _save_canvas_item_state(drag_selection, true);
+ _save_drag_selection_state();
}
if (drag_selection.size() > 0) {
- _restore_canvas_item_state(drag_selection, true);
+ _restore_drag_selection_state();
bool move_local_base = k->is_alt_pressed();
bool move_local_base_rotated = k->is_ctrl_pressed() || k->is_meta_pressed();
@@ -2420,7 +2926,7 @@ bool CanvasItemEditor::_gui_input_select(const Ref &p_event) {
CanvasItem *item = selection_results[0].item;
selection_results.clear();
- _select_click_on_item(item, click, b->is_shift_pressed());
+ _select_click_on_item(item, b->is_shift_pressed());
return true;
} else if (!selection_results.is_empty()) {
@@ -2522,6 +3028,12 @@ bool CanvasItemEditor::_gui_input_select(const Ref &p_event) {
can_select = b->is_pressed() || (drag_type == DRAG_BOX_SELECTION && click.distance_to(drag_from) <= DRAG_THRESHOLD);
}
+ if (can_select) {
+ if (_select_subgizmos(click, b->is_shift_pressed())) {
+ return true;
+ }
+ }
+
if (can_select) {
// Single item selection.
Node *scene = EditorNode::get_singleton()->get_edited_scene();
@@ -2556,7 +3068,7 @@ bool CanvasItemEditor::_gui_input_select(const Ref &p_event) {
return true;
}
} else {
- bool still_selected = _select_click_on_item(ci, click, b->is_shift_pressed());
+ bool still_selected = _select_click_on_item(ci, b->is_shift_pressed());
// Start dragging.
if (still_selected && (tool == TOOL_SELECT || tool == TOOL_MOVE) && b->is_pressed()) {
// Drag the node(s) if requested.
@@ -2592,7 +3104,7 @@ bool CanvasItemEditor::_gui_input_select(const Ref &p_event) {
if (selection2.size() > 0) {
drag_type = DRAG_MOVE;
drag_from = drag_start_origin;
- _save_canvas_item_state(drag_selection);
+ _save_drag_selection_state();
}
return true;
}
@@ -2604,8 +3116,6 @@ bool CanvasItemEditor::_gui_input_select(const Ref &p_event) {
// Confirms box selection.
Node *scene = EditorNode::get_singleton()->get_edited_scene();
if (scene) {
- List selitems;
-
Point2 bsfrom = drag_from;
Point2 bsto = box_selecting_to;
if (bsfrom.x > bsto.x) {
@@ -2615,11 +3125,80 @@ bool CanvasItemEditor::_gui_input_select(const Ref &p_event) {
SWAP(bsfrom.y, bsto.y);
}
- _find_canvas_items_in_rect(Rect2(bsfrom, bsto - bsfrom), scene, &selitems);
+ Rect2 selection_rect = Rect2(bsfrom, bsto - bsfrom);
+
+ // if we have a selected single canvas item, first try to select subgizmos
+ if (selected_canvas_item) {
+ Ref old_gizmo;
+ CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data(selected_canvas_item);
+ if (se) {
+ // clear out existing subgizmo selection unless we want to append
+ if (!b->is_shift_pressed()) {
+ se->subgizmos.clear();
+ old_gizmo = se->gizmo;
+ se->gizmo.unref();
+ }
+
+ bool found_subgizmos = false;
+ Vector][> gizmos = selected_canvas_item->get_gizmos();
+ for (Ref gizmo : gizmos) {
+ if (gizmo.is_null()) {
+ continue;
+ }
+ // only append to subgizmo selection of the currently
+ // selected gizmo
+ if (se->gizmo.is_valid() && se->gizmo != gizmo) {
+ continue;
+ }
+
+ Vector subgizmos = gizmo->_subgizmos_intersect_rect(selection_rect);
+ if (!subgizmos.is_empty()) {
+ se->gizmo = gizmo;
+
+ for (const int &subgizmo_id : subgizmos) {
+ if (!se->subgizmos.has(subgizmo_id)) {
+ se->subgizmos.insert(subgizmo_id, gizmo->_get_subgizmo_transform(subgizmo_id));
+ } else {
+ // technically only when shift is pressed but when it's not
+ // pressed this branch will never be reached because subgizmos are
+ // cleared out above
+ se->subgizmos.erase(subgizmo_id);
+ }
+ }
+ found_subgizmos = true;
+ break; // we only ever select subgizmos from one gizmo
+ }
+ }
+
+ if (!b->is_shift_pressed() || found_subgizmos) {
+ if (se->gizmo.is_valid()) {
+ se->gizmo->redraw();
+ }
+
+ if (old_gizmo != se->gizmo && old_gizmo.is_valid()) {
+ old_gizmo->redraw();
+ }
+
+ update_transform_gizmo();
+ }
+
+ if (found_subgizmos) {
+ _reset_drag();
+ viewport->queue_redraw();
+ return true;
+ }
+ }
+ }
+
+ List selitems;
+
+ _find_canvas_items_in_rect(selection_rect, scene, &selitems);
if (selitems.size() == 1 && editor_selection->get_selection().is_empty()) {
EditorNode::get_singleton()->push_item(selitems.front()->get());
}
for (CanvasItem *E : selitems) {
+ // add_node filters out duplicates, this is important
+ // as _find_canvas_items_in_rect may produce duplicates
editor_selection->add_node(E);
}
}
@@ -2706,7 +3285,7 @@ bool CanvasItemEditor::_gui_input_hover(const Ref &p_event) {
for (int i = 0; i < hovering_results_items.size(); i++) {
CanvasItem *ci = hovering_results_items[i].item;
- if (ci->_edit_use_rect()) {
+ if (_use_edit_rect(ci)) {
continue;
}
@@ -2768,6 +3347,8 @@ void CanvasItemEditor::_gui_input_viewport(const Ref &p_event) {
// print_line("Rotate");
} else if (_gui_input_move(p_event)) {
// print_line("Move");
+ } else if (_gui_input_gizmos(p_event)) {
+ // print_line("Gizmos");
} else if (_gui_input_anchors(p_event)) {
// print_line("Anchors");
} else if (_gui_input_ruler_tool(p_event)) {
@@ -2805,8 +3386,7 @@ void CanvasItemEditor::_commit_drag() {
switch (drag_type) {
// Confirm the pivot move.
case DRAG_PIVOT: {
- _commit_canvas_item_state(
- drag_selection,
+ _commit_drag_selection_state(
vformat(
TTR("Set CanvasItem \"%s\" Pivot Offset to (%d, %d)"),
drag_selection.front()->get()->get_name(),
@@ -2817,17 +3397,13 @@ void CanvasItemEditor::_commit_drag() {
// Confirm the node rotation.
case DRAG_ROTATE: {
if (drag_selection.size() != 1) {
- _commit_canvas_item_state(
- drag_selection,
- vformat(TTR("Rotate %d CanvasItems"), drag_selection.size()),
- true);
+ _commit_drag_selection_state(
+ vformat(TTR("Rotate %d CanvasItems"), drag_selection.size()));
} else {
- _commit_canvas_item_state(
- drag_selection,
+ _commit_drag_selection_state(
vformat(TTR("Rotate CanvasItem \"%s\" to %d degrees"),
drag_selection.front()->get()->get_name(),
- Math::rad_to_deg(drag_selection.front()->get()->_edit_get_rotation())),
- true);
+ Math::rad_to_deg(drag_selection.front()->get()->_edit_get_rotation())));
}
if (key_auto_insert_button->is_pressed()) {
@@ -2841,8 +3417,7 @@ void CanvasItemEditor::_commit_drag() {
case DRAG_ANCHOR_BOTTOM_RIGHT:
case DRAG_ANCHOR_BOTTOM_LEFT:
case DRAG_ANCHOR_ALL: {
- _commit_canvas_item_state(
- drag_selection,
+ _commit_drag_selection_state(
vformat(TTR("Move CanvasItem \"%s\" Anchor"), drag_selection.front()->get()->get_name()));
snap_target[0] = SNAP_TARGET_NONE;
snap_target[1] = SNAP_TARGET_NONE;
@@ -2861,24 +3436,20 @@ void CanvasItemEditor::_commit_drag() {
if (node2d) {
// Extends from Node2D.
// Node2D doesn't have an actual stored rect size, unlike Controls.
- _commit_canvas_item_state(
- drag_selection,
+ _commit_drag_selection_state(
vformat(
TTR("Scale Node2D \"%s\" to (%s, %s)"),
drag_selection.front()->get()->get_name(),
Math::snapped(drag_selection.front()->get()->_edit_get_scale().x, 0.01),
- Math::snapped(drag_selection.front()->get()->_edit_get_scale().y, 0.01)),
- true);
+ Math::snapped(drag_selection.front()->get()->_edit_get_scale().y, 0.01)));
} else {
// Extends from Control.
- _commit_canvas_item_state(
- drag_selection,
+ _commit_drag_selection_state(
vformat(
TTR("Resize Control \"%s\" to (%d, %d)"),
drag_selection.front()->get()->get_name(),
drag_selection.front()->get()->_edit_get_rect().size.x,
- drag_selection.front()->get()->_edit_get_rect().size.y),
- true);
+ drag_selection.front()->get()->_edit_get_rect().size.y));
}
if (key_auto_insert_button->is_pressed()) {
@@ -2894,18 +3465,14 @@ void CanvasItemEditor::_commit_drag() {
case DRAG_SCALE_X:
case DRAG_SCALE_Y: {
if (drag_selection.size() != 1) {
- _commit_canvas_item_state(
- drag_selection,
- vformat(TTR("Scale %d CanvasItems"), drag_selection.size()),
- true);
+ _commit_drag_selection_state(
+ vformat(TTR("Scale %d CanvasItems"), drag_selection.size()));
} else {
- _commit_canvas_item_state(
- drag_selection,
+ _commit_drag_selection_state(
vformat(TTR("Scale CanvasItem \"%s\" to (%s, %s)"),
drag_selection.front()->get()->get_name(),
Math::snapped(drag_selection.front()->get()->_edit_get_scale().x, 0.01),
- Math::snapped(drag_selection.front()->get()->_edit_get_scale().y, 0.01)),
- true);
+ Math::snapped(drag_selection.front()->get()->_edit_get_scale().y, 0.01)));
}
if (key_auto_insert_button->is_pressed()) {
_insert_animation_keys(false, false, true, true);
@@ -2918,19 +3485,15 @@ void CanvasItemEditor::_commit_drag() {
case DRAG_MOVE_Y: {
if (transform.affine_inverse().xform(get_viewport()->get_mouse_position()) != drag_from) {
if (drag_selection.size() != 1) {
- _commit_canvas_item_state(
- drag_selection,
- vformat(TTR("Move %d CanvasItems"), drag_selection.size()),
- true);
+ _commit_drag_selection_state(
+ vformat(TTR("Move %d CanvasItems"), drag_selection.size()));
} else {
- _commit_canvas_item_state(
- drag_selection,
+ _commit_drag_selection_state(
vformat(
TTR("Move CanvasItem \"%s\" to (%d, %d)"),
drag_selection.front()->get()->get_name(),
drag_selection.front()->get()->_edit_get_position().x,
- drag_selection.front()->get()->_edit_get_position().y),
- true);
+ drag_selection.front()->get()->_edit_get_position().y));
}
}
@@ -2950,21 +3513,22 @@ void CanvasItemEditor::_commit_drag() {
}
if (drag_selection.size() > 1) {
- _commit_canvas_item_state(
- drag_selection,
- vformat(TTR("Move %d CanvasItems"), drag_selection.size()),
- true);
+ _commit_drag_selection_state(
+ vformat(TTR("Move %d CanvasItems"), drag_selection.size()));
} else if (drag_selection.size() == 1) {
- _commit_canvas_item_state(
- drag_selection,
+ _commit_drag_selection_state(
vformat(TTR("Move CanvasItem \"%s\" to (%d, %d)"),
drag_selection.front()->get()->get_name(),
drag_selection.front()->get()->_edit_get_position().x,
- drag_selection.front()->get()->_edit_get_position().y),
- true);
+ drag_selection.front()->get()->_edit_get_position().y));
}
} break;
+ case DRAG_GIZMO_HANDLE: {
+ // gizmo plugin is responsible for undo/redo
+ current_gizmo->_commit_handle(current_gizmo_handle, current_gizmo_handle_secondary, current_gizmo_initial_value, false);
+ } break;
+
default:
break;
}
@@ -3771,6 +4335,8 @@ void CanvasItemEditor::_draw_selection() {
CanvasItem *ci = Object::cast_to(E);
CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data(ci);
+ bool use_bounding_rect = _use_edit_rect(ci);
+
// Draw the previous position if we are dragging the node
if (show_helpers &&
(drag_type == DRAG_MOVE || drag_type == DRAG_ROTATE ||
@@ -3779,7 +4345,7 @@ void CanvasItemEditor::_draw_selection() {
const Transform2D pre_drag_xform = transform * se->pre_drag_xform;
const Color pre_drag_color = Color(0.4, 0.6, 1, 0.7);
- if (ci->_edit_use_rect()) {
+ if (use_bounding_rect) {
Vector2 pre_drag_endpoints[4] = {
pre_drag_xform.xform(se->pre_drag_rect.position),
pre_drag_xform.xform(se->pre_drag_rect.position + Vector2(se->pre_drag_rect.size.x, 0)),
@@ -3799,8 +4365,8 @@ void CanvasItemEditor::_draw_selection() {
Transform2D xform = transform * ci->get_screen_transform();
// Draw the selected items position / surrounding boxes
- if (ci->_edit_use_rect()) {
- Rect2 rect = ci->_edit_get_rect();
+ if (use_bounding_rect) {
+ Rect2 rect = _get_edit_rect(ci);
const Vector2 endpoints[4] = {
xform.xform(rect.position),
xform.xform(rect.position + Vector2(rect.size.x, 0)),
@@ -3828,7 +4394,7 @@ void CanvasItemEditor::_draw_selection() {
if (single && !item_locked && transform_tool) {
// Draw the pivot
- if (ci->_edit_use_pivot()) {
+ if (_use_edit_pivot(ci)) {
// Draw the node's pivot
Transform2D unscaled_transform = (xform * ci->get_transform().affine_inverse() * ci->_edit_get_transform()).orthonormalized();
Transform2D simple_xform;
@@ -3852,8 +4418,8 @@ void CanvasItemEditor::_draw_selection() {
}
// Draw the resize handles
- if (tool == TOOL_SELECT && ci->_edit_use_rect() && _is_node_movable(ci)) {
- Rect2 rect = ci->_edit_get_rect();
+ if (tool == TOOL_SELECT && use_bounding_rect && _is_node_movable(ci)) {
+ Rect2 rect = _get_edit_rect(ci);
const Vector2 endpoints[4] = {
xform.xform(rect.position),
xform.xform(rect.position + Vector2(rect.size.x, 0)),
@@ -3890,13 +4456,21 @@ void CanvasItemEditor::_draw_selection() {
if (!selection.is_empty() && transform_tool && show_transformation_gizmos) {
CanvasItem *ci = selection.front()->get();
+ CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data(ci);
+ Transform2D subgizmo_xform;
+ // If subgizmos are selected, we use the subgizmo's transform(s) for positioning the scale/rotate/move handles
+ // similar to the dedicated transform gizmo in 3D.
+ if (se && se->gizmo.is_valid()) {
+ subgizmo_xform = Transform2D().translated(selected_subgizmos_center);
+ }
+
Transform2D xform = transform * ci->get_screen_transform();
bool is_ctrl = Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL);
bool is_alt = Input::get_singleton()->is_key_pressed(Key::ALT);
// Draw the move handles.
if ((tool == TOOL_SELECT && is_alt && !is_ctrl) || tool == TOOL_MOVE) {
- Transform2D unscaled_transform = (xform * ci->get_transform().affine_inverse() * ci->_edit_get_transform()).orthonormalized();
+ Transform2D unscaled_transform = (xform * ci->get_transform().affine_inverse() * ci->_edit_get_transform() * subgizmo_xform).orthonormalized();
Transform2D simple_xform;
if (use_local_space) {
simple_xform = viewport->get_transform() * unscaled_transform;
@@ -3936,7 +4510,7 @@ void CanvasItemEditor::_draw_selection() {
} else {
edit_transform = ci->_edit_get_transform();
}
- Transform2D unscaled_transform = (xform * ci->get_transform().affine_inverse() * edit_transform).orthonormalized();
+ Transform2D unscaled_transform = (xform * ci->get_transform().affine_inverse() * edit_transform * subgizmo_xform).orthonormalized();
Transform2D simple_xform;
if (use_local_space) {
simple_xform = viewport->get_transform() * unscaled_transform;
@@ -4103,7 +4677,7 @@ void CanvasItemEditor::_draw_invisible_nodes_positions(Node *p_node, const Trans
_draw_invisible_nodes_positions(p_node->get_child(i), parent_xform, canvas_xform);
}
- if (show_position_gizmos && ci && !ci->_edit_use_rect() && (!editor_selection->is_selected(ci) || _is_node_locked(ci))) {
+ if (show_position_gizmos && ci && !_use_edit_rect(ci) && (!editor_selection->is_selected(ci) || _is_node_locked(ci))) {
Transform2D xform = transform * canvas_xform * parent_xform;
// Draw the node's position
@@ -4196,6 +4770,12 @@ void CanvasItemEditor::_draw_message() {
}
} break;
+ case DRAG_GIZMO_HANDLE: {
+ StringName gizmo_name = current_gizmo->_get_handle_name(current_gizmo_handle, current_gizmo_handle_secondary);
+ Variant value = current_gizmo->_get_handle_value(current_gizmo_handle, current_gizmo_handle_secondary);
+ message = TTR("Changing:") + " " + gizmo_name + " to " + value.stringify();
+ } break;
+
default:
break;
}
@@ -4286,6 +4866,8 @@ void CanvasItemEditor::_draw_viewport() {
EditorNode::get_singleton()->get_editor_plugins_over()->forward_canvas_draw_over_viewport(viewport);
EditorNode::get_singleton()->get_editor_plugins_force_over()->forward_canvas_force_draw_over_viewport(viewport);
+ update_all_gizmos();
+
if (show_rulers) {
_draw_rulers();
}
@@ -4396,8 +4978,8 @@ void CanvasItemEditor::_notification(int p_what) {
CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data(ci);
Rect2 rect;
- if (ci->_edit_use_rect()) {
- rect = ci->_edit_get_rect();
+ if (_use_edit_rect(ci)) {
+ rect = _get_edit_rect(ci);
} else {
rect = Rect2();
}
@@ -4533,9 +5115,111 @@ void CanvasItemEditor::_selection_changed() {
}
had_visible_selection = has_visible;
}
+
+ if (selected_canvas_item && editor_selection->get_top_selected_node_list().size() != 1) {
+ Vector][> gizmos = selected_canvas_item->get_gizmos();
+ for (Ref gizmo : gizmos) {
+ if (gizmo.is_null()) {
+ continue;
+ }
+ gizmo->_set_selected(false);
+ }
+
+ CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data(selected_canvas_item);
+ if (se) {
+ se->gizmo.unref();
+ se->subgizmos.clear();
+ }
+ selected_canvas_item->update_gizmos();
+ selected_canvas_item = nullptr;
+ }
+
+ // Ensure gizmo updates are performed when the selection changes
+ // outside the 2D view (similar to what is done in 3D for GH-106713).
+ if (!is_visible()) {
+ const List &top_selected = editor_selection->get_top_selected_node_list();
+ if (top_selected.size() == 1) {
+ CanvasItem *new_selected = Object::cast_to(top_selected.back()->get());
+ if (new_selected != selected_canvas_item) {
+ gizmos_dirty = true;
+ }
+ }
+ }
+
+ update_transform_gizmo();
+}
+
+void CanvasItemEditor::refresh_dirty_gizmos() {
+ if (!gizmos_dirty) {
+ return;
+ }
+
+ const List &top_selected = editor_selection->get_top_selected_node_list();
+ if (top_selected.size() == 1) {
+ CanvasItem *new_selected = Object::cast_to(top_selected.back()->get());
+ if (new_selected != selected_canvas_item) {
+ edit(new_selected);
+ }
+ }
+ gizmos_dirty = false;
+}
+
+void CanvasItemEditor::update_transform_gizmo() {
+ if (!selected_canvas_item) {
+ selected_subgizmos_center = Vector2(Math::INF, Math::INF);
+ return;
+ }
+
+ CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data(selected_canvas_item);
+ if (se && se->gizmo.is_valid()) {
+ Vector2 offset;
+ for (const KeyValue &kv : se->subgizmos) {
+ offset += kv.value.orthonormalized().get_origin();
+ }
+ offset /= static_cast(se->subgizmos.size());
+ selected_subgizmos_center = offset;
+ } else {
+ selected_subgizmos_center = Vector2(Math::INF, Math::INF);
+ }
}
void CanvasItemEditor::edit(CanvasItem *p_canvas_item) {
+ if (p_canvas_item != selected_canvas_item) {
+ if (selected_canvas_item) {
+ Vector][> gizmos = selected_canvas_item->get_gizmos();
+ for (Ref editor_gizmo : gizmos) {
+ if (editor_gizmo.is_null()) {
+ continue;
+ }
+ editor_gizmo->_set_selected(false);
+ }
+
+ CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data(selected_canvas_item);
+ if (se) {
+ se->gizmo.unref();
+ se->subgizmos.clear();
+ }
+
+ selected_canvas_item->update_gizmos();
+ }
+
+ selected_canvas_item = p_canvas_item;
+ current_hover_gizmo = Ref();
+ current_hover_gizmo_handle = -1;
+ current_hover_gizmo_handle_secondary = false;
+
+ if (selected_canvas_item) {
+ Vector][> gizmos = selected_canvas_item->get_gizmos();
+ for (Ref editor_gizmo : gizmos) {
+ if (editor_gizmo.is_null()) {
+ continue;
+ }
+ editor_gizmo->_set_selected(true);
+ }
+ selected_canvas_item->update_gizmos();
+ }
+ }
+
if (!p_canvas_item) {
return;
}
@@ -4698,11 +5382,25 @@ void CanvasItemEditor::_button_tool_select(int p_index) {
// Special action that places temporary rotation pivot in the middle of the selection.
List selection = _get_edited_canvas_items();
if (!selection.is_empty()) {
+ // if we have subgizmos selected, put the temp pivot in the middle of the subgizmos
Vector2 center;
- for (const CanvasItem *ci : selection) {
- center += ci->get_viewport()->get_popup_base_transform().xform(ci->_edit_get_position());
+ bool use_subgizmo_center = false;
+ if (selected_canvas_item) {
+ CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data]