BREP_app 0.13.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
//! The 3D viewport — everything that draws + drives the central `brep-render`
//! scene, split out of the thin app shell.
//!
//! [`Viewport`] owns the GPU seam between the engine and egui:
//!
//! * The engine's [`RenderCore`] renders the 3D into an app-owned OFFSCREEN
//!   texture (its own MSAA + full-target clear), on eframe's SHARED wgpu
//!   device/queue.
//! * That offscreen texture is composited into egui's own frame via an
//!   `egui_wgpu` paint callback (`ViewportCallback`/`ViewportPaint`) — a
//!   fullscreen-triangle blit into egui's render pass, at the viewport rect egui
//!   scissors for us — so the 3D and the egui UI share one wgpu frame.
//! * Pointer / wheel / ViewCube over the viewport route into `EngineState`
//!   (mirrors `desktop.rs`).
//!
//! [`Viewport::show`] is the single clean entry the shell calls to draw +
//! interact with the central viewport; [`EngineState`] stays the brain and is
//! borrowed in, never owned here.

use brep_render::controls::{BUTTON_LEFT, BUTTON_MIDDLE, BUTTON_RIGHT};
use brep_render::engine_state::EngineState;
use brep_render::pick::PickCandidate;
use brep_render::style::MultiSelectMode;
use brep_render::render::{FrameParams, GpuScene, RenderCore, COLOR_FORMAT};
use eframe::egui;
use eframe::egui_wgpu;
use std::sync::Arc;

/// GPU-side resources the callback needs, stored in egui's `callback_resources`
/// type-map. Refreshed whenever the offscreen texture is (re)created.
struct ViewportPaint {
    pipeline: wgpu::RenderPipeline,
    bind_group: wgpu::BindGroup,
}

/// The paint callback: a zero-sized, `Send + Sync` marker. All the heavy GPU
/// resources live in `callback_resources`; the actual 3D render already happened
/// (into the offscreen texture) during `App::ui`, so `paint` only blits.
struct ViewportCallback;

/// The app-owned offscreen color target the engine resolves into.
struct Offscreen {
    view: wgpu::TextureView,
    w: u32,
    h: u32,
}

/// The open PICK LIST — the "candidates under the cursor" popup that opens on a
/// plain click with MULTIPLE filter-admitted items under the pointer (and on
/// Alt+click explicitly): the ranked, filter-respecting candidates captured when
/// it opened + the screen anchor (cursor position) to draw the list at.
///
/// `reference` marks a list opened while a reference field is picking: its rows
/// are the field's admitted candidates and a chosen row goes into the field's
/// running list, never into the modeling selection. A list whose mode no longer
/// matches (the picker started or ended under it) is dropped.
struct CandidatePopup {
    anchor: egui::Pos2,
    candidates: Vec<PickCandidate>,
    reference: bool,
}

