Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement wrapWord parameter (#268) #345

Draft
wants to merge 8 commits into
base: dev
Choose a base branch
from

Conversation

marcel-danilewicz-consult-red
Copy link
Contributor

WiP, todo: canvas renderer

@@ -292,6 +293,24 @@ export function layoutText(
maxX = Math.max(maxX, quadX + glyph.width);
curX += glyph.xAdvance;
}
if (
wrapWord &&
charEndX + glyph.width >= lineVertexW &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that correct? Wrap-word I think is meant to only wrap a word when this word alone is larger than the wrapping width.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought it should be wrapped if the width is exceeded. So if a word doesn't fit it should be moved to the next line and then if still doesn't fit it should be wrapped?

Copy link
Contributor

@elsassph elsassph Jul 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be silly to break every word. The point is that sometimes (as reported in #268) you may have a single word larger than the target width, in which case you may prefer to break it.

In L2, that's what wordBreak option does. I recommend checking the implementation (which is TBH rather inefficient). The algorithm actually breaks words "before" doing the layout, so for instance a very long word would be always cut to not exceed the full width, but it won't try to fit a few characters on the previous line. It also supports words being multiple times longer than the available width - imagine for instance using this feature to render text vertically, one letter per line.
See: https://github.com/rdkcentral/Lightning/blob/dev/src/textures/TextTextureRendererAdvanced.mjs#L175

I suggest again to make sure that the requirements are non-ambiguous - ask @wouterlucas for confirmation. Make a diagram/drawing to explain what you are going to do.

In any case it is very wise to have proposed an early draft.

@@ -720,6 +723,18 @@ export class LightningTextTextureRenderer {
realNewlines.push(allLines.length);
}
}
if (wordBreak) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is incorrect in some cases, and inefficient in general. You should instead break words as part of the wrapText function.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But this is inside of wrapText function :)
Could you give me an example of NG case for this code?

Copy link
Contributor

@elsassph elsassph Jul 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, but it is misplaced I think: it re-measures every word which we already measure in the above loop - could the break happen as part of the above wrapping logic? Also isn't that a different breaking logic as SDF? It seems SDF tries to cut the word to fit the line, isn't it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It remeasures whole lines. I'd rather separate normal and break cases, but you are right that the code can be optimized. Algorithms are not the same due to different surrounding code.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No allLines has all the individual words, and realNewlines tells the index of each new line's first word.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the algorithm in last commit. It should be better now, but words still need to be measured in order to find the breaking point. Probably it can be optimized further , but I don't see a way to entirely avoid measurement.

if (remainder.length > 0) {
words.splice(j + 1, 0, remainder);
}
}
if (j === 0 || wordWidthWithSpace > spaceLeft) {
// Skip printing the newline if it's the first word of the line that is.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is where you cut the word to fix within the spaceLeft, and then what remains is cut if remainder still goes over wordWrapWidth.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but what remains is cut in next iteration of for (let j = 0; j < words.length; j++) loop as it is inserted to words at j+1 index and thus treated as another word. I don't see any issue here, but maybe I'm missing something.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok it can work here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW I know that this project tends to use mostly visual regression tests, but this is a case where some unit tests would be greatly beneficial @wouterlucas

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW I know that this project tends to use mostly visual regression tests, but this is a case where some unit tests would be greatly beneficial @wouterlucas

theres a vitest infrastructure as well, just add *.test.ts and it should be picked up

@@ -696,6 +699,16 @@ export class LightningTextTextureRenderer {
const wordWidth = this.measureText(words[j]!, letterSpacing);
const wordWidthWithSpace =
wordWidth + this.measureText(' ', letterSpacing);
if (wordWidthWithSpace > wordWrapWidth && wordBreak) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put conditions in narrowing order:

if (wordBreak && the rest) {

@@ -696,6 +699,16 @@ export class LightningTextTextureRenderer {
const wordWidth = this.measureText(words[j]!, letterSpacing);
const wordWidthWithSpace =
wordWidth + this.measureText(' ', letterSpacing);
if (wordWidthWithSpace > wordWrapWidth && wordBreak) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the algorithm here is different from SDF.

Isn't the goal agreed to cut the word so the beginning of the word fits at the end of the line? e.g. keep what fits within spaceLeft and move the remainder to the next line (where indeed it could still be > wordWrapWidth)

@@ -691,11 +694,21 @@ export class LightningTextTextureRenderer {
const resultLines = [];
let result = '';
let spaceLeft = wordWrapWidth - indent;
const words = lines[i]!.split(' ');
const words = wordBreak ? [lines[i]!] : lines[i]!.split(' ');
Copy link
Contributor

@elsassph elsassph Aug 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok that's kind of right and wrong - very interesting.

The good:

  • If we're breaking text we don't really care about spaces anymore; we can "just" find how many characters fit, and fill the lines,

The bad:

  • This is very inefficient,
  • It awkwardly integrates with the existing loop.

Suggestion:

if (wordBreak) {
  // approach wrapping the lines
} else {
  // original code splitting and wrapping the words
}

Performant approach:

  • This class has a wrapWord(word, wordWrapWidth, suffix) method which has an optimized algorithm to cut a string at a max width. It will return the part that fits, from which you can figure the remainder and repeat until everything has fit.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I tried the wrapWord, but the line break points are a bit different between sdf and canvas.
image
image
I'll push the commit if this is ok.

Copy link
Contributor

@elsassph elsassph Aug 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm ok curious. Why is sit on the next line for SDF? And also why did it keep the space character when wrapping ex magna? Generally you want the wrapping logic to avoid wrapping one space to the next line.
Aside from the the canvas wrapping looks alright, if the definition is to fit as many characters per line as possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got stuck a little bit on this issue. To prevent sit from being carried to the next line additional wrapWord=='normal' condition is needed in line 213:

 // Word wrap check
        if (
          // We are containing the text
          contain !== 'none' &&
          wrapWord == 'normal' &&

Then to break at the correct point
charEndX + glyph.width >= lineVertexW (line 299)
should be changed to
charEndX + glyph.width + glyph.xOffset + 4 >= lineVertexW

The value of 4 was concluded experimentally and I'm not even sure why it is needed there. Here you can see the how breaking looks without it:
image
and with:
image

Finally - I have no idea about additional space before "ex".

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

About sit, I guess we should either wrap or break works, no?

Adding +4 isn't worth it - it's just the subtle differences in how text layout happens.

For the space before ex it's probably because when you cut the line, it cuts just at the space characters, so line starts with a space. We shouldn't carry over a space character to next line when we break, so probably we can remove a starting space character at the beginning of a wrapped line.

@pecoram
Copy link
Contributor

pecoram commented Nov 5, 2024

What is missing to complete this PR? @marcel-danilewicz-consult-red

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants