From dc44abc7f727a2e512cedd02989646f5d9dba136 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Sat, 11 Jul 2026 15:21:09 -0600 Subject: [PATCH] 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. --- docs/ARCHITECTURE.md | 189 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 docs/ARCHITECTURE.md diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..7d4cb01 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -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 ` 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 )`. 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 (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 x1 y1 x2 y2)`, `(circle dia [x y])`, + `(polygon aperture x1 y1 …)`, `(path 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 ())`, `(attach on|off)`, + `(absolute on|off)`. `Package.java` (391) reads `(image (side + front|back) (pin [ (rotate d) ] x y) (outline …) + (keepout …) …)`. +- `Placement.java` / `Component.java` (315) — `(placement (component + (place x y front|back rot [ (lock_type position) ] [ (PN part) ] + …)))`. A `place` with no coords means "not yet placed". +- `Network.java` (1193) — `(network (net [subnet] (pins Comp-Pin …) + (fromto …) (rule …)) (class 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 (base_design ) + (placement …) (was_is) (routes (resolution …) (network_out (net + (wire (path x1 y1 …)) (via 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`. + +