Skip to content

Commit d7c302e

Browse files
lfyyshclaude
andcommitted
schematic: pre-shift ICs before stagger placement to prevent overlap
Moves the IC redistribution to BEFORE stagger fans are placed. Phase 1 identifies qualifying groups, computes how much stagger space each IC will need, collects parts already snapped to each IC, and shifts them apart vertically. Phase 2 then places stagger fans at the final IC positions. This preserves connectivity (snapped parts move with their IC) while preventing fan overlap. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d9e1566 commit d7c302e

1 file changed

Lines changed: 103 additions & 15 deletions

File tree

src/skidl/tools/kicad9/gen_schematic.py

Lines changed: 103 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -623,13 +623,9 @@ def _snap_two_pin_parts(node):
623623
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
626-
A T-junction occurs when 2+ two-pin parts share a signal net with the same
627-
IC pin. When 3+ IC pins on the same IC have the same fan-out count, it's a
628-
repeating pattern. Parts are rearranged so each pin's group steps further
629-
from the IC body, with all parts extending perpendicular.
630-
631-
After staggering, IC groups are vertically redistributed so their stagger
632-
fans don't overlap.
626+
Phase 1: identify stagger groups, compute how much space each needs,
627+
and shift ICs apart vertically so fans won't overlap.
628+
Phase 2: place the staggered parts at the (now separated) IC positions.
633629
"""
634630
perp_map = {"R": "D", "L": "U", "U": "R", "D": "L"}
635631
anti_perp = {"U": "D", "D": "U", "L": "R", "R": "L"}
@@ -675,11 +671,9 @@ def _stagger_tjunctions(node, node_part_ids, snapped, occupied_pins, min_group=2
675671
ic = parts_list[0][4]
676672
ic_groups[id(ic)].append((parts_list[0][3], parts_list))
677673

678-
junction_wires = getattr(node, "_tjunction_wires", [])
679-
suppressed_pins = set()
680-
681-
# Collect stagger group metadata for redistribution pass.
682-
stagger_groups = []
674+
# ── Phase 1: identify qualifying groups and pre-shift ICs ─────────
675+
MM_TO_MILS = 1 / 0.0254
676+
stagger_plans = []
683677

684678
for ic_id, pin_entries in ic_groups.items():
685679
fanout_counts = [len(pl) for _, pl in pin_entries]
@@ -693,10 +687,8 @@ def _stagger_tjunctions(node, node_part_ids, snapped, occupied_pins, min_group=2
693687

694688
ic_part = matching[0][1][0][4]
695689
ic_dir = _pin_world_orient(matching[0][0], ic_part)
696-
perp_dir = perp_map.get(ic_dir, ic_dir)
697690
step_dx, step_dy = _dir_vec.get(ic_dir, (1, 0))
698691

699-
MM_TO_MILS = 1 / 0.0254
700692
max_span = 0
701693
for _, parts_list_scan in matching:
702694
for (scan_part, _, _, _, _) in parts_list_scan:
@@ -709,6 +701,36 @@ def _stagger_tjunctions(node, node_part_ids, snapped, occupied_pins, min_group=2
709701
max_span = max(max_span, span)
710702
step_size = max(100, int(max_span) + 50)
711703

704+
n_pins = len(matching)
705+
stagger_extent = step_size * n_pins + max_span
706+
707+
stagger_plans.append({
708+
"ic_part": ic_part,
709+
"matching": matching,
710+
"ic_dir": ic_dir,
711+
"step_dx": step_dx,
712+
"step_dy": step_dy,
713+
"step_size": step_size,
714+
"stagger_extent": stagger_extent,
715+
"dominant": dominant,
716+
})
717+
718+
if len(stagger_plans) > 1:
719+
_pre_shift_ics(stagger_plans, node, snapped)
720+
721+
# ── Phase 2: place staggered parts at final IC positions ──────────
722+
junction_wires = getattr(node, "_tjunction_wires", [])
723+
suppressed_pins = set()
724+
725+
for plan in stagger_plans:
726+
ic_part = plan["ic_part"]
727+
matching = plan["matching"]
728+
ic_dir = plan["ic_dir"]
729+
step_dx = plan["step_dx"]
730+
step_dy = plan["step_dy"]
731+
step_size = plan["step_size"]
732+
perp_dir = perp_map.get(ic_dir, ic_dir)
733+
712734
def _pin_sort_key(entry, _ic_part=ic_part, _ic_dir=ic_dir):
713735
ic_pin = entry[0]
714736
w = ic_pin.pt * _ic_part.tx
@@ -718,7 +740,7 @@ def _pin_sort_key(entry, _ic_part=ic_part, _ic_dir=ic_dir):
718740

719741
matching.sort(key=_pin_sort_key)
720742

721-
parts_per_pin = dominant
743+
parts_per_pin = plan["dominant"]
722744
anti = anti_perp.get(perp_dir, perp_dir)
723745
extend_dirs = [perp_dir, anti] if parts_per_pin >= 2 else [perp_dir]
724746

@@ -747,6 +769,72 @@ def _pin_sort_key(entry, _ic_part=ic_part, _ic_dir=ic_dir):
747769
node._tjunction_suppressed_pins = suppressed_pins
748770

749771

772+
def _pre_shift_ics(plans, node, snapped):
773+
"""Shift ICs vertically BEFORE stagger placement so fans won't overlap.
774+
775+
Collects all parts already snapped to each IC and moves them together.
776+
The stagger parts haven't been placed yet, so they'll naturally land
777+
at the shifted IC positions in phase 2.
778+
"""
779+
for plan in plans:
780+
ic = plan["ic_part"]
781+
ic_deps = set()
782+
ic_id = id(ic)
783+
784+
for part in node.parts:
785+
if id(part) == ic_id or id(part) not in snapped:
786+
continue
787+
if not _is_two_pin_part(part):
788+
continue
789+
for pin in part.pins:
790+
net = getattr(pin, "net", None)
791+
if not net:
792+
continue
793+
for net_pin in net.pins:
794+
if net_pin.part is ic:
795+
ic_deps.add(id(part))
796+
break
797+
if id(part) in ic_deps:
798+
break
799+
800+
plan["_deps"] = [p for p in node.parts if id(p) in ic_deps]
801+
802+
def _ic_bbox(plan):
803+
ic = plan["ic_part"]
804+
all_parts = [ic] + plan["_deps"]
805+
min_y = float("inf")
806+
max_y = float("-inf")
807+
for part in all_parts:
808+
for pin in part.pins:
809+
w = pin.pt * part.tx
810+
min_y = min(min_y, w.y)
811+
max_y = max(max_y, w.y)
812+
return min_y, max_y
813+
814+
plans.sort(key=lambda p: _ic_bbox(p)[0])
815+
816+
margin = 200
817+
prev_max_y = None
818+
819+
for plan in plans:
820+
ic_min_y, ic_max_y = _ic_bbox(plan)
821+
needed_height = plan["stagger_extent"]
822+
group_max_y = max(ic_max_y, ic_min_y + needed_height)
823+
824+
if prev_max_y is not None and ic_min_y < prev_max_y + margin:
825+
shift = (prev_max_y + margin) - ic_min_y
826+
vec = Point(0, shift)
827+
shifted = set()
828+
for part in [plan["ic_part"]] + plan["_deps"]:
829+
if id(part) not in shifted:
830+
part.tx = part.tx.move(vec)
831+
shifted.add(id(part))
832+
ic_min_y += shift
833+
group_max_y += shift
834+
835+
prev_max_y = group_max_y
836+
837+
750838
def _stub_all_non_explicit(circuit):
751839
"""Stub all nets that weren't explicitly set by the user (labels-only fallback).
752840

0 commit comments

Comments
 (0)