/// The central 3D viewport: the engine's render core + the app-owned offscreen
/// texture + the egui blit that composites it into egui's frame, plus the
/// pointer/wheel/ViewCube routing. Borrows `&mut EngineState` to draw + drive;
/// never owns the engine brain.
pub struct Viewport {
    /// The engine's wgpu render core, built from eframe's device/queue/format.
    core: RenderCore,
    gpu_scene: GpuScene,
    context_gpu_scene: GpuScene,
    /// egui's renderer, so we can push `ViewportPaint` into its callback map.
    egui_renderer: Arc<egui::mutex::RwLock<egui_wgpu::Renderer>>,
    blit_layout: wgpu::BindGroupLayout,
    blit_sampler: wgpu::Sampler,
    blit_pipeline: wgpu::RenderPipeline,
    offscreen: Option<Offscreen>,
    /// Render on the next frame although the engine is not dirty: the
    /// offscreen texture was rebuilt (a new physical size, which a
    /// pixels-per-point change makes at the same logical size) or the GPU
    /// scene was dropped (`forget_document`).
    force_render: bool,
    /// The render inputs of the last rendered frame, for the stale-frame check.
    #[cfg(feature = "automation")]
    rendered_inputs: Option<[(&'static str, u64); 5]>,
    /// True while a camera drag (orbit/pan) started over the viewport is live.
    dragging: bool,
    touch: touch::TouchNavigation,
    touch_suppressed: bool,
    touch_drag_offset: (f64, f64),
    touch_drawing: bool,
    chrome_obscured: bool,
    /// True while a transform-gizmo HANDLE drag (armed via the in-viewport center
    /// sphere toggle) is live — the pointer press landed on a handle, so the drag
    /// drives the gizmo (edits the feature's transform) instead of orbiting the camera.
    gizmo_dragging: bool,
    /// True while a COMPONENT Move gizmo handle drag is live (assemblies §8.5):
    /// the press landed on the armed component gizmo, so the drag free-moves the
    /// GIZMO ([`EngineState::component_drag_to`]) and the release COMMITS the
    /// composed pose + re-solves ([`EngineState::component_release`]) instead of
    /// orbiting the camera.
    component_gizmo_dragging: bool,
    /// `Some(field_key)` while a DIMENSION-ARROW drag is live (Fix 4) — the press
    /// landed on an arrowhead handle in ◎ dimension mode, so the drag edits that
    /// param via [`EngineState::feature_dimension_drag`] instead of orbiting the
    /// camera. `None` when no arrow drag is in flight.
    dim_dragging: Option<String>,
    /// True while a sketch POINT drag started over the viewport is live (S2) — the
    /// press grabbed a movable sketch point, so the drag moves it instead of
    /// orbiting the camera.
    sketch_dragging: bool,
    /// True while a 3D sketch POINT drag started over the viewport is live —
    /// the press grabbed a movable 3D sketch point.
    sketch3d_dragging: bool,
    /// True while a freehand handdraw STROKE started over the viewport is live
    /// (S6b-3) — the press began a stroke, so the drag captures it as a polyline
    /// instead of orbiting the camera.
    sketch_handdrawing: bool,
    /// The viewport rect (egui points) from the last `show`, so the shell can
    /// publish the viewport origin for the headed verifier (which needs to turn
    /// the engine's viewport-local pick coords into page pixels).
    last_rect: Option<egui::Rect>,
    /// The open pick-list popup (a plain multi-candidate click / Alt+click), or
    /// None. Holds the ranked candidates snapshot + the cursor anchor.
    candidate_popup: Option<CandidatePopup>,
    /// True on the frame the popup opens, so the OPENING Alt+click isn't misread
    /// as a click-outside that would close it immediately.
    candidate_popup_fresh: bool,
    /// The HOVER DWELL clock: `ctx.input(|i| i.time)` at the moment the hover
    /// highlight the pointer is now showing FIRST appeared, or `None` while no
    /// highlight is lit. It is what tells the two plain-click behaviours apart —
    /// a click on a highlight that has been standing for the dwell opens the pick
    /// list, a quicker one selects the highlight outright — so the rule is a
    /// property of the thing the user can SEE, not of a hidden timer: the
    /// highlight's own age. Restarted whenever the highlight CHANGES
    /// (`EngineState::hover_at` reports it, which also covers the camera or the
    /// geometry moving under a still pointer) AND whenever the POINTER itself
    /// moves — so travelling across one big face, where the highlight never
    /// changes, accumulates no dwell. See
    /// `interaction::Viewport::hover_dwell_armed`.
    hover_lit_since: Option<f64>,
    /// Where the pointer was on the frame before, so the clock above can be
    /// restarted on real MOVEMENT. Deliberately not egui's own
    /// `time_since_last_movement`: that restamps on ANY `PointerMoved` event,
    /// including a redundant one carrying the position the pointer already had,
    /// which some input paths (and the automation queue, which re-sends a
    /// position before a click) emit — and a dwell a stationary pointer can lose
    /// to a no-op event is a dwell the user cannot reach on purpose.
    hover_last_pos: Option<egui::Pos2>,
    /// The OPEN popup's bounding rect (egui points) from the last draw, so the
    /// viewport click handler can tell a click ON the popup from one BEHIND it
    /// (an entry click belongs to the popup, not a scene pick).
    candidate_popup_rect: Option<egui::Rect>,
    /// Per-entry screen rects of the OPEN popup (index-aligned to its
    /// candidates), published for the headed verifier to click a specific entry.
    candidate_hits: Vec<egui::Rect>,
    /// The dimension label currently being edited (S5): its constraint id + the live
    /// text buffer, or `None` when no field is open. Clicking a label opens it,
    /// Enter applies, Esc cancels.
    editing_dim: Option<(serde_json::Value, String)>,
    /// True on the frame a dim edit field opens, so its seeded text gets focus
    /// before the click-outside logic can close it.
    dim_edit_fresh: bool,
    /// The FEATURE-dimension label currently being edited (FD-1): `(feature_id,
    /// field_key, live text buffer)`, or `None`. Mirrors `editing_dim` for the ◎
    /// dimension-gizmo mode. Clicking a dim label opens it, Enter applies via
    /// [`EngineState::feature_dimension_set_value`], Esc cancels; dragging drives
    /// [`EngineState::feature_dimension_drag`].
    editing_feature_dim: Option<(String, String, String)>,
    /// True on the frame a feature-dim edit field opens (focus seeding).
    feature_dim_edit_fresh: bool,
    /// True while an ASSEMBLY-CONSTRAINT handle drag is live — the press
    /// landed on a distance leader / angle-arc handle, so the drag previews
    /// that constraint's value via [`EngineState::constraint_drag_to`]
    /// (release commits + auto-solves) instead of orbiting the camera.
    constraint_dragging: bool,
    /// The constraint id whose LABEL the pointer hovered LAST frame (the
    /// element-highlight lane) — cleared through
    /// [`EngineState::constraint_hover_end`] when the pointer leaves the labels.
    constraint_label_hovered: Option<String>,
    /// The PMI label chip under the pointer (its referenced geometry is
    /// highlighted; ended exactly once when the pointer leaves).
    pmi_label_hovered: Option<String>,
    /// The PMI label chip being dragged (its world anchor follows the pointer
    /// on the label's depth plane; release ends the coalesced undo step).
    pmi_label_dragging: Option<String>,
    /// The PMI chips drawn LAST frame, `(annotation id, screen rect)` — the
    /// `__brepPmiLabelHit` verifier global (drag / click a chip by rect).
    pmi_label_hits: Vec<(String, egui::Rect)>,
    /// The open RIGHT-CLICK menu of a PMI label chip: the annotation it belongs
    /// to and the screen point it opened at. Its entries are the annotation's
    /// PMI tree row menu.
    pmi_label_menu: Option<(String, egui::Pos2)>,
    /// A component's open RIGHT-CLICK menu: the ACOMP id and where it opened.
    component_menu: Option<(String, egui::Pos2)>,
    /// Its entry rects (`componentmenu/menuitem:<id>:<action>`).
    component_menu_hits: std::collections::HashMap<String, egui::Rect>,
    /// The last viewport right-click as the component menu read it (what the
    /// pointer hit, which component owns it) — `__brepComponentMenu`.
    component_menu_probe: serde_json::Value,
    /// A document-level flow the menu asked for (Edit Part, Replace…), taken
    /// by the shell after the frame ([`Self::take_component_request`]).
    component_request: Option<crate::panels::component_actions::ComponentActionRequest>,
    /// Empty space double-clicked this frame ([`Self::take_empty_double_click`]).
    empty_double_click: bool,
    /// That menu's entries this frame, `menuitem:<annotation id>:<action id>`
    /// — the `__brepPmiMenuHit` verifier global.
    pmi_menu_hits: std::collections::HashMap<String, egui::Rect>,
    /// The SHEET mode's pan/zoom and live drag. While the document has an open
    /// sheet the tile draws paper instead of the 3D scene (`sheet.rs`).
    sheet: sheet::SheetViewport,
    /// The eCAD host's rects and card state while an eCAD workbench draws its
    /// editor here (`ecad.rs`). The editors themselves, and their views, live
    /// on the document.
    ecad: ecad::EcadViewport,
    /// The Move gizmo's typed-value card (`move_card.rs`): its fields and rects.
    move_card: move_card::MoveCard,
    /// The constraint chips drawn LAST frame, `(constraint id, screen rect)`:
    /// a chip's menu closes with it, and the rects are the
    /// `__brepConstraintLabelHit` verifier global.
    constraint_chips_drawn: Vec<(String, egui::Rect)>,
    /// The open RIGHT-CLICK menu of a constraint chip: the constraint and the
    /// screen point it opened at.
    constraint_label_menu: Option<(String, egui::Pos2)>,
    /// That menu's entries this frame, `menuitem:<constraint id>:<action id>`
    /// — the `__brepConstraintMenuHit` verifier global.
    constraint_menu_hits: std::collections::HashMap<String, egui::Rect>,
}

impl Viewport {
    /// Drop everything this viewport is holding ABOUT ONE DOCUMENT — called when
    /// the shell switches the active document (see `crate::document`).
    ///
    /// The GPU cache is the load-bearing half: [`GpuScene`] keys its retained
    /// buffers by SOLID NAME and reuses one while the solid's `revision` is
    /// unchanged, but revisions are per-`EngineState` and every document counts
    /// from the same place — so document B's "Box" rev 1 would be served
    /// document A's uploaded mesh. Only the active document renders, so a full
    /// re-upload on the switch is the right price. The transients after it
    /// (pick popup, open dimension editors, in-flight drags) all name entities
    /// of the document that is going away.
    /// Draw the engine's scene at `width`×`height` under its own settings and
    /// read the frame back as packed RGB (top row first): a motion study's
    /// export frame. The GPU scene is the viewport's own, synced to the scene
    /// as it stands, so a posed frame costs its pose upload and the draw.
    #[cfg(not(target_arch = "wasm32"))]
    pub(crate) fn render_rgb(&mut self, state: &EngineState, camera: &brep_render::camera::Camera, width: u32, height: u32) -> Result<Vec<u8>, String> {
        self.core.sync_scene(&mut self.gpu_scene, &state.scene, &state.settings, state.settings_generation);
        self.core.render_gpu_scene_to_rgb(&mut self.gpu_scene, &state.scene, camera, width, height, &state.settings)
    }

    pub fn forget_document(&mut self) {
        self.gpu_scene = GpuScene::default();
        self.context_gpu_scene = GpuScene::default();
        self.force_render = true;
        self.close_candidate_popup();
        self.candidate_hits.clear();
        self.editing_dim = None;
        self.editing_feature_dim = None;
        self.constraint_label_hovered = None;
        self.pmi_label_hovered = None;
        self.pmi_label_dragging = None;
        self.pmi_label_hits.clear();
        self.pmi_label_menu = None;
        self.pmi_menu_hits.clear();
        self.component_menu = None;
        self.component_menu_hits.clear();
        self.sheet.forget();
        self.ecad = ecad::EcadViewport::default();
        self.move_card = move_card::MoveCard::default();
        self.constraint_chips_drawn.clear();
        self.constraint_label_menu = None;
        self.constraint_menu_hits.clear();
        self.dragging = false;
        self.gizmo_dragging = false;
        self.component_gizmo_dragging = false;
        self.dim_dragging = None;
        self.sketch_dragging = false;
        self.sketch3d_dragging = false;
        self.sketch_handdrawing = false;
        self.constraint_dragging = false;
    }

    /// Refit the OPEN SHEET's paper on this frame's paint — what the toolbar's
    /// Zoom-to-fit does while a sheet is open (see `BrepApp::zoom_to_fit`).
    /// Clears ONLY the fitted marker, so a live pan or drag and the rects
    /// published this frame all survive.
    pub fn request_sheet_fit(&mut self) {
        self.sheet.request_fit();
    }

    /// Drop the sheet viewport's transform and transients — the caller has just
    /// closed the sheet, so the next frame draws the 3D scene.
    pub fn forget_sheet(&mut self) {
        self.sheet.forget();
    }

    /// Close the pick-list popup if open; returns whether one WAS open. The app
    /// shell's global Escape routes here FIRST so dismissing the list never
    /// clears a selection built through it.
    pub fn close_candidate_popup(&mut self) -> bool {
        let was_open = self.candidate_popup.is_some();
        self.candidate_popup = None;
        self.candidate_popup_rect = None;
        was_open
    }
}

mod blit;
pub(crate) mod ecad;
mod interaction;
mod labels;
mod move_card;
mod popups;
mod sheet;
pub(crate) mod touch;

impl Viewport {
    pub(crate) fn set_chrome_obscured(&mut self, obscured: bool) { self.chrome_obscured = obscured; }
}

pub use popups::{modal_open_last_pass, popup_open_last_pass};

/// The hit keys the viewport publishes (see `automation::hit_keys`).
pub static HIT_KEYS: &[crate::automation::hit_keys::HitKeyDoc] = &[
    crate::automation::hit_keys::HitKeyDoc { panel: "cage", prefix: "point:", meaning: "one zero-size rect per CONTROL POINT of the NURBS Solid whose form is open (cage/point:<index> — 0 the bottom pole, then every ring bottom-up, the top pole last), in SCREEN points, at the dot the viewport draws for it. A click SELECTS the point and arms the move/rotate gizmo at the selection's centre; with Shift / Ctrl held (`modifiers_set`) it TOGGLES the point into the selection, so several points move as one. The form's Cage section (`history/cage:`) grows the selection to whole rings and columns and inserts new loops beside it", command: None },
    crate::automation::hit_keys::HitKeyDoc { panel: "candidate", prefix: "", meaning: "one rect per entry of the open pick-candidate popup, keyed by the entry's index \u{2014} the same index `__brepCandidates` carries. `click_widget candidate/<i>` picks that entry (replace, or toggle in Click-toggles mode / with Ctrl held), which is how a script chooses between overlapping faces without a coordinate", command: None },
    crate::automation::hit_keys::HitKeyDoc { panel: "pmilabel", prefix: "", meaning: "one rect per PMI label chip, keyed by annotation id \u{2014} a click SELECTS the annotation (while a reference pick runs, the click goes to the pick under the chip instead, as if the chip were not there), a double click opens its form, a drag moves the label, and a RIGHT-click selects it and opens its menu (`pmimenu/menuitem:`)", command: None },
    crate::automation::hit_keys::HitKeyDoc { panel: "componentmenu", prefix: "menuitem:", meaning: "one rect per entry of a component's open RIGHT-CLICK menu in the 3D view (componentmenu/menuitem:<ACOMP id>:<action id>) \u{2014} a right-click on any face of an assembly component selects that component and opens the same component actions the context bar and the BOM row menu offer (move, move-copy, open-part, replace, toggle-fixed, delete)", command: None },
    crate::automation::hit_keys::HitKeyDoc { panel: "pmimenu", prefix: "menuitem:", meaning: "one rect per entry of a PMI label chip's open RIGHT-CLICK menu (pmimenu/menuitem:<annotation id>:<action id>) \u{2014} the same entries, and the same dispatch, as that annotation's PMI tree row menu (`pmi/menuitem:`): edit, move-up, move-down, delete", command: None },
    crate::automation::hit_keys::HitKeyDoc { panel: "constraintlabel", prefix: "", meaning: "one rect per assembly-constraint chip drawn in the 3D view, keyed by constraint id \u{2014} a click selects the constraint and opens its form (while a reference pick runs, the click goes to the pick under the chip instead, as if the chip were not there); a RIGHT-click selects it and opens its menu (`constraintmenu/menuitem:`)", command: None },
    crate::automation::hit_keys::HitKeyDoc { panel: "constraintmenu", prefix: "menuitem:", meaning: "one entry of an assembly-constraint chip's open RIGHT-CLICK menu (constraintmenu/menuitem:<constraint id>:<flip|edit|suppress|delete>) \u{2014} a right-click on a chip selects that constraint and opens the same entries, run through the same dispatch, as its Constraints panel row's menu (`assemblyconstraints/acon:menuitem:`)", command: None },
    crate::automation::hit_keys::HitKeyDoc { panel: "movecard", prefix: "card", meaning: "the Move gizmo's typed-value card, in the viewport's top-left corner, drawn while the component Move gizmo is armed (one component or a group) \u{2014} what it did last is `__brepComponentMove.typed`", command: None },
    crate::automation::hit_keys::HitKeyDoc { panel: "movecard", prefix: "field:", meaning: "one text field per motion on the Move card: movecard/field:x, field:y, field:z (mm along the gizmo's axes) and field:rx, field:ry, field:rz (degrees about them; a group's axes are the world's). Click one, type a value and press Enter: it moves by exactly that much, solved against the mates, in one undo step, and the field clears. When the mates allow only part of it, that part is applied and the card's last line names the constraint that stopped the rest; when they allow none, nothing moves and a notice names it", command: None },
    // The SHEET mode's own widgets — the third VIEWPORT-hosted widget panel,
    // in screen points with no `panel:clip` (see `viewport/sheet.rs`).
    crate::automation::hit_keys::HitKeyDoc { panel: "sheet", prefix: "paper", meaning: "the open sheet's PAPER rect in screen points \u{2014} a drag on it pans the sheet view", command: None },
    crate::automation::hit_keys::HitKeyDoc { panel: "sheet", prefix: "view:", meaning: "one rect per PLACED view on the open sheet (sheet:view:<placement id>), in screen points \u{2014} DRAGGING it moves the view on the paper (one undo step per drag), a click opens its form in the Sheets pane, a RIGHT-click selects it and opens its menu (`sheet/menuitem:`)", command: Some("sheet_move_view") },
    // The paper carries NO toolbar: fit is the main toolbar's own `toolbar/fit`
    // (which frames the open sheet), zoom is the wheel, and Back to 3D and the
    // sheet's constructions are the Drawing workbench's buttons
    // (`toolbar/workbench:btn:drawing.*`), offered while a sheet is open.
    crate::automation::hit_keys::HitKeyDoc { panel: "sheet", prefix: "dim:", meaning: "one rect per PLACED dimension's value box (sheet/dim:<dimension id>) \u{2014} DRAG it to move that dimension's line, one coalesced undo step per drag; click it to SELECT the dimension, double-click it to open its form in the Sheets pane, right-click it for its menu (`sheet/menuitem:`)", command: Some("sheet_move_dimension") },
    crate::automation::hit_keys::HitKeyDoc { panel: "sheet", prefix: "ord:", meaning: "one rect per ORDINATE SET's datum value box (sheet/ord:<set id>) \u{2014} DRAG it to move the whole run's shared baseline, one coalesced undo step per drag; click it to SELECT the set, double-click it to open the set's form in the Sheets pane, right-click it for its menu (`sheet/menuitem:`). A set whose DATUM is lost publishes none: it has no datum box to grab", command: Some("sheet_move_ordinate") },
    crate::automation::hit_keys::HitKeyDoc { panel: "sheet", prefix: "menuitem:", meaning: "one rect per entry of the open RIGHT-CLICK menu on the paper (sheet/menuitem:<object id>:<action id>) \u{2014} a right-click on a placement, a dimension's value box or an ordinate set's datum box selects that object and opens its Sheets tree row menu (`sheets/menuitem:`), with the same entries and the same dispatch; a right-click on bare paper opens the open sheet's own", command: None },
    crate::automation::hit_keys::HitKeyDoc { panel: "sheet", prefix: "anchor:", meaning: "one rect per ANCHOR CANDIDATE of every placement on the open sheet (sheet/anchor:<reference>, e.g. sheet/anchor:SV2:vertex:Box#1) \u{2014} each projected vertex, each named edge's two ends and midpoint, and each circular edge's centre. Published only while the REFERENCE PICKER is up for a sheet object's reference row (a dimension's anchors, an ordinate set's datum or members, a section's cutting line, a detail's centre or rim \u{2014} opened by that row's `#activate`), because the list is as long as the model's topology; clicking one adds it to the field's list (a list commits from the field's own `#activate`, Done while the pick runs; a single field commits on its pick). A DETAIL placement offers only the candidates inside its circle", command: Some("sheet_anchor_pick") },
    // The eCAD editors' widgets — the tile an eCAD workbench draws its editor
    // in, and the tool card over it (`viewport/ecad.rs`), in screen points.
    crate::automation::hit_keys::HitKeyDoc { panel: "ecad", prefix: "canvas", meaning: "the eCAD editor's tile in screen points, while the Diagram, PCB, Symbol or Pads workbench draws its editor in place of the 3D view \u{2014} the sheet, board, symbol or pads canvas; a click, drag or wheel over it is the editor's own", command: None },
    crate::automation::hit_keys::HitKeyDoc { panel: "ecad", prefix: "label", meaning: "the tool card's Label name field, published while the Label tool is armed in the Diagram or PCB sheet editor; it takes the keyboard on the frame the tool arms, so the name is typed before the label is placed", command: None },
    crate::automation::hit_keys::HitKeyDoc { panel: "ecad", prefix: "component:", meaning: "one rect per component on the eCAD sheet (Diagram or PCB schematic), keyed by its reference (ecad/component:R1) \u{2014} the component's bounds as the editor hit-tests them; a click selects it, a drag moves it. A part whose symbol is split into GATES (a KiCad multi-unit device such as a dual op-amp) also publishes one rect per gate by the sheet's name for it (ecad/component:U1A, ecad/component:U1B), where a click selects the part with that gate picked and a drag or Rotate moves that gate alone; its bare reference is its first gate, so a drag of it moves gate A too. A Shift-click or Shift-drag on any gate picks the WHOLE part, and the drag, Move or Rotate then moves every gate together", command: None },
    crate::automation::hit_keys::HitKeyDoc { panel: "ecad", prefix: "pin:", meaning: "on the eCAD sheet, one small rect per component pin centred on its tip (ecad/pin:R1.2) \u{2014} where a wire starts or ends with the Wire tool, or a wiring connection is dragged from and to; in the Symbol workbench, one rect per pin of the symbol (ecad/pin:1) spanning the pin, and, for the SELECTED pin alone, one small rect on each of its two ends (ecad/pin:1:tip, ecad/pin:1:root) \u{2014} a drag from either aims that pin in one of the four directions and sets its length, holding the other end", command: None },
    crate::automation::hit_keys::HitKeyDoc { panel: "ecad", prefix: "unnumbered-pin:", meaning: "in the Symbol workbench, a pin whose number is BLANK, keyed by its index in the symbol (ecad/unnumbered-pin:1), spanning the pin, with `:tip` and `:root` on its two ends while it is selected, exactly as `pin:` \u{2014} out of the `pin:` family because every key under it could be some pin's number, and `pin:` bare is the family's own prefix", command: None },
    crate::automation::hit_keys::HitKeyDoc { panel: "ecad", prefix: "wire:", meaning: "one small rect per wire on the eCAD sheet, by its index in the document, centred on the middle of its longest segment \u{2014} a click selects the wire", command: None },
    crate::automation::hit_keys::HitKeyDoc { panel: "ecad", prefix: "label:", meaning: "one small rect per net label on the eCAD sheet, by index, centred on the point it labels", command: None },
    crate::automation::hit_keys::HitKeyDoc { panel: "ecad", prefix: "junction:", meaning: "one small rect per junction dot on the eCAD sheet, by index", command: None },
    crate::automation::hit_keys::HitKeyDoc { panel: "ecad", prefix: "no_connect:", meaning: "one small rect per no-connect cross on the eCAD sheet, keyed by the pin it marks (ecad/no_connect:R1.2) and centred on that pin's tip \u{2014} where a click with the No-connect tool takes the mark off", command: None },
    crate::automation::hit_keys::HitKeyDoc { panel: "ecad", prefix: "power_flag:", meaning: "one small rect per power flag (PWR_FLAG) on the eCAD sheet, keyed by the pin it is on (ecad/power_flag:U1.4) and centred on that pin's tip \u{2014} where a click with the Power flag tool takes the flag off", command: None },
    crate::automation::hit_keys::HitKeyDoc { panel: "ecad", prefix: "power_net", meaning: "the tool card's Power net field, published while the Power symbol tool is armed in the PCB sheet editor, with power_net:preset above it: one dropdown of the four usual nets (GND, VCC, +3V3, +5V; its open entries power_net:preset#<name>) \u{2014} the net the next power symbol joins", command: None },
    crate::automation::hit_keys::HitKeyDoc { panel: "ecad", prefix: "part:", meaning: "on the PCB board view, one rect per placed part's courtyard, keyed by its reference (ecad/part:R1) \u{2014} a click selects it, a drag moves it", command: None },
    crate::automation::hit_keys::HitKeyDoc { panel: "ecad", prefix: "track:", meaning: "on the PCB board view, one small rect per track by index, centred on the middle of its longest segment (a drag there slides that segment), and `track:{i}.corner:{j}` on each interior corner j of it (a drag there moves that corner)", command: None },
    crate::automation::hit_keys::HitKeyDoc { panel: "ecad", prefix: "via:", meaning: "on the PCB board view, one rect per via by index", command: None },
    crate::automation::hit_keys::HitKeyDoc { panel: "ecad", prefix: "zone:", meaning: "on the PCB board view, one small rect per copper zone by index, a quarter of the way along its longest outline edge (a click selects the zone, a drag moves the whole zone and refills it), `zone:{i}.corner:{j}` on each corner j (a drag moves that corner and refills), and, while the zone is selected, `zone:{i}.edge:{j}` on the middle of each edge j (a drag there puts a new corner in)", command: None },
    crate::automation::hit_keys::HitKeyDoc { panel: "ecad", prefix: "pad:", meaning: "one rect per PAD, its size as drawn \u{2014} in the Pads workbench keyed by the pad's number (ecad/pad:1), and on the PCB board view by its part and number (ecad/pad:R1.2), where a click SELECTS that pad in its own right and a drag from it moves the part it belongs to. A MECHANICAL pad has no number: in the Pads workbench it is `unnumbered-pad:`, and the board publishes no key for one; it is clicked by its place on the canvas", command: None },
    crate::automation::hit_keys::HitKeyDoc { panel: "ecad", prefix: "unnumbered-pad:", meaning: "in the Pads workbench, a MECHANICAL pad \u{2014} one whose number is BLANK \u{2014} keyed by its index in the footprint (ecad/unnumbered-pad:1), its size as drawn, exactly as `pad:` \u{2014} out of the `pad:` family because every key under it could be some pad's number, and `pad:` bare is the family's own prefix. Its Inspector row is `inspector:row:unnumbered-pad:<index>`", command: None },
    crate::automation::hit_keys::HitKeyDoc { panel: "ecad", prefix: "silk:", meaning: "in the Pads workbench, one small rect per SILKSCREEN line by its index in the footprint (ecad/silk:0), centred on the middle of the line's longest segment \u{2014} a click selects that line (a pad under the same point wins, as it does for a pointer), a drag from it moves it. In the PCB board view, one per silkscreen line of each placed part, by the part's reference and the line's index in its footprint (ecad/silk:R1.0), on a point the canvas answers with that line \u{2014} a click selects the line (copper under the same point wins), a drag from it moves the PART, and Delete leaves it, since it belongs to the footprint", command: None },
    crate::automation::hit_keys::HitKeyDoc { panel: "ecad", prefix: "inspector:", meaning: "in the Symbol, Pads and PCB (board view) workbenches, the INSPECTOR's buttons, fields and list rows (ecad/inspector:next_unbound, ecad/inspector:number_it, ecad/inspector:pin_number, ecad/inspector:aim:up, ecad/inspector:tip:x; Pads: ecad/inspector:place_pad:<pin>, ecad/inspector:put_on:<pin>, ecad/inspector:make_mechanical, ecad/inspector:add_copies; PCB: ecad/inspector:autoroute, ecad/inspector:vias and its choices ecad/inspector:vias:allow|avoid|never, ecad/inspector:run_drc, a finding ecad/inspector:row:finding:<index in the list>, ecad/inspector:fit_outline, ecad/inspector:auto_place, ecad/inspector:outline_width, ecad/inspector:apply_size, ecad/inspector:copper_layers, ecad/inspector:rules_section, ecad/inspector:rule:clearance, ecad/inspector:apply_rules, the Net classes section ecad/inspector:net_classes_section, its net picker ecad/inspector:net_class:net and a net in it ecad/inspector:net_class:net:+5V, the class a net is put in ecad/inspector:net_class:assign with ecad/inspector:net_class:assign:auto (back to the patterns) and ecad/inspector:net_class:assign:Power, the net's readout ecad/inspector:net_class:readout, ecad/inspector:net_class:add, and per class ecad/inspector:net_class:Default and ecad/inspector:net_class:Power:name|clearance|track_width|via_diameter|via_drill|patterns|delete, for a selected zone ecad/inspector:zone_net, ecad/inspector:zone_layer, ecad/inspector:thermal_gap, ecad/inspector:spoke_width, ecad/inspector:min_width, ecad/inspector:fill_zones and with the Zone tool up ecad/inspector:zone_net, ecad/inspector:close_zone, and for a selected part ecad/inspector:rotate, ecad/inspector:flip, ecad/inspector:row:pad:R1.2), a list row by the key its item has on the canvas (ecad/inspector:row:pin:3, ecad/inspector:row:silk:0), and each list's resize grip (ecad/inspector:items_grip; Pads also ecad/inspector:pins_grip, under its Symbol pins list); on the Diagram and PCB SHEET, the Connectivity panel's electrical rule check: its button (ecad/inspector:erc:run, which runs the check and, pressed again, hides the findings), one row per finding by its place in the list (ecad/inspector:erc:row:0, a click zooms the sheet to it) and the fix a finding offers under its row (ecad/inspector:erc:fix:0) \u{2014} each where a click presses it, and only while at least half of it is in view and the Inspector is drawn: a widget scrolled out of the pane or out of a list is not published, and nor is anything while the Inspector is behind another tab", command: None },
    crate::automation::hit_keys::HitKeyDoc { panel: "ecad", prefix: "menuitem:", meaning: "on the PCB board view, one rect per entry of the canvas's open RIGHT-CLICK menu (ecad/menuitem:rotate, ecad/menuitem:flip_side, ecad/menuitem:select_part, ecad/menuitem:route_from_here, ecad/menuitem:fill_zones, ecad/menuitem:delete) \u{2014} a right-click on a part, pad or silkscreen line selects it and opens its part's menu, on a track or a via its own; published only while the menu is open", command: None },
    crate::automation::hit_keys::HitKeyDoc { panel: "ecad", prefix: "stock", meaning: "the tool card's Stock part for new connections field, published while a wiring diagram's Select tool is up; each connection drawn after it carries that stock part number", command: None },
    crate::automation::hit_keys::HitKeyDoc { panel: "ecad", prefix: "outdated:card", meaning: "the out-of-date card over the top middle of the Diagram or PCB tile (folded to a one-line chip after Later), published while a component on the sheet was placed from a part whose saved version changed since (ecad_state's outdated lists them)", command: None },
    crate::automation::hit_keys::HitKeyDoc { panel: "ecad", prefix: "outdated:later", meaning: "the out-of-date card's Later button: folds the card to a one-line chip (outdated:card, a click opens it again) and changes nothing; the components stay ringed, and a changed list opens the card again", command: None },
    crate::automation::hit_keys::HitKeyDoc { panel: "ecad", prefix: "unreadable:card", meaning: "the card that takes the whole Diagram, PCB, Symbol or Pads tile while the editor could not read its block (ecad_state's unreadable says why): it names the parts at fault and never shows the empty sheet's first-run card", command: None },
    crate::automation::hit_keys::HitKeyDoc { panel: "ecad", prefix: "unreadable:fix", meaning: "the unreadable card's repair (ecad_state's repair, e.g. Rename the second R5 to R6): every repeated or blank reference to the next free one of its prefix, as one undo step; drawn only where a rename frees the sheet", command: None },
    crate::automation::hit_keys::HitKeyDoc { panel: "ecad", prefix: "outdated:update", meaning: "the out-of-date card's Update parts button: Assembly > Constraints > Update components, run from where the user is, as one undo step", command: None },
    // A VIEWPORT-hosted widget, so its rects are in screen points and the panel
    // publishes no `panel:clip` (the viewport does not scroll) — see
    // `Viewport::gizmo_anchor_hits_json`, which is why this is not in the
    // history panel's blob.
    crate::automation::hit_keys::HitKeyDoc { panel: "gizmo", prefix: "anchor", meaning: "the armed gizmo's FRAME ORIGIN as a zero-size rect in SCREEN points — in transform mode the orange CENTRE free-move handle, which a drag free-moves the frame in the view plane from and a click toggles to the dimension gizmo. In DIMENSION mode it is still the frame origin, which is where the ◎ origin ball is drawn only when the feature's pivot and its leaders' anchor coincide (a cube at the world origin): the balls themselves are published as `gizmo/ball:`, and that is what a script should click to toggle back", command: None },
    crate::automation::hit_keys::HitKeyDoc { panel: "gizmo", prefix: "axis:", meaning: "one zero-size rect per AXIS ARROW of the drawn transform gizmo (gizmo/axis:x|y|z, the frame's own axes), in SCREEN points, three quarters of the way along the arrow \u{2014} a drag from it translates the frame along that axis, which writes the feature's `transform.position`. Published only where the gizmo's own pick answers with that arrow: an arrow pointing at the camera has none", command: None },
    crate::automation::hit_keys::HitKeyDoc { panel: "gizmo", prefix: "ring:", meaning: "one zero-size rect per ROTATION GRAB ball of the drawn transform gizmo (gizmo/ring:x|y|z, named by the frame axis its arc turns about), in SCREEN points \u{2014} a drag from it rotates the frame about that axis, through the gizmo's origin, which writes the feature's `transform.rotationEuler`. Published only where the gizmo's own pick answers with that ring: an arc seen edge-on, its ball lying on an arrow, has none", command: None },
    crate::automation::hit_keys::HitKeyDoc { panel: "gizmo", prefix: "dim:", meaning: "one zero-size rect per grabbable ARROWHEAD of the drawn \u{25ce} DIMENSION gizmo (gizmo/dim:<field key> \u{2014} gizmo/dim:sizeX, gizmo/dim:radius, gizmo/dim:distance), in SCREEN points, at the cone TIP of a linear leader or the sweep-END ball of an angular one \u{2014} a DRAG from it edits that parameter live, exactly as typing into the form's `history/field:<the same key>` does, and the geometry rebuilds as the pointer travels. Published only where the gizmo's own pick answers with that field, and only while the DIMENSION gizmo is armed \u{2014} opening a feature's form arms it", command: None },
    crate::automation::hit_keys::HitKeyDoc { panel: "gizmo", prefix: "ball:", meaning: "one zero-size rect per orange ORIGIN BALL of the drawn ◎ DIMENSION gizmo (gizmo/ball:<field key>), in SCREEN points — the sphere every leader anchored there starts from, keyed by the first of them, so a cube's three dims share gizmo/ball:sizeX while a cone draws gizmo/ball:radiusBottom and gizmo/ball:radiusTop. A CLICK on one TOGGLES the gizmo back to its transform form, which is the only route an angular-only feature (a revolve) has to the move handles; the angle's own sweep-end ball is a VALUE handle and is published as `gizmo/dim:` instead. Published only where the gizmo's own pick answers with that ball", command: None },
    // The DRAGGABLE constraint handles — the second VIEWPORT-hosted widget
    // panel, in screen points with no `panel:clip`, keyed by constraint id (see
    // `constraint_handle_hits_json`).
    crate::automation::hit_keys::HitKeyDoc { panel: "constraint", prefix: "", meaning: "one zero-size rect per DRAGGABLE assembly-constraint handle, keyed by constraint id \u{2014} a distance arrow's tip, an angle arc's sweep-end handle, in SCREEN points. A bare click is swallowed; DRAGGING it edits that constraint's value and re-solves, which is what `assembly_update_constraint` does without a pointer", command: Some("assembly_update_constraint") },
];