app
Adom Symbol
Public Made by Adomby adom
KiCad symbol creator with interactive viewer and delivery to KiCad/Fusion 360
← Commit history
Sync current source (open-source, up to date)
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)); } }