app
Adom Symbol
Public Made by Adomby adom
KiCad symbol creator with interactive viewer and delivery to KiCad/Fusion 360
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
//! Read a ds2sf `<mpn>-symbol.extracted.json` (datasheet-extracted pins with
//! logical groups + rich descriptions) and build an `IcInput` for our generator
//! with **balanced side assignment** and optional logical grouping. Also returns
//! the pin/group description maps for the viewer tooltips.
//!
//! ds2sf already groups pins (POWER, GROUND, GPIO, DEBUG, …); GPIO is sub-split
//! by port (P0/P1/P2) so a 31-pin GPIO group doesn't pile onto one side.
use crate::sym_gen::{IcInput, Pin};
use regex::Regex;
use serde::Deserialize;
use std::collections::HashMap;
#[derive(Deserialize, Clone)]
pub struct Ds2sfPin {
pub name: String,
pub number: String,
#[serde(rename = "type", default)]
pub ptype: String,
#[serde(default)]
pub group: String,
#[serde(default)]
pub description: String,
}
#[derive(Deserialize, Clone, Default)]
pub struct Ds2sfGroup {
pub name: String,
#[serde(default)]
pub description: String,
}
#[derive(Deserialize, Default)]
pub struct Ds2sfSymbol {
#[serde(rename = "symbolName", default)]
pub symbol_name: String,
#[serde(rename = "referencePrefix", default)]
pub reference_prefix: String,
#[serde(default)]
pub package: String,
#[serde(default)]
pub manufacturer: String,
#[serde(default)]
pub description: String,
#[serde(default)]
pub pins: Vec<Ds2sfPin>,
#[serde(default)]
pub groups: Vec<Ds2sfGroup>,
}
pub fn parse(json: &str) -> anyhow::Result<Ds2sfSymbol> {
Ok(serde_json::from_str(json)?)
}
pub struct LayoutOpts {
pub pin_layout: String, // "left-right" | "all-sides"
pub grouped: bool,
pub ref_pos: String,
pub value_pos: String,
}
/// Build an IcInput from ds2sf pins. Returns (input, pin name/number→desc,
/// group→desc).
pub fn build_ic_input(
sym: &Ds2sfSymbol,
opts: &LayoutOpts,
) -> (IcInput, HashMap<String, String>, HashMap<String, String>) {
// Description maps for the viewer.
let mut pin_desc: HashMap<String, String> = HashMap::new();
for p in &sym.pins {
if !p.description.is_empty() {
pin_desc.insert(p.name.clone(), p.description.clone());
pin_desc.insert(p.number.clone(), p.description.clone());
}
}
let mut group_desc: HashMap<String, String> = HashMap::new();
for g in &sym.groups {
group_desc.insert(g.name.clone(), g.description.clone());
}
// Effective group label per pin: sub-split GPIO by port (P0/P1/P2).
let port_re = Regex::new(r"^P(\d+)").unwrap();
let eff_group = |p: &Ds2sfPin| -> String {
if p.group.eq_ignore_ascii_case("GPIO") {
if let Some(c) = port_re.captures(&p.name) {
return format!("P{}", &c[1]);
}
}
p.group.clone()
};
// Carry port sub-group descriptions from the parent GPIO group.
if let Some(gpio) = sym.groups.iter().find(|g| g.name.eq_ignore_ascii_case("GPIO")) {
for p in &sym.pins {
let eg = eff_group(p);
if eg.starts_with('P') && eg.len() <= 3 {
group_desc.entry(eg).or_insert_with(|| gpio.description.clone());
}
}
}
// Collect groups in first-seen order with their pins.
let mut order: Vec<String> = Vec::new();
let mut by_group: HashMap<String, Vec<&Ds2sfPin>> = HashMap::new();
for p in &sym.pins {
let g = eff_group(p);
if !by_group.contains_key(&g) {
order.push(g.clone());
}
by_group.entry(g).or_default().push(p);
}
// Sides to balance across.
let sides: Vec<&str> = if opts.pin_layout == "all-sides" {
vec!["left", "top", "right", "bottom"]
} else {
vec!["left", "right"]
};
// Greedy bin-pack groups (largest first) into the least-full side.
let mut group_side: HashMap<String, String> = HashMap::new();
if opts.grouped {
let mut sized: Vec<(String, usize)> =
order.iter().map(|g| (g.clone(), by_group[g].len())).collect();
sized.sort_by(|a, b| b.1.cmp(&a.1));
let mut load: Vec<usize> = vec![0; sides.len()];
for (g, n) in sized {
let i = (0..sides.len()).min_by_key(|&i| load[i]).unwrap();
load[i] += n;
group_side.insert(g, sides[i].to_string());
}
} else {
// Ungrouped: split the flat pin list evenly across the sides.
let total = sym.pins.len();
let per = (total + sides.len() - 1) / sides.len();
for (i, p) in sym.pins.iter().enumerate() {
group_side.insert(format!("__{}", p.number), sides[(i / per).min(sides.len() - 1)].to_string());
}
}
// Emit pins ordered by side, then group order, each with explicit side.
let mut out: Vec<Pin> = Vec::new();
for side in &sides {
for g in &order {
for p in &by_group[g] {
let pin_side = if opts.grouped {
group_side.get(g).cloned()
} else {
group_side.get(&format!("__{}", p.number)).cloned()
};
if pin_side.as_deref() != Some(*side) {
continue;
}
out.push(Pin {
name: p.name.clone(),
number: p.number.clone(),
ptype: p.ptype.clone(),
group: if opts.grouped { Some(g.clone()) } else { None },
side: Some(side.to_string()),
});
}
}
}
let input = IcInput {
symbol_name: sym.symbol_name.clone(),
manufacturer: sym.manufacturer.clone(),
package: sym.package.clone(),
description: sym.description.clone(),
datasheet_url: String::new(),
reference_prefix: if sym.reference_prefix.is_empty() { "U".into() } else { sym.reference_prefix.clone() },
pins: out,
pin_layout: opts.pin_layout.clone(),
chip_name_centered: false,
ref_pos: opts.ref_pos.clone(),
value_pos: opts.value_pos.clone(),
distributor_pn: Vec::new(),
};
(input, pin_desc, group_desc)
}