Bounds and Accuracy

Screen Shot 2013-03-26 at 7.46.14 PM

Update: Ryan Petrich to the rescue with a much simpler solution: He writes “If you want the precise bounds, use CGPathGetPathBoundingBox (path.CGPath)”. Thanks Ryan!

 

 

So it turns out that the UIBezierPath bounds command isn’t particularly accurate, which is a problem when using custom paths with Auto Layout. You’ll want to provide accurate content insets to the constraints system.

The image at the top of this post represents both the Apple-provided bounds (the outer rectangle) and the true bounds (the inner rectangle). I visualized the control points for my path (blue circles) to confirm that the bounds were being calculated by taking the union of all points — destination and control points. Unfortunately, that’s not a very good strategy for calculating bounds.

Instead, you really need to calculate the edges of the curves, as they will almost never extend as far as the control points for most drawing.

Here’s the solution I’ve been working with, probably in far more detail than needed.

// Workarounds
void updateMinMax(CGPoint point, CGFloat *minX, CGFloat *maxX, CGFloat *minY, CGFloat *maxY)
{
    if (!minX) return;
    if (!maxX) return;
    if (!minY) return;
    if (!maxY) return;

    if (point.x < *minX)
        *minX = point.x;
    if (point.y < *minY)
         *minY = point.y;
    if (point.x > *maxX)
        *maxX = point.x;
    if (point.y > *maxY)
        *maxY = point.y;
}

// Proper bounds
- (CGRect) calculatedBounds
{
    CGFloat minX = FLT_MAX;
    CGFloat minY = FLT_MAX;
    CGFloat maxX = -(FLT_MAX / 2);
    CGFloat maxY = -(FLT_MAX / 2);

    BezierElement *current = nil;

    for (BezierElement *element in self.elements)
    {
        switch (element.elementType)
        {
            case kCGPathElementMoveToPoint:
            {
                current = element;
                updateMinMax(current.point, &minX, &maxX, &minY, &maxY);
                break;
            }
            case kCGPathElementAddLineToPoint:
            {
                current = element;
                updateMinMax(current.point, &minX, &maxX, &minY, &maxY);
                break;
            }
            case kCGPathElementCloseSubpath:
            {
                break;
            }
            case kCGPathElementAddCurveToPoint:
            {
                for (int i = 0; i <= NUMBER_OF_BEZIER_SAMPLES; i++)
                {
                    CGPoint p = CubicBezierPoint((CGFloat) i / (CGFloat) NUMBER_OF_BEZIER_SAMPLES, current.point, element.controlPoint1, element.controlPoint2, element.point);
                    updateMinMax(p, &minX, &maxX, &minY, &maxY);
                }
                current = element;
                break;
            }
            case kCGPathElementAddQuadCurveToPoint:
            {
                for (int i = 0; i <= NUMBER_OF_BEZIER_SAMPLES; i++)
                {
                    CGPoint p = QuadBezierPoint((CGFloat) i / (CGFloat) NUMBER_OF_BEZIER_SAMPLES, current.point, element.controlPoint1, element.point);
                    updateMinMax(p, &minX, &maxX, &minY, &maxY);
                }
                current = element;
                break;
            }
        }
    }

    // This does not take line width into account
    CGRect baseRect = CGRectMake(minX, minY, (maxX - minX), (maxY - minY));
    return baseRect;
}

- (CGRect) calculatedBoundsWithLineWidth;
{
    return CGRectInset(self.calculatedBounds, -self.lineWidth / 2.0f, -self.lineWidth / 2.0f);
}

Comments are closed.