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}