bevy_yoleck/
vpeol_3d.rs

1//! # Viewport Editing Overlay for 3D games.
2//!
3//! Use this module to implement simple 3D editing for 3D 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 `Vpeol3dPluginForGame` instead when setting up for game.
16//! // - Use topdown is for games that utilize the XZ plane. There is also
17//! //   `Vpeol3dPluginForEditor::sidescroller` for games that mainly need the XY plane.
18//! app.add_plugins(Vpeol3dPluginForEditor::topdown());
19//! ```
20//!
21//! Add the following components to the camera entity:
22//! * [`VpeolCameraState`] in order to select and drag entities.
23//! * [`Vpeol3dCameraControl`] in order to control the camera with the mouse. This one can be
24//!   skipped if there are other means to control the camera inside the editor, or if no camera
25//!   control inside the editor is desired.
26//!
27//! ```no_run
28//! # use bevy::prelude::*;
29//! # use bevy_yoleck::vpeol::prelude::*;
30//! # let commands: Commands = panic!();
31//! commands
32//!     .spawn(Camera3d::default())
33//!     .insert(VpeolCameraState::default())
34//!     // Use a variant of the camera controls that fit the choice of editor plugin.
35//!     .insert(Vpeol3dCameraControl::topdown());
36//! ```
37//!
38//! Entity selection by clicking on it is supported by just adding the plugin. To implement
39//! dragging, there are two options:
40//!
41//! 1. Add  the [`Vpeol3dPosition`] Yoleck component and use it as the source of position (there
42//!    are also [`Vpeol3dRotation`] and [`Vpeol3dScale`], but they don't currently get editing
43//!    support from vpeol_3d). To enable dragging across the third axis, add
44//!    [`Vpeol3dThirdAxisWithKnob`] as well.
45//!     ```no_run
46//!     # use bevy::prelude::*;
47//!     # use bevy_yoleck::prelude::*;
48//!     # use bevy_yoleck::vpeol::prelude::*;
49//!     # use serde::{Deserialize, Serialize};
50//!     # #[derive(Clone, PartialEq, Serialize, Deserialize, Component, Default, YoleckComponent)]
51//!     # struct Example;
52//!     # let mut app = App::new();
53//!     app.add_yoleck_entity_type({
54//!         YoleckEntityType::new("Example")
55//!             .with::<Vpeol3dPosition>() // vpeol_3d dragging
56//!             .with::<Example>() // entity's specific data and systems
57//!             // Optional:
58//!             .insert_on_init_during_editor(|| Vpeol3dThirdAxisWithKnob {
59//!                 knob_distance: 2.0,
60//!                 knob_scale: 0.5,
61//!             })
62//!     });
63//!     ```
64//! 2. Use data passing. vpeol_3d will pass a `Vec3` to the entity being dragged:
65//!     ```no_run
66//!     # use bevy::prelude::*;
67//!     # use bevy_yoleck::prelude::*;
68//!     # use serde::{Deserialize, Serialize};
69//!     # #[derive(Clone, PartialEq, Serialize, Deserialize, Component, Default, YoleckComponent)]
70//!     # struct Example {
71//!     #     position: Vec3,
72//!     # }
73//!     # let mut app = App::new();
74//!     fn edit_example(mut edit: YoleckEdit<(Entity, &mut Example)>, passed_data: Res<YoleckPassedData>) {
75//!         let Ok((entity, mut example)) = edit.single_mut() else { return };
76//!         if let Some(pos) = passed_data.get::<Vec3>(entity) {
77//!             example.position = *pos;
78//!         }
79//!     }
80//!
81//!     fn populate_example(
82//!         mut populate: YoleckPopulate<&Example>,
83//!         asset_server: Res<AssetServer>
84//!     ) {
85//!         populate.populate(|_ctx, mut cmd, example| {
86//!             cmd.insert(Transform::from_translation(example.position));
87//!             cmd.insert(SceneRoot(asset_server.load("scene.glb#Scene0")));
88//!         });
89//!     }
90//!     ```
91//!     When using this option, [`Vpeol3dThirdAxisWithKnob`] can still be used to add the third
92//!     axis knob.
93
94use std::any::TypeId;
95
96use crate::bevy_egui::egui;
97use crate::exclusive_systems::{
98    YoleckEntityCreationExclusiveSystems, YoleckExclusiveSystemDirective,
99};
100use crate::vpeol::{
101    handle_clickable_children_system, ray_intersection_with_mesh, VpeolBasePlugin,
102    VpeolCameraState, VpeolDragPlane, VpeolRepositionLevel, VpeolRootResolver, VpeolSystems,
103};
104use crate::{prelude::*, YoleckDirective, YoleckSchedule};
105use bevy::camera::visibility::VisibleEntities;
106use bevy::color::palettes::css;
107use bevy::input::mouse::MouseWheel;
108use bevy::math::DVec3;
109use bevy::platform::collections::HashMap;
110use bevy::prelude::*;
111use bevy_egui::EguiContexts;
112use serde::{Deserialize, Serialize};
113
114/// Add the systems required for loading levels that use vpeol_3d components
115pub struct Vpeol3dPluginForGame;
116
117impl Plugin for Vpeol3dPluginForGame {
118    fn build(&self, app: &mut App) {
119        app.add_systems(
120            YoleckSchedule::OverrideCommonComponents,
121            vpeol_3d_populate_transform,
122        );
123        #[cfg(feature = "bevy_reflect")]
124        register_reflect_types(app);
125    }
126}
127
128#[cfg(feature = "bevy_reflect")]
129fn register_reflect_types(app: &mut App) {
130    app.register_type::<Vpeol3dPosition>();
131    app.register_type::<Vpeol3dRotation>();
132    app.register_type::<Vpeol3dScale>();
133    app.register_type::<Vpeol3dCameraControl>();
134}
135
136/// Add the systems required for 3D editing.
137///
138/// * 3D camera control (for cameras with [`Vpeol3dCameraControl`])
139/// * Entity selection.
140/// * Entity dragging.
141/// * Connecting nested entities.
142pub struct Vpeol3dPluginForEditor {
143    /// The plane to configure the global [`VpeolDragPlane`] resource with.
144    ///
145    /// Indiviual entities can override this with their own [`VpeolDragPlane`] component.
146    ///
147    /// It is a good idea to match this to [`Vpeol3dCameraControl::plane`].
148    pub drag_plane: InfinitePlane3d,
149}
150
151impl Vpeol3dPluginForEditor {
152    /// For sidescroller games - drag entities along the XY plane.
153    ///
154    /// Indiviual entities can override this with a [`VpeolDragPlane`] component.
155    ///
156    /// Adding [`Vpeol3dThirdAxisWithKnob`] can be used to allow Z axis manipulation.
157    ///
158    /// This combines well with [`Vpeol3dCameraControl::sidescroller`].
159    pub fn sidescroller() -> Self {
160        Self {
161            drag_plane: InfinitePlane3d { normal: Dir3::Z },
162        }
163    }
164
165    /// For games that are not sidescrollers - drag entities along the XZ plane.
166    ///
167    /// Indiviual entities can override this with a [`VpeolDragPlane`] component.
168    ///
169    /// Adding [`Vpeol3dThirdAxisWithKnob`] can be used to allow Y axis manipulation.
170    ///
171    /// This combines well with [`Vpeol3dCameraControl::topdown`].
172    pub fn topdown() -> Self {
173        Self {
174            drag_plane: InfinitePlane3d { normal: Dir3::Y },
175        }
176    }
177}
178
179impl Plugin for Vpeol3dPluginForEditor {
180    fn build(&self, app: &mut App) {
181        app.add_plugins(VpeolBasePlugin);
182        app.add_plugins(Vpeol3dPluginForGame);
183        app.insert_resource(VpeolDragPlane(self.drag_plane));
184
185        app.add_systems(
186            Update,
187            (update_camera_status_for_models,).in_set(VpeolSystems::UpdateCameraState),
188        );
189        app.add_systems(
190            PostUpdate, // to prevent camera shaking
191            (
192                camera_3d_pan,
193                camera_3d_move_along_plane_normal,
194                camera_3d_rotate,
195            )
196                .run_if(in_state(YoleckEditorState::EditorActive)),
197        );
198        app.add_systems(
199            Update,
200            (
201                ApplyDeferred,
202                handle_clickable_children_system::<With<Mesh3d>, ()>,
203                ApplyDeferred,
204            )
205                .chain()
206                .run_if(in_state(YoleckEditorState::EditorActive)),
207        );
208        app.add_yoleck_edit_system(vpeol_3d_edit_position);
209        app.world_mut()
210            .resource_mut::<YoleckEntityCreationExclusiveSystems>()
211            .on_entity_creation(|queue| queue.push_back(vpeol_3d_init_position));
212        app.add_yoleck_edit_system(vpeol_3d_edit_third_axis_with_knob);
213    }
214}
215
216fn update_camera_status_for_models(
217    mut cameras_query: Query<(&mut VpeolCameraState, &VisibleEntities)>,
218    entities_query: Query<(Entity, &GlobalTransform, &Mesh3d)>,
219    mesh_assets: Res<Assets<Mesh>>,
220    root_resolver: VpeolRootResolver,
221) {
222    for (mut camera_state, visible_entities) in cameras_query.iter_mut() {
223        let Some(cursor_ray) = camera_state.cursor_ray else {
224            continue;
225        };
226        for (entity, global_transform, mesh) in
227            entities_query.iter_many(visible_entities.iter(TypeId::of::<Mesh3d>()))
228        {
229            let Some(mesh) = mesh_assets.get(&mesh.0) else {
230                continue;
231            };
232
233            let inverse_transform = global_transform.to_matrix().inverse();
234
235            // Note: the transform may change the ray's length, which Bevy no longer supports
236            // (since version 0.13), so we keep the ray length separately and apply it later to the
237            // distance.
238            let ray_origin = inverse_transform.transform_point3(cursor_ray.origin);
239            let ray_vector = inverse_transform.transform_vector3(*cursor_ray.direction);
240            let Ok((ray_direction, ray_length_factor)) = Dir3::new_and_length(ray_vector) else {
241                continue;
242            };
243
244            let ray_in_object_coords = Ray3d {
245                origin: ray_origin,
246                direction: ray_direction,
247            };
248
249            let Some(distance) = ray_intersection_with_mesh(ray_in_object_coords, mesh) else {
250                continue;
251            };
252
253            let distance = distance / ray_length_factor;
254
255            let Some(root_entity) = root_resolver.resolve_root(entity) else {
256                continue;
257            };
258            camera_state.consider(root_entity, -distance, || cursor_ray.get_point(distance));
259        }
260    }
261}
262
263/// Move and rotate a camera entity with the mouse while inisde the editor.
264#[derive(Component)]
265#[cfg_attr(feature = "bevy_reflect", derive(bevy::reflect::Reflect))]
266pub struct Vpeol3dCameraControl {
267    /// Panning is done by dragging a plane with this as its origin.
268    pub plane_origin: Vec3,
269    /// Panning is done by dragging along this plane.
270    pub plane: InfinitePlane3d,
271    /// Is `Some`, enable mouse rotation. The up direction of the camera will be the specific
272    /// direction.
273    ///
274    /// It is a good idea to match this to [`Vpeol3dPluginForEditor::drag_plane`].
275    pub allow_rotation_while_maintaining_up: Option<Dir3>,
276    /// How much to change the proximity to the plane when receiving scroll event in
277    /// `MouseScrollUnit::Line` units.
278    pub proximity_per_scroll_line: f32,
279    /// How much to change the proximity to the plane when receiving scroll event in
280    /// `MouseScrollUnit::Pixel` units.
281    pub proximity_per_scroll_pixel: f32,
282}
283
284impl Vpeol3dCameraControl {
285    /// Preset for sidescroller games, where the the game world is on the XY plane.
286    ///
287    /// With this preset, the camera rotation is disabled.
288    ///
289    /// This combines well with [`Vpeol3dPluginForEditor::sidescroller`].
290    pub fn sidescroller() -> Self {
291        Self {
292            plane_origin: Vec3::ZERO,
293            plane: InfinitePlane3d {
294                normal: Dir3::NEG_Z,
295            },
296            allow_rotation_while_maintaining_up: None,
297            proximity_per_scroll_line: 2.0,
298            proximity_per_scroll_pixel: 0.01,
299        }
300    }
301
302    /// Preset for games where the the game world is mainly on the XZ plane (though there can still
303    /// be verticality)
304    ///
305    /// This combines well with [`Vpeol3dPluginForEditor::topdown`].
306    pub fn topdown() -> Self {
307        Self {
308            plane_origin: Vec3::ZERO,
309            plane: InfinitePlane3d { normal: Dir3::Y },
310            allow_rotation_while_maintaining_up: Some(Dir3::Y),
311            proximity_per_scroll_line: 2.0,
312            proximity_per_scroll_pixel: 0.01,
313        }
314    }
315
316    fn ray_intersection(&self, ray: Ray3d) -> Option<Vec3> {
317        let distance = ray.intersect_plane(self.plane_origin, self.plane)?;
318        Some(ray.get_point(distance))
319    }
320}
321
322fn camera_3d_pan(
323    mut egui_context: EguiContexts,
324    mouse_buttons: Res<ButtonInput<MouseButton>>,
325    mut cameras_query: Query<(
326        Entity,
327        &mut Transform,
328        &VpeolCameraState,
329        &Vpeol3dCameraControl,
330    )>,
331    mut last_cursor_world_pos_by_camera: Local<HashMap<Entity, Vec3>>,
332) -> Result {
333    enum MouseButtonOp {
334        JustPressed,
335        BeingPressed,
336    }
337
338    let mouse_button_op = if mouse_buttons.just_pressed(MouseButton::Right) {
339        if egui_context.ctx_mut()?.is_pointer_over_area() {
340            return Ok(());
341        }
342        MouseButtonOp::JustPressed
343    } else if mouse_buttons.pressed(MouseButton::Right) {
344        MouseButtonOp::BeingPressed
345    } else {
346        last_cursor_world_pos_by_camera.clear();
347        return Ok(());
348    };
349
350    for (camera_entity, mut camera_transform, camera_state, camera_control) in
351        cameras_query.iter_mut()
352    {
353        let Some(cursor_ray) = camera_state.cursor_ray else {
354            continue;
355        };
356        match mouse_button_op {
357            MouseButtonOp::JustPressed => {
358                let Some(world_pos) = camera_control.ray_intersection(cursor_ray) else {
359                    continue;
360                };
361                last_cursor_world_pos_by_camera.insert(camera_entity, world_pos);
362            }
363            MouseButtonOp::BeingPressed => {
364                if let Some(prev_pos) = last_cursor_world_pos_by_camera.get_mut(&camera_entity) {
365                    let Some(world_pos) = camera_control.ray_intersection(cursor_ray) else {
366                        continue;
367                    };
368                    let movement = *prev_pos - world_pos;
369                    camera_transform.translation += movement;
370                }
371            }
372        }
373    }
374    Ok(())
375}
376
377fn camera_3d_move_along_plane_normal(
378    mut egui_context: EguiContexts,
379    mut cameras_query: Query<(&mut Transform, &Vpeol3dCameraControl)>,
380    mut wheel_events_reader: MessageReader<MouseWheel>,
381) -> Result {
382    if egui_context.ctx_mut()?.is_pointer_over_area() {
383        return Ok(());
384    }
385
386    for (mut camera_transform, camera_control) in cameras_query.iter_mut() {
387        let zoom_amount: f32 = wheel_events_reader
388            .read()
389            .map(|wheel_event| match wheel_event.unit {
390                bevy::input::mouse::MouseScrollUnit::Line => {
391                    wheel_event.y * camera_control.proximity_per_scroll_line
392                }
393                bevy::input::mouse::MouseScrollUnit::Pixel => {
394                    wheel_event.y * camera_control.proximity_per_scroll_pixel
395                }
396            })
397            .sum();
398
399        if zoom_amount == 0.0 {
400            continue;
401        }
402
403        camera_transform.translation += zoom_amount * *camera_control.plane.normal;
404    }
405    Ok(())
406}
407
408fn camera_3d_rotate(
409    mut egui_context: EguiContexts,
410    mouse_buttons: Res<ButtonInput<MouseButton>>,
411    mut cameras_query: Query<(
412        Entity,
413        &mut Transform,
414        &VpeolCameraState,
415        &Vpeol3dCameraControl,
416    )>,
417    mut last_cursor_ray_by_camera: Local<HashMap<Entity, Ray3d>>,
418) -> Result {
419    enum MouseButtonOp {
420        JustPressed,
421        BeingPressed,
422    }
423
424    let mouse_button_op = if mouse_buttons.just_pressed(MouseButton::Middle) {
425        if egui_context.ctx_mut()?.is_pointer_over_area() {
426            return Ok(());
427        }
428        MouseButtonOp::JustPressed
429    } else if mouse_buttons.pressed(MouseButton::Middle) {
430        MouseButtonOp::BeingPressed
431    } else {
432        last_cursor_ray_by_camera.clear();
433        return Ok(());
434    };
435
436    for (camera_entity, mut camera_transform, camera_state, camera_control) in
437        cameras_query.iter_mut()
438    {
439        let Some(maintaining_up) = camera_control.allow_rotation_while_maintaining_up else {
440            continue;
441        };
442        let Some(cursor_ray) = camera_state.cursor_ray else {
443            continue;
444        };
445        match mouse_button_op {
446            MouseButtonOp::JustPressed => {
447                last_cursor_ray_by_camera.insert(camera_entity, cursor_ray);
448            }
449            MouseButtonOp::BeingPressed => {
450                if let Some(prev_ray) = last_cursor_ray_by_camera.get_mut(&camera_entity) {
451                    let rotation =
452                        Quat::from_rotation_arc(*cursor_ray.direction, *prev_ray.direction);
453                    camera_transform.rotate(rotation);
454                    let new_forward = camera_transform.forward();
455                    camera_transform.look_to(*new_forward, *maintaining_up);
456                }
457            }
458        }
459    }
460    Ok(())
461}
462
463/// A position component that's edited and populated by vpeol_3d.
464///
465/// Editing is done with egui, or by dragging the entity on a [`VpeolDragPlane`]  that passes
466/// through the entity. To support dragging perpendicular to that plane, use
467/// [`Vpeol3dThirdAxisWithKnob`].
468#[derive(Clone, PartialEq, Serialize, Deserialize, Component, Default, YoleckComponent)]
469#[serde(transparent)]
470#[cfg_attr(feature = "bevy_reflect", derive(bevy::reflect::Reflect))]
471pub struct Vpeol3dPosition(pub Vec3);
472
473/// Add a knob for dragging the entity perpendicular to the [`VpeolDragPlane`].
474///
475/// Dragging the knob will not actually change any component - it will only pass to the entity a
476/// `Vec3` that describes the drag. Since regular entity dragging is also implemented by passing a
477/// `Vec3`, just adding this component should be enough if there is already an edit system in place
478/// that reads that `Vec3` (such as the edit system for [`Vpeol3dPosition`])
479#[derive(Component)]
480pub struct Vpeol3dThirdAxisWithKnob {
481    /// The distance of the knob from the entity's origin.
482    pub knob_distance: f32,
483    /// A scale for the knob's model.
484    pub knob_scale: f32,
485}
486
487/// A rotation component that's populated (but not edited) by vpeol_3d.
488#[derive(Default, Clone, PartialEq, Serialize, Deserialize, Component, YoleckComponent)]
489#[serde(transparent)]
490#[cfg_attr(feature = "bevy_reflect", derive(bevy::reflect::Reflect))]
491pub struct Vpeol3dRotation(pub Quat);
492
493/// A scale component that's populated (but not edited) by vpeol_3d.
494#[derive(Clone, PartialEq, Serialize, Deserialize, Component, YoleckComponent)]
495#[serde(transparent)]
496#[cfg_attr(feature = "bevy_reflect", derive(bevy::reflect::Reflect))]
497pub struct Vpeol3dScale(pub Vec3);
498
499impl Default for Vpeol3dScale {
500    fn default() -> Self {
501        Self(Vec3::ONE)
502    }
503}
504
505enum CommonDragPlane {
506    NotDecidedYet,
507    WithNormal(Vec3),
508    NoSharedPlane,
509}
510
511impl CommonDragPlane {
512    fn consider(&mut self, normal: Vec3) {
513        *self = match self {
514            CommonDragPlane::NotDecidedYet => CommonDragPlane::WithNormal(normal),
515            CommonDragPlane::WithNormal(current_normal) => {
516                if *current_normal == normal {
517                    CommonDragPlane::WithNormal(normal)
518                } else {
519                    CommonDragPlane::NoSharedPlane
520                }
521            }
522            CommonDragPlane::NoSharedPlane => CommonDragPlane::NoSharedPlane,
523        }
524    }
525
526    fn shared_normal(&self) -> Option<Vec3> {
527        if let CommonDragPlane::WithNormal(normal) = self {
528            Some(*normal)
529        } else {
530            None
531        }
532    }
533}
534
535fn vpeol_3d_edit_position(
536    mut ui: ResMut<YoleckUi>,
537    mut edit: YoleckEdit<(Entity, &mut Vpeol3dPosition, Option<&VpeolDragPlane>)>,
538    global_drag_plane: Res<VpeolDragPlane>,
539    passed_data: Res<YoleckPassedData>,
540) {
541    if edit.is_empty() || edit.has_nonmatching() {
542        return;
543    }
544    // Use double precision to prevent rounding errors when there are many entities.
545    let mut average = DVec3::ZERO;
546    let mut num_entities = 0;
547    let mut transition = Vec3::ZERO;
548
549    let mut common_drag_plane = CommonDragPlane::NotDecidedYet;
550
551    for (entity, position, drag_plane) in edit.iter_matching() {
552        let VpeolDragPlane(drag_plane) = drag_plane.unwrap_or(global_drag_plane.as_ref());
553        common_drag_plane.consider(*drag_plane.normal);
554
555        if let Some(pos) = passed_data.get::<Vec3>(entity) {
556            transition = *pos - position.0;
557        }
558        average += position.0.as_dvec3();
559        num_entities += 1;
560    }
561    average /= num_entities as f64;
562
563    if common_drag_plane.shared_normal().is_none() {
564        transition = Vec3::ZERO;
565        ui.label(
566            egui::RichText::new("Drag plane differs - cannot drag together")
567                .color(egui::Color32::RED),
568        );
569    }
570    ui.horizontal(|ui| {
571        let mut new_average = average;
572        ui.add(egui::DragValue::new(&mut new_average.x).prefix("X:"));
573        ui.add(egui::DragValue::new(&mut new_average.y).prefix("Y:"));
574        ui.add(egui::DragValue::new(&mut new_average.z).prefix("Z:"));
575        transition += (new_average - average).as_vec3();
576    });
577
578    if transition.is_finite() && transition != Vec3::ZERO {
579        for (_, mut position, _) in edit.iter_matching_mut() {
580            position.0 += transition;
581        }
582    }
583}
584
585fn vpeol_3d_init_position(
586    mut egui_context: EguiContexts,
587    ui: Res<YoleckUi>,
588    mut edit: YoleckEdit<(&mut Vpeol3dPosition, Option<&VpeolDragPlane>)>,
589    global_drag_plane: Res<VpeolDragPlane>,
590    cameras_query: Query<&VpeolCameraState>,
591    mouse_buttons: Res<ButtonInput<MouseButton>>,
592) -> YoleckExclusiveSystemDirective {
593    let Ok((mut position, drag_plane)) = edit.single_mut() else {
594        return YoleckExclusiveSystemDirective::Finished;
595    };
596
597    let Some(cursor_ray) = cameras_query
598        .iter()
599        .find_map(|camera_state| camera_state.cursor_ray)
600    else {
601        return YoleckExclusiveSystemDirective::Listening;
602    };
603
604    let VpeolDragPlane(drag_plane) = drag_plane.unwrap_or(global_drag_plane.as_ref());
605    if let Some(distance_to_plane) =
606        cursor_ray.intersect_plane(position.0, InfinitePlane3d::new(*drag_plane.normal))
607    {
608        position.0 = cursor_ray.get_point(distance_to_plane);
609    };
610
611    if egui_context.ctx_mut().unwrap().is_pointer_over_area() || ui.ctx().is_pointer_over_area() {
612        return YoleckExclusiveSystemDirective::Listening;
613    }
614
615    if mouse_buttons.just_released(MouseButton::Left) {
616        return YoleckExclusiveSystemDirective::Finished;
617    }
618
619    YoleckExclusiveSystemDirective::Listening
620}
621
622fn vpeol_3d_edit_third_axis_with_knob(
623    mut edit: YoleckEdit<(
624        Entity,
625        &GlobalTransform,
626        &Vpeol3dThirdAxisWithKnob,
627        Option<&VpeolDragPlane>,
628    )>,
629    global_drag_plane: Res<VpeolDragPlane>,
630    mut knobs: YoleckKnobs,
631    mut mesh_assets: ResMut<Assets<Mesh>>,
632    mut material_assets: ResMut<Assets<StandardMaterial>>,
633    mut mesh_and_material: Local<Option<(Handle<Mesh>, Handle<StandardMaterial>)>>,
634    mut directives_writer: MessageWriter<YoleckDirective>,
635) {
636    if edit.is_empty() || edit.has_nonmatching() {
637        return;
638    }
639
640    let (mesh, material) = mesh_and_material.get_or_insert_with(|| {
641        (
642            mesh_assets.add(Mesh::from(Cylinder {
643                radius: 0.5,
644                half_height: 0.5,
645            })),
646            material_assets.add(Color::from(css::ORANGE_RED)),
647        )
648    });
649
650    let mut common_drag_plane = CommonDragPlane::NotDecidedYet;
651    for (_, _, _, drag_plane) in edit.iter_matching() {
652        let VpeolDragPlane(drag_plane) = drag_plane.unwrap_or(global_drag_plane.as_ref());
653        common_drag_plane.consider(*drag_plane.normal);
654    }
655    let Some(drag_plane_normal) = common_drag_plane.shared_normal() else {
656        return;
657    };
658
659    for (entity, global_transform, third_axis_with_knob, _) in edit.iter_matching() {
660        let entity_position = global_transform.translation();
661
662        for (knob_name, drag_plane_normal) in [
663            ("vpeol-3d-third-axis-knob-positive", drag_plane_normal),
664            ("vpeol-3d-third-axis-knob-negative", -drag_plane_normal),
665        ] {
666            let mut knob = knobs.knob((entity, knob_name));
667            let knob_offset = third_axis_with_knob.knob_distance * drag_plane_normal;
668            let knob_transform = Transform {
669                translation: entity_position + knob_offset,
670                rotation: Quat::from_rotation_arc(Vec3::Y, drag_plane_normal),
671                scale: third_axis_with_knob.knob_scale * Vec3::ONE,
672            };
673            knob.cmd.insert(VpeolDragPlane(InfinitePlane3d {
674                normal: Dir3::new(drag_plane_normal.cross(Vec3::X)).unwrap_or(Dir3::Y),
675            }));
676            knob.cmd.insert((
677                Mesh3d(mesh.clone()),
678                MeshMaterial3d(material.clone()),
679                knob_transform,
680                GlobalTransform::from(knob_transform),
681            ));
682            if let Some(pos) = knob.get_passed_data::<Vec3>() {
683                let vector_from_entity = *pos - knob_offset - entity_position;
684                let along_drag_normal = vector_from_entity.dot(drag_plane_normal);
685                let vector_along_drag_normal = along_drag_normal * drag_plane_normal;
686                let position_along_drag_normal = entity_position + vector_along_drag_normal;
687                // NOTE: we don't need to send this to all the selected entities. This will be
688                // handled in the system that receives the passed data.
689                directives_writer.write(YoleckDirective::pass_to_entity(
690                    entity,
691                    position_along_drag_normal,
692                ));
693            }
694        }
695    }
696}
697
698fn vpeol_3d_populate_transform(
699    mut populate: YoleckPopulate<(
700        &Vpeol3dPosition,
701        Option<&Vpeol3dRotation>,
702        Option<&Vpeol3dScale>,
703        &YoleckBelongsToLevel,
704    )>,
705    levels_query: Query<&VpeolRepositionLevel>,
706) {
707    populate.populate(
708        |_ctx, mut cmd, (position, rotation, scale, belongs_to_level)| {
709            let mut transform = Transform::from_translation(position.0);
710            if let Some(Vpeol3dRotation(rotation)) = rotation {
711                transform = transform.with_rotation(*rotation);
712            }
713            if let Some(Vpeol3dScale(scale)) = scale {
714                transform = transform.with_scale(*scale);
715            }
716
717            if let Ok(VpeolRepositionLevel(level_transform)) =
718                levels_query.get(belongs_to_level.level)
719            {
720                transform = *level_transform * transform;
721            }
722
723            cmd.insert((transform, GlobalTransform::from(transform)));
724        },
725    )
726}