use cranpose_ui_graphics::{Point, Rect};
use crate::{offscreen::composition_bytes_per_pixel, scene::SnapAnchor};
const QUAD_AXIS_ALIGNMENT_TOLERANCE: f32 = 1e-4;
const DEVICE_SNAP_SUBPIXEL_STEPS: f64 = 16.0;
pub(crate) fn offscreen_byte_size(width: u32, height: u32) -> u64 {
(width as u64) * (height as u64) * composition_bytes_per_pixel()
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct DevicePixelBounds {
pub(crate) x: f32,
pub(crate) y: f32,
pub(crate) width: u32,
pub(crate) height: u32,
}
pub(crate) fn anchored_device_rect(
rect: Rect,
snap_anchor: Option<SnapAnchor>,
root_scale: f32,
) -> Rect {
snap_anchor
.and_then(|anchor| {
axis_aligned_quad_rect(canonicalized_anchored_scaled_quad(
[
[rect.x, rect.y],
[rect.x + rect.width, rect.y],
[rect.x, rect.y + rect.height],
[rect.x + rect.width, rect.y + rect.height],
],
anchor,
root_scale,
))
})
.unwrap_or_else(|| canonicalized_scaled_rect(rect, root_scale))
}
pub(crate) fn translation_stable_anchored_device_pixel_bounds(
rect: Rect,
snap_anchor: Option<SnapAnchor>,
root_scale: f32,
max_texture_dim: u32,
) -> Option<DevicePixelBounds> {
if !root_scale.is_finite() || root_scale <= 0.0 {
return None;
}
let device_rect = anchored_device_rect(rect, snap_anchor, root_scale);
let min_x = device_rect.x.floor();
let min_y = device_rect.y.floor();
let width = (device_rect.width.ceil() + 1.0).max(0.0) as u32;
let height = (device_rect.height.ceil() + 1.0).max(0.0) as u32;
if width == 0 || height == 0 || width > max_texture_dim || height > max_texture_dim {
return None;
}
Some(DevicePixelBounds {
x: min_x,
y: min_y,
width,
height,
})
}
pub(crate) fn translate_quad(quad: [[f32; 2]; 4], delta: Point) -> [[f32; 2]; 4] {
quad.map(|[x, y]| [x + delta.x, y + delta.y])
}
pub(crate) fn scaled_quad(quad: [[f32; 2]; 4], scale: f32) -> [[f32; 2]; 4] {
quad.map(|[x, y]| [x * scale, y * scale])
}
pub(crate) fn canonicalize_device_coordinate(value: f32) -> f32 {
if !value.is_finite() {
return value;
}
((f64::from(value) * DEVICE_SNAP_SUBPIXEL_STEPS).round() / DEVICE_SNAP_SUBPIXEL_STEPS) as f32
}
pub(crate) fn canonicalized_scaled_rect(rect: Rect, scale: f32) -> Rect {
let left = canonicalize_device_coordinate(rect.x * scale);
let top = canonicalize_device_coordinate(rect.y * scale);
let right = canonicalize_device_coordinate((rect.x + rect.width) * scale);
let bottom = canonicalize_device_coordinate((rect.y + rect.height) * scale);
Rect {
x: left,
y: top,
width: right - left,
height: bottom - top,
}
}
pub(crate) fn canonicalized_scaled_quad(quad: [[f32; 2]; 4], scale: f32) -> [[f32; 2]; 4] {
quad.map(|[x, y]| {
[
canonicalize_device_coordinate(x * scale),
canonicalize_device_coordinate(y * scale),
]
})
}
pub(crate) fn canonicalized_anchored_scaled_quad(
quad: [[f32; 2]; 4],
anchor: SnapAnchor,
root_scale: f32,
) -> [[f32; 2]; 4] {
if !root_scale.is_finite() || root_scale <= 0.0 {
return canonicalized_scaled_quad(quad, root_scale);
}
let origin = snapped_anchor_device_origin(anchor, root_scale);
quad.map(|[x, y]| {
[
origin.x + canonicalize_device_coordinate((x - anchor.origin.x) * root_scale),
origin.y + canonicalize_device_coordinate((y - anchor.origin.y) * root_scale),
]
})
}
pub(crate) fn snapped_anchor_device_origin(anchor: SnapAnchor, root_scale: f32) -> Point {
if !root_scale.is_finite() || root_scale <= 0.0 {
return Point::default();
}
let device_pixel_step = anchor_device_pixel_step(anchor);
let snapped = |origin: f32| {
let snap_units = f64::from(origin) * f64::from(root_scale) / f64::from(device_pixel_step);
let canonical_snap_units =
(snap_units * DEVICE_SNAP_SUBPIXEL_STEPS).round() / DEVICE_SNAP_SUBPIXEL_STEPS;
(canonical_snap_units.round() * f64::from(device_pixel_step)) as f32
};
Point::new(snapped(anchor.origin.x), snapped(anchor.origin.y))
}
fn anchor_device_pixel_step(anchor: SnapAnchor) -> f32 {
if anchor.device_pixel_step.is_finite() && anchor.device_pixel_step > 0.0 {
anchor.device_pixel_step
} else {
1.0
}
}
pub(crate) fn snap_delta_for_anchor(anchor: SnapAnchor, root_scale: f32) -> Point {
if !root_scale.is_finite() || root_scale <= 0.0 {
return Point::default();
}
let device_pixel_step = anchor_device_pixel_step(anchor);
let snapped_axis_delta = |origin: f32| {
let root_scale = f64::from(root_scale);
let device_pixel_step = f64::from(device_pixel_step);
let snap_units = f64::from(origin) * root_scale / device_pixel_step;
let canonical_snap_units =
(snap_units * DEVICE_SNAP_SUBPIXEL_STEPS).round() / DEVICE_SNAP_SUBPIXEL_STEPS;
let snapped_logical = canonical_snap_units.round() * device_pixel_step / root_scale;
(snapped_logical - f64::from(origin)) as f32
};
Point::new(
snapped_axis_delta(anchor.origin.x),
snapped_axis_delta(anchor.origin.y),
)
}
fn quad_is_axis_aligned_rect(quad: [[f32; 2]; 4]) -> bool {
(quad[0][1] - quad[1][1]).abs() <= QUAD_AXIS_ALIGNMENT_TOLERANCE
&& (quad[2][1] - quad[3][1]).abs() <= QUAD_AXIS_ALIGNMENT_TOLERANCE
&& (quad[0][0] - quad[2][0]).abs() <= QUAD_AXIS_ALIGNMENT_TOLERANCE
&& (quad[1][0] - quad[3][0]).abs() <= QUAD_AXIS_ALIGNMENT_TOLERANCE
}
pub(crate) fn axis_aligned_quad_rect(dest_quad: [[f32; 2]; 4]) -> Option<Rect> {
if !quad_is_axis_aligned_rect(dest_quad) {
return None;
}
let min_x = dest_quad[0][0].min(dest_quad[2][0]);
let max_x = dest_quad[1][0].max(dest_quad[3][0]);
let min_y = dest_quad[0][1].min(dest_quad[1][1]);
let max_y = dest_quad[2][1].max(dest_quad[3][1]);
if !min_x.is_finite()
|| !max_x.is_finite()
|| !min_y.is_finite()
|| !max_y.is_finite()
|| max_x <= min_x
|| max_y <= min_y
{
return None;
}
Some(Rect {
x: min_x,
y: min_y,
width: max_x - min_x,
height: max_y - min_y,
})
}
#[cfg(test)]
#[path = "tests/geometry_tests.rs"]
mod tests;