PDA

View Full Version : Problems with QPainterPath::arcTo()



brcain
3rd November 2014, 22:58
I’m hoping someone might shed some light as to why I’m having a problem with QPainterPath::arcTo() used to draw the inner pie boundary in the image below.
The inner and outer arcs have the same start angle and sweep length, but they don't render that way. Why?
10722

The pie shapes are drawn with:
QPainterPath::arcTo(const QRectF & rectangle, qreal startAngle, qreal sweepLength)

The method is giving unexpected results with the startAngle and sweepLength.

Looking at the image, I expect the inner arc to begin (and end) coincident with the outer arc, but it doesn’t. The center, startAngle, and sweepLength are the same for both.

Note, I computed the desired arc start/end point (shown in the red circle as a yellow square).


void GraphicsPieFramed::createPaths()
{
// Clear the paths.
mOutlinePath = QPainterPath();
mFacePath = QPainterPath();

mMaxelSize = SCENE_SCALE*maxelDiameter(printHeadDiameter(), mVoltage);

double span = -normAngle360(mAngleEnd-mAngleBegin);

mOutlinePath.addRect(-0.5*mSize.width(), -0.5*mSize.height(), mSize.width(), mSize.height());

// outer pie
QPainterPath outerPie;
outerPie.moveTo(QPointF(0.0, 0.0));
outerPie.arcTo(QRectF(-0.5*mSize.width(), -0.5*mSize.height(), mSize.width(), mSize.height()),
-mAngleBegin, span);
outerPie.closeSubpath();
mFacePath.addPath(outerPie);

// inner pie
QPainterPath innerPie;
innerPie.moveTo(QPointF(0.0, 0.0));
innerPie.arcTo(QRectF(-0.5*mSize.width()+mFrameWidth, -0.5*mSize.height()+mFrameHeight,
mSize.width()-2.0*mFrameWidth, mSize.height()-2.0*mFrameHeight),
-mAngleBegin, span);
innerPie.closeSubpath();
mFacePath.addPath(innerPie);

if(mFrameEnabled && mFrameWidth < 0.5*mSize.width() && mFrameHeight < 0.5*mSize.height())
{
mFacePath.addPath(innerPie);
}

mFacePath.addEllipse(QPointF(0.0, 0.0), 3.0, 3.0); // Center location

createMaxelPaths();
createResizeHandlePaths();
createRotateHandlePath();
}



void GraphicsPieFramed::createResizeHandlePaths()
{
// Clear the paths.
mResizeLeftPath = QPainterPath();
mResizeRightPath = QPainterPath();
mResizeBottomPath = QPainterPath();
mResizeTopPath = QPainterPath();
mResizeBottomLeftPath = QPainterPath();
mResizeBottomRightPath = QPainterPath();
mResizeTopLeftPath = QPainterPath();
mResizeTopRightPath = QPainterPath();
mResizeAngleBeginPath = QPainterPath();
mResizeAngleEndPath = QPainterPath();
mResizeFrameWidthPath = QPainterPath();
mResizeFrameHeightPath = QPainterPath();

double const sizeHandleHalf(0.5*SIZE_HANDLE);
double const sizeHandleHalfDiag(sizeHandleHalf*sqrt(2.0));
double const left(-0.5*mSize.width());
double const right(0.5*mSize.width());
double const bottom(-0.5*mSize.height());
double const top(0.5*mSize.height());

// left
mResizeLeftPath.addRect(left-sizeHandleHalf, -sizeHandleHalf, SIZE_HANDLE, SIZE_HANDLE);
// right
mResizeRightPath.addRect(right-sizeHandleHalf, -sizeHandleHalf, SIZE_HANDLE, SIZE_HANDLE);
// bottom
mResizeBottomPath.addRect(-sizeHandleHalf, bottom-sizeHandleHalf, SIZE_HANDLE, SIZE_HANDLE);
// top
mResizeTopPath.addRect(-sizeHandleHalf, top-sizeHandleHalf, SIZE_HANDLE, SIZE_HANDLE);

// bottom-left
mResizeBottomLeftPath.addRect(left-sizeHandleHalf, bottom-sizeHandleHalf, SIZE_HANDLE, SIZE_HANDLE);
// bottom-right
mResizeBottomRightPath.addRect(right-sizeHandleHalf, bottom-sizeHandleHalf, SIZE_HANDLE, SIZE_HANDLE);
// top-left
mResizeTopLeftPath.addRect(left-sizeHandleHalf, top-sizeHandleHalf, SIZE_HANDLE, SIZE_HANDLE);
// top-right
mResizeTopRightPath.addRect(right-sizeHandleHalf, top-sizeHandleHalf, SIZE_HANDLE, SIZE_HANDLE);

// begin angle
double xBegin = 0.5*mSize.width()*cos(DEG2RAD*mAngleBegin);
double yBegin = 0.5*mSize.height()*sin(DEG2RAD*mAngleBegin);
mResizeAngleBeginPath.addEllipse(xBegin-sizeHandleHalfDiag, yBegin-sizeHandleHalfDiag,
2.0*sizeHandleHalfDiag, 2.0*sizeHandleHalfDiag);
// begin angle inner frame
double lenBegin = sqrt(xBegin*xBegin + yBegin*yBegin);
double xBeginPart = mFrameWidth*cos(DEG2RAD*mAngleBegin);
double yBeginPart = mFrameHeight*sin(DEG2RAD*mAngleBegin);
double lenBeginPart = sqrt(xBeginPart*xBeginPart + yBeginPart*yBeginPart);
double x2Begin = interp(xBegin, 0.0, lenBeginPart/lenBegin);
double y2Begin = interp(yBegin, 0.0, lenBeginPart/lenBegin);
mResizeAngleBeginPath.addRect(x2Begin-sizeHandleHalf, y2Begin-sizeHandleHalf, 2.0*sizeHandleHalf, 2.0*sizeHandleHalf);

// end angle
double xEnd = 0.5*mSize.width()*cos(DEG2RAD*mAngleEnd);
double yEnd = 0.5*mSize.height()*sin(DEG2RAD*mAngleEnd);
mResizeAngleEndPath.addEllipse(xEnd-sizeHandleHalfDiag, yEnd-sizeHandleHalfDiag,
2.0*sizeHandleHalfDiag, 2.0*sizeHandleHalfDiag);
// end angle inner frame
double lenEnd = sqrt(xEnd*xEnd + yEnd*yEnd);
double xEndPart = mFrameWidth*cos(DEG2RAD*mAngleEnd);
double yEndPart = mFrameHeight*sin(DEG2RAD*mAngleEnd);
double lenEndPart = sqrt(xEndPart*xEndPart + yEndPart*yEndPart);
double x2End = interp(xEnd, 0.0, lenEndPart/lenEnd);
double y2End = interp(yEnd, 0.0, lenEndPart/lenEnd);
mResizeAngleEndPath.addRect(x2End-sizeHandleHalf, y2End-sizeHandleHalf, 2.0*sizeHandleHalf, 2.0*sizeHandleHalf);

// frame width
mResizeFrameWidthPath.addEllipse(left-sizeHandleHalfDiag+mFrameWidth, top-sizeHandleHalfDiag,
2.0*sizeHandleHalfDiag, 2.0*sizeHandleHalfDiag);

// frame height
mResizeFrameHeightPath.addEllipse(left-sizeHandleHalfDiag, top-sizeHandleHalfDiag-mFrameHeight,
2.0*sizeHandleHalfDiag, 2.0*sizeHandleHalfDiag);
}


