Another project
0

Configure Feed

Select the types of activity you want to include in your feed.

at main 9.0 kB View raw
1use lyon_tessellation::{ 2 BuffersBuilder, FillOptions, FillTessellator, FillVertex, TessellationError, VertexBuffers, 3 path::{Path as LyonPath, builder::WithSvg, math::Point as LyonPoint, path::BuilderImpl}, 4}; 5use swash::{ 6 scale::outline::Outline, 7 zeno::{PathBuilder, PathData as _, Point as ZenoPoint}, 8}; 9 10const FILL_TOLERANCE_PX: f32 = 0.2; 11 12#[derive(Clone, Debug, Default, PartialEq)] 13pub struct TessellatedOutline { 14 pub vertices_px: Vec<[f32; 2]>, 15 pub indices: Vec<u32>, 16} 17 18impl TessellatedOutline { 19 #[must_use] 20 pub fn is_empty(&self) -> bool { 21 self.indices.is_empty() 22 } 23} 24 25pub fn append_outline(builder: &mut WithSvg<BuilderImpl>, outline: &Outline, offset: ZenoPoint) { 26 let mut adapter = ZenoToLyon::with_offset(builder, offset); 27 outline.path().copy_to(&mut adapter); 28} 29 30#[must_use] 31pub fn outline_to_path(outline: &Outline) -> LyonPath { 32 let mut builder = LyonPath::svg_builder(); 33 append_outline(&mut builder, outline, ZenoPoint::new(0.0, 0.0)); 34 builder.build() 35} 36 37pub fn tessellate_path( 38 path: &LyonPath, 39 fill: &mut FillTessellator, 40) -> Result<TessellatedOutline, TessellationError> { 41 let mut buffers: VertexBuffers<[f32; 2], u32> = VertexBuffers::new(); 42 fill.tessellate_path( 43 path, 44 &FillOptions::default().with_tolerance(FILL_TOLERANCE_PX), 45 &mut BuffersBuilder::new(&mut buffers, |v: FillVertex| { 46 [v.position().x, v.position().y] 47 }), 48 )?; 49 Ok(TessellatedOutline { 50 vertices_px: buffers.vertices, 51 indices: buffers.indices, 52 }) 53} 54 55struct ZenoToLyon<'a> { 56 builder: &'a mut WithSvg<BuilderImpl>, 57 offset: ZenoPoint, 58 current: ZenoPoint, 59 open: bool, 60} 61 62impl<'a> ZenoToLyon<'a> { 63 fn with_offset(builder: &'a mut WithSvg<BuilderImpl>, offset: ZenoPoint) -> Self { 64 Self { 65 builder, 66 offset, 67 current: ZenoPoint::new(0.0, 0.0), 68 open: false, 69 } 70 } 71 72 fn map(&self, p: ZenoPoint) -> LyonPoint { 73 LyonPoint::new(p.x + self.offset.x, p.y + self.offset.y) 74 } 75} 76 77impl PathBuilder for ZenoToLyon<'_> { 78 fn current_point(&self) -> ZenoPoint { 79 self.current 80 } 81 82 fn move_to(&mut self, to: impl Into<ZenoPoint>) -> &mut Self { 83 let p = to.into(); 84 self.current = p; 85 self.builder.move_to(self.map(p)); 86 self.open = true; 87 self 88 } 89 90 fn line_to(&mut self, to: impl Into<ZenoPoint>) -> &mut Self { 91 let p = to.into(); 92 self.current = p; 93 self.builder.line_to(self.map(p)); 94 self 95 } 96 97 fn quad_to(&mut self, control: impl Into<ZenoPoint>, to: impl Into<ZenoPoint>) -> &mut Self { 98 let c = control.into(); 99 let p = to.into(); 100 self.current = p; 101 self.builder.quadratic_bezier_to(self.map(c), self.map(p)); 102 self 103 } 104 105 fn curve_to( 106 &mut self, 107 control1: impl Into<ZenoPoint>, 108 control2: impl Into<ZenoPoint>, 109 to: impl Into<ZenoPoint>, 110 ) -> &mut Self { 111 let c1 = control1.into(); 112 let c2 = control2.into(); 113 let p = to.into(); 114 self.current = p; 115 self.builder 116 .cubic_bezier_to(self.map(c1), self.map(c2), self.map(p)); 117 self 118 } 119 120 fn close(&mut self) -> &mut Self { 121 if self.open { 122 self.builder.close(); 123 self.open = false; 124 } 125 self 126 } 127} 128 129#[cfg(test)] 130mod tests { 131 use super::{ 132 FILL_TOLERANCE_PX, TessellatedOutline, ZenoToLyon, append_outline, outline_to_path, 133 tessellate_path, 134 }; 135 use lyon_tessellation::{ 136 FillTessellator, 137 path::{Event, Path as LyonPath, math::Point as LyonPoint}, 138 }; 139 use swash::zeno::{PathBuilder, Point as ZenoPoint}; 140 141 fn zero() -> ZenoPoint { 142 ZenoPoint::new(0.0, 0.0) 143 } 144 145 #[test] 146 fn forwards_zero_offset_path_unchanged() { 147 let mut builder = LyonPath::svg_builder(); 148 { 149 let mut adapter = ZenoToLyon::with_offset(&mut builder, zero()); 150 adapter 151 .move_to(ZenoPoint::new(0.0, 0.0)) 152 .line_to(ZenoPoint::new(2.0, 0.0)) 153 .line_to(ZenoPoint::new(2.0, 1.0)) 154 .close(); 155 } 156 let path = builder.build(); 157 assert!(path.iter().count() >= 4); 158 } 159 160 #[test] 161 fn applies_offset_to_every_emitted_point() { 162 let mut shifted = LyonPath::svg_builder(); 163 { 164 let mut adapter = ZenoToLyon::with_offset(&mut shifted, ZenoPoint::new(10.0, -5.0)); 165 adapter 166 .move_to(ZenoPoint::new(0.0, 0.0)) 167 .line_to(ZenoPoint::new(1.0, 1.0)) 168 .close(); 169 } 170 let path = shifted.build(); 171 let xs_within_offset = path.iter().all(|event| { 172 let bbox = bounding_box(event); 173 bbox.iter().all(|(x, y)| *x >= 10.0 && *y >= -5.0) 174 }); 175 assert!( 176 xs_within_offset, 177 "every emitted point must respect the offset" 178 ); 179 } 180 181 fn bounding_box(event: Event<LyonPoint, LyonPoint>) -> Vec<(f32, f32)> { 182 match event { 183 Event::Begin { at } | Event::End { last: at, .. } => vec![(at.x, at.y)], 184 Event::Line { from, to } => vec![(from.x, from.y), (to.x, to.y)], 185 Event::Quadratic { from, ctrl, to } => { 186 vec![(from.x, from.y), (ctrl.x, ctrl.y), (to.x, to.y)] 187 } 188 Event::Cubic { 189 from, 190 ctrl1, 191 ctrl2, 192 to, 193 } => vec![ 194 (from.x, from.y), 195 (ctrl1.x, ctrl1.y), 196 (ctrl2.x, ctrl2.y), 197 (to.x, to.y), 198 ], 199 } 200 } 201 202 #[test] 203 fn current_point_tracks_last_emitted_point() { 204 let mut builder = LyonPath::svg_builder(); 205 let mut adapter = ZenoToLyon::with_offset(&mut builder, zero()); 206 adapter.move_to(ZenoPoint::new(3.0, 4.0)); 207 let cp = adapter.current_point(); 208 assert!((cp.x - 3.0).abs() < f32::EPSILON); 209 assert!((cp.y - 4.0).abs() < f32::EPSILON); 210 adapter.line_to(ZenoPoint::new(7.0, 8.0)); 211 let cp = adapter.current_point(); 212 assert!((cp.x - 7.0).abs() < f32::EPSILON); 213 assert!((cp.y - 8.0).abs() < f32::EPSILON); 214 } 215 216 #[test] 217 fn close_without_open_subpath_is_noop() { 218 let mut builder = LyonPath::svg_builder(); 219 let mut adapter = ZenoToLyon::with_offset(&mut builder, zero()); 220 adapter.close(); 221 let _ = builder.build(); 222 } 223 224 #[test] 225 fn fill_tolerance_is_subpixel_default() { 226 const _: () = assert!(FILL_TOLERANCE_PX > 0.0); 227 const _: () = assert!(FILL_TOLERANCE_PX < 1.0); 228 } 229 230 #[test] 231 fn tessellate_path_emits_triangles_for_filled_quad() { 232 let mut builder = LyonPath::svg_builder(); 233 { 234 let mut adapter = ZenoToLyon::with_offset(&mut builder, zero()); 235 adapter 236 .move_to(ZenoPoint::new(0.0, 0.0)) 237 .line_to(ZenoPoint::new(1.0, 0.0)) 238 .line_to(ZenoPoint::new(1.0, 1.0)) 239 .line_to(ZenoPoint::new(0.0, 1.0)) 240 .close(); 241 } 242 let path = builder.build(); 243 let mut fill = FillTessellator::new(); 244 let Ok(result) = tessellate_path(&path, &mut fill) else { 245 panic!("tessellation must succeed for a filled quad"); 246 }; 247 assert!(!result.is_empty()); 248 assert!(result.indices.len().is_multiple_of(3)); 249 assert!( 250 result 251 .vertices_px 252 .iter() 253 .all(|[x, y]| { (0.0..=1.0).contains(x) && (0.0..=1.0).contains(y) }) 254 ); 255 } 256 257 #[test] 258 fn tessellate_empty_path_returns_empty_result() { 259 let path = LyonPath::svg_builder().build(); 260 let mut fill = FillTessellator::new(); 261 let Ok(result) = tessellate_path(&path, &mut fill) else { 262 panic!("empty path tessellation must succeed"); 263 }; 264 assert!(result.is_empty()); 265 assert!(result.vertices_px.is_empty()); 266 } 267 268 #[test] 269 fn tessellated_outline_default_is_empty() { 270 let t = TessellatedOutline::default(); 271 assert!(t.is_empty()); 272 assert!(t.vertices_px.is_empty()); 273 assert!(t.indices.is_empty()); 274 } 275 276 #[test] 277 fn append_outline_into_lyon_uses_offset() { 278 use swash::scale::outline::Outline; 279 let outline = Outline::new(); 280 let mut builder = LyonPath::svg_builder(); 281 append_outline(&mut builder, &outline, ZenoPoint::new(5.0, 6.0)); 282 let path = builder.build(); 283 assert_eq!(path.iter().count(), 0, "empty outline emits no events"); 284 } 285 286 #[test] 287 fn outline_to_path_handles_empty_outline() { 288 use swash::scale::outline::Outline; 289 let outline = Outline::new(); 290 let path = outline_to_path(&outline); 291 assert_eq!(path.iter().count(), 0); 292 } 293}