Merge feature/color-coded-resistors: color-coded zigzag resistor symbols
This commit is contained in:
commit
65e7dbe68d
@ -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
|
||||
|
||||
163
backend/src/spicebook/engine/resistor_colors.py
Normal file
163
backend/src/spicebook/engine/resistor_colors.py
Normal file
@ -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)
|
||||
]
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user