Bug 411345 - Scaled SWTGraphics doesn't draw rectangles well.
Summary: Scaled SWTGraphics doesn't draw rectangles well.
Status: ASSIGNED
Alias: None
Product: GEF
Classification: Tools
Component: GEF-Legacy Draw2d (show other bugs)
Version: 3.7.1   Edit
Hardware: PC Windows 7
: P3 normal (vote)
Target Milestone: ---   Edit
Assignee: Alexander Nyßen CLA
QA Contact:
URL:
Whiteboard:
Keywords:
Depends on:
Blocks:
 
Reported: 2013-06-21 05:19 EDT by Jan Krakora CLA
Modified: 2013-12-16 09:21 EST (History)
1 user (show)

See Also:


Attachments
Runnable example (3.45 KB, application/octet-stream)
2013-06-21 05:19 EDT, Jan Krakora CLA
no flags Details
Runnable example (modified) (3.83 KB, application/octet-stream)
2013-10-16 02:40 EDT, Alexander Nyßen CLA
no flags Details

Note You need to log in before you can comment on or make changes to this bug.
Description Jan Krakora CLA 2013-06-21 05:19:31 EDT
Created attachment 232633 [details]
Runnable example

Scaling in GEF 3.x (draw2d) is very frustrating. In addition to imprecise drawing due to the integer coordinates, 
it also draws weird scaled fonts and ugly rectangles. 

So I have decided to use the SWTGraphics instead of the emulated ScaledGraphics. 
This resolved font drawing but it has its own pitfalls. You can run the attached file with simple example to see it.

When using pure GC, scaling is nice and clean. The rectangle is always drawn with line
width 1 and mainly its all edges are always drawn.

On the other hand, with Draw2d's SWTGraphics, the rectangle is sometimes cropped?!, the line width jumps from 
1 to 2 (try to scale to with mouse wheel to something above 2).

