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_surface¶
entity.fit_to_surface(scale) auto-sizes any entity to fit within its surface. scale is the fraction of surface area to fill (0.0 to 1.0). Works with both EntityGroup and Text entities.
By default, fit_to_surface uses bounds(visual=True) so that stroke width is accounted for — stroked entities won't spill beyond surface 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 surface:
group.fit_to_surface(0.5, at=(0.15, 0.15)) # Near top-left, constrained by edges
group.fit_to_surface(0.5, at=(0.5, 0.5)) # Centered
group.fit_to_surface(0.5, at=(0.85, 0.85)) # Near bottom-right
Rotational Fitting¶
When a shape doesn't match the surface'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 surface's proportions. Useful when you want the shape to echo the surface'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_surface(0.85, rotate=True) # maximize fill
# OR
group.fit_to_surface(0.85, match_aspect=True) # match surface 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 in 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
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.
See also
For the full transforms API, see Transforms.
What's Next?¶
Learn how connections link entities with live references and explore the anchor system: