Methods and/or Properties for Getting Dimensions of Lines of Text within Text Node?

Hi,

I’m trying to figure out a way to use the Plugin API to get various internal dimensions of a text node—specifically, the width of each line of text, and the (x, y) position of each line of text, or even better yet, the (x, y) position of any arbitrary character within a text node.

Is this possible?

See the attached image for clarification as to what I am trying to determine programatically.

The plugin API doesn’t explicitly support these text metrics, but you might be able to derive them using the fillGeometry property of the text node.

1 Like

Great idea.

This is precisely the sort of guidance I was looking for, thanks.

So, what I ended up doing was a little bit different, but your suggestion provided the seed for the idea.

Since the actual fillGeometry of a TextNode is a very messy SVG path consisting of many different characters, each constructed with potentially multiple paths, on multiple lines, it is not trivial to calculate the dimensions from this for each line separately. You could do some kind of statistical analysis on each character to determine which belongs to which line, or something, but this is not necessary.

Instead, what I am doing is making a temporary clone of the TextNode, setting its textDecoration property to “UNDERLINE”, if not already set, and then once textDecoration is set to “UNDERLINE” on this temporary cloned TextNode, when I retrieve its fillGeometry property, the first value in the returned array contains the SVG path data of the underlines themselves, separately from the text geometry (which is in the zeroth value of the returned array). The SVG path of the underline geometry, now separated from the complicated character geometry, is trivial to parse for dimensions—splitting the SVG string on the “Z” character that terminates all separate lines in an SVG path, I can loop through the result and very easily get the minimum and maximum values for the line to calculate its width and also grab its y-position. Then it’s just a simple matter of wrapping the original TextNode in a new frame, appending new LineNodes to it for each line required, each positioned and scaled according to the data retrieved earlier, deleting the temporary node, and then styling the new lines in whichever way I need. Easy-peasy custom underlines in Figma.

Now to make the plugin…my goal is to build a plugin that will accurately reproduce all CSS text-decoration functionality associated with underlines, etc (colors, offsets, thickness, styles, etc).

Thanks again!

Hi @James_Yang ,

So I came up against a strange issue.

Whenever I get the fillGeometry of a TextNode with underlines that is already extant in the Figma file (and already had underlines), I get back an array containing two SVG paths, the zeroth item of which is—as expected—the geometry of the text itself, and the first item contains the geometry of the underlines.

However, when I programmatically add underlines to previously non-underlined text using setRangeTextDecoration() and then try to get the fillGeometry of that TextNode, I only get back an array containing a single SVG path, and that geometry does not contain the underline geometry, only the geometry of the text itself. In fact, for a TextNode such as this, to which I programmatically add underlines using setRangeTextDecoration(), the geometry of the underlines is nowhere to be found in its properties.

At first I thought perhaps there was some race condition going on, where the underlines hadn’t been added by the time I tried to console.log() its fillGeometry, but even when I manually go into the console and examine a programmatically-altered TextNode such as this, I still only get back the fillGeometry for the text itself only—not the underline geometry.

Any ideas on what is going on and how I can retrieve the fillGeometry of the underlines in these cases (where the original text has text-decoration “NONE”, and I programmatically alter it to “UNDERLINE” with setRangeTextDecoration).

The problem is probably in your code. Try listening to the documentchange or nodechange event and checking the node property.

The code is extraordinarily simple—I reduced it to a minimum for debugging. It consists entirely of the following:

let node = figma.currentPage.selection[0] as TextNode;    
    
figma.loadFontAsync(node.fontName as FontName).then(() => {
  node.setRangeTextDecoration(0, node.characters.length, 'UNDERLINE');
  console.log(node.fillGeometry);
  figma.closePlugin();
});

Note that the issue only occurs when I try to get the fillGeometry of a TextNode whose underline has been added programmatically added with setRangeTextDecoration() in the context of the plugin. It works perfectly when doing it manually in the console. However, the code used in either context is exactly the same, so am at a loss for what is going on here.

Here is a Loom screen recording showing the unexpected behavior.:

Summary of Behavior in Testing

Note: the “success” criterion below means that fillGeometry returns both the text and underline SVG path data, as two separate items in an array

Running in the console manually

TextNode Type Result
underline already present success
underline added w/ setRangeTextDecoration() success

Running through the plugin

TextNode Type Result
underline already present success
underline added w/ setRangeTextDecoration() failure

Edit:

I decided to throw a sleep() function in that short bit of code after the setRangeTextDecoration() call to see what would happen.

Indeed, that was the issue. Apparently the fillGeometry created by setRangeTextDecoration() does not become available in a synchronous manner and thus cannot be utilized in synchronous code directly after. There is a race condition, as I thought might be the case earlier.

I am not quite sure what can be done about it beyond the horrible option of inserting deliberate sleep() calls in the code. But if a user uses the plugin on a huge amount of text which takes a long time to process, this problem might crop up again. Or otherwise keep polling the TextNode to test whether it has updated or not. These are all awful solutions.

If only Figma made the setRangeTextDecoration() method asynchronous and return a promise so that we could definitively know when such methods have resolved.

1 Like

Very interesting discoveries, I hope Figma team would address those!

I think in this case it would be better to drop the fillGeometry idea and use something like I suggested (or something similar). Many plugins were created (e.g. “Substrate for text”) that use a mechanic like this long before fillGeometry was even added to API and I haven’t noticed any performance downsides.

Hello @Gleb, thank you for your reply.

I am familiar with that plugin, but AFAIK it does not handle multiple lines (or I could not get it to do so), converting all TextNodes to “Auto Width” mode, and it creates the dimensions of the “substrate” (or at least just its height) based on the dimensions of a single letter in a given font, which the user has to specify in the plugin settings (default = “o”). Of course, it is not open source, as far as I can tell, so I can’t see exactly what it is doing, but this is the sense one gets.

As for what you mentioned about your suggestion, I did not see any other post from you in this topic—could you please clarify what your suggestion is again?

Thank you for your help.

PS in experimentation with awaiting a sleep function before accessing the fillGeometry property of a TextNode after updating its textDecoration with setRangeTextDecoration(), I found that even a 1ms long delay with 100,000 words of text works and is very snappy in terms of performance, so it actually seems to be working out this way.

Oh, sorry, I got confused.

  1. I forgot which plugin is using the technique I thought about. I shared this technique years ago on the forum or on discord. Maybe it was Nisa Text?

  2. I read this thread before so I thought I provided my solution but I guess I saw it’s solved already so I didn’t end up posting anything.

My solution is to duplicate text, delete all characters and then add characters or words one by one until you get a line wrap which can be detected by node height.

Use an event listener instead of a delay function:

The event will fire when the node properties change.

1 Like

@tank666 I should have listened to you earlier. on('nodechange', callback) was indeed the answer I needed, not so much from a debugging perspective, but as you correctly point out in your latest message, as a trigger for the rest of the action.

Works great!

Only problem is that I now have to clean up the event listener with off(), but you have to pass an exact reference to the callback function to off(), and the way my code is written, this will be complicated to achieve. Oh well, I will figure it out.

Thanks again.