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:
parent
90714d34b2
commit
ea93e169ea
19 changed files with 976 additions and 336 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -74,7 +74,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
|
|||
[[package]]
|
||||
name = "acadrust"
|
||||
version = "0.4.0"
|
||||
source = "git+https://github.com/OpenAEC-Foundation/acadifc?branch=main#30792198e6faf2875125d5a1c8a189a3fe25a9ca"
|
||||
source = "git+https://github.com/OpenAEC-Foundation/acadifc?branch=main#3ef1e9c3483c44b93264c93965572c23b8678f17"
|
||||
dependencies = [
|
||||
"ahash 0.8.12",
|
||||
"anyhow",
|
||||
|
|
|
|||
|
|
@ -354,7 +354,7 @@ impl OpenCADStudio {
|
|||
// body — wire re-tessellation alone leaves the solid drawn at
|
||||
// its old spot. (#135)
|
||||
if self.tabs[i].scene.any_solid(&handles) {
|
||||
self.tabs[i].scene.populate_meshes_from_document();
|
||||
self.tabs[i].scene.refresh_meshes_for_handles(&handles);
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
|
|
@ -374,7 +374,7 @@ impl OpenCADStudio {
|
|||
let pending = self.begin_undo(i, label, handles.len(), delta_safe);
|
||||
let new_handles = self.tabs[i].scene.copy_entities(&handles, &transform);
|
||||
if self.tabs[i].scene.any_solid(&new_handles) {
|
||||
self.tabs[i].scene.populate_meshes_from_document();
|
||||
self.tabs[i].scene.refresh_meshes_for_handles(&new_handles);
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].scene.deselect_all();
|
||||
|
|
@ -522,7 +522,7 @@ impl OpenCADStudio {
|
|||
self.refresh_properties();
|
||||
}
|
||||
Err(err) => {
|
||||
let _ = self.tabs[i].history.undo_stack.pop();
|
||||
self.discard_last_undo_entry(i);
|
||||
self.command_line.push_error(&err);
|
||||
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
|
||||
if let Some(p) = prompt {
|
||||
|
|
@ -584,10 +584,7 @@ impl OpenCADStudio {
|
|||
self.tabs[i].scene.document.get_entity(nh),
|
||||
Some(acadrust::EntityType::Dimension(_))
|
||||
) {
|
||||
crate::modules::draw::modify::explode::invalidate_dim_block(
|
||||
&mut self.tabs[i].scene.document,
|
||||
nh,
|
||||
);
|
||||
self.tabs[i].scene.invalidate_dim_block_recorded(nh);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -707,10 +704,7 @@ impl OpenCADStudio {
|
|||
self.tabs[i].scene.document.get_entity(nh),
|
||||
Some(acadrust::EntityType::Dimension(_))
|
||||
) {
|
||||
crate::modules::draw::modify::explode::invalidate_dim_block(
|
||||
&mut self.tabs[i].scene.document,
|
||||
nh,
|
||||
);
|
||||
self.tabs[i].scene.invalidate_dim_block_recorded(nh);
|
||||
}
|
||||
}
|
||||
if let Some(cmd) = &mut self.tabs[i].active_cmd {
|
||||
|
|
@ -978,10 +972,7 @@ impl OpenCADStudio {
|
|||
// A restyled dimension renders from its baked *D block —
|
||||
// drop the stale block so the new style shows (#398).
|
||||
if any_dim {
|
||||
crate::modules::draw::modify::explode::invalidate_dim_block(
|
||||
&mut self.tabs[i].scene.document,
|
||||
*h,
|
||||
);
|
||||
self.tabs[i].scene.invalidate_dim_block_recorded(*h);
|
||||
}
|
||||
// Hatch fills render from a prebuilt model (#415).
|
||||
self.tabs[i].scene.refresh_fill_model(*h);
|
||||
|
|
@ -1364,7 +1355,7 @@ impl OpenCADStudio {
|
|||
self.command_line.push_output("PEDIT: applied.");
|
||||
self.refresh_properties();
|
||||
} else {
|
||||
let _ = self.tabs[i].history.undo_stack.pop();
|
||||
self.discard_last_undo_entry(i);
|
||||
self.command_line
|
||||
.push_error("PEDIT: operation not applicable to this entity.");
|
||||
}
|
||||
|
|
@ -1554,8 +1545,15 @@ impl OpenCADStudio {
|
|||
win_max,
|
||||
delta,
|
||||
} => {
|
||||
self.push_undo_snapshot(i, "STRETCH");
|
||||
let structural = handles.iter().any(|handle| {
|
||||
matches!(
|
||||
self.tabs[i].scene.document.get_entity(*handle),
|
||||
Some(acadrust::EntityType::Dimension(_))
|
||||
)
|
||||
});
|
||||
let pending = self.begin_undo(i, "STRETCH", handles.len(), !structural);
|
||||
let mut count = 0usize;
|
||||
let mut changed_handles = Vec::new();
|
||||
|
||||
// Helper: is DXF point (x, y) inside the world-space window?
|
||||
// Drawing plane is world XY (= DXF XY).
|
||||
|
|
@ -1571,6 +1569,7 @@ impl OpenCADStudio {
|
|||
// stale afterwards and must be dropped (see #398 / #372).
|
||||
let mut stretched_dims: Vec<acadrust::Handle> = Vec::new();
|
||||
for handle in &handles {
|
||||
let before = self.tabs[i].scene.document.get_entity_arc(*handle);
|
||||
let Some(entity) = self.tabs[i].scene.document.get_entity_mut(*handle) else {
|
||||
continue;
|
||||
};
|
||||
|
|
@ -1746,7 +1745,11 @@ impl OpenCADStudio {
|
|||
}
|
||||
}
|
||||
if stretched {
|
||||
if let Some(before) = before {
|
||||
self.tabs[i].scene.record_undo_before(*handle, Some(before));
|
||||
}
|
||||
self.tabs[i].scene.mark_entity_dirty(*handle);
|
||||
changed_handles.push(*handle);
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -1754,10 +1757,7 @@ impl OpenCADStudio {
|
|||
// one exists (file roundtrip) — drop it so tessellation falls
|
||||
// back to the live points and the next save re-bakes. (#372)
|
||||
for h in stretched_dims {
|
||||
crate::modules::draw::modify::explode::invalidate_dim_block(
|
||||
&mut self.tabs[i].scene.document,
|
||||
h,
|
||||
);
|
||||
self.tabs[i].scene.invalidate_dim_block_recorded(h);
|
||||
}
|
||||
|
||||
// Geometry was edited in place via get_entity_mut, which the
|
||||
|
|
@ -1765,7 +1765,15 @@ impl OpenCADStudio {
|
|||
// moved entities so the viewport reflects the stretch right away
|
||||
// instead of only on the next unrelated redraw. See #95.
|
||||
if count > 0 {
|
||||
self.tabs[i].scene.bump_geometry_no_blocks();
|
||||
if structural {
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
} else {
|
||||
let changes: Vec<_> = changed_handles
|
||||
.iter()
|
||||
.map(|&handle| (handle, crate::scene::ChangeKind::Modified))
|
||||
.collect();
|
||||
self.tabs[i].scene.bump_entities(&changes);
|
||||
}
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].active_cmd = None;
|
||||
|
|
@ -1775,6 +1783,9 @@ impl OpenCADStudio {
|
|||
self.command_line
|
||||
.push_output(&format!("STRETCH: {count} entity(ies) stretched."));
|
||||
self.refresh_properties();
|
||||
if let Some(pending) = pending {
|
||||
self.commit_undo_delta(i, pending);
|
||||
}
|
||||
}
|
||||
// ── Solid3D creation (BOX / SPHERE / CYLINDER) ────────────────
|
||||
CmdResult::CommitSolid3D { mesh_fn } => {
|
||||
|
|
@ -2322,6 +2333,7 @@ impl OpenCADStudio {
|
|||
self.tabs[i].scene.clear_preview_wire();
|
||||
}
|
||||
CmdResult::DdeditEntity { handle, new_text } => {
|
||||
self.push_undo_snapshot(i, "DDEDIT");
|
||||
let mut updated = false;
|
||||
let mut is_dim = false;
|
||||
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
|
||||
|
|
@ -2355,16 +2367,13 @@ impl OpenCADStudio {
|
|||
if is_dim {
|
||||
// The edited override changed the dimension text; drop its
|
||||
// stale *D block so save re-bakes it. (#181)
|
||||
crate::modules::draw::modify::explode::invalidate_dim_block(
|
||||
&mut self.tabs[i].scene.document,
|
||||
handle,
|
||||
);
|
||||
self.tabs[i].scene.invalidate_dim_block_recorded(handle);
|
||||
}
|
||||
if updated {
|
||||
self.push_undo_snapshot(i, "DDEDIT");
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output("DDEDIT: text updated.");
|
||||
} else {
|
||||
self.discard_last_undo_entry(i);
|
||||
self.command_line
|
||||
.push_error("DDEDIT: entity type not supported.");
|
||||
}
|
||||
|
|
@ -2759,7 +2768,7 @@ fn apply_dimspace(scene: &mut crate::scene::Scene, encoded: &str) {
|
|||
}
|
||||
// The dimension line moved, so its baked *D block is stale — drop it so
|
||||
// the next save re-bakes it (no-op for non-dimensions). (#181)
|
||||
crate::modules::draw::modify::explode::invalidate_dim_block(&mut scene.document, h);
|
||||
scene.invalidate_dim_block_recorded(h);
|
||||
}
|
||||
scene.bump_geometry();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -715,7 +715,7 @@ impl OpenCADStudio {
|
|||
// Stash the erased entities so OOPS can restore them.
|
||||
self.oops_cache = handles
|
||||
.iter()
|
||||
.filter_map(|h| self.tabs[i].scene.document.get_entity(*h).cloned())
|
||||
.filter_map(|h| self.tabs[i].scene.document.get_entity_arc(*h))
|
||||
.collect();
|
||||
self.tabs[i].scene.erase_entities(&handles);
|
||||
self.tabs[i].dirty = true;
|
||||
|
|
|
|||
|
|
@ -78,15 +78,15 @@ impl OpenCADStudio {
|
|||
if self.oops_cache.is_empty() {
|
||||
self.command_line.push_info("OOPS: nothing to restore.");
|
||||
} else {
|
||||
self.push_undo_snapshot(i, "OOPS");
|
||||
let restored = std::mem::take(&mut self.oops_cache);
|
||||
let pending = self.begin_undo(i, "OOPS", restored.len(), true);
|
||||
let restored = self.tabs[i].scene.restore_erased_entities(restored);
|
||||
let n = restored.len();
|
||||
for e in restored {
|
||||
self.tabs[i].scene.add_entity_clone(e);
|
||||
}
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
self.tabs[i].dirty = true;
|
||||
self.refresh_properties();
|
||||
if let Some(pending) = pending {
|
||||
self.commit_undo_delta(i, pending);
|
||||
}
|
||||
self.command_line
|
||||
.push_output(&format!("OOPS: restored {n} object(s)."));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1785,7 +1785,7 @@ impl OpenCADStudio {
|
|||
self.command_line
|
||||
.push_output(&format!("RENAME: '{}' → '{}'.", old_name, new_name));
|
||||
} else {
|
||||
let _ = self.tabs[i].history.undo_stack.pop();
|
||||
self.discard_last_undo_entry(i);
|
||||
if !known {
|
||||
self.command_line.push_error(&format!("RENAME: unknown type '{}'. Use LAYER BLOCK STYLE DIMSTYLE LINETYPE UCS VIEW", type_str));
|
||||
} else if type_str == "BLOCK" {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ use iced;
|
|||
use std::any::Any;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
// ── Dynamic input ──────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -477,51 +478,60 @@ impl DocumentTab {
|
|||
}
|
||||
}
|
||||
|
||||
/// One undo/redo entry. Most edits touch the whole drawing's state and store a
|
||||
/// [`FullSnapshot`] (a document clone); frequent entity-only edits (move / copy /
|
||||
/// draw / erase) instead store a cheap [`DeltaSnapshot`] naming just the entities
|
||||
/// they changed, so an 800k-entity drawing doesn't pay a ~46 ms document clone
|
||||
/// per edit.
|
||||
/// One undo/redo entry. Every edit is represented as a first-touch entity delta
|
||||
/// plus an optional structure-only document image, so history never clones the
|
||||
/// full entity store.
|
||||
#[derive(Clone)]
|
||||
pub(super) enum HistorySnapshot {
|
||||
Full(FullSnapshot),
|
||||
Delta(DeltaSnapshot),
|
||||
}
|
||||
|
||||
impl HistorySnapshot {
|
||||
pub(super) fn label(&self) -> &str {
|
||||
match self {
|
||||
HistorySnapshot::Full(f) => &f.label,
|
||||
HistorySnapshot::Delta(d) => &d.label,
|
||||
}
|
||||
}
|
||||
|
||||
/// Approximate retained history memory. Entity images are shared through
|
||||
/// `Arc`; the estimate conservatively budgets their payloads and optional
|
||||
/// structure state to keep pathological histories bounded.
|
||||
pub(super) fn estimated_bytes(&self) -> usize {
|
||||
match self {
|
||||
HistorySnapshot::Delta(d) => d
|
||||
.entities
|
||||
.len()
|
||||
.saturating_mul(512)
|
||||
.saturating_add(
|
||||
d.structure
|
||||
.as_ref()
|
||||
.map_or(0, |doc| doc.objects.len().saturating_mul(192)),
|
||||
)
|
||||
.saturating_add(d.selected_before.len().saturating_mul(16))
|
||||
.saturating_add(d.selected_after.len().saturating_mul(16))
|
||||
.saturating_add(d.label.len()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A whole-document undo entry: the safe fallback for any command that may touch
|
||||
/// layers, objects, block records, styles or tables.
|
||||
#[derive(Clone)]
|
||||
pub(super) struct FullSnapshot {
|
||||
pub(super) document: CadDocument,
|
||||
pub(super) current_layout: String,
|
||||
pub(super) selected: Vec<Handle>,
|
||||
pub(super) dirty: bool,
|
||||
pub(super) label: String,
|
||||
}
|
||||
|
||||
/// An entity-only undo entry: for each touched handle, its before-image and
|
||||
/// 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
|
||||
/// one delta moves between the undo and redo stacks without re-capturing. The
|
||||
/// command by construction changes no layers / objects / blocks, so only the
|
||||
/// cheap scalars (`current_layout`, selection, `dirty`) ride alongside.
|
||||
/// Optional structure state covers layers, objects, blocks and tables without
|
||||
/// cloning the flat entity store.
|
||||
#[derive(Clone)]
|
||||
pub(super) struct DeltaSnapshot {
|
||||
pub(super) entities: Vec<(Handle, Option<EntityType>, Option<EntityType>)>,
|
||||
pub(super) current_layout: String,
|
||||
pub(super) entities: Vec<(Handle, Option<Arc<EntityType>>, Option<Arc<EntityType>>)>,
|
||||
pub(super) current_layout_before: String,
|
||||
pub(super) current_layout_after: String,
|
||||
pub(super) selected_before: Vec<Handle>,
|
||||
pub(super) selected_after: Vec<Handle>,
|
||||
pub(super) dirty_before: bool,
|
||||
pub(super) dirty_after: bool,
|
||||
/// Opposite non-entity document state. `apply_delta_state` swaps this with
|
||||
/// the live structure, so the same allocation shuttles between undo/redo.
|
||||
pub(super) structure: Option<CadDocument>,
|
||||
pub(super) label: String,
|
||||
}
|
||||
|
||||
|
|
@ -529,4 +539,14 @@ pub(super) struct DeltaSnapshot {
|
|||
pub(super) struct HistoryState {
|
||||
pub(super) undo_stack: Vec<HistorySnapshot>,
|
||||
pub(super) redo_stack: Vec<HistorySnapshot>,
|
||||
pub(super) pending: Option<PendingHistorySnapshot>,
|
||||
}
|
||||
|
||||
pub(super) struct PendingHistorySnapshot {
|
||||
pub(super) label: String,
|
||||
pub(super) current_layout: String,
|
||||
pub(super) selected_before: Vec<Handle>,
|
||||
pub(super) dirty_before: bool,
|
||||
pub(super) structure_before: CadDocument,
|
||||
pub(super) recorder: Arc<acadrust::document::EntityChangeRecorder>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,64 @@
|
|||
use super::{
|
||||
document::{DeltaSnapshot, FullSnapshot, HistorySnapshot},
|
||||
document::{DeltaSnapshot, HistorySnapshot, PendingHistorySnapshot},
|
||||
OpenCADStudio,
|
||||
};
|
||||
use acadrust::{EntityType, Handle};
|
||||
use rustc_hash::FxHashSet as HashSet;
|
||||
use rustc_hash::{FxHashMap, FxHashSet as HashSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Above this many touched entities a "delta-safe" command falls back to a full
|
||||
/// snapshot: deep-cloning that many before-images would cost more than the (now
|
||||
/// Arc-cheap) document clone, and huge bulk edits are rare. Below it, the delta
|
||||
/// keeps a per-edit undo at roughly zero cost.
|
||||
const DELTA_UNDO_MAX_ENTITIES: usize = 5000;
|
||||
const DEFAULT_HISTORY_MAX_ENTRIES: usize = 256;
|
||||
const DEFAULT_HISTORY_MAX_BYTES: usize = 512 * 1024 * 1024;
|
||||
|
||||
fn history_limits() -> (usize, usize) {
|
||||
use std::sync::OnceLock;
|
||||
static LIMITS: OnceLock<(usize, usize)> = OnceLock::new();
|
||||
*LIMITS.get_or_init(|| {
|
||||
let entries = std::env::var("OCS_HISTORY_MAX_ENTRIES")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(DEFAULT_HISTORY_MAX_ENTRIES);
|
||||
let mib = std::env::var("OCS_HISTORY_MAX_MB")
|
||||
.ok()
|
||||
.and_then(|v| v.parse::<usize>().ok())
|
||||
.unwrap_or(DEFAULT_HISTORY_MAX_BYTES / (1024 * 1024));
|
||||
(
|
||||
if entries == 0 { usize::MAX } else { entries },
|
||||
if mib == 0 {
|
||||
usize::MAX
|
||||
} else {
|
||||
mib.saturating_mul(1024 * 1024)
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn defer_history_drop(entries: Vec<HistorySnapshot>) {
|
||||
if entries.is_empty() {
|
||||
return;
|
||||
}
|
||||
use std::sync::{mpsc, OnceLock};
|
||||
static TX: OnceLock<mpsc::Sender<Vec<HistorySnapshot>>> = OnceLock::new();
|
||||
let tx = TX.get_or_init(|| {
|
||||
let (tx, rx) = mpsc::channel::<Vec<HistorySnapshot>>();
|
||||
let _ = std::thread::Builder::new()
|
||||
.name("ocs-history-drop".to_string())
|
||||
.spawn(move || {
|
||||
while let Ok(entries) = rx.recv() {
|
||||
drop(entries);
|
||||
}
|
||||
});
|
||||
tx
|
||||
});
|
||||
if let Err(err) = tx.send(entries) {
|
||||
drop(err.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn defer_history_drop(entries: Vec<HistorySnapshot>) {
|
||||
drop(entries);
|
||||
}
|
||||
|
||||
/// Pre-command state captured by [`OpenCADStudio::begin_undo`] and handed back to
|
||||
/// [`OpenCADStudio::commit_undo_delta`] to close a delta entry. Lives on the
|
||||
|
|
@ -19,6 +68,7 @@ pub(super) struct PendingDelta {
|
|||
current_layout: String,
|
||||
selected_before: Vec<Handle>,
|
||||
dirty_before: bool,
|
||||
structure_before: Option<acadrust::CadDocument>,
|
||||
}
|
||||
|
||||
impl OpenCADStudio {
|
||||
|
|
@ -30,52 +80,187 @@ impl OpenCADStudio {
|
|||
.unwrap_or_else(|| fallback.to_string())
|
||||
}
|
||||
|
||||
pub(super) fn capture_history_snapshot(
|
||||
&self,
|
||||
fn clear_redo_history(&mut self, i: usize) {
|
||||
let discarded = std::mem::take(&mut self.tabs[i].history.redo_stack);
|
||||
defer_history_drop(discarded);
|
||||
}
|
||||
|
||||
fn trim_history(&mut self, i: usize) {
|
||||
let (max_entries, max_bytes) = history_limits();
|
||||
let history = &mut self.tabs[i].history;
|
||||
let mut entries = history
|
||||
.undo_stack
|
||||
.len()
|
||||
.saturating_add(history.redo_stack.len());
|
||||
let mut bytes = history
|
||||
.undo_stack
|
||||
.iter()
|
||||
.chain(history.redo_stack.iter())
|
||||
.fold(0usize, |sum, item| {
|
||||
sum.saturating_add(item.estimated_bytes())
|
||||
});
|
||||
let mut discarded = Vec::new();
|
||||
while entries > max_entries || bytes > max_bytes {
|
||||
let item = if !history.undo_stack.is_empty() {
|
||||
history.undo_stack.remove(0)
|
||||
} else if !history.redo_stack.is_empty() {
|
||||
history.redo_stack.remove(0)
|
||||
} else {
|
||||
break;
|
||||
};
|
||||
entries -= 1;
|
||||
bytes = bytes.saturating_sub(item.estimated_bytes());
|
||||
discarded.push(item);
|
||||
}
|
||||
defer_history_drop(discarded);
|
||||
}
|
||||
|
||||
fn push_undo_entry(&mut self, i: usize, snapshot: HistorySnapshot) {
|
||||
self.tabs[i].history.undo_stack.push(snapshot);
|
||||
self.clear_redo_history(i);
|
||||
self.trim_history(i);
|
||||
}
|
||||
|
||||
pub(super) fn push_single_entity_history(
|
||||
&mut self,
|
||||
i: usize,
|
||||
label: impl Into<String>,
|
||||
) -> HistorySnapshot {
|
||||
HistorySnapshot::Full(FullSnapshot {
|
||||
document: self.tabs[i].scene.document.clone(),
|
||||
current_layout: self.tabs[i].scene.current_layout.clone(),
|
||||
selected: self.tabs[i].scene.selected.iter().copied().collect(),
|
||||
dirty: self.tabs[i].dirty,
|
||||
handle: Handle,
|
||||
before: Arc<EntityType>,
|
||||
) {
|
||||
self.finish_pending_history(i);
|
||||
let Some(after) = self.tabs[i].scene.document.get_entity_arc(handle) else {
|
||||
return;
|
||||
};
|
||||
let selected: Vec<Handle> = self.tabs[i].scene.selected.iter().copied().collect();
|
||||
let dirty_before = self.tabs[i].dirty;
|
||||
let delta = DeltaSnapshot {
|
||||
entities: vec![(handle, Some(before), Some(after))],
|
||||
current_layout_before: self.tabs[i].scene.current_layout.clone(),
|
||||
current_layout_after: self.tabs[i].scene.current_layout.clone(),
|
||||
selected_before: selected.clone(),
|
||||
selected_after: selected,
|
||||
dirty_before,
|
||||
dirty_after: true,
|
||||
structure: None,
|
||||
label: label.into(),
|
||||
})
|
||||
};
|
||||
self.push_undo_entry(i, HistorySnapshot::Delta(delta));
|
||||
}
|
||||
|
||||
pub(super) fn discard_last_undo_entry(&mut self, i: usize) {
|
||||
if self.tabs[i].history.pending.take().is_some() {
|
||||
self.tabs[i].scene.document.end_entity_change_recording();
|
||||
return;
|
||||
}
|
||||
if let Some(discarded) = self.tabs[i].history.undo_stack.pop() {
|
||||
defer_history_drop(vec![discarded]);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn finish_pending_history(&mut self, i: usize) {
|
||||
let Some(mut pending) = self.tabs[i].history.pending.take() else {
|
||||
return;
|
||||
};
|
||||
self.tabs[i].scene.document.end_entity_change_recording();
|
||||
let structure_after = self.tabs[i].scene.document.snapshot_structure();
|
||||
let before_images = pending.recorder.take_before_images();
|
||||
let added_handles: Vec<Handle> = before_images
|
||||
.iter()
|
||||
.filter_map(|(handle, before)| {
|
||||
(before.is_none() && self.tabs[i].scene.document.get_entity(*handle).is_some())
|
||||
.then_some(*handle)
|
||||
})
|
||||
.collect();
|
||||
acadrust::CadDocument::align_added_entity_structure(
|
||||
&mut pending.structure_before,
|
||||
&structure_after,
|
||||
&added_handles,
|
||||
);
|
||||
let structure_changed = pending.structure_before != structure_after;
|
||||
let entities: Vec<_> = before_images
|
||||
.into_iter()
|
||||
.map(|(handle, before)| {
|
||||
let after = self.tabs[i].scene.document.get_entity_arc(handle);
|
||||
(handle, before, after)
|
||||
})
|
||||
.filter(|(_, before, after)| match (before, after) {
|
||||
(Some(before), Some(after)) => !Arc::ptr_eq(before, after),
|
||||
(None, None) => false,
|
||||
_ => true,
|
||||
})
|
||||
.collect();
|
||||
let selected_after: Vec<Handle> = self.tabs[i].scene.selected.iter().copied().collect();
|
||||
let dirty_after = self.tabs[i].dirty;
|
||||
if entities.is_empty()
|
||||
&& !structure_changed
|
||||
&& pending.selected_before == selected_after
|
||||
&& pending.dirty_before == dirty_after
|
||||
{
|
||||
return;
|
||||
}
|
||||
let delta = DeltaSnapshot {
|
||||
entities,
|
||||
current_layout_before: pending.current_layout,
|
||||
current_layout_after: self.tabs[i].scene.current_layout.clone(),
|
||||
selected_before: pending.selected_before,
|
||||
selected_after,
|
||||
dirty_before: pending.dirty_before,
|
||||
dirty_after,
|
||||
structure: structure_changed.then_some(pending.structure_before),
|
||||
label: pending.label,
|
||||
};
|
||||
self.push_undo_entry(i, HistorySnapshot::Delta(delta));
|
||||
}
|
||||
|
||||
pub(super) fn finish_all_pending_history(&mut self) {
|
||||
for i in 0..self.tabs.len() {
|
||||
self.finish_pending_history(i);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn push_undo_snapshot(&mut self, i: usize, label: impl Into<String>) {
|
||||
let snapshot = self.capture_history_snapshot(i, label);
|
||||
self.tabs[i].history.undo_stack.push(snapshot);
|
||||
self.tabs[i].history.redo_stack.clear();
|
||||
self.finish_pending_history(i);
|
||||
let label = label.into();
|
||||
let current_layout = self.tabs[i].scene.current_layout.clone();
|
||||
let selected_before = self.tabs[i].scene.selected.iter().copied().collect();
|
||||
let dirty_before = self.tabs[i].dirty;
|
||||
let structure_before = self.tabs[i].scene.document.snapshot_structure();
|
||||
let recorder = self.tabs[i].scene.document.begin_entity_change_recording();
|
||||
self.tabs[i].history.pending = Some(PendingHistorySnapshot {
|
||||
label,
|
||||
current_layout,
|
||||
selected_before,
|
||||
dirty_before,
|
||||
structure_before,
|
||||
recorder,
|
||||
});
|
||||
}
|
||||
|
||||
/// Begin undo capture for an entity edit that will touch `touched` entities.
|
||||
/// When `delta_safe` (the caller's per-command predicate guarantees the edit
|
||||
/// mutates only entities, through the five Scene primitives) and the edit is
|
||||
/// small enough, starts a cheap Scene delta recording and returns the
|
||||
/// pre-command state to pass to [`OpenCADStudio::commit_undo_delta`] after the
|
||||
/// mutation. Otherwise pushes a full snapshot and returns `None`.
|
||||
/// Starts a cheap Scene delta recording and returns the pre-command state to
|
||||
/// pass to [`OpenCADStudio::commit_undo_delta`] after the mutation. A command
|
||||
/// that may touch layers/objects/block records also retains a structure-only
|
||||
/// document image; the O(N) entity store is excluded.
|
||||
pub(super) fn begin_undo(
|
||||
&mut self,
|
||||
i: usize,
|
||||
label: impl Into<String>,
|
||||
touched: usize,
|
||||
_touched: usize,
|
||||
delta_safe: bool,
|
||||
) -> Option<PendingDelta> {
|
||||
self.finish_pending_history(i);
|
||||
let label = label.into();
|
||||
if delta_safe && touched <= DELTA_UNDO_MAX_ENTITIES {
|
||||
self.tabs[i].scene.begin_undo_recording();
|
||||
Some(PendingDelta {
|
||||
label,
|
||||
current_layout: self.tabs[i].scene.current_layout.clone(),
|
||||
selected_before: self.tabs[i].scene.selected.iter().copied().collect(),
|
||||
dirty_before: self.tabs[i].dirty,
|
||||
})
|
||||
} else {
|
||||
self.push_undo_snapshot(i, label);
|
||||
None
|
||||
}
|
||||
let structure_before =
|
||||
(!delta_safe).then(|| self.tabs[i].scene.document.snapshot_structure());
|
||||
self.tabs[i].scene.begin_undo_recording();
|
||||
Some(PendingDelta {
|
||||
label,
|
||||
current_layout: self.tabs[i].scene.current_layout.clone(),
|
||||
selected_before: self.tabs[i].scene.selected.iter().copied().collect(),
|
||||
dirty_before: self.tabs[i].dirty,
|
||||
structure_before,
|
||||
})
|
||||
}
|
||||
|
||||
/// Copy is delta-safe only when no target is a Dimension and no complete
|
||||
|
|
@ -85,12 +270,13 @@ impl OpenCADStudio {
|
|||
pub(super) fn delta_copy_safe(&self, i: usize, handles: &[Handle]) -> bool {
|
||||
use acadrust::objects::ObjectType;
|
||||
let doc = &self.tabs[i].scene.document;
|
||||
let handle_set: HashSet<Handle> = handles.iter().copied().collect();
|
||||
let copies_dimension = handles
|
||||
.iter()
|
||||
.any(|h| matches!(doc.get_entity(*h), Some(EntityType::Dimension(_))));
|
||||
let copies_complete_group = doc.objects.values().any(|o| match o {
|
||||
ObjectType::Group(g) => {
|
||||
!g.entities.is_empty() && g.entities.iter().all(|h| handles.contains(h))
|
||||
!g.entities.is_empty() && g.entities.iter().all(|h| handle_set.contains(h))
|
||||
}
|
||||
_ => false,
|
||||
});
|
||||
|
|
@ -102,8 +288,9 @@ impl OpenCADStudio {
|
|||
pub(super) fn delta_erase_safe(&self, i: usize, handles: &[Handle]) -> bool {
|
||||
use acadrust::objects::ObjectType;
|
||||
let doc = &self.tabs[i].scene.document;
|
||||
let handle_set: HashSet<Handle> = handles.iter().copied().collect();
|
||||
!doc.objects.values().any(|o| match o {
|
||||
ObjectType::Group(g) => g.entities.iter().any(|h| handles.contains(h)),
|
||||
ObjectType::Group(g) => g.entities.iter().any(|h| handle_set.contains(h)),
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
|
|
@ -145,45 +332,61 @@ impl OpenCADStudio {
|
|||
// entity part stays correct; a leaked layer/group/block just won't
|
||||
// revert). Loud in debug, a warning in release — never a silent wrong.
|
||||
debug_assert!(
|
||||
!rec.is_poisoned(),
|
||||
"delta command '{}' mutated non-entity state",
|
||||
!rec.is_poisoned() || pending.structure_before.is_some(),
|
||||
"delta command '{}' mutated unrecorded non-entity state",
|
||||
pending.label
|
||||
);
|
||||
if rec.is_empty() {
|
||||
if rec.is_empty() && pending.structure_before.is_none() {
|
||||
// Nothing actually changed (e.g. every target was on a locked
|
||||
// layer). Leave the undo/redo stacks untouched.
|
||||
return;
|
||||
}
|
||||
if rec.is_poisoned() {
|
||||
if rec.is_poisoned() && pending.structure_before.is_none() {
|
||||
eprintln!(
|
||||
"[undo] delta '{}' touched non-entity state; undo may be incomplete",
|
||||
pending.label
|
||||
);
|
||||
}
|
||||
let entities: Vec<(Handle, Option<EntityType>, Option<EntityType>)> = rec
|
||||
let entities: Vec<(Handle, Option<Arc<EntityType>>, Option<Arc<EntityType>>)> = rec
|
||||
.into_before_images()
|
||||
.into_iter()
|
||||
.map(|(h, before)| {
|
||||
let after = self.tabs[i].scene.document.get_entity(h).cloned();
|
||||
let after = self.tabs[i].scene.document.get_entity_arc(h);
|
||||
(h, before, after)
|
||||
})
|
||||
.collect();
|
||||
let selected_after = self.tabs[i].scene.selected.iter().copied().collect();
|
||||
let dirty_after = self.tabs[i].dirty;
|
||||
let mut structure = pending.structure_before;
|
||||
if let Some(before_structure) = structure.as_mut() {
|
||||
let after_structure = self.tabs[i].scene.document.snapshot_structure();
|
||||
let added_handles: Vec<Handle> = entities
|
||||
.iter()
|
||||
.filter_map(|(handle, before, after)| {
|
||||
(before.is_none() && after.is_some()).then_some(*handle)
|
||||
})
|
||||
.collect();
|
||||
acadrust::CadDocument::align_added_entity_structure(
|
||||
before_structure,
|
||||
&after_structure,
|
||||
&added_handles,
|
||||
);
|
||||
if *before_structure == after_structure {
|
||||
structure = None;
|
||||
}
|
||||
}
|
||||
let delta = DeltaSnapshot {
|
||||
entities,
|
||||
current_layout: pending.current_layout,
|
||||
current_layout_before: pending.current_layout,
|
||||
current_layout_after: self.tabs[i].scene.current_layout.clone(),
|
||||
selected_before: pending.selected_before,
|
||||
selected_after,
|
||||
dirty_before: pending.dirty_before,
|
||||
dirty_after,
|
||||
structure,
|
||||
label: pending.label,
|
||||
};
|
||||
self.tabs[i]
|
||||
.history
|
||||
.undo_stack
|
||||
.push(HistorySnapshot::Delta(delta));
|
||||
self.tabs[i].history.redo_stack.clear();
|
||||
self.push_undo_entry(i, HistorySnapshot::Delta(delta));
|
||||
}
|
||||
|
||||
/// Apply one side of a delta entry in place: `undo` restores each entity's
|
||||
|
|
@ -191,13 +394,21 @@ impl OpenCADStudio {
|
|||
/// means the entity is absent there (erase it). Reports the exact touched
|
||||
/// handles through `bump_entities` so the incremental caches patch per-handle
|
||||
/// — no full re-tessellation, no `populate_*` document walk.
|
||||
fn apply_delta(&mut self, i: usize, d: &DeltaSnapshot, undo: bool) {
|
||||
// Install the chosen side of every entity image (Scene handles the
|
||||
// in-place / re-insert / remove, the derived-cache reseed and the
|
||||
// block-record dedup); we report the touched handles incrementally.
|
||||
fn apply_delta_state(
|
||||
&mut self,
|
||||
i: usize,
|
||||
d: &mut DeltaSnapshot,
|
||||
undo: bool,
|
||||
) -> Vec<(Handle, crate::scene::ChangeKind)> {
|
||||
// Install the chosen side of every entity image. Derived-cache, geometry
|
||||
// and UI invalidation are deferred until every requested undo/redo step
|
||||
// has been applied.
|
||||
if let Some(structure) = d.structure.take() {
|
||||
let inverse = self.tabs[i].scene.document.swap_structure(structure);
|
||||
d.structure = Some(inverse);
|
||||
}
|
||||
let changes = self.tabs[i].scene.apply_entity_delta(&d.entities, undo);
|
||||
let scene = &mut self.tabs[i].scene;
|
||||
scene.bump_entities(&changes);
|
||||
let (sel, dirty) = if undo {
|
||||
(&d.selected_before, d.dirty_before)
|
||||
} else {
|
||||
|
|
@ -209,49 +420,77 @@ impl OpenCADStudio {
|
|||
.filter(|h| scene.document.get_entity(*h).is_some())
|
||||
.collect();
|
||||
scene.selected = restored;
|
||||
// Delta commands never change the layout; this is a no-op unless a
|
||||
// future caller widens the delta scope, in which case it stays correct.
|
||||
scene.set_current_layout(d.current_layout.clone());
|
||||
scene.clear_preview_wire();
|
||||
// Do not call set_current_layout here: it bumps geometry immediately.
|
||||
// Entity deltas currently preserve the layout; direct assignment also
|
||||
// keeps future widened deltas batchable.
|
||||
scene.current_layout = if undo {
|
||||
d.current_layout_before.clone()
|
||||
} else {
|
||||
d.current_layout_after.clone()
|
||||
};
|
||||
self.tabs[i].dirty = dirty;
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].active_grip = None;
|
||||
self.refresh_properties();
|
||||
changes
|
||||
}
|
||||
|
||||
pub(super) fn restore_history_snapshot(&mut self, i: usize, snapshot: FullSnapshot) {
|
||||
self.tabs[i].scene.document = snapshot.document;
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.set_current_layout(snapshot.current_layout);
|
||||
// Force a re-tessellation: the cached wires were keyed against the
|
||||
// outgoing document / layout and would be returned unchanged
|
||||
// otherwise (`set_current_layout` only bumps on actual change).
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
self.tabs[i].scene.selected = snapshot
|
||||
.selected
|
||||
.into_iter()
|
||||
.filter(|h| self.tabs[i].scene.document.get_entity(*h).is_some())
|
||||
.collect::<HashSet<_>>();
|
||||
self.tabs[i].scene.populate_hatches_from_document();
|
||||
self.tabs[i].scene.populate_images_from_document();
|
||||
self.tabs[i].scene.populate_meshes_from_document();
|
||||
// Keep the Isolate/Hide set in step with the restored per-entity
|
||||
// visibility so End Isolation stays correct after undo/redo.
|
||||
self.tabs[i].scene.sync_hidden_from_invisible();
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.tabs[i].scene.images.clear();
|
||||
/// 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.
|
||||
fn finish_history_apply(
|
||||
&mut self,
|
||||
i: usize,
|
||||
had_full: bool,
|
||||
changes: &[(Handle, crate::scene::ChangeKind)],
|
||||
) {
|
||||
{
|
||||
let scene = &mut self.tabs[i].scene;
|
||||
if had_full {
|
||||
// One document walk per derived category and one geometry bump
|
||||
// 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();
|
||||
for &(handle, kind) in changes {
|
||||
match (net.get(&handle).copied(), kind) {
|
||||
(None, kind) => {
|
||||
net.insert(handle, kind);
|
||||
}
|
||||
(Some(Added), Removed) => {
|
||||
net.remove(&handle);
|
||||
}
|
||||
(Some(Added), _) => {}
|
||||
(Some(Removed), Added) => {
|
||||
net.insert(handle, Modified);
|
||||
}
|
||||
(Some(Modified), Removed) => {
|
||||
net.insert(handle, Removed);
|
||||
}
|
||||
(Some(Removed), _) | (Some(Modified), _) => {}
|
||||
}
|
||||
}
|
||||
let final_changes: Vec<_> = net.into_iter().collect();
|
||||
for &(handle, _) in &final_changes {
|
||||
scene.reseed_derived_caches(handle);
|
||||
}
|
||||
if !final_changes.is_empty() {
|
||||
scene.bump_entities(&final_changes);
|
||||
}
|
||||
}
|
||||
scene.clear_preview_wire();
|
||||
}
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].active_grip = None;
|
||||
self.tabs[i].dirty = snapshot.dirty;
|
||||
let doc_layers = self.tabs[i].scene.document.layers.clone();
|
||||
let vp_info = self.tabs[i].scene.viewport_list();
|
||||
self.tabs[i]
|
||||
.layers
|
||||
.sync_with_viewports(&doc_layers, vp_info);
|
||||
self.sync_ribbon_layers();
|
||||
if had_full {
|
||||
let doc_layers = self.tabs[i].scene.document.layers.clone();
|
||||
let vp_info = self.tabs[i].scene.viewport_list();
|
||||
self.tabs[i]
|
||||
.layers
|
||||
.sync_with_viewports(&doc_layers, vp_info);
|
||||
self.sync_ribbon_layers();
|
||||
}
|
||||
self.refresh_properties();
|
||||
}
|
||||
|
||||
|
|
@ -265,6 +504,7 @@ impl OpenCADStudio {
|
|||
|
||||
pub(super) fn undo_steps(&mut self, steps: usize) {
|
||||
let i = self.active_tab;
|
||||
self.finish_pending_history(i);
|
||||
let available = self.tabs[i].history.undo_stack.len();
|
||||
let steps = steps.min(available);
|
||||
if steps == 0 {
|
||||
|
|
@ -273,22 +513,20 @@ impl OpenCADStudio {
|
|||
}
|
||||
|
||||
let mut last_label = String::new();
|
||||
let mut had_full = false;
|
||||
let mut changes = Vec::new();
|
||||
for _ in 0..steps {
|
||||
let Some(snapshot) = self.tabs[i].history.undo_stack.pop() else {
|
||||
break;
|
||||
};
|
||||
last_label = snapshot.label().to_string();
|
||||
match snapshot {
|
||||
HistorySnapshot::Full(f) => {
|
||||
let current = self.capture_history_snapshot(i, f.label.clone());
|
||||
self.tabs[i].history.redo_stack.push(current);
|
||||
self.restore_history_snapshot(i, f);
|
||||
}
|
||||
HistorySnapshot::Delta(d) => {
|
||||
HistorySnapshot::Delta(mut d) => {
|
||||
// Symmetric: undo applies the before side, then the same
|
||||
// delta rides to the redo stack (it still holds the after
|
||||
// side) — no current-state capture needed.
|
||||
self.apply_delta(i, &d, true);
|
||||
had_full |= d.structure.is_some();
|
||||
changes.extend(self.apply_delta_state(i, &mut d, true));
|
||||
self.tabs[i]
|
||||
.history
|
||||
.redo_stack
|
||||
|
|
@ -296,12 +534,14 @@ impl OpenCADStudio {
|
|||
}
|
||||
}
|
||||
}
|
||||
self.finish_history_apply(i, had_full, &changes);
|
||||
self.command_line
|
||||
.push_output(&format!("Undo: {last_label}"));
|
||||
}
|
||||
|
||||
pub(super) fn redo_steps(&mut self, steps: usize) {
|
||||
let i = self.active_tab;
|
||||
self.finish_pending_history(i);
|
||||
let available = self.tabs[i].history.redo_stack.len();
|
||||
let steps = steps.min(available);
|
||||
if steps == 0 {
|
||||
|
|
@ -310,19 +550,17 @@ impl OpenCADStudio {
|
|||
}
|
||||
|
||||
let mut last_label = String::new();
|
||||
let mut had_full = false;
|
||||
let mut changes = Vec::new();
|
||||
for _ in 0..steps {
|
||||
let Some(snapshot) = self.tabs[i].history.redo_stack.pop() else {
|
||||
break;
|
||||
};
|
||||
last_label = snapshot.label().to_string();
|
||||
match snapshot {
|
||||
HistorySnapshot::Full(f) => {
|
||||
let current = self.capture_history_snapshot(i, f.label.clone());
|
||||
self.tabs[i].history.undo_stack.push(current);
|
||||
self.restore_history_snapshot(i, f);
|
||||
}
|
||||
HistorySnapshot::Delta(d) => {
|
||||
self.apply_delta(i, &d, false);
|
||||
HistorySnapshot::Delta(mut d) => {
|
||||
had_full |= d.structure.is_some();
|
||||
changes.extend(self.apply_delta_state(i, &mut d, false));
|
||||
self.tabs[i]
|
||||
.history
|
||||
.undo_stack
|
||||
|
|
@ -330,6 +568,7 @@ impl OpenCADStudio {
|
|||
}
|
||||
}
|
||||
}
|
||||
self.finish_history_apply(i, had_full, &changes);
|
||||
self.command_line
|
||||
.push_output(&format!("Redo: {last_label}"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -541,7 +541,7 @@ pub(super) struct OpenCADStudio {
|
|||
/// In-memory clipboard: cloned entities waiting to be pasted.
|
||||
clipboard: Vec<acadrust::EntityType>,
|
||||
/// Entities removed by the most recent ERASE, kept so OOPS can restore them.
|
||||
oops_cache: Vec<acadrust::EntityType>,
|
||||
oops_cache: Vec<Arc<acadrust::EntityType>>,
|
||||
/// Paste anchor: lower-left corner of the clipboard entities' bounding box
|
||||
/// (or the point picked by COPYBASE). This point lands under the cursor at
|
||||
/// paste time.
|
||||
|
|
|
|||
|
|
@ -1125,7 +1125,6 @@ impl OpenCADStudio {
|
|||
|
||||
pub(super) fn invalidate_property_targets(&mut self, i: usize, handles: &[Handle]) {
|
||||
for &handle in handles {
|
||||
self.tabs[i].scene.mark_entity_dirty(handle);
|
||||
// Hatch / SOLID fills render from prebuilt cached models; rebuild
|
||||
// them or pattern edits (scale, background, …) stay invisible
|
||||
// (#415).
|
||||
|
|
@ -1134,8 +1133,12 @@ impl OpenCADStudio {
|
|||
// Solid (ACIS) meshes bake their colour into the mesh, so a colour /
|
||||
// layer change needs an explicit recolour — re-tessellating wires
|
||||
// alone wouldn't update them.
|
||||
self.tabs[i].scene.recolor_meshes();
|
||||
self.tabs[i].scene.bump_geometry_no_blocks();
|
||||
self.tabs[i].scene.recolor_meshes_for_handles(handles);
|
||||
let changes: Vec<_> = handles
|
||||
.iter()
|
||||
.map(|&handle| (handle, crate::scene::ChangeKind::Modified))
|
||||
.collect();
|
||||
self.tabs[i].scene.bump_entities(&changes);
|
||||
}
|
||||
|
||||
/// Add an entity to the correct space (model or paper space layout).
|
||||
|
|
@ -1355,6 +1358,10 @@ impl OpenCADStudio {
|
|||
.add_entity_to_layout(entity, &layout)
|
||||
{
|
||||
Ok(new_handle) => {
|
||||
if self.tabs[i].scene.is_recording_undo() {
|
||||
self.tabs[i].scene.record_undo_before(new_handle, None);
|
||||
self.tabs[i].scene.poison_undo_recording();
|
||||
}
|
||||
self.tabs[i].scene.auto_fit_viewport(new_handle);
|
||||
// Adding a viewport straight onto the document layout
|
||||
// bypasses Scene::add_entity, which is what normally
|
||||
|
|
|
|||
|
|
@ -168,10 +168,9 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||
}
|
||||
// A typed grip-menu value reshapes dimensions too — drop a
|
||||
// stale baked *D block (no-op for non-dims). (#398)
|
||||
crate::modules::draw::modify::explode::invalidate_dim_block(
|
||||
&mut self.tabs[i].scene.document,
|
||||
pending.handle,
|
||||
);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.invalidate_dim_block_recorded(pending.handle);
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
self.tabs[i].dirty = true;
|
||||
self.refresh_selected_grips();
|
||||
|
|
@ -987,10 +986,9 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||
}
|
||||
// Menu actions reshape dimensions too — drop a stale baked *D
|
||||
// block so the edit is visible (no-op for non-dims). (#398)
|
||||
crate::modules::draw::modify::explode::invalidate_dim_block(
|
||||
&mut self.tabs[i].scene.document,
|
||||
popup.handle,
|
||||
);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.invalidate_dim_block_recorded(popup.handle);
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
self.tabs[i].dirty = true;
|
||||
self.refresh_selected_grips();
|
||||
|
|
@ -1918,7 +1916,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||
self.tabs[i].dirty = true;
|
||||
} else {
|
||||
// Nothing changed — drop the snapshot pushed a moment ago.
|
||||
let _ = self.tabs[i].history.undo_stack.pop();
|
||||
self.discard_last_undo_entry(i);
|
||||
}
|
||||
self.refresh_properties();
|
||||
Task::none()
|
||||
|
|
|
|||
|
|
@ -134,6 +134,9 @@ impl OpenCADStudio {
|
|||
}
|
||||
}
|
||||
let task = self.update_inner(msg);
|
||||
// Close the document-level first-touch transaction started by
|
||||
// push_undo_snapshot at this message boundary.
|
||||
self.finish_all_pending_history();
|
||||
// After every message, mirror the active command step's prompt so
|
||||
// its history line stays pinned (non-fading) until the step changes.
|
||||
let prompt = self.tabs[self.active_tab]
|
||||
|
|
@ -2228,7 +2231,7 @@ impl OpenCADStudio {
|
|||
// Stash the erased entities so OOPS can restore them.
|
||||
self.oops_cache = handles
|
||||
.iter()
|
||||
.filter_map(|h| self.tabs[i].scene.document.get_entity(*h).cloned())
|
||||
.filter_map(|h| self.tabs[i].scene.document.get_entity_arc(*h))
|
||||
.collect();
|
||||
self.tabs[i].scene.erase_entities(&handles);
|
||||
self.tabs[i].dirty = true;
|
||||
|
|
|
|||
|
|
@ -1932,29 +1932,20 @@ impl OpenCADStudio {
|
|||
// once, dropping the overlay preview.
|
||||
if let Some(h) = self.grip_preview_handle.take() {
|
||||
// Undo entry for the drag (#332): the pre-drag backup
|
||||
// swaps in, the PRE state is snapshotted, then the
|
||||
// dragged result swaps back. Without this a grip edit
|
||||
// (e.g. reshaping a polyline vertex) was un-undoable —
|
||||
// the drag mutated the document without any history.
|
||||
// and live dragged image form a one-entity Arc delta. The old
|
||||
// swap/full-document snapshot made a vertex drag O(document).
|
||||
if let Some(orig) = self.grip_original.take() {
|
||||
if let Some(e) = self.tabs[i].scene.document.get_entity_mut(h) {
|
||||
let dragged = std::mem::replace(e, orig);
|
||||
let snap = self.capture_history_snapshot(i, "GRIP");
|
||||
if let Some(e2) = self.tabs[i].scene.document.get_entity_mut(h) {
|
||||
*e2 = dragged;
|
||||
}
|
||||
self.tabs[i].history.undo_stack.push(snap);
|
||||
self.tabs[i].history.redo_stack.clear();
|
||||
self.tabs[i].dirty = true;
|
||||
}
|
||||
self.push_single_entity_history(i, "GRIP", h, std::sync::Arc::new(orig));
|
||||
self.tabs[i].dirty = true;
|
||||
}
|
||||
self.grip_text_verts = Vec::new();
|
||||
self.grip_text_slide = false;
|
||||
self.tabs[i].scene.hidden.remove(&h);
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
// Only the dragged entity changed — re-tessellate just it.
|
||||
self.tabs[i].scene.mark_entity_dirty(h);
|
||||
self.tabs[i].scene.bump_geometry_no_blocks();
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.bump_entities(&[(h, crate::scene::ChangeKind::Modified)]);
|
||||
}
|
||||
// Placement confirmed — keep the just-added leader.
|
||||
self.grip_add_provisional = None;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
284
src/scene/mod.rs
284
src/scene/mod.rs
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue