feat(chamfer): preserve sources in no-trim mode

This commit is contained in:
ysdirector committed 2026-10-10 07:38:47 +08:00
1 parent 2a2d9b55d9
commit 3129e17602
1 file changed
+188 -4
+188 -4
View File
@@ -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<EntityType>,
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<CmdResult> {
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);