Add Mims-style graph paper background to schematics

Sage-green grid (minor 10pt, major 50pt) on warm off-white canvas,
injected as nested SVG patterns behind all schematic content.
Works across all three renderers (loop, connected, grid).
This commit is contained in:
Ryan Malloy 2026-02-13 09:14:52 -07:00
parent c120a179c8
commit 4f174e9d0f

View File

@ -990,7 +990,7 @@ def _render_connected(parsed: ParsedNetlist, layout: ActiveLayout) -> str:
return d.get_imagedata("svg").decode()
# ── SVG Annotation ────────────────────────────────────────────
# ── SVG Post-Processing ───────────────────────────────────────
# Prefixes whose values are numeric/SI-unit and make sense to edit inline
_EDITABLE_PREFIXES = {"R", "C", "L", "V", "I", "E", "G", "F", "H", "B", "S"}
@ -998,6 +998,94 @@ _EDITABLE_PREFIXES = {"R", "C", "L", "V", "I", "E", "G", "F", "H", "B", "S"}
# SVG namespace
_SVG_NS = "http://www.w3.org/2000/svg"
# Graph paper grid constants (fixed spacing like real engineering paper)
_GRID_MINOR = 10 # minor grid cell size in SVG user units (pt)
_GRID_MAJOR = 50 # major grid cell = 5× minor
_GRID_BG = "#f8faf6" # warm off-white canvas
_GRID_MINOR_CLR = "#d4e4d4" # light sage green
_GRID_MAJOR_CLR = "#b0ccb0" # medium sage green
def _add_graph_paper_bg(svg_str: str, pad: float = 15.0) -> str:
"""Add Mims-style graph paper background behind schematic content.
Injects a subtle green grid pattern (minor 10pt, major 50pt) on a
warm off-white canvas evoking Forrest Mims' hand-drawn engineering
notebook aesthetic. Adds *pad* units of margin around the existing
viewBox so the paper extends beyond the circuit.
"""
ET.register_namespace("", _SVG_NS)
try:
root = ET.fromstring(svg_str)
except ET.ParseError:
logger.warning("Failed to parse SVG for graph paper background")
return svg_str
# ── Determine canvas bounds ──────────────────────────────
viewbox = root.get("viewBox")
if viewbox:
vb_x, vb_y, vb_w, vb_h = (float(v) for v in viewbox.split())
else:
vb_x, vb_y = 0.0, 0.0
vb_w = float(re.sub(r"[a-z]+$", "", root.get("width", "600")))
vb_h = float(re.sub(r"[a-z]+$", "", root.get("height", "400")))
# Expand viewBox by padding for a "drawn on paper" margin
vb_x -= pad
vb_y -= pad
vb_w += 2 * pad
vb_h += 2 * pad
root.set("viewBox", f"{vb_x} {vb_y} {vb_w} {vb_h}")
# ── Build <defs> with nested grid patterns ───────────────
ns = f"{{{_SVG_NS}}}"
defs = ET.Element(f"{ns}defs")
# Minor grid: thin sage lines every 10 units
minor_pat = ET.SubElement(defs, f"{ns}pattern", {
"id": "sb-grid-minor",
"width": str(_GRID_MINOR), "height": str(_GRID_MINOR),
"patternUnits": "userSpaceOnUse",
})
ET.SubElement(minor_pat, f"{ns}path", {
"d": f"M {_GRID_MINOR} 0 L 0 0 0 {_GRID_MINOR}",
"fill": "none", "stroke": _GRID_MINOR_CLR, "stroke-width": "0.3",
})
# Major grid: contains minor grid + thicker lines every 50 units
major_pat = ET.SubElement(defs, f"{ns}pattern", {
"id": "sb-grid-major",
"width": str(_GRID_MAJOR), "height": str(_GRID_MAJOR),
"patternUnits": "userSpaceOnUse",
})
ET.SubElement(major_pat, f"{ns}rect", {
"width": str(_GRID_MAJOR), "height": str(_GRID_MAJOR),
"fill": "url(#sb-grid-minor)",
})
ET.SubElement(major_pat, f"{ns}path", {
"d": f"M {_GRID_MAJOR} 0 L 0 0 0 {_GRID_MAJOR}",
"fill": "none", "stroke": _GRID_MAJOR_CLR, "stroke-width": "0.6",
})
# ── Background rects (behind all schematic content) ──────
bg_rect = ET.Element(f"{ns}rect", {
"x": str(vb_x), "y": str(vb_y),
"width": str(vb_w), "height": str(vb_h),
"fill": _GRID_BG,
})
grid_rect = ET.Element(f"{ns}rect", {
"x": str(vb_x), "y": str(vb_y),
"width": str(vb_w), "height": str(vb_h),
"fill": "url(#sb-grid-major)",
})
# Insert at front: defs first, then bg, then grid overlay
root.insert(0, grid_rect)
root.insert(0, bg_rect)
root.insert(0, defs)
return ET.tostring(root, encoding="unicode")
def annotate_svg(svg_str: str, parsed: ParsedNetlist) -> str:
"""Add data attributes to SVG text elements for interactive editing.
@ -1124,7 +1212,8 @@ def netlist_to_svg(netlist_text: str) -> SchematicResult:
if svg is None:
svg = _render_grid(parsed)
# Post-process: annotate SVG with data attributes for interactivity
# Post-process: graph paper background, then data attributes
svg = _add_graph_paper_bg(svg)
svg = annotate_svg(svg, parsed)
component_map = build_component_map(parsed)