forked from 1b-agent/opencadstudio-upstream
Add source-first joins with retained object identity
This commit is contained in:
1 parent
7d0c059b30
commit
9ca2db82bf
4 files changed
+101
-40
No files matched your search
@@ -3263,6 +3263,26 @@ impl OpenCADStudio {
|
||||
self.command_line.push_info(&p);
|
||||
}
|
||||
}
|
||||
CmdResult::JoinToSource { source, handles } => {
|
||||
if self.reject_locked_edit(i, source) { return Task::none(); }
|
||||
let joined = self.tabs[i].scene.document.get_entity(source).and_then(|entity| {
|
||||
let candidates: Vec<_> = handles.iter().filter(|handle| **handle != source && !self.tabs[i].scene.is_layer_locked(**handle))
|
||||
.filter_map(|handle| self.tabs[i].scene.document.get_entity(*handle).map(|entity| (*handle,entity))).collect();
|
||||
crate::modules::draw::modify::join::join_to_source(entity,&candidates)
|
||||
});
|
||||
if let Some((replacement, consumed)) = joined {
|
||||
self.push_undo_snapshot(i,"JOIN");
|
||||
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(source) { *entity = replacement; }
|
||||
self.tabs[i].scene.erase_entities(&consumed);
|
||||
self.tabs[i].scene.refresh_fill_model(source);
|
||||
self.tabs[i].scene.bump_entities(&[(source,crate::scene::ChangeKind::Modified)]);
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output(&format!("JOIN: {} objects joined to source.",consumed.len()));
|
||||
self.refresh_properties();
|
||||
} else { self.command_line.push_info("JOIN: no compatible objects joined to source."); }
|
||||
self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire(); self.restore_pre_cmd_tangent();
|
||||
}
|
||||
CmdResult::JoinEntities(handles) => {
|
||||
if let Some(handle) = handles
|
||||
.iter()
|
||||
|
||||
@@ -218,7 +218,10 @@ impl OpenCADStudio {
|
||||
self.apply_cmd_result(crate::command::CmdResult::JoinEntities(selected));
|
||||
return Some(task);
|
||||
}
|
||||
let cmd = JoinCommand::new();
|
||||
let mut cmd = JoinCommand::new();
|
||||
if let Some(handle) = selected.first() {
|
||||
if let Some(entity) = self.tabs[i].scene.document.get_entity(*handle).cloned() { cmd = cmd.with_source(*handle, entity); }
|
||||
}
|
||||
self.command_line.push_info(&cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||
}
|
||||
|
||||
@@ -1475,6 +1475,8 @@ pub enum CmdResult {
|
||||
BreakEntity { handle: Handle, p1: DVec3, p2: DVec3 },
|
||||
/// Attempt to join the given entities into fewer merged entities.
|
||||
JoinEntities(Vec<Handle>),
|
||||
/// Join candidates into an explicitly selected source.
|
||||
JoinToSource { source: Handle, handles: Vec<Handle> },
|
||||
/// Apply a polyline-edit operation to one entity; keep command active.
|
||||
PeditOp {
|
||||
handle: Handle,
|
||||
|
||||
@@ -12,62 +12,98 @@
|
||||
use acadrust::types::{Vector2, Vector3};
|
||||
use acadrust::{EntityType, Handle};
|
||||
use glam::DVec3;
|
||||
use crate::t;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
|
||||
// ── Command ────────────────────────────────────────────────────────────────
|
||||
|
||||
pub struct JoinCommand {
|
||||
source: Option<(Handle, EntityType)>,
|
||||
picked: Option<EntityType>,
|
||||
handles: Vec<Handle>,
|
||||
gathering: bool,
|
||||
}
|
||||
|
||||
impl JoinCommand {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
handles: vec![],
|
||||
gathering: true,
|
||||
}
|
||||
pub fn new() -> Self { Self { source: None, picked: None, handles: Vec::new() } }
|
||||
pub fn with_source(mut self, handle: Handle, entity: EntityType) -> Self {
|
||||
self.picked = Some(entity); self.on_entity_pick(handle, DVec3::ZERO); self
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for JoinCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"JOIN"
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str { "JOIN" }
|
||||
fn prompt(&self) -> String {
|
||||
t!(
|
||||
"JOIN Select objects to join (%{count} selected, Enter to apply):",
|
||||
count = self.handles.len()
|
||||
)
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
fn is_selection_gathering(&self) -> bool {
|
||||
self.gathering
|
||||
}
|
||||
|
||||
fn on_selection_complete(&mut self, handles: Vec<Handle>) -> CmdResult {
|
||||
self.handles = handles;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
if self.handles.len() < 2 {
|
||||
return CmdResult::Cancel;
|
||||
match self.source.as_ref().map(|(_, entity)| entity) {
|
||||
None => "JOIN Select source object:".into(),
|
||||
Some(EntityType::Line(_)) => "JOIN Select lines to join to source (Enter to apply):".into(),
|
||||
Some(EntityType::Arc(_)) => "JOIN Select arcs to join to source (Enter to apply):".into(),
|
||||
_ => "JOIN Select objects to join to source (Enter to apply):".into(),
|
||||
}
|
||||
self.gathering = false;
|
||||
CmdResult::JoinEntities(self.handles.clone())
|
||||
}
|
||||
fn needs_entity_pick(&self) -> bool { self.source.is_none() }
|
||||
fn inject_before_entity_pick(&self) -> bool { true }
|
||||
fn inject_picked_entity(&mut self, entity: EntityType) { self.picked = Some(entity); }
|
||||
fn on_entity_pick(&mut self, handle: Handle, _: DVec3) -> CmdResult {
|
||||
if handle.is_null() { return CmdResult::NeedPoint; }
|
||||
if let Some(entity) = self.picked.take() {
|
||||
let supported = match &entity {
|
||||
EntityType::Line(_) | EntityType::Arc(_) => true,
|
||||
EntityType::LwPolyline(p) => !p.is_closed,
|
||||
EntityType::Polyline2D(p) => !p.is_closed(),
|
||||
_ => false,
|
||||
};
|
||||
if supported { self.source = Some((handle, entity)); }
|
||||
}
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
fn is_selection_gathering(&self) -> bool { self.source.is_some() }
|
||||
fn on_selection_complete(&mut self, handles: Vec<Handle>) -> CmdResult {
|
||||
self.handles = handles.into_iter().filter(|handle| self.source.as_ref().is_none_or(|(source,_)| handle != source)).collect(); CmdResult::NeedPoint
|
||||
}
|
||||
fn on_point(&mut self, _: DVec3) -> CmdResult { CmdResult::NeedPoint }
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
let Some((source, _)) = self.source.as_ref() else { return CmdResult::Cancel; };
|
||||
if self.handles.is_empty() { return CmdResult::Cancel; }
|
||||
CmdResult::JoinToSource { source: *source, handles: self.handles.clone() }
|
||||
}
|
||||
}
|
||||
|
||||
// ── Geometry ───────────────────────────────────────────────────────────────
|
||||
/// Join compatible candidates while preserving the source identity and style.
|
||||
pub fn join_to_source(source: &EntityType, candidates: &[(Handle, &EntityType)]) -> Option<(EntityType, Vec<Handle>)> {
|
||||
let mut result = source.clone(); let mut consumed = Vec::new();
|
||||
loop {
|
||||
let mut progress = false;
|
||||
for (handle, candidate) in candidates {
|
||||
if consumed.contains(handle) { continue; }
|
||||
let next = match (&result, *candidate) {
|
||||
(EntityType::Line(a), EntityType::Line(b)) => {
|
||||
let point = |p: &Vector3| [p.x,p.y,p.z];
|
||||
cadkernel::space::source_join::join_collinear_lines([point(&a.start),point(&a.end)],[point(&b.start),point(&b.end)],JOIN_EPS).map(|span| {
|
||||
let mut line = a.clone(); line.start = Vector3::new(span[0][0],span[0][1],span[0][2]); line.end = Vector3::new(span[1][0],span[1][1],span[1][2]); EntityType::Line(line)
|
||||
})
|
||||
}
|
||||
(EntityType::Arc(a), EntityType::Arc(b)) => {
|
||||
let data = |a: &acadrust::entities::Arc| ([a.center.x,a.center.y,a.center.z],[a.normal.x,a.normal.y,a.normal.z],a.radius,[a.start_angle,a.end_angle]);
|
||||
cadkernel::space::source_join::join_cocircular_arcs(data(a),data(b),JOIN_EPS).map(|span| {
|
||||
if span[1]-span[0] >= std::f64::consts::TAU {
|
||||
let mut circle = acadrust::entities::Circle::new(); circle.common = a.common.clone(); circle.center = a.center.clone(); circle.normal = a.normal.clone(); circle.radius = a.radius; circle.thickness = a.thickness; EntityType::Circle(circle)
|
||||
} else { let mut arc = a.clone(); arc.start_angle = span[0]; arc.end_angle = span[1].rem_euclid(std::f64::consts::TAU); EntityType::Arc(arc) }
|
||||
})
|
||||
}
|
||||
(EntityType::LwPolyline(_) | EntityType::Polyline2D(_), _) => {
|
||||
join_entities(&[(result.common().handle,&result),(*handle,*candidate)]).and_then(|(_,mut entities)| {
|
||||
let entity = entities.pop()?;
|
||||
if matches!(&entity,EntityType::Line(_)) { super::pedit::convert_to_polyline(&entity) } else { Some(entity) }
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
if let Some(mut entity) = next {
|
||||
*entity.common_mut() = source.common().clone(); result = entity; consumed.push(*handle); progress = true;
|
||||
}
|
||||
}
|
||||
if !progress { break; }
|
||||
}
|
||||
(!consumed.is_empty()).then_some((result,consumed))
|
||||
}
|
||||
|
||||
/// Endpoint-match tolerance (model units). Segments split from a shared
|
||||
/// vertex meet exactly, so this only absorbs float noise.
|
||||
|
||||
Reference in new issue
Block a user