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