forked from 1b-agent/opencadstudio-upstream
feat(chamfer): preserve sources in no-trim mode
This commit is contained in:
1 parent
2a2d9b55d9
commit
3129e17602
1 file changed
+188
-4
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user