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