-
-
Save connordavenport/ba7c5716c6210f3c1fe7dfd1a723ba6c to your computer and use it in GitHub Desktop.
the rough start of a rf4 anchor tool
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from vanilla import * | |
import merz | |
import mojo.subscriber as subs | |
from mojo.subscriber import Subscriber, registerGlyphEditorSubscriber | |
from mojo.subscriber import registerSubscriberEvent, getRegisteredSubscriberEvents | |
from mojo.extensions import registerExtensionDefaults, setExtensionDefault, getExtensionDefault, removeExtensionDefault | |
from mojo.drawingTools import * | |
from mojo.UI import SetCurrentGlyphByName | |
from math import radians, tan, pi | |
from AppKit import NSColor | |
from fontTools.pens.basePen import BasePen | |
fillColor = (0.6326, 0, 0.9689, 0.3771) | |
''' | |
recent updates: | |
now a dropdown menu in the glyph edit window | |
stores checked components as a preference | |
expand/collapse state persists when switching glyphs | |
close button turns off the view (still need to figure out how to remove the tool) | |
draws glyph guidelines (left, middle, right) | |
to do:B | |
figure out how to turn the damn thing off !!! | |
draw accents that have components (apparently this requires more work than bot.drawGlyph) | |
draw in preview mode | |
make anchor list scrollable for small windows | |
ui to toggle guidelines | |
it's annoying that the focus stays on the controller window after checking a box, what's a better solution to keep the focus on the glyph edit window? p.s. floating windows are bad | |
when anchor selected: | |
only show components for the selected anchor | |
show glyph bounds lines | |
show accent bounds lines | |
left center right top middle bottom | |
for accent and glyph | |
italic-angle adjusted | |
if a derived glyph (e.g.: small-cap) | |
show color status | |
correct, within-fuzz, wrong-x, wrong-y, missing, extra | |
show suggested location | |
show button to fix location | |
x, y, both | |
when anchor not selected: | |
if a derived glyph | |
show color status | |
correct, within-fuzz, wrong-x, wrong-y, missing, extra | |
''' | |
class DecompPen(BasePen): | |
def __init__(self, glyphSet, outPen): | |
super(DecompPen, self).__init__(glyphSet) | |
self._moveTo = outPen.moveTo | |
self._lineTo = outPen.lineTo | |
self._curveToOne = outPen.curveTo | |
self._closePath = outPen.closePath | |
self._endPath = outPen.endPath | |
class anchorHelper(Subscriber): | |
debug = False | |
def build(self): | |
# initial data | |
self.toggleOnOff = True | |
self.getAnchorData() | |
self.added = [] | |
self.anchorOn = [] | |
self.showUI = True | |
self.cs = None | |
self.name = None | |
# testing a stupid idea | |
# self.merzView = merz.MerzView( | |
# (0, 0, 0, 0), | |
# delegate=self, | |
# ) | |
# self.merzView.destroy = Button((3, -26, -3, -3), 'Destroy', callback=self.destroyThis) | |
# self.merzContainer = self.merzView.getMerzContainer() | |
# self.getGlyphEditor().addGlyphEditorSubview(self.merzView) | |
# preferences | |
self.prefKey = 'com.okaytype.anchorHelper' | |
initialDefaults = {self.prefKey+'anchorOn': []} | |
registerExtensionDefaults(initialDefaults) | |
# glyph view layer | |
self.glyphEditor = self.getGlyphEditor() | |
self.anchorPreview = self.glyphEditor.extensionContainer( | |
identifier='com.okay.anchorHelper.background', | |
location='background', | |
clear=True | |
) | |
# ui | |
self.minSize = (-200, 0, 200, 24) | |
self.anchorController = Group(self.minSize) | |
self.anchorController.view = Button((3, 0, -30, 22), 'Anchor Helper', sizeStyle='small', callback=self.anchorExpand) | |
self.anchorController.close = Button((-30, 0, -3, 22), 'X', sizeStyle='small', callback=self.destroyThis) | |
self.glyphEditor.addGlyphEditorSubview(self.anchorController) | |
ns = self.anchorController.getNSView() | |
color = NSColor.colorWithCalibratedRed_green_blue_alpha_(.5, 1, .5, .7) | |
ns.setBackgroundColor_(color) | |
self.anchorOn = getExtensionDefault(self.prefKey+'anchorOn') | |
def destroyThis(self, sender): | |
if self.toggleOnOff == True: | |
self.toggleOnOff = False | |
else: | |
self.toggleOnOff = True | |
self.drawAnchors() | |
self.destroy() | |
def destroy(self): | |
setExtensionDefault(self.prefKey + '.anchorOn', self.anchorOn) | |
self.anchorPreview.clearSublayers() | |
self.getGlyphEditor().removeGlyphEditorSubview(self.anchorController) | |
# dumb idea | |
# self.merzContainer.clearSublayers() | |
# self.getGlyphEditor().removeGlyphEditorSubview(self.merzView) | |
# def terminate(self): | |
# print('terminate') | |
# # how tf do you manage windows and turn off a tool | |
# self.anchorControllerWindow.close() | |
# | |
# | |
# events | |
# | |
def glyphEditorDidSetGlyph(self, info): | |
self.glyph = info['glyph'] | |
if self.glyph is None: | |
return | |
self.componentsToDraw = self.getGlyphComponentData(self.glyph) | |
self.drawAnchorControls() | |
self.drawAnchors() | |
def glyphDidChange(self, info): | |
# def glyphEditorGlyphAnchorsDidChange(self, info): | |
self.glyph = info["glyph"] | |
if self.glyph is None: | |
return | |
self.drawAnchors() | |
# | |
# | |
# build data | |
# | |
def getAnchorData(self): | |
# family-level data | |
# anchor.name: (list of glyph.names) | |
self.anchorData = {} | |
f = CurrentFont() | |
if f is not None: | |
glyphOrder = f.glyphOrder | |
for g in glyphOrder: | |
glyph = f[g] | |
if len(glyph.anchors) > 0: | |
# if len(glyph.anchors) > 0 and len(glyph.components) == 0: # components are harder to draw | |
for anchor in glyph.anchors: | |
if anchor.name not in self.anchorData: | |
self.anchorData[anchor.name] = [] | |
self.anchorData[anchor.name].append(glyph.name) | |
def getGlyphComponentData(self, glyph): | |
self.componentsToDraw = {} | |
if len(glyph.anchors) > 0: | |
for anchor in glyph.anchors: | |
mateAnchor = self.mateAnchor(anchor.name) | |
if mateAnchor not in self.componentsToDraw: | |
self.componentsToDraw[mateAnchor] = [] | |
for component in self.anchorData[mateAnchor]: | |
self.componentsToDraw[mateAnchor].append(component) | |
return self.componentsToDraw | |
# | |
# | |
# helper functions | |
# | |
def mateAnchor(self, anchorName): | |
if '_' == anchorName[0]: | |
mateAnchor = anchorName.replace('_', '') | |
else: | |
mateAnchor = '_' + anchorName | |
return mateAnchor | |
def getAnchorByName(self, glyph, anchorName): | |
return next(( | |
anchor for anchor in glyph.anchors if | |
anchor.name == anchorName), None) | |
def italicShiftedX(self, x, y, italicAngle): | |
if italicAngle != 0: | |
return x + (tan(-italicAngle * pi / 180) * y) | |
else: | |
return x | |
# | |
# | |
# build UI | |
# | |
def drawAnchorControls(self): | |
unit = 22 | |
length = len(self.componentsToDraw) | |
for i in self.componentsToDraw.values(): | |
length += len(i)+1 | |
height = length * unit + 18 | |
if height < self.minSize[3]: | |
height = self.minSize[3] | |
# resize window | |
x, y, w, h = self.anchorController.getPosSize() | |
self.maxSize = (x, y, w, height) | |
if self.showUI == False: | |
self.anchorController.setPosSize(self.minSize, True) | |
else: | |
self.anchorController.setPosSize(self.maxSize, True,) | |
# remove old stuff | |
for added in self.added: | |
if hasattr(self.anchorController, added): | |
delattr(self.anchorController, added) | |
# build new stuff | |
count = 0 | |
y = 25 | |
self.added = [] | |
for anchor in self.componentsToDraw: | |
count += 1 | |
this = TextBox((24, y+5+unit*.5, -8, unit), anchor, sizeStyle='small') | |
name = 'Label'+anchor | |
if hasattr(self.anchorController, name): | |
delattr(self.anchorController, name) | |
self.added.append(name) | |
setattr(self.anchorController, name, this) | |
y += unit*1.5 | |
for component in self.componentsToDraw[anchor]: | |
count += 1 | |
check = False | |
if component in self.anchorOn: | |
check = True | |
this = okCheckBox( | |
(8, y, -8, unit), | |
component, | |
component=component, | |
value=check, | |
sizeStyle='small', | |
callback=self.checkAnchorData, | |
) | |
name = 'CheckBox'+anchor+component | |
if hasattr(self.anchorController, name): | |
delattr(self.anchorController, name) | |
setattr(self.anchorController, name, this) | |
self.added.append(name) | |
y += unit | |
def checkAnchorData(self, sender): | |
component = sender.component | |
if sender.get() == 1 and component not in self.anchorOn: | |
self.anchorOn.append(component) | |
if sender.get() == 0 and component in self.anchorOn: | |
self.anchorOn.remove(component) | |
self.drawAnchors() | |
def anchorExpand(self, sender): | |
if self.showUI == True: | |
self.showUI = False | |
self.anchorController.setPosSize(self.minSize, True) | |
else: | |
self.showUI = True | |
self.anchorController.setPosSize(self.maxSize, True) | |
# | |
# | |
# draw anchor preview | |
# | |
def drawAnchors(self): | |
if self.toggleOnOff == False: | |
self.anchorPreview.clearSublayers() | |
return | |
if self.glyph is None: | |
return | |
font = self.glyph.font | |
# prep drawing | |
self.anchorPreview.clearSublayers() | |
anchorLayer = self.anchorPreview.getSublayer('anchorLayer') | |
if anchorLayer is None: | |
anchorLayer = self.anchorPreview.appendPathSublayer( | |
name='anchorLayer' | |
) | |
# draw | |
with anchorLayer.drawingTools() as bot: | |
bot.stroke(None) | |
bot.fill(*fillColor,) | |
for anchor in self.componentsToDraw: | |
for component in self.componentsToDraw[anchor]: | |
c = font[component] | |
mateAnchor = self.mateAnchor(anchor) | |
cAnchor = self.getAnchorByName(c, anchor) | |
bAnchor = self.getAnchorByName(self.glyph, mateAnchor) | |
shift = (bAnchor.x-cAnchor.x, bAnchor.y-cAnchor.y) | |
name = 'CheckBox'+anchor+component | |
componentVisibility = False | |
if hasattr(self.anchorController, name): | |
componentVisibility = getattr(self.anchorController, name).get() | |
if componentVisibility == True: | |
component = font[component].copy() | |
self.name = component | |
self.cs = RGlyph() | |
pen = DecompPen(font, self.cs.getPen()) | |
component.draw(pen) | |
self.cs.moveBy(shift) | |
bot.drawGlyph(self.cs) | |
self.drawGuide(self.glyph, bot) | |
def glyphEditorDidMouseDown(self, info): | |
lle = info["lowLevelEvents"][0] | |
point = lle["point"] | |
cc = lle["clickCount"] | |
if cc == 3: | |
path = self.cs.naked().getRepresentation("defconAppKit.NSBezierPath") | |
if path.containsPoint_(point): | |
print(self.name) | |
SetCurrentGlyphByName(self.name.name) | |
def drawGuide(self, glyph, bot): | |
font = glyph.font | |
italicAngle = font.info.italicAngle or 0 | |
italicOffset = font.lib.get('com.typemytype.robofont.italicSlantOffset') or 0 | |
bot.fill(None) | |
bot.strokeWidth((1)) | |
bot.stroke(0, 1, 0, .8) | |
# boundsWidth = glyph.bounds[2] - glyph.bounds[0] | |
# rightX = glyph.width - glyph.angledRightMargin or 0 + font.lib.get('com.typemytype.robofont.italicSlantOffset') or 0 | |
# midX = leftX + (glyph.width - glyph.angledLeftMargin or 0 - glyph.angledRightMargin or 0) / 2.0 | |
Ytop = font.info.ascender + 500 | |
Ybot = font.info.descender - 500 | |
# left | |
xL = glyph.angledLeftMargin + italicOffset | |
Xtop = self.italicShiftedX(xL, Ytop, italicAngle) | |
Xbot = self.italicShiftedX(xL, Ybot, italicAngle) | |
bot.newPath() | |
bot.moveTo((Xbot, Ybot)) | |
bot.lineTo((Xtop, Ytop)) | |
bot.closePath() | |
bot.drawPath() | |
# right | |
xR = glyph.width - glyph.angledRightMargin + italicOffset | |
Xtop = self.italicShiftedX(xR, Ytop, italicAngle) | |
Xbot = self.italicShiftedX(xR, Ybot, italicAngle) | |
bot.newPath() | |
bot.moveTo((Xbot, Ybot)) | |
bot.lineTo((Xtop, Ytop)) | |
bot.closePath() | |
bot.drawPath() | |
# middle | |
xC = (xL + xR)/2 | |
Xtop = self.italicShiftedX(xC, Ytop, italicAngle) | |
Xbot = self.italicShiftedX(xC, Ybot, italicAngle) | |
bot.newPath() | |
bot.moveTo((Xbot, Ybot)) | |
bot.lineTo((Xtop, Ytop)) | |
bot.closePath() | |
bot.drawPath() | |
class okCheckBox(CheckBox): | |
def __init__(self, *args, **kwargs): | |
self.component = kwargs['component'] | |
del kwargs['component'] | |
super(okCheckBox, self).__init__(*args, **kwargs) | |
registerGlyphEditorSubscriber(anchorHelper) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment