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.
This commit is contained in:
parent
0270137b9f
commit
dc44abc7f7
189
docs/ARCHITECTURE.md
Normal file
189
docs/ARCHITECTURE.md
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
# 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 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/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>
|
||||||
Loading…
x
Reference in New Issue
Block a user