@@ -623,13 +623,9 @@ def _snap_two_pin_parts(node):
623623def _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+
750838def _stub_all_non_explicit (circuit ):
751839 """Stub all nets that weren't explicitly set by the user (labels-only fallback).
752840
0 commit comments