From 34076b26bb33f72a91387207f0f7879c596e7855 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Mon, 6 Apr 2026 14:54:05 +0300 Subject: [PATCH] feat: MLineStyle manager (MLSTYLE command + dialog) - MLSTYLE command: LIST, NEW, SET, DEL subcommands - Multiline Style Manager dialog: style list, element details (offset, color, linetype), Set Current / New / Delete buttons - MLINE/ML command now reads document header.multiline_style to apply the current style to newly created MLine entities - MlineCommand.with_style() constructor for style-aware multiline drawing Co-Authored-By: Claude Sonnet 4.6 --- src/app/commands.rs | 112 ++++++++++++++++++++- src/app/mod.rs | 14 +++ src/app/update.rs | 92 +++++++++++++++++ src/app/view.rs | 179 ++++++++++++++++++++++++++++++++- src/modules/home/draw/mline.rs | 16 ++- 5 files changed, 407 insertions(+), 6 deletions(-) diff --git a/src/app/commands.rs b/src/app/commands.rs index 1aaf996f2..696df755f 100644 --- a/src/app/commands.rs +++ b/src/app/commands.rs @@ -392,7 +392,8 @@ impl H7CAD { "MLINE"|"ML" => { use crate::modules::home::draw::mline::MlineCommand; - let cmd_obj = MlineCommand::new(); + let style = self.tabs[i].scene.document.header.multiline_style.clone(); + let cmd_obj = MlineCommand::with_style(style); self.command_line.push_info(&cmd_obj.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd_obj)); } @@ -2084,6 +2085,115 @@ impl H7CAD { } // ── DimStyle management ─────────────────────────────────────── + // MLSTYLE — Multiline Style Manager. + // Usage: + // MLSTYLE — open dialog + // MLSTYLE LIST / ? — list all multiline styles + // MLSTYLE NEW — create a new style + // MLSTYLE SET — set current multiline style + // MLSTYLE DEL — delete a style (not Standard) + cmd if cmd == "MLSTYLE" || cmd.starts_with("MLSTYLE ") => { + use acadrust::objects::{MLineStyle, ObjectType}; + let raw_rest = cmd.split_once(' ').map(|(_, r)| r.trim()).unwrap_or(""); + let parts: Vec<&str> = raw_rest.split_whitespace().collect(); + let sub = parts.first().map(|s| s.to_uppercase()).unwrap_or_default(); + match sub.as_str() { + "" | "DIALOG" | "UI" => { + return Task::done(Message::MlStyleDialogOpen); + } + "LIST" | "?" => { + let doc = &self.tabs[i].scene.document; + let current = &doc.header.multiline_style; + let styles: Vec = doc.objects.values() + .filter_map(|o| if let ObjectType::MLineStyle(s) = o { Some(s) } else { None }) + .map(|s| { + let cur = if &s.name == current { " (current)" } else { "" }; + format!("{} [{}]{}", + s.name, + s.elements.len(), + cur) + }) + .collect(); + if styles.is_empty() { + self.command_line.push_output("No multiline styles."); + } else { + self.command_line.push_output(&format!("MLineStyles:\n {}", styles.join("\n "))); + } + } + "NEW" | "N" => { + let name = parts.get(1).copied().unwrap_or("").to_string(); + if name.is_empty() { + self.command_line.push_error("Usage: MLSTYLE NEW "); + } else { + let doc = &self.tabs[i].scene.document; + let exists = doc.objects.values().any(|o| { + matches!(o, ObjectType::MLineStyle(s) if s.name.eq_ignore_ascii_case(&name)) + }); + if exists { + self.command_line.push_error(&format!("MLSTYLE: '{}' already exists.", name)); + } else { + self.push_undo_snapshot(i, "MLSTYLE NEW"); + let mut style = MLineStyle::standard(); + style.name = name.clone(); + let nh = acadrust::Handle::new(self.tabs[i].scene.document.next_handle()); + style.handle = nh; + self.tabs[i].scene.document.objects.insert( + nh, ObjectType::MLineStyle(style) + ); + self.tabs[i].dirty = true; + self.command_line.push_output(&format!("MLSTYLE: '{}' created.", name)); + } + } + } + "SET" | "S" => { + let name = parts.get(1).copied().unwrap_or("").to_string(); + if name.is_empty() { + self.command_line.push_error("Usage: MLSTYLE SET "); + } else { + let doc = &self.tabs[i].scene.document; + let exists = doc.objects.values().any(|o| { + matches!(o, ObjectType::MLineStyle(s) if s.name.eq_ignore_ascii_case(&name)) + }); + if exists { + self.push_undo_snapshot(i, "MLSTYLE SET"); + self.tabs[i].scene.document.header.multiline_style = name.clone(); + self.tabs[i].dirty = true; + self.command_line.push_output(&format!("MLSTYLE: current style set to '{}'.", name)); + } else { + self.command_line.push_error(&format!("MLSTYLE: '{}' not found.", name)); + } + } + } + "DEL" | "DELETE" => { + let name = parts.get(1).copied().unwrap_or("").to_string(); + if name.is_empty() || name.eq_ignore_ascii_case("Standard") { + self.command_line.push_error("Cannot delete the Standard style."); + } else { + let doc = &self.tabs[i].scene.document; + let handle = doc.objects.iter() + .find_map(|(&h, o)| { + if let ObjectType::MLineStyle(s) = o { + if s.name.eq_ignore_ascii_case(&name) { Some(h) } else { None } + } else { None } + }); + if let Some(h) = handle { + self.push_undo_snapshot(i, "MLSTYLE DEL"); + self.tabs[i].scene.document.objects.remove(&h); + self.tabs[i].dirty = true; + self.command_line.push_output(&format!("MLSTYLE: '{}' deleted.", name)); + } else { + self.command_line.push_error(&format!("MLSTYLE: '{}' not found.", name)); + } + } + } + _ => { + self.command_line.push_error( + "Usage: MLSTYLE [LIST|NEW |SET |DEL ]" + ); + } + } + } + cmd if cmd == "DIMSTYLE" || cmd == "DDIM" || cmd.starts_with("DIMSTYLE ") || cmd.starts_with("DDIM ") => { use acadrust::tables::DimStyle; let raw_rest = cmd.split_once(' ').map(|(_, r)| r.trim()).unwrap_or(""); diff --git a/src/app/mod.rs b/src/app/mod.rs index 5b083462b..2d9fadfc6 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -90,6 +90,10 @@ pub(super) struct H7CAD { /// Currently loaded CTB/STB table (None = no override). active_plot_style: Option, + // ── MLineStyle Dialog ───────────────────────────────────────────────── + mlstyle_open: bool, + mlstyle_selected: String, + // ── DimStyle Dialog ─────────────────────────────────────────────────── dimstyle_open: bool, /// Name of the style currently shown in the dialog. @@ -335,6 +339,13 @@ pub enum Message { PlotStyleLoaded(Option), /// Clear the active plot style table. PlotStyleClear, + // ── MLineStyle Dialog ───────────────────────────────────────────────── + MlStyleDialogOpen, + MlStyleDialogClose, + MlStyleDialogSelect(String), + MlStyleDialogSetCurrent, + MlStyleDialogNew, + MlStyleDialogDelete, // ── DimStyle Dialog ─────────────────────────────────────────────────── DimStyleDialogOpen, DimStyleDialogClose, @@ -396,6 +407,9 @@ impl H7CAD { page_setup_scale: "Fit".to_string(), // Plot style active_plot_style: None, + // MLineStyle dialog + mlstyle_open: false, + mlstyle_selected: "Standard".to_string(), // DimStyle dialog dimstyle_open: false, dimstyle_selected: "Standard".to_string(), diff --git a/src/app/update.rs b/src/app/update.rs index 647af0668..8546979f2 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -2126,6 +2126,98 @@ impl H7CAD { Task::none() } + // ── MLineStyle Dialog ───────────────────────────────────────────── + Message::MlStyleDialogOpen => { + use acadrust::objects::ObjectType; + let i = self.active_tab; + let cur = self.tabs[i].scene.document.header.multiline_style.clone(); + let exists = self.tabs[i].scene.document.objects.values().any(|o| { + matches!(o, ObjectType::MLineStyle(s) if s.name == cur) + }); + self.mlstyle_selected = if exists { + cur + } else { + self.tabs[i].scene.document.objects.values() + .find_map(|o| if let ObjectType::MLineStyle(s) = o { Some(s.name.clone()) } else { None }) + .unwrap_or_else(|| "Standard".to_string()) + }; + self.mlstyle_open = true; + Task::none() + } + Message::MlStyleDialogClose => { + self.mlstyle_open = false; + Task::none() + } + Message::MlStyleDialogSelect(name) => { + self.mlstyle_selected = name; + Task::none() + } + Message::MlStyleDialogSetCurrent => { + use acadrust::objects::ObjectType; + let i = self.active_tab; + let name = self.mlstyle_selected.clone(); + let exists = self.tabs[i].scene.document.objects.values().any(|o| { + matches!(o, ObjectType::MLineStyle(s) if s.name == name) + }); + if exists { + self.push_undo_snapshot(i, "MLSTYLE SET"); + self.tabs[i].scene.document.header.multiline_style = name.clone(); + self.tabs[i].dirty = true; + self.command_line.push_output(&format!("Current multiline style: {}", name)); + } + Task::none() + } + Message::MlStyleDialogNew => { + use acadrust::objects::ObjectType; + let i = self.active_tab; + // Generate a unique name. + let doc = &self.tabs[i].scene.document; + let mut n = 1u32; + let base = "MLS"; + let new_name = loop { + let candidate = format!("{}{}", base, n); + let taken = doc.objects.values().any(|o| { + matches!(o, ObjectType::MLineStyle(s) if s.name.eq_ignore_ascii_case(&candidate)) + }); + if !taken { break candidate; } + n += 1; + }; + self.push_undo_snapshot(i, "MLSTYLE NEW"); + let mut style = acadrust::objects::MLineStyle::standard(); + style.name = new_name.clone(); + let nh = acadrust::Handle::new(self.tabs[i].scene.document.next_handle()); + style.handle = nh; + self.tabs[i].scene.document.objects.insert(nh, ObjectType::MLineStyle(style)); + self.mlstyle_selected = new_name; + self.tabs[i].dirty = true; + Task::none() + } + Message::MlStyleDialogDelete => { + use acadrust::objects::ObjectType; + let i = self.active_tab; + let name = self.mlstyle_selected.clone(); + if name.eq_ignore_ascii_case("Standard") { + self.command_line.push_error("Cannot delete the Standard style."); + return Task::none(); + } + let handle = self.tabs[i].scene.document.objects.iter() + .find_map(|(&h, o)| { + if let ObjectType::MLineStyle(s) = o { + if s.name == name { Some(h) } else { None } + } else { None } + }); + if let Some(h) = handle { + self.push_undo_snapshot(i, "MLSTYLE DEL"); + self.tabs[i].scene.document.objects.remove(&h); + // Select first remaining style. + self.mlstyle_selected = self.tabs[i].scene.document.objects.values() + .find_map(|o| if let ObjectType::MLineStyle(s) = o { Some(s.name.clone()) } else { None }) + .unwrap_or_else(|| "Standard".to_string()); + self.tabs[i].dirty = true; + } + Task::none() + } + // ── DimStyle Dialog ─────────────────────────────────────────────── Message::DimStyleDialogOpen => { let i = self.active_tab; diff --git a/src/app/view.rs b/src/app/view.rs index 6b7927b7e..afa7d7e9c 100644 --- a/src/app/view.rs +++ b/src/app/view.rs @@ -227,6 +227,21 @@ impl H7CAD { iced::widget::Space::new().width(0).height(0).into() }; + let mlstyle_layer: Element<'_, Message> = if self.mlstyle_open { + use acadrust::objects::ObjectType; + let tab = &self.tabs[self.active_tab]; + let styles: Vec = tab.scene.document.objects.values() + .filter_map(|o| if let ObjectType::MLineStyle(s) = o { Some(s.name.clone()) } else { None }) + .collect(); + let selected_style = tab.scene.document.objects.values() + .find_map(|o| if let ObjectType::MLineStyle(s) = o { + if s.name == self.mlstyle_selected { Some(s) } else { None } + } else { None }); + mlstyle_overlay(styles, &self.mlstyle_selected, selected_style, tab.scene.document.header.multiline_style.clone()) + } else { + iced::widget::Space::new().width(0).height(0).into() + }; + let dimstyle_layer: Element<'_, Message> = if self.dimstyle_open { let tab = &self.tabs[self.active_tab]; let styles: Vec = tab.scene.document.dim_styles @@ -241,7 +256,7 @@ impl H7CAD { iced::widget::Space::new().width(0).height(0).into() }; - stack![main_ui, self.app_menu.view(), snap_layer, dropdown_layer, layout_ctx_layer, page_setup_layer, dimstyle_layer].into() + stack![main_ui, self.app_menu.view(), snap_layer, dropdown_layer, layout_ctx_layer, page_setup_layer, mlstyle_layer, dimstyle_layer].into() } pub fn subscription(&self) -> Subscription { @@ -1021,3 +1036,165 @@ fn dimstyle_overlay<'a>( stack![catcher, positioned].into() } + +// ── MLineStyle Dialog overlay ─────────────────────────────────────────────── + +fn mlstyle_overlay<'a>( + styles: Vec, + selected: &'a str, + selected_style: Option<&'a acadrust::objects::MLineStyle>, + current_style: String, +) -> Element<'a, Message> { + use iced::Length::Shrink; + + const PANEL_BG: Color = Color { r: 0.15, g: 0.15, b: 0.15, a: 1.0 }; + const BORDER: Color = Color { r: 0.35, g: 0.35, b: 0.35, a: 1.0 }; + const TEXT_COL: Color = Color { r: 0.88, g: 0.88, b: 0.88, a: 1.0 }; + const DIM_COL: Color = Color { r: 0.55, g: 0.55, b: 0.55, a: 1.0 }; + const ACCENT: Color = Color { r: 0.25, g: 0.50, b: 0.85, a: 1.0 }; + const ACTIVE_BG: Color = Color { r: 0.20, g: 0.40, b: 0.70, a: 1.0 }; + + let btn_style = |accent: bool| move |_: &Theme, status: button::Status| button::Style { + background: Some(Background::Color(match (accent, status) { + (true, button::Status::Hovered | button::Status::Pressed) => + Color { r: 0.20, g: 0.42, b: 0.72, a: 1.0 }, + (false, button::Status::Hovered | button::Status::Pressed) => + Color { r: 0.28, g: 0.28, b: 0.28, a: 1.0 }, + (true, _) => ACCENT, + _ => Color { r: 0.22, g: 0.22, b: 0.22, a: 1.0 }, + })), + text_color: TEXT_COL, + border: Border { color: BORDER, width: 1.0, radius: 4.0.into() }, + ..Default::default() + }; + + // Style list. + let style_items: Vec> = styles.iter().map(|name| { + let is_sel = name.as_str() == selected; + let is_cur = *name == current_style; + let label = if is_cur { + format!("{} ◀", name) + } else { + name.clone() + }; + button(text(label).size(11).color(TEXT_COL)) + .on_press(Message::MlStyleDialogSelect(name.clone())) + .style(move |_: &Theme, st| button::Style { + background: Some(Background::Color(match (is_sel, st) { + (true, _) => ACTIVE_BG, + (false, button::Status::Hovered | button::Status::Pressed) => + Color { r: 0.26, g: 0.26, b: 0.26, a: 1.0 }, + _ => Color::TRANSPARENT, + })), + text_color: TEXT_COL, + ..Default::default() + }) + .padding([3, 8]) + .width(Fill) + .into() + }).collect(); + + let style_panel = container( + column(style_items).spacing(2) + ) + .style(|_: &Theme| container::Style { + background: Some(Background::Color(Color { r: 0.12, g: 0.12, b: 0.12, a: 1.0 })), + border: Border { color: BORDER, width: 1.0, radius: 4.0.into() }, + ..Default::default() + }) + .padding(4) + .width(160) + .height(240); + + // Right panel: details for selected style. + let details: Element<'_, Message> = if let Some(s) = selected_style { + let info_row = |label: &'static str, val: String| -> Element<'_, Message> { + row![ + text(label).size(11).color(DIM_COL).width(110), + text(val).size(11).color(TEXT_COL), + ].spacing(8).align_y(iced::Center).into() + }; + let elem_rows: Vec> = s.elements.iter().enumerate().map(|(idx, e)| { + let color_str: String = match &e.color { + acadrust::types::Color::ByLayer => "ByLayer".into(), + acadrust::types::Color::ByBlock => "ByBlock".into(), + acadrust::types::Color::Index(i) => format!("ACI {}", i), + acadrust::types::Color::Rgb { r, g, b } => format!("#{:02X}{:02X}{:02X}", r, g, b), + }; + let lt = if e.linetype.is_empty() { "ByLayer" } else { e.linetype.as_str() }; + row![ + text(format!(" {}:", idx)).size(10).color(DIM_COL).width(20), + text(format!("{:+.3}", e.offset)).size(10).color(TEXT_COL).width(60), + text(color_str).size(10).color(TEXT_COL).width(80), + text(lt).size(10).color(TEXT_COL), + ].spacing(4).align_y(iced::Center).into() + }).collect(); + + let mut col_items: Vec> = vec![ + info_row("Name:", s.name.clone()), + info_row("Elements:", s.elements.len().to_string()), + text(" Off Color Ltype").size(10).color(DIM_COL).into(), + ]; + col_items.extend(elem_rows); + column(col_items).spacing(6).into() + } else { + text("No style selected.").size(11).color(DIM_COL).into() + }; + + let right_panel = column![ + details, + Space::new().height(Fill), + row![ + button(text("Set Current").size(11)) + .on_press(Message::MlStyleDialogSetCurrent) + .style(btn_style(true)) + .padding([5, 10]), + button(text("New").size(11)) + .on_press(Message::MlStyleDialogNew) + .style(btn_style(false)) + .padding([5, 10]), + button(text("Delete").size(11)) + .on_press(Message::MlStyleDialogDelete) + .style(btn_style(false)) + .padding([5, 10]), + ].spacing(6), + ] + .spacing(10) + .width(280) + .height(240); + + let panel = container( + column![ + row![ + text("Multiline Style Manager").size(13).color(TEXT_COL), + Space::new().width(Fill), + button(text("✕").size(12).color(DIM_COL)) + .on_press(Message::MlStyleDialogClose) + .style(|_: &Theme, _| button::Style { + background: Some(Background::Color(Color::TRANSPARENT)), + text_color: DIM_COL, + ..Default::default() + }) + .padding([2, 6]), + ].align_y(iced::Center), + row![style_panel, right_panel].spacing(12).align_y(iced::Top), + ].spacing(10).padding(16) + ) + .style(|_: &Theme| container::Style { + background: Some(Background::Color(PANEL_BG)), + border: Border { color: BORDER, width: 1.0, radius: 6.0.into() }, + ..Default::default() + }) + .width(Shrink); + + let catcher = mouse_area( + container(iced::widget::Space::new().width(Fill).height(Fill)) + ).on_press(Message::MlStyleDialogClose); + + let positioned = container(panel) + .width(Fill).height(Fill) + .align_x(iced::Alignment::Center) + .align_y(iced::Alignment::Center); + + stack![catcher, positioned].into() +} diff --git a/src/modules/home/draw/mline.rs b/src/modules/home/draw/mline.rs index da290bfeb..37c9d5fa9 100644 --- a/src/modules/home/draw/mline.rs +++ b/src/modules/home/draw/mline.rs @@ -17,11 +17,17 @@ pub struct MlineCommand { points: Vec, scale: f64, waiting_scale: bool, + style_name: String, } impl MlineCommand { + #[allow(dead_code)] pub fn new() -> Self { - Self { points: vec![], scale: 1.0, waiting_scale: false } + Self { points: vec![], scale: 1.0, waiting_scale: false, style_name: "Standard".into() } + } + + pub fn with_style(style_name: impl Into) -> Self { + Self { points: vec![], scale: 1.0, waiting_scale: false, style_name: style_name.into() } } } @@ -55,7 +61,7 @@ impl CadCommand for MlineCommand { // Close command if (up == "C" || up == "CLOSE") && self.points.len() >= 3 { - let entity = build_mline(&self.points, self.scale, true); + let entity = build_mline(&self.points, self.scale, true, &self.style_name); return Some(CmdResult::CommitAndExit(entity)); } @@ -85,7 +91,7 @@ impl CadCommand for MlineCommand { if self.points.len() < 2 { return CmdResult::Cancel; } - let entity = build_mline(&self.points, self.scale, false); + let entity = build_mline(&self.points, self.scale, false, &self.style_name); CmdResult::CommitAndExit(entity) } @@ -104,12 +110,13 @@ impl CadCommand for MlineCommand { line_weight_px: 1.0, snap_pts: vec![], tangent_geoms: vec![], + aci: 0, key_vertices: vec![], }) } } -fn build_mline(pts: &[Vec3], scale: f64, closed: bool) -> EntityType { +fn build_mline(pts: &[Vec3], scale: f64, closed: bool, style_name: &str) -> EntityType { let verts: Vec = pts.iter() .map(|p| Vector3::new(p.x as f64, p.z as f64, p.y as f64)) .collect(); @@ -119,5 +126,6 @@ fn build_mline(pts: &[Vec3], scale: f64, closed: bool) -> EntityType { MLine::from_points(&verts) }; mline.scale_factor = scale; + mline.style_name = style_name.to_string(); EntityType::MLine(mline) }