"""``@wp.func`` building blocks for plugin kernels.
Each is a ``wp.Function``: call it from inside your own ``@wp.kernel`` (or another
``@wp.func``), e.g. ``from track_gen.ops import funcs`` then ``funcs.catmull_rom(...)``
in the kernel body. They are the device functions the built-in generators use: the
same operations in the same order. On CPU a plugin kernel that composes them reproduces
the built-in arithmetic bit for bit. On CUDA (the module option ``fuse_fp``, default
``True``; ``wp.set_module_options({"fuse_fp": ...})``)
the compiler may contract a multiply and an add into one FMA, and which pairs it
contracts depends on the code around the inlined call, so a different surrounding
kernel can round differently in the last bit.
The three crossing predicates differ on purpose: :func:`segments_cross` is the strict
sign test used by gate validity; the polygon crossing counter
(:func:`track_gen.ops.self_intersections`) adds a scale-relative collinearity
tolerance on top of :func:`ccw`.
"""
from __future__ import annotations
import warp as wp
from .hash import hash32
__all__ = ["catmull_rom", "ccw", "cross2", "hash32", "nan0", "safe_normalize2",
"segments_cross"]
[docs]
@wp.func
def ccw(ox: float, oy: float, px: float, py: float, qx: float, qy: float) -> float:
"""Orientation of ``(o, p, q)``: ``cross(p - o, q - o)``, i.e. twice the signed
triangle area; ``> 0`` when ``o -> p -> q`` turns counter-clockwise."""
# Returns (q.y-o.y)*(p.x-o.x) - (p.y-o.y)*(q.x-o.x)
return (qy - oy) * (px - ox) - (py - oy) * (qx - ox)
[docs]
@wp.func
def nan0(x: float) -> float:
"""``x`` if finite, else ``0.0`` (``nan_to_num``; a no-op on finite inputs)."""
# NaN/inf -> 0.0 guard (nan_to_num equivalent) for the border self-intersection
# check. wp.where(cond, if_true, if_false) is Warp's non-deprecated select
# primitive. A NO-OP for finite inputs.
return wp.where(wp.isfinite(x), x, 0.0)
[docs]
@wp.func
def cross2(a: wp.vec2f, b: wp.vec2f) -> float:
"""2-D cross product ``a.x*b.y - a.y*b.x``."""
return a[0] * b[1] - a[1] * b[0]
[docs]
@wp.func
def safe_normalize2(v: wp.vec2f) -> wp.vec2f:
"""``v / max(|v|, 1e-8)``: unit vector with a length floor.
An all-NaN vector stays all-NaN; a NaN length is floored to ``1e-8``, so a vector
with one NaN component keeps the other, scaled by ``1e8`` (e.g. ``(nan, 3)`` ->
``(nan, 3e8)``)."""
# Mirrors geometry.safe_normalize: v / clamp_min(||v||, 1e-8). wp.max floors the
# length at 1e-8 and also maps a NaN length to 1e-8, so only the NaN components of
# v come out NaN (an all-NaN vector, e.g. a pruned corner, stays all-NaN).
return v / wp.max(wp.length(v), 1.0e-8)
[docs]
@wp.func
def catmull_rom(p0: wp.vec2f, p1: wp.vec2f, p2: wp.vec2f, p3: wp.vec2f, u: float) -> wp.vec2f:
"""Uniform Catmull-Rom (tension 0.5) segment ``p1 -> p2`` at ``u`` in ``[0, 1]``
(``u = 0`` gives ``p1``, ``u = 1`` gives ``p2``)."""
u2 = u * u
u3 = u2 * u
return 0.5 * (
(2.0 * p1)
+ (-p0 + p2) * u
+ (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * u2
+ (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * u3
)
[docs]
@wp.func
def segments_cross(a: wp.vec2f, b: wp.vec2f, c: wp.vec2f, d: wp.vec2f) -> int:
"""1 if segments ``ab`` and ``cd`` properly cross (strict sign test: touching
or collinear overlap is not a crossing), else 0."""
ab = b - a
cd = d - c
ac = c - a
ad = d - a
ca = a - c
cb = b - c
o1 = cross2(ab, ac)
o2 = cross2(ab, ad)
o3 = cross2(cd, ca)
o4 = cross2(cd, cb)
hit_ab = (o1 > 0.0 and o2 < 0.0) or (o1 < 0.0 and o2 > 0.0)
hit_cd = (o3 > 0.0 and o4 < 0.0) or (o3 < 0.0 and o4 > 0.0)
if hit_ab and hit_cd:
return int(1)
return int(0)