use brepkit_math::vec::{Point3, Vec3};
use brepkit_topology::Topology;
use super::shorter_arc_range;
pub(super) fn segments_for_chord_deviation_a(
radius: f64,
arc_range: f64,
deflection: f64,
angular_tol: f64,
apply_curvature_floor: bool,
) -> usize {
brepkit_math::chord::segments_for_chord_deviation_with_angle(
radius,
arc_range,
deflection,
angular_tol,
0.0,
apply_curvature_floor,
)
}
pub(super) fn plane_axes(normal: Vec3) -> (Vec3, Vec3) {
let up = if normal.x().abs() < 0.9 {
Vec3::new(1.0, 0.0, 0.0)
} else {
Vec3::new(0.0, 1.0, 0.0)
};
let u_axis = normal
.cross(up)
.normalize()
.unwrap_or(Vec3::new(1.0, 0.0, 0.0));
let v_axis = normal
.cross(u_axis)
.normalize()
.unwrap_or(Vec3::new(0.0, 1.0, 0.0));
(u_axis, v_axis)
}
pub(super) fn edge_sample_count(
topo: &Topology,
edge: &brepkit_topology::edge::Edge,
deflection: f64,
angular_tol: f64,
circle_floor: bool,
) -> usize {
use brepkit_topology::edge::EdgeCurve;
match edge.curve() {
EdgeCurve::Line => 2,
EdgeCurve::Circle(c) => {
let radius = c.radius();
if let Ok((t_start, t_end)) = circle_param_range(topo, edge, c) {
let arc_range = (t_end - t_start).abs();
segments_for_chord_deviation_a(
radius,
arc_range,
deflection,
angular_tol,
circle_floor,
) + 1
} else {
segments_for_chord_deviation_a(
radius,
std::f64::consts::TAU,
deflection,
angular_tol,
circle_floor,
) + 1
}
}
EdgeCurve::Ellipse(ellipse) => {
let a = ellipse.semi_major();
let b = ellipse.semi_minor();
let max_curv_radius = a * a / b;
let arc_range = if edge.is_closed() {
std::f64::consts::TAU
} else if let (Ok(sp), Ok(ep)) = (
topo.vertex(edge.start())
.map(brepkit_topology::vertex::Vertex::point),
topo.vertex(edge.end())
.map(brepkit_topology::vertex::Vertex::point),
) {
let ts = ellipse.project(sp);
let mut te = ellipse.project(ep);
if te <= ts {
te += std::f64::consts::TAU;
}
te - ts
} else {
std::f64::consts::TAU
};
segments_for_chord_deviation_a(
max_curv_radius,
arc_range,
deflection,
angular_tol,
true,
)
.min(4096)
}
EdgeCurve::NurbsCurve(nurbs) => {
let (u0, u1) = match (topo.vertex(edge.start()), topo.vertex(edge.end())) {
(Ok(sv), Ok(ev)) => edge.curve().domain_with_endpoints(sv.point(), ev.point()),
_ => nurbs.domain(),
};
let n_spans = nurbs
.control_points()
.len()
.saturating_sub(nurbs.degree())
.max(1);
let coarse_n = (n_spans * 4).clamp(8, 128);
let max_dev = measure_max_chord_deviation(nurbs, u0, u1, coarse_n);
let max_turn = measure_max_segment_turn(nurbs, u0, u1, coarse_n);
let sag_ok = max_dev <= deflection;
let turn_ok = angular_tol <= 0.0 || max_turn <= angular_tol * 0.5;
if sag_ok && turn_ok {
coarse_n
} else {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let sag_n = if sag_ok {
coarse_n
} else {
((coarse_n as f64) * (max_dev / deflection).sqrt()).ceil() as usize
};
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let turn_n = if turn_ok {
coarse_n
} else {
((coarse_n as f64) * (max_turn / (angular_tol * 0.5))).ceil() as usize
};
sag_n.max(turn_n).clamp(8, 4096)
}
}
}
}
pub(super) fn measure_max_chord_deviation(
nurbs: &brepkit_math::nurbs::curve::NurbsCurve,
u0: f64,
u1: f64,
n: usize,
) -> f64 {
let mut max_dev: f64 = 0.0;
#[allow(clippy::cast_precision_loss)]
for i in 0..n {
let t0 = u0 + (u1 - u0) * (i as f64) / (n as f64);
let t1 = u0 + (u1 - u0) * ((i + 1) as f64) / (n as f64);
let p0 = nurbs.evaluate(t0);
let p1 = nurbs.evaluate(t1);
let mid_chord = Point3::new(
(p0.x() + p1.x()) * 0.5,
(p0.y() + p1.y()) * 0.5,
(p0.z() + p1.z()) * 0.5,
);
let mid_curve = nurbs.evaluate((t0 + t1) * 0.5);
let dev = (mid_curve - mid_chord).length();
max_dev = max_dev.max(dev);
}
max_dev
}
pub(super) fn measure_max_segment_turn(
nurbs: &brepkit_math::nurbs::curve::NurbsCurve,
u0: f64,
u1: f64,
n: usize,
) -> f64 {
let mut max_turn: f64 = 0.0;
#[allow(clippy::cast_precision_loss)]
for i in 0..n {
let t0 = u0 + (u1 - u0) * (i as f64) / (n as f64);
let t1 = u0 + (u1 - u0) * ((i + 1) as f64) / (n as f64);
if let (Ok(a), Ok(b)) = (nurbs.tangent(t0), nurbs.tangent(t1)) {
let dot = a.dot(b).clamp(-1.0, 1.0);
max_turn = max_turn.max(dot.acos());
}
}
max_turn
}
pub(super) fn circle_param_range(
topo: &Topology,
edge: &brepkit_topology::edge::Edge,
circle: &brepkit_math::curves::Circle3D,
) -> Result<(f64, f64), crate::OperationsError> {
if edge.is_closed() {
Ok((0.0, std::f64::consts::TAU))
} else {
let sp = topo.vertex(edge.start())?.point();
let ep = topo.vertex(edge.end())?.point();
let ts = circle.project(sp);
let mut te = circle.project(ep);
if te <= ts {
te += std::f64::consts::TAU;
}
Ok((ts, te))
}
}
pub(super) fn sample_edge(
topo: &Topology,
edge: &brepkit_topology::edge::Edge,
deflection: f64,
angular_tol: f64,
circle_floor: bool,
) -> Result<Vec<Point3>, crate::OperationsError> {
use brepkit_geometry::sampling::sample_uniform;
use brepkit_topology::edge::EdgeCurve;
let n = edge_sample_count(topo, edge, deflection, angular_tol, circle_floor);
let points = match edge.curve() {
EdgeCurve::Line => {
vec![
topo.vertex(edge.start())?.point(),
topo.vertex(edge.end())?.point(),
]
}
EdgeCurve::Circle(circle) => {
let (t_start, t_end) = circle_param_range(topo, edge, circle)?;
sample_uniform(circle, t_start, t_end, n)
}
EdgeCurve::Ellipse(ellipse) => {
let (t_start, t_end) = if edge.is_closed() {
(0.0, std::f64::consts::TAU)
} else {
let sp = topo.vertex(edge.start())?.point();
let ep = topo.vertex(edge.end())?.point();
let ts = ellipse.project(sp);
let mut te = ellipse.project(ep);
if te <= ts {
te += std::f64::consts::TAU;
}
(ts, te)
};
sample_uniform(ellipse, t_start, t_end, n)
}
EdgeCurve::NurbsCurve(nurbs) => {
let sp = topo.vertex(edge.start())?.point();
let ep = topo.vertex(edge.end())?.point();
let (t0, t1) = edge.curve().domain_with_endpoints(sp, ep);
let (u0, u1) = nurbs.domain();
let is_subspan = (t0 - u0).abs() > 1e-12 || (t1 - u1).abs() > 1e-12;
let mut pts = sample_uniform(nurbs, t0, t1, n);
if !is_subspan && nurbs_runs_end_to_start(topo, edge, nurbs)? {
pts.reverse();
}
pts
}
};
Ok(points)
}
pub(super) fn nurbs_runs_end_to_start(
topo: &Topology,
edge: &brepkit_topology::edge::Edge,
nurbs: &brepkit_math::nurbs::curve::NurbsCurve,
) -> Result<bool, crate::OperationsError> {
if edge.start() == edge.end() {
return Ok(false);
}
let s = topo.vertex(edge.start())?.point();
let e = topo.vertex(edge.end())?.point();
let (u0, u1) = nurbs.domain();
let p0 = nurbs.evaluate(u0);
let p1 = nurbs.evaluate(u1);
let aligned = (p0 - s).length() + (p1 - e).length();
let flipped = (p0 - e).length() + (p1 - s).length();
Ok(flipped < aligned)
}
pub(super) fn sample_wire_positions(
topo: &Topology,
wire: &brepkit_topology::wire::Wire,
tol: f64,
deflection: f64,
angular_tol: f64,
) -> Result<Vec<Point3>, crate::OperationsError> {
use brepkit_topology::edge::EdgeCurve;
let mut positions = Vec::new();
let sample_curve_into = |evaluate: &dyn Fn(f64) -> Point3,
t_for_index: &dyn Fn(usize) -> f64,
n_samples: usize,
forward: bool,
positions: &mut Vec<Point3>| {
let indices: Box<dyn Iterator<Item = usize>> = if forward {
Box::new(0..n_samples)
} else {
Box::new((1..=n_samples).rev())
};
for i in indices {
#[allow(clippy::cast_precision_loss)]
let t = t_for_index(i);
let pt = evaluate(t);
if positions
.last()
.is_none_or(|p: &Point3| (*p - pt).length() > tol)
{
positions.push(pt);
}
}
};
for oe in wire.edges() {
let edge = topo.edge(oe.edge())?;
match edge.curve() {
EdgeCurve::Circle(circle) => {
let (t_start, t_end) = if edge.is_closed() {
(0.0, std::f64::consts::TAU)
} else {
shorter_arc_range(circle, topo, edge)?
};
let arc_range = (t_end - t_start).abs();
let n_samples = segments_for_chord_deviation_a(
circle.radius(),
arc_range,
deflection,
angular_tol,
false,
);
#[allow(clippy::cast_precision_loss)]
sample_curve_into(
&|t| circle.evaluate(t),
&|i| t_start + (t_end - t_start) * (i as f64) / (n_samples as f64),
n_samples,
oe.is_forward(),
&mut positions,
);
}
EdgeCurve::Ellipse(ellipse) => {
let (t_start, t_end) = if edge.is_closed() {
(0.0, std::f64::consts::TAU)
} else {
let sp = topo.vertex(edge.start())?.point();
let ep = topo.vertex(edge.end())?.point();
let ts = ellipse.project(sp);
let mut te = ellipse.project(ep);
if te <= ts {
te += std::f64::consts::TAU;
}
(ts, te)
};
let arc_range = t_end - t_start;
let max_curv_radius =
ellipse.semi_major() * ellipse.semi_major() / ellipse.semi_minor();
let n_samples = segments_for_chord_deviation_a(
max_curv_radius,
arc_range,
deflection,
angular_tol,
true,
);
#[allow(clippy::cast_precision_loss)]
sample_curve_into(
&|t| ellipse.evaluate(t),
&|i| t_start + (t_end - t_start) * (i as f64) / (n_samples as f64),
n_samples,
oe.is_forward(),
&mut positions,
);
}
EdgeCurve::NurbsCurve(nurbs) => {
let sp = topo.vertex(edge.start())?.point();
let ep = topo.vertex(edge.end())?.point();
let (u0, u1) = edge.curve().domain_with_endpoints(sp, ep);
let full = nurbs.domain();
let is_subspan = (u0 - full.0).abs() > 1e-12 || (u1 - full.1).abs() > 1e-12;
let n_spans = nurbs
.control_points()
.len()
.saturating_sub(nurbs.degree())
.max(1);
let coarse_n = (n_spans * 4).clamp(8, 128);
let max_dev = measure_max_chord_deviation(nurbs, u0, u1, coarse_n);
let max_turn = measure_max_segment_turn(nurbs, u0, u1, coarse_n);
let sag_ok = max_dev <= deflection;
let turn_ok = angular_tol <= 0.0 || max_turn <= angular_tol * 0.5;
#[allow(clippy::cast_sign_loss)]
let n_samples = if sag_ok && turn_ok {
coarse_n
} else {
let sag_n = if sag_ok {
coarse_n
} else {
((coarse_n as f64) * (max_dev / deflection).sqrt()).ceil() as usize
};
let turn_n = if turn_ok {
coarse_n
} else {
((coarse_n as f64) * (max_turn / (angular_tol * 0.5))).ceil() as usize
};
sag_n.max(turn_n)
}
.clamp(8, 4096);
let forward = if is_subspan {
oe.is_forward()
} else {
oe.is_forward() != nurbs_runs_end_to_start(topo, edge, nurbs)?
};
#[allow(clippy::cast_precision_loss)]
sample_curve_into(
&|t| nurbs.evaluate(t),
&|i| u0 + (u1 - u0) * (i as f64) / (n_samples as f64),
n_samples,
forward,
&mut positions,
);
}
EdgeCurve::Line => {
let vid = if oe.is_forward() {
edge.start()
} else {
edge.end()
};
let pt = topo.vertex(vid)?.point();
if positions
.last()
.is_none_or(|p: &Point3| (*p - pt).length() > tol)
{
positions.push(pt);
}
}
}
}
if positions.len() > 2
&& let (Some(first), Some(last)) = (positions.first(), positions.last())
&& (*last - *first).length() < tol
{
positions.pop();
}
Ok(positions)
}