use crate::draw_ctx::FillRule;
use agg_rust::basics::{is_end_poly, is_move_to, is_stop, VertexSource};
use tess2_rust::{ElementType, Tessellator, WindingRule};
#[cfg(target_arch = "wasm32")]
use std::cell::RefCell;
#[cfg(target_arch = "wasm32")]
struct TessContext {
contours: Vec<Vec<[f32; 2]>>,
winding: WindingRule,
label: &'static str,
}
#[cfg(target_arch = "wasm32")]
thread_local! {
static LAST_TESS_CONTEXT: RefCell<Option<TessContext>> = const { RefCell::new(None) };
}
fn log_tess_repro(contours: &[Vec<[f32; 2]>], winding: WindingRule, label: &str) {
use std::fmt::Write;
let total: usize = contours.iter().map(|c| c.len()).sum();
let mut buf = String::new();
let _ = writeln!(
buf,
"tess2 repro for {label} — winding={winding:?}, contours={}, points={}:",
contours.len(),
total
);
for (i, c) in contours.iter().enumerate() {
let _ = write!(buf, " contour[{i}] (n={}) [", c.len());
for (j, pt) in c.iter().enumerate() {
if j > 0 {
buf.push_str(", ");
}
let _ = write!(buf, "({:.6}, {:.6})", pt[0], pt[1]);
}
let _ = writeln!(buf, "]");
}
#[cfg(target_arch = "wasm32")]
{
web_sys::console::error_1(&wasm_bindgen::JsValue::from_str(&buf));
}
#[cfg(not(target_arch = "wasm32"))]
{
eprintln!("{buf}");
}
}
pub fn install_tess_panic_logger() {
#[cfg(target_arch = "wasm32")]
{
let prev = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
LAST_TESS_CONTEXT.with(|cell| {
if let Ok(borrow) = cell.try_borrow() {
if let Some(ctx) = borrow.as_ref() {
log_tess_repro(&ctx.contours, ctx.winding, ctx.label);
}
}
});
prev(info);
}));
}
}
pub(crate) fn try_tessellate<T, F>(
contours: &[Vec<[f32; 2]>],
winding: WindingRule,
label: &'static str,
extract: F,
) -> Option<T>
where
F: FnOnce(&Tessellator) -> Option<T>,
{
use std::panic::{catch_unwind, AssertUnwindSafe};
if contours.is_empty() {
return None;
}
#[cfg(target_arch = "wasm32")]
LAST_TESS_CONTEXT.with(|cell| {
*cell.borrow_mut() = Some(TessContext {
contours: contours.to_vec(),
winding,
label,
});
});
let result = catch_unwind(AssertUnwindSafe(|| {
let mut tess = Tessellator::new();
for c in contours {
let flat: Vec<f64> = c.iter().flat_map(|v| [v[0] as f64, v[1] as f64]).collect();
tess.add_contour(2, &flat);
}
let ok = tess.tessellate(winding, ElementType::Polygons, 3, 2, None);
if !ok || tess.vertex_count() == 0 {
return None;
}
extract(&tess)
}));
#[cfg(target_arch = "wasm32")]
LAST_TESS_CONTEXT.with(|cell| {
*cell.borrow_mut() = None;
});
match result {
Ok(v) => v,
Err(_payload) => {
log_tess_repro(contours, winding, label);
None
}
}
}
pub fn agg_path_to_contours<VS: VertexSource>(path: &mut VS) -> Vec<Vec<[f32; 2]>> {
let mut out: Vec<Vec<[f32; 2]>> = Vec::new();
let mut cur: Vec<[f32; 2]> = Vec::new();
path.rewind(0);
loop {
let (mut x, mut y) = (0.0, 0.0);
let cmd = path.vertex(&mut x, &mut y);
if is_stop(cmd) {
break;
}
if is_move_to(cmd) {
if !cur.is_empty() {
push_contour(&mut out, std::mem::take(&mut cur));
}
cur.push([x as f32, y as f32]);
} else if is_end_poly(cmd) {
if !cur.is_empty() {
push_contour(&mut out, std::mem::take(&mut cur));
}
} else {
cur.push([x as f32, y as f32]);
}
}
if !cur.is_empty() {
push_contour(&mut out, cur);
}
out
}
fn push_contour(out: &mut Vec<Vec<[f32; 2]>>, mut contour: Vec<[f32; 2]>) {
contour = deduplicate_contour_v(&contour);
if contour.len() < 3 {
return;
}
if signed_area_2x(&contour).abs() < 1.0 {
return;
}
out.push(contour);
}
pub fn tessellate_path<VS: VertexSource>(path: &mut VS) -> Option<(Vec<f32>, Vec<u32>)> {
let contours = agg_path_to_contours(path);
if contours.is_empty() {
return None;
}
tessellate_fill(&contours)
}
#[derive(Clone, Debug)]
pub struct CachedTess {
pub vertices: Vec<f32>,
pub indices: Vec<u32>,
pub edge_flags: Vec<u8>,
}
pub fn tessellate_interior<VS: VertexSource>(path: &mut VS) -> Option<CachedTess> {
let contours = agg_path_to_contours(path);
try_tessellate(&contours, WindingRule::Odd, "tessellate_interior", |tess| {
Some(CachedTess {
vertices: tess.vertices().iter().map(|&v| v as f32).collect(),
indices: tess.elements().to_vec(),
edge_flags: tess.edge_flags().to_vec(),
})
})
}
pub fn expand_aa_halo(
transformed_xy: &[f32],
cached: &CachedTess,
halo_px: f32,
) -> Option<(Vec<[f32; 3]>, Vec<u32>)> {
let n_interior = transformed_xy.len() / 2;
let n_indices = cached.indices.len();
if n_indices == 0 {
return None;
}
let mut out_verts: Vec<[f32; 3]> = Vec::with_capacity(n_interior + n_indices * 4);
let mut out_indices: Vec<u32> = Vec::with_capacity(n_indices + n_indices * 2);
for i in 0..n_interior {
out_verts.push([transformed_xy[i * 2], transformed_xy[i * 2 + 1], 1.0]);
}
out_indices.extend_from_slice(&cached.indices);
let n_tris = n_indices / 3;
for t in 0..n_tris {
let ia = cached.indices[t * 3] as usize;
let ib = cached.indices[t * 3 + 1] as usize;
let ic = cached.indices[t * 3 + 2] as usize;
if ia >= n_interior || ib >= n_interior || ic >= n_interior {
continue;
}
let p = [
[transformed_xy[ia * 2], transformed_xy[ia * 2 + 1]],
[transformed_xy[ib * 2], transformed_xy[ib * 2 + 1]],
[transformed_xy[ic * 2], transformed_xy[ic * 2 + 1]],
];
let flag = [
cached.edge_flags.get(t * 3).copied().unwrap_or(0),
cached.edge_flags.get(t * 3 + 1).copied().unwrap_or(0),
cached.edge_flags.get(t * 3 + 2).copied().unwrap_or(0),
];
for k in 0..3 {
if flag[k] == 0 {
continue;
}
let a = p[k];
let b = p[(k + 1) % 3];
let c = p[(k + 2) % 3];
let dx = b[0] - a[0];
let dy = b[1] - a[1];
let len = (dx * dx + dy * dy).sqrt();
if len < 1e-6 {
continue;
}
let mut nx = dy / len * halo_px;
let mut ny = -dx / len * halo_px;
let dot_c = nx * (c[0] - a[0]) + ny * (c[1] - a[1]);
if dot_c > 0.0 {
nx = -nx;
ny = -ny;
}
let base = out_verts.len() as u32;
out_verts.push([a[0], a[1], 1.0]);
out_verts.push([b[0], b[1], 1.0]);
out_verts.push([a[0] + nx, a[1] + ny, 0.0]);
out_verts.push([b[0] + nx, b[1] + ny, 0.0]);
out_indices.extend_from_slice(&[
base,
base + 1,
base + 2,
base + 1,
base + 3,
base + 2,
]);
}
}
Some((out_verts, out_indices))
}
pub fn tessellate_path_aa<VS: VertexSource>(
path: &mut VS,
halo_px: f32,
fill_rule: FillRule,
) -> Option<(Vec<[f32; 3]>, Vec<u32>)> {
let contours = agg_path_to_contours(path);
if contours.is_empty() {
return None;
}
struct TessOut {
verts: Vec<f32>,
indices: Vec<u32>,
flags: Vec<u8>,
vcount: usize,
}
let out = try_tessellate(
&contours,
to_tess_winding_rule(fill_rule),
"tessellate_path_aa",
|tess| {
Some(TessOut {
verts: tess.vertices().iter().map(|&v| v as f32).collect(),
indices: tess.elements().to_vec(),
flags: tess.edge_flags().to_vec(),
vcount: tess.vertex_count(),
})
},
)?;
let in_verts: &[f32] = &out.verts; let in_indices: &[u32] = &out.indices; let edge_flags: &[u8] = &out.flags;
let n_interior = out.vcount;
let n_indices = in_indices.len();
if n_indices == 0 {
return None;
}
let mut out_verts: Vec<[f32; 3]> = Vec::with_capacity(n_interior + n_indices * 4);
let mut out_indices: Vec<u32> = Vec::with_capacity(n_indices + n_indices * 2);
for i in 0..n_interior {
out_verts.push([in_verts[i * 2], in_verts[i * 2 + 1], 1.0]);
}
out_indices.extend_from_slice(in_indices);
let n_tris = n_indices / 3;
for t in 0..n_tris {
let ia = in_indices[t * 3] as usize;
let ib = in_indices[t * 3 + 1] as usize;
let ic = in_indices[t * 3 + 2] as usize;
if ia >= n_interior || ib >= n_interior || ic >= n_interior {
continue;
}
let p = [
[in_verts[ia * 2], in_verts[ia * 2 + 1]],
[in_verts[ib * 2], in_verts[ib * 2 + 1]],
[in_verts[ic * 2], in_verts[ic * 2 + 1]],
];
let flag = [
edge_flags.get(t * 3).copied().unwrap_or(0),
edge_flags.get(t * 3 + 1).copied().unwrap_or(0),
edge_flags.get(t * 3 + 2).copied().unwrap_or(0),
];
for k in 0..3 {
if flag[k] == 0 {
continue;
}
let a = p[k];
let b = p[(k + 1) % 3];
let c = p[(k + 2) % 3]; let dx = b[0] - a[0];
let dy = b[1] - a[1];
let len = (dx * dx + dy * dy).sqrt();
if len < 1e-6 {
continue;
}
let mut nx = dy / len * halo_px;
let mut ny = -dx / len * halo_px;
let dot_c = nx * (c[0] - a[0]) + ny * (c[1] - a[1]);
if dot_c > 0.0 {
nx = -nx;
ny = -ny;
}
let base = out_verts.len() as u32;
out_verts.push([a[0], a[1], 1.0]); out_verts.push([b[0], b[1], 1.0]); out_verts.push([a[0] + nx, a[1] + ny, 0.0]); out_verts.push([b[0] + nx, b[1] + ny, 0.0]); out_indices.extend_from_slice(&[
base,
base + 1,
base + 2,
base + 1,
base + 3,
base + 2,
]);
}
}
Some((out_verts, out_indices))
}
pub(crate) fn to_tess_winding_rule(fill_rule: FillRule) -> WindingRule {
match fill_rule {
FillRule::NonZero => WindingRule::NonZero,
FillRule::EvenOdd => WindingRule::Odd,
}
}
pub fn tessellate_fill(contours: &[Vec<[f32; 2]>]) -> Option<(Vec<f32>, Vec<u32>)> {
if contours.is_empty() {
return None;
}
let cleaned: Vec<Vec<[f32; 2]>> = contours
.iter()
.filter_map(|contour| {
if contour.len() < 3 {
return None;
}
let c = deduplicate_contour(contour);
if c.len() < 3 {
return None;
}
if signed_area_2x(&c).abs() < 1.0 {
return None;
}
Some(c)
})
.collect();
try_tessellate(
&cleaned,
WindingRule::Odd,
"tessellate_fill",
|tess| {
Some((
tess.vertices().iter().map(|&v| v as f32).collect(),
tess.elements().to_vec(),
))
},
)
}
fn signed_area_2x(pts: &[[f32; 2]]) -> f32 {
let n = pts.len();
let mut a = 0.0f32;
for i in 0..n {
let j = (i + 1) % n;
a += pts[i][0] * pts[j][1] - pts[j][0] * pts[i][1];
}
a
}
fn deduplicate_contour(pts: &[[f32; 2]]) -> Vec<[f32; 2]> {
deduplicate_contour_v(pts)
}
fn deduplicate_contour_v(pts: &[[f32; 2]]) -> Vec<[f32; 2]> {
let mut out: Vec<[f32; 2]> = Vec::with_capacity(pts.len());
for &pt in pts {
match out.last() {
Some(&prev) if prev == pt => {}
_ => out.push(pt),
}
}
if out.len() >= 2 && out.first() == out.last() {
out.pop();
}
out
}
pub fn tessellate_rect(x: f32, y: f32, w: f32, h: f32) -> Option<(Vec<f32>, Vec<u32>)> {
let contour = vec![[x, y], [x + w, y], [x + w, y + h], [x, y + h]];
tessellate_fill(&[contour])
}
pub fn tessellate_rounded_rect(
x: f32,
y: f32,
w: f32,
h: f32,
r: f32,
segments: usize,
) -> Option<(Vec<f32>, Vec<u32>)> {
let r = r.min(w * 0.5).min(h * 0.5);
let seg = segments.max(3);
let mut contour: Vec<[f32; 2]> = Vec::with_capacity(seg * 4 + 4);
use std::f32::consts::PI;
let corners = [
(x + w - r, y + r, -PI * 0.5, 0.0), (x + w - r, y + h - r, 0.0, PI * 0.5), (x + r, y + h - r, PI * 0.5, PI), (x + r, y + r, PI, PI * 1.5), ];
for &(cx, cy, start, end) in &corners {
for i in 0..=seg {
let t = i as f32 / seg as f32;
let angle = start + t * (end - start);
contour.push([cx + angle.cos() * r, cy + angle.sin() * r]);
}
}
tessellate_fill(&[contour])
}
pub fn tessellate_circle(
cx: f32,
cy: f32,
r: f32,
segments: usize,
) -> Option<(Vec<f32>, Vec<u32>)> {
let seg = segments.max(8);
use std::f32::consts::TAU;
let contour: Vec<[f32; 2]> = (0..seg)
.map(|i| {
let angle = i as f32 / seg as f32 * TAU;
[cx + angle.cos() * r, cy + angle.sin() * r]
})
.collect();
tessellate_fill(&[contour])
}