ifc_lite_export/
lib.rs

1// SPDX-License-Identifier: MPL-2.0
2//! Domain-format exporters for ifc-lite.
3//!
4//! Phase 1: **HBJSON** (Honeybee energy-model) room export — the analytic, watertight
5//! IFC→Ladybug bridge. Apertures/doors/shades and a glTF migration follow.
6//!
7//! This is the Rust source of truth; CLI / SDK / wasm become thin callers (mirroring how
8//! geometry already flows through `ifc-lite-wasm`).
9
10#[cfg(test)]
11#[macro_use]
12mod test_support;
13
14mod adjacency;
15mod collada;
16mod collada_document;
17mod collada_fmt;
18mod color_space;
19mod constructions;
20mod csv;
21/// The single CSV cell escaper for this crate — RFC 4180 quoting plus the
22/// CWE-1236 formula-injection guard, pinned to the TypeScript half by
23/// `tests/csv_cell_parity.rs`.
24pub mod csv_cell;
25mod dfjson;
26mod error;
27mod file_schema;
28mod frame;
29mod generated;
30mod geom;
31mod gltf;
32mod hbjson;
33mod ifc5;
34mod json;
35mod jsonld;
36mod kmz;
37mod landxml_terrain;
38mod merged;
39mod mesh_input;
40mod model;
41mod quantity_analysis;
42mod quantity_analysis_derived;
43mod obj;
44mod relationships;
45mod rebar_attributes;
46mod rebar_schedule;
47mod openings;
48#[cfg(feature = "parquet-bos")]
49mod parquet_bos;
50mod rooms;
51pub mod rooted_type;
52mod schema_convert;
53mod schema_detect;
54mod schema_ifc2x3_slots;
55mod schema_enum;
56mod schema_ifc4_slots;
57mod schema_owner_history;
58mod schema_pad;
59mod schema_unrepresented;
60pub use schema_pad::padded_type_universe;
61mod shades;
62pub mod source_header;
63mod step;
64mod step_api;
65mod step_cow;
66mod step_header;
67mod step_json;
68mod step_log;
69mod step_slot;
70mod step_text;
71mod step_map_transform;
72pub use step_map_transform::{plan_map_conversion_normalization, MapConversionEntityPatch, MapConversionNormalizationPlan};
73mod usd;
74mod xml_text;
75
76/// The STEP string-literal escaper; `escape`'s docs say why it is public.
77pub use step_text::escape as escape_step_string;
78
79pub use collada::try_export_collada_from_meshes;
80pub use csv::{export_csv, CsvMode, CsvOptions};
81pub use dfjson::DfjsonStats;
82pub use error::ExportError;
83pub use gltf::{
84    export_glb_from_meshes, export_gltf_streaming, export_gltf_streaming_with_index,
85    project_glb_size, project_glb_size_with_index, try_export_glb, try_export_glb_from_meshes,
86    try_export_glb_streaming_bounded, try_export_glb_streaming_bounded_with_index,
87    try_export_glb_with_stats, try_export_glb_with_stats_with_index, GlbSizeProjection,
88    GltfBuffer, GltfOptions, GltfStats,
89};
90pub use hbjson::Model;
91// Re-exported so a caller can `build_entity_index` once and share it across the
92// geometry (`try_export_glb_with_stats_with_index`) and attribute
93// (`stream_export_model_with_index`) passes.
94//
95// `entity_count` is the cheap `O(scan)`, `O(1)`-memory entity tally (issue
96// #1517): a downstream DoS guard can reject a file with a pathological entity
97// count WITHOUT forcing the full index (`build_entity_index(..).len()` would
98// allocate ~20 B/entity — undoing the bounded-memory work).
99pub use ifc_lite_core::{build_entity_index, entity_count, EntityIndex};
100// The attribute model, re-exported so a consumer of the row callback can name
101// the types it is handed, and `IfcType` so it can tell what it was handed.
102pub use ifc_lite_core::{AttributeValue, DecodedEntity, IfcType};
103// Re-exported alongside the model so a consumer of `EntityRow` attribute values
104// can interpret them: those values are in the file's own units, unlike the
105// geometry exporters' output, which is normalised to metres.
106pub use ifc_lite_processing::prepass::UnitScales;
107// Named so a caller can pick a `GltfOptions::tessellation_quality`.
108pub use ifc_lite_processing::TessellationQuality;
109pub use ifc5::{export_ifc5, Ifc5Options};
110// Spatial and type relationships, which `EntityRow` cannot carry because IFC
111// models them as separate entities that are not products.
112pub use relationships::{relationships, Relationships};
113mod rebar_preflight;
114mod rebar_fabrication;
115pub use rebar_fabrication::{RebarFabricationCheck, RebarFabricationCheckStatus,
116    RebarFabricationPolicy, RebarFabricationPolicyError, RebarFabricationReport,
117    RebarScheduleFabricationError};
118pub use rebar_preflight::{RebarPreflightComparison, RebarPreflightError, RebarPreflightLimits,
119    RebarPreflightReport, RebarSchedulePreflightError};
120pub use rebar_schedule::{build_rebar_schedule, build_rebar_schedule_with_preflight,
121    build_rebar_schedule_with_fabrication_precheck, AuthoredRebarAttribute, AuthoredRebarValue,
122    RebarSchedule, RebarScheduleRow, RebarSource, RebarSweep};
123pub use json::{export_json, JsonOptions};
124pub use landxml_terrain::{
125    try_export_landxml_terrain, LandXmlLinearUnit, LandXmlTerrainExport, LandXmlTerrainOptions,
126    LandXmlTerrainRefusal, LandXmlTerrainSurface,
127};
128pub use jsonld::{export_jsonld, export_jsonld_with_filter, JsonLdOptions};
129pub use kmz::{
130    ifc_angle_to_kml_heading, try_export_kmz_collada_from_meshes, AltitudeMode, KmzOptions,
131};
132pub use merged::{
133    deterministic_global_id, export_merged, export_merged_models, export_merged_with_stats,
134    leading_rooted_global_id, ContainerMergeStrategy, MergedModel, MergedOptions, MergedStats,
135    StoreyMergeStrategy, UnitReconciliation,
136};
137pub use model::{
138    build_export_model, build_export_model_with_options, stream_export_model,
139    stream_export_model_with_index, stream_export_model_with_options, EntityRow, ExportModel,
140    ModelOptions, Placement, PropValue, PropertySet, QuantitySet, QuantityValue,
141};
142pub use quantity_analysis_derived::{analyze_quantities, DerivedQuantity,
143    ProductQuantityAnalysis, QuantityAnalysis, QuantitySourceOccurrence};
144pub use quantity_analysis::{analyze_authored_quantities, AuthoredQuantity,
145    AuthoredQuantityAnalysis, ProductQuantities, QuantityConflict, QuantityUnit};
146pub use obj::{export_obj, export_obj_with_stats, ObjOptions, ObjStats};
147#[cfg(feature = "parquet-bos")]
148pub use parquet_bos::{export_bos, ParquetBosOptions};
149pub use step::{
150    export_step, export_step_to_writer, export_step_with_stats, AttrMutation, CopyOnWriteMutation,
151    PropMutation, StepOptions, StepStats,
152};
153pub use step_json::export_step_json;
154pub use step_log::{
155    export_merged_models_with_logs, export_step_with_log, export_step_with_log_to_writer, GeorefMutations, LogExportStats, LogMutation,
156    LogNewEntity, MutationKind, MutationLog, StepCounters,
157};
158pub use usd::{export_usd, UsdOptions};
159
160use ifc_lite_geometry::extract_profiles;
161
162/// Honeybee identifiers may not contain spaces or most special characters; map anything
163/// other than alphanumerics / `_` / `-` to `_`.
164fn sanitize_identifier(s: &str) -> String {
165    let out: String = s
166        .chars()
167        .map(|c| if c.is_alphanumeric() || c == '_' || c == '-' { c } else { '_' })
168        .collect();
169    if out.is_empty() { "model".to_string() } else { out }
170}
171
172/// Options for HBJSON export.
173pub struct HbjsonOptions {
174    /// Model identifier / display name.
175    pub name: String,
176    /// Geometry tolerance in metres (Honeybee default 0.01).
177    pub tolerance: f64,
178}
179
180impl Default for HbjsonOptions {
181    fn default() -> Self {
182        Self { name: "ifc_lite_model".to_string(), tolerance: 0.01 }
183    }
184}
185
186/// Coverage stats for an HBJSON export.
187#[derive(Debug, Clone, serde::Serialize)]
188#[serde(rename_all = "camelCase")]
189pub struct HbjsonStats {
190    /// `IfcSpace` profiles seen in the model.
191    pub spaces: usize,
192    /// Rooms emitted (watertight prisms).
193    pub rooms: usize,
194    /// Spaces skipped as degenerate (malformed footprint / holes / non-extrusion — P5).
195    pub skipped: usize,
196    /// Windows placed as Apertures on exterior wall faces.
197    pub apertures: usize,
198    /// Doors placed on exterior wall faces.
199    pub doors: usize,
200    /// Railing / context shade meshes emitted.
201    pub shades: usize,
202    /// Opaque constructions derived from the IFC material layer sets.
203    pub constructions: usize,
204    /// Interior faces paired as `Surface` adjacencies (2 per shared wall).
205    pub interior_adjacencies: usize,
206}
207
208/// Export the `IfcSpace` volumes in `content` (raw IFC/STEP bytes) as an HBJSON string.
209///
210/// Rooms are built analytically from extruded-area profiles (watertight by construction);
211/// faces are typed Floor / RoofCeiling / Wall with outward normals. Returns a Honeybee-valid
212/// `Model` JSON ready to load via `honeybee.model.Model.from_hbjson`.
213pub fn export_hbjson(content: &[u8], opts: &HbjsonOptions) -> String {
214    export_hbjson_with_stats(content, opts).0
215}
216
217/// Like [`export_hbjson`] but also returns coverage stats (so callers can report how many
218/// spaces were skipped instead of silently truncating).
219pub fn export_hbjson_with_stats(content: &[u8], opts: &HbjsonOptions) -> (String, HbjsonStats) {
220    let profiles = extract_profiles(content, 0);
221    let spaces = profiles.iter().filter(|p| p.ifc_type == "IfcSpace").count();
222    let (mut rooms, origin, skipped) = rooms::build_rooms(&profiles, opts.tolerance);
223    openings::attach_openings(&profiles, &mut rooms, origin);
224    // Pair shared interior walls as Surface adjacencies (drops their exterior openings).
225    let interior_adjacencies = adjacency::solve_adjacency(&mut rooms);
226    let shade_meshes = shades::build_shades(&profiles, origin);
227
228    // Assign representative opaque constructions (from the IFC material layer sets) by face type.
229    let cons = constructions::build_constructions(content, &profiles);
230    for room in &mut rooms {
231        for f in &mut room.faces {
232            let id = match f.face_type {
233                "Wall" => cons.wall.clone(),
234                "Floor" => cons.floor.clone(),
235                "RoofCeiling" => cons.roof.clone(),
236                _ => None,
237            };
238            if let Some(id) = id {
239                f.set_construction(id);
240            }
241        }
242    }
243
244    let apertures = rooms.iter().flat_map(|r| &r.faces).map(|f| f.apertures.len()).sum();
245    let doors = rooms.iter().flat_map(|r| &r.faces).map(|f| f.doors.len()).sum();
246    let shades = shade_meshes.len();
247    let n_constructions = cons.energy.as_ref().map_or(0, |e| e.constructions.len());
248    let stats = HbjsonStats { spaces, rooms: rooms.len(), skipped, apertures, doors, shades, constructions: n_constructions, interior_adjacencies };
249
250    let model = Model::new(&sanitize_identifier(&opts.name), rooms, shade_meshes, cons.energy, opts.tolerance);
251    let json = serde_json::to_string(&model).expect("HBJSON model serializes");
252    (json, stats)
253}
254
255/// Options for DFJSON (Dragonfly) export.
256pub struct DfjsonOptions {
257    /// Model identifier / display name.
258    pub name: String,
259    /// Geometry tolerance in metres (Ladybug Tools default 0.01).
260    pub tolerance: f64,
261}
262
263impl Default for DfjsonOptions {
264    fn default() -> Self {
265        Self { name: "ifc_lite_model".to_string(), tolerance: 0.01 }
266    }
267}
268
269/// Export the `IfcSpace` volumes in `content` (raw IFC/STEP bytes) as a Dragonfly DFJSON
270/// string. Each space becomes an extruded `Room2D` (floor polygon + heights) grouped into
271/// stories — the simpler Ladybug target for mostly-vertical-wall models.
272pub fn export_dfjson(content: &[u8], opts: &DfjsonOptions) -> String {
273    export_dfjson_with_stats(content, opts).0
274}
275
276/// Like [`export_dfjson`] but also returns coverage stats.
277pub fn export_dfjson_with_stats(content: &[u8], opts: &DfjsonOptions) -> (String, DfjsonStats) {
278    let profiles = extract_profiles(content, 0);
279    // Read the file's own IfcBuilding / IfcBuildingStorey containment so stories are the
280    // model's storeys rather than an elevation guess at them (#1911). An empty index
281    // (no spatial structure declared) falls back to the elevation heuristic inside
282    // `build_model`.
283    let spatial = dfjson::spatial_index(content);
284    let (model, stats) =
285        dfjson::build_model(&sanitize_identifier(&opts.name), &profiles, opts.tolerance, Some(&spatial));
286    let json = serde_json::to_string(&model).expect("DFJSON model serializes");
287    (json, stats)
288}
289
290#[cfg(test)]
291#[path = "lib_tests.rs"]
292mod tests;