Skip to content

Transforms & Layout

Rotate, scale, fit, connect, and layer entities for precise control over your compositions.

Rotation

Every entity supports rotation as a parameter or .rotate() method:

cell.add_polygon(
    Polygon.square(size=0.6),
    fill=colors.primary,
    rotation=(nx + ny) * 180,
)

Rotation grid

Squares rotate smoothly from 0 to 360 degrees across the grid.

Scaling with fit_to_cell

entity.fit_to_cell(scale) auto-sizes any entity to fit within its cell. scale is the fraction of cell area to fill (0.0 to 1.0). Works with both EntityGroup and Text entities.

By default, fit_to_cell uses bounds(visual=True) so that stroke width is accounted for — stroked entities won't spill beyond cell edges after fitting. Pass visual=False for pure geometric fitting that ignores stroke width:

Scale comparison

The same group at 20%, 40%, 60%, 80%, 90%, and 100% of cell size.

Position-Aware Fitting

Pass at=(rx, ry) to fit at a specific position within the cell:

group.fit_to_cell(0.5, at=(0.15, 0.15))   # Near top-left, constrained by edges
group.fit_to_cell(0.5, at=(0.5, 0.5))     # Centered
group.fit_to_cell(0.5, at=(0.85, 0.85))   # Near bottom-right

fit_to_cell with positions

Same size, different positions — the entity respects cell boundaries.

Rotational Fitting

When a shape doesn't match the cell's aspect ratio, two fitting modes help fill the space:

  • rotate=True — Finds the rotation angle that maximizes fill. Uses a closed-form O(1) solution (3 candidate angles: 0°, 90°, and the optimal balanced angle).
  • match_aspect=True — Rotates so the bounding box matches the cell's proportions. Useful when you want the shape to echo the cell's shape rather than simply be as large as possible.
group = EntityGroup()
group.add(Rect.at_center((0, 0), 70, 14, fill="coral"))
cell.add(group)
group.fit_to_cell(0.85, rotate=True)        # maximize fill
# OR
group.fit_to_cell(0.85, match_aspect=True)  # match cell proportions

Fitting modes comparison

Default fit vs rotate vs match_aspect — for a wide rect bar (middle row) and the pyfreeform logo (bottom row).

Both modes work on any entity type — EntityGroup, Rect, Polygon, Ellipse, Line, Curve, and Text. Dot is symmetric so rotation is a no-op.

rotate and match_aspect are mutually exclusive — passing both raises ValueError.


Connections

Link entities with Connection objects that auto-update when entities move. Connections are covered in depth on their own page:

Connected network

Dots connected in a grid network — connections link alternate cells.

Connections & Anchors →


z_index Layering

Control draw order with z_index — higher values render on top:

cell.add_fill(color="navy", z_index=0)           # Background
cell.add_ellipse(fill="coral", opacity=0.2, z_index=1)  # Behind
cell.add_polygon(Polygon.hexagon(), fill="gold", z_index=2)  # Middle
cell.add_dot(color="white", z_index=3)            # On top

z_index layering

Four distinct layers: grid lines → ellipse → hexagon → dot.

map_range Utility

map_range() converts a value from one range to another — like converting between units. If a cell's horizontal position (nx) goes from 0 to 1 but you want a radius between 2 and 9:

from pyfreeform import map_range

radius = map_range(nx, 0, 1, 2, 9)
# nx=0.0 → radius 2    (left edge: small)
# nx=0.5 → radius 5.5  (middle: medium)
# nx=1.0 → radius 9    (right edge: large)

rotation = map_range(ny, 0, 1, 0, 90)          # vertical position → rotation
opacity = map_range(nx + ny, 0, 2, 0.3, 1.0)   # diagonal position → opacity

map_range demonstration

Diamonds where size, rotation, and opacity are all driven by mapped position values.

Swap the output range to reverse the direction — map_range(nx, 0, 1, 9, 2) makes the left edge large and the right edge small.


What's Next?

Learn how connections link entities with live references and explore the anchor system:

Connections & Anchors →