Skip to main content

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::RenderTarget;
9use bevy::camera::primitives::{Aabb, MeshAabb};
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, egui};
18
19use crate::entity_management::YoleckRawEntry;
20use crate::knobs::YoleckKnobMarker;
21use crate::prelude::{YoleckEditorState, YoleckUi};
22use crate::{
23    YoleckDirective, YoleckEditMarker, YoleckEditorEvent, YoleckEditorViewportRect,
24    YoleckEntityConstructionSpecs, YoleckManaged, YoleckRunEditSystems, YoleckState,
25};
26
27pub mod prelude {
28    pub use crate::vpeol::{
29        VpeolCameraState, VpeolDragPlane, VpeolOverrideDragPlane, VpeolRepositionLevel,
30        VpeolSelectionCuePlugin, VpeolWillContainClickableChildren, YoleckKnobClick,
31    };
32    #[cfg(feature = "vpeol_2d")]
33    pub use crate::vpeol_2d::{
34        Vpeol2dCameraControl, Vpeol2dPluginForEditor, Vpeol2dPluginForGame, Vpeol2dPosition,
35        Vpeol2dRotatation, Vpeol2dScale,
36    };
37    #[cfg(feature = "vpeol_3d")]
38    pub use crate::vpeol_3d::{
39        Vpeol3dCameraControl, Vpeol3dCameraMode, Vpeol3dPluginForEditor, Vpeol3dPluginForGame,
40        Vpeol3dPosition, Vpeol3dRotation, Vpeol3dScale, Vpeol3dSnapToPlane,
41        Vpeol3dTranslationGizmoConfig, Vpeol3dTranslationGizmoMode, YoleckCameraChoices,
42    };
43}
44
45/// Order of Vpeol operations. Important for abstraction and backends to talk with each other.
46#[derive(SystemSet, Clone, PartialEq, Eq, Debug, Hash)]
47pub enum VpeolSystems {
48    /// Initialize [`VpeolCameraState`]
49    ///
50    /// * Clear all pointing.
51    /// * Update [`entities_of_interest`](VpeolCameraState::entities_of_interest).
52    /// * Update cursor position (can be overridden later if needed)
53    PrepareCameraState,
54    /// Mostly used by the backend to iterate over the entities and determine which ones are
55    /// being pointed (using [`consider`](VpeolCameraState::consider))
56    UpdateCameraState,
57    /// Interpret the mouse data and pass it back to Yoleck.
58    HandleCameraState,
59}
60
61/// Add base systems common for Vpeol editing.
62pub struct VpeolBasePlugin;
63
64impl Plugin for VpeolBasePlugin {
65    fn build(&self, app: &mut App) {
66        app.configure_sets(
67            Update,
68            (
69                VpeolSystems::PrepareCameraState.run_if(in_state(YoleckEditorState::EditorActive)),
70                VpeolSystems::UpdateCameraState.run_if(in_state(YoleckEditorState::EditorActive)),
71                VpeolSystems::HandleCameraState.run_if(in_state(YoleckEditorState::EditorActive)),
72                YoleckRunEditSystems,
73            )
74                .chain(), // .run_if(in_state(YoleckEditorState::EditorActive)),
75        );
76        app.init_resource::<VpeolClipboard>();
77        app.add_systems(
78            Update,
79            (prepare_camera_state, update_camera_world_position)
80                .in_set(VpeolSystems::PrepareCameraState),
81        );
82        app.add_systems(
83            Update,
84            handle_camera_state.in_set(VpeolSystems::HandleCameraState),
85        );
86        app.add_systems(
87            Update,
88            (
89                handle_delete_entity_key,
90                handle_copy_entity_key,
91                handle_paste_entity_key,
92            )
93                .run_if(in_state(YoleckEditorState::EditorActive)),
94        );
95        #[cfg(feature = "bevy_reflect")]
96        app.register_type::<VpeolDragPlane>()
97            .register_type::<VpeolOverrideDragPlane>();
98    }
99}
100
101/// A plane to define the drag direction of entities.
102///
103/// This is a global resource, affecting all Vpeol controlled entities. It can be overridden for
104/// specific entities using [`VpeolOverrideDragPlane`].
105///
106/// This is both a component and a resource. Entities that have the component will use the plane
107/// defined by it, while entities that don't will use the global one defined by the resource. Child
108/// entities will use the plane of the root Yoleck managed entity (if it has one). Knobs will use
109/// the one attached to the knob entity.
110///
111/// This configuration is only meaningful for 3D, but vpeol_2d still requires this resource.
112/// `Vpeol2dPluginForEditor` already adds it as `Vec3::Z`. Don't modify it.
113#[derive(Resource)]
114#[cfg_attr(feature = "bevy_reflect", derive(bevy::reflect::Reflect))]
115pub struct VpeolDragPlane(pub InfinitePlane3d);
116
117impl VpeolDragPlane {
118    pub const XY: Self = Self(InfinitePlane3d { normal: Dir3::Z });
119    pub const XZ: Self = Self(InfinitePlane3d { normal: Dir3::Y });
120}
121
122/// A plane to define the drag direction of specific entities.
123///
124/// This is the component version of the global [`VpeolDragPlane`] resource.
125///
126/// Child entities will use the plane of the root Yoleck managed entity if it has one - otherwise
127/// they'll revert to [`VpeolDragPlane`]. Knobs will use the one attached to the knob entity.
128#[derive(Component)]
129#[cfg_attr(feature = "bevy_reflect", derive(bevy::reflect::Reflect))]
130pub struct VpeolOverrideDragPlane(pub InfinitePlane3d);
131
132impl VpeolOverrideDragPlane {
133    pub const XY: Self = Self(InfinitePlane3d { normal: Dir3::Z });
134    pub const XZ: Self = Self(InfinitePlane3d { normal: Dir3::Y });
135}
136
137/// Data passed between Vpeol abstraction and backends.
138#[derive(Component, Default, Debug)]
139pub struct VpeolCameraState {
140    /// Where this camera considers the cursor to be in the world.
141    pub cursor_ray: Option<Ray3d>,
142    /// The topmost entity being pointed by the cursor.
143    pub entity_under_cursor: Option<(Entity, VpeolCursorPointing)>,
144    /// Entities that may or may not be topmost, but the editor needs to know whether or not they
145    /// are pointed at.
146    pub entities_of_interest: HashMap<Entity, Option<VpeolCursorPointing>>,
147    /// The mouse selection state.
148    pub clicks_on_objects_state: VpeolClicksOnObjectsState,
149}
150
151impl FromTemplate for VpeolCameraState {
152    type Template = VpeolCameraStateTemplate;
153}
154
155#[derive(Default)]
156pub struct VpeolCameraStateTemplate;
157
158impl Template for VpeolCameraStateTemplate {
159    type Output = VpeolCameraState;
160
161    fn build_template(&self, _: &mut bevy::ecs::template::TemplateContext) -> Result<Self::Output> {
162        Ok(VpeolCameraState::default())
163    }
164
165    fn clone_template(&self) -> Self {
166        Self
167    }
168}
169
170/// Information on how the cursor is pointing at an entity.
171#[derive(Clone, Debug)]
172pub struct VpeolCursorPointing {
173    /// The location on the entity, in world coords, where the cursor is pointing.
174    pub cursor_position_world_coords: Vec3,
175    /// Used to determine entity selection priorities.
176    pub z_depth_screen_coords: f32,
177}
178
179/// State for determining how the user is interacting with entities using the mouse buttons.
180#[derive(Default, Debug)]
181pub enum VpeolClicksOnObjectsState {
182    #[default]
183    Empty,
184    BeingDragged {
185        entity: Entity,
186        /// Used for deciding if the cursor has moved.
187        prev_screen_pos: Vec2,
188        /// Offset from the entity's center to the cursor's position on the drag plane.
189        offset: Vec3,
190        select_on_mouse_release: bool,
191    },
192}
193
194impl VpeolCameraState {
195    /// Tell Vpeol the the user is pointing at an entity.
196    ///
197    /// This function may ignore the input if the entity is covered by another entity and is not an
198    /// entity of interest.
199    pub fn consider(
200        &mut self,
201        entity: Entity,
202        z_depth_screen_coords: f32,
203        cursor_position_world_coords: impl FnOnce() -> Vec3,
204    ) {
205        let should_update_entity = if let Some((_, old_cursor)) = self.entity_under_cursor.as_ref()
206        {
207            old_cursor.z_depth_screen_coords < z_depth_screen_coords
208        } else {
209            true
210        };
211
212        if let Some(of_interest) = self.entities_of_interest.get_mut(&entity) {
213            let pointing = VpeolCursorPointing {
214                cursor_position_world_coords: cursor_position_world_coords(),
215                z_depth_screen_coords,
216            };
217            if should_update_entity {
218                self.entity_under_cursor = Some((entity, pointing.clone()));
219            }
220            *of_interest = Some(pointing);
221        } else if should_update_entity {
222            self.entity_under_cursor = Some((
223                entity,
224                VpeolCursorPointing {
225                    cursor_position_world_coords: cursor_position_world_coords(),
226                    z_depth_screen_coords,
227                },
228            ));
229        }
230    }
231
232    pub fn pointing_at_entity(&self, entity: Entity) -> Option<&VpeolCursorPointing> {
233        if let Some((entity_under_cursor, pointing_at)) = &self.entity_under_cursor
234            && *entity_under_cursor == entity
235        {
236            return Some(pointing_at);
237        }
238        self.entities_of_interest.get(&entity)?.as_ref()
239    }
240}
241
242fn prepare_camera_state(
243    mut query: Query<&mut VpeolCameraState>,
244    knob_query: Query<Entity, With<YoleckKnobMarker>>,
245) {
246    for mut camera_state in query.iter_mut() {
247        camera_state.entity_under_cursor = None;
248        camera_state.entities_of_interest = knob_query
249            .iter()
250            .chain(match camera_state.clicks_on_objects_state {
251                VpeolClicksOnObjectsState::Empty => None,
252                VpeolClicksOnObjectsState::BeingDragged { entity, .. } => Some(entity),
253            })
254            .map(|entity| (entity, None))
255            .collect();
256    }
257}
258
259fn update_camera_world_position(
260    mut cameras_query: Query<(
261        &mut VpeolCameraState,
262        &GlobalTransform,
263        &Camera,
264        &RenderTarget,
265    )>,
266    window_getter: WindowGetter,
267) {
268    for (mut camera_state, camera_transform, camera, render_target) in cameras_query.iter_mut() {
269        camera_state.cursor_ray = (|| {
270            let RenderTarget::Window(window_ref) = render_target else {
271                return None;
272            };
273            let window = window_getter.get_window(*window_ref)?;
274            let cursor_in_screen_pos = window.cursor_position()?;
275            camera
276                .viewport_to_world(camera_transform, cursor_in_screen_pos)
277                .ok()
278        })();
279    }
280}
281
282#[derive(SystemParam)]
283pub(crate) struct WindowGetter<'w, 's> {
284    windows: Query<'w, 's, &'static Window>,
285    primary_window: Query<'w, 's, &'static Window, With<PrimaryWindow>>,
286}
287
288impl WindowGetter<'_, '_> {
289    pub fn get_window(&self, window_ref: WindowRef) -> Option<&Window> {
290        match window_ref {
291            WindowRef::Primary => self.primary_window.single().ok(),
292            WindowRef::Entity(window_id) => self.windows.get(window_id).ok(),
293        }
294    }
295}
296
297#[allow(clippy::too_many_arguments)]
298fn handle_camera_state(
299    mut egui_context: EguiContexts,
300    mut query: Query<(&RenderTarget, &mut VpeolCameraState)>,
301    window_getter: WindowGetter,
302    mouse_buttons: Res<ButtonInput<MouseButton>>,
303    keyboard: Res<ButtonInput<KeyCode>>,
304    global_transform_query: Query<&GlobalTransform>,
305    selected_query: Query<(), With<YoleckEditMarker>>,
306    knob_query: Query<Entity, With<YoleckKnobMarker>>,
307    mut directives_writer: MessageWriter<YoleckDirective>,
308    global_drag_plane: Res<VpeolDragPlane>,
309    drag_plane_overrides_query: Query<&VpeolOverrideDragPlane>,
310    editor_viewport: Res<YoleckEditorViewportRect>,
311) -> Result {
312    enum MouseButtonOp {
313        JustPressed,
314        BeingPressed,
315        JustReleased,
316    }
317    let mouse_button_op = if mouse_buttons.just_pressed(MouseButton::Left) {
318        if egui_context.ctx_mut()?.is_pointer_over_egui() {
319            return Ok(());
320        }
321        MouseButtonOp::JustPressed
322    } else if mouse_buttons.just_released(MouseButton::Left) {
323        MouseButtonOp::JustReleased
324    } else if mouse_buttons.pressed(MouseButton::Left) {
325        MouseButtonOp::BeingPressed
326    } else {
327        for (_, mut camera_state) in query.iter_mut() {
328            camera_state.clicks_on_objects_state = VpeolClicksOnObjectsState::Empty;
329        }
330        return Ok(());
331    };
332    for (render_target, mut camera_state) in query.iter_mut() {
333        let Some(cursor_ray) = camera_state.cursor_ray else {
334            continue;
335        };
336        let calc_cursor_in_world_position = |entity: Entity, plane_origin: Vec3| -> Option<Vec3> {
337            let drag_plane = if let Ok(VpeolOverrideDragPlane(drag_plane)) =
338                drag_plane_overrides_query.get(entity)
339            {
340                drag_plane
341            } else {
342                &global_drag_plane.0
343            };
344            let distance = cursor_ray.intersect_plane(plane_origin, *drag_plane)?;
345            Some(cursor_ray.get_point(distance))
346        };
347
348        let RenderTarget::Window(window_ref) = render_target else {
349            continue;
350        };
351        let Some(window) = window_getter.get_window(*window_ref) else {
352            continue;
353        };
354        let Some(cursor_in_screen_pos) = window.cursor_position() else {
355            continue;
356        };
357        if matches!(*window_ref, WindowRef::Primary)
358            && matches!(mouse_button_op, MouseButtonOp::JustPressed)
359            && editor_viewport.rect.is_some_and(|rect| {
360                !rect.contains(egui::Pos2::new(
361                    cursor_in_screen_pos.x,
362                    cursor_in_screen_pos.y,
363                ))
364            })
365        {
366            continue;
367        }
368
369        match (&mouse_button_op, &camera_state.clicks_on_objects_state) {
370            (MouseButtonOp::JustPressed, VpeolClicksOnObjectsState::Empty) => {
371                if keyboard.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]) {
372                    if let Some((entity, _)) = &camera_state.entity_under_cursor {
373                        directives_writer.write(YoleckDirective::toggle_selected(*entity));
374                    }
375                } else if let Some((knob_entity, cursor_pointing)) =
376                    knob_query.iter().find_map(|knob_entity| {
377                        Some((knob_entity, camera_state.pointing_at_entity(knob_entity)?))
378                    })
379                {
380                    directives_writer.write(YoleckDirective::pass_to_entity(
381                        knob_entity,
382                        YoleckKnobClick,
383                    ));
384                    let Ok(knob_transform) = global_transform_query.get(knob_entity) else {
385                        continue;
386                    };
387                    let Some(cursor_in_world_position) = calc_cursor_in_world_position(
388                        knob_entity,
389                        cursor_pointing.cursor_position_world_coords,
390                    ) else {
391                        continue;
392                    };
393                    camera_state.clicks_on_objects_state = VpeolClicksOnObjectsState::BeingDragged {
394                        entity: knob_entity,
395                        prev_screen_pos: cursor_in_screen_pos,
396                        offset: cursor_in_world_position - knob_transform.translation(),
397                        select_on_mouse_release: false,
398                    }
399                } else {
400                    camera_state.clicks_on_objects_state = if let Some((entity, cursor_pointing)) =
401                        &camera_state.entity_under_cursor
402                    {
403                        let Ok(entity_transform) = global_transform_query.get(*entity) else {
404                            continue;
405                        };
406                        let select_on_mouse_release = selected_query.contains(*entity);
407                        if !select_on_mouse_release {
408                            directives_writer.write(YoleckDirective::set_selected(Some(*entity)));
409                        }
410                        let Some(cursor_in_world_position) = calc_cursor_in_world_position(
411                            *entity,
412                            cursor_pointing.cursor_position_world_coords,
413                        ) else {
414                            continue;
415                        };
416                        VpeolClicksOnObjectsState::BeingDragged {
417                            entity: *entity,
418                            prev_screen_pos: cursor_in_screen_pos,
419                            offset: cursor_in_world_position - entity_transform.translation(),
420                            select_on_mouse_release,
421                        }
422                    } else {
423                        directives_writer.write(YoleckDirective::set_selected(None));
424                        VpeolClicksOnObjectsState::Empty
425                    };
426                }
427            }
428            (
429                MouseButtonOp::BeingPressed,
430                VpeolClicksOnObjectsState::BeingDragged {
431                    entity,
432                    prev_screen_pos,
433                    offset,
434                    select_on_mouse_release: _,
435                },
436            ) => {
437                if 0.1 <= prev_screen_pos.distance_squared(cursor_in_screen_pos) {
438                    let Ok(entity_transform) = global_transform_query.get(*entity) else {
439                        continue;
440                    };
441                    let drag_point = entity_transform.translation() + *offset;
442                    let Some(cursor_in_world_position) =
443                        calc_cursor_in_world_position(*entity, drag_point)
444                    else {
445                        continue;
446                    };
447                    directives_writer.write(YoleckDirective::pass_to_entity(
448                        *entity,
449                        cursor_in_world_position - *offset,
450                    ));
451                    camera_state.clicks_on_objects_state =
452                        VpeolClicksOnObjectsState::BeingDragged {
453                            entity: *entity,
454                            prev_screen_pos: cursor_in_screen_pos,
455                            offset: *offset,
456                            select_on_mouse_release: false,
457                        };
458                }
459            }
460            (
461                MouseButtonOp::JustReleased,
462                VpeolClicksOnObjectsState::BeingDragged {
463                    entity,
464                    prev_screen_pos: _,
465                    offset: _,
466                    select_on_mouse_release: true,
467                },
468            ) => {
469                directives_writer.write(YoleckDirective::set_selected(Some(*entity)));
470                camera_state.clicks_on_objects_state = VpeolClicksOnObjectsState::Empty;
471            }
472            _ => {}
473        }
474    }
475    Ok(())
476}
477
478/// A [passed data](crate::knobs::YoleckKnobHandle::get_passed_data) to a knob entity that indicate
479/// it was clicked by the level editor.
480pub struct YoleckKnobClick;
481
482/// Marker for entities that will be interacted in the viewport using their children.
483///
484/// Populate systems should mark the entity with this component when applicable. The viewport
485/// overlay plugin is responsible for handling it by using [`handle_clickable_children_system`].
486#[derive(Component, Default, Clone)]
487pub struct VpeolWillContainClickableChildren;
488
489/// Marker for viewport editor overlay plugins to route child interaction to parent entities.
490#[derive(Component)]
491pub struct VpeolRouteClickTo(pub Entity);
492
493/// Helper utility for finding the Yoleck controlled entity that's in charge of an entity the user
494/// points at.
495#[derive(SystemParam)]
496pub struct VpeolRootResolver<'w, 's> {
497    root_resolver: Query<'w, 's, &'static VpeolRouteClickTo>,
498    #[allow(clippy::type_complexity)]
499    has_managed_query: Query<'w, 's, (), Or<(With<YoleckManaged>, With<YoleckKnobMarker>)>>,
500}
501
502impl VpeolRootResolver<'_, '_> {
503    /// Find the Yoleck controlled entity that's in charge of an entity the user points at.
504    pub fn resolve_root(&self, entity: Entity) -> Option<Entity> {
505        if let Ok(VpeolRouteClickTo(root_entity)) = self.root_resolver.get(entity) {
506            Some(*root_entity)
507        } else {
508            self.has_managed_query.get(entity).ok()?;
509            Some(entity)
510        }
511    }
512}
513
514/// Add [`VpeolRouteClickTo`] of entities marked with [`VpeolWillContainClickableChildren`].
515pub fn handle_clickable_children_system<F, B>(
516    parents_query: Query<(Entity, &Children), With<VpeolWillContainClickableChildren>>,
517    children_query: Query<&Children>,
518    should_add_query: Query<Entity, F>,
519    mut commands: Commands,
520) where
521    F: QueryFilter,
522    B: Default + Bundle,
523{
524    for (parent, children) in parents_query.iter() {
525        if children.is_empty() {
526            continue;
527        }
528        let mut any_added = false;
529        let mut children_to_check: Vec<Entity> = children.iter().collect();
530        while let Some(child) = children_to_check.pop() {
531            if let Ok(child_children) = children_query.get(child) {
532                children_to_check.extend(child_children.iter());
533            }
534            if should_add_query.get(child).is_ok() {
535                commands
536                    .entity(child)
537                    .try_insert((VpeolRouteClickTo(parent), B::default()));
538                any_added = true;
539            }
540        }
541        if any_added {
542            commands
543                .entity(parent)
544                .remove::<VpeolWillContainClickableChildren>();
545        }
546    }
547}
548
549/// Add a pulse effect when an entity is being selected.
550pub struct VpeolSelectionCuePlugin {
551    /// How long, in seconds, the entire pulse effect will take. Defaults to 0.3.
552    pub effect_duration: f32,
553    /// By how much (relative to original size) the entity will grow during the pulse. Defaults to 0.3.
554    pub effect_magnitude: f32,
555}
556
557impl Default for VpeolSelectionCuePlugin {
558    fn default() -> Self {
559        Self {
560            effect_duration: 0.3,
561            effect_magnitude: 0.3,
562        }
563    }
564}
565
566impl Plugin for VpeolSelectionCuePlugin {
567    fn build(&self, app: &mut App) {
568        app.add_systems(Update, manage_selection_transform_components);
569        app.add_systems(PostUpdate, {
570            add_selection_cue_before_transform_propagate(
571                1.0 / self.effect_duration,
572                2.0 * self.effect_magnitude,
573            )
574            .before(TransformSystems::Propagate)
575        });
576        app.add_systems(PostUpdate, {
577            restore_transform_from_cache_after_transform_propagate
578                .after(TransformSystems::Propagate)
579        });
580    }
581}
582
583#[derive(Component)]
584struct SelectionCueAnimation {
585    cached_transform: Transform,
586    progress: f32,
587}
588
589fn manage_selection_transform_components(
590    add_cue_query: Query<Entity, (Without<SelectionCueAnimation>, With<YoleckEditMarker>)>,
591    remove_cue_query: Query<Entity, (With<SelectionCueAnimation>, Without<YoleckEditMarker>)>,
592    mut commands: Commands,
593) {
594    for entity in add_cue_query.iter() {
595        commands.entity(entity).insert(SelectionCueAnimation {
596            cached_transform: Default::default(),
597            progress: 0.0,
598        });
599    }
600    for entity in remove_cue_query.iter() {
601        commands.entity(entity).remove::<SelectionCueAnimation>();
602    }
603}
604
605fn add_selection_cue_before_transform_propagate(
606    time_speedup: f32,
607    magnitude_scale: f32,
608) -> impl FnMut(Query<(&mut SelectionCueAnimation, &mut Transform)>, Res<Time>) {
609    move |mut query, time| {
610        for (mut animation, mut transform) in query.iter_mut() {
611            animation.cached_transform = *transform;
612            if animation.progress < 1.0 {
613                animation.progress += time_speedup * time.delta_secs();
614                let extra = if animation.progress < 0.5 {
615                    animation.progress
616                } else {
617                    1.0 - animation.progress
618                };
619                transform.scale *= 1.0 + magnitude_scale * extra;
620            }
621        }
622    }
623}
624
625fn restore_transform_from_cache_after_transform_propagate(
626    mut query: Query<(&SelectionCueAnimation, &mut Transform)>,
627) {
628    for (animation, mut transform) in query.iter_mut() {
629        *transform = animation.cached_transform;
630    }
631}
632
633pub(crate) fn ray_intersection_with_mesh(ray: Ray3d, mesh: &Mesh) -> Option<f32> {
634    let aabb = mesh.get_aabb()?;
635    let distance_to_aabb = ray_intersection_with_aabb(ray, aabb)?;
636
637    if let Some(mut triangles) = iter_triangles(mesh) {
638        triangles.find_map(|triangle| triangle.ray_intersection(ray))
639    } else {
640        Some(distance_to_aabb)
641    }
642}
643
644fn ray_intersection_with_aabb(ray: Ray3d, aabb: Aabb) -> Option<f32> {
645    let center: Vec3 = aabb.center.into();
646    let mut max_low = f32::NEG_INFINITY;
647    let mut min_high = f32::INFINITY;
648    for (axis, half_extent) in [
649        (Vec3::X, aabb.half_extents.x),
650        (Vec3::Y, aabb.half_extents.y),
651        (Vec3::Z, aabb.half_extents.z),
652    ] {
653        let dot = ray.direction.dot(axis);
654        if dot == 0.0 {
655            let distance_from_center = (ray.origin - center).dot(axis);
656            if half_extent < distance_from_center.abs() {
657                return None;
658            }
659        } else {
660            let low = ray.intersect_plane(center - half_extent * axis, InfinitePlane3d::new(axis));
661            let high = ray.intersect_plane(center + half_extent * axis, InfinitePlane3d::new(axis));
662            let (low, high) = if 0.0 <= dot { (low, high) } else { (high, low) };
663            if let Some(low) = low {
664                max_low = max_low.max(low);
665            }
666            min_high = min_high.min(high?);
667        }
668    }
669    if max_low <= min_high {
670        Some(max_low)
671    } else {
672        None
673    }
674}
675
676fn iter_triangles(mesh: &Mesh) -> Option<impl '_ + Iterator<Item = Triangle>> {
677    if mesh.primitive_topology() != PrimitiveTopology::TriangleList {
678        return None;
679    }
680    let indices = mesh.indices()?;
681    let Some(VertexAttributeValues::Float32x3(positions)) =
682        mesh.attribute(Mesh::ATTRIBUTE_POSITION)
683    else {
684        return None;
685    };
686    let mut it = indices.iter();
687    Some(std::iter::from_fn(move || {
688        Some(Triangle(
689            [it.next()?, it.next()?, it.next()?].map(|idx| Vec3::from_array(positions[idx])),
690        ))
691    }))
692}
693
694#[derive(Debug)]
695struct Triangle([Vec3; 3]);
696
697impl Triangle {
698    fn ray_intersection(&self, ray: Ray3d) -> Option<f32> {
699        let directions = [
700            self.0[1] - self.0[0],
701            self.0[2] - self.0[1],
702            self.0[0] - self.0[2],
703        ];
704        let normal = directions[0].cross(directions[1]); // no need to normalize it
705        let plane = InfinitePlane3d {
706            normal: Dir3::new(normal).ok()?,
707        };
708        let distance = ray.intersect_plane(self.0[0], plane)?;
709        let point = ray.get_point(distance);
710        if self
711            .0
712            .iter()
713            .zip(directions.iter())
714            .all(|(vertex, direction)| {
715                let vertical = direction.cross(normal);
716                vertical.dot(point - *vertex) <= 0.0
717            })
718        {
719            Some(distance)
720        } else {
721            None
722        }
723    }
724}
725
726/// Detects an entity that's being clicked on. Meant to be used with [Yoleck's exclusive edit
727/// systems](crate::exclusive_systems::YoleckExclusiveSystemsQueue) and with Bevy's system piping.
728///
729/// Note that this only returns `Some` when the user clicks on an entity - it does not finish the
730/// exclusive system. The other systems that this gets piped into should decide whether or not it
731/// should be finished.
732pub fn vpeol_read_click_on_entity<Filter: QueryFilter>(
733    mut ui: ResMut<YoleckUi>,
734    cameras_query: Query<&VpeolCameraState>,
735    yoleck_managed_query: Query<&YoleckManaged>,
736    filter_query: Query<(), Filter>,
737    buttons: Res<ButtonInput<MouseButton>>,
738    mut candidate: Local<Option<Entity>>,
739) -> Option<Entity> {
740    let target = if ui.ctx().is_pointer_over_egui() {
741        None
742    } else {
743        cameras_query
744            .iter()
745            .find_map(|camera_state| Some(camera_state.entity_under_cursor.as_ref()?.0))
746    };
747
748    let Some(target) = target else {
749        ui.label("No Target");
750        return None;
751    };
752
753    let Ok(yoleck_managed) = yoleck_managed_query.get(target) else {
754        ui.label("No Target");
755        return None;
756    };
757
758    if !filter_query.contains(target) {
759        ui.label(format!("Invalid Target ({})", yoleck_managed.type_name));
760        return None;
761    }
762    ui.label(format!(
763        "Targeting {:?} ({})",
764        target, yoleck_managed.type_name
765    ));
766
767    if buttons.just_pressed(MouseButton::Left) {
768        *candidate = Some(target);
769    } else if buttons.just_released(MouseButton::Left)
770        && let Some(candidate) = candidate.take()
771        && candidate == target
772    {
773        return Some(target);
774    }
775    None
776}
777
778/// Apply a transform to every entity in the level.
779///
780/// Note that:
781/// * It is the duty of [`vpeol_2d`](crate::vpeol_2d)/[`vpeol_3d`](crate::vpeol_3d) to handle the
782///   actual repositioning, and they do so only for entities that use their existing components
783///   ([`Vpeol2dPosition`](crate::vpeol_2d::Vpeol2dPosition)/[`Vpeol3dPosition`](crate::vpeol_3d::Vpeol3dPosition)
784///   and friends). If there are entities that do not use these mechanisms, it falls under the
785///   responsibility of whatever populates their `Transform` to take this component (of their level
786///   entity) into account.
787/// * The repositioning is done directly on the `Transform` - not on the `GlobalTransform`.
788#[derive(Component, Default, Clone)]
789pub struct VpeolRepositionLevel(pub Transform);
790
791fn handle_delete_entity_key(
792    mut egui_context: EguiContexts,
793    keyboard_input: Res<ButtonInput<KeyCode>>,
794    mut yoleck_state: ResMut<YoleckState>,
795    query: Query<Entity, With<YoleckEditMarker>>,
796    mut commands: Commands,
797    mut writer: MessageWriter<YoleckEditorEvent>,
798) -> Result {
799    if egui_context.ctx_mut()?.egui_wants_keyboard_input() {
800        return Ok(());
801    }
802
803    if keyboard_input.just_pressed(KeyCode::Delete) {
804        for entity in query.iter() {
805            commands.entity(entity).despawn();
806            writer.write(YoleckEditorEvent::EntityDeselected(entity));
807        }
808        if !query.is_empty() {
809            yoleck_state.level_needs_saving = true;
810        }
811    }
812
813    Ok(())
814}
815
816#[derive(Resource)]
817enum VpeolClipboard {
818    #[cfg(feature = "arboard")]
819    Arboard(arboard::Clipboard),
820    Internal(String),
821}
822
823impl FromWorld for VpeolClipboard {
824    fn from_world(_: &mut World) -> Self {
825        #[cfg(feature = "arboard")]
826        match arboard::Clipboard::new() {
827            Ok(clipboard) => {
828                debug!("Arboard clipbaord successfully initiated");
829                return VpeolClipboard::Arboard(clipboard);
830            }
831            Err(err) => {
832                warn!("Cannot initiate Arboard clipboard: {err}");
833            }
834        }
835        VpeolClipboard::Internal(String::new())
836    }
837}
838
839fn handle_copy_entity_key(
840    mut egui_context: EguiContexts,
841    keyboard_input: Res<ButtonInput<KeyCode>>,
842    query: Query<&YoleckManaged, With<YoleckEditMarker>>,
843    construction_specs: Res<YoleckEntityConstructionSpecs>,
844    mut clipboard: ResMut<VpeolClipboard>,
845) -> Result {
846    if egui_context.ctx_mut()?.egui_wants_keyboard_input() {
847        return Ok(());
848    }
849
850    let ctrl_pressed = keyboard_input.any_pressed([KeyCode::ControlLeft, KeyCode::ControlRight]);
851
852    if ctrl_pressed && keyboard_input.just_pressed(KeyCode::KeyC) {
853        let entities: Vec<YoleckRawEntry> = query
854            .iter()
855            .filter_map(|yoleck_managed| {
856                let entity_type =
857                    construction_specs.get_entity_type_info(&yoleck_managed.type_name)?;
858
859                let data: serde_json::Map<String, serde_json::Value> = entity_type
860                    .components
861                    .iter()
862                    .filter_map(|component| {
863                        let component_data = yoleck_managed.components_data.get(component)?;
864                        let handler = &construction_specs.component_handlers[component];
865                        Some((
866                            handler.key().to_string(),
867                            handler.serialize(component_data.as_ref()),
868                        ))
869                    })
870                    .collect();
871
872                Some(YoleckRawEntry {
873                    header: crate::entity_management::YoleckEntryHeader {
874                        type_name: yoleck_managed.type_name.clone(),
875                        name: yoleck_managed.name.clone(),
876                        uuid: None,
877                    },
878                    data,
879                })
880            })
881            .collect();
882
883        if !entities.is_empty()
884            && let Ok(json) = serde_json::to_string(&entities)
885        {
886            match clipboard.as_mut() {
887                #[cfg(feature = "arboard")]
888                VpeolClipboard::Arboard(clipboard) => {
889                    clipboard.set_text(json)?;
890                }
891                VpeolClipboard::Internal(clipboard) => {
892                    *clipboard = json;
893                }
894            }
895        }
896    }
897
898    Ok(())
899}
900
901fn handle_paste_entity_key(
902    mut egui_context: EguiContexts,
903    keyboard_input: Res<ButtonInput<KeyCode>>,
904    yoleck_state: Res<YoleckState>,
905    mut directives_writer: MessageWriter<YoleckDirective>,
906    mut clipboard: ResMut<VpeolClipboard>,
907) -> Result {
908    if egui_context.ctx_mut()?.egui_wants_keyboard_input() {
909        return Ok(());
910    }
911
912    let ctrl_pressed = keyboard_input.pressed(KeyCode::ControlLeft)
913        || keyboard_input.pressed(KeyCode::ControlRight);
914
915    if ctrl_pressed && keyboard_input.just_pressed(KeyCode::KeyV) {
916        #[cfg(feature = "arboard")]
917        let arboard_text_storage: String;
918        let text_to_paste: Option<&str> = match clipboard.as_mut() {
919            #[cfg(feature = "arboard")]
920            VpeolClipboard::Arboard(clipboard) => match clipboard.get_text() {
921                Ok(text) => {
922                    arboard_text_storage = text;
923                    Some(&arboard_text_storage)
924                }
925                Err(err) => {
926                    error!("Cannot load text from arboard: {err}");
927                    None
928                }
929            },
930            VpeolClipboard::Internal(clipboard) => {
931                Some(clipboard.as_str()).filter(|txt| !txt.is_empty())
932            }
933        };
934
935        if let Some(text) = text_to_paste
936            && let Ok(entities) =
937                serde_json::from_str::<Vec<YoleckRawEntry>>(text).inspect_err(|err| {
938                    warn!("Cannot paste - failure to parse copied text: {err}");
939                })
940            && !entities.is_empty()
941        {
942            let level_being_edited = yoleck_state.level_being_edited;
943
944            for entry in entities {
945                directives_writer.write(
946                    YoleckDirective::spawn_entity(level_being_edited, entry.header.type_name, true)
947                        .extend(entry.data.into_iter())
948                        .into(),
949                );
950            }
951        }
952    }
953
954    Ok(())
955}