Last active
May 15, 2024 17:43
-
-
Save frankrolf/a90c95a7c1c798befa9b8b64cd9f1e25 to your computer and use it in GitHub Desktop.
Robofont: close paths based on distance
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
''' | |
Robofont: Smart Close Paths | |
The idea is closing open contours based on point distance rather than | |
membership of a given contour. | |
''' | |
import itertools | |
import math | |
from fontTools.pens.basePen import BasePen | |
from fontTools.ufoLib.pointPen import AbstractPointPen | |
class CleanPen(AbstractPointPen): | |
def __init__(self, pointPen): | |
self.pointPen = pointPen | |
self.currentContour = None | |
self.contour_index = -1 | |
def processContour(self): | |
pointPen = self.pointPen | |
contour = self.currentContour | |
filtered_contour = [] | |
pt_index = 0 | |
for pt_index, pt_data in enumerate(contour): | |
pt_data_prev = contour[(pt_index - 1) % len(contour)] | |
pt_data_curr = contour[(pt_index)] | |
coords_prev = pt_data_prev['point'] | |
coords_curr = pt_data_curr['point'] | |
if ( | |
coords_prev == coords_curr and | |
pt_data_prev['segmentType'] is not None | |
): | |
pass | |
else: | |
filtered_contour.append(pt_data) | |
pt_index += 1 | |
pointPen.beginPath() | |
for data in filtered_contour: | |
pointPen.addPoint(data['point'], **data) | |
pointPen.endPath() | |
def beginPath(self, identifier=None): | |
assert self.currentContour is None | |
self.currentContour = [] | |
self.onCurve = [] | |
self.contour_index += 1 | |
def endPath(self): | |
assert self.currentContour is not None | |
self.processContour() | |
self.currentContour = None | |
def addPoint( | |
self, pt, segmentType=None, smooth=False, name=None, **kwargs | |
): | |
data = dict( | |
point=pt, | |
segmentType=segmentType, | |
smooth=smooth, | |
name=name | |
) | |
data.update(kwargs) | |
self.currentContour.append(data) | |
def addComponent(self, glyphName, transform): | |
pass | |
# assert self.currentContour is None | |
# self.pointPen.addComponent(glyphName, transform) | |
class ClosePen(BasePen): | |
''' | |
A pen that behaves like a normal pen, but does not know endPath. | |
''' | |
def __init__(self, pen): | |
self.other_pen = pen | |
def _moveTo(self, pt): | |
self.other_pen.moveTo(pt) | |
def _lineTo(self, pt): | |
self.other_pen.lineTo(pt) | |
def _curveToOne(self, pt1, pt2, pt3): | |
self.other_pen.curveTo(pt1, pt2, pt3) | |
def endPath(self): | |
self.other_pen.closePath() | |
def closePath(self): | |
self.other_pen.closePath() | |
class PointInfo(object): | |
''' | |
A class that stores info about a given point | |
''' | |
def __init__(self, point): | |
self.x, self.y = point.anchor | |
self.contour = point.contour | |
glyph = self.contour.glyph | |
self.c_index = glyph.contours.index(self.contour) | |
self.p_index = self.contour.bPoints.index(point) | |
if self.p_index == 0: | |
self.lastPoint = False | |
self.firstPoint = True | |
else: | |
self.lastPoint = True | |
self.firstPoint = False | |
def point_delta(pt1, pt2): | |
'''distance between two bpoints''' | |
return math.hypot( | |
(pt1.anchor[0] - pt2.anchor[0]), | |
(pt1.anchor[1] - pt2.anchor[1])) | |
def add_contour_to_end_bPoints(contour_a, contour_b): | |
'''adds one contour to another''' | |
# works in 1.8 not 3 before 3.2 | |
for bp in contour_b.bPoints: | |
# this worked in 1.8: | |
# contour_a.appendBPoint(bp.type, bp.anchor, bp.bcpIn, bp.bcpOut) | |
contour_a.appendBPoint(bp.type, bp.anchor, bp.bcpIn, None) | |
g.removeContour(contour_b) | |
def add_contour_to_end(contour_a, contour_b): | |
'''adds one contour to another''' | |
for p in contour_b.points: | |
if p.type == 'move': | |
contour_a.appendPoint( | |
(p.x, p.y), 'line', p.smooth, p.name, p.identifier) | |
else: | |
contour_a.appendPoint( | |
(p.x, p.y), p.type, p.smooth, p.name, p.identifier) | |
g.removeContour(contour_b) | |
def closest_pair(g): | |
point_list = [] | |
# collect all start- and endpoints of open contours | |
for contour in g.contours: | |
if contour.open: | |
point_list.append(contour.bPoints[0]) | |
point_list.append(contour.bPoints[-1]) | |
# collect the deltas, and find the minmum delta | |
delta_dict = {} | |
for pair in (itertools.combinations(point_list, 2)): | |
# using a dict here since some point combinations may have the same | |
# distance. | |
delta_dict.setdefault(point_delta(*pair), []).append(pair) | |
if delta_dict: | |
min_delta = min(delta_dict.keys()) | |
point_pair = delta_dict.get(min_delta)[0] | |
return point_pair | |
else: | |
return | |
if __name__ == '__main__': | |
g = CurrentGlyph() | |
g.prepareUndo('close closest contours') | |
point_pair = closest_pair(g) | |
# not sure if that really makes sense | |
# start_points = {contour.index: contour.bPoints[0].anchor for contour in g.contours} | |
# if point_pair: # for step-by-step | |
while point_pair: | |
point_a, point_b = point_pair | |
pointinfo_a = PointInfo(point_a) | |
pointinfo_b = PointInfo(point_b) | |
contour_a = pointinfo_a.contour | |
contour_b = pointinfo_b.contour | |
if pointinfo_a.c_index == pointinfo_b.c_index: | |
# points are part of the same contour, | |
# the contour needs to be closed | |
contour = pointinfo_a.contour | |
temp_glyph = RGlyph() | |
pen = ClosePen(temp_glyph.getPen()) | |
contour.draw(pen) | |
g.removeContour(contour) | |
g.appendContour(temp_glyph.contours[0]) | |
temp_glyph.clear() | |
else: | |
# points are part of different contours | |
if pointinfo_a.firstPoint and pointinfo_b.firstPoint: | |
# print('first first') | |
contour_a.reverse() | |
# contour_a.insertBPoint(0, 'corner', (point_b.x, point_b.y)) | |
add_contour_to_end(contour_a, contour_b) | |
contour_a.reverse() | |
elif pointinfo_a.firstPoint and pointinfo_b.lastPoint: | |
# print('first last') | |
contour_a.reverse() | |
contour_b.reverse() | |
add_contour_to_end(contour_a, contour_b) | |
contour_a.reverse() | |
elif pointinfo_a.lastPoint and pointinfo_b.firstPoint: | |
# print('last first') | |
# contour_a.appendBPoint('corner', point_b.anchor) | |
add_contour_to_end(contour_a, contour_b) | |
elif pointinfo_a.lastPoint and pointinfo_b.lastPoint: | |
# print('last last') | |
contour_b.reverse() | |
# contour_a.appendBPoint('corner', point_b.anchor) | |
add_contour_to_end(contour_a, contour_b) | |
point_pair = closest_pair(g) | |
g.changed() | |
g.performUndo() | |
g_copy = RGlyph() | |
g.prepareUndo('clean double points') | |
g.drawPoints(CleanPen(g_copy.getPointPen())) | |
g.clearContours() | |
g.appendGlyph(g_copy) | |
g.changed() | |
g.performUndo() | |
g.deselect() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment