layoutc

Copy-Paste Ready: All Python examples work with pip install -e . and can be copied (or piped) directly into python3. Examples use the included tournament layout files.
layoutc is a command-line utility and Python library for encoding and decoding spatial entity layouts in speedball arena formats. It supports converting between JSON-based layout representations and PNG-based splatmap atlases.
Speedball is a competitive paintball format featuring symmetrical field layouts with inflatable bunkers. This tool helps manage and convert layout data between different formats used by tournament software, game engines, and visualization tools.
Features
- Encode spatial entities from JSON layouts into PNG splatmap atlases.
- Decode spatial entities from PNG splatmap atlases into JSON layouts.
- Support for TSV (Tab-Separated Values) format for tabular data exchange.
- Customizable color depth and pixel pitch for encoding/decoding.
- Support for different spatial units (meters, degrees, turns).
- Quadrant-based spatial representation for efficient encoding/decoding.
- Automatic format detection based on file extensions and content.
- Extensible architecture for adding support for additional file formats.
Installation
For development, clone the repository and install in editable mode:
git clone https://github.com/infimalabs/layoutc.git
cd layoutc
pip install -e .
Otherwise, install via PyPI:
pip install layoutc
Quick Start
To test any code block:
- Windows: Copy the code, then paste into
python3
- macOS: Copy the code, then run
pbpaste | python3 in Terminal or paste into python3
- Linux: Copy the code, then run
xclip -o | python3 in a terminal or paste into python3
If any example doesn't work, ensure you have:
- Python 3.10+ installed
- The
layoutc repository cloned locally
- Your terminal is in the
layoutc project directory
- Installed the package in editable mode:
pip install -e .
- Tried
python3 -m layoutc … instead if layoutc … fails
- Ensure your paste buffer contains what you expect
One use case is converting tournament layout files:
layoutc src/layouts/NXL-World-Cup-2021.json world_cup_2021.png
layoutc world_cup_2021.png world_cup_2021_copy.json
layoutc src/layouts/*World-Cup*.json all_world_cups.png
layoutc src/layouts/*.json -
Copy-paste test:
from layoutc.codec import Codec
codec = Codec()
with open("src/layouts/NXL-World-Cup-2021.json", "rb") as fp:
codec.load(fp)
entities = list(codec)
print(f"âś“ Loaded {len(entities)} entities from World Cup 2021")
print(f"âś“ First entity: {entities[0]}")
Complete workflow example:
from layoutc.codec import Codec
from layoutc.entity import Entity
from layoutc import Unit
import glob
import json
with open("src/layouts/NXL-Texas-2019.json", "r") as f:
texas_data = json.load(f)
print(f"Loaded Texas 2019: {len(texas_data)} bunkers")
codec = Codec()
with open("src/layouts/NXL-Texas-2019.json", "rb") as fp:
codec.load(fp)
with open("texas_atlas.png", "wb") as fp:
codec.dump(fp)
for i, entity_data in enumerate(list(codec)[:3]):
entity = Entity(*entity_data)
display = entity.unfold(Unit.METER, Unit.DEGREE)
print(f"Bunker {i+1}: ({display.x:.1f}m, {display.y:.1f}m, {display.z:.0f}°)")
codec.clear()
world_cup_files = glob.glob("src/layouts/NXL-World-Cup-*.json")
for filename in world_cup_files:
with open(filename, "rb") as fp:
codec.load(fp)
with open("all_world_cups.png", "wb") as fp:
codec.dump(fp)
print(f"Created atlas from {len(world_cup_files)} World Cup layouts")
For Python integration:
from layoutc.codec import Codec
codec = Codec()
with open("src/layouts/NXL-World-Cup-2021.json", "rb") as fp:
codec.load(fp)
with open("world_cup_atlas.png", "wb") as fp:
codec.dump(fp)
print("Converted NXL World Cup 2021 layout to PNG atlas!")
Usage
Command-Line Interface
The layoutc command-line tool supports conversion between JSON, PNG, and TSV formats.
To encode multiple JSON layouts into a single PNG atlas:
layoutc src/layouts/NXL-World-Cup-{2018,2019,2020,2021}.json world_cups_atlas.png
To decode a PNG atlas into a JSON layout:
layoutc world_cups_atlas.png decoded_layouts.json
To convert a layout to TSV format:
layoutc src/layouts/NXL-Barcelona-2019.json barcelona.tsv
Command Syntax
layoutc [input ...] [output]
The last argument is treated as the output file, and all preceding arguments are input files. Use - for stdin/stdout.
Examples using the 23 included tournament layouts:
layoutc src/layouts/*.json -
layoutc src/layouts/NXL-*2021*.json tournaments_2021.png
layoutc src/layouts/NXL-Amsterdam-2019.json amsterdam.png
layoutc src/layouts/NXL-Amsterdam-2019.json amsterdam.tsv
layoutc src/layouts/*.json - | head -10
Options
--depth {254,127}: Set the color depth (default: 254).
--pitch {762,381}: Set the pixel pitch in mm/px (default: 762).
--from ENTITY: Set the input entity type (default: auto-detect).
--into ENTITY: Set the output entity type (default: auto-detect).
-v, --verbose: Show verbose traceback on error.
Format Detection
Input and output formats are automatically detected based on file extensions and content:
.json files are treated as JSON layouts
.png files are treated as PNG atlases
.tsv files are treated as Tab-Separated Values
- Use
--from and --into options to override auto-detection
Python API
The layoutc library provides a Codec class for encoding and decoding spatial entities:
from layoutc.codec import Codec
from layoutc.entity import Entity
from layoutc import Unit
import glob
codec = Codec()
with open("src/layouts/NXL-European-Champs-2021.json", "rb") as fp:
codec.load(fp)
with open("european_champs.png", "wb") as fp:
codec.dump(fp)
codec.clear()
layout_files = glob.glob("src/layouts/NXL-*2022*.json")
for filename in layout_files:
with open(filename, "rb") as fp:
codec.load(fp)
with open("tournaments_2022.png", "wb") as fp:
codec.dump(fp)
print(f"Combined {len(layout_files)} 2022 tournament layouts into atlas!")
codec.clear()
with open("src/layouts/NXL-Las-Vegas-2019.json", "rb") as fp:
codec.load(fp)
print("Las Vegas 2019 Layout entities:")
for entity_data in codec:
entity = Entity(*entity_data)
display_entity = entity.unfold(Unit.METER, Unit.DEGREE)
print(f"Bunker {entity.k} at ({display_entity.x:.1f}m, {display_entity.y:.1f}m, {display_entity.z:.0f}°)")
Advanced: Format-specific handling
Click to expand advanced usage details
Different file formats have different unit assumptions:
- JSON format: Coordinates in meters and degrees (tournament data format)
- TSV format: Coordinates in internal units (millimeters and arc minutes)
- PNG format: Stores data in internal units
from layoutc.entity import json as json_entity
from layoutc.codec import Codec
from layoutc.entity import Entity
from layoutc import Unit
import json
codec = Codec()
with open("src/layouts/NXL-Prague-2019.json", "r") as f:
tournament_data = json.load(f)
first_bunker = tournament_data[0]
entity = json_entity.Entity.make(first_bunker)
codec.add(entity)
entity = Entity(x=1500, y=2000, z=5400)
codec.add(entity)
display_entity = entity @ Unit.METER @ Unit.DEGREE
print(f"Display units: x={display_entity.x}m, y={display_entity.y}m, z={display_entity.z}°")
internal_entity = Entity(x=1, y=1, z=90).fold(Unit.METER, Unit.DEGREE)
print(f"Internal units: x={internal_entity.x}mm, y={internal_entity.y}mm, z={internal_entity.z} arc-min")
The Entity class represents a spatial entity (bunker) with x, y, z coordinates and metadata attributes g (group), v (version), and k (kind/bunker ID).
Key concepts:
- JSON files: Store coordinates in meters and degrees (tournament standard)
- PNG atlases: Efficient binary storage format for multiple layouts
- TSV files: Tab-separated format for debugging and data analysis
- Automatic conversion: The library handles unit conversions between formats transparently
Practical Examples
Here are complete, copy-pasteable examples using the included tournament data:
Convert all tournaments to different formats:
from layoutc.codec import Codec
import glob
codec = Codec()
world_cup_files = glob.glob("src/layouts/NXL-World-Cup-*.json")
for filename in world_cup_files:
with open(filename, "rb") as fp:
codec.load(fp)
with open("world_cups_atlas.png", "wb") as fp:
codec.dump(fp)
print(f"Created atlas from {len(world_cup_files)} World Cup layouts")
Analyze tournament layout data:
import json
with open("src/layouts/NXL-Chicago-2019.json", "r") as f:
layout_data = json.load(f)
print(f"Chicago 2019 has {len(layout_data)} bunkers:")
for bunker in layout_data[:3]:
print(f" Bunker {bunker['bunkerID']}: ({bunker['xPosition']:.1f}m, {bunker['zPosition']:.1f}m, {bunker['yRotation']:.0f}°)")
Create atlas and convert back:
from layoutc.codec import Codec
codec = Codec()
with open("src/layouts/NXL-Barcelona-2019.json", "rb") as fp:
codec.load(fp)
with open("barcelona_atlas.png", "wb") as fp:
codec.dump(fp)
codec.clear()
with open("barcelona_atlas.png", "rb") as fp:
codec.load(fp)
with open("barcelona_restored.json", "wb") as fp:
codec.dump(fp)
with open("barcelona_restored.json", "wb") as fp:
codec.dump(fp)
print("Successfully round-tripped Barcelona layout: JSON -> PNG -> JSON")
Technical Details
Internal representation and unit conversion
The system uses internal units (millimeters and arc minutes) for storage and computation:
- @ operator: Converts FROM internal units TO display units (e.g.,
entity @ Unit.METER @ Unit.DEGREE)
- fold() method: Converts FROM display units TO internal units (e.g.,
.fold(Unit.METER, Unit.DEGREE))
- unfold() method: Like @ operator but also handles quadrants properly
The layoutc module also provides enums and constants for working with spatial units, quadrants, and dimensions:
Unit: Conversion factors (METER=1000, DEGREE=60, TURN=21600)
Quadrant: Spatial quadrants (NE, NW, SW, SE)
Pitch: Pixel resolution (LORES=762mm/px, HIRES=381mm/px)
Depth: Color depth (LORES=127, HIRES=254)
GVK: Group/Version/Kind attributes for entity classification
Order: Atlas ordering for multi-layout collections
Extending layoutc
layoutc can be extended to support additional file formats.
First, create an appropriately-named module under layoutc.entity (ie. *.png is --from=layoutc.entity.png and *.json is --from=layoutc.entity.json). Then, create an Entity subclass in the module and implement its [auto]dump and [auto]load classmethods.
Unless --from or --into is used, layoutc.codec.Codec selects the most-appropriate entity class for each input or output file based on either its extension (dump) or its magic (load).
Development
This project uses Python >=3.10 and pip for dependency management and packaging.
To set up a development environment:
git clone https://github.com/infimalabs/layoutc.git
cd layoutc
python -m venv .venv
source .venv/bin/activate
pip install -e '.[dev]'
pytest -v
layoutc src/layouts/*.json all_tournaments.png
layoutc src/layouts/NXL-World-Cup-2021.json world_cup.tsv
Quick development test:
from layoutc.codec import Codec
codec = Codec()
with open("src/layouts/NXL-Amsterdam-2019.json", "rb") as fp:
codec.load(fp)
print(f"Loaded {len(list(codec))} entities from Amsterdam 2019 layout")
for entity_data in list(codec)[:3]:
print(f" Entity: {entity_data}")
License
layoutc is released under the MIT License. See LICENSE for more information.
Troubleshooting
Common Issues
"No valid entities found in input files"
- Check that your JSON file contains valid layout data with
xPosition, zPosition, yRotation, and bunkerID fields
- Ensure PNG files contain non-zero alpha channel values (entities are stored in the alpha channel)
- Verify file format is supported (JSON, PNG, or TSV)
"Atlas limit exceeded: cannot create more than 256 layout groups"
layoutc supports up to 256 separate layout groups in a single atlas
- Split large collections into multiple smaller atlas files
- Consider combining similar layouts into single groups if appropriate
"X coordinate seems unusually large"
- JSON format expects coordinates in meters and rotations in degrees
- TSV format uses internal units (millimeters and arc minutes)
"Invalid PNG dimensions"
- PNG atlases must have specific aspect ratios: 5:4 (standard), 4:3 (large), or 1:1 (maximum)
- Supported resolutions depend on pitch setting (762 or 381 mm/pixel)
Format auto-detection issues
- Use
--from and --into options to override automatic format detection
- Ensure file extensions match content (.json for JSON, .png for PNG, .tsv for TSV)
Performance Tips
- Use PNG format for storage of large layout collections (more efficient than JSON)
- Higher pitch values (762mm/px) create smaller files but lower spatial resolution
- Lower depth values (127 colors) create smaller files but may reduce precision