app
Board to Molecule
Public Made by Adomby adom
Turn any KiCad, Fusion/EAGLE, or Altium board into an Adom molecule: wings plus MP1-MP4 machine pins, written back in the board's own format.
main
Colby Knox
board-to-molecule 1.0.0: Rust source, six before/after examples (KiCad, Fusion, Altium), Adom 3D viewer readme
eb9ea91
4d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
//! KiCad 9+ embedded files: carry the machine-pin STEP inside the .kicad_pcb.
//!
//! KiCad stores each file as base64 of a zstd frame plus a checksum. We write a
//! valid zstd frame made of raw (stored) blocks, which needs no compressor, and a
//! SHA-256 checksum, which KiCad still accepts from older files and verifies.
//! A board with its pin models embedded renders the pins on any machine, including
//! service-kicad, which only ever sees the .kicad_pcb bytes.
use base64::Engine;
use sha2::{Digest, Sha256};
const MAX_BLOCK: usize = 128 * 1024;
/// A single zstd frame (single segment, known content size) of raw blocks.
pub fn zstd_raw_frame(data: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(data.len() + data.len() / MAX_BLOCK * 3 + 16);
out.extend_from_slice(&[0x28, 0xB5, 0x2F, 0xFD]);
// Frame header descriptor: FCS field 8 bytes (flag 3), single segment, no checksum, no dict.
out.push(0b1110_0000);
out.extend_from_slice(&(data.len() as u64).to_le_bytes());
if data.is_empty() {
out.extend_from_slice(&[1, 0, 0]); // last, raw, size 0
return out;
}
let mut chunks = data.chunks(MAX_BLOCK).peekable();
while let Some(c) = chunks.next() {
let last = chunks.peek().is_none() as u32;
let hdr = last | ((c.len() as u32) << 3); // block type 0 = raw
out.extend_from_slice(&hdr.to_le_bytes()[..3]);
out.extend_from_slice(c);
}
out
}
/// One `(file ...)` entry for an `(embedded_files ...)` block, KiCad's own layout.
pub fn file_entry(name: &str, data: &[u8], indent: &str) -> String {
let b64 = base64::engine::general_purpose::STANDARD.encode(zstd_raw_frame(data));
let sum = hex(&Sha256::digest(data));
let mut s = format!("{indent}(file\n{indent}\t(name \"{name}\")\n{indent}\t(type model)\n{indent}\t(data");
let n = b64.len();
let mut first = 0;
while first < n {
let end = (first + 76).min(n);
s.push('\n');
if first == 0 {
s.push('|');
}
s.push_str(&b64[first..end]);
if end == n {
s.push('|');
}
first = end;
}
s.push_str(&format!("\n{indent}\t)\n{indent}\t(checksum \"{sum}\")\n{indent})"));
s
}
fn hex(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn frame_layout() {
let data = vec![7u8; MAX_BLOCK + 10];
let f = zstd_raw_frame(&data);
assert_eq!(&f[..4], &[0x28, 0xB5, 0x2F, 0xFD]);
assert_eq!(u64::from_le_bytes(f[5..13].try_into().unwrap()), data.len() as u64);
// first block: not last, raw, full size
let h = u32::from_le_bytes([f[13], f[14], f[15], 0]);
assert_eq!(h & 1, 0);
assert_eq!((h >> 3) as usize, MAX_BLOCK);
assert_eq!(f.len(), 13 + 3 + MAX_BLOCK + 3 + 10);
}
}