fix: improve classic leader behavior and annotation linkage

This commit is contained in:
gianlucafiore committed 2026-08-13 11:58:54 -03:00
1 parent 0038e386f7
commit 2d1ee0d936
7 files changed
+418 -73

No files matched your search

+9 -1
View File
@@ -480,7 +480,15 @@ pub fn annotative_offscale_for(
}),
}
}
pub(crate) fn annotation_scale_handles_for_entity(
doc: &CadDocument,
entity_handle: Handle,
) -> Vec<Handle> {
object_scale_memberships(doc, entity_handle)
.into_iter()
.map(|(_, scale_handle)| scale_handle)
.collect()
}
pub fn scale_handle_by_name(doc: &CadDocument, name: &str) -> Option<Handle> {
doc.objects.iter().find_map(|(handle, object)| match object {
ObjectType::Scale(scale)
+141 -3
View File
@@ -551,11 +551,43 @@ impl Scene {
}
pub fn copy_entities(&mut self, handles: &[Handle], t: &EntityTransform) -> Vec<Handle> {
let copy_handles = self.handles_expanded_for_leader_annotations(handles);
// LEADER + attached MTEXT are a logical pair. Their entity clones must not
// retain the source extension dictionary, otherwise both copies share the
// same annotation-context objects.
let leader_pair_handles: Vec<Handle> = copy_handles
.iter()
.flat_map(|&handle| {
let annotation = match self.document.get_entity(handle) {
Some(EntityType::Leader(leader)) if !leader.annotation_handle.is_null() => {
Some(leader.annotation_handle)
}
_ => None,
};
std::iter::once(handle).chain(annotation)
})
.collect();
// Objects on a locked layer can be selected but not copied.
let clones: Vec<(Handle, EntityType)> = handles
let clones: Vec<(Handle, EntityType, Vec<Handle>)> = copy_handles
.iter()
.filter(|&&h| !self.is_layer_locked(h))
.filter_map(|&h| self.document.get_entity(h).cloned().map(|e| (h, e)))
.filter_map(|&h| {
let entity = self.document.get_entity(h)?.clone();
let annotation_scales = if leader_pair_handles.contains(&h) {
crate::scene::annotative::annotation_scale_handles_for_entity(
&self.document,
h,
)
} else {
Vec::new()
};
Some((h, entity, annotation_scales))
})
.collect();
// MIRRTEXT also governs the copy path (default MIRROR keeps the source
// and adds a mirrored copy): keep the copied text right-reading when the
@@ -567,7 +599,7 @@ impl Scene {
let mut new_handles = Vec::with_capacity(clones.len());
let mut handle_map = rustc_hash::FxHashMap::default();
let mut refresh_solid_handles = Vec::new();
for (src_handle, mut entity) in clones {
for (src_handle, mut entity, annotation_scales) in clones {
let text_orient = if preserve_text_orientation {
capture_text_orient(&entity)
} else {
@@ -598,9 +630,31 @@ impl Scene {
}
}
Self::reset_clone_subhandles(&mut self.document, &mut entity);
// An annotative LEADER/MTEXT pair must receive a fresh extension dictionary.
// Keeping this handle would make the copy share the source's context tree.
if !annotation_scales.is_empty() {
entity.common_mut().xdictionary_handle = None;
}
entity.common_mut().handle = Handle::NULL;
let h = self.document.add_entity(entity).unwrap_or(Handle::NULL);
if !h.is_null() {
if !annotation_scales.is_empty() {
for scale_handle in annotation_scales {
crate::scene::annotative::create_annotation_context(
&mut self.document,
h,
scale_handle,
);
}
// Annotation contexts add dictionary/object records outside the entity
// delta itself, so keep undo on the safe full-snapshot path.
if self.is_recording_undo() {
self.poison_undo_recording();
}
}
// Delta-undo: a copy's before-image is "nothing" (undo erases it).
if self.is_recording_undo() {
self.record_undo_before(h, None);
@@ -640,7 +694,35 @@ impl Scene {
handle_map.insert(src_handle, h);
}
}
// A copied LEADER must reference the copied annotation, never the
// source annotation. Both entities now exist, so remap the stored handle.
let leader_links: Vec<(Handle, Handle)> = handle_map
.iter()
.filter_map(|(&source_handle, &copied_handle)| {
let EntityType::Leader(source_leader) =
self.document.get_entity(source_handle)?
else {
return None;
};
let copied_annotation = handle_map
.get(&source_leader.annotation_handle)
.copied()
.unwrap_or(Handle::NULL);
Some((copied_handle, copied_annotation))
})
.collect();
for (leader_handle, annotation_handle) in leader_links {
if let Some(EntityType::Leader(leader)) =
self.document.get_entity_mut(leader_handle)
{
leader.annotation_handle = annotation_handle;
}
let _ = self.sync_displayed_annotation_context(leader_handle);
}
// Complete group copies record their new Group objects and dictionary
// entry as targeted object deltas inside copy_complete_groups.
self.copy_complete_groups(&handle_map);
@@ -956,10 +1038,66 @@ impl Scene {
.get_entity(handle)
.and_then(crate::entities::solid3d::point_of_reference)
.map(|p| [p.x, p.y, p.z]);
// A LEADER's final vertex is the end of its horizontal landing.
// Remember its old position and linked MTEXT so the annotation can follow
// when that grip stretches the landing.
let leader_landing_before = self.document.get_entity(handle).and_then(|entity| {
let EntityType::Leader(leader) = entity else {
return None;
};
let n = leader.vertices.len();
if n < 3 || (grip_id != n - 1 && grip_id != n - 2) || leader.annotation_handle.is_null() {
return None;
}
let point = leader.vertices.last()?;
Some((
leader.annotation_handle,
glam::DVec3::new(point.x, point.y, point.z),
))
});
if let Some(entity) = self.document.get_entity_mut(handle) {
view::dispatch::apply_grip(entity, grip_id, apply);
}
if let Some((annotation_handle, old_landing)) = leader_landing_before {
let new_landing = self.document.get_entity(handle).and_then(|entity| {
let EntityType::Leader(leader) = entity else {
return None;
};
let point = leader.vertices.last()?;
Some(glam::DVec3::new(point.x, point.y, point.z))
});
if let Some(new_landing) = new_landing {
let delta = new_landing - old_landing;
if delta.length_squared() > 1.0e-20 {
if self.is_recording_undo() {
if let Some(before) = self.document.get_entity_arc(annotation_handle) {
self.record_undo_before(annotation_handle, Some(before));
}
}
if let Some(annotation) = self.document.get_entity_mut(annotation_handle) {
view::dispatch::apply_transform(
annotation,
&crate::command::EntityTransform::Translate(delta),
);
}
if self.sync_displayed_annotation_context(annotation_handle) {
self.poison_undo_recording();
}
self.bump_entities(&[(
annotation_handle,
crate::scene::ChangeKind::Modified,
)]);
}
}
}
if self.sync_displayed_annotation_context(handle) {
self.poison_undo_recording();
}
+54 -3
View File
@@ -3,12 +3,46 @@ use super::*;
impl Scene {
// ── Selection ─────────────────────────────────────────────────────────
/// Treat a classic LEADER and its attached annotation as one logical object.
/// Clicking/copying/deleting either side expands to the complete pair.
pub(crate) fn handles_expanded_for_leader_annotations(
&self,
handles: &[Handle],
) -> Vec<Handle> {
let mut expanded = handles.to_vec();
for &handle in handles {
// LEADER -> annotation.
if let Some(EntityType::Leader(leader)) = self.document.get_entity(handle) {
if !leader.annotation_handle.is_null() {
expanded.push(leader.annotation_handle);
}
}
// Annotation -> LEADER.
expanded.extend(self.document.entities().filter_map(|entity| match entity {
EntityType::Leader(leader)
if !leader.annotation_handle.is_null()
&& leader.annotation_handle == handle =>
{
Some(entity.common().handle)
}
_ => None,
}));
}
expanded.sort_unstable_by_key(|handle| handle.value());
expanded.dedup();
expanded
}
pub fn select_entity(&mut self, handle: Handle, exclusive: bool) {
let handles = self.handles_expanded_for_leader_annotations(&[handle]);
if exclusive {
self.selected.clear();
}
self.selected.insert(handle);
self.selected.extend(handles);
self.bump_selection();
}
@@ -21,6 +55,12 @@ impl Scene {
/// only when its contents actually changed. History/file/command paths must
/// use this instead of assigning `selected` directly.
pub(crate) fn replace_selection(&mut self, selected: HashSet<Handle>) {
let handles: Vec<Handle> = selected.iter().copied().collect();
let selected: HashSet<Handle> = self
.handles_expanded_for_leader_annotations(&handles)
.into_iter()
.collect();
if self.selected != selected {
self.selected = selected;
self.bump_selection();
@@ -29,7 +69,14 @@ impl Scene {
/// Remove a single entity from the selection (Shift+click subtractive pick).
pub fn deselect_entity(&mut self, handle: Handle) {
if self.selected.remove(&handle) {
let handles = self.handles_expanded_for_leader_annotations(&[handle]);
let mut changed = false;
for handle in handles {
changed |= self.selected.remove(&handle);
}
if changed {
self.bump_selection();
}
}
@@ -608,10 +655,14 @@ impl Scene {
// ── Erase ─────────────────────────────────────────────────────────────
pub fn erase_entities(&mut self, handles: &[Handle]) {
let erase_handles = self.handles_expanded_for_leader_annotations(handles);
let mut handle_set: HashSet<Handle> = HashSet::default();
let mut erased: Vec<(Handle, ChangeKind)> = Vec::new();
let mut highlight_changed = false;
for &h in handles {
for &h in &erase_handles {
// Objects on a locked layer can't be erased.
if self.is_layer_locked(h) {
continue;