diff --git a/src/modules/draw/modify/fillet.rs b/src/modules/draw/modify/fillet.rs index f3a6608d4..574af4615 100644 --- a/src/modules/draw/modify/fillet.rs +++ b/src/modules/draw/modify/fillet.rs @@ -2139,9 +2139,16 @@ enum ChamferStep { }, } +/// The refusal a no-trim CHAMFER prints for anything but two independent Lines. +const NO_TRIM_CHAMFER_LINES_ONLY: &str = + "No-trim chamfer supports two independent lines only."; + pub struct ChamferCommand { dist1: f64, dist2: f64, + /// Whether the picked source entities are shortened to the chamfer points. + /// No-trim is defined for two independent Lines only. + trim: bool, step: ChamferStep, all_entities: Vec, entity_index: ModifyEntityIndex, @@ -2165,6 +2172,7 @@ impl ChamferCommand { Self { dist1: dist as f64, dist2: defaults::get_chamfer_dist2(), + trim: true, step: ChamferStep::First, all_entities, entity_index, @@ -2221,6 +2229,20 @@ impl ChamferCommand { CmdResult::ReplaceManyContinue(replacements) } + /// No-trim mode keeps both source lines and commits only the chamfer line. + fn continue_after_addition(&mut self, addition: EntityType) -> CmdResult { + self.all_entities.push(addition.clone()); + self.entity_index = ModifyEntityIndex::build(&self.all_entities); + self.step = ChamferStep::First; + self.resume_pick = None; + self.made += 1; + if self.multiple { + CmdResult::CommitEntities(vec![addition]) + } else { + CmdResult::CommitEntitiesAndExit(vec![addition]) + } + } + fn undo_last(&mut self) -> Option { if self.made == 0 { return None; @@ -2291,10 +2313,22 @@ impl CadCommand for ChamferCommand { if !self.multiple { opts.push(CmdOption::new("Multiple", "M")); } + opts.push(if self.trim { + CmdOption::new("No trim", "N") + } else { + CmdOption::new("Trim", "T") + }); opts } ChamferStep::Second { .. } | ChamferStep::SecondPoly { .. } => { - vec![CmdOption::new("Distance", "D")] + vec![ + CmdOption::new("Distance", "D"), + if self.trim { + CmdOption::new("No trim", "N") + } else { + CmdOption::new("Trim", "T") + }, + ] } ChamferStep::WaitingForDist1 | ChamferStep::WaitingForDist2 => vec![], } @@ -2357,6 +2391,14 @@ impl CadCommand for ChamferCommand { | ChamferStep::SecondPoly { .. } => { let t = text.trim(); 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, ChamferStep::First) { match upper.as_str() { "M" | "MULTIPLE" => { @@ -2416,6 +2458,27 @@ impl CadCommand for ChamferCommand { self.resume_pick = None; } + fn preserve_commit_layer(&self) -> bool { + // A no-trim chamfer is derived from the picked lines; keep it on their + // layer instead of moving it to the current creation layer. + true + } + + fn on_entities_committed(&mut self, entities: &[EntityType]) { + let mut handles = entities.iter().map(|entity| entity.common().handle); + for entity in self + .all_entities + .iter_mut() + .filter(|entity| entity.common().handle.is_null()) + { + let Some(handle) = handles.next() else { + break; + }; + entity.common_mut().handle = handle; + } + self.entity_index = ModifyEntityIndex::build(&self.all_entities); + } + fn needs_entity_pick(&self) -> bool { !matches!( self.step, @@ -2445,6 +2508,11 @@ impl CadCommand for ChamferCommand { }; } Some(EntityType::LwPolyline(p)) => { + if !self.trim { + return CmdResult::ReportError( + NO_TRIM_CHAMFER_LINES_ONLY.to_string(), + ); + } self.step = ChamferStep::SecondPoly { h1: handle, poly: p.clone(), @@ -2456,6 +2524,15 @@ impl CadCommand for ChamferCommand { CmdResult::NeedPoint } ChamferStep::SecondPoly { h1, poly, click1 } => { + // A no-trim chamfer cannot preserve a polyline corner as two + // source entities; refuse rather than trimming implicitly. + if !self.trim { + self.step = ChamferStep::First; + self.resume_pick = None; + return CmdResult::ReportError( + NO_TRIM_CHAMFER_LINES_ONLY.to_string(), + ); + } let h1 = *h1; let poly = poly.clone(); let click1 = *click1; @@ -2490,6 +2567,11 @@ impl CadCommand for ChamferCommand { if let Some(l2) = l2 { match compute_chamfer(&l1, click1, self.dist1, &l2, click, self.dist2) { Some((new_l1, new_l2, chamfer_line)) => { + // No-trim keeps both source lines and adds only the + // derived chamfer line. + if !self.trim { + return self.continue_after_addition(chamfer_line); + } // Single mode adds the chamfer line as a new entity; // Multiple folds it into the first line's replacement // so the command can carry on. @@ -2507,6 +2589,10 @@ impl CadCommand for ChamferCommand { } None => CmdResult::NeedPoint, } + } else if !self.trim { + self.step = ChamferStep::First; + self.resume_pick = None; + CmdResult::ReportError(NO_TRIM_CHAMFER_LINES_ONLY.to_string()) } else { CmdResult::NeedPoint } @@ -2559,6 +2645,14 @@ impl CadCommand for ChamferCommand { if let Some((new_l1, new_l2, cline)) = compute_chamfer(&l1, click1, self.dist1, &l2, click, self.dist2) { + if !self.trim { + return vec![WireModel::solid( + "chamfer_line".into(), + entity_pts(&cline), + WireModel::CYAN, + false, + )]; + } return vec![ WireModel::solid( "chamfer_l1".into(), @@ -2584,6 +2678,9 @@ impl CadCommand for ChamferCommand { vec![] } ChamferStep::SecondPoly { h1, poly, click1 } => { + if !self.trim { + return vec![]; + } if handle != *h1 { return vec![]; } @@ -2680,7 +2777,7 @@ mod tests { 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 mut single = ChamferCommand::new(1.0, lines()); - assert_eq!(keywords(&single), ["D", "M"]); + assert_eq!(keywords(&single), ["D", "M", "N"]); single.on_entity_pick(Handle::new(1), DVec3::new(5.0, 0.0, 0.0)); assert!(matches!( single.on_entity_pick(Handle::new(2), DVec3::new(0.0, 5.0, 0.0)), @@ -2689,7 +2786,7 @@ mod tests { let mut multi = ChamferCommand::new(1.0, lines()); assert!(matches!(multi.on_text_input("M"), Some(CmdResult::NeedPoint))); - assert_eq!(keywords(&multi), ["D"], "Multiple is not offered twice"); + assert_eq!(keywords(&multi), ["D", "N"], "Multiple is not offered twice"); multi.on_entity_pick(Handle::new(1), DVec3::new(5.0, 0.0, 0.0)); match multi.on_entity_pick(Handle::new(2), DVec3::new(0.0, 5.0, 0.0)) { CmdResult::ReplaceManyContinue(replacements) => { @@ -2699,10 +2796,97 @@ mod tests { } _ => panic!("multiple mode should keep the command active"), } - assert_eq!(keywords(&multi), ["U", "D"]); + assert_eq!(keywords(&multi), ["U", "D", "N"]); assert!(matches!(multi.on_text_input("U"), Some(CmdResult::UndoDocument))); } + #[test] + fn no_trim_chamfer_adds_only_the_cut_line_and_keeps_source_handles() { + let first = Handle::new(1); + let second = Handle::new(2); + let lines = || vec![ + line(0.0, 0.0, 10.0, 0.0, 1), + line(0.0, 0.0, 0.0, 10.0, 2), + ]; + + let mut single = ChamferCommand::new(1.0, lines()); + single.dist2 = 1.0; + assert!(matches!(single.on_text_input("N"), Some(CmdResult::NeedPoint))); + assert!(!single.trim); + assert!(matches!( + single.on_entity_pick(first, DVec3::new(5.0, 0.0, 0.0)), + CmdResult::NeedPoint + )); + let addition = match single.on_entity_pick(second, DVec3::new(0.0, 5.0, 0.0)) { + CmdResult::CommitEntitiesAndExit(mut entities) => { + assert_eq!(entities.len(), 1); + entities.pop().unwrap() + } + _ => panic!("no-trim CHAMFER should add one line and exit"), + }; + let EntityType::Line(cut) = &addition else { + panic!("no-trim CHAMFER should add a line") + }; + assert!((cut.start.x - 1.0).abs() < 1e-9 && cut.start.y.abs() < 1e-9); + assert!(cut.end.x.abs() < 1e-9 && (cut.end.y - 1.0).abs() < 1e-9); + assert!(single + .all_entities + .iter() + .any(|entity| entity.common().handle == first)); + assert!(single + .all_entities + .iter() + .any(|entity| entity.common().handle == second)); + + let mut multi = ChamferCommand::new(1.0, lines()); + multi.dist2 = 1.0; + assert!(matches!(multi.on_text_input("M"), Some(CmdResult::NeedPoint))); + assert!(matches!(multi.on_text_input("N"), Some(CmdResult::NeedPoint))); + assert!(matches!( + multi.on_entity_pick(first, DVec3::new(5.0, 0.0, 0.0)), + CmdResult::NeedPoint + )); + match multi.on_entity_pick(second, DVec3::new(0.0, 5.0, 0.0)) { + CmdResult::CommitEntities(entities) => assert_eq!(entities.len(), 1), + _ => panic!("multiple no-trim CHAMFER should keep running"), + } + assert!(matches!(multi.step, ChamferStep::First)); + let mut committed = addition; + committed.common_mut().handle = Handle::new(10); + multi.on_entities_committed(&[committed]); + assert!(multi + .all_entities + .iter() + .any(|entity| entity.common().handle == Handle::new(10))); + } + + #[test] + fn no_trim_chamfer_refuses_polyline_corners() { + for token in ["N", "NOTRIM", "NO TRIM", "TRIM=FALSE"] { + let mut command = ChamferCommand::new(1.0, Vec::new()); + assert!(matches!(command.on_text_input(token), Some(CmdResult::NeedPoint))); + assert!(!command.trim, "{token}"); + } + for token in ["T", "TRIM", "TRIM=TRUE"] { + let mut command = ChamferCommand::new(1.0, Vec::new()); + assert!(matches!(command.on_text_input(token), Some(CmdResult::NeedPoint))); + assert!(command.trim, "{token}"); + } + + let mut poly = LwPolyline::new(); + poly.add_point(codec::types::Vector2::new(0.0, 0.0)); + poly.add_point(codec::types::Vector2::new(10.0, 0.0)); + poly.add_point(codec::types::Vector2::new(10.0, 10.0)); + poly.common.handle = Handle::new(3); + let mut command = ChamferCommand::new(1.0, vec![EntityType::LwPolyline(poly)]); + assert!(matches!(command.on_text_input("N"), Some(CmdResult::NeedPoint))); + assert!(matches!( + command.on_entity_pick(Handle::new(3), DVec3::new(5.0, 0.0, 0.0)), + CmdResult::ReportError(_) + )); + assert!(matches!(command.step, ChamferStep::First)); + } + #[test] fn fillet_continues_with_replacement_entities() { let first = Handle::new(1);