Skip to content

Latest commit

 

History

History
1581 lines (1239 loc) · 59.9 KB

File metadata and controls

1581 lines (1239 loc) · 59.9 KB

Ctrl+Grid — Handbook

A complete guide to generating dimensionally accurate PDF (and PNG) templates: grid paper, ruled paper, dot grids, staff paper, mazes, polar targets, tilings, fillable forms, perspective grids and mandalas — for paper formats and for e-ink devices.

The one promise: what says 5 mm measures 5 mm on the printout. No scaling, no "fit to page", no stretching a grid so it comes out even. If a template does not fit a page, Ctrl+Grid tells you — it never silently shrinks it.

This handbook is the user-facing manual. For why the tool is built the way it is, see the specification, pflichtenheft-vorlagengenerator.md (German); for the decisions taken where the spec was silent, see implementation-decisions.md.


Contents

  1. Installation
  2. Quick start
  3. How it works: handle and blades
  4. The command line
  5. The definition file
  6. Units and measures
  7. The page
  8. The pattern area and geometry
  9. The cycle model
  10. Frame furniture
  11. The cover sheet and embedding the definition
  12. The generators
  13. E-ink and devices
  14. Imposition: n-up and booklets
  15. Output formats
  16. Batch output: name lists
  17. Fonts
  18. Presets
  19. Printing accurately
  20. Errors and validation

1. Installation

Ctrl+Grid is a Python CLI, on PyPI as ctrlgrid. With uv you need not install it at all:

uvx ctrlgrid millimeter-a4 --pages 3 -o grid.pdf

That downloads it, runs it and leaves nothing behind. To keep it on your PATH:

uv tool install ctrlgrid

Or with pip:

pip install ctrlgrid

To run the unreleased state of main, point at the repository instead:

uvx --from git+https://github.com/DocAtPrompt/ctrlgrid.git ctrlgrid --help

For development (tests and linter):

uv sync --extra dev && uv run pytest && uv run ruff check .

The bundled music font (for staff clefs) and the presets travel with the package — nothing else to fetch.


2. Quick start

Generate 5 mm graph paper on A4 from a preset:

ctrlgrid millimeter-a4 -o grid.pdf

List the presets, print one to copy and bend, or list the e-ink devices:

ctrlgrid presets
ctrlgrid show millimeter-a4 > mine.yaml
ctrlgrid devices

Run your own definition file:

ctrlgrid -d mine.yaml -o out.pdf

Validate a definition without writing anything:

ctrlgrid check mine.yaml

Run with no arguments in a terminal to open the interactive preset browser (pick a preset, page count and output file).


3. How it works: handle and blades

Ctrl+Grid is a pocket knife: one handle, several blades.

  • The handle owns everything about the page: format, margins, the pattern area, the frame, header and footer, stamp, hole marks, the page loop and the output file.
  • A blade (a generator) only fills the pattern area with marks. It knows nothing about margins or paper size — it is handed a rectangle and asked for marks in local coordinates.

Because of that split, the same generator fills A4, A6, US Letter or a 3:4 e-ink slate without changing a line: only the handle's page settings change. Every definition names exactly one generator.

Everything is measured, then drawn. A run either aborts completely (with an error you can act on) or produces the whole document — never half a file.


4. The command line

Commands

Command What it does
ctrlgrid <preset> Build from a shipped preset (e.g. ctrlgrid millimeter-a4)
ctrlgrid -d <file> Build from your own definition file
ctrlgrid generate … The explicit form of the two above
ctrlgrid check <file> Validate and write nothing (§ 20)
ctrlgrid presets List the shipped presets
ctrlgrid show <preset> Print a preset's definition, ready to copy
ctrlgrid devices List device profiles, with where their figures come from
ctrlgrid (no args) Interactive preset browser (in a terminal)

Choosing the output

The output format follows the file extension of -o:

  • -o out.pdf → a PDF (vector, the reference output).
  • -o out.png → one PNG per page at the device's exact pixel size (needs a device; see §13).

If a target file already exists, the run refuses rather than overwrite it. Pass --force to overwrite.

Flags

All flags beat the definition file (the command line always wins). Switch-style flags (--cover, --strict, --embed-def, --skip-unsupported, --crop-marks, --booklet) only ever turn something on — the definition decides when the flag is absent.

See Appendix C for the full list. The most common:

ctrlgrid --version                                 # which version is installed
ctrlgrid millimeter-a4 --pages 30 -o grid.pdf      # 30 numbered sheets
ctrlgrid millimeter-a4 --cover -o grid.pdf         # + a calibration first sheet
ctrlgrid -d mine.yaml --format a5 -o out.pdf       # override the paper format
ctrlgrid -d mine.yaml --device remarkable-paper-pro -o pad.pdf
ctrlgrid millimeter-a4 --names class.txt -o sheets.pdf   # one sheet per name

5. The definition file

A definition is a YAML file. The smallest possible one names a version and a generator with its settings:

version: 1
generator: lines
families:
  - { direction: horizontal, base_spacing: 5mm }

The top level has three kinds of key:

  1. version — currently always 1. Required.
  2. Handle sectionspage, header, footer, border, stamp, pattern, pages. All optional; each is described below.
  3. The generatorgenerator: <name> plus that generator's own keys at the top level (for example families: for lines, cells: for grid).

Reusing values with anchors

YAML anchors keep a palette in one place. Define them under defs: (a reserved block that is not otherwise interpreted) and reference them with *name:

defs:
  grid:   &grid   "#7799bb"
  accent: &accent "#4466aa"

# … later …
color: [*grid, *grid, *grid, *grid, *accent]

Unknown keys are rejected, not ignored — a misspelled bordr: is an error, never a silently dropped border.


6. Units and measures

Every measurement is written with its unit. There is no bare-number default.

Lengths

Suffix Meaning
mm millimetres — 5mm
cm centimetres — 2cm
in inches — 0.5in
pt printer's points, 1/72 in — 0.15pt (typical stroke width)

Positions are held internally as exact integer micrometres, so a grid never drifts: 72pt is exactly 25400µm, and repeat runs are byte-identical.

Relative measures

%w, %h and %s are a fraction of the pattern area — its width, its height, and its shorter side. They let one definition fill both A4 and a 3:4 e-ink slate:

outer_radius: 48%s     # 48 % of the shorter side, whatever the format

Device pixels

px resolves against a device's pixel density and is only allowed when a device profile is active (§13). On paper it is refused — a printer has no fixed pixel.

Angles

Angles are in degrees: 45deg. Zero points right; positive is counter-clockwise.

Staff spaces

Staff paper measures in sp, one staff space — the distance between two lines of a stave. See the staves generator (§12).

Paper formats

Named formats: a3, a4, a5, a6, letter, legal, tabloid. Or give an exact size:

page:
  format: 210x99mm      # a DL-ish slip
  # or
  format: 8.5x11in

7. The page

The page: section is the handle's description of the sheet.

page:
  format: a4              # or a device (below), or an exact size
  orientation: portrait   # or landscape
  duplex: false           # two-sided binding (see margins)
  background: none        # or a colour, painted under everything
  hole_marks: false       # ISO 838 punch marks at the binding edge
  margin: 10mm            # see below

Margins

margin can be a single value (all four sides) or the four named edges:

page:
  margin:
    top: 15mm
    bottom: 15mm
    inner: 20mm     # the binding edge
    outer: 12mm     # the open edge

Margins are named inner/outer, not left/right, because of duplex. With duplex: true, inner and outer swap on even pages so the wide binding margin always sits at the spine. With duplex off, inner is simply the left margin.

The device instead of a format

Instead of format, name a device to target an e-ink screen (§13). Format and device are mutually exclusive — they are two answers to "what medium".


8. The pattern area and geometry

The pattern area is what is left after margins, header, footer and their gaps are removed. It is the rectangle the generator fills. Everything the generator draws lives here; the handle owns the frame around it.

The pattern: section tunes how a repeating pattern meets the edges of that area.

pattern:
  align: bottom-left     # which corner the grid is anchored at
  snap: none             # or cycle / pixel, per axis
  remainder: center      # where the leftover space goes, per axis

align

Which corner the pattern is anchored to: bottom-left (default), top-left, bottom-right, top-right. The complete block starts at that corner, so any incomplete block falls to the opposite one. Useful when you want whole cells to start at the top-left of the page.

remainder

A pattern rarely divides the area exactly. remainder decides where the leftover goes, per axis (x, y) or both:

  • center (default) — split evenly on both sides, so the leftover reads as breathing room, not a mistake.
  • end — pattern at the origin, all leftover at the far edge.
  • whole_cycles — drop the incomplete cycle entirely; the freed space goes to the far end.
pattern:
  remainder: { x: center, y: end }

snap

By default (none) the pattern keeps its exact spacing and the leftover is placed by remainder. snap instead adjusts the geometry so blocks come out even:

  • cycle — shrink the pattern area to a whole number of cycles (whole blocks only, no partial cells at the edge).
  • pixel — round every step to whole device pixels, for a crisp e-ink render (needs a device; §13).

snap is an error for generators whose pattern has a centre rather than an axis to snap to (polar, mandala, staves, grid, maze, tiling, form) — the tool says so rather than guessing.


9. The cycle model

The cycle model is the heart of the tool. In lines, dots and polar, each visual property — spacing, stroke weight, size, dash, colour — follows its own repeating list, and the lists may be different lengths.

"Every fifth line heavier and blue" is one cycle for weight and one for colour, each five long, running against a one-entry spacing cycle:

generator: lines
families:
  - direction: horizontal
    base_spacing: 1mm            # the base value
    spacing: [1]                 # × 1 each step → 1 mm apart
    base_weight: 0.15pt
    weight: [1, 1, 1, 1, 2.7]    # every fifth line 2.7× as thick
    color: ["#7799bb", "#7799bb", "#7799bb", "#7799bb", "#4466aa"]

Each cycle is a list of dimensionless multipliers of a base_… value. A bare base_spacing with no spacing: cycle means "the same every step". Because the lists run independently and repeat, a short colour cycle against a longer spacing cycle produces rich, regular patterns from a few numbers.

The millimeter-a4 preset (run ctrlgrid show millimeter-a4) is the canonical worked example.


10. Frame furniture

Everything around the pattern area is the handle's. All optional.

Header and footer

header and footer are bands of fixed height with three fields — left, center, right:

header:
  height: 8mm
  gap: 4mm            # blank space between the band and the pattern
  font: { family: sans, size: 10pt }
  left: "{name}"
  center: "Week {page} / {page_count}"
  right: "2026"
  cut: false          # what happens if fields collide (below)

Placeholders are filled per page: {page}, {page_count}, and {name} (from a name list, §16).

If long fields collide, cut: false (the default) refuses the run and names the field and the missing millimetres; cut: true truncates with an ellipsis.

A field may hold an image instead of text — a PNG, never cropped:

header:
  height: 12mm
  left: { image: logo.png, height: 10mm }

A band can carry its own colours — a full-width strip behind it and a text colour. Both are off by default, so a definition without them draws exactly what it drew before. The strip is the height of the band, never more: it stops where gap begins, so the pattern area keeps its white.

header:
  height: 8mm
  gap: 3mm
  background: "#2f3a48"     # the strip
  text_color: "#ffffff"     # the text on it (default black)
  center: "{page}"

Border, background, hole marks, stamp

border:
  weight: 0.3pt
  color: "#333333"
  gap: 2mm            # inset from the pattern area

page:
  background: "#fbfbf7"    # painted under everything
  hole_marks: true         # ISO 838: two marks, 80 mm apart, 12 mm in

stamp:
  text: DRAFT
  angle: 45deg
  opacity: 0.08            # a faint full-page watermark
  size: auto               # sized to ~80 % of the sheet width

--stamp DRAFT on the command line is the quick route to a stamp.

The edge ruler

A printed scale along the edges of the sheet — the one piece of furniture that demonstrates the promise on every page: lay a real ruler against it, and either the numbers agree or your printer scaled.

ruler:
  edges: [bottom, left]   # bottom | left | top | right, at least one
  unit: cm                # mm | cm | in — what the numbers mean
  step: 1mm               # the smallest tick
  mid_every: 5mm          # the medium tick; `none` leaves it out
  label_every: 10mm       # the long tick, and the number beside it
  weight: 0.2pt
  color: "#000000"
  font: { size: 6pt }

Zero sits at the pattern area, not at the paper's corner — the numbers agree with the grid, so the first line is 0 and reading a position off the sheet needs no subtraction. The ticks grow outward into the margin and reserve no space: switching a ruler on leaves the grid exactly where it was.

origin says which corner that zero is, and which way the numbers grow:

