diff --git a/ROADMAP.md b/ROADMAP.md index a0029ba3..fe3d8f42 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -153,7 +153,7 @@ Underlay (PDF/DWF/DGN) | TEXT (DT) | ✅ | | MTEXT (T) | ✅ | | DIMSTYLE yöneticisi (DIMSTYLE/DDIM) | ✅ | -| MLEADERSTYLE | ⬜ | +| MLEADERSTYLE | ✅ | | DIMORDINATE | ✅ | --- @@ -294,7 +294,7 @@ Underlay (PDF/DWF/DGN) | Solid3D tessellation (acadrust ACIS) | ✅ | | Boolean operasyonlar (UNION/SUBTRACT/INTERSECT) | ⬜ | | EXTRUDE / REVOLVE | ✅ | -| SWEEP / LOFT | ⬜ | +| SWEEP / LOFT | ✅ | | 3D ARRAY | ✅ | | STL dışa aktarma (STLOUT) | ✅ | | STEP dışa aktarma | ⬜ | diff --git a/src/app/commands.rs b/src/app/commands.rs index 7fb00840..890c1ee9 100644 --- a/src/app/commands.rs +++ b/src/app/commands.rs @@ -3579,6 +3579,11 @@ impl H7CAD { return Task::done(Message::StlExport); } + // ── Plot Style Editor GUI ───────────────────────────────────── + "PLOTSTYLEPANEL"|"PLOTSTYLEEDITOR"|"STYLESMANAGER" => { + return Task::done(Message::PlotStylePanelOpen); + } + // ── Plot / Page Setup ────────────────────────────────────────── "PRINT"|"PLOT"|"EXPORT" => { return Task::done(Message::PlotExport); diff --git a/src/app/mod.rs b/src/app/mod.rs index 30d29254..af08248d 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -112,6 +112,15 @@ pub(super) struct H7CAD { /// Edit buffer for oblique angle (degrees). textstyle_oblique: String, + // ── Plot Style Panel ────────────────────────────────────────────────── + plotstyle_panel_open: bool, + /// Selected ACI index in the panel (1-255). + plotstyle_panel_aci: u8, + /// Edit buffers for the selected entry. + ps_color_buf: String, + ps_lineweight_buf: String, + ps_screening_buf: String, + // ── DimStyle Dialog ─────────────────────────────────────────────────── dimstyle_open: bool, /// Name of the style currently shown in the dialog. @@ -371,6 +380,21 @@ pub enum Message { PlotStyleLoaded(Option), /// Clear the active plot style table. PlotStyleClear, + /// Open/close the Plot Style panel. + PlotStylePanelOpen, + PlotStylePanelClose, + /// Select an ACI entry in the panel. + PlotStylePanelSelectAci(u8), + /// Edit buffers changed. + PlotStylePanelColorBuf(String), + PlotStylePanelLwBuf(String), + PlotStylePanelScreenBuf(String), + /// Apply current edit buffers to the selected ACI entry. + PlotStylePanelApply, + /// Save the modified table back to disk. + PlotStylePanelSave, + /// Save callback. + PlotStylePanelSavePath(Option), // ── TextStyle Font Browser ──────────────────────────────────────────── TextStyleDialogOpen, TextStyleDialogClose, @@ -490,6 +514,11 @@ impl H7CAD { page_setup_scale: "Fit".to_string(), // Plot style active_plot_style: None, + plotstyle_panel_open: false, + plotstyle_panel_aci: 1, + ps_color_buf: String::new(), + ps_lineweight_buf: "255".to_string(), + ps_screening_buf: "100".to_string(), // TextStyle font browser textstyle_open: false, textstyle_selected: "Standard".to_string(), diff --git a/src/app/update.rs b/src/app/update.rs index 055127f1..d193f756 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -2603,6 +2603,112 @@ impl H7CAD { Task::none() } + // ── Plot Style Panel ────────────────────────────────────────────── + Message::PlotStylePanelOpen => { + self.plotstyle_panel_open = true; + // Initialise edit buffers for ACI 1. + self.plotstyle_panel_aci = 1; + let entry = self.active_plot_style.as_ref() + .and_then(|t| t.aci_entries.get(1)); + self.ps_color_buf = entry.and_then(|e| e.color.map(|[r,g,b]| format!("#{:02X}{:02X}{:02X}", r, g, b))).unwrap_or_default(); + self.ps_lineweight_buf = entry.map(|e| e.lineweight.to_string()).unwrap_or("255".into()); + self.ps_screening_buf = entry.map(|e| e.screening.to_string()).unwrap_or("100".into()); + Task::none() + } + Message::PlotStylePanelClose => { + self.plotstyle_panel_open = false; + Task::none() + } + Message::PlotStylePanelSelectAci(aci) => { + self.plotstyle_panel_aci = aci; + let entry = self.active_plot_style.as_ref() + .and_then(|t| t.aci_entries.get(aci as usize)); + self.ps_color_buf = entry.and_then(|e| e.color.map(|[r,g,b]| format!("#{:02X}{:02X}{:02X}", r, g, b))).unwrap_or_default(); + self.ps_lineweight_buf = entry.map(|e| e.lineweight.to_string()).unwrap_or("255".into()); + self.ps_screening_buf = entry.map(|e| e.screening.to_string()).unwrap_or("100".into()); + Task::none() + } + Message::PlotStylePanelColorBuf(s) => { self.ps_color_buf = s; Task::none() } + Message::PlotStylePanelLwBuf(s) => { self.ps_lineweight_buf = s; Task::none() } + Message::PlotStylePanelScreenBuf(s) => { self.ps_screening_buf = s; Task::none() } + + Message::PlotStylePanelApply => { + let aci = self.plotstyle_panel_aci as usize; + if let Some(table) = self.active_plot_style.as_mut() { + if let Some(entry) = table.aci_entries.get_mut(aci) { + // Parse color. + let color_str = self.ps_color_buf.trim(); + if color_str.is_empty() { + entry.color = None; + } else if color_str.starts_with('#') && color_str.len() == 7 { + let r = u8::from_str_radix(&color_str[1..3], 16).unwrap_or(0); + let g = u8::from_str_radix(&color_str[3..5], 16).unwrap_or(0); + let b = u8::from_str_radix(&color_str[5..7], 16).unwrap_or(0); + entry.color = Some([r, g, b]); + } + if let Ok(lw) = self.ps_lineweight_buf.trim().parse::() { + entry.lineweight = lw; + } + if let Ok(sc) = self.ps_screening_buf.trim().parse::() { + entry.screening = sc.min(100); + } + self.command_line.push_output(&format!("Plot style ACI {aci} updated.")); + } + } else { + // No table loaded: create an identity table and apply. + let mut table = crate::io::plot_style::PlotStyleTable::identity("Custom.ctb"); + if let Some(entry) = table.aci_entries.get_mut(aci) { + let color_str = self.ps_color_buf.trim(); + if color_str.starts_with('#') && color_str.len() == 7 { + let r = u8::from_str_radix(&color_str[1..3], 16).unwrap_or(0); + let g = u8::from_str_radix(&color_str[3..5], 16).unwrap_or(0); + let b = u8::from_str_radix(&color_str[5..7], 16).unwrap_or(0); + entry.color = Some([r, g, b]); + } + if let Ok(lw) = self.ps_lineweight_buf.trim().parse::() { entry.lineweight = lw; } + if let Ok(sc) = self.ps_screening_buf.trim().parse::() { entry.screening = sc.min(100); } + } + self.active_plot_style = Some(table); + self.command_line.push_output(&format!("Created new CTB table, ACI {aci} updated.")); + } + Task::none() + } + + Message::PlotStylePanelSave => { + if self.active_plot_style.is_none() { + self.command_line.push_error("No plot style table loaded. Load or create one first."); + return Task::none(); + } + let default_name = self.active_plot_style.as_ref() + .map(|t| t.name.clone()).unwrap_or("export.ctb".into()); + Task::perform( + async move { + rfd::AsyncFileDialog::new() + .set_title("Save Plot Style Table") + .set_file_name(&default_name) + .add_filter("Plot Style Files", &["ctb", "stb", "CTB", "STB"]) + .add_filter("All Files", &["*"]) + .save_file() + .await + .map(|h| h.path().to_path_buf()) + }, + Message::PlotStylePanelSavePath, + ) + } + + Message::PlotStylePanelSavePath(Some(path)) => { + if let Some(table) = &self.active_plot_style { + match table.save(&path) { + Ok(()) => self.command_line.push_output(&format!( + "Plot style table saved to \"{}\".", path.display() + )), + Err(e) => self.command_line.push_error(&format!("Save error: {e}")), + } + } + Task::none() + } + Message::PlotStylePanelSavePath(None) => Task::none(), + // ── TextStyle Font Browser ──────────────────────────────────────── Message::TextStyleDialogOpen => { let i = self.active_tab; diff --git a/src/app/view.rs b/src/app/view.rs index ccd2413b..bc524abb 100644 --- a/src/app/view.rs +++ b/src/app/view.rs @@ -311,6 +311,19 @@ impl H7CAD { iced::widget::Space::new().width(0).height(0).into() }; + // ── Plot Style Panel ────────────────────────────────────────────── + let plotstyle_layer: Element<'_, Message> = if self.plotstyle_panel_open { + plotstyle_panel_overlay( + self.active_plot_style.as_ref(), + self.plotstyle_panel_aci, + &self.ps_color_buf, + &self.ps_lineweight_buf, + &self.ps_screening_buf, + ) + } 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 @@ -339,7 +352,7 @@ impl H7CAD { } }; - stack![main_ui, self.app_menu.view(), snap_layer, dropdown_layer, layout_ctx_layer, page_setup_layer, textstyle_layer, tablestyle_layer, mlstyle_layer, dimstyle_layer, viewport_ctx_layer].into() + stack![main_ui, self.app_menu.view(), snap_layer, dropdown_layer, layout_ctx_layer, page_setup_layer, textstyle_layer, tablestyle_layer, mlstyle_layer, plotstyle_layer, dimstyle_layer, viewport_ctx_layer].into() } pub fn subscription(&self) -> Subscription { @@ -1753,3 +1766,220 @@ fn mlstyle_overlay<'a>( stack![catcher, positioned].into() } + +// ── Plot Style Panel ─────────────────────────────────────────────────────── + +fn plotstyle_panel_overlay<'a>( + table: Option<&'a crate::io::plot_style::PlotStyleTable>, + selected_aci: u8, + color_buf: &'a str, + lw_buf: &'a str, + screen_buf: &'a str, +) -> Element<'a, Message> { + use iced::Length::Fill; + use iced::Color; + use iced::widget::scrollable; + + 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() + }; + + let field_style = |_: &Theme, _: text_input::Status| text_input::Style { + background: Background::Color(Color { r: 0.10, g: 0.10, b: 0.10, a: 1.0 }), + border: Border { color: BORDER, width: 1.0, radius: 3.0.into() }, + icon: TEXT_COL, + placeholder: DIM_COL, + value: TEXT_COL, + selection: ACCENT, + }; + + // ACI color list (1..=255) + let aci_items: Vec> = (1u8..=255).map(|aci| { + let is_sel = aci == selected_aci; + let has_override = table.and_then(|t| t.aci_entries.get(aci as usize)) + .map(|e| e.color.is_some() || e.lineweight != 255 || e.screening != 100) + .unwrap_or(false); + let lw_str = table.and_then(|t| t.aci_entries.get(aci as usize)) + .and_then(|e| { + if e.lineweight != 255 { + crate::io::plot_style::LW_TABLE.get(e.lineweight as usize) + .map(|lw| format!("{:.2}mm", lw)) + } else { None } + }).unwrap_or_default(); + let color_str = table.and_then(|t| t.aci_entries.get(aci as usize)) + .and_then(|e| e.color.map(|[r, g, b]| format!("#{:02X}{:02X}{:02X}", r, g, b))) + .unwrap_or_default(); + let label = if has_override { + format!("{aci:>3} {color_str:<9} {lw_str}") + } else { + format!("{aci:>3} (default)") + }; + button(text(label).size(10).color(TEXT_COL).font(iced::Font::MONOSPACE)) + .on_press(Message::PlotStylePanelSelectAci(aci)) + .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([2, 8]) + .width(Fill) + .into() + }).collect(); + + let list_panel = container( + scrollable(column(aci_items).spacing(1)).height(300) + ) + .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(260); + + // Right panel: edit selected entry + let entry = table.and_then(|t| t.aci_entries.get(selected_aci as usize)); + let edit_panel: Element<'_, Message> = { + let lbl = |s: &'static str| text(s).size(11).color(DIM_COL); + column![ + row![ + text("ACI:").size(11).color(DIM_COL).width(90), + text(format!("{selected_aci}")).size(11).color(TEXT_COL), + ].spacing(8).align_y(iced::Center), + lbl("Color override (#RRGGBB):"), + text_input("#RRGGBB or blank", color_buf) + .on_input(Message::PlotStylePanelColorBuf) + .style(field_style) + .size(11) + .padding([4, 8]), + lbl("Lineweight index (0-24, 255=obj):"), + text_input("255", lw_buf) + .on_input(Message::PlotStylePanelLwBuf) + .style(field_style) + .size(11) + .padding([4, 8]), + lbl("Screening (0-100):"), + text_input("100", screen_buf) + .on_input(Message::PlotStylePanelScreenBuf) + .style(field_style) + .size(11) + .padding([4, 8]), + Space::new().height(8), + { + let cur_color = entry.and_then(|e| e.color.map(|[r,g,b]| format!("#{:02X}{:02X}{:02X}", r,g,b))).unwrap_or("(none)".into()); + let cur_lw = entry.map(|e| if e.lineweight == 255 { "object".into() } else { + crate::io::plot_style::LW_TABLE.get(e.lineweight as usize) + .map(|lw| format!("{:.2}mm (idx {})", lw, e.lineweight)) + .unwrap_or_else(|| format!("idx {}", e.lineweight)) + }).unwrap_or("—".into()); + let cur_scr = entry.map(|e| format!("{}%", e.screening)).unwrap_or("—".into()); + let vals: Element<'_, Message> = column![ + text("Current values:").size(10).color(DIM_COL), + text(format!(" Color: {cur_color}")).size(10).color(TEXT_COL), + text(format!(" Lweight: {cur_lw}")).size(10).color(TEXT_COL), + text(format!(" Screening: {cur_scr}")).size(10).color(TEXT_COL), + ].spacing(3).into(); + vals + }, + Space::new().height(Fill), + button(text("Apply to ACI").size(11)) + .on_press(Message::PlotStylePanelApply) + .style(btn_style(true)) + .padding([5, 10]), + ] + .spacing(6) + .width(220) + .height(300) + .into() + }; + + let table_name = table.map(|t| t.name.as_str()).unwrap_or("(no table loaded)"); + + let panel = container( + column![ + row![ + text("Plot Style Table Editor").size(13).color(TEXT_COL), + Space::new().width(Fill), + button(text("✕").size(12).color(DIM_COL)) + .on_press(Message::PlotStylePanelClose) + .style(|_: &Theme, _| button::Style { + background: Some(Background::Color(Color::TRANSPARENT)), + text_color: DIM_COL, + ..Default::default() + }) + .padding([2, 6]), + ].align_y(iced::Center), + text(format!("Table: {table_name}")).size(11).color(DIM_COL), + Space::new().height(8), + row![ + list_panel, + Space::new().width(12), + edit_panel, + ].align_y(iced::alignment::Vertical::Top), + Space::new().height(8), + row![ + button(text("Load CTB/STB").size(11)) + .on_press(Message::PlotStyleLoad) + .style(btn_style(false)) + .padding([5, 10]), + button(text("Save As…").size(11)) + .on_press(Message::PlotStylePanelSave) + .style(btn_style(false)) + .padding([5, 10]), + button(text("Clear Table").size(11)) + .on_press(Message::PlotStyleClear) + .style(btn_style(false)) + .padding([5, 10]), + Space::new().width(Fill), + button(text("Close").size(11)) + .on_press(Message::PlotStylePanelClose) + .style(btn_style(false)) + .padding([5, 10]), + ].spacing(8), + ] + .spacing(6) + .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(520) + .height(430); + + let catcher = mouse_area(container(Space::new().width(Fill).height(Fill)) + .width(Fill).height(Fill) + .style(|_: &Theme| container::Style { + background: Some(Background::Color(Color { r: 0.0, g: 0.0, b: 0.0, a: 0.45 })), + ..Default::default() + }) + ).on_press(Message::PlotStylePanelClose); + + let positioned = container(panel) + .width(Fill).height(Fill) + .align_x(iced::Alignment::Center) + .align_y(iced::Alignment::Center); + + stack![catcher, positioned].into() +}