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