forked from 1b-agent/opencadstudio-upstream
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:
1 parent
1071570f40
commit
cd766bc1f5
4 files changed
+220
-48
No files matched your search
@@ -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(),
|
||||
|
||||
@@ -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
@@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
]
|
||||
|
||||
Reference in new issue
Block a user