Skip to main content

pfx_geom/
icon.rs

1use crate::field::{Surface, smooth_min};
2use crate::math::{add, length, normalize};
3use crate::mesh::Mesh;
4use crate::shapes::{Shape, round_cone};
5
6pub const VOXEL: f32 = 0.01;
7pub const RELAX: u32 = 8;
8pub const BLEND_VOXELS: f32 = 2.0;
9pub const CROSS_LIMIT: f32 = 900.0;
10pub const ERROR: f32 = 0.001;
11
12#[derive(Clone, Debug, PartialEq)]
13pub enum PartShape {
14    Ball {
15        at: [f32; 2],
16        z: f32,
17        r: f32,
18    },
19    Drop {
20        at: [f32; 2],
21        z: f32,
22        r: f32,
23        squash: f32,
24        stretch: f32,
25    },
26    Capsule {
27        from: [f32; 2],
28        to: [f32; 2],
29        z: f32,
30        r: f32,
31    },
32    Tube {
33        points: Vec<[f32; 2]>,
34        z: f32,
35        r: f32,
36    },
37    Torus {
38        at: [f32; 2],
39        z: f32,
40        major: f32,
41        minor: f32,
42    },
43    Box {
44        at: [f32; 2],
45        z: f32,
46        half: [f32; 3],
47        round: f32,
48        angle: f32,
49    },
50}
51
52impl PartShape {
53    pub fn ball() -> Self {
54        PartShape::Ball {
55            at: [0.0, 0.0],
56            z: 0.0,
57            r: 0.3,
58        }
59    }
60
61    pub fn drop() -> Self {
62        PartShape::Drop {
63            at: [0.0, 0.0],
64            z: 0.0,
65            r: 1.0,
66            squash: 0.45,
67            stretch: 1.0,
68        }
69    }
70
71    pub fn capsule(from: [f32; 2], to: [f32; 2]) -> Self {
72        PartShape::Capsule {
73            from,
74            to,
75            z: 0.0,
76            r: 0.08,
77        }
78    }
79
80    pub fn tube(points: Vec<[f32; 2]>) -> Self {
81        PartShape::Tube {
82            points,
83            z: 0.0,
84            r: 0.08,
85        }
86    }
87
88    pub fn torus() -> Self {
89        PartShape::Torus {
90            at: [0.0, 0.0],
91            z: 0.0,
92            major: 0.6,
93            minor: 0.1,
94        }
95    }
96
97    pub fn cube() -> Self {
98        PartShape::Box {
99            at: [0.0, 0.0],
100            z: 0.0,
101            half: [0.3, 0.3, 0.1],
102            round: 0.05,
103            angle: 0.0,
104        }
105    }
106
107    pub fn objects(&self) -> usize {
108        match self {
109            PartShape::Capsule { .. } | PartShape::Tube { .. } => 3,
110            _ => 1,
111        }
112    }
113
114    pub fn distance(&self, p: [f32; 3]) -> f32 {
115        match self {
116            PartShape::Ball { at, z, r } => length([p[0] - at[0], p[1] - at[1], p[2] - z]) - r,
117            PartShape::Drop {
118                at,
119                z,
120                r,
121                squash,
122                stretch,
123            } => {
124                let radii = [*r, r * stretch, r * squash];
125                let q = [p[0] - at[0], p[1] - at[1], p[2] - z];
126                let k = length([q[0] / radii[0], q[1] / radii[1], q[2] / radii[2]]);
127                (k - 1.0) * radii[0].min(radii[1]).min(radii[2])
128            }
129            PartShape::Capsule { from, to, z, r } => {
130                let a = [from[0], from[1], *z];
131                let b = [to[0], to[1], *z];
132                round_cone(p, a, b, *r, *r)
133            }
134            PartShape::Tube { points, z, r } => {
135                if points.len() == 1 {
136                    return length([p[0] - points[0][0], p[1] - points[0][1], p[2] - z]) - r;
137                }
138                points
139                    .windows(2)
140                    .map(|w| segment_distance(p, [w[0][0], w[0][1], *z], [w[1][0], w[1][1], *z]))
141                    .fold(f32::INFINITY, f32::min)
142                    - r
143            }
144            PartShape::Torus {
145                at,
146                z,
147                major,
148                minor,
149            } => {
150                let q = [p[0] - at[0], p[1] - at[1], p[2] - z];
151                let ring = (q[0] * q[0] + q[1] * q[1]).sqrt() - major;
152                (ring * ring + q[2] * q[2]).sqrt() - minor
153            }
154            PartShape::Box {
155                at,
156                z,
157                half,
158                round,
159                angle,
160            } => {
161                let (s, c) = angle.to_radians().sin_cos();
162                let d = [p[0] - at[0], p[1] - at[1], p[2] - z];
163                let local = [c * d[0] + s * d[1], -s * d[0] + c * d[1], d[2]];
164                let radius = box_round(*half, *round);
165                let q = [
166                    local[0].abs() - half[0] + radius,
167                    local[1].abs() - half[1] + radius,
168                    local[2].abs() - half[2] + radius,
169                ];
170                length([q[0].max(0.0), q[1].max(0.0), q[2].max(0.0)])
171                    + q[0].max(q[1]).max(q[2]).min(0.0)
172                    - radius
173            }
174        }
175    }
176
177    pub fn bounds(&self) -> ([f32; 3], [f32; 3]) {
178        let around = |at: [f32; 2], z: f32, e: [f32; 3]| {
179            (
180                [at[0] - e[0], at[1] - e[1], z - e[2]],
181                [at[0] + e[0], at[1] + e[1], z + e[2]],
182            )
183        };
184        match self {
185            PartShape::Ball { at, z, r } => around(*at, *z, [*r; 3]),
186            PartShape::Drop {
187                at,
188                z,
189                r,
190                squash,
191                stretch,
192            } => around(*at, *z, [*r, r * stretch, r * squash]),
193            PartShape::Capsule { from, to, z, r } => line_bounds(&[*from, *to], *z, *r),
194            PartShape::Tube { points, z, r } => line_bounds(points, *z, *r),
195            PartShape::Torus {
196                at,
197                z,
198                major,
199                minor,
200            } => around(*at, *z, [major + minor, major + minor, *minor]),
201            PartShape::Box {
202                at, z, half, angle, ..
203            } => {
204                let (s, c) = angle.to_radians().sin_cos();
205                let ex = c.abs() * half[0] + s.abs() * half[1];
206                let ey = s.abs() * half[0] + c.abs() * half[1];
207                around(*at, *z, [ex, ey, half[2]])
208            }
209        }
210    }
211
212    pub fn mesh(&self, error: f32) -> Mesh {
213        match self {
214            PartShape::Ball { at, z, r } => {
215                let base = Shape::Ellipsoid {
216                    radius: *r,
217                    squash: 1.0,
218                }
219                .mesh(error);
220                place(base, [at[0], at[1], *z], 0.0, 1.0)
221            }
222            PartShape::Drop {
223                at,
224                z,
225                r,
226                squash,
227                stretch,
228            } => {
229                let base = Shape::Ellipsoid {
230                    radius: *r,
231                    squash: *squash,
232                }
233                .mesh(error);
234                place(base, [at[0], at[1], *z], 0.0, *stretch)
235            }
236            PartShape::Capsule { from, to, z, r } => Shape::Capsule {
237                a: [from[0], from[1], *z],
238                b: [to[0], to[1], *z],
239                radius: *r,
240            }
241            .mesh(error),
242            PartShape::Tube { points, z, r } => {
243                let pieces = if points.len() == 1 {
244                    vec![
245                        PartShape::Ball {
246                            at: points[0],
247                            z: *z,
248                            r: *r,
249                        }
250                        .mesh(error),
251                    ]
252                } else {
253                    points
254                        .windows(2)
255                        .map(|w| {
256                            Shape::Capsule {
257                                a: [w[0][0], w[0][1], *z],
258                                b: [w[1][0], w[1][1], *z],
259                                radius: *r,
260                            }
261                            .mesh(error)
262                        })
263                        .collect()
264                };
265                join(&pieces)
266            }
267            PartShape::Torus {
268                at,
269                z,
270                major,
271                minor,
272            } => place(
273                Shape::Torus {
274                    major: *major,
275                    minor: *minor,
276                }
277                .mesh(error),
278                [at[0], at[1], *z],
279                0.0,
280                1.0,
281            ),
282            PartShape::Box {
283                at,
284                z,
285                half,
286                round,
287                angle,
288            } => place(
289                Shape::RoundBox {
290                    half: *half,
291                    radius: box_round(*half, *round),
292                }
293                .mesh(error),
294                [at[0], at[1], *z],
295                *angle,
296                1.0,
297            ),
298        }
299    }
300}
301
302fn box_round(half: [f32; 3], round: f32) -> f32 {
303    round.min(half[0].min(half[1]).min(half[2]) * 0.9).max(0.0)
304}
305
306fn line_bounds(points: &[[f32; 2]], z: f32, r: f32) -> ([f32; 3], [f32; 3]) {
307    let mut lo = [f32::INFINITY, f32::INFINITY, z - r];
308    let mut hi = [f32::NEG_INFINITY, f32::NEG_INFINITY, z + r];
309    for p in points {
310        for i in 0..2 {
311            lo[i] = lo[i].min(p[i] - r);
312            hi[i] = hi[i].max(p[i] + r);
313        }
314    }
315    (lo, hi)
316}
317
318fn segment_distance(p: [f32; 3], a: [f32; 3], b: [f32; 3]) -> f32 {
319    let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
320    let ap = [p[0] - a[0], p[1] - a[1], p[2] - a[2]];
321    let l2 = ab[0] * ab[0] + ab[1] * ab[1] + ab[2] * ab[2];
322    let t = if l2 <= 1e-12 {
323        0.0
324    } else {
325        ((ap[0] * ab[0] + ap[1] * ab[1] + ap[2] * ab[2]) / l2).clamp(0.0, 1.0)
326    };
327    length([ap[0] - ab[0] * t, ap[1] - ab[1] * t, ap[2] - ab[2] * t])
328}
329
330fn place(mesh: Mesh, at: [f32; 3], angle: f32, stretch: f32) -> Mesh {
331    let (s, c) = angle.to_radians().sin_cos();
332    let positions = mesh
333        .positions
334        .iter()
335        .map(|p| {
336            let y = p[1] * stretch;
337            add([c * p[0] - s * y, s * p[0] + c * y, p[2]], at)
338        })
339        .collect();
340    let normals = mesh
341        .normals
342        .iter()
343        .map(|n| {
344            let y = n[1] / stretch;
345            normalize([c * n[0] - s * y, s * n[0] + c * y, n[2]])
346        })
347        .collect();
348    Mesh::new(positions, normals, Vec::new(), mesh.uvs, mesh.indices)
349}
350
351pub(crate) fn join(meshes: &[Mesh]) -> Mesh {
352    let mut positions = Vec::new();
353    let mut normals = Vec::new();
354    let mut tangents = Vec::new();
355    let mut uvs = Vec::new();
356    let mut indices = Vec::new();
357    for mesh in meshes {
358        let base = positions.len() as u32;
359        positions.extend_from_slice(&mesh.positions);
360        normals.extend_from_slice(&mesh.normals);
361        tangents.extend_from_slice(&mesh.tangents);
362        uvs.extend_from_slice(&mesh.uvs);
363        indices.extend(mesh.indices.iter().map(|i| i + base));
364    }
365    Mesh::new(positions, normals, tangents, uvs, indices)
366}
367
368#[derive(Clone, Debug, PartialEq)]
369pub struct Part {
370    pub shape: PartShape,
371    pub material: Option<String>,
372}
373
374impl Part {
375    pub fn new(shape: PartShape) -> Self {
376        Self {
377            shape,
378            material: None,
379        }
380    }
381
382    pub fn with_material(mut self, material: &str) -> Self {
383        self.material = Some(material.to_string());
384        self
385    }
386}
387
388#[derive(Clone, Debug, PartialEq)]
389pub struct Icon {
390    pub parts: Vec<Part>,
391    pub fuse: bool,
392    pub voxel: f32,
393    pub blend: Option<f32>,
394    pub relax: u32,
395    pub error: f32,
396}
397
398impl Icon {
399    pub fn new(parts: Vec<Part>) -> Self {
400        Self {
401            parts,
402            fuse: true,
403            voxel: VOXEL,
404            blend: None,
405            relax: RELAX,
406            error: ERROR,
407        }
408    }
409
410    pub fn build(&self) -> Result<Vec<Piece>, String> {
411        for (i, part) in self.parts.iter().enumerate() {
412            check(i, &part.shape)?;
413        }
414        if !(self.voxel > 0.0 && self.voxel.is_finite()) {
415            return Err(format!("icon: voxel must be above 0, got {}", self.voxel));
416        }
417        let mut groups: Vec<(Option<String>, Vec<usize>)> = Vec::new();
418        for (i, part) in self.parts.iter().enumerate() {
419            match groups.iter_mut().find(|(name, _)| *name == part.material) {
420                Some((_, members)) => members.push(i),
421                None => groups.push((part.material.clone(), vec![i])),
422            }
423        }
424        let mut pieces = Vec::new();
425        for (material, members) in groups {
426            let objects: usize = members.iter().map(|&i| self.parts[i].shape.objects()).sum();
427            if self.fuse && objects > 1 {
428                let shapes = members
429                    .iter()
430                    .map(|&i| &self.parts[i].shape)
431                    .collect::<Vec<_>>();
432                pieces.push(Piece {
433                    material,
434                    parts: members,
435                    mesh: self.fused(&shapes),
436                });
437            } else {
438                for i in members {
439                    pieces.push(Piece {
440                        material: material.clone(),
441                        parts: vec![i],
442                        mesh: self.parts[i].shape.mesh(self.error),
443                    });
444                }
445            }
446        }
447        Ok(pieces)
448    }
449
450    pub fn voxel_for(&self, shapes: &[&PartShape]) -> f32 {
451        let (lo, hi) = union_bounds(shapes);
452        let span = (hi[0] - lo[0]).max(hi[1] - lo[1]).max(hi[2] - lo[2]);
453        self.voxel.max(span / CROSS_LIMIT)
454    }
455
456    fn fused(&self, shapes: &[&PartShape]) -> Mesh {
457        let voxel = self.voxel_for(shapes);
458        let blend = self.blend.unwrap_or(BLEND_VOXELS * voxel).max(0.0);
459        let (lo, hi) = union_bounds(shapes);
460        let field = |p: [f32; 3]| {
461            shapes
462                .iter()
463                .map(|s| s.distance(p))
464                .reduce(|a, b| smooth_min(a, b, blend))
465                .unwrap_or(f32::INFINITY)
466        };
467        Surface::new(voxel, self.relax).mesh(&field, sub3(lo, blend), add3(hi, blend))
468    }
469}
470
471fn add3(p: [f32; 3], d: f32) -> [f32; 3] {
472    [p[0] + d, p[1] + d, p[2] + d]
473}
474
475fn sub3(p: [f32; 3], d: f32) -> [f32; 3] {
476    [p[0] - d, p[1] - d, p[2] - d]
477}
478
479fn union_bounds(shapes: &[&PartShape]) -> ([f32; 3], [f32; 3]) {
480    let mut lo = [f32::INFINITY; 3];
481    let mut hi = [f32::NEG_INFINITY; 3];
482    for shape in shapes {
483        let (a, b) = shape.bounds();
484        for i in 0..3 {
485            lo[i] = lo[i].min(a[i]);
486            hi[i] = hi[i].max(b[i]);
487        }
488    }
489    (lo, hi)
490}
491
492fn check(index: usize, shape: &PartShape) -> Result<(), String> {
493    let positive = |name: &str, v: f32| {
494        if v > 0.0 && v.is_finite() {
495            Ok(())
496        } else {
497            Err(format!(
498                "icon part {index}: {name} must be above 0, got {v}"
499            ))
500        }
501    };
502    match shape {
503        PartShape::Ball { r, .. } => positive("r", *r),
504        PartShape::Drop {
505            r, squash, stretch, ..
506        } => {
507            positive("r", *r)?;
508            positive("squash", *squash)?;
509            positive("stretch", *stretch)
510        }
511        PartShape::Capsule { r, .. } => positive("r", *r),
512        PartShape::Tube { points, r, .. } => {
513            if points.is_empty() {
514                return Err(format!("icon part {index}: a tube needs points"));
515            }
516            positive("r", *r)
517        }
518        PartShape::Torus { major, minor, .. } => {
519            positive("major", *major)?;
520            positive("minor", *minor)
521        }
522        PartShape::Box { half, .. } => {
523            for v in half {
524                positive("half", *v)?;
525            }
526            Ok(())
527        }
528    }
529}
530
531#[derive(Clone, Debug)]
532pub struct Piece {
533    pub material: Option<String>,
534    pub parts: Vec<usize>,
535    pub mesh: Mesh,
536}