def offset(a, o): """ Assumes a linear distance field for bounds calculations! """ if o < 0: return Shape('-%sf%g' % (a.math, o), a.bounds.xmin, a.bounds.ymin, a.bounds.zmin, a.bounds.xmax, a.bounds.ymax, a.bounds.zmax) else: return Shape('-%sf%g' % (a.math, o), a.bounds.xmin - o, a.bounds.ymin - o, a.bounds.zmin - o, a.bounds.xmax + o, a.bounds.ymax + o, a.bounds.zmax + o)
def polygon_radius(x, y, r, N): """ Makes a polygon with a center-to-vertex distance r The polygon is oriented so that the bottom is always flat. """ # Find the center-to-edge distance r_ = -r * math.cos(math.pi / N) # Make an offset half-region shape half = Shape('-f%gY' % r_) # Take the union of a bunch of rotated half-region shapes p = functools.reduce(operator.and_, [rotate(half, 360. / N * i) for i in range(N)]) # Apply appropriate bounds and return return Shape(p.math, -r, -r, r, r)
def triangle(x0, y0, x1, y1, x2, y2): """ Defines a triangle from three points. """ # Find the angles of the points about the center xm = (x0 + x1 + x2) / 3. ym = (y0 + y1 + y2) / 3. angles = [ math.atan2(y - ym, x - xm) for x, y in [(x0, y0), (x1, y1), (x2, y2)] ] # Sort the angles so that the smallest one is first if angles[1] < angles[0] and angles[1] < angles[2]: angles = [angles[1], angles[2], angles[0]] elif angles[2] < angles[0] and angles[2] < angles[1]: angles = [angles[2], angles[0], angles[1]] # Enforce that points must be in clockwise order by swapping if necessary if angles[2] > angles[1]: x0, y0, x1, y1 = x1, y1, x0, y0 def edge(x, y, dx, dy): # dy*(X-x)-dx*(Y-y) return '-*f%(dy)g-Xf%(x)g*f%(dx)g-Yf%(y)g' % locals() e0 = edge(x0, y0, x1 - x0, y1 - y0) e1 = edge(x1, y1, x2 - x1, y2 - y1) e2 = edge(x2, y2, x0 - x2, y0 - y2) # -min(e0, min(e1, e2)) return Shape('ni%(e0)si%(e1)s%(e2)s' % locals(), min(x0, x1, x2), min(y0, y1, y2), max(x0, x1, x2), max(y0, y1, y2))
def right_triangle(x, y, w, h): # max(max(x-X,y-Y),X-(x*(Y-y)+(x+w)*(y+h-Y))/h) ws = math.copysign(1, w) hs = math.copysign(1, h) return Shape( 'aa*f%(ws)g-f%(x)gX*f%(hs)g-f%(y)gY*f%(ws)g-X/+*f%(x)g-Yf%(y)g*+f%(x)gf%(w)g-+f%(y)gf%(h)gYf%(h)g' % locals(), x, y, x + w, y + h)
def circle(x0, y0, r): # sqrt((X-x0)**2 + (Y-y0)**2) - r r = abs(r) return Shape( '-r+q%sq%sf%g' % (('-Xf%g' % x0) if x0 else 'X', ('-Yf%g' % y0) if y0 else 'Y', r), x0 - r, y0 - r, x0 + r, y0 + r)
def revolve_x(part): ''' Revolve a part in the XY plane about the X axis. ''' # Y' = sqrt(Y**2 + Z**2) p = part.map('', 'r+qYqZ', '', '') return Shape(p.math, part.xmin, part.xmax, min(-abs(part.ymin), -abs(part.ymax)), max(abs(part.ymin), abs(part.ymax)), part.ymin, part.ymax)
def revolve_y(part): ''' Revolve a part in the XY plane about the Y axis. ''' # X' = sqrt(X**2 + Z**2) p = part.map('r+qXqZ', '', '', '') return Shape(p.math, min(-abs(part.xmin), -abs(part.xmax)), max(abs(part.xmin), abs(part.xmax)), part.ymin, part.ymax, part.xmin, part.xmax)
def revolve_x(a): ''' Revolve a part in the XY plane about the X axis. ''' # Y' = +/- sqrt(Y**2 + Z**2) pos = a.map(Transform('', 'r+qYqZ', '', '', '', '')) neg = a.map(Transform('', 'nr+qYqZ', '', '', '', '')) m = max(abs(a.bounds.ymin), abs(a.bounds.ymax)) return Shape((pos | neg).math, a.bounds.xmin, -m, -m, a.bounds.xmax, m, m)
def circle(x, y, r): """ Defines a circle from a center point and radius. """ # sqrt((X-x)**2 + (Y-y)**2) - r r = abs(r) return Shape( '-r+q%sq%sf%g' % (('-Xf%g' % x) if x else 'X', ('-Yf%g' % y) if y else 'Y', r), x - r, y - r, x + r, y + r)
def invert(a): """ Inverts a shape within its existing bounds. """ if a.bounds.is_bounded_xyz(): return cube(a.bounds.xmin, a.bounds.xmax, a.bounds.ymin, a.bounds.zmax, a.bounds.zmin, a.bounds.zmax) & (~a) elif a.bounds.is_bounded_xy(): return rectangle(a.bounds.xmin, a.bounds.xmax, a.bounds.ymin, a.bounds.ymax) & (~a) else: return Shape()
def repel(part, x, y, z, r): # Shift the part so that it is centered part = move(part, -x, -y, -z) # exponential fallout value # x*(1 - exp(-sqrt(x**2 + y**2 + z**2) / r)) d = '-f1xn/r++qXqYqZf%g' % r p = part.map(Transform('*X' + d, '*Y' + d, '*Z' + d, '', '', '')) b = r / math.e return move( Shape(p.math, part.bounds.xmin - b, part.bounds.ymin - b, part.bounds.zmin - b, part.bounds.xmax + b, part.bounds.ymax + b, part.bounds.zmax + b), x, y, z)
def text(text, x, y, height=1, align='LB'): if text == '': return Shape() dx, dy = 0, -1 text_shape = None for line in text.split('\n'): line_shape = None for c in line: if not c in _glyphs.keys(): print('Warning: Unknown character "%s"' % c) else: chr_math = move(_glyphs[c], dx, dy) if line_shape is None: line_shape = chr_math else: line_shape |= chr_math dx += _widths[c] + 0.1 dx -= 0.1 if line_shape is not None: if align[0] == 'L': pass elif align[0] == 'C': line_shape = move(line_shape, -dx / 2, 0) elif align[0] == 'R': line_shape = move(line_shape, -dx, 0) if text_shape is None: text_shape = line_shape else: text_shape |= line_shape dy -= 1.55 dx = 0 dy += 1.55 if text_shape is None: return None if align[1] == 'T': pass elif align[1] == 'B': text_shape = move( text_shape, 0, -dy, ) elif align[1] == 'C': text_shape = move(text_shape, 0, -dy / 2) if height != 1: text_shape = scale_xy(text_shape, 0, 0, height) return move(text_shape, x, y)
def loft_xy_z(a, b, zmin, zmax): """ Creates a blended loft between two shapes. Input shapes should be 2D (in the XY plane). The resulting loft will be shape a at zmin and b at zmax. """ # ((z-zmin)/(zmax-zmin))*b + ((zmax-z)/(zmax-zmin))*a # In the prefix string below, we add caps at zmin and zmax then # factor out the division by (zmax - zmin) dz = zmax - zmin a_, b_ = a.math, b.math return Shape(('aa-Zf%(zmax)g-f%(zmin)g' + 'Z/+*-Zf%(zmin)g%(b_)s' + '*-f%(zmax)gZ%(a_)sf%(dz)g') % locals(), min(a.bounds.xmin, b.bounds.xmin), min(a.bounds.ymin, b.bounds.ymin), zmin, max(a.bounds.xmax, b.bounds.xmax), max(a.bounds.ymax, b.bounds.ymax), zmax)
def sphere(x0, y0, z0, r): return Shape( '-r++q%sq%sq%sf%g' % (('-Xf%g' % x0) if x0 else 'X', ('-Yf%g' % y0) if y0 else 'Y', ('-Zf%g' % z0) if z0 else 'Z', r), x0 - r, y0 - r, z0 - r, x0 + r, y0 + r, z0 + r)
def torus_z(x, y, z, R, r): return move( Shape('-r+q-f%(R)gr+qXqYqZf%(r)g' % locals(), -(R + r), -(R + r), -r, R + r, R + r, r), x, y, z)
def torus_y(x, y, z, R, r): # sqrt((R - sqrt((X-x)^2+(Z-z)^2))^2 + (Y-y)^2)-r return move( Shape('-r+q-f%(R)gr+qXqZqYf%(r)g' % locals(), -(R + r), -r, -(R + r), R + r, r, R + r), x, y, z)
def rectangle(x0, x1, y0, y1): # max(max(x0 - X, X - x1), max(y0 - Y, Y - y1) return Shape('aa-f%(x0)gX-Xf%(x1)ga-f%(y0)gY-Yf%(y1)g' % locals(), x0, y0, x1, y1)
def sphere(x, y, z, r): return Shape( '-r++q%sq%sq%sf%g' % (('-Xf%g' % x) if x else 'X', ('-Yf%g' % y) if y else 'Y', ('-Zf%g' % z) if z else 'Z', r), x - r, y - r, z - r, x + r, y + r, z + r)
def cylinder_y(x, ymin, ymax, z, r): from fab.types import Shape, Transform # max(sqrt((X-x)^2+(Z-z)^2)-r,max(ymin-Y,Y-ymax)) return Shape( 'a-r+q-Xf%(x)gq-Zf%(z)gf%(r)ga-f%(ymin)gY-Yf%(ymax)g' % locals(), x - r, ymin, z - r, x + r, ymax, z + r)
def set_color(a, r, g, b): """ Applies a given color to an input shape a and returns it. """ q = Shape(a.math, a.bounds) q._r, q._g, q._b = r, g, b return q
def morph(a, b, weight): """ Morphs between two shapes. """ # shape = weight*a+(1-weight)*b s = "+*f%g%s*f%g%s" % (weight, a.math, 1 - weight, b.math) return Shape(s, (a | b).bounds)
def blend(a, b, amount): joint = a | b # sqrt(abs(a)) + sqrt(abs(b)) - amount fillet = Shape('-+rb%srb%sf%g' % (a.math, b.math, amount), joint.bounds) return joint | fillet
def extrude_z(part, z0, z1): # max(part, max(z0-Z, Z-z1)) return Shape('am f1%sa-f%gZ-Zf%g' % (part.math, z0, z1), part.bounds.xmin, part.bounds.ymin, z0, part.bounds.xmax, part.bounds.ymax, z1)
def function_prefix_xyz(fn, xmin, xmax, ymin, ymax, zmin, zmax): """ Takes an arbitrary prefix math-string and makes it a function. Returns the function intersected with the given bounding cube. """ return Shape(fn) & cube(xmin, xmax, ymin, ymax, zmin, zmax)
def rectangle(xmin, xmax, ymin, ymax): # max(max(xmin - X, X - xmax), max(ymin - Y, Y - ymax) return Shape('aa-f%(xmin)gX-Xf%(xmax)ga-f%(ymin)gY-Yf%(ymax)g' % locals(), xmin, ymin, xmax, ymax)
def blend(p0, p1, amount): joint = p0 | p1 # sqrt(abs(p0)) + sqrt(abs(p1)) - amount fillet = Shape('-+rb%srb%sf%g' % (p0.math, p1.math, amount), joint.bounds) return joint | fillet
def cylinder_x(xmin, xmax, y, z, r): from fab.types import Shape, Transform # max(sqrt((Y-y)^2+(Z-z)^2)-r,max(xmin-X,X-xmax)) return Shape( 'a-r+q-Yf%(y)gq-Zf%(z)gf%(r)ga-f%(xmin)gX-Xf%(xmax)g' % locals(), xmin, y - r, z - r, xmax, y + r, z + r)
_widths['y'] = 0.55 _glyphs['y'] = shape shape = rectangle(0, 0.6, 0, 1) shape &= ~triangle(0, 0.1, 0, 0.9, 0.45, 0.9) shape &= ~triangle(0.6, 0.1, 0.15, 0.1, 0.6, 0.9) _widths['Z'] = 0.6 _glyphs['Z'] = shape shape = rectangle(0, 0.6, 0, 0.55) shape &= ~triangle(0, 0.1, 0, 0.45, 0.45, 0.45) shape &= ~triangle(0.6, 0.1, 0.15, 0.1, 0.6, 0.45) _widths['z'] = 0.6 _glyphs['z'] = shape shape = Shape("f1.0", 0, 0.55, 0, 1) _widths[' '] = 0.55 _glyphs[' '] = shape shape = circle(0.075, 0.075, 0.075) shape = scale_y(shape, 0.075, 3) shape &= rectangle(0.0, 0.15, -0.15, 0.075) shape &= ~triangle(0.075, 0.075, 0.0, -0.15, -0.5, 0.075) shape |= circle(0.1, 0.075, 0.075) _widths[','] = 0.175 _glyphs[','] = shape shape = circle(0.075, 0.075, 0.075) _widths['.'] = 0.15 _glyphs['.'] = shape
def extrude_z(part, zmin, zmax): # max(part, max(zmin-Z, Z-zmax)) return Shape('am__f1%sa-f%gZ-Zf%g' % (part.math, zmin, zmax), part.bounds.xmin, part.bounds.ymin, zmin, part.bounds.xmax, part.bounds.ymax, zmax)
def right_triangle(x, y, w, h): # max(max(x-X,y-Y),X-(x*(Y-y)+(x+w)*(y+h-Y))/h) return Shape( 'aa-f%(x)gX-f%(y)gY-X/+*f%(x)g-Yf%(y)g*+f%(x)gf%(w)g-+f%(y)gf%(h)gYf%(h)g' % locals(), x, y, x + w, y + h)