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"}
|
GROUND_NAMES = {"0", "gnd"}
|
||||||
SUPPLY_NAMES = {"vcc", "vdd", "vee", "vss"}
|
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:
|
def _is_ground(node: str) -> bool:
|
||||||
return node.lower() in GROUND_NAMES
|
return node.lower() in GROUND_NAMES
|
||||||
@ -524,8 +535,15 @@ def _path_style(
|
|||||||
supply_term: str,
|
supply_term: str,
|
||||||
input_term: str,
|
input_term: str,
|
||||||
) -> 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 path.end_type == "supply":
|
||||||
|
if term_name == input_term:
|
||||||
|
return "input_up"
|
||||||
return "up" if not is_inverted else "down"
|
return "up" if not is_inverted else "down"
|
||||||
if term_name == input_term and has_signal and len(path.components) > 1:
|
if term_name == input_term and has_signal and len(path.components) > 1:
|
||||||
return "input"
|
return "input"
|
||||||
@ -638,7 +656,7 @@ def _render_loop(parsed: ParsedNetlist, loop: list[SpiceComponent]) -> str:
|
|||||||
d.add(
|
d.add(
|
||||||
_get_element(source, parsed.models)
|
_get_element(source, parsed.models)
|
||||||
.up()
|
.up()
|
||||||
.label(_component_label(source), loc="left", ofst=0.15)
|
.label(_component_label(source), loc="left", ofst=_LABEL_OFST)
|
||||||
)
|
)
|
||||||
d.add(elm.Ground())
|
d.add(elm.Ground())
|
||||||
return d.get_imagedata("svg").decode()
|
return d.get_imagedata("svg").decode()
|
||||||
@ -647,7 +665,7 @@ def _render_loop(parsed: ParsedNetlist, loop: list[SpiceComponent]) -> str:
|
|||||||
src = d.add(
|
src = d.add(
|
||||||
_get_element(source, parsed.models)
|
_get_element(source, parsed.models)
|
||||||
.up()
|
.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
|
# 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(
|
d.add(
|
||||||
_get_element(comp, parsed.models)
|
_get_element(comp, parsed.models)
|
||||||
.right()
|
.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
|
# 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)
|
_get_element(last_comp, parsed.models)
|
||||||
.down()
|
.down()
|
||||||
.toy(src.start)
|
.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
|
# Return wire along the bottom back to source start
|
||||||
@ -679,7 +697,7 @@ def _render_loop(parsed: ParsedNetlist, loop: list[SpiceComponent]) -> str:
|
|||||||
d.add(
|
d.add(
|
||||||
elm.Dot(open=True)
|
elm.Dot(open=True)
|
||||||
.at(src.end)
|
.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
|
# 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]]
|
node_ys = [tiers.get(n, _GRID_MID_Y) for n in comp.nodes[:3]]
|
||||||
center_y = (max(node_ys) + min(node_ys)) / 2
|
center_y = (max(node_ys) + min(node_ys)) / 2
|
||||||
placed_elem = d.add(
|
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:
|
elif len(comp.nodes) >= 2:
|
||||||
# 2-terminal: orient vertically between the two node tiers
|
# 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]
|
# node[0] higher → go down so start=node[0], end=node[1]
|
||||||
placed_elem = d.add(
|
placed_elem = d.add(
|
||||||
elem.at((x, y0)).down().length(length)
|
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:
|
else:
|
||||||
# node[0] lower → go up so start=node[0], end=node[1]
|
# node[0] lower → go up so start=node[0], end=node[1]
|
||||||
placed_elem = d.add(
|
placed_elem = d.add(
|
||||||
elem.at((x, y0)).up().length(length)
|
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:
|
else:
|
||||||
# Fallback: single-node or unusual component
|
# Fallback: single-node or unusual component
|
||||||
placed_elem = d.add(
|
placed_elem = d.add(
|
||||||
elem.at((x, _GRID_MID_Y)).down().length(_GRID_MIN_COMP_LEN)
|
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)
|
positions = _get_terminal_positions(placed_elem, comp)
|
||||||
@ -1059,7 +1077,7 @@ def _render_grid(parsed: ParsedNetlist) -> str:
|
|||||||
px, py = positions[0]
|
px, py = positions[0]
|
||||||
d.add(
|
d.add(
|
||||||
elm.Dot(open=True).at((px, py))
|
elm.Dot(open=True).at((px, py))
|
||||||
.label(node, loc="right", fontsize=9)
|
.label(node, loc="right", fontsize=_NET_LABEL_FONTSIZE)
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -1073,7 +1091,7 @@ def _render_grid(parsed: ParsedNetlist) -> str:
|
|||||||
# Label at the midpoint of the wire
|
# Label at the midpoint of the wire
|
||||||
mx = (positions[0][0] + positions[1][0]) / 2
|
mx = (positions[0][0] + positions[1][0]) / 2
|
||||||
my = (positions[0][1] + positions[1][1]) / 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:
|
else:
|
||||||
# 3+ connections: star wiring with junction dot
|
# 3+ connections: star wiring with junction dot
|
||||||
_draw_star_wires(d, positions)
|
_draw_star_wires(d, positions)
|
||||||
@ -1081,7 +1099,7 @@ def _render_grid(parsed: ParsedNetlist) -> str:
|
|||||||
ys = sorted(p[1] for p in positions)
|
ys = sorted(p[1] for p in positions)
|
||||||
d.add(
|
d.add(
|
||||||
elm.Label().at((xs[len(xs) // 2], ys[len(ys) // 2]))
|
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()
|
return d.get_imagedata("svg").decode()
|
||||||
@ -1096,14 +1114,14 @@ def _label_two_terminal(d, placed, comp: SpiceComponent) -> None:
|
|||||||
d.add(
|
d.add(
|
||||||
elm.Label()
|
elm.Label()
|
||||||
.at(placed.start)
|
.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:
|
if len(comp.nodes) >= 2:
|
||||||
d.add(elm.Dot(radius=0.08).at(placed.end))
|
d.add(elm.Dot(radius=0.08).at(placed.end))
|
||||||
d.add(
|
d.add(
|
||||||
elm.Label()
|
elm.Label()
|
||||||
.at(placed.end)
|
.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(
|
d.add(
|
||||||
elm.Label()
|
elm.Label()
|
||||||
.at(pos)
|
.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 ────────────────────────────────
|
# ── Connected Layout Renderer ────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
@ -1147,20 +1190,31 @@ def _draw_vert_chain(
|
|||||||
import schemdraw.elements as elm
|
import schemdraw.elements as elm
|
||||||
|
|
||||||
direction = "up" if going_up else "down"
|
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):
|
for i, comp in enumerate(components):
|
||||||
elem = _get_element(comp, parsed.models)
|
elem = _get_element(comp, parsed.models)
|
||||||
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)().length(_VERT_CHAIN_MIN_LEN)
|
||||||
elem = elem.label(_component_label(comp), loc=label_loc, ofst=0.15)
|
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)
|
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":
|
if end_type == "ground":
|
||||||
d.add(elm.Ground())
|
d.add(elm.Ground())
|
||||||
elif end_type == "supply":
|
elif end_type == "supply":
|
||||||
d.add(elm.Vdd().label(end_node.upper()))
|
d.add(elm.Vdd().label(end_node.upper()))
|
||||||
else:
|
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):
|
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
|
# Turn downward at the bend — label faces outward
|
||||||
d.push()
|
d.push()
|
||||||
down_label = "right" if going_right else "left"
|
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":
|
if path.end_type == "ground":
|
||||||
d.add(elm.Ground())
|
d.add(elm.Ground())
|
||||||
elif path.end_type == "supply":
|
elif path.end_type == "supply":
|
||||||
d.add(elm.Vdd().label(path.end_node.upper()))
|
d.add(elm.Vdd().label(path.end_node.upper()))
|
||||||
else:
|
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()
|
d.pop()
|
||||||
# Label the junction node at the bend
|
# Label the junction node at the bend
|
||||||
prev = comps[-2]
|
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):
|
if not _is_ground(node) and not _is_supply(node):
|
||||||
loc = "right" if going_right else "left"
|
loc = "right" if going_right else "left"
|
||||||
d.add(
|
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
|
break
|
||||||
else:
|
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
|
# Single-component horizontal path ending at ground/supply/open
|
||||||
if len(comps) == 1:
|
if len(comps) == 1:
|
||||||
@ -1208,7 +1264,7 @@ def _draw_horiz_then_down(d, parsed, start, path, going_right):
|
|||||||
elif path.end_type == "supply":
|
elif path.end_type == "supply":
|
||||||
d.add(elm.Vdd().label(path.end_node.upper()))
|
d.add(elm.Vdd().label(path.end_node.upper()))
|
||||||
else:
|
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:
|
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)
|
# (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), loc="right", ofst=0.15))
|
q = d.add(dev_elem.label(
|
||||||
|
_component_label(layout.device), loc="right",
|
||||||
|
ofst=_DEVICE_LABEL_OFST, valign="center",
|
||||||
|
))
|
||||||
anchors = {
|
anchors = {
|
||||||
"collector": q.collector,
|
"collector": q.collector,
|
||||||
"base": q.base,
|
"base": q.base,
|
||||||
@ -1238,7 +1297,10 @@ 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), loc="right", ofst=0.15))
|
q = d.add(dev_elem.label(
|
||||||
|
_component_label(layout.device), loc="right",
|
||||||
|
ofst=_DEVICE_LABEL_OFST, valign="center",
|
||||||
|
))
|
||||||
anchors = {
|
anchors = {
|
||||||
"drain": q.drain,
|
"drain": q.drain,
|
||||||
"gate": q.gate,
|
"gate": q.gate,
|
||||||
@ -1257,6 +1319,7 @@ def _render_connected(parsed: ParsedNetlist, layout: ActiveLayout) -> str:
|
|||||||
down_paths: list[TerminalPath] = []
|
down_paths: list[TerminalPath] = []
|
||||||
input_paths: list[TerminalPath] = []
|
input_paths: list[TerminalPath] = []
|
||||||
output_paths: list[TerminalPath] = []
|
output_paths: list[TerminalPath] = []
|
||||||
|
input_up_paths: list[TerminalPath] = []
|
||||||
|
|
||||||
for p in term_paths:
|
for p in term_paths:
|
||||||
has_sig = any(c.name in signal_names for c in p.components)
|
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)
|
down_paths.append(p)
|
||||||
elif style == "input":
|
elif style == "input":
|
||||||
input_paths.append(p)
|
input_paths.append(p)
|
||||||
|
elif style == "input_up":
|
||||||
|
input_up_paths.append(p)
|
||||||
else:
|
else:
|
||||||
output_paths.append(p)
|
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,
|
# Mims-style label placement: vertical chain labels default to LEFT
|
||||||
# keeping clear of the VCC/Ground terminators above/below.
|
# because the transistor body label already sits on the RIGHT.
|
||||||
# The transistor label sits on the RIGHT, so left is open.
|
# Parallel offset paths (i > 0) alternate sides to avoid overlap.
|
||||||
# Offset parallel paths (i > 0) flip to RIGHT to face outward.
|
is_left = True
|
||||||
vert_label = "left"
|
|
||||||
|
|
||||||
# Lead stub wires from collector/emitter create clearance from Q
|
# Lead stub wires from collector/emitter create clearance from Q
|
||||||
# (Mims always drew short leads from transistor pins)
|
# (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
|
# 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
|
draw_from = junc.end
|
||||||
elif term_name == supply_term and (up_paths or output_paths):
|
elif term_name == supply_term and (up_paths or down_paths or output_paths):
|
||||||
# Collector/drain: short stub up for clearance
|
# Supply terminal stub: direction matches polarity.
|
||||||
lead = d.add(elm.Line().at(anchor).up().length(0.75))
|
# 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
|
draw_from = lead.end
|
||||||
elif term_name not in (supply_term, input_term) and (down_paths):
|
elif term_name not in (supply_term, input_term) and (up_paths or down_paths):
|
||||||
# Emitter/source: short stub down for clearance
|
# Ground terminal stub: opposite direction from supply.
|
||||||
lead = d.add(elm.Line().at(anchor).down().length(0.75))
|
# 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
|
draw_from = lead.end
|
||||||
else:
|
else:
|
||||||
draw_from = anchor
|
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)
|
# 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 = _choose_label_side(i, is_left)
|
||||||
loc = "right" if i > 0 else vert_label
|
|
||||||
if i > 0:
|
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(
|
_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,
|
label_loc=loc,
|
||||||
@ -1318,9 +1400,9 @@ def _render_connected(parsed: ParsedNetlist, layout: ActiveLayout) -> str:
|
|||||||
# 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
|
loc = _choose_label_side(i, is_left)
|
||||||
if i > 0:
|
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(
|
_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,
|
label_loc=loc,
|
||||||
@ -1332,6 +1414,29 @@ def _render_connected(parsed: ParsedNetlist, layout: ActiveLayout) -> str:
|
|||||||
)
|
)
|
||||||
d.pop()
|
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)
|
# Output paths (right then down)
|
||||||
for p in output_paths:
|
for p in output_paths:
|
||||||
d.push()
|
d.push()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user