use std::collections::BTreeMap;
use axiolid_core::Point3;
use axiolid_model::{GeometryNode, NodeId};
use axiolid_topology::{
BRep, Edge, EdgeId, EdgeUse, Face, FaceBound, Loop, Orientation, Shell, ShellId, Solid, Vertex,
VertexId,
};
use ifc_model::EntityId;
use crate::error::{GeometryError, GeometryResult};
use crate::lower::curve::lower_curve_node;
use crate::lower::session::{DegenerateFacePolicy, LoweringSession};
use crate::lower::surface::lower_surface_node;
use crate::resource::point::CartesianPoint;
use crate::resource::topology::{
expect_type, ConnectedFaceSet, EdgeCurve as EdgeCurveView, EdgeLoop as EdgeLoopView,
Face as FaceView, FaceBound as FaceBoundView, FaceSurface as FaceSurfaceView,
ManifoldSolidBrep, OrientedEdge as OrientedEdgeView, PolyLoop, Subedge as SubedgeView,
VertexPoint as VertexPointView,
};
use crate::select::is_a;
use crate::transform::Transform;
const KIND: &str = "faceted brep";
struct TopologyBuilder {
brep: BRep<NodeId>,
vertices: BTreeMap<EntityId, VertexId>,
edges: BTreeMap<(usize, usize), EdgeId>,
curved_edges: BTreeMap<EntityId, EdgeUse<NodeId>>,
}
impl TopologyBuilder {
fn new() -> Self {
Self {
brep: BRep::default(),
vertices: BTreeMap::new(),
edges: BTreeMap::new(),
curved_edges: BTreeMap::new(),
}
}
fn vertex(&mut self, point: EntityId, position: Point3) -> VertexId {
if let Some(existing) = self.vertices.get(&point) {
return *existing;
}
let id = self.brep.add_vertex(Vertex { position });
self.vertices.insert(point, id);
id
}
fn edge(&mut self, start: VertexId, end: VertexId) -> EdgeUse<NodeId> {
let (low, high) = (start.index(), end.index());
let forward = low <= high;
let key = if forward { (low, high) } else { (high, low) };
let edge = *self.edges.entry(key).or_insert_with(|| {
let (a, b) = if forward { (start, end) } else { (end, start) };
self.brep.add_edge(Edge {
start: a,
end: b,
curve: None,
})
});
EdgeUse {
edge,
orientation: if forward {
Orientation::Forward
} else {
Orientation::Reversed
},
pcurve: None,
}
}
}
pub fn lower_faceted_brep_node(
session: &mut LoweringSession<'_>,
id: EntityId,
frame: Transform,
) -> GeometryResult<NodeId> {
if let Some(node) = session.memoized(id, KIND, frame) {
return Ok(node);
}
session.enter(id, KIND)?;
let result = build(session, id, frame);
session.exit(id);
let node = result?;
session.memoize(id, KIND, frame, node);
Ok(node)
}
fn build(
session: &mut LoweringSession<'_>,
id: EntityId,
frame: Transform,
) -> GeometryResult<NodeId> {
let entity = session.entity(id, id)?;
let view = ManifoldSolidBrep::new(id, entity);
let outer_ref = view.outer()?;
let void_refs = view.voids()?;
let mut builder = TopologyBuilder::new();
let outer = shell(session, &mut builder, id, outer_ref, frame)?;
let mut voids = Vec::with_capacity(void_refs.len());
for void_ref in void_refs {
voids.push(shell(session, &mut builder, id, void_ref, frame)?);
}
builder.brep.add_solid(Solid { outer, voids });
session.node_for(id, GeometryNode::BRep(builder.brep))
}
pub fn lower_shell_node(
session: &mut LoweringSession<'_>,
referrer: EntityId,
id: EntityId,
frame: Transform,
) -> GeometryResult<NodeId> {
if let Some(node) = session.memoized(id, KIND, frame) {
return Ok(node);
}
session.enter(id, KIND)?;
let mut builder = TopologyBuilder::new();
let result = shell(session, &mut builder, referrer, id, frame);
session.exit(id);
result?;
let node = session.node_for(id, GeometryNode::BRep(builder.brep))?;
session.memoize(id, KIND, frame, node);
Ok(node)
}
const FACE_KIND: &str = "face surface";
pub fn lower_face_surface_node(
session: &mut LoweringSession<'_>,
id: EntityId,
frame: Transform,
) -> GeometryResult<NodeId> {
if let Some(node) = session.memoized(id, FACE_KIND, frame) {
return Ok(node);
}
let entity = expect_type(
session.model(),
id,
id,
&["IFCFACESURFACE", "IFCADVANCEDFACE"],
"IfcFaceSurface",
)?;
session.enter(id, FACE_KIND)?;
let mut builder = TopologyBuilder::new();
let result = face(session, &mut builder, id, id, frame);
session.exit(id);
let Some(face_id) = result? else {
return Err(session.degenerate(
id,
&entity.type_name.to_ascii_uppercase(),
"the face's outer bound collapsed and the face was dropped",
));
};
builder.brep.add_shell(Shell {
faces: vec![(face_id, Orientation::Forward)],
closed: false,
});
let node = session.node_for(id, GeometryNode::BRep(builder.brep))?;
session.memoize(id, FACE_KIND, frame, node);
Ok(node)
}
fn shell(
session: &mut LoweringSession<'_>,
builder: &mut TopologyBuilder,
referrer: EntityId,
id: EntityId,
frame: Transform,
) -> GeometryResult<ShellId> {
let entity = expect_type(
session.model(),
referrer,
id,
&["IFCCLOSEDSHELL", "IFCOPENSHELL", "IFCCONNECTEDFACESET"],
"IfcConnectedFaceSet",
)?;
let view = ConnectedFaceSet::new(id, entity);
let closed = view.is_closed();
let mut faces = Vec::new();
for face_ref in view.faces()? {
if let Some(face_id) = face(session, builder, id, face_ref, frame)? {
faces.push((face_id, Orientation::Forward));
}
}
if faces.is_empty() {
return Err(session.degenerate(
id,
"IFCCONNECTEDFACESET",
"every face of the shell collapsed and was dropped".to_string(),
));
}
Ok(builder.brep.add_shell(Shell { faces, closed }))
}
fn face(
session: &mut LoweringSession<'_>,
builder: &mut TopologyBuilder,
referrer: EntityId,
id: EntityId,
frame: Transform,
) -> GeometryResult<Option<axiolid_topology::FaceId>> {
let entity = expect_type(
session.model(),
referrer,
id,
&["IFCFACE", "IFCFACESURFACE", "IFCADVANCEDFACE"],
"IfcFace",
)?;
let view = FaceView::new(id, entity);
let bound_refs = view.bounds()?;
if session.face_policy() == DegenerateFacePolicy::DropAndReport {
let mut collapsed_outer = false;
for &bound_ref in &bound_refs {
if collapsed_poly_loop(session, bound_ref)?.is_none() {
continue;
}
let bound_entity = session.entity(id, bound_ref)?;
if bound_refs.len() == 1 || FaceBoundView::new(bound_ref, bound_entity).is_outer() {
collapsed_outer = true;
}
}
if collapsed_outer {
session.report_dropped_face(id);
return Ok(None);
}
}
let mut bounds = Vec::new();
for bound_ref in bound_refs {
bounds.push(bound(session, builder, id, bound_ref, frame)?);
}
let (surface, orientation) = if session.type_name(id)?.eq_ignore_ascii_case("IFCFACE") {
(None, Orientation::Forward)
} else {
let surface_view = FaceSurfaceView::new(id, entity);
let surface_ref = surface_view.face_surface()?;
let surface_type = &session.entity(id, surface_ref)?.type_name;
if !is_a(&surface_type.to_ascii_uppercase(), "IFCSURFACE") {
return Err(GeometryError::WrongEntityType {
entity: surface_ref,
actual: surface_type.to_string(),
expected: "IfcSurface",
});
}
let node = lower_surface_node(session, surface_ref, frame)?;
let sense = if surface_view.same_sense() {
Orientation::Forward
} else {
Orientation::Reversed
};
(Some(node), sense)
};
Ok(Some(builder.brep.add_face(Face {
surface,
bounds,
orientation,
})))
}
fn collapsed_poly_loop(
session: &LoweringSession<'_>,
bound_ref: EntityId,
) -> GeometryResult<Option<EntityId>> {
let Some(bound_entity) = session.model().get(bound_ref) else {
return Ok(None);
};
let Ok(loop_ref) = FaceBoundView::new(bound_ref, bound_entity).bound() else {
return Ok(None);
};
let Some(loop_entity) = session.model().get(loop_ref) else {
return Ok(None);
};
if !loop_entity.type_name.eq_ignore_ascii_case("IFCPOLYLOOP") {
return Ok(None);
}
let Ok(points) = PolyLoop::new(loop_ref, loop_entity).polygon() else {
return Ok(None);
};
Ok((distinct_edge_count(&points) < 3).then_some(loop_ref))
}
fn distinct_edge_count(points: &[EntityId]) -> usize {
(0..points.len())
.filter(|&index| points[index] != points[(index + 1) % points.len()])
.count()
}
fn bound(
session: &mut LoweringSession<'_>,
builder: &mut TopologyBuilder,
referrer: EntityId,
id: EntityId,
frame: Transform,
) -> GeometryResult<FaceBound> {
let entity = expect_type(
session.model(),
referrer,
id,
&["IFCFACEBOUND", "IFCFACEOUTERBOUND"],
"IfcFaceBound",
)?;
let view = FaceBoundView::new(id, entity);
let bound_ref = view.bound()?;
let bound_type = session.type_name(bound_ref)?;
for (name, rationale) in [
(
"IFCVERTEXLOOP",
"a single-vertex loop bounds zero area, so it contributes no face bound",
),
(
"IFCLOOP",
"generic loop has no concrete point or edge representation",
),
("IFCPATH", "path topology is not a face-bound loop"),
] {
if bound_type.eq_ignore_ascii_case(name) {
return Err(session.unsupported(bound_ref, &bound_type, rationale));
}
}
let loop_id = if bound_type.eq_ignore_ascii_case("IFCEDGELOOP") {
edge_loop(session, builder, id, bound_ref, frame)?
} else {
poly_loop(session, builder, id, bound_ref, frame)?
};
Ok(FaceBound {
loop_id,
orientation: if view.orientation()? {
Orientation::Forward
} else {
Orientation::Reversed
},
outer: view.is_outer(),
})
}
fn poly_loop(
session: &mut LoweringSession<'_>,
builder: &mut TopologyBuilder,
referrer: EntityId,
id: EntityId,
frame: Transform,
) -> GeometryResult<axiolid_topology::LoopId> {
let entity = expect_type(
session.model(),
referrer,
id,
&["IFCPOLYLOOP"],
"IfcPolyLoop",
)?;
let view = PolyLoop::new(id, entity);
let points = view.polygon()?;
let mut vertices = Vec::with_capacity(points.len());
for point_ref in &points {
let point_entity = session.entity(id, *point_ref)?;
let raw = CartesianPoint::new(*point_ref, point_entity).coordinates_3d()?;
let scaled = raw.map(|value| session.units().length(value));
let placed = frame.apply(scaled);
vertices.push(builder.vertex(*point_ref, Point3::from_array(placed)));
}
let mut edges = Vec::with_capacity(vertices.len());
for (index, start) in vertices.iter().enumerate() {
let end = vertices[(index + 1) % vertices.len()];
if *start == end {
continue;
}
edges.push(builder.edge(*start, end));
}
if edges.len() < 3 {
return Err(session.degenerate(
id,
"IFCPOLYLOOP",
format!("loop collapses to {} distinct edges", edges.len()),
));
}
Ok(builder.brep.add_loop(Loop { edges }))
}
fn edge_loop(
session: &mut LoweringSession<'_>,
builder: &mut TopologyBuilder,
referrer: EntityId,
id: EntityId,
frame: Transform,
) -> GeometryResult<axiolid_topology::LoopId> {
let entity = expect_type(
session.model(),
referrer,
id,
&["IFCEDGELOOP"],
"IfcEdgeLoop",
)?;
let view = EdgeLoopView::new(id, entity);
let mut edges = Vec::new();
for oriented_ref in view.edge_list()? {
edges.push(oriented_edge(session, builder, id, oriented_ref, frame)?);
}
Ok(builder.brep.add_loop(Loop { edges }))
}
fn oriented_edge(
session: &mut LoweringSession<'_>,
builder: &mut TopologyBuilder,
referrer: EntityId,
id: EntityId,
frame: Transform,
) -> GeometryResult<EdgeUse<NodeId>> {
let entity = expect_type(
session.model(),
referrer,
id,
&["IFCORIENTEDEDGE"],
"IfcOrientedEdge",
)?;
let view = OrientedEdgeView::new(id, entity);
let base = edge_of_any_kind(session, builder, id, view.edge_element()?, frame)?;
if view.orientation() {
Ok(base)
} else {
Ok(EdgeUse {
edge: base.edge,
orientation: flip(base.orientation),
pcurve: base.pcurve,
})
}
}
fn edge_curve(
session: &mut LoweringSession<'_>,
builder: &mut TopologyBuilder,
referrer: EntityId,
id: EntityId,
frame: Transform,
) -> GeometryResult<EdgeUse<NodeId>> {
if let Some(existing) = builder.curved_edges.get(&id) {
return Ok(*existing);
}
let entity = expect_type(
session.model(),
referrer,
id,
&["IFCEDGE", "IFCEDGECURVE"],
"IfcEdge",
)?;
let view = EdgeCurveView::new(id, entity);
let start = topological_vertex(session, builder, id, view.start()?, frame)?;
let end = topological_vertex(session, builder, id, view.end()?, frame)?;
let (curve, pcurve) = match view.edge_geometry() {
Some(curve_ref) if session.type_name(curve_ref)? == "IFCPCURVE" => {
(None, Some(lower_curve_node(session, curve_ref, frame)?))
}
Some(curve_ref) => (Some(lower_curve_node(session, curve_ref, frame)?), None),
None => (None, None),
};
let orientation = if view.same_sense() {
Orientation::Forward
} else {
Orientation::Reversed
};
let edge = builder.brep.add_edge(Edge { start, end, curve });
let use_ = EdgeUse {
edge,
orientation,
pcurve,
};
builder.curved_edges.insert(id, use_);
Ok(use_)
}
fn subedge(
session: &mut LoweringSession<'_>,
builder: &mut TopologyBuilder,
referrer: EntityId,
id: EntityId,
frame: Transform,
) -> GeometryResult<EdgeUse<NodeId>> {
if let Some(existing) = builder.curved_edges.get(&id) {
return Ok(*existing);
}
let entity = expect_type(session.model(), referrer, id, &["IFCSUBEDGE"], "IfcSubedge")?;
let view = SubedgeView::new(id, entity);
let start = topological_vertex(session, builder, id, view.start()?, frame)?;
let end = topological_vertex(session, builder, id, view.end()?, frame)?;
let parent = edge_of_any_kind(session, builder, id, view.parent_edge()?, frame)?;
let curve = builder
.brep
.edges()
.get(parent.edge.index())
.and_then(|parent_edge| parent_edge.curve);
let edge = builder.brep.add_edge(Edge { start, end, curve });
let use_ = EdgeUse {
edge,
orientation: parent.orientation,
pcurve: parent.pcurve,
};
builder.curved_edges.insert(id, use_);
Ok(use_)
}
fn edge_of_any_kind(
session: &mut LoweringSession<'_>,
builder: &mut TopologyBuilder,
referrer: EntityId,
id: EntityId,
frame: Transform,
) -> GeometryResult<EdgeUse<NodeId>> {
if session.type_name(id)? == "IFCSUBEDGE" {
subedge(session, builder, referrer, id, frame)
} else {
edge_curve(session, builder, referrer, id, frame)
}
}
fn topological_vertex(
session: &mut LoweringSession<'_>,
builder: &mut TopologyBuilder,
referrer: EntityId,
id: EntityId,
frame: Transform,
) -> GeometryResult<VertexId> {
let type_name = session.type_name(id)?;
if type_name.eq_ignore_ascii_case("IFCVERTEX") {
return Err(session.unsupported(
id,
&type_name,
"generic vertex carries no point geometry",
));
}
let entity = expect_type(
session.model(),
referrer,
id,
&["IFCVERTEXPOINT"],
"IfcVertexPoint",
)?;
let point_ref = VertexPointView::new(id, entity).vertex_geometry()?;
let point_entity = session.entity(id, point_ref)?;
let raw = CartesianPoint::new(point_ref, point_entity).coordinates_3d()?;
let scaled = raw.map(|value| session.units().length(value));
let placed = frame.apply(scaled);
Ok(builder.vertex(id, Point3::from_array(placed)))
}
fn flip(orientation: Orientation) -> Orientation {
match orientation {
Orientation::Forward => Orientation::Reversed,
Orientation::Reversed => Orientation::Forward,
}
}