Repository: godotengine/godot
File: editor/plugins/node_3d_editor_plugin.cpp (C++)

Code before:

	collision_reposition = false;
	finish_transform();
	set_message("");
}

void Node3DEditorViewport::apply_transform(Vector3 p_motion, double p_snap) {
	bool local_coords = (spatial_editor->are_local_coords_enabled() && _edit.plane != TRANSFORM_VIEW);
	List<Node *> &selection = editor_selection->get_selected_node_list();
	Basis first_basis;

	if (selection.is_empty()) {
		return;
	}

	Node3D *first_node_3d = nullptr;
	for (Node *node : selection) {
		first_node_3d = Object::cast_to<Node3D>(node);
		if (first_node_3d) {
			break;
		}
	}

	if (!first_node_3d) {
		return;
	}

	first_basis = first_node_3d->get_global_basis();

	for (Node *E : selection) {
		Node3D *sp = Object::cast_to<Node3D>(E);
		Basis relative_basis;
		bool is_relative = false;

		if (!sp) {
			continue;
		}

		Node3DEditorSelectedItem *se = editor_selection->get_node_editor_data<Node3DEditorSelectedItem>(sp);
		if (!se) {
			continue;
		}

		if (sp->has_meta("_edit_lock_")) {
			continue;
		}

		if (local_coords && first_node_3d != sp) {
			relative_basis = first_basis;
			is_relative = true;
		}

Review comment (suggestion):
As I said above, “in a way that the user can explicitly handle it”, some functionality needs to be added to support this.

The current PR is rather hacky and not so usable since it relies on only item order, and if we are going to go Blender-like, we will need some new way to do it, such as Shift+click to select the leader.

Also probably we should send a proposal for that first, though. https://github.com/godotengine/godot-proposals

Code after:
		if (sp->has_meta("_edit_lock_")) {
			continue;
		}

		if (se->gizmo.is_valid()) {
			for (KeyValue<int, Transform3D> &GE : se->subgizmos) {
				Transform3D xform = GE.value;
				Transform3D new_xform = _compute_transform(_edit.mode, se->original * xform, xform, p_motion, p_snap, local_coords, _edit.plane != TRANSFORM_VIEW); // Force orthogonal with subgizmo.
				if (!local_coords) {
					new_xform = se->original.affine_inverse() * new_xform;
				}
				se->gizmo->set_subgizmo_transform(GE.key, new_xform);
			}
		} else {
			Transform3D new_xform = _compute_transform(_edit.mode, se->original, se->original_local, p_motion, p_snap, local_coords, sp->get_rotation_edit_mode() != Node3D::ROTATION_EDIT_MODE_BASIS && _edit.plane != TRANSFORM_VIEW);
			_transform_gizmo_apply(se->sp, new_xform, local_coords);
		}
	}

	spatial_editor->update_transform_gizmo();
	surface->queue_redraw();
}

// Update the current transform operation in response to an input.
void Node3DEditorViewport::update_transform(bool p_shift) {
	Vector3 ray_pos = get_ray_pos(_edit.mouse_pos);
	Vector3 ray = get_ray(_edit.mouse_pos);
	double snap = EDITOR_GET("interface/inspector/default_float_step");
	int snap_step_decimals = Math::range_step_decimals(snap);

	switch (_edit.mode) {
		case TRANSFORM_SCALE: {
			Vector3 motion_mask;
			Plane plane;
			bool plane_mv = false;

			switch (_edit.plane) {
				case TRANSFORM_VIEW:
					motion_mask = Vector3(0, 0, 0);
					plane = Plane(_get_camera_normal(), _edit.center);
					break;
				case TRANSFORM_X_AXIS:
					motion_mask = spatial_editor->get_gizmo_transform().basis.get_column(0).normalized();
					plane = Plane(motion_mask.cross(motion_mask.cross(_get_camera_normal())).normalized(), _edit.center);
					break;
				case TRANSFORM_Y_AXIS:
					motion_mask = spatial_editor->get_gizmo_transform().basis.get_column(1).normalized();
					plane = Plane(motion_mask.cross(motion_mask.cross(_get_camera_normal())).normalized(), _edit.center);
					break;
				case TRANSFORM_Z_AXIS:
					motion_mask = spatial_editor->get_gizmo_transform().basis.get_column(2).normalized();