use axiolid_model::{
GeometryNode, NodeId, SectionAtStation, SolidOperation, StationFrame, SurfaceRelation,
};
use ifc_model::EntityId;
use crate::error::GeometryResult;
use crate::input::profile::{describe_profile, ProfileParameters};
use crate::lower::profile::{lower_open_profile_node, lower_profile_node};
use crate::lower::session::LoweringSession;
use crate::lower::station::axes::{read_placement, section_orientation, SectionOf};
use crate::lower::station::read::distance_expression;
use crate::lower::station::Basis;
use crate::transform::Transform;
pub(crate) const SECTIONED_SOLID_HORIZONTAL_TYPE: &str = "IFCSECTIONEDSOLIDHORIZONTAL";
pub(crate) const SECTIONED_SURFACE_TYPE: &str = "IFCSECTIONEDSURFACE";
const KIND: &str = "sectioned";
const OTHER_BASIS: &str =
"a CrossSectionPositions member is measured along another BasisCurve than the Directrix";
const OVER_SEAM: &str =
"the sections span a tangent discontinuity of the directrix, where IFC4.3 ADD2 \
(8.8.3.35.1) mitres at half angle and the neutral sections interpolate in the frame at \
each distance, which turns there without a mitre";
const MIXED_TAGS: &str =
"some sections state Tags and others do not, so IFC4.3 ADD2 names no pairing between them";
const BRANCHING: &str =
"the sections carry different Tags (branching longitudinal breaklines, 8.8.3.37.1); the \
neutral sectioned surface joins one set of tags through every section";
const TAG_ORDER: &str =
"a section repeats a tag, or orders its tags other than the first section's or its \
reverse, so joining by tag would cross the sheet";
pub(crate) fn lower_sectioned_solid_horizontal_node(
session: &mut LoweringSession<'_>,
id: EntityId,
frame: Transform,
) -> GeometryResult<NodeId> {
memoized(session, id, frame, |session| {
sectioned(session, id, frame, SectionOf::SolidHorizontal)
})
}
pub(crate) fn lower_sectioned_surface_node(
session: &mut LoweringSession<'_>,
id: EntityId,
frame: Transform,
) -> GeometryResult<NodeId> {
memoized(session, id, frame, |session| {
sectioned(session, id, frame, SectionOf::Surface)
})
}
pub(crate) fn sectioned_surface(
session: &mut LoweringSession<'_>,
id: EntityId,
frame: Transform,
) -> GeometryResult<NodeId> {
sectioned(session, id, frame, SectionOf::Surface)
}
fn sectioned(
session: &mut LoweringSession<'_>,
id: EntityId,
frame: Transform,
of: SectionOf,
) -> GeometryResult<NodeId> {
let (owner_type, positions_slot, sections_slot) = match of {
SectionOf::SolidHorizontal => (SECTIONED_SOLID_HORIZONTAL_TYPE, 2, 1),
SectionOf::Surface => (SECTIONED_SURFACE_TYPE, 1, 2),
};
let slots = session.slots(id)?;
let directrix = slots.req_ref(0, "Directrix")?;
let positions = slots.req_ref_list(positions_slot, "CrossSectionPositions")?;
let profiles = slots.req_ref_list(sections_slot, "CrossSections")?;
if positions.len() != profiles.len() {
return Err(session.degenerate(
id,
owner_type,
format!(
"CorrespondingSectionPositions: {} sections and {} positions",
profiles.len(),
positions.len()
),
));
}
if profiles.len() < 2 {
return Err(session.degenerate(id, owner_type, "CrossSections is LIST [2:?]"));
}
let first_type = session.type_name(profiles[0])?;
for profile in &profiles[1..] {
if session.type_name(*profile)? != first_type {
return Err(session.degenerate(
id,
owner_type,
"SectionsSameType: every cross section must be of one entity type",
));
}
}
let basis = Basis::lower(session, id, owner_type, directrix, frame)?;
let mut sections: Vec<SectionAtStation> = Vec::with_capacity(profiles.len());
for (position, profile) in positions.iter().zip(&profiles) {
let placement = read_placement(session, *position)?;
let expression = distance_expression(session, id, owner_type, placement.location)?;
if expression.basis != directrix {
return Err(session.unsupported(id, owner_type, OTHER_BASIS));
}
match of {
SectionOf::SolidHorizontal if expression.longitudinal.is_some() => {
return Err(session.degenerate(
id,
owner_type,
"NoLongitudinalOffsets: a cross section position states OffsetLongitudinal",
))
}
SectionOf::Surface if expression.states_an_offset() => {
return Err(session.degenerate(
id,
owner_type,
"NoOffsets: a cross section position states an offset",
))
}
_ => {}
}
basis.check_on_curve(session, id, owner_type, expression.distance)?;
if let Some(previous) = sections.last() {
if expression.distance <= previous.station.distance {
return Err(session.degenerate(
id,
owner_type,
"CrossSectionPositions must be in strictly increasing order along the \
Directrix",
));
}
}
let orientation = section_orientation(session, id, &placement)?;
let (node, tags) = match of {
SectionOf::SolidHorizontal => (lower_profile_node(session, *profile)?, Vec::new()),
SectionOf::Surface => (
lower_open_profile_node(session, *profile)?,
open_tags(session, *profile)?,
),
};
sections.push(
SectionAtStation::new(node, expression.station())
.with_tags(tags)
.with_orientation(orientation),
);
}
check_tags(session, id, owner_type, §ions)?;
let from = sections[0].station.distance;
let to = sections[sections.len() - 1].station.distance;
basis.check_no_seam(session, id, owner_type, from, to, OVER_SEAM)?;
let node = match of {
SectionOf::SolidHorizontal => {
GeometryNode::SolidOperation(SolidOperation::SectionsAtStations {
directrix: basis.node,
sections,
frame: StationFrame::Section,
})
}
SectionOf::Surface => {
GeometryNode::SurfaceRelation(SurfaceRelation::OpenSectionsAtStations {
directrix: basis.node,
sections,
frame: StationFrame::Section,
})
}
};
session.node_for(id, node)
}
fn open_tags(session: &LoweringSession<'_>, profile: EntityId) -> GeometryResult<Vec<String>> {
let description = describe_profile(session.model(), session.units(), profile)?;
Ok(match description.parameters {
ProfileParameters::OpenCross {
tags: Some(tags), ..
} => tags,
_ => Vec::new(),
})
}
fn check_tags(
session: &LoweringSession<'_>,
id: EntityId,
owner_type: &str,
sections: &[SectionAtStation],
) -> GeometryResult<()> {
let first = §ions[0].tags;
if sections
.iter()
.any(|section| section.tags.is_empty() != first.is_empty())
{
return Err(session.unsupported(id, owner_type, MIXED_TAGS));
}
let sorted = |tags: &[String]| {
let mut set = tags.to_vec();
set.sort_unstable();
set
};
let expected = sorted(first);
if expected.windows(2).any(|pair| pair[0] == pair[1]) {
return Err(session.unsupported(id, owner_type, TAG_ORDER));
}
for section in §ions[1..] {
if sorted(§ion.tags) != expected {
return Err(session.unsupported(id, owner_type, BRANCHING));
}
if section.tags != *first && !section.tags.iter().eq(first.iter().rev()) {
return Err(session.unsupported(id, owner_type, TAG_ORDER));
}
}
Ok(())
}
fn memoized(
session: &mut LoweringSession<'_>,
id: EntityId,
frame: Transform,
build: impl FnOnce(&mut LoweringSession<'_>) -> GeometryResult<NodeId>,
) -> GeometryResult<NodeId> {
if let Some(node) = session.memoized(id, KIND, frame) {
return Ok(node);
}
session.enter(id, KIND)?;
let result = build(session);
session.exit(id);
let node = result?;
session.memoize(id, KIND, frame, node);
Ok(node)
}