Add SES writer helpers: indent writer and routing-result model

Ports two FreeRouting building blocks for session output:

- indent.py: IndentWriter (2-space indented S-expression output) and
  Identifier (reserved-char/non-ASCII/leading-digit quoting), mirroring
  datastructures/IndentFileWriter and IdentifierType. The SES reserved
  set includes '-' and '_', so net and padstack names get quoted.
- model.py: RoutedWire/RoutedVia/RoutedNet/RoutingResult — the minimal
  in-memory routing structure the maze router will populate and the SES
  writer serializes. Coordinates are in DSN units.
This commit is contained in:
2026-07-11 16:34:09 -06:00
parent 3bf4f6bed2
commit 650c732a30
2 changed files with 183 additions and 0 deletions
+80
View File
@@ -0,0 +1,80 @@
"""In-memory routing result — the input the SES writer serializes.
The maze router (a later stage) populates a :class:`RoutingResult`; the SES
writer turns it into the ``(routes (network_out ...))`` scope. Coordinates and
widths are in DSN units (the same units the DSN was parsed in); the writer
rounds them to integers, matching FreeRouting's ``SesWriter`` which emits
integer session coordinates.
"""
from __future__ import annotations
from dataclasses import dataclass, field
__all__ = ["RoutedWire", "RoutedVia", "RoutedNet", "RoutingResult"]
@dataclass
class RoutedWire:
"""A routed trace segment on one layer.
``coords`` is a flat ``[x1, y1, x2, y2, ...]`` list (DSN units); ``width`` is
the full trace width (DSN units). ``fixed`` maps to an emitted
``(type fix)`` marker when set.
"""
layer: str
width: float
coords: list[float]
fixed: bool = False
@dataclass
class RoutedVia:
"""A routed via at ``(x, y)`` (DSN units) using a named padstack."""
padstack: str
x: float
y: float
fixed: bool = False
@dataclass
class RoutedNet:
"""The routed wires and vias belonging to one net."""
name: str
wires: list[RoutedWire] = field(default_factory=list)
vias: list[RoutedVia] = field(default_factory=list)
@property
def has_items(self) -> bool:
return bool(self.wires or self.vias)
@dataclass
class RoutingResult:
"""The complete routing output for a board, grouped by net."""
nets: list[RoutedNet] = field(default_factory=list)
def net(self, name: str, *, create: bool = False) -> RoutedNet | None:
"""Return the :class:`RoutedNet` with ``name`` (creating it if asked)."""
for n in self.nets:
if n.name == name:
return n
if create:
n = RoutedNet(name=name)
self.nets.append(n)
return n
return None
def add_wire(self, net_name: str, wire: RoutedWire) -> None:
self.net(net_name, create=True).wires.append(wire)
def add_via(self, net_name: str, via: RoutedVia) -> None:
self.net(net_name, create=True).vias.append(via)
def routed_nets(self) -> list[RoutedNet]:
"""Nets that actually carry routed geometry (in insertion order)."""
return [n for n in self.nets if n.has_items]