main
Colby Knox 1.1.0: every molecule gets large machine pins on the 32 mm grid; gallery rebuilt; corrected notes on kicad-to-molecule c794385 4d ago
//! Altium lane probe.
//!
//!   altium_probe summary <board.PcbDoc>
//!       bbox, reader counts, and the plan for the board.
//!   altium_probe padlib <molecule.PcbDoc> <template.PcbLib> <footprint> <out.PcbLib>
//!       Copy the last pad record of a written molecule (an MP pad) into a copy of
//!       a single-footprint PcbLib, moved to the footprint origin, so KiCad's own
//!       Altium footprint importer (service-kicad altium fp) can parse it.
//!   altium_probe strict <file>...
//!       Validate compound files with the cfb crate's strict parser.

#![allow(dead_code)]

#[path = "../rs/src/plan.rs"]
mod plan;
#[path = "../rs/src/model.rs"]
mod model;
#[path = "../rs/src/altium/mod.rs"]
mod altium;

use anyhow::{bail, Context, Result};
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
use std::path::Path;

fn read_stream(cf: &mut cfb::CompoundFile<Cursor<Vec<u8>>>, p: &str) -> Result<Vec<u8>> {
    let mut v = vec![];
    cf.open_stream(p).with_context(|| p.to_string())?.read_to_end(&mut v)?;
    Ok(v)
}

/// Byte ranges of the 6-subrecord pad records at the head of a record run.
fn pad_spans(d: &[u8], mut i: usize) -> Vec<(usize, usize)> {
    let mut out = vec![];
    while i < d.len() && d[i] == 2 {
        let start = i;
        i += 1;
        for _ in 0..6 {
            let n = u32::from_le_bytes(d[i..i + 4].try_into().unwrap()) as usize;
            i += 4 + n;
        }
        out.push((start, i));
    }
    out
}

fn padlib(doc: &Path, lib: &Path, fp: &str, out: &Path) -> Result<()> {
    let mut d = cfb::CompoundFile::open(Cursor::new(std::fs::read(doc)?))?;
    let pads = read_stream(&mut d, "/Pads6/Data")?;
    let spans = pad_spans(&pads, 0);
    let &(s, e) = spans.last().context("no pads")?;
    let mut rec = pads[s..e].to_vec();
    // Subrecord 5 offset: type byte + four length-prefixed subrecords.
    let mut o = 1;
    for _ in 0..4 {
        o += 4 + u32::from_le_bytes(rec[o..o + 4].try_into().unwrap()) as usize;
    }
    let s5 = o + 4;
    rec[s5 + 7..s5 + 9].copy_from_slice(&0xFFFFu16.to_le_bytes());
    rec[s5 + 13..s5 + 21].fill(0);

    let mut l = cfb::CompoundFile::open(Cursor::new(std::fs::read(lib)?))?;
    let path = format!("/{fp}/Data");
    let data = read_stream(&mut l, &path)?;
    let n = u32::from_le_bytes(data[0..4].try_into().unwrap()) as usize;
    let lspans = pad_spans(&data, 4 + n);
    if lspans.len() != 1 {
        bail!("expected one pad in {fp}, found {}", lspans.len());
    }
    let (ls, le) = lspans[0];
    let mut nd = data[..ls].to_vec();
    nd.extend_from_slice(&rec);
    nd.extend_from_slice(&data[le..]);
    {
        let mut st = l.open_stream(&path)?;
        st.seek(SeekFrom::Start(0))?;
        st.write_all(&nd)?;
        st.set_len(nd.len() as u64)?;
    }
    l.flush()?;
    std::fs::write(out, l.into_inner().into_inner())?;
    println!("spliced {} byte pad record (library pad was {} bytes) into {}", rec.len(), le - ls, out.display());
    Ok(())
}

fn main() -> Result<()> {
    let a: Vec<String> = std::env::args().collect();
    match a.get(1).map(|s| s.as_str()) {
        Some("summary") if a.len() == 3 => {
            let p = Path::new(&a[2]);
            let bb = altium::outline_bbox(p)?;
            let b = altium::read(p)?;
            let pl = plan::make_plan("altium", &a[2], bb, false);
            println!("bbox {:?} ({:.4} x {:.4} mm)", bb, bb.w(), bb.h());
            println!("components {} pads {} vias {} tracks {}/{} silk {}/{} models {}",
                b.components.len(), b.pad_count(), b.vias.len(), b.tracks_top.len(), b.tracks_bottom.len(),
                b.silk_top.len(), b.silk_bottom.len(), b.models.len());
            println!("meta {:?}", b.meta);
            println!("plan {}", serde_json::to_string(&pl.to_json())?);
        }
        Some("strict") if a.len() >= 3 => {
            for f in &a[2..] {
                let r = cfb::CompoundFile::open_strict(Cursor::new(std::fs::read(f)?));
                println!("{f}: {}", match r { Ok(_) => "strict OK".to_string(), Err(e) => format!("strict FAIL: {e}") });
            }
        }
        Some("padlib") if a.len() == 6 => padlib(Path::new(&a[2]), Path::new(&a[3]), &a[4], Path::new(&a[5]))?,
        _ => bail!("usage: altium_probe summary <PcbDoc> | padlib <molecule.PcbDoc> <template.PcbLib> <footprint> <out.PcbLib>"),
    }
    Ok(())
}