├── .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 |
--------------------------------------------------------------------------------