Skip to main content

bevy_yoleck/
vpeol_2d.rs

1//! # Viewport Editing Overlay for 2D games.
2//!
3//! Use this module to implement simple 2D editing for 2D games.
4//!
5//! To use add the egui and Yoleck plugins to the Bevy app, as well as the plugin of this module:
6//!
7//! ```no_run
8//! # use bevy::prelude::*;
9//! # use bevy_yoleck::bevy_egui::EguiPlugin;
10//! # use bevy_yoleck::prelude::*;
11//! # use bevy_yoleck::vpeol::prelude::*;
12//! # let mut app = App::new();
13//! app.add_plugins(EguiPlugin::default());
14//! app.add_plugins(YoleckPluginForEditor);
15//! // Use `Vpeol2dPluginForGame` instead when setting up for game.
16//! app.add_plugins(Vpeol2dPluginForEditor);
17//! ```
18//!
19//! Add the following components to the camera entity:
20//! * [`VpeolCameraState`] in order to select and drag entities.
21//! * [`Vpeol2dCameraControl`] in order to pan and zoom the camera with the mouse. This one can be
22//!   skipped if there are other means to control the camera inside the editor, or if no camera
23//!   control inside the editor is desired.
24//!
25//! ```no_run
26//! # use bevy::prelude::*;
27//! # use bevy_yoleck::vpeol::VpeolCameraState;
28//! # use bevy_yoleck::vpeol::prelude::*;
29//! # let commands: Commands = panic!();
30//! commands
31//!     .spawn(Camera2d::default())
32//!     .insert(VpeolCameraState::default())
33//!     .insert(Vpeol2dCameraControl::default());
34//! ```
35//!
36//! Entity selection by clicking on it is supported by just adding the plugin. To implement
37//! dragging, there are two options:
38//!
39//! 1. Add  the [`Vpeol2dPosition`] Yoleck component and use it as the source of position. Optionally
40//!    add [`Vpeol2dRotatation`] (edited in degrees) and [`Vpeol2dScale`] (edited with X, Y values)
41//!    for rotation and scale support.
42//!     ```no_run
43//!     # use bevy::prelude::*;
44//!     # use bevy_yoleck::prelude::*;
45//!     # use bevy_yoleck::vpeol::prelude::*;
46//!     # use serde::{Deserialize, Serialize};
47//!     # #[derive(Clone, PartialEq, Serialize, Deserialize, Component, Default, YoleckComponent)]
48//!     # struct Example;
49//!     # let mut app = App::new();
50//!     app.add_yoleck_entity_type({
51//!         YoleckEntityType::new("Example")
52//!             .with::<Vpeol2dPosition>() // vpeol_2d dragging
53//!             .with::<Vpeol2dRotatation>() // optional: rotation with egui (degrees)
54//!             .with::<Vpeol2dScale>() // optional: scale with egui
55//!             .with::<Example>() // entity's specific data and systems
56//!     });
57//!     ```
58//! 2. Use data passing. vpeol_2d will pass a `Vec3` to the entity being dragged:
59//!     ```no_run
60//!     # use bevy::prelude::*;
61//!     # use bevy_yoleck::prelude::*;
62//!     # use serde::{Deserialize, Serialize};
63//!     # #[derive(Clone, PartialEq, Serialize, Deserialize, Component, Default, YoleckComponent)]
64//!     # struct Example {
65//!     #     position: Vec2,
66//!     # }
67//!     # let mut app = App::new();
68//!     fn edit_example(mut edit: YoleckEdit<(Entity, &mut Example)>, passed_data: Res<YoleckPassedData>) {
69//!         let Ok((entity, mut example)) = edit.single_mut() else { return };
70//!         if let Some(pos) = passed_data.get::<Vec3>(entity) {
71//!             example.position = pos.truncate();
72//!         }
73//!     }
74//!
75//!     fn populate_example(mut populate: YoleckPopulate<&Example>) {
76//!         populate.populate(|_ctx, mut cmd, example| {
77//!             cmd.insert(Transform::from_translation(example.position.extend(0.0)));
78//!             cmd.insert(Sprite {
79//!                 // Actual sprite data
80//!                 ..Default::default()
81//!             });
82//!         });
83//!     }
84//!     ```
85
86use std::any::TypeId;
87
88use crate::bevy_egui::{EguiContexts, egui};
89use crate::exclusive_systems::{
90    YoleckEntityCreationExclusiveSystems, YoleckExclusiveSystemDirective,
91};
92use crate::vpeol::{
93    VpeolBasePlugin, VpeolCameraState, VpeolDragPlane, VpeolRepositionLevel, VpeolRootResolver,
94    VpeolSystems, WindowGetter, handle_clickable_children_system, ray_intersection_with_mesh,
95};
96use bevy::camera::RenderTarget;
97use bevy::camera::visibility::VisibleEntities;
98use bevy::input::mouse::MouseWheel;
99use bevy::math::DVec2;
100use bevy::platform::collections::HashMap;
101use bevy::prelude::*;
102use bevy::sprite::Anchor;
103use bevy::text::TextLayoutInfo;
104use serde::{Deserialize, Serialize};
105
106use crate::{YoleckBelongsToLevel, YoleckSchedule, prelude::*};
107
108/// Add the systems required for loading levels that use vpeol_2d components
109pub struct Vpeol2dPluginForGame;
110
111impl Plugin for Vpeol2dPluginForGame {
112    fn build(&self, app: &mut App) {
113        app.add_systems(
114            YoleckSchedule::OverrideCommonComponents,
115            vpeol_2d_populate_transform,
116        );
117        #[cfg(feature = "bevy_reflect")]
118        register_reflect_types(app);
119    }
120}
121
122#[cfg(feature = "bevy_reflect")]
123fn register_reflect_types(app: &mut App) {
124    app.register_type::<Vpeol2dPosition>();
125    app.register_type::<Vpeol2dRotatation>();
126    app.register_type::<Vpeol2dScale>();
127    app.register_type::<Vpeol2dCameraControl>();
128}
129
130/// Add the systems required for 2D editing.
131///
132/// * 2D camera control (for cameras with [`Vpeol2dCameraControl`])
133/// * Entity selection.
134/// * Entity dragging.
135/// * Connecting nested entities.
136pub struct Vpeol2dPluginForEditor;
137
138impl Plugin for Vpeol2dPluginForEditor {
139    fn build(&self, app: &mut App) {
140        app.add_plugins(VpeolBasePlugin);
141        app.add_plugins(Vpeol2dPluginForGame);
142        app.insert_resource(VpeolDragPlane::XY);
143
144        app.add_systems(
145            Update,
146            (
147                update_camera_status_for_sprites,
148                update_camera_status_for_2d_meshes,
149                update_camera_status_for_text_2d,
150            )
151                .in_set(VpeolSystems::UpdateCameraState),
152        );
153        app.add_systems(
154            PostUpdate, // to prevent camera shaking (only seen it in 3D, but still)
155            (camera_2d_pan, camera_2d_zoom).run_if(in_state(YoleckEditorState::EditorActive)),
156        );
157        app.add_systems(
158            Update,
159            (
160                ApplyDeferred,
161                handle_clickable_children_system::<
162                    Or<(With<Sprite>, (With<TextLayoutInfo>, With<Anchor>))>,
163                    (),
164                >,
165                ApplyDeferred,
166            )
167                .chain()
168                .run_if(in_state(YoleckEditorState::EditorActive)),
169        );
170        app.add_yoleck_edit_system(vpeol_2d_edit_transform_group);
171        app.world_mut()
172            .resource_mut::<YoleckEntityCreationExclusiveSystems>()
173            .on_entity_creation(|queue| queue.push_back(vpeol_2d_init_position));
174    }
175}
176
177struct CursorInWorldPos {
178    cursor_in_world_pos: Vec2,
179}
180
181impl CursorInWorldPos {
182    fn from_camera_state(camera_state: &VpeolCameraState) -> Option<Self> {
183        Some(Self {
184            cursor_in_world_pos: camera_state.cursor_ray?.origin.truncate(),
185        })
186    }
187
188    fn cursor_in_entity_space(&self, transform: &GlobalTransform) -> Vec2 {
189        transform
190            .to_matrix()
191            .inverse()
192            .project_point3(self.cursor_in_world_pos.extend(0.0))
193            .truncate()
194    }
195
196    fn check_square(
197        &self,
198        entity_transform: &GlobalTransform,
199        anchor: &Anchor,
200        size: Vec2,
201    ) -> bool {
202        let cursor = self.cursor_in_entity_space(entity_transform);
203        let anchor = anchor.as_vec();
204        let mut min_corner = Vec2::new(-0.5, -0.5) - anchor;
205        let mut max_corner = Vec2::new(0.5, 0.5) - anchor;
206        for corner in [&mut min_corner, &mut max_corner] {
207            corner.x *= size.x;
208            corner.y *= size.y;
209        }
210        min_corner.x <= cursor.x
211            && cursor.x <= max_corner.x
212            && min_corner.y <= cursor.y
213            && cursor.y <= max_corner.y
214    }
215}
216
217#[allow(clippy::type_complexity)]
218fn update_camera_status_for_sprites(
219    mut cameras_query: Query<(&mut VpeolCameraState, &VisibleEntities)>,
220    entities_query: Query<(Entity, &GlobalTransform, &Sprite, &Anchor)>,
221    image_assets: Res<Assets<Image>>,
222    texture_atlas_layout_assets: Res<Assets<TextureAtlasLayout>>,
223    root_resolver: VpeolRootResolver,
224) {
225    for (mut camera_state, visible_entities) in cameras_query.iter_mut() {
226        let Some(cursor) = CursorInWorldPos::from_camera_state(&camera_state) else {
227            continue;
228        };
229
230        for (entity, entity_transform, sprite, anchor) in entities_query
231            // Note: starting from Bevy 0.20, sprites are registered as Mesh2d in the list of
232            // visible entities. We still use the sprites checks for them, because for whatever
233            // reason the ray intersection function does not work with them.
234            .iter_many(visible_entities.iter(TypeId::of::<Mesh2d>()))
235            .matched()
236        {
237            let size = if let Some(custom_size) = sprite.custom_size {
238                custom_size
239            } else if let Some(texture_atlas) = sprite.texture_atlas.as_ref() {
240                let Some(texture_atlas_layout) =
241                    texture_atlas_layout_assets.get(&texture_atlas.layout)
242                else {
243                    continue;
244                };
245                texture_atlas_layout.textures[texture_atlas.index]
246                    .size()
247                    .as_vec2()
248            } else if let Some(texture) = image_assets.get(&sprite.image) {
249                texture.size().as_vec2()
250            } else {
251                continue;
252            };
253            if cursor.check_square(entity_transform, anchor, size) {
254                let z_depth = entity_transform.translation().z;
255                let Some(root_entity) = root_resolver.resolve_root(entity) else {
256                    continue;
257                };
258                camera_state.consider(root_entity, z_depth, || {
259                    cursor.cursor_in_world_pos.extend(z_depth)
260                });
261            }
262        }
263    }
264}
265
266fn update_camera_status_for_2d_meshes(
267    mut cameras_query: Query<(&mut VpeolCameraState, &VisibleEntities)>,
268    entities_query: Query<(Entity, &GlobalTransform, &Mesh2d)>,
269    mesh_assets: Res<Assets<Mesh>>,
270    root_resolver: VpeolRootResolver,
271) {
272    for (mut camera_state, visible_entities) in cameras_query.iter_mut() {
273        let Some(cursor_ray) = camera_state.cursor_ray else {
274            continue;
275        };
276        for (entity, global_transform, mesh) in entities_query
277            .iter_many(visible_entities.iter(TypeId::of::<Mesh2d>()))
278            .matched()
279        {
280            let Some(mesh) = mesh_assets.get(&mesh.0) else {
281                continue;
282            };
283
284            let inverse_transform = global_transform.to_matrix().inverse();
285
286            let ray_in_object_coords = Ray3d {
287                origin: inverse_transform.transform_point3(cursor_ray.origin),
288                direction: inverse_transform
289                    .transform_vector3(*cursor_ray.direction)
290                    .try_into()
291                    .unwrap(),
292            };
293
294            let Some(distance) = ray_intersection_with_mesh(ray_in_object_coords, mesh) else {
295                continue;
296            };
297
298            let Some(root_entity) = root_resolver.resolve_root(entity) else {
299                continue;
300            };
301            camera_state.consider(root_entity, -distance, || cursor_ray.get_point(distance));
302        }
303    }
304}
305
306fn update_camera_status_for_text_2d(
307    mut cameras_query: Query<(&mut VpeolCameraState, &VisibleEntities)>,
308    entities_query: Query<(Entity, &GlobalTransform, &TextLayoutInfo, &Anchor)>,
309    root_resolver: VpeolRootResolver,
310) {
311    for (mut camera_state, visible_entities) in cameras_query.iter_mut() {
312        let Some(cursor) = CursorInWorldPos::from_camera_state(&camera_state) else {
313            continue;
314        };
315
316        for (entity, entity_transform, text_layout_info, anchor) in
317            // Weird that it is not `WithText`...
318            entities_query
319                .iter_many(visible_entities.iter(TypeId::of::<Sprite>()))
320                .matched()
321        {
322            if cursor.check_square(entity_transform, anchor, text_layout_info.size) {
323                let z_depth = entity_transform.translation().z;
324                let Some(root_entity) = root_resolver.resolve_root(entity) else {
325                    continue;
326                };
327                camera_state.consider(root_entity, z_depth, || {
328                    cursor.cursor_in_world_pos.extend(z_depth)
329                });
330            }
331        }
332    }
333}
334
335/// Pan and zoom a camera entity with the mouse while inisde the editor.
336#[derive(Component)]
337#[cfg_attr(feature = "bevy_reflect", derive(bevy::reflect::Reflect))]
338pub struct Vpeol2dCameraControl {
339    /// How much to zoom when receiving scroll event in `MouseScrollUnit::Line` units.
340    pub zoom_per_scroll_line: f32,
341    /// How much to zoom when receiving scroll event in `MouseScrollUnit::Pixel` units.
342    pub zoom_per_scroll_pixel: f32,
343}
344
345impl Default for Vpeol2dCameraControl {
346    fn default() -> Self {
347        Self {
348            zoom_per_scroll_line: 0.2,
349            zoom_per_scroll_pixel: 0.001,
350        }
351    }
352}
353
354fn camera_2d_pan(
355    mut egui_context: EguiContexts,
356    mouse_buttons: Res<ButtonInput<MouseButton>>,
357    mut cameras_query: Query<
358        (Entity, &mut Transform, &VpeolCameraState),
359        With<Vpeol2dCameraControl>,
360    >,
361    mut last_cursor_world_pos_by_camera: Local<HashMap<Entity, Vec2>>,
362) -> Result {
363    enum MouseButtonOp {
364        JustPressed,
365        BeingPressed,
366    }
367
368    let mouse_button_op = if mouse_buttons.just_pressed(MouseButton::Right) {
369        if egui_context.ctx_mut()?.is_pointer_over_egui() {
370            return Ok(());
371        }
372        MouseButtonOp::JustPressed
373    } else if mouse_buttons.pressed(MouseButton::Right) {
374        MouseButtonOp::BeingPressed
375    } else {
376        last_cursor_world_pos_by_camera.clear();
377        return Ok(());
378    };
379
380    for (camera_entity, mut camera_transform, camera_state) in cameras_query.iter_mut() {
381        let Some(cursor_ray) = camera_state.cursor_ray else {
382            continue;
383        };
384        let world_pos = cursor_ray.origin.truncate();
385
386        match mouse_button_op {
387            MouseButtonOp::JustPressed => {
388                last_cursor_world_pos_by_camera.insert(camera_entity, world_pos);
389            }
390            MouseButtonOp::BeingPressed => {
391                if let Some(prev_pos) = last_cursor_world_pos_by_camera.get_mut(&camera_entity) {
392                    let movement = *prev_pos - world_pos;
393                    camera_transform.translation += movement.extend(0.0);
394                }
395            }
396        }
397    }
398    Ok(())
399}
400
401fn camera_2d_zoom(
402    mut egui_context: EguiContexts,
403    window_getter: WindowGetter,
404    mut cameras_query: Query<(
405        &mut Transform,
406        &VpeolCameraState,
407        &Camera,
408        &RenderTarget,
409        &Vpeol2dCameraControl,
410    )>,
411    mut wheel_events_reader: MessageReader<MouseWheel>,
412) -> Result {
413    if egui_context.ctx_mut()?.is_pointer_over_egui() {
414        return Ok(());
415    }
416
417    for (mut camera_transform, camera_state, camera, render_target, camera_control) in
418        cameras_query.iter_mut()
419    {
420        let Some(cursor_ray) = camera_state.cursor_ray else {
421            continue;
422        };
423        let world_pos = cursor_ray.origin.truncate();
424
425        let zoom_amount: f32 = wheel_events_reader
426            .read()
427            .map(|wheel_event| match wheel_event.unit {
428                bevy::input::mouse::MouseScrollUnit::Line => {
429                    wheel_event.y * camera_control.zoom_per_scroll_line
430                }
431                bevy::input::mouse::MouseScrollUnit::Pixel => {
432                    wheel_event.y * camera_control.zoom_per_scroll_pixel
433                }
434            })
435            .sum();
436
437        if zoom_amount == 0.0 {
438            continue;
439        }
440
441        let scale_by = (-zoom_amount).exp();
442
443        let window = if let RenderTarget::Window(window_ref) = render_target {
444            window_getter.get_window(*window_ref).unwrap()
445        } else {
446            continue;
447        };
448        camera_transform.scale.x *= scale_by;
449        camera_transform.scale.y *= scale_by;
450        let Some(cursor_in_screen_pos) = window.cursor_position() else {
451            continue;
452        };
453        let Ok(new_cursor_ray) =
454            camera.viewport_to_world(&((*camera_transform.as_ref()).into()), cursor_in_screen_pos)
455        else {
456            continue;
457        };
458        let new_world_pos = new_cursor_ray.origin.truncate();
459        camera_transform.translation += (world_pos - new_world_pos).extend(0.0);
460    }
461    Ok(())
462}
463
464/// A position component that's edited and populated by vpeol_2d.
465#[derive(Clone, PartialEq, Serialize, Deserialize, Component, Default, YoleckComponent)]
466#[serde(transparent)]
467#[cfg_attr(feature = "bevy_reflect", derive(bevy::reflect::Reflect))]
468pub struct Vpeol2dPosition(pub Vec2);
469
470/// A rotation component that's edited and populated by vpeol_2d.
471///
472/// The rotation is in radians around the Z axis. Editing is done with egui using degrees.
473#[derive(Default, Clone, PartialEq, Serialize, Deserialize, Component, YoleckComponent)]
474#[serde(transparent)]
475#[cfg_attr(feature = "bevy_reflect", derive(bevy::reflect::Reflect))]
476pub struct Vpeol2dRotatation(pub f32);
477
478/// A scale component that's edited and populated by vpeol_2d.
479///
480/// Editing is done with egui using separate drag values for X and Y axes.
481#[derive(Clone, PartialEq, Serialize, Deserialize, Component, YoleckComponent)]
482#[serde(transparent)]
483#[cfg_attr(feature = "bevy_reflect", derive(bevy::reflect::Reflect))]
484pub struct Vpeol2dScale(pub Vec2);
485
486impl Default for Vpeol2dScale {
487    fn default() -> Self {
488        Self(Vec2::ONE)
489    }
490}
491
492fn vpeol_2d_edit_transform_group(
493    mut ui: ResMut<YoleckUi>,
494    position_edit: YoleckEdit<(Entity, &mut Vpeol2dPosition)>,
495    rotation_edit: YoleckEdit<&mut Vpeol2dRotatation>,
496    scale_edit: YoleckEdit<&mut Vpeol2dScale>,
497    passed_data: Res<YoleckPassedData>,
498) {
499    let has_any = !position_edit.is_empty() || !rotation_edit.is_empty() || !scale_edit.is_empty();
500    if !has_any {
501        return;
502    }
503
504    ui.group(|ui| {
505        ui.label(egui::RichText::new("Transform").strong());
506        ui.separator();
507
508        vpeol_2d_edit_position_impl(ui, position_edit, &passed_data);
509        vpeol_2d_edit_rotation_impl(ui, rotation_edit);
510        vpeol_2d_edit_scale_impl(ui, scale_edit);
511    });
512}
513
514fn vpeol_2d_edit_position_impl(
515    ui: &mut egui::Ui,
516    mut edit: YoleckEdit<(Entity, &mut Vpeol2dPosition)>,
517    passed_data: &YoleckPassedData,
518) {
519    if edit.is_empty() || edit.has_nonmatching() {
520        return;
521    }
522    let mut average = DVec2::ZERO;
523    let mut num_entities = 0;
524    let mut transition = Vec2::ZERO;
525    for (entity, position) in edit.iter_matching() {
526        if let Some(pos) = passed_data.get::<Vec3>(entity) {
527            transition = pos.truncate() - position.0;
528        }
529        average += position.0.as_dvec2();
530        num_entities += 1;
531    }
532    average /= num_entities as f64;
533
534    ui.horizontal(|ui| {
535        let mut new_average = average;
536
537        ui.add(egui::Label::new("Position"));
538        ui.add(egui::DragValue::new(&mut new_average.x).prefix("X:"));
539        ui.add(egui::DragValue::new(&mut new_average.y).prefix("Y:"));
540
541        transition += (new_average - average).as_vec2();
542    });
543
544    if transition.is_finite() && transition != Vec2::ZERO {
545        for (_, mut position) in edit.iter_matching_mut() {
546            position.0 += transition;
547        }
548    }
549}
550
551fn vpeol_2d_edit_rotation_impl(ui: &mut egui::Ui, mut edit: YoleckEdit<&mut Vpeol2dRotatation>) {
552    if edit.is_empty() || edit.has_nonmatching() {
553        return;
554    }
555
556    let mut average_rotation = 0.0_f32;
557    let mut num_entities = 0;
558
559    for rotation in edit.iter_matching() {
560        average_rotation += rotation.0;
561        num_entities += 1;
562    }
563    average_rotation /= num_entities as f32;
564
565    ui.horizontal(|ui| {
566        let mut rotation_deg = average_rotation.to_degrees();
567
568        ui.add(egui::Label::new("Rotation"));
569        ui.add(
570            egui::DragValue::new(&mut rotation_deg)
571                .speed(1.0)
572                .suffix("°"),
573        );
574
575        let new_rotation = rotation_deg.to_radians();
576        let transition = new_rotation - average_rotation;
577
578        if transition.is_finite() && transition != 0.0 {
579            for mut rotation in edit.iter_matching_mut() {
580                rotation.0 += transition;
581            }
582        }
583    });
584}
585
586fn vpeol_2d_edit_scale_impl(ui: &mut egui::Ui, mut edit: YoleckEdit<&mut Vpeol2dScale>) {
587    if edit.is_empty() || edit.has_nonmatching() {
588        return;
589    }
590    let mut average = DVec2::ZERO;
591    let mut num_entities = 0;
592
593    for scale in edit.iter_matching() {
594        average += scale.0.as_dvec2();
595        num_entities += 1;
596    }
597    average /= num_entities as f64;
598
599    ui.horizontal(|ui| {
600        let mut new_average = average;
601
602        ui.add(egui::Label::new("Scale"));
603        ui.vertical(|ui| {
604            ui.centered_and_justified(|ui| {
605                let axis_average = (average.x + average.y) / 2.0;
606                let mut new_axis_average = axis_average;
607                if ui
608                    .add(egui::DragValue::new(&mut new_axis_average).speed(0.01))
609                    .dragged()
610                {
611                    // Use difference instead of ration to avoid problems when reaching/crossing
612                    // the zero.
613                    let diff = new_axis_average - axis_average;
614                    new_average.x += diff;
615                    new_average.y += diff;
616                }
617            });
618            ui.horizontal(|ui| {
619                ui.add(
620                    egui::DragValue::new(&mut new_average.x)
621                        .prefix("X:")
622                        .speed(0.01),
623                );
624                ui.add(
625                    egui::DragValue::new(&mut new_average.y)
626                        .prefix("Y:")
627                        .speed(0.01),
628                );
629            });
630        });
631
632        let transition = (new_average - average).as_vec2();
633
634        if transition.is_finite() && transition != Vec2::ZERO {
635            for mut scale in edit.iter_matching_mut() {
636                scale.0 += transition;
637            }
638        }
639    });
640}
641
642fn vpeol_2d_init_position(
643    mut egui_context: EguiContexts,
644    ui: Res<YoleckUi>,
645    mut edit: YoleckEdit<&mut Vpeol2dPosition>,
646    cameras_query: Query<&VpeolCameraState>,
647    mouse_buttons: Res<ButtonInput<MouseButton>>,
648) -> YoleckExclusiveSystemDirective {
649    let Ok(mut position) = edit.single_mut() else {
650        return YoleckExclusiveSystemDirective::Finished;
651    };
652
653    let Some(cursor_ray) = cameras_query
654        .iter()
655        .find_map(|camera_state| camera_state.cursor_ray)
656    else {
657        return YoleckExclusiveSystemDirective::Listening;
658    };
659
660    position.0 = cursor_ray.origin.truncate();
661
662    if egui_context.ctx_mut().unwrap().is_pointer_over_egui() || ui.ctx().is_pointer_over_egui() {
663        return YoleckExclusiveSystemDirective::Listening;
664    }
665
666    if mouse_buttons.just_released(MouseButton::Left) {
667        return YoleckExclusiveSystemDirective::Finished;
668    }
669
670    YoleckExclusiveSystemDirective::Listening
671}
672
673fn vpeol_2d_populate_transform(
674    mut populate: YoleckPopulate<(
675        &Vpeol2dPosition,
676        Option<&Vpeol2dRotatation>,
677        Option<&Vpeol2dScale>,
678        &YoleckBelongsToLevel,
679    )>,
680    levels_query: Query<&VpeolRepositionLevel>,
681) {
682    populate.populate(
683        |_ctx, mut cmd, (position, rotation, scale, belongs_to_level)| {
684            let mut transform = Transform::from_translation(position.0.extend(0.0));
685            if let Some(Vpeol2dRotatation(rotation)) = rotation {
686                transform = transform.with_rotation(Quat::from_rotation_z(*rotation));
687            }
688            if let Some(Vpeol2dScale(scale)) = scale {
689                transform = transform.with_scale(scale.extend(1.0));
690            }
691
692            if let Ok(VpeolRepositionLevel(level_transform)) =
693                levels_query.get(belongs_to_level.level)
694            {
695                transform = *level_transform * transform;
696            }
697
698            cmd.insert((transform, GlobalTransform::from(transform)));
699        },
700    )
701}