use super::entities::{
Classification, Extrusion, IfcEntity, Property, PropertySet, PropertyValue, UnitAssignment,
};
use super::{IfcModel, MaterialInfo, Storey};
use crate::elements::circulation::Stair;
use crate::elements::level::Level;
use crate::elements::openings::{Door, Window};
use crate::elements::styling::Material;
use crate::elements::wall::Wall;
use crate::walker::DecodedElement;
#[derive(Debug, Clone)]
pub struct ElementInput<'a> {
pub decoded: &'a DecodedElement,
pub display_name: String,
pub guid: Option<String>,
pub storey_index: Option<usize>,
pub material_index: Option<usize>,
pub property_set: Option<PropertySet>,
pub location_feet: Option<[f64; 3]>,
pub rotation_radians: Option<f64>,
pub extrusion: Option<Extrusion>,
pub host_element_index: Option<usize>,
pub material_layer_set_index: Option<usize>,
pub material_profile_set_index: Option<usize>,
pub solid_shape: Option<crate::ifc::entities::SolidShape>,
pub representation_map_index: Option<usize>,
}
#[derive(Debug, Clone, Default)]
pub struct BuilderOptions {
pub classifications: Vec<Classification>,
pub units: Vec<UnitAssignment>,
pub project_name: Option<String>,
pub description: Option<String>,
pub storeys: Vec<Storey>,
pub materials: Vec<MaterialInfo>,
}
pub fn wall_extrusion(
wall: &Wall,
wall_type: Option<&crate::elements::wall::WallType>,
length_feet: f64,
) -> Extrusion {
Extrusion {
width_feet: length_feet,
depth_feet: wall_type.and_then(|wt| wt.width_feet).unwrap_or(8.0 / 12.0),
height_feet: wall.unconnected_height_feet.unwrap_or(10.0),
profile_override: None,
}
}
pub fn wall_segment_length_feet(start: [f64; 2], end: [f64; 2]) -> f64 {
let dx = end[0] - start[0];
let dy = end[1] - start[1];
(dx * dx + dy * dy).sqrt()
}
pub fn wall_segment_angle_radians(start: [f64; 2], end: [f64; 2]) -> f64 {
let dx = end[0] - start[0];
let dy = end[1] - start[1];
dy.atan2(dx)
}
pub fn wall_extrusion_from_location_line(
wall: &Wall,
wall_type: Option<&crate::elements::wall::WallType>,
start: [f64; 2],
end: [f64; 2],
) -> Extrusion {
wall_extrusion(wall, wall_type, wall_segment_length_feet(start, end))
}
pub fn wall_layered_extrusions_from_location_line(
wall: &Wall,
start: [f64; 2],
end: [f64; 2],
layer_thicknesses_feet: &[f64],
) -> Vec<Extrusion> {
let length = wall_segment_length_feet(start, end);
let height = wall.unconnected_height_feet.unwrap_or(10.0);
layer_thicknesses_feet
.iter()
.filter(|t| **t > 0.0)
.map(|thickness| Extrusion {
width_feet: length,
depth_feet: *thickness,
height_feet: height,
profile_override: None,
})
.collect()
}
pub fn slab_extrusion(
length_feet: f64,
width_feet: f64,
floor_type: Option<&crate::elements::floor::FloorType>,
) -> Extrusion {
Extrusion {
width_feet: length_feet,
depth_feet: width_feet,
height_feet: floor_type.and_then(|ft| ft.thickness_feet).unwrap_or(1.0),
profile_override: None,
}
}
pub fn polygon_area_sqft(points: &[(f64, f64)]) -> f64 {
if points.len() < 3 {
return 0.0;
}
let n = points.len();
let mut sum = 0.0;
for i in 0..n {
let (x0, y0) = points[i];
let (x1, y1) = points[(i + 1) % n];
sum += x0 * y1 - x1 * y0;
}
(sum * 0.5).abs()
}
pub fn polygon_perimeter_feet(points: &[(f64, f64)]) -> f64 {
if points.len() < 2 {
return 0.0;
}
let n = points.len();
let mut sum = 0.0;
for i in 0..n {
let (x0, y0) = points[i];
let (x1, y1) = points[(i + 1) % n];
let dx = x1 - x0;
let dy = y1 - y0;
sum += (dx * dx + dy * dy).sqrt();
}
sum
}
pub fn floor_extrusion_from_boundary(
boundary: &[(f64, f64)],
floor_type: Option<&crate::elements::floor::FloorType>,
) -> Extrusion {
let thickness = floor_type.and_then(|ft| ft.thickness_feet).unwrap_or(1.0);
if boundary.len() < 3 {
return Extrusion {
width_feet: 1.0,
depth_feet: 1.0,
height_feet: thickness,
profile_override: None,
};
}
Extrusion::arbitrary_closed(boundary.to_vec(), thickness)
}
pub fn floor_base_quantities(
boundary: &[(f64, f64)],
floor_type: Option<&crate::elements::floor::FloorType>,
) -> PropertySet {
let mut props = Vec::new();
let thickness = floor_type.and_then(|ft| ft.thickness_feet).unwrap_or(1.0);
if boundary.len() >= 3 {
let area = polygon_area_sqft(boundary);
let perim = polygon_perimeter_feet(boundary);
props.push(Property {
name: "GrossArea".into(),
value: PropertyValue::AreaSquareFeet(area),
});
props.push(Property {
name: "Perimeter".into(),
value: PropertyValue::LengthFeet(perim),
});
props.push(Property {
name: "Depth".into(),
value: PropertyValue::LengthFeet(thickness),
});
props.push(Property {
name: "GrossVolume".into(),
value: PropertyValue::VolumeCubicFeet(area * thickness),
});
}
PropertySet {
name: "Qto_SlabBaseQuantities".into(),
properties: props,
}
}
pub fn roof_extrusion(
length_feet: f64,
width_feet: f64,
roof_type: Option<&crate::elements::roof::RoofType>,
) -> Extrusion {
Extrusion {
width_feet: length_feet,
depth_feet: width_feet,
height_feet: roof_type.and_then(|rt| rt.thickness_feet).unwrap_or(1.0),
profile_override: None,
}
}
pub fn roof_pitch_radians_from_rise_run(rise: f64, run: f64) -> f64 {
if run <= 0.0 {
return 0.0;
}
(rise / run).atan()
}
pub fn roof_pitch_radians_from_degrees(degrees: f64) -> f64 {
degrees.to_radians()
}
pub fn gabled_roof_ridge_height(span_feet: f64, pitch_rad: f64) -> f64 {
let s = span_feet.max(0.0);
let p = pitch_rad.max(0.0);
(s * 0.5) * p.tan()
}
pub fn hip_roof_ridge_length(length_feet: f64, width_feet: f64) -> f64 {
(length_feet.max(width_feet) - length_feet.min(width_feet)).max(0.0)
}
pub fn gabled_roof_extrusion(
length_feet: f64,
span_feet: f64,
pitch_rad: f64,
roof_type: Option<&crate::elements::roof::RoofType>,
) -> Extrusion {
if pitch_rad <= 0.0 || span_feet <= 0.0 {
return roof_extrusion(length_feet, span_feet, roof_type);
}
let half_span = span_feet * 0.5;
let ridge_h = gabled_roof_ridge_height(span_feet, pitch_rad);
let profile = vec![(-half_span, 0.0), (half_span, 0.0), (0.0, ridge_h)];
Extrusion::arbitrary_closed(profile, length_feet.max(0.0))
}
pub fn hip_roof_brep(
length_feet: f64,
width_feet: f64,
pitch_rad: f64,
) -> (Vec<[f64; 3]>, Vec<super::entities::BrepTriangle>) {
let l = length_feet.max(0.0);
let w = width_feet.max(0.0);
let (long, short) = if l >= w { (l, w) } else { (w, l) };
let ridge = hip_roof_ridge_length(long, short);
let ridge_h = gabled_roof_ridge_height(short, pitch_rad.max(0.0));
let overhang = (long - ridge) * 0.5;
let swap = l < w;
let eave = if !swap {
[
[0.0, 0.0, 0.0], [long, 0.0, 0.0], [long, short, 0.0], [0.0, short, 0.0], ]
} else {
[
[0.0, 0.0, 0.0],
[short, 0.0, 0.0],
[short, long, 0.0],
[0.0, long, 0.0],
]
};
let (ra, rb) = if !swap {
(
[overhang, short * 0.5, ridge_h],
[long - overhang, short * 0.5, ridge_h],
)
} else {
(
[short * 0.5, overhang, ridge_h],
[short * 0.5, long - overhang, ridge_h],
)
};
let mut vertices: Vec<[f64; 3]> = Vec::with_capacity(6);
vertices.extend_from_slice(&eave);
vertices.push(ra); vertices.push(rb);
let triangles = vec![
super::entities::BrepTriangle(0, 1, 5),
super::entities::BrepTriangle(0, 5, 4),
super::entities::BrepTriangle(2, 3, 4),
super::entities::BrepTriangle(2, 4, 5),
super::entities::BrepTriangle(3, 0, 4),
super::entities::BrepTriangle(1, 2, 5),
];
(vertices, triangles)
}
pub fn stair_pitch_radians(riser_height_feet: f64, tread_depth_feet: f64) -> f64 {
if tread_depth_feet <= 0.0 {
return std::f64::consts::FRAC_PI_2;
}
(riser_height_feet / tread_depth_feet).atan()
}
pub fn stair_run_length_feet(riser_count: u32, tread_depth_feet: f64) -> f64 {
if riser_count == 0 {
return 0.0;
}
(riser_count - 1) as f64 * tread_depth_feet.max(0.0)
}
pub fn stair_sawtooth_profile(
riser_count: u32,
riser_height_feet: f64,
tread_depth_feet: f64,
) -> Vec<(f64, f64)> {
if riser_count == 0 {
return Vec::new();
}
let rc = riser_count as f64;
let total_rise = rc * riser_height_feet;
let total_run = stair_run_length_feet(riser_count, tread_depth_feet);
let mut pts: Vec<(f64, f64)> = Vec::with_capacity(2 * riser_count as usize + 3);
pts.push((0.0, 0.0));
pts.push((0.0, total_rise));
pts.push((tread_depth_feet, total_rise));
for i in (0..riser_count).rev() {
let x = (riser_count - 1 - i) as f64 * tread_depth_feet + tread_depth_feet;
let y = i as f64 * riser_height_feet;
pts.push((x, y));
let x_next = x + tread_depth_feet;
pts.push((x_next, y));
}
let _ = total_run; pts
}
pub fn stair_run_extrusion(stair: &Stair, width_feet: f64) -> Extrusion {
let riser_count = stair.actual_riser_count.unwrap_or(0);
let riser_h = stair.actual_riser_height_feet.unwrap_or(0.0);
let tread_d = stair.actual_tread_depth_feet.unwrap_or(0.0);
let w = width_feet.max(0.0);
if riser_count == 0 || riser_h <= 0.0 || tread_d <= 0.0 || w <= 0.0 {
return Extrusion {
width_feet: stair_run_length_feet(riser_count, tread_d).max(1.0),
depth_feet: w.max(1.0),
height_feet: 1.0,
profile_override: None,
};
}
let pts = stair_sawtooth_profile(riser_count, riser_h, tread_d);
Extrusion::arbitrary_closed(pts, w)
}
pub fn stair_tread_extrusion(
tread_depth_feet: f64,
width_feet: f64,
thickness_feet: f64,
) -> Extrusion {
Extrusion {
width_feet: tread_depth_feet.max(0.0),
depth_feet: width_feet.max(0.0),
height_feet: thickness_feet.max(0.0),
profile_override: None,
}
}
pub fn stair_landing_extrusion(
length_feet: f64,
width_feet: f64,
thickness_feet: f64,
) -> Extrusion {
Extrusion {
width_feet: length_feet.max(0.0),
depth_feet: width_feet.max(0.0),
height_feet: thickness_feet.max(0.0),
profile_override: None,
}
}
pub const DEFAULT_OPENING_MARGIN_FEET: f64 = 0.01;
pub fn window_extrusion(width_feet: f64, height_feet: f64, depth_feet: f64) -> Extrusion {
Extrusion {
width_feet: width_feet.max(0.0),
depth_feet: depth_feet.max(0.0),
height_feet: height_feet.max(0.0),
profile_override: None,
}
}
pub fn window_opening_extrusion(
width_feet: f64,
height_feet: f64,
wall_thickness_feet: f64,
margin_feet: f64,
) -> Extrusion {
let m = margin_feet.max(0.0);
Extrusion {
width_feet: (width_feet + 2.0 * m).max(0.0),
depth_feet: wall_thickness_feet.max(0.0),
height_feet: (height_feet + 2.0 * m).max(0.0),
profile_override: None,
}
}
pub fn door_extrusion(width_feet: f64, height_feet: f64, depth_feet: f64) -> Extrusion {
window_extrusion(width_feet, height_feet, depth_feet)
}
pub fn door_opening_extrusion(
width_feet: f64,
height_feet: f64,
wall_thickness_feet: f64,
margin_feet: f64,
) -> Extrusion {
window_opening_extrusion(width_feet, height_feet, wall_thickness_feet, margin_feet)
}
pub fn window_placement_z_feet(host_level_elevation_feet: f64, sill_height_feet: f64) -> f64 {
host_level_elevation_feet + sill_height_feet.max(0.0)
}
pub fn window_dimensions_property_set(
window: &Window,
width_feet: f64,
height_feet: f64,
depth_feet: f64,
) -> PropertySet {
let mut pset = window_property_set(window);
if width_feet > 0.0 {
pset.properties.push(Property {
name: "OverallWidth".into(),
value: PropertyValue::LengthFeet(width_feet),
});
}
if height_feet > 0.0 {
pset.properties.push(Property {
name: "OverallHeight".into(),
value: PropertyValue::LengthFeet(height_feet),
});
}
if depth_feet > 0.0 {
pset.properties.push(Property {
name: "Depth".into(),
value: PropertyValue::LengthFeet(depth_feet),
});
}
pset
}
pub fn door_dimensions_property_set(
door: &Door,
width_feet: f64,
height_feet: f64,
depth_feet: f64,
) -> PropertySet {
let mut pset = door_property_set(door);
if width_feet > 0.0 {
pset.properties.push(Property {
name: "OverallWidth".into(),
value: PropertyValue::LengthFeet(width_feet),
});
}
if height_feet > 0.0 {
pset.properties.push(Property {
name: "OverallHeight".into(),
value: PropertyValue::LengthFeet(height_feet),
});
}
if depth_feet > 0.0 {
pset.properties.push(Property {
name: "Depth".into(),
value: PropertyValue::LengthFeet(depth_feet),
});
}
pset
}
pub fn ceiling_extrusion(
length_feet: f64,
width_feet: f64,
ceiling_type: Option<&crate::elements::ceiling::CeilingType>,
) -> Extrusion {
Extrusion {
width_feet: length_feet,
depth_feet: width_feet,
height_feet: ceiling_type
.and_then(|ct| ct.thickness_feet)
.unwrap_or(1.0 / 12.0),
profile_override: None,
}
}
pub fn column_extrusion(
column: &crate::elements::structural::Column,
profile_width_feet: f64,
profile_depth_feet: f64,
level_elevation_diff_feet: f64,
) -> Extrusion {
let offset_delta =
column.top_offset_feet.unwrap_or(0.0) - column.base_offset_feet.unwrap_or(0.0);
Extrusion {
width_feet: profile_width_feet,
depth_feet: profile_depth_feet,
height_feet: (level_elevation_diff_feet + offset_delta).max(0.1),
profile_override: None,
}
}
pub fn column_height_from_levels(
column: &crate::elements::structural::Column,
base_level_elevation_feet: Option<f64>,
top_level_elevation_feet: Option<f64>,
) -> Option<f64> {
let base_elev = base_level_elevation_feet?;
let top_elev = top_level_elevation_feet?;
let diff = top_elev - base_elev;
let offset_delta =
column.top_offset_feet.unwrap_or(0.0) - column.base_offset_feet.unwrap_or(0.0);
Some((diff + offset_delta).max(0.1))
}
pub fn column_i_shape_extrusion(
overall_width_feet: f64,
overall_depth_feet: f64,
web_thickness_feet: f64,
flange_thickness_feet: f64,
height_feet: f64,
) -> Extrusion {
Extrusion::i_shape(
overall_width_feet,
overall_depth_feet,
web_thickness_feet,
flange_thickness_feet,
height_feet.max(0.1),
)
}
pub fn column_circular_extrusion(radius_feet: f64, height_feet: f64) -> Extrusion {
Extrusion::circle(radius_feet.max(0.0), height_feet.max(0.1))
}
pub fn column_rectangular_hollow_extrusion(
outer_width_feet: f64,
outer_depth_feet: f64,
wall_thickness_feet: f64,
height_feet: f64,
) -> Extrusion {
Extrusion::rectangle_hollow(
outer_width_feet.max(0.0),
outer_depth_feet.max(0.0),
wall_thickness_feet.max(0.0),
height_feet.max(0.1),
)
}
pub fn column_arbitrary_profile_extrusion(
profile_points: Vec<(f64, f64)>,
height_feet: f64,
) -> Extrusion {
Extrusion::arbitrary_closed(profile_points, height_feet.max(0.1))
}
pub fn column_property_set(column: &crate::elements::structural::Column) -> PropertySet {
let mut props = Vec::new();
if let Some(v) = column.base_offset_feet {
props.push(Property {
name: "BaseOffset".into(),
value: PropertyValue::LengthFeet(v),
});
}
if let Some(v) = column.top_offset_feet {
props.push(Property {
name: "TopOffset".into(),
value: PropertyValue::LengthFeet(v),
});
}
if let Some(v) = column.rotation_radians {
props.push(Property {
name: "Rotation".into(),
value: PropertyValue::AngleRadians(v),
});
}
if let Some(v) = column.is_structural {
props.push(Property {
name: "LoadBearing".into(),
value: PropertyValue::Boolean(v),
});
}
PropertySet {
name: "Pset_ColumnCommon".into(),
properties: props,
}
}
pub fn beam_length_3d_feet(start: crate::geometry::Point3, end: crate::geometry::Point3) -> f64 {
let dx = end.x - start.x;
let dy = end.y - start.y;
let dz = end.z - start.z;
(dx * dx + dy * dy + dz * dz).sqrt()
}
pub fn beam_axis_yaw_radians(start: crate::geometry::Point3, end: crate::geometry::Point3) -> f64 {
let dx = end.x - start.x;
let dy = end.y - start.y;
dy.atan2(dx)
}
pub fn beam_axis_pitch_radians(
start: crate::geometry::Point3,
end: crate::geometry::Point3,
) -> f64 {
let dx = end.x - start.x;
let dy = end.y - start.y;
let dz = end.z - start.z;
let horiz = (dx * dx + dy * dy).sqrt();
if horiz == 0.0 && dz == 0.0 {
return 0.0;
}
dz.atan2(horiz)
}
pub fn beam_is_horizontal(
start: crate::geometry::Point3,
end: crate::geometry::Point3,
eps_feet: f64,
) -> bool {
(end.z - start.z).abs() < eps_feet
}
pub fn beam_i_shape_extrusion(
overall_width_feet: f64,
overall_depth_feet: f64,
web_thickness_feet: f64,
flange_thickness_feet: f64,
length_feet: f64,
) -> Extrusion {
Extrusion::i_shape(
overall_width_feet,
overall_depth_feet,
web_thickness_feet,
flange_thickness_feet,
length_feet.max(0.1),
)
}
pub fn beam_rectangular_hollow_extrusion(
outer_width_feet: f64,
outer_depth_feet: f64,
wall_thickness_feet: f64,
length_feet: f64,
) -> Extrusion {
Extrusion::rectangle_hollow(
outer_width_feet.max(0.0),
outer_depth_feet.max(0.0),
wall_thickness_feet.max(0.0),
length_feet.max(0.1),
)
}
pub fn beam_arbitrary_profile_extrusion(
profile_points: Vec<(f64, f64)>,
length_feet: f64,
) -> Extrusion {
Extrusion::arbitrary_closed(profile_points, length_feet.max(0.1))
}
pub fn beam_rectangular_extrusion(width_feet: f64, depth_feet: f64, length_feet: f64) -> Extrusion {
Extrusion {
width_feet: width_feet.max(0.0),
depth_feet: depth_feet.max(0.0),
height_feet: length_feet.max(0.1),
profile_override: None,
}
}
pub fn beam_property_set(beam: &crate::elements::structural::Beam) -> PropertySet {
let mut props = Vec::new();
if let Some(v) = beam.start_level_offset_feet {
props.push(Property {
name: "StartLevelOffset".into(),
value: PropertyValue::LengthFeet(v),
});
}
if let Some(v) = beam.end_level_offset_feet {
props.push(Property {
name: "EndLevelOffset".into(),
value: PropertyValue::LengthFeet(v),
});
}
if let Some(v) = beam.cross_section_rotation_radians {
props.push(Property {
name: "CrossSectionRotation".into(),
value: PropertyValue::AngleRadians(v),
});
}
if let (Some(s), Some(e)) = (beam.start, beam.end) {
props.push(Property {
name: "Span".into(),
value: PropertyValue::LengthFeet(beam_length_3d_feet(s, e)),
});
}
PropertySet {
name: "Pset_BeamCommon".into(),
properties: props,
}
}
pub fn wall_property_set(wall: &Wall) -> PropertySet {
let mut props = Vec::new();
if let Some(v) = wall.base_offset_feet {
props.push(Property {
name: "BaseOffset".into(),
value: PropertyValue::LengthFeet(v),
});
}
if let Some(v) = wall.top_offset_feet {
props.push(Property {
name: "TopOffset".into(),
value: PropertyValue::LengthFeet(v),
});
}
if let Some(v) = wall.unconnected_height_feet {
props.push(Property {
name: "UnconnectedHeight".into(),
value: PropertyValue::LengthFeet(v),
});
}
if let Some(usage) = wall.structural_usage {
props.push(Property {
name: "StructuralUsage".into(),
value: PropertyValue::Text(format!("{usage:?}")),
});
}
if let Some(line) = wall.location_line {
props.push(Property {
name: "LocationLine".into(),
value: PropertyValue::Text(format!("{line:?}")),
});
}
PropertySet {
name: "Pset_WallCommon".into(),
properties: props,
}
}
pub fn door_property_set(door: &Door) -> PropertySet {
let mut props = Vec::new();
if let Some(v) = door.rotation_radians {
props.push(Property {
name: "Rotation".into(),
value: PropertyValue::AngleRadians(v),
});
}
if let Some(v) = door.flip_hand {
props.push(Property {
name: "FlipHand".into(),
value: PropertyValue::Boolean(v),
});
}
if let Some(v) = door.flip_facing {
props.push(Property {
name: "FlipFacing".into(),
value: PropertyValue::Boolean(v),
});
}
PropertySet {
name: "Pset_DoorCommon".into(),
properties: props,
}
}
pub fn window_property_set(window: &Window) -> PropertySet {
let mut props = Vec::new();
if let Some(v) = window.sill_height_feet {
props.push(Property {
name: "SillHeight".into(),
value: PropertyValue::LengthFeet(v),
});
}
if let Some(v) = window.rotation_radians {
props.push(Property {
name: "Rotation".into(),
value: PropertyValue::AngleRadians(v),
});
}
PropertySet {
name: "Pset_WindowCommon".into(),
properties: props,
}
}
pub fn stair_property_set(stair: &Stair) -> PropertySet {
let mut props = Vec::new();
if let Some(v) = stair.actual_riser_count {
props.push(Property {
name: "NumberOfRisers".into(),
value: PropertyValue::Integer(v as i64),
});
}
if let Some(v) = stair.actual_tread_depth_feet {
props.push(Property {
name: "TreadDepth".into(),
value: PropertyValue::LengthFeet(v),
});
}
if let Some(v) = stair.actual_riser_height_feet {
props.push(Property {
name: "RiserHeight".into(),
value: PropertyValue::LengthFeet(v),
});
}
if let Some(total) = stair.total_rise_feet() {
props.push(Property {
name: "TotalRise".into(),
value: PropertyValue::LengthFeet(total),
});
}
PropertySet {
name: "Pset_StairCommon".into(),
properties: props,
}
}
pub fn materials_from_revit(materials: &[Material]) -> Vec<MaterialInfo> {
materials
.iter()
.filter_map(|m| {
Some(MaterialInfo {
name: m.name.clone()?,
color_packed: m.color,
transparency: m.transparency,
})
})
.collect()
}
pub fn storeys_from_levels(levels: &[Level]) -> Vec<Storey> {
levels
.iter()
.filter(|l| l.is_building_story.unwrap_or(true))
.filter_map(|l| {
Some(Storey {
name: l.name.clone()?,
elevation_feet: l.elevation_feet.unwrap_or(0.0),
})
})
.collect()
}
pub fn build_ifc_model(inputs: &[ElementInput<'_>], options: BuilderOptions) -> IfcModel {
let mut entities: Vec<IfcEntity> = Vec::with_capacity(inputs.len());
for input in inputs {
let mapping = super::category_map::lookup(&input.decoded.class);
let ifc_type = mapping
.map(|m| m.ifc_type)
.unwrap_or("IFCBUILDINGELEMENTPROXY");
entities.push(IfcEntity::BuildingElement {
ifc_type: ifc_type.to_string(),
name: input.display_name.clone(),
type_guid: input.guid.clone(),
predefined_type: mapping.and_then(|m| m.predefined_type).map(str::to_string),
storey_index: input.storey_index,
material_index: input.material_index,
property_set: input.property_set.clone(),
location_feet: input.location_feet,
rotation_radians: input.rotation_radians,
extrusion: input.extrusion.clone(),
host_element_index: input.host_element_index,
material_layer_set_index: input.material_layer_set_index,
material_profile_set_index: input.material_profile_set_index,
solid_shape: input.solid_shape.clone(),
representation_map_index: input.representation_map_index,
});
}
let project_name = options.project_name.or_else(|| {
inputs
.first()
.map(|e| format!("RVT project ({})", e.decoded.class))
});
IfcModel {
project_name,
description: options.description,
entities,
classifications: options.classifications,
units: options.units,
building_storeys: options.storeys,
materials: options.materials,
material_layer_sets: Vec::new(),
material_profile_sets: Vec::new(),
representation_maps: Vec::new(),
global_ids: Default::default(),
element_layers: Default::default(),
material_layer_usages: Default::default(),
element_type_materials: Default::default(),
element_type_ids: Default::default(),
material_constituent_sets: Vec::new(),
opening_cuts: Default::default(),
}
}
pub fn entity_type_histogram(model: &IfcModel) -> std::collections::BTreeMap<String, usize> {
let mut out: std::collections::BTreeMap<String, usize> = std::collections::BTreeMap::new();
for e in &model.entities {
if let IfcEntity::BuildingElement { ifc_type, .. } = e {
*out.entry(ifc_type.clone()).or_insert(0) += 1;
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::walker::{DecodedElement, InstanceField};
fn mk_decoded(class: &str) -> DecodedElement {
DecodedElement {
id: None,
class: class.to_string(),
fields: vec![("name".to_string(), InstanceField::String(class.to_string()))],
byte_range: 0..0,
provenance: Default::default(),
}
}
#[test]
fn build_model_maps_known_classes() {
let wall = mk_decoded("Wall");
let floor = mk_decoded("Floor");
let roof = mk_decoded("Roof");
let inputs = vec![
ElementInput {
decoded: &wall,
display_name: "Wall-1".into(),
guid: None,
storey_index: None,
material_index: None,
property_set: None,
location_feet: None,
rotation_radians: None,
extrusion: None,
host_element_index: None,
material_layer_set_index: None,
material_profile_set_index: None,
solid_shape: None,
representation_map_index: None,
},
ElementInput {
decoded: &floor,
display_name: "Slab-1".into(),
guid: None,
storey_index: None,
material_index: None,
property_set: None,
location_feet: None,
rotation_radians: None,
extrusion: None,
host_element_index: None,
material_layer_set_index: None,
material_profile_set_index: None,
solid_shape: None,
representation_map_index: None,
},
ElementInput {
decoded: &roof,
display_name: "Roof-1".into(),
guid: None,
storey_index: None,
material_index: None,
property_set: None,
location_feet: None,
rotation_radians: None,
extrusion: None,
host_element_index: None,
material_layer_set_index: None,
material_profile_set_index: None,
solid_shape: None,
representation_map_index: None,
},
];
let model = build_ifc_model(&inputs, BuilderOptions::default());
assert_eq!(model.entities.len(), 3);
let hist = entity_type_histogram(&model);
assert_eq!(hist.get("IFCWALL"), Some(&1));
assert_eq!(hist.get("IFCSLAB"), Some(&1));
assert_eq!(hist.get("IFCROOF"), Some(&1));
}
#[test]
fn unknown_class_falls_back_to_proxy() {
let custom = mk_decoded("SomeCustomAutodeskExtension");
let inputs = vec![ElementInput {
decoded: &custom,
display_name: "Mystery-1".into(),
guid: None,
storey_index: None,
material_index: None,
property_set: None,
location_feet: None,
rotation_radians: None,
extrusion: None,
host_element_index: None,
material_layer_set_index: None,
material_profile_set_index: None,
solid_shape: None,
representation_map_index: None,
}];
let model = build_ifc_model(&inputs, BuilderOptions::default());
let hist = entity_type_histogram(&model);
assert_eq!(hist.get("IFCBUILDINGELEMENTPROXY"), Some(&1));
}
#[test]
fn project_name_default_uses_first_class() {
let wall = mk_decoded("Wall");
let inputs = vec![ElementInput {
decoded: &wall,
display_name: "Wall-1".into(),
guid: None,
storey_index: None,
material_index: None,
property_set: None,
location_feet: None,
rotation_radians: None,
extrusion: None,
host_element_index: None,
material_layer_set_index: None,
material_profile_set_index: None,
solid_shape: None,
representation_map_index: None,
}];
let model = build_ifc_model(&inputs, BuilderOptions::default());
assert!(
model
.project_name
.as_deref()
.unwrap()
.starts_with("RVT project")
);
}
#[test]
fn project_name_override_wins() {
let wall = mk_decoded("Wall");
let inputs = vec![ElementInput {
decoded: &wall,
display_name: "Wall-1".into(),
guid: None,
storey_index: None,
material_index: None,
property_set: None,
location_feet: None,
rotation_radians: None,
extrusion: None,
host_element_index: None,
material_layer_set_index: None,
material_profile_set_index: None,
solid_shape: None,
representation_map_index: None,
}];
let opts = BuilderOptions {
project_name: Some("Acme HQ".into()),
..Default::default()
};
let model = build_ifc_model(&inputs, opts);
assert_eq!(model.project_name.as_deref(), Some("Acme HQ"));
}
#[test]
fn empty_input_produces_empty_model() {
let model = build_ifc_model(&[], BuilderOptions::default());
assert!(model.entities.is_empty());
assert!(model.project_name.is_none());
}
#[test]
fn histogram_counts_multiple_of_same_type() {
let w1 = mk_decoded("Wall");
let w2 = mk_decoded("Wall");
let w3 = mk_decoded("Wall");
let inputs = vec![
ElementInput {
decoded: &w1,
display_name: "Wall-N".into(),
guid: None,
storey_index: None,
material_index: None,
property_set: None,
location_feet: None,
rotation_radians: None,
extrusion: None,
host_element_index: None,
material_layer_set_index: None,
material_profile_set_index: None,
solid_shape: None,
representation_map_index: None,
},
ElementInput {
decoded: &w2,
display_name: "Wall-E".into(),
guid: None,
storey_index: None,
material_index: None,
property_set: None,
location_feet: None,
rotation_radians: None,
extrusion: None,
host_element_index: None,
material_layer_set_index: None,
material_profile_set_index: None,
solid_shape: None,
representation_map_index: None,
},
ElementInput {
decoded: &w3,
display_name: "Wall-S".into(),
guid: None,
storey_index: None,
material_index: None,
property_set: None,
location_feet: None,
rotation_radians: None,
extrusion: None,
host_element_index: None,
material_layer_set_index: None,
material_profile_set_index: None,
solid_shape: None,
representation_map_index: None,
},
];
let model = build_ifc_model(&inputs, BuilderOptions::default());
let hist = entity_type_histogram(&model);
assert_eq!(hist.get("IFCWALL"), Some(&3));
}
#[test]
fn storeys_from_levels_filters_non_building_stories() {
let l1 = Level {
name: Some("Level 1".into()),
elevation_feet: Some(0.0),
is_building_story: Some(true),
..Default::default()
};
let roof = Level {
name: Some("Roof".into()),
elevation_feet: Some(30.0),
is_building_story: Some(true),
..Default::default()
};
let drafting_ref = Level {
name: Some("Drafting Ref".into()),
elevation_feet: Some(8.0),
is_building_story: Some(false),
..Default::default()
};
let unnamed = Level {
name: None,
elevation_feet: Some(20.0),
is_building_story: Some(true),
..Default::default()
};
let storeys = storeys_from_levels(&[l1, roof, drafting_ref, unnamed]);
assert_eq!(storeys.len(), 2);
assert_eq!(storeys[0].name, "Level 1");
assert_eq!(storeys[1].name, "Roof");
assert_eq!(storeys[1].elevation_feet, 30.0);
}
#[test]
fn storeys_threaded_through_to_step_output() {
let wall = mk_decoded("Wall");
let opts = BuilderOptions {
storeys: vec![
super::super::Storey {
name: "Ground Floor".into(),
elevation_feet: 0.0,
},
super::super::Storey {
name: "Second Floor".into(),
elevation_feet: 10.0,
},
super::super::Storey {
name: "Roof Deck".into(),
elevation_feet: 20.0,
},
],
..Default::default()
};
let inputs = vec![ElementInput {
decoded: &wall,
display_name: "W-1".into(),
guid: None,
storey_index: None,
material_index: None,
property_set: None,
location_feet: None,
rotation_radians: None,
extrusion: None,
host_element_index: None,
material_layer_set_index: None,
material_profile_set_index: None,
solid_shape: None,
representation_map_index: None,
}];
let model = build_ifc_model(&inputs, opts);
let s = super::super::write_step(&model);
assert_eq!(s.matches("IFCBUILDINGSTOREY(").count(), 3);
assert!(s.contains("Ground Floor"));
assert!(s.contains("Second Floor"));
assert!(s.contains("Roof Deck"));
assert!(s.contains("3.048"), "second-floor elevation missing");
assert_eq!(s.matches("IFCRELAGGREGATES(").count(), 3);
}
#[test]
fn empty_storeys_still_emits_one_placeholder() {
let model = build_ifc_model(&[], BuilderOptions::default());
let s = super::super::write_step(&model);
assert_eq!(s.matches("IFCBUILDINGSTOREY(").count(), 1);
assert!(s.contains("Level 1"));
}
#[test]
fn built_model_round_trips_through_step_writer() {
let wall = mk_decoded("Wall");
let door = mk_decoded("Door");
let inputs = vec![
ElementInput {
decoded: &wall,
display_name: "North Wall".into(),
guid: None,
storey_index: None,
material_index: None,
property_set: None,
location_feet: None,
rotation_radians: None,
extrusion: None,
host_element_index: None,
material_layer_set_index: None,
material_profile_set_index: None,
solid_shape: None,
representation_map_index: None,
},
ElementInput {
decoded: &door,
display_name: "Front Door".into(),
guid: Some("DOOR-GUID-42".into()),
storey_index: None,
material_index: None,
property_set: None,
location_feet: None,
rotation_radians: None,
extrusion: None,
host_element_index: None,
material_layer_set_index: None,
material_profile_set_index: None,
solid_shape: None,
representation_map_index: None,
},
];
let model = build_ifc_model(&inputs, BuilderOptions::default());
let step = super::super::write_step(&model);
assert!(step.contains("IFCWALL("));
assert!(step.contains("IFCDOOR("));
assert!(step.contains("North Wall"));
assert!(step.contains("Front Door"));
assert!(step.contains("DOOR-GUID-42"));
assert_eq!(
step.matches("IFCRELCONTAINEDINSPATIALSTRUCTURE(").count(),
1
);
}
#[test]
fn wall_segment_length_cardinal_directions() {
assert!((wall_segment_length_feet([0.0, 0.0], [10.0, 0.0]) - 10.0).abs() < 1e-9);
assert!((wall_segment_length_feet([0.0, 0.0], [0.0, 7.5]) - 7.5).abs() < 1e-9);
assert!((wall_segment_length_feet([0.0, 0.0], [3.0, 4.0]) - 5.0).abs() < 1e-9);
}
#[test]
fn wall_segment_length_reverse_direction_matches() {
let fwd = wall_segment_length_feet([0.0, 0.0], [3.0, 4.0]);
let rev = wall_segment_length_feet([3.0, 4.0], [0.0, 0.0]);
assert!((fwd - rev).abs() < 1e-12);
}
#[test]
fn wall_segment_angle_cardinal_directions() {
use std::f64::consts::{FRAC_PI_2, PI};
assert!(wall_segment_angle_radians([0.0, 0.0], [1.0, 0.0]).abs() < 1e-9);
assert!((wall_segment_angle_radians([0.0, 0.0], [0.0, 1.0]) - FRAC_PI_2).abs() < 1e-9);
assert!((wall_segment_angle_radians([0.0, 0.0], [-1.0, 0.0]).abs() - PI).abs() < 1e-9);
assert!((wall_segment_angle_radians([0.0, 0.0], [0.0, -1.0]) + FRAC_PI_2).abs() < 1e-9);
}
#[test]
fn wall_extrusion_from_location_line_computes_length() {
use crate::elements::wall::{Wall, WallType};
let wall = Wall {
unconnected_height_feet: Some(10.0),
..Default::default()
};
let wall_type = WallType {
width_feet: Some(8.0 / 12.0),
..Default::default()
};
let ex =
wall_extrusion_from_location_line(&wall, Some(&wall_type), [0.0, 0.0], [20.0, 0.0]);
assert!((ex.width_feet - 20.0).abs() < 1e-9);
assert!((ex.depth_feet - 8.0 / 12.0).abs() < 1e-9);
assert!((ex.height_feet - 10.0).abs() < 1e-9);
}
#[test]
fn wall_layered_extrusions_preserves_order_and_thickness() {
use crate::elements::wall::Wall;
let wall = Wall {
unconnected_height_feet: Some(9.0),
..Default::default()
};
let layers = [0.25, 0.5, 0.125]; let out =
wall_layered_extrusions_from_location_line(&wall, [0.0, 0.0], [10.0, 0.0], &layers);
assert_eq!(out.len(), 3);
for (idx, ex) in out.iter().enumerate() {
assert!((ex.width_feet - 10.0).abs() < 1e-9);
assert!((ex.depth_feet - layers[idx]).abs() < 1e-9);
assert!((ex.height_feet - 9.0).abs() < 1e-9);
}
}
#[test]
fn wall_layered_extrusions_skips_zero_thickness_layers() {
use crate::elements::wall::Wall;
let wall = Wall::default();
let layers = [0.25, 0.0, 0.125, -1.0]; let out =
wall_layered_extrusions_from_location_line(&wall, [0.0, 0.0], [1.0, 0.0], &layers);
assert_eq!(out.len(), 2);
assert!((out[0].depth_feet - 0.25).abs() < 1e-9);
assert!((out[1].depth_feet - 0.125).abs() < 1e-9);
}
#[test]
fn wall_layered_extrusions_empty_input_returns_empty() {
use crate::elements::wall::Wall;
let wall = Wall::default();
let out = wall_layered_extrusions_from_location_line(&wall, [0.0, 0.0], [5.0, 0.0], &[]);
assert!(out.is_empty());
}
#[test]
fn polygon_area_unit_square_is_one_sqft() {
let sq = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)];
assert!((polygon_area_sqft(&sq) - 1.0).abs() < 1e-9);
}
#[test]
fn polygon_area_is_winding_order_invariant() {
let ccw = [(0.0, 0.0), (10.0, 0.0), (10.0, 5.0), (0.0, 5.0)];
let cw = [(0.0, 0.0), (0.0, 5.0), (10.0, 5.0), (10.0, 0.0)];
let a_ccw = polygon_area_sqft(&ccw);
let a_cw = polygon_area_sqft(&cw);
assert!((a_ccw - 50.0).abs() < 1e-9);
assert!((a_cw - 50.0).abs() < 1e-9);
}
#[test]
fn polygon_area_l_shape() {
let ell = [
(0.0, 0.0),
(2.0, 0.0),
(2.0, 1.0),
(1.0, 1.0),
(1.0, 2.0),
(0.0, 2.0),
];
assert!((polygon_area_sqft(&ell) - 3.0).abs() < 1e-9);
}
#[test]
fn polygon_area_degenerate_returns_zero() {
assert_eq!(polygon_area_sqft(&[]), 0.0);
assert_eq!(polygon_area_sqft(&[(0.0, 0.0)]), 0.0);
assert_eq!(polygon_area_sqft(&[(0.0, 0.0), (1.0, 1.0)]), 0.0);
}
#[test]
fn polygon_perimeter_unit_square_is_four_feet() {
let sq = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)];
assert!((polygon_perimeter_feet(&sq) - 4.0).abs() < 1e-9);
}
#[test]
fn polygon_perimeter_rectangle_includes_closing_edge() {
let rect = [(0.0, 0.0), (10.0, 0.0), (10.0, 3.0), (0.0, 3.0)];
assert!((polygon_perimeter_feet(&rect) - 26.0).abs() < 1e-9);
}
#[test]
fn polygon_perimeter_degenerate_returns_zero() {
assert_eq!(polygon_perimeter_feet(&[]), 0.0);
assert_eq!(polygon_perimeter_feet(&[(0.0, 0.0)]), 0.0);
}
#[test]
fn floor_extrusion_from_boundary_uses_polygon_profile() {
use crate::elements::floor::FloorType;
let ft = FloorType {
thickness_feet: Some(0.5),
..Default::default()
};
let boundary = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
let ex = floor_extrusion_from_boundary(&boundary, Some(&ft));
assert!((ex.height_feet - 0.5).abs() < 1e-9);
assert!((ex.width_feet - 10.0).abs() < 1e-9);
assert!((ex.depth_feet - 10.0).abs() < 1e-9);
match ex.profile_override {
Some(crate::ifc::entities::ProfileDef::ArbitraryClosed { points }) => {
assert_eq!(points.len(), 4);
}
other => panic!("expected ArbitraryClosed profile, got {:?}", other),
}
}
#[test]
fn floor_extrusion_falls_back_for_degenerate_boundary() {
let ex = floor_extrusion_from_boundary(&[], None);
assert_eq!(ex.width_feet, 1.0);
assert_eq!(ex.depth_feet, 1.0);
assert!((ex.height_feet - 1.0).abs() < 1e-9); assert!(ex.profile_override.is_none());
}
#[test]
fn floor_base_quantities_populates_four_properties() {
use crate::elements::floor::FloorType;
let ft = FloorType {
thickness_feet: Some(0.5),
..Default::default()
};
let boundary = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
let qto = floor_base_quantities(&boundary, Some(&ft));
assert_eq!(qto.name, "Qto_SlabBaseQuantities");
assert_eq!(qto.properties.len(), 4);
let names: Vec<&str> = qto.properties.iter().map(|p| p.name.as_str()).collect();
assert!(names.contains(&"GrossArea"));
assert!(names.contains(&"Perimeter"));
assert!(names.contains(&"Depth"));
assert!(names.contains(&"GrossVolume"));
}
#[test]
fn floor_base_quantities_empty_for_degenerate_boundary() {
let qto = floor_base_quantities(&[], None);
assert_eq!(qto.name, "Qto_SlabBaseQuantities");
assert!(qto.properties.is_empty());
}
#[test]
fn roof_pitch_6_in_12_is_about_26_degrees() {
let rad = roof_pitch_radians_from_rise_run(6.0, 12.0);
let deg = rad.to_degrees();
assert!(
(deg - 26.565).abs() < 0.01,
"6:12 pitch should be ~26.57°, got {deg}"
);
}
#[test]
fn roof_pitch_12_in_12_is_45_degrees() {
let rad = roof_pitch_radians_from_rise_run(12.0, 12.0);
assert!((rad.to_degrees() - 45.0).abs() < 1e-9);
}
#[test]
fn roof_pitch_zero_run_returns_zero() {
assert_eq!(roof_pitch_radians_from_rise_run(5.0, 0.0), 0.0);
assert_eq!(roof_pitch_radians_from_rise_run(5.0, -1.0), 0.0);
}
#[test]
fn roof_pitch_from_degrees_matches() {
let deg = 30.0;
let rad = roof_pitch_radians_from_degrees(deg);
assert!((rad - std::f64::consts::FRAC_PI_6).abs() < 1e-9);
}
#[test]
fn gabled_ridge_height_at_45_degrees() {
let h = gabled_roof_ridge_height(20.0, std::f64::consts::FRAC_PI_4);
assert!((h - 10.0).abs() < 1e-9);
}
#[test]
fn gabled_ridge_height_flat_is_zero() {
assert_eq!(gabled_roof_ridge_height(20.0, 0.0), 0.0);
}
#[test]
fn gabled_ridge_negative_inputs_clamp_to_zero() {
assert_eq!(gabled_roof_ridge_height(-5.0, 1.0), 0.0);
assert_eq!(gabled_roof_ridge_height(5.0, -1.0), 0.0);
}
#[test]
fn hip_roof_ridge_length_rectangle_is_diff() {
assert!((hip_roof_ridge_length(40.0, 20.0) - 20.0).abs() < 1e-9);
assert!((hip_roof_ridge_length(20.0, 40.0) - 20.0).abs() < 1e-9); }
#[test]
fn hip_roof_pyramid_has_zero_ridge() {
assert_eq!(hip_roof_ridge_length(20.0, 20.0), 0.0);
}
#[test]
fn gabled_extrusion_emits_triangular_profile() {
let ex = gabled_roof_extrusion(30.0, 20.0, std::f64::consts::FRAC_PI_4, None);
match ex.profile_override {
Some(crate::ifc::entities::ProfileDef::ArbitraryClosed { points }) => {
assert_eq!(points.len(), 3);
assert!((points[0].1 - 0.0).abs() < 1e-9);
assert!((points[1].1 - 0.0).abs() < 1e-9);
assert!(points[2].1 > 0.0);
}
other => panic!("expected triangular profile, got {:?}", other),
}
assert!((ex.height_feet - 30.0).abs() < 1e-9);
}
#[test]
fn gabled_extrusion_flat_falls_back_to_slab() {
let ex = gabled_roof_extrusion(30.0, 20.0, 0.0, None);
assert!(ex.profile_override.is_none()); assert!((ex.width_feet - 30.0).abs() < 1e-9);
assert!((ex.depth_feet - 20.0).abs() < 1e-9);
}
#[test]
fn hip_brep_has_six_vertices_six_triangles() {
let (v, t) = hip_roof_brep(40.0, 20.0, std::f64::consts::FRAC_PI_4);
assert_eq!(v.len(), 6);
assert_eq!(t.len(), 6); for crate::ifc::entities::BrepTriangle(a, b, c) in &t {
assert!((*a as usize) < v.len());
assert!((*b as usize) < v.len());
assert!((*c as usize) < v.len());
}
}
#[test]
fn hip_brep_pyramid_still_produces_faces() {
let (v, t) = hip_roof_brep(20.0, 20.0, std::f64::consts::FRAC_PI_6);
assert_eq!(v.len(), 6);
assert_eq!(t.len(), 6);
assert!((v[4][0] - v[5][0]).abs() < 1e-9);
assert!((v[4][1] - v[5][1]).abs() < 1e-9);
assert!((v[4][2] - v[5][2]).abs() < 1e-9);
}
#[test]
fn hip_brep_swapped_axes_when_width_larger() {
let (v, _) = hip_roof_brep(20.0, 40.0, std::f64::consts::FRAC_PI_4);
assert!((v[4][0] - 10.0).abs() < 1e-9);
assert!(v[4][1] > 0.0 && v[4][1] < 40.0);
}
#[test]
fn window_extrusion_matches_requested_dimensions() {
let ex = window_extrusion(3.0, 4.0, 0.5);
assert!((ex.width_feet - 3.0).abs() < 1e-9);
assert!((ex.depth_feet - 0.5).abs() < 1e-9);
assert!((ex.height_feet - 4.0).abs() < 1e-9);
assert!(ex.profile_override.is_none());
}
#[test]
fn window_extrusion_clamps_negative_to_zero() {
let ex = window_extrusion(-1.0, -2.0, -0.5);
assert_eq!(ex.width_feet, 0.0);
assert_eq!(ex.depth_feet, 0.0);
assert_eq!(ex.height_feet, 0.0);
}
#[test]
fn window_opening_inflates_width_and_height_but_not_depth() {
let void = window_opening_extrusion(3.0, 4.0, 0.5, 0.1);
assert!((void.width_feet - 3.2).abs() < 1e-9); assert!((void.depth_feet - 0.5).abs() < 1e-9); assert!((void.height_feet - 4.2).abs() < 1e-9); }
#[test]
fn window_opening_zero_margin_matches_window_size() {
let win = window_extrusion(3.0, 4.0, 0.5);
let void = window_opening_extrusion(3.0, 4.0, 0.5, 0.0);
assert!((win.width_feet - void.width_feet).abs() < 1e-9);
assert!((win.height_feet - void.height_feet).abs() < 1e-9);
assert!((win.depth_feet - void.depth_feet).abs() < 1e-9);
}
#[test]
fn door_extrusion_mirrors_window_extrusion() {
let door = door_extrusion(3.0, 7.0, 0.5);
let win = window_extrusion(3.0, 7.0, 0.5);
assert_eq!(door.width_feet, win.width_feet);
assert_eq!(door.height_feet, win.height_feet);
assert_eq!(door.depth_feet, win.depth_feet);
}
#[test]
fn door_opening_mirrors_window_opening() {
let door_void = door_opening_extrusion(3.0, 7.0, 0.5, 0.05);
let win_void = window_opening_extrusion(3.0, 7.0, 0.5, 0.05);
assert_eq!(door_void.width_feet, win_void.width_feet);
assert_eq!(door_void.height_feet, win_void.height_feet);
assert_eq!(door_void.depth_feet, win_void.depth_feet);
}
#[test]
fn window_placement_z_adds_sill_to_level_elevation() {
assert!((window_placement_z_feet(10.0, 3.0) - 13.0).abs() < 1e-9);
assert_eq!(window_placement_z_feet(10.0, 0.0), 10.0);
}
#[test]
fn window_placement_z_negative_sill_clamps_to_level() {
assert_eq!(window_placement_z_feet(10.0, -5.0), 10.0);
}
#[test]
fn window_dimensions_property_set_merges_geometry_with_decoded_props() {
use crate::elements::openings::Window;
let window = Window {
sill_height_feet: Some(3.0),
rotation_radians: Some(std::f64::consts::FRAC_PI_2),
..Default::default()
};
let pset = window_dimensions_property_set(&window, 3.0, 4.0, 0.5);
assert_eq!(pset.name, "Pset_WindowCommon");
let names: Vec<&str> = pset.properties.iter().map(|p| p.name.as_str()).collect();
assert!(names.contains(&"SillHeight"));
assert!(names.contains(&"Rotation"));
assert!(names.contains(&"OverallWidth"));
assert!(names.contains(&"OverallHeight"));
assert!(names.contains(&"Depth"));
}
#[test]
fn window_dimensions_property_set_skips_zero_dimensions() {
use crate::elements::openings::Window;
let window = Window::default();
let pset = window_dimensions_property_set(&window, 0.0, 4.0, 0.0);
let names: Vec<&str> = pset.properties.iter().map(|p| p.name.as_str()).collect();
assert!(!names.contains(&"OverallWidth"));
assert!(names.contains(&"OverallHeight"));
assert!(!names.contains(&"Depth"));
}
#[test]
fn door_dimensions_property_set_merges_geometry() {
use crate::elements::openings::Door;
let door = Door {
flip_hand: Some(true),
..Default::default()
};
let pset = door_dimensions_property_set(&door, 3.0, 7.0, 0.5);
assert_eq!(pset.name, "Pset_DoorCommon");
let names: Vec<&str> = pset.properties.iter().map(|p| p.name.as_str()).collect();
assert!(names.contains(&"FlipHand"));
assert!(names.contains(&"OverallWidth"));
assert!(names.contains(&"OverallHeight"));
assert!(names.contains(&"Depth"));
}
#[test]
fn stair_pitch_standard_7_11_is_about_32_degrees() {
let rad = stair_pitch_radians(7.0 / 12.0, 11.0 / 12.0);
assert!(
(rad.to_degrees() - 32.47).abs() < 0.1,
"7/11 pitch should be ~32.47°, got {}",
rad.to_degrees()
);
}
#[test]
fn stair_pitch_zero_tread_is_90_degrees() {
assert!((stair_pitch_radians(1.0, 0.0) - std::f64::consts::FRAC_PI_2).abs() < 1e-9);
}
#[test]
fn stair_run_length_is_n_minus_one_treads() {
assert!((stair_run_length_feet(16, 1.0) - 15.0).abs() < 1e-9);
}
#[test]
fn stair_run_length_zero_riser_is_zero() {
assert_eq!(stair_run_length_feet(0, 1.0), 0.0);
}
#[test]
fn stair_sawtooth_has_right_vertex_count() {
let pts = stair_sawtooth_profile(3, 0.5, 1.0);
assert_eq!(pts.len(), 9);
}
#[test]
fn stair_sawtooth_starts_at_origin_and_rises_to_top() {
let pts = stair_sawtooth_profile(3, 0.5, 1.0);
assert_eq!(pts[0], (0.0, 0.0));
assert_eq!(pts[1], (0.0, 1.5)); }
#[test]
fn stair_sawtooth_empty_for_zero_risers() {
assert!(stair_sawtooth_profile(0, 0.5, 1.0).is_empty());
}
#[test]
fn stair_run_extrusion_uses_arbitrary_closed_profile() {
use crate::elements::circulation::Stair;
let stair = Stair {
actual_riser_count: Some(10),
actual_riser_height_feet: Some(0.5833),
actual_tread_depth_feet: Some(0.9167),
..Default::default()
};
let ex = stair_run_extrusion(&stair, 4.0);
match ex.profile_override {
Some(crate::ifc::entities::ProfileDef::ArbitraryClosed { points }) => {
assert!(points.len() >= 2 + 1 + 2 * 10);
}
other => panic!("expected sawtooth profile, got {:?}", other),
}
assert!((ex.height_feet - 4.0).abs() < 1e-9); }
#[test]
fn stair_run_extrusion_falls_back_for_missing_dimensions() {
use crate::elements::circulation::Stair;
let stair = Stair::default();
let ex = stair_run_extrusion(&stair, 4.0);
assert!(ex.profile_override.is_none());
assert!(ex.width_feet >= 1.0);
assert!(ex.depth_feet >= 1.0);
assert!(ex.height_feet >= 1.0);
}
#[test]
fn stair_tread_extrusion_matches_dimensions() {
let ex = stair_tread_extrusion(1.0, 4.0, 0.1);
assert!((ex.width_feet - 1.0).abs() < 1e-9);
assert!((ex.depth_feet - 4.0).abs() < 1e-9);
assert!((ex.height_feet - 0.1).abs() < 1e-9);
}
#[test]
fn stair_landing_extrusion_matches_dimensions() {
let ex = stair_landing_extrusion(5.0, 4.0, 0.5);
assert!((ex.width_feet - 5.0).abs() < 1e-9);
assert!((ex.depth_feet - 4.0).abs() < 1e-9);
assert!((ex.height_feet - 0.5).abs() < 1e-9);
}
#[test]
fn column_height_combines_level_diff_and_offsets() {
use crate::elements::structural::Column;
let col = Column {
base_offset_feet: Some(1.0),
top_offset_feet: Some(-0.5),
..Default::default()
};
let h = column_height_from_levels(&col, Some(0.0), Some(10.0));
assert!((h.unwrap() - 8.5).abs() < 1e-9);
}
#[test]
fn column_height_none_when_level_missing() {
use crate::elements::structural::Column;
let col = Column::default();
assert!(column_height_from_levels(&col, None, Some(10.0)).is_none());
assert!(column_height_from_levels(&col, Some(0.0), None).is_none());
}
#[test]
fn column_height_clamps_tiny_or_negative() {
use crate::elements::structural::Column;
let col = Column::default();
let h = column_height_from_levels(&col, Some(10.0), Some(9.9));
assert!(h.unwrap() >= 0.1);
}
#[test]
fn column_i_shape_extrusion_carries_profile() {
let ex = column_i_shape_extrusion(0.75, 1.0, 0.04, 0.06, 12.0);
match ex.profile_override {
Some(crate::ifc::entities::ProfileDef::IShape { .. }) => {}
other => panic!("expected IShape profile, got {:?}", other),
}
assert!((ex.height_feet - 12.0).abs() < 1e-9);
}
#[test]
fn column_circular_extrusion_carries_profile() {
let ex = column_circular_extrusion(0.5, 10.0);
match ex.profile_override {
Some(crate::ifc::entities::ProfileDef::Circle { radius_feet }) => {
assert!((radius_feet - 0.5).abs() < 1e-9);
}
other => panic!("expected Circle profile, got {:?}", other),
}
}
#[test]
fn column_rectangular_hollow_extrusion_carries_profile() {
let ex = column_rectangular_hollow_extrusion(1.0, 1.0, 0.05, 8.0);
match ex.profile_override {
Some(crate::ifc::entities::ProfileDef::RectangleHollow {
wall_thickness_feet,
..
}) => {
assert!((wall_thickness_feet - 0.05).abs() < 1e-9);
}
other => panic!("expected RectangleHollow, got {:?}", other),
}
}
#[test]
fn column_arbitrary_profile_carries_polyline() {
let pts = vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)];
let ex = column_arbitrary_profile_extrusion(pts.clone(), 10.0);
match ex.profile_override {
Some(crate::ifc::entities::ProfileDef::ArbitraryClosed { points }) => {
assert_eq!(points.len(), pts.len());
}
other => panic!("expected ArbitraryClosed, got {:?}", other),
}
}
#[test]
fn column_extrusion_clamps_zero_height() {
let ex = column_circular_extrusion(0.5, 0.0);
assert!(ex.height_feet >= 0.1);
}
#[test]
fn column_property_set_surfaces_all_populated_fields() {
use crate::elements::structural::Column;
let col = Column {
base_offset_feet: Some(0.5),
top_offset_feet: Some(-0.5),
rotation_radians: Some(std::f64::consts::FRAC_PI_4),
is_structural: Some(true),
..Default::default()
};
let pset = column_property_set(&col);
assert_eq!(pset.name, "Pset_ColumnCommon");
let names: Vec<&str> = pset.properties.iter().map(|p| p.name.as_str()).collect();
assert!(names.contains(&"BaseOffset"));
assert!(names.contains(&"TopOffset"));
assert!(names.contains(&"Rotation"));
assert!(names.contains(&"LoadBearing"));
}
#[test]
fn column_property_set_empty_for_unpopulated_column() {
use crate::elements::structural::Column;
let pset = column_property_set(&Column::default());
assert!(pset.properties.is_empty());
}
#[test]
fn beam_length_3d_includes_z_component() {
use crate::geometry::Point3;
let l = beam_length_3d_feet(Point3::new(0.0, 0.0, 0.0), Point3::new(3.0, 4.0, 0.0));
assert!((l - 5.0).abs() < 1e-9); let l2 = beam_length_3d_feet(Point3::new(0.0, 0.0, 0.0), Point3::new(2.0, 2.0, 1.0));
assert!((l2 - 3.0).abs() < 1e-9); }
#[test]
fn beam_axis_yaw_aligns_with_axes() {
use crate::geometry::Point3;
let origin = Point3::new(0.0, 0.0, 0.0);
assert!(beam_axis_yaw_radians(origin, Point3::new(10.0, 0.0, 0.0)).abs() < 1e-9);
assert!(
(beam_axis_yaw_radians(origin, Point3::new(0.0, 10.0, 0.0))
- std::f64::consts::FRAC_PI_2)
.abs()
< 1e-9
);
assert!(
(beam_axis_yaw_radians(origin, Point3::new(-10.0, 0.0, 0.0)) - std::f64::consts::PI)
.abs()
< 1e-9
);
}
#[test]
fn beam_axis_pitch_flat_is_zero() {
use crate::geometry::Point3;
assert!(
beam_axis_pitch_radians(Point3::new(0.0, 0.0, 0.0), Point3::new(10.0, 0.0, 0.0)).abs()
< 1e-9
);
}
#[test]
fn beam_axis_pitch_rising_is_positive() {
use crate::geometry::Point3;
let p = beam_axis_pitch_radians(Point3::new(0.0, 0.0, 0.0), Point3::new(10.0, 0.0, 10.0));
assert!((p - std::f64::consts::FRAC_PI_4).abs() < 1e-9);
}
#[test]
fn beam_axis_pitch_vertical_is_90_degrees() {
use crate::geometry::Point3;
let p = beam_axis_pitch_radians(Point3::new(0.0, 0.0, 0.0), Point3::new(0.0, 0.0, 10.0));
assert!((p - std::f64::consts::FRAC_PI_2).abs() < 1e-9);
}
#[test]
fn beam_axis_pitch_zero_vector_returns_zero() {
use crate::geometry::Point3;
let o = Point3::new(0.0, 0.0, 0.0);
assert_eq!(beam_axis_pitch_radians(o, o), 0.0);
}
#[test]
fn beam_is_horizontal_honours_eps() {
use crate::geometry::Point3;
let start = Point3::new(0.0, 0.0, 0.0);
let slight = Point3::new(10.0, 0.0, 0.001);
assert!(beam_is_horizontal(start, slight, 0.01));
assert!(!beam_is_horizontal(start, slight, 0.0001));
}
#[test]
fn beam_i_shape_extrusion_carries_profile() {
let ex = beam_i_shape_extrusion(0.5, 1.0, 0.04, 0.06, 20.0);
match ex.profile_override {
Some(crate::ifc::entities::ProfileDef::IShape { .. }) => {}
other => panic!("expected IShape profile, got {:?}", other),
}
assert!((ex.height_feet - 20.0).abs() < 1e-9);
}
#[test]
fn beam_rectangular_hollow_carries_wall_thickness() {
let ex = beam_rectangular_hollow_extrusion(1.0, 0.5, 0.05, 15.0);
match ex.profile_override {
Some(crate::ifc::entities::ProfileDef::RectangleHollow {
wall_thickness_feet,
..
}) => {
assert!((wall_thickness_feet - 0.05).abs() < 1e-9);
}
other => panic!("expected RectangleHollow profile, got {:?}", other),
}
}
#[test]
fn beam_arbitrary_profile_preserves_points() {
let pts = vec![(0.0, 0.0), (1.0, 0.0), (0.5, 1.0)];
let ex = beam_arbitrary_profile_extrusion(pts.clone(), 10.0);
match ex.profile_override {
Some(crate::ifc::entities::ProfileDef::ArbitraryClosed { points }) => {
assert_eq!(points.len(), pts.len());
}
other => panic!("expected ArbitraryClosed, got {:?}", other),
}
}
#[test]
fn beam_rectangular_extrusion_has_no_profile_override() {
let ex = beam_rectangular_extrusion(0.5, 0.5, 10.0);
assert!(ex.profile_override.is_none());
}
#[test]
fn beam_property_set_populates_all_fields() {
use crate::elements::structural::Beam;
use crate::geometry::Point3;
let beam = Beam {
start: Some(Point3::new(0.0, 0.0, 0.0)),
end: Some(Point3::new(10.0, 0.0, 0.0)),
start_level_offset_feet: Some(0.5),
end_level_offset_feet: Some(0.5),
cross_section_rotation_radians: Some(std::f64::consts::FRAC_PI_4),
..Default::default()
};
let pset = beam_property_set(&beam);
assert_eq!(pset.name, "Pset_BeamCommon");
let names: Vec<&str> = pset.properties.iter().map(|p| p.name.as_str()).collect();
assert!(names.contains(&"StartLevelOffset"));
assert!(names.contains(&"EndLevelOffset"));
assert!(names.contains(&"CrossSectionRotation"));
assert!(names.contains(&"Span"));
}
#[test]
fn beam_property_set_empty_when_nothing_decoded() {
use crate::elements::structural::Beam;
let pset = beam_property_set(&Beam::default());
assert!(pset.properties.is_empty());
}
}