app
AI Flow: Reverse Engineering
Public Made by Adomby adom
Photos of a PCB to a tagged KiCad copy: parts placed, copper and nets with their evidence, a schematic, a measured confidence score, bench tests and a video of the run.
main
Caleb
Publish 0.5.0
1984b40
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
//! A board the photos cut off. `prep` records, per photo, which sides of the outline are the photo's edge and not the board's
//! (`prep.json` photos[].notBoardEdge). Wherever a size is stated (the report tables, the video), it is then the size of the
//! photographed window, and the words say so: board 9 of the 2026-11-10 set was photographed in part, and the video said
//! "The board measures 44.15 by 26.33 millimetres" with no word that it runs out of both photos.
use crate::items;
use serde_json::Value;
use std::path::Path;
/// The sides, per face, where the photo stops before the board does: [(face, [sides])], only faces with a cut side.
pub fn cut(flow_dir: &Path) -> Vec<(String, Vec<String>)> {
let prep = items::read_json(&flow_dir.join("prep.json")).unwrap_or(Value::Null);
let mut out: Vec<(String, Vec<String>)> = vec![];
for p in prep["photos"].as_array().into_iter().flatten() {
let sides: Vec<String> = p["notBoardEdge"].as_array().into_iter().flatten().filter_map(|x| x.as_str().map(str::to_string)).collect();
if sides.is_empty() { continue; }
let face = p["side"].as_str().unwrap_or("top").to_string();
match out.iter_mut().find(|x| x.0 == face) { Some(x) => { for s in sides { if !x.1.contains(&s) { x.1.push(s); } } } None => out.push((face, sides)) }
}
for x in out.iter_mut() { x.1.sort(); }
out
}
fn list(v: &[String]) -> String { match v.len() { 0 => String::new(), 1 => v[0].clone(), n => format!("{} and {}", v[..n - 1].join(", "), v[n - 1]) } }
/// "the top photo at its left and right sides; the bottom photo at its left and right sides"
pub fn where_cut(cut: &[(String, Vec<String>)]) -> String {
cut.iter().map(|(face, sides)| format!("the {face} photo at its {} side{}", list(sides), if sides.len() == 1 { "" } else { "s" })).collect::<Vec<_>>().join("; ")
}
/// The board's size as the run may state it: of the board when every side is the board's edge, of the photographed window when not.
pub fn size_sentence(flow_dir: &Path) -> Option<String> {
let scale = items::read_json(&flow_dir.join("scale.json")).ok()?;
let (w, h) = (scale["board_mm"][0].as_f64()?, scale["board_mm"][1].as_f64()?);
let c = cut(flow_dir);
Some(if c.is_empty() { format!("The board measures {w:.2} by {h:.2} mm.") }
else { format!("The photographed window measures {w:.2} by {h:.2} mm. This is NOT the board's size: the photos do not show the whole board ({} the outline is the photo's edge, not the board's edge). Parts, copper and nets beyond the window are not in this run.", where_cut(&c) + ":") })
}
/// For the video's facts: what is cut, in the words to speak and show.
pub fn facts(flow_dir: &Path) -> Value {
let c = cut(flow_dir);
if c.is_empty() { return Value::Null; }
serde_json::json!({"faces": c.iter().map(|(f, s)| serde_json::json!({"face": f, "sides": s})).collect::<Vec<_>>(), "where": where_cut(&c)})
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn a_size_is_the_windows_when_a_side_is_the_photos_edge() {
let d = std::env::temp_dir().join(format!("window-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d); std::fs::create_dir_all(&d).unwrap();
std::fs::write(d.join("scale.json"), json!({"board_mm": [44.15, 26.33]}).to_string()).unwrap();
std::fs::write(d.join("prep.json"), json!({"photos": [{"file": "top.jpg", "side": "top", "notBoardEdge": []}, {"file": "bottom.jpg", "side": "bottom"}]}).to_string()).unwrap();
assert!(cut(&d).is_empty() && facts(&d).is_null());
assert_eq!(size_sentence(&d).unwrap(), "The board measures 44.15 by 26.33 mm.");
// board 9: both photos stop before the board does, left and right
std::fs::write(d.join("prep.json"), json!({"photos": [{"file": "top.jpg", "side": "top", "notBoardEdge": ["right", "left"]}, {"file": "bottom.jpg", "side": "bottom", "notBoardEdge": ["left"]}]}).to_string()).unwrap();
let s = size_sentence(&d).unwrap();
assert!(s.starts_with("The photographed window measures 44.15 by 26.33 mm. This is NOT the board's size"), "{s}");
assert!(s.contains("the top photo at its left and right sides; the bottom photo at its left side:"), "{s}");
assert!(!s.contains("The board measures"));
assert_eq!(facts(&d)["faces"][0]["sides"], json!(["left", "right"]));
std::fs::remove_dir_all(&d).ok();
}
}