use std::collections::{BTreeMap, BTreeSet};
use std::error::Error;
use axiolid_contracts::ExecutionOptions;
use axiolid_core::Tolerance;
use axiolid_curve::{Curve2, Curve3};
use axiolid_mesh_compile_contract::MeshCompiler;
use axiolid_model::{GeometryGraph, GeometryNode, NodeId, SolidOperation, SurfaceRelation};
use axiolid_primitive::Primitive;
use axiolid_profile::{Profile, SectionProfile};
use axiolid_surface::Surface;
use axioval::axiolid::{
AxiolidBoundaryCoverageService, AxiolidContactService, AxiolidDerivedRelationshipService,
AxiolidEnvelopeMembershipService, AxiolidFacadeAreaService, AxiolidFreeSpaceService,
AxiolidGeometry, AxiolidGuardService, AxiolidLinearQuantityService,
AxiolidMetricRoutingService, AxiolidPlanAreaService, AxiolidPlanSpanService,
AxiolidProximityService, AxiolidSightService, AxiolidSpaceService, AxiolidTriangleCountService,
AxiolidVerticalExtentService, AxiolidWalkabilityService, AxiolidWalkingSurfaceService,
};
use axioval::bcf;
use axioval::engine::{
BoundaryCoverageServiceHandle, ContactServiceHandle, DerivedRelationshipServiceHandle,
EnvelopeMembershipServiceHandle, EvidenceSession, FacadeAreaServiceHandle,
FreeSpaceServiceHandle, GuardServiceHandle, LinearQuantityServiceHandle,
MetricRoutingServiceHandle, PlanAreaServiceHandle, PlanSpanServiceHandle, PropertyRequest,
PropertyResolution, PropertyResolutionServiceHandle, ProximityServiceHandle, RelationshipQuery,
RelationshipSelectionRequest, RelationshipSelectionServiceHandle, SemanticRelationship,
SightServiceHandle, SourceSnapshot, SpaceServiceHandle, TraversalDirection,
TriangleCountServiceHandle, TypeHierarchyServiceHandle, VerticalExtentServiceHandle,
WalkabilityServiceHandle, WalkingSurfaceServiceHandle,
};
use axioval::ir::{ObjectId, PropertyValue, Report, SourceId};
use ifc_geometry::lower::{LoweringSession, lower_connection_surface, lower_product_net};
use ifc_geometry::{RepresentationPurpose, Transform};
use ifc_model::{Codec, EntityId, Model};
use ifc_spatial::relation::boundary::{ConnectionGeometryAnomaly, SpaceBoundary};
use ifc_spatial::{SpatialAnomaly, SpatialKind, SpatialTree};
use ifc_step::StepCodec;
use std::sync::Arc;
const TOLERANCE: Tolerance = Tolerance::MILLIMETRE;
const CHORD_DEVIATION_METRES: f64 = 1e-3;
const NODE_BUDGET: usize = 100_000;
const NO_BODY: &[&str] = &[
"IfcSpatialElement",
"IfcSpatialStructureElement",
"IfcFeatureElementSubtraction",
"IfcVirtualElement",
"IfcAnnotation",
"IfcGrid",
"IfcPort",
"IfcStructuralItem",
"IfcStructuralActivity",
];
#[derive(Debug, Default)]
pub struct GeometryReport {
pub exact: usize,
pub tessellated: usize,
pub no_body: usize,
pub unmeasured: Vec<(ObjectId, String)>,
}
pub type ModelBytes = BTreeMap<SourceId, Vec<u8>>;
struct Parsed {
model: Model,
units: ifc_geometry::units::UnitScale,
}
fn parse(
snapshots: &[SourceSnapshot],
models: &ModelBytes,
) -> Result<BTreeMap<SourceId, Parsed>, Box<dyn Error>> {
let mut parsed = BTreeMap::new();
for snapshot in snapshots {
let source = snapshot.source();
let bytes = models
.get(source)
.ok_or_else(|| format!("no model bytes for source `{source}`"))?;
let model = StepCodec
.read_bytes(bytes)
.map_err(|error| format!("{}: {error}", source.document))?;
let units = ifc_geometry::units::resolve(&model);
parsed.insert(source.clone(), Parsed { model, units });
}
Ok(parsed)
}
pub fn attach(
session: EvidenceSession,
models: &ModelBytes,
) -> Result<(EvidenceSession, GeometryReport), Box<dyn Error>> {
let snapshots: Vec<SourceSnapshot> = session.snapshots().cloned().collect();
let Some(first) = snapshots.first() else {
return Err("geometry needs a session over at least one source".into());
};
let source = first.source().clone();
let parsed = parse(&snapshots, models)?;
let hierarchy = session
.service::<TypeHierarchyServiceHandle>()
.ok_or("the session has no type hierarchy to classify objects with")?
.clone();
let is_a = |id: &ObjectId, ancestor: &str, kinds: &BTreeMap<ObjectId, String>| {
kinds
.get(id)
.is_some_and(|kind| hierarchy.is_a(&id.source, kind, ancestor).unwrap_or(false))
};
let kinds: BTreeMap<ObjectId, String> = session
.project()
.objects()
.map(|object| (object.id.clone(), object.kind().to_owned()))
.collect();
let is_a = |id: &ObjectId, ancestor: &str| is_a(id, ancestor, &kinds);
let backend = ifc_geometry::compile::default_backend();
let mut geometry = AxiolidGeometry::new();
let mut report = GeometryReport::default();
let mut voids: Vec<(ObjectId, Void)> = Vec::new();
for object in session.project().objects() {
let id = object.id.clone();
let Some(Parsed { model, units }) = parsed.get(&id.source) else {
return Err(format!("no model for source `{}`", id.source).into());
};
let is_space = is_a(&id, "IfcSpace");
let bodiless = !is_a(&id, "IfcProduct")
|| (!is_space && NO_BODY.iter().any(|ancestor| is_a(&id, ancestor)));
if bodiless {
if is_a(&id, "IfcOpeningElement") {
let void = entity_id(&id)
.ok_or_else(|| "not a STEP instance id".to_owned())
.and_then(|entity| mesh(&backend, model, units, entity))
.and_then(|meshed| meshed.ok_or_else(|| "no body representation".into()));
voids.push((id.clone(), void));
}
geometry = geometry.with_no_body(id);
report.no_body += 1;
continue;
}
let Some(entity) = entity_id(&id) else {
report
.unmeasured
.push((id.clone(), "not a STEP instance id".into()));
geometry = geometry.with_unmeasured(id, "not a STEP instance id");
continue;
};
match mesh(&backend, model, units, entity) {
Ok(Some((mesh, true))) => {
geometry = geometry.with_mesh(id, mesh);
report.exact += 1;
}
Ok(Some((mesh, false))) => {
geometry = geometry.with_tessellated_mesh(id, mesh, CHORD_DEVIATION_METRES);
report.tessellated += 1;
}
Ok(None) if is_space => {
geometry = geometry.with_no_body(id);
report.no_body += 1;
}
Ok(None) => {
let reason = "no body representation";
report.unmeasured.push((id.clone(), reason.into()));
geometry = geometry.with_unmeasured(id, reason);
}
Err(error) => {
report.unmeasured.push((id.clone(), error.clone()));
geometry = geometry.with_unmeasured(id, error);
}
}
}
let relationships = session.service::<RelationshipSelectionServiceHandle>();
for (group, members) in groups(relationships, &kinds, &is_a) {
geometry = match members {
Ok(members) => geometry.with_group(group, members),
Err(reason) => geometry.with_undecided_group(group, reason),
};
}
let envelope = envelope_service(&session, &geometry, &source, &kinds)?;
let space = (
space_service(&parsed, &geometry, &source, &kinds, &is_a),
linear_service(&geometry, &voids),
boundary_service(&backend, &parsed, &geometry, &kinds, &is_a),
plan_area_service(&geometry, &source, &voids),
);
let routes = route_services(&geometry, &source, &kinds, &is_a, &voids);
let derived = derived_service(&geometry, &parsed, &kinds, &is_a, voids);
let facade = facade_service(&geometry, &kinds, &is_a);
let session = register(session, &snapshots, geometry, space, envelope, routes)?
.with_host_service(FacadeAreaServiceHandle::new(Arc::new(facade)), &snapshots)?
.with_derived_relationships(
DerivedRelationshipServiceHandle::new(Arc::new(derived)),
&snapshots,
)?;
Ok((session, report))
}
fn register(
session: EvidenceSession,
snapshots: &[SourceSnapshot],
geometry: AxiolidGeometry,
(space, shelves, boundaries, plan_areas): (
AxiolidSpaceService,
AxiolidLinearQuantityService,
AxiolidBoundaryCoverageService,
AxiolidPlanAreaService,
),
envelope: AxiolidEnvelopeMembershipService,
(walkability, routing): (AxiolidWalkabilityService, AxiolidMetricRoutingService),
) -> Result<EvidenceSession, Box<dyn Error>> {
let source = snapshots
.first()
.ok_or("geometry needs a session over at least one source")?
.source()
.clone();
let bound = snapshots;
let session = session
.with_host_service(
ContactServiceHandle::new(Arc::new(AxiolidContactService::new(geometry.clone()))),
bound,
)?
.with_host_service(
FreeSpaceServiceHandle::new(Arc::new(AxiolidFreeSpaceService::new(
geometry.clone(),
source.clone(),
))),
bound,
)?
.with_host_service(SpaceServiceHandle::new(Arc::new(space)), bound)?
.with_host_service(
BoundaryCoverageServiceHandle::new(Arc::new(boundaries)),
bound,
)?
.with_host_service(
GuardServiceHandle::new(Arc::new(AxiolidGuardService::new(
geometry.clone(),
source.clone(),
))),
bound,
)?
.with_host_service(LinearQuantityServiceHandle::new(Arc::new(shelves)), bound)?
.with_host_service(
PlanAreaServiceHandle::new(Arc::new(plan_areas)),
bound,
)?
.with_host_service(
PlanSpanServiceHandle::new(Arc::new(AxiolidPlanSpanService::new(
geometry.clone(),
source.clone(),
))),
bound,
)?
.with_host_service(
TriangleCountServiceHandle::new(Arc::new(AxiolidTriangleCountService::new(
geometry.clone(),
))),
bound,
)?
.with_host_service(
VerticalExtentServiceHandle::new(Arc::new(AxiolidVerticalExtentService::new(
geometry.clone(),
))),
bound,
)?
.with_host_service(
WalkingSurfaceServiceHandle::new(Arc::new(AxiolidWalkingSurfaceService::new(
geometry.clone(),
))),
bound,
)?
.with_host_service(
SightServiceHandle::new(Arc::new(AxiolidSightService::new(geometry.clone()))),
bound,
)?
.with_host_service(
ProximityServiceHandle::new(Arc::new(AxiolidProximityService::new(geometry))),
bound,
)?
.with_host_service(
EnvelopeMembershipServiceHandle::new(Arc::new(envelope)),
bound,
)?
.with_host_service(WalkabilityServiceHandle::new(Arc::new(walkability)), bound)?
.with_host_service(MetricRoutingServiceHandle::new(Arc::new(routing)), bound)?;
Ok(session)
}
fn groups(
relationships: Option<&RelationshipSelectionServiceHandle>,
kinds: &BTreeMap<ObjectId, String>,
is_a: &impl Fn(&ObjectId, &str) -> bool,
) -> Vec<(ObjectId, Result<Vec<ObjectId>, String>)> {
let everything: Vec<ObjectId> = kinds.keys().cloned().collect();
let members = |group: &ObjectId| -> Result<Vec<ObjectId>, String> {
let relationships =
relationships.ok_or("the session has no relationship service to read groups with")?;
let query = RelationshipQuery::Related {
relationship: SemanticRelationship::try_new("IfcRelAssignsToGroup")
.map_err(|error| error.to_string())?,
direction: TraversalDirection::Forward,
follow_chain: false,
};
let request =
RelationshipSelectionRequest::try_new(group.clone(), everything.clone(), query)
.map_err(|error| error.to_string())?;
let selection = relationships
.select(&request)
.map_err(|error| error.to_string())?;
Ok(selection.candidates().to_vec())
};
kinds
.keys()
.filter(|id| is_a(id, "IfcGroup"))
.map(|group| (group.clone(), members(group)))
.collect()
}
fn envelope_service(
session: &EvidenceSession,
geometry: &AxiolidGeometry,
source: &SourceId,
kinds: &BTreeMap<ObjectId, String>,
) -> Result<AxiolidEnvelopeMembershipService, Box<dyn Error>> {
let properties = session
.service::<PropertyResolutionServiceHandle>()
.ok_or("the session has no property service to read declarations with")?;
let value = |object: &ObjectId, name: &str| -> Option<PropertyValue> {
let request = PropertyRequest::try_new(object.clone(), None, name).ok()?;
match properties.resolve(&request).ok()? {
PropertyResolution::Present(resolved) => Some(resolved.property().value.clone()),
PropertyResolution::Absent(_) => None,
}
};
let mut service = AxiolidEnvelopeMembershipService::new(geometry.clone(), source.clone());
for object in kinds.keys() {
if geometry.mesh(object).is_none() {
continue;
}
match value(object, "IsExternal") {
Some(PropertyValue::Boolean(true)) => {
service = service.with_declared_external(object.clone());
}
Some(PropertyValue::Boolean(false)) => {
service = service.with_declared_internal(object.clone());
}
_ => {}
}
}
Ok(service)
}
type Void = Result<(axiolid_mesh::TriMesh, bool), String>;
fn derived_service(
geometry: &AxiolidGeometry,
parsed: &BTreeMap<SourceId, Parsed>,
kinds: &BTreeMap<ObjectId, String>,
is_a: &impl Fn(&ObjectId, &str) -> bool,
voids: Vec<(ObjectId, Void)>,
) -> AxiolidDerivedRelationshipService {
let mut service = AxiolidDerivedRelationshipService::new(geometry.clone());
for id in kinds.keys() {
if is_a(id, "IfcSpace") {
service = service.with_space(id.clone());
} else if is_a(id, "IfcDoor") || is_a(id, "IfcWindow") {
service = service.with_opening(id.clone());
}
}
for (id, void) in voids {
service = match void {
Ok((mesh, true)) => service.with_opening_void(id, mesh),
Ok((mesh, false)) => {
service.with_tessellated_opening_void(id, mesh, CHORD_DEVIATION_METRES)
}
Err(reason) => service.with_unmeasured_opening_void(id, reason),
};
}
with_levels(service, parsed, kinds, is_a)
}
type Height = Result<f64, String>;
type Parent = (SourceId, Option<EntityId>);
fn with_levels(
mut service: AxiolidDerivedRelationshipService,
parsed: &BTreeMap<SourceId, Parsed>,
kinds: &BTreeMap<ObjectId, String>,
is_a: &impl Fn(&ObjectId, &str) -> bool,
) -> AxiolidDerivedRelationshipService {
let trees: BTreeMap<&SourceId, SpatialTree> = parsed
.iter()
.map(|(source, Parsed { model, .. })| (source, SpatialTree::build(model)))
.collect();
let mut siblings: BTreeMap<Parent, Vec<(ObjectId, Height)>> = BTreeMap::new();
for id in kinds.keys().filter(|id| is_a(id, "IfcBuildingStorey")) {
let (Some(entity), Some(Parsed { model, units })) = (entity_id(id), parsed.get(&id.source))
else {
continue;
};
let parent = trees
.get(&id.source)
.and_then(|tree| tree.node(entity))
.and_then(|node| node.parent);
let height = ifc_geometry::product_world_transform(model, units, entity)
.map_err(|error| error.to_string())
.and_then(|frame| {
let up = frame.basis[2];
if up[0].abs() > 1e-12 || up[1].abs() > 1e-12 || (up[2] - 1.0).abs() > 1e-12 {
Err("the storey's placement is tilted".to_owned())
} else {
Ok(frame.origin[2])
}
});
siblings
.entry((id.source.clone(), parent))
.or_default()
.push((id.clone(), height));
}
for levels in siblings.into_values() {
let mut heights: Vec<f64> = levels
.iter()
.filter_map(|(_, height)| height.as_ref().ok().copied())
.collect();
heights.sort_by(f64::total_cmp);
for (level, height) in levels {
service = match height {
Err(reason) => service.with_unmeasured_level(level, reason),
#[allow(clippy::float_cmp)]
Ok(height) if heights.iter().filter(|other| **other == height).count() > 1 => {
service.with_unmeasured_level(level, "another storey shares its elevation")
}
Ok(height) => {
let top = heights.iter().copied().find(|other| *other > height);
service.with_level(level, height, top)
}
};
}
}
service
}
fn plan_area_service(
geometry: &AxiolidGeometry,
source: &SourceId,
voids: &[(ObjectId, Void)],
) -> AxiolidPlanAreaService {
voids.iter().fold(
AxiolidPlanAreaService::new(geometry.clone(), source.clone()),
|service, (id, void)| match void {
Ok((mesh, true)) => service.with_opening_void(id.clone(), mesh.clone()),
Ok((_, false)) | Err(_) => service.with_unmeasured_opening_void(id.clone()),
},
)
}
fn linear_service(
geometry: &AxiolidGeometry,
voids: &[(ObjectId, Void)],
) -> AxiolidLinearQuantityService {
voids.iter().fold(
AxiolidLinearQuantityService::new(geometry.clone()),
|service, (id, void)| match void {
Ok((mesh, true)) => service.with_opening_void(id.clone(), mesh.clone()),
Ok((mesh, false)) => service.with_tessellated_opening_void(
id.clone(),
mesh.clone(),
CHORD_DEVIATION_METRES,
),
Err(_) => service.with_unmeasured_opening_void(id.clone()),
},
)
}
fn route_services(
geometry: &AxiolidGeometry,
source: &SourceId,
kinds: &BTreeMap<ObjectId, String>,
is_a: &impl Fn(&ObjectId, &str) -> bool,
voids: &[(ObjectId, Void)],
) -> (AxiolidWalkabilityService, AxiolidMetricRoutingService) {
(
walkability_service(geometry, source, voids),
routing_service(geometry, source, kinds, is_a, voids),
)
}
fn walkability_service(
geometry: &AxiolidGeometry,
source: &SourceId,
voids: &[(ObjectId, Void)],
) -> AxiolidWalkabilityService {
voids.iter().fold(
AxiolidWalkabilityService::new(geometry.clone(), source.clone()),
|service, (id, void)| match void {
Ok((mesh, true)) => service.with_opening_void(id.clone(), mesh.clone()),
Ok((mesh, false)) => service.with_tessellated_opening_void(id.clone(), mesh.clone()),
Err(reason) => service.with_unmeasured_opening_void(id.clone(), reason.clone()),
},
)
}
fn routing_service(
geometry: &AxiolidGeometry,
source: &SourceId,
kinds: &BTreeMap<ObjectId, String>,
is_a: &impl Fn(&ObjectId, &str) -> bool,
voids: &[(ObjectId, Void)],
) -> AxiolidMetricRoutingService {
let mut service = AxiolidMetricRoutingService::new(geometry.clone(), source.clone());
for id in kinds.keys() {
if is_a(id, "IfcSpace") {
service = service.with_surface(id.clone());
} else if is_a(id, "IfcDoor") {
service = service.with_portal(id.clone());
} else if CONNECTORS.iter().any(|connector| is_a(id, connector)) {
service = service.with_connector(id.clone());
}
}
for (id, void) in voids {
service = match void {
Ok((mesh, true)) => service.with_opening_void(id.clone(), mesh.clone()),
Ok((mesh, false)) => service.with_tessellated_opening_void(id.clone(), mesh.clone()),
Err(reason) => service.with_unmeasured_opening_void(id.clone(), reason.clone()),
};
}
service
}
const CONNECTORS: &[&str] = &[
"IfcStair",
"IfcStairFlight",
"IfcRamp",
"IfcRampFlight",
"IfcTransportElement",
];
fn facade_service(
geometry: &AxiolidGeometry,
kinds: &BTreeMap<ObjectId, String>,
is_a: &impl Fn(&ObjectId, &str) -> bool,
) -> AxiolidFacadeAreaService {
kinds.keys().filter(|id| is_a(id, "IfcSpace")).fold(
AxiolidFacadeAreaService::new(geometry.clone()),
|service, id| service.with_space(id.clone()),
)
}
fn boundary_service(
backend: &impl MeshCompiler,
parsed: &BTreeMap<SourceId, Parsed>,
geometry: &AxiolidGeometry,
kinds: &BTreeMap<ObjectId, String>,
is_a: &impl Fn(&ObjectId, &str) -> bool,
) -> AxiolidBoundaryCoverageService {
let mut service = AxiolidBoundaryCoverageService::new(geometry.clone());
for id in kinds.keys().filter(|id| is_a(id, "IfcSpace")) {
service = service.with_space(id.clone());
}
for (source, Parsed { model, units }) in parsed {
let object = |entity: EntityId| ObjectId {
source: source.clone(),
local_id: entity.to_string(),
};
for boundary in ifc_spatial::relation::boundary::all(model) {
let Some(space) = boundary.space.map(object).filter(|id| is_a(id, "IfcSpace")) else {
continue;
};
let element = boundary
.element
.map(object)
.filter(|id| kinds.contains_key(id));
let id = object(boundary.id);
service = match boundary_surface(backend, model, units, &boundary, &space) {
Ok((mesh, true)) => service.with_boundary(space, id, element, mesh),
Ok((mesh, false)) => service.with_tessellated_boundary(
space,
id,
element,
mesh,
CHORD_DEVIATION_METRES,
),
Err(reason) => service.with_unmeasured_boundary(space, id, element, reason),
};
}
}
service
}
fn boundary_surface(
backend: &impl MeshCompiler,
model: &Model,
units: &ifc_geometry::units::UnitScale,
boundary: &SpaceBoundary,
space: &ObjectId,
) -> Result<(axiolid_mesh::TriMesh, bool), String> {
let connection = match boundary.connection_geometry(model) {
Ok(Some(connection)) => connection,
Ok(None) => return Err("the boundary states no connection geometry".into()),
Err(ConnectionGeometryAnomaly::Dangling { target, .. }) => {
return Err(format!("the connection geometry {target} does not exist"));
}
Err(ConnectionGeometryAnomaly::WrongKind {
target, type_name, ..
}) => {
return Err(format!(
"{target} is a {type_name}, not a connection geometry"
));
}
Err(anomaly) => return Err(format!("unreadable connection geometry: {anomaly:?}")),
};
let frame = space_frame(model, units, space)?;
let mut session = LoweringSession::new(model, units);
let root = lower_connection_surface(&mut session, connection, frame)
.map_err(|error| error.to_string())?;
let lowered = session.finish(root).map_err(|error| error.to_string())?;
let exact = planar(&lowered.graph, lowered.root, &mut NODE_BUDGET.clone());
let mesh = backend
.compile_mesh(
&lowered.graph,
lowered.root,
&ExecutionOptions::new(TOLERANCE),
)
.map_err(|error| format!("mesh compilation refused: {error}"))?;
if mesh.triangle_count() == 0 {
return Err("mesh compilation produced no triangles".into());
}
Ok((mesh, exact))
}
fn space_frame(
model: &Model,
units: &ifc_geometry::units::UnitScale,
space: &ObjectId,
) -> Result<Transform, String> {
let entity = entity_id(space).ok_or("the space is not a STEP instance")?;
match ifc_geometry::product_representation_frame(
model,
units,
entity,
RepresentationPurpose::Body,
) {
Ok(Some(frame)) => Ok(frame),
Ok(None) => ifc_geometry::product_world_transform(model, units, entity)
.map_err(|error| error.to_string()),
Err(error) => Err(error.to_string()),
}
}
fn entity_id(id: &ObjectId) -> Option<EntityId> {
id.local_id.strip_prefix('#')?.parse().ok().map(EntityId)
}
fn mesh(
backend: &impl MeshCompiler,
model: &Model,
units: &ifc_geometry::units::UnitScale,
product: EntityId,
) -> Result<Option<(axiolid_mesh::TriMesh, bool)>, String> {
let mut session = LoweringSession::new(model, units);
let Some(net) = lower_product_net(&mut session, product).map_err(|e| e.to_string())? else {
return Ok(None);
};
let lowered = session.finish(net.root).map_err(|e| e.to_string())?;
let exact = planar(&lowered.graph, lowered.root, &mut NODE_BUDGET.clone());
let mesh = backend
.compile_mesh(
&lowered.graph,
lowered.root,
&ExecutionOptions::new(TOLERANCE),
)
.map_err(|e| format!("mesh compilation refused: {e}"))?;
if mesh.triangle_count() == 0 {
return Err("mesh compilation produced no triangles".into());
}
Ok(Some((mesh, exact)))
}
fn planar(graph: &GeometryGraph, id: NodeId, budget: &mut usize) -> bool {
if *budget == 0 {
return false;
}
*budget -= 1;
let Some(node) = graph.get(id) else {
return false;
};
match node {
GeometryNode::TriMesh(_)
| GeometryNode::PolygonMesh(_)
| GeometryNode::BoundingBox(_)
| GeometryNode::HalfSpace(_)
| GeometryNode::Primitive(Primitive::Block { .. }) => true,
GeometryNode::Instance(instance) => planar(graph, instance.source, budget),
GeometryNode::Collection(children) => {
children.iter().all(|child| planar(graph, *child, budget))
}
GeometryNode::SolidOperation(SolidOperation::Extrusion { profile, .. }) => {
planar(graph, *profile, budget)
}
GeometryNode::SolidOperation(SolidOperation::Boolean { left, right, .. }) => {
planar(graph, *left, budget) && planar(graph, *right, budget)
}
GeometryNode::Profile(profile) => polygonal(profile),
GeometryNode::SurfaceRelation(SurfaceRelation::CurveBounded {
basis, boundaries, ..
}) => {
matches!(
graph.get(*basis),
Some(GeometryNode::Surface(Surface::Plane(_)))
) && boundaries.iter().all(|boundary| {
matches!(
graph.get(*boundary),
Some(
GeometryNode::Curve3(Curve3::Line(_) | Curve3::Polyline(_))
| GeometryNode::Curve2(Curve2::Line(_) | Curve2::Polyline(_))
)
)
})
}
GeometryNode::BRep(brep) => {
brep.faces().iter().all(|face| {
face.surface.is_none_or(|surface| {
matches!(
graph.get(surface),
Some(GeometryNode::Surface(Surface::Plane(_)))
)
})
}) && brep.edges().iter().all(|edge| {
edge.curve.is_none_or(|curve| {
matches!(
graph.get(curve),
Some(GeometryNode::Curve3(Curve3::Line(_) | Curve3::Polyline(_)))
)
})
})
}
_ => false,
}
}
fn polygonal(profile: &Profile) -> bool {
let straight = |curve: &Curve2| matches!(curve, Curve2::Line(_) | Curve2::Polyline(_));
let sharp = |radius: Option<f64>| radius.is_none_or(|r| r == 0.0);
match profile {
Profile::Rectangle(rectangle) => {
sharp(rectangle.outer_radius) && sharp(rectangle.inner_radius)
}
Profile::Section(section) => match section {
SectionProfile::I {
fillet_radius,
flange_edge_radius,
..
} => sharp(*fillet_radius) && sharp(*flange_edge_radius),
SectionProfile::AsymmetricI {
bottom_fillet_radius,
bottom_flange_edge_radius,
top_fillet_radius,
top_flange_edge_radius,
..
} => [
bottom_fillet_radius,
bottom_flange_edge_radius,
top_fillet_radius,
top_flange_edge_radius,
]
.into_iter()
.all(|radius| sharp(*radius)),
SectionProfile::L {
fillet_radius,
edge_radius,
..
}
| SectionProfile::U {
fillet_radius,
edge_radius,
..
}
| SectionProfile::Z {
fillet_radius,
edge_radius,
..
} => sharp(*fillet_radius) && sharp(*edge_radius),
SectionProfile::T {
fillet_radius,
flange_edge_radius,
web_edge_radius,
..
} => sharp(*fillet_radius) && sharp(*flange_edge_radius) && sharp(*web_edge_radius),
SectionProfile::C {
internal_fillet_radius,
..
} => sharp(*internal_fillet_radius),
SectionProfile::Trapezium { .. } => true,
_ => false,
},
Profile::Contour(contour) => std::iter::once(&contour.outer)
.chain(&contour.holes)
.flat_map(|ring| &ring.segments)
.all(|segment| straight(&segment.curve)),
Profile::Derived { basis, .. } => polygonal(basis),
Profile::Composite(parts) => parts.iter().all(polygonal),
_ => false,
}
}
fn space_service(
parsed: &BTreeMap<SourceId, Parsed>,
geometry: &AxiolidGeometry,
source: &SourceId,
kinds: &BTreeMap<ObjectId, String>,
is_a: &impl Fn(&ObjectId, &str) -> bool,
) -> AxiolidSpaceService {
let trees: BTreeMap<&SourceId, (SpatialTree, BTreeSet<EntityId>)> = parsed
.iter()
.map(|(source, Parsed { model, .. })| {
let tree = SpatialTree::build(model);
let ambiguous: BTreeSet<EntityId> = tree
.anomalies()
.iter()
.filter_map(|anomaly| match anomaly {
SpatialAnomaly::ContainedTwice { element, .. } => Some(*element),
SpatialAnomaly::AggregatedTwice { child, .. } => Some(*child),
_ => None,
})
.collect();
(source, (tree, ambiguous))
})
.collect();
let storey_of = |tree: &SpatialTree,
ambiguous: &BTreeSet<EntityId>,
entity: EntityId|
-> Option<EntityId> {
let mut chain = vec![entity];
chain.extend(tree.container_of(entity));
let start = *chain.last()?;
chain.extend(tree.ancestors(start));
if chain.iter().any(|link| ambiguous.contains(link)) {
return None;
}
chain.into_iter().skip(1).find(|link| {
tree.node(*link)
.is_some_and(|node| node.kind == SpatialKind::Storey)
})
};
let mut service = AxiolidSpaceService::new(geometry.clone(), source.clone());
for id in kinds.keys() {
let (Some(entity), Some((tree, ambiguous))) = (entity_id(id), trees.get(&id.source)) else {
continue;
};
if is_a(id, "IfcSpace") {
service = service.with_space(id.clone());
} else if is_a(id, "IfcSlab") {
service = service.with_slab(id.clone());
} else if is_a(id, "IfcRoof") {
service = service.with_roof(id.clone());
} else if is_a(id, "IfcBuilding") {
service = service.with_building(id.clone());
}
let is_structure = tree.node(entity).is_some();
if (!is_structure || is_a(id, "IfcSpace"))
&& !geometry.has_no_body(id)
&& let Some(storey) = storey_of(tree, ambiguous, entity)
{
let storey = ObjectId {
source: id.source.clone(),
local_id: storey.to_string(),
};
service = service.with_storey(id.clone(), storey);
}
}
service
}
pub fn bounds(sessions: &[&EvidenceSession], report: &Report) -> BTreeMap<ObjectId, bcf::Bounds> {
let mut named: BTreeSet<&ObjectId> = BTreeSet::new();
for finding in report.findings() {
named.extend(finding.object_id());
named.extend(&finding.related);
}
for outcome in report.not_evaluated() {
named.extend(outcome.object_id());
}
let mut bounds = BTreeMap::new();
for object in named {
let measured = sessions
.iter()
.filter(|session| session.project().object(object).is_some())
.filter_map(|session| session.service::<ProximityServiceHandle>())
.find_map(|service| service.bounds(object).ok())
.map(|measured| measured.enclosing())
.and_then(|extent| bcf::Bounds::new(extent.min(), extent.max()));
if let Some(measured) = measured {
bounds.insert(object.clone(), measured);
}
}
bounds
}
#[cfg(test)]
mod tests {
use super::attach;
use axioval::engine::{
MetricPoint, MetricRouteOutcome, MetricRouteRequest, MetricRoutingServiceHandle,
MobilityProfile, WalkabilityRequest, WalkabilityRouteOutcome, WalkabilityServiceHandle,
};
use axioval::ifc::import_ifc_session;
use axioval::ir::ObjectId;
fn rooms_and_door() -> String {
let body = |first: u32, x: f64, y: f64, dx: f64, dy: f64, depth: f64| {
let [point, position, profile, solid, shape] = [0, 1, 2, 3, 4].map(|o| first + o);
format!(
"#{point}=IFCCARTESIANPOINT(({x},{y}));\n\
#{position}=IFCAXIS2PLACEMENT2D(#{point},$);\n\
#{profile}=IFCRECTANGLEPROFILEDEF(.AREA.,$,#{position},{dx},{dy});\n\
#{solid}=IFCEXTRUDEDAREASOLID(#{profile},#2,#4,{depth});\n\
#{shape}=IFCSHAPEREPRESENTATION(#5,'Body','SweptSolid',(#{solid}));\n\
#{}=IFCPRODUCTDEFINITIONSHAPE($,$,(#{shape}));\n",
first + 5
)
};
format!(
"ISO-10303-21;\nHEADER;\nFILE_DESCRIPTION((''),'2;1');\nFILE_NAME('n','t',(''),(''),'p','o','a');\nFILE_SCHEMA(('IFC4'));\nENDSEC;\nDATA;\n\
#1=IFCCARTESIANPOINT((0.,0.,0.));\n\
#2=IFCAXIS2PLACEMENT3D(#1,$,$);\n\
#3=IFCLOCALPLACEMENT($,#2);\n\
#4=IFCDIRECTION((0.,0.,1.));\n\
#5=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,#2,$);\n\
{}#16=IFCSPACE('0000000000000000000016',$,$,$,$,#3,#15,$,.ELEMENT.,$,$);\n\
{}#26=IFCSPACE('0000000000000000000026',$,$,$,$,#3,#25,$,.ELEMENT.,$,$);\n\
{}#40=IFCDOOR('0000000000000000000040',$,$,$,$,#3,#35,$,2.1,0.9,$,$,$);\n\
ENDSEC;\nEND-ISO-10303-21;\n",
body(10, 2.0, 2.0, 4.0, 4.0, 3.0),
body(20, 6.2, 2.0, 4.0, 4.0, 3.0),
body(30, 4.1, 2.0, 0.1, 0.9, 2.1),
)
}
#[test]
fn geometry_registers_walkability_and_metric_routing() {
let bytes = rooms_and_door();
let session = import_ifc_session("rooms.ifc", bytes.as_bytes()).unwrap();
let models: super::ModelBytes = session
.snapshots()
.map(|snapshot| (snapshot.source().clone(), bytes.as_bytes().to_vec()))
.collect();
let (session, report) = attach(session, &models).unwrap();
assert_eq!(report.exact, 3, "{report:?}");
let id = |local: &str| ObjectId {
source: session.snapshots().next().unwrap().source().clone(),
local_id: local.to_owned(),
};
let walkability = session.service::<WalkabilityServiceHandle>().unwrap();
let request = |width: f64| {
WalkabilityRequest::try_new(
vec![id("#16"), id("#26")],
vec![id("#40")],
Vec::new(),
width,
None,
true,
false,
)
.unwrap()
};
let wide_enough = walkability.snapshot(&request(0.8)).unwrap();
assert_eq!(
wide_enough.route_between(&id("#16"), &id("#26")).unwrap(),
WalkabilityRouteOutcome::Indeterminate
);
let too_narrow = walkability.snapshot(&request(1.0)).unwrap();
assert_eq!(
too_narrow.route_between(&id("#16"), &id("#26")).unwrap(),
WalkabilityRouteOutcome::Unreachable
);
let routing = session.service::<MetricRoutingServiceHandle>().unwrap();
let route = MetricRouteRequest::new(
MetricPoint::try_new(id("#16"), [1.0, 2.0, 0.0]).unwrap(),
MetricPoint::try_new(id("#26"), [7.0, 2.0, 0.0]).unwrap(),
MobilityProfile::try_new(0.5, 2.0, 0.02, 0.06).unwrap(),
);
assert!(matches!(
routing.route(&route).unwrap(),
MetricRouteOutcome::Blocked(_)
));
}
#[test]
fn only_sharp_sections_are_polygonal() {
use super::polygonal;
use axiolid_profile::{Profile, SectionProfile};
let i = |fillet_radius| {
Profile::Section(SectionProfile::I {
depth: 0.3,
width: 0.3,
web_thickness: 0.011,
flange_thickness: 0.019,
fillet_radius,
flange_edge_radius: None,
flange_slope: Some(0.1),
})
};
assert!(polygonal(&i(None)));
assert!(polygonal(&i(Some(0.0))));
assert!(!polygonal(&i(Some(0.027))));
let c = |internal_fillet_radius| {
Profile::Section(SectionProfile::C {
depth: 0.2,
width: 0.1,
wall_thickness: 0.004,
girth: 0.02,
internal_fillet_radius,
})
};
assert!(polygonal(&c(None)));
assert!(!polygonal(&c(Some(0.004))));
assert!(polygonal(&Profile::Section(SectionProfile::Trapezium {
bottom_x: 0.4,
top_x: 0.2,
y: 0.3,
top_offset: -0.05,
})));
}
}