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
231            entities_query.iter_many(visible_entities.iter(TypeId::of::<Sprite>()))
232        // entities_query.iter()
233        {
234            let size = if let Some(custom_size) = sprite.custom_size {
235                custom_size
236            } else if let Some(texture_atlas) = sprite.texture_atlas.as_ref() {
237                let Some(texture_atlas_layout) =
238                    texture_atlas_layout_assets.get(&texture_atlas.layout)
239                else {
240                    continue;
241                };
242                texture_atlas_layout.textures[texture_atlas.index]
243                    .size()
244                    .as_vec2()
245            } else if let Some(texture) = image_assets.get(&sprite.image) {
246                texture.size().as_vec2()
247            } else {
248                continue;
249            };
250            if cursor.check_square(entity_transform, anchor, size) {
251                let z_depth = entity_transform.translation().z;
252                let Some(root_entity) = root_resolver.resolve_root(entity) else {
253                    continue;
254                };
255                camera_state.consider(root_entity, z_depth, || {
256                    cursor.cursor_in_world_pos.extend(z_depth)
257                });
258            }
259        }
260    }
261}
262
263fn update_camera_status_for_2d_meshes(
264    mut cameras_query: Query<(&mut VpeolCameraState, &VisibleEntities)>,
265    entities_query: Query<(Entity, &GlobalTransform, &Mesh2d)>,
266    mesh_assets: Res<Assets<Mesh>>,
267    root_resolver: VpeolRootResolver,
268) {
269    for (mut camera_state, visible_entities) in cameras_query.iter_mut() {
270        let Some(cursor_ray) = camera_state.cursor_ray else {
271            continue;
272        };
273        for (entity, global_transform, mesh) in
274            entities_query.iter_many(visible_entities.iter(TypeId::of::<Mesh2d>()))
275        {
276            let Some(mesh) = mesh_assets.get(&mesh.0) else {
277                continue;
278            };
279
280            let inverse_transform = global_transform.to_matrix().inverse();
281
282            let ray_in_object_coords = Ray3d {
283                origin: inverse_transform.transform_point3(cursor_ray.origin),
284                direction: inverse_transform
285                    .transform_vector3(*cursor_ray.direction)
286                    .try_into()
287                    .unwrap(),
288            };
289
290            let Some(distance) = ray_intersection_with_mesh(ray_in_object_coords, mesh) else {
291                continue;
292            };
293
294            let Some(root_entity) = root_resolver.resolve_root(entity) else {
295                continue;
296            };
297            camera_state.consider(root_entity, -distance, || cursor_ray.get_point(distance));
298        }
299    }
300}
301
302fn update_camera_status_for_text_2d(
303    mut cameras_query: Query<(&mut VpeolCameraState, &VisibleEntities)>,
304    entities_query: Query<(Entity, &GlobalTransform, &TextLayoutInfo, &Anchor)>,
305    root_resolver: VpeolRootResolver,
306) {
307    for (mut camera_state, visible_entities) in cameras_query.iter_mut() {
308        let Some(cursor) = CursorInWorldPos::from_camera_state(&camera_state) else {
309            continue;
310        };
311
312        for (entity, entity_transform, text_layout_info, anchor) in
313            // Weird that it is not `WithText`...
314            entities_query.iter_many(visible_entities.iter(TypeId::of::<Sprite>()))
315        {
316            if cursor.check_square(entity_transform, anchor, text_layout_info.size) {
317                let z_depth = entity_transform.translation().z;
318                let Some(root_entity) = root_resolver.resolve_root(entity) else {
319                    continue;
320                };
321                camera_state.consider(root_entity, z_depth, || {
322                    cursor.cursor_in_world_pos.extend(z_depth)
323                });
324            }
325        }
326    }
327}
328
329/// Pan and zoom a camera entity with the mouse while inisde the editor.
330#[derive(Component)]
331#[cfg_attr(feature = "bevy_reflect", derive(bevy::reflect::Reflect))]
332pub struct Vpeol2dCameraControl {
333    /// How much to zoom when receiving scroll event in `MouseScrollUnit::Line` units.
334    pub zoom_per_scroll_line: f32,
335    /// How much to zoom when receiving scroll event in `MouseScrollUnit::Pixel` units.
336    pub zoom_per_scroll_pixel: f32,
337}
338
339impl Default for Vpeol2dCameraControl {
340    fn default() -> Self {
341        Self {
342            zoom_per_scroll_line: 0.2,
343            zoom_per_scroll_pixel: 0.001,
344        }
345    }
346}
347
348fn camera_2d_pan(
349    mut egui_context: EguiContexts,
350    mouse_buttons: Res<ButtonInput<MouseButton>>,
351    mut cameras_query: Query<
352        (Entity, &mut Transform, &VpeolCameraState),
353        With<Vpeol2dCameraControl>,
354    >,
355    mut last_cursor_world_pos_by_camera: Local<HashMap<Entity, Vec2>>,
356) -> Result {
357    enum MouseButtonOp {
358        JustPressed,
359        BeingPressed,
360    }
361
362    let mouse_button_op = if mouse_buttons.just_pressed(MouseButton::Right) {
363        if egui_context.ctx_mut()?.is_pointer_over_area() {
364            return Ok(());
365        }
366        MouseButtonOp::JustPressed
367    } else if mouse_buttons.pressed(MouseButton::Right) {
368        MouseButtonOp::BeingPressed
369    } else {
370        last_cursor_world_pos_by_camera.clear();
371        return Ok(());
372    };
373
374    for (camera_entity, mut camera_transform, camera_state) in cameras_query.iter_mut() {
375        let Some(cursor_ray) = camera_state.cursor_ray else {
376            continue;
377        };
378        let world_pos = cursor_ray.origin.truncate();
379
380        match mouse_button_op {
381            MouseButtonOp::JustPressed => {
382                last_cursor_world_pos_by_camera.insert(camera_entity, world_pos);
383            }
384            MouseButtonOp::BeingPressed => {
385                if let Some(prev_pos) = last_cursor_world_pos_by_camera.get_mut(&camera_entity) {
386                    let movement = *prev_pos - world_pos;
387                    camera_transform.translation += movement.extend(0.0);
388                }
389            }
390        }
391    }
392    Ok(())
393}
394
395fn camera_2d_zoom(
396    mut egui_context: EguiContexts,
397    window_getter: WindowGetter,
398    mut cameras_query: Query<(
399        &mut Transform,
400        &VpeolCameraState,
401        &Camera,
402        &RenderTarget,
403        &Vpeol2dCameraControl,
404    )>,
405    mut wheel_events_reader: MessageReader<MouseWheel>,
406) -> Result {
407    if egui_context.ctx_mut()?.is_pointer_over_area() {
408        return Ok(());
409    }
410
411    for (mut camera_transform, camera_state, camera, render_target, camera_control) in
412        cameras_query.iter_mut()
413    {
414        let Some(cursor_ray) = camera_state.cursor_ray else {
415            continue;
416        };
417        let world_pos = cursor_ray.origin.truncate();
418
419        let zoom_amount: f32 = wheel_events_reader
420            .read()
421            .map(|wheel_event| match wheel_event.unit {
422                bevy::input::mouse::MouseScrollUnit::Line => {
423                    wheel_event.y * camera_control.zoom_per_scroll_line
424                }
425                bevy::input::mouse::MouseScrollUnit::Pixel => {
426                    wheel_event.y * camera_control.zoom_per_scroll_pixel
427                }
428            })
429            .sum();
430
431        if zoom_amount == 0.0 {
432            continue;
433        }
434
435        let scale_by = (-zoom_amount).exp();
436
437        let window = if let RenderTarget::Window(window_ref) = render_target {
438            window_getter.get_window(*window_ref).unwrap()
439        } else {
440            continue;
441        };
442        camera_transform.scale.x *= scale_by;
443        camera_transform.scale.y *= scale_by;
444        let Some(cursor_in_screen_pos) = window.cursor_position() else {
445            continue;
446        };
447        let Ok(new_cursor_ray) =
448            camera.viewport_to_world(&((*camera_transform.as_ref()).into()), cursor_in_screen_pos)
449        else {
450            continue;
451        };
452        let new_world_pos = new_cursor_ray.origin.truncate();
453        camera_transform.translation += (world_pos - new_world_pos).extend(0.0);
454    }
455    Ok(())
456}
457
458/// A position component that's edited and populated by vpeol_2d.
459#[derive(Clone, PartialEq, Serialize, Deserialize, Component, Default, YoleckComponent)]
460#[serde(transparent)]
461#[cfg_attr(feature = "bevy_reflect", derive(bevy::reflect::Reflect))]
462pub struct Vpeol2dPosition(pub Vec2);
463
464/// A rotation component that's edited and populated by vpeol_2d.
465///
466/// The rotation is in radians around the Z axis. Editing is done with egui using degrees.
467#[derive(Default, Clone, PartialEq, Serialize, Deserialize, Component, YoleckComponent)]
468#[serde(transparent)]
469#[cfg_attr(feature = "bevy_reflect", derive(bevy::reflect::Reflect))]
470pub struct Vpeol2dRotatation(pub f32);
471
472/// A scale component that's edited and populated by vpeol_2d.
473///
474/// Editing is done with egui using separate drag values for X and Y axes.
475#[derive(Clone, PartialEq, Serialize, Deserialize, Component, YoleckComponent)]
476#[serde(transparent)]
477#[cfg_attr(feature = "bevy_reflect", derive(bevy::reflect::Reflect))]
478pub struct Vpeol2dScale(pub Vec2);
479
480impl Default for Vpeol2dScale {
481    fn default() -> Self {
482        Self(Vec2::ONE)
483    }
484}
485
486fn vpeol_2d_edit_transform_group(
487    mut ui: ResMut<YoleckUi>,
488    position_edit: YoleckEdit<(Entity, &mut Vpeol2dPosition)>,
489    rotation_edit: YoleckEdit<&mut Vpeol2dRotatation>,
490    scale_edit: YoleckEdit<&mut Vpeol2dScale>,
491    passed_data: Res<YoleckPassedData>,
492) {
493    let has_any = !position_edit.is_empty() || !rotation_edit.is_empty() || !scale_edit.is_empty();
494    if !has_any {
495        return;
496    }
497
498    ui.group(|ui| {
499        ui.label(egui::RichText::new("Transform").strong());
500        ui.separator();
501
502        vpeol_2d_edit_position_impl(ui, position_edit, &passed_data);
503        vpeol_2d_edit_rotation_impl(ui, rotation_edit);
504        vpeol_2d_edit_scale_impl(ui, scale_edit);
505    });
506}
507
508fn vpeol_2d_edit_position_impl(
509    ui: &mut egui::Ui,
510    mut edit: YoleckEdit<(Entity, &mut Vpeol2dPosition)>,
511    passed_data: &YoleckPassedData,
512) {
513    if edit.is_empty() || edit.has_nonmatching() {
514        return;
515    }
516    let mut average = DVec2::ZERO;
517    let mut num_entities = 0;
518    let mut transition = Vec2::ZERO;
519    for (entity, position) in edit.iter_matching() {
520        if let Some(pos) = passed_data.get::<Vec3>(entity) {
521            transition = pos.truncate() - position.0;
522        }
523        average += position.0.as_dvec2();
524        num_entities += 1;
525    }
526    average /= num_entities as f64;
527
528    ui.horizontal(|ui| {
529        let mut new_average = average;
530
531        ui.add(egui::Label::new("Position"));
532        ui.add(egui::DragValue::new(&mut new_average.x).prefix("X:"));
533        ui.add(egui::DragValue::new(&mut new_average.y).prefix("Y:"));
534
535        transition += (new_average - average).as_vec2();
536    });
537
538    if transition.is_finite() && transition != Vec2::ZERO {
539        for (_, mut position) in edit.iter_matching_mut() {
540            position.0 += transition;
541        }
542    }
543}
544
545fn vpeol_2d_edit_rotation_impl(ui: &mut egui::Ui, mut edit: YoleckEdit<&mut Vpeol2dRotatation>) {
546    if edit.is_empty() || edit.has_nonmatching() {
547        return;
548    }
549
550    let mut average_rotation = 0.0_f32;
551    let mut num_entities = 0;
552
553    for rotation in edit.iter_matching() {
554        average_rotation += rotation.0;
555        num_entities += 1;
556    }
557    average_rotation /= num_entities as f32;
558
559    ui.horizontal(|ui| {
560        let mut rotation_deg = average_rotation.to_degrees();
561
562        ui.add(egui::Label::new("Rotation"));
563        ui.add(
564            egui::DragValue::new(&mut rotation_deg)
565                .speed(1.0)
566                .suffix("°"),
567        );
568
569        let new_rotation = rotation_deg.to_radians();
570        let transition = new_rotation - average_rotation;
571
572        if transition.is_finite() && transition != 0.0 {
573            for mut rotation in edit.iter_matching_mut() {
574                rotation.0 += transition;
575            }
576        }
577    });
578}
579
580fn vpeol_2d_edit_scale_impl(ui: &mut egui::Ui, mut edit: YoleckEdit<&mut Vpeol2dScale>) {
581    if edit.is_empty() || edit.has_nonmatching() {
582        return;
583    }
584    let mut average = DVec2::ZERO;
585    let mut num_entities = 0;
586
587    for scale in edit.iter_matching() {
588        average += scale.0.as_dvec2();
589        num_entities += 1;
590    }
591    average /= num_entities as f64;
592
593    ui.horizontal(|ui| {
594        let mut new_average = average;
595
596        ui.add(egui::Label::new("Scale"));
597        ui.vertical(|ui| {
598            ui.centered_and_justified(|ui| {
599                let axis_average = (average.x + average.y) / 2.0;
600                let mut new_axis_average = axis_average;
601                if ui
602                    .add(egui::DragValue::new(&mut new_axis_average).speed(0.01))
603                    .dragged()
604                {
605                    // Use difference instead of ration to avoid problems when reaching/crossing
606                    // the zero.
607                    let diff = new_axis_average - axis_average;
608                    new_average.x += diff;
609                    new_average.y += diff;
610                }
611            });
612            ui.horizontal(|ui| {
613                ui.add(
614                    egui::DragValue::new(&mut new_average.x)
615                        .prefix("X:")
616                        .speed(0.01),
617                );
618                ui.add(
619                    egui::DragValue::new(&mut new_average.y)
620                        .prefix("Y:")
621                        .speed(0.01),
622                );
623            });
624        });
625
626        let transition = (new_average - average).as_vec2();
627
628        if transition.is_finite() && transition != Vec2::ZERO {
629            for mut scale in edit.iter_matching_mut() {
630                scale.0 += transition;
631            }
632        }
633    });
634}
635
636fn vpeol_2d_init_position(
637    mut egui_context: EguiContexts,
638    ui: Res<YoleckUi>,
639    mut edit: YoleckEdit<&mut Vpeol2dPosition>,
640    cameras_query: Query<&VpeolCameraState>,
641    mouse_buttons: Res<ButtonInput<MouseButton>>,
642) -> YoleckExclusiveSystemDirective {
643    let Ok(mut position) = edit.single_mut() else {
644        return YoleckExclusiveSystemDirective::Finished;
645    };
646
647    let Some(cursor_ray) = cameras_query
648        .iter()
649        .find_map(|camera_state| camera_state.cursor_ray)
650    else {
651        return YoleckExclusiveSystemDirective::Listening;
652    };
653
654    position.0 = cursor_ray.origin.truncate();
655
656    if egui_context.ctx_mut().unwrap().is_pointer_over_area() || ui.ctx().is_pointer_over_area() {
657        return YoleckExclusiveSystemDirective::Listening;
658    }
659
660    if mouse_buttons.just_released(MouseButton::Left) {
661        return YoleckExclusiveSystemDirective::Finished;
662    }
663
664    YoleckExclusiveSystemDirective::Listening
665}
666
667fn vpeol_2d_populate_transform(
668    mut populate: YoleckPopulate<(
669        &Vpeol2dPosition,
670        Option<&Vpeol2dRotatation>,
671        Option<&Vpeol2dScale>,
672        &YoleckBelongsToLevel,
673    )>,
674    levels_query: Query<&VpeolRepositionLevel>,
675) {
676    populate.populate(
677        |_ctx, mut cmd, (position, rotation, scale, belongs_to_level)| {
678            let mut transform = Transform::from_translation(position.0.extend(0.0));
679            if let Some(Vpeol2dRotatation(rotation)) = rotation {
680                transform = transform.with_rotation(Quat::from_rotation_z(*rotation));
681            }
682            if let Some(Vpeol2dScale(scale)) = scale {
683                transform = transform.with_scale(scale.extend(1.0));
684            }
685
686            if let Ok(VpeolRepositionLevel(level_transform)) =
687                levels_query.get(belongs_to_level.level)
688            {
689                transform = *level_transform * transform;
690            }
691
692            cmd.insert((transform, GlobalTransform::from(transform)));
693        },
694    )
695}