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