use std::{rc::Rc, sync::Arc};
use cranpose_ui_graphics::{ArcGeometry, Brush, CornerRadii, Rect, TAU};
use crate::{
draw::{ClipBounds, RenderItem, ShapeCoverage, clip_rect_to_window, place_shape},
scene::{DrawShape, ImageDraw, Outline, RasterScene, ShadowDraw, TextDraw},
scene_item::ItemRef,
};
const MAX_AREAS: usize = 8;
fn same_shape(old: &DrawShape, new: &DrawShape) -> bool {
let DrawShape {
rect,
snap_anchor,
snap_to_pixel_grid,
brush,
outline,
clip,
blend_mode,
} = old;
*rect == new.rect
&& *snap_anchor == new.snap_anchor
&& *snap_to_pixel_grid == new.snap_to_pixel_grid
&& *brush == new.brush
&& *outline == new.outline
&& *clip == new.clip
&& *blend_mode == new.blend_mode
}
fn solid_fill(shape: &DrawShape) -> bool {
matches!(shape.brush, Brush::Solid(_)) && matches!(shape.outline, Outline::Fill(_))
}
fn moved_fill(old: &DrawShape, new: &DrawShape) -> Option<(Rect, Rect, CornerRadii, Rect)> {
let DrawShape {
rect: _,
snap_anchor: _,
snap_to_pixel_grid,
brush,
outline,
clip,
blend_mode,
} = old;
let alike = solid_fill(old)
&& solid_fill(new)
&& *snap_to_pixel_grid == new.snap_to_pixel_grid
&& *brush == new.brush
&& *outline == new.outline
&& *clip == new.clip
&& *blend_mode == new.blend_mode;
if !alike {
return None;
}
let placed = place_shape(new)?;
let (
ShapeCoverage::Fill {
rect: a,
radii: old_radii,
},
ShapeCoverage::Fill {
rect: b,
radii: new_radii,
},
) = (place_shape(old)?.coverage, placed.coverage)
else {
return None;
};
(old_radii == new_radii).then(|| (a, b, old_radii.unwrap_or_default(), placed.rect))
}
fn edge_strips(a: Rect, b: Rect, radii: CornerRadii) -> [Option<Rect>; 4] {
let (a_right, b_right) = (a.x + a.width, b.x + b.width);
let (a_bottom, b_bottom) = (a.y + a.height, b.y + b.height);
let (left, right) = (a.x.min(b.x), a_right.max(b_right));
let (top, bottom) = (a.y.min(b.y), a_bottom.max(b_bottom));
let strip = |x: f32, to_x: f32, y: f32, to_y: f32| Rect {
x,
y,
width: to_x - x,
height: to_y - y,
};
let left_reach = radii.top_left.max(radii.bottom_left);
let right_reach = radii.top_right.max(radii.bottom_right);
let top_reach = radii.top_left.max(radii.top_right);
let bottom_reach = radii.bottom_left.max(radii.bottom_right);
[
(a.x != b.x).then(|| strip(left, a.x.max(b.x) + left_reach, top, bottom)),
(a_right != b_right).then(|| strip(a_right.min(b_right) - right_reach, right, top, bottom)),
(a.y != b.y).then(|| strip(left, right, top, a.y.max(b.y) + top_reach)),
(a_bottom != b_bottom)
.then(|| strip(left, right, a_bottom.min(b_bottom) - bottom_reach, bottom)),
]
}
fn add_moved_fill_areas(
old: &DrawShape,
new: &DrawShape,
window: ClipBounds,
areas: &mut Vec<ClipBounds>,
) -> Option<Option<ClipBounds>> {
let (a, b, radii, reach) = moved_fill(old, new)?;
for strip in edge_strips(a, b, radii).into_iter().flatten() {
if let Some(area) = clip_rect_to_window(strip, old.clip, window) {
add_area(areas, area);
}
}
Some(clip_rect_to_window(reach, new.clip, window))
}
fn swept_sweeps(old: &DrawShape, new: &DrawShape) -> Option<(f32, f32)> {
let DrawShape {
rect,
snap_anchor,
snap_to_pixel_grid,
brush,
outline,
clip,
blend_mode,
} = old;
let (Outline::Arc(a), Outline::Arc(b)) = (*outline, new.outline) else {
return None;
};
let partial = |arc: ArcGeometry| !arc.is_degenerate() && arc.sweep_angle < TAU;
let alike = (*rect == new.rect || matches!(brush, Brush::Solid(_)))
&& *snap_anchor == new.snap_anchor
&& *snap_to_pixel_grid == new.snap_to_pixel_grid
&& *brush == new.brush
&& *clip == new.clip
&& *blend_mode == new.blend_mode
&& partial(a)
&& partial(b)
&& ArcGeometry {
sweep_angle: b.sweep_angle,
..a
} == b;
alike.then_some(if a.sweep_angle < b.sweep_angle {
(a.sweep_angle, b.sweep_angle)
} else {
(b.sweep_angle, a.sweep_angle)
})
}
fn add_swept_arc_areas(
old: &DrawShape,
new: &DrawShape,
window: ClipBounds,
areas: &mut Vec<ClipBounds>,
) -> bool {
let Some((shorter, longer)) = swept_sweeps(old, new) else {
return false;
};
if let Some(placed) = place_shape(old)
&& let ShapeCoverage::Arc(arc) = placed.coverage
{
let between = ShapeCoverage::Arc(ArcGeometry {
start_angle: arc.start_angle + shorter,
sweep_angle: longer - shorter,
..arc
});
if let Some(area) = clip_rect_to_window(between.reach(placed.rect), old.clip, window) {
add_area(areas, area);
}
}
true
}
fn same_text(old: &TextDraw, new: &TextDraw) -> bool {
let TextDraw {
rect,
snap_anchor,
text,
layout,
ink,
color,
text_style,
font_size,
scale,
clip,
} = old;
*rect == new.rect
&& *snap_anchor == new.snap_anchor
&& (Rc::ptr_eq(text, &new.text) || **text == *new.text)
&& *layout == new.layout
&& *ink == new.ink
&& (Arc::ptr_eq(text_style, &new.text_style) || **text_style == *new.text_style)
&& *color == new.color
&& *font_size == new.font_size
&& *scale == new.scale
&& *clip == new.clip
}
fn same_image(old: &ImageDraw, new: &ImageDraw) -> bool {
let ImageDraw {
rect,
snap_anchor,
image,
alpha,
color_filter,
sampling,
clip,
blend_mode,
src_rect,
transform,
} = old;
*rect == new.rect
&& *transform == new.transform
&& *snap_anchor == new.snap_anchor
&& *image == new.image
&& *alpha == new.alpha
&& *color_filter == new.color_filter
&& *sampling == new.sampling
&& *clip == new.clip
&& *blend_mode == new.blend_mode
&& *src_rect == new.src_rect
}
fn same_shadow(old: &ShadowDraw, new: &ShadowDraw) -> bool {
let ShadowDraw {
shadow,
snap_anchor,
color,
clip,
} = old;
*shadow == new.shadow
&& *snap_anchor == new.snap_anchor
&& *color == new.color
&& *clip == new.clip
}
fn same_item(old: ItemRef<'_>, new: ItemRef<'_>) -> bool {
match (old, new) {
(ItemRef::Shape(old), ItemRef::Shape(new)) => same_shape(old, new),
(ItemRef::Text(old), ItemRef::Text(new)) => same_text(old, new),
(ItemRef::Image(old), ItemRef::Image(new)) => same_image(old, new),
(ItemRef::Shadow(old), ItemRef::Shadow(new)) => same_shadow(old, new),
_ => false,
}
}
enum Narrowing {
Not,
Swept,
Moved(Option<ClipBounds>),
}
fn add_narrowed_areas(
old: ItemRef<'_>,
new: ItemRef<'_>,
window: ClipBounds,
areas: &mut Vec<ClipBounds>,
) -> Narrowing {
let (ItemRef::Shape(old), ItemRef::Shape(new)) = (old, new) else {
return Narrowing::Not;
};
if let Some(reach) = add_moved_fill_areas(old, new, window, areas) {
Narrowing::Moved(reach)
} else if add_swept_arc_areas(old, new, window, areas) {
Narrowing::Swept
} else {
Narrowing::Not
}
}
pub(crate) enum ItemReach<'a> {
Unknown,
Kept(Option<ClipBounds>),
Replaced(&'a mut Option<ClipBounds>),
}
pub(crate) fn add_changed_pair(
old: ItemRef<'_>,
new: ItemRef<'_>,
reach: ItemReach<'_>,
window: ClipBounds,
areas: &mut Vec<ClipBounds>,
) {
if same_item(old, new) {
return;
}
let narrowing = add_narrowed_areas(old, new, window, areas);
let narrowed = !matches!(narrowing, Narrowing::Not);
if narrowed && !matches!(reach, ItemReach::Replaced(_)) {
return;
}
if !narrowed {
let old_bounds = match &reach {
ItemReach::Unknown => old.bounds(window),
ItemReach::Kept(held) => *held,
ItemReach::Replaced(held) => **held,
};
if let Some(area) = old_bounds {
add_area(areas, area);
}
}
let new_bounds = match narrowing {
Narrowing::Moved(reach) => reach,
Narrowing::Not | Narrowing::Swept => new.bounds(window),
};
if !narrowed && let Some(area) = new_bounds {
add_area(areas, area);
}
if let ItemReach::Replaced(held) = reach {
*held = new_bounds;
}
}
fn union(a: ClipBounds, b: ClipBounds) -> ClipBounds {
ClipBounds {
min_x: a.min_x.min(b.min_x),
min_y: a.min_y.min(b.min_y),
max_x: a.max_x.max(b.max_x),
max_y: a.max_y.max(b.max_y),
}
}
fn touch(a: ClipBounds, b: ClipBounds) -> bool {
a.min_x <= b.max_x && b.min_x <= a.max_x && a.min_y <= b.max_y && b.min_y <= a.max_y
}
fn add_area(areas: &mut Vec<ClipBounds>, mut area: ClipBounds) {
if areas.last().is_some_and(|last| last.holds(area)) {
return;
}
while let Some(index) = areas.iter().position(|other| touch(*other, area)) {
area = union(areas.swap_remove(index), area);
}
areas.push(area);
}
fn common_ends(old: &RasterScene, new: &RasterScene) -> (usize, usize) {
let (old_items, new_items) = (&old.order, &new.order);
let shared = old_items.len().min(new_items.len());
let mut start = 0;
let mut runs = new.kept.iter().peekable();
while start < shared {
if let Some(run) = runs.next_if(|run| run.start <= start) {
start = start.max(run.end.min(shared));
continue;
}
if !same_item(old.item(old_items[start]), new.item(new_items[start])) {
break;
}
start += 1;
}
let rest = shared - start;
let same_length = old_items.len() == new_items.len();
let mut end = 0;
let mut runs = new.kept.iter().rev().peekable();
while end < rest {
let index = new_items.len() - 1 - end;
if same_length && let Some(run) = runs.next_if(|run| run.end > index) {
if run.start <= index {
end = (new_items.len() - run.start).min(rest);
}
continue;
}
if !same_item(
old.item(old_items[old_items.len() - 1 - end]),
new.item(new_items[index]),
) {
break;
}
end += 1;
}
(start, end)
}
pub(crate) fn changed_areas(
old: &RasterScene,
old_reach: &[Option<ClipBounds>],
new: &RasterScene,
window: ClipBounds,
areas: &mut Vec<ClipBounds>,
) {
areas.clear();
let old_items = &old.order;
let new_items = &new.order;
let (common_start, common_end) = common_ends(old, new);
let changed = common_start..old_items.len() - common_end;
let reach = old_reach
.get(changed.clone())
.filter(|_| old_reach.len() == old_items.len());
add_changed_runs(
(old, &old_items[changed]),
(new, &new_items[common_start..new_items.len() - common_end]),
reach,
window,
areas,
);
merge_areas(areas);
}
pub(crate) fn add_changed_runs(
(old, old_items): (&RasterScene, &[RenderItem]),
(new, new_items): (&RasterScene, &[RenderItem]),
reach: Option<&[Option<ClipBounds>]>,
window: ClipBounds,
areas: &mut Vec<ClipBounds>,
) {
let paired = old_items.len().min(new_items.len());
for (index, (&old_item, &new_item)) in old_items.iter().zip(new_items).enumerate() {
let held = reach.map_or(ItemReach::Unknown, |reach| ItemReach::Kept(reach[index]));
add_changed_pair(old.item(old_item), new.item(new_item), held, window, areas);
}
for (index, &item) in old_items.iter().enumerate().skip(paired) {
let old_bounds = reach.map_or_else(|| old.item_bounds(item, window), |reach| reach[index]);
if let Some(area) = old_bounds {
add_area(areas, area);
}
}
for &item in &new_items[paired..] {
if let Some(area) = new.item_bounds(item, window) {
add_area(areas, area);
}
}
}
fn pixels(area: ClipBounds) -> i64 {
i64::from(area.max_x - area.min_x) * i64::from(area.max_y - area.min_y)
}
#[inline(never)]
pub(crate) fn merge_areas(areas: &mut Vec<ClipBounds>) {
while areas.len() > MAX_AREAS {
let mut cheapest = (0, 1, i64::MAX);
for (first, &a) in areas.iter().enumerate() {
for (second, &b) in areas.iter().enumerate().skip(first + 1) {
let extra = pixels(union(a, b)) - pixels(a) - pixels(b);
if extra < cheapest.2 {
cheapest = (first, second, extra);
}
}
}
let (first, second, _) = cheapest;
let joined = union(areas.swap_remove(second), areas.swap_remove(first));
add_area(areas, joined);
}
}