use ifc_lite_core::IfcType;
mod indexed_colour;
mod material;
mod surface;
pub use indexed_colour::{
resolve_indexed_colour_map_full, split_mesh_by_indexed_colour, FullIndexedColourMap,
};
pub use material::{
build_element_material_colors, build_material_style_index, flatten_material_color_index,
pick_material_style_for_submesh, pick_opaque_first, resolve_material_ids,
resolve_submesh_color,
};
pub use surface::extract_surface_style_colors;
pub const TRANSPARENCY_ALPHA_THRESHOLD: f32 = 0.95;
#[derive(Debug, Clone)]
pub struct GeometryStyleInfo {
pub color: [f32; 4],
pub shading_color: Option<[f32; 4]>,
pub material_name: Option<String>,
}
impl GeometryStyleInfo {
pub fn from_color(color: [f32; 4]) -> Self {
Self {
color,
shading_color: None,
material_name: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(transparent)]
pub struct Rgba(pub [f32; 4]);
impl Rgba {
pub const fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
Rgba([r, g, b, a])
}
pub const fn from_array(c: [f32; 4]) -> Self {
Rgba(c)
}
pub const fn to_array(self) -> [f32; 4] {
self.0
}
pub const fn alpha(self) -> f32 {
self.0[3]
}
pub fn is_transparent(self) -> bool {
self.0[3] < TRANSPARENCY_ALPHA_THRESHOLD
}
pub fn to_rgba8(self) -> [u8; 4] {
let q = |c: f32| (c.clamp(0.0, 1.0) * 255.0).round() as u8;
[q(self.0[0]), q(self.0[1]), q(self.0[2]), q(self.0[3])]
}
pub fn from_rgba8(c: [u8; 4]) -> Self {
Rgba([
c[0] as f32 / 255.0,
c[1] as f32 / 255.0,
c[2] as f32 / 255.0,
c[3] as f32 / 255.0,
])
}
}
impl From<[f32; 4]> for Rgba {
fn from(c: [f32; 4]) -> Self {
Rgba(c)
}
}
impl From<Rgba> for [f32; 4] {
fn from(c: Rgba) -> Self {
c.0
}
}
pub fn default_color_for_type(ifc_type: IfcType) -> Rgba {
match ifc_type {
IfcType::IfcWall | IfcType::IfcWallStandardCase => Rgba::new(0.85, 0.85, 0.85, 1.0),
IfcType::IfcSlab => Rgba::new(0.7, 0.7, 0.7, 1.0),
IfcType::IfcRoof => Rgba::new(0.6, 0.5, 0.4, 1.0),
IfcType::IfcColumn | IfcType::IfcBeam | IfcType::IfcMember => Rgba::new(0.6, 0.65, 0.7, 1.0),
IfcType::IfcWindow => Rgba::new(0.6, 0.8, 1.0, 0.4),
IfcType::IfcDoor => Rgba::new(0.6, 0.45, 0.3, 1.0),
IfcType::IfcStair | IfcType::IfcStairFlight => Rgba::new(0.75, 0.75, 0.75, 1.0),
IfcType::IfcRailing => Rgba::new(0.4, 0.4, 0.45, 1.0),
IfcType::IfcPlate | IfcType::IfcCovering => Rgba::new(0.8, 0.8, 0.8, 1.0),
IfcType::IfcCurtainWall => Rgba::new(0.5, 0.7, 0.9, 0.5),
IfcType::IfcFurnishingElement => Rgba::new(0.7, 0.55, 0.4, 1.0),
IfcType::IfcSpace => Rgba::new(0.2, 0.85, 1.0, 0.3),
IfcType::IfcSpatialZone => Rgba::new(0.72, 0.35, 0.95, 0.28),
IfcType::IfcOpeningElement => Rgba::new(1.0, 0.42, 0.29, 0.4),
IfcType::IfcSite => Rgba::new(0.4, 0.8, 0.3, 1.0),
IfcType::IfcBuildingElementProxy => Rgba::new(0.6, 0.6, 0.6, 1.0),
_ => Rgba::new(0.8, 0.8, 0.8, 1.0),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rgba_array_round_trip() {
let c = Rgba::new(0.1, 0.2, 0.3, 0.4);
assert_eq!(c.to_array(), [0.1, 0.2, 0.3, 0.4]);
assert_eq!(Rgba::from_array([0.1, 0.2, 0.3, 0.4]), c);
let back: [f32; 4] = c.into();
assert_eq!(back, [0.1, 0.2, 0.3, 0.4]);
assert_eq!(Rgba::from([0.5, 0.6, 0.7, 0.8]).alpha(), 0.8);
}
#[test]
fn quantization_clamps_and_rounds() {
assert_eq!(Rgba::new(0.0, 1.0, 0.5, 1.0).to_rgba8(), [0, 255, 128, 255]);
assert_eq!(Rgba::new(-0.5, 1.5, 0.0, 2.0).to_rgba8(), [0, 255, 0, 255]);
}
#[test]
fn quantization_within_one_step() {
for raw in [0.0_f32, 0.123, 0.4, 0.42, 0.5, 0.555, 0.95, 1.0] {
let back = Rgba::from_rgba8(Rgba::new(raw, raw, raw, raw).to_rgba8()).to_array();
assert!((back[0] - raw).abs() <= 1.0 / 255.0, "drift too large for {raw}");
}
}
#[test]
fn transparency_threshold() {
assert!(Rgba::new(0.6, 0.8, 1.0, 0.4).is_transparent());
assert!(!Rgba::new(0.85, 0.85, 0.85, 1.0).is_transparent());
assert!(!Rgba::new(0.0, 0.0, 0.0, TRANSPARENCY_ALPHA_THRESHOLD).is_transparent());
}
#[test]
fn defaults_cover_known_types() {
assert_eq!(
default_color_for_type(IfcType::IfcWall).to_array(),
[0.85, 0.85, 0.85, 1.0]
);
assert!(default_color_for_type(IfcType::IfcWindow).is_transparent());
assert_eq!(
default_color_for_type(IfcType::IfcProject).to_array(),
[0.8, 0.8, 0.8, 1.0]
);
}
#[test]
fn union_resolves_the_four_contested_types() {
assert_eq!(
default_color_for_type(IfcType::IfcCurtainWall).to_array(),
[0.5, 0.7, 0.9, 0.5],
"curtain wall = wasm glass blue"
);
assert_eq!(
default_color_for_type(IfcType::IfcStairFlight).to_array(),
[0.75, 0.75, 0.75, 1.0],
"stair flight = processing gray (grouped with IfcStair)"
);
assert_eq!(
default_color_for_type(IfcType::IfcBuildingElementProxy).to_array(),
[0.6, 0.6, 0.6, 1.0],
"proxy = processing gray"
);
assert_eq!(
default_color_for_type(IfcType::IfcFurnishingElement).to_array(),
[0.7, 0.55, 0.4, 1.0],
"furnishing = wasm light wood, not processing's darker brown"
);
assert_eq!(
default_color_for_type(IfcType::IfcStair),
default_color_for_type(IfcType::IfcStairFlight)
);
}
}