use bevy::camera::Camera;
use bevy::ecs::message::MessageCursor;
use bevy::picking::backend::{HitData, PointerHits};
use bevy::picking::pointer::{PointerId, PointerLocation};
use bevy::platform::collections::HashMap;
use bevy::prelude::*;
use bevy::ui::{ComputedNode, UiGlobalTransform};
use tiny_skia::Transform;
use super::hit::hit_shape;
use super::paint::view_box_transform;
use super::walk::{ShapeQuery, walk_shapes};
use super::{SvgShape, SvgSurface, ViewBox};
#[cfg(test)]
mod tests;
const HALF_DEPTH_STEP: f32 = 0.000_005;
#[derive(Resource, Default, Debug)]
pub(crate) struct SvgPointerShapeHits {
pub hits: HashMap<PointerId, SvgShapeHit>,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct SvgShapeHit {
pub root: Entity,
pub shape: Entity,
pub user_pos: Vec2,
pub(crate) depth: f32,
}
pub(crate) fn refine_svg_pointer_hits(
mut messages: ResMut<Messages<PointerHits>>,
mut cursor: Local<MessageCursor<PointerHits>>,
mut shape_hits: ResMut<SvgPointerShapeHits>,
pointers: Query<(&PointerId, &PointerLocation)>,
cameras: Query<&Camera>,
roots: Query<(&SvgSurface, &ComputedNode, &UiGlobalTransform, &Children)>,
shapes: ShapeQuery,
) {
shape_hits.hits.clear();
let mut seen: Vec<(PointerId, Entity)> = Vec::new();
let mut refined: Vec<PointerHits> = Vec::new();
for msg in cursor.read(&messages) {
for (entity, data) in &msg.picks {
let Ok((surface, node, transform, children)) = roots.get(*entity) else {
continue; };
if surface.doc.is_some() {
continue; }
if seen.contains(&(msg.pointer, *entity)) {
continue;
}
seen.push((msg.pointer, *entity));
let Some(location) = pointers
.iter()
.find(|(id, _)| **id == msg.pointer)
.and_then(|(_, loc)| loc.location().cloned())
else {
continue;
};
let Ok(camera) = cameras.get(data.camera) else {
continue;
};
let physical = crate::pick_clip::pointer_physical_position(&location, camera);
let Some(normalized) = node.normalize_point(*transform, physical) else {
continue;
};
let local = (normalized + Vec2::splat(0.5)) * node.size;
let scale_factor = super::node_scale_factor(node);
let Some(user_pos) =
cursor_to_user_space(surface.view_box.as_ref(), node.size, scale_factor, local)
else {
continue;
};
let mut ordered: Vec<(Entity, Transform)> = Vec::new();
walk_shapes(
children,
&shapes,
Transform::identity(),
1.0,
&mut |shape_entity, _, t, _| ordered.push((shape_entity, t)),
);
ordered.reverse();
let Some(shape) =
refine_hit(&ordered, |e| shapes.get(e).ok().map(|(s, _)| s), user_pos)
else {
continue; };
let depth = data.depth - HALF_DEPTH_STEP;
let candidate = SvgShapeHit {
root: *entity,
shape,
user_pos,
depth,
};
shape_hits
.hits
.entry(msg.pointer)
.and_modify(|current| {
if depth < current.depth {
*current = candidate;
}
})
.or_insert(candidate);
refined.push(PointerHits::new(
msg.pointer,
vec![(shape, HitData::new(data.camera, depth, None, None))],
msg.order,
));
}
}
for msg in refined {
messages.write(msg);
}
}
pub(crate) fn cursor_to_user_space(
view_box: Option<&ViewBox>,
node_size: Vec2,
scale_factor: f32,
local_physical: Vec2,
) -> Option<Vec2> {
let (w, h) = crate::canvas::clamp_physical_size(node_size);
if w == 0 || h == 0 {
return None;
}
view_box_transform(view_box, w, h, scale_factor)
.invert()
.map(|inverse| map_point(inverse, local_physical))
}
pub(crate) fn refine_hit<'a>(
shapes_topmost_first: &[(Entity, Transform)],
lookup: impl Fn(Entity) -> Option<&'a SvgShape>,
user_pos: Vec2,
) -> Option<Entity> {
shapes_topmost_first
.iter()
.find_map(|&(entity, transform)| {
let shape = lookup(entity)?;
let inverse = transform.invert()?; hit_shape(shape.kind, &shape.attrs, map_point(inverse, user_pos)).then_some(entity)
})
}
pub(super) fn map_point(t: Transform, p: Vec2) -> Vec2 {
let mut pt = tiny_skia::Point::from_xy(p.x, p.y);
t.map_point(&mut pt);
Vec2::new(pt.x, pt.y)
}