use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcSchema, IfcType};
use nalgebra::{Point2, Point3, Vector3};
use crate::{
alignment::{AlignmentCurve, AlignmentFrame},
profiles::ProfileProcessor,
router::GeometryProcessor,
scale_segments,
triangulation::triangulate_polygon,
Error, Mesh, Profile2D, Result, TessellationQuality,
};
const MAX_ANGLE_STEP_RAD: f64 = 0.0349;
const MAX_SUBDIVISIONS: usize = 256;
#[derive(Debug, Clone, Copy)]
struct PositionAlongDirectrix {
distance_along: f64,
offset_lateral: f64,
offset_vertical: f64,
offset_longitudinal: f64,
along_horizontal: bool,
}
impl PositionAlongDirectrix {
fn parse(entity: &DecodedEntity) -> Result<Self> {
let distance_along = entity.get_float(0).ok_or_else(|| {
Error::geometry("IfcDistanceExpression.DistanceAlong is required".to_string())
})?;
let offset_lateral = entity.get_float(1).unwrap_or(0.0);
let offset_vertical = entity.get_float(2).unwrap_or(0.0);
let offset_longitudinal = entity.get_float(3).unwrap_or(0.0);
let along_horizontal = entity
.get(4)
.and_then(|v| v.as_enum())
.map(|s| s == "T")
.unwrap_or(true);
Ok(Self {
distance_along,
offset_lateral,
offset_vertical,
offset_longitudinal,
along_horizontal,
})
}
fn horizontal_station(&self, alignment: Option<&AlignmentCurve>) -> f64 {
if self.along_horizontal {
return self.distance_along;
}
let Some(a) = alignment else {
return self.distance_along;
};
let mut station = self.distance_along;
for _ in 0..4 {
let frame = a.evaluate(station);
let proj = (1.0 - frame.tangent.z * frame.tangent.z).sqrt().max(1e-9);
let next = self.distance_along * proj;
if (next - station).abs() < 1e-6 {
return next;
}
station = next;
}
station
}
}
pub struct SectionedSolidHorizontalProcessor {
profile_processor: ProfileProcessor,
}
impl SectionedSolidHorizontalProcessor {
pub fn new(schema: IfcSchema) -> Self {
Self {
profile_processor: ProfileProcessor::new(schema),
}
}
}
impl Default for SectionedSolidHorizontalProcessor {
fn default() -> Self {
Self::new(IfcSchema::new())
}
}
impl GeometryProcessor for SectionedSolidHorizontalProcessor {
fn process(
&self,
entity: &DecodedEntity,
decoder: &mut EntityDecoder,
_schema: &IfcSchema,
quality: TessellationQuality,
) -> Result<Mesh> {
let directrix_id = entity.get_ref(0).ok_or_else(|| {
Error::geometry("IfcSectionedSolidHorizontal missing Directrix".to_string())
})?;
let sections_attr = entity.get(1).ok_or_else(|| {
Error::geometry("IfcSectionedSolidHorizontal missing CrossSections".to_string())
})?;
let sections_list = sections_attr
.as_list()
.ok_or_else(|| Error::geometry("CrossSections must be a list".to_string()))?;
let positions_attr = entity.get(2).ok_or_else(|| {
Error::geometry("IfcSectionedSolidHorizontal missing CrossSectionPositions".to_string())
})?;
let positions_list = positions_attr
.as_list()
.ok_or_else(|| Error::geometry("CrossSectionPositions must be a list".to_string()))?;
if sections_list.len() != positions_list.len() {
return Err(Error::geometry(format!(
"IfcSectionedSolidHorizontal: CrossSections ({}) and CrossSectionPositions ({}) \
must have equal length",
sections_list.len(),
positions_list.len(),
)));
}
if sections_list.len() < 2 {
return Err(Error::geometry(
"IfcSectionedSolidHorizontal needs at least 2 cross-sections to loft".to_string(),
));
}
let fixed_axis_vertical = entity
.get(3)
.and_then(|v| v.as_enum())
.map(|s| s == "T")
.unwrap_or(true);
let directrix_entity = decoder.decode_by_id(directrix_id)?;
let alignment = AlignmentCurve::parse(&directrix_entity, decoder)?;
let mut authored: Vec<(Profile2D, PositionAlongDirectrix)> =
Vec::with_capacity(sections_list.len());
for (sec_attr, pos_attr) in sections_list.iter().zip(positions_list.iter()) {
let sec_id = sec_attr.as_entity_ref().ok_or_else(|| {
Error::geometry("CrossSection must be an entity reference".to_string())
})?;
let sec_entity = decoder.decode_by_id(sec_id)?;
let profile = self
.profile_processor
.process(&sec_entity, decoder, quality)?;
if profile.outer.len() < 3 {
continue; }
let pos_id = pos_attr.as_entity_ref().ok_or_else(|| {
Error::geometry("CrossSectionPosition must be an entity reference".to_string())
})?;
let pos_entity = decoder.decode_by_id(pos_id)?;
let position = PositionAlongDirectrix::parse(&pos_entity)?;
authored.push((profile, position));
}
if authored.len() < 2 {
return Err(Error::geometry(
"IfcSectionedSolidHorizontal: <2 valid stations after filtering degenerate \
cross-sections — nothing to loft"
.to_string(),
));
}
authored.sort_by(|a, b| {
a.1.distance_along
.partial_cmp(&b.1.distance_along)
.unwrap_or(std::cmp::Ordering::Equal)
});
let mut samples: Vec<(Profile2D, PositionAlongDirectrix)> = Vec::new();
samples.push(authored[0].clone());
for i in 1..authored.len() {
let (prev_prof, prev_pos) = (&authored[i - 1].0, authored[i - 1].1);
let (this_prof, this_pos) = (&authored[i].0, authored[i].1);
let n = subdivisions(&prev_pos, &this_pos, alignment.as_ref(), quality);
for k in 1..n {
let t = k as f64 / n as f64;
let interp_profile = interpolate_profile(prev_prof, this_prof, t);
let interp_pos = lerp_position(&prev_pos, &this_pos, t);
samples.push((interp_profile, interp_pos));
}
samples.push(authored[i].clone());
}
let mut rings_3d: Vec<Vec<Point3<f64>>> = Vec::with_capacity(samples.len());
let mut frames: Vec<AlignmentFrame> = Vec::with_capacity(samples.len());
for (profile, pos) in &samples {
let frame = compute_frame(alignment.as_ref(), pos, fixed_axis_vertical);
rings_3d.push(transform_outer(&profile.outer, &frame));
frames.push(frame);
}
let mut mesh = Mesh::new();
emit_cap(
&mut mesh,
&samples[0].0.outer,
&rings_3d[0],
-frames[0].tangent,
false,
)?;
for i in 1..samples.len() {
let (prev_profile, _) = &samples[i - 1];
let (this_profile, _) = &samples[i];
let prev_ring = &rings_3d[i - 1];
let this_ring = &rings_3d[i];
if prev_profile.outer.len() == this_profile.outer.len()
&& !prev_profile.outer.is_empty()
{
emit_side_walls(&mut mesh, prev_ring, this_ring);
} else {
emit_cap(
&mut mesh,
&prev_profile.outer,
prev_ring,
frames[i - 1].tangent,
true,
)?;
emit_cap(
&mut mesh,
&this_profile.outer,
this_ring,
-frames[i].tangent,
false,
)?;
}
}
let last = samples.len() - 1;
emit_cap(
&mut mesh,
&samples[last].0.outer,
&rings_3d[last],
frames[last].tangent,
true,
)?;
Ok(mesh)
}
fn supported_types(&self) -> Vec<IfcType> {
vec![IfcType::IfcSectionedSolidHorizontal]
}
}
fn straight_y_frame(station: f64) -> AlignmentFrame {
AlignmentFrame {
origin: Point3::new(0.0, station, 0.0),
right: Vector3::new(1.0, 0.0, 0.0),
up: Vector3::new(0.0, 0.0, 1.0),
tangent: Vector3::new(0.0, 1.0, 0.0),
}
}
fn evaluate_alignment(alignment: Option<&AlignmentCurve>, station: f64) -> AlignmentFrame {
match alignment {
Some(a) => a.evaluate(station),
None => straight_y_frame(station),
}
}
fn compute_frame(
alignment: Option<&AlignmentCurve>,
pos: &PositionAlongDirectrix,
fixed_axis_vertical: bool,
) -> AlignmentFrame {
let station = pos.horizontal_station(alignment);
let base = evaluate_alignment(alignment, station);
let (mut right, mut up) = if fixed_axis_vertical {
(base.right, base.up)
} else {
let world_z = Vector3::new(0.0, 0.0, 1.0);
let right_candidate = base.tangent.cross(&world_z);
let right_3d = match right_candidate.try_normalize(1e-9) {
Some(r) => r,
None => Vector3::new(1.0, 0.0, 0.0),
};
let up_3d = right_3d.cross(&base.tangent).normalize();
(right_3d, up_3d)
};
if let Some(a) = alignment {
let roll = a.cant_angle(station);
if roll.abs() > 1e-9 {
let (sin_r, cos_r) = roll.sin_cos();
let new_right = right * cos_r + up * sin_r;
let new_up = -right * sin_r + up * cos_r;
right = new_right;
up = new_up;
}
}
let origin = base.origin
+ base.tangent * pos.offset_longitudinal
+ right * pos.offset_lateral
+ up * pos.offset_vertical;
AlignmentFrame {
origin,
right,
up,
tangent: base.tangent,
}
}
fn transform_outer(outer: &[Point2<f64>], frame: &AlignmentFrame) -> Vec<Point3<f64>> {
outer
.iter()
.map(|p| frame.origin + frame.right * p.x + frame.up * p.y)
.collect()
}
fn subdivisions(
a: &PositionAlongDirectrix,
b: &PositionAlongDirectrix,
alignment: Option<&AlignmentCurve>,
quality: TessellationQuality,
) -> usize {
let span = (b.distance_along - a.distance_along).abs();
if span < 1e-9 {
return 1;
}
let Some(curve) = alignment else {
return 1; };
let s_a = a.horizontal_station(Some(curve));
let s_b = b.horizontal_station(Some(curve));
const PROBES: usize = 16;
let mut total_angle = 0.0;
let mut prev_tan: Option<Vector3<f64>> = None;
for i in 0..=PROBES {
let t = i as f64 / PROBES as f64;
let s = s_a + (s_b - s_a) * t;
let tan = curve.evaluate(s).tangent;
if let Some(prev) = prev_tan {
let cos_a = prev.dot(&tan).clamp(-1.0, 1.0);
total_angle += cos_a.acos();
}
prev_tan = Some(tan);
}
let n_base = (total_angle / MAX_ANGLE_STEP_RAD).ceil() as usize;
scale_segments(n_base, 1, MAX_SUBDIVISIONS, quality)
}
fn interpolate_profile(a: &Profile2D, b: &Profile2D, t: f64) -> Profile2D {
if a.outer.len() != b.outer.len() || a.outer.is_empty() {
return if t < 0.5 { a.clone() } else { b.clone() };
}
let outer: Vec<Point2<f64>> = a
.outer
.iter()
.zip(b.outer.iter())
.map(|(pa, pb)| {
Point2::new(
pa.x * (1.0 - t) + pb.x * t,
pa.y * (1.0 - t) + pb.y * t,
)
})
.collect();
let mut result = Profile2D::new(outer);
if a.holes.len() == b.holes.len() {
for (ha, hb) in a.holes.iter().zip(b.holes.iter()) {
if ha.len() == hb.len() {
let hole: Vec<Point2<f64>> = ha
.iter()
.zip(hb.iter())
.map(|(pa, pb)| {
Point2::new(
pa.x * (1.0 - t) + pb.x * t,
pa.y * (1.0 - t) + pb.y * t,
)
})
.collect();
result.add_hole(hole);
}
}
}
result
}
fn lerp_position(
a: &PositionAlongDirectrix,
b: &PositionAlongDirectrix,
t: f64,
) -> PositionAlongDirectrix {
PositionAlongDirectrix {
distance_along: a.distance_along * (1.0 - t) + b.distance_along * t,
offset_lateral: a.offset_lateral * (1.0 - t) + b.offset_lateral * t,
offset_vertical: a.offset_vertical * (1.0 - t) + b.offset_vertical * t,
offset_longitudinal: a.offset_longitudinal * (1.0 - t)
+ b.offset_longitudinal * t,
along_horizontal: a.along_horizontal,
}
}
fn emit_cap(
mesh: &mut Mesh,
outer_2d: &[Point2<f64>],
ring_3d: &[Point3<f64>],
normal: Vector3<f64>,
forward: bool,
) -> Result<()> {
if outer_2d.len() < 3 || ring_3d.len() != outer_2d.len() {
return Ok(());
}
let indices = triangulate_polygon(outer_2d)?;
let base = (mesh.positions.len() / 3) as u32;
for p in ring_3d {
mesh.add_vertex(*p, normal);
}
for tri in indices.chunks_exact(3) {
let (a, b, c) = (tri[0] as u32, tri[1] as u32, tri[2] as u32);
if forward {
mesh.add_triangle(base + a, base + b, base + c);
} else {
mesh.add_triangle(base + a, base + c, base + b);
}
}
Ok(())
}
fn emit_side_walls(mesh: &mut Mesh, prev_ring: &[Point3<f64>], this_ring: &[Point3<f64>]) {
let n = prev_ring.len();
if n < 2 || this_ring.len() != n {
return;
}
for j in 0..n {
let j1 = (j + 1) % n;
let p0 = prev_ring[j];
let p1 = prev_ring[j1];
let p2 = this_ring[j1];
let p3 = this_ring[j];
let n_face = compute_face_normal(&p0, &p1, &p2);
let v_base = (mesh.positions.len() / 3) as u32;
mesh.add_vertex(p0, n_face);
mesh.add_vertex(p1, n_face);
mesh.add_vertex(p2, n_face);
mesh.add_vertex(p3, n_face);
mesh.add_triangle(v_base, v_base + 1, v_base + 2);
mesh.add_triangle(v_base, v_base + 2, v_base + 3);
}
}
fn compute_face_normal(a: &Point3<f64>, b: &Point3<f64>, c: &Point3<f64>) -> Vector3<f64> {
let ab = b - a;
let ac = c - a;
let n = ab.cross(&ac);
let len = n.norm();
if len > 1e-12 {
n / len
} else {
Vector3::new(0.0, 0.0, 1.0)
}
}