diff --git a/editor/scene/3d/node_3d_editor_plugin.cpp b/editor/scene/3d/node_3d_editor_plugin.cpp index dbe5e5e16855..344f5bcd9418 100644 --- a/editor/scene/3d/node_3d_editor_plugin.cpp +++ b/editor/scene/3d/node_3d_editor_plugin.cpp @@ -625,6 +625,10 @@ void Node3DEditorViewport::cancel_transform() { sp->set_global_transform(se->original); } + for (const KeyValue &pair : _edit.children_original_globals) { + pair.key->set_global_transform(pair.value); + } + collision_reposition = false; finish_transform(); set_message(TTRC("Transform Aborted."), 3); @@ -1136,6 +1140,24 @@ void Node3DEditorViewport::_compute_edit(const Point2 &p_point) { sel_item->original = sel_item->sp->get_global_gizmo_transform(); } } + + if (spatial_editor->is_preserve_children_transform_enabled() && _edit.children_original_globals.is_empty()) { + const List &selection = editor_selection->get_top_selected_node_list(); + for (Node *E : selection) { + Node3D *sp = Object::cast_to(E); + if (!sp) { + continue; + } + + int child_count = sp->get_child_count(); + for (int i = 0; i < child_count; i++) { + Node3D *child = Object::cast_to(sp->get_child(i)); + if (child) { + _edit.children_original_globals[child] = child->get_global_transform(); + } + } + } + } } static Key _get_key_modifier_setting(const String &p_property) { @@ -1437,11 +1459,33 @@ void Node3DEditorViewport::_transform_gizmo_apply(Node3D *p_node, const Transfor return; } + bool preserve_children = spatial_editor->is_preserve_children_transform_enabled(); + + Vector children_global_transforms; + Vector node3d_children; + + if (preserve_children) { + int child_count = p_node->get_child_count(); + for (int i = 0; i < child_count; i++) { + Node3D *child = Object::cast_to(p_node->get_child(i)); + if (child) { + children_global_transforms.push_back(child->get_global_transform()); + node3d_children.push_back(child); + } + } + } + if (p_local) { p_node->set_transform(p_transform); } else { p_node->set_global_transform(p_transform); } + + if (preserve_children) { + for (int i = 0; i < node3d_children.size(); i++) { + node3d_children[i]->set_global_transform(children_global_transforms[i]); + } + } } Transform3D Node3DEditorViewport::_compute_transform(TransformMode p_mode, const Transform3D &p_original, const Transform3D &p_original_local, Vector3 p_motion, double p_extra, bool p_local, bool p_orthogonal, bool p_view_axis) { @@ -5160,6 +5204,8 @@ void Node3DEditorViewport::drop_data_fw(const Point2 &p_point, const Variant &p_ void Node3DEditorViewport::begin_transform(TransformMode p_mode, bool instant) { if (get_selected_count() > 0) { + _edit.children_original_globals.clear(); + _edit.mode = p_mode; _compute_edit(_edit.mouse_pos); _edit.instant = instant; @@ -5218,6 +5264,18 @@ void Node3DEditorViewport::commit_transform() { undo_redo->add_do_method(sp, "set_transform", sp->get_local_gizmo_transform()); undo_redo->add_undo_method(sp, "set_transform", se->original_local); } + + if (!_edit.children_original_globals.is_empty()) { + for (const KeyValue &pair : _edit.children_original_globals) { + Node3D *child = pair.key; + Transform3D original_global = pair.value; + Transform3D current_global = child->get_global_transform(); + + undo_redo->add_do_method(child, "set_global_transform", current_global); + undo_redo->add_undo_method(child, "set_global_transform", original_global); + } + } + undo_redo->commit_action(); collision_reposition = false; @@ -5646,6 +5704,7 @@ void Node3DEditorViewport::finish_transform() { _edit.accumulated_rotation_angle = 0.0; _edit.rotation_angle = 0.0; _edit.gizmo_initiated = false; + _edit.children_original_globals.clear(); spatial_editor->set_local_coords_enabled(_edit.original_local); spatial_editor->update_transform_gizmo(); surface->queue_redraw(); @@ -6692,6 +6751,7 @@ Dictionary Node3DEditor::get_state() const { d["scale_snap"] = snap_scale_value; d["local_coords"] = tool_option_button[TOOL_OPT_LOCAL_COORDS]->is_pressed(); + d["preserve_children_transform"] = tool_option_button[TOOL_OPT_PRESERVE_CHILDREN_TRANSFORM]->is_pressed(); int vc = 0; if (view_layout_menu->get_popup()->is_item_checked(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_1_VIEWPORT))) { @@ -6786,6 +6846,10 @@ void Node3DEditor::set_state(const Dictionary &p_state) { _snap_update(); + if (d.has("preserve_children_transform")) { + tool_option_button[TOOL_OPT_PRESERVE_CHILDREN_TRANSFORM]->set_pressed(d["preserve_children_transform"]); + } + if (d.has("local_coords")) { tool_option_button[TOOL_OPT_LOCAL_COORDS]->set_pressed(d["local_coords"]); update_transform_gizmo(); @@ -7028,6 +7092,47 @@ void Node3DEditor::_menu_item_toggled(bool pressed, int p_option) { viewports[i]->update_transform_gizmo_highlight(); } } break; + + case MENU_TOOL_PRESERVE_CHILDREN_TRANSFORM: { + tool_option_button[TOOL_OPT_PRESERVE_CHILDREN_TRANSFORM]->set_pressed(pressed); + if (pressed) { + EditorNode::get_editor_data().add_undo_redo_inspector_hook_callback(callable_mp(this, &Node3DEditor::_undo_redo_inspector_callback)); + } else { + EditorNode::get_editor_data().remove_undo_redo_inspector_hook_callback(callable_mp(this, &Node3DEditor::_undo_redo_inspector_callback)); + } + } break; + } +} + +void Node3DEditor::_undo_redo_inspector_callback(Object *p_undo_redo, Object *p_edited, const String &p_property, const Variant &p_new_value) { + Node3D *node = Object::cast_to(p_edited); + if (!node) { + return; + } + + static const char *transform_properties[] = { "position", "rotation", "scale", "quaternion", "basis", "transform", nullptr }; + bool is_transform_prop = false; + for (int i = 0; transform_properties[i]; i++) { + if (p_property == transform_properties[i]) { + is_transform_prop = true; + break; + } + } + if (!is_transform_prop) { + return; + } + + EditorUndoRedoManager *undo_redo = Object::cast_to(p_undo_redo); + ERR_FAIL_NULL(undo_redo); + + int child_count = node->get_child_count(); + for (int i = 0; i < child_count; i++) { + Node3D *child = Object::cast_to(node->get_child(i)); + if (child) { + Transform3D child_global = child->get_global_transform(); + undo_redo->add_do_method(child, "set_global_transform", child_global); + undo_redo->add_undo_method(child, "set_global_transform", child_global); + } } } @@ -8619,6 +8724,7 @@ void Node3DEditor::_update_theme() { tool_option_button[TOOL_OPT_LOCAL_COORDS]->set_button_icon(get_editor_theme_icon(SNAME("Object"))); tool_option_button[TOOL_OPT_USE_SNAP]->set_button_icon(get_editor_theme_icon(SNAME("Snap"))); tool_option_button[TOOL_OPT_USE_TRACKBALL]->set_button_icon(get_editor_theme_icon(SNAME("Trackball"))); + tool_option_button[TOOL_OPT_PRESERVE_CHILDREN_TRANSFORM]->set_button_icon(get_editor_theme_icon(SNAME("Pin"))); view_layout_menu->get_popup()->set_item_icon(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_1_VIEWPORT), get_editor_theme_icon(SNAME("Panels1"))); view_layout_menu->get_popup()->set_item_icon(view_layout_menu->get_popup()->get_item_index(MENU_VIEW_USE_2_VIEWPORTS), get_editor_theme_icon(SNAME("Panels2"))); @@ -9596,6 +9702,16 @@ Node3DEditor::Node3DEditor() { tool_option_button[TOOL_OPT_USE_TRACKBALL]->set_shortcut_context(this); tool_option_button[TOOL_OPT_USE_TRACKBALL]->set_accessibility_name(TTRC("Use Trackball")); + tool_option_button[TOOL_OPT_PRESERVE_CHILDREN_TRANSFORM] = memnew(Button); + main_menu_hbox->add_child(tool_option_button[TOOL_OPT_PRESERVE_CHILDREN_TRANSFORM]); + tool_option_button[TOOL_OPT_PRESERVE_CHILDREN_TRANSFORM]->set_toggle_mode(true); + tool_option_button[TOOL_OPT_PRESERVE_CHILDREN_TRANSFORM]->set_theme_type_variation(SceneStringName(FlatButton)); + tool_option_button[TOOL_OPT_PRESERVE_CHILDREN_TRANSFORM]->connect(SceneStringName(toggled), callable_mp(this, &Node3DEditor::_menu_item_toggled).bind(MENU_TOOL_PRESERVE_CHILDREN_TRANSFORM)); + tool_option_button[TOOL_OPT_PRESERVE_CHILDREN_TRANSFORM]->set_shortcut(ED_SHORTCUT("spatial_editor/preserve_children_transform", TTRC("Preserve Children Transform"), Key::P)); + tool_option_button[TOOL_OPT_PRESERVE_CHILDREN_TRANSFORM]->set_shortcut_context(this); + tool_option_button[TOOL_OPT_PRESERVE_CHILDREN_TRANSFORM]->set_accessibility_name(TTRC("Preserve Children Transform")); + tool_option_button[TOOL_OPT_PRESERVE_CHILDREN_TRANSFORM]->set_tooltip_text(TTRC("When enabled, transforming a node will preserve the global transform of its children.\nThis also applies when editing transform properties in the Inspector.")); + main_menu_hbox->add_child(memnew(VSeparator)); sun_button = memnew(Button); sun_button->set_tooltip_text(TTRC("Toggle preview sunlight.\nIf a DirectionalLight3D node is added to the scene, preview sunlight is disabled.")); diff --git a/editor/scene/3d/node_3d_editor_plugin.h b/editor/scene/3d/node_3d_editor_plugin.h index e07314b7d3b1..d779c831c1af 100644 --- a/editor/scene/3d/node_3d_editor_plugin.h +++ b/editor/scene/3d/node_3d_editor_plugin.h @@ -356,6 +356,8 @@ class Node3DEditorViewport : public Control { Vector3 initial_click_vector; Vector3 previous_rotation_vector; bool gizmo_initiated = false; + + HashMap children_original_globals; } _edit; Ref view_3d_controller; @@ -583,6 +585,7 @@ class Node3DEditor : public VBoxContainer { TOOL_OPT_LOCAL_COORDS, TOOL_OPT_USE_SNAP, TOOL_OPT_USE_TRACKBALL, + TOOL_OPT_PRESERVE_CHILDREN_TRANSFORM, TOOL_OPT_MAX }; @@ -686,6 +689,7 @@ class Node3DEditor : public VBoxContainer { MENU_TOOL_LOCAL_COORDS, MENU_TOOL_USE_SNAP, MENU_TOOL_USE_TRACKBALL, + MENU_TOOL_PRESERVE_CHILDREN_TRANSFORM, MENU_TRANSFORM_CONFIGURE_SNAP, MENU_TRANSFORM_DIALOG, MENU_VIEW_USE_1_VIEWPORT, @@ -877,6 +881,8 @@ class Node3DEditor : public VBoxContainer { void _update_theme(); + void _undo_redo_inspector_callback(Object *p_undo_redo, Object *p_edited, const String &p_property, const Variant &p_new_value); + protected: void _notification(int p_what); //void _gui_input(InputEvent p_event); @@ -901,6 +907,7 @@ class Node3DEditor : public VBoxContainer { ToolMode get_tool_mode() const { return tool_mode; } bool are_local_coords_enabled() const { return tool_option_button[Node3DEditor::TOOL_OPT_LOCAL_COORDS]->is_pressed(); } void set_local_coords_enabled(bool on) const { tool_option_button[Node3DEditor::TOOL_OPT_LOCAL_COORDS]->set_pressed(on); } + bool is_preserve_children_transform_enabled() const { return tool_option_button[Node3DEditor::TOOL_OPT_PRESERVE_CHILDREN_TRANSFORM]->is_pressed(); } bool is_snap_enabled() const { return snap_enabled ^ snap_key_enabled; } real_t get_translate_snap() const; real_t get_rotate_snap() const; diff --git a/modules/csg/csg_shape.cpp b/modules/csg/csg_shape.cpp index 8119666f44fa..7f141df07672 100644 --- a/modules/csg/csg_shape.cpp +++ b/modules/csg/csg_shape.cpp @@ -801,6 +801,8 @@ void CSGShape3D::update_shape() { set_base(root_mesh->get_rid()); + update_gizmos(); + #ifndef PHYSICS_3D_DISABLED _update_collision_faces(); #endif // PHYSICS_3D_DISABLED