Sketch Fillet on corners of a line and an arc or two arcs (the tangent arc nearest the corner, curves cut back to it, radius dimension)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
bherbruckandClaude Opus 5.5 committed 2026-10-08 03:43:58 +00:00
1 parent 9a9caa6511
commit 205ddfcf27
2 files changed
+106 -2

No files matched your search

+87 -2
View File
@@ -17,7 +17,7 @@ pub static COMMANDS: &[CommandSpec] = &[
.at("SKETCH", "MODIFY")
.icon("sketch_fillet")
.enabled(in_sketch)
.params("point: corner shared by two lines (\"l1.end\") | a, b: two lines meeting at a corner; radius: expr"),
.params("point: corner shared by two curves (\"l1.end\") | a, b: two lines or arcs meeting at a corner; radius: expr"),
CommandSpec::new("ChamferSketchEqualDistance", "Equal Distance Chamfer", chamfer_equal)
.at("SKETCH", "MODIFY")
.icon("sketch_chamfer")
@@ -483,7 +483,17 @@ fn fillet(s: &mut Session, p: &Value) -> Result<Value> {
if !(r > 1e-9 && r < 1e8) {
return Err(bad(cmd, "radius must be positive"));
}
let (la, lb, c, fa, fb) = corner(sk, p, cmd)?;
let (la, lb, c, fa, fb) = match corner(sk, p, cmd) {
Ok(x) => x,
// Not two lines: a corner with an arc.
Err(e) => {
let Some((arc, cons)) = round_corner(sk, p, r, cmd)? else { return Err(e) };
let mut cons = cons;
let pname = doc.new_model_param(&r_expr, "mm");
cons.push(sk.add_constraint(ConstraintKind::Radius { c: arc, value: r }, Some(pname.clone()))?);
return Ok((ids_of(sk, &[arc]), cons, pname));
}
};
let (pc, pa, pb) = (sk.point(c).unwrap_or_default(), sk.point(fa).unwrap_or_default(), sk.point(fb).unwrap_or_default());
let u = (pa - pc).normalized().ok_or_else(|| bad(cmd, "degenerate line"))?;
let v = (pb - pc).normalized().ok_or_else(|| bad(cmd, "degenerate line"))?;
@@ -510,6 +520,81 @@ fn fillet(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({"curves": out.0, "constraints": out.1, "param": out.2, "sketch": info}))
}
/// Fillet a corner where a line meets an arc, or two arcs meet: the arc of radius `r` touching
/// both, nearest the corner; both curves are cut back to it. None when the corner is not of
/// that kind.
fn round_corner(sk: &mut Sketch, p: &Value, r: f64, cmd: &str) -> Result<Option<(usize, Vec<String>)>> {
let ends = |c: usize| match sk.curves.get(c).map(|c| &c.kind) {
Some(CurveKind::Line { a, b }) | Some(CurveKind::Arc { a, b, .. }) => Some((*a, *b)),
_ => None,
};
let (ca, cb, q) = if let Some(rf) = str_(p, "point") {
let Some(q) = sk.resolve_point(rf) else { return Ok(None) };
let cs: Vec<usize> = (0..sk.curves.len()).filter(|i| ends(*i).is_some_and(|(a, b)| a == q || b == q) && !sk.is_linked_curve(*i)).collect();
match cs[..] {
[a, b] => (a, b, q),
_ => return Ok(None),
}
} else {
let (Some(a), Some(b)) = (str_(p, "a").and_then(|i| sk.curve_index(i)), str_(p, "b").and_then(|i| sk.curve_index(i))) else {
return Ok(None);
};
let (Some((a0, a1)), Some((b0, b1))) = (ends(a), ends(b)) else { return Ok(None) };
let Some(q) = [a0, a1].into_iter().find(|x| *x == b0 || *x == b1) else { return Ok(None) };
(a, b, q)
};
let (Some(sa), Some(sb), Some(pc)) = (sk.shape(ca), sk.shape(cb), sk.point(q)) else { return Ok(None) };
let offsets = |sh: &Shape| -> Vec<Shape> {
match *sh {
Shape::Line { a, b } => match (b - a).normalized() {
Some(d) => [1.0, -1.0].iter().map(|s| Shape::Line { a: a + d.perp() * (r * s), b: b + d.perp() * (r * s) }).collect(),
None => Vec::new(),
},
Shape::Round { c, r: rr, .. } => {
let mut v = vec![Shape::Round { c, r: rr + r, start: 0.0, sweep: TAU }];
if rr - r > 1e-9 {
v.push(Shape::Round { c, r: rr - r, start: 0.0, sweep: TAU });
}
v
}
}
};
let mut best: Option<(f64, Vec2, Vec2, Vec2)> = None;
for oa in offsets(&sa) {
for ob in offsets(&sb) {
for cen in intersections(&oa, &ob) {
let (ta, tb) = (sa.project(cen), sb.project(cen));
if on_bounded(&sa, ta).is_none() || on_bounded(&sb, tb).is_none() || ta.dist(pc) < 1e-9 || tb.dist(pc) < 1e-9 {
continue;
}
let score = ta.dist(pc) + tb.dist(pc);
if best.is_none_or(|b| score < b.0) {
best = Some((score, cen, ta, tb));
}
}
}
}
let (_, cen, ta, tb) = best.ok_or_else(|| bad(cmd, "that radius does not fit the corner"))?;
let (na, nb) = (sk.add_point(ta, None)?, sk.add_point(tb, None)?);
for (c, n) in [(ca, na), (cb, nb)] {
if let Some(cu) = sk.curves.get_mut(c) {
cu.kind.map_points(&|i| if i == q { n } else { i });
}
}
let ccw = (ta - cen).cross(tb - cen) > 0.0;
let arc =
if ccw { sk.add_arc(cen, ta, tb, [None, Some(na), Some(nb)], None)? } else { sk.add_arc(cen, tb, ta, [None, Some(nb), Some(na)], None)? };
let cons = vec![add_c(sk, ConstraintKind::Tangent { a: ca, b: arc })?, add_c(sk, ConstraintKind::Tangent { a: cb, b: arc })?];
let used = sk.curves.iter().any(|x| x.kind.uses(q));
if !used && q != 0 {
let arc_id = sk.curves.get(arc).map(|c| c.id.clone()).unwrap_or_default();
sk.remove_points(&[q]);
let arc = sk.curve_index(&arc_id).unwrap_or(arc);
return Ok(Some((arc, cons)));
}
Ok(Some((arc, cons)))
}
fn chamfer(s: &mut Session, p: &Value, cmd: &str, dists: impl FnOnce(f64) -> Result<(f64, f64)>) -> Result<Value> {
let (out, info) = edit(s, p, cmd, false, |sk, _| {
let (la, lb, c, fa, fb) = corner(sk, p, cmd)?;
@@ -392,3 +392,22 @@ fn ellipse_radii_dimensions() {
let a = profiles(&s)[0];
assert!((a - std::f64::consts::PI * 60.0).abs() < 1e-6, "{a}");
}
#[test]
fn fillet_a_line_and_an_arc() {
let mut s = new_sketch();
// A line into a quarter arc meeting at a sharp corner.
let l = ids(&run(&mut s, "DrawPolyline", json!({"points": [[0, 0], [20, 0]]}))["curves"])[0].clone();
let a = ids(&run(&mut s, "ArcCenterTwoPoint", json!({"center": [30, 0], "start": format!("{l}.end"), "end": [30, -10]}))["curves"])[0].clone();
let _ = a;
let f = run(&mut s, "FilletSketchCmd", json!({"point": format!("{l}.end"), "radius": 2}));
let arc = ids(&f["curves"])[0].clone();
let sk = sketch(&s);
let ai = sk.curve_index(&arc).unwrap();
assert!((sk.radius(ai).unwrap() - 2.0).abs() < 1e-7);
// The fillet touches the line (its centre 2 below it) and the arc's circle (outside it: 10 + 2).
let c = sk.center(ai).unwrap();
assert!((c.y - 2.0).abs() < 1e-6 || (c.y + 2.0).abs() < 1e-6, "{c:?}");
let d = c.dist(Vec2::new(30.0, 0.0));
assert!((d - 12.0).abs() < 1e-6, "{d} {c:?}");
}