391 lines
7.9 KiB
Python
391 lines
7.9 KiB
Python
import functools
|
|
import numpy as np
|
|
import operator
|
|
import copy
|
|
|
|
from . import dn, d3, ease
|
|
|
|
# Constants
|
|
|
|
ORIGIN = np.array((0, 0))
|
|
|
|
X = np.array((1, 0))
|
|
Y = np.array((0, 1))
|
|
|
|
UP = Y
|
|
|
|
# SDF Class
|
|
|
|
_ops = {}
|
|
|
|
|
|
class SDF2:
|
|
def __init__(self, f):
|
|
self.f = f
|
|
|
|
def __call__(self, p):
|
|
return self.f(p).reshape((-1, 1))
|
|
|
|
def __getattr__(self, name):
|
|
if name in _ops:
|
|
f = _ops[name]
|
|
return functools.partial(f, self)
|
|
raise AttributeError
|
|
|
|
def __or__(self, other):
|
|
return union(self, other)
|
|
|
|
def __and__(self, other):
|
|
return intersection(self, other)
|
|
|
|
def __sub__(self, other):
|
|
return difference(self, other)
|
|
|
|
def fillet(self, r):
|
|
newSelf = copy.deepcopy(self)
|
|
newSelf._r = r
|
|
return newSelf
|
|
|
|
def radius(self, *args, **kwargs):
|
|
return self.fillet(*args, **kwargs)
|
|
|
|
def k(self, *args, **kwargs):
|
|
return self.fillet(*args, **kwargs)
|
|
|
|
def r(self, *args, **kwargs):
|
|
return self.fillet(*args, **kwargs)
|
|
|
|
def chamfer(self, c):
|
|
newSelf = copy.deepcopy(self)
|
|
newSelf._c = c
|
|
return newSelf
|
|
|
|
def c(self, *args, **kwargs):
|
|
return self.chamfer(*args, **kwargs)
|
|
|
|
|
|
def sdf2(f):
|
|
@functools.wraps(f)
|
|
def wrapper(*args, **kwargs):
|
|
return SDF2(f(*args, **kwargs))
|
|
|
|
return wrapper
|
|
|
|
|
|
def op2(f):
|
|
@functools.wraps(f)
|
|
def wrapper(*args, **kwargs):
|
|
return SDF2(f(*args, **kwargs))
|
|
|
|
_ops[f.__name__] = wrapper
|
|
return wrapper
|
|
|
|
|
|
def op23(f):
|
|
@functools.wraps(f)
|
|
def wrapper(*args, **kwargs):
|
|
return d3.SDF3(f(*args, **kwargs))
|
|
|
|
_ops[f.__name__] = wrapper
|
|
return wrapper
|
|
|
|
|
|
# Helpers
|
|
|
|
|
|
def _length(a):
|
|
return np.linalg.norm(a, axis=1)
|
|
|
|
|
|
def _normalize(a):
|
|
return a / np.linalg.norm(a)
|
|
|
|
|
|
def _dot(a, b):
|
|
return np.sum(a * b, axis=1)
|
|
|
|
|
|
def _vec(*arrs):
|
|
return np.stack(arrs, axis=-1)
|
|
|
|
|
|
_min = np.minimum
|
|
_max = np.maximum
|
|
|
|
# Primitives
|
|
|
|
|
|
@sdf2
|
|
def circle(radius=None, diameter=None, center=ORIGIN):
|
|
if (radius is not None) == (diameter is not None):
|
|
raise ValueError(f"Specify either radius or diameter")
|
|
if radius is None:
|
|
radius = diameter / 2
|
|
|
|
def f(p):
|
|
return _length(p - center) - radius
|
|
|
|
return f
|
|
|
|
|
|
@sdf2
|
|
def line(normal=UP, point=ORIGIN):
|
|
normal = _normalize(normal)
|
|
|
|
def f(p):
|
|
return np.dot(point - p, normal)
|
|
|
|
return f
|
|
|
|
|
|
@sdf2
|
|
def slab(x0=None, y0=None, x1=None, y1=None, r=None):
|
|
fs = []
|
|
if x0 is not None:
|
|
fs.append(line(X, (x0, 0)))
|
|
if x1 is not None:
|
|
fs.append(line(-X, (x1, 0)))
|
|
if y0 is not None:
|
|
fs.append(line(Y, (0, y0)))
|
|
if y1 is not None:
|
|
fs.append(line(-Y, (0, y1)))
|
|
return intersection(*fs, r=r)
|
|
|
|
|
|
@sdf2
|
|
def rectangle(size=1, center=ORIGIN, a=None, b=None):
|
|
if a is not None and b is not None:
|
|
a = np.array(a)
|
|
b = np.array(b)
|
|
size = b - a
|
|
center = a + size / 2
|
|
return rectangle(size, center)
|
|
size = np.array(size)
|
|
|
|
def f(p):
|
|
q = np.abs(p - center) - size / 2
|
|
return _length(_max(q, 0)) + _min(np.amax(q, axis=1), 0)
|
|
|
|
return f
|
|
|
|
|
|
@sdf2
|
|
def rounded_rectangle(size, radius, center=ORIGIN):
|
|
try:
|
|
r0, r1, r2, r3 = radius
|
|
except TypeError:
|
|
r0 = r1 = r2 = r3 = radius
|
|
|
|
def f(p):
|
|
x = p[:, 0]
|
|
y = p[:, 1]
|
|
r = np.zeros(len(p)).reshape((-1, 1))
|
|
r[np.logical_and(x > 0, y > 0)] = r0
|
|
r[np.logical_and(x > 0, y <= 0)] = r1
|
|
r[np.logical_and(x <= 0, y <= 0)] = r2
|
|
r[np.logical_and(x <= 0, y > 0)] = r3
|
|
q = np.abs(p) - size / 2 + r
|
|
return (
|
|
_min(_max(q[:, 0], q[:, 1]), 0).reshape((-1, 1))
|
|
+ _length(_max(q, 0)).reshape((-1, 1))
|
|
- r
|
|
)
|
|
|
|
return f
|
|
|
|
|
|
@sdf2
|
|
def equilateral_triangle():
|
|
def f(p):
|
|
k = 3**0.5
|
|
p = _vec(np.abs(p[:, 0]) - 1, p[:, 1] + 1 / k)
|
|
w = p[:, 0] + k * p[:, 1] > 0
|
|
q = _vec(p[:, 0] - k * p[:, 1], -k * p[:, 0] - p[:, 1]) / 2
|
|
p = np.where(w.reshape((-1, 1)), q, p)
|
|
p = _vec(p[:, 0] - np.clip(p[:, 0], -2, 0), p[:, 1])
|
|
return -_length(p) * np.sign(p[:, 1])
|
|
|
|
return f
|
|
|
|
|
|
@sdf2
|
|
def hexagon(radius=None, diameter=None):
|
|
if (radius is not None) == (diameter is not None):
|
|
raise ValueError(f"Specify either radius or diameter")
|
|
if radius is None:
|
|
radius = diameter / 2
|
|
radius *= 3**0.5 / 2
|
|
|
|
def f(p):
|
|
k = np.array((3**0.5 / -2, 0.5, np.tan(np.pi / 6)))
|
|
p = np.abs(p)
|
|
p -= 2 * k[:2] * _min(_dot(k[:2], p), 0).reshape((-1, 1))
|
|
p -= _vec(
|
|
np.clip(p[:, 0], -k[2] * radius, k[2] * radius), np.zeros(len(p)) + radius
|
|
)
|
|
return _length(p) * np.sign(p[:, 1])
|
|
|
|
return f
|
|
|
|
|
|
@sdf2
|
|
def rounded_x(w, r):
|
|
def f(p):
|
|
p = np.abs(p)
|
|
q = (_min(p[:, 0] + p[:, 1], w) * 0.5).reshape((-1, 1))
|
|
return _length(p - q) - r
|
|
|
|
return f
|
|
|
|
|
|
def RegularPolygon(n, r=1):
|
|
ri = r * np.cos(np.pi / n)
|
|
return intersection(
|
|
*[slab(y0=-ri).rotate(a) for a in np.arange(0, 2 * np.pi, 2 * np.pi / n)]
|
|
)
|
|
|
|
|
|
@sdf2
|
|
def polygon(points):
|
|
points = [np.array(p) for p in points]
|
|
|
|
def f(p):
|
|
n = len(points)
|
|
d = _dot(p - points[0], p - points[0])
|
|
s = np.ones(len(p))
|
|
for i in range(n):
|
|
j = (i + n - 1) % n
|
|
vi = points[i]
|
|
vj = points[j]
|
|
e = vj - vi
|
|
w = p - vi
|
|
b = w - e * np.clip(np.dot(w, e) / np.dot(e, e), 0, 1).reshape((-1, 1))
|
|
d = _min(d, _dot(b, b))
|
|
c1 = p[:, 1] >= vi[1]
|
|
c2 = p[:, 1] < vj[1]
|
|
c3 = e[0] * w[:, 1] > e[1] * w[:, 0]
|
|
c = _vec(c1, c2, c3)
|
|
s = np.where(np.all(c, axis=1) | np.all(~c, axis=1), -s, s)
|
|
return s * np.sqrt(d)
|
|
|
|
return f
|
|
|
|
|
|
# Positioning
|
|
|
|
|
|
@op2
|
|
def translate(other, offset):
|
|
def f(p):
|
|
return other(p - offset)
|
|
|
|
return f
|
|
|
|
|
|
@op2
|
|
def scale(other, factor):
|
|
try:
|
|
x, y = factor
|
|
except TypeError:
|
|
x = y = factor
|
|
s = (x, y)
|
|
m = min(x, y)
|
|
|
|
def f(p):
|
|
return other(p / s) * m
|
|
|
|
return f
|
|
|
|
|
|
@op2
|
|
def rotate(other, angle):
|
|
s = np.sin(angle)
|
|
c = np.cos(angle)
|
|
m = 1 - c
|
|
matrix = np.array(
|
|
[
|
|
[c, -s],
|
|
[s, c],
|
|
]
|
|
).T
|
|
|
|
def f(p):
|
|
return other(np.dot(p, matrix))
|
|
|
|
return f
|
|
|
|
|
|
@op2
|
|
def circular_array(other, count):
|
|
angles = [i / count * 2 * np.pi for i in range(count)]
|
|
return union(*[other.rotate(a) for a in angles])
|
|
|
|
|
|
# Alterations
|
|
|
|
|
|
@op2
|
|
def elongate(other, size):
|
|
def f(p):
|
|
q = np.abs(p) - size
|
|
x = q[:, 0].reshape((-1, 1))
|
|
y = q[:, 1].reshape((-1, 1))
|
|
w = _min(_max(x, y), 0)
|
|
return other(_max(q, 0)) + w
|
|
|
|
return f
|
|
|
|
|
|
# 2D => 3D Operations
|
|
|
|
|
|
@op23
|
|
def extrude(other, h=np.inf):
|
|
def f(p):
|
|
d = other(p[:, [0, 1]])
|
|
w = _vec(d.reshape(-1), np.abs(p[:, 2]) - h / 2)
|
|
return _min(_max(w[:, 0], w[:, 1]), 0) + _length(_max(w, 0))
|
|
|
|
return f
|
|
|
|
|
|
@op23
|
|
def extrude_to(a, b, h, e=ease.linear):
|
|
def f(p):
|
|
d1 = a(p[:, [0, 1]])
|
|
d2 = b(p[:, [0, 1]])
|
|
t = e(np.clip(p[:, 2] / h, -0.5, 0.5) + 0.5)
|
|
d = d1 + (d2 - d1) * t.reshape((-1, 1))
|
|
w = _vec(d.reshape(-1), np.abs(p[:, 2]) - h / 2)
|
|
return _min(_max(w[:, 0], w[:, 1]), 0) + _length(_max(w, 0))
|
|
|
|
return f
|
|
|
|
|
|
@op23
|
|
def revolve(other, offset=0):
|
|
def f(p):
|
|
xy = p[:, [0, 1]]
|
|
# use horizontal distance to Z axis as X coordinate in 2D shape
|
|
# use Z coordinate as Y coordinate in 2D shape
|
|
q = _vec(_length(xy) - offset, p[:, 2])
|
|
return other(q)
|
|
|
|
return f
|
|
|
|
|
|
# Common
|
|
|
|
union = op2(dn.union)
|
|
difference = op2(dn.difference)
|
|
intersection = op2(dn.intersection)
|
|
blend = op2(dn.blend)
|
|
negate = op2(dn.negate)
|
|
dilate = op2(dn.dilate)
|
|
erode = op2(dn.erode)
|
|
shell = op2(dn.shell)
|
|
repeat = op2(dn.repeat)
|
|
mirror = op2(dn.mirror)
|
|
modulate_between = op2(dn.modulate_between)
|
|
stretch = op2(dn.stretch)
|