fix(annotative): make scale-manager rename and edits actually persist
Double-click rename did nothing and ratio edits were lost when switching scales — and on drawings saved by other programs nothing could be edited at all. - Fallback scales were virtual (no Scale objects), so edit/rename no-oped; materialize the standard set into real staged objects when the manager opens, reverted on close unless applied. - On third-party DWGs the mutators walked the root named-objects dict -> ACAD_SCALELIST, which can be unreachable, and matched dictionary keys that are internal ids (*A1, A0) rather than scale names. Locate the scale-list dict robustly and match scales by object name + handle. - Rename rows inline via double-click; repair a dangling current-scale header pointer on staged rollback. - Stage text/mleader style-manager edits on switch so they persist like the scale manager. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
cd766bc1f5
commit
70225de11f
8 changed files with 414 additions and 190 deletions
|
|
@ -661,12 +661,11 @@ pub(super) struct OpenCADStudio {
|
||||||
|
|
||||||
// ── Annotation-scale Manager ──────────────────────────────────────────
|
// ── Annotation-scale Manager ──────────────────────────────────────────
|
||||||
scale_manager_selected: String,
|
scale_manager_selected: String,
|
||||||
scale_manager_name_buf: String,
|
|
||||||
scale_manager_paper_buf: String,
|
scale_manager_paper_buf: String,
|
||||||
scale_manager_drawing_buf: String,
|
scale_manager_drawing_buf: String,
|
||||||
/// The editor holds an unsaved *new* scale (created only on Apply) rather
|
/// The scale row being renamed inline (double-click), + its edit buffer.
|
||||||
/// than editing the selected one. Reset when a row is selected.
|
scale_rename: Option<String>,
|
||||||
scale_manager_new: bool,
|
scale_rename_buf: String,
|
||||||
/// Open transaction for the scale manager — restored if the window closes
|
/// Open transaction for the scale manager — restored if the window closes
|
||||||
/// without Apply, mirroring the style managers' staging.
|
/// without Apply, mirroring the style managers' staging.
|
||||||
scale_stage: Option<crate::app::style_ops::ScaleStage>,
|
scale_stage: Option<crate::app::style_ops::ScaleStage>,
|
||||||
|
|
@ -1472,8 +1471,16 @@ pub enum Message {
|
||||||
ScaleManagerOpen,
|
ScaleManagerOpen,
|
||||||
/// Select a scale row in the manager (loads it into the editor).
|
/// Select a scale row in the manager (loads it into the editor).
|
||||||
ScaleManagerSelect(String),
|
ScaleManagerSelect(String),
|
||||||
/// Add a new scale from the editor fields.
|
/// Add a new scale to the list (staged) and select it for editing.
|
||||||
ScaleManagerNew,
|
ScaleManagerNew,
|
||||||
|
/// Duplicate the selected scale under a unique name (staged).
|
||||||
|
ScaleManagerCopy,
|
||||||
|
/// Begin inline rename of a scale row (double-click).
|
||||||
|
ScaleRenameStart(String),
|
||||||
|
/// Edit the inline-rename buffer.
|
||||||
|
ScaleRenameEdit(String),
|
||||||
|
/// Commit the inline rename.
|
||||||
|
ScaleRenameCommit,
|
||||||
/// Delete the selected scale.
|
/// Delete the selected scale.
|
||||||
ScaleManagerDelete,
|
ScaleManagerDelete,
|
||||||
/// Set the selected scale as the current annotation scale.
|
/// Set the selected scale as the current annotation scale.
|
||||||
|
|
@ -1481,7 +1488,6 @@ pub enum Message {
|
||||||
/// Apply the editor fields to the selected scale (rename / re-ratio).
|
/// Apply the editor fields to the selected scale (rename / re-ratio).
|
||||||
ScaleManagerApply,
|
ScaleManagerApply,
|
||||||
/// Annotation-scale manager editor field edits.
|
/// Annotation-scale manager editor field edits.
|
||||||
ScaleManagerNameBuf(String),
|
|
||||||
ScaleManagerPaperBuf(String),
|
ScaleManagerPaperBuf(String),
|
||||||
ScaleManagerDrawingBuf(String),
|
ScaleManagerDrawingBuf(String),
|
||||||
/// Toggle the leftmost hamburger's Model/layout list dropdown.
|
/// Toggle the leftmost hamburger's Model/layout list dropdown.
|
||||||
|
|
@ -2260,10 +2266,10 @@ impl OpenCADStudio {
|
||||||
// Layout Manager
|
// Layout Manager
|
||||||
layout_manager_selected: "Model".to_string(),
|
layout_manager_selected: "Model".to_string(),
|
||||||
scale_manager_selected: String::new(),
|
scale_manager_selected: String::new(),
|
||||||
scale_manager_name_buf: String::new(),
|
|
||||||
scale_manager_paper_buf: String::new(),
|
scale_manager_paper_buf: String::new(),
|
||||||
scale_manager_drawing_buf: String::new(),
|
scale_manager_drawing_buf: String::new(),
|
||||||
scale_manager_new: false,
|
scale_rename: None,
|
||||||
|
scale_rename_buf: String::new(),
|
||||||
scale_stage: None,
|
scale_stage: None,
|
||||||
layout_manager_rename_buf: String::new(),
|
layout_manager_rename_buf: String::new(),
|
||||||
plotstyle_panel_aci: 1,
|
plotstyle_panel_aci: 1,
|
||||||
|
|
|
||||||
|
|
@ -612,11 +612,14 @@ fn object_handle(doc: &acadrust::CadDocument, name: &str, kind: StyleKind) -> Op
|
||||||
// ── Annotation-scale manager staging (mirrors the style-manager stage) ──────
|
// ── Annotation-scale manager staging (mirrors the style-manager stage) ──────
|
||||||
|
|
||||||
/// Snapshot of the drawing's annotation-scale *list* a scale manager can 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
|
/// the `Scale` objects and the `ACAD_SCALELIST` dictionary. Set Current takes
|
||||||
/// deliberately excluded — Set Current takes effect immediately, like the pill.
|
/// 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 {
|
pub(super) struct ScaleSnapshot {
|
||||||
scales: Vec<(Handle, ObjectType)>,
|
scales: Vec<(Handle, ObjectType)>,
|
||||||
scalelist: Option<(Handle, ObjectType)>,
|
scalelist: Option<(Handle, ObjectType)>,
|
||||||
|
current: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An in-progress scale-manager transaction (baseline restored on discard).
|
/// An in-progress scale-manager transaction (baseline restored on discard).
|
||||||
|
|
@ -625,6 +628,10 @@ pub(super) struct ScaleStage {
|
||||||
dirty_at_open: bool,
|
dirty_at_open: bool,
|
||||||
baseline: ScaleSnapshot,
|
baseline: ScaleSnapshot,
|
||||||
changed: bool,
|
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 {
|
impl OpenCADStudio {
|
||||||
|
|
@ -637,19 +644,20 @@ impl OpenCADStudio {
|
||||||
.filter(|(_, o)| matches!(o, ObjectType::Scale(_)))
|
.filter(|(_, o)| matches!(o, ObjectType::Scale(_)))
|
||||||
.map(|(h, o)| (*h, o.clone()))
|
.map(|(h, o)| (*h, o.clone()))
|
||||||
.collect();
|
.collect();
|
||||||
let root_h = doc.header.named_objects_dict_handle;
|
let scalelist_h = self.tabs[i].scene.scalelist_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())));
|
let scalelist = scalelist_h.and_then(|h| doc.objects.get(&h).map(|o| (h, o.clone())));
|
||||||
ScaleSnapshot { scales, scalelist }
|
let current = doc.header.current_annotation_scale.clone();
|
||||||
|
ScaleSnapshot {
|
||||||
|
scales,
|
||||||
|
scalelist,
|
||||||
|
current,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn restore_scale_state(&mut self, snap: &ScaleSnapshot) {
|
fn restore_scale_state(&mut self, snap: &ScaleSnapshot) {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let root_h = self.tabs[i].scene.document.header.named_objects_dict_handle;
|
let root_h = self.tabs[i].scene.document.header.named_objects_dict_handle;
|
||||||
let cur_scalelist_h =
|
let cur_scalelist_h = self.tabs[i].scene.scalelist_dict_handle();
|
||||||
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;
|
let doc = &mut self.tabs[i].scene.document;
|
||||||
doc.objects.retain(|_, o| !matches!(o, ObjectType::Scale(_)));
|
doc.objects.retain(|_, o| !matches!(o, ObjectType::Scale(_)));
|
||||||
if let Some(h) = cur_scalelist_h {
|
if let Some(h) = cur_scalelist_h {
|
||||||
|
|
@ -670,6 +678,24 @@ impl OpenCADStudio {
|
||||||
root.entries.push(("ACAD_SCALELIST".to_string(), 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.bump_geometry();
|
self.tabs[i].scene.bump_geometry();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -681,6 +707,7 @@ impl OpenCADStudio {
|
||||||
dirty_at_open,
|
dirty_at_open,
|
||||||
baseline,
|
baseline,
|
||||||
changed: false,
|
changed: false,
|
||||||
|
materialized: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -692,6 +719,14 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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
|
/// 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
|
/// re-baseline so the still-open window keeps staging afresh. A no-op Apply
|
||||||
/// (nothing staged) does nothing.
|
/// (nothing staged) does nothing.
|
||||||
|
|
@ -713,6 +748,9 @@ impl OpenCADStudio {
|
||||||
dirty_at_open: true,
|
dirty_at_open: true,
|
||||||
baseline: edited,
|
baseline: edited,
|
||||||
changed: false,
|
changed: false,
|
||||||
|
// The materialised set is now committed as the new baseline, so it
|
||||||
|
// no longer needs reverting on close.
|
||||||
|
materialized: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -722,7 +760,9 @@ impl OpenCADStudio {
|
||||||
let Some(stage) = self.scale_stage.take() else {
|
let Some(stage) = self.scale_stage.take() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if stage.changed {
|
// 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.restore_scale_state(&stage.baseline);
|
||||||
self.tabs[self.active_tab].dirty = stage.dirty_at_open;
|
self.tabs[self.active_tab].dirty = stage.dirty_at_open;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1653,29 +1653,42 @@ impl OpenCADStudio {
|
||||||
Message::ScaleManagerOpen => {
|
Message::ScaleManagerOpen => {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
self.scale_popup_open = false;
|
self.scale_popup_open = false;
|
||||||
// Snapshot so New / Delete / edits revert if closed without Apply.
|
// Snapshot so New / Copy / Delete / edits revert if closed
|
||||||
|
// without Apply.
|
||||||
self.scale_stage_begin();
|
self.scale_stage_begin();
|
||||||
// Open on the current scale, ready to edit (not a new draft).
|
// Fallback scales are virtual (no real objects), so they can't
|
||||||
self.scale_manager_new = false;
|
// be edited or renamed. Materialise the standard set into real
|
||||||
|
// staged objects so the manager behaves like a drawing with its
|
||||||
|
// own list; the stage reverts them on close unless applied.
|
||||||
|
if self.tabs[i].scene.ensure_real_scale_list() {
|
||||||
|
self.scale_stage_materialized();
|
||||||
|
}
|
||||||
|
self.scale_rename = None;
|
||||||
let cur = self.tabs[i]
|
let cur = self.tabs[i]
|
||||||
.scene
|
.scene
|
||||||
.document
|
.document
|
||||||
.header
|
.header
|
||||||
.current_annotation_scale
|
.current_annotation_scale
|
||||||
.clone();
|
.clone();
|
||||||
self.load_scale_editor(&cur);
|
// Select the current scale, or the first one if it isn't listed.
|
||||||
|
if self.tabs[i].scene.scale_paper_drawing(&cur).is_some() {
|
||||||
|
self.load_scale_editor(&cur);
|
||||||
|
} else if let Some((first, _, _)) =
|
||||||
|
self.tabs[i].scene.scale_list().into_iter().next()
|
||||||
|
{
|
||||||
|
self.load_scale_editor(&first);
|
||||||
|
}
|
||||||
self.active_modal = Some(crate::app::ModalKind::ScaleManager);
|
self.active_modal = Some(crate::app::ModalKind::ScaleManager);
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::ScaleManagerSelect(name) => {
|
Message::ScaleManagerSelect(name) => {
|
||||||
self.scale_manager_new = false;
|
// Stage the current editor edits before switching so they aren't
|
||||||
|
// lost, then load the newly-selected scale.
|
||||||
|
self.scale_rename = None;
|
||||||
|
self.scale_apply_current();
|
||||||
self.load_scale_editor(&name);
|
self.load_scale_editor(&name);
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::ScaleManagerNameBuf(s) => {
|
|
||||||
self.scale_manager_name_buf = s;
|
|
||||||
Task::none()
|
|
||||||
}
|
|
||||||
Message::ScaleManagerPaperBuf(s) => {
|
Message::ScaleManagerPaperBuf(s) => {
|
||||||
self.scale_manager_paper_buf = s;
|
self.scale_manager_paper_buf = s;
|
||||||
Task::none()
|
Task::none()
|
||||||
|
|
@ -1685,64 +1698,83 @@ impl OpenCADStudio {
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::ScaleManagerNew => {
|
Message::ScaleManagerNew => {
|
||||||
// Start a blank draft — nothing is added to the drawing until
|
// Add a new scale to the list immediately (staged) and select it,
|
||||||
// Apply; closing the manager without Apply discards it.
|
// like the style managers' New. The user edits its name / ratio;
|
||||||
self.scale_manager_new = true;
|
// it's kept only if Apply is pressed before the window closes.
|
||||||
self.scale_manager_selected.clear();
|
self.scale_apply_current();
|
||||||
self.scale_manager_name_buf.clear();
|
let i = self.active_tab;
|
||||||
self.scale_manager_paper_buf = "1".to_string();
|
let name = self.unique_scale_name("New Scale");
|
||||||
self.scale_manager_drawing_buf = "1".to_string();
|
if self.tabs[i].scene.add_scale(&name, 1.0, 1.0) {
|
||||||
|
self.load_scale_editor(&name);
|
||||||
|
self.scale_stage_mark();
|
||||||
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::ScaleManagerApply => {
|
Message::ScaleManagerCopy => {
|
||||||
// Fold the editor's add / edit into the staged document, then
|
// Duplicate the selected scale under a unique name (staged).
|
||||||
// commit the whole transaction (this change plus any staged
|
self.scale_apply_current();
|
||||||
// deletes) as one undo entry. A new draft is added; otherwise
|
|
||||||
// the selected scale is renamed / re-ratioed.
|
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let name = self.scale_manager_name_buf.trim().to_string();
|
let sel = self.scale_manager_selected.clone();
|
||||||
let paper = self.scale_manager_paper_buf.trim().parse::<f64>().ok();
|
if !sel.is_empty() {
|
||||||
let drawing = self.scale_manager_drawing_buf.trim().parse::<f64>().ok();
|
let (paper, drawing) =
|
||||||
if let (false, Some(paper), Some(drawing)) = (name.is_empty(), paper, drawing) {
|
self.tabs[i].scene.scale_paper_drawing(&sel).unwrap_or((1.0, 1.0));
|
||||||
if paper > 0.0 && drawing > 0.0 {
|
let name = self.unique_scale_name(&sel);
|
||||||
let ok = if self.scale_manager_new {
|
if self.tabs[i].scene.add_scale(&name, paper, drawing) {
|
||||||
self.tabs[i].scene.add_scale(&name, paper, drawing)
|
self.load_scale_editor(&name);
|
||||||
} else {
|
self.scale_stage_mark();
|
||||||
let old = self.scale_manager_selected.clone();
|
}
|
||||||
if !old.is_empty()
|
}
|
||||||
&& self.tabs[i].scene.edit_scale(&old, &name, paper, drawing)
|
Task::none()
|
||||||
{
|
}
|
||||||
// Follow a rename of the current annotation scale
|
Message::ScaleRenameStart(name) => {
|
||||||
// so its pointer doesn't dangle.
|
// Stage current editor edits, then rename this row inline.
|
||||||
if self.tabs[i]
|
self.scale_apply_current();
|
||||||
.scene
|
self.scale_rename_buf = name.clone();
|
||||||
.document
|
self.scale_rename = Some(name);
|
||||||
.header
|
iced::widget::operation::focus(crate::ui::style::scale_manager::rename_input_id())
|
||||||
.current_annotation_scale
|
}
|
||||||
.eq_ignore_ascii_case(&old)
|
Message::ScaleRenameEdit(s) => {
|
||||||
{
|
self.scale_rename_buf = s;
|
||||||
self.tabs[i].scene.document.header.current_annotation_scale =
|
Task::none()
|
||||||
name.clone();
|
}
|
||||||
}
|
Message::ScaleRenameCommit => {
|
||||||
true
|
let i = self.active_tab;
|
||||||
} else if !old.is_empty()
|
if let Some(old) = self.scale_rename.take() {
|
||||||
&& self.tabs[i].scene.scale_paper_drawing(&old).is_none()
|
let new = self.scale_rename_buf.trim().to_string();
|
||||||
{
|
if !new.is_empty() && !new.eq_ignore_ascii_case(&old) {
|
||||||
// Editing one of the built-in fallback scales the
|
let (paper, drawing) =
|
||||||
// picker shows when the drawing has no scale list
|
self.tabs[i].scene.scale_paper_drawing(&old).unwrap_or((1.0, 1.0));
|
||||||
// of its own — materialise it as a real scale.
|
// Only fall back to add_scale for a built-in fallback (no
|
||||||
self.tabs[i].scene.add_scale(&name, paper, drawing)
|
// real object); never for a real scale whose rename was
|
||||||
} else {
|
// rejected (name collision) — that would duplicate it.
|
||||||
false
|
let ok = self.tabs[i].scene.edit_scale(&old, &new, paper, drawing)
|
||||||
}
|
|| (self.tabs[i].scene.scale_paper_drawing(&old).is_none()
|
||||||
};
|
&& self.tabs[i].scene.add_scale(&new, paper, drawing));
|
||||||
if ok {
|
if ok {
|
||||||
self.scale_manager_new = false;
|
if self.tabs[i]
|
||||||
self.scale_manager_selected = name;
|
.scene
|
||||||
|
.document
|
||||||
|
.header
|
||||||
|
.current_annotation_scale
|
||||||
|
.eq_ignore_ascii_case(&old)
|
||||||
|
{
|
||||||
|
self.tabs[i].scene.document.header.current_annotation_scale =
|
||||||
|
new.clone();
|
||||||
|
}
|
||||||
|
if self.scale_manager_selected.eq_ignore_ascii_case(&old) {
|
||||||
|
self.load_scale_editor(&new);
|
||||||
|
}
|
||||||
self.scale_stage_mark();
|
self.scale_stage_mark();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::ScaleManagerApply => {
|
||||||
|
// Fold the editor into the selected scale, then commit the staged
|
||||||
|
// transaction (this edit plus any New / Copy / Delete since open)
|
||||||
|
// as one undo entry.
|
||||||
|
self.scale_apply_current();
|
||||||
self.scale_stage_commit();
|
self.scale_stage_commit();
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
@ -1756,10 +1788,9 @@ impl OpenCADStudio {
|
||||||
.header
|
.header
|
||||||
.current_annotation_scale
|
.current_annotation_scale
|
||||||
.clone();
|
.clone();
|
||||||
if !self.scale_manager_new && !sel.is_empty() && !sel.eq_ignore_ascii_case(&cur) {
|
if !sel.is_empty() && !sel.eq_ignore_ascii_case(&cur) {
|
||||||
if self.tabs[i].scene.remove_scale(&sel) {
|
if self.tabs[i].scene.remove_scale(&sel) {
|
||||||
self.scale_manager_selected.clear();
|
self.scale_manager_selected.clear();
|
||||||
self.scale_manager_name_buf.clear();
|
|
||||||
self.scale_manager_paper_buf.clear();
|
self.scale_manager_paper_buf.clear();
|
||||||
self.scale_manager_drawing_buf.clear();
|
self.scale_manager_drawing_buf.clear();
|
||||||
self.scale_stage_mark();
|
self.scale_stage_mark();
|
||||||
|
|
@ -3542,6 +3573,7 @@ impl OpenCADStudio {
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::TextStyleDialogSelect(name) => {
|
Message::TextStyleDialogSelect(name) => {
|
||||||
|
self.stage_textstyle_bufs();
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
self.textstyle_selected = name;
|
self.textstyle_selected = name;
|
||||||
self.load_textstyle_bufs(i);
|
self.load_textstyle_bufs(i);
|
||||||
|
|
@ -3655,6 +3687,7 @@ impl OpenCADStudio {
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::TableStyleDialogSelect(name) => {
|
Message::TableStyleDialogSelect(name) => {
|
||||||
|
self.stage_tablestyle_bufs();
|
||||||
self.tablestyle_selected = name;
|
self.tablestyle_selected = name;
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
self.load_tablestyle_bufs(i);
|
self.load_tablestyle_bufs(i);
|
||||||
|
|
@ -3672,25 +3705,7 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::TableStyleApply => {
|
Message::TableStyleApply => {
|
||||||
use acadrust::objects::ObjectType;
|
self.stage_tablestyle_bufs();
|
||||||
let i = self.active_tab;
|
|
||||||
let name = self.tablestyle_selected.clone();
|
|
||||||
let h: Option<f64> = self.ts_hmargin.trim().parse().ok();
|
|
||||||
let v: Option<f64> = self.ts_vmargin.trim().parse().ok();
|
|
||||||
let desc = self.ts_description.clone();
|
|
||||||
for obj in self.tabs[i].scene.document.objects.values_mut() {
|
|
||||||
if let ObjectType::TableStyle(s) = obj {
|
|
||||||
if s.name == name {
|
|
||||||
if let Some(h) = h {
|
|
||||||
s.horizontal_margin = h;
|
|
||||||
}
|
|
||||||
if let Some(v) = v {
|
|
||||||
s.vertical_margin = v;
|
|
||||||
}
|
|
||||||
s.description = desc.clone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.style_stage_commit();
|
self.style_stage_commit();
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
@ -3935,6 +3950,7 @@ impl OpenCADStudio {
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::MLeaderStyleDialogSelect(name) => {
|
Message::MLeaderStyleDialogSelect(name) => {
|
||||||
|
self.stage_mleaderstyle_bufs();
|
||||||
self.mleaderstyle_selected = name;
|
self.mleaderstyle_selected = name;
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
self.load_mleaderstyle_bufs(i);
|
self.load_mleaderstyle_bufs(i);
|
||||||
|
|
@ -4002,6 +4018,8 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
Message::DimStyleDialogSelect(name) => {
|
Message::DimStyleDialogSelect(name) => {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
|
// Stage the current edits before switching so they aren't lost.
|
||||||
|
self.apply_dimstyle_bufs(i);
|
||||||
self.dimstyle_selected = name;
|
self.dimstyle_selected = name;
|
||||||
self.load_dimstyle_bufs(i);
|
self.load_dimstyle_bufs(i);
|
||||||
Task::none()
|
Task::none()
|
||||||
|
|
@ -4078,7 +4096,6 @@ impl OpenCADStudio {
|
||||||
fn load_scale_editor(&mut self, name: &str) {
|
fn load_scale_editor(&mut self, name: &str) {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
self.scale_manager_selected = name.to_string();
|
self.scale_manager_selected = name.to_string();
|
||||||
self.scale_manager_name_buf = name.to_string();
|
|
||||||
match self.tabs[i].scene.scale_paper_drawing(name) {
|
match self.tabs[i].scene.scale_paper_drawing(name) {
|
||||||
Some((p, d)) => {
|
Some((p, d)) => {
|
||||||
self.scale_manager_paper_buf = format!("{p}");
|
self.scale_manager_paper_buf = format!("{p}");
|
||||||
|
|
@ -4091,4 +4108,81 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fold the editor's paper:drawing ratio into the selected scale, keeping
|
||||||
|
/// its name (renaming is done inline in the list). Staged, no commit — so
|
||||||
|
/// the ratio edit survives Apply *and* switching to another row. Editing a
|
||||||
|
/// built-in fallback scale materialises it as a real one.
|
||||||
|
fn scale_apply_current(&mut self) {
|
||||||
|
let i = self.active_tab;
|
||||||
|
let sel = self.scale_manager_selected.clone();
|
||||||
|
if sel.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let paper = self.scale_manager_paper_buf.trim().parse::<f64>().ok();
|
||||||
|
let drawing = self.scale_manager_drawing_buf.trim().parse::<f64>().ok();
|
||||||
|
if let (Some(paper), Some(drawing)) = (paper, drawing) {
|
||||||
|
if paper > 0.0 && drawing > 0.0 {
|
||||||
|
// Skip when the editor still holds the stored ratio, so merely
|
||||||
|
// navigating between scales doesn't dirty the drawing.
|
||||||
|
if let Some((cp, cd)) = self.tabs[i].scene.scale_paper_drawing(&sel) {
|
||||||
|
if (cp - paper).abs() < 1e-9 && (cd - drawing).abs() < 1e-9 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let changed = self.tabs[i].scene.edit_scale(&sel, &sel, paper, drawing)
|
||||||
|
|| (self.tabs[i].scene.scale_paper_drawing(&sel).is_none()
|
||||||
|
&& self.tabs[i].scene.add_scale(&sel, paper, drawing));
|
||||||
|
if changed {
|
||||||
|
self.scale_stage_mark();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write the table-style editor buffers (margins / description) into the
|
||||||
|
/// selected style (staged, no commit), so edits survive switching as well
|
||||||
|
/// as Apply.
|
||||||
|
fn stage_tablestyle_bufs(&mut self) {
|
||||||
|
use acadrust::objects::ObjectType;
|
||||||
|
let i = self.active_tab;
|
||||||
|
let name = self.tablestyle_selected.clone();
|
||||||
|
let h: Option<f64> = self.ts_hmargin.trim().parse().ok();
|
||||||
|
let v: Option<f64> = self.ts_vmargin.trim().parse().ok();
|
||||||
|
let desc = self.ts_description.clone();
|
||||||
|
for obj in self.tabs[i].scene.document.objects.values_mut() {
|
||||||
|
if let ObjectType::TableStyle(s) = obj {
|
||||||
|
if s.name == name {
|
||||||
|
if let Some(h) = h {
|
||||||
|
s.horizontal_margin = h;
|
||||||
|
}
|
||||||
|
if let Some(v) = v {
|
||||||
|
s.vertical_margin = v;
|
||||||
|
}
|
||||||
|
s.description = desc.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A scale name based on `base`, suffixed " (n)" until it's unique in the
|
||||||
|
/// drawing's scale list (used by New / Copy).
|
||||||
|
fn unique_scale_name(&self, base: &str) -> String {
|
||||||
|
let existing: std::collections::HashSet<String> = self.tabs[self.active_tab]
|
||||||
|
.scene
|
||||||
|
.scale_list()
|
||||||
|
.into_iter()
|
||||||
|
.map(|(n, _, _)| n.to_ascii_lowercase())
|
||||||
|
.collect();
|
||||||
|
if !existing.contains(&base.to_ascii_lowercase()) {
|
||||||
|
return base.to_string();
|
||||||
|
}
|
||||||
|
let mut n = 2;
|
||||||
|
loop {
|
||||||
|
let candidate = format!("{base} ({n})");
|
||||||
|
if !existing.contains(&candidate.to_ascii_lowercase()) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
n += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -497,7 +497,9 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task<Message> {
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn on_text_style_apply(&mut self) -> Task<Message> {
|
/// Write the editor buffers into the selected text style (staged, no
|
||||||
|
/// commit), so edits survive switching to another style as well as Apply.
|
||||||
|
pub(super) fn stage_textstyle_bufs(&mut self) {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let name = self.textstyle_selected.clone();
|
let name = self.textstyle_selected.clone();
|
||||||
let font = self.textstyle_font.clone();
|
let font = self.textstyle_font.clone();
|
||||||
|
|
@ -520,6 +522,10 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task<Message> {
|
||||||
s.height = h.max(0.0);
|
s.height = h.max(0.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn on_text_style_apply(&mut self) -> Task<Message> {
|
||||||
|
self.stage_textstyle_bufs();
|
||||||
self.style_stage_commit();
|
self.style_stage_commit();
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
@ -838,7 +844,9 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task<Message> {
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn on_mleader_style_apply(&mut self) -> Task<Message> {
|
/// Write the editor buffers into the selected multileader style (staged, no
|
||||||
|
/// commit), so edits survive switching to another style as well as Apply.
|
||||||
|
pub(super) fn stage_mleaderstyle_bufs(&mut self) {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let (ld, lg, asz, th, sf, bg, fsa, ssa, mp, dt, lc, tc) = (
|
let (ld, lg, asz, th, sf, bg, fsa, ssa, mp, dt, lc, tc) = (
|
||||||
self.mls_landing_distance.parse::<f64>().ok(),
|
self.mls_landing_distance.parse::<f64>().ok(),
|
||||||
|
|
@ -920,6 +928,10 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task<Message> {
|
||||||
s.block_content_scale_z = v;
|
s.block_content_scale_z = v;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn on_mleader_style_apply(&mut self) -> Task<Message> {
|
||||||
|
self.stage_mleaderstyle_bufs();
|
||||||
self.style_stage_commit();
|
self.style_stage_commit();
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,8 @@ impl OpenCADStudio {
|
||||||
&scales,
|
&scales,
|
||||||
&self.scale_manager_selected,
|
&self.scale_manager_selected,
|
||||||
¤t,
|
¤t,
|
||||||
&self.scale_manager_name_buf,
|
self.scale_rename.as_deref(),
|
||||||
|
&self.scale_rename_buf,
|
||||||
&self.scale_manager_paper_buf,
|
&self.scale_manager_paper_buf,
|
||||||
&self.scale_manager_drawing_buf,
|
&self.scale_manager_drawing_buf,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
188
src/scene/mod.rs
188
src/scene/mod.rs
|
|
@ -1825,6 +1825,59 @@ impl Scene {
|
||||||
/// ratio (0.02 for "1:50"). Sorted smallest ratio first (1:100 … 1:1 …
|
/// ratio (0.02 for "1:50"). Sorted smallest ratio first (1:100 … 1:1 …
|
||||||
/// 10:1). Falls back to a standard ratio set when the drawing carries no
|
/// 10:1). Falls back to a standard ratio set when the drawing carries no
|
||||||
/// scale list of its own, so the scale picker is always usable. (#154)
|
/// scale list of its own, so the scale picker is always usable. (#154)
|
||||||
|
/// Standard fallback ratio set as (label, paper/drawing factor). Shown when
|
||||||
|
/// the drawing defines no annotation scales of its own, and materialised as
|
||||||
|
/// real `Scale` objects on demand by [`Scene::ensure_real_scale_list`].
|
||||||
|
const DEFAULT_SCALES: &'static [(&'static str, f64)] = &[
|
||||||
|
("1:500", 0.002),
|
||||||
|
("1:200", 0.005),
|
||||||
|
("1:100", 0.01),
|
||||||
|
("1:50", 0.02),
|
||||||
|
("1:20", 0.05),
|
||||||
|
("1:10", 0.1),
|
||||||
|
("1:5", 0.2),
|
||||||
|
("1:2", 0.5),
|
||||||
|
("1:1", 1.0),
|
||||||
|
("2:1", 2.0),
|
||||||
|
("5:1", 5.0),
|
||||||
|
("10:1", 10.0),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// True when the drawing owns at least one annotation scale of its own
|
||||||
|
/// (i.e. `scale_list` returns real objects rather than the fallback set).
|
||||||
|
fn has_own_scales(&self) -> bool {
|
||||||
|
self.document.objects.values().any(|o| {
|
||||||
|
matches!(o, ObjectType::Scale(s)
|
||||||
|
if !s.is_temporary
|
||||||
|
&& !s.name.contains('|')
|
||||||
|
&& !s.name.to_ascii_uppercase().ends_with("_XREF"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If the drawing has no annotation scales of its own, populate it with the
|
||||||
|
/// standard fallback set as real `Scale` objects (and the `ACAD_SCALELIST`
|
||||||
|
/// dictionary) so they can be selected, edited and renamed uniformly with a
|
||||||
|
/// drawing that ships its own list. Returns `true` if any were created.
|
||||||
|
///
|
||||||
|
/// The scale manager calls this on open and stages the result, so the set
|
||||||
|
/// is discarded again unless the user applies an edit.
|
||||||
|
pub fn ensure_real_scale_list(&mut self) -> bool {
|
||||||
|
if self.has_own_scales() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for &(label, factor) in Self::DEFAULT_SCALES {
|
||||||
|
// Fallback labels are "paper:drawing"; parse them for tidy whole
|
||||||
|
// units, else derive drawing units from the factor (paper = 1).
|
||||||
|
let (paper, drawing) = label
|
||||||
|
.split_once(':')
|
||||||
|
.and_then(|(p, d)| Some((p.trim().parse::<f64>().ok()?, d.trim().parse::<f64>().ok()?)))
|
||||||
|
.filter(|&(p, d)| p > 0.0 && d > 0.0)
|
||||||
|
.unwrap_or((1.0, 1.0 / factor));
|
||||||
|
self.add_scale(label, paper, drawing);
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
pub fn scale_list(&self) -> Vec<(String, f32, f64)> {
|
pub fn scale_list(&self) -> Vec<(String, f32, f64)> {
|
||||||
let mut list: Vec<(String, f32, f64)> = self
|
let mut list: Vec<(String, f32, f64)> = self
|
||||||
.document
|
.document
|
||||||
|
|
@ -1852,21 +1905,7 @@ impl Scene {
|
||||||
// annotation / viewport scale picker would be empty and so appear
|
// annotation / viewport scale picker would be empty and so appear
|
||||||
// broken. Substitute the standard ratio set — file scales still
|
// broken. Substitute the standard ratio set — file scales still
|
||||||
// win whenever the drawing actually defines any. (#154)
|
// win whenever the drawing actually defines any. (#154)
|
||||||
const DEFAULT_SCALES: &[(&str, f64)] = &[
|
list = Self::DEFAULT_SCALES
|
||||||
("1:500", 0.002),
|
|
||||||
("1:200", 0.005),
|
|
||||||
("1:100", 0.01),
|
|
||||||
("1:50", 0.02),
|
|
||||||
("1:20", 0.05),
|
|
||||||
("1:10", 0.1),
|
|
||||||
("1:5", 0.2),
|
|
||||||
("1:2", 0.5),
|
|
||||||
("1:1", 1.0),
|
|
||||||
("2:1", 2.0),
|
|
||||||
("5:1", 5.0),
|
|
||||||
("10:1", 10.0),
|
|
||||||
];
|
|
||||||
list = DEFAULT_SCALES
|
|
||||||
.iter()
|
.iter()
|
||||||
.map(|&(label, vp)| (label.to_string(), (1.0 / vp) as f32, vp))
|
.map(|&(label, vp)| (label.to_string(), (1.0 / vp) as f32, vp))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
@ -1874,18 +1913,61 @@ impl Scene {
|
||||||
list
|
list
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handle of the drawing's `ACAD_SCALELIST` dictionary, located robustly.
|
||||||
|
///
|
||||||
|
/// The canonical path is the root named-objects dict → `"ACAD_SCALELIST"`,
|
||||||
|
/// but DWGs written by other programs don't always expose a resolvable root
|
||||||
|
/// dict (the header handle points at no loaded object). In that case fall
|
||||||
|
/// back to the owner dictionary shared by the drawing's `Scale` objects.
|
||||||
|
/// Returns `None` only when the drawing genuinely has no scale list.
|
||||||
|
pub(crate) fn scalelist_dict_handle(&self) -> Option<Handle> {
|
||||||
|
use acadrust::objects::ObjectType;
|
||||||
|
let root_h = self.document.header.named_objects_dict_handle;
|
||||||
|
if let Some(h) = crate::scene::annotative::as_dict(&self.document, root_h)
|
||||||
|
.and_then(|d| d.get("ACAD_SCALELIST"))
|
||||||
|
{
|
||||||
|
return Some(h);
|
||||||
|
}
|
||||||
|
let owner = self.document.objects.values().find_map(|o| match o {
|
||||||
|
ObjectType::Scale(s) if !s.is_temporary => Some(s.owner_handle),
|
||||||
|
_ => None,
|
||||||
|
})?;
|
||||||
|
matches!(
|
||||||
|
self.document.objects.get(&owner),
|
||||||
|
Some(ObjectType::Dictionary(_))
|
||||||
|
)
|
||||||
|
.then_some(owner)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle of the real (non-temporary) `Scale` object with this display name.
|
||||||
|
/// Scales are matched by their `name` field, not the dictionary key — some
|
||||||
|
/// files key the entries by an internal identifier (`*A1`, `A0`, …) that
|
||||||
|
/// bears no relation to the scale name.
|
||||||
|
fn scale_object_handle(&self, name: &str) -> Option<Handle> {
|
||||||
|
use acadrust::objects::ObjectType;
|
||||||
|
self.document.objects.iter().find_map(|(h, o)| match o {
|
||||||
|
ObjectType::Scale(s) if !s.is_temporary && s.name.eq_ignore_ascii_case(name) => {
|
||||||
|
Some(*h)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Add a named annotation scale to the drawing's `ACAD_SCALELIST`. Returns
|
/// Add a named annotation scale to the drawing's `ACAD_SCALELIST`. Returns
|
||||||
/// `false` if a scale with that name already exists. Creates the SCALELIST
|
/// `false` if a scale with that name already exists. Creates the SCALELIST
|
||||||
/// dictionary (and registers it in the root dictionary) when the drawing
|
/// dictionary (and registers it in the root dictionary) when the drawing
|
||||||
/// has none of its own — many minimal files carry no scale list at all.
|
/// has none of its own — many minimal files carry no scale list at all.
|
||||||
pub fn add_scale(&mut self, name: &str, paper: f64, drawing: f64) -> bool {
|
pub fn add_scale(&mut self, name: &str, paper: f64, drawing: f64) -> bool {
|
||||||
use acadrust::objects::{Dictionary, ObjectType, Scale};
|
use acadrust::objects::{Dictionary, ObjectType, Scale};
|
||||||
let root_h = self.document.header.named_objects_dict_handle;
|
// Reject a duplicate by the scale's display name (the dictionary key may
|
||||||
let scalelist_h = match crate::scene::annotative::as_dict(&self.document, root_h)
|
// be an unrelated internal identifier, so check the objects directly).
|
||||||
.and_then(|d| d.get("ACAD_SCALELIST"))
|
if self.scale_object_handle(name).is_some() {
|
||||||
{
|
return false;
|
||||||
|
}
|
||||||
|
let scalelist_h = match self.scalelist_dict_handle() {
|
||||||
Some(h) => h,
|
Some(h) => h,
|
||||||
None => {
|
None => {
|
||||||
|
let root_h = self.document.header.named_objects_dict_handle;
|
||||||
let h = self.document.allocate_handle();
|
let h = self.document.allocate_handle();
|
||||||
let mut d = Dictionary::new();
|
let mut d = Dictionary::new();
|
||||||
d.handle = h;
|
d.handle = h;
|
||||||
|
|
@ -1899,12 +1981,6 @@ impl Scene {
|
||||||
h
|
h
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Reject a duplicate name (case-insensitive).
|
|
||||||
if crate::scene::annotative::as_dict(&self.document, scalelist_h)
|
|
||||||
.is_some_and(|d| d.entries.iter().any(|(n, _)| n.eq_ignore_ascii_case(name)))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let sh = self.document.allocate_handle();
|
let sh = self.document.allocate_handle();
|
||||||
let mut s = Scale::new(name, paper, drawing);
|
let mut s = Scale::new(name, paper, drawing);
|
||||||
s.handle = sh;
|
s.handle = sh;
|
||||||
|
|
@ -1922,25 +1998,18 @@ impl Scene {
|
||||||
/// — the caller guards that.
|
/// — the caller guards that.
|
||||||
pub fn remove_scale(&mut self, name: &str) -> bool {
|
pub fn remove_scale(&mut self, name: &str) -> bool {
|
||||||
use acadrust::objects::ObjectType;
|
use acadrust::objects::ObjectType;
|
||||||
let root_h = self.document.header.named_objects_dict_handle;
|
let Some(sh) = self.scale_object_handle(name) else {
|
||||||
let Some(scalelist_h) = crate::scene::annotative::as_dict(&self.document, root_h)
|
|
||||||
.and_then(|d| d.get("ACAD_SCALELIST"))
|
|
||||||
else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
let Some(sh) = crate::scene::annotative::as_dict(&self.document, scalelist_h).and_then(
|
|
||||||
|d| {
|
|
||||||
d.entries
|
|
||||||
.iter()
|
|
||||||
.find(|(n, _)| n.eq_ignore_ascii_case(name))
|
|
||||||
.map(|(_, h)| *h)
|
|
||||||
},
|
|
||||||
) else {
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
// Resolve the dictionary before removing the object (the fallback lookup
|
||||||
|
// reads a surviving scale's owner).
|
||||||
|
let dict_h = self.scalelist_dict_handle();
|
||||||
self.document.objects.remove(&sh);
|
self.document.objects.remove(&sh);
|
||||||
if let Some(ObjectType::Dictionary(sl)) = self.document.objects.get_mut(&scalelist_h) {
|
if let Some(dict_h) = dict_h {
|
||||||
sl.entries.retain(|(n, _)| !n.eq_ignore_ascii_case(name));
|
if let Some(ObjectType::Dictionary(sl)) = self.document.objects.get_mut(&dict_h) {
|
||||||
|
// Match by handle — the entry key is not necessarily the name.
|
||||||
|
sl.entries.retain(|(_, h)| *h != sh);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
@ -1949,28 +2018,16 @@ impl Scene {
|
||||||
/// if the scale is missing or the new name collides with another scale.
|
/// if the scale is missing or the new name collides with another scale.
|
||||||
pub fn edit_scale(&mut self, old_name: &str, new_name: &str, paper: f64, drawing: f64) -> bool {
|
pub fn edit_scale(&mut self, old_name: &str, new_name: &str, paper: f64, drawing: f64) -> bool {
|
||||||
use acadrust::objects::ObjectType;
|
use acadrust::objects::ObjectType;
|
||||||
let root_h = self.document.header.named_objects_dict_handle;
|
let Some(sh) = self.scale_object_handle(old_name) else {
|
||||||
let Some(scalelist_h) = crate::scene::annotative::as_dict(&self.document, root_h)
|
|
||||||
.and_then(|d| d.get("ACAD_SCALELIST"))
|
|
||||||
else {
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
let Some(sh) = crate::scene::annotative::as_dict(&self.document, scalelist_h).and_then(
|
// A new name that collides with a *different* scale object is rejected.
|
||||||
|d| {
|
if !new_name.eq_ignore_ascii_case(old_name) {
|
||||||
d.entries
|
if let Some(other) = self.scale_object_handle(new_name) {
|
||||||
.iter()
|
if other != sh {
|
||||||
.find(|(n, _)| n.eq_ignore_ascii_case(old_name))
|
return false;
|
||||||
.map(|(_, h)| *h)
|
}
|
||||||
},
|
}
|
||||||
) else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
// A new name that collides with a *different* scale is rejected.
|
|
||||||
if !new_name.eq_ignore_ascii_case(old_name)
|
|
||||||
&& crate::scene::annotative::as_dict(&self.document, scalelist_h)
|
|
||||||
.is_some_and(|d| d.entries.iter().any(|(n, _)| n.eq_ignore_ascii_case(new_name)))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
if let Some(ObjectType::Scale(s)) = self.document.objects.get_mut(&sh) {
|
if let Some(ObjectType::Scale(s)) = self.document.objects.get_mut(&sh) {
|
||||||
s.name = new_name.to_string();
|
s.name = new_name.to_string();
|
||||||
|
|
@ -1978,9 +2035,12 @@ impl Scene {
|
||||||
s.drawing_units = drawing;
|
s.drawing_units = drawing;
|
||||||
s.is_unit_scale = (paper - drawing).abs() < 1e-10;
|
s.is_unit_scale = (paper - drawing).abs() < 1e-10;
|
||||||
}
|
}
|
||||||
if let Some(ObjectType::Dictionary(sl)) = self.document.objects.get_mut(&scalelist_h) {
|
// Keep the owning dictionary's entry key in sync, matched by handle.
|
||||||
if let Some(e) = sl.entries.iter_mut().find(|(n, _)| n.eq_ignore_ascii_case(old_name)) {
|
if let Some(dict_h) = self.scalelist_dict_handle() {
|
||||||
e.0 = new_name.to_string();
|
if let Some(ObjectType::Dictionary(sl)) = self.document.objects.get_mut(&dict_h) {
|
||||||
|
if let Some(e) = sl.entries.iter_mut().find(|(_, h)| *h == sh) {
|
||||||
|
e.0 = new_name.to_string();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ fn scale_row(label: String, active: bool, msg: Message) -> Element<'static, Mess
|
||||||
}
|
}
|
||||||
|
|
||||||
fn manage_row() -> Element<'static, Message> {
|
fn manage_row() -> Element<'static, Message> {
|
||||||
button(text("Manage scales...").size(11).color(CHECK_COLOR))
|
button(text("Manage...").size(11).color(CHECK_COLOR))
|
||||||
.on_press(Message::ScaleManagerOpen)
|
.on_press(Message::ScaleManagerOpen)
|
||||||
.style(|_: &Theme, status| button::Style {
|
.style(|_: &Theme, status| button::Style {
|
||||||
background: Some(Background::Color(match status {
|
background: Some(Background::Color(match status {
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,16 @@
|
||||||
|
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
use crate::ui::style::style_manager::{hdivider, tb_button, vsep, BG, BORDER, DIM, LIST, TB, TEXT};
|
use crate::ui::style::style_manager::{hdivider, tb_button, vsep, BG, BORDER, DIM, LIST, TB, TEXT};
|
||||||
use iced::widget::{button, column, container, row, scrollable, text, text_input, Space};
|
use iced::widget::{
|
||||||
|
column, container, mouse_area, row, scrollable, text, text_input, Space,
|
||||||
|
};
|
||||||
use iced::{Background, Border, Color, Element, Fill, Theme};
|
use iced::{Background, Border, Color, Element, Fill, Theme};
|
||||||
|
|
||||||
|
/// Inline-rename text-input id, so the rename-start handler can focus it.
|
||||||
|
pub fn rename_input_id() -> iced::widget::Id {
|
||||||
|
iced::widget::Id::new("scale-rename-input")
|
||||||
|
}
|
||||||
|
|
||||||
const INPUT_BG: Color = Color {
|
const INPUT_BG: Color = Color {
|
||||||
r: 0.10,
|
r: 0.10,
|
||||||
g: 0.10,
|
g: 0.10,
|
||||||
|
|
@ -55,7 +62,8 @@ pub fn view_window<'a, 'b>(
|
||||||
scales: &'b [(String, String)],
|
scales: &'b [(String, String)],
|
||||||
selected: &'b str,
|
selected: &'b str,
|
||||||
current: &'b str,
|
current: &'b str,
|
||||||
name_buf: &'a str,
|
rename_active: Option<&'b str>,
|
||||||
|
rename_buf: &'a str,
|
||||||
paper_buf: &'a str,
|
paper_buf: &'a str,
|
||||||
drawing_buf: &'a str,
|
drawing_buf: &'a str,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
|
|
@ -63,6 +71,7 @@ pub fn view_window<'a, 'b>(
|
||||||
let toolbar = container(
|
let toolbar = container(
|
||||||
row![
|
row![
|
||||||
tb_button("New", Message::ScaleManagerNew, false),
|
tb_button("New", Message::ScaleManagerNew, false),
|
||||||
|
tb_button("Copy", Message::ScaleManagerCopy, false),
|
||||||
tb_button("Delete", Message::ScaleManagerDelete, false),
|
tb_button("Delete", Message::ScaleManagerDelete, false),
|
||||||
Space::new().width(Fill),
|
Space::new().width(Fill),
|
||||||
tb_button("Set Current", Message::ScaleManagerSetCurrent, false),
|
tb_button("Set Current", Message::ScaleManagerSetCurrent, false),
|
||||||
|
|
@ -82,6 +91,18 @@ pub fn view_window<'a, 'b>(
|
||||||
let rows: Vec<Element<'_, Message>> = scales
|
let rows: Vec<Element<'_, Message>> = scales
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(name, ratio)| {
|
.map(|(name, ratio)| {
|
||||||
|
// The row being renamed shows an inline text field; a single click
|
||||||
|
// selects, a double click starts renaming.
|
||||||
|
if rename_active == Some(name.as_str()) {
|
||||||
|
return text_input("", rename_buf)
|
||||||
|
.id(rename_input_id())
|
||||||
|
.on_input(Message::ScaleRenameEdit)
|
||||||
|
.on_submit(Message::ScaleRenameCommit)
|
||||||
|
.size(11)
|
||||||
|
.padding([4, 8])
|
||||||
|
.width(Fill)
|
||||||
|
.into();
|
||||||
|
}
|
||||||
let is_sel = name.as_str() == selected;
|
let is_sel = name.as_str() == selected;
|
||||||
let is_cur = name.eq_ignore_ascii_case(current);
|
let is_cur = name.eq_ignore_ascii_case(current);
|
||||||
let check = crate::ui::icons::check_cell(is_cur, CURRENT_CHECK);
|
let check = crate::ui::icons::check_cell(is_cur, CURRENT_CHECK);
|
||||||
|
|
@ -92,26 +113,17 @@ pub fn view_window<'a, 'b>(
|
||||||
]
|
]
|
||||||
.spacing(4)
|
.spacing(4)
|
||||||
.align_y(iced::Center);
|
.align_y(iced::Center);
|
||||||
button(label)
|
let cell = container(label)
|
||||||
.on_press(Message::ScaleManagerSelect(name.clone()))
|
|
||||||
.style(move |_: &Theme, st| button::Style {
|
|
||||||
background: Some(Background::Color(if is_sel {
|
|
||||||
ACTIVE
|
|
||||||
} else if matches!(st, button::Status::Hovered) {
|
|
||||||
Color {
|
|
||||||
r: 0.20,
|
|
||||||
g: 0.20,
|
|
||||||
b: 0.20,
|
|
||||||
a: 1.0,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Color::TRANSPARENT
|
|
||||||
})),
|
|
||||||
text_color: TEXT,
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.width(Fill)
|
|
||||||
.padding([4, 8])
|
.padding([4, 8])
|
||||||
|
.width(Fill)
|
||||||
|
.style(move |_: &Theme| container::Style {
|
||||||
|
background: is_sel.then_some(Background::Color(ACTIVE)),
|
||||||
|
text_color: Some(TEXT),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
mouse_area(cell)
|
||||||
|
.on_press(Message::ScaleManagerSelect(name.clone()))
|
||||||
|
.on_double_click(Message::ScaleRenameStart(name.clone()))
|
||||||
.into()
|
.into()
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
@ -164,11 +176,10 @@ pub fn view_window<'a, 'b>(
|
||||||
let editor = container(
|
let editor = container(
|
||||||
column![
|
column![
|
||||||
text("Scale").size(10).color(DIM),
|
text("Scale").size(10).color(DIM),
|
||||||
field("Name", "e.g. 1:50", name_buf, Message::ScaleManagerNameBuf),
|
|
||||||
field("Paper units", "1", paper_buf, Message::ScaleManagerPaperBuf),
|
field("Paper units", "1", paper_buf, Message::ScaleManagerPaperBuf),
|
||||||
field("Drawing units", "50", drawing_buf, Message::ScaleManagerDrawingBuf),
|
field("Drawing units", "50", drawing_buf, Message::ScaleManagerDrawingBuf),
|
||||||
Space::new().height(6),
|
Space::new().height(6),
|
||||||
text("New starts a blank scale; edit the fields and click Apply to save it. Changes are discarded if you close without Apply.")
|
text("Double-click a scale to rename it; edit its paper : drawing ratio here. New / Copy add a scale. Changes are kept only if you click Apply before closing.")
|
||||||
.size(10)
|
.size(10)
|
||||||
.color(DIM),
|
.color(DIM),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue