Retrieve whatever that was drawn in reporter plugin?

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…

1 Like

You can use layer.completeBezierPath in Python.

2 Likes

I don’t think that method is useful here. I think you need to write a method yourself.

1 Like
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))
1 Like

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?
1 Like

Use the append method to combine paths:

Glyphs will not load plugins if they are crashing. For plugin development, where plugin crashes are expected, enable debug mode:

1 Like
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.

I don’t see a point to collect the paths in a list in the first place. Why not append it to a path directly?

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!

NSBezierPath is actually really simple. It is “just” a collection of points with some nice API around it. This might help: Paths