The grid MVP was the only track reachable from the command line, so the
continuous room engine, channel packing, shove, and 45-degree routing were
unusable by anything shelling out to freeroute.
Add --engine {grid,exact,room} (default grid, unchanged behaviour) plus
--pack / --shove / --diagonal, and honour -mp on the tracks that implement
rip-up passes. Not every engine supports every pass; ENGINE_OPTIONS is the
matrix and an unsupported combination exits 2 with a message naming the
engines that do support the flag, rather than silently no-opping.
Add diagonal_win.dsn: a 45-degree pin-to-pin net with a keepout placed inside the
diagonal's bounding box but clear of the true thin copper. Orthogonal routing
connects it with a Manhattan-length staircase (1600000); diagonal routing
replaces that with a single 45-degree trace at the ideal hypotenuse length
(1131371) -- a 29% reduction -- DRC-clean via the search tree and via an
independent reconstruction of the emitted 45-degree copper's clearance octagon.
test_exact_clearance_is_load_bearing shows the keepout overlaps the coarse
bounding box but not the exact octagon, so the exact clearance (not the box) is
what lets the diagonal route. Determinism, endpoints-on-pads, valid SES, and
diagonal-off-equals-orthogonal (additive, over four fixtures) are covered.
Add diagonal=True to the exact router: after orthogonal routing, each 2-pin net
is retried as the shortest exactly-clean octilinear (0/45/90/135-degree) trace.
A dropped net is recovered; an orthogonal route is replaced only when the
diagonal is strictly shorter. Candidates are the direct 45-degree segment or the
two diagonal-plus-axis two-benders; each segment's diagonal copper is covered by
the exact octagon and its clearance checked with clearance_conflict_shape, so a
trace fits a diagonal corridor the bounding-box cover would reject.
Endpoints stay on the pads; diagonal copper is stored with its exact octagon so
later nets clear it precisely; same-net copper is ignored during the check so the
candidate is verified against every other net before the orthogonal copper is
ripped. With diagonal=False the pass is skipped and output is byte-for-byte the
orthogonal router. Threaded through route_dsn_board_exact /
build_exact_routing_result / route(engine="exact").
Add optional exact/seg fields to TreeShape for a diagonal trace tile: tile holds
the bounding box for broad-phase bucketing, exact holds the tight octagon copper,
and seg=(a,b,half_width) rebuilds the clearance region on demand. Every
orthogonal tile leaves both None and takes the identical box code path, so the
box-only tree stays byte-for-byte unchanged (whole suite green).
clearance_conflict now tests a stored diagonal tile with an exact 2-D overlap;
clearance_conflict_shape checks a diagonal trace's clearance octagon against
stored copper; has_violation runs the exact octagon test whenever either tile is
diagonal and the plain box overlap otherwise. All exact-integer via Simplex
intersection.
A diagonal segment's copper has no axis-aligned exact tile, so segment_box falls
back to the bounding box -- a huge conservative cover that makes diagonal traces
conflict with everything. segment_octagon builds the tight cover instead: the
copper of a 0/45/90/135-degree segment swept by a radius, as a convex integer
Simplex bounded by the four axis and four x+y / x-y diagonal half-planes.
Diagonal bounds widen by ceil_sqrt2(radius) -- the exact integer ceiling of
radius*sqrt(2) via math.isqrt, no float -- rounded outward, so the octagon is a
provable superset of segment (+) disk(radius) (verified over 20k sampled copper
points). Growing by clearance is just a larger radius, giving both the stored
copper tile and the queried clearance region. overlaps_2d tests a real 2-D
overlap via exact Simplex intersection, accepting mixed IntBox/Simplex tiles.
Add channel_pack.dsn: three nets whose pads spread wider than a wall gap, so they
must converge and pack into lanes. Plain room routing (shove on or off) drops >= 1
net under every net ordering; the packer connects all three under every ordering,
DRC-clean (search tree and independent emitted-copper check).
The falsifiable boundary test sweeps the gap width for N=3 and N=4 and asserts the
packer connects all N iff the gap meets the geometric feasibility width
(N-1)*(width+clearance) + 2*(half_width+clearance), never routes fewer than greedy
below it, and never false-packs. Determinism, endpoints-on-pads, valid SES, and
pack=False-equals-default (additive) are covered.
Greedy per-net room routing sends each net through a shared obstacle gap on its
own independently chosen path; the paths collide and some nets drop even when the
gap is physically wide enough for all of them. shove cannot fix this -- it
relocates blockers into existing space, it does not pack lanes.
Add an opt-in pre-pass (pack=True, independent of shove) that groups 2-pin nets
which must cross a common gap, assigns each a parallel lane across the gap's
usable width (pad-ordered so lanes never cross, spaced >= width + clearance) with
staggered fan-in trunks so the converge/diverge jogs never overlap, and realizes
each as an orthogonal trace verified exactly DRC-clean. Packing is all-or-nothing
per group, so it never leaves a channel worse than greedy. With pack=False the
pre-pass is skipped and the output is unchanged.
Scope: single-layer 2-pin nets through one axis-aligned gap, with enough fan
depth to stack the trunks. Threaded through route_dsn_board_rooms /
build_rooms_routing_result / route(engine="room").
Add true_density.dsn: NET_C is a full-height wall on the sole signal layer,
so plain room routing drops a net under every net ordering (an order-independent
obstruction, verified over all permutations), unlike rooms_shove_channel whose
plain drop fits under a reorder.
Test that shove recovers nothing on this board (shoves == 0, same routed set as
plain) while staying DRC-clean, and that the only difference from the recoverable
shove_channel is the wall's span. shove relocates a blocker into existing free
space; where the wall leaves no headroom there is nowhere to relocate to, so it
cannot manufacture density. The recover/fail boundary is the free-space boundary.
_shove_recover_room clobbered a net's _Accepted with a fresh one whose
wire_owners held only the just-recovered wire, while its wires list (the
shared result.wires[net_no]) already held earlier committed connections.
That left wire_owners shorter than the parallel wires list, so a later
net's shove either removed the wrong wire's copper from the search tree
(a silent different-net short: copper stayed in the emitted SES but
vanished from the tree, and has_violation falsely certified it clean) or
raised IndexError and aborted the whole route.
Reuse the net's existing _Accepted on recovery so wire_owners stays
parallel to wires. Fixes both the silent DRC violation and the crash
sibling without touching the DRC-clean or determinism invariants.
Also correct the overstated "density win" framing of the shove_channel
fixture: its plain-routing net drop is an artifact of greedy net order,
not a density limit (the same board routes both nets with zero shoves
under a reorder or with the cosmetic seals removed). Reframed the test
and docstring; the genuine sub-cell win remains the wide_door occupancy
packing (shoves == 0).
Regression tests assert the invariant against the emitted output
directly (reconstructed copper tiles), not just the tree that the bug
fooled, plus determinism, no-crash, and the ordering-artifact bound.
rooms_shove_channel is the headline density win: a sealed NET_A wall
leaves no top<->bottom door, so plain room routing drops NET_C; with
shove=True NET_A is nudged aside and both nets route, DRC-clean and
deterministic. rooms_wide_door seats two nets through one wide gap at
distinct projected gate points with no shove.
Cover the occupancy-aware projection math directly, assert the four
foundation fixtures stay byte-identical under shove=True (shoves==0), and
extend the room DRC-clean parametrize with both new fixtures.
Both upgrades sit behind an opt-in shove=False flag, so the default code
path is byte-for-byte the foundation router (existing room tests are
unchanged by construction).
With shove=True:
- Gate placement becomes occupancy-aware: each edge gate is projected
into the largest other-net-free sub-span of the door, with the reserved
intervals read straight from the search tree. With margin = clearance +
half_width the room decomposition already keeps doors clearance-clear,
so this reduces exactly to the old midpoint clamp where no other-net
copper crosses the door.
- When a net would be dropped, a transactional shove-in-rooms recovery
nudges the committed trace(s) blocking it aside with the exact shove
primitive and re-verifies against the clearance oracle, so both nets fit
through one channel. Every moved trace is journaled and rolled back to
its exact original tiles and wires on any failure -- the room engine
mutates already-committed nets, so the rollback is real rather than the
exact track's no-op.
The board-wide has_violation(clearance) gate stays the final assertion.
Thread the flag through pipeline.route_dsn_board_rooms /
build_rooms_routing_result / route(engine='room', shove=...).
Factor the exact router's shove geometry into route/shove.py: the
conflicting-segment finder and displacement ladder are re-exported from
exact_router (kept byte-unchanged so its shove count and geometry stay
identical), and a room-oriented trace placement helper is added that
returns the inserted tile owner ids for a journaled rollback.
Adds narrow_channel.dsn: a keepout wall with a narrow gap positioned off the
routing grid, so no grid cell centre falls inside the gap's free centreline.
The exact/grid track drops the net (cell quantization can't fit a centreline
in the channel); the continuous room track routes it. This is the headline
result that continuous free-space beats the grid.
Tests: the room router's output is DRC-clean (exact has_violation is None) on
simple/crossing/kicad/narrow_channel; it connects simple/crossing (with a via)
and all four multi-pin KiCad nets; traces are orthogonal and end on pads; and
the continuity contrast asserts exact routes 0 while rooms routes 1, both
clean. Oracle-gated connectivity parity on simple/crossing.
A third engine (engine='room') that replaces the fixed routing grid with
FreeRouting's free-space rooms. Per layer, the space not occupied by items
(inflated by the routing clearance) is decomposed into exact rectangles via
coordinate compression (the orthogonal analogue of
CompleteFreeSpaceExpansionRoom); adjacent rooms share an edge door, and a via
door joins overlapping rooms on two layers. The maze search is an A* over
rooms-through-doors (cost = geometric distance + via cost), so a path may
cross a door anywhere along its width rather than at a quantized cell. The
found room sequence is realized into an exact orthogonal Polyline through the
door mid-gates and verified DRC-clean against the ShapeSearchTree, dropping
the net if it cannot be made exactly clean.
Maps to autoroute/: ExpansionRoom / CompleteFreeSpaceExpansionRoom (rooms),
ExpansionDoor / TargetItemExpansionDoor (doors), MazeSearchAlgo /
AutorouteEngine (search), LocateFoundConnectionAlgo (realization).
Foundation scope: rooms are axis-aligned rectangles (not general Simplex
tiles); realization uses an L-connector through door mid-gates. Any-angle
rooms, optimal gate placement (true sub-cell shove), and optimization passes
are follow-ups. The grid and exact tracks are unchanged; this is opt-in.
Routes simple/crossing (with a via) and all four multi-pin nets of the real
KiCad board, DRC-clean.
Adds shove_needed.dsn: one signal layer (no vias), where NET_A routes as
a straight wall that boxes NET_C out under greedy ordering. Tests assert
the contrast: without shove NET_C is dropped (result still DRC-clean, just
incomplete); with shove NET_A is moved aside and both nets connect, the
exact has_violation check is clean, endpoints stay on pads, and the result
is deterministic. Regression tests confirm shove is a no-op on the simple/
crossing/ripup/kicad boards (same connectivity, DRC-clean, zero shoves).
Oracle-gated parity is included (skips when the JAR cannot route the
synthetic single-layer board).
Before a dropped net is abandoned, the exact router (with shove=True)
tries to make room by moving an existing trace aside instead: it forms a
straight orthogonal candidate for the dropped net, finds the axis-aligned
trace segments that cross it, and shoves each one perpendicular (via
shove_segment) far enough to restore clearance. A shove is accepted only
if the moved trace still connects its pads, clears every other item
exactly against the ShapeSearchTree, and stays inside the board outline,
so DRC-cleanliness is preserved by construction. Shoves are bounded
(max_shove_depth, per-trace shove_cap) and deterministic. The dropped set
now includes nets the grid failed to route, not only exact-clearance
rejections, so shove can recover them.
The pipeline exposes shove (and rip_up) through route_dsn_board_exact /
build_exact_routing_result / route(engine='exact', shove=True); default
off, so the grid track and the no-shove exact track are unchanged.
Displaces one segment of a trace polyline perpendicular by a given
offset, keeping the trace orthogonal and its two pad endpoints fixed: a
jog corner is inserted when the moved segment is first or last, and
interior neighbours simply resize. Collinear/duplicate corners are
dropped. This is the foundation for shoving a trace aside to make room
for another. Unit-tested for single-segment, interior-segment, and
orthogonality-preservation cases.
Adds route/exact_router.py: routes with the grid track's rip-up/multi-layer/
via topology search in orthogonal mode (axis-aligned segments -> exact IntBox
copper), then verifies clearance exactly against a ShapeSearchTree. A net
whose copper would come closer than the clearance to any accepted item is
dropped, so the emitted geometry is DRC-clean by construction; endpoints stay
exactly on pads and vias at real layer transitions.
GridRouter gains a non-breaking orthogonal option (4-connected). The pipeline
gains an engine= selector: 'grid' (default, unchanged, highest coverage) or
'exact' (DRC-verified). The grid MVP stays the fallback.
On the designed boards the exact track routes every net DRC-clean; on the
real KiCad board it matches the grid's connectivity (4/4 multi-pin nets) and
is DRC-clean where the grid MVP's widened centrelines are not. Deferred:
shove, and 45-degree/IntOctagon trace caps.
Tests: the exact router's output has no clearance violation (exact IntBox
check) on every fixture; endpoints on pads; traces orthogonal; crossing uses
a via; and a direct comparison showing the exact track is clean where the
grid MVP violates on the real board. Oracle-gated parity for engine='exact'.
A bounding-box spatial hash (broad phase) with exact IntBox-intersection
narrow phase, replacing the exact router's occupancy grid. Supports
insert/remove-owner and overlap/region queries, a clearance_conflict test
(strict 2-D overlap of the clearance-expanded box, so tiles exactly a
clearance apart are allowed), and has_violation for the whole-board DRC
check. Tiles are tagged routed vs static so pad-vs-pad spacing in the
source design is not counted as a routing violation.
Ports the trace-relevant subset of geometry/planar/{Polyline,PolylineShape,
LineSegment}: a Polyline is a trace centreline (integer corners); a
PolylineShape is that centreline swept by the half-width, returned as exact
convex IntBox tiles (one flush box per orthogonal segment plus a square at
each interior corner to cover the turn). Exact for orthogonal segments; a
conservative bounding box for a diagonal one (never under-reports an
overlap). 45-degree/IntOctagon caps are deferred.
The pyproject declared a freeroute script but freeroute.cli was missing, so the
entry point was broken. Add it: reads a Specctra .dsn, routes (rip-up on by
default), writes SES. Accepts FreeRouting-compatible -de/-do flags so it can be
dropped in wherever 'java -jar freerouting.jar' was called.
Adds ripup_needed.dsn: one signal layer (B.Cu is a power plane, so vias
are impossible) with two nets whose greedy order strands NET_B, but where
rip-up reroutes NET_A around NET_B to connect both.
Tests: greedy (rip_up=False) leaves >= 1 connection unrouted with no vias;
rip-up connects all with no vias; the result is deterministic across
re-runs; rip-up keeps endpoints on pads and produces no same-layer cross
between nets (ripped traces leave no orphaned occupancy). The crossing
board's single-layer test now targets the greedy path explicitly, and a
regression test asserts the real KiCad board still connects all four of
its multi-pin nets. Oracle-gated parity added for the congested board.
Restructures the router so occupancy is tracked per connection, then adds
a rip-up-and-retry loop that recovers from bad greedy net orderings.
When a connection cannot reach its target through free space, a rip-up
search may pass through other nets' traces at an escalating penalty; the
router rips up the connections that path crosses (fully removing their
occupancy), routes the failing connection, and re-queues the ripped
connections. Passes iterate up to max_passes, keeping the best
(fewest-unrouted) result and stopping on full success or when a pass
changes nothing.
Thrash prevention: each connection may be ripped at most rip_cap times,
and the rip penalty escalates with a connection's rip count, so
repeatedly-ripped connections harden into walls. Fully deterministic
(sorted rip sets, connection-creation order, tuple-keyed A* — no RNG).
route()/route_dsn_board()/route_board() take rip_up and max_passes;
rip_up=False reproduces the previous greedy single-pass behaviour for
comparison. RouteResult now reports the unrouted connection count.
Adds crossing_2net.dsn: NET_A is a full-width horizontal wall and NET_B's
pins sit above and below it, so single-layer routing cannot connect NET_B
but two-layer routing succeeds by dipping to the other layer through vias.
Tests assert the multi-layer invariants: single-layer routing leaves a net
unrouted with no vias; multi-layer connects both nets with >= 1 via; the
SES contains a via scope; every via sits at a real layer transition of its
net's path; trace/via endpoints still land on the pads; traces stay on
valid layers and within the outline; and no two different nets cross on the
same layer. Oracle-gated parity tests cover the simple and crossing boards.
Extends the grid router with a layer axis: A* nodes are (col, row, layer),
in-plane moves stay on a layer, and a via move transitions between layers
at a cell for a configurable via_cost (so the router prefers one layer but
changes layers to get through). Occupancy is tracked per (cell, layer);
a through via must be clear on every signal layer and then blocks all of
them for other nets.
RouteResult now carries per-layer wire segments and per-net via locations.
The pipeline emits each segment on its layer and each via as
(via <padstack> x y), using the DSN's via padstack; wire and via
coordinates are converted from board units back to DSN units. route() and
route_dsn_board() take an optional layers= to restrict routing (e.g. a
single layer) for comparison.
Implements a working autorouter and the Java-free replacement for the
freerouting.jar step: dsn_text -> parse_dsn -> build_board -> route ->
write_ses.
GridRouter is a single-layer A* maze search over a uniform occupancy
grid: a cell is blocked by another net's pad (inflated by clearance +
half trace width) or a keepout; each net's ratsnest is connected pin to
pin; a routed trace then blocks other nets. The cell path becomes a
trace polyline whose endpoints are the exact pin locations. Board-unit
paths are converted back to DSN units for the SES (wire (path ...))
scopes.
This is an MVP, not a port of FreeRouting's expansion-room maze:
free-space rooms, rip-up-and-retry, multi-layer via search, and shove
are deferred (they raise quality/coverage, not the connectivity
milestone). It reaches connectivity on boards whose nets route on one
layer without crossing.
Adds tests/dsn/fixtures/simple_2net.dsn (a guaranteed-routable 2-net
board). Invariant tests: both nets route, the emitted SES parses, every
trace stays on a valid layer and within the board outline, and every
routed net's trace endpoints sit exactly on its two pads. An oracle-gated
test asserts connectivity parity with the reference FreeRouting JAR on
the simple board.
Ports the data model of the board Item hierarchy (Item base, Pin,
ObstacleArea/ConductionArea, Via, Trace) and BasicBoard (layers, nets,
clearance, bounding box, items with query helpers), then build_board:
the load-bearing integration that constructs a BasicBoard from a parsed
DsnBoard.
build_board maps layers -> LayerStructure, resolution -> transform,
default clearance rule -> ClearanceMatrix, nets -> Nets plus a
(component,pin)->net map, padstacks x placement -> Pin items, and
keepouts -> ObstacleArea items. Rectangle pads become exact IntBoxes,
convex polygon pads exact Simplexes, circle pads their bounding box
(documented approximation); every pad is centred on its pin location so
it contains that location by construction. Trace/Via are router-produced
and lightweight here (an imported unrouted board has none).
Validated on a real KiCad export (kicad_routable.dsn): layer/net/pin
counts match the parsed DSN, every pin's pad shape contains its origin,
every pin reports a valid net, and pin locations lie in the board
bounding box. An oracle-gated test cross-checks that every net the
reference FreeRouting JAR routes exists on the constructed board.
Ports the foundational board classes: Unit (mil/inch/mm/um with
micrometer scaling), Layer/LayerStructure (the layer stack with name and
signal-layer lookups), CoordinateTransform (DSN<->board scaling by the
resolution), Net/Nets (connectivity keyed by (name, subnet) and a board-
unique net number), and ClearanceMatrix (class x class spacing with a
reserved null class and a default class).
The per-layer axis of ClearanceMatrix is simplified to a single value per
class pair (DSN default clearance rules are layer-independent for the
boards we build); the router phase can add the layer dimension.
Unit-tested: unit scaling/parsing, layer lookups, transform round-trip,
net registration/lookup, and clearance default/append/symmetry.
FreeRouting ships no unit tests for its geometry/router, so there is no
value-level oracle to port against. This adds a dev/test-only harness that runs
the reference JAR to route a DSN, letting freeroute's output be diffed against
the reference implementation — the router phase will assert connectivity parity
(same nets routed) via routed_net_set().
- tests/oracle.py: locate Java 21+ and a freerouting JAR (env overrides:
FREEROUTE_ORACLE_JAVA, FREEROUTING_JAR), route a DSN, and extract routed
connectivity from the SES. requires_oracle skips when no JVM/JAR is present,
so the suite stays Java-free.
- tests/test_oracle.py: routes a routable board end-to-end and checks the
connectivity extraction.
- tests/dsn/fixtures/kicad_routable.dsn: a real KiCad pcbnew-exported DSN
(Arduino_Mega template, path sanitized) that actually routes — the smd_demo
fixture leaves its nets unrouted.
- pyproject: pythonpath=["tests"] so the harness imports as `oracle`.
Specctra's SpecCharASCII includes / and *, and an Identifier may start with /,
so KiCad emits hierarchical net names such as /*52 and /53. The tokenizer
treated any /* as a block-comment start and raised 'unterminated comment' when
no */ followed — rejecting real KiCad DSN. Match FreeRouting's JFlex rule-order
resolution: /* is a comment only when a closing */ exists; otherwise it is an
ordinary name run. Validated against a KiCad 10.0.4 pcbnew-exported DSN (78 nets
incl. /*52, /53).
Ports geometry/planar/Polygon.java (corner de-duplication and collinear
removal, winding number) and PolygonShape.java, including the recursive
split_to_convex that decomposes a simple polygon into convex Simplex
tiles by dividing at concave corners along minimal axis-parallel lines.
Orientation and convexity tests are exact; the division-point search is
approximate (float line evaluation, split point rounded to an integer
corner) as upstream. The concave-corner search starts deterministically
at corner 0 rather than a seeded PRNG; this only affects which valid
decomposition is produced.
Supporting additions: Simplex.from_corners (convex polygon to simplex)
and Line.function_value_approx / function_in_y_value_approx.
Invariant tests over L, plus, staircase and square polygons: tile areas
sum to the polygon area (no gaps, no overlap), a point is in the polygon
iff in some tile, and no point is strictly inside more than one tile
(interiors disjoint). Also covers Polygon normalization and orientation.
Ports geometry/planar/TileShape.java (the border-line-based containment,
area, and centre-of-gravity logic) and Simplex.java (a convex region as
the intersection of directed half-planes). Corners are exact
intersections of consecutive border lines; point containment uses exact
side_of. The remove_redundant_lines normalization — dropping lines that
do not contribute and detecting emptiness — is ported line-for-line.
Supporting additions: Line.compare_to/__lt__ (angular sort order),
Line.fast_equals, Line.side_of_intersection, Line.translate (perpendicular
offset), IntDirection.determinant, and IntBox.to_simplex.
offset is approximate (rounded translated lines, as upstream); enlarge
clips to the enlarged bounding box pending the IntOctagon port.
Since there is no JVM oracle, tests assert invariants: corners lie
exactly on their border lines (exact side_of == 0), IntBox -> Simplex
preserves the region over a sampled grid, intersection is contained in
both operands and a point is in the result iff in both, and get_instance
normalization drops redundant lines and detects empty half-plane pairs.
Ports IntBox, the simplest concrete convex tile (RegularTileShape): exact
integer-corner rectangle with contains (border-inclusive and interior),
intersection, union, intersects/overlaps, offset (round half up),
horizontal/vertical offset, shrink, box containment, translate, and
dimension. Adds the package __init__ exporting the geometry API.
The general convex machinery beyond IntBox — TileShape/Simplex/IntOctagon
and polygon split_to_convex — is deferred to the next geometry phase.
Tests cover boundary vs interior containment, degenerate tiles
(empty/point/segment), edge-touching intersects-vs-overlaps, rational
point on a border, and half-up offset rounding.
Ports Direction/IntDirection (equivalence classes of vectors, gcd-
normalized, exact angular compare) and Line. Line.side_of uses an exact
integer determinant; Line.intersection returns an IntPoint when the
crossing is integral and a RationalPoint otherwise, with the orthogonal
and 45-degree fast paths from the source preserved. Parallel lines yield
a point at infinity (z=0). BigIntDirection is folded into IntDirection
since unbounded int always fits.
Tests cover integral and rational intersections, parallel-line infinity,
exactness beyond double precision (verified via exact collinearity of the
result), and direction normalization/ordering.
Ports the point/vector foundation of geometry/planar: Side and Signum
(three-valued signs), Limits, FloatPoint (approximate), and the exact
IntPoint/IntVector plus projective RationalPoint/RationalVector.
Arithmetic model per type is documented in each module. The key
simplification over the Java source: Python's unbounded int makes the
exact orientation determinants and rational (x,y,z) coordinates trivial,
so side_of is kept exact (upstream uses a double for speed) and no
BigInteger or CRIT_INT overflow promotion is needed. Rational points use
the projective triple with z=0 denoting the point at infinity.
Tests cover collinearity, determinants beyond Java long range, rational
equality/reduction, and integer/rational promotion.
Ports the write path of io/specctra/SesWriter.java to emit a valid
session from a parsed DsnBoard plus a RoutingResult. Each _write_*
function mirrors a write* method upstream:
- session scope with base_design, placement (resolution + components
echoed from the DSN), an empty was_is, and routes
- routes carries resolution, a reduced parser scope, library_out with
the via padstacks, and network_out
- network_out emits (net (wire (path layer width x1 y1 ...)) (via
padstack x y)) for each routed net; integer coordinates via round-
half-up to match Java Math.round
write_ses(board) with no result produces a valid no-op session,
proving the DSN-in / SES-out round trip. Only routes > network_out is
required by SesReader and kicad-cli; the rest is echoed for a
well-formed file.
18 tests: indent/quoting rules, no-op session structure, placement and
library_out echo, routed wire/via serialization, integer rounding, and
round-trips re-parsed through the project's own DSN tokenizer/sexp
(64 tests total across the suite, all green; ruff clean).
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.
Ports the read path of FreeRouting's io/specctra/parser package to a
Java-free Python implementation:
- tokenizer: S-expression lexer mirroring SpecctraFileDescription.flex
(comments, quoted strings, the string_quote IGNORE_QUOTE directive,
case-insensitive keywords, and hash-prefixed names)
- sexp: nested S-expression tree builder
- shapes: rect/circle/polygon/path plus area-with-holes scopes
- model: typed dataclasses for layers, padstacks, images, placements,
nets, net classes, rules, keepouts
- reader: recursive-descent scope readers producing a DsnBoard, one
_read_* function per FreeRouting read_scope method
46 pytest cases cover the tokenizer, tree, shapes, and end-to-end
parsing against hand-crafted fixtures modeled on FreeRouting's own
test DSN files. Parses 90 of 91 upstream fixtures (the one failure is
a binary OLE file, not text DSN).
Documents the packages relevant to the port (io/specctra DSN/SES,
board data model, geometry/planar primitives, autoroute core) with
verified upstream paths and Python-translation notes. Corrects the
seed plan's stale guesses: the Specctra code lives under io/specctra,
not designforms/specctra.
Pure-Python PCB autorouter replacing the freerouting.jar step in kicad-mcp.
Contract is Specctra DSN in, SES out — a drop-in for the JAR so the toolchain
needs no JVM. Includes pyproject (uv/ruff/pytest, src-layout) and a porting
plan seeded for study of the upstream FreeRouting Java source.