class OutlinePen(BasePen): pointClass = MathPoint magicCurve = 0.5522847498 def __init__(self, glyphSet, offset=10, contrast=0, contrastAngle=0, connection="square", cap="round", miterLimit=None, closeOpenPaths=True, optimizeCurve=False, preserveComponents=False, filterDoubles=True): BasePen.__init__(self, glyphSet) self.offset = abs(offset) self.contrast = abs(contrast) self.contrastAngle = contrastAngle self._inputmiterLimit = miterLimit if miterLimit is None: miterLimit = self.offset * 2 self.miterLimit = abs(miterLimit) self.closeOpenPaths = closeOpenPaths self.optimizeCurve = optimizeCurve self.connectionCallback = getattr( self, "connection%s" % (connection.title())) self.capCallback = getattr(self, "cap%s" % (cap.title())) self.originalGlyph = Glyph() self.originalPen = self.originalGlyph.getPen() self.outerGlyph = Glyph() self.outerPen = self.outerGlyph.getPen() self.outerCurrentPoint = None self.outerFirstPoint = None self.outerPrevPoint = None self.innerGlyph = Glyph() self.innerPen = self.innerGlyph.getPen() self.innerCurrentPoint = None self.innerFirstPoint = None self.innerPrevPoint = None self.prevPoint = None self.firstPoint = None self.firstAngle = None self.prevAngle = None self.shouldHandleMove = True self.preserveComponents = preserveComponents self.components = [] self.filterDoubles = filterDoubles self.drawSettings() def _moveTo(self, pt): x, y = pt if self.offset == 0: self.outerPen.moveTo((x, y)) self.innerPen.moveTo((x, y)) return self.originalPen.moveTo((x, y)) p = self.pointClass(x, y) self.prevPoint = p self.firstPoint = p self.shouldHandleMove = True def _lineTo(self, pt): x, y = pt if self.offset == 0: self.outerPen.lineTo((x, y)) self.innerPen.lineTo((x, y)) return self.originalPen.lineTo((x, y)) currentPoint = self.pointClass(x, y) if currentPoint == self.prevPoint: return self.currentAngle = self.prevPoint.angle(currentPoint) thickness = self.getThickness(self.currentAngle) self.innerCurrentPoint = self.prevPoint - self.pointClass( cos(self.currentAngle), sin(self.currentAngle)) * thickness self.outerCurrentPoint = self.prevPoint + self.pointClass( cos(self.currentAngle), sin(self.currentAngle)) * thickness if self.shouldHandleMove: self.shouldHandleMove = False self.innerPen.moveTo(self.innerCurrentPoint) self.innerFirstPoint = self.innerCurrentPoint self.outerPen.moveTo(self.outerCurrentPoint) self.outerFirstPoint = self.outerCurrentPoint self.firstAngle = self.currentAngle else: self.buildConnection() self.innerCurrentPoint = currentPoint - self.pointClass( cos(self.currentAngle), sin(self.currentAngle)) * thickness self.innerPen.lineTo(self.innerCurrentPoint) self.innerPrevPoint = self.innerCurrentPoint self.outerCurrentPoint = currentPoint + self.pointClass( cos(self.currentAngle), sin(self.currentAngle)) * thickness self.outerPen.lineTo(self.outerCurrentPoint) self.outerPrevPoint = self.outerCurrentPoint self.prevPoint = currentPoint self.prevAngle = self.currentAngle def _curveToOne(self, pt1, pt2, pt3): if self.optimizeCurve: curves = splitCubicAtT(self.prevPoint, pt1, pt2, pt3, .5) else: curves = [(self.prevPoint, pt1, pt2, pt3)] for curve in curves: p1, h1, h2, p2 = curve self._processCurveToOne(h1, h2, p2) def _processCurveToOne(self, pt1, pt2, pt3): if self.offset == 0: self.outerPen.curveTo(pt1, pt2, pt3) self.innerPen.curveTo(pt1, pt2, pt3) return self.originalPen.curveTo(pt1, pt2, pt3) p1 = self.pointClass(*pt1) p2 = self.pointClass(*pt2) p3 = self.pointClass(*pt3) if p1 == self.prevPoint: p1 = pointOnACurve(self.prevPoint, p1, p2, p3, 0.01) if p2 == p3: p2 = pointOnACurve(self.prevPoint, p1, p2, p3, 0.99) a1 = self.prevPoint.angle(p1) a2 = p2.angle(p3) self.currentAngle = a1 tickness1 = self.getThickness(a1) tickness2 = self.getThickness(a2) a1bis = self.prevPoint.angle(p1, 0) a2bis = p3.angle(p2, 0) intersectPoint = interSect( (self.prevPoint, self.prevPoint + self.pointClass(cos(a1), sin(a1)) * 100), (p3, p3 + self.pointClass(cos(a2), sin(a2)) * 100)) self.innerCurrentPoint = self.prevPoint - self.pointClass( cos(a1), sin(a1)) * tickness1 self.outerCurrentPoint = self.prevPoint + self.pointClass( cos(a1), sin(a1)) * tickness1 if self.shouldHandleMove: self.shouldHandleMove = False self.innerPen.moveTo(self.innerCurrentPoint) self.innerFirstPoint = self.innerPrevPoint = self.innerCurrentPoint self.outerPen.moveTo(self.outerCurrentPoint) self.outerFirstPoint = self.outerPrevPoint = self.outerCurrentPoint self.firstAngle = a1 else: self.buildConnection() h1 = None if intersectPoint is not None: h1 = interSect( (self.innerCurrentPoint, self.innerCurrentPoint + self.pointClass(cos(a1bis), sin(a1bis)) * tickness1), (intersectPoint, p1)) if h1 is None: h1 = p1 - self.pointClass(cos(a1), sin(a1)) * tickness1 self.innerCurrentPoint = p3 - self.pointClass(cos(a2), sin(a2)) * tickness2 h2 = None if intersectPoint is not None: h2 = interSect( (self.innerCurrentPoint, self.innerCurrentPoint + self.pointClass(cos(a2bis), sin(a2bis)) * tickness2), (intersectPoint, p2)) if h2 is None: h2 = p2 - self.pointClass(cos(a1), sin(a1)) * tickness1 self.innerPen.curveTo(h1, h2, self.innerCurrentPoint) self.innerPrevPoint = self.innerCurrentPoint ######## h1 = None if intersectPoint is not None: h1 = interSect( (self.outerCurrentPoint, self.outerCurrentPoint + self.pointClass(cos(a1bis), sin(a1bis)) * tickness1), (intersectPoint, p1)) if h1 is None: h1 = p1 + self.pointClass(cos(a1), sin(a1)) * tickness1 self.outerCurrentPoint = p3 + self.pointClass(cos(a2), sin(a2)) * tickness2 h2 = None if intersectPoint is not None: h2 = interSect( (self.outerCurrentPoint, self.outerCurrentPoint + self.pointClass(cos(a2bis), sin(a2bis)) * tickness2), (intersectPoint, p2)) if h2 is None: h2 = p2 + self.pointClass(cos(a1), sin(a1)) * tickness1 self.outerPen.curveTo(h1, h2, self.outerCurrentPoint) self.outerPrevPoint = self.outerCurrentPoint self.prevPoint = p3 self.currentAngle = a2 self.prevAngle = a2 def _closePath(self): if self.shouldHandleMove: return if self.offset == 0: self.outerPen.closePath() self.innerPen.closePath() return if not self.prevPoint == self.firstPoint: self._lineTo(self.firstPoint) self.originalPen.closePath() self.innerPrevPoint = self.innerCurrentPoint self.innerCurrentPoint = self.innerFirstPoint self.outerPrevPoint = self.outerCurrentPoint self.outerCurrentPoint = self.outerFirstPoint self.prevAngle = self.currentAngle self.currentAngle = self.firstAngle self.buildConnection(close=True) self.innerPen.closePath() self.outerPen.closePath() def _endPath(self): if self.shouldHandleMove: return self.originalPen.endPath() self.innerPen.endPath() self.outerPen.endPath() if self.closeOpenPaths: innerContour = self.innerGlyph[-1] outerContour = self.outerGlyph[-1] innerContour.reverse() innerContour[0].segmentType = "line" outerContour[0].segmentType = "line" self.buildCap(outerContour, innerContour) for point in innerContour: outerContour.addPoint((point.x, point.y), segmentType=point.segmentType, smooth=point.smooth) self.innerGlyph.removeContour(innerContour) def addComponent(self, glyphName, transform): if self.preserveComponents: self.components.append((glyphName, transform)) else: BasePen.addComponent(self, glyphName, transform) # thickness def getThickness(self, angle): a2 = angle + pi * .5 f = abs(sin(a2 + radians(self.contrastAngle))) f = f**5 return self.offset + self.contrast * f # connections def buildConnection(self, close=False): if not checkSmooth(self.prevAngle, self.currentAngle): if checkInnerOuter(self.prevAngle, self.currentAngle): self.connectionCallback(self.outerPrevPoint, self.outerCurrentPoint, self.outerPen, close) self.connectionInnerCorner(self.innerPrevPoint, self.innerCurrentPoint, self.innerPen, close) else: self.connectionCallback(self.innerPrevPoint, self.innerCurrentPoint, self.innerPen, close) self.connectionInnerCorner(self.outerPrevPoint, self.outerCurrentPoint, self.outerPen, close) elif not self.filterDoubles: self.innerPen.lineTo(self.innerCurrentPoint) self.outerPen.lineTo(self.outerCurrentPoint) def connectionSquare(self, first, last, pen, close): angle_1 = radians(degrees(self.prevAngle) + 90) angle_2 = radians(degrees(self.currentAngle) + 90) tempFirst = first - self.pointClass(cos(angle_1), sin(angle_1)) * self.miterLimit tempLast = last + self.pointClass(cos(angle_2), sin(angle_2)) * self.miterLimit newPoint = interSect((first, tempFirst), (last, tempLast)) if newPoint is not None: if self._inputmiterLimit is not None and roundFloat( newPoint.distance(first)) > self._inputmiterLimit: pen.lineTo(tempFirst) pen.lineTo(tempLast) else: pen.lineTo(newPoint) if not close: pen.lineTo(last) def connectionRound(self, first, last, pen, close): angle_1 = radians(degrees(self.prevAngle) + 90) angle_2 = radians(degrees(self.currentAngle) + 90) tempFirst = first - self.pointClass(sin(angle_1), -cos(angle_1)) tempLast = last + self.pointClass(sin(angle_2), -cos(angle_2)) centerPoint = interSect((first, tempFirst), (last, tempLast)) if centerPoint is None: # the lines are parallel, let's just take the middle centerPoint = (first + last) / 2 angle_diff = (angle_1 - angle_2) % (2 * pi) if angle_diff > pi: angle_diff = 2 * pi - angle_diff angle_half = angle_diff / 2 radius = centerPoint.distance(first) D = radius * (1 - cos(angle_half)) handleLength = (4 * D / 3) / sin(angle_half) # length of the bcp line bcp1 = first - self.pointClass(cos(angle_1), sin(angle_1)) * handleLength bcp2 = last + self.pointClass(cos(angle_2), sin(angle_2)) * handleLength pen.curveTo(bcp1, bcp2, last) def connectionButt(self, first, last, pen, close): if not close: pen.lineTo(last) def connectionInnerCorner(self, first, last, pen, close): if not close: pen.lineTo(last) # caps def buildCap(self, firstContour, lastContour): first = firstContour[-1] last = lastContour[0] first = self.pointClass(first.x, first.y) last = self.pointClass(last.x, last.y) self.capCallback(firstContour, lastContour, first, last, self.prevAngle) first = lastContour[-1] last = firstContour[0] first = self.pointClass(first.x, first.y) last = self.pointClass(last.x, last.y) angle = radians(degrees(self.firstAngle) + 180) self.capCallback(lastContour, firstContour, first, last, angle) def capButt(self, firstContour, lastContour, first, last, angle): # not nothing pass def capRound(self, firstContour, lastContour, first, last, angle): hookedAngle = radians(degrees(angle) + 90) p1 = first - self.pointClass(cos(hookedAngle), sin(hookedAngle)) * self.offset p2 = last - self.pointClass(cos(hookedAngle), sin(hookedAngle)) * self.offset oncurve = p1 + (p2 - p1) * .5 roundness = .54 # should be self.magicCurve h1 = first - self.pointClass( cos(hookedAngle), sin(hookedAngle)) * self.offset * roundness h2 = oncurve + self.pointClass(cos(angle), sin(angle)) * self.offset * roundness firstContour[-1].smooth = True firstContour.addPoint((h1.x, h1.y)) firstContour.addPoint((h2.x, h2.y)) firstContour.addPoint((oncurve.x, oncurve.y), smooth=True, segmentType="curve") h1 = oncurve - self.pointClass(cos(angle), sin(angle)) * self.offset * roundness h2 = last - self.pointClass(cos(hookedAngle), sin(hookedAngle)) * self.offset * roundness firstContour.addPoint((h1.x, h1.y)) firstContour.addPoint((h2.x, h2.y)) lastContour[0].segmentType = "curve" lastContour[0].smooth = True def capSquare(self, firstContour, lastContour, first, last, angle): angle = radians(degrees(angle) + 90) firstContour[-1].smooth = True lastContour[0].smooth = True p1 = first - self.pointClass(cos(angle), sin(angle)) * self.offset firstContour.addPoint((p1.x, p1.y), smooth=False, segmentType="line") p2 = last - self.pointClass(cos(angle), sin(angle)) * self.offset firstContour.addPoint((p2.x, p2.y), smooth=False, segmentType="line") def drawSettings(self, drawOriginal=False, drawInner=False, drawOuter=True): self.drawOriginal = drawOriginal self.drawInner = drawInner self.drawOuter = drawOuter def drawPoints(self, pointPen): if self.drawInner: reversePen = ReverseContourPointPen(pointPen) self.innerGlyph.drawPoints(CleanPointPen(reversePen)) if self.drawOuter: self.outerGlyph.drawPoints(CleanPointPen(pointPen)) if self.drawOriginal: if self.drawOuter: pointPen = ReverseContourPointPen(pointPen) self.originalGlyph.drawPoints(CleanPointPen(pointPen)) for glyphName, transform in self.components: pointPen.addComponent(glyphName, transform) def draw(self, pen): pointPen = PointToSegmentPen(pen) self.drawPoints(pointPen) def getGlyph(self): glyph = Glyph() pointPen = glyph.getPointPen() self.drawPoints(pointPen) return glyph
def test_identifiers(self): glyph = Glyph() pointPen = glyph.getPointPen() pointPen.beginPath(identifier="contour 1") pointPen.addPoint((0, 0), identifier="point 1") pointPen.addPoint((0, 0), identifier="point 2") pointPen.endPath() pointPen.beginPath(identifier="contour 2") pointPen.endPath() pointPen.addComponent("A", (1, 1, 1, 1, 1, 1), identifier="component 1") pointPen.addComponent("A", (1, 1, 1, 1, 1, 1), identifier="component 2") guideline = Guideline() guideline.identifier = "guideline 1" glyph.appendGuideline(guideline) guideline = Guideline() guideline.identifier = "guideline 2" glyph.appendGuideline(guideline) self.assertEqual([contour.identifier for contour in glyph], ["contour 1", "contour 2"]) self.assertEqual([point.identifier for point in glyph[0]], ["point 1", "point 2"]) self.assertEqual( [component.identifier for component in glyph.components], ["component 1", "component 2"]) with self.assertRaises(AssertionError): pointPen.beginPath(identifier="contour 1") pointPen.endPath() pointPen.beginPath() pointPen.addPoint((0, 0)) with self.assertRaises(AssertionError): pointPen.addPoint((0, 0), identifier="point 1") pointPen.endPath() with self.assertRaises(AssertionError): pointPen.addComponent("A", (1, 1, 1, 1, 1, 1), identifier="component 1") g = Guideline() g.identifier = "guideline 1" with self.assertRaises(AssertionError): glyph.appendGuideline(g) self.assertEqual(sorted(glyph.identifiers), [ "component 1", "component 2", "contour 1", "contour 2", "guideline 1", "guideline 2", "point 1", "point 2" ]) glyph.removeContour(glyph[0]) self.assertEqual(sorted(glyph.identifiers), [ "component 1", "component 2", "contour 2", "guideline 1", "guideline 2" ]) glyph.removeComponent(glyph.components[0]) self.assertEqual( sorted(glyph.identifiers), ["component 2", "contour 2", "guideline 1", "guideline 2"]) glyph.removeGuideline(glyph.guidelines[0]) self.assertEqual(sorted(glyph.identifiers), ["component 2", "contour 2", "guideline 2"])
def test_identifiers(self): glyph = Glyph() pointPen = glyph.getPointPen() pointPen.beginPath(identifier="contour 1") pointPen.addPoint((0, 0), identifier="point 1") pointPen.addPoint((0, 0), identifier="point 2") pointPen.endPath() pointPen.beginPath(identifier="contour 2") pointPen.endPath() pointPen.addComponent("A", (1, 1, 1, 1, 1, 1), identifier="component 1") pointPen.addComponent("A", (1, 1, 1, 1, 1, 1), identifier="component 2") guideline = Guideline() guideline.identifier = "guideline 1" glyph.appendGuideline(guideline) guideline = Guideline() guideline.identifier = "guideline 2" glyph.appendGuideline(guideline) self.assertEqual([contour.identifier for contour in glyph], ["contour 1", "contour 2"]) self.assertEqual([point.identifier for point in glyph[0]], ["point 1", "point 2"]) self.assertEqual( [component.identifier for component in glyph.components], ["component 1", "component 2"]) with self.assertRaises(AssertionError): pointPen.beginPath(identifier="contour 1") pointPen.endPath() pointPen.beginPath() pointPen.addPoint((0, 0)) with self.assertRaises(AssertionError): pointPen.addPoint((0, 0), identifier="point 1") pointPen.endPath() with self.assertRaises(AssertionError): pointPen.addComponent("A", (1, 1, 1, 1, 1, 1), identifier="component 1") g = Guideline() g.identifier = "guideline 1" with self.assertRaises(AssertionError): glyph.appendGuideline(g) self.assertEqual( sorted(glyph.identifiers), ["component 1", "component 2", "contour 1", "contour 2", "guideline 1", "guideline 2", "point 1", "point 2"]) glyph.removeContour(glyph[0]) self.assertEqual( sorted(glyph.identifiers), ["component 1", "component 2", "contour 2", "guideline 1", "guideline 2"]) glyph.removeComponent(glyph.components[0]) self.assertEqual( sorted(glyph.identifiers), ["component 2", "contour 2", "guideline 1", "guideline 2"]) glyph.removeGuideline(glyph.guidelines[0]) self.assertEqual( sorted(glyph.identifiers), ["component 2", "contour 2", "guideline 2"])
class OutlinePen(BasePen): pointClass = MathPoint magicCurve = 0.5522847498 def __init__(self, glyphSet, offset=10, contrast=0, contrastAngle=0, connection="square", cap="round", miterLimit=None, closeOpenPaths=True, preserveComponents=False): BasePen.__init__(self, glyphSet) self.offset = abs(offset) self.contrast = abs(contrast) self.contrastAngle = contrastAngle self._inputmiterLimit = miterLimit if miterLimit is None: miterLimit = self.offset * 2 self.miterLimit = abs(miterLimit) self.closeOpenPaths = closeOpenPaths self.connectionCallback = getattr( self, "connection%s" % (connection.title())) self.capCallback = getattr(self, "cap%s" % (cap.title())) self.originalGlyph = Glyph() self.originalPen = self.originalGlyph.getPen() self.outerGlyph = Glyph() self.outerPen = self.outerGlyph.getPen() self.outerCurrentPoint = None self.outerFirstPoint = None self.outerPrevPoint = None self.innerGlyph = Glyph() self.innerPen = self.innerGlyph.getPen() self.innerCurrentPoint = None self.innerFirstPoint = None self.innerPrevPoint = None self.prevPoint = None self.firstPoint = None self.firstAngle = None self.prevAngle = None self.shouldHandleMove = True self.preserveComponents = preserveComponents self.components = [] self.drawSettings() def _moveTo(self, xyFix): (x, y) = xyFix if self.offset == 0: self.outerPen.moveTo((x, y)) self.innerPen.moveTo((x, y)) return self.originalPen.moveTo((x, y)) p = self.pointClass(x, y) self.prevPoint = p self.firstPoint = p self.shouldHandleMove = True def _lineTo(self, xyFix): (x, y) = xyFix if self.offset == 0: self.outerPen.lineTo((x, y)) self.innerPen.lineTo((x, y)) return self.originalPen.lineTo((x, y)) currentPoint = self.pointClass(x, y) if currentPoint == self.prevPoint: return self.currentAngle = self.prevPoint.angle(currentPoint) thickness = self.getThickness(self.currentAngle) self.innerCurrentPoint = self.prevPoint - self.pointClass( cos(self.currentAngle), sin(self.currentAngle)) * thickness self.outerCurrentPoint = self.prevPoint + self.pointClass( cos(self.currentAngle), sin(self.currentAngle)) * thickness if self.shouldHandleMove: self.shouldHandleMove = False self.innerPen.moveTo(self.innerCurrentPoint) self.innerFirstPoint = self.innerCurrentPoint self.outerPen.moveTo(self.outerCurrentPoint) self.outerFirstPoint = self.outerCurrentPoint self.firstAngle = self.currentAngle else: self.buildConnection() self.innerCurrentPoint = currentPoint - self.pointClass( cos(self.currentAngle), sin(self.currentAngle)) * thickness self.innerPen.lineTo(self.innerCurrentPoint) self.innerPrevPoint = self.innerCurrentPoint self.outerCurrentPoint = currentPoint + self.pointClass( cos(self.currentAngle), sin(self.currentAngle)) * thickness self.outerPen.lineTo(self.outerCurrentPoint) self.outerPrevPoint = self.outerCurrentPoint self.prevPoint = currentPoint self.prevAngle = self.currentAngle def _curveToOne(self, xyFix1, xyFix2, xyFix3): (x1, y1) = xyFix1 (x2, y2) = xyFix2 (x3, y3) = xyFix3 if self.offset == 0: self.outerPen.curveTo((x1, y1), (x2, y2), (x3, y3)) self.innerPen.curveTo((x1, y1), (x2, y2), (x3, y3)) return self.originalPen.curveTo((x1, y1), (x2, y2), (x3, y3)) p1 = self.pointClass(x1, y1) p2 = self.pointClass(x2, y2) p3 = self.pointClass(x3, y3) if p1 == self.prevPoint: p1 = pointOnACurve(self.prevPoint, p1, p2, p3, 0.01) if p2 == p3: p2 = pointOnACurve(self.prevPoint, p1, p2, p3, 0.99) a1 = self.prevPoint.angle(p1) a2 = p2.angle(p3) self.currentAngle = a1 tickness1 = self.getThickness(a1) tickness2 = self.getThickness(a2) a1bis = self.prevPoint.angle(p1, 0) a2bis = p3.angle(p2, 0) intersectPoint = interSect( (self.prevPoint, self.prevPoint + self.pointClass(cos(a1), sin(a1)) * 100), (p3, p3 + self.pointClass(cos(a2), sin(a2)) * 100)) self.innerCurrentPoint = self.prevPoint - self.pointClass( cos(a1), sin(a1)) * tickness1 self.outerCurrentPoint = self.prevPoint + self.pointClass( cos(a1), sin(a1)) * tickness1 if self.shouldHandleMove: self.shouldHandleMove = False self.innerPen.moveTo(self.innerCurrentPoint) self.innerFirstPoint = self.innerPrevPoint = self.innerCurrentPoint self.outerPen.moveTo(self.outerCurrentPoint) self.outerFirstPoint = self.outerPrevPoint = self.outerCurrentPoint self.firstAngle = a1 else: self.buildConnection() h1 = None if intersectPoint is not None: h1 = interSect( (self.innerCurrentPoint, self.innerCurrentPoint + self.pointClass(cos(a1bis), sin(a1bis)) * tickness1), (intersectPoint, p1)) if h1 is None: h1 = p1 - self.pointClass(cos(a1), sin(a1)) * tickness1 self.innerCurrentPoint = p3 - self.pointClass(cos(a2), sin(a2)) * tickness2 h2 = None if intersectPoint is not None: h2 = interSect( (self.innerCurrentPoint, self.innerCurrentPoint + self.pointClass(cos(a2bis), sin(a2bis)) * tickness2), (intersectPoint, p2)) if h2 is None: h2 = p2 - self.pointClass(cos(a1), sin(a1)) * tickness1 self.innerPen.curveTo(h1, h2, self.innerCurrentPoint) self.innerPrevPoint = self.innerCurrentPoint ######## h1 = None if intersectPoint is not None: h1 = interSect( (self.outerCurrentPoint, self.outerCurrentPoint + self.pointClass(cos(a1bis), sin(a1bis)) * tickness1), (intersectPoint, p1)) if h1 is None: h1 = p1 + self.pointClass(cos(a1), sin(a1)) * tickness1 self.outerCurrentPoint = p3 + self.pointClass(cos(a2), sin(a2)) * tickness2 h2 = None if intersectPoint is not None: h2 = interSect( (self.outerCurrentPoint, self.outerCurrentPoint + self.pointClass(cos(a2bis), sin(a2bis)) * tickness2), (intersectPoint, p2)) if h2 is None: h2 = p2 + self.pointClass(cos(a1), sin(a1)) * tickness1 self.outerPen.curveTo(h1, h2, self.outerCurrentPoint) self.outerPrevPoint = self.outerCurrentPoint self.prevPoint = p3 self.currentAngle = a2 self.prevAngle = a2 def _closePath(self): if self.shouldHandleMove: return if self.offset == 0: self.outerPen.closePath() self.innerPen.closePath() return if not self.prevPoint == self.firstPoint: self._lineTo(self.firstPoint) self.originalPen.closePath() self.innerPrevPoint = self.innerCurrentPoint self.innerCurrentPoint = self.innerFirstPoint self.outerPrevPoint = self.outerCurrentPoint self.outerCurrentPoint = self.outerFirstPoint self.prevAngle = self.currentAngle self.currentAngle = self.firstAngle self.buildConnection(close=True) self.innerPen.closePath() self.outerPen.closePath() def _endPath(self): if self.shouldHandleMove: return self.originalPen.endPath() self.innerPen.endPath() self.outerPen.endPath() if self.closeOpenPaths: innerContour = self.innerGlyph[-1] outerContour = self.outerGlyph[-1] innerContour.reverse() innerContour[0].segmentType = "line" outerContour[0].segmentType = "line" self.buildCap(outerContour, innerContour) for point in innerContour: outerContour.addPoint((point.x, point.y), segmentType=point.segmentType, smooth=point.smooth) self.innerGlyph.removeContour(innerContour) def addComponent(self, glyphName, transform): if self.preserveComponents: self.components.append((glyphName, transform)) else: BasePen.addComponent(self, glyphName, transform) ## thickness def getThickness(self, angle): a2 = angle + pi * .5 f = abs(sin(a2 + radians(self.contrastAngle))) f = f**5 return self.offset + self.contrast * f ## connections def buildConnection(self, close=False): if not checkSmooth(self.prevAngle, self.currentAngle): if checkInnerOuter(self.prevAngle, self.currentAngle): self.connectionCallback(self.outerPrevPoint, self.outerCurrentPoint, self.outerPen, close) self.connectionInnerCorner(self.innerPrevPoint, self.innerCurrentPoint, self.innerPen, close) else: self.connectionCallback(self.innerPrevPoint, self.innerCurrentPoint, self.innerPen, close) self.connectionInnerCorner(self.outerPrevPoint, self.outerCurrentPoint, self.outerPen, close) def connectionSquare(self, first, last, pen, close): angle_1 = radians(degrees(self.prevAngle) + 90) angle_2 = radians(degrees(self.currentAngle) + 90) tempFirst = first - self.pointClass(cos(angle_1), sin(angle_1)) * self.miterLimit tempLast = last + self.pointClass(cos(angle_2), sin(angle_2)) * self.miterLimit newPoint = interSect((first, tempFirst), (last, tempLast)) if newPoint is not None: if self._inputmiterLimit is not None and roundFloat( newPoint.distance(first)) > self._inputmiterLimit: pen.lineTo(tempFirst) pen.lineTo(tempLast) else: pen.lineTo(newPoint) if not close: pen.lineTo(last) def connectionRound(self, first, last, pen, close): angle_1 = radians(degrees(self.prevAngle) + 90) angle_2 = radians(degrees(self.currentAngle) + 90) tempFirst = first - self.pointClass(cos(angle_1), sin(angle_1)) * self.miterLimit tempLast = last + self.pointClass(cos(angle_2), sin(angle_2)) * self.miterLimit newPoint = interSect((first, tempFirst), (last, tempLast)) if newPoint is None: pen.lineTo(last) return #print "(%s, %s)," % (newPoint.x, newPoint.y) distance1 = newPoint.distance(first) distance2 = newPoint.distance(last) #print distance1, distance2 if roundFloat(distance1) > self.miterLimit + self.contrast: distance1 = self.miterLimit + tempFirst.distance(tempLast) * .7 if roundFloat(distance2) > self.miterLimit + self.contrast: distance2 = self.miterLimit + tempFirst.distance(tempLast) * .7 distance1 *= self.magicCurve distance2 *= self.magicCurve bcp1 = first - self.pointClass(cos(angle_1), sin(angle_1)) * distance1 bcp2 = last + self.pointClass(cos(angle_2), sin(angle_2)) * distance2 pen.curveTo(bcp1, bcp2, last) def connectionButt(self, first, last, pen, close): if not close: pen.lineTo(last) def connectionInnerCorner(self, first, last, pen, close): if not close: pen.lineTo(last) ## caps def buildCap(self, firstContour, lastContour): first = firstContour[-1] last = lastContour[0] first = self.pointClass(first.x, first.y) last = self.pointClass(last.x, last.y) self.capCallback(firstContour, lastContour, first, last, self.prevAngle) first = lastContour[-1] last = firstContour[0] first = self.pointClass(first.x, first.y) last = self.pointClass(last.x, last.y) angle = radians(degrees(self.firstAngle) + 180) self.capCallback(lastContour, firstContour, first, last, angle) def capButt(self, firstContour, lastContour, first, last, angle): ## not nothing pass def capRound(self, firstContour, lastContour, first, last, angle): hookedAngle = radians(degrees(angle) + 90) p1 = first - self.pointClass(cos(hookedAngle), sin(hookedAngle)) * self.offset p2 = last - self.pointClass(cos(hookedAngle), sin(hookedAngle)) * self.offset oncurve = p1 + (p2 - p1) * .5 roundness = .54 h1 = first - self.pointClass( cos(hookedAngle), sin(hookedAngle)) * self.offset * roundness h2 = oncurve + self.pointClass(cos(angle), sin(angle)) * self.offset * roundness firstContour[-1].smooth = True firstContour.addPoint((h1.x, h1.y)) firstContour.addPoint((h2.x, h2.y)) firstContour.addPoint((oncurve.x, oncurve.y), smooth=True, segmentType="curve") h1 = oncurve - self.pointClass(cos(angle), sin(angle)) * self.offset * roundness h2 = last - self.pointClass(cos(hookedAngle), sin(hookedAngle)) * self.offset * roundness firstContour.addPoint((h1.x, h1.y)) firstContour.addPoint((h2.x, h2.y)) lastContour[0].segmentType = "curve" lastContour[0].smooth = True def capSquare(self, firstContour, lastContour, first, last, angle): angle = radians(degrees(angle) + 90) firstContour[-1].smooth = True lastContour[0].smooth = True p1 = first - self.pointClass(cos(angle), sin(angle)) * self.offset firstContour.addPoint((p1.x, p1.y), smooth=False, segmentType="line") p2 = last - self.pointClass(cos(angle), sin(angle)) * self.offset firstContour.addPoint((p2.x, p2.y), smooth=False, segmentType="line") def drawSettings(self, drawOriginal=False, drawInner=False, drawOuter=True): self.drawOriginal = drawOriginal self.drawInner = drawInner self.drawOuter = drawOuter def drawPoints(self, pointPen): if self.drawInner: reversePen = ReverseContourPointPen(pointPen) self.innerGlyph.drawPoints(CleanPointPen(reversePen)) if self.drawOuter: self.outerGlyph.drawPoints(CleanPointPen(pointPen)) if self.drawOriginal: if self.drawOuter: pointPen = ReverseContourPointPen(pointPen) self.originalGlyph.drawPoints(CleanPointPen(pointPen)) for glyphName, transform in self.components: pointPen.addComponent(glyphName, transform) def draw(self, pen): pointPen = PointToSegmentPen(pen) self.drawPoints(pointPen) def getGlyph(self): glyph = Glyph() pointPen = glyph.getPointPen() self.drawPoints(pointPen) return glyph
class OutlineFitterPen(BasePen): pointClass = MathPoint magicCurve = 0.5522847498 def __init__(self, glyphSet, offset=10, contrast=0, contrastAngle=0, connection="square", cap="round", miterLimit=None, closeOpenPaths=True, optimizeCurve=False, preserveComponents=False, filterDoubles=True, alwaysConnect=False): BasePen.__init__(self, glyphSet) self.offset = abs(offset) self.contrast = abs(contrast) self.contrastAngle = contrastAngle self._inputmiterLimit = miterLimit if miterLimit is None: miterLimit = self.offset * 2 self.miterLimit = abs(miterLimit) self.closeOpenPaths = closeOpenPaths self.optimizeCurve = optimizeCurve self.connectionCallback = getattr( self, "connection%s" % (connection.title())) self.capCallback = getattr(self, "cap%s" % (cap.title())) self.originalGlyph = Glyph() self.originalPen = self.originalGlyph.getPen() self.outerGlyph = Glyph() self.outerPen = self.outerGlyph.getPen() self.outerCurrentPoint = None self.outerFirstPoint = None self.outerPrevPoint = None self.innerGlyph = Glyph() self.innerPen = self.innerGlyph.getPen() self.innerCurrentPoint = None self.innerFirstPoint = None self.innerPrevPoint = None self.prevPoint = None self.firstPoint = None self.firstAngle = None self.prevAngle = None self.shouldHandleMove = True self.preserveComponents = preserveComponents self.components = [] self.filterDoubles = filterDoubles self.alwaysConnect = alwaysConnect self.drawSettings() def _moveTo(self, pt): x, y = pt if self.offset == 0: self.outerPen.moveTo((x, y)) self.innerPen.moveTo((x, y)) return self.originalPen.moveTo((x, y)) p = self.pointClass(x, y) self.prevPoint = p self.firstPoint = p self.shouldHandleMove = True def _lineTo(self, pt): x, y = pt if self.offset == 0: self.outerPen.lineTo((x, y)) self.innerPen.lineTo((x, y)) return self.originalPen.lineTo((x, y)) currentPoint = self.pointClass(x, y) if currentPoint == self.prevPoint: return self.currentAngle = self.prevPoint.angle(currentPoint) thickness = self.getThickness(self.currentAngle) self.innerCurrentPoint = self.prevPoint - self.pointClass( cos(self.currentAngle), sin(self.currentAngle)) * thickness self.outerCurrentPoint = self.prevPoint + self.pointClass( cos(self.currentAngle), sin(self.currentAngle)) * thickness if self.shouldHandleMove: self.shouldHandleMove = False self.innerPen.moveTo(self.innerCurrentPoint) self.innerFirstPoint = self.innerCurrentPoint self.outerPen.moveTo(self.outerCurrentPoint) self.outerFirstPoint = self.outerCurrentPoint self.firstAngle = self.currentAngle else: self.buildConnection() self.innerCurrentPoint = currentPoint - self.pointClass( cos(self.currentAngle), sin(self.currentAngle)) * thickness self.innerPen.lineTo(self.innerCurrentPoint) self.innerPrevPoint = self.innerCurrentPoint self.outerCurrentPoint = currentPoint + self.pointClass( cos(self.currentAngle), sin(self.currentAngle)) * thickness self.outerPen.lineTo(self.outerCurrentPoint) self.outerPrevPoint = self.outerCurrentPoint self.prevPoint = currentPoint self.prevAngle = self.currentAngle def _curveToOne(self, pt1, pt2, pt3): if self.optimizeCurve: curves = splitCubicAtT(self.prevPoint, pt1, pt2, pt3, .5) else: curves = [(self.prevPoint, pt1, pt2, pt3)] for curve in curves: p1, h1, h2, p2 = curve self._processCurveToOne(h1, h2, p2) def _processCurveToOne(self, pt1, pt2, pt3): if self.offset == 0: self.outerPen.curveTo(pt1, pt2, pt3) self.innerPen.curveTo(pt1, pt2, pt3) return self.originalPen.curveTo(pt1, pt2, pt3) p1 = self.pointClass(*pt1) p2 = self.pointClass(*pt2) p3 = self.pointClass(*pt3) if p1 == self.prevPoint: p1 = pointOnACurve(self.prevPoint, p1, p2, p3, 0.01) if p2 == p3: p2 = pointOnACurve(self.prevPoint, p1, p2, p3, 0.99) a1 = self.prevPoint.angle(p1) a2 = p2.angle(p3) self.currentAngle = a1 tickness1 = self.getThickness(a1) tickness2 = self.getThickness(a2) a1bis = self.prevPoint.angle(p1, 0) a2bis = p3.angle(p2, 0) self.innerCurrentPoint = self.prevPoint - self.pointClass( cos(a1), sin(a1)) * tickness1 self.outerCurrentPoint = self.prevPoint + self.pointClass( cos(a1), sin(a1)) * tickness1 if self.shouldHandleMove: self.shouldHandleMove = False self.innerPen.moveTo(self.innerCurrentPoint) self.innerFirstPoint = self.innerPrevPoint = self.innerCurrentPoint self.outerPen.moveTo(self.outerCurrentPoint) self.outerFirstPoint = self.outerPrevPoint = self.outerCurrentPoint self.firstAngle = a1 else: self.buildConnection() # Collect four original points originalPoints = [self.prevPoint, p1, p2, p3] # Collect four inner points h1 = p1 - self.pointClass(cos(a1), sin(a1)) * tickness1 h2 = p2 - self.pointClass(cos(a2), sin(a2)) * tickness2 innerPoints = [self.innerCurrentPoint, h1, h2] self.innerCurrentPoint = p3 - self.pointClass(cos(a2), sin(a2)) * tickness2 innerPoints.append(self.innerCurrentPoint) # Collect four outer points h1 = p1 + self.pointClass(cos(a1), sin(a1)) * tickness1 h2 = p2 + self.pointClass(cos(a2), sin(a2)) * tickness2 outerPoints = [self.outerCurrentPoint, h1, h2] self.outerCurrentPoint = p3 + self.pointClass(cos(a2), sin(a2)) * tickness2 outerPoints.append(self.outerCurrentPoint) """ # @@@ NEW TECHNIQUE: # Determine if a curve in/out of a BCP is rotating clockwise or counterclockwise # Compare vectors of the point to the bcp and the point to a split location just after/before the point splits = splitCubicAtT(self.prevPoint, p1, p2, p3, 0.01, 0.99) splitPt0 = self.pointClass(splits[0][-1]) splitPt1 = self.pointClass(splits[-1][0]) # vectors vBcp0 = self.prevPoint - p1 vNext0 = self.prevPoint - splitPt0 vBcp1 = p3 - p2 vNext1 = p3 - splitPt1 # Find which way the vector is rotating by looking at the sign of the dot product dot0 = vBcp0[0] * vNext0[1] - vBcp0[1] * vNext0[0] dot1 = vBcp1[0] * vNext1[1] - vBcp1[1] * vNext1[0] dir0 = 1 dir1 = 1 if dot0 < 0: dir0 = -1 if dot1 < 0: dir1 = -1 """ # @@@ OLD TECHNIQUE: # Determine if a curve in/out of a BCP is rotating clockwise or counterclockwise # Compare vectors of the point to the bcp and the point to the next point vBcp0 = self.prevPoint - p1 vNext0 = self.prevPoint - p3 vBcp1 = self.pointClass(pt3) - p2 vNext1 = self.pointClass(pt3) - self.prevPoint # Find which way the vector is rotating by looking at the sign of the dot product dot0 = vBcp0[0] * vNext0[1] - vBcp0[1] * vNext0[0] dot1 = vBcp1[0] * vNext1[1] - vBcp1[1] * vNext1[0] # Find the angles # ang0 = self.prevPoint.angle(p1) # ang1 = self.pointClass(pt3).angle(p2) # print(ang0, ang1) dir0 = 1 dir1 = 1 if dot0 < 0: dir0 = -1 if dot1 < 0: dir1 = -1 # print(dot0, dot1) """ # If the onCurves are closer to each other than 2x the thickness, reverse the direction of the inner curve angle = self.prevPoint.angle(p3) distance = self.prevPoint.distance(p3) thickness = self.getThickness(angle) reverseInner = False #print(distance, thickness) if distance < thickness * 2: reverseInner = True # @@@ Do this differently, it's not helping Two bugs (maybe related) - When the on-curves are closer to each other than 2x the offset, need to reverse the direction on the inner handles - When the BCPs are in the same direction and are on top of each other, the curve asymmetrically sets the direction (wrong) """ # Flatten the original curve flatPoints = flattenCurve(originalPoints) # Move the BCPs to fit the curve innerPoints = self.fitCurve(innerPoints, flatPoints, dir0, dir1) outerPoints = self.fitCurve(outerPoints, flatPoints, -dir0, -dir1) # Draw the four points self.innerPen.curveTo(innerPoints[1], innerPoints[2], innerPoints[3]) self.innerPrevPoint = self.innerCurrentPoint self.outerPen.curveTo(outerPoints[1], outerPoints[2], outerPoints[3]) self.outerPrevPoint = self.outerCurrentPoint self.prevPoint = p3 self.currentAngle = a2 self.prevAngle = a2 def _closePath(self): if self.shouldHandleMove: return if self.offset == 0: self.outerPen.closePath() self.innerPen.closePath() return if not self.prevPoint == self.firstPoint: self._lineTo(self.firstPoint) self.originalPen.closePath() self.innerPrevPoint = self.innerCurrentPoint self.innerCurrentPoint = self.innerFirstPoint self.outerPrevPoint = self.outerCurrentPoint self.outerCurrentPoint = self.outerFirstPoint self.prevAngle = self.currentAngle self.currentAngle = self.firstAngle self.buildConnection(close=True) self.innerPen.closePath() self.outerPen.closePath() def _endPath(self): if self.shouldHandleMove: return self.originalPen.endPath() self.innerPen.endPath() self.outerPen.endPath() if self.closeOpenPaths: innerContour = self.innerGlyph[-1] outerContour = self.outerGlyph[-1] innerContour.reverse() innerContour[0].segmentType = "line" outerContour[0].segmentType = "line" self.buildCap(outerContour, innerContour) for point in innerContour: outerContour.addPoint((point.x, point.y), segmentType=point.segmentType, smooth=point.smooth) self.innerGlyph.removeContour(innerContour) def addComponent(self, glyphName, transform): if self.preserveComponents: self.components.append((glyphName, transform)) else: BasePen.addComponent(self, glyphName, transform) # thickness def getThickness(self, angle): a2 = angle + pi * .5 f = abs(sin(a2 + radians(self.contrastAngle))) f = f**5 return self.offset + self.contrast * f # fit curve def _testCurve(self, pts, flatPts, f0, f1, inRange=False): # Normalize and scale the BCPs scaled0 = (pts[1] - pts[0]) * f0 pts[1] = pts[1] + scaled0 dist1 = abs(self.pointClass(pts[3]).distance(pts[2])) scaled1 = (pts[3] - pts[2]) * f1 pts[2] = pts[2] + scaled1 # Split at a few locations splitFactors = [0.25, 0.5, 0.75] newSplit = splitCubicAtT(pts[0], pts[1], pts[2], pts[3], *splitFactors) newSplitLocs = [self.pointClass(pts[-1]) for pts in newSplit] newSplitLocs = newSplitLocs[:-1] # Measure these splits back to the flattened original segment # Prepare chunks of the flatPts, if we should be testing inRange # ...a factor of 0.25 should only be compared against 0.2-0.3, 0.5 should be 0.45-0.55, 0.75 should be 0.7-0.8 totalFlats = len(flatPts) flatRanges = [(int(totalFlats * 0.2), int(totalFlats * 0.3)), (int(totalFlats * 0.45), int(totalFlats * 0.55)), (int(totalFlats * 0.7), int(totalFlats * 0.8)) ] # These relate directly to the factors in splitFactors totalDiff = 0 for i, loc in enumerate(newSplitLocs): if inRange: # If it should only check a smaller range of flat points # (experimental) flatPtChunk = flatPts[flatRanges[i][0]:flatRanges[i][1]] else: flatPtChunk = flatPts closestLoc, closestDist = self.findClosest(loc, flatPtChunk) angle = loc.angle(self.pointClass(closestLoc)) expectedDistance = self.getThickness(angle) diff = expectedDistance - closestDist totalDiff += diff return pts, totalDiff def findClosest(self, loc, locs): closest = None closestDist = None for testLoc in locs: d = abs(self.pointClass(loc).distance(self.pointClass(testLoc))) if not closestDist: closestDist = d closest = testLoc else: if d < closestDist: closestDist = d closest = testLoc return closest, closestDist def fitCurve(self, pts, flatPts, dir0, dir1): # pts is a list of four points defining a curve # Use three sample points along the curve to move the BCPs in directions dir0 and dir1 # until the sampled points average out to the correct distance from flatPoints # positive or negative factor for moving the BCPs, depending on the direction the curve is turning f = 0.05 if dir0 > 0: f0 = f else: f0 = -f if dir1 > 0: f1 = f else: f1 = -f closestPts = pts.copy() totalPasses = 0 # First pass, coarse while True: totalPasses += 1 if totalPasses > 50: break closestPts, totalDiff = self._testCurve(closestPts, flatPts, f0, f1) if totalDiff < 0: # The diff crossed zero break # Second pass, more fine and in the other direction f0 *= -0.1 f1 *= -0.1 totalPasses = 0 while True: totalPasses += 1 if totalPasses > 50: break closestPts, totalDiff = self._testCurve(closestPts, flatPts, f0, f1) if totalDiff > 0: # The diff crossed zero in the other direction this time break # Third pass, more fine and in the other direction f0 *= -0.1 f1 *= -0.1 totalPasses = 0 while True: totalPasses += 1 if totalPasses > 50: break closestPts, totalDiff = self._testCurve(closestPts, flatPts, f0, f1) if totalDiff < 0: # The diff crossed zero break return closestPts # connections def buildConnection(self, close=False): if self.alwaysConnect: # Always force a connection for compatibility self.connectionCallback(self.outerPrevPoint, self.outerCurrentPoint, self.outerPen, close) self.connectionCallback(self.innerPrevPoint, self.innerCurrentPoint, self.innerPen, close) else: if not checkSmooth(self.prevAngle, self.currentAngle): if checkInnerOuter(self.prevAngle, self.currentAngle): self.connectionCallback(self.outerPrevPoint, self.outerCurrentPoint, self.outerPen, close) self.connectionInnerCorner(self.innerPrevPoint, self.innerCurrentPoint, self.innerPen, close) else: self.connectionCallback(self.innerPrevPoint, self.innerCurrentPoint, self.innerPen, close) self.connectionInnerCorner(self.outerPrevPoint, self.outerCurrentPoint, self.outerPen, close) elif not self.filterDoubles: self.innerPen.lineTo(self.innerCurrentPoint) self.outerPen.lineTo(self.outerCurrentPoint) def connectionSquare(self, first, last, pen, close): angle_1 = radians(degrees(self.prevAngle) + 90) angle_2 = radians(degrees(self.currentAngle) + 90) tempFirst = first - self.pointClass(cos(angle_1), sin(angle_1)) * self.miterLimit tempLast = last + self.pointClass(cos(angle_2), sin(angle_2)) * self.miterLimit newPoint = interSect((first, tempFirst), (last, tempLast)) if newPoint is not None: if self._inputmiterLimit is not None and roundFloat( newPoint.distance(first)) > self._inputmiterLimit: pen.lineTo(tempFirst) pen.lineTo(tempLast) else: pen.lineTo(newPoint) if not close: pen.lineTo(last) def connectionRound(self, first, last, pen, close): angle_1 = radians(degrees(self.prevAngle) + 90) angle_2 = radians(degrees(self.currentAngle) + 90) tempFirst = first - self.pointClass(sin(angle_1), -cos(angle_1)) tempLast = last + self.pointClass(sin(angle_2), -cos(angle_2)) centerPoint = interSect((first, tempFirst), (last, tempLast)) if centerPoint is None: # the lines are parallel, let's just take the middle centerPoint = (first + last) / 2 angle_diff = (angle_1 - angle_2) % (2 * pi) if angle_diff > pi: angle_diff = 2 * pi - angle_diff angle_half = angle_diff / 2 radius = centerPoint.distance(first) D = radius * (1 - cos(angle_half)) try: handleLength = (4 * D / 3) / sin( angle_half) # length of the bcp line except: handleLength = 0 bcp1 = first - self.pointClass(cos(angle_1), sin(angle_1)) * handleLength bcp2 = last + self.pointClass(cos(angle_2), sin(angle_2)) * handleLength pen.curveTo(bcp1, bcp2, last) def connectionButt(self, first, last, pen, close): if not close: pen.lineTo(last) def connectionInnerCorner(self, first, last, pen, close): if not close: pen.lineTo(last) # caps def buildCap(self, firstContour, lastContour): first = firstContour[-1] last = lastContour[0] first = self.pointClass(first.x, first.y) last = self.pointClass(last.x, last.y) self.capCallback(firstContour, lastContour, first, last, self.prevAngle) first = lastContour[-1] last = firstContour[0] first = self.pointClass(first.x, first.y) last = self.pointClass(last.x, last.y) angle = radians(degrees(self.firstAngle) + 180) self.capCallback(lastContour, firstContour, first, last, angle) def capButt(self, firstContour, lastContour, first, last, angle): # not nothing pass def capRound(self, firstContour, lastContour, first, last, angle): hookedAngle = radians(degrees(angle) + 90) p1 = first - self.pointClass(cos(hookedAngle), sin(hookedAngle)) * self.offset p2 = last - self.pointClass(cos(hookedAngle), sin(hookedAngle)) * self.offset oncurve = p1 + (p2 - p1) * .5 roundness = .54 # should be self.magicCurve h1 = first - self.pointClass( cos(hookedAngle), sin(hookedAngle)) * self.offset * roundness h2 = oncurve + self.pointClass(cos(angle), sin(angle)) * self.offset * roundness firstContour[-1].smooth = True firstContour.addPoint((h1.x, h1.y)) firstContour.addPoint((h2.x, h2.y)) firstContour.addPoint((oncurve.x, oncurve.y), smooth=True, segmentType="curve") h1 = oncurve - self.pointClass(cos(angle), sin(angle)) * self.offset * roundness h2 = last - self.pointClass(cos(hookedAngle), sin(hookedAngle)) * self.offset * roundness firstContour.addPoint((h1.x, h1.y)) firstContour.addPoint((h2.x, h2.y)) lastContour[0].segmentType = "curve" lastContour[0].smooth = True def capSquare(self, firstContour, lastContour, first, last, angle): angle = radians(degrees(angle) + 90) firstContour[-1].smooth = True lastContour[0].smooth = True p1 = first - self.pointClass(cos(angle), sin(angle)) * self.offset firstContour.addPoint((p1.x, p1.y), smooth=False, segmentType="line") p2 = last - self.pointClass(cos(angle), sin(angle)) * self.offset firstContour.addPoint((p2.x, p2.y), smooth=False, segmentType="line") def capRoundsimple(self, firstContour, lastContour, first, last, angle): angle = radians(degrees(angle) + 90) firstContour[-1].smooth = True lastContour[0].smooth = True p1 = first - self.pointClass(cos(angle), sin(angle)) * (self.offset * 1.5) firstContour.addPoint((p1.x, p1.y)) p2 = last - self.pointClass(cos(angle), sin(angle)) * (self.offset * 1.5) firstContour.addPoint((p2.x, p2.y)) lastContour[0].segmentType = "curve" lastContour[0].smooth = True #firstContour.addPoint((last.x, last.y), smooth=True, segmentType="curve") def drawSettings(self, drawOriginal=False, drawInner=False, drawOuter=True): self.drawOriginal = drawOriginal self.drawInner = drawInner self.drawOuter = drawOuter def drawPoints(self, pointPen): if self.drawInner: reversePen = ReverseContourPointPen(pointPen) self.innerGlyph.drawPoints(CleanPointPen(reversePen)) if self.drawOuter: self.outerGlyph.drawPoints(CleanPointPen(pointPen)) if self.drawOriginal: if self.drawOuter: pointPen = ReverseContourPointPen(pointPen) self.originalGlyph.drawPoints(CleanPointPen(pointPen)) for glyphName, transform in self.components: pointPen.addComponent(glyphName, transform) def draw(self, pen): pointPen = PointToSegmentPen(pen) self.drawPoints(pointPen) def getGlyph(self): glyph = Glyph() pointPen = glyph.getPointPen() self.drawPoints(pointPen) return glyph
class OutlinePen(BasePen): pointClass = MathPoint magicCurve = 0.5522847498 def __init__(self, glyphSet, offset=10, contrast=0, contrastAngle=0, connection="square", cap="round", miterLimit=None, closeOpenPaths=True, preserveComponents=False): BasePen.__init__(self, glyphSet) self.offset = abs(offset) self.contrast = abs(contrast) self.contrastAngle = contrastAngle self._inputmiterLimit = miterLimit if miterLimit is None: miterLimit = self.offset * 2 self.miterLimit = abs(miterLimit) self.closeOpenPaths = closeOpenPaths self.connectionCallback = getattr(self, "connection%s" % (connection.title())) self.capCallback = getattr(self, "cap%s" % (cap.title())) self.originalGlyph = Glyph() self.originalPen = self.originalGlyph.getPen() self.outerGlyph = Glyph() self.outerPen = self.outerGlyph.getPen() self.outerCurrentPoint = None self.outerFirstPoint = None self.outerPrevPoint = None self.innerGlyph = Glyph() self.innerPen = self.innerGlyph.getPen() self.innerCurrentPoint = None self.innerFirstPoint = None self.innerPrevPoint = None self.prevPoint = None self.firstPoint = None self.firstAngle = None self.prevAngle = None self.shouldHandleMove = True self.preserveComponents = preserveComponents self.components = [] self.drawSettings() def _moveTo(self, xy): (x, y) = xy if self.offset == 0: self.outerPen.moveTo((x, y)) self.innerPen.moveTo((x, y)) return self.originalPen.moveTo((x, y)) p = self.pointClass(x, y) self.prevPoint = p self.firstPoint = p self.shouldHandleMove = True def _lineTo(self, xy): (x, y) = xy if self.offset == 0: self.outerPen.lineTo((x, y)) self.innerPen.lineTo((x, y)) return self.originalPen.lineTo((x, y)) currentPoint = self.pointClass(x, y) if currentPoint == self.prevPoint: return self.currentAngle = self.prevPoint.angle(currentPoint) thickness = self.getThickness(self.currentAngle) self.innerCurrentPoint = self.prevPoint - self.pointClass(cos(self.currentAngle), sin(self.currentAngle)) * thickness self.outerCurrentPoint = self.prevPoint + self.pointClass(cos(self.currentAngle), sin(self.currentAngle)) * thickness if self.shouldHandleMove: self.shouldHandleMove = False self.innerPen.moveTo(self.innerCurrentPoint) self.innerFirstPoint = self.innerCurrentPoint self.outerPen.moveTo(self.outerCurrentPoint) self.outerFirstPoint = self.outerCurrentPoint self.firstAngle = self.currentAngle else: self.buildConnection() self.innerCurrentPoint = currentPoint - self.pointClass(cos(self.currentAngle), sin(self.currentAngle)) * thickness self.innerPen.lineTo(self.innerCurrentPoint) self.innerPrevPoint = self.innerCurrentPoint self.outerCurrentPoint = currentPoint + self.pointClass(cos(self.currentAngle), sin(self.currentAngle)) * thickness self.outerPen.lineTo(self.outerCurrentPoint) self.outerPrevPoint = self.outerCurrentPoint self.prevPoint = currentPoint self.prevAngle = self.currentAngle def _curveToOne(self, xy1, xy2, xy3): (x1, y1), (x2, y2), (x3, y3) = xy1, xy2, xy3 if self.offset == 0: self.outerPen.curveTo((x1, y1), (x2, y2), (x3, y3)) self.innerPen.curveTo((x1, y1), (x2, y2), (x3, y3)) return self.originalPen.curveTo((x1, y1), (x2, y2), (x3, y3)) p1 = self.pointClass(x1, y1) p2 = self.pointClass(x2, y2) p3 = self.pointClass(x3, y3) if p1 == self.prevPoint: p1 = pointOnACurve(self.prevPoint, p1, p2, p3, 0.01) if p2 == p3: p2 = pointOnACurve(self.prevPoint, p1, p2, p3, 0.99) a1 = self.prevPoint.angle(p1) a2 = p2.angle(p3) self.currentAngle = a1 tickness1 = self.getThickness(a1) tickness2 = self.getThickness(a2) a1bis = self.prevPoint.angle(p1, 0) a2bis = p3.angle(p2, 0) intersectPoint = interSect((self. prevPoint, self.prevPoint + self.pointClass(cos(a1), sin(a1)) * 100), (p3, p3 + self.pointClass(cos(a2), sin(a2)) * 100)) self.innerCurrentPoint = self.prevPoint - self.pointClass(cos(a1), sin(a1)) * tickness1 self.outerCurrentPoint = self.prevPoint + self.pointClass(cos(a1), sin(a1)) * tickness1 if self.shouldHandleMove: self.shouldHandleMove = False self.innerPen.moveTo(self.innerCurrentPoint) self.innerFirstPoint = self.innerPrevPoint = self.innerCurrentPoint self.outerPen.moveTo(self.outerCurrentPoint) self.outerFirstPoint = self.outerPrevPoint = self.outerCurrentPoint self.firstAngle = a1 else: self.buildConnection() h1 = None if intersectPoint is not None: h1 = interSect((self.innerCurrentPoint, self.innerCurrentPoint + self.pointClass(cos(a1bis), sin(a1bis)) * tickness1), (intersectPoint, p1)) if h1 is None: h1 = p1 - self.pointClass(cos(a1), sin(a1)) * tickness1 self.innerCurrentPoint = p3 - self.pointClass(cos(a2), sin(a2)) * tickness2 h2 = None if intersectPoint is not None: h2 = interSect((self.innerCurrentPoint, self.innerCurrentPoint + self.pointClass(cos(a2bis), sin(a2bis)) * tickness2), (intersectPoint, p2)) if h2 is None: h2 = p2 - self.pointClass(cos(a1), sin(a1)) * tickness1 self.innerPen.curveTo(h1, h2, self.innerCurrentPoint) self.innerPrevPoint = self.innerCurrentPoint ######## h1 = None if intersectPoint is not None: h1 = interSect((self.outerCurrentPoint, self.outerCurrentPoint + self.pointClass(cos(a1bis), sin(a1bis)) * tickness1), (intersectPoint, p1)) if h1 is None: h1 = p1 + self.pointClass(cos(a1), sin(a1)) * tickness1 self.outerCurrentPoint = p3 + self.pointClass(cos(a2), sin(a2)) * tickness2 h2 = None if intersectPoint is not None: h2 = interSect((self.outerCurrentPoint, self.outerCurrentPoint + self.pointClass(cos(a2bis), sin(a2bis)) * tickness2), (intersectPoint, p2)) if h2 is None: h2 = p2 + self.pointClass(cos(a1), sin(a1)) * tickness1 self.outerPen.curveTo(h1, h2, self.outerCurrentPoint) self.outerPrevPoint = self.outerCurrentPoint self.prevPoint = p3 self.currentAngle = a2 self.prevAngle = a2 def _closePath(self): if self.shouldHandleMove: return if self.offset == 0: self.outerPen.closePath() self.innerPen.closePath() return if not self.prevPoint == self.firstPoint: self._lineTo(self.firstPoint) self.originalPen.closePath() self.innerPrevPoint = self.innerCurrentPoint self.innerCurrentPoint = self.innerFirstPoint self.outerPrevPoint = self.outerCurrentPoint self.outerCurrentPoint = self.outerFirstPoint self.prevAngle = self.currentAngle self.currentAngle = self.firstAngle self.buildConnection(close=True) self.innerPen.closePath() self.outerPen.closePath() def _endPath(self): if self.shouldHandleMove: return self.originalPen.endPath() self.innerPen.endPath() self.outerPen.endPath() if self.closeOpenPaths: innerContour = self.innerGlyph[-1] outerContour = self.outerGlyph[-1] innerContour.reverse() innerContour[0].segmentType = "line" outerContour[0].segmentType = "line" self.buildCap(outerContour, innerContour) for point in innerContour: outerContour.addPoint((point.x, point.y), segmentType=point.segmentType, smooth=point.smooth) self.innerGlyph.removeContour(innerContour) def addComponent(self, glyphName, transform): if self.preserveComponents: self.components.append((glyphName, transform)) else: BasePen.addComponent(self, glyphName, transform) ## thickness def getThickness(self, angle): a2 = angle + pi * .5 f = abs(sin(a2 + radians(self.contrastAngle))) f = f ** 5 return self.offset + self.contrast * f ## connections def buildConnection(self, close=False): if not checkSmooth(self.prevAngle, self.currentAngle): if checkInnerOuter(self.prevAngle, self.currentAngle): self.connectionCallback(self.outerPrevPoint, self.outerCurrentPoint, self.outerPen, close) self.connectionInnerCorner(self.innerPrevPoint, self.innerCurrentPoint, self.innerPen, close) else: self.connectionCallback(self.innerPrevPoint, self.innerCurrentPoint, self.innerPen, close) self.connectionInnerCorner(self.outerPrevPoint, self.outerCurrentPoint, self.outerPen, close) def connectionSquare(self, first, last, pen, close): angle_1 = radians(degrees(self.prevAngle)+90) angle_2 = radians(degrees(self.currentAngle)+90) tempFirst = first - self.pointClass(cos(angle_1), sin(angle_1)) * self.miterLimit tempLast = last + self.pointClass(cos(angle_2), sin(angle_2)) * self.miterLimit newPoint = interSect((first, tempFirst), (last, tempLast)) if newPoint is not None: if self._inputmiterLimit is not None and roundFloat(newPoint.distance(first)) > self._inputmiterLimit: pen.lineTo(tempFirst) pen.lineTo(tempLast) else: pen.lineTo(newPoint) if not close: pen.lineTo(last) def connectionRound(self, first, last, pen, close): angle_1 = radians(degrees(self.prevAngle)+90) angle_2 = radians(degrees(self.currentAngle)+90) tempFirst = first - self.pointClass(cos(angle_1), sin(angle_1)) * self.miterLimit tempLast = last + self.pointClass(cos(angle_2), sin(angle_2)) * self.miterLimit newPoint = interSect((first, tempFirst), (last, tempLast)) if newPoint is None: pen.lineTo(last) return #print "(%s, %s)," % (newPoint.x, newPoint.y) distance1 = newPoint.distance(first) distance2 = newPoint.distance(last) #print distance1, distance2 if roundFloat(distance1) > self.miterLimit + self.contrast: distance1 = self.miterLimit + tempFirst.distance(tempLast) * .7 if roundFloat(distance2) > self.miterLimit + self.contrast: distance2 = self.miterLimit + tempFirst.distance(tempLast) * .7 distance1 *= self.magicCurve distance2 *= self.magicCurve bcp1 = first - self.pointClass(cos(angle_1), sin(angle_1)) * distance1 bcp2 = last + self.pointClass(cos(angle_2), sin(angle_2)) * distance2 pen.curveTo(bcp1, bcp2, last) def connectionButt(self, first, last, pen, close): if not close: pen.lineTo(last) def connectionInnerCorner(self, first, last, pen, close): if not close: pen.lineTo(last) ## caps def buildCap(self, firstContour, lastContour): first = firstContour[-1] last = lastContour[0] first = self.pointClass(first.x, first.y) last = self.pointClass(last.x, last.y) self.capCallback(firstContour, lastContour, first, last, self.prevAngle) first = lastContour[-1] last = firstContour[0] first = self.pointClass(first.x, first.y) last = self.pointClass(last.x, last.y) angle = radians(degrees(self.firstAngle)+180) self.capCallback(lastContour, firstContour, first, last, angle) def capButt(self, firstContour, lastContour, first, last, angle): ## not nothing pass def capRound(self, firstContour, lastContour, first, last, angle): hookedAngle = radians(degrees(angle)+90) p1 = first - self.pointClass(cos(hookedAngle), sin(hookedAngle)) * self.offset p2 = last - self.pointClass(cos(hookedAngle), sin(hookedAngle)) * self.offset oncurve = p1 + (p2-p1)*.5 roundness = .54 h1 = first - self.pointClass(cos(hookedAngle), sin(hookedAngle)) * self.offset * roundness h2 = oncurve + self.pointClass(cos(angle), sin(angle)) * self.offset * roundness firstContour[-1].smooth = True firstContour.addPoint((h1.x, h1.y)) firstContour.addPoint((h2.x, h2.y)) firstContour.addPoint((oncurve.x, oncurve.y), smooth=True, segmentType="curve") h1 = oncurve - self.pointClass(cos(angle), sin(angle)) * self.offset * roundness h2 = last - self.pointClass(cos(hookedAngle), sin(hookedAngle)) * self.offset * roundness firstContour.addPoint((h1.x, h1.y)) firstContour.addPoint((h2.x, h2.y)) lastContour[0].segmentType = "curve" lastContour[0].smooth = True def capSquare(self, firstContour, lastContour, first, last, angle): angle = radians(degrees(angle)+90) firstContour[-1].smooth = True lastContour[0].smooth = True p1 = first - self.pointClass(cos(angle), sin(angle)) * self.offset firstContour.addPoint((p1.x, p1.y), smooth=False, segmentType="line") p2 = last - self.pointClass(cos(angle), sin(angle)) * self.offset firstContour.addPoint((p2.x, p2.y), smooth=False, segmentType="line") def drawSettings(self, drawOriginal=False, drawInner=False, drawOuter=True): self.drawOriginal = drawOriginal self.drawInner = drawInner self.drawOuter = drawOuter def drawPoints(self, pointPen): if self.drawInner: reversePen = ReverseContourPointPen(pointPen) self.innerGlyph.drawPoints(CleanPointPen(reversePen)) if self.drawOuter: self.outerGlyph.drawPoints(CleanPointPen(pointPen)) if self.drawOriginal: if self.drawOuter: pointPen = ReverseContourPointPen(pointPen) self.originalGlyph.drawPoints(CleanPointPen(pointPen)) for glyphName, transform in self.components: pointPen.addComponent(glyphName, transform) def draw(self, pen): pointPen = PointToSegmentPen(pen) self.drawPoints(pointPen) def getGlyph(self): glyph = Glyph() pointPen = glyph.getPointPen() self.drawPoints(pointPen) return glyph