Skip to content

Commit 9a3c613

Browse files
lfyyshclaude
andcommitted
schematic: redistribute stagger groups, fix power symbol Y-flip, lower min_group
Three fixes: 1. IC groups with stagger fans are now vertically redistributed so they don't overlap. After staggering, each group's bounding box is computed (IC + all dependent parts), sorted by Y, and shifted apart with margin. 2. Power symbol angle mapping swaps U/D to account for Y-axis flip between SKiDL internal coords (Y-up) and KiCad schematic coords (Y-down). 3. Stagger min_group lowered from 3 to 2, so smaller ICs like optocouplers get T-junction treatment. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9008d15 commit 9a3c613

2 files changed

Lines changed: 112 additions & 13 deletions

File tree

src/skidl/tools/kicad9/gen_schematic.py

Lines changed: 107 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -620,17 +620,16 @@ def _snap_two_pin_parts(node):
620620
_stagger_tjunctions(node, node_part_ids, snapped, occupied_pins)
621621

622622

623-
def _stagger_tjunctions(node, node_part_ids, snapped, occupied_pins, min_group=3):
623+
def _stagger_tjunctions(node, node_part_ids, snapped, occupied_pins, min_group=2):
624624
"""Detect repeating T-junction patterns and stagger parts outward from IC.
625625
626626
A T-junction occurs when 2+ two-pin parts share a signal net with the same
627627
IC pin. When 3+ IC pins on the same IC have the same fan-out count, it's a
628628
repeating pattern. Parts are rearranged so each pin's group steps further
629629
from the IC body, with all parts extending perpendicular.
630630
631-
Also moves the pass-1 snapped part from its overlapping position to the
632-
staggered position, and records junction wires on node._tjunction_wires
633-
for sexp_schematic to render.
631+
After staggering, IC groups are vertically redistributed so their stagger
632+
fans don't overlap.
634633
"""
635634
perp_map = {"R": "D", "L": "U", "U": "R", "D": "L"}
636635
anti_perp = {"U": "D", "D": "U", "L": "R", "R": "L"}
@@ -679,6 +678,9 @@ def _stagger_tjunctions(node, node_part_ids, snapped, occupied_pins, min_group=3
679678
junction_wires = getattr(node, "_tjunction_wires", [])
680679
suppressed_pins = set()
681680

681+
# Collect stagger group metadata for redistribution pass.
682+
stagger_groups = []
683+
682684
for ic_id, pin_entries in ic_groups.items():
683685
fanout_counts = [len(pl) for _, pl in pin_entries]
684686
dominant = max(set(fanout_counts), key=fanout_counts.count)
@@ -707,10 +709,10 @@ def _stagger_tjunctions(node, node_part_ids, snapped, occupied_pins, min_group=3
707709
max_span = max(max_span, span)
708710
step_size = max(100, int(max_span) + 50)
709711

710-
def _pin_sort_key(entry):
712+
def _pin_sort_key(entry, _ic_part=ic_part, _ic_dir=ic_dir):
711713
ic_pin = entry[0]
712-
w = ic_pin.pt * ic_part.tx
713-
if ic_dir in ("L", "R"):
714+
w = ic_pin.pt * _ic_part.tx
715+
if _ic_dir in ("L", "R"):
714716
return w.y
715717
return w.x
716718

@@ -720,6 +722,9 @@ def _pin_sort_key(entry):
720722
anti = anti_perp.get(perp_dir, perp_dir)
721723
extend_dirs = [perp_dir, anti] if parts_per_pin >= 2 else [perp_dir]
722724

725+
group_parts = []
726+
group_wires = []
727+
723728
for pin_idx, (ic_pin, parts_list) in enumerate(matching):
724729
ic_pin_world = ic_pin.pt * ic_part.tx
725730

@@ -737,15 +742,109 @@ def _pin_sort_key(entry):
737742
)
738743
snapped.add(id(part))
739744
suppressed_pins.add(id(my_pin))
745+
group_parts.append(part)
740746

741-
junction_wires.append(
747+
group_wires.append(
742748
(ic_pin_world.x, ic_pin_world.y, ox, oy)
743749
)
744750

751+
stagger_groups.append({
752+
"ic_part": ic_part,
753+
"parts": group_parts,
754+
"wires": group_wires,
755+
"ic_dir": ic_dir,
756+
"n_pins": len(matching),
757+
"step_size": step_size,
758+
})
759+
760+
# Redistribute IC groups vertically so stagger fans don't overlap.
761+
if len(stagger_groups) > 1:
762+
_redistribute_stagger_groups(stagger_groups, node, snapped)
763+
764+
for grp in stagger_groups:
765+
junction_wires.extend(grp["wires"])
766+
745767
node._tjunction_wires = junction_wires
746768
node._tjunction_suppressed_pins = suppressed_pins
747769

748770

771+
def _redistribute_stagger_groups(groups, node, snapped):
772+
"""Shift IC groups vertically so their stagger fans don't overlap."""
773+
774+
for grp in groups:
775+
_collect_ic_dependents(grp, node, snapped)
776+
777+
def _group_bbox(grp):
778+
all_parts = [grp["ic_part"]] + grp["all_deps"]
779+
min_y = float("inf")
780+
max_y = float("-inf")
781+
782+
for part in all_parts:
783+
for pin in part.pins:
784+
w = pin.pt * part.tx
785+
min_y = min(min_y, w.y)
786+
max_y = max(max_y, w.y)
787+
788+
return min_y, max_y
789+
790+
groups.sort(key=lambda g: _group_bbox(g)[0])
791+
792+
margin = 200
793+
prev_max_y = None
794+
795+
for grp in groups:
796+
min_y, max_y = _group_bbox(grp)
797+
798+
if prev_max_y is not None and min_y < prev_max_y + margin:
799+
shift = (prev_max_y + margin) - min_y
800+
_shift_group(grp, 0, shift)
801+
min_y += shift
802+
max_y += shift
803+
804+
prev_max_y = max_y
805+
806+
807+
def _collect_ic_dependents(grp, node, snapped):
808+
"""Find all snapped 2-pin parts connected to this IC (not just stagger parts)."""
809+
ic = grp["ic_part"]
810+
ic_id = id(ic)
811+
deps = set(id(p) for p in grp["parts"])
812+
deps.add(ic_id)
813+
814+
for part in node.parts:
815+
if id(part) in deps or id(part) not in snapped:
816+
continue
817+
if not _is_two_pin_part(part):
818+
continue
819+
for pin in part.pins:
820+
net = getattr(pin, "net", None)
821+
if not net:
822+
continue
823+
for net_pin in net.pins:
824+
if net_pin.part is ic:
825+
deps.add(id(part))
826+
break
827+
if id(part) in deps:
828+
break
829+
830+
grp["all_deps"] = [p for p in node.parts if id(p) in deps and p is not ic]
831+
832+
833+
def _shift_group(grp, dx, dy):
834+
"""Shift an IC and all its dependent parts by (dx, dy)."""
835+
vec = Point(dx, dy)
836+
all_parts = [grp["ic_part"]] + grp["all_deps"]
837+
shifted_ids = set()
838+
for part in all_parts:
839+
if id(part) not in shifted_ids:
840+
part.tx = part.tx.move(vec)
841+
shifted_ids.add(id(part))
842+
grp["wires"] = [
843+
(x1 + dx, y1 + dy, x2 + dx, y2 + dy)
844+
for (x1, y1, x2, y2) in grp["wires"]
845+
]
846+
847+
749848
def _stub_all_non_explicit(circuit):
750849
"""Stub all nets that weren't explicitly set by the user (labels-only fallback).
751850

src/skidl/tools/kicad9/sexp_schematic.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -258,15 +258,15 @@ def _power_symbol_to_sexp(pin, net_name, tx):
258258
y = _round_mm(pt.y)
259259

260260
# Rotate the power symbol so its graphical part points AWAY from the
261-
# component, in the same direction as the pin. At angle=0, ground-type
262-
# symbols (GND, GNDA, etc.) have bars pointing down; supply-type symbols
263-
# (VCC, +5V, etc.) have arrows/bars pointing up.
261+
# component. calc_pin_dir returns directions in SKiDL's internal coords
262+
# (Y-up), but KiCad schematics use Y-down, so U and D are swapped.
263+
# At angle=0: GND bars point down, supply arrows point up (KiCad convention).
264264
pin_dir = calc_pin_dir(pin)
265265
_is_gnd = any(g in net_name.upper() for g in ("GND", "VSS", "EARTH"))
266266
if _is_gnd:
267-
angle = {"D": 0, "U": 180, "R": 270, "L": 90}.get(pin_dir, 0)
267+
angle = {"U": 0, "D": 180, "R": 270, "L": 90}.get(pin_dir, 0)
268268
else:
269-
angle = {"U": 0, "D": 180, "R": 90, "L": 270}.get(pin_dir, 0)
269+
angle = {"D": 0, "U": 180, "R": 90, "L": 270}.get(pin_dir, 0)
270270

271271
lib_id = f"power:{net_name}"
272272
inst_uuid = _gen_uuid(f"pwr:{net_name}:{x}:{y}:{_pwr_counter[0]}")

0 commit comments

Comments
 (0)