use std::sync::Arc;
use bevy::{picking::PickingSystems, picking::backend::prelude::*, prelude::*};
use crate::SmudShape;
#[derive(Debug, Clone, Copy)]
pub struct SdfInput {
pub pos: Vec2,
pub bounds: Vec2,
pub params: Vec4,
}
#[derive(Debug, Clone, Default, Component, Reflect)]
#[reflect(Debug, Default, Component)]
pub struct SmudPickingCamera;
#[derive(Clone, Component, Reflect)]
#[reflect(opaque, Component)]
pub struct SmudPickingShape {
pub distance_fn: Arc<dyn Fn(SdfInput) -> f32 + Send + Sync>,
}
impl SmudPickingShape {
pub fn new<F>(distance_fn: F) -> Self
where
F: Fn(Vec2) -> f32 + Send + Sync + 'static,
{
Self::with_input(move |input| distance_fn(input.pos))
}
pub fn with_input<F>(distance_fn: F) -> Self
where
F: Fn(SdfInput) -> f32 + Send + Sync + 'static,
{
Self {
distance_fn: Arc::new(distance_fn),
}
}
}
#[derive(Resource, Reflect, Default)]
#[reflect(Resource, Default)]
pub struct SmudPickingSettings {
pub require_markers: bool,
}
#[derive(Clone)]
pub struct SmudPickingPlugin;
impl Plugin for SmudPickingPlugin {
fn build(&self, app: &mut App) {
app.register_type::<SmudPickingCamera>()
.register_type::<SmudPickingShape>()
.register_type::<SmudPickingSettings>()
.init_resource::<SmudPickingSettings>()
.add_systems(PreUpdate, smud_picking.in_set(PickingSystems::Backend));
}
}
#[allow(clippy::type_complexity)]
pub fn smud_picking(
ray_map: Res<RayMap>,
cameras: Query<(Entity, &Camera, &GlobalTransform, Has<SmudPickingCamera>)>,
settings: Res<SmudPickingSettings>,
shapes: Query<(
Entity,
&SmudShape,
&GlobalTransform,
&ViewVisibility,
Option<&Pickable>,
Option<&SmudPickingShape>,
)>,
mut output: MessageWriter<PointerHits>,
) {
let mut sorted_shapes: Vec<_> = shapes
.iter()
.filter_map(
|(entity, shape, transform, visibility, pickable, sdf_shape)| {
if !visibility.get() || transform.affine().is_nan() {
return None;
}
if settings.require_markers && pickable.is_none() {
return None;
}
if let Some(pickable) = pickable
&& !pickable.is_hoverable
{
return None;
}
Some((entity, shape, transform, pickable, sdf_shape))
},
)
.collect();
sorted_shapes.sort_by(|(_, _, transform_a, _, _), (_, _, transform_b, _, _)| {
transform_b
.translation()
.z
.total_cmp(&transform_a.translation().z)
});
for (&ray_id, &ray) in ray_map.map.iter() {
let (camera_entity, pointer) = (ray_id.camera, ray_id.pointer);
let Ok((cam_entity, camera, cam_transform, cam_can_pick)) = cameras.get(camera_entity)
else {
continue;
};
let marker_requirement = !settings.require_markers || cam_can_pick;
if !camera.is_active || !marker_requirement {
continue;
}
let mut picks = Vec::new();
let mut blocked = false;
for (entity, shape, shape_transform, pickable, sdf_picking) in &sorted_shapes {
if blocked {
break;
}
let shape_z = shape_transform.translation().z;
let ray_start = ray.origin;
let ray_direction = ray.direction;
if ray_direction.z.abs() < f32::EPSILON {
continue;
}
let t = (shape_z - ray_start.z) / ray_direction.z;
let intersection_point = ray_start + *ray_direction * t;
let world_to_shape = shape_transform.affine().inverse();
let local_point = world_to_shape.transform_point3(intersection_point);
let is_hit = if let Some(sdf_shape) = sdf_picking {
let sdf_input = SdfInput {
pos: Vec2::new(local_point.x, local_point.y),
bounds: shape.bounds.half_size,
params: shape.params,
};
let distance = (sdf_shape.distance_fn)(sdf_input);
distance <= 0.0 } else {
let half_size = shape.bounds.half_size;
local_point.x.abs() <= half_size.x && local_point.y.abs() <= half_size.y
};
if is_hit {
let hit_pos_cam = cam_transform
.affine()
.inverse()
.transform_point3(intersection_point);
let depth = -hit_pos_cam.z;
picks.push((
*entity,
HitData::new(
cam_entity,
depth,
Some(intersection_point),
Some(*shape_transform.back()),
),
));
if let Some(pickable) = pickable {
blocked = pickable.should_block_lower;
} else {
blocked = true;
}
}
}
if !picks.is_empty() {
let order = camera.order as f32;
output.write(PointerHits::new(pointer, picks, order));
}
}
}