freeroute/docs/ARCHITECTURE.md
Ryan Malloy dc44abc7f7 Add architecture map of upstream FreeRouting source
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.
2026-07-11 15:21:09 -06:00

190 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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/freeroute/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 n1 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/freeroute/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** — `freeroute board.dsn -o board.ses`.
</content>
</invoke>