use api::units::*;
use crate::spatial_tree::{SpatialTree, SpatialNodeIndex, CoordinateSpaceMapping};
use crate::util::{MatrixHelpers, ScaleOffset};
pub const VERT_QUANTIZE_SCALE: f32 = 4.0;
pub fn quantize(v: f32) -> i32 {
(v * VERT_QUANTIZE_SCALE).round() as i32
}
#[derive(Copy, Clone, Debug, Default, PartialEq, peek_poke::PeekPoke)]
#[cfg_attr(feature = "capture", derive(serde::Serialize))]
#[cfg_attr(feature = "replay", derive(serde::Deserialize))]
pub struct VertRange {
pub offset: u32,
pub count: u32,
}
impl VertRange {
pub const INVALID: VertRange = VertRange { offset: 0, count: 0 };
pub fn is_valid(self) -> bool {
self.count > 0
}
}
pub struct CornersCache {
unquantized: Vec<RasterPoint>,
cached_node: Option<SpatialNodeIndex>,
cached_mapping: CoordinateSpaceMapping<LayoutPixel, LayoutPixel>,
}
impl CornersCache {
pub fn new() -> Self {
CornersCache {
unquantized: Vec::new(),
cached_node: None,
cached_mapping: CoordinateSpaceMapping::Local,
}
}
pub fn pre_update(&mut self) {
self.cached_node = None;
}
pub fn clear_scratch(&mut self) {
self.unquantized.clear();
}
pub fn compute_to_scratch(
&mut self,
local_rect: LayoutRect,
prim_spatial_node: SpatialNodeIndex,
tile_cache_spatial_node: SpatialNodeIndex,
local_to_raster: ScaleOffset,
spatial_tree: &SpatialTree,
) -> VertRange {
if Some(prim_spatial_node) != self.cached_node {
let mapping = spatial_tree.get_relative_transform(
prim_spatial_node,
tile_cache_spatial_node,
);
self.cached_mapping = match mapping {
CoordinateSpaceMapping::ScaleOffset(ref so) if so.is_reflection() => {
CoordinateSpaceMapping::Transform(so.to_transform())
}
other => other,
};
self.cached_node = Some(prim_spatial_node);
}
self.append_corners_from_mapping(local_rect, local_to_raster)
}
fn append_corners_from_mapping(
&mut self,
local_rect: LayoutRect,
local_to_raster: ScaleOffset,
) -> VertRange {
match &self.cached_mapping {
CoordinateSpaceMapping::Local => {
let r: RasterRect = local_to_raster.map_rect(&local_rect);
let offset = self.unquantized.len() as u32;
self.unquantized.push(r.min);
self.unquantized.push(r.max);
VertRange { offset, count: 2 }
}
CoordinateSpaceMapping::ScaleOffset(so) => {
let r: RasterRect = so.then(&local_to_raster).map_rect(&local_rect);
let offset = self.unquantized.len() as u32;
self.unquantized.push(r.min);
self.unquantized.push(r.max);
VertRange { offset, count: 2 }
}
CoordinateSpaceMapping::Transform(m) => {
let raster_m = m.then(&local_to_raster.to_transform::<LayoutPixel, RasterPixel>());
let src = [
local_rect.min,
LayoutPoint::new(local_rect.max.x, local_rect.min.y),
LayoutPoint::new(local_rect.min.x, local_rect.max.y),
local_rect.max,
];
let offset = self.unquantized.len() as u32;
if !raster_m.has_perspective_component() {
for p in &src {
match raster_m.transform_point2d(*p) {
Some(pt) => self.unquantized.push(pt),
None => {
self.unquantized.truncate(offset as usize);
return VertRange::INVALID;
}
}
}
return VertRange { offset, count: 4 };
}
let homogens = [
raster_m.transform_point2d_homogeneous(src[0]),
raster_m.transform_point2d_homogeneous(src[1]),
raster_m.transform_point2d_homogeneous(src[2]),
raster_m.transform_point2d_homogeneous(src[3]),
];
if homogens.iter().all(|h| h.w > 0.0) {
for h in &homogens {
self.unquantized.push(RasterPoint::new(h.x / h.w, h.y / h.w));
}
VertRange { offset, count: 4 }
} else {
for h in &homogens {
self.unquantized.push(RasterPoint::new(h.x, h.y));
self.unquantized.push(RasterPoint::new(h.z, h.w));
}
VertRange { offset, count: 8 }
}
}
}
}
pub fn push_verts(&self, scratch_range: VertRange, dst: &mut Vec<i32>) -> VertRange {
if !scratch_range.is_valid() {
return VertRange::INVALID;
}
let start = scratch_range.offset as usize;
let end = (scratch_range.offset + scratch_range.count) as usize;
let corners = &self.unquantized[start..end];
debug_assert!(corners.len() == 2 || corners.len() == 4 || corners.len() == 8);
let offset = dst.len() as u32;
for p in corners {
dst.push(quantize(p.x));
dst.push(quantize(p.y));
}
VertRange { offset, count: (corners.len() * 2) as u32 }
}
pub fn push_verts_clamped(
&self,
scratch_range: VertRange,
tile_rect: &RasterRect,
dst: &mut Vec<i32>,
) -> VertRange {
if !scratch_range.is_valid() {
return VertRange::INVALID;
}
let start = scratch_range.offset as usize;
let end = (scratch_range.offset + scratch_range.count) as usize;
let corners = &self.unquantized[start..end];
debug_assert!(corners.len() == 2 || corners.len() == 4 || corners.len() == 8);
let offset = dst.len() as u32;
if corners.len() == 8 {
for p in corners {
dst.push(quantize(p.x));
dst.push(quantize(p.y));
}
} else {
for p in corners {
dst.push(quantize(p.x.max(tile_rect.min.x).min(tile_rect.max.x)));
dst.push(quantize(p.y.max(tile_rect.min.y).min(tile_rect.max.y)));
}
}
VertRange { offset, count: (corners.len() * 2) as u32 }
}
}
#[cfg(test)]
mod tests {
use super::*;
use api::units::{LayoutPixel, LayoutPoint, LayoutRect, LayoutTransform};
use euclid::Angle;
fn perspective_rotate_x_translate_y(deg: f32, d: f32, ty: f32) -> LayoutTransform {
let translate = LayoutTransform::translation(0.0, ty, 0.0);
let rotate = LayoutTransform::rotation(1.0, 0.0, 0.0, Angle::degrees(deg));
let mut perspective = LayoutTransform::identity();
perspective.m34 = -1.0 / d;
translate.then(&rotate).then(&perspective)
}
#[test]
fn perspective_camera_plane_fingerprint_differs_per_transform() {
let local_rect = LayoutRect::new(
LayoutPoint::new(0.0, 0.0),
LayoutPoint::new(200.0, 2000.0),
);
let local_to_raster = ScaleOffset::identity();
let mut cache = CornersCache::new();
cache.cached_mapping = CoordinateSpaceMapping::Transform(
perspective_rotate_x_translate_y(80.0, 1000.0, 0.0),
);
cache.clear_scratch();
let r1 = cache.append_corners_from_mapping(local_rect, local_to_raster);
assert!(r1.is_valid(), "fingerprint must not collapse to INVALID");
assert_eq!(r1.count, 8, "fingerprint encodes 4 corners as 8 RasterPoints");
let scratch1: Vec<RasterPoint> = cache.unquantized.clone();
cache.cached_mapping = CoordinateSpaceMapping::Transform(
perspective_rotate_x_translate_y(80.0, 1000.0, -20.0),
);
cache.clear_scratch();
let r2 = cache.append_corners_from_mapping(local_rect, local_to_raster);
assert_eq!(r2.count, 8);
let scratch2: Vec<RasterPoint> = cache.unquantized.clone();
assert_ne!(
scratch1, scratch2,
"different perspective transforms must produce different fingerprints",
);
}
#[test]
fn perspective_camera_plane_fingerprint_stable_for_unchanged_transform() {
let local_rect = LayoutRect::new(
LayoutPoint::new(0.0, 0.0),
LayoutPoint::new(200.0, 2000.0),
);
let local_to_raster = ScaleOffset::identity();
let mut cache = CornersCache::new();
let m = perspective_rotate_x_translate_y(80.0, 1000.0, -40.0);
cache.cached_mapping = CoordinateSpaceMapping::Transform(m);
cache.clear_scratch();
let _ = cache.append_corners_from_mapping(local_rect, local_to_raster);
let scratch1: Vec<RasterPoint> = cache.unquantized.clone();
cache.cached_mapping = CoordinateSpaceMapping::Transform(m);
cache.clear_scratch();
let _ = cache.append_corners_from_mapping(local_rect, local_to_raster);
let scratch2: Vec<RasterPoint> = cache.unquantized.clone();
assert_eq!(
scratch1, scratch2,
"the same perspective transform must produce identical fingerprints",
);
}
#[test]
fn no_perspective_uses_projected_corners() {
let local_rect = LayoutRect::new(
LayoutPoint::new(0.0, 0.0),
LayoutPoint::new(100.0, 100.0),
);
let local_to_raster = ScaleOffset::identity();
let mut cache = CornersCache::new();
cache.cached_mapping = CoordinateSpaceMapping::<LayoutPixel, LayoutPixel>::Transform(
LayoutTransform::rotation(1.0, 0.0, 0.0, Angle::degrees(45.0)),
);
cache.clear_scratch();
let r = cache.append_corners_from_mapping(local_rect, local_to_raster);
assert_eq!(r.count, 4, "non-perspective transform must emit 4 corners");
}
}