ifc-lite-processing 15.1.0

Shared IFC processing pipeline and types used by server and FFI
Documentation
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//! Private evaluated-occurrence normalization; publication is one appearance plan.
use std::collections::BTreeMap;
use super::{evaluated_source, source::Source, *};
use ifc_lite_core::{AttributeValue as A, DecodedEntity};
use rustc_hash::FxHashMap;
use std::sync::Arc;

struct Conversion {
    plan: AppearancePlan,
    binding: AppearanceConversion,
    styled_id: u32,
}
pub(super) struct Normalized {
    request: AppearanceRequest,
    conversions: Vec<Conversion>,
    exclusions: Vec<Exclusion>,
    start: u32,
}
fn list(ids: &[u32]) -> A { A::List(ids.iter().copied().map(A::EntityRef).collect()) }
// Authored, fixed-depth schema values only; never recursively walk source data.
fn wire(value: &A) -> Value {
    match value {
        A::EntityRef(id) => reference(*id), A::Null => Value::Null,
        A::String(s) => json!(s), A::Enum(s) => json!(format!(".{s}.")),
        A::Integer(n) => json!(n), A::Float(n) => json!(n),
        A::List(items) => json!(items.iter().map(wire).collect::<Vec<_>>()),
        _ => unreachable!("fixed authored values"),
    }
}
pub(super) fn authored(plan: &mut AppearancePlan, entities: &mut FxHashMap<u32, Arc<DecodedEntity>>,
    ty: IfcType, attributes: Vec<A>) -> u32 {
    let id = add(plan,ty.name(),attributes.iter().map(wire).collect());
    entities.insert(id,Arc::new(DecodedEntity::new(id,ty,attributes))); id
}

/// Everything one occurrence needs before any private row is authored.
struct Candidate {
    body: DecodedEntity,
    points: Vec<[f64; 3]>,
    mesh: crate::types::mesh::MeshData,
    old_item: u32,
    surface: Vec<u32>,
    old_items: Vec<u32>,
    opening_edits: Vec<DecodedEntity>,
    layers: Vec<DecodedEntity>,
    rounding_bounds: Option<Vec<f64>>,
    removed: Vec<crate::types::mesh::MeshData>,
    fingerprint: String,
    partition: Option<super::evaluated_mask::Partition>,
}
/// One IfcTriangulatedFaceSet over the shared point list, styled like the source.
fn face_set(plan: &mut AppearancePlan, entities: &mut FxHashMap<u32, Arc<DecodedEntity>>,
    coordinates: u32, corners: &[u32], surface: &[u32]) -> (u32, u32) {
    let indices=A::List(corners.chunks_exact(3).map(|tri|A::List(tri.iter().map(|&i|A::Integer(i64::from(i)+1)).collect())).collect());
    let item=authored(plan,entities,IfcType::IfcTriangulatedFaceSet,
        vec![A::EntityRef(coordinates),A::Null,A::Null,indices,A::Null]);
    let styled=authored(plan,entities,IfcType::IfcStyledItem,vec![A::EntityRef(item),list(surface),A::Null]);
    (item,styled)
}

