geomotif.bases.parametric
¶
Bases for motifs defined by a formula.
Three layers, each a special case of the one above it:
- :class:
MultiCurveMotif-- the motif is several parametric strands at once (both branches of a Fermat spiral, both lobes of a Cassini oval). - :class:
ParametricMotif-- the single-strand case: oneposition(u). - :class:
PolarMotif-- the polar case: oneradius(theta), with the cartesian conversion, the center offset and the theta range handled for you.
Implementing any of them buys arc-length resampling, every spacing curve, the transform layer, export and CLI exposure -- the whole library -- for what is usually a single line of maths.
Classes:
| Name | Description |
|---|---|
Curve |
One parametric strand of a motif, and how densely to measure it. |
MultiCurveMotif |
Base for a motif made of several parametric strands. |
ParametricMotif |
Base for a motif defined by a single |
PolarMotif |
Base for a motif defined by a single |
Curve
dataclass
¶
Curve(position: Callable[[float], Point], domain: tuple[float, float] = (0.0, 1.0), closed: bool = False, turns: float = 1.0)
One parametric strand of a motif, and how densely to measure it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
position
|
callable
|
Maps a parameter to a point. Called across |
required |
domain
|
(float, float)
|
Parameter range, inclusive at both ends. May run backwards. |
(0.0, 1.0)
|
closed
|
bool
|
Whether the strand returns to where it started. A closed strand's
final sample is dropped, since :class: |
False
|
turns
|
float
|
How far the strand winds, in whole turns. Only ever used to choose a sample count: a curve that bends more needs measuring more finely. |
1.0
|
MultiCurveMotif
dataclass
¶
Bases: Motif, ABC
Base for a motif made of several parametric strands.
Implement :meth:curves; :meth:build measures each strand and returns
one :class:~geomotif.Path per strand.
Notes
Subclasses implement hooks rather than overriding :meth:build, and the
bases validate inside :meth:build rather than in __post_init__, both
for the same reason: on Python 3.12 the zero-argument super() does not
work inside a slots=True dataclass, so a subclass cannot reliably
chain up to us. Nothing here ever requires it to.
Every field on a base is keyword-only, so a subclass is free to declare its own parameters positionally without tripping over the "no non-default argument after a default one" rule.
Methods:
| Name | Description |
|---|---|
curves |
Return the strands this motif is made of. At least one. |
ParametricMotif
dataclass
¶
Bases: MultiCurveMotif, ABC
Base for a motif defined by a single position(u).
Set :attr:domain and :attr:closed as class variables to describe the
curve's shape; override :meth:sweep_turns if it winds more than once, so
the sample density keeps up with it.
Examples:
::
@register("astroid", family="curve")
@dataclass(frozen=True, slots=True)
class Astroid(ParametricMotif):
domain = (0.0, math.tau)
closed = True
size: float = 1.0
def position(self, u: float) -> Point:
return (
self.size * math.cos(u) ** 3,
self.size * math.sin(u) ** 3,
)
Methods:
| Name | Description |
|---|---|
position |
Return the point at parameter |
sweep_turns |
Return how far the curve winds, in whole turns. |
position
abstractmethod
¶
sweep_turns
¶
Return how far the curve winds, in whole turns.
Used only to pick a sample count. The default of one turn suits any curve that does not loop repeatedly; a spiral should return its actual revolution count so that a tightly wound one is still measured accurately.
Source code in src/geomotif/bases/parametric.py
PolarMotif
dataclass
¶
PolarMotif(*, resolution: int | None = None, center: Point = (0.0, 0.0), theta_start: float = 0.0, theta_span: float = tau)
Bases: ParametricMotif, ABC
Base for a motif defined by a single radius(theta).
The whole extensibility story in eight lines::
@register("my-flower", family="polar")
@dataclass(frozen=True, slots=True)
class MyFlower(PolarMotif):
k: float = 7.0
def radius(self, theta: float) -> float:
return math.sin(self.k * theta) + 0.4 * math.cos(17 * theta)
Notes
A negative radius reflects: the point is placed on the opposite ray,
at theta + pi. That is what the cartesian conversion does naturally,
it is the convention every plot of r = cos(k*theta) assumes, and it is
what makes the petal count of a rose come out right. Clip the radius
yourself in :meth:radius if you want the other convention.
The hook is named radius, so a subclass cannot also have a field
called radius -- the two would collide in the class body. In practice
that never bites: a shape whose radius is a constant parameter rather
than a function of theta is a circle or an arc, and those are
parametric rather than polar for exactly this reason.
Methods:
| Name | Description |
|---|---|
radius |
Return the radius at angle |
with_turns |
Return a copy sweeping |
radius
abstractmethod
¶
with_turns
¶
Return a copy sweeping turns revolutions in the given direction.
The same thing as setting :attr:theta_span to turns * tau, said
the way a wound curve is usually described::
LogarithmicSpiral(b=0.15).with_turns(5, clockwise=True)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
turns
|
float
|
Revolutions to sweep. Fractional turns are fine. |
required |
clockwise
|
bool
|
Sweep direction. Counter-clockwise by default, matching the standard math convention the rest of the library uses. |
False
|