use super::*;
use crate::{KernelRefusal, KernelStage, OrRefuse};
use super::face_move::{
cached_plane, carrier_is_planar, carrier_kind_name, carrier_ruled,
corner_on_plane_and_ruled, plane_and_ruled_carriers, resolve_corner_on_fixed_edge,
retrim_ruled_face, solve_corner, verify_section_between,
};
enum EdgeClass {
Fixed,
Interior,
Boundary { moved_face: u64, fixed_face: u64 },
}
enum EdgeAction {
Rotate,
Rebuild { start: Vec3, end: Vec3 },
Replace { curve: NurbsCurve },
}
enum FaceAction {
RotateSurface,
Retrim(Plane),
}
fn unsupported_carrier(
solid: &BrepSolid,
face_lookup: &HashMap<u64, (usize, usize)>,
face_id: u64,
role: &str,
) -> KernelRefusal {
KernelRefusal::unsupported(KernelStage::Classify, "carrier_kind", format!(
"rotate_faces: {role} is a {}; a rotation re-intersects a PLANE with a plane, a \
cylinder or a cone — whichever of the two it turns — and carries any other curved \
carrier only when the axis leaves it invariant — refusing",
face_label(solid, face_lookup, face_id)
))
}
fn face_label(
solid: &BrepSolid,
face_lookup: &HashMap<u64, (usize, usize)>,
face_id: u64,
) -> String {
match face_lookup.get(&face_id) {
Some(&(shell, face)) => {
let record = &solid.shells[shell].faces[face];
let kind = carrier_kind_name(&record.surface);
match record.name.as_deref() {
Some(name) => format!("`{name}` (face {face_id}) {kind}"),
None => format!("face {face_id} {kind}"),
}
}
None => format!("face {face_id} (missing)"),
}
}
fn face_short(
solid: &BrepSolid,
face_lookup: &HashMap<u64, (usize, usize)>,
face_id: u64,
) -> String {
match face_lookup.get(&face_id) {
Some(&(shell, face)) => match solid.shells[shell].faces[face].name.as_deref() {
Some(name) => format!("`{name}` (face {face_id})"),
None => format!("face {face_id}"),
},
None => format!("face {face_id}"),
}
}
pub(super) fn rotation_about(point: Vec3, direction: Vec3, angle: f64) -> Result<AffineTransform, String> {
let (s, c) = angle.sin_cos();
let t = 1.0 - c;
let (x, y, z) = (direction.x, direction.y, direction.z);
let r = [
t * x * x + c,
t * x * y - s * z,
t * x * z + s * y,
t * x * y + s * z,
t * y * y + c,
t * y * z - s * x,
t * x * z - s * y,
t * y * z + s * x,
t * z * z + c,
];
let a = [point.x, point.y, point.z];
let mut elements = [0.0f64; 16];
for row in 0..3 {
for col in 0..3 {
elements[row * 4 + col] = r[row * 3 + col];
}
elements[row * 4 + 3] = a[row] - (0..3).map(|k| r[row * 3 + k] * a[k]).sum::<f64>();
}
elements[15] = 1.0;
AffineTransform::new(elements)
}
pub(super) fn rotate_direction(rotation: &AffineTransform, vector: Vec3) -> Vec3 {
let m = rotation.elements;
Vec3::new(
m[0] * vector.x + m[1] * vector.y + m[2] * vector.z,
m[4] * vector.x + m[5] * vector.y + m[6] * vector.z,
m[8] * vector.x + m[9] * vector.y + m[10] * vector.z,
)
}
fn rotate_plane(rotation: &AffineTransform, plane: &Plane) -> Plane {
Plane {
origin: rotation.point(plane.origin),
u_dir: rotate_direction(rotation, plane.u_dir),
v_dir: rotate_direction(rotation, plane.v_dir),
normal: rotate_direction(rotation, plane.normal),
}
}
fn loop_area_vector(
loop_record: &LoopRecord,
edges: &HashMap<u64, EdgeRecord>,
op: &str,
) -> Result<Vec3, KernelRefusal> {
let mut points: Vec<Vec3> = Vec::new();
for coedge in &loop_record.coedges {
let edge = edges.get(&coedge.edge_id).ok_or_else(|| {
KernelRefusal::internal(
KernelStage::Sew,
"loop_edge_lookup",
format!("{op}: missing edge {}", coedge.edge_id),
)
})?;
if edge.degenerate {
continue;
}
for step in 0..8 {
let fraction = step as f64 / 8.0;
let fraction = if coedge.forward {
fraction
} else {
1.0 - fraction
};
points.push(edge.curve.evaluate(edge.t0 + (edge.t1 - edge.t0) * fraction).or_refuse(KernelStage::Sew, "evaluate")?);
}
}
if points.len() < 3 {
return Ok(Vec3::default());
}
let mut total = Vec3::default();
for index in 0..points.len() {
let a = points[index];
let b = points[(index + 1) % points.len()];
total = total.add(a.cross(b));
}
Ok(total.scale(0.5))
}
fn orient_plane_to_loop(
face: &FaceRecord,
plane: &Plane,
edges: &HashMap<u64, EdgeRecord>,
) -> Result<Plane, KernelRefusal> {
let Some(outer) = face.loops.first() else {
return Ok(*plane);
};
let area = loop_area_vector(outer, edges, "rotate_faces")?;
let projection = area.dot(plane.normal);
if projection.abs() <= 1e-12 * area.length().max(1.0) {
return Ok(*plane);
}
if (projection > 0.0) == face.same_sense {
return Ok(*plane);
}
Ok(Plane {
origin: plane.origin,
u_dir: plane.u_dir,
v_dir: plane.v_dir.scale(-1.0),
normal: plane.normal.scale(-1.0),
})
}
fn rotate_ruled_frame(
rotation: &AffineTransform,
frame: &crate::RevolutionFrame,
) -> crate::RevolutionFrame {
crate::RevolutionFrame {
origin: rotation.point(frame.origin),
axis: rotate_direction(rotation, frame.axis),
x_axis: rotate_direction(rotation, frame.x_axis),
y_axis: rotate_direction(rotation, frame.y_axis),
}
}
fn carrier_invariant_under(
solid: &BrepSolid,
face_lookup: &HashMap<u64, (usize, usize)>,
face_id: u64,
axis_point: Vec3,
axis_direction: Vec3,
angular_tolerance: f64,
distance_tolerance: f64,
) -> bool {
let Some(&(shell, face)) = face_lookup.get(&face_id) else {
return false;
};
let surface = &solid.shells[shell].faces[face].surface;
if let Some(AnalyticSurface::Plane { u_dir, v_dir, .. }) = surface.analytic() {
let unit = match u_dir.cross(*v_dir).normalized() {
Ok(unit) => unit,
Err(_) => return false,
};
return unit.cross(axis_direction).length() <= angular_tolerance;
}
let Some((frame, _, _, _)) = carrier_ruled(solid, face_lookup, face_id) else {
return false;
};
if frame.axis.cross(axis_direction).length() > angular_tolerance {
return false;
}
let offset = frame.origin.sub(axis_point);
offset
.sub(axis_direction.scale(offset.dot(axis_direction)))
.length()
<= distance_tolerance
}
fn rotated_moved_ruled_carrier(
solid: &BrepSolid,
face_lookup: &HashMap<u64, (usize, usize)>,
rotation: &AffineTransform,
moved: &HashSet<u64>,
adjacent: &HashSet<u64>,
invariant: &dyn Fn(u64) -> bool,
) -> Option<(u64, crate::RevolutionFrame, f64, f64, f64)> {
let mut here = adjacent.iter().copied().filter(|id| moved.contains(id));
let face_id = here.next()?;
if here.next().is_some() {
return None;
}
if invariant(face_id) {
return None;
}
let (frame, rho0, rho1, height) = carrier_ruled(solid, face_lookup, face_id)?;
Some((
face_id,
rotate_ruled_frame(rotation, &frame),
rho0,
rho1,
height,
))
}
fn ruled_offset(frame: &crate::RevolutionFrame, rho0: f64, rho1: f64, height: f64, point: Vec3) -> f64 {
let delta = point.sub(frame.origin);
let axial = delta.dot(frame.axis);
let radial = delta.sub(frame.axis.scale(axial)).length();
let target = if height == 0.0 {
rho0
} else {
rho0 + (rho1 - rho0) * (axial / height)
};
(radial - target).abs()
}
fn verify_chord_on_curved(
solid: &BrepSolid,
face_lookup: &HashMap<u64, (usize, usize)>,
face_id: u64,
edge_id: u64,
start: Vec3,
end: Vec3,
carrier_rotation: Option<&AffineTransform>,
tolerance: f64,
) -> Result<(), KernelRefusal> {
let Some((frame, rho0, rho1, height)) = carrier_ruled(solid, face_lookup, face_id) else {
return Ok(());
};
let frame = match carrier_rotation {
Some(rotation) => rotate_ruled_frame(rotation, &frame),
None => frame,
};
let midpoint = start.add(end).scale(0.5);
for point in [start, midpoint, end] {
let off = ruled_offset(&frame, rho0, rho1, height, point);
if off > 10.0 * tolerance {
return Err(KernelRefusal::unsupported(KernelStage::Validate, "chord_off_carrier", format!(
"rotate_faces: rebuilding edge {edge_id} straight between its re-solved corners would leave its curved neighbour {} (off {off:.3e}); the rotation moves one end of that edge along the carrier and the other not at all, and no straight chord spans that — refusing",
face_short(solid, face_lookup, face_id)
)));
}
}
Ok(())
}
fn verify_point_on_fixed(
solid: &BrepSolid,
face_lookup: &HashMap<u64, (usize, usize)>,
planes: &mut HashMap<u64, Plane>,
face_id: u64,
point: Vec3,
vertex_id: u64,
plane_tolerance: f64,
tolerance: f64,
) -> Result<(), KernelRefusal> {
if let Some((frame, rho0, rho1, height)) = carrier_ruled(solid, face_lookup, face_id) {
let off = ruled_offset(&frame, rho0, rho1, height, point);
if off > 10.0 * tolerance {
return Err(KernelRefusal::unsupported(KernelStage::Validate, "corner_off_curved_neighbour", format!(
"rotate_faces: the rotated corner at vertex {vertex_id} left its curved \
neighbour {} (off {off:.3e}) — refusing",
face_short(solid, face_lookup, face_id)
)));
}
return Ok(());
}
let plane = cached_plane(planes, solid, face_lookup, face_id, plane_tolerance)
.map_err(|_| {
unsupported_carrier(
solid,
face_lookup,
face_id,
&format!("a FIXED neighbour at vertex {vertex_id}"),
)
})?;
let off = point.sub(plane.origin).dot(plane.normal).abs();
if off > 10.0 * tolerance {
return Err(KernelRefusal::unsupported(KernelStage::Validate, "corner_off_fixed_neighbour", format!(
"rotate_faces: the rotated corner at vertex {vertex_id} left its fixed neighbour \
{} (off {off:.3e}) — refusing",
face_short(solid, face_lookup, face_id)
)));
}
Ok(())
}
fn plan_straight_rebuild(
solid: &BrepSolid,
face_lookup: &HashMap<u64, (usize, usize)>,
faces_of_edge: &HashMap<u64, Vec<u64>>,
edge: &EdgeRecord,
start_old: Vec3,
end_old: Vec3,
start_new: Vec3,
end_new: Vec3,
tolerance: f64,
) -> Result<EdgeAction, KernelRefusal> {
let between = || -> String {
let carriers = faces_of_edge
.get(&edge.id)
.map(Vec::as_slice)
.unwrap_or(&[])
.iter()
.map(|&face_id| face_short(solid, face_lookup, face_id))
.collect::<Vec<_>>();
match carriers.len() {
2 => format!("{} and {}", carriers[0], carriers[1]),
_ => carriers.join(", "),
}
};
if edge.degenerate {
return Err(KernelRefusal::unsupported(KernelStage::Refine, "degenerate_edge", format!(
"rotate_faces: degenerate edge {} between {} would need re-stretching (deferred) \
— refusing",
edge.id,
between()
)));
}
if edge.curve.straight_segment(tolerance).is_none() {
return Err(KernelRefusal::unsupported(KernelStage::Refine, "curved_edge_rebuild", format!(
"rotate_faces: edge {} between {} must be re-stretched but is not a straight line; \
a rotation rebuilds a curved edge only where it is the rim of the rotated plane \
against a cylinder or cone, not between two faces that stay put — refusing",
edge.id,
between()
)));
}
let new_chord = end_new.sub(start_new);
if new_chord.length() <= tolerance {
return Err(KernelRefusal::unsupported(KernelStage::Refine, "edge_collapse", format!(
"rotate_faces: the rotation collapses edge {} between {} to zero length — refusing",
edge.id,
between()
)));
}
if end_old.sub(start_old).dot(new_chord) <= 0.0 {
return Err(KernelRefusal::unsupported(KernelStage::Refine, EDGE_INVERSION, format!(
"rotate_faces: the rotation inverts edge {} between {} (the trim is turned inside \
out) — refusing",
edge.id,
between()
)));
}
Ok(EdgeAction::Rebuild {
start: start_new,
end: end_new,
})
}
fn translate_corner_refusal(error: &str, vertex_id: u64) -> String {
let body = error
.trim_start_matches("move_faces: ")
.replace("pushed cap plane", "rotated plane")
.replace("the push drives", "the rotation drives")
.replace("the push tears", "the rotation tears")
.replace("pushed cap", "rotated face");
format!("rotate_faces: at vertex {vertex_id}, {body}")
}
fn closed_curve_area_vector(curve: &NurbsCurve, t0: f64, t1: f64) -> Result<Vec3, String> {
const STEPS: usize = 64;
let mut points = Vec::with_capacity(STEPS);
for step in 0..STEPS {
let t = t0 + (t1 - t0) * (step as f64 / STEPS as f64);
points.push(curve.evaluate(t)?);
}
let mut total = Vec3::default();
for index in 0..points.len() {
total = total.add(points[index].cross(points[(index + 1) % points.len()]));
}
Ok(total.scale(0.5))
}
fn rim_runs_forward(
frame: &crate::RevolutionFrame,
curve: &NurbsCurve,
t0: f64,
t1: f64,
) -> Result<bool, String> {
let azimuth = |point: Vec3| {
let delta = point.sub(frame.origin);
delta.dot(frame.y_axis).atan2(delta.dot(frame.x_axis))
};
const STEPS: usize = 32;
let mut total = 0.0;
let mut previous = azimuth(curve.evaluate(t0)?);
for step in 1..=STEPS {
let t = t0 + (t1 - t0) * (step as f64 / STEPS as f64);
let current = azimuth(curve.evaluate(t)?);
let mut delta = current - previous;
while delta > std::f64::consts::PI {
delta -= std::f64::consts::TAU;
}
while delta < -std::f64::consts::PI {
delta += std::f64::consts::TAU;
}
total += delta;
previous = current;
}
Ok(total >= 0.0)
}
#[allow(clippy::too_many_arguments)]
fn rotated_rim_section(
edge: &EdgeRecord,
frame: &crate::RevolutionFrame,
sense_frame: &crate::RevolutionFrame,
rho0: f64,
rho1: f64,
height: f64,
plane: &Plane,
fixed_reference: Vec3,
start_new: Vec3,
end_new: Vec3,
tolerance: f64,
) -> Result<NurbsCurve, KernelRefusal> {
let azimuth = |point: Vec3| {
let delta = point.sub(frame.origin);
delta.dot(frame.y_axis).atan2(delta.dot(frame.x_axis))
};
let tau = std::f64::consts::TAU;
let wrap = |angle: f64| {
let value = angle % tau;
if value < 0.0 {
value + tau
} else {
value
}
};
let forward = rim_runs_forward(sense_frame, &edge.curve, edge.t0, edge.t1).or_refuse(KernelStage::Refine, "rim_runs_forward")?;
let closed = edge.start_vertex_id == edge.end_vertex_id;
let (anchor, sweep) = if closed {
(azimuth(start_new), tau)
} else if forward {
(azimuth(start_new), wrap(azimuth(end_new) - azimuth(start_new)))
} else {
(azimuth(end_new), wrap(azimuth(start_new) - azimuth(end_new)))
};
if !closed && (sweep <= 1e-9 || sweep >= tau - 1e-9) {
return Err(KernelRefusal::unsupported(KernelStage::Refine, "rim_degenerate", format!(
"rotate_faces: the re-intersected rim of edge {} degenerates to a point or a \
full turn — refusing",
edge.id
)));
}
let mut curve = plane_ruled_section_arc(
plane.origin,
plane.normal,
frame,
rho0,
rho1,
height,
anchor,
sweep,
tolerance,
)
.map_err(|error| {
KernelRefusal::unsupported(KernelStage::Refine, "ruled_section", format!(
"rotate_faces: rebuilding the rim of edge {} as the section of the rotated \
carrier with its fixed neighbour — {}",
edge.id,
error.trim_start_matches("plane_ruled_section_arc: ")
))
})?;
if !forward {
curve = curve.reversed().or_refuse(KernelStage::Refine, "reversed")?;
}
if closed {
let before = closed_curve_area_vector(&edge.curve, edge.t0, edge.t1).or_refuse(KernelStage::Refine, "closed_curve_area_vector")?.dot(fixed_reference);
let [d0, d1] = curve.domain().or_refuse(KernelStage::Refine, "domain")?;
let after = closed_curve_area_vector(&curve, d0, d1).or_refuse(KernelStage::Refine, "closed_curve_area_vector")?.dot(fixed_reference);
if before * after < 0.0 {
curve = curve.reversed().or_refuse(KernelStage::Refine, "reversed")?;
}
}
verify_section_between(
&curve,
frame,
rho0,
rho1,
height,
plane.normal,
plane.normal.dot(plane.origin),
start_new,
end_new,
tolerance,
)
.map_err(|error| error.with_message(|error| error.replace("move_faces:", "rotate_faces:")))?;
Ok(curve)
}
pub fn rotate_faces(
solid: &BrepSolid,
face_ids: &[u64],
axis_point: Vec3,
axis_direction: Vec3,
angle_radians: f64,
) -> Result<BrepSolid, KernelRefusal> {
rotate_faces_rigid(
solid, face_ids, axis_point, axis_direction, angle_radians, Vec3::new(0.0, 0.0, 0.0),
)
}
pub(crate) fn rotate_faces_rigid(
solid: &BrepSolid,
face_ids: &[u64],
axis_point: Vec3,
axis_direction: Vec3,
angle_radians: f64,
translation: Vec3,
) -> Result<BrepSolid, KernelRefusal> {
if !(translation.x.is_finite() && translation.y.is_finite() && translation.z.is_finite()) {
return Err(KernelRefusal::input(KernelStage::Collect, "translation", "rotate_faces: the translation must be finite"));
}
if !(axis_point.x.is_finite() && axis_point.y.is_finite() && axis_point.z.is_finite()) {
return Err(KernelRefusal::input(KernelStage::Collect, "axis_point", "rotate_faces: the axis point must be finite"));
}
if !(axis_direction.x.is_finite()
&& axis_direction.y.is_finite()
&& axis_direction.z.is_finite())
{
return Err(KernelRefusal::input(KernelStage::Collect, "axis_direction", "rotate_faces: the axis direction must be finite"));
}
if !angle_radians.is_finite() {
return Err(KernelRefusal::input(KernelStage::Collect, "angle", "rotate_faces: the angle must be finite"));
}
let axis = axis_direction.normalized().map_err(|_| {
KernelRefusal::input(
KernelStage::Collect,
"axis_length",
"rotate_faces: the axis direction has zero length",
)
})?;
if face_ids.is_empty() {
return Err(KernelRefusal::input(KernelStage::Collect, "selection", "rotate_faces: no faces selected"));
}
let moved: HashSet<u64> = face_ids.iter().copied().collect();
let mut face_lookup: HashMap<u64, (usize, usize)> = HashMap::default();
for (shell_index, shell) in solid.shells.iter().enumerate() {
for (face_index, face) in shell.faces.iter().enumerate() {
face_lookup
.entry(face.id)
.or_insert((shell_index, face_index));
}
}
for &face_id in face_ids {
if !face_lookup.contains_key(&face_id) {
return Err(KernelRefusal::input(KernelStage::Collect, "face_id", format!("rotate_faces: no face with id {face_id}")));
}
}
let scale = solid_model_scale(solid);
let tolerance = (scale * 1e-7).max(1e-9);
let plane_tolerance = (scale * 1e-6).max(1e-7);
let angular_tolerance = 1e-9;
let rigid_tolerance = (scale * 1e-9).max(1e-12);
let mut rotation = rotation_about(axis_point, axis, angle_radians).or_refuse(KernelStage::Collect, "rotation")?;
rotation.elements[3] += translation.x;
rotation.elements[7] += translation.y;
rotation.elements[11] += translation.z;
let mut faces_of_edge: HashMap<u64, Vec<u64>> = HashMap::default();
for shell in &solid.shells {
for face in &shell.faces {
for loop_record in &face.loops {
for coedge in &loop_record.coedges {
faces_of_edge
.entry(coedge.edge_id)
.or_default()
.push(face.id);
}
}
}
}
let invariant = |face_id: u64| -> bool {
let rotation_invariant = carrier_invariant_under(
solid,
&face_lookup,
face_id,
axis_point,
axis,
angular_tolerance,
plane_tolerance,
);
if !rotation_invariant || translation.length() == 0.0 {
return rotation_invariant;
}
let (shell, face) = face_lookup[&face_id];
let surface = &solid.shells[shell].faces[face].surface;
if let Some(AnalyticSurface::Plane { u_dir, v_dir, .. }) = surface.analytic() {
return u_dir.cross(*v_dir).normalized()
.map(|normal| normal.dot(translation).abs() <= rigid_tolerance)
.unwrap_or(false);
}
let Some((frame, rho0, rho1, _)) = carrier_ruled(solid, &face_lookup, face_id) else {
return false;
};
if (rho1 - rho0).abs() > 1e-9 * rho0.abs().max(rho1.abs()).max(1.0) {
return translation.length() <= rigid_tolerance;
}
translation.sub(frame.axis.scale(translation.dot(frame.axis))).length() <= rigid_tolerance
};
let mut classes: HashMap<u64, EdgeClass> = HashMap::default();
let mut planes: HashMap<u64, Plane> = HashMap::default();
for edge in &solid.edges {
let uses = faces_of_edge
.get(&edge.id)
.map(Vec::as_slice)
.unwrap_or(&[]);
let expected = if edge.degenerate { 1 } else { 2 };
if uses.len() != expected {
return Err(KernelRefusal::input(KernelStage::Collect, "non_manifold", format!(
"rotate_faces: edge {} is used {} times (non-manifold input)",
edge.id,
uses.len()
)));
}
let moved_uses = uses.iter().filter(|face_id| moved.contains(*face_id)).count();
let class = if moved_uses == 0 {
EdgeClass::Fixed
} else if moved_uses == uses.len() {
EdgeClass::Interior
} else {
let moved_face = *uses.iter().find(|face_id| moved.contains(*face_id)).unwrap();
let fixed_face = *uses.iter().find(|face_id| !moved.contains(*face_id)).unwrap();
if carrier_is_planar(solid, &face_lookup, fixed_face) {
cached_plane(&mut planes, solid, &face_lookup, fixed_face, plane_tolerance)?;
} else if !invariant(fixed_face)
&& carrier_ruled(solid, &face_lookup, fixed_face).is_some()
&& !edge.degenerate
&& edge.curve.straight_segment(tolerance).is_some()
{
return Err(KernelRefusal::unsupported(KernelStage::Classify, "ruling_edge", format!(
"rotate_faces: boundary edge {} of {} is a straight ruling of its neighbour {} \
— the face is tangent to that carrier or runs along its axis, as beside a \
fillet band — and a rotation re-intersects a cylinder or cone only where the \
face cuts it in a conic — refusing",
edge.id,
face_short(solid, &face_lookup, moved_face),
face_label(solid, &face_lookup, fixed_face)
)));
} else if !invariant(fixed_face)
&& carrier_ruled(solid, &face_lookup, fixed_face).is_none()
{
return Err(unsupported_carrier(
solid,
&face_lookup,
fixed_face,
&format!(
"the fixed neighbour across boundary edge {} of {}",
edge.id,
face_short(solid, &face_lookup, moved_face)
),
));
}
EdgeClass::Boundary {
moved_face,
fixed_face,
}
};
classes.insert(edge.id, class);
}
for edge in &solid.edges {
let EdgeClass::Boundary {
moved_face,
fixed_face,
} = classes[&edge.id]
else {
continue;
};
if invariant(fixed_face) {
continue; }
if carrier_ruled(solid, &face_lookup, fixed_face).is_some() {
continue;
}
if let Some((frame, rho0, rho1, _)) = carrier_ruled(solid, &face_lookup, moved_face)
.filter(|_| !invariant(moved_face))
{
let radius_scale = rho0.abs().max(rho1.abs()).max(1.0);
if (rho1 - rho0).abs() <= 1e-9 * radius_scale {
let turned_axis = rotate_direction(&rotation, frame.axis);
let fixed_plane = planes[&fixed_face];
if turned_axis.dot(fixed_plane.normal).abs() <= PARALLEL_EPS {
return Err(KernelRefusal::unsupported(KernelStage::Classify, AXIS_PARALLEL_LIMIT, format!(
"rotate_faces: {:.6}° lays the axis of {} parallel to its neighbour {} \
across edge {}; the two carriers then meet in a pair of rulings rather \
than a conic, so there is no rim to rebuild — refusing",
angle_radians.to_degrees(),
face_short(solid, &face_lookup, moved_face),
face_short(solid, &face_lookup, fixed_face),
edge.id
)));
}
}
continue;
}
if !carrier_is_planar(solid, &face_lookup, moved_face) {
continue; }
let moved_plane =
cached_plane(&mut planes, solid, &face_lookup, moved_face, plane_tolerance)?;
let fixed_plane = planes[&fixed_face];
let rotated_normal = rotate_direction(&rotation, moved_plane.normal);
if rotated_normal.cross(fixed_plane.normal).length() <= PARALLEL_EPS {
return Err(KernelRefusal::unsupported(KernelStage::Classify, PARALLEL_LIMIT, format!(
"rotate_faces: {:.6}° turns {} parallel to its neighbour {} across edge {}; \
the two carriers no longer meet, so there is no edge to rebuild — refusing",
angle_radians.to_degrees(),
face_short(solid, &face_lookup, moved_face),
face_short(solid, &face_lookup, fixed_face),
edge.id
)));
}
}
let mut vertex_faces: HashMap<u64, HashSet<u64>> = HashMap::default();
for edge in &solid.edges {
if let Some(uses) = faces_of_edge.get(&edge.id) {
for vertex_id in [edge.start_vertex_id, edge.end_vertex_id] {
vertex_faces
.entry(vertex_id)
.or_default()
.extend(uses.iter().copied());
}
}
}
let mut new_vertex: HashMap<u64, Vec3> = HashMap::default();
for vertex in &solid.vertices {
let Some(adjacent) = vertex_faces.get(&vertex.id) else {
continue;
};
if !adjacent.iter().any(|face_id| moved.contains(face_id)) {
continue;
}
if adjacent.iter().all(|&face_id| invariant(face_id))
|| (translation.length() > 0.0
&& adjacent.iter().filter(|face_id| moved.contains(face_id))
.all(|&face_id| invariant(face_id)))
{
continue;
}
let fixed_at: Vec<u64> = adjacent
.iter()
.copied()
.filter(|face_id| !moved.contains(face_id))
.collect();
if fixed_at.is_empty() {
new_vertex.insert(vertex.id, rotation.point(vertex.point));
continue;
}
if fixed_at.iter().all(|&face_id| invariant(face_id)) {
let point = rotation.point(vertex.point);
for &face_id in &fixed_at {
verify_point_on_fixed(
solid,
&face_lookup,
&mut planes,
face_id,
point,
vertex.id,
plane_tolerance,
tolerance,
)?;
}
new_vertex.insert(vertex.id, point);
continue;
}
if fixed_at
.iter()
.any(|&face_id| carrier_ruled(solid, &face_lookup, face_id).is_some() && !invariant(face_id))
{
let moved_here: Vec<u64> = adjacent
.iter()
.copied()
.filter(|face_id| moved.contains(face_id))
.collect();
if moved_here.len() != 1 {
return Err(KernelRefusal::unsupported(KernelStage::Classify, "single_cap_only", format!(
"rotate_faces: the corner at vertex {} touches {} rotated faces against a \
ruled neighbour (a single rotated cap only) — refusing",
vertex.id,
moved_here.len()
)));
}
let cap_plane =
cached_plane(&mut planes, solid, &face_lookup, moved_here[0], plane_tolerance)
.map_err(|_| {
unsupported_carrier(
solid,
&face_lookup,
moved_here[0],
&format!("the ROTATED cap at vertex {}", vertex.id),
)
})?;
let rotated_cap = rotate_plane(&rotation, &cap_plane);
let plane_c = rotated_cap.normal.dot(rotated_cap.origin);
let fixed_edges: Vec<&EdgeRecord> = solid
.edges
.iter()
.filter(|edge| {
(edge.start_vertex_id == vertex.id || edge.end_vertex_id == vertex.id)
&& matches!(classes.get(&edge.id), Some(EdgeClass::Fixed))
})
.collect();
if fixed_edges.len() != 1 {
return Err(KernelRefusal::unsupported(KernelStage::Classify, "single_fixed_edge_only", format!(
"rotate_faces: the corner at vertex {} rides {} fixed edges against a ruled \
neighbour (exactly one required) — refusing",
vertex.id,
fixed_edges.len()
)));
}
let fixed_edge = fixed_edges[0];
let seed = if fixed_edge.start_vertex_id == vertex.id {
fixed_edge.t0
} else {
fixed_edge.t1
};
let carriers = if fixed_edge.curve.straight_segment(tolerance).is_some() {
None
} else {
plane_and_ruled_carriers(
solid,
&face_lookup,
&mut planes,
&fixed_at,
plane_tolerance,
)
};
let corner = match carriers {
Some((fixed_plane, (frame, rho0, rho1, height))) => corner_on_plane_and_ruled(
&fixed_plane,
rotated_cap.normal,
plane_c,
&frame,
rho0,
rho1,
height,
vertex.point,
tolerance,
)
.map_err(|error| error.with_message(|error| translate_corner_refusal(error, vertex.id)))?,
None => resolve_corner_on_fixed_edge(
fixed_edge,
seed,
rotated_cap.normal,
plane_c,
tolerance,
)
.map_err(|error| error.with_message(|error| translate_corner_refusal(error, vertex.id)))?,
};
for &face_id in &fixed_at {
verify_point_on_fixed(
solid,
&face_lookup,
&mut planes,
face_id,
corner,
vertex.id,
plane_tolerance,
tolerance,
)?;
}
let off_cap = corner.sub(rotated_cap.origin).dot(rotated_cap.normal).abs();
if off_cap > 10.0 * tolerance {
return Err(KernelRefusal::internal(KernelStage::Validate, "corner_on_cap", format!(
"rotate_faces: the re-solved corner at vertex {} left the ROTATED cap \
(off {off_cap:.3e}) — refusing",
vertex.id
)));
}
new_vertex.insert(vertex.id, corner);
continue;
}
if let Some(moved_ruled) = rotated_moved_ruled_carrier(
solid,
&face_lookup,
&rotation,
&moved,
&adjacent,
&invariant,
) {
let (moved_face, turned_frame, rho0, rho1, height) = moved_ruled;
if fixed_at.len() != 1 {
return Err(KernelRefusal::unsupported(KernelStage::Classify, "mirror_corner_flats", format!(
"rotate_faces: the corner at vertex {} where the rotated {} meets its \
neighbours touches {} fixed faces (a rotated cylinder or cone is \
re-intersected against exactly one flat there) — refusing",
vertex.id,
face_short(solid, &face_lookup, moved_face),
fixed_at.len()
)));
}
let fixed_face = fixed_at[0];
let flat = cached_plane(&mut planes, solid, &face_lookup, fixed_face, plane_tolerance)
.map_err(|_| {
unsupported_carrier(
solid,
&face_lookup,
fixed_face,
&format!(
"the fixed neighbour of the rotated {} at vertex {}",
face_short(solid, &face_lookup, moved_face),
vertex.id
),
)
})?;
let seam: Vec<&EdgeRecord> = solid
.edges
.iter()
.filter(|edge| {
(edge.start_vertex_id == vertex.id || edge.end_vertex_id == vertex.id)
&& matches!(classes.get(&edge.id), Some(EdgeClass::Interior))
&& !edge.degenerate
})
.collect();
if seam.len() != 1 {
return Err(KernelRefusal::unsupported(KernelStage::Classify, "seam_edge_count", format!(
"rotate_faces: the corner at vertex {} on the rotated {} rides {} of the \
group's own edges (the seam generatrix, exactly one, is what places it) \
— refusing",
vertex.id,
face_short(solid, &face_lookup, moved_face),
seam.len()
)));
}
let seam = seam[0];
let seed = if seam.start_vertex_id == vertex.id {
seam.t0
} else {
seam.t1
};
let turned_seam = EdgeRecord {
curve: transform_curve(&seam.curve, rotation).or_refuse(KernelStage::Refine, "transform_curve")?,
..seam.clone()
};
let corner = resolve_corner_on_fixed_edge(
&turned_seam,
seed,
flat.normal,
flat.normal.dot(flat.origin),
tolerance,
)
.map_err(|error| error.with_message(|error| translate_corner_refusal(error, vertex.id)))?;
verify_point_on_fixed(
solid,
&face_lookup,
&mut planes,
fixed_face,
corner,
vertex.id,
plane_tolerance,
tolerance,
)?;
let off = ruled_offset(&turned_frame, rho0, rho1, height, corner);
if off > 10.0 * tolerance {
return Err(KernelRefusal::internal(KernelStage::Validate, "mirror_corner_on_carrier", format!(
"rotate_faces: the re-solved corner at vertex {} left the ROTATED {} \
(off {off:.3e}) — refusing",
vertex.id,
face_short(solid, &face_lookup, moved_face)
)));
}
new_vertex.insert(vertex.id, corner);
continue;
}
let mut corner_planes = Vec::with_capacity(adjacent.len());
for &face_id in &fixed_at {
corner_planes.push(
cached_plane(&mut planes, solid, &face_lookup, face_id, plane_tolerance).map_err(
|_| {
unsupported_carrier(
solid,
&face_lookup,
face_id,
&format!(
"a FIXED carrier meeting the rotated group at vertex {}",
vertex.id
),
)
},
)?,
);
}
for face_id in adjacent.iter().copied().filter(|id| moved.contains(id)) {
let plane = cached_plane(&mut planes, solid, &face_lookup, face_id, plane_tolerance)
.map_err(|_| {
unsupported_carrier(
solid,
&face_lookup,
face_id,
&format!(
"a ROTATED carrier meeting a fixed neighbour at vertex {}",
vertex.id
),
)
})?;
corner_planes.push(rotate_plane(&rotation, &plane));
}
let corner = solve_corner(&corner_planes).ok_or_else(|| {
KernelRefusal::ill_posed(KernelStage::Refine, "corner_underconstrained", format!(
"rotate_faces: cannot re-intersect the carriers meeting at vertex {} \
(parallel or under-constrained planes) — refusing",
vertex.id
))
})?;
for plane in &corner_planes {
if corner.sub(plane.origin).dot(plane.normal).abs() > tolerance {
return Err(KernelRefusal::unsupported(KernelStage::Refine, "corner_tear", format!(
"rotate_faces: the rotated group tears away from its neighbours at \
vertex {} — refusing rather than emitting an invalid solid",
vertex.id
)));
}
}
new_vertex.insert(vertex.id, corner);
}
let vertex_position: HashMap<u64, Vec3> = solid
.vertices
.iter()
.map(|vertex| (vertex.id, vertex.point))
.collect();
let mut actions: HashMap<u64, EdgeAction> = HashMap::default();
for edge in &solid.edges {
let position = |vertex_id: u64| -> Result<Vec3, KernelRefusal> {
vertex_position.get(&vertex_id).copied().ok_or_else(|| {
KernelRefusal::internal(
KernelStage::Collect,
"vertex_lookup",
format!("rotate_faces: missing vertex {vertex_id}"),
)
})
};
let start_old = position(edge.start_vertex_id)?;
let end_old = position(edge.end_vertex_id)?;
let start_new = new_vertex
.get(&edge.start_vertex_id)
.copied()
.unwrap_or(start_old);
let end_new = new_vertex
.get(&edge.end_vertex_id)
.copied()
.unwrap_or(end_old);
if start_new.sub(start_old).length() == 0.0
&& end_new.sub(end_old).length() == 0.0
&& faces_of_edge[&edge.id]
.iter()
.all(|face_id| !moved.contains(face_id) || invariant(*face_id))
{
continue;
}
let rigid = start_new.sub(rotation.point(start_old)).length() <= rigid_tolerance
&& end_new.sub(rotation.point(end_old)).length() <= rigid_tolerance;
let rebuild = |planes: &mut HashMap<u64, Plane>| -> Result<EdgeAction, KernelRefusal> {
for &face_id in &faces_of_edge[&edge.id] {
if invariant(face_id) {
continue;
}
if carrier_ruled(solid, &face_lookup, face_id).is_some() {
continue;
}
cached_plane(planes, solid, &face_lookup, face_id, plane_tolerance).map_err(
|_| {
unsupported_carrier(
solid,
&face_lookup,
face_id,
&format!("a carrier of edge {}, whose trim the rotation moves", edge.id),
)
},
)?;
}
let action = plan_straight_rebuild(
solid,
&face_lookup,
&faces_of_edge,
edge,
start_old,
end_old,
start_new,
end_new,
tolerance,
)?;
if let EdgeAction::Rebuild { start, end } = &action {
for &face_id in &faces_of_edge[&edge.id] {
verify_chord_on_curved(
solid,
&face_lookup,
face_id,
edge.id,
*start,
*end,
moved.contains(&face_id).then_some(&rotation),
tolerance,
)?;
}
}
Ok(action)
};
match classes[&edge.id] {
EdgeClass::Fixed => {
actions.insert(edge.id, rebuild(&mut planes)?);
}
EdgeClass::Interior => {
if rigid {
actions.insert(edge.id, EdgeAction::Rotate);
} else {
actions.insert(edge.id, rebuild(&mut planes)?);
}
}
EdgeClass::Boundary {
moved_face,
fixed_face,
} => {
if rigid && invariant(fixed_face) {
actions.insert(edge.id, EdgeAction::Rotate);
} else if let Some((frame, rho0, rho1, height)) =
carrier_ruled(solid, &face_lookup, fixed_face).filter(|_| !invariant(fixed_face))
{
let moved_plane =
cached_plane(&mut planes, solid, &face_lookup, moved_face, plane_tolerance)
.map_err(|_| {
unsupported_carrier(
solid,
&face_lookup,
moved_face,
&format!(
"the ROTATED side of boundary edge {} against {}",
edge.id,
face_short(solid, &face_lookup, fixed_face)
),
)
})?;
let curve = rotated_rim_section(
edge,
&frame,
&frame,
rho0,
rho1,
height,
&rotate_plane(&rotation, &moved_plane),
frame.axis,
start_new,
end_new,
tolerance,
)?;
actions.insert(edge.id, EdgeAction::Replace { curve });
} else if let Some((frame, rho0, rho1, height)) =
carrier_ruled(solid, &face_lookup, moved_face)
.filter(|_| !invariant(moved_face))
{
let fixed_plane =
cached_plane(&mut planes, solid, &face_lookup, fixed_face, plane_tolerance)
.map_err(|_| {
unsupported_carrier(
solid,
&face_lookup,
fixed_face,
&format!(
"the FIXED side of boundary edge {} against the rotated {}",
edge.id,
face_short(solid, &face_lookup, moved_face)
),
)
})?;
let curve = rotated_rim_section(
edge,
&rotate_ruled_frame(&rotation, &frame),
&frame,
rho0,
rho1,
height,
&fixed_plane,
fixed_plane.normal,
start_new,
end_new,
tolerance,
)?;
actions.insert(edge.id, EdgeAction::Replace { curve });
} else {
cached_plane(&mut planes, solid, &face_lookup, moved_face, plane_tolerance)
.map_err(|_| {
unsupported_carrier(
solid,
&face_lookup,
moved_face,
&format!(
"the ROTATED side of boundary edge {} against {}",
edge.id,
face_short(solid, &face_lookup, fixed_face)
),
)
})?;
actions.insert(edge.id, rebuild(&mut planes)?);
}
}
}
}
let reshaped: HashSet<u64> = actions
.iter()
.filter(|(_, action)| {
matches!(
action,
EdgeAction::Rebuild { .. } | EdgeAction::Replace { .. }
)
})
.map(|(edge_id, _)| *edge_id)
.collect();
let dirty: HashSet<u64> = actions.keys().copied().collect();
let mut face_actions: Vec<(usize, usize, FaceAction)> = Vec::new();
let mut ruled_retrim_faces: Vec<u64> = Vec::new();
for (shell_index, shell) in solid.shells.iter().enumerate() {
for (face_index, face) in shell.faces.iter().enumerate() {
let edge_ids = || {
face.loops
.iter()
.flat_map(|loop_record| &loop_record.coedges)
.map(|coedge| coedge.edge_id)
};
if moved.contains(&face.id) {
if edge_ids().any(|edge_id| reshaped.contains(&edge_id))
&& carrier_ruled(solid, &face_lookup, face.id).is_some()
&& !invariant(face.id)
{
face_actions.push((shell_index, face_index, FaceAction::RotateSurface));
ruled_retrim_faces.push(face.id);
} else if edge_ids().any(|edge_id| reshaped.contains(&edge_id)) {
let plane =
cached_plane(&mut planes, solid, &face_lookup, face.id, plane_tolerance)
.map_err(|_| {
unsupported_carrier(
solid,
&face_lookup,
face.id,
"the ROTATED face whose rim the neighbours reshaped",
)
})?;
face_actions.push((
shell_index,
face_index,
FaceAction::Retrim(rotate_plane(&rotation, &plane)),
));
} else if edge_ids().any(|edge_id| dirty.contains(&edge_id)) {
face_actions.push((shell_index, face_index, FaceAction::RotateSurface));
} else if !invariant(face.id) {
face_actions.push((shell_index, face_index, FaceAction::RotateSurface));
}
} else if edge_ids().any(|edge_id| dirty.contains(&edge_id)) {
if carrier_is_planar(solid, &face_lookup, face.id) {
let plane =
cached_plane(&mut planes, solid, &face_lookup, face.id, plane_tolerance)?;
face_actions.push((shell_index, face_index, FaceAction::Retrim(plane)));
} else if carrier_ruled(solid, &face_lookup, face.id).is_some() {
ruled_retrim_faces.push(face.id);
} else {
return Err(unsupported_carrier(
solid,
&face_lookup,
face.id,
"a FIXED face the rotation must re-trim",
));
}
}
}
}
let mut result = solid.clone();
for edge in &mut result.edges {
match actions.get(&edge.id) {
Some(EdgeAction::Rotate) => {
edge.curve = transform_curve(&edge.curve, rotation).or_refuse(KernelStage::Sew, "transform_curve")?;
}
Some(EdgeAction::Rebuild { start, end }) => {
edge.curve = make_line(*start, *end).or_refuse(KernelStage::Sew, "make_line")?;
edge.t0 = 0.0;
edge.t1 = 1.0;
}
Some(EdgeAction::Replace { curve }) => {
let [d0, d1] = curve.domain().or_refuse(KernelStage::Sew, "domain")?;
edge.curve = curve.clone();
edge.t0 = d0;
edge.t1 = d1;
}
None => {}
}
}
for vertex in &mut result.vertices {
if let Some(point) = new_vertex.get(&vertex.id) {
vertex.point = *point;
}
}
let final_edges: HashMap<u64, EdgeRecord> = result
.edges
.iter()
.map(|edge| (edge.id, edge.clone()))
.collect();
for (shell_index, face_index, action) in face_actions {
let face = &mut result.shells[shell_index].faces[face_index];
match action {
FaceAction::RotateSurface => {
face.surface = transform_surface(&face.surface, rotation).or_refuse(KernelStage::Sew, "transform_surface")?;
}
FaceAction::Retrim(plane) => {
let plane = if moved.contains(&face.id) {
orient_plane_to_loop(face, &plane, &final_edges)?
} else {
plane
};
retrim_planar_face(face, &plane, &final_edges, scale, "rotate_faces")?;
let face_edges: HashSet<u64> = face
.loops
.iter()
.flat_map(|loop_record| loop_record.coedges.iter().map(|c| c.edge_id))
.collect();
refit_touched_pcurves(
face,
&final_edges,
&face_edges,
true,
tolerance,
"rotate_faces",
)?;
}
}
}
for face_id in ruled_retrim_faces {
retrim_ruled_face(&mut result, face_id, &final_edges, tolerance, "rotate_faces")?;
}
let report = result.validate_detailed(&crate::KernelTolerances::for_solid(&result, 1e-7));
if !report.issues.is_empty() {
return Err(KernelRefusal::internal(KernelStage::Validate, "validate", format!(
"rotate_faces: rotated solid failed validation: {:?}",
report.issues
)));
}
let crossings = crate::loop_self_crossings(&result);
if !crossings.unreadable.is_empty() {
return Err(KernelRefusal::internal(KernelStage::Validate, "crossing_scan_unreadable", format!(
"rotate_faces: the loop self-crossing scan could not read {:?}; refusing rather \
than shipping an unchecked result",
crossings.unreadable
)));
}
if crossings.loops == 0 {
return Err(KernelRefusal::internal(KernelStage::Validate, "crossing_scan_empty",
"rotate_faces: the loop self-crossing scan read no loops at all; refusing rather than shipping an unchecked result"));
}
if crossings.is_flagged() {
return Err(KernelRefusal::internal(KernelStage::Validate, "loop_self_crossing", format!(
"rotate_faces: the rotation makes a re-trimmed neighbour's own loop cross itself \
({}) — refusing",
crossings.summary().unwrap_or_default()
)));
}
if let (Ok(before), Ok(after)) =
(crate::signed_volume_against(solid, 0.0), crate::signed_volume_against(&result, 0.0))
{
if before * after <= 0.0 {
return Err(KernelRefusal::unsupported(KernelStage::Validate, SOLID_INVERSION,
"rotate_faces: the rotation inverts the solid (signed volume changed sign) \
— refusing"));
}
}
if !report.wire_warnings.is_empty() {
let inherited = solid
.validate_detailed(&crate::KernelTolerances::for_solid(solid, 1e-7))
.wire_warnings;
if report.wire_warnings.len() > inherited.len() {
return Err(KernelRefusal::internal(KernelStage::Validate, "chart_winding", format!(
"rotate_faces: the rotation leaves a face whose (u,v) chart is left-handed \
against its own normal ({}) — refusing",
report
.wire_warnings
.iter()
.map(|warning| warning.message.as_str())
.collect::<Vec<_>>()
.join("; ")
)));
}
}
Ok(result)
}