origin For What it does
bottom-left technical drawing the default: x from the left, y from the bottom
top-left screen layouts y counts downwards from the top; x unchanged
bottom-right, top-right mirrored work the same formula, anchored at the other corner
center plotting a function zero in the middle of each edge, negative numbers before it

Leave it out and the ruler follows pattern.align: a grid anchored top-left gets a scale that starts there too. That is the point of the whole feature — a ruler that disagreed with the grid beside it would be worse than none. Write origin explicitly when you want the other thing: to measure the sheet rather than the pattern.

With a centred origin the labelled ticks are symmetric about the middle (the ladder hangs on the zero, not on the edge), and the numbers before it carry a minus sign — which the collision check counts, because it measures the labels the sheet will really carry.

A centred zero is the middle of the pattern area, and the grid does not know that. If you want the scale's nought to fall on a grid line — a coordinate cross wants exactly that — two numbers have to agree with it, and neither is obvious:

  1. The pattern area must be an even multiple of the emphasised period. With a millimetre grid emphasised every fifth line, the area has to be a multiple of 10 mm in that direction, or its middle lands between two heavy lines. It is the area that counts, not the format: 297mm − 2 × 8.5mm = 280mm works, 297mm − 2 × 14mm = 269mm does not.
  2. The weight cycle must put the heavy line first. A cycle counts from mark 0, so weight: [2.4, 1, 1, 1, 1] emphasises the area's own edge and every 5 mm from there. Ordinary millimetre paper is written the other way round ([1, 1, 1, 1, 2.7]), which puts the heavy lines at 4, 9, 14 mm — where the middle will never be.

plot-a4 is the worked example, and a test measures both agreements out of its finished PDF.

Only edges is required. The rest follows unit:

unit step mid_every label_every the numbers read
mm 1mm 5mm 10mm 10 20 30 …
cm 1mm 5mm 10mm 1 2 3 …
in 0.125in 0.5in 1in 1 2 3 …

The intervals are ordinary lengths, so step: 2mm or label_every: 0.25in are equally sayable — but each must be a whole multiple of the one below it, or the run is refused: a numbered tick that sits on no tick of the ladder looks like a scale and measures wrong. A number states its position exactly and is never rounded (label_every: 25mm under unit: cm prints 2.5). On the vertical edges the numbers turn 90°, reading bottom to top.

Edges are physical and do not swap under duplex the way margins do — a scale belongs to the paper, not to the binding.

What gets refused, always before the first page and always in millimetres: a margin too narrow for tick, gap and number (naming the header or footer band when that is what ends the space), and numbers so close together that they would run into one another. Nothing is shrunk to make a ruler fit. On PNG output the run is refused too — the numbers are text, and the PNG writer has no font file.

Example: 13-ruler-edge.yaml.


11. The cover sheet and embedding the definition

The calibration cover

--cover (or pages.cover: true) adds one extra first page carrying three things:

  1. A calibration square of exactly 50 mm and a 100 mm rule, each labelled. Lay a ruler on them: if the measure is off, your printer scaled.
  2. A stroke-weight ladder — short lines from 0.1 to 1.0 pt, each labelled, so you see how thin each weight really prints (and, on a device, its pixel width).
  3. A settings summary — generator, format, margins, base values, cycles, effective period, snap mode, tool version and the definition's checksum. A good sheet stays reproducible years later.

The cover is not counted in page numbering (--pages 30 --cover gives 30 numbered sheets plus the cover), carries no header/footer/border/stamp, and is never scaled by imposition.

Embedding the definition

--embed-def (or pages.embed_def: true) embeds the definition's exact source bytes in the PDF as a file attachment, so the document literally carries its own source and can be regenerated later without hunting for the file. PDF only — on PNG output the run is refused with the reason, rather than dropping the attachment silently.


12. The generators

Every generator fills the pattern area. Colours are #rrggbb. Stroke widths are lengths (the defaults below are the shipped values). Each has a worked example in ../examples/ and, for most, a preset (run ctrlgrid show <name>).

lines — ruled, squared, isometric, log

One or more families of parallel lines. Cross two families for squared paper; angle them for isometric; use law: log10 for graph paper.

generator: lines
families:
  - direction: horizontal      # horizontal | vertical | an angle like 55deg
    base_spacing: 5mm          # required
    base_weight: 0.15pt
    weight: [1, 1, 1, 1, 2.7]  # every fifth line heavier (the cycle model, §9)
    color: ["#000000"]         # one colour, or a cycle
  - direction: vertical        # cross two families for squared paper
    base_spacing: 5mm
    base_weight: 0.15pt
    weight: [1, 1, 1, 1, 2.7]

Each family also takes, all optional:

  • spacing: [ … ] — a cycle of spacing multipliers (uneven rules).
  • offset: <length> — shift the whole family across.
  • style: dashed or dotted with base_dash: <length> (and dash: to set an explicit pattern). base_dash is refused on a solid line — the tool never accepts a setting that would do nothing.
  • extent: { start:, end: } bounds which lines are drawn (never how long a line is — there is no "draw a stroke here" primitive), or count: <n> caps the number of lines.
  • law: log10 with decades: <n> for logarithmic graph paper; decades has no meaning on a linear family and is refused there.
  • governing: true — when two families share an axis and disagree on their period, one must be marked to set it.
# logarithmic graph paper:
families:
  - { direction: horizontal, base_spacing: 20mm, law: log10, decades: 3 }

Slanted families. A direction may be an angle — direction: 55deg — and then three things hold, all of them consequences of one idea:

  • base_spacing, offset and extent are measured perpendicular to the lines, not along an axis. 8 mm at 55° means the lines are 8 mm apart as a ruler laid across them measures, which is what a writing guide needs.
  • Every line is clipped to the pattern area, so the family fills the sheet and never runs into the margin.
  • Line 0 goes through the pattern area's bottom-left corner — the same place the first line of a horizontal family sits — and an unlimited family grows to both sides of it. Perpendicular coordinates below line 0 are negative, so extent: { start: -40mm, end: 0mm } keeps only the lines on that side.

Angles are taken modulo 180° (a line has no direction, so 55deg and 235deg are the same family), and 0deg and 90deg draw exactly what horizontal and vertical draw. Snapping is not supported on a slanted family — it has no cartesian period to snap to, and governing: true or law: log10 on one is refused rather than quietly ignored.

# an italic calligraphy guide: a ruled hand, crossed by the pen slant
families:
  - { direction: horizontal, base_spacing: 5mm, spacing: [2, 1, 1, 3],
      weight: [1, 2, 1, 1] }
  - { direction: 55deg, base_spacing: 18mm, base_weight: 0.12pt }

Examples: 01-lines-squared.yaml and 14-calligraphy-italic.yaml. Presets: millimeter-a4, calligraphy-a4.

dots — dot grids

Two crossed cycles of dots. The interesting question is what happens where they meet — combine decides.

generator: dots
grid:
  x: { base_spacing: 5mm, spacing: [1], offset: 0mm }
  y: { base_spacing: 5mm, spacing: [1] }
base_size: 0.3mm
size_x: [1, 1, 1, 1, 1.8]     # every fifth column larger
size_y: [1, 1, 1, 1, 1.8]
combine: max                  # max | product | intersection_only
color:                        # a single colour, or coloured by an axis:
  axis: cross
  cycle: ["#aab4c0", "#aab4c0", "#aab4c0", "#aab4c0", "#5577aa"]

combine: max gives the full cross grid; intersection_only keeps only dots where both cycles emphasise. A colour cycle must name its axis.

Example: 02-dots-grid.yaml. Preset: dots-5mm.

grid — labelled cell blocks

A count-driven block of square cells with optional labels and fills — battleship boards, score sheets, seating charts, bingo.

generator: grid
cells: { x: 12, y: 12 }        # required
labels:
  columns: "A"                 # A, B, C… (a § 7.10 counting pattern, or a list)
  rows: "n"                    # 1, 2, 3…
weight: 0.3pt
color: "#33475b"
fill: checker                  # none | checker | rows | columns
fill_color: "#eef2f6"
header_row: false
font: { size: 9pt }

Label patterns: n → 1, 2, 3…; a/A → a, b, c / A, B, C; or an explicit list.

Example: 03-grid-battleship.yaml. Preset: grid-a4.

polar — targets, score discs, polar paper

Concentric rings and radial spokes about a centre, with optional labels. Built on the same cycle model as lines.

generator: polar
center: auto                   # or { x: 105mm, y: 148mm }
outer_radius: 48%s             # auto, a length, or relative
rings:
  base_radius: 10mm            # required
  radius: [1]                  # cycle of ring-spacing multipliers
  base_weight: 0.15pt
  weight: [1, 1, 1, 1, 2]
  color: ["#000000"]
spokes:
  base_angle: 30deg            # required
  angle: [1]
  weight: [1]
  color: ["#000000"]
labels:
  spokes: "n"                  # label each spoke / ring with a pattern or list
  spoke_radius: 0.85
  rings: "n"
  font: { size: 8pt }

An optional radial_extent: { start:, end: } on spokes keeps the spokes clear of the centre (like a bullseye with an open middle). Ring and spoke families use the same cycle model as lines, so radius/angle and weight follow repeating lists — every fifth ring heavier, and so on.

Example: 06-polar-target.yaml. Preset: polar-a4.

tiling — hexagons, triangles, rhombi

An edge net of a regular tiling, drawn once (shared edges are not doubled).

generator: tiling
shape: hex                     # hex | tri | square | rhombus | octagon_square
size: 10mm                     # edge length (required; relative allowed)
orientation: pointy            # pointy | flat
weight: 0.4pt
color: "#3a4a5a"
fill: none                     # none | cycle
fill_colors: ["#eef3f7", "#dbe6ef", "#c7d8e8"]   # with fill: cycle
labels: none                   # none | coordinates
font: { size: 7pt }

Example: 07-tiling-hex.yaml. Preset: tiling-hex-a4.

maze — rectangular mazes

A perfect maze with an optional drawn solution. Procedural, so it takes a seed.

generator: maze
cells: { x: 20, y: 28 }        # required
algorithm: backtracker         # backtracker | prim | kruskal
start: bottom-left             # a corner
goal: top-right                # a corner
min_path_factor: 0.0           # 0–1: bias toward a longer solution path
wall_weight: 0.5pt
color: "#000000"
solution_color: "#cc3333"
seed: 0
solution: none                 # none | overlay | separate_page | back_mirrored

solution: separate_page prints the solution on the next sheet; back_mirrored puts it on the back for duplex printing. A maze item can span several sheets — the handle does the doubling and numbering.

Example: 05-maze-booklet.yaml (a multi-page booklet). Preset: maze-medium.

form — fillable forms

Rows of labelled fields — phone logs, checklists, planners, handover sheets. One level of nesting (a row of columns, a column of fields).

generator: form
title: { position: above, font: { size: 8pt } }   # above | inline | none
gap: 3mm
weight: 0.4pt
color: "#000000"
rows:
  - height: 15%                # a percentage of the pattern-area height, or `rest`
    columns:
      - { title: "Name", width: 70% }         # a text field (writing lines)
      - { title: "Done", kind: check }         # a tick box
  - height: rest
    columns:
      - { title: "Priority", kind: choice, options: ["Lo", "Hi"] }

Rows come first, then columns — one level of nesting. A row's height is a percentage or rest; a column's width is a percentage of the row. A field's kind is text (the default: ruled writing lines), check (a tick box) or choice (named options). Labels like "Ja"/"Nein" come from your file, never the tool — Ctrl+Grid adds no language of its own.

Example: 08-form-weekly.yaml. Preset: phone-log-a5.

staves — music and tab

Grouped systems of staff lines, measured in staff spaces (sp). Clefs are real glyphs from a bundled, subset music font.

generator: staves
count: 12                      # number of staves (required)
lines: 5                       # lines per stave (5 = music, 6 = guitar tab)
stave_space: 1.75mm            # the sp unit (or set stave_height)
system_gap: 6.5sp              # space between staves
weight: 0.2pt
color: "#000000"
clef: treble                   # none | treble | bass | alto | tenor
clef_indent: 3mm

Example: 04-staves-treble.yaml. Preset: staves-treble-a4.

perspective — drawing grids

A horizon with one to three vanishing points, each fanning equally divided rays, plus optional true verticals. The grid computes its own geometry (it does not use the cycle model).

generator: perspective
horizon:
  at: 0.55                     # height up the area, 0–1
  weight: 0.3pt
  color: "#000000"
vanishing_points:
  - at: [-0.6, 0.55]           # a point as area fractions, usually off the sheet
    count: 16                  # rays in the fan (≥ 2)
    weight: 0.15pt
    color: "#000000"
  - at: [1.6, 0.55]
    count: 16
    weight: 0.15pt
verticals:
  count: 18                    # evenly spaced true verticals (≥ 2)
  weight: 0.12pt

