feat(layers): add native state manager
Replace session-only layer snapshots with drawing-backed DWG and DXF states, exposed through the ribbon and LAYERSTATE commands.\n\nCloses #562
This commit is contained in:
parent
ad736e20ea
commit
1e624cc6b7
12 changed files with 563 additions and 84 deletions
|
|
@ -212,22 +212,10 @@ impl OpenCADStudio {
|
|||
}
|
||||
|
||||
// LAYERSTATE — save / restore named snapshots of all layer states
|
||||
// (on/off, freeze, lock, colour, linetype, lineweight).
|
||||
// in the drawing's native ACAD_LAYERSTATES dictionary.
|
||||
// LAYERSTATE SAVE <name> | RESTORE <name> | DELETE <name> | ? (list)
|
||||
"LAYERSTATE" | "LAS" | "LMAN" => {
|
||||
use crate::command::KeywordCommand;
|
||||
let c = KeywordCommand::new(
|
||||
"LAYERSTATE",
|
||||
"LAYERSTATE [List / Save / Restore / Delete]:",
|
||||
vec![
|
||||
("List", "LIST", None),
|
||||
("Save", "SAVE", Some("LAYERSTATE SAVE new state name:")),
|
||||
("Restore", "RESTORE", Some("LAYERSTATE RESTORE state name:")),
|
||||
("Delete", "DELETE", Some("LAYERSTATE DELETE state name:")),
|
||||
],
|
||||
);
|
||||
self.command_line.push_info(&c.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(c));
|
||||
return Some(Task::done(Message::LayerStateManagerOpen));
|
||||
}
|
||||
cmd if cmd.starts_with("LAYERSTATE ")
|
||||
|| cmd.starts_with("LAS ")
|
||||
|
|
@ -243,13 +231,14 @@ impl OpenCADStudio {
|
|||
let arg = parts.next().unwrap_or("").trim();
|
||||
match sub.as_str() {
|
||||
"" | "?" | "LIST" => {
|
||||
let states = &self.tabs[i].layer_states;
|
||||
let states = self.tabs[i].scene.document.layer_states();
|
||||
if states.is_empty() {
|
||||
self.command_line.push_info(
|
||||
"LAYERSTATE: no saved states. Use LAYERSTATE SAVE <name>.",
|
||||
);
|
||||
} else {
|
||||
let mut names: Vec<&str> = states.keys().map(|s| s.as_str()).collect();
|
||||
let mut names: Vec<&str> =
|
||||
states.iter().map(|state| state.name.as_str()).collect();
|
||||
names.sort_unstable();
|
||||
self.command_line
|
||||
.push_output(&format!("Saved layer states: {}", names.join(", ")));
|
||||
|
|
@ -259,7 +248,18 @@ impl OpenCADStudio {
|
|||
if arg.is_empty() {
|
||||
self.command_line.push_info("Usage: LAYERSTATE SAVE <name>");
|
||||
} else {
|
||||
self.tabs[i].save_layer_state(arg);
|
||||
let description = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.layer_state(arg)
|
||||
.map(|state| state.description)
|
||||
.unwrap_or_default();
|
||||
self.push_undo_snapshot(i, "LAYERSTATE SAVE");
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.capture_layer_state(arg, description);
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line
|
||||
.push_output(&format!("LAYERSTATE: saved \"{arg}\"."));
|
||||
}
|
||||
|
|
@ -268,7 +268,7 @@ impl OpenCADStudio {
|
|||
if arg.is_empty() {
|
||||
self.command_line
|
||||
.push_info("Usage: LAYERSTATE RESTORE <name>");
|
||||
} else if !self.tabs[i].layer_states.contains_key(arg) {
|
||||
} else if self.tabs[i].scene.document.layer_state(arg).is_none() {
|
||||
self.command_line.push_error(&format!(
|
||||
"LAYERSTATE: no saved state named \"{arg}\"."
|
||||
));
|
||||
|
|
@ -280,11 +280,20 @@ impl OpenCADStudio {
|
|||
.iter()
|
||||
.map(|layer| layer.name.clone())
|
||||
.collect();
|
||||
let undo = self.begin_layer_undo(i, "LAYERSTATE", &names);
|
||||
let n = self.tabs[i].restore_layer_state(arg).unwrap_or(0);
|
||||
self.push_undo_snapshot(i, "LAYERSTATE RESTORE");
|
||||
let n = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.restore_layer_state(arg)
|
||||
.unwrap_or(0);
|
||||
self.tabs[i].active_layer = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.header
|
||||
.current_layer_name
|
||||
.clone();
|
||||
self.tabs[i].scene.invalidate_layer_dependencies(&names);
|
||||
self.tabs[i].dirty = true;
|
||||
self.commit_layer_undo(i, undo);
|
||||
self.refresh_layer_panel();
|
||||
self.command_line.push_output(&format!(
|
||||
"LAYERSTATE: restored \"{arg}\" ({n} layer(s))."
|
||||
|
|
@ -292,13 +301,19 @@ impl OpenCADStudio {
|
|||
}
|
||||
}
|
||||
"DELETE" | "D" => {
|
||||
if self.tabs[i].layer_states.remove(arg).is_some() {
|
||||
if arg.is_empty() {
|
||||
self.command_line
|
||||
.push_output(&format!("LAYERSTATE: deleted \"{arg}\"."));
|
||||
} else {
|
||||
.push_info("Usage: LAYERSTATE DELETE <name>");
|
||||
} else if self.tabs[i].scene.document.layer_state(arg).is_none() {
|
||||
self.command_line.push_error(&format!(
|
||||
"LAYERSTATE: no saved state named \"{arg}\"."
|
||||
));
|
||||
} else {
|
||||
self.push_undo_snapshot(i, "LAYERSTATE DELETE");
|
||||
self.tabs[i].scene.document.delete_layer_state(arg);
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line
|
||||
.push_output(&format!("LAYERSTATE: deleted \"{arg}\"."));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
|
|
|
|||
|
|
@ -102,60 +102,6 @@ fn default_role_for(component: DynComponent) -> crate::command::DynRole {
|
|||
|
||||
// ── Per-document tab state ─────────────────────────────────────────────────
|
||||
|
||||
/// One layer's display state, captured and restored by LAYERSTATE.
|
||||
#[derive(Clone)]
|
||||
pub(super) struct LayerSnap {
|
||||
pub name: String,
|
||||
pub off: bool,
|
||||
pub frozen: bool,
|
||||
pub locked: bool,
|
||||
pub color: acadrust::types::Color,
|
||||
pub line_type: String,
|
||||
pub line_weight: acadrust::types::LineWeight,
|
||||
}
|
||||
|
||||
impl DocumentTab {
|
||||
/// Snapshot every layer's current display state under `name` (overwrites
|
||||
/// any existing state of that name).
|
||||
pub(super) fn save_layer_state(&mut self, name: &str) {
|
||||
let snaps: Vec<LayerSnap> = self
|
||||
.scene
|
||||
.document
|
||||
.layers
|
||||
.iter()
|
||||
.map(|l| LayerSnap {
|
||||
name: l.name.clone(),
|
||||
off: l.flags.off,
|
||||
frozen: l.flags.frozen,
|
||||
locked: l.flags.locked,
|
||||
color: l.color.clone(),
|
||||
line_type: l.line_type.clone(),
|
||||
line_weight: l.line_weight.clone(),
|
||||
})
|
||||
.collect();
|
||||
self.layer_states.insert(name.to_string(), snaps);
|
||||
}
|
||||
|
||||
/// Reapply the saved state `name` to the matching layers; returns the number
|
||||
/// of layers updated, or `None` if no such state exists.
|
||||
pub(super) fn restore_layer_state(&mut self, name: &str) -> Option<usize> {
|
||||
let snaps = self.layer_states.get(name)?.clone();
|
||||
let mut applied = 0usize;
|
||||
for s in &snaps {
|
||||
if let Some(l) = self.scene.document.layers.get_mut(&s.name) {
|
||||
l.flags.off = s.off;
|
||||
l.flags.frozen = s.frozen;
|
||||
l.flags.locked = s.locked;
|
||||
l.color = s.color.clone();
|
||||
l.line_type = s.line_type.clone();
|
||||
l.line_weight = s.line_weight.clone();
|
||||
applied += 1;
|
||||
}
|
||||
}
|
||||
Some(applied)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct DocumentTab {
|
||||
/// Stable identity across tab insert/remove operations. Background work
|
||||
/// must never target a tab by its transient vector index.
|
||||
|
|
@ -221,8 +167,6 @@ pub(super) struct DocumentTab {
|
|||
pub(super) dyn_active: usize,
|
||||
pub(super) history: HistoryState,
|
||||
pub(super) active_layer: String,
|
||||
/// Named layer-state snapshots saved by LAYERSTATE (name → per-layer state).
|
||||
pub(super) layer_states: std::collections::HashMap<String, Vec<LayerSnap>>,
|
||||
/// Currently active UCS. `None` means WCS (identity transform).
|
||||
pub(super) active_ucs: Option<Ucs>,
|
||||
/// Custom model-space background color. `None` = default dark grey.
|
||||
|
|
@ -486,7 +430,6 @@ impl DocumentTab {
|
|||
dyn_active: 0,
|
||||
history: HistoryState::default(),
|
||||
active_layer: "0".to_string(),
|
||||
layer_states: std::collections::HashMap::new(),
|
||||
active_ucs: None,
|
||||
bg_color: None,
|
||||
paper_bg_color: None,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,36 @@ use super::OpenCADStudio;
|
|||
use crate::ui;
|
||||
|
||||
impl OpenCADStudio {
|
||||
pub(super) fn load_layer_state_editor(&mut self, selected: Option<String>) {
|
||||
let i = self.active_tab;
|
||||
if let Some(name) = selected {
|
||||
if let Some(state) = self.tabs[i].scene.document.layer_state(&name) {
|
||||
self.layer_state_name_buf = state.name.clone();
|
||||
self.layer_state_description_buf = state.description;
|
||||
self.layer_state_selected = Some(state.name);
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.layer_state_selected = None;
|
||||
self.layer_state_description_buf.clear();
|
||||
let names: Vec<String> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.layer_states()
|
||||
.into_iter()
|
||||
.map(|state| state.name)
|
||||
.collect();
|
||||
let n = (1usize..)
|
||||
.find(|n| {
|
||||
let candidate = format!("Layer State {n}");
|
||||
!names
|
||||
.iter()
|
||||
.any(|name| name.eq_ignore_ascii_case(&candidate))
|
||||
})
|
||||
.unwrap_or(1);
|
||||
self.layer_state_name_buf = format!("Layer State {n}");
|
||||
}
|
||||
|
||||
/// Reload the `LayerPanel` cache from the document and push the
|
||||
/// fresh state through to the ribbon dropdown + every other
|
||||
/// layer-aware UI mirror. Use this whenever a command mutates the
|
||||
|
|
|
|||
|
|
@ -735,6 +735,12 @@ pub(super) struct OpenCADStudio {
|
|||
layout_manager_selected: String,
|
||||
layout_manager_rename_buf: String,
|
||||
|
||||
// ── Layer State Manager ───────────────────────────────────────────────
|
||||
layer_state_selected: Option<String>,
|
||||
layer_state_name_buf: String,
|
||||
layer_state_description_buf: String,
|
||||
layer_state_filter: String,
|
||||
|
||||
// ── Annotation-scale Manager ──────────────────────────────────────────
|
||||
scale_manager_selected: String,
|
||||
scale_manager_paper_buf: String,
|
||||
|
|
@ -1317,6 +1323,7 @@ pub enum ModalKind {
|
|||
PluginManager,
|
||||
UpdateNotice,
|
||||
Layers,
|
||||
LayerStateManager,
|
||||
Plot,
|
||||
LayoutManager,
|
||||
Plotstyle,
|
||||
|
|
@ -1671,6 +1678,16 @@ pub enum Message {
|
|||
LayerLineweightSet(LineWeight),
|
||||
LayerTransparencyEdit(usize, String),
|
||||
LayerRenameCommit,
|
||||
// ── Layer State Manager ─────────────────────────────────────────────
|
||||
LayerStateManagerOpen,
|
||||
LayerStateManagerSelect(String),
|
||||
LayerStateManagerNew,
|
||||
LayerStateManagerFilter(String),
|
||||
LayerStateManagerName(String),
|
||||
LayerStateManagerDescription(String),
|
||||
LayerStateManagerSave,
|
||||
LayerStateManagerRestore,
|
||||
LayerStateManagerDelete,
|
||||
CursorMoved(Point),
|
||||
ViewportClick,
|
||||
ViewportMove(Point),
|
||||
|
|
@ -2708,6 +2725,10 @@ impl OpenCADStudio {
|
|||
alias_editor_rows: Vec::new(),
|
||||
// Layout Manager
|
||||
layout_manager_selected: "Model".to_string(),
|
||||
layer_state_selected: None,
|
||||
layer_state_name_buf: String::new(),
|
||||
layer_state_description_buf: String::new(),
|
||||
layer_state_filter: String::new(),
|
||||
scale_manager_selected: String::new(),
|
||||
scale_manager_paper_buf: String::new(),
|
||||
scale_manager_drawing_buf: String::new(),
|
||||
|
|
|
|||
|
|
@ -1009,9 +1009,25 @@ impl OpenCADStudio {
|
|||
}
|
||||
self.active_tab = idx;
|
||||
if self.tabs[idx].is_start
|
||||
&& self.active_modal == Some(super::ModalKind::LayoutManager)
|
||||
&& matches!(
|
||||
self.active_modal,
|
||||
Some(
|
||||
super::ModalKind::LayoutManager
|
||||
| super::ModalKind::LayerStateManager
|
||||
)
|
||||
)
|
||||
{
|
||||
self.close_active_modal();
|
||||
} else if self.active_modal == Some(super::ModalKind::LayerStateManager) {
|
||||
let mut names: Vec<String> = self.tabs[idx]
|
||||
.scene
|
||||
.document
|
||||
.layer_states()
|
||||
.into_iter()
|
||||
.map(|state| state.name)
|
||||
.collect();
|
||||
names.sort_by_key(|name| name.to_lowercase());
|
||||
self.load_layer_state_editor(names.into_iter().next());
|
||||
}
|
||||
self.sync_ribbon_layers();
|
||||
self.sync_ribbon_styles();
|
||||
|
|
@ -1401,6 +1417,150 @@ impl OpenCADStudio {
|
|||
Task::none()
|
||||
}
|
||||
|
||||
Message::LayerStateManagerOpen => {
|
||||
let i = self.active_tab;
|
||||
self.ribbon.close_dropdown();
|
||||
if self.tabs[i].is_start {
|
||||
self.command_line
|
||||
.push_info("Open or create a drawing to manage layer states.");
|
||||
return Task::none();
|
||||
}
|
||||
let mut names: Vec<String> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.layer_states()
|
||||
.into_iter()
|
||||
.map(|state| state.name)
|
||||
.collect();
|
||||
names.sort_by_key(|name| name.to_lowercase());
|
||||
self.load_layer_state_editor(names.into_iter().next());
|
||||
self.active_modal = Some(super::ModalKind::LayerStateManager);
|
||||
Task::none()
|
||||
}
|
||||
Message::LayerStateManagerSelect(name) => {
|
||||
self.load_layer_state_editor(Some(name));
|
||||
Task::none()
|
||||
}
|
||||
Message::LayerStateManagerNew => {
|
||||
self.load_layer_state_editor(None);
|
||||
Task::none()
|
||||
}
|
||||
Message::LayerStateManagerFilter(value) => {
|
||||
self.layer_state_filter = value;
|
||||
Task::none()
|
||||
}
|
||||
Message::LayerStateManagerName(value) => {
|
||||
self.layer_state_name_buf = value;
|
||||
Task::none()
|
||||
}
|
||||
Message::LayerStateManagerDescription(value) => {
|
||||
self.layer_state_description_buf = value;
|
||||
Task::none()
|
||||
}
|
||||
Message::LayerStateManagerSave => {
|
||||
let i = self.active_tab;
|
||||
let name = self.layer_state_name_buf.trim().to_string();
|
||||
if name.is_empty() {
|
||||
self.command_line
|
||||
.push_error("Layer state name cannot be empty.");
|
||||
return Task::none();
|
||||
}
|
||||
let old_name = self.layer_state_selected.clone();
|
||||
let duplicate = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.layer_states()
|
||||
.into_iter()
|
||||
.any(|state| {
|
||||
state.name.eq_ignore_ascii_case(&name)
|
||||
&& old_name
|
||||
.as_deref()
|
||||
.is_none_or(|old| !state.name.eq_ignore_ascii_case(old))
|
||||
});
|
||||
if duplicate {
|
||||
self.command_line
|
||||
.push_error(&format!("Layer state \"{name}\" already exists."));
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
self.push_undo_snapshot(i, "LAYERSTATE SAVE");
|
||||
if let Some(old_name) = old_name.as_deref() {
|
||||
if !old_name.eq_ignore_ascii_case(&name) {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.rename_layer_state(old_name, &name);
|
||||
}
|
||||
}
|
||||
self.tabs[i].scene.document.capture_layer_state(
|
||||
&name,
|
||||
self.layer_state_description_buf.trim(),
|
||||
);
|
||||
self.tabs[i].dirty = true;
|
||||
self.layer_state_selected = Some(name.clone());
|
||||
self.layer_state_name_buf = name.clone();
|
||||
self.command_line
|
||||
.push_output(&format!("LAYERSTATE: saved \"{name}\" in the drawing."));
|
||||
Task::none()
|
||||
}
|
||||
Message::LayerStateManagerRestore => {
|
||||
let i = self.active_tab;
|
||||
let Some(name) = self.layer_state_selected.clone() else {
|
||||
return Task::none();
|
||||
};
|
||||
let layer_names: Vec<String> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.layers
|
||||
.iter()
|
||||
.map(|layer| layer.name.clone())
|
||||
.collect();
|
||||
self.push_undo_snapshot(i, "LAYERSTATE RESTORE");
|
||||
let restored = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.restore_layer_state(&name)
|
||||
.unwrap_or(0);
|
||||
let active = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.header
|
||||
.current_layer_name
|
||||
.clone();
|
||||
self.tabs[i].active_layer = active;
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.invalidate_layer_dependencies(&layer_names);
|
||||
self.tabs[i].dirty = true;
|
||||
self.refresh_layer_panel();
|
||||
self.command_line.push_output(&format!(
|
||||
"LAYERSTATE: restored \"{name}\" ({restored} layer(s))."
|
||||
));
|
||||
Task::none()
|
||||
}
|
||||
Message::LayerStateManagerDelete => {
|
||||
let i = self.active_tab;
|
||||
let Some(name) = self.layer_state_selected.clone() else {
|
||||
return Task::none();
|
||||
};
|
||||
self.push_undo_snapshot(i, "LAYERSTATE DELETE");
|
||||
if self.tabs[i].scene.document.delete_layer_state(&name) {
|
||||
self.tabs[i].dirty = true;
|
||||
let mut names: Vec<String> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.layer_states()
|
||||
.into_iter()
|
||||
.map(|state| state.name)
|
||||
.collect();
|
||||
names.sort_by_key(|name| name.to_lowercase());
|
||||
self.load_layer_state_editor(names.into_iter().next());
|
||||
self.command_line
|
||||
.push_output(&format!("LAYERSTATE: deleted \"{name}\"."));
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::WindowCloseRequested(id) => {
|
||||
if self.main_window == Some(id) {
|
||||
if self.tabs.iter().any(|t| t.dirty) {
|
||||
|
|
|
|||
|
|
@ -1571,6 +1571,7 @@ impl OpenCADStudio {
|
|||
PluginManager => (940, 600),
|
||||
UpdateNotice => (560, 460),
|
||||
Layers => (900, 360),
|
||||
LayerStateManager => (720, 420),
|
||||
Plot => (760, 540),
|
||||
LayoutManager => (640, 320),
|
||||
Plotstyle => (780, 540),
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ impl OpenCADStudio {
|
|||
Some(K::PluginManager) => "Plugin Manager",
|
||||
Some(K::UpdateNotice) => "Update Available",
|
||||
Some(K::Layers) => "Layer Manager",
|
||||
Some(K::LayerStateManager) => "Layer State Manager",
|
||||
Some(K::Plot) => "Plot",
|
||||
Some(K::LayoutManager) => "Layout Manager",
|
||||
Some(K::ScaleManager) => "Scale Manager",
|
||||
|
|
@ -119,6 +120,20 @@ impl OpenCADStudio {
|
|||
let tab = &self.tabs[self.active_tab];
|
||||
sized(tab.layers.view_window(self.layer_name_col_w), 900, 360)
|
||||
}
|
||||
super::super::ModalKind::LayerStateManager => {
|
||||
let states = self.tabs[self.active_tab].scene.document.layer_states();
|
||||
sized(
|
||||
crate::ui::window::layer_state_manager::view_window(
|
||||
states,
|
||||
self.layer_state_selected.as_deref(),
|
||||
&self.layer_state_name_buf,
|
||||
&self.layer_state_description_buf,
|
||||
&self.layer_state_filter,
|
||||
),
|
||||
720,
|
||||
420,
|
||||
)
|
||||
}
|
||||
super::super::ModalKind::Plot => {
|
||||
sized(crate::ui::window::plot::view_window(&self.plot_dialog), 760, 540)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue