Merge pull request #87901 from pohy/feat/use-subgizmos-for-path3d-position-editing

Use subgizmos for Path3D position editing and declutter the UI
This commit is contained in:
Rémi Verschelde 2024-02-22 23:34:26 +01:00
commit 031ca87d83
No known key found for this signature in database
GPG Key ID: C3336907360768E1
4 changed files with 196 additions and 44 deletions

View File

@ -646,6 +646,18 @@ void Node3DEditorViewport::cancel_transform() {
continue;
}
if (se && se->gizmo.is_valid()) {
Vector<int> ids;
Vector<Transform3D> restore;
for (const KeyValue<int, Transform3D> &GE : se->subgizmos) {
ids.push_back(GE.key);
restore.push_back(GE.value);
}
se->gizmo->commit_subgizmos(ids, restore, true);
}
sp->set_global_transform(se->original);
}
@ -8071,6 +8083,7 @@ void Node3DEditor::_bind_methods() {
ClassDB::bind_method("_refresh_menu_icons", &Node3DEditor::_refresh_menu_icons);
ClassDB::bind_method("update_all_gizmos", &Node3DEditor::update_all_gizmos);
ClassDB::bind_method("update_transform_gizmo", &Node3DEditor::update_transform_gizmo);
ADD_SIGNAL(MethodInfo("transform_key_request"));
ADD_SIGNAL(MethodInfo("item_lock_status_changed"));

View File

@ -509,7 +509,7 @@ public:
RID sbox_instance_xray;
RID sbox_instance_xray_offset;
Ref<EditorNode3DGizmo> gizmo;
HashMap<int, Transform3D> subgizmos; // map ID -> initial transform
HashMap<int, Transform3D> subgizmos; // Key: Subgizmo ID, Value: Initial subgizmo transform.
Node3DEditorSelectedItem() {
sp = nullptr;

View File

@ -200,22 +200,6 @@ void Path3DGizmo::commit_handle(int p_id, bool p_secondary, const Variant &p_res
EditorUndoRedoManager *ur = EditorUndoRedoManager::get_singleton();
// Primary handles: position.
if (!p_secondary) {
// Special cas for primary handle, the handle id equals control point id.
const int idx = p_id;
if (p_cancel) {
c->set_point_position(idx, p_restore);
return;
}
ur->create_action(TTR("Set Curve Point Position"));
ur->add_do_method(c.ptr(), "set_point_position", idx, c->get_point_position(idx));
ur->add_undo_method(c.ptr(), "set_point_position", idx, p_restore);
ur->commit_action();
return;
}
// Secondary handles: in, out, tilt.
const HandleInfo info = _secondary_handles_info[p_id];
const int idx = info.point_idx;
@ -235,6 +219,7 @@ void Path3DGizmo::commit_handle(int p_id, bool p_secondary, const Variant &p_res
ur->add_do_method(c.ptr(), "set_point_in", idx, Path3DEditorPlugin::singleton->mirror_length_enabled() ? -c->get_point_out(idx) : (-c->get_point_out(idx).normalized() * orig_in_length));
ur->add_undo_method(c.ptr(), "set_point_in", idx, Path3DEditorPlugin::singleton->mirror_length_enabled() ? -static_cast<Vector3>(p_restore) : (-static_cast<Vector3>(p_restore).normalized() * orig_in_length));
}
ur->commit_action();
break;
}
@ -252,6 +237,7 @@ void Path3DGizmo::commit_handle(int p_id, bool p_secondary, const Variant &p_res
ur->add_do_method(c.ptr(), "set_point_out", idx, Path3DEditorPlugin::singleton->mirror_length_enabled() ? -c->get_point_in(idx) : (-c->get_point_in(idx).normalized() * orig_out_length));
ur->add_undo_method(c.ptr(), "set_point_out", idx, Path3DEditorPlugin::singleton->mirror_length_enabled() ? -static_cast<Vector3>(p_restore) : (-static_cast<Vector3>(p_restore).normalized() * orig_out_length));
}
ur->commit_action();
break;
}
@ -263,6 +249,7 @@ void Path3DGizmo::commit_handle(int p_id, bool p_secondary, const Variant &p_res
ur->create_action(TTR("Set Curve Point Tilt"));
ur->add_do_method(c.ptr(), "set_point_tilt", idx, c->get_point_tilt(idx));
ur->add_undo_method(c.ptr(), "set_point_tilt", idx, p_restore);
ur->commit_action();
break;
}
@ -275,7 +262,7 @@ void Path3DGizmo::redraw() {
Ref<StandardMaterial3D> path_material = gizmo_plugin->get_material("path_material", this);
Ref<StandardMaterial3D> path_thin_material = gizmo_plugin->get_material("path_thin_material", this);
Ref<StandardMaterial3D> path_tilt_material = gizmo_plugin->get_material("path_tilt_material", this);
Ref<StandardMaterial3D> handles_material = gizmo_plugin->get_material("handles");
Ref<StandardMaterial3D> path_tilt_muted_material = gizmo_plugin->get_material("path_tilt_muted_material", this);
Ref<StandardMaterial3D> sec_handles_material = gizmo_plugin->get_material("sec_handles");
Ref<Curve3D> c = path->get_curve();
@ -353,48 +340,57 @@ void Path3DGizmo::redraw() {
if (Path3DEditorPlugin::singleton->get_edited_path() == path) {
PackedVector3Array handle_lines;
PackedVector3Array tilt_handle_lines;
PackedVector3Array primary_handle_points;
PackedVector3Array secondary_handle_points;
PackedInt32Array collected_secondary_handle_ids; // Avoid shadowing member on Node3DEditorGizmo.
_secondary_handles_info.resize(c->get_point_count() * 3);
for (int idx = 0; idx < c->get_point_count(); idx++) {
// Collect primary-handles.
const Vector3 pos = c->get_point_position(idx);
primary_handle_points.append(pos);
bool is_current_point_selected = is_subgizmo_selected(idx);
bool is_previous_point_selected = is_subgizmo_selected(idx - 1);
bool is_following_point_selected = is_subgizmo_selected(idx + 1);
HandleInfo info;
info.point_idx = idx;
// Collect in-handles except for the first point.
if (idx > 0) {
info.type = HandleType::HANDLE_TYPE_IN;
const int handle_idx = idx * 3 + 0;
collected_secondary_handle_ids.append(handle_idx);
_secondary_handles_info.write[handle_idx] = info;
if (idx > 0 && (is_current_point_selected || is_previous_point_selected)) {
const Vector3 in = c->get_point_in(idx);
secondary_handle_points.append(pos + in);
handle_lines.append(pos);
handle_lines.append(pos + in);
// Display in-handles only when they are "initialized".
if (in.length_squared() > 0) {
info.type = HandleType::HANDLE_TYPE_IN;
const int handle_idx = idx * 3 + 0;
collected_secondary_handle_ids.append(handle_idx);
_secondary_handles_info.write[handle_idx] = info;
secondary_handle_points.append(pos + in);
handle_lines.append(pos);
handle_lines.append(pos + in);
}
}
// Collect out-handles except for the last point.
if (idx < c->get_point_count() - 1) {
info.type = HandleType::HANDLE_TYPE_OUT;
const int handle_idx = idx * 3 + 1;
collected_secondary_handle_ids.append(handle_idx);
_secondary_handles_info.write[handle_idx] = info;
if (idx < c->get_point_count() - 1 && (is_current_point_selected || is_following_point_selected)) {
const Vector3 out = c->get_point_out(idx);
secondary_handle_points.append(pos + out);
handle_lines.append(pos);
handle_lines.append(pos + out);
// Display out-handles only when they are "initialized".
if (out.length_squared() > 0) {
info.type = HandleType::HANDLE_TYPE_OUT;
const int handle_idx = idx * 3 + 1;
collected_secondary_handle_ids.append(handle_idx);
_secondary_handles_info.write[handle_idx] = info;
secondary_handle_points.append(pos + out);
handle_lines.append(pos);
handle_lines.append(pos + out);
}
}
// Collect tilt-handles.
{
if (is_current_point_selected || is_previous_point_selected || is_following_point_selected) {
// Tilt handle.
{
info.type = HandleType::HANDLE_TYPE_TILT;
const int handle_idx = idx * 3 + 2;
@ -423,7 +419,7 @@ void Path3DGizmo::redraw() {
const Vector3 edge = sin(a) * side + cos(a) * up;
disk.append(pos + edge * disk_size);
}
add_vertices(disk, path_tilt_material, Mesh::PRIMITIVE_LINE_STRIP);
add_vertices(disk, is_current_point_selected ? path_tilt_material : path_tilt_muted_material, Mesh::PRIMITIVE_LINE_STRIP);
}
}
}
@ -436,21 +432,27 @@ void Path3DGizmo::redraw() {
add_lines(tilt_handle_lines, path_tilt_material);
}
if (primary_handle_points.size()) {
add_handles(primary_handle_points, handles_material);
}
if (secondary_handle_points.size()) {
add_handles(secondary_handle_points, sec_handles_material, collected_secondary_handle_ids, false, true);
}
// Draw the gizmo plugin manually, because handles are registered. In which case, the caller code skips drawing the gizmo plugin.
gizmo_plugin->redraw(this);
}
}
void Path3DGizmo::_update_transform_gizmo() {
Node3DEditor::get_singleton()->update_transform_gizmo();
}
Path3DGizmo::Path3DGizmo(Path3D *p_path, float p_disk_size) {
path = p_path;
disk_size = p_disk_size;
set_node_3d(p_path);
orig_in_length = 0;
orig_out_length = 0;
// Connecting to a signal once, rather than plaguing the implementation with calls to `Node3DEditor::update_transform_gizmo`.
path->connect("curve_changed", callable_mp(this, &Path3DGizmo::_update_transform_gizmo));
}
EditorPlugin::AfterGUIInput Path3DEditorPlugin::forward_3d_gui_input(Camera3D *p_camera, const Ref<InputEvent> &p_event) {
@ -819,10 +821,130 @@ Ref<EditorNode3DGizmo> Path3DGizmoPlugin::create_gizmo(Node3D *p_spatial) {
return ref;
}
bool Path3DGizmoPlugin::has_gizmo(Node3D *p_spatial) {
return Object::cast_to<Path3D>(p_spatial) != nullptr;
}
String Path3DGizmoPlugin::get_gizmo_name() const {
return "Path3D";
}
void Path3DGizmoPlugin::redraw(EditorNode3DGizmo *p_gizmo) {
Path3D *path = Object::cast_to<Path3D>(p_gizmo->get_node_3d());
ERR_FAIL_NULL(path);
Ref<Curve3D> curve = path->get_curve();
Ref<StandardMaterial3D> handle_material = get_material("handles", p_gizmo);
PackedVector3Array handles;
for (int idx = 0; idx < curve->get_point_count(); ++idx) {
// Collect handles.
const Vector3 pos = curve->get_point_position(idx);
handles.append(pos);
}
if (handles.size()) {
p_gizmo->add_vertices(handles, handle_material, Mesh::PRIMITIVE_POINTS);
}
}
int Path3DGizmoPlugin::subgizmos_intersect_ray(const EditorNode3DGizmo *p_gizmo, Camera3D *p_camera, const Vector2 &p_point) const {
Path3D *path = Object::cast_to<Path3D>(p_gizmo->get_node_3d());
ERR_FAIL_NULL_V(path, -1);
Ref<Curve3D> curve = path->get_curve();
ERR_FAIL_COND_V(curve.is_null(), -1);
for (int idx = 0; idx < curve->get_point_count(); ++idx) {
Vector3 pos = path->get_global_transform().xform(curve->get_point_position(idx));
if (p_camera->unproject_position(pos).distance_to(p_point) < 20) {
return idx;
}
}
return -1;
}
Vector<int> Path3DGizmoPlugin::subgizmos_intersect_frustum(const EditorNode3DGizmo *p_gizmo, const Camera3D *p_camera, const Vector<Plane> &p_frustum) const {
Vector<int> contained_points;
Path3D *path = Object::cast_to<Path3D>(p_gizmo->get_node_3d());
ERR_FAIL_NULL_V(path, contained_points);
Ref<Curve3D> curve = path->get_curve();
ERR_FAIL_COND_V(curve.is_null(), contained_points);
for (int idx = 0; idx < curve->get_point_count(); ++idx) {
Vector3 pos = path->get_global_transform().xform(curve->get_point_position(idx));
bool is_contained_in_frustum = true;
for (int i = 0; i < p_frustum.size(); ++i) {
if (p_frustum[i].distance_to(pos) > 0) {
is_contained_in_frustum = false;
break;
}
}
if (is_contained_in_frustum) {
contained_points.push_back(idx);
}
}
return contained_points;
}
Transform3D Path3DGizmoPlugin::get_subgizmo_transform(const EditorNode3DGizmo *p_gizmo, int p_id) const {
Path3D *path = Object::cast_to<Path3D>(p_gizmo->get_node_3d());
ERR_FAIL_NULL_V(path, Transform3D());
Ref<Curve3D> curve = path->get_curve();
ERR_FAIL_COND_V(curve.is_null(), Transform3D());
ERR_FAIL_INDEX_V(p_id, curve->get_point_count(), Transform3D());
Basis basis = transformation_locked_basis.has(p_id) ? transformation_locked_basis[p_id] : curve->get_point_baked_posture(p_id, true);
Vector3 pos = curve->get_point_position(p_id);
Transform3D t = Transform3D(basis, pos);
return t;
}
void Path3DGizmoPlugin::set_subgizmo_transform(const EditorNode3DGizmo *p_gizmo, int p_id, Transform3D p_transform) {
Path3D *path = Object::cast_to<Path3D>(p_gizmo->get_node_3d());
ERR_FAIL_NULL(path);
Ref<Curve3D> curve = path->get_curve();
ERR_FAIL_COND(curve.is_null());
ERR_FAIL_INDEX(p_id, curve->get_point_count());
if (!transformation_locked_basis.has(p_id)) {
transformation_locked_basis[p_id] = Basis(curve->get_point_baked_posture(p_id, true));
}
curve->set_point_position(p_id, p_transform.origin);
}
void Path3DGizmoPlugin::commit_subgizmos(const EditorNode3DGizmo *p_gizmo, const Vector<int> &p_ids, const Vector<Transform3D> &p_restore, bool p_cancel) {
Path3D *path = Object::cast_to<Path3D>(p_gizmo->get_node_3d());
ERR_FAIL_NULL(path);
Ref<Curve3D> curve = path->get_curve();
ERR_FAIL_COND(curve.is_null());
transformation_locked_basis.clear();
if (p_cancel) {
for (int i = 0; i < p_ids.size(); ++i) {
curve->set_point_position(p_ids[i], p_restore[i].origin);
}
return;
}
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
undo_redo->create_action(TTR("Set Curve Point Position"));
for (int i = 0; i < p_ids.size(); ++i) {
const int idx = p_ids[i];
undo_redo->add_do_method(curve.ptr(), "set_point_position", idx, curve->get_point_position(idx));
undo_redo->add_undo_method(curve.ptr(), "set_point_position", idx, p_restore[i].origin);
}
undo_redo->commit_action();
}
int Path3DGizmoPlugin::get_priority() const {
return -1;
}
@ -835,6 +957,7 @@ Path3DGizmoPlugin::Path3DGizmoPlugin(float p_disk_size) {
create_material("path_material", path_color);
create_material("path_thin_material", Color(0.6, 0.6, 0.6));
create_material("path_tilt_material", path_tilt_color);
create_material("path_tilt_muted_material", path_tilt_color * 0.7);
create_handle_material("handles", false, EditorNode::get_singleton()->get_editor_theme()->get_icon(SNAME("EditorPathSmoothHandle"), EditorStringName(EditorIcons)));
create_handle_material("sec_handles", false, EditorNode::get_singleton()->get_editor_theme()->get_icon(SNAME("EditorCurveHandle"), EditorStringName(EditorIcons)));
}

View File

@ -63,6 +63,8 @@ class Path3DGizmo : public EditorNode3DGizmo {
// Cache information of secondary handles.
Vector<HandleInfo> _secondary_handles_info;
void _update_transform_gizmo();
public:
virtual String get_handle_name(int p_id, bool p_secondary) const override;
virtual Variant get_handle_value(int p_id, bool p_secondary) const override;
@ -78,11 +80,25 @@ class Path3DGizmoPlugin : public 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<int, Basis> transformation_locked_basis;
protected:
Ref<EditorNode3DGizmo> create_gizmo(Node3D *p_spatial) override;
public:
virtual bool has_gizmo(Node3D *p_spatial) override;
String get_gizmo_name() const override;
virtual void redraw(EditorNode3DGizmo *p_gizmo) override;
virtual int subgizmos_intersect_ray(const EditorNode3DGizmo *p_gizmo, Camera3D *p_camera, const Vector2 &p_point) const override;
virtual Vector<int> subgizmos_intersect_frustum(const EditorNode3DGizmo *p_gizmo, const Camera3D *p_camera, const Vector<Plane> &p_frustum) const override;
virtual Transform3D get_subgizmo_transform(const EditorNode3DGizmo *p_gizmo, int p_id) const override;
virtual void set_subgizmo_transform(const EditorNode3DGizmo *p_gizmo, int p_id, Transform3D p_transform) override;
virtual void commit_subgizmos(const EditorNode3DGizmo *p_gizmo, const Vector<int> &p_ids, const Vector<Transform3D> &p_restore, bool p_cancel = false) override;
int get_priority() const override;
Path3DGizmoPlugin(float p_disk_size);
};