Skip to main content

perforata — Parametric Perforation Pattern Generator

Generate manufacturable cutout patterns (fan grills, vent panels, speaker covers, decorative screens) as DXF/SVG files — driven by a composable node-graph engine with an interactive Streamlit UI.

Demo gallery of all factory presets

The matrix above renders every factory preset; view it live via the 🎬 Demo gallery button in the UI sidebar, or regenerate the image with uv run python -m perforata.demo docs/demo_gallery.png.

The original single-file CLI version lives in mvp/ and still works standalone. This is its structured successor.

Architecture

Patterns are built as a graph of small, well-posed processing nodes (think Blender Geometry Nodes / Grasshopper, in miniature):

Generators ──▶ Modifiers ──▶ Decorators ──▶ Ops ──▶ Exporters
 (centers)     (transform)    (cut shapes)   (crop,  (DXF/SVG)
                   ▲                          filters)
                   │
                Fields (images, gradients, expressions)
  • Generators (perforata.generators) produce a PointCloud of grid centers with attributes (angle, size, tag, lattice coords). The theoretical basis is a 2D Bravais lattice: points are integer combinations of two basis vectors, optionally decorated with a multi-point unit cell. Included: CartesianGrid (with row/column stagger for brickwork layouts), HexGrid, ConcentricRings (the MVP's radial system), and the general Lattice. Cartesian/hex grids accept fit=True to snap the pitch so a whole number of cells spans the panel and edge margins come out balanced on all sides.
  • Fields (perforata.fields) are scalar functions over the unit square: ImageField (sample a logo / letter), TextField, LinearGradient, RadialGradient, ShapeGradient, Expression. Fields compose with + - * operators.
  • Modifiers (perforata.modifiers) transform point clouds: Affine (rotation / scaling / shear as proper 2×2 matrix ops that keep per-point orientations and sizes consistent), PolarWarp (bend a cartesian grid into a circle), FieldModulate — the "convolve an image with the grid" mechanism that drives any point attribute from a sampled field — and DensityWarp, which remaps grid spacing so point density follows a field. Field-sampling modifiers accept region="symmetric" to map the field over an origin-centered box, so radial patterns whose bounding box is slightly lopsided keep the field centered on the true pattern center.
  • Decorators (perforata.decorators) instance actual cutout geometry onto points. ShapeInstancer maps tags → shape recipes, so rows tagged even/odd can get up/down triangles, and major/minor rows can get different shapes entirely. This separation of where centers are from what is cut there is the core design change from the MVP.
  • Ops (perforata.ops) handle manufacturability: Crop (true shapely boolean intersection for flush panel edges — or cull/center modes), MinHoleFilter, MinWall, FitToSize.
  • Exporters (perforata.exporters) write DXF (ezdxf) or SVG, to a path or to bytes (for UI download buttons).

Install

The core engine depends only on numpy; heavier layers are extras:

Extra Adds For
(none) core engine (numpy only)
geo shapely crop slicing, edge clearance
dxf ezdxf DXF export
raster pillow text/image fields
render matplotlib previews, demo gallery
app streamlit (+ all above) interactive UI
all everything

With pip

pip install perforata              # core engine
pip install "perforata[dxf]"       # pick the extras you need
pip install "perforata[all]"       # everything

With uv

As a project dependency:

uv add perforata                   # core engine
uv add "perforata[dxf,render]"     # with extras

As a standalone CLI tool (no project needed — uv manages the venv):

uv tool install "perforata[all]"   # puts `perforata` on your PATH
# or run one-off without installing anything:
uvx --from "perforata[render]" perforata demo -o gallery.png
uvx --from "perforata[all]" perforata ui

Quick start

# Unified CLI
perforata --version
perforata presets list                      # factory presets
perforata presets show honeycomb-vent
perforata validate pipeline.json            # schema-check a params file
perforata render pipeline.json -o out.svg   # params JSON -> SVG/DXF
perforata demo -o gallery.png               # preset matrix  [render]
perforata ui                                # Streamlit UI   [app]

A pipeline params file is plain JSON (the same contract the web platform uses — see perforata.api):

{
  "v": 1,
  "generator": {"type": "HexGrid",
                "params": {"pitch": 9.0, "width": 250, "height": 180}},
  "rules": {"*": {"shape": "hexagon", "fill": 0.8}},
  "manufacturing": {"min_wall": 1.5}
}

Developing from a checkout (requires uv):

uv sync --all-extras          # install everything into .venv
uv run perforata --version    # the CLI, from the checkout
uv run streamlit run app.py   # interactive UI
uv run pytest                 # test suite
uv build                      # sdist + wheel into dist/

Presets

Pipelines can be stored, reloaded and shared:

  • Factory presets — curated pipelines shipped with the package, defined as code in perforata/factory_presets.py (tracked in git). Load them from the "Factory presets" section in the UI sidebar or via perforata presets list|show.
  • User presets — save the current UI pipeline into presets/user/ (git-ignored) as a versioned JSON file. The "Share" button downloads the same JSON for sending to someone else, who can import it via the file uploader. JSON presets cannot execute code on load, so they are safe to share.

⚠️ Legacy .pfp presets (cloudpickle) are still readable behind a deprecation shim, but they are pickle-based and execute code on load — only import .pfp files from sources you trust. Support will be removed in the next minor version; re-save to convert to JSON.

The 🎬 Demo gallery sidebar button renders all factory presets into a single matrix image (the successor of the MVP's --demo grid), with a download button for the PNG. The same renderer runs headlessly through perforata.demo — no Streamlit needed.

Using the library

from perforata.generators import CartesianGrid
from perforata.modifiers import FieldModulate
from perforata.fields import ImageField
from perforata.decorators import ShapeInstancer, ShapeSpec
from perforata.ops import Boundary, Crop
from perforata.exporters import DXFExporter
from perforata.graph import Pipeline

# Triangles alternating orientation row by row, sized by a logo image
Pipeline(
    CartesianGrid(pitch_x=8, pitch_y=8, width=300, height=200,
                  alternate=True),
    FieldModulate("scale", ImageField("logo.png", invert=True),
                  lo=0.15, hi=1.0),
    ShapeInstancer(rules={
        "even": ShapeSpec("triangle", fill=0.45, rotation=90),
        "odd":  ShapeSpec("triangle", fill=0.45, rotation=-90),
    }),
    Crop(Boundary.rect(300, 200), mode="cull", include_outline=True),
    DXFExporter("panel.dxf"),
).run()

For non-linear compositions (shared upstream nodes, multiple generators merged), use perforata.graph.Graph:

from perforata.graph import Graph
from perforata.modifiers import TagFilter, Merge

g = Graph()
g.add("grid", CartesianGrid(pitch_x=10, pitch_y=10, width=100,
                            height=100, alternate=True))
g.add("even", TagFilter("even"), "grid")
g.add("odd", TagFilter("odd"), "grid")
g.add("merged", Merge(), "even", "odd")
g.add("cuts", ShapeInstancer(shape="circle", fill=0.5), "merged")
shapes = g.run("cuts")

Notes

  • Units are dimensionless; treat them as millimeters in CAM. FitToSize and Boundary define true physical dimensions.
  • Hole size is always the inscribed diameter (narrowest opening) — the measurement that matters for airflow and minimum-feature checks.
  • ShapeSpec.fill scales that inscribed diameter relative to the local grid pitch, for every shape: at fill=1.0 neighboring cutouts touch, regardless of whether they are circles, hexagons or triangles. Keep it below 1.0 to leave walls between holes.
  • Crop(mode="slice") boolean-cuts straddling shapes flush with the panel edge; mode="cull" keeps only whole cutouts (usually best for perforation panels, since edge slivers can be unmanufacturable).
  • The UI's Export section includes a Config dump (.txt) button: a plain-text report of the widget state and the constructed node pipeline (plus result stats), for debugging and bug reports.

License

perforata is licensed under the GNU Affero General Public License v3.0 or later (LICENSE). You are free to use, modify, and share it; if you distribute a modified version or offer it as a network service, you must make your source available under the same terms. For commercial licensing outside the AGPL (e.g. embedding the engine in a proprietary CAD plugin), contact the author.

Contributions are welcome under the project's contributor license agreement (CLA.md): you keep ownership of your work and it always stays available under the open-source license, while granting the project the rights needed to also offer commercial licenses. State your agreement in your first pull request.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

perforata-0.3.1.tar.gz (2.2 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

perforata-0.3.1-py3-none-any.whl (74.7 kB view details)

Uploaded Python 3

File details

Details for the file perforata-0.3.1.tar.gz.

File metadata

  • Download URL: perforata-0.3.1.tar.gz
  • Upload date:
  • Size: 2.2 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for perforata-0.3.1.tar.gz
Algorithm Hash digest
SHA256 dceaebe6518622db29edd33762a1731f906a6c07124ceae56ffdf87bb0de8640
MD5 607c5adbd564f336bc306c8454e934da
BLAKE2b-256 7e1cace1856a23b97190e088e00021e0116b42d6de6bf9b1608cb05bf6436e5b

See more details on using hashes here.

Provenance

The following attestation bundles were made for perforata-0.3.1.tar.gz:

Publisher: release.yml on stepbot/perforata

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file perforata-0.3.1-py3-none-any.whl.

File metadata

  • Download URL: perforata-0.3.1-py3-none-any.whl
  • Upload date:
  • Size: 74.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for perforata-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 44203fb71a51467c48a4694856ec1723fc9624667e7e13de3756f87474f7ad73
MD5 bd048bb61927e379d4f414cd2567e1ea
BLAKE2b-256 32f9a94254047d4a4f4b53cde7833ff40e9d8f8cbe9b97a3b662a95b13048378

See more details on using hashes here.

Provenance

The following attestation bundles were made for perforata-0.3.1-py3-none-any.whl:

Publisher: release.yml on stepbot/perforata

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

1.0.0

2 files

0.4.1

2 files

0.4.0

2 files

This release

0.3.1 This release

2 files

0.3.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page