def __init__(self, stroke=1, fill=False, color=(0, 1, 0), closed=True, visible=True, path=[dict(x=0, y=0, z=0)], front=dict(z=1), backface=True, **kwargs): Anchor.__init__(self, **kwargs) self.stroke = stroke self.fill = fill self.color = color self.closed = closed self.visible = visible self.path = path self.backface = backface self.updatePath() self.front = Vector(**front) if type(front) is dict else front self.renderFront = Vector(self.front) self.renderNormal = Vector()
def renderDome(self, ctx, renderer): if not self.visible: return contourAngle = math.atan2(self.renderNormal.y, self.renderNormal.x) domeRadius = self.diameter / 2 # * self.renderNormal.magnitude() startAngle = contourAngle + TAU / 2 endAngle = contourAngle - TAU / 2 a = startAngle pts = [ Vector(x=domeRadius, y=0), Vector(x=domeRadius, y=domeRadius * 4 / 3 * tan(a / 4)), Vector(x=domeRadius * (cos(a) + 4 / 3 * tan(a / 4) * sin(a)), y=domeRadius * (sin(a) - 4 / 3 * tan(a / 4) * cos(a))), Vector(x=domeRadius * cos(a), y=domeRadius * sin(a)), ] pts = [p.add(self.renderOrigin) for p in pts] renderer.stroke(self.stroke, self.color, self.getLineWidth()) renderer.fill(self.fill, self.color) renderer.begin() renderer.move(pts[0]) renderer.bezier(pts[1], pts[2], pts[3]) # renderer.closePath() ctx.drawPath() renderer.end()
def __init__(self, method, points, previousPoint=None): self.method = method self.points = [ mapVectorPoint(point) for point in points ] self.renderPoints = [ mapNewVector(point) for point in points ] self.previousPoint = previousPoint # arc actions come with previous point & corner point # but require bezier control points if method == 'arc': self.controlPoints = [Vector(), Vector()]
def __init__(self, **kwargs): if 'length' in kwargs: self.length = kwargs['length'] Ellipse.__init__(self, **kwargs) # composite shape, create child shapes self.apex = Anchor( addTo=self, translate={'z': self.length}, ) # vectors used for calculation self.renderApex = Vector() self.tangentA = Vector() self.tangentB = Vector() self.surfacePathCommands = [ # points set in renderConeSurface PathCommand('move', [{}], None), PathCommand('line', [{}], None), PathCommand('line', [{}], None), ]
class Cone(Ellipse): length = 1 fill = True def __init__(self, **kwargs): if 'length' in kwargs: self.length = kwargs['length'] Ellipse.__init__(self, **kwargs) # composite shape, create child shapes self.apex = Anchor( addTo=self, translate={'z': self.length}, ) # vectors used for calculation self.renderApex = Vector() self.tangentA = Vector() self.tangentB = Vector() self.surfacePathCommands = [ # points set in renderConeSurface PathCommand('move', [{}], None), PathCommand('line', [{}], None), PathCommand('line', [{}], None), ] def __repr__(self): return '<zDogPy Cone>' def render(self, ctx, renderer): self.renderConeSurface(ctx, renderer) Ellipse.render(self, ctx, renderer) def renderConeSurface(self, ctx, renderer): if not self.visible: return self.renderApex.set(self.apex.renderOrigin) self.renderApex.subtract(self.renderOrigin) scale = self.renderNormal.magnitude() apexDistance = self.renderApex.magnitude2d() normalDistance = self.renderNormal.magnitude2d() # eccentricity eccenAngle = acos(normalDistance / scale) eccen = sin(eccenAngle) radius = self.diameter / 2 * scale # does apex extend beyond eclipse of face isApexVisible = radius * eccen < apexDistance if not isApexVisible: return # update tangents apexAngle = atan2(self.renderNormal.y, self.renderNormal.x) + TAU / 2 projectLength = apexDistance / eccen projectAngle = acos(radius / projectLength) # set tangent points tangentA = self.tangentA tangentB = self.tangentB tangentA.x = cos(projectAngle) * radius * eccen tangentA.y = sin(projectAngle) * radius tangentB.set(self.tangentA) tangentB.y *= -1 tangentA.rotateZ(apexAngle) tangentB.rotateZ(apexAngle) tangentA.add(self.renderOrigin) tangentB.add(self.renderOrigin) self.setSurfaceRenderPoint(0, tangentA) self.setSurfaceRenderPoint(1, self.apex.renderOrigin) self.setSurfaceRenderPoint(2, tangentB) # render renderer.stroke(self.stroke, self.color, self.getLineWidth()) renderer.fill(self.fill, self.color) renderer.begin() renderer.renderPath(self.surfacePathCommands) renderer.end() def setSurfaceRenderPoint(self, index, point): renderPoint = self.surfacePathCommands[index].renderPoints[0] renderPoint.set(point)
def mapVectorPoint(point): if isinstance(point, Vector): return point else: return Vector(**point)
def mapNewVector(point): return Vector(**point)
class Shape(Anchor): actionNames = [ 'move', 'line', 'bezier', 'arc', ] pathCommands = [] def __init__(self, stroke=1, fill=False, color=(0, 1, 0), closed=True, visible=True, path=[dict(x=0, y=0, z=0)], front=dict(z=1), backface=True, **kwargs): Anchor.__init__(self, **kwargs) self.stroke = stroke self.fill = fill self.color = color self.closed = closed self.visible = visible self.path = path self.backface = backface self.updatePath() self.front = Vector(**front) if type(front) is dict else front self.renderFront = Vector(self.front) self.renderNormal = Vector() def __repr__(self): return '<zDogPy Shape>' def updatePath(self): self.setPath() self.updatePathCommands() def setPath(self): pass def updatePathCommands(self): '''Parse path into PathCommands.''' if not self.path: return previousPoint = None self.pathCommands = [] for i, pathPart in enumerate(self.path): # pathPart can be just vector coordinates -> { x, y, z } # or path instruction -> { arc: [ {x0, y0, z0}, {x1, y1, z1} ] } keys = pathPart.keys() method = list(keys)[0] points = pathPart[method] # default to line if no instruction isInstruction = len(keys) == 1 and method in self.actionNames if not isInstruction: method = 'line' points = pathPart # munge single-point methods like line & move without arrays isLineOrMove = method == 'line' or method == 'move' isPointsArray = isinstance(points, list) if isLineOrMove and not isPointsArray: points = [points] # first action is always move if i == 0: method = 'move' # arcs require previous last point command = PathCommand(method, points, previousPoint) # update previousLastPoint previousPoint = command.endRenderPoint self.pathCommands.append(command) # ------ # update # ------ def reset(self): self.renderOrigin.set(self.origin) self.renderFront.set(self.front) # reset command render points for command in self.pathCommands: command.reset() def transform(self, translation, rotation, scale): # calculate render points backface visibility & cone/hemisphere shapes self.renderOrigin.transform(translation, rotation, scale) self.renderFront.transform(translation, rotation, scale) self.renderNormal.set(self.renderOrigin).subtract(self.renderFront) # transform points for command in self.pathCommands: command.transform(translation, rotation, scale) # transform children for child in self.children: child.transform(translation, rotation, scale) def updateSortValue(self): if not len(self.pathCommands): return sortValueTotal = 0 for command in self.pathCommands: sortValueTotal += command.endRenderPoint.z # average sort value of all points # def not geometrically correct, but works for me self.sortValue = sortValueTotal / len(self.pathCommands) # ------ # render # ------ def getLineWidth(self): if not self.stroke: return 0 if self.stroke == True: return 1 return self.stroke def getRenderColor(self): # use backface color if applicable isBackfaceColor = isinstance(self.backface, str) and self.isFacingBack color = self.backface if isBackfaceColor else self.color if type(color) is str: color = hexToRGB(color) return color def render(self, ctx, renderer): length = len(self.pathCommands) if not self.visible or not length: return # do not render if hiding backface self.isFacingBack = self.renderNormal.z > 0 if not self.backface and not self.isFacingBack: return if not renderer: print(f'Zdog renderer required. Set to {renderer}') # render dot or path isDot = length == 1 if isDot: self.renderDot(ctx, renderer) else: self.renderPath(ctx, renderer) def renderDot(self, ctx, renderer): # render lines with no size as circle lineWidth = self.getLineWidth() if not lineWidth: return color = self.getRenderColor() point = self.pathCommands[0].endRenderPoint radius = lineWidth / 2 renderer.stroke(False, color, self.getLineWidth()) renderer.fill(True, color) ctx.oval(point.x - radius, point.y - radius, radius * 2, radius * 2) def renderPath(self, ctx, renderer): isTwoPoints = len( self.pathCommands) == 2 and self.pathCommands[1].method == 'line' isClosed = not isTwoPoints and self.closed color = self.getRenderColor() renderer.stroke(self.stroke, color, self.getLineWidth()) renderer.fill(self.fill, color) renderer.renderPath(self.pathCommands, isClosed) renderer.end() # ---- # copy # ---- copyAttrs = [ 'stroke', 'fill', 'color', 'closed', 'visible', 'path', 'front', 'backface', 'renderFront', 'renderNormal', ] + Anchor.copyAttrs
from math import sqrt, acos from zDogPy.illustration import Illustration from zDogPy.anchor import Anchor from zDogPy.cylinder import Cylinder from zDogPy.hemisphere import Hemisphere from zDogPy.boilerplate import TAU from zDogPy.vector import Vector from zDogPy.cone import Cone from zDogPy.polygon import Polygon sceneSize = 24 ROOT3 = sqrt(3) ROOT5 = sqrt(5) PHI = (1 + ROOT5) / 2 viewRotation = Vector() eggplant = '#636' garnet = '#C25' orange = '#E62' gold = '#EA0' yellow = '#ED0' I = Illustration() I.setSize(sceneSize, sceneSize) solids = [] hourglass = Anchor( addTo=I, translate={
def __init__(self, rotate=None, translate=None, scale=None, addTo=None, **kwargs): self.addTo = addTo self.flatGraph = [] self.sortValue = 0 # transform if translate is None: self.translate = Vector() elif isinstance(translate, Vector): self.translate = translate elif isinstance(translate, dict): self.translate = Vector(**translate) elif isinstance(translate, float) or isinstance(translate, int): self.translate = Vector(translate) if rotate is None: self.rotate = Vector() elif isinstance(translate, Vector): self.rotate = rotate elif isinstance(rotate, dict): self.rotate = Vector(**rotate) elif isinstance(rotate, float) or isinstance(rotate, int): self.rotate = Vector(rotate) if scale is None: self.scale = Vector() elif isinstance(translate, Vector): self.scale = scale elif isinstance(scale, dict): self.scale = Vector(**scale) elif isinstance(scale, float) or isinstance(scale, int): self.scale = Vector(scale) # origin self.origin = Vector() self.renderOrigin = Vector() # children self.children = [] if self.addTo: self.addTo.addChild(self)
class Anchor(Vector): # TODO: make setter/getter for transformation vectors # int or float / Vector / dict # - _translate # - _rotate # - _scale def __init__(self, rotate=None, translate=None, scale=None, addTo=None, **kwargs): self.addTo = addTo self.flatGraph = [] self.sortValue = 0 # transform if translate is None: self.translate = Vector() elif isinstance(translate, Vector): self.translate = translate elif isinstance(translate, dict): self.translate = Vector(**translate) elif isinstance(translate, float) or isinstance(translate, int): self.translate = Vector(translate) if rotate is None: self.rotate = Vector() elif isinstance(translate, Vector): self.rotate = rotate elif isinstance(rotate, dict): self.rotate = Vector(**rotate) elif isinstance(rotate, float) or isinstance(rotate, int): self.rotate = Vector(rotate) if scale is None: self.scale = Vector() elif isinstance(translate, Vector): self.scale = scale elif isinstance(scale, dict): self.scale = Vector(**scale) elif isinstance(scale, float) or isinstance(scale, int): self.scale = Vector(scale) # origin self.origin = Vector() self.renderOrigin = Vector() # children self.children = [] if self.addTo: self.addTo.addChild(self) def __repr__(self): return f'<zDogPy Anchor {self.x} {self.y} {self.z}>' def setOptions(self, options): if not options: return for key, value in options.items(): if hasattr(self, key): setattr(self, key, value) def addChild(self, shape): if shape in self.children: # remove previous parent shape.remove() # keep parent reference shape.addTo = self self.children.append(shape) def removeChild(self, shape): index = self.children.index(shape) del self.children[index] def remove(self): if self.addTo: self.addTo.removeChild(self) # ------ # update # ------ def update(self): # update self self.reset() # update children for child in self.children: child.update() self.transform(self.translate, self.rotate, self.scale) def reset(self): self.renderOrigin.set(self.origin) def transform(self, translation, rotation, scale): self.renderOrigin.transform(translation, rotation, scale) # transform children for i, child in enumerate(self.children): child.transform(translation, rotation, scale) def updateGraph(self): self.update() self.checkFlatGraph() for item in self.flatGraph: item.updateSortValue() # z-sort self.flatGraph.sort(key=cmp_to_key(shapeSorter)) def checkFlatGraph(self): if not self.flatGraph: self.updateFlatGraph() def updateFlatGraph(self): self.flatGraph = self.getFlatGraph() def getFlatGraph(self): # return list of self & all child graph items flatGraph = [self] for child in self.children: childFlatGraph = child.getFlatGraph() flatGraph += childFlatGraph return flatGraph def updateSortValue(self): self.sortValue = self.renderOrigin.z # ------ # render # ------ def render(self, ctx, renderer): pass def renderGraphDrawBot(self): self.checkFlatGraph() for item in self.flatGraph: item.render(ctx, DrawBotRenderer()) # ---- # misc # ---- copyAttrs = [ 'addTo', 'flatGraph', 'sortValue', 'origin', 'renderOrigin', 'children', # 'translate', 'rotate', 'scale' ] def copy(self, **kwargs): attrsDict = {attr: getattr(self, attr) for attr in self.copyAttrs} for attr, value in kwargs.items(): attrsDict[attr] = value copyClass = self.__class__ copyObject = copyClass(**attrsDict) return copyObject def copyGraph(self, **kwargs): clone = self.copy(**kwargs) for child in self.children: print(child.translate, child.rotate, child.scale) child.copyGraph(addTo=clone) clone return clone def normalizeRotate(self): self.rotate.x = self.rotate.x % TAU self.rotate.y = self.rotate.y % TAU self.rotate.z = self.rotate.z % TAU