use crate::profile::Profile2D;
use crate::tessellation::{scale_segments, TessellationQuality};
use crate::{Error, Point2, Point3, Result, Vector3};
use ifc_lite_core::{
AttributeValue, DecodedEntity, EntityDecoder, IfcSchema, IfcType, ProfileCategory,
};
use std::cell::Cell;
use std::f64::consts::PI;
const MAX_CURVE_DEPTH: u32 = 50;
#[derive(Debug, Clone, Copy)]
enum TrimSelect {
Parameter(f64),
Cartesian(Point2<f64>),
}
const SMOOTH_CURVE_SPACING_RATIO: f64 = 1.0 / 16.0;
const SMOOTH_CURVE_LONGEST_EDGE_RATIO: f64 = 0.10;
const RDP_EPSILON_RATIO: f64 = 1.0 / 100.0;
const RDP_EPSILON_MIN: f64 = 5.0e-3;
const THIN_FEATURE_RATIO: f64 = 0.05;
const DOUBLE_BACK_REFLEX_ARC_FRACTION: f64 = 0.15;
const REFLEX_TURN_SIN_TOL: f64 = 1.0e-6;
const SMOOTH_CURVE_MIN_VERTICES: usize = 24;
const SIMPLIFIED_MIN_VERTICES: usize = 12;
#[inline]
fn mirror_profile_about_y_axis(profile: &mut Profile2D) {
for p in &mut profile.outer {
p.x = -p.x;
}
profile.outer.reverse();
for hole in &mut profile.holes {
for p in hole.iter_mut() {
p.x = -p.x;
}
hole.reverse();
}
}
fn perpendicular_distance(p: Point2<f64>, a: Point2<f64>, b: Point2<f64>) -> f64 {
let dx = b.x - a.x;
let dy = b.y - a.y;
let len_sq = dx * dx + dy * dy;
if len_sq < f64::EPSILON {
let ex = p.x - a.x;
let ey = p.y - a.y;
return (ex * ex + ey * ey).sqrt();
}
let cross = (p.x - a.x) * dy - (p.y - a.y) * dx;
cross.abs() / len_sq.sqrt()
}
fn rdp_simplify_open(points: &[Point2<f64>], epsilon: f64) -> Vec<Point2<f64>> {
let n = points.len();
if n < 3 {
return points.to_vec();
}
let mut keep = vec![false; n];
keep[0] = true;
keep[n - 1] = true;
let mut stack: Vec<(usize, usize)> = Vec::new();
stack.push((0, n - 1));
while let Some((start, end)) = stack.pop() {
if end <= start + 1 {
continue;
}
let a = points[start];
let b = points[end];
let mut max_dist = 0.0;
let mut max_idx = start;
for (i, p) in points.iter().enumerate().take(end).skip(start + 1) {
let d = perpendicular_distance(*p, a, b);
if d > max_dist {
max_dist = d;
max_idx = i;
}
}
if max_dist > epsilon {
keep[max_idx] = true;
stack.push((start, max_idx));
stack.push((max_idx, end));
}
}
points
.iter()
.enumerate()
.filter_map(|(i, p)| if keep[i] { Some(*p) } else { None })
.collect()
}
const SELF_INTERSECT_SCAN_MAX_VERTICES: usize = 1024;
fn closed_loop_self_intersects(loop_: &[Point2<f64>]) -> bool {
let n = loop_.len();
if !(4..=SELF_INTERSECT_SCAN_MAX_VERTICES).contains(&n) {
return false;
}
#[inline]
fn orient(a: Point2<f64>, b: Point2<f64>, c: Point2<f64>) -> f64 {
(b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)
}
#[inline]
fn segments_properly_cross(
a: Point2<f64>,
b: Point2<f64>,
c: Point2<f64>,
d: Point2<f64>,
) -> bool {
let d1 = orient(c, d, a);
let d2 = orient(c, d, b);
let d3 = orient(a, b, c);
let d4 = orient(a, b, d);
((d1 > 0.0) != (d2 > 0.0)) && ((d3 > 0.0) != (d4 > 0.0))
}
for i in 0..n {
let a = loop_[i];
let b = loop_[(i + 1) % n];
for j in (i + 1)..n {
if (j + 1) % n == i || (i + 1) % n == j {
continue;
}
let c = loop_[j];
let d = loop_[(j + 1) % n];
if segments_properly_cross(a, b, c, d) {
return true;
}
}
}
false
}
fn loop_doubles_back(loop_: &[Point2<f64>]) -> bool {
let n = loop_.len();
if n < 4 {
return false;
}
let mut signed_area2 = 0.0;
for i in 0..n {
let a = loop_[i];
let b = loop_[(i + 1) % n];
signed_area2 += a.x * b.y - b.x * a.y;
}
if signed_area2 == 0.0 {
return false;
}
let winding = signed_area2.signum();
let mut perimeter = 0.0;
let mut reflex_len = 0.0;
for i in 0..n {
let prev = loop_[(i + n - 1) % n];
let cur = loop_[i];
let next = loop_[(i + 1) % n];
let (ex_in, ey_in) = (cur.x - prev.x, cur.y - prev.y);
let (ex_out, ey_out) = (next.x - cur.x, next.y - cur.y);
let out_len = (ex_out * ex_out + ey_out * ey_out).sqrt();
perimeter += out_len;
let in_len = (ex_in * ex_in + ey_in * ey_in).sqrt();
let denom = in_len * out_len;
if denom <= 0.0 {
continue; }
let sin_turn = (ex_in * ey_out - ey_in * ex_out) / denom;
if sin_turn * winding < -REFLEX_TURN_SIN_TOL {
reflex_len += out_len;
}
}
perimeter > 0.0 && reflex_len / perimeter > DOUBLE_BACK_REFLEX_ARC_FRACTION
}
pub(crate) fn simplify_smooth_curve_polyline(points: &[Point2<f64>]) -> Vec<Point2<f64>> {
let raw_len = points.len();
if raw_len < SMOOTH_CURVE_MIN_VERTICES {
return points.to_vec();
}
let closed = raw_len >= 2
&& (points[0].x - points[raw_len - 1].x).abs() < 1e-9
&& (points[0].y - points[raw_len - 1].y).abs() < 1e-9;
let core: &[Point2<f64>] = if closed {
&points[..raw_len - 1]
} else {
points
};
let n = core.len();
if n < SMOOTH_CURVE_MIN_VERTICES {
return points.to_vec();
}
let mut min_x = f64::INFINITY;
let mut min_y = f64::INFINITY;
let mut max_x = f64::NEG_INFINITY;
let mut max_y = f64::NEG_INFINITY;
for p in core {
if p.x < min_x {
min_x = p.x;
}
if p.y < min_y {
min_y = p.y;
}
if p.x > max_x {
max_x = p.x;
}
if p.y > max_y {
max_y = p.y;
}
}
let dx = max_x - min_x;
let dy = max_y - min_y;
let diag = (dx * dx + dy * dy).sqrt();
if !diag.is_finite() || diag < f64::EPSILON {
return points.to_vec();
}
let mut perimeter = 0.0;
let mut longest_edge: f64 = 0.0;
let mut area2 = 0.0; for i in 0..n {
let a = core[i];
let b = core[(i + 1) % n];
let ex = b.x - a.x;
let ey = b.y - a.y;
let len = (ex * ex + ey * ey).sqrt();
perimeter += len;
if len > longest_edge {
longest_edge = len;
}
area2 += a.x * b.y - b.x * a.y;
}
let mean_edge = perimeter / n as f64;
if mean_edge / diag > SMOOTH_CURVE_SPACING_RATIO {
return points.to_vec();
}
if longest_edge / diag > SMOOTH_CURVE_LONGEST_EDGE_RATIO {
return points.to_vec();
}
let half_thickness = if perimeter > f64::EPSILON {
area2.abs() / (2.0 * perimeter)
} else {
0.0
};
let is_thin = half_thickness / diag < THIN_FEATURE_RATIO;
if is_thin && loop_doubles_back(core) {
return points.to_vec();
}
let epsilon = (diag * RDP_EPSILON_RATIO).max(RDP_EPSILON_MIN);
let mut working: Vec<Point2<f64>> = core.to_vec();
working.push(core[0]); let simplified = rdp_simplify_open(&working, epsilon);
let mut simplified_core = simplified;
if simplified_core.len() >= 2 {
let last = simplified_core.len() - 1;
if (simplified_core[0].x - simplified_core[last].x).abs() < 1e-9
&& (simplified_core[0].y - simplified_core[last].y).abs() < 1e-9
{
simplified_core.pop();
}
}
if simplified_core.len() < SIMPLIFIED_MIN_VERTICES || simplified_core.len() >= n {
return points.to_vec();
}
if simplified_core.len() > SELF_INTERSECT_SCAN_MAX_VERTICES
|| closed_loop_self_intersects(&simplified_core)
{
return points.to_vec();
}
if closed {
let first = simplified_core[0];
simplified_core.push(first);
}
simplified_core
}
const MAX_PROFILE_DEPTH: u32 = 16;
fn trim_polyline(points: &[Point3<f64>], start: f64, end: f64) -> Vec<Point3<f64>> {
let n = points.len();
if n < 2 || end <= start {
return Vec::new();
}
let s = start.clamp(0.0, 1.0);
let e = end.clamp(0.0, 1.0);
let denom = (n - 1) as f64;
let lerp = |t: f64| -> Point3<f64> {
let scaled = t * denom;
let mut idx = scaled.floor() as usize;
if idx >= n - 1 {
return points[n - 1];
}
let frac = scaled - idx as f64;
let a = points[idx];
idx += 1;
let b = points[idx];
Point3::new(
a.x + (b.x - a.x) * frac,
a.y + (b.y - a.y) * frac,
a.z + (b.z - a.z) * frac,
)
};
let mut out = Vec::new();
out.push(lerp(s));
for (i, p) in points.iter().enumerate() {
let t = i as f64 / denom;
if t > s && t < e {
out.push(*p);
}
}
out.push(lerp(e));
out
}
fn approximate_arc_3pt_3d(
p1: Point3<f64>,
p2: Point3<f64>,
p3: Point3<f64>,
num_segments: usize,
) -> Vec<Point3<f64>> {
let a = p2 - p1;
let b = p3 - p1;
let normal = a.cross(&b);
let normal_len_sq = normal.norm_squared();
let arc_span = (p3 - p1).norm();
let collinear_tol = 1e-12_f64.max(arc_span.powi(4) * 1e-12);
if normal_len_sq < collinear_tol {
return vec![p1, p2, p3];
}
let n_hat = normal / normal_len_sq.sqrt();
let d11 = a.dot(&a);
let d22 = b.dot(&b);
let d12 = a.dot(&b);
let denom = 2.0 * (d11 * d22 - d12 * d12);
if denom.abs() < 1e-20 {
return vec![p1, p2, p3];
}
let u = (d22 * (d11 - d12)) / denom;
let v = (d11 * (d22 - d12)) / denom;
let center = p1 + a * u + b * v;
let radius = (p1 - center).norm();
if radius > arc_span * 100.0 {
return vec![p1, p2, p3];
}
let u_axis = (p1 - center) / radius;
let v_axis = n_hat.cross(&u_axis);
let angle_of = |pt: Point3<f64>| -> f64 {
let r = pt - center;
r.dot(&v_axis).atan2(r.dot(&u_axis))
};
let a1 = angle_of(p1); let a2 = angle_of(p2);
let a3 = angle_of(p3);
fn norm_pi(mut a: f64) -> f64 {
let two_pi = 2.0 * std::f64::consts::PI;
a %= two_pi;
if a > std::f64::consts::PI {
a -= two_pi;
} else if a < -std::f64::consts::PI {
a += two_pi;
}
a
}
let diff13 = norm_pi(a3 - a1);
let diff12 = norm_pi(a2 - a1);
let go_direct = if diff13 > 0.0 {
diff12 > 0.0 && diff12 < diff13
} else {
diff12 < 0.0 && diff12 > diff13
};
let sweep = if go_direct {
diff13
} else if diff13 > 0.0 {
diff13 - 2.0 * std::f64::consts::PI
} else {
diff13 + 2.0 * std::f64::consts::PI
};
let mut out = Vec::with_capacity(num_segments + 1);
for i in 0..=num_segments {
let t = i as f64 / num_segments as f64;
let angle = a1 + t * sweep;
let pt = center + (u_axis * radius * angle.cos()) + (v_axis * radius * angle.sin());
out.push(pt);
}
out
}
fn same_point_3d(prev: Option<&Point3<f64>>, next: &Point3<f64>) -> bool {
match prev {
Some(p) => {
(p.x - next.x).abs() < 1e-9
&& (p.y - next.y).abs() < 1e-9
&& (p.z - next.z).abs() < 1e-9
}
None => false,
}
}
fn rounded_rectangle_outline(
half_x: f64,
half_y: f64,
radius: f64,
ccw: bool,
quality: TessellationQuality,
) -> Vec<Point2<f64>> {
if radius <= 1.0e-9 {
let pts = vec![
Point2::new(-half_x, -half_y),
Point2::new(half_x, -half_y),
Point2::new(half_x, half_y),
Point2::new(-half_x, half_y),
];
return if ccw {
pts
} else {
pts.into_iter().rev().collect()
};
}
let segments_per_corner = quality.profile_arc_segments(6, 2);
let half_pi = PI / 2.0;
let corners = [
(half_x - radius, -half_y + radius, -half_pi, 0.0),
(half_x - radius, half_y - radius, 0.0, half_pi),
(-half_x + radius, half_y - radius, half_pi, PI),
(-half_x + radius, -half_y + radius, PI, PI + half_pi),
];
let mut points: Vec<Point2<f64>> = Vec::with_capacity((segments_per_corner + 1) * 4);
const SEAM_TOL: f64 = 1.0e-6;
for (cx, cy, a0, a1) in corners {
for i in 0..=segments_per_corner {
let t = i as f64 / segments_per_corner as f64;
let a = a0 + (a1 - a0) * t;
let pt = Point2::new(cx + radius * a.cos(), cy + radius * a.sin());
if let Some(prev) = points.last() {
if (prev.x - pt.x).abs() < SEAM_TOL && (prev.y - pt.y).abs() < SEAM_TOL {
continue;
}
}
points.push(pt);
}
}
if points.len() >= 2 {
let first = points[0];
let last = points[points.len() - 1];
if (first.x - last.x).abs() < SEAM_TOL && (first.y - last.y).abs() < SEAM_TOL {
points.pop();
}
}
if !ccw {
points.reverse();
}
points
}
fn push_dedup(out: &mut Vec<Point2<f64>>, pt: Point2<f64>) {
if out
.last()
.map_or(true, |p| (p.x - pt.x).abs() > 1.0e-9 || (p.y - pt.y).abs() > 1.0e-9)
{
out.push(pt);
}
}
fn push_arc(
out: &mut Vec<Point2<f64>>,
cx: f64,
cy: f64,
r: f64,
a0: f64,
a1: f64,
segments: usize,
) {
let n = segments.max(1);
for i in 0..=n {
let t = i as f64 / n as f64;
let a = a0 + (a1 - a0) * t;
push_dedup(out, Point2::new(cx + r * a.cos(), cy + r * a.sin()));
}
}
fn round_corner(
prev: Point2<f64>,
corner: Point2<f64>,
next: Point2<f64>,
r: f64,
segments: usize,
) -> Vec<Point2<f64>> {
if r <= 1.0e-9 {
return vec![corner];
}
let ein = corner - prev;
let eout = next - corner;
let (ein_n, eout_n) = (ein.norm(), eout.norm());
if ein_n < r || eout_n < r {
return vec![corner];
}
let ein = ein / ein_n;
let eout = eout / eout_n;
let t_in = corner - ein * r; let t_out = corner + eout * r; let center = corner - ein * r + eout * r;
let mut a0 = (t_in.y - center.y).atan2(t_in.x - center.x);
let mut a1 = (t_out.y - center.y).atan2(t_out.x - center.x);
while a1 - a0 > std::f64::consts::PI {
a1 -= 2.0 * std::f64::consts::PI;
}
while a0 - a1 > std::f64::consts::PI {
a1 += 2.0 * std::f64::consts::PI;
}
let mut out = Vec::with_capacity(segments + 1);
push_arc(&mut out, center.x, center.y, r, a0, a1, segments);
out
}
fn fillet_outline(
sharp: &[Point2<f64>],
radii: &[(usize, f64)],
segments: usize,
) -> Vec<Point2<f64>> {
let n = sharp.len();
let mut out: Vec<Point2<f64>> = Vec::with_capacity(n + radii.len() * segments);
for i in 0..n {
let r = radii
.iter()
.find(|(idx, _)| *idx == i)
.map(|(_, r)| *r)
.unwrap_or(0.0);
if r > 1.0e-9 {
for pt in round_corner(sharp[(i + n - 1) % n], sharp[i], sharp[(i + 1) % n], r, segments)
{
push_dedup(&mut out, pt);
}
} else {
push_dedup(&mut out, sharp[i]);
}
}
if out.len() > 1 {
let (first, last) = (out[0], out[out.len() - 1]);
if (first.x - last.x).abs() <= 1.0e-9 && (first.y - last.y).abs() <= 1.0e-9 {
out.pop();
}
}
out
}
pub struct ProfileProcessor {
schema: IfcSchema,
active_quality: Cell<TessellationQuality>,
}
impl ProfileProcessor {
pub fn new(schema: IfcSchema) -> Self {
Self {
schema,
active_quality: Cell::new(TessellationQuality::Medium),
}
}
#[inline]
fn quality(&self) -> TessellationQuality {
self.active_quality.get()
}
#[inline]
pub fn set_tessellation_quality(&self, quality: TessellationQuality) {
self.active_quality.set(quality);
}
#[inline]
pub fn process(
&self,
profile: &DecodedEntity,
decoder: &mut EntityDecoder,
quality: TessellationQuality,
) -> Result<Profile2D> {
self.active_quality.set(quality);
self.process_with_depth(profile, decoder, 0)
}
fn process_with_depth(
&self,
profile: &DecodedEntity,
decoder: &mut EntityDecoder,
depth: u32,
) -> Result<Profile2D> {
if depth > MAX_PROFILE_DEPTH {
return Err(Error::geometry(format!(
"Profile nesting depth {} exceeds limit {} at #{}",
depth, MAX_PROFILE_DEPTH, profile.id
)));
}
match profile.ifc_type {
IfcType::IfcDerivedProfileDef | IfcType::IfcMirroredProfileDef => {
self.process_derived_with_depth(profile, decoder, depth)
}
_ => match self.schema.profile_category(&profile.ifc_type) {
Some(ProfileCategory::Parametric) => self.process_parametric(profile, decoder),
Some(ProfileCategory::Arbitrary) => self.process_arbitrary(profile, decoder),
Some(ProfileCategory::Composite) => self.process_composite_with_depth(profile, decoder, depth),
_ => Err(Error::geometry(format!(
"Unsupported profile type: {}",
profile.ifc_type
))),
},
}
}
#[inline]
fn process_parametric(
&self,
profile: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<Profile2D> {
let mut base_profile = match profile.ifc_type {
IfcType::IfcRectangleProfileDef => self.process_rectangle(profile),
IfcType::IfcRoundedRectangleProfileDef => self.process_rounded_rectangle(profile),
IfcType::IfcCircleProfileDef => self.process_circle(profile),
IfcType::IfcCircleHollowProfileDef => self.process_circle_hollow(profile),
IfcType::IfcRectangleHollowProfileDef => self.process_rectangle_hollow(profile),
IfcType::IfcIShapeProfileDef => self.process_i_shape(profile),
IfcType::IfcAsymmetricIShapeProfileDef => self.process_asymmetric_i_shape(profile),
IfcType::IfcLShapeProfileDef => self.process_l_shape(profile),
IfcType::IfcUShapeProfileDef => self.process_u_shape(profile),
IfcType::IfcTShapeProfileDef => self.process_t_shape(profile),
IfcType::IfcCShapeProfileDef => self.process_c_shape(profile),
IfcType::IfcZShapeProfileDef => self.process_z_shape(profile),
_ => Err(Error::geometry(format!(
"Unsupported parametric profile: {}",
profile.ifc_type
))),
}?;
base_profile.center_on_bbox();
if let Some(pos_attr) = profile.get(2) {
if !pos_attr.is_null() {
if let Some(pos_entity) = decoder.resolve_ref(pos_attr)? {
if pos_entity.ifc_type == IfcType::IfcAxis2Placement2D {
self.apply_profile_position(&mut base_profile, &pos_entity, decoder)?;
}
}
}
}
Ok(base_profile)
}
fn apply_profile_position(
&self,
profile: &mut Profile2D,
placement: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<()> {
let (loc_x, loc_y) = if let Some(loc_attr) = placement.get(0) {
if !loc_attr.is_null() {
if let Some(loc_entity) = decoder.resolve_ref(loc_attr)? {
let coords = loc_entity
.get(0)
.and_then(|v| v.as_list())
.ok_or_else(|| Error::geometry("Missing point 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);
(x, y)
} else {
(0.0, 0.0)
}
} else {
(0.0, 0.0)
}
} else {
(0.0, 0.0)
};
let (dir_x, dir_y) = if let Some(dir_attr) = placement.get(1) {
if !dir_attr.is_null() {
if let Some(dir_entity) = decoder.resolve_ref(dir_attr)? {
let ratios = dir_entity
.get(0)
.and_then(|v| v.as_list())
.ok_or_else(|| Error::geometry("Missing direction ratios".to_string()))?;
let x = ratios.first().and_then(|v| v.as_float()).unwrap_or(1.0);
let y = ratios.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
let len = (x * x + y * y).sqrt();
if len > 1e-10 {
(x / len, y / len)
} else {
(1.0, 0.0)
}
} else {
(1.0, 0.0)
}
} else {
(1.0, 0.0)
}
} else {
(1.0, 0.0)
};
if loc_x.abs() < 1e-10
&& loc_y.abs() < 1e-10
&& (dir_x - 1.0).abs() < 1e-10
&& dir_y.abs() < 1e-10
{
return Ok(());
}
let x_axis = (dir_x, dir_y);
let y_axis = (-dir_y, dir_x);
for point in &mut profile.outer {
let old_x = point.x;
let old_y = point.y;
point.x = old_x * x_axis.0 + old_y * y_axis.0 + loc_x;
point.y = old_x * x_axis.1 + old_y * y_axis.1 + loc_y;
}
for hole in &mut profile.holes {
for point in hole {
let old_x = point.x;
let old_y = point.y;
point.x = old_x * x_axis.0 + old_y * y_axis.0 + loc_x;
point.y = old_x * x_axis.1 + old_y * y_axis.1 + loc_y;
}
}
Ok(())
}
fn process_derived_with_depth(
&self,
profile: &DecodedEntity,
decoder: &mut EntityDecoder,
depth: u32,
) -> Result<Profile2D> {
let parent_attr = profile
.get(2)
.ok_or_else(|| Error::geometry("Derived profile missing ParentProfile".to_string()))?;
let parent_profile = decoder.resolve_ref(parent_attr)?.ok_or_else(|| {
Error::geometry("Derived profile ParentProfile not found".to_string())
})?;
let mut result = self.process_with_depth(&parent_profile, decoder, depth + 1)?;
if profile.ifc_type == IfcType::IfcMirroredProfileDef {
mirror_profile_about_y_axis(&mut result);
return Ok(result);
}
let Some(operator_attr) = profile.get(3) else {
return Ok(result);
};
if operator_attr.is_null() {
return Ok(result);
}
let Some(operator) = decoder.resolve_ref(operator_attr)? else {
return Ok(result);
};
self.apply_cartesian_transformation_operator_2d(&mut result, &operator, decoder)?;
Ok(result)
}
fn apply_cartesian_transformation_operator_2d(
&self,
profile: &mut Profile2D,
operator: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<()> {
let (origin_x, origin_y) = if let Some(origin_attr) = operator.get(2) {
if let Some(origin_entity) = decoder.resolve_ref(origin_attr)? {
let coords = origin_entity
.get(0)
.and_then(|v| v.as_list())
.ok_or_else(|| {
Error::geometry("Missing operator origin coordinates".to_string())
})?;
(
coords.first().and_then(|v| v.as_float()).unwrap_or(0.0),
coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0),
)
} else {
(0.0, 0.0)
}
} else {
(0.0, 0.0)
};
let scale_x = operator.get_float(3).unwrap_or(1.0);
let scale_y = match operator.ifc_type {
IfcType::IfcCartesianTransformationOperator2DnonUniform => {
operator.get_float(4).unwrap_or(scale_x)
}
_ => scale_x,
};
let axis1 = self.parse_operator_axis_2d(operator.get(0), decoder, (1.0, 0.0))?;
let axis2 = self.parse_operator_axis_2d(operator.get(1), decoder, (0.0, 1.0))?;
let (x_axis, y_axis) = match (axis1, axis2) {
(Some(x_axis), Some(y_axis)) => (x_axis, y_axis),
(Some(x_axis), None) => (x_axis, (-x_axis.1, x_axis.0)),
(None, Some(y_axis)) => ((y_axis.1, -y_axis.0), y_axis),
(None, None) => ((1.0, 0.0), (0.0, 1.0)),
};
for point in &mut profile.outer {
let old_x = point.x;
let old_y = point.y;
point.x = old_x * x_axis.0 * scale_x + old_y * y_axis.0 * scale_y + origin_x;
point.y = old_x * x_axis.1 * scale_x + old_y * y_axis.1 * scale_y + origin_y;
}
for hole in &mut profile.holes {
for point in hole {
let old_x = point.x;
let old_y = point.y;
point.x = old_x * x_axis.0 * scale_x + old_y * y_axis.0 * scale_y + origin_x;
point.y = old_x * x_axis.1 * scale_x + old_y * y_axis.1 * scale_y + origin_y;
}
}
let det = scale_x * scale_y * (x_axis.0 * y_axis.1 - y_axis.0 * x_axis.1);
if det < 0.0 {
profile.outer.reverse();
for hole in &mut profile.holes {
hole.reverse();
}
}
Ok(())
}
fn parse_operator_axis_2d(
&self,
axis_attr: Option<&AttributeValue>,
decoder: &mut EntityDecoder,
default: (f64, f64),
) -> Result<Option<(f64, f64)>> {
let Some(axis_attr) = axis_attr else {
return Ok(None);
};
if axis_attr.is_null() {
return Ok(None);
}
let Some(axis_entity) = decoder.resolve_ref(axis_attr)? else {
return Ok(None);
};
let ratios = axis_entity
.get(0)
.and_then(|v| v.as_list())
.ok_or_else(|| Error::geometry("Missing operator axis ratios".to_string()))?;
let x = ratios
.first()
.and_then(|v| v.as_float())
.unwrap_or(default.0);
let y = ratios
.get(1)
.and_then(|v| v.as_float())
.unwrap_or(default.1);
let len = (x * x + y * y).sqrt();
if len <= 1e-10 {
return Ok(Some(default));
}
Ok(Some((x / len, y / len)))
}
#[inline]
fn process_rectangle(&self, profile: &DecodedEntity) -> Result<Profile2D> {
let x_dim = profile
.get_float(3)
.ok_or_else(|| Error::geometry("Rectangle missing XDim".to_string()))?;
let y_dim = profile
.get_float(4)
.ok_or_else(|| Error::geometry("Rectangle missing YDim".to_string()))?;
let half_x = x_dim / 2.0;
let half_y = y_dim / 2.0;
let points = vec![
Point2::new(-half_x, -half_y),
Point2::new(half_x, -half_y),
Point2::new(half_x, half_y),
Point2::new(-half_x, half_y),
];
Ok(Profile2D::new(points))
}
fn process_rounded_rectangle(&self, profile: &DecodedEntity) -> Result<Profile2D> {
let x_dim = profile
.get_float(3)
.ok_or_else(|| Error::geometry("RoundedRectangle missing XDim".to_string()))?;
let y_dim = profile
.get_float(4)
.ok_or_else(|| Error::geometry("RoundedRectangle missing YDim".to_string()))?;
let radius = profile
.get_float(5)
.ok_or_else(|| Error::geometry("RoundedRectangle missing RoundingRadius".to_string()))?;
let half_x = x_dim / 2.0;
let half_y = y_dim / 2.0;
let r = radius.max(0.0).min(half_x).min(half_y);
if r < 1.0e-9 {
return self.process_rectangle(profile);
}
Ok(Profile2D::new(rounded_rectangle_outline(
half_x,
half_y,
r,
true,
self.quality(),
)))
}
#[inline]
fn process_circle(&self, profile: &DecodedEntity) -> Result<Profile2D> {
let radius = profile
.get_float(3)
.ok_or_else(|| Error::geometry("Circle missing Radius".to_string()))?;
let segments = self.quality().circle_profile_segments(36);
let mut points = Vec::with_capacity(segments);
for i in 0..segments {
let angle = (i as f64) * 2.0 * PI / (segments as f64);
let x = radius * angle.cos();
let y = radius * angle.sin();
points.push(Point2::new(x, y));
}
Ok(Profile2D::new(points))
}
fn process_i_shape(&self, profile: &DecodedEntity) -> Result<Profile2D> {
let overall_width = profile
.get_float(3)
.ok_or_else(|| Error::geometry("I-Shape missing OverallWidth".to_string()))?;
let overall_depth = profile
.get_float(4)
.ok_or_else(|| Error::geometry("I-Shape missing OverallDepth".to_string()))?;
let web_thickness = profile
.get_float(5)
.ok_or_else(|| Error::geometry("I-Shape missing WebThickness".to_string()))?;
let flange_thickness = profile
.get_float(6)
.ok_or_else(|| Error::geometry("I-Shape missing FlangeThickness".to_string()))?;
let fillet = profile
.get_float(7)
.unwrap_or(0.0)
.clamp(0.0, ((overall_depth - 2.0 * flange_thickness) * 0.5)
.min((overall_width - web_thickness) * 0.5)
.max(0.0));
let half_width = overall_width / 2.0;
let half_depth = overall_depth / 2.0;
let half_web = web_thickness / 2.0;
let ftf_bot = -half_depth + flange_thickness;
let ftf_top = half_depth - flange_thickness;
let sharp = [
Point2::new(-half_width, -half_depth), Point2::new(half_width, -half_depth), Point2::new(half_width, ftf_bot), Point2::new(half_web, ftf_bot), Point2::new(half_web, ftf_top), Point2::new(half_width, ftf_top), Point2::new(half_width, half_depth), Point2::new(-half_width, half_depth), Point2::new(-half_width, ftf_top), Point2::new(-half_web, ftf_top), Point2::new(-half_web, ftf_bot), Point2::new(-half_width, ftf_bot), ];
let seg = self.quality().profile_arc_segments(6, 2);
let radii = [(3, fillet), (4, fillet), (9, fillet), (10, fillet)];
Ok(Profile2D::new(fillet_outline(&sharp, &radii, seg)))
}
fn process_asymmetric_i_shape(&self, profile: &DecodedEntity) -> Result<Profile2D> {
let bottom_width = profile
.get_float(3)
.ok_or_else(|| Error::geometry("AsymmetricI missing BottomFlangeWidth".to_string()))?;
let overall_depth = profile
.get_float(4)
.ok_or_else(|| Error::geometry("AsymmetricI missing OverallDepth".to_string()))?;
let web_thickness = profile
.get_float(5)
.ok_or_else(|| Error::geometry("AsymmetricI missing WebThickness".to_string()))?;
let bottom_flange_thickness = profile
.get_float(6)
.ok_or_else(|| Error::geometry("AsymmetricI missing BottomFlangeThickness".to_string()))?;
let top_width = profile
.get_float(8)
.ok_or_else(|| Error::geometry("AsymmetricI missing TopFlangeWidth".to_string()))?;
let top_flange_thickness = profile.get_float(9).unwrap_or(bottom_flange_thickness);
if overall_depth <= bottom_flange_thickness + top_flange_thickness {
return Err(Error::geometry(format!(
"AsymmetricI: OverallDepth {} must exceed BottomFlangeThickness + \
TopFlangeThickness ({} + {} = {})",
overall_depth,
bottom_flange_thickness,
top_flange_thickness,
bottom_flange_thickness + top_flange_thickness,
)));
}
let half_overall_width = bottom_width.max(top_width) * 0.5;
let half_depth = overall_depth * 0.5;
let half_web = web_thickness * 0.5;
let half_bottom = bottom_width * 0.5;
let half_top = top_width * 0.5;
let _ = half_overall_width; let points = vec![
Point2::new(-half_bottom, -half_depth),
Point2::new(half_bottom, -half_depth),
Point2::new(half_bottom, -half_depth + bottom_flange_thickness),
Point2::new(half_web, -half_depth + bottom_flange_thickness),
Point2::new(half_web, half_depth - top_flange_thickness),
Point2::new(half_top, half_depth - top_flange_thickness),
Point2::new(half_top, half_depth),
Point2::new(-half_top, half_depth),
Point2::new(-half_top, half_depth - top_flange_thickness),
Point2::new(-half_web, half_depth - top_flange_thickness),
Point2::new(-half_web, -half_depth + bottom_flange_thickness),
Point2::new(-half_bottom, -half_depth + bottom_flange_thickness),
];
Ok(Profile2D::new(points))
}
fn process_circle_hollow(&self, profile: &DecodedEntity) -> Result<Profile2D> {
let radius = profile
.get_float(3)
.ok_or_else(|| Error::geometry("CircleHollow missing Radius".to_string()))?;
let wall_thickness = profile
.get_float(4)
.ok_or_else(|| Error::geometry("CircleHollow missing WallThickness".to_string()))?;
let inner_radius = radius - wall_thickness;
let segments = self.quality().circle_profile_segments(36);
let mut outer_points = Vec::with_capacity(segments);
for i in 0..segments {
let angle = (i as f64) * 2.0 * PI / (segments as f64);
outer_points.push(Point2::new(radius * angle.cos(), radius * angle.sin()));
}
let mut inner_points = Vec::with_capacity(segments);
for i in (0..segments).rev() {
let angle = (i as f64) * 2.0 * PI / (segments as f64);
inner_points.push(Point2::new(
inner_radius * angle.cos(),
inner_radius * angle.sin(),
));
}
let mut result = Profile2D::new(outer_points);
result.add_hole(inner_points);
Ok(result)
}
fn process_rectangle_hollow(&self, profile: &DecodedEntity) -> Result<Profile2D> {
let x_dim = profile
.get_float(3)
.ok_or_else(|| Error::geometry("RectangleHollow missing XDim".to_string()))?;
let y_dim = profile
.get_float(4)
.ok_or_else(|| Error::geometry("RectangleHollow missing YDim".to_string()))?;
let wall_thickness = profile
.get_float(5)
.ok_or_else(|| Error::geometry("RectangleHollow missing WallThickness".to_string()))?;
let half_x = x_dim / 2.0;
let half_y = y_dim / 2.0;
if wall_thickness >= half_x || wall_thickness >= half_y {
return Err(Error::geometry(format!(
"RectangleHollow WallThickness {} exceeds half dimensions ({}, {})",
wall_thickness, half_x, half_y
)));
}
let inner_half_x = half_x - wall_thickness;
let inner_half_y = half_y - wall_thickness;
let inner_fillet = profile
.get_float(6)
.unwrap_or(0.0)
.max(0.0)
.min(inner_half_x)
.min(inner_half_y);
let outer_fillet = profile
.get_float(7)
.unwrap_or(0.0)
.max(0.0)
.min(half_x)
.min(half_y);
let q = self.quality();
let outer_points =
rounded_rectangle_outline(half_x, half_y, outer_fillet, true, q);
let inner_points =
rounded_rectangle_outline(inner_half_x, inner_half_y, inner_fillet, false, q);
let mut result = Profile2D::new(outer_points);
result.add_hole(inner_points);
Ok(result)
}
fn process_l_shape(&self, profile: &DecodedEntity) -> Result<Profile2D> {
let depth = profile
.get_float(3)
.ok_or_else(|| Error::geometry("L-Shape missing Depth".to_string()))?;
let width = profile
.get_float(4)
.ok_or_else(|| Error::geometry("L-Shape missing Width".to_string()))?;
let t = profile
.get_float(5)
.ok_or_else(|| Error::geometry("L-Shape missing Thickness".to_string()))?;
let rf = profile
.get_float(6)
.unwrap_or(0.0)
.clamp(0.0, (width - t).min(depth - t).max(0.0));
let re = profile.get_float(7).unwrap_or(0.0).clamp(0.0, t * 0.999);
let seg = self.quality().profile_arc_segments(6, 2);
let half_pi = std::f64::consts::FRAC_PI_2;
let pi = std::f64::consts::PI;
let mut p: Vec<Point2<f64>> = Vec::new();
p.push(Point2::new(0.0, 0.0)); p.push(Point2::new(width, 0.0)); if re > 1.0e-9 {
push_arc(&mut p, width - re, t - re, re, 0.0, half_pi, seg);
} else {
p.push(Point2::new(width, t));
}
if rf > 1.0e-9 {
push_arc(&mut p, t + rf, t + rf, rf, 1.5 * pi, pi, seg);
} else {
p.push(Point2::new(t, t));
}
if re > 1.0e-9 {
push_arc(&mut p, t - re, depth - re, re, 0.0, half_pi, seg);
} else {
p.push(Point2::new(t, depth));
}
p.push(Point2::new(0.0, depth));
Ok(Profile2D::new(p))
}
fn process_u_shape(&self, profile: &DecodedEntity) -> Result<Profile2D> {
let depth = profile
.get_float(3)
.ok_or_else(|| Error::geometry("U-Shape missing Depth".to_string()))?;
let flange_width = profile
.get_float(4)
.ok_or_else(|| Error::geometry("U-Shape missing FlangeWidth".to_string()))?;
let web_thickness = profile
.get_float(5)
.ok_or_else(|| Error::geometry("U-Shape missing WebThickness".to_string()))?;
let flange_thickness = profile
.get_float(6)
.ok_or_else(|| Error::geometry("U-Shape missing FlangeThickness".to_string()))?;
let half_depth = depth / 2.0;
let ft = flange_thickness;
let rf = profile
.get_float(7)
.unwrap_or(0.0)
.clamp(0.0, (flange_width - web_thickness).min(half_depth - ft).max(0.0));
let re = profile.get_float(8).unwrap_or(0.0).clamp(0.0, ft * 0.999);
let sharp = [
Point2::new(0.0, -half_depth), Point2::new(flange_width, -half_depth), Point2::new(flange_width, -half_depth + ft), Point2::new(web_thickness, -half_depth + ft), Point2::new(web_thickness, half_depth - ft), Point2::new(flange_width, half_depth - ft), Point2::new(flange_width, half_depth), Point2::new(0.0, half_depth), ];
let seg = self.quality().profile_arc_segments(6, 2);
let radii = [(2, re), (3, rf), (4, rf), (5, re)];
Ok(Profile2D::new(fillet_outline(&sharp, &radii, seg)))
}
fn process_t_shape(&self, profile: &DecodedEntity) -> Result<Profile2D> {
let depth = profile
.get_float(3)
.ok_or_else(|| Error::geometry("T-Shape missing Depth".to_string()))?;
let flange_width = profile
.get_float(4)
.ok_or_else(|| Error::geometry("T-Shape missing FlangeWidth".to_string()))?;
let web_thickness = profile
.get_float(5)
.ok_or_else(|| Error::geometry("T-Shape missing WebThickness".to_string()))?;
let flange_thickness = profile
.get_float(6)
.ok_or_else(|| Error::geometry("T-Shape missing FlangeThickness".to_string()))?;
let half_flange = flange_width / 2.0;
let half_web = web_thickness / 2.0;
let ft = flange_thickness;
let ftf = depth - ft; let rf = profile
.get_float(7)
.unwrap_or(0.0)
.clamp(0.0, (half_flange - half_web).min(ftf).max(0.0));
let r_fl = profile.get_float(8).unwrap_or(0.0).clamp(0.0, ft * 0.999);
let r_web = profile.get_float(9).unwrap_or(0.0).clamp(0.0, half_web * 0.999);
let sharp = [
Point2::new(-half_web, 0.0), Point2::new(-half_web, ftf), Point2::new(-half_flange, ftf), Point2::new(-half_flange, depth), Point2::new(half_flange, depth), Point2::new(half_flange, ftf), Point2::new(half_web, ftf), Point2::new(half_web, 0.0), ];
let seg = self.quality().profile_arc_segments(6, 2);
let radii = [
(0, r_web),
(1, rf),
(2, r_fl),
(5, r_fl),
(6, rf),
(7, r_web),
];
Ok(Profile2D::new(fillet_outline(&sharp, &radii, seg)))
}
fn process_c_shape(&self, profile: &DecodedEntity) -> Result<Profile2D> {
let depth = profile
.get_float(3)
.ok_or_else(|| Error::geometry("C-Shape missing Depth".to_string()))?;
let width = profile
.get_float(4)
.ok_or_else(|| Error::geometry("C-Shape missing Width".to_string()))?;
let wall_thickness = profile
.get_float(5)
.ok_or_else(|| Error::geometry("C-Shape missing WallThickness".to_string()))?;
let girth = profile.get_float(6).unwrap_or(wall_thickness * 2.0);
let half_depth = depth / 2.0;
let t = wall_thickness;
let points = vec![
Point2::new(0.0, -half_depth), Point2::new(width, -half_depth), Point2::new(width, -half_depth + girth), Point2::new(width - t, -half_depth + girth), Point2::new(width - t, -half_depth + t), Point2::new(t, -half_depth + t), Point2::new(t, half_depth - t), Point2::new(width - t, half_depth - t), Point2::new(width - t, half_depth - girth), Point2::new(width, half_depth - girth), Point2::new(width, half_depth), Point2::new(0.0, half_depth), ];
Ok(Profile2D::new(points))
}
fn process_z_shape(&self, profile: &DecodedEntity) -> Result<Profile2D> {
let depth = profile
.get_float(3)
.ok_or_else(|| Error::geometry("Z-Shape missing Depth".to_string()))?;
let flange_width = profile
.get_float(4)
.ok_or_else(|| Error::geometry("Z-Shape missing FlangeWidth".to_string()))?;
let web_thickness = profile
.get_float(5)
.ok_or_else(|| Error::geometry("Z-Shape missing WebThickness".to_string()))?;
let flange_thickness = profile
.get_float(6)
.ok_or_else(|| Error::geometry("Z-Shape missing FlangeThickness".to_string()))?;
let half_depth = depth / 2.0;
let half_web = web_thickness / 2.0;
let points = vec![
Point2::new(-half_web, -half_depth),
Point2::new(-half_web - flange_width, -half_depth),
Point2::new(-half_web - flange_width, -half_depth + flange_thickness),
Point2::new(-half_web, -half_depth + flange_thickness),
Point2::new(-half_web, half_depth - flange_thickness),
Point2::new(half_web, half_depth - flange_thickness),
Point2::new(half_web, half_depth),
Point2::new(half_web + flange_width, half_depth),
Point2::new(half_web + flange_width, half_depth - flange_thickness),
Point2::new(half_web, half_depth - flange_thickness),
Point2::new(half_web, -half_depth + flange_thickness),
Point2::new(-half_web, -half_depth + flange_thickness),
];
Ok(Profile2D::new(points))
}
fn process_arbitrary(
&self,
profile: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<Profile2D> {
let curve_attr = profile
.get(2)
.ok_or_else(|| Error::geometry("Arbitrary profile missing OuterCurve".to_string()))?;
let curve = decoder
.resolve_ref(curve_attr)?
.ok_or_else(|| Error::geometry("Failed to resolve OuterCurve".to_string()))?;
let raw_outer = self.process_curve(&curve, decoder)?;
let outer_points = simplify_smooth_curve_polyline(&raw_outer);
let mut result = Profile2D::new(outer_points);
if profile.ifc_type == IfcType::IfcArbitraryProfileDefWithVoids {
if let Some(inner_curves_attr) = profile.get(3) {
let inner_curves = decoder.resolve_ref_list(inner_curves_attr)?;
for inner_curve in inner_curves {
let raw_hole = self.process_curve(&inner_curve, decoder)?;
let hole_points = simplify_smooth_curve_polyline(&raw_hole);
result.add_hole(hole_points);
}
}
}
Ok(result)
}
#[inline]
fn process_curve(
&self,
curve: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<Vec<Point2<f64>>> {
self.process_curve_with_depth(curve, decoder, 0)
}
fn process_curve_with_depth(
&self,
curve: &DecodedEntity,
decoder: &mut EntityDecoder,
depth: u32,
) -> Result<Vec<Point2<f64>>> {
if depth > MAX_CURVE_DEPTH {
return Err(Error::geometry(format!(
"Curve nesting depth {} exceeds limit {}",
depth, MAX_CURVE_DEPTH
)));
}
match curve.ifc_type {
IfcType::IfcPolyline => self.process_polyline(curve, decoder),
IfcType::IfcIndexedPolyCurve => self.process_indexed_polycurve(curve, decoder),
IfcType::IfcCompositeCurve => {
self.process_composite_curve_with_depth(curve, decoder, depth)
}
IfcType::IfcTrimmedCurve => {
self.process_trimmed_curve_with_depth(curve, decoder, depth)
}
IfcType::IfcCircle => self.process_circle_curve(curve, decoder),
IfcType::IfcEllipse => self.process_ellipse_curve(curve, decoder),
_ => Err(Error::geometry(format!(
"Unsupported curve type: {}",
curve.ifc_type
))),
}
}
#[inline]
pub fn get_curve_points(
&self,
curve: &DecodedEntity,
decoder: &mut EntityDecoder,
quality: TessellationQuality,
) -> Result<Vec<Point3<f64>>> {
self.active_quality.set(quality);
self.get_curve_points_with_depth(curve, decoder, 0)
}
fn get_curve_points_with_depth(
&self,
curve: &DecodedEntity,
decoder: &mut EntityDecoder,
depth: u32,
) -> Result<Vec<Point3<f64>>> {
if depth > MAX_CURVE_DEPTH {
return Err(Error::geometry(format!(
"Curve nesting depth {} exceeds limit {}",
depth, MAX_CURVE_DEPTH
)));
}
match curve.ifc_type {
IfcType::IfcPolyline => self.process_polyline_3d(curve, decoder),
IfcType::IfcCompositeCurve => {
self.process_composite_curve_3d_with_depth(curve, decoder, depth)
}
IfcType::IfcGradientCurve => {
if let Some(base_attr) = curve.get(2) {
if !base_attr.is_null() {
if let Some(base) = decoder.resolve_ref(base_attr)? {
return self.get_curve_points_with_depth(&base, decoder, depth + 1);
}
}
}
self.process_composite_curve_3d_with_depth(curve, decoder, depth)
}
IfcType::IfcCircle => self.process_circle_3d(curve, decoder),
IfcType::IfcIndexedPolyCurve => {
self.process_indexed_polycurve_3d(curve, decoder)
}
IfcType::IfcTrimmedCurve => {
let points_2d = self.process_trimmed_curve_with_depth(curve, decoder, depth)?;
Ok(points_2d
.into_iter()
.map(|p| Point3::new(p.x, p.y, 0.0))
.collect())
}
_ => {
let points_2d = self.process_curve_with_depth(curve, decoder, depth)?;
Ok(points_2d
.into_iter()
.map(|p| Point3::new(p.x, p.y, 0.0))
.collect())
}
}
}
fn process_circle_3d(
&self,
curve: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<Vec<Point3<f64>>> {
let position_attr = curve
.get(0)
.ok_or_else(|| Error::geometry("Circle missing Position".to_string()))?;
let radius = curve
.get_float(1)
.ok_or_else(|| Error::geometry("Circle missing Radius".to_string()))?;
let position = decoder
.resolve_ref(position_attr)?
.ok_or_else(|| Error::geometry("Failed to resolve circle position".to_string()))?;
let (center, x_axis, y_axis) = if position.ifc_type == IfcType::IfcAxis2Placement3D {
let loc_attr = position
.get(0)
.ok_or_else(|| Error::geometry("Axis2Placement3D missing Location".to_string()))?;
let loc = decoder
.resolve_ref(loc_attr)?
.ok_or_else(|| Error::geometry("Failed to resolve location".to_string()))?;
let coords = loc
.get(0)
.and_then(|v| v.as_list())
.ok_or_else(|| Error::geometry("Location missing coordinates".to_string()))?;
let center = Point3::new(
coords.first().and_then(|v| v.as_float()).unwrap_or(0.0),
coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0),
coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0),
);
let z_axis = if let Some(axis_attr) = position.get(1) {
if !axis_attr.is_null() {
let axis = decoder.resolve_ref(axis_attr)?;
if let Some(axis) = axis {
let coords = axis.get(0).and_then(|v| v.as_list());
if let Some(coords) = coords {
Vector3::new(
coords.first().and_then(|v| v.as_float()).unwrap_or(0.0),
coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0),
coords.get(2).and_then(|v| v.as_float()).unwrap_or(1.0),
)
.normalize()
} else {
Vector3::new(0.0, 0.0, 1.0)
}
} else {
Vector3::new(0.0, 0.0, 1.0)
}
} else {
Vector3::new(0.0, 0.0, 1.0)
}
} else {
Vector3::new(0.0, 0.0, 1.0)
};
let x_axis = if let Some(ref_attr) = position.get(2) {
if !ref_attr.is_null() {
let ref_dir = decoder.resolve_ref(ref_attr)?;
if let Some(ref_dir) = ref_dir {
let coords = ref_dir.get(0).and_then(|v| v.as_list());
if let Some(coords) = coords {
Vector3::new(
coords.first().and_then(|v| v.as_float()).unwrap_or(1.0),
coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0),
coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0),
)
.normalize()
} else {
Vector3::new(1.0, 0.0, 0.0)
}
} else {
Vector3::new(1.0, 0.0, 0.0)
}
} else {
Vector3::new(1.0, 0.0, 0.0)
}
} else {
Vector3::new(1.0, 0.0, 0.0)
};
let y_axis = z_axis.cross(&x_axis).normalize();
(center, x_axis, y_axis)
} else {
let loc_attr = position.get(0);
let (cx, cy) = if let Some(attr) = loc_attr {
let loc = decoder.resolve_ref(attr)?;
if let Some(loc) = loc {
let coords = loc.get(0).and_then(|v| v.as_list());
if let Some(coords) = coords {
(
coords.first().and_then(|v| v.as_float()).unwrap_or(0.0),
coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0),
)
} else {
(0.0, 0.0)
}
} else {
(0.0, 0.0)
}
} else {
(0.0, 0.0)
};
(
Point3::new(cx, cy, 0.0),
Vector3::new(1.0, 0.0, 0.0),
Vector3::new(0.0, 1.0, 0.0),
)
};
let segments = scale_segments(24, 8, 96, self.quality());
let mut points = Vec::with_capacity(segments + 1);
for i in 0..=segments {
let angle = 2.0 * std::f64::consts::PI * i as f64 / segments as f64;
let p = center + x_axis * (radius * angle.cos()) + y_axis * (radius * angle.sin());
points.push(p);
}
Ok(points)
}
fn process_polyline_3d(
&self,
curve: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<Vec<Point3<f64>>> {
let points_attr = curve
.get(0)
.ok_or_else(|| Error::geometry("Polyline missing Points".to_string()))?;
let points = decoder.resolve_ref_list(points_attr)?;
let mut result = Vec::with_capacity(points.len());
for point in points {
let coords_attr = point
.get(0)
.ok_or_else(|| Error::geometry("CartesianPoint missing Coordinates".to_string()))?;
let coords = coords_attr
.as_list()
.ok_or_else(|| Error::geometry("Coordinates is not a list".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);
result.push(Point3::new(x, y, z));
}
Ok(result)
}
fn process_composite_curve_3d_with_depth(
&self,
curve: &DecodedEntity,
decoder: &mut EntityDecoder,
depth: u32,
) -> Result<Vec<Point3<f64>>> {
let segments_attr = curve
.get(0)
.ok_or_else(|| Error::geometry("CompositeCurve missing Segments".to_string()))?;
let segments = decoder.resolve_ref_list(segments_attr)?;
let mut result = Vec::new();
let mut last_curve_segment_terminal: Option<Point3<f64>> = None;
for segment in segments {
if segment.ifc_type == IfcType::IfcCurveSegment {
if let Some(placement_attr) = segment.get(1) {
if !placement_attr.is_null() {
if let Some(placement) = decoder.resolve_ref(placement_attr)? {
if let Some((origin, x_axis)) =
axis2_placement_location_and_x_axis_3d(&placement, decoder)
{
if result.last().map_or(true, |last: &Point3<f64>| {
(last - origin).norm() > 1e-9
}) {
result.push(origin);
}
let segment_length = segment
.get(3)
.and_then(|a| a.as_float())
.unwrap_or(0.0);
if segment_length > 1e-9 {
last_curve_segment_terminal =
Some(origin + x_axis * segment_length);
} else {
last_curve_segment_terminal = None;
}
continue;
}
}
}
}
last_curve_segment_terminal = None;
continue;
}
last_curve_segment_terminal = None;
let parent_curve_attr = segment.get(2).ok_or_else(|| {
Error::geometry("CompositeCurveSegment missing ParentCurve".to_string())
})?;
let parent_curve = decoder
.resolve_ref(parent_curve_attr)?
.ok_or_else(|| Error::geometry("Failed to resolve ParentCurve".to_string()))?;
let same_sense = segment
.get(1)
.and_then(|v| match v {
ifc_lite_core::AttributeValue::Enum(e) => Some(e.as_str()),
_ => None,
})
.map(|e| e == "T" || e == "TRUE")
.unwrap_or(true);
let mut segment_points =
self.get_curve_points_with_depth(&parent_curve, decoder, depth + 1)?;
if !same_sense {
segment_points.reverse();
}
if !result.is_empty() && !segment_points.is_empty() {
result.extend(segment_points.into_iter().skip(1));
} else {
result.extend(segment_points);
}
}
if let Some(terminal) = last_curve_segment_terminal {
if result.last().map_or(true, |last: &Point3<f64>| {
(last - terminal).norm() > 1e-9
}) {
result.push(terminal);
}
}
Ok(result)
}
pub fn get_composite_curve_points_trimmed(
&self,
curve: &DecodedEntity,
decoder: &mut EntityDecoder,
start_param: Option<f64>,
end_param: Option<f64>,
) -> Result<Vec<Point3<f64>>> {
let segments_attr = curve
.get(0)
.ok_or_else(|| Error::geometry("CompositeCurve missing Segments".to_string()))?;
let segments = decoder.resolve_ref_list(segments_attr)?;
let num_segments = segments.len();
if num_segments == 0 {
return Ok(Vec::new());
}
let start = start_param.unwrap_or(0.0).max(0.0);
let end = end_param.unwrap_or(num_segments as f64).min(num_segments as f64);
if end <= start {
return Ok(Vec::new());
}
let mut result: Vec<Point3<f64>> = Vec::new();
for (idx, segment) in segments.into_iter().enumerate() {
let seg_start = idx as f64;
let seg_end = seg_start + 1.0;
if seg_end <= start || seg_start >= end {
continue;
}
let parent_curve_attr = segment.get(2).ok_or_else(|| {
Error::geometry("CompositeCurveSegment missing ParentCurve".to_string())
})?;
let parent_curve = decoder
.resolve_ref(parent_curve_attr)?
.ok_or_else(|| Error::geometry("Failed to resolve ParentCurve".to_string()))?;
let same_sense = segment
.get(1)
.and_then(|v| match v {
ifc_lite_core::AttributeValue::Enum(e) => Some(e.as_str()),
_ => None,
})
.map(|e| e == "T" || e == "TRUE")
.unwrap_or(true);
let mut seg_points = self.get_curve_points_with_depth(&parent_curve, decoder, 1)?;
if !same_sense {
seg_points.reverse();
}
if seg_points.len() < 2 {
continue;
}
let local_start = (start - seg_start).clamp(0.0, 1.0);
let local_end = (end - seg_start).clamp(0.0, 1.0);
if local_end <= local_start {
continue;
}
let trimmed = if local_start == 0.0 && local_end == 1.0 {
seg_points
} else {
trim_polyline(&seg_points, local_start, local_end)
};
if trimmed.is_empty() {
continue;
}
const JUNCTION_EPS: f64 = 1e-6;
let mut iter = trimmed.into_iter();
if let Some(first) = iter.next() {
let coincident = result.last().map_or(false, |last| {
(first.x - last.x).abs() < JUNCTION_EPS
&& (first.y - last.y).abs() < JUNCTION_EPS
&& (first.z - last.z).abs() < JUNCTION_EPS
});
if !coincident {
result.push(first);
}
result.extend(iter);
}
}
Ok(result)
}
pub fn get_polyline_points_trimmed(
&self,
curve: &DecodedEntity,
decoder: &mut EntityDecoder,
start_param: Option<f64>,
end_param: Option<f64>,
) -> Result<Vec<Point3<f64>>> {
let points = self.process_polyline_3d(curve, decoder)?;
if points.len() < 2 {
return Ok(points);
}
let max_param = (points.len() - 1) as f64;
let s = start_param.unwrap_or(0.0).clamp(0.0, max_param);
let e = end_param.unwrap_or(max_param).clamp(0.0, max_param);
if e <= s {
return Ok(Vec::new());
}
Ok(trim_polyline(&points, s / max_param, e / max_param))
}
fn process_trimmed_curve_with_depth(
&self,
curve: &DecodedEntity,
decoder: &mut EntityDecoder,
depth: u32,
) -> Result<Vec<Point2<f64>>> {
let basis_attr = curve
.get(0)
.ok_or_else(|| Error::geometry("TrimmedCurve missing BasisCurve".to_string()))?;
let basis_curve = decoder
.resolve_ref(basis_attr)?
.ok_or_else(|| Error::geometry("Failed to resolve BasisCurve".to_string()))?;
let prefer_cartesian = curve
.get(4)
.and_then(|v| v.as_enum())
.map(|m| m == "CARTESIAN")
.unwrap_or(false);
let trim1 = curve
.get(1)
.and_then(|v| self.extract_trim_select(v, prefer_cartesian, decoder));
let trim2 = curve
.get(2)
.and_then(|v| self.extract_trim_select(v, prefer_cartesian, decoder));
let sense = curve
.get(3)
.and_then(|v| match v {
ifc_lite_core::AttributeValue::Enum(s) => Some(s == "T"),
_ => None,
})
.unwrap_or(true);
match basis_curve.ifc_type {
IfcType::IfcCircle | IfcType::IfcEllipse => {
self.process_trimmed_conic(&basis_curve, trim1, trim2, sense, decoder)
}
_ => {
self.process_curve_with_depth(&basis_curve, decoder, depth + 1)
}
}
}
fn extract_trim_select(
&self,
attr: &ifc_lite_core::AttributeValue,
prefer_cartesian: bool,
decoder: &mut EntityDecoder,
) -> Option<TrimSelect> {
let list = attr.as_list()?;
let mut param: Option<f64> = None;
let mut point: Option<Point2<f64>> = None;
for item in list {
if let Some(inner_list) = item.as_list() {
if let Some(type_name) = inner_list.first().and_then(|v| v.as_string()) {
if type_name == "IFCPARAMETERVALUE" {
param = inner_list.get(1).and_then(|v| v.as_float());
continue;
}
}
}
if item.as_entity_ref().is_some() {
if let Ok(Some(pt)) = decoder.resolve_ref(item) {
if pt.ifc_type == IfcType::IfcCartesianPoint {
if let Some(coords) = pt.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);
point = Some(Point2::new(x, y));
}
}
}
continue;
}
if let Some(f) = item.as_float() {
param = Some(f);
}
}
match (prefer_cartesian, point, param) {
(true, Some(p), _) => Some(TrimSelect::Cartesian(p)),
(_, _, Some(f)) => Some(TrimSelect::Parameter(f)),
(_, Some(p), None) => Some(TrimSelect::Cartesian(p)),
_ => None,
}
}
fn process_trimmed_conic(
&self,
basis: &DecodedEntity,
trim1: Option<TrimSelect>,
trim2: Option<TrimSelect>,
sense: bool,
decoder: &mut EntityDecoder,
) -> Result<Vec<Point2<f64>>> {
let radius = basis.get_float(1).unwrap_or(1.0);
let radius2 = if basis.ifc_type == IfcType::IfcEllipse {
basis.get_float(2).unwrap_or(radius)
} else {
radius
};
let (center, rotation) = self.get_placement_2d(basis, decoder)?;
let angle_scale = decoder.plane_angle_to_radians();
let to_angle = |trim: &TrimSelect| -> f64 {
match trim {
TrimSelect::Parameter(v) => v * angle_scale,
TrimSelect::Cartesian(p) => {
let dx = p.x - center.x;
let dy = p.y - center.y;
let lx = dx * rotation.cos() + dy * rotation.sin();
let ly = -dx * rotation.sin() + dy * rotation.cos();
(ly / radius2).atan2(lx / radius)
}
}
};
let start_angle = trim1.as_ref().map(&to_angle).unwrap_or(0.0);
let mut end_angle = trim2
.as_ref()
.map(&to_angle)
.unwrap_or(2.0 * std::f64::consts::PI);
if sense && end_angle < start_angle {
end_angle += 2.0 * std::f64::consts::PI;
} else if !sense && end_angle > start_angle {
end_angle -= 2.0 * std::f64::consts::PI;
}
let arc_angle = (end_angle - start_angle).abs();
let by_angle = (arc_angle / std::f64::consts::FRAC_PI_2 * 8.0).ceil() as usize;
let by_chord = {
const CHORD_TOL_M: f64 = 5.0e-4; let r_eff = radius.abs().max(radius2.abs());
let radius_m = r_eff * decoder.length_unit_scale();
if radius_m > CHORD_TOL_M {
let rel = (CHORD_TOL_M / radius_m).clamp(1e-9, 0.5);
let max_step = 2.0 * (1.0 - rel).acos();
if max_step > 1e-9 {
(arc_angle / max_step).ceil() as usize
} else {
0
}
} else {
0
}
};
let num_segments = self
.quality()
.profile_arc_segments(by_angle.max(by_chord), 2)
.min(128);
let mut points = Vec::with_capacity(num_segments + 1);
let angle_range = if sense {
end_angle - start_angle
} else {
start_angle - end_angle
};
for i in 0..=num_segments {
let t = i as f64 / num_segments as f64;
let angle = if sense {
start_angle + t * angle_range
} else {
start_angle - t * angle_range.abs()
};
let x = radius * angle.cos();
let y = radius2 * angle.sin();
let rx = x * rotation.cos() - y * rotation.sin() + center.x;
let ry = x * rotation.sin() + y * rotation.cos() + center.y;
points.push(Point2::new(rx, ry));
}
Ok(points)
}
fn get_placement_2d(
&self,
entity: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<(Point2<f64>, f64)> {
let placement_attr = match entity.get(0) {
Some(attr) if !attr.is_null() => attr,
_ => return Ok((Point2::new(0.0, 0.0), 0.0)),
};
let placement = match decoder.resolve_ref(placement_attr)? {
Some(p) => p,
None => return Ok((Point2::new(0.0, 0.0), 0.0)),
};
let location_attr = placement.get(0);
let center = if let Some(loc_attr) = location_attr {
if let Some(loc) = decoder.resolve_ref(loc_attr)? {
let coords = loc.get(0).and_then(|v| v.as_list());
if let Some(coords) = coords {
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);
Point2::new(x, y)
} else {
Point2::new(0.0, 0.0)
}
} else {
Point2::new(0.0, 0.0)
}
} else {
Point2::new(0.0, 0.0)
};
let ref_dir_attr_index = if placement.ifc_type == IfcType::IfcAxis2Placement3D {
2
} else {
1
};
let rotation = if let Some(dir_attr) = placement.get(ref_dir_attr_index) {
if let Some(dir) = decoder.resolve_ref(dir_attr)? {
let ratios = dir.get(0).and_then(|v| v.as_list());
if let Some(ratios) = ratios {
let x = ratios.first().and_then(|v| v.as_float()).unwrap_or(1.0);
let y = ratios.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
y.atan2(x)
} else {
0.0
}
} else {
0.0
}
} else {
0.0
};
Ok((center, rotation))
}
fn process_circle_curve(
&self,
curve: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<Vec<Point2<f64>>> {
let radius = curve.get_float(1).unwrap_or(1.0);
let (center, rotation) = self.get_placement_2d(curve, decoder)?;
let segments = self.quality().circle_profile_segments(36);
let mut points = Vec::with_capacity(segments);
for i in 0..segments {
let angle = (i as f64) * 2.0 * PI / (segments as f64);
let x = radius * angle.cos();
let y = radius * angle.sin();
let rx = x * rotation.cos() - y * rotation.sin() + center.x;
let ry = x * rotation.sin() + y * rotation.cos() + center.y;
points.push(Point2::new(rx, ry));
}
Ok(points)
}
fn process_ellipse_curve(
&self,
curve: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<Vec<Point2<f64>>> {
let semi_axis1 = curve.get_float(1).unwrap_or(1.0);
let semi_axis2 = curve.get_float(2).unwrap_or(1.0);
let (center, rotation) = self.get_placement_2d(curve, decoder)?;
let segments = self.quality().circle_profile_segments(36);
let mut points = Vec::with_capacity(segments);
for i in 0..segments {
let angle = (i as f64) * 2.0 * PI / (segments as f64);
let x = semi_axis1 * angle.cos();
let y = semi_axis2 * angle.sin();
let rx = x * rotation.cos() - y * rotation.sin() + center.x;
let ry = x * rotation.sin() + y * rotation.cos() + center.y;
points.push(Point2::new(rx, ry));
}
Ok(points)
}
#[inline]
fn process_polyline(
&self,
polyline: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<Vec<Point2<f64>>> {
let points_attr = polyline
.get(0)
.ok_or_else(|| Error::geometry("Polyline missing Points".to_string()))?;
let point_entities = decoder.resolve_ref_list(points_attr)?;
let mut points = Vec::with_capacity(point_entities.len());
for point_entity in point_entities {
if point_entity.ifc_type != IfcType::IfcCartesianPoint {
continue;
}
let coords_attr = point_entity
.get(0)
.ok_or_else(|| Error::geometry("CartesianPoint missing coordinates".to_string()))?;
let coords = coords_attr
.as_list()
.ok_or_else(|| Error::geometry("Expected coordinate list".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);
points.push(Point2::new(x, y));
}
Ok(points)
}
fn process_indexed_polycurve(
&self,
curve: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<Vec<Point2<f64>>> {
let points_attr = curve
.get(0)
.ok_or_else(|| Error::geometry("IndexedPolyCurve missing Points".to_string()))?;
let points_list = decoder
.resolve_ref(points_attr)?
.ok_or_else(|| Error::geometry("Failed to resolve Points list".to_string()))?;
let coord_list_attr = points_list
.get(0)
.ok_or_else(|| Error::geometry("CartesianPointList2D missing CoordList".to_string()))?;
let coord_list = coord_list_attr
.as_list()
.ok_or_else(|| Error::geometry("Expected coordinate list".to_string()))?;
let all_points: Vec<Point2<f64>> = coord_list
.iter()
.filter_map(|coord| {
coord.as_list().and_then(|coords| {
let x = coords.first()?.as_float()?;
let y = coords.get(1)?.as_float()?;
Some(Point2::new(x, y))
})
})
.collect();
let segments_attr = curve.get(1);
if segments_attr.is_none() || segments_attr.map(|a| a.is_null()).unwrap_or(true) {
return Ok(all_points);
}
let segments = segments_attr
.unwrap()
.as_list()
.ok_or_else(|| Error::geometry("Expected segments list".to_string()))?;
let mut result_points = Vec::new();
for segment in segments {
let (is_arc, indices) = if let Some(segment_list) = segment.as_list() {
if segment_list.len() >= 2 {
let type_name = segment_list
.first()
.and_then(|v| v.as_string())
.unwrap_or("");
let is_arc_type = type_name.to_uppercase().contains("ARC");
if let Some(AttributeValue::List(indices_list)) = segment_list.get(1) {
(is_arc_type, Some(indices_list.as_slice()))
} else {
(false, Some(segment_list))
}
} else {
(false, Some(segment_list))
}
} else {
(false, None)
};
if let Some(indices) = indices {
let idx_values: Vec<usize> = indices
.iter()
.filter_map(|v| v.as_float())
.filter_map(|f| {
if !f.is_finite() || f < 1.0 || f.fract() != 0.0 {
return None;
}
(f as usize).checked_sub(1)
})
.collect();
if is_arc && idx_values.len() == 3 {
let p1 = all_points.get(idx_values[0]).copied();
let p2 = all_points.get(idx_values[1]).copied(); let p3 = all_points.get(idx_values[2]).copied();
if let (Some(start), Some(mid), Some(end)) = (p1, p2, p3) {
let chord_len =
((end.x - start.x).powi(2) + (end.y - start.y).powi(2)).sqrt();
let mid_chord = ((mid.x - (start.x + end.x) / 2.0).powi(2)
+ (mid.y - (start.y + end.y) / 2.0).powi(2))
.sqrt();
let arc_estimate = if chord_len > 1e-10 {
(mid_chord / chord_len).abs().min(1.0).acos() * 2.0
} else {
0.5
};
let arc_base =
(arc_estimate / std::f64::consts::FRAC_PI_2 * 8.0).ceil() as usize;
let num_segments =
self.quality().profile_arc_segments(arc_base, 4).min(16);
let arc_points = self.approximate_arc_3pt(start, mid, end, num_segments);
for pt in arc_points {
if result_points.last() != Some(&pt) {
result_points.push(pt);
}
}
}
} else {
for &idx in &idx_values {
if let Some(&pt) = all_points.get(idx) {
if result_points.last() != Some(&pt) {
result_points.push(pt);
}
}
}
}
}
}
Ok(result_points)
}
fn approximate_arc_3pt(
&self,
p1: Point2<f64>,
p2: Point2<f64>,
p3: Point2<f64>,
num_segments: usize,
) -> Vec<Point2<f64>> {
let ax = p1.x;
let ay = p1.y;
let bx = p2.x;
let by = p2.y;
let cx = p3.x;
let cy = p3.y;
let d = 2.0 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
let arc_span = ((p3.x - p1.x).powi(2) + (p3.y - p1.y).powi(2)).sqrt();
let collinear_tolerance = 1e-6 * arc_span.powi(2).max(1e-10);
if d.abs() < collinear_tolerance {
return vec![p1, p2, p3];
}
let ux_num = (ax * ax + ay * ay) * (by - cy)
+ (bx * bx + by * by) * (cy - ay)
+ (cx * cx + cy * cy) * (ay - by);
let uy_num = (ax * ax + ay * ay) * (cx - bx)
+ (bx * bx + by * by) * (ax - cx)
+ (cx * cx + cy * cy) * (bx - ax);
let ux = ux_num / d;
let uy = uy_num / d;
let center = Point2::new(ux, uy);
let radius = ((p1.x - center.x).powi(2) + (p1.y - center.y).powi(2)).sqrt();
if radius > arc_span * 100.0 {
return vec![p1, p2, p3];
}
let angle1 = (p1.y - center.y).atan2(p1.x - center.x);
let angle3 = (p3.y - center.y).atan2(p3.x - center.x);
let angle2 = (p2.y - center.y).atan2(p2.x - center.x);
fn normalize_angle(a: f64) -> f64 {
let mut a = a % (2.0 * PI);
if a > PI {
a -= 2.0 * PI;
} else if a < -PI {
a += 2.0 * PI;
}
a
}
let diff_direct = normalize_angle(angle3 - angle1);
let diff_to_mid = normalize_angle(angle2 - angle1);
let go_direct = if diff_direct > 0.0 {
diff_to_mid > 0.0 && diff_to_mid < diff_direct
} else {
diff_to_mid < 0.0 && diff_to_mid > diff_direct
};
let start_angle = angle1;
let end_angle = if go_direct {
angle1 + diff_direct
} else {
if diff_direct > 0.0 {
angle1 + diff_direct - 2.0 * PI
} else {
angle1 + diff_direct + 2.0 * PI
}
};
let mut points = Vec::with_capacity(num_segments + 1);
for i in 0..=num_segments {
let t = i as f64 / num_segments as f64;
let angle = start_angle + t * (end_angle - start_angle);
points.push(Point2::new(
center.x + radius * angle.cos(),
center.y + radius * angle.sin(),
));
}
points
}
fn process_indexed_polycurve_3d(
&self,
curve: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<Vec<Point3<f64>>> {
let points_attr = curve
.get(0)
.ok_or_else(|| Error::geometry("IndexedPolyCurve missing Points".to_string()))?;
let points_list = decoder
.resolve_ref(points_attr)?
.ok_or_else(|| Error::geometry("Failed to resolve Points list".to_string()))?;
let coord_list = points_list
.get(0)
.and_then(|a| a.as_list())
.ok_or_else(|| Error::geometry("CartesianPointList missing CoordList".to_string()))?;
let all_points: Vec<Point3<f64>> = coord_list
.iter()
.filter_map(|coord| {
coord.as_list().map(|coords| {
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);
Point3::new(x, y, z)
})
})
.collect();
let segments_attr = curve.get(1);
if segments_attr.is_none() || segments_attr.map(|a| a.is_null()).unwrap_or(true) {
return Ok(all_points);
}
let segments = segments_attr
.unwrap()
.as_list()
.ok_or_else(|| Error::geometry("Expected segments list".to_string()))?;
let mut result: Vec<Point3<f64>> = Vec::new();
for segment in segments {
let (is_arc, indices) = if let Some(segment_list) = segment.as_list() {
if segment_list.len() >= 2 {
let type_name = segment_list
.first()
.and_then(|v| v.as_string())
.unwrap_or("");
let is_arc_type = type_name.to_uppercase().contains("ARC");
if let Some(AttributeValue::List(indices_list)) = segment_list.get(1) {
(is_arc_type, Some(indices_list.as_slice()))
} else {
(false, Some(segment_list))
}
} else {
(false, Some(segment_list))
}
} else {
(false, None)
};
let Some(indices) = indices else { continue };
let idx_values: Vec<usize> = indices
.iter()
.filter_map(|v| v.as_float())
.filter_map(|f| {
if !f.is_finite() || f < 1.0 || f.fract() != 0.0 {
return None;
}
(f as usize).checked_sub(1)
})
.collect();
if is_arc && idx_values.len() == 3 {
let p1 = all_points.get(idx_values[0]).copied();
let p2 = all_points.get(idx_values[1]).copied();
let p3 = all_points.get(idx_values[2]).copied();
if let (Some(start), Some(mid), Some(end)) = (p1, p2, p3) {
let chord = end - start;
let chord_len = chord.norm();
let mid_offset = mid - Point3::new(
0.5 * (start.x + end.x),
0.5 * (start.y + end.y),
0.5 * (start.z + end.z),
);
let mid_dev = mid_offset.norm();
let arc_estimate = if chord_len > 1e-10 {
(mid_dev / chord_len).abs().min(1.0).acos() * 2.0
} else {
0.5
};
let arc_base =
(arc_estimate / std::f64::consts::FRAC_PI_2 * 8.0).ceil() as usize;
let num_segments = scale_segments(arc_base, 4, 16, self.quality());
let arc_points = approximate_arc_3pt_3d(start, mid, end, num_segments);
for pt in arc_points {
if !same_point_3d(result.last(), &pt) {
result.push(pt);
}
}
}
} else {
for &idx in &idx_values {
if let Some(&pt) = all_points.get(idx) {
if !same_point_3d(result.last(), &pt) {
result.push(pt);
}
}
}
}
}
Ok(result)
}
fn process_composite_curve_with_depth(
&self,
curve: &DecodedEntity,
decoder: &mut EntityDecoder,
depth: u32,
) -> Result<Vec<Point2<f64>>> {
let segments_attr = curve
.get(0)
.ok_or_else(|| Error::geometry("CompositeCurve missing Segments".to_string()))?;
let segments = decoder.resolve_ref_list(segments_attr)?;
let mut all_points = Vec::new();
for segment in segments {
if segment.ifc_type != IfcType::IfcCompositeCurveSegment {
continue;
}
let parent_curve_attr = segment.get(2).ok_or_else(|| {
Error::geometry("CompositeCurveSegment missing ParentCurve".to_string())
})?;
let parent_curve = decoder
.resolve_ref(parent_curve_attr)?
.ok_or_else(|| Error::geometry("Failed to resolve ParentCurve".to_string()))?;
let same_sense = segment
.get(1)
.and_then(|v| match v {
ifc_lite_core::AttributeValue::Enum(s) => Some(s == "T" || s == "TRUE"),
_ => None,
})
.unwrap_or(true);
let mut segment_points =
self.process_curve_with_depth(&parent_curve, decoder, depth + 1)?;
if !same_sense {
segment_points.reverse();
}
for pt in segment_points {
if all_points.last() != Some(&pt) {
all_points.push(pt);
}
}
}
Ok(all_points)
}
fn process_composite_with_depth(
&self,
profile: &DecodedEntity,
decoder: &mut EntityDecoder,
depth: u32,
) -> Result<Profile2D> {
let profiles_attr = profile
.get(2)
.ok_or_else(|| Error::geometry("Composite profile missing Profiles".to_string()))?;
let sub_profiles = decoder.resolve_ref_list(profiles_attr)?;
if sub_profiles.is_empty() {
return Err(Error::geometry(
"Composite profile has no sub-profiles".to_string(),
));
}
let mut result = self.process_with_depth(&sub_profiles[0], decoder, depth + 1)?;
for sub_profile in &sub_profiles[1..] {
let hole = self.process_with_depth(sub_profile, decoder, depth + 1)?;
result.add_hole(hole.outer);
}
Ok(result)
}
}
fn axis2_placement_location_and_x_axis_3d(
placement: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Option<(Point3<f64>, nalgebra::Vector3<f64>)> {
let is_3d = placement.ifc_type == IfcType::IfcAxis2Placement3D;
let is_2d = placement.ifc_type == IfcType::IfcAxis2Placement2D;
if !is_2d && !is_3d {
return None;
}
let location_attr = placement.get(0)?;
if location_attr.is_null() {
return None;
}
let location = decoder.resolve_ref(location_attr).ok().flatten()?;
if location.ifc_type != IfcType::IfcCartesianPoint {
return None;
}
let coords = location.get(0)?.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);
let origin = Point3::new(x, y, z);
let ref_dir_idx = if is_3d { 2 } else { 1 };
let mut x_axis = nalgebra::Vector3::x();
if let Some(dir_attr) = placement.get(ref_dir_idx) {
if !dir_attr.is_null() {
if let Some(dir) = decoder.resolve_ref(dir_attr).ok().flatten() {
if dir.ifc_type == IfcType::IfcDirection {
if let Some(ratios) = dir.get(0).and_then(|a| a.as_list()) {
let dx = ratios.first().and_then(|v| v.as_float()).unwrap_or(0.0);
let dy = ratios.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
let dz = ratios.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
let v = nalgebra::Vector3::new(dx, dy, dz);
if v.norm() > 1e-12 {
x_axis = v.normalize();
}
}
}
}
}
}
Some((origin, x_axis))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn closed_loop_self_intersects_detects_bowtie() {
let square = [
Point2::new(0.0, 0.0),
Point2::new(1.0, 0.0),
Point2::new(1.0, 1.0),
Point2::new(0.0, 1.0),
];
assert!(!closed_loop_self_intersects(&square));
let bowtie = [
Point2::new(0.0, 0.0),
Point2::new(1.0, 0.0),
Point2::new(0.0, 1.0),
Point2::new(1.0, 1.0),
];
assert!(closed_loop_self_intersects(&bowtie));
assert!(!closed_loop_self_intersects(&square[..3]));
}
fn annular_sector_loop(radius: f64, thickness: f64, span: f64, seg: usize) -> Vec<Point2<f64>> {
let c = Point2::new(0.0, -radius);
let mut loop_ = Vec::with_capacity(2 * seg + 2);
for i in 0..=seg {
let a = -span / 2.0 + span * (i as f64 / seg as f64);
loop_.push(Point2::new(c.x + radius * a.cos(), c.y + radius * a.sin()));
}
let inner = radius - thickness;
for i in 0..=seg {
let a = span / 2.0 - span * (i as f64 / seg as f64);
loop_.push(Point2::new(c.x + inner * a.cos(), c.y + inner * a.sin()));
}
loop_
}
fn loop_area(loop_: &[Point2<f64>]) -> f64 {
let n = loop_.len();
let mut a = 0.0;
for i in 0..n {
let p = loop_[i];
let q = loop_[(i + 1) % n];
a += p.x * q.y - q.x * p.y;
}
(a * 0.5).abs()
}
#[test]
fn simplify_thin_annular_sector_stays_simple_and_area_preserving() {
let cases = [
(12000.0, 100.0, 240.0_f64),
(12000.0, 300.0, 240.0),
(12000.0, 600.0, 90.0),
(12000.0, 600.0, 180.0),
];
for (radius, thickness, span_deg) in cases {
let span = span_deg.to_radians();
let dense = annular_sector_loop(radius, thickness, span, 200);
assert!(
!closed_loop_self_intersects(&dense),
"input sector r={radius} t={thickness} {span_deg}° should start simple",
);
let out = simplify_smooth_curve_polyline(&dense);
assert!(
!closed_loop_self_intersects(&out),
"simplified sector r={radius} t={thickness} {span_deg}° self-intersects",
);
let (a_in, a_out) = (loop_area(&dense), loop_area(&out));
let rel = (a_out - a_in).abs() / a_in;
assert!(
rel < 0.02,
"sector r={radius} t={thickness} {span_deg}°: area drift {:.1}% \
(in={a_in:.0} out={a_out:.0}) — thin curved wall was distorted",
rel * 100.0,
);
}
}
#[test]
fn simplify_still_reduces_fat_round_disk() {
let mut disk = Vec::new();
let seg = 127;
for i in 0..seg {
let a = std::f64::consts::TAU * (i as f64 / seg as f64);
disk.push(Point2::new(0.5 * a.cos(), 0.5 * a.sin()));
}
let out = simplify_smooth_curve_polyline(&disk);
assert!(
out.len() < disk.len() && out.len() >= SIMPLIFIED_MIN_VERTICES,
"round disk should simplify from {} to [{SIMPLIFIED_MIN_VERTICES}, {}) verts, got {}",
disk.len(),
disk.len(),
out.len(),
);
assert!(!closed_loop_self_intersects(&out));
}
fn ellipse_loop(ar: f64, seg: usize) -> Vec<Point2<f64>> {
let (a, b) = (ar * 0.5, 0.5);
(0..seg)
.map(|i| {
let t = std::f64::consts::TAU * (i as f64 / seg as f64);
Point2::new(a * t.cos(), b * t.sin())
})
.collect()
}
#[test]
fn elongated_filled_ellipse_is_not_thin_gated() {
for ar in [6.0_f64, 8.0, 12.0, 20.0] {
assert!(
!loop_doubles_back(&ellipse_loop(ar, 128)),
"AR={ar} filled ellipse is convex — must not read as doubling back",
);
}
let ellipse = ellipse_loop(8.0, 128);
let out = simplify_smooth_curve_polyline(&ellipse);
assert!(
out.len() < ellipse.len() && out.len() >= SIMPLIFIED_MIN_VERTICES,
"8:1 ellipse must still simplify (was wrongly thin-gated): {} -> {} verts",
ellipse.len(),
out.len(),
);
assert!(!closed_loop_self_intersects(&out));
let rel = (loop_area(&out) - loop_area(&ellipse)).abs() / loop_area(&ellipse);
assert!(rel < 0.08, "8:1 ellipse area drift {:.1}%", rel * 100.0);
}
#[test]
fn doubles_back_ignores_localized_spikes() {
assert!(loop_doubles_back(&annular_sector_loop(
12000.0,
100.0,
(240.0_f64).to_radians(),
128
)));
let mut e = ellipse_loop(8.0, 128);
assert!(!loop_doubles_back(&e));
let tip = (0..e.len())
.max_by(|&i, &j| e[i].x.abs().partial_cmp(&e[j].x.abs()).unwrap())
.unwrap();
e[tip].x *= 0.75;
assert!(
!loop_doubles_back(&e),
"a single notch on a convex ellipse must not read as doubling back",
);
let mut disk: Vec<Point2<f64>> = (0..128)
.map(|i| {
let t = std::f64::consts::TAU * (i as f64 / 128.0);
Point2::new(t.cos(), t.sin())
})
.collect();
disk.insert(1, Point2::new(disk[0].x * 0.9999, disk[0].y * 0.9999));
assert!(
!loop_doubles_back(&disk),
"a sub-mm seam jog on a convex loop must not read as doubling back",
);
}
#[test]
fn doubles_back_independent_of_per_boundary_sampling_density() {
let centre = Point2::new(0.0, -12000.0);
let (r_out, r_in) = (12000.0, 12000.0 - 10.0);
let span = (90.0_f64).to_radians();
let mut loop_ = Vec::new();
for i in 0..=96 {
let a = -span / 2.0 + span * (i as f64 / 96.0);
loop_.push(Point2::new(centre.x + r_out * a.cos(), centre.y + r_out * a.sin()));
}
for i in 0..=16 {
let a = span / 2.0 - span * (i as f64 / 16.0);
loop_.push(Point2::new(centre.x + r_in * a.cos(), centre.y + r_in * a.sin()));
}
assert!(
loop_doubles_back(&loop_),
"a non-uniformly tessellated thin sector must still read as doubling back",
);
let out = simplify_smooth_curve_polyline(&loop_);
assert_eq!(out.len(), loop_.len(), "skewed-sampling thin sector must be gated");
}
#[test]
fn test_rectangle_profile() {
let content = r#"
#1=IFCRECTANGLEPROFILEDEF(.AREA.,$,$,100.0,200.0);
"#;
let mut decoder = EntityDecoder::new(content);
let schema = IfcSchema::new();
let processor = ProfileProcessor::new(schema);
let profile_entity = decoder.decode_by_id(1).unwrap();
let profile = processor
.process(&profile_entity, &mut decoder, TessellationQuality::Medium)
.unwrap();
assert_eq!(profile.outer.len(), 4);
assert!(!profile.outer.is_empty());
}
#[test]
fn test_circle_profile() {
let content = r#"
#1=IFCCIRCLEPROFILEDEF(.AREA.,$,$,50.0);
"#;
let mut decoder = EntityDecoder::new(content);
let schema = IfcSchema::new();
let processor = ProfileProcessor::new(schema);
let profile_entity = decoder.decode_by_id(1).unwrap();
let profile = processor
.process(&profile_entity, &mut decoder, TessellationQuality::Medium)
.unwrap();
assert_eq!(profile.outer.len(), 36); assert!(!profile.outer.is_empty());
}
#[test]
fn test_i_shape_profile() {
let content = r#"
#1=IFCISHAPEPROFILEDEF(.AREA.,$,$,200.0,300.0,10.0,15.0,$,$,$,$);
"#;
let mut decoder = EntityDecoder::new(content);
let schema = IfcSchema::new();
let processor = ProfileProcessor::new(schema);
let profile_entity = decoder.decode_by_id(1).unwrap();
let profile = processor
.process(&profile_entity, &mut decoder, TessellationQuality::Medium)
.unwrap();
assert_eq!(profile.outer.len(), 12); assert!(!profile.outer.is_empty());
}
fn outer_area(profile: &Profile2D) -> f64 {
let p = &profile.outer;
let n = p.len();
let mut a = 0.0;
for i in 0..n {
let b = p[(i + 1) % n];
a += p[i].x * b.y - b.x * p[i].y;
}
a.abs() * 0.5
}
#[test]
fn test_i_shape_honours_fillet_radius() {
let sharp = process_content(
"#1=IFCISHAPEPROFILEDEF(.AREA.,$,$,180.,171.,6.,9.5,$,$,$);\n",
1,
);
let filleted = process_content(
"#1=IFCISHAPEPROFILEDEF(.AREA.,$,$,180.,171.,6.,9.5,15.,$,$);\n",
1,
);
assert_eq!(sharp.outer.len(), 12, "sharp I should stay 12 points");
assert!(
filleted.outer.len() > 12,
"fillets not generated: {} points",
filleted.outer.len()
);
let k = 1.0 - std::f64::consts::FRAC_PI_4;
let expected = 4332.0 + 4.0 * 15.0 * 15.0 * k;
let area = outer_area(&filleted);
assert!(
(area - expected).abs() < 15.0 && area > outer_area(&sharp) + 100.0,
"I fillet area {area:.2} vs expected {expected:.2} (sharp {:.2})",
outer_area(&sharp)
);
let (mnx, mny, mxx, mxy) = outer_bbox(&filleted);
assert!((mxx - mnx - 180.0).abs() < 1e-6 && (mxy - mny - 171.0).abs() < 1e-6);
}
#[test]
fn test_u_shape_honours_radii() {
let sharp = process_content(
"#1=IFCUSHAPEPROFILEDEF(.AREA.,$,$,200.,80.,10.,12.,$,$,$,$);\n",
1,
);
let filleted = process_content(
"#1=IFCUSHAPEPROFILEDEF(.AREA.,$,$,200.,80.,10.,12.,12.,6.,$,$);\n",
1,
);
assert_eq!(sharp.outer.len(), 8);
assert!(filleted.outer.len() > 8, "U fillets not generated");
let k = 1.0 - std::f64::consts::FRAC_PI_4;
let expected = 3680.0 + 2.0 * 144.0 * k - 2.0 * 36.0 * k;
let area = outer_area(&filleted);
assert!(
(area - expected).abs() < 12.0,
"U area {area:.2} vs expected {expected:.2}"
);
let (mnx, mny, mxx, mxy) = outer_bbox(&filleted);
assert!((mxx - mnx - 80.0).abs() < 1e-6 && (mxy - mny - 200.0).abs() < 1e-6);
}
#[test]
fn test_t_shape_honours_radii() {
let sharp = process_content(
"#1=IFCTSHAPEPROFILEDEF(.AREA.,$,$,100.,80.,10.,12.,$,$,$,$,$);\n",
1,
);
let filleted = process_content(
"#1=IFCTSHAPEPROFILEDEF(.AREA.,$,$,100.,80.,10.,12.,8.,4.,3.,$,$);\n",
1,
);
assert_eq!(sharp.outer.len(), 8);
assert!(filleted.outer.len() > 8, "T fillets not generated");
let k = 1.0 - std::f64::consts::FRAC_PI_4;
let expected = 1840.0 + 2.0 * 64.0 * k - 2.0 * 16.0 * k - 2.0 * 9.0 * k;
let area = outer_area(&filleted);
assert!(
(area - expected).abs() < 10.0,
"T area {area:.2} vs expected {expected:.2}"
);
let (mnx, mny, mxx, mxy) = outer_bbox(&filleted);
assert!((mxx - mnx - 80.0).abs() < 1e-6 && (mxy - mny - 100.0).abs() < 1e-6);
}
fn outer_bbox(profile: &Profile2D) -> (f64, f64, f64, f64) {
let mut min_x = f64::INFINITY;
let mut min_y = f64::INFINITY;
let mut max_x = f64::NEG_INFINITY;
let mut max_y = f64::NEG_INFINITY;
for p in &profile.outer {
min_x = min_x.min(p.x);
min_y = min_y.min(p.y);
max_x = max_x.max(p.x);
max_y = max_y.max(p.y);
}
(min_x, min_y, max_x, max_y)
}
fn process_content(content: &str, id: u32) -> Profile2D {
let mut decoder = EntityDecoder::new(content);
let processor = ProfileProcessor::new(IfcSchema::new());
let entity = decoder.decode_by_id(id).unwrap();
processor
.process(&entity, &mut decoder, TessellationQuality::Medium)
.unwrap()
}
#[test]
fn test_u_shape_is_centered() {
let profile =
process_content("#1=IFCUSHAPEPROFILEDEF(.AREA.,$,$,160.,64.,5.,8.4,$,$,$,$);\n", 1);
let (min_x, min_y, max_x, max_y) = outer_bbox(&profile);
assert!((min_x + max_x).abs() < 1e-9, "X not centred: {min_x}..{max_x}");
assert!((min_y + max_y).abs() < 1e-9, "Y not centred: {min_y}..{max_y}");
assert!((max_x - min_x - 64.0).abs() < 1e-9, "width should be FlangeWidth");
assert!((max_y - min_y - 160.0).abs() < 1e-9, "height should be Depth");
}
#[test]
fn test_l_shape_is_centered() {
let profile =
process_content("#1=IFCLSHAPEPROFILEDEF(.AREA.,$,$,100.,80.,10.,$,$,$,$,$);\n", 1);
let (min_x, min_y, max_x, max_y) = outer_bbox(&profile);
assert!((min_x + max_x).abs() < 1e-9, "X not centred: {min_x}..{max_x}");
assert!((min_y + max_y).abs() < 1e-9, "Y not centred: {min_y}..{max_y}");
assert!((max_x - min_x - 80.0).abs() < 1e-9, "width should be Width");
assert!((max_y - min_y - 100.0).abs() < 1e-9, "height should be Depth");
}
#[test]
fn test_l_shape_honours_fillet_and_edge_radii() {
let profile = process_content(
"#1=IFCLSHAPEPROFILEDEF(.AREA.,$,$,100.,100.,10.,12.,6.,$,$,$);\n",
1,
);
assert!(
profile.outer.len() > 6,
"fillets not generated: {} points",
profile.outer.len()
);
let (min_x, min_y, max_x, max_y) = outer_bbox(&profile);
assert!((max_x - min_x - 100.0).abs() < 1e-6, "width {}", max_x - min_x);
assert!((max_y - min_y - 100.0).abs() < 1e-6, "height {}", max_y - min_y);
let k = 1.0 - std::f64::consts::FRAC_PI_4;
let expected = 1900.0 + (144.0 - 72.0) * k;
let n = profile.outer.len();
let mut area = 0.0;
for i in 0..n {
let a = profile.outer[i];
let b = profile.outer[(i + 1) % n];
area += a.x * b.y - b.x * a.y;
}
area = area.abs() * 0.5;
assert!(
(area - expected).abs() < 5.0,
"L fillet area {area:.2} vs expected {expected:.2} — wrong fillet sign/placement"
);
}
#[test]
fn test_t_shape_is_centered() {
let profile = process_content(
"#1=IFCTSHAPEPROFILEDEF(.AREA.,$,$,100.,80.,10.,12.,$,$,$,$,$);\n",
1,
);
let (min_x, min_y, max_x, max_y) = outer_bbox(&profile);
assert!((min_x + max_x).abs() < 1e-9, "X not centred: {min_x}..{max_x}");
assert!((min_y + max_y).abs() < 1e-9, "Y not centred: {min_y}..{max_y}");
assert!((max_x - min_x - 80.0).abs() < 1e-9, "width should be FlangeWidth");
assert!((max_y - min_y - 100.0).abs() < 1e-9, "height should be Depth");
}
#[test]
fn test_c_shape_spans_width_and_depth() {
let profile = process_content(
"#1=IFCCSHAPEPROFILEDEF(.AREA.,$,$,200.,80.,6.,20.,$);\n",
1,
);
let (min_x, min_y, max_x, max_y) = outer_bbox(&profile);
assert!((min_x + max_x).abs() < 1e-9, "X not centred: {min_x}..{max_x}");
assert!((min_y + max_y).abs() < 1e-9, "Y not centred: {min_y}..{max_y}");
assert!(
(max_x - min_x - 80.0).abs() < 1e-9,
"width should be Width (80), got {}",
max_x - min_x
);
assert!(
(max_y - min_y - 200.0).abs() < 1e-9,
"height should be Depth (200), got {}",
max_y - min_y
);
}
#[test]
fn test_arbitrary_profile() {
let content = r#"
#1=IFCCARTESIANPOINT((0.0,0.0));
#2=IFCCARTESIANPOINT((100.0,0.0));
#3=IFCCARTESIANPOINT((100.0,100.0));
#4=IFCCARTESIANPOINT((0.0,100.0));
#5=IFCPOLYLINE((#1,#2,#3,#4,#1));
#6=IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,$,#5);
"#;
let mut decoder = EntityDecoder::new(content);
let schema = IfcSchema::new();
let processor = ProfileProcessor::new(schema);
let profile_entity = decoder.decode_by_id(6).unwrap();
let profile = processor
.process(&profile_entity, &mut decoder, TessellationQuality::Medium)
.unwrap();
assert_eq!(profile.outer.len(), 5); assert!(!profile.outer.is_empty());
}
#[test]
fn test_derived_profile_applies_translation_rotation_and_scale() {
let content = r#"
#1=IFCDIRECTION((0.0,1.0));
#2=IFCCARTESIANPOINT((10.0,20.0));
#3=IFCCARTESIANTRANSFORMATIONOPERATOR2D(#1,$,#2,2.0);
#4=IFCRECTANGLEPROFILEDEF(.AREA.,$,$,2.0,4.0);
#5=IFCDERIVEDPROFILEDEF(.AREA.,$,#4,#3,$);
"#;
let mut decoder = EntityDecoder::new(content);
let schema = IfcSchema::new();
let processor = ProfileProcessor::new(schema);
let profile_entity = decoder.decode_by_id(5).unwrap();
let profile = processor
.process(&profile_entity, &mut decoder, TessellationQuality::Medium)
.unwrap();
assert_eq!(profile.outer.len(), 4);
assert!(profile.outer.contains(&Point2::new(14.0, 18.0)));
assert!(profile.outer.contains(&Point2::new(14.0, 22.0)));
assert!(profile.outer.contains(&Point2::new(6.0, 22.0)));
assert!(profile.outer.contains(&Point2::new(6.0, 18.0)));
}
#[test]
fn test_mirrored_profile_uses_derived_operator() {
let content = r#"
#1=IFCDIRECTION((-1.0,0.0));
#2=IFCDIRECTION((0.0,1.0));
#3=IFCCARTESIANPOINT((0.0,0.0));
#4=IFCCARTESIANTRANSFORMATIONOPERATOR2D(#1,#2,#3,1.0);
#5=IFCRECTANGLEPROFILEDEF(.AREA.,$,$,2.0,4.0);
#6=IFCMIRROREDPROFILEDEF(.AREA.,$,#5,#4,$);
"#;
let mut decoder = EntityDecoder::new(content);
let schema = IfcSchema::new();
let processor = ProfileProcessor::new(schema);
let profile_entity = decoder.decode_by_id(6).unwrap();
let profile = processor
.process(&profile_entity, &mut decoder, TessellationQuality::Medium)
.unwrap();
assert_eq!(profile.outer.len(), 4);
assert!(profile.outer.contains(&Point2::new(1.0, -2.0)));
assert!(profile.outer.contains(&Point2::new(-1.0, -2.0)));
assert!(profile.outer.contains(&Point2::new(-1.0, 2.0)));
assert!(profile.outer.contains(&Point2::new(1.0, 2.0)));
}
fn approx_eq_p3(a: Point3<f64>, b: Point3<f64>, tol: f64) -> bool {
(a.x - b.x).abs() < tol && (a.y - b.y).abs() < tol && (a.z - b.z).abs() < tol
}
#[test]
fn test_trim_polyline_full_range() {
let pts = vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
Point3::new(2.0, 0.0, 0.0),
];
let out = trim_polyline(&pts, 0.0, 1.0);
assert_eq!(out.len(), 3);
assert!(approx_eq_p3(out[0], pts[0], 1e-9));
assert!(approx_eq_p3(out[1], pts[1], 1e-9));
assert!(approx_eq_p3(out[2], pts[2], 1e-9));
}
#[test]
fn test_trim_polyline_halves() {
let pts = vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
Point3::new(2.0, 0.0, 0.0),
];
let first_half = trim_polyline(&pts, 0.0, 0.5);
assert_eq!(first_half.len(), 2);
assert!(approx_eq_p3(first_half[0], Point3::new(0.0, 0.0, 0.0), 1e-9));
assert!(approx_eq_p3(first_half[1], Point3::new(1.0, 0.0, 0.0), 1e-9));
let second_half = trim_polyline(&pts, 0.5, 1.0);
assert_eq!(second_half.len(), 2);
assert!(approx_eq_p3(second_half[0], Point3::new(1.0, 0.0, 0.0), 1e-9));
assert!(approx_eq_p3(second_half[1], Point3::new(2.0, 0.0, 0.0), 1e-9));
}
#[test]
fn test_trim_polyline_strict_interior() {
let pts: Vec<Point3<f64>> = (0..5)
.map(|i| Point3::new(i as f64, 0.0, 0.0))
.collect();
let out = trim_polyline(&pts, 0.25, 0.75);
assert_eq!(out.len(), 3);
assert!(approx_eq_p3(out[0], Point3::new(1.0, 0.0, 0.0), 1e-9));
assert!(approx_eq_p3(out[1], Point3::new(2.0, 0.0, 0.0), 1e-9));
assert!(approx_eq_p3(out[2], Point3::new(3.0, 0.0, 0.0), 1e-9));
}
#[test]
fn test_trim_polyline_invalid_range() {
let pts = vec![Point3::new(0.0, 0.0, 0.0), Point3::new(1.0, 0.0, 0.0)];
assert!(trim_polyline(&pts, 0.5, 0.5).is_empty());
assert!(trim_polyline(&pts, 0.6, 0.4).is_empty());
assert!(trim_polyline(&pts[..1], 0.0, 1.0).is_empty());
}
#[test]
fn test_trim_polyline_two_points_partial() {
let pts = vec![Point3::new(0.0, 0.0, 0.0), Point3::new(10.0, 0.0, 0.0)];
let out = trim_polyline(&pts, 0.3, 0.7);
assert_eq!(out.len(), 2);
assert!(approx_eq_p3(out[0], Point3::new(3.0, 0.0, 0.0), 1e-9));
assert!(approx_eq_p3(out[1], Point3::new(7.0, 0.0, 0.0), 1e-9));
}
#[test]
fn test_composite_curve_trim_first_segment_only() {
let content = r#"
#1=IFCCARTESIANPOINT((0.0,0.0,0.0));
#2=IFCCARTESIANPOINT((0.0,2.0,0.0));
#3=IFCCARTESIANPOINT((0.0,4.0,0.0));
#4=IFCCARTESIANPOINT((0.0,6.0,0.0));
#5=IFCPOLYLINE((#1,#2));
#6=IFCPOLYLINE((#2,#3));
#7=IFCPOLYLINE((#3,#4));
#8=IFCCOMPOSITECURVESEGMENT(.CONTINUOUS.,.T.,#5);
#9=IFCCOMPOSITECURVESEGMENT(.CONTINUOUS.,.T.,#6);
#10=IFCCOMPOSITECURVESEGMENT(.CONTINUOUS.,.T.,#7);
#11=IFCCOMPOSITECURVE((#8,#9,#10),.F.);
"#;
let mut decoder = EntityDecoder::new(content);
let schema = IfcSchema::new();
let processor = ProfileProcessor::new(schema);
let curve = decoder.decode_by_id(11).unwrap();
let pts = processor
.get_composite_curve_points_trimmed(&curve, &mut decoder, Some(0.0), Some(1.0))
.unwrap();
assert_eq!(pts.len(), 2);
assert!(approx_eq_p3(pts[0], Point3::new(0.0, 0.0, 0.0), 1e-9));
assert!(approx_eq_p3(pts[1], Point3::new(0.0, 2.0, 0.0), 1e-9));
let pts = processor
.get_composite_curve_points_trimmed(&curve, &mut decoder, Some(1.0), Some(2.0))
.unwrap();
assert_eq!(pts.len(), 2);
assert!(approx_eq_p3(pts[0], Point3::new(0.0, 2.0, 0.0), 1e-9));
assert!(approx_eq_p3(pts[1], Point3::new(0.0, 4.0, 0.0), 1e-9));
let pts = processor
.get_composite_curve_points_trimmed(&curve, &mut decoder, Some(0.0), Some(3.0))
.unwrap();
assert_eq!(pts.len(), 4);
assert!(approx_eq_p3(pts[0], Point3::new(0.0, 0.0, 0.0), 1e-9));
assert!(approx_eq_p3(pts[3], Point3::new(0.0, 6.0, 0.0), 1e-9));
}
#[test]
fn test_composite_curve_trim_clamps_out_of_range() {
let content = r#"
#1=IFCCARTESIANPOINT((0.0,0.0,0.0));
#2=IFCCARTESIANPOINT((0.0,2.0,0.0));
#3=IFCPOLYLINE((#1,#2));
#4=IFCCOMPOSITECURVESEGMENT(.CONTINUOUS.,.T.,#3);
#5=IFCCOMPOSITECURVE((#4),.F.);
"#;
let mut decoder = EntityDecoder::new(content);
let schema = IfcSchema::new();
let processor = ProfileProcessor::new(schema);
let curve = decoder.decode_by_id(5).unwrap();
let pts = processor
.get_composite_curve_points_trimmed(&curve, &mut decoder, Some(-5.0), Some(1.0))
.unwrap();
assert_eq!(pts.len(), 2);
let pts = processor
.get_composite_curve_points_trimmed(&curve, &mut decoder, Some(0.0), Some(99.0))
.unwrap();
assert_eq!(pts.len(), 2);
let pts = processor
.get_composite_curve_points_trimmed(&curve, &mut decoder, Some(0.5), Some(0.5))
.unwrap();
assert!(pts.is_empty());
let pts = processor
.get_composite_curve_points_trimmed(&curve, &mut decoder, Some(0.8), Some(0.2))
.unwrap();
assert!(pts.is_empty());
}
#[test]
fn test_composite_curve_trim_fractional_multi_segment() {
let content = r#"
#1=IFCCARTESIANPOINT((0.0,0.0,0.0));
#2=IFCCARTESIANPOINT((0.0,2.0,0.0));
#3=IFCCARTESIANPOINT((0.0,4.0,0.0));
#4=IFCCARTESIANPOINT((0.0,6.0,0.0));
#5=IFCPOLYLINE((#1,#2));
#6=IFCPOLYLINE((#2,#3));
#7=IFCPOLYLINE((#3,#4));
#8=IFCCOMPOSITECURVESEGMENT(.CONTINUOUS.,.T.,#5);
#9=IFCCOMPOSITECURVESEGMENT(.CONTINUOUS.,.T.,#6);
#10=IFCCOMPOSITECURVESEGMENT(.CONTINUOUS.,.T.,#7);
#11=IFCCOMPOSITECURVE((#8,#9,#10),.F.);
"#;
let mut decoder = EntityDecoder::new(content);
let schema = IfcSchema::new();
let processor = ProfileProcessor::new(schema);
let curve = decoder.decode_by_id(11).unwrap();
let pts = processor
.get_composite_curve_points_trimmed(&curve, &mut decoder, Some(0.5), Some(2.5))
.unwrap();
let ys: Vec<f64> = pts.iter().map(|p| p.y).collect();
assert_eq!(ys.len(), 4, "got points: {:?}", pts);
assert!((ys[0] - 1.0).abs() < 1e-9);
assert!((ys[1] - 2.0).abs() < 1e-9);
assert!((ys[2] - 4.0).abs() < 1e-9);
assert!((ys[3] - 5.0).abs() < 1e-9);
}
#[test]
fn test_polyline_trim_first_segment() {
let content = r#"
#1=IFCCARTESIANPOINT((0.0,0.0,0.0));
#2=IFCCARTESIANPOINT((0.0,2.0,0.0));
#3=IFCCARTESIANPOINT((0.0,4.0,0.0));
#4=IFCCARTESIANPOINT((0.0,6.0,0.0));
#5=IFCPOLYLINE((#1,#2,#3,#4));
"#;
let mut decoder = EntityDecoder::new(content);
let schema = IfcSchema::new();
let processor = ProfileProcessor::new(schema);
let curve = decoder.decode_by_id(5).unwrap();
let pts = processor
.get_polyline_points_trimmed(&curve, &mut decoder, Some(0.0), Some(1.0))
.unwrap();
assert_eq!(pts.len(), 2);
assert!(approx_eq_p3(pts[0], Point3::new(0.0, 0.0, 0.0), 1e-9));
assert!(approx_eq_p3(pts[1], Point3::new(0.0, 2.0, 0.0), 1e-9));
let pts = processor
.get_polyline_points_trimmed(&curve, &mut decoder, Some(1.0), Some(2.0))
.unwrap();
assert_eq!(pts.len(), 2);
assert!(approx_eq_p3(pts[0], Point3::new(0.0, 2.0, 0.0), 1e-9));
assert!(approx_eq_p3(pts[1], Point3::new(0.0, 4.0, 0.0), 1e-9));
let pts = processor
.get_polyline_points_trimmed(&curve, &mut decoder, Some(0.5), Some(2.5))
.unwrap();
let ys: Vec<f64> = pts.iter().map(|p| p.y).collect();
assert_eq!(ys.len(), 4, "got points: {:?}", pts);
assert!((ys[0] - 1.0).abs() < 1e-9);
assert!((ys[1] - 2.0).abs() < 1e-9);
assert!((ys[2] - 4.0).abs() < 1e-9);
assert!((ys[3] - 5.0).abs() < 1e-9);
}
#[test]
fn test_polyline_trim_clamps_and_inverts() {
let content = r#"
#1=IFCCARTESIANPOINT((0.0,0.0,0.0));
#2=IFCCARTESIANPOINT((0.0,2.0,0.0));
#3=IFCPOLYLINE((#1,#2));
"#;
let mut decoder = EntityDecoder::new(content);
let schema = IfcSchema::new();
let processor = ProfileProcessor::new(schema);
let curve = decoder.decode_by_id(3).unwrap();
let pts = processor
.get_polyline_points_trimmed(&curve, &mut decoder, None, None)
.unwrap();
assert_eq!(pts.len(), 2);
let pts = processor
.get_polyline_points_trimmed(&curve, &mut decoder, Some(0.8), Some(0.2))
.unwrap();
assert!(pts.is_empty());
let pts = processor
.get_polyline_points_trimmed(&curve, &mut decoder, Some(-5.0), Some(99.0))
.unwrap();
assert_eq!(pts.len(), 2);
}
#[test]
fn test_composite_curve_trim_keeps_non_coincident_junction() {
let content = r#"
#1=IFCCARTESIANPOINT((0.0,0.0,0.0));
#2=IFCCARTESIANPOINT((0.0,2.0,0.0));
#3=IFCCARTESIANPOINT((0.0,2.5,0.0));
#4=IFCCARTESIANPOINT((0.0,4.5,0.0));
#5=IFCPOLYLINE((#1,#2));
#6=IFCPOLYLINE((#3,#4));
#7=IFCCOMPOSITECURVESEGMENT(.CONTINUOUS.,.T.,#5);
#8=IFCCOMPOSITECURVESEGMENT(.CONTINUOUS.,.T.,#6);
#9=IFCCOMPOSITECURVE((#7,#8),.F.);
"#;
let mut decoder = EntityDecoder::new(content);
let schema = IfcSchema::new();
let processor = ProfileProcessor::new(schema);
let curve = decoder.decode_by_id(9).unwrap();
let pts = processor
.get_composite_curve_points_trimmed(&curve, &mut decoder, Some(0.0), Some(2.0))
.unwrap();
assert_eq!(pts.len(), 4, "got points: {:?}", pts);
assert!(approx_eq_p3(pts[0], Point3::new(0.0, 0.0, 0.0), 1e-9));
assert!(approx_eq_p3(pts[1], Point3::new(0.0, 2.0, 0.0), 1e-9));
assert!(approx_eq_p3(pts[2], Point3::new(0.0, 2.5, 0.0), 1e-9));
assert!(approx_eq_p3(pts[3], Point3::new(0.0, 4.5, 0.0), 1e-9));
}
#[test]
fn test_composite_curve_trim_same_sense_false() {
let content = r#"
#1=IFCCARTESIANPOINT((0.0,0.0,0.0));
#2=IFCCARTESIANPOINT((0.0,10.0,0.0));
#3=IFCPOLYLINE((#1,#2));
#4=IFCCOMPOSITECURVESEGMENT(.CONTINUOUS.,.F.,#3);
#5=IFCCOMPOSITECURVE((#4),.F.);
"#;
let mut decoder = EntityDecoder::new(content);
let schema = IfcSchema::new();
let processor = ProfileProcessor::new(schema);
let curve = decoder.decode_by_id(5).unwrap();
let pts = processor
.get_composite_curve_points_trimmed(&curve, &mut decoder, Some(0.0), Some(0.3))
.unwrap();
assert_eq!(pts.len(), 2);
assert!(approx_eq_p3(pts[0], Point3::new(0.0, 10.0, 0.0), 1e-9));
assert!(approx_eq_p3(pts[1], Point3::new(0.0, 7.0, 0.0), 1e-9));
}
}