# PentaTile > Lightweight dual-grid autotiling addon for Godot 4.6 PentaTile is a lightweight dual-grid autotiling addon for Godot 4.6+. Users paint with the native TileMapLayer set_cell / erase_cell API; the addon dispatches those logic cells into visual cells through _update_cells. A pluggable PentaTileLayout resource defines mask topology and slot resolution, with native support for Penta, DualGrid16, Wang2Edge, Wang2Corner, Minimal3x3, Blob47Godot, and PixelLab top-down/side-scroller layouts. # Getting Started # Quickstart PentaTile is a Godot 4.6 addon centered on one node: `PentaTileMapLayer`. You paint with Godot's normal `set_cell()` and `erase_cell()` APIs; the layer generates autotiled visuals through its `layout` Resource. ## Fast path 1. Copy `addons/penta_tile/` into a Godot 4.6 project. 1. Enable the plugin in **Project > Project Settings > Plugins**. 1. Add a `PentaTileMapLayer` node to a scene. 1. Pick a `PentaTileLayout` Resource in the `layout` property. 1. Leave `tile_set` empty to use the layout's bundled fallback art, or assign your own `TileSet` that matches the selected layout. 1. Paint with Godot's TileMap tools or call `set_cell()` from code. For the shipped example, open `res://addons/penta_tile/demo/penta_tile_demo.tscn`. ## What to read next - [Installation](https://shilo.github.io/PentaTile/installation/index.md) for release-zip setup. - [Layouts overview](https://shilo.github.io/PentaTile/layouts/index.md) to choose the atlas convention that matches your art. - [What is a Penta tileset?](https://shilo.github.io/PentaTile/penta-tileset/index.md) for the 5-archetype format. - [Authoring Custom Layouts](https://shilo.github.io/PentaTile/custom-layouts/index.md) if you need a new convention. - [LLM Docs](https://shilo.github.io/PentaTile/llm-docs/index.md) for agent-friendly single-file docs. ## Local checks The test suite lives outside the addon package: ``` .\tests\run_tests.ps1 -NoPause ``` On Linux or CI: ``` bash tests/run_tests.sh ``` # Installation ## From a GitHub release Download the release zip and copy its `addons/penta_tile/` folder into your Godot project. The release zip intentionally contains only the addon package; repo tests, planning files, and docs-site sources are not included. ## From this repository Copy or symlink `addons/penta_tile/` into a Godot 4.6 project, then enable the plugin. The root `tests/` directory is for repository verification and does not need to be copied into a game project. ## Build these docs Install the docs dependency in your Python environment: ``` python -m pip install -r requirements-docs.txt mkdocs serve ``` To check the static site: ``` mkdocs build --strict ``` # Layouts # Layouts Overview PentaTile ships eight built-in layouts. A layout owns two pieces of behavior: how a painted logic cell samples neighbors into a mask, and how that mask maps to an atlas tile. | Layout | Class | Grid | Use when | | ---------------------- | ------------------------------------- | -------------- | ----------------------------------------------------------- | | Penta | `PentaTileLayoutPenta` | 1-5 tile strip | You want the signature low-tile-count Penta authoring flow. | | DualGrid 16 | `PentaTileLayoutDualGrid16` | 4x4 | Your art is a 16-tile dual-grid corner-mask atlas. | | Wang 2-Edge | `PentaTileLayoutWang2Edge` | 4x4 | Your art is a 16-state cardinal-edge Wang atlas. | | Wang 2-Corner | `PentaTileLayoutWang2Corner` | 4x4 | Your art uses CR31 corner naming. | | Minimal 3x3 | `PentaTileLayoutMinimal3x3` | 3x3 | You need the 9-tile RPG Maker / legacy Godot-style minimum. | | Blob 47 Godot | `PentaTileLayoutBlob47Godot` | 7x7 | Your art is a 47-tile blob atlas. | | PixelLab Top-Down | `PentaTileLayoutPixelLabTopDown` | 8x8 | Your atlas is PixelLab's top-down output. | | PixelLab Side-Scroller | `PentaTileLayoutPixelLabSideScroller` | 8x8 | Your atlas is PixelLab's side-scroller output. | ## Pick by atlas shape | If your atlas looks like... | Start with | | -------------------------------------------------- | --------------------------------------------------------------------------------------------------- | | A 1-5 tile strip | [Penta](https://shilo.github.io/PentaTile/layouts/penta/index.md) | | A 4x4 sheet where each cell is a corner-mask state | [DualGrid 16](https://shilo.github.io/PentaTile/layouts/dual-grid-16/index.md) | | A 4x4 sheet for cardinal edge connections | [Wang 2-Edge](https://shilo.github.io/PentaTile/layouts/wang-2-edge/index.md) | | A 4x4 sheet for diagonal/corner connections | [Wang 2-Corner](https://shilo.github.io/PentaTile/layouts/wang-2-corner/index.md) | | A 3x3 classic autotile block | [Minimal 3x3](https://shilo.github.io/PentaTile/layouts/minimal-3x3/index.md) | | A 47-tile blob packed into a 7x7 sheet | [Blob 47 Godot](https://shilo.github.io/PentaTile/layouts/blob-47-godot/index.md) | | An 8x8 PixelLab top-down export | [PixelLab Top-Down](https://shilo.github.io/PentaTile/layouts/pixellab-top-down/index.md) | | An 8x8 PixelLab side-scroller export | [PixelLab Side-Scroller](https://shilo.github.io/PentaTile/layouts/pixellab-side-scroller/index.md) | ## Runtime behavior Dual-grid layouts paint visual cells on the half-cell-offset display grid. Single-grid layouts paint directly on logic-painted cells. This distinction matters when authoring art: - Dual-grid templates can use partial-cell silhouettes because neighboring display cells compose the final shape. - Single-grid templates should normally use full-cell artwork for every state, because each painted logic cell owns its full output tile. Custom formats can subclass `PentaTileLayout`; see [Authoring Custom Layouts](https://shilo.github.io/PentaTile/custom-layouts/index.md). # Penta Class: `PentaTileLayoutPenta` Penta is the addon's signature 5-archetype dual-grid format. It supports horizontal and vertical strips and a progressive tile count: ONE, TWO, THREE, FOUR, or FIVE. `AUTO` detects a strip size from the atlas; `AUTO_STRIP` detects each strip independently. The canonical slot order is: 1. `IsolatedCell` 1. `Fill` 1. `Border` 1. `InnerCorner` 1. `OppositeCorners` `OuterCorner` is implicit. PentaTile synthesizes it from `IsolatedCell` at load time instead of giving it a dedicated slot. ## Template FIVE mode template: ONE mode template: ## When to use it Use Penta when you want the smallest useful authored atlas. ONE mode is enough for quick prototypes, while FIVE mode gives an artist explicit control over all five archetypes. Penta is a dual-grid layout, so it works best for ground, walls, blobs, and terrain-like fills where the visible shape can be assembled from corner connectivity. ## Setup 1. Add a `PentaTileMapLayer`. 1. Set `layout` to a `PentaTileLayoutPenta` Resource. 1. Set `axis` to `HORIZONTAL` for a strip across X, or `VERTICAL` for a strip down Y. 1. Leave `tile_count` at `AUTO` for normal one-strip atlases, or choose an explicit mode if you want the inspector preview to show that authoring tier. 1. Assign your `TileSet`, or leave `tile_set` empty to use the bundled fallback. ## Tile count modes | Mode | Authored slots | What PentaTile synthesizes | | ------------ | ----------------------------------------------- | ------------------------------------------------------- | | `ONE` | `IsolatedCell` | Fill, Border, InnerCorner, OppositeCorners, OuterCorner | | `TWO` | `IsolatedCell`, `Fill` | Border, InnerCorner, OppositeCorners, OuterCorner | | `THREE` | `IsolatedCell`, `Fill`, `Border` | InnerCorner, OppositeCorners, OuterCorner | | `FOUR` | `IsolatedCell`, `Fill`, `Border`, `InnerCorner` | OppositeCorners, OuterCorner | | `FIVE` | all five slots | OuterCorner only | | `AUTO` | detected from atlas strip length | depends on detected mode | | `AUTO_STRIP` | detected per strip | depends on each strip | ## Authoring notes - Keep pixels inside each archetype's expected silhouette. PentaTile enforces a canonical silhouette during synthesis so rotated pixels do not bleed into adjacent cells. - `OppositeCorners` anchors mask `9` (`TL + BR`) as the unrotated case. If art made for another tool appears diagonally swapped, flip that tile horizontally. - `AUTO_STRIP` is useful when one atlas source contains several Penta strips, but it is not multi-terrain blending. Mixed terrain transitions are future work. See [What is a Penta tileset?](https://shilo.github.io/PentaTile/penta-tileset/index.md) for the canonical term definition. # DualGrid 16 Class: `PentaTileLayoutDualGrid16` DualGrid 16 uses a 4x4 atlas with one authored tile for each 4-bit corner mask. The mask bits are `TL=1`, `TR=2`, `BL=4`, and `BR=8`. Use it when your art already follows the common 16-tile dual-grid convention and you want every state authored directly with no rotation reuse. ## Template ## Atlas contract | Property | Value | | -------------- | --------------------------------------------- | | Grid | 4 columns x 4 rows | | Tile count | 16 | | Mask bits | `TL=1`, `TR=2`, `BL=4`, `BR=8` | | Dispatch | `atlas_coords = Vector2i(mask % 4, mask / 4)` | | Grid type | dual-grid | | Rotation reuse | none | ## Setup 1. Add `PentaTileMapLayer`. 1. Set `layout` to `PentaTileLayoutDualGrid16`. 1. Author or import a `TileSetAtlasSource` whose 4x4 cells match the mask order. 1. Paint normally. Empty `tile_set` uses the bundled fallback template. ## Authoring notes - Mask `0` is empty and erases the dual-grid display cell. - Because this is dual-grid, the template uses quadrant silhouettes. A full painted logic region is formed by neighboring display cells together. - Use this layout when each of the 16 states needs hand-authored detail. # Wang 2-Edge Class: `PentaTileLayoutWang2Edge` Wang 2-Edge is a single-grid 4x4 atlas using a cardinal-edge mask: `N=1`, `E=2`, `S=4`, `W=8`. Unlike dual-grid layouts, it paints directly on logic-painted cells. `mask=0` still renders a tile, which is important for isolated single-grid cells. ## Template ## Atlas contract | Property | Value | | -------------- | --------------------------------------------- | | Grid | 4 columns x 4 rows | | Tile count | 16 | | Mask bits | `N=1`, `E=2`, `S=4`, `W=8` | | Dispatch | `atlas_coords = Vector2i(mask % 4, mask / 4)` | | Grid type | single-grid | | Rotation reuse | none | ## Setup 1. Add `PentaTileMapLayer`. 1. Set `layout` to `PentaTileLayoutWang2Edge`. 1. Use a 4x4 atlas ordered by mask value. 1. Paint logic cells directly; every painted cell renders one full tile. ## Authoring notes - Make every tile a complete 32x32-style cell, not a partial dual-grid quadrant. Single-grid layouts do not rely on neighboring display cells to complete the visible tile. - `mask=0` is the isolated-cell state, not erase. - This layout is a good fit for roads, paths, pipes, fences, and edge-driven terrain details. # Wang 2-Corner Class: `PentaTileLayoutWang2Corner` Wang 2-Corner is a single-grid 4x4 atlas using CR31 compass-corner naming: `NE=1`, `SE=2`, `SW=4`, `NW=8`. It is visually compatible with DualGrid 16 on the same silhouettes, but the mask bits are named for diagonal neighbors rather than TL/TR/BL/BR quadrants. ## Template ## Atlas contract | Property | Value | | -------------- | --------------------------------------------- | | Grid | 4 columns x 4 rows | | Tile count | 16 | | Mask bits | `NE=1`, `SE=2`, `SW=4`, `NW=8` | | Dispatch | `atlas_coords = Vector2i(mask % 4, mask / 4)` | | Grid type | single-grid | | Rotation reuse | none | ## Setup 1. Add `PentaTileMapLayer`. 1. Set `layout` to `PentaTileLayoutWang2Corner`. 1. Use a 4x4 atlas ordered by mask value. 1. Paint normally. ## Authoring notes - Wang 2-Corner samples diagonal neighbors. Straight 1-tile-wide lines can produce `mask=0`, and that state still renders a valid isolated tile. - Use full-cell artwork. This is a single-grid layout, not a dual-grid compositor. - Choose this over DualGrid 16 when your source art or documentation talks in CR31 `NE/SE/SW/NW` terms. # Minimal 3x3 Class: `PentaTileLayoutMinimal3x3` Minimal 3x3 is the 9-tile cardinal-edge minimum. It maps the 16 possible edge-mask states onto a 3x3 grid with an open-side collapse rule. This is the layout to try for RPG Maker A2-like ground art, legacy Godot 3.x style atlases, and quick low-detail prototypes. ## Template ## Atlas contract | Property | Value | | -------------- | -------------------------- | | Grid | 3 columns x 3 rows | | Tile count | 9 | | Mask bits | `T=1`, `E=2`, `B=4`, `W=8` | | Dispatch | open-side collapse rule | | Grid type | single-grid | | Rotation reuse | none | ## Setup 1. Add `PentaTileMapLayer`. 1. Set `layout` to `PentaTileLayoutMinimal3x3`. 1. Arrange the atlas like a classic 3x3 autotile block: exposed top row, center row, exposed bottom row. 1. Paint normally. ## Open-side collapse The layout chooses a column from west/east openness and a row from top/bottom openness: - west open only -> column 0 - east open only -> column 2 - neither or both -> column 1 - top open only -> row 0 - bottom open only -> row 2 - neither or both -> row 1 This means some 16-state masks share the same tile. That loss is the tradeoff that makes the 9-tile minimum possible. ## Authoring notes - Use full-cell artwork because this is single-grid. - `mask=0` dispatches to the center tile `(1, 1)` so isolated cells render. - This layout is intentionally compact; use Wang 2-Edge if the 3x3 collapse is too lossy for your art. # Blob 47 Godot Class: `PentaTileLayoutBlob47Godot` Blob 47 Godot is a single-grid layout using an 8-bit Moore-neighborhood mask. PentaTile collapses the 256 raw masks to 47 reachable blob states using the standard rule: a corner bit matters only when both adjacent edge bits are set. The bundled fallback atlas is packed into a 7x7 grid with two unused cells. ## Template ## Atlas contract | Property | Value | | -------------- | ------------------------------------------------------------------ | | Grid | 7 columns x 7 rows | | Tile count | 47 used cells plus 2 unused cells | | Mask bits | `N=1`, `E=2`, `S=4`, `W=8`, `NE=16`, `SE=32`, `SW=64`, `NW=128` | | Dispatch | raw 8-bit mask collapses to one of 47 masks, then row-major lookup | | Grid type | single-grid | | Rotation reuse | none | ## Setup 1. Add `PentaTileMapLayer`. 1. Set `layout` to `PentaTileLayoutBlob47Godot`. 1. Use a 7x7 atlas that matches the bundled template packing. 1. Leave the two unused cells transparent or unused. ## Collapse rule The raw 8-bit mask is reduced before lookup: a diagonal corner bit only matters when both adjacent cardinal edge bits are present. For example, `NE` only survives if both `N` and `E` are also set. ## Authoring notes - This is the most detailed shipped single-grid layout. Use it when your art already exists as a 47-blob set. - Empty-looking isolated cells still render through mask `0`; do not treat `mask=0` as erase in custom variants. - The bundled order follows the 47 reachable masks sorted ascending and packed row-major. # PixelLab Top-Down Class: `PentaTileLayoutPixelLabTopDown` PixelLab Top-Down reads PixelLab's 8x8 top-down tileset output. It is a single-grid 4-bit corner-mask layout. PixelLab outputs multiple cells for some masks. In v0.2, PentaTile picks the row-major first cell deterministically. Variation-bank selection is deferred to future variation work. ## Template ## Atlas contract | Property | Value | | -------------- | ---------------------------------------------------- | | Grid | 8 columns x 8 rows | | Tile count | 64 cells | | Mask bits | `TL=1`, `TR=2`, `BL=4`, `BR=8` | | Dispatch | PixelLab cell-to-role table, then role-to-mask cache | | Grid type | single-grid | | Rotation reuse | none | ## Setup 1. Generate or import a PixelLab top-down tileset output. 1. Add `PentaTileMapLayer`. 1. Set `layout` to `PentaTileLayoutPixelLabTopDown`. 1. Assign the PixelLab atlas as the layer `tile_set`, or use the fallback template for quick testing. ## Authoring notes - The PixelLab table contains variation banks. PentaTile v0.2 deliberately uses the row-major first matching cell for deterministic output. - Full variation-bank selection is deferred until the broader variation design. - This is single-grid, so each rendered state should be a complete tile. # PixelLab Side-Scroller Class: `PentaTileLayoutPixelLabSideScroller` PixelLab Side-Scroller reads PixelLab's 8x8 side-scroller tileset output. It shares the same corner-mask behavior as the top-down layout, but uses the side-scroller cell-to-role table. Like the top-down layout, it currently uses deterministic first-cell selection for masks with multiple candidate cells. ## Template ## Atlas contract | Property | Value | | -------------- | ------------------------------------------------------------------ | | Grid | 8 columns x 8 rows | | Tile count | 64 cells | | Mask bits | `TL=1`, `TR=2`, `BL=4`, `BR=8` | | Dispatch | PixelLab side-scroller cell-to-role table, then role-to-mask cache | | Grid type | single-grid | | Rotation reuse | none | ## Setup 1. Generate or import a PixelLab side-scroller tileset output. 1. Add `PentaTileMapLayer`. 1. Set `layout` to `PentaTileLayoutPixelLabSideScroller`. 1. Assign the PixelLab atlas as the layer `tile_set`, or use the fallback template for quick testing. ## Authoring notes - `mask=0` dispatches to the first side-scroller role-12 cell at `(0, 0)`. - Variation-bank picking is deterministic first-cell selection in v0.2. - Use this variant, not Top-Down, when your PixelLab output uses the side-scroller cell table. # Reference # What is a Penta tileset? A **Penta tileset** is a 5-archetype autotile format. This page intentionally tracks the canonical README definition instead of creating a second competing definition. The five archetypes, in canonical slot order: 1. **IsolatedCell** - a tile with all four edges and all four corners exposed; source for synthesizing `OuterCorner`. 1. **Fill** - a tile with all four edges adjacent to the same terrain; the common interior tile. 1. **Border** - a tile on a straight terrain edge. 1. **InnerCorner** - a tile at the inside of an L-bend. 1. **OppositeCorners** - a tile with two diagonally-opposite different-terrain corners. `OuterCorner` is implicit. PentaTile synthesizes it from the corners of `IsolatedCell` at load time, so it does not occupy a dedicated slot. For the diagram and longer explanation, see the README section: https://github.com/Shilo/PentaTile#-what-is-a-penta-tileset # Authoring Custom Layouts Custom layouts are experimental. Prefer the built-in layouts when they match your atlas, and subclass only for a genuinely missing convention. A layout subclasses `PentaTileLayout` and implements three core methods: ``` @tool class_name MyAlwaysFillLayout extends PentaTileLayout func is_dual_grid() -> bool: return false func compute_mask(_coord: Vector2i, _sample_fn: Callable) -> int: return 1 func mask_to_atlas(_mask: int, _strip_index: int = 0) -> PentaTileAtlasSlot: var slot := PentaTileAtlasSlot.new() slot.atlas_coords = Vector2i(0, 0) return slot ``` Guidelines: - Use `_pack_alternative()` when combining alternative tile ids with Godot transform flags. - For single-grid layouts, `mask=0` should usually render an atlas slot rather than erase the cell. - Co-locate a `bitmask_template` PNG next to the layout script when you want inspector preview and fallback TileSet support. - Keep custom layout logic small and table-driven where possible. # API Reference Auto-generated from Godot `##` doc-comments in `addons/penta_tile/**/*.gd`. Do not edit by hand — modify the source comments and rebuild docs. Regeneration is wired via `tools/mkdocs_hooks.py` (`on_pre_build`). Only documented public methods (no leading underscore) and documented `@export` properties are included. Undocumented members are intentionally omitted — add a `##` block above the member to surface it here. ## `PentaTileMapLayer` extends `TileMapLayer` *Source: [`addons/penta_tile/penta_tile_map_layer.gd`](https://github.com/Shilo/PentaTile/blob/main/addons/penta_tile/penta_tile_map_layer.gd)* Dual-grid autotiling `TileMapLayer` subclass driven by a `PentaTileLayout` resource. Users paint with inherited `TileMapLayer.set_cell` and `TileMapLayer.erase_cell`; this node dispatches those logic cells into visual cells through `_update_cells`. The `layout` resource defines the mask topology and slot resolution. Contract invariants: - When `tile_set` is `null` and `layout` is not `null`, the layer auto-fills `tile_set` from `PentaTileLayout.get_fallback_tile_set` (PREVIEW-03). - User-supplied `tile_set` is never overwritten; `_set` flips `_tile_set_is_fallback` to `false` on direct assignment (PREVIEW-04). - `logic_layer_opacity` = `0.0` hides the parent's raw cells via `CanvasItem.self_modulate`.a, not `CanvasItem.visible`; see **Critical Pitfall #7**. See: - .planning/research/PITFALLS.md - .planning/ROADMAP.md Phase 04 Fallback Routing ### Exports #### `atlas_source_id` ``` @export var atlas_source_id: int = -1 ``` Source `TileSetAtlasSource` ID for atlas reads. `-1` means "use the first source discovered in `tile_set`." Set explicitly only when the user's TileSet has multiple sources. #### `layout` ``` @export var layout: PentaTileLayout = _DEFAULT_LAYOUT_SCRIPT.new() ``` The `PentaTileLayout` resource that defines this layer's mask topology and slot resolution. Setting this auto-fills `tile_set` from `PentaTileLayout.get_fallback_tile_set` if no user-supplied TileSet is bound (PREVIEW-03). Default value is a fresh `PentaTileLayoutPenta` per node; clear to `null` for native `TileMapLayer` passthrough. #### `logic_layer_opacity` ``` @export var logic_layer_opacity: float = 0.0 ``` Visibility of the parent's raw non-dispatched cells, applied via `CanvasItem.self_modulate`.a. Defaults to `0.0` so only the dispatched `_PentaTileVisual` child renders. Using modulation instead of `CanvasItem.visible` avoids **Critical Pitfall #7**. #### `visual_z_index_offset` ``` @export var visual_z_index_offset: int = 0 ``` Z-index offset applied to the generated visual layer relative to this logic layer. #### `generated_collision_enabled` ``` @export var generated_collision_enabled: bool = true ``` Enables collision on the generated visual layer that carries dispatched tiles. #### `logic_collision_enabled` ``` @export var logic_collision_enabled: bool = false ``` Enables collision on this hidden logic layer when callers need raw-cell collision. ### Methods #### `rebuild()` ``` func rebuild() -> void ``` Force a full re-dispatch of all painted cells. Useful after external changes to the source `tile_set` that `Resource.changed` did not fire on, such as direct `TileSet.add_source` mutation. ## `PentaTileAtlasSlot` extends `Resource` *Source: [`addons/penta_tile/penta_tile_atlas_slot.gd`](https://github.com/Shilo/PentaTile/blob/main/addons/penta_tile/penta_tile_atlas_slot.gd)* A single atlas-slot record. Returned by PentaTileLayout.mask_to_atlas and consumed by PentaTileMapLayer.\_paint_with_slot. ### Exports #### `atlas_coords` ``` @export var atlas_coords: Vector2i = Vector2i.ZERO ``` The (x, y) coords of the slot in the `TileSetAtlasSource` grid. Read by `PentaTileMapLayer._paint_with_slot`. #### `transform_flags` ``` @export var transform_flags: int = 0 ``` Render-time transforms applied via `TileSetAtlasSource`'s `TileSetAtlasSource.TRANSFORM_FLIP_H`, `TileSetAtlasSource.TRANSFORM_FLIP_V`, and `TileSetAtlasSource.TRANSFORM_TRANSPOSE` flags OR'd together. Shares one int with `alternative_tile` per **Critical Pitfall #1**; combine via `PentaTileLayout._pack_alternative`. #### `alternative_tile` ``` @export var alternative_tile: int = 0 ``` Alt-tile id in the source's alternative grid. MUST be < 4096; the upper bits are reserved for `transform_flags` per **Critical Pitfall #1**. ## `PentaTileLayout` extends `Resource` *Source: [`addons/penta_tile/layouts/penta_tile_layout.gd`](https://github.com/Shilo/PentaTile/blob/main/addons/penta_tile/layouts/penta_tile_layout.gd)* Abstract base for all PentaTile layout topologies. Subclasses implement compute_mask + mask_to_atlas + is_dual_grid. Each layout owns its mask topology (which neighbors / corners / edges feed bits) and its slot resolution (mask -> AtlasSlot). See: - .planning/research/layouts/MASK_UNIFICATION.md §3 (Approach B selection) - .planning/research/layouts/TEMPLATE_CONVENTIONS.md §5 (dual-grid declaration) - .planning/research/PITFALLS.md §3 (\_pack_alternative recipe) @experimental ### Exports #### `bitmask_template` ``` @export var bitmask_template: Texture2D ``` Single PNG that serves as both the inspector preview and the source pixels for `get_fallback_tile_set`. Renamed from Phase 1's `template_image` per LAYOUT-03. #### `description` ``` @export var description: String = "" ``` Multiline description of the layout's mask topology, atlas grid shape, and intended use case. Surfaces in inspector help. ### Methods #### `compute_mask()` ``` func compute_mask(_coord: Vector2i, _sample_fn: Callable) -> int ``` Compute the layout-specific mask for `_coord` using `_sample_fn` as the neighbor-presence query. Returns the mask integer the layout's `mask_to_atlas` consumes. Subclasses must override this abstract base implementation. #### `is_dual_grid()` ``` func is_dual_grid() -> bool ``` Return `true` if this layout paints at the dual-grid half-cell offset, or `false` if it paints directly at the logic cell. See **Critical Pitfall #8** for the single-grid logic-painted gate. ## `PentaTileLayoutBlob47Godot` extends `PentaTileLayout` *Source: [`addons/penta_tile/layouts/penta_tile_layout_blob_47_godot.gd`](https://github.com/Shilo/PentaTile/blob/main/addons/penta_tile/layouts/penta_tile_layout_blob_47_godot.gd)* Blob47Godot — 47-tile blob layout, 8-bit Moore mask, single-grid. Mask convention (D-76): N=1, E=2, S=4, W=8, NE=16, SE=32, SW=64, NW=128. NOT the canonical CR31 clockwise ordering — see \_collapse_8bit_moore for the algorithm. The 256→47 collapse rule (D-78) per BorisTheBrave's reference (https://www.boristhebrave.com/permanent/24/06/cr31/stagecast/wang/blob.html): "A corner bit only matters if both adjacent edges are set." Atlas: 7×7 (Caeles-canonical packing — 47 used cells + 2 unused; the unused cells stay transparent in the bundled bitmask PNG. Atlas slot coords are computed by sorting the 47 reachable masks ascending and packing row-major: index → (col=index%7, row=index/7).) Single-grid: yes — paints directly at the logic cell. Slot table sourced from: BorisTheBrave's published 47-blob reference (D-74). The 47-mask list is what the collapse rule emits when fed all 256 raw inputs (verified via blob_47_collapse_test). Variation banks: not supported — this is a 1-cell-per-mask layout. (Variation deferred to v2 backlog VAR-PIXEL-01.) ### Methods #### `is_dual_grid()` ``` func is_dual_grid() -> bool ``` Blob47Godot is single-grid: it paints directly on logic-painted cells. #### `compute_mask()` ``` func compute_mask(coord: Vector2i, sample_fn: Callable) -> int ``` Compute the raw 8-bit Moore-neighborhood mask for `coord`. `sample_fn` reports painted logic cells for N/E/S/W and diagonals; the raw mask is collapsed by `_collapse_8bit_moore` before atlas dispatch. #### `mask_to_atlas()` ``` func mask_to_atlas(mask: int, _strip_index: int = 0) -> PentaTileAtlasSlot ``` Look up `mask` in the 47-entry BorisTheBrave atlas convention. The raw 8-bit mask collapses first; unmapped masks defensively fall through to slot (0, 0). Mask 0 is a valid single-grid isolated-cell dispatch per **Critical Pitfall #9**. ## `PentaTileLayoutDualGrid16` extends `PentaTileLayout` *Source: [`addons/penta_tile/layouts/penta_tile_layout_dual_grid_16.gd`](https://github.com/Shilo/PentaTile/blob/main/addons/penta_tile/layouts/penta_tile_layout_dual_grid_16.gd)* DualGrid16 layout — 4×4 atlas, 16 unique tiles, 4-bit corner mask. Mask convention: TL=1, TR=2, BL=4, BR=8 (corner mask matching Godot's stock dual-grid template + the dandeliondino addon's `tile_map_dual` convention). NO rotation reuse — every one of the 16 mask states has a dedicated authored tile in a 4-column × 4-row grid. Use this layout when your atlas already ships all 16 corner-mask variants and you want pixel-perfect control. Atlas layout (mask = column + row * 4): Row 0: mask 0 | mask 1 | mask 2 | mask 3 Row 1: mask 4 | mask 5 | mask 6 | mask 7 Row 2: mask 8 | mask 9 | mask 10| mask 11 Row 3: mask 12| mask 13| mask 14| mask 15 Dual-grid: yes — paints at the half-tile-offset display cell. ### Methods #### `is_dual_grid()` ``` func is_dual_grid() -> bool ``` DualGrid16 paints on the dual-grid half-tile offset. #### `compute_mask()` ``` func compute_mask(coord: Vector2i, sample_fn: Callable) -> int ``` Compute the 4-bit corner mask for `coord` using TL=1, TR=2, BL=4, BR=8. `sample_fn` reports which logic cells are painted. #### `mask_to_atlas()` ``` func mask_to_atlas(mask: int, _strip_index: int = 0) -> PentaTileAtlasSlot ``` Convert `mask` to its dedicated 4x4 atlas slot. Mask 0 returns `null` because empty dual-grid display cells erase. ## `PentaTileLayoutMinimal3x3` extends `PentaTileLayout` *Source: [`addons/penta_tile/layouts/penta_tile_layout_minimal_3x3.gd`](https://github.com/Shilo/PentaTile/blob/main/addons/penta_tile/layouts/penta_tile_layout_minimal_3x3.gd)* Minimal3x3 layout — 3×3 atlas, 9 unique tiles, 4-bit edge mask. Mask convention: T=1, E=2, B=4, W=8 (cardinal-edge mask, matching Wang2Edge naming). Single-grid: yes. This layout covers the 9-tile minimum for cardinal-edge autotiling: a 3×3 grid where the center tile is the "fully connected" interior, the corners are the four outer-corner tiles, and the edges are the four cardinal-edge tiles. With only 9 unique tiles, several mask states must reuse tiles — the "open-side" rule collapses all 16 mask states onto the 9-tile palette: A tile lives at the atlas column matching open-W/open-E and atlas row matching open-T/open-B. "Open" means that cardinal neighbor is absent (bit NOT set). When both sides on an axis are open (or both closed), col/row = 1 (center). Diagonal-only collisions (e.g. both T and B absent → col/row both forced to 1) collapse to the center tile — accepted visual loss inherent to the 9-tile minimum. Atlas layout (3 columns × 3 rows, row-major): Row 0 (open-T): NW corner | N edge | NE corner Row 1 (center): W edge | center fill | E edge Row 2 (open-B): SW corner | S edge | SE corner Column assignment: open-W only → col 0 | neither/both → col 1 | open-E only → col 2 Row assignment: open-T only → row 0 | neither/both → row 1 | open-B only → row 2 ### Methods #### `is_dual_grid()` ``` func is_dual_grid() -> bool ``` Minimal3x3 is single-grid: it paints directly on logic-painted cells. #### `compute_mask()` ``` func compute_mask(coord: Vector2i, sample_fn: Callable) -> int ``` Compute the 4-bit cardinal-edge mask for `coord` using T=1, E=2, B=4, W=8. `sample_fn` reports which neighboring logic cells are painted. #### `mask_to_atlas()` ``` func mask_to_atlas(mask: int, _strip_index: int = 0) -> PentaTileAtlasSlot ``` Dispatch `mask` to the collapsed 3x3 atlas slot. ## `PentaTileLayoutPenta` extends `PentaTileLayout` *Source: [`addons/penta_tile/layouts/penta_tile_layout_penta.gd`](https://github.com/Shilo/PentaTile/blob/main/addons/penta_tile/layouts/penta_tile_layout_penta.gd)* PentaTile Penta layout — 5-archetype dual-grid autotiling. Slot ordering (LOCKED in Phase 2 architectural sweep): 0 = IsolatedCell (always present; synthesizes OuterCorner across all modes; feeds other archetypes when their slot is unfilled) 1 = Fill (added at TWO mode and above) 2 = Border (added at THREE mode and above; visual-frequency ordering puts Border before InnerCorner) 3 = InnerCorner (added at FOUR mode and above) 4 = OppositeCorners (added at FIVE mode) OuterCorner is IMPLICIT — synthesized from slot 0 with rotation transforms across all modes. Never has a dedicated slot. (Acceptable per the user-confirmed design: an isolated cell visually IS four outer corners + edges + fill, so OuterCorner art is naturally expressed via slot 0.) Mask convention: TL=1, TR=2, BL=4, BR=8 (corner mask). ANCHORING NOTE (Excalibur.js cross-reference): PentaTile anchors mask 9 (TL+BR, "" diagonal) as the unrotated OppositeCorners case (`_ROTATE_0`). The Excalibur.js dual-grid reference (https://excaliburjs.com/blog/Dual%20Tilemap%20Autotiling%20Technique/) uses the opposite anchor (mask 6 = TR+BL = "/" diagonal). Both are valid conventions. If you author your OppositeCorners tile against the Excalibur convention, mask 6 and mask 9 will appear swapped — flip the sprite horizontally to match PentaTile's anchoring. PentaTile picks mask 9 = `_ROTATE_0` because it matches the project's TL=1 lowest-bit-first ordering (also used in `draw_corner_mask` in the bitmask generator script and across all corner-mask layouts in the project). CODENAME DISCIPLINE: "Penta" is reserved exclusively for the 5-archetype tileset format. This file is the canonical home of that codename. See CLAUDE.md § Coined-Term Discipline. Dual-grid: yes — paints at the half-tile-offset display cell. Synthesis: see PentaTileSynthesis (penta_tile_synthesis.gd). ### Exports #### `axis` ``` @export var axis: Axis = Axis.HORIZONTAL ``` Strip axis: `Axis.HORIZONTAL` reads authored slots along X, while `Axis.VERTICAL` reads them along Y. The synthesized output atlas remains axis-invariant at `Vector2i(slot, strip_index)`. #### `tile_count` ``` @export var tile_count: TileCountMode = TileCountMode.AUTO ``` Source tile count mode. `TileCountMode.AUTO` detects the strip size from the active atlas dimension; `TileCountMode.AUTO_STRIP` detects each strip independently; explicit ONE through FIVE skip detection. ### Methods #### `is_dual_grid()` ``` func is_dual_grid() -> bool ``` Penta is a dual-grid layout: it paints at the half-tile-offset display cell. #### `needs_synthesis()` ``` func needs_synthesis() -> bool ``` Penta requires runtime synthesis so ONE through FOUR authored modes still expose the full five-archetype dispatch table. #### `compute_mask()` ``` func compute_mask(coord: Vector2i, sample_fn: Callable) -> int ``` Compute the 4-bit corner mask for `coord` using TL=1, TR=2, BL=4, BR=8. `sample_fn` is the logic-cell presence query supplied by `PentaTileMapLayer`. #### `mask_to_atlas()` ``` func mask_to_atlas(mask: int, strip_index: int = 0) -> PentaTileAtlasSlot ``` Dispatch `mask` to a Penta slot in `strip_index`. #### `resolve_display_strip()` ``` func resolve_display_strip(coord: Vector2i, sample_atlas_fn: Callable) -> int ``` Return the selected source strip for `coord`. ## `PentaTileLayoutPixelLabSideScroller` extends `PentaTileLayout` *Source: [`addons/penta_tile/layouts/penta_tile_layout_pixel_lab_side_scroller.gd`](https://github.com/Shilo/PentaTile/blob/main/addons/penta_tile/layouts/penta_tile_layout_pixel_lab_side_scroller.gd)* PixelLabSideScroller — 8x8 atlas, single-grid, 4-bit corner mask. Mask convention (D-93): TL=1, TR=2, BL=4, BR=8 — same as top-down. Single-grid: yes. NO rotation reuse (D-90). Atlas: 8 cols × 8 rows = 64 cells. Cell-to-role table comes from tileset_transform.lua's tileset_output_side variant; the role-to-mask bijection is IDENTICAL to top-down (D-94 — locked by spike 003). Both PIXLAB layouts duplicate \_ROLE_TO_MASK per D-98 (GDScript 2 cannot parse cross-class const references). Variation banks: NOT supported in v0.2 — first-cell row-major pick (D-89). Bank pick deferred to v2 backlog VAR-PIXEL-01. Mask=0 dispatch (D-104, Pitfall #9): mask=0 → role 12 → cell (0, 0). Top-down has role 12 first appearing at (2, 2); side-scroller has it at (0, 0) (top-left corner of the side-scroller table). The cache returns the correct entry for each subclass without special-casing. No `pixellab_version: int` field (D-92): per CLAUDE.md no-forward-compat rule, future PixelLab plugin updates trigger a new release + CHANGELOG. Source provenance: - cell-to-role table: tileset_transform.lua:28-36 `tileset_output_side` - role-to-mask bijection: spike 003 README + decode.py ### Methods #### `is_dual_grid()` ``` func is_dual_grid() -> bool ``` PixelLabSideScroller is single-grid: it paints directly on logic-painted cells. #### `compute_mask()` ``` func compute_mask(coord: Vector2i, sample_fn: Callable) -> int ``` Compute the 4-bit corner mask for `coord` using TL=1, TR=2, BL=4, BR=8. `sample_fn` reports which neighboring logic cells are painted. #### `mask_to_atlas()` ``` func mask_to_atlas(mask: int, _strip_index: int = 0) -> PentaTileAtlasSlot ``` Return the cached row-major-first cell for `mask`. For `mask = 0`, dispatches to role 12 -> cell `(0, 0)` per D-104 for the side-scroller variant, unlike top-down's `(2, 2)`. ## `PentaTileLayoutPixelLabTopDown` extends `PentaTileLayout` *Source: [`addons/penta_tile/layouts/penta_tile_layout_pixel_lab_top_down.gd`](https://github.com/Shilo/PentaTile/blob/main/addons/penta_tile/layouts/penta_tile_layout_pixel_lab_top_down.gd)* PixelLabTopDown — 8x8 atlas, single-grid, 4-bit corner mask. Mask convention (D-93): TL=1, TR=2, BL=4, BR=8 (same corner-mask convention as PentaTileLayoutDualGrid16 and PentaTileLayoutWang2Corner). Single-grid: yes — paints directly at the logic cell. NO rotation reuse (D-90): every dispatched slot uses transform_flags=0. Atlas: 8 cols × 8 rows = 64 cells. Cells map to 16 "roles" via tileset_transform.lua's tileset_output table; roles map to 4-bit corner masks via the locked role-to-mask bijection (D-94, spike 003). Variation banks: NOT supported in v0.2 — when multiple cells map to the same mask (e.g. mask 15 / role 6 has 28 cells), mask_to_atlas returns the row-major FIRST cell (D-89). Bank pick deferred to v2 backlog VAR-PIXEL-01 — design-coupled with VAR-01 + MULTITERR-01. Mask=0 dispatch (D-104, Pitfall #9): mask=0 → role 12 → cell (2, 2). Single-grid mask=0 is NOT erase; the cached first-cell entry returns a valid render target so isolated cells render. No `pixellab_version: int` field (D-92): per CLAUDE.md no-forward-compat rule, future PixelLab plugin updates trigger a new release + CHANGELOG. Source provenance: - cell-to-role table: tileset_transform.lua:17-26 `tileset_output` - role-to-mask bijection: spike 003 README + decode.py (12/16 PASS) ### Methods #### `is_dual_grid()` ``` func is_dual_grid() -> bool ``` PixelLabTopDown is single-grid: it paints directly on logic-painted cells. #### `compute_mask()` ``` func compute_mask(coord: Vector2i, sample_fn: Callable) -> int ``` Compute the 4-bit corner mask for `coord` using TL=1, TR=2, BL=4, BR=8. `sample_fn` reports which neighboring logic cells are painted. #### `mask_to_atlas()` ``` func mask_to_atlas(mask: int, _strip_index: int = 0) -> PentaTileAtlasSlot ``` Return the cached row-major-first cell for `mask` (D-89). For `mask = 0` (isolated cell), dispatches to role 12 -> cell `(2, 2)` per D-104 for the top-down variant. ## `PentaTileLayoutWang2Corner` extends `PentaTileLayout` *Source: [`addons/penta_tile/layouts/penta_tile_layout_wang_2_corner.gd`](https://github.com/Shilo/PentaTile/blob/main/addons/penta_tile/layouts/penta_tile_layout_wang_2_corner.gd)* Wang2Corner layout — 4×4 atlas, 16 unique tiles, 4-bit corner mask in CR31 cardinal naming. Mask convention: CR31 NE=1, SE=2, SW=4, NW=8 (corner mask in compass terms). Single-grid: yes — paints directly at the logic cell. NO rotation reuse. Visually identical to PentaTileLayoutDualGrid16 on the same atlas data — same silhouettes, different bit-naming convention. Use this layout if your atlas was authored against CR31 corner-naming docs; use DualGrid16 if your atlas was authored against Godot's TL/TR/BL/BR convention. Both will paint correctly; the difference is which mask bit semantically corresponds to which logic-cell quadrant in the artist's head. NE corresponds to TR neighbor (i.e. coord + Vector2i(1, -1)). SE → BR neighbor (Vector2i(1, 1)). SW → BL neighbor (Vector2i(-1, 1)). NW → TL neighbor (Vector2i(-1, -1)). (Note: Wang2Corner samples DIAGONAL neighbors — NE/SE/SW/NW corner cells — NOT the 2×2 corner-quadrant scheme that Penta's dual-grid uses. This is single-grid: we want to know "is the diagonal neighbor present" to decide the corner appearance.) ### Methods #### `is_dual_grid()` ``` func is_dual_grid() -> bool ``` Wang2Corner is single-grid: it paints directly on logic-painted cells. #### `compute_mask()` ``` func compute_mask(coord: Vector2i, sample_fn: Callable) -> int ``` Compute the 4-bit diagonal-corner mask for `coord` using NE=1, SE=2, SW=4, NW=8. `sample_fn` reports which diagonal logic cells are painted. #### `mask_to_atlas()` ``` func mask_to_atlas(mask: int, _strip_index: int = 0) -> PentaTileAtlasSlot ``` Convert `mask` to its dedicated 4x4 atlas slot. Mask 0 is a valid single-grid isolated-cell dispatch to atlas (0, 0), not an erase; see **Critical Pitfall #9**. ## `PentaTileLayoutWang2Edge` extends `PentaTileLayout` *Source: [`addons/penta_tile/layouts/penta_tile_layout_wang_2_edge.gd`](https://github.com/Shilo/PentaTile/blob/main/addons/penta_tile/layouts/penta_tile_layout_wang_2_edge.gd)* Wang2Edge layout — 4×4 atlas, 16 unique tiles, 4-bit edge mask. Mask convention: CR31 N=1, E=2, S=4, W=8 (cardinal-edge mask). Single-grid: yes — paints directly at the logic cell (no half-tile offset). NO rotation reuse — every one of the 16 mask states has a dedicated authored tile. Also known as 'Marching Squares' in algorithm-centric writeups (e.g., the Excalibur.js dual-grid article); same atlas, different vocabulary. Helps users arriving via marching-squares search terms find the right layout. Atlas layout (mask = column + row * 4): Row 0: mask 0 | mask 1 | mask 2 | mask 3 Row 1: mask 4 | mask 5 | mask 6 | mask 7 Row 2: mask 8 | mask 9 | mask 10| mask 11 Row 3: mask 12| mask 13| mask 14| mask 15 ### Methods #### `is_dual_grid()` ``` func is_dual_grid() -> bool ``` Wang2Edge is single-grid: it paints directly on logic-painted cells. #### `compute_mask()` ``` func compute_mask(coord: Vector2i, sample_fn: Callable) -> int ``` Compute the 4-bit cardinal-edge mask for `coord` using N=1, E=2, S=4, W=8. `sample_fn` reports which neighboring logic cells are painted. #### `mask_to_atlas()` ``` func mask_to_atlas(mask: int, _strip_index: int = 0) -> PentaTileAtlasSlot ``` Convert `mask` to its dedicated 4x4 atlas slot. Mask 0 is a valid single-grid isolated-cell dispatch to atlas (0, 0), not an erase; see **Critical Pitfall #9**. ## `PentaTileSynthesis` extends `RefCounted` *Source: [`addons/penta_tile/penta_tile_synthesis.gd`](https://github.com/Shilo/PentaTile/blob/main/addons/penta_tile/penta_tile_synthesis.gd)* Synthesis machinery for PentaTileLayoutPenta. Builds runtime TileSets from a single source TileSet by extracting sub-regions of slot 0 (IsolatedCell) and assembling synthesized archetypes per the locked anchoring spec (see .planning/phases/02-native-layouts/02-02-PLAN.md Gate 1 / Gate 2). Determinism invariant: same (source_tile_set, axis, tile_count) → bit-identical output (PENTA-SYNTH-06). Re-runs only when these inputs change. Slot ordering (LOCKED — Phase 2 architectural sweep): 0 = IsolatedCell (always authored; source of OuterCorner render-time rotation) 1 = Fill (synthesized from slot 0 in ONE mode; authored in TWO..FIVE) 2 = Border (synthesized from slot 0 in ONE/TWO modes; authored in THREE..FIVE) 3 = InnerCorner (synthesized from slot 0 in ONE/TWO/THREE modes; authored in FOUR/FIVE) 4 = OppositeCorners (synthesized from slot 0 in ONE..FOUR modes; authored in FIVE) OuterCorner has NO synthesized output slot. mask_to_atlas returns slot 0 with rotation flags (ROTATE_90/180/270) at render time — Path B per Gate 1. # About # LLM-Friendly Docs PentaTile publishes auto-generated, single-file documentation surfaces for LLMs and agents on every docs build, following the [llms.txt standard](https://llmstxt.org/). ## Stable URLs - **** — table of contents per the llmstxt.org spec. Small, links to every doc page in nav order. - **** — full content concatenation. One file with quickstart, layout pages, the Penta-tileset definition, custom-layout authoring guide, and the complete GDScript API reference inline. Both are regenerated from the MkDocs source on every push to `main` that touches `docs/`, `mkdocs.yml`, or `addons/penta_tile/**/*.gd`. They are served by GitHub Pages alongside the rendered HTML site. ## What feeds the LLM artifacts 1. **MkDocs source** — every page in `docs/` (rendered as Markdown, not HTML). 1. **GDScript `##` doc-comments** — extracted from `addons/penta_tile/**/*.gd` into a virtual `api-reference.md` page at build time via `tools/mkdocs_hooks.py`. Documented public methods (no leading underscore) and documented `@export` properties are included; undocumented members are intentionally omitted (add a `##` block above the member to surface it). 1. **`mkdocs-llmstxt` plugin** — concatenates the above into `llms.txt` and `llms-full.txt` per the llmstxt.org spec. The pipeline lives in `.github/workflows/docs.yml` and is unconditional — there is no separate "generate LLM artifact" step to remember to run. ## Recommended agent context For agents working **in the repo** (Claude Code, Cursor, etc.): - `AGENTS.md` — project rules, pitfalls, identity guardrails. - `docs/` — task-facing prose (Markdown source, no rendering required). - `addons/penta_tile/**/*.gd` — authoritative API and implementation comments. - `tests/` — rendered-output regression methodology. For agents fetching **over the network** (no repo checkout): - Fetch for everything in one request. - Fetch first if you need a lightweight index before drilling into specific pages. ## History Phase 7 originally rejected an auto-generated LLM artifact in favor of direct-source consumption. That decision was reversed on 2026-04-29 once the project goal widened from "the author's own games" to "readable and widely usable library," and an over-the-network single-file surface became necessary. The reversal and rationale are recorded in `.planning/phases/07-repo-restructure-extract-tests-mkdocs-site-llm-friendly-docs/07-LLM-DOCS-DECISION-REVISION.md`.