geomotif.core.sampling
¶
Arc-length measurement and resampling, generalized to any polyline.
This is the engine that makes "equal spacing" mean equal real distance rather than equal steps of some parameter. A curve is measured with a dense polyline, the cumulative lengths are tabulated, and that table is inverted so a requested fraction of the total length lands exactly where it should.
Because it operates on polylines rather than on any particular curve, every motif in the library -- including fractals, tilings and string art, which have no closed-form parametrization at all -- gets arc-length placement and the whole spacing-curve family for free.
It is also plain Python on tuples of floats, deliberately. An array library
was tried here and lost: converting a design costs more than a single pass
over its vertices saves, and its hypot disagrees with :func:math.dist in the
last bit, which would move every point this table places.
Classes:
| Name | Description |
|---|---|
ArcTable |
Cumulative-length table over a polyline, and the inverse of it. |
Functions:
| Name | Description |
|---|---|
samples_for_turns |
Return a sensible densification count for a curve spanning |
densify |
Evaluate |
resample_path |
Return |
resample |
Return |
ArcTable
¶
Cumulative-length table over a polyline, and the inverse of it.
Building the table is O(n). A lone "where is the point at distance d?"
costs a binary search and one linear interpolation; asking for a whole run
of increasing distances -- which is what resampling does -- walks the table
once between them all instead, so the run is linear in the table rather
than n log n. See :meth:points_at.
Methods:
| Name | Description |
|---|---|
point_at |
Return the point |
point_at_fraction |
Return the point at fraction |
points_at |
Return the point at each distance, in the order they were asked for. |
segment |
Return the part of the polyline lying between two distances. |
points_at_fractions |
Return the point at each fraction of the total length. |
Attributes:
| Name | Type | Description |
|---|---|---|
total |
float
|
Total length of the polyline. |
vertices |
tuple[Point, ...]
|
The measured points, including the closing vertex if closed. |
Source code in src/geomotif/core/sampling.py
vertices
property
¶
The measured points, including the closing vertex if closed.
point_at
¶
Return the point distance along the polyline, clamped to its ends.
A zero-length polyline (every vertex coincident) always returns its single location rather than dividing by zero -- the degenerate case should collapse gracefully, not explode.
Source code in src/geomotif/core/sampling.py
point_at_fraction
¶
points_at
¶
Return the point at each distance, in the order they were asked for.
Exactly what calling :meth:point_at on each in turn returns, and
several times faster for the run of lookups that resampling actually
performs. Those arrive in increasing order, so the segment holding one
is at or after the segment that held the last, and the whole run walks
the table once between them instead of binary-searching all of it every
time.
Order is exploited, never assumed: a distance that goes backwards seeks again, so this has no precondition to get wrong and no fast and slow version to keep in agreement.
Source code in src/geomotif/core/sampling.py
segment
¶
Return the part of the polyline lying between two distances.
The ends are exact -- interpolated where they fall inside a segment -- and every vertex between them is kept as it was, so this is a piece of the polyline rather than a resampling of one. That is what an animation drawing itself on needs: the geometry so far, at the resolution it was built at.
Distances outside the polyline clamp to its ends, and a range that collapses to a point returns that one point.
Source code in src/geomotif/core/sampling.py
points_at_fractions
¶
Return the point at each fraction of the total length.
samples_for_turns
¶
Return a sensible densification count for a curve spanning turns.
Sample density has to scale with how much the curve actually bends, or a tightly wound motif is measured by a polyline that cuts every corner. This is the adaptive heuristic the spiral generator used, exposed so every motif can share one answer.
Source code in src/geomotif/core/sampling.py
densify
¶
densify(fn: Callable[[float], Point], *, samples: int, domain: tuple[float, float] = (0.0, 1.0)) -> tuple[Point, ...]
Evaluate fn at evenly spaced parameters across domain.
Returns samples + 1 points, so both endpoints of the domain are
included and the result contains exactly samples segments.
Source code in src/geomotif/core/sampling.py
resample_path
¶
resample_path(path: Path, count: int | None = None, *, step: float | None = None, spacing: SpacingLike | None = None, by: Placement = 'length') -> Path
Return path resampled to count points, or at a fixed step.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
Path
|
The polyline to resample. |
required |
count
|
int
|
Total number of points to return. Must be >= 2. Mutually exclusive
with |
None
|
step
|
float
|
Fixed real distance between consecutive points; the count falls out
of the geometry. Any remainder shorter than |
None
|
spacing
|
SpacingCurve or callable
|
Distribution of points along the path. Defaults to equal spacing.
Cannot be combined with |
None
|
by
|
('length', 'parameter')
|
|
"length"
|
Returns:
| Type | Description |
|---|---|
Path
|
The resampled path, preserving |
Source code in src/geomotif/core/sampling.py
258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 | |
resample
¶
resample(design: Design, count: int | None = None, *, step: float | None = None, spacing: SpacingLike | None = None, distribute: Distribution = 'length', by: Placement = 'length') -> Design
Return design resampled across all of its paths.
Loose points are passed through untouched: they are already exactly the points the motif meant, with no curve to redistribute them along.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
design
|
Design
|
The design to resample. |
required |
count
|
int
|
Total number of points. How it is split across paths depends on
|
None
|
step
|
float
|
Fixed distance between consecutive points, applied independently to
every path. |
None
|
spacing
|
SpacingCurve or callable
|
Distribution of points along each path. |
None
|
distribute
|
('length', 'even', 'per_path')
|
How a total
|
"length"
|
by
|
('length', 'parameter')
|
Placement mode along each individual path; see :func: |
"length"
|
Returns:
| Type | Description |
|---|---|
Design
|
A new design with the same loose points and metadata. |
Source code in src/geomotif/core/sampling.py
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 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 | |