Skip to main content

fyroxed_base/plugins/collider/
mod.rs

1// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in all
11// copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21//! Collider shape editing plugin.
22
23mod 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}