use ifc_lite_core::{DecodedEntity, EntityDecoder, EntityScanner, IfcType};
use rustc_hash::FxHashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LayerAxis {
Axis1,
Axis2,
Axis3,
}
impl LayerAxis {
pub fn unit_vector(self) -> [f64; 3] {
match self {
LayerAxis::Axis1 => [1.0, 0.0, 0.0],
LayerAxis::Axis2 => [0.0, 1.0, 0.0],
LayerAxis::Axis3 => [0.0, 0.0, 1.0],
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct LayerInfo {
pub material_id: u32,
pub thickness: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub enum LayerBuildup {
Sliceable {
layers: Vec<LayerInfo>,
axis: LayerAxis,
direction_sense: f64,
offset_from_reference_line: f64,
},
NotSliceable,
}
impl LayerBuildup {
pub fn is_sliceable(&self) -> bool {
matches!(self, LayerBuildup::Sliceable { .. })
}
}
#[derive(Debug, Default, Clone, PartialEq)]
pub struct MaterialLayerIndex {
element_to_buildup: FxHashMap<u32, LayerBuildup>,
}
#[derive(Debug, Default, Clone, PartialEq)]
pub struct MaterialLayerFlat {
pub element_ids: Vec<u32>,
pub axis: Vec<u32>,
pub layer_counts: Vec<u32>,
pub direction_sense: Vec<f64>,
pub offset: Vec<f64>,
pub layer_material_ids: Vec<u32>,
pub layer_thicknesses: Vec<f64>,
}
impl MaterialLayerIndex {
pub fn new() -> Self {
Self::default()
}
pub fn from_content<T>(content: &T, decoder: &mut EntityDecoder) -> Self
where
T: AsRef<[u8]> + ?Sized,
{
let content = content.as_ref();
let mut index = Self::new();
let mut scanner = EntityScanner::new(content);
while let Some((id, type_name, start, end)) = scanner.next_entity() {
if type_name != "IFCRELASSOCIATESMATERIAL" {
continue;
}
index.insert_association(id, start, end, decoder);
}
index
}
pub fn from_spans(spans: &[(u32, usize, usize)], decoder: &mut EntityDecoder) -> Self {
let mut index = Self::new();
for &(id, start, end) in spans {
index.insert_association(id, start, end, decoder);
}
index
}
fn insert_association(
&mut self,
id: u32,
start: usize,
end: usize,
decoder: &mut EntityDecoder,
) {
let entity = match decoder.decode_at_with_id(id, start, end) {
Ok(e) => e,
Err(_) => return,
};
let relating_id = match entity.get_ref(5) {
Some(id) => id,
None => return,
};
let related_attr = match entity.get(4) {
Some(a) => a,
None => return,
};
let related_ids: Vec<u32> = match related_attr.as_list() {
Some(list) => list.iter().filter_map(|v| v.as_entity_ref()).collect(),
None => return,
};
if related_ids.is_empty() {
return;
}
let buildup = resolve_buildup(relating_id, decoder);
for obj_id in related_ids {
match self.element_to_buildup.get(&obj_id) {
Some(LayerBuildup::Sliceable { .. }) => continue,
_ => {
self.element_to_buildup.insert(obj_id, buildup.clone());
}
}
}
}
pub fn to_flat(&self) -> MaterialLayerFlat {
let mut flat = MaterialLayerFlat::default();
for (&element_id, buildup) in &self.element_to_buildup {
flat.element_ids.push(element_id);
match buildup {
LayerBuildup::NotSliceable => {
flat.axis.push(0);
flat.layer_counts.push(0);
flat.direction_sense.push(0.0);
flat.offset.push(0.0);
}
LayerBuildup::Sliceable {
layers,
axis,
direction_sense,
offset_from_reference_line,
} => {
flat.axis.push(match axis {
LayerAxis::Axis1 => 1,
LayerAxis::Axis2 => 2,
LayerAxis::Axis3 => 3,
});
flat.layer_counts.push(layers.len() as u32);
flat.direction_sense.push(*direction_sense);
flat.offset.push(*offset_from_reference_line);
for layer in layers {
flat.layer_material_ids.push(layer.material_id);
flat.layer_thicknesses.push(layer.thickness);
}
}
}
}
flat
}
#[allow(clippy::too_many_arguments)]
pub fn from_flat(
element_ids: &[u32],
axis: &[u32],
layer_counts: &[u32],
direction_sense: &[f64],
offset: &[f64],
layer_material_ids: &[u32],
layer_thicknesses: &[f64],
) -> Self {
let mut index = Self::new();
let n = element_ids.len();
if axis.len() < n
|| layer_counts.len() < n
|| direction_sense.len() < n
|| offset.len() < n
{
return index;
}
let mut cursor = 0usize;
for i in 0..n {
let count = layer_counts[i] as usize;
let buildup = if axis[i] == 0 {
LayerBuildup::NotSliceable
} else {
if cursor + count > layer_material_ids.len()
|| cursor + count > layer_thicknesses.len()
{
return index;
}
let mut layers = Vec::with_capacity(count);
for k in 0..count {
layers.push(LayerInfo {
material_id: layer_material_ids[cursor + k],
thickness: layer_thicknesses[cursor + k],
});
}
LayerBuildup::Sliceable {
layers,
axis: match axis[i] {
1 => LayerAxis::Axis1,
3 => LayerAxis::Axis3,
_ => LayerAxis::Axis2,
},
direction_sense: direction_sense[i],
offset_from_reference_line: offset[i],
}
};
cursor += count;
index.element_to_buildup.insert(element_ids[i], buildup);
}
index
}
pub fn get(&self, element_id: u32) -> Option<&LayerBuildup> {
self.element_to_buildup.get(&element_id)
}
pub fn is_sliceable(&self, element_id: u32) -> bool {
matches!(
self.element_to_buildup.get(&element_id),
Some(LayerBuildup::Sliceable { .. })
)
}
pub fn len(&self) -> usize {
self.element_to_buildup.len()
}
pub fn is_empty(&self) -> bool {
self.element_to_buildup.is_empty()
}
pub fn sliceable_count(&self) -> usize {
self.element_to_buildup
.values()
.filter(|b| b.is_sliceable())
.count()
}
}
fn resolve_buildup(material_select_id: u32, decoder: &mut EntityDecoder) -> LayerBuildup {
let entity = match decoder.decode_by_id(material_select_id) {
Ok(e) => e,
Err(_) => return LayerBuildup::NotSliceable,
};
match entity.ifc_type {
IfcType::IfcMaterialLayerSetUsage => resolve_layer_set_usage(&entity, decoder),
_ => LayerBuildup::NotSliceable,
}
}
fn resolve_layer_set_usage(usage: &DecodedEntity, decoder: &mut EntityDecoder) -> LayerBuildup {
let layer_set_id = match usage.get_ref(0) {
Some(id) => id,
None => return LayerBuildup::NotSliceable,
};
let axis = match usage
.get(1)
.and_then(|a| a.as_enum())
.map(str::to_ascii_uppercase)
{
Some(s) if s == "AXIS1" => LayerAxis::Axis1,
Some(s) if s == "AXIS2" => LayerAxis::Axis2,
Some(s) if s == "AXIS3" => LayerAxis::Axis3,
_ => return LayerBuildup::NotSliceable,
};
let direction_sense = match usage
.get(2)
.and_then(|a| a.as_enum())
.map(str::to_ascii_uppercase)
{
Some(s) if s == "POSITIVE" => 1.0_f64,
Some(s) if s == "NEGATIVE" => -1.0_f64,
_ => return LayerBuildup::NotSliceable,
};
let offset = usage.get_float(3).unwrap_or(0.0);
let layer_set_entity = match decoder.decode_by_id(layer_set_id) {
Ok(e) => e,
Err(_) => return LayerBuildup::NotSliceable,
};
if layer_set_entity.ifc_type != IfcType::IfcMaterialLayerSet {
return LayerBuildup::NotSliceable;
}
let layer_ids: Vec<u32> = match layer_set_entity.get(0).and_then(|a| a.as_list()) {
Some(list) => list.iter().filter_map(|v| v.as_entity_ref()).collect(),
None => return LayerBuildup::NotSliceable,
};
if layer_ids.is_empty() {
return LayerBuildup::NotSliceable;
}
let mut layers = Vec::with_capacity(layer_ids.len());
for layer_id in &layer_ids {
let layer = match decoder.decode_by_id(*layer_id) {
Ok(e) => e,
Err(_) => return LayerBuildup::NotSliceable,
};
if layer.ifc_type != IfcType::IfcMaterialLayer {
return LayerBuildup::NotSliceable;
}
let material_id = layer.get_ref(0).unwrap_or(0);
let thickness = layer.get_float(1).unwrap_or(0.0);
if !thickness.is_finite() || thickness <= 0.0 {
continue;
}
layers.push(LayerInfo {
material_id,
thickness,
});
}
if layers.len() < 2 {
return LayerBuildup::NotSliceable;
}
LayerBuildup::Sliceable {
layers,
axis,
direction_sense,
offset_from_reference_line: offset,
}
}
#[cfg(test)]
mod flat_roundtrip_tests {
use super::*;
fn sample_index() -> MaterialLayerIndex {
let mut index = MaterialLayerIndex::new();
index.element_to_buildup.insert(
100,
LayerBuildup::Sliceable {
layers: vec![
LayerInfo { material_id: 200, thickness: 0.05 },
LayerInfo { material_id: 201, thickness: 0.2 },
LayerInfo { material_id: 200, thickness: 0.05 },
],
axis: LayerAxis::Axis2,
direction_sense: 1.0,
offset_from_reference_line: -0.15,
},
);
index.element_to_buildup.insert(
101,
LayerBuildup::Sliceable {
layers: vec![
LayerInfo { material_id: 0, thickness: 0.1 },
LayerInfo { material_id: 300, thickness: 0.25 },
],
axis: LayerAxis::Axis3,
direction_sense: -1.0,
offset_from_reference_line: 0.0,
},
);
index
.element_to_buildup
.insert(102, LayerBuildup::NotSliceable);
index
}
#[test]
fn to_flat_from_flat_is_identity() {
let index = sample_index();
let flat = index.to_flat();
let restored = MaterialLayerIndex::from_flat(
&flat.element_ids,
&flat.axis,
&flat.layer_counts,
&flat.direction_sense,
&flat.offset,
&flat.layer_material_ids,
&flat.layer_thicknesses,
);
assert_eq!(
index, restored,
"flat round-trip must reproduce the index bit-for-bit"
);
}
#[test]
fn empty_index_round_trips_to_empty() {
let index = MaterialLayerIndex::new();
let flat = index.to_flat();
assert!(flat.element_ids.is_empty());
let restored = MaterialLayerIndex::from_flat(
&flat.element_ids,
&flat.axis,
&flat.layer_counts,
&flat.direction_sense,
&flat.offset,
&flat.layer_material_ids,
&flat.layer_thicknesses,
);
assert_eq!(index, restored);
assert!(restored.is_empty());
}
#[test]
fn from_flat_bails_on_truncated_buffer() {
let restored = MaterialLayerIndex::from_flat(&[100], &[], &[], &[], &[], &[], &[]);
assert!(restored.is_empty());
}
}