A plane end is marked with its zone, not a pin, so a route from a pad
outside the plane's outline crashed looking for the pin's location
(the flight-controller board's first placement).
Over 8 net orders it left 25 to 30 fewer connections unrouted on the
dense sense boards and 16 to 18 fewer on two PCBench boards, and was
never clearly worse. KiCad DRC on the sense harness: 41 opens to 28.
The connections a rip-up route crosses are rerouted at once in free
space. If that leaves fewer connections routed than before, the new
route comes out and every displaced connection goes back on its old
path, with its rip count as it was. Even trades are kept.
A route may not leave a cell inside a wire keepout as a wire, nor reach
a pin there by one: it changes layer instead, and the via goes in the
pad. A pin whose every stub enters a keepout is reached by a via only.
A structure keepout named rule_area... (mckicad names KiCad rule areas
so) is checked for overlap only, as KiCad's DRC does, with the free ends
of each wire tested for their round caps.
--access-cost C makes a route pay C to pass where it would close another
pin's way out (its end cells, the via it may need there, and the
clearance around both) while that pin still has connections to route.
A pin inside a wire keepout can only leave by a via; its stub to the
cell centre was track inside the keepout, which the exact check refused.
The via now goes on the pin itself. Pin stubs are also checked exactly
against wire keepouts, as the exact engine will check them.
bench/diagnose.py reports what each refused connection collided with.
Each round routes every connection that has failed so far ahead of the
rest, which keep their order. The promoted set only grows, so two groups
cannot take turns failing; the best round is kept.
The router is deterministic, but its result depends heavily on which
connection routes first. A seed shuffles that order reproducibly, so a
change can be judged over several orders instead of the one it happened
to get.
On a four-layer board with a GND plane on an inner power layer, freeroute
ignored the plane (it only used zones on routing layers) and chained every
GND pad to the next with tracks across the board. Now a through-hole pad
inside a plane is joined to it already, and for a net with a plane every
other group connects straight to the plane: a route may end on any cell
inside it where a via fits (checked lazily, like other vias), and ending
there places that via. Signal nets are unaffected.
Tested on a four-layer DSN: three SMD GND pads each get a short stub and
their own via, the THT GND pad needs nothing, and no track crosses the
board. Ahead of the TerraHawk ESC, a four-layer board.
freeroute exited 0 and said nothing when it dropped connections, which
cost real diagnosis time: on rc4 two simple two-pin nets came back with no
copper and no explanation. The grid now records each connection it could
not route, the exact check adds each it rejects, and the CLI lists them on
stderr (the SES is still written; the exit code stays 0, as the JAR's).
A connection whose ends both sit in one zone of its net is listed apart,
as left to the zone fill: freeroute does not route through a zone it
cannot trust to be solid, but KiCad's fill often joins those pads anyway.
A zone outline is where copper may go, not where it ends up: on the rc4
sense board the board-wide GND pours break into islands around other
nets' tracks, and treating every pad inside them as connected left 28
GND islands unjoined (63 unconnected against 42 without zones). Trust a
zone only when a no-tracks rule area covers it on its layer, as the
power stage's pours are covered; route the rest with tracks as before.
rc4, exact engine, resolution 2: power stage 5 unconnected (JAR 12),
sense board 41 (JAR 32), no DRC errors on either.
freeroute ignored the DSN's planes (KiCad's zones), so it never knew that
pads in a pour are joined by it. On the rc4 power stage those pads sit
inside "keep router off" areas as well, and every connection to them was
dropped without a word.
- Read (plane <net> <shape>) and put each zone on the board as a
ConductionArea of its net. Zones are not obstacles: KiCad refills them
around other nets' copper.
- The grid groups each net's pads and zone pieces that copper already
joins (a pad in a zone, two zones sharing a pad) and routes only between
groups. A connection may end on a zone by a via inside its copper, or by
a track where tracks are allowed.
- wire_keepout now blocks traces only, as KiCad's "no tracks" rule area
does; via_keepout only vias; keepout both, in the grid and in the exact
verifier.
rc4 power stage, exact engine, resolution 2: 2 unconnected, down from 8
(FreeRouting JAR: 12), no DRC errors.
The grid search estimated the remaining distance as a straight line even
when only orthogonal steps exist; Manhattan is the tighter estimate those
moves admit and still never overestimates. --heuristic-weight W (default
1) makes it weighted A*: routes at most W times the shortest (tested).
A cell was exactly one trace plus its clearance, so a trace owning its cell
was the whole clearance model, and the 0.4 mm grid could not reach most
pins of a 0.5 mm pitch package: between two 0.3 mm pads a trace's centre
has a legal band only 0.15 mm wide. Resolution K makes cells K times
finer and has every trace claim a halo, the cells whose centre is nearer
than trace + clearance to one of its own, released again on rip-up.
K = 1 has an empty halo and is the old grid exactly (tested). On a 0.5 mm
row, K = 4 gets every pin out where K = 1 gets two. On the rc4 boards, K = 2
cuts the sense board's opens from 66 to 41 (JAR: 32) and the power board's
to 7 (JAR: 12), still with no DRC errors from freeroute's own copper.
Every route starts and ends with a stub from the pin to its first grid
cell, and the grid never saw that copper: on the rc4 sense board 24 of 39
connections the exact verifier rejected had a later route pass beside an
earlier stub. A route's stubs now claim the cells they come near, a stub
must be clear of other nets to be used, and a route may start from any
enterable neighbour of the pin cell, costed by stub length (Manhattan on
the orthogonal track, so a pin on a cell centre still leaves straight).
Tests: a route near a rounded corner keeps its edge clearance; and a
strict xfail that pins down the remaining gap, a 0.5 mm pin row the grid
cannot give a column to every pin of.
Measured against KiCad's own pads on the TerraHawk rc4 power stage:
- Back-side parts kept their pads on F.Cu (32 pads). Mirror the copper
layer for a back-side part.
- Pad outlines were copied unrotated (29 pads on rotated parts). Place the
outline through the same chain as the pin: pin rotation, back-side
mirror, part rotation. Oval pads, a path with a width, came out as a
zero-height sliver; pad them out by the aperture.
- Multi-shape padstacks used the first shape; use the largest.
- Footprint-level keepouts were parsed but never placed. KiCad exports a
mounting hole's plated ring that way, so the router could not see it.
- Vias were sized like a trace (0.25 mm square for a 0.6 mm via), and every
remaining short and clearance error was a via. Read the size from the
DSN's via padstack and use it in all three engines; the grid engine now
reserves the cells a via's copper reaches.
With these the exact and room engines route that board with no shorts,
clearance or other DRC errors. Pins now carry their component reference.
On the TerraHawk rc4 power stage every engine misread the board's rule areas:
- A no-track ring along the board edge arrives as one keyhole polygon, and
keepout polygons were wrapped in a single convex tile, so the ring became
the whole board. Split keepout polygons into convex pieces instead.
- The grid engine rasterized an area by its overall bounding box, which for
that ring is again the whole board; it routed nothing. Rasterize per tile.
- "Keep router off" rule areas arrive as wire_keepout, which the reader did
not know, so tracks ran through them (172 DRC items_not_allowed). Read it
at board and image level. For now it blocks vias too, which is stricter
than KiCad and cannot create a violation.
- place_keepout only restricts part placement; stop treating it as copper.
The oracle now runs the JAR headless; it was opening FreeRouting's GUI on
the desktop for every oracle test.
SES lengths and coordinates were written in the DSN's unit while the
session declared (resolution um 10), so readers that follow FreeRouting's
SesWriter (KiCad's importer, mckicad's ses_apply) placed every track at a
tenth of its size. The JAR, run on the same DSN, writes F1 at 65000 where
freeroute wrote 6500.
Scale every length by the resolution factor: placements, via padstack
shapes, wire widths and points, and vias. Rotations stay as they are. The
router tests compared SES output against DSN pad positions and so had the
same mistake built in; they now derive the expected values through
ses_scale.
Add the GPLv3 text (the package declared the licence but never shipped it),
a py.typed marker, and an sdist include/exclude allowlist so the distribution
carries only src/, docs/, README, LICENSE and pyproject. reference/ is a clone
of the GPL FreeRouting Java tree kept purely as a porting reference and must
never be redistributed inside this package; tests, caches and build output are
excluded too.
Switch to the PEP 639 SPDX licence expression with license-files, widen the
classifiers, and add an -o alias for -do. Rewrite the README around the engine
matrix: what each track and pass actually does, and what this is not (no
FreeRouting density parity on dense boards, and --diagonal is a recovery and
shortening pass, not a diagonal-native search).
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.
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.
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.
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.
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).
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.
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.
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 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).