use super::records::TolerantCoedgeExtension;
use crate::nurbs::proc_surface::{
DecodedProceduralSurfaceDefinition, EmbeddedRollingBall, EmbeddedScaledCompoundLoftShape,
};
use crate::nurbs::reader::LEN_TO_MM;
use crate::sab::{Record, Token};
use cadmpeg_ir::features::{FinitePoint3, FiniteVector3};
use cadmpeg_ir::geometry::analytic::{
CircleCurve, ConeSurface, CylinderSurface, EllipseCurve, LineCurve, PlaneSurface,
SphereSurface, TorusSurface,
};
use cadmpeg_ir::geometry::{
CurveGeometry, SolvedCurveGeometry, SolvedSurfaceGeometry, SurfaceGeometry,
};
use cadmpeg_ir::ids::EdgeId;
use cadmpeg_ir::math::{Point3, Vector3};
use cadmpeg_ir::scalar::{Angle, NonNegativeLength, NonZeroLength, PositiveLength, PositiveReal};
use cadmpeg_ir::topology::Sense;
use cadmpeg_ir::units::{FiniteVector, OrthonormalFrame3, UnitVector3};
use std::collections::{HashMap, HashSet};
use super::AsmBrep;
const EPS_GEOMETRY_PCURVE_RANGES_ON_DOMAIN_E9: f64 = 1.0e-9;
const EPS_GEOMETRY_ANALYTIC_PROCEDURAL_SURFACE_E10: f64 = 1.0e-10;
const EPS_GEOMETRY_ANALYTIC_ROLLING_BALL_SURFACE_E10: f64 = 1.0e-10;
const EPS_GEOMETRY_LINEAR_NURBS_SPINE_E10: f64 = 1.0e-10;
const EPS_GEOMETRY_RATIONAL_FOUR_ARC_CIRCLE_E12: f64 = 1.0e-12;
const EPS_GEOMETRY_RATIONAL_FOUR_ARC_CIRCLE_E10: f64 = 1.0e-10;
const EPS_GEOMETRY_REDUCE_HOMOGENEOUS_BEZIER_TO_QUADRATIC_E10: f64 = 1.0e-10;
const EPS_GEOMETRY_CLAMP_EDGE_RANGES_TO_CARRIER_DOMAINS_E9: f64 = 1.0e-9;
macro_rules! propagate_resource {
($result:expr) => {
match $result {
Ok(value) => value,
Err(error) => return Some(Err(error)),
}
};
}
pub(in crate::brep) struct Carrier {
pub(super) positions: Vec<[f64; 3]>,
pub(super) vectors: Vec<[f64; 3]>,
doubles: Vec<f64>,
}
pub(super) fn collect_carrier(
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
rec: &Record,
) -> Result<Carrier, cadmpeg_core::CodecError> {
let mut c = Carrier {
positions: Vec::new(),
vectors: Vec::new(),
doubles: Vec::new(),
};
for (index, t) in rec.tokens.iter().enumerate() {
if rec.head() == "cone"
&& matches!(t, Token::Double(_))
&& index > 0
&& matches!(rec.tokens[index - 1], Token::True)
{
continue;
}
match t {
Token::Position(p) => {
ctx.push_vec(&mut c.positions, *p, "ASM carrier positions")?;
}
Token::Vector3(v) => {
ctx.push_vec(&mut c.vectors, *v, "ASM carrier vectors")?;
}
Token::Double(d) => {
ctx.push_vec(&mut c.doubles, *d, "ASM carrier doubles")?;
}
_ => {}
}
}
Ok(c)
}
pub(super) fn scale_point(p: [f64; 3]) -> Point3 {
Point3::new(p[0] * LEN_TO_MM, p[1] * LEN_TO_MM, p[2] * LEN_TO_MM)
}
pub(super) fn norm3(v: [f64; 3]) -> f64 {
Vector3::from(v).norm()
}
fn direction(v: [f64; 3]) -> Option<UnitVector3> {
UnitVector3::normalized(Vector3::from(v))
}
fn frame(axis: [f64; 3], reference: [f64; 3]) -> Option<OrthonormalFrame3> {
OrthonormalFrame3::from_units(direction(axis)?, direction(reference)?)
}
pub(super) fn is_analytic_surface(head: &str) -> bool {
matches!(head, "plane" | "cone" | "sphere" | "torus")
}
pub(super) fn is_analytic_curve(head: &str) -> bool {
matches!(head, "straight" | "ellipse" | "degenerate_curve")
}
pub fn decode_surface(
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
rec: &Record,
) -> Option<Result<(SolvedSurfaceGeometry, bool), cadmpeg_core::CodecError>> {
let carrier = match collect_carrier(ctx, rec) {
Ok(carrier) => carrier,
Err(error) => return Some(Err(error)),
};
decode_surface_carrier(rec, &carrier).map(Ok)
}
fn decode_surface_carrier(rec: &Record, c: &Carrier) -> Option<(SolvedSurfaceGeometry, bool)> {
let origin = *c.positions.first()?;
match rec.head() {
"plane" => Some((
SolvedSurfaceGeometry::Plane(PlaneSurface::new(
FinitePoint3::new(scale_point(origin))?,
frame(*c.vectors.first()?, *c.vectors.get(1)?)?,
)),
false,
)),
"cone" => {
let ratio = *c.doubles.first().unwrap_or(&1.0);
let axis = direction(*c.vectors.first()?)?;
let major = *c.vectors.get(1)?;
let sine = *c.doubles.get(1).unwrap_or(&0.0);
let cosine = *c.doubles.get(2).unwrap_or(&1.0);
let radius = norm3(major) * LEN_TO_MM;
(radius > f64::EPSILON).then_some(())?;
let ref_direction = direction(major)?;
if sine.abs() <= f64::EPSILON && ratio == 1.0 {
Some((
SolvedSurfaceGeometry::Cylinder(CylinderSurface::new(
FinitePoint3::new(scale_point(origin))?,
OrthonormalFrame3::from_units(axis, ref_direction)?,
PositiveLength::new(radius)?,
)),
cosine < 0.0,
))
} else {
let axis = if sine * cosine < 0.0 {
axis.reversed()
} else {
axis
};
Some((
SolvedSurfaceGeometry::Cone(ConeSurface::new(
FinitePoint3::new(scale_point(origin))?,
OrthonormalFrame3::from_units(axis, ref_direction)?,
NonNegativeLength::new(radius)?,
PositiveReal::new(ratio)?,
Angle::new(sine.abs().atan2(cosine.abs()))?,
)),
cosine < 0.0,
))
}
}
"sphere" => {
let signed = *c.doubles.first()?;
Some((
SolvedSurfaceGeometry::Sphere(SphereSurface::new(
FinitePoint3::new(scale_point(origin))?,
frame(*c.vectors.get(1)?, *c.vectors.first()?)?,
NonZeroLength::new(signed * LEN_TO_MM)?,
)),
false,
))
}
"torus" => {
let axis = *c.vectors.first()?;
let ref_direction = *c.vectors.get(1)?;
let major = *c.doubles.first()?;
let minor = *c.doubles.get(1)?;
Some((
SolvedSurfaceGeometry::Torus(TorusSurface::new(
FinitePoint3::new(scale_point(origin))?,
frame(axis, ref_direction)?,
PositiveLength::new(major * LEN_TO_MM)?,
NonZeroLength::new(minor * LEN_TO_MM)?,
)),
false,
))
}
_ => None,
}
}
pub(super) fn vertex_point_ref(record: &Record) -> Option<i64> {
match record.chunk(4) {
Some(Token::Long(_)) => record.ref_at(5),
_ => record.ref_at(4),
}
}
pub(super) fn coedge_pcurve_ref(record: &Record) -> Option<i64> {
match record.chunk(9) {
Some(Token::Long(_)) => record.ref_at(10),
_ => record.ref_at(9),
}
}
pub(super) fn is_vertex_record(record: &Record) -> bool {
matches!(record.head(), "vertex" | "tvertex")
}
pub(super) fn is_edge_record(record: &Record) -> bool {
matches!(record.head(), "edge" | "tedge")
}
pub(super) fn is_coedge_record(record: &Record) -> bool {
matches!(record.head(), "coedge" | "tcoedge")
}
pub(super) fn tolerant_coedge_extension(record: &Record) -> Option<TolerantCoedgeExtension> {
let target = match record.chunk(13)? {
Token::Ref(target) => (*target >= 0).then_some(*target),
_ => return None,
};
match record.chunk(14)? {
Token::Long(0) if matches!(record.chunk(15), Some(Token::Long(0))) => {
Some(TolerantCoedgeExtension::Empty { target })
}
Token::Long(1) => {
let curve_reversed = match record.chunk(15)? {
Token::True => true,
Token::False => false,
_ => return None,
};
if !matches!(record.chunk(16), Some(Token::SubtypeOpen)) {
return None;
}
let open = record
.tokens
.iter()
.enumerate()
.filter(|(_, token)| !token.is_payload_ident())
.nth(16)
.map(|(index, _)| index)?;
let mut depth = 0usize;
let mut close = None;
for (index, token) in record.tokens.iter().enumerate().skip(open) {
match token {
Token::SubtypeOpen => depth += 1,
Token::SubtypeClose => {
depth = depth.checked_sub(1)?;
if depth == 0 {
close = Some(index);
break;
}
}
_ => {}
}
}
let close = close?;
let mut suffix = record
.tokens
.get(close + 1..)?
.iter()
.filter(|token| !token.is_payload_ident());
let parameter_range = match (
suffix.next(),
suffix.next(),
suffix.next(),
suffix.next(),
suffix.next(),
suffix.next(),
) {
(
Some(Token::False),
Some(Token::False),
Some(Token::Long(0)),
None,
None,
None,
) => None,
(
Some(Token::True),
Some(Token::Double(start)),
Some(Token::True),
Some(Token::Double(end)),
Some(Token::Long(0)),
None,
) => Some(cadmpeg_ir::units::FiniteVector::new([*start, *end])?),
_ => return None,
};
let payload_token_count = record
.tokens
.get(open + 1..close)?
.iter()
.filter(|token| !token.is_payload_ident())
.count();
Some(TolerantCoedgeExtension::EmbeddedCurve {
target,
curve_reversed,
payload_token_count: u32::try_from(payload_token_count).ok()?,
parameter_range,
})
}
_ => None,
}
}
pub(super) fn is_known_record_head(head: &str) -> bool {
matches!(
head,
"body"
| "region"
| "lump"
| "shell"
| "subshell"
| "wire"
| "face"
| "loop"
| "point"
| "asmheader"
) || matches!(
head,
"coedge" | "tcoedge" | "edge" | "tedge" | "vertex" | "tvertex"
) || is_analytic_surface(head)
|| is_analytic_curve(head)
|| matches!(head, "spline" | "intcurve" | "pcurve")
}
pub(super) fn is_asm_stream_delimiter(name: &str) -> bool {
matches!(name, "Begin-of-ASM-History-Data" | "End-of-ASM-data")
}
pub(super) fn edge_pcurve_parameter_ranges(edge: &Record) -> Option<[[f64; 2]; 2]> {
let (Some(Token::Double(start)), Some(Token::Double(end))) = (edge.chunk(4), edge.chunk(6))
else {
return None;
};
let direct = [*start, *end];
let negated = [-start, -end];
Some(if matches!(edge.chunk(9), Some(Token::True)) {
[negated, direct]
} else {
[direct, negated]
})
}
pub(super) fn pcurve_ranges_on_domain(
candidate: &cadmpeg_ir::geometry::pcurve::PcurveNurbs,
edge: Option<&Record>,
) -> Option<Vec<[f64; 2]>> {
let first = *candidate
.knots()
.get(usize::try_from(candidate.degree()).ok()?)?;
let last = *candidate.knots().get(candidate.pole_rows().count())?;
(first < last).then_some(())?;
let mut ranges = Vec::new();
for range in edge
.and_then(edge_pcurve_parameter_ranges)
.into_iter()
.flatten()
{
if let Some(range) = (|| {
range
.iter()
.all(|value| {
cadmpeg_ir::math::parameter_in_domain(
*value,
[first, last],
EPS_GEOMETRY_PCURVE_RANGES_ON_DOMAIN_E9,
)
})
.then_some(())?;
let range = range.map(|value| value.clamp(first, last));
(range[0] != range[1]).then_some(range)
})() {
ranges.push(range);
}
}
if !ranges.contains(&[first, last]) {
ranges.push([first, last]);
}
Some(ranges)
}
pub fn decode_curve(
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
rec: &Record,
) -> Option<Result<CurveGeometry, cadmpeg_core::CodecError>> {
let carrier = match collect_carrier(ctx, rec) {
Ok(carrier) => carrier,
Err(error) => return Some(Err(error)),
};
decode_curve_carrier(rec, &carrier).map(Ok)
}
fn decode_curve_carrier(rec: &Record, carrier: &Carrier) -> Option<CurveGeometry> {
let base = *carrier.positions.first()?;
match rec.head() {
"straight" => Some(CurveGeometry::Solved(SolvedCurveGeometry::Line(
LineCurve::new(
FinitePoint3::new(scale_point(base))?,
direction(*carrier.vectors.first()?)?,
),
))),
"ellipse" => {
let axis = *carrier.vectors.first()?;
let reference = *carrier.vectors.get(1)?;
let ratio = *carrier.doubles.first()?;
let major_radius = norm3(reference) * LEN_TO_MM;
let center = FinitePoint3::new(scale_point(base))?;
let conic_frame = frame(axis, reference)?;
if (ratio.abs() - 1.0).abs() <= f64::EPSILON {
Some(CurveGeometry::Solved(SolvedCurveGeometry::Circle(
CircleCurve::new(center, conic_frame, PositiveLength::new(major_radius)?),
)))
} else {
Some(CurveGeometry::Solved(SolvedCurveGeometry::Ellipse(
EllipseCurve::try_from_parts(
center,
conic_frame,
PositiveLength::new(major_radius)?,
PositiveLength::new(major_radius * ratio.abs())?,
)
.ok()?,
)))
}
}
"degenerate_curve" => Some(CurveGeometry::Solved(SolvedCurveGeometry::Degenerate(
cadmpeg_ir::geometry::analytic::DegenerateCurve::try_new(scale_point(base)).ok()?,
))),
_ => None,
}
}
pub(super) fn sense_at(rec: &Record, i: usize) -> Sense {
match rec.chunk(i) {
Some(Token::True) => Sense::Reversed,
_ => Sense::Forward,
}
}
pub(super) fn record_reversed(rec: &Record) -> bool {
let mut previous: Option<&Token> = None;
rec.chunks()
.find_map(|token| {
let result = matches!(token, Token::SubtypeOpen)
.then(|| match previous {
Some(Token::True) => Some(true),
Some(Token::False) => Some(false),
_ => None,
})
.flatten();
previous = Some(token);
result
})
.or_else(|| {
(rec.head() == "intcurve").then(|| matches!(rec.chunk(3), Some(Token::True)))
})
.unwrap_or(false)
}
pub(super) fn reverse_curve_geometry(
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
geometry: &mut CurveGeometry,
) -> Result<(), cadmpeg_core::CodecError> {
match geometry {
CurveGeometry::Solved(SolvedCurveGeometry::Line(line_curve)) => {
line_curve.reverse_parameterization();
}
CurveGeometry::Solved(SolvedCurveGeometry::Circle(circle_curve)) => {
circle_curve.reverse_parameterization();
}
CurveGeometry::Solved(SolvedCurveGeometry::Ellipse(ellipse_curve)) => {
ellipse_curve.reverse_parameterization();
}
CurveGeometry::Solved(SolvedCurveGeometry::Nurbs(curve)) => {
curve.reverse_parameterization(ctx)?;
}
_ => {}
}
Ok(())
}
pub(super) fn reverse_procedural_curve_definition(
definition: &mut cadmpeg_ir::geometry::ProceduralCurveDefinition,
) -> Result<(), &'static str> {
if let cadmpeg_ir::geometry::ProceduralCurveDefinition::Helix(helix) = definition {
helix.try_reverse_parameterization()?;
}
Ok(())
}
pub(super) fn double_at(rec: &Record, i: usize) -> Option<f64> {
match rec.chunk(i) {
Some(Token::Double(d)) => Some(*d),
_ => None,
}
}
pub(super) fn pcurve_parameter_range(rec: &Record) -> Option<[f64; 2]> {
let mut values = rec.chunks().rev();
match (values.next(), values.next()) {
(Some(Token::Double(end)), Some(Token::Double(start))) => Some([*start, *end]),
_ => None,
}
}
pub(super) fn pcurve_inline_tail_flags(rec: &Record) -> Option<[bool; 4]> {
if !matches!(rec.chunk(3), Some(Token::Long(0))) {
return None;
}
let start = rec.chunks().count().checked_sub(6)?;
let mut flags = [false; 4];
for (slot, token) in flags.iter_mut().zip(rec.chunks().skip(start)) {
*slot = match token {
Token::True => true,
Token::False => false,
_ => return None,
};
}
Some(flags)
}
pub(super) fn procedural_surface_definition_is_exact_carrier(
definition: &DecodedProceduralSurfaceDefinition,
) -> bool {
match definition {
DecodedProceduralSurfaceDefinition::Extrusion { .. }
| DecodedProceduralSurfaceDefinition::Sum { .. }
| DecodedProceduralSurfaceDefinition::Helix(_)
| DecodedProceduralSurfaceDefinition::Ruled { .. }
| DecodedProceduralSurfaceDefinition::VertexBlend(_)
| DecodedProceduralSurfaceDefinition::SubSurface { .. } => true,
DecodedProceduralSurfaceDefinition::Sweep(construction) => matches!(
&construction.layout,
crate::nurbs::proc_surface::EmbeddedSweepSurfaceLayout::Revision { form, .. }
if matches!(form.cache, cadmpeg_ir::geometry::RevisionCacheForm::Parameterization(_))
),
DecodedProceduralSurfaceDefinition::Law(construction) => !matches!(
construction.tail,
cadmpeg_ir::geometry::LawSurfaceTail::Full { .. }
),
DecodedProceduralSurfaceDefinition::ScaledCompoundLoft(construction) => matches!(
construction.shape,
EmbeddedScaledCompoundLoftShape::None { .. }
),
DecodedProceduralSurfaceDefinition::Blend {
native: Some(construction),
..
} => matches!(
construction.cache,
cadmpeg_ir::geometry::RevisionCacheForm::Parameterization(_)
),
DecodedProceduralSurfaceDefinition::VariableBlend(construction) => {
matches!(
construction.cache,
cadmpeg_ir::geometry::VariableBlendCache::Parameterization { .. }
| cadmpeg_ir::geometry::VariableBlendCache::Stale {}
)
}
_ => false,
}
}
pub(super) fn analytic_procedural_surface(
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
definition: &DecodedProceduralSurfaceDefinition,
) -> Option<Result<SurfaceGeometry, cadmpeg_core::CodecError>> {
match definition {
DecodedProceduralSurfaceDefinition::Extrusion {
directrix,
direction,
..
} => {
let (center, normal, ref_direction, radius) =
propagate_resource!(rational_four_arc_circle(ctx, directrix)?);
let axis = UnitVector3::normalized(*direction)?;
if 1.0 - axis.as_raw().dot(normal).abs() > EPS_GEOMETRY_ANALYTIC_PROCEDURAL_SURFACE_E10
{
return None;
}
Some(Ok(SurfaceGeometry::Solved(
SolvedSurfaceGeometry::Cylinder(CylinderSurface::new(
FinitePoint3::new(center)?,
OrthonormalFrame3::from_units(axis, UnitVector3::new(ref_direction)?)?,
PositiveLength::new(radius)?,
)),
)))
}
DecodedProceduralSurfaceDefinition::Blend {
supports,
spine: Some(spine),
radius_offsets: [signed_radius, end_offset],
cross_section: cadmpeg_ir::geometry::BlendCrossSection::Circular,
native,
} if signed_radius == end_offset => {
analytic_rolling_ball_surface(ctx, supports, native.as_deref(), spine, *signed_radius)
}
_ => None,
}
}
fn analytic_rolling_ball_surface(
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
supports: &[Option<SurfaceGeometry>; 2],
native: Option<&EmbeddedRollingBall>,
spine: &cadmpeg_ir::geometry::nurbs::NurbsCurve,
signed_radius: f64,
) -> Option<Result<SurfaceGeometry, cadmpeg_core::CodecError>> {
let radius = signed_radius.abs();
if !radius.is_finite() || radius <= f64::EPSILON {
return None;
}
let support = |index: usize| {
supports[index].as_ref().or_else(|| {
native.and_then(|native| {
native.sides[index]
.surface
.as_ref()
.map(|support| &support.surface)
})
})
};
let first = support(0)?;
let second = support(1)?;
if let (
SurfaceGeometry::Solved(SolvedSurfaceGeometry::Plane(plane_surface)),
SurfaceGeometry::Solved(SolvedSurfaceGeometry::Plane(plane_surface_2)),
) = (first, second)
{
let first_origin = plane_surface.origin();
let first_normal = *plane_surface.frame().axis().as_raw();
let second_origin = plane_surface_2.origin();
let second_normal = *plane_surface_2.frame().axis().as_raw();
let (origin, axis) = linear_nurbs_spine(spine)?;
let tolerance = EPS_GEOMETRY_ANALYTIC_ROLLING_BALL_SURFACE_E10 * radius;
let support_intersection = first_normal.cross(second_normal);
let support_intersection_norm = support_intersection.norm();
if support_intersection_norm <= EPS_GEOMETRY_ANALYTIC_ROLLING_BALL_SURFACE_E10
|| 1.0
- axis
.as_raw()
.dot(support_intersection.scale(1.0 / support_intersection_norm))
.abs()
> EPS_GEOMETRY_ANALYTIC_ROLLING_BALL_SURFACE_E10
{
return None;
}
for (plane_origin, plane_normal) in [
(*first_origin, first_normal),
(*second_origin, second_normal),
] {
if axis.as_raw().dot(plane_normal).abs()
> EPS_GEOMETRY_ANALYTIC_ROLLING_BALL_SURFACE_E10
|| (point_vector(plane_origin, origin).dot(plane_normal).abs() - radius).abs()
> tolerance
{
return None;
}
}
return Some(Ok(SurfaceGeometry::Solved(
SolvedSurfaceGeometry::Cylinder(CylinderSurface::new(
FinitePoint3::new(origin)?,
OrthonormalFrame3::from_units(
axis,
UnitVector3::new(cadmpeg_ir::geometry::derive_reference_direction(
*axis.as_raw(),
))?,
)?,
PositiveLength::new(radius)?,
)),
)));
}
let (plane_origin, plane_normal, cylinder_origin, cylinder_axis, cylinder_radius) =
match (first, second) {
(
SurfaceGeometry::Solved(SolvedSurfaceGeometry::Plane(plane_surface)),
SurfaceGeometry::Solved(SolvedSurfaceGeometry::Cylinder(cylinder_surface)),
) => {
let plane_origin = plane_surface.origin().get();
let plane_normal = *plane_surface.frame().axis().as_raw();
let cylinder_origin = cylinder_surface.origin().get();
let cylinder_axis = *cylinder_surface.frame().axis().as_raw();
let cylinder_radius = cylinder_surface.radius().get();
(
plane_origin,
plane_normal,
cylinder_origin,
cylinder_axis,
cylinder_radius,
)
}
(
SurfaceGeometry::Solved(SolvedSurfaceGeometry::Cylinder(cylinder_surface_2)),
SurfaceGeometry::Solved(SolvedSurfaceGeometry::Plane(plane_surface_2)),
) => {
let cylinder_origin = cylinder_surface_2.origin().get();
let cylinder_axis = *cylinder_surface_2.frame().axis().as_raw();
let cylinder_radius = cylinder_surface_2.radius().get();
let plane_origin = plane_surface_2.origin().get();
let plane_normal = *plane_surface_2.frame().axis().as_raw();
(
plane_origin,
plane_normal,
cylinder_origin,
cylinder_axis,
cylinder_radius,
)
}
_ => {
return None;
}
};
let (center, axis, ref_direction, major_radius) =
propagate_resource!(rational_four_arc_circle(ctx, spine)?);
let scale = major_radius.max(radius).max(cylinder_radius);
let tolerance = EPS_GEOMETRY_ANALYTIC_ROLLING_BALL_SURFACE_E10 * scale;
let center_offset = point_vector(cylinder_origin, center);
let axial_offset = center_offset.dot(cylinder_axis);
let radial_offset = center_offset - cylinder_axis.scale(axial_offset);
if 1.0 - axis.dot(plane_normal).abs() > EPS_GEOMETRY_ANALYTIC_ROLLING_BALL_SURFACE_E10
|| 1.0 - axis.dot(cylinder_axis).abs() > EPS_GEOMETRY_ANALYTIC_ROLLING_BALL_SURFACE_E10
|| (point_vector(plane_origin, center).dot(plane_normal).abs() - radius).abs() > tolerance
|| radial_offset.norm() > tolerance
|| ((major_radius - cylinder_radius).abs() - radius).abs() > tolerance
{
return None;
}
Some(Ok(SurfaceGeometry::Solved(SolvedSurfaceGeometry::Torus(
TorusSurface::try_new(center, axis, ref_direction, major_radius, signed_radius).ok()?,
))))
}
fn linear_nurbs_spine(
curve: &cadmpeg_ir::geometry::nurbs::NurbsCurve,
) -> Option<(Point3, UnitVector3)> {
if curve.degree() == 0 || curve.periodic() {
return None;
}
if let Some(weights) = curve.weights() {
let first_sign = weights.first()?.get().signum();
if weights
.iter()
.any(|weight| weight.get().signum() != first_sign)
{
return None;
}
}
let origin = curve.control_points()[0];
let (_, farthest) = curve
.control_points()
.iter()
.copied()
.map(|point| (point_vector(origin.get(), point.get()).norm(), point))
.max_by(|left, right| left.0.total_cmp(&right.0))?;
let extent = point_vector(origin.get(), farthest.get()).norm();
if !extent.is_finite() || extent <= f64::EPSILON {
return None;
}
let axis = UnitVector3::normalized(point_vector(origin.get(), farthest.get()))?;
if curve.control_points().iter().any(|point| {
let relative = point_vector(origin.get(), point.get());
let relative = Vector3::new(
relative.x / extent,
relative.y / extent,
relative.z / extent,
);
axis.as_raw().cross(relative).norm() > EPS_GEOMETRY_LINEAR_NURBS_SPINE_E10
}) {
return None;
}
Some((origin.get(), axis))
}
pub(super) fn rational_four_arc_circle(
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
curve: &cadmpeg_ir::geometry::nurbs::NurbsCurve,
) -> Option<Result<(Point3, Vector3, Vector3, f64), cadmpeg_core::CodecError>> {
let cadmpeg_ir::geometry::nurbs::NurbsPoles3::Rational { points } = curve.pole_rows() else {
return None;
};
let degree = usize::try_from(curve.degree()).ok()?;
if degree < 2 || curve.periodic() || points.len() != 4 * degree + 1 {
return None;
}
let knot_tolerance = EPS_GEOMETRY_RATIONAL_FOUR_ARC_CIRCLE_E12
* (curve.knots()[curve.knots().len() - 1] * 0.5 - curve.knots()[0] * 0.5).abs()
* 2.0;
let spans = [
curve.knots()[0],
curve.knots()[degree + 1],
curve.knots()[2 * degree + 1],
curve.knots()[3 * degree + 1],
curve.knots()[4 * degree + 1],
];
if spans
.windows(2)
.any(|pair| pair[1] - pair[0] <= knot_tolerance)
|| (0..5).any(|span| {
let range = if span == 0 {
0..degree + 1
} else if span == 4 {
4 * degree + 1..curve.knots().len()
} else {
span * degree + 1..(span + 1) * degree + 1
};
curve.knots()[range]
.iter()
.any(|value| (*value - spans[span]).abs() > knot_tolerance)
})
{
return None;
}
let weight_scale = points
.iter()
.map(|pole| pole.weight.get().abs())
.fold(0.0, f64::max);
let mut homogeneous = propagate_resource!(
ctx.collection_vec(points.len(), "ASM rational four-arc homogeneous poles")
);
for pole in points {
let point = pole.point;
let weight = pole.weight.get() / weight_scale;
let homogeneous_pole = [point.x * weight, point.y * weight, point.z * weight, weight];
if !(weight.is_finite()
&& weight != 0.0
&& homogeneous_pole.iter().all(|value| value.is_finite()))
{
return None;
}
homogeneous.push(homogeneous_pole);
}
let mut quadratics = [None; 4];
for (span, quadratic) in quadratics.iter_mut().enumerate() {
*quadratic = Some(propagate_resource!(reduce_homogeneous_bezier_to_quadratic(
ctx,
&homogeneous[span * degree..=span * degree + degree],
)?));
}
let quadratics = [
quadratics[0]?,
quadratics[1]?,
quadratics[2]?,
quadratics[3]?,
];
let base_weight = quadratics[0][0][3];
let weight_scale = base_weight.abs();
let weight_tolerance = EPS_GEOMETRY_RATIONAL_FOUR_ARC_CIRCLE_E10 * weight_scale;
if !base_weight.is_finite()
|| base_weight == 0.0
|| quadratics.iter().any(|span| {
(span[0][3] - base_weight).abs() > weight_tolerance
|| (span[2][3] - base_weight).abs() > weight_tolerance
|| (span[1][3] - base_weight * std::f64::consts::FRAC_1_SQRT_2).abs()
> weight_tolerance
})
{
return None;
}
let quadratic_points = quadratics.map(|span| {
span.map(|point| {
Point3::new(
point[0] / point[3],
point[1] / point[3],
point[2] / point[3],
)
})
});
let point_distance = |left: Point3, right: Point3| point_vector(left, right).norm();
let scale = quadratic_points
.iter()
.flat_map(|span| span.windows(2))
.map(|pair| point_distance(pair[0], pair[1]))
.fold(0.0_f64, f64::max);
let tolerance = EPS_GEOMETRY_RATIONAL_FOUR_ARC_CIRCLE_E10 * scale;
if point_distance(quadratic_points[0][0], quadratic_points[3][2]) > tolerance
|| quadratic_points
.windows(2)
.any(|pair| point_distance(pair[0][2], pair[1][0]) > tolerance)
{
return None;
}
let first_center = point_sum_difference(
quadratic_points[0][0],
quadratic_points[0][2],
quadratic_points[0][1],
);
for span in &quadratic_points {
let [start, control, end] = *span;
let center = point_sum_difference(start, end, control);
if point_distance(center, first_center) > tolerance {
return None;
}
}
let first_radial = point_vector(first_center, quadratic_points[0][0]);
let radius = first_radial.norm();
if !radius.is_finite() || radius <= tolerance {
return None;
}
let mut normal = None;
for span in &quadratic_points {
let radial = point_vector(first_center, span[0]);
let next = point_vector(first_center, span[2]);
let radial_unit = FiniteVector3::new(radial)?.unit_nonzero()?;
let next_unit = FiniteVector3::new(next)?.unit_nonzero()?;
if (radial.norm() - radius).abs() > tolerance
|| radial_unit.dot(next_unit).abs() > EPS_GEOMETRY_RATIONAL_FOUR_ARC_CIRCLE_E10
{
return None;
}
let span_normal = FiniteVector3::new(radial_unit.cross(next_unit))?.unit_nonzero()?;
if normal.is_some_and(|normal: Vector3| {
normal.dot(span_normal) < 1.0 - EPS_GEOMETRY_RATIONAL_FOUR_ARC_CIRCLE_E10
}) {
return None;
}
normal.get_or_insert(span_normal);
}
Some(Ok((
first_center,
normal?,
FiniteVector3::new(first_radial)?.unit_nonzero()?,
radius,
)))
}
fn reduce_homogeneous_bezier_to_quadratic(
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
input: &[[f64; 4]],
) -> Option<Result<[[f64; 4]; 3], cadmpeg_core::CodecError>> {
let mut control =
propagate_resource!(ctx.collection_vec(input.len(), "ASM rational four-arc control copy"));
control.extend_from_slice(input);
while control.len() > 3 {
let degree = control.len() - 1;
let mut reduced = propagate_resource!(
ctx.collection_vec(degree, "ASM rational four-arc degree reduction")
);
reduced.push(control[0]);
for index in 1..degree {
let alpha = cadmpeg_core::convert::f64_from_index(index)?
/ cadmpeg_core::convert::f64_from_index(degree)?;
let denominator = 1.0 - alpha;
reduced.push(std::array::from_fn(|coordinate| {
(control[index][coordinate] - alpha * reduced[index - 1][coordinate]) / denominator
}));
}
if reduced.iter().flatten().any(|value| !value.is_finite()) {
return None;
}
if (0..4).any(|coordinate| {
let scale = control
.iter()
.map(|point| point[coordinate].abs())
.fold(0.0, f64::max);
(reduced[degree - 1][coordinate] - control[degree][coordinate]).abs()
> EPS_GEOMETRY_REDUCE_HOMOGENEOUS_BEZIER_TO_QUADRATIC_E10 * scale
}) {
return None;
}
control = reduced;
}
control.try_into().ok().map(Ok)
}
pub(super) fn point_vector(origin: Point3, point: Point3) -> Vector3 {
Vector3::new(point.x - origin.x, point.y - origin.y, point.z - origin.z)
}
fn point_sum_difference(first: Point3, second: Point3, subtract: Point3) -> Point3 {
Point3::new(
first.x + second.x - subtract.x,
first.y + second.y - subtract.y,
first.z + second.z - subtract.z,
)
}
pub(super) fn clamp_edge_ranges_to_carrier_domains(
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
out: &mut AsmBrep,
) -> Result<(), cadmpeg_core::CodecError> {
let domains: HashMap<&str, [f64; 2]> = ctx.collect_hash_map(
out.curves.iter().filter_map(|curve| match &curve.geometry {
CurveGeometry::Solved(SolvedCurveGeometry::Nurbs(nurbs)) => {
let first = nurbs.knots().get(usize::try_from(nurbs.degree()).ok()?)?;
let last = nurbs.knots().get(nurbs.pole_count())?;
Some((curve.id.as_str(), [*first, *last]))
}
_ => None,
}),
"ASM edge carrier domains",
)?;
for edge in &mut out.edges {
let Some([mut start, mut end]) = edge.param_range().map(FiniteVector::get) else {
continue;
};
let Some([first, last]) = edge.curve().and_then(|curve| domains.get(curve.as_str())) else {
continue;
};
let tolerance = (last * 0.5 - first * 0.5).abs()
* (2.0 * EPS_GEOMETRY_CLAMP_EDGE_RANGES_TO_CARRIER_DOMAINS_E9);
if start < *first && *first - start <= tolerance {
start = *first;
}
if end > *last && end - *last <= tolerance {
end = *last;
}
edge.carrier = cadmpeg_ir::topology::EdgeCarrier::new(
edge.curve()
.map(|curve| curve.try_clone_for_decode(ctx, "ASM edge carrier identity"))
.transpose()?,
Some([start, end]),
)
.map_err(cadmpeg_core::CodecError::malformed)?;
}
Ok(())
}
pub(super) fn classify_body_kinds(
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
out: &mut AsmBrep,
) -> Result<(), cadmpeg_core::CodecError> {
let mut shell_bodies = HashMap::new();
for region in &out.regions {
for shell in ®ion.shells {
ctx.insert_hash_map(&mut shell_bodies, shell, ®ion.body, "ASM shell bodies")?;
}
}
let mut body_has_faces = HashSet::new();
let mut body_has_wires = HashSet::new();
let mut face_bodies = HashMap::new();
for shell in &out.shells {
let Some(body) = shell_bodies.get(&shell.id).copied() else {
continue;
};
if !shell.wire_edges().is_empty() || !shell.free_vertices().is_empty() {
ctx.insert_hash_set(&mut body_has_wires, body, "ASM bodies with wires")?;
}
if !shell.faces().is_empty() {
ctx.insert_hash_set(&mut body_has_faces, body, "ASM bodies with faces")?;
}
for face in shell.faces() {
ctx.insert_hash_map(&mut face_bodies, face, body, "ASM face bodies")?;
}
}
let mut loop_bodies = HashMap::new();
for face in &out.faces {
let Some(body) = face_bodies.get(&face.id).copied() else {
continue;
};
for loop_id in &face.loops {
ctx.insert_hash_map(&mut loop_bodies, loop_id, body, "ASM loop bodies")?;
}
}
let mut coedge_bodies = HashMap::new();
for loop_ in &out.loops {
let Some(body) = loop_bodies.get(&loop_.id).copied() else {
continue;
};
for coedge in loop_.coedges() {
ctx.insert_hash_map(&mut coedge_bodies, coedge, body, "ASM coedge bodies")?;
}
}
let mut edge_use_counts = HashMap::<_, HashMap<&EdgeId, usize>>::new();
for coedge in &out.coedges {
if let Some(body) = coedge_bodies.get(&coedge.id).copied() {
ctx.admit_hash_map_entry(&mut edge_use_counts, &body, "ASM body edge use counts")?;
let counts = edge_use_counts.entry(body).or_default();
ctx.admit_hash_map_entry(counts, &&coedge.edge, "ASM edge use counts")?;
*counts.entry(&coedge.edge).or_default() += 1;
}
}
for body in &mut out.bodies {
if !body_has_faces.contains(&body.id) {
body.kind = cadmpeg_ir::topology::BodyKind::Wire;
continue;
}
if body_has_wires.contains(&body.id) {
body.kind = cadmpeg_ir::topology::BodyKind::General;
continue;
}
let counts = edge_use_counts.get(&body.id);
body.kind = if counts
.is_some_and(|counts| !counts.is_empty() && counts.values().all(|count| *count == 2))
{
cadmpeg_ir::topology::BodyKind::Solid
} else {
cadmpeg_ir::topology::BodyKind::Sheet
};
}
Ok(())
}
#[cfg(test)]
mod analytic_surface_tests {
use super::{collect_carrier, decode_surface};
use crate::sab::{Record, Token};
use std::sync::Arc;
fn assert_carrier_limit(token: Token, operation: &str) {
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
use cadmpeg_core::CodecError;
let arena = DecodeArena::new();
let mut policy = DecodePolicy::service();
policy.limits.max_collection_items = 0;
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
let record = surface_record("plane", vec![token]);
let error = collect_carrier(&ctx, &record)
.err()
.expect("resource refusal");
let CodecError::ResourceLimit(limit) = error else {
panic!("expected resource refusal: {error:?}")
};
assert_eq!(limit.dimension, ResourceDimension::CollectionItems);
assert_eq!(limit.operation, operation);
}
#[test]
fn carrier_positions_refuse_collection_limit() {
assert_carrier_limit(Token::Position([0.0; 3]), "ASM carrier positions");
}
#[test]
fn carrier_vectors_refuse_collection_limit() {
assert_carrier_limit(Token::Vector3([1.0, 0.0, 0.0]), "ASM carrier vectors");
}
#[test]
fn carrier_doubles_refuse_collection_limit() {
assert_carrier_limit(Token::Double(1.0), "ASM carrier doubles");
}
fn surface_record(head: &str, tokens: Vec<Token>) -> Record {
Record {
index: 1,
name: format!("{head}-surface"),
tokens: Arc::from(tokens),
offset: 0,
len: 0,
}
}
#[test]
fn finite_cone_intervals_do_not_shift_the_carrier_scalars() {
let record = surface_record(
"cone",
vec![
Token::Position([0.0, 0.0, 0.0]),
Token::Vector3([0.0, 0.0, 1.0]),
Token::Vector3([7.0, 0.0, 0.0]),
Token::Double(1.0),
Token::True,
Token::Double(2.0),
Token::True,
Token::Double(3.0),
Token::Double(0.0),
Token::Double(1.0),
Token::Double(7.0),
Token::False,
Token::True,
Token::Double(4.0),
Token::True,
Token::Double(5.0),
Token::False,
Token::False,
],
);
crate::test_support::with_service_context(&[], |ctx| {
let carrier = collect_carrier(ctx, &record).unwrap();
assert_eq!(carrier.doubles, [1.0, 0.0, 1.0, 7.0]);
let (surface, _) = decode_surface(ctx, &record).unwrap().unwrap();
assert!(matches!(
surface,
cadmpeg_ir::geometry::SolvedSurfaceGeometry::Cylinder(_)
));
})
.unwrap();
}
#[test]
fn analytic_surfaces_require_complete_serialized_frames() {
let origin = Token::Position([0.0, 0.0, 0.0]);
let axis = Token::Vector3([0.0, 0.0, 1.0]);
let plane = surface_record("plane", vec![origin.clone(), axis.clone()]);
let cone_without_major = surface_record(
"cone",
vec![
origin.clone(),
axis.clone(),
Token::Double(1.0),
Token::Double(0.0),
Token::Double(1.0),
Token::Double(7.0),
],
);
let cone_with_zero_major = surface_record(
"cone",
vec![
origin.clone(),
axis.clone(),
Token::Vector3([0.0, 0.0, 0.0]),
Token::Double(1.0),
],
);
let sphere = surface_record(
"sphere",
vec![origin.clone(), Token::Double(2.0), axis.clone()],
);
let torus = surface_record(
"torus",
vec![origin, axis, Token::Double(3.0), Token::Double(1.0)],
);
for record in [
plane,
cone_without_major,
cone_with_zero_major,
sphere,
torus,
] {
let arena = cadmpeg_core::decode::DecodeArena::new();
let policy = cadmpeg_core::decode::DecodePolicy::service();
let (ctx, _) =
cadmpeg_core::decode::DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
assert!(decode_surface(&ctx, &record).transpose().unwrap().is_none());
}
}
}
#[cfg(test)]
mod sense_tests {
use super::record_reversed;
use crate::sab::{Record, Token};
#[test]
fn intcurve_sense_falls_back_only_when_the_scope_has_no_boolean() {
for (header, before_scope, expected) in [
(Token::True, Token::Long(7), true),
(Token::False, Token::Long(7), false),
(Token::True, Token::False, false),
(Token::False, Token::True, true),
] {
let record = Record {
index: 0,
name: "intcurve".into(),
tokens: vec![
Token::Ref(-1),
Token::Long(-1),
Token::Ref(-1),
header,
before_scope,
Token::SubtypeOpen,
Token::SubtypeClose,
]
.into(),
offset: 0,
len: 0,
};
assert_eq!(record_reversed(&record), expected);
}
}
}
#[cfg(test)]
mod tests {
mod numerical_ranges;
use super::Point3;
const SMALL_CURVED_SPINE_EXTENT: f64 = 1.0e-10;
#[test]
fn rational_circle_degree_reduction_refuses_collection_limit() {
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
use cadmpeg_core::CodecError;
let arena = DecodeArena::new();
let mut policy = DecodePolicy::service();
policy.limits.max_collection_items = 6;
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
let poles = [[0.0, 0.0, 0.0, 1.0]; 4];
let error = super::reduce_homogeneous_bezier_to_quadratic(&ctx, &poles)
.expect("degree-four input")
.expect_err("control copy and degree reduction need seven items");
let CodecError::ResourceLimit(limit) = error else {
panic!("expected collection refusal: {error:?}");
};
assert_eq!(limit.dimension, ResourceDimension::CollectionItems);
}
#[test]
fn numerical_seventh_analytic_spine_rejects_relative_curvature_at_small_scale() {
for scale in [1.0, SMALL_CURVED_SPINE_EXTENT, 1.0e100] {
let spine = |height| {
cadmpeg_ir::geometry::nurbs::NurbsCurve::from_lanes(
&cadmpeg_test_support::service_decode_context(),
2,
vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(0.5 * scale, height * scale, 0.0),
Point3::new(scale, 0.0, 0.0),
],
None,
false,
)
.expect("fixture constructor admission")
.unwrap()
};
assert!(super::linear_nurbs_spine(&spine(0.4)).is_none());
assert!(super::linear_nurbs_spine(&spine(0.0)).is_some());
}
}
#[test]
fn audit_regression_circle_recognition_ignores_knot_units() {
use cadmpeg_ir::geometry::nurbs::NurbsCurve;
let resource_arena = cadmpeg_core::decode::DecodeArena::new();
let (resource_ctx, _) = cadmpeg_core::decode::DecodeContext::from_root_bytes(
&[],
&resource_arena,
&cadmpeg_core::decode::DecodePolicy::default(),
)
.expect("test decode context");
for scale in [1.0, 1e-13] {
let knots = [0., 0., 0., 1., 1., 2., 2., 3., 3., 4., 4., 4.]
.map(|knot| knot * scale)
.to_vec();
let poles = [
(1., 0.),
(1., 1.),
(0., 1.),
(-1., 1.),
(-1., 0.),
(-1., -1.),
(0., -1.),
(1., -1.),
(1., 0.),
]
.into_iter()
.map(|(x, y)| Point3::new(x, y, 0.))
.collect();
let weights = (0..9)
.map(|i| {
if i % 2 == 0 {
1.
} else {
std::f64::consts::FRAC_1_SQRT_2
}
})
.collect();
let circle = NurbsCurve::from_lanes(
&cadmpeg_test_support::service_decode_context(),
2,
knots,
poles,
Some(weights),
false,
)
.expect("fixture constructor admission")
.unwrap();
let (_, _, _, radius) = super::rational_four_arc_circle(&resource_ctx, &circle)
.transpose()
.expect("resource allocation")
.unwrap();
assert!((radius - 1.0).abs() <= 8.0 * f64::EPSILON);
}
}
#[test]
fn audit_regression_edge_clamping_preserves_real_domain_violation() {
use cadmpeg_ir::geometry::nurbs::NurbsCurve;
use cadmpeg_ir::geometry::{Curve, CurveGeometry, SolvedCurveGeometry};
use cadmpeg_ir::ids::{CurveId, EdgeId, VertexId};
use cadmpeg_ir::topology::{Edge, EdgeCarrier};
let resource_arena = cadmpeg_core::decode::DecodeArena::new();
let (resource_ctx, _) = cadmpeg_core::decode::DecodeContext::from_root_bytes(
&[],
&resource_arena,
&cadmpeg_core::decode::DecodePolicy::default(),
)
.expect("test decode context");
let id = CurveId::mint("sat:audit:curve#domain").unwrap();
let curve = NurbsCurve::from_lanes(
&cadmpeg_test_support::service_decode_context(),
1,
vec![0., 0., 1e-12, 1e-12],
vec![Point3::new(0., 0., 0.), Point3::new(1., 0., 0.)],
None,
false,
)
.expect("fixture constructor admission")
.unwrap();
let edge = Edge {
id: EdgeId::mint("sat:audit:edge#domain").unwrap(),
carrier: EdgeCarrier::new(Some(id.clone()), Some([-1e-10, 5e-13])).unwrap(),
start: VertexId::mint("sat:audit:vertex#a").unwrap(),
end: VertexId::mint("sat:audit:vertex#b").unwrap(),
tolerance: None,
};
let mut out = super::AsmBrep {
curves: vec![Curve {
parameter_range: None,
id,
geometry: CurveGeometry::Solved(SolvedCurveGeometry::Nurbs(curve)),
source_object: None,
}],
edges: vec![edge],
..Default::default()
};
super::clamp_edge_ranges_to_carrier_domains(&resource_ctx, &mut out).unwrap();
assert_eq!(
out.edges[0]
.param_range()
.map(cadmpeg_ir::units::FiniteVector::get),
Some([-1e-10, 5e-13])
);
out.edges[0].set_param_range(Some(
cadmpeg_ir::topology::ParameterInterval::new([-1e-23, 5e-13]).unwrap(),
));
super::clamp_edge_ranges_to_carrier_domains(&resource_ctx, &mut out).unwrap();
assert_eq!(
out.edges[0]
.param_range()
.map(cadmpeg_ir::units::FiniteVector::get),
Some([0., 5e-13])
);
}
}