1mod ball;
24mod ball2d;
25mod capsule;
26mod capsule2d;
27mod cone;
28mod cuboid;
29mod cuboid2d;
30mod cylinder;
31mod dummy;
32mod panel;
33mod segment;
34mod segment2d;
35mod triangle;
36mod triangle2d;
37
38use std::sync::LazyLock;
39
40use super::inspector::InspectorPlugin;
41use crate::{
42 camera::PickingOptions,
43 command::SetPropertyCommand,
44 fyrox::{
45 core::{
46 algebra::{UnitQuaternion, Vector2, Vector3},
47 color::Color,
48 math::{plane::Plane, Matrix4Ext},
49 pool::Handle,
50 reflect::Reflect,
51 some_or_return,
52 type_traits::prelude::*,
53 Uuid,
54 },
55 engine::Engine,
56 graph::{SceneGraph, SceneGraphNode},
57 gui::{message::UiMessage, BuildContext},
58 material::{
59 shader::{ShaderResource, ShaderResourceExtension},
60 Material, MaterialResource,
61 },
62 scene::{
63 base::BaseBuilder,
64 collider::{Collider, ColliderShape},
65 dim2,
66 node::Node,
67 sprite::Sprite,
68 sprite::SpriteBuilder,
69 Scene,
70 },
71 },
72 interaction::{
73 calculate_gizmo_distance_scaling, gizmo::move_gizmo::MoveGizmo,
74 make_interaction_mode_button, plane::PlaneKind, InteractionMode,
75 },
76 load_texture,
77 message::MessageSender,
78 plugin::EditorPlugin,
79 plugins::collider::{
80 ball::BallShapeGizmo, ball2d::Ball2DShapeGizmo, capsule::CapsuleShapeGizmo,
81 capsule2d::Capsule2DShapeGizmo, cone::ConeShapeGizmo, cuboid::CuboidShapeGizmo,
82 cuboid2d::Cuboid2DShapeGizmo, cylinder::CylinderShapeGizmo, dummy::DummyShapeGizmo,
83 panel::ColliderControlPanel, segment::SegmentShapeGizmo, segment2d::Segment2DShapeGizmo,
84 triangle::TriangleShapeGizmo, triangle2d::Triangle2DShapeGizmo,
85 },
86 scene::{commands::GameSceneContext, controller::SceneController, GameScene, Selection},
87 settings::Settings,
88 Editor, Message,
89};
90use fyrox::gui::button::Button;
91use fyrox::gui::widget::WidgetMessage;
92use fyrox::scene::camera::Camera;
93use fyrox::scene::pivot::Pivot;
94
95fn try_get_collider_shape(collider: Handle<Node>, scene: &Scene) -> Option<ColliderShape> {
96 scene
97 .graph
98 .try_get_of_type::<Collider>(collider)
99 .ok()
100 .map(|c| c.shape().clone())
101}
102
103fn try_get_collider_shape_mut(
104 collider: Handle<Node>,
105 scene: &mut Scene,
106) -> Option<&mut ColliderShape> {
107 scene
108 .graph
109 .try_get_mut_of_type::<Collider>(collider)
110 .ok()
111 .map(|c| c.shape_mut())
112}
113
114fn try_get_collider_shape_2d(
115 collider: Handle<Node>,
116 scene: &Scene,
117) -> Option<dim2::collider::ColliderShape> {
118 scene
119 .graph
120 .try_get_of_type::<dim2::collider::Collider>(collider)
121 .ok()
122 .map(|c| c.shape().clone())
123}
124
125fn try_get_collider_shape_mut_2d(
126 collider: Handle<Node>,
127 scene: &mut Scene,
128) -> Option<&mut dim2::collider::ColliderShape> {
129 scene
130 .graph
131 .try_get_mut_of_type::<dim2::collider::Collider>(collider)
132 .ok()
133 .map(|c| c.shape_mut())
134}
135
136trait ShapeGizmoTrait {
137 fn for_each_handle(&self, func: &mut dyn FnMut(Handle<Sprite>));
138
139 fn handle_local_position(
140 &self,
141 handle: Handle<Sprite>,
142 collider: Handle<Node>,
143 scene: &Scene,
144 ) -> Option<Vector3<f32>>;
145
146 fn handle_major_axis(
147 &self,
148 _handle: Handle<Sprite>,
149 _collider: Handle<Node>,
150 _scene: &Scene,
151 ) -> Option<Vector3<f32>> {
152 None
153 }
154
155 fn value_by_handle(
156 &self,
157 handle: Handle<Sprite>,
158 collider: Handle<Node>,
159 scene: &Scene,
160 ) -> Option<ShapeHandleValue>;
161
162 fn set_value_by_handle(
163 &self,
164 handle: Handle<Sprite>,
165 value: ShapeHandleValue,
166 collider: Handle<Node>,
167 scene: &mut Scene,
168 );
169
170 fn is_vector_handle(&self, _handle: Handle<Sprite>) -> bool {
171 false
172 }
173
174 fn reset_handles(&self, scene: &mut Scene) {
175 self.for_each_handle(&mut |handle| {
176 scene.graph[handle].set_color(Color::MAROON);
177 });
178 }
179
180 fn destroy(self: Box<Self>, scene: &mut Scene) {
181 self.for_each_handle(&mut |handle| scene.graph.remove_node(handle));
182 }
183
184 fn has_handle(&self, handle: Handle<Sprite>) -> bool {
185 let mut has_handle = false;
186 self.for_each_handle(&mut |other_handle| {
187 if other_handle == handle {
188 has_handle = true
189 }
190 });
191 has_handle
192 }
193
194 fn set_visibility(&self, scene: &mut Scene, visibility: bool) {
195 self.for_each_handle(&mut |handle| {
196 scene.graph[handle].set_visibility(visibility);
197 })
198 }
199
200 fn try_sync_to_collider(
201 &self,
202 collider: Handle<Node>,
203 camera: Handle<Camera>,
204 scene: &mut Scene,
205 ) -> bool {
206 let mut is_ok = true;
207 let transform = scene.graph[collider].global_transform();
208 self.for_each_handle(&mut |handle| {
209 if let Some(local_position) = self.handle_local_position(handle, collider, scene) {
210 let scale = calculate_gizmo_distance_scaling(&scene.graph, camera, handle);
211
212 let sprite = &mut scene.graph[handle];
213 sprite
214 .local_transform_mut()
215 .set_position(transform.transform_point(&local_position.into()).coords)
216 .set_scale(scale)
217 .set_rotation(UnitQuaternion::from_matrix_eps(
218 &transform.basis(),
219 f32::EPSILON,
220 16,
221 Default::default(),
222 ));
223 sprite.set_size(0.05 * scale.x);
224 } else {
225 is_ok = false;
226 }
227 });
228 is_ok
229 }
230}
231
232fn make_shape_gizmo(
233 collider: Handle<Node>,
234 scene: &mut Scene,
235 root: Handle<Pivot>,
236 visible: bool,
237) -> Box<dyn ShapeGizmoTrait> {
238 if let Ok(collider) = scene.graph.try_get_of_type::<Collider>(collider) {
239 let shape = collider.shape().clone();
240 use fyrox::scene::collider::ColliderShape;
241 match shape {
242 ColliderShape::Ball(_) => Box::new(BallShapeGizmo::new(root, visible, scene)),
243 ColliderShape::Cylinder(_) => Box::new(CylinderShapeGizmo::new(visible, root, scene)),
244 ColliderShape::Cone(_) => Box::new(ConeShapeGizmo::new(visible, root, scene)),
245 ColliderShape::Cuboid(_) => Box::new(CuboidShapeGizmo::new(visible, root, scene)),
246 ColliderShape::Capsule(_) => Box::new(CapsuleShapeGizmo::new(visible, root, scene)),
247 ColliderShape::Segment(_) => Box::new(SegmentShapeGizmo::new(root, visible, scene)),
248 ColliderShape::Triangle(_) => Box::new(TriangleShapeGizmo::new(root, visible, scene)),
249 ColliderShape::Trimesh(_)
250 | ColliderShape::Heightfield(_)
251 | ColliderShape::Polyhedron(_) => Box::new(DummyShapeGizmo),
252 }
253 } else if let Ok(collider) = scene
254 .graph
255 .try_get_of_type::<dim2::collider::Collider>(collider)
256 {
257 let shape = collider.shape().clone();
258 use dim2::collider::ColliderShape;
259 match shape {
260 ColliderShape::Ball(_) => Box::new(Ball2DShapeGizmo::new(root, visible, scene)),
261 ColliderShape::Cuboid(_) => Box::new(Cuboid2DShapeGizmo::new(visible, root, scene)),
262 ColliderShape::Capsule(_) => Box::new(Capsule2DShapeGizmo::new(visible, root, scene)),
263 ColliderShape::Segment(_) => Box::new(Segment2DShapeGizmo::new(root, visible, scene)),
264 ColliderShape::Triangle(_) => Box::new(Triangle2DShapeGizmo::new(root, visible, scene)),
265 ColliderShape::Trimesh(_)
266 | ColliderShape::Heightfield(_)
267 | ColliderShape::TileMap(_) => Box::new(DummyShapeGizmo),
268 }
269 } else {
270 Box::new(DummyShapeGizmo)
271 }
272}
273
274static GIZMO_SHADER: LazyLock<ShaderResource> = LazyLock::new(|| {
275 ShaderResource::from_str(
276 Uuid::new_v4(),
277 include_str!("../../../resources/shaders/sprite_gizmo.shader"),
278 Default::default(),
279 )
280 .unwrap()
281});
282
283fn make_handle(scene: &mut Scene, root: Handle<Pivot>, visible: bool) -> Handle<Sprite> {
284 let mut material = Material::from_shader(GIZMO_SHADER.clone());
285
286 material.bind(
287 "diffuseTexture",
288 load_texture!("../../../resources/circle.png"),
289 );
290
291 let handle = SpriteBuilder::new(BaseBuilder::new().with_visibility(visible))
292 .with_material(MaterialResource::new_embedded(material))
293 .with_size(0.05)
294 .with_color(Color::MAROON)
295 .build(&mut scene.graph);
296
297 scene.graph.link_nodes(handle, root);
298
299 handle
300}
301
302#[derive(Copy, Clone)]
303enum ShapeHandleValue {
304 Scalar(f32),
305 Vector(Vector3<f32>),
306}
307
308impl ShapeHandleValue {
309 fn into_scalar(self) -> f32 {
310 match self {
311 ShapeHandleValue::Scalar(scalar) => scalar,
312 ShapeHandleValue::Vector(_) => {
313 unreachable!()
314 }
315 }
316 }
317
318 fn into_vector(self) -> Vector3<f32> {
319 match self {
320 ShapeHandleValue::Scalar(_) => unreachable!(),
321 ShapeHandleValue::Vector(vector) => vector,
322 }
323 }
324}
325
326#[derive(Clone)]
327enum ColliderInitialShape {
328 TwoD(dim2::collider::ColliderShape),
329 ThreeD(ColliderShape),
330}
331
332struct DragContext {
333 handle: Handle<Sprite>,
334 initial_handle_position: Vector3<f32>,
335 plane: Plane,
336 initial_value: ShapeHandleValue,
337 handle_major_axis: Option<Vector3<f32>>,
338 plane_kind: Option<PlaneKind>,
339 initial_shape: ColliderInitialShape,
340}
341
342#[derive(TypeUuidProvider)]
343#[type_uuid(id = "a012dd4c-ce6d-4e7e-8879-fd8eddaa9677")]
344pub struct ColliderShapeInteractionMode {
345 collider: Handle<Node>,
346 shape_gizmo: Box<dyn ShapeGizmoTrait>,
347 move_gizmo: MoveGizmo,
348 drag_context: Option<DragContext>,
349 selected_handle: Handle<Sprite>,
350 message_sender: MessageSender,
351}
352
353impl ColliderShapeInteractionMode {
354 fn set_visibility(
355 &mut self,
356 controller: &dyn SceneController,
357 engine: &mut Engine,
358 visibility: bool,
359 ) {
360 let Some(game_scene) = controller.downcast_ref::<GameScene>() else {
361 return;
362 };
363
364 let scene = &mut engine.scenes[game_scene.scene];
365
366 self.shape_gizmo.set_visibility(scene, visibility);
367 }
368}
369
370impl InteractionMode for ColliderShapeInteractionMode {
371 fn on_left_mouse_button_down(
372 &mut self,
373 _editor_selection: &Selection,
374 controller: &mut dyn SceneController,
375 engine: &mut Engine,
376 mouse_position: Vector2<f32>,
377 _frame_size: Vector2<f32>,
378 settings: &Settings,
379 ) {
380 let Some(game_scene) = controller.downcast_mut::<GameScene>() else {
381 return;
382 };
383
384 let scene = &mut engine.scenes[game_scene.scene];
385
386 if let Some(result) = game_scene.camera_controller.pick(
387 &scene.graph,
388 PickingOptions {
389 cursor_pos: mouse_position,
390 editor_only: true,
391 filter: Some(&mut |handle, _| handle != self.move_gizmo.origin),
392 ignore_back_faces: false,
393 use_picking_loop: false,
394 method: Default::default(),
395 settings: &settings.selection,
396 },
397 ) {
398 let initial_position = scene.graph[result.node].global_position();
399 let camera_view_dir = scene.graph[game_scene.camera_controller.camera]
400 .look_vector()
401 .try_normalize(f32::EPSILON)
402 .unwrap_or_default();
403 let plane = Plane::from_normal_and_point(&-camera_view_dir, &initial_position)
404 .unwrap_or_default();
405 let collider_node = &scene.graph[self.collider];
406
407 let initial_shape = if let Some(collider) = collider_node.component_ref::<Collider>() {
408 ColliderInitialShape::ThreeD(collider.shape().clone())
409 } else if let Some(collider_2d) =
410 collider_node.component_ref::<dim2::collider::Collider>()
411 {
412 ColliderInitialShape::TwoD(collider_2d.shape().clone())
413 } else {
414 unreachable!();
415 };
416
417 let handle = result.node.to_variant();
418 if let Some(handle_value) =
419 self.shape_gizmo
420 .value_by_handle(handle, self.collider, scene)
421 {
422 self.selected_handle = handle;
423
424 self.drag_context = Some(DragContext {
425 handle: result.node.to_variant(),
426 initial_handle_position: initial_position,
427 plane,
428 handle_major_axis: self.shape_gizmo.handle_major_axis(
429 self.selected_handle,
430 self.collider,
431 scene,
432 ),
433 initial_value: handle_value,
434 plane_kind: None,
435 initial_shape,
436 })
437 } else if let Some(plane_kind) =
438 self.move_gizmo.handle_pick(result.node, &mut scene.graph)
439 {
440 if let Some(handle_value) =
441 self.shape_gizmo
442 .value_by_handle(self.selected_handle, self.collider, scene)
443 {
444 self.drag_context = Some(DragContext {
445 handle: self.selected_handle,
446 initial_handle_position: initial_position,
447 plane,
448 handle_major_axis: None,
449 initial_value: handle_value,
450 plane_kind: Some(plane_kind),
451 initial_shape,
452 })
453 }
454 }
455 }
456 }
457
458 fn on_left_mouse_button_up(
459 &mut self,
460 _editor_selection: &Selection,
461 controller: &mut dyn SceneController,
462 engine: &mut Engine,
463 _mouse_pos: Vector2<f32>,
464 _frame_size: Vector2<f32>,
465 _settings: &Settings,
466 ) {
467 let Some(game_scene) = controller.downcast_mut::<GameScene>() else {
468 return;
469 };
470
471 let scene = &mut engine.scenes[game_scene.scene];
472
473 if let Some(drag_context) = self.drag_context.take() {
474 let collider = self.collider;
475
476 let value = if let (Ok(collider), ColliderInitialShape::ThreeD(shape)) = (
477 scene.graph.try_get_mut_of_type::<Collider>(collider),
478 drag_context.initial_shape.clone(),
479 ) {
480 Box::new(std::mem::replace(collider.shape_mut(), shape)) as Box<dyn Reflect>
481 } else if let (Ok(collider), ColliderInitialShape::TwoD(shape)) = (
482 scene
483 .graph
484 .try_get_mut_of_type::<dim2::collider::Collider>(collider),
485 drag_context.initial_shape.clone(),
486 ) {
487 Box::new(std::mem::replace(collider.shape_mut(), shape)) as Box<dyn Reflect>
488 } else {
489 unreachable!();
490 };
491
492 let command = SetPropertyCommand::new("shape".into(), value, move |ctx| {
493 ctx.get_mut::<GameSceneContext>()
494 .scene
495 .graph
496 .try_get_node_mut(collider)
497 .ok()
498 .map(|n| n as &mut dyn Reflect)
499 });
500 self.message_sender.do_command(command);
501 }
502 }
503
504 fn on_mouse_move(
505 &mut self,
506 mouse_offset: Vector2<f32>,
507 mouse_position: Vector2<f32>,
508 _editor_selection: &Selection,
509 controller: &mut dyn SceneController,
510 engine: &mut Engine,
511 frame_size: Vector2<f32>,
512 settings: &Settings,
513 ) {
514 let Some(game_scene) = controller.downcast_mut::<GameScene>() else {
515 return;
516 };
517
518 let scene = &mut engine.scenes[game_scene.scene];
519
520 self.shape_gizmo.reset_handles(scene);
521 self.move_gizmo.reset_state(&mut scene.graph);
522
523 if let Some(result) = game_scene.camera_controller.pick(
524 &scene.graph,
525 PickingOptions {
526 cursor_pos: mouse_position,
527 editor_only: true,
528 filter: Some(&mut |handle, _| handle != self.move_gizmo.origin),
529 ignore_back_faces: false,
530 use_picking_loop: false,
531 method: Default::default(),
532 settings: &settings.selection,
533 },
534 ) {
535 if self.shape_gizmo.has_handle(result.node.to_variant()) {
536 scene.graph[result.node]
537 .as_sprite_mut()
538 .set_color(Color::RED);
539 }
540
541 self.move_gizmo.handle_pick(result.node, &mut scene.graph);
542 }
543
544 if let Some(drag_context) = self.drag_context.as_ref() {
545 match drag_context.initial_value {
546 ShapeHandleValue::Scalar(initial_value) => {
547 let camera = &scene.graph[game_scene.camera_controller.camera];
548 let ray = camera.make_ray(mouse_position, frame_size);
549 if let Some(intersection) = ray.plane_intersection_point(&drag_context.plane) {
550 let inv_transform = scene.graph[self.collider]
551 .global_transform()
552 .try_inverse()
553 .unwrap_or_default();
554 let local_space_drag_dir = inv_transform.transform_vector(
555 &(intersection - drag_context.initial_handle_position),
556 );
557 let sign = local_space_drag_dir
558 .dot(&drag_context.handle_major_axis.unwrap_or_default())
559 .signum();
560 let delta = sign
561 * drag_context
562 .initial_handle_position
563 .metric_distance(&intersection);
564
565 self.shape_gizmo.set_value_by_handle(
566 drag_context.handle,
567 ShapeHandleValue::Scalar(initial_value + delta),
568 self.collider,
569 scene,
570 );
571 }
572 }
573 ShapeHandleValue::Vector(_) => {
574 if let Some(plane_kind) = drag_context.plane_kind {
575 let value = self
576 .shape_gizmo
577 .value_by_handle(drag_context.handle, self.collider, scene)
578 .unwrap()
579 .into_vector();
580
581 let global_offset = self.move_gizmo.calculate_offset(
582 &scene.graph,
583 game_scene.camera_controller.camera,
584 mouse_offset,
585 mouse_position,
586 frame_size,
587 plane_kind,
588 );
589
590 let local_offset = scene.graph[self.collider]
591 .global_transform()
592 .try_inverse()
593 .unwrap_or_default()
594 .transform_vector(&global_offset);
595
596 self.shape_gizmo.set_value_by_handle(
597 drag_context.handle,
598 ShapeHandleValue::Vector(value + local_offset),
599 self.collider,
600 scene,
601 );
602 }
603 }
604 }
605 }
606 }
607
608 fn update(
609 &mut self,
610 _editor_selection: &Selection,
611 controller: &mut dyn SceneController,
612 engine: &mut Engine,
613 _settings: &Settings,
614 ) {
615 let Some(game_scene) = controller.downcast_mut::<GameScene>() else {
616 return;
617 };
618
619 let scene = &mut engine.scenes[game_scene.scene];
620
621 if !self.shape_gizmo.try_sync_to_collider(
622 self.collider,
623 game_scene.camera_controller.camera,
624 scene,
625 ) {
626 let new_gizmo =
627 make_shape_gizmo(self.collider, scene, game_scene.editor_objects_root, true);
628
629 let old_gizmo = std::mem::replace(&mut self.shape_gizmo, new_gizmo);
630
631 old_gizmo.destroy(scene);
632 }
633
634 self.move_gizmo.set_visible(
635 &mut scene.graph,
636 self.shape_gizmo.is_vector_handle(self.selected_handle),
637 );
638 let scale = calculate_gizmo_distance_scaling(
639 &scene.graph,
640 game_scene.camera_controller.camera,
641 self.move_gizmo.origin,
642 );
643 if let Some(handle_local_position) =
644 self.shape_gizmo
645 .handle_local_position(self.selected_handle, self.collider, scene)
646 {
647 let transform = scene.graph[self.collider].global_transform();
648 let position = transform
649 .transform_point(&handle_local_position.into())
650 .coords;
651 self.move_gizmo
652 .transform(&mut scene.graph)
653 .set_position(position)
654 .set_scale(scale);
655 }
656 }
657
658 fn activate(&mut self, controller: &dyn SceneController, engine: &mut Engine) {
659 self.set_visibility(controller, engine, true)
660 }
661
662 fn deactivate(&mut self, controller: &dyn SceneController, engine: &mut Engine) {
663 self.set_visibility(controller, engine, false)
664 }
665
666 fn make_button(&mut self, ctx: &mut BuildContext, selected: bool) -> Handle<Button> {
667 make_interaction_mode_button(
668 ctx,
669 include_bytes!("../../../resources/triangle.png"),
670 "Edit Collider Shape",
671 selected,
672 )
673 }
674
675 fn uuid(&self) -> Uuid {
676 Self::type_uuid()
677 }
678}
679
680#[derive(Default)]
681pub struct ColliderPlugin {
682 panel: Option<ColliderControlPanel>,
683}
684
685impl ColliderPlugin {
686 fn on_selection(&mut self, editor: &mut Editor) -> bool {
687 let mut needs_panel = false;
688 let entry = editor.scenes.current_scene_entry_mut();
689 let game_scene = some_or_return!(entry.controller.downcast_mut::<GameScene>(), false);
690 let scene = &mut editor.engine.scenes[game_scene.scene];
691 if let Some(mode) = entry
692 .interaction_modes
693 .remove_typed::<ColliderShapeInteractionMode>()
694 {
695 mode.shape_gizmo.destroy(scene);
696 }
697
698 let first_selected_collider = entry.selection.as_graph().and_then(|n| {
699 n.nodes().iter().find(|h| {
700 scene.graph.has_component::<Collider>(**h)
701 || scene.graph.has_component::<dim2::collider::Collider>(**h)
702 })
703 });
704
705 if let Some(first_selected_collider) = first_selected_collider {
706 needs_panel = true;
707 let shape_gizmo = make_shape_gizmo(
708 *first_selected_collider,
709 scene,
710 game_scene.editor_objects_root,
711 false,
712 );
713
714 let move_gizmo = MoveGizmo::new(game_scene, &mut editor.engine);
715
716 entry.interaction_modes.add(ColliderShapeInteractionMode {
717 collider: *first_selected_collider,
718 shape_gizmo,
719 move_gizmo,
720 drag_context: None,
721 selected_handle: Default::default(),
722 message_sender: editor.message_sender.clone(),
723 });
724 }
725 needs_panel
726 }
727}
728
729impl EditorPlugin for ColliderPlugin {
730 fn on_ui_message(&mut self, message: &mut UiMessage, editor: &mut Editor) {
731 let entry = editor.scenes.current_scene_entry_mut();
732 let game_scene = some_or_return!(entry.controller.downcast_mut::<GameScene>());
733 let panel = some_or_return!(self.panel.as_mut());
734 panel.handle_ui_message(
735 message,
736 &editor.engine,
737 game_scene,
738 &entry.selection,
739 &editor.message_sender,
740 );
741 }
742
743 fn on_message(&mut self, message: &Message, editor: &mut Editor) {
744 if !matches!(message, Message::SelectionChanged { .. }) {
745 return;
746 }
747 let needs_panel = self.on_selection(editor);
748 if needs_panel && self.panel.is_none() {
749 let inspector = editor.plugins.get::<InspectorPlugin>();
750 let ui = editor.engine.user_interfaces.first_mut();
751 let panel = ColliderControlPanel::new(&mut ui.build_ctx());
752 ui.send(panel.root_widget, WidgetMessage::link_with(inspector.head));
753 self.panel = Some(panel);
754 } else if !needs_panel {
755 if let Some(panel) = self.panel.take() {
756 let ui = editor.engine.user_interfaces.first();
757 panel.destroy(ui);
758 }
759 }
760 }
761}