367 lines
9.8 KiB
Python
367 lines
9.8 KiB
Python
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
|