1use std::any::TypeId;
97
98use crate::bevy_egui::egui;
99use crate::exclusive_systems::{
100 YoleckEntityCreationExclusiveSystems, YoleckExclusiveSystemDirective,
101};
102use crate::vpeol::{
103 handle_clickable_children_system, ray_intersection_with_mesh, VpeolBasePlugin,
104 VpeolCameraState, VpeolDragPlane, VpeolRepositionLevel, VpeolRootResolver, VpeolSystemSet,
105};
106use crate::{prelude::*, YoleckDirective, YoleckSchedule};
107use bevy::color::palettes::css;
108use bevy::input::mouse::MouseWheel;
109use bevy::math::DVec3;
110use bevy::platform::collections::HashMap;
111use bevy::prelude::*;
112use bevy::render::view::VisibleEntities;
113use bevy_egui::EguiContexts;
114use serde::{Deserialize, Serialize};
115
116pub struct Vpeol3dPluginForGame;
118
119impl Plugin for Vpeol3dPluginForGame {
120 fn build(&self, app: &mut App) {
121 app.add_systems(
122 YoleckSchedule::OverrideCommonComponents,
123 vpeol_3d_populate_transform,
124 );
125 #[cfg(feature = "bevy_reflect")]
126 register_reflect_types(app);
127 }
128}
129
130#[cfg(feature = "bevy_reflect")]
131fn register_reflect_types(app: &mut App) {
132 app.register_type::<Vpeol3dPosition>();
133 app.register_type::<Vpeol3dRotation>();
134 app.register_type::<Vpeol3dScale>();
135 app.register_type::<Vpeol3dCameraControl>();
136}
137
138pub struct Vpeol3dPluginForEditor {
145 pub drag_plane: InfinitePlane3d,
151}
152
153impl Vpeol3dPluginForEditor {
154 pub fn sidescroller() -> Self {
162 Self {
163 drag_plane: InfinitePlane3d { normal: Dir3::Z },
164 }
165 }
166
167 pub fn topdown() -> Self {
175 Self {
176 drag_plane: InfinitePlane3d { normal: Dir3::Y },
177 }
178 }
179}
180
181impl Plugin for Vpeol3dPluginForEditor {
182 fn build(&self, app: &mut App) {
183 app.add_plugins(VpeolBasePlugin);
184 app.add_plugins(Vpeol3dPluginForGame);
185 app.insert_resource(VpeolDragPlane(self.drag_plane));
186
187 app.add_systems(
188 Update,
189 (update_camera_status_for_models,).in_set(VpeolSystemSet::UpdateCameraState),
190 );
191 app.add_systems(
192 PostUpdate, (
194 camera_3d_pan,
195 camera_3d_move_along_plane_normal,
196 camera_3d_rotate,
197 )
198 .run_if(in_state(YoleckEditorState::EditorActive)),
199 );
200 app.add_systems(
201 Update,
202 (
203 ApplyDeferred,
204 handle_clickable_children_system::<With<Mesh3d>, ()>,
205 ApplyDeferred,
206 )
207 .chain()
208 .run_if(in_state(YoleckEditorState::EditorActive)),
209 );
210 app.add_yoleck_edit_system(vpeol_3d_edit_position);
211 app.world_mut()
212 .resource_mut::<YoleckEntityCreationExclusiveSystems>()
213 .on_entity_creation(|queue| queue.push_back(vpeol_3d_init_position));
214 app.add_yoleck_edit_system(vpeol_3d_edit_third_axis_with_knob);
215 }
216}
217
218fn update_camera_status_for_models(
219 mut cameras_query: Query<(&mut VpeolCameraState, &VisibleEntities)>,
220 entities_query: Query<(Entity, &GlobalTransform, &Mesh3d)>,
221 mesh_assets: Res<Assets<Mesh>>,
222 root_resolver: VpeolRootResolver,
223) {
224 for (mut camera_state, visible_entities) in cameras_query.iter_mut() {
225 let Some(cursor_ray) = camera_state.cursor_ray else {
226 continue;
227 };
228 for (entity, global_transform, mesh) in
229 entities_query.iter_many(visible_entities.iter(TypeId::of::<Mesh3d>()))
230 {
231 let Some(mesh) = mesh_assets.get(&mesh.0) else {
232 continue;
233 };
234
235 let inverse_transform = global_transform.compute_matrix().inverse();
236
237 let ray_origin = inverse_transform.transform_point3(cursor_ray.origin);
241 let ray_vector = inverse_transform.transform_vector3(*cursor_ray.direction);
242 let Ok((ray_direction, ray_length_factor)) = Dir3::new_and_length(ray_vector) else {
243 continue;
244 };
245
246 let ray_in_object_coords = Ray3d {
247 origin: ray_origin,
248 direction: ray_direction,
249 };
250
251 let Some(distance) = ray_intersection_with_mesh(ray_in_object_coords, mesh) else {
252 continue;
253 };
254
255 let distance = distance / ray_length_factor;
256
257 let Some(root_entity) = root_resolver.resolve_root(entity) else {
258 continue;
259 };
260 camera_state.consider(root_entity, -distance, || cursor_ray.get_point(distance));
261 }
262 }
263}
264
265#[derive(Component)]
267#[cfg_attr(feature = "bevy_reflect", derive(bevy::reflect::Reflect))]
268pub struct Vpeol3dCameraControl {
269 pub plane_origin: Vec3,
271 pub plane: InfinitePlane3d,
273 pub allow_rotation_while_maintaining_up: Option<Dir3>,
278 pub proximity_per_scroll_line: f32,
281 pub proximity_per_scroll_pixel: f32,
284}
285
286impl Vpeol3dCameraControl {
287 pub fn sidescroller() -> Self {
293 Self {
294 plane_origin: Vec3::ZERO,
295 plane: InfinitePlane3d {
296 normal: Dir3::NEG_Z,
297 },
298 allow_rotation_while_maintaining_up: None,
299 proximity_per_scroll_line: 2.0,
300 proximity_per_scroll_pixel: 0.01,
301 }
302 }
303
304 pub fn topdown() -> Self {
309 Self {
310 plane_origin: Vec3::ZERO,
311 plane: InfinitePlane3d { normal: Dir3::Y },
312 allow_rotation_while_maintaining_up: Some(Dir3::Y),
313 proximity_per_scroll_line: 2.0,
314 proximity_per_scroll_pixel: 0.01,
315 }
316 }
317
318 fn ray_intersection(&self, ray: Ray3d) -> Option<Vec3> {
319 let distance = ray.intersect_plane(self.plane_origin, self.plane)?;
320 Some(ray.get_point(distance))
321 }
322}
323
324fn camera_3d_pan(
325 mut egui_context: EguiContexts,
326 mouse_buttons: Res<ButtonInput<MouseButton>>,
327 mut cameras_query: Query<(
328 Entity,
329 &mut Transform,
330 &VpeolCameraState,
331 &Vpeol3dCameraControl,
332 )>,
333 mut last_cursor_world_pos_by_camera: Local<HashMap<Entity, Vec3>>,
334) -> Result {
335 enum MouseButtonOp {
336 JustPressed,
337 BeingPressed,
338 }
339
340 let mouse_button_op = if mouse_buttons.just_pressed(MouseButton::Right) {
341 if egui_context.ctx_mut()?.is_pointer_over_area() {
342 return Ok(());
343 }
344 MouseButtonOp::JustPressed
345 } else if mouse_buttons.pressed(MouseButton::Right) {
346 MouseButtonOp::BeingPressed
347 } else {
348 last_cursor_world_pos_by_camera.clear();
349 return Ok(());
350 };
351
352 for (camera_entity, mut camera_transform, camera_state, camera_control) in
353 cameras_query.iter_mut()
354 {
355 let Some(cursor_ray) = camera_state.cursor_ray else {
356 continue;
357 };
358 match mouse_button_op {
359 MouseButtonOp::JustPressed => {
360 let Some(world_pos) = camera_control.ray_intersection(cursor_ray) else {
361 continue;
362 };
363 last_cursor_world_pos_by_camera.insert(camera_entity, world_pos);
364 }
365 MouseButtonOp::BeingPressed => {
366 if let Some(prev_pos) = last_cursor_world_pos_by_camera.get_mut(&camera_entity) {
367 let Some(world_pos) = camera_control.ray_intersection(cursor_ray) else {
368 continue;
369 };
370 let movement = *prev_pos - world_pos;
371 camera_transform.translation += movement;
372 }
373 }
374 }
375 }
376 Ok(())
377}
378
379fn camera_3d_move_along_plane_normal(
380 mut egui_context: EguiContexts,
381 mut cameras_query: Query<(&mut Transform, &Vpeol3dCameraControl)>,
382 mut wheel_events_reader: EventReader<MouseWheel>,
383) -> Result {
384 if egui_context.ctx_mut()?.is_pointer_over_area() {
385 return Ok(());
386 }
387
388 for (mut camera_transform, camera_control) in cameras_query.iter_mut() {
389 let zoom_amount: f32 = wheel_events_reader
390 .read()
391 .map(|wheel_event| match wheel_event.unit {
392 bevy::input::mouse::MouseScrollUnit::Line => {
393 wheel_event.y * camera_control.proximity_per_scroll_line
394 }
395 bevy::input::mouse::MouseScrollUnit::Pixel => {
396 wheel_event.y * camera_control.proximity_per_scroll_pixel
397 }
398 })
399 .sum();
400
401 if zoom_amount == 0.0 {
402 continue;
403 }
404
405 camera_transform.translation += zoom_amount * *camera_control.plane.normal;
406 }
407 Ok(())
408}
409
410fn camera_3d_rotate(
411 mut egui_context: EguiContexts,
412 mouse_buttons: Res<ButtonInput<MouseButton>>,
413 mut cameras_query: Query<(
414 Entity,
415 &mut Transform,
416 &VpeolCameraState,
417 &Vpeol3dCameraControl,
418 )>,
419 mut last_cursor_ray_by_camera: Local<HashMap<Entity, Ray3d>>,
420) -> Result {
421 enum MouseButtonOp {
422 JustPressed,
423 BeingPressed,
424 }
425
426 let mouse_button_op = if mouse_buttons.just_pressed(MouseButton::Middle) {
427 if egui_context.ctx_mut()?.is_pointer_over_area() {
428 return Ok(());
429 }
430 MouseButtonOp::JustPressed
431 } else if mouse_buttons.pressed(MouseButton::Middle) {
432 MouseButtonOp::BeingPressed
433 } else {
434 last_cursor_ray_by_camera.clear();
435 return Ok(());
436 };
437
438 for (camera_entity, mut camera_transform, camera_state, camera_control) in
439 cameras_query.iter_mut()
440 {
441 let Some(maintaining_up) = camera_control.allow_rotation_while_maintaining_up else {
442 continue;
443 };
444 let Some(cursor_ray) = camera_state.cursor_ray else {
445 continue;
446 };
447 match mouse_button_op {
448 MouseButtonOp::JustPressed => {
449 last_cursor_ray_by_camera.insert(camera_entity, cursor_ray);
450 }
451 MouseButtonOp::BeingPressed => {
452 if let Some(prev_ray) = last_cursor_ray_by_camera.get_mut(&camera_entity) {
453 let rotation =
454 Quat::from_rotation_arc(*cursor_ray.direction, *prev_ray.direction);
455 camera_transform.rotate(rotation);
456 let new_forward = camera_transform.forward();
457 camera_transform.look_to(*new_forward, *maintaining_up);
458 }
459 }
460 }
461 }
462 Ok(())
463}
464
465#[derive(Clone, PartialEq, Serialize, Deserialize, Component, Default, YoleckComponent)]
471#[serde(transparent)]
472#[cfg_attr(feature = "bevy_reflect", derive(bevy::reflect::Reflect))]
473pub struct Vpeol3dPosition(pub Vec3);
474
475#[derive(Component)]
482pub struct Vpeol3dThirdAxisWithKnob {
483 pub knob_distance: f32,
485 pub knob_scale: f32,
487}
488
489#[derive(Default, Clone, PartialEq, Serialize, Deserialize, Component, YoleckComponent)]
491#[serde(transparent)]
492#[cfg_attr(feature = "bevy_reflect", derive(bevy::reflect::Reflect))]
493pub struct Vpeol3dRotation(pub Quat);
494
495#[derive(Clone, PartialEq, Serialize, Deserialize, Component, YoleckComponent)]
497#[serde(transparent)]
498#[cfg_attr(feature = "bevy_reflect", derive(bevy::reflect::Reflect))]
499pub struct Vpeol3dScale(pub Vec3);
500
501impl Default for Vpeol3dScale {
502 fn default() -> Self {
503 Self(Vec3::ONE)
504 }
505}
506
507enum CommonDragPlane {
508 NotDecidedYet,
509 WithNormal(Vec3),
510 NoSharedPlane,
511}
512
513impl CommonDragPlane {
514 fn consider(&mut self, normal: Vec3) {
515 *self = match self {
516 CommonDragPlane::NotDecidedYet => CommonDragPlane::WithNormal(normal),
517 CommonDragPlane::WithNormal(current_normal) => {
518 if *current_normal == normal {
519 CommonDragPlane::WithNormal(normal)
520 } else {
521 CommonDragPlane::NoSharedPlane
522 }
523 }
524 CommonDragPlane::NoSharedPlane => CommonDragPlane::NoSharedPlane,
525 }
526 }
527
528 fn shared_normal(&self) -> Option<Vec3> {
529 if let CommonDragPlane::WithNormal(normal) = self {
530 Some(*normal)
531 } else {
532 None
533 }
534 }
535}
536
537fn vpeol_3d_edit_position(
538 mut ui: ResMut<YoleckUi>,
539 mut edit: YoleckEdit<(Entity, &mut Vpeol3dPosition, Option<&VpeolDragPlane>)>,
540 global_drag_plane: Res<VpeolDragPlane>,
541 passed_data: Res<YoleckPassedData>,
542) {
543 if edit.is_empty() || edit.has_nonmatching() {
544 return;
545 }
546 let mut average = DVec3::ZERO;
548 let mut num_entities = 0;
549 let mut transition = Vec3::ZERO;
550
551 let mut common_drag_plane = CommonDragPlane::NotDecidedYet;
552
553 for (entity, position, drag_plane) in edit.iter_matching() {
554 let VpeolDragPlane(drag_plane) = drag_plane.unwrap_or(global_drag_plane.as_ref());
555 common_drag_plane.consider(*drag_plane.normal);
556
557 if let Some(pos) = passed_data.get::<Vec3>(entity) {
558 transition = *pos - position.0;
559 }
560 average += position.0.as_dvec3();
561 num_entities += 1;
562 }
563 average /= num_entities as f64;
564
565 if common_drag_plane.shared_normal().is_none() {
566 transition = Vec3::ZERO;
567 ui.label(
568 egui::RichText::new("Drag plane differs - cannot drag together")
569 .color(egui::Color32::RED),
570 );
571 }
572 ui.horizontal(|ui| {
573 let mut new_average = average;
574 ui.add(egui::DragValue::new(&mut new_average.x).prefix("X:"));
575 ui.add(egui::DragValue::new(&mut new_average.y).prefix("Y:"));
576 ui.add(egui::DragValue::new(&mut new_average.z).prefix("Z:"));
577 transition += (new_average - average).as_vec3();
578 });
579
580 if transition.is_finite() && transition != Vec3::ZERO {
581 for (_, mut position, _) in edit.iter_matching_mut() {
582 position.0 += transition;
583 }
584 }
585}
586
587fn vpeol_3d_init_position(
588 mut egui_context: EguiContexts,
589 ui: Res<YoleckUi>,
590 mut edit: YoleckEdit<(&mut Vpeol3dPosition, Option<&VpeolDragPlane>)>,
591 global_drag_plane: Res<VpeolDragPlane>,
592 cameras_query: Query<&VpeolCameraState>,
593 mouse_buttons: Res<ButtonInput<MouseButton>>,
594) -> YoleckExclusiveSystemDirective {
595 let Ok((mut position, drag_plane)) = edit.single_mut() else {
596 return YoleckExclusiveSystemDirective::Finished;
597 };
598
599 let Some(cursor_ray) = cameras_query
600 .iter()
601 .find_map(|camera_state| camera_state.cursor_ray)
602 else {
603 return YoleckExclusiveSystemDirective::Listening;
604 };
605
606 let VpeolDragPlane(drag_plane) = drag_plane.unwrap_or(global_drag_plane.as_ref());
607 if let Some(distance_to_plane) =
608 cursor_ray.intersect_plane(position.0, InfinitePlane3d::new(*drag_plane.normal))
609 {
610 position.0 = cursor_ray.get_point(distance_to_plane);
611 };
612
613 if egui_context.ctx_mut().unwrap().is_pointer_over_area() || ui.ctx().is_pointer_over_area() {
614 return YoleckExclusiveSystemDirective::Listening;
615 }
616
617 if mouse_buttons.just_released(MouseButton::Left) {
618 return YoleckExclusiveSystemDirective::Finished;
619 }
620
621 YoleckExclusiveSystemDirective::Listening
622}
623
624fn vpeol_3d_edit_third_axis_with_knob(
625 mut edit: YoleckEdit<(
626 Entity,
627 &GlobalTransform,
628 &Vpeol3dThirdAxisWithKnob,
629 Option<&VpeolDragPlane>,
630 )>,
631 global_drag_plane: Res<VpeolDragPlane>,
632 mut knobs: YoleckKnobs,
633 mut mesh_assets: ResMut<Assets<Mesh>>,
634 mut material_assets: ResMut<Assets<StandardMaterial>>,
635 mut mesh_and_material: Local<Option<(Handle<Mesh>, Handle<StandardMaterial>)>>,
636 mut directives_writer: EventWriter<YoleckDirective>,
637) {
638 if edit.is_empty() || edit.has_nonmatching() {
639 return;
640 }
641
642 let (mesh, material) = mesh_and_material.get_or_insert_with(|| {
643 (
644 mesh_assets.add(Mesh::from(Cylinder {
645 radius: 0.5,
646 half_height: 0.5,
647 })),
648 material_assets.add(Color::from(css::ORANGE_RED)),
649 )
650 });
651
652 let mut common_drag_plane = CommonDragPlane::NotDecidedYet;
653 for (_, _, _, drag_plane) in edit.iter_matching() {
654 let VpeolDragPlane(drag_plane) = drag_plane.unwrap_or(global_drag_plane.as_ref());
655 common_drag_plane.consider(*drag_plane.normal);
656 }
657 let Some(drag_plane_normal) = common_drag_plane.shared_normal() else {
658 return;
659 };
660
661 for (entity, global_transform, third_axis_with_knob, _) in edit.iter_matching() {
662 let entity_position = global_transform.translation();
663
664 for (knob_name, drag_plane_normal) in [
665 ("vpeol-3d-third-axis-knob-positive", drag_plane_normal),
666 ("vpeol-3d-third-axis-knob-negative", -drag_plane_normal),
667 ] {
668 let mut knob = knobs.knob((entity, knob_name));
669 let knob_offset = third_axis_with_knob.knob_distance * drag_plane_normal;
670 let knob_transform = Transform {
671 translation: entity_position + knob_offset,
672 rotation: Quat::from_rotation_arc(Vec3::Y, drag_plane_normal),
673 scale: third_axis_with_knob.knob_scale * Vec3::ONE,
674 };
675 knob.cmd.insert(VpeolDragPlane(InfinitePlane3d {
676 normal: Dir3::new(drag_plane_normal.cross(Vec3::X)).unwrap_or(Dir3::Y),
677 }));
678 knob.cmd.insert((
679 Mesh3d(mesh.clone()),
680 MeshMaterial3d(material.clone()),
681 knob_transform,
682 GlobalTransform::from(knob_transform),
683 ));
684 if let Some(pos) = knob.get_passed_data::<Vec3>() {
685 let vector_from_entity = *pos - knob_offset - entity_position;
686 let along_drag_normal = vector_from_entity.dot(drag_plane_normal);
687 let vector_along_drag_normal = along_drag_normal * drag_plane_normal;
688 let position_along_drag_normal = entity_position + vector_along_drag_normal;
689 directives_writer.write(YoleckDirective::pass_to_entity(
692 entity,
693 position_along_drag_normal,
694 ));
695 }
696 }
697 }
698}
699
700fn vpeol_3d_populate_transform(
701 mut populate: YoleckPopulate<(
702 &Vpeol3dPosition,
703 Option<&Vpeol3dRotation>,
704 Option<&Vpeol3dScale>,
705 &YoleckBelongsToLevel,
706 )>,
707 levels_query: Query<&VpeolRepositionLevel>,
708) {
709 populate.populate(
710 |_ctx, mut cmd, (position, rotation, scale, belongs_to_level)| {
711 let mut transform = Transform::from_translation(position.0);
712 if let Some(Vpeol3dRotation(rotation)) = rotation {
713 transform = transform.with_rotation(*rotation);
714 }
715 if let Some(Vpeol3dScale(scale)) = scale {
716 transform = transform.with_scale(*scale);
717 }
718
719 if let Ok(VpeolRepositionLevel(level_transform)) =
720 levels_query.get(belongs_to_level.level)
721 {
722 transform = *level_transform * transform;
723 }
724
725 cmd.insert((transform, GlobalTransform::from(transform)));
726 },
727 )
728}