fix(fillet): keep no-trim for every pair and the Trim prompt; share TRIMMODE with CHAMFER

#1692 limited no-trim FILLET to two lines and refused arcs and polyline
segments, which #1558 already filleted without trimming (only the arc is
added). It also turned T/N at the object prompts into silent toggles, so
the Trim option no longer reached its Trim/No trim prompt and the choice
was not saved. Keep the arc bookkeeping it added (the new arc can be
picked next, and stays on the picked layer), restore every pair and the
Trim prompt.

CHAMFER's new no-trim mode starts from and saves the same TRIMMODE value
as FILLET instead of always starting trimmed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 5.5 committed 2026-10-11 23:34:09 +03:00
1 parent 6f2d43820f
commit b9ef8c50ed
1 file changed
+19 -92
+19 -92
View File
@@ -794,16 +794,6 @@ impl FilletEntity {
} }
} }
/// The refusal a no-trim FILLET prints for anything but two independent Lines.
const NO_TRIM_LINES_ONLY: &str = "No-trim fillet supports two independent lines only.";
/// No-trim FILLET is defined for two independent Lines only: the addition path
/// keeps both sources untouched, which the arc / polyline branches cannot
/// express without falling back to trimming.
fn no_trim_pair_supported(first: &FilletEntity, second: &FilletEntity) -> bool {
matches!(first, FilletEntity::Line(_)) && matches!(second, FilletEntity::Line(_))
}
/// Compute FILLET between two entities (Line, Arc, or LwPolyline segment). /// Compute FILLET between two entities (Line, Arc, or LwPolyline segment).
/// Returns (trimmed_e1, trimmed_e2, optional_fillet_arc). /// Returns (trimmed_e1, trimmed_e2, optional_fillet_arc).
/// For same-poly corner fillet the two returned entities are identical (the rebuilt poly). /// For same-poly corner fillet the two returned entities are identical (the rebuilt poly).
@@ -1489,8 +1479,8 @@ enum FilletStep {
pub struct FilletCommand { pub struct FilletCommand {
radius: f64, radius: f64,
/// Whether the picked source entities are shortened to the tangent points. /// Whether the picked source entities are shortened to the tangent points
/// No-trim is defined for two independent Lines only. /// (the shared TRIMMODE, changed through the Trim option).
trim: bool, trim: bool,
step: FilletStep, step: FilletStep,
all_entities: Vec<EntityType>, all_entities: Vec<EntityType>,
@@ -1658,22 +1648,11 @@ impl CadCommand for FilletCommand {
} }
opts.push(CmdOption::new("Polyline", "P")); opts.push(CmdOption::new("Polyline", "P"));
opts.push(CmdOption::new("Radius", "R")); opts.push(CmdOption::new("Radius", "R"));
opts.push(if self.trim { opts.push(CmdOption::new("Trim", "T"));
CmdOption::new("No trim", "N")
} else {
CmdOption::new("Trim", "T")
});
opts opts
} }
FilletStep::Second { .. } => { FilletStep::Second { .. } => {
vec![ vec![CmdOption::new("Radius", "R"), CmdOption::new("Trim", "T")]
CmdOption::new("Radius", "R"),
if self.trim {
CmdOption::new("No trim", "N")
} else {
CmdOption::new("Trim", "T")
},
]
} }
FilletStep::TrimMode => { FilletStep::TrimMode => {
vec![CmdOption::new("Trim", "T"), CmdOption::new("No trim", "N")] vec![CmdOption::new("Trim", "T"), CmdOption::new("No trim", "N")]
@@ -1738,14 +1717,6 @@ impl CadCommand for FilletCommand {
FilletStep::First | FilletStep::Second { .. } => { FilletStep::First | FilletStep::Second { .. } => {
let t = text.trim(); let t = text.trim();
let upper = t.to_uppercase(); let upper = t.to_uppercase();
if matches!(upper.as_str(), "N" | "NOTRIM" | "NO TRIM" | "TRIM=FALSE") {
self.trim = false;
return Some(CmdResult::NeedPoint);
}
if matches!(upper.as_str(), "T" | "TRIM" | "TRIM=TRUE") {
self.trim = true;
return Some(CmdResult::NeedPoint);
}
if matches!(self.step, FilletStep::First) && upper == "P" { if matches!(self.step, FilletStep::First) && upper == "P" {
self.step = FilletStep::Polyline; self.step = FilletStep::Polyline;
return Some(CmdResult::NeedPoint); return Some(CmdResult::NeedPoint);
@@ -1849,11 +1820,6 @@ impl CadCommand for FilletCommand {
other => FilletEntity::from_entity(other), other => FilletEntity::from_entity(other),
}); });
if let Some(e) = e1 { if let Some(e) = e1 {
// Do not let no-trim silently fall through to the normal
// trimming path for a first object it cannot preserve.
if !self.trim && !matches!(e, FilletEntity::Line(_)) {
return CmdResult::ReportError(NO_TRIM_LINES_ONLY.to_string());
}
self.step = FilletStep::Second { self.step = FilletStep::Second {
h1: handle, h1: handle,
e1: e, e1: e,
@@ -1888,15 +1854,9 @@ impl CadCommand for FilletCommand {
if let Some(e2) = e2 { if let Some(e2) = e2 {
match compute_fillet_entities(&e1, click1, &e2, click, self.radius) { match compute_fillet_entities(&e1, click1, &e2, click, self.radius) {
Some((new_e1, new_e2, maybe_arc)) => { Some((new_e1, new_e2, maybe_arc)) => {
// In no-trim mode only two independent Lines are // No trim: the objects stay as they are; only the
// supported. Reuse the solver for the same arc // arc is added (#1558).
// branch, then discard its shortened-line results.
if !self.trim { if !self.trim {
if !no_trim_pair_supported(&e1, &e2) {
self.step = FilletStep::First;
self.resume_second = None;
return CmdResult::ReportError(NO_TRIM_LINES_ONLY.to_string());
}
return maybe_arc return maybe_arc
.map(|arc| self.continue_after_addition(arc)) .map(|arc| self.continue_after_addition(arc))
.unwrap_or(CmdResult::NeedPoint); .unwrap_or(CmdResult::NeedPoint);
@@ -1915,10 +1875,6 @@ impl CadCommand for FilletCommand {
} }
None => CmdResult::NeedPoint, None => CmdResult::NeedPoint,
} }
} else if !self.trim {
self.step = FilletStep::First;
self.resume_second = None;
CmdResult::ReportError(NO_TRIM_LINES_ONLY.to_string())
} else { } else {
CmdResult::NeedPoint CmdResult::NeedPoint
} }
@@ -1991,9 +1947,6 @@ impl CadCommand for FilletCommand {
// For non-LwPoly, skip same-entity hover. // For non-LwPoly, skip same-entity hover.
let _ = h1; let _ = h1;
if let Some(e2) = e2 { if let Some(e2) = e2 {
if !self.trim && !no_trim_pair_supported(&e1, &e2) {
return vec![];
}
if let Some((new_e1, new_e2, maybe_arc)) = if let Some((new_e1, new_e2, maybe_arc)) =
compute_fillet_entities(&e1, click1, &e2, click, self.radius) compute_fillet_entities(&e1, click1, &e2, click, self.radius)
{ {
@@ -2198,8 +2151,9 @@ const NO_TRIM_CHAMFER_LINES_ONLY: &str =
pub struct ChamferCommand { pub struct ChamferCommand {
dist1: f64, dist1: f64,
dist2: f64, dist2: f64,
/// Whether the picked source entities are shortened to the chamfer points. /// Whether the picked source entities are shortened to the chamfer points
/// No-trim is defined for two independent Lines only. /// (the TRIMMODE FILLET shares). No-trim is defined for two independent
/// Lines only.
trim: bool, trim: bool,
step: ChamferStep, step: ChamferStep,
all_entities: Vec<EntityType>, all_entities: Vec<EntityType>,
@@ -2224,7 +2178,7 @@ impl ChamferCommand {
Self { Self {
dist1: dist as f64, dist1: dist as f64,
dist2: defaults::get_chamfer_dist2(), dist2: defaults::get_chamfer_dist2(),
trim: true, trim: defaults::get_fillet_trim(),
step: ChamferStep::First, step: ChamferStep::First,
all_entities, all_entities,
entity_index, entity_index,
@@ -2443,10 +2397,12 @@ impl CadCommand for ChamferCommand {
let upper = t.to_uppercase(); let upper = t.to_uppercase();
if matches!(upper.as_str(), "N" | "NOTRIM" | "NO TRIM" | "TRIM=FALSE") { if matches!(upper.as_str(), "N" | "NOTRIM" | "NO TRIM" | "TRIM=FALSE") {
self.trim = false; self.trim = false;
defaults::set_fillet_trim(false);
return Some(CmdResult::NeedPoint); return Some(CmdResult::NeedPoint);
} }
if matches!(upper.as_str(), "T" | "TRIM" | "TRIM=TRUE") { if matches!(upper.as_str(), "T" | "TRIM" | "TRIM=TRUE") {
self.trim = true; self.trim = true;
defaults::set_fillet_trim(true);
return Some(CmdResult::NeedPoint); return Some(CmdResult::NeedPoint);
} }
if matches!(self.step, ChamferStep::First) { if matches!(self.step, ChamferStep::First) {
@@ -2804,16 +2760,16 @@ mod tests {
1.0, 1.0,
vec![line(0.0, 0.0, 10.0, 0.0, 1), line(0.0, 0.0, 0.0, 10.0, 2)], vec![line(0.0, 0.0, 10.0, 0.0, 1), line(0.0, 0.0, 0.0, 10.0, 2)],
); );
assert_eq!(keywords(&command), ["P", "R", "N"]); assert_eq!(keywords(&command), ["P", "R", "T"]);
assert!(command.on_text_input("U").is_none(), "nothing to undo yet"); assert!(command.on_text_input("U").is_none(), "nothing to undo yet");
command.on_entity_pick(Handle::new(1), DVec3::new(5.0, 0.0, 0.0)); command.on_entity_pick(Handle::new(1), DVec3::new(5.0, 0.0, 0.0));
assert!(matches!( assert!(matches!(
command.on_entity_pick(Handle::new(2), DVec3::new(0.0, 5.0, 0.0)), command.on_entity_pick(Handle::new(2), DVec3::new(0.0, 5.0, 0.0)),
CmdResult::ReplaceManyContinue(_) CmdResult::ReplaceManyContinue(_)
)); ));
assert_eq!(keywords(&command), ["U", "P", "R", "N"]); assert_eq!(keywords(&command), ["U", "P", "R", "T"]);
assert!(matches!(command.on_text_input("U"), Some(CmdResult::UndoDocument))); assert!(matches!(command.on_text_input("U"), Some(CmdResult::UndoDocument)));
assert_eq!(keywords(&command), ["P", "R", "N"]); assert_eq!(keywords(&command), ["P", "R", "T"]);
// The host hands the restored document back; the cache follows it. // The host hands the restored document back; the cache follows it.
let mut doc = codec::CadDocument::new(); let mut doc = codec::CadDocument::new();
let _ = doc.add_entity(line(0.0, 0.0, 20.0, 0.0, 7)); let _ = doc.add_entity(line(0.0, 0.0, 20.0, 0.0, 7));
@@ -2831,8 +2787,7 @@ mod tests {
line(0.0, 0.0, 0.0, 10.0, 2), line(0.0, 0.0, 0.0, 10.0, 2),
]; ];
let mut command = FilletCommand::new(1.0, original); let mut command = FilletCommand::new(1.0, original);
assert!(matches!(command.on_text_input("N"), Some(CmdResult::NeedPoint))); command.trim = false;
assert!(!command.trim);
assert!(matches!( assert!(matches!(
command.on_entity_pick(first, DVec3::new(5.0, 0.0, 0.0)), command.on_entity_pick(first, DVec3::new(5.0, 0.0, 0.0)),
CmdResult::NeedPoint CmdResult::NeedPoint
@@ -2877,34 +2832,6 @@ mod tests {
assert_eq!(source_lines[1], (Vector3::new(0.0, 0.0, 0.0), Vector3::new(0.0, 10.0, 0.0))); assert_eq!(source_lines[1], (Vector3::new(0.0, 0.0, 0.0), Vector3::new(0.0, 10.0, 0.0)));
} }
#[test]
fn no_trim_fillet_refuses_non_line_first_picks() {
let mut arc = ArcEnt::new();
arc.center = Vector3::new(10.0, 10.0, 0.0);
arc.radius = 5.0;
arc.start_angle = std::f64::consts::PI;
arc.end_angle = std::f64::consts::PI * 1.5;
arc.common.handle = Handle::new(2);
let mut command = FilletCommand::new(
1.0,
vec![
line(0.0, 10.0, 20.0, 10.0, 1),
EntityType::Arc(arc),
],
);
assert!(matches!(command.on_text_input("N"), Some(CmdResult::NeedPoint)));
assert!(matches!(
command.on_entity_pick(Handle::new(2), DVec3::new(10.0, 5.0, 0.0)),
CmdResult::ReportError(_)
));
assert!(matches!(command.step, FilletStep::First));
assert!(matches!(
command.on_entity_pick(Handle::new(1), DVec3::new(5.0, 10.0, 0.0)),
CmdResult::NeedPoint
));
assert!(matches!(command.step, FilletStep::Second { .. }));
}
#[test] #[test]
fn chamfer_multiple_keeps_going_and_single_ends() { fn chamfer_multiple_keeps_going_and_single_ends() {
let lines = || vec![line(0.0, 0.0, 10.0, 0.0, 1), line(0.0, 0.0, 0.0, 10.0, 2)]; let lines = || vec![line(0.0, 0.0, 10.0, 0.0, 1), line(0.0, 0.0, 0.0, 10.0, 2)];
@@ -2943,7 +2870,7 @@ mod tests {
let mut single = ChamferCommand::new(1.0, lines()); let mut single = ChamferCommand::new(1.0, lines());
single.dist2 = 1.0; single.dist2 = 1.0;
assert!(matches!(single.on_text_input("N"), Some(CmdResult::NeedPoint))); single.trim = false;
assert!(!single.trim); assert!(!single.trim);
assert!(matches!( assert!(matches!(
single.on_entity_pick(first, DVec3::new(5.0, 0.0, 0.0)), single.on_entity_pick(first, DVec3::new(5.0, 0.0, 0.0)),
@@ -2973,7 +2900,7 @@ mod tests {
let mut multi = ChamferCommand::new(1.0, lines()); let mut multi = ChamferCommand::new(1.0, lines());
multi.dist2 = 1.0; multi.dist2 = 1.0;
assert!(matches!(multi.on_text_input("M"), Some(CmdResult::NeedPoint))); assert!(matches!(multi.on_text_input("M"), Some(CmdResult::NeedPoint)));
assert!(matches!(multi.on_text_input("N"), Some(CmdResult::NeedPoint))); multi.trim = false;
assert!(matches!( assert!(matches!(
multi.on_entity_pick(first, DVec3::new(5.0, 0.0, 0.0)), multi.on_entity_pick(first, DVec3::new(5.0, 0.0, 0.0)),
CmdResult::NeedPoint CmdResult::NeedPoint
@@ -3011,7 +2938,7 @@ mod tests {
poly.add_point(codec::types::Vector2::new(10.0, 10.0)); poly.add_point(codec::types::Vector2::new(10.0, 10.0));
poly.common.handle = Handle::new(3); poly.common.handle = Handle::new(3);
let mut command = ChamferCommand::new(1.0, vec![EntityType::LwPolyline(poly)]); let mut command = ChamferCommand::new(1.0, vec![EntityType::LwPolyline(poly)]);
assert!(matches!(command.on_text_input("N"), Some(CmdResult::NeedPoint))); command.trim = false;
assert!(matches!( assert!(matches!(
command.on_entity_pick(Handle::new(3), DVec3::new(5.0, 0.0, 0.0)), command.on_entity_pick(Handle::new(3), DVec3::new(5.0, 0.0, 0.0)),
CmdResult::ReportError(_) CmdResult::ReportError(_)