I am trying to update my BubbleKern reporter plugin. If you don’t know, it looks for the glyph’s extra layer named ‘bubble’, and draws its contents behind the regular outlines. And due to the intention of the plugin, I want to inherit the components’ bubble layers to be drawn too. It does that, but only until the first nesting level.
I thought of exhaustively checking all the components that are being nested, but I thought it may be easier I could retrieve whatever bezierPath that was drawn (with fill() ) in a certain layer. Does anyone know how or other ideas?
def collect_component_layers(layer, filter_func):
for component in (layer.components or []):
sub_glyph = component.component
for sub_layer in sub_glyph.layers:
if filter_func(sub_layer):
yield sub_layer
yield from collect_component_layers(sub_layer, filter_func) # Python 3.3 or later
for selected_layer in Glyphs.font.selectedLayers:
for component_layer in collect_component_layers(selected_layer, lambda l: l.layerId == selected_layer.layerId):
print(component_layer)
Like this? Recursion can be written much simpler with the generator syntax. For perfection you might want to remove overlaps on top of this. I don’t know any other way to enumerate such ‘dependent’ layers…
from AppKit import NSAffineTransform
def collect_component_bezier_paths(layer, filter_func):
def _(layer, filter_func, transform=None):
def get_transform_as_object(component):
return component.pyobjc_instanceMethods.transform()
def concat_transforms(t1, t2):
(t := t1.copy()).appendTransform_(t2); return t
transform = transform or NSAffineTransform.transform()
for component in (layer.components or []):
sub_transform = concat_transforms(transform, get_transform_as_object(component))
sub_glyph = component.component
for sub_layer in sub_glyph.layers:
if filter_func(sub_layer):
yield sub_layer, sub_transform
yield from _(sub_layer, filter_func, sub_transform)
for component_layer, component_transform in _(layer, filter_func):
if p := component_layer.bezierPath:
if not p.isEmpty():
(p := p.copy()).transformUsingAffineTransform_(component_transform)
yield p
for selected_layer in Glyphs.font.selectedLayers:
for bezier_path in collect_component_bezier_paths(selected_layer, lambda l: l.layerId == selected_layer.layerId):
print(bezier_path)
As a note to self, to obtain the ready-to-draw NSBezierPath for each component, a snippet like above would work. It depends on the fill rule, but as for drawing inside a reporter plugin, I guess it’s safer to draw them one by one to the graphic context, rather than making a single NSBezierPath that wraps them all.
By looking at it for a bit, I don’t understand half of the code. As fare as I understand it would do indeed what layer.completeBezierPath would do. But I think the idea was to get the content of the “bubble” layer, not the default. So you need to traverse the components until you find a “bubble” layer. Then go back and apply all transformations from each component.
This is my take:
from AppKit import NSBezierPath
def bubbleLayerForLayer(layer):
for currLayer in layer.parent.layers:
if currLayer.name == "bubble" and layer.associatedMasterId == currLayer.associatedMasterId:
return currLayer
return None
def bubblePathFromComponents(layer):
bubbleLayer = bubbleLayerForLayer(layer)
if bubbleLayer is not None:
return bubbleLayer.completeBezierPath
fullBubblePath = NSBezierPath.new()
for component in layer.components:
compGlyph = component.component
compLayer = compGlyph.layers[layer.associatedMasterId]
bubblePath = bubblePathFromComponents(compLayer)
if bubblePath is not None:
bubblePath.transformUsingAffineTransform_(component.pyobjc_instanceMethods.transform())
fullBubblePath.appendBezierPath_(bubblePath)
if fullBubblePath.elementCount() < 2:
fullBubblePath = None
return fullBubblePath
print(bubblePathFromComponents(Layer))
To be honest, it was one of the earliest projects and I’ve most likely done meaningless stuff. But yeah, I want to traverse all nested components and retrieve their bubbles with transform. With the help of @takaakifuji I could get the traversing part right, but now I have two questions:
How do I merge multiple bezierPaths in one go? I think it’s fast to draw bubbles that way.
After a few crashes, Glyphs no longer loaded my plugin at the start even when it was back to the old state (the plugin name appears in the initial window that lists faulty plugins). How do I get that back?
from AppKit import NSBezierPath
def make_compound_bezier_path(bezier_paths):
p = NSBezierPath.bezierPath()
for bezier_path in bezier_paths:
p.appendBezierPath_(bezier_path)
return p
As @FlorianPircher describes, pass a list/iterable of NSBezierPath to this function (sorry for the disturbing snake_case convention I follow) and you’ll get a single compound NSBezierPath. Georg’s example above uses this technique. Winding rule should apply for overlaps, but they won’t be a problem for 99% cases.
Georg’s example should work perfectly! It’s just a matter of how we compose things I guess? Well, fiddling around NSBezierPath may require an extra layer of knowledge and it’s not that crystal clear to understand. At least for me, that’s one of the area I puzzle to describe to my colleagues who just started to learn how to write things for Glyphs in pure Python…sorry for going off-topic!