Improve schematic label readability and eliminate wire crossings
Replace scattered magic numbers with named layout constants (_LABEL_OFST, _NET_LABEL_FONTSIZE, _PARALLEL_PATH_SPACING, etc.) for consistent spacing. Add "input_up" path classification that routes base/gate bias paths to VCC left-then-up with local Vdd symbols, preventing wire crossings with collector/drain vertical paths. Add _choose_label_side() for smart alternating label placement and _sort_parallel_paths() to draw longest chains closest to the device body. Fix PNP/PFET polarity handling: supply terminal stub direction now matches device polarity instead of always drawing upward. Vertical chain label improvements: - valign='bottom' for down-going chains pushes labels above midpoint - Gap wire (0.5 units) before ground/Vdd terminators prevents overlap - Minimum component length (_VERT_CHAIN_MIN_LEN) for readable labels - valign='center' on device body and horizontal-turn labels
This commit is contained in:
parent
c0639c775c
commit
b8b83fd282
@ -71,6 +71,17 @@ class PlacedComponent:
|
||||
GROUND_NAMES = {"0", "gnd"}
|
||||
SUPPLY_NAMES = {"vcc", "vdd", "vee", "vss"}
|
||||
|
||||
# ── Connected Layout Constants ────────────────────────────────
|
||||
# Replaces scattered magic numbers for label offsets, font sizes, and spacing.
|
||||
_LABEL_OFST = 0.5 # general label offset from component body
|
||||
_LABEL_OFST_TIGHT = 0.2 # horizontal single-component paths
|
||||
_NET_LABEL_FONTSIZE = 10 # net/node labels (was 8-9)
|
||||
_DEVICE_LABEL_OFST = 0.4 # active device body label offset
|
||||
_PARALLEL_PATH_SPACING = 4.0 # between parallel vertical paths (was 2.5)
|
||||
_LEAD_STUB_LENGTH = 1.0 # clearance from transistor pin (was 0.75)
|
||||
_INPUT_JUNCTION_LENGTH = 1.5 # base/gate junction wire (was 1.0)
|
||||
_VERT_CHAIN_MIN_LEN = 4.0 # minimum component length in vertical chains
|
||||
|
||||
|
||||
def _is_ground(node: str) -> bool:
|
||||
return node.lower() in GROUND_NAMES
|
||||
@ -524,8 +535,15 @@ def _path_style(
|
||||
supply_term: str,
|
||||
input_term: str,
|
||||
) -> str:
|
||||
"""Classify a path's drawing style: up, down, input, or output."""
|
||||
"""Classify a path's drawing style: up, down, input, input_up, or output.
|
||||
|
||||
``input_up`` routes input-terminal bias paths (base/gate → VCC) left then
|
||||
up with a local Vdd symbol, keeping them on the input side of the
|
||||
schematic and avoiding wire crossings with collector/drain vertical paths.
|
||||
"""
|
||||
if path.end_type == "supply":
|
||||
if term_name == input_term:
|
||||
return "input_up"
|
||||
return "up" if not is_inverted else "down"
|
||||
if term_name == input_term and has_signal and len(path.components) > 1:
|
||||
return "input"
|
||||
@ -638,7 +656,7 @@ def _render_loop(parsed: ParsedNetlist, loop: list[SpiceComponent]) -> str:
|
||||
d.add(
|
||||
_get_element(source, parsed.models)
|
||||
.up()
|
||||
.label(_component_label(source), loc="left", ofst=0.15)
|
||||
.label(_component_label(source), loc="left", ofst=_LABEL_OFST)
|
||||
)
|
||||
d.add(elm.Ground())
|
||||
return d.get_imagedata("svg").decode()
|
||||
@ -647,7 +665,7 @@ def _render_loop(parsed: ParsedNetlist, loop: list[SpiceComponent]) -> str:
|
||||
src = d.add(
|
||||
_get_element(source, parsed.models)
|
||||
.up()
|
||||
.label(_component_label(source), loc="left", ofst=0.15)
|
||||
.label(_component_label(source), loc="left", ofst=_LABEL_OFST)
|
||||
)
|
||||
|
||||
# All components except the last go right across the top
|
||||
@ -655,7 +673,7 @@ def _render_loop(parsed: ParsedNetlist, loop: list[SpiceComponent]) -> str:
|
||||
d.add(
|
||||
_get_element(comp, parsed.models)
|
||||
.right()
|
||||
.label(_component_label(comp), ofst=0.15)
|
||||
.label(_component_label(comp), ofst=_LABEL_OFST)
|
||||
)
|
||||
|
||||
# Last component goes down, ending at the same y-level as the source start
|
||||
@ -664,7 +682,7 @@ def _render_loop(parsed: ParsedNetlist, loop: list[SpiceComponent]) -> str:
|
||||
_get_element(last_comp, parsed.models)
|
||||
.down()
|
||||
.toy(src.start)
|
||||
.label(_component_label(last_comp), loc="right", ofst=0.15)
|
||||
.label(_component_label(last_comp), loc="right", ofst=_LABEL_OFST)
|
||||
)
|
||||
|
||||
# Return wire along the bottom back to source start
|
||||
@ -679,7 +697,7 @@ def _render_loop(parsed: ParsedNetlist, loop: list[SpiceComponent]) -> str:
|
||||
d.add(
|
||||
elm.Dot(open=True)
|
||||
.at(src.end)
|
||||
.label(node_label, loc="left", fontsize=9)
|
||||
.label(node_label, loc="left", fontsize=_NET_LABEL_FONTSIZE)
|
||||
)
|
||||
|
||||
# Node label at junction between top components and the last (shunt) component
|
||||
@ -1009,7 +1027,7 @@ def _render_grid(parsed: ParsedNetlist) -> str:
|
||||
node_ys = [tiers.get(n, _GRID_MID_Y) for n in comp.nodes[:3]]
|
||||
center_y = (max(node_ys) + min(node_ys)) / 2
|
||||
placed_elem = d.add(
|
||||
elem.at((x, center_y)).label(_component_label(comp), loc="right", ofst=0.15)
|
||||
elem.at((x, center_y)).label(_component_label(comp), loc="right", ofst=_LABEL_OFST)
|
||||
)
|
||||
elif len(comp.nodes) >= 2:
|
||||
# 2-terminal: orient vertically between the two node tiers
|
||||
@ -1021,19 +1039,19 @@ def _render_grid(parsed: ParsedNetlist) -> str:
|
||||
# node[0] higher → go down so start=node[0], end=node[1]
|
||||
placed_elem = d.add(
|
||||
elem.at((x, y0)).down().length(length)
|
||||
.label(_component_label(comp), loc="right", ofst=0.15)
|
||||
.label(_component_label(comp), loc="right", ofst=_LABEL_OFST)
|
||||
)
|
||||
else:
|
||||
# node[0] lower → go up so start=node[0], end=node[1]
|
||||
placed_elem = d.add(
|
||||
elem.at((x, y0)).up().length(length)
|
||||
.label(_component_label(comp), loc="right", ofst=0.15)
|
||||
.label(_component_label(comp), loc="right", ofst=_LABEL_OFST)
|
||||
)
|
||||
else:
|
||||
# Fallback: single-node or unusual component
|
||||
placed_elem = d.add(
|
||||
elem.at((x, _GRID_MID_Y)).down().length(_GRID_MIN_COMP_LEN)
|
||||
.label(_component_label(comp), loc="right", ofst=0.15)
|
||||
.label(_component_label(comp), loc="right", ofst=_LABEL_OFST)
|
||||
)
|
||||
|
||||
positions = _get_terminal_positions(placed_elem, comp)
|
||||
@ -1059,7 +1077,7 @@ def _render_grid(parsed: ParsedNetlist) -> str:
|
||||
px, py = positions[0]
|
||||
d.add(
|
||||
elm.Dot(open=True).at((px, py))
|
||||
.label(node, loc="right", fontsize=9)
|
||||
.label(node, loc="right", fontsize=_NET_LABEL_FONTSIZE)
|
||||
)
|
||||
continue
|
||||
|
||||
@ -1073,7 +1091,7 @@ def _render_grid(parsed: ParsedNetlist) -> str:
|
||||
# Label at the midpoint of the wire
|
||||
mx = (positions[0][0] + positions[1][0]) / 2
|
||||
my = (positions[0][1] + positions[1][1]) / 2
|
||||
d.add(elm.Label().at((mx, my)).label(node, loc="right", fontsize=9))
|
||||
d.add(elm.Label().at((mx, my)).label(node, loc="right", fontsize=_NET_LABEL_FONTSIZE))
|
||||
else:
|
||||
# 3+ connections: star wiring with junction dot
|
||||
_draw_star_wires(d, positions)
|
||||
@ -1081,7 +1099,7 @@ def _render_grid(parsed: ParsedNetlist) -> str:
|
||||
ys = sorted(p[1] for p in positions)
|
||||
d.add(
|
||||
elm.Label().at((xs[len(xs) // 2], ys[len(ys) // 2]))
|
||||
.label(node, loc="right", fontsize=9)
|
||||
.label(node, loc="right", fontsize=_NET_LABEL_FONTSIZE)
|
||||
)
|
||||
|
||||
return d.get_imagedata("svg").decode()
|
||||
@ -1096,14 +1114,14 @@ def _label_two_terminal(d, placed, comp: SpiceComponent) -> None:
|
||||
d.add(
|
||||
elm.Label()
|
||||
.at(placed.start)
|
||||
.label(comp.nodes[0], loc="left", fontsize=8)
|
||||
.label(comp.nodes[0], loc="left", fontsize=_NET_LABEL_FONTSIZE)
|
||||
)
|
||||
if len(comp.nodes) >= 2:
|
||||
d.add(elm.Dot(radius=0.08).at(placed.end))
|
||||
d.add(
|
||||
elm.Label()
|
||||
.at(placed.end)
|
||||
.label(comp.nodes[1], loc="right", fontsize=8)
|
||||
.label(comp.nodes[1], loc="right", fontsize=_NET_LABEL_FONTSIZE)
|
||||
)
|
||||
|
||||
|
||||
@ -1129,10 +1147,35 @@ def _label_multiterminal(d, placed, comp: SpiceComponent) -> None:
|
||||
d.add(
|
||||
elm.Label()
|
||||
.at(pos)
|
||||
.label(label_text, fontsize=8)
|
||||
.label(label_text, fontsize=_NET_LABEL_FONTSIZE)
|
||||
)
|
||||
|
||||
|
||||
# ── Connected Layout Helpers ─────────────────────────────────
|
||||
|
||||
|
||||
def _choose_label_side(path_index: int, is_left_of_device: bool) -> str:
|
||||
"""Pick label side so labels fan outward from the circuit center.
|
||||
|
||||
Even-indexed paths (0, 2, 4, ...) get their natural side.
|
||||
Odd-indexed paths (1, 3, 5, ...) get the opposite side.
|
||||
"""
|
||||
natural = "left" if is_left_of_device else "right"
|
||||
opposite = "right" if is_left_of_device else "left"
|
||||
if path_index % 2 == 0:
|
||||
return natural
|
||||
return opposite
|
||||
|
||||
|
||||
def _sort_parallel_paths(paths: list[TerminalPath]) -> list[TerminalPath]:
|
||||
"""Sort parallel paths: longest chains closest to device (index 0).
|
||||
|
||||
Longer chains draw adjacent to the device body so their wires don't
|
||||
cross shorter-path terminators offset further out.
|
||||
"""
|
||||
return sorted(paths, key=lambda p: -len(p.components))
|
||||
|
||||
|
||||
# ── Connected Layout Renderer ────────────────────────────────
|
||||
|
||||
|
||||
@ -1147,20 +1190,31 @@ def _draw_vert_chain(
|
||||
import schemdraw.elements as elm
|
||||
|
||||
direction = "up" if going_up else "down"
|
||||
# Labels push away from terminators: going down → labels above midpoint,
|
||||
# going up → labels below midpoint (default).
|
||||
label_valign = "bottom" if not going_up else None
|
||||
for i, comp in enumerate(components):
|
||||
elem = _get_element(comp, parsed.models)
|
||||
if i == 0 and start is not None:
|
||||
elem = elem.at(start)
|
||||
elem = getattr(elem, direction)()
|
||||
elem = elem.label(_component_label(comp), loc=label_loc, ofst=0.15)
|
||||
elem = getattr(elem, direction)().length(_VERT_CHAIN_MIN_LEN)
|
||||
label_kw: dict = dict(loc=label_loc, ofst=_LABEL_OFST)
|
||||
if label_valign:
|
||||
label_kw["valign"] = label_valign
|
||||
elem = elem.label(_component_label(comp), **label_kw)
|
||||
d.add(elem)
|
||||
|
||||
# Short gap wire before terminators so ground/Vdd symbols don't
|
||||
# overlap the last component's label text.
|
||||
if end_type in ("ground", "supply"):
|
||||
d.add(getattr(elm.Line(), direction)().length(0.5))
|
||||
|
||||
if end_type == "ground":
|
||||
d.add(elm.Ground())
|
||||
elif end_type == "supply":
|
||||
d.add(elm.Vdd().label(end_node.upper()))
|
||||
else:
|
||||
d.add(elm.Dot(open=True).label(end_node, fontsize=9))
|
||||
d.add(elm.Dot(open=True).label(end_node, fontsize=_NET_LABEL_FONTSIZE))
|
||||
|
||||
|
||||
def _draw_horiz_then_down(d, parsed, start, path, going_right):
|
||||
@ -1180,13 +1234,15 @@ def _draw_horiz_then_down(d, parsed, start, path, going_right):
|
||||
# Turn downward at the bend — label faces outward
|
||||
d.push()
|
||||
down_label = "right" if going_right else "left"
|
||||
d.add(elem.down().label(_component_label(comp), loc=down_label, ofst=0.15))
|
||||
d.add(elem.down().label(
|
||||
_component_label(comp), loc=down_label, ofst=_LABEL_OFST, valign="center",
|
||||
))
|
||||
if path.end_type == "ground":
|
||||
d.add(elm.Ground())
|
||||
elif path.end_type == "supply":
|
||||
d.add(elm.Vdd().label(path.end_node.upper()))
|
||||
else:
|
||||
d.add(elm.Dot(open=True).label(path.end_node, fontsize=9))
|
||||
d.add(elm.Dot(open=True).label(path.end_node, fontsize=_NET_LABEL_FONTSIZE))
|
||||
d.pop()
|
||||
# Label the junction node at the bend
|
||||
prev = comps[-2]
|
||||
@ -1195,11 +1251,11 @@ def _draw_horiz_then_down(d, parsed, start, path, going_right):
|
||||
if not _is_ground(node) and not _is_supply(node):
|
||||
loc = "right" if going_right else "left"
|
||||
d.add(
|
||||
elm.Dot(open=True).label(node, loc=loc, fontsize=9)
|
||||
elm.Dot(open=True).label(node, loc=loc, fontsize=_NET_LABEL_FONTSIZE)
|
||||
)
|
||||
break
|
||||
else:
|
||||
d.add(getattr(elem, h_dir)().label(_component_label(comp), ofst=0.15))
|
||||
d.add(getattr(elem, h_dir)().label(_component_label(comp), ofst=_LABEL_OFST_TIGHT))
|
||||
|
||||
# Single-component horizontal path ending at ground/supply/open
|
||||
if len(comps) == 1:
|
||||
@ -1208,7 +1264,7 @@ def _draw_horiz_then_down(d, parsed, start, path, going_right):
|
||||
elif path.end_type == "supply":
|
||||
d.add(elm.Vdd().label(path.end_node.upper()))
|
||||
else:
|
||||
d.add(elm.Dot(open=True).label(path.end_node, fontsize=9))
|
||||
d.add(elm.Dot(open=True).label(path.end_node, fontsize=_NET_LABEL_FONTSIZE))
|
||||
|
||||
|
||||
def _render_connected(parsed: ParsedNetlist, layout: ActiveLayout) -> str:
|
||||
@ -1228,7 +1284,10 @@ def _render_connected(parsed: ParsedNetlist, layout: ActiveLayout) -> str:
|
||||
# (Mims convention: transistor type label beside the symbol)
|
||||
if layout.device_type.startswith("bjt"):
|
||||
dev_elem = elm.BjtPnp() if is_inverted else elm.BjtNpn()
|
||||
q = d.add(dev_elem.label(_component_label(layout.device), loc="right", ofst=0.15))
|
||||
q = d.add(dev_elem.label(
|
||||
_component_label(layout.device), loc="right",
|
||||
ofst=_DEVICE_LABEL_OFST, valign="center",
|
||||
))
|
||||
anchors = {
|
||||
"collector": q.collector,
|
||||
"base": q.base,
|
||||
@ -1238,7 +1297,10 @@ def _render_connected(parsed: ParsedNetlist, layout: ActiveLayout) -> str:
|
||||
input_term = "base"
|
||||
else:
|
||||
dev_elem = elm.PFet() if is_inverted else elm.NFet()
|
||||
q = d.add(dev_elem.label(_component_label(layout.device), loc="right", ofst=0.15))
|
||||
q = d.add(dev_elem.label(
|
||||
_component_label(layout.device), loc="right",
|
||||
ofst=_DEVICE_LABEL_OFST, valign="center",
|
||||
))
|
||||
anchors = {
|
||||
"drain": q.drain,
|
||||
"gate": q.gate,
|
||||
@ -1257,6 +1319,7 @@ def _render_connected(parsed: ParsedNetlist, layout: ActiveLayout) -> str:
|
||||
down_paths: list[TerminalPath] = []
|
||||
input_paths: list[TerminalPath] = []
|
||||
output_paths: list[TerminalPath] = []
|
||||
input_up_paths: list[TerminalPath] = []
|
||||
|
||||
for p in term_paths:
|
||||
has_sig = any(c.name in signal_names for c in p.components)
|
||||
@ -1269,41 +1332,60 @@ def _render_connected(parsed: ParsedNetlist, layout: ActiveLayout) -> str:
|
||||
down_paths.append(p)
|
||||
elif style == "input":
|
||||
input_paths.append(p)
|
||||
elif style == "input_up":
|
||||
input_up_paths.append(p)
|
||||
else:
|
||||
output_paths.append(p)
|
||||
|
||||
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) + len(input_up_paths)
|
||||
)
|
||||
|
||||
# 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"
|
||||
# Mims-style label placement: vertical chain labels default to LEFT
|
||||
# because the transistor body label already sits on the RIGHT.
|
||||
# Parallel offset paths (i > 0) alternate sides to avoid overlap.
|
||||
is_left = True
|
||||
|
||||
# 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:
|
||||
# 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(_INPUT_JUNCTION_LENGTH))
|
||||
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))
|
||||
elif term_name == supply_term and (up_paths or down_paths or output_paths):
|
||||
# Supply terminal stub: direction matches polarity.
|
||||
# NPN/NFET collector→up; PNP/PFET emitter→down.
|
||||
# Longer when output paths also branch so labels clear the wire.
|
||||
stub = _LEAD_STUB_LENGTH * (1.5 if output_paths else 1.0)
|
||||
if is_inverted:
|
||||
lead = d.add(elm.Line().at(anchor).down().length(stub))
|
||||
else:
|
||||
lead = d.add(elm.Line().at(anchor).up().length(stub))
|
||||
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))
|
||||
elif term_name not in (supply_term, input_term) and (up_paths or down_paths):
|
||||
# Ground terminal stub: opposite direction from supply.
|
||||
# NPN/NFET emitter→down; PNP/PFET collector→up.
|
||||
if is_inverted:
|
||||
lead = d.add(elm.Line().at(anchor).up().length(_LEAD_STUB_LENGTH))
|
||||
else:
|
||||
lead = d.add(elm.Line().at(anchor).down().length(_LEAD_STUB_LENGTH))
|
||||
draw_from = lead.end
|
||||
else:
|
||||
draw_from = anchor
|
||||
|
||||
# Sort all path lists: longest chains closest to device
|
||||
up_paths = _sort_parallel_paths(up_paths)
|
||||
down_paths = _sort_parallel_paths(down_paths)
|
||||
output_paths = _sort_parallel_paths(output_paths)
|
||||
input_paths = _sort_parallel_paths(input_paths)
|
||||
|
||||
# Vertical-up paths (toward supply rail)
|
||||
for i, p in enumerate(up_paths):
|
||||
d.push()
|
||||
# Offset parallel paths flip label side to avoid overlap
|
||||
loc = "right" if i > 0 else vert_label
|
||||
loc = _choose_label_side(i, is_left)
|
||||
if i > 0:
|
||||
d.add(elm.Line().at(draw_from).right(2.5 * i))
|
||||
d.add(elm.Line().at(draw_from).right(_PARALLEL_PATH_SPACING * i))
|
||||
_draw_vert_chain(
|
||||
d, parsed, None, p.components, True, p.end_type, p.end_node,
|
||||
label_loc=loc,
|
||||
@ -1318,9 +1400,9 @@ def _render_connected(parsed: ParsedNetlist, layout: ActiveLayout) -> str:
|
||||
# Vertical-down paths (toward ground)
|
||||
for i, p in enumerate(down_paths):
|
||||
d.push()
|
||||
loc = "right" if i > 0 else vert_label
|
||||
loc = _choose_label_side(i, is_left)
|
||||
if i > 0:
|
||||
d.add(elm.Line().at(draw_from).right(2.5 * i))
|
||||
d.add(elm.Line().at(draw_from).right(_PARALLEL_PATH_SPACING * i))
|
||||
_draw_vert_chain(
|
||||
d, parsed, None, p.components, False, p.end_type, p.end_node,
|
||||
label_loc=loc,
|
||||
@ -1332,6 +1414,29 @@ def _render_connected(parsed: ParsedNetlist, layout: ActiveLayout) -> str:
|
||||
)
|
||||
d.pop()
|
||||
|
||||
# Input-up paths: base/gate bias paths to VCC route left then up
|
||||
# with their own local Vdd symbol — avoids crossing collector paths.
|
||||
#
|
||||
# Geometry: draw_from is already _INPUT_JUNCTION_LENGTH left of
|
||||
# the transistor anchor (junction wire drawn above). The offset
|
||||
# below is measured from draw_from, so the total distance from
|
||||
# the transistor is _INPUT_JUNCTION_LENGTH + input_up_base.
|
||||
# When input_paths also go left, add _PARALLEL_PATH_SPACING to
|
||||
# clear the horizontal signal chain (C1→V1, etc.).
|
||||
input_up_base = _INPUT_JUNCTION_LENGTH
|
||||
if input_paths:
|
||||
input_up_base += _PARALLEL_PATH_SPACING
|
||||
input_up_paths = _sort_parallel_paths(input_up_paths)
|
||||
for i, p in enumerate(input_up_paths):
|
||||
d.push()
|
||||
offset = input_up_base + i * _PARALLEL_PATH_SPACING
|
||||
d.add(elm.Line().at(draw_from).left(offset))
|
||||
_draw_vert_chain(
|
||||
d, parsed, None, p.components, True, p.end_type, p.end_node,
|
||||
label_loc="left",
|
||||
)
|
||||
d.pop()
|
||||
|
||||
# Output paths (right then down)
|
||||
for p in output_paths:
|
||||
d.push()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user