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_PCURVE_SAMPLES: usize = 33;
let mut coedges = Vec::with_capacity(n_faces);
for &ai in &order {
let edge = &arc_edges[ai];
let mut points = Vec::with_capacity(PATCH_PCURVE_SAMPLES);
let mut params = Vec::with_capacity(PATCH_PCURVE_SAMPLES);
let mut previous_u: Option<f64> = None;
for k in 0..PATCH_PCURVE_SAMPLES {
let fraction = k as f64 / (PATCH_PCURVE_SAMPLES - 1) as f64;
let t = edge.t1 - (edge.t1 - edge.t0) * fraction;
let p = edge.curve.evaluate(t)?;
let projection = crate::project_point_to_surface(&surface, p)?;
let mut u = projection.u;
let v = projection.v;
if let Some(pu) = previous_u {
while u - pu > 0.5 {
u -= 1.0;
}
while u - pu < -0.5 {
u += 1.0;
}
}
previous_u = Some(u);
points.push(Vec4 {
x: u,
y: v,
z: 0.0,
w: 1.0,
});
params.push(fraction);
}
let pcurve = crate::fit::interpolate_homogeneous(&points, 3, ¶ms)?;
coedges.push(CoedgeRecord {
id: next_id(),
edge_id: edge.id,
forward: false,
pcurve,
});
}
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 a planar facet, which is only \
determined for 3 faces — 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 [a, b, c] = [corner_point(0)?, corner_point(1)?, corner_point(2)?];
let u_dir = b.sub(a).normalized()?;
let normal = u_dir.cross(c.sub(a)).normalized()?;
let v_dir = normal.cross(u_dir).normalized()?;
let reach = [a, b, c]
.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 _ = center;
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()),
})
}