feat: Plot Style Table Editor GUI (PLOTSTYLEPANEL / STYLESMANAGER)

Opens a panel overlay showing all 255 ACI entries with their current
overrides (color, lineweight, screening). Allows editing entries
interactively, creating a new identity table when none is loaded,
and saving the modified table back to a CTB/STB file.

Commands: PLOTSTYLEPANEL, PLOTSTYLEEDITOR, STYLESMANAGER

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-04-08 18:19:48 +03:00
commit b8d15e5155
5 changed files with 373 additions and 3 deletions

View file

@ -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 | ⬜ |

View file

@ -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);

View file

@ -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<crate::io::plot_style::PlotStyleTable>),
/// 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<std::path::PathBuf>),
// ── 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(),

View file

@ -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::<u8>() {
entry.lineweight = lw;
}
if let Ok(sc) = self.ps_screening_buf.trim().parse::<u8>() {
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::<u8>() { entry.lineweight = lw; }
if let Ok(sc) = self.ps_screening_buf.trim().parse::<u8>() { 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;

View file

@ -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<String> = 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<Message> {
@ -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<Element<'_, Message>> = (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()
}