use bevy::math::{Rect, UVec2, Vec2};
use bevy::prelude::*;
use bevy::render::MainWorld;
use bevy::ui::CalculatedClip;
use crate::layer::clip::LayerClips;
#[derive(Resource, Default)]
pub struct SwappedClips(pub Vec<(Entity, CalculatedClip)>);
pub fn swap_interior_clips_in(mut main_world: ResMut<MainWorld>, mut stash: ResMut<SwappedClips>) {
swap_in(&mut main_world, &mut stash.0);
}
pub fn swap_interior_clips_out(mut main_world: ResMut<MainWorld>, mut stash: ResMut<SwappedClips>) {
swap_out(&mut main_world, &mut stash.0);
}
pub fn swap_in(world: &mut World, stash: &mut Vec<(Entity, CalculatedClip)>) {
stash.clear();
if world
.get_resource::<LayerClips>()
.is_none_or(|clips| clips.strip.is_empty())
{
return;
}
world.resource_scope(|world, clips: Mut<LayerClips>| {
for (&entity, &strip) in &clips.strip {
let Ok(mut e) = world.get_entity_mut(entity) else {
continue;
};
let Some(mut clip) = e.get_mut::<CalculatedClip>() else {
continue;
};
let Some(rects) = clip.rects() else {
continue;
};
let mut interior = CalculatedClip::default();
for rect in &rects[strip.min(rects.len())..] {
interior.push_rect(rect.rect, rect.world_to_clip_local);
}
let original = std::mem::replace(clip.bypass_change_detection(), interior);
stash.push((entity, original));
}
});
}
pub fn swap_out(world: &mut World, stash: &mut Vec<(Entity, CalculatedClip)>) {
for (entity, original) in stash.drain(..) {
if let Ok(mut e) = world.get_entity_mut(entity)
&& let Some(mut clip) = e.get_mut::<CalculatedClip>()
{
*clip.bypass_change_detection() = original;
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ClippedQuad {
pub pos_min: Vec2,
pub pos_max: Vec2,
pub uv_min: Vec2,
pub uv_max: Vec2,
}
pub fn clip_quad(min: Vec2, size: UVec2, inset: f32, clip: Option<Rect>) -> Option<ClippedQuad> {
let size = size.as_vec2();
let (box_min, box_max) = (min + inset, min + size - inset);
let (pos_min, pos_max) = match clip {
None => (box_min, box_max),
Some(c) => (box_min.max(c.min), box_max.min(c.max)),
};
if pos_min.x >= pos_max.x || pos_min.y >= pos_max.y {
return None;
}
let uv_size = size.max(Vec2::ONE);
Some(ClippedQuad {
pos_min,
pos_max,
uv_min: (pos_min - min) / uv_size,
uv_max: (pos_max - min) / uv_size,
})
}
pub fn clip_quad_rotated(
min: Vec2,
size: UVec2,
inset: f32,
clip: &CalculatedClip,
mut push: impl FnMut(Vec2, Vec2),
) -> u32 {
let Some(q) = clip_quad(min, size, inset, None) else {
return 0;
};
let corners = [
q.pos_min,
Vec2::new(q.pos_max.x, q.pos_min.y),
q.pos_max,
Vec2::new(q.pos_min.x, q.pos_max.y),
]
.map(|corner| (corner, ()));
let polygon = bevy::ui_render::clipping::clip_polygon(Some(clip), &corners, |_, _, _| ());
let uv_size = size.as_vec2().max(Vec2::ONE);
let triangles = polygon.len().saturating_sub(2);
for i in 1..=triangles {
for (position, ()) in [polygon[0], polygon[i], polygon[i + 1]] {
push(position, (position - min) / uv_size);
}
}
3 * triangles as u32
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn clip_quad_table() {
let min = Vec2::new(10.0, 20.0);
let size = UVec2::new(100, 50);
let full = clip_quad(min, size, 0.0, None).expect("unclipped quad");
assert_eq!(full.pos_min, min);
assert_eq!(full.pos_max, Vec2::new(110.0, 70.0));
assert_eq!(full.uv_min, Vec2::ZERO);
assert_eq!(full.uv_max, Vec2::ONE);
let q = clip_quad(min, size, 0.0, Some(Rect::new(60.0, 0.0, 300.0, 300.0)))
.expect("partial overlap");
assert_eq!(q.pos_min, Vec2::new(60.0, 20.0));
assert_eq!(q.pos_max, Vec2::new(110.0, 70.0));
assert_eq!(q.uv_min, Vec2::new(0.5, 0.0));
assert_eq!(q.uv_max, Vec2::ONE);
let q = clip_quad(min, size, 0.0, Some(Rect::new(0.0, 0.0, 300.0, 50.0)))
.expect("partial overlap");
assert_eq!(q.pos_min, min);
assert_eq!(q.pos_max, Vec2::new(110.0, 50.0));
assert_eq!(q.uv_min, Vec2::ZERO);
assert_eq!(q.uv_max, Vec2::new(1.0, 0.6));
let q = clip_quad(min, size, 0.0, Some(Rect::new(0.0, 0.0, 500.0, 500.0)))
.expect("containing clip");
assert_eq!(q, full);
assert_eq!(
clip_quad(min, size, 0.0, Some(Rect::new(200.0, 0.0, 300.0, 300.0))),
None
);
assert_eq!(
clip_quad(min, size, 0.0, Some(Rect::new(110.0, 0.0, 300.0, 300.0))),
None
);
assert_eq!(clip_quad(min, size, 0.0, Some(Rect::default())), None);
}
#[test]
fn rotated_clip_cuts_the_quad_to_a_polygon() {
use bevy::math::Affine2;
let min = Vec2::new(10.0, 20.0);
let size = UVec2::splat(100);
let center = min + 50.0;
let diamond = |side: f32, center: Vec2| {
let mut clip = CalculatedClip::default();
clip.push_rect(
Rect::from_center_size(Vec2::ZERO, Vec2::splat(side)),
Affine2::from_angle_translation(45f32.to_radians(), center).inverse(),
);
clip
};
let cut = |clip: &CalculatedClip| {
let mut vertices = Vec::new();
let count = clip_quad_rotated(min, size, 0.0, clip, |p, uv| vertices.push((p, uv)));
assert_eq!(count as usize, vertices.len());
vertices
};
let clip = diamond(100.0, center);
let vertices = cut(&clip);
assert_eq!(vertices.len(), 18, "an octagon: six triangles");
let mut area = 0.0;
for triangle in vertices.chunks(3) {
let [(a, _), (b, _), (c, _)] = triangle else {
unreachable!()
};
area += (*b - *a).perp_dot(*c - *a).abs() / 2.0;
}
let corner = 100.0 - 50.0 * 2f32.sqrt();
assert!(
(area - (10_000.0 - 2.0 * corner * corner)).abs() < 0.1,
"{area}"
);
let rect = clip.rects().unwrap()[0];
for &(p, uv) in &vertices {
let local = rect.world_to_clip_local.transform_point2(p);
assert!(
rect.rect.inflate(1e-3).contains(local),
"{p} outside the clip"
);
assert!(
Rect::from_corners(min, min + 100.0)
.inflate(1e-3)
.contains(p)
);
assert!(uv.abs_diff_eq((p - min) / 100.0, 1e-6));
}
let full = clip_quad(min, size, 0.0, None).unwrap();
let corners = [
(full.pos_min, Vec2::ZERO),
(Vec2::new(full.pos_max.x, full.pos_min.y), Vec2::X),
(full.pos_max, Vec2::ONE),
(full.pos_min, Vec2::ZERO),
(full.pos_max, Vec2::ONE),
(Vec2::new(full.pos_min.x, full.pos_max.y), Vec2::Y),
];
assert_eq!(cut(&diamond(1000.0, center)), corners, "the plain quad");
assert_eq!(cut(&diamond(100.0, center + 500.0)), [], "disjoint");
assert_eq!(cut(&CalculatedClip::FullyClipped), []);
}
#[test]
fn swap_roundtrip() {
use crate::layer::clip::screen_clip;
let mut world = World::new();
let outer = Rect::new(0.0, 0.0, 100.0, 100.0);
let inner = Rect::new(10.0, 10.0, 50.0, 50.0);
let one = screen_clip(outer);
let mut two = one.clone();
two.push_rect(inner, bevy::math::Affine2::IDENTITY);
let a = world.spawn(two.clone()).id();
let b = world.spawn(one.clone()).id();
let outside = world.spawn(two.clone()).id();
let bare = world.spawn_empty().id();
let dead = world.spawn_empty().id();
world.despawn(dead);
let hidden = world.spawn(CalculatedClip::FullyClipped).id();
let shorter = world.spawn(one.clone()).id();
let mut clips = LayerClips::default();
clips.strip.extend([
(a, 1),
(b, 1),
(bare, 1),
(dead, 1),
(hidden, 1),
(shorter, 2),
]);
world.insert_resource(clips);
let mut stash = Vec::new();
swap_in(&mut world, &mut stash);
assert_eq!(world.get::<CalculatedClip>(a).unwrap(), &screen_clip(inner));
assert_eq!(
world.get::<CalculatedClip>(b).unwrap(),
&CalculatedClip::default()
);
assert_eq!(world.get::<CalculatedClip>(outside).unwrap(), &two);
assert!(world.get::<CalculatedClip>(bare).is_none());
assert_eq!(
world.get::<CalculatedClip>(hidden).unwrap(),
&CalculatedClip::FullyClipped
);
assert_eq!(
world.get::<CalculatedClip>(shorter).unwrap(),
&CalculatedClip::default()
);
assert_eq!(stash.len(), 3, "only really-swapped members are stashed");
swap_out(&mut world, &mut stash);
assert_eq!(world.get::<CalculatedClip>(a).unwrap(), &two);
assert_eq!(world.get::<CalculatedClip>(b).unwrap(), &one);
assert_eq!(world.get::<CalculatedClip>(shorter).unwrap(), &one);
assert!(stash.is_empty(), "stash drains on restore");
}
#[test]
fn swap_in_clears_previous_stash() {
let mut world = World::new();
world.insert_resource(LayerClips::default());
let mut stash = vec![(Entity::PLACEHOLDER, CalculatedClip::default())];
swap_in(&mut world, &mut stash);
assert!(stash.is_empty());
}
}