1use 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
114pub 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
136pub struct Vpeol3dPluginForEditor {
143 pub drag_plane: InfinitePlane3d,
149}
150
151impl Vpeol3dPluginForEditor {
152 pub fn sidescroller() -> Self {
160 Self {
161 drag_plane: InfinitePlane3d { normal: Dir3::Z },
162 }
163 }
164
165 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, (
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 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#[derive(Component)]
265#[cfg_attr(feature = "bevy_reflect", derive(bevy::reflect::Reflect))]
266pub struct Vpeol3dCameraControl {
267 pub plane_origin: Vec3,
269 pub plane: InfinitePlane3d,
271 pub allow_rotation_while_maintaining_up: Option<Dir3>,
276 pub proximity_per_scroll_line: f32,
279 pub proximity_per_scroll_pixel: f32,
282}
283
284impl Vpeol3dCameraControl {
285 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 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#[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#[derive(Component)]
480pub struct Vpeol3dThirdAxisWithKnob {
481 pub knob_distance: f32,
483 pub knob_scale: f32,
485}
486
487#[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#[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 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 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}