openbim_ifcx_geometry/
lib.rs

1//! Renderer-neutral geometry and viewer helpers for IFC5 / IFCX.
2//!
3//! The crate decodes the geometry and presentation attributes of
4//! `ifcx_alpha` nodes into typed values. It never depends on a renderer or
5//! GPU API (see `docs/adr/0002`).
6//!
7//! | Module | Attribute | Result |
8//! | --- | --- | --- |
9//! | [`transform`] | `usd::xformop` | [`Transform`], composed with [`world_from_parent`] |
10//! | [`mesh`] | `usd::usdgeom::mesh` | [`TriangleMesh`] |
11//! | [`curves`] | `usd::usdgeom::basiscurves` | [`CurveGeometry`] |
12//! | [`points`] | `points::array`, `points::base64`, `pcd::base64` | [`PointCloud`] |
13//! | [`presentation`] | `usd::usdgeom::visibility`, `bsi::ifc::presentation::*`, `gltf::material` | [`NodePresentation`], resolved over ancestors by [`is_visible`], [`resolve_basic_material`], and [`resolve_mesh_material`] |
14//! | [`scene`] | all of the above, over a composed tree | [`RenderScene`]: instances with node path, world matrix, shared `f32` buffers, materials, bounds |
15//! | [`glb`] | a [`RenderScene`] | binary glTF 2.0 bytes, [`to_glb`] |
16//!
17//! Each decoder takes the attribute's `serde_json::Value` as stored in
18//! [`openbim_ifcx::IfcxNode::attributes`] and returns a [`DecodeError`] for
19//! malformed values instead of panicking. [`PointCloud::from_attributes`] and
20//! [`NodePresentation::from_attributes`] pick their attributes from anything
21//! implementing [`Attributes`]. Coordinates stay `f64`; see [`math`].
22//!
23//! [`RenderScene::from_composition`] walks a composed node tree once and
24//! returns the flat scene a viewer draws, with each shared geometry stored
25//! once and instanced. [`to_glb`] writes that scene as a binary glTF 2.0 file;
26//! the `ifcx2glb` example converts IFCX files from the command line.
27//!
28//! ```
29//! use openbim_ifcx::IfcxFile;
30//! use openbim_ifcx_geometry::{mesh, transform, Transform, TriangleMesh};
31//!
32//! let file = IfcxFile::from_json_str(r#"{
33//!     "header": {"id": "demo", "ifcxVersion": "ifcx_alpha", "dataVersion": "1.0.0",
34//!                "author": "someone", "timestamp": "2026-10-03"},
35//!     "imports": [], "schemas": {},
36//!     "data": [{"path": "slab", "attributes": {
37//!         "usd::xformop": {"transform": [[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,3,1]]},
38//!         "usd::usdgeom::mesh": {"points": [[0,0,0],[1,0,0],[0,1,0]],
39//!                                "faceVertexIndices": [0,1,2]}
40//!     }}]
41//! }"#)?;
42//! let attributes = file.data[0].attributes.as_ref().unwrap();
43//! let local = Transform::from_attribute(&attributes[transform::ATTRIBUTE])?;
44//! let mesh = TriangleMesh::from_attribute(&attributes[mesh::ATTRIBUTE])?;
45//! let world: Vec<_> = mesh.positions.iter().map(|&p| local.transform_point(p)).collect();
46//! assert_eq!(world[1], [1.0, 0.0, 3.0]);
47//! # Ok::<(), Box<dyn std::error::Error>>(())
48//! ```
49//!
50//! Point clouds and presentation:
51//!
52//! ```
53//! use openbim_ifcx_geometry::{is_visible, resolve_basic_material, NodePresentation, PointCloud};
54//! use serde_json::json;
55//!
56//! let node = json!({
57//!     "points::array": {"positions": [[0, 0, 0], [1, 0, 0]]},
58//!     "bsi::ifc::presentation::diffuseColor": [1, 0, 0],
59//! });
60//! let attributes = node.as_object().unwrap();
61//!
62//! let cloud = PointCloud::from_attributes(attributes)?.unwrap();
63//! assert_eq!(cloud.positions, [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]]);
64//!
65//! let own = NodePresentation::from_attributes(attributes)?;
66//! assert!(is_visible(true, own.visibility));
67//! assert_eq!(resolve_basic_material([&own]).color, [1.0, 0.0, 0.0]);
68//! # Ok::<(), Box<dyn std::error::Error>>(())
69//! ```
70//!
71//! A render scene from a composed file:
72//!
73//! ```
74//! use openbim_ifcx::{compose, flatten_owned, IfcxFile};
75//! use openbim_ifcx_geometry::{GeometryKind, RenderScene, SceneOptions};
76//!
77//! let file = IfcxFile::from_json_str(r#"{
78//!     "header": {"id": "demo", "ifcxVersion": "ifcx_alpha", "dataVersion": "1.0.0",
79//!                "author": "someone", "timestamp": "2026-10-03"},
80//!     "imports": [], "schemas": {},
81//!     "data": [
82//!         {"path": "storey", "children": {"Slab": "slab"}, "attributes": {
83//!             "usd::xformop": {"transform": [[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,3,1]]},
84//!             "bsi::ifc::presentation::diffuseColor": [0.8, 0.2, 0.2]}},
85//!         {"path": "slab", "attributes": {"usd::usdgeom::mesh": {
86//!             "points": [[0,0,0],[1,0,0],[0,1,0]], "faceVertexIndices": [0,1,2]}}}
87//!     ]
88//! }"#)?;
89//! let composition = compose(&flatten_owned(file.data))?;
90//! let scene = RenderScene::from_composition(&composition, &SceneOptions::default());
91//! let slab = &scene.instances[0];
92//! assert_eq!(slab.path, "storey/Slab");
93//! assert_eq!(slab.geometry.kind(), GeometryKind::Mesh);
94//! assert_eq!(slab.world.translation(), [0.0, 0.0, 3.0]);
95//! assert!(scene.warnings.is_empty());
96//! # Ok::<(), Box<dyn std::error::Error>>(())
97//! ```
98
99#![forbid(unsafe_code)]
100
101mod attributes;
102pub mod curves;
103pub mod error;
104pub mod glb;
105mod json;
106pub mod math;
107pub mod mesh;
108pub mod points;
109pub mod presentation;
110pub mod scene;
111pub mod transform;
112
113pub use attributes::Attributes;
114pub use curves::{CurveGeometry, Polyline, UnsupportedCurve};
115pub use error::DecodeError;
116pub use glb::{to_glb, GlbError, GlbOptions};
117pub use math::Vec3;
118pub use mesh::TriangleMesh;
119pub use points::PointCloud;
120pub use presentation::{
121    is_visible, resolve_basic_material, resolve_mesh_material, AlphaMode, BasicMaterial,
122    GltfMaterial, Material, NodePresentation, NormalTexture, OcclusionTexture, Visibility,
123};
124pub use scene::{
125    Bounds, GeometryKind, GeometryRef, Instance, LineBuffer, MeshBuffer, Origin, PointBuffer,
126    RenderScene, SceneOptions, SceneWarning, SceneWarningKind,
127};
128pub use transform::{world_from_parent, Transform};