1use std::collections::HashMap;
121use std::fmt;
122use std::sync::Arc;
123
124use openbim_ifcx::{ComposedNode, Composition};
125use serde_json::Value;
126
127use crate::curves::{self, CurveGeometry, UnsupportedCurve};
128use crate::math::{cross, sub, Vec3};
129use crate::mesh::{self, TriangleMesh};
130use crate::points::{PointCloud, PCD_BASE64, POINTS_ARRAY, POINTS_BASE64};
131use crate::presentation::{BasicMaterial, Material, NodePresentation, Visibility};
132use crate::transform::{self, world_from_parent, Transform};
133use crate::DecodeError;
134
135#[derive(Debug, Clone, Copy, PartialEq, Default)]
137pub enum Origin {
138 #[default]
140 Auto,
141 At(Vec3),
144}
145
146#[derive(Debug, Clone, PartialEq)]
148#[non_exhaustive]
149pub struct SceneOptions {
150 pub origin: Origin,
152 pub max_visits: usize,
156 pub max_path_bytes: usize,
161}
162
163impl Default for SceneOptions {
164 fn default() -> Self {
165 Self {
166 origin: Origin::Auto,
167 max_visits: Self::DEFAULT_MAX_VISITS,
168 max_path_bytes: Self::DEFAULT_MAX_PATH_BYTES,
169 }
170 }
171}
172
173impl SceneOptions {
174 pub const DEFAULT_MAX_VISITS: usize = 10_000_000;
176 pub const DEFAULT_MAX_PATH_BYTES: usize = 1 << 30;
178
179 pub fn with_origin(mut self, origin: Origin) -> Self {
181 self.origin = origin;
182 self
183 }
184
185 pub fn with_max_visits(mut self, max_visits: usize) -> Self {
187 self.max_visits = max_visits;
188 self
189 }
190
191 pub fn with_max_path_bytes(mut self, max_path_bytes: usize) -> Self {
193 self.max_path_bytes = max_path_bytes;
194 self
195 }
196}
197
198#[derive(Debug, Clone, Copy, PartialEq)]
200pub struct Bounds {
201 pub min: Vec3,
202 pub max: Vec3,
203}
204
205impl Bounds {
206 pub fn from_points<I: IntoIterator<Item = Vec3>>(points: I) -> Option<Self> {
208 let mut points = points.into_iter();
209 let first = points.next()?;
210 let mut bounds = Self {
211 min: first,
212 max: first,
213 };
214 for p in points {
215 bounds.include(p);
216 }
217 Some(bounds)
218 }
219
220 pub fn include(&mut self, p: Vec3) {
222 for (i, c) in p.into_iter().enumerate() {
223 self.min[i] = self.min[i].min(c);
224 self.max[i] = self.max[i].max(c);
225 }
226 }
227
228 pub fn union(&self, other: &Bounds) -> Bounds {
230 let mut out = *self;
231 out.include(other.min);
232 out.include(other.max);
233 out
234 }
235
236 pub fn center(&self) -> Vec3 {
238 [0, 1, 2].map(|i| (self.min[i] + self.max[i]) / 2.0)
239 }
240
241 pub fn size(&self) -> Vec3 {
243 [0, 1, 2].map(|i| self.max[i] - self.min[i])
244 }
245}
246
247#[derive(Debug, Clone, PartialEq)]
249pub struct MeshBuffer {
250 pub anchor: Vec3,
253 pub positions: Vec<[f32; 3]>,
255 pub normals: Vec<[f32; 3]>,
257 pub indices: Vec<u32>,
259}
260
261impl MeshBuffer {
262 pub fn triangle_count(&self) -> usize {
264 self.indices.len() / 3
265 }
266}
267
268#[derive(Debug, Clone, PartialEq)]
271pub struct LineBuffer {
272 pub anchor: Vec3,
274 pub positions: Vec<[f32; 3]>,
276 pub indices: Vec<u32>,
279}
280
281impl LineBuffer {
282 pub fn segment_count(&self) -> usize {
284 self.indices.len() / 2
285 }
286}
287
288#[derive(Debug, Clone, PartialEq)]
290pub struct PointBuffer {
291 pub anchor: Vec3,
293 pub positions: Vec<[f32; 3]>,
295 pub colors: Option<Vec<[f32; 3]>>,
297}
298
299#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
301pub enum GeometryRef {
302 Mesh(usize),
304 Lines(usize),
306 Points(usize),
308}
309
310#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
312pub enum GeometryKind {
313 Mesh,
314 Lines,
315 Points,
316}
317
318impl GeometryRef {
319 pub fn kind(&self) -> GeometryKind {
321 match self {
322 Self::Mesh(_) => GeometryKind::Mesh,
323 Self::Lines(_) => GeometryKind::Lines,
324 Self::Points(_) => GeometryKind::Points,
325 }
326 }
327}
328
329#[derive(Debug, Clone, PartialEq)]
331pub struct Instance {
332 pub path: String,
337 pub node: String,
339 pub geometry: GeometryRef,
341 pub material: usize,
343 pub world: Transform,
345 pub matrix: [f32; 16],
348 pub bounds: Bounds,
350}
351
352impl Instance {
353 pub fn mirrored(&self) -> bool {
355 self.world.determinant() < 0.0
356 }
357}
358
359#[derive(Debug, Clone, PartialEq)]
361#[non_exhaustive]
362pub enum SceneWarningKind {
363 Decode(DecodeError),
365 UnsupportedCurve(UnsupportedCurve),
367 LimitReached { option: &'static str, limit: usize },
372 OutOfRange,
377}
378
379#[derive(Debug, Clone, PartialEq)]
381pub struct SceneWarning {
382 pub path: String,
384 pub attribute: &'static str,
386 pub kind: SceneWarningKind,
387}
388
389impl fmt::Display for SceneWarning {
390 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
391 write!(f, "{}", display_path(&self.path))?;
392 if !self.attribute.is_empty() {
393 write!(f, " {}", self.attribute)?;
394 }
395 f.write_str(": ")?;
396 match &self.kind {
397 SceneWarningKind::Decode(e) => {
398 let message = e.to_string();
400 let prefix = format!("{}: ", self.attribute);
401 write!(f, "{}", message.strip_prefix(&prefix).unwrap_or(&message))
402 }
403 SceneWarningKind::UnsupportedCurve(c) => {
404 write!(f, "curve type {:?} is not supported", c.curve_type)?;
405 if let Some(basis) = &c.basis {
406 write!(f, " (basis {basis:?})")?;
407 }
408 Ok(())
409 }
410 SceneWarningKind::LimitReached { option, limit } => {
411 write!(
412 f,
413 "scene walk stopped at SceneOptions::{option} = {limit}; \
414 the rest of the tree is left out"
415 )
416 }
417 SceneWarningKind::OutOfRange => f.write_str("coordinates out of f32 range; left out"),
418 }
419 }
420}
421
422fn display_path(path: &str) -> &str {
423 if path.is_empty() {
424 "<root>"
425 } else {
426 path
427 }
428}
429
430#[derive(Debug, Clone, PartialEq, Default)]
432pub struct RenderScene {
433 pub instances: Vec<Instance>,
435 pub meshes: Vec<MeshBuffer>,
436 pub lines: Vec<LineBuffer>,
437 pub points: Vec<PointBuffer>,
438 pub materials: Vec<Material>,
439 pub bounds: Option<Bounds>,
441 pub origin: Vec3,
444 pub warnings: Vec<SceneWarning>,
445}
446
447impl RenderScene {
448 pub fn from_composition(composition: &Composition, options: &SceneOptions) -> Self {
474 Self::from_root(&composition.root(), options)
475 }
476
477 pub fn from_root(root: &ComposedNode, options: &SceneOptions) -> Self {
481 let mut builder = Builder::default();
482 builder.walk(root, options);
483 builder.finish(options)
484 }
485
486 pub fn triangle_count(&self) -> usize {
488 self.instances
489 .iter()
490 .map(|i| match i.geometry {
491 GeometryRef::Mesh(m) => self.meshes[m].triangle_count(),
492 _ => 0,
493 })
494 .sum()
495 }
496
497 pub fn segment_count(&self) -> usize {
499 self.instances
500 .iter()
501 .map(|i| match i.geometry {
502 GeometryRef::Lines(l) => self.lines[l].segment_count(),
503 _ => 0,
504 })
505 .sum()
506 }
507
508 pub fn point_count(&self) -> usize {
510 self.instances
511 .iter()
512 .map(|i| match i.geometry {
513 GeometryRef::Points(p) => self.points[p].positions.len(),
514 _ => 0,
515 })
516 .sum()
517 }
518
519 pub fn instances_at(&self, path: &str) -> &[Instance] {
523 let Some(start) = self.instances.iter().position(|i| i.path == path) else {
524 return &[];
525 };
526 let len = self.instances[start..]
527 .iter()
528 .take_while(|i| i.path == path)
529 .count();
530 &self.instances[start..start + len]
531 }
532
533 fn anchor(&self, geometry: GeometryRef) -> Vec3 {
534 match geometry {
535 GeometryRef::Mesh(i) => self.meshes[i].anchor,
536 GeometryRef::Lines(i) => self.lines[i].anchor,
537 GeometryRef::Points(i) => self.points[i].anchor,
538 }
539 }
540}
541
542#[derive(Clone, Copy)]
544struct NodeInfo {
545 local: Option<Option<Transform>>,
547 invisible: bool,
548 pbr: Option<usize>,
549 basic: Option<usize>,
550 mesh: Option<usize>,
551 lines: Option<usize>,
552 points: Option<(usize, bool)>,
553}
554
555#[derive(Clone, Copy)]
557struct Context {
558 world: Transform,
559 pbr: Option<usize>,
560 basic: Option<usize>,
561}
562
563type Key = *const ();
565
566#[derive(Default)]
567struct Builder {
568 scene: RenderScene,
569 nodes: HashMap<Key, NodeInfo>,
570 meshes: HashMap<Key, Option<usize>>,
571 lines: HashMap<Key, Option<usize>>,
572 points: HashMap<Key, Option<(usize, bool)>>,
573 materials: HashMap<String, usize>,
574 local: HashMap<GeometryRef, (Vec<Vec3>, &'static str)>,
577}
578
579const MAX_COORDINATE: f64 = f32::MAX as f64 / 4.0;
583
584fn in_range(bounds: &Bounds) -> bool {
585 bounds
586 .min
587 .iter()
588 .chain(&bounds.max)
589 .all(|c| c.abs() <= MAX_COORDINATE)
590}
591
592fn all_finite(values: &[[f32; 3]]) -> bool {
593 values.iter().flatten().all(|v| v.is_finite())
594}
595
596fn key<T>(value: &T) -> Key {
597 (value as *const T).cast()
598}
599
600impl Builder {
601 fn walk(&mut self, root: &ComposedNode, options: &SceneOptions) {
602 let mut visits = 1usize;
604 let mut path_bytes = 0usize;
605 if options.max_visits == 0 {
606 self.limit_reached("", "max_visits", 0);
607 return;
608 }
609 let mut stack: Vec<(&ComposedNode, String, Context)> = vec![(
610 root,
611 String::new(),
612 Context {
613 world: Transform::IDENTITY,
614 pbr: None,
615 basic: None,
616 },
617 )];
618 while let Some((node, path, parent)) = stack.pop() {
619 let info = self.info(node, &path);
620 let Some(local) = info.local else { continue };
621 if info.invisible {
622 continue;
623 }
624 let context = Context {
625 world: world_from_parent(&parent.world, local.as_ref()),
626 pbr: info.pbr.or(parent.pbr),
627 basic: info.basic.or(parent.basic),
628 };
629 let basic = || context.basic;
630 if let Some(mesh) = info.mesh {
631 let material = match context.pbr.or(basic()) {
632 Some(m) => m,
633 None => self.material(Material::Basic(BasicMaterial::DEFAULT)),
634 };
635 self.instance(&path, node, GeometryRef::Mesh(mesh), material, &context);
636 }
637 if let Some(lines) = info.lines {
638 let material = match basic() {
639 Some(m) => m,
640 None => self.material(Material::Basic(BasicMaterial::DEFAULT)),
641 };
642 self.instance(&path, node, GeometryRef::Lines(lines), material, &context);
643 }
644 if let Some((points, colored)) = info.points {
645 let material = self.material(Material::Basic(if colored {
646 POINT_COLORS
647 } else {
648 POINT_DEFAULT
649 }));
650 self.instance(&path, node, GeometryRef::Points(points), material, &context);
651 }
652 for (name, child) in node.children.iter().rev() {
653 visits += 1;
654 if visits > options.max_visits {
655 self.limit_reached(&path, "max_visits", options.max_visits);
656 return;
657 }
658 path_bytes = path_bytes
661 .saturating_add(path.len())
662 .saturating_add(name.len())
663 .saturating_add(1);
664 if path_bytes > options.max_path_bytes {
665 self.limit_reached(&path, "max_path_bytes", options.max_path_bytes);
666 return;
667 }
668 let child_path = if path.is_empty() {
669 name.clone()
670 } else {
671 format!("{path}/{name}")
672 };
673 stack.push((child, child_path, context));
674 }
675 }
676 }
677
678 fn instance(
679 &mut self,
680 path: &str,
681 node: &ComposedNode,
682 geometry: GeometryRef,
683 material: usize,
684 context: &Context,
685 ) {
686 self.scene.instances.push(Instance {
687 path: path.to_owned(),
688 node: node.path.clone(),
689 geometry,
690 material,
691 world: context.world,
692 matrix: [0.0; 16],
693 bounds: Bounds {
694 min: [0.0; 3],
695 max: [0.0; 3],
696 },
697 });
698 }
699
700 fn material(&mut self, material: Material) -> usize {
701 let materials = &mut self.scene.materials;
702 *self
703 .materials
704 .entry(format!("{material:?}"))
705 .or_insert_with(|| {
706 materials.push(material);
707 materials.len() - 1
708 })
709 }
710
711 fn fits<B>(
714 &mut self,
715 buffer: Option<B>,
716 path: &str,
717 attribute: &'static str,
718 positions: impl Fn(&B) -> &[[f32; 3]],
719 ) -> Option<B> {
720 let buffer = buffer?;
721 if all_finite(positions(&buffer)) {
722 Some(buffer)
723 } else {
724 self.warn(path, attribute, SceneWarningKind::OutOfRange);
725 None
726 }
727 }
728
729 fn limit_reached(&mut self, path: &str, option: &'static str, limit: usize) {
730 self.warn(path, "", SceneWarningKind::LimitReached { option, limit });
731 }
732
733 fn warn(&mut self, path: &str, attribute: &'static str, kind: SceneWarningKind) {
734 self.scene.warnings.push(SceneWarning {
735 path: path.to_owned(),
736 attribute,
737 kind,
738 });
739 }
740
741 fn info(&mut self, node: &ComposedNode, path: &str) -> NodeInfo {
742 if let Some(info) = self.nodes.get(&key(node)) {
743 return *info;
744 }
745 let attribute = |id: &str| node.attributes.get(id).filter(|v| !v.is_null());
746
747 let local = match attribute(transform::ATTRIBUTE) {
748 None => Some(None),
749 Some(value) => match Transform::from_attribute(value) {
750 Ok(t) => Some(Some(t)),
751 Err(e) => {
752 self.warn(path, transform::ATTRIBUTE, SceneWarningKind::Decode(e));
753 None
754 }
755 },
756 };
757
758 let mut errors = Vec::new();
759 let presentation = NodePresentation::from_attributes_lenient(node, &mut errors);
760 for (attribute, e) in errors {
761 self.warn(path, attribute, SceneWarningKind::Decode(e));
762 }
763 let pbr = presentation
764 .gltf_material
765 .map(|m| self.material(Material::Pbr(m)));
766 let basic = presentation.diffuse_color.map(|color| {
767 self.material(Material::Basic(BasicMaterial {
768 color,
769 opacity: presentation.opacity.unwrap_or(1.0),
770 }))
771 });
772
773 let mesh = attribute(mesh::ATTRIBUTE).and_then(|v| self.mesh(v, path));
774 let lines = attribute(curves::ATTRIBUTE).and_then(|v| self.lines(v, path));
775 let points = [PCD_BASE64, POINTS_ARRAY, POINTS_BASE64]
776 .into_iter()
777 .find_map(|id| attribute(id).map(|v| (id, v)))
778 .and_then(|(id, v)| self.points(id, v, path));
779
780 let info = NodeInfo {
781 local,
782 invisible: presentation.visibility == Some(Visibility::Invisible),
783 pbr,
784 basic,
785 mesh,
786 lines,
787 points,
788 };
789 self.nodes.insert(key(node), info);
790 info
791 }
792
793 fn mesh(&mut self, value: &Arc<Value>, path: &str) -> Option<usize> {
794 if let Some(id) = self.meshes.get(&key(&**value)) {
795 return *id;
796 }
797 let id = match TriangleMesh::from_attribute(value) {
798 Ok(mesh) => self
799 .fits(mesh_buffer(&mesh), path, mesh::ATTRIBUTE, |b| &b.positions)
800 .map(|buffer| {
801 self.scene.meshes.push(buffer);
802 let id = self.scene.meshes.len() - 1;
803 self.local
804 .insert(GeometryRef::Mesh(id), (mesh.positions, mesh::ATTRIBUTE));
805 id
806 }),
807 Err(e) => {
808 self.warn(path, mesh::ATTRIBUTE, SceneWarningKind::Decode(e));
809 None
810 }
811 };
812 self.meshes.insert(key(&**value), id);
813 id
814 }
815
816 fn lines(&mut self, value: &Arc<Value>, path: &str) -> Option<usize> {
817 if let Some(id) = self.lines.get(&key(&**value)) {
818 return *id;
819 }
820 let id = match CurveGeometry::from_attribute(value) {
821 Ok(CurveGeometry::Polylines(polylines)) => self
822 .fits(line_buffer(&polylines), path, curves::ATTRIBUTE, |b| {
823 &b.positions
824 })
825 .map(|buffer| {
826 self.scene.lines.push(buffer);
827 let id = self.scene.lines.len() - 1;
828 let local = polylines.into_iter().flat_map(|l| l.points).collect();
829 self.local
830 .insert(GeometryRef::Lines(id), (local, curves::ATTRIBUTE));
831 id
832 }),
833 Ok(CurveGeometry::Unsupported(curve)) => {
834 self.warn(
835 path,
836 curves::ATTRIBUTE,
837 SceneWarningKind::UnsupportedCurve(curve),
838 );
839 None
840 }
841 Err(e) => {
842 self.warn(path, curves::ATTRIBUTE, SceneWarningKind::Decode(e));
843 None
844 }
845 };
846 self.lines.insert(key(&**value), id);
847 id
848 }
849
850 fn points(
851 &mut self,
852 id: &'static str,
853 value: &Arc<Value>,
854 path: &str,
855 ) -> Option<(usize, bool)> {
856 if let Some(entry) = self.points.get(&key(&**value)) {
857 return *entry;
858 }
859 let decoded = match id {
860 PCD_BASE64 => PointCloud::from_pcd_base64(value),
861 POINTS_ARRAY => PointCloud::from_points_array(value),
862 _ => PointCloud::from_points_base64(value),
863 };
864 let entry = match decoded {
865 Ok(mut cloud) => {
866 let buffer = point_buffer(&mut cloud);
867 let buffer = self.fits(buffer, path, id, |b| &b.positions);
868 let positions = cloud.positions;
869 buffer.map(|buffer| {
870 let colored = buffer.colors.is_some();
871 self.scene.points.push(buffer);
872 let index = self.scene.points.len() - 1;
873 self.local
874 .insert(GeometryRef::Points(index), (positions, id));
875 (index, colored)
876 })
877 }
878 Err(e) => {
879 self.warn(path, id, SceneWarningKind::Decode(e));
880 None
881 }
882 };
883 self.points.insert(key(&**value), entry);
884 entry
885 }
886
887 fn finish(mut self, options: &SceneOptions) -> RenderScene {
888 let mut scene = std::mem::take(&mut self.scene);
889 let out_of_range = |instance: &Instance, attribute| SceneWarning {
890 path: instance.path.clone(),
891 attribute,
892 kind: SceneWarningKind::OutOfRange,
893 };
894
895 let mut world_bounds: Option<Bounds> = None;
900 let mut instances = Vec::with_capacity(scene.instances.len());
901 for mut instance in std::mem::take(&mut scene.instances) {
902 let (positions, attribute) = &self.local[&instance.geometry];
903 let bounds =
904 Bounds::from_points(positions.iter().map(|&p| instance.world.transform_point(p)))
905 .expect("empty buffers are never instanced");
906 let linear = instance.world.rows()[..3].iter().flat_map(|row| &row[..3]);
907 if in_range(&bounds) && linear.into_iter().all(|v| v.abs() <= f64::from(f32::MAX)) {
908 world_bounds = Some(world_bounds.map_or(bounds, |b| b.union(&bounds)));
909 instance.bounds = bounds;
910 instances.push(instance);
911 } else {
912 scene.warnings.push(out_of_range(&instance, attribute));
913 }
914 }
915 scene.origin = match options.origin {
916 Origin::Auto => world_bounds.map_or([0.0; 3], |b| b.center()),
917 Origin::At(origin) => origin,
918 };
919
920 let shift = Transform::from_translation(scene.origin.map(|c| -c));
924 let mut world_bounds: Option<Bounds> = None;
925 for mut instance in instances {
926 let render = Transform::from_translation(scene.anchor(instance.geometry))
927 .then(&instance.world)
928 .then(&shift);
929 instance.matrix = render.to_column_major().map(|v| v as f32);
930 if instance.matrix.iter().all(|v| v.is_finite()) {
931 let bounds = instance.bounds;
932 world_bounds = Some(world_bounds.map_or(bounds, |b| b.union(&bounds)));
933 scene.instances.push(instance);
934 } else {
935 let attribute = self.local[&instance.geometry].1;
936 scene.warnings.push(out_of_range(&instance, attribute));
937 }
938 }
939 scene.bounds = world_bounds;
940 scene
941 }
942}
943
944const POINT_COLORS: BasicMaterial = BasicMaterial {
946 color: [1.0; 3],
947 opacity: 1.0,
948};
949
950const POINT_DEFAULT: BasicMaterial = BasicMaterial {
952 color: [0.0; 3],
953 opacity: 1.0,
954};
955
956fn anchored(points: impl Iterator<Item = Vec3> + Clone) -> Option<(Vec3, Vec<[f32; 3]>)> {
958 let anchor = Bounds::from_points(points.clone())?.center();
959 let relative = points
960 .map(|p| [0, 1, 2].map(|k| (p[k] - anchor[k]) as f32))
961 .collect();
962 Some((anchor, relative))
963}
964
965fn mesh_buffer(mesh: &TriangleMesh) -> Option<MeshBuffer> {
966 if mesh.indices.is_empty() {
967 return None;
968 }
969 let (anchor, positions) = anchored(mesh.positions.iter().copied())?;
970 let mut sums = vec![[0.0f64; 3]; mesh.positions.len()];
973 for t in mesh.triangles() {
974 let [a, b, c] = t.map(|i| mesh.positions[i as usize]);
975 let n = cross(sub(b, a), sub(c, a));
976 for i in t {
977 let sum = &mut sums[i as usize];
978 for k in 0..3 {
979 sum[k] += n[k];
980 }
981 }
982 }
983 let normals = sums
984 .into_iter()
985 .map(|n| {
986 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
987 if len > 0.0 && len.is_finite() {
988 n.map(|c| (c / len) as f32)
989 } else {
990 [0.0, 0.0, 1.0]
991 }
992 })
993 .collect();
994 Some(MeshBuffer {
995 anchor,
996 positions,
997 normals,
998 indices: mesh.indices.clone(),
999 })
1000}
1001
1002fn line_buffer(polylines: &[curves::Polyline]) -> Option<LineBuffer> {
1003 let (anchor, positions) = anchored(polylines.iter().flat_map(|l| l.points.iter().copied()))?;
1004 let mut indices = Vec::new();
1005 let mut start = 0u32;
1006 for line in polylines {
1007 let n = line.points.len() as u32;
1008 for i in 0..n - 1 {
1009 indices.extend([start + i, start + i + 1]);
1010 }
1011 if line.closed && n > 2 {
1012 indices.extend([start + n - 1, start]);
1013 }
1014 start += n;
1015 }
1016 Some(LineBuffer {
1017 anchor,
1018 positions,
1019 indices,
1020 })
1021}
1022
1023fn point_buffer(cloud: &mut PointCloud) -> Option<PointBuffer> {
1024 let (anchor, positions) = anchored(cloud.positions.iter().copied())?;
1025 Some(PointBuffer {
1026 anchor,
1027 positions,
1028 colors: cloud.colors.take(),
1029 })
1030}
1031
1032#[cfg(test)]
1033mod tests {
1034 use super::*;
1035
1036 #[test]
1037 fn vertex_normals_are_area_weighted_and_unit() {
1038 let mesh = TriangleMesh::new(
1041 vec![
1042 [0.0, 0.0, 0.0],
1043 [1.0, 0.0, 0.0],
1044 [0.0, 3.0, 0.0],
1045 [9.0, 9.0, 9.0],
1046 [0.0, 0.0, -1.0],
1047 ],
1048 vec![0, 1, 2, 0, 4, 1],
1049 )
1050 .unwrap();
1051 let buffer = mesh_buffer(&mesh).unwrap();
1052 let shared = [0.0, -1.0 / 10f64.sqrt(), 3.0 / 10f64.sqrt()].map(|c| c as f32);
1054 assert_eq!(buffer.normals[0], shared);
1055 assert_eq!(buffer.normals[1], shared);
1056 assert_eq!(buffer.normals[2], [0.0, 0.0, 1.0]);
1057 assert_eq!(buffer.normals[3], [0.0, 0.0, 1.0], "unused vertex");
1058 assert_eq!(buffer.normals[4], [0.0, -1.0, 0.0]);
1059 assert_eq!(buffer.anchor, [4.5, 4.5, 4.0]);
1060 }
1061
1062 #[test]
1063 fn empty_geometry_makes_no_buffer() {
1064 let mesh = TriangleMesh::new(vec![[0.0; 3]], vec![]).unwrap();
1065 assert!(mesh_buffer(&mesh).is_none());
1066 assert!(line_buffer(&[]).is_none());
1067 }
1068}