What is wrong here??
Comment 1 Alexander Nyßen CLA 2013-10-14 16:01:54 EDT
I think the reason probably is that the computation of the client area within ScalableFreeformLayeredPane is based on a normal rectangle (this is constructed within Figure#getClientArea() instead of a PrecisionRectangle. If remove the final modifier from Figure#getClientArea() and overwrite it as follows within ScalableFreeformLayeredPane, also adjusting the getClientArea(Rectangle), it works for me. Can you please check and confirm?

/**
 * @see org.eclipse.draw2d.Figure#getClientArea()
 */
public Rectangle getClientArea(Rectangle rect) {
  super.getClientArea(rect);
  if (rect instanceof PrecisionRectangle) {
    // ensure that in case a precision rectangle is passed in, bounds
    // are set precise
    PrecisionRectangle r = ((PrecisionRectangle) rect);
    r.setPreciseBounds(rect.x / scale, rect.y / scale, rect.width / scale, rect.height / scale);
  } else {
    // if the passed in rect is no precision rectangle, we have to live
    // with lost precision
    rect.width /= scale;
    rect.height /= scale;
    rect.x /= scale;
    rect.y /= scale;
  }
  return rect;
}
	
public final Rectangle getClientArea() {
  return super.getClientArea(new PrecisionRectangle());
}
Comment 2 Jan Krakora CLA 2013-10-15 14:56:50 EDT
Unfortunately I can't confirm your investigation Alexander. 

Maybe I'm exhausted from the all day programing work but I just can't reproduce it. When I remove the final modifier from Figure#getClientArea() and overwrite the ScalableFreeformLayeredPane as follows:

/**
 * @see org.eclipse.draw2d.Figure#getClientArea()
 */
public Rectangle getClientArea(Rectangle rect) {
  super.getClientArea(rect);
  if (rect instanceof PrecisionRectangle) {
    // ensure that in case a precision rectangle is passed in, bounds
    // are set precise
    PrecisionRectangle r = ((PrecisionRectangle) rect);
    r.setPreciseBounds(rect.x / scale, rect.y / scale, rect.width / scale, rect.height / scale);
  } else {
    // if the passed in rect is no precision rectangle, we have to live
    // with lost precision
    rect.width /= scale;
    rect.height /= scale;
    rect.x /= scale;
    rect.y /= scale;
  }
  return rect;
}
	
public final Rectangle getClientArea() {
  return super.getClientArea(new PrecisionRectangle());
}

The drawing errors (cropped rectangle) are still there. 

Note that using debugging I realize that the condition 

if (rect instanceof PrecisionRectangle) {
...

is never met.

Is it possible you change any part of the code from the runnable example attachment?
Are you using GEF 3.7.1? I don't see any error in my code :-/
Comment 3 Alexander Nyßen CLA 2013-10-15 15:06:48 EDT
You are completely right. Probably it was me that was to exhausted. I will have to take another look...
Comment 4 Alexander Nyßen CLA 2013-10-16 02:40:39 EDT
Created attachment 236514 [details]
Runnable example (modified)

Ok, it seems I have at least tracked it down to be related to clipping of children. If I overwrite paintChildren() in both affected container figures of your example (see attachment) to not clip at all, it works fine for me (could you please confirm?). 

I assume the root cause thus is that clipping calculations are to imprecise in this scenario. I will dig deeper into it...
Comment 5 Jan Krakora CLA 2013-10-16 10:55:28 EDT
Yep, I can confirm that.
Comment 6 Alexander Nyßen CLA 2013-10-17 17:31:33 EDT
Ok, it seems to be a combined problem between clipping a child figure exactly at its bounds (done by default within paintChildren as long as no clipping strategy is specified) and the optimizations concerning line width within RectangleFigure#outlineShape, which - as far as I understand the code - may result in the fact that a figure paints over the left and top borders of its bounds. 

I think we will have to ensure that this does not happen, while taking care that the outline behavior remains consistent with the fill behavior (so we do not run into bug #263290 again).
Comment 7 Alexander Nyßen CLA 2013-10-21 16:19:52 EDT
Having taken a closer look at the optimizations within RectangleFigure#outlineShape() (the same holds for Ellipse, which has identical optimizations and which leads to identical problems) and having also taken a deeper look at the GC paint and fill operations, at least if being run against the Cocoa port of SWT, the code within RectangleFigure should probably be changed to the following:

	protected Rectangle getPaintBounds() {
		float lineWidth = Math.max(1.0f, getLineWidthFloat());
		int leftTopInset = (int) Math.ceil(lineWidth / 2.0f);
		Rectangle r = getBounds().getCopy();
		r.x += leftTopInset;
		r.y += leftTopInset;
		r.width -= leftTopInset + (int) Math.ceil(lineWidth);
		r.height -= leftTopInset + (int) Math.ceil(lineWidth);
		return r;
	}

	/**
	 * @see Shape#fillShape(Graphics)
	 */
	protected void fillShape(Graphics graphics) {
		graphics.fillRectangle(getPaintBounds());
	}

	/**
	 * @see Shape#outlineShape(Graphics)
	 */
	protected void outlineShape(Graphics graphics) {
		graphics.drawRectangle(getPaintBounds());
	}

The reason is, that at least on the SWT Cocoa port, a GC#drawRectangle or GC#drawOval operation will lead to a rectangle that is painted with an overlap of half the line width to the top and left side of the passed in rectangle coordinates and of the full line width to the bottom and right side.

Before applying any changes, I want to make sure, SWT behaves this way on the other ports as well. If this is the case, we can pull the getPaintBounds() method up to Shape and reuse it in Ellipse, so both shapes behave similarly. 

Could you please check whether this holds for Windows 7 as well?
Comment 8 Jan Krakora CLA 2013-10-26 05:18:45 EDT
Great job Alexander. I can confirm this behavior on Windows 7.
Comment 9 Alexander Nyßen CLA 2013-10-28 17:40:06 EDT
(In reply to Jan Krakora from comment #8)
> Great job Alexander. I can confirm this behavior on Windows 7.

Hmm, using the shapes example it seems we are indeed running into bug #263290 again (the selection rectangle in the shapes example does not fully enclose the shape). It seems we are not yet there...
Comment 10 Alexander Nyßen CLA 2013-10-28 17:41:16 EDT
(In reply to Alexander Nyssen from comment #9)
> (In reply to Jan Krakora from comment #8)
> > Great job Alexander. I can confirm this behavior on Windows 7.
> 
> Hmm, using the shapes example it seems we are indeed running into bug
> #263290 again (the selection rectangle in the shapes example does not fully
> enclose the shape). It seems we are not yet there...

... well, not exactly #263290, but very close to the problem reported there...
Comment 11 Alexander Nyßen CLA 2013-12-16 09:21:56 EST
Hmm, I digged deeper into this and things don't get better... 

The optimization I proposed within comment #7 ensures that the shape always paints safely inside its (integer-based) bounds rectangle. Thus, no clipping problems will arise, even if the shape is being scaled. 

However, as mentioned in comment #10, this optimization will lead to problems with the MoveHandle resp. MoveHandleLocator, as it currently does not compensate the line width optimization. 

In the end I have the feeling that this cannot be solved properly as far as figure bounds are based on integers and such kind of optimizations are performed. It will always lead to problems within other parts of the framework, and the impact seems to be quite risky, as I assume a lot of users have already worked around such kind of cases. I will thus not incorporate any of these changes into the 3.10 code base (unless we find a fix that comes with low risk of side-effect).

IMHO, the best thing would probably be to enhance our shapes example with zoom-support (so this issue becomes reproducible there), and then apply proper workarounds only within this respective example, so we have them documented.