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.
12 KiB
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 ((,pcbscope keyword, name string), then callsKeyword.PCB_SCOPE.read_scope(par)to parse the body.readMetadata(stream)is a fast path that parses onlyparser/resolution/structureand stops. Returns a typedBoardReadResult(Success / OutlineMissing / ParseError / IoError). Defaults captured here: it flips the scanner toNAMEstate to read the pcb name cleanly.SesReader.java(392) /SesWriter.java(430) — session-file read/write.RulesReader.java/RulesWriter.java—.rulessidecar 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 singletonKeywordobjects;(→OPEN_BRACKET,)→CLOSED_BRACKET. - Non-keyword atoms are returned as
Integer,Double, orString. - Lexical states are the tricky part. Several keywords call
yybegin(NAME)(orLAYER_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 named0). 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_stringskips leading whitespace, handles a leading", and stops at whitespace /(/)(or a caller-supplied separator such as-forComp-Pinsplitting).
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_scopeloop: on(followed by a knownScopeKeyword, 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: unitmil, resolution100(set inReadScopeParameter).Structure.java(1174) — the big one. Readslayer,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. Thencreate_boardturns it into geometry.layer:(layer <name> (type signal|power|jumper) (use_net …) (rule …)).boundary: a shape on layerpcbis the bounding box; shapes on layersignalare 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_scopereads an optional name + a border shape +(window …)holes- optional
(clearance_class …).
- optional
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) ] …))). Aplacewith 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.javaextends it with the routing operations the autorouter drives.Item.java+ subclasses:Trace/PolylineTrace(a routed wire as aPolyline),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 fromPointbecause float math is inexact) for approximate work.RationalPoint/RationalVector— exact rational intersections of lines.Line/LineSegment/Polyline— aPolylineis a sequence of lines whose n lines define n−1 corners; traces are polylines.Direction,FortyfiveDegreeDirectionencode the angle grid.Shape(interface) →PolylineShape(straight-line borders) →TileShape(convex, half-plane intersection) →Simplex(general convex) andIntBox/IntOctagon(axis- and 45°-aligned boxes).ConvexShape,RegularTileShapeinterfaces.Circle,Ellipsefor round pads.Area/PolylineArea— a shape possibly with holes (border + hole shapes).split_to_convex()(decompose intoTileShape[]),convex_hull(),offset(),contains(),intersection()are the workhorse ops.Limits.java(CRIT_INToverflow 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.javais 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 theCompleteFreeSpaceExpansionRoomgraph.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/RouterSettingsholds 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)
- DSN parser — done this session (
src/rpmroute/dsn/). - SES writer — next. Start with an empty/no-op session to prove the loop
end-to-end through
kicad-cli pcb import specctra-ses. SeeSesWriter.javaandSpecctraSesFileWriter.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))))). - Geometry primitives —
geometry/planarsubset needed by the board model. - Board model —
boarditems + spatial index. - Autorouter — maze/rip-up core, then optimize pass.
- CLI + kicad-mcp integration —
rpmroute board.dsn -o board.ses.