From 365f8fd038ec47b8f22f4960e13c11264be0b515 Mon Sep 17 00:00:00 2001 From: Gustavo Jaruga Cruz Date: Mon, 2 Feb 2026 04:51:07 -0300 Subject: [PATCH] Add option for Path3D to snap to colliders --- editor/scene/3d/path_3d_editor_plugin.cpp | 93 ++++++++++++++++++++++- editor/scene/3d/path_3d_editor_plugin.h | 20 ++++- 2 files changed, 111 insertions(+), 2 deletions(-) diff --git a/editor/scene/3d/path_3d_editor_plugin.cpp b/editor/scene/3d/path_3d_editor_plugin.cpp index 28e0fa7ce17..cc3caea155d 100644 --- a/editor/scene/3d/path_3d_editor_plugin.cpp +++ b/editor/scene/3d/path_3d_editor_plugin.cpp @@ -114,6 +114,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(); @@ -669,10 +692,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")); @@ -748,6 +782,7 @@ void Path3DEditorPlugin::make_visible(bool p_visible) { topmenu_bar->hide(); path = nullptr; } + set_physics_process(p_visible); } void Path3DEditorPlugin::_mode_changed(int p_mode) { @@ -791,6 +826,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; } } @@ -884,6 +924,55 @@ void Path3DEditorPlugin::_notification(int p_what) { 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; + } + } } } @@ -989,6 +1078,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); diff --git a/editor/scene/3d/path_3d_editor_plugin.h b/editor/scene/3d/path_3d_editor_plugin.h index 7a6bff07ff1..7688fd919d2 100644 --- a/editor/scene/3d/path_3d_editor_plugin.h +++ b/editor/scene/3d/path_3d_editor_plugin.h @@ -148,6 +148,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(); @@ -157,7 +158,8 @@ class Path3DEditorPlugin : public EditorPlugin { enum HandleOption { HANDLE_OPTION_ANGLE, - HANDLE_OPTION_LENGTH + HANDLE_OPTION_LENGTH, + HANDLE_OPTION_SNAP_COLLIDER, }; protected: @@ -182,4 +184,20 @@ public: 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; };