refactor(fillet): find the tangent arc in the kernel

FILLET enumerated its own candidates twice, once for a line against an
arc and once for two arcs, each with its own quadratic, its own four
sign combinations and its own conventions about which root meant what.
Both are the same construction: a circle of the given radius tangent to
a curve has its centre on one of that curve's offsets, so the fillet
centre is where an offset of one crosses an offset of the other.

That now comes from the kernel, which does it for any pair of straight
and circular curves and agrees with the corner formula where both apply.
What stays here is the part that genuinely belongs to the command —
which candidate the two picks meant, whether the touch lands on the
drawn part of an arc rather than elsewhere on its circle, and which side
of each entity to keep.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-08-09 01:37:11 +03:00
commit 9babf355b1
3 changed files with 132 additions and 189 deletions

2
Cargo.lock generated
View file

@ -935,7 +935,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]] [[package]]
name = "cadkernel" name = "cadkernel"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/HakanSeven12/cadkernel.git#441e3c6d8a55f3a8e799ef3cbe1e0b4301d66f14" source = "git+https://github.com/HakanSeven12/cadkernel.git#77d8ee9d3ee7f8bfafecbafb0ee3097c4b413ccd"
dependencies = [ dependencies = [
"cavalier_contours", "cavalier_contours",
] ]

View file

