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