Skip to main content

pfx_live/flat/
icons.rs

1use pfx_gpu::wgpu;
2use pfx_gpu::wgpu::util::DeviceExt;
3
4pub const ATLAS_WIDTH: u32 = 1024;
5pub const CELL_TEXELS: f32 = 160.0;
6pub const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::R16Float;
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
9pub struct Icon(pub u32);
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum IconKind {
13    Fill,
14    Line,
15}
16
17#[derive(Clone, Debug, Default, PartialEq)]
18pub struct IconShape {
19    pub polygons: Vec<Vec<[f32; 2]>>,
20    pub lines: Vec<Vec<[f32; 2]>>,
21    pub circles: Vec<([f32; 2], f32)>,
22    pub reach: f32,
23}
24
25impl IconShape {
26    pub fn polygon(points: &[[f32; 2]], reach: f32) -> Self {
27        Self {
28            polygons: vec![points.to_vec()],
29            reach,
30            ..Self::default()
31        }
32    }
33
34    pub fn lines(lines: &[&[[f32; 2]]], reach: f32) -> Self {
35        Self {
36            lines: lines.iter().map(|line| line.to_vec()).collect(),
37            reach,
38            ..Self::default()
39        }
40    }
41
42    pub fn with_circle(mut self, centre: [f32; 2], radius: f32) -> Self {
43        self.circles.push((centre, radius));
44        self
45    }
46
47    pub fn kind(&self) -> IconKind {
48        if self.polygons.is_empty() {
49            IconKind::Line
50        } else {
51            IconKind::Fill
52        }
53    }
54
55    fn bounds(&self) -> Option<[f32; 4]> {
56        let mut points = self
57            .polygons
58            .iter()
59            .chain(&self.lines)
60            .flatten()
61            .copied()
62            .collect::<Vec<_>>();
63        for &(centre, radius) in &self.circles {
64            points.push([centre[0] - radius, centre[1] - radius]);
65            points.push([centre[0] + radius, centre[1] + radius]);
66        }
67        let first = *points.first()?;
68        Some(points.iter().fold(
69            [first[0], first[1], first[0], first[1]],
70            |[x0, y0, x1, y1], p| [x0.min(p[0]), y0.min(p[1]), x1.max(p[0]), y1.max(p[1])],
71        ))
72    }
73
74    pub fn distance(&self, p: [f32; 2]) -> f32 {
75        let mut nearest = f32::MAX;
76        let mut inside = false;
77        for polygon in &self.polygons {
78            for (index, &a) in polygon.iter().enumerate() {
79                let b = polygon[(index + 1) % polygon.len()];
80                nearest = nearest.min(segment(p, a, b));
81                if (a[1] > p[1]) != (b[1] > p[1])
82                    && p[0] < a[0] + (p[1] - a[1]) / (b[1] - a[1]) * (b[0] - a[0])
83                {
84                    inside = !inside;
85                }
86            }
87        }
88        for line in &self.lines {
89            if line.len() == 1 {
90                nearest = nearest.min(segment(p, line[0], line[0]));
91            }
92            for pair in line.windows(2) {
93                nearest = nearest.min(segment(p, pair[0], pair[1]));
94            }
95        }
96        for &(centre, radius) in &self.circles {
97            let offset = [p[0] - centre[0], p[1] - centre[1]];
98            nearest = nearest
99                .min(((offset[0] * offset[0] + offset[1] * offset[1]).sqrt() - radius).abs());
100        }
101        if inside { -nearest } else { nearest }
102    }
103}
104
105fn segment(p: [f32; 2], a: [f32; 2], b: [f32; 2]) -> f32 {
106    let ab = [b[0] - a[0], b[1] - a[1]];
107    let ap = [p[0] - a[0], p[1] - a[1]];
108    let span = ab[0] * ab[0] + ab[1] * ab[1];
109    let t = if span > 0.0 {
110        ((ap[0] * ab[0] + ap[1] * ab[1]) / span).clamp(0.0, 1.0)
111    } else {
112        0.0
113    };
114    let d = [ap[0] - ab[0] * t, ap[1] - ab[1] * t];
115    (d[0] * d[0] + d[1] * d[1]).sqrt()
116}
117
118#[derive(Clone, Copy, Debug, PartialEq)]
119pub struct IconEntry {
120    pub kind: IconKind,
121    pub bounds: [f32; 4],
122    pub uv: [f32; 4],
123}
124
125#[derive(Clone, Debug, PartialEq)]
126pub struct Icons {
127    pub width: u32,
128    pub height: u32,
129    texels: Vec<u16>,
130    entries: Vec<IconEntry>,
131}
132
133impl Icons {
134    pub fn bake(shapes: &[IconShape]) -> Result<Self, String> {
135        let mut cells = Vec::with_capacity(shapes.len());
136        let (mut x, mut y, mut row) = (0u32, 0u32, 0u32);
137        for shape in shapes {
138            let [x0, y0, x1, y1] = shape.bounds().ok_or("an icon has no geometry")?;
139            if !(shape.reach.is_finite() && shape.reach >= 0.0) {
140                return Err("an icon's reach must be finite and nonnegative".into());
141            }
142            let bounds = [
143                x0 - shape.reach,
144                y0 - shape.reach,
145                x1 + shape.reach,
146                y1 + shape.reach,
147            ];
148            let span = (bounds[2] - bounds[0]).max(bounds[3] - bounds[1]);
149            if !(span.is_finite() && span > 0.0) {
150                return Err("an icon has no extent".into());
151            }
152            let density = CELL_TEXELS / span;
153            let size = [
154                ((bounds[2] - bounds[0]) * density).ceil() as u32 + 1,
155                ((bounds[3] - bounds[1]) * density).ceil() as u32 + 1,
156            ];
157            if x + size[0] > ATLAS_WIDTH {
158                x = 0;
159                y += row;
160                row = 0;
161            }
162            cells.push((bounds, density, [x, y], size));
163            x += size[0];
164            row = row.max(size[1]);
165        }
166        let width = ATLAS_WIDTH;
167        let height = (y + row).max(1).next_power_of_two();
168        if height > 8192 {
169            return Err("the icon atlas is taller than 8192 texels".into());
170        }
171        let mut texels = vec![half::f16::from_f32(1.0e4).to_bits(); (width * height) as usize];
172        let mut entries = Vec::with_capacity(shapes.len());
173        for (shape, &(bounds, density, origin, size)) in shapes.iter().zip(&cells) {
174            for row in 0..size[1] {
175                for column in 0..size[0] {
176                    let p = [
177                        bounds[0] + column as f32 / density,
178                        bounds[1] + row as f32 / density,
179                    ];
180                    let at = ((origin[1] + row) * width + origin[0] + column) as usize;
181                    texels[at] = half::f16::from_f32(shape.distance(p)).to_bits();
182                }
183            }
184            let scale = [density / width as f32, density / height as f32];
185            entries.push(IconEntry {
186                kind: shape.kind(),
187                bounds,
188                uv: [
189                    (origin[0] as f32 + 0.5) / width as f32 - bounds[0] * scale[0],
190                    (origin[1] as f32 + 0.5) / height as f32 - bounds[1] * scale[1],
191                    scale[0],
192                    scale[1],
193                ],
194            });
195        }
196        Ok(Self {
197            width,
198            height,
199            texels,
200            entries,
201        })
202    }
203
204    pub fn entry(&self, icon: Icon) -> Option<&IconEntry> {
205        self.entries.get(icon.0 as usize)
206    }
207
208    pub fn len(&self) -> usize {
209        self.entries.len()
210    }
211
212    pub fn is_empty(&self) -> bool {
213        self.entries.is_empty()
214    }
215
216    pub fn texels(&self) -> &[u16] {
217        &self.texels
218    }
219
220    fn texel(&self, x: i64, y: i64) -> f32 {
221        let x = x.clamp(0, i64::from(self.width) - 1) as u32;
222        let y = y.clamp(0, i64::from(self.height) - 1) as u32;
223        half::f16::from_bits(self.texels[(y * self.width + x) as usize]).to_f32()
224    }
225
226    pub fn sample(&self, uv: [f32; 2]) -> f32 {
227        let x = uv[0] * self.width as f32 - 0.5;
228        let y = uv[1] * self.height as f32 - 0.5;
229        let (x0, y0) = (x.floor(), y.floor());
230        let (fx, fy) = (x - x0, y - y0);
231        let (x0, y0) = (x0 as i64, y0 as i64);
232        let top = self.texel(x0, y0) * (1.0 - fx) + self.texel(x0 + 1, y0) * fx;
233        let bottom = self.texel(x0, y0 + 1) * (1.0 - fx) + self.texel(x0 + 1, y0 + 1) * fx;
234        top * (1.0 - fy) + bottom * fy
235    }
236
237    pub fn upload(&self, device: &wgpu::Device, queue: &wgpu::Queue) -> GpuIcons {
238        let texture = device.create_texture_with_data(
239            queue,
240            &wgpu::TextureDescriptor {
241                label: Some("flat icon fields"),
242                size: wgpu::Extent3d {
243                    width: self.width,
244                    height: self.height,
245                    depth_or_array_layers: 1,
246                },
247                mip_level_count: 1,
248                sample_count: 1,
249                dimension: wgpu::TextureDimension::D2,
250                format: FORMAT,
251                usage: wgpu::TextureUsages::TEXTURE_BINDING,
252                view_formats: &[],
253            },
254            wgpu::util::TextureDataOrder::LayerMajor,
255            bytemuck::cast_slice(&self.texels),
256        );
257        let view = texture.create_view(&Default::default());
258        GpuIcons {
259            _texture: texture,
260            view,
261        }
262    }
263}
264
265pub struct GpuIcons {
266    _texture: wgpu::Texture,
267    pub view: wgpu::TextureView,
268}
269
270pub fn pointer() -> IconShape {
271    IconShape::polygon(
272        &[
273            [0.0, 0.0],
274            [0.0, 52.0],
275            [12.0, 41.0],
276            [21.0, 60.0],
277            [30.0, 56.0],
278            [21.0, 37.0],
279            [37.0, 37.0],
280        ],
281        6.0,
282    )
283}
284
285pub fn tick() -> IconShape {
286    IconShape::lines(&[&[[-10.0, 1.0], [-3.0, 9.0], [11.0, -8.0]]], 6.0)
287}
288
289pub fn plus() -> IconShape {
290    IconShape::lines(
291        &[&[[-16.0, 0.0], [16.0, 0.0]], &[[0.0, -16.0], [0.0, 16.0]]],
292        7.0,
293    )
294}
295
296pub fn cross() -> IconShape {
297    IconShape::lines(
298        &[
299            &[[-14.0, -14.0], [14.0, 14.0]],
300            &[[14.0, -14.0], [-14.0, 14.0]],
301        ],
302        7.0,
303    )
304}
305
306pub fn arrows() -> IconShape {
307    IconShape::lines(
308        &[
309            &[[-20.0, 0.0], [-70.0, 0.0]],
310            &[[-52.0, -16.0], [-70.0, 0.0], [-52.0, 16.0]],
311            &[[20.0, 0.0], [70.0, 0.0]],
312            &[[52.0, -16.0], [70.0, 0.0], [52.0, 16.0]],
313        ],
314        6.0,
315    )
316}
317
318pub fn menu() -> IconShape {
319    IconShape::lines(
320        &[
321            &[[-25.0, -20.0], [25.0, -20.0]],
322            &[[-25.0, 0.0], [25.0, 0.0]],
323            &[[-25.0, 20.0], [25.0, 20.0]],
324        ],
325        6.0,
326    )
327}
328
329pub fn search() -> IconShape {
330    IconShape::lines(&[&[[6.0, 7.0], [14.0, 15.0]]], 4.0).with_circle([0.0, 0.0], 9.0)
331}
332
333pub fn page() -> IconShape {
334    IconShape::polygon(
335        &[
336            [0.0, 0.0],
337            [32.0, 0.0],
338            [50.0, 18.0],
339            [50.0, 64.0],
340            [0.0, 64.0],
341        ],
342        3.0,
343    )
344}
345
346pub fn fold() -> IconShape {
347    IconShape::polygon(&[[32.0, 0.0], [32.0, 18.0], [50.0, 18.0]], 3.0)
348}
349
350pub fn funnel() -> IconShape {
351    IconShape::polygon(
352        &[
353            [-12.0, -10.0],
354            [12.0, -10.0],
355            [3.0, 2.0],
356            [3.0, 12.0],
357            [-3.0, 9.0],
358            [-3.0, 2.0],
359        ],
360        3.0,
361    )
362}
363
364pub fn standard() -> Vec<(&'static str, IconShape)> {
365    vec![
366        ("pointer", pointer()),
367        ("tick", tick()),
368        ("plus", plus()),
369        ("cross", cross()),
370        ("arrows", arrows()),
371        ("menu", menu()),
372        ("search", search()),
373        ("page", page()),
374        ("fold", fold()),
375        ("funnel", funnel()),
376    ]
377}
378
379#[cfg(test)]
380mod tests {
381    use super::*;
382
383    #[test]
384    fn polygon_fields_are_signed_and_line_fields_are_not() {
385        let pointer = pointer();
386        assert_eq!(pointer.kind(), IconKind::Fill);
387        assert!(pointer.distance([10.0, 30.0]) < 0.0);
388        assert!((pointer.distance([-3.0, 30.0]) - 3.0).abs() < 1e-5);
389        assert!((pointer.distance([2.0, 30.0]) + 2.0).abs() < 1e-5);
390        let tick = tick();
391        assert_eq!(tick.kind(), IconKind::Line);
392        assert!((tick.distance([-10.0, -2.0]) - 3.0).abs() < 1e-5);
393        assert!(tick.distance([-3.0, 9.0]).abs() < 1e-5);
394        let search = search();
395        assert!((search.distance([0.0, 0.0]) - 9.0).abs() < 1e-5);
396        assert!(search.distance([9.0, 0.0]).abs() < 1e-5);
397    }
398
399    #[test]
400    fn the_atlas_samples_back_its_distances() {
401        let shapes = standard()
402            .into_iter()
403            .map(|(_, shape)| shape)
404            .collect::<Vec<_>>();
405        let icons = Icons::bake(&shapes).unwrap();
406        assert_eq!(icons.len(), shapes.len());
407        assert_eq!(icons.width, ATLAS_WIDTH);
408        assert!(icons.height.is_power_of_two());
409        for (index, shape) in shapes.iter().enumerate() {
410            let entry = *icons.entry(Icon(index as u32)).unwrap();
411            assert_eq!(entry.kind, shape.kind());
412            let [x0, y0, x1, y1] = entry.bounds;
413            for i in 1..20 {
414                for j in 1..20 {
415                    let p = [
416                        x0 + (x1 - x0) * i as f32 / 20.0,
417                        y0 + (y1 - y0) * j as f32 / 20.0,
418                    ];
419                    let uv = [
420                        entry.uv[0] + p[0] * entry.uv[2],
421                        entry.uv[1] + p[1] * entry.uv[3],
422                    ];
423                    let texel = (x1 - x0).max(y1 - y0) / CELL_TEXELS;
424                    let exact = shape.distance(p);
425                    let sampled = icons.sample(uv);
426                    assert!(
427                        (sampled - exact).abs() < texel * 0.75 + exact.abs() * 2e-3,
428                        "icon {index} at {p:?}: {sampled} vs {exact}"
429                    );
430                }
431            }
432        }
433    }
434
435    #[test]
436    fn an_empty_icon_is_refused() {
437        assert!(Icons::bake(&[IconShape::default()]).is_err());
438    }
439}