Transforms
A body's transform is where it is, which way it faces and how big it is. Every point of its shape and every anchor on it is placed by applying that transform. Three operations make it up, and the order you apply them in changes the result.
The idea
- Scale grows or shrinks the body about its own origin.
- Rotate turns it about an axis through its own origin.
- Move shifts it to a position in the world.

A body applies them in that order: scale, then rotate, then move. Because the turn happens before the move, the body always turns about its own center, wherever it is. That's spinning in place.
Do it the other way around, moving first and turning second, and the turn swings the body around the world's origin. That's an orbit. A spinning planet and a moon going around it are the same turn applied in different orders.

In TTE
The Orbit example turns a planet and a moon by the same angle each frame. The planet stays at the origin and only
its orientation changes. The moon's starting position is turned too, so it travels around the planet. Save it as
orbit.py:
"""Orbit: a planet turns in place while a moon orbits it.
An Effect Guide example for the Transforms chapter. Both bodies turn by the same angle each frame. The planet
turns about its own center. The moon's position is turned about the world origin, so it travels around the planet.
"""
from __future__ import annotations
from dataclasses import dataclass
from terminaltexteffects.engine.base_config import BaseConfig
from terminaltexteffects.engine.base_effect import BaseEffect, BaseEffectIterator
from terminaltexteffects.engine.space3d import Cube, Shaded, Space3D, rotation_about
from terminaltexteffects.utils import argutils
from terminaltexteffects.utils.geometry3d import Vec3
def get_effect_resources() -> tuple[str, type[BaseEffect], type[BaseConfig]]:
"""Return the command name, effect class, and configuration class TTE registers."""
return "orbit", Orbit, OrbitConfig
@dataclass
class OrbitConfig(BaseConfig):
"""Configuration for the Orbit effect."""
parser_spec = argutils.ParserSpec(
name="orbit",
help="A planet turns in place while a moon orbits it.",
description="orbit | A planet turns in place while a moon orbits it.",
epilog="Example: tte --canvas-width 80 --canvas-height 24 --anchor-text c orbit",
)
frames: int = argutils.ArgSpec(
name="--frames",
type=argutils.PositiveInt.type_parser,
default=360,
metavar=argutils.PositiveInt.METAVAR,
help="Frames for one full orbit.",
) # pyright: ignore[reportAssignmentType]
"int : Frames for one full orbit."
class OrbitIterator(BaseEffectIterator[OrbitConfig]):
"""Produce the frames of the Orbit effect."""
def __init__(self, effect: Orbit) -> None:
"""Place the planet at the origin and the moon to its right."""
super().__init__(effect)
self.space = Space3D(self.terminal)
self.planet = self.space.body(Cube(16)).draw(Shaded())
self.moon = self.space.body(Cube(6)).draw(Shaded())
self.moon_start = Vec3(30, 0, 0) # Where the moon starts, relative to the origin.
self.tilt = rotation_about("x", 20) # Tip the top toward the viewer, so we look down on the orbit.
self.frame_number = 0
self.text_shown = False
def __next__(self) -> str:
"""Turn both bodies one step, then swap them for the text after one orbit."""
if self.frame_number <= self.config.frames:
turn = rotation_about("y", 360 * self.frame_number / self.config.frames)
# Spin in place: only the planet's orientation changes. Its position stays at the origin.
self.planet.set_rotation(self.tilt * turn)
# Orbit: the moon's position is turned about the origin, then the moon is moved there.
self.moon.set_rotation(self.tilt * turn)
point = (self.tilt * turn).apply(self.moon_start)
self.moon.set_position(point.x, point.y, point.z)
self.space.step()
self.frame_number += 1
elif not self.text_shown:
for body in self.space.bodies:
body.erase()
self.space.step() # Hides the surface characters once nothing is drawn.
for character in self.terminal.get_characters():
self.terminal.set_character_visibility(character, is_visible=True)
self.text_shown = True
else:
raise StopIteration
self.update()
return self.frame
class Orbit(BaseEffect[OrbitConfig]):
"""A planet turns in place while a moon orbits it."""
@property
def _config_cls(self) -> type[OrbitConfig]:
return OrbitConfig
@property
def _iterator_cls(self) -> type[OrbitIterator]:
return OrbitIterator

Body has a method for each operation. Some change the transform relative to where it is now, and some set it
outright:
| Change by | Set to | Effect |
|---|---|---|
rotate(axis, degrees) |
set_rotation(rotation) |
Turn about the body's own origin. |
move_by(x, y, z) |
set_position(x, y, z) |
Move the body's origin. |
scale_by(factor) |
set_scale(scale) |
Grow or shrink about the origin. |
reset() puts all three back to how the body was created and clears its queued tweens. rotation, position and scale read the current
values. Each method returns the body, so calls chain: cube.set_position(10, 0, 0).rotate("y", 45).
To orbit with these methods, compute the moon's position yourself by turning its starting offset, as the example
does, then set_position there.
Under the hood
geometry3d.transform_points applies a transform to many points at once:
from terminaltexteffects.utils.geometry3d import Rotation, Vec3, transform_points
world = transform_points(points, Rotation.about_y(angle), scale=2.0, position=Vec3(10, 0, 0))
It scales each point, rotates it with the rotation's matrix and adds the position. Body.world_point(local) does the
same for one point with the body's own transform.
At cell resolution
- Moving a body by less than a cell often changes nothing on screen. Slow drifts look stepped, so ease them or combine them with a turn.
- Scaling changes how many cells a shape covers. A shape scaled below about 6 columns stops reading as a shape at all (see 3D in a terminal).
- Anchors scale with the body, but characters don't. A text plane scaled to half size crowds two characters into each cell, and only one of them can show.