1use bevy::camera::primitives::{Aabb, MeshAabb};
9use bevy::camera::RenderTarget;
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::knobs::YoleckKnobMarker;
20use crate::prelude::{YoleckEditorState, YoleckUi};
21use crate::{YoleckDirective, YoleckEditMarker, YoleckManaged, YoleckRunEditSystems};
22
23pub mod prelude {
24 pub use crate::vpeol::{
25 VpeolCameraState, VpeolDragPlane, VpeolRepositionLevel, VpeolSelectionCuePlugin,
26 VpeolWillContainClickableChildren, YoleckKnobClick,
27 };
28 #[cfg(feature = "vpeol_2d")]
29 pub use crate::vpeol_2d::{
30 Vpeol2dCameraControl, Vpeol2dPluginForEditor, Vpeol2dPluginForGame, Vpeol2dPosition,
31 Vpeol2dRotatation, Vpeol2dScale,
32 };
33 #[cfg(feature = "vpeol_3d")]
34 pub use crate::vpeol_3d::{
35 Vpeol3dCameraControl, Vpeol3dPluginForEditor, Vpeol3dPluginForGame, Vpeol3dPosition,
36 Vpeol3dRotation, Vpeol3dScale, Vpeol3dThirdAxisWithKnob,
37 };
38}
39
40#[derive(SystemSet, Clone, PartialEq, Eq, Debug, Hash)]
42pub enum VpeolSystems {
43 PrepareCameraState,
49 UpdateCameraState,
52 HandleCameraState,
54}
55
56pub struct VpeolBasePlugin;
58
59impl Plugin for VpeolBasePlugin {
60 fn build(&self, app: &mut App) {
61 app.configure_sets(
62 Update,
63 (
64 VpeolSystems::PrepareCameraState.run_if(in_state(YoleckEditorState::EditorActive)),
65 VpeolSystems::UpdateCameraState.run_if(in_state(YoleckEditorState::EditorActive)),
66 VpeolSystems::HandleCameraState.run_if(in_state(YoleckEditorState::EditorActive)),
67 YoleckRunEditSystems,
68 )
69 .chain(), );
71 app.add_systems(
72 Update,
73 (prepare_camera_state, update_camera_world_position)
74 .in_set(VpeolSystems::PrepareCameraState),
75 );
76 app.add_systems(
77 Update,
78 handle_camera_state.in_set(VpeolSystems::HandleCameraState),
79 );
80 #[cfg(feature = "bevy_reflect")]
81 app.register_type::<VpeolDragPlane>();
82 }
83}
84
85#[derive(Component, Resource)]
95#[cfg_attr(feature = "bevy_reflect", derive(bevy::reflect::Reflect))]
96pub struct VpeolDragPlane(pub InfinitePlane3d);
97
98impl VpeolDragPlane {
99 pub const XY: VpeolDragPlane = VpeolDragPlane(InfinitePlane3d { normal: Dir3::Z });
100 pub const XZ: VpeolDragPlane = VpeolDragPlane(InfinitePlane3d { normal: Dir3::Y });
101}
102
103#[derive(Component, Default, Debug)]
105pub struct VpeolCameraState {
106 pub cursor_ray: Option<Ray3d>,
108 pub entity_under_cursor: Option<(Entity, VpeolCursorPointing)>,
110 pub entities_of_interest: HashMap<Entity, Option<VpeolCursorPointing>>,
113 pub clicks_on_objects_state: VpeolClicksOnObjectsState,
115}
116
117#[derive(Clone, Debug)]
119pub struct VpeolCursorPointing {
120 pub cursor_position_world_coords: Vec3,
122 pub z_depth_screen_coords: f32,
124}
125
126#[derive(Default, Debug)]
128pub enum VpeolClicksOnObjectsState {
129 #[default]
130 Empty,
131 BeingDragged {
132 entity: Entity,
133 prev_screen_pos: Vec2,
135 offset: Vec3,
137 select_on_mouse_release: bool,
138 },
139}
140
141impl VpeolCameraState {
142 pub fn consider(
147 &mut self,
148 entity: Entity,
149 z_depth_screen_coords: f32,
150 cursor_position_world_coords: impl FnOnce() -> Vec3,
151 ) {
152 let should_update_entity = if let Some((_, old_cursor)) = self.entity_under_cursor.as_ref()
153 {
154 old_cursor.z_depth_screen_coords < z_depth_screen_coords
155 } else {
156 true
157 };
158
159 if let Some(of_interest) = self.entities_of_interest.get_mut(&entity) {
160 let pointing = VpeolCursorPointing {
161 cursor_position_world_coords: cursor_position_world_coords(),
162 z_depth_screen_coords,
163 };
164 if should_update_entity {
165 self.entity_under_cursor = Some((entity, pointing.clone()));
166 }
167 *of_interest = Some(pointing);
168 } else if should_update_entity {
169 self.entity_under_cursor = Some((
170 entity,
171 VpeolCursorPointing {
172 cursor_position_world_coords: cursor_position_world_coords(),
173 z_depth_screen_coords,
174 },
175 ));
176 }
177 }
178
179 pub fn pointing_at_entity(&self, entity: Entity) -> Option<&VpeolCursorPointing> {
180 if let Some((entity_under_cursor, pointing_at)) = &self.entity_under_cursor {
181 if *entity_under_cursor == entity {
182 return Some(pointing_at);
183 }
184 }
185 self.entities_of_interest.get(&entity)?.as_ref()
186 }
187}
188
189fn prepare_camera_state(
190 mut query: Query<&mut VpeolCameraState>,
191 knob_query: Query<Entity, With<YoleckKnobMarker>>,
192) {
193 for mut camera_state in query.iter_mut() {
194 camera_state.entity_under_cursor = None;
195 camera_state.entities_of_interest = knob_query
196 .iter()
197 .chain(match camera_state.clicks_on_objects_state {
198 VpeolClicksOnObjectsState::Empty => None,
199 VpeolClicksOnObjectsState::BeingDragged { entity, .. } => Some(entity),
200 })
201 .map(|entity| (entity, None))
202 .collect();
203 }
204}
205
206fn update_camera_world_position(
207 mut cameras_query: Query<(&mut VpeolCameraState, &GlobalTransform, &Camera)>,
208 window_getter: WindowGetter,
209) {
210 for (mut camera_state, camera_transform, camera) in cameras_query.iter_mut() {
211 camera_state.cursor_ray = (|| {
212 let RenderTarget::Window(window_ref) = camera.target else {
213 return None;
214 };
215 let window = window_getter.get_window(window_ref)?;
216 let cursor_in_screen_pos = window.cursor_position()?;
217 camera
218 .viewport_to_world(camera_transform, cursor_in_screen_pos)
219 .ok()
220 })();
221 }
222}
223
224#[derive(SystemParam)]
225pub(crate) struct WindowGetter<'w, 's> {
226 windows: Query<'w, 's, &'static Window>,
227 primary_window: Query<'w, 's, &'static Window, With<PrimaryWindow>>,
228}
229
230impl WindowGetter<'_, '_> {
231 pub fn get_window(&self, window_ref: WindowRef) -> Option<&Window> {
232 match window_ref {
233 WindowRef::Primary => self.primary_window.single().ok(),
234 WindowRef::Entity(window_id) => self.windows.get(window_id).ok(),
235 }
236 }
237}
238
239#[allow(clippy::too_many_arguments)]
240fn handle_camera_state(
241 mut egui_context: EguiContexts,
242 mut query: Query<(&Camera, &mut VpeolCameraState)>,
243 window_getter: WindowGetter,
244 mouse_buttons: Res<ButtonInput<MouseButton>>,
245 keyboard: Res<ButtonInput<KeyCode>>,
246 global_transform_query: Query<&GlobalTransform>,
247 selected_query: Query<(), With<YoleckEditMarker>>,
248 knob_query: Query<Entity, With<YoleckKnobMarker>>,
249 mut directives_writer: MessageWriter<YoleckDirective>,
250 global_drag_plane: Res<VpeolDragPlane>,
251 drag_plane_overrides_query: Query<&VpeolDragPlane>,
252) -> Result {
253 enum MouseButtonOp {
254 JustPressed,
255 BeingPressed,
256 JustReleased,
257 }
258 let mouse_button_op = if mouse_buttons.just_pressed(MouseButton::Left) {
259 if egui_context.ctx_mut()?.is_pointer_over_area() {
260 return Ok(());
261 }
262 MouseButtonOp::JustPressed
263 } else if mouse_buttons.just_released(MouseButton::Left) {
264 MouseButtonOp::JustReleased
265 } else if mouse_buttons.pressed(MouseButton::Left) {
266 MouseButtonOp::BeingPressed
267 } else {
268 for (_, mut camera_state) in query.iter_mut() {
269 camera_state.clicks_on_objects_state = VpeolClicksOnObjectsState::Empty;
270 }
271 return Ok(());
272 };
273 for (camera, mut camera_state) in query.iter_mut() {
274 let Some(cursor_ray) = camera_state.cursor_ray else {
275 continue;
276 };
277 let calc_cursor_in_world_position = |entity: Entity, plane_origin: Vec3| -> Option<Vec3> {
278 let VpeolDragPlane(drag_plane) = drag_plane_overrides_query
279 .get(entity)
280 .unwrap_or(&global_drag_plane);
281 let distance = cursor_ray.intersect_plane(plane_origin, *drag_plane)?;
282 Some(cursor_ray.get_point(distance))
283 };
284
285 let RenderTarget::Window(window_ref) = camera.target else {
286 continue;
287 };
288 let Some(window) = window_getter.get_window(window_ref) else {
289 continue;
290 };
291 let Some(cursor_in_screen_pos) = window.cursor_position() else {
292 continue;
293 };
294
295 match (&mouse_button_op, &camera_state.clicks_on_objects_state) {
296 (MouseButtonOp::JustPressed, VpeolClicksOnObjectsState::Empty) => {
297 if keyboard.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]) {
298 if let Some((entity, _)) = &camera_state.entity_under_cursor {
299 directives_writer.write(YoleckDirective::toggle_selected(*entity));
300 }
301 } else if let Some((knob_entity, cursor_pointing)) =
302 knob_query.iter().find_map(|knob_entity| {
303 Some((knob_entity, camera_state.pointing_at_entity(knob_entity)?))
304 })
305 {
306 directives_writer.write(YoleckDirective::pass_to_entity(
307 knob_entity,
308 YoleckKnobClick,
309 ));
310 let Ok(knob_transform) = global_transform_query.get(knob_entity) else {
311 continue;
312 };
313 let Some(cursor_in_world_position) = calc_cursor_in_world_position(
314 knob_entity,
315 cursor_pointing.cursor_position_world_coords,
316 ) else {
317 continue;
318 };
319 camera_state.clicks_on_objects_state = VpeolClicksOnObjectsState::BeingDragged {
320 entity: knob_entity,
321 prev_screen_pos: cursor_in_screen_pos,
322 offset: cursor_in_world_position - knob_transform.translation(),
323 select_on_mouse_release: false,
324 }
325 } else {
326 camera_state.clicks_on_objects_state = if let Some((entity, cursor_pointing)) =
327 &camera_state.entity_under_cursor
328 {
329 let Ok(entity_transform) = global_transform_query.get(*entity) else {
330 continue;
331 };
332 let select_on_mouse_release = selected_query.contains(*entity);
333 if !select_on_mouse_release {
334 directives_writer.write(YoleckDirective::set_selected(Some(*entity)));
335 }
336 let Some(cursor_in_world_position) = calc_cursor_in_world_position(
337 *entity,
338 cursor_pointing.cursor_position_world_coords,
339 ) else {
340 continue;
341 };
342 VpeolClicksOnObjectsState::BeingDragged {
343 entity: *entity,
344 prev_screen_pos: cursor_in_screen_pos,
345 offset: cursor_in_world_position - entity_transform.translation(),
346 select_on_mouse_release,
347 }
348 } else {
349 directives_writer.write(YoleckDirective::set_selected(None));
350 VpeolClicksOnObjectsState::Empty
351 };
352 }
353 }
354 (
355 MouseButtonOp::BeingPressed,
356 VpeolClicksOnObjectsState::BeingDragged {
357 entity,
358 prev_screen_pos,
359 offset,
360 select_on_mouse_release: _,
361 },
362 ) => {
363 if 0.1 <= prev_screen_pos.distance_squared(cursor_in_screen_pos) {
364 let Ok(entity_transform) = global_transform_query.get(*entity) else {
365 continue;
366 };
367 let drag_point = entity_transform.translation() + *offset;
368 let Some(cursor_in_world_position) =
369 calc_cursor_in_world_position(*entity, drag_point)
370 else {
371 continue;
372 };
373 directives_writer.write(YoleckDirective::pass_to_entity(
374 *entity,
375 cursor_in_world_position - *offset,
376 ));
377 camera_state.clicks_on_objects_state =
378 VpeolClicksOnObjectsState::BeingDragged {
379 entity: *entity,
380 prev_screen_pos: cursor_in_screen_pos,
381 offset: *offset,
382 select_on_mouse_release: false,
383 };
384 }
385 }
386 (
387 MouseButtonOp::JustReleased,
388 VpeolClicksOnObjectsState::BeingDragged {
389 entity,
390 prev_screen_pos: _,
391 offset: _,
392 select_on_mouse_release: true,
393 },
394 ) => {
395 directives_writer.write(YoleckDirective::set_selected(Some(*entity)));
396 camera_state.clicks_on_objects_state = VpeolClicksOnObjectsState::Empty;
397 }
398 _ => {}
399 }
400 }
401 Ok(())
402}
403
404pub struct YoleckKnobClick;
407
408#[derive(Component)]
413pub struct VpeolWillContainClickableChildren;
414
415#[derive(Component)]
417pub struct VpeolRouteClickTo(pub Entity);
418
419#[derive(SystemParam)]
422pub struct VpeolRootResolver<'w, 's> {
423 root_resolver: Query<'w, 's, &'static VpeolRouteClickTo>,
424 #[allow(clippy::type_complexity)]
425 has_managed_query: Query<'w, 's, (), Or<(With<YoleckManaged>, With<YoleckKnobMarker>)>>,
426}
427
428impl VpeolRootResolver<'_, '_> {
429 pub fn resolve_root(&self, entity: Entity) -> Option<Entity> {
431 if let Ok(VpeolRouteClickTo(root_entity)) = self.root_resolver.get(entity) {
432 Some(*root_entity)
433 } else {
434 self.has_managed_query.get(entity).ok()?;
435 Some(entity)
436 }
437 }
438}
439
440pub fn handle_clickable_children_system<F, B>(
442 parents_query: Query<(Entity, &Children), With<VpeolWillContainClickableChildren>>,
443 children_query: Query<&Children>,
444 should_add_query: Query<Entity, F>,
445 mut commands: Commands,
446) where
447 F: QueryFilter,
448 B: Default + Bundle,
449{
450 for (parent, children) in parents_query.iter() {
451 if children.is_empty() {
452 continue;
453 }
454 let mut any_added = false;
455 let mut children_to_check: Vec<Entity> = children.iter().collect();
456 while let Some(child) = children_to_check.pop() {
457 if let Ok(child_children) = children_query.get(child) {
458 children_to_check.extend(child_children.iter());
459 }
460 if should_add_query.get(child).is_ok() {
461 commands
462 .entity(child)
463 .try_insert((VpeolRouteClickTo(parent), B::default()));
464 any_added = true;
465 }
466 }
467 if any_added {
468 commands
469 .entity(parent)
470 .remove::<VpeolWillContainClickableChildren>();
471 }
472 }
473}
474
475pub struct VpeolSelectionCuePlugin {
477 pub effect_duration: f32,
479 pub effect_magnitude: f32,
481}
482
483impl Default for VpeolSelectionCuePlugin {
484 fn default() -> Self {
485 Self {
486 effect_duration: 0.3,
487 effect_magnitude: 0.3,
488 }
489 }
490}
491
492impl Plugin for VpeolSelectionCuePlugin {
493 fn build(&self, app: &mut App) {
494 app.add_systems(Update, manage_selection_transform_components);
495 app.add_systems(PostUpdate, {
496 add_selection_cue_before_transform_propagate(
497 1.0 / self.effect_duration,
498 2.0 * self.effect_magnitude,
499 )
500 .before(TransformSystems::Propagate)
501 });
502 app.add_systems(PostUpdate, {
503 restore_transform_from_cache_after_transform_propagate
504 .after(TransformSystems::Propagate)
505 });
506 }
507}
508
509#[derive(Component)]
510struct SelectionCueAnimation {
511 cached_transform: Transform,
512 progress: f32,
513}
514
515fn manage_selection_transform_components(
516 add_cue_query: Query<Entity, (Without<SelectionCueAnimation>, With<YoleckEditMarker>)>,
517 remove_cue_query: Query<Entity, (With<SelectionCueAnimation>, Without<YoleckEditMarker>)>,
518 mut commands: Commands,
519) {
520 for entity in add_cue_query.iter() {
521 commands.entity(entity).insert(SelectionCueAnimation {
522 cached_transform: Default::default(),
523 progress: 0.0,
524 });
525 }
526 for entity in remove_cue_query.iter() {
527 commands.entity(entity).remove::<SelectionCueAnimation>();
528 }
529}
530
531fn add_selection_cue_before_transform_propagate(
532 time_speedup: f32,
533 magnitude_scale: f32,
534) -> impl FnMut(Query<(&mut SelectionCueAnimation, &mut Transform)>, Res<Time>) {
535 move |mut query, time| {
536 for (mut animation, mut transform) in query.iter_mut() {
537 animation.cached_transform = *transform;
538 if animation.progress < 1.0 {
539 animation.progress += time_speedup * time.delta_secs();
540 let extra = if animation.progress < 0.5 {
541 animation.progress
542 } else {
543 1.0 - animation.progress
544 };
545 transform.scale *= 1.0 + magnitude_scale * extra;
546 }
547 }
548 }
549}
550
551fn restore_transform_from_cache_after_transform_propagate(
552 mut query: Query<(&SelectionCueAnimation, &mut Transform)>,
553) {
554 for (animation, mut transform) in query.iter_mut() {
555 *transform = animation.cached_transform;
556 }
557}
558
559pub(crate) fn ray_intersection_with_mesh(ray: Ray3d, mesh: &Mesh) -> Option<f32> {
560 let aabb = mesh.compute_aabb()?;
561 let distance_to_aabb = ray_intersection_with_aabb(ray, aabb)?;
562
563 if let Some(mut triangles) = iter_triangles(mesh) {
564 triangles.find_map(|triangle| triangle.ray_intersection(ray))
565 } else {
566 Some(distance_to_aabb)
567 }
568}
569
570fn ray_intersection_with_aabb(ray: Ray3d, aabb: Aabb) -> Option<f32> {
571 let center: Vec3 = aabb.center.into();
572 let mut max_low = f32::NEG_INFINITY;
573 let mut min_high = f32::INFINITY;
574 for (axis, half_extent) in [
575 (Vec3::X, aabb.half_extents.x),
576 (Vec3::Y, aabb.half_extents.y),
577 (Vec3::Z, aabb.half_extents.z),
578 ] {
579 let dot = ray.direction.dot(axis);
580 if dot == 0.0 {
581 let distance_from_center = (ray.origin - center).dot(axis);
582 if half_extent < distance_from_center.abs() {
583 return None;
584 }
585 } else {
586 let low = ray.intersect_plane(center - half_extent * axis, InfinitePlane3d::new(axis));
587 let high = ray.intersect_plane(center + half_extent * axis, InfinitePlane3d::new(axis));
588 let (low, high) = if 0.0 <= dot { (low, high) } else { (high, low) };
589 if let Some(low) = low {
590 max_low = max_low.max(low);
591 }
592 if let Some(high) = high {
593 min_high = min_high.min(high);
594 } else {
595 return None;
596 }
597 }
598 }
599 if max_low <= min_high {
600 Some(max_low)
601 } else {
602 None
603 }
604}
605
606fn iter_triangles(mesh: &Mesh) -> Option<impl '_ + Iterator<Item = Triangle>> {
607 if mesh.primitive_topology() != PrimitiveTopology::TriangleList {
608 return None;
609 }
610 let indices = mesh.indices()?;
611 let Some(VertexAttributeValues::Float32x3(positions)) =
612 mesh.attribute(Mesh::ATTRIBUTE_POSITION)
613 else {
614 return None;
615 };
616 let mut it = indices.iter();
617 Some(std::iter::from_fn(move || {
618 Some(Triangle(
619 [it.next()?, it.next()?, it.next()?].map(|idx| Vec3::from_array(positions[idx])),
620 ))
621 }))
622}
623
624#[derive(Debug)]
625struct Triangle([Vec3; 3]);
626
627impl Triangle {
628 fn ray_intersection(&self, ray: Ray3d) -> Option<f32> {
629 let directions = [
630 self.0[1] - self.0[0],
631 self.0[2] - self.0[1],
632 self.0[0] - self.0[2],
633 ];
634 let normal = directions[0].cross(directions[1]); let plane = InfinitePlane3d {
636 normal: Dir3::new(normal).ok()?,
637 };
638 let distance = ray.intersect_plane(self.0[0], plane)?;
639 let point = ray.get_point(distance);
640 if self
641 .0
642 .iter()
643 .zip(directions.iter())
644 .all(|(vertex, direction)| {
645 let vertical = direction.cross(normal);
646 vertical.dot(point - *vertex) <= 0.0
647 })
648 {
649 Some(distance)
650 } else {
651 None
652 }
653 }
654}
655
656pub fn vpeol_read_click_on_entity<Filter: QueryFilter>(
663 mut ui: ResMut<YoleckUi>,
664 cameras_query: Query<&VpeolCameraState>,
665 yoleck_managed_query: Query<&YoleckManaged>,
666 filter_query: Query<(), Filter>,
667 buttons: Res<ButtonInput<MouseButton>>,
668 mut candidate: Local<Option<Entity>>,
669) -> Option<Entity> {
670 let target = if ui.ctx().is_pointer_over_area() {
671 None
672 } else {
673 cameras_query
674 .iter()
675 .find_map(|camera_state| Some(camera_state.entity_under_cursor.as_ref()?.0))
676 };
677
678 let Some(target) = target else {
679 ui.label("No Target");
680 return None;
681 };
682
683 let Ok(yoleck_managed) = yoleck_managed_query.get(target) else {
684 ui.label("No Target");
685 return None;
686 };
687
688 if !filter_query.contains(target) {
689 ui.label(format!("Invalid Target ({})", yoleck_managed.type_name));
690 return None;
691 }
692 ui.label(format!(
693 "Targeting {:?} ({})",
694 target, yoleck_managed.type_name
695 ));
696
697 if buttons.just_pressed(MouseButton::Left) {
698 *candidate = Some(target);
699 } else if buttons.just_released(MouseButton::Left) {
700 if let Some(candidate) = candidate.take() {
701 if candidate == target {
702 return Some(target);
703 }
704 }
705 }
706 None
707}
708
709#[derive(Component)]
720pub struct VpeolRepositionLevel(pub Transform);