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:
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:
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
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
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:
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
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
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: