BREP_app 0.4.0

The BREP CAD application: an eframe (egui + wgpu) host that draws the brep-render 3D engine into an egui frame — native + wasm from one codebase.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
//! Toolbar — a top `Panel` strip of primary actions above the viewport:
//! Undo / Redo, a Wireframe toggle, and Zoom-to-fit, plus the File-actions
//! SEAM (owned by the concurrent file panel). Standard views live on the
//! ViewCube navigation gizmo, not here.
//!
//! Follows the panel pattern (a small state struct + a `show(&mut self, ui,
//! state, store)` the shell calls once), but unlike the left-column sections it
//! creates its OWN top panel — so the shell just calls `self.toolbar.show(…)`
//! FIRST in `App::ui` (before the left panel + viewport) to reserve the strip.
//!
//! The MODEL is engine-owned: the toolbar only TRIGGERS engine methods
//! (`state.undo()` / `state.redo()` / `state.zoom_to_fit()`) and drives the
//! wireframe through the existing
//! settings-apply path (`apply_settings_json` → bumps generation + dirty). It
//! owns only the per-frame `hits` map (widget screen rects) the headed verifier
//! reads to drive real clicks, exactly like the history panel.
//!
//! The File buttons don't touch storage here: a click returns a [`FileAction`]
//! from [`ToolbarPanel::show`] and the shell hands it to the reusable
//! [`crate::panels::file::FileDialog`].
//!
//! Buttons draw their artwork from `assets/glyphs/*.svg`, with the text label in
//! the hover tooltip. The COLOUR icons are single SVGs from the icon catalog
//! (see [`crate::icons`]), painted by [`toolbar_button`] whenever a glyph
//! resolves to colour artwork — they replaced the private-use glyph STACKS this
//! file used to assemble by hand, one layer per colour. Neither depends on an OS
//! font or a bitmap asset. All styling/sizing goes through the shared
//! [`crate::panels::toolbar_button`] helpers (the ONE place toolbar-button style
//! lives), so a change lands globally. Glyph per button:
//!   New        U+E010 (doc)                     colour
//!   Open       U+E011 (folder)                  colour
//!   Save       U+E012 (disk)                    colour
//!   Save As    U+E013 (disk + badge)            colour
//!   Import     U+E014 (tray + down arrow)       colour
//!   Export     U+E015 (tray + up arrow)         colour
//!   Undo       U+E016 (curved arrow)            colour
//!   Redo       U+E017 (curved arrow)            colour
//!   Projctn    U+E018 (camera)                  colour
//!   Submit Bug U+1F41E (bug)                    colour
//!   Wireframe  U+1F578 (single glyph)
//!   Faces      U+E028  (shaded cube)
//!   Edges      U+E029  (edge cube)
//!   Vertices   U+E02A  (corner points)
//!   Fit        U+26F6  (single glyph)
//!   Settings   U+2699  (single glyph)
//!   Help       U+2753  (circled question mark)
//!   Info       U+2139  (circled i)
//!   Properties U+E066  (part tag)
//!
//! The Properties button here is the DOCUMENT's: it opens the open part's own
//! BOM attribute record (see [`crate::panels::part_properties`]), which is a
//! property of the whole document and so has nothing to be selected first.
//! ENTITY inspection is a different thing and is not here: it is opened from the
//! selection-driven CONTEXT bar (see [`crate::panels::context_bar`]), which spawns
//! a pinned per-entity window (see [`crate::panels::info_windows`]).

use crate::automation::hit_keys::HitKeyDoc;
use crate::panels::file::FileAction;
use crate::panels::toolbar_button;
use crate::store::{ModelStore, SETTINGS_KEY};
use crate::workbench;
use brep_render::engine_state::EngineState;
use eframe::egui;
use std::collections::HashMap;


/// What one frame of the toolbar produced for the shell to act on. Extends the
/// old `Option<FileAction>` return so a clicked WORKBENCH button can flow its id
/// out the SAME way a File button flows a [`FileAction`] — the shell matches on
/// the id. Phase 1 declares no workbench buttons, so `workbench_button` is always
/// `None`, but the return path is wired for Phase 2.
#[derive(Default)]
pub struct ToolbarOutcome {
    /// A File button click (New / Open / Save / …), dispatched to the file dialog.
    pub file: Option<FileAction>,
    /// A workbench toolbar button click, surfaced by its `WorkbenchButton::id`.
    pub workbench_button: Option<&'static str>,
    /// The "Submit Bug" button was clicked this frame — the shell begins the
    /// screenshot-capture + report flow (see [`crate::panels::bug_report`]).
    pub bug_report: bool,
}

/// The toolbar's own state: the per-frame map of egui widget screen rects,
/// published to JS for the headed verifier to drive real clicks. Rebuilt each
/// frame (there is no DOM — egui is drawn on the canvas).
#[derive(Default)]
pub struct ToolbarPanel {
    hits: HashMap<String, egui::Rect>,
}

impl ToolbarPanel {
    pub fn new() -> Self {
        Self::default()
    }

    /// Draw the toolbar as a top panel of primary actions. Called FIRST in the
    /// shell's `App::ui` so the strip reserves the top before the left panel and
    /// the central viewport. Rebuilds `hits` each frame as it draws. Returns the
    /// [`FileAction`] a clicked File button requests (the shell dispatches it to
    /// the file dialog), or `None`.
    ///
    /// `settings_open` is the shell-owned open flag of the floating Settings window
    /// (see [`crate::panels::settings`]): the gear button reflects it (highlighted
    /// while open) and toggles it on click. `info_open` is the same arrangement
    /// for the Info window (see [`crate::panels::info`]).
    pub fn show(
        &mut self,
        ui: &mut egui::Ui,
        state: &mut EngineState,
        store: &dyn ModelStore,
        settings_open: &mut bool,
        properties_open: &mut bool,
        info_open: &mut bool,
    ) -> ToolbarOutcome {
        self.hits.clear();
        let mut outcome = ToolbarOutcome::default();
        egui::containers::panel::Panel::top("brep-toolbar")
            .resizable(false)
            .show(ui, |ui| {
                ui.add_space(3.0);
                ui.horizontal_wrapped(|ui| {
                    // Workbench selector FIRST (top-left). It is a UI FILTER, not a
                    // mode switch — it only trims the feature-creation palette /
                    // offers. Its extra BUTTONS render LAST (after the normal icons).
                    self.workbench_selector(ui, state, store);
                    ui.separator();
                    outcome.file = self.file_actions(ui);
                    ui.separator();
                    self.edit_actions(ui, state);
                    ui.separator();
                    self.view_actions(ui, state, store);
                    ui.separator();
                    self.properties_action(ui, properties_open);
                    self.settings_action(ui, settings_open);
                    self.help_action(ui);
                    self.info_action(ui, info_open);
                    outcome.bug_report = self.bug_action(ui);
                    // The active workbench's extra buttons go at the END of the
                    // toolbar, after the standard icons (a leading separator draws
                    // only when the active workbench actually declares buttons).
                    let current = state.settings.workbench.clone();
                    outcome.workbench_button = self.workbench_buttons_row(ui, &current);
                });
                ui.add_space(3.0);
            });
        outcome
    }

    /// The WORKBENCH dropdown: iterates the per-file registry
    /// ([`workbench::WORKBENCHES`]) so labels/order are never hardcoded, shows the
    /// current selection (`state.settings.workbench`), and on change writes the id
    /// through the SAME apply+save path the wireframe/projection toggles use here
    /// (`apply_settings_json` + `store.write`), so it persists across a reload.
    /// Publishes the header + per-item hit-rects for the headed verifier.
    fn workbench_selector(
        &mut self,
        ui: &mut egui::Ui,
        state: &mut EngineState,
        store: &dyn ModelStore,
    ) {
        let current = state.settings.workbench.clone();
        let options: Vec<(&str, &str, &str)> =
            workbench::WORKBENCHES.iter().map(|w| (w.label, w.id, w.glyph)).collect();
        let result =
            toolbar_button::select(ui, "workbench", workbench::resolve(&current).id, &options);
        self.hits.insert("workbench".into(), result.header_rect);
        for (id, rect) in &result.item_rects {
            self.hits.insert(format!("workbench:item:{id}"), *rect);
        }
        if let Some(next) = result.changed {
            let json = serde_json::json!({ "workbench": next }).to_string();
            let _ = state.apply_settings_json(&json);
            // Persist through the same seam wireframe/projection use so the choice
            // survives a reload and the settings blob agrees.
            let _ = store.write(SETTINGS_KEY, &state.settings_json());
        }
    }

    /// Render the ACTIVE workbench's extra toolbar buttons generically. Phase 1:
    /// every workbench declares no buttons, so this draws nothing and returns
    /// `None`.
    fn workbench_buttons_row(&mut self, ui: &mut egui::Ui, current: &str) -> Option<&'static str> {
        let buttons = workbench::workbench_buttons(current);
        // A leading separator only when there is at least one button, so an
        // empty-button workbench (e.g. Modeling) leaves no dangling separator at
        // the toolbar's end.
        if !buttons.is_empty() {
            ui.separator();
        }
        self.draw_workbench_buttons(ui, &buttons)
    }

    /// Draw an explicit list of workbench buttons via the shared button helper,
    /// publishing each hit-rect and surfacing the clicked button's `id` (the
    /// toolbar RETURN PATH — the shell dispatches on it, exactly like a
    /// [`FileAction`]). Split out from [`Self::workbench_buttons_row`] so the
    /// mechanism can be unit-tested with a synthetic button while the real
    /// registry ships none in Phase 1.
    fn draw_workbench_buttons(
        &mut self,
        ui: &mut egui::Ui,
        buttons: &[&workbench::WorkbenchButton],
    ) -> Option<&'static str> {
        let mut clicked = None;
        for button in buttons {
            let resp = toolbar_button::button(ui, button.glyph, button.tooltip);
            self.hits.insert(format!("workbench:btn:{}", button.id), resp.rect);
            if resp.clicked() {
                clicked = Some(button.id);
            }
        }
        clicked
    }

    /// The Help button: opens the generated help site (`brep-docs` writes it to
    /// `web/help/` next to the served page) in a new tab. A circled question
    /// mark (U+2753), drawn in the same line style as the ℹ beside it — this app
    /// ships NO font fallback, so a glyph is only ever a key into the SVG
    /// catalog (`assets/glyphs/icon_2753.svg`) and an uncatalogued character
    /// would paint as tofu on wasm.
    fn help_action(&mut self, ui: &mut egui::Ui) {
        let btn = toolbar_button::button(ui, "\u{2753}", "Help");
        self.hits.insert("help".into(), btn.rect);
        if btn.clicked() {
            // ONE literal for the help URL, shared with the `help_open` command.
            ui.ctx().open_url(egui::OpenUrl::new_tab(crate::automation::HELP_URL));
        }
    }

    /// The Info toggle: opens / closes the floating Info window — the licences
    /// and this session's renderer diagnostics (see [`crate::panels::info`]).
    /// The ℹ glyph (U+2139) this button now carries is the one the single Docs
    /// button used to; Help took the question mark, which is what a user looks
    /// for when they want the manual.
    fn info_action(&mut self, ui: &mut egui::Ui, open: &mut bool) {
        let btn = toolbar_button::toggle(ui, *open, "\u{2139}", "Info (licences and diagnostics)");
        self.hits.insert("info".into(), btn.rect);
        if btn.clicked() {
            *open = !*open;
        }
    }

    /// The Submit Bug button: opens the in-app problem-report flow, which first
    /// grabs a screenshot of the app (UI + 3D model) BEFORE its dialog appears,
    /// then collects a description + optional email and posts the report.
    /// `bug_report` Material Symbol (base glyph U+1F41E) — `toolbar_button`
    /// auto-renders it from the icon catalog: U+1F41E is COLOUR artwork, so the
    /// button paints the SVG rather than the font glyph.
    /// Returns whether it was clicked.
    fn bug_action(&mut self, ui: &mut egui::Ui) -> bool {
        let btn = toolbar_button::button(ui, "\u{1F41E}", "Submit Bug");
        self.hits.insert("bug".into(), btn.rect);
        btn.clicked()
    }

    /// The Part Properties toggle: opens / closes the floating window that edits
    /// the OPEN document's own BOM attributes (Part Number, Material, Mass, …).
    /// A tag glyph (U+E066), reflecting the live open state like the gear beside
    /// it. Document-level, so it needs no selection and is never disabled — every
    /// document is a part, an assembly included (it is one BOM row in its parent).
    fn properties_action(&mut self, ui: &mut egui::Ui, open: &mut bool) {
        let btn = toolbar_button::toggle(ui, *open, "\u{E066}", "Part properties");
        self.hits.insert("properties".into(), btn.rect);
        if btn.clicked() {
            *open = !*open;
        }
    }

    /// The Settings toggle: opens / closes the floating Settings window. A
    /// selectable gear glyph (U+2699, bundled DejaVu font) reflecting the live
    /// open state; the label lives in the tooltip, matching the other buttons.
    fn settings_action(&mut self, ui: &mut egui::Ui, open: &mut bool) {
        // Gear (U+2699) — renders in the bundled DejaVu font (no tofu). A toggle
        // reflecting the live open state.
        let btn = toolbar_button::toggle(ui, *open, "\u{2699}", "Settings");
        self.hits.insert("settings".into(), btn.rect);
        if btn.clicked() {
            *open = !*open;
        }
    }

    /// The published widget hit-rects (egui points) for the headed verifier.
    pub fn hits_json(&self) -> String {
        crate::automation::hit_rects::hits_json(&self.hits)
    }

    /// File actions (New / Open / Save / Save As) as glyph buttons. Each returns
    /// the matching [`FileAction`] on click; the shell hands it to the file
    /// dialog (which owns all storage). Glyph → label in the tooltip.
    fn file_actions(&mut self, ui: &mut egui::Ui) -> Option<FileAction> {
        let mut action = None;
        // New — page (U+1F4C4, the previous-app glyph).
        let new = toolbar_button::button(ui, "\u{E010}", "New");
        self.hits.insert("file:new".into(), new.rect);
        if new.clicked() {
            action = Some(FileAction::New);
        }
        // Open — open folder (U+1F5C1). Kept: the previous app had no Open.
        let open = toolbar_button::button(ui, "\u{E011}", "Open");
        self.hits.insert("file:open".into(), open.rect);
        if open.clicked() {
            action = Some(FileAction::Open);
        }
        // Save — floppy disk (U+1F4BE, the previous-app glyph).
        let save = toolbar_button::button(ui, "\u{E012}", "Save");
        self.hits.insert("file:save".into(), save.rect);
        if save.clicked() {
            action = Some(FileAction::Save);
        }
        // Save As has a dedicated custom-font glyph so its plus badge shares
        // the disk's weight, alignment, and square advance.
        let save_as = toolbar_button::button(ui, "\u{E013}", "Save As");
        self.hits.insert("file:saveas".into(), save_as.rect);
        if save_as.clicked() {
            action = Some(FileAction::SaveAs);
        }
        ui.separator();
        // Import — neutral CAD files use their native readers; meshes run through
        // RANSAC reconstruction before being appended as an IMPORT3D feature.
        let import = toolbar_button::button(ui, "\u{E014}", "Import STEP / IGES / STL / OBJ\u{2026}");
        self.hits.insert("file:import".into(), import.rect);
        if import.clicked() {
            action = Some(FileAction::Import);
        }
        // Export — outbox tray (U+1F4E4): write the model OUT as STEP / IGES / STL.
        let export = toolbar_button::button(ui, "\u{E015}", "Export\u{2026} (STEP / IGES / STL)");
        self.hits.insert("file:export".into(), export.rect);
        if export.clicked() {
            action = Some(FileAction::Export);
        }
        action
    }

    /// Undo / Redo — trigger the engine-owned undo history. Buttons enable only
    /// when a step is available so the affordance reflects the real stack. Glyph
    /// only; the label lives in the tooltip.
    ///
    /// While a sketch is open these SAME buttons drive the per-session SKETCH
    /// history instead of the model-level undo — the sketch has no undo/redo of
    /// its own; it shares this toolbar pair (matching the Ctrl+Z / Ctrl+Shift+Z
    /// keyboard router in `app.rs`).
    fn edit_actions(&mut self, ui: &mut egui::Ui, state: &mut EngineState) {
        let sketch = state.sketch_mode();
        let can_undo = if sketch { state.sketch_can_undo() } else { state.can_undo() };
        let undo = toolbar_button::button_enabled(
            ui,
            can_undo,
            "\u{E016}",
            if sketch { "Undo sketch edit (Ctrl+Z)" } else { "Undo" },
        );
        self.hits.insert("undo".into(), undo.rect);
        if undo.clicked() {
            if sketch {
                state.sketch_undo();
            } else {
                state.undo();
            }
        }
        let can_redo = if sketch { state.sketch_can_redo() } else { state.can_redo() };
        let redo = toolbar_button::button_enabled(
            ui,
            can_redo,
            "\u{E017}",
            if sketch { "Redo sketch edit (Ctrl+Y)" } else { "Redo" },
        );
        self.hits.insert("redo".into(), redo.rect);
        if redo.clicked() {
            if sketch {
                state.sketch_redo();
            } else {
                state.redo();
            }
        }
    }

    /// View actions: the Wireframe toggle (drives `settings.wireframe` through the
    /// settings-apply path + persists it like the settings panel), the Projection
    /// toggle (orthographic ↔ perspective via `set_projection`), Zoom-to-fit, and
    /// quick standard-view buttons.
    fn view_actions(&mut self, ui: &mut egui::Ui, state: &mut EngineState, store: &dyn ModelStore) {
        // Wireframe: reflect the LIVE engine value so the toggle is always honest,
        // and flip it via the same apply-path the settings panel uses (bumps
        // settings_generation + dirty so the GPU re-derives styles).
        let wire = state.settings.wireframe;
        // Open cube with dashed hidden edges (U+1F578).
        let wf = toolbar_button::toggle(ui, wire, "\u{1F578}", "Wireframe");
        self.hits.insert("wireframe".into(), wf.rect);
        if wf.clicked() {
            let next = !wire;
            let _ = state.apply_settings_json(&format!("{{\"wireframe\": {next}}}"));
            // Persist the full settings through the same seam the settings panel
            // uses, so the toggle survives a reload and both views agree.
            let _ = store.write(SETTINGS_KEY, &state.settings_json());
        }

        // Projection: reflect the LIVE camera mode — the toggle is highlighted while
        // in perspective. Flip it through the SAME settings-apply path wireframe uses
        // (`apply_settings_json` reads `orthographic` and drives the camera), then
        // persist the full settings — so, like wireframe, the projection is now a
        // real setting that survives a reload and agrees with the settings panel.
        let is_persp =
            matches!(state.camera.projection, brep_render::view::Projection::Perspective { .. });
        // Still-camera icon artwork — the ortho ↔ perspective
        // toggle. The tooltip names the CURRENT mode.
        let proj_tip = if is_persp {
            "Perspective projection"
        } else {
            "Orthographic projection"
        };
        let proj = toolbar_button::toggle(ui, is_persp, "\u{E018}", proj_tip);
        self.hits.insert("projection".into(), proj.rect);
        if proj.clicked() {
            let want_ortho = is_persp; // currently perspective → switch to orthographic
            let _ = state.apply_settings_json(&format!("{{\"orthographic\": {want_ortho}}}"));
            let _ = store.write(SETTINGS_KEY, &state.settings_json());
        }

        // The three DISPLAY-CLASS toggles, beside wireframe and projection
        // because they answer the same question — what does the viewport draw.
        // Each is a real setting (`showFaces` / `showEdges` / `showVertices`),
        // not a zeroed size: turning edges off and on again must give back the
        // edge width the user chose, not 0.
        for (glyph, label, key, on) in [
            ("\u{E028}", "Show faces", "show:faces", state.settings.show_faces),
            ("\u{E029}", "Show edges", "show:edges", state.settings.show_edges),
            ("\u{E02A}", "Show vertices", "show:vertices", state.settings.show_vertices),
        ] {
            let btn = toolbar_button::toggle(ui, on, glyph, label);
            self.hits.insert(key.into(), btn.rect);
            if btn.clicked() {
                // The settings JSON key is the hit key's tail, capitalised —
                // `show:faces` drives `showFaces`, through the same apply+persist
                // path wireframe and projection use.
                let field = match key {
                    "show:faces" => "showFaces",
                    "show:edges" => "showEdges",
                    _ => "showVertices",
                };
                let next = !on;
                let _ = state.apply_settings_json(&format!("{{\"{field}\": {next}}}"));
                let _ = store.write(SETTINGS_KEY, &state.settings_json());
            }
        }

        // Square-with-four-corners (U+26F6) — the previous-app zoom-to-fit glyph.
        let fit = toolbar_button::button(ui, "\u{26F6}", "Zoom to fit");
        self.hits.insert("fit".into(), fit.rect);
        if fit.clicked() {
            state.zoom_to_fit();
        }
    }
}

// BREP private tests: c8c2edf4400ca9a2

/// The hit keys this panel publishes (see `automation::hit_keys`).
pub static HIT_KEYS: &[HitKeyDoc] = &[
    HitKeyDoc { panel: "toolbar", prefix: "file:new", meaning: "new document", command: Some("doc_new") },
    HitKeyDoc { panel: "toolbar", prefix: "file:open", meaning: "open", command: Some("doc_load") },
    HitKeyDoc { panel: "toolbar", prefix: "file:save", meaning: "save", command: Some("doc_json") },
    HitKeyDoc { panel: "toolbar", prefix: "file:saveas", meaning: "save as", command: Some("doc_json") },
    HitKeyDoc { panel: "toolbar", prefix: "file:import", meaning: "import a file", command: Some("doc_import") },
    HitKeyDoc { panel: "toolbar", prefix: "file:export", meaning: "export", command: Some("doc_export") },
    HitKeyDoc { panel: "toolbar", prefix: "undo", meaning: "undo", command: Some("undo") },
    HitKeyDoc { panel: "toolbar", prefix: "redo", meaning: "redo", command: Some("redo") },
    HitKeyDoc { panel: "toolbar", prefix: "fit", meaning: "zoom to fit", command: Some("zoom_to_fit") },
    HitKeyDoc { panel: "toolbar", prefix: "projection", meaning: "toggle perspective/orthographic", command: Some("set_projection") },
    HitKeyDoc { panel: "toolbar", prefix: "wireframe", meaning: "toggle wireframe", command: Some("settings_set") },
    HitKeyDoc { panel: "toolbar", prefix: "show:faces", meaning: "toggle the shaded faces", command: Some("settings_set") },
    HitKeyDoc { panel: "toolbar", prefix: "show:edges", meaning: "toggle the edges", command: Some("settings_set") },
    HitKeyDoc { panel: "toolbar", prefix: "show:vertices", meaning: "toggle the vertex points", command: Some("settings_set") },
    HitKeyDoc { panel: "toolbar", prefix: "properties", meaning: "open the part properties window", command: Some("part_properties_window") },
    HitKeyDoc { panel: "toolbar", prefix: "settings", meaning: "open the settings window", command: Some("settings_window") },
    HitKeyDoc { panel: "toolbar", prefix: "help", meaning: "open the help site", command: Some("help_open") },
    HitKeyDoc { panel: "toolbar", prefix: "info", meaning: "open the info window (licences + diagnostics)", command: Some("info_window") },
    HitKeyDoc { panel: "toolbar", prefix: "bug", meaning: "open the bug report", command: Some("bug_report_open") },
    HitKeyDoc { panel: "toolbar", prefix: "workbench", meaning: "the workbench dropdown", command: Some("settings_set") },
    HitKeyDoc { panel: "toolbar", prefix: "workbench:btn:", meaning: "the workbench dropdown button", command: Some("workbench_button") },
    HitKeyDoc { panel: "toolbar", prefix: "workbench:item:", meaning: "pick a workbench from the open dropdown (workbench:item:id)", command: Some("settings_set") },
];