forked from 1b-agent/opencadstudio-upstream
JOIN: separate chains join one by one (#1765)
A selection of several unconnected chains, or one holding a closed polyline, failed as a whole; each chain now joins on its own and the rest stays. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
a156a63edd
commit
7b4c13c6ca
1 file changed
+83
-4
@@ -445,14 +445,64 @@ fn segs_of(e: &EntityType) -> Option<Vec<Seg>> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Join `entities` end-to-end: into one entity when they form a single
|
||||
/// chain, otherwise chain by chain, leaving alone what joins nothing (a
|
||||
/// closed polyline, a lone object, a branching set). Returns
|
||||
/// `(removed_handles, new_entities)`, or `None` when nothing joined.
|
||||
pub fn join_entities(entities: &[(Handle, &EntityType)]) -> Option<(Vec<Handle>, Vec<EntityType>)> {
|
||||
if let Some(joined) = join_chain(entities) {
|
||||
return Some(joined);
|
||||
}
|
||||
let ends: Vec<Option<Vec<DVec3>>> = entities
|
||||
.iter()
|
||||
.map(|(_, entity)| {
|
||||
segs_of(entity).map(|segs| segs.iter().flat_map(|seg| [seg.a, seg.b]).collect())
|
||||
})
|
||||
.collect();
|
||||
fn root(group: &mut [usize], mut index: usize) -> usize {
|
||||
while group[index] != index {
|
||||
group[index] = group[group[index]];
|
||||
index = group[index];
|
||||
}
|
||||
index
|
||||
}
|
||||
// ponytail: all-pairs endpoint test, a spatial grid if huge selections drag.
|
||||
let mut group: Vec<usize> = (0..entities.len()).collect();
|
||||
for a in 0..entities.len() {
|
||||
for b in a + 1..entities.len() {
|
||||
let (Some(pa), Some(pb)) = (&ends[a], &ends[b]) else {
|
||||
continue;
|
||||
};
|
||||
if pa.iter().any(|p| pb.iter().any(|q| p.distance(*q) <= JOIN_EPS)) {
|
||||
let (ra, rb) = (root(&mut group, a), root(&mut group, b));
|
||||
group[ra] = rb;
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut chains: std::collections::BTreeMap<usize, Vec<(Handle, &EntityType)>> =
|
||||
Default::default();
|
||||
for (index, entity) in entities.iter().enumerate() {
|
||||
if ends[index].is_some() {
|
||||
chains.entry(root(&mut group, index)).or_default().push(*entity);
|
||||
}
|
||||
}
|
||||
let (mut removed, mut merged) = (Vec::new(), Vec::new());
|
||||
for chain in chains.values().filter(|chain| chain.len() < entities.len()) {
|
||||
if let Some((gone, made)) = join_chain(chain) {
|
||||
removed.extend(gone);
|
||||
merged.extend(made);
|
||||
}
|
||||
}
|
||||
(!removed.is_empty()).then_some((removed, merged))
|
||||
}
|
||||
|
||||
/// Join all `entities` end-to-end into a single entity. Segments join
|
||||
/// wherever their endpoints touch — the angle between them is irrelevant.
|
||||
/// A collinear straight run collapses to one `Line`; a planar chain
|
||||
/// becomes an `LwPolyline` (arcs kept as bulges); a chain with varying Z
|
||||
/// becomes a `Polyline3D` (straight segments only). Returns
|
||||
/// `(removed_handles, new_entities)`, or `None` when the selection isn't a
|
||||
/// single connected chain or holds an unsupported entity.
|
||||
pub fn join_entities(entities: &[(Handle, &EntityType)]) -> Option<(Vec<Handle>, Vec<EntityType>)> {
|
||||
/// becomes a `Polyline3D` (straight segments only). `None` when the
|
||||
/// selection isn't a single connected chain or holds an unsupported entity.
|
||||
fn join_chain(entities: &[(Handle, &EntityType)]) -> Option<(Vec<Handle>, Vec<EntityType>)> {
|
||||
if entities.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
@@ -736,6 +786,35 @@ mod join_tests {
|
||||
);
|
||||
}
|
||||
|
||||
// Several separate squares of lines join square by square (#1765).
|
||||
#[test]
|
||||
fn separate_chains_join_one_by_one() {
|
||||
let square = |o: f64, size: f64| {
|
||||
[
|
||||
line(o, o, o + size, o, 0.0),
|
||||
line(o + size, o, o + size, o + size, 0.0),
|
||||
line(o + size, o + size, o, o + size, 0.0),
|
||||
line(o, o + size, o, o, 0.0),
|
||||
]
|
||||
};
|
||||
let lines: Vec<EntityType> =
|
||||
square(0.0, 10.0).into_iter().chain(square(2.0, 6.0)).collect();
|
||||
let lone = line(50.0, 0.0, 60.0, 0.0, 0.0);
|
||||
let mut pairs: Vec<(Handle, &EntityType)> = lines
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, entity)| (Handle::new(index as u64 + 1), entity))
|
||||
.collect();
|
||||
pairs.push((Handle::new(99), &lone));
|
||||
let (removed, out) = join_entities(&pairs).expect("both squares join");
|
||||
assert_eq!(removed.len(), 8);
|
||||
assert!(!removed.contains(&Handle::new(99)));
|
||||
assert_eq!(out.len(), 2);
|
||||
assert!(out
|
||||
.iter()
|
||||
.all(|entity| matches!(entity, EntityType::LwPolyline(pl) if pl.is_closed)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_join_consumes_only_compatible_lines_and_keeps_direction() {
|
||||
let source_handle = Handle::new(11);
|
||||
|
||||
Reference in new issue
Block a user