git.lucas.co / cce-model
3D model viewer: STL and OBJ
git clone https://git.lucas.co/cce-model.git

src/overlay.rs (8.8K)

  1 //! What is drawn over and under the model as lines: the grid floor, the
  2 //! wireframe and the normals. Each is a list of vertex PAIRS for a wireframe
  3 //! scene draw, in the fitted space the model is drawn in (the unit sphere).
  4 //!
  5 //! The grid is laid out in the FILE's units, so its lines fall on round
  6 //! numbers of millimetres (or whatever the file is in) and one of them runs
  7 //! through the file's origin; only then is it carried into the fitted space.
  8 
  9 use std::collections::HashSet;
 10 
 11 use cce_mesh_io::Mesh;
 12 use cce_ui::engine::Vertex3D;
 13 use glam::Vec3;
 14 
 15 use crate::units;
 16 
 17 /// The colours of the lines, linear RGB, chosen against the background
 18 /// gradient: a minor grid line barely above it, every fifth a step brighter.
 19 const GRID_MINOR: [f32; 3] = [0.075, 0.078, 0.088];
 20 const GRID_MAJOR: [f32; 3] = [0.16, 0.165, 0.185];
 21 const EDGE: [f32; 3] = [0.82, 0.84, 0.9];
 22 const NORMAL_BASE: [f32; 3] = [0.15, 0.55, 0.85];
 23 const NORMAL_TIP: [f32; 3] = [0.55, 0.9, 1.0];
 24 
 25 /// About this many cells across the model's footprint.
 26 const GRID_CELLS: f32 = 12.0;
 27 /// The longest a normal is drawn, in the fitted space (the model is 2
 28 /// across). A dense model's are shorter: see [`normals`].
 29 const NORMAL_LENGTH: f32 = 0.04;
 30 const NORMAL_LENGTH_MIN: f32 = 0.004;
 31 /// Above this many triangles there is no wireframe: its edges outnumber
 32 /// the window's pixels, so it paints the model solid (a 5M-triangle model's
 33 /// was a white silhouette) and costs 360 MB doing it.
 34 pub const MAX_WIRE_TRIANGLES: usize = 2_000_000;
 35 /// More normals than this are thinned to every n-th: past it they are a
 36 /// solid fur that hides the model they describe.
 37 pub const MAX_NORMALS: usize = 50_000;
 38 
 39 /// Where the fitted space sits in the file's: a file point p is drawn at
 40 /// (p − centre) / radius.
 41 #[derive(Debug, Clone, Copy)]
 42 pub struct Fit {
 43     pub centre: Vec3,
 44     pub radius: f32,
 45     /// The model's box, in file units (after the turn upright).
 46     pub lo: Vec3,
 47     pub hi: Vec3,
 48 }
 49 
 50 impl Fit {
 51     fn place(&self, p: Vec3) -> [f32; 3] {
 52         ((p - self.centre) / self.radius).to_array()
 53     }
 54 }
 55 
 56 pub struct Grid {
 57     pub lines: Vec<Vertex3D>,
 58     /// One cell's side, in file units.
 59     pub step: f32,
 60 }
 61 
 62 /// A floor under the model: square, a little wider than its footprint,
 63 /// at the height of its lowest point.
 64 pub fn grid(fit: &Fit) -> Grid {
 65     let size = fit.hi - fit.lo;
 66     let footprint = size.x.max(size.z).max(fit.radius * 0.5);
 67     let step = units::nice_step(footprint / GRID_CELLS);
 68     let half = 0.75 * footprint + step;
 69     let (cx, cz) = ((fit.lo.x + fit.hi.x) / 2.0, (fit.lo.z + fit.hi.z) / 2.0);
 70     // Whole multiples of the step, so the lines sit on round numbers and
 71     // index 0 is the file's origin.
 72     let (x0, x1) = (((cx - half) / step).floor() as i64, ((cx + half) / step).ceil() as i64);
 73     let (z0, z1) = (((cz - half) / step).floor() as i64, ((cz + half) / step).ceil() as i64);
 74     let y = fit.lo.y;
 75     let colour = |k: i64| if k % 5 == 0 { GRID_MAJOR } else { GRID_MINOR };
 76     let mut lines = Vec::new();
 77     let mut line = |a: Vec3, b: Vec3, color: [f32; 3]| {
 78         lines.push(Vertex3D { position: fit.place(a), color });
 79         lines.push(Vertex3D { position: fit.place(b), color });
 80     };
 81     for k in x0..=x1 {
 82         let x = k as f32 * step;
 83         line(Vec3::new(x, y, z0 as f32 * step), Vec3::new(x, y, z1 as f32 * step), colour(k));
 84     }
 85     for k in z0..=z1 {
 86         let z = k as f32 * step;
 87         line(Vec3::new(x0 as f32 * step, y, z), Vec3::new(x1 as f32 * step, y, z), colour(k));
 88     }
 89     Grid { lines, step }
 90 }
 91 
 92 /// Every edge of every triangle, once; nothing past [`MAX_WIRE_TRIANGLES`].
 93 pub fn edges(mesh: &Mesh) -> Vec<Vertex3D> {
 94     if mesh.triangles.len() > MAX_WIRE_TRIANGLES {
 95         return Vec::new();
 96     }
 97     let mut keys: Vec<u64> = Vec::with_capacity(mesh.triangles.len() * 3);
 98     for t in &mesh.triangles {
 99         for (a, b) in [(t[0], t[1]), (t[1], t[2]), (t[2], t[0])] {
100             keys.push(((a.min(b) as u64) << 32) | a.max(b) as u64);
101         }
102     }
103     keys.sort_unstable();
104     keys.dedup();
105     let mut out = Vec::with_capacity(keys.len() * 2);
106     for k in keys {
107         for i in [(k >> 32) as usize, (k & 0xffff_ffff) as usize] {
108             out.push(Vertex3D { position: mesh.positions[i].to_array(), color: EDGE });
109         }
110     }
111     out
112 }
113 
114 /// A short line out of each point along each normal it is lit with
115 /// (`corner`, three per triangle) — one per point on a smooth surface, one
116 /// per face meeting at a crease — and every n-th of them past [`MAX_NORMALS`].
117 ///
118 /// Each is about as long as the gap between the normals drawn (the square
119 /// root of the surface's area over their number), up to [`NORMAL_LENGTH`]:
120 /// at a fixed length, a dense model's normals overlap into a fur that hides
121 /// the surface they describe.
122 pub fn normals(mesh: &Mesh, corner: &[Vec3]) -> Vec<Vertex3D> {
123     let mut seen: HashSet<(u32, [i16; 3])> = HashSet::new();
124     let mut picked: Vec<(Vec3, Vec3)> = Vec::new();
125     for (t, tri) in mesh.triangles.iter().enumerate() {
126         for (k, &v) in tri.iter().enumerate() {
127             let n = corner[t * 3 + k];
128             let q = (n * 1000.0).round();
129             if seen.insert((v, [q.x as i16, q.y as i16, q.z as i16])) {
130                 picked.push((mesh.positions[v as usize], n));
131             }
132         }
133     }
134     let stride = picked.len().div_ceil(MAX_NORMALS).max(1);
135     let area: f32 = mesh
136         .triangles
137         .iter()
138         .map(|t| {
139             let [a, b, c] = t.map(|i| mesh.positions[i as usize]);
140             (b - a).cross(c - a).length() / 2.0
141         })
142         .sum();
143     let shown = picked.len().div_ceil(stride).max(1) as f32;
144     let length = (area / shown).sqrt().clamp(NORMAL_LENGTH_MIN, NORMAL_LENGTH);
145     picked
146         .iter()
147         .step_by(stride)
148         .flat_map(|&(p, n)| {
149             [
150                 Vertex3D { position: p.to_array(), color: NORMAL_BASE },
151                 Vertex3D { position: (p + n * length).to_array(), color: NORMAL_TIP },
152             ]
153         })
154         .collect()
155 }
156 
157 #[cfg(test)]
158 mod tests {
159     use super::*;
160 
161     fn cube() -> Mesh {
162         let p = |i: u32| Vec3::new((i & 1) as f32, ((i >> 1) & 1) as f32, ((i >> 2) & 1) as f32);
163         let quads = [[1, 3, 7, 5], [4, 6, 2, 0], [2, 6, 7, 3], [4, 0, 1, 5], [4, 5, 7, 6], [1, 0, 2, 3]];
164         Mesh {
165             positions: (0..8).map(p).collect(),
166             triangles: quads.iter().flat_map(|q| [[q[0], q[1], q[2]], [q[0], q[2], q[3]]]).collect(),
167             tri_material: vec![0; 12],
168             materials: vec![cce_mesh_io::Material::default()],
169             ..Mesh::default()
170         }
171     }
172 
173     #[test]
174     fn a_cube_has_eighteen_edges() {
175         // Twelve sides and one diagonal across each of its six faces.
176         assert_eq!(edges(&cube()).len(), 18 * 2);
177     }
178 
179     #[test]
180     fn a_cube_has_three_normals_a_corner() {
181         // Every corner meets three faces at 90°, past the crease.
182         let c = cube();
183         assert_eq!(normals(&c, &c.corner_normals(40.0)).len(), 8 * 3 * 2);
184     }
185 
186     #[test]
187     fn the_grid_is_on_round_numbers_under_the_model() {
188         // A 120 × 30 × 80 part standing on z... after the turn: y is up.
189         let fit = Fit { centre: Vec3::new(60.0, 15.0, 40.0), radius: 75.0, lo: Vec3::ZERO, hi: Vec3::new(120.0, 30.0, 80.0) };
190         let g = grid(&fit);
191         assert_eq!(g.step, 10.0, "120 mm across / 12 cells");
192         for pair in g.lines.chunks(2) {
193             let a = Vec3::from(pair[0].position) * fit.radius + fit.centre;
194             assert!((a.y - 0.0).abs() < 1e-3, "the floor is at the model's lowest point");
195             for c in [a.x, a.z] {
196                 assert!((c / g.step - (c / g.step).round()).abs() < 1e-3, "a line end off the grid: {c}");
197             }
198         }
199         // The line through the file's origin is a major one.
200         let origin_line = g.lines.chunks(2).find(|p| {
201             let a = Vec3::from(p[0].position) * fit.radius + fit.centre;
202             let b = Vec3::from(p[1].position) * fit.radius + fit.centre;
203             a.x.abs() < 1e-3 && b.x.abs() < 1e-3
204         });
205         assert_eq!(origin_line.unwrap()[0].color, GRID_MAJOR);
206     }
207 
208     #[test]
209     fn too_many_normals_are_thinned() {
210         let mut m = Mesh::default();
211         let n = 300; // 90,000 points: past the cap, so every other one
212         for i in 0..n {
213             for j in 0..n {
214                 m.positions.push(Vec3::new(i as f32, 0.0, j as f32));
215             }
216         }
217         for i in 0..n - 1 {
218             for j in 0..n - 1 {
219                 let a = (i * n + j) as u32;
220                 m.triangles.push([a, a + 1, a + n as u32]);
221                 m.triangles.push([a + 1, a + n as u32 + 1, a + n as u32]);
222             }
223         }
224         m.tri_material = vec![0; m.triangles.len()];
225         let lines = normals(&m, &m.corner_normals(40.0)).len() / 2;
226         assert!(lines <= MAX_NORMALS && lines > MAX_NORMALS / 3, "{lines}");
227     }
228 }