Keep selected objects outside the joined chain when using JOIN (#86) (#148)

This commit is contained in:
MarekSpodBiedry 2026-10-10 18:13:16 +02:00 committed by GitHub
parent a983cf1ed0
commit 74cbdf7e2f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 28 additions and 2 deletions

View File

@ -1171,6 +1171,8 @@ fn run_breakat(s: &mut Session, p: &Value) -> Result<Value> {
pub(crate) fn join(s: &mut Session, hs: &[Handle]) -> Result<Option<Handle>> {
let d = s.doc()?;
let mut segs: Vec<Segment> = Vec::new();
// Source entity of each segment in `segs`, so only entities that end up in the chain are removed.
let mut owners: Vec<Handle> = 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<Option<Handle>> {
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<Option<Handle>> {
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<Option<Handle>> {
}
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)

View File

@ -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();