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