diff --git a/backend/src/spicebook/engine/schematic.py b/backend/src/spicebook/engine/schematic.py index 0603bb1..bcf0fb8 100644 --- a/backend/src/spicebook/engine/schematic.py +++ b/backend/src/spicebook/engine/schematic.py @@ -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 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)