Skip to content
47 changes: 45 additions & 2 deletions crates/synth-place/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6493,8 +6493,11 @@ fn refine_placement<S: ::std::hash::BuildHasher>(
let undo = current.clone();
apply_recommendation(board, &mut current, action, rotation_overrides);
// The validator rejects a part inside a keepout, so a move that
// would put one there is dropped.
if keepout_overlaps(board, &current) > hits {
// would put one there is dropped. So is one that pushes a part into
// the perimeter channel the outline was fitted with.
if keepout_overlaps(board, &current) > hits
|| enters_edge_margin(board, &undo, &current, &action.refdes)
{
current = undo;
}
}
Expand Down Expand Up @@ -7571,6 +7574,46 @@ fn shorten_wires(board: &Board, mut placement: Placement) -> Placement {
placement
}

/// Whether moving `refdes` from `before` to `after` takes a connector or IC
/// closer to the outline than the fitted edge margin.
///
/// The outline is fitted with that margin round every such part, but a review
/// recommendation sets a coordinate directly: separating two courtyards can
/// push one of them into the perimeter channel, and a soft-hinted connector
/// then ends up nearly flush. A passive keeps its own smaller rim, an
/// edge-flush connector and a board feature their own, so none is judged here.
/// A part already inside the margin may still move, as long as it does not
/// get closer.
fn enters_edge_margin(board: &Board, before: &Placement, after: &Placement, refdes: &str) -> bool {
let Some(component) = board.components.iter().find(|c| c.refdes == refdes) else {
return false;
};
if is_passive(component)
|| is_edge_flush_connector(component)
|| synth_layout::is_board_feature_kind(&component.kind)
{
return false;
}
let gap = |placement: &Placement| {
placement
.components
.iter()
.find(|placed| placed.id == component.id)
.map(|placed| {
let rect = sidecar_courtyard_rect(board, component, placed);
let outline = placement.board_outline;
(rect.min.x_nm - outline.min.x_nm)
.min(outline.max.x_nm - rect.max.x_nm)
.min(rect.min.y_nm - outline.min.y_nm)
.min(outline.max.y_nm - rect.max.y_nm)
})
};
match (gap(before), gap(after)) {
(Some(before), Some(after)) => after < fitted_edge_margin_nm(board) && after < before,
_ => false,
}
}

/// Pull every movable part back inside the copper edge clearance. The review
/// recommendations can set a coordinate directly, bypassing the slot search,
/// so this is the last guarantee that no pad sits on Edge.Cuts.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,67 +2,67 @@
source: crates/synth-place/tests/snapshots.rs
expression: snapshot
---
board_outline_mm: 72.07 x 47.27
board_outline_mm: 72.75 x 48.44

U1 ( 35.80, 23.73) mm rot=Zero layer=Top
U2 ( 19.30, 20.73) mm rot=Zero layer=Top
U3 ( 52.80, 17.73) mm rot=Zero layer=Top
U4 ( 52.80, 26.73) mm rot=Zero layer=Top
U5 ( 56.80, 33.73) mm rot=Zero layer=Top
U6 ( 50.80, 36.73) mm rot=Zero layer=Top
U7 ( 52.80, 41.73) mm rot=Zero layer=Top
U8 ( 48.80, 41.73) mm rot=Zero layer=Top
U9 ( 56.30, 41.73) mm rot=Zero layer=Top
J1 ( 19.30, 8.73) mm rot=OneEighty layer=Top
J2 ( 17.30, 28.23) mm rot=TwoSeventy layer=Top
J3 ( 8.30, 30.73) mm rot=TwoSeventy layer=Top
J4 ( 6.30, 19.23) mm rot=Zero layer=Top
J5 ( 66.30, 23.73) mm rot=Zero layer=Top
Y1 ( 7.80, 8.73) mm rot=Zero layer=Top
SW1 ( 40.30, 35.23) mm rot=Zero layer=Top
SW2 ( 31.80, 10.73) mm rot=Ninety layer=Top
C1 ( 24.30, 25.73) mm rot=Zero layer=Top
C2 ( 26.30, 20.73) mm rot=Zero layer=Top
C3 ( 32.80, 32.73) mm rot=Zero layer=Top
D1 ( 37.30, 14.73) mm rot=Zero layer=Top
D2 ( 37.30, 12.73) mm rot=Zero layer=Top
D3 ( 58.00, 25.50) mm rot=Zero layer=Top
D4 ( 40.50, 11.50) mm rot=Zero layer=Top
D5 ( 26.50, 31.00) mm rot=Zero layer=Top
U1 ( 36.48, 24.21) mm rot=Zero layer=Top
U2 ( 18.98, 20.71) mm rot=Zero layer=Top
U3 ( 53.48, 18.21) mm rot=Zero layer=Top
U4 ( 53.48, 26.71) mm rot=Zero layer=Top
U5 ( 57.48, 34.21) mm rot=Zero layer=Top
U6 ( 51.48, 37.71) mm rot=Zero layer=Top
U7 ( 53.98, 42.71) mm rot=Zero layer=Top
U8 ( 58.48, 42.71) mm rot=Zero layer=Top
U9 ( 49.48, 42.71) mm rot=Zero layer=Top
J1 ( 18.98, 8.71) mm rot=OneEighty layer=Top
J2 ( 17.48, 28.21) mm rot=TwoSeventy layer=Top
J3 ( 8.48, 30.71) mm rot=TwoSeventy layer=Top
J4 ( 5.98, 19.21) mm rot=Zero layer=Top
J5 ( 66.98, 24.21) mm rot=Zero layer=Top
Y1 ( 7.48, 8.71) mm rot=Zero layer=Top
SW1 ( 39.98, 35.71) mm rot=Zero layer=Top
SW2 ( 31.98, 11.21) mm rot=Ninety layer=Top
C1 ( 24.48, 25.71) mm rot=Zero layer=Top
C2 ( 26.98, 21.71) mm rot=Zero layer=Top
C3 ( 32.98, 33.21) mm rot=Zero layer=Top
D1 ( 26.50, 18.00) mm rot=Zero layer=Top
D2 ( 37.48, 13.21) mm rot=Zero layer=Top
D3 ( 59.00, 26.00) mm rot=Zero layer=Top
D4 ( 42.50, 12.00) mm rot=Zero layer=Top
D5 ( 26.00, 31.50) mm rot=Zero layer=Top
D6 ( 40.00, 8.00) mm rot=Zero layer=Top
D7 ( 26.50, 35.00) mm rot=Zero layer=Top
R1 ( 29.80, 4.73) mm rot=Zero layer=Top
R2 ( 5.80, 12.23) mm rot=OneEighty layer=Top
R3 ( 40.80, 39.73) mm rot=Zero layer=Top
R4 ( 33.30, 35.23) mm rot=Zero layer=Top
R5 ( 29.00, 32.50) mm rot=OneEighty layer=Top
R6 ( 23.30, 31.73) mm rot=Zero layer=Top
R7 ( 58.00, 24.00) mm rot=OneEighty layer=Top
R8 ( 40.50, 13.00) mm rot=Zero layer=Top
D7 ( 26.00, 35.50) mm rot=Zero layer=Top
R1 ( 29.48, 5.21) mm rot=Zero layer=Top
R2 ( 5.48, 12.21) mm rot=OneEighty layer=Top
R3 ( 41.48, 40.21) mm rot=Zero layer=Top
R4 ( 32.98, 35.71) mm rot=Zero layer=Top
R5 ( 26.50, 29.50) mm rot=OneEighty layer=Top
R6 ( 22.48, 32.71) mm rot=Zero layer=Top
R7 ( 59.00, 24.50) mm rot=OneEighty layer=Top
R8 ( 41.00, 13.50) mm rot=Zero layer=Top
R9 ( 26.00, 27.50) mm rot=Zero layer=Top
R10 ( 41.80, 9.73) mm rot=Zero layer=Top
R11 ( 27.80, 36.73) mm rot=Zero layer=Top
R12 ( 36.80, 8.73) mm rot=Zero layer=Top
R13 ( 56.00, 40.00) mm rot=Zero layer=Top
R14 ( 49.00, 23.00) mm rot=Zero layer=Top
R15 ( 32.30, 40.23) mm rot=Zero layer=Top
R16 ( 60.30, 20.73) mm rot=Zero layer=Top
R17 ( 49.80, 11.23) mm rot=OneEighty layer=Top
R18 ( 63.80, 32.73) mm rot=Zero layer=Top
C4 ( 23.30, 35.73) mm rot=Zero layer=Top
C5 ( 27.30, 40.23) mm rot=Zero layer=Top
C6 ( 46.80, 7.73) mm rot=Zero layer=Top
C7 ( 17.80, 32.73) mm rot=Zero layer=Top
C8 ( 22.80, 39.23) mm rot=Zero layer=Top
C9 ( 18.30, 35.23) mm rot=Zero layer=Top
C10 ( 17.80, 39.23) mm rot=OneEighty layer=Top
C11 ( 54.80, 11.23) mm rot=Zero layer=Top
C12 ( 51.80, 7.73) mm rot=Zero layer=Top
C13 ( 60.30, 17.23) mm rot=Zero layer=Top
C14 ( 60.30, 13.73) mm rot=Zero layer=Top
C15 ( 59.30, 10.23) mm rot=Zero layer=Top
C16 ( 56.80, 6.23) mm rot=Zero layer=Top
C17 ( 12.80, 39.23) mm rot=Zero layer=Top
C18 ( 64.30, 16.23) mm rot=Zero layer=Top
C19 ( 63.80, 36.73) mm rot=Zero layer=Top
C20 ( 64.30, 12.23) mm rot=Zero layer=Top
R10 ( 41.98, 9.71) mm rot=Zero layer=Top
R11 ( 25.50, 33.00) mm rot=Zero layer=Top
R12 ( 36.98, 8.71) mm rot=Zero layer=Top
R13 ( 47.00, 40.50) mm rot=OneEighty layer=Top
R14 ( 49.50, 23.00) mm rot=Zero layer=Top
R15 ( 32.98, 41.21) mm rot=Zero layer=Top
R16 ( 52.50, 23.00) mm rot=Zero layer=Top
R17 ( 50.48, 11.71) mm rot=OneEighty layer=Top
R18 ( 63.98, 33.21) mm rot=Zero layer=Top
C4 ( 22.98, 36.21) mm rot=Zero layer=Top
C5 ( 27.48, 41.21) mm rot=Zero layer=Top
C6 ( 36.48, 43.71) mm rot=Zero layer=Top
C7 ( 46.98, 7.71) mm rot=Zero layer=Top
C8 ( 17.48, 32.71) mm rot=Zero layer=Top
C9 ( 22.48, 40.21) mm rot=Zero layer=Top
C10 ( 17.98, 35.71) mm rot=OneEighty layer=Top
C11 ( 17.48, 40.21) mm rot=Zero layer=Top
C12 ( 55.48, 11.71) mm rot=Zero layer=Top
C13 ( 12.48, 40.21) mm rot=Zero layer=Top
C14 ( 60.48, 17.71) mm rot=Zero layer=Top
C15 ( 60.48, 14.21) mm rot=Zero layer=Top
C16 ( 52.48, 7.71) mm rot=Zero layer=Top
C17 ( 57.48, 7.71) mm rot=Zero layer=Top
C18 ( 60.98, 10.21) mm rot=Zero layer=Top
C19 ( 63.98, 37.21) mm rot=Zero layer=Top
C20 ( 64.48, 16.71) mm rot=Zero layer=Top
35 changes: 35 additions & 0 deletions crates/synth-registry/src/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,41 @@ footprint_only = true
assert_eq!(ep.number.0, "3");
}

