use bevy::camera::{Camera, ImageRenderTarget, NormalizedRenderTarget};
use bevy::ecs::system::RunSystemOnce;
use bevy::picking::backend::{HitData, PointerHits};
use bevy::picking::pointer::{Location, PointerId, PointerLocation};
use bevy::prelude::*;
use bevy::ui::{ComputedNode, UiGlobalTransform};
use tiny_skia::Transform;
use super::{
HALF_DEPTH_STEP, SvgPointerShapeHits, cursor_to_user_space, refine_hit, refine_svg_pointer_hits,
};
use crate::svg::{ShapeAttrs, ShapeKind, SvgShape, SvgSurface, ViewBox, st};
fn circle(cx: f32, cy: f32, r: f32) -> SvgShape {
SvgShape {
kind: ShapeKind::Circle,
attrs: ShapeAttrs {
cx: st(cx),
cy: st(cy),
r: st(r),
..Default::default()
},
}
}
fn rect(x: f32, y: f32, w: f32, h: f32) -> SvgShape {
SvgShape {
kind: ShapeKind::Rect,
attrs: ShapeAttrs {
x: st(x),
y: st(y),
width: st(w),
height: st(h),
..Default::default()
},
}
}
fn ids(world: &mut World, n: usize) -> Vec<Entity> {
(0..n).map(|_| world.spawn_empty().id()).collect()
}
#[test]
fn refine_hit_topmost_wins_on_overlap() {
let mut world = World::new();
let e = ids(&mut world, 2);
let (top, bottom) = (circle(50.0, 50.0, 40.0), rect(0.0, 0.0, 100.0, 100.0));
let list = [(e[0], Transform::identity()), (e[1], Transform::identity())];
let lookup = |id: Entity| {
if id == e[0] {
Some(&top)
} else if id == e[1] {
Some(&bottom)
} else {
None
}
};
assert_eq!(
refine_hit(&list, lookup, Vec2::new(50.0, 50.0)),
Some(e[0]),
"both shapes contain the point; the topmost (first) must win"
);
assert_eq!(
refine_hit(&list, lookup, Vec2::new(2.0, 2.0)),
Some(e[1]),
"inside the rect but outside the circle: falls to the shape beneath"
);
}
#[test]
fn refine_hit_miss_falls_through() {
let mut world = World::new();
let e = ids(&mut world, 1);
let shape = circle(50.0, 50.0, 10.0);
let list = [(e[0], Transform::identity())];
assert_eq!(
refine_hit(&list, |_| Some(&shape), Vec2::new(90.0, 90.0)),
None
);
}
#[test]
fn refine_hit_inverts_composed_group_transform() {
let mut world = World::new();
let e = ids(&mut world, 1);
let shape = circle(0.0, 0.0, 5.0);
let list = [(e[0], Transform::from_translate(10.0, 0.0))];
assert_eq!(
refine_hit(&list, |_| Some(&shape), Vec2::new(10.0, 0.0)),
Some(e[0]),
"the on-screen (translated) position must hit"
);
assert_eq!(
refine_hit(&list, |_| Some(&shape), Vec2::new(0.0, 0.0)),
None,
"the untranslated position must miss"
);
}
#[test]
fn refine_hit_skips_non_invertible_transforms() {
let mut world = World::new();
let e = ids(&mut world, 2);
let degenerate = circle(50.0, 50.0, 40.0);
let under = rect(0.0, 0.0, 100.0, 100.0);
let list = [
(e[0], Transform::from_scale(0.0, 0.0)),
(e[1], Transform::identity()),
];
let lookup = |id: Entity| {
if id == e[0] {
Some(°enerate)
} else {
Some(&under)
}
};
assert_eq!(
refine_hit(&list, lookup, Vec2::new(50.0, 50.0)),
Some(e[1]),
"the zero-scaled shape is skipped; the rect beneath wins"
);
}
#[test]
fn cursor_maps_through_view_box_min_offset() {
let vb = ViewBox {
min: Vec2::new(10.0, 20.0),
size: Vec2::new(100.0, 100.0),
};
assert_eq!(
cursor_to_user_space(Some(&vb), Vec2::splat(100.0), 1.0, Vec2::new(30.0, 30.0)),
Some(Vec2::new(40.0, 50.0))
);
}
#[test]
fn cursor_maps_to_logical_px_without_view_box_at_dpr2() {
assert_eq!(
cursor_to_user_space(None, Vec2::splat(80.0), 2.0, Vec2::new(40.0, 40.0)),
Some(Vec2::new(20.0, 20.0))
);
}
const POINTER: PointerId = PointerId::Custom(uuid::Uuid::from_u128(0x51C5));
fn world_with_pointer(position: Vec2) -> (World, Entity) {
let mut world = World::new();
world.init_resource::<Messages<PointerHits>>();
world.init_resource::<SvgPointerShapeHits>();
let camera = world.spawn(Camera::default()).id();
world.spawn((
POINTER,
PointerLocation::new(Location {
target: NormalizedRenderTarget::Image(ImageRenderTarget {
handle: Handle::default(),
scale_factor: 1.0,
}),
position,
}),
));
(world, camera)
}
fn spawn_svg_tree(world: &mut World, surface: SvgSurface) -> (Entity, Entity, Entity) {
let root = world
.spawn((
surface,
ComputedNode {
size: Vec2::splat(100.0),
..Default::default()
},
UiGlobalTransform::from_translation(Vec2::splat(50.0)),
))
.id();
let bottom = world
.spawn((rect(0.0, 0.0, 50.0, 50.0), ChildOf(root)))
.id();
let top = world.spawn((circle(30.0, 30.0, 10.0), ChildOf(root))).id();
(root, bottom, top)
}
const ROOT_DEPTH: f32 = 0.00002;
fn send_root_hit(world: &mut World, camera: Entity, root: Entity) {
send_root_hit_at(world, camera, root, ROOT_DEPTH);
}
fn send_root_hit_at(world: &mut World, camera: Entity, root: Entity, depth: f32) {
world
.resource_mut::<Messages<PointerHits>>()
.write(PointerHits::new(
POINTER,
vec![(root, HitData::new(camera, depth, None, None))],
0.5,
));
}
fn drain_hits(world: &mut World) -> Vec<PointerHits> {
world
.resource_mut::<Messages<PointerHits>>()
.drain()
.collect()
}
#[test]
fn system_refines_root_hit_to_topmost_shape() {
let (mut world, camera) = world_with_pointer(Vec2::new(30.0, 30.0));
let (root, bottom, top) = spawn_svg_tree(&mut world, SvgSurface::jsx(None));
send_root_hit(&mut world, camera, root);
world.run_system_once(refine_svg_pointer_hits).unwrap();
let all = drain_hits(&mut world);
assert_eq!(
all.len(),
2,
"the root's message plus exactly one refinement"
);
let refined: Vec<&PointerHits> = all
.iter()
.filter(|m| m.picks.iter().any(|(e, _)| *e != root))
.collect();
assert_eq!(refined.len(), 1);
let msg = refined[0];
assert_eq!(msg.pointer, POINTER);
assert_eq!(msg.picks.len(), 1);
let (entity, data) = &msg.picks[0];
assert_eq!(
*entity, top,
"the circle overlaps the rect at (30,30); topmost wins (not {bottom:?})"
);
assert_eq!(data.camera, camera, "same camera as the root's entry");
assert_eq!(
data.depth,
ROOT_DEPTH - HALF_DEPTH_STEP,
"half a step above the svg node, below anything above it"
);
let hits = world.resource::<SvgPointerShapeHits>();
let hit = hits.hits.get(&POINTER).expect("handoff entry written");
assert_eq!(hit.shape, top);
assert_eq!(hit.root, root);
assert!(
hit.user_pos.distance(Vec2::new(30.0, 30.0)) < 1e-3,
"no viewBox at DPR 1: user space == node-local logical px (float \
noise from the affine inverse aside); got {:?}",
hit.user_pos
);
}
#[test]
fn system_miss_writes_nothing() {
let (mut world, camera) = world_with_pointer(Vec2::new(90.0, 90.0));
let (root, _, _) = spawn_svg_tree(&mut world, SvgSurface::jsx(None));
send_root_hit(&mut world, camera, root);
world.run_system_once(refine_svg_pointer_hits).unwrap();
let all = drain_hits(&mut world);
assert_eq!(all.len(), 1, "only the root's original message survives");
assert!(world.resource::<SvgPointerShapeHits>().hits.is_empty());
}
#[test]
fn system_ignores_file_mode_and_non_svg_entities() {
let (mut world, camera) = world_with_pointer(Vec2::new(30.0, 30.0));
let (file_root, _, _) = spawn_svg_tree(&mut world, SvgSurface::new(Handle::default()));
let plain = world.spawn(ComputedNode::default()).id();
send_root_hit(&mut world, camera, file_root);
send_root_hit(&mut world, camera, plain);
world.run_system_once(refine_svg_pointer_hits).unwrap();
let all = drain_hits(&mut world);
assert_eq!(all.len(), 2, "no refinement for either entry");
assert!(world.resource::<SvgPointerShapeHits>().hits.is_empty());
}
#[test]
fn system_keeps_topmost_root_in_handoff_across_overlapping_roots() {
for deeper_first in [true, false] {
let (mut world, camera) = world_with_pointer(Vec2::new(30.0, 30.0));
let (deep_root, _, deep_top) = spawn_svg_tree(&mut world, SvgSurface::jsx(None));
let (top_root, _, top_top) = spawn_svg_tree(&mut world, SvgSurface::jsx(None));
let order = if deeper_first {
[(deep_root, ROOT_DEPTH), (top_root, 0.0)]
} else {
[(top_root, 0.0), (deep_root, ROOT_DEPTH)]
};
for (root, depth) in order {
send_root_hit_at(&mut world, camera, root, depth);
}
world.run_system_once(refine_svg_pointer_hits).unwrap();
let refined: Vec<PointerHits> = drain_hits(&mut world)
.into_iter()
.filter(|m| m.picks.iter().any(|(e, _)| *e == deep_top || *e == top_top))
.collect();
assert_eq!(
refined.len(),
2,
"each root's entry refines to its own shape (deeper_first={deeper_first})"
);
let hits = world.resource::<SvgPointerShapeHits>();
let hit = hits.hits.get(&POINTER).expect("handoff entry written");
assert_eq!(
hit.root, top_root,
"the topmost (smallest-depth) root wins the handoff (deeper_first={deeper_first})"
);
assert_eq!(hit.shape, top_top);
}
}
#[test]
fn system_dedupes_repeated_entries_for_one_pointer() {
let (mut world, camera) = world_with_pointer(Vec2::new(30.0, 30.0));
let (root, _, top) = spawn_svg_tree(&mut world, SvgSurface::jsx(None));
send_root_hit(&mut world, camera, root);
send_root_hit(&mut world, camera, root);
world.run_system_once(refine_svg_pointer_hits).unwrap();
let refined: Vec<PointerHits> = drain_hits(&mut world)
.into_iter()
.filter(|m| m.picks.iter().any(|(e, _)| *e == top))
.collect();
assert_eq!(
refined.len(),
1,
"two backend messages for one pointer must yield ONE refinement"
);
}