brep_app/workbench/ecad.rs
1//! The eCAD editors, and their actions as workbench buttons.
2//!
3//! Four workbenches host eCAD (`brep_ecad_egui`): **Diagram** and **PCB** run its
4//! sheet-and-board [`Editor`] — on a wiring document and on a schematic
5//! document — while **Symbol** and **Pads** run its [`SymbolEditor`] and
6//! [`FootprintEditor`]. eCAD keeps every toolbar command as data
7//! ([`brep_ecad_egui::Action`]), with predicates over the editor saying whether it is
8//! offered, enabled and pressed, and what its button says. The workbench
9//! buttons here are those actions: each button asks eCAD's own predicate about
10//! the editor it names ([`offered`], [`pressed`], [`disabled`], [`caption`]),
11//! and a press runs eCAD's own action ([`dispatch`]). BREP decides only which
12//! actions reach a row and what they look like.
13//!
14//! # Ids
15//!
16//! A button id leads with its workbench ([`super::button_prefix`]). Diagram
17//! and PCB take their buttons from ONE eCAD table, whose ids lead with a group
18//! (`view.`, `sheet.`, `board.`), so each puts its own workbench in front —
19//! `diagram.sheet.tool.select`, `pcb.sheet.tool.select` — and [`split`] takes
20//! it off again. The symbol and pads editors' ids already lead with `symbol.`
21//! and `pads.`, which ARE their workbenches' ids, so their buttons are eCAD's
22//! ids verbatim. Two workbenches sharing a table is what made the prefix law
23//! necessary: see `a_duplicate_id_names_the_first_workbench_s_button_everywhere`.
24//!
25//! # Which actions reach a row
26//!
27//! eCAD groups its actions ([`brep_ecad_egui::ActionGroup`]). The rows carry View,
28//! Tool, Layer and Context. History (undo, redo) and Zoom (in, out, fit) are
29//! not here: BREP's main toolbar owns Undo, Redo and Zoom to fit, as the
30//! Drawing workbench's sheet already relies on. Selection is keyboard-only in
31//! eCAD's own toolbars too. Of Context, the six actions that only put the tool
32//! down ([`PUT_DOWN`]) are not here: the row's Select and Escape do that, and
33//! BREP's sketch row already teaches "press the lit tool to put it down". The
34//! six copper-layer actions are one MENU button ([`LAYER_MENU_ID`]), because a
35//! layer's name depends on the board's layer count and no fixed picture can say
36//! it.
37//!
38//! # When they are offered
39//!
40//! An eCAD button is offered only while the document HAS editors, its own
41//! workbench is the active one, no sketch is being edited, and eCAD offers the
42//! action ([`active_target`]). The active-workbench condition keeps them out
43//! of All's row: All draws the 3D view, so no editor is on screen under it, and
44//! Diagram's and PCB's copies of one action would stand side by side there —
45//! the duplicate-id collision in another form. The sketch condition is the same
46//! thought for the shared sketch tools: a sketch takes the viewport over, and
47//! its Select beside an editor's Select would be two identical hands.
48
49use super::ButtonState;
50use brep_ecad_egui::{Action, Editor, FootprintEditor, SymbolEditor};
51
52/// One document's eCAD editors: what every eCAD button reads and every press
53/// runs on. The app keeps one per document, beside its engine, so each tab has
54/// its own diagram and board and a tab switch keeps them.
55pub struct Editors {
56 /// The Diagram workbench's editor, on a wiring document.
57 pub diagram: Editor,
58 /// The PCB workbench's editor, on a schematic document with its board.
59 pub pcb: Editor,
60 /// The Symbol workbench's editor: the part's schematic symbol.
61 pub symbol: SymbolEditor,
62 /// The Pads workbench's editor: the part's footprint pads.
63 pub pads: FootprintEditor,
64 /// A selection a HOST asked an editor to make, held until that editor is
65 /// next drawn: `(which editor, the pin or pad NUMBER)`.
66 ///
67 /// Deferred, not set at the moment of asking, because the ask crosses a
68 /// workbench switch. The editor is re-handed its block on the frame it is
69 /// drawn (`viewport::ecad`'s pull), and a load CLEARS the selection — so a
70 /// selection made now would be wiped by the very switch that was supposed
71 /// to show it. [`Self::apply_focus`] runs after the pull instead, which is
72 /// where `set_pins` already runs for the same reason.
73 focus: Option<(Target, String)>,
74}
75
76impl Editors {
77 /// Empty editors: a blank wiring diagram, a blank schematic, a new symbol
78 /// and an empty footprint. The Diagram and PCB libraries offer the parts the
79 /// document's assembly places (`panels::ecad_parts`); no symbols ship with
80 /// the editors.
81 pub fn new() -> Self {
82 let mut diagram = Editor::default();
83 diagram.set_document(brep_ecad_core::Document::new(brep_ecad_core::DocumentKind::Wiring));
84 let mut pcb = Editor::default();
85 pcb.set_document(brep_ecad_core::Document::new(brep_ecad_core::DocumentKind::Schematic));
86 Self {
87 diagram,
88 pcb,
89 symbol: SymbolEditor::blank(),
90 pads: FootprintEditor::new(Default::default(), None, Vec::new()),
91 focus: None,
92 }
93 }
94
95 /// Ask `target`'s editor to select the pin or pad numbered `number` the
96 /// next time it draws. The Qualify panel's jump: a connection point, its
97 /// pin and its pad all carry ONE name, so the number is the whole ask.
98 pub fn focus(&mut self, target: Target, number: String) {
99 self.focus = Some((target, number));
100 }
101
102 /// Make the selection [`Self::focus`] asked for, if it was asked of
103 /// `target`. Called by the host AFTER the pull, once per frame the editor
104 /// is drawn; returns whether anything was selected. A number the editor
105 /// does not have clears the ask all the same — a pin the symbol has not
106 /// got yet is not a standing request.
107 pub fn apply_focus(&mut self, target: Target) -> bool {
108 match self.focus.take_if(|(asked, _)| *asked == target) {
109 Some((Target::Symbol, number)) => self.symbol.select_pin(&number),
110 Some((Target::Pads, number)) => self.pads.select_pad(&number),
111 // Diagram and PCB select components, not pins; nothing asks yet.
112 _ => false,
113 }
114 }
115
116 /// Route selection keys to the visible editor even when the pointer is in a dock.
117 pub fn selection_key(&mut self, target: Target, delete: bool) {
118 match target {
119 Target::Diagram | Target::Pcb => {
120 let editor = if target == Target::Diagram { &mut self.diagram } else { &mut self.pcb };
121 let action = match (editor.view, delete) {
122 (brep_ecad_egui::View::Board, true) => "board.delete",
123 (brep_ecad_egui::View::Board, false) => "board.cancel",
124 (_, true) => "sheet.delete",
125 (_, false) => "sheet.cancel",
126 };
127 editor.run_action(action);
128 }
129 Target::Symbol => { self.symbol.run_action(if delete { "symbol.delete" } else { "symbol.tool.select" }); }
130 Target::Pads => { self.pads.run_action(if delete { "pads.delete" } else { "pads.tool.select" }); }
131 }
132 }
133
134}
135
136impl Default for Editors {
137 fn default() -> Self {
138 Self::new()
139 }
140}
141
142/// Which of the four editors a button acts on.
143#[derive(Clone, Copy, Debug, PartialEq, Eq)]
144pub enum Target {
145 Diagram,
146 Pcb,
147 Symbol,
148 Pads,
149}
150
151impl Target {
152 pub const ALL: [Target; 4] = [Target::Diagram, Target::Pcb, Target::Symbol, Target::Pads];
153
154 /// The workbench that draws this editor. It is also the key of the
155 /// document block the editor's document is kept in (`diagram`, `pcb`,
156 /// `symbol`, `pads`): the two namings agree by construction.
157 pub fn workbench_id(self) -> &'static str {
158 match self {
159 Target::Diagram => "diagram",
160 Target::Pcb => "pcb",
161 Target::Symbol => "symbol",
162 Target::Pads => "pads",
163 }
164 }
165
166 /// The editor a workbench id drives, or `None` for a workbench that is not
167 /// one of the four. A plain mapping — unlike [`active_target`] it asks
168 /// nothing about editors or sketch mode, so the host can ask it at the
169 /// moment it must END a sketch because an eCAD workbench became active.
170 pub fn of_workbench(id: &str) -> Option<Target> {
171 Target::ALL.into_iter().find(|target| target.workbench_id() == id)
172 }
173}
174
175/// The dock pane that draws the active editor's parts library
176/// (`Editor::library_panel`): Diagram's and PCB's.
177pub const LIBRARY_PANEL_ID: &str = "ecadLibrary";
178
179/// The dock pane that draws the active editor's inspector — `Editor::inspector`
180/// for Diagram and PCB, `properties` for the symbol and pads editors. One pane
181/// for all four, as the assembly panes serve both Assembly and Wire harness.
182pub const INSPECTOR_PANEL_ID: &str = "ecadInspector";
183
184/// The editor a button id acts on and the eCAD action id it runs there, or
185/// `None` for an id that is no eCAD button's. The inverse of how the tables
186/// spell their ids: `diagram.` and `pcb.` come off, `symbol.*` and `pads.*`
187/// are eCAD's own.
188pub fn split(id: &str) -> Option<(Target, &str)> {
189 if let Some(action) = id.strip_prefix("diagram.") {
190 Some((Target::Diagram, action))
191 } else if let Some(action) = id.strip_prefix("pcb.") {
192 Some((Target::Pcb, action))
193 } else if id.starts_with("symbol.") {
194 Some((Target::Symbol, id))
195 } else if id.starts_with("pads.") {
196 Some((Target::Pads, id))
197 } else {
198 None
199 }
200}
201
202/// The buttons an eCAD workbench carries that are BREP's OWN, not eCAD actions —
203/// dispatched by the shell's own arms, never by [`dispatch`]. None today. Listed
204/// rather than inferred, so an eCAD button whose id is misspelt fails
205/// `every_ecad_workbench_button_is_an_ecad_action_or_brep_s_own` instead of
206/// passing quietly as one of BREP's.
207pub const BREP_OWN: &[&str] = &[];
208
209/// The editor and eCAD action a button id runs, only when the id IS an action in
210/// that editor's own table: [`split`] reads the prefix, this also asks eCAD.
211pub fn ecad_action(id: &str) -> Option<(Target, &str)> {
212 let (target, action) = split(id)?;
213 let known = match target {
214 Target::Diagram | Target::Pcb => brep_ecad_egui::actions().any(|a| a.id == action),
215 Target::Symbol => brep_ecad_egui::symbol_actions().iter().any(|a| a.id == action),
216 Target::Pads => brep_ecad_egui::pad_actions().iter().any(|a| a.id == action),
217 };
218 known.then_some((target, action))
219}
220
221/// The copper-layer MENU button's id. Not an eCAD action: its entries are
222/// (`board.layer.1` .. `board.layer.6`).
223pub const LAYER_MENU_ID: &str = "pcb.board.layer";
224
225/// The six Context actions that only abandon the gesture in progress and
226/// return to Select — five run `Editor::cancel`, the route one drops the draft
227/// and selects `BoardTool::Select`. The row's Select button and Escape
228/// (`sheet.cancel`, `board.cancel`) already do that, so they are not buttons.
229pub const PUT_DOWN: &[&str] = &[
230 "sheet.place.done",
231 "sheet.wire.done",
232 "sheet.label.done",
233 "sheet.move.cancel",
234 "sheet.junction.done",
235 "board.route.done",
236];
237
238/// Why eCAD disables an action it offers, for the few that it ever does. eCAD
239/// answers `enabled` with a bare bool, so the words are BREP's, one per action,
240/// read from the predicate it declares.
241const DISABLED_BECAUSE: &[(&str, &str)] = &[
242 ("view.schematic", "the autorouter is running"),
243 ("view.board", "the autorouter is running"),
244 ("sheet.wire.finish", "no wire is being drawn"),
245 ("board.route.via", "a via needs a track being drawn on a board with more than one copper layer"),
246 ("board.route.finish", "no track is being drawn"),
247 ("board.zone.finish", "a zone needs three corners before it can be closed"),
248 ("board.zones.fill", "the board has no zones"),
249];
250
251/// The words for an action eCAD disables with no entry in
252/// [`DISABLED_BECAUSE`] — a table gap, which `every_disabled_action_says_why`
253/// fails on rather than letting this reach a user.
254const DISABLED_UNEXPLAINED: &str = "the editor does not allow it right now";
255
256/// The editor the active workbench draws, if it is one of the four and
257/// something could draw it: the document has editors and no sketch is being
258/// edited (a sketch takes the viewport over). See the module doc for why each
259/// condition is there.
260///
261/// The stored workbench id is read directly, not through `resolve`: `resolve`
262/// answers only for a workbench in the dropdown, and these four are declared
263/// before they are registered ([`super::UNREGISTERED`]). The two readings
264/// agree on every registered id.
265pub fn active_target(state: &ButtonState) -> Option<Target> {
266 state.ecad?;
267 if state.engine.sketch_mode() {
268 return None;
269 }
270 Target::of_workbench(&state.engine.settings.workbench)
271}
272
273/// What eCAD says about one of its actions on one of its editors.
274struct Reading {
275 offered: bool,
276 enabled: bool,
277 pressed: Option<bool>,
278 caption: String,
279}
280
281fn read_in<T: 'static>(
282 mut table: impl Iterator<Item = &'static Action<T>>,
283 editor: &T,
284 action: &str,
285) -> Option<Reading> {
286 let action = table.find(|a| a.id == action)?;
287 Some(Reading {
288 offered: action.offered(editor),
289 enabled: action.enabled(editor),
290 pressed: action.pressed(editor),
291 caption: action.caption(editor),
292 })
293}
294
295/// eCAD's reading of `action` on `target`'s editor in `editors`.
296fn read(editors: &Editors, target: Target, action: &str) -> Option<Reading> {
297 match target {
298 Target::Diagram => read_in(brep_ecad_egui::actions(), &editors.diagram, action),
299 Target::Pcb => read_in(brep_ecad_egui::actions(), &editors.pcb, action),
300 Target::Symbol => read_in(brep_ecad_egui::symbol_actions().iter(), &editors.symbol, action),
301 Target::Pads => read_in(brep_ecad_egui::pad_actions().iter(), &editors.pads, action),
302 }
303}
304
305/// eCAD's reading of the action button `id` names, but only while that
306/// button's editor is the one on screen ([`active_target`]).
307fn reading(state: &ButtonState, id: &str) -> Option<Reading> {
308 let (target, action) = split(id)?;
309 if active_target(state) != Some(target) {
310 return None;
311 }
312 read(state.ecad?, target, action)
313}
314
315/// Whether the eCAD button `id` is offered: its editor is on screen and eCAD
316/// offers the action.
317pub fn offered(state: &ButtonState, id: &str) -> bool {
318 reading(state, id).is_some_and(|r| r.offered)
319}
320
321/// Whether the eCAD button `id` draws pressed: eCAD's own toggle state.
322pub fn pressed(state: &ButtonState, id: &str) -> bool {
323 reading(state, id).is_some_and(|r| r.pressed == Some(true))
324}
325
326/// Why the eCAD button `id` cannot act now, or `None` when eCAD enables it.
327pub fn disabled(state: &ButtonState, id: &str) -> Option<&'static str> {
328 let reading = reading(state, id)?;
329 if reading.enabled {
330 return None;
331 }
332 let action = split(id)?.1;
333 Some(
334 DISABLED_BECAUSE
335 .iter()
336 .find(|(known, _)| *known == action)
337 .map_or(DISABLED_UNEXPLAINED, |(_, why)| *why),
338 )
339}
340
341/// The eCAD button `id`'s live caption: eCAD's own button text, which for some
342/// actions describes state (`Bend: vertical first`, a layer's `B.Cu`).
343pub fn caption(state: &ButtonState, id: &str) -> String {
344 reading(state, id).map_or_else(
345 || super::button_by_declared_id(id).map_or_else(String::new, |b| b.tooltip.to_string()),
346 |r| r.caption,
347 )
348}
349
350/// The layer menu's live caption: the menu's own words and the layer it has
351/// on, so the closed menu still says where a track will go.
352pub fn layer_menu_caption(state: &ButtonState) -> String {
353 let on = super::pcb::LAYERS
354 .iter()
355 .find(|entry| entry.offered(state) && entry.is_pressed(state))
356 .map(|entry| entry.label(state));
357 match on {
358 Some(layer) => format!("Copper layer: {layer}"),
359 None => "Copper layer".to_string(),
360 }
361}
362
363/// Run the eCAD action the button `id` names on its editor, as a click on
364/// eCAD's own button does. `None` when `id` is no declared button that IS an
365/// eCAD action — a menu is not one (its entries are), and neither is a button
366/// of BREP's own on an eCAD workbench ([`BREP_OWN`]), which the shell's own
367/// arms run. Without that, such a button would reach eCAD's `run_action`,
368/// read `false`, and be reported handled: a press that vanishes. Otherwise
369/// eCAD's answer, `false` when the action is not offered or not enabled now.
370pub fn dispatch(editors: &mut Editors, id: &str) -> Option<bool> {
371 dispatch_in(&super::declared_workbenches(), editors, id)
372}
373
374/// [`dispatch`] over any registry.
375fn dispatch_in(registry: &[&'static super::Workbench], editors: &mut Editors, id: &str) -> Option<bool> {
376 let button = super::button_in(registry, id)?;
377 if !button.menu.is_empty() {
378 return None;
379 }
380 let (target, action) = ecad_action(id)?;
381 Some(match target {
382 Target::Diagram => editors.diagram.run_action(action),
383 Target::Pcb => editors.pcb.run_action(action),
384 Target::Symbol => editors.symbol.run_action(action),
385 Target::Pads => editors.pads.run_action(action),
386 })
387}
388
389/// One eCAD action as a workbench button. `$id` is the BUTTON id — eCAD's
390/// action id behind its workbench's prefix, see [`split`] — and every
391/// predicate is eCAD's own, asked through this module. `toggle` is an action
392/// eCAD reports a pressed state for (a tool, a view, a layer); `command` one it
393/// does not. The tooltip is eCAD's label, word for word — the tests hold it.
394macro_rules! ecad_button {
395 ($id:literal, $glyph:literal, $tooltip:literal, toggle) => {
396 $crate::workbench::WorkbenchButton {
397 id: $id,
398 glyph: $glyph,
399 tooltip: $tooltip,
400 when: Some(|s| $crate::workbench::ecad::offered(s, $id)),
401 pressed: Some(|s| $crate::workbench::ecad::pressed(s, $id)),
402 disabled: Some(|s| $crate::workbench::ecad::disabled(s, $id)),
403 caption: Some(|s| $crate::workbench::ecad::caption(s, $id)),
404 menu: &[],
405 }
406 };
407 ($id:literal, $glyph:literal, $tooltip:literal, command) => {
408 $crate::workbench::WorkbenchButton {
409 id: $id,
410 glyph: $glyph,
411 tooltip: $tooltip,
412 when: Some(|s| $crate::workbench::ecad::offered(s, $id)),
413 pressed: None,
414 disabled: Some(|s| $crate::workbench::ecad::disabled(s, $id)),
415 caption: Some(|s| $crate::workbench::ecad::caption(s, $id)),
416 menu: &[],
417 }
418 };
419}
420pub(crate) use ecad_button;
421
422/// A workbench whose feature filter offers no features: eCAD's documents are
423/// not history features, as PMI's annotations and Drawing's sheets are not.
424pub(crate) fn no_features(_feature: &super::FeatureInfo<'_>) -> bool {
425 false
426}
427