From 7e10b87a13ac32a750901aacc941176e5413c218 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Fri, 20 Feb 2026 17:36:50 -0700 Subject: [PATCH] Color-code resistor zigzag symbols with standard 4-band colors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Map physical resistor color bands onto the IEEE zigzag schematic symbol. Each sub-segment is colored to match its band (digit, multiplier, tolerance), with wire-colored entry/exit and a gap before tolerance mimicking real spacing. Parseable values 0.01–1GΩ get color; parametric/out-of-range fall back to mono. --- README.md | 5 +- .../src/spicebook/engine/resistor_colors.py | 163 ++++++++++++++++++ backend/src/spicebook/engine/schematic.py | 5 + frontend/public/llms.txt | 11 ++ 4 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 backend/src/spicebook/engine/resistor_colors.py diff --git a/README.md b/README.md index cee08f0..8191201 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ SpiceBook lets you write a document that says: "here's why this filter works, he - Click-drag zoom, scroll to pan - **ngspice backend** -- subprocess execution with binary `.raw` file parsing - **Notebook operations** -- create, open, edit, save, delete, reorder cells, run individually or all at once +- **Color-coded resistor schematics** -- zigzag symbols colored with standard 4-band resistor color codes. A 10kΩ resistor renders Brown-Black-Orange-Gold directly on the schematic symbol, bridging the gap between physical components and abstract schematics. Falls back to standard monochrome for parametric or out-of-range values. - **Keyboard shortcuts** -- `Ctrl+S` save, `Shift+Enter` run cell - **Clean URLs** -- `/notebook/rc-lowpass-filter` via Astro dynamic routes - **15 example notebooks** included: @@ -139,7 +140,9 @@ spicebook/ │ │ ├── config.py # Settings from environment │ │ ├── engine/ # SPICE engine abstraction │ │ │ ├── base.py # Abstract base class -│ │ │ └── ngspice.py # ngspice subprocess + .raw parser +│ │ │ ├── ngspice.py # ngspice subprocess + .raw parser +│ │ │ ├── schematic.py # Netlist → SVG schematic (SchemDraw) +│ │ │ └── resistor_colors.py # Color-coded zigzag resistors │ │ ├── models/ # Pydantic schemas │ │ │ ├── notebook.py # Notebook, Cell, CellOutput │ │ │ └── simulation.py diff --git a/backend/src/spicebook/engine/resistor_colors.py b/backend/src/spicebook/engine/resistor_colors.py new file mode 100644 index 0000000..58f076c --- /dev/null +++ b/backend/src/spicebook/engine/resistor_colors.py @@ -0,0 +1,163 @@ +"""Resistor color code bands mapped onto zigzag schematic symbols. + +Maps standard 4-band resistor color codes directly onto the IEEE zigzag +(zigzag) symbol segments. Each zigzag sub-segment is colored to match +its corresponding band, teaching the color code while reading schematics. + +Band-to-segment mapping (4-band): + Seg 0 (entry half-rise): wire color (default) + Seg 1 (peak-to-valley): 1st significant digit + Seg 2 (valley-to-peak): 2nd significant digit + Seg 3 (peak-to-valley): multiplier + Seg 4 (valley-to-peak): gap — wire color (mimics physical spacing) + Seg 5 (peak-to-valley): tolerance + Seg 6 (exit half-rise): wire color (default) +""" + +import re + +from schemdraw.elements.twoterm import Element2Term +from schemdraw.segments import Segment + +# ── SPICE Value Parser ────────────────────────────────────── + +_SPICE_SUFFIXES = { + "T": 1e12, + "G": 1e9, + "MEG": 1e6, + "K": 1e3, + "M": 1e6, # For resistors, bare M = Mega (milliohm resistors are vanishingly rare) + "U": 1e-6, + "N": 1e-9, + "P": 1e-12, + "F": 1e-15, +} + +_SUFFIX_RE = re.compile( + r"^([0-9]*\.?[0-9]+)\s*(MEG|[TGKMUNPF])?$", + re.IGNORECASE, +) + + +def parse_spice_value(value_str: str) -> float | None: + """Convert a SPICE value string to numeric ohms. + + Handles: "10k", "4.7MEG", "100", "2.2K", "0.1", "10k ic=0" + Returns None for parametric expressions like "{R_val}". + """ + if not value_str: + return None + + token = value_str.strip().split()[0] + + if token.startswith("{"): + return None + + # Try bare numeric first + try: + return float(token) + except ValueError: + pass + + m = _SUFFIX_RE.match(token) + if m: + number = float(m.group(1)) + suffix = (m.group(2) or "").upper() + multiplier = _SPICE_SUFFIXES.get(suffix, 1.0) + return number * multiplier + + return None + + +# ── Hex Colors Tuned for Mims Background (#f8faf6) ───────── + +BAND_HEX: dict[str, str] = { + "black": "#1a1a1a", + "brown": "#8B4513", + "red": "#CC0000", + "orange": "#E67300", + "yellow": "#B8960B", # Dark goldenrod — visible on off-white + "green": "#006400", + "blue": "#0000CC", + "violet": "#7B00B3", + "gray": "#666666", + "white": "#D0D0D0", # Light gray — visible on off-white + "gold": "#B8860B", + "silver": "#808080", + "pink": "#FF69B4", +} + + +# ── Zigzag Geometry (matches SchemDraw 0.22 ResistorIEEE) ─── + +_RW = 1 / 6 # reswidth +_RH = 0.25 # resheight + +_ZIGZAG_POINTS = [ + (0, 0), + (0.5 * _RW, _RH), + (1.5 * _RW, -_RH), + (2.5 * _RW, _RH), + (3.5 * _RW, -_RH), + (4.5 * _RW, _RH), + (5.5 * _RW, -_RH), + (6 * _RW, 0), +] + + +# ── Custom Element ────────────────────────────────────────── + + +class ColorCodedResistor(Element2Term): + """IEEE zigzag resistor with segments colored as standard color code bands. + + Uses an overlay strategy: segments[0] is the full 8-point zigzag path + (needed by Element2Term._place() for lead extension), then individual + 2-point colored segments are appended at a higher zorder so they render + on top, hiding the base path under the band colors. + """ + + def __init__(self, value_ohms: float, tolerance_pct: float = 5, **kwargs): + super().__init__(**kwargs) + + # segments[0]: full zigzag — required for Element2Term lead geometry + self.segments.append(Segment(_ZIGZAG_POINTS)) + + # Get 4-band color names from SchemDraw's existing logic + from schemdraw.pictorial.pictorial import resistor_colors + + band_names = resistor_colors(value_ohms, tolerance_pct) + + # Map band colors to the 7 zigzag sub-segments + hex_colors = _map_4band(band_names) + + # Add colored overlay segments at higher zorder + for i, hex_color in enumerate(hex_colors): + if hex_color is not None: + self.segments.append( + Segment( + [_ZIGZAG_POINTS[i], _ZIGZAG_POINTS[i + 1]], + color=hex_color, + zorder=3, + ) + ) + + +def _map_4band( + band_names: tuple[str, str, str, str | None], +) -> list[str | None]: + """Map 4-band color names to 7 zigzag sub-segment slots. + + Returns a list of 7 hex color strings (or None for wire-colored segments). + The gap at seg 4 mimics the physical spacing before the tolerance band. + """ + c1, c2, c3, ct = band_names + return [ + None, # seg 0: entry half-rise (wire) + BAND_HEX.get(c1), # seg 1: 1st significant digit + BAND_HEX.get(c2), # seg 2: 2nd significant digit + BAND_HEX.get(c3), # seg 3: multiplier + None, # seg 4: gap (wire — mimics physical spacing) + BAND_HEX.get(ct) if ct else None, # seg 5: tolerance + None, # seg 6: exit half-rise (wire) + ] diff --git a/backend/src/spicebook/engine/schematic.py b/backend/src/spicebook/engine/schematic.py index 2f41ff6..8884bfd 100644 --- a/backend/src/spicebook/engine/schematic.py +++ b/backend/src/spicebook/engine/schematic.py @@ -579,6 +579,11 @@ def _get_element(comp: SpiceComponent, models: dict[str, str]): prefix = comp.prefix if prefix == "R": + from spicebook.engine.resistor_colors import ColorCodedResistor, parse_spice_value + + ohms = parse_spice_value(comp.value) + if ohms is not None and 0.01 <= ohms <= 1e9: + return ColorCodedResistor(ohms) return elm.Resistor() elif prefix == "C": return elm.Capacitor() diff --git a/frontend/public/llms.txt b/frontend/public/llms.txt index f98c904..18cd6f6 100644 --- a/frontend/public/llms.txt +++ b/frontend/public/llms.txt @@ -246,6 +246,17 @@ No request body. Cell must be type `spice`. } ``` +**Color-coded resistors**: Resistors with parseable values between 0.01 and 1e9 ohms render as IEEE zigzag symbols with segments colored according to the standard 4-band resistor color code. A 10k resistor shows Brown-Black-Orange-Gold bands directly on the zigzag. The entry/exit half-segments and the gap before the tolerance band use the default wire color, mimicking the physical spacing that indicates reading direction. Parametric values (e.g. `{R_val}`) and out-of-range resistances fall back to a standard monochrome zigzag. + +Band-to-segment mapping (7 zigzag sub-segments): +- Seg 0 (entry): wire color +- Seg 1: 1st significant digit +- Seg 2: 2nd significant digit +- Seg 3: multiplier +- Seg 4: gap (wire color, mimics physical spacing) +- Seg 5: tolerance +- Seg 6 (exit): wire color + --- ### Waveforms