diff --git a/topola-egui/src/app.rs b/topola-egui/src/app.rs index 17ad969..1331393 100644 --- a/topola-egui/src/app.rs +++ b/topola-egui/src/app.rs @@ -128,6 +128,10 @@ impl eframe::App for App { self.update_state(); + if let Some(ref mut workspace) = self.workspace { + workspace.update_appearance_panel(ctx); + } + self.viewport.update(ctx, self.workspace.as_mut()); self.update_locale(); diff --git a/topola-egui/src/appearance_panel.rs b/topola-egui/src/appearance_panel.rs new file mode 100644 index 0000000..f79c048 --- /dev/null +++ b/topola-egui/src/appearance_panel.rs @@ -0,0 +1,217 @@ +// SPDX-FileCopyrightText: 2026 Topola contributors +// +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use std::collections::BTreeMap; + +use egui::{Context, Grid, ScrollArea, SidePanel, widget_text::WidgetText}; +use serde::{Deserialize, Serialize}; +use topola::Board; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Colors { + pub layers: LayerColors, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct LayerColors { + default: LayerColor, + colors: BTreeMap, +} + +impl LayerColors { + pub fn color(&self, layer_name: Option<&str>) -> &LayerColor { + layer_name + .map(|layername| self.colors.get(layername).unwrap_or(&self.default)) + .unwrap_or(&self.default) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct LayerColor { + pub normal: egui::Color32, + pub highlighted: egui::Color32, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct AppearancePanel { + // TODO: + // In1.Cu shall be #7fc87f (#d5ecd5 when selected). + // In2.Cu shall be #ce7d2c (#e8c39e when selected). + pub dark_colors: Colors, + pub light_colors: Colors, + + #[serde(skip)] + pub visible: Box<[bool]>, +} + +impl AppearancePanel { + pub fn new(board: &Board) -> Self { + let dark_colors = Colors { + layers: LayerColors { + default: LayerColor { + normal: egui::Color32::from_rgb(255, 255, 255), + highlighted: egui::Color32::from_rgb(255, 255, 255), + }, + colors: BTreeMap::from([ + ( + "F.Cu".to_string(), + LayerColor { + normal: egui::Color32::from_rgb(255, 52, 52), + highlighted: egui::Color32::from_rgb(255, 100, 100), + }, + ), + ( + "1".to_string(), + LayerColor { + normal: egui::Color32::from_rgb(255, 52, 52), + highlighted: egui::Color32::from_rgb(255, 100, 100), + }, + ), + ( + "B.Cu".to_string(), + LayerColor { + normal: egui::Color32::from_rgb(52, 52, 255), + highlighted: egui::Color32::from_rgb(100, 100, 255), + }, + ), + ( + "2".to_string(), + LayerColor { + normal: egui::Color32::from_rgb(52, 52, 255), + highlighted: egui::Color32::from_rgb(100, 100, 255), + }, + ), + ( + "In1.Cu".to_string(), + LayerColor { + normal: egui::Color32::from_rgb(127, 200, 127), + highlighted: egui::Color32::from_rgb(213, 236, 213), + }, + ), + ( + "In2.Cu".to_string(), + LayerColor { + normal: egui::Color32::from_rgb(206, 125, 44), + highlighted: egui::Color32::from_rgb(232, 195, 158), + }, + ), + ]), + }, + }; + let light_colors = Colors { + layers: LayerColors { + default: LayerColor { + normal: egui::Color32::from_rgb(0, 0, 0), + highlighted: egui::Color32::from_rgb(0, 0, 0), + }, + colors: BTreeMap::from([ + ( + "F.Cu".to_string(), + LayerColor { + normal: egui::Color32::from_rgb(255, 27, 27), + highlighted: egui::Color32::from_rgb(255, 52, 52), + }, + ), + ( + "1".to_string(), + LayerColor { + normal: egui::Color32::from_rgb(255, 27, 27), + highlighted: egui::Color32::from_rgb(255, 52, 52), + }, + ), + ( + "B.Cu".to_string(), + LayerColor { + normal: egui::Color32::from_rgb(27, 27, 255), + highlighted: egui::Color32::from_rgb(52, 52, 255), + }, + ), + ( + "2".to_string(), + LayerColor { + normal: egui::Color32::from_rgb(27, 27, 255), + highlighted: egui::Color32::from_rgb(52, 52, 255), + }, + ), + ( + "In1.Cu".to_string(), + LayerColor { + normal: egui::Color32::from_rgb(76, 169, 76), + highlighted: egui::Color32::from_rgb(127, 200, 127), + }, + ), + ( + "In2.Cu".to_string(), + LayerColor { + normal: egui::Color32::from_rgb(183, 80, 12), + highlighted: egui::Color32::from_rgb(206, 125, 44), + }, + ), + ]), + }, + }; + + let layer_count = board.layout().layer_count(); + let visible = core::iter::repeat(true) + .take(*layer_count) + .collect::>(); + Self { + dark_colors, + light_colors, + visible, + } + } + + pub fn update(&mut self, ctx: &Context, board: &Board) { + SidePanel::right("appearance_panel").show(ctx, |ui| { + ui.label("Layers"); + let row_height = ui.spacing().interact_size.y; + ScrollArea::vertical().show_rows( + ui, + row_height, + self.visible.len(), + |ui, row_range| { + let start_row = row_range.start; + Grid::new("appearance_layers") + .min_col_width(ui.spacing().icon_width) + .num_columns(3) + .start_row(start_row) + .show(ui, |ui| { + for (layer, visible) in self.visible[row_range].iter_mut().enumerate() { + let layer = layer + start_row; + let layer_name = board.layer_name(layer); + + // unnamed layers can't be used for routing + /*if layer_name.is_some() { + ui.radio_value( + &mut options.planar.principal_layer, + layer, + WidgetText::default(), + ); + } else { + // dummy item to bump the grid + ui.label(""); + }*/ + + ui.checkbox(visible, WidgetText::default()); + ui.label( + layer_name + .map(|i| i.to_string()) + .unwrap_or_else(|| format!("{} - Unnamed layer", layer)), + ); + ui.end_row(); + } + }) + }, + ); + }); + } + + pub fn colors(&self, ctx: &Context) -> &Colors { + match ctx.theme() { + egui::Theme::Dark => &self.dark_colors, + egui::Theme::Light => &self.light_colors, + } + } +} diff --git a/topola-egui/src/displayer.rs b/topola-egui/src/displayer.rs index 146991f..c3c27e6 100644 --- a/topola-egui/src/displayer.rs +++ b/topola-egui/src/displayer.rs @@ -46,24 +46,58 @@ impl Displayer { ); for (_, joint) in workspace.board.layout().joints().collection() { - self.paint_joint(ctx, ui, viewport, joint); + if workspace.appearance_panel.visible[joint.layer] { + self.paint_joint( + ctx, + ui, + viewport, + joint, + workspace + .appearance_panel + .colors(ctx) + .layers + .color(workspace.board.layer_name(joint.layer)) + .normal, + ); + } } for (i, segment) in workspace.board.layout().segments().collection() { - self.paint_segment( - ctx, - ui, - viewport, - segment, - workspace - .board - .layout() - .segment_endpoints(SegmentId::new(i)), - ); + if workspace.appearance_panel.visible[segment.layer] { + self.paint_segment( + ctx, + ui, + viewport, + segment, + workspace + .board + .layout() + .segment_endpoints(SegmentId::new(i)), + workspace + .appearance_panel + .colors(ctx) + .layers + .color(workspace.board.layer_name(segment.layer)) + .normal, + ); + } } for (_, polygon) in workspace.board.layout().polygons().collection() { - self.paint_polygon(ctx, ui, viewport, polygon); + if workspace.appearance_panel.visible[polygon.layer] { + self.paint_polygon( + ctx, + ui, + viewport, + polygon, + workspace + .appearance_panel + .colors(ctx) + .layers + .color(workspace.board.layer_name(polygon.layer)) + .normal, + ); + } } } @@ -73,11 +107,12 @@ impl Displayer { ui: &egui::Ui, viewport: &Viewport, joint: &Joint, + color: egui::Color32, ) { ui.painter().circle_filled( egui::pos2(joint.position[0] as f32, joint.position[1] as f32), joint.radius as f32, - egui::Color32::RED, + color, ); } @@ -88,13 +123,14 @@ impl Displayer { viewport: &Viewport, segment: &Segment, endpoints: [[i64; 2]; 2], + color: egui::Color32, ) { ui.painter().line_segment( [ egui::pos2(endpoints[0][0] as f32, endpoints[0][1] as f32), egui::pos2(endpoints[1][0] as f32, endpoints[1][1] as f32), ], - egui::Stroke::new(segment.half_width as f32 * 2.0, egui::Color32::RED), + egui::Stroke::new(segment.half_width as f32 * 2.0, color), ); } @@ -104,6 +140,7 @@ impl Displayer { ui: &egui::Ui, viewport: &Viewport, polygon: &Polygon, + color: egui::Color32, ) { let points: Vec = polygon .vertices @@ -114,7 +151,7 @@ impl Displayer { ui.painter().add(egui::Shape::convex_polygon( points, egui::Color32::RED, - egui::Stroke::new(5.0 / viewport.scale_factor(), egui::Color32::RED), + egui::Stroke::new(5.0 / viewport.scale_factor(), color), )); } } diff --git a/topola-egui/src/main.rs b/topola-egui/src/main.rs index fb1a024..96eba3e 100644 --- a/topola-egui/src/main.rs +++ b/topola-egui/src/main.rs @@ -7,6 +7,7 @@ mod action; mod actions; mod app; +mod appearance_panel; mod displayer; mod menu_bar; mod translator; diff --git a/topola-egui/src/workspace.rs b/topola-egui/src/workspace.rs index a4f50a8..b0714f8 100644 --- a/topola-egui/src/workspace.rs +++ b/topola-egui/src/workspace.rs @@ -4,14 +4,24 @@ use topola::Board; -use crate::translator::Translator; +use crate::{appearance_panel::AppearancePanel, translator::Translator}; pub struct Workspace { pub board: Board, + pub appearance_panel: AppearancePanel, } impl Workspace { pub fn new(board: Board, tr: &Translator) -> Self { - Self { board } + let appearance_panel = AppearancePanel::new(&board); + + Self { + board, + appearance_panel, + } + } + + pub fn update_appearance_panel(&mut self, ctx: &egui::Context) { + self.appearance_panel.update(ctx, &self.board); } } diff --git a/topola/src/board.rs b/topola/src/board.rs index f972929..d21f120 100644 --- a/topola/src/board.rs +++ b/topola/src/board.rs @@ -26,9 +26,9 @@ pub struct Board { } impl Board { - pub fn new(boundary: Vec<[i64; 2]>) -> Self { + pub fn new(boundary: Vec<[i64; 2]>, layer_count: usize) -> Self { Self { - layout: Layout::new(boundary), + layout: Layout::new(boundary, layer_count), layer_names: BiBTreeMap::new(), net_names: BiBTreeMap::new(), } @@ -36,11 +36,12 @@ impl Board { pub fn with_names( boundary: Vec<[i64; 2]>, + layer_count: usize, layer_names: BiBTreeMap, net_names: BiBTreeMap, ) -> Self { Self { - layout: Layout::new(boundary), + layout: Layout::new(boundary, layer_count), layer_names, net_names, } @@ -66,20 +67,20 @@ impl Board { self.layout.add_polygon(polygon) } - pub fn layer_name(&self, layer: usize) -> &str { - &self.layer_names.get_by_left(&layer).unwrap() + pub fn layer_name(&self, layer: usize) -> Option<&str> { + self.layer_names.get_by_left(&layer).map(String::as_str) } - pub fn layer_id(&self, name: &str) -> usize { - *self.layer_names.get_by_right(name).unwrap() + pub fn layer_id(&self, name: &str) -> Option { + self.layer_names.get_by_right(name).copied() } - pub fn net_name(&self, net: usize) -> &str { - &self.net_names.get_by_left(&net).unwrap() + pub fn net_name(&self, net: usize) -> Option<&str> { + self.net_names.get_by_left(&net).map(String::as_str) } - pub fn net_id(&self, name: &str) -> usize { - *self.net_names.get_by_right(name).unwrap() + pub fn net_id(&self, name: &str) -> Option { + self.net_names.get_by_right(name).copied() } } diff --git a/topola/src/layout.rs b/topola/src/layout.rs index 81717b4..a78c0f0 100644 --- a/topola/src/layout.rs +++ b/topola/src/layout.rs @@ -132,6 +132,8 @@ pub struct Polygon { pub struct Layout { boundary: Vec<[i64; 2]>, place_boundary: Vec<[i64; 2]>, + layer_count: usize, + joints: Recorder>, segments: Recorder>, arcs: Recorder>, @@ -140,10 +142,12 @@ pub struct Layout { } impl Layout { - pub fn new(boundary: Vec<[i64; 2]>) -> Self { + pub fn new(boundary: Vec<[i64; 2]>, layer_count: usize) -> Self { Self { boundary: boundary.clone(), place_boundary: boundary, + layer_count, + joints: Recorder::new(StableVec::new()), segments: Recorder::new(StableVec::new()), arcs: Recorder::new(StableVec::new()), diff --git a/topola/src/specctra.rs b/topola/src/specctra.rs index 78650d7..275563d 100644 --- a/topola/src/specctra.rs +++ b/topola/src/specctra.rs @@ -52,6 +52,7 @@ impl Board { .into_iter() .map(|p| [p.x as i64, p.y as i64]) .collect(), + dsn.pcb.structure.layers.len(), layer_names, net_names, ); @@ -251,7 +252,7 @@ impl Board { } fn layer(board: &Board, layers: &[Layer], name: &str, front: bool) -> usize { - let image_layer = board.layer_id(name); + let image_layer = board.layer_id(name).unwrap(); if front { image_layer