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(selected_canvas_item); + if (se && se->gizmo.is_valid()) { + use_subgizmo_center = true; + for (const KeyValue &subgizmo : se->subgizmos) { + center += (selected_canvas_item->get_transform() * subgizmo.value).get_origin(); + } + temp_pivot = selected_canvas_item->get_viewport()->get_popup_base_transform().xform(center / se->subgizmos.size()); + } + } + if (!use_subgizmo_center) { + for (const CanvasItem *ci : selection) { + center += ci->get_viewport()->get_popup_base_transform().xform(ci->_edit_get_position()); + } + temp_pivot = center / selection.size(); } - temp_pivot = center / selection.size(); } } @@ -5186,6 +5884,22 @@ void CanvasItemEditor::_popup_callback(int p_op) { EditorSettings::get_singleton()->set_project_metadata("2d_editor", "auto_resampling_enabled", auto_resampling_enabled); } break; } + + int gizmo_plugin_index = p_op - SHOW_USER_DEFINED_GIZMO; + if (gizmo_plugin_index >= 0 && gizmo_plugin_index < gizmo_plugins_by_name.size()) { + Ref gizmo_plugin = gizmo_plugins_by_name[gizmo_plugin_index]; + bool gizmos_visible = gizmo_plugin->is_gizmos_visible(); + int menu_item_index = gizmos_menu->get_item_index(p_op); + if (gizmos_visible) { + gizmos_menu->set_item_checked(menu_item_index, false); + gizmo_plugin->set_gizmos_visible(false); + viewport->queue_redraw(); + } else { + gizmos_menu->set_item_checked(menu_item_index, true); + gizmo_plugin->set_gizmos_visible(true); + viewport->queue_redraw(); + } + } } void CanvasItemEditor::_set_owner_for_node_and_children(Node *p_node, Node *p_owner) { @@ -5210,8 +5924,8 @@ void CanvasItemEditor::_focus_selection(int p_op) { continue; } Rect2 item_rect; - if (ci->_edit_use_rect()) { - item_rect = ci->_edit_get_rect(); + if (_use_edit_rect(ci)) { + item_rect = _get_edit_rect(ci); } else { item_rect = Rect2(); } @@ -5256,8 +5970,14 @@ void CanvasItemEditor::_reset_drag() { void CanvasItemEditor::_bind_methods() { ClassDB::bind_method("_get_editor_data", &CanvasItemEditor::_get_editor_data); + ClassDB::bind_method("_request_gizmo", &CanvasItemEditor::_request_gizmo); + ClassDB::bind_method("_request_gizmo_for_id", &CanvasItemEditor::_request_gizmo_for_id); + ClassDB::bind_method("_set_subgizmo_selection", &CanvasItemEditor::_set_subgizmo_selection); + ClassDB::bind_method("_clear_subgizmo_selection", &CanvasItemEditor::_clear_subgizmo_selection); + ClassDB::bind_method(D_METHOD("update_viewport"), &CanvasItemEditor::update_viewport); ClassDB::bind_method(D_METHOD("center_at", "position"), &CanvasItemEditor::center_at); + ClassDB::bind_method("update_all_gizmos", &CanvasItemEditor::update_all_gizmos); ClassDB::bind_method("_set_owner_for_node_and_children", &CanvasItemEditor::_set_owner_for_node_and_children); @@ -5298,15 +6018,31 @@ Dictionary CanvasItemEditor::get_state() const { state["show_lock_gizmos"] = show_lock_gizmos; state["show_group_gizmos"] = show_group_gizmos; state["show_transformation_gizmos"] = show_transformation_gizmos; + { + Dictionary gizmo_state; + for (const Ref &plugin : gizmo_plugins_by_name) { + if (!plugin->can_be_hidden()) { + continue; + } + // this will not work correctly if get_gizmo_name() is not overridden or not unique, + // but we have no other identifier available + gizmo_state[plugin->get_gizmo_name()] = plugin->is_gizmos_visible(); + } + + state["show_user_defined_gizmos"] = gizmo_state; + } state["snap_rotation"] = snap_rotation; state["snap_scale"] = snap_scale; state["snap_relative"] = snap_relative; state["snap_pixel"] = snap_pixel; + return state; } void CanvasItemEditor::set_state(const Dictionary &p_state) { bool update_scrollbars = false; + bool update_gizmos = false; + Dictionary state = p_state; if (state.has("zoom")) { // Compensate the editor scale, so that the editor scale can be changed @@ -5438,26 +6174,34 @@ void CanvasItemEditor::set_state(const Dictionary &p_state) { if (state.has("show_position_gizmos")) { show_position_gizmos = state["show_position_gizmos"]; - int idx = gizmos_menu->get_item_index(SHOW_POSITION_GIZMOS); - gizmos_menu->set_item_checked(idx, show_position_gizmos); + update_gizmos = true; } if (state.has("show_lock_gizmos")) { show_lock_gizmos = state["show_lock_gizmos"]; - int idx = gizmos_menu->get_item_index(SHOW_LOCK_GIZMOS); - gizmos_menu->set_item_checked(idx, show_lock_gizmos); + update_gizmos = true; } if (state.has("show_group_gizmos")) { show_group_gizmos = state["show_group_gizmos"]; - int idx = gizmos_menu->get_item_index(SHOW_GROUP_GIZMOS); - gizmos_menu->set_item_checked(idx, show_group_gizmos); + update_gizmos = true; } if (state.has("show_transformation_gizmos")) { show_transformation_gizmos = state["show_transformation_gizmos"]; - int idx = gizmos_menu->get_item_index(SHOW_TRANSFORMATION_GIZMOS); - gizmos_menu->set_item_checked(idx, show_transformation_gizmos); + update_gizmos = true; + } + + if (state.has("show_user_defined_gizmos")) { + Dictionary gizmo_state = state["show_user_defined_gizmos"]; + for (String plugin_gizmo_name : gizmo_state.keys()) { + for (const Ref &plugin : gizmo_plugins_by_name) { + if (plugin->get_gizmo_name() == plugin_gizmo_name) { + plugin->set_gizmos_visible(gizmo_state[plugin_gizmo_name]); + } + } + } + update_gizmos = true; } if (state.has("show_zoom_control")) { @@ -5492,6 +6236,11 @@ void CanvasItemEditor::set_state(const Dictionary &p_state) { if (update_scrollbars) { _update_scrollbars(); } + + if (update_gizmos) { + _update_gizmos_menu(); + } + viewport->queue_redraw(); } @@ -5601,6 +6350,176 @@ void CanvasItemEditor::center_at(const Point2 &p_pos) { update_viewport(); } +struct _GizmoPluginPriorityComparator { + bool operator()(const Ref &a, const Ref &b) const { + if (a->get_priority() == b->get_priority()) { + return a->get_name() < b->get_name(); + } + return a->get_priority() > b->get_priority(); + } +}; + +struct _GizmoPluginNameComparator { + bool operator()(const Ref &a, const Ref &b) const { + return a->get_name() < b->get_name(); + } +}; + +void CanvasItemEditor::add_gizmo_plugin(Ref p_plugin) { + ERR_FAIL_COND(p_plugin.is_null()); + + gizmo_plugins_by_priority.push_back(p_plugin); + gizmo_plugins_by_priority.sort_custom<_GizmoPluginPriorityComparator>(); + + gizmo_plugins_by_name.push_back(p_plugin); + gizmo_plugins_by_name.sort_custom<_GizmoPluginNameComparator>(); + + _update_gizmos_menu(); +} + +void CanvasItemEditor::remove_gizmo_plugin(Ref p_plugin) { + gizmo_plugins_by_priority.erase(p_plugin); + gizmo_plugins_by_name.erase(p_plugin); + + _update_gizmos_menu(); +} + +void CanvasItemEditor::_request_gizmo(Object *p_obj) { + CanvasItem *ci = Object::cast_to(p_obj); + if (!ci) { + return; + } + + bool is_selected = (ci == selected_canvas_item); + Node *edited_scene = EditorNode::get_singleton()->get_edited_scene(); + if (edited_scene && (ci == edited_scene || (ci->get_owner() && edited_scene->is_ancestor_of(ci)))) { + for (int i = 0; i < gizmo_plugins_by_priority.size(); i++) { + Ref gizmo = gizmo_plugins_by_priority.write[i]->get_gizmo(ci); + + if (gizmo.is_valid()) { + ci->add_gizmo(gizmo); + + if (is_selected != gizmo->_is_selected()) { + gizmo->_set_selected(is_selected); + } + } + } + if (!ci->get_gizmos().is_empty()) { + ci->update_gizmos(); + } + } +} + +void CanvasItemEditor::_request_gizmo_for_id(ObjectID p_id) { + CanvasItem *ci = ObjectDB::get_instance(p_id); + if (ci) { + _request_gizmo(ci); + } +} + +void CanvasItemEditor::_set_subgizmo_selection(Object *p_obj, Ref p_gizmo, int p_id, Transform2D p_transform) { + if (p_id == -1) { + _clear_subgizmo_selection(p_obj); + return; + } + + CanvasItem *ci = nullptr; + if (p_obj) { + ci = Object::cast_to(p_obj); + } else { + ci = selected_canvas_item; + } + + if (!ci) { + return; + } + + CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data(ci); + if (se) { + se->subgizmos.clear(); + se->subgizmos.insert(p_id, p_transform); + se->gizmo = p_gizmo; + ci->update_gizmos(); + update_transform_gizmo(); + } +} + +void CanvasItemEditor::_clear_subgizmo_selection(Object *p_obj) { + CanvasItem *ci = nullptr; + if (p_obj) { + ci = Object::cast_to(p_obj); + } else { + ci = selected_canvas_item; + } + + if (!ci) { + return; + } + + CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data(ci); + if (se) { + se->subgizmos.clear(); + se->gizmo.unref(); + ci->update_gizmos(); + update_transform_gizmo(); + } +} + +void _update_all_canvas_item_gizmos(Node *p_node) { + ERR_FAIL_NULL(p_node); + CanvasItem *canvas_item = Object::cast_to(p_node); + if (canvas_item) { + canvas_item->update_gizmos(); + } + + for (int i = p_node->get_child_count() - 1; i >= 0; --i) { + _update_all_canvas_item_gizmos(p_node->get_child(i)); + } +} + +void CanvasItemEditor::update_all_gizmos(Node *p_node) { + if (!p_node && is_inside_tree()) { + p_node = get_tree()->get_edited_scene_root(); + } + + if (!p_node) { + // No edited scene, so nothing to update. + return; + } + _update_all_canvas_item_gizmos(p_node); +} + +bool CanvasItemEditor::is_subgizmo_selected(int p_id) { + CanvasItemEditorSelectedItem *se = selected_canvas_item ? editor_selection->get_node_editor_data(selected_canvas_item) : nullptr; + if (se) { + return se->subgizmos.has(p_id); + } + return false; +} + +bool CanvasItemEditor::is_current_selected_gizmo(const EditorCanvasItemGizmo *p_gizmo) { + CanvasItemEditorSelectedItem *se = selected_canvas_item ? editor_selection->get_node_editor_data(selected_canvas_item) : nullptr; + if (se) { + return se->gizmo == p_gizmo; + } + return false; +} + +Vector CanvasItemEditor::get_subgizmo_selection() { + CanvasItemEditorSelectedItem *se = selected_canvas_item ? editor_selection->get_node_editor_data(selected_canvas_item) : nullptr; + Vector result; + if (se) { + for (const KeyValue &entry : se->subgizmos) { + result.push_back(entry.key); + } + } + return result; +} + +void CanvasItemEditor::clear_subgizmo_selection(Object *p_obj) { + _clear_subgizmo_selection(p_obj); +} + CanvasItemEditor::CanvasItemEditor() { snap_target[0] = SNAP_TARGET_NONE; snap_target[1] = SNAP_TARGET_NONE; @@ -5972,12 +6891,8 @@ CanvasItemEditor::CanvasItemEditor() { gizmos_menu->set_name("GizmosMenu"); gizmos_menu->connect(SceneStringName(id_pressed), callable_mp(this, &CanvasItemEditor::_popup_callback)); gizmos_menu->set_hide_on_checkable_item_selection(false); - gizmos_menu->add_check_shortcut(ED_SHORTCUT("canvas_item_editor/show_position_gizmos", TTRC("Position")), SHOW_POSITION_GIZMOS); - gizmos_menu->add_check_shortcut(ED_SHORTCUT("canvas_item_editor/show_lock_gizmos", TTRC("Lock")), SHOW_LOCK_GIZMOS); - gizmos_menu->add_check_shortcut(ED_SHORTCUT("canvas_item_editor/show_group_gizmos", TTRC("Group")), SHOW_GROUP_GIZMOS); - gizmos_menu->add_check_shortcut(ED_SHORTCUT("canvas_item_editor/show_transformation_gizmos", TTRC("Transformation")), SHOW_TRANSFORMATION_GIZMOS); - p->add_child(gizmos_menu); - p->add_submenu_item(TTRC("Gizmos"), "GizmosMenu"); + p->add_submenu_node_item(TTRC("Gizmos"), gizmos_menu); + _update_gizmos_menu(); p->add_separator(); p->add_shortcut(ED_SHORTCUT("canvas_item_editor/center_selection", TTRC("Center Selection"), Key::F), VIEW_CENTER_TO_SELECTION); @@ -6115,6 +7030,8 @@ CanvasItemEditor::CanvasItemEditor() { singleton = this; set_process_shortcut_input(true); + add_to_group(SceneStringName(_canvas_item_editor_group)); + clear(); // Make sure values are initialized. // Update the menus' checkboxes. diff --git a/editor/scene/canvas_item_editor_plugin.h b/editor/scene/canvas_item_editor_plugin.h index 29ea7e5fc184..b1f99993cd67 100644 --- a/editor/scene/canvas_item_editor_plugin.h +++ b/editor/scene/canvas_item_editor_plugin.h @@ -31,6 +31,7 @@ #pragma once #include "editor/plugins/editor_plugin.h" +#include "editor/scene/2d/canvas_item_editor_gizmos.h" #include "scene/gui/box_container.h" class AcceptDialog; @@ -66,8 +67,10 @@ class CanvasItemEditorSelectedItem : public Object { Transform2D pre_drag_xform; Rect2 pre_drag_rect; - List pre_drag_bones_length; - List pre_drag_bones_undo_state; + // the gizmo that is currently supplying subgizmos + Ref gizmo; + HashMap subgizmos; // Key: Subgizmo ID, Value: Initial subgizmo transform. + Vector2 pre_drag_pivot; Dictionary undo_state; }; @@ -150,6 +153,8 @@ class CanvasItemEditor : public VBoxContainer { SKELETON_MAKE_BONES, SKELETON_SHOW_BONES, AUTO_RESAMPLE_CANVAS_ITEMS, + // offset for the first user-defined gizmo plugin + SHOW_USER_DEFINED_GIZMO = 10000, }; enum DragType { @@ -181,7 +186,8 @@ class CanvasItemEditor : public VBoxContainer { DRAG_V_GUIDE, DRAG_H_GUIDE, DRAG_DOUBLE_GUIDE, - DRAG_KEY_MOVE + DRAG_KEY_MOVE, + DRAG_GIZMO_HANDLE, }; enum GridVisibility { @@ -272,6 +278,7 @@ class CanvasItemEditor : public VBoxContainer { bool key_scale = false; bool pan_pressed = false; + // temporary pivot, in canvas coordinates Vector2 temp_pivot = Vector2(Math::INF, Math::INF); bool ruler_tool_active = false; @@ -391,6 +398,11 @@ class CanvasItemEditor : public VBoxContainer { bool is_hovering_h_guide = false; bool is_hovering_v_guide = false; + Ref current_gizmo = nullptr; + int current_gizmo_handle = -1; + bool current_gizmo_handle_secondary = false; + Variant current_gizmo_initial_value; + bool updating_value_dialog = false; Transform2D original_transform; @@ -409,6 +421,9 @@ class CanvasItemEditor : public VBoxContainer { Ref reset_transform_rotation_shortcut; Ref reset_transform_scale_shortcut; + Vector> gizmo_plugins_by_priority; + Vector> gizmo_plugins_by_name; + Ref panner; void _pan_callback(Vector2 p_scroll_vec, Ref p_event); void _zoom_callback(float p_zoom_factor, Vector2 p_origin, Ref p_event); @@ -417,20 +432,23 @@ class CanvasItemEditor : public VBoxContainer { bool _is_node_movable(const Node *p_node, bool p_popup_warning = false); void _get_canvas_items_at_pos(const Point2 &p_pos, Vector &r_items, bool p_allow_locked = false); void _find_canvas_items_in_rect(const Rect2 &p_rect, Node *p_node, List *r_items, const Transform2D &p_parent_xform = Transform2D(), const Transform2D &p_canvas_xform = Transform2D()); - - bool _select_click_on_item(CanvasItem *item, Point2 p_click_pos, bool p_append); + bool _select_subgizmos(Point2 p_click_pos, bool p_append); + bool _select_click_on_item(CanvasItem *item, bool p_append); + CanvasItemEditorSelectedItem *_get_selected_subgizmo() const; ConfirmationDialog *snap_dialog = nullptr; CanvasItem *ref_item = nullptr; - void _save_canvas_item_state(const List &p_canvas_items, bool save_bones = false); - void _restore_canvas_item_state(const List &p_canvas_items, bool restore_bones = false); - void _commit_canvas_item_state(const List &p_canvas_items, const String &action_name, bool commit_bones = false); + void _save_drag_selection_state(); + void _restore_drag_selection_state(); + void _commit_drag_selection_state(const String &action_name); Vector2 _anchor_to_position(const Control *p_control, Vector2 anchor); Vector2 _position_to_anchor(const Control *p_control, Vector2 position); + void _update_gizmos_menu(); + void _prepare_view_menu(); void _popup_callback(int p_op); bool updating_scroll = false; @@ -496,6 +514,7 @@ class CanvasItemEditor : public VBoxContainer { void _draw_viewport(); + bool _gui_input_gizmos(const Ref &p_event); bool _gui_input_anchors(const Ref &p_event); bool _gui_input_move(const Ref &p_event); bool _gui_input_open_scene_on_double_click(const Ref &p_event); @@ -560,6 +579,13 @@ class CanvasItemEditor : public VBoxContainer { friend class CanvasItemEditorPlugin; + CanvasItem *selected_canvas_item = nullptr; + + void _request_gizmo(Object *p_obj); + void _request_gizmo_for_id(ObjectID p_id); + void _set_subgizmo_selection(Object *p_obj, Ref p_gizmo, int p_id, Transform2D p_transform = Transform2D()); + void _clear_subgizmo_selection(Object *p_obj = nullptr); + protected: void _notification(int p_what); @@ -630,6 +656,29 @@ class CanvasItemEditor : public VBoxContainer { EditorSelection *editor_selection = nullptr; + /* GIZMOS */ + + Ref current_hover_gizmo; + int current_hover_gizmo_handle; + bool current_hover_gizmo_handle_secondary; + bool gizmos_dirty = false; + Vector2 selected_subgizmos_center; + + bool is_current_selected_gizmo(const EditorCanvasItemGizmo *p_gizmo); + Ref get_current_hover_gizmo() { return current_hover_gizmo; } + int get_current_hover_gizmo_handle(bool &r_secondary) const { + r_secondary = current_hover_gizmo_handle_secondary; + return current_hover_gizmo_handle; + } + bool is_subgizmo_selected(int p_id); + Vector get_subgizmo_selection(); + void clear_subgizmo_selection(Object *p_obj = nullptr); + void refresh_dirty_gizmos(); + void update_transform_gizmo(); + void update_all_gizmos(Node *p_node = nullptr); + void add_gizmo_plugin(Ref p_plugin); + void remove_gizmo_plugin(Ref p_plugin); + CanvasItemEditor(); }; diff --git a/scene/main/canvas_item.cpp b/scene/main/canvas_item.cpp index 849772e244c6..5a7fc9e82175 100644 --- a/scene/main/canvas_item.cpp +++ b/scene/main/canvas_item.cpp @@ -49,6 +49,9 @@ STATIC_ASSERT_INCOMPLETE_TYPE(class, RenderingServer); #include "servers/display/accessibility_server.h" #include "servers/rendering/rendering_server.h" +CanvasItemGizmo::CanvasItemGizmo() { +} + #define ERR_DRAW_GUARD \ ERR_FAIL_COND_MSG(!drawing, "Drawing is only allowed inside this node's `_draw()`, functions connected to its `draw` signal, or when it receives NOTIFICATION_DRAW.") @@ -82,6 +85,135 @@ void CanvasItem::_propagate_visibility_changed(bool p_parent_visible_in_tree) { _handle_visibility_change(p_parent_visible_in_tree); } +void CanvasItem::add_gizmo(Ref p_gizmo) { + ERR_THREAD_GUARD; +#ifdef TOOLS_ENABLED + if (data.gizmos_disabled || p_gizmo.is_null()) { + return; + } + data.gizmos.push_back(p_gizmo); + + if (is_inside_tree()) { + p_gizmo->create(); + if (is_visible_in_tree()) { + p_gizmo->redraw(); + } + p_gizmo->transform(); + } +#endif +} + +void CanvasItem::remove_gizmo(Ref p_gizmo) { + ERR_THREAD_GUARD; +#ifdef TOOLS_ENABLED + int idx = data.gizmos.find(p_gizmo); + if (idx != -1) { + data.gizmos.remove_at(idx); + } +#endif +} + +void CanvasItem::clear_gizmos() { + ERR_THREAD_GUARD; +#ifdef TOOLS_ENABLED + data.gizmos.clear(); + data.gizmos_requested = false; +#endif +} + +void CanvasItem::_update_gizmos() { +#ifdef TOOLS_ENABLED + if (data.gizmos_disabled || !is_inside_tree() || !data.gizmos_dirty) { + data.gizmos_dirty = false; + return; + } + data.gizmos_dirty = false; + for (Ref &gizmo : data.gizmos) { + if (is_visible_in_tree()) { + gizmo->redraw(); + } else { + gizmo->clear(); + } + } +#endif +} + +void CanvasItem::update_gizmos() { + ERR_THREAD_GUARD; +#ifdef TOOLS_ENABLED + if (!is_inside_tree()) { + return; + } + + if (data.gizmos.is_empty()) { + if (!data.gizmos_requested) { + data.gizmos_requested = true; + // done this way to avoid having editor references in CanvasItem + get_tree()->call_group_flags(SceneTree::GROUP_CALL_DEFERRED, SceneStringName(_canvas_item_editor_group), SNAME("_request_gizmo_for_id"), get_instance_id()); + } + return; + } + + if (data.gizmos_dirty) { + return; + } + + data.gizmos_dirty = true; + callable_mp(this, &CanvasItem::_update_gizmos).call_deferred(); +#endif +} + +TypedArray CanvasItem::get_gizmos_bind() const { + ERR_THREAD_GUARD_V(TypedArray()); + TypedArray ret; +#ifdef TOOLS_ENABLED + for (const Ref &gizmo : data.gizmos) { + ret.push_back(Variant(gizmo.ptr())); + } +#endif + return ret; +} + +Vector> CanvasItem::get_gizmos() const { + ERR_THREAD_GUARD_V(Vector>()); +#ifdef TOOLS_ENABLED + return data.gizmos; +#else + return Vector>(); +#endif +} + +void CanvasItem::set_subgizmo_selection(Ref p_gizmo, int p_id, Transform2D p_transform) { + ERR_THREAD_GUARD; +#ifdef TOOLS_ENABLED + if (!is_inside_tree()) { + return; + } + + if (is_part_of_edited_scene()) { + // done this way to avoid having editor references in CanvasItem + get_tree()->call_group_flags(SceneTree::GROUP_CALL_DEFERRED, SceneStringName(_canvas_item_editor_group), SNAME("_set_subgizmo_selection"), this, p_gizmo, p_id, p_transform); + } +#endif +} + +void CanvasItem::clear_subgizmo_selection() { + ERR_THREAD_GUARD; +#ifdef TOOLS_ENABLED + if (!is_inside_tree()) { + return; + } + + if (data.gizmos.is_empty()) { + return; + } + + if (is_part_of_edited_scene()) { + // done this way to avoid having editor references in CanvasItem + get_tree()->call_group_flags(SceneTree::GROUP_CALL_DEFERRED, SceneStringName(_canvas_item_editor_group), SNAME("_clear_subgizmo_selection"), this); + } +#endif +} void CanvasItem::set_visible(bool p_visible) { ERR_MAIN_THREAD_GUARD; if (visible == p_visible) { @@ -102,6 +234,13 @@ void CanvasItem::_handle_visibility_change(bool p_visible) { RenderingServer::get_singleton()->canvas_item_set_visible(canvas_item, p_visible); notification(NOTIFICATION_VISIBILITY_CHANGED); +#ifdef TOOLS_ENABLED + if (!data.gizmos.is_empty()) { + data.gizmos_dirty = true; + _update_gizmos(); + } +#endif + if (p_visible) { queue_redraw(); } else { @@ -392,6 +531,12 @@ void CanvasItem::_notification(int p_what) { notification(NOTIFICATION_RESET_PHYSICS_INTERPOLATION); } +#ifdef TOOLS_ENABLED + if (is_part_of_edited_scene() && !data.gizmos_requested) { + data.gizmos_requested = true; + get_tree()->call_group_flags(SceneTree::GROUP_CALL_DEFERRED, SceneStringName(_canvas_item_editor_group), SNAME("_request_gizmo_for_id"), get_instance_id()); + } +#endif } break; case NOTIFICATION_EXIT_TREE: { ERR_MAIN_THREAD_GUARD; @@ -1362,6 +1507,13 @@ void CanvasItem::_bind_methods() { ClassDB::bind_method(D_METHOD("_edit_get_transform"), &CanvasItem::_edit_get_transform); #endif //TOOLS_ENABLED + ClassDB::bind_method(D_METHOD("update_gizmos"), &CanvasItem::update_gizmos); + ClassDB::bind_method(D_METHOD("add_gizmo", "gizmo"), &CanvasItem::add_gizmo); + ClassDB::bind_method(D_METHOD("get_gizmos"), &CanvasItem::get_gizmos_bind); + ClassDB::bind_method(D_METHOD("clear_gizmos"), &CanvasItem::clear_gizmos); + ClassDB::bind_method(D_METHOD("set_subgizmo_selection", "gizmo", "id", "transform"), &CanvasItem::set_subgizmo_selection); + ClassDB::bind_method(D_METHOD("clear_subgizmo_selection"), &CanvasItem::clear_subgizmo_selection); + ClassDB::bind_method(D_METHOD("get_canvas_item"), &CanvasItem::get_canvas_item); ClassDB::bind_method(D_METHOD("set_visible", "visible"), &CanvasItem::set_visible); @@ -1799,6 +1951,12 @@ CanvasItem::CanvasItem() : _define_ancestry(AncestralClass::CANVAS_ITEM); canvas_item = RenderingServer::get_singleton()->canvas_item_create(); + +#if TOOLS_ENABLED + data.gizmos_requested = false; + data.gizmos_disabled = false; + data.gizmos_dirty = false; +#endif } CanvasItem::~CanvasItem() { diff --git a/scene/main/canvas_item.h b/scene/main/canvas_item.h index 9dd741a0d972..0a72498c6b5d 100644 --- a/scene/main/canvas_item.h +++ b/scene/main/canvas_item.h @@ -44,6 +44,20 @@ class StyleBox; class Window; class World2D; +class CanvasItemGizmo : public RefCounted { + GDCLASS(CanvasItemGizmo, RefCounted); + +public: + virtual void create() = 0; + virtual void transform() = 0; + virtual void clear() = 0; + virtual void redraw() = 0; + virtual void free() = 0; + + CanvasItemGizmo(); + virtual ~CanvasItemGizmo() {} +}; + class CanvasItem : public Node { GDCLASS(CanvasItem, Node); @@ -95,6 +109,13 @@ class CanvasItem : public Node { // an optimization for faster traversal. LocalVector canvas_item_children; uint32_t index_in_parent = UINT32_MAX; +#ifdef TOOLS_ENABLED + Vector> gizmos; + bool gizmos_requested : 1; + bool gizmos_disabled : 1; + bool gizmos_dirty : 1; + bool transform_dirty : 1; +#endif } data; int light_mask = 1; @@ -164,6 +185,8 @@ class CanvasItem : public Node { void _notify_transform_deferred(); const StringName *_instance_shader_parameter_get_remap(const StringName &p_name) const; + void _update_gizmos(); + protected: bool _set(const StringName &p_name, const Variant &p_value); bool _get(const StringName &p_name, Variant &r_ret) const; @@ -265,6 +288,16 @@ class CanvasItem : public Node { void update_draw_order(); + /* GIZMOS */ + Vector> get_gizmos() const; + TypedArray get_gizmos_bind() const; + void add_gizmo(Ref p_gizmo); + void remove_gizmo(Ref p_gizmo); + void clear_gizmos(); + void update_gizmos(); + void set_subgizmo_selection(Ref p_gizmo, int p_id, Transform2D p_transform = Transform2D()); + void clear_subgizmo_selection(); + /* VISIBILITY */ void set_visible(bool p_visible); diff --git a/scene/scene_string_names.h b/scene/scene_string_names.h index b4827345a620..05624283ce75 100644 --- a/scene/scene_string_names.h +++ b/scene/scene_string_names.h @@ -106,6 +106,7 @@ class SceneStringNames { const StringName screen_entered = "screen_entered"; const StringName screen_exited = "screen_exited"; + const StringName _canvas_item_editor_group = "_canvas_item_editor_group"; const StringName _spatial_editor_group = "_spatial_editor_group"; const StringName _request_gizmo = "_request_gizmo";