1use crate::{Diagnostic, MAX_COORDINATE_M, Mesh, Vec3};
7use alloc::{format, vec, vec::Vec};
8
9pub const MAX_PROCEDURAL_VERTICES: usize = 262_144;
11pub const MAX_PROCEDURAL_INDICES: usize = 1_572_864;
13
14pub fn suzanne() -> Result<Mesh, Diagnostic> {
23 let data = include_bytes!("../assets/suzanne.mesh");
24 let failure = || Diagnostic::plain("bundled Suzanne mesh is invalid");
25 let read_u32 = |start| {
26 data.get(start..start + 4)
27 .and_then(|bytes| bytes.try_into().ok())
28 .map(u32::from_le_bytes)
29 .ok_or_else(failure)
30 };
31 let count = read_u32(0)? as usize;
32 let index_count = read_u32(4)? as usize;
33 if count > MAX_PROCEDURAL_VERTICES
34 || index_count > MAX_PROCEDURAL_INDICES
35 || data.len() != 8 + count * 48 + index_count * 4
36 {
37 return Err(failure());
38 }
39 let read_f64 = |start| {
40 data.get(start..start + 8)
41 .and_then(|bytes| bytes.try_into().ok())
42 .map(f64::from_le_bytes)
43 .ok_or_else(failure)
44 };
45 let mut mesh = Mesh {
46 positions: Vec::with_capacity(count),
47 normals: Vec::with_capacity(count),
48 indices: Vec::with_capacity(index_count),
49 };
50 for i in 0..count {
51 let offset = 8 + i * 48;
52 mesh.positions.push(Vec3 {
53 x: read_f64(offset)?,
54 y: read_f64(offset + 8)?,
55 z: read_f64(offset + 16)?,
56 });
57 mesh.normals.push(Vec3 {
58 x: read_f64(offset + 24)?,
59 y: read_f64(offset + 32)?,
60 z: read_f64(offset + 40)?,
61 });
62 }
63 for i in 0..index_count {
64 mesh.indices.push(read_u32(8 + count * 48 + i * 4)?);
65 }
66 validate_mesh(&mesh)?;
67 Ok(mesh)
68}
69
70#[derive(Clone, Copy, Debug, PartialEq)]
72pub struct ProfilePoint {
73 pub radius_m: f64,
75 pub y_m: f64,
77}
78
79pub fn validate_mesh(mesh: &Mesh) -> Result<(), Diagnostic> {
87 if mesh.positions.is_empty() || mesh.positions.len() != mesh.normals.len() {
88 return Err(Diagnostic::plain(
89 "mesh positions and normals must be nonempty and have equal length",
90 ));
91 }
92 if mesh.positions.len() > MAX_PROCEDURAL_VERTICES || mesh.indices.len() > MAX_PROCEDURAL_INDICES
93 {
94 return Err(Diagnostic::plain(
95 "mesh exceeds procedural vertex or index budget",
96 ));
97 }
98 if mesh.indices.is_empty() || !mesh.indices.len().is_multiple_of(3) {
99 return Err(Diagnostic::plain(
100 "mesh indices must contain nonempty triangle triples",
101 ));
102 }
103 for position in &mesh.positions {
104 valid_vector(*position, "mesh position")?;
105 }
106 for normal in &mesh.normals {
107 valid_vector(*normal, "mesh normal")?;
108 if length(*normal) <= 1e-12 {
109 return Err(Diagnostic::plain("mesh normals must be nonzero"));
110 }
111 }
112 if mesh
113 .indices
114 .iter()
115 .any(|&index| index as usize >= mesh.positions.len())
116 {
117 return Err(Diagnostic::plain(
118 "mesh indices must address an existing vertex",
119 ));
120 }
121 Ok(())
122}
123
124impl Mesh {
125 pub fn validate(&self) -> Result<(), Diagnostic> {
127 validate_mesh(self)
128 }
129
130 pub fn translated(&self, offset_m: Vec3) -> Result<Self, Diagnostic> {
132 self.validate()?;
133 valid_vector(offset_m, "translation")?;
134 let mut result = self.clone();
135 for point in &mut result.positions {
136 *point = add(*point, offset_m);
137 valid_vector(*point, "translated position")?;
138 }
139 Ok(result)
140 }
141
142 pub fn rotated_xyz(&self, radians: Vec3) -> Result<Self, Diagnostic> {
144 self.validate()?;
145 valid_vector(radians, "rotation")?;
146 let mut result = self.clone();
147 for point in &mut result.positions {
148 *point = rotate_xyz(*point, radians);
149 valid_vector(*point, "rotated position")?;
150 }
151 for normal in &mut result.normals {
152 *normal = normalized(rotate_xyz(*normal, radians))?;
153 }
154 Ok(result)
155 }
156
157 pub fn scaled(&self, scale: Vec3) -> Result<Self, Diagnostic> {
160 self.validate()?;
161 valid_vector(scale, "scale")?;
162 if scale.x.abs() <= 1e-12 || scale.y.abs() <= 1e-12 || scale.z.abs() <= 1e-12 {
163 return Err(Diagnostic::plain("mesh scale axes must be nonzero"));
164 }
165 let mut result = self.clone();
166 for point in &mut result.positions {
167 *point = mul(*point, scale);
168 valid_vector(*point, "scaled position")?;
169 }
170 for normal in &mut result.normals {
171 *normal = normalized(Vec3 {
172 x: normal.x / scale.x,
173 y: normal.y / scale.y,
174 z: normal.z / scale.z,
175 })?;
176 }
177 if scale.x * scale.y * scale.z < 0.0 {
178 result.reverse_indices();
182 }
183 Ok(result)
184 }
185
186 pub fn reversed_winding(mut self) -> Self {
188 self.reverse_winding();
189 self
190 }
191
192 fn reverse_winding(&mut self) {
193 self.reverse_indices();
194 for normal in &mut self.normals {
195 *normal = Vec3 {
196 x: -normal.x,
197 y: -normal.y,
198 z: -normal.z,
199 };
200 }
201 }
202
203 fn reverse_indices(&mut self) {
204 for triangle in self.indices.as_chunks_mut::<3>().0 {
205 triangle.swap(1, 2);
206 }
207 }
208
209 pub fn map_positions<F>(&self, map: F) -> Result<Self, Diagnostic>
212 where
213 F: Fn(Vec3) -> Vec3,
214 {
215 self.validate()?;
216 let mut result = self.clone();
217 for point in &mut result.positions {
218 *point = map(*point);
219 valid_vector(*point, "mapped position")?;
220 }
221 result.normals = smooth_normals(&result.positions, &result.indices)?;
222 Ok(result)
223 }
224}
225
226pub fn grid(width_m: f64, depth_m: f64, columns: usize, rows: usize) -> Result<Mesh, Diagnostic> {
228 positive(width_m, "grid width")?;
229 positive(depth_m, "grid depth")?;
230 if columns == 0 || rows == 0 {
231 return Err(Diagnostic::plain("grid columns and rows must be positive"));
232 }
233 let vertices = (columns + 1)
234 .checked_mul(rows + 1)
235 .ok_or_else(|| Diagnostic::plain("grid vertex count overflow"))?;
236 let indices = columns
237 .checked_mul(rows)
238 .and_then(|n| n.checked_mul(6))
239 .ok_or_else(|| Diagnostic::plain("grid index count overflow"))?;
240 budget(vertices, indices)?;
241 let mut positions = Vec::with_capacity(vertices);
242 let mut normals = Vec::with_capacity(vertices);
243 let mut out = Vec::with_capacity(indices);
244 for row in 0..=rows {
245 for column in 0..=columns {
246 positions.push(Vec3 {
247 x: (column as f64 / columns as f64 - 0.5) * width_m,
248 y: 0.,
249 z: (row as f64 / rows as f64 - 0.5) * depth_m,
250 });
251 normals.push(Vec3 {
252 x: 0.,
253 y: 1.,
254 z: 0.,
255 });
256 }
257 }
258 for row in 0..rows {
259 for column in 0..columns {
260 let a = (row * (columns + 1) + column) as u32;
261 let b = a + columns as u32 + 1;
262 out.extend_from_slice(&[a, b, a + 1, a + 1, b, b + 1]);
263 }
264 }
265 Ok(Mesh {
266 positions,
267 normals,
268 indices: out,
269 })
270}
271
272pub fn cone(radius_m: f64, height_m: f64, segments: usize) -> Result<Mesh, Diagnostic> {
274 positive(radius_m, "cone radius")?;
275 positive(height_m, "cone height")?;
276 if segments < 3 {
277 return Err(Diagnostic::plain("cone requires at least three segments"));
278 }
279 budget(
280 segments + 2,
281 segments
282 .checked_mul(6)
283 .ok_or_else(|| Diagnostic::plain("cone index count overflow"))?,
284 )?;
285 let mut positions = Vec::with_capacity(segments + 2);
286 let mut normals = Vec::with_capacity(segments + 2);
287 let mut indices = Vec::with_capacity(segments * 6);
288 let slope = radius_m / height_m;
289 for index in 0..segments {
290 let angle = index as f64 / segments as f64 * core::f64::consts::TAU;
291 let (c, s) = (libm::cos(angle), libm::sin(angle));
292 positions.push(Vec3 {
293 x: radius_m * c,
294 y: -height_m / 2.,
295 z: radius_m * s,
296 });
297 normals.push(normalized(Vec3 {
298 x: c,
299 y: slope,
300 z: s,
301 })?);
302 }
303 let apex = segments as u32;
304 positions.push(Vec3 {
305 x: 0.,
306 y: height_m / 2.,
307 z: 0.,
308 });
309 normals.push(Vec3 {
310 x: 0.,
311 y: 1.,
312 z: 0.,
313 });
314 let center = apex + 1;
315 positions.push(Vec3 {
316 x: 0.,
317 y: -height_m / 2.,
318 z: 0.,
319 });
320 normals.push(Vec3 {
321 x: 0.,
322 y: -1.,
323 z: 0.,
324 });
325 for index in 0..segments {
326 let next = ((index + 1) % segments) as u32;
327 let index = index as u32;
328 indices.extend_from_slice(&[index, next, apex, center, next, index]);
329 }
330 Ok(Mesh {
331 positions,
332 normals,
333 indices,
334 })
335}
336
337pub fn uv_sphere(radius_m: f64, rings: usize, segments: usize) -> Result<Mesh, Diagnostic> {
339 positive(radius_m, "sphere radius")?;
340 if rings < 2 || segments < 3 {
341 return Err(Diagnostic::plain(
342 "sphere requires at least two rings and three segments",
343 ));
344 }
345 let vertices = (rings + 1)
346 .checked_mul(segments + 1)
347 .ok_or_else(|| Diagnostic::plain("sphere vertex count overflow"))?;
348 let indices = rings
349 .checked_mul(segments)
350 .and_then(|n| n.checked_mul(6))
351 .ok_or_else(|| Diagnostic::plain("sphere index count overflow"))?;
352 budget(vertices, indices)?;
353 let mut positions = Vec::with_capacity(vertices);
354 let mut normals = Vec::with_capacity(vertices);
355 let mut out = Vec::with_capacity(indices);
356 for ring in 0..=rings {
357 let phi = ring as f64 / rings as f64 * core::f64::consts::PI;
358 for segment in 0..=segments {
359 let theta = segment as f64 / segments as f64 * core::f64::consts::TAU;
360 let normal = Vec3 {
361 x: libm::sin(phi) * libm::cos(theta),
362 y: libm::cos(phi),
363 z: libm::sin(phi) * libm::sin(theta),
364 };
365 positions.push(Vec3 {
366 x: normal.x * radius_m,
367 y: normal.y * radius_m,
368 z: normal.z * radius_m,
369 });
370 normals.push(normal);
371 }
372 }
373 for ring in 0..rings {
374 for segment in 0..segments {
375 let a = (ring * (segments + 1) + segment) as u32;
376 let b = a + segments as u32 + 1;
377 out.extend_from_slice(&[a, a + 1, b, a + 1, b + 1, b]);
378 }
379 }
380 Ok(Mesh {
381 positions,
382 normals,
383 indices: out,
384 })
385}
386
387pub fn torus_mesh(
389 major_radius_m: f64,
390 minor_radius_m: f64,
391 rings: usize,
392 sides: usize,
393) -> Result<Mesh, Diagnostic> {
394 positive(major_radius_m, "torus major radius")?;
395 positive(minor_radius_m, "torus minor radius")?;
396 if minor_radius_m >= major_radius_m {
397 return Err(Diagnostic::plain(
398 "torus minor radius must be less than major radius",
399 ));
400 }
401 if rings < 3 || sides < 3 {
402 return Err(Diagnostic::plain(
403 "torus requires at least three rings and sides",
404 ));
405 }
406 let vertices = (rings + 1)
407 .checked_mul(sides + 1)
408 .ok_or_else(|| Diagnostic::plain("torus vertex count overflow"))?;
409 let indices = rings
410 .checked_mul(sides)
411 .and_then(|n| n.checked_mul(6))
412 .ok_or_else(|| Diagnostic::plain("torus index count overflow"))?;
413 budget(vertices, indices)?;
414 let mut positions = Vec::with_capacity(vertices);
415 let mut normals = Vec::with_capacity(vertices);
416 let mut out = Vec::with_capacity(indices);
417 for ring in 0..=rings {
418 let u = ring as f64 / rings as f64 * core::f64::consts::TAU;
419 let (cu, su) = (libm::cos(u), libm::sin(u));
420 for side in 0..=sides {
421 let v = side as f64 / sides as f64 * core::f64::consts::TAU;
422 let (cv, sv) = (libm::cos(v), libm::sin(v));
423 let normal = Vec3 {
424 x: cu * cv,
425 y: sv,
426 z: su * cv,
427 };
428 positions.push(Vec3 {
429 x: (major_radius_m + minor_radius_m * cv) * cu,
430 y: minor_radius_m * sv,
431 z: (major_radius_m + minor_radius_m * cv) * su,
432 });
433 normals.push(normal);
434 }
435 }
436 for ring in 0..rings {
437 for side in 0..sides {
438 let a = (ring * (sides + 1) + side) as u32;
439 let b = a + sides as u32 + 1;
440 out.extend_from_slice(&[a, a + 1, b, a + 1, b + 1, b]);
441 }
442 }
443 Ok(Mesh {
444 positions,
445 normals,
446 indices: out,
447 })
448}
449
450pub fn lathe(profile: &[ProfilePoint], segments: usize) -> Result<Mesh, Diagnostic> {
453 if profile.len() < 2 || segments < 3 {
454 return Err(Diagnostic::plain(
455 "lathe requires at least two profile points and three segments",
456 ));
457 }
458 for point in profile {
459 if !point.radius_m.is_finite()
460 || point.radius_m < 0.
461 || point.radius_m > MAX_COORDINATE_M
462 || !point.y_m.is_finite()
463 || point.y_m.abs() > MAX_COORDINATE_M
464 {
465 return Err(Diagnostic::plain(
466 "lathe profile must be finite, bounded, and have nonnegative radii",
467 ));
468 }
469 }
470 let columns = segments
471 .checked_add(1)
472 .ok_or_else(|| Diagnostic::plain("lathe segment overflow"))?;
473 let vertices = profile
474 .len()
475 .checked_mul(columns)
476 .ok_or_else(|| Diagnostic::plain("lathe vertex count overflow"))?;
477 let indices = (profile.len() - 1)
478 .checked_mul(segments)
479 .and_then(|n| n.checked_mul(6))
480 .ok_or_else(|| Diagnostic::plain("lathe index count overflow"))?;
481 budget(vertices, indices)?;
482 let mut positions = Vec::with_capacity(vertices);
483 let mut out = Vec::with_capacity(indices);
484 for point in profile {
485 for segment in 0..=segments {
486 let angle = segment as f64 / segments as f64 * core::f64::consts::TAU;
487 positions.push(Vec3 {
488 x: point.radius_m * libm::cos(angle),
489 y: point.y_m,
490 z: point.radius_m * libm::sin(angle),
491 });
492 }
493 }
494 for row in 0..profile.len() - 1 {
495 for column in 0..segments {
496 let a = (row * columns + column) as u32;
497 let b = a + columns as u32;
498 out.extend_from_slice(&[a, a + 1, b, a + 1, b + 1, b]);
499 }
500 }
501 let normals = smooth_normals(&positions, &out)?;
502 Ok(Mesh {
503 positions,
504 normals,
505 indices: out,
506 })
507}
508
509pub fn parametric_surface<F>(
511 u_segments: usize,
512 v_segments: usize,
513 sample: F,
514) -> Result<Mesh, Diagnostic>
515where
516 F: Fn(f64, f64) -> Vec3,
517{
518 if u_segments == 0 || v_segments == 0 {
519 return Err(Diagnostic::plain(
520 "parametric surface segments must be positive",
521 ));
522 }
523 let vertices = (u_segments + 1)
524 .checked_mul(v_segments + 1)
525 .ok_or_else(|| Diagnostic::plain("parametric surface vertex count overflow"))?;
526 let indices = u_segments
527 .checked_mul(v_segments)
528 .and_then(|n| n.checked_mul(6))
529 .ok_or_else(|| Diagnostic::plain("parametric surface index count overflow"))?;
530 budget(vertices, indices)?;
531 let mut positions = Vec::with_capacity(vertices);
532 let mut out = Vec::with_capacity(indices);
533 for v in 0..=v_segments {
534 for u in 0..=u_segments {
535 let point = sample(u as f64 / u_segments as f64, v as f64 / v_segments as f64);
536 valid_vector(point, "parametric sample")?;
537 positions.push(point);
538 }
539 }
540 for v in 0..v_segments {
541 for u in 0..u_segments {
542 let a = (v * (u_segments + 1) + u) as u32;
543 let b = a + u_segments as u32 + 1;
544 out.extend_from_slice(&[a, a + 1, b, a + 1, b + 1, b]);
545 }
546 }
547 let normals = smooth_normals(&positions, &out)?;
548 Ok(Mesh {
549 positions,
550 normals,
551 indices: out,
552 })
553}
554
555fn budget(vertices: usize, indices: usize) -> Result<(), Diagnostic> {
556 if vertices > MAX_PROCEDURAL_VERTICES
557 || indices > MAX_PROCEDURAL_INDICES
558 || vertices > u32::MAX as usize
559 {
560 Err(Diagnostic::plain(
561 "procedural mesh exceeds vertex or index budget",
562 ))
563 } else {
564 Ok(())
565 }
566}
567fn positive(value: f64, name: &str) -> Result<(), Diagnostic> {
568 if value.is_finite() && value > 0. && value <= MAX_COORDINATE_M {
569 Ok(())
570 } else {
571 Err(Diagnostic::plain(format!(
572 "{name} must be finite and in 0..={MAX_COORDINATE_M}"
573 )))
574 }
575}
576fn valid_vector(value: Vec3, name: &str) -> Result<(), Diagnostic> {
577 if [value.x, value.y, value.z]
578 .into_iter()
579 .all(|v| v.is_finite() && v.abs() <= MAX_COORDINATE_M)
580 {
581 Ok(())
582 } else {
583 Err(Diagnostic::plain(format!(
584 "{name} must be finite and within coordinate bounds"
585 )))
586 }
587}
588fn add(a: Vec3, b: Vec3) -> Vec3 {
589 Vec3 {
590 x: a.x + b.x,
591 y: a.y + b.y,
592 z: a.z + b.z,
593 }
594}
595fn mul(a: Vec3, b: Vec3) -> Vec3 {
596 Vec3 {
597 x: a.x * b.x,
598 y: a.y * b.y,
599 z: a.z * b.z,
600 }
601}
602fn length(v: Vec3) -> f64 {
603 libm::hypot(libm::hypot(v.x, v.y), v.z)
604}
605fn normalized(v: Vec3) -> Result<Vec3, Diagnostic> {
606 let l = length(v);
607 if l <= 1e-12 || !l.is_finite() {
608 Err(Diagnostic::plain("normal must be finite and nonzero"))
609 } else {
610 Ok(Vec3 {
611 x: v.x / l,
612 y: v.y / l,
613 z: v.z / l,
614 })
615 }
616}
617fn rotate_xyz(v: Vec3, r: Vec3) -> Vec3 {
618 let (sx, cx) = (libm::sin(r.x), libm::cos(r.x));
619 let (sy, cy) = (libm::sin(r.y), libm::cos(r.y));
620 let (sz, cz) = (libm::sin(r.z), libm::cos(r.z));
621 let y = v.y * cx - v.z * sx;
622 let z = v.y * sx + v.z * cx;
623 let x = v.x * cy + z * sy;
624 let z = -v.x * sy + z * cy;
625 Vec3 {
626 x: x * cz - y * sz,
627 y: x * sz + y * cz,
628 z,
629 }
630}
631fn smooth_normals(positions: &[Vec3], indices: &[u32]) -> Result<Vec<Vec3>, Diagnostic> {
632 let mut normals = vec![Vec3::ZERO; positions.len()];
633 for triangle in indices.as_chunks::<3>().0 {
634 let a = positions[triangle[0] as usize];
635 let b = positions[triangle[1] as usize];
636 let c = positions[triangle[2] as usize];
637 let ab = Vec3 {
638 x: b.x - a.x,
639 y: b.y - a.y,
640 z: b.z - a.z,
641 };
642 let ac = Vec3 {
643 x: c.x - a.x,
644 y: c.y - a.y,
645 z: c.z - a.z,
646 };
647 let n = Vec3 {
648 x: ab.y * ac.z - ab.z * ac.y,
649 y: ab.z * ac.x - ab.x * ac.z,
650 z: ab.x * ac.y - ab.y * ac.x,
651 };
652 if length(n) <= 1e-18 {
653 return Err(Diagnostic::plain(
654 "procedural mesh contains a degenerate triangle",
655 ));
656 }
657 for &i in triangle {
658 normals[i as usize] = add(normals[i as usize], n);
659 }
660 }
661 normals.into_iter().map(normalized).collect()
662}
663
664#[cfg(test)]
665mod tests {
666 use super::*;
667 #[test]
668 fn procedural_meshes_are_valid_and_negative_scale_preserves_faces() {
669 let mesh = grid(2., 2., 2, 2).unwrap();
670 assert!(mesh.validate().is_ok());
671 let mirrored = mesh
672 .scaled(Vec3 {
673 x: -1.,
674 y: 1.,
675 z: 1.,
676 })
677 .unwrap();
678 assert_eq!(mirrored.indices[0..3], [0, 1, 3]);
679 for triangle in mirrored.indices.as_chunks::<3>().0 {
680 let a = mirrored.positions[triangle[0] as usize];
681 let b = mirrored.positions[triangle[1] as usize];
682 let c = mirrored.positions[triangle[2] as usize];
683 let cross = Vec3 {
684 x: (b.y - a.y) * (c.z - a.z) - (b.z - a.z) * (c.y - a.y),
685 y: (b.z - a.z) * (c.x - a.x) - (b.x - a.x) * (c.z - a.z),
686 z: (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x),
687 };
688 let normal = mirrored.normals[triangle[0] as usize];
689 assert!(
690 cross.x * normal.x + cross.y * normal.y + cross.z * normal.z > 0.0,
691 "mirrored face winding must agree with transformed normals"
692 );
693 }
694 assert!(cone(1., 2., 16).unwrap().validate().is_ok());
695 assert!(
696 lathe(
697 &[
698 ProfilePoint {
699 radius_m: 0.2,
700 y_m: -1.
701 },
702 ProfilePoint {
703 radius_m: 0.8,
704 y_m: 0.
705 },
706 ProfilePoint {
707 radius_m: 0.3,
708 y_m: 1.
709 }
710 ],
711 16
712 )
713 .unwrap()
714 .validate()
715 .is_ok()
716 );
717 }
718 #[test]
719 fn parametric_surface_rejects_degenerate_or_nonfinite() {
720 assert!(parametric_surface(1, 1, |_, _| Vec3::ZERO).is_err());
721 assert!(
722 parametric_surface(1, 1, |_, _| Vec3 {
723 x: f64::NAN,
724 y: 0.,
725 z: 0.
726 })
727 .is_err()
728 );
729 }
730 #[test]
731 fn bundled_suzanne_is_a_valid_bounded_mesh() {
732 let mesh = suzanne().expect("bundled Suzanne");
733 assert_eq!(mesh.positions.len(), 2_012);
734 assert_eq!(mesh.normals.len(), mesh.positions.len());
735 assert_eq!(mesh.indices.len(), 3_936 * 3);
736 mesh.validate().expect("valid bundled mesh");
737 }
738}