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 }