@ -15,8 +15,11 @@ use acadrust::entities::{Arc as ArcEnt, Line as LineEnt, LwPolyline};
use super::geom; use super::geom;
use super::geom::{arc_points as arc_pts, line_line as ll, normalize_angle as norm_angle}; use super::geom::{arc_points as arc_pts, line_line as ll, normalize_angle as norm_angle};
use acadrust::kernel::geom2d::{ use acadrust::kernel::geom2d::{
circle_circle_points as circle_circle_pts, fillet_between_rays, line_circle, circle_circle_points as circle_circle_pts, fillet_between_rays, fillets_between, line_circle,
Arc as KernelArc, Curve as KernelCurve, Fillet as KernelFillet,
Line as KernelLine, Tolerance,
}; };
use acadrust::entities::EntityCommon;
use acadrust::types::Vector3; use acadrust::types::Vector3;
use acadrust::{EntityType, Handle}; use acadrust::{EntityType, Handle};
use glam::DVec3; use glam::DVec3;
@ -24,6 +27,11 @@ use crate::t;
const TAU: f64 = std::f64::consts::TAU; const TAU: f64 = std::f64::consts::TAU;
/// How close two positions must be to count as one while a fillet is being
/// placed. Loose enough to survive the rounding in a drawing's stored
/// coordinates, tight enough not to merge two distinct candidate centres.
const FILLET_TOLERANCE: f64 = 1.0e-9;
use crate::command::{CadCommand, CmdResult}; use crate::command::{CadCommand, CmdResult};
use crate::modules::draw::defaults; use crate::modules::draw::defaults;
use crate::modules::IconKind; use crate::modules::IconKind;
@ -677,6 +685,58 @@ fn rebuild_poly_from_trimmed_line(
} }
/// Fillet a Line and an Arc. /// Fillet a Line and an Arc.
/// Fillets of `radius` tangent to both curves, nearest pick first.
///
/// The candidates come from the kernel, which finds them by crossing the two
/// curves' offsets — the construction that works whether or not the curves
/// meet. What is left here is the choice between them, which needs the two
/// pick points and so cannot be made anywhere else.
fn ranked_fillets(
a: &KernelCurve,
click_a: [f64; 2],
b: &KernelCurve,
click_b: [f64; 2],
radius: f64,
) -> Vec<KernelFillet> {
let reach = |from: [f64; 2], to: [f64; 2]| (to[0] - from[0]).hypot(to[1] - from[1]);
let mut found = fillets_between(a, b, radius, Tolerance::new(FILLET_TOLERANCE));
found.sort_by(|first, second| {
let one = reach(click_a, first.tangent1) + reach(click_b, first.tangent2);
let other = reach(click_a, second.tangent1) + reach(click_b, second.tangent2);
one.total_cmp(&other)
});
found
}
/// The entity's shape as a kernel curve.
fn line_curve(l: &LineEnt) -> KernelCurve {
KernelCurve::Line(KernelLine {
start: [l.start.x, l.start.y],
end: [l.end.x, l.end.y],
})
}
fn arc_curve(a: &ArcEnt) -> KernelCurve {
KernelCurve::Arc(KernelArc {
centre: [a.center.x, a.center.y],
radius: a.radius,
start_angle: a.start_angle,
end_angle: a.end_angle,
})
}
/// An arc entity built from a kernel fillet.
fn fillet_entity(fillet: &KernelFillet, radius: f64, z: f64, common: &EntityCommon) -> ArcEnt {
let mut arc = ArcEnt::new();
arc.common = common.clone();
arc.common.handle = Handle::NULL;
arc.center = Vector3::new(fillet.centre[0], fillet.centre[1], z);
arc.radius = radius;
arc.start_angle = fillet.start_angle;
arc.end_angle = fillet.end_angle;
arc
}
fn fillet_line_arc( fn fillet_line_arc(
line: &LineEnt, line: &LineEnt,
click_line: [f64; 2], click_line: [f64; 2],
@ -723,116 +783,47 @@ fn fillet_line_arc(
return Some((EntityType::Line(new_line), EntityType::Arc(new_arc), None)); return Some((EntityType::Line(new_line), EntityType::Arc(new_arc), None));
} }
// For r>0: find the fillet arc center — tangent to both the line and the circle. // The candidates come from the kernel: a circle of `radius` tangent to
// The fillet circle center lies at distance (radius) from the line // both has its centre where an offset of the line crosses an offset of
// and at distance |ar ± radius| from the arc center. // the arc's circle. What was here enumerated the same four combinations
// Sign: outside=ar+radius (external), inside=ar-radius (internal). // by hand, with its own quadratic and its own sign conventions.
let perp_x = -u[1]; for fillet in ranked_fillets(
let perp_y = u[0]; &line_curve(line),
click_line,
let mut best: Option<(EntityType, EntityType, EntityType)> = None; &arc_curve(arc),
let mut best_dist = f64::MAX; click_arc,
radius,
for sign_perp in &[-1.0_f64, 1.0_f64] { ) {
for sign_circle in &[-1.0_f64, 1.0_f64] { // The touch on the arc has to be on the drawn part of it, not on the
// Candidate fillet center offset from line by ±radius in perp direction. // rest of the circle it belongs to.
// Find point on offset line closest to arc center at distance |ar + sign*radius|. let tp_arc_angle = arc_angle_at(ac, fillet.tangent2);
let off_dist = ar + sign_circle * radius;
if off_dist < 1e-9 {
continue;
}
// The fillet center is at distance `off_dist` from the arc center
// and also at distance `radius` from the line (perpendicular).
// Parametrize: fc = p1 + t*u + sign_perp*radius*perp
// |fc - ac|^2 = off_dist^2
// (p1[0] + t*u[0] + sign_perp*radius*perp_x - ac[0])^2 + ...= off_dist^2
let qx = p1[0] + sign_perp * radius * perp_x - ac[0];
let qy = p1[1] + sign_perp * radius * perp_y - ac[1];
// (qx + t*u[0])^2 + (qy + t*u[1])^2 = off_dist^2
let qa = u[0] * u[0] + u[1] * u[1]; // = 1.0
let qb = 2.0 * (qx * u[0] + qy * u[1]);
let qc = qx * qx + qy * qy - off_dist * off_dist;
let disc = qb * qb - 4.0 * qa * qc;
if disc < 0.0 {
continue;
}
let sq = disc.sqrt();
for &sign_t in &[-1.0_f64, 1.0_f64] {
let t_fc = (-qb + sign_t * sq) / (2.0 * qa);
let fc = [
p1[0] + t_fc * u[0] + sign_perp * radius * perp_x,
p1[1] + t_fc * u[1] + sign_perp * radius * perp_y,
];
// Tangent point on the line
let tp_line = [p1[0] + t_fc * u[0], p1[1] + t_fc * u[1]];
// Tangent point on the arc circle
let fd = [(ac[0] - fc[0]), (ac[1] - fc[1])];
let fdl = (fd[0] * fd[0] + fd[1] * fd[1]).sqrt().max(1e-12);
let tp_arc = [ac[0] + fd[0] / fdl * ar, ac[1] + fd[1] / fdl * ar];
// The tangent point on the arc must be within the arc's angular range
let tp_arc_angle = arc_angle_at(ac, tp_arc);
let tp_arc_clamped = clamp_angle_to_arc(tp_arc_angle, a_start, a_end); let tp_arc_clamped = clamp_angle_to_arc(tp_arc_angle, a_start, a_end);
if (norm_angle(tp_arc_angle) - norm_angle(tp_arc_clamped)).abs() > 0.01 { if (norm_angle(tp_arc_angle) - norm_angle(tp_arc_clamped)).abs() > 0.01 {
continue; continue;
} }
let Some(new_line) = trim_line_to_point(line, fillet.tangent1, click_line) else {
// The tangent point on the line must be on the correct side of the click
// (prefer the intersection closest to the click)
let dist_to_click_line =
(tp_line[0] - click_line[0]).hypot(tp_line[1] - click_line[1]);
let dist_to_click_arc = (tp_arc[0] - click_arc[0]).hypot(tp_arc[1] - click_arc[1]);
let dist_total = dist_to_click_line + dist_to_click_arc;
if dist_total >= best_dist {
continue;
}
// Build trimmed line
let Some(new_line) = trim_line_to_point(line, tp_line, click_line) else {
continue; continue;
}; };
// Keep the side of the arc the click is on.
// Build trimmed arc let arc_click_rel = (arc_angle_at(ac, click_arc) - a_start).rem_euclid(TAU);
let arc_click_angle = arc_angle_at(ac, click_arc);
let arc_click_rel = (arc_click_angle - a_start).rem_euclid(TAU);
let tp_arc_rel = (tp_arc_clamped - a_start).rem_euclid(TAU); let tp_arc_rel = (tp_arc_clamped - a_start).rem_euclid(TAU);
let new_arc = if tp_arc_rel <= arc_click_rel { let new_arc = if tp_arc_rel <= arc_click_rel {
trim_arc(arc, tp_arc_clamped, a_end) trim_arc(arc, tp_arc_clamped, a_end)
} else { } else {
trim_arc(arc, a_start, tp_arc_clamped) trim_arc(arc, a_start, tp_arc_clamped)
}; };
return Some((
// Build fillet arc angles
let fa_line = arc_angle_at(fc, tp_line);
let fa_arc = arc_angle_at(fc, tp_arc);
let cross = (tp_line[0] - fc[0]) * (tp_arc[1] - fc[1])
- (tp_line[1] - fc[1]) * (tp_arc[0] - fc[0]);
let (fstart, fend) = if cross >= 0.0 {
(fa_line, fa_arc)
} else {
(fa_arc, fa_line)
};
let mut fillet_arc = ArcEnt::new();
fillet_arc.common = line.common.clone();
fillet_arc.common.handle = Handle::NULL;
fillet_arc.center = Vector3::new(fc[0], fc[1], z);
fillet_arc.radius = radius;
fillet_arc.start_angle = norm_angle(fstart);
fillet_arc.end_angle = norm_angle(fend);
best_dist = dist_total;
best = Some((
EntityType::Line(new_line), EntityType::Line(new_line),
EntityType::Arc(new_arc), EntityType::Arc(new_arc),
EntityType::Arc(fillet_arc), Some(EntityType::Arc(fillet_entity(
&fillet,
radius,
z,
&line.common,
))),
)); ));
} }
} None
}
best.map(|(l, a, f)| (l, a, Some(f)))
} }
/// Fillet two arcs. /// Fillet two arcs.
@ -884,52 +875,22 @@ fn fillet_arc_arc(
return Some((EntityType::Arc(new_a1), EntityType::Arc(new_a2), None)); return Some((EntityType::Arc(new_a1), EntityType::Arc(new_a2), None));
} }
// For r>0: find a circle of `radius` tangent to both arc circles. // Same construction as the line-and-arc case, from the same place: the
// Center lies at |r1 ± radius| from c1 and |r2 ± radius| from c2. // centre is where an offset of one circle crosses an offset of the other.
let mut best: Option<(EntityType, EntityType, EntityType)> = None; for fillet in ranked_fillets(&arc_curve(a1), click1, &arc_curve(a2), click2, radius) {
let mut best_dist = f64::MAX; // Both touches have to be on the drawn parts of their arcs.
let tp1_angle = arc_angle_at(c1, fillet.tangent1);
for sign1 in &[-1.0_f64, 1.0_f64] { let tp2_angle = arc_angle_at(c2, fillet.tangent2);
let d1 = r1 + sign1 * radius; let tc1 = clamp_angle_to_arc(tp1_angle, s1, e1);
if d1 < 1e-9 { let tc2 = clamp_angle_to_arc(tp2_angle, s2, e2);
continue; if (norm_angle(tp1_angle) - norm_angle(tc1)).abs() > 0.01
} || (norm_angle(tp2_angle) - norm_angle(tc2)).abs() > 0.01
for sign2 in &[-1.0_f64, 1.0_f64] { {
let d2 = r2 + sign2 * radius;
if d2 < 1e-9 {
continue;
}
for fc in circle_circle_pts(c1, d1, c2, d2) {
// Tangent points on each arc
let fd1 = [(c1[0] - fc[0]), (c1[1] - fc[1])];
let fdl1 = (fd1[0] * fd1[0] + fd1[1] * fd1[1]).sqrt().max(1e-12);
let tp1 = [c1[0] + fd1[0] / fdl1 * r1, c1[1] + fd1[1] / fdl1 * r1];
let fd2 = [(c2[0] - fc[0]), (c2[1] - fc[1])];
let fdl2 = (fd2[0] * fd2[0] + fd2[1] * fd2[1]).sqrt().max(1e-12);
let tp2 = [c2[0] + fd2[0] / fdl2 * r2, c2[1] + fd2[1] / fdl2 * r2];
// Tangent points must lie within respective arc ranges
let tp1a = arc_angle_at(c1, tp1);
let tp2a = arc_angle_at(c2, tp2);
let tc1 = clamp_angle_to_arc(tp1a, s1, e1);
let tc2 = clamp_angle_to_arc(tp2a, s2, e2);
if (norm_angle(tp1a) - norm_angle(tc1)).abs() > 0.01 {
continue;
}
if (norm_angle(tp2a) - norm_angle(tc2)).abs() > 0.01 {
continue;
}
let dist_total = (tp1[0] - click1[0]).hypot(tp1[1] - click1[1])
+ (tp2[0] - click2[0]).hypot(tp2[1] - click2[1]);
if dist_total >= best_dist {
continue; continue;
} }
let ca1 = clamp_angle_to_arc(arc_angle_at(c1, click1), s1, e1); let ca1 = clamp_angle_to_arc(arc_angle_at(c1, click1), s1, e1);
let ca2 = clamp_angle_to_arc(arc_angle_at(c2, click2), s2, e2); let ca2 = clamp_angle_to_arc(arc_angle_at(c2, click2), s2, e2);
let new_a1 = if (tc1 - s1).rem_euclid(TAU) <= (ca1 - s1).rem_euclid(TAU) { let new_a1 = if (tc1 - s1).rem_euclid(TAU) <= (ca1 - s1).rem_euclid(TAU) {
trim_arc(a1, tc1, e1) trim_arc(a1, tc1, e1)
} else { } else {
@ -940,31 +901,13 @@ fn fillet_arc_arc(
} else { } else {
trim_arc(a2, s2, tc2) trim_arc(a2, s2, tc2)
}; };
return Some((
let fa1 = arc_angle_at(fc, tp1);
let fa2 = arc_angle_at(fc, tp2);
let cross =
(tp1[0] - fc[0]) * (tp2[1] - fc[1]) - (tp1[1] - fc[1]) * (tp2[0] - fc[0]);
let (fstart, fend) = if cross >= 0.0 { (fa1, fa2) } else { (fa2, fa1) };
let mut fillet_arc = ArcEnt::new();
fillet_arc.common = a1.common.clone();
fillet_arc.common.handle = Handle::NULL;
fillet_arc.center = Vector3::new(fc[0], fc[1], z);
fillet_arc.radius = radius;
fillet_arc.start_angle = norm_angle(fstart);
fillet_arc.end_angle = norm_angle(fend);
best_dist = dist_total;
best = Some((
EntityType::Arc(new_a1), EntityType::Arc(new_a1),
EntityType::Arc(new_a2), EntityType::Arc(new_a2),
EntityType::Arc(fillet_arc), Some(EntityType::Arc(fillet_entity(&fillet, radius, z, &a1.common))),
)); ));
} }
} None
}
best.map(|(a1, a2, f)| (a1, a2, Some(f)))
} }
/// Trim a line endpoint nearest to the intersection point, keeping the click side. /// Trim a line endpoint nearest to the intersection point, keeping the click side.

View file

@ -1,6 +1,6 @@
use super::*; use super::*;
use acadrust::kernel::geom2d::{bounded_faces, Line}; use acadrust::kernel::geom2d::{bounded_faces, Line, Tolerance};
/// How far apart two points may be and still be taken for the same one. /// How far apart two points may be and still be taken for the same one.
/// ///
@ -65,6 +65,6 @@ impl Scene {
} }
} }
bounded_faces(&segments, WELD_TOLERANCE) bounded_faces(&segments, Tolerance::new(WELD_TOLERANCE))
} }
} }