From b3248418d538684cfedc6b21756b542fe13f0cd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Thom=C3=A4?= Date: Fri, 14 Nov 2025 16:47:43 +0100 Subject: [PATCH 01/26] Add infrastructure for CanvasItem gizmos. --- editor/register_editor_types.cpp | 3 + editor/scene/2d/canvas_item_editor_gizmos.cpp | 810 ++++++++++++++++++ editor/scene/2d/canvas_item_editor_gizmos.h | 207 +++++ editor/scene/canvas_item_editor_plugin.h | 15 + scene/main/canvas_item.cpp | 4 + scene/main/canvas_item.h | 18 + 6 files changed, 1057 insertions(+) create mode 100644 editor/scene/2d/canvas_item_editor_gizmos.cpp create mode 100644 editor/scene/2d/canvas_item_editor_gizmos.h 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..c18e342299f2 --- /dev/null +++ b/editor/scene/2d/canvas_item_editor_gizmos.cpp @@ -0,0 +1,810 @@ +/**************************************************************************/ +/* 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 "editor/scene/canvas_item_editor_plugin.h" +#include "modules/gdscript/gdscript_tokenizer.h" +#include "scene/resources/mesh.h" + +#define HANDLE_HALF_SIZE 9.5 + +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 (int i = 0; i < instances.size(); i++) { + if (instances[i].instance.is_valid()) { + RS::get_singleton()->free_rid(instances[i].instance); + instances.write[i].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(); +} + +void EditorCanvasItemGizmo::redraw() { + if (!GDVIRTUAL_CALL(_redraw)) { + ERR_FAIL_NULL(gizmo_plugin); + gizmo_plugin->redraw(this); + } + + if (CanvasItemEditor::get_singleton()->is_current_selected_gizmo(this)) { + CanvasItemEditor::get_singleton()->update_transform_gizmo(); + } +} + +String EditorCanvasItemGizmo::get_handle_name(int p_id, bool p_secondary) const { + String ret; + if (GDVIRTUAL_CALL(_get_handle_name, p_id, p_secondary, ret)) { + return ret; + } + + ERR_FAIL_NULL_V(gizmo_plugin, ""); + return gizmo_plugin->get_handle_name(this, p_id, p_secondary); +} + +bool EditorCanvasItemGizmo::is_handle_highlighted(int p_id, bool p_secondary) const { + bool success; + if (GDVIRTUAL_CALL(_is_handle_highlighted, p_id, p_secondary, success)) { + return success; + } + + ERR_FAIL_NULL_V(gizmo_plugin, false); + return gizmo_plugin->is_handle_highlighted(this, p_id, p_secondary); +} + +Variant EditorCanvasItemGizmo::get_handle_value(int p_id, bool p_secondary) const { + Variant value; + if (GDVIRTUAL_CALL(_get_handle_value, p_id, p_secondary, value)) { + return value; + } + + ERR_FAIL_NULL_V(gizmo_plugin, Variant()); + return gizmo_plugin->get_handle_value(this, p_id, p_secondary); +} + +void EditorCanvasItemGizmo::begin_handle_action(int p_id, bool p_secondary) { + if (GDVIRTUAL_CALL(_begin_handle_action, p_id, p_secondary)) { + return; + } + + ERR_FAIL_NULL(gizmo_plugin); + gizmo_plugin->begin_handle_action(this, p_id, p_secondary); +} + +void EditorCanvasItemGizmo::set_handle(int p_id, bool p_secondary, const Point2 &p_point) { + if (GDVIRTUAL_CALL(_set_handle, p_id, p_secondary, p_point)) { + return; + } + + ERR_FAIL_NULL(gizmo_plugin); + 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) { + if (GDVIRTUAL_CALL(_commit_handle, p_id, p_secondary, p_restore, p_cancel)) { + return; + } + + ERR_FAIL_NULL(gizmo_plugin); + gizmo_plugin->commit_handle(this, p_id, p_secondary, p_restore, p_cancel); +} + +int EditorCanvasItemGizmo::subgizmos_intersect_point(const Point2 &p_point) const { + int id; + if (GDVIRTUAL_CALL(_subgizmos_intersect_point, p_point, id)) { + return id; + } + + ERR_FAIL_NULL_V(gizmo_plugin, -1); + return gizmo_plugin->subgizmos_intersect_point(this, p_point); +} + +Vector EditorCanvasItemGizmo::subgizmos_intersect_rect(const Rect2 &p_rect) const { + Vector ret; + if (GDVIRTUAL_CALL(_subgizmos_intersect_rect, p_rect, ret)) { + return ret; + } + + ERR_FAIL_NULL_V(gizmo_plugin, Vector()); + return gizmo_plugin->subgizmos_intersect_rect(this, p_rect); +} + +Transform2D EditorCanvasItemGizmo::get_subgizmo_transform(int p_id) const { + Transform2D ret; + if (GDVIRTUAL_CALL(_get_subgizmo_transform, p_id, ret)) { + return ret; + } + + ERR_FAIL_NULL_V(gizmo_plugin, Transform2D()); + return gizmo_plugin->get_subgizmo_transform(this, p_id); +} + +void EditorCanvasItemGizmo::set_subgizmo_transform(int p_id, const Transform2D &p_transform) { + if (GDVIRTUAL_CALL(_set_subgizmo_transform, p_id, p_transform)) { + return; + } + + ERR_FAIL_NULL(gizmo_plugin); + 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) { + 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; + } + + ERR_FAIL_NULL(gizmo_plugin); + 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_hidden) { + 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()); + int layer = p_hidden ? 0 : 1 << CanvasItemEditorViewport::GIZMO_EDIT_LAYER; + RS::get_singleton()->canvas_item_set_visibility_layer(instance, layer); +} + +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, hidden); + 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, hidden); + 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, hidden); + 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, hidden); + 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, const Color &p_color, 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); + + Vector vertices; + vertices.resize(p_handles.size()); + for (int i = 0; i < p_handles.size(); i++) { + vertices.write[i] = Vector3(p_handles[i].x, p_handles[i].y, 0); + } + + Instance ins; + Ref mesh = memnew(ArrayMesh); + + Array a; + a.resize(RS::ARRAY_MAX); + a[RS::ARRAY_VERTEX] = vertices; + Vector colors; + { + colors.resize(p_handles.size()); + Color *w = colors.ptrw(); + for (int i = 0; i < p_handles.size(); i++) { + int id = p_ids.is_empty() ? i : p_ids[i]; + + Color col(1, 1, 1, 1); + if (is_handle_highlighted(id, p_secondary)) { + col = Color(0, 0, 1, 0.9); + } + + if (!is_current_hover_gizmo || current_hover_handle != id || p_secondary != current_hover_handle_secondary) { + col.a = 0.8; + } + + w[i] = col; + } + } + a[RS::ARRAY_COLOR] = colors; + mesh->add_surface_from_arrays(Mesh::PRIMITIVE_POINTS, a); + if (valid) { + RS::get_singleton()->canvas_item_add_mesh(ins.instance, mesh->get_rid(), Transform2D(), p_color); + ins.create_instance(canvas_item, hidden); + } + + instances.push_back(ins); + + int current_size = handle_list.size(); + handle_list.resize(current_size + p_handles.size()); + for (int i = 0; i < p_handles.size(); 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 (hidden && !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 (int i = 0; i < collision_segments.size(); i++) { + Vector2 global_position = transform.xform(collision_segments[i]); + if (p_rect.has_point(global_position)) { + return true; + } + } + + // same for collision polygons + for (int i = 0; i < collision_polygons.size(); i++) { + for (int j = 0; j < collision_polygons[i].size(); j++) { + Vector2 global_position = transform.xform(collision_polygons[i][j]); + if (p_rect.has_point(global_position)) { + return true; + } + } + } + + // for rectangles we check if they overlap + Transform2D inverse_transform = transform.affine_inverse(); + for (int i = 0; i < collision_rects.size(); i++) { + if (collision_rects[i].intersects_transformed(inverse_transform, p_rect)) { + return true; + } + } + + return false; +} + +void EditorCanvasItemGizmo::handles_intersect_point(const Point2 &p_point, 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 (hidden) { + return; + } + + Transform2D screen_transform = canvas_item->get_global_transform_with_canvas() * canvas_item->get_viewport()->get_screen_transform(); + float min_d = 1e20; + + for (int i = 0; i < secondary_handles.size(); i++ ) { + Vector2 screen_pos = screen_transform.xform(secondary_handles[i]); + float distance = screen_pos.distance_to(p_point); + if ( distance < HANDLE_HALF_SIZE && 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++ ) { + Vector2 screen_pos = screen_transform.xform(handles[i]); + float distance = screen_pos.distance_to(p_point); + if ( distance < HANDLE_HALF_SIZE && 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 { + ERR_FAIL_NULL_V(canvas_item, false); + ERR_FAIL_COND_V(!valid, false); + + if (hidden && !gizmo_plugin->is_selectable_when_hidden()) { + return false; + } + + Transform2D to_local = canvas_item->get_global_transform().affine_inverse(); + Point2 local_point = to_local.xform(p_point); + + + 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(local_point, a, b); + if (closest.distance_to(local_point) < 8) { // TODO: 3d uses a magic 8 here, not sure why + return true; + } + } + + for (int i = 0; i < collision_rects.size(); i++) { + if (collision_rects[i].has_point(local_point)) { + return true; + } + } + + for (int i = 0; i < collision_polygons.size(); i++) { + if (Geometry2D::is_point_in_polygon(local_point, collision_polygons[i])) { + 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 (int i = 0; i < instances.size(); i++) { + instances.write[i].create_instance(canvas_item, hidden); + } + + transform(); +} + + +void EditorCanvasItemGizmo::transform() { + ERR_FAIL_NULL(canvas_item); + ERR_FAIL_COND(!valid); + for (int i = 0; i < instances.size(); i++) { + RS::get_singleton()->canvas_item_set_transform(instances[i].instance, canvas_item->get_global_transform()); + } +} + +void EditorCanvasItemGizmo::free() { + ERR_FAIL_NULL(RS::get_singleton()); + ERR_FAIL_NULL(canvas_item); + ERR_FAIL_COND(!valid); + + // TODO: i'm not sure why the 3D variant doesn't just call clear + // and repeats the freeing loop. + clear(); + valid = false; +} + +void EditorCanvasItemGizmo::set_hidden(bool p_hidden) { + hidden = p_hidden; + int layer = p_hidden ? 0 : 1 << CanvasItemEditorViewport::GIZMO_EDIT_LAYER; + for (int i = 0; i < instances.size(); i++) { + RS::get_singleton()->canvas_item_set_visibility_layer(instances[i].instance, layer); + } +} + +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_polyline, 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", "color", "ids", "secondary"), &EditorCanvasItemGizmo::add_handles, DEFVAL(false)); + ClassDB::bind_method(D_METHOD("set_canvas_item", "canvas_item"), &EditorCanvasItemGizmo::_set_canvas_item); + ClassDB::bind_method(D_METHOD("set_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_hidden", "hidden"), &EditorCanvasItemGizmo::set_hidden); + 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); + + GDVIRTUAL_BIND(_redraw); + GDVIRTUAL_BIND(_get_handle_name, "id", "secondary"); + GDVIRTUAL_BIND(_is_handle_highlighted, "id", "secondary"); + + 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"); + 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; + hidden = false; + selected = false; + canvas_item = nullptr; + gizmo_plugin = nullptr; +} + + 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; +} + +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_hidden(current_state == HIDDEN); + + current_gizmos.insert(ref.ptr()); + return ref; +} + +void EditorCanvasItemGizmoPlugin::_bind_methods() { + 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(_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", "screen_pos"); + GDVIRTUAL_BIND(_commit_handle, "gizmo", "handle_id", "secondary", "restore", "cancel"); + + GDVIRTUAL_BIND(_subgizmos_intersect_point, "gizmo", "screen_pos"); + 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::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_screen_pos) { + GDVIRTUAL_CALL(_set_handle, Ref(p_gizmo), p_id, p_secondary, p_screen_pos); +} + +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) const { + int ret = -1; + GDVIRTUAL_CALL(_subgizmos_intersect_point, Ref(p_gizmo), p_point, 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[i] = p_transforms[i]; + } + + GDVIRTUAL_CALL(_commit_subgizmos, Ref(p_gizmo), p_ids, transforms, p_cancel); +} + +void EditorCanvasItemGizmoPlugin::set_state(int p_state) { + current_state = p_state; + for (EditorCanvasItemGizmo *gizmo : current_gizmos) { + gizmo->set_hidden(p_state == HIDDEN); + } +} + +int EditorCanvasItemGizmoPlugin::get_state() const { + return current_state; +} + +void EditorCanvasItemGizmoPlugin::unregister_gizmo(EditorCanvasItemGizmo *p_gizmo) { + current_gizmos.erase(p_gizmo); +} + +EditorCanvasItemGizmoPlugin::EditorCanvasItemGizmoPlugin() { + current_state = VISIBLE; +} + +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..87d462044b83 --- /dev/null +++ b/editor/scene/2d/canvas_item_editor_gizmos.h @@ -0,0 +1,207 @@ +/**************************************************************************/ +/* 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 "core/object/ref_counted.h" +#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_hidden = false); + }; + + bool selected; + + Vector collision_segments; + Vector collision_rects; + Vector> collision_polygons; + + Vector handles; + Vector handle_ids; + Vector secondary_handles; + Vector secondary_handle_ids; + + bool valid; + bool hidden; + 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) + 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) + + GDVIRTUAL1RC(int, _subgizmos_intersect_point, Vector2) + 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, const Color &p_color, const Vector &p_ids = Vector(), bool p_secondary = false); + + 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) 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); + 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, bool p_shift_pressed, int &r_id, bool &r_secondary); + bool intersect_point(const Point2 &p_point) 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_hidden(bool p_hidden); + void set_plugin(EditorCanvasItemGizmoPlugin *p_plugin); + + EditorCanvasItemGizmo(); + ~EditorCanvasItemGizmo(); +}; + + +class EditorCanvasItemGizmoPlugin : public Resource { + GDCLASS(EditorCanvasItemGizmoPlugin, Resource); + +public: + static const int VISIBLE = 0; + static const int HIDDEN = 1; + static const int ON_TOP = 2; + +protected: + int current_state; + 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) + 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) + + GDVIRTUAL2RC(int, _subgizmos_intersect_point, Ref, Vector2) + 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: + 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 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); + + virtual int subgizmos_intersect_point(const EditorCanvasItemGizmo *p_gizmo, const Vector2 &p_point) 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_state(int p_state); + int get_state() const; + void unregister_gizmo(EditorCanvasItemGizmo *p_gizmo); + + EditorCanvasItemGizmoPlugin(); + virtual ~EditorCanvasItemGizmoPlugin(); +}; diff --git a/editor/scene/canvas_item_editor_plugin.h b/editor/scene/canvas_item_editor_plugin.h index 29ea7e5fc184..ba21a8edc7aa 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; @@ -630,6 +631,17 @@ class CanvasItemEditor : public VBoxContainer { EditorSelection *editor_selection = nullptr; + /* GIZMOS */ + + // TODO: actually implement this. + bool is_current_selected_gizmo(const EditorCanvasItemGizmo *p_gizmo) {return false;}; + Ref get_current_hover_gizmo() {return Ref();}; + int get_current_hover_gizmo_handle(bool &r_secondary) const { r_secondary = false; return -1; } + bool is_subgizmo_selected(int p_id) {return false;}; + Vector get_subgizmo_selection() {return Vector();}; + void update_transform_gizmo() {}; + void update_all_gizmos() {}; + CanvasItemEditor(); }; @@ -698,6 +710,9 @@ class CanvasItemEditorViewport : public Control { void _notification(int p_what); public: + static constexpr int32_t GIZMO_EDIT_LAYER = 26; + + virtual bool can_drop_data(const Point2 &p_point, const Variant &p_data) const override; virtual void drop_data(const Point2 &p_point, const Variant &p_data) override; diff --git a/scene/main/canvas_item.cpp b/scene/main/canvas_item.cpp index 849772e244c6..125527af21cf 100644 --- a/scene/main/canvas_item.cpp +++ b/scene/main/canvas_item.cpp @@ -49,6 +49,10 @@ 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.") diff --git a/scene/main/canvas_item.h b/scene/main/canvas_item.h index 9dd741a0d972..f463e1ae28e5 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); @@ -265,6 +279,10 @@ class CanvasItem : public Node { void update_draw_order(); + /* GIZMOS */ + // TODO: actually implement this. + void remove_gizmo(Ref p_gizmo) {} + /* VISIBILITY */ void set_visible(bool p_visible); From ff36c2e835916427c29e73e1eb3cf2a39132f014 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Thom=C3=A4?= Date: Thu, 20 Nov 2025 07:42:15 +0100 Subject: [PATCH 02/26] Implement plugin registration and node picking by mouse click. --- editor/plugins/editor_plugin.cpp | 12 ++ editor/plugins/editor_plugin.h | 4 + editor/scene/2d/canvas_item_editor_gizmos.cpp | 6 +- editor/scene/2d/canvas_item_editor_gizmos.h | 2 +- editor/scene/canvas_item_editor_plugin.cpp | 124 ++++++++++++++++- editor/scene/canvas_item_editor_plugin.h | 20 ++- scene/main/canvas_item.cpp | 127 ++++++++++++++++++ scene/main/canvas_item.h | 17 ++- scene/scene_string_names.h | 1 + 9 files changed, 304 insertions(+), 9 deletions(-) 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/scene/2d/canvas_item_editor_gizmos.cpp b/editor/scene/2d/canvas_item_editor_gizmos.cpp index c18e342299f2..309bfb3786a1 100644 --- a/editor/scene/2d/canvas_item_editor_gizmos.cpp +++ b/editor/scene/2d/canvas_item_editor_gizmos.cpp @@ -462,7 +462,7 @@ bool EditorCanvasItemGizmo::intersect_point(const Point2 &p_point) const { Vector2 a = collision_segments[i]; Vector2 b = collision_segments[i+1]; Vector2 closest = Geometry2D::get_closest_point_to_segment(local_point, a, b); - if (closest.distance_to(local_point) < 8) { // TODO: 3d uses a magic 8 here, not sure why + if (closest.distance_to(local_point) < 8) { // TODO: GIZMOS 3d uses a magic 8 here, not sure why return true; } } @@ -547,7 +547,7 @@ void EditorCanvasItemGizmo::set_plugin(EditorCanvasItemGizmoPlugin *p_plugin) { } void EditorCanvasItemGizmo::_bind_methods() { - ClassDB::bind_method(D_METHOD("add_circle", "position", "radius", "color"), &EditorCanvasItemGizmo::add_polyline, DEFVAL(Color(1, 1, 1))); + 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))); @@ -556,7 +556,7 @@ void EditorCanvasItemGizmo::_bind_methods() { ClassDB::bind_method(D_METHOD("add_collision_polygon", "polygon"), &EditorCanvasItemGizmo::add_collision_polygon); ClassDB::bind_method(D_METHOD("add_handles", "handles", "color", "ids", "secondary"), &EditorCanvasItemGizmo::add_handles, DEFVAL(false)); ClassDB::bind_method(D_METHOD("set_canvas_item", "canvas_item"), &EditorCanvasItemGizmo::_set_canvas_item); - ClassDB::bind_method(D_METHOD("set_canvas_item"), &EditorCanvasItemGizmo::get_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_hidden", "hidden"), &EditorCanvasItemGizmo::set_hidden); diff --git a/editor/scene/2d/canvas_item_editor_gizmos.h b/editor/scene/2d/canvas_item_editor_gizmos.h index 87d462044b83..53fe17cac763 100644 --- a/editor/scene/2d/canvas_item_editor_gizmos.h +++ b/editor/scene/2d/canvas_item_editor_gizmos.h @@ -107,7 +107,7 @@ class EditorCanvasItemGizmo : public CanvasItemGizmo { 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); + void set_selected(bool p_selected) { selected = p_selected;} bool is_selected() const { return selected; }; void set_canvas_item(CanvasItem *p_canvas_item); diff --git a/editor/scene/canvas_item_editor_plugin.cpp b/editor/scene/canvas_item_editor_plugin.cpp index 241d8ba54d6a..323f7b91d307 100644 --- a/editor/scene/canvas_item_editor_plugin.cpp +++ b/editor/scene/canvas_item_editor_plugin.cpp @@ -676,8 +676,11 @@ 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)) { + + // legacy way + if (ci->_edit_is_selected_on_click(point, local_grab_distance)) { Node2D *node = Object::cast_to(ci); SelectResult res; @@ -686,6 +689,26 @@ void CanvasItemEditor::find_canvas_items_at_pos(const Point2 &p_pos, Node *p_nod res.has_z = node; r_items.push_back(res); } + + // 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 (int i = 0; i < gizmos.size(); i++) { + Ref gizmo = gizmos[i]; + if (gizmo.is_null()) { + continue; + } + if (gizmo->intersect_point(point)) { + 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); + } + } } } @@ -5256,8 +5279,12 @@ 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(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); @@ -5601,6 +5628,99 @@ 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 _update_all_canvas_item_gizmos(Node *p_node) { + for (int i = p_node->get_child_count() - 1; 0 <= i; --i) { + CanvasItem *canvas_item = Object::cast_to(p_node->get_child(i)); + if (canvas_item) { + canvas_item->update_gizmos(); + } + + _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); +} + + + CanvasItemEditor::CanvasItemEditor() { snap_target[0] = SNAP_TARGET_NONE; snap_target[1] = SNAP_TARGET_NONE; @@ -6115,6 +6235,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 ba21a8edc7aa..683b7880d730 100644 --- a/editor/scene/canvas_item_editor_plugin.h +++ b/editor/scene/canvas_item_editor_plugin.h @@ -410,6 +410,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); @@ -432,6 +435,9 @@ class CanvasItemEditor : public VBoxContainer { Vector2 _anchor_to_position(const Control *p_control, Vector2 anchor); Vector2 _position_to_anchor(const Control *p_control, Vector2 position); + + void _update_gizmos_menu() {} // TODO: GIZMOS, implement. + void _prepare_view_menu(); void _popup_callback(int p_op); bool updating_scroll = false; @@ -561,6 +567,12 @@ 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); + + protected: void _notification(int p_what); @@ -633,14 +645,18 @@ class CanvasItemEditor : public VBoxContainer { /* GIZMOS */ - // TODO: actually implement this. + // TODO: GIZMOS actually implement this. bool is_current_selected_gizmo(const EditorCanvasItemGizmo *p_gizmo) {return false;}; Ref get_current_hover_gizmo() {return Ref();}; int get_current_hover_gizmo_handle(bool &r_secondary) const { r_secondary = false; return -1; } bool is_subgizmo_selected(int p_id) {return false;}; Vector get_subgizmo_selection() {return Vector();}; void update_transform_gizmo() {}; - void update_all_gizmos() {}; + 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 125527af21cf..e9127c6e2b78 100644 --- a/scene/main/canvas_item.cpp +++ b/scene/main/canvas_item.cpp @@ -86,6 +86,114 @@ 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 (p_gizmo.is_valid() && 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) { + p_gizmo->free(); + data.gizmos.remove_at(idx); + } +#endif +} + +void CanvasItem::clear_gizmos() { + ERR_THREAD_GUARD; +#ifdef TOOLS_ENABLED + for (int i = 0; i < data.gizmos.size(); i++) { + data.gizmos.write[i]->free(); + } + 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 (int i = 0; i < data.gizmos.size(); i++) { + if (is_visible_in_tree()) { + data.gizmos.write[i]->redraw(); + } else { + data.gizmos.write[i]->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 (int i = 0; i < data.gizmos.size(); i++) { + ret.push_back(Variant(data.gizmos[i].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_visible(bool p_visible) { ERR_MAIN_THREAD_GUARD; if (visible == p_visible) { @@ -106,6 +214,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 { @@ -396,6 +511,13 @@ 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; @@ -1366,6 +1488,11 @@ void CanvasItem::_bind_methods() { ClassDB::bind_method(D_METHOD("_edit_get_transform"), &CanvasItem::_edit_get_transform); #endif //TOOLS_ENABLED + 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("get_canvas_item"), &CanvasItem::get_canvas_item); ClassDB::bind_method(D_METHOD("set_visible", "visible"), &CanvasItem::set_visible); diff --git a/scene/main/canvas_item.h b/scene/main/canvas_item.h index f463e1ae28e5..319dbe429784 100644 --- a/scene/main/canvas_item.h +++ b/scene/main/canvas_item.h @@ -109,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; @@ -178,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; @@ -280,8 +289,12 @@ class CanvasItem : public Node { void update_draw_order(); /* GIZMOS */ - // TODO: actually implement this. - void remove_gizmo(Ref p_gizmo) {} + 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(); /* VISIBILITY */ 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"; From 112b0a30b06832a4957aa8cb9634b884cdd32c4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Thom=C3=A4?= Date: Fri, 28 Nov 2025 08:38:02 +0100 Subject: [PATCH 03/26] Implement rectangle selection for canvas item gizmos. --- editor/scene/2d/canvas_item_editor_gizmos.cpp | 55 ++++--------------- editor/scene/2d/canvas_item_editor_gizmos.h | 11 ++-- editor/scene/canvas_item_editor_plugin.cpp | 28 ++++++++-- editor/scene/canvas_item_editor_plugin.h | 23 ++++---- scene/main/canvas_item.cpp | 7 --- 5 files changed, 52 insertions(+), 72 deletions(-) diff --git a/editor/scene/2d/canvas_item_editor_gizmos.cpp b/editor/scene/2d/canvas_item_editor_gizmos.cpp index 309bfb3786a1..373958d9690d 100644 --- a/editor/scene/2d/canvas_item_editor_gizmos.cpp +++ b/editor/scene/2d/canvas_item_editor_gizmos.cpp @@ -201,7 +201,6 @@ void EditorCanvasItemGizmo::set_canvas_item(CanvasItem *p_canvas_item) { canvas_item = p_canvas_item; } - void EditorCanvasItemGizmo::Instance::create_instance(CanvasItem *p_base, bool p_hidden) { ERR_FAIL_NULL(p_base); @@ -220,7 +219,6 @@ void EditorCanvasItemGizmo::add_circle(const Vector2 &p_pos, float p_radius, con instances.push_back(ins); } - void EditorCanvasItemGizmo::add_polygon(const Vector &p_polygon, const Color &p_color) { ERR_FAIL_NULL(canvas_item); @@ -251,7 +249,6 @@ void EditorCanvasItemGizmo::add_polyline(const Vector &p_points, const instances.push_back(ins); } - void EditorCanvasItemGizmo::add_rect(const Rect2 &p_rect, const Color &p_color) { ERR_FAIL_NULL(canvas_item); @@ -261,7 +258,6 @@ void EditorCanvasItemGizmo::add_rect(const Rect2 &p_rect, const Color &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(); @@ -271,7 +267,6 @@ void EditorCanvasItemGizmo::add_collision_segments(const Vector &p_line } } - void EditorCanvasItemGizmo::add_collision_rect(const Rect2 &p_rect) { collision_rects.push_back(p_rect); } @@ -334,7 +329,7 @@ void EditorCanvasItemGizmo::add_handles(const Vector &p_handles, const a[RS::ARRAY_COLOR] = colors; mesh->add_surface_from_arrays(Mesh::PRIMITIVE_POINTS, a); if (valid) { - RS::get_singleton()->canvas_item_add_mesh(ins.instance, mesh->get_rid(), Transform2D(), p_color); + RS::get_singleton()->canvas_item_add_mesh(ins.instance, mesh->get_rid(), Transform2D(), p_color); ins.create_instance(canvas_item, hidden); } @@ -355,7 +350,7 @@ void EditorCanvasItemGizmo::add_handles(const Vector &p_handles, const } } -bool EditorCanvasItemGizmo::intersect_rect(const Rect2 &p_rect) const{ +bool EditorCanvasItemGizmo::intersect_rect(const Rect2 &p_rect) const { ERR_FAIL_NULL_V(canvas_item, false); ERR_FAIL_COND_V(!valid, false); @@ -409,15 +404,14 @@ void EditorCanvasItemGizmo::handles_intersect_point(const Point2 &p_point, bool Transform2D screen_transform = canvas_item->get_global_transform_with_canvas() * canvas_item->get_viewport()->get_screen_transform(); float min_d = 1e20; - for (int i = 0; i < secondary_handles.size(); i++ ) { + for (int i = 0; i < secondary_handles.size(); i++) { Vector2 screen_pos = screen_transform.xform(secondary_handles[i]); float distance = screen_pos.distance_to(p_point); - if ( distance < HANDLE_HALF_SIZE && distance < min_d ) { + if (distance < HANDLE_HALF_SIZE && distance < min_d) { min_d = distance; if (secondary_handle_ids.is_empty()) { r_id = i; - } - else { + } else { r_id = secondary_handle_ids[i]; } r_secondary = true; @@ -430,15 +424,14 @@ void EditorCanvasItemGizmo::handles_intersect_point(const Point2 &p_point, bool min_d = 1e20; - for (int i = 0; i < handles.size(); i++ ) { + for (int i = 0; i < handles.size(); i++) { Vector2 screen_pos = screen_transform.xform(handles[i]); float distance = screen_pos.distance_to(p_point); - if ( distance < HANDLE_HALF_SIZE && distance < min_d ) { + if (distance < HANDLE_HALF_SIZE && distance < min_d) { min_d = distance; if (handle_ids.is_empty()) { r_id = i; - } - else { + } else { r_id = handle_ids[i]; } r_secondary = false; @@ -457,10 +450,9 @@ bool EditorCanvasItemGizmo::intersect_point(const Point2 &p_point) const { Transform2D to_local = canvas_item->get_global_transform().affine_inverse(); Point2 local_point = to_local.xform(p_point); - for (int i = 0; i < collision_segments.size(); i += 2) { Vector2 a = collision_segments[i]; - Vector2 b = collision_segments[i+1]; + Vector2 b = collision_segments[i + 1]; Vector2 closest = Geometry2D::get_closest_point_to_segment(local_point, a, b); if (closest.distance_to(local_point) < 8) { // TODO: GIZMOS 3d uses a magic 8 here, not sure why return true; @@ -482,7 +474,6 @@ bool EditorCanvasItemGizmo::intersect_point(const Point2 &p_point) const { return false; } - bool EditorCanvasItemGizmo::is_subgizmo_selected(int p_id) const { CanvasItemEditor *ed = CanvasItemEditor::get_singleton(); ERR_FAIL_NULL_V(ed, false); @@ -514,7 +505,6 @@ void EditorCanvasItemGizmo::create() { transform(); } - void EditorCanvasItemGizmo::transform() { ERR_FAIL_NULL(canvas_item); ERR_FAIL_COND(!valid); @@ -587,7 +577,7 @@ EditorCanvasItemGizmo::EditorCanvasItemGizmo() { gizmo_plugin = nullptr; } - EditorCanvasItemGizmo::~EditorCanvasItemGizmo() { +EditorCanvasItemGizmo::~EditorCanvasItemGizmo() { if (gizmo_plugin != nullptr) { gizmo_plugin->unregister_gizmo(this); } @@ -605,7 +595,6 @@ String EditorCanvasItemGizmoPlugin::get_gizmo_name() const { return "Unnamed Gizmo"; } - int EditorCanvasItemGizmoPlugin::get_priority() const { int ret = 0; if (GDVIRTUAL_CALL(_get_priority, ret)) { @@ -615,7 +604,7 @@ int EditorCanvasItemGizmoPlugin::get_priority() const { } Ref EditorCanvasItemGizmoPlugin::get_gizmo(CanvasItem *p_canvas_item) { - if (get_script_instance() && get_script_instance() -> has_method("_get_gizmo")) { + if (get_script_instance() && get_script_instance()->has_method("_get_gizmo")) { return get_script_instance()->call("_get_gizmo", p_canvas_item); } @@ -663,7 +652,6 @@ bool EditorCanvasItemGizmoPlugin::has_gizmo(CanvasItem *p_canvas_item) { return success; } - Ref EditorCanvasItemGizmoPlugin::create_gizmo(CanvasItem *p_canvas_item) { Ref ret; if (GDVIRTUAL_CALL(_create_gizmo, p_canvas_item, ret)) { @@ -724,7 +712,7 @@ void EditorCanvasItemGizmoPlugin::set_handle(const EditorCanvasItemGizmo *p_gizm GDVIRTUAL_CALL(_set_handle, Ref(p_gizmo), p_id, p_secondary, p_screen_pos); } -void EditorCanvasItemGizmoPlugin::commit_handle(const EditorCanvasItemGizmo *p_gizmo, int p_id, bool p_secondary, const Variant& p_restore, bool p_cancel) { +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); } @@ -789,22 +777,3 @@ EditorCanvasItemGizmoPlugin::~EditorCanvasItemGizmoPlugin() { 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 index 53fe17cac763..07b02a606649 100644 --- a/editor/scene/2d/canvas_item_editor_gizmos.h +++ b/editor/scene/2d/canvas_item_editor_gizmos.h @@ -61,7 +61,7 @@ class EditorCanvasItemGizmo : public CanvasItemGizmo { Vector instances; CanvasItem *canvas_item = nullptr; - void _set_canvas_item(CanvasItem *p_canvas_item) {set_canvas_item(Object::cast_to(p_canvas_item));} + void _set_canvas_item(CanvasItem *p_canvas_item) { set_canvas_item(Object::cast_to(p_canvas_item)); } protected: static void _bind_methods(); @@ -107,13 +107,13 @@ class EditorCanvasItemGizmo : public CanvasItemGizmo { 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_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;} + CanvasItem *get_canvas_item() const { return canvas_item; } - Ref get_plugin() const {return gizmo_plugin;} + Ref get_plugin() const { return gizmo_plugin; } bool intersect_rect(const Rect2 &p_rect) const; void handles_intersect_point(const Point2 &p_point, bool p_shift_pressed, int &r_id, bool &r_secondary); @@ -136,7 +136,6 @@ class EditorCanvasItemGizmo : public CanvasItemGizmo { ~EditorCanvasItemGizmo(); }; - class EditorCanvasItemGizmoPlugin : public Resource { GDCLASS(EditorCanvasItemGizmoPlugin, Resource); diff --git a/editor/scene/canvas_item_editor_plugin.cpp b/editor/scene/canvas_item_editor_plugin.cpp index 323f7b91d307..3440d3cc946c 100644 --- a/editor/scene/canvas_item_editor_plugin.cpp +++ b/editor/scene/canvas_item_editor_plugin.cpp @@ -806,6 +806,7 @@ void CanvasItemEditor::_find_canvas_items_in_rect(const Rect2 &p_rect, Node *p_n } xform *= ci->get_transform(); + // legacy way if (ci->_edit_use_rect()) { Rect2 rect = ci->_edit_get_rect(); if (p_rect.has_point(xform.xform(rect.position)) && @@ -819,6 +820,18 @@ 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 (int i = 0; i < gizmos.size(); i++) { + Ref gizmo = gizmos[i]; + if (gizmo.is_null()) { + continue; + } + if (gizmo->intersect_rect(p_rect)) { + r_items->push_back(ci); + } + } } } @@ -5662,7 +5675,6 @@ void CanvasItemEditor::remove_gizmo_plugin(Ref p_pl _update_gizmos_menu(); } - void CanvasItemEditor::_request_gizmo(Object *p_obj) { CanvasItem *ci = Object::cast_to(p_obj); if (!ci) { @@ -5671,12 +5683,12 @@ void CanvasItemEditor::_request_gizmo(Object *p_obj) { 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))) { + 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); + Ref gizmo = gizmo_plugins_by_priority.write[i]->get_gizmo(ci); if (gizmo.is_valid()) { - ci -> add_gizmo(gizmo); + ci->add_gizmo(gizmo); if (is_selected != gizmo->is_selected()) { gizmo->set_selected(is_selected); @@ -5719,7 +5731,13 @@ void CanvasItemEditor::update_all_gizmos(Node *p_node) { _update_all_canvas_item_gizmos(p_node); } - +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; +} CanvasItemEditor::CanvasItemEditor() { snap_target[0] = SNAP_TARGET_NONE; diff --git a/editor/scene/canvas_item_editor_plugin.h b/editor/scene/canvas_item_editor_plugin.h index 683b7880d730..92fa63ce62a8 100644 --- a/editor/scene/canvas_item_editor_plugin.h +++ b/editor/scene/canvas_item_editor_plugin.h @@ -67,6 +67,9 @@ class CanvasItemEditorSelectedItem : public Object { Transform2D pre_drag_xform; Rect2 pre_drag_rect; + Ref gizmo; + HashMap subgizmos; // Key: Subgizmo ID, Value: Initial subgizmo transform. + List pre_drag_bones_length; List pre_drag_bones_undo_state; @@ -435,7 +438,6 @@ class CanvasItemEditor : public VBoxContainer { Vector2 _anchor_to_position(const Control *p_control, Vector2 anchor); Vector2 _position_to_anchor(const Control *p_control, Vector2 position); - void _update_gizmos_menu() {} // TODO: GIZMOS, implement. void _prepare_view_menu(); @@ -572,7 +574,6 @@ class CanvasItemEditor : public VBoxContainer { void _request_gizmo(Object *p_obj); void _request_gizmo_for_id(ObjectID p_id); - protected: void _notification(int p_what); @@ -646,18 +647,19 @@ class CanvasItemEditor : public VBoxContainer { /* GIZMOS */ // TODO: GIZMOS actually implement this. - bool is_current_selected_gizmo(const EditorCanvasItemGizmo *p_gizmo) {return false;}; - Ref get_current_hover_gizmo() {return Ref();}; - int get_current_hover_gizmo_handle(bool &r_secondary) const { r_secondary = false; return -1; } - bool is_subgizmo_selected(int p_id) {return false;}; - Vector get_subgizmo_selection() {return Vector();}; - void update_transform_gizmo() {}; + bool is_current_selected_gizmo(const EditorCanvasItemGizmo *p_gizmo); + Ref get_current_hover_gizmo() { return Ref(); } + int get_current_hover_gizmo_handle(bool &r_secondary) const { + r_secondary = false; + return -1; + } + bool is_subgizmo_selected(int p_id) { return false; } + Vector get_subgizmo_selection() { return Vector(); } + 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(); }; @@ -728,7 +730,6 @@ class CanvasItemEditorViewport : public Control { public: static constexpr int32_t GIZMO_EDIT_LAYER = 26; - virtual bool can_drop_data(const Point2 &p_point, const Variant &p_data) const override; virtual void drop_data(const Point2 &p_point, const Variant &p_data) override; diff --git a/scene/main/canvas_item.cpp b/scene/main/canvas_item.cpp index e9127c6e2b78..a995646867f5 100644 --- a/scene/main/canvas_item.cpp +++ b/scene/main/canvas_item.cpp @@ -52,7 +52,6 @@ STATIC_ASSERT_INCOMPLETE_TYPE(class, RenderingServer); 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.") @@ -126,7 +125,6 @@ void CanvasItem::clear_gizmos() { #endif } - void CanvasItem::_update_gizmos() { #ifdef TOOLS_ENABLED if (data.gizmos_disabled || !is_inside_tree() || !data.gizmos_dirty) { @@ -144,7 +142,6 @@ void CanvasItem::_update_gizmos() { #endif } - void CanvasItem::update_gizmos() { ERR_THREAD_GUARD; #ifdef TOOLS_ENABLED @@ -171,8 +168,6 @@ void CanvasItem::update_gizmos() { #endif } - - TypedArray CanvasItem::get_gizmos_bind() const { ERR_THREAD_GUARD_V(TypedArray()); TypedArray ret; @@ -184,7 +179,6 @@ TypedArray CanvasItem::get_gizmos_bind() const { return ret; } - Vector> CanvasItem::get_gizmos() const { ERR_THREAD_GUARD_V(Vector>()); #ifdef TOOLS_ENABLED @@ -1492,7 +1486,6 @@ void CanvasItem::_bind_methods() { 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("get_canvas_item"), &CanvasItem::get_canvas_item); ClassDB::bind_method(D_METHOD("set_visible", "visible"), &CanvasItem::set_visible); From e1af0e18dc506c3420cf3ec2e503ca5b0c8a486b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Thom=C3=A4?= Date: Fri, 28 Nov 2025 19:53:13 +0100 Subject: [PATCH 04/26] Fix up first round of review comments. --- editor/scene/2d/canvas_item_editor_gizmos.cpp | 52 +++++++++---------- editor/scene/2d/canvas_item_editor_gizmos.h | 1 - editor/scene/canvas_item_editor_plugin.cpp | 8 ++- scene/main/canvas_item.cpp | 18 +++---- 4 files changed, 35 insertions(+), 44 deletions(-) diff --git a/editor/scene/2d/canvas_item_editor_gizmos.cpp b/editor/scene/2d/canvas_item_editor_gizmos.cpp index 373958d9690d..31100af02852 100644 --- a/editor/scene/2d/canvas_item_editor_gizmos.cpp +++ b/editor/scene/2d/canvas_item_editor_gizmos.cpp @@ -32,7 +32,6 @@ #include "core/math/geometry_2d.h" #include "editor/scene/canvas_item_editor_plugin.h" -#include "modules/gdscript/gdscript_tokenizer.h" #include "scene/resources/mesh.h" #define HANDLE_HALF_SIZE 9.5 @@ -57,10 +56,10 @@ bool EditorCanvasItemGizmo::is_editable() const { void EditorCanvasItemGizmo::clear() { ERR_FAIL_NULL(RenderingServer::get_singleton()); - for (int i = 0; i < instances.size(); i++) { - if (instances[i].instance.is_valid()) { - RS::get_singleton()->free_rid(instances[i].instance); - instances.write[i].instance = RID(); + for (Instance &instance : instances) { + if (instance.instance.is_valid()) { + RS::get_singleton()->free_rid(instance.instance); + instance.instance = RID(); } } @@ -206,7 +205,7 @@ void EditorCanvasItemGizmo::Instance::create_instance(CanvasItem *p_base, bool p instance = RS::get_singleton()->canvas_item_create(); RS::get_singleton()->canvas_item_set_parent(instance, p_base->get_canvas_item()); - int layer = p_hidden ? 0 : 1 << CanvasItemEditorViewport::GIZMO_EDIT_LAYER; + int layer = p_hidden ? 0 : (1 << CanvasItemEditorViewport::GIZMO_EDIT_LAYER); RS::get_singleton()->canvas_item_set_visibility_layer(instance, layer); } @@ -302,7 +301,8 @@ void EditorCanvasItemGizmo::add_handles(const Vector &p_handles, const } Instance ins; - Ref mesh = memnew(ArrayMesh); + Ref mesh; + mesh.instantiate(); Array a; a.resize(RS::ARRAY_MAX); @@ -362,17 +362,17 @@ bool EditorCanvasItemGizmo::intersect_rect(const Rect2 &p_rect) const { // for collision segments it is enough if at least one point // of a segment is inside the rectangle - for (int i = 0; i < collision_segments.size(); i++) { - Vector2 global_position = transform.xform(collision_segments[i]); + 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 (int i = 0; i < collision_polygons.size(); i++) { - for (int j = 0; j < collision_polygons[i].size(); j++) { - Vector2 global_position = transform.xform(collision_polygons[i][j]); + 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; } @@ -381,8 +381,8 @@ bool EditorCanvasItemGizmo::intersect_rect(const Rect2 &p_rect) const { // for rectangles we check if they overlap Transform2D inverse_transform = transform.affine_inverse(); - for (int i = 0; i < collision_rects.size(); i++) { - if (collision_rects[i].intersects_transformed(inverse_transform, p_rect)) { + for (const Rect2 &collision_rect : collision_rects) { + if (collision_rect.intersects_transformed(inverse_transform, p_rect)) { return true; } } @@ -459,14 +459,14 @@ bool EditorCanvasItemGizmo::intersect_point(const Point2 &p_point) const { } } - for (int i = 0; i < collision_rects.size(); i++) { - if (collision_rects[i].has_point(local_point)) { + for (const Rect2 &collision_rect : collision_rects) { + if (collision_rect.has_point(local_point)) { return true; } } - for (int i = 0; i < collision_polygons.size(); i++) { - if (Geometry2D::is_point_in_polygon(local_point, collision_polygons[i])) { + for (const Vector &collision_polygon : collision_polygons) { + if (Geometry2D::is_point_in_polygon(local_point, collision_polygon)) { return true; } } @@ -498,8 +498,8 @@ void EditorCanvasItemGizmo::create() { ERR_FAIL_COND(valid); valid = true; - for (int i = 0; i < instances.size(); i++) { - instances.write[i].create_instance(canvas_item, hidden); + for (Instance &instance : instances) { + instance.create_instance(canvas_item, hidden); } transform(); @@ -508,8 +508,8 @@ void EditorCanvasItemGizmo::create() { void EditorCanvasItemGizmo::transform() { ERR_FAIL_NULL(canvas_item); ERR_FAIL_COND(!valid); - for (int i = 0; i < instances.size(); i++) { - RS::get_singleton()->canvas_item_set_transform(instances[i].instance, canvas_item->get_global_transform()); + for (const Instance &instance : instances) { + RS::get_singleton()->canvas_item_set_transform(instance.instance, canvas_item->get_global_transform()); } } @@ -526,9 +526,9 @@ void EditorCanvasItemGizmo::free() { void EditorCanvasItemGizmo::set_hidden(bool p_hidden) { hidden = p_hidden; - int layer = p_hidden ? 0 : 1 << CanvasItemEditorViewport::GIZMO_EDIT_LAYER; - for (int i = 0; i < instances.size(); i++) { - RS::get_singleton()->canvas_item_set_visibility_layer(instances[i].instance, layer); + int layer = p_hidden ? 0 : (1 << CanvasItemEditorViewport::GIZMO_EDIT_LAYER); + for (const Instance &instance : instances) { + RS::get_singleton()->canvas_item_set_visibility_layer(instance.instance, layer); } } @@ -591,7 +591,7 @@ String EditorCanvasItemGizmoPlugin::get_gizmo_name() const { 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."); + 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"; } diff --git a/editor/scene/2d/canvas_item_editor_gizmos.h b/editor/scene/2d/canvas_item_editor_gizmos.h index 07b02a606649..fb97bcfd54c3 100644 --- a/editor/scene/2d/canvas_item_editor_gizmos.h +++ b/editor/scene/2d/canvas_item_editor_gizmos.h @@ -30,7 +30,6 @@ #pragma once -#include "core/object/ref_counted.h" #include "scene/main/canvas_item.h" class EditorCanvasItemGizmoPlugin; diff --git a/editor/scene/canvas_item_editor_plugin.cpp b/editor/scene/canvas_item_editor_plugin.cpp index 3440d3cc946c..8202e1f8943b 100644 --- a/editor/scene/canvas_item_editor_plugin.cpp +++ b/editor/scene/canvas_item_editor_plugin.cpp @@ -695,8 +695,7 @@ void CanvasItemEditor::find_canvas_items_at_pos(const Point2 &p_pos, Node *p_nod // function is filtering for duplicates anyways, so we're not spending // any extra effort here. Vector> gizmos = ci->get_gizmos(); - for (int i = 0; i < gizmos.size(); i++) { - Ref gizmo = gizmos[i]; + for (Ref gizmo : gizmos) { if (gizmo.is_null()) { continue; } @@ -823,8 +822,7 @@ void CanvasItemEditor::_find_canvas_items_in_rect(const Rect2 &p_rect, Node *p_n // gizmos way Vector> gizmos = ci->get_gizmos(); - for (int i = 0; i < gizmos.size(); i++) { - Ref gizmo = gizmos[i]; + for (Ref gizmo : gizmos) { if (gizmo.is_null()) { continue; } @@ -5709,7 +5707,7 @@ void CanvasItemEditor::_request_gizmo_for_id(ObjectID p_id) { } void _update_all_canvas_item_gizmos(Node *p_node) { - for (int i = p_node->get_child_count() - 1; 0 <= i; --i) { + for (int i = p_node->get_child_count() - 1; i >= 0; --i) { CanvasItem *canvas_item = Object::cast_to(p_node->get_child(i)); if (canvas_item) { canvas_item->update_gizmos(); diff --git a/scene/main/canvas_item.cpp b/scene/main/canvas_item.cpp index a995646867f5..0e6b5a2ecb3a 100644 --- a/scene/main/canvas_item.cpp +++ b/scene/main/canvas_item.cpp @@ -93,7 +93,7 @@ void CanvasItem::add_gizmo(Ref p_gizmo) { } data.gizmos.push_back(p_gizmo); - if (p_gizmo.is_valid() && is_inside_tree()) { + if (is_inside_tree()) { p_gizmo->create(); if (is_visible_in_tree()) { p_gizmo->redraw(); @@ -108,7 +108,6 @@ void CanvasItem::remove_gizmo(Ref p_gizmo) { #ifdef TOOLS_ENABLED int idx = data.gizmos.find(p_gizmo); if (idx != -1) { - p_gizmo->free(); data.gizmos.remove_at(idx); } #endif @@ -117,9 +116,6 @@ void CanvasItem::remove_gizmo(Ref p_gizmo) { void CanvasItem::clear_gizmos() { ERR_THREAD_GUARD; #ifdef TOOLS_ENABLED - for (int i = 0; i < data.gizmos.size(); i++) { - data.gizmos.write[i]->free(); - } data.gizmos.clear(); data.gizmos_requested = false; #endif @@ -132,11 +128,11 @@ void CanvasItem::_update_gizmos() { return; } data.gizmos_dirty = false; - for (int i = 0; i < data.gizmos.size(); i++) { + for (Ref &gizmo : data.gizmos) { if (is_visible_in_tree()) { - data.gizmos.write[i]->redraw(); + gizmo->redraw(); } else { - data.gizmos.write[i]->clear(); + gizmo->clear(); } } #endif @@ -164,7 +160,6 @@ void CanvasItem::update_gizmos() { data.gizmos_dirty = true; callable_mp(this, &CanvasItem::_update_gizmos).call_deferred(); - #endif } @@ -172,8 +167,8 @@ TypedArray CanvasItem::get_gizmos_bind() const { ERR_THREAD_GUARD_V(TypedArray()); TypedArray ret; #ifdef TOOLS_ENABLED - for (int i = 0; i < data.gizmos.size(); i++) { - ret.push_back(Variant(data.gizmos[i].ptr())); + for (const Ref &gizmo : data.gizmos) { + ret.push_back(Variant(gizmo.ptr())); } #endif return ret; @@ -511,7 +506,6 @@ void CanvasItem::_notification(int p_what) { 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; From 64e104a6a369c898d937ad175090f4324bcbd21a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Thom=C3=A4?= Date: Tue, 9 Dec 2025 07:15:23 +0100 Subject: [PATCH 05/26] Implement handle dragging. --- editor/scene/2d/canvas_item_editor_gizmos.cpp | 71 +++++------ editor/scene/2d/canvas_item_editor_gizmos.h | 3 +- editor/scene/canvas_item_editor_plugin.cpp | 116 +++++++++++++++++- editor/scene/canvas_item_editor_plugin.h | 19 ++- scene/main/canvas_item.cpp | 9 +- 5 files changed, 163 insertions(+), 55 deletions(-) diff --git a/editor/scene/2d/canvas_item_editor_gizmos.cpp b/editor/scene/2d/canvas_item_editor_gizmos.cpp index 31100af02852..a41f44c78047 100644 --- a/editor/scene/2d/canvas_item_editor_gizmos.cpp +++ b/editor/scene/2d/canvas_item_editor_gizmos.cpp @@ -31,6 +31,7 @@ #include "canvas_item_editor_gizmos.h" #include "core/math/geometry_2d.h" +#include "editor/editor_node.h" #include "editor/scene/canvas_item_editor_plugin.h" #include "scene/resources/mesh.h" @@ -60,6 +61,8 @@ void EditorCanvasItemGizmo::clear() { if (instance.instance.is_valid()) { RS::get_singleton()->free_rid(instance.instance); instance.instance = RID(); + RS::get_singleton()->free_rid(instance.handle_multimesh); + instance.handle_multimesh = RID(); } } @@ -274,7 +277,7 @@ void EditorCanvasItemGizmo::add_collision_polygon(const Vector &p_polyg collision_polygons.push_back(p_polygon); } -void EditorCanvasItemGizmo::add_handles(const Vector &p_handles, const Color &p_color, const Vector &p_ids, bool p_secondary) { +void EditorCanvasItemGizmo::add_handles(const Vector &p_handles, Ref p_texture, const Vector &p_ids, bool p_secondary) { if (!is_selected() || !is_editable()) { return; } @@ -290,54 +293,38 @@ void EditorCanvasItemGizmo::add_handles(const Vector &p_handles, const ERR_FAIL_COND_MSG(p_handles.size() != p_ids.size(), "The number of IDs should be the same as the number of handles."); } + // TODO: GIZMOS - implement hover support 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); - Vector vertices; - vertices.resize(p_handles.size()); - for (int i = 0; i < p_handles.size(); i++) { - vertices.write[i] = Vector3(p_handles[i].x, p_handles[i].y, 0); + Ref texture = p_texture; + if (p_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_NULL(texture); + Size2 texture_size = texture->get_size(); - Instance ins; - Ref mesh; - mesh.instantiate(); + Control *viewport = CanvasItemEditor::get_singleton()->get_viewport_control(); - Array a; - a.resize(RS::ARRAY_MAX); - a[RS::ARRAY_VERTEX] = vertices; - Vector colors; - { - colors.resize(p_handles.size()); - Color *w = colors.ptrw(); - for (int i = 0; i < p_handles.size(); i++) { - int id = p_ids.is_empty() ? i : p_ids[i]; - - Color col(1, 1, 1, 1); - if (is_handle_highlighted(id, p_secondary)) { - col = Color(0, 0, 1, 0.9); - } - - if (!is_current_hover_gizmo || current_hover_handle != id || p_secondary != current_hover_handle_secondary) { - col.a = 0.8; - } + // 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(); - w[i] = col; - } - } - a[RS::ARRAY_COLOR] = colors; - mesh->add_surface_from_arrays(Mesh::PRIMITIVE_POINTS, a); - if (valid) { - RS::get_singleton()->canvas_item_add_mesh(ins.instance, mesh->get_rid(), Transform2D(), p_color); - ins.create_instance(canvas_item, hidden); + int64_t handle_count = p_handles.size(); + for (const Vector2 &h : p_handles) { + Vector2 position = xform.xform(h); + Instance ins; + ins.create_instance(viewport, hidden); + instances.push_back(ins); + // TODO: GIZMOS - the handles currently draw over the rulers when panning, we probably don't want this. + RS::get_singleton()->canvas_item_add_texture_rect(ins.instance, Rect2(position - texture_size / 2, texture_size), texture->get_rid()); } - instances.push_back(ins); - + // update internal handle lists int current_size = handle_list.size(); - handle_list.resize(current_size + p_handles.size()); - for (int i = 0; i < p_handles.size(); i++) { + handle_list.resize(current_size + handle_count); + for (int i = 0; i < handle_count; i++) { handle_list.write[current_size + i] = p_handles[i]; } @@ -401,7 +388,7 @@ void EditorCanvasItemGizmo::handles_intersect_point(const Point2 &p_point, bool return; } - Transform2D screen_transform = canvas_item->get_global_transform_with_canvas() * canvas_item->get_viewport()->get_screen_transform(); + Transform2D screen_transform = CanvasItemEditor::get_singleton()->get_canvas_transform() * canvas_item->get_screen_transform(); float min_d = 1e20; for (int i = 0; i < secondary_handles.size(); i++) { @@ -636,7 +623,7 @@ void EditorCanvasItemGizmoPlugin::_bind_methods() { 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", "screen_pos"); + 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", "screen_pos"); @@ -708,8 +695,8 @@ void EditorCanvasItemGizmoPlugin::begin_handle_action(const EditorCanvasItemGizm 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_screen_pos) { - GDVIRTUAL_CALL(_set_handle, Ref(p_gizmo), p_id, p_secondary, p_screen_pos); +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) { diff --git a/editor/scene/2d/canvas_item_editor_gizmos.h b/editor/scene/2d/canvas_item_editor_gizmos.h index fb97bcfd54c3..083f2355b295 100644 --- a/editor/scene/2d/canvas_item_editor_gizmos.h +++ b/editor/scene/2d/canvas_item_editor_gizmos.h @@ -39,6 +39,7 @@ class EditorCanvasItemGizmo : public CanvasItemGizmo { struct Instance { RID instance; + RID handle_multimesh; Transform2D xform; void create_instance(CanvasItem *p_base, bool p_hidden = false); @@ -91,7 +92,7 @@ class EditorCanvasItemGizmo : public CanvasItemGizmo { void add_collision_rect(const Rect2 &p_rect); void add_collision_polygon(const Vector &p_polygon); - void add_handles(const Vector &p_handles, const Color &p_color, const Vector &p_ids = Vector(), bool p_secondary = false); + void add_handles(const Vector &p_handles, Ref p_texture, const Vector &p_ids = Vector(), bool p_secondary = false); virtual bool is_handle_highlighted(int p_id, bool p_secondary) const; virtual String get_handle_name(int p_id, bool p_secondary) const; diff --git a/editor/scene/canvas_item_editor_plugin.cpp b/editor/scene/canvas_item_editor_plugin.cpp index 8202e1f8943b..bc4c4c4536b7 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" @@ -1704,6 +1703,57 @@ 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 pos = b->get_position(); + for (const Ref &ci_gizmo : gizmos) { + Ref editor_gizmo = ci_gizmo; + if (editor_gizmo.is_valid()) { + int index; + bool secondary; + editor_gizmo->handles_intersect_point(pos, 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 = editor_gizmo; + current_gizmo_handle = index; + current_gizmo_handle_secondary = secondary; + current_gizmo_initial_value = editor_gizmo->get_handle_value(index, secondary); + editor_gizmo->begin_handle_action(index, secondary); + return true; + } + } + } + } + } + + // 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; @@ -2802,6 +2852,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)) { @@ -2999,6 +3051,11 @@ void CanvasItemEditor::_commit_drag() { } } 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; } @@ -4230,6 +4287,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; } @@ -4320,6 +4383,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(); } @@ -4570,6 +4635,44 @@ void CanvasItemEditor::_selection_changed() { } 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 (const Ref &gizmo : gizmos) { + Ref editor_gizmo = gizmo; + 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 (const Ref &gizmo : gizmos) { + Ref editor_gizmo = gizmo; + if (editor_gizmo.is_null()) { + continue; + } + editor_gizmo->set_selected(true); + } + selected_canvas_item->update_gizmos(); + } + } + if (!p_canvas_item) { return; } @@ -5707,12 +5810,13 @@ void CanvasItemEditor::_request_gizmo_for_id(ObjectID p_id) { } void _update_all_canvas_item_gizmos(Node *p_node) { - for (int i = p_node->get_child_count() - 1; i >= 0; --i) { - CanvasItem *canvas_item = Object::cast_to(p_node->get_child(i)); - if (canvas_item) { - canvas_item->update_gizmos(); - } + 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)); } } diff --git a/editor/scene/canvas_item_editor_plugin.h b/editor/scene/canvas_item_editor_plugin.h index 92fa63ce62a8..98c8cdf3acc3 100644 --- a/editor/scene/canvas_item_editor_plugin.h +++ b/editor/scene/canvas_item_editor_plugin.h @@ -185,7 +185,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 { @@ -395,6 +396,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; @@ -505,6 +511,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); @@ -646,12 +653,16 @@ class CanvasItemEditor : public VBoxContainer { /* GIZMOS */ + Ref current_hover_gizmo; + int current_hover_gizmo_handle; + bool current_hover_gizmo_handle_secondary; + // TODO: GIZMOS actually implement this. bool is_current_selected_gizmo(const EditorCanvasItemGizmo *p_gizmo); - Ref get_current_hover_gizmo() { return Ref(); } + Ref get_current_hover_gizmo() { return current_hover_gizmo; } int get_current_hover_gizmo_handle(bool &r_secondary) const { - r_secondary = false; - return -1; + r_secondary = current_hover_gizmo_handle_secondary; + return current_hover_gizmo_handle; } bool is_subgizmo_selected(int p_id) { return false; } Vector get_subgizmo_selection() { return Vector(); } diff --git a/scene/main/canvas_item.cpp b/scene/main/canvas_item.cpp index 0e6b5a2ecb3a..f1d7c6f39500 100644 --- a/scene/main/canvas_item.cpp +++ b/scene/main/canvas_item.cpp @@ -129,10 +129,9 @@ void CanvasItem::_update_gizmos() { } data.gizmos_dirty = false; for (Ref &gizmo : data.gizmos) { + gizmo->clear(); if (is_visible_in_tree()) { gizmo->redraw(); - } else { - gizmo->clear(); } } #endif @@ -1917,6 +1916,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() { From 7c59a2af97bf18e2bbe4b62add646db6b64b9a37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Thom=C3=A4?= Date: Thu, 11 Dec 2025 07:02:34 +0100 Subject: [PATCH 06/26] Apply review changes. --- editor/scene/2d/canvas_item_editor_gizmos.cpp | 8 ++++---- editor/scene/canvas_item_editor_plugin.cpp | 9 +++------ editor/scene/canvas_item_editor_plugin.h | 2 +- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/editor/scene/2d/canvas_item_editor_gizmos.cpp b/editor/scene/2d/canvas_item_editor_gizmos.cpp index a41f44c78047..127194dab613 100644 --- a/editor/scene/2d/canvas_item_editor_gizmos.cpp +++ b/editor/scene/2d/canvas_item_editor_gizmos.cpp @@ -98,7 +98,7 @@ String EditorCanvasItemGizmo::get_handle_name(int p_id, bool p_secondary) const } bool EditorCanvasItemGizmo::is_handle_highlighted(int p_id, bool p_secondary) const { - bool success; + bool success = false; if (GDVIRTUAL_CALL(_is_handle_highlighted, p_id, p_secondary, success)) { return success; } @@ -145,7 +145,7 @@ void EditorCanvasItemGizmo::commit_handle(int p_id, bool p_secondary, const Vari } int EditorCanvasItemGizmo::subgizmos_intersect_point(const Point2 &p_point) const { - int id; + int id = -1; if (GDVIRTUAL_CALL(_subgizmos_intersect_point, p_point, id)) { return id; } @@ -299,11 +299,11 @@ void EditorCanvasItemGizmo::add_handles(const Vector &p_handles, Refget_current_hover_gizmo_handle(current_hover_handle_secondary); Ref texture = p_texture; - if (p_texture.is_null()) { + 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_NULL(texture); + ERR_FAIL_COND(texture.is_null()); Size2 texture_size = texture->get_size(); Control *viewport = CanvasItemEditor::get_singleton()->get_viewport_control(); diff --git a/editor/scene/canvas_item_editor_plugin.cpp b/editor/scene/canvas_item_editor_plugin.cpp index bc4c4c4536b7..8b97377e4513 100644 --- a/editor/scene/canvas_item_editor_plugin.cpp +++ b/editor/scene/canvas_item_editor_plugin.cpp @@ -1713,8 +1713,7 @@ bool CanvasItemEditor::_gui_input_gizmos(const Ref &p_event) { // mouse clicks if (b.is_valid() && b->get_button_index() == MouseButton::LEFT && b->is_pressed()) { Point2 pos = b->get_position(); - for (const Ref &ci_gizmo : gizmos) { - Ref editor_gizmo = ci_gizmo; + for (Ref editor_gizmo : gizmos) { if (editor_gizmo.is_valid()) { int index; bool secondary; @@ -4638,8 +4637,7 @@ 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 (const Ref &gizmo : gizmos) { - Ref editor_gizmo = gizmo; + for (Ref editor_gizmo : gizmos) { if (editor_gizmo.is_null()) { continue; } @@ -4662,8 +4660,7 @@ void CanvasItemEditor::edit(CanvasItem *p_canvas_item) { if (selected_canvas_item) { Vector> gizmos = selected_canvas_item->get_gizmos(); - for (const Ref &gizmo : gizmos) { - Ref editor_gizmo = gizmo; + for (Ref editor_gizmo : gizmos) { if (editor_gizmo.is_null()) { continue; } diff --git a/editor/scene/canvas_item_editor_plugin.h b/editor/scene/canvas_item_editor_plugin.h index 98c8cdf3acc3..744d1aab95e8 100644 --- a/editor/scene/canvas_item_editor_plugin.h +++ b/editor/scene/canvas_item_editor_plugin.h @@ -186,7 +186,7 @@ class CanvasItemEditor : public VBoxContainer { DRAG_H_GUIDE, DRAG_DOUBLE_GUIDE, DRAG_KEY_MOVE, - DRAG_GIZMO_HANDLE + DRAG_GIZMO_HANDLE, }; enum GridVisibility { From 92b99c73c33952d1b2d3ce579aeeda423a1a2799 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Thom=C3=A4?= Date: Thu, 11 Dec 2025 07:11:10 +0100 Subject: [PATCH 07/26] Add gizmo menu support. --- editor/scene/2d/canvas_item_editor_gizmos.cpp | 14 +- editor/scene/2d/canvas_item_editor_gizmos.h | 1 - editor/scene/canvas_item_editor_plugin.cpp | 135 ++++++++++++++---- editor/scene/canvas_item_editor_plugin.h | 4 +- 4 files changed, 120 insertions(+), 34 deletions(-) diff --git a/editor/scene/2d/canvas_item_editor_gizmos.cpp b/editor/scene/2d/canvas_item_editor_gizmos.cpp index 127194dab613..804c04586228 100644 --- a/editor/scene/2d/canvas_item_editor_gizmos.cpp +++ b/editor/scene/2d/canvas_item_editor_gizmos.cpp @@ -312,13 +312,21 @@ void EditorCanvasItemGizmo::add_handles(const Vector &p_handles, Refget_canvas_transform() * canvas_item->get_screen_transform(); int64_t handle_count = p_handles.size(); - for (const Vector2 &h : p_handles) { - Vector2 position = xform.xform(h); + 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, hidden); 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); + } + // TODO: GIZMOS - the handles currently draw over the rulers when panning, we probably don't want this. - RS::get_singleton()->canvas_item_add_texture_rect(ins.instance, Rect2(position - texture_size / 2, texture_size), texture->get_rid()); + 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 diff --git a/editor/scene/2d/canvas_item_editor_gizmos.h b/editor/scene/2d/canvas_item_editor_gizmos.h index 083f2355b295..1d320b3a876b 100644 --- a/editor/scene/2d/canvas_item_editor_gizmos.h +++ b/editor/scene/2d/canvas_item_editor_gizmos.h @@ -142,7 +142,6 @@ class EditorCanvasItemGizmoPlugin : public Resource { public: static const int VISIBLE = 0; static const int HIDDEN = 1; - static const int ON_TOP = 2; protected: int current_state; diff --git a/editor/scene/canvas_item_editor_plugin.cpp b/editor/scene/canvas_item_editor_plugin.cpp index 8b97377e4513..f91605105ab6 100644 --- a/editor/scene/canvas_item_editor_plugin.cpp +++ b/editor/scene/canvas_item_editor_plugin.cpp @@ -929,6 +929,35 @@ Vector2 CanvasItemEditor::_position_to_anchor(const Control *p_control, Vector2 return output; } +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; + const int state = plugin->get_state(); + + gizmos_menu->add_check_item(plugin_name, plugin_id); + int index = gizmos_menu->get_item_index(plugin_id); + gizmos_menu->set_item_checked(index, state == EditorCanvasItemGizmoPlugin::VISIBLE); + } +} + void CanvasItemEditor::_save_canvas_item_state(const List &p_canvas_items, bool save_bones) { original_transform = Transform2D(); bool transform_stored = false; @@ -1714,21 +1743,22 @@ bool CanvasItemEditor::_gui_input_gizmos(const Ref &p_event) { if (b.is_valid() && b->get_button_index() == MouseButton::LEFT && b->is_pressed()) { Point2 pos = b->get_position(); for (Ref editor_gizmo : gizmos) { - if (editor_gizmo.is_valid()) { - int index; - bool secondary; - editor_gizmo->handles_intersect_point(pos, 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 = editor_gizmo; - current_gizmo_handle = index; - current_gizmo_handle_secondary = secondary; - current_gizmo_initial_value = editor_gizmo->get_handle_value(index, secondary); - editor_gizmo->begin_handle_action(index, secondary); - return true; - } + if (editor_gizmo.is_null()) { + continue; + } + int index; + bool secondary; + editor_gizmo->handles_intersect_point(pos, 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 = editor_gizmo; + current_gizmo_handle = index; + current_gizmo_handle_secondary = secondary; + current_gizmo_initial_value = editor_gizmo->get_handle_value(index, secondary); + editor_gizmo->begin_handle_action(index, secondary); + return true; } } } @@ -5320,6 +5350,28 @@ 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()) { + int plugin_state = gizmo_plugins_by_name[gizmo_plugin_index]->get_state(); + int menu_item_index = gizmos_menu->get_item_index(p_op); + switch (plugin_state) { + case EditorCanvasItemGizmoPlugin::VISIBLE: { + gizmo_plugins_by_name[gizmo_plugin_index]->set_state(EditorCanvasItemGizmoPlugin::HIDDEN); + gizmos_menu->set_item_checked(menu_item_index, false); + viewport->queue_redraw(); + } break; + + case EditorCanvasItemGizmoPlugin::HIDDEN: { + gizmo_plugins_by_name[gizmo_plugin_index]->set_state(EditorCanvasItemGizmoPlugin::VISIBLE); + gizmos_menu->set_item_checked(menu_item_index, true); + viewport->queue_redraw(); + } break; + + default: + break; + } + } } void CanvasItemEditor::_set_owner_for_node_and_children(Node *p_node, Node *p_owner) { @@ -5436,15 +5488,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->get_state(); + } + + 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 @@ -5576,26 +5644,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_state(gizmo_state[plugin_gizmo_name]); + } + } + } + update_gizmos = true; } if (state.has("show_zoom_control")) { @@ -5630,6 +5706,11 @@ void CanvasItemEditor::set_state(const Dictionary &p_state) { if (update_scrollbars) { _update_scrollbars(); } + + if (update_gizmos) { + _update_gizmos_menu(); + } + viewport->queue_redraw(); } @@ -6209,12 +6290,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); diff --git a/editor/scene/canvas_item_editor_plugin.h b/editor/scene/canvas_item_editor_plugin.h index 744d1aab95e8..b7659bf884bf 100644 --- a/editor/scene/canvas_item_editor_plugin.h +++ b/editor/scene/canvas_item_editor_plugin.h @@ -154,6 +154,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 { @@ -444,7 +446,7 @@ class CanvasItemEditor : public VBoxContainer { Vector2 _anchor_to_position(const Control *p_control, Vector2 anchor); Vector2 _position_to_anchor(const Control *p_control, Vector2 position); - void _update_gizmos_menu() {} // TODO: GIZMOS, implement. + void _update_gizmos_menu(); void _prepare_view_menu(); void _popup_callback(int p_op); From fecb68c253728e97fcd16e3de559e7e3750122c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Thom=C3=A4?= Date: Tue, 16 Dec 2025 08:19:23 +0100 Subject: [PATCH 08/26] Add subgizmo selection. --- editor/scene/canvas_item_editor_plugin.cpp | 233 ++++++++++++++++++++- editor/scene/canvas_item_editor_plugin.h | 11 +- scene/main/canvas_item.cpp | 34 +++ scene/main/canvas_item.h | 2 + 4 files changed, 274 insertions(+), 6 deletions(-) diff --git a/editor/scene/canvas_item_editor_plugin.cpp b/editor/scene/canvas_item_editor_plugin.cpp index f91605105ab6..6c3ed1afa979 100644 --- a/editor/scene/canvas_item_editor_plugin.cpp +++ b/editor/scene/canvas_item_editor_plugin.cpp @@ -2642,6 +2642,48 @@ bool CanvasItemEditor::_gui_input_select(const Ref &p_event) { return true; } + // first try to select subgizmos + if (selected_canvas_item) { + CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data(selected_canvas_item); + bool intersected_subgizmo = false; + if (se) { + for (Ref gizmo : selected_canvas_item->get_gizmos()) { + if (gizmo.is_null()) { + continue; + } + + int subgizmo_id = gizmo->subgizmos_intersect_point(click); + if (subgizmo_id != -1) { + if (b->is_shift_pressed()) { + if (se->subgizmos.has(subgizmo_id)) { + se->subgizmos.erase(subgizmo_id); + } else { + se->subgizmos.insert(subgizmo_id, gizmo->get_subgizmo_transform(subgizmo_id)); + } + } else { + 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; + } + + gizmo->redraw(); + update_transform_gizmo(); + intersected_subgizmo = true; + break; // only select the first hit + } + } + } + + if (intersected_subgizmo) { + return true; + } + } + // Find the item to select. CanvasItem *ci = nullptr; @@ -2717,8 +2759,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) { @@ -2728,7 +2768,74 @@ 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()); } @@ -4661,6 +4768,53 @@ 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::edit(CanvasItem *p_canvas_item) { @@ -5444,6 +5598,8 @@ void CanvasItemEditor::_bind_methods() { 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); @@ -5887,6 +6043,54 @@ void CanvasItemEditor::_request_gizmo_for_id(ObjectID p_id) { } } +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); @@ -5911,6 +6115,14 @@ void CanvasItemEditor::update_all_gizmos(Node *p_node) { _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) { @@ -5919,6 +6131,21 @@ bool CanvasItemEditor::is_current_selected_gizmo(const EditorCanvasItemGizmo *p_ 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; diff --git a/editor/scene/canvas_item_editor_plugin.h b/editor/scene/canvas_item_editor_plugin.h index b7659bf884bf..d6cfd1957f2f 100644 --- a/editor/scene/canvas_item_editor_plugin.h +++ b/editor/scene/canvas_item_editor_plugin.h @@ -582,6 +582,8 @@ class CanvasItemEditor : public VBoxContainer { 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); @@ -658,16 +660,19 @@ class CanvasItemEditor : public VBoxContainer { Ref current_hover_gizmo; int current_hover_gizmo_handle; bool current_hover_gizmo_handle_secondary; + bool gizmos_dirty = false; - // TODO: GIZMOS actually implement this. 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) { return false; } - Vector get_subgizmo_selection() { return Vector(); } + bool is_subgizmo_selected(int p_id); + Vector get_subgizmo_selection(); + void clear_subgizmo_selection(Object *p_obj = nullptr); + void refresh_dirty_gizmos(); + // TODO: GIZMOS actually implement this. void update_transform_gizmo() {} void update_all_gizmos(Node *p_node = nullptr); void add_gizmo_plugin(Ref p_plugin); diff --git a/scene/main/canvas_item.cpp b/scene/main/canvas_item.cpp index f1d7c6f39500..8f50904cefe1 100644 --- a/scene/main/canvas_item.cpp +++ b/scene/main/canvas_item.cpp @@ -182,6 +182,37 @@ Vector> CanvasItem::get_gizmos() const { #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) { @@ -1475,9 +1506,12 @@ 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"), &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); diff --git a/scene/main/canvas_item.h b/scene/main/canvas_item.h index 319dbe429784..0a72498c6b5d 100644 --- a/scene/main/canvas_item.h +++ b/scene/main/canvas_item.h @@ -295,6 +295,8 @@ class CanvasItem : public Node { 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 */ From 947c8f04d00c92d43728b4a0977cacd3b06f0b6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Thom=C3=A4?= Date: Mon, 29 Dec 2025 08:58:12 +0100 Subject: [PATCH 09/26] Add subgizmo movement. --- editor/scene/2d/canvas_item_editor_gizmos.cpp | 8 +- editor/scene/2d/canvas_item_editor_gizmos.h | 3 +- editor/scene/canvas_item_editor_plugin.cpp | 301 +++++++++++------- editor/scene/canvas_item_editor_plugin.h | 13 +- scene/main/canvas_item.cpp | 3 +- 5 files changed, 192 insertions(+), 136 deletions(-) diff --git a/editor/scene/2d/canvas_item_editor_gizmos.cpp b/editor/scene/2d/canvas_item_editor_gizmos.cpp index 804c04586228..d4ef9cba5460 100644 --- a/editor/scene/2d/canvas_item_editor_gizmos.cpp +++ b/editor/scene/2d/canvas_item_editor_gizmos.cpp @@ -61,8 +61,6 @@ void EditorCanvasItemGizmo::clear() { if (instance.instance.is_valid()) { RS::get_singleton()->free_rid(instance.instance); instance.instance = RID(); - RS::get_singleton()->free_rid(instance.handle_multimesh); - instance.handle_multimesh = RID(); } } @@ -77,6 +75,8 @@ void EditorCanvasItemGizmo::clear() { } void EditorCanvasItemGizmo::redraw() { + clear(); + if (!GDVIRTUAL_CALL(_redraw)) { ERR_FAIL_NULL(gizmo_plugin); gizmo_plugin->redraw(this); @@ -634,7 +634,7 @@ void EditorCanvasItemGizmoPlugin::_bind_methods() { 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", "screen_pos"); + GDVIRTUAL_BIND(_subgizmos_intersect_point, "gizmo", "point"); GDVIRTUAL_BIND(_subgizmos_intersect_rect, "gizmo", "rect"); GDVIRTUAL_BIND(_get_subgizmo_transform, "gizmo", "subgizmo_id"); GDVIRTUAL_BIND(_set_subgizmo_transform, "gizmo", "subgizmo_id", "transform"); @@ -737,7 +737,7 @@ void EditorCanvasItemGizmoPlugin::commit_subgizmos(const EditorCanvasItemGizmo * TypedArray transforms; transforms.reserve(p_transforms.size()); for (int i = 0; i < p_transforms.size(); i++) { - transforms[i] = p_transforms[i]; + transforms.append(p_transforms[i]); } GDVIRTUAL_CALL(_commit_subgizmos, Ref(p_gizmo), p_ids, transforms, p_cancel); diff --git a/editor/scene/2d/canvas_item_editor_gizmos.h b/editor/scene/2d/canvas_item_editor_gizmos.h index 1d320b3a876b..cec835493db5 100644 --- a/editor/scene/2d/canvas_item_editor_gizmos.h +++ b/editor/scene/2d/canvas_item_editor_gizmos.h @@ -39,7 +39,6 @@ class EditorCanvasItemGizmo : public CanvasItemGizmo { struct Instance { RID instance; - RID handle_multimesh; Transform2D xform; void create_instance(CanvasItem *p_base, bool p_hidden = false); @@ -50,6 +49,8 @@ class EditorCanvasItemGizmo : public CanvasItemGizmo { 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; diff --git a/editor/scene/canvas_item_editor_plugin.cpp b/editor/scene/canvas_item_editor_plugin.cpp index 6c3ed1afa979..23763c57972b 100644 --- a/editor/scene/canvas_item_editor_plugin.cpp +++ b/editor/scene/canvas_item_editor_plugin.cpp @@ -832,7 +832,61 @@ void CanvasItemEditor::_find_canvas_items_in_rect(const Rect2 &p_rect, Node *p_n } } -bool CanvasItemEditor::_select_click_on_item(CanvasItem *item, Point2 p_click_pos, bool p_append) { +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; + } + + int subgizmo_id = gizmo->subgizmos_intersect_point(p_click_pos); + 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; + } + } + } + } + return false; +} + +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()) { @@ -958,13 +1012,20 @@ void CanvasItemEditor::_update_gizmos_menu() { } } -void CanvasItemEditor::_save_canvas_item_state(const List &p_canvas_items, bool save_bones) { +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 (se->gizmo.is_valid()) { + // if we currently have a subgizmo selection, we are not going to change the + // canvas item itself. since the subgizmo state is saved during selection, we + // don't need to do anything here. + continue; + } + if (!transform_stored) { original_transform = ci->get_global_transform(); transform_stored = true; @@ -981,16 +1042,62 @@ 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() { + 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); } } -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) { + 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) { Dictionary old_state = editor_selection->get_node_editor_data(ci)->undo_state; Dictionary new_state = ci->_edit_get_state(); @@ -1010,13 +1117,6 @@ void CanvasItemEditor::_commit_canvas_item_state(const List &p_can 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); - } - } } } undo_redo->add_do_method(viewport, "queue_redraw"); @@ -1047,7 +1147,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(); } @@ -1559,7 +1659,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) { @@ -1581,7 +1681,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()); @@ -1606,7 +1706,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(); @@ -1663,7 +1763,7 @@ bool CanvasItemEditor::_gui_input_rotate(const Ref &p_event) { } 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) { @@ -1678,7 +1778,7 @@ 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); + _restore_drag_selection_state(); for (CanvasItem *ci : drag_selection) { drag_to = transform.affine_inverse().xform(m->get_position()); //Rotate the opposite way if the canvas item's compounded scale has an uneven number of negative elements @@ -1706,7 +1806,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; @@ -1828,7 +1928,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; } } @@ -1840,7 +1940,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()); @@ -1916,7 +2016,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(); @@ -1985,7 +2085,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; } } @@ -2088,7 +2188,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(); @@ -2153,7 +2253,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) { @@ -2165,7 +2265,7 @@ 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); + _restore_drag_selection_state(); drag_to = transform.affine_inverse().xform(m->get_position()); @@ -2291,7 +2391,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; @@ -2346,7 +2446,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 { @@ -2362,7 +2462,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; @@ -2405,6 +2505,21 @@ bool CanvasItemEditor::_gui_input_move(const Ref &p_event) { } for (CanvasItem *ci : drag_selection) { + CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data(ci); + if (se->gizmo.is_valid()) { + // if we currently have a subgizmo selection, we only move this subgizmo selection - + // not the node - similar to how this is done in 3D. + for (KeyValue &entry : se->subgizmos) { + // we use the delta for the snapped position, this way we get snapping for subgizmos + Transform2D new_xform = entry.value.translated(new_pos - previous_pos); + 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(); + continue; + } + // 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)); } @@ -2419,7 +2534,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(); @@ -2445,11 +2560,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(); @@ -2533,7 +2648,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()) { @@ -2635,6 +2750,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(); @@ -2642,48 +2763,6 @@ bool CanvasItemEditor::_gui_input_select(const Ref &p_event) { return true; } - // first try to select subgizmos - if (selected_canvas_item) { - CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data(selected_canvas_item); - bool intersected_subgizmo = false; - if (se) { - for (Ref gizmo : selected_canvas_item->get_gizmos()) { - if (gizmo.is_null()) { - continue; - } - - int subgizmo_id = gizmo->subgizmos_intersect_point(click); - if (subgizmo_id != -1) { - if (b->is_shift_pressed()) { - if (se->subgizmos.has(subgizmo_id)) { - se->subgizmos.erase(subgizmo_id); - } else { - se->subgizmos.insert(subgizmo_id, gizmo->get_subgizmo_transform(subgizmo_id)); - } - } else { - 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; - } - - gizmo->redraw(); - update_transform_gizmo(); - intersected_subgizmo = true; - break; // only select the first hit - } - } - } - - if (intersected_subgizmo) { - return true; - } - } - // Find the item to select. CanvasItem *ci = nullptr; @@ -2711,7 +2790,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. @@ -2747,7 +2826,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; } @@ -3027,8 +3106,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(), @@ -3039,17 +3117,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()) { @@ -3063,8 +3137,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; @@ -3083,24 +3156,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()) { @@ -3116,18 +3185,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); @@ -3140,19 +3205,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)); } } @@ -3172,18 +3233,14 @@ 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; diff --git a/editor/scene/canvas_item_editor_plugin.h b/editor/scene/canvas_item_editor_plugin.h index d6cfd1957f2f..d7a147776717 100644 --- a/editor/scene/canvas_item_editor_plugin.h +++ b/editor/scene/canvas_item_editor_plugin.h @@ -70,9 +70,6 @@ class CanvasItemEditorSelectedItem : public Object { Ref gizmo; HashMap subgizmos; // Key: Subgizmo ID, Value: Initial subgizmo transform. - List pre_drag_bones_length; - List pre_drag_bones_undo_state; - Dictionary undo_state; }; @@ -432,16 +429,16 @@ 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); 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); diff --git a/scene/main/canvas_item.cpp b/scene/main/canvas_item.cpp index 8f50904cefe1..f1adf786784a 100644 --- a/scene/main/canvas_item.cpp +++ b/scene/main/canvas_item.cpp @@ -129,9 +129,10 @@ void CanvasItem::_update_gizmos() { } data.gizmos_dirty = false; for (Ref &gizmo : data.gizmos) { - gizmo->clear(); if (is_visible_in_tree()) { gizmo->redraw(); + } else { + gizmo->clear(); } } #endif From c668fd093007e8118a0b8cbaf9ad3b3085569495 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Thom=C3=A4?= Date: Tue, 30 Dec 2025 08:37:54 +0100 Subject: [PATCH 10/26] Add subgizmo rotation. --- editor/scene/canvas_item_editor_plugin.cpp | 30 +++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/editor/scene/canvas_item_editor_plugin.cpp b/editor/scene/canvas_item_editor_plugin.cpp index 23763c57972b..9b643fdc901f 100644 --- a/editor/scene/canvas_item_editor_plugin.cpp +++ b/editor/scene/canvas_item_editor_plugin.cpp @@ -1745,7 +1745,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; @@ -1783,6 +1791,26 @@ bool CanvasItemEditor::_gui_input_rotate(const Ref &p_event) { drag_to = transform.affine_inverse().xform(m->get_position()); //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); + + CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data(ci); + if (se->gizmo.is_valid()) { + // if we currently have a subgizmo selection, we only rotate this subgizmo selection - + // not the node - similar to how this is done in 3D. + + // we rotate around the drag rotation center, so we need the angle drag_from -> drag_rotation_center -> drag_to + 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) { + // since we rotate around the drag rotation center, we move there, rotate and move back + Transform2D new_xform = entry.value.translated(-drag_rotation_center).rotated(angle).translated(drag_rotation_center); + 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(); + continue; + } + //no subgizmos selected - rotate the canvas item real_t prev_rotation = ci->_edit_get_rotation(); real_t new_rotation = snap_angle(ci->_edit_get_rotation() + (opposite ? -1 : 1) * (drag_from - drag_rotation_center).angle_to(drag_to - drag_rotation_center), prev_rotation); From 06e2466ceccbf280b53af7f7ba5df96baeaf5611 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Thom=C3=A4?= Date: Tue, 30 Dec 2025 10:02:19 +0100 Subject: [PATCH 11/26] Fix gizmo picking on transformed items, simplify interface. --- editor/scene/2d/canvas_item_editor_gizmos.cpp | 42 +++++++------------ editor/scene/2d/canvas_item_editor_gizmos.h | 16 +++---- editor/scene/canvas_item_editor_plugin.cpp | 15 +++++-- 3 files changed, 35 insertions(+), 38 deletions(-) diff --git a/editor/scene/2d/canvas_item_editor_gizmos.cpp b/editor/scene/2d/canvas_item_editor_gizmos.cpp index d4ef9cba5460..fe2975d97b04 100644 --- a/editor/scene/2d/canvas_item_editor_gizmos.cpp +++ b/editor/scene/2d/canvas_item_editor_gizmos.cpp @@ -35,8 +35,6 @@ #include "editor/scene/canvas_item_editor_plugin.h" #include "scene/resources/mesh.h" -#define HANDLE_HALF_SIZE 9.5 - bool EditorCanvasItemGizmo::is_editable() const { ERR_FAIL_NULL_V(canvas_item, false); @@ -144,14 +142,14 @@ void EditorCanvasItemGizmo::commit_handle(int p_id, bool p_secondary, const Vari gizmo_plugin->commit_handle(this, p_id, p_secondary, p_restore, p_cancel); } -int EditorCanvasItemGizmo::subgizmos_intersect_point(const Point2 &p_point) const { +int EditorCanvasItemGizmo::subgizmos_intersect_point(const Point2 &p_point, real_t p_max_distance) const { int id = -1; - if (GDVIRTUAL_CALL(_subgizmos_intersect_point, p_point, id)) { + if (GDVIRTUAL_CALL(_subgizmos_intersect_point, p_point, p_max_distance, id)) { return id; } ERR_FAIL_NULL_V(gizmo_plugin, -1); - return gizmo_plugin->subgizmos_intersect_point(this, p_point); + return gizmo_plugin->subgizmos_intersect_point(this, p_point, p_max_distance); } Vector EditorCanvasItemGizmo::subgizmos_intersect_rect(const Rect2 &p_rect) const { @@ -385,7 +383,7 @@ bool EditorCanvasItemGizmo::intersect_rect(const Rect2 &p_rect) const { return false; } -void EditorCanvasItemGizmo::handles_intersect_point(const Point2 &p_point, bool p_shift_pressed, int &r_id, bool &r_secondary) { +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; @@ -396,13 +394,10 @@ void EditorCanvasItemGizmo::handles_intersect_point(const Point2 &p_point, bool return; } - Transform2D screen_transform = CanvasItemEditor::get_singleton()->get_canvas_transform() * canvas_item->get_screen_transform(); - float min_d = 1e20; - + real_t min_d = 1e20; for (int i = 0; i < secondary_handles.size(); i++) { - Vector2 screen_pos = screen_transform.xform(secondary_handles[i]); - float distance = screen_pos.distance_to(p_point); - if (distance < HANDLE_HALF_SIZE && distance < min_d) { + 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; @@ -418,11 +413,9 @@ void EditorCanvasItemGizmo::handles_intersect_point(const Point2 &p_point, bool } min_d = 1e20; - for (int i = 0; i < handles.size(); i++) { - Vector2 screen_pos = screen_transform.xform(handles[i]); - float distance = screen_pos.distance_to(p_point); - if (distance < HANDLE_HALF_SIZE && distance < min_d) { + 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; @@ -434,7 +427,7 @@ void EditorCanvasItemGizmo::handles_intersect_point(const Point2 &p_point, bool } } -bool EditorCanvasItemGizmo::intersect_point(const Point2 &p_point) const { +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); @@ -442,26 +435,23 @@ bool EditorCanvasItemGizmo::intersect_point(const Point2 &p_point) const { return false; } - Transform2D to_local = canvas_item->get_global_transform().affine_inverse(); - Point2 local_point = to_local.xform(p_point); - 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(local_point, a, b); - if (closest.distance_to(local_point) < 8) { // TODO: GIZMOS 3d uses a magic 8 here, not sure why + 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(local_point)) { + if (collision_rect.has_point(p_point)) { return true; } } for (const Vector &collision_polygon : collision_polygons) { - if (Geometry2D::is_point_in_polygon(local_point, collision_polygon)) { + if (Geometry2D::is_point_in_polygon(p_point, collision_polygon)) { return true; } } @@ -711,9 +701,9 @@ void EditorCanvasItemGizmoPlugin::commit_handle(const EditorCanvasItemGizmo *p_g 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) const { +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, ret); + GDVIRTUAL_CALL(_subgizmos_intersect_point, Ref(p_gizmo), p_point, p_max_distance, ret); return ret; } diff --git a/editor/scene/2d/canvas_item_editor_gizmos.h b/editor/scene/2d/canvas_item_editor_gizmos.h index cec835493db5..7c0a1adf97c0 100644 --- a/editor/scene/2d/canvas_item_editor_gizmos.h +++ b/editor/scene/2d/canvas_item_editor_gizmos.h @@ -77,7 +77,7 @@ class EditorCanvasItemGizmo : public CanvasItemGizmo { GDVIRTUAL3(_set_handle, int, bool, Vector2) GDVIRTUAL4(_commit_handle, int, bool, Variant, bool) - GDVIRTUAL1RC(int, _subgizmos_intersect_point, Vector2) + 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) @@ -102,8 +102,8 @@ class EditorCanvasItemGizmo : public CanvasItemGizmo { 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) const; - virtual Vector subgizmos_intersect_rect(const Rect2 &p_rect) const; + virtual int subgizmos_intersect_point(const Point2 &p_point, real_t p_max_distance) const; // TODO: GIZMOS: docs -> this position is in local space. + virtual Vector subgizmos_intersect_rect(const Rect2 &p_rect) const; // TODO: GIZMOS: docs -> this rect is in canvas space 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); @@ -116,9 +116,9 @@ class EditorCanvasItemGizmo : public CanvasItemGizmo { Ref get_plugin() const { return gizmo_plugin; } - bool intersect_rect(const Rect2 &p_rect) const; - void handles_intersect_point(const Point2 &p_point, bool p_shift_pressed, int &r_id, bool &r_secondary); - bool intersect_point(const Point2 &p_point) const; + bool intersect_rect(const Rect2 &p_rect) const; // TODO: GIZMOS: docs -> this rect is in canvas space + void handles_intersect_point(const Point2 &p_point, real_t p_max_distance, bool p_shift_pressed, int &r_id, bool &r_secondary); // TODO: GIZMOS: docs -> this position is in local space, so is distance + bool intersect_point(const Point2 &p_point, real_t p_max_distance) const; // TODO: GIZMOS: docs -> this position is in local space, so is max_distance bool is_subgizmo_selected(int p_id) const; Vector get_subgizmo_selection() const; @@ -169,7 +169,7 @@ class EditorCanvasItemGizmoPlugin : public Resource { GDVIRTUAL4(_set_handle, Ref, int, bool, Vector2) GDVIRTUAL5(_commit_handle, Ref, int, bool, Variant, bool) - GDVIRTUAL2RC(int, _subgizmos_intersect_point, Ref, Vector2) + 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) @@ -190,7 +190,7 @@ class EditorCanvasItemGizmoPlugin : public Resource { 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); - virtual int subgizmos_intersect_point(const EditorCanvasItemGizmo *p_gizmo, const Vector2 &p_point) const; + 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); diff --git a/editor/scene/canvas_item_editor_plugin.cpp b/editor/scene/canvas_item_editor_plugin.cpp index 9b643fdc901f..dbb500146243 100644 --- a/editor/scene/canvas_item_editor_plugin.cpp +++ b/editor/scene/canvas_item_editor_plugin.cpp @@ -698,7 +698,7 @@ void CanvasItemEditor::find_canvas_items_at_pos(const Point2 &p_pos, Node *p_nod if (gizmo.is_null()) { continue; } - if (gizmo->intersect_point(point)) { + if (gizmo->intersect_point(point, local_grab_distance)) { Node2D *node = Object::cast_to(ci); _SelectResult res; res.item = ci; @@ -843,7 +843,11 @@ bool CanvasItemEditor::_select_subgizmos(Point2 p_click_pos, bool p_append) { continue; } - int subgizmo_id = gizmo->subgizmos_intersect_point(p_click_pos); + 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)) { @@ -1869,14 +1873,17 @@ bool CanvasItemEditor::_gui_input_gizmos(const Ref &p_event) { // mouse clicks if (b.is_valid() && b->get_button_index() == MouseButton::LEFT && b->is_pressed()) { - Point2 pos = b->get_position(); + Point2 click = transform.affine_inverse().xform(b->get_position()); for (Ref editor_gizmo : gizmos) { if (editor_gizmo.is_null()) { continue; } int index; bool secondary; - editor_gizmo->handles_intersect_point(pos, b->is_shift_pressed(), index, 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); + editor_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); From 9c5e448f997345a1f7bd1e4d19c2e07e4b9363c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Thom=C3=A4?= Date: Fri, 9 Jan 2026 08:49:58 +0100 Subject: [PATCH 12/26] Fix gizmo movement on rotated and scaled transforms. Fix subgizmo deselection. --- editor/scene/canvas_item_editor_plugin.cpp | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/editor/scene/canvas_item_editor_plugin.cpp b/editor/scene/canvas_item_editor_plugin.cpp index dbb500146243..299717f6db1a 100644 --- a/editor/scene/canvas_item_editor_plugin.cpp +++ b/editor/scene/canvas_item_editor_plugin.cpp @@ -885,6 +885,16 @@ bool CanvasItemEditor::_select_subgizmos(Point2 p_click_pos, bool p_append) { 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; @@ -2542,11 +2552,16 @@ bool CanvasItemEditor::_gui_input_move(const Ref &p_event) { for (CanvasItem *ci : drag_selection) { CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data(ci); if (se->gizmo.is_valid()) { + // we use the delta for the snapped position, this way we get snapping for subgizmos + Vector2 delta = new_pos - previous_pos; + // delta is in canvas space, we need to convert it to local space for the gizmo + Transform2D parent_xform_inv = ci->get_global_transform().affine_inverse(); + delta = parent_xform_inv.xform(delta); + // if we currently have a subgizmo selection, we only move this subgizmo selection - // not the node - similar to how this is done in 3D. for (KeyValue &entry : se->subgizmos) { - // we use the delta for the snapped position, this way we get snapping for subgizmos - Transform2D new_xform = entry.value.translated(new_pos - previous_pos); + 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 From 38dd2f14f6968a43ad21fd366e65a56d429c5f66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Thom=C3=A4?= Date: Tue, 13 Jan 2026 20:18:22 +0100 Subject: [PATCH 13/26] Add support for editing boundaries and pivots (WIP, no undo yet). --- editor/scene/2d/canvas_item_editor_gizmos.cpp | 288 +++++++++++++++++- editor/scene/2d/canvas_item_editor_gizmos.h | 56 +++- editor/scene/canvas_item_editor_plugin.cpp | 163 +++++++--- editor/scene/canvas_item_editor_plugin.h | 1 + 4 files changed, 459 insertions(+), 49 deletions(-) diff --git a/editor/scene/2d/canvas_item_editor_gizmos.cpp b/editor/scene/2d/canvas_item_editor_gizmos.cpp index fe2975d97b04..80cd5a0b26ab 100644 --- a/editor/scene/2d/canvas_item_editor_gizmos.cpp +++ b/editor/scene/2d/canvas_item_editor_gizmos.cpp @@ -32,6 +32,7 @@ #include "core/math/geometry_2d.h" #include "editor/editor_node.h" +#include "editor/editor_undo_redo_manager.h" #include "editor/scene/canvas_item_editor_plugin.h" #include "scene/resources/mesh.h" @@ -52,6 +53,12 @@ bool EditorCanvasItemGizmo::is_editable() const { return false; } +bool EditorCanvasItemGizmo::has_boundary() const { + ERR_FAIL_NULL_V(canvas_item, false); + ERR_FAIL_NULL_V(gizmo_plugin, false); + return GDVIRTUAL_IS_OVERRIDDEN(_get_boundary) || gizmo_plugin->has_boundary(this); +} + void EditorCanvasItemGizmo::clear() { ERR_FAIL_NULL(RenderingServer::get_singleton()); @@ -70,6 +77,8 @@ void EditorCanvasItemGizmo::clear() { handle_ids.clear(); secondary_handles.clear(); secondary_handle_ids.clear(); + use_boundary_handle = false; + use_pivot_handle = false; } void EditorCanvasItemGizmo::redraw() { @@ -85,6 +94,86 @@ void EditorCanvasItemGizmo::redraw() { } } +void EditorCanvasItemGizmo::begin_boundary_action() { + if (GDVIRTUAL_CALL(_begin_boundary_action)) { + return; + } + + ERR_FAIL_NULL(gizmo_plugin); + gizmo_plugin->begin_boundary_action(this); +} + +Rect2 EditorCanvasItemGizmo::get_boundary() const { + Rect2 ret; + if (GDVIRTUAL_CALL(_get_boundary, ret)) { + return ret; + } + + ERR_FAIL_NULL_V(gizmo_plugin, Rect2()); + return gizmo_plugin->get_boundary(this); +} + +void EditorCanvasItemGizmo::set_boundary(const Rect2 &p_rect) { + if (GDVIRTUAL_CALL(_set_boundary, p_rect)) { + return; + } + ERR_FAIL_NULL(gizmo_plugin); + gizmo_plugin->set_boundary(this, p_rect); +} + +void EditorCanvasItemGizmo::commit_boundary(const Rect2 &p_restore, bool p_cancel) { + if (GDVIRTUAL_CALL(_commit_boundary, p_restore, p_cancel)) { + return; + } + + ERR_FAIL_NULL(gizmo_plugin); + gizmo_plugin->commit_boundary(this, p_restore, p_cancel); +} + +bool EditorCanvasItemGizmo::has_pivot() const { + ERR_FAIL_NULL_V(canvas_item, false); + ERR_FAIL_NULL_V(gizmo_plugin, false); + + return GDVIRTUAL_IS_OVERRIDDEN(_get_pivot) || gizmo_plugin->has_pivot(this); +} + +void EditorCanvasItemGizmo::begin_pivot_action() { + if (GDVIRTUAL_CALL(_begin_pivot_action)) { + return; + } + + ERR_FAIL_NULL(gizmo_plugin); + gizmo_plugin->begin_pivot_action(this); +} + +Vector2 EditorCanvasItemGizmo::get_pivot() const { + Vector2 ret; + if (GDVIRTUAL_CALL(_get_pivot, ret)) { + return ret; + } + + ERR_FAIL_NULL_V(gizmo_plugin, Vector2()); + return gizmo_plugin->get_pivot(this); +} + +void EditorCanvasItemGizmo::set_pivot(const Vector2 &p_point) { + if (GDVIRTUAL_CALL(_set_pivot, p_point)) { + return; + } + + ERR_FAIL_NULL(gizmo_plugin); + gizmo_plugin->set_pivot(this, p_point); +} + +void EditorCanvasItemGizmo::commit_pivot(const Vector2 &p_restore, bool p_cancel) { + if (GDVIRTUAL_CALL(_commit_pivot, p_restore, p_cancel)) { + return; + } + + ERR_FAIL_NULL(gizmo_plugin); + gizmo_plugin->commit_pivot(this, p_restore, p_cancel); +} + String EditorCanvasItemGizmo::get_handle_name(int p_id, bool p_secondary) const { String ret; if (GDVIRTUAL_CALL(_get_handle_name, p_id, p_secondary, ret)) { @@ -542,6 +631,16 @@ void EditorCanvasItemGizmo::_bind_methods() { GDVIRTUAL_BIND(_get_handle_name, "id", "secondary"); GDVIRTUAL_BIND(_is_handle_highlighted, "id", "secondary"); + GDVIRTUAL_BIND(_get_boundary); + GDVIRTUAL_BIND(_begin_boundary_action); + GDVIRTUAL_BIND(_set_boundary, "boundary"); + GDVIRTUAL_BIND(_commit_boundary, "restore", "cancel"); + + GDVIRTUAL_BIND(_get_pivot); + GDVIRTUAL_BIND(_begin_pivot_action); + GDVIRTUAL_BIND(_set_pivot, "pivot"); + GDVIRTUAL_BIND(_commit_pivot, "restore", "cancel"); + GDVIRTUAL_BIND(_get_handle_value, "id", "secondary"); GDVIRTUAL_BIND(_begin_handle_action, "id", "secondary"); GDVIRTUAL_BIND(_set_handle, "id", "secondary", "point"); @@ -560,6 +659,8 @@ EditorCanvasItemGizmo::EditorCanvasItemGizmo() { selected = false; canvas_item = nullptr; gizmo_plugin = nullptr; + use_boundary_handle = false; + use_pivot_handle = false; } EditorCanvasItemGizmo::~EditorCanvasItemGizmo() { @@ -588,6 +689,32 @@ int EditorCanvasItemGizmoPlugin::get_priority() const { return 0; } +Transform2D EditorCanvasItemGizmoPlugin::calculate_transform(const CanvasItem *p_canvas_item, const Rect2 &p_before, const Rect2 &p_after) { + ERR_FAIL_NULL_V(p_canvas_item, Transform2D()); + + 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; + + Transform2D local_xf = p_canvas_item->get_transform(); + Transform2D postxf; + postxf.set_rotation_scale_and_skew(local_xf.get_rotation(), local_xf.get_scale(), local_xf.get_skew()); + new_pos = postxf.xform(new_pos); + + return Transform2D().translated(new_pos).scaled(new_scale); +} + 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); @@ -607,6 +734,8 @@ Ref EditorCanvasItemGizmoPlugin::get_gizmo(CanvasItem *p_ } void EditorCanvasItemGizmoPlugin::_bind_methods() { + ClassDB::bind_static_method("EditorCanvasItemGizmoPlugin", D_METHOD("calculate_transform", "canvas_item", "before", "after"), &EditorCanvasItemGizmoPlugin::calculate_transform); + GDVIRTUAL_BIND(_has_gizmo, "for_canvas_item"); GDVIRTUAL_BIND(_create_gizmo, "for_canvas_item"); @@ -616,10 +745,20 @@ void EditorCanvasItemGizmoPlugin::_bind_methods() { GDVIRTUAL_BIND(_is_selectable_when_hidden); GDVIRTUAL_BIND(_redraw, "gizmo"); + + GDVIRTUAL_BIND(_begin_boundary_action, "gizmo"); + GDVIRTUAL_BIND(_set_boundary, "gizmo", "boundary"); + GDVIRTUAL_BIND(_get_boundary, "gizmo"); + GDVIRTUAL_BIND(_commit_boundary, "gizmo", "restore", "cancel"); + + GDVIRTUAL_BIND(_begin_pivot_action, "gizmo"); + GDVIRTUAL_BIND(_set_pivot, "gizmo", "pivot"); + GDVIRTUAL_BIND(_get_pivot, "gizmo"); + GDVIRTUAL_BIND(_commit_pivot, "gizmo", "restore", "cancel"); + 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"); @@ -671,6 +810,153 @@ void EditorCanvasItemGizmoPlugin::redraw(EditorCanvasItemGizmo *p_gizmo) { GDVIRTUAL_CALL(_redraw, p_gizmo); } +bool EditorCanvasItemGizmoPlugin::has_boundary(const EditorCanvasItemGizmo *p_gizmo) const { + ERR_FAIL_NULL_V(p_gizmo, false); + // we have a boundary if the user overrides _get_boundary, _set_boundary and _commit_boundary + bool get_boundary_overridden = GDVIRTUAL_IS_OVERRIDDEN(_get_boundary); + bool set_boundary_overridden = GDVIRTUAL_IS_OVERRIDDEN(_set_boundary); + bool commit_boundary_overridden = GDVIRTUAL_IS_OVERRIDDEN(_commit_boundary); + + if (get_boundary_overridden && set_boundary_overridden && commit_boundary_overridden) { + return true; + } + + // if any one is overridden we have some invalid state that we should warn the user about (it's either all or none) + if (get_boundary_overridden || set_boundary_overridden || commit_boundary_overridden) { + WARN_PRINT_ONCE("The gizmo plugin " + get_name() + " has overridden _get_boundary, _set_boundary or _commit_boundary, but not all of them. This is not allowed. Please override _get_boundary, _set_boundary and _commit_boundary to enable the boundary handles."); + return false; + } + + // fall back to _edit_use_rect, in case we apply gizmos to built-in nodes. + CanvasItem *canvas_item = p_gizmo->get_canvas_item(); + ERR_FAIL_NULL_V(canvas_item, false); + return canvas_item->_edit_use_rect(); +} + +void EditorCanvasItemGizmoPlugin::begin_boundary_action(const EditorCanvasItemGizmo *p_gizmo) { + ERR_FAIL_NULL(p_gizmo); + GDVIRTUAL_CALL(_begin_boundary_action, Ref(p_gizmo)); +} + +void EditorCanvasItemGizmoPlugin::set_boundary(const EditorCanvasItemGizmo *p_gizmo, const Rect2 &p_boundary) { + ERR_FAIL_NULL(p_gizmo); + if (GDVIRTUAL_IS_OVERRIDDEN(_set_boundary)) { + GDVIRTUAL_CALL(_set_boundary, Ref(p_gizmo), p_boundary); + return; + } + + // fall back to _edit_set_rect, in case we apply gizmos to built-in nodes. + CanvasItem *ci = p_gizmo->get_canvas_item(); + ERR_FAIL_NULL(ci); + ci->_edit_set_rect(p_boundary); +} + +Rect2 EditorCanvasItemGizmoPlugin::get_boundary(const EditorCanvasItemGizmo *p_gizmo) const { + ERR_FAIL_NULL_V(p_gizmo, Rect2()); + if (GDVIRTUAL_IS_OVERRIDDEN(_get_boundary)) { + Rect2 ret; + GDVIRTUAL_CALL(_get_boundary, Ref(p_gizmo), ret); + return ret; + } + + CanvasItem *ci = p_gizmo->get_canvas_item(); + ERR_FAIL_NULL_V(ci, Rect2()); + return ci->_edit_get_rect(); +} + +void EditorCanvasItemGizmoPlugin::commit_boundary(const EditorCanvasItemGizmo *p_gizmo, const Rect2 &p_restore, bool p_cancel) { + ERR_FAIL_NULL(p_gizmo); + if (GDVIRTUAL_IS_OVERRIDDEN(_commit_boundary)) { + GDVIRTUAL_CALL(_commit_boundary, Ref(p_gizmo), p_restore, p_cancel); + } + + CanvasItem *ci = p_gizmo->get_canvas_item(); + ERR_FAIL_NULL(ci); + if (p_cancel) { + ci->_edit_set_rect(p_restore); + } else { + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action("Change size"); + undo_redo->add_do_method(ci, "_edit_set_rect", ci->_edit_get_rect()); + undo_redo->add_undo_method(ci, "_edit_set_rect", p_restore); + undo_redo->commit_action(); + } +} + +bool EditorCanvasItemGizmoPlugin::has_pivot(const EditorCanvasItemGizmo *p_gizmo) const { + ERR_FAIL_NULL_V(p_gizmo, false); + // we have a pivot if the user overrides _get_pivot, _set_pivot and _commit_pivot + bool get_pivot_overridden = GDVIRTUAL_IS_OVERRIDDEN(_get_pivot); + bool set_pivot_overridden = GDVIRTUAL_IS_OVERRIDDEN(_set_pivot); + bool commit_pivot_overridden = GDVIRTUAL_IS_OVERRIDDEN(_commit_pivot); + + if (get_pivot_overridden && set_pivot_overridden && commit_pivot_overridden) { + return true; + } + + // if any one is overridden we have some invalid state that we should warn the user about (it's either all or none) + if (get_pivot_overridden || set_pivot_overridden || commit_pivot_overridden) { + WARN_PRINT_ONCE("The gizmo plugin " + get_name() + " has overridden _get_pivot, _set_pivot or _commit_pivot, but not all of them. This is not allowed. Please override _get_pivot, _set_pivot and _commit_pivot to enable the pivot handles."); + return false; + } + + // fall back to _edit_use_pivot, in case we apply gizmos to built-in nodes. + CanvasItem *canvas_item = p_gizmo->get_canvas_item(); + ERR_FAIL_NULL_V(canvas_item, false); + return canvas_item->_edit_use_pivot(); +} + +void EditorCanvasItemGizmoPlugin::begin_pivot_action(const EditorCanvasItemGizmo *p_gizmo) { + ERR_FAIL_NULL(p_gizmo); + GDVIRTUAL_CALL(_begin_pivot_action, Ref(p_gizmo)); +} + +void EditorCanvasItemGizmoPlugin::set_pivot(const EditorCanvasItemGizmo *p_gizmo, const Point2 &p_pivot) { + ERR_FAIL_NULL(p_gizmo); + if (GDVIRTUAL_IS_OVERRIDDEN(_set_pivot)) { + GDVIRTUAL_CALL(_set_pivot, Ref(p_gizmo), p_pivot); + return; + } + + // fall back to _edit_set_pivot, in case we apply gizmos to built-in nodes. + 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(); +} + +void EditorCanvasItemGizmoPlugin::commit_pivot(const EditorCanvasItemGizmo *p_gizmo, const Point2 &p_restore, bool p_cancel) { + ERR_FAIL_NULL(p_gizmo); + if (GDVIRTUAL_IS_OVERRIDDEN(_commit_pivot)) { + GDVIRTUAL_CALL(_commit_pivot, Ref(p_gizmo), p_restore, p_cancel); + return; + } + + CanvasItem *ci = p_gizmo->get_canvas_item(); + ERR_FAIL_NULL(ci); + if (p_cancel) { + ci->_edit_set_pivot(p_restore); + } else { + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action("Change pivot"); + undo_redo->add_do_method(ci, "_edit_set_pivot", ci->_edit_get_pivot()); + undo_redo->add_undo_method(ci, "_edit_set_pivot", p_restore); + undo_redo->commit_action(); + } +} + 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); diff --git a/editor/scene/2d/canvas_item_editor_gizmos.h b/editor/scene/2d/canvas_item_editor_gizmos.h index 7c0a1adf97c0..a7c2df9d72d9 100644 --- a/editor/scene/2d/canvas_item_editor_gizmos.h +++ b/editor/scene/2d/canvas_item_editor_gizmos.h @@ -57,6 +57,9 @@ class EditorCanvasItemGizmo : public CanvasItemGizmo { Vector secondary_handles; Vector secondary_handle_ids; + bool use_boundary_handle; + bool use_pivot_handle; + bool valid; bool hidden; Vector instances; @@ -70,6 +73,17 @@ class EditorCanvasItemGizmo : public CanvasItemGizmo { EditorCanvasItemGizmoPlugin *gizmo_plugin = nullptr; GDVIRTUAL0(_redraw) + + GDVIRTUAL0RC(Rect2, _get_boundary) + GDVIRTUAL0(_begin_boundary_action) + GDVIRTUAL1(_set_boundary, Rect2) + GDVIRTUAL2(_commit_boundary, Rect2, bool) + + GDVIRTUAL0RC(Vector2, _get_pivot) + GDVIRTUAL0(_begin_pivot_action) + GDVIRTUAL1(_set_pivot, Vector2) + GDVIRTUAL2(_commit_pivot, Vector2, bool) + GDVIRTUAL2RC(String, _get_handle_name, int, bool) GDVIRTUAL2RC(bool, _is_handle_highlighted, int, bool) GDVIRTUAL2RC(Variant, _get_handle_value, int, bool) @@ -95,6 +109,18 @@ class EditorCanvasItemGizmo : public CanvasItemGizmo { void add_handles(const Vector &p_handles, Ref p_texture, const Vector &p_ids = Vector(), bool p_secondary = false); + virtual bool has_boundary() const; + virtual Rect2 get_boundary() const; + virtual void begin_boundary_action(); + virtual void set_boundary(const Rect2 &p_rect); + virtual void commit_boundary(const Rect2 &p_restore, bool p_cancel = false); + + virtual bool has_pivot() const; + virtual Vector2 get_pivot() const; + virtual void begin_pivot_action(); + virtual void set_pivot(const Vector2 &p_pivot); + virtual void commit_pivot(const Vector2 &p_restore, bool p_cancel = false); + 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; @@ -161,10 +187,20 @@ class EditorCanvasItemGizmoPlugin : public Resource { GDVIRTUAL0RC(bool, _is_selectable_when_hidden) GDVIRTUAL1(_redraw, Ref) + + GDVIRTUAL1(_begin_boundary_action, Ref) + GDVIRTUAL2(_set_boundary, Ref, Rect2) + GDVIRTUAL1RC(Rect2, _get_boundary, Ref) + GDVIRTUAL3(_commit_boundary, Ref, Rect2, bool) + + GDVIRTUAL1(_begin_pivot_action, Ref) + GDVIRTUAL2(_set_pivot, Ref, Vector2) + GDVIRTUAL1RC(Vector2, _get_pivot, Ref) + GDVIRTUAL3(_commit_pivot, Ref, Vector2, bool) + 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) @@ -176,6 +212,8 @@ class EditorCanvasItemGizmoPlugin : public Resource { GDVIRTUAL4(_commit_subgizmos, Ref, Vector, TypedArray, bool) public: + static Transform2D calculate_transform(const CanvasItem *p_canvas_item, 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; @@ -183,6 +221,21 @@ class EditorCanvasItemGizmoPlugin : public Resource { virtual bool can_commit_handle_on_click() const; virtual void redraw(EditorCanvasItemGizmo *p_gizmo); + // boundary handle + virtual bool has_boundary(const EditorCanvasItemGizmo *p_gizmo) const; + virtual void begin_boundary_action(const EditorCanvasItemGizmo *p_gizmo); + virtual void set_boundary(const EditorCanvasItemGizmo *p_gizmo, const Rect2 &p_rect); + virtual Rect2 get_boundary(const EditorCanvasItemGizmo *p_gizmo) const; + virtual void commit_boundary(const EditorCanvasItemGizmo *p_gizmo, const Rect2 &p_restore, bool p_cancel = false); + + // pivot handle + virtual bool has_pivot(const EditorCanvasItemGizmo *p_gizmo) const; + virtual void begin_pivot_action(const EditorCanvasItemGizmo *p_gizmo); + virtual void set_pivot(const EditorCanvasItemGizmo *p_gizmo, const Vector2 &p_point); + virtual Vector2 get_pivot(const EditorCanvasItemGizmo *p_gizmo) const; + virtual void commit_pivot(const EditorCanvasItemGizmo *p_gizmo, const Vector2 &p_restore, bool p_cancel = false); + + // 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; @@ -190,6 +243,7 @@ class EditorCanvasItemGizmoPlugin : public Resource { 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; diff --git a/editor/scene/canvas_item_editor_plugin.cpp b/editor/scene/canvas_item_editor_plugin.cpp index 299717f6db1a..540a4be58ed2 100644 --- a/editor/scene/canvas_item_editor_plugin.cpp +++ b/editor/scene/canvas_item_editor_plugin.cpp @@ -273,6 +273,44 @@ class SnapDialog : public ConfirmationDialog { } }; +Ref _get_bounding_rect_gizmo(const CanvasItem *ci) { + ERR_FAIL_NULL_V(ci, Ref()); + for (Ref gizmo : ci->get_gizmos()) { + if (gizmo.is_null()) { + continue; + } + // highest priority gizmo wins + if (gizmo->has_boundary()) { + return gizmo; + } + } + + return Ref(); +} + +bool _get_pivot(const CanvasItem *ci, Vector2 &r_pivot) { + // Return the pivot of the canvas item, if any gizmo defines it. If no gizmo defines it, + // fall back to the legacy system (_edit_use_pivot/_edit_get_pivot). + ERR_FAIL_NULL_V(ci, false); + + Vector> gizmos = ci->get_gizmos(); + for (Ref gizmo : gizmos) { + if (gizmo.is_null()) { + continue; + } + // highest priority gizmo wins + if (gizmo->has_pivot()) { + r_pivot = gizmo->get_pivot(); + return true; + } + // TODO: GIZMOS: should we print a warning if multiple gizmos say they have pivots? + } + + // if no gizmo took it, fall back to the legacy system. + r_pivot = ci->_edit_get_pivot(); + return ci->_edit_use_pivot(); +} + bool CanvasItemEditor::_is_node_locked(const Node *p_node) const { return p_node->get_meta("_edit_lock_", false); } @@ -353,9 +391,12 @@ 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()); + Ref bounding_rect_gizmo = _get_bounding_rect_gizmo(ci); + bool use_rect = bounding_rect_gizmo.is_valid() || ci->_edit_use_rect(); + if (use_rect) { + Rect2 rect = bounding_rect_gizmo.is_valid() ? bounding_rect_gizmo->get_boundary() : ci->_edit_get_rect(); + 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()); @@ -392,9 +433,12 @@ 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()); + Ref bounding_rect_gizmo = _get_bounding_rect_gizmo(parent_ci); + bool use_rect = bounding_rect_gizmo.is_valid() || parent_ci->_edit_use_rect(); + if (use_rect) { + Rect2 rect = bounding_rect_gizmo.is_valid() ? bounding_rect_gizmo->get_boundary() : parent_ci->_edit_get_rect(); + 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); @@ -417,9 +461,12 @@ 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()); + Ref bounding_rect_gizmo = _get_bounding_rect_gizmo(p_self_canvas_item); + bool use_rect = bounding_rect_gizmo.is_valid() || p_self_canvas_item->_edit_use_rect(); + if (use_rect) { + Rect2 rect = bounding_rect_gizmo.is_valid() ? bounding_rect_gizmo->get_boundary() : p_self_canvas_item->_edit_get_rect(); + 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); } @@ -427,8 +474,11 @@ 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()); + Ref bounding_rect_gizmo = _get_bounding_rect_gizmo(p_self_canvas_item); + bool use_rect = bounding_rect_gizmo.is_valid() || p_self_canvas_item->_edit_use_rect(); + if (use_rect) { + Rect2 rect = bounding_rect_gizmo.is_valid() ? bounding_rect_gizmo->get_boundary() : p_self_canvas_item->_edit_get_rect(); + 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()); @@ -577,13 +627,15 @@ 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()); + Ref bounding_rect_gizmo = _get_bounding_rect_gizmo(ci); + Rect2 first_rect = bounding_rect_gizmo.is_valid() ? bounding_rect_gizmo->get_boundary() : ci->_edit_get_rect(); + Rect2 rect = Rect2(ci->get_global_transform_with_canvas().xform(first_rect.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(); + Ref bounding_rect_gizmo2 = _get_bounding_rect_gizmo(ci2); + Rect2 current_rect = bounding_rect_gizmo2.is_valid() ? bounding_rect_gizmo2->get_boundary() : ci2->_edit_get_rect(); 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)); @@ -618,7 +670,8 @@ 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(); + Ref bounding_rect_gizmo = _get_bounding_rect_gizmo(ci); + Rect2 rect = bounding_rect_gizmo.is_valid() ? bounding_rect_gizmo->get_boundary() : ci->_edit_get_rect(); if (r_first) { r_rect = Rect2(xform.xform(rect.get_center()), Size2()); r_first = false; @@ -804,7 +857,8 @@ void CanvasItemEditor::_find_canvas_items_in_rect(const Rect2 &p_rect, Node *p_n } xform *= ci->get_transform(); - // legacy way + // 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)) && @@ -1047,8 +1101,10 @@ void CanvasItemEditor::_save_drag_selection_state() { se->undo_state = ci->_edit_get_state(); se->pre_drag_xform = ci->get_screen_transform(); - if (ci->_edit_use_rect()) { - se->pre_drag_rect = ci->_edit_get_rect(); + Ref bounding_rect_gizmo = _get_bounding_rect_gizmo(ci); + Rect2 rect = bounding_rect_gizmo.is_valid() ? bounding_rect_gizmo->get_boundary() : ci->_edit_get_rect(); + if (rect != Rect2()) { + se->pre_drag_rect = rect; } else { se->pre_drag_rect = Rect2(); } @@ -1780,10 +1836,15 @@ bool CanvasItemEditor::_gui_input_rotate(const Ref &p_event) { CanvasItem *ci = drag_selection.front()->get(); 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 { - drag_rotation_center = ci->get_screen_transform().get_origin(); + Vector2 pivot; + bool use_pivot = _get_pivot(ci, pivot); + + if (use_pivot) { + drag_rotation_center = ci->get_screen_transform().xform(pivot); + } else { + drag_rotation_center = ci->get_screen_transform().get_origin(); + } } _save_drag_selection_state(); return true; @@ -2082,8 +2143,9 @@ 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(); + Ref bounding_rect_gizmo = _get_bounding_rect_gizmo(ci); + Rect2 rect = bounding_rect_gizmo.is_valid() ? bounding_rect_gizmo->get_boundary() : ci->_edit_get_rect(); + if (rect != Rect2() && _is_node_movable(ci)) { Transform2D xform = transform * ci->get_screen_transform(); const Vector2 endpoints[4] = { @@ -2150,7 +2212,8 @@ bool CanvasItemEditor::_gui_input_resize(const Ref &p_event) { bool uniform = m->is_shift_pressed(); bool symmetric = m->is_alt_pressed(); - Rect2 local_rect = ci->_edit_get_rect(); + Ref bounding_rect_gizmo = _get_bounding_rect_gizmo(ci); + Rect2 local_rect = bounding_rect_gizmo.is_valid() ? bounding_rect_gizmo->get_boundary() : ci->_edit_get_rect(); 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(); @@ -3055,7 +3118,8 @@ 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()) { + Ref bounding_rect_gizmo = _get_bounding_rect_gizmo(ci); + if (bounding_rect_gizmo.is_valid() || ci->_edit_use_rect()) { continue; } @@ -3156,12 +3220,14 @@ void CanvasItemEditor::_commit_drag() { switch (drag_type) { // Confirm the pivot move. case DRAG_PIVOT: { + Vector2 pivot; + _get_pivot(drag_selection.front()->get(), pivot); _commit_drag_selection_state( vformat( TTR("Set CanvasItem \"%s\" Pivot Offset to (%d, %d)"), drag_selection.front()->get()->get_name(), - drag_selection.front()->get()->_edit_get_pivot().x, - drag_selection.front()->get()->_edit_get_pivot().y)); + pivot.x, + pivot.y)); } break; // Confirm the node rotation. @@ -3214,12 +3280,14 @@ void CanvasItemEditor::_commit_drag() { Math::snapped(drag_selection.front()->get()->_edit_get_scale().y, 0.01))); } else { // Extends from Control. + Ref bounding_rect_gizmo = _get_bounding_rect_gizmo(drag_selection.front()->get()); + Rect2 rect = bounding_rect_gizmo.is_valid() ? bounding_rect_gizmo->get_boundary() : drag_selection.front()->get()->_edit_get_rect(); _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)); + rect.size.x, + rect.size.y)); } if (key_auto_insert_button->is_pressed()) { @@ -4105,6 +4173,9 @@ void CanvasItemEditor::_draw_selection() { CanvasItem *ci = Object::cast_to(E); CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data(ci); + Ref bounding_rect_gizmo = _get_bounding_rect_gizmo(ci); + bool use_bounding_rect = bounding_rect_gizmo.is_valid() || ci->_edit_use_rect(); + // Draw the previous position if we are dragging the node if (show_helpers && (drag_type == DRAG_MOVE || drag_type == DRAG_ROTATE || @@ -4113,7 +4184,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)), @@ -4133,8 +4204,9 @@ 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 = bounding_rect_gizmo.is_valid() ? bounding_rect_gizmo->get_boundary() : ci->_edit_get_rect(); + const Vector2 endpoints[4] = { xform.xform(rect.position), xform.xform(rect.position + Vector2(rect.size.x, 0)), @@ -4186,8 +4258,9 @@ 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 = bounding_rect_gizmo.is_valid() ? bounding_rect_gizmo->get_boundary() : ci->_edit_get_rect(); + const Vector2 endpoints[4] = { xform.xform(rect.position), xform.xform(rect.position + Vector2(rect.size.x, 0)), @@ -4437,7 +4510,9 @@ 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))) { + Ref bounding_rect_gizmo = _get_bounding_rect_gizmo(ci); + bool use_bounding_rect = bounding_rect_gizmo.is_valid() || ci->_edit_use_rect(); + if (show_position_gizmos && ci && !use_bounding_rect && (!editor_selection->is_selected(ci) || _is_node_locked(ci))) { Transform2D xform = transform * canvas_xform * parent_xform; // Draw the node's position @@ -4737,12 +4812,9 @@ void CanvasItemEditor::_notification(int p_what) { for (CanvasItem *ci : selection) { CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data(ci); - Rect2 rect; - if (ci->_edit_use_rect()) { - rect = ci->_edit_get_rect(); - } else { - rect = Rect2(); - } + Ref bounding_rect_gizmo = _get_bounding_rect_gizmo(ci); + Rect2 rect = bounding_rect_gizmo.is_valid() ? bounding_rect_gizmo->get_boundary() : ci->_edit_get_rect(); + Transform2D xform = ci->get_global_transform(); if (rect != se->prev_rect || xform != se->prev_xform) { @@ -5656,12 +5728,9 @@ void CanvasItemEditor::_focus_selection(int p_op) { if (!canvas_item_transform.is_finite()) { continue; } - Rect2 item_rect; - if (ci->_edit_use_rect()) { - item_rect = ci->_edit_get_rect(); - } else { - item_rect = Rect2(); - } + + Ref bounding_rect_gizmo = _get_bounding_rect_gizmo(ci); + Rect2 item_rect = bounding_rect_gizmo.is_valid() ? bounding_rect_gizmo->get_boundary() : ci->_edit_get_rect(); Vector2 pos = canvas_item_transform.get_origin(); const Vector2 scale = canvas_item_transform.get_scale(); const real_t angle = canvas_item_transform.get_rotation(); diff --git a/editor/scene/canvas_item_editor_plugin.h b/editor/scene/canvas_item_editor_plugin.h index d7a147776717..69e31b0818a1 100644 --- a/editor/scene/canvas_item_editor_plugin.h +++ b/editor/scene/canvas_item_editor_plugin.h @@ -67,6 +67,7 @@ class CanvasItemEditorSelectedItem : public Object { Transform2D pre_drag_xform; Rect2 pre_drag_rect; + // the gizmo that is currently supplying subgizmos Ref gizmo; HashMap subgizmos; // Key: Subgizmo ID, Value: Initial subgizmo transform. From 3c38e3babc59d5dc837f442bbe1c1b94f33b6a4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Thom=C3=A4?= Date: Mon, 19 Jan 2026 08:59:17 +0100 Subject: [PATCH 14/26] Refactor gizmo integration. Add undo/redo for scale/pivot. --- editor/scene/2d/canvas_item_editor_gizmos.cpp | 185 +++++------------ editor/scene/2d/canvas_item_editor_gizmos.h | 37 ++-- editor/scene/canvas_item_editor_plugin.cpp | 192 +++++++++--------- editor/scene/canvas_item_editor_plugin.h | 1 + 4 files changed, 171 insertions(+), 244 deletions(-) diff --git a/editor/scene/2d/canvas_item_editor_gizmos.cpp b/editor/scene/2d/canvas_item_editor_gizmos.cpp index 80cd5a0b26ab..49893413da3e 100644 --- a/editor/scene/2d/canvas_item_editor_gizmos.cpp +++ b/editor/scene/2d/canvas_item_editor_gizmos.cpp @@ -53,12 +53,6 @@ bool EditorCanvasItemGizmo::is_editable() const { return false; } -bool EditorCanvasItemGizmo::has_boundary() const { - ERR_FAIL_NULL_V(canvas_item, false); - ERR_FAIL_NULL_V(gizmo_plugin, false); - return GDVIRTUAL_IS_OVERRIDDEN(_get_boundary) || gizmo_plugin->has_boundary(this); -} - void EditorCanvasItemGizmo::clear() { ERR_FAIL_NULL(RenderingServer::get_singleton()); @@ -93,14 +87,14 @@ void EditorCanvasItemGizmo::redraw() { CanvasItemEditor::get_singleton()->update_transform_gizmo(); } } - -void EditorCanvasItemGizmo::begin_boundary_action() { - if (GDVIRTUAL_CALL(_begin_boundary_action)) { - return; +bool EditorCanvasItemGizmo::has_boundary() const { + bool ret = false; + if (GDVIRTUAL_CALL(_has_boundary, ret)) { + return ret; } - ERR_FAIL_NULL(gizmo_plugin); - gizmo_plugin->begin_boundary_action(this); + ERR_FAIL_NULL_V(gizmo_plugin, false); + return gizmo_plugin->has_boundary(this); } Rect2 EditorCanvasItemGizmo::get_boundary() const { @@ -109,6 +103,10 @@ Rect2 EditorCanvasItemGizmo::get_boundary() const { return ret; } + return _get_boundary(); +} + +Rect2 EditorCanvasItemGizmo::_get_boundary() const { ERR_FAIL_NULL_V(gizmo_plugin, Rect2()); return gizmo_plugin->get_boundary(this); } @@ -121,29 +119,13 @@ void EditorCanvasItemGizmo::set_boundary(const Rect2 &p_rect) { gizmo_plugin->set_boundary(this, p_rect); } -void EditorCanvasItemGizmo::commit_boundary(const Rect2 &p_restore, bool p_cancel) { - if (GDVIRTUAL_CALL(_commit_boundary, p_restore, p_cancel)) { - return; - } - - ERR_FAIL_NULL(gizmo_plugin); - gizmo_plugin->commit_boundary(this, p_restore, p_cancel); -} - bool EditorCanvasItemGizmo::has_pivot() const { - ERR_FAIL_NULL_V(canvas_item, false); - ERR_FAIL_NULL_V(gizmo_plugin, false); - - return GDVIRTUAL_IS_OVERRIDDEN(_get_pivot) || gizmo_plugin->has_pivot(this); -} - -void EditorCanvasItemGizmo::begin_pivot_action() { - if (GDVIRTUAL_CALL(_begin_pivot_action)) { - return; + bool ret = false; + if (GDVIRTUAL_CALL(_has_pivot, ret)) { + return ret; } - - ERR_FAIL_NULL(gizmo_plugin); - gizmo_plugin->begin_pivot_action(this); + ERR_FAIL_NULL_V(gizmo_plugin, false); + return gizmo_plugin->has_pivot(this); } Vector2 EditorCanvasItemGizmo::get_pivot() const { @@ -165,15 +147,6 @@ void EditorCanvasItemGizmo::set_pivot(const Vector2 &p_point) { gizmo_plugin->set_pivot(this, p_point); } -void EditorCanvasItemGizmo::commit_pivot(const Vector2 &p_restore, bool p_cancel) { - if (GDVIRTUAL_CALL(_commit_pivot, p_restore, p_cancel)) { - return; - } - - ERR_FAIL_NULL(gizmo_plugin); - gizmo_plugin->commit_pivot(this, p_restore, p_cancel); -} - String EditorCanvasItemGizmo::get_handle_name(int p_id, bool p_secondary) const { String ret; if (GDVIRTUAL_CALL(_get_handle_name, p_id, p_secondary, ret)) { @@ -632,14 +605,10 @@ void EditorCanvasItemGizmo::_bind_methods() { GDVIRTUAL_BIND(_is_handle_highlighted, "id", "secondary"); GDVIRTUAL_BIND(_get_boundary); - GDVIRTUAL_BIND(_begin_boundary_action); GDVIRTUAL_BIND(_set_boundary, "boundary"); - GDVIRTUAL_BIND(_commit_boundary, "restore", "cancel"); GDVIRTUAL_BIND(_get_pivot); - GDVIRTUAL_BIND(_begin_pivot_action); GDVIRTUAL_BIND(_set_pivot, "pivot"); - GDVIRTUAL_BIND(_commit_pivot, "restore", "cancel"); GDVIRTUAL_BIND(_get_handle_value, "id", "secondary"); GDVIRTUAL_BIND(_begin_handle_action, "id", "secondary"); @@ -689,7 +658,7 @@ int EditorCanvasItemGizmoPlugin::get_priority() const { return 0; } -Transform2D EditorCanvasItemGizmoPlugin::calculate_transform(const CanvasItem *p_canvas_item, const Rect2 &p_before, const Rect2 &p_after) { +Transform2D EditorCanvasItemGizmoPlugin::boundary_change_to_transform(const CanvasItem *p_canvas_item, const Rect2 &p_before, const Rect2 &p_after) { ERR_FAIL_NULL_V(p_canvas_item, Transform2D()); Vector2 zero_offset; @@ -712,7 +681,7 @@ Transform2D EditorCanvasItemGizmoPlugin::calculate_transform(const CanvasItem *p postxf.set_rotation_scale_and_skew(local_xf.get_rotation(), local_xf.get_scale(), local_xf.get_skew()); new_pos = postxf.xform(new_pos); - return Transform2D().translated(new_pos).scaled(new_scale); + return Transform2D().scaled(new_scale).translated(new_pos); } Ref EditorCanvasItemGizmoPlugin::get_gizmo(CanvasItem *p_canvas_item) { @@ -734,7 +703,9 @@ Ref EditorCanvasItemGizmoPlugin::get_gizmo(CanvasItem *p_ } void EditorCanvasItemGizmoPlugin::_bind_methods() { - ClassDB::bind_static_method("EditorCanvasItemGizmoPlugin", D_METHOD("calculate_transform", "canvas_item", "before", "after"), &EditorCanvasItemGizmoPlugin::calculate_transform); + ClassDB::bind_static_method("EditorCanvasItemGizmoPlugin", D_METHOD("boundary_change_to_transform", "canvas_item", "before", "after"), &EditorCanvasItemGizmoPlugin::boundary_change_to_transform); + + ClassDB::bind_method(D_METHOD("_set_boundary", "gizmo", "boundary"), &EditorCanvasItemGizmoPlugin::_set_boundary); GDVIRTUAL_BIND(_has_gizmo, "for_canvas_item"); GDVIRTUAL_BIND(_create_gizmo, "for_canvas_item"); @@ -746,15 +717,13 @@ void EditorCanvasItemGizmoPlugin::_bind_methods() { GDVIRTUAL_BIND(_redraw, "gizmo"); - GDVIRTUAL_BIND(_begin_boundary_action, "gizmo"); + GDVIRTUAL_BIND(_has_boundary, "gizmo"); GDVIRTUAL_BIND(_set_boundary, "gizmo", "boundary"); GDVIRTUAL_BIND(_get_boundary, "gizmo"); - GDVIRTUAL_BIND(_commit_boundary, "gizmo", "restore", "cancel"); - GDVIRTUAL_BIND(_begin_pivot_action, "gizmo"); + GDVIRTUAL_BIND(_has_pivot, "gizmo"); GDVIRTUAL_BIND(_set_pivot, "gizmo", "pivot"); GDVIRTUAL_BIND(_get_pivot, "gizmo"); - GDVIRTUAL_BIND(_commit_pivot, "gizmo", "restore", "cancel"); GDVIRTUAL_BIND(_get_handle_name, "gizmo", "handle_id", "secondary"); GDVIRTUAL_BIND(_is_handle_highlighted, "gizmo", "handle_id", "secondary"); @@ -812,40 +781,31 @@ void EditorCanvasItemGizmoPlugin::redraw(EditorCanvasItemGizmo *p_gizmo) { bool EditorCanvasItemGizmoPlugin::has_boundary(const EditorCanvasItemGizmo *p_gizmo) const { ERR_FAIL_NULL_V(p_gizmo, false); - // we have a boundary if the user overrides _get_boundary, _set_boundary and _commit_boundary - bool get_boundary_overridden = GDVIRTUAL_IS_OVERRIDDEN(_get_boundary); - bool set_boundary_overridden = GDVIRTUAL_IS_OVERRIDDEN(_set_boundary); - bool commit_boundary_overridden = GDVIRTUAL_IS_OVERRIDDEN(_commit_boundary); - - if (get_boundary_overridden && set_boundary_overridden && commit_boundary_overridden) { - return true; - } - - // if any one is overridden we have some invalid state that we should warn the user about (it's either all or none) - if (get_boundary_overridden || set_boundary_overridden || commit_boundary_overridden) { - WARN_PRINT_ONCE("The gizmo plugin " + get_name() + " has overridden _get_boundary, _set_boundary or _commit_boundary, but not all of them. This is not allowed. Please override _get_boundary, _set_boundary and _commit_boundary to enable the boundary handles."); - return false; + bool ret = false; + if (GDVIRTUAL_CALL(_has_boundary, Ref(p_gizmo), ret)) { + return ret; } + return _has_boundary(p_gizmo); +} - // fall back to _edit_use_rect, in case we apply gizmos to built-in nodes. +bool EditorCanvasItemGizmoPlugin::_has_boundary(const EditorCanvasItemGizmo *p_gizmo) const { + ERR_FAIL_NULL_V(p_gizmo, false); CanvasItem *canvas_item = p_gizmo->get_canvas_item(); ERR_FAIL_NULL_V(canvas_item, false); return canvas_item->_edit_use_rect(); } -void EditorCanvasItemGizmoPlugin::begin_boundary_action(const EditorCanvasItemGizmo *p_gizmo) { - ERR_FAIL_NULL(p_gizmo); - GDVIRTUAL_CALL(_begin_boundary_action, Ref(p_gizmo)); -} - void EditorCanvasItemGizmoPlugin::set_boundary(const EditorCanvasItemGizmo *p_gizmo, const Rect2 &p_boundary) { ERR_FAIL_NULL(p_gizmo); if (GDVIRTUAL_IS_OVERRIDDEN(_set_boundary)) { GDVIRTUAL_CALL(_set_boundary, Ref(p_gizmo), p_boundary); return; } + _set_boundary(p_gizmo, p_boundary); +} - // fall back to _edit_set_rect, in case we apply gizmos to built-in nodes. +void EditorCanvasItemGizmoPlugin::_set_boundary(const EditorCanvasItemGizmo *p_gizmo, Rect2 p_boundary) { + ERR_FAIL_NULL(p_gizmo); CanvasItem *ci = p_gizmo->get_canvas_item(); ERR_FAIL_NULL(ci); ci->_edit_set_rect(p_boundary); @@ -858,67 +818,44 @@ Rect2 EditorCanvasItemGizmoPlugin::get_boundary(const EditorCanvasItemGizmo *p_g GDVIRTUAL_CALL(_get_boundary, Ref(p_gizmo), ret); return ret; } + return _get_boundary(p_gizmo); +} +Rect2 EditorCanvasItemGizmoPlugin::_get_boundary(const EditorCanvasItemGizmo *p_gizmo) const { + ERR_FAIL_NULL_V(p_gizmo, Rect2()); CanvasItem *ci = p_gizmo->get_canvas_item(); ERR_FAIL_NULL_V(ci, Rect2()); return ci->_edit_get_rect(); } -void EditorCanvasItemGizmoPlugin::commit_boundary(const EditorCanvasItemGizmo *p_gizmo, const Rect2 &p_restore, bool p_cancel) { - ERR_FAIL_NULL(p_gizmo); - if (GDVIRTUAL_IS_OVERRIDDEN(_commit_boundary)) { - GDVIRTUAL_CALL(_commit_boundary, Ref(p_gizmo), p_restore, p_cancel); - } - - CanvasItem *ci = p_gizmo->get_canvas_item(); - ERR_FAIL_NULL(ci); - if (p_cancel) { - ci->_edit_set_rect(p_restore); - } else { - EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); - undo_redo->create_action("Change size"); - undo_redo->add_do_method(ci, "_edit_set_rect", ci->_edit_get_rect()); - undo_redo->add_undo_method(ci, "_edit_set_rect", p_restore); - undo_redo->commit_action(); - } -} - bool EditorCanvasItemGizmoPlugin::has_pivot(const EditorCanvasItemGizmo *p_gizmo) const { ERR_FAIL_NULL_V(p_gizmo, false); - // we have a pivot if the user overrides _get_pivot, _set_pivot and _commit_pivot - bool get_pivot_overridden = GDVIRTUAL_IS_OVERRIDDEN(_get_pivot); - bool set_pivot_overridden = GDVIRTUAL_IS_OVERRIDDEN(_set_pivot); - bool commit_pivot_overridden = GDVIRTUAL_IS_OVERRIDDEN(_commit_pivot); - - if (get_pivot_overridden && set_pivot_overridden && commit_pivot_overridden) { - return true; - } - - // if any one is overridden we have some invalid state that we should warn the user about (it's either all or none) - if (get_pivot_overridden || set_pivot_overridden || commit_pivot_overridden) { - WARN_PRINT_ONCE("The gizmo plugin " + get_name() + " has overridden _get_pivot, _set_pivot or _commit_pivot, but not all of them. This is not allowed. Please override _get_pivot, _set_pivot and _commit_pivot to enable the pivot handles."); - return false; + if (GDVIRTUAL_IS_OVERRIDDEN(_has_pivot)) { + bool ret = false; + GDVIRTUAL_CALL(_has_pivot, Ref(p_gizmo), ret); + return ret; } + return _has_pivot(p_gizmo); +} - // fall back to _edit_use_pivot, in case we apply gizmos to built-in nodes. +bool EditorCanvasItemGizmoPlugin::_has_pivot(const EditorCanvasItemGizmo *p_gizmo) const { + ERR_FAIL_NULL_V(p_gizmo, false); CanvasItem *canvas_item = p_gizmo->get_canvas_item(); ERR_FAIL_NULL_V(canvas_item, false); return canvas_item->_edit_use_pivot(); } -void EditorCanvasItemGizmoPlugin::begin_pivot_action(const EditorCanvasItemGizmo *p_gizmo) { - ERR_FAIL_NULL(p_gizmo); - GDVIRTUAL_CALL(_begin_pivot_action, Ref(p_gizmo)); -} - void EditorCanvasItemGizmoPlugin::set_pivot(const EditorCanvasItemGizmo *p_gizmo, const Point2 &p_pivot) { ERR_FAIL_NULL(p_gizmo); if (GDVIRTUAL_IS_OVERRIDDEN(_set_pivot)) { GDVIRTUAL_CALL(_set_pivot, Ref(p_gizmo), p_pivot); return; } + _set_pivot(p_gizmo, p_pivot); +} - // fall back to _edit_set_pivot, in case we apply gizmos to built-in nodes. +void EditorCanvasItemGizmoPlugin::_set_pivot(const EditorCanvasItemGizmo *p_gizmo, const Vector2 &p_pivot) { + ERR_FAIL_NULL(p_gizmo); CanvasItem *ci = p_gizmo->get_canvas_item(); ERR_FAIL_NULL(ci); ci->_edit_set_pivot(p_pivot); @@ -931,32 +868,16 @@ Point2 EditorCanvasItemGizmoPlugin::get_pivot(const EditorCanvasItemGizmo *p_giz GDVIRTUAL_CALL(_get_pivot, Ref(p_gizmo), ret); return ret; } + return _get_pivot(p_gizmo); +} +Point2 EditorCanvasItemGizmoPlugin::_get_pivot(const EditorCanvasItemGizmo *p_gizmo) const { + ERR_FAIL_NULL_V(p_gizmo, Point2()); CanvasItem *ci = p_gizmo->get_canvas_item(); ERR_FAIL_NULL_V(ci, Point2()); return ci->_edit_get_pivot(); } -void EditorCanvasItemGizmoPlugin::commit_pivot(const EditorCanvasItemGizmo *p_gizmo, const Point2 &p_restore, bool p_cancel) { - ERR_FAIL_NULL(p_gizmo); - if (GDVIRTUAL_IS_OVERRIDDEN(_commit_pivot)) { - GDVIRTUAL_CALL(_commit_pivot, Ref(p_gizmo), p_restore, p_cancel); - return; - } - - CanvasItem *ci = p_gizmo->get_canvas_item(); - ERR_FAIL_NULL(ci); - if (p_cancel) { - ci->_edit_set_pivot(p_restore); - } else { - EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); - undo_redo->create_action("Change pivot"); - undo_redo->add_do_method(ci, "_edit_set_pivot", ci->_edit_get_pivot()); - undo_redo->add_undo_method(ci, "_edit_set_pivot", p_restore); - undo_redo->commit_action(); - } -} - 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); diff --git a/editor/scene/2d/canvas_item_editor_gizmos.h b/editor/scene/2d/canvas_item_editor_gizmos.h index a7c2df9d72d9..e91b258465ed 100644 --- a/editor/scene/2d/canvas_item_editor_gizmos.h +++ b/editor/scene/2d/canvas_item_editor_gizmos.h @@ -74,15 +74,13 @@ class EditorCanvasItemGizmo : public CanvasItemGizmo { GDVIRTUAL0(_redraw) + GDVIRTUAL0RC(bool, _has_boundary) GDVIRTUAL0RC(Rect2, _get_boundary) - GDVIRTUAL0(_begin_boundary_action) GDVIRTUAL1(_set_boundary, Rect2) - GDVIRTUAL2(_commit_boundary, Rect2, bool) + GDVIRTUAL0RC(bool, _has_pivot) GDVIRTUAL0RC(Vector2, _get_pivot) - GDVIRTUAL0(_begin_pivot_action) GDVIRTUAL1(_set_pivot, Vector2) - GDVIRTUAL2(_commit_pivot, Vector2, bool) GDVIRTUAL2RC(String, _get_handle_name, int, bool) GDVIRTUAL2RC(bool, _is_handle_highlighted, int, bool) @@ -111,15 +109,12 @@ class EditorCanvasItemGizmo : public CanvasItemGizmo { virtual bool has_boundary() const; virtual Rect2 get_boundary() const; - virtual void begin_boundary_action(); + virtual Rect2 _get_boundary() const; virtual void set_boundary(const Rect2 &p_rect); - virtual void commit_boundary(const Rect2 &p_restore, bool p_cancel = false); virtual bool has_pivot() const; virtual Vector2 get_pivot() const; - virtual void begin_pivot_action(); virtual void set_pivot(const Vector2 &p_pivot); - virtual void commit_pivot(const Vector2 &p_restore, bool p_cancel = false); virtual bool is_handle_highlighted(int p_id, bool p_secondary) const; virtual String get_handle_name(int p_id, bool p_secondary) const; @@ -188,15 +183,13 @@ class EditorCanvasItemGizmoPlugin : public Resource { GDVIRTUAL1(_redraw, Ref) - GDVIRTUAL1(_begin_boundary_action, Ref) + GDVIRTUAL1RC(bool, _has_boundary, Ref) GDVIRTUAL2(_set_boundary, Ref, Rect2) GDVIRTUAL1RC(Rect2, _get_boundary, Ref) - GDVIRTUAL3(_commit_boundary, Ref, Rect2, bool) - GDVIRTUAL1(_begin_pivot_action, Ref) + GDVIRTUAL1RC(bool, _has_pivot, Ref) GDVIRTUAL2(_set_pivot, Ref, Vector2) GDVIRTUAL1RC(Vector2, _get_pivot, Ref) - GDVIRTUAL3(_commit_pivot, Ref, Vector2, bool) GDVIRTUAL3RC(String, _get_handle_name, Ref, int, bool) GDVIRTUAL3RC(bool, _is_handle_highlighted, Ref, int, bool) @@ -212,7 +205,7 @@ class EditorCanvasItemGizmoPlugin : public Resource { GDVIRTUAL4(_commit_subgizmos, Ref, Vector, TypedArray, bool) public: - static Transform2D calculate_transform(const CanvasItem *p_canvas_item, const Rect2 &p_before, const Rect2 &p_after); + static Transform2D boundary_change_to_transform(const CanvasItem *p_canvas_item, const Rect2 &p_before, const Rect2 &p_after); virtual String get_gizmo_name() const; virtual int get_priority() const; @@ -223,19 +216,27 @@ class EditorCanvasItemGizmoPlugin : public Resource { virtual void redraw(EditorCanvasItemGizmo *p_gizmo); // boundary handle virtual bool has_boundary(const EditorCanvasItemGizmo *p_gizmo) const; - virtual void begin_boundary_action(const EditorCanvasItemGizmo *p_gizmo); virtual void set_boundary(const EditorCanvasItemGizmo *p_gizmo, const Rect2 &p_rect); virtual Rect2 get_boundary(const EditorCanvasItemGizmo *p_gizmo) const; - virtual void commit_boundary(const EditorCanvasItemGizmo *p_gizmo, const Rect2 &p_restore, bool p_cancel = false); + +private: + virtual bool _has_boundary(const EditorCanvasItemGizmo *p_gizmo) const; + virtual void _set_boundary(const EditorCanvasItemGizmo *p_gizmo, Rect2 p_rect); + virtual Rect2 _get_boundary(const EditorCanvasItemGizmo *p_gizmo) const; // pivot handle +public: virtual bool has_pivot(const EditorCanvasItemGizmo *p_gizmo) const; - virtual void begin_pivot_action(const EditorCanvasItemGizmo *p_gizmo); - virtual void set_pivot(const EditorCanvasItemGizmo *p_gizmo, const Vector2 &p_point); virtual Vector2 get_pivot(const EditorCanvasItemGizmo *p_gizmo) const; - virtual void commit_pivot(const EditorCanvasItemGizmo *p_gizmo, const Vector2 &p_restore, bool p_cancel = false); + virtual void set_pivot(const EditorCanvasItemGizmo *p_gizmo, const Vector2 &p_point); + +private: + 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); // extra handles +public: 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; diff --git a/editor/scene/canvas_item_editor_plugin.cpp b/editor/scene/canvas_item_editor_plugin.cpp index 540a4be58ed2..c449e345ceeb 100644 --- a/editor/scene/canvas_item_editor_plugin.cpp +++ b/editor/scene/canvas_item_editor_plugin.cpp @@ -273,42 +273,67 @@ class SnapDialog : public ConfirmationDialog { } }; -Ref _get_bounding_rect_gizmo(const CanvasItem *ci) { - ERR_FAIL_NULL_V(ci, Ref()); - for (Ref gizmo : ci->get_gizmos()) { - if (gizmo.is_null()) { - continue; +// 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->get_boundary(); } - // highest priority gizmo wins - if (gizmo->has_boundary()) { - return gizmo; + } + 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->has_boundary(); } } + return p_canvas_item->_edit_use_rect(); +} - return Ref(); +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->set_boundary(p_rect); + return; + } + } + p_canvas_item->_edit_set_rect(p_rect); } -bool _get_pivot(const CanvasItem *ci, Vector2 &r_pivot) { - // Return the pivot of the canvas item, if any gizmo defines it. If no gizmo defines it, - // fall back to the legacy system (_edit_use_pivot/_edit_get_pivot). - ERR_FAIL_NULL_V(ci, false); +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(); +} - Vector> gizmos = ci->get_gizmos(); - for (Ref gizmo : gizmos) { - if (gizmo.is_null()) { - continue; +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; } - // highest priority gizmo wins - if (gizmo->has_pivot()) { - r_pivot = gizmo->get_pivot(); + } + 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; } - // TODO: GIZMOS: should we print a warning if multiple gizmos say they have pivots? } - - // if no gizmo took it, fall back to the legacy system. - r_pivot = ci->_edit_get_pivot(); - return ci->_edit_use_pivot(); + return p_canvas_item->_edit_use_pivot(); } bool CanvasItemEditor::_is_node_locked(const Node *p_node) const { @@ -391,10 +416,8 @@ 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) { - Ref bounding_rect_gizmo = _get_bounding_rect_gizmo(ci); - bool use_rect = bounding_rect_gizmo.is_valid() || ci->_edit_use_rect(); - if (use_rect) { - Rect2 rect = bounding_rect_gizmo.is_valid() ? bounding_rect_gizmo->get_boundary() : ci->_edit_get_rect(); + 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()); @@ -433,10 +456,8 @@ 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())) { - Ref bounding_rect_gizmo = _get_bounding_rect_gizmo(parent_ci); - bool use_rect = bounding_rect_gizmo.is_valid() || parent_ci->_edit_use_rect(); - if (use_rect) { - Rect2 rect = bounding_rect_gizmo.is_valid() ? bounding_rect_gizmo->get_boundary() : parent_ci->_edit_get_rect(); + 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); @@ -461,10 +482,8 @@ 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)) { - Ref bounding_rect_gizmo = _get_bounding_rect_gizmo(p_self_canvas_item); - bool use_rect = bounding_rect_gizmo.is_valid() || p_self_canvas_item->_edit_use_rect(); - if (use_rect) { - Rect2 rect = bounding_rect_gizmo.is_valid() ? bounding_rect_gizmo->get_boundary() : p_self_canvas_item->_edit_get_rect(); + 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); @@ -474,10 +493,8 @@ 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)) { - Ref bounding_rect_gizmo = _get_bounding_rect_gizmo(p_self_canvas_item); - bool use_rect = bounding_rect_gizmo.is_valid() || p_self_canvas_item->_edit_use_rect(); - if (use_rect) { - Rect2 rect = bounding_rect_gizmo.is_valid() ? bounding_rect_gizmo->get_boundary() : p_self_canvas_item->_edit_get_rect(); + 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 { @@ -627,15 +644,13 @@ Rect2 CanvasItemEditor::_get_encompassing_rect_from_list(const Listget(); - Ref bounding_rect_gizmo = _get_bounding_rect_gizmo(ci); - Rect2 first_rect = bounding_rect_gizmo.is_valid() ? bounding_rect_gizmo->get_boundary() : ci->_edit_get_rect(); - Rect2 rect = Rect2(ci->get_global_transform_with_canvas().xform(first_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(); - Ref bounding_rect_gizmo2 = _get_bounding_rect_gizmo(ci2); - Rect2 current_rect = bounding_rect_gizmo2.is_valid() ? bounding_rect_gizmo2->get_boundary() : 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)); @@ -670,8 +685,7 @@ void CanvasItemEditor::_expand_encompassing_rect_using_children(Rect2 &r_rect, c xform *= p_parent_xform; } xform *= ci->get_transform(); - Ref bounding_rect_gizmo = _get_bounding_rect_gizmo(ci); - Rect2 rect = bounding_rect_gizmo.is_valid() ? bounding_rect_gizmo->get_boundary() : 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; @@ -1101,8 +1115,7 @@ void CanvasItemEditor::_save_drag_selection_state() { se->undo_state = ci->_edit_get_state(); se->pre_drag_xform = ci->get_screen_transform(); - Ref bounding_rect_gizmo = _get_bounding_rect_gizmo(ci); - Rect2 rect = bounding_rect_gizmo.is_valid() ? bounding_rect_gizmo->get_boundary() : ci->_edit_get_rect(); + Rect2 rect = _get_edit_rect(ci); if (rect != Rect2()) { se->pre_drag_rect = rect; } else { @@ -1168,7 +1181,9 @@ void CanvasItemEditor::_commit_drag_selection_state(const String &action_name) { // if we are here, no subgizmo was selected, and we do the regular canvas item commit for (CanvasItem *ci : drag_selection) { - Dictionary old_state = editor_selection->get_node_editor_data(ci)->undo_state; + CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data(ci); + + Dictionary old_state = se->undo_state; Dictionary new_state = ci->_edit_get_state(); if (old_state.hash() != new_state.hash()) { @@ -1713,7 +1728,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); } } @@ -1738,7 +1753,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; @@ -1759,7 +1774,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; } @@ -1836,15 +1851,10 @@ bool CanvasItemEditor::_gui_input_rotate(const Ref &p_event) { CanvasItem *ci = drag_selection.front()->get(); if (!Math::is_inf(temp_pivot.x) || !Math::is_inf(temp_pivot.y)) { drag_rotation_center = temp_pivot; + } else if (_use_edit_pivot(ci)) { + drag_rotation_center = ci->get_screen_transform().xform(_get_edit_pivot(ci)); } else { - Vector2 pivot; - bool use_pivot = _get_pivot(ci, pivot); - - if (use_pivot) { - drag_rotation_center = ci->get_screen_transform().xform(pivot); - } else { - drag_rotation_center = ci->get_screen_transform().get_origin(); - } + drag_rotation_center = ci->get_screen_transform().get_origin(); } _save_drag_selection_state(); return true; @@ -2143,9 +2153,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(); - Ref bounding_rect_gizmo = _get_bounding_rect_gizmo(ci); - Rect2 rect = bounding_rect_gizmo.is_valid() ? bounding_rect_gizmo->get_boundary() : ci->_edit_get_rect(); - if (rect != Rect2() && _is_node_movable(ci)) { + 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] = { @@ -2212,8 +2221,7 @@ bool CanvasItemEditor::_gui_input_resize(const Ref &p_event) { bool uniform = m->is_shift_pressed(); bool symmetric = m->is_alt_pressed(); - Ref bounding_rect_gizmo = _get_bounding_rect_gizmo(ci); - Rect2 local_rect = bounding_rect_gizmo.is_valid() ? bounding_rect_gizmo->get_boundary() : 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(); @@ -2284,7 +2292,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; } @@ -3118,8 +3126,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; - Ref bounding_rect_gizmo = _get_bounding_rect_gizmo(ci); - if (bounding_rect_gizmo.is_valid() || ci->_edit_use_rect()) { + if (_use_edit_rect(ci)) { continue; } @@ -3220,14 +3227,12 @@ void CanvasItemEditor::_commit_drag() { switch (drag_type) { // Confirm the pivot move. case DRAG_PIVOT: { - Vector2 pivot; - _get_pivot(drag_selection.front()->get(), pivot); _commit_drag_selection_state( vformat( TTR("Set CanvasItem \"%s\" Pivot Offset to (%d, %d)"), drag_selection.front()->get()->get_name(), - pivot.x, - pivot.y)); + drag_selection.front()->get()->_edit_get_pivot().x, + drag_selection.front()->get()->_edit_get_pivot().y)); } break; // Confirm the node rotation. @@ -3280,14 +3285,12 @@ void CanvasItemEditor::_commit_drag() { Math::snapped(drag_selection.front()->get()->_edit_get_scale().y, 0.01))); } else { // Extends from Control. - Ref bounding_rect_gizmo = _get_bounding_rect_gizmo(drag_selection.front()->get()); - Rect2 rect = bounding_rect_gizmo.is_valid() ? bounding_rect_gizmo->get_boundary() : drag_selection.front()->get()->_edit_get_rect(); _commit_drag_selection_state( vformat( TTR("Resize Control \"%s\" to (%d, %d)"), drag_selection.front()->get()->get_name(), - rect.size.x, - rect.size.y)); + drag_selection.front()->get()->_edit_get_rect().size.x, + drag_selection.front()->get()->_edit_get_rect().size.y)); } if (key_auto_insert_button->is_pressed()) { @@ -4173,8 +4176,7 @@ void CanvasItemEditor::_draw_selection() { CanvasItem *ci = Object::cast_to(E); CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data(ci); - Ref bounding_rect_gizmo = _get_bounding_rect_gizmo(ci); - bool use_bounding_rect = bounding_rect_gizmo.is_valid() || ci->_edit_use_rect(); + bool use_bounding_rect = _use_edit_rect(ci); // Draw the previous position if we are dragging the node if (show_helpers && @@ -4205,8 +4207,7 @@ void CanvasItemEditor::_draw_selection() { // Draw the selected items position / surrounding boxes if (use_bounding_rect) { - Rect2 rect = bounding_rect_gizmo.is_valid() ? bounding_rect_gizmo->get_boundary() : ci->_edit_get_rect(); - + Rect2 rect = _get_edit_rect(ci); const Vector2 endpoints[4] = { xform.xform(rect.position), xform.xform(rect.position + Vector2(rect.size.x, 0)), @@ -4234,7 +4235,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; @@ -4259,8 +4260,7 @@ void CanvasItemEditor::_draw_selection() { // Draw the resize handles if (tool == TOOL_SELECT && use_bounding_rect && _is_node_movable(ci)) { - Rect2 rect = bounding_rect_gizmo.is_valid() ? bounding_rect_gizmo->get_boundary() : ci->_edit_get_rect(); - + Rect2 rect = _get_edit_rect(ci); const Vector2 endpoints[4] = { xform.xform(rect.position), xform.xform(rect.position + Vector2(rect.size.x, 0)), @@ -4510,9 +4510,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); } - Ref bounding_rect_gizmo = _get_bounding_rect_gizmo(ci); - bool use_bounding_rect = bounding_rect_gizmo.is_valid() || ci->_edit_use_rect(); - if (show_position_gizmos && ci && !use_bounding_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 @@ -4812,9 +4810,12 @@ void CanvasItemEditor::_notification(int p_what) { for (CanvasItem *ci : selection) { CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data(ci); - Ref bounding_rect_gizmo = _get_bounding_rect_gizmo(ci); - Rect2 rect = bounding_rect_gizmo.is_valid() ? bounding_rect_gizmo->get_boundary() : ci->_edit_get_rect(); - + Rect2 rect; + if (_use_edit_rect(ci)) { + rect = _get_edit_rect(ci); + } else { + rect = Rect2(); + } Transform2D xform = ci->get_global_transform(); if (rect != se->prev_rect || xform != se->prev_xform) { @@ -5728,9 +5729,12 @@ void CanvasItemEditor::_focus_selection(int p_op) { if (!canvas_item_transform.is_finite()) { continue; } - - Ref bounding_rect_gizmo = _get_bounding_rect_gizmo(ci); - Rect2 item_rect = bounding_rect_gizmo.is_valid() ? bounding_rect_gizmo->get_boundary() : ci->_edit_get_rect(); + Rect2 item_rect; + if (_use_edit_rect(ci)) { + item_rect = _get_edit_rect(ci); + } else { + item_rect = Rect2(); + } Vector2 pos = canvas_item_transform.get_origin(); const Vector2 scale = canvas_item_transform.get_scale(); const real_t angle = canvas_item_transform.get_rotation(); diff --git a/editor/scene/canvas_item_editor_plugin.h b/editor/scene/canvas_item_editor_plugin.h index 69e31b0818a1..72f3a7222053 100644 --- a/editor/scene/canvas_item_editor_plugin.h +++ b/editor/scene/canvas_item_editor_plugin.h @@ -70,6 +70,7 @@ class CanvasItemEditorSelectedItem : public Object { // 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; }; From b703d294904a8d9fad1ca6fa33b736c399f38925 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Thom=C3=A4?= Date: Fri, 23 Jan 2026 09:16:53 +0100 Subject: [PATCH 15/26] Simplify code. --- editor/scene/2d/canvas_item_editor_gizmos.cpp | 351 ++++++++++-------- editor/scene/2d/canvas_item_editor_gizmos.h | 121 +++--- editor/scene/canvas_item_editor_plugin.cpp | 218 ++++++----- editor/scene/canvas_item_editor_plugin.h | 1 + 4 files changed, 390 insertions(+), 301 deletions(-) diff --git a/editor/scene/2d/canvas_item_editor_gizmos.cpp b/editor/scene/2d/canvas_item_editor_gizmos.cpp index 49893413da3e..dbad3ed085c1 100644 --- a/editor/scene/2d/canvas_item_editor_gizmos.cpp +++ b/editor/scene/2d/canvas_item_editor_gizmos.cpp @@ -76,10 +76,10 @@ void EditorCanvasItemGizmo::clear() { } void EditorCanvasItemGizmo::redraw() { + ERR_FAIL_NULL(gizmo_plugin); clear(); if (!GDVIRTUAL_CALL(_redraw)) { - ERR_FAIL_NULL(gizmo_plugin); gizmo_plugin->redraw(this); } @@ -87,175 +87,208 @@ void EditorCanvasItemGizmo::redraw() { CanvasItemEditor::get_singleton()->update_transform_gizmo(); } } -bool EditorCanvasItemGizmo::has_boundary() const { +bool EditorCanvasItemGizmo::_edit_use_rect() const { + ERR_FAIL_NULL_V(gizmo_plugin, false); bool ret = false; - if (GDVIRTUAL_CALL(_has_boundary, ret)) { + if (GDVIRTUAL_CALL(_edit_use_rect, ret)) { return ret; } - ERR_FAIL_NULL_V(gizmo_plugin, false); - return gizmo_plugin->has_boundary(this); + return gizmo_plugin->_edit_use_rect(this); } -Rect2 EditorCanvasItemGizmo::get_boundary() const { +Rect2 EditorCanvasItemGizmo::_edit_get_rect() const { + ERR_FAIL_NULL_V(gizmo_plugin, Rect2()); Rect2 ret; - if (GDVIRTUAL_CALL(_get_boundary, ret)) { + if (GDVIRTUAL_CALL(_edit_get_rect, ret)) { return ret; } - - return _get_boundary(); -} - -Rect2 EditorCanvasItemGizmo::_get_boundary() const { - ERR_FAIL_NULL_V(gizmo_plugin, Rect2()); - return gizmo_plugin->get_boundary(this); + return gizmo_plugin->_edit_get_rect(this); } -void EditorCanvasItemGizmo::set_boundary(const Rect2 &p_rect) { - if (GDVIRTUAL_CALL(_set_boundary, p_rect)) { +void EditorCanvasItemGizmo::_edit_set_rect(const Rect2 &p_rect) { + ERR_FAIL_NULL(gizmo_plugin); + if (GDVIRTUAL_CALL(_edit_set_rect, p_rect)) { return; } - ERR_FAIL_NULL(gizmo_plugin); - gizmo_plugin->set_boundary(this, p_rect); + gizmo_plugin->_edit_set_rect(this, p_rect); } -bool EditorCanvasItemGizmo::has_pivot() const { +bool EditorCanvasItemGizmo::_has_pivot() const { + ERR_FAIL_NULL_V(gizmo_plugin, false); bool ret = false; if (GDVIRTUAL_CALL(_has_pivot, ret)) { return ret; } - ERR_FAIL_NULL_V(gizmo_plugin, false); - return gizmo_plugin->has_pivot(this); + return gizmo_plugin->_has_pivot(this); } -Vector2 EditorCanvasItemGizmo::get_pivot() const { +Vector2 EditorCanvasItemGizmo::_get_pivot() const { + ERR_FAIL_NULL_V(gizmo_plugin, Vector2()); Vector2 ret; if (GDVIRTUAL_CALL(_get_pivot, ret)) { return ret; } - ERR_FAIL_NULL_V(gizmo_plugin, Vector2()); - return gizmo_plugin->get_pivot(this); + return gizmo_plugin->_get_pivot(this); } -void EditorCanvasItemGizmo::set_pivot(const Vector2 &p_point) { +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. + // TODO: GIZMOS: docs - this needs to be in the docs as its non-obvious behavior + 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); - gizmo_plugin->set_pivot(this, p_point); + + // 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. + // TODO: GIZMOS: docs - should probably state in the docs that the dictionary may contain additional items. + 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 { +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; } - ERR_FAIL_NULL_V(gizmo_plugin, ""); - return gizmo_plugin->get_handle_name(this, p_id, p_secondary); + return gizmo_plugin->_get_handle_name(this, p_id, p_secondary); } -bool EditorCanvasItemGizmo::is_handle_highlighted(int p_id, bool p_secondary) const { +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; } - ERR_FAIL_NULL_V(gizmo_plugin, false); - return gizmo_plugin->is_handle_highlighted(this, p_id, p_secondary); + return gizmo_plugin->_is_handle_highlighted(this, p_id, p_secondary); } -Variant EditorCanvasItemGizmo::get_handle_value(int p_id, bool p_secondary) const { +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; } - ERR_FAIL_NULL_V(gizmo_plugin, Variant()); - return gizmo_plugin->get_handle_value(this, p_id, p_secondary); + return gizmo_plugin->_get_handle_value(this, p_id, p_secondary); } -void EditorCanvasItemGizmo::begin_handle_action(int p_id, bool 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; } - ERR_FAIL_NULL(gizmo_plugin); - gizmo_plugin->begin_handle_action(this, p_id, p_secondary); + gizmo_plugin->_begin_handle_action(this, p_id, p_secondary); } -void EditorCanvasItemGizmo::set_handle(int p_id, bool p_secondary, const Point2 &p_point) { +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; } - ERR_FAIL_NULL(gizmo_plugin); - gizmo_plugin->set_handle(this, p_id, p_secondary, p_point); + 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) { +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; } - ERR_FAIL_NULL(gizmo_plugin); - gizmo_plugin->commit_handle(this, p_id, p_secondary, p_restore, p_cancel); + 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 { +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; } - ERR_FAIL_NULL_V(gizmo_plugin, -1); - return gizmo_plugin->subgizmos_intersect_point(this, p_point, p_max_distance); + return gizmo_plugin->_subgizmos_intersect_point(this, p_point, p_max_distance); } -Vector EditorCanvasItemGizmo::subgizmos_intersect_rect(const Rect2 &p_rect) const { +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; } - ERR_FAIL_NULL_V(gizmo_plugin, Vector()); - return gizmo_plugin->subgizmos_intersect_rect(this, p_rect); + return gizmo_plugin->_subgizmos_intersect_rect(this, p_rect); } -Transform2D EditorCanvasItemGizmo::get_subgizmo_transform(int p_id) const { +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; } - ERR_FAIL_NULL_V(gizmo_plugin, Transform2D()); - return gizmo_plugin->get_subgizmo_transform(this, p_id); + return gizmo_plugin->_get_subgizmo_transform(this, p_id); } -void EditorCanvasItemGizmo::set_subgizmo_transform(int p_id, const Transform2D &p_transform) { +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; } - ERR_FAIL_NULL(gizmo_plugin); - gizmo_plugin->set_subgizmo_transform(this, p_id, p_transform); + 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) { - TypedArray restore; - restore.resize(p_restore.size()); - for (int i = 0; i < p_restore.size(); i++) { - restore[i] = p_restore[i]; - } +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; + if (GDVIRTUAL_CALL(_commit_subgizmos, p_ids, restore, p_cancel)) { + return; + } } - ERR_FAIL_NULL(gizmo_plugin); - gizmo_plugin->commit_subgizmos(this, p_ids, p_restore, p_cancel); + gizmo_plugin->_commit_subgizmos(this, p_ids, p_restore, p_cancel); } void EditorCanvasItemGizmo::set_canvas_item(CanvasItem *p_canvas_item) { @@ -263,12 +296,12 @@ void EditorCanvasItemGizmo::set_canvas_item(CanvasItem *p_canvas_item) { canvas_item = p_canvas_item; } -void EditorCanvasItemGizmo::Instance::create_instance(CanvasItem *p_base, bool p_hidden) { +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()); - int layer = p_hidden ? 0 : (1 << CanvasItemEditorViewport::GIZMO_EDIT_LAYER); + int layer = p_visible ? (1 << CanvasItemEditorViewport::GIZMO_EDIT_LAYER) : 0; RS::get_singleton()->canvas_item_set_visibility_layer(instance, layer); } @@ -276,7 +309,7 @@ void EditorCanvasItemGizmo::add_circle(const Vector2 &p_pos, float p_radius, con ERR_FAIL_NULL(canvas_item); Instance ins; - ins.create_instance(canvas_item, hidden); + 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); } @@ -291,7 +324,7 @@ void EditorCanvasItemGizmo::add_polygon(const Vector &p_polygon, const } Instance ins; - ins.create_instance(canvas_item, hidden); + ins.create_instance(canvas_item, visible); RS::get_singleton()->canvas_item_add_polygon(ins.instance, p_polygon, colors); instances.push_back(ins); } @@ -306,7 +339,7 @@ void EditorCanvasItemGizmo::add_polyline(const Vector &p_points, const } Instance ins; - ins.create_instance(canvas_item, hidden); + ins.create_instance(canvas_item, visible); RS::get_singleton()->canvas_item_add_polyline(ins.instance, p_points, colors); instances.push_back(ins); } @@ -315,7 +348,7 @@ void EditorCanvasItemGizmo::add_rect(const Rect2 &p_rect, const Color &p_color) ERR_FAIL_NULL(canvas_item); Instance ins; - ins.create_instance(canvas_item, hidden); + ins.create_instance(canvas_item, visible); RS::get_singleton()->canvas_item_add_rect(ins.instance, p_rect, p_color); instances.push_back(ins); } @@ -338,7 +371,7 @@ void EditorCanvasItemGizmo::add_collision_polygon(const Vector &p_polyg } void EditorCanvasItemGizmo::add_handles(const Vector &p_handles, Ref p_texture, const Vector &p_ids, bool p_secondary) { - if (!is_selected() || !is_editable()) { + if (!_is_selected() || !is_editable()) { return; } @@ -377,11 +410,11 @@ void EditorCanvasItemGizmo::add_handles(const Vector &p_handles, Refis_selectable_when_hidden()) { + if (!visible && !gizmo_plugin->is_selectable_when_hidden()) { return false; } @@ -452,7 +485,7 @@ void EditorCanvasItemGizmo::handles_intersect_point(const Point2 &p_point, real_ ERR_FAIL_NULL(canvas_item); ERR_FAIL_COND(!valid); - if (hidden) { + if (!visible) { return; } @@ -493,7 +526,7 @@ bool EditorCanvasItemGizmo::intersect_point(const Point2 &p_point, const real_t ERR_FAIL_NULL_V(canvas_item, false); ERR_FAIL_COND_V(!valid, false); - if (hidden && !gizmo_plugin->is_selectable_when_hidden()) { + if (!visible && !gizmo_plugin->is_selectable_when_hidden()) { return false; } @@ -546,7 +579,7 @@ void EditorCanvasItemGizmo::create() { valid = true; for (Instance &instance : instances) { - instance.create_instance(canvas_item, hidden); + instance.create_instance(canvas_item, visible); } transform(); @@ -565,15 +598,15 @@ void EditorCanvasItemGizmo::free() { ERR_FAIL_NULL(canvas_item); ERR_FAIL_COND(!valid); - // TODO: i'm not sure why the 3D variant doesn't just call clear + // TODO: GIZMOS - i'm not sure why the 3D variant doesn't just call clear // and repeats the freeing loop. clear(); valid = false; } -void EditorCanvasItemGizmo::set_hidden(bool p_hidden) { - hidden = p_hidden; - int layer = p_hidden ? 0 : (1 << CanvasItemEditorViewport::GIZMO_EDIT_LAYER); +void EditorCanvasItemGizmo::set_visible(bool p_visible) { + visible = p_visible; + int layer = p_visible ? (1 << CanvasItemEditorViewport::GIZMO_EDIT_LAYER) : 0; for (const Instance &instance : instances) { RS::get_singleton()->canvas_item_set_visibility_layer(instance.instance, layer); } @@ -596,26 +629,32 @@ void EditorCanvasItemGizmo::_bind_methods() { 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_hidden", "hidden"), &EditorCanvasItemGizmo::set_hidden); + 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(_get_boundary); - GDVIRTUAL_BIND(_set_boundary, "boundary"); + 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"); + 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"); @@ -624,7 +663,7 @@ void EditorCanvasItemGizmo::_bind_methods() { EditorCanvasItemGizmo::EditorCanvasItemGizmo() { valid = false; - hidden = false; + visible = false; selected = false; canvas_item = nullptr; gizmo_plugin = nullptr; @@ -696,7 +735,7 @@ Ref EditorCanvasItemGizmoPlugin::get_gizmo(CanvasItem *p_ ref->set_plugin(this); ref->set_canvas_item(p_canvas_item); - ref->set_hidden(current_state == HIDDEN); + ref->set_visible(gizmos_visible); current_gizmos.insert(ref.ptr()); return ref; @@ -705,8 +744,6 @@ Ref EditorCanvasItemGizmoPlugin::get_gizmo(CanvasItem *p_ void EditorCanvasItemGizmoPlugin::_bind_methods() { ClassDB::bind_static_method("EditorCanvasItemGizmoPlugin", D_METHOD("boundary_change_to_transform", "canvas_item", "before", "after"), &EditorCanvasItemGizmoPlugin::boundary_change_to_transform); - ClassDB::bind_method(D_METHOD("_set_boundary", "gizmo", "boundary"), &EditorCanvasItemGizmoPlugin::_set_boundary); - GDVIRTUAL_BIND(_has_gizmo, "for_canvas_item"); GDVIRTUAL_BIND(_create_gizmo, "for_canvas_item"); @@ -717,14 +754,17 @@ void EditorCanvasItemGizmoPlugin::_bind_methods() { GDVIRTUAL_BIND(_redraw, "gizmo"); - GDVIRTUAL_BIND(_has_boundary, "gizmo"); - GDVIRTUAL_BIND(_set_boundary, "gizmo", "boundary"); - GDVIRTUAL_BIND(_get_boundary, "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"); @@ -732,7 +772,7 @@ void EditorCanvasItemGizmoPlugin::_bind_methods() { 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"); + 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"); @@ -779,158 +819,165 @@ void EditorCanvasItemGizmoPlugin::redraw(EditorCanvasItemGizmo *p_gizmo) { GDVIRTUAL_CALL(_redraw, p_gizmo); } -bool EditorCanvasItemGizmoPlugin::has_boundary(const EditorCanvasItemGizmo *p_gizmo) const { +bool EditorCanvasItemGizmoPlugin::_edit_use_rect(const EditorCanvasItemGizmo *p_gizmo) const { ERR_FAIL_NULL_V(p_gizmo, false); bool ret = false; - if (GDVIRTUAL_CALL(_has_boundary, Ref(p_gizmo), ret)) { + if (GDVIRTUAL_CALL(_edit_use_rect, Ref(p_gizmo), ret)) { return ret; } - return _has_boundary(p_gizmo); -} - -bool EditorCanvasItemGizmoPlugin::_has_boundary(const EditorCanvasItemGizmo *p_gizmo) const { - ERR_FAIL_NULL_V(p_gizmo, false); CanvasItem *canvas_item = p_gizmo->get_canvas_item(); ERR_FAIL_NULL_V(canvas_item, false); return canvas_item->_edit_use_rect(); } -void EditorCanvasItemGizmoPlugin::set_boundary(const EditorCanvasItemGizmo *p_gizmo, const Rect2 &p_boundary) { +void EditorCanvasItemGizmoPlugin::_edit_set_rect(const EditorCanvasItemGizmo *p_gizmo, Rect2 p_boundary) { ERR_FAIL_NULL(p_gizmo); - if (GDVIRTUAL_IS_OVERRIDDEN(_set_boundary)) { - GDVIRTUAL_CALL(_set_boundary, Ref(p_gizmo), p_boundary); + if (GDVIRTUAL_IS_OVERRIDDEN(_edit_set_rect)) { + GDVIRTUAL_CALL(_edit_set_rect, Ref(p_gizmo), p_boundary); return; } - _set_boundary(p_gizmo, p_boundary); -} - -void EditorCanvasItemGizmoPlugin::_set_boundary(const EditorCanvasItemGizmo *p_gizmo, Rect2 p_boundary) { - ERR_FAIL_NULL(p_gizmo); CanvasItem *ci = p_gizmo->get_canvas_item(); ERR_FAIL_NULL(ci); ci->_edit_set_rect(p_boundary); } -Rect2 EditorCanvasItemGizmoPlugin::get_boundary(const EditorCanvasItemGizmo *p_gizmo) const { +Rect2 EditorCanvasItemGizmoPlugin::_edit_get_rect(const EditorCanvasItemGizmo *p_gizmo) const { ERR_FAIL_NULL_V(p_gizmo, Rect2()); - if (GDVIRTUAL_IS_OVERRIDDEN(_get_boundary)) { + if (GDVIRTUAL_IS_OVERRIDDEN(_edit_get_rect)) { Rect2 ret; - GDVIRTUAL_CALL(_get_boundary, Ref(p_gizmo), ret); + GDVIRTUAL_CALL(_edit_get_rect, Ref(p_gizmo), ret); return ret; } - return _get_boundary(p_gizmo); -} - -Rect2 EditorCanvasItemGizmoPlugin::_get_boundary(const EditorCanvasItemGizmo *p_gizmo) const { - ERR_FAIL_NULL_V(p_gizmo, Rect2()); 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 { +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; } - return _has_pivot(p_gizmo); -} -bool EditorCanvasItemGizmoPlugin::_has_pivot(const EditorCanvasItemGizmo *p_gizmo) const { - ERR_FAIL_NULL_V(p_gizmo, false); 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 Point2 &p_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; } - _set_pivot(p_gizmo, p_pivot); -} - -void EditorCanvasItemGizmoPlugin::_set_pivot(const EditorCanvasItemGizmo *p_gizmo, const Vector2 &p_pivot) { - ERR_FAIL_NULL(p_gizmo); 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 { +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; } - return _get_pivot(p_gizmo); -} - -Point2 EditorCanvasItemGizmoPlugin::_get_pivot(const EditorCanvasItemGizmo *p_gizmo) const { - ERR_FAIL_NULL_V(p_gizmo, Point2()); CanvasItem *ci = p_gizmo->get_canvas_item(); ERR_FAIL_NULL_V(ci, Point2()); return ci->_edit_get_pivot(); } -bool EditorCanvasItemGizmoPlugin::is_handle_highlighted(const EditorCanvasItemGizmo *p_gizmo, int p_id, bool p_secondary) const { +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 + // TODO: GIZMOS: docs - be sure to mention this in the docs + 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); + + // first restore underlying canvas item state + CanvasItem *ci = p_gizmo->get_canvas_item(); + ERR_FAIL_NULL(ci); + ci->_edit_set_state(p_state); + + // then allow GDScript code to do their own restores on a known good state + // TODO: GIZMOS: docs - mention that the dictionary may contain extra elements + if (GDVIRTUAL_IS_OVERRIDDEN(_edit_set_state)) { + GDVIRTUAL_CALL(_edit_set_state, Ref(p_gizmo), 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 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 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) { +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) { +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) { +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 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 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 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) { +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) { +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++) { @@ -940,15 +987,15 @@ void EditorCanvasItemGizmoPlugin::commit_subgizmos(const EditorCanvasItemGizmo * GDVIRTUAL_CALL(_commit_subgizmos, Ref(p_gizmo), p_ids, transforms, p_cancel); } -void EditorCanvasItemGizmoPlugin::set_state(int p_state) { - current_state = p_state; +void EditorCanvasItemGizmoPlugin::set_gizmos_visible(bool p_visible) { + gizmos_visible = p_visible; for (EditorCanvasItemGizmo *gizmo : current_gizmos) { - gizmo->set_hidden(p_state == HIDDEN); + gizmo->set_visible(p_visible); } } -int EditorCanvasItemGizmoPlugin::get_state() const { - return current_state; +bool EditorCanvasItemGizmoPlugin::is_gizmos_visible() const { + return gizmos_visible; } void EditorCanvasItemGizmoPlugin::unregister_gizmo(EditorCanvasItemGizmo *p_gizmo) { @@ -956,7 +1003,7 @@ void EditorCanvasItemGizmoPlugin::unregister_gizmo(EditorCanvasItemGizmo *p_gizm } EditorCanvasItemGizmoPlugin::EditorCanvasItemGizmoPlugin() { - current_state = VISIBLE; + gizmos_visible = true; } EditorCanvasItemGizmoPlugin::~EditorCanvasItemGizmoPlugin() { diff --git a/editor/scene/2d/canvas_item_editor_gizmos.h b/editor/scene/2d/canvas_item_editor_gizmos.h index e91b258465ed..16d23d7c0e3e 100644 --- a/editor/scene/2d/canvas_item_editor_gizmos.h +++ b/editor/scene/2d/canvas_item_editor_gizmos.h @@ -41,7 +41,7 @@ class EditorCanvasItemGizmo : public CanvasItemGizmo { RID instance; Transform2D xform; - void create_instance(CanvasItem *p_base, bool p_hidden = false); + void create_instance(CanvasItem *p_base, bool p_visible = false); }; bool selected; @@ -61,7 +61,7 @@ class EditorCanvasItemGizmo : public CanvasItemGizmo { bool use_pivot_handle; bool valid; - bool hidden; + bool visible; Vector instances; CanvasItem *canvas_item = nullptr; @@ -74,14 +74,17 @@ class EditorCanvasItemGizmo : public CanvasItemGizmo { GDVIRTUAL0(_redraw) - GDVIRTUAL0RC(bool, _has_boundary) - GDVIRTUAL0RC(Rect2, _get_boundary) - GDVIRTUAL1(_set_boundary, Rect2) + 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) @@ -107,30 +110,32 @@ class EditorCanvasItemGizmo : public CanvasItemGizmo { void add_handles(const Vector &p_handles, Ref p_texture, const Vector &p_ids = Vector(), bool p_secondary = false); - virtual bool has_boundary() const; - virtual Rect2 get_boundary() const; - virtual Rect2 _get_boundary() const; - virtual void set_boundary(const Rect2 &p_rect); + 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 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 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; // TODO: GIZMOS: docs -> this position is in local space. - virtual Vector subgizmos_intersect_rect(const Rect2 &p_rect) const; // TODO: GIZMOS: docs -> this rect is in canvas space - 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); + virtual int _subgizmos_intersect_point(const Point2 &p_point, real_t p_max_distance) const; // TODO: GIZMOS: docs -> this position is in local space. + virtual Vector _subgizmos_intersect_rect(const Rect2 &p_rect) const; // TODO: GIZMOS: docs -> this rect is in canvas space + 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_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; } @@ -151,7 +156,7 @@ class EditorCanvasItemGizmo : public CanvasItemGizmo { virtual bool is_editable() const; - void set_hidden(bool p_hidden); + void set_visible(bool p_visible); void set_plugin(EditorCanvasItemGizmoPlugin *p_plugin); EditorCanvasItemGizmo(); @@ -161,12 +166,8 @@ class EditorCanvasItemGizmo : public CanvasItemGizmo { class EditorCanvasItemGizmoPlugin : public Resource { GDCLASS(EditorCanvasItemGizmoPlugin, Resource); -public: - static const int VISIBLE = 0; - static const int HIDDEN = 1; - protected: - int current_state; + bool gizmos_visible = true; HashSet current_gizmos; static void _bind_methods(); @@ -183,14 +184,17 @@ class EditorCanvasItemGizmoPlugin : public Resource { GDVIRTUAL1(_redraw, Ref) - GDVIRTUAL1RC(bool, _has_boundary, Ref) - GDVIRTUAL2(_set_boundary, Ref, Rect2) - GDVIRTUAL1RC(Rect2, _get_boundary, 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) @@ -214,46 +218,37 @@ class EditorCanvasItemGizmoPlugin : public Resource { virtual bool can_commit_handle_on_click() const; virtual void redraw(EditorCanvasItemGizmo *p_gizmo); - // boundary handle - virtual bool has_boundary(const EditorCanvasItemGizmo *p_gizmo) const; - virtual void set_boundary(const EditorCanvasItemGizmo *p_gizmo, const Rect2 &p_rect); - virtual Rect2 get_boundary(const EditorCanvasItemGizmo *p_gizmo) const; - -private: - virtual bool _has_boundary(const EditorCanvasItemGizmo *p_gizmo) const; - virtual void _set_boundary(const EditorCanvasItemGizmo *p_gizmo, Rect2 p_rect); - virtual Rect2 _get_boundary(const EditorCanvasItemGizmo *p_gizmo) const; + 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 -public: - 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); - -private: 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 -public: - 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); + 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); + 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_state(int p_state); - int get_state() const; + void set_gizmos_visible(bool p_visible); + bool is_gizmos_visible() const; void unregister_gizmo(EditorCanvasItemGizmo *p_gizmo); EditorCanvasItemGizmoPlugin(); diff --git a/editor/scene/canvas_item_editor_plugin.cpp b/editor/scene/canvas_item_editor_plugin.cpp index c449e345ceeb..3026134cd6e6 100644 --- a/editor/scene/canvas_item_editor_plugin.cpp +++ b/editor/scene/canvas_item_editor_plugin.cpp @@ -278,7 +278,7 @@ 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->get_boundary(); + return gizmo->_edit_get_rect(); } } return p_canvas_item->_edit_get_rect(); @@ -288,7 +288,7 @@ 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->has_boundary(); + return gizmo->_edit_use_rect(); } } return p_canvas_item->_edit_use_rect(); @@ -296,9 +296,10 @@ bool _use_edit_rect(const CanvasItem *p_canvas_item) { void _set_edit_rect(CanvasItem *p_canvas_item, const Rect2 &p_rect) { ERR_FAIL_NULL(p_canvas_item); + print_line("SET_EDIT_RECT ", p_rect); for (Ref gizmo : p_canvas_item->get_gizmos()) { if (gizmo.is_valid()) { - gizmo->set_boundary(p_rect); + gizmo->_edit_set_rect(p_rect); return; } } @@ -308,8 +309,8 @@ void _set_edit_rect(CanvasItem *p_canvas_item, const Rect2 &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(); + if (gizmo.is_valid() && gizmo->_has_pivot()) { + return gizmo->_get_pivot(); } } return p_canvas_item->_edit_get_pivot(); @@ -318,8 +319,8 @@ Vector2 _get_edit_pivot(const CanvasItem *p_canvas_item) { 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); + if (gizmo.is_valid() && gizmo->_has_pivot()) { + gizmo->_set_pivot(p_pivot); return; } } @@ -329,13 +330,34 @@ void _set_edit_pivot(CanvasItem *p_canvas_item, const Vector2 &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()) { + 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); } @@ -746,6 +768,8 @@ void CanvasItemEditor::find_canvas_items_at_pos(const Point2 &p_pos, Node *p_nod const real_t local_grab_distance = xform.basis_xform(Vector2(grab_distance, 0)).length() / zoom; // legacy way + // TODO: GIZMOS: we should probably reverse order here, first try gizmo and use this as fallback logic + // this would also fix the duplicate issue (see below). if (ci->_edit_is_selected_on_click(point, local_grab_distance)) { Node2D *node = Object::cast_to(ci); @@ -873,6 +897,9 @@ void CanvasItemEditor::_find_canvas_items_in_rect(const Rect2 &p_rect, Node *p_n // 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 + // TODO: GIZMOS: when we add gizmos to a built-in object the gizmo should use the bounding rect of the + // built-in object as additional collision shape. then we can move this down and delegate to the gizmo + // if present and only use this as fallback. if (ci->_edit_use_rect()) { Rect2 rect = ci->_edit_get_rect(); if (p_rect.has_point(xform.xform(rect.position)) && @@ -915,13 +942,13 @@ bool CanvasItemEditor::_select_subgizmos(Point2 p_click_pos, bool p_append) { 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); + 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)); + se->subgizmos.insert(subgizmo_id, gizmo->_get_subgizmo_transform(subgizmo_id)); } selection_changed = true; } else { @@ -934,7 +961,7 @@ bool CanvasItemEditor::_select_subgizmos(Point2 p_click_pos, bool p_append) { 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)); + se->subgizmos.insert(subgizmo_id, gizmo->_get_subgizmo_transform(subgizmo_id)); } } @@ -1086,11 +1113,11 @@ void CanvasItemEditor::_update_gizmos_menu() { String plugin_name = plugin->get_gizmo_name(); int plugin_id = SHOW_USER_DEFINED_GIZMO + i; - const int state = plugin->get_state(); + 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, state == EditorCanvasItemGizmoPlugin::VISIBLE); + gizmos_menu->set_item_checked(index, gizmos_visible); } } @@ -1101,19 +1128,12 @@ void CanvasItemEditor::_save_drag_selection_state() { for (CanvasItem *ci : drag_selection) { CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data(ci); if (se) { - if (se->gizmo.is_valid()) { - // if we currently have a subgizmo selection, we are not going to change the - // canvas item itself. since the subgizmo state is saved during selection, we - // don't need to do anything here. - continue; - } - if (!transform_stored) { original_transform = ci->get_global_transform(); 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(); Rect2 rect = _get_edit_rect(ci); if (rect != Rect2()) { @@ -1128,7 +1148,8 @@ void CanvasItemEditor::_save_drag_selection_state() { void CanvasItemEditor::_restore_drag_selection_state() { for (CanvasItem *ci : drag_selection) { CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data(ci); - + // TODO: GIZMOS: this should probably move out here as subgizmo workflow is slightly different from + // normal workflow and this is just making things hard to read. 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. @@ -1139,7 +1160,7 @@ void CanvasItemEditor::_restore_drag_selection_state() { xforms.push_back(entry.value); } // rollback subgizmo changes - se->gizmo->commit_subgizmos(ids, xforms, true); + se->gizmo->_commit_subgizmos(ids, xforms, true); // since only one gizmo can ever be selected at the same time, we stop it here. return; @@ -1149,8 +1170,18 @@ void CanvasItemEditor::_restore_drag_selection_state() { // 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_drag_selection_state(const String &action_name) { @@ -1167,11 +1198,11 @@ void CanvasItemEditor::_commit_drag_selection_state(const String &action_name) { xforms.push_back(entry.value); } // commit subgizmo changes (gizmos do their own redo/undo) - se->gizmo->commit_subgizmos(ids, xforms, false); + 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); + 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. @@ -1184,7 +1215,7 @@ void CanvasItemEditor::_commit_drag_selection_state(const String &action_name) { CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data(ci); Dictionary old_state = se->undo_state; - Dictionary new_state = ci->_edit_get_state(); + Dictionary new_state = _get_edit_state(ci); if (old_state.hash() != new_state.hash()) { modified_canvas_items.push_back(ci); @@ -1200,8 +1231,22 @@ void CanvasItemEditor::_commit_drag_selection_state(const String &action_name) { 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 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"); @@ -1871,31 +1916,33 @@ bool CanvasItemEditor::_gui_input_rotate(const Ref &p_event) { if (drag_type == DRAG_ROTATE) { // Rotate the node if (m.is_valid()) { - _restore_drag_selection_state(); - for (CanvasItem *ci : drag_selection) { - drag_to = transform.affine_inverse().xform(m->get_position()); - //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); - - CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data(ci); - if (se->gizmo.is_valid()) { - // if we currently have a subgizmo selection, we only rotate this subgizmo selection - - // not the node - similar to how this is done in 3D. + drag_to = transform.affine_inverse().xform(m->get_position()); - // we rotate around the drag rotation center, so we need the angle drag_from -> drag_rotation_center -> drag_to + // 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) { // since we rotate around the drag rotation center, we move there, rotate and move back Transform2D new_xform = entry.value.translated(-drag_rotation_center).rotated(angle).translated(drag_rotation_center); - se->gizmo->set_subgizmo_transform(entry.key, new_xform); + 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(); - continue; + return true; } - //no subgizmos selected - rotate the canvas item + } + + // normal node handling + _restore_drag_selection_state(); + for (CanvasItem *ci : drag_selection) { + 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(); real_t new_rotation = snap_angle(ci->_edit_get_rotation() + (opposite ? -1 : 1) * (drag_from - drag_rotation_center).angle_to(drag_to - drag_rotation_center), prev_rotation); @@ -1972,8 +2019,8 @@ bool CanvasItemEditor::_gui_input_gizmos(const Ref &p_event) { current_gizmo = editor_gizmo; current_gizmo_handle = index; current_gizmo_handle_secondary = secondary; - current_gizmo_initial_value = editor_gizmo->get_handle_value(index, secondary); - editor_gizmo->begin_handle_action(index, secondary); + current_gizmo_initial_value = editor_gizmo->_get_handle_value(index, secondary); + editor_gizmo->_begin_handle_action(index, secondary); return true; } } @@ -1985,7 +2032,7 @@ bool CanvasItemEditor::_gui_input_gizmos(const Ref &p_event) { // 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())); + current_gizmo->_set_handle(current_gizmo_handle, current_gizmo_handle_secondary, xform.xform(m->get_position())); viewport->queue_redraw(); return true; } @@ -2216,7 +2263,7 @@ 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(); @@ -2620,26 +2667,31 @@ bool CanvasItemEditor::_gui_input_move(const Ref &p_event) { } } - for (CanvasItem *ci : drag_selection) { - CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data(ci); - if (se->gizmo.is_valid()) { + // subgizmo handling + { + CanvasItemEditorSelectedItem *se = _get_selected_subgizmo(); + if (se) { // we use the delta for the snapped position, this way we get snapping for subgizmos - Vector2 delta = new_pos - previous_pos; - // delta is in canvas space, we need to convert it to local space for the gizmo - Transform2D parent_xform_inv = ci->get_global_transform().affine_inverse(); - delta = parent_xform_inv.xform(delta); + // 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. - // if we currently have a subgizmo selection, we only move this subgizmo selection - - // not the node - similar to how this is done in 3D. for (KeyValue &entry : se->subgizmos) { Transform2D new_xform = entry.value.translated(delta); - se->gizmo->set_subgizmo_transform(entry.key, new_xform); + 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(); - continue; + // 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)); @@ -2994,13 +3046,13 @@ bool CanvasItemEditor::_gui_input_select(const Ref &p_event) { continue; } - Vector subgizmos = gizmo->subgizmos_intersect_rect(selection_rect); + 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)); + 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 @@ -3367,7 +3419,7 @@ void CanvasItemEditor::_commit_drag() { 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); + current_gizmo->_commit_handle(current_gizmo_handle, current_gizmo_handle_secondary, current_gizmo_initial_value, false); } break; default: @@ -4604,8 +4656,8 @@ 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); + 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; @@ -4955,7 +5007,7 @@ void CanvasItemEditor::_selection_changed() { if (gizmo.is_null()) { continue; } - gizmo->set_selected(false); + gizmo->_set_selected(false); } CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data(selected_canvas_item); @@ -5005,7 +5057,7 @@ void CanvasItemEditor::edit(CanvasItem *p_canvas_item) { if (editor_gizmo.is_null()) { continue; } - editor_gizmo->set_selected(false); + editor_gizmo->_set_selected(false); } CanvasItemEditorSelectedItem *se = editor_selection->get_node_editor_data(selected_canvas_item); @@ -5028,7 +5080,7 @@ void CanvasItemEditor::edit(CanvasItem *p_canvas_item) { if (editor_gizmo.is_null()) { continue; } - editor_gizmo->set_selected(true); + editor_gizmo->_set_selected(true); } selected_canvas_item->update_gizmos(); } @@ -5687,23 +5739,17 @@ void CanvasItemEditor::_popup_callback(int p_op) { int gizmo_plugin_index = p_op - SHOW_USER_DEFINED_GIZMO; if (gizmo_plugin_index >= 0 && gizmo_plugin_index < gizmo_plugins_by_name.size()) { - int plugin_state = gizmo_plugins_by_name[gizmo_plugin_index]->get_state(); + 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); - switch (plugin_state) { - case EditorCanvasItemGizmoPlugin::VISIBLE: { - gizmo_plugins_by_name[gizmo_plugin_index]->set_state(EditorCanvasItemGizmoPlugin::HIDDEN); - gizmos_menu->set_item_checked(menu_item_index, false); - viewport->queue_redraw(); - } break; - - case EditorCanvasItemGizmoPlugin::HIDDEN: { - gizmo_plugins_by_name[gizmo_plugin_index]->set_state(EditorCanvasItemGizmoPlugin::VISIBLE); - gizmos_menu->set_item_checked(menu_item_index, true); - viewport->queue_redraw(); - } break; - - default: - break; + 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(); } } } @@ -5832,7 +5878,7 @@ Dictionary CanvasItemEditor::get_state() const { } // 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->get_state(); + gizmo_state[plugin->get_gizmo_name()] = plugin->is_gizmos_visible(); } state["show_user_defined_gizmos"] = gizmo_state; @@ -6003,7 +6049,7 @@ void CanvasItemEditor::set_state(const Dictionary &p_state) { 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_state(gizmo_state[plugin_gizmo_name]); + plugin->set_gizmos_visible(gizmo_state[plugin_gizmo_name]); } } } @@ -6205,8 +6251,8 @@ void CanvasItemEditor::_request_gizmo(Object *p_obj) { if (gizmo.is_valid()) { ci->add_gizmo(gizmo); - if (is_selected != gizmo->is_selected()) { - gizmo->set_selected(is_selected); + if (is_selected != gizmo->_is_selected()) { + gizmo->_set_selected(is_selected); } } } diff --git a/editor/scene/canvas_item_editor_plugin.h b/editor/scene/canvas_item_editor_plugin.h index 72f3a7222053..6ab65e4f0c01 100644 --- a/editor/scene/canvas_item_editor_plugin.h +++ b/editor/scene/canvas_item_editor_plugin.h @@ -433,6 +433,7 @@ class CanvasItemEditor : public VBoxContainer { 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_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; From b20702e86eda5467a0879f9413bd70a504a75273 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Thom=C3=A4?= Date: Mon, 26 Jan 2026 08:10:01 +0100 Subject: [PATCH 16/26] Fix implementation of boundary change. --- editor/scene/2d/canvas_item_editor_gizmos.cpp | 12 ++---------- editor/scene/2d/canvas_item_editor_gizmos.h | 2 +- editor/scene/canvas_item_editor_plugin.cpp | 1 - 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/editor/scene/2d/canvas_item_editor_gizmos.cpp b/editor/scene/2d/canvas_item_editor_gizmos.cpp index dbad3ed085c1..2e990ab1a691 100644 --- a/editor/scene/2d/canvas_item_editor_gizmos.cpp +++ b/editor/scene/2d/canvas_item_editor_gizmos.cpp @@ -697,9 +697,7 @@ int EditorCanvasItemGizmoPlugin::get_priority() const { return 0; } -Transform2D EditorCanvasItemGizmoPlugin::boundary_change_to_transform(const CanvasItem *p_canvas_item, const Rect2 &p_before, const Rect2 &p_after) { - ERR_FAIL_NULL_V(p_canvas_item, Transform2D()); - +Transform2D EditorCanvasItemGizmoPlugin::boundary_change_to_transform(const Rect2 &p_before, const Rect2 &p_after) { Vector2 zero_offset; Size2 new_scale(1, 1); @@ -714,12 +712,6 @@ Transform2D EditorCanvasItemGizmoPlugin::boundary_change_to_transform(const Canv } Point2 new_pos = p_after.position + p_after.size * zero_offset; - - Transform2D local_xf = p_canvas_item->get_transform(); - Transform2D postxf; - postxf.set_rotation_scale_and_skew(local_xf.get_rotation(), local_xf.get_scale(), local_xf.get_skew()); - new_pos = postxf.xform(new_pos); - return Transform2D().scaled(new_scale).translated(new_pos); } @@ -742,7 +734,7 @@ Ref EditorCanvasItemGizmoPlugin::get_gizmo(CanvasItem *p_ } void EditorCanvasItemGizmoPlugin::_bind_methods() { - ClassDB::bind_static_method("EditorCanvasItemGizmoPlugin", D_METHOD("boundary_change_to_transform", "canvas_item", "before", "after"), &EditorCanvasItemGizmoPlugin::boundary_change_to_transform); + 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"); diff --git a/editor/scene/2d/canvas_item_editor_gizmos.h b/editor/scene/2d/canvas_item_editor_gizmos.h index 16d23d7c0e3e..a0b22d8c2578 100644 --- a/editor/scene/2d/canvas_item_editor_gizmos.h +++ b/editor/scene/2d/canvas_item_editor_gizmos.h @@ -209,7 +209,7 @@ class EditorCanvasItemGizmoPlugin : public Resource { GDVIRTUAL4(_commit_subgizmos, Ref, Vector, TypedArray, bool) public: - static Transform2D boundary_change_to_transform(const CanvasItem *p_canvas_item, const Rect2 &p_before, const Rect2 &p_after); + 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; diff --git a/editor/scene/canvas_item_editor_plugin.cpp b/editor/scene/canvas_item_editor_plugin.cpp index 3026134cd6e6..d921c50294b7 100644 --- a/editor/scene/canvas_item_editor_plugin.cpp +++ b/editor/scene/canvas_item_editor_plugin.cpp @@ -296,7 +296,6 @@ bool _use_edit_rect(const CanvasItem *p_canvas_item) { void _set_edit_rect(CanvasItem *p_canvas_item, const Rect2 &p_rect) { ERR_FAIL_NULL(p_canvas_item); - print_line("SET_EDIT_RECT ", p_rect); for (Ref gizmo : p_canvas_item->get_gizmos()) { if (gizmo.is_valid()) { gizmo->_edit_set_rect(p_rect); From 2606ac076148698fafbb1139264d7d480c671587 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Thom=C3=A4?= Date: Mon, 26 Jan 2026 09:03:30 +0100 Subject: [PATCH 17/26] Add handle hover support. --- editor/scene/2d/canvas_item_editor_gizmos.cpp | 5 +- editor/scene/canvas_item_editor_plugin.cpp | 46 ++++++++++++++++--- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/editor/scene/2d/canvas_item_editor_gizmos.cpp b/editor/scene/2d/canvas_item_editor_gizmos.cpp index 2e990ab1a691..5fcd2fc0c4bb 100644 --- a/editor/scene/2d/canvas_item_editor_gizmos.cpp +++ b/editor/scene/2d/canvas_item_editor_gizmos.cpp @@ -386,7 +386,6 @@ void EditorCanvasItemGizmo::add_handles(const Vector &p_handles, Refget_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); @@ -418,6 +417,10 @@ void EditorCanvasItemGizmo::add_handles(const Vector &p_handles, Refcanvas_item_add_texture_rect(ins.instance, Rect2(position - texture_size / 2, texture_size), texture->get_rid(), false, modulate); } diff --git a/editor/scene/canvas_item_editor_plugin.cpp b/editor/scene/canvas_item_editor_plugin.cpp index d921c50294b7..ecff39667246 100644 --- a/editor/scene/canvas_item_editor_plugin.cpp +++ b/editor/scene/canvas_item_editor_plugin.cpp @@ -2001,8 +2001,8 @@ bool CanvasItemEditor::_gui_input_gizmos(const Ref &p_event) { // 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 editor_gizmo : gizmos) { - if (editor_gizmo.is_null()) { + for (Ref gizmo : gizmos) { + if (gizmo.is_null()) { continue; } int index; @@ -2010,20 +2010,54 @@ bool CanvasItemEditor::_gui_input_gizmos(const Ref &p_event) { 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); - editor_gizmo->handles_intersect_point(local_pos, local_grab_distance, b->is_shift_pressed(), index, secondary); + 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 = editor_gizmo; + current_gizmo = gizmo; current_gizmo_handle = index; current_gizmo_handle_secondary = secondary; - current_gizmo_initial_value = editor_gizmo->_get_handle_value(index, secondary); - editor_gizmo->_begin_handle_action(index, 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 From fd19dd4e82eed5d6b695481bbcc34d0c5983fc29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Thom=C3=A4?= Date: Thu, 29 Jan 2026 08:39:51 +0100 Subject: [PATCH 18/26] Add subgizmo rotation and temp pivot support. --- editor/scene/2d/canvas_item_editor_gizmos.cpp | 2 - editor/scene/canvas_item_editor_plugin.cpp | 64 ++++++++++++++++--- 2 files changed, 55 insertions(+), 11 deletions(-) diff --git a/editor/scene/2d/canvas_item_editor_gizmos.cpp b/editor/scene/2d/canvas_item_editor_gizmos.cpp index 5fcd2fc0c4bb..15ac4d6116ae 100644 --- a/editor/scene/2d/canvas_item_editor_gizmos.cpp +++ b/editor/scene/2d/canvas_item_editor_gizmos.cpp @@ -601,8 +601,6 @@ void EditorCanvasItemGizmo::free() { ERR_FAIL_NULL(canvas_item); ERR_FAIL_COND(!valid); - // TODO: GIZMOS - i'm not sure why the 3D variant doesn't just call clear - // and repeats the freeing loop. clear(); valid = false; } diff --git a/editor/scene/canvas_item_editor_plugin.cpp b/editor/scene/canvas_item_editor_plugin.cpp index ecff39667246..4e62a56ac533 100644 --- a/editor/scene/canvas_item_editor_plugin.cpp +++ b/editor/scene/canvas_item_editor_plugin.cpp @@ -1893,8 +1893,12 @@ 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 (se && se->gizmo.is_valid()) { + // if subgizmos are selected, the rotation center is the position of the first subgizmo + drag_rotation_center = ci->get_screen_transform().xform(se->subgizmos.begin()->value.get_origin()); } else if (_use_edit_pivot(ci)) { drag_rotation_center = ci->get_screen_transform().xform(_get_edit_pivot(ci)); } else { @@ -1925,12 +1929,20 @@ bool CanvasItemEditor::_gui_input_rotate(const Ref &p_event) { 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) { - // since we rotate around the drag rotation center, we move there, rotate and move back - Transform2D new_xform = entry.value.translated(-drag_rotation_center).rotated(angle).translated(drag_rotation_center); + 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); } - // need a redraw here because moving subgizmos needs to re-draw the canvas item - // we modified to view effects. viewport->queue_redraw(); return true; } @@ -4382,13 +4394,33 @@ 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()) { + if (se->subgizmos.size() == 1) { + // single transform, keep offset/rotation, ignore scale + subgizmo_xform = se->subgizmos.begin()->value.orthonormalized(); + } else { + // multiple transforms, calculate average offset, ignore scale/rotation + Vector2 offset; + for (const KeyValue &kv : se->subgizmos) { + offset += kv.value.orthonormalized().get_origin(); + } + offset /= static_cast(se->subgizmos.size()); + subgizmo_xform = Transform2D().translated(offset); + } + } + 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; @@ -4428,7 +4460,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; @@ -5281,11 +5313,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(); } } From dcc09144113ce064950c8034bf0221cf656fb6b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Thom=C3=A4?= Date: Mon, 16 Feb 2026 08:54:26 +0100 Subject: [PATCH 19/26] Add subgizmo scale support --- editor/scene/canvas_item_editor_plugin.cpp | 109 +++++++++++++++++---- editor/scene/canvas_item_editor_plugin.h | 5 +- 2 files changed, 94 insertions(+), 20 deletions(-) diff --git a/editor/scene/canvas_item_editor_plugin.cpp b/editor/scene/canvas_item_editor_plugin.cpp index 4e62a56ac533..3dab8d85db6e 100644 --- a/editor/scene/canvas_item_editor_plugin.cpp +++ b/editor/scene/canvas_item_editor_plugin.cpp @@ -1896,9 +1896,6 @@ bool CanvasItemEditor::_gui_input_rotate(const Ref &p_event) { 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 (se && se->gizmo.is_valid()) { - // if subgizmos are selected, the rotation center is the position of the first subgizmo - drag_rotation_center = ci->get_screen_transform().xform(se->subgizmos.begin()->value.get_origin()); } else if (_use_edit_pivot(ci)) { drag_rotation_center = ci->get_screen_transform().xform(_get_edit_pivot(ci)); } else { @@ -2421,6 +2418,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); } } @@ -2473,10 +2476,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_drag_selection_state(); - 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) { @@ -4395,23 +4461,11 @@ void CanvasItemEditor::_draw_selection() { 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()) { - if (se->subgizmos.size() == 1) { - // single transform, keep offset/rotation, ignore scale - subgizmo_xform = se->subgizmos.begin()->value.orthonormalized(); - } else { - // multiple transforms, calculate average offset, ignore scale/rotation - Vector2 offset; - for (const KeyValue &kv : se->subgizmos) { - offset += kv.value.orthonormalized().get_origin(); - } - offset /= static_cast(se->subgizmos.size()); - subgizmo_xform = Transform2D().translated(offset); - } + subgizmo_xform = Transform2D().translated(selected_subgizmos_center); } Transform2D xform = transform * ci->get_screen_transform(); @@ -5114,6 +5168,25 @@ void CanvasItemEditor::refresh_dirty_gizmos() { 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) { diff --git a/editor/scene/canvas_item_editor_plugin.h b/editor/scene/canvas_item_editor_plugin.h index 6ab65e4f0c01..d4a7b9d757c7 100644 --- a/editor/scene/canvas_item_editor_plugin.h +++ b/editor/scene/canvas_item_editor_plugin.h @@ -278,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; @@ -661,6 +662,7 @@ class CanvasItemEditor : public VBoxContainer { 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; } @@ -672,8 +674,7 @@ class CanvasItemEditor : public VBoxContainer { Vector get_subgizmo_selection(); void clear_subgizmo_selection(Object *p_obj = nullptr); void refresh_dirty_gizmos(); - // TODO: GIZMOS actually implement this. - void update_transform_gizmo() {} + 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); From 875ec25edea545e6f86394a2e2bb9f29a8dc7be7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Thom=C3=A4?= Date: Tue, 24 Feb 2026 07:10:10 +0100 Subject: [PATCH 20/26] Fix typo. --- editor/scene/canvas_item_editor_plugin.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/scene/canvas_item_editor_plugin.cpp b/editor/scene/canvas_item_editor_plugin.cpp index 3dab8d85db6e..94db55d4c4a5 100644 --- a/editor/scene/canvas_item_editor_plugin.cpp +++ b/editor/scene/canvas_item_editor_plugin.cpp @@ -790,7 +790,7 @@ void CanvasItemEditor::find_canvas_items_at_pos(const Point2 &p_pos, Node *p_nod } if (gizmo->intersect_point(point, local_grab_distance)) { Node2D *node = Object::cast_to(ci); - _SelectResult res; + SelectResult res; res.item = ci; res.z_index = node ? node->get_z_index() : 0; res.has_z = true; From 9cdc8c67b2e0c92f0ada5c2b6ebbd7b3574e7a51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Thom=C3=A4?= Date: Wed, 25 Feb 2026 07:32:42 +0100 Subject: [PATCH 21/26] Add documentation for new API. --- doc/classes/CanvasItem.xml | 42 +++ doc/classes/CanvasItemGizmo.xml | 12 + doc/classes/EditorCanvasItemGizmo.xml | 281 ++++++++++++++++++++ doc/classes/EditorCanvasItemGizmoPlugin.xml | 237 +++++++++++++++++ doc/classes/EditorPlugin.xml | 15 ++ 5 files changed, 587 insertions(+) create mode 100644 doc/classes/CanvasItemGizmo.xml create mode 100644 doc/classes/EditorCanvasItemGizmo.xml create mode 100644 doc/classes/EditorCanvasItemGizmoPlugin.xml 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..ec878d28e5ce --- /dev/null +++ b/doc/classes/EditorCanvasItemGizmo.xml @@ -0,0 +1,281 @@ + + + + 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. + + + + + + + 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. + + + + + + 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..f2ba41f06715 --- /dev/null +++ b/doc/classes/EditorCanvasItemGizmoPlugin.xml @@ -0,0 +1,237 @@ + + + + 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. + + + + + + + + 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. + + + + + + + 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. + + + + + + + + + + + + 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]. + + From 523a5390c7e58c88293d3823fa05db0518500780 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Thom=C3=A4?= Date: Wed, 25 Feb 2026 07:33:17 +0100 Subject: [PATCH 22/26] Fix missing parameter names. --- scene/main/canvas_item.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scene/main/canvas_item.cpp b/scene/main/canvas_item.cpp index f1adf786784a..5a7fc9e82175 100644 --- a/scene/main/canvas_item.cpp +++ b/scene/main/canvas_item.cpp @@ -1511,7 +1511,7 @@ void CanvasItem::_bind_methods() { 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"), &CanvasItem::set_subgizmo_selection); + 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); From 3f5ee17e9e3444272f72225fa8d233b0eb9e3023 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Thom=C3=A4?= Date: Wed, 25 Feb 2026 08:35:17 +0100 Subject: [PATCH 23/26] Address TODO comments. --- doc/classes/EditorCanvasItemGizmo.xml | 2 ++ doc/classes/EditorCanvasItemGizmoPlugin.xml | 4 ++- editor/scene/2d/canvas_item_editor_gizmos.cpp | 4 --- editor/scene/2d/canvas_item_editor_gizmos.h | 10 +++--- editor/scene/canvas_item_editor_plugin.cpp | 32 ++++++++----------- 5 files changed, 24 insertions(+), 28 deletions(-) diff --git a/doc/classes/EditorCanvasItemGizmo.xml b/doc/classes/EditorCanvasItemGizmo.xml index ec878d28e5ce..f987866c13aa 100644 --- a/doc/classes/EditorCanvasItemGizmo.xml +++ b/doc/classes/EditorCanvasItemGizmo.xml @@ -50,6 +50,7 @@ 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. @@ -65,6 +66,7 @@ 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]. diff --git a/doc/classes/EditorCanvasItemGizmoPlugin.xml b/doc/classes/EditorCanvasItemGizmoPlugin.xml index f2ba41f06715..4ef3895c5664 100644 --- a/doc/classes/EditorCanvasItemGizmoPlugin.xml +++ b/doc/classes/EditorCanvasItemGizmoPlugin.xml @@ -69,6 +69,7 @@ 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. @@ -86,6 +87,7 @@ 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]. @@ -230,7 +232,7 @@ - + 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/editor/scene/2d/canvas_item_editor_gizmos.cpp b/editor/scene/2d/canvas_item_editor_gizmos.cpp index 15ac4d6116ae..cc7541ae4f5e 100644 --- a/editor/scene/2d/canvas_item_editor_gizmos.cpp +++ b/editor/scene/2d/canvas_item_editor_gizmos.cpp @@ -148,7 +148,6 @@ Dictionary EditorCanvasItemGizmo::_edit_get_state() const { // 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. - // TODO: GIZMOS: docs - this needs to be in the docs as its non-obvious behavior Dictionary ret; if (GDVIRTUAL_IS_OVERRIDDEN(_edit_get_state)) { GDVIRTUAL_CALL(_edit_get_state, ret); @@ -170,7 +169,6 @@ void EditorCanvasItemGizmo::_edit_set_state(const Dictionary &p_state) { // 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. - // TODO: GIZMOS: docs - should probably state in the docs that the dictionary may contain additional items. if (GDVIRTUAL_IS_OVERRIDDEN(_edit_set_state)) { GDVIRTUAL_CALL(_edit_set_state, p_state); } @@ -893,7 +891,6 @@ Dictionary EditorCanvasItemGizmoPlugin::_edit_get_state(const EditorCanvasItemGi } // similar to EditorCanvasItemGizmo::_edit_get_state, we merge the results - // TODO: GIZMOS: docs - be sure to mention this in the docs Dictionary base = ci->_edit_get_state(); if (ret.is_empty()) { @@ -912,7 +909,6 @@ void EditorCanvasItemGizmoPlugin::_edit_set_state(const EditorCanvasItemGizmo *p ci->_edit_set_state(p_state); // then allow GDScript code to do their own restores on a known good state - // TODO: GIZMOS: docs - mention that the dictionary may contain extra elements if (GDVIRTUAL_IS_OVERRIDDEN(_edit_set_state)) { GDVIRTUAL_CALL(_edit_set_state, Ref(p_gizmo), p_state); } diff --git a/editor/scene/2d/canvas_item_editor_gizmos.h b/editor/scene/2d/canvas_item_editor_gizmos.h index a0b22d8c2578..5044e89ae196 100644 --- a/editor/scene/2d/canvas_item_editor_gizmos.h +++ b/editor/scene/2d/canvas_item_editor_gizmos.h @@ -128,8 +128,8 @@ class EditorCanvasItemGizmo : public CanvasItemGizmo { 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; // TODO: GIZMOS: docs -> this position is in local space. - virtual Vector _subgizmos_intersect_rect(const Rect2 &p_rect) const; // TODO: GIZMOS: docs -> this rect is in canvas space + 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); @@ -142,9 +142,9 @@ class EditorCanvasItemGizmo : public CanvasItemGizmo { Ref get_plugin() const { return gizmo_plugin; } - bool intersect_rect(const Rect2 &p_rect) const; // TODO: GIZMOS: docs -> this rect is in canvas space - void handles_intersect_point(const Point2 &p_point, real_t p_max_distance, bool p_shift_pressed, int &r_id, bool &r_secondary); // TODO: GIZMOS: docs -> this position is in local space, so is distance - bool intersect_point(const Point2 &p_point, real_t p_max_distance) const; // TODO: GIZMOS: docs -> this position is in local space, so is max_distance + 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; diff --git a/editor/scene/canvas_item_editor_plugin.cpp b/editor/scene/canvas_item_editor_plugin.cpp index 94db55d4c4a5..d084e9eb47a1 100644 --- a/editor/scene/canvas_item_editor_plugin.cpp +++ b/editor/scene/canvas_item_editor_plugin.cpp @@ -766,19 +766,6 @@ void CanvasItemEditor::find_canvas_items_at_pos(const Point2 &p_pos, Node *p_nod const Vector2 point = xform.xform(p_pos); const real_t local_grab_distance = xform.basis_xform(Vector2(grab_distance, 0)).length() / zoom; - // legacy way - // TODO: GIZMOS: we should probably reverse order here, first try gizmo and use this as fallback logic - // this would also fix the duplicate issue (see below). - if (ci->_edit_is_selected_on_click(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 = node; - r_items.push_back(res); - } - // gizmos way // note: this may produce duplicate entries but the calling // function is filtering for duplicates anyways, so we're not spending @@ -797,6 +784,17 @@ void CanvasItemEditor::find_canvas_items_at_pos(const Point2 &p_pos, Node *p_nod 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; + res.item = ci; + res.z_index = node ? node->get_z_index() : 0; + res.has_z = node; + r_items.push_back(res); + } } } @@ -896,9 +894,6 @@ void CanvasItemEditor::_find_canvas_items_in_rect(const Rect2 &p_rect, Node *p_n // 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 - // TODO: GIZMOS: when we add gizmos to a built-in object the gizmo should use the bounding rect of the - // built-in object as additional collision shape. then we can move this down and delegate to the gizmo - // if present and only use this as fallback. if (ci->_edit_use_rect()) { Rect2 rect = ci->_edit_get_rect(); if (p_rect.has_point(xform.xform(rect.position)) && @@ -1145,10 +1140,9 @@ void CanvasItemEditor::_save_drag_selection_state() { } 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); - // TODO: GIZMOS: this should probably move out here as subgizmo workflow is slightly different from - // normal workflow and this is just making things hard to read. 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. @@ -3203,6 +3197,8 @@ bool CanvasItemEditor::_gui_input_select(const Ref &p_event) { 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); } } From 04251f7c7b3dc49c8223fc97e7f7b03b33f220a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Thom=C3=A4?= Date: Thu, 26 Feb 2026 09:23:07 +0100 Subject: [PATCH 24/26] Fix handles drawing over the guide rulers. --- editor/scene/2d/canvas_item_editor_gizmos.cpp | 7 ++----- editor/scene/canvas_item_editor_plugin.h | 2 -- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/editor/scene/2d/canvas_item_editor_gizmos.cpp b/editor/scene/2d/canvas_item_editor_gizmos.cpp index cc7541ae4f5e..0908349fb282 100644 --- a/editor/scene/2d/canvas_item_editor_gizmos.cpp +++ b/editor/scene/2d/canvas_item_editor_gizmos.cpp @@ -299,8 +299,7 @@ void EditorCanvasItemGizmo::Instance::create_instance(CanvasItem *p_base, bool p instance = RS::get_singleton()->canvas_item_create(); RS::get_singleton()->canvas_item_set_parent(instance, p_base->get_canvas_item()); - int layer = p_visible ? (1 << CanvasItemEditorViewport::GIZMO_EDIT_LAYER) : 0; - RS::get_singleton()->canvas_item_set_visibility_layer(instance, layer); + 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) { @@ -419,7 +418,6 @@ void EditorCanvasItemGizmo::add_handles(const Vector &p_handles, Refcanvas_item_add_texture_rect(ins.instance, Rect2(position - texture_size / 2, texture_size), texture->get_rid(), false, modulate); } @@ -605,9 +603,8 @@ void EditorCanvasItemGizmo::free() { void EditorCanvasItemGizmo::set_visible(bool p_visible) { visible = p_visible; - int layer = p_visible ? (1 << CanvasItemEditorViewport::GIZMO_EDIT_LAYER) : 0; for (const Instance &instance : instances) { - RS::get_singleton()->canvas_item_set_visibility_layer(instance.instance, layer); + RS::get_singleton()->canvas_item_set_visible(instance.instance, p_visible); } } diff --git a/editor/scene/canvas_item_editor_plugin.h b/editor/scene/canvas_item_editor_plugin.h index d4a7b9d757c7..b1f99993cd67 100644 --- a/editor/scene/canvas_item_editor_plugin.h +++ b/editor/scene/canvas_item_editor_plugin.h @@ -747,8 +747,6 @@ class CanvasItemEditorViewport : public Control { void _notification(int p_what); public: - static constexpr int32_t GIZMO_EDIT_LAYER = 26; - virtual bool can_drop_data(const Point2 &p_point, const Variant &p_data) const override; virtual void drop_data(const Point2 &p_point, const Variant &p_data) override; From de9d2404754dd33f24ad0adf8c3f20c201ebabfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Thom=C3=A4?= Date: Mon, 16 Mar 2026 08:25:59 +0100 Subject: [PATCH 25/26] Fix restore order of edit_get_state/edit_state_state for gizmos. --- editor/scene/2d/canvas_item_editor_gizmos.cpp | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/editor/scene/2d/canvas_item_editor_gizmos.cpp b/editor/scene/2d/canvas_item_editor_gizmos.cpp index 0908349fb282..59a5426d15a0 100644 --- a/editor/scene/2d/canvas_item_editor_gizmos.cpp +++ b/editor/scene/2d/canvas_item_editor_gizmos.cpp @@ -299,6 +299,8 @@ void EditorCanvasItemGizmo::Instance::create_instance(CanvasItem *p_base, bool p 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); } @@ -620,7 +622,7 @@ void EditorCanvasItemGizmo::_bind_methods() { 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", "color", "ids", "secondary"), &EditorCanvasItemGizmo::add_handles, DEFVAL(false)); + 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); @@ -900,15 +902,17 @@ Dictionary EditorCanvasItemGizmoPlugin::_edit_get_state(const EditorCanvasItemGi void EditorCanvasItemGizmoPlugin::_edit_set_state(const EditorCanvasItemGizmo *p_gizmo, const Dictionary &p_state) { ERR_FAIL_NULL(p_gizmo); - // first restore underlying canvas item state - CanvasItem *ci = p_gizmo->get_canvas_item(); - ERR_FAIL_NULL(ci); - ci->_edit_set_state(p_state); - - // then allow GDScript code to do their own restores on a known good state + // 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 { From 0edca497a148e2857ca480798aaa1e2fa1fe6513 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Thom=C3=A4?= Date: Mon, 16 Mar 2026 08:49:25 +0100 Subject: [PATCH 26/26] Fix compile errors after rebase. --- editor/scene/2d/canvas_item_editor_gizmos.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/editor/scene/2d/canvas_item_editor_gizmos.cpp b/editor/scene/2d/canvas_item_editor_gizmos.cpp index 59a5426d15a0..77da9c714a41 100644 --- a/editor/scene/2d/canvas_item_editor_gizmos.cpp +++ b/editor/scene/2d/canvas_item_editor_gizmos.cpp @@ -31,10 +31,13 @@ #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);