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
|
- Click-drag zoom, scroll to pan
|
||||||
- **ngspice backend** -- subprocess execution with binary `.raw` file parsing
|
- **ngspice backend** -- subprocess execution with binary `.raw` file parsing
|
||||||
- **Notebook operations** -- create, open, edit, save, delete, reorder cells, run individually or all at once
|
- **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
|
- **Keyboard shortcuts** -- `Ctrl+S` save, `Shift+Enter` run cell
|
||||||
- **Clean URLs** -- `/notebook/rc-lowpass-filter` via Astro dynamic routes
|
- **Clean URLs** -- `/notebook/rc-lowpass-filter` via Astro dynamic routes
|
||||||
- **15 example notebooks** included:
|
- **15 example notebooks** included:
|
||||||
@ -139,7 +140,9 @@ spicebook/
|
|||||||
│ │ ├── config.py # Settings from environment
|
│ │ ├── config.py # Settings from environment
|
||||||
│ │ ├── engine/ # SPICE engine abstraction
|
│ │ ├── engine/ # SPICE engine abstraction
|
||||||
│ │ │ ├── base.py # Abstract base class
|
│ │ │ ├── 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
|
│ │ ├── models/ # Pydantic schemas
|
||||||
│ │ │ ├── notebook.py # Notebook, Cell, CellOutput
|
│ │ │ ├── notebook.py # Notebook, Cell, CellOutput
|
||||||
│ │ │ └── simulation.py
|
│ │ │ └── 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
|
prefix = comp.prefix
|
||||||
|
|
||||||
if prefix == "R":
|
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()
|
return elm.Resistor()
|
||||||
elif prefix == "C":
|
elif prefix == "C":
|
||||||
return elm.Capacitor()
|
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
|
### Waveforms
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user