Skip to content

Instantly share code, notes, and snippets.

@adbac
Last active April 8, 2026 19:31
Show Gist options
  • Select an option

  • Save adbac/2d2150d5e90a1008c1b8ecb28febfc60 to your computer and use it in GitHub Desktop.

Select an option

Save adbac/2d2150d5e90a1008c1b8ecb28febfc60 to your computer and use it in GitHub Desktop.
A new version of ProductionType's magneticMetrics script, updated for RoboFont 4 (using the Subscriber and merz modules)
"""
v.2.3
Observer to make sidebearings magnetic to outline modifications.
Its aim to be used as a startup script so it can be activated and deactivated whenever you need.
Version history:
ProductionType:
- v1.002: first private release
- v1.003: fix magnet multiple drawing
Adrien Bachelart:
- v2.000: Updated for RF4: use Subscriber instead of observers and merz instead of mojo.drawingTools
- v2.001: maintenance and performance
- Define the magnet factory outside of the Subscriber
- Stop updating the internal data when the tool isn't active
- Check that font isn't `None` when using it
- v2.1: fixed long-standing performance issue with infinite subscriber callback loop that was slowing down the whole glyph editor.
- v2.2:
- Add support for italic magnets positioning (with slant offset)
- Centralize lib key logic
- v2.3:
- Used angled glyph margins for when the italic angle is set
"""
import math
from typing import Literal
import merz
from fontTools.misc import transform
from merz.tools.drawingTools import NSImageDrawingTools
from mojo.events import extractNSEvent
from mojo.subscriber import (
Subscriber,
disableSubscriberEvents,
registerGlyphEditorSubscriber,
)
PRESSED_KEY = "M"
# Register merz magnet factory
def magnetSymbolFactory(scale=1):
bot = NSImageDrawingTools((50 * scale, 50 * scale))
bot.scale(scale)
bot.fill(0, 0, 0, 0.75)
pen = bot.BezierPath()
pen.moveTo((25, 15))
pen.curveTo((20.0, 15.0), (17, 16), (17, 20))
pen.lineTo((17, 45))
pen.curveTo((17.0, 48.0), (15, 50), (9, 50))
pen.curveTo((2, 50), (0, 48.0), (0, 45))
pen.lineTo((0, 22))
pen.curveTo((0, 6), (9, 0), (25, 0))
pen.curveTo((41.0, 0.0), (50, 6), (50, 22))
pen.lineTo((50, 45))
pen.curveTo((50, 48), (48, 50), (41, 50))
pen.curveTo((35, 50), (33, 48), (33, 45))
pen.lineTo((33, 20))
pen.curveTo((33, 16), (30, 15), (25, 15))
pen.closePath()
bot.drawPath(pen)
bot.fill(1)
pen = bot.BezierPath()
pen.moveTo((25, 3))
pen.curveTo((11.0, 3), (3, 8), (3, 22))
pen.lineTo((3, 34))
pen.lineTo((14, 34))
pen.lineTo((14, 20))
pen.curveTo((14.0, 14.0), (18, 12), (25, 12))
pen.curveTo((32.0, 12.0), (36.0, 14.0), (36, 20))
pen.lineTo((36, 34))
pen.lineTo((47, 34))
pen.lineTo((47, 22))
pen.curveTo((47, 8), (39, 3), (25, 3))
pen.closePath()
bot.drawPath(pen)
image = bot.getImage()
return image
LIB_KEY = lambda s=None: (
f"com.adrienbc.MagneticMargins{('.' + s) if s is not None else ''}"
)
MAGNET_SYMBOL_KEY = LIB_KEY("magnet")
merz.SymbolImageVendor.registerImageFactory(MAGNET_SYMBOL_KEY, magnetSymbolFactory)
# Register Subscriber
class MagneticMetricsSubscriber(Subscriber):
def build(self):
self.glyphEditor = self.getGlyphEditor()
self.glyph = self.glyphEditor.getGlyph().asFontParts()
self.font = self.glyph.font
self.leftMargin, self.rightMargin = 0, 0
self.updateMarginsDataFromGlyph()
self.status = 0
self.magnetScale = 0.3
self.magnetsLayer = self.glyphEditor.extensionContainer(LIB_KEY())
self.magnetsLayer.setVisible(False)
self.leftMagnet = self.magnetsLayer.appendSymbolSublayer(
imageSettings=dict(name=MAGNET_SYMBOL_KEY, scale=self.magnetScale)
)
self.rightMagnet = self.magnetsLayer.appendSymbolSublayer(
imageSettings=dict(name=MAGNET_SYMBOL_KEY, scale=self.magnetScale)
)
self.updateMagnetsPosition()
def glyphEditorDidKeyDown(self, info):
event = extractNSEvent(info["NSEvent"])
keyDown = event["keyDown"]
if keyDown == PRESSED_KEY:
if self.status == 0:
self.status = 1
self.updateGlyphReference()
self.updateMarginsDataFromGlyph()
self.updateMagnetsPosition()
self.magnetsLayer.setVisible(True)
else:
self.status = 0
self.magnetsLayer.setVisible(False)
def glyphEditorDidSetGlyph(self, info):
if self.status == 1:
self.updateGlyphReference()
self.updateMarginsDataFromGlyph()
self.updateMagnetsPosition()
glyphEditorGlyphDidChangeContoursDelay = 0.01
def glyphEditorGlyphDidChangeContours(self, info):
if self.status == 1:
self.updateGlyphFromMarginsData()
self.updateMagnetsPosition()
def updateGlyphReference(self):
self.glyph = self.glyphEditor.getGlyph().asFontParts()
self.font = self.glyph.font
def updateMarginsDataFromGlyph(self):
self.leftMargin = self.glyph.angledLeftMargin
self.rightMargin = self.glyph.angledRightMargin
def updateGlyphFromMarginsData(self):
with disableSubscriberEvents():
self.glyph.angledLeftMargin = self.leftMargin
self.glyph.angledRightMargin = self.rightMargin
def getMagnetPosition(self, side: Literal["left", "right"]):
# Y position
if self.font is not None and self.font.info.capHeight is not None:
yPos = self.font.info.capHeight / 2
else:
yPos = 0
# Simplest case = no italic
if self.font is not None and self.font.info.italicAngle in {None, 0}:
if side == "left":
return (0, yPos)
else:
return (self.glyph.width, yPos)
offset = self.font.lib.get("com.typemytype.robofont.italicSlantOffset", 0)
# Calculate slanted position
x = y = math.radians(-(self.font.info.italicAngle or 0))
matrix = transform.Identity.skew(x=x, y=y)
t = transform.Transform()
oX, oY = (0, 0)
t = t.translate(oX, oY)
t = t.transform(matrix)
t = t.translate(-oX, -oY)
trans = tuple(t)
ot = transform.Transform(*trans)
n = ot.transformPoint(
((0 if side == "left" else self.glyph.width) + offset, yPos)
)
return (n[0], yPos)
def updateMagnetsPosition(self):
self.leftMagnet.setPosition(self.getMagnetPosition("left"))
self.rightMagnet.setPosition(self.getMagnetPosition("right"))
registerGlyphEditorSubscriber(MagneticMetricsSubscriber)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment