diff --git a/crates/adom-aiflow/src/main.rs b/crates/adom-aiflow/src/main.rs index caf048f..b59d626 100644 --- a/crates/adom-aiflow/src/main.rs +++ b/crates/adom-aiflow/src/main.rs @@ -3425,7 +3425,12 @@ fn main() { for k in ["powerInputs", "testPoints"] { audit[k] = labels[k].clone(); } std::fs::write(out, serde_json::to_string_pretty(&audit).unwrap()).unwrap_or_else(|e| err(&format!("{}: {e}", out.display()), &[])); let data = read_json(&input.display().to_string()); - let mut proof = silkproof::proof(&audit, &data, &b).unwrap_or_else(|why| { + // a live `silkscreen proof` deviation on the run's current board and spec answers the label count only + let waiver = if board_path == r.current_board() { + let (bs, ss) = run_hashes(&r); + deviations::live(&r.data, &bs, &ss).into_iter().find(|d| d["item"] == "silkscreen proof") + } else { None }; + let mut proof = silkproof::proof_with(&audit, &data, &b, waiver.as_ref()).unwrap_or_else(|why| { r.log("silkscreen-proof-refused", json!({"board": board_path, "audit": out, "why": why})); err(&format!("silkscreen proof not recorded: {}", why.join("; ")), &["Build the inventory from the whole board (every footprint by its reference, every via and hole on both faces) and fix the labels, then `silkscreen-audit --record` again.".into()]) }); @@ -3444,6 +3449,7 @@ fn main() { // 0.2.18: what the labels say, not only where they sit: power inputs without a voltage, test points without their net for w in silk_polarity::warnings(&b).iter().filter(|w| w.contains("power input") || w.contains("test point")) { eprintln!("Warning: {w}"); } for f in fab["findings"].as_array().into_iter().flatten().filter_map(Value::as_str) { eprintln!("Warning (Fab layer, not blocking): {f}. {}", fab_layer::TIDY_HINT); } + if !proof["labelDensityWaived"].is_null() { eprintln!("Note: the label count ({} for {} footprints) is answered by the deviation \"silkscreen proof\": {}", proof["labelDensityWaived"]["labels"], proof["labelDensityWaived"]["footprints"], proof["labelDensityWaived"]["why"]); } ok(&format!("{}silkscreen step proof recorded: {} footprints and {} vias covered, {} labels\n{}", String::from_utf8_lossy(&result.stdout), proof["footprints"], proof["vias"], proof["labels"], silk_polarity::label_check_lines(&labels).join("\n")), &["Geometry audit only; native DRC, label association and both native 2D/3D views remain required. Then `step 3d`.".into()]); return; } @@ -4957,7 +4963,12 @@ fn main() { let plan_v = plan_with_board_copper(&read_json(&planp), &board_now); let spec = spec_of(&r); let zsp = dir.join("zone-state.json"); - let zs = if zsp.is_file() { Some(read_json(&zsp.display().to_string())) } else { None }; + let zs = if zsp.is_file() { Some(read_json(&zsp.display().to_string())) } else { + // no live measurement on this run (offline pours, adopted): read the pours from the saved board's fills + let z = aiflow_analyze::pour_cover::zone_state_from_board(&board_now); + let n = z["zones"].as_array().map(|a| a.len()).unwrap_or(0); + if n > 0 { eprintln!("no live copper measurement on this run (no zone-state.json): the pours are read from the saved board's own fills ({n} filled zone(s))"); Some(z) } else { None } + }; let name = format!("analyze-{what}"); r.stage_start(&name, Some("binary"), None); let (mut heuristic_passed, mut res) = match what.as_str() { "current" => aiflow_analyze::analyze_current_on_board(&plan_v, &spec, zs.as_ref(), Some(&pad_layers_of(&board_now)), &board_now), "thermal" => aiflow_analyze::analyze_thermal_on_board(&plan_v, &spec, zs.as_ref(), &board_now), other => err(&format!("analyze {other}: use current, thermal or impedance"), &[]) }; diff --git a/crates/aiflow-analyze/src/pour_cover.rs b/crates/aiflow-analyze/src/pour_cover.rs index 6bafd28..880c4be 100644 --- a/crates/aiflow-analyze/src/pour_cover.rs +++ b/crates/aiflow-analyze/src/pour_cover.rs @@ -70,6 +70,34 @@ fn net_fills(board: &Board, net: &str) -> Vec { out } +/// Offline runs (pours written with pcbnew in a container and adopted with `verify-pours --from-board`, +/// no live editor): the zone state `measure` reads from the editor, built from the saved board's own +/// filled polygons instead, so `analyze current|thermal` see the pours the board really carries. +/// Only zones with saved fills count (an unfilled zone gives nothing); keepouts are skipped. +pub fn zone_state_from_board(board: &Board) -> serde_json::Value { + let mut zones = Vec::new(); + for z in board.root.find_all("zone") { + if z.find("keepout").is_some() { continue; } + let net = board.item_net(z); + if net.is_empty() { continue; } + let mut fills = Vec::new(); + let mut layers: Vec = Vec::new(); + for f in z.find_all("filled_polygon") { + let Some(layer) = f.find("layer").and_then(|l| l.value()) else { continue }; + let poly = pts_of(f); + if poly.len() < 3 { continue; } + let mut a = 0.0; + for i in 0..poly.len() { let (p, q) = (poly[i], poly[(i + 1) % poly.len()]); a += p.0 * q.1 - q.0 * p.1; } + fills.push(serde_json::json!({"layer": layer, "areaMm2": ((a / 2.0).abs() * 10.0).round() / 10.0})); + if !layers.iter().any(|l| l == layer) { layers.push(layer.to_string()); } + } + if fills.is_empty() { continue; } + let name = z.find("name").and_then(|n| n.value()).unwrap_or("").to_string(); + zones.push(serde_json::json!({"type": "copper", "net": net, "name": name, "layers": layers, "fills": fills})); + } + serde_json::json!({"source": "saved-board", "zones": zones}) +} + fn in_box(p: Pt, b: (f64, f64, f64, f64), m: f64) -> bool { p.0 >= b.0 - m && p.0 <= b.2 + m && p.1 >= b.1 - m && p.1 <= b.3 + m } fn pad_on(p: &aiflow_board::Pad, layer: &str) -> bool {