app
Adom DS2SF - Datasheet to Symbol and Footprint
Public Made by Adomby adom
Datasheet to symbol + footprint + provenance JSON — pins and pads, extracted from the PDF.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
use anyhow::{Context as _, Result};
use serde_json::{json, Map, Value};
use std::fs;
use crate::context::Context;
use crate::extract::{FootprintPass, SymbolPass};
use crate::normalize::Resolved;
pub fn write_all(
ctx: &Context,
sym: &SymbolPass,
fp: Option<&FootprintPass>,
norm: Option<&Resolved>,
model: &str,
) -> Result<()> {
write_symbol_json(ctx, sym)?;
if let Some(f) = fp {
write_footprint_json(ctx, sym, f, norm)?;
}
write_provenance_json(ctx, sym, fp, norm, model)?;
Ok(())
}
fn write_symbol_json(ctx: &Context, sym: &SymbolPass) -> Result<()> {
// Strip provenance fields from pins for the sym_create-shaped output —
// those fields belong only in the provenance JSON, not the input to sym_create.
let pins: Vec<Value> = sym
.pins
.iter()
.map(|p| {
let mut o = Map::new();
o.insert("number".into(), Value::String(p.number.clone()));
o.insert("name".into(), Value::String(p.name.clone()));
o.insert("type".into(), Value::String(p.kind.clone()));
if !p.group.is_empty() {
o.insert("group".into(), Value::String(p.group.clone()));
}
if !p.description.is_empty() {
o.insert("description".into(), Value::String(p.description.clone()));
}
Value::Object(o)
})
.collect();
let mut out = Map::new();
out.insert("symbolName".into(), Value::String(sym.symbol_name.clone()));
if let Some(m) = sym.manufacturer.clone() {
out.insert("manufacturer".into(), Value::String(m));
}
if let Some(p) = sym.package.clone() {
out.insert("package".into(), Value::String(p));
}
if let Some(d) = sym.description.clone() {
out.insert("description".into(), Value::String(d));
}
if let Some(u) = ctx.datasheet_url.clone() {
out.insert("datasheetUrl".into(), Value::String(u));
}
out.insert("category".into(), Value::String(sym.category.clone()));
out.insert("referencePrefix".into(), Value::String(sym.reference_prefix.clone()));
if let Some(db) = sym.discrete_baseline.as_ref() {
let mut baseline = Map::new();
baseline.insert("library".into(), Value::String(db.library.clone()));
baseline.insert("symbol".into(), Value::String(db.symbol.clone()));
out.insert("discreteBaseline".into(), Value::Object(baseline));
}
out.insert("pins".into(), Value::Array(pins));
out.insert(
"groups".into(),
Value::Array(
sym.groups
.iter()
.map(|g| {
let mut o = Map::new();
o.insert("name".into(), Value::String(g.name.clone()));
if !g.description.is_empty() {
o.insert("description".into(), Value::String(g.description.clone()));
}
Value::Object(o)
})
.collect(),
),
);
if !sym.variants.is_empty() {
out.insert("variants".into(), Value::Array(sym.variants.clone()));
}
let path = ctx.symbol_out_path();
fs::write(&path, serde_json::to_string_pretty(&out)? + "\n")
.with_context(|| format!("writing {}", path.display()))?;
Ok(())
}
fn write_footprint_json(
ctx: &Context,
sym: &SymbolPass,
fp: &FootprintPass,
norm: Option<&Resolved>,
) -> Result<()> {
let mut body = Map::new();
body.insert("x".into(), json!(fp.body_dimensions.x));
body.insert("y".into(), json!(fp.body_dimensions.y));
if let Some(t) = fp.body_dimensions.thickness_max {
body.insert("thickness_max".into(), json!(t));
}
// Strip provenance keys from leadDimensions so the output is consumer-clean.
let mut lead_clean = Map::new();
if let Some(obj) = fp.lead_dimensions.as_object() {
for (k, v) in obj {
if matches!(k.as_str(), "source_page" | "figure_label") {
continue;
}
lead_clean.insert(k.clone(), v.clone());
}
}
let mut out = Map::new();
out.insert("footprintName".into(), Value::String(sym.symbol_name.clone()));
if let Some(m) = sym.manufacturer.clone() {
out.insert("manufacturer".into(), Value::String(m));
}
out.insert("package".into(), Value::String(fp.package_name.clone()));
if let Some(d) = sym.description.clone() {
out.insert("description".into(), Value::String(d));
}
if let Some(u) = ctx.datasheet_url.clone() {
out.insert("datasheetUrl".into(), Value::String(u));
}
out.insert("padCount".into(), json!(fp.pad_count));
out.insert("bodyDimensions".into(), Value::Object(body));
out.insert("leadDimensions".into(), Value::Object(lead_clean));
out.insert("padDescriptions".into(), Value::Object(fp.pad_descriptions.clone()));
out.insert("symbolPinMap".into(), Value::Object(fp.symbol_pin_map.clone()));
// Mechanical section — JEDEC dimensions, lead shape, pin-1 indicator,
// extracted directly from the datasheet mechanical drawing by Claude.
if let Some(mech) = &fp.mechanical {
out.insert("mechanical".into(), mech.clone());
}
// Per-pad position array for chipsmith cross-validation.
let computed = crate::fpsvg::compute_pads(fp);
if !computed.is_empty() {
out.insert("pads".into(), serde_json::to_value(&computed)?);
}
// 3D chip geometry — enough for chipsmith/OCCT to construct the chip
// model from scratch without a manufacturer STEP file. When mechanical.jedec_dims
// is present, chip_3d uses measured values; otherwise heuristic defaults.
let chip_3d = crate::fpsvg::compute_chip_3d(fp);
out.insert("chip_3d".into(), serde_json::to_value(&chip_3d)?);
// Lead annotations — per-pin metadata ordered for service-step2glb /create-chip.
// Embeds signal names into the STEP file as XCAF annotations.
let annotations = crate::fpsvg::compute_lead_annotations(fp);
if !annotations.is_empty() {
out.insert("lead_annotations".into(), serde_json::to_value(&annotations)?);
}
if let Some(n) = norm {
if let Some(b) = n.kicad_baseline.as_ref() {
out.insert(
"kicad_baseline".into(),
json!({ "library": b.library, "name": b.name, "source": b.source }),
);
out.insert(
"package_outline_source".into(),
Value::String("service-kicad-stdlib".into()),
);
} else {
out.insert("kicad_baseline".into(), Value::Null);
out.insert(
"package_outline_source".into(),
Value::String("datasheet-extracted".into()),
);
}
}
let path = ctx.footprint_out_path();
fs::write(&path, serde_json::to_string_pretty(&out)? + "\n")
.with_context(|| format!("writing {}", path.display()))?;
Ok(())
}
fn write_provenance_json(
ctx: &Context,
sym: &SymbolPass,
fp: Option<&FootprintPass>,
norm: Option<&Resolved>,
model: &str,
) -> Result<()> {
let pin_provenance: Vec<Value> = sym
.pins
.iter()
.map(|p| {
let mut o = Map::new();
o.insert("pin_number".into(), Value::String(p.number.clone()));
o.insert("pin_name".into(), Value::String(p.name.clone()));
o.insert("page".into(), json!(p.source_page));
o.insert(
"table_caption".into(),
Value::String(p.source_table_caption.clone()),
);
o.insert("confidence".into(), Value::String(p.confidence.clone()));
if !p.notes.is_empty() {
o.insert("notes".into(), Value::String(p.notes.clone()));
}
Value::Object(o)
})
.collect();
let symbol_block = json!({
"pin_provenance": pin_provenance,
"groups_emitted": sym.groups.iter().map(|g| g.name.clone()).collect::<Vec<_>>(),
});
let footprint_block = match fp {
Some(f) => {
let mut dim_prov: Vec<Value> = Vec::new();
dim_prov.push(json!({
"field": "bodyDimensions.x",
"value": f.body_dimensions.x,
"page": f.body_dimensions.source_page,
"figure_label": f.body_dimensions.figure_label,
}));
dim_prov.push(json!({
"field": "bodyDimensions.y",
"value": f.body_dimensions.y,
"page": f.body_dimensions.source_page,
"figure_label": f.body_dimensions.figure_label,
}));
if let Some(t) = f.body_dimensions.thickness_max {
dim_prov.push(json!({
"field": "bodyDimensions.thickness_max",
"value": t,
"page": f.body_dimensions.source_page,
"figure_label": f.body_dimensions.figure_label,
}));
}
if let Some(obj) = f.lead_dimensions.as_object() {
let lead_page = obj.get("source_page").and_then(|v| v.as_u64()).unwrap_or(0);
let lead_label = obj.get("figure_label").and_then(|v| v.as_str()).unwrap_or("").to_string();
for (k, v) in obj {
if matches!(k.as_str(), "source_page" | "figure_label") {
continue;
}
dim_prov.push(json!({
"field": format!("leadDimensions.{k}"),
"value": v,
"page": lead_page,
"figure_label": lead_label,
}));
}
}
let kicad_baseline = norm
.and_then(|n| n.kicad_baseline.as_ref())
.map(|b| json!({ "library": b.library, "name": b.name }))
.unwrap_or(Value::Null);
let attempts = norm
.map(|n| {
n.search_attempts
.iter()
.map(|a| {
json!({ "library": a.library, "name": a.name, "result": a.result })
})
.collect::<Vec<_>>()
})
.unwrap_or_default();
json!({
"package_name_provenance": {
"value": f.package_name,
"page": f.package_name_source_page,
"info_json_value": ctx.package_hint,
"info_json_used": ctx.package_hint.is_some(),
"is_standard_jedec": f.is_standard_jedec
},
"kicad_baseline": kicad_baseline,
"kicad_baseline_search_attempts": attempts,
"dimension_provenance": dim_prov
})
}
None => Value::Null,
};
let merged_review = crate::extract::merge_review_required(sym, fp);
let prov = json!({
"schema_version": "1",
"mpn": ctx.mpn,
"manufacturer": ctx.manufacturer,
"datasheet": {
"path": ctx.datasheet_path,
"url": ctx.datasheet_url,
"sha256": ctx.datasheet_sha256,
"source_tier": ctx.datasheet_source_tier,
"source_host": ctx.datasheet_source_host,
"fetched_at": ctx.fetched_at,
"page_count": ctx.page_count
},
"tooling": {
"extractor": "ds2sf",
"extractor_version": env!("CARGO_PKG_VERSION"),
"extracted_at": ctx.extracted_at,
"claude_model": model,
"service_kicad_used": norm.is_some(),
"service_kicad_baseline_match":
norm.map(|n| n.kicad_baseline.is_some()).unwrap_or(false)
},
"symbol": symbol_block,
"footprint": footprint_block,
"review_required": merged_review
});
let path = ctx.provenance_path();
fs::write(&path, serde_json::to_string_pretty(&prov)? + "\n")
.with_context(|| format!("writing {}", path.display()))?;
Ok(())
}
pub fn summary_line(
ctx: &Context,
sym: &SymbolPass,
fp: Option<&FootprintPass>,
norm: Option<&Resolved>,
) -> String {
let baseline = match norm.and_then(|n| n.kicad_baseline.as_ref()) {
Some(b) => format!("{}:{}", b.library, b.name),
None => "no stdlib match".to_string(),
};
let groups: Vec<&str> = sym.groups.iter().map(|g| g.name.as_str()).collect();
let groups_str = if groups.is_empty() {
"(no groups)".to_string()
} else {
groups.join(", ")
};
let pkg = fp.map(|f| f.package_name.as_str()).unwrap_or("(no footprint pass)");
let pad_count = fp.map(|f| f.pad_count).unwrap_or(0);
let review = crate::extract::merge_review_required(sym, fp).len();
format!(
"{} — {} pins, package {} ({} pads), {}, groups: {}, review_required: {}",
ctx.mpn,
sym.pins.len(),
pkg,
pad_count,
baseline,
groups_str,
review,
)
}