use crate::style::{FullIndexedColourMap, GeometryStyleInfo};
use crate::types::mesh::{MeshData, MeshTextureData, RawInstanceOccurrence};
use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcType};
use ifc_lite_geometry::{
calculate_normals, compose_instance_world_row_major, orient_mesh_outward_verdict, BoolFailure,
GeometryHasher, GeometryRouter, Mesh, ResolvedTextureMap, SubMeshCollection,
};
use rustc_hash::{FxHashMap, FxHashSet};
use std::collections::BTreeMap;
use crate::processor::{convert_mesh_to_site_local, get_refs_from_list};
#[path = "element_degenerate.rs"]
mod degenerate;
#[derive(Debug, Clone, Default)]
pub struct ElementMeshMetadata {
pub global_id: Option<String>,
pub name: Option<String>,
pub presentation_layer: Option<String>,
pub space_zone_properties: Option<BTreeMap<String, String>>,
}
#[derive(Debug, Clone)]
pub enum ElementJobKind {
Product,
TypeProduct { rep_maps: Vec<(u32, u8)> },
}
pub struct ElementMeshJob<'a> {
pub id: u32,
pub ifc_type: IfcType,
pub entity: &'a DecodedEntity,
pub kind: ElementJobKind,
pub element_color: Option<[f32; 4]>,
pub metadata: Option<&'a ElementMeshMetadata>,
}
pub struct MeshProductionContext<'a> {
pub void_index: &'a FxHashMap<u32, Vec<u32>>,
pub geometry_style_index: &'a FxHashMap<u32, GeometryStyleInfo>,
pub indexed_colour_full: &'a FxHashMap<u32, FullIndexedColourMap>,
pub element_material_colors: &'a FxHashMap<u32, Vec<[f32; 4]>>,
pub texture_index: &'a FxHashMap<u32, ResolvedTextureMap>,
pub site_local_rotation: Option<&'a Vec<f64>>,
}
#[derive(Debug, Clone, Copy)]
pub struct GeometryHashConfig {
pub tolerance: f64,
pub world_rtc: [f64; 3],
}
#[derive(Debug, Clone, Copy, Default)]
pub struct MeshProductionOptions {
pub geometry_hash: Option<GeometryHashConfig>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TypeGeometryMode {
SuppressInstanced,
EmitTagged,
}
pub fn plan_type_geometry(
rep_map_ids: &[u32],
referenced_representation_maps: &FxHashSet<u32>,
type_is_instantiated: bool,
mode: TypeGeometryMode,
) -> Vec<(u32, u8)> {
if mode == TypeGeometryMode::SuppressInstanced && type_is_instantiated {
return Vec::new();
}
let class: u8 = if type_is_instantiated { 2 } else { 1 };
rep_map_ids
.iter()
.filter(|rm| !referenced_representation_maps.contains(rm))
.map(|rm| (*rm, class))
.collect()
}
pub struct ProducedElementMeshes {
pub meshes: Vec<MeshData>,
pub instance_occurrences: Vec<RawInstanceOccurrence>,
pub geometry_hash: Option<u64>,
pub geometry_aabb: Option<[f64; 6]>,
pub geometry_volume: Option<f64>,
pub geometry_closure: Option<ifc_lite_geometry::GeometryClosure>,
pub csg_failures: FxHashMap<u32, Vec<BoolFailure>>,
pub degenerate_triangles_dropped: u64,
}
pub fn produce_element_meshes(
job: &ElementMeshJob<'_>,
ctx: &MeshProductionContext<'_>,
opts: &MeshProductionOptions,
decoder: &mut EntityDecoder,
router: &GeometryRouter,
) -> ProducedElementMeshes {
ifc_lite_geometry::kernel::budget::begin_element();
degenerate::begin_element();
let mut hasher = match (&job.kind, opts.geometry_hash) {
(ElementJobKind::Product, Some(cfg)) => {
Some(GeometryHasher::new(cfg.tolerance, cfg.world_rtc))
}
_ => None,
};
let (meshes, instance_occurrences) = produce_inner(job, ctx, decoder, router, &mut hasher);
let csg_failures = router.take_csg_failures();
let degenerate_triangles_dropped = degenerate::dropped_this_element();
let (geometry_hash, geometry_aabb, geometry_volume, geometry_closure) = match hasher {
Some(mut h) if !h.is_empty() => {
h.retract_closure_if_mesh_edited(degenerate_triangles_dropped);
(Some(h.finish()), h.world_aabb(), h.volume(), Some(h.closure()))
}
_ => (None, None, None, None),
};
ProducedElementMeshes {
meshes,
instance_occurrences,
geometry_hash,
geometry_aabb,
geometry_volume,
geometry_closure,
csg_failures,
degenerate_triangles_dropped,
}
}
fn produce_inner(
job: &ElementMeshJob<'_>,
ctx: &MeshProductionContext<'_>,
decoder: &mut EntityDecoder,
router: &GeometryRouter,
hasher: &mut Option<GeometryHasher>,
) -> (Vec<MeshData>, Vec<RawInstanceOccurrence>) {
let has_representation = job.entity.get(6).is_some_and(|a| !a.is_null());
if !has_representation && job.ifc_type != IfcType::IfcAlignment {
return (Vec::new(), Vec::new());
}
let element_color = job
.element_color
.unwrap_or_else(|| crate::style::default_color_for_type(job.ifc_type).to_array());
if let ElementJobKind::TypeProduct { rep_maps } = &job.kind {
return (
produce_type_geometry(job, rep_maps, element_color, ctx, decoder, router),
Vec::new(),
);
}
let has_openings = ctx
.void_index
.get(&job.id)
.is_some_and(|openings| !openings.is_empty());
let layer_class = if router.is_material_layer_sliceable(job.id) {
GEOM_CLASS_LAYER_SLICE
} else {
0
};
if has_openings {
if let Ok(sub_meshes) =
router.process_element_with_submeshes_and_voids(job.entity, decoder, ctx.void_index)
{
if !sub_meshes.is_empty() {
let (out, occ) =
emit_sub_meshes(job, sub_meshes, element_color, ctx, decoder, hasher, layer_class);
if !out.is_empty() || !occ.is_empty() {
return (out, occ);
}
}
}
} else {
if let Ok(sub_meshes) =
router.process_element_with_submeshes_textured(job.entity, decoder, ctx.texture_index)
{
if !sub_meshes.is_empty() {
let (out, occ) =
emit_sub_meshes(job, sub_meshes, element_color, ctx, decoder, hasher, layer_class);
if !out.is_empty() || !occ.is_empty() {
return (out, occ);
}
}
}
}
let _ = router.take_csg_failures();
let mut mesh_candidate = router
.process_element_with_voids(job.entity, decoder, ctx.void_index)
.ok();
let needs_fallback = match mesh_candidate.as_ref() {
Some(mesh) => mesh.is_empty() && !router.host_consumed_by_void(job.id),
None => true,
};
if needs_fallback {
mesh_candidate = router.process_element(job.entity, decoder).ok();
}
let Some(mut mesh) = mesh_candidate else {
return (Vec::new(), Vec::new());
};
if mesh.is_empty() {
return (Vec::new(), Vec::new());
}
let verdict = orient_mesh_outward_verdict(&mut mesh);
if verdict.flipped {
calculate_normals(&mut mesh);
}
if !ctx.indexed_colour_full.is_empty() {
if let Some(full) =
find_indexed_colour_for_element(job.entity, ctx.indexed_colour_full, decoder)
{
let geometry_id = full.geometry_id;
if let Some(groups) = crate::style::split_mesh_by_indexed_colour(&mesh, full) {
if let Some(h) = hasher.as_mut() {
h.add_oriented_mesh(&mesh.positions, &mesh.indices, mesh.origin, verdict);
}
let mut out: Vec<MeshData> = Vec::with_capacity(groups.len());
for (color, mut part) in groups {
if part.normals.len() != part.positions.len() {
calculate_normals(&mut part);
}
out.push(build_mesh_data(
job,
part,
color.to_array(),
None,
Some(geometry_id),
0,
ctx,
None,
));
}
if !out.is_empty() {
return (out, Vec::new());
}
}
}
}
if mesh.normals.len() != mesh.positions.len() {
calculate_normals(&mut mesh);
}
if let Some(h) = hasher.as_mut() {
h.add_oriented_mesh(&mesh.positions, &mesh.indices, mesh.origin, verdict);
}
(
vec![build_mesh_data(job, mesh, element_color, None, None, 0, ctx, None)],
Vec::new(),
)
}
fn emit_sub_meshes(
job: &ElementMeshJob<'_>,
sub_meshes: SubMeshCollection,
element_color: [f32; 4],
ctx: &MeshProductionContext<'_>,
decoder: &mut EntityDecoder,
hasher: &mut Option<GeometryHasher>,
slice_class: u8,
) -> (Vec<MeshData>, Vec<RawInstanceOccurrence>) {
let mut out: Vec<MeshData> = Vec::with_capacity(sub_meshes.len());
let mut occurrences: Vec<RawInstanceOccurrence> = Vec::new();
let material_colors = ctx.element_material_colors.get(&job.id);
let mut mat_color_idx = 0usize;
for sub in sub_meshes.sub_meshes {
let mut sub_mesh = sub.mesh;
if sub_mesh.is_empty() {
if let Some(im) = sub_mesh.instance_meta.as_ref().filter(|im| im.instanceable) {
let style = ctx.geometry_style_index.get(&sub.geometry_id);
let direct_color = style.map(|s| s.color).or_else(|| {
find_geometry_item_color(sub.geometry_id, ctx.geometry_style_index, decoder)
});
let color = crate::style::resolve_submesh_color(
direct_color,
material_colors.map(|v| v.as_slice()),
&mut mat_color_idx,
element_color,
);
occurrences.push(RawInstanceOccurrence {
express_id: job.id,
ifc_type: job.ifc_type.name().to_string(),
global_id: job.metadata.and_then(|m| m.global_id.clone()),
name: job.metadata.and_then(|m| m.name.clone()),
presentation_layer: job.metadata.and_then(|m| m.presentation_layer.clone()),
color,
rep_identity: im.rep_identity,
world_transform: compose_instance_world_row_major(im),
});
}
continue;
}
let verdict = orient_mesh_outward_verdict(&mut sub_mesh);
if verdict.flipped || sub_mesh.normals.len() != sub_mesh.positions.len() {
calculate_normals(&mut sub_mesh);
}
let style = ctx.geometry_style_index.get(&sub.geometry_id);
let direct_color = style.map(|s| s.color).or_else(|| {
find_geometry_item_color(sub.geometry_id, ctx.geometry_style_index, decoder)
});
let color = crate::style::resolve_submesh_color(
direct_color,
material_colors.map(|v| v.as_slice()),
&mut mat_color_idx,
element_color,
);
let material_name = style
.and_then(|s| s.material_name.as_ref())
.map(ToString::to_string)
.or_else(|| infer_opening_subpart_material_name(&job.ifc_type, color, sub.geometry_id));
if let Some(h) = hasher.as_mut() {
h.add_oriented_mesh(&sub_mesh.positions, &sub_mesh.indices, sub_mesh.origin, verdict);
}
if let (Some(uvs), Some(texture)) = (sub.uvs, sub.texture.as_ref()) {
if uvs.len() / 2 == sub_mesh.positions.len() / 3 {
let mut mesh_data = build_mesh_data(
job,
sub_mesh,
color,
material_name,
Some(sub.geometry_id),
slice_class,
ctx,
Some(uvs),
);
mesh_data.texture = Some(MeshTextureData::from_attachment(texture));
out.push(mesh_data);
continue;
}
}
if let Some(full) = ctx.indexed_colour_full.get(&sub.geometry_id) {
if let Some(groups) = crate::style::split_mesh_by_indexed_colour(&sub_mesh, full) {
for (rgba, mut part) in groups {
if part.normals.len() != part.positions.len() {
calculate_normals(&mut part);
}
out.push(build_mesh_data(
job,
part,
rgba.to_array(),
None,
Some(sub.geometry_id),
slice_class,
ctx,
None,
));
}
continue;
}
}
out.push(build_mesh_data(
job,
sub_mesh,
color,
material_name,
Some(sub.geometry_id),
slice_class,
ctx,
None,
));
}
(out, occurrences)
}
pub const GEOM_CLASS_LAYER_SLICE: u8 = 3;
fn produce_type_geometry(
job: &ElementMeshJob<'_>,
rep_maps: &[(u32, u8)],
element_color: [f32; 4],
ctx: &MeshProductionContext<'_>,
decoder: &mut EntityDecoder,
router: &GeometryRouter,
) -> Vec<MeshData> {
let mut out: Vec<MeshData> = Vec::new();
for &(rep_map_id, geometry_class) in rep_maps {
let Ok(rep_map) = decoder.decode_by_id(rep_map_id) else {
continue;
};
let Ok(parts) =
router.process_representation_map_with_texture(&rep_map, decoder, ctx.texture_index)
else {
continue;
};
if parts.is_empty() {
continue;
}
let color =
resolve_color_for_representation_map(rep_map_id, ctx.geometry_style_index, decoder)
.unwrap_or(element_color);
for (mut mesh, uvs, texture) in parts {
if mesh.is_empty() {
continue;
}
if mesh.normals.len() != mesh.positions.len() {
calculate_normals(&mut mesh);
}
let part_uvs = if texture.is_some() { Some(uvs) } else { None };
let mut mesh_data =
build_mesh_data(job, mesh, color, None, None, geometry_class, ctx, part_uvs);
if let Some(tex) = texture {
mesh_data.texture = Some(MeshTextureData::from_attachment(&tex));
}
out.push(mesh_data);
}
}
out
}
#[allow(clippy::too_many_arguments)] fn build_mesh_data(
job: &ElementMeshJob<'_>,
mut mesh: Mesh,
color: [f32; 4],
material_name: Option<String>,
geometry_item_id: Option<u32>,
geometry_class: u8,
ctx: &MeshProductionContext<'_>,
uvs: Option<Vec<f32>>,
) -> MeshData {
degenerate::clean(&mut mesh);
let welded_uvs = match ifc_lite_geometry::mesh_weld::weld_indexed(
&mesh.positions,
&mesh.normals,
uvs.as_deref(),
&mesh.indices,
) {
Some((wp, wn, wuv, wi)) => {
mesh.positions = wp;
mesh.normals = wn;
mesh.indices = wi;
wuv
}
None => uvs,
};
let mesh_origin = mesh.origin;
let instance = if ctx.site_local_rotation.is_none() {
mesh.instance_meta.take()
} else {
None
};
let (local_bounds, local_to_world) = if ctx.site_local_rotation.is_none() {
(mesh.local_bounds, mesh.local_to_world)
} else {
(None, None)
};
let mut mesh_data = MeshData::new(
job.id,
job.ifc_type.name().to_string(),
mesh.positions,
mesh.normals,
mesh.indices,
color,
)
.with_origin(mesh_origin)
.with_instance(instance)
.with_local_bounds(local_bounds)
.with_local_to_world(local_to_world);
if let Some(meta) = job.metadata {
mesh_data = mesh_data
.with_element_metadata(
meta.global_id.clone(),
meta.name.clone(),
meta.presentation_layer.clone(),
)
.with_properties(meta.space_zone_properties.clone());
}
if material_name.is_some() || geometry_item_id.is_some() {
mesh_data = mesh_data.with_style_metadata(material_name, geometry_item_id);
}
if geometry_class != 0 {
mesh_data = mesh_data.with_geometry_class(geometry_class);
}
mesh_data.uvs = welded_uvs;
convert_mesh_to_site_local(&mut mesh_data, ctx.site_local_rotation);
mesh_data
}
pub(crate) fn find_geometry_item_color(
geometry_id: u32,
geometry_styles: &FxHashMap<u32, GeometryStyleInfo>,
decoder: &mut EntityDecoder,
) -> Option<[f32; 4]> {
if let Some(style) = geometry_styles.get(&geometry_id) {
return Some(style.color);
}
let geom = decoder.decode_by_id(geometry_id).ok()?;
if geom.ifc_type != IfcType::IfcMappedItem {
return None;
}
let mapping_source_id = geom.get_ref(0)?;
let representation_map = decoder.decode_by_id(mapping_source_id).ok()?;
let mapped_representation_id = representation_map.get_ref(1)?;
let mapped_representation = decoder.decode_by_id(mapped_representation_id).ok()?;
let items = get_refs_from_list(&mapped_representation, 3)?;
for underlying in items {
if let Some(color) = find_geometry_item_color(underlying, geometry_styles, decoder) {
return Some(color);
}
}
None
}
pub(crate) fn resolve_color_for_representation_map(
rep_map_id: u32,
geometry_style_index: &FxHashMap<u32, GeometryStyleInfo>,
decoder: &mut EntityDecoder,
) -> Option<[f32; 4]> {
let rep_map = decoder.decode_by_id(rep_map_id).ok()?;
let mapped_rep_id = rep_map.get_ref(1)?;
let mapped_rep = decoder.decode_by_id(mapped_rep_id).ok()?;
let item_ids = get_refs_from_list(&mapped_rep, 3)?;
for item_id in item_ids {
if let Some(style) = geometry_style_index.get(&item_id) {
return Some(style.color);
}
if let Some(color) = find_geometry_item_color(item_id, geometry_style_index, decoder) {
return Some(color);
}
}
None
}
pub(crate) fn find_indexed_colour_for_element<'a>(
entity: &DecodedEntity,
indexed_colour_full: &'a FxHashMap<u32, FullIndexedColourMap>,
decoder: &mut EntityDecoder,
) -> Option<&'a FullIndexedColourMap> {
let pds_id = entity.get_ref(6)?;
let pds = decoder.decode_by_id(pds_id).ok()?;
let repr_ids = get_refs_from_list(&pds, 2)?;
for repr_id in repr_ids {
if let Ok(repr) = decoder.decode_by_id(repr_id) {
if let Some(items) = get_refs_from_list(&repr, 3) {
for item_id in items {
if let Some(full) = indexed_colour_full.get(&item_id) {
return Some(full);
}
}
}
}
}
None
}
fn is_opening_with_subparts(ifc_type: &IfcType) -> bool {
matches!(ifc_type, IfcType::IfcWindow | IfcType::IfcDoor)
}
pub(crate) fn infer_opening_subpart_material_name(
ifc_type: &IfcType,
color: [f32; 4],
geometry_id: u32,
) -> Option<String> {
if !is_opening_with_subparts(ifc_type) {
return None;
}
let prefix = match ifc_type {
IfcType::IfcDoor => "Door",
_ => "Window",
};
if color[3] <= 0.65 {
return Some(format!("{}_Glass", prefix));
}
Some(format!("{}_Frame_{}", prefix, geometry_id))
}
#[cfg(test)]
#[path = "element_tests.rs"]
mod tests;