ifc_lite_export/
hbjson.rs

1// SPDX-License-Identifier: MPL-2.0
2//! Serde structs for the Honeybee HBJSON model schema (geometry subset).
3//!
4//! Field names/tags mirror exactly what `honeybee-schema` serializes (verified by
5//! round-tripping honeybee-written `.hbjson` through this crate). `Face3D.plane` is
6//! intentionally omitted — Honeybee derives it from the boundary on load.
7
8use serde::Serialize;
9
10/// A planar polygon: an ordered, wound boundary of `[x, y, z]` points (metres, Z-up).
11#[derive(Serialize)]
12pub struct Face3D {
13    #[serde(rename = "type")]
14    pub ty: &'static str, // "Face3D"
15    pub boundary: Vec<[f64; 3]>,
16}
17
18impl Face3D {
19    pub fn new(boundary: Vec<[f64; 3]>) -> Self {
20        Self { ty: "Face3D", boundary }
21    }
22}
23
24/// A boundary condition. `Outdoors`/`Ground` are the bare form; `Surface` (interior adjacency)
25/// also carries the adjacent face + room identifiers.
26#[derive(Serialize)]
27pub struct BoundaryCondition {
28    #[serde(rename = "type")]
29    pub ty: &'static str, // "Outdoors" | "Ground" | "Surface" | "Adiabatic"
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub boundary_condition_objects: Option<Vec<String>>,
32}
33
34impl BoundaryCondition {
35    pub fn new(ty: &'static str) -> Self {
36        Self { ty, boundary_condition_objects: None }
37    }
38    /// A `Surface` boundary condition pointing at the adjacent `[face, room]`.
39    pub fn surface(adjacent_face: String, adjacent_room: String) -> Self {
40        Self { ty: "Surface", boundary_condition_objects: Some(vec![adjacent_face, adjacent_room]) }
41    }
42}
43
44#[derive(Serialize)]
45pub struct TypedProps {
46    #[serde(rename = "type")]
47    pub ty: &'static str,
48}
49
50/// A Honeybee energy material (one opaque layer). Thicknesses come from the IFC material
51/// layer set; thermal properties are defaulted by material-name keyword (IFC rarely carries
52/// conductivity/density), so U-values are a sensible starting point, not authoritative.
53#[derive(Serialize, Clone)]
54pub struct EnergyMaterial {
55    #[serde(rename = "type")]
56    pub ty: &'static str, // "EnergyMaterial"
57    pub identifier: String,
58    pub roughness: &'static str, // "MediumRough"
59    pub thickness: f64,
60    pub conductivity: f64,
61    pub density: f64,
62    pub specific_heat: f64,
63    pub thermal_absorptance: f64,
64    pub solar_absorptance: f64,
65    pub visible_absorptance: f64,
66}
67
68impl EnergyMaterial {
69    pub fn new(identifier: String, thickness: f64, conductivity: f64, density: f64, specific_heat: f64) -> Self {
70        Self {
71            ty: "EnergyMaterial",
72            identifier,
73            roughness: "MediumRough",
74            thickness,
75            conductivity,
76            density,
77            specific_heat,
78            thermal_absorptance: 0.9,
79            solar_absorptance: 0.7,
80            visible_absorptance: 0.7,
81        }
82    }
83}
84
85/// An opaque construction referencing its materials (outside → inside) by identifier.
86#[derive(Serialize, Clone)]
87pub struct OpaqueConstruction {
88    #[serde(rename = "type")]
89    pub ty: &'static str, // "OpaqueConstructionAbridged"
90    pub identifier: String,
91    pub materials: Vec<String>,
92}
93
94impl OpaqueConstruction {
95    pub fn new(identifier: String, materials: Vec<String>) -> Self {
96        Self { ty: "OpaqueConstructionAbridged", identifier, materials }
97    }
98}
99
100/// Per-face energy properties carrying an optional construction reference.
101#[derive(Serialize)]
102pub struct FaceEnergy {
103    #[serde(rename = "type")]
104    pub ty: &'static str, // "FaceEnergyPropertiesAbridged"
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub construction: Option<String>,
107}
108
109#[derive(Serialize)]
110pub struct FaceProperties {
111    #[serde(rename = "type")]
112    pub ty: &'static str, // "FacePropertiesAbridged"
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub energy: Option<FaceEnergy>,
115}
116
117/// Model-level energy properties: the material + construction libraries.
118#[derive(Serialize)]
119pub struct ModelEnergy {
120    #[serde(rename = "type")]
121    pub ty: &'static str, // "ModelEnergyProperties"
122    pub materials: Vec<EnergyMaterial>,
123    pub constructions: Vec<OpaqueConstruction>,
124}
125
126#[derive(Serialize)]
127pub struct ModelProperties {
128    #[serde(rename = "type")]
129    pub ty: &'static str, // "ModelProperties"
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub energy: Option<ModelEnergy>,
132}
133
134/// A window — a planar sub-face of a parent wall Face, coplanar and within its boundary.
135#[derive(Serialize)]
136pub struct Aperture {
137    #[serde(rename = "type")]
138    pub ty: &'static str, // "Aperture"
139    pub identifier: String,
140    pub display_name: String,
141    pub properties: TypedProps,
142    pub geometry: Face3D,
143    pub is_operable: bool,
144    pub boundary_condition: BoundaryCondition,
145}
146
147impl Aperture {
148    pub fn new(identifier: String, geometry: Face3D, is_operable: bool) -> Self {
149        let display_name = identifier.clone();
150        Self {
151            ty: "Aperture",
152            identifier,
153            display_name,
154            properties: TypedProps { ty: "AperturePropertiesAbridged" },
155            geometry,
156            is_operable,
157            boundary_condition: BoundaryCondition::new("Outdoors"),
158        }
159    }
160}
161
162/// A door — a planar sub-face of a parent wall Face.
163#[derive(Serialize)]
164pub struct Door {
165    #[serde(rename = "type")]
166    pub ty: &'static str, // "Door"
167    pub identifier: String,
168    pub display_name: String,
169    pub properties: TypedProps,
170    pub geometry: Face3D,
171    pub is_glass: bool,
172    pub boundary_condition: BoundaryCondition,
173}
174
175impl Door {
176    pub fn new(identifier: String, geometry: Face3D, is_glass: bool) -> Self {
177        let display_name = identifier.clone();
178        Self {
179            ty: "Door",
180            identifier,
181            display_name,
182            properties: TypedProps { ty: "DoorPropertiesAbridged" },
183            geometry,
184            is_glass,
185            boundary_condition: BoundaryCondition::new("Outdoors"),
186        }
187    }
188}
189
190/// An arbitrary triangle mesh used for shading context (railings, balconies, etc.).
191/// No watertightness required — the render mesh is the right source here.
192#[derive(Serialize)]
193pub struct Mesh3D {
194    #[serde(rename = "type")]
195    pub ty: &'static str, // "Mesh3D"
196    pub vertices: Vec<[f64; 3]>,
197    pub faces: Vec<[usize; 3]>,
198}
199
200#[derive(Serialize)]
201pub struct ShadeMesh {
202    #[serde(rename = "type")]
203    pub ty: &'static str, // "ShadeMesh"
204    pub identifier: String,
205    pub display_name: String,
206    pub properties: TypedProps,
207    pub geometry: Mesh3D,
208}
209
210impl ShadeMesh {
211    pub fn new(identifier: String, vertices: Vec<[f64; 3]>, faces: Vec<[usize; 3]>) -> Self {
212        let display_name = identifier.clone();
213        Self {
214            ty: "ShadeMesh",
215            identifier,
216            display_name,
217            properties: TypedProps { ty: "ShadeMeshPropertiesAbridged" },
218            geometry: Mesh3D { ty: "Mesh3D", vertices, faces },
219        }
220    }
221}
222
223/// One face of a Room. `face_type` is "Wall" | "RoofCeiling" | "Floor" | "AirBoundary".
224#[derive(Serialize)]
225pub struct Face {
226    #[serde(rename = "type")]
227    pub ty: &'static str, // "Face"
228    pub identifier: String,
229    pub display_name: String,
230    pub properties: FaceProperties,
231    pub geometry: Face3D,
232    pub face_type: &'static str,
233    pub boundary_condition: BoundaryCondition,
234    #[serde(skip_serializing_if = "Vec::is_empty")]
235    pub apertures: Vec<Aperture>,
236    #[serde(skip_serializing_if = "Vec::is_empty")]
237    pub doors: Vec<Door>,
238}
239
240impl Face {
241    pub fn new(identifier: String, geometry: Face3D, face_type: &'static str, bc: &'static str) -> Self {
242        let display_name = identifier.clone();
243        Self {
244            ty: "Face",
245            identifier,
246            display_name,
247            properties: FaceProperties { ty: "FacePropertiesAbridged", energy: None },
248            geometry,
249            face_type,
250            boundary_condition: BoundaryCondition::new(bc),
251            apertures: Vec::new(),
252            doors: Vec::new(),
253        }
254    }
255
256    /// Assign an opaque construction (by identifier) to this face's energy properties.
257    pub fn set_construction(&mut self, construction: String) {
258        self.properties.energy = Some(FaceEnergy {
259            ty: "FaceEnergyPropertiesAbridged",
260            construction: Some(construction),
261        });
262    }
263
264    /// Make this face an interior `Surface` adjacency to `(adj_face, adj_room)`. Drops any
265    /// apertures/doors — they were placed as exterior openings; interior openings need a
266    /// matched pair on the other side (out of scope here).
267    pub fn set_surface_bc(&mut self, adj_face: String, adj_room: String) {
268        self.boundary_condition = BoundaryCondition::surface(adj_face, adj_room);
269        self.apertures.clear();
270        self.doors.clear();
271    }
272}
273
274/// A closed volume of faces (one thermal zone).
275#[derive(Serialize)]
276pub struct Room {
277    #[serde(rename = "type")]
278    pub ty: &'static str, // "Room"
279    pub identifier: String,
280    pub display_name: String,
281    pub properties: TypedProps,
282    pub faces: Vec<Face>,
283}
284
285impl Room {
286    pub fn new(identifier: String, faces: Vec<Face>) -> Self {
287        let display_name = identifier.clone();
288        Self {
289            ty: "Room",
290            identifier,
291            display_name,
292            properties: TypedProps { ty: "RoomPropertiesAbridged" },
293            faces,
294        }
295    }
296}
297
298/// The top-level Honeybee model.
299#[derive(Serialize)]
300pub struct Model {
301    #[serde(rename = "type")]
302    pub ty: &'static str, // "Model"
303    pub identifier: String,
304    pub display_name: String,
305    pub units: &'static str, // "Meters"
306    pub properties: ModelProperties,
307    pub rooms: Vec<Room>,
308    #[serde(skip_serializing_if = "Vec::is_empty")]
309    pub shade_meshes: Vec<ShadeMesh>,
310    pub tolerance: f64,
311    pub angle_tolerance: f64,
312    pub version: &'static str,
313}
314
315impl Model {
316    pub fn new(
317        identifier: &str,
318        rooms: Vec<Room>,
319        shade_meshes: Vec<ShadeMesh>,
320        energy: Option<ModelEnergy>,
321        tolerance: f64,
322    ) -> Self {
323        Self {
324            ty: "Model",
325            identifier: identifier.to_string(),
326            display_name: identifier.to_string(),
327            units: "Meters",
328            properties: ModelProperties { ty: "ModelProperties", energy },
329            rooms,
330            shade_meshes,
331            tolerance,
332            angle_tolerance: 1.0,
333            version: "1.0.0",
334        }
335    }
336}