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

12 KiB
Raw Permalink Blame History

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-generatedSpecctraDsnStreamReader.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 primitivesgeometry/planar subset needed by the board model.
  4. Board modelboard items + spatial index.
  5. Autorouter — maze/rip-up core, then optimize pass.
  6. CLI + kicad-mcp integrationfreeroute board.dsn -o board.ses.