--- a/crates/adom-aiflow/src/checks.rs +++ b/crates/adom-aiflow/src/checks.rs @@ -332,6 +332,7 @@ for r in fixed.keys() { if !refs.contains(r) { refs.push(r.clone()); } } } let mut moved = Vec::new(); + let mut moved_refs: Vec<&String> = Vec::new(); for r in &refs { if let (Some(a), Some(c)) = (b0.footprints.get(r), b.footprints.get(r)) { // The explicit position was checked above. It may intentionally revise a @@ -341,12 +342,28 @@ && ((a.x - c.x).abs() > 0.005 || (a.y - c.y).abs() > 0.005); if moved_position || (a.rot - c.rot).abs() > 0.01 { moved.push(format!("{r} ({:.2}, {:.2}, {:.0} deg) -> ({:.2}, {:.2}, {:.0} deg)", a.x, a.y, a.rot, c.x, c.y, c.rot)); + moved_refs.push(r); } } else { f.error(format!("fixed reference {r} missing from current or starting board"), "Restore the reference and its reviewed interface binding; absence is not an unchanged position."); } } + // A rename in place is not a move (the molecule rule itself asks for one when the start board + // numbered its corner pins wrongly): every "moved" reference now sits exactly where another + // moved reference sat on the start board, same footprint and rotation, one to one. The + // physical interface is unchanged; the rest of this check judges the new names (MP1 front-left). + let pose_eq = |a: &aiflow_board::Footprint, c: &aiflow_board::Footprint| a.lib == c.lib && (a.x - c.x).abs() <= 0.005 && (a.y - c.y).abs() <= 0.005 && (a.rot - c.rot).abs() <= 0.01; + let mut taken = vec![false; moved_refs.len()]; + let mut renamed = Vec::new(); + let rename_only = !moved_refs.is_empty() && moved_refs.iter().all(|r| { + let c = &b.footprints[*r]; + match moved_refs.iter().enumerate().find(|(i, s)| !taken[*i] && pose_eq(&b0.footprints[**s], c)) { + Some((i, s)) => { taken[i] = true; renamed.push(format!("{r} is the part that was {s}")); true } + None => false, + } + }); if moved.is_empty() { f.ok(format!("{} fixed references match explicit positions or starting-board locks; anchor and rotations retained", refs.len())); } + else if rename_only { f.ok(format!("{} fixed references renamed in place, nothing moved (same footprints, positions and rotations as the start board): {}", renamed.len(), renamed.join("; "))); } else { f.error(format!("fixed references moved since the run started: {}", moved.join("; ")), "Put them back where the start board had them (spec.fixedRefs); placement must not move the molecule interface."); } } pin_class_check(b, spec, &m, bom, &mut f); @@ -1319,6 +1336,20 @@ assert!(f.errors.iter().any(|e| e.contains("moved since the run started")), "{:?}", f.lines); } + #[test] + fn corner_pins_renamed_in_place_are_not_moved() { + // the start board numbered MP1 and MP3 the other way round; the human renamed them to the rule + let spec = json!({"molecule": {}}); + let start = BOARD.replace("\"MP1\"", "\"MPX\"").replace("\"MP3\"", "\"MP1\"").replace("\"MPX\"", "\"MP3\""); + let f = molecule_check(&board(BOARD), &spec, Some(&board(&start))); + assert!(f.errors.is_empty(), "{:?}", f.lines); + assert!(f.lines.iter().any(|l| l.contains("renamed in place") && l.contains("MP1 is the part that was MP3")), "{:?}", f.lines); + // a rename plus a real move is still a move + let moved = BOARD.replace("(at 100 84) (property \"Reference\" \"MP3\")", "(at 100 86) (property \"Reference\" \"MP3\")"); + let f = molecule_check(&board(&moved), &spec, Some(&board(&start))); + assert!(f.errors.iter().any(|e| e.contains("moved since the run started")), "{:?}", f.lines); + } + #[test] fn explicit_interface_revision_retains_all_other_locks() { let spec = json!({"molecule": {"fixed": {"MC1": [0, 10]}}, "fixedRefs": ["MC1"]});