From c120a179c83964a571b5d7df0aaac5eec93fe9e4 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Fri, 13 Feb 2026 08:43:11 -0700 Subject: [PATCH] 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 --- backend/src/spicebook/engine/schematic.py | 61 +++++++++++++++++------ 1 file changed, 46 insertions(+), 15 deletions(-) diff --git a/backend/src/spicebook/engine/schematic.py b/backend/src/spicebook/engine/schematic.py index 418edb6..0603bb1 100644 --- a/backend/src/spicebook/engine/schematic.py +++ b/backend/src/spicebook/engine/schematic.py @@ -775,8 +775,14 @@ def _label_multiterminal(d, placed, comp: SpiceComponent) -> None: # ── Connected Layout Renderer ──────────────────────────────── -def _draw_vert_chain(d, parsed, start, components, going_up, end_type, end_node): - """Draw a chain of components vertically, terminated by Vdd or Ground.""" +def _draw_vert_chain( + 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 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: elem = elem.at(start) elem = getattr(elem, direction)() - elem = elem.label(_component_label(comp), loc="left") + elem = elem.label(_component_label(comp), loc=label_loc) d.add(elem) 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 if is_last and len(comps) > 1: - # Turn downward at the bend + # Turn downward at the bend — label faces outward 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": d.add(elm.Ground()) 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} 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"): 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 = { "collector": q.collector, "base": q.base, @@ -869,7 +877,7 @@ 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))) + q = d.add(dev_elem.label(_component_label(layout.device), loc="right")) anchors = { "drain": q.drain, "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) - # 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: + # Base/gate: junction wire left for bias branching junc = d.add(elm.Line().at(anchor).left(1)) 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: draw_from = anchor # 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 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( - 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: _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() # Vertical-down paths (toward ground) for i, p in enumerate(down_paths): d.push() + loc = "right" if i > 0 else vert_label 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( - 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: _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()