use functional_trait::functional_trait;
use glam::{Affine2, Vec2};
use crate::data::runtime::mesh::MeshIdx;
use crate::data::runtime::payload::{MeshPayload, PayloadUsage};
#[functional_trait]
pub trait AsTexture {
fn uv(&self, u: f32, v: f32) -> [f32; 4];
}
#[functional_trait]
pub trait AsBitmap {
fn set_pixel(&mut self, x: usize, y: usize, rgba: [f32; 4]);
}
pub trait Clip {
fn clip_software(&mut self);
}
pub trait Merge {
fn merge_meshes(&mut self, separate_at_clip: bool);
}
impl Merge for Vec<MeshPayload> {
fn merge_meshes(&mut self, separate_at_clip: bool) {
struct Group {
depth: usize,
positions: Vec<Vec2>,
uvs: Vec<Vec2>,
indices: Vec<u32>,
tint: [f32; 4],
slot: crate::data::runtime::slot::SlotIdx,
mesh_idx: MeshIdx,
}
let mut depth = 0usize;
let mut groups: Vec<Group> = Vec::new();
for payload in std::mem::take(self) {
match payload.usage {
PayloadUsage::ClipStart { .. } => {
if separate_at_clip {
depth += 1;
}
}
PayloadUsage::ClipEnd { .. } => {
if separate_at_clip {
depth = depth.saturating_sub(1);
}
}
PayloadUsage::Mesh(mesh_idx) => {
let d = if separate_at_clip { depth } else { 0 };
let uv_transform = payload.uv_transform;
let uvs: Vec<Vec2> = payload
.uvs
.iter()
.map(|&uv| uv_transform.transform_point2(uv))
.collect();
let mut gi = None;
for (i, g) in groups.iter().enumerate() {
if g.depth == d && g.tint == payload.tint_color {
gi = Some(i);
break;
}
}
let gi = match gi {
Some(i) => i,
None => {
groups.push(Group {
depth: d,
positions: Vec::new(),
uvs: Vec::new(),
indices: Vec::new(),
tint: payload.tint_color,
slot: payload.slot,
mesh_idx,
});
groups.len() - 1
}
};
let g = &mut groups[gi];
let base = g.positions.len() as u32;
g.positions.extend(payload.positions);
g.uvs.extend(uvs);
g.indices.extend(payload.indices.iter().map(|&i| i + base));
}
}
}
*self = groups
.into_iter()
.map(|g| {
MeshPayload::new(
PayloadUsage::Mesh(g.mesh_idx),
g.slot,
g.positions,
g.uvs,
g.indices,
g.tint,
Affine2::IDENTITY,
)
})
.collect();
}
}
pub trait Render {
fn render_pixel<T: AsTexture + ?Sized>(
&self,
x: f32,
y: f32,
tex: Option<&T>,
premultiplied_alpha: bool,
) -> [f32; 4];
fn aabb(&self) -> Option<(Vec2, Vec2)>;
fn render_bitmap<T: AsTexture + ?Sized, B: AsBitmap + ?Sized>(
&self,
width: usize,
height: usize,
half_extent: Vec2,
offset: Vec2,
tex: Option<&T>,
premultiplied_alpha: bool,
out: &mut B,
);
fn render_tint<B: AsBitmap + ?Sized>(
&self,
width: usize,
height: usize,
half_extent: Vec2,
offset: Vec2,
out: &mut B,
) {
self.render_bitmap(width, height, half_extent, offset, None::<&dyn AsTexture>, false, out)
}
}
struct ClipResult {
positions: Vec<Vec2>,
uvs: Vec<Vec2>,
indices: Vec<u32>,
}
impl Clip for Vec<MeshPayload> {
fn clip_software(&mut self) {
let mut clip_stack: Vec<Vec<Vec2>> = Vec::new();
let mut out: Vec<MeshPayload> = Vec::with_capacity(self.len());
for mut payload in std::mem::take(self) {
match payload.usage {
PayloadUsage::ClipStart { .. } => {
let poly = normalize_polygon(std::mem::take(&mut payload.positions));
let poly = match clip_stack.last() {
Some(top) => clip_polygon(&poly, top).unwrap_or_default(),
None => poly,
};
if !poly.is_empty() {
clip_stack.push(poly);
}
}
PayloadUsage::ClipEnd { .. } => {
clip_stack.pop();
}
PayloadUsage::Mesh(_) => {
if !clip_stack.is_empty() {
let mut positions = std::mem::take(&mut payload.positions);
let mut uvs = std::mem::take(&mut payload.uvs);
let mut indices = std::mem::take(&mut payload.indices);
let mut fully_cut = false;
for poly in &clip_stack {
match clip_mesh(&positions, &uvs, &indices, poly) {
Some(result) => {
positions = result.positions;
uvs = result.uvs;
indices = result.indices;
}
None => {
fully_cut = true;
break;
}
}
}
if fully_cut {
continue;
}
payload.positions = positions;
payload.uvs = uvs;
payload.indices = indices;
}
}
}
out.push(payload);
}
*self = out;
}
}
#[inline]
fn blend_src(tc: [f32; 4], tint: [f32; 4], premultiplied_alpha: bool) -> [f32; 4] {
if premultiplied_alpha {
[
tc[0] * tint[0] * tint[3],
tc[1] * tint[1] * tint[3],
tc[2] * tint[2] * tint[3],
tc[3] * tint[3],
]
} else {
[
tc[0] * tint[0],
tc[1] * tint[1],
tc[2] * tint[2],
tc[3] * tint[3],
]
}
}
#[inline]
fn src_over(dst: &mut [f32; 4], src: [f32; 4], premultiplied_alpha: bool) {
if src[3] >= 1.0 {
*dst = src;
} else if premultiplied_alpha {
let ia = 1.0 - src[3];
dst[0] = src[0] + dst[0] * ia;
dst[1] = src[1] + dst[1] * ia;
dst[2] = src[2] + dst[2] * ia;
dst[3] = src[3] + dst[3] * ia;
} else {
let a = src[3];
let ia = 1.0 - a;
dst[0] = src[0] * a + dst[0] * ia;
dst[1] = src[1] * a + dst[1] * ia;
dst[2] = src[2] * a + dst[2] * ia;
dst[3] = a + dst[3] * ia;
}
}
impl Render for Vec<MeshPayload> {
fn render_pixel<T: AsTexture + ?Sized>(
&self,
x: f32,
y: f32,
tex: Option<&T>,
premultiplied_alpha: bool,
) -> [f32; 4] {
const EPS: f32 = 1e-4;
let mut acc = [0.0f32; 4];
for payload in self.iter().rev() {
let PayloadUsage::Mesh(_) = payload.usage else {
continue;
};
let tint = payload.tint_color;
for tri in payload.indices.chunks_exact(3) {
let p0 = payload.positions[tri[0] as usize];
let p1 = payload.positions[tri[1] as usize];
let p2 = payload.positions[tri[2] as usize];
let v0 = p1 - p0;
let v1 = p2 - p0;
let v2 = Vec2::new(x, y) - p0;
let denom = v0.x * v1.y - v1.x * v0.y;
if denom.abs() <= 1e-9 {
continue;
}
let inv = 1.0 / denom;
let u = (v2.x * v1.y - v1.x * v2.y) * inv;
let v = (v0.x * v2.y - v2.x * v0.y) * inv;
if u < -EPS || v < -EPS || u + v > 1.0 + EPS {
continue;
}
let uv0 = payload.uvs[tri[0] as usize];
let uv1 = payload.uvs[tri[1] as usize];
let uv2 = payload.uvs[tri[2] as usize];
let uv = uv0 + (uv1 - uv0) * u + (uv2 - uv0) * v;
let tc = match tex {
Some(t) => t.uv(uv.x, uv.y),
None => [1.0, 1.0, 1.0, 1.0],
};
let src = blend_src(tc, tint, premultiplied_alpha);
let (sr, sg, sb, sa) = (src[0], src[1], src[2], src[3]);
let mut pm = if premultiplied_alpha {
src
} else {
[sr * sa, sg * sa, sb * sa, sa]
};
src_over(&mut pm, acc, true);
acc = pm;
break;
}
if acc[3] >= 1.0 {
break; }
}
acc
}
fn aabb(&self) -> Option<(Vec2, Vec2)> {
let mut min = Vec2::splat(f32::INFINITY);
let mut max = Vec2::splat(f32::NEG_INFINITY);
let mut found = false;
for payload in self {
let PayloadUsage::Mesh(_) = payload.usage else {
continue;
};
for &p in &payload.positions {
min = min.min(p);
max = max.max(p);
found = true;
}
}
found.then_some((min, max))
}
fn render_bitmap<T: AsTexture + ?Sized, B: AsBitmap + ?Sized>(
&self,
width: usize,
height: usize,
half_extent: Vec2,
offset: Vec2,
tex: Option<&T>,
premultiplied_alpha: bool,
out: &mut B,
) {
if width == 0 || height == 0 {
return;
}
let mut buf = vec![[0.0f32; 4]; width * height];
let to_col =
|v: f32| { ((v - offset.x) / half_extent.x + 1.0) * 0.5 * width as f32 } as isize;
let to_row = |v: f32| {
height as isize
- 1
- (((v - offset.y) / half_extent.y + 1.0) * 0.5 * height as f32) as isize
};
for payload in self {
let PayloadUsage::Mesh(_) = payload.usage else {
continue;
};
let tint = payload.tint_color;
for tri in payload.indices.chunks_exact(3) {
let p0 = payload.positions[tri[0] as usize];
let p1 = payload.positions[tri[1] as usize];
let p2 = payload.positions[tri[2] as usize];
let uv0 = payload.uvs[tri[0] as usize];
let uv1 = payload.uvs[tri[1] as usize];
let uv2 = payload.uvs[tri[2] as usize];
let min_x = p0.x.min(p1.x).min(p2.x);
let max_x = p0.x.max(p1.x).max(p2.x);
let min_y = p0.y.min(p1.y).min(p2.y);
let max_y = p0.y.max(p1.y).max(p2.y);
let c0 = to_col(min_x).max(0).min(width as isize - 1);
let c1 = (to_col(max_x) + 1).clamp(0, width as isize);
let r0 = to_row(max_y).max(0).min(height as isize - 1);
let r1 = (to_row(min_y) + 1).clamp(0, height as isize);
if c0 >= c1 || r0 >= r1 {
continue;
}
let v0 = p1 - p0;
let v1 = p2 - p0;
let denom = v0.x * v1.y - v1.x * v0.y;
if denom.abs() <= 1e-9 {
continue;
}
let inv = 1.0 / denom;
for row in r0..r1 {
for col in c0..c1 {
let sx = (col as f32 + 0.5) / width as f32 * 2.0 * half_extent.x
- half_extent.x
+ offset.x;
let sy =
(height as f32 - 0.5 - row as f32) / height as f32 * 2.0 * half_extent.y
- half_extent.y
+ offset.y;
let v2 = Vec2::new(sx, sy) - p0;
let u = (v2.x * v1.y - v1.x * v2.y) * inv;
let v = (v0.x * v2.y - v2.x * v0.y) * inv;
if u < 0.0 || v < 0.0 || u + v > 1.0 {
continue;
}
let uv = uv0 + (uv1 - uv0) * u + (uv2 - uv0) * v;
let tc = match tex {
Some(t) => t.uv(uv.x, uv.y),
None => [1.0, 1.0, 1.0, 1.0],
};
let src = blend_src(tc, tint, premultiplied_alpha);
let dst = &mut buf[row as usize * width + col as usize];
src_over(dst, src, premultiplied_alpha);
}
}
}
}
for (i, pixel) in buf.iter().enumerate() {
if pixel[3] > 0.0 {
out.set_pixel(i % width, i / width, *pixel);
}
}
}
}
fn clip_mesh(
positions: &[Vec2],
uvs: &[Vec2],
indices: &[u32],
clip_polygon: &[Vec2],
) -> Option<ClipResult> {
debug_assert!(clip_polygon.len() >= 3);
if clip_polygon.len() < 3 || positions.is_empty() {
return None;
}
let mut out_positions: Vec<Vec2> = Vec::new();
let mut out_uvs: Vec<Vec2> = Vec::new();
let mut out_indices: Vec<u32> = Vec::new();
let edges: Vec<(Vec2, Vec2)> = clip_polygon
.iter()
.zip(
clip_polygon
.iter()
.skip(1)
.chain(clip_polygon.iter().take(1)),
)
.map(|(&a, &b)| (a, b))
.collect();
let mut cur: Vec<(Vec2, Vec2)> = Vec::with_capacity(3 + edges.len());
let mut next: Vec<(Vec2, Vec2)> = Vec::with_capacity(3 + edges.len());
for tri in indices.chunks_exact(3) {
let [i0, i1, i2] = [tri[0], tri[1], tri[2]];
if (i0 as usize) >= positions.len()
|| (i1 as usize) >= positions.len()
|| (i2 as usize) >= positions.len()
{
continue;
}
cur.clear();
cur.push((positions[i0 as usize], uvs[i0 as usize]));
cur.push((positions[i1 as usize], uvs[i1 as usize]));
cur.push((positions[i2 as usize], uvs[i2 as usize]));
let mut clipped = false;
for &(a, b) in &edges {
next.clear();
let n = cur.len();
for k in 0..n {
let curr = cur[k];
let nxt = cur[(k + 1) % n];
let inside_curr = inside(curr.0, a, b);
let inside_next = inside(nxt.0, a, b);
match (inside_curr, inside_next) {
(true, true) => next.push(nxt),
(true, false) => {
if let Some(p) = intersect(curr, nxt, a, b) {
next.push(p);
}
}
(false, true) => {
if let Some(p) = intersect(curr, nxt, a, b) {
next.push(p);
}
next.push(nxt);
}
(false, false) => {}
}
}
std::mem::swap(&mut cur, &mut next);
if cur.len() < 3 {
clipped = true;
break;
}
}
if clipped || cur.len() < 3 {
continue;
}
let base = out_positions.len() as u32;
for &(p, u) in &cur {
out_positions.push(p);
out_uvs.push(u);
}
for k in 1..cur.len() - 1 {
out_indices.push(base);
out_indices.push(base + k as u32);
out_indices.push(base + k as u32 + 1);
}
}
if out_indices.is_empty() {
return None;
}
Some(ClipResult {
positions: out_positions,
uvs: out_uvs,
indices: out_indices,
})
}
fn clip_polygon(poly: &[Vec2], clip: &[Vec2]) -> Option<Vec<Vec2>> {
let mut cur: Vec<Vec2> = Vec::with_capacity(poly.len() + clip.len());
cur.extend_from_slice(poly);
let mut next: Vec<Vec2> = Vec::with_capacity(poly.len() + clip.len());
for i in 0..clip.len() {
let a = clip[i];
let b = clip[(i + 1) % clip.len()];
next.clear();
let n = cur.len();
for k in 0..n {
let c = cur[k];
let d = cur[(k + 1) % n];
let inside_c = inside(c, a, b);
let inside_d = inside(d, a, b);
match (inside_c, inside_d) {
(true, true) => next.push(d),
(true, false) => {
if let Some(p) = intersect_pos(c, d, a, b) {
next.push(p);
}
}
(false, true) => {
if let Some(p) = intersect_pos(c, d, a, b) {
next.push(p);
}
next.push(d);
}
(false, false) => {}
}
}
std::mem::swap(&mut cur, &mut next);
if cur.len() < 3 {
return None;
}
}
Some(cur)
}
#[inline]
fn inside(p: Vec2, a: Vec2, b: Vec2) -> bool {
(b - a).perp_dot(p - a) >= 0.0
}
fn normalize_polygon(mut poly: Vec<Vec2>) -> Vec<Vec2> {
if poly.len() < 3 {
return poly;
}
let mut area2 = 0.0;
for i in 0..poly.len() {
let a = poly[i];
let b = poly[(i + 1) % poly.len()];
area2 += a.x * b.y - b.x * a.y;
}
if area2 < 0.0 {
poly.reverse();
}
poly
}
#[inline]
fn intersect(curr: (Vec2, Vec2), next: (Vec2, Vec2), a: Vec2, b: Vec2) -> Option<(Vec2, Vec2)> {
let p = intersect_pos(curr.0, next.0, a, b)?;
let e2 = next.0 - curr.0;
let t = if e2.length_squared() > 1e-12 {
((p - curr.0).dot(e2) / e2.length_squared()).clamp(0.0, 1.0)
} else {
0.0
};
let u = curr.1 + (next.1 - curr.1) * t;
Some((p, u))
}
#[inline]
fn intersect_pos(c: Vec2, d: Vec2, a: Vec2, b: Vec2) -> Option<Vec2> {
let e1 = b - a;
let e2 = d - c;
let denom = e2.perp_dot(e1);
if denom.abs() < 1e-9 {
return None;
}
let t = (a - c).perp_dot(e1) / denom;
let t = t.clamp(0.0, 1.0);
Some(c + e2 * t)
}
#[cfg(test)]
mod tests {
use super::*;
fn square_payload(min: Vec2, max: Vec2) -> MeshPayload {
let positions = vec![
Vec2::new(min.x, min.y),
Vec2::new(max.x, min.y),
Vec2::new(max.x, max.y),
Vec2::new(min.x, max.y),
];
let uvs = vec![
Vec2::new(0.0, 0.0),
Vec2::new(1.0, 0.0),
Vec2::new(1.0, 1.0),
Vec2::new(0.0, 1.0),
];
MeshPayload::new(
PayloadUsage::Mesh(unsafe { crate::custom_index::AsIndex::from_index(0) }),
unsafe { crate::custom_index::AsIndex::from_index(0) },
positions,
uvs,
vec![0, 1, 2, 0, 2, 3],
[1.0; 4],
glam::Affine2::IDENTITY,
)
}
#[test]
fn clip_software_cuts_mesh_to_polygon() {
let clip_polygon = vec![
Vec2::new(0.0, 0.0),
Vec2::new(1.0, 0.0),
Vec2::new(1.0, 1.0),
Vec2::new(0.0, 1.0),
];
let mut payloads = vec![
MeshPayload::new(
PayloadUsage::ClipStart {
id: unsafe { crate::custom_index::AsIndex::from_index(0) },
},
unsafe { crate::custom_index::AsIndex::from_index(0) },
clip_polygon,
Vec::new(),
Vec::new(),
[1.0; 4],
glam::Affine2::IDENTITY,
),
square_payload(Vec2::new(-0.5, -0.5), Vec2::new(1.5, 1.5)),
MeshPayload::new(
PayloadUsage::ClipEnd {
id: unsafe { crate::custom_index::AsIndex::from_index(0) },
},
unsafe { crate::custom_index::AsIndex::from_index(0) },
Vec::new(),
Vec::new(),
Vec::new(),
[1.0; 4],
glam::Affine2::IDENTITY,
),
];
payloads.clip_software();
assert!(matches!(payloads[0].usage, PayloadUsage::ClipStart { .. }));
assert!(matches!(payloads[2].usage, PayloadUsage::ClipEnd { .. }));
let mesh = &payloads[1];
assert_eq!(mesh.positions.len(), mesh.uvs.len());
assert!(mesh.indices.len() >= 6);
for p in &mesh.positions {
assert!(p.x >= -1e-5 && p.x <= 1.0 + 1e-5, "x={p}");
assert!(p.y >= -1e-5 && p.y <= 1.0 + 1e-5, "y={p}");
}
assert!(
mesh.positions.len() > 4,
"裁剪后应产生新顶点: {}",
mesh.positions.len()
);
}
#[test]
fn clip_software_removes_fully_cut_mesh() {
let clip_polygon = vec![
Vec2::new(0.0, 0.0),
Vec2::new(1.0, 0.0),
Vec2::new(1.0, 1.0),
Vec2::new(0.0, 1.0),
];
let mut payloads = vec![
MeshPayload::new(
PayloadUsage::ClipStart {
id: unsafe { crate::custom_index::AsIndex::from_index(0) },
},
unsafe { crate::custom_index::AsIndex::from_index(0) },
clip_polygon,
Vec::new(),
Vec::new(),
[1.0; 4],
glam::Affine2::IDENTITY,
),
square_payload(Vec2::new(2.0, 2.0), Vec2::new(3.0, 3.0)),
MeshPayload::new(
PayloadUsage::ClipEnd {
id: unsafe { crate::custom_index::AsIndex::from_index(0) },
},
unsafe { crate::custom_index::AsIndex::from_index(0) },
Vec::new(),
Vec::new(),
Vec::new(),
[1.0; 4],
glam::Affine2::IDENTITY,
),
];
payloads.clip_software();
assert_eq!(payloads.len(), 2, "完全被裁掉的网格应移除");
}
#[test]
fn clip_aya_real_data_overlaps() {
let clip_polygon = vec![
Vec2::new(-89.20994, 555.9128),
Vec2::new(-66.166855, 560.08997),
Vec2::new(-44.71911, 560.0063),
Vec2::new(-25.425596, 557.4294),
Vec2::new(-11.843452, 552.75586),
Vec2::new(-21.501608, 545.0098),
Vec2::new(-35.769283, 541.7343),
Vec2::new(-53.30312, 539.16125),
Vec2::new(-72.196556, 537.8067),
Vec2::new(-95.73134, 542.27637),
];
let mesh_positions = vec![
Vec2::new(-52.31537, 580.1522),
Vec2::new(-32.943085, 569.6864),
Vec2::new(-21.479704, 544.66406),
Vec2::new(-21.48632, 528.79535),
Vec2::new(-40.4547, 525.3975),
Vec2::new(-55.146145, 528.87854),
Vec2::new(-69.72027, 525.9573),
Vec2::new(-80.33296, 536.7864),
Vec2::new(-86.4835, 546.76855),
Vec2::new(-70.21397, 581.0802),
];
let mesh_uvs: Vec<Vec2> = (0..mesh_positions.len())
.map(|i| Vec2::splat(i as f32))
.collect();
let indices: Vec<u32> = vec![
0, 1, 2, 0, 2, 3, 0, 3, 4, 0, 4, 5, 0, 5, 6, 0, 6, 7, 0, 7, 8, 0, 8, 9, 0, 9, 1,
];
let poly = normalize_polygon(clip_polygon.clone());
let area2 = {
let mut a = 0.0;
for i in 0..poly.len() {
let p = poly[i];
let q = poly[(i + 1) % poly.len()];
a += p.x * q.y - q.x * p.y;
}
a
};
assert!(area2 > 0.0, "规范化后应逆时针: {area2}");
let result = clip_mesh(&mesh_positions, &mesh_uvs, &indices, &poly);
assert!(result.is_some(), "重叠明显的网格不应被完全裁掉");
if let Some(r) = result {
eprintln!(
"aya clip: {} verts {} tris (in poly {})",
r.positions.len(),
r.indices.len(),
poly.len()
);
assert!(!r.indices.is_empty());
}
}
#[test]
fn render_pixel_samples_rendered_triangle() {
let mut payloads = vec![square_payload(
Vec2::new(-200.0, -200.0),
Vec2::new(200.0, 200.0),
)];
payloads[0].tint_color = [1.0, 0.0, 0.0, 1.0];
let c = payloads.render_pixel(0.0, 0.0, None::<&dyn AsTexture>, false);
assert!(
c[0] > 0.9 && c[1] < 0.1 && c[2] < 0.1 && c[3] > 0.9,
"{c:?}"
);
let c = payloads.render_pixel(500.0, 500.0, None::<&dyn AsTexture>, false);
assert_eq!(c, [0.0, 0.0, 0.0, 0.0], "{c:?}");
let mut red = square_payload(Vec2::new(-200.0, -200.0), Vec2::new(200.0, 200.0));
red.tint_color = [1.0, 0.0, 0.0, 1.0];
let mut blue = square_payload(Vec2::new(-50.0, -50.0), Vec2::new(50.0, 50.0));
blue.tint_color = [0.0, 0.0, 1.0, 1.0];
let payloads = vec![red, blue];
let c = payloads.render_pixel(0.0, 0.0, None::<&dyn AsTexture>, false);
assert!(c[2] > 0.9 && c[0] < 0.1, "{c:?}");
let c = payloads.render_pixel(150.0, 150.0, None::<&dyn AsTexture>, false);
assert!(c[0] > 0.9 && c[2] < 0.1, "{c:?}");
}
#[test]
fn render_pixel_applies_texture_uv() {
let tex: &dyn AsTexture = &|u: f32, _v: f32| -> [f32; 4] {
if u < 0.5 {
[1.0, 0.0, 0.0, 1.0]
} else {
[0.0, 1.0, 0.0, 1.0]
}
};
let mut payloads = vec![square_payload(Vec2::new(0.0, 0.0), Vec2::new(100.0, 100.0))];
payloads[0].tint_color = [1.0; 4];
let c = payloads.render_pixel(25.0, 50.0, Some(tex), false);
assert!(c[0] > 0.9 && c[1] < 0.1, "{c:?}");
let c = payloads.render_pixel(75.0, 50.0, Some(tex), false);
assert!(c[1] > 0.9 && c[0] < 0.1, "{c:?}");
}
fn test_bitmap<F: FnOnce(&mut dyn AsBitmap)>(w: usize, h: usize, run: F) -> Vec<[f32; 4]> {
let mut pixels = vec![[0.0, 0.0, 0.0, 0.0]; w * h];
run(&mut |x: usize, y: usize, rgba: [f32; 4]| {
pixels[y * w + x] = rgba;
});
pixels
}
#[test]
fn render_bitmap_fills_triangle_bbox() {
let mut payloads = vec![square_payload(Vec2::new(0.0, 0.0), Vec2::new(100.0, 100.0))];
payloads[0].tint_color = [1.0, 0.0, 0.0, 1.0];
let pixels = test_bitmap(8, 8, |bmp| {
payloads.render_bitmap(8, 8, Vec2::splat(100.0), Vec2::ZERO, None::<&dyn AsTexture>, false, bmp);
});
assert!(pixels[8 + 5][0] > 0.9, "{:?}", pixels[8 + 5]);
assert!(
pixels[2 * 8 + 2][3] == 0.0,
"外部应透明: {:?}",
pixels[2 * 8 + 2]
);
assert!(pixels[3 * 8 + 7][0] > 0.9, "{:?}", pixels[3 * 8 + 7]);
assert!(
pixels[5 * 8 + 1][3] == 0.0,
"外部应透明: {:?}",
pixels[5 * 8 + 1]
);
let pixels = test_bitmap(8, 8, |bmp| {
payloads.render_bitmap(
8,
8,
Vec2::splat(100.0),
Vec2::new(100.0, 100.0),
None::<&dyn AsTexture>,
false,
bmp,
);
});
assert!(
pixels[5 * 8 + 2][0] > 0.9,
"位移后应命中: {:?}",
pixels[5 * 8 + 2]
);
assert!(
pixels[8 + 6][3] == 0.0,
"位移后外部应透明: {:?}",
pixels[8 + 6]
);
}
#[test]
fn render_bitmap_later_payload_overwrites() {
let mut red = square_payload(Vec2::new(-100.0, -100.0), Vec2::new(100.0, 100.0));
red.tint_color = [1.0, 0.0, 0.0, 1.0];
let mut blue = square_payload(Vec2::new(-25.0, -25.0), Vec2::new(25.0, 25.0));
blue.tint_color = [0.0, 0.0, 1.0, 1.0];
let payloads = vec![red, blue];
let pixels = test_bitmap(8, 8, |bmp| {
payloads.render_bitmap(8, 8, Vec2::splat(100.0), Vec2::ZERO, None::<&dyn AsTexture>, false, bmp);
});
assert!(
pixels[4 * 8 + 4][2] > 0.9 && pixels[4 * 8 + 4][0] < 0.1,
"{:?}",
pixels[4 * 8 + 4]
);
assert!(
pixels[8 + 1][0] > 0.9 && pixels[8 + 1][2] < 0.1,
"{:?}",
pixels[8 + 1]
);
}
#[test]
fn render_bitmap_blends_transparent_over_opaque() {
let mut red = square_payload(Vec2::new(-100.0, -100.0), Vec2::new(100.0, 100.0));
red.tint_color = [1.0, 0.0, 0.0, 1.0];
let mut blue = square_payload(Vec2::new(-50.0, -50.0), Vec2::new(50.0, 50.0));
blue.tint_color = [0.0, 0.0, 1.0, 0.5];
let payloads = vec![red, blue];
let pixels = test_bitmap(8, 8, |bmp| {
payloads.render_bitmap(8, 8, Vec2::splat(100.0), Vec2::ZERO, None::<&dyn AsTexture>, false, bmp);
});
let c = pixels[4 * 8 + 4];
assert!(c[3] > 0.99, "alpha 应不透明: {c:?}");
assert!(
(c[0] - 0.5).abs() < 0.1 && (c[2] - 0.5).abs() < 0.1,
"{c:?}"
);
assert!(
pixels[8 + 1][0] > 0.9 && pixels[8 + 1][3] > 0.99,
"{:?}",
pixels[8 + 1]
);
let c = payloads.render_pixel(0.0, 0.0, None::<&dyn AsTexture>, false);
assert!(
c[3] > 0.99 && (c[0] - 0.5).abs() < 0.1 && (c[2] - 0.5).abs() < 0.1,
"{c:?}"
);
}
#[test]
fn aabb_covers_all_meshes() {
let payloads: Vec<MeshPayload> = Vec::new();
assert_eq!(payloads.aabb(), None);
let a = square_payload(Vec2::new(-100.0, -100.0), Vec2::new(100.0, 100.0));
let b = square_payload(Vec2::new(200.0, 300.0), Vec2::new(400.0, 500.0));
let payloads = vec![a, b];
let (min, max) = payloads.aabb().unwrap();
assert!((min.x - (-100.0)).abs() < 1e-4, "{min:?}");
assert!((min.y - (-100.0)).abs() < 1e-4, "{min:?}");
assert!((max.x - 400.0).abs() < 1e-4, "{max:?}");
assert!((max.y - 500.0).abs() < 1e-4, "{max:?}");
}
#[test]
fn render_tint_produces_silhouette() {
let mut blue = square_payload(Vec2::new(-50.0, -50.0), Vec2::new(50.0, 50.0));
blue.tint_color = [0.0, 0.0, 1.0, 1.0];
let payloads = vec![blue];
let pixels = test_bitmap(8, 8, |bmp| {
payloads.render_tint(8, 8, Vec2::splat(100.0), Vec2::ZERO, bmp);
});
let c = pixels[4 * 8 + 4];
assert!((c[2] - 1.0).abs() < 0.1 && c[0] < 0.1, "{c:?}");
assert!(c[3] > 0.99, "{c:?}");
assert!(pixels[8 + 1][3] == 0.0, "{:?}", pixels[8 + 1]);
let pixels2 = test_bitmap(8, 8, |bmp| {
payloads.render_bitmap(8, 8, Vec2::splat(100.0), Vec2::ZERO, None::<&dyn AsTexture>, false, bmp);
});
assert_eq!(pixels, pixels2);
}
#[test]
fn render_premultiplied_matches_straight() {
let mut payloads = vec![square_payload(Vec2::new(0.0, 0.0), Vec2::new(100.0, 100.0))];
payloads[0].tint_color = [1.0, 1.0, 1.0, 0.5];
let straight: &dyn AsTexture = &|u: f32, _v: f32| -> [f32; 4] {
if u < 0.5 {
[0.8, 0.2, 0.4, 0.5]
} else {
[0.2, 0.6, 0.4, 0.5]
}
};
let premul: &dyn AsTexture = &|u: f32, _v: f32| -> [f32; 4] {
if u < 0.5 {
[0.4, 0.1, 0.2, 0.5]
} else {
[0.1, 0.3, 0.2, 0.5]
}
};
for x in [25.0, 75.0] {
let a = payloads.render_pixel(x, 50.0, Some(straight), false);
let b = payloads.render_pixel(x, 50.0, Some(premul), true);
for i in 0..4 {
assert!(
(a[i] - b[i]).abs() < 1e-4,
"straight {a:?} vs premul {b:?} at x={x}"
);
}
}
let px_s = test_bitmap(8, 8, |bmp| {
payloads.render_bitmap(8, 8, Vec2::splat(100.0), Vec2::ZERO, Some(straight), false, bmp);
});
let px_p = test_bitmap(8, 8, |bmp| {
payloads.render_bitmap(8, 8, Vec2::splat(100.0), Vec2::ZERO, Some(premul), true, bmp);
});
for (s, p) in px_s.iter().zip(&px_p) {
for i in 0..4 {
assert!((s[i] - p[i]).abs() < 1e-4, "straight {s:?} vs premul {p:?}");
}
}
}
#[test]
fn merge_meshes_combines_without_clip() {
let mut payloads = vec![
square_payload(Vec2::new(0.0, 0.0), Vec2::new(100.0, 100.0)),
square_payload(Vec2::new(100.0, 0.0), Vec2::new(200.0, 100.0)),
];
let before = payloads.render_pixel(150.0, 50.0, None::<&dyn AsTexture>, false);
payloads.merge_meshes(false);
assert_eq!(payloads.len(), 1);
let m = &payloads[0];
assert_eq!(m.positions.len(), 8);
assert_eq!(m.uvs.len(), 8);
assert_eq!(m.indices.len(), 12);
for &i in &m.indices {
assert!((i as usize) < m.positions.len(), "index {i} out of range");
}
let after = payloads.render_pixel(150.0, 50.0, None::<&dyn AsTexture>, false);
assert_eq!(before, after);
}
#[test]
fn merge_meshes_separates_at_clip() {
let clip_polygon = vec![
Vec2::new(0.0, 0.0),
Vec2::new(50.0, 0.0),
Vec2::new(50.0, 50.0),
Vec2::new(0.0, 50.0),
];
let clip_start = || {
MeshPayload::new(
PayloadUsage::ClipStart {
id: unsafe { crate::custom_index::AsIndex::from_index(0) },
},
unsafe { crate::custom_index::AsIndex::from_index(0) },
clip_polygon.clone(),
Vec::new(),
Vec::new(),
[1.0; 4],
glam::Affine2::IDENTITY,
)
};
let clip_end = || {
MeshPayload::new(
PayloadUsage::ClipEnd {
id: unsafe { crate::custom_index::AsIndex::from_index(0) },
},
unsafe { crate::custom_index::AsIndex::from_index(0) },
Vec::new(),
Vec::new(),
Vec::new(),
[1.0; 4],
glam::Affine2::IDENTITY,
)
};
let inner = square_payload(Vec2::new(0.0, 0.0), Vec2::new(50.0, 50.0));
let outer = square_payload(Vec2::new(100.0, 100.0), Vec2::new(200.0, 200.0));
let mut payloads = vec![clip_start(), inner.clone(), clip_end(), outer.clone()];
payloads.merge_meshes(true);
assert_eq!(payloads.len(), 2, "clip 内外应分开: {:?}", payloads.len());
let mut payloads = vec![clip_start(), inner, clip_end(), outer];
payloads.merge_meshes(false);
assert_eq!(payloads.len(), 1);
assert_eq!(payloads[0].positions.len(), 8);
}
#[test]
fn merge_meshes_groups_by_tint() {
let mut a = square_payload(Vec2::new(0.0, 0.0), Vec2::new(100.0, 100.0));
a.tint_color = [1.0, 0.0, 0.0, 1.0];
let mut b = square_payload(Vec2::new(100.0, 100.0), Vec2::new(200.0, 200.0));
b.tint_color = [0.0, 0.0, 1.0, 1.0];
let mut payloads = vec![a, b];
payloads.merge_meshes(false);
assert_eq!(payloads.len(), 2, "不同 tint 应分成独立组");
let (red, blue) = if payloads[0].tint_color[0] > 0.9 {
(&payloads[0], &payloads[1])
} else {
(&payloads[1], &payloads[0])
};
assert!(red.tint_color[0] > 0.9 && red.positions.len() == 4);
assert!(blue.tint_color[2] > 0.9 && blue.positions.len() == 4);
let c0 = payloads.render_pixel(50.0, 50.0, None::<&dyn AsTexture>, false);
assert!(c0[0] > 0.9 && c0[2] < 0.1, "{c0:?}");
let c1 = payloads.render_pixel(150.0, 150.0, None::<&dyn AsTexture>, false);
assert!(c1[2] > 0.9 && c1[0] < 0.1, "{c1:?}");
}
#[test]
fn merge_meshes_applies_uv_transform() {
let mut a = square_payload(Vec2::new(0.0, 0.0), Vec2::new(100.0, 100.0));
a.uv_transform = glam::Affine2::from_scale(Vec2::splat(2.0));
let mut b = square_payload(Vec2::new(100.0, 100.0), Vec2::new(200.0, 200.0));
b.uv_transform = glam::Affine2::from_scale(Vec2::splat(3.0));
let mut payloads = vec![a, b];
payloads.merge_meshes(false);
assert_eq!(payloads.len(), 1, "uv_transform 应用掉后不同变换的网格可合并");
let m = &payloads[0];
assert_eq!(m.uv_transform, glam::Affine2::IDENTITY);
assert_eq!(m.uvs[0], Vec2::new(0.0, 0.0));
assert_eq!(m.uvs[1], Vec2::new(2.0, 0.0));
assert_eq!(m.uvs[2], Vec2::new(2.0, 2.0));
assert_eq!(m.uvs[4], Vec2::new(0.0, 0.0));
assert_eq!(m.uvs[5], Vec2::new(3.0, 0.0));
assert_eq!(m.uvs[6], Vec2::new(3.0, 3.0));
}
#[test]
fn merge_meshes_nested_clip_layers() {
let poly = || {
vec![
Vec2::new(0.0, 0.0),
Vec2::new(50.0, 0.0),
Vec2::new(50.0, 50.0),
Vec2::new(0.0, 50.0),
]
};
let clip_start = || {
MeshPayload::new(
PayloadUsage::ClipStart {
id: unsafe { crate::custom_index::AsIndex::from_index(0) },
},
unsafe { crate::custom_index::AsIndex::from_index(0) },
poly(),
Vec::new(),
Vec::new(),
[1.0; 4],
glam::Affine2::IDENTITY,
)
};
let clip_end = || {
MeshPayload::new(
PayloadUsage::ClipEnd {
id: unsafe { crate::custom_index::AsIndex::from_index(0) },
},
unsafe { crate::custom_index::AsIndex::from_index(0) },
Vec::new(),
Vec::new(),
Vec::new(),
[1.0; 4],
glam::Affine2::IDENTITY,
)
};
let mesh = || square_payload(Vec2::new(0.0, 0.0), Vec2::new(50.0, 50.0));
let mut payloads = vec![
mesh(),
clip_start(),
mesh(),
clip_start(),
mesh(),
clip_end(),
clip_end(),
mesh(),
];
payloads.merge_meshes(true);
assert_eq!(payloads.len(), 3, "应为 3 组(外/层1/层2): {}", payloads.len());
let mut sizes: Vec<usize> = payloads.iter().map(|p| p.positions.len()).collect();
sizes.sort_unstable();
assert_eq!(sizes, vec![4, 4, 8], "{sizes:?}");
}
}