use axiolid_core::Vec3;
use axiolid_curve::KnotSpec;
use axiolid_model::{GeometryNode, NodeId, SurfaceRelation};
use axiolid_surface::{
BSplineSurface as KernelBSpline, Cylinder, Plane as KernelPlane, Sphere, Surface, Torus,
};
use ifc_model::EntityId;
use crate::curve::bspline::KnotType;
use crate::error::GeometryResult;
use crate::lower::curve::{finite_values, lower_curve_node};
use crate::lower::session::LoweringSession;
use crate::resource::direction::resolve_unit;
use crate::resource::placement::axis_placement_transform;
use crate::resource::placement::Axis1Placement;
use crate::resource::point::CartesianPoint;
use crate::surface::bounded::{CurveBoundedPlane, CurveBoundedSurface, RectangularTrimmedSurface};
use crate::surface::bspline::BSplineSurface;
use crate::surface::elementary::{CylindricalSurface, Plane, SphericalSurface, ToroidalSurface};
use crate::surface::swept::SurfaceOfLinearExtrusion;
use crate::surface::swept::SurfaceOfRevolution;
use crate::transform::Transform;
const SURFACE: &str = "surface";
pub fn lower_surface_node(
session: &mut LoweringSession<'_>,
id: EntityId,
frame: Transform,
) -> GeometryResult<NodeId> {
if let Some(existing) = session.memoized(id, SURFACE, frame) {
return Ok(existing);
}
session.enter(id, SURFACE)?;
let result = (|| -> GeometryResult<NodeId> {
let type_name = session.type_name(id)?.to_ascii_uppercase();
match type_name.as_str() {
"IFCPLANE" => lower_plane(session, id, frame),
"IFCSURFACEOFLINEAREXTRUSION" => lower_linear_extrusion(session, id, frame),
"IFCCYLINDRICALSURFACE" => lower_cylinder(session, id, frame),
"IFCSPHERICALSURFACE" => lower_sphere(session, id, frame),
"IFCTOROIDALSURFACE" => lower_torus(session, id, frame),
"IFCBSPLINESURFACEWITHKNOTS" | "IFCRATIONALBSPLINESURFACEWITHKNOTS" => {
lower_bspline(session, id, frame)
}
"IFCSURFACEOFREVOLUTION" => lower_revolution(session, id, frame),
"IFCRECTANGULARTRIMMEDSURFACE" => lower_rectangular_trimmed(session, id, frame),
"IFCCURVEBOUNDEDPLANE" => lower_curve_bounded(session, id, frame),
"IFCCURVEBOUNDEDSURFACE" => lower_curve_bounded_surface(session, id, frame),
other => Err(session.unsupported(id, other, "curved and B-spline surfaces")),
}
})();
session.exit(id);
let node = result?;
session.memoize(id, SURFACE, frame, node);
Ok(node)
}
pub fn lower_plane(
session: &mut LoweringSession<'_>,
id: EntityId,
frame: Transform,
) -> GeometryResult<NodeId> {
let entity = session.entity(id, id)?;
let view = Plane::new(id, entity);
let position_id = view.position_ref()?;
let position = session.entity(id, position_id)?;
let local = axis_placement_transform(session.model(), position_id, position)?
.to_metres(session.units());
let placed = frame.compose(&local);
let normal = placed.apply_direction([0.0, 0.0, 1.0]);
let length = (normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]).sqrt();
if !length.is_finite() || length <= f64::EPSILON {
return Err(session.degenerate(
id,
"IFCPLANE",
"plane normal is zero-length or non-finite",
));
}
let plane = KernelPlane {
frame: placed.to_geom_frame(id)?,
};
session.node_for(id, GeometryNode::Surface(Surface::Plane(plane)))
}
pub fn lower_linear_extrusion(
session: &mut LoweringSession<'_>,
id: EntityId,
frame: Transform,
) -> GeometryResult<NodeId> {
let entity = session.entity(id, id)?;
let view = SurfaceOfLinearExtrusion::new(id, entity);
let placed = match view.position_ref() {
Some(position_id) => {
let position = session.entity(id, position_id)?;
let local = axis_placement_transform(session.model(), position_id, position)?
.to_metres(session.units());
frame.compose(&local)
}
None => frame,
};
let curve_id = view.swept_curve_ref()?;
let swept_curve = swept_generatrix(session, id, curve_id, placed)?;
let raw = resolve_unit(session.model(), id, view.extruded_direction_ref()?)?;
let direction = placed.apply_direction(raw);
let length =
(direction[0] * direction[0] + direction[1] * direction[1] + direction[2] * direction[2])
.sqrt();
if !length.is_finite() || length <= f64::EPSILON {
return Err(session.degenerate(
id,
"IFCSURFACEOFLINEAREXTRUSION",
"extruded direction is zero-length or non-finite",
));
}
session.node_for(
id,
GeometryNode::SurfaceRelation(SurfaceRelation::LinearExtrusion {
swept_curve,
direction: Vec3::from_array(direction),
}),
)
}
#[cfg(test)]
mod tests;
fn placed_frame(
session: &mut LoweringSession<'_>,
owner: EntityId,
position_id: EntityId,
frame: Transform,
) -> GeometryResult<axiolid_core::Frame3> {
let position = session.entity(owner, position_id)?;
let local = axis_placement_transform(session.model(), position_id, position)?
.to_metres(session.units());
frame.compose(&local).to_geom_frame(owner)
}
fn to_metres(session: &LoweringSession<'_>, value: f64) -> f64 {
value * session.units().length_to_metres
}
pub(crate) fn trim_converter(
session: &LoweringSession<'_>,
basis_kind: &str,
) -> impl Fn(f64) -> f64 {
let angular = matches!(
basis_kind,
"IFCCYLINDRICALSURFACE"
| "IFCSPHERICALSURFACE"
| "IFCTOROIDALSURFACE"
| "IFCSURFACEOFREVOLUTION"
);
let factor = if angular {
session.units().angle_to_radians
} else {
session.units().length_to_metres
};
move |value: f64| value * factor
}
fn cartesian_point_3d(
session: &LoweringSession<'_>,
referrer: EntityId,
id: EntityId,
) -> GeometryResult<[f64; 3]> {
let entity = session.entity(referrer, id)?;
let point = CartesianPoint::new(id, entity);
point.coordinates_3d()
}
pub fn lower_cylinder(
session: &mut LoweringSession<'_>,
id: EntityId,
frame: Transform,
) -> GeometryResult<NodeId> {
let entity = session.entity(id, id)?;
let view = CylindricalSurface::new(id, entity);
let placed = placed_frame(session, id, view.position_ref()?, frame)?;
let radius = to_metres(session, view.radius()?);
session.node_for(
id,
GeometryNode::Surface(Surface::Cylinder(Cylinder {
frame: placed,
radius,
})),
)
}
pub fn lower_sphere(
session: &mut LoweringSession<'_>,
id: EntityId,
frame: Transform,
) -> GeometryResult<NodeId> {
let entity = session.entity(id, id)?;
let view = SphericalSurface::new(id, entity);
let placed = placed_frame(session, id, view.position_ref()?, frame)?;
let radius = to_metres(session, view.radius()?);
session.node_for(
id,
GeometryNode::Surface(Surface::Sphere(Sphere {
frame: placed,
radius,
})),
)
}
pub fn lower_torus(
session: &mut LoweringSession<'_>,
id: EntityId,
frame: Transform,
) -> GeometryResult<NodeId> {
let entity = session.entity(id, id)?;
let view = ToroidalSurface::new(id, entity);
let placed = placed_frame(session, id, view.position_ref()?, frame)?;
let major_radius = to_metres(session, view.major_radius()?);
let minor_radius = to_metres(session, view.minor_radius()?);
session.node_for(
id,
GeometryNode::Surface(Surface::Torus(Torus {
frame: placed,
major_radius,
minor_radius,
})),
)
}
fn knot_spec(source: KnotType) -> KnotSpec {
match source {
KnotType::Uniform => KnotSpec::Uniform,
KnotType::QuasiUniform => KnotSpec::QuasiUniform,
KnotType::PiecewiseBezier => KnotSpec::PiecewiseBezier,
KnotType::Unspecified => KnotSpec::Unspecified,
}
}
pub fn lower_bspline(
session: &mut LoweringSession<'_>,
id: EntityId,
frame: Transform,
) -> GeometryResult<NodeId> {
let entity = session.entity(id, id)?;
let view = BSplineSurface::new(id, entity);
let type_name = session.type_name(id)?;
let u_degree = u16::try_from(view.u_degree()?)
.map_err(|_| session.degenerate(id, &type_name, "UDegree exceeds u16"))?;
let v_degree = u16::try_from(view.v_degree()?)
.map_err(|_| session.degenerate(id, &type_name, "VDegree exceeds u16"))?;
let u = view.u_knots()?.ok_or_else(|| {
session.unsupported(id, &type_name, "B-spline surface without explicit u knots")
})?;
let v = view.v_knots()?.ok_or_else(|| {
session.unsupported(id, &type_name, "B-spline surface without explicit v knots")
})?;
let u_declared: u128 = u.multiplicities.iter().map(|m| *m as u128).sum();
session.check_aggregate(id, &type_name, "u knot multiplicities", u_declared)?;
let v_declared: u128 = v.multiplicities.iter().map(|m| *m as u128).sum();
session.check_aggregate(id, &type_name, "v knot multiplicities", v_declared)?;
let grid = view.control_points()?;
session.check_aggregate(
id,
&type_name,
"control grid points",
grid.u_count() as u128 * grid.v_count() as u128,
)?;
let mut control_points = Vec::with_capacity(grid.u_count());
for row in grid.rows() {
let mut out_row = Vec::with_capacity(row.len());
for point_id in row {
let raw = cartesian_point_3d(session, id, *point_id)?;
let placed = frame.apply(raw.map(|value| to_metres(session, value)));
finite_values(
session,
id,
&type_name,
"transformed control point",
&placed,
)?;
out_row.push(axiolid_core::Point3::from_array(placed));
}
control_points.push(out_row);
}
finite_values(session, id, &type_name, "UKnots", &u.values)?;
finite_values(session, id, &type_name, "VKnots", &v.values)?;
let u_multiplicities = multiplicities(session, id, &type_name, u.multiplicities)?;
let v_multiplicities = multiplicities(session, id, &type_name, v.multiplicities)?;
let weights = view.weights()?;
if let Some(rows) = weights.as_ref() {
for row in rows {
finite_values(session, id, &type_name, "WeightsData", row)?;
}
}
let u_closed = view.u_closed().ok_or_else(|| {
session.unsupported(id, &type_name, "unknown UClosed is not lossless in bool")
})?;
let v_closed = view.v_closed().ok_or_else(|| {
session.unsupported(id, &type_name, "unknown VClosed is not lossless in bool")
})?;
let surface = KernelBSpline {
u_degree,
v_degree,
control_points,
u_knots: u.values,
u_multiplicities,
v_knots: v.values,
v_multiplicities,
weights,
u_closed,
v_closed,
self_intersect: view.self_intersect(),
knot_spec: knot_spec(view.knot_spec()),
};
session.node_for(id, GeometryNode::Surface(Surface::BSpline(surface)))
}
fn multiplicities(
session: &LoweringSession<'_>,
id: EntityId,
type_name: &str,
values: Vec<usize>,
) -> GeometryResult<Vec<u32>> {
values
.into_iter()
.map(|value| {
u32::try_from(value)
.map_err(|_| session.degenerate(id, type_name, "multiplicity exceeds u32"))
})
.collect()
}
fn swept_generatrix(
session: &mut LoweringSession<'_>,
_owner: EntityId,
swept_curve: EntityId,
frame: Transform,
) -> GeometryResult<NodeId> {
let kind = session.type_name(swept_curve)?.to_ascii_uppercase();
let curve_id = if kind == "IFCARBITRARYOPENPROFILEDEF" || kind == "IFCARBITRARYCLOSEDPROFILEDEF"
{
session.slots(swept_curve)?.req_ref(2, "Curve")?
} else {
swept_curve
};
lower_curve_node(session, curve_id, frame)
}
pub fn lower_revolution(
session: &mut LoweringSession<'_>,
id: EntityId,
frame: Transform,
) -> GeometryResult<NodeId> {
let entity = session.entity(id, id)?;
let view = SurfaceOfRevolution::new(id, entity);
let swept_curve = swept_generatrix(session, id, view.swept_curve_ref()?, frame)?;
let axis_id = view.axis_position_ref()?;
let axis_entity = session.entity(id, axis_id)?;
let axis = Axis1Placement::new(axis_id, axis_entity);
let raw_origin = axis.location(session.model())?;
let origin = frame.apply([
to_metres(session, raw_origin[0]),
to_metres(session, raw_origin[1]),
to_metres(session, raw_origin[2]),
]);
let direction = frame.apply_direction(axis.axis(session.model())?);
session.node_for(
id,
GeometryNode::SurfaceRelation(SurfaceRelation::Revolution {
swept_curve,
axis_origin: axiolid_core::Point3::from_array(origin),
axis_direction: axiolid_core::Vec3::from_array(direction),
}),
)
}
pub fn lower_rectangular_trimmed(
session: &mut LoweringSession<'_>,
id: EntityId,
frame: Transform,
) -> GeometryResult<NodeId> {
let entity = session.entity(id, id)?;
let view = RectangularTrimmedSurface::new(id, entity);
let basis_id = view.basis_surface_ref()?;
let basis = lower_surface_node(session, basis_id, frame)?;
let rect = view.rectangle()?;
let basis_kind = session.type_name(basis_id)?.to_ascii_uppercase();
let convert = trim_converter(session, &basis_kind);
session.node_for(
id,
GeometryNode::SurfaceRelation(SurfaceRelation::RectangularTrimmed {
basis,
u: (convert(rect.u1), convert(rect.u2)),
v: (convert(rect.v1), convert(rect.v2)),
u_sense: rect.usense,
v_sense: rect.vsense,
}),
)
}
pub fn lower_curve_bounded(
session: &mut LoweringSession<'_>,
id: EntityId,
frame: Transform,
) -> GeometryResult<NodeId> {
let entity = session.entity(id, id)?;
let view = CurveBoundedPlane::new(id, entity);
let basis = lower_surface_node(session, view.basis_surface_ref()?, frame)?;
let parameter_space = Transform::identity();
let mut boundaries = vec![lower_curve_node(
session,
view.outer_boundary_ref()?,
parameter_space,
)?];
for inner in view.inner_boundary_refs() {
boundaries.push(lower_curve_node(session, inner, parameter_space)?);
}
session.node_for(
id,
GeometryNode::SurfaceRelation(SurfaceRelation::CurveBounded {
basis,
boundaries,
implicit_outer: false,
}),
)
}
pub fn lower_curve_bounded_surface(
session: &mut LoweringSession<'_>,
id: EntityId,
frame: Transform,
) -> GeometryResult<NodeId> {
let entity = session.entity(id, id)?;
let view = CurveBoundedSurface::new(id, entity);
let basis = lower_surface_node(session, view.basis_surface_ref()?, frame)?;
let refs = view.boundary_refs()?;
let mut boundaries = Vec::with_capacity(refs.len());
for boundary in refs {
boundaries.push(lower_curve_node(session, boundary, frame)?);
}
session.node_for(
id,
GeometryNode::SurfaceRelation(SurfaceRelation::CurveBounded {
basis,
boundaries,
implicit_outer: view.implicit_outer(),
}),
)
}