fix(bedit): optimize block editor close and preserve warm render caches

Avoid rebuilding derived caches and clearing tessellation memos when closing the block editor on clean exit or discard. Scope entity delta computation to block record members instead of scanning and cloning the entire drawing, and add a fast path to the dirty check when no local commands were executed.
This commit is contained in:
Karim Jerbi committed 2026-10-15 09:41:01 +01:00
1 parent cccea4008b
commit f3973f08a7
3 files changed
+118 -34

No files matched your search

+92 -33
View File
@@ -11,10 +11,16 @@ use rustc_hash::{FxHashMap, FxHashSet};
/// document's Arc storage; only modified entities detach during editing. /// document's Arc storage; only modified entities detach during editing.
pub(super) struct BlockEditCheckpoint { pub(super) struct BlockEditCheckpoint {
document: CadDocument, document: CadDocument,
structure: CadDocument,
constraints: Vec<crate::scene::parametric_constraints::ParametricConstraintSet>, constraints: Vec<crate::scene::parametric_constraints::ParametricConstraintSet>,
parameters: crate::scene::named_parameters::ParameterTable, parameters: crate::scene::named_parameters::ParameterTable,
dirty: bool, dirty: bool,
active_layer: String, active_layer: String,
hatches: FxHashMap<codec::Handle, crate::scene::HatchModel>,
images: FxHashMap<codec::Handle, crate::scene::ImageModel>,
meshes: FxHashMap<codec::Handle, crate::scene::MeshLodSet>,
block_meshes: FxHashMap<codec::Handle, crate::scene::MeshLodSet>,
solid_models: FxHashMap<codec::Handle, kernel::brep::Body>,
} }
pub(super) struct BlockEditHistory { pub(super) struct BlockEditHistory {
@@ -76,14 +82,21 @@ impl OpenCADStudio {
} }
} }
fn block_edit_checkpoint(&self, i: usize) -> BlockEditCheckpoint { fn block_edit_checkpoint(&mut self, i: usize) -> BlockEditCheckpoint {
let tab = &self.tabs[i]; let tab = &mut self.tabs[i];
let structure = tab.scene.document.snapshot_structure();
BlockEditCheckpoint { BlockEditCheckpoint {
document: tab.scene.document.clone(), document: tab.scene.document.clone(),
structure,
constraints: tab.scene.parametric_constraints.clone(), constraints: tab.scene.parametric_constraints.clone(),
parameters: tab.scene.named_parameters.clone(), parameters: tab.scene.named_parameters.clone(),
dirty: tab.dirty, dirty: tab.dirty,
active_layer: tab.active_layer.clone(), active_layer: tab.active_layer.clone(),
hatches: tab.scene.hatches.clone(),
images: tab.scene.images.clone(),
meshes: tab.scene.meshes.clone(),
block_meshes: tab.scene.block_meshes.clone(),
solid_models: tab.scene.solid_models.clone(),
} }
} }
@@ -107,43 +120,67 @@ impl OpenCADStudio {
let history = tab.block_edit_history.as_ref()?; let history = tab.block_edit_history.as_ref()?;
let checkpoint = &history.checkpoint; let checkpoint = &history.checkpoint;
let session = tab.active_block_edit_session()?; let session = tab.active_block_edit_session()?;
let before_doc = if for_save { let (before_doc, before_structure_base) = if for_save {
session if let Some(ref pre_doc) = session.unpersisted_pre_document {
.unpersisted_pre_document let mut pre_clone = pre_doc.clone();
.as_ref() let pre_struct = pre_clone.snapshot_structure();
.unwrap_or(&checkpoint.document) (pre_doc, pre_struct)
} else {
(&checkpoint.document, checkpoint.structure.clone())
}
} else { } else {
&checkpoint.document (&checkpoint.document, checkpoint.structure.clone())
}; };
let before: FxHashMap<_, _> = before_doc let mut candidate_handles: FxHashSet<codec::Handle> = FxHashSet::default();
.entities() if let Some(record) = before_doc.block_records.get(&session.block_name) {
.map(|e| { candidate_handles.extend(record.entity_handles.iter().copied());
let h = e.common().handle; if !record.block_entity_handle.is_null() {
(h, before_doc.get_entity_arc(h).unwrap()) candidate_handles.insert(record.block_entity_handle);
}) }
.collect(); if !record.block_end_handle.is_null() {
let after: FxHashMap<_, _> = tab candidate_handles.insert(record.block_end_handle);
.scene }
.document }
.entities() if let Some(record) = tab.scene.document.block_records.get(&session.block_name) {
.map(|e| { candidate_handles.extend(record.entity_handles.iter().copied());
let h = e.common().handle; if !record.block_entity_handle.is_null() {
(h, tab.scene.document.get_entity_arc(h).unwrap()) candidate_handles.insert(record.block_entity_handle);
}) }
.collect(); if !record.block_end_handle.is_null() {
let mut handles: FxHashSet<_> = before.keys().copied().collect(); candidate_handles.insert(record.block_end_handle);
handles.extend(after.keys().copied()); }
let mut entities: Vec<_> = handles }
let mut sub_handles = Vec::new();
for &h in &candidate_handles {
let entity_opt = before_doc
.get_entity(h)
.or_else(|| tab.scene.document.get_entity(h));
if let Some(EntityType::Insert(ins)) = entity_opt {
for attr in &ins.attributes {
sub_handles.push(attr.common.handle);
}
if let Some(seqend) = ins.seqend_handle {
sub_handles.push(seqend);
}
}
}
candidate_handles.extend(sub_handles);
let mut entities: Vec<_> = candidate_handles
.into_iter() .into_iter()
.filter_map(|h| { .filter_map(|h| {
let a = before.get(&h); let a = before_doc.get_entity_arc(h);
let b = after.get(&h); let b = tab.scene.document.get_entity_arc(h);
(a != b).then(|| (h, a.cloned(), b.cloned())) match (&a, &b) {
(Some(a_arc), Some(b_arc)) if std::sync::Arc::ptr_eq(a_arc, b_arc) => None,
(Some(a_arc), Some(b_arc)) if a_arc == b_arc => None,
(None, None) => None,
_ => Some((h, a, b)),
}
}) })
.collect(); .collect();
entities.sort_by_key(|(handle, _, _)| handle.value()); entities.sort_by_key(|(handle, _, _)| handle.value());
let mut before_structure = before_doc.clone(); let mut before_structure = before_structure_base;
before_structure = before_structure.snapshot_structure();
let mut after_structure = current_structure; let mut after_structure = current_structure;
// The editor temporarily uses Model rendering. Drawing history must // The editor temporarily uses Model rendering. Drawing history must
// retain the layout to which the editor returns. // retain the layout to which the editor returns.
@@ -230,6 +267,22 @@ impl OpenCADStudio {
} }
pub(super) fn block_edit_dirty(&mut self, i: usize) -> bool { pub(super) fn block_edit_dirty(&mut self, i: usize) -> bool {
let tab = &self.tabs[i];
let Some(history) = tab.block_edit_history.as_ref() else {
return false;
};
let Some(session) = tab.active_block_edit_session() else {
return false;
};
if tab.history.undo_stack.is_empty()
&& tab.history.pending.is_none()
&& session.unpersisted_pre_document.is_none()
&& history.checkpoint.active_layer == tab.active_layer
&& history.checkpoint.dirty == tab.dirty
&& history.checkpoint.parameters == tab.scene.named_parameters
{
return false;
}
self.block_edit_delta(i, false).is_some() self.block_edit_delta(i, false).is_some()
} }
@@ -498,7 +551,7 @@ impl OpenCADStudio {
self.visibility_popup = None; self.visibility_popup = None;
history.checkpoint = self.block_edit_checkpoint(i); history.checkpoint = self.block_edit_checkpoint(i);
self.tabs[i].block_edit_history = Some(history); self.tabs[i].block_edit_history = Some(history);
self.tabs[i].scene.rebuild_derived_caches(); self.tabs[i].scene.bump_geometry();
self.tabs[i].refresh_active_ucs(); self.tabs[i].refresh_active_ucs();
self.refresh_block_palette(); self.refresh_block_palette();
self.refresh_properties(); self.refresh_properties();
@@ -586,6 +639,11 @@ impl OpenCADStudio {
self.tabs[i].scene.named_parameters = history.checkpoint.parameters; self.tabs[i].scene.named_parameters = history.checkpoint.parameters;
self.tabs[i].active_layer = history.checkpoint.active_layer; self.tabs[i].active_layer = history.checkpoint.active_layer;
self.tabs[i].dirty = history.checkpoint.dirty; self.tabs[i].dirty = history.checkpoint.dirty;
self.tabs[i].scene.hatches = history.checkpoint.hatches;
self.tabs[i].scene.images = history.checkpoint.images;
self.tabs[i].scene.meshes = history.checkpoint.meshes;
self.tabs[i].scene.block_meshes = history.checkpoint.block_meshes;
self.tabs[i].scene.solid_models = history.checkpoint.solid_models;
} }
self.tabs[i].history = history.main; self.tabs[i].history = history.main;
self.tabs[i].scene.object_isolation = history.return_isolation; self.tabs[i].scene.object_isolation = history.return_isolation;
@@ -596,6 +654,7 @@ impl OpenCADStudio {
None, None,
&session.return_layout, &session.return_layout,
&session.return_camera, &session.return_camera,
!discard,
); );
self.cancel_attr_editor(); self.cancel_attr_editor();
self.discard_block_tests(self.tabs[i].id); self.discard_block_tests(self.tabs[i].id);
+22
View File
@@ -1333,3 +1333,25 @@ fn locked_bedit_with_preselected_insert_opens_directly() {
"EDIT_ME" "EDIT_ME"
); );
} }
#[test]
fn locked_bedit_clean_close_preserves_block_epoch_and_render_caches() {
let (mut app, reference) = fixture();
let tab = app.active_tab;
let epoch_before = app.tabs[tab].scene.block_epoch;
begin(&mut app, reference);
let _ = app.dispatch_command("BCLOSE");
assert_eq!(app.tabs[tab].scene.block_epoch, epoch_before);
}
#[test]
fn locked_bedit_discard_preserves_block_epoch_and_render_caches() {
let (mut app, reference) = fixture();
let tab = app.active_tab;
let epoch_before = app.tabs[tab].scene.block_epoch;
begin(&mut app, reference);
circle(&mut app, 1.0);
let _ = app.dispatch_command("BCLOSE");
let _ = app.update(Message::BlockEditChangesDiscard);
assert_eq!(app.tabs[tab].scene.block_epoch, epoch_before);
}
+4 -1
View File
@@ -1080,6 +1080,7 @@ impl OpenCADStudio {
return_block: Option<&str>, return_block: Option<&str>,
return_layout: &str, return_layout: &str,
return_camera: &crate::scene::view::camera::Camera, return_camera: &crate::scene::view::camera::Camera,
saved: bool,
) { ) {
self.tabs[i].scene.block_edit_block = None; self.tabs[i].scene.block_edit_block = None;
self.tabs[i].scene.deselect_all(); self.tabs[i].scene.deselect_all();
@@ -1121,7 +1122,9 @@ impl OpenCADStudio {
self.tabs[i].scene.camera_generation += 1; self.tabs[i].scene.camera_generation += 1;
self.tabs[i].last_synced_camera_gen = self.tabs[i].scene.camera_generation; self.tabs[i].last_synced_camera_gen = self.tabs[i].scene.camera_generation;
self.tabs[i].scene.rebuild_derived_caches(); if !saved {
self.tabs[i].scene.bump_geometry_no_blocks();
}
let layers = self.tabs[i].scene.document.layers.clone(); let layers = self.tabs[i].scene.document.layers.clone();
let viewports = self.tabs[i].scene.viewport_list(); let viewports = self.tabs[i].scene.viewport_list();
self.tabs[i].layers.sync_with_viewports(&layers, viewports); self.tabs[i].layers.sync_with_viewports(&layers, viewports);