Loop blends: sharp corners between straight edges meet in mitres (ellipse arcs), arcs of exactly the blend's radius become sphere octants (pole-free), and loops plus the other edges blend in order (every edge of an extruded part: sphere and torus corners)
Oracle: 58/76. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
457ed7fed7
commit
7ca6e9278b
4 files changed
+223
-28
No files matched your search
+157
-23
@@ -169,13 +169,19 @@ fn build(solid: &Solid, si: usize, faces: &[&mt::Face], fi: usize, wi: usize, n:
|
||||
let owner: Vec<usize> = split.iter().map(|x| x.0).collect();
|
||||
let segs: Vec<Seg> = split.iter().map(|x| x.1).collect();
|
||||
let k = segs.len();
|
||||
// Smooth joints.
|
||||
// Joints: smooth, or sharp between two lines (the blends then meet in a mitre).
|
||||
let mut mitre = vec![false; k];
|
||||
for i in 0..k {
|
||||
let (Some(prev), Some(cur)) = (segs.get((i + k - 1) % k), segs.get(i)) else { continue };
|
||||
let p = cur.start();
|
||||
let (Some(ta), Some(tb)) = (prev.tangent(p), cur.tangent(p)) else { return Err(unsupported("degenerate loop edge")) };
|
||||
if ta.dot(tb) < 1.0 - 1e-6 {
|
||||
return Err(unsupported("blending a loop with sharp corners"));
|
||||
if !(matches!(prev, Seg::Line { .. }) && matches!(cur, Seg::Line { .. })) || ta.dot(tb) < -1.0 + 1e-3 {
|
||||
return Err(unsupported("blending a loop with sharp corners next to arcs"));
|
||||
}
|
||||
if let Some(x) = mitre.get_mut(i) {
|
||||
*x = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Walls: the other face of each edge, perpendicular to the face; all on one side.
|
||||
@@ -226,26 +232,91 @@ fn build(solid: &Solid, si: usize, faces: &[&mt::Face], fi: usize, wi: usize, n:
|
||||
let sb = r / (phi / 2.0).tan();
|
||||
Some((m, t, sb, r / (phi / 2.0).sin()))
|
||||
};
|
||||
// The straight seam between the walls at a sharp joint (direction away from the face).
|
||||
let seam_dir = |i: usize, p: Vec3| -> Option<Vec3> {
|
||||
let o = owner.get(i)?;
|
||||
let v = edges.get(*o)?.front().clone();
|
||||
let loop_ids: Vec<mt::EdgeID> = edges.iter().map(|e| e.id()).collect();
|
||||
let mut dirs = Vec::new();
|
||||
for g in faces {
|
||||
for e in g.edge_iter() {
|
||||
if loop_ids.contains(&e.id()) || !(*e.front() == v || *e.back() == v) {
|
||||
continue;
|
||||
}
|
||||
let other = if *e.front() == v { e.back().clone() } else { e.front().clone() };
|
||||
if let Some(d) = (from_p3(other.point()) - p).normalized()
|
||||
&& !dirs.iter().any(|x: &Vec3| x.dot(d) > 1.0 - 1e-9)
|
||||
{
|
||||
dirs.push(d);
|
||||
}
|
||||
}
|
||||
}
|
||||
match dirs[..] {
|
||||
[d] => Some(d),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
let mut pa = Vec::with_capacity(k);
|
||||
let mut pb = Vec::with_capacity(k);
|
||||
let mut centres = Vec::with_capacity(k);
|
||||
for i in 0..k {
|
||||
let p = segs.get(i).map(|s| s.start()).unwrap_or_default();
|
||||
let (m, t, sb, dc) = frame(i, p).ok_or_else(|| unsupported("the face and wall meet at an angle the blend can't take"))?;
|
||||
pa.push(p + m * sb);
|
||||
pb.push(p + t * sb);
|
||||
centres.push(p + (m + t).normalized().unwrap_or(m) * dc);
|
||||
if !mitre.get(i).copied().unwrap_or(false) {
|
||||
pa.push(p + m * sb);
|
||||
pb.push(p + t * sb);
|
||||
continue;
|
||||
}
|
||||
// Sharp joint: the face rails meet where the two offset lines cross; the wall rails
|
||||
// meet on the seam.
|
||||
let ip = (i + k - 1) % k;
|
||||
let (m0, t0, sb0, _) = frame(ip, p).ok_or_else(|| unsupported("the face and wall meet at an angle the blend can't take"))?;
|
||||
let (Some(d0), Some(d1)) = (segs.get(ip).and_then(|x| x.tangent(p)), segs.get(i).and_then(|x| x.tangent(p))) else {
|
||||
return Err(unsupported("degenerate loop edge"));
|
||||
};
|
||||
let x = d0.cross(d1);
|
||||
let lam = ((m * sb - m0 * sb0).cross(d1)).dot(x) / x.len2();
|
||||
pa.push(p + m0 * sb0 + d0 * lam);
|
||||
let e = seam_dir(i, p).ok_or_else(|| unsupported("a sharp corner without one straight seam"))?;
|
||||
let (h0, h1) = (e.dot(t0), e.dot(t));
|
||||
if h0 < 1e-6 || h1 < 1e-6 || (sb0 / h0 - sb / h1).abs() > tol * 10.0 {
|
||||
return Err(unsupported("a sharp corner whose walls meet the blend differently"));
|
||||
}
|
||||
pb.push(p + e * (sb / h1));
|
||||
}
|
||||
// The offset arcs must not collapse.
|
||||
for s in &segs {
|
||||
// The offset arcs must not turn inside out; an arc of exactly the blend's radius shrinks
|
||||
// to a point (a rounded corner rounded again: the blend there is a sphere).
|
||||
let mut collapsed = vec![false; k];
|
||||
for (i, s) in segs.iter().enumerate() {
|
||||
if let Seg::Arc { a, c, .. } = *s {
|
||||
let toward = into(s, a).dot(c - a) > 0.0;
|
||||
if toward && a.dist(c) <= r + tol {
|
||||
if toward
|
||||
&& (a.dist(c) - r).abs() <= tol * 10.0
|
||||
&& !mitre.get(i).copied().unwrap_or(true)
|
||||
&& !mitre.get((i + 1) % k).copied().unwrap_or(true)
|
||||
{
|
||||
if let Some(x) = collapsed.get_mut(i) {
|
||||
*x = true;
|
||||
}
|
||||
} else if toward && a.dist(c) <= r + tol {
|
||||
return Err(unsupported("the blend is larger than a corner radius"));
|
||||
}
|
||||
}
|
||||
}
|
||||
let va: Vec<mt::Vertex> = pa.iter().map(|p| builder::vertex(p3(*p))).collect();
|
||||
if collapsed.iter().all(|x| *x) {
|
||||
return Err(unsupported("the blend is larger than a corner radius"));
|
||||
}
|
||||
let mut va: Vec<mt::Vertex> = pa.iter().map(|p| builder::vertex(p3(*p))).collect();
|
||||
// A collapsed piece starts and ends at one vertex.
|
||||
for i in 0..k {
|
||||
if collapsed.get(i).copied().unwrap_or(false)
|
||||
&& let Some(v) = va.get(i).cloned()
|
||||
&& let Some(slot) = va.get_mut((i + 1) % k)
|
||||
{
|
||||
*slot = v;
|
||||
}
|
||||
}
|
||||
let vb: Vec<mt::Vertex> = pb.iter().map(|p| builder::vertex(p3(*p))).collect();
|
||||
let centre = |i: usize| centres.get(i).copied().unwrap_or_default();
|
||||
let profile_mid = |i: usize| {
|
||||
@@ -253,16 +324,50 @@ fn build(solid: &Solid, si: usize, faces: &[&mt::Face], fi: usize, wi: usize, n:
|
||||
c + (s.start() - c).normalized().unwrap_or(n) * r
|
||||
};
|
||||
// Profiles at the joints (face side → wall side).
|
||||
let profiles: Vec<mt::Edge> = (0..k)
|
||||
.map(|i| {
|
||||
let (Some(a), Some(b)) = (va.get(i), vb.get(i)) else {
|
||||
return builder::line(&builder::vertex(p3(Vec3::ZERO)), &builder::vertex(p3(Vec3::X)));
|
||||
};
|
||||
if round { builder::circle_arc(a, b, p3(profile_mid(i))) } else { builder::line(a, b) }
|
||||
})
|
||||
.collect();
|
||||
let mut profiles: Vec<mt::Edge> = Vec::with_capacity(k);
|
||||
for i in 0..k {
|
||||
let (Some(a), Some(b)) = (va.get(i), vb.get(i)) else { return Err(KernelError::Failed("joint".into())) };
|
||||
if !round {
|
||||
profiles.push(builder::line(a, b));
|
||||
continue;
|
||||
}
|
||||
if !mitre.get(i).copied().unwrap_or(false) {
|
||||
profiles.push(builder::circle_arc(a, b, p3(profile_mid(i))));
|
||||
continue;
|
||||
}
|
||||
// Mitre: the cross-section circle of this edge's blend where it passes the face-side
|
||||
// point, slid along the edge onto the mitre plane (an ellipse arc, on both blends).
|
||||
let p = segs.get(i).map(|x| x.start()).unwrap_or_default();
|
||||
let d1 = segs.get(i).and_then(|x| x.tangent(p)).ok_or_else(|| unsupported("tangent"))?;
|
||||
let (pa_i, pb_i) = (from_p3(a.point()), from_p3(b.point()));
|
||||
let lam = (pa_i - p).dot(d1);
|
||||
let (_, t, sb, _) = frame(i, p).ok_or_else(|| unsupported("frame"))?;
|
||||
let cc = centre(i) + d1 * lam;
|
||||
let q = p + t * sb + d1 * lam;
|
||||
let mid = cc + ((pa_i - cc) + (q - cc)).normalized().unwrap_or(t) * r;
|
||||
let arc = builder::circle_arc(&builder::vertex(p3(pa_i)), &builder::vertex(p3(q)), p3(mid));
|
||||
let nb = (pa_i - p).cross(pb_i - p).normalized().ok_or_else(|| unsupported("mitre plane"))?;
|
||||
let den = d1.dot(nb);
|
||||
if den.abs() < 1e-9 {
|
||||
return Err(unsupported("mitre plane along the edge"));
|
||||
}
|
||||
// x ↦ x − d1 ((x − p)·nb) / (d1·nb), column-major.
|
||||
let col = |j: usize| {
|
||||
let ej = [Vec3::X, Vec3::Y, Vec3::Z][j];
|
||||
ej - d1 * (ej.dot(nb) / den)
|
||||
};
|
||||
let (c0, c1, c2) = (col(0), col(1), col(2));
|
||||
let tr = d1 * (p.dot(nb) / den);
|
||||
let mat = mt::Matrix4::new(c0.x, c0.y, c0.z, 0.0, c1.x, c1.y, c1.z, 0.0, c2.x, c2.y, c2.z, 0.0, tr.x, tr.y, tr.z, 1.0);
|
||||
let mut curve = arc.curve();
|
||||
mt::Transformed::transform_by(&mut curve, mat);
|
||||
profiles.push(mt::Edge::new(a, b, curve));
|
||||
}
|
||||
let rail = |vs: &[mt::Vertex], i: usize, offset: &dyn Fn(Vec3, &Seg) -> Vec3| -> Option<mt::Edge> {
|
||||
let s = segs.get(i)?;
|
||||
if std::ptr::eq(vs, va.as_slice()) && collapsed.get(i).copied().unwrap_or(false) {
|
||||
return None;
|
||||
}
|
||||
let (a, b) = (vs.get(i)?, vs.get((i + 1) % k)?);
|
||||
Some(match *s {
|
||||
Seg::Line { .. } => builder::line(a, b),
|
||||
@@ -278,8 +383,10 @@ fn build(solid: &Solid, si: usize, faces: &[&mt::Face], fi: usize, wi: usize, n:
|
||||
let i = piece_of(s);
|
||||
frame(i, p).map(|(_, t, sb, _)| p + t * sb).unwrap_or(p)
|
||||
};
|
||||
let face_rails: Vec<mt::Edge> =
|
||||
(0..k).map(|i| rail(&va, i, &face_off)).collect::<Option<_>>().ok_or_else(|| KernelError::Failed("rail".into()))?;
|
||||
let face_rails: Vec<Option<mt::Edge>> = (0..k).map(|i| rail(&va, i, &face_off)).collect();
|
||||
if face_rails.iter().zip(&collapsed).any(|(r, c)| r.is_none() && !c) {
|
||||
return Err(KernelError::Failed("rail".into()));
|
||||
}
|
||||
let wall_rails: Vec<mt::Edge> =
|
||||
(0..k).map(|i| rail(&vb, i, &wall_off)).collect::<Option<_>>().ok_or_else(|| KernelError::Failed("rail".into()))?;
|
||||
// Seams between neighbouring walls at the joints get shorter.
|
||||
@@ -314,14 +421,14 @@ fn build(solid: &Solid, si: usize, faces: &[&mt::Face], fi: usize, wi: usize, n:
|
||||
}
|
||||
// Rebuild faces.
|
||||
let idx_of = |id: mt::EdgeID| loop_ids.iter().position(|x| *x == id);
|
||||
let rebuild = |g: &mt::Face, rails: &[mt::Edge]| -> Result<mt::Face> {
|
||||
let rebuild = |g: &mt::Face, rails: &[Option<mt::Edge>]| -> Result<mt::Face> {
|
||||
let mut wires = Vec::new();
|
||||
for w in g.absolute_boundaries() {
|
||||
let mut es = Vec::new();
|
||||
for e in w.edge_iter() {
|
||||
if let Some(i) = idx_of(e.id()) {
|
||||
// Rails run in loop order; an edge used the other way takes them reversed.
|
||||
let mine: Vec<&mt::Edge> = owner.iter().zip(rails).filter(|(o, _)| **o == i).map(|(_, r)| r).collect();
|
||||
let mine: Vec<&mt::Edge> = owner.iter().zip(rails).filter(|(o, _)| **o == i).filter_map(|(_, r)| r.as_ref()).collect();
|
||||
let same = edges.get(i).is_some_and(|le| le.orientation() == e.orientation());
|
||||
if same {
|
||||
es.extend(mine.into_iter().cloned());
|
||||
@@ -346,7 +453,8 @@ fn build(solid: &Solid, si: usize, faces: &[&mt::Face], fi: usize, wi: usize, n:
|
||||
if gi == fi {
|
||||
out.push(rebuild(g, &face_rails)?);
|
||||
} else if walls.contains(&gi) {
|
||||
out.push(rebuild(g, &wall_rails)?);
|
||||
let wr: Vec<Option<mt::Edge>> = wall_rails.iter().cloned().map(Some).collect();
|
||||
out.push(rebuild(g, &wr)?);
|
||||
} else if g.edge_iter().any(|e| seam_subst.contains_key(&e.id())) {
|
||||
out.push(rebuild(g, &[])?);
|
||||
} else {
|
||||
@@ -362,7 +470,30 @@ fn build(solid: &Solid, si: usize, faces: &[&mt::Face], fi: usize, wi: usize, n:
|
||||
continue;
|
||||
};
|
||||
let swept: mt::Shell = match *s {
|
||||
Seg::Line { a, b } => vec![builder::tsweep(pr, v3(b - a))].into(),
|
||||
Seg::Line { a, b } => {
|
||||
// The cross-section at the start, swept along the whole edge and beyond (the
|
||||
// face may reach past the edge's ends at a mitre).
|
||||
let d = (b - a).normalized().ok_or_else(|| unsupported("degenerate loop edge"))?;
|
||||
let (_, t, sb, _) = frame(i, a).ok_or_else(|| unsupported("frame"))?;
|
||||
let m = into(s, a);
|
||||
let ext = size;
|
||||
let back = d * -ext;
|
||||
let (fa, wa) = (builder::vertex(p3(a + m * sb + back)), builder::vertex(p3(a + t * sb + back)));
|
||||
let base = if round { builder::circle_arc(&fa, &wa, p3(profile_mid(i) + back)) } else { builder::line(&fa, &wa) };
|
||||
vec![builder::tsweep(&base, v3(b - a + d * (2.0 * ext)))].into()
|
||||
}
|
||||
Seg::Arc { .. } if collapsed.get(i).copied().unwrap_or(false) => {
|
||||
// A sphere about the profile's centre, its poles well away from the corner
|
||||
// patch (a pole inside a face's corner confuses the meshing).
|
||||
let c = centre(i);
|
||||
let corners = [from_p3(pr.front().point()), from_p3(pr.back().point()), from_p3(pr_next.back().point())];
|
||||
let g = corners.iter().fold(Vec3::ZERO, |acc, q| acc + (*q - c).normalized().unwrap_or(Vec3::ZERO));
|
||||
let g = g.normalized().ok_or_else(|| unsupported("sphere corner"))?;
|
||||
let w = g.any_perp();
|
||||
let (top, eq, bottom) = (builder::vertex(p3(c + w * r)), p3(c + g * r), builder::vertex(p3(c - w * r)));
|
||||
let meridian = builder::circle_arc(&top, &bottom, eq);
|
||||
builder::rsweep(&meridian, p3(c), v3(w), mt::Rad(std::f64::consts::TAU))
|
||||
}
|
||||
Seg::Arc { c, axis, angle, .. } => builder::rsweep(pr, p3(c), v3(axis), mt::Rad(angle)),
|
||||
};
|
||||
let mut surf = swept.face_iter().next().map(|x| x.oriented_surface()).ok_or_else(|| KernelError::Failed("blend surface".into()))?;
|
||||
@@ -375,7 +506,10 @@ fn build(solid: &Solid, si: usize, faces: &[&mt::Face], fi: usize, wi: usize, n:
|
||||
surf = mt::Invertible::inverse(&surf);
|
||||
}
|
||||
}
|
||||
let w: mt::Wire = vec![fr.inverse(), pr.clone(), wr.clone(), pr_next.inverse()].into();
|
||||
let w: mt::Wire = match fr {
|
||||
Some(fr) => vec![fr.inverse(), pr.clone(), wr.clone(), pr_next.inverse()].into(),
|
||||
None => vec![pr.clone(), wr.clone(), pr_next.inverse()].into(),
|
||||
};
|
||||
let face = mt::Face::try_new(vec![w.clone()], surf.clone()).map_err(|e| KernelError::Failed(format!("blend face: {e}")))?;
|
||||
out.push(face);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user