import math from operator import itemgetter class Shader(object): MODE_EVEN_ODD = 0 MODE_NONZERO = 1 def __init__(self, unshadedThreshold=1., lightestSpacing=3., darkestSpacing=0.5, angle=45, crossHatch=False): self.unshadedThreshold = unshadedThreshold self.lightestSpacing = lightestSpacing self.darkestSpacing = darkestSpacing self.angle = angle self.secondaryAngle = angle + 90 self.crossHatch = False def isActive(self): return self.unshadedThreshold > 0.000001 def setDrawingDirectionAngle(self, drawingDirectionAngle): self.drawingDirectionAngle = drawingDirectionAngle if drawingDirectionAngle is None: return if 90 < (self.angle - drawingDirectionAngle) % 360 < 270: self.angle = (self.angle + 180) % 360 if 90 < (self.secondaryAngle - drawingDirectionAngle) % 360 < 270: self.secondaryAngle = (self.secondaryAngle + 180) % 360 def shade(self, polygon, grayscale, avoidOutline=True, mode=None): if mode is None: mode = Shader.MODE_EVEN_ODD if grayscale >= self.unshadedThreshold: return [] intensity = (self.unshadedThreshold-grayscale) / float(self.unshadedThreshold) spacing = self.lightestSpacing * (1-intensity) + self.darkestSpacing * intensity lines = Shader.shadePolygon(polygon, self.angle, spacing, avoidOutline=avoidOutline, mode=mode, alternate=(self.drawingDirectionAngle is None)) if self.crossHatch: lines += Shader.shadePolygon(polygon, self.angle+90, spacing, avoidOutline=avoidOutline, mode=mode, alternate=(self.drawingDirectionAngle is None)) return lines @staticmethod def shadePolygon(polygon, angleDegrees, spacing, avoidOutline=True, mode=None, alternate=True): if mode is None: mode = Shader.MODE_EVEN_ODD rotate = complex(math.cos(angleDegrees * math.pi / 180.), math.sin(angleDegrees * math.pi / 180.)) polygon = [(line[0] / rotate,line[1] / rotate) for line in polygon] spacing = float(spacing) toAvoid = list(set(line[0].imag for line in polygon)|set(line[1].imag for line in polygon)) if len(toAvoid) <= 1: deltaY = (toAvoid[0]-spacing/2.) % spacing else: # find largest interval toAvoid.sort() largestIndex = 0 largestLen = 0 for i in range(len(toAvoid)): l = ( toAvoid[i] - toAvoid[i-1] ) % spacing if l > largestLen: largestIndex = i largestLen = l deltaY = (toAvoid[largestIndex-1] + largestLen / 2.) % spacing minY = min(min(line[0].imag,line[1].imag) for line in polygon) maxY = max(max(line[0].imag,line[1].imag) for line in polygon) y = minY + ( - minY ) % spacing + deltaY if y > minY + spacing: y -= spacing y += 0.01 odd = False all = [] while y < maxY: intersections = [] for line in polygon: z = line[0] z1 = line[1] if z1.imag == y or z.imag == y: # roundoff generated corner case -- ignore -- TODO break if z1.imag < y < z.imag or z.imag < y < z1.imag: if z1.real == z.real: intersections.append(( complex(z.real, y), z.imag<y, line)) else: m = (z1.imag-z.imag)/(z1.real-z.real) # m * (x - z.real) = y - z.imag # so: x = (y - z.imag) / m + z.real intersections.append( (complex((y-z.imag)/m + z.real, y), z.imag<y, line) ) intersections.sort(key=lambda datum: datum[0].real) thisLine = [] if mode == Shader.MODE_EVEN_ODD: for i in range(0,len(intersections)-1,2): thisLine.append((intersections[i], intersections[i+1])) elif mode == Shader.MODE_NONZERO: count = 0 for i in range(0,len(intersections)-1): if intersections[i][1]: count += 1 else: count -= 1 if count != 0: thisLine.append((intersections[i], intersections[i+1])) else: raise ValueError() if odd and alternate: thisLine = list(reversed([(l[1],l[0]) for l in thisLine])) if not avoidOutline and len(thisLine) and len(all) and all[-1][1][2] == thisLine[0][0][2]: # follow along outline to avoid an extra pen bob all.append( (all[-1][1], thisLine[0][0]) ) all += thisLine odd = not odd y += spacing return [(line[0][0]*rotate, line[1][0]*rotate) for line in all] if __name__ == '__main__': polygon=(0+0j, 10+10j, 10+0j, 0+0j) print(shadePolygon(polygon,90,1))