├── Add Anchors From File.py ├── AutoCursiveAttachment - Add Anchors.py ├── AutoCursiveAttachment - Get Sample.py ├── Autokern.py ├── Cadence Grid.py ├── Close But No Cigar.py ├── Comb.py ├── Copy Myanmar Anchors.py ├── Curve All Straights.py ├── Delete Close Points.py ├── Interpolated Nudge.py ├── Kern Optimizer.py ├── LICENSE ├── LightSpace.py ├── Make Compatible.py ├── Make bottom-left node first.py ├── Myanmar Medial Ra Maker.py ├── Nastaliq Connection Editor.py ├── Optical Center.py ├── README.md ├── Raise Ascender.py ├── Recipe Dumper.py ├── Rename To Glyphs Default Names.py ├── Reset Anchors.py ├── Round Everything.py ├── Sandblast.py ├── Sansomatic.py ├── Straighten All Curves.py ├── Tallest And Shortest.py ├── Why You Not Compatible.py ├── componentize.py └── glyphmonkey.py /Add Anchors From File.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Add Anchors From File 2 | # -*- coding: utf-8 -*- 3 | import GlyphsApp 4 | from Foundation import NSPoint, NSValue, NSMinY, NSMaxY, NSMinX, NSMaxX 5 | import csv 6 | 7 | 8 | with open('anchors.csv') as csvfile: 9 | reader = csv.DictReader(csvfile) 10 | for row in reader: 11 | if "name" in row: 12 | glyph = row["name"] 13 | elif "glyph" in row: 14 | glyph = row["glyph"] 15 | else: 16 | raise ValueError("Glyph name column not found") 17 | if not glyph in Glyphs.font.glyphs: 18 | print("Could not find glyph %s" % glyph) 19 | continue 20 | anchornames = [ x.replace("_x","") for x in row.keys() if "_x" in x] 21 | Layer = Glyphs.font.glyphs[glyph].layers[0] 22 | print(glyph) 23 | for a in anchornames: 24 | if not row[a+"_x"] or not row[a+"_y"]: 25 | continue 26 | x = int(row[a+"_x"]) 27 | y = int(row[a+"_y"]) 28 | print(a, x, y) 29 | Layer.anchors[a] = GSAnchor(a) 30 | Layer.anchors[a].position = NSPoint(x,y) 31 | -------------------------------------------------------------------------------- /AutoCursiveAttachment - Add Anchors.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: AutoCursiveAttachment - Add Anchors 2 | # -*- coding: utf-8 -*- 3 | __doc__=""" 4 | Automatic attachment generation 5 | """ 6 | import GlyphsApp 7 | from Foundation import NSPoint, NSValue, NSMinY, NSMaxY, NSMinX, NSMaxX 8 | from itertools import tee 9 | import math 10 | 11 | def shouldHaveExit(Layer): 12 | # This is ugly and should use Unicode properties 13 | n = str(Layer.parent.name) 14 | return "ini" in n.lower() or "med" in n.lower() 15 | 16 | def shouldHaveEntry(Layer): 17 | n = str(Layer.parent.name) 18 | return "fin" in n.lower() or "med" in n.lower() 19 | 20 | def pairwise(iterable): 21 | a, b = tee(iterable) 22 | next(b, None) 23 | return zip(a, b) 24 | 25 | def calcTangent(t,segment): 26 | # calculates reference Tangent Point (from its coordinates plugin will be able to get tangent's direction) 27 | if len(segment) == 4: # for curves 28 | divided = divideCurve(segment[0], segment[1], segment[2], segment[3], t) 29 | R2 = divided[4] 30 | elif len(segment) == 2: # for line 31 | R2 = segment[1] 32 | Tangent = NSPoint(R2.x,R2.y) 33 | return Tangent 34 | 35 | def angle( A, B ): 36 | try: 37 | """calc angle between AB and Xaxis """ 38 | xDiff = A.x - B.x 39 | yDiff = A.y - B.y 40 | if yDiff== 0 or xDiff== 0: 41 | tangens = 0 42 | else: 43 | tangens = yDiff / xDiff 44 | 45 | angle = math.atan( tangens ) 46 | return angle 47 | except: 48 | print(traceback.format_exc()) 49 | 50 | def clonePath(path): 51 | p2 = path.copy() 52 | p2.convertToCubic() 53 | return p2 54 | 55 | def calcClosestInfo(layer, pt): 56 | closestPoint = None 57 | closestPath = None 58 | closestPathTime = None 59 | dist = 100000 60 | for path in layer.paths: 61 | path = clonePath(path) 62 | currClosestPoint, currPathTime = path.nearestPointOnPath_pathTime_(pt, None) 63 | currDist = distance(currClosestPoint, pt) 64 | if currDist < dist: 65 | dist = currDist 66 | closestPoint = currClosestPoint 67 | closestPathTime = currPathTime 68 | closestPath = path 69 | if closestPathTime is None: 70 | return None 71 | n = math.floor(closestPathTime) 72 | OnNode = closestPath.nodes[n] 73 | if not OnNode: 74 | return None 75 | if OnNode.type == CURVE: 76 | segment = (closestPath.nodes[n - 3].position, closestPath.nodes[n - 2].position, closestPath.nodes[n - 1].position, OnNode.position) 77 | else: 78 | prevPoint = closestPath.nodes[n - 1] 79 | if prevPoint: 80 | segment = (prevPoint.position, OnNode.position) 81 | else: 82 | return 83 | 84 | TangentDirection = calcTangent(closestPathTime % 1, segment) 85 | directionAngle = angle(closestPoint,TangentDirection) 86 | 87 | if TangentDirection.x == segment[0].x: # eliminates problem with vertical lines ###UGLY? 88 | directionAngle = -math.pi/2 89 | 90 | yTanDistance = math.sin(directionAngle) 91 | xTanDistance = math.cos(directionAngle) 92 | closestPointTangent = NSPoint(xTanDistance+closestPoint.x,yTanDistance+closestPoint.y) 93 | 94 | return { 95 | "onCurve": closestPoint, 96 | "pathTime": closestPathTime, 97 | "path": closestPath, 98 | "segment": segment, 99 | "directionAngle": directionAngle 100 | } 101 | 102 | def mysqdistance(p1,p2): 103 | return (p1.x-p2.x)*(p1.x-p2.x) + (p1.y-p2.y)*(p1.y-p2.y) 104 | def lerp(t,a,b): 105 | return NSValue.valueWithPoint_(NSPoint(int((1-t)*a.x + t*b.x), int((1-t)*a.y + t*b.y))) 106 | 107 | Layer = Glyphs.font.selectedLayers[0] 108 | 109 | if not Layer.master.customParameters["autocursiveattachment_distbelow"]: 110 | Message("No sample found", "Run 'Get Sample' script first") 111 | 112 | # Do entry anchor 113 | distbelow = Layer.master.customParameters["autocursiveattachment_distbelow"] 114 | distabove = Layer.master.customParameters["autocursiveattachment_distabove"] 115 | targetDistance = distbelow + distabove 116 | prop = distbelow / targetDistance 117 | 118 | def doTrial(anchor, x): 119 | startPoint = NSMakePoint(x, NSMinY(Layer.bounds)) 120 | endPoint = NSMakePoint(x, NSMaxY(Layer.bounds)) 121 | result = Layer.calculateIntersectionsStartPoint_endPoint_(startPoint, endPoint) 122 | if len(result) <= 2: 123 | return None, None 124 | result = result[1:3] # Generally speaking it will be the lowest 125 | # Except jeem. Urgh. 126 | for r in pairwise(result): 127 | distance = r[1].y - r[0].y 128 | if distance < 10 or distance > 500: 129 | continue 130 | result = r 131 | break 132 | if distance < 10 or distance > 500: 133 | return None, None 134 | bottomClosest = calcClosestInfo(Layer, NSMakePoint(result[0].x, result[0].y)) 135 | bt = bottomClosest["directionAngle"] 136 | tt = calcClosestInfo(Layer, NSMakePoint(result[1].x, result[1].y))["directionAngle"] 137 | # if anchor == "exit": 138 | # bt = bt 139 | # tt = - tt 140 | score = (distance - targetDistance)**2 141 | print("Square distance diff", score) 142 | tangentScore = 5 143 | print("Top tangent was: ", tt) 144 | print("Expected top tangent was: ", Layer.master.customParameters["autocursiveattachment_topTangent"]) 145 | print("Bottom tangent was: ", bt) 146 | print("Expected bottom tangent was: ", Layer.master.customParameters["autocursiveattachment_bottomTangent"]) 147 | 148 | tangentContribution = ( tangentScore*(bt-Layer.master.customParameters["autocursiveattachment_bottomTangent"])) ** 2 + (tangentScore*(tt-Layer.master.customParameters["autocursiveattachment_topTangent"])) ** 2 149 | print("Tangent contribution to score", tangentContribution) 150 | score = score + tangentContribution 151 | # if anchor == "exit": 152 | # score = score + x**2 153 | # else: 154 | # score = score + (Layer.width-x)**2 155 | placement = NSMakePoint(x, prop * result[1].y + (1-prop) * (result[0].y)) 156 | return score, placement 157 | 158 | 159 | if shouldHaveExit(Layer) and not "exit" in Layer.anchors: 160 | bestScore = 99999 161 | bestPoint = None 162 | for potX in range(int(NSMinX(Layer.bounds)),100): 163 | score, placement = doTrial("exit",potX) 164 | if score: 165 | print("Score for %i is %f" % (potX, score)) 166 | if score < bestScore: 167 | bestScore = score 168 | bestPoint = placement 169 | Layer.anchors['exit'] = GSAnchor("exit") 170 | Layer.anchors["exit"].position = bestPoint 171 | 172 | if shouldHaveEntry(Layer) and not "entry" in Layer.anchors: 173 | bestScore = 99999 174 | bestPoint = None 175 | for potX in range(int(NSMaxX(Layer.bounds)+100),int(NSMaxX(Layer.bounds))-100, -1): 176 | score, placement = doTrial("entry",potX) 177 | if score: 178 | print("Score for %i is %f" % (potX, score)) 179 | if score < bestScore: 180 | bestScore = score 181 | bestPoint = placement 182 | Layer.anchors['entry'] = GSAnchor("entry") 183 | Layer.anchors["entry"].position = bestPoint 184 | 185 | -------------------------------------------------------------------------------- /AutoCursiveAttachment - Get Sample.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: AutoCursiveAttachment - Get Sample 2 | # -*- coding: utf-8 -*- 3 | __doc__=""" 4 | Get information from a sample anchor for automatic attachment generation 5 | """ 6 | import GlyphsApp 7 | from Foundation import NSPoint, NSValue, NSMinY, NSMaxY 8 | from itertools import tee 9 | import math 10 | 11 | def pairwise(iterable): 12 | a, b = tee(iterable) 13 | next(b, None) 14 | return zip(a, b) 15 | 16 | def calcTangent(t,segment): 17 | # calculates reference Tangent Point (from its coordinates plugin will be able to get tangent's direction) 18 | if len(segment) == 4: # for curves 19 | divided = divideCurve(segment[0], segment[1], segment[2], segment[3], t) 20 | R2 = divided[4] 21 | elif len(segment) == 2: # for line 22 | R2 = segment[1] 23 | 24 | Tangent = NSPoint(R2.x,R2.y) 25 | return Tangent 26 | 27 | def angle( A, B ): 28 | try: 29 | """calc angle between AB and Xaxis """ 30 | xDiff = A.x - B.x 31 | yDiff = A.y - B.y 32 | if yDiff== 0 or xDiff== 0: 33 | tangens = 0 34 | else: 35 | tangens = yDiff / xDiff 36 | 37 | angle = math.atan( tangens ) 38 | return angle 39 | except: 40 | print(traceback.format_exc()) 41 | 42 | def clonePath(path): 43 | p2 = path.copy() 44 | p2.convertToCubic() 45 | return p2 46 | 47 | def calcClosestInfo(layer, pt): 48 | closestPoint = None 49 | closestPath = None 50 | closestPathTime = None 51 | dist = 100000 52 | for path in layer.paths: 53 | path = clonePath(path) 54 | currClosestPoint, currPathTime = path.nearestPointOnPath_pathTime_(pt, None) 55 | currDist = distance(currClosestPoint, pt) 56 | if currDist < dist: 57 | dist = currDist 58 | closestPoint = currClosestPoint 59 | closestPathTime = currPathTime 60 | closestPath = path 61 | if closestPathTime is None: 62 | return None 63 | n = math.floor(closestPathTime) 64 | OnNode = closestPath.nodes[n] 65 | if not OnNode: 66 | return None 67 | if OnNode.type == CURVE: 68 | segment = (closestPath.nodes[n - 3].position, closestPath.nodes[n - 2].position, closestPath.nodes[n - 1].position, OnNode.position) 69 | else: 70 | prevPoint = closestPath.nodes[n - 1] 71 | if prevPoint: 72 | segment = (prevPoint.position, OnNode.position) 73 | else: 74 | return 75 | 76 | TangentDirection = calcTangent(closestPathTime % 1, segment) 77 | directionAngle = angle(closestPoint,TangentDirection) 78 | 79 | if TangentDirection.x == segment[0].x: # eliminates problem with vertical lines ###UGLY? 80 | directionAngle = -math.pi/2 81 | 82 | yTanDistance = math.sin(directionAngle) 83 | xTanDistance = math.cos(directionAngle) 84 | closestPointTangent = NSPoint(xTanDistance+closestPoint.x,yTanDistance+closestPoint.y) 85 | 86 | return { 87 | "onCurve": closestPoint, 88 | "pathTime": closestPathTime, 89 | "path": closestPath, 90 | "segment": segment, 91 | "directionAngle": directionAngle 92 | } 93 | 94 | def mysqdistance(p1,p2): 95 | return (p1.x-p2.x)*(p1.x-p2.x) + (p1.y-p2.y)*(p1.y-p2.y) 96 | def lerp(t,a,b): 97 | return NSValue.valueWithPoint_(NSPoint(int((1-t)*a.x + t*b.x), int((1-t)*a.y + t*b.y))) 98 | 99 | Layer = Glyphs.font.selectedLayers[0] 100 | inout = list(filter(lambda x: x.name == "entry" or x.name=="exit", Layer.anchors)) 101 | if not inout: 102 | Message("No anchors found", "Create an entry or exit anchor to use this script") 103 | 104 | distabove = 0 105 | distbelow = 0 106 | bottomTangent = 0 107 | topTangent = 0 108 | 109 | for anchor in inout: 110 | startPoint = NSMakePoint(anchor.x, NSMinY(Layer.bounds)) 111 | endPoint = NSMakePoint(anchor.x, NSMaxY(Layer.bounds)) 112 | result = Layer.calculateIntersectionsStartPoint_endPoint_(startPoint, endPoint) 113 | if len(result) <= 2: 114 | Message("No paths found", "Anchor needs to be within a path") 115 | result = sorted(list(set(result[1:-1])), key= lambda p:p.y) 116 | # Remove repeated results 117 | print("Result was", result) 118 | selected = min(pairwise(result), key=lambda x: mysqdistance(anchor.position, x[0])+mysqdistance(anchor.position, x[1])) 119 | print("Selected" , selected) 120 | distabove = distabove + abs(max([p.y - anchor.y for p in selected])) 121 | distbelow = distbelow + abs(max([anchor.y - p.y for p in selected])) 122 | bottomClosest = calcClosestInfo(Layer, NSMakePoint(selected[0].x, selected[0].y)) 123 | print(bottomClosest) 124 | bt = bottomClosest["directionAngle"] 125 | tt = calcClosestInfo(Layer, NSMakePoint(selected[1].x, selected[1].y))["directionAngle"] 126 | # if anchor.name == "exit": 127 | # bt = -bt 128 | # tt = - tt 129 | print("TT %s angle = %f" %(anchor.name, tt)) 130 | print("BT %s angle = %f" %(anchor.name, bt)) 131 | bottomTangent = bottomTangent + bt 132 | topTangent = topTangent + tt 133 | 134 | topTangent = topTangent / len(inout) 135 | 136 | Layer.master.customParameters["autocursiveattachment_distbelow"] = distbelow / len(inout) 137 | Layer.master.customParameters["autocursiveattachment_distabove"] = distabove / len(inout) 138 | Layer.master.customParameters["autocursiveattachment_bottomTangent"] = bottomTangent / len(inout) 139 | Layer.master.customParameters["autocursiveattachment_topTangent"] = topTangent / len(inout) 140 | Message("Sample taken", "Now use 'add anchors'") 141 | -------------------------------------------------------------------------------- /Autokern.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Autokern 2 | # -*- coding: utf-8 -*- 3 | __doc__=""" 4 | Autokerner! 5 | """ 6 | 7 | import vanilla 8 | import math 9 | import itertools 10 | 11 | import os 12 | pathname = os.path.dirname(os.path.realpath(__file__)) 13 | extralibs = pathname+"/extra_python_libs/" 14 | os.chdir(pathname) 15 | print(pathname) 16 | sys.path.insert(0,extralibs) 17 | print(sys.path) 18 | import np_utils 19 | 20 | from Foundation import NSMinX, NSMakePoint 21 | 22 | failed = [] 23 | try: 24 | from sklearn.cluster import DBSCAN 25 | from sklearn.metrics.pairwise import euclidean_distances 26 | except Exception, e: 27 | failed.append("sklearn") 28 | 29 | try: 30 | import numpy as np 31 | except Exception, e: 32 | failed.append("numpy") 33 | 34 | try: 35 | import requests 36 | except Exception, e: 37 | failed.append("requests") 38 | 39 | try: 40 | import keras 41 | from keras import backend as K 42 | except Exception, e: 43 | failed.append("keras", "np_utils") 44 | 45 | testLetters = [] 46 | 47 | # Uncomment this to limit to a subset 48 | # testLetters = ["H","T", "Tcedilla", "o", "a", "period","A","V"] 49 | 50 | batch_size = 1024 51 | 52 | windowHeight = 450 53 | windowWidth = 720 54 | samples = 100 55 | url = "http://www.simon-cozens.org/downloads/kernmodel.hdf5" 56 | filename = "kernmodel.hdf5" 57 | 58 | # Helper class needed to load model 59 | class WeightedCategoricalCrossEntropy(object): 60 | def __init__(self, matrix): 61 | self.weights = matrix 62 | self.__name__ = 'w_categorical_crossentropy' 63 | 64 | def __call__(self, y_true, y_pred): 65 | return self.w_categorical_crossentropy(y_true, y_pred) 66 | 67 | def w_categorical_crossentropy(self, y_true, y_pred): 68 | nb_cl = len(self.weights) 69 | final_mask = K.zeros_like(y_pred[..., 0]) 70 | y_pred_max = K.max(y_pred, axis=-1) 71 | y_pred_max = K.expand_dims(y_pred_max, axis=-1) 72 | y_pred_max_mat = K.equal(y_pred, y_pred_max) 73 | for c_p, c_t in itertools.product(range(nb_cl), range(nb_cl)): 74 | w = K.cast(self.weights[c_t, c_p], K.floatx()) 75 | y_p = K.cast(y_pred_max_mat[..., c_p], K.floatx()) 76 | y_t = K.cast(y_true[..., c_t], K.floatx()) 77 | final_mask += w * y_p * y_t 78 | return K.categorical_crossentropy(y_pred, y_true) * final_mask 79 | 80 | 81 | class Autokern(): 82 | def __init__(self,lgroups, rgroups, sideBearings): 83 | self.lgroups = lgroups 84 | self.rgroups = rgroups 85 | self.sideBearings = sideBearings 86 | self.w = vanilla.FloatingWindow( (windowWidth, 160), "Autokerning") 87 | self.w.text_anchorL = vanilla.TextBox( (10, 10, windowWidth - 10, 25), "", "center") 88 | self.w.justLowerCheck = vanilla.CheckBox( (60, 35, 100, 20), "a-z", callback = self.justLowerCallback) 89 | self.w.justUpperCheck = vanilla.CheckBox( (160, 35, 100, 20), "A-Z", callback = self.justLowerCallback) 90 | self.w.allPairsCheck = vanilla.CheckBox( (260, 35, 100, 20), "All pairs", callback = self.allPairsCallback, value=True) 91 | self.w.progressBar = vanilla.ProgressBar( (10, 70, windowWidth-20, 10)) 92 | self.w.proceed = vanilla.Button( (windowWidth/2 -50, -50, 100,20), "Kern!", callback = self.kern) 93 | self.w.open() 94 | 95 | def allPairsCallback(self,sender): 96 | global testLetters 97 | if sender.get(): 98 | testLetters = [] 99 | self.w.justLowerCheck.set(False) 100 | self.w.justLowerCheck.enable(False) 101 | self.w.justUpperCheck.set(False) 102 | self.w.justUpperCheck.enable(False) 103 | else: 104 | self.w.justLowerCheck.enable(True) 105 | self.w.justUpperCheck.enable(True) 106 | print(testLetters) 107 | 108 | def justLowerCallback(self,sender): 109 | global testLetters 110 | testLetters = [] 111 | if self.w.justLowerCheck.get(): 112 | testLetters.extend("abcdefghijklmnopqrstuvwxyz") # .extend is a trick here 113 | if self.w.justUpperCheck.get(): 114 | testLetters.extend("ABCDEFGHIJKLMNOPQRSTUVWXYZ") 115 | if len(testLetters) > 1: 116 | self.w.allPairsCheck.set(False) 117 | else: 118 | self.w.allPairsCheck.set(True) 119 | print(testLetters) 120 | 121 | def kern(self,sender): 122 | self.w.text_anchorL.set("Gathering input tensors...") 123 | input_tensors = { "pair": [], "rightofl": [], "leftofr": [], "leftofl": [], "rightofr": [], "rightofo": [], "rightofH": [] } 124 | 125 | masterID = Glyphs.font.selectedLayers[0].associatedMasterId 126 | mwidth = Glyphs.font.glyphs["m"].layers[masterID].width 127 | 128 | def rightcontour(g): 129 | return np.array(self.sideBearings[g]["right"])/mwidth 130 | def leftcontour(g): 131 | return np.array(self.sideBearings[g]["left"])/mwidth 132 | def bin_to_label(value, mwidth): 133 | rw = 800 134 | scale = mwidth/rw 135 | if value == 0: 136 | low = int(-150 * scale); high = int(-100 * scale) 137 | if value == 1: 138 | low = int(-100 * scale); high = int(-70 * scale) 139 | if value == 2: 140 | low = int(-70 * scale); high = int(-50 * scale) 141 | if value == 3: 142 | low = int(-50 * scale); high = int(-45 * scale) 143 | if value == 4: 144 | low = int(-45 * scale); high = int(-40 * scale) 145 | if value == 5: 146 | low = int(-40 * scale); high = int(-35 * scale) 147 | if value == 6: 148 | low = int(-35 * scale); high = int(-30 * scale) 149 | if value == 7: 150 | low = int(-30 * scale); high = int(-25 * scale) 151 | if value == 8: 152 | low = int(-25 * scale); high = int(-20 * scale) 153 | if value == 9: 154 | low = int(-20 * scale); high = int(-15 * scale) 155 | if value == 10: 156 | low = int(-15 * scale); high = int(-10 * scale) 157 | if value == 11: 158 | low = int(-11 * scale); high = int(-5 * scale) 159 | if value == 12: 160 | low = int(-5 * scale); high = int(-0 * scale) 161 | if value == 13: 162 | return 0 163 | if value == 14: 164 | low = int(0 * scale); high = int(5 * scale) 165 | if value == 15: 166 | low = int(5 * scale); high = int(10 * scale) 167 | if value == 16: 168 | low = int(10 * scale); high = int(15 * scale) 169 | if value == 17: 170 | low = int(15 * scale); high = int(20 * scale) 171 | if value == 18: 172 | low = int(20 * scale); high = int(25 * scale) 173 | if value == 19: 174 | low = int(25 * scale); high = int(30 * scale) 175 | if value == 20: 176 | low = int(30 * scale); high = int(50 * scale) 177 | return int((low+high)/10)*5 178 | count = 0 179 | 180 | if len(testLetters) > 0: 181 | self.rgroups = [] 182 | self.lgroups = [] 183 | for l in testLetters: 184 | self.rgroups.append([l]) 185 | self.lgroups.append([l]) 186 | 187 | total = len(self.rgroups)*len(self.lgroups) 188 | for r in self.rgroups: 189 | for l in self.lgroups: 190 | # The first in a group is the exemplar. 191 | # The last in the group is the group name. 192 | right = r[0] 193 | left = l[0] 194 | input_tensors["pair"].append([ l[-1], r[-1] ]) 195 | input_tensors["rightofl"].append(rightcontour(left)) 196 | input_tensors["leftofr"].append(leftcontour(right)) 197 | input_tensors["rightofr"].append(rightcontour(right)) 198 | input_tensors["leftofl"].append(leftcontour(left)) 199 | input_tensors["rightofo"].append(rightcontour("o")) 200 | input_tensors["rightofH"].append(rightcontour("H")) 201 | count = count + 1 202 | self.w.progressBar.set( 100 * count / total ) 203 | self.w.text_anchorL.set("Enumerating kern pairs (this will take a while)...") 204 | count = 0 205 | # Split into batches... 206 | indices = np.arange(total) 207 | batches = total / batch_size 208 | self.w.progressBar.set( 100 * count / total ) 209 | total_pairs = 0 210 | 211 | while count < total: 212 | bLow = count 213 | bHigh = count + batch_size 214 | batch_tensors = {} 215 | for k in input_tensors.keys(): 216 | batch_tensors[k] = np.array(input_tensors[k][bLow:bHigh]) 217 | predictions = np.array(self.model.predict(batch_tensors)) 218 | classes = np.argmax(predictions, axis=1) 219 | for pair, prediction in zip(batch_tensors["pair"],classes): 220 | units = bin_to_label(prediction,mwidth) 221 | if len(testLetters) > 0: 222 | print(pair[0],pair[1],units) 223 | if units != 0: 224 | total_pairs = total_pairs + 1 225 | Glyphs.font.setKerningForPair(masterID, pair[0], pair[1], units) 226 | else: 227 | Glyphs.font.removeKerningForPair(masterID, pair[0], pair[1]) 228 | count += batch_size 229 | self.w.progressBar.set( 100 * count / total ) 230 | self.w.text_anchorL.set("We're done. Created %i kern pairs." % total_pairs) 231 | self.w.proceed.enable(False) 232 | 233 | def go(self): 234 | print("Loading model") 235 | self.w.text_anchorL.set("Loading model...") 236 | weight_matrix = [] 237 | self.model = keras.models.load_model(filename, custom_objects={'w_categorical_crossentropy': WeightedCategoricalCrossEntropy(weight_matrix)}, compile=False) 238 | self.w.text_anchorL.set("Model loaded. Let's do this. (Close the Kerning window before hitting the button.)") 239 | 240 | class ModelDownloader(): 241 | def __init__(self, next): 242 | self.w = vanilla.FloatingWindow( (windowWidth, 100), "Model Downloader") 243 | self.w.text_anchorL = vanilla.TextBox( (10, 10, windowWidth - 10, 15), "Downloading latest kerning model", "center") 244 | self.w.progressBar = vanilla.ProgressBar( (10, windowHeight-30, windowWidth-20, 10)) 245 | self.next = next 246 | 247 | def go(self): 248 | self.w.open() 249 | with open(filename, 'wb') as f: 250 | response = requests.get(url, stream=True) 251 | total = response.headers.get('content-length') 252 | 253 | if total is None: 254 | f.write(response.content) 255 | else: 256 | downloaded = 0 257 | total = int(total) 258 | for data in response.iter_content(chunk_size=max(int(total/1000), 1024*1024)): 259 | downloaded += len(data) 260 | f.write(data) 261 | done = 100*downloaded/total 262 | self.w.progressBar.set( done ) 263 | if done < total: 264 | self.w.text_anchorL.set("Download failed :(") 265 | os.remove(filename) 266 | else: 267 | self.w.close() 268 | self.next.go() 269 | 270 | def getMargins(layer, y): 271 | startPoint = NSMakePoint(NSMinX(layer.bounds), y) 272 | endPoint = NSMakePoint(NSMaxX(layer.bounds), y) 273 | 274 | result = layer.calculateIntersectionsStartPoint_endPoint_(startPoint, endPoint) 275 | count = len(result) 276 | if (count <= 2): 277 | return (None, None) 278 | 279 | left = 1 280 | right = count - 2 281 | return (result[left].pointValue().x, result[right].pointValue().x) 282 | 283 | # a list of margins 284 | 285 | def marginList(layer, steps): 286 | listL, listR = [], [] 287 | # works over glyph copy 288 | cleanLayer = layer.copyDecomposedLayer() 289 | for y in reversed(steps): 290 | lpos, rpos = getMargins(cleanLayer, y) 291 | if lpos is not None: 292 | listL.append(lpos) 293 | else: 294 | listL.append(0) 295 | if rpos is not None: 296 | listR.append(layer.width-rpos) 297 | else: 298 | listR.append(0) 299 | 300 | return listL, listR 301 | 302 | class ClusterKernWindow( object ): 303 | def __init__( self ): 304 | try: 305 | self.w = vanilla.FloatingWindow( (windowWidth, windowHeight), "Cluster Kern Groups") 306 | instructions = "To prepare for kerning, we need to automatically set the kern groups. First we will group your glyphs by their left and right edges. Adjust the sliders to change the grouping tolerance. When the groups look reasonable, press 'Proceed'. Alternatively, you can load your existing kern groups." 307 | self.w.instr = vanilla.TextBox( (10, 10, windowWidth - 10, 45), instructions, "left", sizeStyle='small') 308 | 309 | self.w.text_anchorL = vanilla.TextBox( (10, 50, windowWidth/2 - 10, 15), "Left Tolerance", "center", sizeStyle='small') 310 | self.w.lSlider = vanilla.Slider( (10, 60,-(10+windowWidth/2),15),0.01,500, callback = self.buttonPressed) 311 | self.w.text_anchorR = vanilla.TextBox( (windowWidth/2, 50, windowWidth/2 - 10, 15), "Right Tolerance", "center", sizeStyle='small') 312 | self.w.rSlider = vanilla.Slider( (10+windowWidth/2,60,-10,15), 0.01, 500, callback = self.buttonPressed) 313 | self.w.useMine = vanilla.Button( (windowWidth/2 -200, -50, 100,20), "Just Use Mine", callback = self.useMine) 314 | self.w.calculate = vanilla.Button( (windowWidth/2 -50, -50, 100,20), "Calculate", callback = self.buttonPressed) 315 | 316 | self.w.proceed = vanilla.Button( (-120, -50, 100,20), "Proceed", callback = self.checkModelAndApply) 317 | self.w.proceed.enable(False) 318 | self.w.progressBar = vanilla.ProgressBar( (10, windowHeight-20, windowWidth-20, 10)) 319 | self.w.progressBar.set(0) 320 | self.w.resultsL = vanilla.TextEditor( (10,80, windowWidth/2 - 10, -60) ) 321 | self.w.resultsR = vanilla.TextEditor( (windowWidth/2,80, windowWidth/2 - 10, -60) ) 322 | self.w.open() 323 | self.lefts = [] 324 | self.rights = [] 325 | self.loadSidebearings() 326 | self.mineUsed = False 327 | except Exception, e: 328 | print(e) 329 | 330 | def loadSidebearings(self): 331 | if len(self.lefts) == 0: 332 | if not Glyphs.font.selectedLayers: 333 | raise "Oops, you need to select a layer!" 334 | 335 | glyphcount = len(Glyphs.font.glyphs) 336 | masterID = Glyphs.font.selectedLayers[0].associatedMasterId 337 | master = Glyphs.font.masters[masterID] 338 | minY = master.descender 339 | maxY = master.capHeight 340 | allSteps = list(range(int(minY),int(maxY),1)) 341 | steps = [] 342 | for i in range(samples): 343 | steps.append(allSteps[int(math.floor(i*len(allSteps) / samples))]) 344 | c = 0 345 | self.w.progressBar.set(0) 346 | lefts = [] 347 | rights = [] 348 | self.sideBearings = {} 349 | self.glyphOrder = [] 350 | glyphSet = Glyphs.font.glyphs 351 | for a in glyphSet: 352 | if len(testLetters) > 0: 353 | if not a.name in testLetters: 354 | continue 355 | self.glyphOrder.append(a) 356 | l = a.layers[masterID] 357 | c = c + 1 358 | listL, listR = marginList(l, steps) 359 | self.lefts.append(listL) 360 | self.sideBearings[a.name] = { "left": listL, "right": listR } 361 | self.rights.append(listR) 362 | self.w.progressBar.set( 100 * c / float(len(glyphSet)) ) 363 | 364 | def buttonPressed(self, sender): 365 | db1 = DBSCAN(eps=self.w.lSlider.get(), min_samples=1).fit(self.lefts) 366 | db2 = DBSCAN(eps=self.w.rSlider.get(), min_samples=1).fit(self.rights) 367 | 368 | labels = db1.labels_ 369 | groups = [] 370 | for i in range(0,len(set(labels))): 371 | groups.append([]) 372 | 373 | for i in range(0,len(labels)): 374 | groups[labels[i]].append(self.glyphOrder[i].name) 375 | 376 | lText = "" 377 | for g in groups: 378 | if len(g) > 1: 379 | lText += ", ".join(g) + "\n\n" 380 | g.append(g[0]) 381 | lText += "Total groups and ungrouped characters: %i" % len(groups) 382 | self.lgroups = groups 383 | self.w.resultsL.set(lText) 384 | 385 | labels = db2.labels_ 386 | groups = [] 387 | for i in range(0,len(set(labels))): 388 | groups.append([]) 389 | 390 | for i in range(0,len(labels)): 391 | groups[labels[i]].append(self.glyphOrder[i].name) 392 | 393 | rText = "" 394 | for g in groups: 395 | if len(g) > 1: 396 | rText += ", ".join(g) + "\n\n" 397 | g.append(g[0]) 398 | 399 | rText += "Total groups and ungrouped characters: %i" % len(groups) 400 | self.w.resultsR.set(rText) 401 | self.mineUsed = False 402 | self.w.proceed.enable(True) 403 | self.rgroups = groups 404 | 405 | # print(self.lgroups) 406 | # print(self.rgroups) 407 | 408 | def useMine(self, sender): 409 | glyphSet = Glyphs.font.glyphs 410 | lgroupsHash = {} 411 | rgroupsHash = {} 412 | for a in glyphSet: 413 | lk = a.rightKerningKey # *Right* side of glyph when it is on the left of the pair.... 414 | rk = a.leftKerningKey # *Left* side of glyph when it is on the right of the pair.... 415 | if not lk in lgroupsHash: 416 | lgroupsHash[lk] = [] 417 | if not rk in rgroupsHash: 418 | rgroupsHash[rk] = [] 419 | lgroupsHash[lk].append(a.name) 420 | rgroupsHash[rk].append(a.name) 421 | lText = "" 422 | rText = "" 423 | self.lgroups = [] 424 | self.rgroups = [] 425 | for k,v in lgroupsHash.items(): 426 | if len(v) > 1: 427 | lText += ", ".join(v) + "\n\n" 428 | v.append(k) 429 | self.lgroups.append(v) 430 | for k,v in rgroupsHash.items(): 431 | if len(v) > 1: 432 | rText += ", ".join(v) + "\n\n" 433 | v.append(k) 434 | self.rgroups.append(v) 435 | lText += "Total groups and ungrouped characters: %i" % len(self.lgroups) 436 | rText += "Total groups and ungrouped characters: %i" % len(self.rgroups) 437 | self.w.resultsR.set(rText) 438 | self.w.resultsL.set(lText) 439 | # print(self.lgroups) 440 | # print(self.rgroups) 441 | self.mineUsed = True 442 | self.w.proceed.enable(True) 443 | 444 | def checkModelAndApply(self, sender): 445 | print("Closing window") 446 | self.w.close() 447 | if not self.mineUsed: 448 | # Now we apply the groups given 449 | for l in self.lgroups: 450 | glyphs = l[:-1] 451 | key = l[-1] 452 | for g in glyphs: 453 | Glyphs.font.glyphs[g].leftKerningGroup = key 454 | for r in self.rgroups: 455 | glyphs = r[:-1] 456 | key = r[-1] 457 | for g in glyphs: 458 | Glyphs.font.glyphs[g].rightKerningGroup = key 459 | 460 | autokern = Autokern(self.lgroups, self.rgroups, self.sideBearings) 461 | if not os.path.isfile(filename): 462 | todo = ModelDownloader(next = autokern) 463 | else: 464 | todo = autokern 465 | todo.go() 466 | 467 | 468 | if len(failed)>0: 469 | w = vanilla.FloatingWindow( (windowWidth, windowHeight), "Install Required Modules") 470 | w.text_anchorL = vanilla.TextBox( (10, 10, windowWidth - 10, 15), "The following Python modules need to be installed before this script can run:", "center", sizeStyle='small') 471 | w.text_anchorR = vanilla.TextBox( (10, 30, windowWidth - 10, 15), ", ".join(failed)) 472 | w.instructions = vanilla.TextBox( (10, 50, - 10, -15), "") 473 | instructions = [] 474 | if not os.path.exists(extralibs): 475 | instructions.append("Create the directory "+extralibs) 476 | instructions.append("pip install numpy keras tensorflow np_utils -t '"+extralibs+"'") 477 | w.instructions.set("\n".join(instructions)) 478 | def closeW(_): 479 | w.close() 480 | w.button = vanilla.Button( (windowWidth/2 - 50, -50, 100,20), "OK", callback = closeW) 481 | w.open() 482 | else: 483 | ClusterKernWindow() 484 | -------------------------------------------------------------------------------- /Cadence Grid.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Cadence grid 2 | # -*- coding: utf-8 -*- 3 | __doc__=""" 4 | Add regular guidelines according to LeMo cadencing method. 5 | """ 6 | 7 | import vanilla 8 | windowHeight = 70 9 | from Foundation import NSPoint 10 | 11 | class CadenceGrid( object ): 12 | def __init__( self ): 13 | try: 14 | self.w = vanilla.FloatingWindow( (200, windowHeight), "Cadence grid") 15 | self.w.text_anchor = vanilla.TextBox( (15, 12, 130, 17), "Stem width", sizeStyle='small') 16 | self.w.stemWidth = vanilla.EditText( (100, 10, 50, 20), "70", sizeStyle = 'small') 17 | self.w.go = vanilla.Button((-80, -32, -15, 17), "Cadence", sizeStyle='small', callback=self.addGrid) 18 | self.w.setDefaultButton( self.w.go ) 19 | self.w.open() 20 | except Exception, e: 21 | print(e) 22 | 23 | def addGrid(self,sender): 24 | try: 25 | stem = self.w.stemWidth.get() 26 | cadence = float(stem) / 5 # Assuming I understand LeMo correctly... 27 | self.w.close() 28 | Font = Glyphs.font 29 | FontMaster = Font.selectedFontMaster 30 | x = 0 31 | while FontMaster.guides: 32 | del(FontMaster.guides[0]) 33 | 34 | while x < 1000: 35 | try: # GLYPHS 3 36 | g = GSGuide() 37 | except: 38 | g = GSGuideLine() 39 | g.position = NSPoint(x,0) 40 | g.angle = 90.0 41 | FontMaster.guides.append(g) 42 | x = x + cadence 43 | 44 | except Exception, e: 45 | print(e) 46 | 47 | CadenceGrid() -------------------------------------------------------------------------------- /Close But No Cigar.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Close, but no cigar... 2 | # -*- coding: utf-8 -*- 3 | __doc__=""" 4 | Report on angles, widths and positions which are nearly, but not quite, right 5 | """ 6 | 7 | within = 0.05 # 5% of target 8 | angleTolerance = 2 9 | # Check stems and angles 10 | 11 | Glyphs.clearLog() 12 | 13 | from glyphmonkey import GSLineSegment 14 | 15 | def closeButNotRelative(l1, l2, tolerance): 16 | return l1 != l2 and (l2 * (1-tolerance) < l1 and l1 < l2 * (1+tolerance)) 17 | 18 | def closeButNotAbsolute(l1, l2, tolerance): 19 | return l1 != l2 and (l2-tolerance) < l1 and l1 < l2+tolerance 20 | 21 | def checkHorizontal(layer, s, h): 22 | thisLen = s.length 23 | thisAng = s.angle 24 | for stem in h: 25 | if thisLen == stem: return 26 | 27 | if (-angleTolerance < thisAng and thisAng < angleTolerance) or (180-angleTolerance < thisAng and thisAng < 180+angleTolerance): 28 | for stem in h: 29 | if closeButNotRelative(thisLen, stem, within): 30 | print("\nSegment %s in %s, has length %s, should be horizontal stem length %s?" % (s, layer, thisLen, stem)) 31 | 32 | def checkVertical(layer, s,v): 33 | thisAng = s.angle 34 | thisLen = s.length 35 | for stem in v: 36 | if thisLen == stem: return 37 | 38 | if (90-angleTolerance < thisAng and thisAng < 90+angleTolerance) or (-90-angleTolerance < thisAng and thisAng < -90+angleTolerance): 39 | for stem in v: 40 | if closeButNotRelative(thisLen, stem, within): 41 | print("\nSegment %s in %s, has length %s, should be vertical stem length %s?" % (s, layer, thisLen, stem)) 42 | 43 | def checkAngle(layer, s): 44 | thisAng = s.angle 45 | if (thisAng != 0 and (-angleTolerance < thisAng and thisAng < angleTolerance)): 46 | print("\nNearly-perpendicular %s in %s, angle was %s, should be horizonal?" % (s, layer, thisAng)) 47 | 48 | if (thisAng != 180 and (180-angleTolerance < thisAng and thisAng < 180+angleTolerance)): 49 | print("\nNearly-perpendicular %s in %s, angle was %s, should be horizontal?" % (s, layer, thisAng)) 50 | 51 | if (thisAng != 90 and (90-angleTolerance < thisAng and thisAng < 90+angleTolerance)): 52 | print("\nNearly-perpendicular %s in %s, angle was %s, should be vertical?" % (s, layer, thisAng)) 53 | 54 | if (thisAng != -90 and (-90-angleTolerance < thisAng and thisAng < -90+angleTolerance)): 55 | print("\nNearly-perpendicular %s in %s, angle was %s, should be vertical?" % (s, layer, thisAng)) 56 | 57 | def checkNodePosition(layer, node, m): 58 | px, py = node.position.x, node.position.y 59 | if closeButNotAbsolute(px,0,2): 60 | print("\nX co-ordinate of %s in %s nearly (but not quite) zero" % (node, layer)) 61 | if closeButNotAbsolute(py,0,2): 62 | print("\nY co-ordinate of %s in %s nearly (but not quite) zero" % (node, layer)) 63 | 64 | if closeButNotAbsolute(py,m.ascender,2): 65 | print("\nY co-ordinate of %s in %s nearly (but not quite) ascender" % (node, layer)) 66 | if closeButNotAbsolute(py,m.capHeight,2): 67 | print("\nY co-ordinate of %s in %s nearly (but not quite) cap height" % (node, layer)) 68 | if closeButNotAbsolute(py,m.xHeight,2): 69 | print("\nY co-ordinate of %s in %s nearly (but not quite) x height" % (node, layer)) 70 | if closeButNotAbsolute(py,m.descender,2): 71 | print("\nY co-ordinate of %s in %s nearly (but not quite) descender" % (node, layer)) 72 | 73 | for l1 in Glyphs.font.selectedLayers: 74 | glyph = l1.parent 75 | l = glyph.layers[0] 76 | m = Glyphs.font.masters[l.associatedMasterId] 77 | try: # GLYPHS3 78 | stems = m.stems 79 | h = [x.value() for x in filter(lambda x:x.metric().horizontal(), stems)] 80 | v = [x.value() for x in filter(lambda x:not x.metric().horizontal(), stems)] 81 | except Exception as e: 82 | v = m.verticalStems 83 | h = m.horizontalStems 84 | for p in l.paths: 85 | for s in p.segments: 86 | thisAng = s.angle 87 | checkAngle(l, s) 88 | if h and len(h) > 0: 89 | checkHorizontal(l, s, h) 90 | if v and len(v) > 0: 91 | checkVertical(l, s, v) 92 | for n in p.nodes: 93 | checkNodePosition(l,n,m) 94 | -------------------------------------------------------------------------------- /Comb.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Comb 2 | # -*- coding: utf-8 -*- 3 | __doc__=""" 4 | Drag a comb through a path 5 | """ 6 | from math import cos, sin 7 | from glyphmonkey import * 8 | 9 | pathset = [] 10 | for a in Glyphs.font.selectedLayers[0].paths: 11 | 12 | # Find the two smallest "ends" 13 | l1, s1, l2, s2 = None, None, None, None 14 | 15 | for i in range(0,len(a.segments)): 16 | s = a.segments[i] 17 | if type(s) is GSLineSegment and (not l1 or s.length < l1): 18 | s1 = i 19 | l1 = s.length 20 | 21 | for i in range(0,len(a.segments)): 22 | s = a.segments[i] 23 | if type(s) is GSLineSegment and (s.length >= l1 and (not l2 or s.length < l2) and i != s1): 24 | s2 = i 25 | l2 = s.length 26 | 27 | if s1 > s2: s1, s2 = s2, s1 28 | print("Identified path end segments:") 29 | print(a.segments[s1], a.segments[s2]) 30 | # Find two edges between segments 31 | edge1 = [ a.segments[i] for i in range(s1+1, s2) ] 32 | edge2 = [ a.segments[i] for i in range(s2+1, len(a.segments))] 33 | edge2.extend([a.segments[i] for i in range(0, s1)]) 34 | for i in range(0, len(edge2)): edge2[i].reverse() 35 | edge2.reverse() 36 | print("\nIdentified edges") 37 | print("Edge 1:", edge1) 38 | print("Edge 2:", edge2) 39 | if len(edge1) != len(edge2): 40 | print("Edges not compatible - differing number of points") 41 | raise TypeError 42 | stripes = [ [0, 0.05], [0.1,0.15], [0.2,0.3], [0.35,0.6], [0.65,0.75],[0.8,0.85],[0.95, 1] ] 43 | for i in stripes: 44 | start, end = i[0],i[1] 45 | 46 | segs1 = [] 47 | segs2 = [] 48 | for i in range(0, len(edge1)): 49 | segs1.append(edge1[i].interpolate(edge2[i],start)) 50 | segs2.append(edge1[i].interpolate(edge2[i],end)) 51 | for i in range(0, len(segs2)): segs2[i].reverse() 52 | segs2.reverse() 53 | segs1.append(GSLineSegment(tuple = (segs1[-1]._seg[-1],segs2[0]._seg[0]))) 54 | segs1.extend(segs2) 55 | segs1.append(GSLineSegment(tuple = (segs2[-1]._seg[-1],segs1[0]._seg[0]))) 56 | 57 | segs = segs1 58 | 59 | path = GSPath() 60 | path.parent = a.parent 61 | path.segments = segs 62 | pathset.append(path) 63 | path.closed = True 64 | Glyphs.font.selectedLayers[0].paths = pathset -------------------------------------------------------------------------------- /Copy Myanmar Anchors.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Copy Myanmar Anchors 2 | # -*- coding: utf-8 -*- 3 | import GlyphsApp 4 | 5 | wa = Glyphs.font.glyphs["ဝ"] 6 | ka = Glyphs.font.glyphs["က"] 7 | singlebowl = "ခဂငစဎဒဓပဖဗမဧဋဌဍ" 8 | doublebowl = "ဃဆညတထဘယလသဟအဢ" 9 | 10 | def copyanchors(g1, g2): 11 | for l1, l2 in zip(g1.layers, g2.layers): 12 | for anchor in l1.anchors: 13 | copied = GSAnchor(anchor.name, anchor.position) 14 | l2.anchors[anchor.name] = copied 15 | 16 | for g in singlebowl: 17 | if not Glyphs.font.glyphs[g]: 18 | continue 19 | copyanchors(wa, Glyphs.font.glyphs[g]) 20 | 21 | for g in doublebowl: 22 | if not Glyphs.font.glyphs[g]: 23 | continue 24 | copyanchors(ka, Glyphs.font.glyphs[g]) 25 | -------------------------------------------------------------------------------- /Curve All Straights.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Curve All Straights 2 | # -*- coding: utf-8 -*- 3 | __doc__=""" 4 | Turn corners into smooths (useful when tracing) 5 | """ 6 | import GlyphsApp 7 | from Foundation import NSPoint, NSValue 8 | 9 | def lerp(t,a,b): 10 | return NSValue.valueWithPoint_(NSPoint(int((1-t)*a.x + t*b.x), int((1-t)*a.y + t*b.y))) 11 | 12 | Layer = Glyphs.font.selectedLayers[0] 13 | testSelection = len(Layer.selection) > 0 14 | 15 | for p in Layer.paths: 16 | if testSelection and not p.selected: 17 | continue 18 | news = [] 19 | for idx,segment in enumerate(p.segments): 20 | if len(segment) == 4: 21 | news.append(segment) 22 | else: 23 | s,e = segment[0], segment[-1] 24 | news.append((s,lerp(0.33,s,e),lerp(0.66,s,e),e)) 25 | print(news) 26 | p.segments = news 27 | for i,n in enumerate(p.nodes): 28 | # n.type = GlyphsApp.GSCURVE 29 | n.connection = GlyphsApp.GSSMOOTH 30 | -------------------------------------------------------------------------------- /Delete Close Points.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Delete Close Points 2 | # -*- coding: utf-8 -*- 3 | __doc__=""" 4 | Delete points which are very close to (or on top of) other points 5 | """ 6 | from glyphmonkey import * 7 | 8 | arbitraryConstant = 5 9 | 10 | for p in Glyphs.font.selectedLayers[0].paths: 11 | p.segments = [ seg for seg in p.segments if seg.length > arbitraryConstant ] 12 | 13 | Glyphs.redraw() 14 | -------------------------------------------------------------------------------- /Interpolated Nudge.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Interpolated nudge 2 | # -*- coding: utf-8 -*- 3 | __doc__=""" 4 | Nudge points while keeping tension 5 | """ 6 | 7 | import vanilla 8 | windowHeight = 100 9 | windowWidth = 120 10 | 11 | class InterpolatedNudgeWindow( object ): 12 | def __init__( self ): 13 | try: 14 | self.w = vanilla.FloatingWindow( (windowWidth, windowHeight), "Interpolated Nudge") 15 | self.w.text_anchor = vanilla.TextBox( (15, 12, 130, 17), "Amount", sizeStyle='small') 16 | self.w.amount = vanilla.EditText( (65, 9, 50, 20), "10", sizeStyle = 'small') 17 | self.w.up = vanilla.Button(((windowWidth-20)/2, 20, 30, 40), "^", sizeStyle='small', callback=self.up) 18 | self.w.down = vanilla.Button(((windowWidth-20)/2, (windowHeight-40), 30, 40), "v", sizeStyle='small', callback=self.down) 19 | self.w.left = vanilla.Button((10, (windowHeight-20)/2, 30, 40), "<", sizeStyle='small', callback=self.left) 20 | self.w.right = vanilla.Button((-30, (windowHeight-20)/2, 30, 40), ">", sizeStyle='small', callback=self.right) 21 | self.w.open() 22 | self.w.amount.selectAll() 23 | except Exception, e: 24 | print(e) 25 | 26 | def nextOnCurve(self, n): 27 | n = n.nextNode 28 | while n.type == OFFCURVE: n = n.nextNode 29 | return n 30 | 31 | def prevOnCurve(self, n): 32 | n = n.prevNode 33 | while n.type == OFFCURVE: n = n.prevNode 34 | return n 35 | 36 | def adjust(self, handle, node, diffX, diffY, dx, dy): 37 | adjX, adjY = 0, 0 38 | if diffX != 0: 39 | adjX = ((node.position.x-handle.position.x)/diffX) * dx 40 | if diffY != 0: 41 | adjY = ((node.position.y-handle.position.y)/diffY) * dy 42 | 43 | if handle.type == OFFCURVE: 44 | handle.position = (handle.position.x + adjX, handle.position.y + adjY) 45 | 46 | def nudge(self, n, deltax, deltay): 47 | nn = self.nextOnCurve(n) 48 | pn = self.prevOnCurve(n) 49 | 50 | diffX, diffY = nn.position.x - n.position.x, nn.position.y - n.position.y 51 | if nn != n.nextNode: 52 | self.adjust(nn.prevNode, nn, diffX, diffY, deltax, deltay) 53 | self.adjust(nn.nextNode, nn, diffX, diffY, deltax, deltay) 54 | 55 | diffX, diffY = pn.position.x - n.position.x, pn.position.y - n.position.y 56 | if pn != n.prevNode: 57 | self.adjust(pn.prevNode, pn, diffX, diffY, deltax, deltay) 58 | self.adjust(pn.nextNode, pn, diffX, diffY, deltax, deltay) 59 | 60 | n.position = (n.position.x + deltax, n.position.y + deltay) 61 | if n.prevNode.type == OFFCURVE: 62 | n.prevNode.position = (n.prevNode.position.x + deltax, n.prevNode.position.y + deltay) 63 | if n.nextNode.type == OFFCURVE: 64 | n.nextNode.position = (n.nextNode.position.x + deltax, n.nextNode.position.y + deltay) 65 | 66 | def up(self,sender): 67 | try: 68 | val = float(self.w.amount.get()) 69 | self.doIt(0,val) 70 | except Exception, e: 71 | print(e) 72 | 73 | def down(self,sender): 74 | val = float(self.w.amount.get()) 75 | self.doIt(0,-val) 76 | 77 | def left(self,sender): 78 | val = float(self.w.amount.get()) 79 | self.doIt(-val,0) 80 | 81 | def right(self,sender): 82 | val = float(self.w.amount.get()) 83 | self.doIt(val,0) 84 | 85 | def doIt(self, dx, dy): 86 | for n in Glyphs.font.selectedLayers[0].selection: 87 | self.nudge(n,dx,dy) 88 | Glyphs.redraw() 89 | 90 | InterpolatedNudgeWindow() -------------------------------------------------------------------------------- /Kern Optimizer.py: -------------------------------------------------------------------------------- 1 | font = Glyphs.font 2 | LSBs = {} 3 | RSBs = {} 4 | lkgs = {} 5 | rkgs = {} 6 | glyphs = [x.name for x in font.glyphs] 7 | 8 | def _kp(l,r): 9 | v = font.kerningForPair(font.selectedFontMaster.id, l, r) 10 | if v > 1000: return 0 11 | return v 12 | 13 | def kp(l,r): 14 | lkg = lkgs[l] 15 | rkg = rkgs[l] 16 | if _kp(l,r): return _kp(l,r) 17 | if lkg and _kp(lkg, r): return _kp(lkg, r) 18 | if rkg and _kp(l, rkg): return _kp(l, rkg) 19 | if lkg and rkg and _kp(lkg, rkg): return _kp(lkg, rkg) 20 | return 0 21 | 22 | matrix = {} 23 | glyphset = [] 24 | for l in glyphs: 25 | # It's backwards 26 | lkgs[l] = font.glyphs[l].rightKerningGroup and "@MMK_L_"+font.glyphs[l].rightKerningGroup 27 | rkgs[l] = font.glyphs[l].leftKerningGroup and "@MMK_R_"+font.glyphs[l].leftKerningGroup 28 | 29 | matrix[l] = {} 30 | LSBs[l] = 0 # XX 31 | RSBs[l] = 0 # XX 32 | some = False 33 | 34 | for r in glyphs: 35 | matrix[l][r] = kp(l,r) 36 | if matrix[l][r]: some = True 37 | if some: 38 | glyphset.append(l) 39 | 40 | # For debugging 41 | def displayMatrix(): 42 | print(" [ ] "), 43 | for r in sorted(glyphset): 44 | print( " [ "+r+" ]"), 45 | print("") 46 | for l in sorted(glyphset): 47 | print( " [ "+l+" ]"), 48 | for r in sorted(glyphset): 49 | print("%6g" % matrix[l][r]), 50 | print(" ") 51 | print("Score: %g" % scoreMatrix()) 52 | 53 | def scoreMatrix(): 54 | score = 0 55 | for l in glyphset: 56 | for r in glyphset: 57 | score += matrix[l][r]**2*10 58 | return int(score) 59 | 60 | def pivotL(l, pivotValue): 61 | LSBs[l] = LSBs[l] + pivotValue 62 | for g in glyphset: 63 | matrix[l][g] -= pivotValue 64 | 65 | def pivotR(r, pivotValue): 66 | RSBs[r] = RSBs[r] + pivotValue 67 | for g in glyphset: 68 | matrix[g][r] -= pivotValue 69 | 70 | def tryOptimize(l,r): 71 | pivotValue = matrix[l][r] 72 | if pivotValue == 0: return False 73 | s1 = scoreMatrix() 74 | pivotL(l,pivotValue) 75 | if scoreMatrix() < s1: return True 76 | pivotL(l,-pivotValue) 77 | pivotR(r,pivotValue) 78 | if scoreMatrix() < s1: return True 79 | pivotR(r,-pivotValue) 80 | return False 81 | 82 | keepGoing = True 83 | 84 | print("Base score was: %g", scoreMatrix()) 85 | # print("OPTIMIZING...") 86 | # while keepGoing: 87 | # s = scoreMatrix() 88 | # # BRUTE FORCE FOR THE WIN! 89 | # for l in glyphset: 90 | # for r in glyphset: 91 | # tryOptimize(l,r) 92 | # if s == scoreMatrix(): 93 | # keepGoing = False 94 | # print("Final score was: %g", scoreMatrix()) 95 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Simon Cozens 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LightSpace.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: LightSpace 2 | # -*- coding: utf-8 -*- 3 | __doc__=""" 4 | Experimental spacer based on Hochuli's "light from above and below" 5 | """ 6 | 7 | layer = Glyphs.font.selectedLayers[0] # current layer 8 | layerId = layer.layerId 9 | 10 | def light(layer, spectralFactor): 11 | x = layer.bounds.origin.x 12 | end = layer.bounds.origin.x + layer.bounds.size.width 13 | delta = 5 14 | toptotal = 0 15 | bottomtotal =0 16 | # Lazy way to get ascender, descender, avoids grubbing in master objects 17 | bottom = layer.bounds.origin.y - layer.BSB 18 | top = layer.bounds.origin.y + layer.bounds.size.height + layer.TSB 19 | 20 | while x < end: 21 | startPoint = NSMakePoint(x, bottom) 22 | endPoint = NSMakePoint(x, top) 23 | result = layer.calculateIntersectionsStartPoint_endPoint_(startPoint, endPoint) 24 | if (len(result) > 2): 25 | t = result[1] 26 | b = result[-2] 27 | toptotal += top-t.y * delta 28 | bottomtotal += b.y-bottom * delta 29 | x += delta 30 | return toptotal*spectralFactor + bottomtotal, toptotal, bottomtotal 31 | 32 | def sideLight(layer): 33 | bottom = layer.bounds.origin.y - layer.BSB 34 | top = layer.bounds.origin.y + layer.bounds.size.height + layer.TSB 35 | return (top-bottom) * (layer.RSB + layer.LSB) 36 | 37 | # Find a spectralFactor which equalizes u and n 38 | ewe = Glyphs.font.glyphs['u'].layers[layerId] 39 | enn = Glyphs.font.glyphs['n'].layers[layerId] 40 | _,uTop,uBottom = light(ewe,1) 41 | _,nTop,nBottom = light(enn,1) 42 | num = sideLight(ewe) - sideLight(enn) + uBottom - nBottom 43 | denom = sideLight(enn) - sideLight(ewe) + nTop - uTop 44 | spectralFactor = num / denom 45 | print(spectralFactor) 46 | 47 | goal = (light(enn,spectralFactor)[0] + sideLight(enn)) # / (enn.LSB+enn.RSB + enn.bounds.size.width) 48 | 49 | 50 | # I can't be bothered to do the math. Brute force it. 51 | iterations = 0 52 | layer.beginChanges() 53 | balance = layer.RSB / layer.LSB 54 | totalLight = light(layer,spectralFactor)[0] 55 | while True: 56 | if iterations > 100: break 57 | iterations = iterations + 1 58 | current = (totalLight + sideLight(layer)) # / (layer.LSB+layer.RSB + layer.bounds.size.width) 59 | print(current,goal) 60 | if abs(current - goal) < 1.0: break 61 | factor = current / goal 62 | if current < goal: 63 | layer.LSB = layer.LSB + factor 64 | layer.RSB = layer.LSB * balance 65 | if current > goal: 66 | layer.LSB = layer.LSB - factor 67 | layer.RSB = layer.LSB * balance 68 | 69 | layer.endChanges() -------------------------------------------------------------------------------- /Make Compatible.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Make compatible 2 | # -*- coding: utf-8 -*- 3 | 4 | layers = Glyphs.font.selectedLayers[0].glyph().layers 5 | paths = [l.paths for l in layers] 6 | 7 | 8 | def lerp(p1, p2, t): 9 | return NSMakePoint(p1.x + t * (p2.x - p1.x), p1.y + t * (p2.y - p1.y)) 10 | 11 | 12 | for per_paths in zip(*paths): 13 | segs = [p.segments for p in per_paths] 14 | to_fix = [] 15 | for seg_set in zip(*segs): 16 | lengths = [len(seg) for seg in seg_set] 17 | if all(l == lengths[0] for l in lengths): 18 | continue 19 | # Find the ones which are lines 20 | for ix, seg in enumerate(seg_set): 21 | if len(seg) == 2: 22 | fix_object = {"master": ix, "line": [seg[0], seg[1]]} 23 | # Locate this line by point index 24 | path_points = per_paths[ix].nodes 25 | for pt_ix, pt in enumerate(path_points): 26 | next_pt = path_points[(pt_ix + 1) % len(path_points)] 27 | if ( 28 | pt.position.x == seg[0].x 29 | and pt.position.y == seg[0].y 30 | and next_pt.position.x == seg[1].x 31 | and next_pt.position.y == seg[1].y 32 | ): 33 | fix_object["point_index"] = pt_ix 34 | to_fix.append(fix_object) 35 | 36 | # Sort by line and index, highest index first 37 | for line in sorted( 38 | to_fix, key=lambda fix_obj: (fix_obj["master"], -fix_obj["point_index"]) 39 | ): 40 | nodes = per_paths[line["master"]].nodes 41 | start_of_line = nodes[line["point_index"]] 42 | end_of_line = nodes[line["point_index"] + 1 % len(nodes)] 43 | new_offcurve_1 = GSNode(lerp(start_of_line, end_of_line, 1 / 3.0), GSOFFCURVE) 44 | new_offcurve_2 = GSNode(lerp(start_of_line, end_of_line, 2 / 3.0), GSOFFCURVE) 45 | nodelist = list(nodes) 46 | nodelist[line["point_index"] + 1 : line["point_index"] + 1] = [ 47 | new_offcurve_1, 48 | new_offcurve_2, 49 | ] 50 | per_paths[line["master"]].nodes = nodelist 51 | end_of_line.type = GSCURVE 52 | -------------------------------------------------------------------------------- /Make bottom-left node first.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Make bottom left node first 2 | # -*- coding: utf-8 -*- 3 | __doc__=""" 4 | Makes the bottom left node in each path the first node in all masters 5 | """ 6 | 7 | def left(x): 8 | return x.position.x 9 | def bottom(x): 10 | return x.position.y 11 | 12 | 13 | layers = Glyphs.font.selectedLayers 14 | for aLayer in layers: 15 | for idx, thisLayer in enumerate(aLayer.parent.layers): 16 | for p in thisLayer.paths: 17 | oncurves = filter(lambda n: n.type != "offcurve", list(p.nodes)) 18 | n = sorted(sorted(oncurves, key = bottom),key=left)[0] 19 | n.makeNodeFirst() -------------------------------------------------------------------------------- /Myanmar Medial Ra Maker.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Myanmar Medial Ra Maker 2 | # -*- coding: utf-8 -*- 3 | import GlyphsApp 4 | import numpy as np 5 | import re 6 | 7 | 8 | number_of_glyphs = 5 9 | 10 | def mean(it): 11 | return sum(it)/len(it) 12 | 13 | def roundto(x,y): 14 | return int(x/y)*y 15 | 16 | def ssq(j, i, sum_x, sum_x_sq): 17 | if (j > 0): 18 | muji = (sum_x[i] - sum_x[j-1]) / (i - j + 1) 19 | sji = sum_x_sq[i] - sum_x_sq[j-1] - (i - j + 1) * muji ** 2 20 | else: 21 | sji = sum_x_sq[i] - sum_x[i] ** 2 / (i+1) 22 | 23 | return 0 if sji < 0 else sji 24 | 25 | def fill_row_k(imin, imax, k, S, J, sum_x, sum_x_sq, N): 26 | if imin > imax: return 27 | 28 | i = (imin+imax) // 2 29 | S[k][i] = S[k-1][i-1] 30 | J[k][i] = i 31 | 32 | jlow = k 33 | 34 | if imin > k: 35 | jlow = int(max(jlow, J[k][imin-1])) 36 | jlow = int(max(jlow, J[k-1][i])) 37 | 38 | jhigh = i-1 39 | if imax < N-1: 40 | jhigh = int(min(jhigh, J[k][imax+1])) 41 | 42 | for j in range(jhigh, jlow-1, -1): 43 | sji = ssq(j, i, sum_x, sum_x_sq) 44 | 45 | if sji + S[k-1][jlow-1] >= S[k][i]: break 46 | 47 | # Examine the lower bound of the cluster border 48 | # compute s(jlow, i) 49 | sjlowi = ssq(jlow, i, sum_x, sum_x_sq) 50 | 51 | SSQ_jlow = sjlowi + S[k-1][jlow-1] 52 | 53 | if SSQ_jlow < S[k][i]: 54 | S[k][i] = SSQ_jlow 55 | J[k][i] = jlow 56 | 57 | jlow += 1 58 | 59 | SSQ_j = sji + S[k-1][j-1] 60 | if SSQ_j < S[k][i]: 61 | S[k][i] = SSQ_j 62 | J[k][i] = j 63 | 64 | fill_row_k(imin, i-1, k, S, J, sum_x, sum_x_sq, N) 65 | fill_row_k(i+1, imax, k, S, J, sum_x, sum_x_sq, N) 66 | 67 | def fill_dp_matrix(data, S, J, K, N): 68 | sum_x = np.zeros(N, dtype=np.float_) 69 | sum_x_sq = np.zeros(N, dtype=np.float_) 70 | 71 | # median. used to shift the values of x to improve numerical stability 72 | shift = data[N//2] 73 | 74 | for i in range(N): 75 | if i == 0: 76 | sum_x[0] = data[0] - shift 77 | sum_x_sq[0] = (data[0] - shift) ** 2 78 | else: 79 | sum_x[i] = sum_x[i-1] + data[i] - shift 80 | sum_x_sq[i] = sum_x_sq[i-1] + (data[i] - shift) ** 2 81 | 82 | S[0][i] = ssq(0, i, sum_x, sum_x_sq) 83 | J[0][i] = 0 84 | 85 | for k in range(1, K): 86 | if (k < K-1): 87 | imin = max(1, k) 88 | else: 89 | imin = N-1 90 | 91 | fill_row_k(imin, N-1, k, S, J, sum_x, sum_x_sq, N) 92 | 93 | def ckmeans(data, n_clusters): 94 | if n_clusters <= 0: 95 | raise ValueError("Cannot classify into 0 or less clusters") 96 | if n_clusters > len(data): 97 | raise ValueError("Cannot generate more classes than there are data values") 98 | 99 | # if there's only one value, return it; there's no sensible way to split 100 | # it. This means that len(ckmeans([data], 2)) may not == 2. Is that OK? 101 | unique = len(set(data)) 102 | if unique == 1: 103 | return [data] 104 | 105 | data.sort() 106 | n = len(data) 107 | 108 | S = np.zeros((n_clusters, n), dtype=np.float_) 109 | 110 | J = np.zeros((n_clusters, n), dtype=np.uint64) 111 | 112 | fill_dp_matrix(data, S, J, n_clusters, n) 113 | 114 | clusters = [] 115 | cluster_right = n-1 116 | 117 | for cluster in range(n_clusters-1, -1, -1): 118 | cluster_left = int(J[cluster][cluster_right]) 119 | clusters.append(data[cluster_left:cluster_right+1]) 120 | 121 | if cluster > 0: 122 | cluster_right = cluster_left - 1 123 | 124 | return list(reversed(clusters)) 125 | 126 | def stem_width(path): 127 | segs = [s for s in path.segments if len(s) == 2 and s[0].x == s[1].x] 128 | segs = sorted(segs, key=lambda s: abs(s[0].y-s[1].y)) 129 | return abs(segs[-1][0].x - segs[-2][0].x) 130 | 131 | ra = Glyphs.font.glyphs["103C"].layers[0] 132 | if len(ra.paths) != 5: 133 | print("Medial ra glyph needs five paths") 134 | 135 | stem, top, upper_hook, bottom, lower_hook = ra.paths 136 | ra_enclosure = ra.bounds.size.width - (stem_width(stem) + stem_width(lower_hook)) + 2 * ra.LSB 137 | 138 | widths = {} 139 | 140 | def copypath(path, translate=0, stretchleft=0, stretchright=0): 141 | newpath = GSPath() 142 | for n in path.nodes: 143 | newnode = GSNode(NSMakePoint(n.position.x+translate,n.position.y), n.type) 144 | newnode.smooth = n.smooth 145 | newpath.nodes.append(newnode) 146 | newpath.closed = True 147 | if stretchleft: 148 | nodes = newpath.nodes 149 | nodes[0].position = NSMakePoint(nodes[0].position.x - stretchleft, nodes[0].position.y) 150 | nodes[-1].position = NSMakePoint(nodes[-1].position.x - stretchleft, nodes[-1].position.y) 151 | if stretchright: 152 | nodes = newpath.nodes 153 | nodes[0].position = NSMakePoint(nodes[0].position.x + stretchright, nodes[0].position.y) 154 | nodes[-1].position = NSMakePoint(nodes[-1].position.x + stretchright, nodes[-1].position.y) 155 | return newpath 156 | 157 | print(ra_enclosure) 158 | widths = [] 159 | for i in range(0x1000, 0x1021): # Yes I know there are more 160 | if not Glyphs.font.glyphs["%04X" % i]: 161 | continue 162 | widths.append( Glyphs.font.glyphs["%04X" % i].layers[0].width ) 163 | 164 | target_widths = [roundto(mean(x),10) for x in ckmeans(widths, number_of_glyphs)] 165 | 166 | def make_a_ra(basename, width): 167 | g = Glyphs.font.glyphs[basename] 168 | if not g: 169 | g = GSGlyph(basename) 170 | newLayer = GSLayer() 171 | newLayer.associatedMasterId = Glyphs.font.masters[-1].id 172 | Glyphs.font.glyphs[basename] = g 173 | g.layers.append(newLayer) 174 | l = g.layers[0] 175 | extension = width-ra_enclosure 176 | l.paths.append(stem) 177 | if not (re.search(r"\b.notop\b",basename)): 178 | l.paths.append(copypath(top, stretchright=extension/2)) 179 | if not (re.search(r"\b.notophook\b",basename) or re.search(r"\b.notop\b",basename)): 180 | l.paths.append(copypath(upper_hook, stretchleft=extension/2,translate=extension)) 181 | if not (re.search(r"\b.nobottom\b",basename)): 182 | l.paths.append(copypath(bottom, stretchright=extension/2)) 183 | if not (re.search(r"\b.nobottomhook\b",basename) or re.search(r"\b.nobottom\b",basename)): 184 | l.paths.append(copypath(lower_hook, stretchleft=extension/2,translate=extension)) 185 | l.width = ra.width 186 | l.LSB = ra.LSB 187 | 188 | for width in target_widths: 189 | if width <= ra_enclosure: 190 | continue 191 | print("Making a ra of width: %i" % width) 192 | basename = "medial-ra.w%i" % width 193 | make_a_ra(basename, width) 194 | make_a_ra(basename+".notop", width) 195 | make_a_ra(basename+".notophook", width) 196 | make_a_ra(basename+".nobottom", width) 197 | make_a_ra(basename+".nobottomhook", width) 198 | make_a_ra(basename+".notop.nobottom", width) 199 | make_a_ra(basename+".notophook.nobottom", width) 200 | make_a_ra(basename+".notop.nobottomhook", width) 201 | make_a_ra(basename+".notophook.nobottomhook", width) 202 | 203 | make_a_ra("medial-ra.notop", ra_enclosure) 204 | make_a_ra("medial-ra.notophook", ra_enclosure) 205 | make_a_ra("medial-ra.nobottom", ra_enclosure) 206 | make_a_ra("medial-ra.nobottomhook", ra_enclosure) 207 | make_a_ra("medial-ra.notop.nobottom", width) 208 | make_a_ra("medial-ra.notophook.nobottom", width) 209 | make_a_ra("medial-ra.notop.nobottomhook", width) 210 | make_a_ra("medial-ra.notophook.nobottomhook", width) 211 | 212 | -------------------------------------------------------------------------------- /Nastaliq Connection Editor.py: -------------------------------------------------------------------------------- 1 | # MenuTitle: Nastaliq Connection Editor 2 | # -*- coding: utf-8 -*- 3 | __doc__ = """ 4 | Edit Nastaliq connections in a font that conforms to Qalmi glyph naming 5 | convention 6 | """ 7 | import sys 8 | from AppKit import NSObject 9 | import vanilla 10 | import csv 11 | from io import StringIO 12 | import re 13 | from AppKit import NSView, NSColor, NSRectFill, NSBezierPath, NSAffineTransform 14 | from vanilla.vanillaBase import VanillaBaseObject, VanillaCallbackWrapper 15 | import traceback 16 | 17 | 18 | def glyphsort(x): 19 | x = re.sub(r"(\D)([0-9])$", r"\g<1>0\2", x) 20 | x = re.sub(r"^GAF", "KAF", x) 21 | x = re.sub(r"^TE", "BE", x) 22 | return x 23 | 24 | 25 | if "GlyphView" not in locals(): 26 | 27 | class GlyphView(NSView): 28 | @objc.python_method 29 | def setGlyphs(self, glyphs): 30 | self.glyphs = glyphs 31 | self.setNeedsDisplay_(True) 32 | 33 | @objc.python_method 34 | def setMaster(self, master_id): 35 | self.master = master_id 36 | self.setNeedsDisplay_(True) 37 | 38 | def drawRect_(self, rect): 39 | try: 40 | NSColor.whiteColor().set() 41 | NSRectFill(self.bounds()) 42 | NSColor.blackColor().setFill() 43 | p = NSBezierPath.bezierPath() 44 | xcursor = 0 45 | ycursor = 0 46 | for i, g in enumerate(self.glyphs): 47 | layer = g.layers[self.master] 48 | if i > 0: 49 | # Do anchor correction here 50 | prevlayer = self.glyphs[i - 1].layers[self.master] 51 | entry = prevlayer.anchors["entry"] 52 | exit = layer.anchors["exit"] 53 | if entry and exit: 54 | diffX = entry.position.x - exit.position.x 55 | diffY = entry.position.y - exit.position.y 56 | xcursor = xcursor + diffX 57 | ycursor = ycursor + diffY 58 | else: 59 | NSColor.redColor().setFill() 60 | else: 61 | xcursor = xcursor - layer.bounds.origin.x 62 | thisPath = NSBezierPath.bezierPath() 63 | thisPath.appendBezierPath_(layer.completeBezierPath) 64 | t = NSAffineTransform.transform() 65 | t.translateXBy_yBy_(xcursor, -layer.master.descender + ycursor) 66 | thisPath.transformUsingAffineTransform_(t) 67 | p.appendBezierPath_(thisPath) 68 | 69 | t = NSAffineTransform.transform() 70 | if xcursor > 0: 71 | master = self.glyphs[0].layers[self.master].master 72 | vscale = self.bounds().size.height / ( 73 | master.ascender - master.descender 74 | ) 75 | hscale = self.bounds().size.width / xcursor 76 | t.scaleBy_(min(hscale, vscale)) 77 | p.transformUsingAffineTransform_(t) 78 | p.fill() 79 | except Exception as e: 80 | print("Oops!", sys.exc_info()[0], "occured.") 81 | traceback.print_exc(file=sys.stdout) 82 | 83 | 84 | class NastaliqEditor(object): 85 | def __init__(self, connections): 86 | self.connections = connections 87 | columns = [ 88 | {"title": x, "editable": x != "Left Glyph", "width": 40} 89 | for x in self.connections["colnames"] 90 | ] 91 | columns[0]["width"] = 100 92 | self.w = vanilla.Window((1000, 1000), "Nastaliq Editor", closable=True) 93 | self.w.LeftLabel = vanilla.TextBox((-200, 10, 200, 17), "", alignment="center") 94 | self.w.LeftButton = vanilla.Button( 95 | (-200, 30, 30, 17), "<", callback=self.decrement 96 | ) 97 | self.w.RightLabel = vanilla.TextBox((-170, 30, 140, 17), "", alignment="center") 98 | self.w.RightButton = vanilla.Button( 99 | (-30, 30, 30, 17), ">", callback=self.increment 100 | ) 101 | self.w.myList = vanilla.List( 102 | (0, 0, -300, -0), 103 | self.connections["rows"], 104 | columnDescriptions=columns, 105 | editCallback=self.editCallback, 106 | menuCallback=self.menuCallback, 107 | ) 108 | self.w.myList._clickTarget = VanillaCallbackWrapper( 109 | self.clickCallback 110 | ) 111 | self.w.myList._tableView.setTarget_(self.w.myList._clickTarget) 112 | self.w.myList._tableView.setAction_("action:") 113 | 114 | self.w.CompileButton = vanilla.Button( 115 | (-200, -20, 200, 17), "Compile", callback=self.compile 116 | ) 117 | 118 | self.glyphView = GlyphView.alloc().init() 119 | self.glyphView.glyphs = [] 120 | self.glyphView.master = Glyphs.font.masters[0].id 121 | self.glyphView.setFrame_(((0, 0), (600, 400))) 122 | self.w.scrollView = vanilla.ScrollView((-280, 50, 300, 400), self.glyphView) 123 | 124 | self.w.masterDropdown = vanilla.PopUpButton((-250, 500, -100, 20), 125 | [x.name for x in Glyphs.font.masters], 126 | callback=self.setMaster 127 | ) 128 | self.selectedPair = None 129 | self.inAdd = False 130 | self.w.open() 131 | 132 | def setMaster(self, sender): 133 | master_ix = sender.get() 134 | self.glyphView.setMaster(Glyphs.font.masters[master_ix].id) 135 | 136 | def editCallback(self, sender): 137 | if self.inAdd: 138 | return 139 | ccol, crow = self.w.myList.getEditedColumnAndRow() 140 | print("Col was ", crow, ccol) 141 | newdata = self.w.myList[crow][self.connections["colnames"][ccol]] 142 | print("New data was ", newdata) 143 | self.setNewPair(crow, ccol, newdata) 144 | sys.stdout.flush() 145 | 146 | def clickCallback(self, sender): 147 | crow = self.w.myList._tableView.clickedRow() 148 | ccol = self.w.myList._tableView.clickedColumn() 149 | if ccol < 1: 150 | return 151 | self.setNewPair(crow, ccol) 152 | 153 | def decrement(self, sender): 154 | try: 155 | self.add(-1) 156 | except Exception as e: 157 | print("Oops!", sys.exc_info()[0], "occured.") 158 | traceback.print_exc(file=sys.stdout) 159 | 160 | def increment(self, sender): 161 | try: 162 | self.add(1) 163 | except Exception as e: 164 | print("Oops!", sys.exc_info()[0], "occured.") 165 | traceback.print_exc(file=sys.stdout) 166 | 167 | 168 | def add(self, increment): 169 | if not self.selectedPair: 170 | return 171 | crow, ccol = self.selectedPair 172 | colname = self.connections["colnames"][ccol] 173 | availableAlternates = [ str(g.name) for g in Glyphs.font.glyphs if str(g.name).startswith(colname) ] 174 | currentAlternate = colname+str(self.connections["rows"][crow][colname]) 175 | if not currentAlternate in availableAlternates: 176 | # Weirdness 177 | return 178 | 179 | # Add +increment 180 | index = availableAlternates.index(currentAlternate) 181 | print(availableAlternates, currentAlternate) 182 | if index == 0 and increment == -1: return 183 | if index == len(availableAlternates)-1 and increment == 1: return 184 | newGlyph = availableAlternates[index + increment] 185 | data = newGlyph[len(colname):] 186 | print(data) 187 | self.inAdd = True 188 | self.setNewPair(crow, ccol, data) 189 | newdict = self.w.myList[crow] 190 | newdict[colname] = data 191 | self.w.myList[crow] = newdict 192 | self.inAdd = False 193 | 194 | def setNewPair(self, crow, ccol, newdata=None): 195 | left = self.connections["rows"][crow]["Left Glyph"] 196 | colname = self.connections["colnames"][ccol] 197 | if newdata and Glyphs.font.glyphs[colname + str(newdata)]: 198 | self.connections["rows"][crow][colname] = newdata 199 | Glyphs.font.userData["nastaliqConnections"] = self.connections 200 | data = self.connections["rows"][crow][colname] 201 | self.w.LeftLabel.set(left) 202 | self.w.RightLabel.set(colname + str(data)) 203 | self.selectedPair = (crow, ccol) 204 | 205 | if Glyphs.font.glyphs[colname + str(data)]: 206 | leftglyph = Glyphs.font.glyphs[left] 207 | rightglyph = Glyphs.font.glyphs[colname + str(data)] 208 | self.glyphView.setGlyphs([leftglyph, rightglyph]) 209 | sys.stdout.flush() 210 | 211 | def menuCallback(self, sender): 212 | sys.stdout.flush() 213 | 214 | def compile(self, sender): 215 | rows = Glyphs.font.userData["nastaliqConnections"]["rows"] 216 | rules = {} 217 | 218 | for line in rows: 219 | left_glyph = line["Left Glyph"] 220 | remainder = line.items() 221 | for (g, v) in remainder: 222 | if g == "Left Glyph": 223 | continue 224 | old = g + "1" 225 | if v == "1" or v == 1 or not v: 226 | continue 227 | replacement = g + str(v) 228 | if not old in rules: 229 | rules[old] = {} 230 | if not replacement in rules[old]: 231 | rules[old][replacement] = [] 232 | if left_glyph in Glyphs.font.glyphs: 233 | rules[old][replacement].append(left_glyph) 234 | code = "" 235 | for oldglyph in rules: 236 | if oldglyph not in Glyphs.font.glyphs: 237 | continue 238 | for replacement in rules[oldglyph]: 239 | if replacement not in Glyphs.font.glyphs: 240 | continue 241 | context = rules[oldglyph][replacement] 242 | if len(context) > 1: 243 | context = "[ %s ]" % (" ".join(context)) 244 | else: 245 | context = context[0] 246 | code = code + ( 247 | "rsub %s' %s by %s;\n" % (oldglyph, context, replacement) 248 | ) 249 | print(code) 250 | 251 | if not Glyphs.font.features["rlig"]: 252 | Glyphs.font.features["rlig"] = GSFeature("rlig", "") 253 | Glyphs.font.features["rlig"].code = "lookupflag IgnoreMarks;\n" + code 254 | Message("rlig feature written", "New feature rules written") 255 | 256 | 257 | def mergeConnections(new, old): 258 | # Turn old into a dict 259 | dOld = {} 260 | for row in old["rows"]: 261 | dOld[row["Left Glyph"]] = row 262 | for row in new["rows"]: 263 | for col in new["colnames"]: 264 | if row["Left Glyph"] in dOld and col in dOld[row["Left Glyph"]]: 265 | row[col] = dOld[row["Left Glyph"]][col] 266 | 267 | 268 | def kickoff(): 269 | # Check we have a font open and it's Qalmi-like 270 | if not Glyphs.font: 271 | Message("No font open", "Open a font") 272 | return 273 | 274 | connectables = [ 275 | x.name for x in Glyphs.font.glyphs if re.match(r".*[mif](sd?)?[0-9]+$", x.name) 276 | ] 277 | medials = [x for x in connectables if re.match(r".*m(sd)?[0-9]+$", x)] 278 | initials = [x for x in connectables if re.match(r".*i(sd)?[0-9]+$", x)] 279 | finals = [x for x in connectables if re.match(r".*f(sd?)?[0-9]+$", x)] 280 | medialstems = sorted(set([re.sub("(sd)?[0-9]+$", "", x) for x in medials])) 281 | initialstems = sorted(set([re.sub("(sd)?[0-9]+$", "", x) for x in initials])) 282 | 283 | if len(medials) == 0: 284 | Message( 285 | "Bad glyph name convention", 286 | "Glyph names must conform to Qalmi convention: RASM{m,i,u,f}number", 287 | ) 288 | return 289 | 290 | connections = {} 291 | 292 | connections["colnames"] = ["Left Glyph"] 293 | connections["colnames"].extend(medialstems) 294 | connections["colnames"].extend(initialstems) 295 | # Setup dummy data 296 | connections["rows"] = [] 297 | for row in sorted(medials, key=glyphsort): 298 | connections["rows"].append({colname: 1 for colname in connections["colnames"]}) 299 | connections["rows"][-1]["Left Glyph"] = row 300 | for row in sorted(finals, key=glyphsort): 301 | connections["rows"].append({colname: 1 for colname in connections["colnames"]}) 302 | connections["rows"][-1]["Left Glyph"] = row 303 | 304 | # Do we have some connection data already? 305 | if Glyphs.font.userData["nastaliqConnections"]: 306 | mergeConnections(connections, Glyphs.font.userData["nastaliqConnections"]) 307 | w = NastaliqEditor(connections) 308 | 309 | 310 | try: 311 | kickoff() 312 | except Exception as e: 313 | print("Oops!", sys.exc_info()[0], "occured.") 314 | traceback.print_exc(file=sys.stdout) 315 | -------------------------------------------------------------------------------- /Optical Center.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Determine Optical Center 2 | # -*- coding: utf-8 -*- 3 | __doc__ = """ 4 | Reports the optical center of the current layer. If there is a 'top' anchor, aligns it on the horizontal optical center 5 | """ 6 | import glyphmonkey 7 | layer = Glyphs.font.selectedLayers[0] 8 | 9 | i = layer.horizontalOpticalCenter() 10 | print("Horizontal optical center:", i) 11 | if layer.anchors['top']: 12 | layer.anchors['top'].position = [i, layer.anchors['top'].position.y] 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GlyphsScripts 2 | 3 | This repository contains some scripts for the Glyphs font editor. Some of them require the `glyphmonkey` library, which is part of this repository, to be installed. Dropping it into your scripts folder should be fine. 4 | 5 | ## Cadence grid 6 | 7 | This sets up guidelines according to the LeMo cadencing method. Ideally it should also autospace your glyphs, but it doesn't at the moment. 8 | 9 | ## Close But No Cigar 10 | 11 | This checks a glyph for: stem widths which are close to but not the stem widths you set in the master info; segments which are close to but not perpendicular; points which are close to but not on the baseline, x height, or cap height. The report turns up in the Macro window. This requires `glyphmonkey`. 12 | 13 | ## Comb 14 | 15 | Drags a "comb" through your glyphs, for a very 70s effect. The glyphs need to be constructed in a *very* particular way. They need to be made up of paths which are topologically equivalent to rectangles. The "comb" will be dragged from the shortest edge to the second shortest edge. Between those two edges, the paths should be equivalent - same number of points, same types of curve/straight. They don't need to be absolutely symmetrical, but they do need to be conformant. So: to draw an O, draw two Us, rotate one and put it on top of the other. You can draw an L in the normal way, but a P should consistent of a straight and a U curve. You can change the "teeth" of the comb by changing the `stripes` array, which is an array of start and stop positions. Requires `glyphmonkey`. 16 | 17 | ## Delete Close Points 18 | 19 | Removes short segments, deleting points which are improbably close to other points. Requires `glyphmonkey`. 20 | 21 | ## Raise ascender 22 | 23 | Raises only the points higher than x height. 24 | 25 | ## Straighten all curves 26 | 27 | Deletes all handles (off-curve points) to turn your curves into lines. 28 | -------------------------------------------------------------------------------- /Raise Ascender.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Raise ascender 2 | # -*- coding: utf-8 -*- 3 | __doc__=""" 4 | Raise points above the X-height by the given amount 5 | """ 6 | 7 | import vanilla 8 | windowHeight = 100 9 | 10 | Font = Glyphs.font 11 | 12 | class RaiseAscender( object ): 13 | def __init__( self ): 14 | try: 15 | self.w = vanilla.FloatingWindow( (200, windowHeight), "Raise ascender") 16 | self.w.text_anchor = vanilla.TextBox( (15, 12, 130, 17), "Amount", sizeStyle='small') 17 | self.w.amount = vanilla.EditText( (100, 10, 50, 20), "10", sizeStyle = 'small') 18 | self.w.goThis = vanilla.Button((10, 15, 150, 57), "Raise this master", sizeStyle='small', callback=self.goThis) 19 | self.w.goAll = vanilla.Button((10, 50, 150, 57), "Raise all masters", sizeStyle='small', callback=self.goAll) 20 | self.w.setDefaultButton( self.w.goThis ) 21 | self.w.open() 22 | except Exception, e: 23 | print(e) 24 | 25 | def goAll(self,sender): 26 | try: 27 | for thisLayer in Font.selectedLayers[0].parent.layers: 28 | thisLayer.setDisableUpdates() 29 | thisGlyph = thisLayer.parent 30 | self.raiseGlyph(thisGlyph, thisLayer) 31 | thisLayer.setEnableUpdates() 32 | except Exception, e: 33 | print(e) 34 | 35 | def goThis(self, sender): 36 | try: 37 | for thisLayer in Font.selectedLayers: 38 | thisLayer.setDisableUpdates() 39 | thisGlyph = thisLayer.parent 40 | self.raiseGlyph(thisGlyph,thisLayer) 41 | thisLayer.setEnableUpdates() 42 | except Exception, e: 43 | print(e) 44 | 45 | def raiseGlyph(self, thisGlyph, thisLayer): 46 | try: 47 | raiseAmount = float(self.w.amount.get()) 48 | thisGlyph.beginUndo() 49 | for thisPath in thisLayer.paths: 50 | for thisNode in thisPath.nodes: 51 | if thisNode.y > (Font.masters[thisLayer.associatedMasterId].xHeight + 10): 52 | thisNode.y += raiseAmount 53 | thisGlyph.endUndo() 54 | 55 | except Exception, e: 56 | print(e) 57 | 58 | RaiseAscender() -------------------------------------------------------------------------------- /Recipe Dumper.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Recipe dumper 2 | # -*- coding: utf-8 -*- 3 | __doc__=""" 4 | Show a recipe for the selected glyphs which can be fed to "Add Glyphs..." 5 | """ 6 | 7 | for l in Glyphs.font.selectedLayers: 8 | if l.components: 9 | print("+".join(c.componentName for c in l.components)+'='+l.glyph().name) -------------------------------------------------------------------------------- /Rename To Glyphs Default Names.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Rename to Glyphs Default Names 2 | # -*- coding: utf-8 -*- 3 | __doc__=""" 4 | Makes the names equal to what you would get when you add them in Glyphs 5 | """ 6 | 7 | mapping = { 8 | "Euro": "euro", 9 | "arrowup": "upArrow", 10 | "arrowright": "rightArrow", 11 | "arrowdown": "downArrow" , 12 | "arrowleft":"leftArrow", 13 | "arrowboth": "leftRightArrow", 14 | "arrowupdn": "upDownArrow", 15 | "uni01DD": "schwa", 16 | "uni0294": "glottalstop", 17 | "dotlessi": "idotless", 18 | "uni019B": "lambdastroke", 19 | "uni026B": "lmiddletilde", 20 | "uni00B9": "onesuperior", 21 | "uni00B2": "twosuperior", 22 | "uni00B3": "threesuperior", 23 | "guillemotleft": "guillemetleft", 24 | "guillemotright": "guillemetright", 25 | "uni0308": "dieresiscomb", 26 | "uni0307": "dotaccentcomb", 27 | "uni030B": "hungarumlautcomb", 28 | "uni0302": "circumflexcomb", 29 | "uni030C": "caroncomb", 30 | "uni0306": "brevecomb", 31 | "uni030A": "ringcomb", 32 | "uni0304": "macroncomb", 33 | "uni0312": "commaturnedabovecomb", 34 | "uni0313": "commaabovecomb", 35 | "uni0315": "commaaboverightcomb", 36 | "uni0326": "commaaccentcomb", 37 | "uni0327": "cedillacomb", 38 | "uni0328": "ogonekcomb", 39 | "uni0335": "strokeshortcomb", 40 | "uni0336": "strokelongcomb", 41 | "uni0337": "slashshortcomb", 42 | "uni0338": "slashlongcomb", 43 | "uni02BC": "apostrophemod", 44 | 45 | 46 | } 47 | 48 | Font = Glyphs.font 49 | selectedGlyphs = [ x.parent for x in Font.selectedLayers ] 50 | 51 | def renameGlyph( thisGlyph ): 52 | oldName = thisGlyph.name 53 | if oldName in mapping: 54 | newName = mapping[oldName] 55 | thisGlyph.name = newName 56 | print "%s --> %s" % (oldName, newName) 57 | 58 | Font.disableUpdateInterface() 59 | 60 | for thisGlyph in selectedGlyphs: 61 | renameGlyph( thisGlyph ) 62 | 63 | Font.enableUpdateInterface() 64 | -------------------------------------------------------------------------------- /Reset Anchors.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Reset Anchors 2 | # -*- coding: utf-8 -*- 3 | __doc__=""" 4 | Delete all anchors and add them again 5 | """ 6 | 7 | Glyphs.font.selectedLayers[0].parent.beginUndo() 8 | for layer in Glyphs.font.selectedLayers[0].parent.layers: 9 | layer.anchors = [] 10 | layer.addMissingAnchors() 11 | Glyphs.font.selectedLayers[0].end.beginUndo() -------------------------------------------------------------------------------- /Round Everything.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Round Everything 2 | # -*- coding: utf-8 -*- 3 | __doc__=""" 4 | Round all coordinates of everything to integers 5 | """ 6 | 7 | from Foundation import NSPoint 8 | 9 | 10 | def roundattrs(thing, attrs): 11 | for attr in attrs: 12 | if isinstance(getattr(thing, attr), float): 13 | setattr(thing, attr, round(getattr(thing, attr))) 14 | 15 | for g in Glyphs.font.glyphs: 16 | g.beginUndo() 17 | for l in g.layers: 18 | roundattrs(l, ["LSB", "RSB", "TSB", "BSB", "width", "vertWidth"]) 19 | for guide in l.guides: 20 | guide.position = NSPoint(round(guide.position.x), round(guide.position.y)) 21 | for anchor in l.anchors: 22 | anchor.position = NSPoint(round(anchor.position.x), round(anchor.position.y)) 23 | l.roundCoordinates() 24 | g.endUndo() 25 | -------------------------------------------------------------------------------- /Sandblast.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Sandblast 2 | # -*- coding: utf-8 -*- 3 | __doc__=""" 4 | Drill randomly-sized holes in the glyph 5 | """ 6 | 7 | import random 8 | import math 9 | from Foundation import NSMakePoint 10 | 11 | layer = Glyphs.font.selectedLayers[0] # current layer 12 | left, bottom = layer.bounds.origin.x, layer.bounds.origin.y 13 | width, height = layer.bounds.size.width, layer.bounds.size.height 14 | 15 | dust = [] 16 | f = layer.completeBezierPath 17 | 18 | points = 1000 19 | while points: 20 | x = random.randrange(left, left+width) 21 | y = random.randrange(bottom, bottom+height) 22 | if not f.containsPoint_(NSMakePoint(x,y)): 23 | continue 24 | sides = random.randrange(15,30) 25 | p = GSPath() 26 | angle = math.radians(360/sides) 27 | 28 | for i in range(sides): 29 | p.nodes.append(GSNode( (x, y), LINE)) 30 | delta = abs(random.gauss(5,20) / float(sides)) 31 | 32 | x = x + math.cos(i * angle) * (random.random() + delta) 33 | y = y + math.sin(i * angle) * (random.random() + delta) 34 | points = points - 1 35 | p.closed = True 36 | if p.direction == layer.paths[0].direction: 37 | p.reverse() 38 | dust.append(p) 39 | 40 | GSPathOperator = objc.lookUpClass("GSPathOperator") 41 | 42 | pathOp = GSPathOperator.alloc().init() 43 | 44 | inpaths = list(layer.paths) 45 | pathOp.subtractPaths_from_error_( dust, inpaths, None ) 46 | 47 | layer.paths.extend(inpaths) 48 | 49 | 50 | # layer.paths.extend(dust) 51 | 52 | layer.correctPathDirection() 53 | -------------------------------------------------------------------------------- /Sansomatic.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Sans-o-Matic 2 | # -*- coding: utf-8 -*- 3 | __doc__=""" 4 | This is the stupidest script ever. Stupid, stupid, stupid. 5 | """ 6 | 7 | import vanilla 8 | from math import sin,pi,tan,atan 9 | 10 | capHeight = None 11 | Dimensions = None 12 | 13 | def clearLayer(): 14 | Glyphs.font.selectedLayers[0].paths = [] 15 | 16 | def getControl(controls, label): 17 | if controls[label].has_key("value"): 18 | # print("Value of "+label, controls[label]["value"]) 19 | return controls[label]["value"] 20 | # print("Using default for "+label, controls[label]["default"]) 21 | return controls[label]["default"] 22 | 23 | def lerp(t,a,b): 24 | return NSValue.valueWithPoint_(NSPoint(int((1-t)*a.x + t*b.x), int((1-t)*a.y + t*b.y))) 25 | import math 26 | def distance( node1, node2 ): 27 | return math.hypot( node1.x - node2.x, node1.y - node2.y ) 28 | 29 | def drawRectangle(x,y,w,h): 30 | bl = GSNode((x,y), type = LINE) 31 | br = GSNode((x+w,y), type = LINE) 32 | tr = GSNode((x+w,y+h), type = LINE) 33 | tl = GSNode((x,y+h), type = LINE) 34 | p = GSPath() 35 | p.nodes = [bl,br,tr,tl] 36 | p.closed = True 37 | Glyphs.font.selectedLayers[0].paths.append(p) 38 | 39 | def stemWidthForTargetWeight(s,angle): 40 | return s / sin(pi/2-angle) 41 | 42 | def drawAngledRectangle(x,y,w,h,angle): 43 | correctedWidth = stemWidthForTargetWeight(w, angle) 44 | bottomOffset = tan(angle) * h 45 | bl = GSNode((x+bottomOffset,y), type = LINE) 46 | br = GSNode((x+correctedWidth+bottomOffset,y), type = LINE) 47 | tr = GSNode((x+correctedWidth,y+h), type = LINE) 48 | tl = GSNode((x,y+h), type = LINE) 49 | p = GSPath() 50 | p.nodes = [bl,br,tr,tl] 51 | p.closed = True 52 | Glyphs.font.selectedLayers[0].paths.append(p) 53 | return x+bottomOffset 54 | 55 | def getDimension(name): 56 | Font = Glyphs.font 57 | masterID = Glyphs.font.selectedLayers[0].associatedMasterId 58 | Dimensions = Font.userData["GSDimensionPlugin.Dimensions"][masterID] 59 | if not Dimensions or not Dimensions[name]: 60 | print("You must declare the "+name+" dimension in the palette") 61 | raise 62 | return float(Dimensions[name]) 63 | 64 | options = {} 65 | sansomatic = {} 66 | 67 | 68 | # A 69 | 70 | def A(controls): 71 | angle = getControl(controls, "Angle") 72 | stem = getDimension("HV") 73 | crossbar = getDimension("HH") 74 | overlap = getControl(controls, "Overlap") / 100.0 * stem 75 | xHeight = getControl(controls, "Crossbar Height") / 100.0 * capHeight 76 | newX = drawAngledRectangle(0, capHeight, stem, -capHeight,angle) 77 | drawAngledRectangle(newX*2 - overlap, capHeight, stem, -capHeight,-angle) 78 | xbarOffset = tan(pi-angle)*xHeight 79 | xbarLen = tan(pi-angle)*(capHeight-(xHeight+crossbar))*2 - overlap 80 | drawRectangle(xbarOffset, xHeight + crossbar/2, xbarLen, crossbar) 81 | Glyphs.font.selectedLayers[0].LSB = Glyphs.font.selectedLayers[0].RSB = 5 82 | Glyphs.font.selectedLayers[0].removeOverlap() 83 | 84 | sansomatic["A"] = { 85 | 'controls': { 86 | 'Angle': { 'type': "Slider", 'min': 0.1, 'max':0.5, 'default': 0.3 }, 87 | 'Overlap': { 'type': "Slider", 'min': 0, 'max':100, 'default': 0 }, 88 | 'Crossbar Height': { 'type': "Slider", 'min': 10, 'max':60, 'default': 25 }, 89 | }, 90 | 'method': A 91 | } 92 | 93 | # E 94 | 95 | def E(controls): 96 | width = getControl(controls, "Leg Length") 97 | stem = getDimension("HV") 98 | crossbar = getDimension("HH") 99 | drawRectangle(stem, 0, width, crossbar) 100 | F(controls) 101 | 102 | sansomatic["E"] = { 103 | 'controls': { 104 | 'Leg Length': { 'type': "Slider", 'min': 100, 'max':700, 'default': 300 }, 105 | 'Crossbar Percentage': { 'type': "Slider", 'min': 20, 'max':100, 'default': 75 } 106 | }, 107 | 'method': E 108 | } 109 | 110 | # F 111 | 112 | def F(controls): 113 | width = getControl(controls, "Leg Length") 114 | ratio = getControl(controls, "Crossbar Percentage") / 100.0 115 | stem = getDimension("HV") 116 | crossbar = getDimension("HH") 117 | drawRectangle(0, 0, stem, capHeight) 118 | drawRectangle(stem, capHeight-crossbar, width, crossbar) 119 | drawRectangle(stem, (capHeight/2)-(crossbar/2), width * ratio, crossbar) 120 | Glyphs.font.selectedLayers[0].LSB = Glyphs.font.selectedLayers[0].RSB = 50 121 | Glyphs.font.selectedLayers[0].removeOverlap() 122 | 123 | sansomatic["F"] = { 124 | 'controls': { 125 | 'Leg Length': { 'type': "Slider", 'min': 100, 'max':700, 'default': 300 }, 126 | 'Crossbar Percentage': { 'type': "Slider", 'min': 20, 'max':100, 'default': 75 } 127 | }, 128 | 'method': F 129 | } 130 | 131 | # H 132 | 133 | def H(controls): 134 | width = getControl(controls, "Crossbar Width") 135 | stem = getDimension("HV") 136 | crossbar = getDimension("HH") 137 | drawRectangle(0, 0, stem, capHeight) 138 | drawRectangle(width+stem, 0, stem, capHeight) 139 | drawRectangle(stem,(capHeight/2)-(crossbar/2), width, crossbar) 140 | Glyphs.font.selectedLayers[0].LSB = Glyphs.font.selectedLayers[0].RSB = 50 141 | Glyphs.font.selectedLayers[0].removeOverlap() 142 | 143 | sansomatic["H"] = { 144 | 'controls': { 145 | 'Crossbar Width': { 'type': "Slider", 'min': 100, 'max':700, 'default': 300 } 146 | }, 147 | 'method': H 148 | } 149 | 150 | # I 151 | 152 | def I(controls): 153 | stem = getDimension("HV") 154 | crossbarHt = getDimension("HH") 155 | drawRectangle(0, 0, stem, capHeight) 156 | if getControl(controls, "Crossbars?"): 157 | crossbarLen = getControl(controls, "Crossbar Width") 158 | drawRectangle(-crossbarLen/2 + stem/2, capHeight-crossbarHt, crossbarLen, crossbarHt) 159 | drawRectangle(-crossbarLen/2 + stem/2, 0, crossbarLen, crossbarHt) 160 | Glyphs.font.selectedLayers[0].LSB = Glyphs.font.selectedLayers[0].RSB = 50 161 | Glyphs.font.selectedLayers[0].removeOverlap() 162 | 163 | 164 | sansomatic["I"] = { 165 | 'controls': { 166 | 'Crossbars?': { 'type': "CheckBox", 'default': False }, 167 | 'Crossbar Width': { 'type': "Slider", 'min': 0, 'max':700, 'default': 500 } 168 | }, 169 | 'method': I 170 | } 171 | 172 | # L 173 | 174 | def L(controls): 175 | width = getControl(controls, "Leg Length") 176 | stem = getDimension("HV") 177 | crossbar = getDimension("HH") 178 | drawRectangle(0, 0, stem, capHeight) 179 | drawRectangle(stem, 0, width, crossbar) 180 | Glyphs.font.selectedLayers[0].LSB = Glyphs.font.selectedLayers[0].RSB = 50 181 | Glyphs.font.selectedLayers[0].removeOverlap() 182 | 183 | sansomatic["L"] = { 184 | 'controls': { 185 | 'Leg Length': { 'type': "Slider", 'min': 0, 'max':700, 'default': 300 } 186 | }, 187 | 'method': L 188 | } 189 | 190 | 191 | # N 192 | 193 | def N(controls): 194 | angle = getControl(controls, "Angle") 195 | 196 | stem = getDimension("HV") 197 | newX = drawAngledRectangle(0, 0, stem, capHeight,angle) 198 | width = tan(angle)*capHeight + stemWidthForTargetWeight(stem,angle) 199 | drawRectangle(newX+(stemWidthForTargetWeight(stem,angle)-stem), 0,stem,capHeight) 200 | drawRectangle(0, 0,stem,capHeight) 201 | Glyphs.font.selectedLayers[0].LSB = Glyphs.font.selectedLayers[0].RSB = 50 202 | Glyphs.font.selectedLayers[0].removeOverlap() 203 | 204 | sansomatic["N"] = { 205 | 'controls': { 206 | 'Angle': { 'type': "Slider", 'min': 0.3, 'max':0.523, 'default': 0.4 }, 207 | }, 208 | 'method': N 209 | } 210 | 211 | # O 212 | 213 | def O(controls): 214 | width = getControl(controls, "Width") 215 | stem = getDimension("OV") 216 | crossbar = getDimension("OH") 217 | htension = getControl(controls,"H Tension") 218 | vtension = getControl(controls,"V Tension") 219 | overshoot = getControl(controls,"Overshoot") 220 | 221 | bc = GSNode((width/2,-overshoot), type = CURVE) 222 | bc.smooth = True 223 | bcl = GSNode((width/2-htension*(width/2),-overshoot), type = OFFCURVE) 224 | bcr = GSNode((width/2+htension*(width/2),-overshoot), type = OFFCURVE) 225 | l = GSNode((0,capHeight/2), type = CURVE) 226 | l.smooth = True 227 | lr = GSNode((0,capHeight/2-vtension*(capHeight/2)), type = OFFCURVE) 228 | ll = GSNode((0,capHeight/2+vtension*(capHeight/2)), type = OFFCURVE) 229 | r = GSNode((width,capHeight/2), type = CURVE) 230 | rr = GSNode((width,capHeight/2-vtension*(capHeight/2)), type = OFFCURVE) 231 | rl = GSNode((width,capHeight/2+vtension*(capHeight/2)), type = OFFCURVE) 232 | r.smooth = True 233 | tc = GSNode((width/2,capHeight+overshoot), type = CURVE) 234 | tc.smooth = True 235 | tcl = GSNode((width/2-htension*(width/2),capHeight+overshoot), type = OFFCURVE) 236 | tcr = GSNode((width/2+htension*(width/2),capHeight+overshoot), type = OFFCURVE) 237 | 238 | p = GSPath() 239 | p.nodes = [bcr,rr,r,rl,tcr,tc,tcl,ll,l,lr,bcl,bc] 240 | p.closed = True 241 | Glyphs.font.selectedLayers[0].paths.append(p) 242 | 243 | htension = htension * (width/capHeight) 244 | vtension = vtension * (width/capHeight) 245 | 246 | bc = GSNode((width/2,crossbar), type = CURVE) 247 | bc.smooth = True 248 | bcl = GSNode((width/2-htension*(width/2),crossbar), type = OFFCURVE) 249 | bcr = GSNode((width/2+htension*(width/2),crossbar), type = OFFCURVE) 250 | l = GSNode((stem,capHeight/2), type = CURVE) 251 | l.smooth = True 252 | lr = GSNode((stem,capHeight/2-vtension*(capHeight/2)), type = OFFCURVE) 253 | ll = GSNode((stem,capHeight/2+vtension*(capHeight/2)), type = OFFCURVE) 254 | r = GSNode((width-stem,capHeight/2), type = CURVE) 255 | rr = GSNode((width-stem,capHeight/2-vtension*(capHeight/2)), type = OFFCURVE) 256 | rl = GSNode((width-stem,capHeight/2+vtension*(capHeight/2)), type = OFFCURVE) 257 | r.smooth = True 258 | tc = GSNode((width/2,capHeight-crossbar), type = CURVE) 259 | tc.smooth = True 260 | tcl = GSNode((width/2-htension*(width/2),capHeight-crossbar), type = OFFCURVE) 261 | tcr = GSNode((width/2+htension*(width/2),capHeight-crossbar), type = OFFCURVE) 262 | 263 | p = GSPath() 264 | p.nodes = [bcr,rr,r,rl,tcr,tc,tcl,ll,l,lr,bcl,bc] 265 | p.closed = True 266 | Glyphs.font.selectedLayers[0].paths.append(p) 267 | 268 | Glyphs.font.selectedLayers[0].correctPathDirection() 269 | 270 | 271 | Glyphs.font.selectedLayers[0].LSB = Glyphs.font.selectedLayers[0].RSB = 15 272 | # Glyphs.font.selectedLayers[0].removeOverlap() 273 | 274 | sansomatic["O"] = { 275 | 'controls': { 276 | 'Width': { 'type': "Slider", 'min': 0, 'max':700, 'default': 450 }, 277 | 'Overshoot': { 'type': "Slider", 'min': 0, 'max':20, 'default': 10 }, 278 | 'H Tension': { 'type': "Slider", 'min': 0.4, 'max':1, 'default': 0.6 }, 279 | 'V Tension': { 'type': "Slider", 'min': 0.4, 'max':1, 'default': 0.75 } 280 | }, 281 | 'method': O 282 | } 283 | 284 | def S(controls): 285 | #capHeight = master.capHeight 286 | 287 | topwidth = getControl(controls, "Top Width") 288 | bottomwidth = getControl(controls, "Bottom Width") 289 | 290 | center = 400 291 | tau = getControl(controls, "Tension") 292 | alpha = getControl(controls, "Bar Straightness") 293 | barHeight = getControl(controls, "Bar Height") * capHeight 294 | leftThick = 55 295 | rightThick = 80 296 | barWidth = getControl(controls, "Bar Thickness") 297 | slope = getControl(controls, "Bar Slope") 298 | sign = 1 299 | overshoot = getControl(controls, "Overshoot") 300 | hairline = getControl(controls, "Hairline Thickness") 301 | 302 | def doCurve(sign): 303 | if sign == 1: 304 | top= capHeight + overshoot 305 | else: 306 | top= capHeight + overshoot - hairline 307 | t = GSNode((center,top), type = LINE) 308 | t.smooth = True 309 | 310 | xleft = center - topwidth - sign * leftThick/2 311 | c = GSNode((center, barHeight - sign * barWidth/2), type = LINE) 312 | 313 | i2 = NSPoint(xleft, top) 314 | i = NSPoint(xleft, barHeight - sign * barWidth/2 + slope * (topwidth + sign * leftThick/2)) 315 | t1 = GSNode(lerp(tau, t.position, i2).pointValue(), type = OFFCURVE) 316 | x = GSNode(lerp(alpha, c.position, i).pointValue(), type = CURVE) 317 | x1 = GSNode(lerp(tau, x.position, i).pointValue(), type = OFFCURVE) 318 | 319 | xright = center + bottomwidth - sign * rightThick/2 320 | 321 | i_lower = NSPoint(xright, barHeight - sign * barWidth/2 - slope * (bottomwidth-sign * rightThick/2)) 322 | x_lower = GSNode(lerp(alpha, c.position, i_lower).pointValue(), type = LINE) 323 | x1_lower = GSNode(lerp(tau, x_lower.position, i_lower).pointValue(), type = OFFCURVE) 324 | 325 | 326 | # yc-y=s * xc-x 327 | 328 | d_i2_t1 = distance(i2,t1) 329 | d_x1_i = distance(x1,i) 330 | 331 | yl_num = d_i2_t1 * i.y - d_x1_i * t.y + math.sqrt(d_i2_t1 * d_x1_i * i.y * i.y - 2 * d_i2_t1 * d_x1_i * i.y * t.y + d_i2_t1 * d_x1_i * t.y * t.y) 332 | yl_den = d_i2_t1 - d_x1_i 333 | yl = yl_num / yl_den 334 | 335 | 336 | l = GSNode(( xleft, yl), type = CURVE) 337 | l.smooth = True 338 | l1 = GSNode(lerp(tau, l.position, i2).pointValue(), type = OFFCURVE) 339 | l2 = GSNode(lerp(tau, l.position, i).pointValue(), type = OFFCURVE) 340 | 341 | 342 | if sign == -1: 343 | base = -overshoot 344 | else: 345 | base = -overshoot + hairline 346 | 347 | b = GSNode((center,base), type = CURVE) 348 | # b.smooth = True 349 | 350 | i2_lower = NSPoint(xright, b.y) 351 | 352 | b1 = GSNode(lerp(tau, b.position, i2_lower).pointValue(), type = OFFCURVE) 353 | 354 | l1 = GSNode(lerp(tau, l.position, i2).pointValue(), type = OFFCURVE) 355 | l2 = GSNode(lerp(tau, l.position, i).pointValue(), type = OFFCURVE) 356 | 357 | d_i2_lower_b1 = distance(i2_lower,b1) 358 | d_x1_lower_i_lower = distance(i_lower,x1_lower) 359 | yr_num = d_i2_lower_b1 * i_lower.y - d_x1_lower_i_lower * b.y - math.sqrt(d_i2_lower_b1 * d_x1_lower_i_lower * i_lower.y * i_lower.y - 2 * d_i2_lower_b1 * d_x1_lower_i_lower * i_lower.y * b.y + d_i2_lower_b1 * d_x1_lower_i_lower * b.y * b.y) 360 | yr_den = d_i2_lower_b1 - d_x1_lower_i_lower 361 | yr = yr_num / yr_den 362 | r = GSNode(( xright, yr), type = CURVE) 363 | r1 = GSNode(lerp(tau, r.position, i2_lower).pointValue(), type = OFFCURVE) 364 | r2 = GSNode(lerp(tau, r.position, i_lower).pointValue(), type = OFFCURVE) 365 | r.smooth = True 366 | if sign == 1: 367 | return [t,t1,l1,l, l2, x1,x, x_lower,x1_lower,r2,r,r1,b1,b] 368 | b.type = LINE 369 | x_lower.type = CURVE 370 | x.type = LINE 371 | t.type = CURVE 372 | 373 | return [b, b1, r1, r, r2, x1_lower, x_lower, x, x1, l2, l, l1, t1, t] 374 | 375 | p = GSPath() 376 | n = [] 377 | n.extend(doCurve(1)) 378 | nodes2 = doCurve(-1) 379 | n.extend(nodes2) 380 | p.nodes = n 381 | p.closed = True 382 | Glyphs.font.selectedLayers[0].paths.append(p) 383 | Glyphs.font.selectedLayers[0].LSB = Glyphs.font.selectedLayers[0].RSB = 50 384 | 385 | sansomatic["S"] = { 386 | 'controls': { 387 | 'Overshoot': { 'type': "Slider", 'min': 0, 'max':20, 'default': 10 }, 388 | 'Tension': { 'type': "Slider", 'min': 0.5, 'max':0.9, 'default': 0.6 }, 389 | 'Top Width': { 'type': "Slider", 'min': 0, 'max':700, 'default': 200 }, 390 | 'Bottom Width': { 'type': "Slider", 'min': 0, 'max':700, 'default': 225 }, 391 | 'Bar Straightness': { 'type': "Slider", 'min': 0, 'max':0.5, 'default': 0.2 }, 392 | 'Bar Slope': { 'type': "Slider", 'min': 0, 'max':0.5, 'default': 0.2 }, 393 | 'Bar Height': { 'type': "Slider", 'min': 0.5, 'max':0.7, 'default': 0.55 }, 394 | 'Bar Thickness': { 'type': "Slider", 'min': 0, 'max':300, 'default': 120 }, 395 | 'Hairline Thickness': { 'type': "Slider", 'min': 0, 'max':300, 'default': 20 } 396 | 397 | }, 398 | 'method': S 399 | } 400 | 401 | # T 402 | 403 | def T(controls): 404 | width = getControl(controls, "Crossbar Width") 405 | stem = getDimension("HV") 406 | crossbar = getDimension("HH") 407 | width = width + stem 408 | drawRectangle(0, capHeight-crossbar, width, crossbar) 409 | drawRectangle(width/2 - stem/2, 0, stem, capHeight) 410 | Glyphs.font.selectedLayers[0].LSB = Glyphs.font.selectedLayers[0].RSB = 50 411 | Glyphs.font.selectedLayers[0].removeOverlap() 412 | 413 | sansomatic["T"] = { 414 | 'controls': { 415 | 'Crossbar Width': { 'type': "Slider", 'min': 0, 'max':700, 'default': 300 } 416 | }, 417 | 'method': T 418 | } 419 | 420 | # V 421 | 422 | def V(controls): 423 | angle = getControl(controls, "Angle") 424 | stem = getDimension("HV") 425 | overlap = getControl(controls, "Overlap") / 100 * stem 426 | newX = drawAngledRectangle(0, 0, stem, capHeight,angle) 427 | drawAngledRectangle(newX*2 + overlap, 0, stem, capHeight,-angle) 428 | Glyphs.font.selectedLayers[0].LSB = Glyphs.font.selectedLayers[0].RSB = 5 429 | Glyphs.font.selectedLayers[0].removeOverlap() 430 | 431 | sansomatic["V"] = { 432 | 'controls': { 433 | 'Angle': { 'type': "Slider", 'min': 0.1, 'max':0.5, 'default': 0.3 }, 434 | 'Overlap': { 'type': "Slider", 'min': 0, 'max':100, 'default': 0 }, 435 | }, 436 | 'method': V 437 | } 438 | 439 | 440 | # X 441 | 442 | def X(controls): 443 | angle = getControl(controls, "Angle") 444 | stem = getDimension("HV") 445 | newX = drawAngledRectangle(0, 0, stem, capHeight,angle) 446 | drawAngledRectangle(newX, 0, stem, capHeight,-angle) 447 | Glyphs.font.selectedLayers[0].LSB = Glyphs.font.selectedLayers[0].RSB = 5 448 | Glyphs.font.selectedLayers[0].removeOverlap() 449 | 450 | sansomatic["X"] = { 451 | 'controls': { 452 | 'Angle': { 'type': "Slider", 'min': 0.3, 'max':0.8, 'default': 0.523 }, 453 | }, 454 | 'method': X 455 | } 456 | 457 | # Y 458 | 459 | def Y(controls): 460 | angle = getControl(controls, "Angle") 461 | stem = getDimension("HV") 462 | joinHeight = (capHeight-stem) * getControl(controls, "Join Height") / 100.0 463 | newX = drawAngledRectangle(0, joinHeight, stem, capHeight-joinHeight,angle) 464 | drawAngledRectangle(newX*2, joinHeight, stem, capHeight-joinHeight,-angle) 465 | drawRectangle(newX,0,stemWidthForTargetWeight(stem,angle),joinHeight) 466 | Glyphs.font.selectedLayers[0].LSB = Glyphs.font.selectedLayers[0].RSB = 5 467 | Glyphs.font.selectedLayers[0].removeOverlap() 468 | 469 | sansomatic["Y"] = { 470 | 'controls': { 471 | 'Angle': { 'type': "Slider", 'min': 0.3, 'max':0.8, 'default': 0.4 }, 472 | 'Join Height': { 'type': "Slider", 'min': 25, 'max':75, 'default': 50 }, 473 | 474 | }, 475 | 'method': Y 476 | } 477 | 478 | # Z 479 | 480 | def Z(controls): 481 | angle = getControl(controls, "Angle") 482 | 483 | stem = getDimension("HV") 484 | crossbar = getDimension("HH") 485 | newX = drawAngledRectangle(0, 0, stem, capHeight,-angle) 486 | offset = tan(angle)*crossbar 487 | width = tan(angle)*capHeight + stemWidthForTargetWeight(stem,angle) 488 | drawRectangle(newX+offset, 0,width-offset,crossbar) 489 | drawRectangle(newX, capHeight-crossbar,width-offset,crossbar) 490 | Glyphs.font.selectedLayers[0].LSB = Glyphs.font.selectedLayers[0].RSB = 5 491 | Glyphs.font.selectedLayers[0].removeOverlap() 492 | 493 | sansomatic["Z"] = { 494 | 'controls': { 495 | 'Angle': { 'type': "Slider", 'min': 0.3, 'max':0.523, 'default': 0.4 }, 496 | }, 497 | 'method': Z 498 | } 499 | 500 | myWindow = None 501 | 502 | def createWindow(controls, fn): 503 | winWidth = 300 504 | winHeight = 40 * (1+len(controls)) + 20 505 | myWindow = vanilla.FloatingWindow((winWidth, winHeight), "Sans-o-Matic") 506 | x = 10 507 | y = 10 508 | for k in controls: 509 | v = controls[k] 510 | label = vanilla.TextBox((x,y,150,30), k) 511 | x = x + 120 512 | def controlCallback (s): 513 | for k2 in controls: 514 | v2 = controls[k2] 515 | controls[k2]["value"] = getattr(myWindow,k2).get() 516 | clearLayer() 517 | fn(controls) 518 | 519 | if v['type'] == "Slider": 520 | control = vanilla.Slider((x,y,-10,30), 521 | minValue = v['min'], 522 | maxValue = v['max'], 523 | value=v['default'], 524 | callback = controlCallback 525 | ) 526 | elif v['type'] == "CheckBox": 527 | control = vanilla.CheckBox((x,y,10,10),"", 528 | callback = controlCallback 529 | ) 530 | setattr(myWindow,k,control) 531 | setattr(myWindow,k+"label",label) 532 | 533 | y = y + 40 534 | x = 10 535 | def dismiss(s): 536 | myWindow.close() 537 | myWindow = None 538 | myWindow.dismiss = vanilla.Button((-75, y, -15, -15), "OK", sizeStyle='regular', callback=dismiss ) 539 | fn(controls) 540 | myWindow.open() 541 | 542 | def rebuild(): 543 | masterID = Glyphs.font.selectedLayers[0].associatedMasterId 544 | master = Glyphs.font.masters[masterID] 545 | global capHeight 546 | capHeight = master.capHeight 547 | 548 | Font = Glyphs.font 549 | glyph = Glyphs.font.selectedLayers[0].parent 550 | Dimensions = Font.userData["GSDimensionPlugin.Dimensions"] 551 | if not Dimensions: 552 | print("You must set your stem widths in the dimension palette") 553 | Dimensions = Dimensions[masterID] 554 | 555 | print(sansomatic.keys()) 556 | if sansomatic.has_key(glyph.string): 557 | clearLayer() 558 | controls = sansomatic[glyph.string]["controls"] 559 | if len(controls) > 0: 560 | createWindow(controls, sansomatic[glyph.string]["method"]) 561 | else: 562 | print("Don't know how to draw a "+glyph.string) 563 | 564 | rebuild() 565 | NSNotificationCenter.defaultCenter.addObserver_selector_name_object_(None, rebuild, "GSUpdateInterface", None) 566 | -------------------------------------------------------------------------------- /Straighten All Curves.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Straighten All Curves 2 | # -*- coding: utf-8 -*- 3 | __doc__=""" 4 | Turn curve shapes into straight lines 5 | """ 6 | 7 | for p in Glyphs.font.selectedLayers[0].paths: 8 | news = [] 9 | for idx,segment in enumerate(p.segments): 10 | news.append((segment[0],segment[-1])) 11 | p.segments = news 12 | -------------------------------------------------------------------------------- /Tallest And Shortest.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Tallest and Shortest 2 | # -*- coding: utf-8 -*- 3 | __doc__=""" 4 | Open a tab with glyphs with the highest ascender and lowest descender 5 | """ 6 | from PyObjCTools.AppHelper import callAfter 7 | import GlyphsApp 8 | 9 | tallest = "" 10 | lowest = "" 11 | minBottom = 9999 12 | maxTop = -9999 13 | for glyph in Glyphs.font.glyphs: 14 | layer = glyph.layers[font.selectedFontMaster.id] 15 | top = layer.bounds.origin.y + layer.bounds.size.height 16 | bottom = layer.bounds.origin.y 17 | if bottom < minBottom: 18 | lowest = "/"+glyph.name 19 | minBottom = bottom 20 | if top > maxTop: 21 | maxTop = top 22 | tallest = "/"+glyph.name 23 | 24 | callAfter(Glyphs.currentDocument.windowController().addTabWithString_, tallest + lowest) -------------------------------------------------------------------------------- /Why You Not Compatible.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Why You Not Compatible? 2 | # -*- coding: utf-8 -*- 3 | __doc__=""" 4 | Finds compatibility issues in the selected glyphs 5 | """ 6 | 7 | Font = Glyphs.font 8 | 9 | def checkALayer(aLayer): 10 | differences = [] 11 | pathCounts = [] 12 | anchors = [] 13 | paths = [] 14 | components = [] 15 | for idx, thisLayer in enumerate(aLayer.parent.layers): 16 | pcount = len(thisLayer.paths) 17 | if idx > 0: 18 | if pcount != pathCounts[-1]: 19 | differences.append({"type": "path counts", 20 | "prevLayer": Font.selectedLayers[0].parent.layers[idx-1].name, 21 | "thisLayer": thisLayer.name, 22 | "prevItem": pathCounts[-1], 23 | "thisItem": pcount 24 | }) 25 | pathCounts.append(pcount) 26 | 27 | if len(differences) == 0: # If path counts differ all bets are off 28 | for pathIdx, path in enumerate(thisLayer.paths): 29 | if idx >0: 30 | if len(path.nodes) != len(paths[idx-1][pathIdx].nodes): 31 | differences.append({"type": "path "+str(1+pathIdx)+", node count", 32 | "prevLayer": Font.selectedLayers[0].parent.layers[idx-1].name, 33 | "thisLayer": thisLayer.name, 34 | "prevItem": len(paths[idx-1][pathIdx].nodes), 35 | "thisItem": len(path.nodes) 36 | }) 37 | else: 38 | for nodeIdx, node in enumerate(path.nodes): 39 | prevNode = paths[idx-1][pathIdx].nodes[nodeIdx] 40 | if node.type != prevNode.type: 41 | differences.append({"type": "path "+str(1+pathIdx)+", node "+str(1+nodeIdx)+ " node types", 42 | "prevLayer": Font.selectedLayers[0].parent.layers[idx-1].name, 43 | "thisLayer": thisLayer.name, 44 | "prevItem": prevNode.type, 45 | "thisItem": node.type 46 | }) 47 | paths.append(thisLayer.paths) 48 | 49 | thisAnchors = set(thisLayer.anchors.keys()) 50 | if len(anchors) > 0: 51 | diff = thisAnchors.symmetric_difference(anchors[-1]) 52 | if len(diff) > 0: 53 | differences.append({"type": "anchors", 54 | "prevLayer": Font.selectedLayers[0].parent.layers[idx-1].name, 55 | "thisLayer": thisLayer.name, 56 | "thisItem": ", ".join(thisAnchors), 57 | "prevItem": ", ".join(anchors[-1]) 58 | }) 59 | anchors.append(thisAnchors) 60 | 61 | thisComponents = set() 62 | for item in thisLayer.components: 63 | thisComponents.add(item.name) 64 | 65 | if len(components) > 0: 66 | diffComp = thisComponents.symmetric_difference(components[-1]) 67 | if len(diffComp) > 0: 68 | differences.append({"type": "components", 69 | "prevLayer": Font.selectedLayers[0].parent.layers[idx-1].name, 70 | "thisLayer": thisLayer.name, 71 | "thisItem": ", ".join(thisComponents), 72 | "prevItem": ", ".join(components[-1]) 73 | }) 74 | components.append(thisComponents) 75 | 76 | if len(differences) == 0: 77 | print(thisLayer.parent.name + " is compatible") 78 | else: 79 | print(thisLayer.parent.name + " is not compatible") 80 | for diff in differences: 81 | print(diff["type"]+" differ:") 82 | print(" "+diff["prevLayer"] + " has "+str(diff["prevItem"])) 83 | print(" "+diff["thisLayer"] + " has "+str(diff["thisItem"])) 84 | 85 | for aLayer in Font.selectedLayers: 86 | checkALayer(aLayer) 87 | print("\n") 88 | -------------------------------------------------------------------------------- /componentize.py: -------------------------------------------------------------------------------- 1 | from glyphsLib import GSFont, GSGlyph, GSComponent, load, GSLayer, GSPath 2 | from glyphsLib.types import Transform 3 | import sys 4 | from statistics import mean 5 | 6 | import argparse 7 | 8 | parser = argparse.ArgumentParser(description="Componentize a Glyphs file") 9 | parser.add_argument( 10 | "--full-matrix", 11 | choices=["identity", "translation", "uniform-scale", "free"], 12 | dest="full_matrix", 13 | default="uniform-scale", 14 | help="Allowable matrices for replacing a full glyph", 15 | ) 16 | parser.add_argument( 17 | "--partial-matrix", 18 | choices=["identity", "translation", "uniform-scale", "free"], 19 | dest="partial_matrix", 20 | default="uniform-scale", 21 | help="Allowable matrices for replacing a set of paths", 22 | ) 23 | parser.add_argument("--output", metavar="GLYPHS", help="Output glyphs file") 24 | parser.add_argument("input", metavar="GLYPHS", help="Glyphs file to componentize") 25 | args = parser.parse_args() 26 | if not args.output: 27 | args.output = args.input.replace(".glyphs", "-componentized.glyphs") 28 | 29 | try: 30 | import tqdm 31 | 32 | progress = tqdm.tqdm 33 | except ModuleNotFoundError: 34 | 35 | def progress(iterator, **kwargs): 36 | return iterator 37 | 38 | 39 | def square_error(a, b): 40 | return (a - b) ** 2 41 | 42 | 43 | def linear_regression(xs, ys): 44 | mean_x = mean(xs) 45 | mean_y = mean(ys) 46 | numer = 0 47 | denom = 0 48 | for x, y in zip(xs, ys): 49 | numer += (x - mean_x) * (y - mean_y) 50 | denom += (x - mean_x) ** 2 51 | m = numer / denom 52 | c = mean_y - (m * mean_x) 53 | 54 | # Check residuals 55 | def y_pred(x): 56 | return x * m + c 57 | 58 | t_x = mean(square_error(y_pred(x), y_true) for x, y_true in zip(xs, ys)) 59 | 60 | return m, c, t_x 61 | 62 | 63 | try: 64 | import numpy as np 65 | 66 | def linear_regression(xs, ys): 67 | x = np.array(xs) 68 | y = np.array(ys) 69 | A = np.vstack([x, np.ones(len(x))]).T 70 | m, c = np.linalg.lstsq(A, y, rcond=None)[0] 71 | y_pred = x * m + c 72 | return m, c, np.mean((y_pred - y) ** 2) 73 | 74 | except ModuleNotFoundError: 75 | pass 76 | 77 | 78 | def matrix_ok(matrix, compatibility): 79 | if compatibility == "identity": 80 | return matrix == (1, 0, 0, 1, 0, 0) 81 | if compatibility == "translation": 82 | return tuple(matrix[0:4]) == (1, 0, 0, 1) 83 | if compatibility == "uniform-scale": 84 | return matrix[0] == matrix[3] 85 | return True 86 | 87 | 88 | def GSLayer_is_fully_compatible(self, other): 89 | if self.components or other.components: 90 | return None 91 | if len(self.paths) != len(other.paths): 92 | return None 93 | if sum(len(x.nodes) for x in self.paths) < 8: 94 | # Don't bother merging 95 | return None 96 | 97 | # First check for equality 98 | my_compatible_paths = {} 99 | their_compatible_paths = {} 100 | for p1i, p1 in enumerate(self.paths): 101 | for p2i, p2 in enumerate(other.paths): 102 | matrix = p1.is_equal(p2) 103 | if matrix: 104 | my_compatible_paths[p1i] = (p2i, matrix) 105 | their_compatible_paths[p2i] = (p1i, matrix) 106 | 107 | if len(my_compatible_paths) == len(self.paths) and len( 108 | their_compatible_paths 109 | ) == len(other.paths): 110 | return (1, 0, 0, 1, 0, 0) 111 | 112 | my_compatible_paths = {} 113 | their_compatible_paths = {} 114 | last_matrix = None 115 | for p1i, p1 in enumerate(self.paths): 116 | for p2i, p2 in enumerate(other.paths): 117 | matrix = p1.is_compatible(p2) 118 | if matrix: 119 | my_compatible_paths[p1i] = (p2i, matrix) 120 | their_compatible_paths[p2i] = (p1i, matrix) 121 | if last_matrix and last_matrix != matrix: 122 | return None 123 | last_matrix = matrix 124 | break 125 | if len(my_compatible_paths) != len(self.paths): 126 | return None 127 | if len(their_compatible_paths) != len(other.paths): 128 | return None 129 | return last_matrix 130 | 131 | 132 | GSLayer.is_fully_compatible = GSLayer_is_fully_compatible 133 | 134 | 135 | def GSLayer_replace_with(self, other, matrix): 136 | self.paths = [] 137 | self.components = [GSComponent(other.name, transform=Transform(*matrix))] 138 | 139 | 140 | GSLayer.replace_with = GSLayer_replace_with 141 | 142 | 143 | def GSGlyph_replace_subset(self, other, matrices, shape_indices, reverse): 144 | masters = self.parent.masters 145 | for master, matrix in zip(masters, matrices): 146 | if reverse: 147 | matrix = (1 / matrix[0], 0, 0, 1 / matrix[3], -matrix[4], -matrix[5]) 148 | if any(abs(matrix[0]) >= 2 or abs(matrix[3]) >= 2 for matrix in matrices): 149 | return 150 | for p in shape_indices: 151 | if reverse: 152 | his_shape, my_shape = p 153 | else: 154 | my_shape, his_shape = p 155 | layer = self.layers[master.id] 156 | layer.shapes[my_shape] = GSComponent( 157 | other.name, transform=Transform(*matrix) 158 | ) 159 | 160 | 161 | GSGlyph.replace_subset = GSGlyph_replace_subset 162 | 163 | # @dataclass 164 | # class Path: 165 | # coords: GlyphCoordinates 166 | # signature: bytearray 167 | # parent: Glyph 168 | # index: int 169 | 170 | # def __post_init__(self): 171 | # x1, y1 = zip(*self.coords) 172 | # self.x_coords = np.array(x1) 173 | # self.y_coords = np.array(y1) 174 | 175 | 176 | def GSPath_is_equal(self, other): 177 | if len(self.nodes) != len(other.nodes): 178 | return None 179 | coords = [n.position for n in self.nodes] 180 | other_coords = [n.position for n in other.nodes] 181 | for i in range(len(self.nodes)): 182 | if (other_coords[i:] + other_coords[:i]) == coords: 183 | return (1, 0, 0, 1, 0, 0) 184 | return False 185 | 186 | 187 | GSPath.is_equal = GSPath_is_equal 188 | 189 | GSPath.signature = lambda self: [n.type for n in self.nodes] 190 | GSPath.index = lambda self: self.parent.paths.index(self) 191 | GSPath.shape_index = lambda self: self.parent.shapes.index(self) 192 | 193 | 194 | def GSPath_is_compatible(self, other): 195 | if self.signature() != other.signature(): 196 | return None 197 | self_x_coords = [n.position.x for n in self.nodes] 198 | self_y_coords = [n.position.y for n in self.nodes] 199 | other_x_coords = [n.position.x for n in other.nodes] 200 | other_y_coords = [n.position.y for n in other.nodes] 201 | 202 | if other_x_coords == self_x_coords and other_y_coords == self_y_coords: 203 | return (1, 0, 0, 1, 0, 0) 204 | 205 | diffs = sum( 206 | [square_error(my, their) for my, their in zip(self_x_coords, other_x_coords)] 207 | ) 208 | diffs += sum( 209 | [square_error(my, their) for my, their in zip(self_y_coords, other_y_coords)] 210 | ) 211 | if diffs / len(self.nodes) < 50: 212 | # It's a pure translation 213 | mean_x = mean(my - their for my, their in zip(self_x_coords, other_x_coords)) 214 | mean_y = mean(my - their for my, their in zip(self_y_coords, other_y_coords)) 215 | return (1, 0, 0, 1, -mean_x, -mean_y) 216 | 217 | slope1, intercept1, residual_x = linear_regression(self_x_coords, other_x_coords) 218 | if residual_x > 10: 219 | return None 220 | slope2, intercept2, residual_y = linear_regression(self_y_coords, other_y_coords) 221 | if residual_y > 10: 222 | return None 223 | 224 | return ( 225 | round(slope1 * 1000) / 1000, 226 | 0, 227 | 0, 228 | round(slope2 * 1000) / 1000, 229 | int(intercept1), 230 | int(intercept2), 231 | ) 232 | 233 | 234 | GSPath.is_compatible = GSPath_is_compatible 235 | 236 | 237 | def GSPath_is_compatible_in_all_masters(self, other, compatibility): 238 | font = self.parent.parent.parent 239 | matrices = [] 240 | g1 = self.parent.parent 241 | g2 = other.parent.parent 242 | for master in font.masters: 243 | l1 = g1.layers[master.id] 244 | l2 = g2.layers[master.id] 245 | p1 = l1.paths[self.index()] 246 | p2 = l2.paths[other.index()] 247 | matrix = p1.is_compatible(p2) 248 | if not matrix or not matrix_ok(matrix, compatibility): 249 | return None 250 | matrices.append(matrix) 251 | return matrices 252 | 253 | 254 | GSPath.is_compatible_in_all_masters = GSPath_is_compatible_in_all_masters 255 | 256 | 257 | def build_paths_by_sig(glyphs): 258 | paths_by_sig = {} 259 | for glyph in glyphs.values(): 260 | layer = glyph.layers[0] 261 | for p in layer.paths: 262 | paths_by_sig.setdefault(tuple(p.signature()), []).append(p) 263 | 264 | to_strip = [] 265 | for sig, paths in paths_by_sig.items(): 266 | if len(paths) < 2 or len(sig) <= 4: 267 | to_strip.append(sig) 268 | for sig in to_strip: 269 | del paths_by_sig[sig] 270 | return paths_by_sig 271 | 272 | 273 | font = load(args.input) 274 | masters = [x.id for x in font.masters] 275 | 276 | edited = set() 277 | 278 | sharable_paths = {} 279 | 280 | print("Searching for exact componentable glyphs") 281 | glyphlist: list[GSGlyph] = list(font.glyphs) 282 | for ix, g1 in progress(enumerate(glyphlist), total=len(glyphlist)): 283 | for g2 in glyphlist[ix + 1 :]: 284 | matrices = [] 285 | for master in masters: 286 | l1 = g1.layers[master] 287 | if not l1.paths: 288 | continue 289 | l2 = g2.layers[master] 290 | if not l2.paths: 291 | continue 292 | matrix = l2.is_fully_compatible(l1) 293 | if matrix and matrix_ok(matrix, args.full_matrix): 294 | matrices.append((l2, l1, matrix)) 295 | else: 296 | break 297 | if len(matrices) == len(masters): 298 | print( 299 | "Replacing %s with %s" 300 | % (matrices[0][0].parent.name, matrices[0][1].parent.name) 301 | ) 302 | for l1, l2, matrix in matrices: 303 | l2.replace_with(l1, matrix) 304 | edited.add(l2) 305 | elif len(matrices) > len(masters) / 2: 306 | print( 307 | "%s was compatible with %s in %i masters; not replacing, check manually" 308 | % ( 309 | matrices[0][0].parent.name, 310 | matrices[0][1].parent.name, 311 | len(matrices), 312 | ) 313 | ) 314 | 315 | paths_by_sig = build_paths_by_sig(font.glyphs) 316 | print("Searching for partially componentable glyphs") 317 | mergelist = {} 318 | 319 | for pathlists in progress(paths_by_sig.values()): 320 | for i, path1 in enumerate(pathlists): 321 | for path2 in pathlists[i + 1 :]: 322 | if path1.parent == path2.parent: 323 | continue 324 | matrices = path1.is_compatible_in_all_masters(path2, args.partial_matrix) 325 | if matrices: 326 | mergelist.setdefault( 327 | (path1.parent.parent.name, path2.parent.parent.name), [] 328 | ).append([path1.shape_index(), path2.shape_index(), matrices]) 329 | 330 | for g1, g2 in mergelist.keys(): 331 | if g1 in edited or g2 in edited: 332 | continue 333 | paths = mergelist[(g1, g2)] 334 | marked_left = set() 335 | marked_right = set() 336 | # Find the largest subset of identical matrix 337 | by_matrix = {} 338 | for p1, p2, matrix in paths: 339 | by_matrix.setdefault(tuple(matrix), []).append((p1, p2)) 340 | for matrices, path_indices in sorted(by_matrix.items(), key=lambda k: -len(k[1])): 341 | if len(path_indices) == len(font.glyphs[g1].layers[0].shapes): 342 | print(f"{g2} contains {g1}") 343 | font.glyphs[g2].replace_subset( 344 | font.glyphs[g1], matrices, path_indices, True 345 | ) 346 | elif len(path_indices) == len(font.glyphs[g2].layers[0].shapes): 347 | print(f"{g1} contains {g2}") 348 | font.glyphs[g1].replace_subset( 349 | font.glyphs[g2], matrices, path_indices, False 350 | ) 351 | 352 | font.save(args.output) 353 | -------------------------------------------------------------------------------- /glyphmonkey.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: 2 | # -*- coding: utf-8 -*- 3 | __doc__="" 4 | import GlyphsApp 5 | from GlyphsApp import Proxy 6 | from math import atan2, sqrt, cos, sin, radians, degrees 7 | from Foundation import NSMakePoint, NSValue, NSMakeRect 8 | 9 | # Make GSNodes hashable 10 | 11 | class GSLineSegment(object): 12 | def __init__(self, tuple = None, owner = None, idx = 0): 13 | self._seg = tuple 14 | if not self._seg: self._seg = owner._segments[idx] 15 | self._owner = owner 16 | self._owneridx = idx 17 | 18 | def __repr__(self): 19 | """Return list-lookalike of representation string of objects""" 20 | return "" % (self.start.x,self.start.y,self.end.x,self.end.y) 21 | 22 | def _seg(self): return self._seg 23 | @property 24 | def start(self): return self._seg[0].position 25 | @property 26 | def end(self): return self._seg[-1].position 27 | 28 | # For backward compatibility 29 | def __getitem__(self, Key): 30 | if Key < 0: 31 | Key = self.__len__() + Key 32 | # There is a horribly subtle distinction between an NSValue and 33 | # an NSPoint. SpeedPunk expects to see an NSValue here and dies 34 | # if it doesn't have one. 35 | return NSValue.valueWithPoint_(self._seg[Key].position) 36 | 37 | @property 38 | def area(self): 39 | xa, ya = self.start.x, self.start.y/20 40 | xb, yb = xa, ya 41 | xc, yc = self.end.x, self.end.y/20 42 | xd, yd = xc, yc 43 | return (xb-xa)*(10*ya + 6*yb + 3*yc + yd) + (xc-xb)*( 4*ya + 6*yb + 6*yc + 4*yd) +(xd-xc)*( ya + 3*yb + 6*yc + 10*yd) 44 | 45 | @property 46 | def length(self): 47 | l1 = self.start.x - self.end.x 48 | l2 = self.start.y - self.end.y 49 | return sqrt(l1 * l1 + l2 * l2) 50 | 51 | @property 52 | def angle(self): 53 | e = self.end 54 | s = self.start 55 | return degrees(atan2(e.y - s.y, e.x - s.x)) 56 | 57 | @property 58 | def selected(self): 59 | return self.start.selected and self.end.selected 60 | 61 | def reverse(self): 62 | self._seg = self._seg[::-1] 63 | 64 | def _interpolatenspoint(self, p1, p2, t): 65 | dx = (p2.x - p1.x) * t 66 | dy = (p2.y - p1.y) * t 67 | return NSMakePoint(p1.x + dx, p1.y + dy) 68 | 69 | def interpolate(self, other, t): 70 | if type(other) != type(self): raise TypeError 71 | l = [] 72 | for i in range(0, len(self)): 73 | newnode = self._seg[i].copy() # Urgh 74 | newnode.position = self._interpolatenspoint(self._seg[i].position, other._seg[i].position, t) 75 | l.append(newnode) 76 | return type(self)(l) 77 | 78 | def __len__(self): 79 | return 2 80 | 81 | class GSCurveSegment(GSLineSegment): 82 | def __repr__(self): 83 | return "" % ( 84 | self.start.x, self.start.y, 85 | self.handle1.x, self.handle1.y, 86 | self.handle2.x, self.handle2.y, 87 | self.end.x,self.end.y 88 | ) 89 | 90 | @property 91 | def handle1(self): return self._seg[1].position 92 | @property 93 | def handle2(self): return self._seg[2].position 94 | 95 | @property 96 | def area(self): 97 | xa, ya = self.start.x, self.start.y/20 98 | xb, yb = self.handle1.x, self.handle1.y/20 99 | xc, yc = self.handle2.x, self.handle2.y/20 100 | xd, yd = self.end.x, self.end.y/20 101 | return (xb-xa)*(10*ya + 6*yb + 3*yc + yd) + (xc-xb)*( 4*ya + 6*yb + 6*yc + 4*yd) +(xd-xc)*( ya + 3*yb + 6*yc + 10*yd) 102 | 103 | def angle(self): # XXX This is wrong 104 | e = self.end 105 | s = self.start 106 | return degrees(atan2(e.y - s.y, e.x - s.x)) 107 | 108 | def interpolate_at_fraction(self, t): 109 | if t < 0 or t > 1: 110 | raise Exception("interpolate_at_fraction should be called with a number between 0 and 1") 111 | t1 = 1.0 - t; 112 | t1_3 = t1*t1*t1 113 | t1_3a = (3*t)*(t1*t1) 114 | t1_3b = (3*(t*t))*t1; 115 | t1_3c = (t * t * t ) 116 | x = (self.start.x * t1_3) + (t1_3a * self.handle1.x) + (t1_3b * self.handle2.x) + (t1_3c * self.end.x) 117 | y = (self.start.y * t1_3) + (t1_3a * self.handle1.y) + (t1_3b * self.handle2.y) + (t1_3c * self.end.y) 118 | return (x,y) 119 | 120 | @property 121 | def length(self): 122 | steps = 50 123 | t = 0.0 124 | length = 0 125 | previous = () 126 | while t < 1.0: 127 | this = self.interpolate_at_fraction(t) 128 | if t > 0: 129 | dx = previous[0] - this[0] 130 | dy = previous[1] - this[1] 131 | length = length + sqrt(dx*dx+dy*dy) 132 | t = t + 1.0/steps 133 | previous = this 134 | return length 135 | 136 | def __len__(self): 137 | return 4 138 | 139 | class PathSegmentsProxy (Proxy): 140 | # Actually we're not going to use .segments at all, because we 141 | # want to be able to access things like GSNode.selected 142 | def toSegments(p): 143 | segList = [] 144 | nodeList = p._owner.nodes 145 | thisSeg = (nodeList[-1],) 146 | for i in range(0,len(nodeList)): 147 | thisSeg = thisSeg + (nodeList[i],) 148 | if nodeList[i].type != GlyphsApp.GSOFFCURVE: 149 | segList.append(thisSeg) 150 | thisSeg = (nodeList[i],) 151 | return segList 152 | def __getitem__(self, Key): 153 | if Key < 0: 154 | Key = self.__len__() + Key 155 | segs = self.toSegments() 156 | if len(segs[Key]) == 2: 157 | return GSLineSegment( owner = self._owner, idx = Key, tuple = segs[Key]) 158 | else: 159 | return GSCurveSegment( owner = self._owner, idx = Key, tuple = segs[Key]) 160 | def __setitem__(self, Key, Layer): 161 | if Key < 0: 162 | Key = self.__len__() + Key 163 | # XXX 164 | def __len__(self): 165 | return len(self.toSegments()) 166 | def values(self): 167 | return map(self.__getitem__, range(0,self.__len__())) 168 | 169 | # Unfortunately working with segments doesn't always *work*. So we 170 | # map a segment list to a node list 171 | GSNode = GlyphsApp.GSNode 172 | 173 | def toNodeList(segments): 174 | nodelist = [] 175 | closed = (segments[-1].end.x == segments[0].start.x and segments[-1].end.y == segments[0].start.y) 176 | nodelist.append( GSNode((segments[0].start.x, segments[0].start.y), GlyphsApp.GSLINE) ) 177 | for i in range(0,len(segments)): 178 | s = segments[i] 179 | t = GlyphsApp.GSCURVE 180 | c = GlyphsApp.GSSMOOTH 181 | if type(s) is GSLineSegment: 182 | t = GlyphsApp.GSLINE 183 | else: 184 | s1 = s.handle1 185 | nodelist.append(GSNode((s1.x,s1.y), type = GlyphsApp.GSOFFCURVE)) 186 | s2 = s.handle2 187 | nodelist.append(GSNode((s2.x,s2.y), type = GlyphsApp.GSOFFCURVE)) 188 | 189 | ns = i+1 190 | if ns >= len(segments): ns = 0 191 | if type(segments[ns]) is GSLineSegment: 192 | c = GlyphsApp.GSSHARP 193 | e = s.end 194 | node = GSNode((e.x, e.y), t) 195 | node.connection = c 196 | nodelist.append(node) 197 | 198 | n = segments[0].start 199 | 200 | return nodelist 201 | 202 | GlyphsApp.GSPath.segments = property( lambda self: PathSegmentsProxy(self), 203 | lambda self, value: 204 | self.setNodes_(toNodeList(value)) 205 | ) 206 | 207 | def nodeRotate(self, ox, oy, angle): 208 | angle = radians(angle) 209 | newX = ox + (self.position.x-ox)*cos(angle) - (self.position.y-oy)*sin(angle) 210 | newY = oy + (self.position.x-ox)*sin(angle) + (self.position.y-oy)*cos(angle) 211 | self.position = (round(newX,2), round(newY,2)) 212 | 213 | def nodeReflect(self, p0, p1): 214 | dx = p1.x - p0.x 215 | dy = p1.y - p0.y 216 | a = (dx * dx - dy * dy) / (dx * dx + dy * dy) 217 | b = 2 * dx * dy / (dx * dx + dy * dy) 218 | x = a * (self.position.x - p0.x) + b * (self.position.y - p0.y) + p0.x 219 | y = b * (self.position.x - p0.x) - a * (self.position.y - p0.y) + p0.y 220 | self.position =(round(x,2), round(y,2)) 221 | 222 | def nodeInterpolate(self, other, t): 223 | dx = (other.position.x - self.position.x) * t 224 | dy = (other.position.y - self.position.y) * t 225 | new = self.copy() 226 | new.position = (self.position.x + dx, self.position.y + dy) 227 | return new 228 | 229 | GlyphsApp.GSNode.rotate = nodeRotate 230 | GlyphsApp.GSNode.reflect = nodeReflect 231 | GlyphsApp.GSNode.interpolate = nodeInterpolate 232 | 233 | def _layerCenter(self): 234 | bounds = self.bounds 235 | ox = bounds.origin.x + bounds.size.width / 2 236 | oy = bounds.origin.y + bounds.size.height / 2 237 | return NSMakePoint(ox, oy) 238 | GlyphsApp.GSLayer.center = _layerCenter 239 | 240 | def horizontalCenterOfWeight(self): 241 | sampleX = self.bounds.origin.x 242 | sampleHeight = 5 243 | stripAreas = [] 244 | totalArea = 0 245 | p = self.bezierPath() 246 | 247 | # This is slow and stupid but it works 248 | while sampleX < self.bounds.origin.x + self.bounds.size.width: 249 | sampleY = self.bounds.origin.y 250 | thisArea = 0 251 | while sampleY < self.bounds.origin.y + self.bounds.size.height: 252 | if p.containsPoint_([sampleX, sampleY]): 253 | thisArea = thisArea + 1 254 | sampleY = sampleY + sampleHeight 255 | stripAreas.append(thisArea) 256 | totalArea = totalArea + thisArea 257 | sampleX = sampleX + 1 258 | 259 | area = 0 260 | for i, v in enumerate(stripAreas): 261 | area = area + v 262 | if area > totalArea /2 : 263 | return i 264 | 265 | GlyphsApp.GSLayer.horizontalCenterOfWeight = horizontalCenterOfWeight 266 | 267 | def horizontalOpticalCenter(self): 268 | cw = self.horizontalCenterOfWeight() 269 | tc = self.center()[0] 270 | return tc + (cw-tc) / 3 # This is an approximation, obviously... 271 | GlyphsApp.GSLayer.horizontalOpticalCenter = horizontalOpticalCenter 272 | 273 | ### additional GSPath methods 274 | 275 | def layerCenter(self): 276 | return self.parent.center() 277 | 278 | def pathCenter(self): 279 | bounds = self.bounds 280 | ox = bounds.origin.x + bounds.size.width / 2 281 | oy = bounds.origin.y + bounds.size.height / 2 282 | return NSMakePoint(ox, oy) 283 | 284 | def pathRotate(self, angle=-1, ox=-1, oy=-1): 285 | if angle == -1: angle = 180 286 | if ox == -1 and oy == -1: 287 | if self.parent: # Almost always 288 | ox, oy = self.layerCenter().x, self.layerCenter().y 289 | else: 290 | ox, oy = self.center().x, self.center().y 291 | 292 | for n in self.nodes: 293 | n.rotate(ox, oy, angle) 294 | return self 295 | 296 | def pathReflect(self, p0 = -1, p1 = -1): 297 | if p0 == -1 and p1 == -1: 298 | if self.parent: # Almost always 299 | p0 = self.layerCenter() 300 | p1 = self.layerCenter() 301 | else: 302 | p0 = self.center() 303 | p1 = self.center() 304 | p1.y = p1.y + 100 305 | 306 | for n in self.nodes: 307 | n.reflect(p0, p1) 308 | return self 309 | 310 | def pathDiff(p1, p2): 311 | nodes1 = set((n.position.x,n.position.y) for n in p1.nodes) 312 | nodes2 = set((n.position.x,n.position.y) for n in p2.nodes) 313 | return nodes1 - nodes2 314 | 315 | def pathEqual(p1, p2): 316 | pd = pathDiff(p1, p2) 317 | return len(pd) == 0 318 | 319 | def pathToNodeSet(self): 320 | return GSNodeSet(self.nodes) 321 | 322 | GlyphsApp.GSPath.layerCenter = layerCenter 323 | GlyphsApp.GSPath.center = pathCenter 324 | GlyphsApp.GSPath.rotate = pathRotate 325 | GlyphsApp.GSPath.reflect = pathReflect 326 | GlyphsApp.GSPath.equal = pathEqual 327 | GlyphsApp.GSPath.diff = pathDiff 328 | GlyphsApp.GSPath.toNodeSet = pathToNodeSet 329 | 330 | class GSNodeSet(object): 331 | def toKey(self,n): 332 | return "%s %s %s" % (n.position.x, n.position.y, n.type) 333 | 334 | def __init__(self, nodes): 335 | self._dict = {} 336 | for n in nodes: 337 | self._dict[self.toKey(n)] = n 338 | 339 | def __repr__(self): 340 | return "" % (len(self)) 341 | 342 | def __len__(self): 343 | return len(self._dict) 344 | 345 | @property 346 | def nodes(self): 347 | return self._dict.values() 348 | 349 | @property 350 | def bounds(self): 351 | minx, maxx, miny, maxy = None, None, None, None 352 | if len(self) < 1: return None 353 | for p in self.nodes: 354 | pos = p.position 355 | if minx == None or pos.x < minx: minx = pos.x 356 | if maxx == None or pos.x > maxx: maxx = pos.x 357 | if miny == None or pos.y < minx: miny = pos.y 358 | if maxy == None or pos.y > maxx: maxy = pos.y 359 | 360 | return NSMakeRect(minx, miny, maxx-minx, maxy-miny) 361 | 362 | @property 363 | def center(self): 364 | if len(self) < 1: return None 365 | b = self.bounds 366 | return NSMakePoint(b.origin.x + b.size.width / 2, b.origin.y + b.size.height / 2, ) 367 | 368 | def copy(self): 369 | return GSNodeSet(n.copy() for n in self.nodes) 370 | 371 | def diff(ns1, ns2): 372 | nodes1 = set((n.position.x,n.position.y) for n in ns1.nodes) 373 | nodes2 = set((n.position.x,n.position.y) for n in ns2.nodes) 374 | return nodes1 - nodes2 375 | 376 | def equal(p1, p2): 377 | pd = p1.diff(p2) 378 | return len(pd) == 0 379 | 380 | def rotate(self, angle=-1, ox=-1, oy=-1): 381 | if angle == -1: angle = 180 382 | if ox == -1 and oy == -1: 383 | ox, oy = self.center.x, self.center.y 384 | 385 | for n in self.nodes: 386 | n.rotate(ox, oy, angle) 387 | return self 388 | 389 | def reflect(self, p0 = -1, p1 = -1): 390 | if p0 == -1 and p1 == -1: 391 | p0 = self.center 392 | p1 = self.center 393 | p1.y = p1.y + 100 394 | 395 | for n in self.nodes: 396 | n.reflect(p0, p1) 397 | return self 398 | 399 | def selectedNodeSet(layer): 400 | sel = [] 401 | for n in layer.selection: 402 | if isinstance(n, GSNode): 403 | sel.append(n) 404 | return GSNodeSet(sel) 405 | 406 | GlyphsApp.GSLayer.selectedNodeSet = selectedNodeSet 407 | 408 | # Does p have rotational symmetry? 409 | # ox, oy = p.layerCenter() 410 | # p.equal(p.copy().rotate(angle=180, ox=ox, oy=oy)) 411 | --------------------------------------------------------------------------------