← Commit history

Sync current source (open-source, up to date)

John Lauer ·a9108ee3e6 ·3mo ago ·parent fc00e9d
2 files changed +115−21
src/render.rs+89
@@ -135,6 +135,95 @@ fn prop_justify(block: &str, name: &str) -> String {     re.captures(block).map(|c| format!(" {}", &c[1])).unwrap_or_default() } +/// Composite an iso 3D chip PNG into the symbol body centre — the "default+/// symbol with the 3D chip in it" the chip-fetcher dashboard card shows. We+/// place the image centred in the symbol's largest body rectangle (the chip+/// outline), sized to ~62% of it so pin names/numbers near the edges stay+/// readable, matching the live app's centred-3D default.+pub fn composite_iso_into_symbol(svg: &str, block: &str, _name: &str, iso_path: &std::path::Path) -> Result<String> {+    let bytes = std::fs::read(iso_path)+        .with_context(|| format!("reading iso {}", iso_path.display()))?;+    let mime = if iso_path.extension().and_then(|e| e.to_str()) == Some("svg") {+        "image/svg+xml"+    } else { "image/png" };+    let data = format!("data:{mime};base64,{}", b64(&bytes));++    // Body rectangle in symbol-local coords → SVG coords (PX/PY origin, Y flip).+    const PX: f64 = 100.0;+    const PY: f64 = 100.0;+    let (cx, cy, bw, bh) = match largest_body_rect(block) {+        Some((sx, sy, ex, ey)) => {+            let cx = PX + (sx + ex) / 2.0;+            let cy = PY - (sy + ey) / 2.0; // Y flip+            (cx, cy, (ex - sx).abs(), (ey - sy).abs())+        }+        None => match symbol_local_bbox(block) {+            Some((minx, maxx, miny, maxy)) => {+                let cx = PX + (minx + maxx) / 2.0;+                let cy = PY - (miny + maxy) / 2.0;+                (cx, cy, (maxx - minx).abs(), (maxy - miny).abs())+            }+            None => return Err(anyhow!("no body geometry to place iso")),+        },+    };+    // Cover ~62% of the body; the image keeps its own aspect ratio (meet).+    let frac = 0.62;+    let w = bw * frac;+    let h = bh * frac;+    let x = cx - w / 2.0;+    let y = cy - h / 2.0;+    let image = format!(+        "<image x=\"{x:.3}\" y=\"{y:.3}\" width=\"{w:.3}\" height=\"{h:.3}\" \+         preserveAspectRatio=\"xMidYMid meet\" opacity=\"0.96\" href=\"{data}\" />"+    );+    // Inject just before </svg> so it paints on top of the symbol body.+    if let Some(pos) = svg.rfind("</svg>") {+        let mut out = String::with_capacity(svg.len() + image.len() + 8);+        out.push_str(&svg[..pos]);+        out.push_str(&image);+        out.push_str(&svg[pos..]);+        Ok(out)+    } else {+        Err(anyhow!("no </svg> close tag to inject iso into"))+    }+}++/// The largest `(rectangle (start x y) (end x y))` in the symbol block — the+/// chip body outline. Returns (start_x, start_y, end_x, end_y) of the max-area+/// rectangle, or None if the symbol has no rectangle (e.g. a polyline body).+fn largest_body_rect(block: &str) -> Option<(f64, f64, f64, f64)> {+    let re = Regex::new(r"(?s)\(rectangle\s*\(start (-?[\d.]+) (-?[\d.]+)\)\s*\(end (-?[\d.]+) (-?[\d.]+)\)").unwrap();+    let mut best: Option<(f64, f64, f64, f64)> = None;+    let mut best_area = 0.0;+    for c in re.captures_iter(block) {+        if let (Ok(sx), Ok(sy), Ok(ex), Ok(ey)) =+            (c[1].parse::<f64>(), c[2].parse::<f64>(), c[3].parse::<f64>(), c[4].parse::<f64>())+        {+            let area = (ex - sx).abs() * (ey - sy).abs();+            if area > best_area {+                best_area = area;+                best = Some((sx, sy, ex, ey));+            }+        }+    }+    best+}++/// Minimal base64 (no extra crate dep here).+fn b64(data: &[u8]) -> String {+    const T: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";+    let mut out = String::with_capacity((data.len() + 2) / 3 * 4);+    for chunk in data.chunks(3) {+        let b = [chunk[0], *chunk.get(1).unwrap_or(&0), *chunk.get(2).unwrap_or(&0)];+        let n = ((b[0] as u32) << 16) | ((b[1] as u32) << 8) | b[2] as u32;+        out.push(T[((n >> 18) & 63) as usize] as char);+        out.push(T[((n >> 12) & 63) as usize] as char);+        out.push(if chunk.len() > 1 { T[((n >> 6) & 63) as usize] as char } else { '=' });+        out.push(if chunk.len() > 2 { T[(n & 63) as usize] as char } else { '=' });+    }+    out+}+ fn crop_to_symbol(svg: &str, block: &str) -> String {     let (minx, maxx, miny, maxy) = match symbol_local_bbox(block) {         Some(b) => b,
src/server.rs+26−21
@@ -40,23 +40,17 @@ pub struct AppState {     pub kicad_sym: String,    // raw .kicad_sym of the current symbol (for Send-to delivery) } -/// Map a source slug to a friendly label (mirrors chip-fetcher's source_label).-fn source_label(slug: &str) -> String {+/// Map a source slug to a friendly label. Delegates to the SHARED provenance+/// rule (see the chip-provenance-labeling skill): label by where it was actually+/// downloaded, name the maker site, never call an aggregator "Manufacturer".+fn source_label(slug: &str, manufacturer: &str) -> String {     match slug {-        "auto" => "Auto · grouped",-        "ds2sf" => "ds2sf · datasheet",-        "snapmagic" | "snapeda" => "SnapEDA",-        "ultralibrarian" | "cse_ul" | "ul" => "Ultra Librarian",-        "cse" | "componentsearch" => "Component Search",-        "manufacturer" | "mfr" => "Manufacturer",-        "mouser" => "Mouser",-        "digikey" => "DigiKey",-        "arrow" => "Arrow",-        "lcsc" => "LCSC",-        "kicad_community" => "KiCad community",-        other => other,+        "auto" => "Auto · grouped".to_string(),+        "ds2sf" => format!("PDF Datasheet from {}", crate::provenance::source_label("manufacturer", manufacturer)),+        // Every other symbol source is a KiCad-library symbol render → name the+        // download source + the format, e.g. "Component Search Engine - KiCad Library".+        other => format!("{} - KiCad Library", crate::provenance::source_label(other, manufacturer)),     }-    .to_string() }  /// What the studio is currently showing — drives /relayout regeneration.@@ -296,25 +290,36 @@ fn regenerate() {     for v in &m.variants {         sources.push(v.clone());     }+    // Never list the same source twice (a `.manufacturer.` variant is a tagged+    // copy of the canonical CSE symbol — same thing, one entry).+    let mut seen = std::collections::HashSet::new();+    sources.retain(|s| seen.insert(s.clone()));      // Friendly labels per source. The canonical "manufacturer" entry is shown by     // its REAL origin (e.g. "SnapEDA") from info.json.source_of_record / provenance,     // so the user sees where it came from, not a generic word.+    let manufacturer = read_file(&m.dir, "info.json")+        .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())+        .and_then(|v| v.get("manufacturer").and_then(|x| x.as_str()).map(String::from))+        .unwrap_or_default();     let mut source_labels: std::collections::HashMap<String, String> = std::collections::HashMap::new();     for s in &sources {-        source_labels.insert(s.clone(), source_label(s));+        source_labels.insert(s.clone(), source_label(s, &manufacturer));     }     if m.has_mfr {-        let origin = read_file(&m.dir, "info.json")+        // The canonical "manufacturer" entry is labeled by WHERE it was downloaded+        // (discovery_source) — NOT who authored it (content_origin). A TI model+        // pulled from CSE reads "Component Search Engine", not "Manufacturer".+        let origin = read_file(&m.dir, &format!("{}-file-provenance.json", m.mpn))             .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())-            .and_then(|v| v.get("source_of_record").and_then(|s| s.get("kicad_sym")).and_then(|s| s.as_str()).map(String::from))+            .and_then(|v| v.get(format!("{}.kicad_sym", m.mpn)).and_then(|e| e.get("discovery_source").or_else(|| e.get("content_origin"))).and_then(|s| s.as_str()).map(String::from))             .or_else(|| {-                read_file(&m.dir, &format!("{}-file-provenance.json", m.mpn))+                read_file(&m.dir, "info.json")                     .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())-                    .and_then(|v| v.get(format!("{}.kicad_sym", m.mpn)).and_then(|e| e.get("content_origin").or_else(|| e.get("discovery_source"))).and_then(|s| s.as_str()).map(String::from))+                    .and_then(|v| v.get("source_of_record").and_then(|s| s.get("kicad_sym")).and_then(|s| s.as_str()).map(String::from))             });         if let Some(o) = origin {-            source_labels.insert("manufacturer".to_string(), source_label(&o));+            source_labels.insert("manufacturer".to_string(), source_label(&o, &manufacturer));         }     }