Fillet/chamfer: point the drag arrow into the corner on inside edges (#1394)

* Fillet/chamfer: point the drag arrow into the corner on inside edges (#1384)

The radius/distance arrow always pointed against the faces' outward
bisector, which is into the body on an outside edge but away from the
corner on an inside one, where the blend adds material. The arrow and
the spline radius handles now take each edge's convexity into account,
so dragging out along the arrow grows the value on both kinds of edge,
and each edge of a mixed chain follows its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Tests: no unreachable! in the blend arrow helper

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bherbruckandClaude Opus 5.5 authored and GitHub committed 2026-10-15 08:58:51 -04:00
1 parent ad30a2350e
commit 6eaf7458ab
3 files changed
+154 -19

No files matched your search

+49 -19
View File
@@ -116,7 +116,13 @@ pub fn anchor(app: &SolveApp, d: &Dialog) -> Option<(Vec3, Option<Vec3>)> {
(at, normal.or(dir.map(|d| -d)))
}
Kind::Section { .. } | Kind::OffsetPlane { .. } | Kind::OffsetFaces { .. } => (at, normal),
// Fillet radii, chamfer distances and shell thickness grow into the body.
// Fillet radii and chamfer distances grow the way `blend_grow_dir` gives (into the body
// on an outside edge, into the open corner on an inside one); shell thickness grows into
// the body.
Kind::Fillet { .. } if matches!(first, Sel::Edge { .. }) => {
let Sel::Edge { body, index, .. } = first else { return None };
(at, st.body(body).and_then(|b| blend_grow_dir(&b.mesh(), *index, at)))
}
Kind::Fillet { .. } | Kind::Shell { .. } => (at, normal.map(|n| -n)),
Kind::Sheet(_) => (at, crate::dialogs_sheet::arrow(app, d).or(normal)),
Kind::Plastic(_) => (at, normal),
@@ -178,23 +184,46 @@ fn reference_height(app: &SolveApp, d: &Dialog, label: &str, plane: &solidcraft_
Some(solidcraft_engine::doc::object_height(&app.session.world_state(), plane, centre, p))
}
/// The outward direction between the two faces at an edge (a fillet's radius arrow points the
/// other way, into the body).
fn edge_bisector(m: &Mesh, index: usize, p: Vec3) -> Option<Vec3> {
/// The two faces at an edge, near `p`: for each, the normal and centroid of its triangle nearest
/// `p`.
fn edge_sides(m: &Mesh, index: usize, p: Vec3) -> Option<Vec<(Vec3, Vec3)>> {
let faces = m.edge_faces.get(index)?;
let mut sum = Vec3::ZERO;
for f in faces {
let n = m
.triangles
.iter()
.zip(&m.tri_face)
.filter(|(_, tf)| *tf == f)
.filter_map(|(t, _)| m.tri(t))
.min_by(|a, b| ((a[0] + a[1] + a[2]) / 3.0).dist(p).total_cmp(&((b[0] + b[1] + b[2]) / 3.0).dist(p)))
.and_then(|[a, b, c]| (b - a).cross(c - a).normalized())?;
sum += n;
}
sum.normalized()
faces
.iter()
.map(|f| {
m.triangles
.iter()
.zip(&m.tri_face)
.filter(|(_, tf)| *tf == f)
.filter_map(|(t, _)| m.tri(t))
.min_by(|a, b| ((a[0] + a[1] + a[2]) / 3.0).dist(p).total_cmp(&((b[0] + b[1] + b[2]) / 3.0).dist(p)))
.and_then(|[a, b, c]| Some(((b - a).cross(c - a).normalized()?, (a + b + c) / 3.0)))
})
.collect()
}
/// The outward direction between the two faces at an edge: the mean of their normals.
fn edge_bisector(m: &Mesh, index: usize, p: Vec3) -> Option<Vec3> {
edge_sides(m, index, p)?.iter().fold(Vec3::ZERO, |sum, (n, _)| sum + *n).normalized()
}
/// Whether the edge is an inside (concave) one: each face near it lies in front of the other's
/// plane, where on an outside edge it lies behind. Only the offset across the edge counts, since
/// each normal is square to the edge.
fn edge_concave(m: &Mesh, index: usize, p: Vec3) -> Option<bool> {
let sides = edge_sides(m, index, p)?;
let [(n1, c1), (n2, c2)] = sides.as_slice() else { return None };
let s = (*c1 - p).dot(*n2) + (*c2 - p).dot(*n1);
Some(s > 0.0)
}
/// The direction a fillet's radius or a chamfer's distance grows from an edge, along the bisector
/// of its faces: into the body on an outside edge, where the blend takes material away, and out
/// into the open corner on an inside edge, where it adds material (#1384). Dragging along it
/// increases the value either way.
pub(crate) fn blend_grow_dir(m: &Mesh, index: usize, p: Vec3) -> Option<Vec3> {
let n = edge_bisector(m, index, p)?;
Some(if edge_concave(m, index, p).unwrap_or(false) { n } else { -n })
}
/// Whether a dynamically registered canvas handle owns this point before navigation.
@@ -609,8 +638,9 @@ fn radius_spline_handles(app: &SolveApp, ui: &mut egui::Ui, painter: &egui::Pain
let (spline, start) = law.as_ref()?;
let other = if start.dist(*a) <= start.dist(*b) { *b } else { *a };
let mid = *start + (other - *start) * 0.5;
// Into the body, between the two faces (the edge is straight between planar faces).
let n = -edge_bisector(mesh, *index, mid)?;
// Between the two faces, the way this edge's radius grows: into the body on an outside
// edge, into the corner on an inside one (the edge is straight between planar faces).
let n = blend_grow_dir(mesh, *index, mid)?;
let curve = (0..=48)
.filter_map(|k| {
let u = f64::from(k) / 48.0;