From 74cbdf7e2ff6c4bde60c55466e6e3d46bbf25999 Mon Sep 17 00:00:00 2001 From: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com> Date: Sat, 10 Oct 2026 18:13:16 +0200 Subject: [PATCH] Keep selected objects outside the joined chain when using JOIN (#86) (#148) --- crates/engine/src/cmd/modify.rs | 13 +++++++++++-- crates/engine/src/cmd/modify2_tests.rs | 17 +++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/modify.rs b/crates/engine/src/cmd/modify.rs index 600cea7..46874d1 100644 --- a/crates/engine/src/cmd/modify.rs +++ b/crates/engine/src/cmd/modify.rs @@ -1171,6 +1171,8 @@ fn run_breakat(s: &mut Session, p: &Value) -> Result { pub(crate) fn join(s: &mut Session, hs: &[Handle]) -> Result> { let d = s.doc()?; let mut segs: Vec = Vec::new(); + // Source entity of each segment in `segs`, so only entities that end up in the chain are removed. + let mut owners: Vec = Vec::new(); let mut common = None; for h in hs { let Some(e) = d.entity(*h) else { continue }; @@ -1181,12 +1183,14 @@ pub(crate) fn join(s: &mut Session, hs: &[Handle]) -> Result> { EntityKind::LwPolyline(p) if !p.closed => segs.extend(Polyline { vertices: p.vertices.clone(), closed: false }.segments()), _ => {} } + owners.resize(segs.len(), *h); } if segs.len() < 2 { return Err(EngineError::Other("Select at least two objects to join.".into())); } // Chain greedily from the first segment. let mut chain = vec![segs.remove(0)]; + let mut used = vec![owners.remove(0)]; let tol = 1e-6; loop { let start = chain.first().map(Segment::start).unwrap_or_default(); @@ -1197,6 +1201,7 @@ pub(crate) fn join(s: &mut Session, hs: &[Handle]) -> Result> { break; }; let s0 = segs.remove(i); + used.push(owners.remove(i)); if s0.start().near(end, tol) { chain.push(s0); } else if s0.end().near(end, tol) { @@ -1227,7 +1232,7 @@ pub(crate) fn join(s: &mut Session, hs: &[Handle]) -> Result> { } let space = s.space(); let doc = s.doc_mut()?; - for h in hs { + for h in hs.iter().filter(|h| used.contains(*h)) { doc.remove_entity(*h); } let nh = doc.new_handle(); @@ -1318,7 +1323,11 @@ impl SelectThen { } Op::Join => { match join(s, &self.objs) { - Ok(_) => s.echo(format!("{} objects joined into 1 polyline", self.objs.len())), + Ok(_) => { + // Objects outside the joined chain are kept, so count only the ones JOIN consumed. + let n = s.doc().map(|d| self.objs.iter().filter(|h| d.entity(**h).is_none()).count()).unwrap_or(0); + s.echo(format!("{n} objects joined into 1 polyline")) + } Err(e) => s.echo(e.to_string()), } Ok(Step::Done) diff --git a/crates/engine/src/cmd/modify2_tests.rs b/crates/engine/src/cmd/modify2_tests.rs index f7f547e..4746ac6 100644 --- a/crates/engine/src/cmd/modify2_tests.rs +++ b/crates/engine/src/cmd/modify2_tests.rs @@ -94,6 +94,23 @@ fn pedit_json_options() { assert!(s.execute("pedit", &json!({"handle": c, "option": "close"})).is_err()); } +#[test] +fn join_keeps_selected_objects_outside_the_chain() { + let mut s = Session::new(); + let a = h(&s.execute("line", &json!({"points": [[0, 0], [5, 0]]})).unwrap()); + let b = h(&s.execute("line", &json!({"points": [[5, 0], [5, 5]]})).unwrap()); + let far = h(&s.execute("line", &json!({"points": [[50, 50], [60, 60]]})).unwrap()); + let c = h(&s.execute("circle", &json!({"center": [50, 50], "radius": 2})).unwrap()); + let r = s.execute("join", &json!({"handles": [a, b, far, c]})).unwrap(); + let joined = r["handle"].as_str().unwrap().to_string(); + assert_eq!(poly(&kind(&s, &joined)).vertices.len(), 3); + let d = s.doc().unwrap(); + assert!(d.entity(Handle::parse_hex(&a).unwrap()).is_none()); + assert!(d.entity(Handle::parse_hex(&b).unwrap()).is_none()); + assert!(d.entity(Handle::parse_hex(&far).unwrap()).is_some(), "unconnected line stays"); + assert!(d.entity(Handle::parse_hex(&c).unwrap()).is_some(), "unrelated circle stays"); +} + #[test] fn pedit_command_line() { let mut s = Session::new();