Ok, I got things to work with the messy solution I was trying to avoid. In the interest of the public good I'm posting below the core function which calculates the width in scene coordinates for each segment in a polyline so that the entire line displays with a constant user-defined width when the horizontal and vertical scaling factors (m11() and m22()) are different. The polyline object itself is derived from QGraphicsPathItem so this function calculates the QPainterPath that is used by the base class for drawing.
If there's a better way to do this I'd dearly like to know, but I'm starting to think that the reason Qt doesn't implement scale-invariant pen widths > 1 is because of the performance issues obvious below:
// If the horizontal and vertical scales of the graphics view are different the display width of a line segment will
// depend on the angle the segment makes with the horizontal axis. If we think of the two scaling factors
// as the major and minor axes of an ellipse the width scale will be the radius of the ellipse at an angle 90 degrees
// from the angle of line segment. The equation for an ellipse in polar coordinates centered at the origin is
// r = a b / sqrt(b^2 cos^2(theta) + a^2 sin^2(theta)) where a and b are the semi-major and semi-minor axes and theta
// is (in our case) the angle perpendicular to the line segment. We can get the angle of the line segment (alpha) using
// trigonometry, and since we really want the squares of the sine and cosine it's even easier:
// sin^2(alpha) = deltax^2/(deltax^2+deltay^2) and cos^2(alpha) = deltay^2/(deltax^2+deltay^2).
// (deltax and deltay are the x and y displacements of the segment)
// Since alpha and theta are perpendicular we can replace sin^2(theta) in the
// ellipse equation with cos^2(alpha) and cos^2(theta) with sin^2(alpha). This gives us the expression
// r = width scale = xScale yScale / sqrt(yScale^2 sin^2(alpha) + xScale^2 cos^2(alpha))
void MGraphicsLine::recalculateSegmentWidths(const QTransform &t)
{
double xScale = 1.0/t.m11();
double yScale = fabs(1.0/t.m22());
prepareGeometryChange(); // the bounding rect will change as a result of recalculating the widths
// recalculate the path for the polyline using the new widths
mShapePath = QPainterPath();
for (int i=0; i<mLineSegments.size(); i++)
{
QPointF p1 = mLineSegments.at(i).first;
QPointF p2 = mLineSegments.at(i).second;
double widthScale;
if (xScale == yScale)
widthScale = xScale;
else
{
double cosSq = 0, sinSq = 0;
calculateSquaredSegmentAngles(p1, p2, cosSq, sinSq);
widthScale = getEllipseRadius(xScale, yScale, sinSq, cosSq);
}
double width = mLineWidth*widthScale; // mLineWidth is the desired display width
QPainterPath segPath;
segPath.moveTo(p1);
segPath.lineTo(p2);
QPainterPathStroker stroker;
stroker.setWidth(width);
stroker.setDashPattern(mLineStyle);
stroker.setCapStyle(Qt::FlatCap); // can't use a cap, to avoid issues with different scales
QPainterPath strokePath = stroker.createStroke(segPath);
mShapePath.addPath(strokePath);
}
setPath(mShapePath);
}
static bool calculateSquaredSegmentAngles(const QPointF &p1, const QPointF &p2, double &cosSq, double &sinSq)
{
cosSq = 0;
sinSq = 0;
double deltaX = p1.x()-p2.x();
double deltaY = p1.y()-p2.y();
double hypotSquared = deltaX*deltaX+deltaY*deltaY;
if (hypotSquared)
{
cosSq = deltaX*deltaX/hypotSquared;
sinSq = deltaY*deltaY/hypotSquared;
return true;
}
return false;
}
static double getEllipseRadius(double a, double b, double squaredCos, double squaredSin)
{
double widthScale;
if (a == b)
return a;
if (squaredCos == 0)
return b;
else if (squaredSin == 0)
return a;
return a*b/sqrt(a*a*squaredSin + b*b*squaredCos);
}
Bookmarks