A vanishing point's at is a point in area fractions (0 = left/bottom edge, 1 = right/top), so a value below 0 or above 1 puts it off the sheet — where vanishing points usually sit. An optional base (top/bottom/left/right) picks which edge is divided into the fan. Rays are clipped to the pattern area. Example: 09-perspective-2pt.yaml. Preset: perspective-2pt-a4.

mandala — rotationally symmetric templates

A template to draw on: an N-fold scaffold plus motif families that carry the symmetry. Everything faces up (a vertical axis).

generator: mandala
sectors: 12                    # order of symmetry (required, ≥ 2)
center: auto                   # or { x: …, y: … }
outer_radius: 48%s             # auto, a length, or relative

The motif families (all optional):

Family What it draws
rings concentric guide circles, evenly spaced (count, weight, color)
spokes N radial guides (inner clears the centre, weight, color)
rosette N circles on the spokes (at, radius, mirror)
petals a ring of pointed leaves, each two arcs (inner, outer, width, mirror)
beads dots on a ring (at, count, size, rotate)
scallops a wavy ring of arcs (at, count, depth, inward)
pinwheel small polygons round a ring, twisted (at, size, sides, count, twist)
polygons inscribed regular or star polygons (radius, sides, step, rotate, fill_color)

rosette, petals, beads, scallops and pinwheel each take one entry or a list — layered bands at different radii. at, radius, inner, outer, width, depth and size (for pinwheel) are shares of the outer radius (0–1).

petals:
  inner: 0.34
  outer: 0.94
  width: 0.12
  mirror: true
beads:
  - { at: 0.99, count: 36, size: 0.9mm }
  - { at: 0.20, count: 12, size: 1.1mm }
rosette:
  - { at: 0.5, radius: 0.16, mirror: true }

Examples: 10-mandala.yaml and 10b-mandala-scallops.yaml. Preset: mandala-a4.

net — box nets you cut out and fold

Measurements in, a law computes the net: cut lines solid, creases dashed, glue tabs worked out. The one generator whose output you can check with scissors.

generator: net
style: tuck_top        # tuck_top | tray
length: 80mm           # inner dimensions — the space inside the box
width: 50mm
height: 30mm
thickness: 0.3mm       # the material; 0 for thin paper
glue_tab: 12mm
tuck: 15mm             # the tongue that slides into the front wall (tuck_top)

Two conventions are worth knowing before you change a number:

  • The dimensions are inner ones — the space the contents need, which is the number you actually have.
  • thickness has one rule: a panel that closes over a layer is widened by it, a flap that slides inside one is shortened by it. 0 gives the ideal net, which is right for paper; 0.3mm is ordinary 250 g/m² card.

style: tray is an open box — base, four walls, four glue tabs. style: tuck_top is the classic carton — a wall strip with a glue tab, and a lid with a tongue at each end supported by dust flaps.

Cuts and creases are drawn through the ordinary keys, so § 2a's fold notation needs nothing new: fold: { style: dashed } is a valley fold, fold: { dash: [3, 1, 1, 1] } a mountain fold.

A net is never scaled. If it does not fit the sheet the run is refused with the flat size it needs and the size there is — a box 2 mm out does not close, so "fit to page" would be worse than an error. What else is refused: a dimension of zero, a thickness at or past half the smallest dimension, tuck or dust on a tray where they would do nothing, and an unknown style.

Example: 16-net-tray.yaml. Preset: box-tuck-a4.

Fold notation

Paper folding has a notation — Yoshizawa–Randlett — and Ctrl+Grid needs no new keys for it: it is the dash machinery of §9, named.

Means Write
valley fold (fold towards you) style: dashed
mountain fold (fold away) style: dashed with dash: [3, 1, 1, 1] — dash-dot
reference crease (a guide, not a fold) style: dotted, or a lighter color
cut line style: solid — what net draws its outline with

base_dash sets the scale: the numbers in dash are multiples of it, so base_dash: 0.7mm with dash: [3, 1, 1, 1] is 2.1 mm on, 0.7 off, 0.7 on, 0.7 off. Keep the dashes short relative to the spacing — at three or four dashes per cell the sheet reads as a grid; at one it reads as a row of ticks.

Which lines are valleys and which are mountains is a property of the model, not of the paper, so a pre-creasing sheet does the useful half: it tells them apart. precrease-16-a4 is one — a 16 × 16 grid of valleys with both diagonals as mountains, on a square field:

ctrlgrid precrease-16-a4 -o precrease.pdf

Its margins make the pattern area itself square (176 × 176 mm), so the diagonals are clipped to the field instead of running out over the rest of the sheet — and because a slanted family is spaced perpendicular to itself (§7.1), a diagonal spacing of 11 mm / √2 puts one diagonal through every corner of every cell.

calendar — a linked, write-on planner

Unlike every generator above, calendar does not fill one page — it produces a whole linked document: one PDF of about 400 pages you navigate with the pen. Tap a date and land on that day; tap a note number and land on that note. Built for the reMarkable (the links are internal PDF links, which it follows), but it prints on paper too.

generator: calendar
year: 2026
week_start: monday          # or sunday
months:   [January, …]      # 12 names; English if omitted
weekdays: [Mon, Tue, …]     # 7 names; English if omitted
holiday_color: "#fce9e4"    # the fill for a marked day with no colour of its own
holidays:
  - { date: 2026-12-25, label: Christmas }
  - { date: 2026-05-03, label: "Mama's birthday", color: "#ffd9ec" }
holidays_file: holidays.ics      # or a YAML list — merged with the inline ones
legend:                          # what the colours mean, on the contents page
  - { color: "#fce9e4", label: "Public holidays" }
  - { color: "#ffd9ec", label: "Birthdays" }
title_page: { title: "2026", subtitle: "your name", background: "#2f3a48" }  # opt-in; +logo
year_view:  { weekend_shade: "#f0f2f5", cell_link: day, day_numbers: both }
month_view: { weekend_shade: "#f0f2f5", surface: lines }
week_view:  { surface: lines, tasks: true }   # opt-in: one page per week
day:
  blocks:                   # an ordered list — reorder, resize, repeat
    - { type: schedule, from: 7, to: 22, height: 55%, surface: lines, half_hours: true }
    - { type: todo,     rows: 8,          height: 20% }
    - { type: notes,    height: rest,     surface: grid }
notes:                      # one pad, or a list of them
  - { count: 20, surface: dots, label: "Sketches" }   # blank | lines | dots | grid

