ifc-geometry 0.3.1

IFC semantic views lowered into the format-neutral geometry DAG.
Documentation
//! Product placement and representation selection.
//!
//! # Why placement is composed here and not per item
//!
//! A representation item is authored in its product local space.
//! The product hangs off an IfcLocalPlacement chain that walks up
//! the spatial tree to the site. Lowering an item without that
//! chain puts every product at the world origin: the geometry is
//! individually correct and the building is a heap.
//!
//! # Units are converted once
//!
//! Placement coordinates are raw file units, so the resolver
//! composes the chain unconverted and this module converts the
//! composed result. Converting per link would raise the scale to
//! the power of the chain depth.

use axiolid_model::NodeId;
use ifc_model::EntityId;

use crate::error::{GeometryError, GeometryResult};
// Moved to `input::product`: it never needed the kernel. Re-exported so
// the pre-existing `lower::context::geometric_products` path still resolves.
use crate::input::context::context_of;
pub use crate::input::product::geometric_products;
use crate::input::representation::{
    select_product_representation, Representation, RepresentationPurpose,
};
use crate::lower::dispatch::lower_representation_item;
use crate::lower::session::LoweringSession;
use crate::resource::placement::axis_placement_transform;
use crate::transform::Transform;

pub use crate::constraint::product_world_transform;
pub use crate::input::representation::select_shape_representation;

/// Lower every item of a product selected representation into one graph.
///
/// All items share one session, so a product whose Body holds several
/// solids yields one graph with one Collection root rather than N
/// disconnected graphs the caller has to merge.
pub fn lower_product_items(
    session: &mut LoweringSession<'_>,
    product: EntityId,
) -> GeometryResult<Option<NodeId>> {
    lower_product_representation(session, product, RepresentationPurpose::Body)
}

/// Select and lower a product representation for an explicit consumer path.
pub fn lower_product_representation(
    session: &mut LoweringSession<'_>,
    product: EntityId,
    purpose: RepresentationPurpose,
) -> GeometryResult<Option<NodeId>> {
    let placement = product_world_transform(session.model(), session.units(), product)?;
    let Some(representation) = select_product_representation(session.model(), product, purpose)?
    else {
        return Ok(None);
    };
    // Model space is the context's frame; the product's chain is expressed
    // inside it, so the context frame composes above the placement.
    let world = context_world_transform(session, representation)?.compose(&placement);

    let entity = session
        .model()
        .get(representation)
        .ok_or(GeometryError::MissingEntity {
            referrer: product,
            missing: representation,
        })?;
    let items = Representation::new(representation, entity).items()?;
    let mut roots = Vec::with_capacity(items.len());
    for item in items {
        roots.push(lower_representation_item(session, item, world)?);
    }
    match roots.len() {
        0 => Ok(None),
        1 => Ok(Some(roots[0])),
        _ => Ok(Some(session.node_for(
            product,
            axiolid_model::GeometryNode::Collection(roots),
        )?)),
    }
}

/// The frame a representation's items are authored in.
///
/// `IfcGeometricRepresentationContext.WorldCoordinateSystem` is mandatory and
/// defines model space for every representation that names the context. Most
/// files write the identity, so ignoring it looks correct on a corpus; a file
/// that surveys its site into a real coordinate system does not agree.
///
/// A sub-context inherits the value through `ParentContext`, which
/// [`RepresentationContext::world_coordinate_system`] already resolves.
/// Coordinates are raw file units, so the frame converts once here, matching
/// how the placement chain is handled.
fn context_world_transform(
    session: &LoweringSession<'_>,
    representation: EntityId,
) -> GeometryResult<Transform> {
    let model = session.model();
    let Some(context) = context_of(model, representation) else {
        return Ok(Transform::identity());
    };
    let Some(placement_id) = context.world_coordinate_system(model) else {
        return Ok(Transform::identity());
    };
    let placement = model
        .get(placement_id)
        .ok_or(GeometryError::MissingEntity {
            referrer: context.id(),
            missing: placement_id,
        })?;
    Ok(axis_placement_transform(model, placement_id, placement)?.to_metres(session.units()))
}