#[test]
fn a_footprint_only_pin_must_not_exist_on_the_symbol() {
// The inverse of the exposed-pad case: when the flag is set on a
// number the symbol actually carries, the loader must reject it,
// otherwise a real symbol pin could be marked footprint-only and
// bypass pin validation.
if crate::kicad_pin_check::kicad_pin_numbers("Device:R").is_none() {
eprintln!("skipping footprint-only guard test: KiCad symbol library not present");
return;
}
// use a number present on the symbol (2), not a duplicate
let toml = r#"
id = "bogus_footprint_only"
kind = "resistor"
kicad_symbol = "Device:R"
[[pins]]
name = "p1"
number = "1"
electrical_type = "passive"
[[pins]]
name = "ep"
number = "2"
electrical_type = "passive"
footprint_only = true
"#;
let err = load_from_strs(&[("bogus_footprint_only.synth.toml", toml)])
.expect_err("pad 2 is on the symbol and must not be footprint-only");
match err {
LoadError::KicadPinMismatch {
registry_number, ..
} => assert_eq!(registry_number, "2"),
other => panic!("expected KicadPinMismatch, got {other:?}"),
}
}

#[test]
fn a_pad_that_is_not_footprint_only_is_still_checked() {
// The same part without the flag must still be rejected, or the
Expand Down
6 changes: 0 additions & 6 deletions registry/parts/memory/24lc256_eeprom.synth.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,6 @@ max_current_ma = 50.0

# Pin names and numbers come from the stock symbol the part names, which
# extends `24LC16`, cross-checked against the 8 pads of the SOIC-8 footprint.
#
# The previous map had `vdd` on pad 1, `gnd` on 2, `sda` on 3 and `scl` on 4.
# On this package those four pads are A0, A1, A2 and GND, so a board wired to
# this part put its supply on an address pin and its ground on another. The
# four names keep their meaning and move to the pads that carry it.

[[pins]]
name = "a0"
number = "1"
Expand Down
2 changes: 1 addition & 1 deletion registry/parts/passives/ferrite_bead_0603.synth.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
id = "ferrite_bead_0603"
kind = "inductor"
description = "0603 ferrite bead, 600Ω @ 100MHz — EMI suppression"
kicad_symbol = "Device:Ferrite_Bead"
kicad_symbol = "Device:FerriteBead"
kicad_footprint = "Inductor_SMD:L_0603_1608Metric"

[footprint_dimensions]
Expand Down
21 changes: 18 additions & 3 deletions registry/parts/sensors/tmp117_temperature.synth.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,26 @@ id = "tmp117_temperature"
kind = "sensor"
description = "TI TMP117 high-accuracy temperature sensor, I²C"
kicad_symbol = "Sensor_Temperature:TMP117xxDRV"
kicad_footprint = "Package_TO_SOT_SMD:SOT-563"
# The symbol the part names is `TMP117xxDRV`, and DRV is TI's code for the
# 2.00 x 2.00 mm six-pin WSON with an exposed pad. The footprint said
# `SOT-563`, a 1.6 x 1.2 mm leaded package that is a different part in a
# different body, so every exported land, pad position and courtyard was
# taken from the wrong outline.
#
# Pad 7 is the exposed thermal pad, declared `footprint_only` because the
# stock symbol has no pin for it: `TMP117xxDRV` extends `TMP102xxDRL`, which
# stops at pin 6. That flag is what lets this part name the right footprint
# and still account for every copper pad; without it the two were mutually
# exclusive.
#
# It is not `required`. The datasheet has the pad not connected to device
# ground and asks for it to be left unsoldered for best measurement accuracy,
# and says that if it is soldered it must be left floating or grounded.
kicad_footprint = "Package_SON:WSON-6-1EP_2x2mm_P0.65mm_EP1x1.6mm"

[footprint_dimensions]
width_mm = 1.6
height_mm = 1.6
width_mm = 2.0
height_mm = 2.0
courtyard_margin_mm = 0.25

[operating_conditions]
Expand Down
Loading