Skip to main content

agg_gui/gl_renderer/
aa_texture_mesh.rs

1//! Texture-based AA mesh — direct port of MatterCAD agg-sharp's
2//! `AARenderTesselator.cs`.
3//!
4//! # Why this exists
5//!
6//! The older [`super::tessellate_path_aa`] in this crate generates a
7//! 1-pixel-wide halo strip with per-vertex alpha 1 → 0 *across the
8//! strip*.  For ADJACENT polygons that share an edge (the pixel
9//! alignment test, or any tiled rect) one polygon's outward halo
10//! paints α≈0.5 into the neighbour's interior, mixing white + black
11//! to gray under SrcOver — the long-standing pixel-test bug.
12//!
13//! AGG-sharp's `AARenderTesselator` solves this by routing the AA
14//! through a **1024-wide alpha-step texture** (col 0 α=0, cols 1+
15//! α=opaque) sampled with LINEAR filtering.  Each AA edge produces a
16//! triangle fan whose texture coordinates are arranged so that:
17//!
18//! - The polygon edge vertices map to `U = 1/1023` — exactly between
19//!   texel 0 and texel 1's centres, so the LINEAR filter returns
20//!   α ≈ 0.5 right ON the polygon edge.
21//! - The 1-unit-outward extruded vertices map to `U = 0` — at texel
22//!   0 (α = 0).
23//! - The non-AA interior vertex maps to `U = (1 + edgeDotP3) / 1023`
24//!   — well past texel 1, so the interior renders at α = opaque.
25//!
26//! With this geometry the alpha transition lives *entirely within one
27//! texel*, which in screen space maps to about half a pixel of fade
28//! centred on the polygon edge.  No outward strip painting alpha
29//! beyond the polygon's true extent, no bleed into neighbours.
30//!
31//! # Driver: per-triangle edge-flag count (RenderLastToGL)
32//!
33//! `tess2` emits triangles with per-vertex edge flags telling us
34//! whether the edge starting at that vertex is on the original
35//! polygon boundary.  Sum across the three vertices to get the
36//! triangle's AA-edge count:
37//!
38//! - 0 → [`draw_non_aa_triangle`] (interior triangle, sampled at high
39//!   U so α = opaque everywhere)
40//! - 1 → [`draw_1_edge_triangle`] with the right vertex permutation
41//! - 2 → [`draw_2_edge_triangle`] — two AA edges + non-AA filler
42//! - 3 → [`draw_3_edge_triangle`] — three AA edges meeting at the
43//!   centroid
44
45use crate::draw_ctx::FillRule;
46use crate::gl_renderer::tess2_bridge::{
47    agg_path_to_contours, to_tess_winding_rule, try_tessellate,
48};
49use agg_rust::basics::VertexSource;
50
51/// One vertex of an AA-texture mesh: position in path/screen space + a
52/// 2-D texcoord into the 1024-wide alpha-step texture.
53///
54/// `#[repr(C)]` with no padding — four `f32`s = 16 bytes per vertex —
55/// so callers can `bytemuck::cast_slice` straight from this slice.
56#[repr(C)]
57#[derive(Clone, Copy, Debug, Default, PartialEq)]
58pub struct AaTexVertex {
59    pub pos: [f32; 2],
60    pub uv: [f32; 2],
61}
62
63/// Tessellate a path into a mesh ready for the AGG-sharp-style
64/// texture-AA pipeline.
65///
66/// Mirrors `AARenderTesselator::RenderLastToGL`: per-triangle the
67/// boundary-edge count from tess2 selects the appropriate emit
68/// helper.  No CPU-side halo strip; no per-vertex alpha attribute.
69pub fn tessellate_path_aa_texture<VS: VertexSource>(
70    path: &mut VS,
71    fill_rule: FillRule,
72) -> Option<(Vec<AaTexVertex>, Vec<u32>)> {
73    let contours = agg_path_to_contours(path);
74    if contours.is_empty() {
75        return None;
76    }
77
78    struct TessOut {
79        verts: Vec<f32>,
80        indices: Vec<u32>,
81        flags: Vec<u8>,
82        vcount: usize,
83    }
84    let out = try_tessellate(
85        &contours,
86        to_tess_winding_rule(fill_rule),
87        "tessellate_path_aa_texture",
88        |tess| {
89            Some(TessOut {
90                verts: tess.vertices().iter().map(|&v| v as f32).collect(),
91                indices: tess.elements().to_vec(),
92                flags: tess.edge_flags().to_vec(),
93                vcount: tess.vertex_count(),
94            })
95        },
96    )?;
97
98    let in_verts = &out.verts;
99    let in_indices = &out.indices;
100    let edge_flags = &out.flags;
101
102    let n_interior = out.vcount;
103    let n_indices = in_indices.len();
104    if n_indices == 0 {
105        return None;
106    }
107
108    let mut out_verts: Vec<AaTexVertex> = Vec::with_capacity(n_indices * 5);
109    let mut out_indices: Vec<u32> = Vec::with_capacity(n_indices * 3);
110
111    let n_tris = n_indices / 3;
112    for t in 0..n_tris {
113        let ia = in_indices[t * 3] as usize;
114        let ib = in_indices[t * 3 + 1] as usize;
115        let ic = in_indices[t * 3 + 2] as usize;
116        if ia >= n_interior || ib >= n_interior || ic >= n_interior {
117            continue;
118        }
119        let v0 = [in_verts[ia * 2], in_verts[ia * 2 + 1]];
120        let v1 = [in_verts[ib * 2], in_verts[ib * 2 + 1]];
121        let v2 = [in_verts[ic * 2], in_verts[ic * 2 + 1]];
122
123        let e0 = edge_flags.get(t * 3).copied().unwrap_or(0);
124        let e1 = edge_flags.get(t * 3 + 1).copied().unwrap_or(0);
125        let e2 = edge_flags.get(t * 3 + 2).copied().unwrap_or(0);
126
127        match e0 + e1 + e2 {
128            0 => draw_non_aa_triangle(&mut out_verts, &mut out_indices, v0, v1, v2),
129            1 => {
130                if e0 == 1 {
131                    draw_1_edge_triangle(&mut out_verts, &mut out_indices, v0, v1, v2);
132                } else if e1 == 1 {
133                    draw_1_edge_triangle(&mut out_verts, &mut out_indices, v1, v2, v0);
134                } else {
135                    draw_1_edge_triangle(&mut out_verts, &mut out_indices, v2, v0, v1);
136                }
137            }
138            2 => {
139                if e0 == 1 {
140                    if e1 == 1 {
141                        draw_2_edge_triangle(&mut out_verts, &mut out_indices, v0, v1, v2);
142                    } else {
143                        draw_2_edge_triangle(&mut out_verts, &mut out_indices, v2, v0, v1);
144                    }
145                } else {
146                    draw_2_edge_triangle(&mut out_verts, &mut out_indices, v1, v2, v0);
147                }
148            }
149            3 => draw_3_edge_triangle(&mut out_verts, &mut out_indices, v0, v1, v2),
150            _ => {}
151        }
152    }
153
154    Some((out_verts, out_indices))
155}
156
157/// Emit an interior triangle whose three vertices all sample at high
158/// U (≫ 1/1023) — the alpha-step texture returns α = opaque there, so
159/// the triangle renders at full polygon color with no edge AA.
160///
161/// Mirrors `AARenderTesselator::DrawNonAATriangle` — same exact texcoords.
162fn draw_non_aa_triangle(
163    verts: &mut Vec<AaTexVertex>,
164    indices: &mut Vec<u32>,
165    p0: [f32; 2],
166    p1: [f32; 2],
167    p2: [f32; 2],
168) {
169    let base = verts.len() as u32;
170    verts.push(AaTexVertex {
171        pos: p0,
172        uv: [0.2, 0.25],
173    });
174    verts.push(AaTexVertex {
175        pos: p1,
176        uv: [0.2, 0.75],
177    });
178    verts.push(AaTexVertex {
179        pos: p2,
180        uv: [0.9, 0.5],
181    });
182    indices.extend_from_slice(&[base, base + 1, base + 2]);
183}
184
185/// Emit a triangle fan for ONE anti-aliased edge.  Direct port of
186/// `AARenderTesselator::Draw1EdgeTriangle`.
187///
188/// `aa_p0 → aa_p1` is the AA edge (a polygon boundary); `non_aa_point`
189/// is the triangle's third vertex (an interior point).  We extrude
190/// the AA edge 1 unit OUTWARD (away from `non_aa_point`) and emit a
191/// fan anchored at `aa_p0` with three triangles:
192///
193/// 1. `(aa_p0, p0_offset, p1_offset)` — first half of the extruded strip
194/// 2. `(aa_p0, p1_offset, aa_p1)` — second half of the extruded strip
195/// 3. `(aa_p0, aa_p1, non_aa_point)` — the original triangle interior
196///
197/// Texcoords are arranged so the alpha-step texture's α = 0 column lands
198/// at the extruded edge and the α = opaque columns cover the interior.
199fn draw_1_edge_triangle(
200    verts: &mut Vec<AaTexVertex>,
201    indices: &mut Vec<u32>,
202    aa_p0: [f32; 2],
203    aa_p1: [f32; 2],
204    non_aa_point: [f32; 2],
205) {
206    if aa_p0 == aa_p1 || aa_p1 == non_aa_point || non_aa_point == aa_p0 {
207        return;
208    }
209
210    let edge = [aa_p1[0] - aa_p0[0], aa_p1[1] - aa_p0[1]];
211    let len = (edge[0] * edge[0] + edge[1] * edge[1]).sqrt();
212    if len < 1e-6 {
213        return;
214    }
215    let edge_n = [edge[0] / len, edge[1] / len];
216
217    // PerpendicularRight in agg-sharp Vector2 convention: (x, y) → (y, -x).
218    let mut normal = [edge_n[1], -edge_n[0]];
219    let dot_n_third =
220        normal[0] * (non_aa_point[0] - aa_p0[0]) + normal[1] * (non_aa_point[1] - aa_p0[1]);
221    let edge_dot_p3 = if dot_n_third < 0.0 {
222        -dot_n_third
223    } else {
224        // Flip the normal so it points AWAY from non_aa_point — same
225        // sign-fix as AGG-sharp's `Draw1EdgeTriangle`.
226        normal = [-normal[0], -normal[1]];
227        dot_n_third
228    };
229
230    // 1-unit outward offset.  `tessellate_path_aa_texture` is called with
231    // the path already CTM-transformed on the CPU side (same convention
232    // as the legacy halo path), so one path unit = one screen pixel for
233    // identity scale.
234    let p0_offset = [aa_p0[0] + normal[0], aa_p0[1] + normal[1]];
235    let p1_offset = [aa_p1[0] + normal[0], aa_p1[1] + normal[1]];
236
237    // Same texcoord constants as agg-sharp.  `1/1023` puts the polygon
238    // edge between texel 0 (α=0) and texel 1 (α=opaque) of the
239    // 1024-wide step texture — under LINEAR filtering that produces
240    // the analytic α≈0.5 transition right at the geometric edge.
241    let inv_1023 = 1.0 / 1023.0_f32;
242    let tex_p0 = [inv_1023, 0.25];
243    let tex_p1 = [inv_1023, 0.75];
244    let tex_p2 = [(1.0 + edge_dot_p3) * inv_1023, 0.25];
245    let tex_p0_off = [0.0, 0.25];
246    let tex_p1_off = [0.0, 0.75];
247
248    let base = verts.len() as u32;
249    verts.push(AaTexVertex {
250        pos: aa_p0,
251        uv: tex_p0,
252    });
253    verts.push(AaTexVertex {
254        pos: p0_offset,
255        uv: tex_p0_off,
256    });
257    verts.push(AaTexVertex {
258        pos: p1_offset,
259        uv: tex_p1_off,
260    });
261    verts.push(AaTexVertex {
262        pos: aa_p1,
263        uv: tex_p1,
264    });
265    verts.push(AaTexVertex {
266        pos: non_aa_point,
267        uv: tex_p2,
268    });
269    indices.extend_from_slice(&[
270        base,
271        base + 1,
272        base + 2,
273        base,
274        base + 2,
275        base + 3,
276        base,
277        base + 3,
278        base + 4,
279    ]);
280}
281
282/// Two adjacent edges are AA: `p0→p1` and `p1→p2`.  `p2→p0` is interior.
283///
284/// Strategy is a direct port of agg-sharp's `Draw2EdgeTriangle`: emit a
285/// Draw1Edge fan for each AA edge with a non-AA point placed just
286/// inward of that edge's midpoint (so the fan's "interior triangle"
287/// is essentially degenerate — covers a sliver near the edge only).
288/// Then a non-AA triangle covers the full polygon interior.
289fn draw_2_edge_triangle(
290    verts: &mut Vec<AaTexVertex>,
291    indices: &mut Vec<u32>,
292    p0: [f32; 2],
293    p1: [f32; 2],
294    p2: [f32; 2],
295) {
296    let centroid = [(p0[0] + p1[0] + p2[0]) / 3.0, (p0[1] + p1[1] + p2[1]) / 3.0];
297    let mid_p0p1 = [(p0[0] + p1[0]) * 0.5, (p0[1] + p1[1]) * 0.5];
298    let mid_p1p2 = [(p1[0] + p2[0]) * 0.5, (p1[1] + p2[1]) * 0.5];
299    // Same `.001` fudge agg-sharp uses so the inner point isn't
300    // co-linear with the AA edge (which would zero out the fan).
301    let inner_p0p1 = [
302        mid_p0p1[0] + (centroid[0] - mid_p0p1[0]) * 0.001,
303        mid_p0p1[1] + (centroid[1] - mid_p0p1[1]) * 0.001,
304    ];
305    let inner_p1p2 = [
306        mid_p1p2[0] + (centroid[0] - mid_p1p2[0]) * 0.001,
307        mid_p1p2[1] + (centroid[1] - mid_p1p2[1]) * 0.001,
308    ];
309    draw_1_edge_triangle(verts, indices, p0, p1, inner_p0p1);
310    draw_1_edge_triangle(verts, indices, p1, p2, inner_p1p2);
311    draw_non_aa_triangle(verts, indices, p0, p1, p2);
312}
313
314/// All three edges are AA — port of `Draw3EdgeTriangle`.  Each edge
315/// gets its own Draw1Edge fan with the centroid as the non-AA point;
316/// together the three fans tile the triangle interior exactly.
317fn draw_3_edge_triangle(
318    verts: &mut Vec<AaTexVertex>,
319    indices: &mut Vec<u32>,
320    p0: [f32; 2],
321    p1: [f32; 2],
322    p2: [f32; 2],
323) {
324    let centroid = [(p0[0] + p1[0] + p2[0]) / 3.0, (p0[1] + p1[1] + p2[1]) / 3.0];
325    draw_1_edge_triangle(verts, indices, p0, p1, centroid);
326    draw_1_edge_triangle(verts, indices, p1, p2, centroid);
327    draw_1_edge_triangle(verts, indices, p2, p0, centroid);
328}