use brepkit_math::mat::Mat4;
use brepkit_math::nurbs::curve::NurbsCurve;
use brepkit_math::nurbs::surface_fitting::interpolate_surface;
use brepkit_math::tolerance::Tolerance;
use brepkit_math::vec::{Point3, Vec3};
use brepkit_topology::Topology;
use brepkit_topology::edge::{Edge, EdgeCurve};
use brepkit_topology::face::{Face, FaceId, FaceSurface};
use brepkit_topology::shell::Shell;
use brepkit_topology::solid::{Solid, SolidId};
use brepkit_topology::vertex::{Vertex, VertexId};
use brepkit_topology::wire::{OrientedEdge, Wire};
use crate::dot_normal_point;
struct Frame {
origin: Point3,
tangent: Vec3,
up: Vec3,
right: Vec3,
}
fn compute_frames(
path: &NurbsCurve,
num_segments: usize,
initial_up: Vec3,
is_closed: bool,
) -> Result<Vec<Frame>, crate::OperationsError> {
let frame_count = if is_closed {
num_segments
} else {
num_segments + 1
};
let mut frames = Vec::with_capacity(frame_count);
let (u0, u1) = path.domain();
let t0 = path.tangent(u0)?;
let up0 = orthogonalize(initial_up, t0);
let right0 = t0.cross(up0);
frames.push(Frame {
origin: path.evaluate(u0),
tangent: t0,
up: up0,
right: right0,
});
let last_k = if is_closed {
num_segments - 1
} else {
num_segments
};
for k in 1..=last_k {
#[allow(clippy::cast_precision_loss)]
let t_param = u0 + (u1 - u0) * (k as f64) / (num_segments as f64);
let origin = path.evaluate(t_param);
let tangent = path.tangent(t_param)?;
let prev = &frames[k - 1];
let v1 = origin - prev.origin;
let c1 = v1.dot(v1);
let (up_l, tangent_l) = if c1 < 1e-30 {
(prev.up, prev.tangent)
} else {
let up_r = prev.up - v1 * (2.0 * v1.dot(prev.up) / c1);
let t_r = prev.tangent - v1 * (2.0 * v1.dot(prev.tangent) / c1);
(up_r, t_r)
};
let v2 = tangent - tangent_l;
let c2 = v2.dot(v2);
let up = if c2 < 1e-30 {
orthogonalize(up_l, tangent)
} else {
let reflected = up_l - v2 * (2.0 * v2.dot(up_l) / c2);
orthogonalize(reflected, tangent)
};
let right = tangent.cross(up);
frames.push(Frame {
origin,
tangent,
up,
right,
});
}
Ok(frames)
}
fn profile_basis(input_normal: Vec3) -> (Vec3, Vec3, Vec3) {
let tangent = input_normal
.normalize()
.unwrap_or_else(|_| Vec3::new(0.0, 0.0, 1.0));
let up = orthogonalize(pick_reference_axis(tangent), tangent);
let right = tangent.cross(up);
(right, up, tangent)
}
fn orthogonalize(v: Vec3, tangent: Vec3) -> Vec3 {
let projected = v - tangent * tangent.dot(v);
projected.normalize().unwrap_or_else(|_| {
let candidate = if tangent.x().abs() < 0.9 {
Vec3::new(1.0, 0.0, 0.0)
} else {
Vec3::new(0.0, 1.0, 0.0)
};
let proj2 = candidate - tangent * tangent.dot(candidate);
proj2.normalize().unwrap_or(Vec3::new(0.0, 0.0, 1.0))
})
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum ProfilePlacement {
AsPositioned,
CentroidOnPath,
}
const PROFILE_PERP_MIN_COS: f64 = 0.99;
pub(crate) fn resolve_placement(
placement: ProfilePlacement,
input_normal: Vec3,
path_tangent_0: Vec3,
) -> ProfilePlacement {
match placement {
ProfilePlacement::AsPositioned
if input_normal.dot(path_tangent_0).abs() < PROFILE_PERP_MIN_COS =>
{
ProfilePlacement::CentroidOnPath
}
other => other,
}
}
fn transform_point(
point: Point3,
reference: Point3,
initial_right: Vec3,
initial_up: Vec3,
initial_tangent: Vec3,
frame: &Frame,
) -> Point3 {
let offset = point - reference;
let local_r = initial_right.dot(offset);
let local_u = initial_up.dot(offset);
let local_t = initial_tangent.dot(offset);
frame.origin + frame.right * local_r + frame.up * local_u + frame.tangent * local_t
}
struct SweptWireData {
ring_verts: Vec<Vec<VertexId>>,
ring_edges: Vec<Vec<brepkit_topology::edge::EdgeId>>,
path_edges: Vec<Vec<brepkit_topology::edge::EdgeId>>,
n: usize,
}
#[allow(clippy::too_many_arguments)]
fn sweep_wire_through_frames(
topo: &mut Topology,
wire_id: brepkit_topology::wire::WireId,
reference: Point3,
initial_right: Vec3,
initial_up: Vec3,
initial_tangent: Vec3,
frames: &[Frame],
num_segments: usize,
is_closed: bool,
) -> Result<SweptWireData, crate::OperationsError> {
let tol = Tolerance::new();
let wire = topo.wire(wire_id)?;
let oriented: Vec<_> = wire.edges().to_vec();
let n = oriented.len();
let mut verts: Vec<VertexId> = Vec::with_capacity(n);
for oe in &oriented {
let edge = topo.edge(oe.edge())?;
let vid = oe.oriented_start(edge);
verts.push(vid);
}
let positions: Vec<Point3> = verts
.iter()
.map(|&vid| {
topo.vertex(vid)
.map(brepkit_topology::vertex::Vertex::point)
})
.collect::<Result<_, _>>()?;
let mut ring_verts: Vec<Vec<VertexId>> = Vec::with_capacity(num_segments + 1);
for frame in frames {
let ring: Vec<VertexId> = positions
.iter()
.map(|&pos| {
let transformed = transform_point(
pos,
reference,
initial_right,
initial_up,
initial_tangent,
frame,
);
topo.add_vertex(Vertex::new(transformed, tol.linear))
})
.collect();
ring_verts.push(ring);
}
if is_closed {
ring_verts.push(ring_verts[0].clone());
}
let real_ring_count = if is_closed {
ring_verts.len() - 1
} else {
ring_verts.len()
};
let mut ring_edges: Vec<Vec<brepkit_topology::edge::EdgeId>> =
Vec::with_capacity(num_segments + 1);
for ring in &ring_verts[..real_ring_count] {
let edges: Vec<_> = (0..n)
.map(|i| {
let next = (i + 1) % n;
topo.add_edge(Edge::new(ring[i], ring[next], EdgeCurve::Line))
})
.collect();
ring_edges.push(edges);
}
if is_closed {
ring_edges.push(ring_edges[0].clone());
}
let mut path_edges: Vec<Vec<brepkit_topology::edge::EdgeId>> = Vec::with_capacity(num_segments);
for seg in 0..num_segments {
let edges: Vec<_> = (0..n)
.map(|i| {
topo.add_edge(Edge::new(
ring_verts[seg][i],
ring_verts[seg + 1][i],
EdgeCurve::Line,
))
})
.collect();
path_edges.push(edges);
}
Ok(SweptWireData {
ring_verts,
ring_edges,
path_edges,
n,
})
}
fn build_inner_side_faces(
topo: &mut Topology,
iwd: &SweptWireData,
num_segments: usize,
) -> Result<Vec<FaceId>, crate::OperationsError> {
let mut faces = Vec::new();
for seg in 0..num_segments {
for i in 0..iwd.n {
let next_i = (i + 1) % iwd.n;
let p0 = topo.vertex(iwd.ring_verts[seg][i])?.point();
let p1 = topo.vertex(iwd.ring_verts[seg][next_i])?.point();
let p_next = topo.vertex(iwd.ring_verts[seg + 1][i])?.point();
let edge_dir = p1 - p0;
let path_dir = p_next - p0;
let side_normal = path_dir
.cross(edge_dir)
.normalize()
.unwrap_or(Vec3::new(1.0, 0.0, 0.0));
let side_d = dot_normal_point(side_normal, p0);
let side_wire = Wire::new(
vec![
OrientedEdge::new(iwd.path_edges[seg][i], true),
OrientedEdge::new(iwd.ring_edges[seg + 1][i], true),
OrientedEdge::new(iwd.path_edges[seg][next_i], false),
OrientedEdge::new(iwd.ring_edges[seg][i], false),
],
true,
)
.map_err(crate::OperationsError::Topology)?;
let side_wire_id = topo.add_wire(side_wire);
let fid = topo.add_face(Face::new(
side_wire_id,
vec![],
FaceSurface::Plane {
normal: side_normal,
d: side_d,
},
));
faces.push(fid);
}
}
Ok(faces)
}
fn build_inner_cap_wires(
topo: &mut Topology,
inner_data: &[SweptWireData],
ring_idx: usize,
reversed: bool,
) -> Result<Vec<brepkit_topology::wire::WireId>, crate::OperationsError> {
let mut wires = Vec::new();
for iwd in inner_data {
let edges: Vec<OrientedEdge> = if reversed {
(0..iwd.n)
.rev()
.map(|i| OrientedEdge::new(iwd.ring_edges[ring_idx][i], false))
.collect()
} else {
(0..iwd.n)
.map(|i| OrientedEdge::new(iwd.ring_edges[ring_idx][i], true))
.collect()
};
let wire = Wire::new(edges, true).map_err(crate::OperationsError::Topology)?;
wires.push(topo.add_wire(wire));
}
Ok(wires)
}
#[must_use]
pub fn densify_path_points(points: &[Point3]) -> Vec<Point3> {
const GAP_RATIO: f64 = 4.0;
const MAX_INSERT_PER_SEGMENT: usize = 256;
if points.len() < 3 {
return points.to_vec();
}
let mut gaps: Vec<f64> = points
.windows(2)
.map(|w| (w[1] - w[0]).length())
.filter(|d| *d > 1e-12)
.collect();
if gaps.is_empty() {
return points.to_vec();
}
gaps.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let median = gaps[gaps.len() / 2];
let max_gap = median * GAP_RATIO;
if max_gap <= 0.0 {
return points.to_vec();
}
let mut out: Vec<Point3> = Vec::with_capacity(points.len());
for w in points.windows(2) {
let (a, b) = (w[0], w[1]);
out.push(a);
let seg = (b - a).length();
if seg > max_gap {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let steps = ((seg / max_gap).ceil() as usize).min(MAX_INSERT_PER_SEGMENT);
for s in 1..steps {
#[allow(clippy::cast_precision_loss)]
let f = (s as f64) / (steps as f64);
out.push(a + (b - a) * f);
}
}
}
if let Some(&last) = points.last() {
out.push(last);
}
out
}
const STRAIGHT_SAMPLES: usize = 8;
fn profile_outer_centroid(
topo: &Topology,
profile: FaceId,
) -> Result<Point3, crate::OperationsError> {
let face = topo.face(profile)?;
let wire = topo.wire(face.outer_wire())?;
let (mut sx, mut sy, mut sz) = (0.0_f64, 0.0_f64, 0.0_f64);
let mut count = 0_usize;
for oe in wire.edges() {
let edge = topo.edge(oe.edge())?;
let start = topo.vertex(edge.start())?.point();
let end = topo.vertex(edge.end())?.point();
let (t0, t1) = edge.curve().domain_with_endpoints(start, end);
for k in 0..4 {
let t = t0 + (t1 - t0) * (f64::from(k) / 4.0);
let p = edge.curve().evaluate_with_endpoints(t, start, end);
sx += p.x();
sy += p.y();
sz += p.z();
count += 1;
}
}
if count == 0 {
return Err(crate::OperationsError::InvalidInput {
reason: "sweep profile has no outer-wire edges".into(),
});
}
#[allow(clippy::cast_precision_loss)]
let n = count as f64;
Ok(Point3::new(sx / n, sy / n, sz / n))
}
fn try_straight_extrude(
topo: &mut Topology,
profile: FaceId,
path: &NurbsCurve,
placement: ProfilePlacement,
) -> Result<Option<SolidId>, crate::OperationsError> {
let tol = Tolerance::new();
let normal = match topo.face(profile)?.surface() {
FaceSurface::Plane { normal, .. } => *normal,
_ => return Ok(None),
};
let start = path.evaluate(0.0);
let end = path.evaluate(1.0);
let chord = end - start;
let length = chord.length();
if length < tol.linear {
return Ok(None); }
let Ok(dir) = chord.normalize() else {
return Ok(None);
};
for k in 1..STRAIGHT_SAMPLES {
#[allow(clippy::cast_precision_loss)]
let t = k as f64 / STRAIGHT_SAMPLES as f64;
let v = path.evaluate(t) - start;
let along = v.dot(dir);
let perp = (v - dir * along).length();
if perp > tol.linear * 100.0 || along < -tol.linear || along > length + tol.linear {
return Ok(None);
}
}
if normal.dot(dir).abs() < 1.0 - 1e-6 {
return Ok(None);
}
let moved = crate::copy::copy_face(topo, profile)?;
if placement == ProfilePlacement::CentroidOnPath {
let centroid = profile_outer_centroid(topo, profile)?;
let shift = start - centroid;
crate::transform::transform_face(
topo,
moved,
&Mat4::translation(shift.x(), shift.y(), shift.z()),
)?;
}
Ok(Some(crate::extrude::extrude(topo, moved, dir, length)?))
}
pub fn sweep(
topo: &mut Topology,
profile: FaceId,
path: &NurbsCurve,
) -> Result<SolidId, crate::OperationsError> {
sweep_placed(topo, profile, path, ProfilePlacement::AsPositioned)
}
#[allow(clippy::too_many_lines)]
pub(crate) fn sweep_placed(
topo: &mut Topology,
profile: FaceId,
path: &NurbsCurve,
placement: ProfilePlacement,
) -> Result<SolidId, crate::OperationsError> {
let tol = Tolerance::new();
if path.control_points().len() < 2 {
return Err(crate::OperationsError::InvalidInput {
reason: "sweep path must have at least 2 control points".into(),
});
}
if let Some(solid) = try_straight_extrude(topo, profile, path, placement)? {
return Ok(solid);
}
let face_data = topo.face(profile)?;
let input_wire_id = face_data.outer_wire();
let inner_wire_ids: Vec<brepkit_topology::wire::WireId> = face_data.inner_wires().to_vec();
let start_end_coincide = tol.approx_eq(
(path.evaluate(1.0) - path.evaluate(0.0)).length_squared(),
0.0,
);
let is_closed = if start_end_coincide {
let mid = path.evaluate(0.5);
let mid_dist_sq = (mid - path.evaluate(0.0)).length_squared();
if tol.approx_eq(mid_dist_sq, 0.0) {
return Err(crate::OperationsError::InvalidInput {
reason: "sweep path has zero length (start and end coincide)".into(),
});
}
true
} else {
false
};
let input_wire = topo.wire(input_wire_id)?;
let original_oriented: Vec<_> = input_wire.edges().to_vec();
if original_oriented.is_empty() {
return Err(crate::OperationsError::InvalidInput {
reason: "sweep profile has no edges".into(),
});
}
let input_oriented = crate::extrude::maybe_split_closed_wire(
topo,
&original_oriented,
tol.linear,
crate::extrude::DEFAULT_DEFLECTION,
)?;
let n = input_oriented.len();
let mut input_verts: Vec<VertexId> = Vec::with_capacity(n);
for oe in &input_oriented {
let edge = topo.edge(oe.edge())?;
let vid = oe.oriented_start(edge);
input_verts.push(vid);
}
let mut input_positions: Vec<Point3> = input_verts
.iter()
.map(|&vid| {
topo.vertex(vid)
.map(brepkit_topology::vertex::Vertex::point)
})
.collect::<Result<_, _>>()?;
let mut input_normal = crate::winding::newell_normal(&input_positions)
.normalize()
.unwrap_or(Vec3::new(0.0, 0.0, 1.0));
let path_tangent_0 = path.tangent(0.0)?;
if crate::winding::ensure_ccw_positions(&mut input_positions, path_tangent_0) {
input_normal = -input_normal;
}
let num_segments = (path.control_points().len() * 2).max(4);
let up_hint = orthogonalize(input_normal, path_tangent_0);
let frames = compute_frames(path, num_segments, up_hint, is_closed)?;
let (reference, initial_right, initial_up, initial_tangent) =
match resolve_placement(placement, input_normal, path_tangent_0) {
ProfilePlacement::AsPositioned => (
frames[0].origin,
frames[0].right,
frames[0].up,
frames[0].tangent,
),
ProfilePlacement::CentroidOnPath => {
let (r, u, t) = profile_basis(input_normal);
(crate::winding::polygon_centroid(&input_positions), r, u, t)
}
};
let mut ring_verts: Vec<Vec<VertexId>> = Vec::with_capacity(num_segments + 1);
for frame in &frames {
let ring: Vec<VertexId> = input_positions
.iter()
.map(|&pos| {
let transformed = transform_point(
pos,
reference,
initial_right,
initial_up,
initial_tangent,
frame,
);
topo.add_vertex(Vertex::new(transformed, tol.linear))
})
.collect();
ring_verts.push(ring);
}
if is_closed {
ring_verts.push(ring_verts[0].clone());
}
let real_ring_count = if is_closed {
num_segments
} else {
num_segments + 1
};
let mut ring_edges: Vec<Vec<brepkit_topology::edge::EdgeId>> =
Vec::with_capacity(num_segments + 1);
for ring in &ring_verts[..real_ring_count] {
let edges: Vec<_> = (0..n)
.map(|i| {
let next = (i + 1) % n;
topo.add_edge(Edge::new(ring[i], ring[next], EdgeCurve::Line))
})
.collect();
ring_edges.push(edges);
}
if is_closed {
ring_edges.push(ring_edges[0].clone());
}
let mut path_edges: Vec<Vec<brepkit_topology::edge::EdgeId>> = Vec::with_capacity(num_segments);
for seg in 0..num_segments {
let edges: Vec<_> = (0..n)
.map(|i| {
topo.add_edge(Edge::new(
ring_verts[seg][i],
ring_verts[seg + 1][i],
EdgeCurve::Line,
))
})
.collect();
path_edges.push(edges);
}
let mut inner_swept: Vec<SweptWireData> = Vec::new();
for &iw_id in &inner_wire_ids {
inner_swept.push(sweep_wire_through_frames(
topo,
iw_id,
reference,
initial_right,
initial_up,
initial_tangent,
&frames,
num_segments,
is_closed,
)?);
}
let mut all_faces = Vec::with_capacity(num_segments * n + if is_closed { 0 } else { 2 });
if !is_closed {
let start_inner_wires = build_inner_cap_wires(topo, &inner_swept, 0, true)?;
let start_verts = crate::cap::ring_point_positions(topo, &ring_verts[0])?;
let outward = crate::cap::outward_normal(&start_verts, -frames[0].tangent)?;
all_faces.push(crate::cap::build_cap_face(
topo,
&ring_edges[0],
start_inner_wires,
&start_verts,
outward,
true,
)?);
}
for seg in 0..num_segments {
for i in 0..n {
let next_i = (i + 1) % n;
let p0 = topo.vertex(ring_verts[seg][i])?.point();
let p1 = topo.vertex(ring_verts[seg][next_i])?.point();
let p_next = topo.vertex(ring_verts[seg + 1][i])?.point();
let edge_dir = p1 - p0;
let path_dir = p_next - p0;
let side_normal = edge_dir
.cross(path_dir)
.normalize()
.unwrap_or(Vec3::new(1.0, 0.0, 0.0));
let side_d = dot_normal_point(side_normal, p0);
let side_wire = Wire::new(
vec![
OrientedEdge::new(ring_edges[seg][i], true),
OrientedEdge::new(path_edges[seg][next_i], true),
OrientedEdge::new(ring_edges[seg + 1][i], false),
OrientedEdge::new(path_edges[seg][i], false),
],
true,
)
.map_err(crate::OperationsError::Topology)?;
let side_wire_id = topo.add_wire(side_wire);
let side_face = topo.add_face(Face::new(
side_wire_id,
vec![],
FaceSurface::Plane {
normal: side_normal,
d: side_d,
},
));
all_faces.push(side_face);
}
}
for iwd in &inner_swept {
let inner_faces = build_inner_side_faces(topo, iwd, num_segments)?;
all_faces.extend(inner_faces);
}
if !is_closed {
let end_inner_wires = build_inner_cap_wires(topo, &inner_swept, num_segments, false)?;
let end_verts = crate::cap::ring_point_positions(topo, &ring_verts[num_segments])?;
let outward = crate::cap::outward_normal(&end_verts, frames[num_segments].tangent)?;
all_faces.push(crate::cap::build_cap_face(
topo,
&ring_edges[num_segments],
end_inner_wires,
&end_verts,
outward,
false,
)?);
}
let shell = Shell::new(all_faces).map_err(crate::OperationsError::Topology)?;
let shell_id = topo.add_shell(shell);
let solid = topo.add_solid(Solid::new(shell_id, vec![]));
Ok(solid)
}
#[allow(clippy::too_many_lines)]
pub fn sweep_smooth(
topo: &mut Topology,
profile: FaceId,
path: &NurbsCurve,
) -> Result<SolidId, crate::OperationsError> {
let tol = Tolerance::new();
if path.control_points().len() < 2 {
return Err(crate::OperationsError::InvalidInput {
reason: "sweep path must have at least 2 control points".into(),
});
}
let face_data = topo.face(profile)?;
let input_wire_id = face_data.outer_wire();
let inner_wire_ids_smooth: Vec<brepkit_topology::wire::WireId> =
face_data.inner_wires().to_vec();
let start_end_coincide_smooth = tol.approx_eq(
(path.evaluate(1.0) - path.evaluate(0.0)).length_squared(),
0.0,
);
let is_closed = if start_end_coincide_smooth {
let mid = path.evaluate(0.5);
let mid_dist_sq = (mid - path.evaluate(0.0)).length_squared();
if tol.approx_eq(mid_dist_sq, 0.0) {
return Err(crate::OperationsError::InvalidInput {
reason: "sweep path has zero length".into(),
});
}
true
} else {
false
};
let input_wire = topo.wire(input_wire_id)?;
let original_oriented: Vec<_> = input_wire.edges().to_vec();
if original_oriented.is_empty() {
return Err(crate::OperationsError::InvalidInput {
reason: "sweep profile has no edges".into(),
});
}
let input_oriented = crate::extrude::maybe_split_closed_wire(
topo,
&original_oriented,
tol.linear,
crate::extrude::DEFAULT_DEFLECTION,
)?;
let n = input_oriented.len();
let mut input_verts: Vec<VertexId> = Vec::with_capacity(n);
for oe in &input_oriented {
let edge = topo.edge(oe.edge())?;
let vid = oe.oriented_start(edge);
input_verts.push(vid);
}
let mut input_positions: Vec<Point3> = input_verts
.iter()
.map(|&vid| {
topo.vertex(vid)
.map(brepkit_topology::vertex::Vertex::point)
})
.collect::<Result<_, _>>()?;
let mut input_normal = crate::winding::newell_normal(&input_positions)
.normalize()
.unwrap_or(Vec3::new(0.0, 0.0, 1.0));
let path_tangent_0 = path.tangent(0.0)?;
if crate::winding::ensure_ccw_positions(&mut input_positions, path_tangent_0) {
input_normal = -input_normal;
}
let num_segments = (path.control_points().len() * 2).max(4);
let up_hint = orthogonalize(input_normal, path_tangent_0);
let frames = compute_frames(path, num_segments, up_hint, is_closed)?;
let (reference, initial_right, initial_up, initial_tangent) =
match resolve_placement(ProfilePlacement::AsPositioned, input_normal, path_tangent_0) {
ProfilePlacement::AsPositioned => (
frames[0].origin,
frames[0].right,
frames[0].up,
frames[0].tangent,
),
ProfilePlacement::CentroidOnPath => {
let (r, u, t) = profile_basis(input_normal);
(crate::winding::polygon_centroid(&input_positions), r, u, t)
}
};
let num_rings = frames.len();
let ring_positions: Vec<Vec<Point3>> = frames
.iter()
.map(|frame| {
input_positions
.iter()
.map(|&pos| {
transform_point(
pos,
reference,
initial_right,
initial_up,
initial_tangent,
frame,
)
})
.collect()
})
.collect();
if is_closed {
return sweep(topo, profile, path);
}
let first_ring: Vec<VertexId> = ring_positions[0]
.iter()
.map(|&p| topo.add_vertex(Vertex::new(p, tol.linear)))
.collect();
let last_ring: Vec<VertexId> = ring_positions[num_rings - 1]
.iter()
.map(|&p| topo.add_vertex(Vertex::new(p, tol.linear)))
.collect();
let first_ring_edges: Vec<_> = (0..n)
.map(|i| {
let next = (i + 1) % n;
topo.add_edge(Edge::new(first_ring[i], first_ring[next], EdgeCurve::Line))
})
.collect();
let last_ring_edges: Vec<_> = (0..n)
.map(|i| {
let next = (i + 1) % n;
topo.add_edge(Edge::new(last_ring[i], last_ring[next], EdgeCurve::Line))
})
.collect();
let mut inner_swept_smooth: Vec<SweptWireData> = Vec::new();
for &iw_id in &inner_wire_ids_smooth {
inner_swept_smooth.push(sweep_wire_through_frames(
topo,
iw_id,
reference,
initial_right,
initial_up,
initial_tangent,
&frames,
num_segments,
is_closed,
)?);
}
let mut all_faces = Vec::with_capacity(n + 2);
let start_inner_wires_smooth = build_inner_cap_wires(topo, &inner_swept_smooth, 0, true)?;
let start_outward = crate::cap::outward_normal(&ring_positions[0], -frames[0].tangent)?;
all_faces.push(crate::cap::build_cap_face(
topo,
&first_ring_edges,
start_inner_wires_smooth,
&ring_positions[0],
start_outward,
true,
)?);
let degree_u = (num_rings - 1).min(3);
let degree_v = 1;
let rail_edges: Vec<_> = (0..n)
.map(|i| topo.add_edge(Edge::new(first_ring[i], last_ring[i], EdgeCurve::Line)))
.collect();
for i in 0..n {
let next_i = (i + 1) % n;
let grid: Vec<Vec<Point3>> = (0..num_rings)
.map(|k| vec![ring_positions[k][i], ring_positions[k][next_i]])
.collect();
let surface =
interpolate_surface(&grid, degree_u, degree_v).map_err(crate::OperationsError::Math)?;
let mid0 = ring_positions[0][i] + (ring_positions[0][next_i] - ring_positions[0][i]) * 0.5;
let mid1 = ring_positions[1][i] + (ring_positions[1][next_i] - ring_positions[1][i]) * 0.5;
let edge_dir = ring_positions[0][next_i] - ring_positions[0][i];
let mut outward = edge_dir.cross(mid1 - mid0);
if outward.length() < tol.linear {
outward = mid0 - crate::winding::polygon_centroid(&ring_positions[0]);
}
let reversed = surface
.normal(0.0, 0.5)
.map(|nrm| nrm.dot(outward) < 0.0)
.unwrap_or(false);
let side_wire = if reversed {
Wire::new(
vec![
OrientedEdge::new(rail_edges[i], true),
OrientedEdge::new(last_ring_edges[i], true),
OrientedEdge::new(rail_edges[next_i], false),
OrientedEdge::new(first_ring_edges[i], false),
],
true,
)
} else {
Wire::new(
vec![
OrientedEdge::new(first_ring_edges[i], true),
OrientedEdge::new(rail_edges[next_i], true),
OrientedEdge::new(last_ring_edges[i], false),
OrientedEdge::new(rail_edges[i], false),
],
true,
)
}
.map_err(crate::OperationsError::Topology)?;
let side_wire_id = topo.add_wire(side_wire);
let mut face = Face::new(side_wire_id, vec![], FaceSurface::Nurbs(surface));
if reversed {
face.set_reversed(true);
}
all_faces.push(topo.add_face(face));
}
for iwd in &inner_swept_smooth {
let inner_faces = build_inner_side_faces(topo, iwd, num_segments)?;
all_faces.extend(inner_faces);
}
let end_inner_wires_smooth =
build_inner_cap_wires(topo, &inner_swept_smooth, num_segments, false)?;
let end_outward =
crate::cap::outward_normal(&ring_positions[num_rings - 1], frames[num_segments].tangent)?;
all_faces.push(crate::cap::build_cap_face(
topo,
&last_ring_edges,
end_inner_wires_smooth,
&ring_positions[num_rings - 1],
end_outward,
false,
)?);
let shell = Shell::new(all_faces).map_err(crate::OperationsError::Topology)?;
let shell_id = topo.add_shell(shell);
Ok(topo.add_solid(Solid::new(shell_id, vec![])))
}
#[derive(Debug, Clone, Copy, Default)]
pub enum SweepContactMode {
#[default]
RotationMinimizing,
Fixed,
ConstantNormal(Vec3),
}
#[derive(Debug, Clone, Copy, Default)]
pub enum SweepCornerMode {
#[default]
Smooth,
Miter,
Round,
}
#[derive(Default)]
pub struct SweepOptions {
pub contact_mode: SweepContactMode,
pub corner_mode: SweepCornerMode,
pub scale_law: Option<Box<dyn Fn(f64) -> f64 + Send + Sync>>,
pub segments: usize,
pub aux_spine: Option<NurbsCurve>,
}
impl std::fmt::Debug for SweepOptions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SweepOptions")
.field("contact_mode", &self.contact_mode)
.field("corner_mode", &self.corner_mode)
.field(
"scale_law",
&self.scale_law.as_ref().map(|_| "fn(f64)->f64"),
)
.field("segments", &self.segments)
.field("aux_spine", &self.aux_spine.as_ref().map(|_| "NurbsCurve"))
.finish()
}
}
#[allow(clippy::too_many_lines)]
pub fn sweep_with_options(
topo: &mut Topology,
profile: FaceId,
path: &NurbsCurve,
options: &SweepOptions,
) -> Result<SolidId, crate::OperationsError> {
let tol = Tolerance::new();
if path.control_points().len() < 2 {
return Err(crate::OperationsError::InvalidInput {
reason: "sweep path must have at least 2 control points".into(),
});
}
if matches!(options.corner_mode, SweepCornerMode::Miter) && !detect_kinks(path).is_empty() {
return sweep_miter(topo, profile, path, options);
}
let face_data = topo.face(profile)?;
let input_wire_id = face_data.outer_wire();
let inner_wire_ids_opts: Vec<brepkit_topology::wire::WireId> = face_data.inner_wires().to_vec();
let start_end_coincide_opts = tol.approx_eq(
(path.evaluate(1.0) - path.evaluate(0.0)).length_squared(),
0.0,
);
if start_end_coincide_opts {
let mid = path.evaluate(0.5);
let mid_dist_sq = (mid - path.evaluate(0.0)).length_squared();
if tol.approx_eq(mid_dist_sq, 0.0) {
return Err(crate::OperationsError::InvalidInput {
reason: "sweep path has zero length (start and end coincide)".into(),
});
}
return sweep(topo, profile, path);
}
if options.scale_law.is_none()
&& options.aux_spine.is_none()
&& let Some(solid) =
try_straight_extrude(topo, profile, path, ProfilePlacement::AsPositioned)?
{
return Ok(solid);
}
let input_wire = topo.wire(input_wire_id)?;
let original_oriented: Vec<_> = input_wire.edges().to_vec();
if original_oriented.is_empty() {
return Err(crate::OperationsError::InvalidInput {
reason: "sweep profile has no edges".into(),
});
}
let input_oriented = crate::extrude::maybe_split_closed_wire(
topo,
&original_oriented,
tol.linear,
crate::extrude::DEFAULT_DEFLECTION,
)?;
let n = input_oriented.len();
let mut input_verts: Vec<VertexId> = Vec::with_capacity(n);
for oe in &input_oriented {
let edge = topo.edge(oe.edge())?;
let vid = oe.oriented_start(edge);
input_verts.push(vid);
}
let mut input_positions: Vec<Point3> = input_verts
.iter()
.map(|&vid| {
topo.vertex(vid)
.map(brepkit_topology::vertex::Vertex::point)
})
.collect::<Result<_, _>>()?;
let mut input_normal = crate::winding::newell_normal(&input_positions)
.normalize()
.unwrap_or(Vec3::new(0.0, 0.0, 1.0));
let path_tangent_0 = path.tangent(0.0)?;
if crate::winding::ensure_ccw_positions(&mut input_positions, path_tangent_0) {
input_normal = -input_normal;
}
let num_segments = if options.segments > 0 {
options.segments
} else {
(path.control_points().len() * 2).max(4)
};
let frames: Vec<Frame> = if let Some(aux) = options.aux_spine.as_ref() {
let mut out = Vec::with_capacity(num_segments + 1);
let mut prev_up: Option<Vec3> = None;
for k in 0..=num_segments {
#[allow(clippy::cast_precision_loss)]
let t = k as f64 / num_segments as f64;
let origin = path.evaluate(t);
let tangent = path.tangent(t).unwrap_or(path_tangent_0);
let guide_dir = aux.evaluate(t) - origin;
let up = if guide_dir.length() < 1e-9 {
let seed = prev_up.unwrap_or_else(|| pick_reference_axis(tangent));
orthogonalize(seed, tangent)
} else {
orthogonalize(guide_dir, tangent)
};
prev_up = Some(up);
out.push(Frame {
origin,
tangent,
up,
right: tangent.cross(up),
});
}
out
} else {
match options.contact_mode {
SweepContactMode::RotationMinimizing => {
let up_hint = orthogonalize(input_normal, path_tangent_0);
compute_frames(path, num_segments, up_hint, false)?
}
SweepContactMode::Fixed => {
let tangent0 = path_tangent_0;
let up = orthogonalize(input_normal, tangent0);
let right = tangent0.cross(up);
(0..=num_segments)
.map(|k| {
#[allow(clippy::cast_precision_loss)]
let t = k as f64 / num_segments as f64;
Frame {
origin: path.evaluate(t),
tangent: path.tangent(t).unwrap_or(tangent0),
up,
right,
}
})
.collect()
}
SweepContactMode::ConstantNormal(normal_dir) => {
(0..=num_segments)
.map(|k| {
#[allow(clippy::cast_precision_loss)]
let t = k as f64 / num_segments as f64;
let tangent = path.tangent(t).unwrap_or(Vec3::new(0.0, 0.0, 1.0));
let up = orthogonalize(normal_dir, tangent);
let right = tangent.cross(up);
Frame {
origin: path.evaluate(t),
tangent,
up,
right,
}
})
.collect()
}
}
};
let (reference, initial_right, initial_up, initial_tangent) =
match resolve_placement(ProfilePlacement::AsPositioned, input_normal, path_tangent_0) {
ProfilePlacement::AsPositioned => (
frames[0].origin,
frames[0].right,
frames[0].up,
frames[0].tangent,
),
ProfilePlacement::CentroidOnPath => {
let (r, u, t) = profile_basis(input_normal);
(crate::winding::polygon_centroid(&input_positions), r, u, t)
}
};
let mut ring_verts: Vec<Vec<VertexId>> = Vec::with_capacity(num_segments + 1);
for (k, frame) in frames.iter().enumerate() {
#[allow(clippy::cast_precision_loss)]
let t = k as f64 / num_segments as f64;
let scale = options.scale_law.as_ref().map_or(1.0, |law| law(t));
let ring: Vec<VertexId> = input_positions
.iter()
.map(|&pos| {
let mut transformed = transform_point(
pos,
reference,
initial_right,
initial_up,
initial_tangent,
frame,
);
if (scale - 1.0).abs() > tol.linear {
let offset = transformed - frame.origin;
transformed = frame.origin
+ Vec3::new(offset.x() * scale, offset.y() * scale, offset.z() * scale);
}
topo.add_vertex(Vertex::new(transformed, tol.linear))
})
.collect();
ring_verts.push(ring);
}
let mut ring_edges: Vec<Vec<brepkit_topology::edge::EdgeId>> =
Vec::with_capacity(num_segments + 1);
for ring in &ring_verts {
let edges: Vec<_> = (0..n)
.map(|i| {
let next = (i + 1) % n;
topo.add_edge(Edge::new(ring[i], ring[next], EdgeCurve::Line))
})
.collect();
ring_edges.push(edges);
}
let mut path_edges: Vec<Vec<brepkit_topology::edge::EdgeId>> = Vec::with_capacity(num_segments);
for seg in 0..num_segments {
let edges: Vec<_> = (0..n)
.map(|i| {
topo.add_edge(Edge::new(
ring_verts[seg][i],
ring_verts[seg + 1][i],
EdgeCurve::Line,
))
})
.collect();
path_edges.push(edges);
}
let mut inner_swept_opts: Vec<SweptWireData> = Vec::new();
for &iw_id in &inner_wire_ids_opts {
inner_swept_opts.push(sweep_wire_through_frames(
topo,
iw_id,
reference,
initial_right,
initial_up,
initial_tangent,
&frames,
num_segments,
false,
)?);
}
let mut all_faces = Vec::with_capacity(num_segments * n + 2);
let start_inner_wires_opts = build_inner_cap_wires(topo, &inner_swept_opts, 0, true)?;
let start_verts = crate::cap::ring_point_positions(topo, &ring_verts[0])?;
let start_outward = crate::cap::outward_normal(&start_verts, -frames[0].tangent)?;
all_faces.push(crate::cap::build_cap_face(
topo,
&ring_edges[0],
start_inner_wires_opts,
&start_verts,
start_outward,
true,
)?);
for seg in 0..num_segments {
for i in 0..n {
let next_i = (i + 1) % n;
let p0 = topo.vertex(ring_verts[seg][i])?.point();
let p1 = topo.vertex(ring_verts[seg][next_i])?.point();
let p_next = topo.vertex(ring_verts[seg + 1][i])?.point();
let edge_dir = p1 - p0;
let path_dir = p_next - p0;
let side_normal = edge_dir
.cross(path_dir)
.normalize()
.unwrap_or(Vec3::new(1.0, 0.0, 0.0));
let side_d = dot_normal_point(side_normal, p0);
let side_wire = Wire::new(
vec![
OrientedEdge::new(ring_edges[seg][i], true),
OrientedEdge::new(path_edges[seg][next_i], true),
OrientedEdge::new(ring_edges[seg + 1][i], false),
OrientedEdge::new(path_edges[seg][i], false),
],
true,
)
.map_err(crate::OperationsError::Topology)?;
let side_wire_id = topo.add_wire(side_wire);
let side_face = topo.add_face(Face::new(
side_wire_id,
vec![],
FaceSurface::Plane {
normal: side_normal,
d: side_d,
},
));
all_faces.push(side_face);
}
}
for iwd in &inner_swept_opts {
let inner_faces = build_inner_side_faces(topo, iwd, num_segments)?;
all_faces.extend(inner_faces);
}
let end_inner_wires_opts = build_inner_cap_wires(topo, &inner_swept_opts, num_segments, false)?;
let end_verts = crate::cap::ring_point_positions(topo, &ring_verts[num_segments])?;
let end_outward = crate::cap::outward_normal(&end_verts, frames[num_segments].tangent)?;
all_faces.push(crate::cap::build_cap_face(
topo,
&ring_edges[num_segments],
end_inner_wires_opts,
&end_verts,
end_outward,
false,
)?);
let shell = Shell::new(all_faces).map_err(crate::OperationsError::Topology)?;
let shell_id = topo.add_shell(shell);
Ok(topo.add_solid(Solid::new(shell_id, vec![])))
}
fn detect_kinks(path: &NurbsCurve) -> Vec<f64> {
const KNOT_EPS: f64 = 1e-10;
const KINK_ANGLE_RAD: f64 = 0.0175;
let p = path.degree();
let knots = path.knots();
let (u_min, u_max) = path.domain();
let mut kinks = Vec::new();
let mut i = 0;
while i < knots.len() {
let u = knots[i];
if u <= u_min + KNOT_EPS || u >= u_max - KNOT_EPS {
i += 1;
continue;
}
let mut mult = 1;
while i + mult < knots.len() && (knots[i + mult] - u).abs() < KNOT_EPS {
mult += 1;
}
if mult >= p {
let eps = 1e-8;
if let (Ok(t_before), Ok(t_after)) = (path.tangent(u - eps), path.tangent(u + eps)) {
let dot = t_before.dot(t_after).clamp(-1.0, 1.0);
let angle = dot.acos();
if angle > KINK_ANGLE_RAD {
kinks.push(u);
}
}
}
i += mult;
}
kinks
}
#[allow(clippy::too_many_lines)]
fn sweep_miter(
topo: &mut Topology,
profile: FaceId,
path: &NurbsCurve,
options: &SweepOptions,
) -> Result<SolidId, crate::OperationsError> {
use brepkit_math::nurbs::knot_ops::curve_split;
let tol = Tolerance::new();
let kinks = detect_kinks(path);
debug_assert!(
!kinks.is_empty(),
"sweep_miter should only be called when the path has kinks"
);
let face_data = topo.face(profile)?;
let mut input_normal = match face_data.surface() {
FaceSurface::Plane { normal, .. } => *normal,
_ => {
return Err(crate::OperationsError::InvalidInput {
reason: "sweep of non-planar faces is not supported".into(),
});
}
};
let input_wire_id = face_data.outer_wire();
let inner_wire_ids: Vec<brepkit_topology::wire::WireId> = face_data.inner_wires().to_vec();
let input_wire = topo.wire(input_wire_id)?;
let original_oriented: Vec<_> = input_wire.edges().to_vec();
if original_oriented.is_empty() {
return Err(crate::OperationsError::InvalidInput {
reason: "sweep profile has no edges".into(),
});
}
let input_oriented = crate::extrude::maybe_split_closed_wire(
topo,
&original_oriented,
tol.linear,
crate::extrude::DEFAULT_DEFLECTION,
)?;
let n = input_oriented.len();
let mut input_verts: Vec<VertexId> = Vec::with_capacity(n);
for oe in &input_oriented {
let edge = topo.edge(oe.edge())?;
let vid = oe.oriented_start(edge);
input_verts.push(vid);
}
let mut input_positions: Vec<Point3> = input_verts
.iter()
.map(|&vid| {
topo.vertex(vid)
.map(brepkit_topology::vertex::Vertex::point)
})
.collect::<Result<_, _>>()?;
let (domain_start, _domain_end) = path.domain();
let path_tangent_0 = path.tangent(domain_start)?;
if crate::winding::ensure_ccw_positions(&mut input_positions, path_tangent_0) {
input_normal = -input_normal;
}
let placement = resolve_placement(ProfilePlacement::AsPositioned, input_normal, path_tangent_0);
let reference = match placement {
ProfilePlacement::AsPositioned => path.evaluate(domain_start),
ProfilePlacement::CentroidOnPath => crate::winding::polygon_centroid(&input_positions),
};
let mut global_basis: Option<(Vec3, Vec3, Vec3)> = None;
let mut sub_paths: Vec<NurbsCurve> = Vec::with_capacity(kinks.len() + 1);
let mut remaining = path.clone();
let mut offset = domain_start;
for &kink_u in &kinks {
let split_u = kink_u - offset + remaining.domain().0;
let (left, right) = curve_split(&remaining, split_u)?;
sub_paths.push(left);
offset = kink_u;
remaining = right;
}
sub_paths.push(remaining);
let mut all_faces: Vec<FaceId> = Vec::new();
let mut prev_end_ring: Option<Vec<VertexId>> = None;
let mut prev_end_ring_edges: Option<Vec<brepkit_topology::edge::EdgeId>> = None;
let mut prev_end_on_bisector = false;
let mut prev_up: Option<(Vec3, Vec3)> = None;
for (seg_idx, sub_path) in sub_paths.iter().enumerate() {
let is_first = seg_idx == 0;
let is_last = seg_idx == sub_paths.len() - 1;
let sub_tangent_0 = sub_path.tangent(sub_path.domain().0)?;
let up_hint = match prev_up {
None => orthogonalize(input_normal, sub_tangent_0),
Some((up_prev, t_prev)) => {
let cross = t_prev.cross(sub_tangent_0);
let transported = match cross.normalize() {
Ok(axis) => {
let angle = t_prev.dot(sub_tangent_0).clamp(-1.0, 1.0).acos();
crate::revolve::rotate_vec(up_prev, axis, angle)
}
Err(_) => up_prev, };
orthogonalize(transported, sub_tangent_0)
}
};
let num_segments = if options.segments > 0 {
options.segments
} else {
(sub_path.control_points().len() * 2).max(4)
};
let sub_frames = match options.contact_mode {
SweepContactMode::RotationMinimizing => {
compute_frames(sub_path, num_segments, up_hint, false)?
}
SweepContactMode::Fixed => {
let up = orthogonalize(input_normal, sub_tangent_0);
let right = sub_tangent_0.cross(up);
(0..=num_segments)
.map(|k| {
let (u0, u1) = sub_path.domain();
#[allow(clippy::cast_precision_loss)]
let t = u0 + (u1 - u0) * (k as f64 / num_segments as f64);
Frame {
origin: sub_path.evaluate(t),
tangent: sub_path.tangent(t).unwrap_or(sub_tangent_0),
up,
right,
}
})
.collect()
}
SweepContactMode::ConstantNormal(normal_dir) => (0..=num_segments)
.map(|k| {
let (u0, u1) = sub_path.domain();
#[allow(clippy::cast_precision_loss)]
let t = u0 + (u1 - u0) * (k as f64 / num_segments as f64);
let tangent = sub_path.tangent(t).unwrap_or(Vec3::new(0.0, 0.0, 1.0));
let up = orthogonalize(normal_dir, tangent);
let right = tangent.cross(up);
Frame {
origin: sub_path.evaluate(t),
tangent,
up,
right,
}
})
.collect(),
};
if global_basis.is_none() {
global_basis = Some((sub_frames[0].right, sub_frames[0].up, sub_frames[0].tangent));
}
if let Some(last) = sub_frames.last() {
prev_up = Some((last.up, last.tangent));
}
let (initial_right, initial_up, initial_tangent) = match (placement, global_basis) {
(ProfilePlacement::AsPositioned, Some((r, u, t))) => (r, u, t),
_ => (sub_frames[0].right, sub_frames[0].up, sub_frames[0].tangent),
};
let mut ring_positions: Vec<Vec<Point3>> = sub_frames
.iter()
.map(|frame| {
input_positions
.iter()
.map(|&pos| {
transform_point(
pos,
reference,
initial_right,
initial_up,
initial_tangent,
frame,
)
})
.collect()
})
.collect();
let mut exit_on_bisector = false;
if !is_last && matches!(placement, ProfilePlacement::AsPositioned) {
let kink_u = kinks[seg_idx];
let eps = 1e-8;
let t_b = path.tangent(kink_u - eps)?;
let t_a = path.tangent(kink_u + eps)?;
if let Ok(bisector) = (t_b + t_a).normalize() {
let kink_point = path.evaluate(kink_u);
let t_end = sub_frames[num_segments].tangent;
let denom = bisector.dot(t_end);
let spacing = (sub_frames[num_segments].origin
- sub_frames[num_segments - 1].origin)
.length();
if denom.abs() > 0.1 {
let slid: Vec<Point3> = ring_positions[num_segments]
.iter()
.map(|&p| p + t_end * (bisector.dot(kink_point - p) / denom))
.collect();
let max_slide = ring_positions[num_segments]
.iter()
.zip(&slid)
.map(|(&p, &q)| (q - p).length())
.fold(0.0_f64, f64::max);
if max_slide < spacing * 0.95 {
ring_positions[num_segments] = slid;
exit_on_bisector = true;
}
}
}
}
let entry_shared = match (&prev_end_ring, prev_end_on_bisector) {
(Some(prev_ring), true) => {
let t_start = sub_frames[0].tangent;
let mut ahead = true;
for (i, &vid) in prev_ring.iter().enumerate() {
let p_prev = topo.vertex(vid)?.point();
if (ring_positions[1][i] - p_prev).dot(t_start) <= tol.linear {
ahead = false;
break;
}
}
ahead
}
_ => false,
};
let mut ring_verts: Vec<Vec<VertexId>> = Vec::with_capacity(num_segments + 1);
for (ring_idx, positions) in ring_positions.iter().enumerate() {
if let (0, true, Some(prev_ring)) = (ring_idx, entry_shared, prev_end_ring.as_ref()) {
ring_verts.push(prev_ring.clone());
continue;
}
let ring: Vec<VertexId> = positions
.iter()
.map(|&p| topo.add_vertex(Vertex::new(p, tol.linear)))
.collect();
ring_verts.push(ring);
}
#[allow(clippy::useless_let_if_seq)]
let mut miter_ring_edges_for_reuse: Option<Vec<brepkit_topology::edge::EdgeId>> = None;
if entry_shared {
miter_ring_edges_for_reuse.clone_from(&prev_end_ring_edges);
} else if let Some(ref prev_ring) = prev_end_ring {
let kink_idx = seg_idx - 1;
let kink_u = kinks[kink_idx];
let eps = 1e-8;
let t_before = path.tangent(kink_u - eps)?;
let t_after = path.tangent(kink_u + eps)?;
let bisector = (t_before + t_after).normalize().unwrap_or(t_before);
let kink_point = path.evaluate(kink_u);
let miter_ring: Vec<VertexId> = (0..n)
.map(|i| {
let prev_pos = topo
.vertex(prev_ring[i])
.map(brepkit_topology::vertex::Vertex::point)
.unwrap_or(kink_point);
let curr_pos = topo
.vertex(ring_verts[0][i])
.map(brepkit_topology::vertex::Vertex::point)
.unwrap_or(kink_point);
let ray_dir = curr_pos - prev_pos;
let denom = bisector.dot(ray_dir);
let miter_pos = if denom.abs() > tol.linear {
let d = bisector.dot(Vec3::new(
kink_point.x() - prev_pos.x(),
kink_point.y() - prev_pos.y(),
kink_point.z() - prev_pos.z(),
));
let t_intersect = d / denom;
prev_pos + ray_dir * t_intersect
} else {
Point3::new(
(prev_pos.x() + curr_pos.x()) * 0.5,
(prev_pos.y() + curr_pos.y()) * 0.5,
(prev_pos.z() + curr_pos.z()) * 0.5,
)
};
topo.add_vertex(Vertex::new(miter_pos, tol.linear))
})
.collect();
let miter_ring_edges: Vec<brepkit_topology::edge::EdgeId> = (0..n)
.map(|i| {
let next = (i + 1) % n;
topo.add_edge(Edge::new(miter_ring[i], miter_ring[next], EdgeCurve::Line))
})
.collect();
let prev_ring_edges = prev_end_ring_edges.as_ref().ok_or_else(|| {
crate::OperationsError::InvalidInput {
reason: "internal error: missing previous ring edges".into(),
}
})?;
let prev_to_miter_path_edges: Vec<brepkit_topology::edge::EdgeId> = (0..n)
.map(|i| topo.add_edge(Edge::new(prev_ring[i], miter_ring[i], EdgeCurve::Line)))
.collect();
for i in 0..n {
let next_i = (i + 1) % n;
let p0 = topo.vertex(prev_ring[i])?.point();
let p1 = topo.vertex(prev_ring[next_i])?.point();
let p_next = topo.vertex(miter_ring[i])?.point();
let edge_dir = p1 - p0;
let path_dir = p_next - p0;
let side_normal = edge_dir
.cross(path_dir)
.normalize()
.unwrap_or(Vec3::new(1.0, 0.0, 0.0));
let side_d = dot_normal_point(side_normal, p0);
let side_wire = Wire::new(
vec![
OrientedEdge::new(prev_ring_edges[i], true),
OrientedEdge::new(prev_to_miter_path_edges[next_i], true),
OrientedEdge::new(miter_ring_edges[i], false),
OrientedEdge::new(prev_to_miter_path_edges[i], false),
],
true,
)
.map_err(crate::OperationsError::Topology)?;
let side_wire_id = topo.add_wire(side_wire);
all_faces.push(topo.add_face(Face::new(
side_wire_id,
vec![],
FaceSurface::Plane {
normal: side_normal,
d: side_d,
},
)));
}
ring_verts[0] = miter_ring;
miter_ring_edges_for_reuse = Some(miter_ring_edges);
}
let mut ring_edges: Vec<Vec<brepkit_topology::edge::EdgeId>> =
Vec::with_capacity(num_segments + 1);
for (ring_idx, ring) in ring_verts.iter().enumerate() {
if ring_idx == 0 {
if let Some(ref reused) = miter_ring_edges_for_reuse {
ring_edges.push(reused.clone());
} else {
let edges: Vec<_> = (0..n)
.map(|i| {
let next = (i + 1) % n;
topo.add_edge(Edge::new(ring[i], ring[next], EdgeCurve::Line))
})
.collect();
ring_edges.push(edges);
}
} else {
let edges: Vec<_> = (0..n)
.map(|i| {
let next = (i + 1) % n;
topo.add_edge(Edge::new(ring[i], ring[next], EdgeCurve::Line))
})
.collect();
ring_edges.push(edges);
}
}
let mut path_edges: Vec<Vec<brepkit_topology::edge::EdgeId>> =
Vec::with_capacity(num_segments);
for seg in 0..num_segments {
let edges: Vec<_> = (0..n)
.map(|i| {
topo.add_edge(Edge::new(
ring_verts[seg][i],
ring_verts[seg + 1][i],
EdgeCurve::Line,
))
})
.collect();
path_edges.push(edges);
}
let mut inner_swept: Vec<SweptWireData> = Vec::new();
for &iw_id in &inner_wire_ids {
inner_swept.push(sweep_wire_through_frames(
topo,
iw_id,
reference,
initial_right,
initial_up,
initial_tangent,
&sub_frames,
num_segments,
false,
)?);
}
if is_first {
let start_reversed_edges: Vec<OrientedEdge> = (0..n)
.rev()
.map(|i| OrientedEdge::new(ring_edges[0][i], false))
.collect();
let start_wire =
Wire::new(start_reversed_edges, true).map_err(crate::OperationsError::Topology)?;
let start_wire_id = topo.add_wire(start_wire);
let start_inner_wires = build_inner_cap_wires(topo, &inner_swept, 0, true)?;
let start_normal = -sub_frames[0].tangent;
let start_d = dot_normal_point(start_normal, topo.vertex(ring_verts[0][0])?.point());
all_faces.push(topo.add_face(Face::new(
start_wire_id,
start_inner_wires,
FaceSurface::Plane {
normal: start_normal,
d: start_d,
},
)));
}
for seg in 0..num_segments {
for i in 0..n {
let next_i = (i + 1) % n;
let p0 = topo.vertex(ring_verts[seg][i])?.point();
let p1 = topo.vertex(ring_verts[seg][next_i])?.point();
let p_next = topo.vertex(ring_verts[seg + 1][i])?.point();
let edge_dir = p1 - p0;
let path_dir = p_next - p0;
let side_normal = edge_dir
.cross(path_dir)
.normalize()
.unwrap_or(Vec3::new(1.0, 0.0, 0.0));
let side_d = dot_normal_point(side_normal, p0);
let side_wire = Wire::new(
vec![
OrientedEdge::new(ring_edges[seg][i], true),
OrientedEdge::new(path_edges[seg][next_i], true),
OrientedEdge::new(ring_edges[seg + 1][i], false),
OrientedEdge::new(path_edges[seg][i], false),
],
true,
)
.map_err(crate::OperationsError::Topology)?;
let side_wire_id = topo.add_wire(side_wire);
all_faces.push(topo.add_face(Face::new(
side_wire_id,
vec![],
FaceSurface::Plane {
normal: side_normal,
d: side_d,
},
)));
}
}
for iwd in &inner_swept {
let inner_faces = build_inner_side_faces(topo, iwd, num_segments)?;
all_faces.extend(inner_faces);
}
if is_last {
let end_edges: Vec<OrientedEdge> = (0..n)
.map(|i| OrientedEdge::new(ring_edges[num_segments][i], true))
.collect();
let end_wire = Wire::new(end_edges, true).map_err(crate::OperationsError::Topology)?;
let end_wire_id = topo.add_wire(end_wire);
let end_inner_wires = build_inner_cap_wires(topo, &inner_swept, num_segments, false)?;
let end_normal = sub_frames[num_segments].tangent;
let end_d = dot_normal_point(
end_normal,
topo.vertex(ring_verts[num_segments][0])?.point(),
);
all_faces.push(topo.add_face(Face::new(
end_wire_id,
end_inner_wires,
FaceSurface::Plane {
normal: end_normal,
d: end_d,
},
)));
}
prev_end_ring = Some(ring_verts[num_segments].clone());
prev_end_ring_edges = Some(ring_edges[num_segments].clone());
prev_end_on_bisector = exit_on_bisector;
}
let shell = Shell::new(all_faces).map_err(crate::OperationsError::Topology)?;
let shell_id = topo.add_shell(shell);
Ok(topo.add_solid(Solid::new(shell_id, vec![])))
}
pub fn multi_section_sweep(
topo: &mut Topology,
spine: &NurbsCurve,
sections: &[(FaceId, f64)],
ruled: bool,
) -> Result<SolidId, crate::OperationsError> {
if sections.len() < 2 {
return Err(crate::OperationsError::InvalidInput {
reason: format!(
"multi-section sweep requires at least 2 sections, got {}",
sections.len()
),
});
}
if spine.control_points().len() < 2 {
return Err(crate::OperationsError::InvalidInput {
reason: "multi-section sweep spine must have at least 2 control points".into(),
});
}
for &(_, p) in sections {
if !(0.0..=1.0).contains(&p) {
return Err(crate::OperationsError::InvalidInput {
reason: format!("section parameter {p} is outside [0, 1]"),
});
}
}
let dense_segments: usize = 64;
let t_start = spine.tangent(0.0)?;
let initial_up = orthogonalize(pick_reference_axis(t_start), t_start);
let dense = compute_frames(spine, dense_segments, initial_up, false)?;
let mut ordered: Vec<(FaceId, f64)> = sections.to_vec();
ordered.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
let mut placed: Vec<FaceId> = Vec::with_capacity(ordered.len());
for (face_id, p) in ordered {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let idx = ((p * dense_segments as f64).round() as usize).min(dense_segments);
let tangent = spine.tangent(p)?;
let up = orthogonalize(dense[idx].up, tangent);
let frame = Frame {
origin: spine.evaluate(p),
tangent,
up,
right: tangent.cross(up),
};
let mat = profile_to_frame_matrix(topo, face_id, &frame)?;
let placed_face = crate::copy::copy_face(topo, face_id)?;
crate::transform::transform_face(topo, placed_face, &mat)?;
placed.push(placed_face);
}
if ruled {
crate::loft::loft(topo, &placed)
} else {
crate::loft::loft_smooth(topo, &placed)
}
}
fn pick_reference_axis(dir: Vec3) -> Vec3 {
if dir.x().abs() < 0.9 {
Vec3::new(1.0, 0.0, 0.0)
} else {
Vec3::new(0.0, 1.0, 0.0)
}
}
fn profile_to_frame_matrix(
topo: &Topology,
face_id: FaceId,
frame: &Frame,
) -> Result<Mat4, crate::OperationsError> {
let boundary = crate::boolean::face_polygon(topo, face_id)?;
if boundary.is_empty() {
return Err(crate::OperationsError::InvalidInput {
reason: "multi-section sweep profile has no boundary".into(),
});
}
let normal = match topo.face(face_id)?.surface() {
FaceSurface::Plane { normal, .. } => normal.normalize().unwrap_or(*normal),
_ => crate::winding::newell_normal(&boundary)
.normalize()
.unwrap_or(Vec3::new(0.0, 0.0, 1.0)),
};
let centroid = crate::winding::polygon_centroid(&boundary);
let p_x = orthogonalize(pick_reference_axis(normal), normal);
let p_y = p_x.cross(normal);
let t_cols = [
[frame.right.x(), frame.up.x(), frame.tangent.x()],
[frame.right.y(), frame.up.y(), frame.tangent.y()],
[frame.right.z(), frame.up.z(), frame.tangent.z()],
];
let l_cols = [
[p_x.x(), p_y.x(), normal.x()],
[p_x.y(), p_y.y(), normal.y()],
[p_x.z(), p_y.z(), normal.z()],
];
let mut rot = [[0.0_f64; 3]; 3];
for i in 0..3 {
for j in 0..3 {
rot[i][j] = (0..3).map(|k| t_cols[i][k] * l_cols[j][k]).sum();
}
}
let c = [centroid.x(), centroid.y(), centroid.z()];
let o = [frame.origin.x(), frame.origin.y(), frame.origin.z()];
let trans: [f64; 3] =
std::array::from_fn(|i| o[i] - (0..3).map(|k| rot[i][k] * c[k]).sum::<f64>());
Ok(Mat4([
[rot[0][0], rot[0][1], rot[0][2], trans[0]],
[rot[1][0], rot[1][1], rot[1][2], trans[1]],
[rot[2][0], rot[2][1], rot[2][2], trans[2]],
[0.0, 0.0, 0.0, 1.0],
]))
}
pub fn sweep_guided(
topo: &mut Topology,
profile: FaceId,
spine: &NurbsCurve,
aux: NurbsCurve,
) -> Result<SolidId, crate::OperationsError> {
sweep_with_options(
topo,
profile,
spine,
&SweepOptions {
aux_spine: Some(aux),
..Default::default()
},
)
}
mod spine;
pub fn sweep_along_edges(
topo: &mut Topology,
profile: FaceId,
edges: &[brepkit_topology::edge::EdgeId],
) -> Result<SolidId, crate::OperationsError> {
if edges.is_empty() {
return Err(crate::OperationsError::InvalidInput {
reason: "sweep_along_edges requires at least one edge".into(),
});
}
if let Some(solid) = spine::try_analytic_spine_sweep(topo, profile, edges)? {
return Ok(solid);
}
let tol = Tolerance::new();
let mut points: Vec<Point3> = Vec::new();
for &eid in edges {
let edge_data = topo.edge(eid)?;
let start = topo.vertex(edge_data.start())?.point();
if points
.last()
.is_none_or(|p: &Point3| (*p - start).length() > tol.linear)
{
points.push(start);
}
match edge_data.curve() {
EdgeCurve::NurbsCurve(curve) => {
let (u0, u1) = curve.domain();
let n_samples = 4;
for i in 1..n_samples {
#[allow(clippy::cast_precision_loss)]
let frac = i as f64 / n_samples as f64;
points.push(curve.evaluate(u0 + frac * (u1 - u0)));
}
}
EdgeCurve::Circle(circle) => {
let t_start = circle.project(start);
let end_pt = topo.vertex(edge_data.end())?.point();
let mut t_end = circle.project(end_pt);
if t_end <= t_start {
t_end += std::f64::consts::TAU;
}
let n_samples = 8;
for i in 1..n_samples {
#[allow(clippy::cast_precision_loss)]
let t = t_start + (t_end - t_start) * (i as f64) / (n_samples as f64);
points.push(circle.evaluate(t));
}
}
EdgeCurve::Ellipse(ellipse) => {
let t_start = ellipse.project(start);
let end_pt = topo.vertex(edge_data.end())?.point();
let mut t_end = ellipse.project(end_pt);
if t_end <= t_start {
t_end += std::f64::consts::TAU;
}
let n_samples = 8;
for i in 1..n_samples {
#[allow(clippy::cast_precision_loss)]
let t = t_start + (t_end - t_start) * (i as f64) / (n_samples as f64);
points.push(ellipse.evaluate(t));
}
}
EdgeCurve::Line => {}
}
let end = topo.vertex(edge_data.end())?.point();
points.push(end);
}
if points.len() < 2 {
return Err(crate::OperationsError::InvalidInput {
reason: "sweep_along_edges: need at least 2 distinct points".into(),
});
}
let points = densify_path_points(&points);
let degree = std::cmp::min(3, points.len() - 1);
let path_curve = brepkit_math::nurbs::fitting::interpolate(&points, degree)?;
sweep(topo, profile, &path_curve)
}
#[cfg(test)]
mod tests;