use crate::flatten::{flatten, DEFAULT_TOLERANCE};
use crate::path::{polygon_convexity, Convexity, FillRule, Path, Verb};
use crate::stroke::{LineCap, LineJoin, StrokeStyle};
use glam::Vec2;
#[derive(Debug, Clone, Default, PartialEq)]
pub struct VertexBuffers {
pub vertices: Vec<Vec2>,
pub indices: Vec<u32>,
}
impl VertexBuffers {
pub fn clear(&mut self) {
self.vertices.clear();
self.indices.clear();
}
pub fn triangle_count(&self) -> usize {
self.indices.len() / 3
}
pub fn is_empty(&self) -> bool {
self.indices.is_empty()
}
pub fn is_well_formed(&self) -> bool {
self.indices.len() % 3 == 0
&& self
.indices
.iter()
.all(|i| (*i as usize) < self.vertices.len())
}
}
fn usable_tolerance(tolerance: f32) -> f32 {
const FLOOR: f32 = 1e-6;
if !tolerance.is_finite() || tolerance <= 0.0 {
return DEFAULT_TOLERANCE;
}
tolerance.max(FLOOR)
}
#[derive(Default)]
pub struct Tessellator {
buffers: VertexBuffers,
fill: lyon_tessellation::FillTessellator,
stroke: lyon_tessellation::StrokeTessellator,
}
impl Tessellator {
pub fn new() -> Self {
Self::default()
}
pub fn fill(&mut self, path: &Path, tolerance: f32) -> &VertexBuffers {
if !path.is_finite() || !path.is_within_tessellation_range() {
self.buffers.clear();
return &self.buffers;
}
let tolerance = usable_tolerance(tolerance);
self.buffers.clear();
let polylines = flatten(path, tolerance);
if polylines.is_empty() {
return &self.buffers;
}
if polylines.len() == 1
&& polygon_convexity(strip_closing_duplicate(&polylines[0])) == Convexity::Convex
{
fan_fill(strip_closing_duplicate(&polylines[0]), &mut self.buffers);
return &self.buffers;
}
if polylines.len() == 1
&& ear_fill(strip_closing_duplicate(&polylines[0]), &mut self.buffers)
{
return &self.buffers;
}
self.buffers.clear();
general_fill(
&polylines,
path.fill_rule(),
&mut self.fill,
&mut self.buffers,
);
&self.buffers
}
#[doc(hidden)]
pub fn fill_general(&mut self, path: &Path, tolerance: f32) -> &VertexBuffers {
self.buffers.clear();
if !path.is_finite() || !path.is_within_tessellation_range() {
return &self.buffers;
}
let polylines = flatten(path, usable_tolerance(tolerance));
if polylines.is_empty() {
return &self.buffers;
}
general_fill(
&polylines,
path.fill_rule(),
&mut self.fill,
&mut self.buffers,
);
&self.buffers
}
pub fn stroke(&mut self, path: &Path, style: &StrokeStyle, tolerance: f32) -> &VertexBuffers {
if !path.is_finite() || !path.is_within_tessellation_range() {
self.buffers.clear();
return &self.buffers;
}
let tolerance = usable_tolerance(tolerance);
use lyon_tessellation::{
BuffersBuilder, LineCap as LyonCap, LineJoin as LyonJoin, StrokeOptions,
};
self.buffers.clear();
if path.is_empty() || !style.is_visible() {
return &self.buffers;
}
let lyon_path = to_lyon_path(path);
let options = StrokeOptions::default()
.with_line_width(style.width)
.with_tolerance(tolerance)
.with_miter_limit((style.miter_limit * 0.5).max(StrokeOptions::MINIMUM_MITER_LIMIT))
.with_line_cap(match style.cap {
LineCap::Butt => LyonCap::Butt,
LineCap::Round => LyonCap::Round,
LineCap::Square => LyonCap::Square,
})
.with_line_join(match style.join {
LineJoin::Miter => LyonJoin::Miter,
LineJoin::Round => LyonJoin::Round,
LineJoin::Bevel => LyonJoin::Bevel,
});
let mut geometry: lyon_tessellation::VertexBuffers<Vec2, u32> =
lyon_tessellation::VertexBuffers::new();
{
let mut builder = BuffersBuilder::new(&mut geometry, ToVec2);
if self
.stroke
.tessellate_path(&lyon_path, &options, &mut builder)
.is_err()
{
return &self.buffers;
}
}
self.buffers.vertices.extend_from_slice(&geometry.vertices);
self.buffers.indices.extend_from_slice(&geometry.indices);
&self.buffers
}
pub fn buffers(&self) -> &VertexBuffers {
&self.buffers
}
}
fn to_lyon_path(path: &Path) -> lyon_tessellation::path::Path {
use lyon_tessellation::math::Point as LyonPoint;
use lyon_tessellation::path::Path as LyonPath;
let pt = |p: Vec2| LyonPoint::new(p.x, p.y);
let mut builder = LyonPath::builder();
let mut open = false;
for (verb, points) in path.segments() {
match verb {
Verb::MoveTo => {
if open {
builder.end(false);
}
builder.begin(pt(points[0]));
open = true;
}
Verb::LineTo if open => {
builder.line_to(pt(points[0]));
}
Verb::QuadTo if open => {
builder.quadratic_bezier_to(pt(points[0]), pt(points[1]));
}
Verb::CubicTo if open => {
builder.cubic_bezier_to(pt(points[0]), pt(points[1]), pt(points[2]));
}
Verb::Close if open => {
builder.end(true);
open = false;
}
_ => {}
}
}
if open {
builder.end(false);
}
builder.build()
}
fn strip_closing_duplicate(points: &[Vec2]) -> &[Vec2] {
if points.len() > 2 && points.first() == points.last() {
&points[..points.len() - 1]
} else {
points
}
}
fn fan_fill(points: &[Vec2], out: &mut VertexBuffers) {
if points.len() < 3 {
return;
}
out.vertices.extend_from_slice(points);
for i in 1..points.len() as u32 - 1 {
out.indices.extend_from_slice(&[0, i, i + 1]);
}
}
fn ear_fill(points: &[Vec2], out: &mut VertexBuffers) -> bool {
const MAX_EAR_POINTS: usize = 16;
let n = points.len();
if !(3..=MAX_EAR_POINTS).contains(&n) {
return false;
}
if !is_simple_polygon(points) {
return false;
}
let flat: Vec<f64> = points
.iter()
.flat_map(|p| [f64::from(p.x), f64::from(p.y)])
.collect();
let Ok(indices) = earcutr::earcut(&flat, &[], 2) else {
return false;
};
if indices.len() != (n - 2) * 3 {
return false;
}
for triangle in indices.chunks_exact(3) {
let [a, b, c] = [triangle[0], triangle[1], triangle[2]];
if a >= n || b >= n || c >= n || a == b || b == c || a == c {
return false;
}
}
out.vertices.extend_from_slice(points);
out.indices
.extend(indices.into_iter().map(|index| index as u32));
true
}
fn is_simple_polygon(points: &[Vec2]) -> bool {
let n = points.len();
let edge = |i: usize| (points[i], points[(i + 1) % n]);
for i in 0..n {
let (a, b) = edge(i);
if a == b {
return false;
}
let last = if i == 0 { n - 1 } else { n };
for j in (i + 2)..last {
let (c, d) = edge(j);
if segments_meet(a, b, c, d) {
return false;
}
}
}
true
}
fn segments_meet(a: Vec2, b: Vec2, c: Vec2, d: Vec2) -> bool {
let side = |p: Vec2, q: Vec2, r: Vec2| (q - p).perp_dot(r - p);
let (d1, d2) = (side(c, d, a), side(c, d, b));
let (d3, d4) = (side(a, b, c), side(a, b, d));
if ((d1 > 0.0 && d2 < 0.0) || (d1 < 0.0 && d2 > 0.0))
&& ((d3 > 0.0 && d4 < 0.0) || (d3 < 0.0 && d4 > 0.0))
{
return true;
}
let within = |p: Vec2, q: Vec2, r: Vec2| {
r.x >= p.x.min(q.x) && r.x <= p.x.max(q.x) && r.y >= p.y.min(q.y) && r.y <= p.y.max(q.y)
};
(d1 == 0.0 && within(c, d, a))
|| (d2 == 0.0 && within(c, d, b))
|| (d3 == 0.0 && within(a, b, c))
|| (d4 == 0.0 && within(a, b, d))
}
fn general_fill(
polylines: &[Vec<Vec2>],
fill_rule: FillRule,
tessellator: &mut lyon_tessellation::FillTessellator,
out: &mut VertexBuffers,
) {
use lyon_tessellation::math::Point as LyonPoint;
use lyon_tessellation::path::Path as LyonPath;
use lyon_tessellation::{BuffersBuilder, FillOptions, FillRule as LyonFillRule};
let mut builder = LyonPath::builder();
for line in polylines {
let stripped = strip_closing_duplicate(line);
if stripped.len() < 3 {
continue;
}
builder.begin(LyonPoint::new(stripped[0].x, stripped[0].y));
for p in &stripped[1..] {
builder.line_to(LyonPoint::new(p.x, p.y));
}
builder.close();
}
let lyon_path = builder.build();
let options = FillOptions::default().with_fill_rule(match fill_rule {
FillRule::NonZero => LyonFillRule::NonZero,
FillRule::EvenOdd => LyonFillRule::EvenOdd,
});
let mut geometry: lyon_tessellation::VertexBuffers<Vec2, u32> =
lyon_tessellation::VertexBuffers::new();
{
let mut builder = BuffersBuilder::new(&mut geometry, ToVec2);
if tessellator
.tessellate_path(&lyon_path, &options, &mut builder)
.is_err()
{
return;
}
}
out.vertices.extend_from_slice(&geometry.vertices);
out.indices.extend_from_slice(&geometry.indices);
}
struct ToVec2;
impl lyon_tessellation::FillVertexConstructor<Vec2> for ToVec2 {
fn new_vertex(&mut self, vertex: lyon_tessellation::FillVertex) -> Vec2 {
let p = vertex.position();
Vec2::new(p.x, p.y)
}
}
impl lyon_tessellation::StrokeVertexConstructor<Vec2> for ToVec2 {
fn new_vertex(&mut self, vertex: lyon_tessellation::StrokeVertex) -> Vec2 {
let p = vertex.position();
Vec2::new(p.x, p.y)
}
}
fn signed_area2(a: Vec2, b: Vec2, c: Vec2) -> f32 {
(b - a).perp_dot(c - a)
}
pub fn covered_area(buffers: &VertexBuffers) -> f32 {
buffers
.indices
.chunks_exact(3)
.map(|t| {
let (a, b, c) = (
buffers.vertices[t[0] as usize],
buffers.vertices[t[1] as usize],
buffers.vertices[t[2] as usize],
);
signed_area2(a, b, c).abs() * 0.5
})
.sum()
}
pub fn polygon_area(points: &[Vec2]) -> f32 {
if points.len() < 3 {
return 0.0;
}
let mut acc = 0.0;
for i in 0..points.len() {
let a = points[i];
let b = points[(i + 1) % points.len()];
acc += a.perp_dot(b);
}
(acc * 0.5).abs()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::flatten::DEFAULT_TOLERANCE;
use crate::path::PathBuilder;
fn square() -> Path {
let mut b = PathBuilder::new();
b.move_to(Vec2::new(0.0, 0.0))
.line_to(Vec2::new(10.0, 0.0))
.line_to(Vec2::new(10.0, 10.0))
.line_to(Vec2::new(0.0, 10.0))
.close();
b.build()
}
fn el_shape() -> Path {
let mut b = PathBuilder::new();
b.move_to(Vec2::new(0.0, 0.0))
.line_to(Vec2::new(10.0, 0.0))
.line_to(Vec2::new(10.0, 4.0))
.line_to(Vec2::new(4.0, 4.0))
.line_to(Vec2::new(4.0, 10.0))
.line_to(Vec2::new(0.0, 10.0))
.close();
b.build()
}
#[test]
fn convex_fill_fans_from_the_first_vertex() {
let mut t = Tessellator::new();
let buffers = t.fill(&square(), DEFAULT_TOLERANCE);
assert_eq!(buffers.vertices.len(), 4);
assert_eq!(buffers.triangle_count(), 2);
assert!(buffers.indices.chunks_exact(3).all(|t| t[0] == 0));
assert!(buffers.is_well_formed());
}
#[test]
fn convex_fill_covers_exactly_the_polygon_area() {
let mut t = Tessellator::new();
let buffers = t.fill(&square(), DEFAULT_TOLERANCE);
assert!((covered_area(buffers) - 100.0).abs() < 0.01);
}
#[test]
fn concave_fill_covers_the_polygon_and_not_its_hull() {
let path = el_shape();
let mut t = Tessellator::new();
let buffers = t.fill(&path, DEFAULT_TOLERANCE);
assert!(buffers.is_well_formed());
let area = covered_area(buffers);
assert!(
(area - 64.0).abs() < 0.5,
"expected the L's own area, got {area}"
);
}
#[test]
fn indices_always_address_real_vertices() {
let mut t = Tessellator::new();
for path in [square(), el_shape()] {
let buffers = t.fill(&path, DEFAULT_TOLERANCE);
assert!(buffers.is_well_formed());
}
}
#[test]
fn empty_and_degenerate_paths_produce_no_triangles() {
let mut t = Tessellator::new();
assert!(t.fill(&Path::default(), DEFAULT_TOLERANCE).is_empty());
let mut b = PathBuilder::new();
b.move_to(Vec2::ZERO).line_to(Vec2::new(1.0, 1.0)).close();
assert!(t.fill(&b.build(), DEFAULT_TOLERANCE).is_empty());
}
#[test]
fn buffers_are_reused_across_calls_without_leaking_previous_geometry() {
let mut t = Tessellator::new();
let first = t.fill(&el_shape(), DEFAULT_TOLERANCE).triangle_count();
assert!(first > 0);
let second = t.fill(&square(), DEFAULT_TOLERANCE);
assert_eq!(second.triangle_count(), 2);
assert_eq!(second.vertices.len(), 4);
}
#[test]
fn fill_rule_changes_the_result_for_overlapping_subpaths() {
let build = |rule: FillRule| {
let mut b = PathBuilder::new().with_fill_rule(rule);
b.move_to(Vec2::new(0.0, 0.0))
.line_to(Vec2::new(10.0, 0.0))
.line_to(Vec2::new(10.0, 10.0))
.line_to(Vec2::new(0.0, 10.0))
.close()
.move_to(Vec2::new(3.0, 3.0))
.line_to(Vec2::new(7.0, 3.0))
.line_to(Vec2::new(7.0, 7.0))
.line_to(Vec2::new(3.0, 7.0))
.close();
b.build()
};
let mut t = Tessellator::new();
let nonzero = covered_area(t.fill(&build(FillRule::NonZero), DEFAULT_TOLERANCE));
let evenodd = covered_area(t.fill(&build(FillRule::EvenOdd), DEFAULT_TOLERANCE));
assert!((nonzero - 100.0).abs() < 0.5, "non-zero got {nonzero}");
assert!((evenodd - 84.0).abs() < 0.5, "even-odd got {evenodd}");
}
#[test]
fn polygon_area_matches_the_shoelace_result() {
let unit = [
Vec2::new(0.0, 0.0),
Vec2::new(2.0, 0.0),
Vec2::new(2.0, 3.0),
Vec2::new(0.0, 3.0),
];
assert!((polygon_area(&unit) - 6.0).abs() < 1e-5);
let reversed: Vec<_> = unit.iter().rev().copied().collect();
assert!((polygon_area(&reversed) - 6.0).abs() < 1e-5);
}
#[test]
fn a_curved_path_tessellates_to_roughly_its_true_area() {
let r = 10.0f32;
let k = 0.552_284_8 * r;
let mut b = PathBuilder::new();
b.move_to(Vec2::new(r, 0.0))
.cubic_to(Vec2::new(r, k), Vec2::new(k, r), Vec2::new(0.0, r))
.cubic_to(Vec2::new(-k, r), Vec2::new(-r, k), Vec2::new(-r, 0.0))
.cubic_to(Vec2::new(-r, -k), Vec2::new(-k, -r), Vec2::new(0.0, -r))
.cubic_to(Vec2::new(k, -r), Vec2::new(r, -k), Vec2::new(r, 0.0))
.close();
let mut t = Tessellator::new();
let area = covered_area(t.fill(&b.build(), 0.05));
let expected = std::f32::consts::PI * r * r;
assert!(
(area - expected).abs() / expected < 0.01,
"got {area}, expected about {expected}"
);
}
}
#[cfg(test)]
mod stroke_tests {
use super::*;
use crate::flatten::DEFAULT_TOLERANCE;
use crate::path::PathBuilder;
fn segment(len: f32) -> Path {
let mut b = PathBuilder::new();
b.move_to(Vec2::ZERO).line_to(Vec2::new(len, 0.0));
b.build()
}
#[test]
fn butt_cap_covers_exactly_length_times_width() {
let mut t = Tessellator::new();
let buffers = t.stroke(&segment(10.0), &StrokeStyle::new(2.0), DEFAULT_TOLERANCE);
assert!(buffers.is_well_formed());
let area = covered_area(buffers);
assert!((area - 20.0).abs() < 0.01, "expected 20, got {area}");
}
#[test]
fn round_cap_adds_a_disc_worth_of_area() {
let mut t = Tessellator::new();
let style = StrokeStyle::new(2.0).with_cap(LineCap::Round);
let area = covered_area(t.stroke(&segment(10.0), &style, 0.01));
let expected = 20.0 + std::f32::consts::PI;
assert!(
(area - expected).abs() < 0.1,
"expected about {expected}, got {area}"
);
}
#[test]
fn square_cap_extends_by_a_half_width_at_each_end() {
let mut t = Tessellator::new();
let style = StrokeStyle::new(2.0).with_cap(LineCap::Square);
let area = covered_area(t.stroke(&segment(10.0), &style, DEFAULT_TOLERANCE));
assert!((area - 24.0).abs() < 0.01, "expected 24, got {area}");
}
#[test]
fn caps_are_ordered_by_the_area_they_add() {
let mut t = Tessellator::new();
let path = segment(10.0);
let area = |cap| {
let mut t2 = Tessellator::new();
covered_area(t2.stroke(&path, &StrokeStyle::new(2.0).with_cap(cap), 0.01))
};
let (butt, round, square) = (
area(LineCap::Butt),
area(LineCap::Round),
area(LineCap::Square),
);
assert!(butt < round && round < square, "{butt} {round} {square}");
let _ = t.stroke(&path, &StrokeStyle::default(), DEFAULT_TOLERANCE);
}
#[test]
fn width_scales_area_linearly() {
let path = segment(10.0);
let mut t = Tessellator::new();
let narrow = covered_area(t.stroke(&path, &StrokeStyle::new(1.0), DEFAULT_TOLERANCE));
let wide = covered_area(t.stroke(&path, &StrokeStyle::new(4.0), DEFAULT_TOLERANCE));
assert!((wide - narrow * 4.0).abs() < 0.01, "{narrow} {wide}");
}
#[test]
fn an_invisible_stroke_produces_nothing() {
let mut t = Tessellator::new();
for width in [0.0, -2.0, f32::NAN] {
let buffers = t.stroke(&segment(10.0), &StrokeStyle::new(width), DEFAULT_TOLERANCE);
assert!(buffers.is_empty(), "width {width} produced geometry");
}
}
#[test]
fn an_empty_path_produces_nothing() {
let mut t = Tessellator::new();
assert!(t
.stroke(&Path::default(), &StrokeStyle::new(2.0), DEFAULT_TOLERANCE)
.is_empty());
}
fn corner(degrees: f32) -> Path {
let half = degrees.to_radians() / 2.0;
let r = 50.0;
let mut b = PathBuilder::new();
b.move_to(Vec2::new(half.sin(), half.cos()) * r)
.line_to(Vec2::ZERO)
.line_to(Vec2::new(-half.sin(), half.cos()) * r);
b.build()
}
fn join_area(path: &Path, join: LineJoin, limit: f32) -> f32 {
let mut t = Tessellator::new();
let style = StrokeStyle::new(3.0)
.with_join(join)
.with_miter_limit(limit);
covered_area(t.stroke(path, &style, 0.02))
}
#[test]
fn miter_degrades_to_bevel_at_the_svg_threshold() {
let sharper = corner(28.5);
assert_eq!(
join_area(&sharper, LineJoin::Miter, 4.0),
join_area(&sharper, LineJoin::Bevel, 4.0),
"below the threshold a miter join must produce bevel geometry"
);
let shallower = corner(29.5);
assert!(
join_area(&shallower, LineJoin::Miter, 4.0)
> join_area(&shallower, LineJoin::Bevel, 4.0),
"above the threshold the miter must survive"
);
}
#[test]
fn raising_the_limit_re_enables_a_miter_that_would_otherwise_bevel() {
let path = corner(20.0);
let bevelled = join_area(&path, LineJoin::Miter, 4.0);
let mitered = join_area(&path, LineJoin::Miter, 8.0);
assert!(
mitered > bevelled,
"a higher limit should keep the spike: {mitered} vs {bevelled}"
);
}
#[test]
fn bevel_covers_less_than_miter_where_the_miter_survives() {
let path = corner(90.0);
assert!(join_area(&path, LineJoin::Bevel, 4.0) < join_area(&path, LineJoin::Miter, 4.0));
}
#[test]
fn a_miter_limit_below_the_backend_minimum_does_not_panic() {
let path = corner(90.0);
for limit in [0.0, 0.5, 1.0, 1.9] {
let area = join_area(&path, LineJoin::Miter, limit);
assert!(area > 0.0, "limit {limit} produced no geometry");
}
}
#[test]
fn stroking_a_curve_does_not_pre_flatten_into_spurious_joins() {
let mut b = PathBuilder::new();
b.move_to(Vec2::new(0.0, 0.0)).cubic_to(
Vec2::new(0.0, 40.0),
Vec2::new(60.0, 40.0),
Vec2::new(60.0, 0.0),
);
let path = b.build();
let area = |limit: f32| {
let mut t = Tessellator::new();
let style = StrokeStyle::new(4.0)
.with_join(LineJoin::Miter)
.with_miter_limit(limit);
covered_area(t.stroke(&path, &style, 0.1))
};
let (tight, generous) = (area(1.0), area(10.0));
assert!(
(tight - generous).abs() / generous < 0.01,
"miter limit changed a smooth curve's area: {tight} vs {generous}"
);
}
#[test]
fn stroke_buffers_do_not_leak_between_calls() {
let mut t = Tessellator::new();
let big = t.stroke(&segment(100.0), &StrokeStyle::new(10.0), DEFAULT_TOLERANCE);
let big_tris = big.triangle_count();
assert!(big_tris > 0);
let small = t.stroke(&segment(1.0), &StrokeStyle::new(1.0), DEFAULT_TOLERANCE);
assert!(small.is_well_formed());
let area = covered_area(small);
assert!(
(area - 1.0).abs() < 0.01,
"stale geometry inflated area to {area}"
);
}
}
#[cfg(test)]
mod ear_clipping {
use super::*;
fn ell() -> Vec<Vec2> {
vec![
Vec2::new(0.0, 0.0),
Vec2::new(40.0, 0.0),
Vec2::new(40.0, 10.0),
Vec2::new(10.0, 10.0),
Vec2::new(10.0, 40.0),
Vec2::new(0.0, 40.0),
]
}
fn bowtie() -> Vec<Vec2> {
vec![
Vec2::new(0.0, 0.0),
Vec2::new(40.0, 40.0),
Vec2::new(40.0, 0.0),
Vec2::new(0.0, 40.0),
]
}
fn triangle_area_sum(out: &VertexBuffers) -> f32 {
out.indices
.chunks_exact(3)
.map(|t| {
let (a, b, c) = (
out.vertices[t[0] as usize],
out.vertices[t[1] as usize],
out.vertices[t[2] as usize],
);
((b - a).perp_dot(c - a) * 0.5).abs()
})
.sum()
}
#[test]
fn a_concave_simple_contour_is_ear_clipped() {
let points = ell();
let mut out = VertexBuffers::default();
assert!(ear_fill(&points, &mut out), "an L is simple and concave");
assert_eq!(out.vertices.len(), points.len());
assert_eq!(out.indices.len(), (points.len() - 2) * 3);
assert!((triangle_area_sum(&out) - 700.0).abs() < 0.01);
}
#[test]
fn fill_sends_a_concave_contour_to_ear_clipping() {
let points = ell();
let mut builder = Path::builder().with_fill_rule(FillRule::NonZero);
builder.move_to(points[0]);
for p in &points[1..] {
builder.line_to(*p);
}
builder.close();
let path = builder.build();
assert_eq!(path.convexity(), Convexity::Concave, "the L is concave");
let mut tess = Tessellator::new();
let filled = tess.fill(&path, 0.25);
assert_eq!(filled.vertices, points, "the contour's own vertices");
assert_eq!(filled.indices.len(), (points.len() - 2) * 3);
}
#[test]
fn a_self_intersecting_contour_is_refused() {
let mut out = VertexBuffers::default();
assert!(
!ear_fill(&bowtie(), &mut out),
"a bowtie must not pass verification"
);
assert!(out.indices.is_empty(), "a refusal writes nothing");
let mut builder = Path::builder().with_fill_rule(FillRule::NonZero);
builder.move_to(bowtie()[0]);
for p in &bowtie()[1..] {
builder.line_to(*p);
}
builder.close();
let mut tess = Tessellator::new();
let filled = tess.fill(&builder.build(), 0.25);
assert!(
(triangle_area_sum(filled) - 800.0).abs() < 1.0,
"two lobes of 400: {}",
triangle_area_sum(filled)
);
}
#[test]
fn a_contour_past_the_cap_is_declined() {
let big: Vec<Vec2> = (0..513)
.map(|i| {
let t = i as f32 * 0.01;
Vec2::new(t.cos() * 100.0, t.sin() * 100.0)
})
.collect();
let mut out = VertexBuffers::default();
assert!(!ear_fill(&big, &mut out), "past the cap");
assert!(out.indices.is_empty());
}
}
#[cfg(test)]
mod routes_agree_on_real_shapes {
use super::*;
use crate::superellipse::RoundSuperellipse;
fn area(out: &VertexBuffers) -> f64 {
out.indices
.chunks_exact(3)
.map(|t| {
let (a, b, c) = (
out.vertices[t[0] as usize],
out.vertices[t[1] as usize],
out.vertices[t[2] as usize],
);
f64::from(((b - a).perp_dot(c - a) * 0.5).abs())
})
.sum()
}
#[test]
fn a_rounded_superellipse_fills_the_same_area_either_route() {
for (size, radius) in [(100.0, 25.0), (100.0, 49.0), (240.0, 60.0), (64.0, 8.0)] {
let path = RoundSuperellipse::with_radius(
crate::Rect::new(Vec2::ZERO, Vec2::new(size, size)),
radius,
)
.to_path();
let mut tess = Tessellator::new();
let chosen = area(tess.fill(&path, 0.25));
let general = area(tess.fill_general(&path, 0.25));
assert!(
(chosen - general).abs() <= 0.001 * general.max(1.0),
"size {size} radius {radius}: chosen route fills {chosen}, general {general}"
);
}
}
}