Source code for skidl.tools.kicad7.backend

# -*- coding: utf-8 -*-

# The MIT License (MIT) - Copyright (c) Dave Vandenbout.

"""Concrete KiCad-9 implementation of the tool-agnostic SchematicBackend.

Every method here delegates VERBATIM to the existing functions in
``sexp_schematic.py`` — this is a thin adapter so the agnostic decision layer
(``schematics/decisions.py``) can reach the KiCad render geometry and emission
primitives through the interface in ``schematics/backend.py`` without importing
``skidl.tools.kicad9`` directly. The KiCad coordinate math and S-expression
syntax stay in ``sexp_schematic.py``; nothing here changes output.
"""

from skidl.geometry import Point, Tx

from . import sexp_schematic as _ksch


[docs] class Kicad9Backend: """Adapter exposing kicad9 geometry + emission as a SchematicBackend. Interface status (honest): geometry queries (pin_render_pos, pin_render_dir, is_power_net_name, render_xy, label_bbox) are LIVE — consumed by schematics.decisions. Among emission primitives, emit_wire and emit_no_connect ARE on the live path; emit_label/emit_part/ emit_power_symbol/emit_junction are defined for completeness but the renderer still uses the module-level functions for those. solve_snap_tx is DEFERRED (raises; see doc P1b). """ supports_snap = True # ---- GEOMETRY ----
[docs] def pin_render_pos(self, pin, sheet_tx): """KiCad render-mm position of a pin == ``_kicad_pin_pos``.""" return _ksch._kicad_pin_pos(pin, getattr(pin.part, "tx", Tx()), sheet_tx)
[docs] def pin_render_dir(self, pin, sheet_tx): """Render-space pin direction == ``calc_pin_dir``. Note: the present ``calc_pin_dir`` ignores ``sheet_tx`` (see the architecture doc, section 3 / section 6). Folding ``sheet_tx`` in would change output, so this adapter preserves the current behavior exactly. """ return _ksch.calc_pin_dir(pin)
[docs] def is_power_net_name(self, name): return name in _ksch.pwr_symbol_names
[docs] def render_xy(self, lx, ly, part, sheet_tx): """Render-mm of an arbitrary part-local point == ``_render_xy``.""" return _ksch._render_xy(lx, ly, getattr(part, "tx", Tx()), sheet_tx)
[docs] def round_mm(self, val): return _ksch._round_mm(val)
[docs] def solve_snap_tx(self, part, my_pin, target_render_xy, extend_dir, sheet_tx): # DEFERRED (doc P1b) and NOT wired into the snap pipeline, which solves # part.tx in PLACEMENT space via schematics.snap._compute_snap_tx called # directly from snap.py. This interface method can't delegate correctly # anyway (it lacks `other_pin`, which _compute_snap_tx requires), and a # render-space solver here would change output. Raise rather than # silently mis-solve. raise NotImplementedError( "solve_snap_tx is deferred (doc P1b); snap solves in placement space " "via schematics.snap._compute_snap_tx, not through this interface." )
[docs] def label_bbox(self, text): """Rendered net-label box size in mm. Matches the fixed box that ``_deconflict_labels`` uses today (LABEL_W x LABEL_H), so the relocated deconfliction is byte-identical. """ return (10.0, 2.0)
# ---- EMISSION ----
[docs] def emit_wire(self, x1, y1, x2, y2, *, net_name=None, uuid_seed=None): """Build a bare wire Sexp at render-mm coords. Mirrors the inline wire construction in the decision functions; the caller supplies the UUID seed so emitted UUIDs match the originals. """ from simp_sexp import Sexp if uuid_seed is None: uuid_seed = f"wire:{x1}:{y1}:{x2}:{y2}" return Sexp( [ "wire", ["pts", ["xy", x1, y1], ["xy", x2, y2]], ["stroke", ["width", 0], ["type", "default"]], ["uuid", _ksch._gen_uuid(uuid_seed)], ] )
[docs] def emit_label(self, pin, sheet_tx, *, at=None, angle=None, force=False): # `at`/`angle` override placement is part of the interface contract but # not implemented here (the renderer positions labels at the pin via # net_label_to_sexp). Reject explicitly so callers can't assume override # support that isn't present. if at is not None or angle is not None: raise NotImplementedError( "emit_label override placement (at/angle) is not yet supported." ) return _ksch.net_label_to_sexp(pin, tx=sheet_tx, force=force)
[docs] def emit_no_connect(self, x, y, *, uuid_seed=None): from simp_sexp import Sexp if uuid_seed is None: uuid_seed = f"nc:{x}:{y}" return Sexp( [ "no_connect", ["at", _ksch._round_mm(x), _ksch._round_mm(y)], ["uuid", _ksch._gen_uuid(uuid_seed)], ] )
[docs] def emit_power_symbol(self, pin, net_name, sheet_tx): return _ksch._power_symbol_to_sexp(pin, net_name, sheet_tx)
[docs] def emit_part(self, part, sheet_tx, uuid_path): return _ksch.part_to_sexp(part, uuid_path, tx=sheet_tx)
[docs] def apply_label_deconfliction(self, elements, node, sheet_tx): """Read global_label/wire Sexps, run the agnostic deconfliction decision, then mutate label ``at`` coords + append connecting wires. The Sexp reading and mutation (tool-specific) stay here; the overlap detection + nudge-target decision lives in ``schematics.decisions.deconflict_labels``. """ from skidl.schematics import decisions as _decisions GRID = 1.27 def _cell(x, y): return (round(x / GRID), round(y / GRID)) # Build the occupancy seed in element order (label anchors keyed by # net, wire endpoints keyed None) — one pass, matching the original. occupied_seed = [] for elem in elements: if not hasattr(elem, "__getitem__") or len(elem) < 1: continue if elem[0] == "global_label": at = next( (s for s in elem if hasattr(s, "__getitem__") and len(s) and s[0] == "at"), None, ) if at and len(at) >= 3: occupied_seed.append((_cell(float(at[1]), float(at[2])), elem[1])) elif elem[0] == "wire": pts = next( (s for s in elem if hasattr(s, "__getitem__") and len(s) and s[0] == "pts"), None, ) if pts: for xy in pts[1:]: if hasattr(xy, "__getitem__") and len(xy) >= 3 and xy[0] == "xy": occupied_seed.append((_cell(float(xy[1]), float(xy[2])), None)) # Extract label records to move (in element order), with their `at` sexp. labels = [] at_by_idx = {} for i, elem in enumerate(elements): if not (hasattr(elem, "__getitem__") and len(elem) >= 1 and elem[0] == "global_label"): continue at = next( (s for s in elem if hasattr(s, "__getitem__") and len(s) > 0 and s[0] == "at"), None, ) if at is None or len(at) < 4: continue labels.append((i, elem[1], float(at[1]), float(at[2]), int(at[3]))) at_by_idx[i] = at moves, new_wires = _decisions.deconflict_labels( labels, occupied_seed, node, self, sheet_tx ) # Apply moves. for idx, nx, ny in moves: at = at_by_idx[idx] at[1], at[2] = nx, ny # Append connecting wires. from simp_sexp import Sexp for ax, ay, nx, ny in new_wires: elements.append( Sexp( [ "wire", ["pts", ["xy", _ksch._round_mm(ax), _ksch._round_mm(ay)], ["xy", nx, ny]], ["stroke", ["width", 0], ["type", "default"]], ["uuid", _ksch._gen_uuid(f"dcwire:{ax}:{ay}:{nx}:{ny}")], ] ) )
[docs] def emit_junction(self, x, y): # Junctions are emitted per-net via junction_to_sexp today; this # primitive is provided for interface completeness. from simp_sexp import Sexp return Sexp( [ "junction", ["at", _ksch._round_mm(x), _ksch._round_mm(y)], ["diameter", 0], ["color", 0, 0, 0, 0], ["uuid", _ksch._gen_uuid(f"junction:{x}:{y}")], ] )
__all__ = ["Kicad9Backend"]