Improve connected schematic label placement (Mims-style)

- Add lead stub wires (0.75 unit) from collector and emitter pins
  for clearance between transistor and first component
- Transistor label placed right of body, all chain labels on left
- Offset parallel paths (RE/CE) label on right, facing outward
- Wider parallel path spacing (2.5 units) for label breathing room
- Down-turning components label outward based on path direction
- Parameterized label_loc in _draw_vert_chain for context-aware placement
This commit is contained in:
Ryan Malloy 2026-02-13 08:43:11 -07:00
parent 4bc68a58bd
commit c120a179c8

View File

@ -775,8 +775,14 @@ def _label_multiterminal(d, placed, comp: SpiceComponent) -> None:
# ── Connected Layout Renderer ──────────────────────────────── # ── Connected Layout Renderer ────────────────────────────────
def _draw_vert_chain(d, parsed, start, components, going_up, end_type, end_node): def _draw_vert_chain(
"""Draw a chain of components vertically, terminated by Vdd or Ground.""" d, parsed, start, components, going_up, end_type, end_node, label_loc="right"
):
"""Draw a chain of components vertically, terminated by Vdd or Ground.
label_loc controls which side labels appear on ("left" or "right").
Classic drafting rule: labels face outward from the circuit center.
"""
import schemdraw.elements as elm import schemdraw.elements as elm
direction = "up" if going_up else "down" direction = "up" if going_up else "down"
@ -785,7 +791,7 @@ def _draw_vert_chain(d, parsed, start, components, going_up, end_type, end_node)
if i == 0 and start is not None: if i == 0 and start is not None:
elem = elem.at(start) elem = elem.at(start)
elem = getattr(elem, direction)() elem = getattr(elem, direction)()
elem = elem.label(_component_label(comp), loc="left") elem = elem.label(_component_label(comp), loc=label_loc)
d.add(elem) d.add(elem)
if end_type == "ground": if end_type == "ground":
@ -810,9 +816,10 @@ def _draw_horiz_then_down(d, parsed, start, path, going_right):
is_last = i == len(comps) - 1 is_last = i == len(comps) - 1
if is_last and len(comps) > 1: if is_last and len(comps) > 1:
# Turn downward at the bend # Turn downward at the bend — label faces outward
d.push() d.push()
d.add(elem.down().label(_component_label(comp), loc="right")) down_label = "right" if going_right else "left"
d.add(elem.down().label(_component_label(comp), loc=down_label))
if path.end_type == "ground": if path.end_type == "ground":
d.add(elm.Ground()) d.add(elm.Ground())
elif path.end_type == "supply": elif path.end_type == "supply":
@ -856,10 +863,11 @@ def _render_connected(parsed: ParsedNetlist, layout: ActiveLayout) -> str:
signal_names = {s.name for s in layout.signal_sources} signal_names = {s.name for s in layout.signal_sources}
is_inverted = layout.device_type in ("bjt_pnp", "pfet") is_inverted = layout.device_type in ("bjt_pnp", "pfet")
# Place active device at the center # Place active device at the center — label goes right of the body
# (Mims convention: transistor type label beside the symbol)
if layout.device_type.startswith("bjt"): if layout.device_type.startswith("bjt"):
dev_elem = elm.BjtPnp() if is_inverted else elm.BjtNpn() dev_elem = elm.BjtPnp() if is_inverted else elm.BjtNpn()
q = d.add(dev_elem.label(_component_label(layout.device))) q = d.add(dev_elem.label(_component_label(layout.device), loc="right"))
anchors = { anchors = {
"collector": q.collector, "collector": q.collector,
"base": q.base, "base": q.base,
@ -869,7 +877,7 @@ def _render_connected(parsed: ParsedNetlist, layout: ActiveLayout) -> str:
input_term = "base" input_term = "base"
else: else:
dev_elem = elm.PFet() if is_inverted else elm.NFet() dev_elem = elm.PFet() if is_inverted else elm.NFet()
q = d.add(dev_elem.label(_component_label(layout.device))) q = d.add(dev_elem.label(_component_label(layout.device), loc="right"))
anchors = { anchors = {
"drain": q.drain, "drain": q.drain,
"gate": q.gate, "gate": q.gate,
@ -905,38 +913,61 @@ def _render_connected(parsed: ParsedNetlist, layout: ActiveLayout) -> str:
total = len(up_paths) + len(down_paths) + len(input_paths) + len(output_paths) total = len(up_paths) + len(down_paths) + len(input_paths) + len(output_paths)
# Junction wire for input terminal when paths branch # Mims-style label placement: vertical labels go LEFT,
# keeping clear of the VCC/Ground terminators above/below.
# The transistor label sits on the RIGHT, so left is open.
# Offset parallel paths (i > 0) flip to RIGHT to face outward.
vert_label = "left"
# Lead stub wires from collector/emitter create clearance from Q
# (Mims always drew short leads from transistor pins)
if term_name == input_term and total > 1: if term_name == input_term and total > 1:
# Base/gate: junction wire left for bias branching
junc = d.add(elm.Line().at(anchor).left(1)) junc = d.add(elm.Line().at(anchor).left(1))
draw_from = junc.end draw_from = junc.end
elif term_name == supply_term and (up_paths or output_paths):
# Collector/drain: short stub up for clearance
lead = d.add(elm.Line().at(anchor).up().length(0.75))
draw_from = lead.end
elif term_name not in (supply_term, input_term) and (down_paths):
# Emitter/source: short stub down for clearance
lead = d.add(elm.Line().at(anchor).down().length(0.75))
draw_from = lead.end
else: else:
draw_from = anchor draw_from = anchor
# Vertical-up paths (toward supply rail) # Vertical-up paths (toward supply rail)
for i, p in enumerate(up_paths): for i, p in enumerate(up_paths):
d.push() d.push()
# Offset parallel paths flip label side to avoid overlap
loc = "right" if i > 0 else vert_label
if i > 0: if i > 0:
d.add(elm.Line().at(draw_from).right(1.5 * i)) d.add(elm.Line().at(draw_from).right(2.5 * i))
_draw_vert_chain( _draw_vert_chain(
d, parsed, None, p.components, True, p.end_type, p.end_node d, parsed, None, p.components, True, p.end_type, p.end_node,
label_loc=loc,
) )
else: else:
_draw_vert_chain( _draw_vert_chain(
d, parsed, draw_from, p.components, True, p.end_type, p.end_node d, parsed, draw_from, p.components, True, p.end_type, p.end_node,
label_loc=loc,
) )
d.pop() d.pop()
# Vertical-down paths (toward ground) # Vertical-down paths (toward ground)
for i, p in enumerate(down_paths): for i, p in enumerate(down_paths):
d.push() d.push()
loc = "right" if i > 0 else vert_label
if i > 0: if i > 0:
d.add(elm.Line().at(draw_from).right(1.5 * i)) d.add(elm.Line().at(draw_from).right(2.5 * i))
_draw_vert_chain( _draw_vert_chain(
d, parsed, None, p.components, False, p.end_type, p.end_node d, parsed, None, p.components, False, p.end_type, p.end_node,
label_loc=loc,
) )
else: else:
_draw_vert_chain( _draw_vert_chain(
d, parsed, draw_from, p.components, False, p.end_type, p.end_node d, parsed, draw_from, p.components, False, p.end_type, p.end_node,
label_loc=loc,
) )
d.pop() d.pop()