Thanks!

wysota
4th November 2014, 08:48
I would guess the rectangle you pass to the inner arcTo() call is incorrect.

brcain
4th November 2014, 15:31
Shouldn't a smaller (same center) rectangle and same start/sweep angle just imply a smaller arc radii? The start angle should still be the same (and coincident), right?

Thanks for the suggestion. I will look.

wysota
4th November 2014, 16:02
The rectangle serves as the bounding box for the arc thus it matters where the sides of the box are and not where the centre is. The arc will begin at one side of the rectangle and end at the other side. You might want to add drawing the box to your code to see how the arcs are drawn related to their corresponding boxes.

brcain
4th November 2014, 18:55
I drew the outer and inner boxes in green.
From what I've read ...
- The rectangle is used to define the maximum extents (and shape) of the arc.
- The start/end angle depend upon the arcTo() specified start angle and sweep angle; the arc is not required to start at one side of the rectangle and end at the other side.
Restated: The rectangle defines the ellipse; the start/sweep angle define the arc of the ellipse.

From Qt Assistant:

Creates an arc that occupies the given rectangle, beginning at the specified startAngle and extending sweepLength degrees counter-clockwise.
Am I missing something?

The start angle in the image below is off. The specified angle is the same as the outer pie shape.
10724

I drew the outer and inner boxes in green.
From what I've read ...
- The rectangle is used to define the maximum extents (and shape) of the arc.
- The start/end angle depend upon the arcTo() specified start angle and sweep angle; the arc is not required to start at one side of the rectangle and end at the other side.
Restated: The rectangle defines the ellipse; the start/sweep angle define the arc of the ellipse.

From Qt Assistant:

Creates an arc that occupies the given rectangle, beginning at the specified startAngle and extending sweepLength degrees counter-clockwise.
Am I missing something?

The start angle in the image below is off. The specified angle is the same as the outer pie shape.
10724

Added after 35 minutes:

Here is a sequence of images that emphasize how the rectangle defines the ellipse (note inner/outer green rectangle) and the start/sweep angle define the arc of the ellipse.
10727

wysota
4th November 2014, 19:14
Is there a question here somewhere? You can clearly see on your images that the rectangle is not defined correctly. Your outer rectangle is a square(-ish), your inner one is not. Thus your outer arc lies on a circle and your inner does not.

brcain
4th November 2014, 19:50
The inner rectangle is not meant to be square-ish. Why did you infer that?
My question regards the inner ellipse. An ellipse (by definition) is not restricted to being square-ish.

