use super::*;
pub(super) fn build_octant_face(
center: Vec3,
normals: [Vec3; 3],
radius: f64,
t_ids: [u64; 3],
t_pts: [Vec3; 3],
arcs: &[(u64, u64, u64)],
name: Option<&str>,
next_id: &mut dyn FnMut() -> u64,
) -> Result<(FaceRecord, EdgeRecord), String> {
let [m1, m2, m3] = normals;
if m1.cross(m2).dot(m3) < 0.5 {
return Err("round_convex_corner: octant normals are not right-handed".into());
}
let half = std::f64::consts::FRAC_PI_2;
let meridian = crate::make_arc(center, m1, m3, radius, -half, half)?;
let surface = crate::make_revolution(center, m3, &meridian, std::f64::consts::TAU)?;
let [t1, t2, t3] = t_ids;
let e_pole = next_id();
let pole_edge = EdgeRecord {
id: e_pole,
curve: crate::make_line(t_pts[2], t_pts[2])?,
t0: 0.0,
t1: 1.0,
start_vertex_id: t3,
end_vertex_id: t3,
degenerate: true,
name: None,
};
let find = |from: u64, to: u64| -> Result<(u64, bool), String> {
for &(eid, s, e) in arcs {
if s == from && e == to {
return Ok((eid, true));
}
if s == to && e == from {
return Ok((eid, false));
}
}
Err(format!(
"round_convex_corner: no tangent arc between vertices {from} and {to}"
))
};
let (e12, f12) = find(t1, t2)?; let (e23, f23) = find(t2, t3)?; let (e31, f31) = find(t3, t1)?;
let pl = |u0, v0, u1, v1| crate::sweep_topology::parameter_line(u0, v0, u1, v1);
let loop_id = next_id();
let coedges = vec![
CoedgeRecord {
id: next_id(),
edge_id: e12,
forward: f12,
pcurve: pl(0.0, 0.5, 0.25, 0.5)?,
},
CoedgeRecord {
id: next_id(),
edge_id: e23,
forward: f23,
pcurve: pl(0.25, 0.5, 0.25, 1.0)?,
},
CoedgeRecord {
id: next_id(),
edge_id: e_pole,
forward: true,
pcurve: pl(0.25, 1.0, 0.0, 1.0)?,
},
CoedgeRecord {
id: next_id(),
edge_id: e31,
forward: f31,
pcurve: pl(0.0, 1.0, 0.0, 0.5)?,
},
];
let mid = surface.evaluate(0.125, 0.75)?;
let surface_normal = surface.normal(0.125, 0.75)?;
let outward = mid.sub(center).normalized()?;
let same_sense = surface_normal.dot(outward) > 0.0;
let face = FaceRecord {
id: next_id(),
surface,
same_sense,
loops: vec![LoopRecord {
id: loop_id,
coedges,
}],
name: name.map(|value| value.to_string()),
};
Ok((face, pole_edge))
}
pub(in crate::blend) fn build_general_corner_patch(
center: Vec3,
radius: f64,
normals: &[Vec3],
arc_edges: &[EdgeRecord],
outward: bool,
name: Option<&str>,
next_id: &mut dyn FnMut() -> u64,
) -> Result<FaceRecord, String> {
use std::f64::consts::{FRAC_PI_2, PI, TAU};
let n_faces = normals.len();
if n_faces < 3 || arc_edges.len() != n_faces {
return Err(format!(
"build_general_corner_patch: expected N≥3 faces with one arc each, \
found {n_faces} faces and {} arcs",
arc_edges.len()
));
}
let pc = normals
.iter()
.fold(Vec3::default(), |acc, n| acc.add(*n))
.normalized()?;
let e0 = pc.perpendicular()?;
let e1 = pc.cross(e0).normalized()?;
let mut crit: Vec<Vec3> = normals.to_vec();
for i in 0..n_faces {
for j in (i + 1)..n_faces {
if let Ok(mid) = normals[i].add(normals[j]).normalized() {
crit.push(mid);
}
}
}
let mut best_phi = 0.0f64;
let mut best_max = f64::INFINITY;
for k in 0..360 {
let phi = PI * k as f64 / 360.0;
let polar = e0.scale(phi.cos()).add(e1.scale(phi.sin()));
let worst = crit
.iter()
.fold(0.0f64, |acc, d| acc.max(d.dot(polar).abs()));
if worst < best_max {
best_max = worst;
best_phi = phi;
}
}
let polar = e0
.scale(best_phi.cos())
.add(e1.scale(best_phi.sin()))
.normalized()?;
let x_axis = pc.scale(-1.0);
let meridian = crate::make_arc(center, x_axis, polar, radius, -FRAC_PI_2, FRAC_PI_2)?;
let surface = crate::make_revolution(center, polar, &meridian, TAU)?;
let mut order = vec![0usize];
let mut used = vec![false; n_faces];
used[0] = true;
let mut chain_vertex = arc_edges[0].start_vertex_id;
for _ in 0..(n_faces - 1) {
let next = (0..n_faces)
.find(|&j| !used[j] && arc_edges[j].end_vertex_id == chain_vertex)
.ok_or("build_general_corner_patch: tangent arcs do not form a closed N-gon")?;
used[next] = true;
order.push(next);
chain_vertex = arc_edges[next].start_vertex_id;
}
if chain_vertex != arc_edges[0].end_vertex_id {
return Err("build_general_corner_patch: tangent arc loop is not closed".into());
}
const PATCH_TRACK_SAMPLES: usize = 512;
const PATCH_TRACK_SAMPLES_LIMIT: usize = 4096;
let floor = crate::pcurve::PCURVE_REFINEMENT_TOLERANCE;
let mut coedges = Vec::with_capacity(n_faces);
for &ai in &order {
let edge = &arc_edges[ai];
let at = |fraction: f64| edge.curve.evaluate(edge.t1 - (edge.t1 - edge.t0) * fraction);
let breaks = super::super::track_fit::curve_breaks(&edge.curve, edge.t0, edge.t1, false);
let fit = super::super::track_fit::fit_curve_track(
&surface,
&at,
&breaks,
floor,
PATCH_TRACK_SAMPLES,
PATCH_TRACK_SAMPLES_LIMIT,
)?;
if std::env::var("BREP_DEBUG_TRACK_FIT").is_ok() {
eprintln!(
"TRACK_FIT corner patch arc {}: miss {:.3e} at {} samples",
edge.id, fit.miss, fit.samples
);
}
if !fit.on_floor {
return Err(format!(
"{} the corner patch's pcurve along arc {} misses its arc by {:.3e} at {} \
samples, against a floor of {floor:.1e}",
crate::blend::PCURVE_OFF_FLOOR,
edge.id,
fit.miss,
fit.samples
));
}
coedges.push(CoedgeRecord {
id: next_id(),
edge_id: edge.id,
forward: false,
pcurve: fit.curve,
});
}
let centre_projection =
crate::project_point_to_surface(&surface, center.add(pc.scale(radius)))?;
let surface_normal = surface.normal(centre_projection.u, centre_projection.v)?;
let same_sense = (surface_normal.dot(pc) > 0.0) == outward;
let loop_id = next_id();
Ok(FaceRecord {
id: next_id(),
surface,
same_sense,
loops: vec![LoopRecord {
id: loop_id,
coedges,
}],
name: name.map(|value| value.to_string()),
})
}
pub(in crate::blend) fn build_chamfer_corner_facet(
center: Vec3,
normals: &[Vec3],
chord_edges: &[EdgeRecord],
outward: bool,
name: Option<&str>,
next_id: &mut dyn FnMut() -> u64,
) -> Result<FaceRecord, String> {
let n_faces = normals.len();
if n_faces < 3 || chord_edges.len() != n_faces {
return Err(format!(
"blend network: a chamfered star closes with the facet its end chords bound, which \
needs N≥3 faces carrying one chord each — this vertex has {n_faces} faces and {} \
sections",
chord_edges.len()
));
}
let pc = normals
.iter()
.fold(Vec3::default(), |acc, n| acc.add(*n))
.normalized()?;
let mut order = vec![0usize];
let mut used = vec![false; n_faces];
used[0] = true;
let mut chain_vertex = chord_edges[0].start_vertex_id;
for _ in 0..(n_faces - 1) {
let next = (0..n_faces)
.find(|&j| !used[j] && chord_edges[j].end_vertex_id == chain_vertex)
.ok_or("blend network: the chamfer facet's sections do not form a closed N-gon")?;
used[next] = true;
order.push(next);
chain_vertex = chord_edges[next].start_vertex_id;
}
if chain_vertex != chord_edges[0].end_vertex_id {
return Err("blend network: the chamfer facet's section loop is not closed".into());
}
let corner_point = |index: usize| -> Result<Vec3, String> {
let edge = &chord_edges[index];
edge.curve.evaluate(edge.t0)
};
let points = order
.iter()
.map(|&index| corner_point(index))
.collect::<Result<Vec<_>, String>>()?;
let [a, b] = [points[0], points[1]];
let u_dir = b.sub(a).normalized()?;
let mut newell = Vec3::default();
for index in 0..n_faces {
let from = points[index];
let to = points[(index + 1) % n_faces];
newell = newell.add(from.sub(a).cross(to.sub(a)));
}
let normal = newell.normalized()?;
let v_dir = normal.cross(u_dir).normalized()?;
let radius = points
.iter()
.fold(0.0f64, |worst, point| worst.max(point.sub(center).length()));
let flatness = 1e-9 * (1.0 + radius);
let skew = points
.iter()
.fold(0.0f64, |worst, point| worst.max(point.sub(a).dot(normal).abs()));
if skew > flatness {
return Err(format!(
"blend network: the {n_faces} chords at a chamfered star bound a SKEW N-gon \
({skew:.3e} out of plane against {flatness:.3e}); a non-planar corner fill is not \
implemented"
));
}
let reach = points
.iter()
.fold(0.0f64, |worst, point| worst.max(point.sub(a).length()))
.max(1e-6)
* 4.0;
let origin = a.sub(u_dir.scale(reach)).sub(v_dir.scale(reach));
let plane = crate::make_plane(origin, u_dir, v_dir, 2.0 * reach, 2.0 * reach)?;
let uv_of = |point: Vec3| -> Result<(f64, f64), String> {
let projection = crate::project_point_to_surface(&plane, point)?;
Ok((projection.u, projection.v))
};
let mut coedges = Vec::with_capacity(n_faces);
for &index in &order {
let edge = &chord_edges[index];
let (u0, v0) = uv_of(edge.curve.evaluate(edge.t1)?)?;
let (u1, v1) = uv_of(edge.curve.evaluate(edge.t0)?)?;
coedges.push(CoedgeRecord {
id: next_id(),
edge_id: edge.id,
forward: false,
pcurve: crate::sweep_topology::parameter_line(u0, v0, u1, v1)?,
});
}
let plane_normal = plane.normal(0.5, 0.5)?;
let same_sense = (plane_normal.dot(pc) > 0.0) == outward;
let loop_id = next_id();
Ok(FaceRecord {
id: next_id(),
surface: plane,
same_sense,
loops: vec![LoopRecord {
id: loop_id,
coedges,
}],
name: name.map(|value| value.to_string()),
})
}