use skia_rs_core::{Point, Rect, Scalar};
use skia_rs_path::{Path, PathBuilder, PathElement};
#[derive(Debug, Clone, Copy, PartialEq, Default)]
#[repr(C)]
pub struct TessVertex {
pub position: [f32; 2],
pub uv: [f32; 2],
}
impl TessVertex {
#[inline]
pub const fn new(x: f32, y: f32, u: f32, v: f32) -> Self {
Self {
position: [x, y],
uv: [u, v],
}
}
#[inline]
pub fn from_point(p: Point) -> Self {
Self {
position: [p.x, p.y],
uv: [0.0, 0.0],
}
}
}
pub type TessIndex = u32;
#[derive(Debug, Clone, Default)]
pub struct TessMesh {
pub vertices: Vec<TessVertex>,
pub indices: Vec<TessIndex>,
}
impl TessMesh {
pub fn new() -> Self {
Self::default()
}
pub fn with_capacity(vertex_capacity: usize, index_capacity: usize) -> Self {
Self {
vertices: Vec::with_capacity(vertex_capacity),
indices: Vec::with_capacity(index_capacity),
}
}
pub fn clear(&mut self) {
self.vertices.clear();
self.indices.clear();
}
pub fn is_empty(&self) -> bool {
self.vertices.is_empty() || self.indices.is_empty()
}
pub fn triangle_count(&self) -> usize {
self.indices.len() / 3
}
pub fn add_vertex(&mut self, vertex: TessVertex) -> TessIndex {
let idx = self.vertices.len() as TessIndex;
self.vertices.push(vertex);
idx
}
pub fn add_triangle(&mut self, a: TessIndex, b: TessIndex, c: TessIndex) {
self.indices.push(a);
self.indices.push(b);
self.indices.push(c);
}
pub fn merge(&mut self, other: &TessMesh) {
let base_index = self.vertices.len() as TessIndex;
self.vertices.extend_from_slice(&other.vertices);
self.indices
.extend(other.indices.iter().map(|i| i + base_index));
}
}
#[derive(Debug, Clone, Copy)]
pub struct TessQuality {
pub tolerance: Scalar,
pub max_subdivisions: u32,
}
impl Default for TessQuality {
fn default() -> Self {
Self {
tolerance: 0.25,
max_subdivisions: 10,
}
}
}
impl TessQuality {
pub const LOW: Self = Self {
tolerance: 1.0,
max_subdivisions: 5,
};
pub const MEDIUM: Self = Self {
tolerance: 0.5,
max_subdivisions: 8,
};
pub const HIGH: Self = Self {
tolerance: 0.25,
max_subdivisions: 10,
};
pub const VERY_HIGH: Self = Self {
tolerance: 0.1,
max_subdivisions: 15,
};
}
pub struct PathTessellator {
quality: TessQuality,
contour_points: Vec<Point>,
}
impl PathTessellator {
pub fn new() -> Self {
Self {
quality: TessQuality::default(),
contour_points: Vec::new(),
}
}
pub fn with_quality(quality: TessQuality) -> Self {
Self {
quality,
contour_points: Vec::new(),
}
}
pub fn tessellate_fill(&mut self, path: &Path) -> TessMesh {
let mut mesh = TessMesh::new();
self.contour_points.clear();
let mut current_point = Point::zero();
let mut contour_start = Point::zero();
for element in path.iter() {
match element {
PathElement::Move(p) => {
self.flush_contour(&mut mesh);
current_point = p;
contour_start = p;
self.contour_points.push(p);
}
PathElement::Line(p) => {
self.contour_points.push(p);
current_point = p;
}
PathElement::Quad(ctrl, end) => {
self.flatten_quad(current_point, ctrl, end);
current_point = end;
}
PathElement::Conic(ctrl, end, weight) => {
self.flatten_conic(current_point, ctrl, end, weight);
current_point = end;
}
PathElement::Cubic(ctrl1, ctrl2, end) => {
self.flatten_cubic(current_point, ctrl1, ctrl2, end);
current_point = end;
}
PathElement::Close => {
if current_point != contour_start {
self.contour_points.push(contour_start);
}
self.flush_contour(&mut mesh);
current_point = contour_start;
}
}
}
self.flush_contour(&mut mesh);
mesh
}
pub fn tessellate_stroke(&mut self, path: &Path, stroke_width: Scalar) -> TessMesh {
let mut mesh = TessMesh::new();
let half_width = stroke_width * 0.5;
self.contour_points.clear();
let mut current_point = Point::zero();
let mut contour_start = Point::zero();
for element in path.iter() {
match element {
PathElement::Move(p) => {
self.flush_stroke_contour(&mut mesh, half_width, false);
current_point = p;
contour_start = p;
self.contour_points.push(p);
}
PathElement::Line(p) => {
self.contour_points.push(p);
current_point = p;
}
PathElement::Quad(ctrl, end) => {
self.flatten_quad(current_point, ctrl, end);
current_point = end;
}
PathElement::Conic(ctrl, end, weight) => {
self.flatten_conic(current_point, ctrl, end, weight);
current_point = end;
}
PathElement::Cubic(ctrl1, ctrl2, end) => {
self.flatten_cubic(current_point, ctrl1, ctrl2, end);
current_point = end;
}
PathElement::Close => {
if current_point != contour_start {
self.contour_points.push(contour_start);
}
self.flush_stroke_contour(&mut mesh, half_width, true);
current_point = contour_start;
}
}
}
self.flush_stroke_contour(&mut mesh, half_width, false);
mesh
}
fn flatten_quad(&mut self, p0: Point, p1: Point, p2: Point) {
let steps = self.quad_subdivisions(p0, p1, p2);
for i in 1..=steps {
let t = i as Scalar / steps as Scalar;
let p = Self::eval_quad(p0, p1, p2, t);
self.contour_points.push(p);
}
}
fn flatten_conic(&mut self, p0: Point, p1: Point, p2: Point, w: Scalar) {
if (w - 1.0).abs() < 0.001 {
self.flatten_quad(p0, p1, p2);
return;
}
let base_d = Self::point_to_line_distance(p1, p0, p2);
let weight_amp = 1.0 + (w - 1.0).abs().min(4.0);
let d = base_d * weight_amp;
let steps = ((d / self.quality.tolerance).sqrt().ceil() as u32)
.max(2)
.min(self.quality.max_subdivisions.max(2));
for i in 1..=steps {
let t = i as Scalar / steps as Scalar;
let p = Self::eval_conic(p0, p1, p2, w, t);
self.contour_points.push(p);
}
}
fn flatten_cubic(&mut self, p0: Point, p1: Point, p2: Point, p3: Point) {
let steps = self.cubic_subdivisions(p0, p1, p2, p3);
for i in 1..=steps {
let t = i as Scalar / steps as Scalar;
let p = Self::eval_cubic(p0, p1, p2, p3, t);
self.contour_points.push(p);
}
}
fn quad_subdivisions(&self, p0: Point, p1: Point, p2: Point) -> u32 {
let d = Self::point_to_line_distance(p1, p0, p2);
let steps = ((d / self.quality.tolerance).sqrt().ceil() as u32).max(1);
steps.min(self.quality.max_subdivisions)
}
fn cubic_subdivisions(&self, p0: Point, p1: Point, p2: Point, p3: Point) -> u32 {
let d1 = Self::point_to_line_distance(p1, p0, p3);
let d2 = Self::point_to_line_distance(p2, p0, p3);
let d = d1.max(d2);
let steps = ((d / self.quality.tolerance).sqrt().ceil() as u32).max(1);
steps.min(self.quality.max_subdivisions)
}
fn eval_quad(p0: Point, p1: Point, p2: Point, t: Scalar) -> Point {
let mt = 1.0 - t;
let mt2 = mt * mt;
let t2 = t * t;
Point::new(
mt2 * p0.x + 2.0 * mt * t * p1.x + t2 * p2.x,
mt2 * p0.y + 2.0 * mt * t * p1.y + t2 * p2.y,
)
}
fn eval_conic(p0: Point, p1: Point, p2: Point, w: Scalar, t: Scalar) -> Point {
let mt = 1.0 - t;
let mt2 = mt * mt;
let t2 = t * t;
let wt = 2.0 * w * mt * t;
let denom = mt2 + wt + t2;
Point::new(
(mt2 * p0.x + wt * p1.x + t2 * p2.x) / denom,
(mt2 * p0.y + wt * p1.y + t2 * p2.y) / denom,
)
}
fn eval_cubic(p0: Point, p1: Point, p2: Point, p3: Point, t: Scalar) -> Point {
let mt = 1.0 - t;
let mt2 = mt * mt;
let mt3 = mt2 * mt;
let t2 = t * t;
let t3 = t2 * t;
Point::new(
mt3 * p0.x + 3.0 * mt2 * t * p1.x + 3.0 * mt * t2 * p2.x + t3 * p3.x,
mt3 * p0.y + 3.0 * mt2 * t * p1.y + 3.0 * mt * t2 * p2.y + t3 * p3.y,
)
}
fn point_to_line_distance(p: Point, line_start: Point, line_end: Point) -> Scalar {
let dx = line_end.x - line_start.x;
let dy = line_end.y - line_start.y;
let len_sq = dx * dx + dy * dy;
if len_sq < 1e-10 {
return ((p.x - line_start.x).powi(2) + (p.y - line_start.y).powi(2)).sqrt();
}
let num = ((p.x - line_start.x) * dy - (p.y - line_start.y) * dx).abs();
num / len_sq.sqrt()
}
fn flush_contour(&mut self, mesh: &mut TessMesh) {
if self.contour_points.len() < 3 {
self.contour_points.clear();
return;
}
if self.contour_points.len() > 3 {
let first = self.contour_points[0];
let last = *self.contour_points.last().unwrap();
if (first.x - last.x).abs() < 1e-6 && (first.y - last.y).abs() < 1e-6 {
self.contour_points.pop();
}
}
if self.contour_points.len() < 3 {
self.contour_points.clear();
return;
}
let base_idx = mesh.vertices.len() as TessIndex;
let vertices: Vec<TessVertex> = self
.contour_points
.iter()
.map(|p| TessVertex::from_point(*p))
.collect();
mesh.vertices.extend(vertices);
ear_clip_triangulate(&self.contour_points, base_idx, mesh);
self.contour_points.clear();
}
fn flush_stroke_contour(&mut self, mesh: &mut TessMesh, half_width: Scalar, closed: bool) {
if self.contour_points.len() < 2 {
self.contour_points.clear();
return;
}
let n = self.contour_points.len();
let mut left_vertices = Vec::with_capacity(n);
let mut right_vertices = Vec::with_capacity(n);
for i in 0..n {
let prev_idx = if i == 0 {
if closed { n - 1 } else { 0 }
} else {
i - 1
};
let next_idx = if i == n - 1 {
if closed { 0 } else { n - 1 }
} else {
i + 1
};
let p = self.contour_points[i];
let prev = self.contour_points[prev_idx];
let next = self.contour_points[next_idx];
let tangent = if i == 0 && !closed {
Point::new(next.x - p.x, next.y - p.y)
} else if i == n - 1 && !closed {
Point::new(p.x - prev.x, p.y - prev.y)
} else {
let t1 = Point::new(p.x - prev.x, p.y - prev.y);
let t2 = Point::new(next.x - p.x, next.y - p.y);
Point::new(t1.x + t2.x, t1.y + t2.y)
};
let len = (tangent.x * tangent.x + tangent.y * tangent.y).sqrt();
if len < 1e-10 {
left_vertices.push(TessVertex::from_point(p));
right_vertices.push(TessVertex::from_point(p));
continue;
}
let normal = Point::new(-tangent.y / len, tangent.x / len);
left_vertices.push(TessVertex::new(
p.x + normal.x * half_width,
p.y + normal.y * half_width,
0.0,
0.0,
));
right_vertices.push(TessVertex::new(
p.x - normal.x * half_width,
p.y - normal.y * half_width,
1.0,
0.0,
));
}
let base_idx = mesh.vertices.len() as TessIndex;
mesh.vertices.extend(left_vertices);
let right_base = mesh.vertices.len() as TessIndex;
mesh.vertices.extend(right_vertices);
for i in 0..(n - 1) {
let i = i as TessIndex;
mesh.add_triangle(base_idx + i, right_base + i, base_idx + i + 1);
mesh.add_triangle(base_idx + i + 1, right_base + i, right_base + i + 1);
}
if closed && n > 2 {
let last = (n - 1) as TessIndex;
mesh.add_triangle(base_idx + last, right_base + last, base_idx);
mesh.add_triangle(base_idx, right_base + last, right_base);
}
self.contour_points.clear();
}
}
impl Default for PathTessellator {
fn default() -> Self {
Self::new()
}
}
fn polygon_signed_area(points: &[Point]) -> Scalar {
let n = points.len();
if n < 3 {
return 0.0;
}
let mut area: Scalar = 0.0;
for i in 0..n {
let p = points[i];
let q = points[(i + 1) % n];
area += p.x * q.y - q.x * p.y;
}
area * 0.5
}
#[inline]
fn triangle_cross(a: Point, b: Point, c: Point) -> Scalar {
(b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)
}
fn point_in_triangle(p: Point, a: Point, b: Point, c: Point) -> bool {
let d1 = triangle_cross(p, a, b);
let d2 = triangle_cross(p, b, c);
let d3 = triangle_cross(p, c, a);
let has_neg = d1 < 0.0 || d2 < 0.0 || d3 < 0.0;
let has_pos = d1 > 0.0 || d2 > 0.0 || d3 > 0.0;
!(has_neg && has_pos)
}
fn ear_clip_triangulate(points: &[Point], base_idx: TessIndex, mesh: &mut TessMesh) {
let n = points.len();
if n < 3 {
return;
}
let ccw = polygon_signed_area(points) >= 0.0;
let mut remaining: Vec<usize> = (0..n).collect();
let max_iters = 2 * n * n + 4;
let mut iters = 0;
while remaining.len() > 3 {
iters += 1;
if iters > max_iters {
break;
}
let m = remaining.len();
let mut ear_pos: Option<usize> = None;
for i in 0..m {
let ia = remaining[(i + m - 1) % m];
let ib = remaining[i];
let ic = remaining[(i + 1) % m];
let a = points[ia];
let b = points[ib];
let c = points[ic];
let cross = triangle_cross(a, b, c);
let is_convex = if ccw { cross > 0.0 } else { cross < 0.0 };
if !is_convex {
continue;
}
let mut is_ear = true;
for &other in &remaining {
if other == ia || other == ib || other == ic {
continue;
}
if point_in_triangle(points[other], a, b, c) {
is_ear = false;
break;
}
}
if is_ear {
ear_pos = Some(i);
break;
}
}
match ear_pos {
Some(i) => {
let m = remaining.len();
let ia = remaining[(i + m - 1) % m];
let ib = remaining[i];
let ic = remaining[(i + 1) % m];
if ccw {
mesh.add_triangle(
base_idx + ia as TessIndex,
base_idx + ib as TessIndex,
base_idx + ic as TessIndex,
);
} else {
mesh.add_triangle(
base_idx + ia as TessIndex,
base_idx + ic as TessIndex,
base_idx + ib as TessIndex,
);
}
remaining.remove(i);
}
None => {
break;
}
}
}
if remaining.len() == 3 {
let ia = remaining[0];
let ib = remaining[1];
let ic = remaining[2];
let a = points[ia];
let b = points[ib];
let c = points[ic];
let cross = triangle_cross(a, b, c);
if cross.abs() > 1e-12 {
if ccw == (cross > 0.0) {
mesh.add_triangle(
base_idx + ia as TessIndex,
base_idx + ib as TessIndex,
base_idx + ic as TessIndex,
);
} else {
mesh.add_triangle(
base_idx + ia as TessIndex,
base_idx + ic as TessIndex,
base_idx + ib as TessIndex,
);
}
}
} else if remaining.len() > 3 {
let ia = remaining[0];
for w in remaining.windows(2).skip(1) {
let ib = w[0];
let ic = w[1];
mesh.add_triangle(
base_idx + ia as TessIndex,
base_idx + ib as TessIndex,
base_idx + ic as TessIndex,
);
}
}
}
pub fn tessellate_rect(rect: Rect) -> TessMesh {
let mut mesh = TessMesh::with_capacity(4, 6);
let v0 = mesh.add_vertex(TessVertex::new(rect.left, rect.top, 0.0, 0.0));
let v1 = mesh.add_vertex(TessVertex::new(rect.right, rect.top, 1.0, 0.0));
let v2 = mesh.add_vertex(TessVertex::new(rect.right, rect.bottom, 1.0, 1.0));
let v3 = mesh.add_vertex(TessVertex::new(rect.left, rect.bottom, 0.0, 1.0));
mesh.add_triangle(v0, v1, v2);
mesh.add_triangle(v0, v2, v3);
mesh
}
pub fn tessellate_rounded_rect(rect: Rect, radius: Scalar, quality: TessQuality) -> TessMesh {
let mut mesh = TessMesh::new();
let r = radius.min(rect.width() * 0.5).min(rect.height() * 0.5);
if r < 0.001 {
return tessellate_rect(rect);
}
let segments = ((std::f32::consts::PI * r / quality.tolerance).ceil() as usize)
.max(4)
.min(quality.max_subdivisions as usize);
let center = rect.center();
let center_idx = mesh.add_vertex(TessVertex::new(center.x, center.y, 0.5, 0.5));
let mut edge_vertices = Vec::new();
for i in 0..=segments {
let angle =
std::f32::consts::PI + (i as f32 / segments as f32) * std::f32::consts::FRAC_PI_2;
let x = rect.left + r + r * angle.cos();
let y = rect.top + r + r * angle.sin();
let u = (x - rect.left) / rect.width();
let v = (y - rect.top) / rect.height();
edge_vertices.push(mesh.add_vertex(TessVertex::new(x, y, u, v)));
}
for i in 0..=segments {
let angle =
std::f32::consts::PI * 1.5 + (i as f32 / segments as f32) * std::f32::consts::FRAC_PI_2;
let x = rect.right - r + r * angle.cos();
let y = rect.top + r + r * angle.sin();
let u = (x - rect.left) / rect.width();
let v = (y - rect.top) / rect.height();
edge_vertices.push(mesh.add_vertex(TessVertex::new(x, y, u, v)));
}
for i in 0..=segments {
let angle = (i as f32 / segments as f32) * std::f32::consts::FRAC_PI_2;
let x = rect.right - r + r * angle.cos();
let y = rect.bottom - r + r * angle.sin();
let u = (x - rect.left) / rect.width();
let v = (y - rect.top) / rect.height();
edge_vertices.push(mesh.add_vertex(TessVertex::new(x, y, u, v)));
}
for i in 0..=segments {
let angle = std::f32::consts::FRAC_PI_2
+ (i as f32 / segments as f32) * std::f32::consts::FRAC_PI_2;
let x = rect.left + r + r * angle.cos();
let y = rect.bottom - r + r * angle.sin();
let u = (x - rect.left) / rect.width();
let v = (y - rect.top) / rect.height();
edge_vertices.push(mesh.add_vertex(TessVertex::new(x, y, u, v)));
}
let n = edge_vertices.len();
for i in 0..n {
let next = (i + 1) % n;
mesh.add_triangle(center_idx, edge_vertices[i], edge_vertices[next]);
}
mesh
}
pub fn tessellate_circle(center: Point, radius: Scalar, quality: TessQuality) -> TessMesh {
let mut mesh = TessMesh::new();
let segments = ((2.0 * std::f32::consts::PI * radius / quality.tolerance).ceil() as usize)
.max(8)
.min(quality.max_subdivisions as usize * 4);
let center_idx = mesh.add_vertex(TessVertex::new(center.x, center.y, 0.5, 0.5));
let mut edge_vertices = Vec::with_capacity(segments);
for i in 0..segments {
let angle = (i as f32 / segments as f32) * 2.0 * std::f32::consts::PI;
let x = center.x + radius * angle.cos();
let y = center.y + radius * angle.sin();
let u = 0.5 + 0.5 * angle.cos();
let v = 0.5 + 0.5 * angle.sin();
edge_vertices.push(mesh.add_vertex(TessVertex::new(x, y, u, v)));
}
for i in 0..segments {
let next = (i + 1) % segments;
mesh.add_triangle(center_idx, edge_vertices[i], edge_vertices[next]);
}
mesh
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tess_vertex() {
let v = TessVertex::new(1.0, 2.0, 0.5, 0.5);
assert_eq!(v.position, [1.0, 2.0]);
assert_eq!(v.uv, [0.5, 0.5]);
}
#[test]
fn test_tess_mesh() {
let mut mesh = TessMesh::new();
assert!(mesh.is_empty());
let v0 = mesh.add_vertex(TessVertex::new(0.0, 0.0, 0.0, 0.0));
let v1 = mesh.add_vertex(TessVertex::new(1.0, 0.0, 1.0, 0.0));
let v2 = mesh.add_vertex(TessVertex::new(0.5, 1.0, 0.5, 1.0));
mesh.add_triangle(v0, v1, v2);
assert!(!mesh.is_empty());
assert_eq!(mesh.triangle_count(), 1);
assert_eq!(mesh.vertices.len(), 3);
assert_eq!(mesh.indices.len(), 3);
}
#[test]
fn test_tessellate_rect() {
let rect = Rect::from_xywh(0.0, 0.0, 100.0, 50.0);
let mesh = tessellate_rect(rect);
assert_eq!(mesh.vertices.len(), 4);
assert_eq!(mesh.indices.len(), 6);
assert_eq!(mesh.triangle_count(), 2);
}
#[test]
fn test_tessellate_circle() {
let mesh = tessellate_circle(Point::new(50.0, 50.0), 25.0, TessQuality::MEDIUM);
assert!(mesh.vertices.len() > 8);
assert!(mesh.triangle_count() >= 8);
}
#[test]
fn test_tessellate_rounded_rect() {
let rect = Rect::from_xywh(0.0, 0.0, 100.0, 50.0);
let mesh = tessellate_rounded_rect(rect, 10.0, TessQuality::MEDIUM);
assert!(mesh.vertices.len() > 4);
assert!(mesh.triangle_count() > 2);
}
#[test]
fn test_path_tessellator_fill() {
let mut tessellator = PathTessellator::new();
let mut builder = PathBuilder::new();
builder
.move_to(0.0, 0.0)
.line_to(100.0, 0.0)
.line_to(100.0, 100.0)
.line_to(0.0, 100.0)
.close();
let path = builder.build();
let mesh = tessellator.tessellate_fill(&path);
assert!(!mesh.is_empty());
assert!(mesh.vertices.len() >= 4);
assert!(mesh.triangle_count() >= 2);
}
#[test]
fn test_path_tessellator_stroke() {
let mut tessellator = PathTessellator::new();
let mut builder = PathBuilder::new();
builder
.move_to(0.0, 0.0)
.line_to(100.0, 0.0)
.line_to(100.0, 100.0);
let path = builder.build();
let mesh = tessellator.tessellate_stroke(&path, 2.0);
assert!(!mesh.is_empty());
assert!(mesh.vertices.len() >= 6);
}
#[test]
fn test_quality_presets() {
assert!(TessQuality::LOW.tolerance > TessQuality::HIGH.tolerance);
assert!(TessQuality::LOW.max_subdivisions < TessQuality::HIGH.max_subdivisions);
}
#[test]
fn test_polygon_signed_area() {
let ccw = [
Point::new(0.0, 0.0),
Point::new(10.0, 0.0),
Point::new(10.0, 10.0),
Point::new(0.0, 10.0),
];
assert!(polygon_signed_area(&ccw) > 0.0);
let cw = [
Point::new(0.0, 0.0),
Point::new(0.0, 10.0),
Point::new(10.0, 10.0),
Point::new(10.0, 0.0),
];
assert!(polygon_signed_area(&cw) < 0.0);
}
#[test]
fn test_ear_clip_concave_l_shape() {
let pts = [
Point::new(0.0, 0.0),
Point::new(20.0, 0.0),
Point::new(20.0, 10.0),
Point::new(10.0, 10.0),
Point::new(10.0, 20.0),
Point::new(0.0, 20.0),
];
let mut mesh = TessMesh::new();
for p in &pts {
mesh.add_vertex(TessVertex::from_point(*p));
}
ear_clip_triangulate(&pts, 0, &mut mesh);
assert_eq!(mesh.triangle_count(), 4, "L-shape must emit n-2 triangles");
let notch = Point::new(15.0, 15.0);
for tri in mesh.indices.chunks(3) {
let a = pts[tri[0] as usize];
let b = pts[tri[1] as usize];
let c = pts[tri[2] as usize];
assert!(
!point_in_triangle(notch, a, b, c),
"triangle ({:?},{:?},{:?}) covers the concave notch",
a,
b,
c
);
}
}
#[test]
fn test_ear_clip_u_shape_concave() {
let pts = [
Point::new(0.0, 0.0),
Point::new(30.0, 0.0),
Point::new(30.0, 30.0),
Point::new(20.0, 30.0),
Point::new(20.0, 10.0),
Point::new(10.0, 10.0),
Point::new(10.0, 30.0),
Point::new(0.0, 30.0),
];
let mut mesh = TessMesh::new();
for p in &pts {
mesh.add_vertex(TessVertex::from_point(*p));
}
ear_clip_triangulate(&pts, 0, &mut mesh);
assert_eq!(mesh.triangle_count(), 6);
let hole = Point::new(15.0, 20.0);
for tri in mesh.indices.chunks(3) {
let a = pts[tri[0] as usize];
let b = pts[tri[1] as usize];
let c = pts[tri[2] as usize];
assert!(!point_in_triangle(hole, a, b, c));
}
}
#[test]
fn test_ear_clip_clockwise_input() {
let pts = [
Point::new(0.0, 0.0),
Point::new(0.0, 20.0),
Point::new(10.0, 20.0),
Point::new(10.0, 10.0),
Point::new(20.0, 10.0),
Point::new(20.0, 0.0),
];
let mut mesh = TessMesh::new();
for p in &pts {
mesh.add_vertex(TessVertex::from_point(*p));
}
ear_clip_triangulate(&pts, 0, &mut mesh);
assert_eq!(mesh.triangle_count(), 4);
}
#[test]
fn test_path_tessellator_concave_fill() {
let mut tessellator = PathTessellator::new();
let mut builder = PathBuilder::new();
builder
.move_to(0.0, 0.0)
.line_to(20.0, 0.0)
.line_to(20.0, 10.0)
.line_to(10.0, 10.0)
.line_to(10.0, 20.0)
.line_to(0.0, 20.0)
.close();
let path = builder.build();
let mesh = tessellator.tessellate_fill(&path);
assert_eq!(mesh.triangle_count(), 4);
}
#[test]
fn test_conic_adaptive_subdivision() {
let mut t = PathTessellator::with_quality(TessQuality {
tolerance: 0.25,
max_subdivisions: 64,
});
t.contour_points.push(Point::new(0.0, 0.0));
t.flatten_conic(
Point::new(0.0, 0.0),
Point::new(5.0, 5.0),
Point::new(10.0, 0.0),
3.0,
);
let heavy = t.contour_points.len();
t.contour_points.clear();
t.contour_points.push(Point::new(0.0, 0.0));
t.flatten_conic(
Point::new(0.0, 0.0),
Point::new(5.0, 0.01),
Point::new(10.0, 0.0),
3.0,
);
let gentle = t.contour_points.len();
assert!(
heavy > gentle,
"sharp conic ({} steps) should emit more segments than a gentle one ({} steps)",
heavy,
gentle,
);
}
}