fix(selection): separate transient visibility
Keep object isolation out of persisted entity visibility and clear stale grips across block-editor transitions. Refs #512 Refs #517
This commit is contained in:
parent
7f41e1666f
commit
d5be78a187
10 changed files with 295 additions and 106 deletions
|
|
@ -444,12 +444,22 @@ impl OpenCADStudio {
|
|||
});
|
||||
|
||||
self.tabs[i].scene.deselect_all();
|
||||
// The first click of a double-click selects the model-space
|
||||
// INSERT and populates the cached grip overlay. Clearing only
|
||||
// Scene::selected leaves that INSERT's grips active inside the
|
||||
// block editor, where dragging one moves the outer reference
|
||||
// instead of block-local geometry (#517).
|
||||
self.tabs[i].active_grip = None;
|
||||
self.grip_hover = None;
|
||||
self.grip_popup = None;
|
||||
self.visibility_popup = None;
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
// Frame the camera on the block's own geometry (block-local, near
|
||||
// origin) — fit_all() goes through current_layout_block_handle so
|
||||
// it already scopes to the edited block. Without this the view
|
||||
// stays wherever model/paper space was. (#261)
|
||||
self.tabs[i].scene.fit_all();
|
||||
self.refresh_properties();
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_info(&format!(
|
||||
|
|
@ -470,11 +480,16 @@ impl OpenCADStudio {
|
|||
// Edits are live on the block record — just leave the block space.
|
||||
self.tabs[i].scene.block_edit_block = None;
|
||||
self.tabs[i].scene.deselect_all();
|
||||
self.tabs[i].active_grip = None;
|
||||
self.grip_hover = None;
|
||||
self.grip_popup = None;
|
||||
self.visibility_popup = None;
|
||||
self.tabs[i].scene.set_current_layout(session.return_layout.clone());
|
||||
// Return the view to where it was when BEDIT began (#425).
|
||||
*self.tabs[i].scene.camera.borrow_mut() = session.return_camera.clone();
|
||||
self.tabs[i].scene.camera_generation += 1;
|
||||
self.tabs[i].scene.rebuild_derived_caches();
|
||||
self.refresh_properties();
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output(&format!(
|
||||
"BEDIT: Block \"{}\" saved. All references updated.",
|
||||
|
|
@ -521,11 +536,16 @@ impl OpenCADStudio {
|
|||
}
|
||||
self.tabs[i].scene.block_edit_block = None;
|
||||
self.tabs[i].scene.deselect_all();
|
||||
self.tabs[i].active_grip = None;
|
||||
self.grip_hover = None;
|
||||
self.grip_popup = None;
|
||||
self.visibility_popup = None;
|
||||
self.tabs[i].scene.set_current_layout(session.return_layout.clone());
|
||||
// Return the view to where it was when BEDIT began (#425).
|
||||
*self.tabs[i].scene.camera.borrow_mut() = session.return_camera.clone();
|
||||
self.tabs[i].scene.camera_generation += 1;
|
||||
self.tabs[i].scene.rebuild_derived_caches();
|
||||
self.refresh_properties();
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output(&format!(
|
||||
"BEDIT: Block \"{}\" edit discarded.",
|
||||
|
|
|
|||
|
|
@ -491,9 +491,16 @@ impl OpenCADStudio {
|
|||
.push_error("ISOLATEOBJECTS: select the objects to isolate first.");
|
||||
} else {
|
||||
let n = self.tabs[i].scene.selected.len();
|
||||
self.push_undo_snapshot(i, "ISOLATEOBJECTS");
|
||||
let before = self.tabs[i].scene.object_isolation.clone();
|
||||
let selected_before =
|
||||
self.tabs[i].scene.selected.iter().copied().collect();
|
||||
self.tabs[i].scene.isolate_selected();
|
||||
self.tabs[i].dirty = true;
|
||||
self.push_object_visibility_history(
|
||||
i,
|
||||
"ISOLATEOBJECTS",
|
||||
before,
|
||||
selected_before,
|
||||
);
|
||||
self.command_line.push_info(&format!(
|
||||
"Isolated {n} object(s). UNISOLATEOBJECTS to restore."
|
||||
));
|
||||
|
|
@ -507,9 +514,17 @@ impl OpenCADStudio {
|
|||
.push_error("HIDEOBJECTS: select the objects to hide first.");
|
||||
} else {
|
||||
let n = self.tabs[i].scene.selected.len();
|
||||
self.push_undo_snapshot(i, "HIDEOBJECTS");
|
||||
let before = self.tabs[i].scene.object_isolation.clone();
|
||||
let selected_before =
|
||||
self.tabs[i].scene.selected.iter().copied().collect();
|
||||
self.tabs[i].scene.hide_selected();
|
||||
self.tabs[i].dirty = true;
|
||||
self.push_object_visibility_history(
|
||||
i,
|
||||
"HIDEOBJECTS",
|
||||
before,
|
||||
selected_before,
|
||||
);
|
||||
self.refresh_properties();
|
||||
self.command_line
|
||||
.push_info(&format!("Hid {n} object(s). UNISOLATEOBJECTS to restore."));
|
||||
}
|
||||
|
|
@ -518,9 +533,16 @@ impl OpenCADStudio {
|
|||
// UNISOLATEOBJECTS — bring back everything hidden by Isolate / Hide
|
||||
"UNISOLATEOBJECTS" => {
|
||||
if self.tabs[i].scene.is_isolation_active() {
|
||||
self.push_undo_snapshot(i, "UNISOLATEOBJECTS");
|
||||
let before = self.tabs[i].scene.object_isolation.clone();
|
||||
let selected_before =
|
||||
self.tabs[i].scene.selected.iter().copied().collect();
|
||||
self.tabs[i].scene.end_isolation();
|
||||
self.tabs[i].dirty = true;
|
||||
self.push_object_visibility_history(
|
||||
i,
|
||||
"UNISOLATEOBJECTS",
|
||||
before,
|
||||
selected_before,
|
||||
);
|
||||
self.command_line
|
||||
.push_info("Isolation ended — all objects shown.");
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use crate::modules::draw::modify::block_edit::BlockEditSession;
|
|||
use crate::modules::draw::modify::refedit::RefEditSession;
|
||||
use crate::scene::pick::grip::GripEdit;
|
||||
use crate::scene::GripDef;
|
||||
use crate::scene::Scene;
|
||||
use crate::scene::{ObjectIsolationState, Scene};
|
||||
use crate::snap::SnapResult;
|
||||
use crate::ui::{LayerPanel, PropertiesPanel};
|
||||
use acadrust::tables::Ucs;
|
||||
|
|
@ -518,12 +518,14 @@ impl DocumentTab {
|
|||
#[derive(Clone)]
|
||||
pub(super) enum HistorySnapshot {
|
||||
Delta(DeltaSnapshot),
|
||||
ObjectVisibility(ObjectVisibilitySnapshot),
|
||||
}
|
||||
|
||||
impl HistorySnapshot {
|
||||
pub(super) fn label(&self) -> &str {
|
||||
match self {
|
||||
HistorySnapshot::Delta(d) => &d.label,
|
||||
HistorySnapshot::ObjectVisibility(v) => &v.label,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -544,10 +546,42 @@ impl HistorySnapshot {
|
|||
.saturating_add(d.selected_before.len().saturating_mul(16))
|
||||
.saturating_add(d.selected_after.len().saturating_mul(16))
|
||||
.saturating_add(d.label.len()),
|
||||
HistorySnapshot::ObjectVisibility(v) => v
|
||||
.before
|
||||
.hidden
|
||||
.len()
|
||||
.saturating_add(
|
||||
v.before
|
||||
.keep
|
||||
.as_ref()
|
||||
.map_or(0, rustc_hash::FxHashSet::len),
|
||||
)
|
||||
.saturating_add(v.after.hidden.len())
|
||||
.saturating_add(
|
||||
v.after
|
||||
.keep
|
||||
.as_ref()
|
||||
.map_or(0, rustc_hash::FxHashSet::len),
|
||||
)
|
||||
.saturating_add(v.selected_before.len())
|
||||
.saturating_add(v.selected_after.len())
|
||||
.saturating_mul(16)
|
||||
.saturating_add(v.label.len()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Symmetric undo/redo image for session-only object visibility. It contains
|
||||
/// no document entity data and therefore can never make isolation serializable.
|
||||
#[derive(Clone)]
|
||||
pub(super) struct ObjectVisibilitySnapshot {
|
||||
pub(super) before: ObjectIsolationState,
|
||||
pub(super) after: ObjectIsolationState,
|
||||
pub(super) selected_before: Vec<Handle>,
|
||||
pub(super) selected_after: Vec<Handle>,
|
||||
pub(super) label: String,
|
||||
}
|
||||
|
||||
/// A transactional undo entry: for each touched handle, its before-image and
|
||||
/// after-image (`None` = the entity was absent on that side, i.e. an add or an
|
||||
/// erase). Symmetric — undo applies the before side, redo the after side — so
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
use super::{
|
||||
document::{
|
||||
DeltaSnapshot, HistorySnapshot, ObjectEntryDelta, PendingHistorySnapshot,
|
||||
StructureSnapshot, TableEntryDelta,
|
||||
DeltaSnapshot, HistorySnapshot, ObjectEntryDelta, ObjectVisibilitySnapshot,
|
||||
PendingHistorySnapshot, StructureSnapshot, TableEntryDelta,
|
||||
},
|
||||
OpenCADStudio,
|
||||
};
|
||||
use crate::scene::ObjectIsolationState;
|
||||
use acadrust::{EntityType, Handle};
|
||||
use rustc_hash::{FxHashMap, FxHashSet as HashSet};
|
||||
use std::sync::Arc;
|
||||
|
|
@ -157,6 +158,32 @@ impl OpenCADStudio {
|
|||
self.trim_history(i);
|
||||
}
|
||||
|
||||
pub(super) fn push_object_visibility_history(
|
||||
&mut self,
|
||||
i: usize,
|
||||
label: impl Into<String>,
|
||||
before: ObjectIsolationState,
|
||||
selected_before: Vec<Handle>,
|
||||
) {
|
||||
self.finish_pending_history(i);
|
||||
let after = self.tabs[i].scene.object_isolation.clone();
|
||||
let selected_after: Vec<Handle> =
|
||||
self.tabs[i].scene.selected.iter().copied().collect();
|
||||
if before == after && selected_before == selected_after {
|
||||
return;
|
||||
}
|
||||
self.push_undo_entry(
|
||||
i,
|
||||
HistorySnapshot::ObjectVisibility(ObjectVisibilitySnapshot {
|
||||
before,
|
||||
after,
|
||||
selected_before,
|
||||
selected_after,
|
||||
label: label.into(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn push_single_entity_history(
|
||||
&mut self,
|
||||
i: usize,
|
||||
|
|
@ -870,6 +897,27 @@ impl OpenCADStudio {
|
|||
changes
|
||||
}
|
||||
|
||||
fn apply_object_visibility_state(
|
||||
&mut self,
|
||||
i: usize,
|
||||
snapshot: &ObjectVisibilitySnapshot,
|
||||
undo: bool,
|
||||
) {
|
||||
let (state, selected) = if undo {
|
||||
(&snapshot.before, &snapshot.selected_before)
|
||||
} else {
|
||||
(&snapshot.after, &snapshot.selected_after)
|
||||
};
|
||||
let scene = &mut self.tabs[i].scene;
|
||||
scene.object_isolation = state.clone();
|
||||
scene.selected = selected
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|handle| scene.document.get_entity(*handle).is_some())
|
||||
.collect();
|
||||
scene.bump_geometry_no_blocks();
|
||||
}
|
||||
|
||||
/// Perform the one expensive cache/UI synchronization required after a
|
||||
/// batch of history steps. Intermediate states are never rendered, so
|
||||
/// rebuilding them only multiplies latency.
|
||||
|
|
@ -888,7 +936,6 @@ impl OpenCADStudio {
|
|||
// for the final state. Unlike the old path, decoded images stay
|
||||
// cached instead of being immediately cleared.
|
||||
scene.rebuild_derived_caches();
|
||||
scene.sync_hidden_from_invisible();
|
||||
} else if !changes.is_empty() {
|
||||
use crate::scene::ChangeKind::{Added, Modified, Removed};
|
||||
let mut net: FxHashMap<Handle, crate::scene::ChangeKind> = FxHashMap::default();
|
||||
|
|
@ -974,6 +1021,13 @@ impl OpenCADStudio {
|
|||
.redo_stack
|
||||
.push(HistorySnapshot::Delta(d));
|
||||
}
|
||||
HistorySnapshot::ObjectVisibility(v) => {
|
||||
self.apply_object_visibility_state(i, &v, true);
|
||||
self.tabs[i]
|
||||
.history
|
||||
.redo_stack
|
||||
.push(HistorySnapshot::ObjectVisibility(v));
|
||||
}
|
||||
}
|
||||
}
|
||||
self.finish_history_apply(i, had_full, structure_changed, &changes);
|
||||
|
|
@ -1010,6 +1064,13 @@ impl OpenCADStudio {
|
|||
.undo_stack
|
||||
.push(HistorySnapshot::Delta(d));
|
||||
}
|
||||
HistorySnapshot::ObjectVisibility(v) => {
|
||||
self.apply_object_visibility_state(i, &v, false);
|
||||
self.tabs[i]
|
||||
.history
|
||||
.undo_stack
|
||||
.push(HistorySnapshot::ObjectVisibility(v));
|
||||
}
|
||||
}
|
||||
}
|
||||
self.finish_history_apply(i, had_full, structure_changed, &changes);
|
||||
|
|
|
|||
|
|
@ -549,7 +549,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||
*e = orig;
|
||||
}
|
||||
}
|
||||
self.tabs[i].scene.hidden.remove(&h);
|
||||
self.tabs[i].scene.preview_hidden.remove(&h);
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
// Geometry restored to the backup — re-tessellate just it.
|
||||
self.tabs[i]
|
||||
|
|
|
|||
|
|
@ -821,10 +821,10 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|||
.unwrap_or_else(|| "Model".to_string()),
|
||||
};
|
||||
}
|
||||
// Rebuild the Isolate/Hide set from the entities the file itself
|
||||
// marks invisible (DXF code 60), so hidden objects stay hidden on
|
||||
// reopen and End Isolation can bring them back.
|
||||
self.tabs[i].scene.sync_hidden_from_invisible();
|
||||
// Object isolation is session-only. A newly opened drawing must
|
||||
// not inherit the previous tab's filter, and persisted entity
|
||||
// visibility remains independent (not an isolation session).
|
||||
self.tabs[i].scene.reset_transient_visibility();
|
||||
crate::io::linetypes::populate_document(&mut self.tabs[i].scene.document);
|
||||
self.tabs[i].properties = PropertiesPanel::empty();
|
||||
// Seed the current table / multileader style from the file's
|
||||
|
|
|
|||
|
|
@ -742,11 +742,11 @@ impl OpenCADStudio {
|
|||
// the whole model on every move.
|
||||
if self.grip_preview_handle != Some(grip.handle) {
|
||||
if let Some(prev) = self.grip_preview_handle.take() {
|
||||
self.tabs[i].scene.hidden.remove(&prev);
|
||||
self.tabs[i].scene.preview_hidden.remove(&prev);
|
||||
}
|
||||
// Back up the original geometry so Esc can cancel the drag.
|
||||
self.grip_original = self.tabs[i].scene.document.get_entity(grip.handle).cloned();
|
||||
self.tabs[i].scene.hidden.insert(grip.handle);
|
||||
self.tabs[i].scene.preview_hidden.insert(grip.handle);
|
||||
// Hiding changes exactly one resident run. Publishing a full
|
||||
// delta here made the first grip move rebuild every wire.
|
||||
self.tabs[i]
|
||||
|
|
@ -2016,7 +2016,7 @@ impl OpenCADStudio {
|
|||
}
|
||||
self.grip_text_verts = Vec::new();
|
||||
self.grip_text_slide = false;
|
||||
self.tabs[i].scene.hidden.remove(&h);
|
||||
self.tabs[i].scene.preview_hidden.remove(&h);
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
// Only the dragged entity changed — re-tessellate just it.
|
||||
self.tabs[i]
|
||||
|
|
|
|||
Loading…
Reference in a new issue