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}