├── .gitignore ├── DrawBot ├── Ellipse.py ├── EllipseSuper.py └── simon.txt ├── LICENSE ├── NibSimulator.glyphsReporter └── Contents │ ├── Info.plist │ ├── MacOS │ └── plugin │ └── Resources │ └── plugin.py ├── README.md ├── build_glyphs_plugin.sh ├── demo ├── NibSimulator.glyphs └── NibSimulator.ufo │ ├── fontinfo.plist │ ├── glyphs.background │ ├── c.glif │ ├── ccedilla.glif │ ├── contents.plist │ ├── l.glif │ ├── layerinfo.plist │ ├── m.glif │ ├── n.glif │ ├── o.glif │ └── ograve.glif │ ├── glyphs │ ├── c.glif │ ├── ccedilla.glif │ ├── contents.plist │ ├── l.glif │ ├── layerinfo.plist │ ├── m.glif │ ├── n.glif │ ├── o.glif │ └── ograve.glif │ ├── layercontents.plist │ ├── lib.plist │ └── metainfo.plist ├── images ├── formula-by-simon-cozens.jpeg ├── rectangle-rgb.png └── superellipse.png ├── lib └── nibLib │ ├── __init__.py │ ├── geometry.py │ ├── pens │ ├── __init__.py │ ├── bezier.py │ ├── drawing.py │ ├── nibPen.py │ ├── ovalNibPen.py │ ├── rectNibPen.py │ └── superellipseNibPen.py │ ├── typing.py │ └── ui │ ├── __init__.py │ ├── glyphs.py │ └── robofont.py ├── scripts ├── Glyphs - Add nib guide layer from background.py ├── NibLibG.py └── NibLibRF.py ├── setup.cfg ├── setup.py └── tests └── geometry_test.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | venv/ 3 | *.egg-info 4 | dist/ 5 | build/ 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /DrawBot/Ellipse.py: -------------------------------------------------------------------------------- 1 | #a = 300 2 | #b = 200 3 | 4 | #phi = radians(45) 5 | #alpha = 30 6 | 7 | #nib_angle = 5 8 | 9 | Variable([ 10 | dict(name="a", ui="Slider", 11 | args=dict( 12 | value=300, 13 | minValue=0, 14 | maxValue=500)), 15 | dict(name="b", ui="Slider", 16 | args=dict( 17 | value=200, 18 | minValue=0, 19 | maxValue=500)), 20 | dict(name="alpha", ui="Slider", 21 | args=dict( 22 | value=45, 23 | minValue=0, 24 | maxValue=180)), 25 | dict(name="phi", ui="Slider", 26 | args=dict( 27 | value=45, 28 | minValue=0, 29 | maxValue=180)), 30 | dict(name="nib_angle", ui="Slider", 31 | args=dict( 32 | value=30, 33 | minValue=-90, 34 | maxValue=90)), 35 | ], globals()) 36 | 37 | alpha = radians(alpha) 38 | phi = radians(phi) 39 | nib_angle = radians(nib_angle) 40 | 41 | def _get_point_on_ellipse_at_tangent_angle(a, b, alpha): 42 | 43 | print "Tangent angle: %0.2f°" % degrees(alpha) 44 | 45 | t = atan2(- b , (a * tan(alpha))) 46 | t = atan(-b /(a * tan(alpha))) 47 | print "Parameter t: %0.2f" % (t + nib_angle) 48 | 49 | return _get_point_on_ellipse_at_t(a, b, t) 50 | 51 | 52 | def _get_point_on_ellipse_at_t(a, b, t): 53 | 54 | # t is not the angle, it is just a parameter 55 | 56 | x = a * cos(t) 57 | y = b * sin(t) 58 | 59 | return x, y 60 | 61 | 62 | def _get_point_on_ellipse_at_angle_from_center(a, b, phi): 63 | 64 | # This time, phi is the real center angle 65 | 66 | div = sqrt(a**2 * tan(phi) ** 2 / b ** 2 + 1) 67 | 68 | x = a / div 69 | y = a * tan(phi) / div 70 | 71 | return x, y 72 | 73 | 74 | def draw_arrowhead(size=10, x=0, y=0): 75 | line((x-size, y+size), (x, y)) 76 | line((x, y), (x-size, y-size)) 77 | 78 | def draw_cross(size=10, x=0, y=0, phi=45): 79 | save() 80 | rotate(phi) 81 | line((x-size, y+size), (x+size, y-size)) 82 | line((x+size, y+size), (x-size, y-size)) 83 | restore() 84 | 85 | 86 | newDrawing() 87 | size(1000, 1000) 88 | fill(None) 89 | stroke(0) 90 | strokeWidth(0.25) 91 | 92 | translate(500, 500) 93 | save() 94 | rotate(degrees(nib_angle)) 95 | line((-500, 0), (500, 0)) 96 | line((0, 500), (0, -500)) 97 | oval(-a, -b, 2*a, 2*b) 98 | restore() 99 | 100 | x, y = _get_point_on_ellipse_at_angle_from_center(a, b, phi) 101 | print "Center angle: %0.2f°" % degrees(phi) 102 | save() 103 | stroke(1, 0, 0) 104 | rotate(degrees(nib_angle)) 105 | line((0, 0), (x, y)) 106 | rect(x-1, y-1, 2, 2) 107 | restore() 108 | 109 | stroke(0, 0, 1) 110 | x, y = _get_point_on_ellipse_at_tangent_angle(a, b, alpha - nib_angle) 111 | print "Point on ellipse: %0.2f | %0.2f (ellipse reference system)" % (x, y) 112 | save() 113 | stroke(0,1,0) 114 | rotate(degrees(nib_angle)) 115 | line((0, 0), (x, y)) 116 | rect(x-1, y-1, 2, 2) 117 | restore() 118 | print "Nib angle: %0.2f°" % degrees(nib_angle) 119 | 120 | # Calculate the un-transformed point coordinates 121 | xnew = x * cos(nib_angle) - y * sin(nib_angle) 122 | ynew = x * sin(nib_angle) + y * cos(nib_angle) 123 | print "Point on ellipse: %0.2f | %0.2f" % (xnew, ynew) 124 | 125 | line((0, 0), (xnew, ynew)) 126 | rect(xnew-1, ynew-1, 2, 2) 127 | translate(xnew, ynew) 128 | 129 | save() 130 | rotate(degrees(alpha)) 131 | 132 | line((-200, 0), (200, 0)) 133 | draw_cross(5, phi = degrees(-alpha)) 134 | draw_arrowhead(5, -200) 135 | draw_arrowhead(5, 200) 136 | 137 | # draw the center angle arc 138 | 139 | rotate(degrees(-alpha)) 140 | stroke(1, 0, 1, 0.2) 141 | strokeWidth(1) 142 | line((0, 0), (100, 0)) 143 | 144 | newPath() 145 | arc((0, 0), 80, 0, degrees(alpha), False) 146 | drawPath() 147 | strokeWidth(0) 148 | fill(1, 0, 1, 0.5) 149 | fontSize(12) 150 | font("Times-Italic") 151 | text("𝛼 = %0.1f°" % degrees(alpha), (20, 10)) 152 | restore() 153 | -------------------------------------------------------------------------------- /DrawBot/EllipseSuper.py: -------------------------------------------------------------------------------- 1 | import cmath 2 | 3 | # a = 300 4 | # b = 200 5 | 6 | # phi = radians(45) 7 | # alpha = 30 8 | 9 | # nib_angle = 5 10 | 11 | Variable([ 12 | dict(name="a", ui="Slider", 13 | args=dict( 14 | value=300, 15 | minValue=0, 16 | maxValue=500)), 17 | dict(name="b", ui="Slider", 18 | args=dict( 19 | value=200, 20 | minValue=0, 21 | maxValue=500)), 22 | dict(name="n", ui="Slider", 23 | args=dict( 24 | value=2.5, 25 | minValue=2, 26 | maxValue=10)), 27 | dict(name="phi", ui="Slider", 28 | args=dict( 29 | value=45, 30 | minValue=0, 31 | maxValue=360)), 32 | dict(name="alpha", ui="Slider", 33 | args=dict( 34 | value=45, 35 | minValue=0, 36 | maxValue=360)), 37 | dict(name="nib_angle", ui="Slider", 38 | args=dict( 39 | value=0, 40 | minValue=-90, 41 | maxValue=90)), 42 | ], globals()) 43 | 44 | alpha = radians(alpha) 45 | nib_angle = radians(nib_angle) 46 | phi = radians(phi) 47 | 48 | 49 | # Path optimization tools 50 | 51 | def distanceBetweenPoints(p0, p1, doRound=False): 52 | # Calculate the distance between two points 53 | d = sqrt((p0[0] - p1[0]) ** 2 + (p0[1] - p1[1]) ** 2) 54 | if doRound: 55 | return int(round(d)) 56 | else: 57 | return d 58 | 59 | 60 | class Triangle(object): 61 | def __init__(self, A, B, C): 62 | self.A = A 63 | self.B = B 64 | self.C = C 65 | 66 | def sides(self): 67 | self.a = distanceBetweenPoints(self.B, self.C) 68 | self.b = distanceBetweenPoints(self.A, self.C) 69 | self.c = distanceBetweenPoints(self.A, self.B) 70 | return self.a, self.b, self.c 71 | 72 | def height_a(self): 73 | a, b, c = self.sides() 74 | s = (a + b + c) / 2 75 | h = 2 * sqrt(s * (s-a) * (s-b) * (s-c)) / a 76 | return h 77 | 78 | 79 | def optimizePointPath(p, dist=0.49): 80 | print("Input number of points:", len(p)) 81 | num_points = len(p) 82 | p0 = p[0] 83 | optimized = [p0] 84 | i = 0 85 | j = 1 86 | while i < num_points -2: 87 | p1 = p[i+1] 88 | p2 = p[i+2] 89 | t = Triangle(p0, p2, p1) 90 | h = t.height_a() 91 | # print(i, h) 92 | if t.height_a() > dist: 93 | optimized.extend([p1]) 94 | p0 = p[i] 95 | else: 96 | pass 97 | # print("Skip:", i+1, p1) 98 | i += 1 99 | j += 1 100 | # if j > 13: 101 | # break 102 | optimized.extend([p[-1]]) 103 | print("Optimized number of points:", len(optimized)) 104 | return optimized 105 | 106 | 107 | def get_superellipse_points(a, b, n, alpha=0, steps=100): 108 | points = [] 109 | for i in range(0, steps + 1): 110 | t = i * 0.5 * pi / steps 111 | points.append(( 112 | a * cos(t) ** (2 / n) * cos(alpha) - b * sin(t) ** (2 / n) * sin(alpha), 113 | a * cos(t) ** (2 / n) * sin(alpha) + b * sin(t) ** (2 / n) * cos(alpha), 114 | )) 115 | try: 116 | points = optimizePointPath(points, 1) 117 | except: 118 | print("oops") 119 | pass 120 | points.extend([(-p[0], p[1]) for p in reversed(points)]) 121 | points.extend([(-p[0], -p[1]) for p in reversed(points)]) 122 | return points 123 | 124 | def superellipse(a, b, n, alpha, steps=100): 125 | points = get_superellipse_points(a, b, n, 0, steps) 126 | save() 127 | rotate(degrees(alpha)) 128 | newPath() 129 | moveTo(points[0]) 130 | for p in points[1:]: 131 | lineTo(p) 132 | closePath() 133 | drawPath() 134 | restore() 135 | 136 | # def _get_point_on_ellipse_at_tangent_angle(a, b, alpha): 137 | # 138 | # phi = atan2( 139 | # - b, 140 | # a * tan(alpha) 141 | # ) 142 | # 143 | # x = a * cos(phi) 144 | # y = b * sin(phi) 145 | # 146 | # return x, y 147 | 148 | def _get_point_on_superellipse_at_tangent_angle(a, b, n, alpha): 149 | 150 | print("a =", a) 151 | print("b =", b) 152 | 153 | exp = 1 / (2 - 2 / n) + 0j 154 | print("Exponent:", exp) 155 | 156 | factor1 = (-b / a) ** -exp 157 | factor2 = cmath.tan(alpha) ** exp 158 | print("factor1 =", factor1) 159 | print("factor2 =", factor2) 160 | phi = atan2( 161 | 1, 162 | (factor1 * factor2).real, 163 | ) 164 | 165 | print("phi =", degrees(phi)) 166 | 167 | return _get_point_on_superellipse_at_angle_from_center(a, b, n, phi) 168 | 169 | 170 | def _get_point_on_superellipse_at_angle_from_center(a, b, n, phi): 171 | 172 | x = a * cos(phi) ** (2 / n + 0j) 173 | y = b * sin(phi) ** (2 / n + 0j) 174 | 175 | print(x, y) 176 | 177 | return x.real, y.real 178 | 179 | 180 | def draw_arrowhead(size=10, x=0, y=0): 181 | line((x-size, y+size), (x, y)) 182 | line((x, y), (x-size, y-size)) 183 | 184 | 185 | def draw_cross(size=10, x=0, y=0, phi=45): 186 | save() 187 | rotate(phi) 188 | line((x-size, y+size), (x+size, y-size)) 189 | line((x+size, y+size), (x-size, y-size)) 190 | restore() 191 | 192 | 193 | print("Superellipse with n = %0.2f" % n) 194 | 195 | newDrawing() 196 | size(1000, 1000) 197 | fill(None) 198 | stroke(0) 199 | strokeWidth(0.25) 200 | 201 | translate(500, 500) 202 | save() 203 | rotate(degrees(nib_angle)) 204 | stroke(1, 0, 0) 205 | line((-500, 0), (500, 0)) 206 | line((0, 500), (0, -500)) 207 | stroke(1, 1, 0) 208 | oval(-a, -b, 2*a, 2*b) 209 | rotate(degrees(-nib_angle)) 210 | stroke(0) 211 | strokeWidth(1) 212 | superellipse(a, b, n, nib_angle) 213 | restore() 214 | 215 | save() 216 | rotate(degrees(nib_angle)) 217 | stroke(0, 1, 0) 218 | x, y = _get_point_on_superellipse_at_angle_from_center(a, b, n, phi) 219 | line((0, 0), (x, y)) 220 | rect(x-1, y-1, 2, 2) 221 | restore() 222 | 223 | stroke(0, 0, 1) 224 | x, y = _get_point_on_superellipse_at_tangent_angle(a, b, n, alpha - nib_angle) 225 | print(x, y) 226 | save() 227 | stroke(0,1,0) 228 | rotate(degrees(nib_angle)) 229 | line((0, 0), (x, y)) 230 | rect(x-1, y-1, 2, 2) 231 | restore() 232 | # print("Nib angle:", degrees(nib_angle)) 233 | 234 | # Calculate the un-transformed point coordinates 235 | xnew = x * cos(nib_angle) - y * sin(nib_angle) 236 | ynew = x * sin(nib_angle) + y * cos(nib_angle) 237 | # print(xnew, ynew) 238 | 239 | line((0, 0), (xnew, ynew)) 240 | rect(xnew-1, ynew-1, 2, 2) 241 | translate(xnew, ynew) 242 | 243 | save() 244 | rotate(degrees(alpha)) 245 | 246 | line((-300, 0), (300, 0)) 247 | draw_cross(5, phi = degrees(-alpha)) 248 | draw_arrowhead(5, -300) 249 | draw_arrowhead(5, 300) 250 | 251 | # draw the center angle arc 252 | 253 | rotate(degrees(-alpha)) 254 | stroke(1, 0, 1, 0.2) 255 | strokeWidth(1) 256 | line((0, 0), (100, 0)) 257 | 258 | newPath() 259 | arc((0, 0), 80, 0, degrees(alpha), False) 260 | drawPath() 261 | strokeWidth(0) 262 | fill(1, 0, 1, 0.5) 263 | fontSize(12) 264 | font("Times-Italic") 265 | text("𝛼 = %0.1f°" % degrees(alpha), (20, 10)) 266 | 267 | restore() 268 | -------------------------------------------------------------------------------- /DrawBot/simon.txt: -------------------------------------------------------------------------------- 1 | https://twitter.com/simoncozens/status/925601356634767361 2 | 3 | That last operation (cot^-1) is arccot, also known as arctan(1/θ). In python, you'll want math.atan2(θ,1). 4 | 5 | And so I *think* the relevant Python code is 6 | atan2(1,((-b/a)**(-1/(2-2/n)) * tan(alpha) ** (1/(2-2/n))).real) 7 | (Note the .real!) 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Jens Kutilek 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 | -------------------------------------------------------------------------------- /NibSimulator.glyphsReporter/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | plugin 9 | CFBundleIdentifier 10 | de.kutilek.NibSimulator 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | NibSimulator 15 | CFBundleShortVersionString 16 | 1.2 17 | CFBundleVersion 18 | 3 19 | 25 | NSHumanReadableCopyright 26 | Copyright, Jens Kutilek, 2023 27 | NSPrincipalClass 28 | NibSimulator 29 | PyMainFileNames 30 | 31 | plugin.py 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /NibSimulator.glyphsReporter/Contents/MacOS/plugin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenskutilek/nibLib/f41b01ab32029a705358cc8f22257fa6f4637785/NibSimulator.glyphsReporter/Contents/MacOS/plugin -------------------------------------------------------------------------------- /NibSimulator.glyphsReporter/Contents/Resources/plugin.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import objc 4 | from AppKit import NSClassFromString 5 | from GlyphsApp import * 6 | from GlyphsApp.plugins import * 7 | from GlyphsApp.drawingTools import * 8 | 9 | from nibLib.ui.glyphs import JKNibGlyphs 10 | 11 | 12 | class NibSimulator(ReporterPlugin): 13 | @objc.python_method 14 | def settings(self): 15 | self.menuName = Glyphs.localize( 16 | { 17 | "en": "Nib Simulator", 18 | "de": "Schreibfedersimulator", 19 | } 20 | ) 21 | self.generalContextMenus = [ 22 | { 23 | "name": Glyphs.localize( 24 | { 25 | "en": "Nib Settings...", 26 | "de": "Schreibfedereinstellungen ...", 27 | } 28 | ), 29 | "action": self.openSettingsWindow_, 30 | } 31 | ] 32 | self.w = None 33 | 34 | @objc.python_method 35 | def background(self, layer): 36 | if self.w is None: 37 | return 38 | 39 | # If the layer has changed, we need a new guide layer 40 | if layer != self.w.glyph: 41 | self.w.glyph = layer 42 | 43 | currentController = self.controller.view().window().windowController() 44 | if currentController: 45 | tool = currentController.toolDrawDelegate() 46 | if not ( 47 | tool.isKindOfClass_(NSClassFromString("GlyphsToolText")) 48 | or tool.isKindOfClass_(NSClassFromString("GlyphsToolHand")) 49 | or tool.isKindOfClass_( 50 | NSClassFromString("GlyphsToolTrueTypeInstructor") 51 | ) 52 | ): 53 | self.w.draw_preview_glyph(scale=self.getScale()) 54 | 55 | @objc.python_method 56 | def window_will_close(self): 57 | self.w = None 58 | 59 | def openSettingsWindow_(self, sender): 60 | font = Glyphs.font 61 | layer = None 62 | master = None 63 | if font is not None: 64 | sel = font.selectedLayers 65 | layer = None if sel is None else sel[0] 66 | if layer is not None: 67 | master = layer.master 68 | 69 | self.w = JKNibGlyphs(layer, master, self) 70 | 71 | @objc.python_method 72 | def __file__(self): 73 | """Please leave this method unchanged""" 74 | return __file__ 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nibLib 2 | 3 | Nib simulation library for font editors (well, Glyphs and RoboFont at the moment). 4 | 5 | ## Available nib shapes 6 | 7 | * Rectangle 8 | * Oval 9 | * Superellipse 10 | 11 | ## Screenshots 12 | 13 | 14 | 15 | 16 | 17 | ## Installation 18 | 19 | ### Glyphs 20 | 21 | #### User Installation 22 | 23 | Use the provided `NibSimulator.glyphsReporter.zip` from the [latest release](https://github.com/jenskutilek/nibLib/releases). Unzip, then double-click the `NibSimulator.glyphsReporter` file to install it into Glyphs. 24 | 25 | #### Developer Installation 26 | 27 | You need to install the required Python modules, as the `NibSimulator.glyphsReporter` in the repository does not contain them: 28 | 29 | 1. Clone or download the git repository 30 | 2. In the repository, use the pip command corresponding to your major and minor Python 31 | version that you use in Glyphs to install nibLib and its dependencies, e.g. if you 32 | have Python 3.11.8: 33 | 34 | ```bash 35 | pip3.11 install --user . 36 | ``` 37 | 38 | Or install them into the Glyphs scripts folder: 39 | 40 | ```bash 41 | pip3.11 install -t ~/Library/Application\ Support/Glyphs\ 3/Scripts/site-packages . 42 | ``` 43 | 44 | ### RoboFont 45 | 46 | Danger: The RoboFont version used to work, but I haven't tested it myself recently after 47 | changing lots of stuff in the code. 48 | 49 | Put the folder `nibLib` from `lib` somewhere RoboFont can import Python modules from, 50 | e.g. `~/Library/Application Support/RoboFont/external_packages`. 51 | 52 | You also need to install the required package `beziers`, and some more packages that 53 | RoboFont already provides: `defconAppKit`, `fontPens`, and `fontTools`. 54 | 55 | Open the script `NibLibRF.py` in RoboFont’s macro panel and run it. NibLib will use any 56 | path in the bottom-most layer as a guide path. 57 | 58 | ## Known bugs 59 | 60 | * This is a development version, everything may be broken. 61 | * The "Ellipse" nib is broken, but you can use the Superellipse with a superness setting 62 | of 2.0 instead. 63 | -------------------------------------------------------------------------------- /build_glyphs_plugin.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | PYTHON=/Library/Frameworks/Python.framework/Versions/3.11/bin/python3.11 4 | PLUGIN=NibSimulator.glyphsReporter 5 | RESOURCES=Contents/Resources 6 | 7 | # Make sure the target folder exists 8 | mkdir -p ./dist 9 | 10 | cp -R $PLUGIN ./dist/ 11 | 12 | # Install dependencies 13 | $PYTHON -m pip install -t "./dist/$PLUGIN/$RESOURCES" . 14 | -------------------------------------------------------------------------------- /demo/NibSimulator.glyphs: -------------------------------------------------------------------------------- 1 | { 2 | .appVersion = "3236"; 3 | .formatVersion = 3; 4 | DisplayStrings = ( 5 | ao 6 | ); 7 | date = "2024-02-08 16:38:41 +0000"; 8 | familyName = "New Font"; 9 | fontMaster = ( 10 | { 11 | id = m01; 12 | metricValues = ( 13 | { 14 | over = 16; 15 | pos = 800; 16 | }, 17 | { 18 | over = 16; 19 | pos = 700; 20 | }, 21 | { 22 | over = 16; 23 | pos = 500; 24 | }, 25 | { 26 | over = -16; 27 | }, 28 | { 29 | over = -16; 30 | pos = -200; 31 | }, 32 | { 33 | } 34 | ); 35 | name = Regular; 36 | userData = { 37 | de.kutilek.NibSimulator.angle = 30; 38 | de.kutilek.NibSimulator.guide = nib; 39 | de.kutilek.NibSimulator.height = 40; 40 | de.kutilek.NibSimulator.model = Superellipse; 41 | de.kutilek.NibSimulator.super = 2; 42 | de.kutilek.NibSimulator.width = 100; 43 | }; 44 | } 45 | ); 46 | glyphs = ( 47 | { 48 | glyphname = A; 49 | layers = ( 50 | { 51 | layerId = m01; 52 | width = 600; 53 | } 54 | ); 55 | unicode = 65; 56 | }, 57 | { 58 | glyphname = B; 59 | layers = ( 60 | { 61 | layerId = m01; 62 | width = 600; 63 | } 64 | ); 65 | unicode = 66; 66 | }, 67 | { 68 | glyphname = C; 69 | layers = ( 70 | { 71 | layerId = m01; 72 | width = 600; 73 | } 74 | ); 75 | unicode = 67; 76 | }, 77 | { 78 | glyphname = D; 79 | layers = ( 80 | { 81 | layerId = m01; 82 | width = 600; 83 | } 84 | ); 85 | unicode = 68; 86 | }, 87 | { 88 | glyphname = E; 89 | layers = ( 90 | { 91 | layerId = m01; 92 | width = 600; 93 | } 94 | ); 95 | unicode = 69; 96 | }, 97 | { 98 | glyphname = F; 99 | layers = ( 100 | { 101 | layerId = m01; 102 | width = 600; 103 | } 104 | ); 105 | unicode = 70; 106 | }, 107 | { 108 | glyphname = G; 109 | layers = ( 110 | { 111 | layerId = m01; 112 | width = 600; 113 | } 114 | ); 115 | unicode = 71; 116 | }, 117 | { 118 | glyphname = H; 119 | layers = ( 120 | { 121 | layerId = m01; 122 | width = 600; 123 | } 124 | ); 125 | unicode = 72; 126 | }, 127 | { 128 | glyphname = I; 129 | layers = ( 130 | { 131 | layerId = m01; 132 | width = 600; 133 | } 134 | ); 135 | unicode = 73; 136 | }, 137 | { 138 | glyphname = J; 139 | layers = ( 140 | { 141 | layerId = m01; 142 | width = 600; 143 | } 144 | ); 145 | unicode = 74; 146 | }, 147 | { 148 | glyphname = K; 149 | layers = ( 150 | { 151 | layerId = m01; 152 | width = 600; 153 | } 154 | ); 155 | unicode = 75; 156 | }, 157 | { 158 | glyphname = L; 159 | layers = ( 160 | { 161 | layerId = m01; 162 | width = 600; 163 | } 164 | ); 165 | unicode = 76; 166 | }, 167 | { 168 | glyphname = M; 169 | layers = ( 170 | { 171 | layerId = m01; 172 | width = 600; 173 | } 174 | ); 175 | unicode = 77; 176 | }, 177 | { 178 | glyphname = N; 179 | layers = ( 180 | { 181 | layerId = m01; 182 | width = 600; 183 | } 184 | ); 185 | unicode = 78; 186 | }, 187 | { 188 | glyphname = O; 189 | layers = ( 190 | { 191 | layerId = m01; 192 | width = 600; 193 | } 194 | ); 195 | unicode = 79; 196 | }, 197 | { 198 | glyphname = P; 199 | layers = ( 200 | { 201 | layerId = m01; 202 | width = 600; 203 | } 204 | ); 205 | unicode = 80; 206 | }, 207 | { 208 | glyphname = Q; 209 | layers = ( 210 | { 211 | layerId = m01; 212 | width = 600; 213 | } 214 | ); 215 | unicode = 81; 216 | }, 217 | { 218 | glyphname = R; 219 | layers = ( 220 | { 221 | layerId = m01; 222 | width = 600; 223 | } 224 | ); 225 | unicode = 82; 226 | }, 227 | { 228 | glyphname = S; 229 | layers = ( 230 | { 231 | layerId = m01; 232 | width = 600; 233 | } 234 | ); 235 | unicode = 83; 236 | }, 237 | { 238 | glyphname = T; 239 | layers = ( 240 | { 241 | layerId = m01; 242 | width = 600; 243 | } 244 | ); 245 | unicode = 84; 246 | }, 247 | { 248 | glyphname = U; 249 | layers = ( 250 | { 251 | layerId = m01; 252 | width = 600; 253 | } 254 | ); 255 | unicode = 85; 256 | }, 257 | { 258 | glyphname = V; 259 | layers = ( 260 | { 261 | layerId = m01; 262 | width = 600; 263 | } 264 | ); 265 | unicode = 86; 266 | }, 267 | { 268 | glyphname = W; 269 | layers = ( 270 | { 271 | layerId = m01; 272 | width = 600; 273 | } 274 | ); 275 | unicode = 87; 276 | }, 277 | { 278 | glyphname = X; 279 | layers = ( 280 | { 281 | layerId = m01; 282 | width = 600; 283 | } 284 | ); 285 | unicode = 88; 286 | }, 287 | { 288 | glyphname = Y; 289 | layers = ( 290 | { 291 | layerId = m01; 292 | width = 600; 293 | } 294 | ); 295 | unicode = 89; 296 | }, 297 | { 298 | glyphname = Z; 299 | layers = ( 300 | { 301 | layerId = m01; 302 | width = 600; 303 | } 304 | ); 305 | unicode = 90; 306 | }, 307 | { 308 | glyphname = a; 309 | lastChange = "2024-02-08 17:11:15 +0000"; 310 | layers = ( 311 | { 312 | background = { 313 | shapes = ( 314 | { 315 | closed = 0; 316 | nodes = ( 317 | (56,338,ls), 318 | (90,418,o), 319 | (150,481,o), 320 | (252,481,cs), 321 | (387,481,o), 322 | (430,410,o), 323 | (430,245,cs), 324 | (430,106,ls), 325 | (430,52,o), 326 | (456,18,o), 327 | (503,18,cs), 328 | (529,18,o), 329 | (553,26,o), 330 | (571,35,cs) 331 | ); 332 | }, 333 | { 334 | closed = 0; 335 | nodes = ( 336 | (427,264,l), 337 | (177,247,o), 338 | (60,219,o), 339 | (60,122,cs), 340 | (60,59,o), 341 | (107,16,o), 342 | (191,16,cs), 343 | (296,16,o), 344 | (353,66,o), 345 | (429,143,cs) 346 | ); 347 | } 348 | ); 349 | }; 350 | layerId = m01; 351 | shapes = ( 352 | { 353 | closed = 1; 354 | nodes = ( 355 | (240,-14,o), 356 | (307,13,o), 357 | (366,57,cs), 358 | (386,72,l), 359 | (389,40,o), 360 | (403,7,o), 361 | (435,-6,cs), 362 | (447,-11,o), 363 | (460,-12,o), 364 | (472,-12,cs), 365 | (506,-12,o), 366 | (536,-3,o), 367 | (566,10,cs), 368 | (580,16,o), 369 | (593,24,o), 370 | (603,34,cs), 371 | (609,40,o), 372 | (618,50,o), 373 | (615,59,c), 374 | (614,60,ls), 375 | (605,72,o), 376 | (582,63,o), 377 | (571,58,cs), 378 | (567,56,ls), 379 | (557,51,o), 380 | (545,48,o), 381 | (533,48,cs), 382 | (491,48,o), 383 | (474,84,o), 384 | (474,127,cs), 385 | (474,266,ls), 386 | (474,328,o), 387 | (472,417,o), 388 | (419,468,cs), 389 | (383,503,o), 390 | (330,511,o), 391 | (282,511,cs), 392 | (197,511,o), 393 | (115,470,o), 394 | (61,405,cs), 395 | (41,380,o), 396 | (26,353,o), 397 | (13,324,cs), 398 | (10,316,o), 399 | (10,308,o), 400 | (26,308,cs), 401 | (49,308,o), 402 | (73,322,o), 403 | (88,337,cs), 404 | (92,341,o), 405 | (98,348,o), 406 | (100,354,cs), 407 | (115,386,o), 408 | (133,424,o), 409 | (166,441,cs), 410 | (183,450,o), 411 | (204,451,o), 412 | (222,451,cs), 413 | (272,451,o), 414 | (324,438,o), 415 | (353,394,cs), 416 | (373,363,o), 417 | (380,326,o), 418 | (383,289,c), 419 | (327,284,o), 420 | (270,278,o), 421 | (215,265,cs), 422 | (167,254,o), 423 | (120,239,o), 424 | (79,209,cs), 425 | (44,184,o), 426 | (16,147,o), 427 | (16,102,cs), 428 | (16,48,o), 429 | (51,8,o), 430 | (101,-7,cs), 431 | (120,-13,o), 432 | (141,-14,o), 433 | (161,-14,cs) 434 | ); 435 | }, 436 | { 437 | closed = 1; 438 | nodes = ( 439 | (160,46,o), 440 | (104,80,o), 441 | (104,143,cs), 442 | (104,185,o), 443 | (123,193,o), 444 | (160,203,cs), 445 | (181,209,o), 446 | (203,213,o), 447 | (224,216,cs), 448 | (277,224,o), 449 | (332,229,o), 450 | (386,233,c), 451 | (386,133,l), 452 | (360,105,o), 453 | (330,73,o), 454 | (292,58,cs), 455 | (269,49,o), 456 | (245,46,o), 457 | (221,46,cs) 458 | ); 459 | } 460 | ); 461 | width = 600; 462 | }, 463 | { 464 | associatedMasterId = m01; 465 | layerId = "78F09F41-7ACC-44BA-90D2-FA028DDD6F29"; 466 | name = nib; 467 | shapes = ( 468 | { 469 | closed = 1; 470 | nodes = ( 471 | (429,20,o), 472 | (532,123,o), 473 | (532,252,cs), 474 | (532,379,o), 475 | (429,482,o), 476 | (301,482,cs), 477 | (172,482,o), 478 | (69,379,o), 479 | (69,252,cs), 480 | (69,123,o), 481 | (172,20,o), 482 | (301,20,cs) 483 | ); 484 | } 485 | ); 486 | visible = 1; 487 | width = 600; 488 | } 489 | ); 490 | unicode = 97; 491 | }, 492 | { 493 | glyphname = b; 494 | layers = ( 495 | { 496 | layerId = m01; 497 | width = 600; 498 | } 499 | ); 500 | unicode = 98; 501 | }, 502 | { 503 | glyphname = c; 504 | layers = ( 505 | { 506 | layerId = m01; 507 | width = 600; 508 | } 509 | ); 510 | unicode = 99; 511 | }, 512 | { 513 | glyphname = d; 514 | lastChange = "2024-02-08 16:51:06 +0000"; 515 | layers = ( 516 | { 517 | layerId = m01; 518 | width = 600; 519 | }, 520 | { 521 | associatedMasterId = m01; 522 | layerId = "2F539C6D-9429-4CE8-B40C-D7D6B1215EC2"; 523 | name = nib; 524 | width = 600; 525 | } 526 | ); 527 | unicode = 100; 528 | }, 529 | { 530 | glyphname = e; 531 | layers = ( 532 | { 533 | layerId = m01; 534 | width = 600; 535 | } 536 | ); 537 | unicode = 101; 538 | }, 539 | { 540 | glyphname = f; 541 | layers = ( 542 | { 543 | layerId = m01; 544 | width = 600; 545 | } 546 | ); 547 | unicode = 102; 548 | }, 549 | { 550 | glyphname = g; 551 | layers = ( 552 | { 553 | layerId = m01; 554 | width = 600; 555 | } 556 | ); 557 | unicode = 103; 558 | }, 559 | { 560 | glyphname = h; 561 | layers = ( 562 | { 563 | layerId = m01; 564 | width = 600; 565 | } 566 | ); 567 | unicode = 104; 568 | }, 569 | { 570 | glyphname = i; 571 | layers = ( 572 | { 573 | layerId = m01; 574 | width = 600; 575 | } 576 | ); 577 | unicode = 105; 578 | }, 579 | { 580 | glyphname = j; 581 | layers = ( 582 | { 583 | layerId = m01; 584 | width = 600; 585 | } 586 | ); 587 | unicode = 106; 588 | }, 589 | { 590 | glyphname = k; 591 | layers = ( 592 | { 593 | layerId = m01; 594 | width = 600; 595 | } 596 | ); 597 | unicode = 107; 598 | }, 599 | { 600 | glyphname = l; 601 | layers = ( 602 | { 603 | layerId = m01; 604 | width = 600; 605 | } 606 | ); 607 | unicode = 108; 608 | }, 609 | { 610 | glyphname = m; 611 | layers = ( 612 | { 613 | layerId = m01; 614 | width = 600; 615 | } 616 | ); 617 | unicode = 109; 618 | }, 619 | { 620 | glyphname = n; 621 | lastChange = "2024-02-08 17:10:06 +0000"; 622 | layers = ( 623 | { 624 | background = { 625 | shapes = ( 626 | { 627 | closed = 0; 628 | nodes = ( 629 | (134,478,l), 630 | (150,326,o), 631 | (148,209,o), 632 | (134,22,c), 633 | (192,281,o), 634 | (248,484,o), 635 | (380,484,cs), 636 | (474,484,o), 637 | (513,391,o), 638 | (478,209,cs), 639 | (450,62,o), 640 | (479,22,o), 641 | (535,22,cs), 642 | (561,22,o), 643 | (584,27,o), 644 | (616,41,cs) 645 | ); 646 | } 647 | ); 648 | }; 649 | layerId = m01; 650 | shapes = ( 651 | { 652 | closed = 1; 653 | nodes = ( 654 | (177,503,ls), 655 | (168,515,o), 656 | (145,506,o), 657 | (134,501,cs), 658 | (125,496,o), 659 | (115,490,o), 660 | (107,483,cs), 661 | (100,477,o), 662 | (86,461,o), 663 | (90,453,c), 664 | (107,305,o), 665 | (101,154,o), 666 | (90,5,c), 667 | (88,-3,o), 668 | (91,-8,o), 669 | (104,-8,cs), 670 | (127,-8,o), 671 | (151,6,o), 672 | (166,21,cs), 673 | (171,26,o), 674 | (179,34,o), 675 | (179,42,c), 676 | (207,156,o), 677 | (227,276,o), 678 | (281,381,cs), 679 | (291,402,o), 680 | (303,422,o), 681 | (318,440,cs), 682 | (322,444,o), 683 | (326,449,o), 684 | (332,451,c), 685 | (336,454,o), 686 | (343,454,o), 687 | (347,454,cs), 688 | (350,454,ls), 689 | (473,447,o), 690 | (449,271,o), 691 | (434,191,c), 692 | (434,189,ls), 693 | (427,141,o), 694 | (415,91,o), 695 | (429,44,cs), 696 | (433,30,o), 697 | (440,16,o), 698 | (452,7,cs), 699 | (466,-4,o), 700 | (484,-8,o), 701 | (501,-8,cs), 702 | (505,-8,ls), 703 | (541,-9,o), 704 | (574,2,o), 705 | (607,14,cs), 706 | (623,19,o), 707 | (637,29,o), 708 | (648,40,cs), 709 | (654,46,o), 710 | (663,56,o), 711 | (660,65,c), 712 | (659,66,ls), 713 | (650,78,o), 714 | (627,69,o), 715 | (616,64,cs), 716 | (613,62,l), 717 | (599,57,o), 718 | (585,54,o), 719 | (570,52,c), 720 | (566,53,o), 721 | (562,53,o), 722 | (558,53,cs), 723 | (490,61,o), 724 | (513,176,o), 725 | (521,223,c), 726 | (522,226,o), 727 | (523,228,o), 728 | (523,230,c), 729 | (542,331,o), 730 | (557,507,o), 731 | (416,514,c), 732 | (411,516,o), 733 | (405,515,o), 734 | (398,514,cs), 735 | (370,512,o), 736 | (342,502,o), 737 | (316,488,cs), 738 | (256,456,o), 739 | (217,401,o), 740 | (188,341,c), 741 | (187,392,o), 742 | (184,444,o), 743 | (178,496,c), 744 | (179,498,o), 745 | (179,500,o), 746 | (178,502,cs) 747 | ); 748 | } 749 | ); 750 | width = 640; 751 | }, 752 | { 753 | associatedMasterId = m01; 754 | layerId = "11004791-9D3B-4196-92CF-0EA1DF5DF6E6"; 755 | name = nib; 756 | shapes = ( 757 | { 758 | closed = 0; 759 | nodes = ( 760 | (465,480,l), 761 | (465,20,l), 762 | (135,480,l), 763 | (135,20,l) 764 | ); 765 | } 766 | ); 767 | width = 600; 768 | } 769 | ); 770 | unicode = 110; 771 | }, 772 | { 773 | glyphname = o; 774 | lastChange = "2024-02-08 17:10:36 +0000"; 775 | layers = ( 776 | { 777 | layerId = m01; 778 | shapes = ( 779 | { 780 | closed = 1; 781 | nodes = ( 782 | (240,-14,o), 783 | (307,13,o), 784 | (366,57,cs), 785 | (386,72,l), 786 | (389,40,o), 787 | (403,7,o), 788 | (435,-6,cs), 789 | (447,-11,o), 790 | (460,-12,o), 791 | (472,-12,cs), 792 | (506,-12,o), 793 | (536,-3,o), 794 | (566,10,cs), 795 | (580,16,o), 796 | (593,24,o), 797 | (603,34,cs), 798 | (609,40,o), 799 | (618,50,o), 800 | (615,59,c), 801 | (614,60,ls), 802 | (605,72,o), 803 | (582,63,o), 804 | (571,58,cs), 805 | (567,56,ls), 806 | (557,51,o), 807 | (545,48,o), 808 | (533,48,cs), 809 | (491,48,o), 810 | (474,84,o), 811 | (474,127,cs), 812 | (474,266,ls), 813 | (474,328,o), 814 | (472,417,o), 815 | (419,468,cs), 816 | (383,503,o), 817 | (330,511,o), 818 | (282,511,cs), 819 | (197,511,o), 820 | (115,470,o), 821 | (61,405,cs), 822 | (41,380,o), 823 | (26,353,o), 824 | (13,324,cs), 825 | (10,316,o), 826 | (10,308,o), 827 | (26,308,cs), 828 | (49,308,o), 829 | (73,322,o), 830 | (88,337,cs), 831 | (92,341,o), 832 | (98,348,o), 833 | (100,354,cs), 834 | (115,386,o), 835 | (133,424,o), 836 | (166,441,cs), 837 | (183,450,o), 838 | (204,451,o), 839 | (222,451,cs), 840 | (272,451,o), 841 | (324,438,o), 842 | (353,394,cs), 843 | (373,363,o), 844 | (380,326,o), 845 | (383,289,c), 846 | (327,284,o), 847 | (270,278,o), 848 | (215,265,cs), 849 | (167,254,o), 850 | (120,239,o), 851 | (79,209,cs), 852 | (44,184,o), 853 | (16,147,o), 854 | (16,102,cs), 855 | (16,48,o), 856 | (51,8,o), 857 | (101,-7,cs), 858 | (120,-13,o), 859 | (141,-14,o), 860 | (161,-14,cs) 861 | ); 862 | }, 863 | { 864 | closed = 1; 865 | nodes = ( 866 | (160,46,o), 867 | (104,80,o), 868 | (104,143,cs), 869 | (104,185,o), 870 | (123,193,o), 871 | (160,203,cs), 872 | (181,209,o), 873 | (203,213,o), 874 | (224,216,cs), 875 | (277,224,o), 876 | (332,229,o), 877 | (386,233,c), 878 | (386,133,l), 879 | (360,105,o), 880 | (330,73,o), 881 | (292,58,cs), 882 | (269,49,o), 883 | (245,46,o), 884 | (221,46,cs) 885 | ); 886 | } 887 | ); 888 | width = 600; 889 | }, 890 | { 891 | associatedMasterId = m01; 892 | layerId = "92634438-7509-45EA-ADCE-164F52BE2D27"; 893 | name = nib; 894 | width = 600; 895 | } 896 | ); 897 | unicode = 111; 898 | }, 899 | { 900 | glyphname = p; 901 | lastChange = "2024-02-08 17:07:27 +0000"; 902 | layers = ( 903 | { 904 | layerId = m01; 905 | width = 600; 906 | }, 907 | { 908 | associatedMasterId = m01; 909 | layerId = "8C40F3FD-CFBD-4BC5-A601-D1944B422101"; 910 | name = nib; 911 | width = 600; 912 | } 913 | ); 914 | unicode = 112; 915 | }, 916 | { 917 | glyphname = q; 918 | layers = ( 919 | { 920 | layerId = m01; 921 | width = 600; 922 | } 923 | ); 924 | unicode = 113; 925 | }, 926 | { 927 | glyphname = r; 928 | layers = ( 929 | { 930 | layerId = m01; 931 | width = 600; 932 | } 933 | ); 934 | unicode = 114; 935 | }, 936 | { 937 | glyphname = s; 938 | layers = ( 939 | { 940 | layerId = m01; 941 | width = 600; 942 | } 943 | ); 944 | unicode = 115; 945 | }, 946 | { 947 | glyphname = t; 948 | layers = ( 949 | { 950 | layerId = m01; 951 | width = 600; 952 | } 953 | ); 954 | unicode = 116; 955 | }, 956 | { 957 | glyphname = u; 958 | layers = ( 959 | { 960 | layerId = m01; 961 | width = 600; 962 | } 963 | ); 964 | unicode = 117; 965 | }, 966 | { 967 | glyphname = v; 968 | layers = ( 969 | { 970 | layerId = m01; 971 | width = 600; 972 | } 973 | ); 974 | unicode = 118; 975 | }, 976 | { 977 | glyphname = w; 978 | layers = ( 979 | { 980 | layerId = m01; 981 | width = 600; 982 | } 983 | ); 984 | unicode = 119; 985 | }, 986 | { 987 | glyphname = x; 988 | layers = ( 989 | { 990 | layerId = m01; 991 | width = 600; 992 | } 993 | ); 994 | unicode = 120; 995 | }, 996 | { 997 | glyphname = y; 998 | layers = ( 999 | { 1000 | layerId = m01; 1001 | width = 600; 1002 | } 1003 | ); 1004 | unicode = 121; 1005 | }, 1006 | { 1007 | glyphname = z; 1008 | layers = ( 1009 | { 1010 | layerId = m01; 1011 | width = 600; 1012 | } 1013 | ); 1014 | unicode = 122; 1015 | }, 1016 | { 1017 | glyphname = one; 1018 | layers = ( 1019 | { 1020 | layerId = m01; 1021 | width = 600; 1022 | } 1023 | ); 1024 | unicode = 49; 1025 | }, 1026 | { 1027 | glyphname = two; 1028 | layers = ( 1029 | { 1030 | layerId = m01; 1031 | width = 600; 1032 | } 1033 | ); 1034 | unicode = 50; 1035 | }, 1036 | { 1037 | glyphname = three; 1038 | layers = ( 1039 | { 1040 | layerId = m01; 1041 | width = 600; 1042 | } 1043 | ); 1044 | unicode = 51; 1045 | }, 1046 | { 1047 | glyphname = four; 1048 | layers = ( 1049 | { 1050 | layerId = m01; 1051 | width = 600; 1052 | } 1053 | ); 1054 | unicode = 52; 1055 | }, 1056 | { 1057 | glyphname = five; 1058 | layers = ( 1059 | { 1060 | layerId = m01; 1061 | width = 600; 1062 | } 1063 | ); 1064 | unicode = 53; 1065 | }, 1066 | { 1067 | glyphname = six; 1068 | layers = ( 1069 | { 1070 | layerId = m01; 1071 | width = 600; 1072 | } 1073 | ); 1074 | unicode = 54; 1075 | }, 1076 | { 1077 | glyphname = seven; 1078 | layers = ( 1079 | { 1080 | layerId = m01; 1081 | width = 600; 1082 | } 1083 | ); 1084 | unicode = 55; 1085 | }, 1086 | { 1087 | glyphname = eight; 1088 | layers = ( 1089 | { 1090 | layerId = m01; 1091 | width = 600; 1092 | } 1093 | ); 1094 | unicode = 56; 1095 | }, 1096 | { 1097 | glyphname = nine; 1098 | layers = ( 1099 | { 1100 | layerId = m01; 1101 | width = 600; 1102 | } 1103 | ); 1104 | unicode = 57; 1105 | }, 1106 | { 1107 | glyphname = zero; 1108 | layers = ( 1109 | { 1110 | layerId = m01; 1111 | width = 600; 1112 | } 1113 | ); 1114 | unicode = 48; 1115 | }, 1116 | { 1117 | glyphname = period; 1118 | layers = ( 1119 | { 1120 | layerId = m01; 1121 | width = 600; 1122 | } 1123 | ); 1124 | unicode = 46; 1125 | }, 1126 | { 1127 | glyphname = comma; 1128 | layers = ( 1129 | { 1130 | layerId = m01; 1131 | width = 600; 1132 | } 1133 | ); 1134 | unicode = 44; 1135 | }, 1136 | { 1137 | glyphname = hyphen; 1138 | layers = ( 1139 | { 1140 | layerId = m01; 1141 | width = 600; 1142 | } 1143 | ); 1144 | unicode = 45; 1145 | }, 1146 | { 1147 | glyphname = space; 1148 | layers = ( 1149 | { 1150 | layerId = m01; 1151 | width = 200; 1152 | } 1153 | ); 1154 | unicode = 32; 1155 | } 1156 | ); 1157 | metrics = ( 1158 | { 1159 | type = ascender; 1160 | }, 1161 | { 1162 | type = "cap height"; 1163 | }, 1164 | { 1165 | type = "x-height"; 1166 | }, 1167 | { 1168 | type = baseline; 1169 | }, 1170 | { 1171 | type = descender; 1172 | }, 1173 | { 1174 | type = "italic angle"; 1175 | } 1176 | ); 1177 | unitsPerEm = 1000; 1178 | versionMajor = 1; 1179 | versionMinor = 0; 1180 | } 1181 | -------------------------------------------------------------------------------- /demo/NibSimulator.ufo/fontinfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ascender 6 | 750 7 | capHeight 8 | 750 9 | descender 10 | -250 11 | guidelines 12 | 13 | postscriptBlueValues 14 | 15 | postscriptFamilyBlues 16 | 17 | postscriptFamilyOtherBlues 18 | 19 | postscriptOtherBlues 20 | 21 | postscriptStemSnapH 22 | 23 | postscriptStemSnapV 24 | 25 | unitsPerEm 26 | 1000 27 | xHeight 28 | 500 29 | 30 | 31 | -------------------------------------------------------------------------------- /demo/NibSimulator.ufo/glyphs.background/c.glif: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /demo/NibSimulator.ufo/glyphs.background/ccedilla.glif: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /demo/NibSimulator.ufo/glyphs.background/contents.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | c 6 | c.glif 7 | ccedilla 8 | ccedilla.glif 9 | l 10 | l.glif 11 | m 12 | m.glif 13 | n 14 | n.glif 15 | o 16 | o.glif 17 | ograve 18 | ograve.glif 19 | 20 | 21 | -------------------------------------------------------------------------------- /demo/NibSimulator.ufo/glyphs.background/l.glif: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /demo/NibSimulator.ufo/glyphs.background/layerinfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | color 6 | 0.5,1,0,0.7 7 | 8 | 9 | -------------------------------------------------------------------------------- /demo/NibSimulator.ufo/glyphs.background/m.glif: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /demo/NibSimulator.ufo/glyphs.background/n.glif: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /demo/NibSimulator.ufo/glyphs.background/o.glif: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /demo/NibSimulator.ufo/glyphs.background/ograve.glif: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /demo/NibSimulator.ufo/glyphs/c.glif: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /demo/NibSimulator.ufo/glyphs/ccedilla.glif: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | -------------------------------------------------------------------------------- /demo/NibSimulator.ufo/glyphs/contents.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | c 6 | c.glif 7 | ccedilla 8 | ccedilla.glif 9 | l 10 | l.glif 11 | m 12 | m.glif 13 | n 14 | n.glif 15 | o 16 | o.glif 17 | ograve 18 | ograve.glif 19 | 20 | 21 | -------------------------------------------------------------------------------- /demo/NibSimulator.ufo/glyphs/l.glif: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /demo/NibSimulator.ufo/glyphs/layerinfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | color 6 | 1,0.75,0,0.7 7 | 8 | 9 | -------------------------------------------------------------------------------- /demo/NibSimulator.ufo/glyphs/m.glif: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /demo/NibSimulator.ufo/glyphs/n.glif: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /demo/NibSimulator.ufo/glyphs/o.glif: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /demo/NibSimulator.ufo/glyphs/ograve.glif: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | -------------------------------------------------------------------------------- /demo/NibSimulator.ufo/layercontents.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | foreground 7 | glyphs 8 | 9 | 10 | background 11 | glyphs.background 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /demo/NibSimulator.ufo/lib.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.typemytype.robofont.background.layerStrokeColor 6 | 7 | 0.0 8 | 0.8 9 | 0.2 10 | 0.7 11 | 12 | com.typemytype.robofont.foreground.layerStrokeColor 13 | 14 | 0.5 15 | 0.0 16 | 0.5 17 | 0.7 18 | 19 | com.typemytype.robofont.segmentType 20 | curve 21 | com.typesupply.defcon.sortDescriptor 22 | 23 | 24 | ascending 25 | 26 | A 27 | Agrave 28 | Aacute 29 | Acircumflex 30 | Atilde 31 | Adieresis 32 | Aring 33 | B 34 | C 35 | Ccedilla 36 | D 37 | E 38 | Egrave 39 | Eacute 40 | Ecircumflex 41 | Edieresis 42 | F 43 | G 44 | H 45 | I 46 | Igrave 47 | Iacute 48 | Icircumflex 49 | Idieresis 50 | J 51 | K 52 | L 53 | M 54 | N 55 | Ntilde 56 | O 57 | Ograve 58 | Oacute 59 | Ocircumflex 60 | Otilde 61 | Odieresis 62 | P 63 | Q 64 | R 65 | S 66 | T 67 | U 68 | Ugrave 69 | Uacute 70 | Ucircumflex 71 | Udieresis 72 | V 73 | W 74 | X 75 | Y 76 | Yacute 77 | Z 78 | AE 79 | Eth 80 | Oslash 81 | Thorn 82 | a 83 | agrave 84 | aacute 85 | acircumflex 86 | atilde 87 | adieresis 88 | aring 89 | b 90 | c 91 | ccedilla 92 | d 93 | e 94 | egrave 95 | eacute 96 | ecircumflex 97 | edieresis 98 | f 99 | g 100 | h 101 | i 102 | igrave 103 | iacute 104 | icircumflex 105 | idieresis 106 | j 107 | k 108 | l 109 | m 110 | n 111 | ntilde 112 | o 113 | ograve 114 | oacute 115 | ocircumflex 116 | otilde 117 | odieresis 118 | p 119 | q 120 | r 121 | s 122 | t 123 | u 124 | ugrave 125 | uacute 126 | ucircumflex 127 | udieresis 128 | v 129 | w 130 | x 131 | y 132 | yacute 133 | ydieresis 134 | z 135 | ordfeminine 136 | ordmasculine 137 | germandbls 138 | ae 139 | eth 140 | oslash 141 | thorn 142 | dotlessi 143 | mu 144 | circumflex 145 | caron 146 | zero 147 | one 148 | two 149 | three 150 | four 151 | five 152 | six 153 | seven 154 | eight 155 | nine 156 | twosuperior 157 | threesuperior 158 | onesuperior 159 | onequarter 160 | onehalf 161 | threequarters 162 | underscore 163 | hyphen 164 | parenleft 165 | bracketleft 166 | braceleft 167 | parenright 168 | bracketright 169 | braceright 170 | quoteleft 171 | quoteright 172 | exclam 173 | quotedbl 174 | numbersign 175 | percent 176 | ampersand 177 | asterisk 178 | comma 179 | period 180 | slash 181 | colon 182 | semicolon 183 | question 184 | at 185 | backslash 186 | exclamdown 187 | periodcentered 188 | questiondown 189 | plus 190 | less 191 | equal 192 | greater 193 | bar 194 | asciitilde 195 | logicalnot 196 | plusminus 197 | multiply 198 | divide 199 | minus 200 | dollar 201 | cent 202 | sterling 203 | currency 204 | yen 205 | asciicircum 206 | grave 207 | dieresis 208 | macron 209 | acute 210 | cedilla 211 | breve 212 | dotaccent 213 | ring 214 | ogonek 215 | tilde 216 | hungarumlaut 217 | brokenbar 218 | section 219 | copyright 220 | registered 221 | degree 222 | paragraph 223 | space 224 | guillemotleft 225 | guillemotright 226 | 227 | type 228 | glyphList 229 | 230 | 231 | de.kutilek.NibSimulator.angle 232 | 45.0 233 | de.kutilek.NibSimulator.guide 234 | background 235 | de.kutilek.NibSimulator.height 236 | 20 237 | de.kutilek.NibSimulator.model 238 | Oval 239 | de.kutilek.NibSimulator.super 240 | 4.013503074645996 241 | de.kutilek.NibSimulator.width 242 | 60 243 | de.kutilek.constraints 244 | 245 | public.glyphOrder 246 | 247 | A 248 | Agrave 249 | Aacute 250 | Acircumflex 251 | Atilde 252 | Adieresis 253 | Aring 254 | B 255 | C 256 | Ccedilla 257 | D 258 | E 259 | Egrave 260 | Eacute 261 | Ecircumflex 262 | Edieresis 263 | F 264 | G 265 | H 266 | I 267 | Igrave 268 | Iacute 269 | Icircumflex 270 | Idieresis 271 | J 272 | K 273 | L 274 | M 275 | N 276 | Ntilde 277 | O 278 | Ograve 279 | Oacute 280 | Ocircumflex 281 | Otilde 282 | Odieresis 283 | P 284 | Q 285 | R 286 | S 287 | T 288 | U 289 | Ugrave 290 | Uacute 291 | Ucircumflex 292 | Udieresis 293 | V 294 | W 295 | X 296 | Y 297 | Yacute 298 | Z 299 | AE 300 | Eth 301 | Oslash 302 | Thorn 303 | a 304 | agrave 305 | aacute 306 | acircumflex 307 | atilde 308 | adieresis 309 | aring 310 | b 311 | c 312 | ccedilla 313 | d 314 | e 315 | egrave 316 | eacute 317 | ecircumflex 318 | edieresis 319 | f 320 | g 321 | h 322 | i 323 | igrave 324 | iacute 325 | icircumflex 326 | idieresis 327 | j 328 | k 329 | l 330 | m 331 | n 332 | ntilde 333 | o 334 | ograve 335 | oacute 336 | ocircumflex 337 | otilde 338 | odieresis 339 | p 340 | q 341 | r 342 | s 343 | t 344 | u 345 | ugrave 346 | uacute 347 | ucircumflex 348 | udieresis 349 | v 350 | w 351 | x 352 | y 353 | yacute 354 | ydieresis 355 | z 356 | ordfeminine 357 | ordmasculine 358 | germandbls 359 | ae 360 | eth 361 | oslash 362 | thorn 363 | dotlessi 364 | mu 365 | circumflex 366 | caron 367 | zero 368 | one 369 | two 370 | three 371 | four 372 | five 373 | six 374 | seven 375 | eight 376 | nine 377 | twosuperior 378 | threesuperior 379 | onesuperior 380 | onequarter 381 | onehalf 382 | threequarters 383 | underscore 384 | hyphen 385 | parenleft 386 | bracketleft 387 | braceleft 388 | parenright 389 | bracketright 390 | braceright 391 | quoteleft 392 | quoteright 393 | exclam 394 | quotedbl 395 | numbersign 396 | percent 397 | ampersand 398 | asterisk 399 | comma 400 | period 401 | slash 402 | colon 403 | semicolon 404 | question 405 | at 406 | backslash 407 | exclamdown 408 | periodcentered 409 | questiondown 410 | plus 411 | less 412 | equal 413 | greater 414 | bar 415 | asciitilde 416 | logicalnot 417 | plusminus 418 | multiply 419 | divide 420 | minus 421 | dollar 422 | cent 423 | sterling 424 | currency 425 | yen 426 | asciicircum 427 | grave 428 | dieresis 429 | macron 430 | acute 431 | cedilla 432 | breve 433 | dotaccent 434 | ring 435 | ogonek 436 | tilde 437 | hungarumlaut 438 | brokenbar 439 | section 440 | copyright 441 | registered 442 | degree 443 | paragraph 444 | space 445 | guillemotleft 446 | guillemotright 447 | 448 | 449 | 450 | -------------------------------------------------------------------------------- /demo/NibSimulator.ufo/metainfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | creator 6 | com.github.fonttools.ufoLib 7 | formatVersion 8 | 3 9 | 10 | 11 | -------------------------------------------------------------------------------- /images/formula-by-simon-cozens.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenskutilek/nibLib/f41b01ab32029a705358cc8f22257fa6f4637785/images/formula-by-simon-cozens.jpeg -------------------------------------------------------------------------------- /images/rectangle-rgb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenskutilek/nibLib/f41b01ab32029a705358cc8f22257fa6f4637785/images/rectangle-rgb.png -------------------------------------------------------------------------------- /images/superellipse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenskutilek/nibLib/f41b01ab32029a705358cc8f22257fa6f4637785/images/superellipse.png -------------------------------------------------------------------------------- /lib/nibLib/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | DEBUG = False 4 | DEBUG_CENTER_POINTS = False 5 | DEBUG_CURVE_POINTS = False 6 | # DEBUG_SEGMENTS = False 7 | 8 | # Defaults keys 9 | 10 | extensionID = "de.kutilek.NibSimulator" 11 | rf_guide_key = "%s.%s" % (extensionID, "guide_glyph") 12 | def_angle_key = "%s.%s" % (extensionID, "angle") 13 | def_width_key = "%s.%s" % (extensionID, "width") 14 | def_height_key = "%s.%s" % (extensionID, "height") 15 | def_local_key = "%s.%s" % (extensionID, "local") 16 | def_guide_key = "%s.%s" % (extensionID, "guide") 17 | def_super_key = "%s.%s" % (extensionID, "super") 18 | def_model_key = "%s.%s" % (extensionID, "model") 19 | -------------------------------------------------------------------------------- /lib/nibLib/geometry.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from beziers.path import BezierPath as SCBezierPath 4 | from beziers.point import Point as SCPoint 5 | from fontPens.penTools import getCubicPoint 6 | from fontTools.misc.bezierTools import calcCubicArcLength 7 | from functools import cached_property 8 | from math import atan2, sqrt 9 | from nibLib.typing import CCurve, TPoint 10 | from typing import List, Sequence, Tuple 11 | 12 | 13 | def getPointsFromCurve(p: CCurve, div=0.75) -> List[TPoint]: 14 | """Return a list of points for the given cubic curve segment. 15 | 16 | Args: 17 | p (CCurve): The cubic curve segment as four control points. 18 | div (float, optional): The dividend to be used to split the curve. Defaults to 0.75. 19 | 20 | Returns: 21 | List[TPoint]: The list of points. 22 | """ 23 | points = [] 24 | length = calcCubicArcLength(p[0], p[1], p[2], p[3]) 25 | t = 0 26 | step = div / length 27 | # print("Length:", d, "Steps:", step) 28 | while t < 1: 29 | points.append(getCubicPoint(t, p[0], p[1], p[2], p[3])) 30 | t += step 31 | points.append(p[3]) 32 | return points 33 | 34 | 35 | def getPathFromPoints(points: Sequence[TPoint]) -> List[Tuple[TPoint, ...]]: 36 | """Return a path with lines and curves from a sequence of points. 37 | 38 | Args: 39 | points (Sequence[TPoint]): The list of points. 40 | 41 | Returns: 42 | List[Tuple[TPoint, ...]]: The path. 43 | """ 44 | curve_points = SCBezierPath().fromPoints( 45 | [SCPoint(x, y) for x, y in points], 46 | error=1.0, 47 | cornerTolerance=1.0, 48 | maxSegments=10000, 49 | ) 50 | 51 | # Reconvert Simon's BezierPath segments to our segment type 52 | curves: List[Tuple[TPoint, ...]] = [] 53 | first = True 54 | for segment in curve_points.asSegments(): 55 | if first: 56 | # For the first segment, add the move point 57 | p = segment[0] 58 | curves.append(((p.x, p.y),)) 59 | first = False 60 | # Do we have to check the length of the tuples? Or are they always cubic curves 61 | # with 3 points? 62 | curves.append(tuple([(p.x, p.y) for p in segment[1:]])) 63 | return curves 64 | 65 | 66 | def angleBetweenPoints(p0: TPoint, p1: TPoint) -> float: 67 | """Return the angle between two points. 68 | 69 | Args: 70 | p0 (TPoint): The first point. 71 | p1 (TPoint): The second point. 72 | 73 | Returns: 74 | float: The angle in radians. 75 | """ 76 | return atan2(p1[1] - p0[1], p1[0] - p0[0]) 77 | 78 | 79 | def distanceBetweenPoints(p0: TPoint, p1: TPoint, doRound=False) -> float | int: 80 | """Return the distance between two points. 81 | 82 | Args: 83 | p0 (TPoint): The first point. 84 | p1 (TPoint): The second point. 85 | doRound (bool, optional): Whether to round the distance. Defaults to False. 86 | 87 | Returns: 88 | float | int: The distance. 89 | """ 90 | d = sqrt((p0[0] - p1[0]) ** 2 + (p0[1] - p1[1]) ** 2) 91 | if doRound: 92 | return round(d) 93 | else: 94 | return d 95 | 96 | 97 | def halfPoint(p0: TPoint, p1: TPoint, doRound=False) -> TPoint: 98 | """Return a point halfway between two points. 99 | 100 | Args: 101 | p0 (TPoint): The first point. 102 | p1 (TPoint): The second point. 103 | doRound (bool, optional): Whether to round the coordinates of the halfway point. Defaults to False. 104 | 105 | Returns: 106 | TPoint: The halfway point. 107 | """ 108 | x0, y0 = p0 109 | x1, y1 = p1 110 | xh = 0.5 * (x0 + x1) 111 | yh = 0.5 * (y0 + y1) 112 | if doRound: 113 | return round(xh), round(yh) 114 | return xh, yh 115 | 116 | 117 | class Triangle(object): 118 | """ 119 | A triangle with points A, B, C; sides a, b, c; angles α, β, γ. 120 | 121 | C 122 | . γ .. 123 | b . .. a 124 | . .. 125 | . α β.. 126 | A . . . . . . . . . . . . . B 127 | c 128 | """ 129 | 130 | def __init__(self, A: TPoint, B: TPoint, C: TPoint) -> None: 131 | """Initialize the triangle with three points A, B, C. 132 | 133 | Args: 134 | A (TPoint): The point A. 135 | B (TPoint): The point B. 136 | C (TPoint): The point C. 137 | """ 138 | self.A = A 139 | self.B = B 140 | self.C = C 141 | 142 | @cached_property 143 | def sides(self) -> Tuple[float, float, float]: 144 | """Return the lengths of the three sides of the triangle. 145 | 146 | Returns: 147 | Tuple[float, float, float]: The lengths of the sides a, b, c. 148 | """ 149 | self.a = distanceBetweenPoints(self.B, self.C) 150 | self.b = distanceBetweenPoints(self.A, self.C) 151 | self.c = distanceBetweenPoints(self.A, self.B) 152 | return self.a, self.b, self.c 153 | 154 | @cached_property 155 | def height_a(self) -> float: 156 | """Return the height over side a. 157 | 158 | Returns: 159 | float: The height. 160 | """ 161 | a, b, c = self.sides 162 | s = (a + b + c) / 2 163 | h = 2 * sqrt(s * (s - a) * (s - b) * (s - c)) / a 164 | return h 165 | 166 | 167 | def optimizePointPath(p: Sequence[TPoint], dist=0.49) -> List[TPoint]: 168 | """Return an optimized version of a list of points. A points will be skipped unless 169 | its distance from the line formed by its previous to its next point is greater than 170 | `dist`. 171 | 172 | Args: 173 | p (Sequence[TPoint]): The path as a sequence of points. 174 | dist (float, optional): The maximum distance. Defaults to 0.49. 175 | 176 | Returns: 177 | Sequence[TPoint]: The optimized sequence of points. 178 | """ 179 | num_points = len(p) 180 | p0 = p[0] 181 | optimized = [p0] # Keep the first point of the original path 182 | i = 0 183 | while i < num_points - 2: 184 | p1 = p[i + 1] 185 | p2 = p[i + 2] 186 | t = Triangle(p0, p2, p1) 187 | if t.height_a > dist: 188 | optimized.append(p1) 189 | p0 = p[i] 190 | else: 191 | pass 192 | i += 1 193 | optimized.append(p[-1]) # Keep the last point of the original path 194 | return optimized 195 | -------------------------------------------------------------------------------- /lib/nibLib/pens/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from nibLib.pens.ovalNibPen import OvalNibPen 4 | from nibLib.pens.rectNibPen import RectNibPen 5 | from nibLib.pens.superellipseNibPen import SuperellipseNibPen 6 | 7 | 8 | nib_models = { 9 | "Superellipse": SuperellipseNibPen, 10 | "Oval": OvalNibPen, 11 | "Rectangle": RectNibPen, 12 | } 13 | -------------------------------------------------------------------------------- /lib/nibLib/pens/bezier.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from fontTools.misc.bezierTools import ( 4 | calcCubicParameters, 5 | solveQuadratic, 6 | splitCubicAtT, 7 | epsilon, 8 | ) 9 | from fontTools.misc.transform import Transform 10 | from nibLib.typing import TPoint 11 | from typing import List, Tuple 12 | 13 | 14 | def normalize_quadrant(q: float) -> float: 15 | r = 2 * q 16 | nearest = round(r) 17 | e = abs(nearest - r) 18 | if e > epsilon: 19 | return q 20 | rounded = nearest * 0.5 21 | return rounded 22 | 23 | 24 | def split_at_extrema( 25 | pt1: TPoint, pt2: TPoint, pt3: TPoint, pt4: TPoint, transform=Transform() 26 | ) -> List[Tuple[TPoint, TPoint, TPoint, TPoint]]: 27 | """ 28 | Add extrema to a cubic curve, after applying a transformation. 29 | Example :: 30 | 31 | >>> # A segment in which no extrema will be added. 32 | >>> split_at_extrema((297, 52), (406, 52), (496, 142), (496, 251)) 33 | [((297, 52), (406, 52), (496, 142), (496, 251))] 34 | >>> from fontTools.misc.transform import Transform 35 | >>> split_at_extrema((297, 52), (406, 52), (496, 142), (496, 251), Transform().rotate(-27)) 36 | [((297.0, 52.0), (84.48072108963274, -212.56513799170233), (15.572491694678519, -361.3686192413668), (15.572491694678547, -445.87035970621713)), ((15.572491694678547, -445.8703597062171), (15.572491694678554, -506.84825401175414), (51.4551516055374, -534.3422304091257), (95.14950889754756, -547.6893014808263))] 37 | """ 38 | # Transform the points for extrema calculation; 39 | # transform is expected to rotate the points by - nib angle. 40 | t2, t3, t4 = transform.transformPoints([pt2, pt3, pt4]) 41 | # When pt1 is the current point of the path, it is already transformed, so 42 | # we keep it like it is. 43 | t1 = pt1 44 | 45 | (ax, ay), (bx, by), c, d = calcCubicParameters(t1, t2, t3, t4) 46 | ax *= 3.0 47 | ay *= 3.0 48 | bx *= 2.0 49 | by *= 2.0 50 | 51 | # vertical 52 | roots = [t for t in solveQuadratic(ay, by, c[1]) if 0 < t < 1] 53 | 54 | # horizontal 55 | roots += [t for t in solveQuadratic(ax, bx, c[0]) if 0 < t < 1] 56 | 57 | # Use only unique roots and sort them 58 | # They should be unique before, or can a root be duplicated (in h and v?) 59 | roots = sorted(set(roots)) 60 | 61 | if not roots: 62 | return [(t1, t2, t3, t4)] 63 | 64 | return splitCubicAtT(t1, t2, t3, t4, *roots) 65 | 66 | 67 | if __name__ == "__main__": 68 | import sys 69 | import doctest 70 | 71 | sys.exit(doctest.testmod().failed) 72 | -------------------------------------------------------------------------------- /lib/nibLib/pens/drawing.py: -------------------------------------------------------------------------------- 1 | from AppKit import NSBezierPath, NSColor 2 | 3 | 4 | from nibLib.typing import TPoint 5 | from typing import Sequence 6 | 7 | 8 | def draw_path(path: Sequence[Sequence[TPoint]] | None, width=1.0) -> None: 9 | """ 10 | Build a NSBezierPath from `path`. The NSBezierPath is then drawn in the current 11 | context. 12 | """ 13 | 14 | if not path: 15 | return 16 | 17 | subpath = NSBezierPath.alloc().init() 18 | subpath.setLineWidth_(width) 19 | # subpath.setStrokeColor_(NSColor()) 20 | subpath.moveToPoint_(path[0][0]) 21 | for p in path[1:]: 22 | if len(p) == 3: 23 | # curve 24 | A, B, C = p 25 | subpath.curveToPoint_controlPoint1_controlPoint2_(C, A, B) 26 | else: 27 | subpath.lineToPoint_(p[0]) 28 | 29 | subpath.closePath() 30 | NSColor.colorWithCalibratedRed_green_blue_alpha_(0, 0.2, 1, 0.5).set() 31 | subpath.stroke() 32 | -------------------------------------------------------------------------------- /lib/nibLib/pens/nibPen.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from fontTools.misc.transform import Transform 4 | from fontTools.pens.basePen import BasePen 5 | from math import pi 6 | from nibLib.pens.drawing import draw_path 7 | from nibLib.typing import TPoint 8 | from typing import Sequence 9 | 10 | 11 | class NibPen(BasePen): 12 | def __init__( 13 | self, 14 | glyphSet, 15 | angle: float, # The nib's angle in radians. 16 | width: float, 17 | height: float, 18 | show_nib_faces=False, 19 | nib_superness=2.5, 20 | trace=False, 21 | round_coords=False, 22 | ): 23 | """The base class for all nib pens. 24 | 25 | Args: 26 | glyphSet (Dict): The glyph set, used to access components. 27 | angle (float): The nib's angle over the horizontal in radians. 28 | width (float): The width of the nib. 29 | height (float): The height of the nib. 30 | show_nib_faces (bool, optional): Whether the nib face should be drawn separately. Defaults to False. 31 | nib_superness (float, optional): The superness of the nib shape. Only used in superelliptical nibs. Defaults to 2.5. 32 | trace (bool, optional): Whether the path should be traced. Defaults to False. 33 | round_coords (bool, optional): Whether the coordinates of the resulting path should be rounded. Defaults to False. 34 | """ 35 | BasePen.__init__(self, glyphSet) 36 | 37 | # Reduce the angle if it is greater than 180° or smaller than -180° 38 | self.angle = angle 39 | if self.angle > pi: 40 | self.angle -= pi 41 | elif self.angle < -pi: 42 | self.angle += pi 43 | 44 | # Store a transform, used for calculating extrema in some nib models 45 | self.transform = Transform().rotate(-self.angle) 46 | self.transform_reverse = Transform().rotate(self.angle) 47 | 48 | self.width = width 49 | self.height = height 50 | 51 | # Cache the half width and height 52 | self.a = 0.5 * width 53 | self.b = 0.5 * height 54 | 55 | # Used for drawing 56 | self.color = show_nib_faces 57 | self.highlight_nib_faces = False 58 | self._scale = 1.0 59 | 60 | # Used for superelliptical nibs 61 | self.nib_superness = nib_superness 62 | 63 | # Whether to trace the path; otherwise it is just drawn for preview 64 | self.trace = trace 65 | 66 | # Should the coordinates of the nib path be rounded? 67 | self.round_coords = round_coords 68 | 69 | # Initialize the nib face path 70 | # This is only needed for more complex shapes 71 | self.setup_nib() 72 | 73 | self.path = [] 74 | 75 | self._currentPoint: TPoint | None = None 76 | 77 | def round_pt(self, pt: TPoint) -> TPoint: 78 | # Round a point based on self.round_coords 79 | if not self.round_coords: 80 | return pt 81 | x, y = pt 82 | return round(x), round(y) 83 | 84 | def setup_nib(self) -> None: 85 | pass 86 | 87 | def addComponent(self, baseName: str, transformation: Transform) -> None: 88 | """Components are ignored. 89 | 90 | Args: 91 | baseName (str): The base glyph name of the component. 92 | transformation (Transform): The component's transformation. 93 | """ 94 | pass 95 | 96 | def addPath(self, path: Sequence[Sequence[TPoint]] | None = None) -> None: 97 | """ 98 | Add a path to the nib path. 99 | """ 100 | if path is None: 101 | return 102 | 103 | tr_path = [self.transform_reverse.transformPoints(pts) for pts in path] 104 | if self.trace: 105 | self.path.append(tr_path) 106 | else: 107 | draw_path(tr_path, width=1 / self._scale) 108 | 109 | def addPathRaw(self, path: Sequence[Sequence[TPoint]] | None = None) -> None: 110 | """ 111 | Add a path to the nib path. The path is added as is, i.e. the points must have 112 | been transformed by the caller. 113 | """ 114 | if path is None: 115 | return 116 | 117 | if self.trace: 118 | self.path.append(path) 119 | else: 120 | draw_path(path, width=1 / self._scale) 121 | 122 | def trace_path(self, out_glyph, clear=True) -> None: 123 | """Trace the path into the supplied glyph. 124 | 125 | Args: 126 | out_glyph (RGlyph): The glyph to receive the traced path. 127 | """ 128 | if clear: 129 | out_glyph.clear() 130 | p = out_glyph.getPen() 131 | first = True 132 | for path in self.path: 133 | if first: 134 | first = False 135 | else: 136 | p.closePath() 137 | p.moveTo(self.round_pt(path[0][0])) 138 | for segment in path[1:]: 139 | if len(segment) == 1: 140 | p.lineTo(self.round_pt(segment[0])) 141 | elif len(segment) == 3: 142 | p.curveTo( 143 | self.round_pt(segment[0]), 144 | self.round_pt(segment[1]), 145 | self.round_pt(segment[2]), 146 | ) 147 | 148 | else: 149 | print("Unknown segment type:", segment) 150 | p.closePath() 151 | # tmp.correctDirection() 152 | # out_glyph.removeOverlap() 153 | # out_glyph.removeOverlap() 154 | # out_glyph.update() 155 | -------------------------------------------------------------------------------- /lib/nibLib/pens/ovalNibPen.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from math import atan2, cos, degrees, sin, tan 4 | from nibLib.typing import TPoint 5 | 6 | 7 | # from nibLib import DEBUG_CENTER_POINTS, DEBUG_CURVE_POINTS 8 | from nibLib.geometry import ( 9 | angleBetweenPoints, 10 | getPointsFromCurve, 11 | optimizePointPath, 12 | ) 13 | from nibLib.pens.rectNibPen import RectNibPen 14 | 15 | 16 | class OvalNibPen(RectNibPen): 17 | def _get_tangent_point(self, alpha: float) -> TPoint: 18 | """Return the point on the ellipse at the given angle alpha. 19 | 20 | Args: 21 | alpha (float): The angle in radians. 22 | 23 | Returns: 24 | TPoint: The tangent point. 25 | """ 26 | 27 | # Calculate the point on the ellipse 28 | # at the given tangent angle alpha. 29 | # t not the angle from the ellipse center, but just a parameter. 30 | 31 | t = atan2(-self.b, (self.a * tan(alpha))) 32 | x = self.a * cos(t) 33 | y = self.b * sin(t) 34 | 35 | return x, y 36 | 37 | def _get_rotated_tangent_point(self, pt: TPoint) -> TPoint: 38 | x, y = pt 39 | x1 = x * cos(self.angle) - y * sin(self.angle) 40 | y1 = x * sin(self.angle) + y * cos(self.angle) 41 | 42 | return x1, y1 43 | 44 | def _draw_nib_face(self, pt: TPoint) -> None: 45 | return 46 | save() 47 | # fill(*self.path_fill) 48 | # strokeWidth(0) 49 | # stroke(None) 50 | translate(pt[0], pt[1]) 51 | rotate(degrees(self.angle)) 52 | oval(-self.a, -self.b, self.width, self.height) 53 | restore() 54 | 55 | def _moveTo(self, pt: TPoint) -> None: 56 | self._currentPoint = pt 57 | self.contourStart = pt 58 | if not self.trace: 59 | self._draw_nib_face(pt) 60 | 61 | def _lineTo(self, pt: TPoint) -> None: 62 | if self._currentPoint is None: 63 | raise ValueError 64 | 65 | # angle from the previous to the current point 66 | phi = angleBetweenPoints(self._currentPoint, pt) 67 | # print(u"%0.2f°: %s -> %s" % (degrees(phi), self._currentPoint, pt)) 68 | pt0 = self._get_tangent_point(phi - self.angle) 69 | x, y = self._get_rotated_tangent_point(pt0) 70 | px, py = pt 71 | cx, cy = self._currentPoint 72 | 73 | self.addPath( 74 | [ 75 | ((cx + x, cy + y),), # move 76 | ((px + x, py + y),), # line 77 | ((px - x, py - y),), # line 78 | ((cx - x, cy - y),), # line 79 | ] 80 | ) 81 | 82 | if not self.trace: 83 | self._draw_nib_face(pt) 84 | 85 | self._currentPoint = pt 86 | 87 | def _curveToOne(self, pt1: TPoint, pt2: TPoint, pt3: TPoint) -> None: 88 | if self._currentPoint is None: 89 | raise ValueError 90 | 91 | # Break curve into line segments 92 | points = getPointsFromCurve((self._currentPoint, pt1, pt2, pt3), 5) 93 | 94 | # Draw points of center line 95 | # if DEBUG_CENTER_POINTS: 96 | # save() 97 | # stroke(None) 98 | # strokeWidth(0) 99 | # fill(0, 0, 0, self.alpha) 100 | # for x, y in points: 101 | # rect(x - 1, y - 1, 2, 2) 102 | # restore() 103 | 104 | # Calculate angles between points 105 | 106 | # The first angle is that of the curve start point to bcp1 107 | angles = [angleBetweenPoints(self._currentPoint, pt1)] 108 | 109 | for i in range(1, len(points)): 110 | phi = angleBetweenPoints(points[i - 1], points[i]) 111 | angles.append(phi) 112 | 113 | # The last angle is that of bcp2 point to the curve end point 114 | angles.append(angleBetweenPoints(pt2, pt3)) 115 | 116 | # Find points on ellipse for each angle 117 | inner = [] 118 | outer = [] 119 | 120 | # stroke(None) 121 | 122 | for i, p in enumerate(points): 123 | pt0 = self._get_tangent_point(angles[i] - self.angle) 124 | 125 | x, y = self._get_rotated_tangent_point(pt0) 126 | outer.append((p[0] + x, p[1] + y)) 127 | # if DEBUG_CURVE_POINTS: 128 | # # Draw outer points in red 129 | # save() 130 | # fill(1, 0, 0, self.alpha) 131 | # rect(p[0] + x - 1, p[1] + y - 1, 2, 2) 132 | # restore() 133 | 134 | x, y = self._get_rotated_tangent_point((-pt0[0], -pt0[1])) 135 | inner.append((p[0] + x, p[1] + y)) 136 | # if DEBUG_CURVE_POINTS: 137 | # # Draw inner points in green 138 | # save() 139 | # fill(0, 0.8, 0, self.alpha) 140 | # rect(p[0] + x - 1, p[1] + y - 1, 2, 2) 141 | # restore() 142 | 143 | if inner and outer: 144 | 145 | inner = optimizePointPath(inner, 0.3) 146 | outer = optimizePointPath(outer, 0.3) 147 | 148 | path = [] 149 | path.append((outer[0],)) # move 150 | for p in outer[1:]: 151 | path.append((p,)) # line 152 | inner.reverse() 153 | for p in inner: 154 | path.append((p,)) # line 155 | path.append((outer[0],)) # line 156 | self.addPath(path) 157 | 158 | if not self.trace: 159 | self._draw_nib_face(pt3) 160 | 161 | self._currentPoint = pt3 162 | 163 | def _closePath(self) -> None: 164 | self._currentPoint = None 165 | 166 | def _endPath(self) -> None: 167 | self._currentPoint = None 168 | -------------------------------------------------------------------------------- /lib/nibLib/pens/rectNibPen.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from math import atan2, pi 4 | from nibLib.pens.bezier import normalize_quadrant, split_at_extrema 5 | from nibLib.pens.nibPen import NibPen 6 | from nibLib.typing import TPoint 7 | from typing import Tuple 8 | 9 | 10 | class RectNibPen(NibPen): 11 | def transformPoint(self, pt: TPoint, d=1) -> TPoint: 12 | return ( 13 | pt[0] + self.a * d, 14 | pt[1] + self.b, 15 | ) 16 | 17 | def transformPointHeight(self, pt: TPoint, d=1) -> TPoint: 18 | return ( 19 | pt[0] + self.a * d, 20 | pt[1] - self.b, 21 | ) 22 | 23 | def transformedRect(self, P: TPoint) -> Tuple[TPoint, TPoint, TPoint, TPoint]: 24 | """ 25 | Transform a point to a rect describing the four points of the nib face. 26 | 27 | D-------------------------C 28 | | P | 29 | A-------------------------B 30 | """ 31 | A = self.transformPointHeight(P, -1) 32 | B = self.transformPointHeight(P) 33 | C = self.transformPoint(P) 34 | D = self.transformPoint(P, -1) 35 | return A, B, C, D 36 | 37 | def _moveTo(self, pt: TPoint) -> None: 38 | t = self.transform.transformPoint(pt) 39 | self._currentPoint = t 40 | self.contourStart = pt 41 | 42 | def _lineTo(self, pt: TPoint) -> None: 43 | """ 44 | Points of the nib face: 45 | 46 | D1 C1 D2 C2 47 | X-------------------------X X-------------------------X 48 | | X | ------> | X | 49 | X-------------------------X X-------------------------X 50 | A1 B1 A2 B2 51 | 52 | The points A2, B2, C2, D2 are the points of the nib face translated to 53 | the end of the current stroke. 54 | """ 55 | if self._currentPoint is None: 56 | raise ValueError 57 | 58 | t = self.transform.transformPoint(pt) 59 | 60 | A1, B1, C1, D1 = self.transformedRect(self._currentPoint) 61 | A2, B2, C2, D2 = self.transformedRect(t) 62 | 63 | x1, y1 = self._currentPoint 64 | x2, y2 = t 65 | 66 | # Angle between nib and path 67 | rho = atan2(y2 - y1, x2 - x1) 68 | 69 | path = None 70 | Q = rho / pi 71 | 72 | if 0 <= Q < 0.5: 73 | path = ((A1,), (B1,), (B2,), (C2,), (D2,), (D1,)) 74 | 75 | elif 0.5 <= Q <= 1: 76 | path = ((A1,), (B1,), (C1,), (C2,), (D2,), (A2,)) 77 | 78 | elif -1 <= Q < -0.5: 79 | path = ((A2,), (B2,), (B1,), (C1,), (D1,), (D2,)) 80 | 81 | elif -0.5 <= Q < 0: 82 | path = ((A2,), (B2,), (C2,), (C1,), (D1,), (A1,)) 83 | 84 | self.addPath(path) 85 | 86 | self._currentPoint = t 87 | 88 | def _curveToOne(self, pt1, pt2, pt3): 89 | if self._currentPoint is None: 90 | raise ValueError 91 | 92 | # Insert extrema at angle 93 | segments = split_at_extrema( 94 | self._currentPoint, pt1, pt2, pt3, transform=self.transform 95 | ) 96 | for segment in segments: 97 | pt0, pt1, pt2, pt3 = segment 98 | self._curveToOneNoExtrema(pt1, pt2, pt3) 99 | 100 | def _curveToOneNoExtrema(self, pt1, pt2, pt3): 101 | if self._currentPoint is None: 102 | raise ValueError 103 | 104 | A1, B1, C1, D1 = self.transformedRect(self._currentPoint) 105 | 106 | # Control points 107 | Ac1, Bc1, Cc1, Dc1 = self.transformedRect(pt1) 108 | Ac2, Bc2, Cc2, Dc2 = self.transformedRect(pt2) 109 | 110 | # End points 111 | A2, B2, C2, D2 = self.transformedRect(pt3) 112 | 113 | # Angle at start of curve 114 | x0, y0 = self._currentPoint 115 | x1, y1 = pt1 116 | rho1 = atan2(y1 - y0, x1 - x0) 117 | 118 | # Angle at end of curve 119 | x2, y2 = pt2 120 | x3, y3 = pt3 121 | 122 | rho2 = atan2(y3 - y2, x3 - x2) 123 | 124 | path = None 125 | 126 | Q1 = normalize_quadrant(rho1 / pi) 127 | Q2 = normalize_quadrant(rho2 / pi) 128 | 129 | """ 130 | Points of the nib face: 131 | 132 | D1 C1 D2 C2 133 | X-------------------------X X-------------------------X 134 | | X | ------> | X | 135 | X-------------------------X X-------------------------X 136 | A1 B1 A2 B2 137 | 138 | The points A2, B2, C2, D2 are the points of the nib face translated to 139 | the end of the current stroke. 140 | """ 141 | if Q1 == 0: 142 | if Q2 == 0: 143 | path = ((B2,), (C2,), (Cc2, Cc1, C1), (D1,), (A1,), (Ac1, Ac2, A2)) 144 | elif 0 < Q2 < 0.5: 145 | path = ((A1,), (B1,), (Bc1, Bc2, B2), (C2,), (D2,), (Dc2, Dc1, D1)) 146 | elif 0.5 <= Q2 <= 1: 147 | path = ((A1,), (B1,), (Bc1, Bc2, B2), (C2,), (D2,), (Dc2, Dc1, D1)) 148 | elif -0.5 <= Q2 < 0: 149 | path = ((B2,), (C2,), (Cc2, Cc1, C1), (D1,), (A1,), (Ac1, Ac2, A2)) 150 | 151 | elif 0 < Q1 < 0.5: 152 | if Q2 == 0: 153 | path = ((A1,), (B1,), (Bc1, Bc2, B2), (C2,), (D2,), (Dc2, Dc1, D1)) 154 | elif 0 <= Q2 < 0.5: 155 | path = ((A1,), (B1,), (Bc1, Bc2, B2), (C2,), (D2,), (Dc2, Dc1, D1)) 156 | elif 0.5 <= Q2 <= 1: 157 | path = ((A1,), (B1,), (Bc1, Bc2, B2), (C2,), (D2,), (Dc2, Dc1, D1)) 158 | elif -1 < Q2 < -0.5: 159 | pass 160 | elif -0.5 <= Q2 < 0: 161 | path = ((B2,), (C2,), (Cc2, Cc1, C1), (D1,), (A1,), (Ac1, Ac2, A2)) 162 | 163 | elif Q1 == 0.5: 164 | if 0 <= Q2 < 0.5: 165 | path = ((A1,), (B1,), (Bc1, Bc2, B2), (C2,), (D2,), (Dc2, Dc1, D1)) 166 | elif Q2 == 0.5: 167 | path = ((A1,), (B1,), (Bc1, Bc2, B2), (C2,), (D2,), (Dc2, Dc1, D1)) 168 | elif 0.5 <= Q2 <= 1: 169 | path = ((B1,), (C1,), (Cc1, Cc2, C2), (D2,), (A2,), (Ac2, Ac1, A1)) 170 | elif Q2 == -1: 171 | path = ((B1,), (C1,), (Cc1, Cc2, C2), (D2,), (A2,), (Ac2, Ac1, A1)) 172 | 173 | elif 0.5 < Q1 < 1: 174 | if 0 <= Q2 < 0.5: 175 | path = ((A1,), (B1,), (Bc1, Bc2, B2), (C2,), (D2,), (Dc2, Dc1, D1)) 176 | elif 0.5 <= Q2 <= 1: 177 | path = ((B1,), (C1,), (Cc1, Cc2, C2), (D2,), (A2,), (Ac2, Ac1, A1)) 178 | elif Q2 == -1: 179 | path = ((B1,), (C1,), (Cc1, Cc2, C2), (D2,), (A2,), (Ac2, Ac1, A1)) 180 | elif -1 < Q2 < -0.5: 181 | path = ((A2,), (B2,), (Bc2, Bc1, B1), (C1,), (D1,), (Dc1, Dc2, D2)) 182 | elif -0.5 <= Q2 < 0: 183 | path = ((A2,), (B2,), (Bc2, Bc1, B1), (C1,), (D1,), (Dc1, Dc2, D2)) 184 | 185 | elif Q1 == 1: 186 | if 0 <= Q2 < 0.5: 187 | path = ((A1,), (B1,), (Bc1, Bc2, B2), (C2,), (D2,), (Dc2, Dc1, D1)) 188 | elif Q2 == 0.5: 189 | path = ((B1,), (C1,), (Cc1, Cc2, C2), (D2,), (A2,), (Ac2, Ac1, A1)) 190 | elif 0.5 < Q2 < 1: 191 | path = ((B1,), (C1,), (Cc1, Cc2, C2), (D2,), (A2,), (Ac2, Ac1, A1)) 192 | elif Q2 == 1: 193 | path = ((B1,), (C1,), (Cc1, Cc2, C2), (D2,), (A2,), (Ac2, Ac1, A1)) 194 | elif Q2 == -1: 195 | path = ((B1,), (C1,), (Cc1, Cc2, C2), (D2,), (A2,), (Ac2, Ac1, A1)) 196 | elif -1 < Q2 < -0.5: 197 | path = ((A2,), (B2,), (Bc2, Bc1, B1), (C1,), (D1,), (Dc1, Dc2, D2)) 198 | elif -0.5 <= Q2 < 0: 199 | path = ((A2,), (B2,), (Bc2, Bc1, B1), (C1,), (D1,), (Dc1, Dc2, D2)) 200 | 201 | elif Q1 == -1: 202 | if 0 <= Q2 < 0.5: 203 | path = ((A1,), (B1,), (Bc1, Bc2, B2), (C2,), (D2,), (Dc2, Dc1, D1)) 204 | elif Q2 == 0.5: 205 | path = ((B1,), (C1,), (Cc1, Cc2, C2), (D2,), (A2,), (Ac2, Ac1, A1)) 206 | elif 0.5 < Q2 < 1: 207 | path = ((B1,), (C1,), (Cc1, Cc2, C2), (D2,), (A2,), (Ac2, Ac1, A1)) 208 | elif Q2 == 1: 209 | path = ((B1,), (C1,), (Cc1, Cc2, C2), (D2,), (A2,), (Ac2, Ac1, A1)) 210 | elif Q2 == -1: 211 | path = ((B1,), (C1,), (Cc1, Cc2, C2), (D2,), (A2,), (Ac2, Ac1, A1)) 212 | elif -1 < Q2 < -0.5: 213 | path = ((A2,), (B2,), (Bc2, Bc1, B1), (C1,), (D1,), (Dc1, Dc2, D2)) 214 | elif -0.5 <= Q2 < 0: 215 | path = ((A2,), (B2,), (Bc2, Bc1, B1), (C1,), (D1,), (Dc1, Dc2, D2)) 216 | 217 | elif -1 < Q1 < -0.5: 218 | if 0 <= Q2 < 0.5: 219 | print("Crash") 220 | elif 0.5 <= Q2 <= 1: 221 | path = ((A2,), (B2,), (Bc2, Bc1, B1), (C1,), (D1,), (Dc1, Dc2, D2)) 222 | elif Q2 == -1: 223 | path = ((A2,), (B2,), (Bc2, Bc1, B1), (C1,), (D1,), (Dc1, Dc2, D2)) 224 | elif -1 < Q2 < -0.5: 225 | path = ((A2,), (B2,), (Bc2, Bc1, B1), (C1,), (D1,), (Dc1, Dc2, D2)) 226 | elif -0.5 <= Q2 < 0: 227 | path = ((A2,), (B2,), (Bc2, Bc1, B1), (C1,), (D1,), (Dc1, Dc2, D2)) 228 | 229 | elif Q1 == -0.5: 230 | if Q2 == -1: 231 | path = ((A2,), (B2,), (Bc2, Bc1, B1), (C1,), (D1,), (Dc1, Dc2, D2)) 232 | elif -1 < Q2 < -0.5: 233 | path = ((A2,), (B2,), (Bc2, Bc1, B1), (C1,), (D1,), (Dc1, Dc2, D2)) 234 | elif Q2 == -0.5: 235 | path = ((A2,), (B2,), (Bc2, Bc1, B1), (C1,), (D1,), (Dc1, Dc2, D2)) 236 | elif -0.5 <= Q2 < 0: 237 | path = ((B2,), (C2,), (Cc2, Cc1, C1), (D1,), (A1,), (Ac1, Ac2, A2)) 238 | elif Q2 == 0.0: 239 | path = ((B2,), (C2,), (Cc2, Cc1, C1), (D1,), (A1,), (Ac1, Ac2, A2)) 240 | elif Q2 == 1: 241 | path = ((A2,), (B2,), (Bc2, Bc1, B1), (C1,), (D1,), (Dc1, Dc2, D2)) 242 | 243 | elif -0.5 <= Q1 < 0: 244 | if 0 <= Q2 < 0.5: 245 | path = ((B2,), (C2,), (Cc2, Cc1, C1), (D1,), (A1,), (Ac1, Ac2, A2)) 246 | elif 0.5 <= Q2 <= 1: 247 | path = ((A2,), (B2,), (Bc2, Bc1, B1), (C1,), (D1,), (Dc1, Dc2, D2)) 248 | elif Q2 == -1: 249 | path = ((A2,), (B2,), (Bc2, Bc1, B1), (C1,), (D1,), (Dc1, Dc2, D2)) 250 | elif -1 < Q2 < -0.5: 251 | path = ((A2,), (B2,), (Bc2, Bc1, B1), (C1,), (D1,), (Dc1, Dc2, D2)) 252 | elif -0.5 <= Q2 < 0: 253 | path = ((B2,), (C2,), (Cc2, Cc1, C1), (D1,), (A1,), (Ac1, Ac2, A2)) 254 | 255 | self.addPath(path) 256 | 257 | self._currentPoint = pt3 258 | 259 | def _closePath(self): 260 | # Glyphs calls closePath though it is not really needed there ...? 261 | self._lineTo(self.contourStart) 262 | self._currentPoint = None 263 | 264 | def _endPath(self): 265 | if self._currentPoint: 266 | # A1, B1, C1, D1 = self.transformedRect(self._currentPoint) 267 | # self.addPath(((A1,), (B1,), (C1,), (D1,))) 268 | pass 269 | self._currentPoint = None 270 | -------------------------------------------------------------------------------- /lib/nibLib/pens/superellipseNibPen.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import operator 4 | 5 | from fontTools.misc.transform import Transform 6 | from math import cos, degrees, pi, sin 7 | from nibLib.typing import TPoint 8 | from nibLib.geometry import ( 9 | angleBetweenPoints, 10 | getPathFromPoints, 11 | getPointsFromCurve, 12 | optimizePointPath, 13 | ) 14 | from nibLib.pens.ovalNibPen import OvalNibPen 15 | 16 | 17 | # Settings for curve trace 18 | TRACE_ERROR = 0.5 19 | TRACE_CORNER_TOLERANCE = 1.0 20 | TRACE_MAXIMUM_SEGMENTS = 1000 21 | 22 | 23 | DEBUG_CENTER_POINTS = False 24 | DEBUG_CURVE_POINTS = False 25 | 26 | 27 | class SuperellipseNibPen(OvalNibPen): 28 | def setup_nib(self) -> None: 29 | steps = 100 30 | points = [] 31 | # Build a quarter of the superellipse with the requested number of steps 32 | for i in range(0, steps + 1): 33 | t = i * 0.5 * pi / steps 34 | points.append( 35 | ( 36 | self.a * cos(t) ** (2 / self.nib_superness), 37 | self.b * sin(t) ** (2 / self.nib_superness), 38 | ) 39 | ) 40 | try: 41 | points = optimizePointPath(points, 0.02) 42 | except: 43 | print("Error optimizing point path.") 44 | pass 45 | 46 | # Just add the remaining three quarters by transposing the existing points 47 | points.extend([(-x, y) for x, y in reversed(points)]) 48 | points.extend([(x, -y) for x, y in reversed(points)]) 49 | 50 | self.nib_face_path = points 51 | self.nib_face_path_transformed = points.copy() 52 | self.nib_drawing_path = getPathFromPoints(points) 53 | self.cache_angle = None 54 | 55 | def _get_rotated_point(self, pt: TPoint, phi: float) -> TPoint: 56 | x, y = pt 57 | cp = cos(phi) 58 | sp = sin(phi) 59 | x1 = x * cp - y * sp 60 | y1 = x * sp + y * cp 61 | 62 | return x1, y1 63 | 64 | def transform_nib_path(self, alpha: float) -> None: 65 | t = Transform().rotate(-alpha) 66 | self.nib_face_path_transformed = t.transformPoints(self.nib_face_path) 67 | self.cache_angle = alpha 68 | 69 | def _get_tangent_point(self, alpha: float) -> TPoint: 70 | 71 | # Calculate the point on the superellipse 72 | # at the given tangent angle alpha. 73 | 74 | # For now, we do this the pedestrian way, until I can figure out 75 | # how to calculate the tangent point directly. 76 | 77 | if self.cache_angle != alpha: 78 | self.transform_nib_path(alpha) 79 | 80 | x, y = max(self.nib_face_path_transformed, key=operator.itemgetter(1)) 81 | x, y = Transform().rotate(alpha).transformPoint((x, y)) # .rotate(-self.angle) 82 | return x, y 83 | 84 | def _moveTo(self, pt: TPoint) -> None: 85 | t = self.transform.transformPoint(pt) 86 | self._currentPoint = t 87 | self.contourStart = pt 88 | self._draw_nib_face(pt) 89 | 90 | def _lineTo(self, pt: TPoint) -> None: 91 | if self._currentPoint is None: 92 | raise ValueError 93 | 94 | cx, cy = self._currentPoint 95 | 96 | t = self.transform.transformPoint(pt) 97 | tx, ty = t 98 | 99 | # angle from the previous to the current point 100 | phi = angleBetweenPoints(self._currentPoint, t) 101 | # print(u"%0.2f°: %s -> %s" % (degrees(phi), self._currentPoint, pt)) 102 | 103 | x, y = self._get_tangent_point(phi) 104 | 105 | p0 = (cx + x, cy + y) 106 | p1 = (tx + x, ty + y) 107 | p2 = (tx - x, ty - y) 108 | p3 = (cx - x, cy - y) 109 | 110 | self.addPath([[p0], [p3], [p2], [p1]]) 111 | self._draw_nib_face(pt) 112 | 113 | self._currentPoint = t 114 | 115 | def _curveToOne(self, pt1: TPoint, pt2: TPoint, pt3: TPoint) -> None: 116 | if self._currentPoint is None: 117 | raise ValueError 118 | 119 | # if not self.trace and DEBUG_CENTER_POINTS or DEBUG_CURVE_POINTS: 120 | # save() 121 | 122 | t1 = self.transform.transformPoint(pt1) 123 | t2 = self.transform.transformPoint(pt2) 124 | t3 = self.transform.transformPoint(pt3) 125 | 126 | # Break curve into line segments 127 | points = getPointsFromCurve((self._currentPoint, t1, t2, t3), 5) 128 | 129 | # Draw points of center line 130 | # if DEBUG_CENTER_POINTS: 131 | # stroke(None) 132 | # strokeWidth(0) 133 | # fill(0, 0, 0, self.alpha) 134 | # for p in points: 135 | # x, y = self.transform_reverse.transformPoint(p) 136 | # rect(x - 1, y - 1, 2, 2) 137 | 138 | # Calculate angles between points 139 | 140 | # The first angle is that of the curve start point to bcp1 141 | angles = [angleBetweenPoints(self._currentPoint, t1)] 142 | 143 | for i in range(1, len(points)): 144 | phi = angleBetweenPoints(points[i - 1], points[i]) 145 | angles.append(phi) 146 | 147 | # The last angle is that of bcp2 point to the curve end point 148 | angles.append(angleBetweenPoints(t2, t3)) 149 | 150 | # Find points on ellipse for each angle 151 | inner = [] 152 | outer = [] 153 | 154 | # stroke(None) 155 | for i, p in enumerate(points): 156 | x, y = self._get_tangent_point(angles[i]) 157 | 158 | pp = self._get_rotated_point((p[0] + x, p[1] + y), self.angle) 159 | outer.append(pp) 160 | 161 | if not self.trace and DEBUG_CURVE_POINTS: 162 | # Draw outer points in red 163 | fill(1, 0, 0, self.alpha) 164 | pr = self.transform_reverse.transformPoint((p[0] + x, p[1] + y)) 165 | rect(pr[0] - 1, pr[1] - 1, 2, 2) 166 | 167 | pp = self._get_rotated_point((p[0] - x, p[1] - y), self.angle) 168 | inner.append(pp) 169 | if not self.trace and DEBUG_CURVE_POINTS: 170 | # Draw inner points in green 171 | fill(0, 0.8, 0, self.alpha) 172 | pr = self.transform_reverse.transformPoint((p[0] - x, p[1] - y)) 173 | rect(pr[0] - 1, pr[1] - 1, 2, 2) 174 | 175 | if inner and outer: 176 | if self.trace: 177 | outer.reverse() 178 | outer = getPathFromPoints(outer) 179 | inner = getPathFromPoints(inner) 180 | self.path.append(outer + inner) 181 | else: 182 | inner = optimizePointPath(inner, 0.3) 183 | outer = optimizePointPath(outer, 0.3) 184 | outer.reverse() 185 | optimized = optimizePointPath(outer + inner, 1) 186 | self.addPath([[self.transform.transformPoint(o)] for o in optimized]) 187 | self._draw_nib_face(pt3) 188 | 189 | self._currentPoint = t3 190 | # if not self.trace and DEBUG_CENTER_POINTS or DEBUG_CURVE_POINTS: 191 | # restore() 192 | 193 | def _closePath(self) -> None: 194 | self.lineTo(self.contourStart) 195 | self._currentPoint = None 196 | 197 | def _endPath(self) -> None: 198 | self._currentPoint = None 199 | 200 | def _draw_nib_face(self, pt: TPoint) -> None: 201 | x, y = pt 202 | nib = [] 203 | t = Transform().translate(x, y).rotate(self.angle) 204 | for seg in self.nib_drawing_path: 205 | seg_path = [] 206 | for p in seg: 207 | seg_path.append(t.transformPoint(p)) 208 | nib.append(seg_path) 209 | self.addPathRaw(nib) 210 | -------------------------------------------------------------------------------- /lib/nibLib/typing.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple, TypeAlias 2 | 3 | TPoint: TypeAlias = Tuple[float, float] 4 | CCurve: TypeAlias = Tuple[TPoint, TPoint, TPoint, TPoint] 5 | -------------------------------------------------------------------------------- /lib/nibLib/ui/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import vanilla 4 | 5 | from defconAppKit.windows.baseWindow import BaseWindowController 6 | from math import degrees, radians 7 | from nibLib import ( 8 | def_angle_key, 9 | def_width_key, 10 | def_height_key, 11 | def_local_key, 12 | def_guide_key, 13 | def_super_key, 14 | def_model_key, 15 | ) 16 | from nibLib.pens import nib_models 17 | from typing import Any, List 18 | 19 | 20 | class JKNib(BaseWindowController): 21 | settings_attr = "lib" 22 | 23 | def __init__(self, glyph, font, caller=None) -> None: 24 | self.model = "Superellipse" 25 | self.angle = radians(30) 26 | self.width = 60 27 | self.height = 2 28 | self.superness = 2.5 29 | self.line_join = "round" # bevel, round 30 | self.font_layers = [] 31 | self._guide_layer_name: str | None = None 32 | self.guide_layer = None 33 | self.nib_pen = nib_models[self.model] 34 | 35 | self._draw_nib_faces = False 36 | self._draw_in_preview_mode = False 37 | 38 | self._glyph = glyph 39 | self._font = font 40 | self.caller = caller 41 | 42 | # window dimensions 43 | width = 300 44 | height = 322 45 | 46 | self.w = vanilla.FloatingWindow((width, height), "Nib Simulator") 47 | 48 | col = 60 49 | y = 10 50 | 51 | self.w.model_label = vanilla.TextBox((8, y, col - 8, 20), "Model") 52 | self.w.model_select = vanilla.PopUpButton( 53 | (col, y, -48, 20), 54 | nib_models.keys(), 55 | callback=self._model_select_callback, 56 | ) 57 | 58 | y += 32 59 | self.w.angle_label = vanilla.TextBox((8, y, col - 8, 20), "Angle") 60 | self.w.angle_slider = vanilla.Slider( 61 | (col, y, -48, 20), 62 | minValue=0, 63 | maxValue=180, 64 | value=30, 65 | tickMarkCount=7, 66 | callback=self._nib_angle_callback, 67 | stopOnTickMarks=False, 68 | ) 69 | self.w.angle_text = vanilla.TextBox( 70 | (-40, y, -8, 20), "%i" % int(round(degrees(self.angle))) 71 | ) 72 | 73 | y += 24 74 | 75 | self.w.width_label = vanilla.TextBox((8, y, col - 8, 20), "Width") 76 | self.w.width_slider = vanilla.Slider( 77 | (col, y, -48, 20), 78 | minValue=0, 79 | maxValue=200, 80 | value=self.width, 81 | # tickMarkCount=7, 82 | callback=self._nib_width_callback, 83 | # stopOnTickMarks=False, 84 | ) 85 | self.w.width_text = vanilla.TextBox((-40, y, -8, 20), "%i" % self.width) 86 | 87 | y += 24 88 | 89 | self.w.height_label = vanilla.TextBox((8, y, col - 8, 20), "Height") 90 | self.w.height_slider = vanilla.Slider( 91 | (col, y, -48, 20), 92 | minValue=1, 93 | maxValue=200, 94 | value=self.height, 95 | # tickMarkCount=7, 96 | callback=self._nib_height_callback, 97 | # stopOnTickMarks=False, 98 | ) 99 | self.w.height_text = vanilla.TextBox((-40, y, -8, 20), "%i" % self.height) 100 | 101 | y += 24 102 | 103 | self.w.superness_label = vanilla.TextBox((8, y, col - 8, 20), "Super") 104 | self.w.superness_slider = vanilla.Slider( 105 | (col, y, -48, 20), 106 | minValue=1.1, 107 | maxValue=12.0, 108 | value=self.superness, 109 | callback=self._nib_superness_callback, 110 | ) 111 | self.w.superness_text = vanilla.TextBox( 112 | (-40, y, -8, 20), "%0.2f" % self.superness 113 | ) 114 | 115 | y += 32 116 | self.w.guide_label = vanilla.TextBox((8, y, col - 8, 20), "Guide") 117 | self.w.guide_select = vanilla.PopUpButton( 118 | (col, y, -48, 20), 119 | [], 120 | callback=self._guide_select_callback, 121 | ) 122 | 123 | y += 32 124 | self.w.glyph_local = vanilla.CheckBox( 125 | (col, y, -40, 20), 126 | "Glyph Uses Local Parameters", 127 | callback=self._glyph_local_callback, 128 | value=False, 129 | ) 130 | 131 | y += 32 132 | self.w.display_label = vanilla.TextBox((8, y, col - 8, 20), "Display") 133 | self.w.draw_space = vanilla.CheckBox( 134 | (col, y, -48, 20), 135 | "Draw In Space Center", 136 | callback=self._draw_space_callback, 137 | value=False, 138 | ) 139 | 140 | y += 24 141 | self.w.draw_preview = vanilla.CheckBox( 142 | (col, y, -48, 20), 143 | "Draw In Preview Mode", 144 | callback=self._draw_preview_callback, 145 | value=False, 146 | ) 147 | 148 | y += 24 149 | self.w.draw_faces = vanilla.CheckBox( 150 | (col, y, -48, 20), 151 | "Draw Nib Faces In RGB", 152 | callback=self._draw_faces_callback, 153 | value=False, 154 | ) 155 | 156 | y += 32 157 | self.w.trace_outline = vanilla.Button( 158 | (col, y, 120, 20), title="Trace Outline", callback=self._trace_callback 159 | ) 160 | 161 | self.envSpecificInit() 162 | self.load_settings() 163 | self._update_layers() 164 | # self._update_ui() 165 | # self.w.trace_outline.enable(False) 166 | self.w.bind("close", self.windowCloseCallback) 167 | self.w.open() 168 | self._update_current_glyph_view() 169 | 170 | @property 171 | def font(self): 172 | """ 173 | Return the font or master, whatever stores the nib settings. 174 | 175 | Returns: 176 | None | GSFontMaster | RFont: The font. 177 | """ 178 | return self._font 179 | 180 | @font.setter 181 | def font(self, value) -> None: 182 | if value != self._font: 183 | self.save_settings() 184 | 185 | self._font = value 186 | if value is None: 187 | self.glyph = None 188 | self.guide_layer_name = None 189 | else: 190 | self.load_settings() 191 | 192 | @property 193 | def glyph(self): 194 | """ 195 | Return the glyph or layer, whatever the nib writes in. 196 | 197 | Returns: 198 | None | GSLayer | RGlyph: The layer. 199 | """ 200 | return self._glyph 201 | 202 | @glyph.setter 203 | def glyph(self, value) -> None: 204 | print(f"Set glyph: {value}") 205 | if value != self._glyph: 206 | self.save_settings() 207 | 208 | self._glyph = value 209 | if value is None: 210 | self.guide_layer_name = None 211 | else: 212 | self.load_settings() 213 | # Update the layers; also tries to find the current guide layer in the new 214 | # glyph 215 | self._update_layers() 216 | 217 | def envSpecificInit(self) -> None: 218 | pass 219 | 220 | def windowCloseCallback(self, sender) -> None: 221 | if self.font is not None: 222 | self.save_settings() 223 | if self.caller is not None: 224 | self.caller.window_will_close() 225 | self.envSpecificQuit() 226 | self._update_current_glyph_view() 227 | 228 | def envSpecificQuit(self) -> None: 229 | pass 230 | 231 | def _update_current_glyph_view(self) -> None: 232 | # Overwrite with editor-specific update call 233 | pass 234 | 235 | def _update_layers(self) -> None: 236 | """ 237 | Called when the layer list in the UI should be updated. Sets the UI layer 238 | list to the new layer names and selects the default guide layer. 239 | """ 240 | cur_layer = self.guide_layer_name 241 | self.font_layers = self.getLayerList() 242 | self.w.guide_select.setItems(self.font_layers) 243 | if self.font_layers: 244 | if cur_layer in self.font_layers: 245 | self.guide_layer_name = cur_layer 246 | else: 247 | last_layer = len(self.font_layers) - 1 248 | self.w.guide_select.set(last_layer) 249 | self.guide_layer_name = self.font_layers[last_layer] 250 | 251 | def getLayerList(self) -> List[str]: 252 | """Return a list of layer names. The user can choose the guide layer from those. 253 | 254 | Returns: 255 | List[str]: The list of layer names. 256 | """ 257 | return [] 258 | 259 | def _update_ui(self) -> None: 260 | i = 0 261 | for i, model in enumerate(self.w.model_select.getItems()): 262 | if model == self.model: 263 | break 264 | self.w.model_select.set(i) 265 | if self.model in nib_models: 266 | self.nib_pen = nib_models[self.model] 267 | else: 268 | self.nib_pen = nib_models[list(nib_models.keys())[0]] 269 | self.w.angle_slider.set(degrees(self.angle)) 270 | self.w.angle_text.set("%i" % int(round(degrees(self.angle)))) 271 | self.w.width_slider.set(self.width) 272 | self.w.width_text.set("%i" % self.width) 273 | self.w.width_slider.setMinValue(self.height + 1) 274 | self.w.height_slider.set(self.height) 275 | self.w.height_text.set("%i" % self.height) 276 | self.w.height_slider.set(self.height) 277 | self.w.superness_text.set("%0.2f" % self.superness) 278 | self.w.superness_slider.set(self.superness) 279 | if self.font is None: 280 | self.w.guide_select.setItems([]) 281 | else: 282 | if self.guide_layer_name in self.font_layers: 283 | self.w.guide_select.setItems(self.font_layers) 284 | self.w.guide_select.set(self.font_layers.index(self.guide_layer_name)) 285 | else: 286 | self._update_layers() 287 | self.check_secondary_ui() 288 | 289 | def check_secondary_ui(self) -> None: 290 | if self.model == "Superellipse": 291 | self.w.superness_slider.enable(True) 292 | else: 293 | self.w.superness_slider.enable(False) 294 | # if self.model == "Rectangle": 295 | # self.w.draw_faces.enable(True) 296 | # else: 297 | # self.w.draw_faces.enable(False) 298 | 299 | @property 300 | def guide_layer_name(self) -> str | None: 301 | return self._guide_layer_name 302 | 303 | @guide_layer_name.setter 304 | def guide_layer_name(self, value: str | None) -> None: 305 | self._guide_layer_name = value 306 | if self._guide_layer_name is None: 307 | self.w.guide_select.setItem(None) 308 | self.guide_layer = None 309 | else: 310 | if self.w.guide_select.getItem() != self._guide_layer_name: 311 | self.w.guide_select.setItem(self._guide_layer_name) 312 | self._set_guide_layer(self._guide_layer_name) 313 | self._update_current_glyph_view() 314 | 315 | def _set_guide_layer(self, name: str) -> None: 316 | """ 317 | Override in subclass to set self.guide_layer to the actual layer object. 318 | """ 319 | raise NotImplementedError 320 | 321 | def _guide_select_callback(self, sender) -> None: 322 | """ 323 | User selected the guide layer from the list 324 | """ 325 | name = sender.getItem() 326 | print(f"Selected layer: {name}") 327 | self.guide_layer_name = name 328 | 329 | def _model_select_callback(self, sender) -> None: 330 | self.model = self.w.model_select.getItems()[sender.get()] 331 | self.nib_pen = nib_models[self.model] 332 | self.check_secondary_ui() 333 | self._update_current_glyph_view() 334 | 335 | def _nib_angle_callback(self, sender) -> None: 336 | angle = int(round(sender.get())) 337 | self.angle = radians(angle) 338 | self.w.angle_text.set("%i" % angle) 339 | self._update_current_glyph_view() 340 | 341 | def _nib_width_callback(self, sender) -> None: 342 | self.width = int(round(sender.get())) 343 | self.w.width_text.set("%i" % self.width) 344 | self.w.height_slider.setMaxValue(self.width) 345 | self._update_current_glyph_view() 346 | 347 | def _nib_height_callback(self, sender) -> None: 348 | self.height = int(round(sender.get())) 349 | self.w.height_text.set("%i" % self.height) 350 | self.w.width_slider.setMinValue(self.height) 351 | self._update_current_glyph_view() 352 | 353 | def _nib_superness_callback(self, sender) -> None: 354 | s = round(sender.get(), 1) 355 | if s > 6: 356 | self.superness = 6 + (s - 6) ** 2 357 | else: 358 | self.superness = s 359 | self.w.superness_text.set("%0.1f" % s) 360 | self._update_current_glyph_view() 361 | 362 | def _glyph_local_callback(self, sender) -> None: 363 | value = sender.get() 364 | # print("Local:", value) 365 | self.save_to_lib(self.glyph, def_local_key, False) 366 | if not value: 367 | self.load_settings() 368 | 369 | def _draw_space_callback(self, sender) -> None: 370 | pass 371 | 372 | def _draw_preview_callback(self, sender) -> None: 373 | self._draw_in_preview_mode = sender.get() 374 | self._update_current_glyph_view() 375 | 376 | def _draw_faces_callback(self, sender) -> None: 377 | self._draw_nib_faces = sender.get() 378 | self._update_current_glyph_view() 379 | 380 | def get_guide_representation(self, glyph, font, angle): 381 | # TODO: Rotate, add extreme points, rotate back 382 | return glyph.copy() 383 | 384 | def _trace_callback(self, sender) -> None: 385 | raise NotImplementedError 386 | 387 | def _setup_draw(self, preview=False) -> None: 388 | pass 389 | 390 | def draw_preview_glyph(self, preview=False) -> None: 391 | raise NotImplementedError 392 | 393 | def save_to_lib(self, font_or_glyph, libkey, value) -> None: 394 | if font_or_glyph is None: 395 | print("Can not save, there is nowhere to save to") 396 | return 397 | 398 | lib = getattr(font_or_glyph, self.settings_attr) 399 | if value is None: 400 | if lib and libkey in lib: 401 | del lib[libkey] 402 | else: 403 | if lib and libkey in lib: 404 | if lib[libkey] != value: 405 | lib[libkey] = value 406 | else: 407 | lib[libkey] = value 408 | 409 | def load_from_lib(self, font_or_glyph, libkey, attr=None) -> Any: 410 | if font_or_glyph is None: 411 | return False 412 | 413 | lib = getattr(font_or_glyph, self.settings_attr) 414 | value = lib.get(libkey, None) 415 | if attr is not None: 416 | if value is not None: 417 | setattr(self, attr, value) 418 | # print("load:", libkey, value) 419 | return value 420 | 421 | def save_settings(self) -> None: 422 | has_local_settings = self.w.glyph_local.get() 423 | if has_local_settings: 424 | # print("Saving settings to", self.glyph) 425 | for setting, value in [ 426 | (def_angle_key, degrees(self.angle)), 427 | (def_width_key, self.width), 428 | (def_height_key, self.height), 429 | (def_guide_key, self.guide_layer_name), 430 | (def_local_key, has_local_settings), 431 | (def_super_key, self.superness), 432 | (def_model_key, self.model), 433 | ]: 434 | self.save_to_lib(self.glyph, setting, value) 435 | else: 436 | for setting in [ 437 | def_angle_key, 438 | def_width_key, 439 | def_height_key, 440 | def_guide_key, 441 | def_local_key, 442 | ]: 443 | self.save_to_lib(self.glyph, setting, None) 444 | # print("Saving settings to", self.font) 445 | for setting, value in [ 446 | (def_angle_key, degrees(self.angle)), 447 | (def_width_key, self.width), 448 | (def_height_key, self.height), 449 | (def_guide_key, self.guide_layer_name), 450 | (def_super_key, self.superness), 451 | (def_model_key, self.model), 452 | ]: 453 | self.save_to_lib(self.font, setting, value) 454 | 455 | def load_settings(self) -> None: 456 | has_local_settings = self.load_from_lib(self.glyph, def_local_key) 457 | if has_local_settings: 458 | # print("Loading settings from glyph", self.glyph) 459 | self.w.glyph_local.set(True) 460 | angle = self.load_from_lib(self.glyph, def_angle_key) 461 | if angle is None: 462 | self.angle = 0 463 | else: 464 | self.angle = radians(angle) 465 | for setting, attr in [ 466 | (def_width_key, "width"), 467 | (def_height_key, "height"), 468 | (def_guide_key, "guide_layer_name"), 469 | (def_super_key, "superness"), 470 | (def_model_key, "model"), 471 | ]: 472 | self.load_from_lib(self.glyph, setting, attr) 473 | else: 474 | # print("Loading settings from font", self.font) 475 | self.w.glyph_local.set(False) 476 | angle = self.load_from_lib(self.font, def_angle_key) 477 | if angle is None: 478 | self.angle = 0 479 | else: 480 | self.angle = radians(angle) 481 | for setting, attr in [ 482 | (def_width_key, "width"), 483 | (def_height_key, "height"), 484 | (def_guide_key, "guide_layer_name"), 485 | (def_super_key, "superness"), 486 | (def_model_key, "model"), 487 | ]: 488 | self.load_from_lib(self.font, setting, attr) 489 | self._update_ui() 490 | -------------------------------------------------------------------------------- /lib/nibLib/ui/glyphs.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from GlyphsApp import Glyphs, GSBackgroundLayer 4 | 5 | # from GlyphsApp.drawingTools import * 6 | from nibLib.ui import JKNib 7 | from typing import List 8 | 9 | 10 | class JKNibGlyphs(JKNib): 11 | settings_attr = "userData" 12 | 13 | def envSpecificInit(self) -> None: 14 | self.w.draw_space.enable(False) 15 | self.w.draw_preview.enable(False) 16 | self.w.draw_faces.enable(False) 17 | 18 | def envSpecificQuit(self) -> None: 19 | pass 20 | 21 | def getLayerList(self) -> List[str]: 22 | return ["nib", "background"] 23 | 24 | def _update_current_glyph_view(self) -> None: 25 | # Make sure the current view gets redrawn 26 | currentTabView = Glyphs.font.currentTab 27 | if currentTabView: 28 | currentTabView.graphicView().setNeedsDisplay_(True) 29 | 30 | def _set_guide_layer(self, name: str) -> None: 31 | """ 32 | Set self.guide_layer to the actual layer object. 33 | """ 34 | if name == "background": 35 | # Use the current background 36 | if isinstance(self.glyph, GSBackgroundLayer): 37 | # print("Using the layer itself as it seems to be a background layer") 38 | self.guide_layer = self.glyph 39 | else: 40 | # print("Using the background of the current layer") 41 | self.guide_layer = self.glyph.background 42 | return 43 | 44 | mid = self.glyph.master.id 45 | found = False 46 | for layer in self.glyph.parent.layers: 47 | if name == layer.name and layer.associatedMasterId == mid: 48 | self.guide_layer = layer 49 | found = True 50 | break 51 | if not found: 52 | self.guide_layer = None 53 | 54 | def draw_preview_glyph(self, preview=False, scale=1.0) -> None: 55 | if self.guide_layer is None: 56 | return 57 | 58 | # save() 59 | # TODO: Reuse pen object. 60 | # Needs modifications to the pens before possible. 61 | p = self.nib_pen( 62 | self.font, 63 | self.angle, 64 | self.width, 65 | self.height, 66 | self._draw_nib_faces, 67 | nib_superness=self.superness, 68 | ) 69 | p._scale = scale 70 | 71 | try: 72 | self.guide_layer.draw(p) 73 | except AttributeError: 74 | print("AttributeError", self.guide_layer) 75 | pass 76 | # restore() 77 | 78 | def _trace_callback(self, sender) -> None: 79 | if self.guide_layer is None: 80 | return 81 | 82 | p = self.nib_pen( 83 | self.font, 84 | self.angle, 85 | self.width, 86 | self.height, 87 | self._draw_nib_faces, 88 | nib_superness=self.superness, 89 | trace=True, 90 | ) 91 | 92 | try: 93 | self.guide_layer.draw(p) 94 | p.trace_path(self.glyph) 95 | except AttributeError: 96 | pass 97 | -------------------------------------------------------------------------------- /lib/nibLib/ui/robofont.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | # RoboFont-internal packages 4 | # from lib.tools.extremePoints import ExtremePointPen 5 | # from math import degrees 6 | from mojo.drawingTools import fill, lineJoin, restore, save, strokeWidth, stroke 7 | from mojo.events import addObserver, removeObserver 8 | from mojo.roboFont import CurrentFont, CurrentGlyph, RGlyph 9 | from mojo.UI import UpdateCurrentGlyphView 10 | from nibLib.ui import JKNib 11 | from nibLib import DEBUG, rf_guide_key 12 | from typing import Any, List 13 | 14 | 15 | def NibGuideGlyphFactory(glyph, font, angle: float): 16 | # pen = ExtremePointPen(vertical=True, horizontal=True) 17 | g = RGlyph(glyph).copy() 18 | # g.rotateBy(degrees(-angle)) 19 | # # g.extremePoints() 20 | # g.drawPoints(pen) 21 | # g.clear() 22 | # out_pen = g.getPointPen() 23 | # pen.drawPoints(out_pen) 24 | # g.rotateBy(degrees(angle)) 25 | return g 26 | 27 | 28 | def _registerFactory(): 29 | # From https://github.com/typesupply/glyph-nanny/blob/master/Glyph%20Nanny.roboFontExt/lib/glyphNanny.py 30 | # always register if debugging 31 | # otherwise only register if it isn't registered 32 | from defcon import registerRepresentationFactory, Glyph 33 | 34 | if DEBUG: 35 | if rf_guide_key in Glyph.representationFactories: 36 | for font in AllFonts(): 37 | for glyph in font: 38 | glyph.naked().destroyAllRepresentations() 39 | registerRepresentationFactory(rf_guide_key, NibGuideGlyphFactory) 40 | else: 41 | if rf_guide_key not in Glyph.representationFactories: 42 | registerRepresentationFactory(Glyph, rf_guide_key, NibGuideGlyphFactory) 43 | 44 | 45 | def _unregisterFactory(): 46 | from defcon import unregisterRepresentationFactory, Glyph 47 | 48 | try: 49 | unregisterRepresentationFactory(Glyph, rf_guide_key) 50 | except: 51 | pass 52 | 53 | 54 | class JKNibRoboFont(JKNib): 55 | observers = ( 56 | ("_preview", "drawBackground"), 57 | ("_preview", "drawInactive"), 58 | ("_previewFull", "drawPreview"), 59 | ("_glyph_changed", "currentGlyphChanged"), 60 | ("_font_changed", "fontBecameCurrent"), 61 | ("_font_resign", "fontResignCurrent"), 62 | ) 63 | settings_attr = "lib" 64 | 65 | def __init__(self) -> None: 66 | super(JKNibRoboFont, self).__init__(CurrentGlyph(), CurrentFont()) 67 | 68 | def envSpecificInit(self) -> None: 69 | self.setUpBaseWindowBehavior() 70 | self.addObservers() 71 | _registerFactory() 72 | 73 | def envSpecificQuit(self) -> None: 74 | self.removeObservers() 75 | _unregisterFactory() 76 | 77 | def addObservers(self) -> None: 78 | for method, observer in self.observers: 79 | addObserver(self, method, observer) 80 | 81 | def removeObservers(self) -> None: 82 | for method, observer in self.observers: 83 | removeObserver(self, observer) 84 | if self.w.draw_space.get(): 85 | removeObserver(self, "spaceCenterDraw") 86 | 87 | def getLayerList(self) -> List[str]: 88 | """Return a list of layer names. The user can choose the guide layer from those. 89 | 90 | Returns: 91 | List[str]: The list of layer names. 92 | """ 93 | return ["foreground"] + self.font.layerOrder 94 | 95 | def _update_current_glyph_view(self) -> None: 96 | UpdateCurrentGlyphView() 97 | 98 | def _update_layers(self) -> None: 99 | """ 100 | Called when the layer list in the UI should be updated. Sets the UI layer 101 | list to the new layer names and selects the default guide layer. 102 | """ 103 | self.font_layers = self.getLayerList() 104 | self.w.guide_select.setItems(self.font_layers) 105 | if self.font_layers: 106 | # Select the last layer 107 | last_layer = len(self.font_layers) - 1 108 | self.w.guide_select.set(last_layer) 109 | self.guide_layer_name = self.font_layers[last_layer] 110 | 111 | def _draw_space_callback(self, sender) -> None: 112 | # RF-specific: Draw in space center 113 | value = sender.get() 114 | if value: 115 | addObserver(self, "_previewFull", "spaceCenterDraw") 116 | else: 117 | removeObserver(self, "spaceCenterDraw") 118 | 119 | def get_guide_representation(self, glyph: RGlyph, font, angle: float): 120 | return glyph.getLayer(self.guide_layer_name).getRepresentation( 121 | rf_guide_key, font=font, angle=angle 122 | ) 123 | 124 | def _trace_callback(self, sender) -> None: 125 | if self.guide_layer_name is None: 126 | self._update_layers() 127 | return 128 | guide_glyph = self.glyph.getLayer(self.guide_layer_name) 129 | glyph = self.get_guide_representation( 130 | glyph=guide_glyph, font=guide_glyph.font, angle=self.angle 131 | ) 132 | p = self.nib_pen( 133 | self.font, 134 | self.angle, 135 | self.width, 136 | self.height, 137 | self._draw_nib_faces, 138 | nib_superness=self.superness, 139 | trace=True, 140 | ) 141 | glyph.draw(p) 142 | p.trace_path(self.glyph) 143 | 144 | def _draw_preview(self, notification, preview=False) -> None: 145 | self.draw_preview_glyph(preview=preview) 146 | 147 | def _preview(self, notification) -> None: 148 | self.draw_preview_glyph(False) 149 | 150 | def _previewFull(self, notification) -> None: 151 | if self._draw_in_preview_mode: 152 | self.draw_preview_glyph(True) 153 | 154 | def _glyph_changed(self, notification) -> None: 155 | if self.glyph is not None: 156 | self.save_settings() 157 | self.glyph = notification["glyph"] 158 | self.font = CurrentFont() 159 | self.font_layers = self.getLayerList() 160 | if self.glyph is not None: 161 | self.load_settings() 162 | 163 | def _setup_draw(self, preview=False) -> None: 164 | if preview: 165 | fill(0) 166 | stroke(0) 167 | else: 168 | fill(0.6, 0.7, 0.9, 0.5) 169 | stroke(0.6, 0.7, 0.9) 170 | # strokeWidth(self.height) 171 | # strokeWidth(1) 172 | strokeWidth(0) 173 | stroke(None) 174 | lineJoin(self.line_join) 175 | 176 | def draw_preview_glyph(self, preview=False) -> None: 177 | if self.guide_layer_name is None: 178 | self._update_layers() 179 | return 180 | guide_glyph = self.glyph.getLayer(self.guide_layer_name) 181 | glyph = self.get_guide_representation( 182 | glyph=guide_glyph, font=guide_glyph.font, angle=self.angle 183 | ) 184 | save() 185 | self._setup_draw(preview=preview) 186 | # TODO: Reuse pen object. 187 | # Needs modifications to the pens before possible. 188 | p = self.nib_pen( 189 | self.font, 190 | self.angle, 191 | self.width, 192 | self.height, 193 | self._draw_nib_faces, 194 | nib_superness=self.superness, 195 | ) 196 | glyph.draw(p) 197 | restore() 198 | 199 | def _font_resign(self, notification=None) -> None: 200 | self.save_settings() 201 | 202 | def _font_changed(self, notification) -> None: 203 | self.font = notification["font"] 204 | self._update_layers() 205 | -------------------------------------------------------------------------------- /scripts/Glyphs - Add nib guide layer from background.py: -------------------------------------------------------------------------------- 1 | #MenuTitle: Add Nib Guide Layer from Current Background 2 | import copy 3 | 4 | mid = Layer.master.id 5 | n = GSLayer() 6 | n.name = "nib" 7 | n.width = Layer.width 8 | n.associatedMasterId = mid 9 | n.shapes = copy.copy(Layer.background.shapes) 10 | Layer.parent.layers.append(n) 11 | 12 | Layer.background.clear() 13 | -------------------------------------------------------------------------------- /scripts/NibLibG.py: -------------------------------------------------------------------------------- 1 | from nibLib.ui.glyphs import JKNib 2 | JKNib() 3 | -------------------------------------------------------------------------------- /scripts/NibLibRF.py: -------------------------------------------------------------------------------- 1 | from nibLib.ui.robofont import JKNibRoboFont 2 | 3 | OpenWindow(JKNibRoboFont) 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = nibLib 3 | version = 0.1.1 4 | description = Nib simulation for font editors 5 | long_description = file: README.md 6 | long_description_content_type = text/markdown; charset=UTF-8 7 | url = https://www.kutilek.de/ 8 | author = Jens Kutilek 9 | classifiers = 10 | Programming Language :: Python :: 3 11 | Operating System :: Mac OS 12 | Environment :: Plugins 13 | 14 | [options] 15 | zip_safe = False 16 | package_dir= 17 | =lib 18 | packages=find: 19 | platforms = any 20 | install_requires = 21 | beziers >= 0.5.0 22 | defconappkit @ git+https://github.com/robotools/defconAppKit.git 23 | fontpens >= 0.2.4 24 | fontTools >= 4.39.2 25 | python_requires = >=3.8 26 | 27 | [options.packages.find] 28 | where=lib 29 | 30 | [bdist_wheel] 31 | universal = 1 32 | 33 | [flake8] 34 | select = B, C, E, F, W, T4, B9 35 | # last two ignores: * imports, unused imports 36 | # ignore = E203, E266, E501, W503, E741, F403, F401 37 | max-line-length = 88 38 | max-complexity = 19 39 | exclude = .git, __pycache__, build, dist, .eggs, .tox 40 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() 4 | -------------------------------------------------------------------------------- /tests/geometry_test.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from nibLib.geometry import getPathFromPoints 4 | 5 | 6 | class GeometryTest(TestCase): 7 | def test_getPathFromPoints(self): 8 | pts = [(0, 0), (100, 100)] 9 | assert getPathFromPoints(pts) == [ 10 | ((0.0, 0.0),), 11 | ( 12 | (33.333333333333336, 33.333333333333336), 13 | (66.66666666666667, 66.66666666666667), 14 | (100.0, 100.0), 15 | ), 16 | ] 17 | --------------------------------------------------------------------------------