Another project
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}