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
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -78,7 +78,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "acadrust"
|
name = "acadrust"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
source = "git+https://github.com/OpenAEC-Foundation/acadifc.git?rev=5a2069a#5a2069a7abe6b440fbd3bbacb4dfff8555f082c9"
|
source = "git+https://github.com/OpenAEC-Foundation/acadifc.git?rev=a737c2c#a737c2c6c10d3639b6b82f19aa04ad33a52fa187"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash 0.8.12",
|
"ahash 0.8.12",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ windows-sys = { version = "0.61", features = ["Win32_UI_Shell", "Win32_UI_Window
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
# Track standard DWG object relationships used by the scene integration.
|
# Track standard DWG object relationships used by the scene integration.
|
||||||
acadrust = { git = "https://github.com/OpenAEC-Foundation/acadifc.git", rev = "5a2069a" }
|
acadrust = { git = "https://github.com/OpenAEC-Foundation/acadifc.git", rev = "a737c2c" }
|
||||||
|
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
# Native enables the plugin host runtime (out-of-process plugins).
|
# Native enables the plugin host runtime (out-of-process plugins).
|
||||||
|
|
|
||||||
|
|
@ -212,22 +212,10 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
|
|
||||||
// LAYERSTATE — save / restore named snapshots of all layer states
|
// 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 SAVE <name> | RESTORE <name> | DELETE <name> | ? (list)
|
||||||
"LAYERSTATE" | "LAS" | "LMAN" => {
|
"LAYERSTATE" | "LAS" | "LMAN" => {
|
||||||
use crate::command::KeywordCommand;
|
return Some(Task::done(Message::LayerStateManagerOpen));
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
cmd if cmd.starts_with("LAYERSTATE ")
|
cmd if cmd.starts_with("LAYERSTATE ")
|
||||||
|| cmd.starts_with("LAS ")
|
|| cmd.starts_with("LAS ")
|
||||||
|
|
@ -243,13 +231,14 @@ impl OpenCADStudio {
|
||||||
let arg = parts.next().unwrap_or("").trim();
|
let arg = parts.next().unwrap_or("").trim();
|
||||||
match sub.as_str() {
|
match sub.as_str() {
|
||||||
"" | "?" | "LIST" => {
|
"" | "?" | "LIST" => {
|
||||||
let states = &self.tabs[i].layer_states;
|
let states = self.tabs[i].scene.document.layer_states();
|
||||||
if states.is_empty() {
|
if states.is_empty() {
|
||||||
self.command_line.push_info(
|
self.command_line.push_info(
|
||||||
"LAYERSTATE: no saved states. Use LAYERSTATE SAVE <name>.",
|
"LAYERSTATE: no saved states. Use LAYERSTATE SAVE <name>.",
|
||||||
);
|
);
|
||||||
} else {
|
} 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();
|
names.sort_unstable();
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_output(&format!("Saved layer states: {}", names.join(", ")));
|
.push_output(&format!("Saved layer states: {}", names.join(", ")));
|
||||||
|
|
@ -259,7 +248,18 @@ impl OpenCADStudio {
|
||||||
if arg.is_empty() {
|
if arg.is_empty() {
|
||||||
self.command_line.push_info("Usage: LAYERSTATE SAVE <name>");
|
self.command_line.push_info("Usage: LAYERSTATE SAVE <name>");
|
||||||
} else {
|
} 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
|
self.command_line
|
||||||
.push_output(&format!("LAYERSTATE: saved \"{arg}\"."));
|
.push_output(&format!("LAYERSTATE: saved \"{arg}\"."));
|
||||||
}
|
}
|
||||||
|
|
@ -268,7 +268,7 @@ impl OpenCADStudio {
|
||||||
if arg.is_empty() {
|
if arg.is_empty() {
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_info("Usage: LAYERSTATE RESTORE <name>");
|
.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!(
|
self.command_line.push_error(&format!(
|
||||||
"LAYERSTATE: no saved state named \"{arg}\"."
|
"LAYERSTATE: no saved state named \"{arg}\"."
|
||||||
));
|
));
|
||||||
|
|
@ -280,11 +280,20 @@ impl OpenCADStudio {
|
||||||
.iter()
|
.iter()
|
||||||
.map(|layer| layer.name.clone())
|
.map(|layer| layer.name.clone())
|
||||||
.collect();
|
.collect();
|
||||||
let undo = self.begin_layer_undo(i, "LAYERSTATE", &names);
|
self.push_undo_snapshot(i, "LAYERSTATE RESTORE");
|
||||||
let n = self.tabs[i].restore_layer_state(arg).unwrap_or(0);
|
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].scene.invalidate_layer_dependencies(&names);
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
self.commit_layer_undo(i, undo);
|
|
||||||
self.refresh_layer_panel();
|
self.refresh_layer_panel();
|
||||||
self.command_line.push_output(&format!(
|
self.command_line.push_output(&format!(
|
||||||
"LAYERSTATE: restored \"{arg}\" ({n} layer(s))."
|
"LAYERSTATE: restored \"{arg}\" ({n} layer(s))."
|
||||||
|
|
@ -292,13 +301,19 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"DELETE" | "D" => {
|
"DELETE" | "D" => {
|
||||||
if self.tabs[i].layer_states.remove(arg).is_some() {
|
if arg.is_empty() {
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_output(&format!("LAYERSTATE: deleted \"{arg}\"."));
|
.push_info("Usage: LAYERSTATE DELETE <name>");
|
||||||
} else {
|
} else if self.tabs[i].scene.document.layer_state(arg).is_none() {
|
||||||
self.command_line.push_error(&format!(
|
self.command_line.push_error(&format!(
|
||||||
"LAYERSTATE: no saved state named \"{arg}\"."
|
"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 ─────────────────────────────────────────────────
|
// ── 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 {
|
pub(super) struct DocumentTab {
|
||||||
/// Stable identity across tab insert/remove operations. Background work
|
/// Stable identity across tab insert/remove operations. Background work
|
||||||
/// must never target a tab by its transient vector index.
|
/// 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) dyn_active: usize,
|
||||||
pub(super) history: HistoryState,
|
pub(super) history: HistoryState,
|
||||||
pub(super) active_layer: String,
|
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).
|
/// Currently active UCS. `None` means WCS (identity transform).
|
||||||
pub(super) active_ucs: Option<Ucs>,
|
pub(super) active_ucs: Option<Ucs>,
|
||||||
/// Custom model-space background color. `None` = default dark grey.
|
/// Custom model-space background color. `None` = default dark grey.
|
||||||
|
|
@ -486,7 +430,6 @@ impl DocumentTab {
|
||||||
dyn_active: 0,
|
dyn_active: 0,
|
||||||
history: HistoryState::default(),
|
history: HistoryState::default(),
|
||||||
active_layer: "0".to_string(),
|
active_layer: "0".to_string(),
|
||||||
layer_states: std::collections::HashMap::new(),
|
|
||||||
active_ucs: None,
|
active_ucs: None,
|
||||||
bg_color: None,
|
bg_color: None,
|
||||||
paper_bg_color: None,
|
paper_bg_color: None,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,36 @@ use super::OpenCADStudio;
|
||||||
use crate::ui;
|
use crate::ui;
|
||||||
|
|
||||||
impl OpenCADStudio {
|
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
|
/// Reload the `LayerPanel` cache from the document and push the
|
||||||
/// fresh state through to the ribbon dropdown + every other
|
/// fresh state through to the ribbon dropdown + every other
|
||||||
/// layer-aware UI mirror. Use this whenever a command mutates the
|
/// 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_selected: String,
|
||||||
layout_manager_rename_buf: 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 ──────────────────────────────────────────
|
// ── Annotation-scale Manager ──────────────────────────────────────────
|
||||||
scale_manager_selected: String,
|
scale_manager_selected: String,
|
||||||
scale_manager_paper_buf: String,
|
scale_manager_paper_buf: String,
|
||||||
|
|
@ -1317,6 +1323,7 @@ pub enum ModalKind {
|
||||||
PluginManager,
|
PluginManager,
|
||||||
UpdateNotice,
|
UpdateNotice,
|
||||||
Layers,
|
Layers,
|
||||||
|
LayerStateManager,
|
||||||
Plot,
|
Plot,
|
||||||
LayoutManager,
|
LayoutManager,
|
||||||
Plotstyle,
|
Plotstyle,
|
||||||
|
|
@ -1671,6 +1678,16 @@ pub enum Message {
|
||||||
LayerLineweightSet(LineWeight),
|
LayerLineweightSet(LineWeight),
|
||||||
LayerTransparencyEdit(usize, String),
|
LayerTransparencyEdit(usize, String),
|
||||||
LayerRenameCommit,
|
LayerRenameCommit,
|
||||||
|
// ── Layer State Manager ─────────────────────────────────────────────
|
||||||
|
LayerStateManagerOpen,
|
||||||
|
LayerStateManagerSelect(String),
|
||||||
|
LayerStateManagerNew,
|
||||||
|
LayerStateManagerFilter(String),
|
||||||
|
LayerStateManagerName(String),
|
||||||
|
LayerStateManagerDescription(String),
|
||||||
|
LayerStateManagerSave,
|
||||||
|
LayerStateManagerRestore,
|
||||||
|
LayerStateManagerDelete,
|
||||||
CursorMoved(Point),
|
CursorMoved(Point),
|
||||||
ViewportClick,
|
ViewportClick,
|
||||||
ViewportMove(Point),
|
ViewportMove(Point),
|
||||||
|
|
@ -2708,6 +2725,10 @@ impl OpenCADStudio {
|
||||||
alias_editor_rows: Vec::new(),
|
alias_editor_rows: Vec::new(),
|
||||||
// Layout Manager
|
// Layout Manager
|
||||||
layout_manager_selected: "Model".to_string(),
|
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_selected: 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(),
|
||||||
|
|
|
||||||
|
|
@ -1009,9 +1009,25 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
self.active_tab = idx;
|
self.active_tab = idx;
|
||||||
if self.tabs[idx].is_start
|
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();
|
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_layers();
|
||||||
self.sync_ribbon_styles();
|
self.sync_ribbon_styles();
|
||||||
|
|
@ -1401,6 +1417,150 @@ impl OpenCADStudio {
|
||||||
Task::none()
|
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) => {
|
Message::WindowCloseRequested(id) => {
|
||||||
if self.main_window == Some(id) {
|
if self.main_window == Some(id) {
|
||||||
if self.tabs.iter().any(|t| t.dirty) {
|
if self.tabs.iter().any(|t| t.dirty) {
|
||||||
|
|
|
||||||
|
|
@ -1571,6 +1571,7 @@ impl OpenCADStudio {
|
||||||
PluginManager => (940, 600),
|
PluginManager => (940, 600),
|
||||||
UpdateNotice => (560, 460),
|
UpdateNotice => (560, 460),
|
||||||
Layers => (900, 360),
|
Layers => (900, 360),
|
||||||
|
LayerStateManager => (720, 420),
|
||||||
Plot => (760, 540),
|
Plot => (760, 540),
|
||||||
LayoutManager => (640, 320),
|
LayoutManager => (640, 320),
|
||||||
Plotstyle => (780, 540),
|
Plotstyle => (780, 540),
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ impl OpenCADStudio {
|
||||||
Some(K::PluginManager) => "Plugin Manager",
|
Some(K::PluginManager) => "Plugin Manager",
|
||||||
Some(K::UpdateNotice) => "Update Available",
|
Some(K::UpdateNotice) => "Update Available",
|
||||||
Some(K::Layers) => "Layer Manager",
|
Some(K::Layers) => "Layer Manager",
|
||||||
|
Some(K::LayerStateManager) => "Layer State Manager",
|
||||||
Some(K::Plot) => "Plot",
|
Some(K::Plot) => "Plot",
|
||||||
Some(K::LayoutManager) => "Layout Manager",
|
Some(K::LayoutManager) => "Layout Manager",
|
||||||
Some(K::ScaleManager) => "Scale Manager",
|
Some(K::ScaleManager) => "Scale Manager",
|
||||||
|
|
@ -119,6 +120,20 @@ impl OpenCADStudio {
|
||||||
let tab = &self.tabs[self.active_tab];
|
let tab = &self.tabs[self.active_tab];
|
||||||
sized(tab.layers.view_window(self.layer_name_col_w), 900, 360)
|
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 => {
|
super::super::ModalKind::Plot => {
|
||||||
sized(crate::ui::window::plot::view_window(&self.plot_dialog), 760, 540)
|
sized(crate::ui::window::plot::view_window(&self.plot_dialog), 760, 540)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -960,10 +960,33 @@ impl Ribbon {
|
||||||
.on_input(Message::RibbonLayerFilterChanged)
|
.on_input(Message::RibbonLayerFilterChanged)
|
||||||
.size(11)
|
.size(11)
|
||||||
.padding([4, 6]);
|
.padding([4, 6]);
|
||||||
|
let state_manager = button(
|
||||||
|
row![
|
||||||
|
crate::ui::icons::themed_arrow_right(9.0),
|
||||||
|
text("Layer State Manager…").size(11),
|
||||||
|
]
|
||||||
|
.spacing(7)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
)
|
||||||
|
.on_press(Message::LayerStateManagerOpen)
|
||||||
|
.style(popup_row_style)
|
||||||
|
.width(Fill)
|
||||||
|
.padding([6, 10]);
|
||||||
let panel = container(
|
let panel = container(
|
||||||
column![
|
column![
|
||||||
container(search).padding([4, 4]),
|
container(search).padding([4, 4]),
|
||||||
scrollable(column(rows)).height(Length::Fixed(list_h)),
|
scrollable(column(rows)).height(Length::Fixed(list_h)),
|
||||||
|
container(state_manager)
|
||||||
|
.width(Fill)
|
||||||
|
.padding([3, 4])
|
||||||
|
.style(|theme: &Theme| container::Style {
|
||||||
|
border: Border {
|
||||||
|
color: theme.extended_palette().background.neutral.color,
|
||||||
|
width: 1.0,
|
||||||
|
radius: 0.0.into(),
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
.spacing(2),
|
.spacing(2),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
270
src/ui/window/layer_state_manager.rs
Normal file
270
src/ui/window/layer_state_manager.rs
Normal file
|
|
@ -0,0 +1,270 @@
|
||||||
|
//! Layer State Manager — native DWG/DXF named layer-state UI.
|
||||||
|
|
||||||
|
use crate::app::Message;
|
||||||
|
use acadrust::{LayerState, LayerStateMask};
|
||||||
|
use iced::widget::{button, column, container, row, scrollable, text, text_input, Space};
|
||||||
|
use iced::{Background, Border, Element, Fill, Theme};
|
||||||
|
|
||||||
|
fn muted(theme: &Theme) -> iced::widget::text::Style {
|
||||||
|
iced::widget::text::Style {
|
||||||
|
color: Some(
|
||||||
|
theme
|
||||||
|
.extended_palette()
|
||||||
|
.background
|
||||||
|
.base
|
||||||
|
.text
|
||||||
|
.scale_alpha(0.65),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn button_style(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style {
|
||||||
|
move |theme, status| {
|
||||||
|
if accent {
|
||||||
|
button::primary(theme, status)
|
||||||
|
} else {
|
||||||
|
button::secondary(theme, status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_style(selected: bool) -> impl Fn(&Theme, button::Status) -> button::Style {
|
||||||
|
move |theme, status| {
|
||||||
|
if selected {
|
||||||
|
button::primary(theme, status)
|
||||||
|
} else {
|
||||||
|
button::subtle(theme, status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn divider<'a>() -> Element<'a, Message> {
|
||||||
|
container(Space::new().width(Fill).height(1))
|
||||||
|
.width(Fill)
|
||||||
|
.height(1)
|
||||||
|
.style(|theme: &Theme| container::Style {
|
||||||
|
background: Some(Background::Color(
|
||||||
|
theme.extended_palette().background.neutral.color,
|
||||||
|
)),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mask_summary(mask: LayerStateMask) -> String {
|
||||||
|
let properties = [
|
||||||
|
(LayerStateMask::ON, "On/Off"),
|
||||||
|
(LayerStateMask::FROZEN, "Freeze"),
|
||||||
|
(LayerStateMask::LOCKED, "Lock"),
|
||||||
|
(LayerStateMask::PLOT, "Plot"),
|
||||||
|
(LayerStateMask::COLOR, "Color"),
|
||||||
|
(LayerStateMask::LINE_TYPE, "Linetype"),
|
||||||
|
(LayerStateMask::LINE_WEIGHT, "Lineweight"),
|
||||||
|
(LayerStateMask::PLOT_STYLE, "Plot style"),
|
||||||
|
(LayerStateMask::TRANSPARENCY, "Transparency"),
|
||||||
|
];
|
||||||
|
properties
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(flag, label)| mask.contains(flag).then_some(label))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn view_window<'a>(
|
||||||
|
states: Vec<LayerState>,
|
||||||
|
selected: Option<&'a str>,
|
||||||
|
name: &'a str,
|
||||||
|
description: &'a str,
|
||||||
|
filter: &'a str,
|
||||||
|
) -> Element<'a, Message> {
|
||||||
|
let selected_state = selected.and_then(|selected| {
|
||||||
|
states
|
||||||
|
.iter()
|
||||||
|
.find(|state| state.name.eq_ignore_ascii_case(selected))
|
||||||
|
});
|
||||||
|
let query = filter.trim().to_lowercase();
|
||||||
|
let rows: Vec<Element<'_, Message>> = states
|
||||||
|
.iter()
|
||||||
|
.filter(|state| {
|
||||||
|
query.is_empty()
|
||||||
|
|| state.name.to_lowercase().contains(&query)
|
||||||
|
|| state.description.to_lowercase().contains(&query)
|
||||||
|
})
|
||||||
|
.map(|state| {
|
||||||
|
let is_selected =
|
||||||
|
selected.is_some_and(|selected| state.name.eq_ignore_ascii_case(selected));
|
||||||
|
let subtitle = if state.description.is_empty() {
|
||||||
|
format!("{} layers", state.layers.len())
|
||||||
|
} else {
|
||||||
|
state.description.clone()
|
||||||
|
};
|
||||||
|
button(
|
||||||
|
column![
|
||||||
|
text(state.name.clone()).size(12),
|
||||||
|
text(subtitle).size(10).style(muted),
|
||||||
|
]
|
||||||
|
.spacing(2),
|
||||||
|
)
|
||||||
|
.on_press(Message::LayerStateManagerSelect(state.name.clone()))
|
||||||
|
.style(list_style(is_selected))
|
||||||
|
.padding([6, 9])
|
||||||
|
.width(Fill)
|
||||||
|
.into()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let empty: Element<'_, Message> = container(
|
||||||
|
column![
|
||||||
|
text(if states.is_empty() {
|
||||||
|
"No layer states in this drawing"
|
||||||
|
} else {
|
||||||
|
"No matching layer states"
|
||||||
|
})
|
||||||
|
.size(11)
|
||||||
|
.style(muted),
|
||||||
|
text("Choose New to capture the current layer settings.")
|
||||||
|
.size(10)
|
||||||
|
.style(muted),
|
||||||
|
]
|
||||||
|
.spacing(4),
|
||||||
|
)
|
||||||
|
.padding(14)
|
||||||
|
.into();
|
||||||
|
|
||||||
|
let state_list: Element<'_, Message> = if rows.is_empty() {
|
||||||
|
empty
|
||||||
|
} else {
|
||||||
|
scrollable(column(rows).spacing(2)).height(Fill).into()
|
||||||
|
};
|
||||||
|
|
||||||
|
let left = container(
|
||||||
|
column![
|
||||||
|
text_input("Search layer states…", filter)
|
||||||
|
.on_input(Message::LayerStateManagerFilter)
|
||||||
|
.size(11)
|
||||||
|
.padding([5, 8]),
|
||||||
|
container(state_list)
|
||||||
|
.width(Fill)
|
||||||
|
.height(Fill)
|
||||||
|
.padding(3)
|
||||||
|
.style(|theme: &Theme| container::Style {
|
||||||
|
border: Border {
|
||||||
|
color: theme.extended_palette().background.neutral.color,
|
||||||
|
width: 1.0,
|
||||||
|
radius: 3.0.into(),
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
.spacing(8)
|
||||||
|
.height(Fill),
|
||||||
|
)
|
||||||
|
.width(280)
|
||||||
|
.height(Fill)
|
||||||
|
.padding(iced::Padding {
|
||||||
|
top: 12.0,
|
||||||
|
right: 8.0,
|
||||||
|
bottom: 12.0,
|
||||||
|
left: 12.0,
|
||||||
|
});
|
||||||
|
|
||||||
|
let details = if let Some(state) = selected_state {
|
||||||
|
column![
|
||||||
|
text("Saved state details").size(13),
|
||||||
|
row![
|
||||||
|
text("Layers").size(10).style(muted).width(92),
|
||||||
|
text(state.layers.len().to_string()).size(11),
|
||||||
|
]
|
||||||
|
.spacing(8),
|
||||||
|
row![
|
||||||
|
text("Current layer").size(10).style(muted).width(92),
|
||||||
|
text(if state.current_layer.is_empty() {
|
||||||
|
"—".to_string()
|
||||||
|
} else {
|
||||||
|
state.current_layer.clone()
|
||||||
|
})
|
||||||
|
.size(11),
|
||||||
|
]
|
||||||
|
.spacing(8),
|
||||||
|
text("Restored properties").size(10).style(muted),
|
||||||
|
text(mask_summary(state.mask)).size(11),
|
||||||
|
]
|
||||||
|
.spacing(7)
|
||||||
|
} else {
|
||||||
|
column![
|
||||||
|
text("New layer state").size(13),
|
||||||
|
text("Save captures the current settings of every layer in the drawing.")
|
||||||
|
.size(11)
|
||||||
|
.style(muted),
|
||||||
|
]
|
||||||
|
.spacing(7)
|
||||||
|
};
|
||||||
|
|
||||||
|
let restore = if selected_state.is_some() {
|
||||||
|
button(text("Restore").size(11))
|
||||||
|
.on_press(Message::LayerStateManagerRestore)
|
||||||
|
.style(button_style(true))
|
||||||
|
} else {
|
||||||
|
button(text("Restore").size(11)).style(button_style(true))
|
||||||
|
};
|
||||||
|
let delete = if selected_state.is_some() {
|
||||||
|
button(text("Delete").size(11))
|
||||||
|
.on_press(Message::LayerStateManagerDelete)
|
||||||
|
.style(button::danger)
|
||||||
|
} else {
|
||||||
|
button(text("Delete").size(11)).style(button::danger)
|
||||||
|
};
|
||||||
|
|
||||||
|
let right = container(
|
||||||
|
column![
|
||||||
|
row![
|
||||||
|
button(text("New").size(11))
|
||||||
|
.on_press(Message::LayerStateManagerNew)
|
||||||
|
.style(button_style(false))
|
||||||
|
.padding([5, 12]),
|
||||||
|
Space::new().width(Fill),
|
||||||
|
restore.padding([5, 12]),
|
||||||
|
delete.padding([5, 12]),
|
||||||
|
]
|
||||||
|
.spacing(6)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
divider(),
|
||||||
|
text("Name").size(10).style(muted),
|
||||||
|
text_input("Layer state name", name)
|
||||||
|
.on_input(Message::LayerStateManagerName)
|
||||||
|
.on_submit(Message::LayerStateManagerSave)
|
||||||
|
.size(11)
|
||||||
|
.padding([5, 8]),
|
||||||
|
text("Description").size(10).style(muted),
|
||||||
|
text_input("Optional description", description)
|
||||||
|
.on_input(Message::LayerStateManagerDescription)
|
||||||
|
.size(11)
|
||||||
|
.padding([5, 8]),
|
||||||
|
Space::new().height(8),
|
||||||
|
details,
|
||||||
|
Space::new().height(Fill),
|
||||||
|
text("Layer states are stored inside the drawing and remain available after reopening it.")
|
||||||
|
.size(10)
|
||||||
|
.style(muted),
|
||||||
|
button(text(if selected_state.is_some() {
|
||||||
|
"Save / Update"
|
||||||
|
} else {
|
||||||
|
"Save New State"
|
||||||
|
})
|
||||||
|
.size(11))
|
||||||
|
.on_press(Message::LayerStateManagerSave)
|
||||||
|
.style(button_style(true))
|
||||||
|
.padding([6, 14]),
|
||||||
|
]
|
||||||
|
.spacing(7)
|
||||||
|
.height(Fill),
|
||||||
|
)
|
||||||
|
.width(Fill)
|
||||||
|
.height(Fill)
|
||||||
|
.padding([12, 12]);
|
||||||
|
|
||||||
|
container(row![left, right].height(Fill))
|
||||||
|
.width(Fill)
|
||||||
|
.height(Fill)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
pub mod about;
|
pub mod about;
|
||||||
pub mod layout_manager;
|
pub mod layout_manager;
|
||||||
|
pub mod layer_state_manager;
|
||||||
pub mod plot;
|
pub mod plot;
|
||||||
pub mod plugin_manager;
|
pub mod plugin_manager;
|
||||||
pub mod shortcuts;
|
pub mod shortcuts;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue