BREP_app 0.10.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
//! Global toolbar shell: File menu and either Ribbon or Classic presentation.
//!
//! Both renderers consume the same offered-command snapshots and button helper.
//! Command targets retain the existing engine, workbench, plugin and creation
//! dispatch paths. This panel owns presentation and automation hit rectangles;
//! the shell owns File dialogs and creation outcomes.

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;

/// Toolbar actions returned to the shell for dispatch.
#[derive(Default)]
pub struct ToolbarOutcome {
    pub toggle_mobile_panels: bool,
    pub documents: super::document_tabs::TabsOutcome,
    /// A File button click (New / Open / Save / …), dispatched to the file dialog.
    pub file: Option<FileAction>,
    pub recent_document: Option<String>,
    pub creation: super::workbench_toolbar::WorkbenchToolbarOutcome,
    /// A workbench toolbar button click, surfaced by its `WorkbenchButton::id`.
    pub workbench_button: Option<&'static str>,
    pub plugin_action: Option<String>,
    pub plugins: bool,
    pub javascript: bool,
    /// 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,
    /// Zoom-to-fit was clicked. It is SURFACED rather than run here because what
    /// "fit" means depends on what the viewport is drawing — the 3D scene, or
    /// the OPEN SHEET's paper — and the sheet's transform lives in the viewport,
    /// not the engine. `BrepApp::zoom_to_fit` owns that branch, and the
    /// `zoom_to_fit` command calls the very same method.
    pub zoom_to_fit: bool,
    /// A row of the PLM review inbox was opened: the review it
    /// names (`panels::plm_review`, S4). The shell hands it to the PLM pane.
    pub plm_review: Vec<crate::panels::plm_review::PlmReviewEvent>,
}

/// 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>,
    ribbon: super::ribbon::RibbonPanel,
    pub read_only: bool,
    pub(crate) documents: super::document_tabs::MenuState,
    /// The PLM review inbox badge beside the connection indicator (S4).
    inbox: crate::panels::plm_review::InboxBadge,
}



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,
        ecad: Option<&crate::workbench::ecad::Editors>,
        store: &dyn ModelStore,
        settings_open: &mut bool,
        properties_open: &mut bool,
        info_open: &mut bool,
    ) -> ToolbarOutcome {
        self.hits.clear();
        if crate::viewport::touch::compact(ui.ctx()) {
            return self.show_compact(ui, state, ecad, store, settings_open, properties_open, info_open);
        }
        let mut outcome = ToolbarOutcome::default();
        egui::containers::panel::Panel::top("brep-toolbar")
            .resizable(false)
            .show(ui, |ui| {
                ui.add_space(3.0);
                let style = state.settings.toolbar_style;
                let mut target = None;
                toolbar_button::wrapped(ui, |ui| {
                    let file = super::file::file_menu(
                        ui,
                        state,
                        store,
                        settings_open,
                        properties_open,
                        &mut self.hits,
                    );
                    outcome.file = file.action;
                    outcome.recent_document = file.recent;
                    outcome.plugins |= file.plugins;
                    outcome.javascript |= file.javascript;
                    if style == brep_render::style::ToolbarStyle::Ribbon {
                        self.ribbon.tabs(ui, &mut self.hits);
                    } else {
                        let commands = self.commands(state, ecad, *info_open);
                        let mut last_group = None;
                        for command in commands.iter().filter(|c| !c.is_creation()) {
                            let segments = command.segments();
                            let group = (segments[0], segments[1]);
                            if last_group != Some(group) {
                                ui.separator();
                                last_group = Some(group);
                            }
                            if let Some(t) = super::ribbon::draw_command(
                                ui,
                                command,
                                true,
                                false,
                                &mut self.hits,
                            ) {
                                target = Some(t);
                            }
                        }
                    }
                    outcome.plm_review = self.plm_indicator(ui, store, settings_open);
                });
                if style == brep_render::style::ToolbarStyle::Ribbon {
                    let commands = self.commands(state, ecad, *info_open);
                    target = self.ribbon.show(ui, &commands, &mut self.hits).or(target);
                }
                if let Some(target) = target {
                    self.dispatch_target(ui.ctx(), target, state, store, info_open, &mut outcome);
                }
                ui.add_space(3.0);
            });
        outcome
    }

    fn commands(
        &self,
        state: &EngineState,
        ecad: Option<&workbench::ecad::Editors>,
        info_open: bool,
    ) -> Vec<workbench::OfferedCommand> {
        let mut commands = workbench::offered_commands(
            &workbench::ButtonState {
                engine: state,
                ecad,
            },
            info_open,
        );
        if self.read_only {
            for c in commands.iter_mut().filter(|c| c.is_creation()) {
                c.disabled_reason = Some("Document is read-only".into());
            }
        }
        commands
    }

    fn show_compact(
        &mut self, ui: &mut egui::Ui, state: &mut EngineState,
        ecad: Option<&crate::workbench::ecad::Editors>, store: &dyn ModelStore,
        settings_open: &mut bool, properties_open: &mut bool, info_open: &mut bool,
    ) -> ToolbarOutcome {
        let mut outcome = ToolbarOutcome::default();
        let mut target = None;
        egui::containers::panel::Panel::top("brep-mobile-toolbar").resizable(false).show(ui, |ui| {
            ui.horizontal_wrapped(|ui| {
                let file = super::file::file_menu(ui, state, store, settings_open, properties_open, &mut self.hits);
                outcome.file = file.action;
                outcome.recent_document = file.recent;
                outcome.plugins = file.plugins;
                outcome.javascript = file.javascript;
                let mut commands = self.commands(state, ecad, *info_open);
                if state.any_sketch_mode() {
                    commands.sort_by_key(|c| !matches!(&c.target, workbench::CommandTarget::Workbench(_)));
                }
                let menu = ui.menu_button("Tools", |ui| {
                    ui.set_max_width((ui.ctx().content_rect().width() - 32.0).max(100.0));
                    egui::ScrollArea::vertical().max_height((ui.ctx().content_rect().height() - 80.0).max(80.0)).show(ui, |ui| {
                        let mut group = String::new();
                        for command in &commands {
                            let segments = command.segments();
                            let heading = format!("{} / {}", segments[0], segments[1]);
                            if heading != group { ui.separator(); ui.weak(&heading); group = heading; }
                            if let Some(chosen) = super::ribbon::draw_command(ui, command, true, true, &mut self.hits) {
                                target = Some(chosen);
                            }
                        }
                    });
                });
                self.hits.insert("ribbon:overflow".into(), menu.response.rect);
                let (documents, button) = super::document_tabs::menu(ui, &self.documents);
                self.hits.insert("mobile:documents".into(), button.rect);
                outcome.documents = documents;
                for command in commands.iter().filter(|c| matches!(&c.target, workbench::CommandTarget::Shell("undo" | "redo"))) {
                    if let Some(chosen) = super::ribbon::draw_command(ui, command, true, false, &mut self.hits) { target = Some(chosen); }
                }
                // A DOM entry button shares this toolbar slot so the browser
                // receives the trusted click required by the Fullscreen API.
                let (slot, _) = ui.allocate_exact_size(egui::vec2(44.0, 44.0), egui::Sense::hover());
                ui.ctx().data_mut(|d| d.insert_temp(egui::Id::new("brep-mobile-fullscreen-slot"), slot));
                self.hits.insert("mobile:fullscreen".into(), slot);
                outcome.plm_review = self.plm_indicator(ui, store, settings_open);
            });
            if let Some(target) = target {
                self.dispatch_target(ui.ctx(), target, state, store, info_open, &mut outcome);
            }
        });
        outcome
    }

    fn dispatch_target(
        &mut self,
        ctx: &egui::Context,
        target: workbench::CommandTarget,
        state: &mut EngineState,
        store: &dyn ModelStore,
        info_open: &mut bool,
        outcome: &mut ToolbarOutcome,
    ) {
        use workbench::CommandTarget;
        let key = match target {
            CommandTarget::Workbench(id) => {
                outcome.workbench_button = Some(id);
                return;
            }
            CommandTarget::Plugin(id) => {
                outcome.plugin_action = Some(id);
                return;
            }
            CommandTarget::Feature(id) => {
                outcome.creation.feature = Some(id);
                return;
            }
            CommandTarget::Constraint(id) => {
                outcome.creation.constraint = Some(id);
                return;
            }
            CommandTarget::Annotation(id) => {
                outcome.creation.annotation = Some(id);
                return;
            }
            CommandTarget::Shell(key) => key,
        };
        match key {
            "file:new" => outcome.file = Some(FileAction::New),
            "file:open" => outcome.file = Some(FileAction::Open),
            "file:save" => outcome.file = Some(FileAction::Save),
            "file:saveas" => outcome.file = Some(FileAction::SaveAs),
            "file:import" => outcome.file = Some(FileAction::Import),
            "file:export" => outcome.file = Some(FileAction::Export),
            "undo" => {
                if state.sketch_mode() {
                    state.sketch_undo();
                } else if state.sketch3d_mode() {
                    state.sketch3d_undo();
                } else {
                    state.undo();
                }
            }
            "redo" => {
                if state.sketch_mode() {
                    state.sketch_redo();
                } else if state.sketch3d_mode() {
                    state.sketch3d_redo();
                } else {
                    state.redo();
                }
            }
            "fit" => outcome.zoom_to_fit = true,
            "info" => *info_open = !*info_open,
            "bug" => outcome.bug_report = true,
            "workbench:manage:plugins" => outcome.plugins = true,
            "workbench:manage:javascript" => outcome.javascript = true,
            "help" => ctx.open_url(egui::OpenUrl::new_tab(crate::automation::HELP_URL)),
            key => {
                let (field, value) = match key {
                    "wireframe" => ("wireframe", !state.settings.wireframe),
                    "projection" => (
                        "orthographic",
                        matches!(
                            state.camera.projection,
                            brep_render::view::Projection::Perspective { .. }
                        ),
                    ),
                    "show:faces" => ("showFaces", !state.settings.show_faces),
                    "show:edges" => ("showEdges", !state.settings.show_edges),
                    "show:vertices" => ("showVertices", !state.settings.show_vertices),
                    _ => return,
                };
                let _ = state.apply_settings_json(&serde_json::json!({field: value}).to_string());
                let _ = store.write(SETTINGS_KEY, &state.settings_json());
            }
        }
    }

    pub fn home_hits(&self) -> HashMap<String, egui::Rect> {
        self.hits
            .iter()
            .filter(|(key, _)| key.starts_with("wbtb:"))
            .map(|(k, r)| (k.clone(), *r))
            .collect()
    }



    /// The PLM CONNECTION INDICATOR (plan S1 part 3): present only while the
    /// store reports a PLM, so a file-only session has none. `PLM: <user>`
    /// when this session is on the server, `PLM: sign in` when it is
    /// configured and not on it (plain text: no glyph a font could lack);
    /// the tooltip says where and as whom, or why not, and a click opens
    /// Settings, whose PLM tab signs in. S4's inbox badge sits beside it
    /// while the session is on the server; the rows it opens are returned.
    fn plm_indicator(
        &mut self,
        ui: &mut egui::Ui,
        store: &dyn ModelStore,
        settings_open: &mut bool,
    ) -> Vec<crate::panels::plm_review::PlmReviewEvent> {
        use crate::plm::connection::Status;
        let Some(session) = store.plm_session() else {
            return Vec::new();
        };
        ui.separator();
        let (label, tip) = match &session.status {
            Status::Connected { url, username } => (
                format!("PLM: {username}"),
                format!("On the PLM at {url} as {username}"),
            ),
            Status::NotConnected { url, reason } => (
                "PLM: sign in".to_string(),
                match reason {
                    Some(why) => format!(
                        "Not connected to the PLM at {url}: {why}. Settings → PLM signs in."
                    ),
                    None => format!("Not connected to the PLM at {url}. Settings → PLM signs in."),
                },
            ),
        };
        let btn = ui.button(label).on_hover_text(tip);
        self.hits.insert("plm:indicator".into(), btn.rect);
        if btn.clicked() {
            *settings_open = true;
        }
        if let Some(url) = crate::panels::plm_host::web_url(store, "#/reviews") {
            let link = crate::panels::plm_host::web_link(ui, "PLM inbox", url);
            self.hits.insert("plm:inbox".into(), link.rect);
        }
        Vec::new()
    }

    /// Ask the review inbox again on the next frame: the user just decided,
    /// submitted or released something (`panels::plm_review`).
    pub fn refresh_inbox(&mut self) {
        self.inbox.refresh();
    }

    /// The review inbox badge, for tests (never drawn: the toolbar links to the
    /// web inbox instead, and publishes no blob for it).
    pub fn inbox(&self) -> &crate::panels::plm_review::InboxBadge {
        &self.inbox
    }

    /// 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)
    }

    /// For a frame the toolbar is not drawn (a focused reference pick): none
    /// of its rects may stay published.
    pub fn clear_hits(&mut self) {
        self.hits.clear();
    }

}


/// The hit keys this panel publishes (see `automation::hit_keys`).
pub static HIT_KEYS: &[HitKeyDoc] = &[
    HitKeyDoc { panel: "toolbar", prefix: "workbench:manage:plugins", meaning: "open the installed plugin manager: the Plugins row under Settings in the File menu (both styles)", command: Some("plugins_window") },
    HitKeyDoc { panel: "toolbar", prefix: "workbench:manage:javascript", meaning: "open the JavaScript editor: the JavaScript row under Settings in the File menu (both styles)", command: Some("javascript_window") },
    HitKeyDoc { panel: "toolbar", prefix: "plugin:action:", meaning: "open the form for a named plugin action", command: Some("plugin_action") },
    HitKeyDoc { panel: "toolbar", prefix: "file:menu", meaning: "open the shared File menu", command: None },
    HitKeyDoc { panel: "toolbar", prefix: "file:recent", meaning: "recent documents section and document rows, present only when nonempty", command: Some("doc_load") },
    HitKeyDoc { panel: "toolbar", prefix: "file:recent:thumb:", meaning: "the thumbnail square at the left of a recent-document row (file:recent:thumb:name): the document's saved picture, or the neutral placeholder before it has one", command: Some("doc_load") },
    HitKeyDoc { panel: "toolbar", prefix: "ribbon:", meaning: "ribbon tabs (ribbon:tab:<Tab>), group borders (ribbon:group:<Group>) and the overflow menu (ribbon:overflow)", command: None },
    #[cfg(target_arch = "wasm32")]
    HitKeyDoc { panel: "toolbar", prefix: "mobile:", meaning: "compact browser drawer controls", command: None },
    HitKeyDoc { panel: "toolbar", prefix: "wbtb:", meaning: "Home creation commands; same keys as the Classic feature strip", command: Some("feature_add") },
    HitKeyDoc { panel: "toolbar", prefix: "file:new", meaning: "new document", command: Some("doc_new") },
    HitKeyDoc { panel: "toolbar", prefix: "file:newmenu", meaning: "legacy key for the flat New part row in File", command: Some("doc_new_of_class") },
    HitKeyDoc { panel: "toolbar", prefix: "file:newclass:", meaning: "a New menu row: file:newclass:normal, file:newclass:family, file:newclass:template (visible directly in File)", command: Some("doc_new_of_class") },
    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 \u{2014} the whole model, or the OPEN SHEET's whole paper while a sheet is open", 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: "plm:", meaning: "plm:indicator opens connection settings; plm:inbox opens the PLM web inbox in another tab", 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: "legacy opener key for the shared File menu", command: Some("settings_set") },
    HitKeyDoc { panel: "toolbar", prefix: "workbench:btn:", meaning: "one button of the ACTIVE workbench's action row (workbench:btn:<button id>) \u{2014} published only while that button is offered, so Drawing's `drawing.sheet_close`, the six `drawing.dim.*` constructions, the two `drawing.ord.*` sets, `drawing.section` and `drawing.detail` are here exactly while a sheet is open, and the ten `sketch.tool.*` draw tools and `sketch.autoconstrain` (which EVERY workbench carries) exactly while a sketch is being edited; the armed construction and the armed draw tool draw pressed. A button that is offered but DISABLED (an eCAD action its editor does not allow yet) draws greyed and `workbench_button` refuses it with the reason. A MENU button (PCB's copper-layer picker) opens a menu whose entries publish this key while it is OPEN. Classic wraps available buttons; Ribbon places them in their declared tabs and groups, compacting Large buttons before whole-group overflow. Menu entries publish only while their menu is open", command: Some("workbench_button") },
    HitKeyDoc { panel: "toolbar", prefix: "workbench:item:", meaning: "pick a radio row from File / Workbench (workbench:item:id)", command: Some("settings_set") },
];