- added sdf folder ( doesnt work via pip or git=)
This commit is contained in:
366
sdf/dn.py
Normal file
366
sdf/dn.py
Normal file
@@ -0,0 +1,366 @@
|
||||
import itertools
|
||||
from functools import reduce, partial
|
||||
import warnings
|
||||
|
||||
from . import ease
|
||||
|
||||
import numpy as np
|
||||
|
||||
_min = np.minimum
|
||||
_max = np.maximum
|
||||
|
||||
|
||||
def distance_to_plane(p, origin, normal):
|
||||
"""
|
||||
Calculate the distance of a point ``p`` to the plane around ``origin`` with
|
||||
normal ``normal``. This is dimension-independent, so e.g. the z-coordinate
|
||||
can be omitted.
|
||||
|
||||
Args:
|
||||
p (array): either [x,y,z] or [[x,y,z],[x,y,z],...]
|
||||
origin (vector): a point on the plane
|
||||
normal (vector): normal vector of the plane
|
||||
|
||||
Returns:
|
||||
int: distance to plane
|
||||
"""
|
||||
normal = normal / np.linalg.norm(normal)
|
||||
return abs((p - origin) @ normal)
|
||||
|
||||
|
||||
def minimum(a, b, r=0):
|
||||
if r:
|
||||
Δ = b - a
|
||||
h = np.clip(0.5 + 0.5 * Δ / r, 0, 1)
|
||||
return b - Δ * h - r * h * (1 - h)
|
||||
else:
|
||||
return np.minimum(a, b)
|
||||
|
||||
|
||||
def maximum(a, b, r=0):
|
||||
if r:
|
||||
Δ = b - a
|
||||
h = np.clip(0.5 - 0.5 * Δ / r, 0, 1)
|
||||
return b - Δ * h + r * h * (1 - h)
|
||||
else:
|
||||
return np.maximum(a, b)
|
||||
|
||||
|
||||
def union(*sdfs, chamfer=0, c=0, radius=0, r=0, fillet=0, f=0):
|
||||
c = max(chamfer, c)
|
||||
r = max(radius, r, fillet, f)
|
||||
sqrt05 = np.sqrt(0.5)
|
||||
|
||||
def f(p):
|
||||
sdfs_ = iter(sdfs)
|
||||
d1 = next(sdfs_)(p)
|
||||
for sdf in sdfs_:
|
||||
d2 = sdf(p)
|
||||
R = r or getattr(sdf, "_r", 0)
|
||||
C = c or getattr(sdf, "_c", 0)
|
||||
parts = (d1, d2)
|
||||
if C:
|
||||
parts = (minimum(d1, d2), (d1 + d2 - C) * sqrt05)
|
||||
d1 = minimum(*parts, R)
|
||||
|
||||
return d1
|
||||
|
||||
return f
|
||||
|
||||
|
||||
def intersection(*sdfs, chamfer=0, c=0, radius=0, r=0, fillet=0, f=0):
|
||||
c = max(chamfer, c)
|
||||
r = max(radius, r, fillet, f)
|
||||
sqrt05 = np.sqrt(0.5)
|
||||
|
||||
def f(p):
|
||||
sdfs_ = iter(sdfs)
|
||||
d1 = next(sdfs_)(p)
|
||||
for sdf in sdfs_:
|
||||
d2 = sdf(p)
|
||||
R = r or getattr(sdf, "_r", 0)
|
||||
C = c or getattr(sdf, "_c", 0)
|
||||
parts = (d1, d2)
|
||||
if C:
|
||||
parts = (maximum(d1, d2), (d1 + d2 + C) * sqrt05)
|
||||
d1 = maximum(*parts, R)
|
||||
|
||||
return d1
|
||||
|
||||
return f
|
||||
|
||||
|
||||
def difference(*sdfs, chamfer=0, c=0, radius=0, r=0, fillet=0, f=0):
|
||||
c = max(chamfer, c)
|
||||
r = max(radius, r, fillet, f)
|
||||
sqrt05 = np.sqrt(0.5)
|
||||
|
||||
def f(p):
|
||||
sdfs_ = iter(sdfs)
|
||||
d1 = next(sdfs_)(p)
|
||||
for sdf in sdfs_:
|
||||
d2 = sdf(p)
|
||||
R = r or getattr(sdf, "_r", 0)
|
||||
C = c or getattr(sdf, "_c", 0)
|
||||
parts = (d1, -d2)
|
||||
if C:
|
||||
parts = (maximum(d1, -d2), (d1 - d2 + C) * sqrt05)
|
||||
d1 = maximum(*parts, R)
|
||||
|
||||
return d1
|
||||
|
||||
return f
|
||||
|
||||
|
||||
def union_legacy(a, *bs, r=None):
|
||||
def f(p):
|
||||
d1 = a(p)
|
||||
for b in bs:
|
||||
d2 = b(p)
|
||||
K = k or getattr(b, "_r", None)
|
||||
if K is None:
|
||||
d1 = _min(d1, d2)
|
||||
else:
|
||||
h = np.clip(0.5 + 0.5 * (d2 - d1) / K, 0, 1)
|
||||
m = d2 + (d1 - d2) * h
|
||||
d1 = m - K * h * (1 - h)
|
||||
return d1
|
||||
|
||||
return f
|
||||
|
||||
|
||||
def difference_legacy(a, *bs, r=None):
|
||||
def f(p):
|
||||
d1 = a(p)
|
||||
for b in bs:
|
||||
d2 = b(p)
|
||||
K = k or getattr(b, "_r", None)
|
||||
if K is None:
|
||||
d1 = _max(d1, -d2)
|
||||
else:
|
||||
h = np.clip(0.5 - 0.5 * (d2 + d1) / K, 0, 1)
|
||||
m = d1 + (-d2 - d1) * h
|
||||
d1 = m + K * h * (1 - h)
|
||||
return d1
|
||||
|
||||
return f
|
||||
|
||||
|
||||
def intersection_legacy(a, *bs, r=None):
|
||||
def f(p):
|
||||
d1 = a(p)
|
||||
for b in bs:
|
||||
d2 = b(p)
|
||||
K = k or getattr(b, "_r", None)
|
||||
if K is None:
|
||||
d1 = _max(d1, d2)
|
||||
else:
|
||||
h = np.clip(0.5 - 0.5 * (d2 - d1) / K, 0, 1)
|
||||
m = d2 + (d1 - d2) * h
|
||||
d1 = m + K * h * (1 - h)
|
||||
return d1
|
||||
|
||||
return f
|
||||
|
||||
|
||||
def blend(a, *bs, r=0.5):
|
||||
def f(p):
|
||||
d1 = a(p)
|
||||
for b in bs:
|
||||
d2 = b(p)
|
||||
K = k or getattr(b, "_r", None)
|
||||
d1 = K * d2 + (1 - K) * d1
|
||||
return d1
|
||||
|
||||
return f
|
||||
|
||||
|
||||
def negate(other):
|
||||
def f(p):
|
||||
return -other(p)
|
||||
|
||||
return f
|
||||
|
||||
|
||||
def dilate(other, r):
|
||||
def f(p):
|
||||
return other(p) - r
|
||||
|
||||
return f
|
||||
|
||||
|
||||
def erode(other, r):
|
||||
def f(p):
|
||||
return other(p) + r
|
||||
|
||||
return f
|
||||
|
||||
|
||||
def shell(other, thickness=1, type="center"):
|
||||
"""
|
||||
Keep only a margin of a given thickness around the object's boundary.
|
||||
|
||||
Args:
|
||||
thickness (float): the resulting thickness
|
||||
type (str): what kind of shell to generate.
|
||||
|
||||
``"center"`` (default)
|
||||
shell is spaced symmetrically around boundary
|
||||
``"outer"``
|
||||
the resulting shell will be ``thickness`` larger than before
|
||||
``"inner"``
|
||||
the resulting shell will be as large as before
|
||||
"""
|
||||
return dict(
|
||||
center=lambda p: np.abs(other(p)) - thickness / 2,
|
||||
inner=other - other.erode(thickness),
|
||||
outer=other.dilate(thickness) - other,
|
||||
)[type]
|
||||
|
||||
|
||||
def modulate_between(sdf, a, b, e=ease.in_out_cubic):
|
||||
"""
|
||||
Apply a distance offset transition between two control points
|
||||
(e.g. make a rod thicker or thinner at some point or add a bump)
|
||||
|
||||
Args:
|
||||
a, b (vectors): the two control points
|
||||
e (scalar function): the distance offset function, will be called with
|
||||
values between 0 (at control point ``a``) and 1 (at control point
|
||||
``b``). Its result will be subtracted from the given SDF, thus
|
||||
enlarging the object by that value.
|
||||
"""
|
||||
|
||||
# unit vector from control point a to b
|
||||
ab = (ab := b - a) / (L := np.linalg.norm(ab))
|
||||
|
||||
def f(p):
|
||||
# project current point onto control direction, clip and apply easing
|
||||
offset = e(np.clip((p - a) @ ab / L, 0, 1))
|
||||
return (dist := sdf(p)) - offset.reshape(dist.shape)
|
||||
|
||||
return f
|
||||
|
||||
|
||||
def stretch(sdf, a, b, symmetric=False, e=ease.linear):
|
||||
"""
|
||||
Grab the object at point ``a`` and stretch the entire plane to ``b``.
|
||||
|
||||
Args:
|
||||
a, b (point vectors): the control points
|
||||
symmetric (bool): also stretch the same into the other direction.
|
||||
e (Easing): easing to apply
|
||||
|
||||
Examples
|
||||
========
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# make a capsule
|
||||
sphere(5).stretch(ORIGIN, 10*Z).save() # same as capsule(ORIGIN, 10*Z, 5)
|
||||
# make an egg
|
||||
sphere(5).stretch(ORIGIN, 10*Z, e=ease.smoothstep[:0.44]).save()
|
||||
"""
|
||||
ab = (ab := b - a) / (L := np.linalg.norm(ab))
|
||||
|
||||
def f(p):
|
||||
# s = ”how far are we between a and b as fraction?”
|
||||
# if symmetric=True this also goes into the negative direction
|
||||
s = np.clip((p - a) @ ab / L, -1 if symmetric else 0, 1)
|
||||
# we return the sdf at a point 'behind' (p minus ...)
|
||||
# the current point, but we go only as far back as the stretch distance
|
||||
# at max
|
||||
return sdf(p - (np.sign(s) * e(abs(s)) * L * ab[:, np.newaxis]).T)
|
||||
|
||||
return f
|
||||
|
||||
|
||||
def shear(sdf, fix, grab, move, e=ease.linear):
|
||||
"""
|
||||
Grab the object at point ``grab`` and shear the entire plane in direction
|
||||
``move``, keeping point ``fix`` in place. If ``move`` is orthogonal to the
|
||||
direction ``fix``->``grab``, then this operation is a shear.
|
||||
|
||||
Args:
|
||||
fix, grab (point vectors): the control points
|
||||
move (point vector): direction to shear to
|
||||
e (Easing): easing to apply
|
||||
|
||||
Examples
|
||||
========
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# make a capsule
|
||||
box([20,10,50]).shear(fix=-15*Z, grab=15*Z, move=-5*X, e=ease.smoothstep)
|
||||
"""
|
||||
ab = (ab := grab - fix) / (L := np.linalg.norm(ab))
|
||||
|
||||
def f(p):
|
||||
# s = ”how far are we between a and b as fraction?”
|
||||
s = (p - fix) @ ab / L
|
||||
return sdf(p - move * np.expand_dims(e(np.clip(s, 0, 1)), axis=1))
|
||||
|
||||
return f
|
||||
|
||||
|
||||
def mirror(other, direction, at=0):
|
||||
"""
|
||||
Mirror around a given plane defined by ``origin`` reference point and
|
||||
``direction``.
|
||||
|
||||
Args:
|
||||
direction (vector): direction to mirror to (e.g. :any:`X` to mirror along X axis)
|
||||
at (3D vector): point to mirror at. Default is the origin.
|
||||
"""
|
||||
direction = direction / np.linalg.norm(direction)
|
||||
|
||||
def f(p):
|
||||
projdir = np.expand_dims((p - at) @ direction, axis=1) * direction
|
||||
# mirrored point:
|
||||
# - project 'p' onto 'direction' (result goes into 'projdir' direction)
|
||||
# - projected point is at 'at + projdir'
|
||||
# - remember direction from projected point to the original point (p - (at + projdir))
|
||||
# - from origin 'at' go backwards the projected direction (at - projdir)
|
||||
# - from that target, move along the remembered direction (p - (at + projdir))
|
||||
# - pmirr = at - projdir + (p - (at + projdir))
|
||||
# - the 'at' cancels out, the projdir is subtracted twice from the point
|
||||
return other(p - 2 * projdir)
|
||||
|
||||
return f
|
||||
|
||||
|
||||
def repeat(other, spacing, count=None, padding=0):
|
||||
count = np.array(count) if count is not None else None
|
||||
spacing = np.array(spacing)
|
||||
|
||||
def neighbors(dim, padding, spacing):
|
||||
try:
|
||||
padding = [padding[i] for i in range(dim)]
|
||||
except Exception:
|
||||
padding = [padding] * dim
|
||||
try:
|
||||
spacing = [spacing[i] for i in range(dim)]
|
||||
except Exception:
|
||||
spacing = [spacing] * dim
|
||||
for i, s in enumerate(spacing):
|
||||
if s == 0:
|
||||
padding[i] = 0
|
||||
axes = [list(range(-p, p + 1)) for p in padding]
|
||||
return list(itertools.product(*axes))
|
||||
|
||||
def f(p):
|
||||
q = np.divide(p, spacing, out=np.zeros_like(p), where=spacing != 0)
|
||||
if count is None:
|
||||
index = np.round(q)
|
||||
else:
|
||||
index = np.clip(np.round(q), -count, count)
|
||||
|
||||
indexes = [index + n for n in neighbors(p.shape[-1], padding, spacing)]
|
||||
A = [other(p - spacing * i) for i in indexes]
|
||||
a = A[0]
|
||||
for b in A[1:]:
|
||||
a = _min(a, b)
|
||||
return a
|
||||
|
||||
return f
|
||||
Reference in New Issue
Block a user