1use super::math::{self, V3};
9use super::{Mesh, geom};
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub enum Format {
14 Obj,
15 Stl,
16 Glb,
18 Gltf,
20 Ply,
21 Off,
23}
24
25impl Format {
26 pub const ALL: [Format; 6] = [Format::Glb, Format::Gltf, Format::Off, Format::Obj, Format::Ply, Format::Stl];
28
29 pub fn ext(self) -> &'static str {
30 match self {
31 Format::Obj => "obj",
32 Format::Stl => "stl",
33 Format::Glb => "glb",
34 Format::Gltf => "gltf",
35 Format::Ply => "ply",
36 Format::Off => "off",
37 }
38 }
39}
40
41fn token(name: &str, fallback: &str) -> String {
43 let t: String = name.trim().chars().map(|c| if c.is_whitespace() { '_' } else { c }).collect();
44 if t.is_empty() { fallback.to_string() } else { t }
45}
46
47pub fn obj<'a>(meshes: impl IntoIterator<Item = &'a Mesh>, mtl_name: &str) -> (String, String) {
50 use std::fmt::Write;
51 let meshes: Vec<&Mesh> = meshes.into_iter().collect();
52 let mut out = String::from("# Exported by Falcon\n");
53 let mut mtl = String::new();
54 let mut seen: Vec<String> = Vec::new();
55 if meshes.iter().any(|m| m.material.is_some()) {
56 let _ = writeln!(out, "mtllib {mtl_name}");
57 }
58 let (mut v_base, mut n_base) = (1usize, 1usize);
59 for (k, m) in meshes.iter().enumerate() {
60 let _ = writeln!(out, "o {}", token(&m.name, &format!("mesh{}", k + 1)));
61 for p in &m.positions {
62 let _ = writeln!(out, "v {} {} {}", p[0], p[1], p[2]);
63 }
64 let normals = m.normals.as_ref().filter(|n| n.len() == m.positions.len());
65 if let Some(ns) = normals {
66 for n in ns {
67 let _ = writeln!(out, "vn {} {} {}", n[0], n[1], n[2]);
68 }
69 }
70 if let Some(mat) = &m.material {
71 let name = token(&mat.name, &format!("material{}", seen.len() + 1));
72 if !seen.contains(&name) {
73 let c = mat.color;
74 let _ = writeln!(mtl, "newmtl {name}\nKd {} {} {}\nd {}\n", c[0], c[1], c[2], mat.opacity);
75 seen.push(name.clone());
76 }
77 let _ = writeln!(out, "usemtl {name}");
78 }
79 for t in m.indices.as_chunks::<3>().0 {
80 let [a, b, c] = t.map(|i| i as usize);
81 if normals.is_some() {
82 let _ = writeln!(out, "f {0}//{3} {1}//{4} {2}//{5}", a + v_base, b + v_base, c + v_base, a + n_base, b + n_base, c + n_base);
83 } else {
84 let _ = writeln!(out, "f {} {} {}", a + v_base, b + v_base, c + v_base);
85 }
86 }
87 v_base += m.positions.len();
88 if let Some(ns) = normals {
89 n_base += ns.len();
90 }
91 }
92 (out, mtl)
93}
94
95pub fn stl<'a>(meshes: impl IntoIterator<Item = &'a Mesh>) -> Vec<u8> {
98 let meshes: Vec<&Mesh> = meshes.into_iter().collect();
99 let count: usize = meshes.iter().map(|m| m.triangle_count()).sum();
100 let mut out = Vec::with_capacity(84 + count * 50);
101 let mut header = b"Exported by Falcon".to_vec();
102 header.resize(80, 0);
103 out.extend_from_slice(&header);
104 out.extend_from_slice(&(count as u32).to_le_bytes());
105 for m in meshes {
106 for t in m.indices.as_chunks::<3>().0 {
107 let p: [V3; 3] = t.map(|i| m.positions[i as usize]);
108 let n = geom::face_normal(p[0], p[1], p[2]);
109 for v in std::iter::once(n).chain(p) {
110 for c in v {
111 out.extend_from_slice(&c.to_le_bytes());
112 }
113 }
114 out.extend_from_slice(&[0, 0]);
115 }
116 }
117 out
118}
119
120fn linear(c: f32) -> f32 {
123 let c = c.clamp(0.0, 1.0);
124 if c <= 0.040_45 { c / 12.92 } else { ((c + 0.055) / 1.055).powf(2.4) }
125}
126
127fn gltf_parts<'a>(meshes: impl IntoIterator<Item = &'a Mesh>, uri: impl FnOnce(&[u8]) -> Option<String>) -> (serde_json::Value, Vec<u8>) {
131 use serde_json::{Value, json};
132 let mut bin: Vec<u8> = Vec::new();
133 let (mut views, mut accessors, mut gl_meshes, mut nodes, mut materials) = (Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::<Value>::new());
134 let mut mat_names: Vec<String> = Vec::new();
135 let mut push = |bin: &mut Vec<u8>, bytes: Vec<u8>, count: usize, ty: &str, component: u32, target: u32, minmax: Option<(V3, V3)>| {
137 while !bin.len().is_multiple_of(4) {
138 bin.push(0);
139 }
140 views.push(json!({"buffer": 0, "byteOffset": bin.len(), "byteLength": bytes.len(), "target": target}));
141 bin.extend_from_slice(&bytes);
142 let mut a = json!({"bufferView": views.len() - 1, "componentType": component, "count": count, "type": ty});
143 if let Some((lo, hi)) = minmax {
144 a["min"] = json!(lo);
145 a["max"] = json!(hi);
146 }
147 accessors.push(a);
148 accessors.len() - 1
149 };
150 let floats = |vs: &[V3]| vs.iter().flatten().flat_map(|c| c.to_le_bytes()).collect::<Vec<u8>>();
151 for (k, m) in meshes.into_iter().enumerate() {
152 if m.positions.is_empty() || m.indices.is_empty() {
153 continue;
154 }
155 let lo = m.positions.iter().fold([f32::INFINITY; 3], |a, p| [a[0].min(p[0]), a[1].min(p[1]), a[2].min(p[2])]);
156 let hi = m.positions.iter().fold([f32::NEG_INFINITY; 3], |a, p| [a[0].max(p[0]), a[1].max(p[1]), a[2].max(p[2])]);
157 const ARRAY: u32 = 34962;
158 const ELEMENTS: u32 = 34963;
159 const FLOAT: u32 = 5126;
160 let mut attrs = serde_json::Map::new();
161 attrs.insert("POSITION".into(), json!(push(&mut bin, floats(&m.positions), m.positions.len(), "VEC3", FLOAT, ARRAY, Some((lo, hi)))));
162 if let Some(ns) = m.normals.as_ref().filter(|n| n.len() == m.positions.len()) {
163 attrs.insert("NORMAL".into(), json!(push(&mut bin, floats(ns), ns.len(), "VEC3", FLOAT, ARRAY, None)));
164 }
165 if let Some(cs) = m.colors.as_ref().filter(|c| c.len() == m.positions.len()) {
166 let lin: Vec<V3> = cs.iter().map(|c| c.map(linear)).collect();
167 attrs.insert("COLOR_0".into(), json!(push(&mut bin, floats(&lin), lin.len(), "VEC3", FLOAT, ARRAY, None)));
168 }
169 let idx: Vec<u8> = m.indices.iter().flat_map(|i| i.to_le_bytes()).collect();
170 let indices = push(&mut bin, idx, m.indices.len(), "SCALAR", 5125, ELEMENTS, None);
171 let mut prim = json!({"attributes": attrs, "indices": indices, "mode": 4});
172 if let Some(mat) = &m.material {
173 let at = mat_names.iter().position(|n| *n == mat.name).unwrap_or_else(|| {
174 let c = mat.color.map(linear);
175 let mut v = json!({"name": mat.name, "pbrMetallicRoughness": {"baseColorFactor": [c[0], c[1], c[2], mat.opacity.clamp(0.0, 1.0)], "metallicFactor": 0.0}});
176 if mat.opacity < 1.0 {
177 v["alphaMode"] = json!("BLEND");
178 }
179 materials.push(v);
180 mat_names.push(mat.name.clone());
181 materials.len() - 1
182 });
183 prim["material"] = json!(at);
184 }
185 let name = if m.name.trim().is_empty() { format!("mesh{}", k + 1) } else { m.name.clone() };
186 gl_meshes.push(json!({"name": name, "primitives": [prim]}));
187 nodes.push(json!({"name": name, "mesh": gl_meshes.len() - 1}));
188 }
189 while !bin.len().is_multiple_of(4) {
190 bin.push(0);
191 }
192 let mut buffer = json!({"byteLength": bin.len()});
193 if let Some(u) = uri(&bin) {
194 buffer["uri"] = json!(u);
195 }
196 let mut doc = json!({
197 "asset": {"version": "2.0", "generator": "Falcon"},
198 "scene": 0,
199 "scenes": [{"nodes": (0..nodes.len()).collect::<Vec<_>>()}],
200 "nodes": nodes,
201 "meshes": gl_meshes,
202 });
203 if !bin.is_empty() {
204 doc["buffers"] = json!([buffer]);
205 doc["bufferViews"] = json!(views);
206 doc["accessors"] = json!(accessors);
207 }
208 if !materials.is_empty() {
209 doc["materials"] = json!(materials);
210 }
211 (doc, bin)
212}
213
214pub fn glb<'a>(meshes: impl IntoIterator<Item = &'a Mesh>) -> Vec<u8> {
217 let (doc, bin) = gltf_parts(meshes, |_| None);
218 let mut json = serde_json::to_vec(&doc).unwrap_or_default();
219 while !json.len().is_multiple_of(4) {
220 json.push(b' ');
221 }
222 let total = 12 + 8 + json.len() + if bin.is_empty() { 0 } else { 8 + bin.len() };
223 let mut out = Vec::with_capacity(total);
224 out.extend_from_slice(b"glTF");
225 out.extend_from_slice(&2u32.to_le_bytes());
226 out.extend_from_slice(&(total as u32).to_le_bytes());
227 out.extend_from_slice(&(json.len() as u32).to_le_bytes());
228 out.extend_from_slice(b"JSON");
229 out.extend_from_slice(&json);
230 if !bin.is_empty() {
231 out.extend_from_slice(&(bin.len() as u32).to_le_bytes());
232 out.extend_from_slice(b"BIN\0");
233 out.extend_from_slice(&bin);
234 }
235 out
236}
237
238pub fn gltf<'a>(meshes: impl IntoIterator<Item = &'a Mesh>) -> String {
240 use base64::Engine;
241 let (doc, _) = gltf_parts(meshes, |bin| Some(format!("data:application/octet-stream;base64,{}", base64::engine::general_purpose::STANDARD.encode(bin))));
242 serde_json::to_string_pretty(&doc).unwrap_or_default()
243}
244
245pub fn ply<'a>(meshes: impl IntoIterator<Item = &'a Mesh>) -> Vec<u8> {
250 let meshes: Vec<&Mesh> = meshes.into_iter().filter(|m| !m.positions.is_empty()).collect();
251 let vertices: usize = meshes.iter().map(|m| m.positions.len()).sum();
252 let faces: usize = meshes.iter().map(|m| m.triangle_count()).sum();
253 let normals = !meshes.is_empty() && meshes.iter().all(|m| m.normals.as_ref().is_some_and(|n| n.len() == m.positions.len()));
254 let colors = meshes.iter().any(|m| m.colors.is_some() || m.material.is_some());
255 let mut head = format!("ply\nformat binary_little_endian 1.0\ncomment Exported by Falcon\nelement vertex {vertices}\nproperty float x\nproperty float y\nproperty float z\n");
256 if normals {
257 head.push_str("property float nx\nproperty float ny\nproperty float nz\n");
258 }
259 if colors {
260 head.push_str("property uchar red\nproperty uchar green\nproperty uchar blue\n");
261 }
262 head.push_str(&format!("element face {faces}\nproperty list uchar int vertex_indices\nend_header\n"));
263 let mut out = head.into_bytes();
264 let byte = |c: f32| (c.clamp(0.0, 1.0) * 255.0).round() as u8;
265 for m in &meshes {
266 let fallback = m.material.as_ref().map_or(super::DEFAULT_COLOR, |mat| mat.color);
267 for (i, p) in m.positions.iter().enumerate() {
268 for c in p {
269 out.extend_from_slice(&c.to_le_bytes());
270 }
271 if normals && let Some(n) = m.normals.as_ref().and_then(|ns| ns.get(i)) {
272 for c in n {
273 out.extend_from_slice(&c.to_le_bytes());
274 }
275 }
276 if colors {
277 let c = m.colors.as_ref().and_then(|cs| cs.get(i)).copied().unwrap_or(fallback);
278 out.extend(c.map(byte));
279 }
280 }
281 }
282 let mut base = 0u32;
283 for m in &meshes {
284 for t in m.indices.as_chunks::<3>().0 {
285 out.push(3);
286 for i in t {
287 out.extend_from_slice(&((base + i) as i32).to_le_bytes());
288 }
289 }
290 base += m.positions.len() as u32;
291 }
292 out
293}
294
295pub fn off<'a>(meshes: impl IntoIterator<Item = &'a Mesh>) -> String {
299 use std::fmt::Write;
300 let meshes: Vec<&Mesh> = meshes.into_iter().filter(|m| !m.positions.is_empty()).collect();
301 let vertices: usize = meshes.iter().map(|m| m.positions.len()).sum();
302 let faces: usize = meshes.iter().map(|m| m.triangle_count()).sum();
303 let mut out = format!("OFF\n{vertices} {faces} 0\n");
304 for p in meshes.iter().flat_map(|m| &m.positions) {
305 let _ = writeln!(out, "{} {} {}", p[0], p[1], p[2]);
306 }
307 let mut base = 0usize;
308 for m in &meshes {
309 for t in m.indices.as_chunks::<3>().0 {
310 let [a, b, c] = t.map(|i| i as usize + base);
311 let _ = writeln!(out, "3 {a} {b} {c}");
312 }
313 base += m.positions.len();
314 }
315 out
316}
317
318pub fn write<'a>(meshes: impl IntoIterator<Item = &'a Mesh>, format: Format, path: &std::path::Path) -> std::io::Result<()> {
321 match format {
322 Format::Stl => std::fs::write(path, stl(meshes)),
323 Format::Glb => std::fs::write(path, glb(meshes)),
324 Format::Gltf => std::fs::write(path, gltf(meshes)),
325 Format::Ply => std::fs::write(path, ply(meshes)),
326 Format::Off => std::fs::write(path, off(meshes)),
327 Format::Obj => {
328 let mtl_path = path.with_extension("mtl");
329 let mtl_name = mtl_path.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
330 let (obj, mtl) = obj(meshes, &mtl_name);
331 std::fs::write(path, obj)?;
332 if !mtl.is_empty() {
333 std::fs::write(mtl_path, mtl)?;
334 }
335 Ok(())
336 }
337 }
338}
339
340#[derive(Clone, Copy, Debug, PartialEq)]
342pub struct Hit {
343 pub point: V3,
344 pub normal: V3,
346 pub mesh: usize,
348}
349
350pub fn pick(meshes: &[Mesh], visible: impl Fn(usize) -> bool, origin: V3, dir: V3) -> Option<Hit> {
354 let mut best: Option<(f32, Hit)> = None;
355 for (k, m) in meshes.iter().enumerate().filter(|(k, _)| visible(*k)) {
356 for t in m.indices.as_chunks::<3>().0 {
357 let [a, b, c] = t.map(|i| m.positions[i as usize]);
358 let (e1, e2) = (math::sub(b, a), math::sub(c, a));
359 let p = math::cross(dir, e2);
360 let det = math::dot(e1, p);
361 if det.abs() < 1e-12 {
362 continue;
363 }
364 let inv = 1.0 / det;
365 let s = math::sub(origin, a);
366 let u = math::dot(s, p) * inv;
367 if !(0.0..=1.0).contains(&u) {
368 continue;
369 }
370 let q = math::cross(s, e1);
371 let v = math::dot(dir, q) * inv;
372 if v < 0.0 || u + v > 1.0 {
373 continue;
374 }
375 let d = math::dot(e2, q) * inv;
376 if d > 0.0 && best.is_none_or(|(bd, _)| d < bd) {
377 let hit = Hit { point: math::add(origin, math::scale(dir, d)), normal: geom::face_normal(a, b, c), mesh: k };
378 best = Some((d, hit));
379 }
380 }
381 }
382 best.map(|(_, h)| h)
383}
384
385#[derive(Clone, Copy, Debug, PartialEq)]
389pub struct Measure {
390 pub distance: f32,
391 pub angle: f32,
392 pub parallel: Option<f32>,
393}
394
395pub fn measure(a: &Hit, b: &Hit) -> Measure {
396 let distance = math::length(math::sub(b.point, a.point));
397 let cos = math::dot(a.normal, b.normal).clamp(-1.0, 1.0);
398 let angle = cos.acos().to_degrees();
399 let parallel = (cos.abs() > 1.0 - 1e-5).then(|| math::dot(math::sub(b.point, a.point), a.normal).abs());
400 Measure { distance, angle, parallel }
401}
402
403#[cfg(test)]
404mod tests {
405 use super::super::Material;
406 use super::super::geom::tests::cube;
407 use super::*;
408
409 #[test]
410 fn obj_round_trips_through_the_reader() {
411 let mut m = cube(false);
412 m.name = "box one".into();
413 m.material = Some(Material { name: "red paint".into(), color: [1.0, 0.0, 0.0], opacity: 1.0 });
414 let second = Mesh { name: "".into(), ..cube(false) };
415 let (text, mtl) = obj([&m, &second], "x.mtl");
416 assert!(text.starts_with("# Exported by Falcon\nmtllib x.mtl\no box_one\n"));
417 assert!(mtl.contains("newmtl red_paint\nKd 1 0 0"));
418 assert!(text.contains("o mesh2"));
419 let side = |name: &str| (name == "x.mtl").then(|| mtl.clone().into_bytes());
420 let back = super::super::parse(text.as_bytes(), "obj", &side).unwrap();
421 assert_eq!(back.triangle_count(), 24);
422 assert_eq!(back.meshes[0].material.as_ref().map(|m| m.color), Some([1.0, 0.0, 0.0]));
423 }
424
425 #[test]
426 fn stl_round_trips_through_the_reader() {
427 let m = cube(false);
428 let bytes = stl([&m]);
429 assert_eq!(bytes.len(), 84 + 12 * 50);
430 let back = super::super::parse(&bytes, "stl", &|_| None).unwrap();
431 assert_eq!(back.triangle_count(), 12);
432 assert_eq!(back.bounds(), Some(([0.0; 3], [1.0; 3])));
433 }
434
435 fn painted() -> (Mesh, Mesh) {
436 let mut a = cube(true);
437 a.name = "box".into();
438 a.material = Some(Material { name: "red".into(), color: [0.8, 0.2, 0.1], opacity: 1.0 });
439 a.normals = Some(a.indices.as_chunks::<3>().0.iter().flat_map(|t| {
440 let n = geom::face_normal(a.positions[t[0] as usize], a.positions[t[1] as usize], a.positions[t[2] as usize]);
441 [n; 3]
442 }).collect());
443 let mut b = cube(false);
444 b.positions.iter_mut().for_each(|p| p[0] += 2.0);
445 b.colors = Some(vec![[0.0, 1.0, 0.5]; b.positions.len()]);
446 (a, b)
447 }
448
449 fn close(a: V3, b: V3) -> bool {
450 (0..3).all(|i| (a[i] - b[i]).abs() < 2e-3)
451 }
452
453 #[test]
454 fn glb_round_trips_through_the_reader() {
455 let (a, b) = painted();
456 let bytes = glb([&a, &b]);
457 assert_eq!(&bytes[..4], b"glTF");
458 assert_eq!(u32::from_le_bytes(bytes[8..12].try_into().unwrap()) as usize, bytes.len());
459 assert_eq!(bytes.len() % 4, 0);
460 let back = super::super::parse(&bytes, "glb", &|_| None).unwrap();
461 assert_eq!((back.meshes.len(), back.triangle_count(), back.vertex_count()), (2, 24, a.positions.len() + b.positions.len()));
462 assert_eq!(back.meshes[0].name, "box");
463 let mat = back.meshes[0].material.as_ref().unwrap();
464 assert!(close(mat.color, [0.8, 0.2, 0.1]), "sRGB survives the linear store: {:?}", mat.color);
465 assert!(back.meshes[0].normals.is_some());
466 assert!(close(back.meshes[1].colors.as_ref().unwrap()[0], [0.0, 1.0, 0.5]));
467 assert_eq!(back.bounds(), Some(([0.0; 3], [3.0, 1.0, 1.0])));
468 let empty = glb(std::iter::empty());
470 assert_eq!(u32::from_le_bytes(empty[8..12].try_into().unwrap()) as usize, empty.len());
471 }
472
473 #[test]
474 fn gltf_embeds_its_buffer() {
475 let (a, b) = painted();
476 let text = gltf([&a, &b]);
477 assert!(text.contains("data:application/octet-stream;base64,"));
478 let back = super::super::gltf::parse_gltf(text.as_bytes(), &|_| None).unwrap();
479 assert_eq!(back.triangle_count(), 24);
480 assert!(close(back.meshes[0].material.as_ref().unwrap().color, [0.8, 0.2, 0.1]));
481 }
482
483 #[test]
484 fn ply_round_trips_through_the_reader() {
485 let (a, b) = painted();
486 let bytes = ply([&a, &b]);
487 let head = String::from_utf8_lossy(&bytes[..bytes.windows(11).position(|w| w == b"end_header\n").unwrap()]).into_owned();
488 assert!(head.contains(&format!("element vertex {}\n", a.positions.len() + b.positions.len())));
489 assert!(!head.contains("nx"), "one mesh has no normals, so none are written");
490 let back = super::super::parse(&bytes, "ply", &|_| None).unwrap();
491 assert_eq!(back.triangle_count(), 24);
492 let cs = back.meshes[0].colors.as_ref().unwrap();
493 assert!(close(cs[0], [204.0 / 255.0, 51.0 / 255.0, 26.0 / 255.0]), "the material colour per vertex: {:?}", cs[0]);
494 assert!(close(cs[a.positions.len()], [0.0, 1.0, 128.0 / 255.0]));
495 assert_eq!(back.bounds(), Some(([0.0; 3], [3.0, 1.0, 1.0])));
496 let plain = ply([&cube(false)]);
498 assert!(!String::from_utf8_lossy(&plain).contains("red"));
499 assert_eq!(super::super::parse(&plain, "ply", &|_| None).unwrap().triangle_count(), 12);
500 }
501
502 #[test]
503 fn off_round_trips_through_the_reader() {
504 let (a, b) = painted();
505 let text = off([&a, &b]);
506 let nv = a.positions.len() + b.positions.len();
507 assert!(text.starts_with(&format!("OFF\n{nv} 24 0\n")), "{}", &text[..20]);
508 assert_eq!(text.lines().count(), 2 + nv + 24);
509 let back = super::super::off::parse(text.as_bytes()).unwrap();
510 let m = &back.meshes[0];
511 let all: Vec<V3> = a.positions.iter().chain(&b.positions).copied().collect();
512 assert_eq!(m.positions.len(), all.len());
513 assert!(m.positions.iter().zip(&all).all(|(p, q)| close(*p, *q)));
514 let want: Vec<u32> = a.indices.iter().copied().chain(b.indices.iter().map(|i| i + a.positions.len() as u32)).collect();
515 assert_eq!(m.indices, want);
516 assert_eq!(back.bounds(), Some(([0.0; 3], [3.0, 1.0, 1.0])));
517 }
518
519 #[test]
520 fn off_offsets_indices_per_mesh() {
521 let (a, b) = painted();
522 let text = off([&a, &b]);
523 let n = a.positions.len();
524 let last = text.lines().last().unwrap();
525 let ix: Vec<usize> = last.split(' ').skip(1).map(|s| s.parse().unwrap()).collect();
526 assert!(ix.iter().all(|&i| i >= n && i < n + b.positions.len()), "{last}");
527 assert!(off(std::iter::empty()).starts_with("OFF\n0 0 0\n"));
528 }
529
530 #[test]
531 fn off_reader_takes_comments_polygons_and_colours() {
532 let text = "OFF\n# comment\n\n4 1 0\n0 0 0\n1 0 0\n1 1 0 # c\n0 1 0\n4 0 1 2 3 255 0 0\n";
533 let m = &super::super::off::parse(text.as_bytes()).unwrap().meshes[0];
534 assert_eq!(m.indices, vec![0, 1, 2, 0, 2, 3]);
535 }
536
537 #[test]
538 fn rays_hit_the_nearest_visible_face() {
539 let meshes = vec![cube(false)];
540 let h = pick(&meshes, |_| true, [0.5, 0.5, 5.0], [0.0, 0.0, -1.0]).unwrap();
541 assert!((h.point[2] - 1.0).abs() < 1e-5, "{h:?}");
542 assert!((h.normal[2].abs() - 1.0).abs() < 1e-5);
543 assert!(pick(&meshes, |_| false, [0.5, 0.5, 5.0], [0.0, 0.0, -1.0]).is_none());
544 assert!(pick(&meshes, |_| true, [5.0, 5.0, 5.0], [0.0, 0.0, -1.0]).is_none());
545 let far = pick(&meshes, |_| true, [0.5, 0.5, -5.0], [0.0, 0.0, 1.0]).unwrap();
546 let m = measure(&h, &far);
547 assert!((m.distance - 1.0).abs() < 1e-5);
548 assert!((m.parallel.unwrap() - 1.0).abs() < 1e-5);
549 let side = pick(&meshes, |_| true, [5.0, 0.5, 0.5], [-1.0, 0.0, 0.0]).unwrap();
550 let m = measure(&h, &side);
551 assert!((m.angle - 90.0).abs() < 1e-3 && m.parallel.is_none());
552 }
553}