use bimifc_model::{AttributeValue, DecodedEntity, EntityResolver, IfcType};
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LightFixtureData {
pub id: u64,
pub global_id: Option<String>,
pub name: Option<String>,
pub description: Option<String>,
pub object_type: Option<String>,
pub position: (f64, f64, f64),
pub storey: Option<String>,
pub storey_elevation: Option<f64>,
pub fixture_type: Option<LightFixtureTypeData>,
pub light_sources: Vec<LightSourceData>,
pub properties: FxHashMap<String, PropertySetData>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LightFixtureTypeData {
pub id: u64,
pub name: Option<String>,
pub description: Option<String>,
pub predefined_type: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LightSourceData {
pub id: u64,
pub source_type: String,
pub color_temperature: Option<f64>,
pub luminous_flux: Option<f64>,
pub emission_source: Option<String>,
pub intensity: Option<f64>,
pub color_rgb: Option<(f64, f64, f64)>,
pub distribution: Option<LightDistributionData>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LightDistributionData {
pub distribution_type: String,
pub planes: Vec<DistributionPlane>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DistributionPlane {
pub main_angle: f64,
pub intensities: Vec<(f64, f64)>, }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PropertySetData {
pub name: String,
pub properties: FxHashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LightingExport {
pub schema: String,
pub project_name: Option<String>,
pub building_name: Option<String>,
pub storeys: Vec<StoreyData>,
pub light_fixtures: Vec<LightFixtureData>,
pub light_fixture_types: Vec<LightFixtureTypeData>,
pub summary: LightingSummary,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StoreyData {
pub id: u64,
pub name: String,
pub elevation: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LightingSummary {
pub total_fixtures: usize,
pub total_light_sources: usize,
pub fixtures_per_storey: FxHashMap<String, usize>,
pub fixture_types_used: Vec<String>,
pub total_luminous_flux: Option<f64>,
}
pub fn extract_lighting_data(resolver: &dyn EntityResolver) -> LightingExport {
let mut export = LightingExport {
schema: String::new(),
project_name: None,
building_name: None,
storeys: Vec::new(),
light_fixtures: Vec::new(),
light_fixture_types: Vec::new(),
summary: LightingSummary {
total_fixtures: 0,
total_light_sources: 0,
fixtures_per_storey: FxHashMap::default(),
fixture_types_used: Vec::new(),
total_luminous_flux: None,
},
};
let projects = resolver.entities_by_type(&IfcType::IfcProject);
if let Some(project) = projects.first() {
export.project_name = project.get_string(2).map(|s| s.to_string());
}
let buildings = resolver.entities_by_type(&IfcType::IfcBuilding);
if let Some(building) = buildings.first() {
export.building_name = building.get_string(2).map(|s| s.to_string());
}
let storeys = resolver.entities_by_type(&IfcType::IfcBuildingStorey);
for storey in storeys {
let name = storey
.get_string(2)
.map(|s| s.to_string())
.unwrap_or_default();
let elevation = storey.get_float(9).unwrap_or(0.0);
export.storeys.push(StoreyData {
id: storey.id.0 as u64,
name,
elevation,
});
}
let fixture_types = resolver.entities_by_type(&IfcType::IfcLightFixtureType);
for fixture_type in fixture_types {
let type_data = extract_fixture_type(&fixture_type);
export.light_fixture_types.push(type_data);
}
let fixtures = resolver.entities_by_type(&IfcType::IfcLightFixture);
let mut total_flux: f64 = 0.0;
let mut has_flux = false;
for fixture in fixtures {
let fixture_data = extract_fixture(&fixture, resolver);
if let Some(ref storey) = fixture_data.storey {
*export
.summary
.fixtures_per_storey
.entry(storey.clone())
.or_insert(0) += 1;
}
for source in &fixture_data.light_sources {
if let Some(flux) = source.luminous_flux {
total_flux += flux;
has_flux = true;
}
}
export.summary.total_light_sources += fixture_data.light_sources.len();
export.light_fixtures.push(fixture_data);
}
export.summary.total_fixtures = export.light_fixtures.len();
if has_flux {
export.summary.total_luminous_flux = Some(total_flux);
}
for fixture in &export.light_fixtures {
if let Some(ref ft) = fixture.fixture_type {
if let Some(ref name) = ft.name {
if !export.summary.fixture_types_used.contains(name) {
export.summary.fixture_types_used.push(name.clone());
}
}
}
}
export
}
fn extract_fixture_type(entity: &DecodedEntity) -> LightFixtureTypeData {
LightFixtureTypeData {
id: entity.id.0 as u64,
name: entity.get_string(2).map(|s| s.to_string()),
description: entity.get_string(3).map(|s| s.to_string()),
predefined_type: entity.get_enum(9).map(|s| s.to_string()),
}
}
fn extract_fixture(entity: &DecodedEntity, resolver: &dyn EntityResolver) -> LightFixtureData {
let global_id = entity.get_string(0).map(|s| s.to_string());
let name = entity.get_string(2).map(|s| s.to_string());
let description = entity.get_string(3).map(|s| s.to_string());
let object_type = entity.get_string(4).map(|s| s.to_string());
let position = extract_position(entity, resolver);
let fixture_type = entity.get_ref(5).and_then(|type_ref| {
resolver
.get(type_ref)
.map(|type_entity| extract_fixture_type(&type_entity))
});
let light_sources = extract_light_sources(entity, resolver);
LightFixtureData {
id: entity.id.0 as u64,
global_id,
name,
description,
object_type,
position,
storey: None, storey_elevation: None,
fixture_type,
light_sources,
properties: FxHashMap::default(),
}
}
fn extract_position(entity: &DecodedEntity, resolver: &dyn EntityResolver) -> (f64, f64, f64) {
let placement_ref = match entity.get_ref(5) {
Some(id) => id,
None => return (0.0, 0.0, 0.0),
};
let placement = match resolver.get(placement_ref) {
Some(p) => p,
None => return (0.0, 0.0, 0.0),
};
if placement.ifc_type == IfcType::IfcLocalPlacement {
if let Some(rel_placement_ref) = placement.get_ref(1) {
if let Some(axis_placement) = resolver.get(rel_placement_ref) {
return extract_cartesian_point(&axis_placement, resolver);
}
}
}
(0.0, 0.0, 0.0)
}
fn extract_cartesian_point(
axis_placement: &DecodedEntity,
resolver: &dyn EntityResolver,
) -> (f64, f64, f64) {
let point_ref = match axis_placement.get_ref(0) {
Some(id) => id,
None => return (0.0, 0.0, 0.0),
};
let point = match resolver.get(point_ref) {
Some(p) => p,
None => return (0.0, 0.0, 0.0),
};
if point.ifc_type == IfcType::IfcCartesianPoint {
if let Some(coords) = point.get_list(0) {
let x = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0);
let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
let z = coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
return (x, y, z);
}
}
(0.0, 0.0, 0.0)
}
fn extract_light_sources(
fixture: &DecodedEntity,
resolver: &dyn EntityResolver,
) -> Vec<LightSourceData> {
let fixture_id = fixture.id;
let group_rels = resolver.entities_by_type(&IfcType::IfcRelAssignsToGroup);
for rel in &group_rels {
if let Some(group_ref) = rel.get_ref(6) {
if group_ref == fixture_id {
if let Some(related_list) = rel.get_list(5) {
return related_list
.iter()
.filter_map(|item| {
if let AttributeValue::EntityRef(source_id) = item {
resolver.get(*source_id).and_then(|source| {
if source.ifc_type == IfcType::IfcLightSourceGoniometric {
Some(extract_goniometric_source(&source, resolver))
} else {
None
}
})
} else {
None
}
})
.collect();
}
}
}
}
let mut sources = Vec::new();
if let Some(pds_id) = fixture.get_ref(6) {
if let Some(pds) = resolver.get(pds_id) {
if let Some(rep_refs) = pds.get_refs(2) {
for rep_id in rep_refs {
if let Some(rep) = resolver.get(rep_id) {
find_goniometric_in_items(&rep, resolver, &mut sources);
}
}
}
}
}
if sources.is_empty() {
let type_rels = resolver.entities_by_type(&IfcType::IfcRelDefinesByType);
for rel in &type_rels {
if let Some(related_list) = rel.get_list(4) {
let is_related = related_list
.iter()
.any(|v| v.as_entity_ref() == Some(fixture_id));
if is_related {
if let Some(type_id) = rel.get_ref(5) {
if let Some(type_entity) = resolver.get(type_id) {
if let Some(map_refs) = type_entity.get_refs(6) {
for map_id in map_refs {
if let Some(map) = resolver.get(map_id) {
if let Some(mapped_rep_id) = map.get_ref(1) {
if let Some(mapped_rep) = resolver.get(mapped_rep_id) {
find_goniometric_in_items(
&mapped_rep,
resolver,
&mut sources,
);
}
}
}
}
}
}
}
}
}
}
}
sources
}
fn find_goniometric_in_items(
rep: &DecodedEntity,
resolver: &dyn EntityResolver,
sources: &mut Vec<LightSourceData>,
) {
let item_refs = match rep.get_refs(3) {
Some(refs) => refs,
None => return,
};
for item_id in item_refs {
if let Some(item) = resolver.get(item_id) {
match item.ifc_type {
IfcType::IfcLightSourceGoniometric => {
sources.push(extract_goniometric_source(&item, resolver));
}
IfcType::IfcMappedItem => {
if let Some(map_id) = item.get_ref(0) {
if let Some(map) = resolver.get(map_id) {
if let Some(mapped_rep_id) = map.get_ref(1) {
if let Some(mapped_rep) = resolver.get(mapped_rep_id) {
find_goniometric_in_items(&mapped_rep, resolver, sources);
}
}
}
}
}
_ => {}
}
}
}
}
fn extract_goniometric_source(
entity: &DecodedEntity,
resolver: &dyn EntityResolver,
) -> LightSourceData {
let color_temperature = entity.get_float(6);
let luminous_flux = entity.get_float(7);
let emission_source = entity.get_enum(8).map(|s| s.to_string());
let intensity = entity.get_float(3);
let color_rgb = entity.get_ref(1).and_then(|color_ref| {
resolver.get(color_ref).and_then(|color| {
let r = color.get_float(1)?;
let g = color.get_float(2)?;
let b = color.get_float(3)?;
Some((r, g, b))
})
});
let distribution = entity.get_ref(9).and_then(|dist_ref| {
resolver
.get(dist_ref)
.map(|dist| extract_distribution(&dist, resolver))
});
LightSourceData {
id: entity.id.0 as u64,
source_type: "GONIOMETRIC".to_string(),
color_temperature,
luminous_flux,
emission_source,
intensity,
color_rgb,
distribution,
}
}
fn extract_distribution(
entity: &DecodedEntity,
resolver: &dyn EntityResolver,
) -> LightDistributionData {
let distribution_type = entity
.get_enum(0)
.map(|s| s.to_string())
.unwrap_or_else(|| "TYPE_C".to_string());
let mut planes = Vec::new();
if let Some(data_list) = entity.get_list(1) {
for data_item in data_list {
if let AttributeValue::EntityRef(data_ref) = data_item {
if let Some(data_entity) = resolver.get(*data_ref) {
let main_angle = data_entity.get_float(0).unwrap_or(0.0);
let mut intensities = Vec::new();
if let (Some(angles), Some(values)) =
(data_entity.get_list(1), data_entity.get_list(2))
{
for (angle, value) in angles.iter().zip(values.iter()) {
let a = angle.as_float().unwrap_or(0.0);
let v = value.as_float().unwrap_or(0.0);
intensities.push((a, v));
}
}
planes.push(DistributionPlane {
main_angle,
intensities,
});
}
}
}
}
LightDistributionData {
distribution_type,
planes,
}
}
pub fn light_source_to_eulumdat(source: &LightSourceData) -> Option<eulumdat::Eulumdat> {
let dist = source.distribution.as_ref()?;
if dist.planes.is_empty() {
return None;
}
let c_angles: Vec<f64> = dist.planes.iter().map(|p| p.main_angle).collect();
let g_angles: Vec<f64> = dist.planes[0].intensities.iter().map(|(a, _)| *a).collect();
let flux = source.luminous_flux.unwrap_or(0.0);
let temp = source.color_temperature.unwrap_or(0.0);
let emitter = source.emission_source.clone().unwrap_or_default();
let intensities: Vec<Vec<f64>> = dist
.planes
.iter()
.map(|p| p.intensities.iter().map(|(_, v)| *v).collect())
.collect();
Some(build_eulumdat(
String::new(),
&c_angles,
&g_angles,
flux,
temp,
&emitter,
&intensities,
))
}
pub fn light_source_to_ldt(source: &LightSourceData) -> Option<String> {
light_source_to_eulumdat(source).map(|ldt| ldt.to_ldt())
}
pub fn goniometric_to_eulumdat(src: &bimifc_model::GoniometricData) -> eulumdat::Eulumdat {
let c_angles: Vec<f64> = src.planes.iter().map(|p| p.c_angle).collect();
let g_angles: Vec<f64> = if src.planes.is_empty() {
Vec::new()
} else {
src.planes[0].gamma_angles.clone()
};
let intensities: Vec<Vec<f64>> = src.planes.iter().map(|p| p.intensities.clone()).collect();
build_eulumdat(
src.name.clone(),
&c_angles,
&g_angles,
src.luminous_flux,
src.colour_temperature,
&src.emitter_type,
&intensities,
)
}
pub fn goniometric_to_ldt(src: &bimifc_model::GoniometricData) -> String {
goniometric_to_eulumdat(src).to_ldt()
}
fn build_eulumdat(
name: String,
c_angles: &[f64],
g_angles: &[f64],
luminous_flux: f64,
colour_temperature: f64,
emitter_type: &str,
intensities_cd: &[Vec<f64>],
) -> eulumdat::Eulumdat {
use eulumdat::{Eulumdat, LampSet, Symmetry};
let mut ldt = Eulumdat::new();
ldt.luminaire_name = name;
ldt.c_angles = c_angles.to_vec();
ldt.g_angles = g_angles.to_vec();
ldt.num_c_planes = ldt.c_angles.len();
ldt.num_g_planes = ldt.g_angles.len();
if ldt.num_c_planes > 1 {
ldt.c_plane_distance = ldt.c_angles[1] - ldt.c_angles[0];
}
if ldt.num_g_planes > 1 {
ldt.g_plane_distance = ldt.g_angles[1] - ldt.g_angles[0];
}
let max_c = ldt.c_angles.last().copied().unwrap_or(0.0);
ldt.symmetry = if max_c <= 1.0 {
Symmetry::VerticalAxis
} else if max_c <= 91.0 {
Symmetry::BothPlanes
} else if max_c <= 181.0 {
Symmetry::PlaneC0C180
} else {
Symmetry::None
};
let flux_factor = if luminous_flux > 0.0 {
luminous_flux / 1000.0
} else {
1.0
};
ldt.intensities = intensities_cd
.iter()
.map(|plane| plane.iter().map(|&v| v / flux_factor).collect())
.collect();
let cct_str = if colour_temperature > 0.0 {
format!("{:.0}K", colour_temperature)
} else {
String::new()
};
ldt.lamp_sets.push(LampSet {
num_lamps: 1,
lamp_type: emitter_type.to_string(),
total_luminous_flux: luminous_flux,
color_appearance: cct_str,
color_rendering_group: String::new(),
wattage_with_ballast: 0.0,
});
ldt.conversion_factor = flux_factor;
ldt
}
pub fn export_to_json(export: &LightingExport) -> String {
serde_json::to_string_pretty(export).unwrap_or_else(|_| "{}".to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use bimifc_model::IfcParser;
#[test]
fn test_light_fixture_data_serialization() {
let fixture = LightFixtureData {
id: 123,
global_id: Some("abc-def".to_string()),
name: Some("Test Fixture".to_string()),
description: None,
object_type: None,
position: (1.0, 2.0, 3.0),
storey: Some("Ground Floor".to_string()),
storey_elevation: Some(0.0),
fixture_type: None,
light_sources: vec![],
properties: FxHashMap::default(),
};
let json = serde_json::to_string(&fixture).unwrap();
assert!(json.contains("Test Fixture"));
assert!(json.contains("123"));
}
#[test]
fn test_gldf_light_sources_per_fixture() {
let gldf_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.join("ifc/gldf-exported.ifc");
if !gldf_path.exists() {
return;
}
let content = std::fs::read_to_string(&gldf_path).unwrap();
let parser = crate::StepParser::new();
let model = parser.parse(&content).unwrap();
let export = extract_lighting_data(model.resolver());
assert_eq!(export.light_fixtures.len(), 3);
for fixture in &export.light_fixtures {
eprintln!(
"Fixture #{} '{}': {} sources",
fixture.id,
fixture.name.as_deref().unwrap_or("?"),
fixture.light_sources.len()
);
for s in &fixture.light_sources {
eprintln!(
" Source #{}: flux={:?} dist={}",
s.id,
s.luminous_flux,
s.distribution
.as_ref()
.map(|d| format!("{} planes", d.planes.len()))
.unwrap_or_else(|| "none".to_string())
);
}
assert_eq!(
fixture.light_sources.len(),
2,
"Fixture '{}' (#{}) should have 2 sources, got {}",
fixture.name.as_deref().unwrap_or("?"),
fixture.id,
fixture.light_sources.len()
);
}
assert_eq!(export.summary.total_light_sources, 6);
}
#[test]
fn test_relux_light_sources_via_representation() {
let relux_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.join("ifc/relux-test.ifc");
if !relux_path.exists() {
return;
}
let content = std::fs::read_to_string(&relux_path).unwrap();
let parser = crate::StepParser::new();
let model = parser.parse(&content).unwrap();
let export = extract_lighting_data(model.resolver());
eprintln!("Relux fixtures: {}", export.light_fixtures.len());
for fixture in &export.light_fixtures {
eprintln!(
"Fixture #{} '{}': {} sources",
fixture.id,
fixture.name.as_deref().unwrap_or("?"),
fixture.light_sources.len()
);
for s in &fixture.light_sources {
eprintln!(
" Source #{}: flux={:?} dist={}",
s.id,
s.luminous_flux,
s.distribution
.as_ref()
.map(|d| format!("{} ({} planes)", d.distribution_type, d.planes.len()))
.unwrap_or_else(|| "none".to_string())
);
}
}
assert!(
!export.light_fixtures.is_empty(),
"Expected light fixtures in relux-test.ifc"
);
let total_sources: usize = export
.light_fixtures
.iter()
.map(|f| f.light_sources.len())
.sum();
assert!(
total_sources > 0,
"Expected light sources in relux-test.ifc fixtures"
);
}
}