perf: make edit history incremental

Replace full-document snapshots with Arc first-touch deltas and batch cache invalidation. Reuse interaction and GPU category caches across unrelated edits.
This commit is contained in:
Hakan Seven committed 2026-07-24 23:25:33 +03:00
1 parent 90714d34b2
commit ea93e169ea
19 files changed
+979 -339

No files matched your search

+100 -31
View File
@@ -335,7 +335,7 @@ impl Scene {
// overwritten) so an undo can restore it, and poison if this replace
// also created a layer or crossed a block boundary.
if self.is_recording_undo() {
let before = self.document.get_entity(handle).cloned();
let before = self.document.get_entity_arc(handle);
self.record_undo_before(handle, before);
if creates_layer || affects_blocks {
self.poison_undo_recording();
@@ -382,8 +382,8 @@ impl Scene {
/// [`Scene::update_entity`]; used by delta-undo when it re-applies an
/// entity's before / after image so the fills and meshes follow.
pub(crate) fn reseed_derived_caches(&mut self, handle: Handle) {
let (hatch_seed, image_seed, mesh_seed) = match self.document.get_entity(handle) {
None => (None, None, None),
let (hatch_seed, image_seed) = match self.document.get_entity(handle) {
None => (None, None),
Some(entity) => {
let hatch_seed = if let EntityType::Hatch(dxf) = entity {
let color = self.render_style(entity).0;
@@ -395,22 +395,7 @@ impl Scene {
None
};
let image_seed = self.image_seed_for(entity);
let facet_res = self.document.header.facet_resolution;
let isolines = self.document.header.isolines.max(0) as usize;
let mesh_seed = if matches!(
entity,
EntityType::Solid3D(_)
| EntityType::Region(_)
| EntityType::Body(_)
| EntityType::Surface(_)
) {
let color = self.render_style(entity).0;
crate::entities::solid3d::tessellate_volume(entity, color, facet_res, isolines)
.map(offset_mesh_lod_set)
} else {
None
};
(hatch_seed, image_seed, mesh_seed)
(hatch_seed, image_seed)
}
};
self.hatches.remove(&handle);
@@ -423,8 +408,78 @@ impl Scene {
if let Some(model) = image_seed {
self.images.insert(handle, model);
}
if let Some(model) = mesh_seed {
self.meshes.insert(handle, model);
self.refresh_meshes_for_handles(&[handle]);
}
/// Re-tessellate only the named ACIS entities. The former edit path
/// cleared and rebuilt every solid in the drawing when one selected solid
/// moved or was copied.
pub fn refresh_meshes_for_handles(&mut self, handles: &[Handle]) {
if handles.is_empty() {
return;
}
let layout_blocks: std::collections::HashSet<Handle> = self
.document
.objects
.values()
.filter_map(|o| match o {
acadrust::objects::ObjectType::Layout(l) if !l.block_record.is_null() => {
Some(l.block_record)
}
_ => None,
})
.collect();
let entries: Vec<(Handle, std::sync::Arc<EntityType>, [f32; 4], bool)> = handles
.iter()
.filter_map(|&handle| {
let entity = self.document.get_entity_arc(handle)?;
if !matches!(
entity.as_ref(),
EntityType::Solid3D(_)
| EntityType::Region(_)
| EntityType::Body(_)
| EntityType::Surface(_)
) {
return None;
}
let color = self.render_style(entity.as_ref()).0;
let top_level = layout_blocks.contains(&entity.common().owner_handle);
Some((handle, entity, color, top_level))
})
.collect();
for handle in handles {
self.meshes.remove(handle);
self.block_meshes.remove(handle);
self.solid_models.remove(handle);
}
let facet_res = self.document.header.facet_resolution;
let isolines = self.document.header.isolines.max(0) as usize;
use crate::par::prelude::*;
let built: Vec<(Handle, MeshLodSet, bool)> = entries
.into_par_iter()
.filter_map(|(handle, entity, color, top_level)| {
crate::entities::solid3d::tessellate_volume(
entity.as_ref(),
color,
facet_res,
isolines,
)
.map(|mesh| {
let mesh = if top_level {
offset_mesh_lod_set(mesh)
} else {
mesh
};
(handle, mesh, top_level)
})
})
.collect();
for (handle, mesh, top_level) in built {
if top_level {
self.meshes.insert(handle, mesh);
} else {
self.block_meshes.insert(handle, mesh);
}
}
}
@@ -1834,6 +1889,11 @@ impl Scene {
/// Decode and cache all RasterImage entities from the current document.
/// Silently skips images whose files cannot be read.
pub fn populate_images_from_document(&mut self) {
self.populate_images_from_document_unbumped();
self.bump_geometry();
}
fn populate_images_from_document_unbumped(&mut self) {
self.images.clear();
let entries: Vec<(Handle, acadrust::entities::RasterImage)> = self
.document
@@ -1851,7 +1911,6 @@ impl Scene {
self.images.insert(handle, model);
}
}
self.bump_geometry();
}
/// Rebuild the cached fill model (hatch / DXF SOLID) for `handle` after
@@ -1876,6 +1935,11 @@ impl Scene {
}
pub fn populate_hatches_from_document(&mut self) {
self.populate_hatches_from_document_unbumped();
self.bump_geometry();
}
fn populate_hatches_from_document_unbumped(&mut self) {
self.hatches.clear();
let entries: Vec<(Handle, EntityType)> = self
@@ -1907,8 +1971,6 @@ impl Scene {
model.map(|m| (handle, m))
})
.collect();
self.bump_geometry();
}
/// Tessellate all `Solid3D` entities in the current document into
@@ -1932,7 +1994,7 @@ impl Scene {
}
pub fn populate_meshes_from_document(&mut self) {
self.populate_meshes_impl(false);
self.populate_meshes_impl(false, true);
}
/// Like [`populate_meshes_from_document`] but tessellates only solids
@@ -1946,10 +2008,10 @@ impl Scene {
/// only the newly merged xref solids" — the dominant cost when a drawing
/// attaches several large xrefs. (#203)
pub fn populate_missing_meshes_from_document(&mut self) {
self.populate_meshes_impl(true);
self.populate_meshes_impl(true, true);
}
fn populate_meshes_impl(&mut self, incremental: bool) {
fn populate_meshes_impl(&mut self, incremental: bool, bump: bool) {
if !incremental {
self.meshes.clear();
self.block_meshes.clear();
@@ -2016,15 +2078,18 @@ impl Scene {
}
}
self.bump_geometry();
if bump {
self.bump_geometry();
}
}
/// Rebuild hatch / image / mesh caches after the document is modified
/// outside the normal `add_entity` path (e.g. REFCLOSE SAVE).
pub fn rebuild_derived_caches(&mut self) {
self.populate_hatches_from_document();
self.populate_images_from_document();
self.populate_meshes_from_document();
self.populate_hatches_from_document_unbumped();
self.populate_images_from_document_unbumped();
self.populate_meshes_impl(false, false);
self.bump_geometry();
}
/// Build a solid-fill HatchModel for a DXF Solid entity.
@@ -2218,13 +2283,17 @@ impl Scene {
}
pub fn clear(&mut self) {
self.document.record_all_entities_for_transaction();
self.document = CadDocument::new();
self.selected = HashSet::default();
self.preview_wires = vec![];
self.preview_text = vec![];
self.current_layout = "Model".to_string();
self.hatches = HashMap::default();
self.images = HashMap::default();
self.meshes = HashMap::default();
self.block_meshes = HashMap::default();
self.solid_models = HashMap::default();
*self.camera.borrow_mut() = Camera::default();
self.camera_generation += 1;
self.bump_geometry();
+209 -75
View File
@@ -177,7 +177,7 @@ pub struct UndoRecording {
/// so repeated touches within one command keep the true pre-command state).
/// A value of `None` marks an entity *added* by the command (no prior
/// state). `order` preserves first-touch order for a deterministic delta.
before: HashMap<Handle, Option<EntityType>>,
before: HashMap<Handle, Option<Arc<EntityType>>>,
order: Vec<Handle>,
poisoned: bool,
}
@@ -197,7 +197,7 @@ impl UndoRecording {
/// order. A `None` before-image means the entity was added by the command.
/// The app pairs each with the entity's current (after) state to build the
/// invertible delta entry.
pub fn into_before_images(mut self) -> Vec<(Handle, Option<EntityType>)> {
pub fn into_before_images(mut self) -> Vec<(Handle, Option<Arc<EntityType>>)> {
self.order
.drain(..)
.map(|h| (h, self.before.remove(&h).flatten()))
@@ -983,6 +983,16 @@ pub struct Scene {
Arc<crate::scene::pick::interaction_index::InteractionIndex>,
)>,
>,
/// Large resident interaction index kept as an immutable base across small
/// entity edits. Geometry-journal handles form a tombstone/delta overlay;
/// the exact index is rebuilt only after the overlay grows too large.
interaction_base_index_cache: RefCell<
Option<(
u64,
u64,
Arc<crate::scene::pick::interaction_index::InteractionIndex>,
)>,
>,
/// Auxiliary broad phase for objects that have no wire representation:
/// hatch-only and solid-only block instances.
interaction_handle_index_cache: RefCell<
@@ -1286,6 +1296,7 @@ impl Scene {
selection_generation: 0,
wire_cache: RefCell::new(None),
interaction_index_cache: RefCell::new(Vec::new()),
interaction_base_index_cache: RefCell::new(None),
interaction_handle_index_cache: RefCell::new(None),
interaction_aux_work_cache: std::cell::Cell::new(None),
sort_cache: RefCell::new(None),
@@ -1585,7 +1596,7 @@ impl Scene {
/// is `None` for a freshly added entity. No-op when not recording — the
/// callers guard with [`Scene::is_recording_undo`] so the clone is skipped
/// entirely on the common (no-recording) path.
pub(crate) fn record_undo_before(&mut self, handle: Handle, before: Option<EntityType>) {
pub(crate) fn record_undo_before(&mut self, handle: Handle, before: Option<Arc<EntityType>>) {
if let Some(rec) = self.undo_recording.as_mut() {
if !rec.before.contains_key(&handle) {
rec.order.push(handle);
@@ -1606,35 +1617,14 @@ impl Scene {
/// Re-apply one side of an entity delta to the document. For each
/// `(handle, before, after)`, install the chosen `target` — `before` when
/// `undo`, `after` on redo: overwrite the entity in place, re-insert it with
/// its original handle, or remove it — reseeding the per-handle derived
/// caches so fills/meshes follow. Returns the exact per-handle change list
/// for the caller to report via [`Scene::bump_entities`]; it does not bump,
/// touch the selection, or rebuild any whole-document cache.
///
/// `remove_entity` leaves a handle dangling in its owner block record's
/// `entity_handles` (harmless — a render/save lookup miss is skipped), so a
/// re-insert's `add_entity` push would make it appear twice and emit the
/// entity twice on save. Every to-be-re-inserted handle is therefore
/// stripped from the block records in one pass first, leaving exactly one
/// entry after the re-insert.
/// its original handle, or remove it. Returns the exact per-handle change
/// list; derived-cache reseeding and the geometry bump are deferred so a
/// multi-step undo/redo can process each final handle only once.
pub(crate) fn apply_entity_delta(
&mut self,
entities: &[(Handle, Option<EntityType>, Option<EntityType>)],
entities: &[(Handle, Option<Arc<EntityType>>, Option<Arc<EntityType>>)],
undo: bool,
) -> Vec<(Handle, ChangeKind)> {
let reinsert: HashSet<Handle> = entities
.iter()
.filter_map(|(h, before, after)| {
let target = if undo { before } else { after };
(target.is_some() && self.document.get_entity(*h).is_none()).then_some(*h)
})
.collect();
if !reinsert.is_empty() {
for br in self.document.block_records.iter_mut() {
br.entity_handles.retain(|h| !reinsert.contains(h));
}
}
let mut changes: Vec<(Handle, ChangeKind)> = Vec::with_capacity(entities.len());
for (h, before, after) in entities {
let target = if undo { before } else { after };
@@ -1642,52 +1632,25 @@ impl Scene {
match target {
Some(ent) => {
if existed {
if let Some(slot) = self.document.get_entity_mut(*h) {
*slot = ent.clone();
}
let _ = self.document.replace_entity_arc(*h, Arc::clone(ent));
changes.push((*h, ChangeKind::Modified));
} else {
// Re-insert with the original handle: the stored image
// keeps it, and document.add_entity honours a preset,
// non-null handle (routing to its owner block record).
let _ = self.document.add_entity(ent.clone());
// Removal keeps the original block membership in place;
// restoring only the flat storage avoids an O(all block
// members) scan and cannot duplicate the owner link.
let _ = self.document.restore_entity_arc(Arc::clone(ent));
changes.push((*h, ChangeKind::Added));
}
self.reseed_derived_caches(*h);
}
None => {
if existed {
self.document.remove_entity(*h);
// Drops the now-absent entity's hatch/image/mesh caches.
self.reseed_derived_caches(*h);
self.document.remove_entity_arc(*h);
changes.push((*h, ChangeKind::Removed));
}
}
}
}
// Integrity net: no re-inserted handle may appear more than once across
// the block records (a duplicate would emit the entity twice on save).
// Debug-only, and skipped entirely when nothing was re-inserted (the
// common transform/property-edit case).
#[cfg(debug_assertions)]
if !reinsert.is_empty() {
let mut counts: HashMap<Handle, u32> = HashMap::default();
for br in self.document.block_records.iter() {
for h in &br.entity_handles {
if reinsert.contains(h) {
*counts.entry(*h).or_default() += 1;
}
}
}
for (h, c) in counts {
debug_assert!(
c <= 1,
"delta re-insert duplicated handle {h:?} in block records ({c}×)"
);
}
}
changes
}
@@ -1853,6 +1816,41 @@ impl Scene {
}
}
/// Recolour only the named cached solids after a property edit.
pub fn recolor_meshes_for_handles(&mut self, handles: &[Handle]) {
let bg = self.bg_color;
let colors: HashMap<Handle, [f32; 4]> = handles
.iter()
.filter_map(|&handle| {
self.document.get_entity(handle).map(|entity| {
let mut color = self.render_style(entity).0;
if self
.refedit_keep
.as_ref()
.is_some_and(|keep| !keep.contains(&handle))
{
color = crate::scene::cache::block_cache::fade_toward_bg(color, bg);
}
(handle, color)
})
})
.collect();
for handle in handles {
let Some(color) = colors.get(handle) else {
continue;
};
if let Some(set) = self.meshes.get_mut(handle) {
for lod in &mut set.lods {
lod.color = *color;
}
} else if let Some(set) = self.block_meshes.get_mut(handle) {
for lod in &mut set.lods {
lod.color = *color;
}
}
}
}
/// Enter / leave the REFEDIT fade. `keep` holds the edited entities (left
/// bright); everything else renders faded. Re-tessellates wires and
/// recolours solids so the change shows immediately. (#136)
@@ -4572,6 +4570,13 @@ impl Scene {
let entry = cache.remove(position);
let index = Arc::clone(&entry.3);
cache.push(entry);
if self.current_layout == "Model" {
*self.interaction_base_index_cache.borrow_mut() = Some((
self.geometry_epoch,
self.interaction_space_key(),
Arc::clone(&index),
));
}
return index;
}
}
@@ -4586,9 +4591,118 @@ impl Scene {
if cache.len() > MAX_CACHED_SOURCES {
cache.remove(0);
}
if self.current_layout == "Model" {
*self.interaction_base_index_cache.borrow_mut() = Some((
self.geometry_epoch,
self.interaction_space_key(),
Arc::clone(&index),
));
}
index
}
const INTERACTION_OVERLAY_MAX_HANDLES: usize = 2_048;
fn interaction_overlay_base(
&self,
) -> Option<(
Arc<crate::scene::pick::interaction_index::InteractionIndex>,
Vec<(Handle, ChangeKind)>,
)> {
let space_key = self.interaction_space_key();
let (epoch, index) = {
let cache = self.interaction_base_index_cache.borrow();
let (epoch, cached_space, index) = cache.as_ref()?;
if *epoch == self.geometry_epoch || *cached_space != space_key {
return None;
}
(*epoch, Arc::clone(index))
};
let changes = self.replay_since(epoch)?;
(changes.len() <= Self::INTERACTION_OVERLAY_MAX_HANDLES).then_some((index, changes))
}
fn interaction_overlay_wires(
&self,
base_handles: impl IntoIterator<Item = u64>,
changes: &[(Handle, ChangeKind)],
) -> Arc<Vec<WireModel>> {
let changed: HashSet<Handle> = changes.iter().map(|(handle, _)| *handle).collect();
let mut handles: HashSet<Handle> = base_handles
.into_iter()
.map(Handle::new)
.filter(|handle| !changed.contains(handle))
.collect();
handles.extend(changes.iter().filter_map(|(handle, kind)| {
(!matches!(kind, ChangeKind::Removed)).then_some(*handle)
}));
let memo = self.resident_tess_memo.borrow();
let mut wires = Vec::new();
let mut misses = Vec::new();
for handle in handles {
if self.document.get_entity(handle).is_none() {
continue;
}
if let Some(entity_wires) = memo.get(&handle) {
wires.extend(entity_wires.iter().cloned());
} else {
misses.push(handle);
}
}
drop(memo);
if !misses.is_empty() {
wires.extend(self.wire_models_for(&misses));
}
Arc::new(wires)
}
fn indexed_interaction_candidates_xy(
&self,
wires: Arc<Vec<WireModel>>,
aabb: [f64; 4],
) -> crate::scene::pick::interaction_index::InteractionCandidates {
if let Some((base, changes)) = self.interaction_overlay_base() {
let handles = base.query_wire_handles_xy(aabb);
let local = self.interaction_overlay_wires(handles, &changes);
let local_index =
crate::scene::pick::interaction_index::InteractionIndex::build(&local);
return local_index.query_xy(local, aabb);
}
self.interaction_index(&wires).query_xy(wires, aabb)
}
fn indexed_interaction_candidates_screen(
&self,
wires: Arc<Vec<WireModel>>,
screen_rect: [f32; 4],
view_rot: glam::Mat4,
eye: glam::DVec3,
bounds: iced::Rectangle,
) -> crate::scene::pick::interaction_index::InteractionCandidates {
if let Some((base, changes)) = self.interaction_overlay_base() {
let handles = base.query_wire_handles_screen(screen_rect, view_rot, eye, bounds);
let local = self.interaction_overlay_wires(handles, &changes);
let local_index =
crate::scene::pick::interaction_index::InteractionIndex::build(&local);
return local_index.query_screen(local, screen_rect, view_rot, eye, bounds);
}
self.interaction_index(&wires)
.query_screen(wires, screen_rect, view_rot, eye, bounds)
}
fn indexed_interaction_pick_radius(
&self,
wires: &Arc<Vec<WireModel>>,
base_radius_px: f32,
) -> f32 {
if let Some((base, _)) = self.interaction_overlay_base() {
base.pick_radius_px(base_radius_px)
} else {
self.interaction_index(wires).pick_radius_px(base_radius_px)
}
}
fn interaction_source_is_resident(
&self,
wires: &Arc<Vec<WireModel>>,
@@ -4611,11 +4725,33 @@ impl Scene {
) -> Arc<crate::scene::pick::interaction_index::InteractionHandleIndex> {
let space_key = self.interaction_space_key();
{
let cache = self.interaction_handle_index_cache.borrow();
if let Some((epoch, cached_space, index)) = cache.as_ref() {
if *epoch == self.geometry_epoch && *cached_space == space_key {
return Arc::clone(index);
let reuse = {
let cache = self.interaction_handle_index_cache.borrow();
match cache.as_ref() {
Some((epoch, cached_space, index))
if *cached_space == space_key
&& self.category_cache_valid(*epoch, |handle| {
self.hatches.contains_key(&handle)
|| self.meshes.contains_key(&handle)
|| self.block_meshes.contains_key(&handle)
|| matches!(
self.document.get_entity(handle),
Some(EntityType::Insert(_))
)
}) =>
{
Some(Arc::clone(index))
}
_ => None,
}
};
if let Some(index) = reuse {
if let Some((epoch, _, _)) =
self.interaction_handle_index_cache.borrow_mut().as_mut()
{
*epoch = self.geometry_epoch;
}
return index;
}
}
let mut entries: Vec<(u64, [f64; 6])> = Vec::new();
@@ -4708,9 +4844,8 @@ impl Scene {
{
return crate::scene::pick::interaction_index::InteractionCandidates::all(wires);
}
let index = self.interaction_index(&wires);
let radius_px = if include_line_weight {
index.pick_radius_px(radius_px)
self.indexed_interaction_pick_radius(&wires, radius_px)
} else {
radius_px
};
@@ -4728,7 +4863,7 @@ impl Scene {
(1.0 - ndc.y) * 0.5 * bounds.height,
);
let radius = radius_px.max(0.0);
return index.query_screen(
return self.indexed_interaction_candidates_screen(
wires,
[
screen.x - radius,
@@ -4758,7 +4893,7 @@ impl Scene {
cursor.x + radius,
cursor.y + radius,
];
index.query_xy(wires, query)
self.indexed_interaction_candidates_xy(wires, query)
}
/// Shared rectangular broad phase for box/lasso/fence and command windows.
@@ -4782,10 +4917,9 @@ impl Scene {
return crate::scene::pick::interaction_index::InteractionCandidates::all(wires);
}
if flat_ortho {
self.interaction_index(&wires).query_xy(wires, aabb)
self.indexed_interaction_candidates_xy(wires, aabb)
} else {
self.interaction_index(&wires)
.query_screen(wires, screen_rect, view_rot, eye, bounds)
self.indexed_interaction_candidates_screen(wires, screen_rect, view_rot, eye, bounds)
}
}
@@ -4820,7 +4954,7 @@ impl Scene {
pub fn interaction_handles_in_world_aabb(&self, aabb: [f64; 4]) -> HashSet<Handle> {
let wires = self.hit_test_wires();
let candidates = self.interaction_index(&wires).query_xy(wires, aabb);
let candidates = self.indexed_interaction_candidates_xy(wires, aabb);
let mut handles: HashSet<Handle> = candidates
.iter()
.filter_map(|wire| Self::handle_from_wire_name(&wire.name))
@@ -6179,11 +6313,11 @@ mod delta_undo_tests {
fn build_delta(
scene: &Scene,
rec: UndoRecording,
) -> Vec<(Handle, Option<EntityType>, Option<EntityType>)> {
) -> Vec<(Handle, Option<Arc<EntityType>>, Option<Arc<EntityType>>)> {
rec.into_before_images()
.into_iter()
.map(|(h, before)| {
let after = scene.document.get_entity(h).cloned();
let after = scene.document.get_entity_arc(h);
(h, before, after)
})
.collect()
+36 -3
View File
@@ -77,6 +77,39 @@ fn mirror_true_text_flags(e: &mut EntityType) {
}
impl Scene {
/// Invalidate a dimension's baked block while capturing every removed
/// sub-entity for an active history transaction.
pub fn invalidate_dim_block_recorded(&mut self, handle: Handle) {
if self.is_recording_undo() {
let owned: Vec<Handle> = self
.document
.get_entity(handle)
.and_then(|entity| match entity {
EntityType::Dimension(d) => {
let name = d.base().block_name.clone();
self.document.block_records.get(&name).map(|record| {
let mut handles = record.entity_handles.clone();
handles.push(record.block_entity_handle);
handles.push(record.block_end_handle);
handles
})
}
_ => None,
})
.unwrap_or_default();
if let Some(before) = self.document.get_entity_arc(handle) {
self.record_undo_before(handle, Some(before));
}
for owned_handle in owned {
if let Some(before) = self.document.get_entity_arc(owned_handle) {
self.record_undo_before(owned_handle, Some(before));
}
}
self.poison_undo_recording();
}
crate::modules::draw::modify::explode::invalidate_dim_block(&mut self.document, handle);
}
// ── Modify (transform / copy) ─────────────────────────────────────────
pub fn transform_entities(&mut self, handles: &[Handle], t: &EntityTransform) {
@@ -137,7 +170,7 @@ impl Scene {
for &h in handles {
// Delta-undo: capture the pre-transform image before mutating.
if self.is_recording_undo() {
let before = self.document.get_entity(h).cloned();
let before = self.document.get_entity_arc(h);
self.record_undo_before(h, before);
}
if let Some(entity) = self.document.get_entity_mut(h) {
@@ -172,7 +205,7 @@ impl Scene {
// must capture their before-images here or a dimension move won't undo.
for h in &dim_block_subs {
if self.is_recording_undo() {
let before = self.document.get_entity(*h).cloned();
let before = self.document.get_entity_arc(*h);
self.record_undo_before(*h, before);
}
if let Some(entity) = self.document.get_entity_mut(*h) {
@@ -409,7 +442,7 @@ impl Scene {
// the grip only moved a definition point, so the baked graphics are
// stale — drop them so tessellation falls back to the live geometry
// and the next save re-bakes (no-op for non-dimensions). (#398)
crate::modules::draw::modify::explode::invalidate_dim_block(&mut self.document, handle);
self.invalidate_dim_block_recorded(handle);
// Translate MeshModel vertices by the same delta the grip applied.
if let Some(old) = old_por {
+34
View File
@@ -455,6 +455,10 @@ impl<T: Copy + Ord> SpatialSet<T> {
pub struct InteractionIndex {
wires: SpatialSet<u32>,
/// Stable entity handle for each source wire. Lets a stale-but-valid base
/// index feed an incremental overlay after small edits without retaining
/// the old heavyweight wire set or trusting shifted vector indices.
wire_handles: Vec<Option<u64>>,
segments: SpatialSet<SegmentRef>,
snap_points: SpatialSet<SnapPointRef>,
key_vertices: SpatialSet<KeyVertexRef>,
@@ -518,6 +522,10 @@ impl InteractionIndex {
}
pub fn build(wires: &[WireModel]) -> Self {
let wire_handles: Vec<Option<u64>> = wires
.iter()
.map(|wire| wire.name.parse::<u64>().ok())
.collect();
let mut wire_entries = Vec::with_capacity(wires.len());
let mut segment_entries = Vec::new();
let mut snap_point_entries = Vec::new();
@@ -692,6 +700,7 @@ impl InteractionIndex {
Self {
wires,
wire_handles,
segments,
snap_points,
key_vertices,
@@ -708,6 +717,31 @@ impl InteractionIndex {
base_radius_px.max(self.max_line_half_width_px)
}
fn queried_wire_handles(&self, mut indices: Vec<u32>) -> Vec<u64> {
indices.extend_from_slice(&self.unbounded_wires);
let mut handles: Vec<u64> = indices
.into_iter()
.filter_map(|index| self.wire_handles.get(index as usize).copied().flatten())
.collect();
handles.sort_unstable();
handles.dedup();
handles
}
pub fn query_wire_handles_xy(&self, aabb: [f64; 4]) -> Vec<u64> {
self.queried_wire_handles(self.wires.query_xy(aabb))
}
pub fn query_wire_handles_screen(
&self,
screen_rect: [f32; 4],
view_rot: Mat4,
eye: DVec3,
bounds: Rectangle,
) -> Vec<u64> {
self.queried_wire_handles(self.wires.query_screen(screen_rect, view_rot, eye, bounds))
}
pub fn query_xy(&self, wires: Arc<Vec<WireModel>>, aabb: [f64; 4]) -> InteractionCandidates {
let mut wire_indices = self.wires.query_xy(aabb);
wire_indices.extend_from_slice(&self.unbounded_wires);
+22
View File
@@ -217,6 +217,19 @@ pub struct Pipeline {
gpu_face3d_fill: Option<Face3DGpu>,
gpu_face3d_edges: Vec<WireGpu>,
pub viewcube: ViewCubePipeline,
/// Strong source guards for category-specific GPU uploads. Holding the old
/// Arc makes pointer identity ABA-safe: an unchanged category reuses the
/// same Arc even when an unrelated entity advances `geometry_epoch`.
pub cached_hatch_source: Option<std::sync::Arc<Vec<HatchModel>>>,
pub cached_wipeout_source: Option<std::sync::Arc<Vec<HatchModel>>>,
pub cached_image_source: Option<std::sync::Arc<Vec<ImageModel>>>,
pub cached_text_source: Option<std::sync::Arc<Vec<text_gpu::TextVertex>>>,
pub cached_mesh_source: Option<std::sync::Arc<Vec<MeshLodSet>>>,
pub cached_face3d_source: Option<std::sync::Arc<Vec<WireModel>>>,
pub cached_face3d_wire_source: Option<std::sync::Arc<Vec<WireModel>>>,
pub cached_face3d_depth_source:
Option<std::sync::Arc<rustc_hash::FxHashMap<u64, [f32; 2]>>>,
pub cached_fill_mode: bool,
/// Last `(geometry_epoch, camera_generation)` value for which GPU buffers
/// were uploaded. We re-upload when either side changes — pan/zoom bumps
/// camera_generation, which triggers re-culling and a fresh upload.
@@ -1458,6 +1471,15 @@ impl Pipeline {
gpu_face3d_fill: None,
gpu_face3d_edges: vec![],
viewcube,
cached_hatch_source: None,
cached_wipeout_source: None,
cached_image_source: None,
cached_text_source: None,
cached_mesh_source: None,
cached_face3d_source: None,
cached_face3d_wire_source: None,
cached_face3d_depth_source: None,
cached_fill_mode: false,
cached_epoch: (u64::MAX, u64::MAX, u64::MAX),
cached_wire_id: u64::MAX,
cached_selection: (u64::MAX, u64::MAX),
+31 -3
View File
@@ -330,6 +330,7 @@ impl Scene {
// ── Erase ─────────────────────────────────────────────────────────────
pub fn erase_entities(&mut self, handles: &[Handle]) {
let handle_set: HashSet<Handle> = handles.iter().copied().collect();
let mut erased: Vec<(Handle, ChangeKind)> = Vec::new();
for &h in handles {
// Objects on a locked layer can't be erased.
@@ -338,13 +339,15 @@ impl Scene {
}
// Delta-undo: capture the removed entity so an undo can re-insert it.
if self.is_recording_undo() {
let before = self.document.get_entity(h).cloned();
let before = self.document.get_entity_arc(h);
self.record_undo_before(h, before);
}
self.document.remove_entity(h);
self.document.remove_entity_arc(h);
self.selected.remove(&h);
self.hatches.remove(&h);
self.images.remove(&h);
self.meshes.remove(&h);
self.block_meshes.remove(&h);
self.solid_models.remove(&h);
erased.push((h, ChangeKind::Removed));
}
@@ -358,7 +361,7 @@ impl Scene {
.filter_map(|obj| match obj {
ObjectType::Group(g) => {
let before = g.entities.len();
g.entities.retain(|h| !handles.contains(h));
g.entities.retain(|h| !handle_set.contains(h));
if g.entities.len() != before {
groups_changed = true;
}
@@ -390,4 +393,29 @@ impl Scene {
// from the tessellation memos too).
self.bump_entities(&erased);
}
/// Restore erased Arc-backed entities without re-linking their still-present
/// block-record handles. Used by OOPS and history replay.
pub fn restore_erased_entities(&mut self, entities: Vec<Arc<EntityType>>) -> Vec<Handle> {
let mut restored = Vec::with_capacity(entities.len());
let mut changes = Vec::with_capacity(entities.len());
for entity in entities {
let handle = entity.common().handle;
if handle.is_null() || self.document.get_entity(handle).is_some() {
continue;
}
if self.is_recording_undo() {
self.record_undo_before(handle, None);
}
if self.document.restore_entity_arc(entity).is_some() {
self.reseed_derived_caches(handle);
restored.push(handle);
changes.push((handle, ChangeKind::Added));
}
}
if !changes.is_empty() {
self.bump_entities(&changes);
}
restored
}
}
+84 -31
View File
@@ -262,6 +262,14 @@ impl shader::Primitive for Primitive {
inner.cached_selection = (u64::MAX, u64::MAX);
inner.cached_mesh_key = (u64::MAX, u64::MAX);
inner.cached_face3d_key = (u64::MAX, false);
inner.cached_hatch_source = None;
inner.cached_wipeout_source = None;
inner.cached_image_source = None;
inner.cached_text_source = None;
inner.cached_mesh_source = None;
inner.cached_face3d_source = None;
inner.cached_face3d_wire_source = None;
inner.cached_face3d_depth_source = None;
inner.render_sig = u64::MAX;
}
// The MSAA / depth / resolve textures are always sized to the
@@ -328,35 +336,70 @@ impl shader::Primitive for Primitive {
// the view toggle so 2D fills stay on even when the user picks
// the Wireframe overlay style.
let face3d_fill_active = fill_mode && !vp.view_wireframe;
if cur_key != inner.cached_epoch {
// Hatches carry a selected-tint, so re-upload on a geometry OR
// a selection change (issue #71); images / meshes only need a
// geometry change.
let geo_changed = vp.geometry_epoch != inner.cached_epoch.0;
let sel_changed = vp.selected_sig != inner.cached_epoch.2;
if geo_changed || sel_changed {
if fill_mode {
inner.upload_hatches(device, queue, &vp.hatches[..]);
inner.upload_wipeouts(device, &vp.wipeout_hatches[..]);
} else {
inner.upload_hatches(device, queue, &[]);
inner.upload_wipeouts(device, &[]);
}
}
if geo_changed {
inner.upload_images(device, queue, &vp.images[..]);
inner.upload_text(device, queue, &vp.text_verts[..]);
}
inner.cached_epoch = cur_key;
let fill_changed = inner.cached_fill_mode != fill_mode;
let hatch_changed = inner
.cached_hatch_source
.as_ref()
.map_or(true, |source| !Arc::ptr_eq(source, &vp.hatches));
let wipeout_changed = inner
.cached_wipeout_source
.as_ref()
.map_or(true, |source| !Arc::ptr_eq(source, &vp.wipeout_hatches));
if hatch_changed || fill_changed {
inner.upload_hatches(
device,
queue,
if fill_mode { &vp.hatches[..] } else { &[] },
);
inner.cached_hatch_source = Some(Arc::clone(&vp.hatches));
}
if wipeout_changed || fill_changed {
inner.upload_wipeouts(
device,
if fill_mode {
&vp.wipeout_hatches[..]
} else {
&[]
},
);
inner.cached_wipeout_source = Some(Arc::clone(&vp.wipeout_hatches));
}
if inner
.cached_image_source
.as_ref()
.map_or(true, |source| !Arc::ptr_eq(source, &vp.images))
{
inner.upload_images(device, queue, &vp.images[..]);
inner.cached_image_source = Some(Arc::clone(&vp.images));
}
if inner
.cached_text_source
.as_ref()
.map_or(true, |source| !Arc::ptr_eq(source, &vp.text_verts))
{
inner.upload_text(device, queue, &vp.text_verts[..]);
inner.cached_text_source = Some(Arc::clone(&vp.text_verts));
}
inner.cached_fill_mode = fill_mode;
inner.cached_epoch = cur_key;
// Face3D edge/fill buffers are world-space and selection-independent
// (upload_face3d takes no selection input), so they only change with
// the geometry or the 3D-fill toggle — never on a pan/orbit. Gating
// here on `(geometry_epoch, face3d_fill_active)` instead of inside the
// `cur_key` block (which carries `camera_generation`) stops a camera
// move from re-walking every wire to rebuild the Face3D fill buffer.
let face3d_key = (vp.geometry_epoch, face3d_fill_active);
if face3d_key != inner.cached_face3d_key {
// on its three source Arcs avoids rebuilding it when another entity
// category alone advances `geometry_epoch`.
let face3d_changed = inner
.cached_face3d_source
.as_ref()
.map_or(true, |source| !Arc::ptr_eq(source, &vp.face3d_wires))
|| inner
.cached_face3d_wire_source
.as_ref()
.map_or(true, |source| !Arc::ptr_eq(source, &vp.wires))
|| inner
.cached_face3d_depth_source
.as_ref()
.map_or(true, |source| !Arc::ptr_eq(source, &vp.draw_depths));
if face3d_changed || face3d_fill_active != inner.cached_face3d_key.1 {
inner.upload_face3d(
device,
&vp.face3d_wires[..],
@@ -364,7 +407,10 @@ impl shader::Primitive for Primitive {
!face3d_fill_active,
&vp.draw_depths,
);
inner.cached_face3d_key = face3d_key;
inner.cached_face3d_source = Some(Arc::clone(&vp.face3d_wires));
inner.cached_face3d_wire_source = Some(Arc::clone(&vp.wires));
inner.cached_face3d_depth_source = Some(Arc::clone(&vp.draw_depths));
inner.cached_face3d_key = (vp.geometry_epoch, face3d_fill_active);
}
// Wire buffers are world-space, so a camera move alone doesn't
// change them — only the view_proj uniform (uploaded every frame).
@@ -531,17 +577,24 @@ impl shader::Primitive for Primitive {
);
inner.cached_selection = sel_key;
}
// Batched solid meshes — geometry-only, so they ride the geometry
// epoch alone and stay resident across camera moves and selection /
// hover changes (no per-pick rebuild of the whole solid set).
if vp.geometry_epoch != inner.cached_mesh_batch_epoch {
// Batched solid meshes stay resident while unrelated entity
// categories change.
if inner
.cached_mesh_source
.as_ref()
.map_or(true, |source| !Arc::ptr_eq(source, &vp.meshes))
{
inner.upload_mesh_batch(device, &vp.meshes[..]);
inner.cached_mesh_source = Some(Arc::clone(&vp.meshes));
inner.cached_mesh_batch_epoch = vp.geometry_epoch;
}
// Selection / hover highlight overlay — tinted copies of just the
// picked solids, rebuilt only when the highlight set (or geometry)
// changes. Drawn over the static batch so the base never re-packs.
let hl_key = (vp.geometry_epoch, vp.selection_generation);
let hl_key = (
Arc::as_ptr(&vp.meshes) as usize as u64,
vp.selection_generation,
);
if hl_key != inner.cached_highlight_key {
inner.upload_mesh_highlight(
device,