use crate::bevy_egui::egui;
use crate::exclusive_systems::{
YoleckEntityCreationExclusiveSystems, YoleckExclusiveSystemDirective,
};
use crate::vpeol::{
handle_clickable_children_system, ray_intersection_with_mesh, VpeolBasePlugin,
VpeolCameraState, VpeolDragPlane, VpeolRepositionLevel, VpeolRootResolver, VpeolSystemSet,
};
use crate::{prelude::*, YoleckDirective, YoleckSchedule};
use bevy::color::palettes::css;
use bevy::input::mouse::MouseWheel;
use bevy::math::DVec3;
use bevy::prelude::*;
use bevy::render::view::{VisibleEntities, WithMesh};
use bevy::utils::HashMap;
use bevy_egui::EguiContexts;
use serde::{Deserialize, Serialize};
pub struct Vpeol3dPluginForGame;
impl Plugin for Vpeol3dPluginForGame {
fn build(&self, app: &mut App) {
app.add_systems(
YoleckSchedule::OverrideCommonComponents,
vpeol_3d_populate_transform,
);
#[cfg(feature = "bevy_reflect")]
register_reflect_types(app);
}
}
#[cfg(feature = "bevy_reflect")]
fn register_reflect_types(app: &mut App) {
app.register_type::<Vpeol3dPosition>();
app.register_type::<Vpeol3dRotation>();
app.register_type::<Vpeol3dScale>();
app.register_type::<Vpeol3dCameraControl>();
}
pub struct Vpeol3dPluginForEditor {
pub drag_plane: InfinitePlane3d,
}
impl Vpeol3dPluginForEditor {
pub fn sidescroller() -> Self {
Self {
drag_plane: InfinitePlane3d { normal: Dir3::Z },
}
}
pub fn topdown() -> Self {
Self {
drag_plane: InfinitePlane3d { normal: Dir3::Y },
}
}
}
impl Plugin for Vpeol3dPluginForEditor {
fn build(&self, app: &mut App) {
app.add_plugins(VpeolBasePlugin);
app.add_plugins(Vpeol3dPluginForGame);
app.insert_resource(VpeolDragPlane(self.drag_plane));
app.add_systems(
Update,
(update_camera_status_for_models,).in_set(VpeolSystemSet::UpdateCameraState),
);
app.add_systems(
PostUpdate, (
camera_3d_pan,
camera_3d_move_along_plane_normal,
camera_3d_rotate,
)
.run_if(in_state(YoleckEditorState::EditorActive)),
);
app.add_systems(
Update,
(
apply_deferred,
handle_clickable_children_system::<With<Handle<Mesh>>, ()>,
apply_deferred,
)
.chain()
.run_if(in_state(YoleckEditorState::EditorActive)),
);
app.add_yoleck_edit_system(vpeol_3d_edit_position);
app.world_mut()
.resource_mut::<YoleckEntityCreationExclusiveSystems>()
.on_entity_creation(|queue| queue.push_back(vpeol_3d_init_position));
app.add_yoleck_edit_system(vpeol_3d_edit_third_axis_with_knob);
}
}
fn update_camera_status_for_models(
mut cameras_query: Query<(&mut VpeolCameraState, &VisibleEntities)>,
entities_query: Query<(Entity, &GlobalTransform, &Handle<Mesh>)>,
mesh_assets: Res<Assets<Mesh>>,
root_resolver: VpeolRootResolver,
) {
for (mut camera_state, visible_entities) in cameras_query.iter_mut() {
let Some(cursor_ray) = camera_state.cursor_ray else {
continue;
};
for (entity, global_transform, mesh) in
entities_query.iter_many(visible_entities.iter::<WithMesh>())
{
let Some(mesh) = mesh_assets.get(mesh) else {
continue;
};
let inverse_transform = global_transform.compute_matrix().inverse();
let ray_origin = inverse_transform.transform_point3(cursor_ray.origin);
let ray_vector = inverse_transform.transform_vector3(*cursor_ray.direction);
let Ok((ray_direction, ray_length_factor)) = Dir3::new_and_length(ray_vector) else {
continue;
};
let ray_in_object_coords = Ray3d {
origin: ray_origin,
direction: ray_direction,
};
let Some(distance) = ray_intersection_with_mesh(ray_in_object_coords, mesh) else {
continue;
};
let distance = distance / ray_length_factor;
let Some(root_entity) = root_resolver.resolve_root(entity) else {
continue;
};
camera_state.consider(root_entity, -distance, || cursor_ray.get_point(distance));
}
}
}
#[derive(Component)]
#[cfg_attr(feature = "bevy_reflect", derive(bevy::reflect::Reflect))]
pub struct Vpeol3dCameraControl {
pub plane_origin: Vec3,
pub plane: InfinitePlane3d,
pub allow_rotation_while_maintaining_up: Option<Dir3>,
pub proximity_per_scroll_line: f32,
pub proximity_per_scroll_pixel: f32,
}
impl Vpeol3dCameraControl {
pub fn sidescroller() -> Self {
Self {
plane_origin: Vec3::ZERO,
plane: InfinitePlane3d {
normal: Dir3::NEG_Z,
},
allow_rotation_while_maintaining_up: None,
proximity_per_scroll_line: 2.0,
proximity_per_scroll_pixel: 0.01,
}
}
pub fn topdown() -> Self {
Self {
plane_origin: Vec3::ZERO,
plane: InfinitePlane3d { normal: Dir3::Y },
allow_rotation_while_maintaining_up: Some(Dir3::Y),
proximity_per_scroll_line: 2.0,
proximity_per_scroll_pixel: 0.01,
}
}
fn ray_intersection(&self, ray: Ray3d) -> Option<Vec3> {
let distance = ray.intersect_plane(self.plane_origin, self.plane)?;
Some(ray.get_point(distance))
}
}
fn camera_3d_pan(
mut egui_context: EguiContexts,
mouse_buttons: Res<ButtonInput<MouseButton>>,
mut cameras_query: Query<(
Entity,
&mut Transform,
&VpeolCameraState,
&Vpeol3dCameraControl,
)>,
mut last_cursor_world_pos_by_camera: Local<HashMap<Entity, Vec3>>,
) {
enum MouseButtonOp {
JustPressed,
BeingPressed,
}
let mouse_button_op = if mouse_buttons.just_pressed(MouseButton::Right) {
if egui_context.ctx_mut().is_pointer_over_area() {
return;
}
MouseButtonOp::JustPressed
} else if mouse_buttons.pressed(MouseButton::Right) {
MouseButtonOp::BeingPressed
} else {
last_cursor_world_pos_by_camera.clear();
return;
};
for (camera_entity, mut camera_transform, camera_state, camera_control) in
cameras_query.iter_mut()
{
let Some(cursor_ray) = camera_state.cursor_ray else {
continue;
};
match mouse_button_op {
MouseButtonOp::JustPressed => {
let Some(world_pos) = camera_control.ray_intersection(cursor_ray) else {
continue;
};
last_cursor_world_pos_by_camera.insert(camera_entity, world_pos);
}
MouseButtonOp::BeingPressed => {
if let Some(prev_pos) = last_cursor_world_pos_by_camera.get_mut(&camera_entity) {
let Some(world_pos) = camera_control.ray_intersection(cursor_ray) else {
continue;
};
let movement = *prev_pos - world_pos;
camera_transform.translation += movement;
}
}
}
}
}
fn camera_3d_move_along_plane_normal(
mut egui_context: EguiContexts,
mut cameras_query: Query<(&mut Transform, &Vpeol3dCameraControl)>,
mut wheel_events_reader: EventReader<MouseWheel>,
) {
if egui_context.ctx_mut().is_pointer_over_area() {
return;
}
for (mut camera_transform, camera_control) in cameras_query.iter_mut() {
let zoom_amount: f32 = wheel_events_reader
.read()
.map(|wheel_event| match wheel_event.unit {
bevy::input::mouse::MouseScrollUnit::Line => {
wheel_event.y * camera_control.proximity_per_scroll_line
}
bevy::input::mouse::MouseScrollUnit::Pixel => {
wheel_event.y * camera_control.proximity_per_scroll_pixel
}
})
.sum();
if zoom_amount == 0.0 {
continue;
}
camera_transform.translation += zoom_amount * *camera_control.plane.normal;
}
}
fn camera_3d_rotate(
mut egui_context: EguiContexts,
mouse_buttons: Res<ButtonInput<MouseButton>>,
mut cameras_query: Query<(
Entity,
&mut Transform,
&VpeolCameraState,
&Vpeol3dCameraControl,
)>,
mut last_cursor_ray_by_camera: Local<HashMap<Entity, Ray3d>>,
) {
enum MouseButtonOp {
JustPressed,
BeingPressed,
}
let mouse_button_op = if mouse_buttons.just_pressed(MouseButton::Middle) {
if egui_context.ctx_mut().is_pointer_over_area() {
return;
}
MouseButtonOp::JustPressed
} else if mouse_buttons.pressed(MouseButton::Middle) {
MouseButtonOp::BeingPressed
} else {
last_cursor_ray_by_camera.clear();
return;
};
for (camera_entity, mut camera_transform, camera_state, camera_control) in
cameras_query.iter_mut()
{
let Some(maintaining_up) = camera_control.allow_rotation_while_maintaining_up else {
continue;
};
let Some(cursor_ray) = camera_state.cursor_ray else {
continue;
};
match mouse_button_op {
MouseButtonOp::JustPressed => {
last_cursor_ray_by_camera.insert(camera_entity, cursor_ray);
}
MouseButtonOp::BeingPressed => {
if let Some(prev_ray) = last_cursor_ray_by_camera.get_mut(&camera_entity) {
let rotation =
Quat::from_rotation_arc(*cursor_ray.direction, *prev_ray.direction);
camera_transform.rotate(rotation);
let new_forward = camera_transform.forward();
camera_transform.look_to(*new_forward, *maintaining_up);
}
}
}
}
}
#[derive(Clone, PartialEq, Serialize, Deserialize, Component, Default, YoleckComponent)]
#[serde(transparent)]
#[cfg_attr(feature = "bevy_reflect", derive(bevy::reflect::Reflect))]
pub struct Vpeol3dPosition(pub Vec3);
#[derive(Component)]
pub struct Vpeol3dThirdAxisWithKnob {
pub knob_distance: f32,
pub knob_scale: f32,
}
#[derive(Default, Clone, PartialEq, Serialize, Deserialize, Component, YoleckComponent)]
#[serde(transparent)]
#[cfg_attr(feature = "bevy_reflect", derive(bevy::reflect::Reflect))]
pub struct Vpeol3dRotation(pub Quat);
#[derive(Clone, PartialEq, Serialize, Deserialize, Component, YoleckComponent)]
#[serde(transparent)]
#[cfg_attr(feature = "bevy_reflect", derive(bevy::reflect::Reflect))]
pub struct Vpeol3dScale(pub Vec3);
impl Default for Vpeol3dScale {
fn default() -> Self {
Self(Vec3::ONE)
}
}
enum CommonDragPlane {
NotDecidedYet,
WithNormal(Vec3),
NoSharedPlane,
}
impl CommonDragPlane {
fn consider(&mut self, normal: Vec3) {
*self = match self {
CommonDragPlane::NotDecidedYet => CommonDragPlane::WithNormal(normal),
CommonDragPlane::WithNormal(current_normal) => {
if *current_normal == normal {
CommonDragPlane::WithNormal(normal)
} else {
CommonDragPlane::NoSharedPlane
}
}
CommonDragPlane::NoSharedPlane => CommonDragPlane::NoSharedPlane,
}
}
fn shared_normal(&self) -> Option<Vec3> {
if let CommonDragPlane::WithNormal(normal) = self {
Some(*normal)
} else {
None
}
}
}
fn vpeol_3d_edit_position(
mut ui: ResMut<YoleckUi>,
mut edit: YoleckEdit<(Entity, &mut Vpeol3dPosition, Option<&VpeolDragPlane>)>,
global_drag_plane: Res<VpeolDragPlane>,
passed_data: Res<YoleckPassedData>,
) {
if edit.is_empty() || edit.has_nonmatching() {
return;
}
let mut average = DVec3::ZERO;
let mut num_entities = 0;
let mut transition = Vec3::ZERO;
let mut common_drag_plane = CommonDragPlane::NotDecidedYet;
for (entity, position, drag_plane) in edit.iter_matching() {
let VpeolDragPlane(drag_plane) = drag_plane.unwrap_or(global_drag_plane.as_ref());
common_drag_plane.consider(*drag_plane.normal);
if let Some(pos) = passed_data.get::<Vec3>(entity) {
transition = *pos - position.0;
}
average += position.0.as_dvec3();
num_entities += 1;
}
average /= num_entities as f64;
if common_drag_plane.shared_normal().is_none() {
transition = Vec3::ZERO;
ui.label(
egui::RichText::new("Drag plane differs - cannot drag together")
.color(egui::Color32::RED),
);
}
ui.horizontal(|ui| {
let mut new_average = average;
ui.add(egui::DragValue::new(&mut new_average.x).prefix("X:"));
ui.add(egui::DragValue::new(&mut new_average.y).prefix("Y:"));
ui.add(egui::DragValue::new(&mut new_average.z).prefix("Z:"));
transition += (new_average - average).as_vec3();
});
if transition.is_finite() && transition != Vec3::ZERO {
for (_, mut position, _) in edit.iter_matching_mut() {
position.0 += transition;
}
}
}
fn vpeol_3d_init_position(
mut egui_context: EguiContexts,
ui: Res<YoleckUi>,
mut edit: YoleckEdit<(&mut Vpeol3dPosition, Option<&VpeolDragPlane>)>,
global_drag_plane: Res<VpeolDragPlane>,
cameras_query: Query<&VpeolCameraState>,
mouse_buttons: Res<ButtonInput<MouseButton>>,
) -> YoleckExclusiveSystemDirective {
let Ok((mut position, drag_plane)) = edit.get_single_mut() else {
return YoleckExclusiveSystemDirective::Finished;
};
let Some(cursor_ray) = cameras_query
.iter()
.find_map(|camera_state| camera_state.cursor_ray)
else {
return YoleckExclusiveSystemDirective::Listening;
};
let VpeolDragPlane(drag_plane) = drag_plane.unwrap_or(global_drag_plane.as_ref());
if let Some(distance_to_plane) =
cursor_ray.intersect_plane(position.0, InfinitePlane3d::new(*drag_plane.normal))
{
position.0 = cursor_ray.get_point(distance_to_plane);
};
if egui_context.ctx_mut().is_pointer_over_area() || ui.ctx().is_pointer_over_area() {
return YoleckExclusiveSystemDirective::Listening;
}
if mouse_buttons.just_released(MouseButton::Left) {
return YoleckExclusiveSystemDirective::Finished;
}
YoleckExclusiveSystemDirective::Listening
}
fn vpeol_3d_edit_third_axis_with_knob(
mut edit: YoleckEdit<(
Entity,
&GlobalTransform,
&Vpeol3dThirdAxisWithKnob,
Option<&VpeolDragPlane>,
)>,
global_drag_plane: Res<VpeolDragPlane>,
mut knobs: YoleckKnobs,
mut mesh_assets: ResMut<Assets<Mesh>>,
mut material_assets: ResMut<Assets<StandardMaterial>>,
mut mesh_and_material: Local<Option<(Handle<Mesh>, Handle<StandardMaterial>)>>,
mut directives_writer: EventWriter<YoleckDirective>,
) {
if edit.is_empty() || edit.has_nonmatching() {
return;
}
let (mesh, material) = mesh_and_material.get_or_insert_with(|| {
(
mesh_assets.add(Mesh::from(Cylinder {
radius: 0.5,
half_height: 0.5,
})),
material_assets.add(Color::from(css::ORANGE_RED)),
)
});
let mut common_drag_plane = CommonDragPlane::NotDecidedYet;
for (_, _, _, drag_plane) in edit.iter_matching() {
let VpeolDragPlane(drag_plane) = drag_plane.unwrap_or(global_drag_plane.as_ref());
common_drag_plane.consider(*drag_plane.normal);
}
let Some(drag_plane_normal) = common_drag_plane.shared_normal() else {
return;
};
for (entity, global_transform, third_axis_with_knob, _) in edit.iter_matching() {
let entity_position = global_transform.translation();
for (knob_name, drag_plane_normal) in [
("vpeol-3d-third-axis-knob-positive", drag_plane_normal),
("vpeol-3d-third-axis-knob-negative", -drag_plane_normal),
] {
let mut knob = knobs.knob((entity, knob_name));
let knob_offset = third_axis_with_knob.knob_distance * drag_plane_normal;
let knob_transform = Transform {
translation: entity_position + knob_offset,
rotation: Quat::from_rotation_arc(Vec3::Y, drag_plane_normal),
scale: third_axis_with_knob.knob_scale * Vec3::ONE,
};
knob.cmd.insert(VpeolDragPlane(InfinitePlane3d {
normal: Dir3::new(drag_plane_normal.cross(Vec3::X)).unwrap_or(Dir3::Y),
}));
knob.cmd.insert(PbrBundle {
mesh: mesh.clone(),
material: material.clone(),
transform: knob_transform,
global_transform: knob_transform.into(),
..Default::default()
});
if let Some(pos) = knob.get_passed_data::<Vec3>() {
let vector_from_entity = *pos - knob_offset - entity_position;
let along_drag_normal = vector_from_entity.dot(drag_plane_normal);
let vector_along_drag_normal = along_drag_normal * drag_plane_normal;
let position_along_drag_normal = entity_position + vector_along_drag_normal;
directives_writer.send(YoleckDirective::pass_to_entity(
entity,
position_along_drag_normal,
));
}
}
}
}
fn vpeol_3d_populate_transform(
mut populate: YoleckPopulate<(
&Vpeol3dPosition,
Option<&Vpeol3dRotation>,
Option<&Vpeol3dScale>,
&YoleckBelongsToLevel,
)>,
levels_query: Query<&VpeolRepositionLevel>,
) {
populate.populate(
|_ctx, mut cmd, (position, rotation, scale, belongs_to_level)| {
let mut transform = Transform::from_translation(position.0);
if let Some(Vpeol3dRotation(rotation)) = rotation {
transform = transform.with_rotation(*rotation);
}
if let Some(Vpeol3dScale(scale)) = scale {
transform = transform.with_scale(*scale);
}
if let Ok(VpeolRepositionLevel(level_transform)) =
levels_query.get(belongs_to_level.level)
{
transform = *level_transform * transform;
}
cmd.insert(TransformBundle {
local: transform,
global: transform.into(),
});
},
)
}