The page types, each exactly one page (if a view is small, use the device's zoom — nothing scrolls or scales):

  • Title (opt-in via title_page) — a cover: a full-sheet background colour with a centred title and subtitle in text_color, an optional logo (a PNG above the title) and an optional background_image (a PNG over the whole sheet; background_fit: cover fills and crops, contain fits it inside, and transparent areas show the colour through). Both paths are relative to the definition file. The header and footer bands are off on the cover; header: true / footer: true put them back, each on its own.
  • Contents — links to the overviews, the months and the note indices, one centred column with the groups separated by whitespace. legend adds a colour key at the foot of it — a swatch and your words for it.
  • Full-year overview — the whole year on one page as twelve mini-months, three across, numbers only: a day number jumps to its day, a month name to its month, and the week number beside each row jumps to its week when week pages exist.
  • Half-year 1 & 2 — two month-column × day-row tables (Jan–Jun, Jul–Dec); a month header jumps to its month, a day cell to its day (cell_link: day | month | none). Short months' columns simply end — no empty cells. Six columns are wide enough that the eye loses the row on the way across, so day_numbers: both repeats the 1…31 column on the right edge as well.
  • Month — a list of every day; the weekday and the date link to the day page, marked days are labelled and filled, weekends shaded.
  • Day — your ordered blocks (a timed schedule, a todo list, notes), each with a writing surface (blank / lines / dots / grid) and a height in per cent or rest. A schedule takes from/to hours, and half_hours: true rules the half hours too — the hour line then steps up a shade so the half stays subordinate. Leave day: out entirely and a day is one lined page.
  • Week (opt-in via week_view) — one page per week (~53), seven day sections in week_start order with a writing surface and a tasks column; each date jumps to its day page. Weeks that straddle the year edge show the outside days without a link.
  • Notesnotes: takes one pad or a list of them: lined pages to write on, squared ones to reckon on, dotted ones to sketch on. Each pad has its own count, surface and label, numbers its pages from 1, and gets its own index — you reach for "Sketches 3", not for "note 23". The indexes link to one another, and a long pad paginates its index over several sheets.

Every page carries a small nav strip (Index · Year · Month · Notes) at the right edge as underlined text — a link is just underlined text plus its tap box, to save space and bytes.

Marked days are not only public holidays: a birthday or an anniversary is the same thing to the calendar, and color per entry is what tells them apart — an entry without one takes holiday_color. The mark shows in all five views. legend is how the meanings get named; the calendar knows the colours and never what they stand for, so a legend line without a colour is refused rather than guessed at.

Holidays from a file. holidays_file reads either a YAML list of { date, label } or an .ics with concrete dates — a public-holiday feed exported from a calendar app, for instance. The path is relative to the definition file. Entries are filtered to year and merged with the inline list, where an inline entry wins on a shared date: hand-authored beats a feed. Recurring or timed .ics events are skipped and counted, and the run report names the source and how many entries survived.

Names come from your definition (English by default), so the calendar adds no language of its own. Dates are computed from year, so the same definition always gives the same PDF. The optional header is constant on every page — set header: { center: "{year}", right: "your name" } for the year and a personalization (no page numbers), and give the band a background and text_color (§ 10) if you want it to read as a spine. It is PDF only: on PNG the run is refused, because links and text cannot live in a PNG.

Nothing is ever scaled to fit, so a view that cannot fit its page is refused before page one, naming the millimetres it needed: a contents column longer than the sheet, a mini-month wider than a third of the width, a month whose 31 rows would fall below a readable size, day blocks whose fixed heights add up past 100 %.

Preset: calendar-a4 (ctrlgrid show calendar-a4), which turns on every view; worked example: 12-calendar-year.yaml — 405 pages, and the one example that ships without its PDF.

The language of the sheet

The tool ships no translations and never will (§3.4) — but it writes no word of its own on your sheet either. Everything printed comes from the definition:

months: [Jänner, Februar, März, April, Mai, Juni,
         Juli, August, September, Oktober, November, Dezember]
weekdays: [Mo, Di, Mi, Do, Fr, Sa, So]
notes: [{count: 40, label: Notizen}]
words:
  index: Inhalt
  year: Jahr
  month: Monat
  week: Woche
  notes: Notizen
  contents: Inhaltsverzeichnis
  full_year_overview: Jahresübersicht
  half_year: Halbjahr
  full_year: ganzes Jahr

words: names the calendar's own vocabulary — the navigation strip at the top of every page and the headings. Leave it out and you get the English above, so no existing definition changes. A notebook's one word is contents_title.

One limit to know about, and it is the same one the README lists first: the standard PDF fonts reach Latin-1. German, French, Spanish, Italian, Portuguese, Dutch and the Nordic languages are covered. Polish, Czech, Hungarian, Turkish, Romanian and Croatian are not — ń, ř, ő, ğ, ă, č need a font of your own:

font: {file: "~/Library/Fonts/NotoSans-Regular.ttf", size: 9pt}

A document takes one font for the whole document — there is nothing on a calendar page that wants two — and a blade takes one per labelled thing (grid, polar.labels, form.title, tiling, and the ruler). Header and footer have their own.

A character the font cannot draw is refused before the first page, naming the character and the word it came from — it is never printed as a box.

Which font, and where it goes. Two stages, and the difference matters (§10.3):

family: sans | serif | mono file: /path/to/font.ttf
what it is Helvetica, Times, Courier — three of the 14 fonts every PDF reader has any TrueType/OpenType file on your machine
embedded? no, and it does not need to be yes, and subset to the glyphs actually used
coverage Latin-1 whatever the file has
the PDF carries no font at all carries the font, and is the same everywhere

It is a path and never a font name. Name lookup works differently on every platform and answers differently depending on what happens to be installed — which is precisely the unreliability this tool exists to avoid. A path is unambiguous, checkable, and can be named in an error message.

Before embedding, the fsType field of the font's OS/2 table is read: a font whose licence forbids embedding stops the run and is named. There is no quiet substitution — a PDF that violates a font licence is not an acceptable output. The cover sheet records the file name and the font's version, so a sheet that came out well can be reproduced years later.

So: making the PDF needs the file on your machine; the PDF it makes does not.

notebook — several papers in one linked document

The other document generator, and the one that composes the rest: sections, each filled by an ordinary generator, with a contents page that links to them. One PDF on an e-ink device instead of twelve.

generator: notebook
title_page: { title: "Notebook", subtitle: "your name" }   # optional
sections:
  - label: "Bullet journal"
    pages: 40
    divider: true          # a sheet with the section's name before it
    generator: dots
    grid: { x: { base_spacing: 5mm }, y: { base_spacing: 5mm } }
    base_size: 0.4mm
  - label: "Music"
    pages: 10
    generator: staves
    count: 10
    stave_space: 1.8mm
    clef: treble

A section is a definition in miniature: generator: and then that generator's own keys, exactly as at the top of a file. Anything a blade can do, a section can do — copy the keys out of any preset. A typo inside a section is reported by that blade, naming the section (sections.2).

The pages come in this order: the optional title, the contents, then each section — its divider if it asked for one, then its pages. The contents names each section, links to it, and prints the page it starts on, because a notebook is a paper object too and there a link is only underlined text.

A section may be a puzzle book. maze with solution: separate_page puts each solution on the sheet after its puzzle, and inside a section that works exactly as it does on its own:

  - label: "Mazes"
    pages: 12          # twelve *puzzles* — twenty-four pages
    generator: maze
    cells: { x: 16, y: 22 }
    solution: separate_page

pages: counts puzzles, not sheets, the same way --pages 12 does for a maze on its own. With solution: back_mirrored the solution goes on the back of its puzzle's sheet, so it shows through against the light — and because that needs the puzzle on the front of a sheet, the notebook may insert one blank leaf before the section to line the parity up. The run says so when it does.

That mode also needs the pattern area to sit in the same place on both sides: either duplex: false, or inner and outer margins equal. Otherwise the run is refused, naming the section.

Bands are worth setting here: {section} names the section a page belongs to and {page} counts, both filled per page.

header: { height: 8mm, gap: 3mm, left: "{section}" }
footer: { height: 8mm, gap: 3mm, right: "{page} / {page_count}" }

What is refused, before the first page: an unknown generator (with the list of known ones), a document generator inside a section — notebooks do not nest — pages: 0, an empty sections:, anything a section's own generator refuses for the page size, a contents page too long for its sheet, and a section whose generator needs more than one sheet per item (a maze with solution: separate_page, whose solutions could not be paired with their puzzles here).

Not in this version, and named rather than forgotten: no per-section snap, remainder or align (those are page geometry for the whole document), no per-section paper size.

What a document generator does and does not take

Both calendar and notebook own their pages, and that decides which of the handle's keys apply to them.

They take the whole page model. page.margin including duplex — the margins swap on even sheets, which is what a bound notebook wants — plus page.background, page.hole_marks, border, ruler, stamp, and a header and footer whose placeholders are filled per page ({page}, and {section} in a notebook).

They refuse three things by name, rather than accepting them and doing nothing:

Asked for Why it is refused
--nup imposition works on a page loop a document does not have, and it would destroy the links a calendar is made of
--cover a document writes its own first page; title_page is the place for one
pattern.align it anchors a pattern in its area, and a document page is not one pattern area

Preset: notebook-a4 (106 pages). Example: 15-notebook.yaml.


13. E-ink and devices

Instead of a paper format, target an e-ink screen. The device's physical size comes from its pixel count divided by its density, so the template fits the screen exactly.

page:
  device: remarkable-paper-pro

Run ctrlgrid devices to list profiles and where their figures come from. Two ship today: the reMarkable Paper Pro (owner-verified, a colour device) and the reMarkable 2 (manufacturer-specified, monochrome).

With a device active:

  • px resolves against the density, so you can size things in pixels.
  • snap: pixel rounds every step to whole pixels for a crisp render, and the exact resulting size is reported.
  • The media check runs automatically: it warns when a line is too thin to survive the screen's resolution, or when two colours collapse to the same grey on a monochrome device. --strict turns every such warning into an error (good for a CI check of a preset set).

The media check runs whatever the output format — a PDF built for a device is checked just like a PNG.


14. Imposition: n-up and booklets

Print several small pages onto one large sheet, without scaling:

ctrlgrid -d card.yaml --format a6 --pages 4 --nup 2x2 --nup-sheet a4 -o sheet.pdf

--nup CxR lays a C×R grid of pages at 100 %. If the small pages do not fit the big sheet at full size, the run refuses (it never shrinks them — that would break the promise). --crop-marks adds trim marks. The cover sheet is exempt from imposition.

A document generator (calendar, notebook) refuses --nup outright: it writes its own pages rather than running the page loop imposition works on, and imposing would destroy the links such a document is made of.

Booklets

ctrlgrid millimeter-a4 --format a5 --pages 8 --booklet --nup-sheet 297x210mm -o booklet.pdf

Four sheet sides carrying pages 8-1, 2-7, 6-3, 4-5. Print double-sided flipping on the short edge, fold the stack in half, staple through the fold. The run report names the flip and the one thing to check on the first sheet: page 2 must come out behind page 1.

If your printer turns the paper the other way, say so and build it again:

ctrlgrid millimeter-a4 --format a5 --pages 8 --booklet --booklet-flip long \
    --nup-sheet 297x210mm -o booklet.pdf

The backs then come out printed upside down, which is exactly right: a long-edge turn shows them upside down, so they have to be printed that way to be read the right way up.

If your pages carry no numbers — plain grid paper, say — nothing on the sheet shows whether the flip was right, and the run says so. Add a footer for one test run:

footer:
  height: 10mm
  center: "{page}"

Then blank the footer again. With genuinely identical pages the flip makes no visible difference anyway; it matters as soon as the pages differ.

Three things to know before the first run:

  • The sheet must be landscape. Two portrait pages sit side by side, and --nup-sheet a4 is portrait — so write the size out, as above. If you forget, the refusal tells you which size to use.
  • A page count that is not a multiple of four is padded with blank leaves, and the run says how many. A folded sheet carries four pages; nothing is scaled to make the count come out even.
  • One signature. Every sheet nests inside the next, one staple through the fold. Past roughly forty pages the fold creep becomes visible and the stack staples badly — split the work into several booklets instead.

--booklet and --nup are refused together, and a document generator refuses --booklet for the same reason it refuses --nup.


15. Output formats

  • PDF — the reference output: vector, exact MediaBox, embedded and subset fonts. Same input produces byte-identical output every run (fixed creation date, content-derived document id), so a definition under version control has a stable PDF.
  • PNG — one file per page at the device's exact pixel resolution. Chosen by a .png extension on -o. The PNG writer cannot draw text (the standard fonts have no glyph file), so a run that needs text on PNG is refused by name, with the way out (name a font file, drop the text, or output PDF).

When the writer cannot draw something

A run is refused up front when the chosen writer lacks a feature the definition needs — text on PNG is the usual case, and a calendar's links the other. That is the right default: half a file is worse than none.

--skip-unsupported is the explicit way out. The run then leaves out what cannot be drawn and carries on, and says what it left out:

ctrlgrid -d pad.yaml -o pad.png --skip-unsupported
  note: --skip-unsupported: leaving out text — the writer cannot draw it,
        so those marks are not on the sheet at all (§ 10.2)

The name is deliberate: an --anyway next to --force would be two flags that both read "do it regardless" and do different things — --force is about the file, this one about the content.


16. Batch output: name lists

Give a list of names, one per line, and get one sheet per name with {name} filled in:

ctrlgrid millimeter-a4 --names class3b.txt -o sheets.pdf
header:
  height: 8mm
  left: "{name}"

With --names and no --pages, the list leads: one sheet per entry. With a count as well, the count leads and entries repeat or are cut (a notice tells you when a list was cut, rather than cutting it silently). Each named page also gets a PDF bookmark.


17. Fonts

Text (header/footer, labels, the stamp, form titles) uses one of three logical families: serif, sans, mono. These map to the standard PDF fonts, whose metrics are fixed, so geometry is identical on every machine.

For characters outside Latin-1 (ł, ğ, ő), name a font file:

header:
  font: { file: fonts/Inter.ttf, size: 10pt }

The file is embedded and subset (only the glyphs used travel with the PDF). Its embedding licence is checked, not assumed: a font whose fsType forbids embedding aborts the run and is named — never quietly swapped for another.


18. Presets

Presets are ordinary definition files that ship with the tool — they are also the documentation. There is one for every generator:

dots-5mm            grid-a4             mandala-a4          maze-medium
perspective-2pt-a4  phone-log-a5        polar-a4            staves-treble-a4
tiling-hex-a4       box-tuck-a4         calendar-a4         notebook-a4

And seven that are papers rather than generators — every one of them is lines with a different cycle, which is the argument of §9 made in files:

Preset What it is
calligraphy-a4 an italic guide: a ruled hand crossed by the 55° pen slant and a 135° angle check
seyes-a4 the French school ruling (grands carreaux): 2 mm rows with every fourth line strong, 8 mm verticals, and the red margin rule
mizige-a4 米字格, Chinese character practice: 20 mm boxes with both midlines and both diagonals
knitting-chart-a4 chart paper proportional to a knitting gauge — a stitch is wider than a row, so the two axes differ
plot-a4 a sheet for plotting by hand: millimetre grid, a coordinate cross through the middle, and a scale whose zero sits there too
precrease-16-a4 a 16 × 16 pre-creasing sheet for folding, with both diagonals — and the fold notation of §12 shown in one file
millimeter-a4 plain 5 mm graph paper, the one to start from
ctrlgrid presets                       # list them
ctrlgrid show millimeter-a4            # print one
ctrlgrid show millimeter-a4 > mine.yaml   # copy and bend it
ctrlgrid millimeter-a4 --format a5 -o out.pdf   # run one with overrides

19. Printing accurately

The whole point is scale, so a printer that rescales defeats it. Two habits:

  1. Print at 100 %, not "fit to page". Most viewers default to fitting the page and quietly scale to about 96 %. Choose "Actual size" / "100 %". Run with --cover and measure the 50 mm square and 100 mm rule with a ruler — if they are wrong, the driver scaled.
  2. Mind thin lines and character coverage. On a home printer a 0.1 pt line is fine; on an e-ink screen it may vanish — the media check warns. Without a font file, ä ö ü ß é à ñ ç work but ł ğ ő do not; name a font file for those.

The example gallery in ../examples/ shows one rendered sheet per generator, each with its definition and PDF.


20. Errors and validation

Ctrl+Grid validates everything before writing page one, and either aborts completely or builds completely — it never leaves a half-written file. ctrlgrid check <file> runs exactly that validation and writes nothing.

The design principle: a PDF that is almost right is the worst outcome, so the tool fails loudly rather than guessing. An error names the field, the value and, where it can, the fix in your own units. A feature named in the spec but not yet built refuses with a message naming what is missing — never a silently ignored key.


Appendix A — units

Unit Kind Where
mm cm in pt length everywhere
%w %h %s length, relative to the pattern area (width / height / shorter side) most length fields
px length, device pixels only with a device profile
deg angle angles
sp staff space staves
dpi density device profiles only

Appendix B — paper formats

a3, a4, a5, a6, letter, legal, tabloid, or an exact size such as 210x99mm or 8.5x11in. Or a device (§13) instead of a format.

Appendix C — command-line flags

Flag Meaning
-V, --version print the version and exit
-d <file> build from a definition file
-o <file> output path; the extension picks PDF or PNG
--pages <n> number of numbered sheets
--names <file> one sheet per name; fills {name}
--format <name> paper format
--device <id> e-ink device profile (instead of a format)
--orientation <portrait|landscape> orientation
--stamp <text> full-page stamp
--cover add the calibration cover sheet
--embed-def embed the definition's source in the PDF
--seed <n> seed for procedural generators (maze)
--strict turn media warnings into errors
--skip-unsupported leave out what the writer cannot draw, and say so
--nup <CxR> N-up imposition, never scaled
--nup-sheet <format> the sheet format for imposition
--booklet impose as a folded, saddle-stitched booklet, never scaled
--booklet-flip <short|long> which edge the printer turns on (default short)
--crop-marks trim marks with --nup or --booklet
--force overwrite an existing output file
--quiet report only the output path

Switch flags (--cover, --strict, --embed-def, --skip-unsupported, --crop-marks, --booklet) only turn something on; the definition decides when the flag is absent. All flags beat the definition.

Appendix D — generators at a glance

Generator Produces Key required field
lines squared, ruled, isometric, log/semi-log families
dots dot grids with emphasis grid
grid labelled cell blocks cells
polar targets, score discs, polar paper — (rings/spokes)
tiling hex/tri/square/rhombus/octagon nets shape, size
maze rectangular mazes, optional solutions cells
form fillable forms rows
staves music staves, guitar tab, clefs count
perspective 1–3 point vanishing-point grids — (horizon/points)
mandala rotationally symmetric templates sectors
calendar a linked, write-on planner PDF (many pages) year