use crate::triangulation::{calculate_polygon_normal, project_to_2d};
use crate::{Error, Point3, Result};
use ifc_lite_core::{DecodedEntity, EntityDecoder};
use nalgebra::Matrix4;
use super::helpers::get_axis2_placement_transform_by_id;
pub(super) fn process_advanced_face(
face: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<(Vec<f32>, Vec<u32>)> {
let surface_attr = face
.get(1)
.ok_or_else(|| Error::geometry("AdvancedFace missing FaceSurface".to_string()))?;
let surface = decoder
.resolve_ref(surface_attr)?
.ok_or_else(|| Error::geometry("Failed to resolve FaceSurface".to_string()))?;
let surface_type = surface.ifc_type.as_str().to_uppercase();
let same_sense = face
.get(2)
.and_then(|a| a.as_enum())
.map(|e| e == "T" || e == "TRUE")
.unwrap_or(true);
let result = if surface_type == "IFCPLANE" {
process_planar_face(face, decoder)
} else if surface_type == "IFCBSPLINESURFACEWITHKNOTS" {
process_bspline_face(&surface, decoder, None)
} else if surface_type == "IFCRATIONALBSPLINESURFACEWITHKNOTS" {
let weights = parse_rational_weights(&surface);
process_bspline_face(&surface, decoder, weights.as_deref())
} else if surface_type == "IFCCYLINDRICALSURFACE" {
process_cylindrical_face(face, &surface, decoder)
} else if surface_type == "IFCSURFACEOFREVOLUTION" {
process_surface_of_revolution_face(face, &surface, decoder)
} else if surface_type == "IFCSURFACEOFLINEAREXTRUSION"
|| surface_type == "IFCCONICALSURFACE"
|| surface_type == "IFCSPHERICALSURFACE"
|| surface_type == "IFCTOROIDALSURFACE"
{
process_planar_face(face, decoder)
} else {
#[cfg(feature = "debug_geometry")]
eprintln!(
"[ifc-lite][advanced_face] face #{} unsupported surface {}",
face.id, surface_type
);
Ok((Vec::new(), Vec::new()))
};
#[cfg(feature = "debug_geometry")]
{
if let Ok((ref pos, ref idx)) = result {
if pos.is_empty() || idx.is_empty() {
eprintln!(
"[ifc-lite][advanced_face] face #{} surface={} produced 0 tris (verts={}, idx={})",
face.id,
surface_type,
pos.len() / 3,
idx.len() / 3,
);
}
}
}
if !same_sense {
result.map(|(positions, mut indices)| {
for tri in indices.chunks_exact_mut(3) {
tri.swap(0, 2);
}
(positions, indices)
})
} else {
result
}
}
#[inline]
fn bspline_basis(i: usize, p: usize, u: f64, knots: &[f64]) -> f64 {
if p == 0 {
if knots[i] <= u && u < knots[i + 1] {
1.0
} else {
0.0
}
} else {
let left = {
let denom = knots[i + p] - knots[i];
if denom.abs() < 1e-10 {
0.0
} else {
(u - knots[i]) / denom * bspline_basis(i, p - 1, u, knots)
}
};
let right = {
let denom = knots[i + p + 1] - knots[i + 1];
if denom.abs() < 1e-10 {
0.0
} else {
(knots[i + p + 1] - u) / denom * bspline_basis(i + 1, p - 1, u, knots)
}
};
left + right
}
}
fn evaluate_bspline_surface(
u: f64,
v: f64,
u_degree: usize,
v_degree: usize,
control_points: &[Vec<Point3<f64>>],
u_knots: &[f64],
v_knots: &[f64],
weights: Option<&[Vec<f64>]>,
) -> Point3<f64> {
let mut result = Point3::new(0.0, 0.0, 0.0);
let mut weight_sum = 0.0;
for (i, row) in control_points.iter().enumerate() {
let n_i = bspline_basis(i, u_degree, u, u_knots);
for (j, cp) in row.iter().enumerate() {
let n_j = bspline_basis(j, v_degree, v, v_knots);
let basis = n_i * n_j;
if basis.abs() > 1e-10 {
let w = weights
.and_then(|ws| ws.get(i))
.and_then(|row_w| row_w.get(j))
.copied()
.unwrap_or(1.0);
let weighted_basis = basis * w;
result.x += weighted_basis * cp.x;
result.y += weighted_basis * cp.y;
result.z += weighted_basis * cp.z;
weight_sum += weighted_basis;
}
}
}
if weights.is_some() && weight_sum.abs() > 1e-10 {
result.x /= weight_sum;
result.y /= weight_sum;
result.z /= weight_sum;
}
result
}
fn tessellate_bspline_surface(
u_degree: usize,
v_degree: usize,
control_points: &[Vec<Point3<f64>>],
u_knots: &[f64],
v_knots: &[f64],
weights: Option<&[Vec<f64>]>,
u_segments: usize,
v_segments: usize,
) -> Option<(Vec<f32>, Vec<u32>)> {
let mut positions = Vec::new();
let mut indices = Vec::new();
let n_u = control_points.len();
let n_v = control_points.first().map_or(0, |r| r.len());
let min_u_knots = n_u + u_degree + 1;
let min_v_knots = n_v + v_degree + 1;
if u_knots.len() < min_u_knots || v_knots.len() < min_v_knots {
return None;
}
if u_degree >= u_knots.len() || v_degree >= v_knots.len() {
return None;
}
if u_knots.len() - u_degree - 1 >= u_knots.len()
|| v_knots.len() - v_degree - 1 >= v_knots.len()
{
return None;
}
let u_min = u_knots[u_degree];
let u_max = u_knots[u_knots.len() - u_degree - 1];
let v_min = v_knots[v_degree];
let v_max = v_knots[v_knots.len() - v_degree - 1];
for i in 0..=u_segments {
let u = u_min + (u_max - u_min) * (i as f64 / u_segments as f64);
let u = u.min(u_max - 1e-6).max(u_min);
for j in 0..=v_segments {
let v = v_min + (v_max - v_min) * (j as f64 / v_segments as f64);
let v = v.min(v_max - 1e-6).max(v_min);
let point = evaluate_bspline_surface(
u,
v,
u_degree,
v_degree,
control_points,
u_knots,
v_knots,
weights,
);
positions.push(point.x as f32);
positions.push(point.y as f32);
positions.push(point.z as f32);
if i < u_segments && j < v_segments {
let base = (i * (v_segments + 1) + j) as u32;
let next_u = base + (v_segments + 1) as u32;
indices.push(base);
indices.push(base + 1);
indices.push(next_u + 1);
indices.push(base);
indices.push(next_u + 1);
indices.push(next_u);
}
}
}
Some((positions, indices))
}
pub(super) fn parse_rational_weights(bspline: &DecodedEntity) -> Option<Vec<Vec<f64>>> {
let weights_attr = bspline.get(12)?;
let rows = weights_attr.as_list()?;
let mut result = Vec::with_capacity(rows.len());
for row in rows {
let cols = row.as_list()?;
let row_weights: Vec<f64> = cols.iter().filter_map(|v| v.as_float()).collect();
if row_weights.is_empty() {
return None;
}
result.push(row_weights);
}
Some(result)
}
fn parse_control_points(
bspline: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<Vec<Vec<Point3<f64>>>> {
let cp_list_attr = bspline
.get(2)
.ok_or_else(|| Error::geometry("BSplineSurface missing ControlPointsList".to_string()))?;
let rows = cp_list_attr
.as_list()
.ok_or_else(|| Error::geometry("Expected control point list".to_string()))?;
let mut result = Vec::with_capacity(rows.len());
for row in rows {
let cols = row
.as_list()
.ok_or_else(|| Error::geometry("Expected control point row".to_string()))?;
let mut row_points = Vec::with_capacity(cols.len());
for col in cols {
if let Some(point_id) = col.as_entity_ref() {
let point = decoder.decode_by_id(point_id)?;
let coords = point.get(0).and_then(|v| v.as_list()).ok_or_else(|| {
Error::geometry("CartesianPoint missing coordinates".to_string())
})?;
let x = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0);
let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
let z = coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
row_points.push(Point3::new(x, y, z));
}
}
result.push(row_points);
}
Ok(result)
}
fn expand_knots(knot_values: &[f64], multiplicities: &[i64]) -> Vec<f64> {
let mut expanded = Vec::new();
for (knot, &mult) in knot_values.iter().zip(multiplicities.iter()) {
for _ in 0..mult {
expanded.push(*knot);
}
}
expanded
}
fn parse_knot_vectors(bspline: &DecodedEntity) -> Result<(Vec<f64>, Vec<f64>)> {
let u_mult_attr = bspline
.get(7)
.ok_or_else(|| Error::geometry("BSplineSurface missing UMultiplicities".to_string()))?;
let u_mults: Vec<i64> = u_mult_attr
.as_list()
.ok_or_else(|| Error::geometry("Expected U multiplicities list".to_string()))?
.iter()
.filter_map(|v| v.as_int())
.collect();
let v_mult_attr = bspline
.get(8)
.ok_or_else(|| Error::geometry("BSplineSurface missing VMultiplicities".to_string()))?;
let v_mults: Vec<i64> = v_mult_attr
.as_list()
.ok_or_else(|| Error::geometry("Expected V multiplicities list".to_string()))?
.iter()
.filter_map(|v| v.as_int())
.collect();
let u_knots_attr = bspline
.get(9)
.ok_or_else(|| Error::geometry("BSplineSurface missing UKnots".to_string()))?;
let u_knot_values: Vec<f64> = u_knots_attr
.as_list()
.ok_or_else(|| Error::geometry("Expected U knots list".to_string()))?
.iter()
.filter_map(|v| v.as_float())
.collect();
let v_knots_attr = bspline
.get(10)
.ok_or_else(|| Error::geometry("BSplineSurface missing VKnots".to_string()))?;
let v_knot_values: Vec<f64> = v_knots_attr
.as_list()
.ok_or_else(|| Error::geometry("Expected V knots list".to_string()))?
.iter()
.filter_map(|v| v.as_float())
.collect();
let u_knots = expand_knots(&u_knot_values, &u_mults);
let v_knots = expand_knots(&v_knot_values, &v_mults);
Ok((u_knots, v_knots))
}
fn extract_vertex_coords(vertex: &DecodedEntity, decoder: &mut EntityDecoder) -> Option<Point3<f64>> {
let point_attr = vertex.get(0)?;
let point = decoder.resolve_ref(point_attr).ok().flatten()?;
let coords = point.get(0).and_then(|v| v.as_list())?;
let x = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0);
let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
let z = coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
Some(Point3::new(x, y, z))
}
fn evaluate_bspline_curve(
t: f64,
degree: usize,
control_points: &[Point3<f64>],
knots: &[f64],
) -> Point3<f64> {
let mut result = Point3::new(0.0, 0.0, 0.0);
for (i, cp) in control_points.iter().enumerate() {
let basis = bspline_basis(i, degree, t, knots);
if basis.abs() > 1e-10 {
result.x += basis * cp.x;
result.y += basis * cp.y;
result.z += basis * cp.z;
}
}
result
}
fn sample_bspline_edge_curve(
curve: &DecodedEntity,
start: &Point3<f64>,
curve_forward: bool,
decoder: &mut EntityDecoder,
) -> Vec<Point3<f64>> {
let degree = curve.get_float(0).unwrap_or(3.0) as usize;
let cp_list = match curve.get(1).and_then(|a| a.as_list()) {
Some(list) => list,
None => return vec![*start],
};
let control_points: Vec<Point3<f64>> = cp_list
.iter()
.filter_map(|ref_val| {
let id = ref_val.as_entity_ref()?;
let pt = decoder.decode_by_id(id).ok()?;
let coords = pt.get(0)?.as_list()?;
let x = coords.first()?.as_float().unwrap_or(0.0);
let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
let z = coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
Some(Point3::new(x, y, z))
})
.collect();
if control_points.len() <= degree {
return vec![*start];
}
let mults: Vec<i64> = curve
.get(6)
.and_then(|a| a.as_list())
.map(|l| l.iter().filter_map(|v| v.as_int()).collect())
.unwrap_or_default();
let knot_values: Vec<f64> = curve
.get(7)
.and_then(|a| a.as_list())
.map(|l| l.iter().filter_map(|v| v.as_float()).collect())
.unwrap_or_default();
if mults.is_empty() || knot_values.is_empty() {
return vec![*start];
}
let knots = expand_knots(&knot_values, &mults);
let t_min = knots[degree];
let t_max = knots[knots.len() - degree - 1];
let n_segments = (control_points.len() * 2).clamp(4, 16);
let mut points = Vec::with_capacity(n_segments + 1);
points.push(*start);
for i in 1..n_segments {
let frac = i as f64 / n_segments as f64;
let t = if curve_forward {
t_min + (t_max - t_min) * frac
} else {
t_max - (t_max - t_min) * frac
};
let t_clamped = t.min(t_max - 1e-6).max(t_min);
let pt = evaluate_bspline_curve(t_clamped, degree, &control_points, &knots);
if let Some(prev) = points.last() {
let dist_sq = (pt.x - prev.x).powi(2) + (pt.y - prev.y).powi(2) + (pt.z - prev.z).powi(2);
if dist_sq < 1e-12 {
continue;
}
}
points.push(pt);
}
points
}
fn read_axis2_placement_3d(
placement: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> (Point3<f64>, nalgebra::Vector3<f64>, nalgebra::Vector3<f64>) {
use nalgebra::Vector3;
let location = placement
.get(0)
.and_then(|a| decoder.resolve_ref(a).ok().flatten())
.and_then(|p| {
let coords = p.get(0).and_then(|v| v.as_list())?;
let x = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0);
let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
let z = coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
Some(Point3::new(x, y, z))
})
.unwrap_or_else(|| Point3::new(0.0, 0.0, 0.0));
let read_dir = |entity: &DecodedEntity| -> Option<Vector3<f64>> {
let coords = entity.get(0).and_then(|v| v.as_list())?;
let x = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0);
let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
let z = coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
Some(Vector3::new(x, y, z))
};
let axis_z = placement
.get(1)
.and_then(|a| decoder.resolve_ref(a).ok().flatten())
.and_then(|e| read_dir(&e))
.and_then(|v| v.try_normalize(1e-12))
.unwrap_or_else(|| Vector3::new(0.0, 0.0, 1.0));
let mut axis_x = placement
.get(2)
.and_then(|a| decoder.resolve_ref(a).ok().flatten())
.and_then(|e| read_dir(&e))
.unwrap_or_else(|| {
if axis_z.x.abs() < 0.9 {
Vector3::new(1.0, 0.0, 0.0)
} else {
Vector3::new(0.0, 1.0, 0.0)
}
});
axis_x -= axis_z * axis_x.dot(&axis_z);
let axis_x = axis_x.try_normalize(1e-12).unwrap_or_else(|| {
let candidates = [
Vector3::new(1.0, 0.0, 0.0),
Vector3::new(0.0, 1.0, 0.0),
Vector3::new(0.0, 0.0, 1.0),
];
let pick = candidates
.iter()
.min_by(|a, b| {
let da = axis_z.dot(a).abs();
let db = axis_z.dot(b).abs();
da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
})
.copied()
.unwrap_or(Vector3::new(1.0, 0.0, 0.0));
let ortho = pick - axis_z * pick.dot(&axis_z);
ortho
.try_normalize(1e-12)
.unwrap_or(Vector3::new(1.0, 0.0, 0.0))
});
(location, axis_z, axis_x)
}
fn sample_circle_edge_curve(
curve: &DecodedEntity,
start: &Point3<f64>,
end: &Point3<f64>,
curve_forward: bool,
decoder: &mut EntityDecoder,
) -> Vec<Point3<f64>> {
use std::f64::consts::TAU;
let radius = match curve.get(1).and_then(|v| v.as_float()) {
Some(r) if r > 0.0 => r,
_ => return vec![*start],
};
let placement = match curve.get(0).and_then(|a| decoder.resolve_ref(a).ok().flatten()) {
Some(p) => p,
None => return vec![*start],
};
let (center, axis_z, axis_x) = read_axis2_placement_3d(&placement, decoder);
let axis_y = axis_z.cross(&axis_x);
let project_angle = |p: &Point3<f64>| -> f64 {
let v = p - center;
v.dot(&axis_y).atan2(v.dot(&axis_x))
};
let a_start = project_angle(start);
let a_end = project_angle(end);
let mut ccw_delta = (a_end - a_start).rem_euclid(TAU);
let mut cw_delta = (a_start - a_end).rem_euclid(TAU);
let coincident = (start - end).norm() < 1e-6 * radius.max(1.0);
if coincident || ccw_delta < 1e-9 {
ccw_delta = TAU;
cw_delta = TAU;
}
let (delta, sign) = if curve_forward {
(ccw_delta, 1.0_f64)
} else {
(cw_delta, -1.0_f64)
};
let n_segments = ((delta / (TAU / 30.0)).ceil() as usize).clamp(2, 32);
let mut points = Vec::with_capacity(n_segments);
points.push(*start);
for i in 1..n_segments {
let t = delta * (i as f64) / (n_segments as f64);
let angle = a_start + sign * t;
let p = center + axis_x * (radius * angle.cos()) + axis_y * (radius * angle.sin());
points.push(p);
}
points
}
fn extract_edge_loop_points(
loop_entity: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Vec<Point3<f64>> {
let edges = match loop_entity.get(0).and_then(|a| a.as_list()) {
Some(e) => e,
None => return Vec::new(),
};
let mut polygon_points = Vec::new();
for edge_ref in edges {
let edge_id = match edge_ref.as_entity_ref() {
Some(id) => id,
None => continue,
};
let oriented_edge = match decoder.decode_by_id(edge_id) {
Ok(e) => e,
Err(_) => continue,
};
let orientation = oriented_edge
.get(3)
.and_then(|a| a.as_enum())
.map(|e| e == "T" || e == "TRUE")
.unwrap_or(true);
let edge_curve = match oriented_edge
.get(2)
.and_then(|attr| decoder.resolve_ref(attr).ok().flatten())
{
Some(ec) => ec,
None => {
let vertex = oriented_edge
.get(0)
.and_then(|attr| decoder.resolve_ref(attr).ok().flatten());
if let Some(v) = vertex {
if let Some(pt) = extract_vertex_coords(&v, decoder) {
polygon_points.push(pt);
}
}
continue;
}
};
let edge_same_sense = edge_curve.get(3).and_then(|a| a.as_enum())
.map(|e| e == "T" || e == "TRUE").unwrap_or(true);
let curve_forward = orientation == edge_same_sense;
let start_vertex = edge_curve
.get(0)
.and_then(|attr| decoder.resolve_ref(attr).ok().flatten());
let end_vertex = edge_curve
.get(1)
.and_then(|attr| decoder.resolve_ref(attr).ok().flatten());
let edge_start_pt = start_vertex.as_ref().and_then(|v| extract_vertex_coords(v, decoder));
let edge_end_pt = end_vertex.as_ref().and_then(|v| extract_vertex_coords(v, decoder));
let (walk_start, _walk_end) = if orientation {
(edge_start_pt, edge_end_pt)
} else {
(edge_end_pt, edge_start_pt)
};
let edge_geometry = edge_curve
.get(2)
.and_then(|attr| decoder.resolve_ref(attr).ok().flatten());
if let Some(geom) = edge_geometry {
let geom_type = geom.ifc_type.as_str().to_uppercase();
if geom_type == "IFCBSPLINECURVEWITHKNOTS" {
let s = walk_start.unwrap_or(Point3::new(0.0, 0.0, 0.0));
let sampled = sample_bspline_edge_curve(&geom, &s, curve_forward, decoder);
polygon_points.extend(sampled);
continue;
}
if geom_type == "IFCCIRCLE" {
if let (Some(s), Some(e)) = (walk_start, _walk_end) {
let sampled = sample_circle_edge_curve(&geom, &s, &e, curve_forward, decoder);
polygon_points.extend(sampled);
continue;
}
}
}
if let Some(pt) = walk_start {
polygon_points.push(pt);
}
}
polygon_points
}
fn process_planar_face(
face: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<(Vec<f32>, Vec<u32>)> {
use crate::triangulation::{project_to_2d_with_basis, triangulate_polygon_with_holes};
use ifc_lite_core::IfcType;
let bounds_attr = face
.get(0)
.ok_or_else(|| Error::geometry("AdvancedFace missing Bounds".to_string()))?;
let bounds = bounds_attr
.as_list()
.ok_or_else(|| Error::geometry("Expected bounds list".to_string()))?;
let mut outer_points: Option<Vec<Point3<f64>>> = None;
let mut hole_points: Vec<Vec<Point3<f64>>> = Vec::new();
for bound in bounds {
let Some(bound_id) = bound.as_entity_ref() else {
continue;
};
let bound_entity = decoder.decode_by_id(bound_id)?;
let loop_attr = bound_entity
.get(0)
.ok_or_else(|| Error::geometry("FaceBound missing Bound".to_string()))?;
let loop_entity = decoder
.resolve_ref(loop_attr)?
.ok_or_else(|| Error::geometry("Failed to resolve loop".to_string()))?;
if !loop_entity.ifc_type.as_str().eq_ignore_ascii_case("IFCEDGELOOP") {
continue;
}
let mut points = extract_edge_loop_points(&loop_entity, decoder);
if points.len() < 3 {
continue;
}
let orientation = bound_entity
.get(1)
.and_then(|a| a.as_enum())
.map(|e| e == "T" || e == "TRUE")
.unwrap_or(true);
if !orientation {
points.reverse();
}
let is_outer = bound_entity.ifc_type == IfcType::IfcFaceOuterBound;
if is_outer || outer_points.is_none() {
if is_outer {
if let Some(prev_outer) = outer_points.take() {
hole_points.push(prev_outer);
}
}
outer_points = Some(points);
} else {
hole_points.push(points);
}
}
let Some(outer) = outer_points else {
return Ok((Vec::new(), Vec::new()));
};
let normal = calculate_polygon_normal(&outer);
let (outer_2d, u_axis, v_axis, origin) = project_to_2d(&outer, &normal);
let holes_2d: Vec<Vec<nalgebra::Point2<f64>>> = hole_points
.iter()
.map(|h| project_to_2d_with_basis(h, &u_axis, &v_axis, &origin))
.collect();
let mut positions = Vec::with_capacity((outer.len() + hole_points.iter().map(|h| h.len()).sum::<usize>()) * 3);
for p in outer.iter().chain(hole_points.iter().flat_map(|h| h.iter())) {
positions.push(p.x as f32);
positions.push(p.y as f32);
positions.push(p.z as f32);
}
let indices = match triangulate_polygon_with_holes(&outer_2d, &holes_2d) {
Ok(idx) => idx.into_iter().map(|i| i as u32).collect(),
Err(_) => {
let mut idx = Vec::with_capacity((outer.len() - 2) * 3);
for i in 1..outer.len() - 1 {
idx.push(0u32);
idx.push(i as u32);
idx.push(i as u32 + 1);
}
idx
}
};
Ok((positions, indices))
}
pub(super) fn process_bspline_face(
bspline: &DecodedEntity,
decoder: &mut EntityDecoder,
weights: Option<&[Vec<f64>]>,
) -> Result<(Vec<f32>, Vec<u32>)> {
let u_degree = bspline.get_float(0).unwrap_or(3.0) as usize;
let v_degree = bspline.get_float(1).unwrap_or(1.0) as usize;
let control_points = parse_control_points(bspline, decoder)?;
let (u_knots, v_knots) = parse_knot_vectors(bspline)?;
let u_segments = (control_points.len() * 3).clamp(8, 24);
let v_segments = if !control_points.is_empty() {
(control_points[0].len() * 3).clamp(4, 24)
} else {
4
};
match tessellate_bspline_surface(
u_degree,
v_degree,
&control_points,
&u_knots,
&v_knots,
weights,
u_segments,
v_segments,
) {
Some((positions, indices)) => Ok((positions, indices)),
None => Ok((Vec::new(), Vec::new())),
}
}
fn process_cylindrical_face(
face: &DecodedEntity,
surface: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<(Vec<f32>, Vec<u32>)> {
let radius = surface
.get(1)
.and_then(|v| v.as_float())
.ok_or_else(|| Error::geometry("CylindricalSurface missing Radius".to_string()))?;
let position_attr = surface.get(0);
let axis_transform = if let Some(attr) = position_attr {
if let Some(pos_id) = attr.as_entity_ref() {
get_axis2_placement_transform_by_id(pos_id, decoder)?
} else {
Matrix4::identity()
}
} else {
Matrix4::identity()
};
let boundary_points: Vec<Point3<f64>> = extract_edge_loop_points_for_bounds(face, decoder);
if boundary_points.is_empty() {
return Ok((Vec::new(), Vec::new()));
}
let inv_transform = axis_transform
.try_inverse()
.unwrap_or(Matrix4::identity());
let local_points: Vec<Point3<f64>> = boundary_points
.iter()
.map(|p| inv_transform.transform_point(p))
.collect();
let mut angles: Vec<f64> = local_points
.iter()
.map(|p| {
let mut a = p.y.atan2(p.x);
if a < 0.0 {
a += std::f64::consts::TAU;
}
a
})
.collect();
angles.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
angles.dedup_by(|a, b| (*a - *b).abs() < 1e-6);
let (min_angle, max_angle) = if angles.len() < 2 {
(0.0, std::f64::consts::TAU)
} else {
let n = angles.len();
let mut max_gap = 0.0;
let mut max_gap_idx = 0usize;
for i in 0..n {
let next = if i + 1 < n {
angles[i + 1]
} else {
angles[0] + std::f64::consts::TAU
};
let gap = next - angles[i];
if gap > max_gap {
max_gap = gap;
max_gap_idx = i;
}
}
let start = angles[(max_gap_idx + 1) % n];
let end_raw = angles[max_gap_idx];
let end = if end_raw < start {
end_raw + std::f64::consts::TAU
} else {
end_raw
};
let span = end - start;
if span < 1e-6 || span > std::f64::consts::TAU - 1e-6 {
(0.0, std::f64::consts::TAU)
} else {
(start, end)
}
};
let mut min_z = f64::MAX;
let mut max_z = f64::MIN;
for p in &local_points {
min_z = min_z.min(p.z);
max_z = max_z.max(p.z);
}
let angle_span = max_angle - min_angle;
let height = max_z - min_z;
let angle_segments =
((angle_span / (std::f64::consts::PI / 18.0)).ceil() as usize).clamp(6, 32);
let height_segments = ((height / (radius * 2.0)).ceil() as usize).clamp(1, 8);
let mut positions = Vec::new();
let mut indices = Vec::new();
for h in 0..=height_segments {
let z = min_z + (height * h as f64 / height_segments as f64);
for a in 0..=angle_segments {
let angle = min_angle + (angle_span * a as f64 / angle_segments as f64);
let x = radius * angle.cos();
let y = radius * angle.sin();
let local_point = Point3::new(x, y, z);
let world_point = axis_transform.transform_point(&local_point);
positions.push(world_point.x as f32);
positions.push(world_point.y as f32);
positions.push(world_point.z as f32);
}
}
let cols = angle_segments + 1;
for h in 0..height_segments {
for a in 0..angle_segments {
let base = (h * cols + a) as u32;
let next_row = base + cols as u32;
indices.push(base);
indices.push(base + 1);
indices.push(next_row + 1);
indices.push(base);
indices.push(next_row + 1);
indices.push(next_row);
}
}
Ok((positions, indices))
}
fn sample_curve_polyline(
curve: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Vec<Point3<f64>> {
use std::f64::consts::TAU;
let kind = curve.ifc_type.as_str().to_uppercase();
if kind == "IFCBSPLINECURVEWITHKNOTS" {
let mut pts = sample_bspline_edge_curve(
curve,
&Point3::new(0.0, 0.0, 0.0),
true,
decoder,
);
if !pts.is_empty() {
let degree = curve.get_float(0).unwrap_or(3.0) as usize;
if let (Some(cp_list), Some(mults), Some(knot_values)) = (
curve.get(1).and_then(|a| a.as_list()),
curve
.get(6)
.and_then(|a| a.as_list())
.map(|l| l.iter().filter_map(|v| v.as_int()).collect::<Vec<_>>()),
curve
.get(7)
.and_then(|a| a.as_list())
.map(|l| l.iter().filter_map(|v| v.as_float()).collect::<Vec<_>>()),
) {
let cps: Vec<Point3<f64>> = cp_list
.iter()
.filter_map(|r| {
let id = r.as_entity_ref()?;
let pt = decoder.decode_by_id(id).ok()?;
let coords = pt.get(0)?.as_list()?;
let x = coords.first()?.as_float().unwrap_or(0.0);
let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
let z = coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
Some(Point3::new(x, y, z))
})
.collect();
if !cps.is_empty() && !mults.is_empty() && !knot_values.is_empty() {
let knots = expand_knots(&knot_values, &mults);
if knots.len() > degree {
let t0 = knots[degree];
pts[0] = evaluate_bspline_curve(t0, degree, &cps, &knots);
let t_max_idx = knots.len().saturating_sub(degree + 1);
if t_max_idx > degree {
let t_max = knots[t_max_idx];
let p_end = evaluate_bspline_curve(t_max, degree, &cps, &knots);
let near_dup = pts
.last()
.map(|p| (p - p_end).norm_squared() < 1e-18)
.unwrap_or(false);
if !near_dup {
pts.push(p_end);
}
}
}
}
}
}
return pts;
}
if kind == "IFCLINE" {
let pnt = curve
.get(0)
.and_then(|a| decoder.resolve_ref(a).ok().flatten())
.and_then(|p| {
let coords = p.get(0).and_then(|v| v.as_list())?;
let x = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0);
let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
let z = coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
Some(Point3::new(x, y, z))
});
let (dir, mag) = curve
.get(1)
.and_then(|a| decoder.resolve_ref(a).ok().flatten())
.map(|v| {
let direction = v.get(0).and_then(|a| decoder.resolve_ref(a).ok().flatten());
let magnitude = v.get(1).and_then(|a| a.as_float()).unwrap_or(1.0);
let dir = direction
.and_then(|d| {
let coords = d.get(0).and_then(|v| v.as_list())?;
let x = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0);
let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
let z = coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
Some(nalgebra::Vector3::new(x, y, z))
})
.and_then(|v| v.try_normalize(1e-12))
.unwrap_or_else(|| nalgebra::Vector3::new(1.0, 0.0, 0.0));
(dir, magnitude)
})
.unwrap_or_else(|| (nalgebra::Vector3::new(1.0, 0.0, 0.0), 1.0));
let start = pnt.unwrap_or_else(|| Point3::new(0.0, 0.0, 0.0));
return vec![start, start + dir * mag];
}
if kind == "IFCCIRCLE" {
let radius = curve.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
if radius <= 0.0 {
return Vec::new();
}
let placement = match curve.get(0).and_then(|a| decoder.resolve_ref(a).ok().flatten()) {
Some(p) => p,
None => return Vec::new(),
};
let (center, axis_z, axis_x) = read_axis2_placement_3d(&placement, decoder);
let axis_y = axis_z.cross(&axis_x);
let n = 24usize;
return (0..=n)
.map(|i| {
let a = TAU * (i as f64) / (n as f64);
center + axis_x * (radius * a.cos()) + axis_y * (radius * a.sin())
})
.collect();
}
if kind == "IFCTRIMMEDCURVE" {
let basis = match curve.get(0).and_then(|a| decoder.resolve_ref(a).ok().flatten()) {
Some(b) => b,
None => return Vec::new(),
};
let basis_kind = basis.ifc_type.as_str().to_uppercase();
let sense = curve
.get(3)
.and_then(|a| a.as_enum())
.map(|e| e == "T" || e == "TRUE")
.unwrap_or(true);
let mut read_trim_point = |idx: usize| -> Option<Point3<f64>> {
let list = curve.get(idx)?.as_list()?;
for v in list {
if let Some(id) = v.as_entity_ref() {
if let Ok(e) = decoder.decode_by_id(id) {
if e.ifc_type.as_str().eq_ignore_ascii_case("IFCCARTESIANPOINT") {
let coords = e.get(0).and_then(|a| a.as_list())?;
let x = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0);
let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
let z = coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
return Some(Point3::new(x, y, z));
}
}
}
}
None
};
let p1 = read_trim_point(1);
let p2 = read_trim_point(2);
if basis_kind == "IFCCIRCLE" {
if let (Some(p_start), Some(p_end)) = (p1, p2) {
let mut pts = sample_circle_edge_curve(&basis, &p_start, &p_end, sense, decoder);
pts.push(p_end);
return pts;
}
}
if basis_kind == "IFCBSPLINECURVEWITHKNOTS" {
if let (Some(p_start), Some(p_end)) = (p1, p2) {
let mut pts = sample_bspline_edge_curve(&basis, &p_start, sense, decoder);
pts.push(p_end);
return pts;
}
if let Some(p_start) = p1 {
return sample_bspline_edge_curve(&basis, &p_start, sense, decoder);
}
}
if basis_kind == "IFCLINE" {
if let (Some(p_start), Some(p_end)) = (p1, p2) {
return if sense {
vec![p_start, p_end]
} else {
vec![p_end, p_start]
};
}
}
return sample_curve_polyline(&basis, decoder);
}
Vec::new()
}
fn process_surface_of_revolution_face(
face: &DecodedEntity,
surface: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<(Vec<f32>, Vec<u32>)> {
use nalgebra::Vector3;
use std::f64::consts::TAU;
let swept = surface
.get(0)
.and_then(|a| decoder.resolve_ref(a).ok().flatten());
let axis_pos = surface
.get(2)
.and_then(|a| decoder.resolve_ref(a).ok().flatten());
let (axis_origin, axis_dir) = if let Some(ap) = axis_pos {
let loc = ap
.get(0)
.and_then(|a| decoder.resolve_ref(a).ok().flatten())
.and_then(|p| {
let coords = p.get(0).and_then(|v| v.as_list())?;
let x = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0);
let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
let z = coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
Some(Point3::new(x, y, z))
})
.unwrap_or_else(|| Point3::new(0.0, 0.0, 0.0));
let dir = ap
.get(1)
.and_then(|a| decoder.resolve_ref(a).ok().flatten())
.and_then(|d| {
let coords = d.get(0).and_then(|v| v.as_list())?;
let x = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0);
let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
let z = coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
Some(Vector3::new(x, y, z))
})
.and_then(|v| v.try_normalize(1e-12))
.unwrap_or_else(|| Vector3::new(0.0, 0.0, 1.0));
(loc, dir)
} else {
(Point3::new(0.0, 0.0, 0.0), Vector3::new(0.0, 0.0, 1.0))
};
let profile_pts: Vec<Point3<f64>> = match swept {
Some(s) if s.ifc_type.as_str().eq_ignore_ascii_case("IFCARBITRARYOPENPROFILEDEF") => {
if let Some(curve) = s.get(2).and_then(|a| decoder.resolve_ref(a).ok().flatten()) {
sample_curve_polyline(&curve, decoder)
} else {
Vec::new()
}
}
Some(s) => sample_curve_polyline(&s, decoder),
None => Vec::new(),
};
if profile_pts.len() < 2 {
return process_planar_face(face, decoder);
}
let ref_dir = if axis_dir.x.abs() < 0.9 {
Vector3::new(1.0, 0.0, 0.0)
} else {
Vector3::new(0.0, 1.0, 0.0)
};
let axis_x = (ref_dir - axis_dir * ref_dir.dot(&axis_dir))
.try_normalize(1e-12)
.unwrap_or_else(|| Vector3::new(1.0, 0.0, 0.0));
let axis_y = axis_dir.cross(&axis_x);
let boundary = extract_edge_loop_points_for_bounds(face, decoder);
let (a_min, span) = if boundary.is_empty() {
(0.0, TAU)
} else {
let mut angles: Vec<f64> = boundary
.iter()
.map(|p| {
let v = p - axis_origin;
let mut a = v.dot(&axis_y).atan2(v.dot(&axis_x));
if a < 0.0 {
a += TAU;
}
a
})
.collect();
angles.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
angles.dedup_by(|a, b| (*a - *b).abs() < 1e-6);
if angles.len() < 2 {
(0.0, TAU)
} else {
let n = angles.len();
let mut max_gap = 0.0;
let mut max_gap_idx = 0usize;
for i in 0..n {
let next = if i + 1 < n { angles[i + 1] } else { angles[0] + TAU };
let gap = next - angles[i];
if gap > max_gap {
max_gap = gap;
max_gap_idx = i;
}
}
let start = angles[(max_gap_idx + 1) % n];
let end_raw = angles[max_gap_idx];
let end = if end_raw < start { end_raw + TAU } else { end_raw };
let s = end - start;
if s < 1e-6 || s > TAU - 1e-6 {
(0.0, TAU)
} else {
(start, s)
}
}
};
let n_angle = ((span / (TAU / 36.0)).ceil() as usize).clamp(4, 48);
let n_v = profile_pts.len();
let local_profile: Vec<(f64, f64, f64)> = profile_pts
.iter()
.map(|p| {
let r = p - axis_origin;
(r.dot(&axis_x), r.dot(&axis_y), r.dot(&axis_dir))
})
.collect();
let natural_angle = local_profile
.iter()
.find(|&&(rx, ry, _)| rx.hypot(ry) > 1e-9)
.map(|&(rx, ry, _)| ry.atan2(rx))
.unwrap_or(0.0);
let mut positions = Vec::with_capacity((n_angle + 1) * n_v * 3);
for i in 0..=n_angle {
let boundary_angle = a_min + span * (i as f64) / (n_angle as f64);
let v = boundary_angle - natural_angle;
let cos_v = v.cos();
let sin_v = v.sin();
for &(rx, ry, z) in &local_profile {
let nrx = rx * cos_v - ry * sin_v;
let nry = rx * sin_v + ry * cos_v;
let world = axis_origin + axis_x * nrx + axis_y * nry + axis_dir * z;
positions.push(world.x as f32);
positions.push(world.y as f32);
positions.push(world.z as f32);
}
}
let mut indices = Vec::with_capacity(n_angle * (n_v - 1) * 6);
for i in 0..n_angle {
for j in 0..(n_v - 1) {
let a = (i * n_v + j) as u32;
let b = a + n_v as u32;
let c = b + 1;
let d = a + 1;
indices.push(a);
indices.push(b);
indices.push(c);
indices.push(a);
indices.push(c);
indices.push(d);
}
}
if positions.is_empty() || indices.is_empty() {
return process_planar_face(face, decoder);
}
Ok((positions, indices))
}
fn extract_edge_loop_points_for_bounds(
face: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Vec<Point3<f64>> {
let mut all = Vec::new();
let bounds = match face.get(0).and_then(|a| a.as_list()) {
Some(b) => b,
None => return all,
};
for bound in bounds {
if let Some(bound_id) = bound.as_entity_ref() {
if let Ok(bound_entity) = decoder.decode_by_id(bound_id) {
if let Some(loop_attr) = bound_entity.get(0) {
if let Some(loop_entity) = decoder.resolve_ref(loop_attr).ok().flatten() {
if loop_entity
.ifc_type
.as_str()
.eq_ignore_ascii_case("IFCEDGELOOP")
{
all.extend(extract_edge_loop_points(&loop_entity, decoder));
}
}
}
}
}
}
all
}