← Commit history

Merge PR #415: Report a library Fusion refuses instead of calling it opened

Colby Knox ·73b4f61cb6 ·7d ago ·parent 54ca150
3 files changed +38−4
rust/bridge/src/mint.rs+9−2
@@ -367,9 +367,16 @@ pub fn build_library_3d_core(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value)         }     }     let mut opened = Value::Null;-    if let Some(b) = &bound { if args["openWhenDone"].as_bool().unwrap_or(true) { opened = json!(crate::orchestrations::open_lbr(ctx, &json!({"filePath": b}), pass)["success"].as_bool() == Some(true)); } }+    let mut open_error = Value::Null;+    if let Some(b) = &bound { if args["openWhenDone"].as_bool().unwrap_or(true) {+        let op = crate::orchestrations::open_lbr(ctx, &json!({"filePath": b}), pass);+        let ok = op["success"].as_bool() == Some(true);+        opened = json!(ok);+        if !ok { open_error = op["error"].clone(); }+    } }     let n_ok = results.iter().filter(|r| r["success"].as_bool() == Some(true)).count();-    json!({"success": n_ok > 0, "cancelled": cancelled, "boundLbr": bound, "partsBound": n_ok, "partsTotal": total, "parts": results, "screenshots": shots, "opened": opened,+    // Parts minted but a library Fusion will not open is a failed build, whatever each part did.+    json!({"success": n_ok > 0 && open_error.is_null(), "error": open_error, "cancelled": cancelled, "boundLbr": bound, "partsBound": n_ok, "partsTotal": total, "parts": results, "screenshots": shots, "opened": opened,         "_hint": format!("{n_ok}/{total} parts got a RENDERING footprint+chip 3D package; all bindings injected into {} and the library opened once. NEXT: fusion_check_dialogs should be 0 (no broken-ref). For clean device previews + screenshots, save to the cloud and REOPEN from the cloud (fusion_aps_open) - the in-session view often gets an empty 'Untitled' shoved in front. Each device's lower-right 3D preview should show footprint + chip (NOT 'Thumbnail download failed'). The BEFORE (footprint) / AFTER (chip placed) PNGs are in screenshots[] (on the box in %TEMP%/adom-fusion-shots; ab pulls them to you as localFullPath) - pull them with desktop_pull_file (or re-capture via desktop_screenshot_window) for the demo video. AFTER (recommended): call fusion_capture_library_views per showcase part to grab the symbol / footprint / component (pin<->pad mapped) views - those are what make EEs trust the library, and the 3D-only shots miss them. PITFALLS: needs Fusion running. ⚠️ RELAY TIMEOUT: a many-part run takes minutes but the Adom Bridge relay times out the REQUEST at ~60s - you may get 'Request timed out' even though the build KEEPS RUNNING server-side and finishes. Do NOT assume it failed: wait, then verify by reading the boundLbr (count <package3d name=) + the before/after PNGs (%TEMP%/adom-fusion-shots on the box, pulled to you as localFullPath). If a part is missing, just re-run build_library_3d for the missing parts pointing outLbrPath at the SAME file - binding injection is now MERGE-AWARE and idempotent, so it accumulates onto the existing bindings (it no longer wipes the parts that already succeeded). Each part also self-retries once on a transient first-part failure. Full recipe: the fusion-multipart-libraries skill.", bound.as_deref().unwrap_or("None"))}) } 
rust/bridge/src/orchestrations.rs+4−2
@@ -99,7 +99,7 @@ pub fn open_lbr(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value) -> Value) ->             "acceptedArgs": ["filePath", "lbrPath", "libraryPath", "path"], "receivedArgs": received, "_hint": "Re-call with a Windows-local path, e.g. fusion_open_lbr {\"lbrPath\": \"C:/tmp/rayparts/tja1050.lbr\"}."});     }     let pf = fusion_lbr::preflight_lbr(std::path::Path::new(&file_path));-    let hard_ok = ["file-readable", "size-sane", "xml-parses", "root-is-eagle"].iter().all(|k| pf.checks.iter().find(|c| c.check == *k).map(|c| c.ok).unwrap_or(true));+    let hard_ok = ["file-readable", "size-sane", "xml-parses", "root-is-eagle", "package3d-refs-resolve"].iter().all(|k| pf.checks.iter().find(|c| c.check == *k).map(|c| c.ok).unwrap_or(true));     let file_path = file_path.replace('\\', "/");     if !hard_ok {         let sz = std::fs::metadata(&file_path).ok().map(|m| m.len());@@ -164,7 +164,9 @@ pub fn open_lbr(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value) -> Value) ->     if !verification.is_null() { response["data"]["verification"] = verification; }     let (shots, _) = crate::lifecycle::post_open_screenshot(ctx, 2.0);     response["data"]["postOpenScreenshot"] = shots;-    response+    // Fusion reports a refused library only through its Error modal: the command itself succeeds+    // and leaves an empty design. Without this, every caller reads the refusal as a good open.+    crate::small_verbs::apply_failure_dialogs(response, &ctx.caller, &ctx.pids) }  /// Python's `repr(bytes)`.
rust/lbr/src/lib.rs+25
@@ -889,6 +889,14 @@ pub fn preflight_lbr(path: &std::path::Path) -> Preflight {         }         c("pads-unique", dups.is_empty(), if dups.is_empty() { "all pad names unique per package".into() } else {             format!("duplicate pad names - Fusion's importer refuses the ENTIRE library over these: {}. Rename to e.g. 17, 17@2, 17@3...", dups.iter().take(6).cloned().collect::<Vec<_>>().join(", ")) });+        // A 3D package naming a package the library does not have makes Fusion refuse the ENTIRE+        // library: an Error modal and an empty design. Only libraries with bindings get this check.+        if root.descendants().any(|n| el(n, "packageinstance")) {+            let names: Vec<&str> = root.descendants().filter(|n| el(*n, "package")).filter_map(|p| p.attribute("name")).collect();+            let dangling: Vec<&str> = root.descendants().filter(|n| el(*n, "packageinstance")).filter_map(|p| p.attribute("name")).filter(|n| !names.iter().any(|k| k.eq_ignore_ascii_case(n))).collect();+            c("package3d-refs-resolve", dangling.is_empty(), if dangling.is_empty() { "every 3D binding names a package in this library".into() } else {+                format!("3D binding(s) name packages this library does not have, so Fusion refuses the ENTIRE library: {}. Bind to the <package> name, not the deviceset's.", dangling.iter().take(6).copied().collect::<Vec<_>>().join(", ")) });+        }     }     finish(checks, xml.as_deref()) }@@ -913,6 +921,23 @@ fn finish(checks: Vec<Check>, xml: Option<&str>) -> Preflight {  #[cfg(test)] mod heatsink_tests {+    #[test]+    fn preflight_refuses_a_3d_binding_to_a_missing_package() {+        let lib = |instance: &str| format!(r#"<?xml version="1.0"?><eagle><drawing><library><packages><package name="SOIC-8"><smd name="1" x="0" y="0" dx="1" dy="1" layer="1"/></package></packages><packages3d><package3d name="{instance}" wip_urn="urn:x"><packageinstances><packageinstance name="{instance}"/></packageinstances></package3d></packages3d><symbols><symbol name="S"/></symbols><devicesets><deviceset name="NY8A054ES8"><devices><device name="" package="SOIC-8"><connects/></device></devices></deviceset></devicesets></library></drawing></eagle>"#);+        let dir = std::env::temp_dir().join(format!("fusion-lbr-refs-{}", std::process::id()));+        std::fs::create_dir_all(&dir).unwrap();+        let check = |instance: &str| {+            let path = dir.join(format!("{instance}.lbr"));+            std::fs::write(&path, lib(instance)).unwrap();+            crate::preflight_lbr(&path).checks.into_iter().find(|c| c.check == "package3d-refs-resolve").expect("the check runs on a library with bindings")+        };+        let bad = check("NY8A054ES8");+        assert!(!bad.ok, "a binding to the deviceset's name is refused");+        assert!(bad.detail.contains("NY8A054ES8"), "and names it: {}", bad.detail);+        assert!(check("SOIC-8").ok, "a binding to the real package passes");+        let _ = std::fs::remove_dir_all(&dir);+    }+     #[test]     fn heatsink_vias_are_not_shorts() {         let lbr = r#"<eagle version="9.6.2"><drawing><library><packages><package name="QFN-EP">