maplibre/sdf/
tessellation.rs

1//! Tessellation for lines and polygons is implemented here.
2
3use csscolorparser::Color;
4use geozero::{ColumnValue, FeatureProcessor, GeomProcessor, PropertyProcessor};
5use lyon::{
6    geom::euclid::{Box2D, Point2D},
7    tessellation::{
8        geometry_builder::MaxIndex, BuffersBuilder, FillOptions, FillTessellator, VertexBuffers,
9    },
10};
11
12use crate::{
13    legacy::TileSpace,
14    render::shaders::ShaderSymbolVertex,
15    sdf::{
16        text::{Anchor, Glyph, GlyphSet, SymbolVertexBuilder},
17        Feature,
18    },
19};
20
21/// Vertex buffers index data type.
22pub type IndexDataType = u32; // Must match INDEX_FORMAT
23
24type GeoResult<T> = geozero::error::Result<T>;
25
26/// Build tessellations with vectors.
27pub struct TextTessellator<I: std::ops::Add + From<lyon::tessellation::VertexId> + MaxIndex> {
28    glyphs: GlyphSet,
29
30    // output
31    pub quad_buffer: VertexBuffers<ShaderSymbolVertex, I>,
32    pub features: Vec<Feature>,
33
34    // iteration variables
35    current_index: usize,
36    current_text: Option<String>,
37    current_origin: Option<Box2D<f32, TileSpace>>,
38}
39
40impl<I: std::ops::Add + From<lyon::tessellation::VertexId> + MaxIndex> Default
41    for TextTessellator<I>
42{
43    fn default() -> Self {
44        let data = include_bytes!("../../../data/0-255.pbf");
45        let glyphs = GlyphSet::try_from(data.as_slice()).unwrap();
46        Self {
47            glyphs,
48            quad_buffer: VertexBuffers::new(),
49            features: Vec::new(),
50            current_index: 0,
51            current_text: None,
52            current_origin: None,
53        }
54    }
55}
56
57enum StringGlyph<'a> {
58    Char(char),
59    Glyph(&'a Glyph),
60}
61
62impl<I: std::ops::Add + From<lyon::tessellation::VertexId> + MaxIndex> TextTessellator<I> {
63    pub fn tessellate_glyph_quads(
64        &mut self,
65        origin: [f32; 2],
66        label_text: &str,
67        color: Color,
68    ) -> Option<Box2D<f32, TileSpace>> {
69        let mut tessellator = FillTessellator::new();
70
71        let mut next_origin = origin;
72
73        let texture_dimensions = self.glyphs.get_texture_dimensions();
74        let texture_dimensions = (texture_dimensions.0 as f32, texture_dimensions.1 as f32);
75
76        // TODO: handle line wrapping / line height
77        let mut bbox = None;
78        for str_glyph in label_text
79            .chars()
80            .map(|c| {
81                self.glyphs
82                    .glyphs
83                    .get(&c)
84                    .map(StringGlyph::Glyph)
85                    .unwrap_or_else(|| StringGlyph::Char(c))
86            })
87            .collect::<Vec<_>>()
88        {
89            let glyph = match str_glyph {
90                StringGlyph::Glyph(glyph) => glyph,
91                StringGlyph::Char(c) => match c {
92                    ' ' => {
93                        next_origin[0] += 10.0; // Spaces are 10 units wide
94                        continue;
95                    }
96                    _ => {
97                        log::error!("unhandled char {}", c);
98                        continue;
99                    }
100                },
101            };
102
103            let glyph_dims = glyph.buffered_dimensions();
104            let width = glyph_dims.0 as f32;
105            let height = glyph_dims.1 as f32;
106
107            let glyph_anchor = [
108                next_origin[0] + glyph.left_bearing as f32,
109                next_origin[1] - glyph.top_bearing as f32,
110                0.,
111            ];
112
113            let glyph_bbox = Box2D::new(
114                (glyph_anchor[0], glyph_anchor[1]).into(),
115                (glyph_anchor[0] + width, glyph_anchor[1] + height).into(),
116            );
117
118            bbox = bbox.map_or_else(
119                || Some(glyph_bbox),
120                |bbox: Box2D<_, TileSpace>| Some(bbox.union(&glyph_bbox)),
121            );
122
123            tessellator
124                .tessellate_rectangle(
125                    &glyph_bbox.cast_unit(),
126                    &FillOptions::default(),
127                    &mut BuffersBuilder::new(
128                        &mut self.quad_buffer,
129                        SymbolVertexBuilder {
130                            glyph_anchor,
131                            text_anchor: [origin[0], origin[1], 0.0],
132                            texture_dimensions,
133                            sprite_dimensions: (width, height),
134                            sprite_offset: (
135                                glyph.origin_offset().0 as f32,
136                                glyph.origin_offset().1 as f32,
137                            ),
138                            color: color.to_rgba8(), // TODO: is this conversion oke?
139                            glyph: true,             // Set here to true to use SDF rendering
140                        },
141                    ),
142                )
143                .ok()?;
144
145            next_origin[0] += glyph.advance() as f32 + 1.0;
146        }
147
148        bbox
149    }
150}
151
152impl<I: std::ops::Add + From<lyon::tessellation::VertexId> + MaxIndex> GeomProcessor
153    for TextTessellator<I>
154{
155    fn xy(&mut self, x: f64, y: f64, _idx: usize) -> GeoResult<()> {
156        if self.current_origin.is_some() {
157            //FIXME
158            unreachable!("Text labels have only a single origin point")
159        } else {
160            self.current_origin = Some(Box2D::new(
161                Point2D::new(x as f32, y as f32),
162                Point2D::new(x as f32, y as f32),
163            ))
164        }
165
166        Ok(())
167    }
168
169    fn point_begin(&mut self, _idx: usize) -> GeoResult<()> {
170        Ok(())
171    }
172
173    fn point_end(&mut self, _idx: usize) -> GeoResult<()> {
174        Ok(())
175    }
176
177    fn multipoint_begin(&mut self, _size: usize, _idx: usize) -> GeoResult<()> {
178        Ok(())
179    }
180
181    fn multipoint_end(&mut self, _idx: usize) -> GeoResult<()> {
182        Ok(())
183    }
184
185    fn linestring_begin(&mut self, _tagged: bool, _size: usize, _idx: usize) -> GeoResult<()> {
186        Ok(())
187    }
188
189    fn linestring_end(&mut self, _tagged: bool, _idx: usize) -> GeoResult<()> {
190        Ok(())
191    }
192
193    fn multilinestring_begin(&mut self, _size: usize, _idx: usize) -> GeoResult<()> {
194        Ok(())
195    }
196
197    fn multilinestring_end(&mut self, _idx: usize) -> GeoResult<()> {
198        Ok(())
199    }
200
201    fn polygon_begin(&mut self, _tagged: bool, _size: usize, _idx: usize) -> GeoResult<()> {
202        Ok(())
203    }
204
205    fn polygon_end(&mut self, _tagged: bool, _idx: usize) -> GeoResult<()> {
206        Ok(())
207    }
208
209    fn multipolygon_begin(&mut self, _size: usize, _idx: usize) -> GeoResult<()> {
210        Ok(())
211    }
212
213    fn multipolygon_end(&mut self, _idx: usize) -> GeoResult<()> {
214        Ok(())
215    }
216}
217
218impl<I: std::ops::Add + From<lyon::tessellation::VertexId> + MaxIndex> PropertyProcessor
219    for TextTessellator<I>
220{
221    fn property(
222        &mut self,
223        _idx: usize,
224        name: &str,
225        value: &ColumnValue,
226    ) -> geozero::error::Result<bool> {
227        // TODO: Support different tags
228        if name == "name" {
229            match value {
230                ColumnValue::String(str) => {
231                    self.current_text = Some(str.to_string());
232                }
233                _ => {}
234            }
235        }
236        Ok(true)
237    }
238}
239
240impl<I: std::ops::Add + From<lyon::tessellation::VertexId> + MaxIndex> FeatureProcessor
241    for TextTessellator<I>
242{
243    fn feature_end(&mut self, _idx: u64) -> geozero::error::Result<()> {
244        if let (Some(origin), Some(text)) = (&self.current_origin, self.current_text.clone()) {
245            if text.is_empty() {
246                panic!("dud")
247            }
248            let anchor = Anchor::BottomLeft;
249            // TODO: add more anchor possibilities
250            let origin = match anchor {
251                Anchor::Center => origin.center(), // FIXME: origin is currently always a point
252                Anchor::BottomLeft => origin.min,
253                _ => unimplemented!("no support for this anchor"),
254            };
255            let bbox = self.tessellate_glyph_quads(
256                origin.to_array(),
257                text.as_str(),
258                Color::from_linear_rgba(1.0, 0., 0., 1.),
259            );
260
261            let next_index = self.quad_buffer.indices.len();
262            let start = self.current_index;
263            let end = next_index;
264            self.current_index = next_index;
265
266            self.features.push(Feature {
267                bbox: bbox.unwrap_or(Box2D::new(origin, origin)),
268                indices: start..end,
269                text_anchor: origin.cast(),
270                str: text,
271            });
272
273            self.current_origin = None;
274            self.current_text = None;
275        }
276        Ok(())
277    }
278}