My question: Does the arcTo() function work in such a way that the rectangle defines an ellipse and the start/sweep angle define an arc of the ellipse?

The previous sequences of images supports the rectangle assertion, but the begin angle seems to be off.

The images represent a pie shape that is allowed to have inner and outer ellipse fill boundaries.

Here is an example of the outer boundary not being square-ish.
10728
You can see the fill algorithm is working perfectly. However, the arc start/sweep angles are not consistent -- despite the fact that the values specified for the start/sweep angles for the inner and outer ellipses are precisely the same. I even hand calculated the end and start angles which are shown on the inner ellipse as yellow squares. It appears there is a bug in the function ... or at a minimum, the documentation. However, I am willing to admit I may be missing something.

wysota
4th November 2014, 22:20
The inner rectangle is not meant to be square-ish. Why did you infer that?

That was my impression from your first post. But apparently I was wrong.


My question: Does the arcTo() function work in such a way that the rectangle defines an ellipse and the start/sweep angle define an arc of the ellipse?

QPainterPath::arcTo() docs show a visual hint how the arc is interpreted with regard to the rectangle. The rectangle defines an ellipse the arc sweeps over starting from startAngle and ending at startAngle+sweepLength. Angles are specified in degrees.

Here is a simple testbed:


#include <QtWidgets>

class Widget : public QWidget {
Q_OBJECT
Q_PROPERTY(int startAngle READ startAngle WRITE setStartAngle NOTIFY startAngleChanged)
Q_PROPERTY(int sweepLength READ sweepLength WRITE setSweepLength NOTIFY sweepLengthChanged)

public:
Widget(QWidget *parent = 0) : QWidget(parent) {
m_startAngle = 0;
m_sweepLength = 0;
connect(this, SIGNAL(startAngleChanged(int)), SLOT(update()));
connect(this, SIGNAL(sweepLengthChanged(int)), SLOT(update()));
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
}

int startAngle() const
{
return m_startAngle;
}
int sweepLength() const
{
return m_sweepLength;
}

public slots:
void setStartAngle(int arg)
{
if (m_startAngle == arg)
return;

m_startAngle = arg;
emit startAngleChanged(arg);
}
void setSweepLength(int arg)
{
if (m_sweepLength == arg)
return;

m_sweepLength = arg;
emit sweepLengthChanged(arg);
}

protected:
void paintEvent(QPaintEvent *pe) {
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
QLinearGradient myGradient(0, 0, width(), height());
myGradient.setColorAt(0, Qt::yellow);
myGradient.setColorAt(1, QColor("orange"));

QRect rectangle = rect().adjusted(10,10,-10,-10);

QPen myPen;
myPen.setWidth(1);
myPen.setColor(palette().color(QPalette::Dark));

QPointF center = rectangle.center();


QPainterPath myPath;
myPath.moveTo(center);
myPath.arcTo(rectangle, m_startAngle,
m_sweepLength);
myPath.closeSubpath();

QPen dashPen;
dashPen.setColor(Qt::red);
dashPen.setStyle(Qt::DashLine);
painter.setPen(dashPen);
painter.setBrush(Qt::NoBrush);
painter.drawRect(rectangle);

painter.setBrush(myGradient);
painter.setPen(myPen);
painter.drawPath(myPath);
}

signals:
void startAngleChanged(int arg);
void sweepLengthChanged(int arg);
private:
int m_startAngle;
int m_sweepLength;
};

#include "main.moc"

int main(int argc, char **argv) {
QApplication app(argc, argv);

QWidget w;
QVBoxLayout *l = new QVBoxLayout(&w);
Widget *arcWidget = new Widget;
l->addWidget(arcWidget);
QSlider *startSlider = new QSlider(Qt::Horizontal);
QSlider *sweepSlider = new QSlider(Qt::Horizontal);
startSlider->setRange(0, 360);
sweepSlider->setRange(0, 360);
l->addWidget(startSlider);
l->addWidget(sweepSlider);
QObject::connect(startSlider, SIGNAL(valueChanged(int)), arcWidget, SLOT(setStartAngle(int)));
QObject::connect(sweepSlider, SIGNAL(valueChanged(int)), arcWidget, SLOT(setSweepLength(int)));
sweepSlider->setValue(90);
w.show();
w.resize(800, 600);

return app.exec();
}

brcain
4th November 2014, 23:10
The example you wrote is producing the same effect.
Note resizing the rectangle (keeping start/sweep angles the same) results in a different measurable start angle.
The measurable start angle goes from around 15 degrees to around 40 degrees.
10731
I'm still somewhat puzzled what the start angle actually means with arcTo(); it appears to be ether the arc length or dependent upon the rectangle's aspect ratio.
Perhaps I need to adjust my start angle based upon this aspect ratio.
I'll run a desktop protractor app to measure the actual start/sweep angles to test my hypothesis.

It appears the starting arc length (relative to x-axis) remains constant. Computing the actual arc length of an ellipse requires elliptic integrals. Nonetheless, I would have thought the starting arc length would vary, and the start angle would remain the same.