The router began as a port of FreeRouting and has grown its own engine (negotiated congestion, tree nets, exact checks, axis escapes), and 'freeroute' was one letter from the project it no longer follows. The package, module and command are now rpmroute; the freeroute command stays as an alias, and the RPMROUTE / RPMROUTE_ORACLE_JAVA variables fall back to the old FREEROUTE names. Version 2026.10.07. Routing is unchanged: a PCBench board routes to a byte-identical session either way.
190 lines
12 KiB
Markdown
190 lines
12 KiB
Markdown
# Upstream FreeRouting architecture — a port map
|
||
|
||
This maps the parts of the FreeRouting Java source (cloned into the gitignored
|
||
`reference/freerouting/`) that matter for a Java-free Python port. It was
|
||
written by reading the actual source, not the Specctra spec. Package paths here
|
||
are verified against the clone (commit fetched via `--depth 1` on
|
||
2026-07-11) and **differ from the seed plan's guesses** — the DSN/SES code lives
|
||
under `io/specctra/`, not `designforms/specctra/`.
|
||
|
||
Java root in the clone: `src/main/java/app/freerouting/`.
|
||
|
||
## 1. Specctra DSN / SES I/O — `io/specctra/`
|
||
|
||
This is the I/O boundary the port must match: `kicad-cli` produces the `.dsn`
|
||
input and consumes the `.ses` output.
|
||
|
||
### Entry points (`io/specctra/`)
|
||
- `DsnReader.java` (294 lines) — the modern read entry point. `readBoard(stream,…)`
|
||
validates the `(pcb <name>` header with a literal three-token check
|
||
(`(` , `pcb` scope keyword, name string), then calls
|
||
`Keyword.PCB_SCOPE.read_scope(par)` to parse the body. `readMetadata(stream)`
|
||
is a fast path that parses only `parser`/`resolution`/`structure` and stops.
|
||
Returns a typed `BoardReadResult` (Success / OutlineMissing / ParseError /
|
||
IoError). Defaults captured here: it flips the scanner to `NAME` state to read
|
||
the pcb name cleanly.
|
||
- `SesReader.java` (392) / `SesWriter.java` (430) — session-file read/write.
|
||
- `RulesReader.java` / `RulesWriter.java` — `.rules` sidecar files.
|
||
- `DsnWriter.java` (97) — DSN writer entry.
|
||
|
||
### Tokenizer + scope readers (`io/specctra/parser/`)
|
||
The lexer is **JFlex-generated** — `SpecctraDsnStreamReader.java` (1467 lines) is
|
||
a packed DFA; do **not** port the DFA. The human-readable grammar is in
|
||
`SpecctraFileDescription.flex` (312 lines) and that is what to reimplement.
|
||
Key lexical facts extracted from the flex file:
|
||
- Whitespace: `\r \n \f \t space`. Comments: `#…` to EOL, and `/* … */`.
|
||
- Two quote chars: `"` (STRING1) and `'` (STRING2). No escaping — inside a
|
||
quoted string `\` is a literal backslash; the matching quote ends it.
|
||
- Keywords are matched case-insensitively (`%ignorecase`) and returned as
|
||
singleton `Keyword` objects; `(` → `OPEN_BRACKET`, `)` → `CLOSED_BRACKET`.
|
||
- Non-keyword atoms are returned as `Integer`, `Double`, or `String`.
|
||
- **Lexical states are the tricky part.** Several keywords call `yybegin(NAME)`
|
||
(or `LAYER_NAME`, `COMPONENT_NAME`, `SPEC_CHAR`, `IGNORE_QUOTE`) so the *next*
|
||
token is forced to be read as a string even when it looks like a number
|
||
(a net or layer literally named `0`). The Python port reconciles this by
|
||
keeping every unquoted atom as a token that preserves both its raw text and a
|
||
best-effort numeric value, and letting each scope reader decide which to use —
|
||
exactly the decision the lexical state encodes.
|
||
- High-level scanner helpers (bottom of `SpecctraDsnStreamReader.java`):
|
||
`next_string(ignoreNewline, leadingSep)`, `next_string_list(sep)`,
|
||
`next_double()`, `next_closing_bracket()`. `next_string` skips leading
|
||
whitespace, handles a leading `"`, and stops at whitespace / `(` / `)` (or a
|
||
caller-supplied separator such as `-` for `Comp-Pin` splitting).
|
||
|
||
Scope-reader classes (each a recursive-descent reader over the token stream —
|
||
this is the structure the Python `reader.py` mirrors):
|
||
- `ScopeKeyword.java` — base class. `read_scope` loop: on `(` followed by a
|
||
known `ScopeKeyword`, recurse; unknown scope → `skip_scope` (bracket
|
||
counting). This tolerant skip is the backbone of the whole reader.
|
||
- `Parser.java` — `(parser (string_quote ") (host_cad …) (host_version …)
|
||
(constant …) (write_resolution …) (generated_by_freerouting))`.
|
||
- `Resolution.java` — `(resolution <unit> <int>)`. Defaults: unit `mil`,
|
||
resolution `100` (set in `ReadScopeParameter`).
|
||
- `Structure.java` (1174) — the big one. Reads `layer`, `boundary`, `via`
|
||
(routing via padstack names, incl. `(spare …)`), `rule` (default width/
|
||
clearance), `keepout` / `via_keepout` / `place_keepout`, `plane`, `control
|
||
(via_at_smd on|off)`, `snap_angle`, `autoroute_settings`, `flip_style`. Then
|
||
`create_board` turns it into geometry. `layer`: `(layer <name> (type
|
||
signal|power|jumper) (use_net …) (rule …))`. `boundary`: a shape on layer
|
||
`pcb` is the bounding box; shapes on layer `signal` are the outline.
|
||
- `Shape.java` (575) — shape grammar shared everywhere:
|
||
`(rect <layer> x1 y1 x2 y2)`, `(circle <layer> dia [x y])`,
|
||
`(polygon <layer> aperture x1 y1 …)`, `(path <layer> width x1 y1 …)`.
|
||
`read_area_scope` reads an optional name + a border shape + `(window …)` holes
|
||
+ optional `(clearance_class …)`.
|
||
- `Library.java` (323) — `(library (padstack …) (image …))`.
|
||
`padstack`: name, one or more `(shape (<shape>))`, `(attach on|off)`,
|
||
`(absolute on|off)`. `Package.java` (391) reads `(image <name> (side
|
||
front|back) (pin <padstack> [ (rotate d) ] <pinname> x y) (outline …)
|
||
(keepout …) …)`.
|
||
- `Placement.java` / `Component.java` (315) — `(placement (component <libname>
|
||
(place <refdes> x y front|back rot [ (lock_type position) ] [ (PN part) ]
|
||
…)))`. A `place` with no coords means "not yet placed".
|
||
- `Network.java` (1193) — `(network (net <name> [subnet] (pins Comp-Pin …)
|
||
(fromto …) (rule …)) (class <name> net… (circuit (use_via …)) (rule …))
|
||
(class_class …))`. Pin refs split component/pin on the first `-`.
|
||
- `NetClass.java`, `Net.java`, `Rule.java` (width/clearance rules),
|
||
`AutorouteSettings.java` — supporting readers.
|
||
- Data holders: `Layer`, `Padstack`(core), `PinInfo`, `ComponentPlacement`,
|
||
`Circle/Rectangle/Polygon/PolygonPath/PolylinePath`.
|
||
|
||
**Python port status:** items 1 (tokenizer), the shape grammar, and every
|
||
structure/library/network/placement/parser/resolution scope above are
|
||
implemented in `src/rpmroute/dsn/` this session (see below).
|
||
|
||
## 2. Board data model — `board/`
|
||
|
||
Constructed by `Structure.create_board`. Central classes:
|
||
- `BasicBoard.java` — geometric item container: insert/delete/pick items,
|
||
layer structure, bounding box, `insert_obstacle`, `insert_via_obstacle`,
|
||
`insert_conduction_area`, `insert_component_obstacle`. `RoutingBoard.java`
|
||
extends it with the routing operations the autorouter drives.
|
||
- `Item.java` + subclasses: `Trace`/`PolylineTrace` (a routed wire as a
|
||
`Polyline`), `Via`, `DrillItem`, `Pin`, `ObstacleArea`/`ConductionArea`/
|
||
`ComponentObstacleArea`/`ViaObstacleArea` (keepouts & planes), `BoardOutline`.
|
||
- `Component.java` / `Components.java` — placed component instances.
|
||
- `Layer.java` / `LayerStructure.java` — signal/power layers, index 0 = top.
|
||
- `SearchTreeManager.java` + `ShapeSearchTree*.java` — the spatial index
|
||
(MinAreaTree R-tree variants, one per angle restriction) used for fast
|
||
obstacle queries during routing.
|
||
- Shoving/tightening: `ShoveTraceAlgo`, `ForcedPadAlgo`, `ForcedViaAlgo`,
|
||
`PullTightAlgo{,45,90,AnyAngle}`.
|
||
- `Unit.java` (mil/inch/mm/um), `CoordinateTransform` (dsn↔board scaling),
|
||
`AngleRestriction` (NINETY / FORTYFIVE / NONE).
|
||
|
||
**Python translation:** a `board` package of dataclasses + a spatial index.
|
||
`Item` hierarchy → dataclasses with a shared base; the search tree → an rtree/
|
||
STRtree (shapely) or a hand-rolled bbox tree. Not started this session.
|
||
|
||
## 3. Planar geometry — `geometry/planar/` (34 files)
|
||
|
||
The math the router stands on. Key types and their Python analogues:
|
||
- `IntPoint` / `IntVector` — integer point/vector (board coords). `FloatPoint`
|
||
(double tuple, not derived from `Point` because float math is inexact) for
|
||
approximate work. `RationalPoint`/`RationalVector` — exact rational
|
||
intersections of lines.
|
||
- `Line` / `LineSegment` / `Polyline` — a `Polyline` is a sequence of lines
|
||
whose n lines define n−1 corners; traces are polylines. `Direction`,
|
||
`FortyfiveDegreeDirection` encode the angle grid.
|
||
- `Shape` (interface) → `PolylineShape` (straight-line borders) → `TileShape`
|
||
(convex, half-plane intersection) → `Simplex` (general convex) and
|
||
`IntBox`/`IntOctagon` (axis- and 45°-aligned boxes). `ConvexShape`,
|
||
`RegularTileShape` interfaces. `Circle`, `Ellipse` for round pads.
|
||
- `Area` / `PolylineArea` — a shape possibly with holes (border + hole shapes).
|
||
- `split_to_convex()` (decompose into `TileShape[]`), `convex_hull()`,
|
||
`offset()`, `contains()`, `intersection()` are the workhorse ops.
|
||
- `Limits.java` (`CRIT_INT` overflow guard used when picking the coord scale).
|
||
|
||
**Python translation:** this is the highest-risk port (exact rational geometry,
|
||
convex decomposition). Options: lean on `shapely` for polygon ops and add a thin
|
||
45°/octagon layer, or port `Simplex`/`TileShape` directly for bit-exact parity
|
||
with the JAR oracle. Decide when the board model lands. Not started this session.
|
||
|
||
## 4. Autorouter core — `autoroute/`
|
||
|
||
- `BatchAutorouter.java` / `BatchAutorouterThread.java` — the top loop: pick the
|
||
next unrouted connection, route it, rip up and retry on failure, repeat over
|
||
passes. `BatchAutorouterV19.java` is a newer variant; `BatchFanout`,
|
||
`BatchOptimizer{,MultiThreaded}` are the fanout/optimize passes.
|
||
- `AutorouteEngine.java` — per-board routing state (expansion rooms, drill pages,
|
||
the maze search driver). Holds the `CompleteFreeSpaceExpansionRoom` graph.
|
||
- `MazeSearchAlgo.java` — the A*/Dijkstra maze search over expansion rooms
|
||
("route an incomplete connection via a maze search"). `MazeSearchElement`,
|
||
`MazeListElement`, `MazeShoveTraceAlgo` (shove obstacles while searching).
|
||
- `LocateFoundConnectionAlgo{,45Degree,AnyAngle}.java` — turn a found maze path
|
||
back into concrete trace geometry at the right angle restriction.
|
||
- `ExpansionRoom` / `FreeSpaceExpansionRoom` / `ObstacleExpansionRoom` /
|
||
`ExpansionDoor` / `ExpansionDrill` — the free-space decomposition the maze
|
||
search walks. `SortedRoomNeighbours{,45Degree,Orthogonal}` order neighbours.
|
||
- `AutorouteControl.java` — cost function parameters (via costs, preferred-
|
||
direction trace costs, ripup costs, start pass number).
|
||
- `InsertFoundConnectionAlgo.java` — commit the located connection to the board.
|
||
|
||
**Python translation:** the last and largest piece. An MVP can start grid/
|
||
Manhattan-45° maze routing decoupled from the exact expansion-room model, then
|
||
converge toward FreeRouting's free-space rooms. Not started this session.
|
||
|
||
## 5. Supporting packages (context)
|
||
- `rules/` — `BoardRules`, `ClearanceMatrix`, `NetClass`, `Net`, `ViaInfo`,
|
||
`DefaultItemClearanceClasses`. The clearance matrix is class×class×layer.
|
||
- `core/` — `Padstack`, `Padstacks`, `Package`, `Packages`, `BoardLibrary`,
|
||
`RoutingJob`. `settings/RouterSettings` holds autoroute config.
|
||
- `datastructures/` — `UndoableObjects`, `IndentFileWriter` (the DSN/SES pretty
|
||
printer), `IdentifierType` (quoting on write), identification-number generators.
|
||
- `drc/` — design-rule checks. `logger/FRLogger` — logging (warnings collected
|
||
into the read result).
|
||
|
||
## Port order (this file drives the roadmap)
|
||
1. **DSN parser** — done this session (`src/rpmroute/dsn/`).
|
||
2. **SES writer** — next. Start with an empty/no-op session to prove the loop
|
||
end-to-end through `kicad-cli pcb import specctra-ses`. See `SesWriter.java`
|
||
and `SpecctraSesFileWriter.java`: `(session <name> (base_design <name>)
|
||
(placement …) (was_is) (routes (resolution …) (network_out (net <name>
|
||
(wire (path <layer> <width> x1 y1 …)) (via <padstack> x y)))))`.
|
||
3. **Geometry primitives** — `geometry/planar` subset needed by the board model.
|
||
4. **Board model** — `board` items + spatial index.
|
||
5. **Autorouter** — maze/rip-up core, then optimize pass.
|
||
6. **CLI + kicad-mcp integration** — `rpmroute board.dsn -o board.ses`.
|
||
</content>
|
||
</invoke>
|