use std::borrow::Cow;
use std::f64::consts::{FRAC_PI_2, TAU};
use cadmpeg_ir::geometry::pcurve::{
LinePcurve, PcurveGeometry, PcurveNurbs, PcurveNurbsPoles, WeightedPole2,
};
use cadmpeg_ir::geometry::{DirectedParameterRange, SolvedSurfaceGeometry};
use cadmpeg_ir::math::Point2;
use cadmpeg_ir::topology::Edge;
use cadmpeg_ir::transform::Transform2;
use cadmpeg_ir::units::FinitePoint2;
const EPS_PARAMETER_RANGE_MATCH: f64 = 1.0e-9;
const EPS_LOOP_JOIN: f64 = 1.0e-6;
const EPS_CONIC_PERIOD: f64 = 1.0e-9;
pub(super) fn align_line_to_edge(
line: &LinePcurve,
source_range: Option<[f64; 2]>,
edge: &Edge,
) -> Option<PcurveGeometry> {
let (Some([source_start, source_end]), Some(target)) = (source_range, edge.param_range())
else {
return Some(PcurveGeometry::Line(*line));
};
let [target_start, target_end] = target.get();
if [source_start, source_end] == [target_start, target_end] {
return Some(PcurveGeometry::Line(*line));
}
let scale = (source_end - source_start) / (target_end - target_start);
let offset = source_start - scale * target_start;
let origin = line.origin().get();
let direction = *line.direction().as_raw();
LinePcurve::try_new(
Point2::new(
origin.u + offset * direction.u,
origin.v + offset * direction.v,
),
Point2::new(scale * direction.u, scale * direction.v),
)
.ok()
.map(PcurveGeometry::Line)
}
pub(super) fn align_placed_line_to_edge(
geometry: &PcurveGeometry,
source_range: Option<[f64; 2]>,
edge: &Edge,
) -> Option<PcurveGeometry> {
let mut transforms = Vec::new();
let mut basis = geometry;
while let PcurveGeometry::Transformed(placed) = basis {
transforms.push(*placed.transform());
basis = placed.basis();
}
let PcurveGeometry::Line(line) = basis else {
return None;
};
let mut aligned = align_line_to_edge(line, source_range, edge)?;
for transform in transforms.into_iter().rev() {
aligned = PcurveGeometry::Transformed(
cadmpeg_ir::geometry::pcurve::PlacedPcurve::try_new(Box::new(aligned), transform)
.ok()?,
);
}
Some(aligned)
}
pub(super) fn ranges_match(source_range: Option<[f64; 2]>, edge: &Edge) -> bool {
let (Some([source_start, source_end]), Some(target)) = (source_range, edge.param_range())
else {
return true;
};
let [target_start, target_end] = target.get();
let tolerance = EPS_PARAMETER_RANGE_MATCH
* (source_end - source_start)
.abs()
.max((target_end - target_start).abs())
.max(1.0);
(source_start - target_start).abs() <= tolerance && (source_end - target_end).abs() <= tolerance
}
pub(super) fn lower_placed_conic(
geometry: &PcurveGeometry,
source_range: Option<DirectedParameterRange>,
) -> Cow<'_, PcurveGeometry> {
let lowered = (|| {
let mut transform = Transform2::identity();
let mut basis = geometry;
while let PcurveGeometry::Transformed(placed) = basis {
transform = compose_2d(transform, *placed.transform())?;
basis = placed.basis();
}
let scaled = |axis: Point2, radius: f64| Point2::new(axis.u * radius, axis.v * radius);
let (center, x, y) = match basis {
PcurveGeometry::Circle(circle) => (
circle.center().get(),
scaled(circle.x_axis().get(), circle.radius().get()),
scaled(circle.y_axis().get(), circle.radius().get()),
),
PcurveGeometry::Ellipse(ellipse) => (
ellipse.center().get(),
scaled(ellipse.x_axis().get(), ellipse.major_radius().get()),
scaled(ellipse.y_axis().get(), ellipse.minor_radius().get()),
),
_ => return None,
};
if crate::geometry::pcurve_placement_bakes(geometry) {
return None;
}
let [start, end] = source_range.map_or([0.0, TAU], |range| {
let [a, b] = range.endpoints();
[a.min(b), a.max(b)]
});
let span = end - start;
if !span.is_finite() || span <= 0.0 || span > TAU * (1.0 + EPS_CONIC_PERIOD) {
return None;
}
let spans = (1_u32..=4)
.find(|spans| span <= FRAC_PI_2 * f64::from(*spans) * (1.0 + EPS_CONIC_PERIOD))?;
let step = span / f64::from(spans);
let weight = (step / 2.0).cos();
let at = |angle: f64, reach: f64| {
transform.apply_point(Point2::new(
center.u + reach * (angle.cos() * x.u + angle.sin() * y.u),
center.v + reach * (angle.cos() * x.v + angle.sin() * y.v),
))
};
let mut points = vec![at(start, 1.0)];
let mut weights = vec![1.0];
let mut knots = vec![start, start, start];
let mut angle = start;
for index in 1..=spans {
let last = index == spans;
let next = if last {
end
} else {
start + step * f64::from(index)
};
points.push(at(angle + step / 2.0, 1.0 / weight));
points.push(at(next, 1.0));
weights.extend([weight, 1.0]);
knots.extend(std::iter::repeat_n(next, if last { 3 } else { 2 }));
angle = next;
}
let poles = PcurveNurbsPoles::from_lanes(points, Some(weights)).ok()?;
let nurbs = PcurveNurbs::new(2, knots, poles, false).ok()?;
Some(PcurveGeometry::Nurbs { nurbs })
})();
lowered.map_or_else(|| Cow::Borrowed(geometry), Cow::Owned)
}
fn compose_2d(outer: Transform2, inner: Transform2) -> Option<Transform2> {
let left = outer.rows();
let right = inner.rows();
let entry = |row: usize, column: usize| {
(0..3)
.map(|inner| left[row][inner] * right[inner][column])
.sum::<f64>()
};
Transform2::affine([
[entry(0, 0), entry(0, 1), entry(0, 2)],
[entry(1, 0), entry(1, 1), entry(1, 2)],
])
}
pub(super) fn align_spline_to_edge<'a>(
geometry: &'a PcurveGeometry,
source_range: Option<[f64; 2]>,
edge: &Edge,
) -> Cow<'a, PcurveGeometry> {
let aligned = (|| {
let PcurveGeometry::Nurbs { nurbs } = geometry else {
return None;
};
let [source_start, source_end] = source_range?;
let [target_start, target_end] = edge.param_range()?.get();
let source_span = source_end - source_start;
let target_span = target_end - target_start;
if !source_span.is_finite()
|| !target_span.is_finite()
|| source_span == 0.0
|| target_span <= 0.0
{
return None;
}
let tolerance = EPS_PARAMETER_RANGE_MATCH * source_span.abs().max(target_span).max(1.0);
if (source_start - target_start).abs() <= tolerance
&& (source_end - target_end).abs() <= tolerance
{
return None;
}
let scale = target_span / source_span;
let to_edge = |knot: &f64| target_start + (knot - source_start) * scale;
let reversed = source_span < 0.0;
let knots = if reversed {
nurbs.knots().iter().rev().map(to_edge).collect::<Vec<_>>()
} else {
nurbs.knots().iter().map(to_edge).collect::<Vec<_>>()
};
if knots.iter().any(|knot| !knot.is_finite()) {
return None;
}
let poles = if reversed {
match nurbs.pole_rows() {
PcurveNurbsPoles::Polynomial { points } => PcurveNurbsPoles::Polynomial {
points: points.iter().rev().copied().collect(),
},
PcurveNurbsPoles::Rational { points } => PcurveNurbsPoles::Rational {
points: points.iter().rev().copied().collect(),
},
}
} else {
nurbs.pole_rows().clone()
};
let nurbs = PcurveNurbs::new(nurbs.degree(), knots, poles, nurbs.periodic()).ok()?;
Some(PcurveGeometry::Nurbs { nurbs })
})();
aligned.map_or_else(|| Cow::Borrowed(geometry), Cow::Owned)
}
pub(super) fn surface_periods(surface: &SolvedSurfaceGeometry) -> [Option<f64>; 2] {
match surface {
SolvedSurfaceGeometry::Cylinder(_)
| SolvedSurfaceGeometry::Cone(_)
| SolvedSurfaceGeometry::Sphere(_) => [Some(TAU), None],
SolvedSurfaceGeometry::Torus(_) => [Some(TAU), Some(TAU)],
SolvedSurfaceGeometry::Nurbs(nurbs) => [
nurbs
.u_periodic()
.then(|| knot_domain(nurbs.u_knots().as_slice(), nurbs.u_degree()))
.flatten(),
nurbs
.v_periodic()
.then(|| knot_domain(nurbs.v_knots().as_slice(), nurbs.v_degree()))
.flatten(),
],
_ => [None, None],
}
}
fn knot_domain(knots: &[f64], degree: u32) -> Option<f64> {
let degree = usize::try_from(degree).ok()?;
let start = *knots.get(degree)?;
let end = *knots.get(knots.len().checked_sub(degree.checked_add(1)?)?)?;
let span = end - start;
(span.is_finite() && span > 0.0).then_some(span)
}
pub(super) fn loop_period_shifts(
ends: &[([f64; 2], [f64; 2])],
periods: [Option<f64>; 2],
) -> Vec<[f64; 2]> {
let mut shifts = vec![[0.0; 2]; ends.len()];
for index in 1..ends.len() {
let previous_end = [
ends[index - 1].1[0] + shifts[index - 1][0],
ends[index - 1].1[1] + shifts[index - 1][1],
];
let start = ends[index].0;
let mut shift = [0.0; 2];
let mut joined = true;
for axis in 0..2 {
let gap = previous_end[axis] - start[axis];
if let Some(period) = periods[axis] {
shift[axis] = (gap / period).round() * period;
}
let tolerance = EPS_LOOP_JOIN * periods[axis].unwrap_or(1.0).max(1.0);
joined &= (gap - shift[axis]).abs() <= tolerance;
}
if joined {
shifts[index] = shift;
}
}
shifts
}
pub(super) fn shifted(geometry: &PcurveGeometry, shift: [f64; 2]) -> Option<PcurveGeometry> {
let PcurveGeometry::Nurbs { nurbs } = geometry else {
return None;
};
let moved = |point: FinitePoint2| {
let point = point.get();
FinitePoint2::new(Point2::new(point.u + shift[0], point.v + shift[1]))
};
let poles = match nurbs.pole_rows() {
PcurveNurbsPoles::Polynomial { points } => PcurveNurbsPoles::Polynomial {
points: points
.iter()
.copied()
.map(moved)
.collect::<Option<Vec<_>>>()?,
},
PcurveNurbsPoles::Rational { points } => PcurveNurbsPoles::Rational {
points: points
.iter()
.map(|pole| {
Some(WeightedPole2 {
point: moved(pole.point)?,
weight: pole.weight,
})
})
.collect::<Option<Vec<_>>>()?,
},
};
let nurbs = PcurveNurbs::new(
nurbs.degree(),
nurbs.knots().as_slice().to_vec(),
poles,
nurbs.periodic(),
)
.ok()?;
Some(PcurveGeometry::Nurbs { nurbs })
}
#[cfg(test)]
mod tests;