pub(super) fn prepare(bytes: &[u8], request: &AppearanceRequest, source: &mut Source<'_>) -> Result<Normalized,String> {
    if !matches!(request.schema.as_str(),"IFC4"|"IFC4X3") || request.product_ids.is_empty()
        || request.product_ids.len()>10_000 || request.source_revision.len()>4096 {
        return Err("Evaluated appearance requires a bounded IFC4/IFC4X3 scope".into());
    }
    validate_image_uri(&request.image_uri)?;
    mapping::validate(&request.mapping)?;
    let masks=super::evaluated_mask::validate(request)?;
    texture_budget::preflight(source)?;
    if request.next_express_id <= source.types.last_key_value().map_or(0,|(id,_)|*id) {
        return Err("Stale allocator watermark overlaps effective IFC source".into());
    }
    let mut styles = page_source::appearance(bytes,source);
    let textures = ifc_lite_geometry::build_texture_index(bytes,&mut source.decoder);
    let mut consumers=BTreeMap::<u32,BTreeSet<u32>>::new();
    for (&host,openings) in &styles.void_index {
        for &opening in openings {consumers.entry(opening).or_default().insert(host);}
    }
    let exclusive:BTreeSet<_>=consumers.into_iter().filter_map(|(id,hosts)|(hosts.len()==1).then_some(id)).collect();
    let mut normalized = Normalized { request:request.clone(), conversions:Vec::new(), exclusions:Vec::new(), start:request.next_express_id };
    normalized.request.representation_policy=RepresentationPolicy::Preserve;
    normalized.request.product_ids.clear();
    normalized.request.face_masks.clear();
    let mut budget=budget::PlanBudget::default();
    let mut seen=BTreeSet::new();
    for &product_id in &request.product_ids {
        if !seen.insert(product_id) { continue; }
        if source.product_items(product_id).is_ok() {
            if masks.contains_key(&product_id) {
                normalized.exclusions.push(Exclusion {product_id,reason:super::evaluated_mask::DIRECT_BODY.into()}); continue;
            }
            normalized.request.product_ids.push(product_id); continue;
        }
        let candidate=(|| {
            let opening_edits=super::evaluated_openings::prepare(source,product_id,
                styles.void_index.get(&product_id).map_or(&[],Vec::as_slice),&exclusive)?;
            if matches!(request.mapping,Mapping::ExistingUv {..}) { return Err("Evaluated occurrence conversion requires a new planar or box mapping".into()); }
            let (product, body)=evaluated_source::body(source,product_id)?;
            let layers=super::evaluated_replacement::layers(source,body.id)?;
            // One explicit local-frame evaluation supplies both the authored
            // replacement and its placement-independent fingerprint (#4550).
            // Re-evaluating the same CSG surface solely for identity would
            // double the opt-in appearance planner's dominant work.
            let mut meshes=canonical::produce_evaluated(source,product_id,&textures,Some(&styles))?;
            if meshes.len()!=1 { return Err("Evaluated appearance currently requires one unambiguous source surface".into()); }
            let mesh=meshes.remove(0);
            if mesh.texture.is_some() || mesh.uvs.is_some() { return Err("Evaluated conversion of textured source surfaces is not supported yet".into()); }
            let old_item=mesh.geometry_item_id.ok_or("Missing evaluated source item provenance")?;
            evaluated_source::validate_style_tree(source,&body,old_item)?;
            let surface=evaluated_source::surface_styles(source,old_item)?;
            if mesh.positions.len()%3!=0 || mesh.normals.len()!=mesh.positions.len() || mesh.indices.len()%3!=0 || mesh.positions.is_empty()
                || mesh.indices.is_empty() || mesh.positions.iter().chain(&mesh.normals).chain(&mesh.color).any(|n|!n.is_finite())
                || mesh.origin.iter().any(|n|!n.is_finite())
                || mesh.indices.iter().any(|&i|i as usize>=mesh.positions.len()/3) {
                return Err("Canonical source geometry is invalid".into());
            }
            budget.reserve(mesh.positions.len()/3,mesh.indices.len()/3,0)?;
            let removed=super::evaluated_openings::removed_meshes(source,
                styles.void_index.get(&product_id).map_or(&[],Vec::as_slice),
                &opening_edits,&textures,&styles,&mut budget)?;
            let points=evaluated_source::local_points(source,&product,&mesh)?;
            // The explicit local frame and the normal target frame can round
            // equivalent f64 world corners through different f32 values. Use
            // the existing per-corner cast bound for that representation-only
            // difference, with or without opening edits.
            let rounding_bounds=Some(super::evaluated_precision::local_cast_bounds(
                &points,source.decoder.length_unit_scale())?);
            let fingerprint=super::evaluated_mask::fingerprint(product.get_string(0).ok_or("Product has no GlobalId")?,
                &points,source.decoder.length_unit_scale(),&mesh.indices)?;
            let partition=masks.get(&product_id).map(|mask|super::evaluated_mask::partition(mask,&fingerprint,&mesh.indices)).transpose()?.flatten();
            Ok(Candidate {old_items:source::refs(body.get(3))?,body:body.clone(),points,mesh,old_item,surface,opening_edits,layers,rounding_bounds,removed,fingerprint,partition})
        })();
        let Candidate {mut body,points,mesh,old_item,surface,old_items,opening_edits,layers,rounding_bounds,removed,fingerprint,partition}=match candidate {
            Ok(value)=>value,
            Err(reason)=> {
                if budget.exhausted { return Err(budget::BUDGET_ERROR.into()); }
                normalized.exclusions.push(Exclusion {product_id,reason}); continue;
            }
        };
        let rows=if partition.is_some() {6} else {4};
        if u64::from(normalized.request.next_express_id)+rows+layers.len() as u64>=u64::from(u32::MAX) || source.types.len()+rows as usize+layers.len()>200_000 {
            return Err("Evaluated appearance entity capacity exceeded".into());
        }
        let mut plan=AppearancePlan {next_express_id:normalized.request.next_express_id,
            next_available_express_id:normalized.request.next_express_id,..Default::default()};
        let mut entities=FxHashMap::default();
        let point_rows=A::List(points.into_iter().map(|p|A::List(p.into_iter().map(A::Float).collect())).collect());
        let mut point_attributes=vec![point_rows];
        if request.schema=="IFC4X3" {point_attributes.push(A::Null);}
        let coordinates=authored(&mut plan,&mut entities,IfcType::IfcCartesianPointList3D,point_attributes);
        // The masked face set is authored first so `plan.items` keeps ascending
        // creation order; the retained face set follows under the same wrapper.
        let (item,styled)=face_set(&mut plan,&mut entities,coordinates,partition.as_ref().map_or(&mesh.indices,|p|&p.masked),&surface);
        let retained=partition.as_ref().map(|p|face_set(&mut plan,&mut entities,coordinates,&p.retained,&surface));
        let items:Vec<u32>=std::iter::once(item).chain(retained.map(|(id,_)|id)).collect();
        super::evaluated_replacement::replace(source,product_id,&mut body,&items,layers,&mut plan,&mut entities)?;
        for mut opening in opening_edits {
            Arc::make_mut(&mut opening.attributes)[1]=A::String("Reference".into());
            plan.edits.push(PositionalEdit {express_id:opening.id,index:1,value:json!("Reference")});
            entities.insert(opening.id,Arc::new(opening));
        }
        let body_id=body.id;
        entities.insert(body.id,Arc::new(body));
        source.decoder.inject_shared_cache(&entities);
        for entity in &plan.created { source.types.insert(entity.express_id,entities[&entity.express_id].ifc_type); }
        for old in old_items { if let Some(parents)=source.incoming.get_mut(&old) {parents.remove(&body_id);} }
        source.incoming.insert(coordinates,items.iter().copied().collect());
        for &(face,styled_row) in std::iter::once(&(item,styled)).chain(retained.as_ref()) {
            source.incoming.insert(face,BTreeSet::from([body_id,styled_row]));
            source.styled_items.insert(face,vec![styled_row]);
            let (_,style)=crate::prepass::surface_style_from_styled_item(&entities[&styled_row],&mut source.decoder)
                .ok_or("Converted surface style failed canonical resolution")?;
            styles.geometry_style_index.insert(face,style);
        }
        if let Some((retained_item,_))=retained {
            source.evaluated_splits.insert(product_id,source::SplitBody {textured:item,retained:retained_item});
        }
        for owner in removed.iter().map(|mesh|mesh.express_id).collect::<BTreeSet<_>>() {
            if !canonical::produce(source,owner,&textures,Some(&styles))?.is_empty() {
                return Err("Converted Reference opening still produces canonical geometry".into());
            }
        }
        // Every authored face set must reproduce its share of the source
        // corners exactly, with the source colour and material retained.
        let target=canonical::produce(source,product_id,&textures,Some(&styles))?;
        let expected:Vec<(u32,&[u32])>=match (&partition,retained) {
            (None,None)=>vec![(item,&mesh.indices)],
            (Some(p),Some((retained_item,_)))=>vec![(item,&p.masked),(retained_item,&p.retained)],
            _=>return Err("Face mask split bookkeeping is inconsistent".into()),
        };
        if target.len()!=expected.len() { return Err("Evaluated replacement changed canonical submesh count".into()); }
        for (face,corners) in expected {
            let target=target.iter().find(|m|m.geometry_item_id==Some(face)).ok_or("Evaluated replacement lost an authored face set")?;
            if corners.len()!=target.indices.len() || mesh.color!=target.color || mesh.material_name!=target.material_name {
                return Err("Evaluated replacement changed source appearance or triangle count".into());
            }
            for (&a,&b) in corners.iter().zip(&target.indices) {
                let before=canonical::corner_position(&mesh.positions,a)?;
                let after=canonical::corner_position(&target.positions,b)?;
                let equivalent=if let Some(bounds)=&rounding_bounds {
                    super::evaluated_precision::same_corner(before,mesh.origin,after,target.origin,bounds[a as usize])
                } else {(0..3).all(|axis|f64::from(before[axis])+mesh.origin[axis]==f64::from(after[axis])+target.origin[axis])};
                if !equivalent {return Err("Evaluated replacement exceeds its canonical coordinate precision contract".into());}
            }
        }
        normalized.request.next_express_id=plan.next_available_express_id;
        normalized.request.product_ids.push(product_id);
        let context=source.context.as_ref().ok_or("Missing canonical source frame")?;
        let rtc_offset: [f64;3]=context.meta.frame.rtc_offset().into();
        if rtc_offset.iter().any(|value|!value.is_finite()) {return Err("Invalid canonical source RTC frame".into());}
        normalized.conversions.push(Conversion {plan,styled_id:styled,binding:AppearanceConversion {
            product_id,representation_id:body_id,source_geometry_item_id:old_item,geometry_item_id:item,
            source_indices:mesh.indices,source_positions:mesh.positions,source_normals:mesh.normals,
            source_origin:mesh.origin,source_color:mesh.color,rtc_offset,surface_fingerprint:fingerprint,
            masked_triangles:partition.map(|p|p.triangles),retained_geometry_item_id:retained.map(|(id,_)|id),
            source_removed_meshes:removed }});
    }
    Ok(normalized)
}
impl Normalized {
    pub(super) fn request(&self)->&AppearanceRequest {&self.request}
    pub(super) fn patch_styles(&self, source:&mut Source<'_>, styles:&mut crate::prepass::ResolvedPrepass)->Result<(),String> {
        for conversion in &self.conversions {
            let styled=source.entity(conversion.styled_id)?;
            let (_,style)=crate::prepass::surface_style_from_styled_item(&styled,&mut source.decoder).ok_or("Missing converted style")?;
            styles.geometry_style_index.insert(conversion.binding.geometry_item_id,style);
        }
        Ok(())
    }
    pub(super) fn compose(self,mut plan:AppearancePlan, source:&Source<'_>)->Result<(AppearancePlan,BTreeMap<u32,u32>),String> {
        let accepted:BTreeSet<_>=plan.items.iter().map(|item|item.product_id).collect();
        for conversion in self.conversions {
            if accepted.contains(&conversion.binding.product_id) {
                plan.created.extend(conversion.plan.created); plan.edits.extend(conversion.plan.edits);
                plan.conversions.push(conversion.binding);
            }
        }
        // The common wire contract edits existing rows. Fold edits of private
        // normalization rows into their creation records before publication.
        let created:std::collections::BTreeMap<_,_>=plan.created.iter().enumerate().map(|(index,entity)|(entity.express_id,index)).collect();
        let edits=std::mem::take(&mut plan.edits);
        for edit in edits {
            if let Some(&index)=created.get(&edit.express_id) {
                plan.created[index].attributes[edit.index]=edit.value;
            } else { plan.edits.push(edit); }
        }
        plan.next_express_id=self.start;
        plan.exclusions.extend(self.exclusions);
        let ids=super::evaluated_allocation::compact(&mut plan,&source.types)?;
        Ok((plan,ids))
    }
}

#[cfg(test)]
#[path = "evaluated_tests.rs"]
pub(crate) mod tests;