fix(annotative): stage scale-manager edits like the style managers

The scale manager now runs as a transaction, matching the style managers'
staging: New starts a blank editor draft (nothing is added to the drawing
until Apply), Delete removes immediately but staged, and closing the window
without Apply restores the snapshot taken on open — so a scale created or
deleted mid-session is discarded on cancel. Apply commits the staged list
changes as a single undo entry. This replaces the earlier behaviour where
New copied the selected scale and edits landed immediately.

A `changed` flag guards the commit so a no-op / failed Apply (blank or
invalid fields, or an edit that didn't apply) no longer dirties the drawing
or pushes a phantom undo entry. Set Current is deliberately outside the
transaction — it takes effect immediately like the scale pill (and syncs
CANNOSCALEVALUE), so it is never rolled back on close. Renaming the current
scale follows the pointer; editing one of the built-in fallback scales (shown
when the drawing has no scale list) materialises it as a real scale; the
snapshot restore touches only the ACAD_SCALELIST root entry.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-07-13 12:14:40 +03:00
1 parent 1071570f40
commit cd766bc1f5
4 files changed
+220 -48

No files matched your search

+8
View File
@@ -664,6 +664,12 @@ pub(super) struct OpenCADStudio {
scale_manager_name_buf: String,
scale_manager_paper_buf: String,
scale_manager_drawing_buf: String,
/// The editor holds an unsaved *new* scale (created only on Apply) rather
/// than editing the selected one. Reset when a row is selected.
scale_manager_new: bool,
/// Open transaction for the scale manager — restored if the window closes
/// without Apply, mirroring the style managers' staging.
scale_stage: Option<crate::app::style_ops::ScaleStage>,
// ── Plot Style Panel ──────────────────────────────────────────────────
/// Selected ACI index in the panel (1-255).
@@ -2257,6 +2263,8 @@ impl OpenCADStudio {
scale_manager_name_buf: String::new(),
scale_manager_paper_buf: String::new(),
scale_manager_drawing_buf: String::new(),
scale_manager_new: false,
scale_stage: None,
layout_manager_rename_buf: String::new(),
plotstyle_panel_aci: 1,
ps_color_buf: String::new(),
+120
View File
@@ -608,3 +608,123 @@ fn object_handle(doc: &acadrust::CadDocument, name: &str, kind: StyleKind) -> Op
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. The current scale is
/// deliberately excluded — Set Current takes effect immediately, like the pill.
pub(super) struct ScaleSnapshot {
scales: Vec<(Handle, ObjectType)>,
scalelist: Option<(Handle, ObjectType)>,
}
/// 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,
}
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 root_h = doc.header.named_objects_dict_handle;
let scalelist_h =
crate::scene::annotative::as_dict(doc, root_h).and_then(|d| d.get("ACAD_SCALELIST"));
let scalelist = scalelist_h.and_then(|h| doc.objects.get(&h).map(|o| (h, o.clone())));
ScaleSnapshot { scales, scalelist }
}
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 =
crate::scene::annotative::as_dict(&self.tabs[i].scene.document, root_h)
.and_then(|d| d.get("ACAD_SCALELIST"));
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));
}
}
self.tabs[i].scene.bump_geometry();
}
/// 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,
});
}
/// 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;
}
}
/// 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,
});
}
/// 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;
};
if stage.changed {
self.restore_scale_state(&stage.baseline);
self.tabs[self.active_tab].dirty = stage.dirty_at_open;
}
}
}
+74 -31
View File
@@ -77,6 +77,9 @@ impl OpenCADStudio {
) {
self.style_stage_discard();
}
if self.active_modal == Some(ScaleManager) {
self.scale_stage_discard();
}
match self.active_modal {
Some(Layers) => self.ribbon.deactivate_tool_if("LAYERS"),
Some(Plot) => {
@@ -1650,6 +1653,10 @@ impl OpenCADStudio {
Message::ScaleManagerOpen => {
let i = self.active_tab;
self.scale_popup_open = false;
// Snapshot so New / Delete / edits revert if closed without Apply.
self.scale_stage_begin();
// Open on the current scale, ready to edit (not a new draft).
self.scale_manager_new = false;
let cur = self.tabs[i]
.scene
.document
@@ -1661,6 +1668,7 @@ impl OpenCADStudio {
Task::none()
}
Message::ScaleManagerSelect(name) => {
self.scale_manager_new = false;
self.load_scale_editor(&name);
Task::none()
}
@@ -1677,35 +1685,69 @@ impl OpenCADStudio {
Task::none()
}
Message::ScaleManagerNew => {
let i = self.active_tab;
if let (name, Some(paper), Some(drawing)) = self.scale_editor_values() {
if !name.is_empty() && paper > 0.0 && drawing > 0.0 {
self.push_undo_snapshot(i, "SCALELISTEDIT");
if self.tabs[i].scene.add_scale(&name, paper, drawing) {
self.scale_manager_selected = name;
self.tabs[i].dirty = true;
}
}
}
// Start a blank draft — nothing is added to the drawing until
// Apply; closing the manager without Apply discards it.
self.scale_manager_new = true;
self.scale_manager_selected.clear();
self.scale_manager_name_buf.clear();
self.scale_manager_paper_buf = "1".to_string();
self.scale_manager_drawing_buf = "1".to_string();
Task::none()
}
Message::ScaleManagerApply => {
// Fold the editor's add / edit into the staged document, then
// commit the whole transaction (this change plus any staged
// deletes) as one undo entry. A new draft is added; otherwise
// the selected scale is renamed / re-ratioed.
let i = self.active_tab;
let old = self.scale_manager_selected.clone();
if !old.is_empty() {
if let (name, Some(paper), Some(drawing)) = self.scale_editor_values() {
if !name.is_empty() && paper > 0.0 && drawing > 0.0 {
self.push_undo_snapshot(i, "SCALELISTEDIT");
if self.tabs[i].scene.edit_scale(&old, &name, paper, drawing) {
self.scale_manager_selected = name;
self.tabs[i].dirty = true;
let name = self.scale_manager_name_buf.trim().to_string();
let paper = self.scale_manager_paper_buf.trim().parse::<f64>().ok();
let drawing = self.scale_manager_drawing_buf.trim().parse::<f64>().ok();
if let (false, Some(paper), Some(drawing)) = (name.is_empty(), paper, drawing) {
if paper > 0.0 && drawing > 0.0 {
let ok = if self.scale_manager_new {
self.tabs[i].scene.add_scale(&name, paper, drawing)
} else {
let old = self.scale_manager_selected.clone();
if !old.is_empty()
&& self.tabs[i].scene.edit_scale(&old, &name, paper, drawing)
{
// Follow a rename of the current annotation scale
// so its pointer doesn't dangle.
if self.tabs[i]
.scene
.document
.header
.current_annotation_scale
.eq_ignore_ascii_case(&old)
{
self.tabs[i].scene.document.header.current_annotation_scale =
name.clone();
}
true
} else if !old.is_empty()
&& self.tabs[i].scene.scale_paper_drawing(&old).is_none()
{
// Editing one of the built-in fallback scales the
// picker shows when the drawing has no scale list
// of its own — materialise it as a real scale.
self.tabs[i].scene.add_scale(&name, paper, drawing)
} else {
false
}
};
if ok {
self.scale_manager_new = false;
self.scale_manager_selected = name;
self.scale_stage_mark();
}
}
}
self.scale_stage_commit();
Task::none()
}
Message::ScaleManagerDelete => {
// Staged: reverted on close unless a later Apply commits it.
let i = self.active_tab;
let sel = self.scale_manager_selected.clone();
let cur = self.tabs[i]
@@ -1714,16 +1756,21 @@ impl OpenCADStudio {
.header
.current_annotation_scale
.clone();
if !sel.is_empty() && !sel.eq_ignore_ascii_case(&cur) {
self.push_undo_snapshot(i, "SCALELISTEDIT");
if !self.scale_manager_new && !sel.is_empty() && !sel.eq_ignore_ascii_case(&cur) {
if self.tabs[i].scene.remove_scale(&sel) {
self.scale_manager_selected.clear();
self.tabs[i].dirty = true;
self.scale_manager_name_buf.clear();
self.scale_manager_paper_buf.clear();
self.scale_manager_drawing_buf.clear();
self.scale_stage_mark();
}
}
Task::none()
}
Message::ScaleManagerSetCurrent => {
// Set Current takes effect immediately, exactly like the scale
// pill — it isn't part of the staged list transaction, so it is
// never rolled back when the manager closes.
let i = self.active_tab;
let sel = self.scale_manager_selected.clone();
if let Some((_, anno, _)) = self
@@ -1734,9 +1781,13 @@ impl OpenCADStudio {
.find(|(n, _, _)| n.eq_ignore_ascii_case(&sel))
{
self.tabs[i].scene.annotation_scale = anno;
self.tabs[i].scene.document.header.current_annotation_scale = sel;
self.tabs[i].scene.document.header.current_annotation_scale = sel.clone();
if let Some((p, d)) = self.tabs[i].scene.scale_paper_drawing(&sel) {
if d != 0.0 {
self.tabs[i].scene.document.header.annotation_scale_value = p / d;
}
}
self.tabs[i].scene.bump_geometry();
self.tabs[i].dirty = true;
}
Task::none()
}
@@ -4040,12 +4091,4 @@ impl OpenCADStudio {
}
}
/// The scale-manager editor's (name, paper?, drawing?) parsed from buffers.
fn scale_editor_values(&self) -> (String, Option<f64>, Option<f64>) {
(
self.scale_manager_name_buf.trim().to_string(),
self.scale_manager_paper_buf.trim().parse::<f64>().ok(),
self.scale_manager_drawing_buf.trim().parse::<f64>().ok(),
)
}
}
+18 -17
View File
@@ -146,28 +146,29 @@ pub fn view_window<'a, 'b>(
});
// ── Right: editor (name + paper:drawing) ──────────────────────────────
let field = |label: &'static str, value: &str, on: fn(String) -> Message| {
row![
text(label).size(11).color(DIM).width(96),
text_input("", value)
.on_input(on)
.style(field_style)
.size(12)
.padding([5, 8])
.width(Fill),
]
.align_y(iced::Center)
.spacing(6)
};
let field =
|label: &'static str, ph: &'static str, value: &str, on: fn(String) -> Message| {
row![
text(label).size(11).color(DIM).width(96),
text_input(ph, value)
.on_input(on)
.style(field_style)
.size(12)
.padding([5, 8])
.width(Fill),
]
.align_y(iced::Center)
.spacing(6)
};
let editor = container(
column![
text("Scale").size(10).color(DIM),
field("Name", name_buf, Message::ScaleManagerNameBuf),
field("Paper units", paper_buf, Message::ScaleManagerPaperBuf),
field("Drawing units", drawing_buf, Message::ScaleManagerDrawingBuf),
field("Name", "e.g. 1:50", name_buf, Message::ScaleManagerNameBuf),
field("Paper units", "1", paper_buf, Message::ScaleManagerPaperBuf),
field("Drawing units", "50", drawing_buf, Message::ScaleManagerDrawingBuf),
Space::new().height(6),
text("Type the ratio in the fields, then New to add or Apply to update the selected scale.")
text("New starts a blank scale; edit the fields and click Apply to save it. Changes are discarded if you close without Apply.")
.size(10)
.color(DIM),
]