use serde::{Deserialize, Serialize};
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum IfcEntity {
Project {
name: Option<String>,
description: Option<String>,
long_name: Option<String>,
},
BuildingElementType {
ifc_type: String,
name: String,
description: Option<String>,
},
BuildingElement {
ifc_type: String,
name: String,
type_guid: Option<String>,
#[serde(default)]
predefined_type: Option<String>,
#[serde(default)]
storey_index: Option<usize>,
#[serde(default)]
material_index: Option<usize>,
#[serde(default)]
property_set: Option<PropertySet>,
#[serde(default)]
location_feet: Option<[f64; 3]>,
#[serde(default)]
rotation_radians: Option<f64>,
#[serde(default)]
extrusion: Option<Extrusion>,
#[serde(default)]
host_element_index: Option<usize>,
#[serde(default)]
material_layer_set_index: Option<usize>,
#[serde(default)]
material_profile_set_index: Option<usize>,
#[serde(default)]
solid_shape: Option<SolidShape>,
#[serde(default)]
representation_map_index: Option<usize>,
},
TypeObject {
name: String,
shape_representations: Vec<String>,
},
Aggregate { whole: usize, parts: Vec<usize> },
ElementPropertySet { element: usize, set: PropertySet },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Classification {
pub source: ClassificationSource,
pub edition: Option<String>,
pub items: Vec<ClassificationItem>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ClassificationSource {
OmniClass,
Uniformat,
Other(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClassificationItem {
pub code: String,
pub name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Extrusion {
pub width_feet: f64,
pub depth_feet: f64,
pub height_feet: f64,
#[serde(default)]
pub profile_override: Option<ProfileDef>,
}
impl Extrusion {
pub fn rectangle(width_feet: f64, depth_feet: f64, height_feet: f64) -> Self {
Self {
width_feet,
depth_feet,
height_feet,
profile_override: None,
}
}
pub fn circle(radius_feet: f64, height_feet: f64) -> Self {
let diameter = radius_feet * 2.0;
Self {
width_feet: diameter,
depth_feet: diameter,
height_feet,
profile_override: Some(ProfileDef::Circle { radius_feet }),
}
}
pub fn i_shape(
overall_width_feet: f64,
overall_depth_feet: f64,
web_thickness_feet: f64,
flange_thickness_feet: f64,
height_feet: f64,
) -> Self {
Self {
width_feet: overall_width_feet,
depth_feet: overall_depth_feet,
height_feet,
profile_override: Some(ProfileDef::IShape {
overall_width_feet,
overall_depth_feet,
web_thickness_feet,
flange_thickness_feet,
}),
}
}
pub fn t_shape(
overall_depth_feet: f64,
flange_width_feet: f64,
web_thickness_feet: f64,
flange_thickness_feet: f64,
height_feet: f64,
) -> Self {
Self {
width_feet: flange_width_feet,
depth_feet: overall_depth_feet,
height_feet,
profile_override: Some(ProfileDef::TShape {
overall_depth_feet,
flange_width_feet,
web_thickness_feet,
flange_thickness_feet,
}),
}
}
pub fn l_shape(
overall_depth_feet: f64,
overall_width_feet: f64,
thickness_feet: f64,
height_feet: f64,
) -> Self {
Self {
width_feet: overall_width_feet,
depth_feet: overall_depth_feet,
height_feet,
profile_override: Some(ProfileDef::LShape {
overall_depth_feet,
overall_width_feet,
thickness_feet,
}),
}
}
pub fn u_shape(
overall_depth_feet: f64,
flange_width_feet: f64,
web_thickness_feet: f64,
flange_thickness_feet: f64,
height_feet: f64,
) -> Self {
Self {
width_feet: flange_width_feet,
depth_feet: overall_depth_feet,
height_feet,
profile_override: Some(ProfileDef::UShape {
overall_depth_feet,
flange_width_feet,
web_thickness_feet,
flange_thickness_feet,
}),
}
}
pub fn rectangle_hollow(
overall_width_feet: f64,
overall_depth_feet: f64,
wall_thickness_feet: f64,
height_feet: f64,
) -> Self {
Self {
width_feet: overall_width_feet,
depth_feet: overall_depth_feet,
height_feet,
profile_override: Some(ProfileDef::RectangleHollow {
overall_width_feet,
overall_depth_feet,
wall_thickness_feet,
}),
}
}
pub fn circle_hollow(radius_feet: f64, wall_thickness_feet: f64, height_feet: f64) -> Self {
let diameter = radius_feet * 2.0;
Self {
width_feet: diameter,
depth_feet: diameter,
height_feet,
profile_override: Some(ProfileDef::CircleHollow {
radius_feet,
wall_thickness_feet,
}),
}
}
pub fn arbitrary_closed(points: Vec<(f64, f64)>, height_feet: f64) -> Self {
let (min_x, max_x, min_y, max_y) = points.iter().fold(
(
f64::INFINITY,
f64::NEG_INFINITY,
f64::INFINITY,
f64::NEG_INFINITY,
),
|(mn_x, mx_x, mn_y, mx_y), (x, y)| {
(mn_x.min(*x), mx_x.max(*x), mn_y.min(*y), mx_y.max(*y))
},
);
let width = (max_x - min_x).max(0.0);
let depth = (max_y - min_y).max(0.0);
Self {
width_feet: width,
depth_feet: depth,
height_feet,
profile_override: Some(ProfileDef::ArbitraryClosed { points }),
}
}
pub fn arbitrary_with_voids(
points: Vec<(f64, f64)>,
voids: Vec<Vec<(f64, f64)>>,
height_feet: f64,
) -> Self {
let mut base = Self::arbitrary_closed(points, height_feet);
if let Some(ProfileDef::ArbitraryClosed { points }) = base.profile_override.take() {
base.profile_override = Some(ProfileDef::ArbitraryWithVoids { points, voids });
}
base
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum IfcBooleanOp {
Union,
Difference,
Intersection,
}
impl IfcBooleanOp {
pub fn as_step_keyword(self) -> &'static str {
match self {
IfcBooleanOp::Union => ".UNION.",
IfcBooleanOp::Difference => ".DIFFERENCE.",
IfcBooleanOp::Intersection => ".INTERSECTION.",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct BrepTriangle(pub u32, pub u32, pub u32);
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SolidShape {
RevolvedArea {
profile: ProfileDef,
axis_origin_feet: [f64; 3],
axis_direction: [f64; 3],
angle_radians: f64,
},
BooleanResult {
op: IfcBooleanOp,
operand_a: Box<SolidShape>,
operand_b: Box<SolidShape>,
},
FacetedBrep {
vertices_feet: Vec<[f64; 3]>,
triangles: Vec<BrepTriangle>,
},
ExtrudedArea(Extrusion),
SweptPath {
profile: ProfileDef,
directrix_points_feet: Vec<[f64; 3]>,
fixed_reference: [f64; 3],
},
PlacedExtrusion {
profile: ProfileDef,
origin_feet: [f64; 3],
axis: [f64; 3],
ref_direction: [f64; 3],
depth_feet: f64,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ProfileDef {
Rectangle { width_feet: f64, depth_feet: f64 },
Circle { radius_feet: f64 },
IShape {
overall_width_feet: f64,
overall_depth_feet: f64,
web_thickness_feet: f64,
flange_thickness_feet: f64,
},
TShape {
overall_depth_feet: f64,
flange_width_feet: f64,
web_thickness_feet: f64,
flange_thickness_feet: f64,
},
LShape {
overall_depth_feet: f64,
overall_width_feet: f64,
thickness_feet: f64,
},
UShape {
overall_depth_feet: f64,
flange_width_feet: f64,
web_thickness_feet: f64,
flange_thickness_feet: f64,
},
RectangleHollow {
overall_width_feet: f64,
overall_depth_feet: f64,
wall_thickness_feet: f64,
},
CircleHollow {
radius_feet: f64,
wall_thickness_feet: f64,
},
ArbitraryClosed { points: Vec<(f64, f64)> },
ArbitraryWithVoids {
points: Vec<(f64, f64)>,
voids: Vec<Vec<(f64, f64)>>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MaterialLayer {
pub material_index: Option<usize>,
pub thickness_feet: f64,
#[serde(default)]
pub name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MaterialLayerSet {
pub name: String,
pub layers: Vec<MaterialLayer>,
#[serde(default)]
pub description: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum LayerSetDirection {
Axis1,
Axis2,
Axis3,
}
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct OpeningCut {
pub x_dim_feet: f64,
pub y_dim_feet: f64,
pub centre_feet: [f64; 2],
#[serde(default)]
pub z_range_feet: Option<[f64; 2]>,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct MaterialConstituentSet {
pub material_indices: Vec<usize>,
#[serde(default)]
pub names: Vec<String>,
pub elements: Vec<usize>,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct MaterialLayerSetUsage {
pub direction: LayerSetDirection,
pub positive: bool,
pub offset_feet: f64,
}
impl MaterialLayerSet {
pub fn total_thickness_feet(&self) -> f64 {
self.layers.iter().map(|l| l.thickness_feet).sum()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MaterialProfile {
pub material_index: usize,
pub profile_name: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub profile: Option<ProfileDef>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MaterialProfileSet {
pub name: String,
pub profiles: Vec<MaterialProfile>,
#[serde(default)]
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RepresentationMap {
#[serde(default)]
pub name: Option<String>,
pub shape: SolidShape,
#[serde(default)]
pub origin_feet: [f64; 3],
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UnitAssignment {
pub forge_identifier: String,
pub ifc_mapping: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ForgeUnit {
Millimeters,
Centimeters,
Decimeters,
Meters,
Kilometers,
Inches,
Feet,
Yards,
Miles,
SquareMillimeters,
SquareCentimeters,
SquareMeters,
SquareFeet,
SquareInches,
SquareYards,
Acres,
Hectares,
CubicMillimeters,
CubicCentimeters,
CubicMeters,
CubicFeet,
CubicInches,
CubicYards,
Liters,
UsGallons,
Radians,
Degrees,
Grads,
Kilograms,
Grams,
Pounds,
Seconds,
Minutes,
Hours,
Other(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum IfcUnitType {
Length,
Area,
Volume,
PlaneAngle,
Mass,
Time,
}
impl IfcUnitType {
pub fn as_step_token(self) -> &'static str {
match self {
IfcUnitType::Length => "LENGTHUNIT",
IfcUnitType::Area => "AREAUNIT",
IfcUnitType::Volume => "VOLUMEUNIT",
IfcUnitType::PlaneAngle => "PLANEANGLEUNIT",
IfcUnitType::Mass => "MASSUNIT",
IfcUnitType::Time => "TIMEUNIT",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum IfcUnitEmission {
Si {
unit_type: IfcUnitType,
prefix: Option<&'static str>,
name: &'static str,
},
ConversionBased {
unit_type: IfcUnitType,
derived_name: &'static str,
factor_to_si: f64,
si_base_name: &'static str,
},
}
impl ForgeUnit {
pub fn from_forge_identifier(id: &str) -> Self {
let trimmed = id.trim().to_ascii_lowercase();
let stripped = trimmed
.strip_prefix("autodesk.unit.unit:")
.unwrap_or(&trimmed);
let bare = stripped.split('-').next().unwrap_or(stripped);
match bare {
"millimeters" | "mm" => ForgeUnit::Millimeters,
"centimeters" | "cm" => ForgeUnit::Centimeters,
"decimeters" | "dm" => ForgeUnit::Decimeters,
"meters" | "m" => ForgeUnit::Meters,
"kilometers" | "km" => ForgeUnit::Kilometers,
"inches" | "in" => ForgeUnit::Inches,
"feet" | "ft" | "fractionalinches" | "feetandfractionalinches" => ForgeUnit::Feet,
"yards" | "yd" => ForgeUnit::Yards,
"miles" | "mi" => ForgeUnit::Miles,
"squaremillimeters" => ForgeUnit::SquareMillimeters,
"squarecentimeters" => ForgeUnit::SquareCentimeters,
"squaremeters" => ForgeUnit::SquareMeters,
"squarefeet" => ForgeUnit::SquareFeet,
"squareinches" => ForgeUnit::SquareInches,
"squareyards" => ForgeUnit::SquareYards,
"acres" => ForgeUnit::Acres,
"hectares" => ForgeUnit::Hectares,
"cubicmillimeters" => ForgeUnit::CubicMillimeters,
"cubiccentimeters" => ForgeUnit::CubicCentimeters,
"cubicmeters" => ForgeUnit::CubicMeters,
"cubicfeet" => ForgeUnit::CubicFeet,
"cubicinches" => ForgeUnit::CubicInches,
"cubicyards" => ForgeUnit::CubicYards,
"liters" | "litres" => ForgeUnit::Liters,
"usgallons" | "gallons" => ForgeUnit::UsGallons,
"radians" | "rad" => ForgeUnit::Radians,
"degrees" | "deg" => ForgeUnit::Degrees,
"grads" | "gradians" => ForgeUnit::Grads,
"kilograms" | "kg" => ForgeUnit::Kilograms,
"grams" | "g" => ForgeUnit::Grams,
"pounds" | "lb" | "lbs" | "poundsmass" => ForgeUnit::Pounds,
"seconds" | "s" => ForgeUnit::Seconds,
"minutes" | "min" => ForgeUnit::Minutes,
"hours" | "hr" | "h" => ForgeUnit::Hours,
_ => ForgeUnit::Other(id.to_string()),
}
}
pub fn ifc_emission(&self) -> Option<IfcUnitEmission> {
use IfcUnitType::*;
match self {
ForgeUnit::Millimeters => Some(IfcUnitEmission::Si {
unit_type: Length,
prefix: Some("MILLI"),
name: "METRE",
}),
ForgeUnit::Centimeters => Some(IfcUnitEmission::Si {
unit_type: Length,
prefix: Some("CENTI"),
name: "METRE",
}),
ForgeUnit::Decimeters => Some(IfcUnitEmission::Si {
unit_type: Length,
prefix: Some("DECI"),
name: "METRE",
}),
ForgeUnit::Meters => Some(IfcUnitEmission::Si {
unit_type: Length,
prefix: None,
name: "METRE",
}),
ForgeUnit::Kilometers => Some(IfcUnitEmission::Si {
unit_type: Length,
prefix: Some("KILO"),
name: "METRE",
}),
ForgeUnit::Inches => Some(IfcUnitEmission::ConversionBased {
unit_type: Length,
derived_name: "INCH",
factor_to_si: 0.0254,
si_base_name: "METRE",
}),
ForgeUnit::Feet => Some(IfcUnitEmission::ConversionBased {
unit_type: Length,
derived_name: "FOOT",
factor_to_si: 0.3048,
si_base_name: "METRE",
}),
ForgeUnit::Yards => Some(IfcUnitEmission::ConversionBased {
unit_type: Length,
derived_name: "YARD",
factor_to_si: 0.9144,
si_base_name: "METRE",
}),
ForgeUnit::Miles => Some(IfcUnitEmission::ConversionBased {
unit_type: Length,
derived_name: "MILE",
factor_to_si: 1609.344,
si_base_name: "METRE",
}),
ForgeUnit::SquareMillimeters => Some(IfcUnitEmission::Si {
unit_type: Area,
prefix: Some("MILLI"),
name: "SQUARE_METRE",
}),
ForgeUnit::SquareCentimeters => Some(IfcUnitEmission::Si {
unit_type: Area,
prefix: Some("CENTI"),
name: "SQUARE_METRE",
}),
ForgeUnit::SquareMeters => Some(IfcUnitEmission::Si {
unit_type: Area,
prefix: None,
name: "SQUARE_METRE",
}),
ForgeUnit::SquareFeet => Some(IfcUnitEmission::ConversionBased {
unit_type: Area,
derived_name: "SQUARE_FOOT",
factor_to_si: 0.092_903_04,
si_base_name: "SQUARE_METRE",
}),
ForgeUnit::SquareInches => Some(IfcUnitEmission::ConversionBased {
unit_type: Area,
derived_name: "SQUARE_INCH",
factor_to_si: 0.000_645_16,
si_base_name: "SQUARE_METRE",
}),
ForgeUnit::SquareYards => Some(IfcUnitEmission::ConversionBased {
unit_type: Area,
derived_name: "SQUARE_YARD",
factor_to_si: 0.836_127_36,
si_base_name: "SQUARE_METRE",
}),
ForgeUnit::Acres => Some(IfcUnitEmission::ConversionBased {
unit_type: Area,
derived_name: "ACRE",
factor_to_si: 4_046.856_422_4,
si_base_name: "SQUARE_METRE",
}),
ForgeUnit::Hectares => Some(IfcUnitEmission::ConversionBased {
unit_type: Area,
derived_name: "HECTARE",
factor_to_si: 10_000.0,
si_base_name: "SQUARE_METRE",
}),
ForgeUnit::CubicMillimeters => Some(IfcUnitEmission::Si {
unit_type: Volume,
prefix: Some("MILLI"),
name: "CUBIC_METRE",
}),
ForgeUnit::CubicCentimeters => Some(IfcUnitEmission::Si {
unit_type: Volume,
prefix: Some("CENTI"),
name: "CUBIC_METRE",
}),
ForgeUnit::CubicMeters => Some(IfcUnitEmission::Si {
unit_type: Volume,
prefix: None,
name: "CUBIC_METRE",
}),
ForgeUnit::CubicFeet => Some(IfcUnitEmission::ConversionBased {
unit_type: Volume,
derived_name: "CUBIC_FOOT",
factor_to_si: 0.028_316_846_592,
si_base_name: "CUBIC_METRE",
}),
ForgeUnit::CubicInches => Some(IfcUnitEmission::ConversionBased {
unit_type: Volume,
derived_name: "CUBIC_INCH",
factor_to_si: 0.000_016_387_064,
si_base_name: "CUBIC_METRE",
}),
ForgeUnit::CubicYards => Some(IfcUnitEmission::ConversionBased {
unit_type: Volume,
derived_name: "CUBIC_YARD",
factor_to_si: 0.764_554_857_984,
si_base_name: "CUBIC_METRE",
}),
ForgeUnit::Liters => Some(IfcUnitEmission::ConversionBased {
unit_type: Volume,
derived_name: "LITRE",
factor_to_si: 0.001,
si_base_name: "CUBIC_METRE",
}),
ForgeUnit::UsGallons => Some(IfcUnitEmission::ConversionBased {
unit_type: Volume,
derived_name: "US_GALLON",
factor_to_si: 0.003_785_411_784,
si_base_name: "CUBIC_METRE",
}),
ForgeUnit::Radians => Some(IfcUnitEmission::Si {
unit_type: PlaneAngle,
prefix: None,
name: "RADIAN",
}),
ForgeUnit::Degrees => Some(IfcUnitEmission::ConversionBased {
unit_type: PlaneAngle,
derived_name: "DEGREE",
factor_to_si: std::f64::consts::PI / 180.0,
si_base_name: "RADIAN",
}),
ForgeUnit::Grads => Some(IfcUnitEmission::ConversionBased {
unit_type: PlaneAngle,
derived_name: "GRAD",
factor_to_si: std::f64::consts::PI / 200.0,
si_base_name: "RADIAN",
}),
ForgeUnit::Kilograms => Some(IfcUnitEmission::Si {
unit_type: Mass,
prefix: None,
name: "GRAM",
}),
ForgeUnit::Grams => Some(IfcUnitEmission::Si {
unit_type: Mass,
prefix: Some("MILLI"),
name: "GRAM",
}),
ForgeUnit::Pounds => Some(IfcUnitEmission::ConversionBased {
unit_type: Mass,
derived_name: "POUND",
factor_to_si: 0.453_592_37,
si_base_name: "GRAM",
}),
ForgeUnit::Seconds => Some(IfcUnitEmission::Si {
unit_type: Time,
prefix: None,
name: "SECOND",
}),
ForgeUnit::Minutes => Some(IfcUnitEmission::ConversionBased {
unit_type: Time,
derived_name: "MINUTE",
factor_to_si: 60.0,
si_base_name: "SECOND",
}),
ForgeUnit::Hours => Some(IfcUnitEmission::ConversionBased {
unit_type: Time,
derived_name: "HOUR",
factor_to_si: 3600.0,
si_base_name: "SECOND",
}),
ForgeUnit::Other(_) => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PropertySet {
pub name: String,
pub properties: Vec<Property>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Property {
pub name: String,
pub value: PropertyValue,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", content = "value")]
pub enum PropertyValue {
Text(String),
Integer(i64),
Real(f64),
Boolean(bool),
LengthFeet(f64),
PositiveLengthFeet(f64),
AngleRadians(f64),
AreaSquareFeet(f64),
VolumeCubicFeet(f64),
CountValue(i64),
TimeSeconds(f64),
MassPounds(f64),
}
impl PropertyValue {
pub fn to_step(&self) -> String {
match self {
PropertyValue::Text(s) => format!("IFCTEXT('{}')", escape_step_string(s)),
PropertyValue::Integer(n) => format!("IFCINTEGER({n})"),
PropertyValue::Real(v) => format!("IFCREAL({v:.6})"),
PropertyValue::Boolean(b) => {
format!("IFCBOOLEAN(.{}.)", if *b { "T" } else { "F" })
}
PropertyValue::LengthFeet(ft) => {
let metres = ft * 0.3048;
format!("IFCLENGTHMEASURE({metres:.6})")
}
PropertyValue::PositiveLengthFeet(ft) => {
let metres = ft * 0.3048;
format!("IFCPOSITIVELENGTHMEASURE({metres:.6})")
}
PropertyValue::AngleRadians(r) => format!("IFCPLANEANGLEMEASURE({r:.6})"),
PropertyValue::AreaSquareFeet(sqft) => {
let sqm = sqft * 0.09290304;
format!("IFCAREAMEASURE({sqm:.6})")
}
PropertyValue::VolumeCubicFeet(cuft) => {
let cum = cuft * 0.028316846592;
format!("IFCVOLUMEMEASURE({cum:.6})")
}
PropertyValue::CountValue(n) => format!("IFCCOUNTMEASURE({n})"),
PropertyValue::TimeSeconds(s) => format!("IFCTIMEMEASURE({s:.6})"),
PropertyValue::MassPounds(lb) => {
let kg = lb * 0.45359237;
format!("IFCMASSMEASURE({kg:.6})")
}
}
}
}
fn escape_step_string(s: &str) -> String {
super::step_writer::escape(s)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn property_value_to_step_primitives() {
assert_eq!(PropertyValue::Integer(42).to_step(), "IFCINTEGER(42)");
assert_eq!(PropertyValue::Real(1.25).to_step(), "IFCREAL(1.250000)");
assert_eq!(PropertyValue::Boolean(true).to_step(), "IFCBOOLEAN(.T.)");
assert_eq!(PropertyValue::Boolean(false).to_step(), "IFCBOOLEAN(.F.)");
assert_eq!(
PropertyValue::Text("hello".into()).to_step(),
"IFCTEXT('hello')"
);
assert_eq!(
PropertyValue::Text("it's".into()).to_step(),
"IFCTEXT('it''s')"
);
assert_eq!(
PropertyValue::Text("Genérico - 200 mm".into()).to_step(),
"IFCTEXT('Gen\\X2\\00E9\\X0\\rico - 200 mm')"
);
}
#[test]
fn property_value_to_step_length_and_angle() {
assert_eq!(
PropertyValue::LengthFeet(10.0).to_step(),
"IFCLENGTHMEASURE(3.048000)"
);
let step = PropertyValue::AngleRadians(std::f64::consts::FRAC_PI_2).to_step();
assert!(step.starts_with("IFCPLANEANGLEMEASURE(1.570796"));
}
#[test]
fn material_layer_set_total_thickness() {
let lset = MaterialLayerSet {
name: "Ext - Generic 8\" Wall".into(),
description: None,
layers: vec![
MaterialLayer {
material_index: Some(0),
thickness_feet: 5.0 / 12.0, name: Some("Finish".into()),
},
MaterialLayer {
material_index: Some(1),
thickness_feet: 2.0 / 12.0, name: Some("Structure".into()),
},
MaterialLayer {
material_index: Some(2),
thickness_feet: 1.0 / 12.0, name: Some("Air Gap".into()),
},
],
};
let total = lset.total_thickness_feet();
assert!((total - (8.0 / 12.0)).abs() < 1e-9);
}
#[test]
fn property_value_to_step_quantities() {
assert_eq!(
PropertyValue::AreaSquareFeet(1.0).to_step(),
"IFCAREAMEASURE(0.092903)"
);
assert_eq!(
PropertyValue::AreaSquareFeet(100.0).to_step(),
"IFCAREAMEASURE(9.290304)"
);
assert_eq!(
PropertyValue::VolumeCubicFeet(1.0).to_step(),
"IFCVOLUMEMEASURE(0.028317)"
);
assert_eq!(
PropertyValue::CountValue(12).to_step(),
"IFCCOUNTMEASURE(12)"
);
assert_eq!(
PropertyValue::TimeSeconds(3.0).to_step(),
"IFCTIMEMEASURE(3.000000)"
);
assert_eq!(
PropertyValue::MassPounds(1.0).to_step(),
"IFCMASSMEASURE(0.453592)"
);
}
#[test]
fn forge_unit_parses_canonical_ids() {
assert_eq!(
ForgeUnit::from_forge_identifier("autodesk.unit.unit:millimeters-1.0.1"),
ForgeUnit::Millimeters
);
assert_eq!(
ForgeUnit::from_forge_identifier("autodesk.unit.unit:feet-1.0.1"),
ForgeUnit::Feet
);
assert_eq!(
ForgeUnit::from_forge_identifier("autodesk.unit.unit:degrees-1.0.1"),
ForgeUnit::Degrees
);
}
#[test]
fn forge_unit_parses_without_version() {
assert_eq!(
ForgeUnit::from_forge_identifier("autodesk.unit.unit:meters"),
ForgeUnit::Meters
);
}
#[test]
fn forge_unit_parses_bare_names() {
assert_eq!(
ForgeUnit::from_forge_identifier("millimeters"),
ForgeUnit::Millimeters
);
assert_eq!(ForgeUnit::from_forge_identifier("Feet"), ForgeUnit::Feet);
}
#[test]
fn forge_unit_unknown_falls_through_to_other() {
let fu = ForgeUnit::from_forge_identifier("autodesk.unit.unit:furlongsPerFortnight-1.0.1");
match fu {
ForgeUnit::Other(id) => assert!(id.contains("furlongsPerFortnight")),
_ => panic!("unknown units must map to ForgeUnit::Other(_)"),
}
}
#[test]
fn forge_unit_length_metric_emits_si() {
match ForgeUnit::Millimeters.ifc_emission().unwrap() {
IfcUnitEmission::Si {
unit_type,
prefix,
name,
} => {
assert_eq!(unit_type, IfcUnitType::Length);
assert_eq!(prefix, Some("MILLI"));
assert_eq!(name, "METRE");
}
_ => panic!("millimeters should emit as IfcSIUnit"),
}
}
#[test]
fn forge_unit_feet_emits_conversion_based() {
match ForgeUnit::Feet.ifc_emission().unwrap() {
IfcUnitEmission::ConversionBased {
unit_type,
derived_name,
factor_to_si,
si_base_name,
} => {
assert_eq!(unit_type, IfcUnitType::Length);
assert_eq!(derived_name, "FOOT");
assert!((factor_to_si - 0.3048).abs() < 1e-12);
assert_eq!(si_base_name, "METRE");
}
_ => panic!("feet should emit as IfcConversionBasedUnit"),
}
}
#[test]
fn forge_unit_all_known_variants_have_emission() {
for fu in [
ForgeUnit::Millimeters,
ForgeUnit::Centimeters,
ForgeUnit::Decimeters,
ForgeUnit::Meters,
ForgeUnit::Kilometers,
ForgeUnit::Inches,
ForgeUnit::Feet,
ForgeUnit::Yards,
ForgeUnit::Miles,
ForgeUnit::SquareMillimeters,
ForgeUnit::SquareCentimeters,
ForgeUnit::SquareMeters,
ForgeUnit::SquareFeet,
ForgeUnit::SquareInches,
ForgeUnit::SquareYards,
ForgeUnit::Acres,
ForgeUnit::Hectares,
ForgeUnit::CubicMillimeters,
ForgeUnit::CubicCentimeters,
ForgeUnit::CubicMeters,
ForgeUnit::CubicFeet,
ForgeUnit::CubicInches,
ForgeUnit::CubicYards,
ForgeUnit::Liters,
ForgeUnit::UsGallons,
ForgeUnit::Radians,
ForgeUnit::Degrees,
ForgeUnit::Grads,
ForgeUnit::Kilograms,
ForgeUnit::Grams,
ForgeUnit::Pounds,
ForgeUnit::Seconds,
ForgeUnit::Minutes,
ForgeUnit::Hours,
] {
assert!(
fu.ifc_emission().is_some(),
"ForgeUnit {fu:?} has no ifc_emission mapping"
);
}
assert!(ForgeUnit::Other("x".into()).ifc_emission().is_none());
}
#[test]
fn ifc_unit_type_step_tokens() {
assert_eq!(IfcUnitType::Length.as_step_token(), "LENGTHUNIT");
assert_eq!(IfcUnitType::Area.as_step_token(), "AREAUNIT");
assert_eq!(IfcUnitType::Volume.as_step_token(), "VOLUMEUNIT");
assert_eq!(IfcUnitType::PlaneAngle.as_step_token(), "PLANEANGLEUNIT");
assert_eq!(IfcUnitType::Mass.as_step_token(), "MASSUNIT");
assert_eq!(IfcUnitType::Time.as_step_token(), "TIMEUNIT");
}
}