porta 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. porta-1.0.0/.github/workflows/ci.yml +41 -0
  2. porta-1.0.0/.github/workflows/publish.yml +25 -0
  3. porta-1.0.0/.gitignore +27 -0
  4. porta-1.0.0/CLAUDE.md +53 -0
  5. porta-1.0.0/LICENSE +21 -0
  6. porta-1.0.0/PKG-INFO +131 -0
  7. porta-1.0.0/README.md +104 -0
  8. porta-1.0.0/docs/door.md +126 -0
  9. porta-1.0.0/docs/img/adjacency.svg +43 -0
  10. porta-1.0.0/docs/img/align_default.svg +31 -0
  11. porta-1.0.0/docs/img/align_end.svg +31 -0
  12. porta-1.0.0/docs/img/align_start.svg +31 -0
  13. porta-1.0.0/docs/img/auto-flank.svg +43 -0
  14. porta-1.0.0/docs/img/auto-shift.svg +36 -0
  15. porta-1.0.0/docs/img/auto-single.svg +40 -0
  16. porta-1.0.0/docs/img/auto-union.svg +32 -0
  17. porta-1.0.0/docs/img/capstone.svg +74 -0
  18. porta-1.0.0/docs/img/corner.svg +34 -0
  19. porta-1.0.0/docs/img/door-capstone.svg +78 -0
  20. porta-1.0.0/docs/img/door-declarations.svg +38 -0
  21. porta-1.0.0/docs/img/door-multi.svg +28 -0
  22. porta-1.0.0/docs/img/door-outside.svg +29 -0
  23. porta-1.0.0/docs/img/door-overview.svg +35 -0
  24. porta-1.0.0/docs/img/door-standalone.svg +36 -0
  25. porta-1.0.0/docs/img/flank.svg +43 -0
  26. porta-1.0.0/docs/img/flank_align.svg +35 -0
  27. porta-1.0.0/docs/img/overview.svg +37 -0
  28. porta-1.0.0/docs/img/readme.svg +47 -0
  29. porta-1.0.0/docs/img/root.svg +25 -0
  30. porta-1.0.0/docs/img/shift.svg +49 -0
  31. porta-1.0.0/docs/img/span.svg +28 -0
  32. porta-1.0.0/docs/room.md +362 -0
  33. porta-1.0.0/examples/manor.porta +22 -0
  34. porta-1.0.0/pyproject.toml +74 -0
  35. porta-1.0.0/src/build_figures.py +70 -0
  36. porta-1.0.0/src/porta/__init__.py +9 -0
  37. porta-1.0.0/src/porta/cli.py +101 -0
  38. porta-1.0.0/src/porta/errors.py +60 -0
  39. porta-1.0.0/src/porta/layout.py +639 -0
  40. porta-1.0.0/src/porta/model.py +139 -0
  41. porta-1.0.0/src/porta/parser.py +350 -0
  42. porta-1.0.0/src/porta/py.typed +0 -0
  43. porta-1.0.0/src/porta/render.py +232 -0
  44. porta-1.0.0/tests/fixtures/layouts/corridor.porta +4 -0
  45. porta-1.0.0/tests/fixtures/layouts/corridor.svg +41 -0
  46. porta-1.0.0/tests/fixtures/layouts/external-door-wide.porta +2 -0
  47. porta-1.0.0/tests/fixtures/layouts/external-door-wide.svg +20 -0
  48. porta-1.0.0/tests/fixtures/layouts/external-door.porta +2 -0
  49. porta-1.0.0/tests/fixtures/layouts/external-door.svg +20 -0
  50. porta-1.0.0/tests/fixtures/layouts/match-anchor-chain.porta +3 -0
  51. porta-1.0.0/tests/fixtures/layouts/match-anchor-chain.svg +34 -0
  52. porta-1.0.0/tests/fixtures/layouts/match-anchor-corner.porta +3 -0
  53. porta-1.0.0/tests/fixtures/layouts/match-anchor-corner.svg +30 -0
  54. porta-1.0.0/tests/fixtures/layouts/match-anchor-fill.porta +3 -0
  55. porta-1.0.0/tests/fixtures/layouts/match-anchor-fill.svg +36 -0
  56. porta-1.0.0/tests/fixtures/layouts/match-anchor-height.porta +2 -0
  57. porta-1.0.0/tests/fixtures/layouts/match-anchor-height.svg +30 -0
  58. porta-1.0.0/tests/fixtures/layouts/match-anchor-shift.porta +2 -0
  59. porta-1.0.0/tests/fixtures/layouts/match-anchor-shift.svg +27 -0
  60. porta-1.0.0/tests/fixtures/layouts/match-anchor-up.porta +3 -0
  61. porta-1.0.0/tests/fixtures/layouts/match-anchor-up.svg +30 -0
  62. porta-1.0.0/tests/fixtures/layouts/match-anchor-width.porta +2 -0
  63. porta-1.0.0/tests/fixtures/layouts/match-anchor-width.svg +30 -0
  64. porta-1.0.0/tests/fixtures/layouts/same-direction-auto.porta +3 -0
  65. porta-1.0.0/tests/fixtures/layouts/same-direction-auto.svg +30 -0
  66. porta-1.0.0/tests/fixtures/layouts/same-direction.porta +3 -0
  67. porta-1.0.0/tests/fixtures/layouts/same-direction.svg +30 -0
  68. porta-1.0.0/tests/fixtures/layouts/snug-fit-auto.porta +4 -0
  69. porta-1.0.0/tests/fixtures/layouts/snug-fit-auto.svg +42 -0
  70. porta-1.0.0/tests/fixtures/layouts/snug-fit.porta +4 -0
  71. porta-1.0.0/tests/fixtures/layouts/snug-fit.svg +42 -0
  72. porta-1.0.0/tests/fixtures/layouts/three-room-incidental-door.porta +4 -0
  73. porta-1.0.0/tests/fixtures/layouts/three-room-incidental-door.svg +36 -0
  74. porta-1.0.0/tests/fixtures/layouts/three-room-shift-east.porta +3 -0
  75. porta-1.0.0/tests/fixtures/layouts/three-room-shift-east.svg +28 -0
  76. porta-1.0.0/tests/fixtures/layouts/three-room-shift-south.porta +3 -0
  77. porta-1.0.0/tests/fixtures/layouts/three-room-shift-south.svg +28 -0
  78. porta-1.0.0/tests/fixtures/layouts/three-room-square.porta +3 -0
  79. porta-1.0.0/tests/fixtures/layouts/three-room-square.svg +28 -0
  80. porta-1.0.0/tests/fixtures/layouts/two-room-door-full.porta +2 -0
  81. porta-1.0.0/tests/fixtures/layouts/two-room-door-full.svg +27 -0
  82. porta-1.0.0/tests/fixtures/layouts/two-room-door-offset.porta +2 -0
  83. porta-1.0.0/tests/fixtures/layouts/two-room-door-offset.svg +27 -0
  84. porta-1.0.0/tests/fixtures/layouts/two-room-door-up-offset.porta +2 -0
  85. porta-1.0.0/tests/fixtures/layouts/two-room-door-up-offset.svg +27 -0
  86. porta-1.0.0/tests/fixtures/layouts/two-room-door-up-wide.porta +2 -0
  87. porta-1.0.0/tests/fixtures/layouts/two-room-door-up-wide.svg +27 -0
  88. porta-1.0.0/tests/fixtures/layouts/two-room-door-wide.porta +2 -0
  89. porta-1.0.0/tests/fixtures/layouts/two-room-door-wide.svg +27 -0
  90. porta-1.0.0/tests/fixtures/layouts/two-room-down-align-end-shift.porta +2 -0
  91. porta-1.0.0/tests/fixtures/layouts/two-room-down-align-end-shift.svg +25 -0
  92. porta-1.0.0/tests/fixtures/layouts/two-room-down-align-end.porta +2 -0
  93. porta-1.0.0/tests/fixtures/layouts/two-room-down-align-end.svg +25 -0
  94. porta-1.0.0/tests/fixtures/layouts/two-room-down-overhang-end.porta +2 -0
  95. porta-1.0.0/tests/fixtures/layouts/two-room-down-overhang-end.svg +23 -0
  96. porta-1.0.0/tests/fixtures/layouts/two-room-down-overhang.porta +2 -0
  97. porta-1.0.0/tests/fixtures/layouts/two-room-down-overhang.svg +23 -0
  98. porta-1.0.0/tests/fixtures/layouts/two-room-down-shift-east.porta +2 -0
  99. porta-1.0.0/tests/fixtures/layouts/two-room-down-shift-east.svg +22 -0
  100. porta-1.0.0/tests/fixtures/layouts/two-room-down-shift-west.porta +2 -0
  101. porta-1.0.0/tests/fixtures/layouts/two-room-down-shift-west.svg +22 -0
  102. porta-1.0.0/tests/fixtures/layouts/two-room-down.porta +2 -0
  103. porta-1.0.0/tests/fixtures/layouts/two-room-down.svg +21 -0
  104. porta-1.0.0/tests/fixtures/layouts/two-room-left-align-end-shift.porta +2 -0
  105. porta-1.0.0/tests/fixtures/layouts/two-room-left-align-end-shift.svg +25 -0
  106. porta-1.0.0/tests/fixtures/layouts/two-room-left-align-end.porta +2 -0
  107. porta-1.0.0/tests/fixtures/layouts/two-room-left-align-end.svg +25 -0
  108. porta-1.0.0/tests/fixtures/layouts/two-room-left-overhang-end.porta +2 -0
  109. porta-1.0.0/tests/fixtures/layouts/two-room-left-overhang-end.svg +23 -0
  110. porta-1.0.0/tests/fixtures/layouts/two-room-left-overhang.porta +2 -0
  111. porta-1.0.0/tests/fixtures/layouts/two-room-left-overhang.svg +23 -0
  112. porta-1.0.0/tests/fixtures/layouts/two-room-left-shift-north.porta +2 -0
  113. porta-1.0.0/tests/fixtures/layouts/two-room-left-shift-north.svg +22 -0
  114. porta-1.0.0/tests/fixtures/layouts/two-room-left-shift-south.porta +2 -0
  115. porta-1.0.0/tests/fixtures/layouts/two-room-left-shift-south.svg +22 -0
  116. porta-1.0.0/tests/fixtures/layouts/two-room-left.porta +2 -0
  117. porta-1.0.0/tests/fixtures/layouts/two-room-left.svg +21 -0
  118. porta-1.0.0/tests/fixtures/layouts/two-room-no-door.porta +2 -0
  119. porta-1.0.0/tests/fixtures/layouts/two-room-no-door.svg +26 -0
  120. porta-1.0.0/tests/fixtures/layouts/two-room-right-align-end-shift.porta +2 -0
  121. porta-1.0.0/tests/fixtures/layouts/two-room-right-align-end-shift.svg +25 -0
  122. porta-1.0.0/tests/fixtures/layouts/two-room-right-align-end.porta +2 -0
  123. porta-1.0.0/tests/fixtures/layouts/two-room-right-align-end.svg +25 -0
  124. porta-1.0.0/tests/fixtures/layouts/two-room-right-overhang-end.porta +2 -0
  125. porta-1.0.0/tests/fixtures/layouts/two-room-right-overhang-end.svg +23 -0
  126. porta-1.0.0/tests/fixtures/layouts/two-room-right-overhang.porta +2 -0
  127. porta-1.0.0/tests/fixtures/layouts/two-room-right-overhang.svg +23 -0
  128. porta-1.0.0/tests/fixtures/layouts/two-room-right-shift-north.porta +2 -0
  129. porta-1.0.0/tests/fixtures/layouts/two-room-right-shift-north.svg +22 -0
  130. porta-1.0.0/tests/fixtures/layouts/two-room-right-shift-south.porta +2 -0
  131. porta-1.0.0/tests/fixtures/layouts/two-room-right-shift-south.svg +22 -0
  132. porta-1.0.0/tests/fixtures/layouts/two-room-right.porta +2 -0
  133. porta-1.0.0/tests/fixtures/layouts/two-room-right.svg +21 -0
  134. porta-1.0.0/tests/fixtures/layouts/two-room-two-doors.porta +3 -0
  135. porta-1.0.0/tests/fixtures/layouts/two-room-two-doors.svg +32 -0
  136. porta-1.0.0/tests/fixtures/layouts/two-room-up-align-end-shift.porta +2 -0
  137. porta-1.0.0/tests/fixtures/layouts/two-room-up-align-end-shift.svg +25 -0
  138. porta-1.0.0/tests/fixtures/layouts/two-room-up-align-end.porta +2 -0
  139. porta-1.0.0/tests/fixtures/layouts/two-room-up-align-end.svg +25 -0
  140. porta-1.0.0/tests/fixtures/layouts/two-room-up-overhang-end.porta +2 -0
  141. porta-1.0.0/tests/fixtures/layouts/two-room-up-overhang-end.svg +23 -0
  142. porta-1.0.0/tests/fixtures/layouts/two-room-up-overhang.porta +2 -0
  143. porta-1.0.0/tests/fixtures/layouts/two-room-up-overhang.svg +23 -0
  144. porta-1.0.0/tests/fixtures/layouts/two-room-up-shift-east.porta +2 -0
  145. porta-1.0.0/tests/fixtures/layouts/two-room-up-shift-east.svg +22 -0
  146. porta-1.0.0/tests/fixtures/layouts/two-room-up-shift-west.porta +2 -0
  147. porta-1.0.0/tests/fixtures/layouts/two-room-up-shift-west.svg +22 -0
  148. porta-1.0.0/tests/fixtures/layouts/two-room-up.porta +2 -0
  149. porta-1.0.0/tests/fixtures/layouts/two-room-up.svg +21 -0
  150. porta-1.0.0/tests/fixtures/layouts/union-horizontal.porta +3 -0
  151. porta-1.0.0/tests/fixtures/layouts/union-horizontal.svg +30 -0
  152. porta-1.0.0/tests/fixtures/manor.svg +125 -0
  153. porta-1.0.0/tests/test_cli.py +126 -0
  154. porta-1.0.0/tests/test_layout.py +675 -0
  155. porta-1.0.0/tests/test_model.py +25 -0
  156. porta-1.0.0/tests/test_parser.py +371 -0
  157. porta-1.0.0/tests/test_render.py +303 -0
@@ -0,0 +1,41 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ concurrency:
9
+ group: ${{ github.workflow }}-${{ github.ref }}
10
+ cancel-in-progress: true
11
+
12
+ jobs:
13
+ check:
14
+ name: ${{ matrix.name }}
15
+ runs-on: ubuntu-latest
16
+ strategy:
17
+ fail-fast: false
18
+ matrix:
19
+ include:
20
+ - { name: test, cmd: "pytest" }
21
+ - { name: lint, cmd: "ruff check ." }
22
+ - { name: format, cmd: "ruff format --check ." }
23
+ - { name: types, cmd: "mypy" }
24
+ - { name: docs, cmd: "python src/build_figures.py && git diff --exit-code" }
25
+ steps:
26
+ - uses: actions/checkout@v4
27
+ - uses: astral-sh/setup-uv@v6
28
+ with:
29
+ python-version: "3.14"
30
+ enable-cache: true
31
+ - run: uv run --extra dev ${{ matrix.cmd }}
32
+
33
+ build:
34
+ runs-on: ubuntu-latest
35
+ steps:
36
+ - uses: actions/checkout@v4
37
+ - uses: astral-sh/setup-uv@v6
38
+ with:
39
+ python-version: "3.14"
40
+ enable-cache: true
41
+ - run: uv build
@@ -0,0 +1,25 @@
1
+ name: publish
2
+
3
+ # Publish to PyPI via Trusted Publishing (OIDC) — no stored token.
4
+ # Fires when a GitHub release is published; can also be run by hand
5
+ # (Actions -> publish -> Run workflow) to publish an already-made release.
6
+ on:
7
+ release:
8
+ types: [published]
9
+ workflow_dispatch:
10
+
11
+ permissions:
12
+ contents: read
13
+
14
+ jobs:
15
+ publish:
16
+ runs-on: ubuntu-latest
17
+ environment: pypi
18
+ permissions:
19
+ id-token: write # required for Trusted Publishing
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+ - uses: astral-sh/setup-uv@v6
23
+ - run: uv run --extra dev pytest
24
+ - run: uv build
25
+ - run: uv publish
porta-1.0.0/.gitignore ADDED
@@ -0,0 +1,27 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ build/
6
+ dist/
7
+ .eggs/
8
+
9
+ # Virtual envs / tooling
10
+ .venv/
11
+ .uv/
12
+ uv.lock
13
+
14
+ # Test / coverage
15
+ .pytest_cache/
16
+ .coverage
17
+ htmlcov/
18
+
19
+ # Editors / OS
20
+ .DS_Store
21
+ .idea/
22
+ .vscode/
23
+
24
+ # Rendered output (examples may emit these locally)
25
+ *.svg
26
+ !docs/**/*.svg
27
+ !tests/fixtures/**/*.svg
porta-1.0.0/CLAUDE.md ADDED
@@ -0,0 +1,53 @@
1
+ # porta
2
+
3
+ A standalone Python package: a relational DSL for authoring D&D floor plans and
4
+ rendering them to SVG. CLI-driven, zero runtime dependencies.
5
+
6
+ The user-facing reference is [`docs/room.md`](docs/room.md) (rooms, placement,
7
+ auto-dimensions, validation) and [`docs/door.md`](docs/door.md) (doors); for
8
+ exact behaviour the code is the source of truth. The body below is just working
9
+ conventions.
10
+
11
+ ## Orientation
12
+
13
+ - **What it does** — parse a `.porta` spec (rooms + relational placement) →
14
+ resolve geometry by DAG propagation → render SVG.
15
+ - **Package layout** — `src/porta/`: `cli.py` (argparse entry), `parser.py`
16
+ (`.porta` → model), `model.py` (dataclasses), `layout.py` (relations →
17
+ coordinates, validation), `render.py` (model → SVG/ASCII), `errors.py` (the
18
+ `PortaError` hierarchy, each carrying a source line). Tests in `tests/`.
19
+ `src/build_figures.py` regenerates the doc figures (a dev tool, not shipped).
20
+ - **Consumer** — the `isles` D&D vault (sibling repo) installs porta via
21
+ `uv add --editable ../porta`. The `.porta` sources and rendered SVGs live in
22
+ *that* repo, not here. porta knows nothing about isles.
23
+
24
+ ## Workflow
25
+
26
+ - Run with **`uv`**: `uv run porta draw <in>.porta -o <out>.svg`.
27
+ - Gates (all run in CI on push/PR; run them locally before pushing):
28
+ `uv run --extra dev pytest` · `ruff check .` · `ruff format --check .` ·
29
+ `mypy` · and a `docs` job that rebuilds the figures and fails on any drift.
30
+ - Doc figures (`docs/img/`) are generated from fenced ` ```porta ` examples
31
+ (those with a path on the fence) in `README.md` and `docs/*.md` by
32
+ `src/build_figures.py` — don't hand-edit them, and keep the examples valid
33
+ (every one is solved on each build).
34
+ - Use **`python`**, never `python3`.
35
+ - Use **relative paths** in shell/git commands.
36
+ - When handing the user a path to open, avoid spaces in it.
37
+
38
+ ## Conventions
39
+
40
+ - Modern type hints (`list[str]`, `X | None`); dataclasses for the model.
41
+ - Google-style docstrings on public functions.
42
+ - Keep the runtime dependency-free: SVG via stdlib string/XML templating.
43
+ - Small, pure, testable functions — especially in `layout.py`, where geometry
44
+ resolution and overlap detection should be unit-tested on tiny inputs.
45
+ - Tests: prefer `pytest.mark.parametrize` for families of similar cases (valid
46
+ vs. invalid inputs, error conditions, geometry fixtures) over copy-pasted
47
+ near-identical test functions. Once a test is parametrized, adding a case is
48
+ one line — so be liberal and keep coverage comprehensive (give each case a
49
+ readable `id`). Reserve standalone test functions for genuinely distinct
50
+ assertions.
51
+ - Tests mirror the source: one `tests/test_<module>.py` per `src/porta/<module>.py`
52
+ (e.g. `test_layout.py` covers all of `layout.py`). Error types in `errors.py`
53
+ are tested where they're raised, not in a separate file.
porta-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 William J. Bradshaw
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
porta-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,131 @@
1
+ Metadata-Version: 2.4
2
+ Name: porta
3
+ Version: 1.0.0
4
+ Summary: A relational DSL for authoring and rendering SVG floorplans.
5
+ Project-URL: Homepage, https://github.com/willbradshaw/porta
6
+ Project-URL: Repository, https://github.com/willbradshaw/porta
7
+ Project-URL: Issues, https://github.com/willbradshaw/porta/issues
8
+ Author: Will Bradshaw
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: cartography,dnd,dsl,floorplan,svg,ttrpg
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: End Users/Desktop
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Topic :: Games/Entertainment :: Role-Playing
19
+ Classifier: Topic :: Multimedia :: Graphics
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.14
22
+ Provides-Extra: dev
23
+ Requires-Dist: mypy>=1.11; extra == 'dev'
24
+ Requires-Dist: pytest>=8; extra == 'dev'
25
+ Requires-Dist: ruff>=0.6; extra == 'dev'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # `porta`
29
+
30
+ `porta` is two things:
31
+
32
+ 1. A **DSL** for concisely defining a relational floorplan; and
33
+ 2. A **software package** for deterministically converting a floorplan into
34
+ a clean SVG map.
35
+
36
+ ```porta docs/img/readme.svg
37
+ room hall "Great Hall" 40x20 root
38
+ room parlour "Parlour" 20x20 left-of hall
39
+ room kitchen "Kitchen" 20x20 right-of hall
40
+ room study "Study" ?x20 down-of parlour
41
+ ```
42
+
43
+ <img alt="Four rooms rendered to an SVG floor plan" src="docs/img/readme.svg" width="60%">
44
+
45
+ ## The `porta` DSL
46
+
47
+ `porta` floorplans are defined in `.porta` files adhering to a strict specification.
48
+ Each room is defined in a single line relative to one or more anchor rooms,
49
+ forming a dependency graph all the way back to the initial root room. This model
50
+ creates a 1-to-1 correspondence between the floorplan and the final SVG map it
51
+ produces, allowing `porta` to rapidly and deterministically generate one from
52
+ the other.
53
+
54
+ Files are read line by line: a `#` starts a comment that runs to the end of the
55
+ line (a `#` inside a quoted name is literal), and blank lines are ignored.
56
+
57
+ For more on the `porta` DSL specification, see the following documentation:
58
+
59
+ - [**Rooms**](docs/room.md) — the `room` statement, room positioning,
60
+ auto-dimensions (`?`), and the layout resolution procedure.
61
+ - [**Doors**](docs/door.md) — default doors & how to modify them, and
62
+ the `door` statement for explicitly adding non-default doors.
63
+
64
+ ## The `porta` tool
65
+
66
+ Once a valid floorplan has been written, `porta draw` converts it into an
67
+ SVG map in a two-step process. First, the dependency graph is traversed
68
+ and the relations in the floorplan are converted into a coordinate system.
69
+ Second, that coordinate system is used to deterministically generate an
70
+ SVG file.
71
+
72
+ ```sh
73
+ porta draw plan.porta -o plan.svg
74
+ ```
75
+
76
+ The solved coordinate system can also be viewed and debugged directly
77
+ as an ASCII grid:
78
+
79
+ ```sh
80
+ porta draw plan.porta --debug-ascii
81
+ ```
82
+
83
+ A plan that can't be solved (due to overlaps, missing anchors, gaps
84
+ between a room and its anchor, etc) will fail and raise an error.
85
+
86
+ ## Installation
87
+
88
+ `porta` is published on PyPI and can be installed with `pip`:
89
+
90
+ ```sh
91
+ pip install porta
92
+ ```
93
+
94
+ Alternatively, install it from a checkout of this repo. Add the `[dev]` extra
95
+ to pull in the test and lint tools as well:
96
+
97
+ ```sh
98
+ pip install -e . # editable install
99
+ pip install -e ".[dev]" # ... plus pytest, ruff, and mypy
100
+ ```
101
+
102
+ ## Why `porta`?
103
+
104
+ `porta` is built to be:
105
+
106
+ - **Text-based** — a plan is plain text: editable in any editor, diffable, and
107
+ version-controlled alongside your campaign notes. No WYSIWYG, no binary files.
108
+ - **Concise** — a terse syntax with strong defaults; a whole plan is a handful
109
+ of short lines.
110
+ - **Deterministic** — no procedural generation and no randomness; the same plan
111
+ always renders the same map.
112
+ - **Relational** — rooms are placed against other rooms, never drawn by hand or
113
+ given raw coordinates.
114
+ - **Spatial** — the output is a flush, to-scale floor plan in tabletop
115
+ conventions, not a flowchart or connectivity graph.
116
+ - **Fast** — placement is a single pass over a dependency graph, with no
117
+ constraint solver to run.
118
+ - **AI-friendly** — terse, text-only, and predictable, so a language model can
119
+ read and write plans reliably (this one falls out of the rest).
120
+
121
+ No existing tool hits all of these:
122
+
123
+ - **Procedural and AI generators** (Watabou, Inkarnate) aren't deterministic or
124
+ controllable — they invent a layout instead of rendering yours.
125
+ - **GUI map editors** (Dungeondraft, Dungeon Scrawl) aren't text: mouse-driven,
126
+ binary, and kept apart from your notes.
127
+ - **Diagram tools** (Mermaid, Graphviz, D2) give connectivity, not a spatial,
128
+ to-scale map.
129
+ - **Hand-written SVG** is text, but neither concise nor relational — verbose and
130
+ easy to get wrong.
131
+
porta-1.0.0/README.md ADDED
@@ -0,0 +1,104 @@
1
+ # `porta`
2
+
3
+ `porta` is two things:
4
+
5
+ 1. A **DSL** for concisely defining a relational floorplan; and
6
+ 2. A **software package** for deterministically converting a floorplan into
7
+ a clean SVG map.
8
+
9
+ ```porta docs/img/readme.svg
10
+ room hall "Great Hall" 40x20 root
11
+ room parlour "Parlour" 20x20 left-of hall
12
+ room kitchen "Kitchen" 20x20 right-of hall
13
+ room study "Study" ?x20 down-of parlour
14
+ ```
15
+
16
+ <img alt="Four rooms rendered to an SVG floor plan" src="docs/img/readme.svg" width="60%">
17
+
18
+ ## The `porta` DSL
19
+
20
+ `porta` floorplans are defined in `.porta` files adhering to a strict specification.
21
+ Each room is defined in a single line relative to one or more anchor rooms,
22
+ forming a dependency graph all the way back to the initial root room. This model
23
+ creates a 1-to-1 correspondence between the floorplan and the final SVG map it
24
+ produces, allowing `porta` to rapidly and deterministically generate one from
25
+ the other.
26
+
27
+ Files are read line by line: a `#` starts a comment that runs to the end of the
28
+ line (a `#` inside a quoted name is literal), and blank lines are ignored.
29
+
30
+ For more on the `porta` DSL specification, see the following documentation:
31
+
32
+ - [**Rooms**](docs/room.md) — the `room` statement, room positioning,
33
+ auto-dimensions (`?`), and the layout resolution procedure.
34
+ - [**Doors**](docs/door.md) — default doors & how to modify them, and
35
+ the `door` statement for explicitly adding non-default doors.
36
+
37
+ ## The `porta` tool
38
+
39
+ Once a valid floorplan has been written, `porta draw` converts it into an
40
+ SVG map in a two-step process. First, the dependency graph is traversed
41
+ and the relations in the floorplan are converted into a coordinate system.
42
+ Second, that coordinate system is used to deterministically generate an
43
+ SVG file.
44
+
45
+ ```sh
46
+ porta draw plan.porta -o plan.svg
47
+ ```
48
+
49
+ The solved coordinate system can also be viewed and debugged directly
50
+ as an ASCII grid:
51
+
52
+ ```sh
53
+ porta draw plan.porta --debug-ascii
54
+ ```
55
+
56
+ A plan that can't be solved (due to overlaps, missing anchors, gaps
57
+ between a room and its anchor, etc) will fail and raise an error.
58
+
59
+ ## Installation
60
+
61
+ `porta` is published on PyPI and can be installed with `pip`:
62
+
63
+ ```sh
64
+ pip install porta
65
+ ```
66
+
67
+ Alternatively, install it from a checkout of this repo. Add the `[dev]` extra
68
+ to pull in the test and lint tools as well:
69
+
70
+ ```sh
71
+ pip install -e . # editable install
72
+ pip install -e ".[dev]" # ... plus pytest, ruff, and mypy
73
+ ```
74
+
75
+ ## Why `porta`?
76
+
77
+ `porta` is built to be:
78
+
79
+ - **Text-based** — a plan is plain text: editable in any editor, diffable, and
80
+ version-controlled alongside your campaign notes. No WYSIWYG, no binary files.
81
+ - **Concise** — a terse syntax with strong defaults; a whole plan is a handful
82
+ of short lines.
83
+ - **Deterministic** — no procedural generation and no randomness; the same plan
84
+ always renders the same map.
85
+ - **Relational** — rooms are placed against other rooms, never drawn by hand or
86
+ given raw coordinates.
87
+ - **Spatial** — the output is a flush, to-scale floor plan in tabletop
88
+ conventions, not a flowchart or connectivity graph.
89
+ - **Fast** — placement is a single pass over a dependency graph, with no
90
+ constraint solver to run.
91
+ - **AI-friendly** — terse, text-only, and predictable, so a language model can
92
+ read and write plans reliably (this one falls out of the rest).
93
+
94
+ No existing tool hits all of these:
95
+
96
+ - **Procedural and AI generators** (Watabou, Inkarnate) aren't deterministic or
97
+ controllable — they invent a layout instead of rendering yours.
98
+ - **GUI map editors** (Dungeondraft, Dungeon Scrawl) aren't text: mouse-driven,
99
+ binary, and kept apart from your notes.
100
+ - **Diagram tools** (Mermaid, Graphviz, D2) give connectivity, not a spatial,
101
+ to-scale map.
102
+ - **Hand-written SVG** is text, but neither concise nor relational — verbose and
103
+ easy to get wrong.
104
+
@@ -0,0 +1,126 @@
1
+ # Doors
2
+
3
+ By default, `porta` draws a **door** on every wall between a declared
4
+ [room](room.md) and its anchor. Explicit [door declarations](#door-declarations)
5
+ and [statements](#the-door-statement) are needed only to change a door from
6
+ its default settings (remove it, resize it, move it) or to add an additional door
7
+ in a non-default location.
8
+
9
+ > [!NOTE]
10
+ > Doors are drawn as thick black marks straddling the walls between
11
+ > the rooms they connect. They only appear in rendered SVG; the
12
+ > ASCII debug view (`porta draw <plan>.porta --debug-ascii`)
13
+ > shows the room layout without them.
14
+
15
+ ## Default doors
16
+
17
+ Every [relation](room.md#relations) connecting two rooms is given a
18
+ door by default. These default doors are 5 feet wide and positioned
19
+ as close to the centre of the wall as possible. If they cannot be placed
20
+ fully centrally due to the 5-foot wall grid, they are positioned
21
+ immediately above (for vertical walls) or to the left of the center
22
+ (for horizontal walls).
23
+
24
+ ```porta img/door-overview.svg
25
+ room hall "Hall" 20x20 root
26
+ room kitchen "Kitchen" 20x20 right-of hall
27
+ room study "Study" 20x20 down-of hall
28
+ ```
29
+
30
+ <img alt="Three rooms with a default door on each shared wall" src="img/door-overview.svg" width="70%">
31
+
32
+ ## Door declarations
33
+
34
+ The door drawn by a [relation](room.md#relations) can be modified by
35
+ a **door declaration** added to the end of that relation. There are two
36
+ types of door declaration:
37
+
38
+ - A `no-door` declaration suppresses the default door on that relation.
39
+ - A `door*` declaration changes the **width** and/or **offset** of the
40
+ door: `door=W` sets the door width to `W` feet, `door@O` sets the
41
+ start of the door to `O` feet from the start of the wall, and
42
+ `door=W@O` sets both.
43
+
44
+ ```porta img/door-declarations.svg
45
+ room r "Root" 20x20 root
46
+ room a "Room A" 20x20 right-of r no-door
47
+ room b "Room B" 20x20 down-of r door=10
48
+ room c "Room C" 20x20 right-of b door@15
49
+ ```
50
+
51
+ <img alt="Four rooms with various modifications to their doors" src="img/door-declarations.svg" width="70%">
52
+
53
+ ## The `door` statement
54
+
55
+ Some doors aren't tied to a positioning relation. A standalone **`door`
56
+ statement**, on its own line, adds one.
57
+
58
+ ### Between two rooms: `door <a> <b>`
59
+
60
+ Two rooms can share a wall without either being the other's anchor. A
61
+ `door` statement connects them, following the same width and offset
62
+ syntax as [door declarations](#door-declarations):
63
+
64
+ ```porta img/door-standalone.svg
65
+ room hall "Hall" 40x20 root
66
+ room east "East" 20x20 down-of hall
67
+ room west "West" 20x20 down-of hall shift=20
68
+ door=10@0 east west
69
+ ```
70
+
71
+ <img alt="Two rooms below a hall, joined by a standalone door" src="img/door-standalone.svg" width="70%">
72
+
73
+ `door` statements can also be used to add additional doors to walls that
74
+ already have them, as long as the doors do not overlap:
75
+
76
+ ```porta img/door-multi.svg
77
+ room r "Root" 20x20 root
78
+ room a "Room A" 20x20 right-of r door@5
79
+ door@15 r a
80
+ ```
81
+
82
+ <img alt="A pair of rooms linked by two doors" src="img/door-multi.svg" width="70%">
83
+
84
+ ### To the outside: `door <room> outside <side>`
85
+
86
+ An **external** door opens a room onto the outside, on a named side — `up`,
87
+ `down`, `left`, or `right`:
88
+
89
+ ```porta img/door-outside.svg
90
+ room a "Room A" 20x20 root
91
+ room b "Room B" 20x20 right-of a
92
+ door a outside left
93
+ door b outside down
94
+ ```
95
+
96
+ <img alt="Two rooms with external doors on their outer walls" src="img/door-outside.svg" width="70%">
97
+
98
+ ## Invalid doors
99
+
100
+ `porta` rejects a door it can't place:
101
+
102
+ - An explicit `door` on a relation whose rooms share no wall (they meet only at
103
+ a corner).
104
+ - A door wider than its wall, or pushed past the wall's end by its offset.
105
+ - Two doors that overlap on the same wall.
106
+ - An external door on a side that isn't exterior — a room sits flush there.
107
+
108
+ ## Putting it together
109
+
110
+ ```porta img/door-capstone.svg
111
+ room hall "Hall" 20x40 root
112
+ room drawing "Drawing Room" 30x40 left-of hall door=20
113
+ room dining "Dining Room" 30x20 right-of hall door@10
114
+ room kitchen "Kitchen" 30x20 right-of hall align=end
115
+ room pantry "Pantry" 10x? right-of dining right-of kitchen
116
+ room porch "Porch" 20x10 down-of hall
117
+ room cloak "Cloakroom" 10x10 down-of drawing left-of porch
118
+ room scullery "Scullery" 15x10 down-of kitchen align=end shift=-5
119
+ room passage "Passage" ?x10 right-of porch left-of scullery
120
+ door=10@5 dining kitchen
121
+ door porch outside down
122
+ door dining outside up
123
+ door drawing outside left
124
+ ```
125
+
126
+ <img alt="The manor ground floor with its doors controlled" src="img/door-capstone.svg" width="70%">
@@ -0,0 +1,43 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1100" height="1268" viewBox="-50 -25 110 126.8">
2
+ <rect x="-50" y="-25" width="110" height="126.8" fill="#e0e0e0" />
3
+ <g stroke="#bbb" stroke-width="0.15">
4
+ <line x1="-15" y1="-15" x2="-15" y2="25" />
5
+ <line x1="-10" y1="-15" x2="-10" y2="25" />
6
+ <line x1="-5" y1="-15" x2="-5" y2="25" />
7
+ <line x1="0" y1="-15" x2="0" y2="25" />
8
+ <line x1="5" y1="-15" x2="5" y2="25" />
9
+ <line x1="10" y1="-15" x2="10" y2="25" />
10
+ <line x1="15" y1="-15" x2="15" y2="25" />
11
+ <line x1="20" y1="-15" x2="20" y2="25" />
12
+ <line x1="25" y1="-15" x2="25" y2="25" />
13
+ <line x1="-15" y1="-15" x2="25" y2="-15" />
14
+ <line x1="-15" y1="-10" x2="25" y2="-10" />
15
+ <line x1="-15" y1="-5" x2="25" y2="-5" />
16
+ <line x1="-15" y1="0" x2="25" y2="0" />
17
+ <line x1="-15" y1="5" x2="25" y2="5" />
18
+ <line x1="-15" y1="10" x2="25" y2="10" />
19
+ <line x1="-15" y1="15" x2="25" y2="15" />
20
+ <line x1="-15" y1="20" x2="25" y2="20" />
21
+ <line x1="-15" y1="25" x2="25" y2="25" />
22
+ </g>
23
+ <rect data-room="a" x="-15" y="0" width="15" height="10" fill="none" stroke="black" stroke-width="0.5" />
24
+ <text data-room="a" x="-7.5" y="5" text-anchor="middle" dominant-baseline="central" font-size="6">A</text>
25
+ <rect data-room="b" x="10" y="0" width="15" height="10" fill="none" stroke="black" stroke-width="0.5" />
26
+ <text data-room="b" x="17.5" y="5" text-anchor="middle" dominant-baseline="central" font-size="6">B</text>
27
+ <rect data-room="c" x="0" y="-15" width="10" height="15" fill="none" stroke="black" stroke-width="0.5" />
28
+ <text data-room="c" x="5" y="-7.5" text-anchor="middle" dominant-baseline="central" font-size="6">C</text>
29
+ <rect data-room="d" x="0" y="10" width="10" height="15" fill="none" stroke="black" stroke-width="0.5" />
30
+ <text data-room="d" x="5" y="17.5" text-anchor="middle" dominant-baseline="central" font-size="6">D</text>
31
+ <rect data-room="r" x="0" y="0" width="10" height="10" fill="none" stroke="black" stroke-width="0.5" />
32
+ <text data-room="r" x="5" y="5" text-anchor="middle" dominant-baseline="central" font-size="6">R</text>
33
+ <line class="door" x1="0" y1="0" x2="0" y2="5" stroke="black" stroke-width="1.5" />
34
+ <line class="door" x1="0" y1="0" x2="5" y2="0" stroke="black" stroke-width="1.5" />
35
+ <line class="door" x1="0" y1="10" x2="5" y2="10" stroke="black" stroke-width="1.5" />
36
+ <line class="door" x1="10" y1="0" x2="10" y2="5" stroke="black" stroke-width="1.5" />
37
+ <text class="scale" x="-40" y="44.2" font-size="6">1 square = 5 ft</text>
38
+ <text class="key" x="-40" y="53.8" font-size="6">A Left room (15x10 ft)</text>
39
+ <text class="key" x="-40" y="63.4" font-size="6">B Right room (15x10 ft)</text>
40
+ <text class="key" x="-40" y="73" font-size="6">C Up room (10x15 ft)</text>
41
+ <text class="key" x="-40" y="82.6" font-size="6">D Down room (10x15 ft)</text>
42
+ <text class="key" x="-40" y="92.2" font-size="6">R Root room (10x10 ft)</text>
43
+ </svg>
@@ -0,0 +1,31 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1100" height="876" viewBox="-45 -10 110 87.6">
2
+ <rect x="-45" y="-10" width="110" height="87.6" fill="#e0e0e0" />
3
+ <g stroke="#bbb" stroke-width="0.15">
4
+ <line x1="-10" y1="0" x2="-10" y2="20" />
5
+ <line x1="-5" y1="0" x2="-5" y2="20" />
6
+ <line x1="0" y1="0" x2="0" y2="20" />
7
+ <line x1="5" y1="0" x2="5" y2="20" />
8
+ <line x1="10" y1="0" x2="10" y2="20" />
9
+ <line x1="15" y1="0" x2="15" y2="20" />
10
+ <line x1="20" y1="0" x2="20" y2="20" />
11
+ <line x1="25" y1="0" x2="25" y2="20" />
12
+ <line x1="30" y1="0" x2="30" y2="20" />
13
+ <line x1="-10" y1="0" x2="30" y2="0" />
14
+ <line x1="-10" y1="5" x2="30" y2="5" />
15
+ <line x1="-10" y1="10" x2="30" y2="10" />
16
+ <line x1="-10" y1="15" x2="30" y2="15" />
17
+ <line x1="-10" y1="20" x2="30" y2="20" />
18
+ </g>
19
+ <rect data-room="a" x="-10" y="0" width="10" height="10" fill="none" stroke="black" stroke-width="0.5" />
20
+ <text data-room="a" x="-5" y="5" text-anchor="middle" dominant-baseline="central" font-size="6">A</text>
21
+ <rect data-room="b" x="20" y="0" width="10" height="10" fill="none" stroke="black" stroke-width="0.5" />
22
+ <text data-room="b" x="25" y="5" text-anchor="middle" dominant-baseline="central" font-size="6">B</text>
23
+ <rect data-room="r" x="0" y="0" width="20" height="20" fill="none" stroke="black" stroke-width="0.5" />
24
+ <text data-room="r" x="10" y="10" text-anchor="middle" dominant-baseline="central" font-size="12">R</text>
25
+ <line class="door" x1="0" y1="0" x2="0" y2="5" stroke="black" stroke-width="1.5" />
26
+ <line class="door" x1="20" y1="0" x2="20" y2="5" stroke="black" stroke-width="1.5" />
27
+ <text class="scale" x="-35" y="39.2" font-size="6">1 square = 5 ft</text>
28
+ <text class="key" x="-35" y="48.8" font-size="6">A Left room (10x10 ft)</text>
29
+ <text class="key" x="-35" y="58.4" font-size="6">B Right room (10x10 ft)</text>
30
+ <text class="key" x="-35" y="68" font-size="6">R Root room (20x20 ft)</text>
31
+ </svg>
@@ -0,0 +1,31 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1100" height="876" viewBox="-45 -10 110 87.6">
2
+ <rect x="-45" y="-10" width="110" height="87.6" fill="#e0e0e0" />
3
+ <g stroke="#bbb" stroke-width="0.15">
4
+ <line x1="-10" y1="0" x2="-10" y2="20" />
5
+ <line x1="-5" y1="0" x2="-5" y2="20" />
6
+ <line x1="0" y1="0" x2="0" y2="20" />
7
+ <line x1="5" y1="0" x2="5" y2="20" />
8
+ <line x1="10" y1="0" x2="10" y2="20" />
9
+ <line x1="15" y1="0" x2="15" y2="20" />
10
+ <line x1="20" y1="0" x2="20" y2="20" />
11
+ <line x1="25" y1="0" x2="25" y2="20" />
12
+ <line x1="30" y1="0" x2="30" y2="20" />
13
+ <line x1="-10" y1="0" x2="30" y2="0" />
14
+ <line x1="-10" y1="5" x2="30" y2="5" />
15
+ <line x1="-10" y1="10" x2="30" y2="10" />
16
+ <line x1="-10" y1="15" x2="30" y2="15" />
17
+ <line x1="-10" y1="20" x2="30" y2="20" />
18
+ </g>
19
+ <rect data-room="a" x="-10" y="10" width="10" height="10" fill="none" stroke="black" stroke-width="0.5" />
20
+ <text data-room="a" x="-5" y="15" text-anchor="middle" dominant-baseline="central" font-size="6">A</text>
21
+ <rect data-room="b" x="20" y="10" width="10" height="10" fill="none" stroke="black" stroke-width="0.5" />
22
+ <text data-room="b" x="25" y="15" text-anchor="middle" dominant-baseline="central" font-size="6">B</text>
23
+ <rect data-room="r" x="0" y="0" width="20" height="20" fill="none" stroke="black" stroke-width="0.5" />
24
+ <text data-room="r" x="10" y="10" text-anchor="middle" dominant-baseline="central" font-size="12">R</text>
25
+ <line class="door" x1="0" y1="10" x2="0" y2="15" stroke="black" stroke-width="1.5" />
26
+ <line class="door" x1="20" y1="10" x2="20" y2="15" stroke="black" stroke-width="1.5" />
27
+ <text class="scale" x="-35" y="39.2" font-size="6">1 square = 5 ft</text>
28
+ <text class="key" x="-35" y="48.8" font-size="6">A Left room (10x10 ft)</text>
29
+ <text class="key" x="-35" y="58.4" font-size="6">B Right room (10x10 ft)</text>
30
+ <text class="key" x="-35" y="68" font-size="6">R Root room (20x20 ft)</text>
31
+ </svg>