Avoid full document, cache, and GPU rebuilds for entity deltas so add, delete, history, selection, and live polyline edits stay responsive on large drawings.
891 lines
35 KiB
Rust
891 lines
35 KiB
Rust
//! Shared CRUD layer for every style manager (text / dimension / table /
|
|
//! multileader / multiline).
|
|
//!
|
|
//! The five managers all expose the same list operations — New, Copy, Delete,
|
|
//! Rename, Set-Current — over a named collection of styles. Only the *property
|
|
//! editor* and the *storage backend* differ, so those are the only parts kept
|
|
//! per-manager:
|
|
//!
|
|
//! * **Table-backed** (text, dim): live in `Table<T>`, keyed by upper-cased
|
|
//! name. Renaming must re-key the entry and rewrite name-based entity
|
|
//! references (TEXT/MTEXT `style`, DIMENSION `style_name`).
|
|
//! * **Object-backed** (table, multileader, multiline): live in
|
|
//! `document.objects`, keyed by handle. Renaming only mutates the `name`
|
|
//! field; entities reference these by handle, so nothing else moves.
|
|
//!
|
|
//! Centralising the flow here is what fixes the bug class that kept recurring
|
|
//! when each manager was hand-copied: a dead New, a missing ribbon refresh, a
|
|
//! style added without a handle (dropped on DWG save, issue #67).
|
|
|
|
use super::OpenCADStudio;
|
|
use acadrust::objects::{MLineStyle, MultiLeaderStyle, ObjectType, TableStyle};
|
|
use acadrust::tables::{DimStyle, TextStyle};
|
|
use acadrust::types::Handle;
|
|
|
|
/// Guarantee the built-in "Standard" style of every kind exists in `doc` —
|
|
/// a foreign or damaged file saved without them leaves the style dropdowns
|
|
/// broken with no way to recover, and new text/dimensions have nothing to
|
|
/// reference (#366). Missing entries are re-seeded with the app defaults.
|
|
/// Called on every file open; a no-op for healthy documents.
|
|
pub(crate) fn ensure_standard_styles(doc: &mut acadrust::CadDocument) {
|
|
if !doc
|
|
.text_styles
|
|
.iter()
|
|
.any(|s| s.name.eq_ignore_ascii_case("Standard"))
|
|
{
|
|
let mut s = TextStyle::new("Standard");
|
|
s.handle = doc.allocate_handle();
|
|
let _ = doc.text_styles.add(s);
|
|
}
|
|
if !doc
|
|
.dim_styles
|
|
.iter()
|
|
.any(|s| s.name.eq_ignore_ascii_case("Standard"))
|
|
{
|
|
let mut s = DimStyle::new("Standard");
|
|
s.handle = doc.allocate_handle();
|
|
let _ = doc.dim_styles.add(s);
|
|
}
|
|
let has = |doc: &acadrust::CadDocument, pred: fn(&ObjectType) -> Option<&str>| {
|
|
doc.objects
|
|
.values()
|
|
.filter_map(pred)
|
|
.any(|n| n.eq_ignore_ascii_case("Standard"))
|
|
};
|
|
if !has(doc, |o| match o {
|
|
ObjectType::TableStyle(s) => Some(&s.name),
|
|
_ => None,
|
|
}) {
|
|
let mut s = TableStyle::standard();
|
|
s.handle = doc.allocate_handle();
|
|
doc.objects.insert(s.handle, ObjectType::TableStyle(s));
|
|
}
|
|
if !has(doc, |o| match o {
|
|
ObjectType::MultiLeaderStyle(s) => Some(&s.name),
|
|
_ => None,
|
|
}) {
|
|
let mut s = MultiLeaderStyle::standard();
|
|
s.handle = doc.allocate_handle();
|
|
doc.objects.insert(s.handle, ObjectType::MultiLeaderStyle(s));
|
|
}
|
|
if !has(doc, |o| match o {
|
|
ObjectType::MLineStyle(s) => Some(&s.name),
|
|
_ => None,
|
|
}) {
|
|
let mut s = MLineStyle::standard();
|
|
s.handle = doc.allocate_handle();
|
|
doc.objects.insert(s.handle, ObjectType::MLineStyle(s));
|
|
}
|
|
}
|
|
|
|
/// Which style manager an operation targets. Carried by the shared rename
|
|
/// messages so one handler can dispatch to the right storage.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum StyleKind {
|
|
Text,
|
|
Dim,
|
|
Table,
|
|
MLeader,
|
|
MLine,
|
|
}
|
|
|
|
impl StyleKind {
|
|
/// True when this style feeds the ribbon's quick-set dropdown.
|
|
fn in_ribbon(self) -> bool {
|
|
!matches!(self, StyleKind::MLine)
|
|
}
|
|
}
|
|
|
|
impl OpenCADStudio {
|
|
// ── Queries ────────────────────────────────────────────────────────────
|
|
|
|
/// All style names for `kind`, in display order (object-backed styles are
|
|
/// sorted by name so the `HashMap` backing them renders stably).
|
|
pub(super) fn style_names(&self, kind: StyleKind) -> Vec<String> {
|
|
let doc = &self.tabs[self.active_tab].scene.document;
|
|
let from_objects = |pick: fn(&ObjectType) -> Option<&str>| -> Vec<String> {
|
|
let mut v: Vec<String> = doc
|
|
.objects
|
|
.values()
|
|
.filter_map(pick)
|
|
.map(str::to_string)
|
|
.collect();
|
|
v.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase()));
|
|
v
|
|
};
|
|
match kind {
|
|
StyleKind::Text => doc.text_styles.iter().map(|s| s.name.clone()).collect(),
|
|
StyleKind::Dim => doc.dim_styles.iter().map(|s| s.name.clone()).collect(),
|
|
StyleKind::Table => from_objects(|o| match o {
|
|
ObjectType::TableStyle(s) => Some(s.name.as_str()),
|
|
_ => None,
|
|
}),
|
|
StyleKind::MLeader => from_objects(|o| match o {
|
|
ObjectType::MultiLeaderStyle(s) => Some(s.name.as_str()),
|
|
_ => None,
|
|
}),
|
|
StyleKind::MLine => from_objects(|o| match o {
|
|
ObjectType::MLineStyle(s) => Some(s.name.as_str()),
|
|
_ => None,
|
|
}),
|
|
}
|
|
}
|
|
|
|
pub(super) fn style_selected(&self, kind: StyleKind) -> String {
|
|
match kind {
|
|
StyleKind::Text => self.textstyle_selected.clone(),
|
|
StyleKind::Dim => self.dimstyle_selected.clone(),
|
|
StyleKind::Table => self.tablestyle_selected.clone(),
|
|
StyleKind::MLeader => self.mleaderstyle_selected.clone(),
|
|
StyleKind::MLine => self.mlstyle_selected.clone(),
|
|
}
|
|
}
|
|
|
|
fn set_style_selected(&mut self, kind: StyleKind, name: String) {
|
|
match kind {
|
|
StyleKind::Text => self.textstyle_selected = name,
|
|
StyleKind::Dim => self.dimstyle_selected = name,
|
|
StyleKind::Table => self.tablestyle_selected = name,
|
|
StyleKind::MLeader => self.mleaderstyle_selected = name,
|
|
StyleKind::MLine => self.mlstyle_selected = name,
|
|
}
|
|
}
|
|
|
|
fn style_exists(&self, kind: StyleKind, name: &str) -> bool {
|
|
self.style_names(kind)
|
|
.iter()
|
|
.any(|n| n.eq_ignore_ascii_case(name))
|
|
}
|
|
|
|
/// First free `Style{n}` name for a fresh style.
|
|
fn unique_new_name(&self, kind: StyleKind) -> String {
|
|
(1u32..)
|
|
.map(|n| format!("Style{n}"))
|
|
.find(|c| !self.style_exists(kind, c))
|
|
.unwrap()
|
|
}
|
|
|
|
/// First free `{base} ({n})` name for a copy / disambiguated entry.
|
|
fn unique_suffixed_name(&self, kind: StyleKind, base: &str) -> String {
|
|
(1u32..)
|
|
.map(|n| format!("{base} ({n})"))
|
|
.find(|c| !self.style_exists(kind, c))
|
|
.unwrap()
|
|
}
|
|
|
|
// ── Per-manager glue (the only kind-specific list code) ────────────────
|
|
|
|
/// Reload the property-editor buffers for the kinds that have them.
|
|
fn load_style_bufs(&mut self, kind: StyleKind) {
|
|
let i = self.active_tab;
|
|
match kind {
|
|
StyleKind::Text => self.load_textstyle_bufs(i),
|
|
StyleKind::Dim => self.load_dimstyle_bufs(i),
|
|
StyleKind::MLeader => self.load_mleaderstyle_bufs(i),
|
|
StyleKind::Table | StyleKind::MLine => {}
|
|
}
|
|
}
|
|
|
|
/// Refresh anything that mirrors the style list / current style after a
|
|
/// mutation (ribbon dropdowns, geometry that depends on the style).
|
|
fn after_style_change(&mut self, kind: StyleKind) {
|
|
if kind.in_ribbon() {
|
|
self.sync_ribbon_styles();
|
|
}
|
|
}
|
|
|
|
fn insert_default_style(&mut self, kind: StyleKind, name: &str, handle: Handle) {
|
|
let doc = &mut self.tabs[self.active_tab].scene.document;
|
|
match kind {
|
|
StyleKind::Text => {
|
|
let mut s = TextStyle::new(name);
|
|
s.handle = handle;
|
|
let _ = doc.text_styles.add(s);
|
|
}
|
|
StyleKind::Dim => {
|
|
let mut s = DimStyle::new(name);
|
|
s.handle = handle;
|
|
let _ = doc.dim_styles.add(s);
|
|
}
|
|
StyleKind::Table => {
|
|
let mut s = TableStyle::standard();
|
|
s.name = name.to_string();
|
|
s.handle = handle;
|
|
doc.objects.insert(handle, ObjectType::TableStyle(s));
|
|
}
|
|
StyleKind::MLeader => {
|
|
let mut s = MultiLeaderStyle::new(name);
|
|
s.handle = handle;
|
|
doc.objects.insert(handle, ObjectType::MultiLeaderStyle(s));
|
|
}
|
|
StyleKind::MLine => {
|
|
let mut s = MLineStyle::standard();
|
|
s.name = name.to_string();
|
|
s.handle = handle;
|
|
doc.objects.insert(handle, ObjectType::MLineStyle(s));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Clone the style named `src` under `name` with a fresh `handle`.
|
|
/// Returns false if `src` no longer exists.
|
|
fn clone_style_as(&mut self, kind: StyleKind, src: &str, name: &str, handle: Handle) -> bool {
|
|
let doc = &mut self.tabs[self.active_tab].scene.document;
|
|
match kind {
|
|
StyleKind::Text => {
|
|
if let Some(mut s) = doc.text_styles.get(src).cloned() {
|
|
s.name = name.to_string();
|
|
s.handle = handle;
|
|
let _ = doc.text_styles.add(s);
|
|
return true;
|
|
}
|
|
}
|
|
StyleKind::Dim => {
|
|
if let Some(mut s) = doc.dim_styles.get(src).cloned() {
|
|
s.name = name.to_string();
|
|
s.handle = handle;
|
|
let _ = doc.dim_styles.add(s);
|
|
return true;
|
|
}
|
|
}
|
|
StyleKind::Table => {
|
|
if let Some(mut s) = find_object_style(doc, src, |o| match o {
|
|
ObjectType::TableStyle(s) => Some((s.name.as_str(), s.clone())),
|
|
_ => None,
|
|
}) {
|
|
s.name = name.to_string();
|
|
s.handle = handle;
|
|
doc.objects.insert(handle, ObjectType::TableStyle(s));
|
|
return true;
|
|
}
|
|
}
|
|
StyleKind::MLeader => {
|
|
if let Some(mut s) = find_object_style(doc, src, |o| match o {
|
|
ObjectType::MultiLeaderStyle(s) => Some((s.name.as_str(), s.clone())),
|
|
_ => None,
|
|
}) {
|
|
s.name = name.to_string();
|
|
s.handle = handle;
|
|
doc.objects.insert(handle, ObjectType::MultiLeaderStyle(s));
|
|
return true;
|
|
}
|
|
}
|
|
StyleKind::MLine => {
|
|
if let Some(mut s) = find_object_style(doc, src, |o| match o {
|
|
ObjectType::MLineStyle(s) => Some((s.name.as_str(), s.clone())),
|
|
_ => None,
|
|
}) {
|
|
s.name = name.to_string();
|
|
s.handle = handle;
|
|
doc.objects.insert(handle, ObjectType::MLineStyle(s));
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
fn remove_style_storage(&mut self, kind: StyleKind, name: &str) -> bool {
|
|
let doc = &mut self.tabs[self.active_tab].scene.document;
|
|
match kind {
|
|
StyleKind::Text => doc.text_styles.remove(name).is_some(),
|
|
StyleKind::Dim => doc.dim_styles.remove(name).is_some(),
|
|
StyleKind::Table | StyleKind::MLeader | StyleKind::MLine => {
|
|
let kind2 = kind;
|
|
if let Some(h) = object_handle(doc, name, kind2) {
|
|
doc.objects.remove(&h).is_some()
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Rename `old`→`new` in the backing store, re-keying table entries and
|
|
/// rewriting name-based references + current-style pointers.
|
|
fn rename_style_storage(&mut self, kind: StyleKind, old: &str, new: &str) {
|
|
let i = self.active_tab;
|
|
match kind {
|
|
StyleKind::Text => {
|
|
let doc = &mut self.tabs[i].scene.document;
|
|
if let Some(mut s) = doc.text_styles.get(old).cloned() {
|
|
s.name = new.to_string();
|
|
if !s.handle.is_valid() {
|
|
s.handle = doc.allocate_handle();
|
|
}
|
|
let _ = doc.text_styles.add(s);
|
|
}
|
|
doc.text_styles.remove(old);
|
|
if doc.header.current_text_style_name.eq_ignore_ascii_case(old) {
|
|
doc.header.current_text_style_name = new.to_string();
|
|
}
|
|
for e in doc.entities_mut() {
|
|
match e {
|
|
acadrust::entities::EntityType::Text(t)
|
|
if t.style.eq_ignore_ascii_case(old) =>
|
|
{
|
|
t.style = new.to_string();
|
|
}
|
|
acadrust::entities::EntityType::MText(t)
|
|
if t.style.eq_ignore_ascii_case(old) =>
|
|
{
|
|
t.style = new.to_string();
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
StyleKind::Dim => {
|
|
let doc = &mut self.tabs[i].scene.document;
|
|
if let Some(mut s) = doc.dim_styles.get(old).cloned() {
|
|
s.name = new.to_string();
|
|
if !s.handle.is_valid() {
|
|
s.handle = doc.allocate_handle();
|
|
}
|
|
let _ = doc.dim_styles.add(s);
|
|
}
|
|
doc.dim_styles.remove(old);
|
|
if doc.header.current_dimstyle_name.eq_ignore_ascii_case(old) {
|
|
doc.header.current_dimstyle_name = new.to_string();
|
|
}
|
|
for e in doc.entities_mut() {
|
|
if let acadrust::entities::EntityType::Dimension(d) = e {
|
|
if d.base().style_name.eq_ignore_ascii_case(old) {
|
|
d.base_mut().style_name = new.to_string();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
StyleKind::Table => {
|
|
let doc = &mut self.tabs[i].scene.document;
|
|
if let Some(h) = object_handle(doc, old, kind) {
|
|
if let Some(ObjectType::TableStyle(s)) = doc.objects.get_mut(&h) {
|
|
s.name = new.to_string();
|
|
}
|
|
}
|
|
if self.ribbon.active_table_style.eq_ignore_ascii_case(old) {
|
|
self.ribbon.active_table_style = new.to_string();
|
|
}
|
|
}
|
|
StyleKind::MLeader => {
|
|
let doc = &mut self.tabs[i].scene.document;
|
|
if let Some(h) = object_handle(doc, old, kind) {
|
|
if let Some(ObjectType::MultiLeaderStyle(s)) = doc.objects.get_mut(&h) {
|
|
s.name = new.to_string();
|
|
}
|
|
}
|
|
if self.tabs[i].active_mleader_style.eq_ignore_ascii_case(old) {
|
|
self.tabs[i].active_mleader_style = new.to_string();
|
|
}
|
|
if self.ribbon.active_mleader_style.eq_ignore_ascii_case(old) {
|
|
self.ribbon.active_mleader_style = new.to_string();
|
|
}
|
|
}
|
|
StyleKind::MLine => {
|
|
let doc = &mut self.tabs[i].scene.document;
|
|
if let Some(h) = object_handle(doc, old, kind) {
|
|
if let Some(ObjectType::MLineStyle(s)) = doc.objects.get_mut(&h) {
|
|
s.name = new.to_string();
|
|
}
|
|
}
|
|
if doc.header.multiline_style.eq_ignore_ascii_case(old) {
|
|
doc.header.multiline_style = new.to_string();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Public operations (called by the message handlers) ─────────────────
|
|
|
|
// Note: the structural ops below mutate the document live (so the dialog
|
|
// shows a preview) but do NOT mark the tab dirty, push undo, or rebuild the
|
|
// drawing. Those side effects are deferred to `style_stage_commit` (Apply);
|
|
// closing the window without Apply calls `style_stage_discard`, which
|
|
// restores the snapshot taken when the manager opened.
|
|
|
|
pub(super) fn style_new(&mut self, kind: StyleKind) {
|
|
let i = self.active_tab;
|
|
let name = self.unique_new_name(kind);
|
|
let h = self.tabs[i].scene.document.allocate_handle();
|
|
self.insert_default_style(kind, &name, h);
|
|
self.set_style_selected(kind, name.clone());
|
|
self.load_style_bufs(kind);
|
|
self.after_style_change(kind);
|
|
self.command_line
|
|
.push_output(&format!("Style '{name}' created."));
|
|
}
|
|
|
|
pub(super) fn style_copy(&mut self, kind: StyleKind) {
|
|
let i = self.active_tab;
|
|
let src = self.style_selected(kind);
|
|
let name = self.unique_suffixed_name(kind, &src);
|
|
let h = self.tabs[i].scene.document.allocate_handle();
|
|
if !self.clone_style_as(kind, &src, &name, h) {
|
|
return;
|
|
}
|
|
self.set_style_selected(kind, name.clone());
|
|
self.load_style_bufs(kind);
|
|
self.after_style_change(kind);
|
|
self.command_line
|
|
.push_output(&format!("Style '{name}' created."));
|
|
}
|
|
|
|
pub(super) fn style_delete(&mut self, kind: StyleKind) {
|
|
let name = self.style_selected(kind);
|
|
if name.eq_ignore_ascii_case("Standard") {
|
|
self.command_line
|
|
.push_error("Cannot delete the Standard style.");
|
|
return;
|
|
}
|
|
if !self.remove_style_storage(kind, &name) {
|
|
return;
|
|
}
|
|
let first = self
|
|
.style_names(kind)
|
|
.into_iter()
|
|
.next()
|
|
.unwrap_or_else(|| "Standard".to_string());
|
|
self.set_style_selected(kind, first);
|
|
self.load_style_bufs(kind);
|
|
self.after_style_change(kind);
|
|
self.command_line
|
|
.push_output(&format!("Style '{name}' deleted."));
|
|
}
|
|
|
|
/// Begin inline rename of the double-clicked style.
|
|
pub(super) fn style_rename_start(&mut self, kind: StyleKind, name: String) {
|
|
self.set_style_selected(kind, name.clone());
|
|
self.load_style_bufs(kind);
|
|
self.style_rename_buf = name.clone();
|
|
self.style_rename = Some(name);
|
|
}
|
|
|
|
/// Commit the inline rename. No-op (with feedback) on empty / unchanged /
|
|
/// colliding names, and the Standard style cannot be renamed.
|
|
pub(super) fn style_rename_commit(&mut self, kind: StyleKind) {
|
|
let Some(old) = self.style_rename.take() else {
|
|
return;
|
|
};
|
|
let new = self.style_rename_buf.trim().to_string();
|
|
self.style_rename_buf.clear();
|
|
if new.is_empty() || new.eq_ignore_ascii_case(&old) {
|
|
return;
|
|
}
|
|
if old.eq_ignore_ascii_case("Standard") {
|
|
self.command_line
|
|
.push_error("Cannot rename the Standard style.");
|
|
return;
|
|
}
|
|
if self.style_exists(kind, &new) {
|
|
self.command_line
|
|
.push_error(&format!("Style '{new}' already exists."));
|
|
return;
|
|
}
|
|
self.rename_style_storage(kind, &old, &new);
|
|
if self.style_selected(kind).eq_ignore_ascii_case(&old) {
|
|
self.set_style_selected(kind, new.clone());
|
|
}
|
|
self.load_style_bufs(kind);
|
|
self.after_style_change(kind);
|
|
self.command_line
|
|
.push_output(&format!("Renamed '{old}' → '{new}'."));
|
|
}
|
|
|
|
pub(super) fn style_rename_cancel(&mut self) {
|
|
self.style_rename = None;
|
|
self.style_rename_buf.clear();
|
|
}
|
|
|
|
// ── Staging: nothing persists until Apply ──────────────────────────────
|
|
//
|
|
// A style manager is a transaction. When it opens we snapshot the style
|
|
// tables / objects / current pointers; every New / Copy / Delete / Rename /
|
|
// Set Current / property edit mutates the live document for an in-dialog
|
|
// preview, but the tab stays clean and the drawing is not rebuilt. Apply
|
|
// commits (marks dirty, pushes one undo entry, rebuilds); closing the
|
|
// window without Apply discards by restoring the snapshot.
|
|
|
|
/// Snapshot the style-related document state into a transferable record.
|
|
fn capture_style_state(&self) -> StyleStateSnapshot {
|
|
let i = self.active_tab;
|
|
let doc = &self.tabs[i].scene.document;
|
|
let style_objects = doc
|
|
.objects
|
|
.iter()
|
|
.filter(|(_, o)| {
|
|
matches!(
|
|
o,
|
|
ObjectType::TableStyle(_)
|
|
| ObjectType::MLineStyle(_)
|
|
| ObjectType::MultiLeaderStyle(_)
|
|
)
|
|
})
|
|
.map(|(&h, o)| (h, o.clone()))
|
|
.collect();
|
|
StyleStateSnapshot {
|
|
text_styles: doc.text_styles.clone(),
|
|
dim_styles: doc.dim_styles.clone(),
|
|
style_objects,
|
|
current_text: doc.header.current_text_style_name.clone(),
|
|
current_dim: doc.header.current_dimstyle_name.clone(),
|
|
multiline_style: doc.header.multiline_style.clone(),
|
|
current_table: doc.header.current_table_style_name.clone(),
|
|
current_mleader: doc.header.current_mleader_style_name.clone(),
|
|
active_table: self.ribbon.active_table_style.clone(),
|
|
active_mleader: self.ribbon.active_mleader_style.clone(),
|
|
tab_active_mleader: self.tabs[i].active_mleader_style.clone(),
|
|
}
|
|
}
|
|
|
|
/// Overwrite the live style state with a snapshot (used by commit's undo
|
|
/// dance and by discard).
|
|
pub(super) fn restore_style_state(&mut self, i: usize, snap: &StyleStateSnapshot) {
|
|
let doc = &mut self.tabs[i].scene.document;
|
|
doc.text_styles = snap.text_styles.clone();
|
|
doc.dim_styles = snap.dim_styles.clone();
|
|
doc.objects.retain(|_, o| {
|
|
!matches!(
|
|
o,
|
|
ObjectType::TableStyle(_)
|
|
| ObjectType::MLineStyle(_)
|
|
| ObjectType::MultiLeaderStyle(_)
|
|
)
|
|
});
|
|
for (h, o) in &snap.style_objects {
|
|
doc.objects.insert(*h, o.clone());
|
|
}
|
|
doc.header.current_text_style_name = snap.current_text.clone();
|
|
doc.header.current_dimstyle_name = snap.current_dim.clone();
|
|
doc.header.multiline_style = snap.multiline_style.clone();
|
|
doc.header.current_table_style_name = snap.current_table.clone();
|
|
doc.header.current_mleader_style_name = snap.current_mleader.clone();
|
|
if i == self.active_tab {
|
|
self.ribbon.active_table_style = snap.active_table.clone();
|
|
self.ribbon.active_mleader_style = snap.active_mleader.clone();
|
|
}
|
|
self.tabs[i].active_mleader_style = snap.tab_active_mleader.clone();
|
|
}
|
|
|
|
/// Begin a staging transaction for a freshly-opened style manager.
|
|
pub(super) fn style_stage_begin(&mut self) {
|
|
let dirty_at_open = self.tabs[self.active_tab].dirty;
|
|
let baseline = self.capture_style_state();
|
|
self.style_stage = Some(StyleStage {
|
|
dirty_at_open,
|
|
baseline,
|
|
});
|
|
}
|
|
|
|
/// Commit the staged changes (Apply): make them permanent with a single
|
|
/// undo entry, mark the tab dirty, and rebuild the drawing.
|
|
pub(super) fn style_stage_commit(&mut self) {
|
|
let i = self.active_tab;
|
|
let Some(stage) = self.style_stage.take() else {
|
|
// No staged edits: a bare Apply has no geometry work to publish.
|
|
self.sync_ribbon_styles();
|
|
return;
|
|
};
|
|
let edited = self.capture_style_state();
|
|
let changed = edited != stage.baseline;
|
|
if changed {
|
|
self.tabs[i].dirty = true;
|
|
let (text_names, dim_names, object_handles) = edited.changed_keys(&stage.baseline);
|
|
self.tabs[i].scene.invalidate_text_style_dependencies_many(&text_names);
|
|
self.tabs[i]
|
|
.scene
|
|
.invalidate_dim_style_dependencies_many(&dim_names);
|
|
self.tabs[i]
|
|
.scene
|
|
.invalidate_object_style_dependencies(&object_handles);
|
|
self.commit_style_undo(i, stage.baseline, edited.clone(), stage.dirty_at_open);
|
|
} else {
|
|
self.tabs[i].dirty = stage.dirty_at_open;
|
|
}
|
|
self.sync_ribbon_styles();
|
|
// Re-baseline so further edits in the still-open window stage afresh.
|
|
self.style_stage = Some(StyleStage {
|
|
dirty_at_open: self.tabs[i].dirty,
|
|
baseline: edited,
|
|
});
|
|
}
|
|
|
|
/// Discard staged changes (window closed without Apply): restore the
|
|
/// snapshot taken when the manager opened.
|
|
pub(super) fn style_stage_discard(&mut self) {
|
|
let Some(stage) = self.style_stage.take() else {
|
|
return;
|
|
};
|
|
self.restore_style_state(self.active_tab, &stage.baseline);
|
|
self.tabs[self.active_tab].dirty = stage.dirty_at_open;
|
|
self.sync_ribbon_styles();
|
|
}
|
|
}
|
|
|
|
/// Snapshot of every document field a style manager can touch.
|
|
#[derive(Clone, PartialEq)]
|
|
pub(super) struct StyleStateSnapshot {
|
|
text_styles: acadrust::tables::Table<TextStyle>,
|
|
dim_styles: acadrust::tables::Table<DimStyle>,
|
|
style_objects: Vec<(Handle, ObjectType)>,
|
|
current_text: String,
|
|
current_dim: String,
|
|
multiline_style: String,
|
|
current_table: String,
|
|
current_mleader: String,
|
|
active_table: String,
|
|
active_mleader: String,
|
|
tab_active_mleader: String,
|
|
}
|
|
|
|
impl StyleStateSnapshot {
|
|
pub(super) fn estimated_bytes(&self) -> usize {
|
|
self.text_styles
|
|
.iter()
|
|
.count()
|
|
.saturating_mul(320)
|
|
.saturating_add(self.dim_styles.iter().count().saturating_mul(1024))
|
|
.saturating_add(self.style_objects.len().saturating_mul(512))
|
|
}
|
|
|
|
pub(super) fn changed_keys(&self, other: &Self) -> (Vec<String>, Vec<String>, Vec<Handle>) {
|
|
let text_names = self
|
|
.text_styles
|
|
.iter()
|
|
.filter(|style| other.text_styles.get(&style.name) != Some(*style))
|
|
.map(|style| style.name.clone())
|
|
.chain(
|
|
other
|
|
.text_styles
|
|
.iter()
|
|
.filter(|style| self.text_styles.get(&style.name) != Some(*style))
|
|
.map(|style| style.name.clone()),
|
|
)
|
|
.collect::<rustc_hash::FxHashSet<_>>()
|
|
.into_iter()
|
|
.collect();
|
|
let dim_names = self
|
|
.dim_styles
|
|
.iter()
|
|
.filter(|style| other.dim_styles.get(&style.name) != Some(*style))
|
|
.map(|style| style.name.clone())
|
|
.chain(
|
|
other
|
|
.dim_styles
|
|
.iter()
|
|
.filter(|style| self.dim_styles.get(&style.name) != Some(*style))
|
|
.map(|style| style.name.clone()),
|
|
)
|
|
.collect::<rustc_hash::FxHashSet<_>>()
|
|
.into_iter()
|
|
.collect();
|
|
let self_objects: rustc_hash::FxHashMap<_, _> =
|
|
self.style_objects.iter().map(|(h, o)| (*h, o)).collect();
|
|
let other_objects: rustc_hash::FxHashMap<_, _> =
|
|
other.style_objects.iter().map(|(h, o)| (*h, o)).collect();
|
|
let object_handles = self
|
|
.style_objects
|
|
.iter()
|
|
.chain(other.style_objects.iter())
|
|
.map(|(handle, _)| *handle)
|
|
.collect::<rustc_hash::FxHashSet<_>>()
|
|
.into_iter()
|
|
.filter(|handle| self_objects.get(handle) != other_objects.get(handle))
|
|
.collect();
|
|
(text_names, dim_names, object_handles)
|
|
}
|
|
}
|
|
|
|
/// An in-progress style-manager transaction.
|
|
pub(super) struct StyleStage {
|
|
dirty_at_open: bool,
|
|
baseline: StyleStateSnapshot,
|
|
}
|
|
|
|
// ── Object-store helpers ───────────────────────────────────────────────────
|
|
|
|
/// Find the object-backed style named `name` and return a clone. `pick` maps a
|
|
/// matching variant to `(its name, a clone of the inner style)`.
|
|
fn find_object_style<T>(
|
|
doc: &acadrust::CadDocument,
|
|
name: &str,
|
|
pick: impl Fn(&ObjectType) -> Option<(&str, T)>,
|
|
) -> Option<T> {
|
|
doc.objects.values().find_map(|o| {
|
|
let (n, val) = pick(o)?;
|
|
n.eq_ignore_ascii_case(name).then_some(val)
|
|
})
|
|
}
|
|
|
|
fn object_handle(doc: &acadrust::CadDocument, name: &str, kind: StyleKind) -> Option<Handle> {
|
|
doc.objects.iter().find_map(|(&h, o)| {
|
|
let matches = match (kind, o) {
|
|
(StyleKind::Table, ObjectType::TableStyle(s)) => s.name.eq_ignore_ascii_case(name),
|
|
(StyleKind::MLeader, ObjectType::MultiLeaderStyle(s)) => {
|
|
s.name.eq_ignore_ascii_case(name)
|
|
}
|
|
(StyleKind::MLine, ObjectType::MLineStyle(s)) => s.name.eq_ignore_ascii_case(name),
|
|
_ => false,
|
|
};
|
|
matches.then_some(h)
|
|
})
|
|
}
|
|
|
|
// ── Annotation-scale manager staging (mirrors the style-manager stage) ──────
|
|
|
|
/// Snapshot of the drawing's annotation-scale *list* a scale manager can stage:
|
|
/// the `Scale` objects and the `ACAD_SCALELIST` dictionary. Set Current takes
|
|
/// effect immediately (like the pill) and is *not* reverted; `current` is kept
|
|
/// only to repair the header pointer if a staged rename/delete of the current
|
|
/// scale is rolled back, leaving it dangling.
|
|
pub(super) struct ScaleSnapshot {
|
|
scales: Vec<(Handle, ObjectType)>,
|
|
scalelist: Option<(Handle, ObjectType)>,
|
|
current: String,
|
|
}
|
|
|
|
/// An in-progress scale-manager transaction (baseline restored on discard).
|
|
/// `changed` guards against a no-op Apply dirtying the drawing / pushing undo.
|
|
pub(super) struct ScaleStage {
|
|
dirty_at_open: bool,
|
|
baseline: ScaleSnapshot,
|
|
changed: bool,
|
|
/// The manager materialised the fallback scale set into real objects on
|
|
/// open. Even with no user edit these must be reverted on close, so the
|
|
/// discard restores the baseline when this is set (not only on `changed`).
|
|
materialized: bool,
|
|
}
|
|
|
|
impl OpenCADStudio {
|
|
fn capture_scale_state(&self) -> ScaleSnapshot {
|
|
let i = self.active_tab;
|
|
let doc = &self.tabs[i].scene.document;
|
|
let scales: Vec<(Handle, ObjectType)> = doc
|
|
.objects
|
|
.iter()
|
|
.filter(|(_, o)| matches!(o, ObjectType::Scale(_)))
|
|
.map(|(h, o)| (*h, o.clone()))
|
|
.collect();
|
|
let scalelist_h = self.tabs[i].scene.scalelist_dict_handle();
|
|
let scalelist = scalelist_h.and_then(|h| doc.objects.get(&h).map(|o| (h, o.clone())));
|
|
let current = doc.header.current_annotation_scale.clone();
|
|
ScaleSnapshot {
|
|
scales,
|
|
scalelist,
|
|
current,
|
|
}
|
|
}
|
|
|
|
fn restore_scale_state(&mut self, snap: &ScaleSnapshot) {
|
|
let i = self.active_tab;
|
|
let root_h = self.tabs[i].scene.document.header.named_objects_dict_handle;
|
|
let cur_scalelist_h = self.tabs[i].scene.scalelist_dict_handle();
|
|
let doc = &mut self.tabs[i].scene.document;
|
|
doc.objects.retain(|_, o| !matches!(o, ObjectType::Scale(_)));
|
|
if let Some(h) = cur_scalelist_h {
|
|
doc.objects.remove(&h);
|
|
}
|
|
for (h, o) in &snap.scales {
|
|
doc.objects.insert(*h, o.clone());
|
|
}
|
|
if let Some((h, o)) = &snap.scalelist {
|
|
doc.objects.insert(*h, o.clone());
|
|
}
|
|
// Restore ONLY the ACAD_SCALELIST root entry — leave every other root
|
|
// entry alone (a modal blocks concurrent edits, but be surgical anyway).
|
|
let baseline_scalelist_h = snap.scalelist.as_ref().map(|(h, _)| *h);
|
|
if let Some(ObjectType::Dictionary(root)) = doc.objects.get_mut(&root_h) {
|
|
root.entries.retain(|(k, _)| k != "ACAD_SCALELIST");
|
|
if let Some(h) = baseline_scalelist_h {
|
|
root.entries.push(("ACAD_SCALELIST".to_string(), h));
|
|
}
|
|
}
|
|
// Repair a dangling current-scale pointer. A staged rename/delete of the
|
|
// current scale rewrote the header outside this snapshot; if the live
|
|
// name no longer resolves against the restored list, fall back to the
|
|
// snapshot's name. A Set Current to a still-valid scale is preserved.
|
|
let cur = self.tabs[i]
|
|
.scene
|
|
.document
|
|
.header
|
|
.current_annotation_scale
|
|
.clone();
|
|
let resolves = self.tabs[i]
|
|
.scene
|
|
.scale_list()
|
|
.iter()
|
|
.any(|(n, _, _)| n.eq_ignore_ascii_case(&cur));
|
|
if !resolves {
|
|
self.tabs[i].scene.document.header.current_annotation_scale = snap.current.clone();
|
|
}
|
|
self.tabs[i].scene.invalidate_annotation_dependencies();
|
|
}
|
|
|
|
/// Begin a staging transaction for a freshly-opened scale manager.
|
|
pub(super) fn scale_stage_begin(&mut self) {
|
|
let dirty_at_open = self.tabs[self.active_tab].dirty;
|
|
let baseline = self.capture_scale_state();
|
|
self.scale_stage = Some(ScaleStage {
|
|
dirty_at_open,
|
|
baseline,
|
|
changed: false,
|
|
materialized: false,
|
|
});
|
|
}
|
|
|
|
/// Flag that the staged scale list has actually been mutated, so a later
|
|
/// Apply commits (and a no-op Apply doesn't dirty the drawing).
|
|
pub(super) fn scale_stage_mark(&mut self) {
|
|
if let Some(s) = self.scale_stage.as_mut() {
|
|
s.changed = true;
|
|
}
|
|
}
|
|
|
|
/// Flag that the manager materialised the fallback scale set on open, so
|
|
/// the discard reverts it even when the user made no edit.
|
|
pub(super) fn scale_stage_materialized(&mut self) {
|
|
if let Some(s) = self.scale_stage.as_mut() {
|
|
s.materialized = true;
|
|
}
|
|
}
|
|
|
|
/// Commit staged scale changes (Apply): one undo entry, mark dirty, and
|
|
/// re-baseline so the still-open window keeps staging afresh. A no-op Apply
|
|
/// (nothing staged) does nothing.
|
|
pub(super) fn scale_stage_commit(&mut self) {
|
|
let i = self.active_tab;
|
|
let Some(stage) = self.scale_stage.take() else {
|
|
return;
|
|
};
|
|
if !stage.changed {
|
|
self.scale_stage = Some(stage);
|
|
return;
|
|
}
|
|
let edited = self.capture_scale_state();
|
|
self.restore_scale_state(&stage.baseline);
|
|
self.push_undo_snapshot(i, "SCALELISTEDIT");
|
|
self.restore_scale_state(&edited);
|
|
self.tabs[i].dirty = true;
|
|
self.scale_stage = Some(ScaleStage {
|
|
dirty_at_open: true,
|
|
baseline: edited,
|
|
changed: false,
|
|
// The materialised set is now committed as the new baseline, so it
|
|
// no longer needs reverting on close.
|
|
materialized: false,
|
|
});
|
|
}
|
|
|
|
/// Discard staged scale changes (window closed without Apply): restore the
|
|
/// snapshot taken when the manager opened. No-op when nothing was staged.
|
|
pub(super) fn scale_stage_discard(&mut self) {
|
|
let Some(stage) = self.scale_stage.take() else {
|
|
return;
|
|
};
|
|
// Revert on a real edit *or* when only the fallback set was materialised
|
|
// (opened but not applied) — otherwise those 12 scales would leak in.
|
|
if stage.changed || stage.materialized {
|
|
self.restore_scale_state(&stage.baseline);
|
|
self.tabs[self.active_tab].dirty = stage.dirty_at_open;
|
|
}
|
|
}
|
|
}
|