1/**************************************************************************/
2/* path_3d_editor_plugin.cpp */
3/**************************************************************************/
4/* This file is part of: */
5/* GODOT ENGINE */
6/* https://godotengine.org */
7/**************************************************************************/
8/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
9/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
10/* */
11/* Permission is hereby granted, free of charge, to any person obtaining */
12/* a copy of this software and associated documentation files (the */
13/* "Software"), to deal in the Software without restriction, including */
14/* without limitation the rights to use, copy, modify, merge, publish, */
15/* distribute, sublicense, and/or sell copies of the Software, and to */
16/* permit persons to whom the Software is furnished to do so, subject to */
17/* the following conditions: */
18/* */
19/* The above copyright notice and this permission notice shall be */
20/* included in all copies or substantial portions of the Software. */
21/* */
22/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
23/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
24/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
25/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
26/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
27/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
28/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
29/**************************************************************************/
30
31#include "path_3d_editor_plugin.h"
32
33#include "core/math/geometry_2d.h"
34#include "core/math/geometry_3d.h"
35#include "core/os/keyboard.h"
36#include "editor/editor_node.h"
37#include "editor/editor_settings.h"
38#include "editor/editor_string_names.h"
39#include "editor/editor_undo_redo_manager.h"
40#include "node_3d_editor_plugin.h"
41#include "scene/gui/menu_button.h"
42#include "scene/resources/curve.h"
43
44String Path3DGizmo::get_handle_name(int p_id, bool p_secondary) const {
45 Ref<Curve3D> c = path->get_curve();
46 if (c.is_null()) {
47 return "";
48 }
49
50 // Primary handles: position.
51 if (!p_secondary) {
52 return TTR("Curve Point #") + itos(p_id);
53 }
54
55 // Secondary handles: in, out, tilt.
56 const HandleInfo info = _secondary_handles_info[p_id];
57 switch (info.type) {
58 case HandleType::HANDLE_TYPE_IN:
59 return TTR("Handle In #") + itos(info.point_idx);
60 case HandleType::HANDLE_TYPE_OUT:
61 return TTR("Handle Out #") + itos(info.point_idx);
62 case HandleType::HANDLE_TYPE_TILT:
63 return TTR("Handle Tilt #") + itos(info.point_idx);
64 }
65
66 return "";
67}
68
69Variant Path3DGizmo::get_handle_value(int p_id, bool p_secondary) const {
70 Ref<Curve3D> c = path->get_curve();
71 if (c.is_null()) {
72 return Variant();
73 }
74
75 // Primary handles: position.
76 if (!p_secondary) {
77 original = c->get_point_position(p_id);
78 return original;
79 }
80
81 // Secondary handles: in, out, tilt.
82 const HandleInfo info = _secondary_handles_info[p_id];
83 Vector3 ofs;
84 switch (info.type) {
85 case HandleType::HANDLE_TYPE_TILT:
86 return c->get_point_tilt(info.point_idx);
87 case HandleType::HANDLE_TYPE_IN:
88 ofs = c->get_point_in(info.point_idx);
89 break;
90 case HandleType::HANDLE_TYPE_OUT:
91 ofs = c->get_point_out(info.point_idx);
92 break;
93 }
94
95 original = ofs + c->get_point_position(info.point_idx);
96 return ofs;
97}
98
99void Path3DGizmo::set_handle(int p_id, bool p_secondary, Camera3D *p_camera, const Point2 &p_point) {
100 Ref<Curve3D> c = path->get_curve();
101 if (c.is_null()) {
102 return;
103 }
104
105 const Transform3D gt = path->get_global_transform();
106 const Transform3D gi = gt.affine_inverse();
107 const Vector3 ray_from = p_camera->project_ray_origin(p_point);
108 const Vector3 ray_dir = p_camera->project_ray_normal(p_point);
109 const Plane p = Plane(p_camera->get_transform().basis.get_column(2), gt.xform(original));
110
111 // Primary handles: position.
112 if (!p_secondary) {
113 Vector3 inters;
114 // Special cas for primary handle, the handle id equals control point id.
115 const int idx = p_id;
116 if (p.intersects_ray(ray_from, ray_dir, &inters)) {
117 if (Node3DEditor::get_singleton()->is_snap_enabled()) {
118 float snap = Node3DEditor::get_singleton()->get_translate_snap();
119 inters.snap(Vector3(snap, snap, snap));
120 }
121
122 Vector3 local = gi.xform(inters);
123 c->set_point_position(idx, local);
124 }
125
126 return;
127 }
128
129 // Secondary handles: in, out, tilt.
130 const HandleInfo info = _secondary_handles_info[p_id];
131 switch (info.type) {
132 case HandleType::HANDLE_TYPE_OUT:
133 case HandleType::HANDLE_TYPE_IN: {
134 const int idx = info.point_idx;
135 const Vector3 base = c->get_point_position(idx);
136
137 Vector3 inters;
138 if (p.intersects_ray(ray_from, ray_dir, &inters)) {
139 if (!Path3DEditorPlugin::singleton->is_handle_clicked()) {
140 orig_in_length = c->get_point_in(idx).length();
141 orig_out_length = c->get_point_out(idx).length();
142 Path3DEditorPlugin::singleton->set_handle_clicked(true);
143 }
144
145 Vector3 local = gi.xform(inters) - base;
146 if (Node3DEditor::get_singleton()->is_snap_enabled()) {
147 float snap = Node3DEditor::get_singleton()->get_translate_snap();
148 local.snap(Vector3(snap, snap, snap));
149 }
150
151 if (info.type == HandleType::HANDLE_TYPE_IN) {
152 c->set_point_in(idx, local);
153 if (Path3DEditorPlugin::singleton->mirror_angle_enabled()) {
154 c->set_point_out(idx, Path3DEditorPlugin::singleton->mirror_length_enabled() ? -local : (-local.normalized() * orig_out_length));
155 }
156 } else {
157 c->set_point_out(idx, local);
158 if (Path3DEditorPlugin::singleton->mirror_angle_enabled()) {
159 c->set_point_in(idx, Path3DEditorPlugin::singleton->mirror_length_enabled() ? -local : (-local.normalized() * orig_in_length));
160 }
161 }
162 }
163 break;
164 }
165 case HandleType::HANDLE_TYPE_TILT: {
166 const int idx = info.point_idx;
167 const Vector3 position = c->get_point_position(idx);
168 const Basis posture = c->get_point_baked_posture(idx);
169 const Vector3 tangent = -posture.get_column(2);
170 const Vector3 up = posture.get_column(1);
171 const Plane p_tilt = Plane(tangent, position);
172
173 Vector3 intersection;
174
175 if (p_tilt.intersects_ray(ray_from, ray_dir, &intersection)) {
176 Vector3 direction = intersection - position;
177 direction.normalize(); // FIXME: redundant?
178 real_t tilt_angle = up.signed_angle_to(direction, tangent);
179
180 if (Node3DEditor::get_singleton()->is_snap_enabled()) {
181 real_t snap = Node3DEditor::get_singleton()->get_rotate_snap();
182
183 tilt_angle = Math::rad_to_deg(tilt_angle) + snap * 0.5; // Else it won't reach +180.
184 tilt_angle -= Math::fmod(tilt_angle, snap);
185 tilt_angle = Math::deg_to_rad(tilt_angle);
186 }
187
188 c->set_point_tilt(idx, tilt_angle);
189 }
190 break;
191 }
192 }
193}
194
195void Path3DGizmo::commit_handle(int p_id, bool p_secondary, const Variant &p_restore, bool p_cancel) {
196 Ref<Curve3D> c = path->get_curve();
197 if (c.is_null()) {
198 return;
199 }
200
201 EditorUndoRedoManager *ur = EditorUndoRedoManager::get_singleton();
202
203 // Primary handles: position.
204 if (!p_secondary) {
205 // Special cas for primary handle, the handle id equals control point id.
206 const int idx = p_id;
207 if (p_cancel) {
208 c->set_point_position(idx, p_restore);
209 return;
210 }
211 ur->create_action(TTR("Set Curve Point Position"));
212 ur->add_do_method(c.ptr(), "set_point_position", idx, c->get_point_position(idx));
213 ur->add_undo_method(c.ptr(), "set_point_position", idx, p_restore);
214 ur->commit_action();
215
216 return;
217 }
218
219 // Secondary handles: in, out, tilt.
220 const HandleInfo info = _secondary_handles_info[p_id];
221 const int idx = info.point_idx;
222 switch (info.type) {
223 case HandleType::HANDLE_TYPE_OUT: {
224 if (p_cancel) {
225 c->set_point_out(idx, p_restore);
226
227 return;
228 }
229
230 ur->create_action(TTR("Set Curve Out Position"));
231 ur->add_do_method(c.ptr(), "set_point_out", idx, c->get_point_out(idx));
232 ur->add_undo_method(c.ptr(), "set_point_out", idx, p_restore);
233
234 if (Path3DEditorPlugin::singleton->mirror_angle_enabled()) {
235 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));
236 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));
237 }
238 ur->commit_action();
239 break;
240 }
241 case HandleType::HANDLE_TYPE_IN: {
242 if (p_cancel) {
243 c->set_point_in(idx, p_restore);
244 return;
245 }
246
247 ur->create_action(TTR("Set Curve In Position"));
248 ur->add_do_method(c.ptr(), "set_point_in", idx, c->get_point_in(idx));
249 ur->add_undo_method(c.ptr(), "set_point_in", idx, p_restore);
250
251 if (Path3DEditorPlugin::singleton->mirror_angle_enabled()) {
252 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));
253 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));
254 }
255 ur->commit_action();
256 break;
257 }
258 case HandleType::HANDLE_TYPE_TILT: {
259 if (p_cancel) {
260 c->set_point_tilt(idx, p_restore);
261 return;
262 }
263 ur->create_action(TTR("Set Curve Point Tilt"));
264 ur->add_do_method(c.ptr(), "set_point_tilt", idx, c->get_point_tilt(idx));
265 ur->add_undo_method(c.ptr(), "set_point_tilt", idx, p_restore);
266 ur->commit_action();
267 break;
268 }
269 }
270}
271
272void Path3DGizmo::redraw() {
273 clear();
274
275 Ref<StandardMaterial3D> path_material = gizmo_plugin->get_material("path_material", this);
276 Ref<StandardMaterial3D> path_thin_material = gizmo_plugin->get_material("path_thin_material", this);
277 Ref<StandardMaterial3D> handles_material = gizmo_plugin->get_material("handles");
278 Ref<StandardMaterial3D> sec_handles_material = gizmo_plugin->get_material("sec_handles");
279
280 Ref<Curve3D> c = path->get_curve();
281 if (c.is_null()) {
282 return;
283 }
284
285 real_t interval = 0.1;
286 const real_t length = c->get_baked_length();
287
288 // 1. Draw curve and bones.
289 if (length > CMP_EPSILON) {
290 const int sample_count = int(length / interval) + 2;
291 interval = length / (sample_count - 1); // Recalculate real interval length.
292
293 Vector<Transform3D> frames;
294 frames.resize(sample_count);
295
296 {
297 Transform3D *w = frames.ptrw();
298
299 for (int i = 0; i < sample_count; i++) {
300 w[i] = c->sample_baked_with_rotation(i * interval, true, true);
301 }
302 }
303
304 const Transform3D *r = frames.ptr();
305 Vector<Vector3> _collision_segments;
306 Vector<Vector3> bones;
307 Vector<Vector3> ribbon;
308 for (int i = 0; i < sample_count; i++) {
309 const Vector3 p1 = r[i].origin;
310 const Vector3 side = r[i].basis.get_column(0);
311 const Vector3 up = r[i].basis.get_column(1);
312 const Vector3 forward = r[i].basis.get_column(2);
313
314 // Collision segments.
315 if (i != sample_count) {
316 const Vector3 p2 = r[i + 1].origin;
317 _collision_segments.push_back(p1);
318 _collision_segments.push_back(p2);
319 }
320
321 // Path3D as a ribbon.
322 ribbon.push_back(p1);
323
324 // Fish Bone.
325 const Vector3 p_left = p1 + (side + forward - up * 0.3) * 0.06;
326 const Vector3 p_right = p1 + (-side + forward - up * 0.3) * 0.06;
327 bones.push_back(p1);
328 bones.push_back(p_left);
329
330 bones.push_back(p1);
331 bones.push_back(p_right);
332 }
333
334 add_collision_segments(_collision_segments);
335 add_lines(bones, path_material);
336 add_vertices(ribbon, path_material, Mesh::PRIMITIVE_LINE_STRIP);
337 }
338
339 // 2. Draw handles when selected.
340 if (Path3DEditorPlugin::singleton->get_edited_path() == path) {
341 PackedVector3Array handle_lines;
342 PackedVector3Array primary_handle_points;
343 PackedVector3Array secondary_handle_points;
344 PackedInt32Array collected_secondary_handle_ids; // Avoid shadowing member on Node3DEditorGizmo.
345
346 _secondary_handles_info.resize(c->get_point_count() * 3);
347
348 for (int idx = 0; idx < c->get_point_count(); idx++) {
349 // Collect primary-handles.
350 const Vector3 pos = c->get_point_position(idx);
351 primary_handle_points.append(pos);
352
353 HandleInfo info;
354 info.point_idx = idx;
355
356 // Collect in-handles except for the first point.
357 if (idx > 0) {
358 info.type = HandleType::HANDLE_TYPE_IN;
359 const int handle_idx = idx * 3 + 0;
360 collected_secondary_handle_ids.append(handle_idx);
361 _secondary_handles_info.write[handle_idx] = info;
362
363 const Vector3 in = c->get_point_in(idx);
364 secondary_handle_points.append(pos + in);
365 handle_lines.append(pos);
366 handle_lines.append(pos + in);
367 }
368
369 // Collect out-handles except for the last point.
370 if (idx < c->get_point_count()) {
371 info.type = HandleType::HANDLE_TYPE_OUT;
372 const int handle_idx = idx * 3 + 1;
373 collected_secondary_handle_ids.append(handle_idx);
374 _secondary_handles_info.write[handle_idx] = info;
375
376 const Vector3 out = c->get_point_out(idx);
377 secondary_handle_points.append(pos + out);
378 handle_lines.append(pos);
379 handle_lines.append(pos + out);
380 }
381
382 // Collect tilt-handles.
383 {
384 {
385 info.type = HandleType::HANDLE_TYPE_TILT;
386 const int handle_idx = idx * 3 + 2;
387 collected_secondary_handle_ids.append(handle_idx);
388 _secondary_handles_info.write[handle_idx] = info;
389
390 const Basis posture = c->get_point_baked_posture(idx, true);
391 const Vector3 up = posture.get_column(1);
392 secondary_handle_points.append(pos + up);
393 handle_lines.append(pos);
394 handle_lines.append(pos + up);
395 }
396
397 // Tilt disk.
398 {
399 const Basis posture = c->get_point_baked_posture(idx, false);
400 const Vector3 up = posture.get_column(1);
401 const Vector3 side = posture.get_column(0);
402
403 PackedVector3Array disk;
404 disk.append(pos);
405
406 const int n = 24;
407 for (int i = 0; i <= n; i++) {
408 const float a = Math_TAU * i / n;
409 const Vector3 edge = sin(a) * side + cos(a) * up;
410 disk.append(pos + edge);
411 }
412 add_vertices(disk, path_material, Mesh::PRIMITIVE_LINE_STRIP);
413 }
414 }
415 }
416
417 if (handle_lines.size() > 1) {
418 add_lines(handle_lines, path_thin_material);
419 }
420 if (primary_handle_points.size()) {
421 add_handles(primary_handle_points, handles_material);
422 }
423 if (secondary_handle_points.size()) {
424 add_handles(secondary_handle_points, sec_handles_material, collected_secondary_handle_ids, false, true);
425 }
426 }
427}
428
429Path3DGizmo::Path3DGizmo(Path3D *p_path) {
430 path = p_path;
431 set_node_3d(p_path);
432 orig_in_length = 0;
433 orig_out_length = 0;
434}
435
436EditorPlugin::AfterGUIInput Path3DEditorPlugin::forward_3d_gui_input(Camera3D *p_camera, const Ref<InputEvent> &p_event) {
437 if (!path) {
438 return EditorPlugin::AFTER_GUI_INPUT_PASS;
439 }
440 Ref<Curve3D> c = path->get_curve();
441 if (c.is_null()) {
442 return EditorPlugin::AFTER_GUI_INPUT_PASS;
443 }
444 Transform3D gt = path->get_global_transform();
445 Transform3D it = gt.affine_inverse();
446
447 static const int click_dist = 10; //should make global
448
449 Ref<InputEventMouseButton> mb = p_event;
450
451 if (mb.is_valid()) {
452 Point2 mbpos(mb->get_position().x, mb->get_position().y);
453
454 if (!mb->is_pressed()) {
455 set_handle_clicked(false);
456 }
457
458 if (mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT && (curve_create->is_pressed() || (curve_edit->is_pressed() && mb->is_ctrl_pressed()))) {
459 //click into curve, break it down
460 Vector<Vector3> v3a = c->tessellate();
461 int rc = v3a.size();
462 int closest_seg = -1;
463 Vector3 closest_seg_point;
464
465 if (rc >= 2) {
466 int idx = 0;
467 const Vector3 *r = v3a.ptr();
468 float closest_d = 1e20;
469
470 if (p_camera->unproject_position(gt.xform(c->get_point_position(0))).distance_to(mbpos) < click_dist) {
471 return EditorPlugin::AFTER_GUI_INPUT_PASS; //nope, existing
472 }
473
474 for (int i = 0; i < c->get_point_count() - 1; i++) {
475 //find the offset and point index of the place to break up
476 int j = idx;
477 if (p_camera->unproject_position(gt.xform(c->get_point_position(i + 1))).distance_to(mbpos) < click_dist) {
478 return EditorPlugin::AFTER_GUI_INPUT_PASS; //nope, existing
479 }
480
481 while (j < rc && c->get_point_position(i + 1) != r[j]) {
482 Vector3 from = r[j];
483 Vector3 to = r[j + 1];
484 real_t cdist = from.distance_to(to);
485 from = gt.xform(from);
486 to = gt.xform(to);
487 if (cdist > 0) {
488 Vector2 s[2];
489 s[0] = p_camera->unproject_position(from);
490 s[1] = p_camera->unproject_position(to);
491 Vector2 inters = Geometry2D::get_closest_point_to_segment(mbpos, s);
492 float d = inters.distance_to(mbpos);
493
494 if (d < 10 && d < closest_d) {
495 closest_d = d;
496 closest_seg = i;
497 Vector3 ray_from = p_camera->project_ray_origin(mbpos);
498 Vector3 ray_dir = p_camera->project_ray_normal(mbpos);
499
500 Vector3 ra, rb;
501 Geometry3D::get_closest_points_between_segments(ray_from, ray_from + ray_dir * 4096, from, to, ra, rb);
502
503 closest_seg_point = it.xform(rb);
504 }
505 }
506 j++;
507 }
508 if (idx == j) {
509 idx++; //force next
510 } else {
511 idx = j; //swap
512 }
513
514 if (j == rc) {
515 break;
516 }
517 }
518 }
519
520 EditorUndoRedoManager *ur = EditorUndoRedoManager::get_singleton();
521 if (closest_seg != -1) {
522 //subdivide
523
524 ur->create_action(TTR("Split Path"));
525 ur->add_do_method(c.ptr(), "add_point", closest_seg_point, Vector3(), Vector3(), closest_seg + 1);
526 ur->add_undo_method(c.ptr(), "remove_point", closest_seg + 1);
527 ur->commit_action();
528 return EditorPlugin::AFTER_GUI_INPUT_STOP;
529
530 } else {
531 Vector3 origin;
532 if (c->get_point_count() == 0) {
533 origin = path->get_transform().get_origin();
534 } else {
535 origin = gt.xform(c->get_point_position(c->get_point_count() - 1));
536 }
537 Plane p(p_camera->get_transform().basis.get_column(2), origin);
538 Vector3 ray_from = p_camera->project_ray_origin(mbpos);
539 Vector3 ray_dir = p_camera->project_ray_normal(mbpos);
540
541 Vector3 inters;
542 if (p.intersects_ray(ray_from, ray_dir, &inters)) {
543 ur->create_action(TTR("Add Point to Curve"));
544 ur->add_do_method(c.ptr(), "add_point", it.xform(inters), Vector3(), Vector3(), -1);
545 ur->add_undo_method(c.ptr(), "remove_point", c->get_point_count());
546 ur->commit_action();
547 return EditorPlugin::AFTER_GUI_INPUT_STOP;
548 }
549
550 //add new at pos
551 }
552
553 } 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()))) {
554 for (int i = 0; i < c->get_point_count(); i++) {
555 real_t dist_to_p = p_camera->unproject_position(gt.xform(c->get_point_position(i))).distance_to(mbpos);
556 real_t dist_to_p_out = p_camera->unproject_position(gt.xform(c->get_point_position(i) + c->get_point_out(i))).distance_to(mbpos);
557 real_t dist_to_p_in = p_camera->unproject_position(gt.xform(c->get_point_position(i) + c->get_point_in(i))).distance_to(mbpos);
558 real_t dist_to_p_up = p_camera->unproject_position(gt.xform(c->get_point_position(i) + c->get_point_baked_posture(i, true).get_column(1))).distance_to(mbpos);
559
560 // Find the offset and point index of the place to break up.
561 // Also check for the control points.
562 if (dist_to_p < click_dist) {
563 EditorUndoRedoManager *ur = EditorUndoRedoManager::get_singleton();
564 ur->create_action(TTR("Remove Path Point"));
565 ur->add_do_method(c.ptr(), "remove_point", i);
566 ur->add_undo_method(c.ptr(), "add_point", c->get_point_position(i), c->get_point_in(i), c->get_point_out(i), i);
567 ur->commit_action();
568 return EditorPlugin::AFTER_GUI_INPUT_STOP;
569 } else if (dist_to_p_out < click_dist) {
570 EditorUndoRedoManager *ur = EditorUndoRedoManager::get_singleton();
571 ur->create_action(TTR("Reset Out-Control Point"));
572 ur->add_do_method(c.ptr(), "set_point_out", i, Vector3());
573 ur->add_undo_method(c.ptr(), "set_point_out", i, c->get_point_out(i));
574 ur->commit_action();
575 return EditorPlugin::AFTER_GUI_INPUT_STOP;
576 } else if (dist_to_p_in < click_dist) {
577 EditorUndoRedoManager *ur = EditorUndoRedoManager::get_singleton();
578 ur->create_action(TTR("Reset In-Control Point"));
579 ur->add_do_method(c.ptr(), "set_point_in", i, Vector3());
580 ur->add_undo_method(c.ptr(), "set_point_in", i, c->get_point_in(i));
581 ur->commit_action();
582 return EditorPlugin::AFTER_GUI_INPUT_STOP;
583 } else if (dist_to_p_up < click_dist) {
584 EditorUndoRedoManager *ur = EditorUndoRedoManager::get_singleton();
585 ur->create_action(TTR("Reset Point Tilt"));
586 ur->add_do_method(c.ptr(), "set_point_tilt", i, 0.0f);
587 ur->add_undo_method(c.ptr(), "set_point_tilt", i, c->get_point_tilt(i));
588 ur->commit_action();
589 return EditorPlugin::AFTER_GUI_INPUT_STOP;
590 }
591 }
592 }
593 if (curve_edit_curve->is_pressed()) {
594 mb->set_shift_pressed(true);
595 }
596 }
597
598 return EditorPlugin::AFTER_GUI_INPUT_PASS;
599}
600
601void Path3DEditorPlugin::edit(Object *p_object) {
602 if (p_object) {
603 path = Object::cast_to<Path3D>(p_object);
604 if (path) {
605 if (path->get_curve().is_valid()) {
606 path->get_curve()->emit_signal(SNAME("changed"));
607 }
608 }
609 } else {
610 Path3D *pre = path;
611 path = nullptr;
612 if (pre) {
613 pre->get_curve()->emit_signal(SNAME("changed"));
614 }
615 }
616
617 update_overlays();
618 //collision_polygon_editor->edit(Object::cast_to<Node>(p_object));
619}
620
621bool Path3DEditorPlugin::handles(Object *p_object) const {
622 return p_object->is_class("Path3D");
623}
624
625void Path3DEditorPlugin::make_visible(bool p_visible) {
626 if (p_visible) {
627 topmenu_bar->show();
628 } else {
629 topmenu_bar->hide();
630
631 {
632 Path3D *pre = path;
633 path = nullptr;
634 if (pre && pre->get_curve().is_valid()) {
635 pre->get_curve()->emit_signal(SNAME("changed"));
636 }
637 }
638 }
639}
640
641void Path3DEditorPlugin::_mode_changed(int p_mode) {
642 curve_create->set_pressed(p_mode == MODE_CREATE);
643 curve_edit_curve->set_pressed(p_mode == MODE_EDIT_CURVE);
644 curve_edit->set_pressed(p_mode == MODE_EDIT);
645 curve_del->set_pressed(p_mode == MODE_DELETE);
646}
647
648void Path3DEditorPlugin::_close_curve() {
649 Ref<Curve3D> c = path->get_curve();
650 if (c.is_null()) {
651 return;
652 }
653 if (c->get_point_count() < 2) {
654 return;
655 }
656 if (c->get_point_position(0) == c->get_point_position(c->get_point_count() - 1)) {
657 return;
658 }
659 EditorUndoRedoManager *ur = EditorUndoRedoManager::get_singleton();
660 ur->create_action(TTR("Close Curve"));
661 ur->add_do_method(c.ptr(), "add_point", c->get_point_position(0), c->get_point_in(0), c->get_point_out(0), -1);
662 ur->add_undo_method(c.ptr(), "remove_point", c->get_point_count());
663 ur->commit_action();
664}
665
666void Path3DEditorPlugin::_handle_option_pressed(int p_option) {
667 PopupMenu *pm;
668 pm = handle_menu->get_popup();
669
670 switch (p_option) {
671 case HANDLE_OPTION_ANGLE: {
672 bool is_checked = pm->is_item_checked(HANDLE_OPTION_ANGLE);
673 mirror_handle_angle = !is_checked;
674 pm->set_item_checked(HANDLE_OPTION_ANGLE, mirror_handle_angle);
675 pm->set_item_disabled(HANDLE_OPTION_LENGTH, !mirror_handle_angle);
676 } break;
677 case HANDLE_OPTION_LENGTH: {
678 bool is_checked = pm->is_item_checked(HANDLE_OPTION_LENGTH);
679 mirror_handle_length = !is_checked;
680 pm->set_item_checked(HANDLE_OPTION_LENGTH, mirror_handle_length);
681 } break;
682 }
683}
684
685void Path3DEditorPlugin::_update_theme() {
686 // TODO: Split the EditorPlugin instance from the UI instance and connect this properly.
687 // See the 2D path editor for inspiration.
688 curve_edit->set_icon(EditorNode::get_singleton()->get_editor_theme()->get_icon(SNAME("CurveEdit"), EditorStringName(EditorIcons)));
689 curve_edit_curve->set_icon(EditorNode::get_singleton()->get_editor_theme()->get_icon(SNAME("CurveCurve"), EditorStringName(EditorIcons)));
690 curve_create->set_icon(EditorNode::get_singleton()->get_editor_theme()->get_icon(SNAME("CurveCreate"), EditorStringName(EditorIcons)));
691 curve_del->set_icon(EditorNode::get_singleton()->get_editor_theme()->get_icon(SNAME("CurveDelete"), EditorStringName(EditorIcons)));
692 curve_close->set_icon(EditorNode::get_singleton()->get_editor_theme()->get_icon(SNAME("CurveClose"), EditorStringName(EditorIcons)));
693}
694
695void Path3DEditorPlugin::_notification(int p_what) {
696 switch (p_what) {
697 case NOTIFICATION_ENTER_TREE: {
698 curve_create->connect("pressed", callable_mp(this, &Path3DEditorPlugin::_mode_changed).bind(MODE_CREATE));
699 curve_edit_curve->connect("pressed", callable_mp(this, &Path3DEditorPlugin::_mode_changed).bind(MODE_EDIT_CURVE));
700 curve_edit->connect("pressed", callable_mp(this, &Path3DEditorPlugin::_mode_changed).bind(MODE_EDIT));
701 curve_del->connect("pressed", callable_mp(this, &Path3DEditorPlugin::_mode_changed).bind(MODE_DELETE));
702 curve_close->connect("pressed", callable_mp(this, &Path3DEditorPlugin::_close_curve));
703
704 _update_theme();
705 } break;
706
707 case NOTIFICATION_READY: {
708 Node3DEditor::get_singleton()->connect("theme_changed", callable_mp(this, &Path3DEditorPlugin::_update_theme));
709 } break;
710 }
711}
712
713void Path3DEditorPlugin::_bind_methods() {
714}
715
716Path3DEditorPlugin *Path3DEditorPlugin::singleton = nullptr;
717
718Path3DEditorPlugin::Path3DEditorPlugin() {
719 path = nullptr;
720 singleton = this;
721 mirror_handle_angle = true;
722 mirror_handle_length = true;
723
724 Ref<Path3DGizmoPlugin> gizmo_plugin;
725 gizmo_plugin.instantiate();
726 Node3DEditor::get_singleton()->add_gizmo_plugin(gizmo_plugin);
727
728 topmenu_bar = memnew(HBoxContainer);
729 topmenu_bar->hide();
730 Node3DEditor::get_singleton()->add_control_to_menu_panel(topmenu_bar);
731
732 curve_edit = memnew(Button);
733 curve_edit->set_flat(true);
734 curve_edit->set_toggle_mode(true);
735 curve_edit->set_focus_mode(Control::FOCUS_NONE);
736 curve_edit->set_tooltip_text(TTR("Select Points") + "\n" + TTR("Shift+Drag: Select Control Points") + "\n" + keycode_get_string((Key)KeyModifierMask::CMD_OR_CTRL) + TTR("Click: Add Point") + "\n" + TTR("Right Click: Delete Point"));
737 topmenu_bar->add_child(curve_edit);
738
739 curve_edit_curve = memnew(Button);
740 curve_edit_curve->set_flat(true);
741 curve_edit_curve->set_toggle_mode(true);
742 curve_edit_curve->set_focus_mode(Control::FOCUS_NONE);
743 curve_edit_curve->set_tooltip_text(TTR("Select Control Points (Shift+Drag)"));
744 topmenu_bar->add_child(curve_edit_curve);
745
746 curve_create = memnew(Button);
747 curve_create->set_flat(true);
748 curve_create->set_toggle_mode(true);
749 curve_create->set_focus_mode(Control::FOCUS_NONE);
750 curve_create->set_tooltip_text(TTR("Add Point (in empty space)") + "\n" + TTR("Split Segment (in curve)"));
751 topmenu_bar->add_child(curve_create);
752
753 curve_del = memnew(Button);
754 curve_del->set_flat(true);
755 curve_del->set_toggle_mode(true);
756 curve_del->set_focus_mode(Control::FOCUS_NONE);
757 curve_del->set_tooltip_text(TTR("Delete Point"));
758 topmenu_bar->add_child(curve_del);
759
760 curve_close = memnew(Button);
761 curve_close->set_flat(true);
762 curve_close->set_focus_mode(Control::FOCUS_NONE);
763 curve_close->set_tooltip_text(TTR("Close Curve"));
764 topmenu_bar->add_child(curve_close);
765
766 PopupMenu *menu;
767
768 handle_menu = memnew(MenuButton);
769 handle_menu->set_text(TTR("Options"));
770 topmenu_bar->add_child(handle_menu);
771
772 menu = handle_menu->get_popup();
773 menu->add_check_item(TTR("Mirror Handle Angles"));
774 menu->set_item_checked(HANDLE_OPTION_ANGLE, mirror_handle_angle);
775 menu->add_check_item(TTR("Mirror Handle Lengths"));
776 menu->set_item_checked(HANDLE_OPTION_LENGTH, mirror_handle_length);
777 menu->connect("id_pressed", callable_mp(this, &Path3DEditorPlugin::_handle_option_pressed));
778
779 curve_edit->set_pressed(true);
780}
781
782Path3DEditorPlugin::~Path3DEditorPlugin() {
783}
784
785Ref<EditorNode3DGizmo> Path3DGizmoPlugin::create_gizmo(Node3D *p_spatial) {
786 Ref<Path3DGizmo> ref;
787
788 Path3D *path = Object::cast_to<Path3D>(p_spatial);
789 if (path) {
790 ref = Ref<Path3DGizmo>(memnew(Path3DGizmo(path)));
791 }
792
793 return ref;
794}
795
796String Path3DGizmoPlugin::get_gizmo_name() const {
797 return "Path3D";
798}
799
800int Path3DGizmoPlugin::get_priority() const {
801 return -1;
802}
803
804Path3DGizmoPlugin::Path3DGizmoPlugin() {
805 Color path_color = EDITOR_DEF("editors/3d_gizmos/gizmo_colors/path", Color(0.5, 0.5, 1.0, 0.8));
806 create_material("path_material", path_color);
807 create_material("path_thin_material", Color(0.5, 0.5, 0.5));
808 create_handle_material("handles", false, EditorNode::get_singleton()->get_editor_theme()->get_icon(SNAME("EditorPathSmoothHandle"), EditorStringName(EditorIcons)));
809 create_handle_material("sec_handles", false, EditorNode::get_singleton()->get_editor_theme()->get_icon(SNAME("EditorCurveHandle"), EditorStringName(EditorIcons)));
810}
811