From 3f87d3442d85d5a3438363432178964b59a21e1d Mon Sep 17 00:00:00 2001 From: Pijus Zacharka Date: Sat, 11 Jul 2026 16:27:25 +0300 Subject: [PATCH 1/3] cherry pick commit 3cfcedd and 365f8fd cherry picked commits 3cfcedd6d57e171b54a342842bf807479b0c903e (make changing disk size for path3d editor plugin tilt gizmo not require engine restart) and 365f8fd038ec47b8f22f4960e13c11264be0b515 (add option to snap handles to colliders in path3d editor plugin) from Godot. --- editor/scene/3d/path_3d_editor_plugin.cpp | 130 ++++++++++++++++++++-- editor/scene/3d/path_3d_editor_plugin.h | 30 +++-- editor/settings/editor_settings.cpp | 2 +- 3 files changed, 142 insertions(+), 20 deletions(-) diff --git a/editor/scene/3d/path_3d_editor_plugin.cpp b/editor/scene/3d/path_3d_editor_plugin.cpp index ab67ddafd7..59c73a470a 100644 --- a/editor/scene/3d/path_3d_editor_plugin.cpp +++ b/editor/scene/3d/path_3d_editor_plugin.cpp @@ -122,6 +122,29 @@ void Path3DGizmo::set_handle(int p_id, bool p_secondary, Camera3D *p_camera, con Vector3 inters; // Special case for primary handle, the handle id equals control point id. const int idx = p_id; + if (!Path3DEditorPlugin::singleton->_edit.waiting_handle_physics || !Path3DEditorPlugin::singleton->_edit.in_physics_frame) { + Path3DEditorPlugin::singleton->_edit.waiting_handle_physics = true; + Path3DEditorPlugin::singleton->_edit.gizmo_handle = p_id; + Path3DEditorPlugin::singleton->_edit.gizmo_handle_secondary = p_secondary; + Path3DEditorPlugin::singleton->_edit.gizmo_camera = p_camera; + Path3DEditorPlugin::singleton->_edit.mouse_pos = p_point; + return; + // Only continue if inside physics frame and waiting for physics. + } + if (Path3DEditorPlugin::singleton->snap_to_collider) { + PhysicsDirectSpaceState3D *ss = p_camera->get_world_3d()->get_direct_space_state(); + + PhysicsDirectSpaceState3D::RayParameters ray_params; + ray_params.from = ray_from; + ray_params.to = ray_from + ray_dir * p_camera->get_far(); + PhysicsDirectSpaceState3D::RayResult result; + if (ss->intersect_ray(ray_params, result)) { + Vector3 local = gi.xform(result.position); + c->set_point_position(idx, local); + return; + } + // Will continue and do the plane intersect_ray if doesn't hit anything. + } if (p.intersects_ray(ray_from, ray_dir, &inters)) { if (Node3DEditor::get_singleton()->is_snap_enabled()) { float snap = Node3DEditor::get_singleton()->get_translate_snap(); @@ -416,7 +439,9 @@ void Path3DGizmo::redraw() { PackedInt32Array collected_secondary_handle_ids; // Avoid shadowing member on Node3DEditorGizmo. _secondary_handles_info.resize(c->get_point_count() * 3); - + + const float disk_size = EDITOR_GET("editors/3d_gizmos/gizmo_settings/path3d_tilt_disk_size"); + for (int idx = 0; idx < c->get_point_count(); idx++) { // Collect primary-handles. const Vector3 pos = c->get_point_position(idx); @@ -547,9 +572,8 @@ void Path3DGizmo::_update_transform_gizmo() { Node3DEditor::get_singleton()->update_transform_gizmo(); } -Path3DGizmo::Path3DGizmo(Path3D *p_path, float p_disk_size) { +Path3DGizmo::Path3DGizmo(Path3D *p_path) { path = p_path; - disk_size = p_disk_size; set_node_3d(p_path); orig_in_length = 0; orig_out_length = 0; @@ -676,10 +700,21 @@ EditorPlugin::AfterGUIInput Path3DEditorPlugin::forward_3d_gui_input(Camera3D *p } else { origin = gt.xform(c->get_point_position(c->get_point_count() - 1)); } - Plane p(p_camera->get_transform().basis.get_column(2), origin); + Vector3 ray_from = viewport->get_ray_pos(mbpos); Vector3 ray_dir = viewport->get_ray(mbpos); + if (snap_to_collider) { + _edit.click_ray_pos = ray_from; + _edit.click_ray_dir = ray_dir * p_camera->get_far(); + _edit.gizmo_camera = p_camera; + _edit.origin = origin; + _edit.waiting_point_physics = true; + return EditorPlugin::AFTER_GUI_INPUT_STOP; + } + + Plane p(p_camera->get_transform().basis.get_column(2), origin); + Vector3 inters; if (p.intersects_ray(ray_from, ray_dir, &inters)) { ur->create_action(TTR("Add Point to Curve")); @@ -693,6 +728,7 @@ EditorPlugin::AfterGUIInput Path3DEditorPlugin::forward_3d_gui_input(Camera3D *p } } else if (mb->is_pressed() && ((mb->get_button_index() == MouseButton::LEFT && curve_del->is_pressed()) || (mb->get_button_index() == MouseButton::RIGHT && curve_edit->is_pressed()))) { + const float disk_size = EDITOR_GET("editors/3d_gizmos/gizmo_settings/path3d_tilt_disk_size"); for (int i = 0; i < c->get_point_count(); i++) { real_t dist_to_p = viewport->point_to_screen(gt.xform(c->get_point_position(i))).distance_to(mbpos); real_t dist_to_p_out = viewport->point_to_screen(gt.xform(c->get_point_position(i) + c->get_point_out(i))).distance_to(mbpos); @@ -755,7 +791,7 @@ void Path3DEditorPlugin::edit(Object *p_object) { } update_overlays(); - //collision_polygon_editor->edit(Object::cast_to(p_object)); + // collision_polygon_editor->edit(Object::cast_to(p_object)); } bool Path3DEditorPlugin::handles(Object *p_object) const { @@ -776,6 +812,8 @@ void Path3DEditorPlugin::make_visible(bool p_visible) { } } } + + set_physics_process(p_visible); } void Path3DEditorPlugin::_mode_changed(int p_mode) { @@ -819,6 +857,11 @@ void Path3DEditorPlugin::_handle_option_pressed(int p_option) { mirror_handle_length = !is_checked; pm->set_item_checked(HANDLE_OPTION_LENGTH, mirror_handle_length); } break; + case HANDLE_OPTION_SNAP_COLLIDER: { + bool is_checked = pm->is_item_checked(HANDLE_OPTION_SNAP_COLLIDER); + snap_to_collider = !is_checked; + pm->set_item_checked(HANDLE_OPTION_SNAP_COLLIDER, snap_to_collider); + } break; } } @@ -905,14 +948,78 @@ void Path3DEditorPlugin::_bind_methods() { ClassDB::bind_method(D_METHOD("_restore_curve_points"), &Path3DEditorPlugin::_restore_curve_points); } +void Path3DEditorPlugin::_notification(int p_what) { + switch (p_what) { + case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: { + if (!path) { + return; + } + + if (!EditorSettings::get_singleton()->check_changed_settings_in_group("editors/3d_gizmos/gizmo_settings")) { + return; + } + + path->update_gizmos(); + } break; + case NOTIFICATION_PHYSICS_PROCESS: { + if (_edit.waiting_point_physics) { + _edit.waiting_point_physics = false; + const Transform3D gt = path->get_global_transform(); + const Transform3D it = gt.affine_inverse(); + Ref c = path->get_curve(); + EditorUndoRedoManager *ur = EditorUndoRedoManager::get_singleton(); + PhysicsDirectSpaceState3D *ss = get_tree()->get_root()->get_world_3d()->get_direct_space_state(); + if (ss) { + PhysicsDirectSpaceState3D::RayParameters ray_params; + PhysicsDirectSpaceState3D::RayResult result; + ray_params.from = _edit.click_ray_pos; + ray_params.to = ray_params.from + _edit.click_ray_dir; + bool hit_something = false; + Vector3 inters; + if (ss->intersect_ray(ray_params, result)) { + inters = result.position; + hit_something = true; + } else { + Plane p(_edit.gizmo_camera->get_transform().basis.get_column(2), _edit.origin); + if (p.intersects_ray(ray_params.from, _edit.click_ray_dir, &inters)) { + hit_something = true; + } + } + if (hit_something) { + ur->create_action(TTR("Add Point to Curve")); + ur->add_do_method(c.ptr(), "add_point", it.xform(inters), Vector3(), Vector3(), -1); + ur->add_undo_method(c.ptr(), "remove_point", c->get_point_count()); + ur->commit_action(); + } + } + } + if (_edit.waiting_handle_physics) { + _edit.in_physics_frame = true; + + // Find gizmo reference. + Vector> gizmos = path->get_gizmos(); + for (Ref seg : gizmos) { + if (seg.is_valid()) { + _edit.gizmo = seg; + break; + } + } + + _edit.gizmo->set_handle(_edit.gizmo_handle, _edit.gizmo_handle_secondary, _edit.gizmo_camera, _edit.mouse_pos); + _edit.in_physics_frame = false; + _edit.waiting_handle_physics = false; + } + } + } +} + Path3DEditorPlugin::Path3DEditorPlugin() { singleton = this; mirror_handle_angle = true; mirror_handle_length = true; - disk_size = EDITOR_GET("editors/3d_gizmos/gizmo_settings/path3d_tilt_disk_size"); - - Ref gizmo_plugin = memnew(Path3DGizmoPlugin(disk_size)); + Ref gizmo_plugin; + gizmo_plugin.instantiate(); Node3DEditor::get_singleton()->add_gizmo_plugin(gizmo_plugin); path_3d_gizmo_plugin = gizmo_plugin; @@ -1002,6 +1109,8 @@ Path3DEditorPlugin::Path3DEditorPlugin() { menu->set_item_checked(HANDLE_OPTION_ANGLE, mirror_handle_angle); menu->add_check_item(TTR("Mirror Handle Lengths")); menu->set_item_checked(HANDLE_OPTION_LENGTH, mirror_handle_length); + menu->add_check_item(TTR("Snap to Colliders")); + menu->set_item_checked(HANDLE_OPTION_SNAP_COLLIDER, snap_to_collider); menu->connect(SceneStringName(id_pressed), callable_mp(this, &Path3DEditorPlugin::_handle_option_pressed)); curve_edit->set_pressed_no_signal(true); @@ -1015,7 +1124,7 @@ Ref Path3DGizmoPlugin::create_gizmo(Node3D *p_spatial) { Path3D *path = Object::cast_to(p_spatial); if (path) { - ref.instantiate(path, disk_size); + ref.instantiate(path); } return ref; @@ -1193,10 +1302,9 @@ int Path3DGizmoPlugin::get_priority() const { return -1; } -Path3DGizmoPlugin::Path3DGizmoPlugin(float p_disk_size) { +Path3DGizmoPlugin::Path3DGizmoPlugin() { Color path_color = SceneTree::get_singleton()->get_debug_paths_color(); Color path_tilt_color = EDITOR_GET("editors/3d_gizmos/gizmo_colors/path_tilt"); - disk_size = p_disk_size; create_material("path_material", path_color); create_material("path_thin_material", Color(0.6, 0.6, 0.6)); diff --git a/editor/scene/3d/path_3d_editor_plugin.h b/editor/scene/3d/path_3d_editor_plugin.h index a49bfecf03..095de49c8c 100644 --- a/editor/scene/3d/path_3d_editor_plugin.h +++ b/editor/scene/3d/path_3d_editor_plugin.h @@ -67,7 +67,6 @@ class Path3DGizmo : public EditorNode3DGizmo { mutable Vector3 original; mutable float orig_in_length; mutable float orig_out_length; - mutable float disk_size = 0.8; /// Index that should have swapped control points for achieving an outwards curve. int swapped_control_points_idx = -1; @@ -85,14 +84,12 @@ class Path3DGizmo : public EditorNode3DGizmo { virtual void commit_handle(int p_id, bool p_secondary, const Variant &p_restore, bool p_cancel = false) override; virtual void redraw() override; - Path3DGizmo(Path3D *p_path = nullptr, float p_disk_size = 0.8); + Path3DGizmo(Path3D *p_path = nullptr); }; class Path3DGizmoPlugin : public EditorNode3DGizmoPlugin { GDCLASS(Path3DGizmoPlugin, EditorNode3DGizmoPlugin); - float disk_size = 0.8; - /// Locking basis is meant to ensure a predictable behavior during translation of the curve points in "local space transform mode". /// Without the locking, the gizmo/point, in "local space transform mode", wouldn't follow a straight path and would curve and twitch in an unpredictable way. HashMap transformation_locked_basis; @@ -113,7 +110,7 @@ class Path3DGizmoPlugin : public EditorNode3DGizmoPlugin { virtual void commit_subgizmos(const EditorNode3DGizmo *p_gizmo, const Vector &p_ids, const Vector &p_restore, bool p_cancel = false) override; int get_priority() const override; - Path3DGizmoPlugin(float p_disk_size); + Path3DGizmoPlugin(); }; class Path3DEditorPlugin : public EditorPlugin { @@ -139,8 +136,6 @@ class Path3DEditorPlugin : public EditorPlugin { Button *create_curve_button = nullptr; ConfirmationDialog *clear_points_dialog = nullptr; - float disk_size = 0.8; - enum Mode { MODE_CREATE, MODE_EDIT, @@ -161,6 +156,7 @@ class Path3DEditorPlugin : public EditorPlugin { bool handle_clicked = false; bool mirror_handle_angle = true; bool mirror_handle_length = true; + bool snap_to_collider = true; void _create_curve(); void _confirm_clear_points(); @@ -170,10 +166,12 @@ class Path3DEditorPlugin : public EditorPlugin { enum HandleOption { HANDLE_OPTION_ANGLE, - HANDLE_OPTION_LENGTH + HANDLE_OPTION_LENGTH, + HANDLE_OPTION_SNAP_COLLIDER, }; protected: + virtual void _notification(int p_what); static void _bind_methods(); public: @@ -194,4 +192,20 @@ class Path3DEditorPlugin : public EditorPlugin { void set_handle_clicked(bool clicked) { handle_clicked = clicked; } Path3DEditorPlugin(); + +private: + struct EditData { + Vector3 click_ray_dir; + Vector3 click_ray_pos; + Vector3 origin; + Point2 mouse_pos; + bool show_rotation_line = false; + Ref gizmo; + int gizmo_handle = 0; + bool gizmo_handle_secondary = false; + Camera3D *gizmo_camera; + bool waiting_point_physics = false; + bool waiting_handle_physics = false; + bool in_physics_frame = false; + } _edit; }; diff --git a/editor/settings/editor_settings.cpp b/editor/settings/editor_settings.cpp index a649ea9000..418b43b548 100644 --- a/editor/settings/editor_settings.cpp +++ b/editor/settings/editor_settings.cpp @@ -900,7 +900,7 @@ void EditorSettings::_load_defaults(Ref p_extra_config) { EDITOR_SETTING_USAGE(Variant::COLOR, PROPERTY_HINT_NONE, "editors/3d_gizmos/gizmo_colors/ik_chain", Color(0.6, 0.9, 0.8), "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_RESTART_IF_CHANGED) _initial_set("editors/3d_gizmos/gizmo_settings/bone_axis_length", (float)0.1); EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "editors/3d_gizmos/gizmo_settings/bone_shape", 1, "Wire,Octahedron"); - EDITOR_SETTING_USAGE(Variant::FLOAT, PROPERTY_HINT_NONE, "editors/3d_gizmos/gizmo_settings/path3d_tilt_disk_size", 0.8, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_RESTART_IF_CHANGED) + EDITOR_SETTING_USAGE(Variant::FLOAT, PROPERTY_HINT_NONE, "editors/3d_gizmos/gizmo_settings/path3d_tilt_disk_size", 0.8, "", PROPERTY_USAGE_DEFAULT) EDITOR_SETTING_USAGE(Variant::FLOAT, PROPERTY_HINT_RANGE, "editors/3d_gizmos/gizmo_settings/lightmap_gi_probe_size", 0.4, "0.0,1.0,0.001,or_greater", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_RESTART_IF_CHANGED) // If a line is a multiple of this, it uses the primary grid color. From 525cc84f5152eb8a1b9f59f8fdfa018495c5df30 Mon Sep 17 00:00:00 2001 From: Pijus Zacharka Date: Sat, 11 Jul 2026 16:52:06 +0300 Subject: [PATCH 2/3] format code --- editor/scene/3d/path_3d_editor_plugin.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/editor/scene/3d/path_3d_editor_plugin.cpp b/editor/scene/3d/path_3d_editor_plugin.cpp index 59c73a470a..46c02fce47 100644 --- a/editor/scene/3d/path_3d_editor_plugin.cpp +++ b/editor/scene/3d/path_3d_editor_plugin.cpp @@ -439,9 +439,9 @@ void Path3DGizmo::redraw() { PackedInt32Array collected_secondary_handle_ids; // Avoid shadowing member on Node3DEditorGizmo. _secondary_handles_info.resize(c->get_point_count() * 3); - + const float disk_size = EDITOR_GET("editors/3d_gizmos/gizmo_settings/path3d_tilt_disk_size"); - + for (int idx = 0; idx < c->get_point_count(); idx++) { // Collect primary-handles. const Vector3 pos = c->get_point_position(idx); From 18e80fb6185e8ead0d018fdf50462380b2f2f0df Mon Sep 17 00:00:00 2001 From: Pijus Zacharka Date: Sun, 12 Jul 2026 10:50:31 +0300 Subject: [PATCH 3/3] check if snap_to_collider is enabled in set_handle, to no waste process frames on unused feature --- editor/scene/3d/path_3d_editor_plugin.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/scene/3d/path_3d_editor_plugin.cpp b/editor/scene/3d/path_3d_editor_plugin.cpp index 46c02fce47..33a5cda0dc 100644 --- a/editor/scene/3d/path_3d_editor_plugin.cpp +++ b/editor/scene/3d/path_3d_editor_plugin.cpp @@ -122,7 +122,7 @@ void Path3DGizmo::set_handle(int p_id, bool p_secondary, Camera3D *p_camera, con Vector3 inters; // Special case for primary handle, the handle id equals control point id. const int idx = p_id; - if (!Path3DEditorPlugin::singleton->_edit.waiting_handle_physics || !Path3DEditorPlugin::singleton->_edit.in_physics_frame) { + if (Path3DEditorPlugin::singleton->snap_to_collider && (!Path3DEditorPlugin::singleton->_edit.waiting_handle_physics || !Path3DEditorPlugin::singleton->_edit.in_physics_frame)) { Path3DEditorPlugin::singleton->_edit.waiting_handle_physics = true; Path3DEditorPlugin::singleton->_edit.gizmo_handle = p_id; Path3DEditorPlugin::singleton->_edit.gizmo_handle_secondary = p_secondary;