porta
porta is two things:
- A DSL for concisely defining a relational floorplan; and
- A software package for deterministically converting a floorplan into a clean SVG map.
room hall "Great Hall" 40x20 root
room parlor "Parlor" 20x20 left-of hall
room kitchen "Kitchen" 20x20 right-of hall
room study "Study" ?x20 down-of parlor
The porta DSL
porta floorplans are defined in .porta files adhering to a strict specification.
Each room is defined in a single line relative to one or more anchor rooms,
forming a dependency graph all the way back to the initial root room. This model
creates a 1-to-1 correspondence between the floorplan and the final SVG map it
produces, allowing porta to rapidly and deterministically generate one from
the other.
Files are read line by line: a # starts a comment that runs to the end of the
line (a # inside a quoted name is literal), and blank lines are ignored.
Any statement can be continued onto the next physical line by ending it with a backslash, separated from the last token by whitespace:
room pn "" 40x10 root
room pw "" 10x20 down-of pn
room pe "" 10x20 down-of pn align=end
room ps "" 40x10 down-of pw \
down-of pe
block passages "Passages" glyph=pn \
pn pw \
pe ps
Continued lines parse as one statement, and errors still point at the physical line of the offending token. A backslash anywhere else — mid-line, glued to a token, or followed by a comment — is an error, as is a blank (or comment-only) line inside a continued statement.
For more on the porta DSL specification, see the following documentation:
- Rooms — the
roomstatement, room positioning, auto-dimensions (?), disconnected components, and the layout resolution procedure. - Windows — interior and exterior windows, widths and offsets.
- Doors — default doors & how to modify them, and
the
doorstatement for explicitly adding non-default doors. - Blocks — the
blockstatement for joining multiplerooms into a single non-rectangular space. - Dividers — the
dividerstatement for drawing a dividing line between two members of a block. - Links — the
linkstatement for joining disconnected components into one continuous plan. - Stairs — the
stairsstatement for flights leading up or down off a floor, or between levels within one.
The porta tool
Once a valid floorplan has been written, porta draw converts it into an
SVG map in a two-step process. First, the dependency graph is traversed
and the relations in the floorplan are converted into a coordinate system.
Second, that coordinate system is used to deterministically generate an
SVG file.
porta draw plan.porta -o plan.svg
Override SVG appearance with a grouped JSON file:
porta draw plan.porta --style sepia.json -o plan.svg
{
"page": {"background": "#fff8e7", "line_color": "#332211"},
"typography": {
"text_color": {"value": "#654321", "description": "Sepia ink"}
}
}
default_style.json documents the supported settings.
Both plain values and value/description records are accepted, including mixtures.
Omitted settings retain their defaults; groups merge without replacing their other
settings. Unknown keys and invalid values produce an error. With --debug-ascii,
--style applies label settings and ignores visual settings. See
glyphs for label schemes and numbering starts.
Window and scale fills follow the background; symbols and grid use the shared line color, with separate grid opacity. Font fitting uses Palatino-based estimates, so other fonts may fit differently in the SVG viewer.
The solved coordinate system can also be viewed and debugged directly as an ASCII grid:
porta draw plan.porta --debug-ascii
A plan that can't be solved (due to overlaps, missing anchors, gaps between a room and its anchor, etc) will fail and raise an error.
Installation
porta requires Python 3.14 or newer and is published on PyPI:
pip install porta
Alternatively, install it from a checkout of this repo. Add the [dev] extra
to pull in the test and lint tools as well:
pip install -e . # editable install
pip install -e ".[dev]" # ... plus pytest, ruff, and mypy
For development with uv, set up the environment from the repository root:
uv sync --extra dev
Why porta?
porta is built to be:
- Text-based — a plan is plain text: editable in any editor, diffable, and version-controlled alongside your campaign notes. No WYSIWYG, no binary files.
- Concise — a terse syntax with strong defaults; a whole plan is a handful of short lines.
- Deterministic — no procedural generation and no randomness; the same plan always renders the same map.
- Relational — rooms are placed against other rooms, never drawn by hand or given raw coordinates.
- Spatial — the output is a flush, to-scale floor plan in tabletop conventions, not a flowchart or connectivity graph.
- Fast — placement is a single pass over a dependency graph, with no constraint solver to run.
- AI-friendly — terse, text-only, and predictable, so a language model can read and write plans reliably (this one falls out of the rest).
No existing tool hits all of these:
- Procedural and AI generators (Watabou, Inkarnate) aren't deterministic or controllable — they invent a layout instead of rendering yours.
- GUI map editors (Dungeondraft, Dungeon Scrawl) aren't text: mouse-driven, binary, and kept apart from your notes.
- Diagram tools (Mermaid, Graphviz, D2) give connectivity, not a spatial, to-scale map.
- Hand-written SVG is text, but neither concise nor relational — verbose and easy to get wrong.
Metadata
Release files for porta 1.8.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| porta-1.8.0.tar.gz | 190.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| porta-1.8.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 250.8 kB
Release files / porta-1.8.0.tar.gz
| Download URL | porta-1.8.0.tar.gz |
|---|---|
| Size | 190.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
cfc728e8966d8167bc5b6576f0affa976f7c4204a7f75405924f76ea27b0efc5
|
|
BLAKE2b-256 checksum How to use checksums |
3338796e88b4b9017b806104a9c27b773b362622857ecf67ba2aafb9a301f9d8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / porta-1.8.0-py3-none-any.whl
| Download URL | porta-1.8.0-py3-none-any.whl |
|---|---|
| Size | 60.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
041a7b300a972f16955986cbea710f71261b5e837b298d1dc247a7c9c823f12f
|
|
BLAKE2b-256 checksum How to use checksums |
9dc3223e75b5588e492cb3aa5e01da9e5f49f67fce16e567e04bfe24202f5934
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|