KiCad - the KiCad Bridge
Public Made by Adomby adom
Drive your own KiCad from an AI, in the background: open every editor, read the whole board as data (the EDA Bridge Standard board model), stack-up, part heights, netlists, place, route, pour, DRC and exports.
Refresh exact linked PCB editor and 3D viewer through native menus #16
astra/linked-view-refresh → masterAI Flow needs both native views refreshed after board changes. Adds kicad_refresh_board_views {pcbHwnd}: validate the exact PCB editor, resolve only its uniquely bound already-open viewer, discover both exact View > Refresh menu actions, then invoke them without focus or mouse. Ambiguous/stale bindings refuse. A partial invocation reports which window received its request. Success explicitly says renderVerified:false; subsequent screenshots remain required.
Based on live 1.0.25 commit 6e873f2b7afa57d5d5552909b2eb0cd544edacba. Focused patch rebased after PR #15 publication. Production cargo check and workspace tests pass (26 bridge + 233 core). Exact menu matching excludes Refresh Plugins and Refresh All. Only verbs_windows.rs and the new acceptance document change; all predecessor modules preserved.
Native Windows acceptance remains pending: two independent editor/viewer pairs, visible silk updates in both surfaces, stale/ambiguous refusal, preserved camera/foreground. This requests refresh, does not claim synchronous geometry rebuilding or save the board. Please verify through the owning development bridge. Partial #104 follow-up.
Diff Skip to comments (1)
docs/linked-view-refresh.mdadded+7@@ -0,0 +1,7 @@+# Refresh the linked board views++After native edits, call `kicad_refresh_board_views {pcbHwnd}` with the current exact PCB editor HWND. The viewer must already be open and uniquely associated with that editor. The bridge uses its editor/viewer binding logic and refuses missing or ambiguous identities instead of picking the first viewer.++It discovers both exact `View > Refresh` menu actions before invoking either, then requests both redraws using window-targeted native menu calls. It does not save, open windows, move the cursor, change camera pose or take focus. This is a rendering request, not rendering proof: the response says `refreshRequested:true`, `renderVerified:false`. Inspect the subsequent `kicad_state` screenshots for both surfaces. A failure after one menu invocation lists `refreshRequestedFor`; do not assume neither ran.++Native acceptance: two boards with separate viewers; refresh each exact pair without touching the other; reject stale PCB handles and ambiguous viewer bindings; verify visible changes appear in both 2D and 3D after a native silk edit; preserve camera and foreground; no substitute windows. Source tests cover exact menu labels and retain the existing viewer-binding tests, but do not establish those Windows results.
rust/crates/kicad-bridge/src/verbs_windows.rs+112@@ -47,6 +47,15 @@ use kicad_platform::{native, unsupported, Placement, WindowInfo}; // ── Catalog ─────────────────────────────────────────────────────────────────── pub static VERBS: &[Verb] = &[+ Verb {+ name: "kicad_refresh_board_views",+ summary: "Refresh the exact PCB editor and its already-open linked 3D viewer without focus or mouse.",+ mechanism: Mechanism::Window, risk: "read", timeout_sec: 30,+ input: "{pcbHwnd}", example: "kicad_refresh_board_views {pcbHwnd:12345}",+ hint: "Requires an existing uniquely bound viewer. Discovers the exact View > Refresh menu on both windows before invoking either. Returns refreshRequested, not rendering proof; inspect kicad_state screenshots after redraw.",+ related: &["kicad_state", "kicad_open_3d_viewer", "kicad_silk_text_batch"],+ pitfalls: &["Does not open windows, substitute another editor/viewer, save the board, or claim that the 3D model has finished rebuilding."],+ }, Verb { name: "kicad_launch", summary: "Cold-start KiCad (project manager) when nothing is running.",@@ -304,6 +313,7 @@ pub fn dispatch(state: &mut State, command: &str, args: &Value) -> Option<Value> "kicad_open_footprint_editor" => open_footprint_editor(state, args), "kicad_open_3d_viewer" => open_3d_viewer(args), "kicad_3d_view" => view_3d(args),+ "kicad_refresh_board_views" => refresh_board_views(args), "kicad_menu_items" => menu_items(args), "kicad_open_editors" => open_editors(), "kicad_close" => close_kicad(state, args),@@ -2023,6 +2033,92 @@ fn open_3d_from(frame: u64, label: &str, pathways: &mut Vec<String>) -> Result<O /// `kicad_3d_view`: the 3D Viewer's View-menu commands by name, repeated with pauses, or a tour. /// `kicad_menu_items`: the native menu bar of a KiCad window, read-only.+fn is_view_refresh(path: &[String]) -> bool {+ let clean = |s: &str| {+ s.replace('&', "")+ .split('\t')+ .next()+ .unwrap_or("")+ .trim()+ .to_lowercase()+ };+ path.len() == 2 && clean(&path[0]) == "view" && clean(&path[1]) == "refresh"+}++pub(crate) fn refresh_board_views(args: &Value) -> Value {+ let Some(frame) = arg_any_u64(args, &["pcbHwnd"]) else {+ return fail(+ "source_editor_required",+ "Pass the exact pcbHwnd.",+ "Read kicad_open_editors first.",+ );+ };+ invalidate();+ let windows = kicad_windows();+ let Some(source) = windows+ .iter()+ .find(|w| w.hwnd == frame && w.kind() == WindowKind::PcbEditor)+ else {+ return fail(+ "source_editor_missing",+ "Requested PCB editor is not open.",+ "No substitute was used.",+ );+ };+ let viewers = windows_of_kind(WindowKind::Viewer3d);+ let before: Vec<_> = viewers.iter().map(|w| w.hwnd).collect();+ let count = windows+ .iter()+ .filter(|w| {+ w.pid == source.pid+ && matches!(+ w.kind(),+ WindowKind::PcbEditor | WindowKind::FootprintEditor+ )+ })+ .count();+ let known = VIEWER_SOURCE_BINDINGS.lock().ok().and_then(|m| {+ m.get(&frame)+ .filter(|(pid, _)| *pid == source.pid)+ .map(|(_, h)| *h)+ });+ let Some(viewer) = viewer_for_frame(frame, source.pid, viewers, &before, count, known) else {+ return fail(+ "viewer_binding_ambiguous",+ "No uniquely linked open 3D viewer.",+ "Open the viewer from this exact PCB editor first; nothing was refreshed.",+ );+ };+ let mut commands = Vec::new();+ for hwnd in [frame, viewer.hwnd] {+ let items = match native().menu_tree(hwnd) {+ Ok(items) => items,+ Err(e) => return fail("menu_walk_failed", &e, "No refresh was invoked."),+ };+ let matches: Vec<_> = items+ .iter()+ .filter(|m| m.id != 0 && is_view_refresh(&m.path))+ .collect();+ if matches.len() != 1 {+ return fail(+ "refresh_action_missing",+ "Exact View > Refresh menu not uniquely available.",+ "No fallback menu or keyboard action was invoked.",+ );+ }+ commands.push((hwnd, matches[0].id));+ }+ let mut invoked = Vec::new();+ for (hwnd, id) in commands {+ if let Err(e) = native().menu_invoke(hwnd, id) {+ return json!({"success":false,"errorCode":"refresh_invoke_failed","error":e,"refreshRequestedFor":invoked,"sourceHwnd":frame,"viewerHwnd":viewer.hwnd});+ }+ invoked.push(hwnd);+ }+ invalidate();+ json!({"success":true,"refreshRequested":true,"sourceHwnd":frame,"sourcePid":source.pid,"viewerHwnd":viewer.hwnd,"viewerPid":viewer.pid,"renderVerified":false,"_hint":"Both refresh commands were requested without foreground or cursor actions. Inspect kicad_state screenshots after rebuilding; command acknowledgement is not render completion."})+}+ pub(crate) fn menu_items(args: &Value) -> Value { let hwnd = match arg_any_u64(args, &["hwnd"]).or_else(|| foreground_kicad_hwnd()) { Some(h) => h,@@ -3171,3 +3267,19 @@ fn rescan_libraries(state: &mut State, args: &Value) -> Value { "_hint": "KiCad was restarted but no window appeared within 60 s; kicad_window_info shows what is up."}) } ++#[cfg(test)]+mod linked_refresh_tests {+ #[test]+ fn exact_view_refresh_excludes_plugins_and_partial_labels() {+ let path = |a: &[&str]| a.iter().map(|s| s.to_string()).collect::<Vec<_>>();+ assert!(super::is_view_refresh(&path(&["&View", "&Refresh\tF5"])));+ for a in [+ vec!["Tools", "Refresh Plugins"],+ vec!["View", "Refresh All"],+ vec!["Refresh"],+ ] {+ assert!(!super::is_view_refresh(&path(&a)));+ }+ }+}
Comments
Log in to comment.