use super::*;
pub(super) fn derive_angle_scale(entities: &HashMap<usize, Entity>) -> Option<f64> {
let resolve = |id: usize, depth: usize| -> Option<f64> { unit_angle_scale(entities, id, depth) };
for entity in entities.values() {
if !entity.has("GLOBAL_UNIT_ASSIGNED_CONTEXT") {
continue;
}
let Some(args) = entity.find("GLOBAL_UNIT_ASSIGNED_CONTEXT") else {
continue;
};
let Some(units) = args.first().and_then(|value| value.as_list().ok()) else {
continue;
};
if let Some(scale) = units
.iter()
.filter_map(|unit| unit.as_ref_id().ok())
.find_map(|unit_id| resolve(unit_id, 0))
{
return Some(scale);
}
}
for (&id, entity) in entities {
if entity.has("PLANE_ANGLE_UNIT") {
if let Some(scale) = resolve(id, 0) {
return Some(scale);
}
}
}
None
}
fn unit_angle_scale(
entities: &HashMap<usize, Entity>,
id: usize,
depth: usize,
) -> Option<f64> {
if depth > 4 {
return None;
}
let entity = entities.get(&id)?;
if !entity.has("PLANE_ANGLE_UNIT") {
return None;
}
if let Some(args) = entity.find("SI_UNIT") {
if !args.get(1).is_some_and(|name| name.enum_is("RADIAN")) {
return None;
}
let prefix = match args.first() {
Some(Value::Enum(prefix)) => match prefix.as_str() {
"MILLI" => 1e-3,
"CENTI" => 1e-2,
"DECI" => 1e-1,
"DECA" => 1e1,
"HECTO" => 1e2,
"KILO" => 1e3,
"MICRO" => 1e-6,
"NANO" => 1e-9,
_ => return None,
},
_ => 1.0,
};
return Some(prefix);
}
if let Some(args) = entity.find("CONVERSION_BASED_UNIT") {
let measure_entity = entities.get(&args.get(1)?.as_ref_id().ok()?)?;
let measure_args = measure_entity
.find("PLANE_ANGLE_MEASURE_WITH_UNIT")
.or_else(|| measure_entity.find("MEASURE_WITH_UNIT"))?;
let value = match measure_args.first()? {
Value::Typed(_, inner) => inner.first()?.as_real().ok()?,
other => other.as_real().ok()?,
};
let base = unit_angle_scale(entities, measure_args.get(1)?.as_ref_id().ok()?, depth + 1)?;
return Some(value * base);
}
None
}
#[derive(Clone, Debug)]
pub(super) enum StepSurface {
Plane(Frame),
Cylinder { frame: Frame, radius: f64 },
Cone {
frame: Frame,
radius: f64,
semi_angle: f64,
},
Sphere { frame: Frame, radius: f64 },
Torus {
frame: Frame,
major_radius: f64,
minor_radius: f64,
},
}
impl StepSurface {
pub(super) fn evaluate(&self, u: f64, v: f64) -> Vec3 {
let radial = |frame: &Frame, rho: f64, along: f64| {
frame
.origin
.add(frame.x.scale(rho * u.cos()))
.add(frame.y.scale(rho * u.sin()))
.add(frame.z.scale(along))
};
match self {
StepSurface::Plane(frame) => frame.origin.add(frame.x.scale(u)).add(frame.y.scale(v)),
StepSurface::Cylinder { frame, radius } => radial(frame, *radius, v),
StepSurface::Cone {
frame,
radius,
semi_angle,
} => radial(frame, radius + v * semi_angle.tan(), v),
StepSurface::Sphere { frame, radius } => {
radial(frame, radius * v.cos(), radius * v.sin())
}
StepSurface::Torus {
frame,
major_radius,
minor_radius,
} => radial(
frame,
major_radius + minor_radius * v.cos(),
minor_radius * v.sin(),
),
}
}
pub(super) fn invert(&self, point: Vec3) -> (Option<f64>, f64) {
let azimuth = |frame: &Frame, offset: Vec3| {
let x = offset.dot(frame.x);
let y = offset.dot(frame.y);
let scale = 1.0 + offset.length();
(x.hypot(y) > 1e-12 * scale).then(|| y.atan2(x))
};
match self {
StepSurface::Plane(frame) => {
let offset = point.sub(frame.origin);
(Some(offset.dot(frame.x)), offset.dot(frame.y))
}
StepSurface::Cylinder { frame, .. } | StepSurface::Cone { frame, .. } => {
let offset = point.sub(frame.origin);
(azimuth(frame, offset), offset.dot(frame.z))
}
StepSurface::Sphere { frame, .. } => {
let offset = point.sub(frame.origin);
let rho = offset.dot(frame.x).hypot(offset.dot(frame.y));
(azimuth(frame, offset), offset.dot(frame.z).atan2(rho))
}
StepSurface::Torus {
frame,
major_radius,
..
} => {
let offset = point.sub(frame.origin);
let rho = offset.dot(frame.x).hypot(offset.dot(frame.y));
(
azimuth(frame, offset),
offset.dot(frame.z).atan2(rho - major_radius),
)
}
}
}
fn periods(&self) -> (Option<f64>, Option<f64>) {
match self {
StepSurface::Plane(_) => (None, None),
StepSurface::Cylinder { .. } | StepSurface::Cone { .. } | StepSurface::Sphere { .. } => {
(Some(TAU), None)
}
StepSurface::Torus { .. } => (Some(TAU), Some(TAU)),
}
}
fn parameter_scales(&self, length_scale: f64, angle_scale: f64) -> (f64, f64) {
match self {
StepSurface::Plane(_) => (length_scale, length_scale),
StepSurface::Cylinder { .. } | StepSurface::Cone { .. } => (angle_scale, length_scale),
StepSurface::Sphere { .. } | StepSurface::Torus { .. } => (angle_scale, angle_scale),
}
}
fn needs_angle_unit(&self) -> bool {
!matches!(self, StepSurface::Plane(_))
}
fn parameter_metric(&self, v: f64) -> [f64; 2] {
match self {
StepSurface::Plane(_) => [1.0, 1.0],
StepSurface::Cylinder { radius, .. } => [radius.abs().max(f64::MIN_POSITIVE), 1.0],
StepSurface::Cone {
radius, semi_angle, ..
} => [
(radius + v * semi_angle.tan())
.abs()
.max(f64::MIN_POSITIVE),
(1.0 / semi_angle.cos()).abs().max(f64::MIN_POSITIVE),
],
StepSurface::Sphere { radius, .. } => [
(radius * v.cos()).abs().max(f64::MIN_POSITIVE),
radius.abs().max(f64::MIN_POSITIVE),
],
StepSurface::Torus {
major_radius,
minor_radius,
..
} => [
(major_radius + minor_radius * v.cos())
.abs()
.max(f64::MIN_POSITIVE),
minor_radius.abs().max(f64::MIN_POSITIVE),
],
}
}
}
#[derive(Clone, Debug)]
enum Curve2d {
Line { point: [f64; 2], direction: [f64; 2] },
Bounded(NurbsCurve),
}
impl Curve2d {
fn evaluate(&self, s: f64) -> Result<[f64; 2], String> {
Ok(match self {
Curve2d::Line { point, direction } => {
[point[0] + s * direction[0], point[1] + s * direction[1]]
}
Curve2d::Bounded(curve) => {
let value = curve.evaluate(s)?;
[value.x, value.y]
}
})
}
fn nearest(&self, target: [f64; 2]) -> Result<f64, String> {
Ok(match self {
Curve2d::Line { point, direction } => {
let length_squared = direction[0] * direction[0] + direction[1] * direction[1];
if length_squared <= 0.0 {
return Err("step_import: degenerate supplied 2D line".into());
}
((target[0] - point[0]) * direction[0] + (target[1] - point[1]) * direction[1])
/ length_squared
}
Curve2d::Bounded(curve) => {
crate::project_point_to_curve(curve, Vec3::new(target[0], target[1], 0.0))?.u
}
})
}
fn scaled(self, u_scale: f64, v_scale: f64) -> Result<Self, String> {
if u_scale == 1.0 && v_scale == 1.0 {
return Ok(self);
}
Ok(match self {
Curve2d::Line { point, direction } => Curve2d::Line {
point: [point[0] * u_scale, point[1] * v_scale],
direction: [direction[0] * u_scale, direction[1] * v_scale],
},
Curve2d::Bounded(curve) => {
let control_points = curve
.control_points
.iter()
.map(|control| {
let weight = control.w;
let point = control.point()?;
Ok(Vec4::from_point(
Vec3::new(point.x * u_scale, point.y * v_scale, 0.0),
weight,
))
})
.collect::<Result<Vec<_>, String>>()?;
Curve2d::Bounded(NurbsCurve::new(
curve.degree,
curve.knots.clone(),
control_points,
)?)
}
})
}
}
#[derive(Clone, Debug)]
pub(super) struct SuppliedPcurve {
pub(super) basis_ref: usize,
pub(super) surface: StepSurface,
curve: Curve2d,
metric_curve: Curve2d,
metric: [f64; 2],
}
impl SuppliedPcurve {
pub(super) fn snap(
&self,
point: Vec3,
previous: Option<(f64, f64)>,
) -> Result<((f64, f64), Vec3), String> {
let (azimuth, v_raw) = self.surface.invert(point);
let (u_period, v_period) = self.surface.periods();
let mut u = match azimuth {
Some(value) => value,
None => previous.map(|(u, _)| u).unwrap_or(0.0),
};
let mut v = v_raw;
if let (Some(period), Some((previous_u, _))) = (u_period, previous) {
u = lift_to_branch(u, previous_u, period);
}
if let (Some(period), Some((_, previous_v))) = (v_period, previous) {
v = lift_to_branch(v, previous_v, period);
}
if previous.is_none() {
let seed = self.seed_uv()?;
if let Some(period) = u_period {
u = lift_to_branch(u, seed[0], period);
}
if let Some(period) = v_period {
v = lift_to_branch(v, seed[1], period);
}
}
let s = self
.metric_curve
.nearest([u * self.metric[0], v * self.metric[1]])?;
let snapped = self.curve.evaluate(s)?;
Ok((
(snapped[0], snapped[1]),
self.surface.evaluate(snapped[0], snapped[1]),
))
}
pub(super) fn span_in_periods(&self) -> (Option<f64>, Option<f64>) {
let Curve2d::Bounded(curve) = &self.curve else {
return (None, None);
};
let Ok([start, end]) = curve.domain() else {
return (None, None);
};
let samples = 64usize;
let (mut min_u, mut max_u) = (f64::INFINITY, f64::NEG_INFINITY);
let (mut min_v, mut max_v) = (f64::INFINITY, f64::NEG_INFINITY);
for index in 0..=samples {
let s = start + (end - start) * index as f64 / samples as f64;
let Ok([u, v]) = self.curve.evaluate(s) else {
return (None, None);
};
min_u = min_u.min(u);
max_u = max_u.max(u);
min_v = min_v.min(v);
max_v = max_v.max(v);
}
let (u_period, v_period) = self.surface.periods();
(
u_period.map(|period| (max_u - min_u) / period),
v_period.map(|period| (max_v - min_v) / period),
)
}
fn seed_uv(&self) -> Result<[f64; 2], String> {
match &self.curve {
Curve2d::Line { point, .. } => Ok(*point),
Curve2d::Bounded(curve) => {
let [start, end] = curve.domain()?;
self.curve.evaluate(0.5 * (start + end))
}
}
}
}
fn mean_v(curve: &Curve2d) -> Option<f64> {
match curve {
Curve2d::Line { point, .. } => Some(point[1]),
Curve2d::Bounded(curve) => {
let mut total = 0.0;
for control in &curve.control_points {
total += control.point().ok()?.y;
}
Some(total / curve.control_points.len().max(1) as f64)
}
}
}
fn lift_to_branch(value: f64, reference: f64, period: f64) -> f64 {
if !(period > 0.0) {
return value;
}
value + period * ((reference - value) / period).round()
}
impl<'a> Resolver<'a> {
pub(super) fn supplied_pcurves(&self, curve_ref: usize) -> Vec<SuppliedPcurve> {
let Some(angle_scale) = self.angle_scale() else {
return self.read_bundle(curve_ref, None);
};
self.read_bundle(curve_ref, Some(angle_scale))
}
fn read_bundle(&self, curve_ref: usize, angle_scale: Option<f64>) -> Vec<SuppliedPcurve> {
let Ok(entity) = self.get(curve_ref) else {
return Vec::new();
};
let mut supplied = Vec::new();
for wrapper in ["SURFACE_CURVE", "SEAM_CURVE", "INTERSECTION_CURVE"] {
let Some(args) = entity.find(wrapper) else {
continue;
};
let Some(associated) = args.get(2).and_then(|value| value.as_list().ok()) else {
continue;
};
for value in associated {
let Ok(pcurve_ref) = value.as_ref_id() else {
continue;
};
if let Some(pcurve) = self.read_pcurve(pcurve_ref, angle_scale) {
supplied.push(pcurve);
}
}
break;
}
supplied
}
fn read_pcurve(&self, id: usize, angle_scale: Option<f64>) -> Option<SuppliedPcurve> {
let entity = self.get(id).ok()?;
let args = entity.find("PCURVE")?;
let basis_ref = args.get(1)?.as_ref_id().ok()?;
let representation_ref = args.get(2)?.as_ref_id().ok()?;
let surface = self.step_surface(basis_ref)?;
let angle_scale = match (surface.needs_angle_unit(), angle_scale) {
(true, None) => return None,
(_, scale) => scale.unwrap_or(1.0),
};
let representation = self.get(representation_ref).ok()?;
let items = representation
.find("DEFINITIONAL_REPRESENTATION")
.or_else(|| representation.find("REPRESENTATION"))?
.get(1)?
.as_list()
.ok()?;
let curve_ref = items.first()?.as_ref_id().ok()?;
let raw = self.read_curve2d(curve_ref)?;
let (u_scale, v_scale) = surface.parameter_scales(self.length_scale, angle_scale);
let curve = raw.scaled(u_scale, v_scale).ok()?;
let metric = surface.parameter_metric(mean_v(&curve).unwrap_or(0.0));
let metric_curve = curve.clone().scaled(metric[0], metric[1]).ok()?;
Some(SuppliedPcurve {
basis_ref,
surface,
curve,
metric_curve,
metric,
})
}
fn step_surface(&self, id: usize) -> Option<StepSurface> {
let entity = self.get(id).ok()?;
if let Some(args) = entity.find("PLANE") {
return Some(StepSurface::Plane(
self.placement(args.get(1)?.as_ref_id().ok()?).ok()?,
));
}
if let Some(args) = entity.find("CYLINDRICAL_SURFACE") {
return Some(StepSurface::Cylinder {
frame: self.placement(args.get(1)?.as_ref_id().ok()?).ok()?,
radius: self.length(args.get(2)?.as_real().ok()?),
});
}
if let Some(args) = entity.find("CONICAL_SURFACE") {
let mut semi_angle = args.get(3)?.as_real().ok()?;
if semi_angle.abs() >= std::f64::consts::FRAC_PI_2 {
semi_angle = semi_angle.to_radians();
}
return Some(StepSurface::Cone {
frame: self.placement(args.get(1)?.as_ref_id().ok()?).ok()?,
radius: self.length(args.get(2)?.as_real().ok()?),
semi_angle,
});
}
if let Some(args) = entity.find("SPHERICAL_SURFACE") {
return Some(StepSurface::Sphere {
frame: self.placement(args.get(1)?.as_ref_id().ok()?).ok()?,
radius: self.length(args.get(2)?.as_real().ok()?),
});
}
if let Some(args) = entity.find("TOROIDAL_SURFACE") {
return Some(StepSurface::Torus {
frame: self.placement(args.get(1)?.as_ref_id().ok()?).ok()?,
major_radius: self.length(args.get(2)?.as_real().ok()?),
minor_radius: self.length(args.get(3)?.as_real().ok()?),
});
}
None
}
fn read_curve2d(&self, id: usize) -> Option<Curve2d> {
let entity = self.get(id).ok()?;
if let Some(args) = entity.find("LINE") {
let point = self.raw_point2d(args.get(1)?.as_ref_id().ok()?)?;
let vector_entity = self.get(args.get(2)?.as_ref_id().ok()?).ok()?;
let vector_args = vector_entity.find("VECTOR")?;
let direction = self.raw_direction2d(vector_args.get(1)?.as_ref_id().ok()?)?;
let magnitude = vector_args.get(2)?.as_real().ok()?;
return Some(Curve2d::Line {
point,
direction: [direction[0] * magnitude, direction[1] * magnitude],
});
}
if let Some(args) = entity.find("CIRCLE") {
let frame = self.raw_frame2d(args.get(1)?.as_ref_id().ok()?)?;
let radius = args.get(2)?.as_real().ok()?;
return build_ellipse_arc(&frame, radius, radius, 0.0, TAU)
.ok()
.map(Curve2d::Bounded);
}
if let Some(args) = entity.find("ELLIPSE") {
let frame = self.raw_frame2d(args.get(1)?.as_ref_id().ok()?)?;
let semi_major = args.get(2)?.as_real().ok()?;
let semi_minor = args.get(3)?.as_real().ok()?;
return build_ellipse_arc(&frame, semi_major, semi_minor, 0.0, TAU)
.ok()
.map(Curve2d::Bounded);
}
if let (Some(spline), Some(with_knots)) = (
entity.find("B_SPLINE_CURVE"),
entity.find("B_SPLINE_CURVE_WITH_KNOTS"),
) {
let degree = spline.first()?.as_real().ok()? as usize;
let point_refs = spline.get(1)?.as_list().ok()?;
let multiplicities = with_knots.first()?.as_list().ok()?;
let knot_values = with_knots.get(1)?.as_list().ok()?;
let weights = match entity.find("RATIONAL_B_SPLINE_CURVE") {
Some(rational) => rational
.first()?
.as_list()
.ok()?
.iter()
.map(Value::as_real)
.collect::<Result<Vec<_>, _>>()
.ok()?,
None => vec![1.0; point_refs.len()],
};
return self.build_curve2d(degree, point_refs, multiplicities, knot_values, &weights);
}
if let Some(args) = entity.find("B_SPLINE_CURVE_WITH_KNOTS") {
let degree = args.get(1)?.as_real().ok()? as usize;
let point_refs = args.get(2)?.as_list().ok()?;
let multiplicities = args.get(6)?.as_list().ok()?;
let knot_values = args.get(7)?.as_list().ok()?;
let weights = vec![1.0; point_refs.len()];
return self.build_curve2d(degree, point_refs, multiplicities, knot_values, &weights);
}
None
}
fn build_curve2d(
&self,
degree: usize,
point_refs: &[Value],
multiplicities: &[Value],
knot_values: &[Value],
weights: &[f64],
) -> Option<Curve2d> {
let mut knots = expand_knots(multiplicities, knot_values).ok()?;
let mut control_points = point_refs
.iter()
.enumerate()
.map(|(index, value)| {
let raw = self.raw_point2d(value.as_ref_id().ok()?)?;
Some(Vec4::from_point(
Vec3::new(raw[0], raw[1], 0.0),
weights.get(index).copied().unwrap_or(1.0),
))
})
.collect::<Option<Vec<_>>>()?;
clamp_spline(degree, &mut knots, &mut control_points).ok()?;
NurbsCurve::new(degree, knots, control_points)
.ok()
.map(Curve2d::Bounded)
}
fn raw_point2d(&self, id: usize) -> Option<[f64; 2]> {
let coords = self.get(id).ok()?.find("CARTESIAN_POINT")?.get(1)?.as_list().ok()?;
Some([
coords.first().map(Value::as_real)?.ok()?,
coords.get(1).and_then(|v| v.as_real().ok()).unwrap_or(0.0),
])
}
fn raw_direction2d(&self, id: usize) -> Option<[f64; 2]> {
let coords = self.get(id).ok()?.find("DIRECTION")?.get(1)?.as_list().ok()?;
let x = coords.first().map(Value::as_real)?.ok()?;
let y = coords.get(1).and_then(|v| v.as_real().ok()).unwrap_or(0.0);
let length = x.hypot(y);
(length > 0.0).then(|| [x / length, y / length])
}
fn raw_frame2d(&self, id: usize) -> Option<Frame> {
let entity = self.get(id).ok()?;
let args = entity
.find("AXIS2_PLACEMENT_2D")
.or_else(|| entity.find("AXIS2_PLACEMENT_3D"))?;
let origin = self.raw_point2d(args.get(1)?.as_ref_id().ok()?)?;
let reference = match args.get(2) {
Some(Value::Ref(dir_ref)) => self.raw_direction2d(*dir_ref)?,
_ => [1.0, 0.0],
};
Some(Frame {
origin: Vec3::new(origin[0], origin[1], 0.0),
x: Vec3::new(reference[0], reference[1], 0.0),
y: Vec3::new(-reference[1], reference[0], 0.0),
z: Vec3::new(0.0, 0.0, 1.0),
})
}
}