openbim_ifcx_geometry/
scene.rs

1//! A flat, renderer-neutral render scene over a composed node tree.
2//!
3//! [`RenderScene::from_composition`] walks the composed tree of
4//! [`openbim_ifcx::compose()`] once and returns everything a viewer draws:
5//!
6//! - [`Instance`]s: one per drawn geometry of a node, with the node's path
7//!   (for picking), its world [`Transform`], a [`GeometryRef`] into the shared
8//!   buffers, a material index, and world bounds;
9//! - shared geometry buffers: [`MeshBuffer`] (triangles with vertex normals),
10//!   [`LineBuffer`] (line segments), [`PointBuffer`] (points with optional
11//!   colours), all `f32`;
12//! - the [`Material`] table;
13//! - the scene's `f64` world [`Bounds`] and the render [`origin`](RenderScene::origin);
14//! - [`SceneWarning`]s for attribute values that could not be decoded.
15//!
16//! # Walk
17//!
18//! Starting at the artificial root, each node is visited once per path that
19//! reaches it, parents before children, children in composed order. For each
20//! node:
21//!
22//! 1. An `invisible` node is left out with its whole subtree
23//!    ([`is_visible`](crate::is_visible)).
24//! 2. Its world transform is its `usd::xformop` applied in its parent's world
25//!    transform ([`world_from_parent`]); a node without one inherits it.
26//! 3. A mesh is drawn with the nearest `gltf::material`, else the nearest
27//!    `diffuseColor` with its own opacity, else grey 0.6 opaque; curves use
28//!    the latter two only ([`resolve_mesh_material`](crate::resolve_mesh_material),
29//!    [`resolve_basic_material`](crate::resolve_basic_material)).
30//! 4. Each of its mesh, curves, and point cloud becomes an instance.
31//!
32//! Instance paths name the node by child names from the root, as
33//! `site/Storey/Wall`, which [`Composition::get`] resolves. [`Instance::node`]
34//! is the path of the node that defined it, [`ComposedNode::path`].
35//!
36//! # Instancing
37//!
38//! Composition shares sub-trees and attribute values through [`Arc`]: every
39//! node that inherits a type, or names the same child, holds the same
40//! attribute value. The scene decodes each distinct value once, keyed by its
41//! `Arc` pointer, and stores one buffer that all instances reference. Equal
42//! values written separately in a file are not merged.
43//!
44//! # Precision and origin
45//!
46//! Coordinates stay `f64` until the end. Each buffer stores its vertices
47//! relative to an [`anchor`](MeshBuffer::anchor), the centre of the
48//! geometry's own bounding box, so `f32` keeps sub-millimetre precision even
49//! for georeferenced vertices (upstream samples reach 4.9 × 10⁶ m in mesh
50//! points). The scene's [`origin`](RenderScene::origin) is subtracted from
51//! world coordinates; by default it is the centre of the world bounds. Each
52//! instance's render [`matrix`](Instance::matrix) maps buffer coordinates to
53//! world coordinates minus the origin: anchor, then world transform, then
54//! origin shift, multiplied in `f64` and rounded to `f32` last. Add the
55//! origin back, for example as a root translation, to place the scene in the
56//! file's coordinates.
57//!
58//! # Differences from the reference viewer
59//!
60//! The reference viewer is `src/viewer/render.ts` of
61//! buildingSMART/IFC5-development at `1a63082`.
62//!
63//! - A node with a mesh, curves, and a point cloud yields all three; the
64//!   viewer draws only the first of mesh, curves, point cloud. No upstream
65//!   example has two kinds on one node.
66//! - The viewer applies no transform to the root it is given, which is always
67//!   the artificial root without attributes. [`RenderScene::from_root`]
68//!   applies the given node's transform like any other.
69//! - The viewer draws lines in 0.8 times the resolved colour. The scene keeps
70//!   the resolved colour; the darkening is a styling choice for renderers.
71//! - The viewer draws `pcd::base64` points without colours in white, through
72//!   three.js `PCDLoader`'s default, and other points without colours in
73//!   black. The scene uses black for all of them.
74//! - The viewer computes vertex normals with three.js
75//!   `computeVertexNormals`; so does the scene, area-weighted over the
76//!   triangles that share a vertex. A vertex in no triangle with an area
77//!   gets `+Z`, so that every normal is a unit vector.
78//! - See [`presentation`](crate::presentation) for opacity `0`,
79//!   `visibility: "inherited"`, and all-`false` glTF materials.
80//!
81//! # Mirroring
82//!
83//! A world transform with a negative determinant mirrors its geometry and
84//! reverses the winding of its triangles. Buffers are shared between
85//! instances and keep the file's winding; [`Instance::mirrored`] reports
86//! these instances, and renderers flip their front face (glTF viewers and
87//! three.js do so from the node matrix).
88//!
89//! # Warnings
90//!
91//! Building a scene does not fail. A value that cannot be decoded becomes a
92//! [`SceneWarning`] with the node path and attribute id, and only that value
93//! is skipped: a malformed mesh leaves out the mesh, a malformed colour falls
94//! back to the inherited one. A malformed `usd::xformop` leaves out the node
95//! with its subtree, since nothing below it can be placed. Linear curves of
96//! an unsupported `type` are skipped with a warning as well. Each distinct
97//! value is reported once, at the first path that reaches it. Empty
98//! geometry is skipped without a warning.
99//!
100//! Buffers and render matrices are `f32`, so coordinates they cannot hold
101//! are left out with a [`SceneWarningKind::OutOfRange`] warning rather than
102//! written as infinities: a geometry whose vertices span more than
103//! `f32::MAX`, and an instance whose world bounds reach beyond a quarter of
104//! `f32::MAX` (about 8.5 × 10³⁷) or whose render matrix overflows `f32`.
105//! Every value in a scene is therefore finite.
106//!
107//! # Limits
108//!
109//! Composition shares sub-trees, so a small file can describe a tree whose
110//! paths are exponential in number: 30 nodes that each name the next twice
111//! as children reach the last one along 2³⁰ paths. The walk visits a node
112//! once per path, so [`SceneOptions`] bounds it: at most
113//! [`max_visits`](SceneOptions::max_visits) node visits (10 million by
114//! default) and at most [`max_path_bytes`](SceneOptions::max_path_bytes)
115//! bytes of instance paths built (1 GiB by default). When either is reached
116//! the walk stops, the scene keeps what was walked before, and one
117//! [`SceneWarningKind::LimitReached`] warning names the path where it
118//! stopped.
119
120use 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/// Where the render origin of a scene lies.
136#[derive(Debug, Clone, Copy, PartialEq, Default)]
137pub enum Origin {
138    /// The centre of the scene's world bounds, or zero for an empty scene.
139    #[default]
140    Auto,
141    /// A fixed point in world coordinates; `At([0.0; 3])` disables the
142    /// shift.
143    At(Vec3),
144}
145
146/// Options for [`RenderScene::from_composition`].
147#[derive(Debug, Clone, PartialEq)]
148#[non_exhaustive]
149pub struct SceneOptions {
150    /// The render origin; [`Origin::Auto`] by default.
151    pub origin: Origin,
152    /// Most node visits of the walk, counting a node once per path that
153    /// reaches it; [`DEFAULT_MAX_VISITS`](Self::DEFAULT_MAX_VISITS) by
154    /// default. See [Limits](self#limits).
155    pub max_visits: usize,
156    /// Most bytes of node paths the walk builds, summed over every visit;
157    /// [`DEFAULT_MAX_PATH_BYTES`](Self::DEFAULT_MAX_PATH_BYTES) by default.
158    /// Bounds the memory of instance paths in deep trees with long names.
159    /// See [Limits](self#limits).
160    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    /// Default of [`max_visits`](Self::max_visits): 10 million.
175    pub const DEFAULT_MAX_VISITS: usize = 10_000_000;
176    /// Default of [`max_path_bytes`](Self::max_path_bytes): 1 GiB.
177    pub const DEFAULT_MAX_PATH_BYTES: usize = 1 << 30;
178
179    /// Sets the render origin.
180    pub fn with_origin(mut self, origin: Origin) -> Self {
181        self.origin = origin;
182        self
183    }
184
185    /// Sets [`max_visits`](Self::max_visits).
186    pub fn with_max_visits(mut self, max_visits: usize) -> Self {
187        self.max_visits = max_visits;
188        self
189    }
190
191    /// Sets [`max_path_bytes`](Self::max_path_bytes).
192    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/// An axis-aligned box in `f64`.
199#[derive(Debug, Clone, Copy, PartialEq)]
200pub struct Bounds {
201    pub min: Vec3,
202    pub max: Vec3,
203}
204
205impl Bounds {
206    /// The smallest box containing every point, or `None` for none.
207    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    /// Grows the box to contain `p`.
221    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    /// The smallest box containing both.
229    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    /// The centre.
237    pub fn center(&self) -> Vec3 {
238        [0, 1, 2].map(|i| (self.min[i] + self.max[i]) / 2.0)
239    }
240
241    /// The edge lengths.
242    pub fn size(&self) -> Vec3 {
243        [0, 1, 2].map(|i| self.max[i] - self.min[i])
244    }
245}
246
247/// A shared triangle mesh, from `usd::usdgeom::mesh`.
248#[derive(Debug, Clone, PartialEq)]
249pub struct MeshBuffer {
250    /// Local coordinates of the buffer's zero: the centre of the mesh's
251    /// local bounding box. A vertex's local position is `anchor + position`.
252    pub anchor: Vec3,
253    /// Vertex positions relative to `anchor`.
254    pub positions: Vec<[f32; 3]>,
255    /// One unit normal per vertex, area-weighted over its triangles.
256    pub normals: Vec<[f32; 3]>,
257    /// Three indices per triangle, counter-clockwise front faces.
258    pub indices: Vec<u32>,
259}
260
261impl MeshBuffer {
262    /// Number of triangles.
263    pub fn triangle_count(&self) -> usize {
264        self.indices.len() / 3
265    }
266}
267
268/// Shared line segments, from the linear curves of
269/// `usd::usdgeom::basiscurves`.
270#[derive(Debug, Clone, PartialEq)]
271pub struct LineBuffer {
272    /// See [`MeshBuffer::anchor`].
273    pub anchor: Vec3,
274    /// Vertex positions relative to `anchor`, every polyline in order.
275    pub positions: Vec<[f32; 3]>,
276    /// Two indices per segment. A closed polyline adds a segment from its
277    /// last vertex back to its first.
278    pub indices: Vec<u32>,
279}
280
281impl LineBuffer {
282    /// Number of line segments.
283    pub fn segment_count(&self) -> usize {
284        self.indices.len() / 2
285    }
286}
287
288/// Shared points, from `pcd::base64`, `points::array`, or `points::base64`.
289#[derive(Debug, Clone, PartialEq)]
290pub struct PointBuffer {
291    /// See [`MeshBuffer::anchor`].
292    pub anchor: Vec3,
293    /// Point positions relative to `anchor`.
294    pub positions: Vec<[f32; 3]>,
295    /// One linear RGB colour per point, if the source has colours.
296    pub colors: Option<Vec<[f32; 3]>>,
297}
298
299/// What an instance draws: an index into one of the scene's buffer lists.
300#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
301pub enum GeometryRef {
302    /// Index into [`RenderScene::meshes`].
303    Mesh(usize),
304    /// Index into [`RenderScene::lines`].
305    Lines(usize),
306    /// Index into [`RenderScene::points`].
307    Points(usize),
308}
309
310/// The kind of geometry an instance draws.
311#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
312pub enum GeometryKind {
313    Mesh,
314    Lines,
315    Points,
316}
317
318impl GeometryRef {
319    /// The kind of buffer referenced.
320    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/// One drawn geometry of one node at one path.
330#[derive(Debug, Clone, PartialEq)]
331pub struct Instance {
332    /// Child names from the root, joined by `/`, for example
333    /// `site/Storey/Wall`. Resolve it with [`Composition::get`] to show the
334    /// node's attributes on pick. Unique per node, shared by the instances
335    /// of one node with several kinds of geometry.
336    pub path: String,
337    /// The path of the node that defined this node, [`ComposedNode::path`].
338    pub node: String,
339    /// The shared geometry drawn.
340    pub geometry: GeometryRef,
341    /// Index into [`RenderScene::materials`].
342    pub material: usize,
343    /// The node's world transform in the file's coordinates.
344    pub world: Transform,
345    /// Column-major `f32` matrix from buffer coordinates to world
346    /// coordinates minus the scene origin, the layout of glTF `node.matrix`.
347    pub matrix: [f32; 16],
348    /// World bounds of the drawn vertices, in the file's coordinates.
349    pub bounds: Bounds,
350}
351
352impl Instance {
353    /// Whether the world transform mirrors, so triangle winding is reversed.
354    pub fn mirrored(&self) -> bool {
355        self.world.determinant() < 0.0
356    }
357}
358
359/// Why part of a node was left out of a scene.
360#[derive(Debug, Clone, PartialEq)]
361#[non_exhaustive]
362pub enum SceneWarningKind {
363    /// The attribute value could not be decoded.
364    Decode(DecodeError),
365    /// Curves of a type that is not drawn yet.
366    UnsupportedCurve(UnsupportedCurve),
367    /// The walk reached a limit of [`SceneOptions`] at this path and
368    /// stopped; nodes not visited by then are left out. `option` names the
369    /// field, `max_visits` or `max_path_bytes`, and `limit` its value. The
370    /// warning's `attribute` is empty.
371    LimitReached { option: &'static str, limit: usize },
372    /// Coordinates that the scene's `f32` buffers and render matrices
373    /// cannot hold: a geometry spanning more than `f32::MAX`, or an
374    /// instance placed beyond about 8.5 × 10³⁷ or scaled past `f32::MAX`.
375    /// The geometry or the instance is left out.
376    OutOfRange,
377}
378
379/// A value skipped while building a scene; see the [module docs](self).
380#[derive(Debug, Clone, PartialEq)]
381pub struct SceneWarning {
382    /// The first instance path at which the value was found.
383    pub path: String,
384    /// The attribute id, for example `usd::usdgeom::mesh`.
385    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                // Some decoders already name the attribute; do not repeat it.
399                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/// A flat render scene; see the [module docs](self).
431#[derive(Debug, Clone, PartialEq, Default)]
432pub struct RenderScene {
433    /// Drawn geometry, parents before children.
434    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    /// World bounds of every instance, `None` for an empty scene.
440    pub bounds: Option<Bounds>,
441    /// World coordinates of the render origin; instance matrices map to
442    /// world coordinates minus this point.
443    pub origin: Vec3,
444    pub warnings: Vec<SceneWarning>,
445}
446
447impl RenderScene {
448    /// Builds the scene of a composition under its artificial root
449    /// ([`Composition::root`]).
450    ///
451    /// ```
452    /// use openbim_ifcx::{compose, flatten, IfcxNode};
453    /// use openbim_ifcx_geometry::scene::{GeometryRef, RenderScene, SceneOptions};
454    ///
455    /// let nodes: Vec<IfcxNode> = serde_json::from_str(r#"[
456    ///     {"path": "panel", "attributes": {"usd::usdgeom::mesh": {
457    ///         "points": [[0, 0, 0], [1, 0, 0], [0, 1, 0]], "faceVertexIndices": [0, 1, 2]}}},
458    ///     {"path": "a", "inherits": {"type": "panel"}},
459    ///     {"path": "b", "inherits": {"type": "panel"}, "attributes": {"usd::xformop":
460    ///         {"transform": [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [5, 0, 0, 1]]}}}
461    /// ]"#)?;
462    /// let composition = compose(&flatten(&nodes))?;
463    /// let scene = RenderScene::from_composition(&composition, &SceneOptions::default());
464    /// assert_eq!(scene.meshes.len(), 1, "stored once");
465    /// let paths: Vec<_> = scene.instances.iter().map(|i| i.path.as_str()).collect();
466    /// assert_eq!(paths, ["a", "b"]);
467    /// assert_eq!(scene.instances[1].geometry, GeometryRef::Mesh(0));
468    /// let bounds = scene.bounds.unwrap();
469    /// assert_eq!((bounds.min, bounds.max), ([0.0; 3], [6.0, 1.0, 0.0]));
470    /// assert_eq!(scene.origin, [3.0, 0.5, 0.0]);
471    /// # Ok::<(), Box<dyn std::error::Error>>(())
472    /// ```
473    pub fn from_composition(composition: &Composition, options: &SceneOptions) -> Self {
474        Self::from_root(&composition.root(), options)
475    }
476
477    /// Builds the scene of `root` and everything below it. Instance paths
478    /// are relative to `root`, which itself has path `""`; its own
479    /// attributes, including a transform, apply like those of any node.
480    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    /// Number of triangles drawn, counting every instance.
487    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    /// Number of line segments drawn, counting every instance.
498    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    /// Number of points drawn, counting every instance.
509    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    /// The instances drawn for the node at `path`, at most one per
520    /// geometry kind. They are adjacent because instances are listed in
521    /// walk order. Linear in the number of instances.
522    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/// A node's own decoded attributes, cached per composed node.
543#[derive(Clone, Copy)]
544struct NodeInfo {
545    /// `None`: leave the node and its subtree out.
546    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/// What a node passes on to its children.
556#[derive(Clone, Copy)]
557struct Context {
558    world: Transform,
559    pbr: Option<usize>,
560    basic: Option<usize>,
561}
562
563/// Raw pointers serve only as identities here; they are never dereferenced.
564type 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 `f64` vertices of each buffer, for exact world bounds, and the
575    /// attribute id it was decoded from.
576    local: HashMap<GeometryRef, (Vec<Vec3>, &'static str)>,
577}
578
579/// Largest world coordinate magnitude an instance may reach: a quarter of
580/// `f32::MAX`, so that the origin, coordinates relative to it, and their
581/// differences all stay finite in `f32`.
582const 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        // Counted when a node is queued, so the queue is bounded as well.
603        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                // Checked before the path is built, so a limit also bounds
659                // the largest single allocation.
660                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    /// `buffer`, unless its anchored `f32` positions overflow, which only
712    /// coordinates spanning more than `f32::MAX` do; that is a warning.
713    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        // World bounds first: an instance placed beyond MAX_COORDINATE,
896        // with a transform that overflows f64, or with a rotation and scale
897        // part that overflows f32 (the render matrix keeps that part as it
898        // is) is left out before it can move the origin.
899        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        // Then the f32 render matrix. Its translation can still overflow in
921        // contrived cases; those instances are left out as well, after the
922        // origin was chosen.
923        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
944/// Points with colours: white, so the per-point colours show unchanged.
945const POINT_COLORS: BasicMaterial = BasicMaterial {
946    color: [1.0; 3],
947    opacity: 1.0,
948};
949
950/// Points without colours: black, as in the reference viewer.
951const POINT_DEFAULT: BasicMaterial = BasicMaterial {
952    color: [0.0; 3],
953    opacity: 1.0,
954};
955
956/// The centre of the bounding box, and `points` relative to it in `f32`.
957fn 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    // three.js computeVertexNormals: sum the unnormalised face normals,
971    // which weights each by twice the triangle's area.
972    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        // Two triangles share the edge 0-1: a large one in the XY plane and
1039        // a small one in the XZ plane. Vertex 3 is in no triangle.
1040        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        // Face normals (unnormalised): (0, 0, 3) and (0, -1, 0).
1053        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}