geomotif.core.transform
¶
Affine transforms and the composite operators built on them.
This layer is why the motif catalog stays a sane size. Mandalas,
snowflakes, rosettes, kaleidoscopes and most tessellations are
:func:radial_repeat or :func:tile applied to one small motif, not thirty
hardcoded classes.
:class:Affine follows the SVG/PostScript convention: the six coefficients
(a, b, c, d, e, f) are the matrix ::
| a c e |
| b d f |
| 0 0 1 |
so a point maps to (a*x + c*y + e, b*x + d*y + f).
Classes:
| Name | Description |
|---|---|
Affine |
A 2D affine transform, composable with |
Functions:
| Name | Description |
|---|---|
layer |
Overlay designs into one. Equivalent to repeated |
radial_repeat |
Repeat |
symmetry_group |
Apply a full cyclic |
mirror_axis |
Return |
tile |
Repeat |
jitter |
Randomly displace every point, for controlled hand-drawn irregularity. |
snap |
Move every point onto the nearest line of a square grid. |
fit_to |
Scale and center |
clip_to |
Trim |
offset_path |
Return a parallel copy of |
Affine
dataclass
¶
Affine(a: float = 1.0, b: float = 0.0, c: float = 0.0, d: float = 1.0, e: float = 0.0, f: float = 0.0)
A 2D affine transform, composable with @ and callable on points.
Defaults to the identity, so Affine() is a no-op you can build on.
Methods:
| Name | Description |
|---|---|
identity |
Return the transform that changes nothing. |
translate |
Move by |
rotate |
Rotate by |
scale |
Scale by |
mirror |
Reflect across the line at |
shear |
Slant by |
inverse |
Return the transform that undoes this one. |
Attributes:
| Name | Type | Description |
|---|---|---|
determinant |
float
|
Signed area scale factor; negative when the transform reflects. |
determinant
property
¶
Signed area scale factor; negative when the transform reflects.
identity
classmethod
¶
translate
classmethod
¶
rotate
classmethod
¶
Rotate by angle radians, counter-clockwise in y-up coordinates.
Source code in src/geomotif/core/transform.py
scale
classmethod
¶
Scale by sx horizontally and sy vertically (sy defaults to sx).
Source code in src/geomotif/core/transform.py
mirror
classmethod
¶
Reflect across the line at angle radians passing through through.
Source code in src/geomotif/core/transform.py
shear
classmethod
¶
inverse
¶
inverse() -> Affine
Return the transform that undoes this one.
Raises:
| Type | Description |
|---|---|
ValueError
|
If the transform is singular (a zero scale factor, say), which collapses the plane onto a line and cannot be undone. |
Source code in src/geomotif/core/transform.py
layer
¶
Overlay designs into one. Equivalent to repeated +.
radial_repeat
¶
Repeat design n times evenly around a point -- the mandala workhorse.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
design
|
Design
|
The unit to repeat. |
required |
n
|
int
|
Number of copies, including the original. Must be >= 1. |
required |
about
|
(float, float)
|
Center of rotation. |
(0.0, 0.0)
|
mirror
|
bool
|
Also emit a reflected copy in each sector, giving dihedral rather than merely cyclic symmetry. |
False
|
Source code in src/geomotif/core/transform.py
symmetry_group
¶
Apply a full cyclic Cn or dihedral Dn symmetry group.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
design
|
Design
|
The fundamental domain to replicate. |
required |
group
|
str
|
|
required |
Source code in src/geomotif/core/transform.py
mirror_axis
¶
Return design overlaid with its reflection across a line.
tile
¶
Repeat design on a rectangular lattice.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
design
|
Design
|
The unit cell contents. |
required |
cols
|
int
|
Lattice size. Both must be >= 1. |
required |
rows
|
int
|
Lattice size. Both must be >= 1. |
required |
dx
|
float
|
Spacing between columns and rows. |
required |
dy
|
float
|
Spacing between columns and rows. |
required |
stagger
|
float
|
Fraction of |
0.0
|
Source code in src/geomotif/core/transform.py
jitter
¶
Randomly displace every point, for controlled hand-drawn irregularity.
Each coordinate is offset independently by a uniform value in
[-amount, amount]. The RNG is private to this call -- the global
:mod:random state is never touched -- so a given seed always
reproduces the same result no matter what else the program is doing.
Source code in src/geomotif/core/transform.py
snap
¶
snap(design: Design, step: float = 1.0, *, mode: SnapMode = 'half-even', drop_duplicates: bool = True) -> Design
Move every point onto the nearest line of a square grid.
This is rounding applied to the design rather than to each file as it is
written, which is the difference that matters: every exporter then agrees,
and a plot of the result shows what the file will actually contain.
design.snapped() alone rounds to whole units.
Snapping trades this library's exact arc-length spacing for grid alignment. Points that were an equal real distance apart come out equal only to within half a step, so snap after resampling and choose a step well below the spacing if the evenness is what you are there for.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
design
|
Design
|
What to snap. |
required |
step
|
float
|
Grid size, in the design's own units. Must be finite and positive.
|
1.0
|
mode
|
('half-even', 'half-up', 'floor', 'ceil', 'trunc')
|
How a coordinate between two grid lines is resolved. |
"half-even"
|
drop_duplicates
|
bool
|
Remove points that a coarse grid has landed on top of their immediate neighbour, and then any stroke left with fewer than two points. On by default, because those are zero-length segments: ink a plotter cannot draw and a pen-down/pen-up it should not spend the time on. Turn it off to keep the point count exactly as it was, which is what a caller feeding a fixed-size buffer or a per-point parallel array needs. |
True
|
Returns:
| Type | Description |
|---|---|
Design
|
Snapped, with each surviving stroke's style following it across. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
>>> from geomotif import Design, Path
>>> square = Design((Path(((0.4, 0.4), (9.6, 0.4), (9.6, 9.6))),))
>>> list(snap(square))
[(0.0, 0.0), (10.0, 0.0), (10.0, 10.0)]
>>> list(snap(square, 0.25))
[(0.5, 0.5), (9.5, 0.5), (9.5, 9.5)]
Source code in src/geomotif/core/transform.py
346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 | |
fit_to
¶
fit_to(design: Design, width: float, height: float, *, padding: float = 0.0, flip_y: bool = False) -> Design
Scale and center design inside a canvas. See :meth:Design.fit.
Source code in src/geomotif/core/transform.py
clip_to
¶
Trim design to a rectangle, splitting paths that leave and re-enter.
Clipped paths come back open even if they went in closed: a shape whose outline has been cut is no longer a closed loop, and pretending otherwise would draw a chord across the gap. Loose points outside the rectangle are dropped.
Source code in src/geomotif/core/transform.py
offset_path
¶
Return a parallel copy of path, offset by distance to its left.
Left is relative to the direction of travel in y-up coordinates, so a negative distance offsets to the right. Corners are mitered, with a limit that falls back to a plain bevel on very sharp turns.
This is the "simple parallel stroke" of guilloché and knot outlines, not a CAD offset: self-intersections on tight concave corners are not cleaned up, and the result may cross itself where the offset exceeds the local radius of curvature.