bevy_yoleck/
vpeol.rs

1//! # Viewport Editing Overlay - utilities for editing entities from a viewport.
2//!
3//! This module does not do much, but provide common functionalities for more concrete modules like
4//! [`vpeol_2d`](crate::vpeol_2d) and [`vpeol_3d`](crate::vpeol_3d).
5//!
6//! `vpeol` modules also support `bevy_reflect::Reflect` by enabling the feature `beavy_reflect`.
7
8use bevy::camera::primitives::{Aabb, MeshAabb};
9use bevy::camera::RenderTarget;
10use bevy::ecs::query::QueryFilter;
11use bevy::ecs::system::SystemParam;
12use bevy::mesh::VertexAttributeValues;
13use bevy::platform::collections::HashMap;
14use bevy::prelude::*;
15use bevy::render::render_resource::PrimitiveTopology;
16use bevy::window::{PrimaryWindow, WindowRef};
17use bevy_egui::EguiContexts;
18
19use crate::knobs::YoleckKnobMarker;
20use crate::prelude::{YoleckEditorState, YoleckUi};
21use crate::{YoleckDirective, YoleckEditMarker, YoleckManaged, YoleckRunEditSystems};
22
23pub mod prelude {
24    pub use crate::vpeol::{
25        VpeolCameraState, VpeolDragPlane, VpeolRepositionLevel, VpeolSelectionCuePlugin,
26        VpeolWillContainClickableChildren, YoleckKnobClick,
27    };
28    #[cfg(feature = "vpeol_2d")]
29    pub use crate::vpeol_2d::{
30        Vpeol2dCameraControl, Vpeol2dPluginForEditor, Vpeol2dPluginForGame, Vpeol2dPosition,
31        Vpeol2dRotatation, Vpeol2dScale,
32    };
33    #[cfg(feature = "vpeol_3d")]
34    pub use crate::vpeol_3d::{
35        Vpeol3dCameraControl, Vpeol3dPluginForEditor, Vpeol3dPluginForGame, Vpeol3dPosition,
36        Vpeol3dRotation, Vpeol3dScale, Vpeol3dThirdAxisWithKnob,
37    };
38}
39
40/// Order of Vpeol operations. Important for abstraction and backends to talk with each other.
41#[derive(SystemSet, Clone, PartialEq, Eq, Debug, Hash)]
42pub enum VpeolSystems {
43    /// Initialize [`VpeolCameraState`]
44    ///
45    /// * Clear all pointing.
46    /// * Update [`entities_of_interest`](VpeolCameraState::entities_of_interest).
47    /// * Update cursor position (can be overridden later if needed)
48    PrepareCameraState,
49    /// Mostly used by the backend to iterate over the entities and determine which ones are
50    /// being pointed (using [`consider`](VpeolCameraState::consider))
51    UpdateCameraState,
52    /// Interpret the mouse data and pass it back to Yoleck.
53    HandleCameraState,
54}
55
56/// Add base systems common for Vpeol editing.
57pub struct VpeolBasePlugin;
58
59impl Plugin for VpeolBasePlugin {
60    fn build(&self, app: &mut App) {
61        app.configure_sets(
62            Update,
63            (
64                VpeolSystems::PrepareCameraState.run_if(in_state(YoleckEditorState::EditorActive)),
65                VpeolSystems::UpdateCameraState.run_if(in_state(YoleckEditorState::EditorActive)),
66                VpeolSystems::HandleCameraState.run_if(in_state(YoleckEditorState::EditorActive)),
67                YoleckRunEditSystems,
68            )
69                .chain(), // .run_if(in_state(YoleckEditorState::EditorActive)),
70        );
71        app.add_systems(
72            Update,
73            (prepare_camera_state, update_camera_world_position)
74                .in_set(VpeolSystems::PrepareCameraState),
75        );
76        app.add_systems(
77            Update,
78            handle_camera_state.in_set(VpeolSystems::HandleCameraState),
79        );
80        #[cfg(feature = "bevy_reflect")]
81        app.register_type::<VpeolDragPlane>();
82    }
83}
84
85/// A plane to define the drag direction of entities.
86///
87/// This is both a component and a resource. Entities that have the component will use the plane
88/// defined by it, while entities that don't will use the global one defined by the resource.
89/// Child entities will use the plane of the root Yoleck managed entity (if it has one). Knobs will
90/// use the one attached to the knob entity.
91///
92/// This configuration is only meaningful for 3D, but vpeol_2d still requires it resource.
93/// `Vpeol2dPluginForEditor` already adds it as `Vec3::Z`. Don't modify it.
94#[derive(Component, Resource)]
95#[cfg_attr(feature = "bevy_reflect", derive(bevy::reflect::Reflect))]
96pub struct VpeolDragPlane(pub InfinitePlane3d);
97
98impl VpeolDragPlane {
99    pub const XY: VpeolDragPlane = VpeolDragPlane(InfinitePlane3d { normal: Dir3::Z });
100    pub const XZ: VpeolDragPlane = VpeolDragPlane(InfinitePlane3d { normal: Dir3::Y });
101}
102
103/// Data passed between Vpeol abstraction and backends.
104#[derive(Component, Default, Debug)]
105pub struct VpeolCameraState {
106    /// Where this camera considers the cursor to be in the world.
107    pub cursor_ray: Option<Ray3d>,
108    /// The topmost entity being pointed by the cursor.
109    pub entity_under_cursor: Option<(Entity, VpeolCursorPointing)>,
110    /// Entities that may or may not be topmost, but the editor needs to know whether or not they
111    /// are pointed at.
112    pub entities_of_interest: HashMap<Entity, Option<VpeolCursorPointing>>,
113    /// The mouse selection state.
114    pub clicks_on_objects_state: VpeolClicksOnObjectsState,
115}
116
117/// Information on how the cursor is pointing at an entity.
118#[derive(Clone, Debug)]
119pub struct VpeolCursorPointing {
120    /// The location on the entity, in world coords, where the cursor is pointing.
121    pub cursor_position_world_coords: Vec3,
122    /// Used to determine entity selection priorities.
123    pub z_depth_screen_coords: f32,
124}
125
126/// State for determining how the user is interacting with entities using the mouse buttons.
127#[derive(Default, Debug)]
128pub enum VpeolClicksOnObjectsState {
129    #[default]
130    Empty,
131    BeingDragged {
132        entity: Entity,
133        /// Used for deciding if the cursor has moved.
134        prev_screen_pos: Vec2,
135        /// Offset from the entity's center to the cursor's position on the drag plane.
136        offset: Vec3,
137        select_on_mouse_release: bool,
138    },
139}
140
141impl VpeolCameraState {
142    /// Tell Vpeol the the user is pointing at an entity.
143    ///
144    /// This function may ignore the input if the entity is covered by another entity and is not an
145    /// entity of interest.
146    pub fn consider(
147        &mut self,
148        entity: Entity,
149        z_depth_screen_coords: f32,
150        cursor_position_world_coords: impl FnOnce() -> Vec3,
151    ) {
152        let should_update_entity = if let Some((_, old_cursor)) = self.entity_under_cursor.as_ref()
153        {
154            old_cursor.z_depth_screen_coords < z_depth_screen_coords
155        } else {
156            true
157        };
158
159        if let Some(of_interest) = self.entities_of_interest.get_mut(&entity) {
160            let pointing = VpeolCursorPointing {
161                cursor_position_world_coords: cursor_position_world_coords(),
162                z_depth_screen_coords,
163            };
164            if should_update_entity {
165                self.entity_under_cursor = Some((entity, pointing.clone()));
166            }
167            *of_interest = Some(pointing);
168        } else if should_update_entity {
169            self.entity_under_cursor = Some((
170                entity,
171                VpeolCursorPointing {
172                    cursor_position_world_coords: cursor_position_world_coords(),
173                    z_depth_screen_coords,
174                },
175            ));
176        }
177    }
178
179    pub fn pointing_at_entity(&self, entity: Entity) -> Option<&VpeolCursorPointing> {
180        if let Some((entity_under_cursor, pointing_at)) = &self.entity_under_cursor {
181            if *entity_under_cursor == entity {
182                return Some(pointing_at);
183            }
184        }
185        self.entities_of_interest.get(&entity)?.as_ref()
186    }
187}
188
189fn prepare_camera_state(
190    mut query: Query<&mut VpeolCameraState>,
191    knob_query: Query<Entity, With<YoleckKnobMarker>>,
192) {
193    for mut camera_state in query.iter_mut() {
194        camera_state.entity_under_cursor = None;
195        camera_state.entities_of_interest = knob_query
196            .iter()
197            .chain(match camera_state.clicks_on_objects_state {
198                VpeolClicksOnObjectsState::Empty => None,
199                VpeolClicksOnObjectsState::BeingDragged { entity, .. } => Some(entity),
200            })
201            .map(|entity| (entity, None))
202            .collect();
203    }
204}
205
206fn update_camera_world_position(
207    mut cameras_query: Query<(&mut VpeolCameraState, &GlobalTransform, &Camera)>,
208    window_getter: WindowGetter,
209) {
210    for (mut camera_state, camera_transform, camera) in cameras_query.iter_mut() {
211        camera_state.cursor_ray = (|| {
212            let RenderTarget::Window(window_ref) = camera.target else {
213                return None;
214            };
215            let window = window_getter.get_window(window_ref)?;
216            let cursor_in_screen_pos = window.cursor_position()?;
217            camera
218                .viewport_to_world(camera_transform, cursor_in_screen_pos)
219                .ok()
220        })();
221    }
222}
223
224#[derive(SystemParam)]
225pub(crate) struct WindowGetter<'w, 's> {
226    windows: Query<'w, 's, &'static Window>,
227    primary_window: Query<'w, 's, &'static Window, With<PrimaryWindow>>,
228}
229
230impl WindowGetter<'_, '_> {
231    pub fn get_window(&self, window_ref: WindowRef) -> Option<&Window> {
232        match window_ref {
233            WindowRef::Primary => self.primary_window.single().ok(),
234            WindowRef::Entity(window_id) => self.windows.get(window_id).ok(),
235        }
236    }
237}
238
239#[allow(clippy::too_many_arguments)]
240fn handle_camera_state(
241    mut egui_context: EguiContexts,
242    mut query: Query<(&Camera, &mut VpeolCameraState)>,
243    window_getter: WindowGetter,
244    mouse_buttons: Res<ButtonInput<MouseButton>>,
245    keyboard: Res<ButtonInput<KeyCode>>,
246    global_transform_query: Query<&GlobalTransform>,
247    selected_query: Query<(), With<YoleckEditMarker>>,
248    knob_query: Query<Entity, With<YoleckKnobMarker>>,
249    mut directives_writer: MessageWriter<YoleckDirective>,
250    global_drag_plane: Res<VpeolDragPlane>,
251    drag_plane_overrides_query: Query<&VpeolDragPlane>,
252) -> Result {
253    enum MouseButtonOp {
254        JustPressed,
255        BeingPressed,
256        JustReleased,
257    }
258    let mouse_button_op = if mouse_buttons.just_pressed(MouseButton::Left) {
259        if egui_context.ctx_mut()?.is_pointer_over_area() {
260            return Ok(());
261        }
262        MouseButtonOp::JustPressed
263    } else if mouse_buttons.just_released(MouseButton::Left) {
264        MouseButtonOp::JustReleased
265    } else if mouse_buttons.pressed(MouseButton::Left) {
266        MouseButtonOp::BeingPressed
267    } else {
268        for (_, mut camera_state) in query.iter_mut() {
269            camera_state.clicks_on_objects_state = VpeolClicksOnObjectsState::Empty;
270        }
271        return Ok(());
272    };
273    for (camera, mut camera_state) in query.iter_mut() {
274        let Some(cursor_ray) = camera_state.cursor_ray else {
275            continue;
276        };
277        let calc_cursor_in_world_position = |entity: Entity, plane_origin: Vec3| -> Option<Vec3> {
278            let VpeolDragPlane(drag_plane) = drag_plane_overrides_query
279                .get(entity)
280                .unwrap_or(&global_drag_plane);
281            let distance = cursor_ray.intersect_plane(plane_origin, *drag_plane)?;
282            Some(cursor_ray.get_point(distance))
283        };
284
285        let RenderTarget::Window(window_ref) = camera.target else {
286            continue;
287        };
288        let Some(window) = window_getter.get_window(window_ref) else {
289            continue;
290        };
291        let Some(cursor_in_screen_pos) = window.cursor_position() else {
292            continue;
293        };
294
295        match (&mouse_button_op, &camera_state.clicks_on_objects_state) {
296            (MouseButtonOp::JustPressed, VpeolClicksOnObjectsState::Empty) => {
297                if keyboard.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]) {
298                    if let Some((entity, _)) = &camera_state.entity_under_cursor {
299                        directives_writer.write(YoleckDirective::toggle_selected(*entity));
300                    }
301                } else if let Some((knob_entity, cursor_pointing)) =
302                    knob_query.iter().find_map(|knob_entity| {
303                        Some((knob_entity, camera_state.pointing_at_entity(knob_entity)?))
304                    })
305                {
306                    directives_writer.write(YoleckDirective::pass_to_entity(
307                        knob_entity,
308                        YoleckKnobClick,
309                    ));
310                    let Ok(knob_transform) = global_transform_query.get(knob_entity) else {
311                        continue;
312                    };
313                    let Some(cursor_in_world_position) = calc_cursor_in_world_position(
314                        knob_entity,
315                        cursor_pointing.cursor_position_world_coords,
316                    ) else {
317                        continue;
318                    };
319                    camera_state.clicks_on_objects_state = VpeolClicksOnObjectsState::BeingDragged {
320                        entity: knob_entity,
321                        prev_screen_pos: cursor_in_screen_pos,
322                        offset: cursor_in_world_position - knob_transform.translation(),
323                        select_on_mouse_release: false,
324                    }
325                } else {
326                    camera_state.clicks_on_objects_state = if let Some((entity, cursor_pointing)) =
327                        &camera_state.entity_under_cursor
328                    {
329                        let Ok(entity_transform) = global_transform_query.get(*entity) else {
330                            continue;
331                        };
332                        let select_on_mouse_release = selected_query.contains(*entity);
333                        if !select_on_mouse_release {
334                            directives_writer.write(YoleckDirective::set_selected(Some(*entity)));
335                        }
336                        let Some(cursor_in_world_position) = calc_cursor_in_world_position(
337                            *entity,
338                            cursor_pointing.cursor_position_world_coords,
339                        ) else {
340                            continue;
341                        };
342                        VpeolClicksOnObjectsState::BeingDragged {
343                            entity: *entity,
344                            prev_screen_pos: cursor_in_screen_pos,
345                            offset: cursor_in_world_position - entity_transform.translation(),
346                            select_on_mouse_release,
347                        }
348                    } else {
349                        directives_writer.write(YoleckDirective::set_selected(None));
350                        VpeolClicksOnObjectsState::Empty
351                    };
352                }
353            }
354            (
355                MouseButtonOp::BeingPressed,
356                VpeolClicksOnObjectsState::BeingDragged {
357                    entity,
358                    prev_screen_pos,
359                    offset,
360                    select_on_mouse_release: _,
361                },
362            ) => {
363                if 0.1 <= prev_screen_pos.distance_squared(cursor_in_screen_pos) {
364                    let Ok(entity_transform) = global_transform_query.get(*entity) else {
365                        continue;
366                    };
367                    let drag_point = entity_transform.translation() + *offset;
368                    let Some(cursor_in_world_position) =
369                        calc_cursor_in_world_position(*entity, drag_point)
370                    else {
371                        continue;
372                    };
373                    directives_writer.write(YoleckDirective::pass_to_entity(
374                        *entity,
375                        cursor_in_world_position - *offset,
376                    ));
377                    camera_state.clicks_on_objects_state =
378                        VpeolClicksOnObjectsState::BeingDragged {
379                            entity: *entity,
380                            prev_screen_pos: cursor_in_screen_pos,
381                            offset: *offset,
382                            select_on_mouse_release: false,
383                        };
384                }
385            }
386            (
387                MouseButtonOp::JustReleased,
388                VpeolClicksOnObjectsState::BeingDragged {
389                    entity,
390                    prev_screen_pos: _,
391                    offset: _,
392                    select_on_mouse_release: true,
393                },
394            ) => {
395                directives_writer.write(YoleckDirective::set_selected(Some(*entity)));
396                camera_state.clicks_on_objects_state = VpeolClicksOnObjectsState::Empty;
397            }
398            _ => {}
399        }
400    }
401    Ok(())
402}
403
404/// A [passed data](crate::knobs::YoleckKnobHandle::get_passed_data) to a knob entity that indicate
405/// it was clicked by the level editor.
406pub struct YoleckKnobClick;
407
408/// Marker for entities that will be interacted in the viewport using their children.
409///
410/// Populate systems should mark the entity with this component when applicable. The viewport
411/// overlay plugin is responsible for handling it by using [`handle_clickable_children_system`].
412#[derive(Component)]
413pub struct VpeolWillContainClickableChildren;
414
415/// Marker for viewport editor overlay plugins to route child interaction to parent entities.
416#[derive(Component)]
417pub struct VpeolRouteClickTo(pub Entity);
418
419/// Helper utility for finding the Yoleck controlled entity that's in charge of an entity the user
420/// points at.
421#[derive(SystemParam)]
422pub struct VpeolRootResolver<'w, 's> {
423    root_resolver: Query<'w, 's, &'static VpeolRouteClickTo>,
424    #[allow(clippy::type_complexity)]
425    has_managed_query: Query<'w, 's, (), Or<(With<YoleckManaged>, With<YoleckKnobMarker>)>>,
426}
427
428impl VpeolRootResolver<'_, '_> {
429    /// Find the Yoleck controlled entity that's in charge of an entity the user points at.
430    pub fn resolve_root(&self, entity: Entity) -> Option<Entity> {
431        if let Ok(VpeolRouteClickTo(root_entity)) = self.root_resolver.get(entity) {
432            Some(*root_entity)
433        } else {
434            self.has_managed_query.get(entity).ok()?;
435            Some(entity)
436        }
437    }
438}
439
440/// Add [`VpeolRouteClickTo`] of entities marked with [`VpeolWillContainClickableChildren`].
441pub fn handle_clickable_children_system<F, B>(
442    parents_query: Query<(Entity, &Children), With<VpeolWillContainClickableChildren>>,
443    children_query: Query<&Children>,
444    should_add_query: Query<Entity, F>,
445    mut commands: Commands,
446) where
447    F: QueryFilter,
448    B: Default + Bundle,
449{
450    for (parent, children) in parents_query.iter() {
451        if children.is_empty() {
452            continue;
453        }
454        let mut any_added = false;
455        let mut children_to_check: Vec<Entity> = children.iter().collect();
456        while let Some(child) = children_to_check.pop() {
457            if let Ok(child_children) = children_query.get(child) {
458                children_to_check.extend(child_children.iter());
459            }
460            if should_add_query.get(child).is_ok() {
461                commands
462                    .entity(child)
463                    .try_insert((VpeolRouteClickTo(parent), B::default()));
464                any_added = true;
465            }
466        }
467        if any_added {
468            commands
469                .entity(parent)
470                .remove::<VpeolWillContainClickableChildren>();
471        }
472    }
473}
474
475/// Add a pulse effect when an entity is being selected.
476pub struct VpeolSelectionCuePlugin {
477    /// How long, in seconds, the entire pulse effect will take. Defaults to 0.3.
478    pub effect_duration: f32,
479    /// By how much (relative to original size) the entity will grow during the pulse. Defaults to 0.3.
480    pub effect_magnitude: f32,
481}
482
483impl Default for VpeolSelectionCuePlugin {
484    fn default() -> Self {
485        Self {
486            effect_duration: 0.3,
487            effect_magnitude: 0.3,
488        }
489    }
490}
491
492impl Plugin for VpeolSelectionCuePlugin {
493    fn build(&self, app: &mut App) {
494        app.add_systems(Update, manage_selection_transform_components);
495        app.add_systems(PostUpdate, {
496            add_selection_cue_before_transform_propagate(
497                1.0 / self.effect_duration,
498                2.0 * self.effect_magnitude,
499            )
500            .before(TransformSystems::Propagate)
501        });
502        app.add_systems(PostUpdate, {
503            restore_transform_from_cache_after_transform_propagate
504                .after(TransformSystems::Propagate)
505        });
506    }
507}
508
509#[derive(Component)]
510struct SelectionCueAnimation {
511    cached_transform: Transform,
512    progress: f32,
513}
514
515fn manage_selection_transform_components(
516    add_cue_query: Query<Entity, (Without<SelectionCueAnimation>, With<YoleckEditMarker>)>,
517    remove_cue_query: Query<Entity, (With<SelectionCueAnimation>, Without<YoleckEditMarker>)>,
518    mut commands: Commands,
519) {
520    for entity in add_cue_query.iter() {
521        commands.entity(entity).insert(SelectionCueAnimation {
522            cached_transform: Default::default(),
523            progress: 0.0,
524        });
525    }
526    for entity in remove_cue_query.iter() {
527        commands.entity(entity).remove::<SelectionCueAnimation>();
528    }
529}
530
531fn add_selection_cue_before_transform_propagate(
532    time_speedup: f32,
533    magnitude_scale: f32,
534) -> impl FnMut(Query<(&mut SelectionCueAnimation, &mut Transform)>, Res<Time>) {
535    move |mut query, time| {
536        for (mut animation, mut transform) in query.iter_mut() {
537            animation.cached_transform = *transform;
538            if animation.progress < 1.0 {
539                animation.progress += time_speedup * time.delta_secs();
540                let extra = if animation.progress < 0.5 {
541                    animation.progress
542                } else {
543                    1.0 - animation.progress
544                };
545                transform.scale *= 1.0 + magnitude_scale * extra;
546            }
547        }
548    }
549}
550
551fn restore_transform_from_cache_after_transform_propagate(
552    mut query: Query<(&SelectionCueAnimation, &mut Transform)>,
553) {
554    for (animation, mut transform) in query.iter_mut() {
555        *transform = animation.cached_transform;
556    }
557}
558
559pub(crate) fn ray_intersection_with_mesh(ray: Ray3d, mesh: &Mesh) -> Option<f32> {
560    let aabb = mesh.compute_aabb()?;
561    let distance_to_aabb = ray_intersection_with_aabb(ray, aabb)?;
562
563    if let Some(mut triangles) = iter_triangles(mesh) {
564        triangles.find_map(|triangle| triangle.ray_intersection(ray))
565    } else {
566        Some(distance_to_aabb)
567    }
568}
569
570fn ray_intersection_with_aabb(ray: Ray3d, aabb: Aabb) -> Option<f32> {
571    let center: Vec3 = aabb.center.into();
572    let mut max_low = f32::NEG_INFINITY;
573    let mut min_high = f32::INFINITY;
574    for (axis, half_extent) in [
575        (Vec3::X, aabb.half_extents.x),
576        (Vec3::Y, aabb.half_extents.y),
577        (Vec3::Z, aabb.half_extents.z),
578    ] {
579        let dot = ray.direction.dot(axis);
580        if dot == 0.0 {
581            let distance_from_center = (ray.origin - center).dot(axis);
582            if half_extent < distance_from_center.abs() {
583                return None;
584            }
585        } else {
586            let low = ray.intersect_plane(center - half_extent * axis, InfinitePlane3d::new(axis));
587            let high = ray.intersect_plane(center + half_extent * axis, InfinitePlane3d::new(axis));
588            let (low, high) = if 0.0 <= dot { (low, high) } else { (high, low) };
589            if let Some(low) = low {
590                max_low = max_low.max(low);
591            }
592            if let Some(high) = high {
593                min_high = min_high.min(high);
594            } else {
595                return None;
596            }
597        }
598    }
599    if max_low <= min_high {
600        Some(max_low)
601    } else {
602        None
603    }
604}
605
606fn iter_triangles(mesh: &Mesh) -> Option<impl '_ + Iterator<Item = Triangle>> {
607    if mesh.primitive_topology() != PrimitiveTopology::TriangleList {
608        return None;
609    }
610    let indices = mesh.indices()?;
611    let Some(VertexAttributeValues::Float32x3(positions)) =
612        mesh.attribute(Mesh::ATTRIBUTE_POSITION)
613    else {
614        return None;
615    };
616    let mut it = indices.iter();
617    Some(std::iter::from_fn(move || {
618        Some(Triangle(
619            [it.next()?, it.next()?, it.next()?].map(|idx| Vec3::from_array(positions[idx])),
620        ))
621    }))
622}
623
624#[derive(Debug)]
625struct Triangle([Vec3; 3]);
626
627impl Triangle {
628    fn ray_intersection(&self, ray: Ray3d) -> Option<f32> {
629        let directions = [
630            self.0[1] - self.0[0],
631            self.0[2] - self.0[1],
632            self.0[0] - self.0[2],
633        ];
634        let normal = directions[0].cross(directions[1]); // no need to normalize it
635        let plane = InfinitePlane3d {
636            normal: Dir3::new(normal).ok()?,
637        };
638        let distance = ray.intersect_plane(self.0[0], plane)?;
639        let point = ray.get_point(distance);
640        if self
641            .0
642            .iter()
643            .zip(directions.iter())
644            .all(|(vertex, direction)| {
645                let vertical = direction.cross(normal);
646                vertical.dot(point - *vertex) <= 0.0
647            })
648        {
649            Some(distance)
650        } else {
651            None
652        }
653    }
654}
655
656/// Detects an entity that's being clicked on. Meant to be used with [Yoleck's exclusive edit
657/// systems](crate::exclusive_systems::YoleckExclusiveSystemsQueue) and with Bevy's system piping.
658///
659/// Note that this only returns `Some` when the user clicks on an entity - it does not finish the
660/// exclusive system. The other systems that this gets piped into should decide whether or not it
661/// should be finished.
662pub fn vpeol_read_click_on_entity<Filter: QueryFilter>(
663    mut ui: ResMut<YoleckUi>,
664    cameras_query: Query<&VpeolCameraState>,
665    yoleck_managed_query: Query<&YoleckManaged>,
666    filter_query: Query<(), Filter>,
667    buttons: Res<ButtonInput<MouseButton>>,
668    mut candidate: Local<Option<Entity>>,
669) -> Option<Entity> {
670    let target = if ui.ctx().is_pointer_over_area() {
671        None
672    } else {
673        cameras_query
674            .iter()
675            .find_map(|camera_state| Some(camera_state.entity_under_cursor.as_ref()?.0))
676    };
677
678    let Some(target) = target else {
679        ui.label("No Target");
680        return None;
681    };
682
683    let Ok(yoleck_managed) = yoleck_managed_query.get(target) else {
684        ui.label("No Target");
685        return None;
686    };
687
688    if !filter_query.contains(target) {
689        ui.label(format!("Invalid Target ({})", yoleck_managed.type_name));
690        return None;
691    }
692    ui.label(format!(
693        "Targeting {:?} ({})",
694        target, yoleck_managed.type_name
695    ));
696
697    if buttons.just_pressed(MouseButton::Left) {
698        *candidate = Some(target);
699    } else if buttons.just_released(MouseButton::Left) {
700        if let Some(candidate) = candidate.take() {
701            if candidate == target {
702                return Some(target);
703            }
704        }
705    }
706    None
707}
708
709/// Apply a transform to every entity in the level.
710///
711/// Note that:
712/// * It is the duty of [`vpeol_2d`](crate::vpeol_2d)/[`vpeol_3d`](crate::vpeol_3d) to handle the
713///   actual repositioning, and they do so only for entities that use their existing components
714///   ([`Vpeol2dPosition`](crate::vpeol_2d::Vpeol2dPosition)/[`Vpeol3dPosition`](crate::vpeol_3d::Vpeol3dPosition)
715///   and friends). If there are entities that do not use these mechanisms, it falls under the
716///   responsibility of whatever populates their `Transform` to take this component (of their level
717///   entity) into account.
718/// * The repositioning is done directly on the `Transform` - not on the `GlobalTransform`.
719#[derive(Component)]
720pub struct VpeolRepositionLevel(pub Transform);