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

Rectangles with zero sizes are drawn incorrectly #174

Open
christianbrugger opened this issue Aug 8, 2023 · 5 comments
Open

Rectangles with zero sizes are drawn incorrectly #174

christianbrugger opened this issue Aug 8, 2023 · 5 comments

Comments

@christianbrugger
Copy link

Currently when rects are stroked with one being exactly 0, Blend2d draws something weird.

Fiddle example

BLImage render(const BLContextCreateInfo& cci) {
  BLImage img(500, 140, BL_FORMAT_PRGB32);
  BLContext ctx(img, cci);

  ctx.setFillStyle(BLRgba32(0xFFFFFFFFu));
  ctx.fillRect(0, 0, 500, 140);

  ctx.setStrokeWidth(10);
  ctx.setStrokeStyle(BLRgba32(0xFF000000u));
  ctx.strokeRect( 50, 70, 1, 1);  // 1 x 1 px  --> draws a small 11 x 11 px rect perfect
  ctx.strokeRect(150, 70, 0, 0);  // 0 x 0 px  --> draws nothing fair
  ctx.strokeRect(250, 70, 1, 0);  // 1 x 0 px  --> draws a long 41 x 10 px line, this is wrong
  ctx.strokeRect(350, 70, 0, 1);  // 0 x 1 px  --> draws a long 10 x 41 px line, this is wrong

  return img;
}

image

As you can see, the things drawn with zero sizes are even bigger than the ones with size 1x1 px.

I guess it should draw the same as for the case 0x1px, namely no stroking at all.

At max 10 x 11 px, but nothing that is 41 px long.

@yzrmn
Copy link
Member

yzrmn commented Aug 8, 2023

Hi, this is expected behavior because the default stroke join is BL_STROKE_JOIN_MITER_CLIP.

BLImage render(const BLContextCreateInfo& cci) {
  BLImage img(500, 140, BL_FORMAT_PRGB32);
  BLContext ctx(img, cci);

  ctx.setFillStyle(BLRgba32(0xFFFFFFFFu));
  ctx.fillRect(0, 0, 500, 140);

  ctx.setStrokeWidth(10);
  ctx.setStrokeJoin(BL_STROKE_JOIN_MITER_BEVEL);
  ctx.setStrokeStyle(BLRgba32(0xFF000000u));
  ctx.strokeRect( 50, 70, 1, 1);  // 1 x 1 px  --> draws a small 11 x 11 px rect perfect
  ctx.strokeRect(150, 70, 0, 0);  // 0 x 0 px  --> draws nothing fair
  ctx.strokeRect(250, 70, 1, 0);  // 1 x 0 px  --> draws a long 41 x 10 px line, this is wrong
  ctx.strokeRect(350, 70, 0, 1);  // 0 x 1 px  --> draws a long 10 x 41 px line, this is wrong

  return img;
}

Did you expect the output of this code?

@christianbrugger
Copy link
Author

christianbrugger commented Aug 9, 2023

Hi, yes, this looks like what I had in mind. Also thanks for the reference to MITER, as it completely explains what is going on.

So here is what I assume is happening. With one side being 0px, the stroke is approximated with 2 points instead of 4, as two are the same. Both points now have an opening angle of 0° instead of 90° and a miter is added. As it is infinitely long it is clipped at the miter limit.

I did some research on the topic, and saw some discussions around SVG 2.0 and miter-clip. They agreed, that for an angle approaching 0° the meter should remain and be equal to the limit value. This agrees with the above behavior. Here is a good animation.

I tried to make an SVG to see what other implementations make of miter-clip in the case of rect. However, I couldn't find any software that supports it, neither Chrome, Firefox, Inkscape or Adobe Illustrator. So that didn't lead to anything useful.

Still I am not sure if the behavior is the correct one. The corner being treated as having 0° is not a good limit when one side going to zero, from a mathematical point of view. It should remain at having 90° and therefore not justify a meter to be added.
However, as we are now talking about some corner cases, I don't think it makes sense to adjust this. Maybe it is even the right behavior.

For now I am checking my width and height of my rectangles and not drawing them if one is zero as a workaround. I might consider changing the meter behavior, so I don't need to do this anymore. Thank you.

@kobalicek
Copy link
Member

This is a tricky border case.

I will try to explain what is happening, and why the current behavior is maybe the only one that makes sense.

When you want to stroke a rectangle, the stroker in general doesn't see a rectangle, only the path commands that represent the rectangle.

In a regular case, it would go something like:

  • moveTo(x, y);
  • lineTo(x + w, y);
  • lineTo(x + w, y + h);
  • lineTo(x, y + h);
  • close(); // closes to [x, y]

If one w/h is zero, it really appears as a command that has the exact coordinate of the previous command, which then basically changes the angle between the vertices as the vertex that is equal to the previous vertex (or very very close to it) is ignored by the stroker.

Then, if w == 0, the stroker sees the following:

  • moveTo(x, y);
  • lineTo(x, y + h);
  • close(); // closes to [x, y]

so it really becomes a line and joins apply to its start/end points basically.

What you see is actually miter-clip, as miter- clip (BL_STROKE_JOIN_MITER_CLIP) is the default join used by Blend2D. The regular SVG miter is BL_STROKE_JOIN_MITER_BEVEL - the reason for this name is very simple - if Miter is exceeded, it's beveled, similarly for having BL_STROKE_JOIN_MITER_ROUND, which is a Blend2D extension to standard join modes, which just does ROUND instead of BEVEL when exceeded.

@christianbrugger
Copy link
Author

christianbrugger commented Aug 9, 2023

Thank you. I agree, on a path level given the 4 points this is the only behavior that makes sense.

I looked a bit more into SVG, although I don't know if the goal here is to follow them even. In the SVG1.1 specification there is a section about the size properties of rect:

width = "length"
The width of the rectangle.
A negative value is an error (seeError processing). A value of zero disables rendering of the element.
Animatable: yes.

The new SVG2 specification is missing that sentence, although someone made an issue about adding it, without further comment.

Maybe we should not generate a path on the rect level if one side is zero.

@kobalicek
Copy link
Member

Yeah that's an interesting question.

At the moment, when you fill a zero width/height rectangle, the rendering context returns SUCCESS, when w/h is negative, it should return INVALID_GEOMETRY. It's an interesting case - is zero invalid or should it just prevent the rendering? And, when you add zero width/height to a path, should the geometry be added or rejected?

I don't really know what is the right behavior to be honest.

One can do strokeRect(x, y, blMax(w, 0.000001), blMax(h, 0.000001)) to fix the zero width/height issue - this way the rectangle should have all 4 points and the stroker should understand what to do.

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

No branches or pull requests

3 participants